Spring AOP 通知执行顺序

候选人小孙在面试快手时,被问到这样一个问题:

"我在同一个切面里写了三个通知,@Before、@AfterReturning 和 @Around,这三个的执行顺序是什么?"

小孙说:"Around 最先,然后 Before,然后业务方法,然后 AfterReturning,最后 After。"

面试官追问:"那如果有两个 Around 通知呢?谁先谁后?"

小孙开始不确定了。

面试官继续:"我再加一个切面,两个切面的 Around 谁先执行?"

小孙彻底卡住。

【面试官心理】 通知执行顺序这道题,80% 的候选人只能答出"Around 包围"这个概念,但一到"多个 Around 的顺序"和"跨切面的顺序"就全军覆没。我要的不是背顺序,是理解 Spring 为什么要这么设计。

一、通知类型与基本执行顺序 🔴

1.1 Spring AOP 的五种通知

Spring AOP 定义了五种通知类型,它们在方法执行链路中的位置是固定的:

通知类型执行时机能否阻止原方法
@Around完全包围目标方法,可以在其前后执行✅ 可不调用 proceed() 阻止
@Before目标方法执行前❌ 无法阻止
@AfterReturning目标方法正常返回后❌ 无法阻止
@AfterThrowing目标方法抛出异常后❌ 无法阻止
@After目标方法执行后(无论正常还是异常)❌ 无法阻止,类似 finally

1.2 单切面、单通知的执行流程

graph TD
    A[代理方法入口] --> B[@Around - 前置部分]
    B --> C{@Before}
    C -->|无异常| D[目标方法执行]
    C -->|抛出异常| E[跳至 @AfterThrowing]
    D --> F[@AfterReturning - 正常返回]
    D -->|有异常| G[@AfterThrowing - 异常处理]
    G --> H[@After - 始终执行]
    F --> H
    H --> I[@Around - 后置部分]
    I --> J{是否调用 proceed}
    J -->|是| K[继续执行]
    J -->|否| L[直接返回]
    K --> D

同一切面内的 Around 只有一个,这是 Spring AOP 的设计约束——每个切面的每个切点上,同一种通知类型只会出现一次。

1.3 同一切面内多个通知的执行顺序

这是最容易出错的地方。

@Aspect
@Component
@Order(1) // 切面优先级,数字越小优先级越高
public class PerformanceAspect {

    // 第一个切点
    @Pointcut("execution(* com.xxx.UserService.save(..))")
    public void savePointcut() {}

    // 第二个切点
    @Pointcut("execution(* com.xxx.OrderService.*(..))")
    public void orderPointcut() {}

    @Around("savePointcut()")
    public Object aroundSave(ProceedingJoinPoint pjp) {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            System.out.println("save 耗时: " + (System.currentTimeMillis() - start));
        }
    }

    @Around("orderPointcut()")
    public Object aroundOrder(ProceedingJoinPoint pjp) {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            System.out.println("order 耗时: " + (System.currentTimeMillis() - start));
        }
    }
}

saveUser() 方法同时匹配两个 Around 时,谁先执行?

答案是:按照切面的 @Order 顺序,或者 Ordered 接口的返回值,从小到大依次执行

⚠️

90% 的候选人不知道这个坑:同一切面内的多个 Around 通知,如果匹配了同一个方法,它们的执行顺序不是按代码顺序,而是按切点定义的顺序。但更准确地说,这取决于 AspectJ 的切点匹配结果。

二、proceed() 的位置决定一切 🟡

Around 通知中最关键的是 proceed() 的位置——它是目标方法调用的分界线。

@Around("execution(* com.xxx.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    // A1. 前置逻辑(Before 的效果)

    // B. 这里调用 proceed(),目标方法才真正执行
    Object result = pjp.proceed();

    // C1. 后置逻辑(AfterReturning 的效果)
    return result;
}

但 Around 远比这个复杂——proceed() 可以被调用多次(虽然这在实际项目中非常罕见):

@Around("execution(* com.xxx.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("第1次调用前");

    // 第一次 proceed - 触发目标方法
    Object result = pjp.proceed();

    System.out.println("第1次调用后,第2次调用前");

    // 第二次 proceed - 目标方法再执行一次!
    Object result2 = pjp.proceed();

    System.out.println("第2次调用后");

    return result;
}

【面试官心理】 我问他 proceed() 能调用几次,其实是在看他有没有真正动手写过 Around 通知的源码逻辑。大部分候选人只知道"Around 可以控制是否执行目标方法",不知道 proceed() 本质上是一个可重入的方法调用链节点。

三、多个切面的执行顺序 🔴

这是面试官最喜欢的追问方向。

3.1 切面优先级:@Order vs Ordered

Spring 用两个机制控制切面优先级:

方式一:@Order 注解

@Aspect
@Component
@Order(1) // 数字越小,优先级越高
public class LoggingAspect { /* ... */ }

@Aspect
@Component
@Order(2)
public class TransactionAspect { /* ... */ }

方式二:实现 Ordered 接口

@Aspect
@Component
public class LoggingAspect implements Ordered {
    @Override
    public int getOrder() {
        return 1; // 返回值越小,优先级越高
    }
}

3.2 完整执行顺序图

当有多个切面时,执行顺序由优先级决定:

graph TD
    subgraph 切面1\_优先级1
        A1[@Around - 前置]
        A2[@Before]
        A3[目标方法]
        A4[@AfterReturning / @AfterThrowing]
        A5[@After]
        A6[@Around - 后置]
    end

    subgraph 切面2\_优先级2
        B1[@Around - 前置]
        B2[@Before]
        B3[切面1的Around proceed]
        B4[@AfterReturning / @AfterThrowing]
        B5[@After]
        B6[@Around - 后置]
    end

    A1 --> A2 --> A3 --> A4 --> A5 --> A6
    B1 --> B2 --> B3 --> B4 --> B5 --> B6

    style A1 fill:#ffcccc
    style B1 fill:#ccffcc
    style A3 fill:#ccccff
    style B3 fill:#ccffcc

核心规律

  • @Before 按优先级从小到大依次执行(优先级高的先织入)
  • @After@AfterReturning/@AfterThrowing 按优先级从大到小依次执行(优先级低的后清理)
  • Around 的前置部分等价于 Before,后置部分等价于 After
💡

记忆技巧:Before 升序执行(1, 2, 3...),After 降序执行(3, 2, 1...)。就像递归的入栈和出栈一样,优先级低的先入后出,优先级高的先入先出。

3.3 具体执行顺序示例

// 切面A - Order(1),优先级最高
@Aspect
@Component
@Order(1)
public class AspectA {
    @Before("execution(* com.xxx.*.*(..))")
    public void before() {
        System.out.println("AspectA @Before");
    }

    @After("execution(* com.xxx.*.*(..))")
    public void after() {
        System.out.println("AspectA @After");
    }
}

// 切面B - Order(2)
@Aspect
@Component
@Order(2)
public class AspectB {
    @Before("execution(* com.xxx.*.*(..))")
    public void before() {
        System.out.println("AspectB @Before");
    }

    @After("execution(* com.xxx.*.*(..))")
    public void after() {
        System.out.println("AspectB @After");
    }
}

实际输出顺序:

AspectA @Before       ← 优先级高,先执行前置
AspectB @Before       ← 优先级低,后执行前置
  → 目标方法执行 ←
AspectB @After        ← 优先级低,先执行后置(后进先出)
AspectA @After        ← 优先级高,后执行后置(后进先出)

四、❌ 错误示范

候选人原话一

"Around 通知就是比 Before 和 After 先执行,所以优先级最高。"

问题诊断

  • Around 并不是"最优先",它的前置部分在 Before 之前,后置部分在 After 之后
  • 这是对"包围"概念的误解,不是时间上的优先级,而是包裹关系

候选人原话二

"多个切面的执行顺序是按照代码中出现的顺序。"

问题诊断

  • Spring AOP 根本不管代码顺序,只看 @OrderOrdered
  • 如果没有显式指定顺序,结果是不确定的(由 Bean 注册顺序决定)

候选人原话三

"@After 和 @AfterReturning 是一样的,AfterReturning 执行完了 After 才执行。"

问题诊断

  • @After 相当于 try-finally 中的 finally,无论是否抛异常都执行
  • @AfterReturning 只有正常返回时才执行
  • 两者执行顺序是:@After@AfterReturning/@AfterThrowing 之后

【面试官心理】 这道题我一般这样追问:如果一个方法正常返回,@AfterReturning 和 @After 谁先执行?如果方法抛了异常呢?答对的占 30%。再追问两个切面的 Around 谁先执行,能答出"按 @Order 升序"的只有 15%。

五、生产避坑

场景:某系统上线后,数据库事务频繁失效。排查发现是切面顺序写反了。

@Aspect
@Component
@Order(1) // ⚠️ 错误:事务切面优先级太低
public class TransactionAspect {
    @Around("execution(* com.xxx..*.save*(..))")
    public Object around(ProceedingJoinPoint pjp) {
        return pjp.proceed(); // Around 在这里调用目标方法
    }
}

@Aspect
@Component
@Order(2) // 日志切面优先级反而更高
public class LoggingAspect {
    @Before("execution(* com.xxx..*.save*(..))")
    public void before() {
        log.info("开始保存..."); // ⚠️ 这个在事务开启之前就执行了
    }
}

问题在于:如果 Around 通知没有主动开启事务(比如漏写了 @Transactional),而日志切面的 @Before 在 Around 的 proceed() 之前执行,那么事务的开启时机就被日志切面夹在了中间,导致某些场景下事务边界错乱。

正确做法:事务切面应该设置最高优先级(Order 数字最小),确保事务的开启和提交在所有其他切面之前和之后。

@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // -2147483648,事务最高优先级
public class TransactionAspect { /* ... */ }

Spring 事务的默认 Order 是 Integer.MAX_VALUE(最低优先级),确保它在外层。所有自定义切面如果不指定 Order,默认也是 Integer.MAX_VALUE,按注册顺序执行。