声明切点

切点确定感兴趣的连接点,从而使我们能够控制何时运行建议。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 工厂的紧密集成的一种特殊功能,在该工厂中,通过名称标识特定 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 更常用于绑定形式。有关如何在 advice 主体中使用代理对象,请参见关于声明 Advice的部分。
  • 目标对象实现AccountService接口的任何连接点(在 Spring AOP 中仅限方法执行)

    target(com.xyz.service.AccountService)
    target 更常用于绑定形式。有关如何在 advice 主体中使用目标对象,请参见声明 Advice部分。
  • 带有一个参数且在运行时传递的参数为Serializable的任何连接点(在 Spring AOP 中仅限方法执行)

    args(java.io.Serializable)
    args 更常用于绑定形式。有关如何在 advice 主体中使用方法参数,请参见声明 Advice部分。

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

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

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

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

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

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

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

    bean(*Service)

编写良好的切入点

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

但是,AspectJ 只能使用它被告知的内容。为了获得最佳匹配性能,您应该考虑您试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为三类:种类、范围和上下文

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

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

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

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