两个 around 怎样改变同一个返回值

订单方法返回字符串 order。A 通知把结果包装成 A(结果),B 通知包装成 B(结果)。按照 A、B 的链顺序执行,调用者得到 A(B(order));把链顺序改成 B、A,结果变成 B(A(order))。通知顺序不只决定日志先后,也可以改变业务结果。

实验固定 Spring Framework 6.2.11、JDK 21,源码 SHA 4c134254642d88e058aa004bdaf44168e1be7bb2。完整入口是 examples/spring-framework-lab/src/main/java/blog/spring/Chapter17.java。它用 AOP Alliance MethodInterceptor 表达 around 语义,随后对照 AspectJ around 的适配路径;没有把两套 proceed API 的所有细节当成完全相同。

进入、返回和异常展开的拦截器链

proceed 推进的是同一条调用链

ReflectiveMethodInvocation 保存 proxy、target、方法、参数、目标类和拦截器列表。currentInterceptorIndex 初始为 -1。proceed 首先检查是否已到最后一项;如果是,就调用 invokeJoinpoint 执行目标。否则索引递增,取得下一项并进入通知。固定版本调用链

1
2
3
4
5
6
7
8
9
10
11
trace.add(name + "+");
try {
Object value = invocation.proceed();
trace.add(name + " return");
return name + "(" + value + ")";
} catch (Throwable failure) {
trace.add(name + " throw");
throw failure;
} finally {
trace.add(name + " finally");
}

A 的 proceed 可能进入 B,B 的 proceed 才进入目标。A 并不知道下一步一定是目标对象。如果链中还有参数校验、缓存或事务拦截器,这些也在目标前面。把 proceed 口头解释成“直接调用业务方法”,容易忽略后续拦截器仍可拒绝调用或修改结果。

第 16 篇的动态匹配项也由 proceed 处理。不满足运行时条件时,索引仍向后推进,跳过当前通知而不重置链。链的位置是每次 invocation 的状态;不是保存在单例通知中的全局计数。

正常返回按照调用栈展开

本文记录完整列表并作相等断言,而不凭日志肉眼判断:

1
[A+, B+, target, B return, B finally, A return, A finally]

目标先返回 order,B 的 proceed 完成,于是 B 记录 return 并准备返回 B(order)。B 的 finally 在这个返回真正交给 A 前执行。A 得到 B(order),再次包装成 A(B(order)),最后执行自身 finally。

进入顺序和退出顺序相反,是嵌套方法调用的后果。它不要求框架在目标执行后另建一条逆序链。可以用同一调用栈解释 around 的开始、返回、异常和清理,无需为每个现象创造一套独立优先级规则。

返回值是通知契约的一部分。MethodInterceptor 返回 Object 不代表代理的调用者接受任意类型。接口方法仍有声明返回类型;返回 null 给基本类型,或返回不兼容的对象,会在代理边界暴露错误。本文使用 String 到 String 的变换,以免把顺序实验与类型错误混在一起。

异常路径不会执行正常返回部分

OrderTarget 在 fail=true 时抛出预先创建的 IllegalStateException。保存异常实例使实验能断言最终抛出的对象与目标异常是同一个引用,而不是只有同一个类名或 message。

1
[A+, B+, target, B throw, B finally, A throw, A finally]

B 的 proceed 没有正常返回,因此跳过 B return。B 捕获后原样抛出,finally 仍执行。A 接收到异常,经历同样过程,最终调用者收到目标的那个异常对象。本文对列表和异常身份分别断言;只看到 finally 日志,不能推断目标已经成功。

异常是否继续传播取决于通知代码。另一个实验在外层捕获 IllegalStateException 并返回 fallback,调用者获得正常结果。这会影响更外层的事务或重试通知能看到什么信号。它是明确改变行为的恢复策略,不能在不知道外层契约时为了“让调用不报错”而随意加入。

这里没有执行数据库事务。只能证明外层拦截器看到的是内部最终返回或抛出的结果,不能据此宣称吞异常一定提交或一定回滚。事务状态还受 rollback-only 标记、传播和管理器行为影响,后续事务篇分别验收。

顺序必须追到实际装配入口

实验给 A 设置 order=20,给 B 设置 order=10,但手工 ProxyFactory.addAdvisor(a) 后再 addAdvisor(b),调用仍为 A 包住 B。这个 API 按加入的位置维护 Advisor 列表,不会因为两个对象实现 Ordered 就自动对整张列表排序。

随后实验对同两个 Advisor 显式调用 AnnotationAwareOrderComparator.sort(),再按排序结果加入新的工厂。现在 order 更小的 B 位于外层,返回 B(A(order))。这项对照把“对象携带顺序值”和“装配入口实际应用排序”分开。

自动代理路径的 AbstractAdvisorAutoProxyCreator.sortAdvisors() 会调用排序器。AspectJ 自动代理的子类还处理 AspectJ 优先级与同一切面中的通知关系。应查具体入口,不宜把本文手动列表顺序扩展成所有自动装配规则。自动代理排序源码

官方文档说明不同切面可以使用 Ordered 或 @Order 表达优先级,高优先级通知进入较早、退出较晚;没有声明的同类通知顺序不应作为业务依赖。这里的“高优先级”需要转换成实际 order 数值语义,不能简单把数字更大理解成更优先。通知优先级文档

不调用 proceed 会发生什么

第三个代理只安装一个返回 cached 的通知,从不调用 proceed。即使传入 fail=true,目标也没有执行,trace 保持空,调用者正常取得 cached。由此可以反证“代理调用返回就说明目标方法执行过”。

缓存通知可以合法短路;鉴权通知可以在目标之前抛异常。排障时应确认是否到达目标、是否有结果替换,以及结果来自哪一层,而不是只在目标内打一个永远没有命中的断点。

重复 proceed 需要更谨慎。ReflectiveMethodInvocation 的索引是可变状态,直接对同一个 invocation 随意调用两次,不能假设第二次会从最外层重新执行。AspectJ 的 MethodInvocationProceedingJoinPoint.proceed() 使用 invocableClone,为 around 中的后续执行提供独立的调用位置;它仍不会自动保证重复业务操作幂等。AspectJ proceed 适配

本文没有执行重复扣库存或重试实验,因而不提供“多次 proceed 安全”的结论。上游 AspectJAutoProxyCreatorTests.retryAspect 与 aspectsAreAppliedInDefinedOrder 可用于继续阅读重试与排序的实际测试约束;这些测试源码被阅读,本文实际运行的是自己定义的顺序、短路与异常场景。上游 around 与排序测试

复现与练习

1
./mvnw -q compile exec:java -Dexec.mainClass=blog.spring.Chapter17

evidence/17/local-20261002/run.txt 保存两条完整 TRACE、返回值断言、异常身份断言以及短路与恢复结果。全部通过才输出 RESULT Chapter17 PASS;程序没有创建执行器和外部资源。

反例题:只给两个 Advisor 设置不同 order 值,是否能保证任何 ProxyFactory 都自动按该值排序?手动加入的第一个场景已经否定。排序值需要被装配入口读取和应用。

可执行练习:把 B 的 finally 改为抛出一个新的 IllegalArgumentException,再执行 fail=true。保留目标异常身份断言,观察它失败,因为清理异常替换了正在传播的异常。随后恢复 finally,只把原异常加为新异常的 cause,检查调用者看到的顶层类型与 cause。这个实验能区分“异常对象原样传播”和“保留因果链后翻译异常”。

系列入口:深入 Spring(00):从手动组装到可验证的容器实验。