基于 CGLIB 库的动态代理机制

以前的文章咱们详细的介绍了 JDK 自身的 API 所提供的一种动态代理的实现,它的实现相对而言是简单的,可是却有一个很是致命性的缺陷,就是只能为接口中的方法完成代理,而委托类本身的方法或者父类中的方法都不可能被代理。java

CGLIB 应运而生,它是一个高性能的,底层基于 ASM 框架的一个代码生成框架,它完美的解决了 JDK 版本的动态代理只能为接口方法代理的单一性不足问题,具体怎么作的咱们一块儿来看。git

CGLIB 的动态代理机制

再详细介绍 CGLIB 原理以前,咱们先完整的跑起来一个例子吧,毕竟有目的性的学习老是不容易放弃的。github

image

image

Student 类是咱们的委托类,它自己继承 Father 类并实现 Person 接口。bash

image

CGLIB 的拦截器有点像 JDK 动态代理中的处理器。微信

image

能够看到,CGLIB 建立的代理类是委托类的子类,因此能够被强转为委托类类型。框架

image

从输出结果能够看到,全部的方法都获得了代理。工具

image

这算是 CGLIB 的一个最简单应用了,你们不妨复制代码本身运行一下,接着咱们会一点点来分析这段代码。性能

咱们首先来看看 CGLIB 生成的代理类具备什么样的结构,经过设置系统属性:学习

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,本地磁盘路径)
复制代码

能够指定 CGLIB 将动态生成的代理类保存至指定的磁盘路径下。接着咱们反编译一下这个代理类,有不少优秀的第三方反编译工具,这里我推荐给你们一个网站,该网站能够直接为咱们反编译一个 Class 文件。网站

JAVA 反向工程网

因而你能够在你指定的磁盘目录下找到 CGLIB 为你保存下来的代理类,你只要将它上传到这个网站上,就会获得该文件反编译后的 java 文件。

首先看看这个代理类的继承体系

image

Student 是咱们须要代理的委托类型,结果生成的代理类就直接继承了委托类。这一个小设计就完美的解决了 JDK 动态代理那个单一代理的缺陷,继承了委托类,就能够反射出委托类接口中的全部方法,父类中的全部方法,自身定义的全部方法,完成这些方法的代理就完成了对委托类全部方法的代理。

Factory 接口中定义了几个方法,用于设置和获取回调,也就是咱们的拦截器,有关拦截器的部分待会说。

接着这部分,程序反射了父类,也就是是委托类,全部的方法,包括委托类的父类及父接口中的方法。

image

最后一部分,重写了父类全部的方法,这里以一个方法为例。

image

显然,代理类重写了父类中全部的方法,而且这些方法的逻辑也是很简单的,将当前的方法签名做为参数传入到拦截器中,这里也称拦截器为『回调』。

因此,从这一点来看,CGLIB 的方法调用是和 JDK 动态代理是相似的,都是须要依赖一个回调器,只不过这里咱们称为拦截器,JDK 中称为处理器。

可是这里我要提醒你的是,代理类中每个方法都具备两个版本,一个是原名重写的方法,另外一个是不通过拦截器的对应方法。这是 CGLIB 中 FastClass 机制的一个结果,这里我只想引发你的注意而已,有关 FastClass 待会会介绍。

至此,咱们研究了代理类的基本结构,大致上是相似于 JDK 动态代理的,不一样点在于,CGLIB 生成的代理类直接继承咱们的委托类以致于可以代理委托类中全部的方法。

既然代理类中全部的方法调用都会转交拦截器,那么咱们就来看看这个拦截器的各个参数都表明什么意思。

image

自定义拦截器很简单,只须要实现咱们 MethodInterceptor 接口并重写其 intercept 方法便可。这个方法有四个参数,咱们分别看看都表明着什么。

  • obj:它表明的是咱们代理类的实例对象
  • method:当前调用方法的引用
  • arg:调用该方法的形式参数
  • proxy:它也表明着当前方法的引用,基于 FastClass 机制

咱们知道 Method 是基于反射来调用方法的,可是反射的效率老是要低于直接的方法调用的,而 MethodProxy 基于 FastClass 机制对方法直接下标索引,并经过索引直接定位和调用方法,是一点性能上的提高。

咱们看一个 MethodProxy 实例的工厂方法源码:

public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
    MethodProxy proxy = new MethodProxy();
    proxy.sig1 = new Signature(name1, desc);
    proxy.sig2 = new Signature(name2, desc);
    proxy.createInfo = new MethodProxy.CreateInfo(c1, c2);
    return proxy;
}
复制代码

其中,形式参数 desc 表明的是一个方法的方法描述符,c1 表明的是这个方法所属的类,值通常是咱们的委托类,c2 表明的值每每是咱们生成的代理类。而 name1 是委托类中该方法的方法名,name2 是代理类中该方法的方法名。

举个例子:

var1 = Class.forName("Main.Student");
var0 = Class.forName("Main.Student$$EnhancerByCGLIB$$56e20d66");
MethodProxy.create(var1, var0, "()V", "sayHello", "CGLIB$sayHello$3");
复制代码

var1 是咱们的委托类,var0 是该委托类的代理类,「()V」是 sayHello 方法的方法签名,「CGLIB$sayHello$3」是 sayHello 方法在代理类中的方法名。

有了这几个参数,MethodProxy 就能够初始化一个 FastClassInfo。

private static class FastClassInfo {
    FastClass f1;
    FastClass f2;
    int i1;
    int i2;
    private FastClassInfo() {
    }
}
复制代码

而 FastClass 是个什么呢,其实内部是有点复杂的,这里简单给你们说一下。

FastClass 有点装饰者模式的意思,内部包含一个 Class 对象,而且会对其中全部的方法进行一个索引标记,因而外部对于任意方法的调用只须要提供一个索引值,FastClass 就可以快速定位到具体的方法。

而这里的 f1 内部包装的会是咱们的委托类,f2 则会包装咱们的代理类,i1 是当前方法在 f1 中的索引值,i2 是当前方法在 f2 中的索引值。

因此,基于 FastClass 的方法调用也是简单的,invoke 方法中指定一个索引便可,而不须要传统的反射方式,须要给 invoke 方法传入调用者,而后在经过反射调用的该方法进行调用。

总的来讲,一个 MethodProxy 实例会对应两个 FastClass 实例,一个包装了委托类,而且暴露了该方法索引,另外一个包装了代理类,一样暴露了该方法在代理类中的索引。

好,如今考你们一下:

image

MethodProxy 中 invoke 方法和 invokeSuper 方法分别调用的是哪一个方法?代理类中的?仍是委托类中的?

答案是:invoke 方法会调用后者,invokeSuper 则会调用前者。

image

可能不少人仍是有点绕,其实很简单,一个 FastClass 实例会绑定一个 Class 类型,而且会对该 Class 中全部的方法进行一个索引标记。

那么按照咱们说的,f1 绑定的是咱们的委托类,f2 绑定的是咱们的代理类,而不管你是用 f1 或是 f2 来调用这个 invoke 方法,你都是须要传入一个 obj 实例的,而这个实例就是咱们的代理类实例,因为 f1.i1 对应的方法签名是 「public final void run」,而 f2.i2 对应的方法签名则是「final void CGLIB$0」。

因此,f1.i1.invoke 和 f2.i2.invoke 调用的是同一个实例的不一样方法,这也说明了为何 CGLIB 搞出来的代理类每种方法都有两个形式的缘由,但我的以为这样的设计有点无用功,还容易形成死循环,增长理解难度。

而这个 FastClass 的 invoke 方法也没那么神秘:

image

不要想太复杂,一个 FastClass 实例只不过扫描了内部 Class 类型的基本方法后,在 invoke 方法中列出 switch-case 选项,而每一次 invoke 的调用都是先匹配一下索引,而后让目标对象直接调用目标方法。

因此这里会引起一个问题,死循环的问题。咱们的拦截器通常都是这样写的:

System.out.println("Before:" + method);
Object object = proxy.invokeSuper(obj, arg);
System.out.println("After:" + method);
return object;
复制代码

invokeSuper 会调用 「final void CGLIB$0」方法,间接调用委托类的对应方法。而若是你改为 invoke,像这样:

System.out.println("Before:" + method);
Object object = proxy.invoke(obj, arg);
System.out.println("After:" + method);
return object;
复制代码

结果就是死循环,为何呢?

invoke 方法调用的是和委托类中方法具备同样签名的方法,最终走到咱们的代理类里面,就会再通过一次拦截器,而拦截器又不停的回调,它俩就在这死循环了。

至此,我以为对于 CGLIB 的基本原理我已经介绍完了,你须要整理一下逻辑,理解它从头至尾的执行过程。

CGLIB 的不足

咱们老说,CGLIB 解决了 JDK 动态代理的致命问题,单一的代理机制。它能够代理父类以及自身、父接口中的方法,可是你注意一下,我没有说全部的方法都能代理

CGLIB 的最大不足在于,它须要继承咱们的委托类,因此若是委托类被修饰为 final,那就意味着,这个类 CGLIB 代理不了。

天然的,即使某个类不是 final 类,可是其中若是有 final 修饰的方法,那么该方法也是不能被代理的。这一点从咱们反射的源码能够看出来,CGLIB 生成的代理类须要重写委托类中全部的方法,而一个修饰为 final 的方法是不容许重写的。

总的来讲,CGLIB 已经很是的优秀了,瑕不掩瑜。几乎市面上主流的框架中都不可避免的使用了 CGLIB,之后会带你们分析框架源码,到时候咱们再见 CGLIB !


文章中的全部代码、图片、文件都云存储在个人 GitHub 上:

(https://github.com/SingleYam/overview_java)

欢迎关注微信公众号:OneJavaCoder,全部文章都将同步在公众号上。

image
相关文章
相关标签/搜索