Lives in the UK. Likes blogging, cycling and eating lemon drizzle cake. Roger is a DZone MVB and is not an employee of DZone and has posted 143 posts at DZone. You can read more from them at their website. View Full User Profile

Auditing a Spring MVC Webapp with AspectJ. Part 2

07.04.2013
| 4122 views |
  • submit to reddit

Now, this is the blog you want to read if you're interested in creating a Spring MVC Webapp that uses Aspect Oriented Programming (AOP) in the form of Aspectj's @Aspect and @Before annotations to audit a user's visit to a screen. 

As I said in my last blog auditing a user’s visits to a screen is one of those few cross-cutting concerns that Aspect Oriented Programming (AOP) solves very well. The idea in the case of my demo code, is that you add an annotation to the appropriate controllers and every time a user visits a page, then that visit is recorded. Using this technique you can construct a picture of the most popular screens and therefore the most popular chunks of functionality in your application. Knowing these details makes it easier to decide where to aim your development effort as it doesn’t pay to develop those chunks of your application that hardly anyone ever uses.

For the demo-code I created a simple Spring MVC application that has two screens: a home page and a help page. On top of this I’ve created a simple annotation: @Audit, which is used to mark a controller as one that needs auditing (not all of them will, especially if you choose to audit function points rather than individual screens) and to tell the advice object the screen id. This I've demonstrated in the snippet of code below:

@Audit("Home")@RequestMapping(value = "/", method = RequestMethod.GET)public String home(Locale locale, Model model) {


Before getting stuck in to the AspectJ side of things, the first thing to do is to create a standard Spring MVC web app using the Spring Template designed for the job:


The next thing to do is to make a whole bunch of changes to the POM file as described in my previous blog. These are necessary for everything to work, though they're not all essential; however, be sure that you add the aspectJwearver dependency and remove the AspectJ plugin definition.

The app has two controllers and two simple JSPs. The first controller is the HomeController taken from the Spring MVC app and whilst the second is a HelpController designed to display help on any page of the application. I've included the HelpController'sshowHelp(…) method below, but that's just for completeness. It doesn't really matter in this case what the controllers do so long as there are a couple to audit.

@Controller()public class HelpController {
@Audit("Help") // User has visited the help page@RequestMapping(value = "/help", method = RequestMethod.GET)public String showHelp(@RequestParam int pageId, Model model) {
String help = getHelpPage(pageId);

  model.addAttribute("helpText", help);return "help";}


From the code above, you can see that both of my RequestMapping methods are annotated with an @Audit annotation, so the next step is its definition:

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Audit {
String value();}


The key things about this code are the retention policy and the target. The retention policy must be set toRetentionPolicy.RUNTIME, which means that the compiler doesn't throw the annotation away and makes sure that it's there, loaded into the JVM, at runtime. The @Target defines where you can apply the annotation. In this case I want it applied to methods only, so the target is ElementType.METHOD. The annotation MUST contain a value, which is this case is used to hold the name of the screen that the user is currently visiting.

The next key abstraction is the AuditAdvice class as shown below:

@Aspectpublic class AuditAdvice {
@Autowiredprivate AuditService auditService;
/**
  * Advice for auditing a user's visit to a page. The rule is that the Before annotation
  * applies to any method in any class in the com.captaindebug.audit.controller package
  * where the class name ends in 'Controller' and the method is annotated by @Audit.
  * 
  * @param auditAnnotation
  *  Audit annotation holds the name of the screen we're auditing.
  */@Before("execution(public String com.captaindebug.audit.controller.*Controller.*(..)) && @annotation(auditAnnotation) ")public void myBeforeLogger(Audit auditAnnotation) {
auditService.audit(auditAnnotation.value());}

}


This is annotated with two AspectJ annotations: @Aspect and @Before. The @Aspect annotation marks the AuditAdvice class as anaspect, whilst the @Before annotation means that the auditScreen(…) method gets called before any method whose definition matches the expression that is the @Before annotation's argument. 

This expression is idea is rather cool. I've already covered the construction of the execution expression in my blog on Using AspectJ’s @AfterThrowing Advice in your Spring App; however, to sum this up, I'm going to apply the @Before annotated method to any method that has public visibility, returns a String, is in the com.captaindebug.audit.controller package and has the word Controlleras part of the class name. In other words, I'm making it difficult to apply this execution expression to anything but my application's controllers and those controllers MUST be annotated by an @Audit annotation as described by the @annotation(auditAnnotation)expression and the auditScreen(…) method's Audit auditAnnotation argument. This means that I can't inadvertently apply the@Audit annotation to anything but a controller

The AuditAdvice class delegates the responsibility for the actual auditing to an AuditService. This is a dummy service, so instead of doing something useful like storing the audit event in a database, it simply adds it to a log file. 

@Servicepublic class AuditService {
private static Logger logger = LoggerFactory.getLogger(AuditService.class);
/**
  * Audit this screen against the current user name
  * 
  * It's more useful to put this info into a database so that that you can count visits to
  * pages and figure out how often they're used. That way, you can focus your design on the
  * popular parts of your application. The logger is just for demo purposes.
  */public void audit(String screenName) {
String userName = getCurrentUser();

  logger.info("Audit: {} - {}", userName, screenName);
}
/**
  * Get the current logged on user name by whatever mechanism available
  */private String getCurrentUser() {return "Fred";}

}


So, that's the code covered, all that's left to do now is to sort out the Spring config file and there's not much to do here. Firstly, as with any AOP application, you need to add the following AOP enabling line:

<aop:aspectj-autoproxy/>


...together with its schema details:

xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"


And secondly, you need to tell the Spring context about your advice class(es) by updating the context:components-scan element:

<context:component-scan base-package="com.captaindebug.audit">
<context:include-filter type="aspectj"
expression="com.captaindebug.audit.aspectj.AuditAdvice" />
</context:component-scan>


You can also optionally remove the version numbers from the end of the schema location URIs. For example:

http://www.springframework.org/schema/context/spring-context.3.0.xsd


becomes:

http://www.springframework.org/schema/context/spring-context.xsd


The reason for doing this is that it simplifies upgrading Spring versions at some point in the future as schema URIs without any version numbers seem to point to the latest version of that schema.

For completeness, my config file looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">




<!-- DispatcherServlet Context: defines this servlet's request-processing 
infrastructure -->




<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />




<!-- Handles HTTP GET requests for /resources/** by efficiently serving 
up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />




<!-- Resolves views selected for rendering by @Controllers to .jsp resources 
in the /WEB-INF/views directory -->
<beans:bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>




<aop:aspectj-autoproxy/>




<context:component-scan base-package="com.captaindebug.audit">
<context:include-filter type="aspectj"
expression="com.captaindebug.audit.aspectj.AuditAdvice" />
</context:component-scan>




</beans:beans>


Finally, when you run the application the user's visit to the home page is recorded. When the user clicks on the help link the visit to the help page is also recorded. The output in the log file looks something like this:

INFO : com.captaindebug.audit.service.AuditService - Audit: Fred - Home
INFO : com.captaindebug.audit.controller.HomeController - Welcome home! the client locale is en_US
INFO : com.captaindebug.audit.service.AuditService - Audit: Fred - Help

The code for this and the next blog is available on github: https://github.com/roghughe/captaindebug/tree/master/audit-aspectj

Published at DZone with permission of Roger Hughes, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)