声明切入点

切入点决定了感兴趣的连接点,从而使我们能够控制通知何时运行。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 开发者手册)或 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 不支持的其他切入点指示符:callgetsetpreinitializationstaticinitializationinitializationhandleradviceexecutionwithincodecflowcflowbelowif@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 还支持一个名为 bean 的额外 PCD。此 PCD 允许你将连接点的匹配限制为特定的命名 Spring bean 或一组命名 Spring bean(在使用通配符时)。bean PCD 具有以下形式:

bean(idOrNameOfBean)

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

bean PCD 仅在 Spring AOP 中受支持,而在 原生 AspectJ 织入中不受支持。它是 Spring 对 AspectJ 定义的标准 PCD 的特定扩展,因此,对于在 @Aspect 模型中声明的方面不可用。 bean PCD 在实例级别(基于 Spring bean 名称 概念)而不是仅在类型级别(织入式 AOP 的限制)操作。 基于实例的切入点指示符是 Spring 基于代理的 AOP 框架及其与 Spring bean 工厂紧密集成的一项特殊功能,在 Spring bean 工厂中,通过名称识别特定 bean 是自然而直接的。

组合切入点表达式

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

  • Java

  • Kotlin

package com.xyz;

public class Pointcuts {

	@Pointcut("execution(public * *(..))")
	public void publicMethod() {} [id="CO1-1"][id="CO1-1"][id="CO1-1"](1)

	@Pointcut("within(com.xyz.trading..*)")
	public void inTrading() {} [id="CO1-2"][id="CO1-2"][id="CO1-2"](2)

	@Pointcut("publicMethod() && inTrading()")
	public void tradingOperation() {} [id="CO1-3"][id="CO1-3"][id="CO1-3"](3)
}
1 如果方法执行连接点表示任何公共方法的执行,则 publicMethod 匹配。
2 如果方法执行在交易模块中,则 inTrading 匹配。
3 如果方法执行表示交易模块中的任何公共方法,则 tradingOperation 匹配。
package com.xyz

class Pointcuts {

	@Pointcut("execution(public * *(..))")
	fun publicMethod() {} [id="CO2-1"][id="CO1-4"][id="CO1-4"](1)

	@Pointcut("within(com.xyz.trading..*)")
	fun inTrading() {} [id="CO2-2"][id="CO1-5"][id="CO1-5"](2)

	@Pointcut("publicMethod() && inTrading()")
	fun tradingOperation() {} [id="CO2-3"][id="CO1-6"][id="CO1-6"](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(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

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

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

  • 任何公共方法的执行:[indent="0",subs="verbatim"] execution(public * *(..))

  • 任何名称以 set 开头的方法的执行:[indent="0",subs="verbatim"] execution(* set*(..))

  • AccountService 接口定义的任何方法的执行:[indent="0",subs="verbatim"] execution(* com.xyz.service.AccountService.*(..))

  • service 包中定义的任何方法的执行:[indent="0",subs="verbatim"] execution(* com.xyz.service..(..))

  • 在 service 包或其子包之一中定义的任何方法的执行:[indent="0",subs="verbatim"] execution(* com.xyz.service...(..))

  • 服务包内的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] within(com.xyz.service.*)

  • 服务包或其子包之一内的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] within(com.xyz.service..*)

  • 代理实现 AccountService 接口的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] this(com.xyz.service.AccountService)

this 更常以绑定形式使用。有关如何在通知体中使代理对象可用,请参阅 声明通知 部分。

  • 目标对象实现 AccountService 接口的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] target(com.xyz.service.AccountService)

target 更常以绑定形式使用。有关如何在通知体中使目标对象可用,请参阅 声明通知 部分。

  • 接受单个参数且运行时传入的参数是 Serializable 的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] args(java.io.Serializable)

args 更常以绑定形式使用。有关如何在通知体中使方法参数可用,请参阅 声明通知 部分。

请注意,此示例中给出的切入点与 execution(* *(java.io.Serializable)) 不同。args 版本在运行时传入的参数是 Serializable 时匹配,而 execution 版本在方法签名声明单个 Serializable 类型参数时匹配。 * 目标对象具有 @Transactional 注解的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] @target(org.springframework.transaction.annotation.Transactional)

你也可以以绑定形式使用 @target。有关如何在通知体中使注解对象可用,请参阅 声明通知 部分。

  • 目标对象的声明类型具有 @Transactional 注解的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] @within(org.springframework.transaction.annotation.Transactional)

你也可以以绑定形式使用 @within。有关如何在通知体中使注解对象可用,请参阅 声明通知 部分。

  • 执行方法具有 @Transactional 注解的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] @annotation(org.springframework.transaction.annotation.Transactional)

你也可以以绑定形式使用 @annotation。有关如何在通知体中使注解对象可用,请参阅 声明通知 部分。

  • 接受单个参数,并且传入参数的运行时类型具有 @Classified 注解的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] @args(com.xyz.security.Classified)

你也可以以绑定形式使用 @args。有关如何在通知体中使注解对象可用,请参阅 声明通知 部分。

  • 在名为 tradeService 的 Spring bean 上的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] bean(tradeService)

  • 在名称与通配符表达式 *Service 匹配的 Spring bean 上的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] bean(*Service)

编写好的切入点

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

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

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

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

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

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