ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring AOP核心源码:MethodProxy的invoke与invokeSuper解析

Spring AOP核心源码:MethodProxy的invoke与invokeSuper解析 如果有人问我 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 里按CtrlN搜 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创建代理。如果目标类没有接口或者配置了proxyTargetClasstrueSpring 会选用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 ProxyCGLIB MethodProxy能否代理无接口类不能可以调用原始方法Method.invoke 反射FastClass 索引调用拦截器入口InvocationHandler.invokeMethodInterceptor.interceptSpring 默认场景接口存在时使用proxyTargetClasstrue 或无接口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是代理类的 FastClassi1是代理类里该方法的索引。如果你传入的obj是代理对象本身那么它调用的就是代理类重写过的方法也就是说会再次进入拦截器。这个特性很容易让人模板套错后面我会单独展开。invoke还有一个隐藏行为如果目标方法内部又调用了自身方法因为代理对象仍然是被增强的对象所以内层调用同样会走拦截器这个由代理机制决定MethodProxy 本身无法改变。2.3 invokeSuper(Object obj, Object[] args) 源码路径和invoke对称的是invokeSuperpublic Object invokeSuper(Object obj, Object[] args) throws Throwable { try { return fastClassInfo.f2.invoke(fastClassInfo.i2, obj, args); } catch (InvocationTargetException e) { throw e.getCause(); } }f2是原始父类的 FastClassi2是原始父类里该方法的索引。因此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 确实比裸反射快又没快到碾压级别。我的观点是选型时不要单纯为了性能用 CGLIBSpring 选哪个是全局策略问题MethodProxy 的 FastClass 更大的价值在于支撑了“代理类动态调用原方法”这个能力且不引入递归。4. Spring AOP 中的实战拦截器里到底该调用哪个方法4.1 标准写法DynamicAdvisedInterceptor 的回调逻辑看 Spring 源码里的org.springframework.aop.framework.adapter包DynamicAdvisedInterceptor实现了MethodInterceptorintercept 方法核心逻辑是构造一个ReflectiveMethodInvocation然后在责任链全部执行完成后调用invokeJoinpoint()。在 CGLIB 场景下这个invokeJoinpoint其实是通过 MethodProxy 完成的。代码大致长这样public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // 根据 proxy/method 等信息拿到 Advisor 链 ListObject chain this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); Object invocation new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy); return invocation.proceed(); }CglibMethodInvocation继承自ReflectiveMethodInvocation并重写invokeJoinpointprotected 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 时的常见错误假设你自己实现了一个MethodInterceptorpublic 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.MethodProxyCGLIB 版本不一致时很容易出问题。解决方法统一使用 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 混为一谈你至少能一眼看出问题出在哪。
返回列表