JDK 动态代理 vs CGLIB
面试官翻到候选人小赵的简历,上面写着"精通 Spring 框架原理",开口问道:
"Spring 默认用 CGLIB 做代理,你知道为什么吗?"
小赵说:"因为 CGLIB 性能更好?"面试官追问:"好多少?怎么测的?"
小赵语塞。面试官继续:"那 JDK 动态代理的 newProxyInstance 方法,接收哪三个参数?"
小赵支支吾吾答不上来。
【面试官心理】 这道题我用来试探候选人对 Spring 源码的钻研程度。能说出 CGLIB 和 JDK 代理差异的是基本操作,能讲清楚 proxyTargetClass 演进历史的是 P6,能分析出 Spring Boot 为什么要改默认行为的,基本是 P7 了。
一、两种代理的底层原理 🔴
1.1 JDK 动态代理:接口即契约
JDK 动态代理的核心思想是:我不继承你,我只和你签契约(接口)。
当你调用 Proxy.newProxyInstance() 时,JVM 在内存中动态生成一个 $Proxy0 类(名字按顺序递增),这个类:
- 继承自
java.lang.reflect.Proxy - 实现你传入的所有接口
- 在每个接口方法中,调用
InvocationHandler.invoke()
生成的代理类大约长这样(简化版):
newProxyInstance 的三个参数:
ClassLoader:加载生成的$Proxy0类,大多数场景用目标类的 ClassLoader 就够Interface[]:代理类要实现的接口数组(必须是接口,不能是类)InvocationHandler:方法调用的处理器,代理对象的每次方法调用都会触发这个 Handler
1.2 CGLIB 代理:继承即能力
CGLIB 的核心思想是:我不是在接口上做文章,我直接继承你,用字节码生成一个你的子类。
Enhancer 是 CGLIB 的核心类,它的工作流程:
- 设置父类(目标类)
- 设置回调(
MethodInterceptor) - 调用
enhancer.create()生成子类实例
生成的代理类大约长这样(简化原理):
1.3 InvocationHandler vs MethodInterceptor
很多候选人搞混了 method.invoke() 和 methodProxy.invoke() 的区别:
method.invoke(target, args)是反射调用,要走 JVM 的方法访问检查methodProxy.invoke(target, args)是直接调用,绕过反射,直接跳转methodProxy.invokeSuper(obj, args)是调用父类的实现(即目标方法本身)
在 CGLIB 拦截器中,如果要调用原方法,必须用 methodProxy.invokeSuper(),而不是 method.invoke(),否则会引发死循环。
二、Spring 的代理演进 🟡
2.1 版本演进历史
Spring 的代理策略经历了三个阶段,这个演进历史是 P6/P7 的高频追问:
2.2 Objenesis 机制:绕过构造器
这是 Spring 4.x 引入的重要改进,也是 Spring Boot 2.x 默认 CGLIB 的技术基础之一。
传统 CGLIB 创建代理对象时,需要调用目标类的默认构造器:
问题是:如果目标类的构造器有参数,或者构造器里有重要逻辑(副作用),调用默认构造器就会出问题。
Spring 4.x 引入 Objenesis:
这意味着即使目标类没有默认构造器,CGLIB 代理也能创建成功,且不会触发构造器中的任何逻辑。
2.3 为什么 Spring Boot 默认用 CGLIB?
Spring Boot 2.x 在全局配置中设置了 spring.aop.proxy-target-class = true,强制使用 CGLIB。原因有三:
- 无接口依赖:很多业务类不实现任何接口,但同样需要 AOP 能力(事务、日志等)
- 方法覆盖:CGLIB 可以代理所有非 final、非 static 方法,而 JDK 代理只能代理接口方法
- Objenesis 成熟:Spring 4.x+ 内置 Objenesis,解决了 CGLIB 对默认构造器的依赖
matchIfMissing = true 意味着如果没配置 spring.aop.proxy-target-class,默认就强制 CGLIB。这是 Spring Boot 的设计哲学:安全优于灵活。CGLIB 功能覆盖范围更广,默认用 CGLIB 出错的概率更低。
三、核心对比表 🟡
四、❌ 错误示范
候选人原话一:
"CGLIB 比 JDK 动态代理快,因为 CGLIB 是编译期的。"
问题诊断:
- CGLIB 和 JDK 代理都是运行期字节码生成,不是编译期
- 两者性能差距主要在是否有 FastClass 优化,而非生成时机
候选人原话二:
"JDK 动态代理只能代理接口,因为 Proxy 继承了 java.lang.Object,而 Object 是一切类的父类。"
问题诊断:
- 这句话后半句是对的,但没解释清楚为什么"只能代理接口"
- 正确原因:Proxy 类需要实现你传入的接口数组,所以接口是必需的
- 实际上
$Proxy0继承 Proxy 并实现接口,同时也间接是 Object 的子类
候选人原话三:
"Spring 用 CGLIB 是因为 CGLIB 性能更好,Spring Boot 强制用 CGLIB 是因为性能考虑。"
问题诊断:
- 性能只是原因之一,不是全部
- 更深层的原因是功能覆盖范围:Spring Boot 时代大量业务类不实现接口,但同样需要事务、AOP 等能力
- 混淆了"功能需求"和"性能优化"的优先级
【面试官心理】 能答出"JDK 动态代理要求接口"这个基本点的占 60%,能解释清楚 CGLIB 的 FastClass 机制的是 40%,能讲出 Spring Boot 为什么改默认策略的只有 15%。这道题我一般从"为什么默认用 CGLIB"切入,往版本演进、性能对比、Spring Boot 设计哲学三个方向追问。
五、实战追问链
第一层:Spring 用的是哪种代理?答:"CGLIB 或 JDK 动态代理。"——基本概念
第二层:什么情况下用 JDK,什么情况下用 CGLIB?答:"看目标类有没有实现接口。"——理解判断逻辑
第三层:@EnableAspectJAutoProxy(proxyTargetClass = false) 会发生什么?答:"强制用 JDK 动态代理。"——参数理解
第四层:为什么 Spring Boot 要强制 CGLIB?它考虑了哪些因素?
答不出来 → P6 候选人;能说出 Objenesis 和无接口依赖 → P7 候选人
六、生产避坑
坑一:final 类无法代理
CGLIB 通过继承实现代理,所以被 final 修饰的类无法被 CGLIB 代理。AOP 切面对这类 Bean 不会生效,且 Spring 不会报错,只会在运行时发现"事务没生效"。
排查方法:开启 debug 日志 logging.level.org.springframework.aop=DEBUG,Spring 会打印哪些 Bean 被跳过了代理创建。
坑二:self-invocation 在两种代理下都失效
不管是 JDK 代理还是 CGLIB 代理,内部方法调用走的都是 this 引用,不是代理对象。这个问题与选哪种代理无关,是 Spring AOP 的根本限制。
坑三:性能测试的坑
很多人在本地测 JDK vs CGLIB 性能,结果发现差不多甚至 JDK 更快。这是因为测试没有预热——JIT 编译器对反射调用有内联优化,多次调用后 method.invoke() 的开销会显著下降。FastClass 的优势在首次调用和大量方法签名不同的场景下更明显。