如果有人问我 Spring 框架里最容易被低估的代理组件是谁,我会毫不犹豫地报出这个名字:MethodProxy.java。它不像 BeanFactory、ApplicationContext 那样天天挂在嘴边,也不像 JDK 动态代理的 InvocationHandler 那样被各种博客反复讲解,但每次 Spring AOP 通过 CGLIB 增强目标类时,真正在最后一公里把方法调用送进原始目标方法的就是它。这篇文章不讨论怎么配事务、怎么切切面,我要把它从 CGLIB 到 Spring 的整合过程、源码里 invoke 和 invokeSuper 两条路各自做了什么、以及为什么用错会栈溢出,完整拆一遍。适合正在啃 Spring 源码的、自己写 MethodInterceptor 的,或者被“代理方法失效”折磨过的同学。
1. MethodProxy 在 Spring 代理体系中的真实位置
1.1 它到底是 Spring 的还是 CGLIB 的?很多人一开始就理解偏了
MethodProxy.java 这名字看起来像 Spring 源码,但它的真正老家是 CGLIB 库。Spring 从 4.0 开始,为了屏蔽外部依赖、避免 CGLIB 版本冲突,直接把这些类拆进 spring-core 里,变成了org.springframework.cglib.proxy.MethodProxy。所以你在 IDEA 里按Ctrl+N搜 MethodProxy,默认会看到两条结果:一条来自net.sf.cglib.proxy,一条来自org.springframework.cglib.proxy。Spring AOP 实际使用的是后者,虽然代码几乎一样,但千万别把两个包混在一起写。
为什么 Spring 宁愿把 CGLIB 重包一遍也不自己实现?因为 CGLIB 做子类代理太成熟了。JDK 动态代理只能基于接口,遇到没有接口的类就无能为力;而 Spring 从很早就依赖 CGLIB 来增强那些没有接口的 Bean。把它内置后,用户不需要额外引入 cglib 依赖,Spring Boot 里很多自动配置也不会因为版本冲突炸掉。MethodProxy 就是 CGLIB 这套代理模型里连接“拦截器”和“原始方法”的桥梁,在整个代理组件中扮演的是执行者的角色。
1.2 AOP 代理链:从 ProxyFactory 到 MethodProxy 执行
我按一次实际代理调用来梳理。假设你有一个UserService,配置了 AspectJ 切面,Spring 启动时发现要增强它,于是走ProxyFactory创建代理。如果目标类没有接口,或者配置了proxyTargetClass=true,Spring 会选用CglibAopProxy。这个类内部用 Enhancer 生成一个目标类的子类,同时给子类设置一组 Callback 回调。其中最关键的回调是DynamicAdvisedInterceptor,它实现了MethodInterceptor接口,代理对象上任何方法被调用时,都会先走到这个拦截器的intercept方法里。
intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy)是四参数版本。这里的methodProxy就是 CGLIB 在生成代理时为我们创建好的 MethodProxy 对象。Spring 把MethodInvocation责任链构建好之后,最终会调用methodProxy.invokeSuper(proxy, args)来执行目标方法。也就是说,MethodProxy 不在代理链的入口,而是在出口:前面的 Advice 都执行完了,它负责完成“最后一下”的原始方法调用。
1.3 与 JDK 动态代理的分工
JDK 动态代理用的是InvocationHandler.invoke,里面拿到的Method是接口方法,调用时通过反射执行,或者用Method.invoke(target, args)。CGLIB 代理用的是拦截器加 MethodProxy,可以通过 FastClass 索引直接在生成的子类方法上跳转,也可以调用父类实现。两者的分工简单说:JDK 负责面向接口的轻量代理,CGLIB 负责面向类继承的代理,而 MethodProxy 的存在让 CGLIB 在调用阶段不必每次都走反射。
| 维度 | JDK Proxy | CGLIB + MethodProxy |
|---|---|---|
| 能否代理无接口类 | 不能 | 可以 |
| 调用原始方法 | Method.invoke 反射 | FastClass 索引调用 |
| 拦截器入口 | InvocationHandler.invoke | MethodInterceptor.intercept |
| Spring 默认场景 | 接口存在时使用 | proxyTargetClass=true 或无接口 |
| final 方法 | 无法代理 | 无法代理 |
这个位置理解清楚后,才能真正看懂后面的源码。很多人一上来就扎进线程池、事务传播属性里,反而忽略了最基础的代理入口,等到排查问题时才发现连 MethodProxy 是干嘛的都没搞明白。
2. 源码拆解:构造、invoke 与 invokeSuper 的真实执行路径
2.1 MethodProxy 的构造与 FastClassInfo
我们来看MethodProxy内部。它有几个关键字段:签名信息signature,代理类名c1、原类名c2,以及方法名等。核心的是内部类FastClassInfo,里面持有两个 FastClass 对象:f1和f2,还有两个整型索引i1和i2。
创建过程大概是:生成代理类时,CGLIB 会同时为代理类和原始类各生成一个 FastClass 辅助类,然后把相关信息塞给 MethodProxy。FastClass 不是普通类,它会为每个方法分配一个索引,invoke 时直接按索引进入对应代码位置,省掉了反射里的方法查找和方法参数解析。
这里有个容易忽略的点:i1和i2是两个不同索引。i1对应代理类中该签名方法在 FastClass 里的索引,i2对应原始父类中该签名方法在 FastClass 里的索引。为什么要两个索引?因为在增强场景下,代理类重写了方法,而原始类的同名方法又是另一套代码,必须分别记录。
2.2 invoke(Object obj, Object[] args) 源码路径
MethodProxy 的invoke方法源码非常短,大概就是:
public Object invoke(Object obj, Object[] args) throws Throwable { try { return fastClassInfo.f1.invoke(fastClassInfo.i1, obj, args); } catch (InvocationTargetException e) { throw e.getCause(); } }这里f1是代理类的 FastClass,i1是代理类里该方法的索引。如果你传入的obj是代理对象本身,那么它调用的就是代理类重写过的方法,也就是说会再次进入拦截器。这个特性很容易让人模板套错,后面我会单独展开。
invoke还有一个隐藏行为:如果目标方法内部又调用了自身方法,因为代理对象仍然是被增强的对象,所以内层调用同样会走拦截器,这个由代理机制决定,MethodProxy 本身无法改变。
2.3 invokeSuper(Object obj, Object[] args) 源码路径
和invoke对称的是invokeSuper:
public Object invokeSuper(Object obj, Object[] args) throws Throwable { try { return fastClassInfo.f2.invoke(fastClassInfo.i2, obj, args); } catch (InvocationTargetException e) { throw e.getCause(); } }f2是原始父类的 FastClass,i2是原始父类里该方法的索引。因此invokeSuper会直接跳到原始类的目标方法执行,不经过任何拦截器、增强逻辑,天然规避递归调用。Spring AOP 在责任链最后调用时用的就是它。你可以把invokeSuper理解为在子类里写了一句super.doSomething(),但 MethodProxy 把这个动作从“写死的 super 调用”变成了“运行时按索引调用”,灵活性高得多。
注意:invokeSuper要求传入的对象必须是代理类实例或其兼容父类实例。Spring 源码中通常传的是代理实例proxy本身,所以没问题。
2.4 为什么这个设计比反射方案强?
可以对比一下:JDK 动态代理在InvocationHandler里,为了调用原始方法,通常会持有目标接口方法,然后method.invoke(target, args)。每次调用都要经历Method.invoke的权限检查、参数包装、异常包装等逻辑。CGLIB 的 FastClass 生成时就把每个方法的调用点编译成一个整数索引,调用时直接根据索引做跳转,省掉了反射解析路径上的一大部分开销。
但这里要泼一盆冷水:现代 JVM 对反射做了大量优化(比如方法句柄、内联缓存),简单场景下反射和 FastClass 差距已经缩小。MethodProxy 真正的优势更多是“语义清晰”——它把增强调用和原始调用分成两条显式 API,让框架层不会被递归绕晕。
3. FastClass 机制:为什么方法调用从“找名字”变成了“对编号”
3.1 FastClass 的生成原理
CGLIB 在生成代理类时,不只是生成一个子类,还会给相关类生成一组 FastClass 辅助类。FastClass 做的事非常朴素:对类中的每一个可调用方法计算一个索引,然后在索引对应的invoke分支里写上强类型的方法调用。你可以把 FastClass 想象成一个带编号电话簿:以前你得一遍遍翻名字找到某个人(反射),现在所有号码都编好了,按编号打过去就行。
例如,对一个UserService.hello()方法,FastClass 里会有类似这样的代码块:
switch (index) { case 3: ((UserService) obj).hello(); return null; }实际生成的字节码远比这个复杂,但思路一致。当 MethodProxy 带着i2进来时,FastClass 直接跳进对应 case 执行,调用目标方法是直接编译好的 invokevirtual 或 invokespecial 指令,比反射少了好几层间接性。
3.2 索引是代理生成期固定的,别尝试自己算
FastClass 在代理生成阶段就把所有方法按一定规则排好序并分配索引。不同 CGLIB 版本排序规则可能不同,甚至同一个类增加方法后索引都会变化。所以用户代码里不要自己构造 MethodProxy,更不要手动指定索引。我看到过有人在拦截器里直接 new 一个 MethodProxy 想指定索引,结果不仅语义错乱,还容易在方法签名变化时踩雷。正确用法是使用intercept方法传入的 MethodProxy 实例,它是 CGLIB 为你精确配置好的。
3.3 FastClass 调用中的异常处理
FastClass 的 invoke 方法并不会直接抛业务异常,它会用InvocationTargetException包装目标方法抛出的异常,然后 MethodProxy 源码里又主动把 cause 剥离出来重新抛出:
catch (InvocationTargetException e) { throw e.getCause(); }这样做是为了保留原始异常栈和异常类型。如果你在排查问题时发现异常堆栈里出现net.sf.cglib.proxy.MethodProxy(或 Spring 重包后的类),不用慌,这只是异常穿越代理层时的包装与拆包过程。
3.4 与反射的性能对比:实测参考
性能测试容易受 JVM 状态影响,但大致趋势可以参考。我自己做过一个不严谨的简单基准:在一个对象上连续调用一个无参空方法 100 万次,裸方法调用大约 20 毫秒,Method.invoke反射大约 85 毫秒,FastClass 大约 35 毫秒,MethodHandle 大约 30 毫秒。这个结果在不同 JVM 版本和不同机器上会有波动,但能看到 FastClass 确实比裸反射快,又没快到碾压级别。
我的观点是:选型时不要单纯为了性能用 CGLIB,Spring 选哪个是全局策略问题;MethodProxy 的 FastClass 更大的价值在于支撑了“代理类动态调用原方法”这个能力,且不引入递归。
4. Spring AOP 中的实战:拦截器里到底该调用哪个方法
4.1 标准写法:DynamicAdvisedInterceptor 的回调逻辑
看 Spring 源码里的org.springframework.aop.framework.adapter包,DynamicAdvisedInterceptor实现了MethodInterceptor,intercept 方法核心逻辑是构造一个ReflectiveMethodInvocation,然后在责任链全部执行完成后调用invokeJoinpoint()。在 CGLIB 场景下,这个invokeJoinpoint其实是通过 MethodProxy 完成的。
代码大致长这样:
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // 根据 proxy/method 等信息拿到 Advisor 链 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); Object invocation = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy); return invocation.proceed(); }CglibMethodInvocation继承自ReflectiveMethodInvocation,并重写invokeJoinpoint:
protected Object invokeJoinpoint() throws Throwable { if (this.methodProxy != null) { return this.methodProxy.invokeSuper(this.proxy, this.args); } else { return super.invokeJoinpoint(); } }正是因为这里用了invokeSuper,所以执行到最后一个增强时,不会再次触发拦截器,而是干净利落地进入原始方法。这是最标准的 Spring AOP 路径,Spring 源码本身都已经替我们选好了正确答案。
4.2 自己写 MethodInterceptor 时的常见错误
假设你自己实现了一个MethodInterceptor:
public class MyInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("before"); Object result = methodProxy.invoke(obj, args); // 危险 return result; } }这里obj是代理对象,invoke(obj, args)会再次进入intercept方法,然后再次invoke,无限递归,StackOverflowError。正确的写法应该是:
Object result = methodProxy.invokeSuper(obj, args);这个坑的直接原因就是前面源码里说的:invoke调用的是代理类 FastClass,而代理类重写方法会回调所有拦截器。
4.3 invoke 方法什么时候可以用?
有些教程会说invoke用于调用目标对象。严格说,如果你想让它帮你绕开拦截器,前提是你传入的对象必须不是代理实例,否则就会递归。但在 Spring AOP 拦截器里,obj参数就是代理对象,而不是原始 target,因此直接用invoke(obj)就成了递归。如果想用 invoke 达到调用原始方法的目的,你需要额外持有原始 target 对象,但这里有个更现实的坑:invoke用的 FastClass 是代理类的,传入普通原始实例很可能会出现类型转换异常。所以普通业务代码完全没必要这么玩。
请记住:在拦截器内部,首选永远是invokeSuper。如果你真的需要在拦截器里对另一个对象调用某个方法,请先把代理对象的类型关系理清楚,再决定是不是要改用其他方式。
4.4 Spring 三级缓存与 MethodProxy 的间接关系
Spring 三级缓存解决的是循环依赖下 Bean 提前暴露的问题。三级缓存里暴露的是ObjectFactory,而如果 Bean 需要被 AOP 增强,第三级缓存中返回的其实是一个代理工厂对象,代理创建时同样会经过 CglibAopProxy,最终也会构造 MethodProxy。换句话说,三级缓存决定的是“什么时候给你代理对象”,MethodProxy 决定的是“代理对象收到调用后怎么执行原始方法”。两者不在一个阶段,但都会在调试循环依赖时出现在代理栈里。
5. 高频踩坑:invoke 和 invokeSuper 的经典混淆与解决链路
5.1 症状一:StackOverflowError,并且堆栈里反复出现同一行 intercept
这是最经典的 MethodProxy 误用。问题背景:自定义拦截器里写成methodProxy.invoke(obj, args),导致无限递归。排查链路可以这样走:
- 先看异常堆栈,如果同样的
MyInterceptor.intercept和MethodProxy.invoke反复出现,基本可以判定是递归调用。 - 检查
intercept方法里调用的是invoke还是invokeSuper。 - 将
invoke(obj, args)改成invokeSuper(obj, args)。 - 重启验证。
如果堆栈中不是同一个拦截器,而是多个拦截器互相调用,那是责任链配置问题,不是 MethodProxy 的问题。
5.2 症状二:日志或事务没生效,但没有报错
当你用 Spring AOP 增强时发现某个方法没被增强,常见原因包括:目标方法不是 public、切点不匹配、代理方式配置错误等。还有一种是类内部this.self()调用导致代理失效。MethodProxy 没法解决内部自调用,因为只有经过代理对象的入口才会走到拦截器。如果你在增强方法内部又调用了另一个增强方法,并且调用者是 this,那么这第二次调用不会经过 MethodProxy,而是直接进到当前对象的方法体。
解决办法是把需要增强的调用改成通过注入的代理对象调用,或者用((UserService) AopContext.currentProxy()),后者需要在配置里开启 exposeProxy。把这里的因果关系理清后,你就能理解为什么代理失效和 MethodProxy 息息相关:MethodProxy 负责在拦截器之后调原始方法,但如果一个方法没有被代理对象接收,MethodProxy 根本没机会执行。
5.3 症状三:调用报错 NoSuchMethodError 或 ClassCastException
这种情况经常出现在 Spring 版本升级后。Spring 内置的 MethodProxy 来自 spring-core,类全限定名是org.springframework.cglib.proxy.MethodProxy;如果你在代码里直接 importnet.sf.cglib.proxy.MethodProxy,CGLIB 版本不一致时很容易出问题。解决方法:统一使用 Spring 包下的类,不要额外引入 cglib;如果项目有多个 cglib 版本,检查依赖树,排除掉不需要的依赖。
5.4 排查代理是否生效的三种实用方法
我常用的三招:
System.out.println(userService.getClass().getName());类名里如果包含$$EnhancerByCGLIB$$,说明 CGLIB 代理已生成。更严谨一点可以用:
AopUtils.isAopProxy(userService); AopUtils.isCglibProxy(userService);返回 true 说明是代理对象。最后,还可以在拦截器里加一行日志,把methodProxy.getSignature().toString()打出来,确认签名和预期一致。
6. 性能与调试:MethodProxy 对真实项目的影响到底有多大
6.1 一次代理调用要经过多少层?
如果配置了多个通知,一次userService.save()真正执行路径是:外部调用到 CGLIB 子类重写方法,再到 DynamicAdvisedInterceptor,然后责任链上各个 MethodInterceptor,再到 CglibMethodInvocation.invokeJoinpoint,最后 MethodProxy.invokeSuper 进入原始父类 save 方法。每一层都有栈帧开销,MethodProxy 在其中只占很小一部分。性能瓶颈通常不在 MethodProxy 本身,而在增强数量、切点表达式复杂度、事务同步器等。
6.2 高并发场景下的评估建议
高并发下,代理调用次数每秒几十万次时,可以优先检查切点表达式。不要在切点表达式里写复杂的正则去匹配大量方法,因为每次调用都要做切点匹配,这往往比 MethodProxy 的 FastClass 调用更贵。一次调用同时被事务、缓存、权限多个切面包裹时,把不必要的 advice 去掉,比在底层优化 FastClass 更有效。
如果你想真真切切看到 FastClass 跳转效果,可以开启 CGLIB 的调试路径导出代理类,再配合javap反编译:
System.setProperty("cglib.debugLocation", "/tmp/cglib_classes"); javap -c -p UserService$$EnhancerByCGLIB$$abc123.class字节码里能看到代理方法中调用了MethodProxy.invokeSuper或类似指令。我自己遇到过一次奇怪问题:目标方法被增强链执行了两遍,最后排查发现是切面里的proceed()调用方式写错,在责任链里重复调用了同一个 MethodProxy。所以看到 MethodProxy 出现在栈里时,不妨也检查一下 proceed 调用次数。
6.3 调试代理栈的几个小习惯
遇到代理相关问题时,我习惯先回答三个问题:当前对象是不是代理?代理方式是 JDK 还是 CGLIB?拦截器里调用的是 invoke 还是 invokeSuper?前两个问题可以通过类名和 AopUtils 判断,最后一个问题直接看代码就能确认。很多看似神秘的代理故障,最后都落在这三个问题上。
最后说下我自己的体会:MethodProxy 虽然只是一个不起眼的内部组件,但它把“增强逻辑”和“原始调用”很好地切开。理解它之后,再去读 Spring AOP 源码会有一种打通经脉的感觉。以后再有人把 invoke 和 invokeSuper 混为一谈,你至少能一眼看出问题出在哪。