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

相关推荐
weixin_307779131 小时前
从“人操作仪器”到“Agent 自主发现”:OPL 金属材料自驱动实验室 MVP 技术论述
开发语言·人工智能·算法·架构
笑鸿的学习笔记1 小时前
C++笔记之SIMD与SSE指令集
开发语言·c++·笔记
福兮说2 小时前
URL 编码的七个坑:c++ 传到后端变空格、%25 套娃、截断 emoji 直接报错
开发语言·前端·javascript·node.js·url
小的时候可菜了2 小时前
JS 异步的本质:从单线程到 thenable(一条进化链)
开发语言·前端·javascript
可乐鸡翅yeah_2 小时前
hls.js 配置 loadStartPosition,点播起始播放位置踩坑
开发语言·javascript·网络协议·ecmascript·m3u8在线
en.en..2 小时前
Linux 进程退出exit(函数)
java·开发语言·算法
Cx330❀2 小时前
【Qt 进阶指南】详解 Qt 常用标准对话框与主窗口组件架构
开发语言·qt·ui·性能优化·架构·图形渲染
Light Gao2 小时前
Encoder 与 Decoder 详解:从 Embedding 到 Seq2Seq 的完整数据流
java·开发语言·embedding
HZZD_HZZD2 小时前
弱网下CoAP还省吗?合众致达Cat.1电表90天实测:MQTT重连流量占52%、续航差距从38%拉大到73%
开发语言·物联网·php·腾讯云