JDK动态代理与CGLIB原理对比:从底层机制到Spring实战
这道题我熟基本每次聊到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.Proxy和java.lang.reflect.InvocationHandler。Proxy负责生成代理类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.saveGeneratedFilestrue高版本 JDK 用-Djdk.proxy.ProxyGenerator.saveGeneratedFilestrue把字节码 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动态代理目标对象要求必须实现接口普通类即可不要求接口底层机制运行期生成实现接口的代理类运行期生成目标类的子类核心APIProxyInvocationHandlerEnhancerMethodInterceptor字节码操作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(ClassUserServiceImpl 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 在运行期生成实现指定接口的代理类调用被转发给InvocationHandlerCGLIB 在运行期生成目标类的子类通过重写方法实现拦截。第三层讲限制。JDK 必须要求目标类实现接口只能代理接口方法CGLIB 不要求接口但无法代理 final 类、无法拦截 final/private/static 方法。第四层讲性能特征。CGLIB 底层用 FastClass 索引机制调用目标方法调用效率通常更高JDK 是反射调用但在高版本 JDK 中性能差距在缩小。创建代理对象的耗时上CGLIB 首次生成字节码更重。第五层讲框架落地。Spring AOP 的默认策略目标类有接口时用 JDK 动态代理Boot 2.x 默认开启 CGLIB 类代理。同时要能解释proxyTargetClass参数的作用。第六层讲选型经验。如果系统面向接口设计、强调规范JDK 动态代理足够如果目标类无接口、或需要对接口之外的方法也做增强用 CGLIB。一句话收尾能用接口就用 JDKJDK 手里没接口才轮到 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 靠继承生孩子。把这两句话记牢再从字节码层面去理解它们的实现差异这道面试题就能变成你展示技术深度的加分项。

相关新闻

最新新闻

日新闻

周新闻

月新闻