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

news2025/6/20 3:23:27

文章目录

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MACBOOK PRO M2 MAX 安装Stable Diffusion及文生图实例

以前偶尔会使用Midjourney生成一些图片&#xff0c;现在使用的头像就是当时花钱在Midjourney上生成的。前段时间从某鱼上拍了一台性价比还不错的macbook&#xff0c;想着不如自己部署Stable Diffusion&#xff08;以下简称SD&#xff09;尝试一下。 网上有很多教程&#xff0c…

四个领域,企业官网依然无可替代。

2023-10-23 14:17贝格前端工场 企业官网在以下领域无可替代&#xff1a; 专业性强的领域&#xff1a;如金融、法律、医学等&#xff0c;这些领域专业性很强&#xff0c;需要权威、专业的官网来提供详细、准确的信息1。需要展示企业形象、实力的领域&#xff1a;如制造业、房地…

【2023最全kafka面试和答案】

2023最全kafka面试和答案 ​ 1.Kafka中的ISR(InSyncReplicate)、OSR(OutSyncReplicate)、AR(AllReplicate)代表什么&#xff1f; ISR : 速率和leader相差低于10秒的follower的集合OSR : 速率和leader相差大于10秒的followerAR : 所有分区的followerARISROSR 2.Kafka中的HW、L…

Flink 学习3 - 流处理API的基本转换算子 + 多流转换算子

流处理API-Transform 1、基本转换算子 map、flatMap、filter通常被统一称为基本转换算子&#xff08;简单转换算子&#xff09; DataStream 里没有 reduce 和 sum 这类聚合操作的方法&#xff0c;因为 Flink 设计中&#xff0c;所有数据必须先分组才能做聚合操作。 先 keyB…

Java设计模式:建造者模式之经典与流式的三种实现(四)

本文将深入探讨Java中建造者模式的两种实现方式&#xff1a;经典建造者与流式建造者。建造者模式是一种创建型设计模式&#xff0c;它允许你构建复杂对象的步骤分解&#xff0c;使得对象的创建过程更加清晰和灵活。我们将通过示例代码详细解释这两种实现方式&#xff0c;并分析…

图分割 Graph Partition 学习笔记1

文章目录 前言一、graph-partition是什么&#xff1f;二、具体分类三、graph-partition的意义参考链接 前言 最近在学习图论划分的方法&#xff0c;碰巧搜索到了这个算是对我而言全新的一个体系&#xff0c;在这里将逐步记载自己的学习资料和进度&#xff0c;希望和大家一起探讨…

《汇编语言》第3版(王爽)实验9

第9章 实验9 编程&#xff1a;在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串 ‘welcome to masm!’ assume cs:code,ds:datadata segmentdb welcome to masm!,0 data endscode segmentstart:mov ax,data mov ds,ax ;ds指向data段mov ax,0B800H ;显存空间从B800H…

波奇学Linux: 信号捕捉

sigaction:修改信号对应的handler方法 act输入型参数&#xff0c;oldact输出型参数 void (*sa_handler) (int) //修改的自定义函数 sigset_t sa_mask // void handler(int signo) {cout<<"catch a signal, signal number: "<<signo<<endl; } int …

C#实现快速排序算法

C#实现快速排序算法 以下是C#中的快速排序算法实现示例&#xff1a; using System;class QuickSort {// 快速排序入口函数public static void Sort(int[] array){QuickSortRecursive(array, 0, array.Length - 1);}// 递归函数实现快速排序private static void QuickSortRecu…

记录一次排查负载均衡不能创建的排查过程

故障现象&#xff0c;某云上&#xff0c;运维同事在创建负载均衡的时候&#xff0c;发现可以创建资源&#xff0c;但是创建完之后&#xff0c;不显示对应的负载均衡。 创建负载均衡时候&#xff0c;按f12发现console有如下报错 后来请后端网络同事排查日志发现&#xff0c;是后…

机器学习--循环神经网络(RNN)3

本篇文章结合具体的例子来介绍一下LSTM运算方式以及原理。请结合上篇文章的介绍食用。 一、具体例子 如上图所示&#xff0c;网络里面只有一个 LSTM 的单元&#xff0c;输入都是三维的向量&#xff0c;输出都是一维的输出。 这三维的向量跟输出还有记忆元的关系是这样的。 假设…

【蓝桥杯】k倍区间

一.题目描述 二.问题分析 对于该问题&#xff0c;标签上写的是暴力&#xff0c;但是如果使用暴力的话&#xff0c;会超时。 首先&#xff0c;对于两个数a&#xff0c;b&#xff08;假设a小于b&#xff09;&#xff0c;若a与b对k取余后结果相同&#xff0c;则b-a可以整除k。 …

Edu 18 Colored balls -- 题解

目录 Colored Balls&#xff1a; 题目大意: 思路解析&#xff1a; 代码实现&#xff1a; Colored Balls&#xff1a; 题目大意: 思路解析&#xff1a; 我们对于一个数n&#xff0c;如果分组大小超过了 根号n&#xff0c;那么便不可能将n 分为多个组&#xff0c;并且组间差距…

苍穹外卖学习-----2024/03/09

1.菜品分页查询 代码在这里 分页查询菜品 2.删除菜品 [链接]param 1、概览 本文将带你了解 Spring 中 RequestParam 注解的用法。 简单地说&#xff0c;可以使用 RequestParam 从请求中提取查询参数、表单参数甚至是多个参数。 2、示例端点 假设我们有一个端点 /api/foos&a…

qt一个项目只能有一个QMainWindow,其他小窗口只能继承QWidget

我继承QMainWindow&#xff0c;结果就出现奇奇怪怪的现象&#xff0c;我人都疯了 这些接口全他妈不能用 删了换成QWidget就可以用了

重建大师下图界面上的颜色区域分别代表着什么?

分别代表相对精度、绝对精度。 重建大师是一款专为超大规模实景三维数据生产而设计的集群并行处理软件&#xff0c;输入倾斜照片&#xff0c;激光点云&#xff0c;POS信息及像控点&#xff0c;输出高精度彩色网格模型&#xff0c;可一键完成空三、自动建模和LOD构建。 下载地…

idea连接远程服务器

1. 双击shift&#xff0c;出现如下界面 2. 远程连接 原文来自这个up主的&#xff0c;点击蓝色字体就可以跳转啦&#xff01; 输入主机ip、用户名、密码&#xff0c;点击Test Connection验证&#xff0c;最后点击ok添加成功 有用的话记得给俺点个赞&#xff0c;靴靴~

赶紧来学Python回调函数

在Python中&#xff0c;回调函数是一种非常重要的编程概念&#xff0c;它允许我们将一个函数作为参数传递给另一个函数&#xff0c;并在需要时由另一个函数调用。回调函数的使用可以使代码更加灵活和可重用&#xff0c;尤其在异步编程、事件驱动编程中非常常用。 1.普通函数调…

波动数列 刷题笔记

思路分析 dp 找出状态转移方程 设d为a或者-b 代码 #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N1010,MOD100000007; int get_mod(int a,int b){ return (a%bb)%b; …

悲观锁和乐观锁

悲观锁&#xff1a;比较悲观&#xff0c;认为线程安全问题一定会发生&#xff0c;因此在操作数据之前先获取锁&#xff0c;确保线程串行执行。-例如Synchronized、Lock都属于悲观锁。 乐观锁&#xff1a;比较乐观&#xff0c;认为线程安全问题不一定会发生&#xff0c;因此不加…