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

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

CGLIB 的动态代理机制

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

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

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

可以看到,CGLIB 创建的代理类是委托类的子类,所以可以被强转为委托类类型。

从输出结果可以看到,所有的方法都得到了代理。

这算是 CGLIB 的一个最简单应用了,大家不妨复制代码自己运行一下,接着我们会一点点来分析这段代码。

我们首先来看看 CGLIB 生成的代理类具有什么样的结构,通过设置系统属性:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,本地磁盘路径)

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

JAVA 反向工程网

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

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

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

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

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

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

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

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

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

至此,我们研究了代理类的基本结构,大体上是类似于 JDK 动态代理的,不同点在于,CGLIB 生成的代理类直接继承我们的委托类以至于能够代理委托类中所有的方法。

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

自定义拦截器很简单,只需要实现我们 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 实例,一个包装了委托类,并且暴露了该方法索引,另一个包装了代理类,同样暴露了该方法在代理类中的索引。

好,现在考大家一下:

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

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

可能很多人还是有点绕,其实很简单,一个 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 方法也没那么神秘:

不要想太复杂,一个 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,所有文章都将同步在公众号上。

基于 CGLIB 库的动态代理机制的更多相关文章

  1. 基于 JDK 的动态代理机制

    『动态代理』其实源于设计模式中的代理模式,而代理模式就是使用代理对象完成用户请求,屏蔽用户对真实对象的访问. 举个最简单的例子,比如我们想要「FQ」访问国外网站,因为我们并没有墙掉所有国外的 IP,所 ...

  2. CGLIB动态代理机制,各个方面都有写到

    CGLIB库介绍 代理提供了一个可扩展的机制来控制被代理对象的访问,其实说白了就是在对象访问的时候加了一层封装.JDK从1.3版本起就提供了一个动态代理,它使用起来非常简单,但是有个明显的缺点:需要目 ...

  3. Java动态代理机制——Cglib

    上一篇说过JDK动态代理机制,只能代理实现了接口的类,这就造成了限制.对于没有实现接口的类,我们可以用Cglib动态代理机制来实现. Cglib是针对类生成代理,主要是对用户类生成一个子类.因为有继承 ...

  4. Cglib 与 JDK动态代理

    作者:xiaolyuh 时间:2019/09/20 09:58 AOP 代理的两种实现: jdk是代理接口,私有方法必然不会存在在接口里,所以就不会被拦截到: cglib是子类,private的方法照 ...

  5. 详解Java动态代理机制

    之前介绍的反射和注解都是Java中的动态特性,还有即将介绍的动态代理也是Java中的一个动态特性.这些动态特性使得我们的程序很灵活.动态代理是面向AOP编程的基础.通过动态代理,我们可以在运行时动态创 ...

  6. AOP之proceedingjoinpoint和joinpoint区别(获取各对象备忘)、动态代理机制及获取原理代理对象、获取Mybatis Mapper接口原始对象

    现在AOP的场景越来越多,所以我们有必要理解下和AOP相关的一些概念和机制. import org.aspectj.lang.reflect.SourceLocation; public interf ...

  7. 学习CGLIB与JDK动态代理的区别

    动态代理 代理模式是Java中常见的一种模式.代理又分为静态代理和动态代理.静态代理就是显式指定的代理,静态代理的优点是由程序员自行指定代理类并进行编译和运行,缺点是一个代理类只能对一个接口的实现类进 ...

  8. 性能优于JDK代理,CGLib如何实现动态代理

    按照代理的创建时期,代理类可以分为两种. 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译.在程序运行前,代理类的.class文件就已经存在了. 动态代理:在程序运行时,运用反射机制动态创建 ...

  9. 面试造火箭系列,栽在了cglib和jdk动态代理

    "喂,你好,我是XX巴巴公司的技术面试官,请问你是张小帅吗".声音是从电话那头传来的 "是的,你好".小帅暗喜,大厂终于找上我了. "下面我们来进行一 ...

随机推荐

  1. BZOJ1026或洛谷2657 [SCOI2009]windy数

    BZOJ原题链接 洛谷原题链接 简单的数位\(DP\),套模板就好. #include<cstdio> #include<cstring> using namespace st ...

  2. 将VSCode设置成中文语言环境

    VSCode是一款轻量级的好用的编译软件,今天小编来将软件默认的英文语言环境变为我们熟悉的中文语言环境. 工具/原料   电脑一台 安装有VSCode 方法/步骤     首先打开VSCode软件,可 ...

  3. C++学习札记(1)

    指针 按别名传递 下面是个例子: #include <iostream> using namespace std; void swap(int &a,int &b) { i ...

  4. Visual Studio2013 配置opencv3.3.0 x64系统

    注:小白一个,第一次写博客,可能会有一些理解上的错误,只此记录自己测试成功的坎坷之路,已备以后查看,同时给有需要之人. 我是win10 64 位,之前安装了visual studio 2013, 现在 ...

  5. pip更换国内源

    学习Python开发,据说pip是很好用的一个Python包管理工具,于是尝试使用,但源异常慢,于是切换至国内的源(清华源). 在~/.pip/pip.conf (如果没有此文件则自行新建) 内容 [ ...

  6. Android系统的镜像文件的打包过程

    在前面一篇文章中,我们分析了Android模块的编译过程.当Android系统的所有模块都编译好之后,我们就可以对编译出来的模块文件进行打包了.打包结果是获得一系列的镜像文件,例如system.img ...

  7. Release file is expired, Updates for this repository will not be applied.(资源索引文件过期问题)

    将Debian下载源同步到本地之后,通过本地资源地址进行apt update操作时提示过期问题: E: Release file for http://localhost/security/dists ...

  8. 设计模式之观察者模式(c++)

    Observer 模式应该可以说是应用最多.影响最广的模式之一,因为 Observer 的一个实例 Model/View/Control( MVC) 结构在系统开发架构设计中有着很重要的地位和意义, ...

  9. 单片机之PID算法

    说到PID算法,想必大部人并不陌生,PID算法在很多方面都有重要应用,比如电机的速度控制,恒温槽的温度控制,四轴飞行器的平衡控制等等,作为闭环控制系统中的一种重要算法,其优点和可实现性都成为人们的首选 ...

  10. unigui结合JS方法记录

    在js中界面上所有组件都当成html里来控制 .控制按钮事件  document.getElementById(MainForm.UniButton4.getId()).click(); 这个方法让J ...