★ JDK 动态代理与 CGLIB 区别与选择
结论先行:两者都用于在运行期生成代理对象以拦截方法调用。JDK 动态代理基于接口:目标必须实现接口,代理类实现同一接口,通过 InvocationHandler.invoke 分发;CGLIB 基于继承:用字节码生成目标类的子类,通过 MethodInterceptor 拦截,不要求接口,但不能代理 final 类、不能覆写 final/static 方法。Spring 的选择逻辑:目标类有接口时默认可用 JDK 代理,无接口必须 CGLIB;Spring Boot 2.x 起默认 proxy-target-class = true,即一律 CGLIB(除非显式关闭)。
一、原理对比表
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 机制 | 生成实现目标接口的代理类 | 字节码生成目标类的子类 |
| 前提 | 目标必须有接口 | 目标类可继承(非 final)、方法非 final |
| 拦截器 | InvocationHandler.invoke | MethodInterceptor.intercept |
| 可代理范围 | 仅接口声明的方法 | 类中可覆写的 public/protected 方法 |
| 额外依赖 | JDK 自带 | cglib/objenesis(Spring 已内嵌 repackaged 版本) |
| 特点 | 类型要强转成接口 | 无需接口;Boot 2+ 默认使用 |
二、Spring 如何选择
- DefaultAopProxyFactory:proxyTargetClass = true → CGLIB;否则目标有接口 → JDK 代理,无接口 → CGLIB;
- Spring Boot 2.0+ 默认 true(spring.aop.proxy-target-class=true),因此现代项目绝大多数 Bean 都是 CGLIB 代理;
- Spring Framework 6 起仍在用 CGLIB(内部重打包),并没有移除;JDK17+ 对原生动态代理做了优化,两者性能差异已很小。
// JDK 动态代理(手写示意)
PayService proxy = (PayService) Proxy.newProxyInstance(
PayService.class.getClassLoader(),
new Class[]{PayService.class},
(p, method, args) -> { System.out.println("before"); return method.invoke(target, args); });
// CGLIB(手写示意)
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderService.class); // 子类化,无需接口
enhancer.setCallback((MethodInterceptor) (obj, m, args, mp)
-> { System.out.println("before"); return mp.invoke(target, args); });
OrderService proxy = (OrderService) enhancer.create();
常见追问 / 记忆点
- 追问 1:哪些情况 CGLIB 也救不了?答:final 类、final 方法、static 方法、private 方法——这也是 @Transactional 写在 final/private 方法上失效的原因之一。
- 追问 2:为什么 Boot 默认 CGLIB?答:很多类没接口(如直接 @Service 无接口实现),统一 CGLIB 免去"有接口没接口行为不一致"的坑。
- 追问 3:性能谁更好?答:JDK 代理每调用走反射 invoke(JDK17+ 已优化);CGLIB 子类调用接近原生。工程上按默认即可,不必过度纠结。
- 记忆点:"JDK 认接口,CGLIB 认子类;final/static/private 两边都不管;Boot 默认 CGLIB"。