一文拆解Spring AOP 核心概念与通知类型
系列导航:上一篇跑通了第一个 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)
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
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 提取公共切点表达式。
@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 的洋葱模型

到此这篇关于一文拆解Spring AOP 核心概念与通知类型的文章就介绍到这了,更多相关Spring AOP概念内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
关于Java中的实体类要 implements Serializable的原因分析
这篇文章主要介绍了Java中的实体类为什么要 implements Serializable,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2023-06-06


最新评论