项目学生:使用AOP简化代码
這是Project Student的一部分。
許多人堅信方法應適合您的編輯器窗口(例如20行),而有些人則認為方法應小于此范圍。 這個想法是一種方法應該做一件事,而只能做一件事。 如果它做的還不止于此,則應將其分解為多種方法,而舊方法的“一件事”就是協調新方法。
這并不意味著在任意數量的行之后拆分一種方法。 有時方法自然會更大。 仍然是一個很好的問題。
那么,如何識別不只一件事的代碼? 一個好的試金石是代碼是否在多種方法中重復。 典型的例子是持久性類中的事務管理。 每個持久性類都需要它,并且代碼始終看起來相同。
另一個示例是Resource類中未處理的異常處理程序。 每個面向REST的方法都需要處理此問題,并且代碼始終看起來相同。
那是理論。 在實踐中,代碼可能很丑陋并且收益不大。 幸運的是,有一個解決方案:面向方面的編程(AOP)。 這使我們可以在方法調用之前或之后透明地編織代碼。 這通常使我們可以大大簡化我們的方法。
設計決策
AspectJ –我正在通過Spring注入來使用AspectJ。
局限性
使用CRUD方法,AspectJ切入點表達式相對簡單。 當添加了更復雜的功能時,情況可能并非如此。
資源方法中未處理的異常
我們首先關心的是資源方法中未處理的異常。 不管怎樣,Jersey都會返回SERVER INTERNAL ERROR(服務器內部錯誤)(500)消息,但是它可能包含堆棧跟蹤信息和我們不希望攻擊者知道的其他內容。 如果我們自己發送它,我們可以控制它包含的內容。 我們可以在所有方法中添加一個“ catch”塊,但可以將其復制到AOP方法中。 這將使我們所有的Resource方法更加苗條和易于閱讀。
此類還檢查“找不到對象”異常。 在單個Resource類中將很容易處理,但會使代碼混亂。 將異常處理程序放在此處可使我們的方法專注于快樂路徑并保證響應的一致性。
該類有兩個優化。 首先,它顯式檢查UnitTestException并在這種情況下跳過詳細的日志記錄。 我最大的煩惱之一是測試,當一切都按預期方式運行時,將堆棧跟蹤信息充斥日志。 這使得不可能針對明顯的問題瀏覽日志。 單個更改可以使問題更容易發現。
其次,它使用與目標類(例如CourseResource)關聯的記錄器,而不是與AOP類關聯的記錄器。 除了更清晰之外,這還使我們可以有選擇地更改單個Resource(而不是全部)的日志記錄級別。
另一個技巧是在處理程序中調用ExceptionService 。 該服務可以對異常做一些有用的事情,例如,它可以創建或更新Jira票證。 這還沒有實現,所以我只是發表評論以說明它的去向。
@Aspect @Component public class UnexpectedResourceExceptionHandler {@Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource)")public Object checkForUnhandledException(ProceedingJoinPoint pjp) throws Throwable {Object results = null;Logger log = Logger.getLogger(pjp.getSignature().getClass());try {results = pjp.proceed(pjp.getArgs());} catch (ObjectNotFoundException e) {// this is safe to log since we know that we've passed filtering.String args = Arrays.toString(pjp.getArgs());results = Response.status(Status.NOT_FOUND).entity("object not found: " + args).build();if (log.isDebugEnabled()) {log.debug("object not found: " + args);}} catch (Exception e) {// find the method we called. We can't cache this since the method// may be overloadedMethod method = findMethod(pjp); if ((method != null) && Response.class.isAssignableFrom(method.getReturnType())) {// if the method returns a response we can return a 500 message.if (!(e instanceof UnitTestException)) {if (log.isInfoEnabled()) {log.info(String.format("%s(): unhandled exception: %s", pjp.getSignature().getName(),e.getMessage()), e);}} else if (log.isTraceEnabled()) {log.info("unit test exception: " + e.getMessage());}results = Response.status(Status.INTERNAL_SERVER_ERROR).build();} else {// DO NOT LOG THE EXCEPTION. That just clutters the log - let// the final handler log it.throw e;}}return results;}/*** Find method called via reflection.*/Method findMethod(ProceedingJoinPoint pjp) {Class[] argtypes = new Class[pjp.getArgs().length];for (int i = 0; i < argtypes.length; i++) {argtypes[i] = pjp.getArgs()[i].getClass();}Method method = null;try {// @SuppressWarnings("unchecked")method = pjp.getSignature().getDeclaringType().getMethod(pjp.getSignature().getName(), argtypes);} catch (Exception e) {Logger.getLogger(UnexpectedResourceExceptionHandler.class).info(String.format("could not find method for %s.%s", pjp.getSignature().getDeclaringType().getName(),pjp.getSignature().getName()));}return method;} }REST發布值檢查
我們的Resource方法也有很多樣板代碼來檢查REST參數。 它們是否為非空,電子郵件地址的格式是否正確,等等。同樣,很容易將大部分代碼移入AOP方法并簡化Resource方法。
我們首先定義一個接口,該接口指示可以驗證REST傳輸對象。 第一個版本使我們可以簡單地接受或拒絕,改進的版本可以使我們有辦法告訴客戶具體問題是什么。
public interface Validatable {boolean validate(); }現在,我們擴展了先前的REST傳輸對象,以添加一種驗證方法。
兩個筆記。 首先,名稱和電子郵件地址接受Unicode字母,而不僅僅是標準ASCII字母。 隨著我們的世界國際化,這一點很重要。
其次,我添加了一個toString()方法,但是由于它使用了未經處理的值,因此這是不安全的。 我將在稍后處理消毒。
@XmlRootElement public class NameAndEmailAddressRTO implements Validatable {// names must be alphabetic, an apostrophe, a dash or a space. (Anne-Marie,// O'Brien). This pattern should accept non-Latin characters.// digits and colon are added to aid testing. Unlikely but possible in real// names.private static final Pattern NAME_PATTERN = Pattern.compile("^[\\p{L}\\p{Digit}' :-]+$");// email address must be well-formed. This pattern should accept non-Latin// characters.private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@([\\p{L}\\p{Digit}-]+\\.)?[\\p{L}]+");private String name;private String emailAddress;private String testUuid;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getEmailAddress() {return emailAddress;}public void setEmailAddress(String emailAddress) {this.emailAddress = emailAddress;}public String getTestUuid() {return testUuid;}public void setTestUuid(String testUuid) {this.testUuid = testUuid;}/*** Validate values.*/@Overridepublic boolean validate() {if ((name == null) || !NAME_PATTERN.matcher(name).matches()) {return false;}if ((emailAddress == null) || !EMAIL_PATTERN.matcher(emailAddress).matches()) {return false;}if ((testUuid != null) && !StudentUtil.isPossibleUuid(testUuid)) {return false;}return true;}@Overridepublic String toString() {// FIXME: this is unsafe!return String.format("NameAndEmailAddress('%s', '%s', %s)", name, emailAddress, testUuid);} }我們對其他REST傳輸對象進行了類似的更改。
現在,我們可以編寫AOP方法來檢查CRUD操作的參數。 和以前一樣,使用與資源關聯的記錄器而不是AOP類來寫入日志。
這些方法還記錄Resource方法的條目。 同樣,它是樣板,在此進行簡化了Resource方法。 記錄該方法的退出和運行時間也很簡單,但是在這種情況下,我們應該使用一個股票記錄器AOP類。
@Aspect @Component public class CheckPostValues {/*** Check post values on create method.* * @param pjp* @return* @throws Throwable*/@Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && args(validatable,..)")public Object checkParametersCreate(ProceedingJoinPoint pjp, Validatable rto) throws Throwable {final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());final String name = pjp.getSignature().getName();Object results = null;if (rto.validate()) {// this should be safe since parameters have been validated.if (log.isDebugEnabled()) {log.debug(String.format("%s(%s): entry", name, Arrays.toString(pjp.getArgs())));}results = pjp.proceed(pjp.getArgs());} else {// FIXME: this is unsafeif (log.isInfoEnabled()) {log.info(String.format("%s(%s): bad arguments", name, Arrays.toString(pjp.getArgs())));}// TODO: tell caller what the problems wereresults = Response.status(Status.BAD_REQUEST).build();}return results;}/*** Check post values on update method.* * @param pjp* @return* @throws Throwable*/@Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && args(uuid,validatable,..)")public Object checkParametersUpdate(ProceedingJoinPoint pjp, String uuid, Validatable rto) throws Throwable {final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());final String name = pjp.getSignature().getName();Object results = null;if (!StudentUtil.isPossibleUuid(uuid)) {// this is a possible attack.if (log.isInfoEnabled()) {log.info(String.format("%s(): uuid", name));}results = Response.status(Status.BAD_REQUEST).build();} else if (rto.validate()) {// this should be safe since parameters have been validated.if (log.isDebugEnabled()) {log.debug(String.format("%s(%s): entry", name, Arrays.toString(pjp.getArgs())));}results = pjp.proceed(pjp.getArgs());} else {// FIXME: this is unsafeif (log.isInfoEnabled()) {log.info(String.format("%s(%s): bad arguments", name, Arrays.toString(pjp.getArgs())));}// TODO: tell caller what the problems wereresults = Response.status(Status.BAD_REQUEST).build();}return results;}/*** Check post values on delete method. This is actually a no-op but it* allows us to log method entry.* * @param pjp* @return* @throws Throwable*/@Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && args(uuid,version) && execution(* *.delete*(..))")public Object checkParametersDelete(ProceedingJoinPoint pjp, String uuid, Integer version) throws Throwable {final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());final String name = pjp.getSignature().getName();Object results = null;if (!StudentUtil.isPossibleUuid(uuid)) {// this is a possible attack.if (log.isInfoEnabled()) {log.info(String.format("%s(): uuid", name));}results = Response.status(Status.BAD_REQUEST).build();} else {// this should be safe since parameters have been validated.if (log.isDebugEnabled()) {log.debug(String.format("%s(%s): entry", name, Arrays.toString(pjp.getArgs())));}results = pjp.proceed(pjp.getArgs());}return results;}/*** Check post values on find methods. This is actually a no-op but it allows* us to log method entry.* * @param pjp* @return* @throws Throwable*/@Around("target(com.invariantproperties.sandbox.student.webservice.server.rest.AbstractResource) && execution(* *.find*(..))")public Object checkParametersFind(ProceedingJoinPoint pjp) throws Throwable {final Logger log = Logger.getLogger(pjp.getSignature().getDeclaringType());if (log.isDebugEnabled()) {log.debug(String.format("%s(%s): entry", pjp.getSignature().getName(), Arrays.toString(pjp.getArgs())));}final Object results = pjp.proceed(pjp.getArgs());return results;} }更新了Spring配置
我們必須告訴Spring搜索AOP類。 這是對我們的配置文件的單行更改。
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-3.0.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-3.0.xsd"><aop:aspectj-autoproxy/> </beans>更新資源
現在,我們可以簡化資源類。 僅有幾種方法可以簡化為幸福道路。
@Service @Path("/course") public class CourseResource extends AbstractResource {private static final Logger LOG = Logger.getLogger(CourseResource.class);private static final Course[] EMPTY_COURSE_ARRAY = new Course[0];@Resourceprivate CourseFinderService finder;@Resourceprivate CourseManagerService manager;@Resourceprivate TestRunService testRunService;/*** Default constructor.*/public CourseResource() {}/*** Set values used in unit tests. (Required due to AOP)* * @param finder* @param manager* @param testService*/void setServices(CourseFinderService finder, CourseManagerService manager, TestRunService testRunService) {this.finder = finder;this.manager = manager;this.testRunService = testRunService;}/*** Get all Courses.* * @return*/@GET@Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })public Response findAllCourses() {final List courses = finder.findAllCourses();final List results = new ArrayList(courses.size());for (Course course : courses) {results.add(scrubCourse(course));}final Response response = Response.ok(results.toArray(EMPTY_COURSE_ARRAY)).build();return response;}/*** Create a Course.* * FIXME: what about uniqueness violations?* * @param req* @return*/@POST@Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })@Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })public Response createCourse(CourseInfo req) {final String code = req.getCode();final String name = req.getName();Response response = null;Course course = null;if (req.getTestUuid() != null) {TestRun testRun = testRunService.findTestRunByUuid(req.getTestUuid());if (testRun != null) {course = manager.createCourseForTesting(code, name, req.getSummary(), req.getDescription(),req.getCreditHours(), testRun);} else {response = Response.status(Status.BAD_REQUEST).entity("unknown test UUID").build();}} else {course = manager.createCourse(code, name, req.getSummary(), req.getDescription(), req.getCreditHours());}if (course == null) {response = Response.status(Status.INTERNAL_SERVER_ERROR).build();} else {response = Response.created(URI.create(course.getUuid())).entity(scrubCourse(course)).build();}return response;}/*** Get a specific Course.* * @param uuid* @return*/@Path("/{courseId}")@GET@Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })public Response getCourse(@PathParam("courseId") String id) {// 'object not found' handled by AOPCourse course = finder.findCourseByUuid(id);final Response response = Response.ok(scrubCourse(course)).build();return response;}/*** Update a Course.* * FIXME: what about uniqueness violations?* * @param id* @param req* @return*/@Path("/{courseId}")@POST@Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })@Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_XML })public Response updateCourse(@PathParam("courseId") String id, CourseInfo req) {final String name = req.getName();// 'object not found' handled by AOPfinal Course course = finder.findCourseByUuid(id);final Course updatedCourse = manager.updateCourse(course, name, req.getSummary(), req.getDescription(),req.getCreditHours());final Response response = Response.ok(scrubCourse(updatedCourse)).build();return response;}/*** Delete a Course.* * @param id* @return*/@Path("/{courseId}")@DELETEpublic Response deleteCourse(@PathParam("courseId") String id, @PathParam("version") Integer version) {// we don't use AOP handler since it's okay for there to be no matchtry {manager.deleteCourse(id, version);} catch (ObjectNotFoundException exception) {LOG.debug("course not found: " + id);}final Response response = Response.noContent().build();return response;} }單元測試
單元測試需要對每個測試進行更改,因為我們不能簡單地實例化被測試的對象–我們必須使用Spring,以便正確編織AOP類。 幸運的是,這實際上是唯一的更改–我們檢索資源并通過package-private方法而不是package-private構造函數設置服務。
我們還需要為服務bean創建Spring值。 配置器類負責此工作。
@Configuration @ComponentScan(basePackages = { "com.invariantproperties.sandbox.student.webservice.server.rest" }) @ImportResource({ "classpath:applicationContext-rest.xml" }) // @PropertySource("classpath:application.properties") public class TestRestApplicationContext1 {@Beanpublic CourseFinderService courseFinderService() {return null;}@Beanpublic CourseManagerService courseManagerService() {return null;}....整合測試
集成測試不需要任何更改。
源代碼
- 源代碼位于https://github.com/beargiles/project-student [github]和http://beargiles.github.io/project-student/ [github頁面]。
翻譯自: https://www.javacodegeeks.com/2014/01/project-student-simplifying-code-with-aop.html
總結
以上是生活随笔為你收集整理的项目学生:使用AOP简化代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 创建一个简单的JAX-RS Messag
- 下一篇: 传小米汽车已试生产近一个月 每周50辆样