目录
[1. 概念推导:切面是如何运转的?](#1. 概念推导:切面是如何运转的?)
[2. 流程推演:五种通知类型的执行轨迹](#2. 流程推演:五种通知类型的执行轨迹)
[流程推演 1:目标方法正常执行(/test/t1)](#流程推演 1:目标方法正常执行(/test/t1))
[流程推演 2:目标方法发生异常(/test/t2,代码为 int a = 10 / 0;)](#流程推演 2:目标方法发生异常(/test/t2,代码为 int a = 10 / 0;))
[3. 流程优化:重复表达式的痛与 @Pointcut](#3. 流程优化:重复表达式的痛与 @Pointcut)
[4. 流程优化:多切面时的执行顺序与 @Order](#4. 流程优化:多切面时的执行顺序与 @Order)

系列导航:上一篇跑通了第一个 AOP 程序,本篇拆解 AOP 的核心术语,并重点推演正常和异常两种流程下,五种通知类型的执行顺序。
1. 概念推导:切面是如何运转的?
AOP 的执行逻辑可以概括为三步:找准目标 → 施加增强 → 组成切面。
-
怎么找准目标?
我们需要制定一个规则,例如"拦截 controller 包下的所有方法"。这个规则叫切点(Pointcut)。
-
目标是哪些?
被规则命中的真实方法,例如
queryBookById,叫连接点(JoinPoint)。切点可以理解为保存了众多连接点的集合。 -
施加什么增强?
拦截到目标方法后要做的工作,例如统计耗时,叫通知(Advice)。
-
组合起来叫什么?
切点 + 通知 = 切面(Aspect)。切面既告诉程序"对谁做",也告诉程序"做什么"和"什么时候做"。
Spring AOP 核心概念关系图

2. 流程推演:五种通知类型的执行轨迹
Spring 提供了 5 种通知类型:
| 通知类型 | 含义 |
|---|---|
@Around |
环绕通知,目标方法前后都执行 |
@Before |
前置通知,目标方法前执行 |
@AfterReturning |
返回后通知,正常返回后执行,有异常不执行 |
@AfterThrowing |
异常后通知,目标方法抛异常后执行 |
@After |
后置通知,目标方法后执行,无论是否异常都执行,类似 finally |
为了看清执行顺序,我们写一个包含 5 种通知的切面 AspectDemo,拦截 TestController。
全套演示核心代码(AspectDemo.java)
java
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo {
// 公共切点:抽取表达式,所有通知复用
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
@Before("pt()")
public void doBefore(JoinPoint joinPoint){
log.info("@Before 前置通知执行");
}
@AfterReturning("pt()")
public void doAfterReturning(JoinPoint joinPoint){
log.info("@AfterReturning 返回通知执行");
}
@AfterThrowing("pt()")
public void doAfterThrowing(JoinPoint joinPoint){
log.info("@AfterThrowing 异常通知执行");
}
@After("pt()")
public void doAfter(JoinPoint joinPoint){
log.info("@After 后置通知执行");
}
@Around("pt()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
log.info("@Around 环绕【前置】逻辑");
Object result = null;
try {
// 执行目标方法
result = pjp.proceed();
} catch (Throwable throwable) {
throw throwable;
}
log.info("@Around 环绕【后置】逻辑");
return result;
}
}
测试 Controller 代码 TestController.java
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
// 正常执行接口
@GetMapping("/t1")
public String t1(){
log.info("===== 目标方法 t1 正在执行 =====");
return "success";
}
// 会抛出异常的接口
@GetMapping("/t2")
public String t2(){
log.info("===== 目标方法 t2 正在执行 =====");
int a = 10 / 0;
return "success";
}
}
流程推演 1:目标方法正常执行(/test/t1)
-
请求进入,首先触发
@Around的前置逻辑。 -
接着触发
@Before。 -
执行目标方法
t1()。 -
目标方法正常返回,触发
@AfterReturning。 -
无论是否异常都要收尾,触发
@After。 -
最后回到
@Around,执行后置逻辑。
正常运行时,@AfterThrowing 不会执行。
正常执行时的通知顺序

流程推演 2:目标方法发生异常(/test/t2,代码为 int a = 10 / 0;)
-
请求进入,触发
@Around的前置逻辑。 -
接着触发
@Before。 -
执行目标方法
t2(),抛出ArithmeticException。 -
异常被捕获,触发
@AfterThrowing。 -
依然要执行收尾工作,触发
@After。 -
因为
pjp.proceed()抛出异常,@Around的后置逻辑不再执行,@AfterReturning也不会执行。
异常执行时的通知顺序 
核心注意事项
-
@Around的返回值必须是Object,因为它要接收pjp.proceed()的返回值;否则目标方法即使有返回值,也无法传给调用方。 -
除
@Around外,其他四个通知都只是"旁观者",不需要在参数中写ProceedingJoinPoint,它们无需控制目标方法执行。 -
@Around需要调用proceed()让原始方法执行;其他通知不需要考虑目标方法执行。
3. 流程优化:重复表达式的痛与 @Pointcut
痛点 :上面的 5 个通知,每个注解里都写了一长串 execution(* com.example.demo.controller.*.*(..))。如果哪天要改拦截规则,得改 5 次。
方案 :用 @Pointcut 提取公共切点表达式。
java
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){} // 定义切点
@Before("pt()") // 引用切点
public void doBefore() { ... }
流程变化:Spring 解析时,会先找到 pt() 这个空方法,拿到里面的表达式去匹配,再把匹配结果应用到 doBefore 等通知上。
注意:
@Pointcut方法使用private时,仅当前切面类可用;其他切面类要引用时,需要改为public,引用方式为全限定类名.方法名()。
4. 流程优化:多切面时的执行顺序与 @Order
痛点 :如果 AspectDemo2、AspectDemo3 都拦截了同一个方法,谁先执行?
默认规则 :Spring 默认按切面类名的字母排序。@Before 字母靠前的先执行,@After 字母靠前的后执行。
问题:类名不可控,逻辑容易混乱。
方案 :使用 @Order(数字)。数字越小,优先级越高。
-
@Before阶段:优先级高的切面先执行。 -
@After阶段:优先级高的切面后执行,因为它被包在最外层。
这就像穿衣服和脱衣服:先穿的最晚脱。
@Order 的洋葱模型
