Pointcuts 确定感兴趣的连接点( join points),从而使我们能够控制 advice 的运行时间。Spring AOP 只支持 Spring Bean 的 方法执行连接点,所以你可以把 pointcut 看作是对 Spring Bean 上的 方法执行的匹配。一个切点声明有两个部分:一个由名称和任何参数组成的签名,以及一个切点表达式,它决定了我们到底对哪些方法的执行感兴趣。在 AOP 的 @AspectJ 注解式中,一个切点签名是由普通的方法定义提供的,而切点表达式是通过使用 @Pointcut 注解来表示的(作为切点签名的方法必须有一个 void 返回类型)。

一个例子可以帮助我们明确区分切点签名和切点表达式的区别。下面的例子定义了一个名为 anyOldTransfer 的切点,它匹配任何名为 transfer 的方法的执行。

  1. @Pointcut("execution(* transfer(..))") // 切入点表达式
  2. private void anyOldTransfer() {} // 切入点签名

构成 @Pointcut 注解的值的 pointcut 表达式是一个常规的 AspectJ pointcut 表达式。关于 Aspect J的 pointcut 语言的全面讨论,请参见《AspectJ 编程指南》(以及 AspectJ 5 Developer’s Notebook 的扩展部分)或关于 AspectJ 的书籍之一(如 Colye r等人的《Eclipse AspectJ》,或 Ramnivas Laddad 的《AspectJ in Action》)。

支持切入点的指示器

Spring AOP 支持以下 AspectJ 的切点指定器(PCD),以便在切点表达式中使用:

  • execution:用于匹配 方法执行 的连接点。这是在使用 Spring AOP 时要使用的主要切点指定符。

  • within:将匹配限制在 某些类 的连接点上(当使用 Spring AOP 时,在匹配类型中声明的方法的执行)。

  • this:限制匹配到连接点(使用 Spring AOP 时方法的执行),其中 Bean 引用(Spring AOP 代理)是给定类型的实例。

  • target:限制对连接点的匹配(使用 Spring AOP 时方法的执行),目标对象(被代理的应用程序对象)是给定类型的实例。

  • args:限制对连接点的匹配(使用 Spring AOP 时方法的执行),参数是给定类型的实例,指定参数类型和参数数量

  • @target:限制对连接点的匹配(使用 Spring AOP 时方法的执行),其中执行对象的类有一个给定类型的注解。

  • @args: 限制对连接点的匹配(使用 Spring AOP 时方法的执行),其中实际传递的参数的运行时间类型有给定类型的注解。

  • @within: 限制匹配到具有给定注解的类型中的连接点(使用 Spring AOP 时,执行在具有给定注解的类型中声明的方法)。

  • @annotation:限制匹配到连接点的主体(在 Spring AOP 中被运行的方法)具有给定注解的连接点。

切点表达式的类型还是比较多的,官方文档感觉描述也不是特别的清楚也有可能是翻译的问题,可以参考下这篇博客

:::info 其他 pointcut 类型

完整的 AspectJ pointcut 语言支持 Spring 不支持的其他 pointcut 指定符:call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow , if, @this, @withincode 。在 Spring AOP 解释的 pointcut 表达式中使用这些 pointcut 指定符会导致抛出 IllegalArgumentException。

Spring AOP 支持的切点符号集可能会在未来的版本中扩展,以支持更多 AspectJ 的切点符号。 :::

因为 Spring AOP 将匹配限制在方法的执行连接点上,所以前面对切点代号的讨论给出的定义比你在 AspectJ 编程指南中找到的要窄。此外, AspectJ 本身有基于类型的语义,在执行连接点,this 和 target 都指同一个对象:执行方法的对象。Spring AOP 是一个基于代理的系统,区分了代理对象本身(与 this 绑定)和代理背后的目标对象(与 target 绑定)。

:::info 由于 Spring的AOP 框架是基于代理的,根据定义,目标对象内部的调用是不会被拦截的。对于 JDK 代理,只有代理上的公共接口方法调用可以被拦截。使用 CGLIB,代理上的 public 和 protected 的方法调用都可以被拦截(如果需要的话,甚至可以拦截包中的可见方法)。然而,通过代理的普通交互应该始终通过 public 签名来设计。

需要注意的是,pointcut 的定义通常会与任何被拦截的方法相匹配。如果一个切点是严格意义上的只公开的,即使是在 CGLIB 代理场景下,通过代理进行潜在的非公开交互,它也需要相应地被定义。

如果你的拦截需求包括目标类中的方法调用甚至构造函数,可以考虑使用 Spring 驱动的本地 AspectJ 编织,而不是 Spring 的基于代理的 AOP 框架。这构成了不同的 AOP 使用模式,具有不同的特点,所以在做决定之前一定要让自己熟悉织构。 :::

Spring AOP 还支持一个额外的名为 bean 的 PCD。这个 PCD 允许你将连接点的匹配限制在一个特定的命名的 Spring Bean 或命名的 Spring Bean 的集合(当使用通配符时)。bean PCD 有以下形式:

  1. bean(idOrNameOfBean)

id 或 NameOfBean 标记可以是任何 Spring Bean 的名称。我们提供了使用 *字符的有限通配符支持,因此,如果你为你的 Spring Bean 建立了一些命名惯例,你可以写一个 bean PCD 表达式来选择它们。就像其他的 pointcut 符号一样,Bean PCD 也可以和 &&(和)、||(或)、和 !(否定)运算符一起使用。

:::info 仅在 Spring AOP 中支持 bean PCD,而在本地 AspectJ 织构中不支持。它是对 AspectJ 定义的标准 PCD 的一个特定的 Spring 扩展,因此不适用于 @Aspect 模型中声明的切面。

Bean PCD 在实例级(建立在 Spring Bean 名称概念的基础上)而不是仅在类型级(基于织构的 AOP 仅限于此)进行操作。基于实例的切点指定器是 Spring 基于代理的 AOP 框架的一种特殊能力,它与 Spring Bean 工厂紧密结合,通过名称来识别特定的 bean 是很自然和直接的。 :::

组合 pointcut 表达式

你可以通过使用 &&||!来组合 pointcut 表达式。你也可以通过名称来引用 pointcut 表达式。下面的例子显示了三个切点表达式:

  1. @Pointcut("execution(public * *(..))")
  2. private void anyPublicOperation() {}
  3. @Pointcut("within(com.xyz.myapp.trading..*)")
  4. private void inTrading() {}
  5. @Pointcut("anyPublicOperation() && inTrading()")
  6. private void tradingOperation() {}
  • anyPublicOperation:如果一个方法的执行连接点代表了任何 public 方法的执行,则与之匹配任何 public 方法的执行。
  • inTrading: 如果一个方法的执行是在 trading 模块中,则匹配。
  • tradingOperation:前面两个条件都满足这与之匹配

正如前面所显示的那样,从较小的命名组件中建立更复杂的 pointcut 表达式是一种最佳做法。当通过名字引用点切时,正常的 Java 可见性规则适用(你可以在同一类型中看到 private 切点,在层次结构中看到 protected 的切点,在任何地方看到 public 切点,等等)。可见性并不影响切点的匹配。

共享通用切入点定义

在处理企业应用程序时,开发人员经常想从几个切面来引用应用程序的模块和特定的操作集。我们建议定义一个 CommonPointcuts 切面,它可以为这个目的捕获常见的 pointcut 表达。这样的一个切面通常类似于下面的例子:

  1. package cn.mrcode.study.springdocsread.aspect;
  2. import org.aspectj.lang.annotation.Pointcut;
  3. /**
  4. * 公共切入点
  5. *
  6. * @author mrcode
  7. */
  8. public class CommonPointcuts {
  9. /**
  10. * 如果方法是在 com.xyz.myapp.web 包或该包下的任何子包中的类型中定义的,则连接点位于 Web 层中。
  11. */
  12. @Pointcut("within(com.xyz.myapp.web..*)")
  13. public void inWebLayer() {
  14. }
  15. /**
  16. * 如果方法在 com.xyz.myapp.service 包或该包下的任何子包中的类型中定义,则连接点位于 service 中。
  17. */
  18. @Pointcut("within(com.xyz.myapp.service..*)")
  19. public void inServiceLayer() {
  20. }
  21. /**
  22. * 如果方法在 com.xyz.myapp.dao 包或该包下的任何子包中的类型中定义,则连接点位于数据访问(dao)层中。
  23. */
  24. @Pointcut("within(com.xyz.myapp.dao..*)")
  25. public void inDataAccessLayer() {
  26. }
  27. /**
  28. * <pre>
  29. * 业务服务是定义在服务接口上的任何方法的执行。这个定义假设接口放在 “service” 包中,实现类型放在子包中。
  30. *
  31. * 如果按功能区域对服务接口进行分组(例如,在包 com.xyz.myapp.abc.service 和 com.xyz.myapp.def.service
  32. * 中),那么切入点表达式 “execution(* com.xyz.myapp..service .*.*(..))" 可以代替使用。
  33. * 或者,您可以使用 “bean” PCD 编写表达式,例如 “bean(*Service)”。 (这假设您以一致的方式命名了 Spring 服务 bean。)
  34. * </pre>
  35. */
  36. @Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
  37. public void businessService() {
  38. }
  39. /**
  40. * 数据访问操作是定义在 dao 接口上的任何方法的执行。这个定义假定接口放在 “dao” 包中,实现类型放在子包中。
  41. */
  42. @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
  43. public void dataAccessOperation() {
  44. }
  45. }

你可以在任何需要 pointcut 表达式的地方引用在这样一个切面定义的 pointcut 。例如,为了使服务层成为事务性的,你可以这样写:

  1. <aop:config>
  2. <aop:advisor
  3. pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
  4. advice-ref="tx-advice"/>
  5. </aop:config>
  6. <tx:advice id="tx-advice">
  7. <tx:attributes>
  8. <tx:method name="*" propagation="REQUIRED"/>
  9. </tx:attributes>
  10. </tx:advice>

<aop:config><aop: advisor> 元素在 基于 xml 的 AOP 支持中讨论事务元素在事务管理中讨论

例子

Spring AOP 用户可能最常使用的是执行点指定器。执行表达式的格式如下:

  1. execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

除了返回类型模式(前面片段中的 ret-type-pattern)、name-pattern 和 param-pattern 之外的所有部分都是可选的。返回类型模式决定了方法的返回类型必须是什么,以使连接点被匹配。

  • modifiers-pattern:方法的可见性,如 public,protected;
  • ret-type-pattern / 方法返回值类型:*是最常被用作返回类型模式的。它匹配任何返回类型。一个完全限定的类型名称只在方法返回给定的类型时匹配。
  • declaring-type-pattern / 方法所在类的全路径:如 com.spring.Aspect;
  • name-pattern / 名称模式:匹配方法名称。你可以使用 *通配符作为名称模式的全部或部分。如果你指定了一个 declaring-type-pattern ,请在它的尾部使用一个 . 来链接 name-pattern 模式。
  • param-pattern / 参数模式:稍微复杂一些
    • ()匹配一个不需要参数的方法,
    • (...)匹配任何数量(零或更多)的参数。
    • (*)模式匹配一个需要任何类型的参数的方法。
    • (*,String)匹配一个需要两个参数的方法。第一个参数可以是任何类型,而第二个参数必须是一个字符串。

更多信息请参考《AspectJ 编程指南》的语言语义部分。

下面的例子显示了一些常见的切入点表达式:

  • 任何 public 方法
    1. // 这里使用了 方法可见性 返回值类型 方法名称模式 参数模式
    2. // 含义就是,任何返回值,任何方法,任何参数类型 的 public 方法
    3. execution(public * *(..))
  • 名称以 set 开头的任何方法

    1. // 返回值类型 方法名称模式 参数模式
    2. execution(* set*(..))
  • AccountService 接口中的任何方法

    1. // 返回值类型 类路径 方法名称(类路径和方法名称后面使用 . 链接) 方法常数
    2. execution(* com.xyz.service.AccountService.*(..))
  • service 包中定义的任何方法

    1. // 返回值类型 类路径 方法名称 方法参数
    2. // com.xyz.service.*.* 这里是表示 service 下的任何类的任何方法,所以是 .*.*
    3. execution(* com.xyz.service.*.*(..))
  • service 包或则子包下的任何方法

    1. // 返回值类型 类路径 方法名称 方法参数
    2. execution(* com.xyz.service..*.*(..))
  • service 包内的任何连接点(仅在 Spring AOP 中方法执行)

    1. within(com.xyz.service.*)
  • service 包或子包下的任何方法(仅在 Spring AOP 中方法执行)

    1. within(com.xyz.service..*)
  • 代理实现 AccountService 接口的任何连接点(仅在 Spring AOP 中执行方法):

    1. this(com.xyz.service.AccountService)

    this 更常用于绑定形式。关于如何使代理对象在 advice 中可用,请参见声明 advice 的部分。

  • 任何连接点(仅在 Spring AOP 中的方法执行),目标对象实现了 AccountService 接口

    1. target(com.xyz.service.AccountService)

    target更常用于绑定形式。关于如何使代理对象在 advice 中可用,请参见声明 advice 的部分。

  • 任何连接点(仅在 Spring AOP 中的方法执行)都需要一个参数,并且在运行时传递的参数是可序列化的。

    1. // 接受一个参数,并且是可序列化的方法
    2. args(java.io.Serializable)

    请注意,本例中给出的 pointcut 语句与 execution(* *(java.io.Serializable))不同。如果在运行时传递的参数是 Serializable,则 args 版本匹配,如果方法签名声明了一个 Serializable 类型的单一参数,则执行版本匹配。

  • 任何连接点(仅在 Spring AOP 中的方法执行),目标对象有 @Transactional注解。

    1. @target(org.springframework.transaction.annotation.Transactional)
  • 任何连接点(仅在 Spring AOP 中的方法执行),其中目标对象的声明类型有一个 @Transactional 注解

    1. @within(org.springframework.transaction.annotation.Transactional)
  • 任何连接点(仅在 Spring AOP 中的方法执行),其中执行的方法有一个 @Transactional 注解。

    1. @annotation(org.springframework.transaction.annotation.Transactional)
  • 任何连接点(仅在 Spring AOP 中的方法执行),它需要一个参数,并且所传递的参数的运行时类型具有 @Classified 注解。

    1. @args(com.xyz.security.Classified)
  • 在一个名为 tradeService 的 Spring Bean 上的任何连接点(仅在 Spring AOP 中方法执行)

    1. bean(tradeService)
  • 在 Spring Bean 上的任何连接点(仅在 Spring AOP 中执行方法),其名称与通配符表达式 *Service相匹配

    1. bean(*Service)

编写好的切入点

在编译过程中,AspectJ 处理 pointcuts,以优化匹配性能。检查代码并确定每个连接点是否匹配(静态或动态)一个给定的 pointcut 是一个昂贵的过程。(动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中放置一个测试,以确定在代码运行时是否存在实际的匹配)。在第一次遇到一个 pointcut 声明时,AspectJ 会将其改写成匹配过程的最佳形式。这是什么意思?基本上,pointcut 语句被重写成 DNF(Disjunctive Normal Form),并且 pointcut 语句的组件被排序,以便首先检查那些评估起来比较便宜的组件。这意味着你不需要担心理解各种切点指定器的性能,可以在切点声明中以任何顺序提供它们。

然而,AspectJ 只能用它被告知的东西来工作。为了获得最佳的匹配性能,你应该思考他们要实现的目标,并在定义中尽可能地缩小匹配的搜索空间。现有的指定器自然分为三组之一:种类、范围和上下文:

  • 种类指示符选择一种特点的连接点:execution(执行), get, set, call, handler.
  • 范围指定器选择了一组感兴趣的连接点(可能有许多种):withinwithincode
  • 上下文指定器根据上下文进行匹配(并可选择绑定):thistarget@annotation

一个写得好的连接点应该至少包括前两种类型(种类和范围)。你可以包括上下文指定符,以便根据连接点的上下文进行匹配,或者将该上下文绑定在 advice 中使用。只提供种类指定符或只提供上下文指定符是可行的,但由于额外的处理和分析,可能会影响织造性能(所用时间和内存)。范围指定符的匹配速度非常快,使用它们意味着 AspectJ 可以非常迅速地排除那些不应该被进一步处理的连接点组。如果可能的话,一个好的连接点应该总是包括一个。