当项目中使用声明式事务(@Transactional)的同时,又定义了自定义 AOP 切面(例如统一日志、操作审计、权限校验等),一个绕不开的问题就是:这些切面的执行顺序是怎样的?如果顺序不符合预期,该如何调整?
9.5.1 默认顺序与隐含风险
Spring 在创建代理时会为每个 Bean 叠加多个切面,这些切面最终形成一条“拦截器链”。对于事务切面而言,Spring 通过 InfrastructureAdvisorAutoProxyCreator 将其封装为一个 Advisor,并赋予固定的优先级:
- 事务 Advisor 的默认顺序为
Ordered.LOWEST_PRECEDENCE(即Integer.MAX_VALUE),这意味着在所有自定义 Advisor 中,事务切面默认处于最外层。
这种默认设计的意图是:自定义切面通常关注业务逻辑本身(如参数校验、业务日志),而事务是底层基础设施的保障,理应最先被“包围”,从而保证整个调用链都在同一个事务边界内。
然而,某些场景下这种默认顺序会带来严重问题。想象一个自定义切面负责在方法执行后向数据库的另一张审计表中插入记录。如果该切面运行在事务边界之外,那么主流程的回滚不会影响审计记录;反之,如果审计记录应当随主事务一起回滚,就需要让自定义切面位于事务边界之内。此外,一些自定义切面可能依赖当前事务上下文(如读取已提交但尚未最终提交的数据),也要精确控制它们相对事务的包裹关系。
9.5.2 用 @Order 或 Ordered 接口控制优先级
Spring AOP 中,切面的执行顺序由 Ordered 接口值决定,值越小,优先级越高,越先执行,也越晚退出。对于使用 @Aspect + @Component 定义的切面,可以通过 @Order 注解直接指定顺序;对于编程式 Advisor,可让其实现 Ordered 接口。
事务切面的顺序则由 @EnableTransactionManagement 的 order 属性控制(仅限注解法启动事务时),或者通过 XML 中的 <tx:annotation-driven order="…"/> 设置。默认是 Ordered.LOWEST_PRECEDENCE。
┌──────────────────────────────────┐
│ @Transactional Order=HIGHEST│ ← 默认最大,最外层
│ ┌───────────────────────────┐ │
│ │ 自定义切面 @Order(1) │ │
│ │ ┌───────────────────┐ │ │
│ │ │ 业务方法 │ │ │
│ │ └───────────────────┘ │ │
│ └───────────────────────────┘ │
└──────────────────────────────────┘
如果需要让自定义切面运行在事务之内,应将其 @Order 值设为小于 Ordered.LOWEST_PRECEDENCE(即任意较小的正整数),并保证它小于事务的 order。反过来,若希望自定义切面在事务提交后才执行(比如只记录已提交成功的结果),可以将其顺序设置为大于 LOWEST_PRECEDENCE 或直接使用 @Order(Ordered.LOWEST_PRECEDENCE + 1)。
9.5.3 实战示例:审计记录需同事务一起回滚
假设有一个服务方法 placeOrder,它需要在新事务中创建订单,同时通过自定义切面 AuditAspect 将操作记录插入到审计表。
目标:当订单创建过程中发生异常,订单记录回滚,审计记录也必须回滚。也就是说,审计切面必须在事务边界之内执行。
实现步骤:
- 定义审计切面,并指定一个比事务更优先的
@Order值,确保它被事务包裹。
@Aspect
@Component
@Order(1) // 值远小于 LOWEST_PRECEDENCE,优先于事务
public class AuditAspect {
@Around("@annotation(auditable)")
public Object audit(ProceedingJoinPoint pjp, Auditable auditable) throws Throwable {
String action = auditable.value();
Object result = null;
try {
result = pjp.proceed(); // 执行业务方法(事务尚未提交)
insertAuditLog(action, "SUCCESS");
} catch (Exception e) {
insertAuditLog(action, "FAILURE");
throw e;
}
return result;
}
private void insertAuditLog(String action, String status) {
// 记录动作到数据库。因为处于同一事务,回滚时会一并撤销。
}
}
- 保持事务管理的默认顺序(
LOWEST_PRECEDENCE),或显式设置为一个较大的值,确保它“包围”审计切面。
@Configuration
@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE) // 默认如此
public class AppConfig {
}
此时执行链为:事务开始 → 审计切面 → 业务方法 → 审计切面结束 → 事务提交/回滚。由于审计切面的数据库操作与业务方法处于同一事务,任何异常都会导致全部回滚。
9.5.4 反向场景:仅记录已提交的结果
某些监控切面需要在事务成功提交后才记录信息,且不能因记录失败影响主业务。此时可以反转顺序:将自定义切面的 order 设为大于事务的 order,使其运行在事务边界之外;或者干脆放弃 AOP,改用事件发布监听 TransactionSynchronization 回调(例如 TransactionSynchronizationManager.registerSynchronization()),这往往更加可靠,但这里先讨论切面顺序方案。
@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE + 1) // 确保在事务切面之后,变成最外层
public class PostCommitLogAspect {
@AfterReturning("@annotation(TxLog)")
public void logAfterCommit(TxLog txLog) {
// 此时代理链外层的 @Transactional 尚未提交,但如果顺序高于事务,则会先于事务提交执行。
// 实际上,需要更精准的“提交后”控制,推荐使用 TransactionSynchronization。
// 这里仅演示顺序效果。
}
}
但需要注意,@Around 切面很难保证在事务提交之后执行数据库写入,因为一旦 method 返回,事务管理器的后置通知仍会完成提交操作,而自定义切面的外层就可能是最后执行的。更稳妥的做法是结合 TransactionSynchronization:
@Aspect
@Component
public class SafePostCommitAspect {
@AfterReturning("@annotation(TxLog)")
public void afterReturning() {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
// 事务确实已提交,此处执行安全的后续操作
}
});
}
}
这避开了切面之间顺序的微妙影响,属于生产推荐的方案。
9.5.5 多个自定义切面之间的协调
当项目出现多个自定义切面时,建议为每个切面明确定义 @Order 值,并绘制清晰的“洋葱图”。常用的分层参考如下:
数值小 (先执行, 后退出) → 数值大 (后执行, 先退出)
-------------------------------------------------
安全切面 @Order(1)
参数校验 @Order(2)
业务日志 @Order(3)
自定义缓存 @Order(4)
事务 Ordered.LOWEST_PRECEDENCE (默认)
外部 API 调用监控 @Order(LOWEST_PRECEDENCE + 1)
这种显示声明顺序的习惯,能够避免因 AOP 自动排序带来的不可预测行为,也让团队的代码可读性大幅提升。注意,声明顺序时一定要确认各切面相互之间不存在强依赖,否则需重构为单一职责或合并切面。
总结来说,调整事务与自定义切面的执行顺序是关键且常被忽略的实践。默认的事务最外层策略通常正确,但涉及事务内审计、数据一致性要求时,应主动压低自定义切面的优先级值;而需要最终执行或事务无关的操作应提升其 order 或采用更精准的事务同步机制。有了清晰的顺序控制,才能发挥 AOP 的最大价值而不引入隐形 Bug。