JDK动态代理与CGLIB原理对比:从底层机制到Spring实战
2026/9/7 15:28:18 网站建设 项目流程

这道题我熟,基本每次聊到Spring动态代理相关话题,不管是一面还是二面,它都能被拎出来问一遍。说实话它不算难,但答法差别很大。很多人只背结论——“JDK动态代理要求接口,CGLIB不要求接口,CGLIB性能更好”,但真要说出个所以然来,比如JDK底层怎么生成代理类、CGLIB为什么能代理普通类、Spring到底怎么选,往往就含糊了。这篇文章我就把JDK动态代理和CGLIB从原理到实战彻底拆一遍,把源码层面和面试层面的关键点都理清楚,既能帮到准备面试的朋友,也能让你在实际项目里更清楚两种代理的适用边界。

1. 从静态代理到动态代理:两条技术路线是怎么来的

1.1 代理模式到底解决了什么问题

听名字你可能觉得“代理”很玄,其实它就是我们平时说的“中间人”。生活中的例子太多了:你租房不会一个个房东去谈,而是找中介;你打官司不直接上法庭,而是请律师。程序员世界里的代理也是同一个思路:不直接操作目标对象,而是在目标对象前面放一个代理对象,由代理对象负责拦截调用、加前置逻辑、加后置逻辑,然后再把请求转发给真正的目标对象。

用代码来说,最原始的静态代理长这样:

public interface UserService { void addUser(String name); } public class UserServiceImpl implements UserService { @Override public void addUser(String name) { System.out.println("新增用户:" + name); } } public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target = target; } @Override public void addUser(String name) { System.out.println("开启事务..."); target.addUser(name); System.out.println("提交事务..."); } }

问题也很明显:每给一个类加代理,就要手动写一个代理类,接口一多类就爆炸;而且事务、日志、权限这些横切逻辑会被复制粘贴到各个代理类里,维护成本高得吓人。静态代理的维护痛点,正是动态代理出现的原始动力。

1.2 JDK和CGLIB各自选了哪条路

动态代理的核心诉求是:我不提前写代理类,而是在程序运行的时候动态生成一个代理对象。实现这个目标有两条主流技术路线。

第一条路线来自JDK官方,叫JDK动态代理,它要求目标对象必须实现接口。它的思路可以参考一个“签合同”的过程:接口就是合同,代理对象和真实对象都照着合同办事,外部只看得到合同层面定义的那些方法。因为JDK在底层生成的代理类会实现同样的接口,所以代理类能和目标类平起平坐地对外提供服务。

第二条路线是CGLIB,它的思路非常像“生儿子继承家业”。CGLIB会在运行期生成目标类的一个子类,然后通过重写父类方法来完成方法拦截。因为是在继承关系上做文章,它天然不要求目标类实现任何接口,只要能继承就行。

用一张很直白的方式理解:

  • JDK动态代理:面向接口编程,是“按合同办事”。
  • CGLIB动态代理:面向继承编程,是“让儿子继承老子”。

这俩路线的差别,决定了它们的使用限制、底层机制和性能特征都不一样,接下来逐个拆开看。

2. JDK动态代理底层源码拆解:凭什么一定要接口

2.1 核心类与调用流程

JDK动态代理涉及的类很少,核心就是两个:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandlerProxy负责生成代理类,InvocationHandler负责定义当代理方法被调用时应该执行什么逻辑。一段最基础的代码是这样:

public class JdkProxyFactory { public static Object getProxy(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> { System.out.println("前置处理:方法执行前"); Object result = method.invoke(target, args); System.out.println("后置处理:方法执行后"); return result; } ); } }

Proxy.newProxyInstance接收三个参数:类加载器、接口数组、InvocationHandler。整个执行流程可以分为四步。

第一步,JDK 会根据传入的接口,在运行时动态生成一个代理类的字节码。这个代理类已经继承了Proxy,同时实现了你传入的所有接口。这是关键所在,因为 Java 是单继承,代理类既然已经继承了Proxy,就注定没办法再继承任何其他类了,所以它只能依靠接口来扩展能力。

第二步,通过传入的类加载器把这个字节码加载进 JVM,生成代理类对应的Class对象。

第三步,创建代理类的实例。此时构造方法会接收一个InvocationHandler的参数,代理对象内部把这个 handler 保存下来,作为后续所有方法调用的统一分发入口。

第四步,外部调用代理对象上的任何接口方法时,代理对象不会直接执行业务逻辑,而是把这次调用包装成一个Method对象,连同参数一起交给InvocationHandler.invoke()方法处理。你在 handler 里可以选择直接返回、继续转发给目标对象、或者在前前后后附加其他逻辑。

2.2 为什么JDK动态代理必须有接口

这是面试里最容易卡壳的问题,也是理解 JDK 动态代理的关键。前面提到了,JDK 生成的代理类继承了ProxyProxy是一个普通的 Java 类。Java 的类继承只能是单继承,一个类只能有一个直接的父类。既然代理类已经占用了Proxy这个父类名额,那它就不可能再去继承你的目标类。

所以想让外部代码像调用目标类一样调用代理类,唯一可行的路就是靠“接口”这个中间层:代理类和目标类都实现同一个接口,外部面对接口编程,不关心真正的实现是谁。

我们反推一下验证:如果目标类没有实现任何接口,JDK 动态代理就没法在“代理类”和“目标类”之间建立任何共同契约。代理类不能继承目标类,又缺乏接口作为契约,外部调用时连方法签名都对不上号,代理自然无法生效。

有个很常见的细节值得注意:JDK 动态代理只能代理接口中声明的方法,目标类自己额外写的方法是无法被代理拦截的。比如UserServiceImpl里除了实现接口方法外,自己又加了一个sayHello(),用 JDK 动态代理去代理它的接口,调用sayHello()并不会走拦截逻辑,因为代理类根本不认识这个方法。

2.3 代理内存中的实际形态

为了让你更直观地理解,我写个测试代码,把生成的代理类保存到磁盘上看看它长什么样。可以通过 JVM 参数-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true(高版本 JDK 用-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true)把字节码 dump 出来,再用反编译工具打开。

生成的代理类结构大致是:

public final class $Proxy0 extends Proxy implements UserService { private static Method m1; private static Method m3; public $Proxy0(InvocationHandler h) { super(h); } public final void addUser(String name) throws { try { super.h.invoke(this, m3, new Object[]{name}); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } }

看到没,$Proxy0有且只有一个父类Proxy,同时实现了UserService接口。接口里定义的方法都被重写成了“把所有调用转发给super.h.invoke()”的形式。这就是为什么“接口”对 JDK 动态代理来说是硬性条件——没有接口,就没有契约。

3. CGLIB动态代理底层原理:怎样“生儿子”继承家业

3.1 核心API与工作流程

CGLIB 的用法和 JDK 动态代理在代码形态上有点像,但核心角色不同。CGLIB 常用的类是net.sf.cglib.proxy.Enhancer,拦截器是net.sf.cglib.proxy.MethodInterceptor。基本代码长这样:

public class CglibProxyFactory { public static Object getProxy(Class<?> targetClass) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(targetClass); enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> { System.out.println("前置处理:方法执行前"); Object result = proxy.invokeSuper(obj, args); System.out.println("后置处理:方法执行后"); return result; }); return enhancer.create(); } }

CGLIB 构建代理的过程分这么几步:

第一步,Enhancer设置targetClass为父类,它意味着 CGLIB 要生成的代理类是这个目标类的子类。这一步奠定了“继承”这个基调。

第二步,setCallback设置拦截器。CGLIB 的回调类型有多种,MethodInterceptor是最常用的一种,它会在目标方法被调用时介入。

第三步,enhancer.create()动态生成目标类的子类字节码,并实例化这个子类对象。对象创建完成后,外部拿到的就是这样一个“子类代理对象”。

第四步,调用代理对象的任何非 final、非 private、非 static 方法时,调用都会被拦截到MethodInterceptor.intercept()方法里。注意这里的核心:proxy.invokeSuper(obj, args)会通过 CGLIB 生成的快速方法索引直接调用目标类的方法,而不是走 JDK 的反射Method.invoke(),这个机制在后面聊性能时会非常重要。

3.2 CGLIB生成的代理类是什么形态

CGLIB 生成的类也是运行期字节码增强的产物,通过对目标类做继承和重写实现。比如你有一个普通的OrderService类,里面有一个createOrder()方法,CGLIB 会生成类似这样的子类(简化描述):

public class OrderService$$EnhancerByCGLIB$$xxxx extends OrderService { private MethodInterceptor interceptor; public final void createOrder() { MethodInterceptor interceptor = this.interceptor; if (interceptor != null) { // 拦截器存在,走拦截逻辑 interceptor.intercept(this, createOrderMethod, args, createOrderProxy); } else { super.createOrder(); } } }

看到这里你应该就理解了“为什么 CGLIB 能代理没有接口的普通类”:因为它自己就是这个目标类的子类,天然拥有目标类的类型。在外面把代理对象强转成目标类类型毫无问题。

3.3 CGLIB的硬限制:final类与final方法

“不要求接口”不代表 CGLIB 无所不能,它有一个边界非常清楚:final 类不能被 CGLIB 代理,final 方法不会被 CGLIB 拦截

原因就是继承。被 final 修饰的类不能再有子类,CGLIB 就生成不了代理子类;被 final 修饰的方法不能被子类重写,CGLIB 即使生成了子类,也没有办法拦截这个方法。同样,private 方法、static 方法也不会被拦截,因为 private 方法对子类不可见,static 方法属于类级别、不属于实例方法重写的范畴。

顺带提一个很多老项目会踩到的坑:Spring 早期的版本中,如果遇到需要增强的类是 final 的,启动时就会直接报错。这也是为什么很多 Spring 教程里特别强调“被代理的类不要写 final 方法”,特别是你在用 CGLIB 动态代理做切面增强时,写final方法等于告诉 Spring:这个方法你管不了。

4. 两者核心对比:一张表说清全部差异

4.1 面试级对比清单

把前面几段的原理提炼成面试时可复述的对比表,这张表基本覆盖了这道题所有得分点:

对比维度JDK动态代理CGLIB动态代理
目标对象要求必须实现接口普通类即可,不要求接口
底层机制运行期生成实现接口的代理类运行期生成目标类的子类
核心APIProxy+InvocationHandlerEnhancer+MethodInterceptor
字节码操作JDK内置,无需第三方依赖依赖ASM字节码框架操作字节码
方法调用方式反射Method.invoke()FastClass索引直接调用
可拦截范围仅接口中声明的方法非final、非private、非static方法
final类/方法不影响(反正不依赖继承)不能代理final类,不能拦截final方法
依赖情况JDK原生支持,零额外依赖需引入cglib或spring-core内置的cglib重打包版
创建代理对象性能相对快首次生成子类较慢
方法调用性能JDK 8及以前相对慢,JDK 17+有优化通常更快,接近直接调用
Spring中的使用默认当目标类有接口时采用默认当目标类无接口或强制开启时采用

这张表里最容易被问爆的是“性能”那一行。需要强调,性能不能一概而论。CGLIB 在代理对象的创建速度上不算优势,因为它要动态生成子类字节码,首次创建代理对象比 JDK 动态代理更重;但一旦代理对象创建完成,CGLIB 的方法调用由于使用了 FastClass 机制,往往比 JDK 动态代理的反射调用效率更高。不过这个差距在 JDK 17 之后被大幅拉近了,新版 JDK 对Proxy的反射调用做了明显优化,在高版本环境里两者实际差异已经没那么大。

4.2 Spring AOP为什么默认能适配两种代理

Spring AOP 是动态代理应用最典型的场景。它本身不直接创建代理逻辑,而是把决策封装在了DefaultAopProxyFactory里。大致逻辑可以用伪代码描述:

if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { // 选择JDK动态代理 return new JdkDynamicAopProxy(config); } else { // 选择CGLIB动态代理 return new ObjenesisCglibAopProxy(config); }

也就是说,默认情况下 Spring 会优先看目标类是不是接口。Spring Boot 2.x 开始,官方把spring.aop.proxy-target-class默认值改成了true,也就是说 Boot 2.x 之后默认优先使用 CGLIB,哪怕目标类有接口。但这只在引入spring-boot-starter-aop时生效,而且这个策略可以主动配置调整。

在 Spring 面试追问环节,面试官经常会顺着问:“为什么 Spring Boot 2.x 默认改用 CGLIB 了?”主要原因有两个:一是 CGLIB 不要求接口,在很多接口缺失、历史遗留代码堆里更容易兜底;二是 Spring 官方认为“面向类代理”比“面向接口代理”对使用者更透明,使用者不必为了做 AOP 强行设计一层接口。不过这里也不能捧一踩一,JDK 动态代理零额外依赖、与接口设计结合得好,在强接口规范的项目里依然是干净的选择。

5. 实操环节:完整示例与四个必踩的坑

5.1 一次完整的双实现对比测试

空谈原理不过瘾,真刀真枪跑一遍更有说服力。我写一个最典型的例子:给一个用户服务加日志增强,分别用 JDK 动态代理和 CGLIB 实现,观察输出差异。

首先定义一个接口和实现类:

public interface UserService { void saveUser(String name); } public class UserServiceImpl implements UserService { @Override public void saveUser(String name) { System.out.println("UserServiceImpl.saveUser执行:" + name); } public void selfMethod() { System.out.println("UserServiceImpl.selfMethod,接口中没有这个方法"); } }

JDK 动态代理实现:

public class JdkLogProxy { public static UserService createProxy(UserService target) { return (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> { System.out.println("[JDK代理] 方法开始:" + method.getName()); Object result = method.invoke(target, args); System.out.println("[JDK代理] 方法结束:" + method.getName()); return result; } ); } }

CGLIB 动态代理实现:

public class CglibLogProxy { public static UserService createProxy(Class<UserServiceImpl> targetClass) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(targetClass); enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> { System.out.println("[CGLIB代理] 方法开始:" + method.getName()); Object result = proxy.invokeSuper(obj, args); System.out.println("[CGLIB代理] 方法结束:" + method.getName()); return result; }); return (UserService) enhancer.create(); } }

测试代码:

public class ProxyCompareTest { public static void main(String[] args) { UserServiceImpl target = new UserServiceImpl(); UserService jdkProxy = JdkLogProxy.createProxy(target); jdkProxy.saveUser("张三"); System.out.println("------------------------"); UserServiceImpl cglibProxy = (UserServiceImpl) CglibLogProxy.createProxy(UserServiceImpl.class); cglibProxy.saveUser("李四"); cglibProxy.selfMethod(); } }

结果非常有意思:

[JDK代理] 方法开始:saveUser UserServiceImpl.saveUser执行:张三 [JDK代理] 方法结束:saveUser ------------------------ [CGLIB代理] 方法开始:saveUser UserServiceImpl.saveUser执行:李四 [CGLIB代理] 方法结束:saveUser [CGLIB代理] 方法开始:selfMethod UserServiceImpl.selfMethod,接口中没有这个方法 [CGLIB代理] 方法结束:selfMethod

注意看最后一行。JDK 代理完全不可能拦截到selfMethod(),因为接口里没有声明它;而 CGLIB 因为直接生成子类,所有非 final 的 public 方法都会被重写,所以连这种接口之外的 public 方法也拦截到了。这一个实际输出比背十遍理论都容易记。

5.2 坑一:自调用问题导致代理失效

这个坑在 Spring 事务里非常出名。假设你在一个类里写了一个方法,它调用了同类中的另一个@Transactional方法,事务经常“不生效”。原因就是代理机制:Spring 容器中注入的对象是代理对象,但当你从类内部用this.xxx()调用时,这个引用指向的是目标对象本身,不是代理对象。方法调用直接走目标对象的方法,根本不经过代理,切面逻辑自然也就不执行。

两种解法比较常见:一是把内部调用改成通过代理对象自调用,比如从 Spring 容器里重新拿一次代理对象;二是在业务设计上尽量避免同类内部互相调用,把被调用的方法放到另一个 Bean 里,通过依赖注入去调用。理解了 JDK 动态代理和 CGLIB 的本质,你就能明白这事跟用哪种代理毫无关系,问题出在“自调用绕过了代理层”。

5.3 坑二:final方法被静默跳过

我见过特别多的人用 CGLIB 做 AOP 时,方法上加了final,结果切面悄悄不生效,排查半天都找不到原因。原因在前面已经说了,final 方法无法被子类重写,CGLIB 只能在生成的子类里跳过这个方法。

更阴间的是它不报错,默认当无事发生。Spring 日志里往往屁都不打印一条,你只能自己去翻方法是不是被 final 修饰了。经验之谈:设计被 AOP 增强的 Bean 时,压根别用 final;如果你在维护老代码,先检查方法修饰符。

5.4 坑三:CGLIB对private和static方法同样无能为力

private方法是子类不可见的,CGLIB 没法重写它;static方法属于类级别,不参与实例方法重写。这两类方法对代理来说都是“透明”的,这意味着你写在 private 方法里的逻辑不会经过代理层。这其实是好事还是坏事要分场景看,但面试时提到这一点能展示出你不仅知道 CGLIB 能做什么,还很清楚它的边界。

5.5 坑四:代理对象强转失败与类加载器问题

JDK 动态代理生成的代理对象类型是$Proxy0,它实现了业务接口,但它跟目标类之间没有继承关系。如果你把它强制转换成目标类类型,比如(UserServiceImpl) jdkProxy,就会直接抛ClassCastException。这种错误经常出现在老代码里:接口定义得好好的,但代码里习惯性用实现类接收对象。

还有类加载器问题。Proxy.newProxyInstance的第一个参数必须能正确加载接口。实际项目中如果目标对象来自不同的类加载器,接口和实现类不是同一个加载器加载的,可能会出现IllegalArgumentException提示接口无法被加载。遇到这类问题先确认类加载器上下文,再看看是不是自定义类加载器场景下接口可见性出了问题。

6. 面试官想听到的层次化回答:从背答案到讲原理

6.1 六步答题思路

如果这道题是面试现场问到的,把前面内容压缩成六句话的层次结构会非常加分:

第一层,直接定义。JDK 动态代理基于接口,CGLIB 基于继承,这是最根本的区别。

第二层,讲底层机制。JDK 在运行期生成实现指定接口的代理类,调用被转发给InvocationHandler;CGLIB 在运行期生成目标类的子类,通过重写方法实现拦截。

第三层,讲限制。JDK 必须要求目标类实现接口,只能代理接口方法;CGLIB 不要求接口,但无法代理 final 类、无法拦截 final/private/static 方法。

第四层,讲性能特征。CGLIB 底层用 FastClass 索引机制调用目标方法,调用效率通常更高;JDK 是反射调用,但在高版本 JDK 中性能差距在缩小。创建代理对象的耗时上,CGLIB 首次生成字节码更重。

第五层,讲框架落地。Spring AOP 的默认策略:目标类有接口时用 JDK 动态代理;Boot 2.x 默认开启 CGLIB 类代理。同时要能解释proxyTargetClass参数的作用。

第六层,讲选型经验。如果系统面向接口设计、强调规范,JDK 动态代理足够;如果目标类无接口、或需要对接口之外的方法也做增强,用 CGLIB。一句话收尾:能用接口就用 JDK,JDK 手里没接口才轮到 CGLIB 补位

6.2 高频追问与参考回答

追问一:“JDK 动态代理生成的代理类能保存下来看吗?”

可以,设置系统属性保存生成的字节码文件到磁盘,反编译后能看到代理类继承Proxy、实现业务接口、所有方法都转发给InvocationHandler。这个答案能直观验证你前面说的原理,也说明你确实读过源码层面的东西。

追问二:“CGLIB 一定比 JDK 动态代理快吗?”

不一定,而且不能一刀切。方法调用层面 CGLIB 早期确实有优势,但 JDK 新版本的反射机制已经优化得很强;对象创建层面 CGLIB 因为要生成子类字节码,首次创建更慢。真要给结论,就分两个维度讲,不要笼统说谁快谁慢。

追问三:“如果接口方法上标记了@Transactional,用 JDK 动态代理执行时会怎样?”

会走事务增强逻辑,因为@Transactional是声明在接口方法上的,JDK 代理类实现了接口的方法,Spring 在解析切点的时候可以通过Method对象上的注解信息匹配到切点,进而套上事务拦截器。这也是为什么很多老项目在接口上加事务注解也能生效的原因。

追问四:“能同时用 JDK 动态代理和 CGLIB 代理同一个对象吗?”

严格意义上是不能在一次代理里混用两种机制的,但在 Spring 中可以通过嵌套代理组合,比如一个对象先被 JDK 代理增强,代理对象再被 CGLIB 代理增强,形成多层代理结构。不过嵌套代理调试起来非常费劲,不建议随便这么干。

追问五:“动态代理和 AOP 是什么关系?”

动态代理是实现 AOP 最常用的技术手段。AOP 关注的是切面、通知、切点这些抽象概念,而动态代理负责把“方法调用被拦截、切面逻辑被执行”这件事落到字节码和对象层面。通过这个问题,面试官能看出你是背概念还是真的理解它们的分工。

写在最后的一点实际体会

我自己在项目里用这两种代理总结出的经验是:新项目如果从零开始做,凡是确定会被增强的模块,我几乎都会先定义接口,优先用 JDK 动态代理。倒不是因为 CGLIB 不好,而是接口本身就是很好的设计约束,它能倒逼你把类的对外能力和内部实现拆干净。只有遇到改造老系统、目标类没接口又不好大动代码的情况,我才会明确选用 CGLIB 代理来兜底。还有个小建议,如果你在 Spring 工程里排查代理不生效的问题,第一件事别急着怀疑代理机制,先用AopUtils.isAopProxy()确认你拿到的对象到底是不是代理对象,再检查方法有没有被 final 修饰、是不是发生了自调用。顺着这个顺序排查,八九成的问题都能快速定位。动态代理的原理说透了其实并不复杂,内核就两句话:JDK 靠接口立契约,CGLIB 靠继承生孩子。把这两句话记牢,再从字节码层面去理解它们的实现差异,这道面试题就能变成你展示技术深度的加分项。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询