AOP 通知的类型详解
通知(Advice)是切面在切点上执行的具体动作,它定义了"在什么时机做什么"。Spring AOP 提供了五种通知类型,每种对应不同的执行时机和适用场景。
一、五种通知总览
| 通知类型 | 注解 | 执行时机 | 能否获取返回值 | 能否获取异常 | 能否阻止目标方法 |
|---|---|---|---|---|---|
| 前置通知 | @Before |
目标方法执行前 | ❌ | ❌ | ❌ |
| 后置通知 | @After |
目标方法执行后(无论异常) | ❌ | ❌ | ❌ |
| 返回通知 | @AfterReturning |
目标方法正常返回后 | ✅ | ❌ | ❌ |
| 异常通知 | @AfterThrowing |
目标方法抛出异常后 | ❌ | ✅ | ❌ |
| 环绕通知 | @Around |
目标方法执行前后 | ✅ | ✅ | ✅ |
二、前置通知 @Before
在目标方法执行之前执行。适合做参数校验、日志记录、权限检查等准备工作。
java
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("开始执行:" + methodName + ",参数:" + Arrays.toString(args));
}
特点:
- 无法阻止目标方法执行(除非抛出异常)。
- 无法获取返回值。
- 无法获取异常。
- 如果需要修改参数,只能用
@Around。
适用场景:
- 记录方法调用日志
- 参数合法性校验
- 权限检查(不通过则抛异常)
- 设置 MDC 上下文
注意: @Before 中抛出异常会阻止目标方法执行,但不会触发 @AfterThrowing(因为 @AfterThrowing 只捕获目标方法本身的异常)。
三、后置通知 @After
在目标方法执行之后执行,无论目标方法正常返回还是抛出异常。
java
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println("执行结束:" + joinPoint.getSignature().getName());
}
特点:
- 类似
finally块,一定会执行。 - 无法获取返回值。
- 无法获取异常。
- 无法阻止目标方法执行。
适用场景:
- 清理资源(如关闭连接、释放锁)
- 记录方法执行结束日志
- 统计方法调用次数
注意: @After 和 @AfterReturning 的区别在于,@After 无论是否异常都会执行,@AfterReturning 只在正常返回时执行。如果同时使用,@AfterReturning 先执行,@After 后执行。
四、返回通知 @AfterReturning
在目标方法正常返回后执行,可以获取返回值。
java
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("返回结果:" + result);
}
特点:
- 只在目标方法正常返回时执行。
- 可以获取返回值,通过
returning属性指定参数名。 - 可以修改返回值?不能直接修改,但可以替换返回对象的内容。
- 如果目标方法抛出异常,不会执行。
returning 属性的类型匹配:
java
// 只匹配返回 String 的方法
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturning(JoinPoint jp, String result) {
// 如果目标方法返回的不是 String,此通知不执行
}
// 匹配任意返回值
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturning(JoinPoint jp, Object result) {
// 所有正常返回都会执行
}
适用场景:
- 记录方法返回值日志
- 对返回值做后处理(如脱敏、格式化)
- 缓存结果
注意: returning 指定的参数名必须与切点表达式中的名称一致,且类型要匹配。如果类型不匹配,通知不会执行。
五、异常通知 @AfterThrowing
在目标方法抛出异常后执行,可以获取异常对象。
java
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
System.out.println("执行异常:" + joinPoint.getSignature().getName()
+ ",异常信息:" + ex.getMessage());
}
特点:
- 只在目标方法抛出异常时执行。
- 可以获取异常对象,通过
throwing属性指定参数名。 - 无法处理异常(异常会继续向上抛出),只能记录。
- 无法获取返回值。
throwing 属性的类型过滤:
java
// 只捕获 IllegalArgumentException
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowing(JoinPoint jp, IllegalArgumentException ex) {
// 只有 IllegalArgumentException 才会触发
}
// 捕获所有异常
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowing(JoinPoint jp, Exception ex) {
// 所有 Exception 子类都会触发
}
// 捕获 Throwable
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowing(JoinPoint jp, Throwable ex) {
// 所有异常和错误都会触发
}
适用场景:
- 统一记录异常日志
- 异常监控和告警
- 异常信息收集
注意: @AfterThrowing 不能吞掉异常,异常会继续向上抛出。如果需要处理异常(吞掉或转换),必须用 @Around。
六、环绕通知 @Around
功能最全的通知,可以在目标方法执行前后都插入逻辑,控制目标方法是否执行、修改参数、修改返回值、处理异常。
java
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
long start = System.currentTimeMillis();
Object result;
try {
// 前置逻辑
System.out.println("开始执行:" + methodName);
// 执行目标方法
result = pjp.proceed();
// 返回后逻辑
long cost = System.currentTimeMillis() - start;
System.out.println("执行成功:" + methodName + ",耗时:" + cost + "ms");
return result;
} catch (Throwable e) {
// 异常逻辑
long cost = System.currentTimeMillis() - start;
System.out.println("执行失败:" + methodName + ",耗时:" + cost + "ms");
throw e;
} finally {
// 最终逻辑
System.out.println("执行结束:" + methodName);
}
}
特点:
- 可以控制目标方法是否执行(不调用
proceed()则不执行)。 - 可以修改方法参数(传入新的参数给
proceed())。 - 可以修改返回值(返回不同的对象)。
- 可以处理异常(吞掉或转换)。
- 必须返回
Object,且要返回proceed()的结果(除非有意修改)。 - 方法参数是
ProceedingJoinPoint,不是JoinPoint。
修改参数:
java
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
if (args.length > 0 && args[0] instanceof String) {
args[0] = ((String) args[0]).trim(); // 修改参数
}
return pjp.proceed(args); // 传入修改后的参数
}
修改返回值:
java
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
if (result instanceof String) {
return ((String) result).toUpperCase(); // 修改返回值
}
return result;
}
吞掉异常:
java
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
try {
return pjp.proceed();
} catch (Exception e) {
System.out.println("异常被吞掉:" + e.getMessage());
return null; // 不再抛出异常
}
}
适用场景:
- 性能监控(统计耗时)
- 事务管理(开启、提交、回滚)
- 缓存(先查缓存,未命中才执行)
- 重试(失败后重试)
- 限流(超过阈值则拒绝)
- 修改参数或返回值
七、通知的执行顺序
1. 单个切面中的执行顺序
@Around 前半部分
→ @Before
→ 目标方法执行
→ @AfterReturning(正常返回)或 @AfterThrowing(抛出异常)
→ @After
@Around 后半部分
完整示例:
java
@Aspect
@Component
public class OrderAspect {
@Before("serviceLayer()")
public void before() {
System.out.println("1. @Before");
}
@AfterReturning("serviceLayer()")
public void afterReturning() {
System.out.println("3. @AfterReturning");
}
@After("serviceLayer()")
public void after() {
System.out.println("4. @After");
}
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("0. @Around 前");
Object result = pjp.proceed();
System.out.println("5. @Around 后");
return result;
}
}
输出:
0. @Around 前
1. @Before
(目标方法执行)
3. @AfterReturning
4. @After
5. @Around 后
2. 多个切面的执行顺序
用 @Order 控制切面优先级,数值越小优先级越高。
java
@Aspect
@Component
@Order(1)
public class LogAspect {
@Before("serviceLayer()")
public void before() { System.out.println("LogAspect @Before"); }
@After("serviceLayer()")
public void after() { System.out.println("LogAspect @After"); }
}
@Aspect
@Component
@Order(2)
public class TransactionAspect {
@Before("serviceLayer()")
public void before() { System.out.println("TransactionAspect @Before"); }
@After("serviceLayer()")
public void after() { System.out.println("TransactionAspect @After"); }
}
输出:
LogAspect @Before
TransactionAspect @Before
(目标方法执行)
TransactionAspect @After
LogAspect @After
@Before 按 @Order 从小到大执行,@After 按 @Order 从大到小执行。这就像栈结构,先进后出。
八、通知类型的对比总结
| 对比维度 | @Before | @After | @AfterReturning | @AfterThrowing | @Around |
|---|---|---|---|---|---|
| 执行时机 | 方法前 | 方法后(总是) | 正常返回后 | 异常抛出后 | 方法前后 |
| 获取参数 | ✅ | ✅ | ✅ | ✅ | ✅ |
| 获取返回值 | ❌ | ❌ | ✅ | ❌ | ✅ |
| 获取异常 | ❌ | ❌ | ❌ | ✅ | ✅ |
| 阻止方法执行 | ❌ | ❌ | ❌ | ❌ | ✅ |
| 修改参数 | ❌ | ❌ | ❌ | ❌ | ✅ |
| 修改返回值 | ❌ | ❌ | ❌ | ❌ | ✅ |
| 处理异常 | ❌ | ❌ | ❌ | ❌ | ✅ |
| 参数类型 | JoinPoint | JoinPoint | JoinPoint | JoinPoint | ProceedingJoinPoint |
九、如何选择通知类型
| 需求 | 推荐通知 |
|---|---|
| 记录方法调用前日志 | @Before |
| 记录方法调用后日志(无论异常) | @After |
| 记录返回值 | @AfterReturning |
| 记录异常 | @AfterThrowing |
| 统计方法耗时 | @Around |
| 事务管理 | @Around |
| 缓存处理 | @Around |
| 修改参数 | @Around |
| 修改返回值 | @Around |
| 吞掉异常 | @Around |
| 重试机制 | @Around |
选择原则:
- 简单的日志记录用
@Before、@After、@AfterReturning。 - 需要获取返回值用
@AfterReturning。 - 需要获取异常用
@AfterThrowing。 - 需要控制方法执行、修改参数/返回值、处理异常时,用
@Around。
@Around 虽然功能最全,但代码最复杂,容易出错。能用简单通知解决的,不要用 @Around。
十、常见问题
1. @Around 忘记返回 proceed() 的结果
java
// ❌ 错误:目标方法的返回值被丢弃
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
pjp.proceed();
return null;
}
// ✅ 正确
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
2. @AfterReturning 的 returning 类型不匹配
java
// 目标方法返回 void,但通知声明了 String 类型
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturning(JoinPoint jp, String result) {
// 不会执行,因为返回值类型不匹配
}
3. @AfterThrowing 不能处理异常
@AfterThrowing 只能记录异常,不能吞掉异常。异常会继续向上抛出。如果需要处理异常,必须用 @Around。
4. 通知的执行顺序与预期不符
多个切面时,用 @Order 明确指定顺序。@Before 按 @Order 升序执行,@After 按 @Order 降序执行。
5. @Around 中调用 proceed() 多次
proceed() 可以调用多次,实现重试机制。但要注意,多次调用会多次执行目标方法。
java
@Around("serviceLayer()")
public Object retry(ProceedingJoinPoint pjp) throws Throwable {
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
return pjp.proceed();
} catch (Exception e) {
if (i == maxRetries - 1) throw e;
System.out.println("重试第 " + (i + 1) + " 次");
}
}
return null;
}
十一、总结
| 通知 | 一句话说明 | 核心能力 |
|---|---|---|
@Before |
目标方法前执行 | 前置准备、日志、校验 |
@After |
目标方法后执行(总是) | 资源清理、收尾日志 |
@AfterReturning |
正常返回后执行 | 获取返回值、结果处理 |
@AfterThrowing |
异常抛出后执行 | 获取异常、异常记录 |
@Around |
目标方法前后执行 | 全控制:参数、返回值、异常 |
五种通知各有用途,@Before 和 @After 适合简单的日志和清理,@AfterReturning 和 @AfterThrowing 适合针对结果的处理,@Around 功能最全但最复杂。理解每种通知的执行时机和能力边界,就能在实际开发中选择最合适的通知类型,写出清晰、健壮的切面代码。