SpringAOP拦截器链递归与事务钩子补偿源码实战

拦截器链不是循环是递归:一次读透 Spring AOP MethodInvocation 与链式调用的源码实战

这是 Spring 事务源码系列的第四篇。前两篇的运行期、第三篇的织入期都反复出现同一个身影:invocation.proceed()------第一篇里它是 TransactionInterceptor "放行业务方法"的那行代码,第三篇里它是织入链排序的终点。但 proceed 本身是什么,一直没拆。读之前我以为拦截器链是框架用一个循环依次调拦截器;读完后认知被刷新:链上根本没有驱动者------框架只造了一个带着游标的调用对象,每 个拦截器自己决定要不要、以及何时调用 proceed,链是递归坍缩出来的,不是循环推着走的。顺带还清了第三篇两笔小债:ExposeInvocationInterceptor 补进链里干什么用,以及 processCommit 异常路径上那个 beforeCompletionInvoked 标记的补偿逻辑。


S --- Situation:织入讲完了,"穿过链"是黑盒

第三篇结束时的链路是:proxy 对象生成,拦截器链(排序后的 advisor 转换而来)挂在它身上。但业务方法被调用后、真正执行前,proxy 内部发生了什么,系列里一直是黑盒:

  1. TransactionInterceptor#invoke 里那行 invocation::proceed,proceed 到底去了哪?为什么它返回了,业务方法就算执行完了?
  2. 链的顺序 第三篇说是 @Order 排出来的,但排完之后顺序是在消费?框架里找不到一个"for 循环遍历拦截器"的代码;
  3. 第三篇 extendAdvisors 补进链头的 ExposeInvocationInterceptor,一个没有切点的拦截器杵在链上做什么;
  4. 事务那篇还留了个钩子补偿 的坑:processCommit 里有个 beforeCompletionInvoked 标记,catch 异常时专门检查它------补偿什么?往深了问还有三连:Spring 自己有什么逻辑必须靠钩子②执行?程序员自己实现钩子②会踩到什么?以及------到底该不该实现它?

这四个问题的答案,前三个都在同一个类里:ReflectiveMethodInvocation;第四个问题的追问贯穿 spring-tx 和 spring-jdbc。

T --- Task:三个目标,一个约束

  1. 拆掉 proceed :说清 ReflectiveMethodInvocation#proceed 的数据结构(游标)、递归结构(谁调谁)、终点(目标方法怎么真正执行);
  2. 回收第三篇的伏笔ExposeInvocationInterceptor 的作用与它在链头的必然性;
  3. 补上钩子补偿beforeCompletionInvoked 标记解决的异常路径问题,以及更深一层------钩子②的执行保证究竟保护谁、自己实现它的三个意外、按用途选钩子的实践结论。

约束:延续系列标准------结论回溯到源码的具体方法;基线 Spring 5.x(spring-aop / spring-tx)。

A --- Action:一个游标、一种递归、一处补偿

第一步:两类 proxy,殊途同归到 MethodInvocation

第三篇讲过 proxy 选型:JDK 走 JdkDynamicAopProxy、CGLIB 走 ObjenesisCglibAopProxy。两者拦截入口的方法名不同(invoke vs intercept),但主干完全一样:

java 复制代码
// JdkDynamicAopProxy#invoke(156 行起,CGLIB 的 intercept 636 行起同构):
target = targetSource.getTarget();
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) {
    // 没有拦截器:直接反射调 target,连 MethodInvocation 都不建
    retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
    MethodInvocation invocation =
            new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
    retVal = invocation.proceed();   // ← 一切从这行开始
}

JdkDynamicAopProxy.java 精简------注意空链捷径:没有切面命中的方法,proxy 开销只是一次判空。

上面是文字版主干,画成图------proxy 的 invoke/intercept 在"进入拦截链之前"还有一段完整的判断路径,两类 proxy 各一条分支:
#mermaid-svg-MeVDaqJv6tCM1PUq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MeVDaqJv6tCM1PUq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MeVDaqJv6tCM1PUq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MeVDaqJv6tCM1PUq .error-icon{fill:#552222;}#mermaid-svg-MeVDaqJv6tCM1PUq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MeVDaqJv6tCM1PUq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MeVDaqJv6tCM1PUq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MeVDaqJv6tCM1PUq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MeVDaqJv6tCM1PUq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MeVDaqJv6tCM1PUq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MeVDaqJv6tCM1PUq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MeVDaqJv6tCM1PUq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MeVDaqJv6tCM1PUq .marker.cross{stroke:#333333;}#mermaid-svg-MeVDaqJv6tCM1PUq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MeVDaqJv6tCM1PUq p{margin:0;}#mermaid-svg-MeVDaqJv6tCM1PUq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MeVDaqJv6tCM1PUq .cluster-label text{fill:#333;}#mermaid-svg-MeVDaqJv6tCM1PUq .cluster-label span{color:#333;}#mermaid-svg-MeVDaqJv6tCM1PUq .cluster-label span p{background-color:transparent;}#mermaid-svg-MeVDaqJv6tCM1PUq .label text,#mermaid-svg-MeVDaqJv6tCM1PUq span{fill:#333;color:#333;}#mermaid-svg-MeVDaqJv6tCM1PUq .node rect,#mermaid-svg-MeVDaqJv6tCM1PUq .node circle,#mermaid-svg-MeVDaqJv6tCM1PUq .node ellipse,#mermaid-svg-MeVDaqJv6tCM1PUq .node polygon,#mermaid-svg-MeVDaqJv6tCM1PUq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MeVDaqJv6tCM1PUq .rough-node .label text,#mermaid-svg-MeVDaqJv6tCM1PUq .node .label text,#mermaid-svg-MeVDaqJv6tCM1PUq .image-shape .label,#mermaid-svg-MeVDaqJv6tCM1PUq .icon-shape .label{text-anchor:middle;}#mermaid-svg-MeVDaqJv6tCM1PUq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MeVDaqJv6tCM1PUq .rough-node .label,#mermaid-svg-MeVDaqJv6tCM1PUq .node .label,#mermaid-svg-MeVDaqJv6tCM1PUq .image-shape .label,#mermaid-svg-MeVDaqJv6tCM1PUq .icon-shape .label{text-align:center;}#mermaid-svg-MeVDaqJv6tCM1PUq .node.clickable{cursor:pointer;}#mermaid-svg-MeVDaqJv6tCM1PUq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MeVDaqJv6tCM1PUq .arrowheadPath{fill:#333333;}#mermaid-svg-MeVDaqJv6tCM1PUq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MeVDaqJv6tCM1PUq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MeVDaqJv6tCM1PUq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MeVDaqJv6tCM1PUq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MeVDaqJv6tCM1PUq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MeVDaqJv6tCM1PUq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MeVDaqJv6tCM1PUq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MeVDaqJv6tCM1PUq .cluster text{fill:#333;}#mermaid-svg-MeVDaqJv6tCM1PUq .cluster span{color:#333;}#mermaid-svg-MeVDaqJv6tCM1PUq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MeVDaqJv6tCM1PUq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MeVDaqJv6tCM1PUq rect.text{fill:none;stroke-width:0;}#mermaid-svg-MeVDaqJv6tCM1PUq .icon-shape,#mermaid-svg-MeVDaqJv6tCM1PUq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MeVDaqJv6tCM1PUq .icon-shape p,#mermaid-svg-MeVDaqJv6tCM1PUq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MeVDaqJv6tCM1PUq .icon-shape .label rect,#mermaid-svg-MeVDaqJv6tCM1PUq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MeVDaqJv6tCM1PUq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MeVDaqJv6tCM1PUq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MeVDaqJv6tCM1PUq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 公共主干:拼链 → 判空 → 放行
是,JDK 路线
否,CGLIB 路线

命中
未命中
空链捷径
非空
否,业务方法
调用方执行 proxy.method()
第三篇的选型target 实现了接口?
JdkDynamicAopProxy#invoke(实现 InvocationHandler,proxy 对象实现 target 的接口)
CglibAopProxy$DynamicAdvisedInterceptor#intercept(proxy 对象是 target 类的子类对象)
equals / hashCodeDecoratingProxy / Advised接口方法?
直接分发,不走链(getDecoratedClass、读 proxy 配置等)
target = targetSource.getTarget()(每次现取,第五篇主角)
查 methodCache(键 = MethodCacheKey)
直接复用缓存的拦截器链
现场生成拦截器链
① advisor 切点匹配遍历 advisor,逐个问切点:这个 method 命中吗?(第三篇的匹配)
② advisor → 拦截器advice 已是 MethodInterceptor 直接用,否则经 AdvisorAdapter 适配(如 TransactionInterceptor 天然实现该接口)
写入 methodCache(下次同方法直接命中,第五篇主角)
chain 为空?
AopUtils.invokeJoinpointUsingReflection直接反射调 target(不建 MethodInvocation)
new ReflectiveMethodInvocation(或 CglibMethodInvocation)链 + 游标 index=-1
invocation.proceed()→ 进入踩坑一的拦截器链递归
返回调用方

读图要点:JDK 分支比 CGLIB 多一道"接口方法分诊"(equals/hashCode/DecoratingProxy/Advised 这些方法不进链,直接分发)------因为 JDK proxy 会实现这些附加接口,必须先把它们摘出去;CGLIB 分支没有这一步,直接进公共主干。两分支真正的分家点只有终点反射(第三步),主干完全共用------这正是"殊途同归到 MethodInvocation"的含义。黄底的空链捷径值得记:没有切面命中的方法,整个 AOP 的开销就是一次判空加一次反射 。中间的"生成拦截链"两步是本篇与前后的接缝:①切点匹配承接第三篇的 advisor 匹配,②advisor→拦截器的转换里 TransactionInterceptor 天然实现 MethodInterceptor 接口(第一篇的入口),生成结果写进 methodCache 则是第五篇的主角------只有第一次调用走这两步,之后每次都命中缓存直取。

先拼链、再建调用对象、然后只调一次 proceed------getInterceptorsAndDynamicInterceptionAdvice 把 advisor 转换成拦截器列表(这步有缓存,结果按方法缓存在 AdvisedSupport 的 methodCache 里,不是每次现排)。JDK 和 CGLIB 的差异只剩终点怎么反射,后面第三步讲。

⚠️ 踩坑一:链不是循环推的,是拦截器自己拉的

proceed 全文不到 30 行(160-189 行),是整个 AOP 最浓的一段代码:

java 复制代码
public Object proceed() throws Throwable {
    // 如果执行到了最后一个拦截器,那么直接反射调用目标方法
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint();        // ← 终点:真正执行业务方法
    }
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    ...
    return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    //                                        注意参数:把 this 递给拦截器 ↑
}

ReflectiveMethodInvocation.java:160 精简------游标 currentInterceptorIndex 初始值是 -1

结构上它就两件事:游标到头就反射执行目标方法;没到头就取下一个拦截器,把调用对象自己递给拦截器的 invoke ,然后返回。注意框架到这就撒手了------它没有循环、没有回调表,下一步推进的责任交给了拦截器:拦截器在自己的逻辑里想继续,就得调 mi.proceed()(递归进下一层);不想继续,直接返回结果,后面的拦截器和目标方法全部不执行
#mermaid-svg-M4mkCGI54kIlGhrp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M4mkCGI54kIlGhrp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M4mkCGI54kIlGhrp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M4mkCGI54kIlGhrp .error-icon{fill:#552222;}#mermaid-svg-M4mkCGI54kIlGhrp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M4mkCGI54kIlGhrp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M4mkCGI54kIlGhrp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M4mkCGI54kIlGhrp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M4mkCGI54kIlGhrp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M4mkCGI54kIlGhrp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M4mkCGI54kIlGhrp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M4mkCGI54kIlGhrp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M4mkCGI54kIlGhrp .marker.cross{stroke:#333333;}#mermaid-svg-M4mkCGI54kIlGhrp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M4mkCGI54kIlGhrp p{margin:0;}#mermaid-svg-M4mkCGI54kIlGhrp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M4mkCGI54kIlGhrp .cluster-label text{fill:#333;}#mermaid-svg-M4mkCGI54kIlGhrp .cluster-label span{color:#333;}#mermaid-svg-M4mkCGI54kIlGhrp .cluster-label span p{background-color:transparent;}#mermaid-svg-M4mkCGI54kIlGhrp .label text,#mermaid-svg-M4mkCGI54kIlGhrp span{fill:#333;color:#333;}#mermaid-svg-M4mkCGI54kIlGhrp .node rect,#mermaid-svg-M4mkCGI54kIlGhrp .node circle,#mermaid-svg-M4mkCGI54kIlGhrp .node ellipse,#mermaid-svg-M4mkCGI54kIlGhrp .node polygon,#mermaid-svg-M4mkCGI54kIlGhrp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M4mkCGI54kIlGhrp .rough-node .label text,#mermaid-svg-M4mkCGI54kIlGhrp .node .label text,#mermaid-svg-M4mkCGI54kIlGhrp .image-shape .label,#mermaid-svg-M4mkCGI54kIlGhrp .icon-shape .label{text-anchor:middle;}#mermaid-svg-M4mkCGI54kIlGhrp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M4mkCGI54kIlGhrp .rough-node .label,#mermaid-svg-M4mkCGI54kIlGhrp .node .label,#mermaid-svg-M4mkCGI54kIlGhrp .image-shape .label,#mermaid-svg-M4mkCGI54kIlGhrp .icon-shape .label{text-align:center;}#mermaid-svg-M4mkCGI54kIlGhrp .node.clickable{cursor:pointer;}#mermaid-svg-M4mkCGI54kIlGhrp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M4mkCGI54kIlGhrp .arrowheadPath{fill:#333333;}#mermaid-svg-M4mkCGI54kIlGhrp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M4mkCGI54kIlGhrp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M4mkCGI54kIlGhrp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M4mkCGI54kIlGhrp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M4mkCGI54kIlGhrp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M4mkCGI54kIlGhrp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M4mkCGI54kIlGhrp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M4mkCGI54kIlGhrp .cluster text{fill:#333;}#mermaid-svg-M4mkCGI54kIlGhrp .cluster span{color:#333;}#mermaid-svg-M4mkCGI54kIlGhrp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M4mkCGI54kIlGhrp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M4mkCGI54kIlGhrp rect.text{fill:none;stroke-width:0;}#mermaid-svg-M4mkCGI54kIlGhrp .icon-shape,#mermaid-svg-M4mkCGI54kIlGhrp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M4mkCGI54kIlGhrp .icon-shape p,#mermaid-svg-M4mkCGI54kIlGhrp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M4mkCGI54kIlGhrp .icon-shape .label rect,#mermaid-svg-M4mkCGI54kIlGhrp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M4mkCGI54kIlGhrp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M4mkCGI54kIlGhrp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M4mkCGI54kIlGhrp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 自己决定调 mi.proceed()
递归继续
proxy 入口 proxy.method()
new ReflectiveMethodInvocation(链 + 游标 index=-1)
proceed()++index 取第 i 个拦截器
拦截器A.invoke(mi)(如 ExposeInvocationInterceptor)
proceed() 递归++index
拦截器B.invoke(mi)(如 TransactionInterceptor)开事务 → mi.proceed()
游标到头invokeJoinpoint()
目标方法真正执行(CGLIB: methodProxy.invoke / JDK: 反射)
返回值沿递归逐层回落拦截器B: commit 事务
拦截器A: finally 恢复现场

对照第一篇:TransactionInterceptor 的五段式编排之所以能"前面开事务、后面提交",靠的正是它夹在递归中间------proceed 之前是"事务前",proceed 返回之后是"事务后"。

flowchart 画得出递归的"去程",但"返回值沿调用栈逐层回落"这种双向时序,用时序图更一目了然------同一模型换一个视角:
目标方法 拦截器B(事务拦截器) 拦截器A(链头登记员) 调用方 目标方法 拦截器B(事务拦截器) 拦截器A(链头登记员) 调用方 #mermaid-svg-6DnegyVGtPp5fTZw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6DnegyVGtPp5fTZw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6DnegyVGtPp5fTZw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6DnegyVGtPp5fTZw .error-icon{fill:#552222;}#mermaid-svg-6DnegyVGtPp5fTZw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6DnegyVGtPp5fTZw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6DnegyVGtPp5fTZw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6DnegyVGtPp5fTZw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6DnegyVGtPp5fTZw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6DnegyVGtPp5fTZw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6DnegyVGtPp5fTZw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6DnegyVGtPp5fTZw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6DnegyVGtPp5fTZw .marker.cross{stroke:#333333;}#mermaid-svg-6DnegyVGtPp5fTZw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6DnegyVGtPp5fTZw p{margin:0;}#mermaid-svg-6DnegyVGtPp5fTZw .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6DnegyVGtPp5fTZw text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6DnegyVGtPp5fTZw .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6DnegyVGtPp5fTZw .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6DnegyVGtPp5fTZw .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6DnegyVGtPp5fTZw .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6DnegyVGtPp5fTZw #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6DnegyVGtPp5fTZw .sequenceNumber{fill:white;}#mermaid-svg-6DnegyVGtPp5fTZw #sequencenumber{fill:#333;}#mermaid-svg-6DnegyVGtPp5fTZw #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6DnegyVGtPp5fTZw .messageText{fill:#333;stroke:none;}#mermaid-svg-6DnegyVGtPp5fTZw .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6DnegyVGtPp5fTZw .labelText,#mermaid-svg-6DnegyVGtPp5fTZw .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6DnegyVGtPp5fTZw .loopText,#mermaid-svg-6DnegyVGtPp5fTZw .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6DnegyVGtPp5fTZw .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6DnegyVGtPp5fTZw .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6DnegyVGtPp5fTZw .noteText,#mermaid-svg-6DnegyVGtPp5fTZw .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6DnegyVGtPp5fTZw .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6DnegyVGtPp5fTZw .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6DnegyVGtPp5fTZw .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6DnegyVGtPp5fTZw .actorPopupMenu{position:absolute;}#mermaid-svg-6DnegyVGtPp5fTZw .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-6DnegyVGtPp5fTZw .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6DnegyVGtPp5fTZw .actor-man circle,#mermaid-svg-6DnegyVGtPp5fTZw line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6DnegyVGtPp5fTZw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} proceed 之前 = "事务前" proceed 返回之后 = "事务后" mi.proceed()(index: 0)invocation.set(mi) 登记上下文mi.proceed()(index: 1)createTransactionIfNecessary 开事务mi.proceed()(游标到头)业务逻辑真正执行(invokeJoinpoint)返回值 / 异常commit / rollback 收尾返回值回落finally 恢复外层上下文最终返回值

每个拦截器在时序上"夹住"了它下方的整段递归------B 的开事务和 commit 分别在目标方法执行的两侧,这就是"环绕"的物理形态。

哪个拦截器不调 proceed,链就从它这里断掉。这不是 Bug 是特性:缓存命中直接返回的拦截器、鉴权失败直接抛的拦截器,都是在利用"不 proceed"短路整条链------但也意味着写自定义拦截器时,"忘记调 proceed"是静默吞掉目标方法的典型事故(方法不报错、就是不执行)。教训一句话:链的推进权在拦截器手里,框架只递游标。

第二步:回收伏笔------ExposeInvocationInterceptor 是链头的"登记员"

第三篇 extendAdvisors 每逢有命中就往链头补的默认拦截器,作用只有一件事:把当前 MethodInvocation 放进 ThreadLocal,让链上任何位置的代码都能拿到本次调用的上下文(target 对象、方法、参数):

java 复制代码
public Object invoke(MethodInvocation mi) throws Throwable {
    MethodInvocation oldInvocation = invocation.get();   // 记住外层(嵌套 proxy 时)
    invocation.set(mi);                                   // 登记当前调用
    try {
        return mi.proceed();                              // 链头,必然放行
    }
    finally {
        invocation.set(oldInvocation);                    // 恢复外层
    }
}

ExposeInvocationInterceptor.java:89 精简------它的 order 是 HIGHEST_PRECEDENCE + 1,保证几乎总在链头。

值得单独一看的是它的现场保护结构oldInvocation 记旧值、finally 恢复------和第一篇 TransactionInfobindToThread() / restoreThreadLocalStatus() 一模一样的套路。Spring 处理"嵌套 + ThreadLocal"的固定模式就是:进栈前记旧值、finally 恢复------事务栈这么干、AOP 调用上下文这么干,第二篇的挂起/恢复本质也是这个模式的变体。三篇博客在这里合流。

还有个源码里写得很直白的坑:currentInvocation() 取不到时抛的 IllegalStateException,报错文案专门警告 "advices with order HIGHEST_PRECEDENCE will execute before ExposeInvocationInterceptor" ------因为它的 order 是 HIGHEST_PRECEDENCE + 1,如果自定义切面 order 恰好是最小值,就排到了登记员前面,执行时 ThreadLocal 还是空的。

第三步:两类 proxy 唯一的实质差异------终点怎么反射

游标到头后的 invokeJoinpoint(),JDK 与 CGLIB 各有各的实现:

java 复制代码
// JDK 路线:纯反射
return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);

// CGLIB 路线(CglibMethodInvocation 重写):优先走 MethodProxy
if (this.methodProxy != null) {
    return this.methodProxy.invoke(this.target, this.arguments);  // FastClass,免反射
}
else { return super.invokeJoinpoint(); }

ReflectiveMethodInvocation.java:198 / CglibAopProxy.java:749 精简------源码注释原话:对 public 方法是"边际性能提升"。

JDK 与 CGLIB 的全部差异,收窄到最后这一步调用方式上:前者反射、后者用 CGLIB 生成的 FastClass 直接索引调用。前面的拼链、建调用对象、递归推进完全共用同一套 ReflectiveMethodInvocation------第三篇"选型三条件"决定了用哪种 proxy,走到终点时两种 proxy 的差别只剩这一行。

⚠️ 踩坑二:钩子②的补偿------为什么 commit 异常还要补调 beforeCompletion

回到事务侧最后一个欠账。第三篇画了 processCommit 的四钩子时间线,但有个细节当时略过:triggerBeforeCommittriggerBeforeCompletion 之间夹着一个布尔标记,catch 异常时专门检查它:

java 复制代码
prepareForCommit(status);
triggerBeforeCommit(status);         // 钩子①:这里抛异常的话 ↓
triggerBeforeCompletion(status);     // 钩子②:就执行不到
beforeCompletionInvoked = true;      // ← 只有执行到这行,标记才为 true
...
catch (RuntimeException | Error ex) {
    if (!beforeCompletionInvoked) {
        triggerBeforeCompletion(status);   // ← 补偿:钩子②必须被保证执行
    }
    doRollbackOnCommitException(status, ex);
    throw ex;
}

AbstractPlatformTransactionManager.java#processCommit 826-850 精简。

这段在补偿一个真实的不对称:钩子②(beforeCompletion)按契约是"事务即将完成"的最终通知 ------同步器靠它清理资源,无论提交成败都要执行 。但钩子①是用户代码,它抛异常会把钩子②顶掉。所以框架用 beforeCompletionInvoked 标记做了兜底:异常路径上发现钩子②没跑过,先补调再回滚。钩子①抛异常不影响钩子②的执行保证------这是写同步器时最容易误判的点,很多人以为 beforeCommit 出错钩子②就被跳过了。对照 processRollback:那里钩子②在最前面(900 行)先执行,天然不存在被顶掉的问题,所以没有这个标记------一个标记的有无,反推出两条路径的执行顺序差异。

追问一层:为什么钩子②必须被保证?------因为 Spring 自己的东西在里面跑

钩子②不是给用户预留的空位。全仓库搜 registerSynchronization 的内部调用方,能列出一串框架自身的资源清理逻辑:

内部使用方 在钩子里做什么
DataSourceUtils.ConnectionSynchronization(spring-jdbc:474) beforeCompletion 里提前归还连接 ------javadoc 原话 "avoid issues with strict JTA implementations that expect the close call before transaction completion",JTA 严格校验"连接必须在事务完成前释放"的时序
spring-orm 的 JPA/Hibernate 集成(ExtendedEntityManagerCreator 等) 事务级 EntityManager/Session 的回滚与关闭;afterCompletion 里还有"未提交则补回滚"的兜底
TransactionAwareCacheDecorator(spring-context-support) 缓存写延迟到事务成功后才落
@TransactionalEventListener(第三篇讲过) 把"扣下"的事件挂到钩子④分流执行

所以补偿标记保护的不是某个具体功能,而是一份资源清理契约:钩子②承诺"任何结局前执行",上面这些清理逻辑全部依赖这份承诺。钩子①一抛异常就跳过钩子②,泄漏的是这些内部资源。

⚠️ 追问二层:自己实现钩子②,三个意料之外

意外一:钩子②里抛的异常会被静默吞掉,只打一行 error 日志。 看触发器实现,四个钩子四种异常策略:

java 复制代码
public static void triggerBeforeCompletion() {          // 钩子②:吞异常
    for (TransactionSynchronization synchronization : ...) {
        try {
            synchronization.beforeCompletion();
        } catch (Throwable tsex) {
            logger.error("TransactionSynchronization.beforeCompletion threw exception", tsex);  // 只记日志
        }
    }
}

TransactionSynchronizationUtils.java:104 精简。四个钩子对照:钩子① beforeCommit 异常一路抛出 、事务转回滚;钩子③ afterCommit 抛给调用方 但事务已提交(第三篇踩坑一);钩子②④ 吞掉只记日志

这个"吞"是设计而非缺陷:钩子②④运行在事务收尾的关键路径上,同步器异常绝不能干扰提交/回滚本身。但对业务代码的后果是:清理失败业务无感知、告警不响,资源泄漏变成静默的

意外二:内层事务方法结束不触发钩子,全部攒到最外层。 triggerBeforeCompletion 有个前提 status.isNewSynchronization(),而"是否真拥有同步"算得很细:

java 复制代码
// 除了传入的标志之外还需要判断当前线程上的同步是否激活
boolean actualNewSynchronization = newSynchronization &&
        !TransactionSynchronizationManager.isSynchronizationActive();

AbstractPlatformTransactionManager.java:573------REQUIRED 内层加入外层事务时同步已激活,actualNewSynchronization=false。

REQUIRED 传播下内层方法的四个钩子一个都不触发 ,全等外层事务提交时统一跑。以为"每个 @Transactional 方法结束都会执行我的清理",实测内层根本不执行。

意外三:钩子②运行时物理提交还没发生。 它排在 doCommit/doRollback 之前 ------里面查库读到的是未提交数据;写 SQL 会搭上事务"末班车"跟着一起提交(此时连接仍绑在 ThreadLocal、autoCommit=false)。与钩子③④"提交后才执行"语义相反,最易混用。另外钩子④的 status 参数可能收到 STATUS_UNKNOWN(commit 异常路径,processCommit 844 行),按状态分支的代码要处理这个值。

那到底该不该实现钩子②?

不是"不该实现",而是按用途选钩子 。javadoc 的契约(TransactionSynchronization.java:92-100)写得很直白:钩子②定位 "resource cleanup before transaction completion, for any outcome" 、异常 "logged but not propagated"------两句话正好框定它的适用域:

  • 业务通知类 (发消息、刷缓存、通知下游):别用钩子②,用 @TransactionalEventListener 的 AFTER_COMMIT(挂在钩子④、提交后执行、语义匹配);
  • 资源释放类 :钩子②是正确位置,但要守三条纪律------只放"失败也无所谓"的清理代码;清理失败自己打告警(框架只 logger.error 一行);别在钩子里读库查事务结果(提交未发生、读不到真实状态)。

教训一句话:钩子的异常策略就是它的适用域说明书------"吞异常"的钩子只配做清理,"抛异常"的钩子才承载业务语义;设计自己的收尾钩子时,先想清楚异常该往哪去,再决定吞还是抛。

笔者的建议 :业务项目里尽量不要实现 beforeCompletion 这个钩子。它的一切特性------异常被吞、清理失败无感知、REQUIRED 内层不触发、执行时提交未发生------都说明它是为框架内部的资源管理设计的契约位,不是给业务代码用的高级回调。真有"事务结束前"的需求,优先把逻辑挪到 @TransactionalEventListener 的 AFTER_COMPLETION(拿到最终状态、还有 fallbackExecution 兜底);确实需要自定义同步器时,也优先实现 afterCompletion 而不是 beforeCompletion。上面"资源释放类三条纪律"是给不得不用的人的护栏,不是推荐用法。

印证:@TransactionalEventListener 的枚举就是这份结论的官方背书

验证"按用途选钩子"最直接的办法,是看 Spring 自己给业务代码开了哪些口子。@TransactionalEventListenerphase 属性只有四个枚举值:

java 复制代码
public enum TransactionPhase {
    BEFORE_COMMIT,      // 钩子①
    AFTER_COMMIT,       // 钩子④,status == STATUS_COMMITTED
    AFTER_ROLLBACK,     // 钩子④,status == STATUS_ROLLED_BACK
    AFTER_COMPLETION    // 钩子④,任意状态
}

TransactionPhase.java 全文------四个值只对应钩子①和钩子④,钩子②和钩子③没有入口。

对照 TransactionSynchronizationEventAdapter 的重写方法也能印证:它只实现了 beforeCommit(boolean)afterCompletion(int)(108-125 行),钩子②压根没被接线。为什么:

  • 钩子②不暴露 :注解定位是"业务通知",而钩子②是框架内部的资源清理契约位------业务代码进去就是踩前面三个意外的坑,框架索性从枚举里删掉。这可以看作 API 层面的防呆:javadoc 契约约束不住的场景,直接不提供入口;
  • 钩子③也不暴露 :钩子③与钩子④在"提交后执行"上语义重叠,但钩子③不是必经路径(回滚场景走不到它),钩子④按状态分流反而更完整------AFTER_ROLLBACK 想在回滚后收到通知,只有钩子④能挂。

Spring 用 API 可见性表达契约TransactionSynchronization 接口面向框架内部和高级用户(四个钩子全开),@TransactionalEventListener 面向业务代码(只开两个安全位------钩子①和按状态分流的钩子④)。业务侧"提交前"只有 BEFORE_COMMIT 一个口子,"提交后"全部汇到钩子④。选型时不用背规则:枚举里有的才用,需要钩子②③的场景,先问自己是不是在做资源清理

R --- Result:黑盒拆完,系列合拢

结论 源码出处 状态
两类 proxy 入口同构:拼链→空链捷径→建 MethodInvocation→proceed 一次 JdkDynamicAopProxy#invoke 156-221 / CglibAopProxy#intercept 636
链是递归不是循环:拦截器拿 mi 自决是否 proceed,游标 -1 起 ReflectiveMethodInvocation#proceed 160-189
不调 proceed 即断链,目标方法静默不执行 同上------框架无驱动循环,唯一推进力是拦截器
ExposeInvocationInterceptor = 链头登记员,ThreadLocal + 记旧值恢复 ExposeInvocationInterceptor 89-98,order=HIGHEST+1
JDK/CGLIB 差异收窄在终点:反射 vs FastClass invokeJoinpoint 198 / CglibMethodInvocation 749-755
钩子②有执行保证,靠 beforeCompletionInvoked 标记补偿钩子①的异常 processCommit 790-847
钩子②的执行保证服务于框架内部资源清理(连接提前释放、JPA 会话回滚、缓存延迟落库等) DataSourceUtils 474 / ExtendedEntityManagerCreator 474 / TransactionAwareCacheDecorator
四钩子四种异常策略:①抛出转回滚、②④吞掉记日志、③抛给调用方但已提交 TransactionSynchronizationUtils 94/104/121/166
REQUIRED 内层不触发任何钩子(actualNewSynchronization=false),攒到最外层 newTransactionStatus 573 + triggerBeforeCompletion 1010
@TransactionalEventListener 只开钩子①和钩子④两个口子,钩子②③不暴露(API 层防呆) TransactionPhase 枚举 + TransactionSynchronizationEventAdapter 108-125

四篇至此拼成全图:织入期 (第三篇:BeanPostProcessor 遍历→advisor 匹配→proxy 生成)→ 链期 (本篇:拼链→递归推进→终点反射)→ 事务期 (第一、二篇:拦截器内五段编排→传播分派→doBegin/savepoint/suspend)→ 收尾期(第三、四篇:钩子时间线与补偿)。

沉淀的三条原则

  1. 看到"链"先找驱动者------拦截器链、过滤器链、同步器链,第一问永远是"谁在推进";驱动者框架还是链上节点自己,决定了责任与短路语义;本篇答案是后者,所以短路权在拦截器;
  2. "前后夹逻辑"的中间件,识别它是否站在递归的夹层里------事务拦截器、@Around、日志切面能夹住目标方法,是因为它们调的 proceed 就是下一层递归;理解这一点,"环绕通知为什么能改返回值"这类问题不再需要记;
  3. 兜底要靠标记,不要靠顺序假设 :钩子②的执行保证来自 beforeCompletionInvoked 显式标记,而不是"钩子①一般不会抛异常"的祈祷------processRollback 没有这个标记恰恰因为它用顺序天然保证了,两种解法对照着看,比背结论有用得多。

遗留事项

两个点仍可深挖,作为系列可能的第五篇:拦截器链的缓存与动态性AdvisedSupport 的 methodCache 何时失效、setTarget 运行时换 target 对象对已生成 proxy 的影响);以及 AspectJ 风格 advice 对 proceed 的恰好一次约束ProceedingJoinPoint 为什么强制 proceed 只能调一次,和本篇递归模型的关系)。


本文全部基于开源框架(Spring)的公开源码研读,代码片段为源码节选精简,无敏感信息。

相关推荐
SL_staff1 小时前
制造业私有化文档平台的技术实践:从知识孤岛到可追溯知识资产
java·spring·开源
SL_staff2 小时前
3天上线OKR系统:一名HR与1名工程师如何用JVS完成全栈交付
java·低代码·开源
2501_933923253 小时前
统一功能:统一返回格式+统一异常处理
spring·java-ee·状态模式·统一数据返回
wzdark4 小时前
基于链表的内存池设计与内存复用机制4
java·数据结构·链表
cnkeysky4 小时前
idea 中的 maven 项目重新加载子模块
java·idea
Escalating_xu4 小时前
【System V 信号量】从 P/V 原语到 Builder 封装:写出可控、可清理的进程互斥组件
java·linux·开发语言·jvm
今天AI了吗4 小时前
什么是 AI Agent?它与直接调用大模型 API 有何区别
java·网络·人工智能·架构·java-ee
步行cgn4 小时前
Spring 注入 Map 集合详解
数据库·python·spring
隐退山林4 小时前
JavaEE进阶:SpringAOP
java·java-ee