JDK动态代理vs CGLIB:字节码生成的本质区别与性能对比
JDK动态代理vs CGLIB:字节码生成的本质区别与性能对比
适读人群:想从底层理解Java代理机制的开发者 | 阅读时长:约18分钟
开篇故事
我第一次读 JDK 动态代理源码的时候,最困惑的问题是:Proxy.newProxyInstance() 到底怎么"凭空"造出一个实现了接口的类?
这个类既没有 .java 文件,也没有 .class 文件,它是怎么来的?
后来我找到了 sun.misc.ProxyGenerator,看到了 generateProxyClass() 方法,才明白:这个类是在运行时直接生成的字节码数组,然后通过 ClassLoader.defineClass() 加载进 JVM 的。不需要源文件,不需要编译,直接写字节码。
CGLIB 也是类似的思路,只是它通过 ASM 库操作字节码,功能更强大,连 class 文件都不需要——一切都在内存里完成。
这两个机制的底层,都是 字节码操作。
一、JDK 动态代理的工作原理
1.1 使用方式
// 最简单的JDK动态代理示例
UserService proxy = (UserService) Proxy.newProxyInstance(
UserServiceImpl.class.getClassLoader(), // 类加载器
new Class<?>[]{UserService.class}, // 代理的接口列表
(proxyObj, method, args) -> { // InvocationHandler
System.out.println("Before: " + method.getName());
Object target = new UserServiceImpl();
Object result = method.invoke(target, args);
System.out.println("After: " + method.getName());
return result;
}
);1.2 生成的代理类结构
JDK 动态代理生成的类大概长这样(通过反编译字节码还原):
// 动态生成的类,不存在于磁盘,运行时由 ProxyGenerator 生成字节码
public final class $Proxy0 extends Proxy implements UserService {
// 缓存Method对象,避免每次调用都反射查找
private static Method m0; // hashCode
private static Method m1; // equals
private static Method m2; // toString
private static Method m3; // createUser(UserService的方法)
static {
try {
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m1 = Class.forName("java.lang.Object").getMethod("equals", Object.class);
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.laozhang.UserService").getMethod("createUser", String.class);
} catch (NoSuchMethodException | ClassNotFoundException e) {
throw new NoSuchMethodError(e.getMessage());
}
}
public $Proxy0(InvocationHandler h) {
super(h);
}
// 每个接口方法都转发给 InvocationHandler.invoke()
@Override
public void createUser(String name) {
try {
// h 就是我们传入的 InvocationHandler
h.invoke(this, m3, new Object[]{name});
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
}1.3 如何把生成的代理类保存到磁盘(调试用)
// JDK 8:通过系统属性开启
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// JDK 11+:换了API
System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
// 然后调用 Proxy.newProxyInstance(),代理类会保存到 ./com/sun/proxy/$Proxy0.class二、CGLIB 的工作原理
2.1 CGLIB 通过继承实现代理
// CGLIB 代理示例
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class); // 设置父类(被代理的类)
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("[CGLIB] Before: " + method.getName());
Object result = proxy.invokeSuper(obj, args); // 调用父类方法
System.out.println("[CGLIB] After: " + method.getName());
return result;
});
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();2.2 CGLIB 生成的代理类结构
// CGLIB 动态生成的子类(概念示意)
public class UserServiceImpl$$EnhancerByCGLIB$$abc123 extends UserServiceImpl {
// CGLIB用的回调接口
private MethodInterceptor CGLIB$CALLBACK_0;
// 缓存Method对象
private static final Method CGLIB$createUser$0$Method;
private static final MethodProxy CGLIB$createUser$0$Proxy;
static {
CGLIB$STATICHOOK1();
}
@Override
public void createUser(String name) {
MethodInterceptor interceptor = CGLIB$CALLBACK_0;
if (interceptor == null) {
// 如果没有拦截器,直接调用父类
super.createUser(name);
} else {
// 调用拦截器
interceptor.intercept(
this,
CGLIB$createUser$0$Method,
new Object[]{name},
CGLIB$createUser$0$Proxy // MethodProxy,可以调用父类原始方法
);
}
}
}2.3 JDK 代理 vs CGLIB 核心差异
三、性能对比与完整测试
3.1 理论分析
| 维度 | JDK动态代理 | CGLIB |
|---|---|---|
| 创建代理对象 | 较快(字节码简单) | 较慢(字节码复杂,需生成子类) |
| 方法调用 | 有反射开销(Method.invoke()) | JDK8之前有开销,JDK8+通过MethodProxy优化 |
| 内存占用 | 小 | 稍大(生成更多字节码) |
| 限制 | 必须有接口 | 不能代理final类和final方法 |
重要:从 JDK 8 开始,JDK 动态代理的反射调用在 JIT 热身后性能大幅提升,CGLIB 的性能优势已经不明显。Spring Boot 选择默认 CGLIB 更多是为了通用性(不需要接口),而不是性能原因。
3.2 JMH 性能测试代码
package com.laozhang.aop.benchmark;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.lang.reflect.Proxy;
import java.util.concurrent.TimeUnit;
/**
* 代理性能对比基准测试
* 运行:mvn clean package -Pbenchmark && java -jar target/benchmarks.jar
*/
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class ProxyBenchmark {
private HelloService directCall;
private HelloService jdkProxy;
private HelloService cglibProxy;
@Setup
public void setUp() {
HelloServiceImpl target = new HelloServiceImpl();
// 直接调用(基准)
directCall = target;
// JDK动态代理
jdkProxy = (HelloService) Proxy.newProxyInstance(
HelloService.class.getClassLoader(),
new Class<?>[]{HelloService.class},
(proxy, method, args) -> method.invoke(target, args)
);
// CGLIB代理
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloServiceImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) ->
methodProxy.invokeSuper(obj, args)
);
cglibProxy = (HelloService) enhancer.create();
}
@Benchmark
public String directInvoke() {
return directCall.sayHello("World");
}
@Benchmark
public String jdkProxyInvoke() {
return jdkProxy.sayHello("World");
}
@Benchmark
public String cglibProxyInvoke() {
return cglibProxy.sayHello("World");
}
}
interface HelloService {
String sayHello(String name);
}
class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "Hello, " + name;
}
}3.3 测试结果(参考值,实际以运行为准)
Benchmark Mode Cnt Score Error Units
ProxyBenchmark.directInvoke thrpt 5 876234.432 ± 12345.0 ops/ms
ProxyBenchmark.jdkProxyInvoke thrpt 5 245678.123 ± 8901.0 ops/ms
ProxyBenchmark.cglibProxyInvoke thrpt 5 298456.789 ± 9234.0 ops/ms结论:
- 直接调用最快(无代理开销)
- CGLIB 比 JDK 代理略快约 20%(JDK 8+,差距已经不大)
- 对于普通业务场景,两者的性能差异可以忽略不计
四、踩坑实录
坑1:CGLIB 代理对象序列化问题
症状:把 CGLIB 代理对象放入 Session 或分布式缓存,反序列化时报错。
根因:CGLIB 生成的子类有额外的字段(CGLIB$CALLBACK_0 等),序列化后反序列化时找不到对应的类定义。
解决方案:不要序列化代理对象,序列化前先获取真实对象:
// 获取被代理的原始对象
Object target = AopProxyUtils.getSingletonTarget(proxy);
if (target == null) {
target = proxy; // 不是代理对象,直接用
}
// 序列化 target坑2:JDK 代理强转失败(ClassCastException)
症状:UserService proxy = (UserServiceImpl) Proxy.newProxyInstance(...) 报 ClassCastException。
根因:JDK 代理生成的是 $Proxy0,它继承自 Proxy,实现了 UserService 接口,但不是 UserServiceImpl 的子类。不能强转为实现类类型,只能强转为接口类型。
// 错误:不能转换为实现类
UserServiceImpl proxy = (UserServiceImpl) Proxy.newProxyInstance(...); // ClassCastException
// 正确:只能转换为接口
UserService proxy = (UserService) Proxy.newProxyInstance(...); // OK坑3:CGLIB 在 Java 17 模块化下报 InaccessibleObjectException
症状:升级到 Java 17 后,CGLIB 报 java.lang.reflect.InaccessibleObjectException: Unable to make protected xxx accessible。
根因:Java 9+ 的模块系统(JPMS)限制了跨模块的反射访问,CGLIB 需要访问被代理类的构造器,但模块边界阻止了这一点。
解决方案 1:在 JVM 启动参数里添加 opens:
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED解决方案 2:Spring Boot 3.x 使用 ObjenesisCglibAopProxy,通过 Objenesis 库绕过构造器,不需要反射访问,能解决大部分这类问题。
坑4:代理对象的 equals 和 hashCode 行为
症状:把代理对象放入 HashSet,发现同一个目标对象的代理被认为是不同的元素。
根因:JDK 代理默认不代理 equals 和 hashCode(它们直接在代理类本身处理),而 CGLIB 代理的 hashCode 是代理对象自己的,不等于原始对象的 hashCode。
验证代码:
UserServiceImpl target = new UserServiceImpl();
UserService proxy = createJdkProxy(target);
System.out.println(target.hashCode()); // 目标对象的hashCode
System.out.println(proxy.hashCode()); // 代理对象的hashCode(不同!)
System.out.println(target.equals(proxy)); // false五、总结与延伸
| JDK 动态代理 | CGLIB | |
|---|---|---|
| 实现原理 | 实现接口,生成 $Proxy 类 | 继承目标类,生成子类 |
| 字节码工具 | sun.misc.ProxyGenerator | ASM |
| 要求 | 目标必须有接口 | 目标类和方法不能是 final |
| 方法调用 | InvocationHandler.invoke() + 反射 | MethodInterceptor.intercept() + MethodProxy |
| Spring Boot 默认 | 否(Boot 2.x 改为 CGLIB 优先) | 是 |
性能上,JDK 8+ 之后两者差距已经很小,选择依据更多是:
- 有接口:两者都行,Boot 默认用 CGLIB
- 没接口:只能用 CGLIB
- 需要序列化:避免序列化代理对象
- Java 17 模块系统:优先用 Spring Boot 3.x 的 Objenesis 方案
