人人都会AI编程

9.2 切点表达式语法与常用写法

更新时间:2026-07-10

切点表达式是 AOP 中最容易被“会用但写不精准”的部分。它的本质是告诉 Spring:要对哪些类的哪些方法进行增强。Spring AOP 采用 AspectJ 的切点表达式语法,但只支持方法执行这一个连接点类型(即 execution 最常用),同时搭配少量辅助表达式完成更精确的筛选。

9.2.1 execution 表达式——主力切点描述符

execution 用来匹配方法执行的连接点,语法模板如下:

execution(修饰符? 返回类型 包名.类名?方法名(参数列表) throws 异常?)

其中 ? 表示可选项。一个完整且实用的写法大致是:

execution(public * com.example.service.*.*(..))

拆解其各部分的含义与规则:

  • 修饰符:可省略,常见 publicprotected,通常不写或写 * 表示任意。
  • 返回类型:必填。 表示任意返回值,也可写具体类型如 Stringvoidvoid 的区分:如果想匹配所有方法含返回值,用 *;只匹配 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)
  

相比 executionwithin 的粒度更粗,不能约束方法名和参数,但表达式更简洁,适合“某层所有 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 会在每次调用时进行参数提取,开销很小,但滥用复杂表达式可能增加一些反射成本,常规业务无需担心。

掌握这些表达式和组合技巧后,切面编程才能真正做到“指哪打哪”。下一节将进入不同类型通知的具体编写与执行顺序,让切面逻辑真正运转起来。