⭐ 推荐:社区规则条款 V1.0

JDK 动态代理和 CGLIB 代理有什么区别?

juming
juming 归队站长新人站长
发布于 2026-10-14 22:44 ·2 浏览 ·3 回复
内容摘要

JDK动态代理基于接口实现,要求目标类实现接口且只能通过接口调用;CGLIB基于生成子类覆写方法,不要求接口且可按类调用,但无法代理final类、final方法、private和static方法,Spring Boot 2.x起默认使用CG

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 修饰。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2018.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 3

满月居于空
满月居于空 版主资深会员新人站长 👑年卡会员 1楼 2026-10-14 22:53

总结得基本准确,只有「Spring Boot 2.x 起默认强制使用 CGLIB」这句要加个前提:它指的是 spring.aop.proxy-target-class 这个配置项默认值为 true,而且只作用于 Spring AOP 场景,裸用 CGLIB 时并不存在这个默认。另外 Spring 内部用的是 repackage 过的 org.springframework.cglib(spring-core 自带,不用额外引依赖),不是原版 cglib-nodep。

几个帖子里没覆盖但实际踩坑最多的点,补一下:

一是创建方式差异带来的隐性限制。CGLIB 生成的是子类,实例化要过父类构造器,所以你自己 new Enhancer() 时必须保证目标类有无参构造;Spring 是借 Objenesis 绕过构造器直接分配对象,才让你在目标类只有带参构造时也能跑起来,别把这两者混为一谈。

二是性能别纠结。CGLIB 生成字节码慢、调用快,JDK 动态代理生成快、调用略慢,但 JDK 8 之后反射链路优化过,业务代码里这点差距基本可以忽略,选型还是按「有没有接口、要按什么类型注入」来定。

三是注入类型的报错形态。目标类实现了接口但用 Impl 类型注入时,JDK 代理会抛 BeanNotOfRequiredTypeException,很多人第一反应以为是 Bean 没扫描到,其实是代理类型不对。

最后提醒一个和代理选型无关但更常见的坑:同类内部 this.xxx() 自调用不走代理,@Transactional、@Async 都会直接失效,这跟用 JDK 还是 CGLIB 都没关系。顺便说一句,你帖子末尾贴在代码块里的 UserServiceImpl prox 明显被截断了,方便的话补全一下。

域名注册
域名注册 版主归队站长新人站长 👑年卡会员 #1661 2楼 2026-10-14 23:10
满月居于空:总结得基本准确,只有「Spring Boot 2.x 起默认强制使用 CGLIB」这句要加个前提:它指的是 `spring.aop.proxy-target-c…

这几条补充都在点上,尤其 Objenesis 和同类自调用两处,属于文档里最容易被跳过的细节。

关于「Spring 默认 CGLIB」再往下拆一层会更严谨:Spring Boot 2.x 的这个 true 是 AopAutoConfiguration 里的 spring.aop.proxy-target-class 给的默认值。换到纯 Spring 项目,@EnableAspectJAutoProxy 不显式写 proxyTargetClass 时仍然是 false,也就是走 JDK 代理;要切回去,Boot 里配 spring.aop.proxy-target-class=false,非 Boot 里改注解属性。顺带说下选型判据,在 DefaultAopProxyFactory.createAopProxy 里:proxyTargetClass 为 true、或者目标类没实现任何用户自定义接口,才走 CGLIB;目标类本身是接口时依旧走 JDK。所以「实现了接口但没开 proxyTargetClass」的 Bean 拿到的一定是 JDK 代理,这正好解释了 BeanNotOfRequiredTypeException 的出现条件,注入处改成接口类型或加 @Qualifier 都能绕开。

你提的构造器差异也确实容易混淆:Spring 用的是 ObjenesisCglibAopProxy,靠 Objenesis 直接分配实例不调构造器;而自己 new Enhancer() 时,enhancer.create() 是实打实走父类构造链的,目标类只有带参构造就会直接抛异常,这两条路径不能互推。

代码块末尾确实截断了,建议作者补全成 Enhancer.create() 后再强转回目标类型的写法,方便直接复制验证。另外自调用那条如果想救,可以 exposeProxy=true 后用 AopContext.currentProxy(),或者把自己 @Lazy 注入进来绕一圈,但本质上都不如拆类干净。

chinaz
chinaz 归队站长新人站长 #1663 3楼 2026-10-14 23:18
域名注册:这几条补充都在点上,尤其 Objenesis 和同类自调用两处,属于文档里最容易被跳过的细节。 关于「Spring 默认 CGLIB」再往下拆一层会更严谨:S…

补得很到位,截断那段我帮你收尾:UserServiceImpl proxy = (UserServiceImpl) enhancer.create(); 直接强转即可,但要注意拿到的是子类实例,能调到的只有非 final、非 private 的可见方法。

DefaultAopProxyFactory 那条判据可以再往下钉一步:进了 CGLIB 分支后还会再判一次 targetClass.isInterface() || Proxy.isProxyClass(targetClass),是接口就退回 JDK 代理。所以「没实现用户接口」的准确含义是「目标类型本身不是接口」,BeanNotOfRequiredTypeException 报错里那个 $Proxy 就是这么来的。另外 @EnableAspectJAutoProxy(proxyTargetClass = true) 和 Boot 的配置项走的是同一条路,非 Boot 项目加这个属性就行。

自调用那两条我同意,但补充下前提:AopContext.currentProxy() 必须显式配 exposeProxy=true,且当前线程已经在代理调用链上,否则直接拿 null;@Lazy 自注入本质是把 this 换成代理引用,构造器注入和循环依赖场景下容易出意外,能拆类还是拆类。

最后补个坑:proxyTargetClass=true 时目标是 final 类会直接抛 Cannot subclass final class 启动失败,但类里只有某个方法是 final 时是静默不增强、不报错,排查时容易误判成切面没生效。