声明切点

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

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

  • Java

  • Kotlin

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

作为@Pointcut注解值的切点表达式是常规的AspectJ切点表达式。有关AspectJ切点语言的完整讨论,请参阅AspectJ编程指南(以及扩展,请参阅AspectJ 5开发者手册)或AspectJ相关书籍(如Colyer等人的Eclipse AspectJ或Ramnivas Laddad的AspectJ实战)。

支持的切点设计器

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编织中受支持。这是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 {

	/**
	 * 如果方法在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)”。(这假定您已经以一致的方式命名了Spring服务bean。)
	 */
	@Pointcut("execution(* com.xyz..service.*.*(..))")
	public void businessService() {}

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

}
package com.xyz

import org.aspectj.lang.annotation.Pointcut

class CommonPointcuts {

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

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

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

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

	/**
	 * 数据访问操作是在DAO接口上定义的任何方法的执行。此定义假定接口放置在“dao”包中,并且实现类型放置在子包中。
	 */
	@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>元素在基于模式的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编程指南的语言语义部分。

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

  • 任何公共方法的执行:

    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在绑定形式中更常用。有关如何在建议主体中使目标对象可用,请参阅声明建议部分。
  • 仅在Spring AOP中的任何连接点(方法执行)中,接受单个参数并且运行时传递的参数是Serializable的连接点:

    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只能处理它所告知的内容。为了获得最佳匹配性能,您应该考虑您试图实现的目标,并尽可能缩小定义中的匹配搜索空间。现有的设计符自然分为三组:种类、范围和上下文:

  • 种类设计符选择特定类型的连接点:executiongetsetcallhandler

  • 范围设计符选择一组感兴趣的连接点(可能是多种类型):withinwithincode

  • 上下文设计符基于上下文匹配(并可选择绑定):thistarget@annotation

一个良好编写的切入点应该至少包括前两种类型(种类和范围)。您可以包括上下文设计符以基于连接点上下文匹配或绑定该上下文以供通知使用。仅提供一种种类设计符或仅提供一种上下文设计符是有效的,但可能会影响织入性能(时间和内存使用),因为需要额外的处理和分析。范围设计符非常快速匹配,使用它们意味着AspectJ可以非常快速地排除不应进一步处理的连接点组。一个良好的切入点应该尽可能包括其中一个。