Spring AOP 核心进阶:切入点表达式 + 通知类型 + 环绕通知避坑指南(Spring系列8)

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 书写技巧(最佳实践)

  1. 描述切入点优先描述接口,不描述实现类
  2. 访问控制修饰符可省略
  3. 返回值类型查询类使用 * 通配
  4. 包名尽量不使用 .. 匹配,效率过低
  5. 接口名/类名可使用 *Service 通配
  6. 方法名书写以动词精准匹配,名词用 *
  7. 参数根据业务灵活调整
  8. 通常不使用异常作为匹配规则

二、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 完全包裹方法 可修改参数、控制执行、修改返回值

结论

    1. 只有 @AfterReturning(返回后通知)和 @Around(环绕通知)可以获取方法的返回值,其余通知类型无法直接拿到方法执行结果。
    1. 只有 @Around(环绕通知)可以修改方法入参、并将修改后的参数重新传入原始方法执行,同时可以自主控制原始方法是否执行、何时执行,是功能最强大的通知类型。
    1. 前置 / 后置 / 异常通知仅能做「无侵入式的附加操作」,无法干预原始方法的执行流程和参数。

环绕通知核心示例(参数去空格)

复制代码
@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,必须:

  1. Object(或对应类型)接收 proceed() 的结果

  2. 必须 return 结果

  3. 返回值类型要与原方法一致,或是其父类

    // 原方法: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;

最终超级总结(背这个就够)

  1. 不用返回值 (任何地方都不用)→ 环绕通知可以写 void不接收、不 return
  2. 要用返回值 (AOP 内部 或 主程序需要)→ 环绕通知不能写 void ,必须接收 + return
  3. 返回值类型:必须与原方法一致,或是其父类
  4. @After / @AfterReturning :一律 void,不管原方法有没有返回值

五、环绕通知注意事项(避坑指南)

  1. 环绕通知必须依赖 ProceedingJoinPoint 才能调用原始方法
  2. 不调用 proceed()跳过原始方法(可用于拦截、限流)
  3. 原始方法 void:环绕可 voidObject
  4. 原始方法有返回值:
    不用结果 → 可 void
    要用结果 → 必须 Object + return
  5. 环绕通知必须抛出 Throwable

六、通知能力终极对比(面试必考)

通知类型 能否获取返回值 能否修改入参 能否控制执行
@Before
@After
@AfterReturning
@AfterThrowing
@Around

最终三大铁律

  1. 只有 @AfterReturning 和 @Around 可以获取返回值
  2. 只有 @Around 可以修改参数、控制方法执行
  3. return 先执行,通知后执行

七、全文总结

  1. 切入点表达式:语法、通配符、书写规范,精准匹配方法
  2. 5种通知执行顺序:Before → 目标方法 → AfterReturning/AfterThrowing → After
  3. 核心区别:After 始终执行,AfterReturning 仅正常返回执行
  4. 环绕通知是全能通知,可替代其他所有通知,还能实现方法拦截
  5. 环绕返回值终极规则:用则接收返回,不用则可void
相关推荐
清汤饺子2 小时前
Cursor + Claude Code 组合使用心得:我为什么不只用一个 AI 编程工具
前端·javascript·后端
weitingfu2 小时前
Excel VBA 入门到精通(二):变量、数据类型与运算符
java·大数据·开发语言·学习·microsoft·excel·vba
无责任此方_修行中2 小时前
Redis 的"三面"人生:开源世界的权力转移
redis·后端·程序员
某人辛木2 小时前
Maven一步到位
java·maven
一条咸鱼_SaltyFish2 小时前
DDD 架构重构实践:AI Skills 如何赋能DDD设计与重构
java·人工智能·ai·重构·架构·ddd·领域驱动设计
想唱rap2 小时前
线程之条件变量和生产消费模型
java·服务器·开发语言·数据库·mysql·ubuntu
花千树-0102 小时前
Java AI + TTS:让大模型开口说话
java·人工智能·ai·chatgpt·langchain·aigc·ai编程
Boop_wu2 小时前
[Java 算法] 栈
java·开发语言·算法
不爱吃炸鸡柳2 小时前
C++ STL 核心:string 从入门到精通(面试+源码+OJ实战)
java·c++·面试