基于 CGLIB 库的动态代理机制

2,866 阅读8分钟

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

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

CGLIB 的动态代理机制

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

image

image

Student 类是我们的委托类,它本身继承 Father 类并实现 Person 接口。

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