【JavaEE29-后端部分】Spring AOP 切点表达式详解——精准定位,想切哪里切哪里


开篇:快递员怎么知道把包裹送到你家?

老铁们,你有没有想过一个问题:快递员每天要送几百个包裹,他是怎么知道哪个包裹送到哪一家的?

答案是:地址。地址写得越详细,快递员就越容易找到你家。

  • 中国 → 范围太大了,不知道哪个省
  • 中国贵州省 → 范围缩小了一点
  • 中国贵州省黔南布依族苗族自治州 → 更具体了
  • 中国贵州省黔南布依族苗族自治州××小区1号楼202室 → 精准定位

在 Spring AOP 中,切点表达式就相当于这个"地址"。它告诉 AOP:你要对哪些方法进行增强?是所有的 Controller 方法,还是某个包下的所有类,还是某个特定的方法?

本期,我们就来学习切点表达式的语法规则,让你能够精准定位任何你想要增强的方法。


一、切点表达式是什么?

1.1 官方定义

切点表达式(Pointcut Expression)是一组规则,用来匹配一个或多个连接点(方法)。它的作用就是告诉 AOP:"我要对哪些方法下手"。

1.2 生活化理解

想象你是一个快递公司的调度员,你要给快递员分派任务:

  • "把本区域所有快递都送过去" → 匹配所有方法
  • "只送图书相关的快递" → 匹配 controller 包下的方法
  • "只送给张三的快递" → 匹配 addBook 方法
  • "只送贵州省内的快递" → 匹配 com.zhongge 包下的方法

切点表达式就是这种"指令",它用一套固定的语法来编写。


二、最常用的切点表达式:execution

execution 译为执行,aop中表示 匹配方法执行

2.1 基本语法

java 复制代码
execution(访问修饰符 返回类型 包名.类名.方法名(参数) 异常)

其中:

  • 访问修饰符publicprivate 等,可以省略
  • 异常:可以省略

最简形式

java 复制代码
execution(返回类型 包名.类名.方法名(参数))

2.2 举个例子

java 复制代码
// 匹配 BookController 中的 addBook 方法(无参数)
execution(* com.zhongge.controller.BookController.addBook())

// 匹配 BookController类 中的所有方法
execution(* com.zhongge.controller.BookController.*(..))

// 匹配 controller 包下所有类的所有方法
execution(* com.zhongge.controller.*.*(..))

// 匹配 com.zhongge 包下所有类的所有方法
execution(* com.zhongge..*.*(..))

三、通配符详解:*..

3.1 * ------ 匹配任意一个元素

* 可以匹配:返回类型、包名(一层)、类名、方法名、参数类型(一个)

位置 写法 含义
返回类型 * 任意返回类型
包名(一层) com.zhongge.*.controller 匹配 com.zhongge.book.controller
类名 *Controller 匹配 BookControllerUserController
方法名 * 任意方法名
参数(一个) (*) 任意一个参数

示例

java 复制代码
// 匹配任意返回类型
execution(* com.zhongge.controller.BookController.addBook())

// 匹配 BookController 中以 get 开头的方法
execution(* com.zhongge.controller.BookController.get*(..))

// 匹配 BookController 中只有一个参数的方法
execution(* com.zhongge.controller.BookController.*(*))

3.2 .. ------ 匹配多个连续的元素

.. 可以匹配:多层包路径、任意个数的参数

位置 写法 含义
包名 com.zhongge..controller 匹配 com.zhongge.controllercom.zhongge.book.controller
参数 (..) 任意个数、任意类型的参数
参数 (String, ..) 第一个参数是 String,后面任意

示例

java 复制代码
// 匹配 com.zhongge 包及其子包下所有类的所有方法
execution(* com.zhongge..*.*(..))

// 匹配任意个数参数的方法
execution(* com.zhongge.controller.BookController.*(..))

// 匹配第一个参数是 Integer,后面任意参数的方法
execution(* com.zhongge.controller.BookController.*(Integer, ..))

四、实战演练:一步步写出精准的切点表达式

假设我们的包结构如下:

复制代码
com.zhongge
├── controller
│   ├── BookController
│   │   ├── addBook(BookInfo)
│   │   ├── getListByPage(PageRequest)
│   │   ├── queryBookById(Integer)
│   │   ├── updateBook(BookInfo)
│   │   └── batchDelete(List<Integer>)
│   └── UserController
│       └── login(String, String)
├── service
│   ├── BookService
│   └── UserService
└── mapper
    ├── BookMapper
    └── UserMapper

4.1 匹配单个方法

java 复制代码
// 匹配 BookController 中的 addBook 方法(参数是 BookInfo)
execution(* com.zhongge.controller.BookController.addBook(com.zhongge.model.BookInfo))

// 简化写法:用 * 代替全限定类名
execution(* com.zhongge.controller.BookController.addBook(*))

4.2 匹配一个类中的所有方法

java 复制代码
// 匹配 BookController 中的所有方法
execution(* com.zhongge.controller.BookController.*(..))

4.3 匹配一个包下的所有类中的所有方法

java 复制代码
// 匹配 controller 包下所有类的所有方法
execution(* com.zhongge.controller.*.*(..))

4.4 匹配一个包及其子包下的所有类中的所有方法

java 复制代码
// 匹配 com.zhongge 包及其子包下所有类的所有方法
execution(* com.zhongge..*.*(..))

4.5 匹配符合命名规则的方法

java 复制代码
// 匹配所有以 get 开头的方法
execution(* com.zhongge.controller.*.get*(..))

// 匹配所有以 query 开头的方法
execution(* com.zhongge.controller.*.query*(..))

4.6 匹配特定参数的方法

java 复制代码
// 匹配只有一个参数的方法
execution(* com.zhongge.controller.*.*(*))

// 匹配第一个参数是 Integer 的方法
execution(* com.zhongge.controller.*.*(Integer, ..))

// 匹配无参数的方法
execution(* com.zhongge.controller.*.*())

4.7 组合多个条件

java 复制代码
// 匹配 BookController 中返回类型为 Result 的方法
execution(com.zhongge.model.Result com.zhongge.controller.BookController.*(..))

// 匹配 public 方法
execution(public * com.zhongge.controller.*.*(..))

五、第二种切点表达式:@annotation

annotation译为注解

5.1 什么时候用 @annotation

execution 表达式虽然强大,但它有一个缺点:必须按照规则来匹配。如果你的需求是"匹配多个无规则的方法",比如:

  • BookController 中的 addBook 方法
  • UserController 中的 login 方法
  • TestController 中的 test 方法

这几个方法没有共同的包名、类名、方法名前缀,用 execution 很难匹配。这时候,我们可以用 自定义注解 + @annotation 的方式。

5.2 自定义注解

先创建一个自定义注解,比如叫 @Log

java 复制代码
package com.zhongge.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 这个注解只能用在方法上
@Target(ElementType.METHOD)
// 这个注解在运行时保留,这样 AOP 才能读取到
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    // 可以定义属性,比如模块名称
    String value() default "";
}

注解的两个关键注解

  • @Target:指定这个注解可以用在哪里(类、方法、字段等)
  • @Retention:指定这个注解保留到什么时候(源代码、编译期、运行时)

5.3 在需要增强的方法上添加注解

java 复制代码
@RestController
@RequestMapping("/book")
public class BookController {

    @Log("添加图书")
    @RequestMapping("/addBook")
    public Result addBook(BookInfo bookInfo) { ... }

    @RequestMapping("/queryBookById")
    public BookInfo queryBookById(Integer bookId) { ... }  // 这个不会被增强
}

@RestController
@RequestMapping("/user")
public class UserController {

    @Log("用户登录")
    @RequestMapping("/login")
    public Boolean login(String name, String password, HttpSession session) { ... }
}

5.4 编写切面,用 @annotation 匹配

java 复制代码
@Slf4j
@Aspect//告诉Spring这是一个切面类
@Component//将这个切面类交给Spring管理
public class LogAspect {

    // 匹配所有加了 @Log 注解的方法
    @Before("@annotation(com.zhongge.annotation.Log)")
    public void before(JoinPoint joinPoint) {
        log.info("开始执行方法:{}", joinPoint.getSignature().getName());
    }

    @After("@annotation(com.zhongge.annotation.Log)")
    public void after(JoinPoint joinPoint) {
        log.info("方法执行完毕:{}", joinPoint.getSignature().getName());
    }

    // 如果需要获取注解的属性值
    @Around("@annotation(log)")
    public Object around(ProceedingJoinPoint joinPoint, Log log) throws Throwable {
        log.info("模块:{},方法:{} 开始执行", log.value(), joinPoint.getSignature().getName());
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        log.info("模块:{},方法:{} 执行耗时:{} ms", log.value(), joinPoint.getSignature().getName(), end - start);
        return result;
    }
}
  • JoinPoint(连接点) :可以用在 @Before@After@AfterReturning@AfterThrowing 中。
  • ProceedingJoinPoint(可执行连接点)只能 用在 @Around 中。

1)、为什么会有两个不同的对象?

AOP 在"环绕通知"(@Around)时,需要你手动决定 是否执行原始方法(proceed()),并且可以修改返回值、处理异常。所以 Spring 专门给你一个 ProceedingJoinPoint,它继承JoinPoint,多了 proceed() 方法。

而在其他通知(@Before@After 等)中,原始方法一定会被执行 (你拦不住),你只需要读一些信息(方法名、参数等),所以给你 JoinPoint 就够了。


2)、正确用法对照表
通知类型 能用的参数 是否必须调用 proceed()
@Before JoinPoint
@After JoinPoint
@AfterReturning JoinPoint,还可以加 returning 接收返回值
@AfterThrowing JoinPoint,还可以加 throwing 接收异常
@Around 必须ProceedingJoinPoint ,否则原始方法不执行

3)、一句话记忆

@Around 要用 ProceedingJoinPoint(因为要 proceed),其他通知用 JoinPoint(因为只需要读信息)。


5.5 执行效果

访问 /book/addBook,控制台输出:

text 复制代码
模块:添加图书,方法:addBook 开始执行
... 业务执行 ...
模块:添加图书,方法:addBook 执行耗时:35 ms

访问 /user/login,控制台输出:

text 复制代码
模块:用户登录,方法:login 开始执行
... 业务执行 ...
模块:用户登录,方法:login 执行耗时:12 ms

访问 /book/queryBookById(没有加 @Log 注解),没有任何日志输出。

这就是 @annotation 的强大之处:想增强哪个方法,就在哪个方法上加个注解,非常灵活!


5.6 简单理解原理

逻辑是这样的:我们要对某个连接点(也就是某个方法)进行统一处理,说白了就是要对这个方法"下手"。但是这些方法比较特殊,可能不在同一个包、同一个类里,也没有什么规律可循,所以我们不能用 execution 表达式去统一匹配它们。

那怎么办呢?用注解。我们自己定义一个"标签"------也就是自定义注解。

第一步,先把这个标签定义出来(自定义一个注解)。定义好之后,你想对哪个方法下手,就在哪个方法上贴上这个标签。贴了标签,就代表这个方法要被统一处理。

贴好标签之后,我们再写一个切面类,专门负责"收拾"这些贴了标签的方法。在切面类里,我们用 @annotation 来引出这些方法,并写上自定义注解的全限定类名(包名 + 类名)。这样,切面就知道要去处理哪些方法了。


六、切点表达式速查表

6.1 常用 execution 表达式

需求 表达式
匹配 controller 包下所有类的所有方法 execution(* com.zhongge.controller.*.*(..))
匹配 controller 包及其子包下所有类的所有方法 execution(* com.zhongge.controller..*.*(..))
匹配 BookController 中的所有方法 execution(* com.zhongge.controller.BookController.*(..))
匹配 BookController 中的无参方法 execution(* com.zhongge.controller.BookController.*())
匹配 BookController 中返回类型为 Result 的方法 execution(com.zhongge.model.Result com.zhongge.controller.BookController.*(..))
匹配所有 public 方法 execution(public * *(..))
匹配所有以 get 开头的方法 execution(* com.zhongge.controller.*.get*(..))
匹配只有一个参数的方法 execution(* com.zhongge.controller.*.*(*))
匹配第一个参数是 Integer 的方法 execution(* com.zhongge.controller.*.*(Integer, ..))

6.2 @annotation 表达式

需求 表达式
匹配所有加了 @Log 注解的方法 @annotation(com.zhongge.annotation.Log)
匹配所有加了任意注解的方法,比如匹配加了 @RequestMapping 注解的方法 @annotation(org.springframework.web.bind.annotation.RequestMapping)

6.3 组合表达式(&&||!

java 复制代码
// 匹配 controller 包下所有方法,但排除 BookController
@Pointcut("execution(* com.zhongge.controller.*.*(..)) && !execution(* com.zhongge.controller.BookController.*(..))")

// 匹配 BookController 或 UserController 中的方法
@Pointcut("execution(* com.zhongge.controller.BookController.*(..)) || execution(* com.zhongge.controller.UserController.*(..))")

七、结语:记住三句话

  1. execution:按"地址"匹配,适合有规律的方法(同一个包、同一个类、同一个命名规则)。
  2. @annotation:按"标签"匹配,适合无规律的方法(想增强哪个就加个注解)。
  3. 通配符* 匹配一个,.. 匹配多个,组合使用威力无穷。

掌握了切点表达式,你就掌握了 AOP 的"瞄准镜"------想切哪里,就能切到哪里!

下一篇预告 :我们将学习 Spring AOP 的底层原理------动态代理,了解它是如何在运行时"偷偷"增强你的方法的。敬请期待!

如果觉得有用,别忘了点赞、收藏、关注,我们下期见!

相关推荐
gf13211112 小时前
【python_使用指定应用发送飞书卡片】
java·python·飞书
弹简特2 小时前
【JavaEE28-后端部分】Spring AOP 通知详解——五种“增强时机”,一网打尽
java·spring·spring aop
lulu12165440782 小时前
谷歌Gemma 4实战指南:Apache 2.0开源,移动端AI新时代来临
java·开发语言·人工智能·开源·apache·ai编程
程序员阿明2 小时前
spring boot在普通方法中获取HttpServletRequest及其使用的方式
java·spring boot·后端
花千树-0102 小时前
Spring Boot 启动慢排查与优化实战指南
java·spring boot·后端·spring
小江的记录本2 小时前
【Docker】《 Docker 高频常用命令速查表 》
java·前端·后端·http·docker·容器·eureka
kaixiang3002 小时前
若依RuoYi实战
java·服务器·前端
SunnyDays10112 小时前
使用 Java 高效管理 Excel 分页符:添加、删除与预览全攻略
java·excel分页符
一 乐2 小时前
智能农田管理|基于springboot + vue智能农田管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·智能农田管理系统