问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

SpringBoot源码学习之CGLIB动态代理原理

创作时间:
作者:
@小白创作中心

SpringBoot源码学习之CGLIB动态代理原理

引用
CSDN
1.
https://blog.csdn.net/qq_39108894/article/details/140476696

1.介绍

CGLIBCode 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、非staticpublic修饰的方法,接着调用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接口的拦截器对象,由拦截器决定目标方法执行的时机。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号