Tuesday, 2 July 2013

Auditing a Spring MVC Webapp with AspectJ. Part 2

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's showHelp(…) 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 to RetentionPolicy.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:

@Aspect
public class AuditAdvice {

 
@Autowired
 
private 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 an aspect, 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 Controller as 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.

@Service
public 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

No comments: