☰
JDK动态代理与CGLIB的区别:从原理到选型,一篇讲透
2026/10/14 5:20:20 网站建设 项目流程

如果有人让我给Java面试题排个热度榜,“JDK动态代理与CGLIB实现的区别”大概率能进前五。这题难就难在它不是靠死记硬背能过关的——你背熟了“JDK基于接口、CGLIB基于继承”,转头看业务代码照样懵:为什么事务注解没生效?为什么AOP加在某个类上就报错?为什么代理对象一强转就炸?这些问题的答案,其实全都藏在两个动态代理机制的细节里。

这篇文章不打算复述教科书,而是从“一份代理代码到底是怎么跑起来的”这个角度,把JDK动态代理和CGLIB的生成方式、调用链、性能差异、选型策略和常见坑位逐个拆开,最后给你一张可以直接抄作业的决策清单。如果你在学AOP、读过一些框架源码却对Proxy和Enhancer一知半解,或者只是想让面试这道题答出层次,这篇文章都值得看完。

1. 先说清楚:为什么一个代理会有两种实现路子

1.1 从静态代理说起:代码是真的会腐烂的

假设现在要给订单服务加方法耗时统计。最朴素的写法是给每个方法前后掐表:

public interface OrderService { void createOrder(String orderNo); void cancelOrder(String orderNo); } public class OrderServiceImpl implements OrderService { @Override public void createOrder(String orderNo) { // 业务逻辑 System.out.println("create order: " + orderNo); } @Override public void cancelOrder(String orderNo) { System.out.println("cancel order: " + orderNo); } } public class OrderServiceProxy implements OrderService { private final OrderService target; public OrderServiceProxy(OrderService target) { this.target = target; } @Override public void createOrder(String orderNo) { long start = System.currentTimeMillis(); target.createOrder(orderNo); System.out.println("createOrder cost: " + (System.currentTimeMillis() - start) + "ms"); } @Override public void cancelOrder(String orderNo) { long start = System.currentTimeMillis(); target.cancelOrder(orderNo); System.out.println("cancelOrder cost: " + (System.currentTimeMillis() - start) + "ms"); } }

这段代码有个很明显的气味:代理类里的增强逻辑,完全是围绕“接口方法逐个手抄”来写的。如果OrderService再加一个方法,OrderServiceImpl和OrderServiceProxy都要跟着改;如果想让日志、权限、事务这类横切逻辑同时作用在十个业务类上,你就要写十个结构几乎一样的代理类,复制粘贴到手软。

这种模式就是静态代理。它在教学场景里非常优雅,在真实工程里却很快会变成维护噩梦。原因很简单:增强逻辑和业务代码被绑死在了同一个代理类里,复用性和扩展性都撑不住。需求一变,代码就要重构,这就是“代码腐烂”最常见的一种表现。

1.2 动态代理的思路:不再手写,而是运行时生成

动态代理做的事情,本质上是把上一节里的“人工抄写代理类”换成“机器自动生成代理类”。你可以把静态代理理解成每门课都手工誊写一份PPT,动态代理则是准备好一套模板和一份数据,机器按模板批量生成PPT。这样增强逻辑可以抽成公共模块,代理类不再需要人工维护。

Java生态里,实现动态代理的主流方案有两套:一套是JDK自带的动态代理,它只认接口;另一套是CGLIB,它靠生成目标类的子类来干活。这两套方案一个官配、一个第三方,机制不同、限制不同、适用的对象也不同。搞清楚它们各自是怎么生成代理类、怎么拦截方法、又各自有哪些边界条件,才能真正理解那些AOP框架为什么一会儿用这个、一会儿用那个。

2. JDK动态代理:它的世界只有接口

2.1 InvocationHandler和Proxy:最小可运行示例

JDK动态代理从代码层面看,只有两个核心角色。一个是InvocationHandler,它定义了“当代理对象的方法被调用时,应该做什么增强”;另一个是Proxy,它负责拿着接口和InvocationHandler去生成代理对象。

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class LogHandler implements InvocationHandler { private final Object target; public LogHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before method: " + method.getName()); Object result = method.invoke(target, args); System.out.println("after method: " + method.getName()); return result; } }

然后我们用Proxy.newProxyInstance创建代理对象:

public class JdkProxyDemo { public static void main(String[] args) { OrderService target = new OrderServiceImpl(); OrderService proxy = (OrderService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogHandler(target) ); proxy.createOrder("NO-2024-001"); } }

运行结果非常直白:

before method: createOrder create order: NO-2024-001 after method: createOrder

这个示例虽然短,但把JDK动态代理的三个关键参数暴露出来了:ClassLoader、接口数组、InvocationHandler。很多人在这一步会忽略一个事实:第二个参数是“接口列表”,而不是“目标类”。也就是说,你传进去的是接口,拿到的是实现了这些接口的代理对象。目标类在这里的作用,仅仅是被handler通过反射调用而已。

2.2 代理类的真面目:它在字节码层面做了什么

要理解“为什么JDK只认接口”,得看一眼JDK动态代理生成的代理类在字节码层面长什么样。Proxy.newProxyInstance内部会通过ProxyClassFactory生成一个新的类,这个类大致是这样的:

public final class $Proxy0 extends Proxy implements OrderService { private static Method m1; // hashCode private static Method m2; // equals private static Method m3; // toString private static Method m4; // createOrder public $Proxy0(InvocationHandler h) { super(h); } @Override public final void createOrder(String orderNo) { super.h.invoke(this, m4, new Object[]{orderNo}); } }

关键信息就在extends Proxy implements OrderService这一行。Java是单继承的,代理类已经占用了Proxy这个父类位置,剩下的扩展能力只能通过接口来提供。所以JDK动态代理必须要求你传入接口,否则它生成的类就没有可以“挂靠”的类型了。

调用链也很清晰:调用方持有的是接口类型引用,真正的方法调用落到$Proxy0上,$Proxy0里的方法只做一件事——把调用转发给构造器传入的InvocationHandler.invoke。invoke里通过Method.invoke(target, args)反射调用真实目标,再把返回值交还给代理对象。

这也解释了为什么JDK动态代理无法代理没有接口的类:没有接口,它就缺了一张“可实现的契约”,也就无法生成一个能被安全替换目标的代理类。这套设计放在JDK 1.3那个年代,其实是非常合理的取舍:接口本身就是一种抽象边界,代理对象和真实对象都实现同一组接口,调用方不需要知道背后到底是哪个实例。

2.3 JDK代理的边界条件

JDK动态代理的边界条件,总结起来是三句话。第一,目标类必须实现至少一个接口,否则直接IllegalArgumentException。第二,只能拦截接口里声明的方法;实现类自己额外加的public方法,从接口引用根本看不到,自然也就代理不到。第三,代理对象不能被强转成目标实现类,因为它和目标类之间没有任何继承关系,转型一定失败。

这些边界条件在写AOP时候经常以隐蔽的方式冒出来。最典型的就是:你给某个Service类配了切面,切面确实生效了,但你在业务代码里用OrderServiceImpl这个具体类型去接收从容器里拿出来的Bean,结果强转直接ClassCastException。原因就是容器返回的是$Proxy0,它跟OrderServiceImpl没有血缘关系。

3. CGLIB:不靠接口,靠“改字节码生儿子”

3.1 当目标类没有接口时怎么办

JDK动态代理的“接口依赖”在大量老项目里是个硬伤——早年间很多Service类压根没抽象接口,类上直接摞方法。这种情况要动态代理,就得靠CGLIB。

CGLIB的核心是三件套:Enhancer负责创建代理对象,MethodInterceptor负责定义增强逻辑,MethodProxy负责在拦截后调用真正的目标方法。看一段最简代码:

import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class OrderInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("before method: " + method.getName()); Object result = proxy.invokeSuper(obj, args); System.out.println("after method: " + method.getName()); return result; } } public class CglibProxyDemo { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OrderServiceImpl.class); enhancer.setCallback(new OrderInterceptor()); OrderServiceImpl proxy = (OrderServiceImpl) enhancer.create(); proxy.createOrder("NO-2024-002"); } }

注意,这里的OrderServiceImpl不需要实现任何接口,我们照样把代理对象强转成了OrderServiceImpl。这就是CGLIB和JDK动态代理最显眼的差异:CGLIB的代理对象是目标类的子类,所以可以直接按目标类型接收。

3.2 继承路线下的调用链

CGLIB能这么做,是因为它在运行时生成了一个目标类的子类。这个子类会重写目标类里所有非final、非private、非static的方法,在每个重写方法里插入对MethodInterceptor.intercept的调用。调用链大致是:

调用方 -> CGLIB生成的子类重写方法 ->MethodInterceptor.intercept->MethodProxy.invokeSuper-> 目标类原始方法

这里有个值得注意的性能细节:MethodProxy.invokeSuper并没有走纯反射,而是利用CGLIB额外生成的FastClass索引机制,在代理类和目标类之间建立方法索引,调用时直接定位并调用,省掉了一大截反射开销。这也是过去很长一段时间里,CGLIB在方法调用性能上被评价优于JDK动态代理的主要原因。

实际上CGLIB在生成子类时还会一并生成两个FastClass辅助类,一个用于描述目标类的快速调用索引,一个用于描述代理类的快速调用索引。所以CGLIB创建代理对象时的开销比JDK大,因为它干的活更多:生成两个类的字节码、加载、链接,再创建代理实例。

3.3 继承路线的天然限制

“继承”这条路从来不是没有代价的。CGLIB的天花板必须列清楚:

  • 目标类如果是final的,直接没法生成子类,CGLIB当场趴窝。
  • 目标方法如果是final的,子类无法重写,拦截不到。
  • private和static方法天然不可被重写,也不会进入拦截器。
  • 默认情况下,Enhancer.create()会调用目标类的无参构造器来创建代理对象实例,如果目标类没有无参构造器,会报错;有些框架会额外接上Objenesis这类库来绕过构造器。
  • CGLIB对包私有方法的重写能力和类所在的包名相关,跨包继承时包私有方法无法重写,从而无法被拦截。

所以在给“能不能CGLIB代理”下结论之前,先检查类和方法的关键字是最稳妥的做法。很多线上报错,最终追到根上就是某个final方法或者final类堵住了代理生成的路。

4. 六个维度硬碰硬:二者到底差在哪

4.1 创建成本与调用成本

讲动态代理区别的文章,几乎没有不提性能的。但性能这块鱼龙混杂,老说法很多已经过时。先把结论放在前面:不要盲目听信“JDK代理慢、CGLIB快”这种一刀切的说法,它至少在JDK 7之后就越来越不准了。

从创建成本看:JDK动态代理每拿到一组接口,生成一个$ProxyN类;CGLIB代理每拿到一个目标类,除了要生成代理子类,还要生成两个FastClass辅助类,期间涉及大量字节码操作和类加载。CGLIB的创建开销明显更重。如果你的应用启动时要为上千个Bean创建代理,这个差距是有感知的。

从调用成本看:早年JDK动态代理靠反射调用目标方法,CGLIB靠FastClass索引调用,两者差距明显;但JDK 7u之后对动态代理的反射调用做了持续的优化,调用性能差距已经缩小到绝大多数业务场景可以忽略。真正的差距要放在“每秒几十万次代理调用”这种高压场景里,借助压测工具才能量化出来。

结论是:性能不该成为你选型的首要理由。首要理由永远是被代理对象“长什么样”。

4.2 代理对象的身份:能强转成什么

这是二者在工程上差异最显著的一点。JDK动态代理生成的代理对象,只实现了传入的接口,和具体实现类没有任何继承关系,所以你不能把代理对象强转成实现类;CGLIB生成的代理对象是目标类的子类,强转成目标类或者目标类实现的接口都没问题。

这种身份差异会直接影响依赖注入、缓存key、类型判断这类逻辑。举个例子:一个方法接收OrderServiceImpl类型的参数,如果你传入的是JDK代理对象,编译期没问题,运行期直接ClassCastException;换成CGLIB代理对象就安然无恙。这也是很多老项目从JDK代理切换到强制CGLIB代理后,某些平时要强转的地方突然不再报错的原因。

4.3 方法可见性对代理能力的影响

两种代理在“谁能被拦截”这件事上的覆盖范围也完全不同。我用一张表格把规则列清楚:

方法类型JDK动态代理CGLIB
接口中的public方法可以拦截可以拦截
实现类独有的public方法拦截不到(接口不可见)可以拦截
protected方法接口不识别可以拦截
包私有方法接口不识别同包场景可以拦截,跨包不行
final方法无法拦截无法拦截
private方法无法拦截无法拦截
static方法无法拦截无法拦截

从覆盖范围来看,CGLIB确实更“宽”,这也是它在AOP框架里经常被选作兜底方案的原因。但这并不代表CGLIB全面优于JDK——JDK代理胜在规范、轻量、无需引入第三方依赖,而且它对接口的强制要求反过来促使你面向抽象编程。

4.4 一张对比表收尾

把核心维度汇总成一张表,面试和做选型时可以直接翻:

对比维度JDK动态代理CGLIB
实现机制运行时生成代理类,实现传入接口运行时生成目标类的子类
依赖JDK内置,无需第三方库需要引入CGLIB/ASM相关依赖
被代理对象要求必须有接口普通类即可,但不能是final类
代理对象类型只能按接口接收可按目标类子类接收
方法拦截范围仅接口中的public方法非final、非private、非static方法
创建开销相对较轻相对较重(生成子类+FastClass)
调用开销JDK 7u后已趋近CGLIBFastClass索引,整体稳定
典型应用面向接口编程的容器、AOP、切面无接口的遗留类、部分框架默认方案

5. 框架里怎么选:默认策略背后的逻辑

5.1 AOP框架的选择顺序

跟着框架源码走一遍,你会发现主流的AOP实现选代理时基本遵循同一套规则:先看目标类有没有接口,有接口就优先用JDK动态代理;没有接口就自动切到CGLIB;如果用户显式要求强制使用CGLIB,那么哪怕目标类有接口,也一律生成子类代理。

为什么接口优先?一方面是历史原因,JDK动态代理出现得早,几乎所有经典框架都围绕接口设计过默认行为;另一方面是设计哲学,面向接口编程本来就更利于解耦、替换和单元测试。但现实工程里“没有接口的类”实在太多了,这些类要加事务、加权限、加审计,没有CGLIB兜底几乎寸步难行。

另外值得留意的是,新一代框架的默认倾向正在发生变化。很多框架在新版本里把默认代理改成了“优先CGLIB”,理由是CGLIB不需要强制接口,而且对具体类型更友好,能减少一部分“拿到接口代理却拿不到实现类”的强转问题。理解这种趋势背后的原因,比背下某个框架的默认值更有价值。

5.2 强制切换时需要注意什么

如果你在配置里显式开启了强制子类代理,有几件事必须提前确认。第一,目标类不能是final类,否则启动阶段就会报错。第二,被切入的方法不能是final方法,否则切面静默失效——这种情况最坑,不报错、没日志,功能就是起不来。第三,构造器行为会变化,CGLIB默认会调用父类无参构造器,如果你的目标类构造器里有初始化逻辑,要留心执行顺序变化带来的副作用。

我在实际项目里踩过一个印象深刻的坑:某个配置类里放了一个final方法,切面声明要拦截它,结果所有调用看起来都正常,可日志里根本没有切面的输出。排查了半天,最后反编译代理类才发现,final方法压根没被重写,拦截器根本没挂上。这个教训我后来写进了项目组的开发规范:被AOP管理的方法,一辈子不要加final。

5.3 一张决策清单

选型的时候照着下面这几条判断就行:

  1. 目标类是否实现接口?如果纯粹没有接口,别纠结,直接CGLIB。
  2. 接口是否完整暴露了你需要拦截的方法?如果切面只关心接口声明的方法,JDK动态代理够用且更清爽。
  3. 调用方是否需要用具体实现类接收代理对象?需要的话,老老实实用CGLIB。
  4. 目标类或方法是否出现了final、private、static?出现任何一个,都要重新审视切点设计,否则代理可能直接失效。
  5. 有没有极端的高并发代理调用热点?有的话启动后做一轮压测,用数据说话,别凭感觉选。

6. 绕不开的高频坑位(含排查链路)

6.1 自调用陷阱:为什么走完代理还是没增强

这是动态代理相关问题上榜率最高的一道题。场景长这样:同一个业务类里,userList()调用了userDetail(),切面只切了userDetail(),结果你从容器里拿到代理对象,调用userList(),发现userDetail()的切面完全没生效。

原因不复杂:内部调用用的是this,而this是原始对象,不是代理对象。代理对象只在“外部调用”那一层被使用,当代码进入真实对象内部后,this指向的就是原始实例,后续的方法调用自然不会再经过代理。打个比方,代理对象像门口的保安,你进大门时保安查了你的证件,但进入大楼后内部房间之间的走动,保安并不在场。

这个问题的排查链路通常是:确认拿到的确实是代理对象、确认调用入口走的是代理引用、确认目标方法没有final/private、确认不是类内部自调用。前三个都排掉之后,基本就是自调用问题。解决思路有三种:把内部调用改成通过代理引用调用、拆开两个方法到不同Bean,或者改用AopContext.currentProxy()这类工具获取当前代理,但后者需要额外开启暴露代理对象的开关。

JDK动态代理和CGLIB在这个问题上表现一致,它不属于某个实现方案的缺陷,而是“代理模式”这个设计本身的公共约束。

6.2 equals/hashCode/toString被代理后的怪异行为

动态代理还有一个容易被忽视的暗坑:Object类里的equals、hashCode、toString这三个方法,在两种代理机制下都可能被当作普通方法处理,从而产生怪异行为。

JDK动态代理的代理类会把这三个方法也转发给InvocationHandler.invoke。如果你的handler里统一处理所有方法,并且习惯性地打印代理对象,就可能触发toString递归调用,最终栈溢出。更隐蔽的是equals:两个不同的代理对象指向同一个目标对象时,equals结果取决于你的handler怎么处理,跟目标对象的equals实现可能完全不一致。

CGLIB在这方面有自己的封装,但同样不保证和你的直觉一致。我的规避经验是:handler里显式区分Object的三个方法,优先交给proxy自己处理或者直接返回固定逻辑,不要让它们走到通用的反射分支里去。如果业务上有按对象内容判断相等性的需求,主动调用目标对象的equals逻辑,而不是依赖代理对象的默认行为。

6.3 并发创建代理类时的ClassDefFoundError

这个坑年代稍微久远一点,但依然会出现在老JDK版本或者极端并发启动的场景里。现象是:多个线程同时开始为同一组接口创建动态代理时,某个线程加载代理类加载到一半,另一个线程也跑来加载同名的类,触发类定义竞态,抛出ClassDefFoundError。

CGLIB在生成代理类时同样存在类似的竞态窗口,因为每个代理类的生成和定义不是原子的。解决经验有几条:把代理对象的创建集中到应用启动阶段,用单例持有,避免运行时大量并发创建同类代理;条件允许的话升级JDK版本,新版对类加载竞态的处理要完善得多;一旦遇到这类问题,优先排查ClassLoader和类定义锁,而不是怀疑业务代码。

6.4 调试技巧:看一眼代理对象到底是谁

在业务代码里排查动态代理问题时,最快的定位方式是“看身份”。我会习惯性地在关键位置输出一行日志:

System.out.println(proxy.getClass().getName()); System.out.println(proxy instanceof OrderService); System.out.println(proxy instanceof OrderServiceImpl);

如果输出的是com.sun.proxy.$Proxy0,说明走的是JDK动态代理;如果输出的是OrderServiceImpl$$EnhancerByCGLIB$$xxxx,说明走的是CGLIB。这两行类名信息,几乎能帮你在一分钟内判断出代理机制、代理类型以及强转是否安全。

这招排查速度极快。有一次同事拿了个“怎么切面突然不生效”的问题来,我让他先打印代理类名,结果发现他拿到的根本是原始对象而不是代理对象,后面的排查方向一下子就正了。多打印一行日志,往往比硬啃半天源码快得多。

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

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

立即咨询