AOP 通知的类型详解

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 功能最全但最复杂。理解每种通知的执行时机和能力边界,就能在实际开发中选择最合适的通知类型,写出清晰、健壮的切面代码。

相关推荐
wangchunyu1141 天前
JAVA AQS与Lock实现原理(含CAS)
java·开发语言·word
爱喝水的鱼丶1 天前
SAP-ABAP:ECC→S/4HANA ABAP 迁移升级指南——程序改造、语法兼容与性能优化 导语
运维·开发语言·性能优化·sap·abap·经验交流·开发交流
小灰灰搞电子1 天前
Rust+Slint 实现FTP客户端调试助手源码分享
开发语言·后端·rust
外收内放1 天前
面向对象高级(实战案例)
开发语言·python·学习
fanruitian1 天前
python else用法
开发语言·python
FRing031 天前
JavaScript入门到精通二十六(变量提升、函数剩余参数、展开运算符、ES6箭头函数)
开发语言·前端·javascript·es6·箭头函数
云深麋鹿1 天前
C++ | 基于红黑树实现mymap与myset
开发语言·c++
weixin_417396831 天前
桌面云终端PXE启动失败怎么排查?从DHCP Option到镜像下发
开发语言·前端·php·技术·酒店·综合布线·弱电
某不知名網友1 天前
ROS2进阶:服务通信、自定义SRV接口、参数服务器与基于服务的参数通信详解
java·开发语言·microsoft
java_nnnn1 天前
LeetCode刷题滑动窗口(水果成篮+找到字符串中所有字母的异位词)Java
java·开发语言·leetcode