SpringBoot源码学习之CGLIB动态代理原理
SpringBoot源码学习之CGLIB动态代理原理
1.介绍
CGLIB(Code Generation Library代码生成库)是一个能在程序运行期间动态生成代理对象的框架,底层通过ASM框架去动态的生成字节码对象,字节码对象再转为内存中的Class对象,最后再变为代理对象实例。ASM框架是一个通用的 Java 字节码操作和分析框架,它可以用来修改现有的类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,而CGLIB框架正是通过ASM框架的能力去动态生成字节码对象,最后利用反射创建出cglib代理对象实例。
cglib代理对象的工作原理简单概括就是,它通过继承目标类去重写目标方法,在执行这些重写的方法时,并不是直接调用原目标方法,而是通过MethodInterceptor拦截器将目标方法拦截下来,拦截器作为一个中间层,执行完它的拦截逻辑后,最终才会利用反射调用目标方法。例如AOP切面编程的前置通知,就是通过拦截器先执行前置通知的具体逻辑,后放行调用目标方法。
2.使用
(1)引入相关依赖
使用CGLIB需要引入cglib-2.2.jar(里面包含了ASM框架,不再需要额外引入)。不过,本文是SpringBoot源码学习的系列文章之一,因此只需要引入org.springframework:spring-core即可,它包含了CGLIB、ASM框架的完整代码,在这里引入依赖如下。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
(2)简单案例使用
①定义目标类
public class TestService {
public void test1(){ // 可以被cglib代理对象代理执行
System.out.println("TestService正在执行test1方法");
}
private static void test2(){ // 非private的、static方法无法被cglib代理对象代理执行
System.out.println("TestService正在执行test2静态方法");
}
public final void test4(){ // final方法无法被cglib代理对象代理执行
System.out.println("TestService正在执行test4公有final方法");
}
}
②定义回调函数
实现了MethodInterceptor接口的回调函数非常有用,它们是cglib代理对象的核心组成,它们会在一个合适的时机通过反射调用目标方法,如下伪代码所示,会先执行自己的拦截逻辑,后再调用目标方法。
public class TestCglibProxy implements MethodInterceptor {
/**
*
* @param proxy cglib代理对象
* @param method 目标方法Method对象
* @param objects 方法入参
* @param methodProxy cglib方法代理
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
System.out.println("cglib回调函数开始工作...");
methodProxy.invokeSuper(proxy, objects);// 调用代理类的父类——目标类的方法
return null;
}
}
③创建cglib代理对象
在cglib框架中,一般通过Enhancer对象去创建代理对象,它需要一个超类Class对象以及回调函数。
public class CglibMainTest {
public static void main(String[] args) {
// ASM框架生成的字节码对象是存在内存中,我们可以设置此配置值将该
// 字节码文件写进项目路径下的文件中
System.setProperty("cglib.debugLocation",
"F:\\SourceCodeStudy\\myStudy\\src\\main\\java\\com
\\zst\\study\\cglibtest\\classes");
Callback callback = new TestCglibProxy();
// 通过Enhancer去创建cglib代理对象
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestService.class);//设置要继承的超类,即目标类Class对象
enhancer.setCallback(callback);//设置回调函数,没有它,目标方法无法被拦截调用
TestService proxy = (TestService) enhancer.create();
proxy.test1();
}
}
④运行结果
3.基本原理
在CGLIB框架在创建代理对象的过程中,通过创建一个Enhancer对象,为其设置超类Class、回调函数。接着从其超类(目标类)的Class对象中获取所有Method对象,最终过滤出非final、非static的public修饰的方法,接着调用ASM框架的相关API去做一些创建类的信息、字段信息、方法信息等,将它们创建为一个byte字节组成的数组,即字节码对象。再通过ASM框架将程序内存中的字节码对象以Class文件写入指定路径(非必须步骤,需要自行设置),最后将得到的字节码对象转为代理Class对象以及回调函数通过反射创建一个实例对象。
接下来,对上述四大步骤进行源码解读。
①从目标类获取所有可用的Method对象
在Enhancer.generateClass(ClassVisitor v))方法中,会从设置的超类Class对象以及它继承的父类、所有接口中获取所有声明的Method对象,然后过滤掉final的方法(final修饰方法表示不可重写)、static的方法(static的资源属于类,而不是具体的某个实例对象,因此无法重写父类静态方法实现多态)。
②创建类的信息、字段信息、方法信息等
类的信息、字段信息、方法信息等的创建需要借用ASM框架的相关API完成,在ASM框架中,会在程序运行期间将这些信息写进一个byte字节组成的数组,即字节码对象。
类信息的定义是通过ClassVisitor的对象去完成的,它是ASM框架中的一个类,在cglib框架中是通过ClassEmitter去操纵ClassVisitor对象的。可以看到,ClassEmitter.begin_class(...)方法中传入了cglib代理对象自身的一个类名、父类Class对象、需要实现的所有接口等信息,又会将这些参数传递给ClassVisitor对象,最终经过ASM框架的处理,一个类信息就定义出来了。
字段信息的定义是通过ClassEmitter.declare_field(...)去操作的,底层原理是通过调用ASM框架的ClassVisitor.visitField(...)方法将字段信息写入上一步创建好的类信息里面。其中的某些字段非常重要,因为它们正是负责执行目标方法的。
其中一个字段便是类型为MethodInterceptor(回调函数必须实现此接口)、名称叫CGLIB$CALLBACK_0,在cglib代理对象中重写的所有的目标方法都会用到它。
另外一个字段也非常重要,它就是类型为ThreadLocal、名字是CGLIB$THREAD_CALLBACKS的变量,它将用作是给cglib代理对象存储回调函数。
方法信息的定义是通过CodeEmitter去实现的,它有许多方法,如load_this()、getfield(...)、if_jump(...)等方法,在这里我们只需要记住这些方法最终还是借助ASM框架的能力将方法信息写入代理类中去。
③内存中的字节码对象存储为Class文件
CGLIB框架默认是不会将动态代理的字节码对象写入项目路径中的,因为JAVA运行期间也是读取Class文件到内存再转为Class对象,但是我们可以通过设置系统变量cglib.debugLocation,它的值为保存这些Class文件的路径。
④字节码对象转为Class对象、设置回调函数、创建代理对象
当DefaultGeneratorStrategy.generate(this)完成时,cglib代理的字节码对象就已经创建完成。【this就是Enhance对象,它继承了AbstractClassGenerator类】,然后会利用反射机制将字节码对象转为一个Class对象。
接着,会创建一个EnhancerFactoryData对象,它会利用刚才创建好的代理Class对象,在设置它的方法信息时,设置了一个名称为CGLIB$SET_THREAD_CALLBACKS的静态方法,它的作用便是将回调函数设置到CGLIB$THREAD_CALLBACKS变量去。
最后,通过代理Class对象的默认无参构造函数对象,利用反射创建一个cglib代理对象实例。
⑤cglib代理字节码对象源码
写入指定路径的Class文件如下,它的确继承了目标类,并且实现了Factory接口(Factory接口有一个setCallbacks(Callback[] var1)方法,在Spring的实践中是把cglib代理对象创建出来后,再回调这个setCallbacks方法设置回调函数)
这是刚才通过反射调用代理Class对象的CGLIB$SET_THREAD_CALLBACKS方法,逻辑很简单,就是将回调函数赋值给本地线程变量CGLIB$THREAD_CALLBACKS。
接下来,看看cglib代理对象重写的目标方法,首先尝试获取CGLIB$CALLBACK_0这个回调函数,如果不存在则调用CGLIB$BIND_CALLBACKS(this)方法从本地线程获取CGLIB$THREAD_CALLBACKS变量(Callback[]数组),将它的第一个元素赋值给CGLIB$CALLBACK_0。
最终,当CGLIB$CALLBACK_0不是null值的时候,回调重写MethodInterceptor接口的intercept(...)方法,实现不同的逻辑。若是null值,则直接调用父类的方法。
4.在Spring的应用
在Spring框架中,代理对象往往应用于AOP切面编程和事务编程。非接口类目前默认会被创建cglib代理对象,当执行切面逻辑或者需要操作事务时,会进入DynamicAdvisedInterceptor这个拦截器,它也实现了MethodInterceptor接口,在重写的intercept(...)方法中实现了拦截再调用目标方法的逻辑。
5.总结
经过简单案例和源码分析,cglib代理对象的奥秘也被揭露,借助CGLIB和ASM两大框架,在程序运行期间动态生成字节码对象——>Class对象——>代理对象实例,代理对象重写了目标方法,执行重写的方法时,并不是直接调用原目标方法,而是通过实现MethodInterceptor接口的拦截器对象,由拦截器决定目标方法执行的时机。