Enterprise Java

Project Student: Simplifying Code With AOP

This is part of Project Student.

Many people strongly believe that methods should fit within your editor window (say, 20 lines), and some people believe that methods should be even smaller than that. The idea is that a method should do one thing and only one thing. If it does more than that it should be broken apart into multiple methods and the old method’s “one thing” is to coordinate the new methods.

This does not mean breaking apart a method after an arbitrary number of lines. Sometimes methods are naturally larger. It’s still always a good question to ask.

So how do you recognize code that does more than one thing? A good touchstone is if the code is duplicated in multiple methods. The canonical example is transaction management in persistence classes. Every persistence class needs it and the code always looks the same.

Another example is the unhandled exception handler in my Resource classes. Every REST-facing method needs to deal with this and the code always looks the same.

That’s the theory. In practice the code can be ugly and with modest gains. Fortunately there’s a solution: Aspect-Oriented Programming (AOP). This allows us to transparently weave code before or after our method calls. This often lets us simplify our methods dramatically.

Design Decisions

AspectJ – I’m using AspectJ via Spring injection.

Limitations

The AspectJ pointcut expressions are relatively simple with the CRUD methods. That might not be true as more complex functionality is added.

Unhandled Exceptions in Resource Methods

Our first concern is unhandled exceptions in resource methods. Jersey will return a SERVER INTERNAL ERROR (500) message anyway but it will probably include a stack trace and other content that we don’t want an attacker to know. We can control what it includes if we send it ourselves. We could add a ‘catch’ block in all of our methods but it’s boilerplate that we can move into an AOP method. That will leave all of our Resource methods a lot slimmer and easier to read.

This class also checks for “Object Not Found” exceptions. It would be easy to handle in the individual Resource classes but it muddies the code. Putting the exception handler here allows our methods to focus on the happy path and guarantees consistency in the response.

This class has two optimizations. First, it explicitly checks for a UnitTestException and skips detailed logging in that case. One of my biggest pet peeve is tests that that flood the logs with stack traces when everything is working like expected. That makes it impossible to skim the logs for obvious problems. This single change can make problems much easier to find.

Second, it uses the logger associated with the targeted class, e.g., CourseResource, instead of with the AOP class. Besides being clearer this allows us to selectively change the logging level for a single Resource instead of all of them.

Another trick is to call an ExceptionService in our handler. This is a service that can do something useful with exceptions, e.g., it might create or update a Jira ticket. This hasn’t been implemented so I just put in a comment to show where it goes.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@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 overloaded
            Method 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 Post Values Checking

Our Resource methods also have a lot of boilerplate code to check the REST parameters. Are they non-null, are email addresses well-formed, etc. Again it’s easy to move much of this code into an AOP method and simplify the Resource method.

We start by defining an interface that indicates a REST transfer object can be validated. This first version gives us a simple thumbs-up or thumbs-down, an improved version will give us a way to tell the client what the specific problems are.

1
2
3
4
public interface Validatable {
 
    boolean validate();
}

We now extend our earlier REST transfer objects to add a validation method.

Two notes. First, the name and email address accept Unicode letters, not just the standard ASCII letters. This is important as our world becomes internationalized.

Second, I’ve added a toString() method but it’s unsafe since it uses unsanitized values. I’ll address sanitization later.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@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.
     */
    @Override
    public 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;
    }
 
    @Override
    public String toString() {
        // FIXME: this is unsafe!
        return String.format("NameAndEmailAddress('%s', '%s', %s)", name, emailAddress, testUuid);
    }
}

We make similar changes to the other REST transfer objects.

We can now write our AOP methods to check the parameters to our CRUD operations. As before the logs are written using the logger associated with the Resource instead of the AOP class.

These methods also log the entry of the Resource methods. Again it’s boilerplate and doing it here simplifies the Resource methods. It would be trivial to also log the method’s exit and elapsed time but we should use a stock logger AOP class in that case.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
@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 unsafe
            if (log.isInfoEnabled()) {
                log.info(String.format("%s(%s): bad arguments", name, Arrays.toString(pjp.getArgs())));
            }
            // TODO: tell caller what the problems were
            results = 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 unsafe
            if (log.isInfoEnabled()) {
                log.info(String.format("%s(%s): bad arguments", name, Arrays.toString(pjp.getArgs())));
            }
            // TODO: tell caller what the problems were
            results = 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;
    }
}

Updated Spring Configuration

We must tell Spring to search for AOP classes. This is a one-line change to our configuration file.

Updated Resource

We can now simplify our Resource classes. Several methods have been reduced to the happy path alone.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
@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];
 
    @Resource
    private CourseFinderService finder;
 
    @Resource
    private CourseManagerService manager;
 
    @Resource
    private 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 AOP
        Course 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 AOP
        final 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}")
    @DELETE
    public 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 match
        try {
            manager.deleteCourse(id, version);
        } catch (ObjectNotFoundException exception) {
            LOG.debug("course not found: " + id);
        }
 
        final Response response = Response.noContent().build();
 
        return response;
    }
}

Unit Tests

The unit tests require a change to every test since we can’t simply instantiate the objects being tested – we must use Spring so the AOP classes are properly weaved. Fortunately that’s essentially the only change – we retrieve the Resource and set the services via a package-private method instead of via package-private constructors.

We also need to create Spring values for our service beans. A Configurer class takes care of this.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Configuration
@ComponentScan(basePackages = { "com.invariantproperties.sandbox.student.webservice.server.rest" })
@ImportResource({ "classpath:applicationContext-rest.xml" })
// @PropertySource("classpath:application.properties")
public class TestRestApplicationContext1 {
 
    @Bean
    public CourseFinderService courseFinderService() {
        return null;
    }
 
    @Bean
    public CourseManagerService courseManagerService() {
        return null;
    }
 
    ....

Integration Tests

The integration tests do not require any changes.

Source Code

 

Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button