Java动态代理

动态代理在Java中有着广泛的应用,比如Spring AOP、Hibernate数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。
本文主要介绍Java中两种常见的动态代理方式:JDK原生动态代理和CGLIB动态代理。

代理模式

给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。
更多内容参考:代理模式
根据字节码的创建时机来分类,可以分为静态代理和动态代理:

  • 静态代理:程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
  • 动态代理:字节码在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件。

静态代理

静态代理实现

先通过实例来学习静态代理,然后理解静态代理的缺点

  1. 编写UserService接口及其实现类
public interface UserService {
    void update();
    void select();
}

public class UserServiceImpl implements UserService {

    @Override
    public void update() {
        System.out.println("执行update");
    }

    @Override
    public void select() {
        System.out.println("执行select");
    }
}
  1. 假设我们现在想要给update和select方法执行时添加日志功能。为了不改变原有UserService的实现,我们使用静态代理
public class ProxyUserServiceImpl implements UserService {
    /* 被代理对象 */
    private final UserService target;

    public ProxyUserServiceImpl(UserService target) {
        this.target = target;
    }

    @Override
    public void update() {
        before();
        target.update();
        after();
    }

    @Override
    public void select() {
        before();
        target.select();
        after();
    }

    private void before() {
        System.out.println("开始:" + new Date());
    }

    private void after() {
        System.out.println("结束: " + new Date());
    }
}
  1. 测试效果
public class TestStaticProxy {
    @Test
    public void test() {
        UserService userService = new ProxyUserServiceImpl(new UserServiceImpl());
        userService.select();
        userService.update();
    }
}

结果很显然,我们实现了日志功能,且没有侵入原代码。

静态代理的缺点

虽然静态代理实现简单,不侵入原代码,但是缺点也很明显。

  1. 加入现在xxxService,xxxxService同样想添加日志功能,由于代理类要和代理对象实现一样的接口,有以下两种方式:
    • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大。
    • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类。
  2. 当接口改动时,需同时维护代理类和代理对象。

从静态代理到动态代理

仔细想想,其实代理对象只需要满足以下两个条件:

  1. 被代理对象有的方法,代理对象应该也有。
  2. 在代理对象的方法中的某个位置,调用了被代理对象的方法

怎么解决?

  1. 实现同一个接口或者代理对象继承自被代理对象(其实对应了JDK动态代理和CGLib动态代理),但实现接口方式有时候无法满足需求(下文提到)
  2. 观察我们写的静态代理的例子,调用update方法,我们在代理对象的update方法中调用了before,update,after;调用select方法,我们在代理对象的select方法中调用了before,update,after;唯一的区别就在于被调用的方法不一样,那么完全可以把这种结构提取成xxx方法,只需要知道调用的是哪个方法即可,只要我们能获取目标方法的必要信息,就可以在xxx方法中调用before,目标方法,after。调用一个方法,我们需要知道方法名、返回值、参数列表,在Java中,这些不就是一个Method对象里的属性吗?依照这个思路,我们可以把上面的静态代理改写一下
public class ProxyUserServiceImpl implements UserService {
    /* 被代理对象 */
    private final UserService target;

    /* 目标方法的返回值不确定,所以统一返回Object */
    public Object xxx(Method targetMethod, Object[] args){
        before();
        Object ret = targetMethod.invoke(target, args);
        after();
        return ret;
    }

    public ProxyUserServiceImpl(UserService target) {
        this.target = target;
    }

    @Override
    public void update() {
       xxx(update方法,参数列表)
    }

    @Override
    public void select() {
        xxx(select方法,参数列表);
    }

    private void before() {
        System.out.println("开始:" + new Date());
    }

    private void after() {
        System.out.println("结束: " + new Date());
    }
}

现在,只要我们能解析被代理对象中的每个方法,,并在代理对象对应的方法中当成参数传递给xxx方法,如果还有其他方法,那么也这样处理。
这个解析方法的过程,可以用反射来实现,这样,代理对象根据被代理对象动态生成了。
对了,其实这就是JDK动态代理的实现方式。

JDK动态代理

JDK动态代理实现

  1. UserService同上
  2. 编写LogHandler实现InvocationHadler接口
public class LogHandler implements InvocationHandler {
    private final Object target;

    public LogHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object ret = method.invoke(target, args);
        after();
        return ret;
    }

    private void before() {
        System.out.println("开始:" + new Date());
    }

    private void after() {
        System.out.println("结束: " + new Date());
    }
}
  1. 测试效果,与我们编写的静态代理一样,此时如果我们需要改动UserService接口,LogHandler类无需改变。
public static void main(String[] args) {
        /* 将动态生成的字节码文件保存到本地 */
        System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        UserService userService = (UserService) Proxy.newProxyInstance(
                UserServiceImpl.class.getClassLoader(),
                UserServiceImpl.class.getInterfaces(), 
                new LogHandler(new UserServiceImpl()));
        userService.update();
        userService.select();
    }

代理类字节码文件

生成的字节码文件为com/sum/proxy/$Proxy0.class,由IDEA反编译如下:

public final class $Proxy0 extends Proxy implements UserService {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m0;
    private static Method m3;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
	...
    }

    public final String toString() throws  {
        ...
    }

    public final void select() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
       ...
    }

    public final void update() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("com.rufeng.service.UserService").getMethod("select");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m3 = Class.forName("com.rufeng.service.UserService").getMethod("update");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}
  • 代理类继承了Proxy类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法。
  • 类和所有方法都被public final修饰,所以代理类只可被使用,不可以再被继承。
  • 每个接口方法都有一个Method对象来描述,Method对象在static静态代码块中创建,以 m + 数字 的格式命名。
  • 调用方法的时候通过super.h.invoke调用,实际调用的即为我们编写的InvocationHandler类。

两个问题

  1. 为什么JDK动态代理只能代理实现接口的类?假如一个类存在不属于接口的方法还能被代理吗?

    • 根据反编译的字节码文件可以看到,代理类继承自Proxy类,所以代理类无法再继承自被代理类,只能实现相同的接口。本人认为这并不是主要原因,其实在代理类中设计到父类的不过是一个简单的super调用,可能实现接口的方式更符合面向接口设计的规范,以及Java可实现多个接口但不能多继承的特点吧。
    • 假如存在一个方法不属于接口,那么这个方法不会代理类中,自然无法被代理了,这种情况下,只能使用继承的方式实现动态代理了。
  2. 被代理类中方法嵌套调用,只有最外层方法会被代理。
    这个其实很好理解,假如在update方法中调用了select方法,update方法的调用这是代理类,而select方法的调用者来自于method.invoke(target, args)中的target,也就是被代理对象,即未被代理的原方法。

总结

JDK的动态代理是基于反射实现。JDK通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler接口的invoke方法。并且这个代理类是Proxy类的子类。这就是JDK动态代理大致的实现方式。

  • 优点
    1. JDK动态代理是JDK原生的,不需要任何依赖即可使用;
    2. 通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;
  • 缺点
    1. 如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;
    2. JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入;
    3. JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低。

CGLib动态代理

CGLib动态代理实现

  1. UserService不变,编写LogInterceptor
public class LogInterceptor implements MethodInterceptor {
    /**
     * 调用代理类的任何方法都会经过此拦截器
     *
     * @param proxy       代理对象
     * @param method      原方法
     * @param objects     原参数列表
     * @param methodProxy 代理方法
     *                    invoke调用代理对象的方法,然后又被拦截,无限递归;
     *                    invokeSuper调用父类方法即原方法
     * @return Object
     * @throws Throwable throwable
     */
    @Override
    public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before(method);
        Object ret = methodProxy.invokeSuper(proxy, objects);
        /* 不加if无限递归 */
        if (!"hashCode".equals(method.getName()) && !"toString".equals(method.getName())) {
            System.out.println("proxy:" + proxy);
        }
        after(method);
        return ret;
    }

    private void before(Method method) {
        System.out.println("开始:" + method);
    }

    private void after(Method method) {
        System.out.println("结束: " + method);
    }
}
  1. 测试执行
@Test
public void testCGLib() {
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, ".");
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(UserServiceImpl.class);
    enhancer.setCallback(new LogInterceptor());
    UserService service = (UserService) enhancer.create();
    service.update();
}

执行结果如下:

开始:public void com.rufeng.service.impl.UserServiceImpl.update()
执行update
开始:public java.lang.String java.lang.Object.toString()
开始:public native int java.lang.Object.hashCode()
结束: public native int java.lang.Object.hashCode()
结束: public java.lang.String java.lang.Object.toString()
proxy:com.rufeng.service.impl.UserServiceImpl$$EnhancerByCGLIB$$5cd2520d@649d209a
结束: public void com.rufeng.service.impl.UserServiceImpl.update()

字节码文件

/* 继承自被代理对象 */
public class UserServiceImpl$$EnhancerByCGLIB$$5cd2520d 
extends UserServiceImpl implements Factory {
    /* 一个Methodinteceptor对象、各种方法及其方法代理 */
    private boolean CGLIB$BOUND;
    public static Object CGLIB$FACTORY_DATA;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private static Object CGLIB$CALLBACK_FILTER;
    private static final Method CGLIB$update$0$Method;
    private static final MethodProxy CGLIB$update$0$Proxy;
    private static final Object[] CGLIB$emptyArgs;
    private static final Method CGLIB$select$1$Method;
    private static final MethodProxy CGLIB$select$1$Proxy;
    private static final Method CGLIB$equals$2$Method;
    private static final MethodProxy CGLIB$equals$2$Proxy;
    private static final Method CGLIB$toString$3$Method;
    private static final MethodProxy CGLIB$toString$3$Proxy;
    private static final Method CGLIB$hashCode$4$Method;
    private static final MethodProxy CGLIB$hashCode$4$Proxy;
    private static final Method CGLIB$clone$5$Method;
    private static final MethodProxy CGLIB$clone$5$Proxy;

    /* 修改字节码 */
    static void CGLIB$STATICHOOK1() {
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        Class var0 = Class.forName("com.rufeng.service.impl.UserServiceImpl$$EnhancerByCGLIB$$5cd2520d");
        Class var1;
        Method[] var10000 = ReflectUtils.findMethods(
new String[]{"update", "()V", "select", "()V"}, 
(var1 = Class.forName("com.rufeng.service.impl.UserServiceImpl")).getDeclaredMethods());
        CGLIB$update$0$Method = var10000[0];
        CGLIB$update$0$Proxy = MethodProxy.create(var1, var0, "()V", "update", "CGLIB$update$0");
        CGLIB$select$1$Method = var10000[1];
        CGLIB$select$1$Proxy = MethodProxy.create(var1, var0, "()V", "select", "CGLIB$select$1");
        var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
        CGLIB$equals$2$Method = var10000[0];
        CGLIB$equals$2$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$2");
        CGLIB$toString$3$Method = var10000[1];
        CGLIB$toString$3$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$3");
        CGLIB$hashCode$4$Method = var10000[2];
        CGLIB$hashCode$4$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$4");
        CGLIB$clone$5$Method = var10000[3];
        CGLIB$clone$5$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$5");
    }

    final void CGLIB$update$0() {
        super.update();
    }

    public final void update() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            /* 调用拦截器 */
            var10000.intercept(this, CGLIB$update$0$Method, CGLIB$emptyArgs, CGLIB$update$0$Proxy);
        } else {
            super.update();
        }
    }
  • 在其中,我们可以明显地看到Java字节码的身影,CGLib通过操作被代理的字节码生成这个类的子类
  • 同样实现了equals、hashCode等方法及其方法代理,拦截器中有四个参数,比较方便我们在其中针对不同的方法实现不同的拦截效果

总结

CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。

  • 优点

    1. 使用CGLib代理的类,不需要实现接口,因为CGLib生成的代理类是直接继承自需要被代理的类;
    2. CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;
    3. CGLib生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib执行代理方法的效率要高于JDK的动态代理;
  • 缺点:

    1. 由于CGLib的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final类,则无法使用CGLib代理;
    2. 由于CGLib实现代理方法的方式是重写父类的方法,所以无法对final方法,或者private方法进行代理,因为子类无法重写这些方法;
    3. CGLib生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢;

总结

写本文的原因是学习Spring事务时碰到事务失效的场景,以及对Spring的事务传播机制感到疑惑,虽然网上一搜基本能解决问题,但往往知其然不知其所以然。想要弄明白其中的道理,势必从Spring事务的底层实现入手。
关于Spring事务,可以看这篇

Q.E.D.


一切很好,不缺烦恼。