通知执行顺序
候选人小周在面试滴滴 P6 时,面试官问道:
"Spring AOP 的通知执行顺序是什么?"
小周说:"Before 在方法之前,After 在方法之后..."
面试官追问:"那 Around 呢?AfterReturning 和 AfterThrowing 什么时候执行?"
小周说:"方法返回后...还是结束后?"
面试官:"如果有多个切面,顺序怎么定?"
小周答不出来了。
【面试官心理】
这道题我用来测试候选人对 AOP 通知执行模型的完整理解。知道 Before/After 的占 60%,能说清 Around 位置的占 40%,能说清多切面顺序的占 20%。能全部答清楚的,基本都动手写过 AOP 代码。
一、核心问题 🔴
1.1 问题拆解
第一层:单个通知
- "Spring AOP 有哪几种通知类型?"
- "Before、After、AfterReturning、AfterThrowing 的区别是什么?"
- "Around 通知和普通通知有什么关系?"
第二层:执行顺序
- "同一个切面中,多个通知的执行顺序是什么?"
- "Before 通知中抛出异常,后续通知还会执行吗?"
- "Around 通知中不调用 proceed() 会怎样?"
第三层:多切面顺序
- "多个切面的执行顺序怎么定?"
- "@Order 和 Ordered 接口是什么关系?"
- "Spring 内部的切面(事务、缓存)和用户自定义切面的顺序是什么?"
1.2 ❌ 错误示范
候选人原话 A:"Before 在方法前执行,After 在方法后执行,很简单。"
问题诊断:
- 不知道有 AfterReturning 和 AfterThrowing
- 不理解 Around 通知的作用
- 不知道正常返回和异常返回的区别
候选人原话 B:"AfterReturning 和 After 都是方法执行完后执行,没有区别。"
问题诊断:
- 完全混淆了两个通知
- AfterReturning 只在正常返回时执行
- AfterThrowing 只在异常抛出时执行
- After 在两种情况下都会执行
候选人原话 C:"多个切面按切面类的字母顺序执行。"
问题诊断:
- 完全错误
- 顺序由 @Order 或 Ordered 接口决定
- 和字母顺序无关
1.3 标准回答
P5 回答:五种通知类型
Spring AOP 提供五种通知类型:
@Aspect
@Component
public class DemoAspect {
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
System.out.println("【Before】方法执行前");
}
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println("【After】方法执行后(无论成功还是异常)");
}
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("【AfterReturning】方法正常返回,返回值:" + result);
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
System.out.println("【AfterThrowing】方法抛出异常:" + e.getMessage());
}
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("【Around-Before】");
try {
Object result = pjp.proceed(); // 执行目标方法
System.out.println("【Around-AfterReturning】返回值:" + result);
return result;
} catch (Exception e) {
System.out.println("【Around-AfterThrowing】异常:" + e.getMessage());
throw e;
} finally {
System.out.println("【Around-Finally】");
}
}
}
1.4 追问升级
追问 1:通知执行顺序详解
单个通知的执行顺序(Around 包裹普通通知时):
// 假设一个方法被 @Around 和 @Before 同时标注
@Around("execution(* save(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("1. Around - before proceed");
Object result = pjp.proceed(); // 调用目标方法
// 这里才是"方法执行"的位置
System.out.println("4. Around - after proceed");
return result;
}
@Before("execution(* save(..))")
public void before() {
System.out.println("2. Before"); // 在 proceed() 之前执行
}
@AfterReturning("execution(* save(..))")
public void afterReturning() {
System.out.println("3. AfterReturning"); // 在 proceed() 之后执行
}
执行顺序:
1. Around 的前置逻辑
2. Before
3. 目标方法执行
4. AfterReturning(正常返回)
或者 AfterThrowing(抛出异常)
5. Around 的后置逻辑
如果 Around 不调用 proceed():
@Around("execution(* save(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("前置逻辑");
// ❌ 不调用 proceed(),目标方法永远不会被执行!
// return pjp.proceed(); // 被注释掉了
System.out.println("后置逻辑");
return "fake result"; // 直接返回假的结果
}
⚠️
Around 通知如果不调用 proceed(),目标方法就不会被执行!这是一种"短路"机制,常用于权限校验:不满足条件时直接返回,不继续执行。
@Around("execution(* com.example.service.*.*(..))")
public Object checkPermission(ProceedingJoinPoint pjp) throws Throwable {
if (!hasPermission()) {
throw new SecurityException("没有权限");
}
return pjp.proceed(); // 有权限才执行
}
追问 2:正常返回 vs 异常抛出的执行顺序
// 正常返回时的执行顺序
@Around
@Before
// 目标方法执行
@AfterReturning
@After
@Around(proceed 后)
// 异常抛出时的执行顺序
@Around
@Before
// 目标方法执行,抛出异常
@AfterThrowing
@After
@Around(catch 块)
完整顺序图示:
┌─────────────────────────────────────────────────────────────┐
│ Around 通知开始 │
│ try { │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Before 通知 │ │
│ │ 目标方法执行 │ │
│ │ AfterReturning 通知(正常返回时) │ │
│ │ 或 AfterThrowing 通知(异常时) │ │
│ │ After 通知(无论哪种情况) │ │
│ └─────────────────────────────────────────────────┘ │
│ } catch (Exception e) { │
│ // Around 通知的 catch 块 │
│ } finally { │
│ // Around 通知的 finally 块 │
│ } │
└─────────────────────────────────────────────────────────────┘
追问 3:多切面的执行顺序
这是面试中的高频追问:
// 切面1:@Order(1)
@Aspect
@Component
@Order(1)
public class TransactionAspect {
@Around("execution(* com.example..*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[事务] 开启事务");
try {
Object result = pjp.proceed();
System.out.println("[事务] 提交事务");
return result;
} catch (Exception e) {
System.out.println("[事务] 回滚事务");
throw e;
}
}
}
// 切面2:@Order(2)
@Aspect
@Component
@Order(2)
public class LoggingAspect {
@Around("execution(* com.example..*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[日志] 方法开始: " + pjp.getSignature());
try {
Object result = pjp.proceed();
System.out.println("[日志] 方法结束");
return result;
} catch (Exception e) {
System.out.println("[日志] 方法异常");
throw e;
}
}
}
执行顺序:
方法调用
└─ [TransactionAspect] @Around - 开启事务 ← Order(1),最外层
└─ [LoggingAspect] @Around - 方法开始 ← Order(2),第二层
└─ [Before] @Before ← 如果有
└─ 目标方法执行
└─ [AfterReturning] @AfterReturning
└─ [After] @After
└─ [LoggingAspect] @Around - 方法结束 返回
└─ [TransactionAspect] @Around - 提交事务 返回
@Order 和 Ordered 的关系:
// @Order 是 Ordered 接口的注解形式
@Retention(RetentionPolicy.RUNTIME)
public @interface Order {
int value() default Ordered.LOWEST_PRECEDENCE;
}
// Ordered 接口定义
public interface Ordered {
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; // 最高优先级
int LOWEST_PRECEDENCE = Integer.MAX_VALUE; // 最低优先级
int getOrder();
}
// @Order(1) 等价于实现 Ordered 接口并返回 1
// 数字越小,优先级越高
追问 4:Spring 内部切面的优先级
Spring 框架内部注册了很多切面,它们的优先级是固定的:
如果要让自己定义的切面在事务之前执行:
@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 1) // 比事务更早执行
public class MyAspect {
// ...
}
二、延伸问题 🟡
2.1 JoinPoint 和 ProceedingJoinPoint
// @Before、@After、@AfterReturning、@AfterThrowing 可以获取 JoinPoint
@Before("execution(* save(..))")
public void before(JoinPoint joinPoint) {
// JoinPoint 提供的 API
String name = joinPoint.getSignature().getName(); // 方法名
Object[] args = joinPoint.getArgs(); // 参数
Object target = joinPoint.getTarget(); // 目标对象
Object this_ = joinPoint.getThis(); // 代理对象
}
// @Around 可以获取 ProceedingJoinPoint(JoinPoint 的子类)
@Around("execution(* save(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// ProceedingJoinPoint 额外提供的方法
Object result = pjp.proceed(); // 执行目标方法
Object result = pjp.proceed(args); // 用修改后的参数执行
return result; // 可以修改返回值
}
2.2 通知中的参数传递
// 使用 @Pointcut 定义切点,可以复用
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() { }
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() { }
// 在通知中组合切点
@Before("serviceLayer() && userServiceMethods()")
public void before() {
// 同时匹配 UserService 中的方法
}
// 传递参数给通知
@Before("execution(* save(Long, ..)) && args(id, ..)")
public void beforeWithId(Long id) {
System.out.println("save 方法的参数 id = " + id);
}
2.3 切点表达式详解
// execution 表达式
execution([修饰符] 返回类型 [类名.]方法名(参数) [异常])
// 示例
execution(public void com.example.service.OrderService.createOrder(Long))
execution(* com.example.service.*.*(..)) // com.example.service 下所有类的所有方法
execution(* com.example.service..*.*(..)) // 包括子包
execution(* *..service.*.*(..)) // 所有包下的 service 类
execution(* *.UserService+.*(..)) // UserService 及其子类
execution(* save(Long, ..)) // 第一个参数是 Long,后续任意
execution(* save(.., Long)) // 最后一个参数是 Long
// 其他切点指示符
within(com.example.service.*) // 匹配某个包下所有类
this(com.example.service.UserService) // 代理对象是 UserService 类型
target(com.example.service.UserService) // 目标对象是 UserService 类型
args(Long, String) // 参数类型匹配
@target(org.springframework.stereotype.Service) // 类上有这个注解
@within(org.springframework.stereotype.Service) // 在 @Service 标注的类中
@annotation(org.springframework.transaction.Transactional) // 方法有这个注解
@args(com.example.NotNull) // 参数类型有这个注解
三、生产避坑
3.1 Around 和其他通知混用的坑
Around 和 Before/After 混用时,执行顺序可能和预期不同:
@Aspect
@Component
public class BadAspect {
@Around("execution(* save(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("1. Around before");
Object result = pjp.proceed();
System.out.println("4. Around after");
return result;
}
@Before("execution(* save(..))")
public void before() {
System.out.println("2. Before");
}
@After("execution(* save(..))")
public void after() {
System.out.println("3. After");
}
}
执行结果:1 → 2 → 目标方法 → 3 → 4
如果 Around 不调用 proceed(),Before 仍然会执行(因为 Before 是在 Around 的 proceed() 调用链中执行的),但目标方法和 After 不会执行。
3.2 AfterThrowing 不阻止异常传播
@AfterThrowing("execution(* save(..))")
public void afterThrowing(Exception e) {
System.out.println("捕获到异常,但异常仍会传播!");
// ❌ 这里无法阻止异常传播
// 只能在异常传播前做一些处理(如日志)
}
// 如果想"吞掉"异常,使用 Around:
@Around("execution(* save(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
try {
return pjp.proceed();
} catch (Exception e) {
System.out.println("捕获并处理异常");
return null; // 返回 null,不抛出异常
}
}
3.3 多切面对同一方法生效时的顺序问题
// 如果两个切面都切同一个方法,且逻辑相互依赖
// 顺序不同可能导致不同的结果
@Aspect @Order(1)
public class AspectA {
@Around("execution(* compute(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("A 开始");
Object result = pjp.proceed();
System.out.println("A 结束");
return result;
}
}
@Aspect @Order(2)
public class AspectB {
@Around("execution(* compute(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("B 开始");
Object result = pjp.proceed();
System.out.println("B 结束");
return result;
}
}
执行结果:A 开始 → B 开始 → 目标方法 → B 结束 → A 结束
如果 AspectA 在 AspectB 开始之后执行某些逻辑(比如修改共享状态),结果会不同。
四、工程选型
4.1 通知类型的选择
4.2 切面设计原则
- 单一职责:一个切面只做一件事
- 切点精确:不要用过于宽泛的切点
- 顺序清晰:多个切面时要明确顺序
- 异常处理:Around 通知要正确处理异常
五、面试总结
通知执行顺序这道题,考的是对 AOP 执行模型的完整理解。
P5 候选人能说出 Before、After、Around 的基本区别。
P6 候选人能说清 AfterReturning 和 AfterThrowing 的区别,能说清 Around 中 proceed() 的作用,能说清多切面的顺序由 @Order 决定。
P7 候选人能画完整的执行顺序图,能解释 Around 不调用 proceed() 的后果,能说出 Spring 内部切面的优先级。
记住,AOP 通知不是背顺序,而是理解"洋葱模型"——外层先进入,内层先执行。