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 2191 (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 Interceptorthe 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.