February 23, 2011

Best Practice Aspect-Oriented Programming with JBoss AOP

In my recent project I have been working with JBoss AOP. There have been some pitfalls, I have fallen into and in this blog I will share those you.

First of all. Do not choose aspect-oriented programming to solve everyday Java problems. Example of good cases are:
  • Logging
  • Caching
  • Security
  • Error Handling
But why does AOP does not suites to solve common Java problem? Lets look at an example with Spring and AspectJ.

The Logic class:
public class FooPojo { public void setFoo(Object inparam) { /* logic goes here... */ } }
The Aspect class:
@Aspect public class FooAspect {
    @Before("execution(void set*(*))") public void handle() { /* logic goes here... */ }
}
Boilerplate XML configuration files
<beans>
    <aop:aspectj-autoproxy>
        <aop:include name="fooAspect" />
    </aop:aspectj-autoproxy>
    <bean id="fooAspect" class="se.msc.example.aop.FooAspect" />
</beans>

<beans>
    <import resource= "aspects-config.xml"/>
    <bean name="foo" class="se.msc.example.aop.FooPojo" />
</beans>


And now call it
((FooPojo) new ClassPathXmlApplicationContext(“application-config.xml”).getBean(“foo”)).setFoo(null);
The problem with the code above, is that in FooPojo there is no hint at all, that other code will be called. This can be very confusing for a junior programmer and also to a much more skilled programmer that is not familiar with AOP. So how to make AOP more clearer and more understandable? The answer is to look at other framework and how they have solved it. Take for example Spring. They use AOP very heavily under the hood, to solve common tasks as marking classes as transactional (@Transactional). And in J2EE, Oracle uses also Annotation, e.g. in JAX-WS they have the method Annotation @WebMethod, to signal that a method is a Web Service method. So lets copy that pattern, to write your own Annotation, as marker in the code that you want to apply apsect-oriented programming to. Our own Annotation, to trigger Aspect:
package se.msc.example.aop;

import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Cache Annotation.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ METHOD })
public @interface Cache {

 String attribute() default "";
    // String value() default ""; if attribute name is 'value', you do not need to specify it 
}

Our Aspect. Here we use interface implementation. This solution works on JBoss 4.3.0 - 5.1.0:
package se.msc.example.aop;

import java.lang.annotation.Annotation;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.log4j.Logger;
import org.jboss.aop.advice.Interceptor;
import org.jboss.aop.joinpoint.Invocation;
import org.jboss.aop.joinpoint.MethodInvocation;

/**
 * Cache interceptor demo. See also jboss-aop.xml. 
 */
public class CacheInterceptor implements Interceptor {
 protected final Logger log = Logger.getLogger(this.getClass());
 private final ConcurrentHashMap<Long, Object> cache = new ConcurrentHashMap<Long, Object>();
 
 /**
  * @see org.jboss.aop.advice.Interceptor#getName()
  */
 public String getName() {
  return this.getClass().getSimpleName();
 }

 /**
  * @see org.jboss.aop.advice.Interceptor#invoke(Invocation)
  */
 public Object invoke(Invocation invocation) throws Throwable {
  Object rtnObject = beforeInvocation((MethodInvocation) invocation);
  if (rtnObject != null) {
   if (log.isDebugEnabled()) log.debug("CACHED.");
   return rtnObject; // if cached, return 
  }
  try {
   rtnObject = invocation.invokeNext();
   afterInvocation((MethodInvocation) invocation, rtnObject);
  } catch (Throwable t) {
   onInvocationException((MethodInvocation) invocation, t);
  }
  return rtnObject;
 }

 protected Object beforeInvocation(MethodInvocation invocation) throws Throwable {
  if (log.isDebugEnabled()) log.debug("beforeInvocation...");
  Object[] args = invocation.getArguments();
  // always check for null and array length, when using reflection
  if (args.length > 0 && args[0] != null && (args[0] instanceof Long)) return cache.get(args[0]);
  return null;
 }

 protected void afterInvocation(MethodInvocation invocation, Object rtnObject) throws Throwable {
  if (log.isDebugEnabled()) log.debug("afterInvocation...");
  if (rtnObject == null) return;
  Object[] args = invocation.getArguments();
  // always check for null and array length, when using reflection
  if (args.length > 0 && args[0] != null && (args[0] instanceof Long)) {
   if (log.isDebugEnabled()) log.debug("ADD id=" + args[0] + ", object='" + rtnObject + "'.");
   cache.put((Long) args[0], rtnObject);
  }
 }

 protected void onInvocationException(MethodInvocation invocation, Throwable invocationException)
         throws Throwable {
  if (log.isDebugEnabled()) log.debug("onInvocationException...");
  log.error("onInvocationException", invocationException);
  // if nothing we can do, re-throw
  throw invocationException;
 }
 
 // ----------------------- Helper Method -----------------------
 
 protected <t extends Annotation> T getMethodAnnotation(MethodInvocation invocation,
         Class<t> annotationClass) {
  return invocation.getMethod().getAnnotation(annotationClass);
 } 
}

The JBoss AOP configuration file, META-INF/jboss-aop.xml
<?xml version="1.0" encoding="UTF-8"?>
<aop xmlns="urn:jboss:aop-beans:1.0">
 <!-- Singleton Interceptor -->
 <!-- http://docs.jboss.com/aop/1.3/aspect-framework/reference/en/html_single/index.html -->
 <interceptor name="Cache" class="se.msc.example.aop.CacheInterceptor" scope="PER_VM" />
 <bind pointcut="all(@se.msc.example.aop.Cache)">
  <interceptor-ref name="Cache" />
 </bind>
</aop>

Now lets create a Test class, that we annotate with our own Annotation:
package se.msc.example.aop;

import java.util.HashMap;
import java.util.Map;

/**
 * Simulate DB Service.
 */
public class Dummy {
 private Map<Long, String> db = new HashMap<Long, String>();
 
 @Cache
 public String load(Long id) {
  if (!db.containsKey(id)) db.put(id, "NEW " + id);
  return db.get(id);
 }
 
 public void update(long id, String str) {
  db.put(id, str);
 }
}

And a Unit Test to verify it is working.
package se.msc.example.aop;

import java.util.HashMap;
import java.util.Map;

/**
 * Simulate DB Service.
 */
public class Dummy {
 private Map<Long, String> db = new HashMap<Long, String>();
 
 @Cache
 public String load(Long id) {
  if (!db.containsKey(id)) db.put(id, "NEW " + id);
  return db.get(id);
 }
 
 public void update(long id, String str) {
  db.put(id, str);
 }
}

And here is the maven pom.xml
<?xml version="1.0" encoding="UTF-8" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelversion>4.0.0</modelVersion>
 <groupid>se.msc.example</groupId>
 <artifactid>aop-demo</artifactId>
 <version>1.0-SNAPSHOT</version>
 <packaging>jar</packaging>
 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  <jbossaop.version>2.1.1.GA</jbossaop.version>
 </properties>
 <repositories>
  <repository>
   <id>repository.jboss.org</id>
   <name>Jboss Repository for Maven</name>
   <url>http://repository.jboss.org/maven2/</url>
  </repository>
 </repositories>
 <pluginrepositories>
  <pluginrepository>
   <id>repository.jboss.org</id>
   <name>Jboss Repository for Maven</name>
   <url>http://repository.jboss.org/maven2/</url>
  </pluginRepository>
 </pluginRepositories>
 <dependencies>
  <!-- JBoss 5.1.0.GA -->
  <dependency>
   <groupid>org.jboss.aop</groupId>
   <artifactid>jboss-aop</artifactId>
   <version>2.1.1.GA</version>
  </dependency>
  <!-- Test support -->
  <dependency>
   <groupid>junit</groupId>
   <artifactid>junit</artifactId>
   <version>4.8.1</version>
   <scope>test</scope>
  </dependency>
 </dependencies>
 <build>
  <pluginmanagement>
   <plugins>
    <plugin>
     <groupid>org.apache.maven.plugins</groupId>
     <artifactid>maven-compiler-plugin</artifactId>
     <version>2.0.2</version>
     <configuration>
      <source>1.6</source>
      <target>1.6</target>
      <!-- Plugin ignores default value 'project.build.sourceEncoding' -->
      <encoding>${project.build.sourceEncoding}</encoding>
     </configuration>
    </plugin>
    <plugin>
     <groupid>org.apache.maven.plugins</groupId>
     <artifactid>maven-jar-plugin</artifactId>
     <version>2.2</version>
     <configuration>
      <archive>
       <manifest>
        <!-- Adding Implementation And Specification Details -->
        <adddefaultimplementationentries>true</addDefaultImplementationEntries>
       </manifest>
      </archive>
     </configuration>
    </plugin>
    <plugin>
     <groupid>org.apache.maven.plugins</groupId>
     <artifactid>maven-surefire-plugin</artifactId>
     <version>2.4.3</version>
     <configuration>
      <!-- Konfiguration för AOP loadtime-weaving -->
      <argline>-javaagent:"${settings.localRepository}/org/jboss/aop/jboss-aop/${jbossaop.version}/jboss-aop-${jbossaop.version}.jar"</argLine>
     </configuration>
    </plugin>    
   </plugins>
  </pluginManagement>
 </build>
 <reporting>
  <plugins>
   <plugin>
    <groupid>org.codehaus.mojo</groupId>
    <artifactid>cobertura-maven-plugin</artifactId>
    <version>2.3</version>
   </plugin>
  </plugins>
 </reporting>
</project>

To make the example complete, we also need to supply a log4j configuration file.
<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE log4j:configuration PUBLIC "-//log4j/log4j Configuration//EN" "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
 <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
  <param name="Target" value="System.out" />
  <layout class="org.apache.log4j.PatternLayout">
   <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%p] %c:%L - %m%n" />
  </layout>
 </appender>
 <appender name="FILE_APPENDER" class="org.apache.log4j.RollingFileAppender">
  <param name="File" value="/tmp/server.log" />
  <param name="Append" value="true" />
  <layout class="org.apache.log4j.PatternLayout">
   <param name="ConversionPattern" value="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
  </layout>
 </appender>
 <logger name="se.msc.example.aop">
  <level value="DEBUG" />
  <appender-ref ref="CONSOLE" />
 </logger>
 <root>
  <level value="WARN" />
  <appender-ref ref="CONSOLE" />
 </root>
</log4j:configuration>


To run/debug this inside Eclipse we need to copy the argLine from the maven pom file to the unit test file configuration.



For more about JBoss AOP Maven plugin , see http://community.jboss.org/wiki/JBossAOPMavenPlugin.

No comments: