一文搞懂CGLIB动态代理-全网最细的男人

首先思考这样两个问题。

CGLIB没有使用反射,那它是如何实现运行时动态调用的呢?

什么是CGLIB无限循环调用问题,怎么产生的?


上一篇文章《一文搞懂Java动态代理:为什么Mybatis Mapper不需要实现类?》介绍了动态代理的前世今生,虽然讲了很多基础的内容,但是大家给我的反馈是都很感兴趣,想要我接着聊一下CGLIB,这不就来了吗。为了尽量将CGLIB写的通俗易懂,我也是查阅了各种资料,并且以身试坑,终于加班加点的赶出来了。喜欢“IT果果日记”文章的朋友建议收藏+关注,方便以后复习查阅。如需转载请注明文章来源及原地址。支持原创,侵权必究。


目录

596db67823496b4f58cdfd739ca7f665.jpeg

CGLIB动态代理

CGLIB(Code Generation Library)是一个代码生成类库,它可以为没有提供实现接口的类生成代理,这一点和JDK动态代理不一样,JDK动态代理只能支持对目标接口的代理。为什么CGLIB可以支持对类生成代理?本文后面会介绍。

与JDK动态代理利用反射实现的原理不同,CGLIB相对于JDK动态代理性能更好,因为CGLIB底层采用的是轻量的字节码处理框架ASM,动态生成字节码,CGLIB还采用FastClass机制,其性能和普通代码没有区别。所以当有一定性能要求时,CGLIB比JDK动态代理更加合适。

我们先来看下CGLIB动态代理的实现。首先引入maven依赖。

666f192c910bdaa7d1d0ffab918bbfe8.jpeg

接着对目标对象增强。JDK动态代理增强的是接口,而CGLIB增强的是类。这里有两个目标对象,一个是给原告收集证据,另一个是给原告打官司。通过实现律师代理对象代替原告做这些事情。

6434ffeef1befea5ae07b76fe14ad1b0.jpeg

所以这里我们直接增强LawEvidenceImpl类和LawsuitImpl类。还有一个不同于JDK动态代理的点是JDK的增强方式是实现InvocationHandler接口,而CGLIB的增强方式是实现MethodInterceptor接口。CGLIB的增强代码如下:

fa4262ad689dc160f4b13259a8ad0daa.jpeg

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动态代理有哪些不同:

3770ea565c2f8594b5c98f0e7767c259.jpeg

执行结果:

c1b6bc8877de33f7a2532b7124a337fd.jpeg

CGLIB和JDK动态代理的实现步骤有其相似之处,都需要实现一个增强接口,再通过某种创建语句得到代理对象,最后调用代理对象的增强方法。代码层级结构如图所示。

188c9db3066d101a00bb51068629c463.jpeg

这里还要说明一下CGLIB相对于JDK动态代理的一个不同点,这个不同点不注意的时候很可能把自己给坑了。在写MethodInterceptor#intercept拦截方法的实现逻辑时,最好指定你想要增强目标对象的哪些方法,否则CGLIB默认会增强目标对象及其父类的所有非final方法、非private方法。

9d5b29001b4c2a8e5512e7ff36876bdd.jpeg

如果代码不像上面那样加一个判断,那么CGLIB会对目标对象的父类方法进行增强,例如Object.class类的toString和hashCode方法。

我在调试的时候发现一个非常有趣的现象,当我直接run代码时,返回如下图正常的结果。

4009b4b5430e505b902ba2024ef68ff5.jpeg

但当我debug断点运行代码时,控制台就会重复打印 "律师向原告了解案情,并代替"。

d16d452a12f20b783d3e410b304dbe89.jpeg

我把代码里加上一段日志,就能发现其问题的原因在哪里了。

34ee2c97b37f80b1f65f79b31542a6b4.jpeg

结果显示重复的打印日志是因为目标对象的父类Object.class的方法toString和hashCode也被增强了,而且这两个增强方法只在断点时才被调用,run的时候不会被调用,隐藏的非常深。

b72aed9ad6999a42b9585575e6d89f8c.jpeg

244c70265593bb219e8873b3b0c8e4c9.jpeg

反编译

CGLIB同样可以查看生成的class文件,在客户端Client的开头加一段代码即可。

76b1753132ef96fb0d4987c6b841c3c1.jpeg

再次运行Client,发现项目目录下生成了6个Class文件,CGLIB会为每个目标对象生成3个Class文件,本文的示例中因为有两个目标对象,所以一共生成了6个Class文件。

6eecbb57c1aae39a67497d0d627b3b95.jpeg

CGLIB生成的Class文件命名以$$拼接而成,生成的代理类名规则如下。

目标类名$$EnhancerByCGLIB$$随机字符串

例如目标类名是LawEvidenceImpl,随机字符串是a794660b,中间加上固定名EnhancerByCGLIB,所以得到代理类名是LawEvidenceImpl$$EnhancerByCGLIB$$a794660b。

CGLIB生成的代理类继承的是目标类(被代理类)。

a62a8a5d9790ae32a6c03b223bc3ad8e.jpeg

与JDK动态代理不一样,JDK动态代理的代理类继承的是Proxy类,并且实现了目标接口(被代理接口)。这也是为什么JDK动态代理不能对类代理的原因。

23526b07fce6816ba8159efe8000e06c.jpeg

CGLIB代理的流程

  • 利用Enhancer类的create方法创建增强对象,增强对象的类型是目标类(LawEvidenceImpl)的子类,所以增强对象继承了目标类的方法(collect方法)

9f7e309d1f0e5284d022dd42d4c00406.jpeg

  • 增强对象调用目标方法(collect)时,会触发拦截器的intercept方法

2f87f396dc163942a9466b9ab84bf347.jpeg

  • 拦截器的intercept方法实现增强逻辑,并且调用目标方法。

88236296d0bd0702a4a1ed44463566d8.jpeg

那么问题来了,MethodProxy是如何通过invokeSuper方法调用目标方法的呢?是和JDK动态代理一样使用的Java反射实现的吗?


FastClass机制

CGLIB显然不是通过Java反射实现对目标方法的动态调用的。CGLIB采用的是FastClass机制,通过建立目标方法的索引,调用时查找索引就能得到真正的目标方法,这种方式的性能要优于Java反射。

要想理解FastClass机制的原理,我们先从一个简单的示例入手,下面的示例代码模仿了FastClass的工作原理,实现了对Java对象ProxyObject动态调用其方法的功能。

a8f969ea61428f4ae56056ed5bbb3928.jpeg

在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。

b8a5e9ebb85ef390b4b9dd74262c19d6.jpeg

f1被赋予的是LawEvidenceImpl$$FastClassByCGLIB$$30055069,从名字我们就能知道它是LawEvidenceImpl这个目标对象的FastClass,i1是collect方法的索引,其值为0;

f2被赋予的是LawEvidenceImpl$$EnhancerByCGLIB$$a794660b$$FastClassByCGLIB$$580325c0,我们把名字用$$拆分可知它是CGLIB生成的代理对象的FastClass,i2是collect方法的索引,其值为15

41af4deb1d62fca2894d16537b522aae.jpeg

所以现在知道了CGLIB生成的另外两个Class文件是用来干嘛的吧?它们一个是目标对象的FastClass,一个是代理对象的FastClass。

d18c77455f77c244980525b055cb2ed7.jpeg

说它们是FastClass,是因为它们都继承于FastClass,它们也都重写了FastClass的getIndex()方法和invoke()方法。

4ef2c1d244423fd2795e5b376084e9d9.jpeg

我们再回到MethodProxy的invoke方法。虽然f1保存的是目标方法的索引,看似invoke调用的是目标对象的方法,但实际上我们要看fci.f1.invoke的第二个参数obj,它传递的是一个代理对象LawEvidenceImpl$$EnhancerByCGLIB$$a794660b。

9eb4ee0334201c7ec35edd0c9ea20756.jpeg

我们带着fci.i1的方法索引进入到FastClass的invoke方法里看一看它究竟会找到哪个方法进行调用?我们发现调用的是var10000.collect()方法,var10000是传递而来的代理对象。

45e213766513336f949a4a738780e2b4.jpeg

我们再进一步来到代理对象看看它的collect方法,有没有很熟悉,这不就是我们最开始进来的那个方法吗,现在代码会再一次重复执行之前的intercept增强方法。

b0506a2da4af06f2f93916b39e1e7291.jpeg

所以MethodProxy#invoke方法会无限循环调用代理对象的collect方法,最后由于栈的层数达到JVM的极限,爆出了StackOverflowError的错误。

3ad9d8e2306e693ce7c2a375efb16e45.jpeg

我们再来看看MethodProxy的invokeSuper方法。和invoke一样,obj这个参数依然传递的是代理对象,但是此时的index已经变成了fci.i2,它的值是20,这个值为20的索引会指向哪个方法呢?

591b753b11ee8042267ec88f0dd0666e.jpeg

我们去fci.f2这个FastClass里看看,找到它的invoke方法,在switch语句里有一个case 20,返回的是var10000.CGLIB$collect$0(),var10000是代理对象。

f0f11b09567c067342758c931a951952.jpeg

生成的代理对象的collect$0()方法调用的是super.collect(),即父类的collect方法,前面已经提到了代理对象是继承自目标对象的,所以这里最终调用的是原目标方法,因此不会再循环调用增强的collect方法了。

74bac5a03ba074713b021d8170a674a6.jpeg

既然我们知道了MethodProxy的invoke方法和invokeSuper方法是通过fci.f1和fci.f2这两个FastClass实现的动态调用。现在只剩下最后一个问题,fci.f1和fci.f2是在什么时候被初始化的呢?答案就是在每次调用invoke或invokeSuper方法时,都会执行一个init方法。

84aa05e3adc9bff13abdf03e5a1a01ef.jpeg

在init方法里面,fci.f1和fci.f2会被分别赋予不同的值,这又取决于MethodProxy在创建时的create方法里初始化的CreateInfo对象,CreateInfo对象的c1和c2分别是代理对象的类和目标对象的类。

cac68df99547f245dd0274527ecaab11.jpeg

fci.i1和fci.i2则来自于create方法的参数name1和name2查询的方法索引。如下图所示,name1的值是目标方法"collect",name2的值是增强方法"CGLIB$collect$0"。CGLIB会通过两个方法名生成不同的方法索引。

0e7e2da6bed64327e89c2322746df685.jpeg

以上就是CGLIB调用代理方法的整个过程。由于CGLIB生成代理和两个FastClass的过程比较复杂,本文就不再深究其实现原理。感兴趣的朋友可以研究一下CGLIB的类生成器ClassGenerator的工作原理。

64223cf6959565ac7fdf85b28e832411.jpeg

JDK动态代理和CGLIB

下表是果果梳理的两种动态代理方式,从不同维度出发的优势与劣势。

eb9c2fc08846604ef6436e34946c88e3.jpeg

  • JDK原生支持
  • 随JDK圆滑升级。JDK升级新版本时CGLIB需要更新,而JDK动态代理可以圆滑过渡。
  • 生成效率。JDK直接生成,CGLIB采用ASM框架,而且CGLIB代理实现更复杂,所以生成效率更低。
  • 运行速度。JDK动态代理采用反射机制,而CGLIB采用FastClass机制性能更佳。
  • 无侵入。CGLIB代理的目标对象无需实现接口,而JDK动态代理的目标对象则需要实现某个接口。

此外,两者的代理实现方式也不同。JDK实现目标对象接口;CGLIB继承目标对象。所以JDK动态代理只能对实现了接口的类代理;而CGLIB因为是对类实现代理,所以不能对final修饰过的类代理。

f98b53cbb2adafd4348f905c860ae2ef.jpeg

喜欢“IT果果日记”文章的朋友建议收藏+关注,方便以后复习查阅。如需转载请注明文章来源及原地址。支持原创,侵权必究。

8ddd1a29e8a6951229b5936db3751fc5.jpeg

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT果果日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值