InformIT

A Primer for Aspect-Oriented Programming in Java

Date: Jun 25, 2004

Return to the article

AOP is not a successor to OOP, but a new way of looking at object functionality: allowing the behavior of an object to be modularized and used across other components. Tim Stevens explains the difference and shows how powerful AOP can be. Perhaps too powerful.

Java AOP Basics

Aspect-oriented programming (AOP), although far from a new topic, has recently become quite a hot one. Many folks in the coding industry are touting AOP as the successor to the object-oriented programming (OOP) model; however, as we'll see in this article, despite a similar acronym AOP addresses few if any of the same concerns as OOP. In short, AOP allows for code behavior to be divided into core components (aspects), which can be injected into arbitrary locations easily. Method calls can be intercepted, augmented, or redirected, as can field access and even inheritance—in many cases without code changes.

Whereas OOP doctrine is to group functionality into objects and create relationships between those objects, AOP says to think of functionality (here called aspects or concerns) as being independent from any class. AOP primarily deals with what are called crosscutting concerns, which are aspects of functionality that are needed but are unrelated to the actual behavior of the class in which they're needed. The prototypical (and overused) example is logging—something that most applications must provide, but that generally has nothing to do with the applications or their objects. AOP doctrine says to abstract such aspects of applications, in order to make them accessible regardless of class inheritance.

Aspects can be plugged into code at join points—places such as method calls, field access, and exception handling. You must give instructions (advice in AOP-speak) for what to do at these join points. Exactly how you provide advice differs widely depending on which AOP implementation you're using, but often it's via something like an XML configuration file or metadata within code, generally using something like regular expressions to identify join points.

AOP also gives developers access to compile-time behavior much like multiple inheritance, called introductions. With introductions, you can force a certain class to implement a separate interface, without touching the code of the class itself.

Plenty of other functionality is provided, and many implementations of AOP have their own additional features. AOP has a lot of power when changing application behavior, but the ease with which these changes can be implemented and the way in which code execution can be modified outside the code itself is bound to cause serious headaches for some developers. We'll touch on this topic again, but now that we have the lingo down, let's look at an example.

NOTE

We'll use the JBoss implementation of AOP, as its way of describing rules of advice via XML configuration files means that we can quickly and easily handle most AOP functionality without modifying or recompiling our example. Also, for the scope of this example, JBoss AOP relies only on the Sun Java compiler and runtime, along with its own libraries. Other Java AOP implementations use keywords and extensions to the Java language itself, relying on non–Sun compilers to generate their classes. There are advantages to each type of implementation, but in this simple example JBoss makes our work easier.

A Simple AOP Example

Suppose we have a simple class that takes a number from user input and determines whether that number is prime. Several ways are available to perform such a check, and Joe, the author of this class, is overly cautious: He has written three separate methods for verification, and the number entered is said to be prime only if all three methods agree. Here's the application code:

public class ComplexFormulae
{
  /**
   * Highly inefficient but overly thorough algorithm
   */
  public boolean isPrimeOne(BigInteger number)
  {
    BigInteger testNumber = new BigInteger("2");

    while(testNumber.compareTo(number) < 0)
    {
      if(number.mod(testNumber).equals(
        new BigInteger("0")))
      {  return false;  }
      testNumber = testNumber.add(
        new BigInteger("1"));
    }

    return true;
  }

  /**
   * Slightly more efficient algorithm
   */
  public boolean isPrimeTwo(BigInteger number)
  {
    BigInteger testNumber = new BigInteger("2");

    while(testNumber.compareTo(
      number.divide(new BigInteger("2"))) <= 0)
    {
      if(number.mod(testNumber).equals(
        new BigInteger("0")))
      {  return false;  }

      testNumber = testNumber.add(
        new BigInteger("1"));
    }

    return true;
  }

  /**
   * Reliant on the BigInteger class, unknown performance
   */
  public boolean isPrimeThree(BigInteger number)
  {
    return number.isProbablePrime(50);
  }

  public static void main(String[] args)
  {
    ComplexFormulae   formulae  = new ComplexFormulae();
    BufferedReader   reader   = null;
    BigInteger     number   = null;
    String       numberStr  = "";

    boolean       isPrimeOne   = false;
    boolean       isPrimeTwo   = false;
    boolean       isPrimeThree  = false;

    reader = new BufferedReader(
      new InputStreamReader(System.in));

    while(number == null)
    {
      System.out.println(
        "Please enter the number to test:");
      try
      {  numberStr = reader.readLine(); }
        catch(Exception e)
        {
          System.out.println(
            "Error reading input: " + e);
          System.exit(1);
        }
      try
      {  number = new BigInteger(numberStr.trim()); }
        catch(NumberFormatException nfe)
        {
          System.out.println(numberStr +
            " is not a number!" + '\n' + nfe);
        }
    }

    isPrimeThree = formulae.isPrimeThree(number);
    isPrimeTwo = formulae.isPrimeTwo(number);
    isPrimeOne = formulae.isPrimeOne(number);

    if(isPrimeThree && isPrimeTwo && isPrimeOne)
      System.out.println(numberStr +
        " is a prime number!");
    else
      System.out.println(numberStr +
        " is not a prime number!");

  }
}

Now Speed It Up, Jack

Unfortunately, running the sample code I just showed you on any prime number greater than 219–1 (the next greatest is 2,147,483,647) takes a very long time to complete on most computers. What's taking so long? The typical brute-force way to find out would be to change the code and put in some timing logic. Or you could run the code through a debugger or performance analyzer and get timings that way. But AOP provides a rather easy way to do just that without changing code, recompiling, or relying on any debugging or profiling tools.

The first thing we need to do is code an instance of Interceptor—the class that JBoss uses to intercept method calls, field access, object instantiation, etc. It's a simple interface, requiring only two methods of the implementer. In this case, we want to intercept any calls to the three methods above, track how long they take to execute, and print the method name and the timing after the methods have completed. The code to do so is quite simple:

public class PerformanceInterceptor implements Interceptor
{
  public String getName()
  {  return "PerformanceInterceptor";  }

  public Object invoke(Invocation invocation) throws Throwable
  {
    long  startTime  = new Date().getTime();
    long  endTime   = 0l;
    MethodInvocation   methodInvoke  = null;

    //Must ensure that this is only bound to method
    // invocations, else ClassCastException will ensue
    methodInvoke = (MethodInvocation)invocation;

    try
    {  return invocation.invokeNext(); }
    finally
    {
      endTime = new Date().getTime();
      System.out.println("Execution of " +
        methodInvoke.actualMethod.getName() +
        " took " + (endTime - startTime) + " millis");
    }
  }
}

The work is handled in the invoke() method. This method receives an instance of Invocation, a JBoss class, which contains all the information required to determine what is being intercepted. In this case, we'll be receiving a MethodInvocation instance, as a method is being intercepted, and we use it only to get the name of the method being intercepted. But, with similar code, it would be easy to modify the parameters we pass along to the method being intercepted, change the return values from that method, modify exception handling, or simply absorb the method call and do nothing.

The trick is to bind this PerformanceInterceptor class back to our underperforming ComplexFormulae class, and that's achieved through a combination of an XML configuration file and a custom classloader. First, the configuration file:

<?xml version="1.0" encoding="UTF-8"?>
<aop>
  <bind pointcut=
    "execution(public boolean ComplexFormulae->isPrime*(*))">
    <interceptor class="PerformanceInterceptor"/>
  </bind>
</aop>

The <bind> element indicates which methods to intercept, and what to do with them. The pointcut attribute specifies a statement much like a regular expression, identifying the points to intercept. In this case, we're looking for public methods within the ComplexFormulae class that return a type Boolean and have a name that begins with isPrime. If we also wanted to time the main() method, we could make a simple change to indicate all public methods:

<?xml version="1.0" encoding="UTF-8"?>
<aop>
  <bind pointcut=
    "execution(public * ComplexFormulae->*(*))">
    <interceptor class="PerformanceInterceptor"/>
  </bind>
</aop>

Now, any public method, regardless of name or return type, will be timed. Of course, we're assuming that you run your code through the JBoss AOP classloader. To do that, save the above XML code into a file (generally jboss-aop.xml, but we'll use performance-binding.xml here), and provide the following two system parameters on your call to Java:

java -Djava.system.class.loader=org.jboss.aop.standalone.SystemClassLoader
-Djboss.aop.path=performance-binding.xml ComplexFormulae

Running the above command using the second binding file will render output that looks something like this:

Please enter the number to test:
2147483647
Execution of ComplexFormulae$isPrimeThree$WithoutAdvisement took 16 millis
Execution of ComplexFormulae$isPrimeTwo$WithoutAdvisement took 2778875 millis
Execution of ComplexFormulae$isPrimeOne$WithoutAdvisement took 3162281 millis
2147483647 is a prime number!
Execution of ComplexFormulae$main$WithoutAdvisement took 5943734 millis

Altering the Example by Circumvention

isPrimeThree(), the method that relies on the BigInteger.isProbablePrime() method, vastly outpaces the other, more "thorough" methods. Faced with these facts, and facing customers screaming about the performance of this application, we can disable the two slower methods. But suppose that the coder who's responsible for this class is on vacation and the code isn't accessible to anyone else! No problem, as circumventing those methods' execution via AOP is a piece of cake. Simply change the invoke() method on our Interceptor class to look like this:

public Object invoke(Invocation invocation) throws Throwable
{
  try
  {  return new Boolean(true);  }
  finally
  {}
}

And change our XML binding to intercept only the two methods we want to switch off:

<bind pointcut=
  "execution(public * ComplexFormulae->isPrimeOne(*))">
  <interceptor class="PerformanceRedirector"/>
</bind>
<bind pointcut=
  "execution(public * ComplexFormulae->isPrimeTwo(*))">
  <interceptor class="PerformanceRedirector"/>
</bind>

Now, whenever isPrimeOne() or isPrimeTwo() is called, a Boolean value of true will always be returned nearly instantly, meaning that the application relies entirely on isPrimeThree().

CAUTION

This is a great fix, right?

Perhaps to some. But to many coders (including me), the ease of doing something like this is a little disconcerting. We've now radically changed the behavior of the application, yet nothing in the code has been modified to indicate this change, and unless the change is well-documented somewhere, when Joe comes back from vacation he's liable to be rather confused. While this is an unrealistic example, it's easy to see how AOP can be a dangerous weapon without clear policies and best practices. This is why care must be exercised when adopting AOP development, as its benefits in making quick and sweeping modifications to application behavior are often outweighed by the need for new process controls and documentation.

Conclusion

To sum things up, AOP has the potential to change the way in which applications are developed, but it also introduces developers and project managers to a new tangle of issues surrounding application behavior. Also, it should be clear now that AOP is not a successor to OOP, but a new way of looking at object functionality: allowing the behavior of an object to be modularized and used across other components—without creating complex class hierarchies, but instead creating complex webs of AOP configuration. AOP is nothing new, but is now facing the heat and the attention of developers everywhere. Will it revolutionize software development as OOP did in the 1980s? Possibly, but, also like OOP, it'll take a long time to catch on.

800 East 96th Street, Indianapolis, Indiana 46240