探索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();}@Overridepublic 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;}
}

测试类:

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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VSCODE解决git合并过程中的冲突问题;error: failed to push some refs to

1&#xff1a;异常现象 推送有冲突的git修改到远端的时候&#xff0c;会有如下提示 git.exe push --progress “origin” master:master To http://gitlab.xxx.com/dujunqiu/test.git ! [rejected] master -> master (fetch first) error: failed to push some refs to ‘…

【蓝牙协议栈】【经典蓝牙】【BLE蓝牙】蓝牙协议规范(HCI、L2CAP、SDP、RFOCMM)

目录 1. 蓝牙协议规范&#xff08;HCI、L2CAP、SDP、RFOCMM&#xff09; 1.1 主机控制接口协议 HCI 1.2 逻辑链路控制与适配协议 L2CAP 1.3 服务发现协议SDP 1.4 串口仿真协议 RFCOMM 1. 蓝牙协议规范&#xff08;HCI、L2CAP、SDP、RFOCMM&#xff09; 1.1 主机控制接口协…

如何将应用一键部署至多个环境?丨Walrus教程

在 Walrus 平台上&#xff0c;运维团队在资源定义&#xff08;Resource Definition&#xff09;中声明提供的资源类型&#xff0c;通过设置匹配规则&#xff0c;将不同的资源部署模板应用到不同类型的环境、项目等。与此同时&#xff0c;研发人员无需关注底层具体实现方式&…

2024年学生服务器申请流程,以阿里云学生机为例

阿里云学生服务器免费申请&#xff0c;之前是云翼计划学生服务器9元/月&#xff0c;现在是高校计划&#xff0c;学生服务器可以免费申请&#xff0c;先完成学生认证即可免费领取一台云服务器ECS&#xff0c;配置为2核2G、1M带宽、40G系统盘&#xff0c;在云服务器ECS实例过期之…

【Pytorch、torchvision、CUDA 各个版本对应关系以及安装指令】

Pytorch、torchvision、CUDA 各个版本对应关系以及安装指令 1、名词解释 1.1 CUDA CUDA&#xff08;Compute Unified Device Architecture&#xff09;是由NVIDIA开发的用于并行计算的平台和编程模型。CUDA旨在利用NVIDIA GPU&#xff08;图形处理单元&#xff09;的强大计算…

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

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

2024年如何批量下载知乎问题下的所有回答?

最近写了个批量下载知乎问题下的回答工具2024 年开发的第一个知乎脚本神器 &#xff1a; 导出的excel文件包含每个回答的回答链接&#xff0c;回答作者&#xff0c;回答内容&#xff0c;回答时间和回答的更新时间&#xff0c;本来想把回答里的图片也下载了&#xff0c;但是有些…

2024最新多目标优化算法:多目标螳螂搜索算法MOMSA求解46个多目标测试函数+1个工程应用+4种评价指标(提供MATLAB代码)

一、多目标螳螂搜索算法MOMSA 多目标螳螂搜索算法&#xff08;Multi-objective Mantis Search Algorithm &#xff0c;MOMSA&#xff09;由Mohammed Jameel和Mohamed Abouhawwash于2024年提出&#xff0c;其灵感来自于螳螂独特的狩猎行为和性同类相食行为。所提出的 MOMSA 算法…

20行代码搞定PDF表格转为Excel表

1.环境准备 安装好python并且配置好环境安装pdfplumber、xlwt库使用Vscode或者PyCharm等编辑器 在pycharm中如果报红&#xff0c;可以鼠标点击报红的库&#xff0c;altenter进行安装 2.代码部分 import pdfplumber import xlwt # 读取源pdf文件 pdf pdfplumber.open("…

(产品之美系列三)小红书投票组建,利用用户好奇心,增大互动

小红书发布笔记或者视频&#xff0c;可以带一个投票功能。此投票功能与其他的有什么不同呢&#xff1f; 发布一个话题:你觉得王维和李白哪个更帅&#xff1f; 如果你自己不投票&#xff0c;就是看不到结果。当你投票之后: 可以知道选择王维的有百分之八十二。 启发:小红书投…

为什么要做接口测试? 怎么用Jmeter接口测试工具? 你都会了吗? 这里给大家全面介绍

Jmeter是Apache公司开发的基于Java语言的压力测试工具&#xff0c;可以做接口测试&#xff0c;也可以做性能测试。 jdk:建议1.8以上 jmeter:不要用最新版。用最新版的下1-2个版本 一、什么是接口以及为什么要做接口测试 如果要进行接口必需了解什么是接口&#xff1f; 接口…

就业班 2401--3.8 Linux Day14--阿帕奇+LNMP(编译安装)

一、WEB服务器 ^世上最重要的事&#xff0c;不在于我们在何处&#xff0c;而在于我们朝着什么方向走。^ 1、WEB服务简介 # 目前最主流的三个Web服务器是Apache、Nginx、 IIS。 - WEB服务器一般指网站服务器&#xff0c;可以向浏览器等Web客户端提供网站的访问&#xff0c;让全…