人人都会AI编程

9.3 多切面优先级控制与执行顺序

更新时间:2026-07-11

当一个目标方法被多个切面同时拦截时,它们的执行顺序就不再是“先声明、先执行”那么简单直观。实际项目中经常会出现这样的场景:一个业务方法需要同时经过事务管理切面权限校验切面操作日志切面甚至自定义幂等性切面。如果它们的执行顺序混乱,轻则造成日志记录错位,重则导致事务提前提交或回滚异常。因此,清楚地控制切面的优先级,是确保横切逻辑按预期协作的关键。

9.3.1 为什么顺序至关重要

考虑一个典型的企业服务方法:

@Transactional
@RequiresPermission("order:update")
@AuditLog(operation = "更新订单")
public void updateOrder(Order order) {
    orderDao.update(order);
}

这里涉及三个切面:事务切面权限切面日志切面。如果它们的执行顺序是随机的,可能会出现以下问题:

  • 日志切面在事务切面之前执行,方法还未提交,日志就已记录下来。如果后续发生回滚,日志中会留下一条“成功更新”的记录,与实际不符。
  • 权限切面在事务之外执行,即使权限检查失败抛出异常,事务切面的开启和回滚也已经发生,造成不必要的资源开销。
  • 日志切面内部如果调用了其他需要事务的方法,可能因为事务未开启而执行在无事务的上下文中,导致数据不一致。

因此,规范的做法是让 事务切面在最内层(靠近业务方法),权限切面在中间,日志或其他审计切面在最外层。这样能确保权限校验不消耗事务资源,日志记录在事务上下文内但能感知最终结果。

9.3.2 Spring AOP 中的默认顺序

Spring AOP 在管理多个切面时,遵循以下基本原则:

  • 同一连接点的多个通知,按照切面优先级排序执行。高优先级的切面,其前置通知先执行,后置通知后执行。
  • 环绕通知:因为它包裹了目标方法,高优先级的环绕通知会“先入栈”,即它的前置部分最先执行,后置部分最后执行。
  • 同一个切面内部的不同通知,执行顺序是固定的:@Around@Before目标方法@AfterReturning / @AfterThrowing@After@Around 的后置部分。

当存在多个切面类时,Spring 需要一种机制来确定切面之间的先后顺序。

9.3.3 控制优先级的方式

Spring 提供了两种显式控制切面执行顺序的方式:实现 Ordered 接口使用 @Order 注解。它们实际上都对应同一个底层排序规则:值越小,优先级越高(越先执行)。

1. 使用 @Order 注解

这是最直接、最常用的方法。在切面类上标注 @Order(n),数字 n 可以是任意整数,包括负数。Spring 会按照数值从小到大的顺序排列切面。

@Aspect
@Component
@Order(1)
public class TransactionAspect {
    // 事务管理逻辑
}

@Aspect
@Component
@Order(2)
public class PermissionAspect {
    // 权限校验逻辑
}

@Aspect
@Component
@Order(3)
public class AuditLogAspect {
    // 审计日志逻辑
}

在这个配置下,执行顺序为:

  1. AuditLogAspect(优先级最高,Order=1)的前置通知最先执行。
  2. PermissionAspect(Order=2)的前置通知接着执行。
  3. TransactionAspect(Order=3)的前置通知最后执行。
  4. 目标方法执行。
  5. 返回时,TransactionAspect 的后置通知最先执行,然后是 PermissionAspect,最后是 AuditLogAspect

简单记忆:小数字 = 高优先级 = 最外层切入、最后收回。这和同心圆的模型一致:数字小代表最外层,包裹数字大的内层。

2. 实现 Ordered 接口

如果切面类是由 Java Config 或手工注册的 Bean,也可以通过实现 org.springframework.core.Ordered 接口,并重写 getOrder() 方法来指定优先级。效果与 @Order 完全相同。

@Component
public class SecurityAspect implements Ordered {
    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 100; // 使用常量控制
    }
    // 切面逻辑...
}

当同时使用 @Order 注解和实现 Ordered 接口时,Spring 会优先采用 Ordered 接口的返回值。不过在实际开发中,为了避免混淆,建议统一使用 @Order,使意图更显式。

9.3.4 真实场景的优先级配置建议

根据横切关注点的职责不同,可以参照以下顺序进行切面优先级规划(数值越小越外层):

  • 异常监控与链路追踪切面:最外层,@Order(1)。用于开启 Tracing span、记录异常通知等,确保能捕捉到所有内部切面和业务代码抛出的异常。
  • 接口幂等性切面@Order(10)。在方法执行前就检查 Redis token,避免不必要的权限、事务开销。
  • 操作审计与日志记录@Order(20)。记录完整的请求参数和响应,包括方法执行耗时。
  • 权限与安全校验@Order(30)。确保有权限时才继续进行事务操作。
  • 缓存切面@Order(40)。先检查缓存,命中则直接返回,避免进入事务。
  • 事务管理切面@Order(50)(或其他偏大的数字)。事务切面应当是最内层的横切逻辑,只在业务逻辑真正需要时打开事务。

注意,Spring 内部的事务管理、缓存等切面的优先级有自己的默认值(如 @Transactional 的切面由 TransactionInterceptor 实现,其 Order 为 Ordered.LOWEST_PRECEDENCE,即 Integer.MAX_VALUE)。如果你自定义了事务相关切面,需要确保它的优先级数值大于业务切面,这样才能包裹在内部。

9.3.5 验证执行顺序

排查切面顺序问题最有效的手段是日志记录。在每个切面的前后通知中打印切入点和当前时间戳,就能直观地看到调用链。

@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("[一级切面] 前置执行: " + pjp.getSignature());
    Object result = pjp.proceed();
    System.out.println("[一级切面] 后置执行: " + pjp.getSignature());
    return result;
}

同时运行多个不同 Order 的切面,观察控制台输出,即可确认实际顺序是否符合预期。在单元测试中,也可以利用 @SpringBootTest 和 AOP 代理,编写断言检查切面调用时序。

9.3.6 注意事项

  • 仅对代理有效:优先级控制只作用于通过 Spring AOP 代理调用的方法。如果同一个类内部自调用,AOP 不会拦截,多个切面也不会生效。此时需借助 ApplicationContext 获取代理实例或使用 AspectJ 编译时织入。
  • 环绕通知破坏顺序的风险:如果高优先级的环绕通知吞掉了异常,低优先级的切面的异常通知将不会执行。这在设计时要格外谨慎,尤其是事务切面,务必保证异常向上传播。
  • 同一通知类型的重复声明:如果在一个切面类中定义了多个相同类型的通知(如多个 @Before),它们按照在类中的声明顺序执行,但这不是官方保证的行为,不应依赖。明确区分建议将它们拆分到不同切面类中,用 @Order 控制。

掌握多切面的优先级控制后,你便能将事务、安全、日志、缓存这些横切逻辑妥善编排,构建出行为确定、可维护性高的企业应用切面体系。