在讲解动态代理前我们先聊聊什么是静态代理。
假设有一天领导突发奇想,给你下发了一个需求:
统计项目中所有类的方法执行耗时。
在拿到需求的那一刻,脑海中冒出来的第一个想法是:
在每个方法的第一行和最后一行加上时间埋点,再打印一行日志不就完事了。
抄起键盘准备开干,想了想又开始犹豫了:
在每个方法都加几行代码,这不是侵入式修改吗?
听架构师大佬说这样的场景可以用代理模式,那尝试一下,具体做法如下。
(1)为工程里每个类都写一个代理类,让它与目标类实现同一个接口。图中标红色的就是代理类。
(2)在代理类里面维护一个目标实现类,调用代理类的方法时还是会去调用目标类的方法,只不过在前后加了一些其他逻辑代码。也就是说后面客户端不需要直接调用目标实现类,只需要调用代理类即可,这样就间接调用了对应方法。
用一个公式总结一下:代理类 = 增强代码 + 目标实现类 。
下面这个图中,计算耗时的逻辑就是增强代码。
(3)在所有 new 目标类的地方都替换为 new 代理类,并将目标类作为构造方法参数传入;所有使用目标类调用的地方全部都替换为代理类调用。
如果你看懂了上面的实现方法,那么恭喜你已经掌握了静态代理的核心思想。
静态代理的思路非常简单,就是给每一个目标实现类写一个对应的代理实现类,但是如果一个项目有几千甚至有几万个类,这个工作量可想而知。
前面我们还隐藏了一个假设:每个类都会实现一个接口。那如果一个类没有实现任何接口,代理类如何实现呢?
好了,我们来总结一下静态代理的缺点:
静态代理需要针对每个目标实现类写一个对应的代理类,如果目标类的方法有变动,代理类也要跟着动,维护成本非常高。
静态代理必须依赖接口。
既然知道了静态代理的缺点,那有没有办法实现少些或者不写代理类来实现代理功能呢?答案是有,动态代理。
在正式介绍动态代理前,我们先复习一下 java 中对象是如何创建的。
我们在项目中使用一行代码就可以简单创建一个对象,实际上经过的流程还是很复杂的。
// 创建对象
A a = new A();
(1)java 源文件经过编译生成字节码文件(.class结尾);
(2)类加载器将 class 文件加载到 JVM 内存中,就是常说的方法区,生成 Class 对象;
(3)执行 new,申请一块内存区域,紧接着创建一个对象放在 JVM 对象,准确地说是新生代;
上面的流程中提到了 Class 对象,有两个概念初学者很容易混淆:Class 对象 和 实例对象。
Class 对象简单来说就是 Class 类的实例,Class 类描述了所有的类;实例对象是通过 Class 对象创建出来的。
从上面的分析可以看出来,要想创建一个实例,最最关键的是获得 Class 对象。
有些同学可能有疑问了,我写代码的时候创建对象没有用到 Class 对象呀,那是因为 Java 语言底层帮你封装了细节。Java 语言给我们提供了new 这个关键字,new 实在太好用了,一行代码就可以创建一个对象。
我们再回到前面讲的静态代理,静态代理最重要的是提前写一个代理类,有了代理类就可以 new 一个代理对象。但是每次都去写一个代理类是不是太麻烦了?!
再稍微扩展一下思路,有没有办法不写代理类还能生成一个代理对象呢?可以,上面讲的通过代理类 Class 对象就可以生成代理对象,那如何获取代理类 Class 对象呢?我们接着往下看。
Class对象包含了一个类的所有信息,如:构造方法、成员方法、成员属性等。
如果我们不写代理类,似乎无法获得代理类 Class 对象,但稍稍动一动脑:代理类和目标类实现的是同一组接口,是不是可以通过接口间接获得代理类 Class 对象。
代理类和目标类实现了同一组接口,这就说明他们大体结构都是一致的,这样我们对代理对象的操作都可以转移到目标对象身上,代理对象只需要专注于增强代码的实现。
上面说了这么多其实是在引入动态代理的概念,动态代理相对于静态代理最大的区别就是不需要事先写好代理类,一般在程序的运行过程中动态产生代理类对象。
JDK 原生提供了动态代理的实现,主要是通过java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
这两个类配合使用。
Proxy类有个静态方法,传入类加载器和一组接口就可以返回代理 Class 对象。
public static Class> getProxyClass(ClassLoader loader, Class>... interfaces)
这个方法的作用简单来说就是,会将你传入一组接口类的结构信息"拷贝"到一个新的 Class 对象中,新的 Class对象带有构造器是可以创建对象的。
一句话总结:Proxy.getProxyClass()
这个静态方法的本质是以 Class 造 Class。
拿到了 Class 对象,就可以使用反射创建实例对象了:
// Proxy.getProxyClass 默认会生成一个带参数的构造方法,这里指定参数获取构造方法
Constructor constructor = aClazz.getConstructor(InvocationHandler.class);
// 使用反射创建代理对象
A a1 = constructor.newInstance(new InvocationHandler() {});
眼尖的同学已经看到了,创建实例的时候需要传入一个 InvocationHandler 对象,说明代理对象中必然有一个成员变量去接收。在调用代理对象的方法时实际上会去执行 InvocationHandler 对象的 invoke方法,画个图理解一下:
invoke 方法里可以写增强代码,然后调用目标对象 work 方法。
总结一下流程:
(1)通过 Proxy.getProxyClass() 方法获取代理类 Class 对象;
(2)通过反射 aClazz.getConstructor() 获取构造器对象;
(3)定义InvocationHandler类并实例化,当然也可以直接使用匿名内部类;
(4)通过反射 constructor.newInstance() 创建代理类对象;
(5)调用代理方法;
看了上面的流程,是不是觉得比静态代理还要繁琐,有没有更加优雅的方法?当然有!
为了尽量简化操作,JDK Proxy 类直接提供了一个静态方法:
public static Object newProxyInstance(ClassLoader loader, Class>[] interfaces, InvocationHandler h)
这个方法传入类加载器、一组接口和 InvocationHandler 对象直接就可以返回代理对象了,有了代理对象就可以调用代理方法了,是不是 so easy?!
newProxyInstance方法本质上帮我们省略了获取代理类对象和通过代理类对象创建代理类的过程,这些细节全部隐藏了。
所以真正在项目中直接使用newProxyInstance这个方法就好了,上面讲的那些流程是为了方便大家理解整个过程。
看到这里我相信大家应该能看懂JDK 原生动态代理了。
JDK 动态代理,一旦目标类有了明确的接口,完全可以通过接口生成一个代理 Class 对象,通过代理 Class 对象就可以创建代理对象。
这里可以看出 JDK 动态代理有个限制必须要求目标类实现了接口,那加入一个目标类没有实现接口,那岂不是不能使用动态代理了?
cglib 就是为了实现这个目标而出现的,利用asm开源包对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
我们通过几个问题简单对比一下 JDK 和 cglib 动态代理的区别。
问题 1:cglib 和 JDK 动态代理的区别?
JDK 动态代理:利用 InvocationHandler 加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
cglib 动态代理:利用ASM框架,将目标对象类生成的class文件加载进来,通过修改其字节码生成代理子类
问题 2:cglib 比 JDK快?
cglib底层是ASM字节码生成框架,在 JDK 1.6 前字节码生成要比反射的效率高
在 JDK 1.6 之后 JDK 逐步对动态代理进行了优化,在 1.8 的时候 JDK 的效率已经高于 cglib
问题 3:Spring框架什么时候用 cglib 什么时候用 JDK 动态代理?
目标对象生成了接口默认用 JDK 动态代理
如果目标对象没有实现接口,必须采用cglib
当然如果目标对象使用了接口也可以强制使用cglib
使用代理模式可以避免侵入式修改原有代码。代理分为:静态代理和动态代理。
静态代理要求目标类必须实现接口,通过新建代理类并且与目标类实现同一组接口,最终实现通过代理类间接调用目标类的方法。
关于代理类,可以用一个公式总结一下:代理类 = 增强代码 + 目标实现类 。
静态代理必须要求提前写好代理类,使用起来比较繁琐,这就引入了动态代理。
动态代理是在程序运行的过程中动态生成代理类,根据实现方式的不同进而分为:JDK原生动态代理和CGLIB动态代理。
JDK 动态代理通过反射+InvocationHandler 机制动态生成代理类来实现,要求目标类必须实现接口。cglib 不要求目标类实现接口,通过修改字节码方式生成目标类的子类,这就是代理类。
动态代理不仅在 RPC 框架中被使用,还在其他地方有着广泛的应用场景,比如:Spring AOP、测试框架 mock、用户鉴权、日志、全局异常处理、事务处理等。