人人都会AI编程

5.4 通知调用链与执行顺序原理

更新时间:2026-07-10

当多个切面同时作用于同一个连接点时,Spring AOP 会将它们组织成一个有序的通知调用链。开发者需要理解这个调用链的构建规则和执行流程,才能避免因顺序混乱导致的隐形 Bug,并合理利用顺序实现业务需求(如先执行权限校验再开启事务)。

5.4.1 单一切面的通知执行顺序

在一个切面类内部,不同类型的通知会按照固定的生命周期顺序执行。假设有如下切面:

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void beforeAdvice() {
        System.out.println("Before");
    }

    @After("execution(* com.example.service.*.*(..))")
    public void afterAdvice() {
        System.out.println("After");
    }

    @AfterReturning("execution(* com.example.service.*.*(..))")
    public void afterReturningAdvice() {
        System.out.println("AfterReturning");
    }

    @AfterThrowing("execution(* com.example.service.*.*(..))")
    public void afterThrowingAdvice() {
        System.out.println("AfterThrowing");
    }

    @Around("execution(* com.example.service.*.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Around before");
        Object result = joinPoint.proceed();
        System.out.println("Around after");
        return result;
    }
}

当被拦截的服务方法正常执行(无异常)时,输出顺序为:

Around before
Before
(目标方法执行)
AfterReturning
After
Around after

若目标方法抛出异常,则顺序变为:

Around before
Before
(目标方法抛出异常)
AfterThrowing
After
(异常向上传播,Around 不再执行 after 部分)

可以看到规则:

  • 环绕通知包裹在最外层,proceed() 之前的部分最先执行,之后的部分最后执行。
  • 前置通知在进入目标方法之前执行。
  • 返回通知(或异常通知,二者互斥)在目标方法执行后触发。
  • 后置通知 @After 总是执行,行为类似 finally 块。

5.4.2 多个切面的执行顺序

当多个切面作用于同一个目标方法时,它们的执行顺序需要通过 切面优先级 来控制。Spring 遵循以下规则:

  • 默认顺序:多个切面之间没有明确的先后关系,执行顺序取决于切面类的全限定名或 Spring 容器加载顺序,这是不可靠的,不应依赖。
  • 显式指定顺序:让切面类实现 org.springframework.core.Ordered 接口,或使用 @Order 注解指定整数值。数值越小,优先级越高,前置通知越先执行,但返回/后置通知越后执行(类似同心圆结构)。

例如:

@Order(1)
@Aspect
@Component
public class SecurityAspect {
    @Before("...")
    public void checkAuth() { 
        System.out.println("Security check");
    }
}

@Order(2)
@Aspect
@Component
public class TransactionAspect {
    @Around("...")
    public Object manageTx(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Transaction begin");
        Object result = joinPoint.proceed();
        System.out.println("Transaction commit");
        return result;
    }
}

当它们同时拦截一个服务方法时,执行顺序为:

Security check      ← 优先级高(1)的前置通知先执行
Transaction begin   ← 优先级低(2)的环绕通知前逻辑
(目标方法执行)
Transaction commit  ← 低优先级后逻辑
(Security 无后置逻辑,但如果有 @After 或 @AfterReturning,会在此之后执行)

规律总结:多个切面的执行模型可以理解为同心圆嵌套——优先级最高的切面在最外层,优先级最低的在最内层。前置通知按优先级从高到低(小→大)执行,后置/返回/环绕的后半部分按优先级从低到高(大→小)执行。

5.4.3 实际场景:控制事务与锁的顺序

在真实项目中,通常会为同一连接点配置事务切面和自定义切面(如分布式锁、权限校验)。如果顺序不对,可能出现“先提交事务再释放锁”导致数据不一致,或者“先校验权限再开启事务”以避免事务占用时间过长。

事务先执行,锁后执行为例(保证方法内先获取锁,事务最后提交):

@Order(1) // 外层:事务
@Aspect
@Component
public class TransactionalAspect {
    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object handleTx(ProceedingJoinPoint joinPoint) throws Throwable {
        // 事务开始逻辑(实际 Spring 事务更复杂,这里仅示意)
        System.out.println("TX begin");
        Object result = joinPoint.proceed();
        System.out.println("TX commit");
        return result;
    }
}

@Order(2) // 内层:锁
@Aspect
@Component
public class LockAspect {
    @Around("@annotation(com.example.Locked)")
    public Object handleLock(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Lock acquired");
        Object result = joinPoint.proceed();
        System.out.println("Lock released");
        return result;
    }
}

执行时序:TX begin → Lock acquired → 业务方法 → Lock released → TX commit。事务获取在最外层,确保锁在事务内释放,避免并发问题。

5.4.4 环绕通知与连接点的传递

在调用链中,每个环绕通知的 ProceedingJoinPoint 对象的 proceed() 方法实际上是调用链中的下一个节点。如果某个环绕通知忘记调用 proceed(),整个链就会中断,目标方法不会被执行,后续通知也不会触发(除非有意而为,比如权限校验失败主动阻止调用)。

因此,使用环绕通知时必须谨慎处理异常:

@Around("...")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
    try {
        // 前置增强
        Object result = joinPoint.proceed();
        // 返回增强
        return result;
    } catch (Throwable e) {
        // 异常增强
        throw new RuntimeException(e);
    } finally {
        // 类似 @After 的逻辑
    }
}

这里的 catch 块若吞掉了异常而不重新抛出,则后续的异常通知和事务回滚可能被跳过,需要特别注意。

5.4.5 通过日志验证执行顺序

为了在实际开发中验证切面的执行顺序,可以临时为多个切面添加日志,利用线程 ID 和时间戳观察输出。一个更系统的做法是引入 org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator 相关的 TRACE 日志,或利用 AOP 拦截器链的调试工具。

但更简单的做法永远是编写一个集成测试,固定 @Order 后断言日志输出的先后次序,将顺序规则固化为测试用例,防止未来重构时误调优先级。

5.4.6 小结

掌握通知调用链与执行顺序需重点记住:

  1. 单切面内@Around 包裹全部,@Before 最先,然后是目标方法,再是 @AfterReturning/@AfterThrowing,最后是 @After(类似 finally)。
  2. 多切面间:通过 @OrderOrdered 接口决定嵌套层级,小数值在外层,大数值在内层。
  3. 实际应用:按业务需求精心编排切面顺序,并用测试锁定该约定。

理解了这套调用链机制,当排查“为什么事务没有回滚”或“为什么权限检查跑到日志打印之后”这类问题时,就能有清晰的排查方向——检查 @Order 的配置,跟踪环绕通知中的 proceed() 调用点,一切都会变得直观明确。