Spring AOP 进阶:切入点表达式、5种通知类型全解析(含环绕通知避坑指南)
一、AOP 切入点表达式详解
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void ptx(){}
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
本节我们系统学习切入点表达式的语法格式、通配符、书写技巧,彻底搞懂如何精准匹配需要增强的方法。
1.1 核心概念
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
我们以 update() 方法为例,有两种描述方式:
// 方式一:接口形式(推荐)
execution(void com.itheima.dao.BookDao.update())
// 方式二:实现类形式
execution(void com.itheima.dao.impl.BookDaoImpl.update())
两种描述方式的区别
| 描述方式 | 匹配逻辑 | 适用场景 |
|---|---|---|
| 接口形式 | 匹配所有实现了该接口、方法签名完全符合的实现类方法 | 业务层通用增强,覆盖所有实现类 |
| 实现类形式 | 仅精准匹配该具体实现类的方法,其他实现类不匹配 | 仅针对特定实现类做增强 |
原理:Spring AOP 基于动态代理(JDK 动态代理优先走接口),通过接口定义来识别方法,只要实现类遵循接口契约,就能被匹配。
1.2 语法格式
切入点表达式标准格式:
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
我们通过一个例子拆解:
execution(public User com.itheima.service.UserService.findById(int))
execution:动作关键字,描述切入点的行为动作public:访问修饰符(可省略)User:返回值类型com.itheima.service:包名,多级使用点连接UserService:类/接口名称findById:方法名int:参数类型,多个类型用逗号隔开- 异常名:方法定义中抛出指定异常(可省略)
1.3 通配符使用
通配符用于简化切入点表达式,常用3种:
| 通配符 | 作用 | 示例 |
|---|---|---|
* |
单个独立的任意符号,可作为前缀/后缀匹配 | execution(public * com.itheima.*.UserService.find*(..)) |
.. |
多个连续的任意符号,常用于简化包名与参数 | execution(public User com..UserService.findById(..)) |
+ |
专用于匹配子类类型(使用频率低) | execution(* *..*Service+.*(..)) |
案例拆解(基于入门案例的 BookDao)
// 1. 匹配接口 update() 方法(推荐)
execution(void com.itheima.dao.BookDao.update())
// 2. 匹配实现类 update() 方法
execution(void com.itheima.dao.impl.BookDaoImpl.update())
// 3. 匹配实现类所有方法
execution(* com.itheima.dao.impl.BookDaoImpl.*(..))
// 4. 匹配 com 包下任意层级的 update() 方法
execution(void com..*.update())
// 5. 匹配项目中所有以 e 结尾的方法
execution(* *..*.*e(..))
// 6. 匹配业务层所有以 find 开头的方法
execution(* com.itheima.*.*Service.find*(..))
1.4 书写技巧(最佳实践)
- 描述切入点优先描述接口,不描述实现类
- 访问控制修饰符可省略
- 返回值类型查询类使用
*通配 - 包名尽量不使用
..匹配,效率过低 - 接口名/类名可使用
*Service通配 - 方法名书写以动词精准匹配,名词用
* - 参数根据业务灵活调整
- 通常不使用异常作为匹配规则
二、AOP 5种通知类型全解析
Spring AOP 共提供5种通知类型,对应方法执行的不同阶段:
2.1 通知执行顺序与核心定义
前置通知 (@Before) → 原方法执行前
原方法执行
后置通知 (@After) → 原方法 return 之后 / 抛异常之后 一定会执行
返回后通知 (@AfterReturning) → 原方法正常 return 之后 才执行
🔥 重点必须牢记:
@After、@AfterReturning 都是在 return 之后执行 ,不是在 return 之前!
一句话:return 先执行,通知后执行。
2.2 通知类型总览
| 通知类型 | 注解 | 执行时机 | 核心特点 |
|---|---|---|---|
| 前置通知 | @Before |
方法执行前 | 方法执行前触发 |
| 后置通知 | @After |
方法执行后 | 无论是否异常都执行 |
| 返回后通知 | @AfterReturning |
方法正常返回后 | 只有正常return才执行,异常不执行 |
| 异常后通知 | @AfterThrowing |
方法抛出异常后 | 仅异常触发 |
| 环绕通知 | @Around |
完全包裹方法 | 可修改参数、控制执行、修改返回值 |
结论
-
- 只有 @AfterReturning(返回后通知)和 @Around(环绕通知)可以获取方法的返回值,其余通知类型无法直接拿到方法执行结果。
-
- 只有 @Around(环绕通知)可以修改方法入参、并将修改后的参数重新传入原始方法执行,同时可以自主控制原始方法是否执行、何时执行,是功能最强大的通知类型。
-
- 前置 / 后置 / 异常通知仅能做「无侵入式的附加操作」,无法干预原始方法的执行流程和参数。
环绕通知核心示例(参数去空格)
@Component
@Aspect
public class MyAdvice {
// 匹配所有业务方法
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){}
// 环绕通知:唯一能修改参数的通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 1. 获取原始参数
Object[] args = pjp.getArgs();
// 2. 修改参数(去空格)
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) {
args[i] = ((String) args[i]).trim();
}
}
// 3. 传入新参数,执行原始方法(控制执行)
Object result = pjp.proceed(args);
// 4. 返回结果(Around也能获取返回值)
return result;
}
}
极简区别(必背)
@After(后置最终通知):无论正常返回还是抛异常,都执行@AfterReturning(返回后通知):只有正常 return 才执行,异常不执行
区别就是:异常后会不会执行。
2.3 环绕通知(核心重点)
环绕通知是功能最强大的通知,通过 ProceedingJoinPoint.proceed() 调用目标方法。
- 执行
proceed()之前 → 前置逻辑 - 调用
proceed()→ 执行目标方法 - 执行
proceed()之后 → 后置逻辑
环绕通知标准模板
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
try {
// 前置逻辑
System.out.println("环绕 - 前置");
// 执行目标方法
result = joinPoint.proceed();
// 返回后逻辑
System.out.println("环绕 - 正常返回");
} catch (Exception e) {
// 异常逻辑
System.out.println("环绕 - 异常");
throw e;
} finally {
// 最终逻辑(等价 @After)
System.out.println("环绕 - 最终");
}
return result;
}
环绕通知的核心特性:控制目标方法执行(拦截/放行)
这段描述的是 Spring AOP 中环绕通知(@Around)的一个核心特性:通过控制是否调用目标方法,实现对原始操作的"拦截"或"隔离"。
具体原理
环绕通知是所有通知类型中唯一能控制目标方法是否执行 的通知,因为它通过 ProceedingJoinPoint 对象的 proceed() 方法显式调用目标方法。
- 如果调用
proceed():目标方法会正常执行(相当于"放行")。 - 如果不调用
proceed():目标方法会被跳过(相当于"拦截"),此时可以返回自定义结果或直接抛出异常。
权限校验的例子
@Around("execution(* com.example.service.OrderService.approve(..))")
public Object checkManagerPermission(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 获取当前用户权限
String userRole = SecurityContext.getCurrentUserRole();
// 2. 权限校验
if ("MANAGER".equals(userRole)) {
// 有权限:调用目标方法(放行)
System.out.println("权限校验通过,执行审批操作");
return joinPoint.proceed(); // 执行原始的 approve() 方法
} else {
// 无权限:不调用目标方法(拦截),返回提示或抛出异常
System.out.println("权限不足,仅经理可审批订单");
throw new AccessDeniedException("无审批权限");
}
}
三、5种通知使用演示(基础代码略)
基础工程代码(pom、Dao、配置类、运行类)与前文一致,此处不再重复。
3.1 前置通知 @Before
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
3.2 后置通知 @After
@After("pt()")
public void after() {
System.out.println("after advice ...");
}
3.3 返回后通知 @AfterReturning
@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
3.4 异常后通知 @AfterThrowing
@AfterThrowing("pt2()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
3.5 环绕通知 @Around
@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before ...");
pjp.proceed();
System.out.println("around after ...");
}
四、 环绕通知返回值终极详解
核心逻辑
环绕通知的返回值类型,需要与原方法返回值类型一致,或是其父类;
但 @After 后置通知、@AfterReturning 返回后通知,只做监听不修改结果,方法一律用 void 即可。
解释:
- @Around 环绕通知 :必须接收
ProceedingJoinPoint,必须调用proceed(),可以修改/控制返回值,所以返回类型必须匹配。 - @After / @AfterReturning :只是监听方法执行结果,不修改、不接管返回值 ,所以方法都是
void,不需要返回值。
核心一句话(最精准)
环绕通知是否需要接收/返回值,只取决于一件事:
【业务代码(AOP 通知内部 或 主程序调用处)到底有没有用到原方法的返回值】
分场景清晰说明(带原方法示例)
先给出本次用到的【原方法】
// 原方法:有返回值 int
public int select() {
System.out.println("查询中...");
return 100;
}
场景 1:任何地方都不需要原方法返回值
(AOP 里不用、主程序也不用)
结论:
即使原方法有返回值 int ,环绕通知依然可以写 void ,可以不接收返回值,直接调用 proceed()。
// 原方法有返回值 int
// 但业务完全不用返回值 → 环绕可以写 void
@Around("pt2()")
public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕前置");
pjp.proceed(); // 直接执行,不接收返回值
System.out.println("环绕后置");
}
调用代码(也不接收返回值):
public static void main(String[] args) {
BookDao bean = ctx.getBean(BookDao.class);
bean.select(); // 调用但不接收结果
}
场景 2:需要使用原方法返回值
(AOP 内部要打印/修改 或 主程序要接收结果)
结论:
环绕通知不能写 void,必须:
-
用
Object(或对应类型)接收proceed()的结果 -
必须 return 结果
-
返回值类型要与原方法一致,或是其父类
// 原方法:int select()
// 需要使用返回值 → 环绕必须用 Object 接收并返回
@Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕前置");
Object ret = pjp.proceed(); // 必须接收
System.out.println("原方法返回值:" + ret); // 使用返回值
return ret; // 必须返回
}
调用代码(需要接收结果):
public static void main(String[] args) {
BookDao bean = ctx.getBean(BookDao.class);
int result = bean.select(); // 主程序需要结果
System.out.println("最终结果:" + result);
}
一张表彻底看懂
| 业务场景 | 原方法返回值 | 环绕通知写法 | 必须做什么 |
|---|---|---|---|
| AOP、主程序都不用返回值 | int / Object / void | void |
直接 pjp.proceed(); 不接收、不 return |
| AOP 或主程序需要返回值 | int / Object | Object |
Object ret = pjp.proceed(); 必须 return ret; |
最终超级总结(背这个就够)
- 不用返回值 (任何地方都不用)→ 环绕通知可以写
void,不接收、不 return - 要用返回值 (AOP 内部 或 主程序需要)→ 环绕通知不能写 void ,必须接收 + return
- 返回值类型:必须与原方法一致,或是其父类
- @After / @AfterReturning :一律
void,不管原方法有没有返回值
五、环绕通知注意事项(避坑指南)
- 环绕通知必须依赖
ProceedingJoinPoint才能调用原始方法 - 不调用
proceed()会跳过原始方法(可用于拦截、限流) - 原始方法
void:环绕可void或Object - 原始方法有返回值:
不用结果 → 可void
要用结果 → 必须Object+ return - 环绕通知必须抛出
Throwable
六、通知能力终极对比(面试必考)
| 通知类型 | 能否获取返回值 | 能否修改入参 | 能否控制执行 |
|---|---|---|---|
| @Before | ❌ | ❌ | ❌ |
| @After | ❌ | ❌ | ❌ |
| @AfterReturning | ✅ | ❌ | ❌ |
| @AfterThrowing | ❌ | ❌ | ❌ |
| @Around | ✅ | ✅ | ✅ |
最终三大铁律
- 只有 @AfterReturning 和 @Around 可以获取返回值
- 只有 @Around 可以修改参数、控制方法执行
- return 先执行,通知后执行
七、全文总结
- 切入点表达式:语法、通配符、书写规范,精准匹配方法
- 5种通知执行顺序:Before → 目标方法 → AfterReturning/AfterThrowing → After
- 核心区别:After 始终执行,AfterReturning 仅正常返回执行
- 环绕通知是全能通知,可替代其他所有通知,还能实现方法拦截
- 环绕返回值终极规则:用则接收返回,不用则可void