1.前言
在设计模式中,动态代理模式的学习中,遇到一个疑问:动态动态代理只能代理接口类,有接口才能工作,那么这个时候如果没有接口需要代理该怎么办?
后来找到的解决方案是CGlib代理模式。
2.什么是CGlib
CGlib是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。那么与JDK的动态代理二者之间的区别是:
jdk动态代理是利用拦截器加上反射机制生成一个实现代理接口的匿名类。CGlib动态代理是利用ASM开源包,将被代理对象类的Class文件加载进来,通过修改其字节码来处理。
那什么时候使用jdk动态代理,什么时候使用CGlib动态代理?
1、在Spring中,如果目标实现了接口,默认使用jdk动态代理。jdk动态代理只能针对接口类,非接口类无法代理。
2、若目标bean没有实现接口,则使用CGlib动态代理。CGlib的实现原理是针对被代理类生成一个子类,覆盖其中的方法并实现增强,但是采用的手段是继承。因此,被代理类中使用final声明的方法,无法使用CGlib动态代理,因为final声明的方法无法被继承。
但是因为 主要是缺少文档和示例造成CGlib的学习上存在一定难度。
3.CGlib原理
CGLIB能够使用ASM字节码生成框架,采用字节码技术动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。(注意在jdk6之前确实比jdk动态的代理效率要高。但是每一次jdk版本是升级,jdk动态代理的效率都得到提升,而CGlib逐渐有点跟不上步伐了。)但是对于final方法,无法进行代理。目前Spring的AOP应用的就是CGlib代理。
CGlib的结构图如下:
推荐使用的是cglib-nodep-2.2.jar包,这个包不需要关联asm的jar包,jar包内部包含asm的类。包中类的说明如下:
net.sf.cglib.asm:ASM字节码操作和分析的框架
net.sf.cglib.beans: JavaBean相关的工具类
net.sf.cglib.core: 底层字节码处理类,他们大部分与ASM有关系
net.sf.cglib.reflect: 实现快速反射和C#风格代理的类
net.sf.cglib.transform: 编译期或运行期类和类文件的转换
net.sf.cglib.proxy: 实现创建代理和方法拦截器的类
net.sf.cglib.util: 集合排序等工具类
PS:什么是ASM字节码?
4.CGlib应用实例
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * Author:jrliu * Date:2019/10/16 * Description:代理模式之CGLIB代理模式 */ //真实主题 class RealSubject { public void request() { System.out.println("访问真实主题"); } } class CGlibProxy implements MethodInterceptor { //这里可以使用泛型封装一下,避免TestCGlibProxy中调用setSuperclass和setCallback public <T> T getProxy(Class<T> cls){ return (T) Enhancer.create(cls, this); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { before(); Object result = methodProxy.invokeSuper(o, objects); after(); return result; } private void before() { System.out.println("hello,CGlib!"); } private void after() { System.out.println("bye,CGlib!"); } } public class TestCGlibProxy { public static void main(String[] args) { //尝试使用CGlib代理模式实现 System.out.println("下面是使用CGlib代理模式实现"); Enhancer enhancer =new Enhancer(); enhancer.setSuperclass(RealSubject.class); enhancer.setCallback(new CGlibProxy()); RealSubject realSubject = (RealSubject)enhancer.create(); realSubject.request(); } }
1、 使用CGlib动态代理需要导入cglib-nodep-2.2.jar
2、CGlib动态代理实现了MethodInterceptor,并且填充intercept方法,传入的参数中最后一个methodProxy,其实就是一个方法拦截器,因此CGlib可以增强非接口类的方法。调用methodProxy的invokeSuper方法,将被代理的对象以及方法参数args传入即可。对执行目标类产生一个子类,通过方法拦截技术拦截所有父类方法的调用。
使用CglibProxy实现动态代理的一般步骤:
1、创建类实现接口MethodInterceptor,并重写intercept方法
2、创建被代理类
3、调用代理类自定义的方法,得到一个代理实例
4、通过代理实例调用被代理类的需要执行的方法
5.Enhancer和MethodInterceptor
在上面的程序中,引入了Enhancer和MethodInterceptor
Enhancer类是CGLib中的一个字节码增强器,是CGlib中最常用的类。它可以对想要处理的类进行扩展, 首先将被代理类TargetObject设置成父类,然后设置拦截器TargetInterceptor,最后执行enhancer.create()动态生成一个代理类,并从Object强制转型成父类型TargetObject。见下面:
Enhancer enhancer =new Enhancer();
enhancer.setSuperclass(RealSubject.class);/设置被继承类(父类)
enhancer.setCallback(new CGlibProxy());//设置回调
RealSubject realSubject = (RealSubject)enhancer.create();
Enhancer既能够代理普通的class,也能够代理接口。Enhancer创建一个被代理对象的子类并且拦截所有的方法调用(包括从Object中继承的toString和hashCode方法)。Enhancer不能够拦截final方法,例如Object.getClass()方法,这是由于Java final方法语义决定的。基于同样的道理,Enhancer也不能对fianl类进行代理操作。这也是Hibernate为什么不能持久化final class的原因。
MethodInterceptor是方法拦截器接口, 在调用目标方法时,CGLib会回调MethodInterceptor接口方法拦截,来实现代理逻辑,类似于JDK中的InvocationHandler接口; 该接口中只有一个intercept方法,这个方法的作用是拦截和回调,拦截指的是在在具体的执行方法的前、后加上其他的代码来扩展功能,回调指的是在这个方法中调用父类(被代理类)中的指定方法。
6.ASM字节码
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
为什么要动态生成 Java 类?
动态生成 Java 类与 AOP 密切相关的。AOP 的初衷在于软件设计世界中存在这么一类代码,零散而又耦合:零散是由于一些公有的功能(诸如著名的 log 例子)分散在所有模块之中;同时改变 log 功能又会影响到所有的模块。出现这样的缺陷,很大程度上是由于传统的 面向对象编程注重以继承关系为代表的“纵向”关系,而对于拥有相同功能或者说方面 (Aspect)的模块之间的“横向”关系不能很好地表达。 对于没有多继承的 Java 来说,更是如此。传统的解决方案是使用 Decorator 模式,它可以在一定程度上改善耦合,而功能仍旧是分散的
改造一个类的一个方法还好,如果是变动整个模块,使用装饰者模式很快就会演化成另一个噩梦。动态改变 Java 类就是要解决 AOP 的问题,提供一种得到系统支持的可编程的方法,自动化地生成或者增强 Java 代码。这种技术已经广泛应用于最新的 Java 框架内,如 Spring 等。
选择ASM?
最直接的改造 Java 类的方法莫过于直接改写 class 文件。Java 规范详细说明了 class 文件的格式,直接编辑字节码确实可以改变 Java 类的行为。但是要求使用者对 Java class 文件的格式了熟于心:小心地推算出想改造的函数相对文件首部的偏移量,同时重新计算 class 文件的校验码以通过 Java 虚拟机的安全机制。直接操作class文件是比较麻烦的,就跟为什么我们都选择使用框架一样,框架屏蔽了底层的复杂性。ASM就是操作class的一把利器。
使用 ASM 动态生成类,不需要像早年的 class hacker 一样,熟知 class 文件的每一段,以及它们的功能、长度、偏移量以及编码方式。ASM 会给我们照顾好这一切的,我们只要告诉 ASM 要改动什么就可以了 —— 当然,我们首先得知道要改什么:对类文件格式了解的越多,我们就能更好地使用 ASM 这个利器。
ASM 能够通过改造既有类,直接生成需要的代码。增强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。同时,ASM 与 Proxy 编程不同,不需要为增强代码而新定义一个接口,生成的代码可以覆盖原来的类,或者是原始类的子类。它是一个普通的 Java 类而不是 proxy 类,甚至可以在应用程序的类框架中拥有自己的位置,派生自己的子类。相比之下,Proxy是面向接口的,所有使用 Proxy 的对象都必须定义一个接口,而且用这些对象的代码也必须是对接口编程的:Proxy 生成的对象是接口一致的而不是对象一致的:例子中 Proxy.newProxyInstance生成的是实现 Account接口的对象而不是 AccountImpl的子类。这对于软件架构设计,尤其对于既有软件系统是有一定掣肘的。Proxy 通过反射实现的,必须在效率上付出代价:有实验数据表明,调用反射比一般的函数开销至少要大 10 倍。而且,从程序实现上可以看出,对 proxy class 的所有方法调用都要通过使用反射的 invoke 方法。因此,对于性能关键的应用,使用 proxy class 是需要精心考虑的,以避免反射成为整个应用的瓶颈。