Search |
||||||||||||
Implement Your Own Proxy-Based AOP Framework
Tue, 2005-11-01
|
||||||||||||
| |||||||
Aspect-oriented programming (AOP) is well-suited to managing application crosscutting concerns, such as logging, security, and transaction management. AOP provides a complement to object-oriented programming (OOP), which is still the most common and powerful methodology to address core business concerns. AOP can reduce code scattering, tangling, and duplication in applications. Based on their implementation approaches, AOP frameworks can be classified into two categories:
JDK dynamic proxy has been available since JDK 1.3. The proxy class, which implements a list of interfaces specified at runtime, is dynamically created by the JVM. Method invocations on the proxy class are delegated to the underlying proxied object. JDK dynamic proxy is simple to use, but, like all reflective code, it is somewhat slower. For most situations, the overhead is not critical. Another limitation is that it can only implement interfaces.
What if you want to proxy legacy classes that do not have
interfaces? You can use CGLIB. CGLIB is a powerful, high-performance code generation library. Under the cover, it uses
ASM, a small but fast
bytecode manipulation framework, to transform existing byte code to
generate new classes. CGLIB is faster than the JDK dynamic proxy
approach. Essentially, it dynamically generates a subclass to
override the non-final methods of the proxied class
and wires up hooks that call back to the user-defined interceptors.
To help you understand and demystify AOP, this article shows you how to create a simple AOP framework using both JDK dynamic proxy and CGLIB. This framework supports declarative transaction management. This article uses Java 5 features, including annotations and generics. Since JDK dynamic proxy is simpler, this article starts with dynamic proxy.
A proxy factory is the central place to create proxies for the requested target classes. Clients of proxies do not know how the proxies are created.
public interface DynamicProxyFactory{
<T> T createProxy(Class<T> clazz,
T target,
AOPInterceptor interceptor);
}
To create a dynamic proxy, you need a list of proxy interfaces
and a target object. There is a set of rules about the proxy
interfaces. You can look at the
java.lang.reflect.Proxy documentation for details. For
simplicity, this article uses a single interface only. You can
ignore the interceptor argument for now; it will be discussed in
the next section.
public <T> T createProxy(Class<T> clazz,
T target, AOPInterceptor interceptor) {
InvocationHandler handler =
new DynamicProxyInvocationHandler(target,
interceptor);
return (T)Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class<?>[] {clazz},
handler);
}
The implementation of the proxy factory is simple. First, it
creates an instance of InvocationHandler, which is one
of the two key dynamic proxy APIs. Then, it uses the static method
Proxy.newProxyInstance to create a proxy that
implements the interface passed in as its second parameter. Note
that the second argument of the newProxyInstance method is an array of
Class<?> instead of Class<T>.
Arrays cannot be created if the element type is generic, but an
unbounded wildcard can be used.
All method invocations on the generated proxy class are
forwarded to InvocationHandler's single method:
public Object invoke(Object proxy,
Method method, Object[] args)
Let's see how this method is implemented in DynamicProxyInvocationHandler.java:
public class DynamicProxyInvocationHandler
implements InvocationHandler {
private Object target;
private AOPInterceptor interceptor;
public DynamicProxyInvocationHandler(Object target,
AOPInterceptor interceptor) {
this.target = target;
this.interceptor = interceptor;
}
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable{
try {
interceptor.before(method, args);
Object returnValue = method.invoke(target, args);
interceptor.after(method, args);
return returnValue;
} catch(Throwable t) {
interceptor.afterThrowing(method, args, t);
throw t;
} finally {
interceptor.afterFinally(method, args);
}
}
}
If you ignore the interceptor-related code, the implementation
of the invoke method is straightforward. It uses reflective invocation
on the Method object to delegate to the target
object.
As discussed in the preceding section, all method invocations on the
proxy class are forwarded to the invoke method of
InvocationHandler. The invoke method
delegates calls to the target object. Since all method
calls have to go through the single invoke method, you
can apply the decorator pattern on that method, or even immediately
return without further delegating to the target object. If you
decorate that method before delegating to the target object, you
are essentially applying AOP before advice. If you add custom code
after delegating to the target object, you are essentially applying
AOP after advice. If, instead of delegating to the target object,
you route method calls to a different path, you are applying
"around" advice. Now it should be easy for you to figure out what
afterThrowing and afterReturn advices
mean. For details on each kind of advice, you can refer to the book AspectJ in
Action.
In this article, some method advices are grouped into the
AOPInterceptor. As its name implies, it intercepts
method invocations on the target object through decorating the
invoke method of InvocationHandler, as shown in the
DynamicProxyInvocationHandler class. The method
advices should be decoupled in the real world. Advices for
finally blocks are not common, but are added here for
demonstration purposes.
public interface AOPInterceptor {
void before(Method method, Object[] args);
void after(Method method, Object[] args);
void afterThrowing(Method method, Object[] args, Throwable throwable);
void afterFinally(Method method, Object[] args);
}
Typically, you do not want to intercept all of the method calls. That is, advices are applied only to the methods or classes you are interested in. You can set the classes and methods through regular expressions in XML files, annotations, or other mechanisms. At run time, your framework should be able to decide whether or not to apply any advices by matching the current class and method with those specified in your configuration files or annotations. Even the runtime argument values for a method can be used to determine which advice should be applied.
The TransactionAnnotation is a simple annotation,
as shown below:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TransactionAnnotation {
String value();
}
The @Retention meta-annotation is set to
RetentionPolicy.RUNTIME so that it can be accessed via
reflection. You can attach annotations to methods only since the
@Target meta annotation is set to
ElementType.METHOD.
Assume you have a business service, called
PersistenceService. It requires a new transaction in
its save method, but transactions are not supported in
its load method.
public interface PersistenceService {
@TransactionAnnotation("REQUIRES_NEW")
void save(long id, String data);
@TransactionAnnotation("NOT_SUPPORTED")
String load(long id);
}
Now you need a transaction. Assume you have a transaction API like this:
public interface Transaction{
void open();
void rollBack();
void commit();
void closeIfStillOpen();
}
The transaction starts by calling the open method
and must be closed after use. Here is the transaction interceptor
that performs declarative transaction management for
PersistenceService:
public class TransactionInterceptor
implements AOPInterceptor {
private Transaction transaction;
public void before(Method method, Object[] args) {
if (isRequiresNew(method)) {
transaction = new TransactionAdapter();
transaction.open();
}
}
public void after(Method method, Object[] args) {
if (transaction != null) {
transaction.commit();
}
}
public void afterThrowing(Method method,
Object[] args, Throwable t) {
if (transaction != null) {
transaction.rollBack();
}
}
public void afterFinally(Method method, Object[] args) {
if (transaction != null) {
transaction.closeIfStillOpen();
transaction = null;
}
}
protected boolean isRequiresNew(Method method) {
TransactionAnnotation transactionAnnotation =
method.getAnnotation(TransactionAnnotation.class);
if (transactionAnnotation != null) {
if ("REQUIRES_NEW".equals(
transactionAnnotation.value())){
return true;
}
}
return false;
}
}
Now you can plug in the transaction interceptor into a proxy when the proxy is created.
DynamicProxyFactory proxyFactory = new DynamicProxyFactoryImpl();
AOPInterceptor interceptor = new TransactionInterceptor();
PersistenceService proxy =
proxyFactory.createProxy(PersistenceService.class,
new PersistenceServiceImpl(),
interceptor);
proxy.save(1, "Jason Zhicheng Li");
String data = proxy.load(1);
You can run the manual test from the attached source code to see
the results. As annotated in the PersistenceService
interface, the save method is executed in a new
transaction context, but there is no transaction for the
load method.
Without much coding, you can externalize the proxy creation through dependency injection. If you have experience in dependency injection frameworks, like Spring, it should be familiar to you. All you need to do is to configure interface type, target, and interceptor in your proxy factory. You can even add a layer of abstraction for the target by passing a target holder instance into the proxy factory. The target holder has a reference to the real target and it can be instantiated without a real target. Then you can implement advanced features such as hot swapping or pooling of real targets and virtual proxies.
Similar to InvocationHandler and Proxy
in dynamic proxy, there are two key APIs in CGLIB proxy,
MethodInterceptor and Enhancer. The
MethodInterceptor is the general callback interface
used by Enhancer, which dynamically generates
subclasses to override the non-final methods of the superclass.
MethodInterceptor is responsible for intercepting all
method calls in the generated proxy. You can invoke custom code
before and after the invocation of the super methods, and even skip
invocation of the super methods. Typically, a single callback is
used per enhanced class, but you can use
CallbackFilter to control which callback to use for a
method.
Let's first create a CGLIB MethodInterceptor.
public class CGLIBMethodInterceptor
implements MethodInterceptor {
private AOPInterceptor interceptor;
public CGLIBMethodInterceptor(AOPInterceptor interceptor) {
this.interceptor = interceptor;
}
public Object intercept(Object object, Method method,
Object[] args, MethodProxy methodProxy )
throws Throwable {
try {
interceptor.before(method, args);
Object returnValue =
methodProxy.invokeSuper(object, args);
interceptor.after(method, args);
return returnValue;
} catch(Throwable t) {
interceptor.afterThrowing(method, args, t);
throw t;
} finally {
interceptor.afterFinally(method, args);
}
}
The implementation is very similar to
DynamicProxyInvocationHandler in dynamic proxy, but
note that there is no target object and the type
T is the concrete class type, not the interface type
as in DynamicProxyFactory. The real method is invoked
by using MethodProxy, which is faster, instead of the
Method object. Now let's create the proxy factory:
public class CGLIBProxyFactoryImpl
implements CGLIBProxyFactory {
public <T> T createProxy(Class<T> clazz,
AOPInterceptor interceptor) {
MethodInterceptor methodInterceptor =
new CGLIBMethodInterceptor(interceptor);
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(methodInterceptor);
return (T)enhancer.create();
}
}
After you set the superclass type and method interceptor, you
simply call the create() method on the
Enhancer object to create a proxy. Optionally, you can
configure CallbackFilter to map a method to a callback
by calling the setCallbackFilter(CallbackFilter) method.
In addition, you can specify the proxy class to implement a set of
interfaces. In this CGLIB implementation, since no interface is
specified, the transaction attributes must be declared in the
PersistenceService implementation instead of the
interface.
Similarly, you can implement interceptors to address logging, validation, auditing, caching, and security, which are orthogonal to core business concerns. As shown above, both dynamic proxy and CGLIB implementation are simple to implement, but you must be aware that important issues such as performance, exception handling, and threading are not covered here.
The AOP implementation in this article is simplified for clarity, but it shows you the essentials of proxy-based AOP frameworks. AOP decouples crosscutting concerns, such as the transaction management demonstrated in this article, from application core concerns. With aspect-oriented design and programming, you can significantly simplify your design and implementation. In some cases, however, third-party AOP frameworks cannot be used due to non-technical reasons, such as corporate policies and license issues. As shown in this article, you can implement your own AOP framework that is tailored to meet your needs. JDK dynamic-proxy-based implementation is simpler, since it uses standard Java. That means there are no third-party libraries or build-time bytecode instrumentation. Alternatively, you can choose CGLIB to proxy legacy classes and have better performance, but you need to introduce multiple third-party libraries into your system. At that moment, you should ask yourself if you need to pick an available AOP framework, which is often more complete and sophisticated than your roll-your-own AOP implementation.
Jason Zhicheng Li would like to thank Brian Paulsmeyer and Mark Volkmann for their help in reviewing this article.
|
|