学习 Java 动态代理

882 阅读12分钟

前言

代理是编程中一个重要的概念,也是一种重要的设计模式。Spring 框架中大量的使用了代理,比如 Spring事务管理就是通过代理对象实现的。掌握代理技术是我们学习框架源码的基础之一。

如何理解代理

其实 代理 就是简单的字面意思,把 元素A 的一件事情交给另一个 元素B 代为处理,在 元素B 的处理过程中包含了 元素A 的处理过程,同时在 元素A 的处理过程前后上又增加了一些额外的逻辑。没错,它就是 AOP 最典型的实现。

这里我想举一个生活中的例子。

2025 年我(A)准备举办婚礼,由于媳妇儿是高中同学,高中班主任(S)在我住校期间对我们寝室以及我本人都比较好,我想邀请她参加我的婚礼。但是由于很多年没有联系,考虑到参加婚礼她肯定要随份子,我不知道她内心的意愿,所以我把邀请她参加婚礼的事情委托给另一个室友(B),在我邀请前探一探口风。

BA 邀请 S 之前先和 S 寒暄一番探得口风 S 不排斥参加 A 的婚礼,并不在乎份子钱,于是告诉 A,随即 A 电话邀请 S 于 xx 日期参加自己的婚礼,S 同意后,B 将婚礼名单多添加一人,请柬多制作一份。

在上面的例子中,原始对象 A,代理对象 B,核心业务逻辑邀请动作仍然是 A 执行的,代理对象 BA 执行的前后做了一些额外的操作。如下图,这就是一个简单的代理案例。

image.png

整个行为是 代理对象 B 发起的,但是核心动作仍然是原始对象 A 处理,只是在这个处理过程的前后 B 加了一些额外的处理逻辑。

为什么需要代理

上面的例子中,我们可能会疑惑,为什么这两件事要让 代理对象B 去实现,A 在方法中自己实现不可以吗?当然可以,但是我们考虑这样一种场景,如果对于一类业务方法,几百上千个业务方法都需要这样的逻辑,那么我们不可能每一个业务方法都写上重复的前置处理和后置处理的代码。

其实这就引出了 Spring 事务管理,我们不可能把所有业务方法的事务都内嵌在业务方法中开启事务、回滚事务。那样代码太冗余,也难以维护,所以才用代理的方式把事务交给 Spring 管理。

静态代理

静态代理是通过编码显示定义一个代理类来实现对目标类的代理,代理类和目标类实现同样的接口,通过代理类调用目标类的方法,在目标类的方法前后添加代理的处理逻辑。步骤如下

  • 定义一个接口及其实现类
  • 创建一个代理类同样实现这个接口
  • 在代理类中包裹目标对象,然后在代理类的方法调用目标类中的对应方法。在调用目标方法的前后自定义增强逻辑

代码实现

首先定义一个接口

interface Invite {
    void call();
}

然后定义目标类

public class Person implements Invite {
    @Override
    public void call() {
        System.out.println("电话邀请...");
    }
}

定义代理类

public class Proxy implements Invite {
    private Person person;
    public Proxy(Person person) {
        this.person = person;
    }
    @Override
    public void call() {
        System.out.println("前置处理...探口风");
        person.call();
        System.out.println("后置处理...制作请柬");
    }
}

这样我们就使用了代理类包裹一个原始类来执行逻辑,并且在执行原始类真正的逻辑前后自定义处理逻辑,这里我们用两行打印语句模拟自定义处理逻辑。

public class StaticProxyTest {
    public static void main(String[] args) {
        Person a = new Person();
        Proxy proxy = new Proxy(a);
        proxy.call();//代理执行
    }
}

可以看到静态代理很简单,我们需要理解的不是代码,而是它的设计思想,设计模式。

静态代理的不足

通过上面的例子我们能够发现,静态代理需要我们编译阶段就定义好代理类。如果系统中很多类都需要代理的话,那么需要为每一个类都创建代理类,会使得增加很多代码,也难以维护。

另外代理类和被代理类的耦合性很强。因为在静态代理中,代理类需要显式地实现与被代理类相同的接口或继承相同的父类,这使得代理类的可复用性和灵活性受到限制。比如接口如果新增方法,那么代理类中也需要修改代码。

为了解决这两个问题,所以 Java 设计了动态代理。

JDK 动态代理

动态代理是指程序运行时动态创建代理类和代理对象,然后将代理对象的业务方法(上面案例的 call())委托给一个 InvocationHandler.invoke() 方法。在这个回调函数中调用目标对象的业务方法,在目标方法前后执行增强逻辑。

代码实现

既然是运行时动态生成,我们不难猜出,JDK 中的动态代理是用反射技术实现的。InvitePerson 类不变,我们不再需要自己定义代理类,直接使用 JDK 提供的动态代理类 java.lang.reflect.Proxy

public class DynamicProxyTest {
    public static void main(String[] args) {
        Person a = new Person();
        Invite invite = (Invite) Proxy.newProxyInstance(DynamicProxyTest.class.getClassLoader(), new Class[]{Invite.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("前置处理...探口风");
                //调用原始对象的 call 方法
                method.invoke(a, args);
                System.out.println("后置处理...制作请柬");
                return null;
            }
        });
        //调用代理对象的 call 方法,它被委托给了 InvocationHandler.invoke()
        invite.call();
    }
}

注意这里的 Proxy 不是我们上面自定义的,是 JDK 反射包下面给我们提供的类,用 newProxyInstance() 方法可以动态创建一个代理类和对象。该方法接受以下三个参数

  • ClassLoader loader :类加载器
  • Class<?>[] interfaces:要代理哪个接口类型
  • InvocationHandler h : 回调函数,调用代理对象的方法时会回调,在这里写我们的增强逻辑。

注意动态代理的代理对象的行为都是委托给 InvocationHandler 的,换句话说,代理对象调用任何方法都是在执行 InvocationHandler.invoke()

Arthars 反编译代理类源码

由于动态代理的代理类和对象都是运行时生成的,它越过了编译阶段,直接到达字节码阶段,那么我们有什么办法能够在程序运行时一睹代理类源码的芳容呢?

阿里巴巴提供了一个工具 Arthars,它可以连接上我们运行时的进程,反编译某个类。首先去官网下载工具 Arthas文档和下载 然后根据文档教程使用即可,下面我们写一段示例代码来查看我们运行时动态生成的代理类源码是什么样子。

对上一节的例子稍微修改

public class DynamicProxyTest {
    public static void main(String[] args) throws IOException {
        Person a = new Person();
        Invite invite = (Invite) Proxy.newProxyInstance(DynamicProxyTest.class.getClassLoader(), new Class[]{Invite.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("前置处理...探口风");
                method.invoke(a, args);
                System.out.println("后置处理...制作请柬");
                return null;
            }
        });
        //打印代理类类型
        System.out.println(invite.getClass());
        invite.call();
        //接受控制台输入,让程序不要停止
        System.in.read();
    }
}

运行起来得到代理类的完整类 class 对象。

image.png

然后打开命令窗口启动 arthas。执行下面的命令

java -jar ${your arthas path}\arthas-boot.jar

根据控制台提示选择要连接 Java 的进程,然后使用下面的命令查看反编译的类源代码。

jad com.example.demoproxy.proxy.$Proxy0

之后命令窗口就会展示动态代理出来的类的源代码。这里截取关键部分

image.png

可以看到代理类的源码,call() 其实调用的就是 InvocationHandler.invoke()。而 invoke() 中包含了我们代理类的增强逻辑。

JDK 动态代理的缺陷

  • 只能对实现了接口的类进行代理。假如我们代理的目标类没有实现任何接口,就无法被代理。

为了弥补 JDK 动态代理的缺陷,我们还有一种动态代理的方式:CGLIB 代理

Cglib 动态代理

Cglib 代理是 Spring 提供的动态代理技术,它不需要目标类实现接口。

代码实现

改造我们的 Person 类,不实现任何接口

public class Person {
    public void call() {
        System.out.println("电话邀请...");
    }
}

编写测试代码

public class DynamicProxyTest {
    public static void main(String[] args) throws IOException {
        Person a = new Person();
        Person person = (Person) Enhancer.create(Person.class, new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                System.out.println("前置处理...探口风");
                method.invoke(a, args);//反射调用目标方法
                //proxy.invoke(a, args);//非反射调用目标方法
                System.out.println("后置处理...制作请柬");
                return null;
            }
        });
        person.call();
    }
}

这里回调函数参数类型是 org.springframework.cglib.proxy.Callback 接口的函数,MethodInterceptor 是它的子接口。Enhancer.create() 还有其他重载的方法,可以传递多个回调函数。Spring 框架中通常使用的是下面的方法来代理

public static Object create(Class superclass, Class[] interfaces, CallbackFilter filter, Callback[] callbacks)

如果有兴趣的话也可以使用 Arthas 查看 Cglib 代理出来的类源码,比 JDK 的动态代理要复杂一些

代理类的类型

我们在上面的代码示例中,打印代理类的类型,以及其父类的类型

//...省略
System.out.println("代理类类型:"+person.getClass());
System.out.println("代理类父类类型:"person.getClass().getSuperclass());
person.call();

执行程序可以得到结果

代理类类型:class com.example.demo1springtransaction.proxy.Person$$EnhancerByCGLIB$$89914536
代理类父类类型:class com.example.demo1springtransaction.proxy.Person
前置处理...探口风
电话邀请...
后置处理...制作请柬

发现代理类的父类居然是目标类,由此可得出结论,我们使用 Cglib 代理出来的代理对象实际上是被代理的对象的一个子类。它是通过重写父类的方法进行增强的。

如果目标类的类上或者方法上用了 final 关键字修饰,那么 Cglib 代理增强的功能将会失效,因为 final 类不能被继承,final 方法不能被重写。

JDK 动态代理和 CGLIB 对比

适用范围

如果需要被代理的类未实现接口,那么没有选择,在这两者中只能选择 Cglib 的方式代理。

如果需要被代理的的类或者方法使用了 final 关键字修饰,那么两者中没有选择,只能使用 JDK 的动态代理。

性能

网上有人说 JDK 的动态代理 Proxy 是通过反射调用方法的。所以性能要稍微比 Cglib 低。其实这个问题要从两个阶段来看

  • 创建对象阶段

创建对象的阶段,由于 Proxy 是直接用反射 Constructor 类构造代理对象,而 Cglib 源码在使用 Constructor 创建代理对象前还有一堆复杂的逻辑处理,所以在创建对象这个动作上 Proxy 的效率要高于 Cglib

  • 调用方法阶段

调用方法阶段由于 Proxy 是通过反射调用,Cglib 因为重写了父类方法,相当于用子类对象直接调用自己的方法,按道理说性能上 Cglib 要稍微高于 Proxy

这里我们写一个 demo 来测试,Spring 的版本是 6.1.8,代码如下


//Proxy 的目标代理接口
interface Invite {
    void call();
}

class Person1 implements Invite {
    @Override
    public void call() {}
}

//Cglib 的目标代理类
class Person2 {
    public void call() {}
}

//测试类
public class CompareProxyAndCglib {
    public static void main(String[] args) {
        f1();
        f2();
    }
    //Proxy 方式
    private static void f2() {
        List<Invite> list = new ArrayList<>(10000000);
        long start = System.currentTimeMillis();
        for(int i=0;i<10000000;i++){
            Invite invite = (Invite) Proxy.newProxyInstance(CompareProxyAndCglib.class.getClassLoader(), new Class[]{Invite.class}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    method.invoke(new Person1(), args);
                    return null;
                }
            });
            list.add(invite);
        }
        long end = System.currentTimeMillis();
        System.out.println("Proxy 创建代理对象花费时间:"+(end-start));

        long start2 = System.currentTimeMillis();
        list.forEach(Invite::call);
        long end2 = System.currentTimeMillis();
        System.out.println("Proxy 调用代理方法花费时间:"+(end2-start2));
    }
    
    //Cglib 方式
    private static void f1() {
        List<Person2> list = new ArrayList<>(10000000);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            Person2 person = (Person2) Enhancer.create(Person2.class, new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    proxy.invoke(new Person2(), args);
                    return null;
                }
            });
            list.add(person);
        }
        long end = System.currentTimeMillis();
        System.out.println("CGLIB 创建代理对象花费时间:" + (end - start));

        long start2 = System.currentTimeMillis();
        list.forEach(Person2::call);
        long end2 = System.currentTimeMillis();
        System.out.println("CGLIB 调用代理方法花费时间:" + (end2 - start2));
    }
}

如上代码,我们分别用 Proxy 代理和 Cglib 代理来测试创建 1000w 次代理对象和调用 1000w 次代理对象的方法所耗费的时间。

  • JDK8 的结果

image.png

  • JDK11 的结果

image.png

  • JDK22 的结果

image.png

经过多次测试得出上述结果,从上述结果来看,在高版本 JDK 中,创建对象阶段 Proxy 的性能明显高于 Cglib。从调用代理方法的阶段来看两者没有明显的差别。这也能够说明为什么 Spring 框架中如果我们需要代理的类实现了接口的话,它优先帮我们选择 JDK 的动态代理。因为 整体性能来说,JDK 的动态代理是高于 Cglib 的。

另外我们可以注意到,随着 JDK 版本的升级,不管是调用代理方法花费的时间还是创建代理对象花费的时间,都显著的在减少,这说明随着 JDK 版本的升级,性能是在逐步提高的。

也许你可能会有这样的想法,那就是这花费时间的减少也许并不是动态代理的性能升级了,而是相关的 GC 垃圾收集器更高级了,或者一些 GC 参数调整的更好了。没错,不可否认的确有很大的可能是因为 GC。但是这不影响我们的出一个总结论,在生产环境中,条件允许的情况下升级 JDK 版本绝对是可以提高程序整体性能的。所以我们不应该抱着 Java8 用到死的想法~~

从 Spring 事务来看 Cgilib 代理

如下代码

@Service
@Slf4j
public class TestService {
    @Transactional
    public void test(){}
}

事务开启到结束的步骤大致如下

  • 创建 Bean 的时候解析 @Transactional 注解,将方法名作为 key,事务信息(传播行为、隔离级别等)作为 value 存储到本地缓存中。

  • Spring 容器启动后在 Bean 创建的过程中会判断是否需要代理这个 Bean。由于有 @Transactional 注解,并且没有实现接口,所以会使用 CGLIB 代理,创建代理对象。

  • 创建代理对象的时候先拿到 BeanFactoryTransactionAttributeSourceAdvisor 绑定回调通知,它里面有一个实现 MethodInterceptor 接口的实现类 TransactionInterceptor 注意这里不是 cglib 包下面的 MethodInterceptor,是 aop 包下的通知接口,全类名 org.springframework.transaction.interceptor.TransactionInterceptor。这里 Cglib 是使用下面的 API 创建代理对象的

public static Object create(Class superclass, Class[] interfaces, CallbackFilter filter, Callback[] callbacks) {
    Enhancer e = new Enhancer();
    e.setSuperclass(superclass);
    e.setInterfaces(interfaces);
    e.setCallbackFilter(filter);
    e.setCallbacks(callbacks);
    return e.create();
}

这个 API 可以传递多个回调函数,这里的 org.aopalliance.intercept.MethodInterceptor 是一个通知,创建代理对象时被封装成一个 org.springframework.cglib.proxy.Callback 回调函数。

  • 调用目标方法的时候通过代理,会调用刚刚被封装成 Callback 的回调函数,在回调函数中调用 TransactionInterceptor.invoke(), 这里面会调用 invokeWithinTransaction(),进行事务前置处理,开启事务等,事务信息从第一步的缓存中拿到。

  • 前置处理完毕,执行真正的 TestService.service() 业务方法

  • 执行目标方法后,执行拦截器后置处理方法,提交或者回滚事务

总览来看 Spring 的事务其实实现很简单,就是将原业务方法代理,在代理方法中前置增强就是先开启数据库事务,然后执行业务操作,后置增强就是判断是提交还是回滚事务。

结语

动态代理在我们平时写业务代码的时候很少用到,但是在各大框架中应用广阔,想要熟练的掌握一些框架的原理,动态代理是我们必须要掌握的技能,这是我们阅读 Spring 源码的基础之一。

如果这篇文章对你有帮助,记得点赞加关注!你的支持就是我继续创作的动力!