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

文章目录

    • CGLIB介绍
    • CGLIB使用示例
    • CGLIB核心原理分析
      • 代理类分析
      • 代理方法分析
    • FastClass机制分析

在这里插入图片描述

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动态代理的一个重要原因。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/443109.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【echarts】xAxis鼠标事件失效问题

项目中用到echarts柱状图&#xff0c;出现x轴标签文字过长重叠问题&#xff0c;在pass掉标签倾斜、换行方案之后最终决定限制文字长度&#xff0c;超出以…占位&#xff0c;鼠标悬浮时显示完整tooltip。 但编写过程中发现xAxis鼠标事件无法触发&#xff0c;只有bar区域是可触发…

19 卷积层【李沐动手学深度学习v2课程笔记】

目录 1. 从全连接到卷积 2. 卷积层 3. 图像卷积代码 3.1 互相关运算 3.2 实现二维卷积层 3.3 图像中目标的边缘检测 3.4 学习卷积核 4. 小结 1. 从全连接到卷积 在欧几里得几何中&#xff0c;平移是一种几何变换&#xff0c;表示把一幅图像或一个空间中的每一个点在相同…

mysql中insert … select锁范围

1、执行 insert … select 的时候&#xff0c;对目标表也不是锁全表&#xff0c;而是只锁住需要访问的资源。 例如&#xff0c; CREATE TABLE t (id int(11) NOT NULL AUTO_INCREMENT,c int(11) DEFAULT NULL,d int(11) DEFAULT NULL,PRIMARY KEY (id),UNIQUE KEY c (c) ) EN…

streamlit初学-用streamlit实现云台控制界面

用streamlit实现云台控制界面 效果图PC上的效果手机上的效果 源码: 本文演示了,如何用streamlit做一个云台控制界面。功能包括:用户登录,事件的处理,图片的更新 版本信息: streamlit_authenticator: 下载链接streamlit : 1.31.1python: 3.11 修改点: streamlit_authenticato…

【嵌入式】字体极限瘦身术:Fontmin在嵌入式UI中的魔法应用(附3500常用汉字)

1. 概述 在嵌入式系统的用户界面&#xff08;UI&#xff09;设计中&#xff0c;字体的选择和优化至关重要。一个恰当的字体不仅能够提升用户体验&#xff0c;还能彰显产品特色。然而&#xff0c;由于嵌入式设备常常受限于存储空间和处理能力&#xff0c;大型字体文件可能成为性…

arkTS语法

lineHeight与css不同&#xff1f; 1、arkTS是什么 在继承了TS语法的基础上&#xff0c;主要扩展了声明式UI开发相关的能力 声明式UI是一种编写用户界面的范式。 2、声明组件的完整语法 3、自定义组件的语法使用 struct arkTS新增的关键字&#xff0c;是用于自定义组件或者自…

餐饮行业咨询数据在哪里查找?

1.中国饭店协会&#xff1a;国资委和商务部等政府指导发展&#xff0c;参与制定行业国家标准、行业标准与行业自律规则。按月出版《中国饭店业》会员刊物、及时更新协会官方网站和官方微信&#xff0c;方便会员单位及时掌握国内外饭店与餐饮业的最新动态。宣传企业经典案例、反…

使用jquery的autocomplete属性实现联想补全操作

平时使用百度&#xff0c;淘宝等软件搜索时&#xff0c;常见一个搜索框联想提示&#xff0c;感觉确实好用但没有研究过原理&#xff0c;最近恰巧工作中遇到一个同样的场景&#xff0c;不同于大厂使用高端的Python&#xff0c;这次需要使用jQuery的autocomplete属性来自动联想补…

【深度学习笔记】6_7 门控循环单元(GRU)

注&#xff1a;本文为《动手学深度学习》开源内容&#xff0c;部分标注了个人理解&#xff0c;仅为个人学习记录&#xff0c;无抄袭搬运意图 6.7 门控循环单元&#xff08;GRU&#xff09; 上一节介绍了循环神经网络中的梯度计算方法。我们发现&#xff0c;当时间步数较大或者…

vue 下载的插件从哪里上传?npm发布插件详细记录

文章参考&#xff1a; 参考文章一&#xff1a; 封装vue插件并发布到npm详细步骤_vue-cli 封装插件-CSDN博客 参考文章二&#xff1a; npm发布vue插件步骤、组件、package、adduser、publish、getElementsByClassName、important、export、default、target、dest_export default…

HTML静态网页成品作业(HTML+CSS+JS)——和平精英介绍设计制作(4个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;使用Javacsript代码实现图片轮播&#xff0c;共有4个页面。 二、作品…

Pytorch学习 day08(最大池化层、非线性激活层)

最大池化层 最大池化&#xff0c;也叫上采样&#xff0c;是池化核在输入图像上不断移动&#xff0c;并取对应区域中的最大值&#xff0c;目的是&#xff1a;在保留输入特征的同时&#xff0c;减小输入数据量&#xff0c;加快训练。参数设置如下&#xff1a; kernel_size&#…

微信加好友频繁会被封号吗?

微信加好友频繁会被封号吗&#xff1f; 微信规定,每个人每天最多可以加20个好友&#xff0c;但一天之内如果频繁加好友&#xff0c;微信可能会出现异常提示&#xff0c;需要暂停好友添加操作。 面对微信上突如其来的大量好友申请&#xff0c;一定要谨慎处理&#xff0c;以免被…

Golang搭建grpc环境

简介 OS : Windows 11 Golang 版本: go1.22.0 grpc : 1.2 protobuffer: 1.28代理 没有代理国内环境下载不了库七牛CDN &#xff08;试过可用&#xff09; go env -w GOPROXYhttps://goproxy.cn,direct阿里云代理(运行grpc时下载包出现报错 ): go env -w GOPROXYhttps://mirr…

CCProxy代理服务器地址的设置步骤

目录 前言 一、下载和安装CCProxy 二、启动CCProxy并设置代理服务器地址 三、验证代理服务器设置是否生效 四、使用CCProxy进行代理设置的代码示例 总结 前言 CCProxy是一款常用的代理服务器软件&#xff0c;可以帮助用户实现网络共享和上网代理。本文将详细介绍CCProxy…

IntelliJ IDEA 2020.2.4试用方法

打开idea&#xff0c;准备好ide-eval-resetter压缩包。 将准备好的压缩包拖入idea中 选中弹窗中的自动重置选项&#xff0c;并点击重置 查看免费试用时长

[数据集][目标检测]变电站缺陷检测数据集VOC+YOLO格式8307张17类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;8307 标注数量(xml文件个数)&#xff1a;8307 标注数量(txt文件个数)&#xff1a;8307 标注…

汽车大灯汽车尾灯破裂裂纹破损破洞掉角崩角等问题能修复吗?修复后需要注意什么?

汽车灯罩破损修复后&#xff0c;车主需要注意以下几点&#xff1a; 检查修复效果&#xff1a;修复完成后&#xff0c;车主应该仔细检查灯罩的修复效果&#xff0c;确保破损部分已经被填补并恢复原有的透明度和光泽。如果修复效果不理想&#xff0c;需要及时联系维修店进行处理…

问题:前端获取long型数值精度丢失,后面几位都为0

文章目录 问题分析解决 问题 通过接口获取到的数据和 Postman 获取到的数据不一样&#xff0c;仔细看 data 的第17位之后 分析 该字段类型是long类型问题&#xff1a;前端接收到数据后&#xff0c;发现精度丢失&#xff0c;当返回的结果超过17位的时候&#xff0c;后面的全…

什么是工业级物联网智能网关?如何远程控制PLC?

在这个信息爆炸的时代&#xff0c;物联网技术已经逐渐渗透到我们生活的方方面面&#xff0c;而工业级物联网智能网关作为连接工业设备和云端的重要桥梁&#xff0c;更是引领着工业4.0时代的浪潮。那么&#xff0c;究竟什么是工业级物联网智能网关呢&#xff1f;今天&#xff0c…