切点表达式是 AOP 中最容易被“会用但写不精准”的部分。它的本质是告诉 Spring:要对哪些类的哪些方法进行增强。Spring AOP 采用 AspectJ 的切点表达式语法,但只支持方法执行这一个连接点类型(即 execution 最常用),同时搭配少量辅助表达式完成更精确的筛选。
9.2.1 execution 表达式——主力切点描述符
execution 用来匹配方法执行的连接点,语法模板如下:
execution(修饰符? 返回类型 包名.类名?方法名(参数列表) throws 异常?)
其中 ? 表示可选项。一个完整且实用的写法大致是:
execution(public * com.example.service.*.*(..))
拆解其各部分的含义与规则:
- 修饰符:可省略,常见
public、protected,通常不写或写*表示任意。 - 返回类型:必填。
表示任意返回值,也可写具体类型如String、void。和void的区分:如果想匹配所有方法含返回值,用*;只匹配 void 方法,须显式写void。 - 包名:可带通配符。
com.example.service表示该包下的类;com.example..(两个点)表示该包及其所有子包。 - 类名:可带通配符。
UserService精确匹配;Service匹配以 Service 结尾的类;匹配该包下所有类。 - 方法名:与类名类似,
匹配所有方法,find匹配以 find 开头的方法。 - 参数列表:
(..)表示任意参数(零或多个);()表示无参;(*, String)表示第一个参数任意类型、第二个为 String。若需匹配具体类型,须写全限定类名,如(java.util.List, ..)代表第一个参数是 List,后续任意。 - 异常声明:极少使用,一般省略。
直接上常用示例:
- 任意公共方法:
execution(public * *(..))
- service 包下所有类的所有方法:
execution(* com.example.service.*.*(..))
- service 包及其子包下所有类的所有方法:
execution(* com.example.service..*.*(..))
- 以
find开头的方法:
execution(* find*(..))
- 返回类型为 String 且无参的方法:
execution(String *.getName())
- 第一个参数为 Long 的方法:
execution(* *(Long, ..))
日常开发中,最通用的写法就是匹配整个应用的服务层,例如:
@Pointcut("execution(* com.yourcompany..service..*.*(..))")
public void serviceLayer() {}
这条表达式覆盖了 com.yourcompany 下所有子包中 service 包及其子包的所有类的所有方法,既能涵盖核心业务,又排除了控制器、工具类等。
9.2.2 其他常用切点描述符
在实际应用中,execution 往往需要与其他描述符组合使用,实现更精细的控制。
1. within —— 包或类的范围限定
within 按类型匹配,语法简单:
within(包名.类名)
它不关心方法签名,只要是在指定的包或类中即被匹配。例如:
- 匹配
com.example.controller包下的所有方法:
within(com.example.controller.*)
- 匹配
UserService类的所有方法:
within(com.example.service.UserService)
相比 execution,within 的粒度更粗,不能约束方法名和参数,但表达式更简洁,适合“某层所有 Bean”的场景。
2. @annotation —— 自定义注解精确打标
这是生产中最“优雅”的做法:自定义一个注解(如 @Log ),然后在切面上通过 @annotation 匹配带有该注解的方法。
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
String value() default "";
}
// 切面
@Before("@annotation(auditLog)")
public void audit(JoinPoint joinPoint, AuditLog auditLog) {
String operation = auditLog.value();
// 记录审计日志
}
使用 @annotation(全限定名) 与 execution 组合时,注意 @annotation 本身就可以作为切点表达式参数传入,从而获得注解实例。
3. args —— 依据参数类型或值过滤
args 侧重于运行时参数的实际类型。例如:
- 拦截所有参数中包含
Long类型的方法(不考虑顺序):
args(Long, ..)
- 拦截第一个参数是
Order对象的方法,并且可以获得该参数:
@Before("execution(* com.example..*.*(..)) && args(order, ..)")
public void handleOrder(Order order) { ... }
与 execution 不同,args 匹配的是运行时实参类型,可以用于动态绑定参数给通知方法。
4. @target 与 @within —— 类级别注解的匹配
@target(注解):目标对象的类上有指定注解时匹配(动态代理时按实际类型判断)。@within(注解):声明该类型的类上有指定注解时匹配,语义上略有差异,但多数场景相似。
常用于拦截带有 @RestController、@Service 等注解的类的所有方法,但与 within 基于包名不同,它基于注解驱动,适合注解划分清晰的项目。
5. bean —— 按 Bean 名称匹配
这是 Spring AOP 独有的描述符,方便直接指定 Spring 容器中的 Bean:
bean(userService) // 匹配名为 userService 的 Bean 的所有方法
bean(*Service) // 匹配所有名称以 Service 结尾的 Bean
在需要精确控制增强某个 Bean 而不修改其类本身时非常有用。
9.2.3 组合与切点复用
一个切点表达式可以组合多个描述符,用 &&(且)、||(或)、!(非)连接。括号用于控制优先级。
例如,只拦截 service 包中公开的、带有 @Transactional 注解的方法:
execution(public * com.example.service.*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)
对于可复用的切点,建议用 @Pointcut 定义在切面类中,供多个通知引用:
@Aspect
@Component
public class SystemArchitecture {
@Pointcut("execution(* com.example..service..*.*(..))")
public void businessService() {}
@Pointcut("execution(* com.example..controller..*.*(..))")
public void webController() {}
}
@Aspect
@Component
public class LoggingAspect {
@Before("SystemArchitecture.businessService()")
public void logBusiness(JoinPoint jp) { ... }
}
这种“架构切点”的抽取,让大型项目中的切面管理变得清晰且易于维护。
9.2.4 实践中的真实注意事项
- 避免大范围通配:如
execution( (..))会匹配所有 Bean 的所有方法,可能造成性能损耗和意料之外的增强,除非极明确的监控场景。 - 注意代理限制:Spring AOP 基于代理,默认只能拦截 public 方法。private、protected 方法(除非使用 AspectJ 的编译时织入)不会被拦截,表达式写得再准确也无济于事。
- 调试切点匹配:可在测试类中利用
@Autowired获取目标 Bean,观察是否为代理对象,或者使用org.springframework.aop的日志级别 DEBUG 来查看代理创建和匹配信息。 - 参数绑定的性能:
args和@annotation绑定参数时,Spring 会在每次调用时进行参数提取,开销很小,但滥用复杂表达式可能增加一些反射成本,常规业务无需担心。
掌握这些表达式和组合技巧后,切面编程才能真正做到“指哪打哪”。下一节将进入不同类型通知的具体编写与执行顺序,让切面逻辑真正运转起来。