声明切点

切点确定感兴趣的连接点,从而使我们能够控制通知何时运行。 Infra AOP 仅支持 Infra bean 的方法执行连接点,因此你可以将切点视为匹配 Infra bean 上方法的执行。 切点声明由两部分组成:包含名称和任何参数的签名,以及确定我们要关注哪些方法执行的切点表达式。 在 AOP 的 @AspectJ 注解风格中,切点签名由常规方法定义提供, 切点表达式由 @Pointcut 注解指示(作为切点签名的方法必须具有 void 返回类型)。

一个例子可能有助于阐明切点签名和切点表达式之间的区别。 下例定义了一个名为 anyOldTransfer 的切点,它匹配任何名为 transfer 的方法的执行:

  • Java

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

构成 @Pointcut 注解值的切点表达式是常规的 AspectJ 切点表达式。 有关 AspectJ 切点语言的完整讨论,请参阅 AspectJ 编程指南 (以及扩展部分的 AspectJ 5 开发人员笔记本) 或有关 AspectJ 的书籍之一(例如 Colyer 等人著的 Eclipse AspectJ,或 Ramnivas Laddad 著的 AspectJ in Action)。

支持的切点指示符

Infra AOP 支持以下 AspectJ 切点指示符(PCD)用于切点表达式:

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

  • within: 将匹配限制为特定类型内的连接点(使用 Infra AOP 时为在匹配类型内声明的方法的执行)。

  • this: 将匹配限制为 bean 引用(Infra AOP 代理)是给定类型的实例的连接点(使用 Infra AOP 时为方法的执行)。

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

  • args: 将匹配限制为参数是给定类型的实例的连接点(使用 Infra AOP 时为方法的执行)。

  • @target: 将匹配限制为执行对象的类具有给定类型的注解的连接点(使用 Infra AOP 时为方法的执行)。

  • @args: 将匹配限制为传递的实际参数的运行时类型具有给定类型的注解的连接点(使用 Infra AOP 时为方法的执行)。

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

  • @annotation: 将匹配限制为连接点的主题(在 Infra AOP 中运行的方法)具有给定注解的连接点。

其他切点类型

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

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

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

由于 Infra AOP 框架基于代理的性质,目标对象内的调用根据定义不会被拦截。 对于 JDK 代理,只能拦截代理上的公共接口方法调用。 对于 CGLIB,代理上的公共和受保护方法调用会被拦截(如果需要,甚至包括包可见方法)。 但是,通过代理的常见交互应始终通过公共签名进行设计。

请注意,切点定义通常与任何拦截的方法匹配。 如果切点严格意味着仅限公共方法,即使在具有通过代理进行潜在非公共交互的 CGLIB 代理场景中,也需要相应地定义它。

如果你的拦截需求包括目标类中的方法调用甚至构造函数,请考虑使用 Infra 驱动的 原生 AspectJ 织入 而不是 Infra 基于代理的 AOP 框架。 这构成了具有不同特征的不同 AOP 使用模式,因此在做出决定之前请务必熟悉织入。

Infra AOP 还支持一个名为 bean 的附加 PCD。 此 PCD 允许你将连接点的匹配限制为特定的命名 Infra bean 或一组命名 Infra bean(使用通配符时)。 bean PCD 具有以下形式:

bean(idOrNameOfBean)

idOrNameOfBean 标记可以是任何 Infra bean 的名称。 提供了使用 * 字符的有限通配符支持,因此,如果你为 Infra bean 建立一些命名约定, 你可以编写 bean PCD 表达式来选择它们。 与其他切点指示符一样,bean PCD 也可以与 && (与)、|| (或) 和 ! (非) 运算符一起使用。

bean PCD 仅在 Infra AOP 中受支持,在原生 AspectJ 织入中不受支持。 它是对 AspectJ 定义的标准 PCD 的 Infra 特定扩展,因此不可用于在 @Aspect 模型中声明的切面。

bean PCD 在实例级别(基于 Infra bean 名称概念)操作,而不是仅在类型级别(基于织入的 AOP 仅限于此)操作。 基于实例的切点指示符是 Infra 基于代理的 AOP 框架及其与 Infra bean 工厂紧密集成的特殊能力, 通过名称识别特定 bean 是自然而直接的。

组合切点表达式

你可以使用 &&||! 组合切点表达式。你也可以按名称引用切点表达式。 下例显示了三个切点表达式:

  • Java

package com.xyz;

public class Pointcuts {

  @Pointcut("execution(public * *(..))")
  public void publicMethod() {} (1)

  @Pointcut("within(com.xyz.trading..*)")
  public void inTrading() {} (2)

  @Pointcut("publicMethod() && inTrading()")
  public void tradingOperation() {} (3)
}
1 publicMethod 匹配如果方法执行连接点表示任何公共方法的执行。
2 inTrading 匹配如果方法执行在交易模块中。
3 tradingOperation 匹配如果方法执行表示交易模块中的任何公共方法。

最佳实践是像上面那样用较小的_命名切点_构建更复杂的切点表达式。 当按名称引用切点时,适用正常的 Java 可见性规则(你可以看到同一类型中的 private 切点、 层次结构中的 protected 切点、任何地方的 public 切点等)。 可见性不影响切点匹配。

共享命名切点定义

在处理企业应用程序时,开发人员通常需要从多个切面引用应用程序的模块和特定的操作集。 为此,我们建议定义一个专用类来捕获常用的_命名切点_表达式。 此类通常类似于以下 CommonPointcuts 示例(尽管类的命名取决于你):

  • Java

package com.xyz;

import org.aspectj.lang.annotation.Pointcut;

public class CommonPointcuts {

  /**
   * 如果方法定义在 com.xyz.web 包或其下的任何子包中的类型中,
   * 则连接点在 web 层中。
   */
  @Pointcut("within(com.xyz.web..*)")
  public void inWebLayer() {}

  /**
   * 如果方法定义在 com.xyz.service 包或其下的任何子包中的类型中,
   * 则连接点在服务层中。
   */
  @Pointcut("within(com.xyz.service..*)")
  public void inServiceLayer() {}

  /**
   * 如果方法定义在 com.xyz.dao 包或其下的任何子包中的类型中,
   * 则连接点在数据访问层中。
   */
  @Pointcut("within(com.xyz.dao..*)")
  public void inDataAccessLayer() {}

  /**
   * 业务服务是在服务接口上定义的任何方法的执行。
   * 此定义假设接口放置在 "service" 包中,并且实现类型在子包中。
   *
   * 如果你按功能区域对服务接口进行分组(例如,
   * 在包 com.xyz.abc.service 和 com.xyz.def.service 中),
   * 则可以使用切点表达式 "execution(* com.xyz..service.*.*(..))"。
   *
   * 或者,你可以使用 'bean' PCD 编写表达式,如 "bean(*Service)"。
   * (这假设你以一致的方式命名了 Infra 服务 bean。)
   */
  @Pointcut("execution(* com.xyz..service.*.*(..))")
  public void businessService() {}

  /**
   * 数据访问操作是在 DAO 接口上定义的任何方法的执行。
   * 此定义假设接口放置在 "dao" 包中,并且实现类型在子包中。
   */
  @Pointcut("execution(* com.xyz.dao.*.*(..))")
  public void dataAccessOperation() {}

}

你可以在任何需要切点表达式的地方引用此类中定义的切点,方法是引用类的完全限定名称结合 @Pointcut 方法的名称。 例如,要使服务层具有事务性,你可以编写以下引用 com.xyz.CommonPointcuts.businessService() _命名切点_的内容:

<aop:config>
  <aop:advisor
    pointcut="com.xyz.CommonPointcuts.businessService()"
    advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
  <tx:attributes>
    <tx:method name="*" propagation="REQUIRED"/>
  </tx:attributes>
</tx:advice>

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

示例

Infra AOP 用户可能最常使用 execution 切点指示符。 执行表达式的格式如下:

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

除了返回类型模式(前面片段中的 ret-type-pattern)、名称模式和参数模式外,所有部分都是可选的。 返回类型模式确定方法的返回类型必须是什么才能匹配连接点。 * 最常用作返回类型模式。它匹配任何返回类型。 完全限定的类型名称仅当方法返回给定类型时才匹配。 名称模式匹配方法名称。你可以使用 * 通配符作为名称模式的全部或一部分。 如果你指定声明类型模式,请包含后缀 . 以将其连接到名称模式组件。 参数模式稍微复杂一些:() 匹配不带参数的方法,而 (..) 匹配任意数量(零个或多个)的参数。 (*) 模式匹配接受一个任何类型参数的方法。 (*,String) 匹配接受两个参数的方法。第一个可以是任何类型,而第二个必须是 String。 有关更多信息,请参阅 AspectJ 编程指南的 语言语义 部分。

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

  • 任何公共方法的执行:

    execution(public * *(..))
  • 名称以 set 开头的任何方法的执行:

    execution(* set*(..))
  • AccountService 接口定义的任何方法的执行:

    execution(* com.xyz.service.AccountService.*(..))
  • service 包中定义的任何方法的执行:

    execution(* com.xyz.service.*.*(..))
  • service 包或其子包之一中定义的任何方法的执行:

    execution(* com.xyz.service..*.*(..))
  • service 包内的任何连接点(在 Infra AOP 中仅限方法执行):

    within(com.xyz.service.*)
  • service 包或其子包之一内的任何连接点(在 Infra AOP 中仅限方法执行):

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

    this(com.xyz.service.AccountService)
    this 更常用于绑定形式。有关如何使代理对象在通知体中可用的信息, 请参阅 声明通知 部分。
  • 目标对象实现 AccountService 接口的任何连接点(在 Infra AOP 中仅限方法执行):

    target(com.xyz.service.AccountService)
    target 更常用于绑定形式。有关如何使目标对象在通知体中可用的信息, 请参阅 声明通知 部分。
  • 接受单个参数且运行时传递的参数是 Serializable 的任何连接点(在 Infra AOP 中仅限方法执行):

    args(java.io.Serializable)
    args 更常用于绑定形式。有关如何使方法参数在通知体中可用的信息, 请参阅 声明通知 部分。

    请注意,此示例中给出的切点与 execution(* *(java.io.Serializable)) 不同。 args 版本在运行时传递的参数是 Serializable 时匹配,而 execution 版本在方法签名声明单个 Serializable 类型参数时匹配。

  • 目标对象具有 @Transactional 注解的任何连接点(在 Infra AOP 中仅限方法执行):

    @target(infra.transaction.annotation.Transactional)
    你也可以在绑定形式中使用 @target。有关如何使注解对象在通知体中可用的信息, 请参阅 声明通知 部分。
  • 目标对象的声明类型具有 @Transactional 注解的任何连接点(在 Infra AOP 中仅限方法执行):

    @within(infra.transaction.annotation.Transactional)
    你也可以在绑定形式中使用 @within。有关如何使注解对象在通知体中可用的信息, 请参阅 声明通知 部分。
  • 执行方法具有 @Transactional 注解的任何连接点(在 Infra AOP 中仅限方法执行):

    @annotation(infra.transaction.annotation.Transactional)
    你也可以在绑定形式中使用 @annotation。有关如何使注解对象在通知体中可用的信息, 请参阅 声明通知 部分。
  • 接受单个参数,并且传递的参数的运行时类型具有 @Classified 注解的任何连接点(在 Infra AOP 中仅限方法执行):

    @args(com.xyz.security.Classified)
    你也可以在绑定形式中使用 @args。有关如何使注解对象在通知体中可用的信息, 请参阅 声明通知 部分。
  • 名为 tradeService 的 Infra bean 上的任何连接点(在 Infra AOP 中仅限方法执行):

    bean(tradeService)
  • 名称匹配通配符表达式 *Service 的 Infra bean 上的任何连接点(在 Infra AOP 中仅限方法执行):

    bean(*Service)

编写好的切点

在编译期间,AspectJ 处理切点以优化匹配性能。 检查代码并确定每个连接点是否匹配(静态或动态)给定的切点是一个代价高昂的过程。 (动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中放置测试以确定代码运行时是否存在实际匹配)。 首次遇到切点声明时,AspectJ 会将其重写为用于匹配过程的最佳形式。这意味着什么? 基本上,切点被重写为 DNF(析取范式),并且切点的组件被排序,以便首先检查那些评估成本较低的组件。 这意味着你不必担心了解各种切点指示符的性能,并且可以在切点声明中以任何顺序提供它们。

但是,AspectJ 只能处理它被告知的内容。为了获得最佳的匹配性能,你应该考虑你想要通过定义实现什么, 并尽可能缩小定义的匹配搜索空间。现有的指示符自然分为三组:类型(kinded)、作用域(scoping)和上下文(contextual):

  • 类型指示符选择特定类型的连接点: execution, get, set, call, 和 handler

  • 作用域指示符选择一组感兴趣的连接点(可能是多种类型):withinwithincode

  • 上下文指示符根据上下文进行匹配(并可选地绑定): this, target, 和 @annotation

编写良好的切点应至少包含前两种类型(类型和作用域)。 你可以包含上下文指示符以根据连接点上下文进行匹配或绑定该上下文以在通知中使用。 仅提供类型指示符或仅提供上下文指示符也可以工作,但由于额外的处理和分析,可能会影响织入性能(使用的时间和内存)。 作用域指示符的匹配速度非常快,使用它们意味着 AspectJ 可以非常快速地排除不应进一步处理的连接点组。 如果可能,好的切点应始终包含一个作用域指示符。