通知执行顺序

候选人小周在面试滴滴 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 提供五种通知类型:

通知类型执行时机特点
@Before目标方法执行之前无法阻止执行,除非抛异常
@After目标方法执行之后(无论正常还是异常)相当于 finally
@AfterReturning目标方法正常返回后只在正常返回时执行
@AfterThrowing目标方法抛出异常后只在异常抛出时执行
@Around包裹目标方法可以阻止、修改参数、修改返回值
@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
@Aroundcatch 块)

完整顺序图示:

┌─────────────────────────────────────────────────────────────┐
│                     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 框架内部注册了很多切面,它们的优先级是固定的:

切面Order 值说明
SaveMethodValidationInterceptorOrdered.HIGHEST_PRECEDENCE + 500Hibernate 验证
PersistenceExceptionTranslationInterceptorOrdered.LOWEST_PRECEDENCEJPA 异常翻译
TransactionInterceptorOrdered.LOWEST_PRECEDENCESpring 事务
CachingInterceptorOrdered.LOWEST_PRECEDENCE + 10Spring Cache

如果要让自己定义的切面在事务之前执行:

@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 通知类型的选择

场景推荐通知原因
权限校验@Around可以阻止方法执行
记录日志@Before + @AfterReturning + @AfterThrowing覆盖所有分支
性能监控@Around可以计算方法执行时间
事务管理@Around需要在异常时回滚
缓存@Around + @After需要在返回前检查缓存
简单日志@Before只关心入口,不关心出口

4.2 切面设计原则

  1. 单一职责:一个切面只做一件事
  2. 切点精确:不要用过于宽泛的切点
  3. 顺序清晰:多个切面时要明确顺序
  4. 异常处理:Around 通知要正确处理异常

五、面试总结

通知执行顺序这道题,考的是对 AOP 执行模型的完整理解。

P5 候选人能说出 Before、After、Around 的基本区别。 P6 候选人能说清 AfterReturning 和 AfterThrowing 的区别,能说清 Around 中 proceed() 的作用,能说清多切面的顺序由 @Order 决定。 P7 候选人能画完整的执行顺序图,能解释 Around 不调用 proceed() 的后果,能说出 Spring 内部切面的优先级。

记住,AOP 通知不是背顺序,而是理解"洋葱模型"——外层先进入,内层先执行。