动态代理实现的比较

Published: 25 Feb 2014 Category: Java

动态代理实现的比较

有的时候我们需要拦截方法的调用,来执行我们自己的逻辑。如果你不是Java EE CDI规范的拥趸,也不想使用类似aspectj的AOP框架,你还可以用别的简单有效的方式。

JDK1.5引入了java.lang.reflect.Proxy,你可以通过它给一个指定的接口创建动态代理。程序每次调用动态代理类的时候,都会调用到代理类的InvocationHandler。因此在框架或者库的代码执行之前,你可以动态控制应该执行什么代码。

另外一个JDK代理的实现是字节码框架,比如javassist或者cglib,它们都提供了类似的功能。你可以通过子类来决定你应该调用父类的哪个方法,或者你想要拦截哪个方法。这当然需要引入一个第三方库到你的工程依赖中,并且可能需要时不时的更新下版本,而使用JDK的代理实现,则已经包含在Java的运行环境里面了。

我们来进一步的研究下这三种实现方式。为了比较javassist/cglib和JDK的代理实现,我们需要一个接口以及一个实现,因为JDK的实现机制只支持接口方式,不能通过子类来完成:

public interface IExample {
    void setName(String name);
}
 
public class Example implements IExample {
    private String name;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

为了能把代理对象上的方法调用委托给实际的对象,我们先创建了一个Example对象的实例,放到一个final变量里,然后通过InvocationHandler来调用它。

final Example example = new Example();
InvocationHandler invocationHandler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(example, args);
    }
};
return (IExample) Proxy.newProxyInstance(JavaProxy.class.getClassLoader(), new Class[]{IExample.class}, invocationHandler);

从这个代码示例里面可以看到,创建一个代理非常简单:调用newProxyInstance静态方法,并且提供一个类加载器,一个代理可能实现的接口的数组,还有一个InvocationHandler的实现。为了方便演示,我们这个实现只是把调用转给了前面刚创建的一个Example实例。在实际过程中,你可以根据调用 方法名和参数来执行一些更复杂的操作。

现在我们来看下javassist的实现:

ProxyFactory factory = new ProxyFactory();
factory.setSuperclass(Example.class);
Class aClass = factory.createClass();
final IExample newInstance = (IExample) aClass.newInstance();
MethodHandler methodHandler = new MethodHandler() {
    @Override
    public Object invoke(Object self, Method overridden, Method proceed, Object[] args) throws Throwable {
        return proceed.invoke(newInstance, args);
    }
};
((ProxyObject)newInstance).setHandler(methodHandler);
return newInstance;

这里我们创建了一个ProxyFactory,它需要知道哪个类需要生成子类。然后我们用这个代理工厂生成了一个Class对象,这个对象是可以无限使用的。这里的MethodHandler的作用和InvocationHandler差不多,当实例的方法被调用的时候,它们也会对应的被调起。这里我们还是只调用了一下Example的实例。

最后一个但同样也很重要的,我们来看下cglib版的实现:

final Example example = new Example();
IExample exampleProxy = (IExample) Enhancer.create(IExample.class, new MethodInterceptor() {
    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        return method.invoke(example, args);
    }
});
return exampleProxy;

在cglib里面,我们用Enhancer来实现一个指定的接口,它需要传入一个MethodInterceptor的实例。这个回调方法的实现跟javassist的例子差不多,也是通过反射的API调用了Example的一个实现。

现在我们看到了三个版本的不同实现,我们来分析下它们运行时的表现。这里写了一个简单的单元测试,用来衡量这三种实现的执行时间:

@Test
public void testPerformance() {
    final IExample example = JavaProxy.createExample();
    long measure = TimeMeasurement.measure(new TimeMeasurement.Execution() {
        @Override
        public void execute() {
            for (long i = 0; i < JavassistProxyTest.NUMBER_OF_ITERATIONS; i++) {
                example.setName("name");
            }
        }
    });
    System.out.println("Proxy: "+measure+" ms");
}

我们对这个操作重复执行多次,来对JVM进行压测,以便Hotspot编译器对热点路径生成本地代码。下面的图表是这三种实现的平均运行时间:

为了更好的评估各个代理实现的效率,我们也列出了Example对象的方法正常调用的执行时间(“No Proxy”)。首先我们可以确定的是,代理的实现和原生实现相比,效率至少慢了十倍。同时我们还注意到几种代理实现的差别。令人意外的是JDK的代理类和cglib的实现一样快。只有javassist的版本,和cglib想比,差了大约一倍。

结论:运行时代理非常简单易用,你可以使用不同的实现方式。JDK的代理只支持接口而javassist和cglib允许你通过子类来实现。它们在运行时的性能表现比标准实现要慢十倍,同时这三种实现的表现也各不相同。

原文链接