首先思考这样两个问题。
CGLIB没有使用反射,那它是如何实现运行时动态调用的呢?
什么是CGLIB无限循环调用问题,怎么产生的?
上一篇文章《一文搞懂Java动态代理:为什么Mybatis Mapper不需要实现类?》介绍了动态代理的前世今生,虽然讲了很多基础的内容,但是大家给我的反馈是都很感兴趣,想要我接着聊一下CGLIB,这不就来了吗。为了尽量将CGLIB写的通俗易懂,我也是查阅了各种资料,并且以身试坑,终于加班加点的赶出来了。喜欢“IT果果日记”文章的朋友建议收藏+关注,方便以后复习查阅。如需转载请注明文章来源及原地址。支持原创,侵权必究。
目录
CGLIB动态代理
CGLIB(Code Generation Library)是一个代码生成类库,它可以为没有提供实现接口的类生成代理,这一点和JDK动态代理不一样,JDK动态代理只能支持对目标接口的代理。为什么CGLIB可以支持对类生成代理?本文后面会介绍。
与JDK动态代理利用反射实现的原理不同,CGLIB相对于JDK动态代理性能更好,因为CGLIB底层采用的是轻量的字节码处理框架ASM,动态生成字节码,CGLIB还采用FastClass机制,其性能和普通代码没有区别。所以当有一定性能要求时,CGLIB比JDK动态代理更加合适。
我们先来看下CGLIB动态代理的实现。首先引入maven依赖。
cglib
cglib-nodep
3.2.4
接着对目标对象增强。JDK动态代理增强的是接口,而CGLIB增强的是类。这里有两个目标对象,一个是给原告收集证据,另一个是给原告打官司。通过实现律师代理对象代替原告做这些事情。
public class LawEvidenceImpl implements LawEvidence {
@Override
public void collect() {
System.out.println("原告收集证据!");
}
}
// ...
public class LawsuitImpl implements Lawsuit {
@Override
public void lawsuit() {
System.out.println("原告打官司!");
}
}
所以这里我们直接增强LawEvidenceImpl类和LawsuitImpl类。还有一个不同于JDK动态代理的点是JDK的增强方式是实现InvocationHandler接口,而CGLIB的增强方式是实现MethodInterceptor接口。CGLIB的增强代码如下:
public class LawInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.print("律师向原告了解案情,并代替");
Object result = methodProxy.invokeSuper(obj, objects);
return result;
}
}
MethodInterceptor接口的intercept方法有4个参数:
- obj: 增强的对象
- method: 被拦截的方法
- objects: 被拦截方法的参数
- methodProxy: java.lang.reflect.Method类的代理类,可以实现对目标类方法的调用
需要强调一点的是MethodInterceptor接口里如果想要调用原目标对象的方法,必须使用methodProxy#invokeSuper方法,methodProxy还有一个methodProxy#invoke方法,如果使用invoke方法会发生无限循环调用的问题。你可以简单理解为invokeSuper是调用的目标对象的原方法,而invoke是调用的代理对象的增强方法,这就导致了程序再一次进入到增强的拦截方法intercept里,周而复始。具体的原因我会在后面讲解CGLIB原理FastClass机制的时候介绍。
LawEvidenceImpl类和LawsuitImpl类的代码与前文保持一致就行了,现在我们来写一个客户端看下CGLIB的使用与JDK动态代理有哪些不同:
public class Client {
public static void main(String[] args) {
String path = System.getProperty("user.dir");
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, path);
Enhancer evidenceEnhancer = new Enhancer();
evidenceEnhancer.setSuperclass(LawEvidenceImpl.class);
evidenceEnhancer.setCallback(new LawInterceptor());
LawEvidenceImpl evidenceProxy = (LawEvidenceImpl) evidenceEnhancer.create();
evidenceProxy.collect();
Enhancer lawsuitEnhancer = new Enhancer();
lawsuitEnhancer.setSuperclass(LawsuitImpl.class);
lawsuitEnhancer.setCallback(new LawInterceptor());
LawsuitImpl lawsuitProxy = (LawsuitImpl) lawsuitEnhancer.create();
lawsuitProxy.lawsuit();
}
}
执行结果:
CGLIB和JDK动态代理的实现步骤有其相似之处,都需要实现一个增强接口,再通过某种创建语句得到代理对象,最后调用代理对象的增强方法。代码层级结构如图所示。
这里还要说明一下CGLIB相对于JDK动态代理的一个不同点,这个不同点不注意的时候很可能把自己给坑了。在写MethodInterceptor#intercept拦截方法的实现逻辑时,最好指定你想要增强目标对象的哪些方法,否则CGLIB默认会增强目标对象及其父类的所有非final方法、非private方法。
public class LawInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if (!method.getName().equals("collect") && !method.getName().equals("lawsuit")) {
return null;
}
System.out.print("律师向原告了解案情,并代替");
Object result = methodProxy.invokeSuper(o, objects);
return result;
}
}
如果代码不像上面那样加一个判断,那么CGLIB会对目标对象的父类方法进行增强,例如Object.class类的toString和hashCode方法。
我在调试的时候发现一个非常有趣的现象,当我直接run代码时,返回如下图正常的结果。
但当我debug断点运行代码时,控制台就会重复打印 "律师向原告了解案情,并代替"。
我把代码里加上一段日志,就能发现其问题的原因在哪里了。
public class LawInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("增强方法:" + method.getName());
System.out.print("律师向原告了解案情,并代替");
Object result = methodProxy.invokeSuper(o, objects);
return result;
}
}
结果显示重复的打印日志是因为目标对象的父类Object.class的方法toString和hashCode也被增强了,而且这两个增强方法只在断点时才被调用,run的时候不会被调用,隐藏的非常深。
反编译
CGLIB同样可以查看生成的class文件,在客户端Client的开头加一段代码即可。
String path = System.getProperty("user.dir");
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, path);
再次运行Client,发现项目目录下生成了6个Class文件,CGLIB会为每个目标对象生成3个Class文件,本文的示例中因为有两个目标对象,所以一共生成了6个Class文件。
CGLIB生成的Class文件命名以$$拼接而成,生成的代理类名规则如下。
目标类名$$EnhancerByCGLIB$$随机字符串
例如目标类名是LawEvidenceImpl,随机字符串是a794660b,中间加上固定名EnhancerByCGLIB,所以得到代理类名是LawEvidenceImpl$$EnhancerByCGLIB$$a794660b。
CGLIB生成的代理类继承的是目标类(被代理类)。
与JDK动态代理不一样,JDK动态代理的代理类继承的是Proxy类,并且实现了目标接口(被代理接口)。这也是为什么JDK动态代理不能对类代理的原因。
CGLIB代理的流程:
- 利用Enhancer类的create方法创建增强对象,增强对象的类型是目标类(LawEvidenceImpl)的子类,所以增强对象继承了目标类的方法(collect方法)
- 增强对象调用目标方法(collect)时,会触发拦截器的intercept方法
- 拦截器的intercept方法实现增强逻辑,并且调用目标方法。
那么问题来了,MethodProxy是如何通过invokeSuper方法调用目标方法的呢?是和JDK动态代理一样使用的Java反射实现的吗?
FastClass机制
CGLIB显然不是通过Java反射实现对目标方法的动态调用的。CGLIB采用的是FastClass机制,通过建立目标方法的索引,调用时查找索引就能得到真正的目标方法,这种方式的性能要优于Java反射。
要想理解FastClass机制的原理,我们先从一个简单的示例入手,下面的示例代码模仿了FastClass的工作原理,实现了对Java对象ProxyObject动态调用其方法的功能。
/** FastClassDemo */
public class FastClassDemo {
public static void main(String[] args) {
ProxyObject realObject = new ProxyObject();
FastClass fc = new FastClass();
int index = fc.getIndex("f()V"); // V表示方法返回的是void
fc.invoke(index, realObject, null);
}
}
/** 代理对象,FassClass动态调用其方法 */
class ProxyObject {
public void f() {
System.out.println("f method");
}
public void g() {
System.out.println("g method");
}
}
/** FastClass */
class FastClass {
public Object invoke(int index, Object o, Object[] par) {
ProxyObject realObject = (ProxyObject) o;
switch (index) {
case 1:
realObject.f();
return null;
case 2:
realObject.g();
return null;
}
return null;
}
public int getIndex(String signature) {
switch (signature.hashCode()) {
case 3078479: // 3078479 是字符串f()V的hash code
return 1;
case 3108270:
return 2;
}
return -1;
}
}
在FastClass机制出来以前,我们普遍使用Java反射实现动态调用,只要知道某个对象及其方法名即可。但是Java反射的最大问题是其太重,它要经过一些列权限校验、JVM方法区查找方法定义、native方法调用等。FastClass则不同,它和普通调用的区别仅仅是它要经过一层方法索引的查找。
上面示例中,在已经知道代理对象ProxyObject及其方法名"f()V"之后,只需要两个步骤:
- 通过我们自定义的FastClass#getIndex()方法,得到方法"f()V"的索引;
- 通过FastClass#invoke()方法,调用方法"f()V"。
理解了上面的简单示例,我们再来分析一下CGLIB是如何使用FastClass机制的。前面我们留下一个疑问,MethodProxy是如何通过invokeSuper方法调用目标方法的呢?
要想得到这个问题的答案,我们可以直接进去调试一下看看。下面截图我把MethodProxy的invoke方法和invokeSuper方法的代码都展示出来,目的就是想告诉大家两个方法之间的区别。可以看到invoke方法使用的是FastClassInfo里的f1;而invokeSuper方法使用的是f2。
f1被赋予的是LawEvidenceImpl$$FastClassByCGLIB$$30055069,从名字我们就能知道它是LawEvidenceImpl这个目标对象的FastClass,i1是collect方法的索引,其值为0;
f2被赋予的是LawEvidenceImpl$$EnhancerByCGLIB$$a794660b$$FastClassByCGLIB$$580325c0,我们把名字用$$拆分可知它是CGLIB生成的代理对象的FastClass,i2是collect方法的索引,其值为15
所以现在知道了CGLIB生成的另外两个Class文件是用来干嘛的吧?它们一个是目标对象的FastClass,一个是代理对象的FastClass。
说它们是FastClass,是因为它们都继承于FastClass,它们也都重写了FastClass的getIndex()方法和invoke()方法。
我们再回到MethodProxy的invoke方法。虽然f1保存的是目标方法的索引,看似invoke调用的是目标对象的方法,但实际上我们要看fci.f1.invoke的第二个参数obj,它传递的是一个代理对象LawEvidenceImpl$$EnhancerByCGLIB$$a794660b。
我们带着fci.i1的方法索引进入到FastClass的invoke方法里看一看它究竟会找到哪个方法进行调用?我们发现调用的是var10000.collect()方法,var10000是传递而来的代理对象。
我们再进一步来到代理对象看看它的collect方法,有没有很熟悉,这不就是我们最开始进来的那个方法吗,现在代码会再一次重复执行之前的intercept增强方法。
所以MethodProxy#invoke方法会无限循环调用代理对象的collect方法,最后由于栈的层数达到JVM的极限,爆出了StackOverflowError的错误。
我们再来看看MethodProxy的invokeSuper方法。和invoke一样,obj这个参数依然传递的是代理对象,但是此时的index已经变成了fci.i2,它的值是20,这个值为20的索引会指向哪个方法呢?
我们去fci.f2这个FastClass里看看,找到它的invoke方法,在switch语句里有一个case 20,返回的是var10000.CGLIB$collect$0(),var10000是代理对象。
生成的代理对象的collect$0()方法调用的是super.collect(),即父类的collect方法,前面已经提到了代理对象是继承自目标对象的,所以这里最终调用的是原目标方法,因此不会再循环调用增强的collect方法了。
既然我们知道了MethodProxy的invoke方法和invokeSuper方法是通过fci.f1和fci.f2这两个FastClass实现的动态调用。现在只剩下最后一个问题,fci.f1和fci.f2是在什么时候被初始化的呢?答案就是在每次调用invoke或invokeSuper方法时,都会执行一个init方法。
在init方法里面,fci.f1和fci.f2会被分别赋予不同的值,这又取决于MethodProxy在创建时的create方法里初始化的CreateInfo对象,CreateInfo对象的c1和c2分别是代理对象的类和目标对象的类。
fci.i1和fci.i2则来自于create方法的参数name1和name2查询的方法索引。如下图所示,name1的值是目标方法"collect",name2的值是增强方法"CGLIB$collect$0"。CGLIB会通过两个方法名生成不同的方法索引。
以上就是CGLIB调用代理方法的整个过程。由于CGLIB生成代理和两个FastClass的过程比较复杂,本文就不再深究其实现原理。感兴趣的朋友可以研究一下CGLIB的类生成器ClassGenerator的工作原理。
JDK动态代理和CGLIB
下表是果果梳理的两种动态代理方式,从不同维度出发的优势与劣势。
JDK动态代理 | CGLIB | |
JDK原生支持 | ? | ? |
随JDK圆滑升级 | ? | ? |
生成效率 | ? | ? |
运行速度 | ? | ? |
无侵入 | ? | ? |
- JDK原生支持
- 随JDK圆滑升级。JDK升级新版本时CGLIB需要更新,而JDK动态代理可以圆滑过渡。
- 生成效率。JDK直接生成,CGLIB采用ASM框架,而且CGLIB代理实现更复杂,所以生成效率更低。
- 运行速度。JDK动态代理采用反射机制,而CGLIB采用FastClass机制性能更佳。
- 无侵入。CGLIB代理的目标对象无需实现接口,而JDK动态代理的目标对象则需要实现某个接口。
此外,两者的代理实现方式也不同。JDK实现目标对象接口;CGLIB继承目标对象。所以JDK动态代理只能对实现了接口的类代理;而CGLIB因为是对类实现代理,所以不能对final修饰过的类代理。
喜欢“IT果果日记”文章的朋友建议收藏+关注,方便以后复习查阅。如需转载请注明文章来源及原地址。支持原创,侵权必究。