
开篇:快递员怎么知道把包裹送到你家?
老铁们,你有没有想过一个问题:快递员每天要送几百个包裹,他是怎么知道哪个包裹送到哪一家的?
答案是:地址。地址写得越详细,快递员就越容易找到你家。
中国→ 范围太大了,不知道哪个省中国贵州省→ 范围缩小了一点中国贵州省黔南布依族苗族自治州→ 更具体了中国贵州省黔南布依族苗族自治州××小区1号楼202室→ 精准定位
在 Spring AOP 中,切点表达式就相当于这个"地址"。它告诉 AOP:你要对哪些方法进行增强?是所有的 Controller 方法,还是某个包下的所有类,还是某个特定的方法?
本期,我们就来学习切点表达式的语法规则,让你能够精准定位任何你想要增强的方法。
一、切点表达式是什么?
1.1 官方定义
切点表达式(Pointcut Expression)是一组规则,用来匹配一个或多个连接点(方法)。它的作用就是告诉 AOP:"我要对哪些方法下手"。
1.2 生活化理解
想象你是一个快递公司的调度员,你要给快递员分派任务:
- "把本区域所有快递都送过去" → 匹配所有方法
- "只送图书相关的快递" → 匹配
controller包下的方法 - "只送给张三的快递" → 匹配
addBook方法 - "只送贵州省内的快递" → 匹配
com.zhongge包下的方法
切点表达式就是这种"指令",它用一套固定的语法来编写。
二、最常用的切点表达式:execution
execution 译为执行,aop中表示 匹配方法执行
2.1 基本语法
java
execution(访问修饰符 返回类型 包名.类名.方法名(参数) 异常)
其中:
- 访问修饰符 :
public、private等,可以省略 - 异常:可以省略
最简形式:
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 |
匹配 BookController、UserController 等 |
| 方法名 | * |
任意方法名 |
| 参数(一个) | (*) |
任意一个参数 |
示例:
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.controller、com.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.*(..))")
七、结语:记住三句话
execution:按"地址"匹配,适合有规律的方法(同一个包、同一个类、同一个命名规则)。@annotation:按"标签"匹配,适合无规律的方法(想增强哪个就加个注解)。- 通配符 :
*匹配一个,..匹配多个,组合使用威力无穷。
掌握了切点表达式,你就掌握了 AOP 的"瞄准镜"------想切哪里,就能切到哪里!
下一篇预告 :我们将学习 Spring AOP 的底层原理------动态代理,了解它是如何在运行时"偷偷"增强你的方法的。敬请期待!
如果觉得有用,别忘了点赞、收藏、关注,我们下期见!