当前位置: 首页 > news >正文

Java代理(四)动态代理之CGLIB

1 已有JDK Dynamic Proxy,为什么还要CGLIB?

JDK Dynamic Proxy 和 CGLib 是两种常用的代理技术,它们各自有不同的适用场景和局限性。在已经有了 JDK Dynamic Proxy 的情况下,还需要 CGLib 的原因:


1.1. 类 vs 接口

JDK Dynamic Proxy 只能代理实现了接口的类。
它通过 java.lang.reflect.Proxy 类生成代理对象,要求目标类必须实现一个或多个接口。

CGLib 可以代理没有实现接口的类。
它通过字节码操作(使用 ASM 库)动态生成目标类的子类来实现代理。


1.2. 功能限制

JDK Dynamic Proxy 无法代理类中的私有方法、静态方法或 final 方法。
它只能代理接口中声明的方法。

CGLib 可以代理类中的非私有方法(包括 protected 和 public 方法),并且支持对类本身进行增强。


1.3. 字节码增强需求

CGLib 提供了更强大的字节码操作能力,适用于需要深度修改类行为的场景。
例如,AOP 框架中可能需要拦截构造函数调用或修改类的内部逻辑,这在 JDK Dynamic Proxy 中是无法实现的。

2 怎样使用CGLIB实现动态代理?

CGLIB(Code Generation Library)是一个强大的、高性能的代码生成库,它广泛应用于AOP框架中,如Spring AOP。CGLIB通过生成一个被代理类的子类来实现代理,从而避免了Java代理的接口限制。以下是使用CGLIB实现动态代理的基本步骤:


2.1添加依赖

如果你使用的是Maven项目,需要在pom.xml中添加CGLIB的依赖:

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

2.2创建被代理类

创建一个普通的Java类,这个类的方法将被代理。这里使用前面讲过的示例代码: BusinessCalculator.

public class BusinessCalculator implements Calculator {

    @Override
    public Long add(Integer a, Integer b) {
        long result = 0;
        for(int i=0; i<100000000; i++) {
            result += i + a + b;
        }
        return result;
    }

    @Override
    public Long subtract(Integer a, Integer b) {
        long result = 0;
        for(int i=0; i<100000000; i++) {
            result += i + a - b;
        }
        return result;
    }

    @Override
    public Long multiply(Integer a, Integer b) {
        long result = 0;
        for(int i=0; i<100000000; i++) {
            result += i + a * b;
        }
        return result;
    }

    @Override
    public Long divide(Integer a, Integer b) {
        long result = 0;
        for(int i=0; i<100000000; i++) {
            result += i + a / b;
        }
        return result;
    }
}

2.3实现MethodInterceptor接口

创建一个拦截器类,实现net.sf.cglib.proxy.MethodInterceptor接口,并重写intercept方法

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class BusinessCalculatorInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before method: " + method.getName());
        
        // 调用原始方法
        Object result = proxy.invokeSuper(obj, args);
        
        System.out.println("After method: " + method.getName());
        return result;
    }
}

2.4. 创建代理对象

使用CGLIB的Enhancer类创建目标类的代理对象。
示例代码如下:

import net.sf.cglib.proxy.Enhancer;

public class CglibProxyDemo {

    public static void main(String[] args) {
        // 创建Enhancer对象
        Enhancer enhancer = new Enhancer();
        // 设置目标类
        enhancer.setSuperclass(BusinessCalculator.class);
        // 设置回调函数
        enhancer.setCallback(new BusinessCalculatorInterceptor());
        // 创建代理对象
        BusinessCalculator proxyInstance = (BusinessCalculator) enhancer.create();

        // 调用代理对象的方法
        System.out.println("Result of add: " + proxyInstance.add(1, 2));
        System.out.println("Result of subtract: " + proxyInstance.subtract(5, 3));
        System.out.println("Result of multiply: " + proxyInstance.multiply(6, 7));
        System.out.println("Result of divide: " + proxyInstance.divide(8, 9));
    }

}

注意事项
目标类不能为final:CGLIB通过生成目标类的子类来实现动态代理,因此目标类不能声明为final。
性能开销:CGLIB在运行时生成字节码,可能会带来一定的性能开销。


通过以上步骤,您可以成功使用CGLIB实现动态代理。

2.5 运行效果

我的运行环境是JDK17, 运行上面代码时抛出异常:

Exception in thread "main" java.lang.ExceptionInInitializerError
	at org.derek.CglibProxyDemo.main(CglibProxyDemo.java:9)
Caused by: net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InaccessibleObjectException-->Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @4f2410ac
	at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:464)
	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:96)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:94)
	at net.sf.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
	at net.sf.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:119)
	at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
	at net.sf.cglib.core.KeyFactory$Generator.create(KeyFactory.java:221)
	at net.sf.cglib.core.KeyFactory.create(KeyFactory.java:174)
	at net.sf.cglib.core.KeyFactory.create(KeyFactory.java:153)
	at net.sf.cglib.proxy.Enhancer.<clinit>(Enhancer.java:73)
	... 1 more

这个错误是由于 Java 9 及以上版本引入的模块化系统(Java Platform Module System, JPMS)导致的。模块化系统对类的访问进行了严格的限制,而CGLIB尝试通过反射访问 java.lang 包中的类或方法,从而导致 java.lang.ClassFormatError。

解决方案:

  • IntelliJ IDEA

    1. 打开运行配置(Run Configurations)。

    2. 在 VM options 中添加 --add-opens java.base/java.lang=ALL-UNNAMED

添加上面的VM 参数后,运行效果如下:
 

"C:\Program Files\Java\jdk-17\bin\java.exe" --add-opens java.base/java.lang=ALL-UNNAMED " org.derek.CglibProxyDemo


Before method: add
After method: add
Result of add: 5000000250000000
Before method: subtract
After method: subtract
Result of subtract: 5000000150000000
Before method: multiply
After method: multiply
Result of multiply: 5000004150000000
Before method: divide
After method: divide
Result of divide: 4999999950000000

3 CGLIB 实现动态代理的原理?

CGLIB 实现动态代理的原理主要基于字节码生成技术。以下是其核心原理:

3.1. 子类生成

CGLIB 通过在运行时动态生成目标类的子类来实现代理。它会继承目标类,并重写其中的所有非 final 方法。
继承机制:CGLIB 创建的目标类的子类会覆盖父类的方法,从而可以在方法调用前后插入自定义逻辑。
限制条件:目标类不能是 final 类型,且方法不能是 final 或 static,因为这些方法无法被子类覆盖。

3.2. 拦截器回调

CGLIB 使用回调机制(如 MethodInterceptor)来拦截目标方法的调用。当代理对象的方法被调用时,实际执行的是子类中重写的方法,而该方法会通过回调机制将控制权交给拦截器。
核心接口:MethodInterceptor 是 CGLIB 提供的核心接口,开发者需要实现该接口的 intercept 方法。
拦截逻辑:在 intercept 方法中,可以添加方法调用前后的逻辑,并通过MethodProxy.invokeSuper 调用原始方法。

3.3. 字节码生成

CGLIB 基于 ASM(一个 Java 字节码操作框架)生成字节码。它会在运行时动态创建一个新的类,该类继承了目标类并实现了所需的代理逻辑。
字节码操作:CGLIB 会解析目标类的字节码结构,生成一个新的子类字节码,并加载到 JVM 中。
性能优化:由于字节码生成和加载的过程较为复杂,CGLIB 的初始化开销较大,但一旦生成完成,运行时性能较高。

3.4. Enhancer 工具类

CGLIB 提供了 Enhancer 类作为核心工具,用于创建代理对象。
设置父类:通过 enhancer.setSuperclass(Class) 指定目标类。
设置回调:通过 enhancer.setCallback(Callback) 设置拦截器。
创建代理:调用 enhancer.create() 方法生成代理对象

3.5. 方法调用流程

以下是 CGLIB 动态代理的方法调用流程:
用户调用代理对象的方法。
代理对象(实际上是目标类的子类)拦截该方法调用。
调用 MethodInterceptor.intercept 方法,执行拦截逻辑。
在拦截器中通过 MethodProxy.invokeSuper 调用目标类的原始方法。
返回结果给用户。

3.6 示例代码说明

结合上下文中的 BusinessCalculator 类的add方法:

public class BusinessCalculator {
    public Long add(Integer a, Integer b) {
        long result = 0;
        for (int i = 0; i < 100000000; i++) {
            result += i + a + b;
        }
        return result;
    }
}

GLIB 会生成一个 BusinessCalculator 的子类,

BusinessCalculator$$EnhancerByCGLIB$$9b8a9c45

子类中包含下面的代码:

public final Long add(Integer var1, Integer var2) {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        return var10000 != null ? (Long)var10000.intercept(this, CGLIB$add$0$Method, new Object[]{var1, var2}, CGLIB$add$0$Proxy) : super.add(var1, var2);
    }

子类会覆盖 add 方法,并在方法内部调用拦截器的 intercept 方法。
拦截器中可以通过 MethodProxy.invokeSuper 调用原始的 add 方法。

怎样查看所有生成的源文件?

// 启用CGLIB调试模式,将生成的字节码文件保存到指定目录
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "net\\cglib");

感兴趣的童鞋,可以添加上面的代码,查看生成的类的源文件。结构如下:

总结
CGLIB 的核心原理是通过字节码生成技术动态创建目标类的子类,并利用回调机制拦截方法调用。相比 JDK 动态代理(基于接口),CGLIB 更适合对没有实现接口的类进行代理,但在性能和灵活性上各有优劣。

相关文章:

  • 单网卡上绑定多个虚拟IP(AI回答)
  • linux部署成功,但外网无法访问
  • 数据结构与算法:子数组最大累加和问题及扩展
  • 百度查询的ip与命令行输入 ipconfig 显示的IP地址有以下主要区别:
  • 管家婆财贸ERP BB102.采购销售订金管理
  • 快速生成mysql测试数据10w条
  • CSP/信奥赛C++中格式化输入输出scanf和printf的使用详解
  • 快速上手示例(以BEVFormer为例)
  • 【蓝桥杯】考前冲刺!
  • Unity中的静态合批使用整理
  • Oracle 数据库中,并行 DML
  • XSLFO XSLT:深入解析两种强大的XML转换技术
  • leetcode0069. x 的平方根-easy
  • 从零构建大语言模型全栈开发指南:第五部分:行业应用与前沿探索-5.1.2行业落地挑战:算力成本与数据隐私解决方案
  • 操作系统(二):实时系统介绍与实例分析
  • PM2 在 Node.js 项目中的使用与部署指南
  • 【力扣hot100题】(047)路径总和Ⅲ
  • 如何在Android中使用匿名内部类?
  • 人工智能混合编程实践:C++调用封装好的DLL进行图像超分重建
  • MinIO 入门指南:高性能对象存储的安装与使用