声明切入点

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

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

  • Java

  • Kotlin

@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
@Pointcut("execution(* transfer(..))") // the pointcut expression
private fun anyOldTransfer() {} // the pointcut signature

形成 @Pointcut 注解值的切入点表达式是常规的 AspectJ 切入点表达式。有关 AspectJ 切入点语言的完整讨论,请参阅AspectJ 编程指南(以及扩展部分,AspectJ 5 Developer’s Notebook)或 AspectJ 相关书籍(例如 Colyer 等人著的 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 中运行的方法)的主题具有给定注解的位置。

其他切入点类型

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

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

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

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

请注意,切入点定义通常匹配任何被拦截的方法。即使在 CGLIB 代理场景中可能通过代理进行非公共交互,如果切入点严格意味着只匹配公共方法,则需要相应地定义它。

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

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

bean(idOrNameOfBean)

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

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

bean PCD 在实例级别(基于 Spring bean 名称概念)操作,而不是仅在类型级别(织入式 AOP 的限制)。基于实例的切入点指示符是 Spring 基于代理的 AOP 框架及其与 Spring bean 工厂紧密集成的一个特殊能力,在这种情况下,按名称识别特定 bean 是自然而直接的。

组合切入点表达式

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

  • Java

  • Kotlin

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 匹配交易模块中任何公共方法的执行。
package com.xyz

class Pointcuts {

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

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

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

如上所示,最佳实践是使用较小的命名切入点构建更复杂的切入点表达式。按名称引用切入点时,适用常规的 Java 可见性规则(你可以在同一类型中看到 private 切入点,在继承层级中看到 protected 切入点,在任何地方看到 public 切入点等)。可见性不影响切入点匹配。

共享命名切入点定义

在开发企业应用时,开发者通常需要在多个切面中引用应用的模块和特定的操作集合。为此,我们建议定义一个专门的类来捕获常用的命名切入点表达式。此类通常类似于以下 CommonPointcuts 示例(尽管类的名称由你决定)

  • Java

  • Kotlin

package com.xyz;

import org.aspectj.lang.annotation.Pointcut;

public class CommonPointcuts {

	/**
	 * A join point is in the web layer if the method is defined
	 * in a type in the com.xyz.web package or any sub-package
	 * under that.
	 */
	@Pointcut("within(com.xyz.web..*)")
	public void inWebLayer() {}

	/**
	 * A join point is in the service layer if the method is defined
	 * in a type in the com.xyz.service package or any sub-package
	 * under that.
	 */
	@Pointcut("within(com.xyz.service..*)")
	public void inServiceLayer() {}

	/**
	 * A join point is in the data access layer if the method is defined
	 * in a type in the com.xyz.dao package or any sub-package
	 * under that.
	 */
	@Pointcut("within(com.xyz.dao..*)")
	public void inDataAccessLayer() {}

	/**
	 * A business service is the execution of any method defined on a service
	 * interface. This definition assumes that interfaces are placed in the
	 * "service" package, and that implementation types are in sub-packages.
	 *
	 * If you group service interfaces by functional area (for example,
	 * in packages com.xyz.abc.service and com.xyz.def.service) then
	 * the pointcut expression "execution(* com.xyz..service.*.*(..))"
	 * could be used instead.
	 *
	 * Alternatively, you can write the expression using the 'bean'
	 * PCD, like so "bean(*Service)". (This assumes that you have
	 * named your Spring service beans in a consistent fashion.)
	 */
	@Pointcut("execution(* com.xyz..service.*.*(..))")
	public void businessService() {}

	/**
	 * A data access operation is the execution of any method defined on a
	 * DAO interface. This definition assumes that interfaces are placed in the
	 * "dao" package, and that implementation types are in sub-packages.
	 */
	@Pointcut("execution(* com.xyz.dao.*.*(..))")
	public void dataAccessOperation() {}

}
package com.xyz

import org.aspectj.lang.annotation.Pointcut

class CommonPointcuts {

	/**
	 * A join point is in the web layer if the method is defined
	 * in a type in the com.xyz.web package or any sub-package
	 * under that.
	 */
	@Pointcut("within(com.xyz.web..*)")
	fun inWebLayer() {}

	/**
	 * A join point is in the service layer if the method is defined
	 * in a type in the com.xyz.service package or any sub-package
	 * under that.
	 */
	@Pointcut("within(com.xyz.service..*)")
	fun inServiceLayer() {}

	/**
	 * A join point is in the data access layer if the method is defined
	 * in a type in the com.xyz.dao package or any sub-package
	 * under that.
	 */
	@Pointcut("within(com.xyz.dao..*)")
	fun inDataAccessLayer() {}

	/**
	 * A business service is the execution of any method defined on a service
	 * interface. This definition assumes that interfaces are placed in the
	 * "service" package, and that implementation types are in sub-packages.
	 *
	 * If you group service interfaces by functional area (for example,
	 * in packages com.xyz.abc.service and com.xyz.def.service) then
	 * the pointcut expression "execution(* com.xyz..service.*.*(..))"
	 * could be used instead.
	 *
	 * Alternatively, you can write the expression using the 'bean'
	 * PCD, like so "bean(*Service)". (This assumes that you have
	 * named your Spring service beans in a consistent fashion.)
	 */
	@Pointcut("execution(* com.xyz..service.*.*(..))")
	fun businessService() {}

	/**
	 * A data access operation is the execution of any method defined on a
	 * DAO interface. This definition assumes that interfaces are placed in the
	 * "dao" package, and that implementation types are in sub-packages.
	 */
	@Pointcut("execution(* com.xyz.dao.*.*(..))")
	fun 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 支持中讨论。事务元素在事务管理中讨论。

示例

Spring AOP 用户最常使用 execution 切入点指示符。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 包内的任何连接点(在 Spring AOP 中仅为方法执行)

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

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

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

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

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

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

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

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

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

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

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

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

    bean(*Service)

编写好的切入点

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

然而,AspectJ 只能处理它被告知的内容。为了获得最佳的匹配性能,您应该考虑您想要实现什么,并在定义中尽可能地缩小匹配的搜索空间。现有的指示符自然分为三组:种类指示符(kinded)、范围指示符(scoping)和上下文指示符(contextual)。

  • 种类指示符选择特定种类的连接点:executiongetsetcallhandler

  • 范围指示符选择一组感兴趣的连接点(可能包含多种种类):withinwithincode

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

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