Intercepting Method Invocations on POJOs using jAdvise

Intercepting Method Invocations on POJOs using jAdvise

By Bob Lee, OCI Software Engineer

February 2003


Introduction

Sun introduced dynamic proxies in JDK 1.3 greatly simplifying applications of the proxy design pattern. Dynamic proxies make light work of decorating and overriding method invocations at runtime and even completely fabricating implementations from scratch. We no longer need to abuse ourselves, manually coding proxy classes or fighting with code generators in our build scripts. Dynamic proxies take the sting out of these rote processes. Since their release, I've implemented everything from debugging to caching to persistence using dynamic proxies, often times blowing away 75% of an existing code base in a single refactoring. All I needed was an interface.

That last point is key: dynamic proxies need interfaces. Every once in a while, I found myself without an interface. I came across situations where I wanted to wield this newly found weapon against POJOs (plain old Java objects). Requirements to intercept EJB method invocations on the server side finally broke the camel's back. I accomplished intercepting invocations on the client side easily enough by wrapping the client stubs in dynamic proxies that implemented the Enterprise JavaBean's remote interface, however on the server side, I only had the bean implementation to work with. WebLogic, the application server of choice for my client at that time, provided no means of inserting custom interceptors.

Using my small amount of byte code knowledge and WebLogic's support for an application class preprocessor, I set out to implement a generic framework for intercepting method invocations on POJOs. The final result, jAdvise, supports intercepting method invocations on member and static methods, chaining method interceptors, dynamically swapping out interceptor stacks at runtime, a simple API and extremely good performance.

Background & Terminology

Aspect oriented software development or AOSD is a technology for modularizing the cross cutting concerns of system, aspects not easily modeled using traditional OO methodologies. Logic that crosscuts method implementations qualifies as a crosscutting concern. For example, in a purely OO system, developers must repeat the same debugging code over and over in each method implementation:

  1. class MyClass {
  2.  
  3. Log log = ...;
  4.  
  5. public void foo(String value) {
  6. log.debug("foo(" + value + ") called.");
  7. ...
  8. log.debug("foo() returned.");
  9. }
  10.  
  11. public int bar(int value, int otherValue) {
  12. log.debug("bar(" + value + ", " +
  13. otherValue + ") called.");
  14. ...
  15. if (someCondition) {
  16. log.debug("bar() returned 5.");
  17. return 5;
  18. }
  19. ...
  20. log.debug("bar() returned -1.");
  21. return -1;
  22. }
  23.  
  24. }

As this trivial example illustrates, crosscutting concerns quickly seep into and obfuscate business logic. AOSD systems automatically weave existing application code providing a means for abstracting out such logic. The jAdvise implementation utilizes bytecode manipulation to insert method invocation hooks into Java classes.

I borrowed terms commonly used in the AOSD realm when naming the jAdvise classes, and I often describe jAdvise as an AOSD framework. I'll use the following vocabulary to discuss the design and function of jAdvise:

AspectJ

AspectJ is perhaps one of the most popular Java AOP implementations and was started at Xerox PARC. The Eclipse open source project now hosts future AspectJ development. AspectJ defines language extensions enabling developers to implement aspects using first class constructs. AspectJ uses a special compiler to compile Java source and weave aspect logic. Alternately, jAdvise instruments bytecode and inserts generic hooks so that aspects can easily be added and removed at runtime without reloading classes.

Architectural Overview

We must instrument a class before we can advise it. In jAdvise, an instrumentor filters a class's byte code before it actually gets loaded. jAdvise currently supports instrumentation at both build time and runtime. The build time command line instrumentor inputs a normal class and outputs the instrumented version. The runtime instrumentor implements a ClassLoader that filters the byte code as it gets loaded. I've also utilized a third method in cases where I wanted dynamic instrumentation, but I needed to work with another vendor's ClassLoader. WebLogic for example supports a ClassPreprocessor. At runtime, WebLogic passes the byte code for each application class to the ClassPreprocessor implementation. A jAdvise ClassPreprocessor implementation simply instruments the passed byte code and returns the instrumented version.

The instrumentor creates a copy of each method implementation and replaces the original with a new implementation that passes the method invocation down a chain of aspects to the original implementation. A normal method simply executes the business logic and returns.

Figure 1

The instrumented version of the method wraps up the method invocation and passes it down the aspect chain to the original method implementation.

Figure 2

The Invocation class encapsulates a method invocation. The Invocation instance provides a reference to the advised object, the invoked method, the arguments passed to the method, and a method for invoking the next item in the chain, which could be another aspect or the actual method implementation. getAdvised() returns null if the method is static.

  1. class Invocation {
  2. Object getAdvised();
  3. Object[] getArguments();
  4. Method getMethod();
  5. Object invokeNext() throws Throwable;
  6. ...
  7. }

An aspect or method interceptor simply implements a single method which accepts the Invocation instance, performs some filtering, and invokes the next item in the chain.

  1. interface Aspect {
  2. Object invoke(Invocation i) throws Throwable;
  3. }

An Advisor instance advises each class. A client can look up the Advisor instance for a given class using the static getInstance(Class) factory method. Clients get and set the chains atomically using arrays of Aspects.

  1. class Advisor {
  2. Aspect[] getAspects();
  3. void setAspects(Aspect[] aspects);
  4. static Advisor getInstance(Class clazz);
  5. ...
  6. }

The jAdvise implementation currently only supports class-level granularity, meaning that developers can apply aspects on a per class basis. The same aspects apply to both member and static methods for all instances of a class. Aspect implementations can filter based on methods to achieve further granularity.

Javassist

After a great deal of research and experimentation with byte code engineering frameworks, I put my money on Javassist. Created by Shigeru Chiba, Javassist supports source level abstraction and has a number of helper methods for boxing and unboxing primitive arguments and return values and copying method arguments into Object arrays. The source level abstraction allows developers to generate byte code from Java source code rather than hand coding it. I was able to take advantage of this feature in roughly two thirds of the jAdvise implementation.

Using jAdvise

We can easily solve the debugging example outlined in the introduction using jAdvise. Let's start with our example class minus the debugging logic. As you can see, doing so decreases the noise level, allowing us to concentrate on our application logic.

  1. class MyClass {
  2.  
  3. public void foo(String value) {
  4. ...
  5. }
  6.  
  7. public int bar(int value, int otherValue) {
  8. ...
  9. if (someCondition) {
  10. return 5;
  11. }
  12. ...
  13. return -1;
  14. }
  15.  
  16. }

Before we can add aspects, we must instrument our compiled class. We can accomplish this is one of two ways. The build time instrumentor filters the bytecode and outputs an instrumented classfile to the specified destination directory:

java org.crazybob.aop.Instrumentor [destination directory] [class[ class...]] 

Alternately, we can run our application through the InstrumentingLoader. The InstrumentingLoader utilizes a customized ClassLoader to instrument and load classes. The InstrumentingLoader automatically instruments all classes. Using the InstrumentingLoader requires a small change to the way we execute our application. With our application classes in the class path, we'd normally execute our application using the java command:

java MyClass [arguments]

Assuming the jAdvise jar is in our classpath, we can now execute our application through the InstrumentingLoader, and all non-system classes will be advisable:

java org.crazybob.aop.InstrumentingLoader MyClass [arguments]

Next, we implement our debugging aspect. Like our previous example, the debugging aspect logs which method we invoke at the beginning of the method and the return value at the end:

  1. public class DebuggingAspect implements Aspect {
  2.  
  3. Log log = ...;
  4.  
  5. public Object invoke(Invocation i)
  6. throws Throwable {
  7. log.debug("Invocation: " + i);
  8. Object result;
  9. try {
  10. result = i.invokeNext();
  11. }
  12. catch (Throwable t) {
  13. log.debug("Exception: " + t);
  14. throw t;
  15. }
  16. log.debug("Result: " + result);
  17. return result;
  18. }
  19.  
  20. }

Last, we apply the aspect to our class. We do so through the class's Advisor. We look up the Advisor instance using the static factory method and use it to set the aspect chain:

  1. Aspect[] aspects = new Aspect[] {
  2. new DebuggingAspect()
  3. };
  4. Advisor advisor = Advisor.getInstance(MyClass.class);
  5. advisor.setAspects(aspects);

Now, even though MyClass contains no debugging logic, the application logs the invocation and result of each method.

We can utilize this same tool to implement performance monitoring, transparent persistence, and implicit transactions for POJOs. In an example available for download on my site, I combined jAdvise with a sequence diagramming tool to produce live sequence diagrams of running applications.

Performance

The jAdvise framework creates only one object per method invocation, the Object array for the method arguments. jAdvise pools and reuses Invocation instances.

jAdvise also adds special optimization logic; if no aspects are present, the method simply delegates directly to the original implementation without creating an argument array. When no aspects are present, instrumenting a method adds roughly 0.00002 ms overhead to method invocations on a Pentium III 650. In other words, it's safe to simply instrument every class in an application.

Summary

The jAdvise AOSD framework enables us to apply the same time and effort saving patterns we use in dynamic proxy-based development to POJOs and static methods. Using the jAdvise framework, developers can easily modify a method's behavior to return different values, catch exceptions, and throw new exceptions without needing interfaces or source code.

References

[1] jAdvise Home Page.
http://crazybob.org/
[2] Javassist Home Page
http://www.csg.is.titech.ac.jp/~chiba/javassist/
[3] AspectJ Home Page
http://www.eclipse.org/aspectj/
[4] Aspect-Oriented Software Development
http://www.aosd.net/



Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.


secret