探索Cglib:解析动态代理的神奇之处

在这里插入图片描述

动态代理系列文章:
深入探索JDK动态代理:从入门到原理的全面解析
探索Cglib:解析动态代理的神奇之处
全网最全解析!Spring与非Spring环境下获取动态代理对象的原始目标对象

CGLIB介绍

CGLIB(Code Generation Library)是一个开源项目!是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。Hibernate用它来实现PO(Persistent Object 持久化对象)字节码的动态生成。

CGLIB是一个强大的高性能的代码生成包。它广泛的被许多AOP的框架使用,例如Spring AOP为他们提供方法的interception(拦截)。
CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。

除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

CGLIB使用示例

首先引入对应依赖:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

以下是一个简单的CGLIB代理示例:

需要注意的是:

由于是采用的是继承方式,因此final类无法使用CGLIB来进行代理。此外,对于static方法或final方法,由于这些方法无法被重写,所以CGLIB也无法为其提供代理。

/**
 * 目标对象,没有实现任何接口
 */
public class UserDaoForCglib {

    public void save() {
        System.out.println("----cglib代理----已经保存数据!----");
    }
	//final方法无法被重写,也就无法被代理
   public final void saveFinal() {
      System.out.println("final方法不可继续重写,所以不能进行代理");
   }

	//static方法无法被重写,也就无法被代理
   public static void saveStatic() {
      System.out.println("static方法不可继续重写,所以不能进行代理");
   }

}

下面是一个Cglib代理工厂类,创建代理对象核心步骤如下

  1. 创建Enhancer实例
  2. 通过setSuperclass方法来设置目标类
  3. 通过setCallback 方法来设置拦截对象
  4. create方法生成Target的代理类,并返回代理类的实例
/**
 * Cglib子类代理工厂
 * 对UserDao在内存中动态构建一个子类对象
 */
public class ProxyFactoryForCglib implements MethodInterceptor {
    //维护目标对象
    private Object target;

    public ProxyFactoryForCglib(Object target) {
        this.target = target;
    }

    //给目标对象创建一个代理对象
    public Object getProxyInstance(){
		System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\code");

		//1.工具类
        Enhancer en = new Enhancer();
        //2.设置父类
        en.setSuperclass(target.getClass());
        //3.设置回调函数
        en.setCallback(this);
        //4.创建子类(代理对象)
        return en.create();

    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("开始事务...");

        //执行目标对象的方法
        Object returnValue = method.invoke(target, args);

        System.out.println("提交事务...");

        return returnValue;
    }
}

测试类:

    @Test
    public void testCglib() {
        //目标对象
        UserDaoForCglib target = new UserDaoForCglib();

        //代理对象
        UserDaoForCglib proxy = (UserDaoForCglib) new ProxyFactoryForCglib(target).getProxyInstance();

		//执行代理对象的方法
        proxy.save();
    }

在CGLIB动态代理的过程中,字节码是运行时生成的,通常我们不能直接查看到这些字节码,因为它们是在内存中动态生成并直接加载的。但是,如果你想要分析这个过程,我们可以通过一些工具来打印或保存这些生成的字节码。

CGLIB本身并不提供直接打印字节码到控制台的功能,但是可以使用DebuggingClassWriter来将生成的字节码保存到文件系统中。然后,我们可以使用一些字节码查看工具来查看这些类的内容。
上面的getProxyInstance 方法中使用了System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\code") 方法将生成的字节码保存到指定的文件路径,此时将生成的字节码采用idea打开即可:
image-20240309105720151

总共会有3个类!按道理说应该只有一个,多出来的两个类怎么回事?
其实多出来的这两个class类就是为CGLIB中重要的fastClass机制而生成的。下面会另外讲解fastClass机制

CGLIB核心原理分析

Cglib的核心原理是在运行时动态生成字节码,以创建实例对象并拦截方法调用。当我们使用Enhancer创建代理对象时,Cglib会动态生成一个新的Java类,该类继承自被代理类,并覆盖被代理类的方法。在覆盖的方法中,Cglib会调用用户定义的MethodInterceptor回调,并将方法调用转发给被代理对象。

代理类分析

CGLIB动态代理在应用时,实际上是通过继承被代理类来创建一个子类,并在子类中覆写方法实现增强。在运行时,CGLIB会使用Java二进制代码生成技术,生成被代理类的子类的字节码,并加载到JVM中。这个过程并不需要被代理类的源代码。

CGLIB代理的原理可以简化为以下几步:

  1. 生成子类:实现对被代理类的继承,覆写其方法。
  2. 方法拦截:在子类中覆写的方法里,调用MethodInterceptor里的intercept方法来实现方法的拦截。
  3. 执行代理方法:通过MethodProxy来调用被代理类原有的方法,此时可以在调用前后执行自定义逻辑。

与JDK动态代理相比,CGLIB能够代理普通类,不仅仅是接口。这是因为CGLIB通过直接操作字节码,生成被代理类的子类,因此它不受只能代理接口的限制。

以下是简化后的代理类:UserDaoForCglib$$EnhancerByCGLIB$$5b79f296 类是由CGLIB生成的 HelloService 类的子类。类名中包含了原始类名、CGLIB特有的标识和一串哈希值,以保证类名的唯一性。

public class UserDaoForCglib$$EnhancerByCGLIB$$5b79f296 extends UserDaoForCglib implements Factory {
    private boolean CGLIB$BOUND;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private static final Method CGLIB$save$0$Method;
    private static final MethodProxy CGLIB$save$0$Proxy;
    
    static void CGLIB$STATICHOOK1() {
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        Class var0;
        ClassLoader var10000 = (var0 = Class.forName("com.apple.designpattern.objectenhance.proxy3.UserDaoForCglib$$EnhancerByCGLIB$$5b79f296")).getClassLoader();
        CGLIB$emptyArgs = new Object[0];
        CGLIB$save$0$Proxy = MethodProxy.create(var10000, (CGLIB$save$0$Method = Class.forName("com.apple.designpattern.objectenhance.proxy3.UserDaoForCglib").getDeclaredMethod("save")).getDeclaringClass(), var0, "()V", "save", "CGLIB$save$0"); 
    }

    final void CGLIB$save$0() {
        super.save();
    }

    public final void save() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$save$0$Method, CGLIB$emptyArgs, CGLIB$save$0$Proxy);
        } else {
            super.save();
        }
    }

   

    private static final void CGLIB$BIND_CALLBACKS(Object var0) {
        UserDaoForCglib$$EnhancerByCGLIB$$5b79f296 var1 = (UserDaoForCglib$$EnhancerByCGLIB$$5b79f296)var0;
        if (!var1.CGLIB$BOUND) {
            var1.CGLIB$BOUND = true;
            Object var10000 = CGLIB$THREAD_CALLBACKS.get();
            if (var10000 == null) {
                var10000 = CGLIB$STATIC_CALLBACKS;
                if (var10000 == null) {
                    return;
                }
            }

            var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
        }

    }

    static {
        CGLIB$STATICHOOK1();
    }
}

首先我们可以发现:由于是采用的是继承方式,因此final类无法使用CGLIB来进行代理。此外,对于static方法或final方法,由于这些方法无法被重写,所以CGLIB也无法为其提供代理。

所以我们字节码文件中也不能重写原来的saveFinalsaveStatic方法

代理方法分析

重点来看看save方法:

    public final void save() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$save$0$Method, CGLIB$emptyArgs, CGLIB$save$0$Proxy);
        } else {
            super.save();
        }
    }

1、动态代理的回调初始化

首先查找当前对象(this)中名为CGLIB$CALLBACK_0MethodInterceptor字段。这个字段存储了之前设置的回调接口实例,通常在代理对象生成阶段被初始化。如果CGLIB$CALLBACK_0null,则通过调用CGLIB$BIND_CALLBACKS(this)试图进行绑定或初始化。这个过程保证了在实际执行代理方法之前,回调接口已被正确设置。

2、方法拦截器的调用处理

随后进行一个判断,如果CGLIB$CALLBACK_0(也就是MethodInterceptor的实例)不为null,意味着我们有方法拦截逻辑需要执行。此时,通过调用拦截器的intercept方法来处理需要代理的方法(这里为save方法)的调用。

这个intercept方法的四个参数意义如下:

  1. this —— 代表当前代理对象的实例;
  2. CGLIB$save00Method —— 表示静态变量引用,它直接指向被代理类中的save方法的Method对象。CGLIB 通过 ASM(一种Java字节码操作和分析框架)在类加载时期生成代理类,所以这里使用直接指向save方法的引用提高了效率;
  3. CGLIB$emptyArgs —— 方法调用时本应传入的参数数组,这里表示save方法没有参数;
  4. CGLIB$save00Proxy —— 对应于save方法的代理方法引用,其内部逻辑由 CGLIB 生成并包含了原方法的调用。如果需要,可以通过它来直接调用原始save方法。

3、降级执行逻辑

如果CGLIB$CALLBACK_0null,也就是说没有为save方法设置拦截逻辑,则直接调用父类的save方法,这就完成了一个基本的方法拦截逻辑和调用。

所以正常情况我们会调用到var10000.intercept方法 最终也就是ProxyFactoryForCglib中的intercept方法,在这里我们就可以做自己的一些拦截操作,例如日志记录、权限检查、事务处理等等

public class ProxyFactoryForCglib implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("开始事务...");

        //执行目标对象的方法
        Object returnValue = method.invoke(target, args);

        System.out.println("提交事务...");

        return returnValue;
    }
}

FastClass机制分析

CGLIB的FastClass机制是其性能优化的一个重要方面。FastClass机制通过为被代理类和代理类各自生成一个FastClass类来加速方法的调用。FastClass不使用反射来调用被代理类的原始方法,而是采用索引号来直接调用,从而避免了反射调用的性能开销。
image-20240309185451144

  • UserDaoForCglib$$FastClassByCGLIB$$3c746232.class 给被代理类生成一个FastClass类
  • UserDaoForCglib$$EnhancerByCGLIB$$5b79f296$$FastClassByCGLIB$$f8bb03ec.class 给代理类生成一个FastClass类

FastClass机制背后的核心是一个巨大的switch语句,每一个case对应被代理类中的一个方法。调用方法时只需传入方法的索引和参数,FastClass即可直接定位并调用目标方法。

当我们调用intercept方法时,实际上是通过FastClass机制找到方法的索引,然后通过索引快速调用被代理的方法。

public class UserDaoForCglib$$FastClassByCGLIB$$3c746232 extends FastClass {

    public UserDaoForCglib(Class classToProxy) {
        super(classToProxy);
    }

    public int getIndex(String signature) {
        // 根据方法签名查找方法的索引
    }

    public Object invoke(int index, Object obj, Object[] args) {
        // 根据索引直接执行对应的方法
        UserDaoForCglib instance = (UserDaoForCglib) obj;
        switch(index) {
            case 0:
                instance.test();
                return null;
            default:
                throw new IllegalArgumentException();
        }
    }
}


FastClass主要完成了两个任务:(理解成MySQL通过索引快速定位查询数据)

  1. 将方法的调用转换成索引的调用,这个索引是在FastClass生成时就确定好的。
  2. 通过索引快速定位并直接调用目标方法,跳过反射调用的开销。

FastClass机制大大提升了CGLIB动态代理的调用效率,让动态代理的成本降低,这也是CGLIB在性能上通常优于JDK动态代理的一个重要原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值