Spring AOP 通知执行顺序
候选人小孙在面试快手时,被问到这样一个问题:
"我在同一个切面里写了三个通知,@Before、@AfterReturning 和 @Around,这三个的执行顺序是什么?"
小孙说:"Around 最先,然后 Before,然后业务方法,然后 AfterReturning,最后 After。"
面试官追问:"那如果有两个 Around 通知呢?谁先谁后?"
小孙开始不确定了。
面试官继续:"我再加一个切面,两个切面的 Around 谁先执行?"
小孙彻底卡住。
【面试官心理】
通知执行顺序这道题,80% 的候选人只能答出"Around 包围"这个概念,但一到"多个 Around 的顺序"和"跨切面的顺序"就全军覆没。我要的不是背顺序,是理解 Spring 为什么要这么设计。
一、通知类型与基本执行顺序 🔴
1.1 Spring AOP 的五种通知
Spring AOP 定义了五种通知类型,它们在方法执行链路中的位置是固定的:
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 根本不管代码顺序,只看
@Order 或 Ordered
- 如果没有显式指定顺序,结果是不确定的(由 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,按注册顺序执行。