JDK 动态代理和 CGLIB 代理的核心区别是「代理谁」:JDK 动态代理要求目标类实现接口,运行时生成一个实现了相同接口的代理类;CGLIB 不要求接口,直接生成目标类的子类来覆写方法。目标类有接口、只需要按接口调用时用 JDK 动态代理;目标类没有接口、或者必须按类本身调用(比如要注入 Impl 类型)时用 CGLIB。Spring Boot 2.x 起默认强制使用 CGLIB。
JDK 动态代理和 CGLIB 代理最核心的区别是什么?
结论:JDK 动态代理是「接口实现」,CGLIB 是「子类继承」,这决定了它们各自的限制。
JDK 动态代理由 java.lang.reflect.Proxy 和 InvocationHandler 组成,代理类继承 Proxy、实现目标的全部接口,方法调用统一进到 invoke(proxy, method, args) 里,再靠 method.invoke(target, args) 反射转发。CGLIB 底层用 ASM 直接写字节码,通过 Enhancer 生成 目标类名$$EnhancerByCGLIB$$xxxx 这样的子类,方法调用走 MethodInterceptor.intercept,用 MethodProxy.invokeSuper 转发。
因为一个是实现接口、一个是继承父类,所以 JDK 代理只能通过接口引用使用,CGLIB 可以强转成目标类本身。
JDK 动态代理怎么写?有什么硬性限制?
结论:目标类必须实现接口,否则 Proxy.newProxyInstance 无从下手。
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class},
(p, m, args) -> {
System.out.println("before " + m.getName());
return m.invoke(target, args);
});
限制有三条:一是目标类必须至少实现一个接口,否则没法生成代理;二是只能增强接口中声明的方法,实现类里接口之外的方法代理不到;三是代理类命名有规律,JDK 8 是 com.sun.proxy.$Proxy0,JDK 9 模块化之后变成 jdk.proxy1.$Proxy0。
CGLIB 代理怎么写?哪些方法代理不了?
结论:CGLIB 能代理没有接口的类,但 final 类、final 方法、private 方法、static 方法一律代理不了。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("before " + method.getName());
return proxy.invokeSuper(obj, args);
});
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
原因是子类要覆写方法,final 禁止覆写、private 子类根本不可见、static 属于类不参与多态,所以这四类都被跳过——不是报错,而是「增强静默失效」,这是线上排查 @Transactional 不生效时最常见的原因之一。
另外,JDK 9 以后 CGLIB 原本依赖的 sun.misc.Unsafe.defineClass 已不可用,新版本改用 MethodHandles.Lookup.defineClass 等方案;在 JDK 17 上跑老版本 CGLIB(例如 cglib 3.2.4)会遇到模块封装导致的 InaccessibleObjectException,Spring 5.2 起把 CGLIB 重新打包进 spring-core(包名 org.springframework.cglib),升级 Spring 版本即可规避。
Spring AOP 默认用哪种?怎么改?
结论:Spring Framework 的 AOP 在目标实现了接口时默认用 JDK 动态代理,Spring Boot 2.0 起通过 spring.aop.proxy-target-class=true 默认改为 CGLIB。
配置项就是 spring.aop.proxy-target-class,设为 false 会退回「有接口用 JDK、没接口用 CGLIB」的老行为;用注解也可以在启动类上加 @EnableAspectJAutoProxy(proxyTargetClass = true)。需要注意,Spring AOP 只对容器里的 Bean 生效,且只增强 public 方法,类内部 this.method() 这种自调用绕过代理,事务和缓存注解都不会触发。
性能上该选哪个?
结论:JDK 8 以后两者调用性能差距已经很小,按「有没有接口」选即可,不要为性能强行换代理方式。
早期版本 CGLIB 靠 FastClass 索引调用、避开反射,方法调用确实快于 JDK 代理;创建代理对象反而是 JDK 更快,因为 CGLIB 要先生成并加载字节码。JDK 8 对反射做了膨胀优化后,两者单次调用差距通常在纳秒级。真正影响选择的是兼容性:目标类没有接口就必须 CGLIB,目标类是 final 类则两者都用不了,只能改为面向接口或抽一层接口出来。
一句话收束:有接口、按接口注入用 JDK 动态代理;无接口、要按类注入用 CGLIB,同时避开 final 类与 final/private/static 方法;Spring Boot 2.x 起默认就是 CGLIB,遇到代理失效先怀疑自调用、非 public 方法和 final 修饰。