Saturday, November 21, 2009

AspectJ in Action

In a business application, the core concern of the application is the business functionality but, other concerns are also present. Most obvious is authentication (verifying the user) and authorization (providing the user access to only a limited set of functions and resources, usually determined by the roles the user is assigned). It is recognized as good programming practice to keep separate the concerns of one domain (business functionality) from those of another (authentication and authorization). Concerns from different domains are called "cross cutting concerns". Separating concerns makes code easier to understand and maintain and reduces the opportunities for error. Aspect oriented programming (AOP) provides tools which address the desire to keep various concerns separate. AspectJ is a toolset for implementing AOP in Java.

AOP has a vocabulary for the solution model it provides. AOP requires that the functionality of the cross-cutting concern be developed in a module that is known as advice. Advice is run at the points where the cross-cutting advice is needed, i.e., pointcuts. In the code below, the aspect is SecurityAspect.aj. The aspect is the code that weaves the Authenticator (the advice) and the CheckingAccount.deposit() method (the pointcut) together.

This example weaves a security component, the Authenticator class, as an aspect into the domain logic. The aspect requires the user to supply his user-id and password before the deposit() methods is called. Note that AOP allows development of the business logic to proceed completely independently of the development of the security components.

In my aspectj.home folder I have aspectj-1.6.6.jar, aspectrt.jar and aspectjtools-1.5.3.jar.

build.xml
<project name="MessageCommunicator" default="run" basedir=".">
 <property name="src"            location="src/main/java"/> 
 <property name="test.src"       location="src/test/java"/>
 <property name="build"          location="build"/>
 <property name="build.classes"  location="build/main/classes"/>
 <property name="test.classes"   location="build/test/classes"/>
 <property name="aspectj.home"   location="/users/greghelton/dev/lib/aspectj"/>

 <path id="project.classpath">
  <pathelement location="${build.classes}"/>
  <fileset dir="${aspectj.home}">
   <include name="**/*.jar"/>
  </fileset>
 </path>

 <target name="clean">
  <delete dir="${build}"/>
  <delete dir="${test.classes}"/>
 </target>
 
 <target name="init" depends="clean">
  <mkdir dir="${build.classes}"/>
  <mkdir dir="${test.classes}"/>
 </target>

 <taskdef resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties">
  <classpath>
   <pathelement location="${aspectj.home}/aspectjtools-1.5.3.jar"/>
  </classpath>
 </taskdef>

 <target name="compile" depends="init">
  <iajc destdir="${build.classes}" source="1.5" srcdir="${src}">
   <classpath refid="project.classpath" />
  </iajc>
 </target>

 <target name="run" depends="compile">
  <java classname="ajia.main.Main">
   <classpath><path refid="project.classpath"/></classpath>
  </java>
 </target>
</project>
Main.java
package ajia.main;
import ajia.banking.CheckingAccount;

public class Main { 
  public static void main(String[] args) {
    CheckingAccount account = new CheckingAccount();
    account.deposit( 300 );
  }
}
CheckingAccount.java
package ajia.banking;
public class CheckingAccount { 
   public void deposit(int dollars) {
      System.out.println("Deposited $" + dollars);
   }
}
AuthenticationException.java
package ajia.security;

public class AuthenticationException extends RuntimeException {
  public AuthenticationException(String message) {
    super(message);
  }
}
Authenticator.java
package ajia.security;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Authenticator {
  private ThreadLocal authenticatedUser = new ThreadLocal();
 
  public void authenticate() {
    if (isAuthenticated()) {
      return;
    }
    String[] userNamePassword = getUserNamePassword();
    if (!userNamePassword[0].equals(userNamePassword[1])) {
      throw new AuthenticationException("User/password didn't match");
    }
    authenticatedUser.set(userNamePassword[0]);
  }
 
  public boolean isAuthenticated() {
    return authenticatedUser.get() != null;
  }
 
  public String[] getUserNamePassword() {
     boolean usePrintln = Boolean.getBoolean("ant.run");
     String[] userNamePassword = new String[2];
     try {
      if (usePrintln) {
        System.out.println("Username: ");
      } else {
        System.out.print("Username: ");
      }
      BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
      userNamePassword[0] = in.readLine().trim();
      if (usePrintln) {
        System.out.println("Password: ");
      } else {
        System.out.print("Password: ");
      }
       userNamePassword[1] = in.readLine().trim();
     } catch (IOException ex) {
       // ignore... will return array of null strings
     }
     return userNamePassword;
   }
}
SecurityAspect.aj
package ajia.security;
import ajia.banking.CheckingAccount;

public aspect SecurityAspect { 
  private Authenticator authenticator = new Authenticator(); 

  pointcut secureAccess() 
    : execution(* CheckingAccount.deposit(..)); 
  
  before() : secureAccess() { 
    System.out.println("Authenticating user"); 
    authenticator.authenticate();
  } 
}
When the code is compiled and run, the Authenticator throws an exception which prevents CheckingAccount.deposit() from executing if the user-id and password do not match.  Note the anomaly the ANT tool imposes when the run target is executed: the print statements, although executed before the keyboard input is read, don't appear on the screen until after the keyboard input is accepted.
[greg:aspect] ant
Buildfile: build.xml

clean:
   [delete] Deleting directory /Users/greghelton/dev/src/java/aspect/build

init:
    [mkdir] Created dir: /Users/greghelton/dev/src/java/aspect/build/main/classes
    [mkdir] Created dir: /Users/greghelton/dev/src/java/aspect/build/test/classes

compile:

run:
     [java] Checking and authenticating user
greg
greg
     [java] Username: Password: Deposited $300

BUILD SUCCESSFUL
Total time: 11 seconds
AspectJ in Action by Ramnivas Laddad, has a very good discussion of system design including this gem - The architect of a system is often faced with underdesign/overdesign issues.  In just a few sentences the author highlights how YAGNI, agile, levels of abstraction and the cost of change all benefit from AOP. This is covered in sections 1.8.1 and 1.8.2 of the early access version of the 2nd edition of this book.