Implement Your Own Proxy-Based AOP Framework

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:

  1. Class-weaving-based, such as AspectJ and JBoss AOP. Core and crosscutting concerns are implemented independently. Class weaving is the process of integrating the concern implementations to form the final system. Weaving can be performed at compile, load, and run time. Both AspectJ and JBoss AOP are very powerful AOP implementations. They provide field interception, caller side interception, and constructor interception.
  2. Proxy-based, such as Spring AOP, Nanning, and dynaop. With proxies, method invocations on an object can be intercepted to inject custom code. The aforementioned AOP frameworks use JDK dynamic proxy, CGLIB proxy, or both. Unlike the class-weaving-based ones, proxy-based AOP frameworks are simpler and often focus on method interception. Most of the time, Java developers use method interception only. Some proxy-based AOP implementations, such as Spring AOP, provide close integration with AspectJ to take advantage of its capabilities.

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.

Creating a Proxy Factory

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.

Adding an Interceptor

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);
}

AOP Framework in Action

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.

Alternative Implementation Using CGLIB

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.

Conclusion

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.

Resources

Jason Zhicheng Li would like to thank Brian Paulsmeyer and Mark Volkmann for their help in reviewing this article.

Jason Zhicheng Li is a senior software engineer with Object Computing, Inc. in St. Louis, MO.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值