跳到主要内容
版本:7.0.3

声明切点(Pointcut)

Hunyuan 7b 中英对照 Declaring a Pointcut

切点(Pointcuts)决定了我们感兴趣的连接点(join points),从而让我们能够控制通知(advice)何时执行。Spring AOP仅支持针对Spring Bean的方法执行连接点,因此可以将切点视为与Spring Bean上的方法执行相匹配的机制。切点声明包含两部分:一部分是签名(signature),由名称和任何参数组成;另一部分是切点表达式(pointcut expression),用于精确指定我们感兴趣的方法执行。在AOP的@AspectJ注解风格中,切点签名通过常规的方法定义来提供,而切点表达式则通过使用@Pointcut注解来表示(作为切点签名的方法必须具有void返回类型)。

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

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

构成@Pointcut注解值的点切表达式(pointcut expression)是一种常规的AspectJ点切表达式。有关AspectJ点切语言的完整说明,请参阅AspectJ编程指南(对于扩展功能,可参考AspectJ 5开发者手册),或者相关AspectJ书籍,例如Colyer等人编写的《Eclipse AspectJ》,以及Ramnivas Laddad所著的《AspectJ in Action》。

支持的切点标识符

Spring AOP支持以下AspectJ切点标识符(Pointcut Designators, PCD)用于切点表达式中:

  • execution:用于匹配方法执行切点。这是在使用Spring AOP时主要使用的切点限定符。

  • within:限制匹配在特定类型内的切点(即在使用Spring AOP时,匹配在某个类型内部声明的方法的执行处)。

  • this:限制匹配在执行方法时,bean引用(Spring AOP代理)是给定类型的实例的切点。

  • target:限制匹配在执行方法时,目标对象(被代理的应用对象)是给定类型的实例的切点。

  • args:限制匹配在执行方法时,参数是给定类型实例的切点。

  • @target:限制匹配在执行方法时,执行对象的类带有给定类型注解的切点。

  • @args:限制匹配在执行方法时,实际传递的参数的运行时类型带有给定类型注解的切点。

  • @within:限制匹配在带有给定注解的类型内的切点(即在使用Spring AOP时,匹配在带有该注解的类型中声明的方法的执行处)。

  • @annotation:限制匹配在执行方法时,该方法(在Spring AOP中运行的方法)带有给定注解的切点。

其他切点类型

AspectJ 的完整切点语言支持一些 Spring 不支持的额外切点标识符:callgetsetpreinitializationstaticinitializationinitializationhandleradviceexecutionwithincodecflowcflowbelowif@this@withincode。在由 Spring AOP 解释的切点表达式中使用这些切点标识符会导致抛出 IllegalArgumentException

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

由于Spring AOP仅将匹配限制在方法执行连接点(join points)上,因此前面关于切点设计器(pointcut designators)的讨论所给出的定义,比你在AspectJ编程指南中找到的定义要狭隘。此外,AspectJ本身具有基于类型的语义(type-based semantics),在方法执行连接点上,thistarget都指向同一个对象:即执行该方法的对象。Spring AOP是一个基于代理(proxy-based)的系统,它区分了代理对象本身(绑定到this)和代理背后的目标对象(绑定到target)。

备注

由于Spring的AOP框架是基于代理的,因此按照定义,目标对象内部的调用是无法被拦截的。对于JDK代理来说,只有代理上的公共接口方法调用才能被拦截。而使用CGLIB时,代理上的公共方法和受保护方法调用都可以被拦截(如有必要,甚至包内可见的方法也可以被拦截)。不过,通过代理进行的常见交互应该始终设计为使用公共签名。

需要注意的是,切点(pointcut)的定义通常是针对任何被拦截的方法的。如果某个切点严格来说只适用于公共方法,在即使存在非公共方法调用的CGLIB代理场景下,也需要相应地进行定义。

如果你的拦截需要包括目标类中的方法调用甚至构造函数,那么可以考虑使用Spring驱动的原生AspectJ编织,而不是Spring基于代理的AOP框架。这是一种具有不同特性的AOP使用方式,所以在做出决定之前,请务必熟悉相关的编织技术。

Spring AOP还支持一个名为bean的额外PCD(Pointcut Definition)。这个PCD允许你将连接点(join point)的匹配限制在特定的命名Spring bean上,或者在一组命名的Spring bean上(当使用通配符时)。bean PCD的形式如下:

bean(idOrNameOfBean)

idOrNameOfBean 令牌可以是任何 Spring bean 的名称。系统提供了对 * 字符的有限通配支持,因此,如果您为 Spring beans 制定了某些命名规范,就可以编写 bean PCD 表达式来选择它们。与其他切点标识符一样,bean PCD 也可以与 &&(与)、||(或)和 !(非)运算符一起使用。

备注

bean PCD 仅在 Spring AOP 中得到支持,而在原生 AspectJ 编织中则不支持。这是 Spring 对 AspectJ 定义的标准 PCDs 的一种特定扩展,因此,它不能用于在 @Aspect 模型中声明的切面。

bean PCD 在实例级别上进行操作(基于 Spring bean 名称的概念),而不仅仅是在类型级别(基于编织的 AOP 仅限于类型级别)。基于实例的切入点标识符是 Spring 基于代理的 AOP 框架的一项特殊功能,该框架与 Spring bean 工厂紧密集成,在这种集成下,通过名称来识别特定的 bean 是非常自然且直接的。

组合切点表达式

您可以使用 &&||! 来组合切点表达式。您也可以通过名称来引用切点表达式。以下示例展示了三个切点表达式:

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
}
  • publicMethod 匹配表示执行任何公共方法的情况。

  • inTrading 匹配方法执行发生在交易模块中的情况。

  • tradingOperation 匹配方法执行是交易模块中的任何公共方法的情况。

如上所示,将更复杂的切点表达式构建为较小的命名切点是一种最佳实践。当通过名称引用切点时,普通的Java可见性规则适用(你可以在同一类型中看到private切点,在层次结构中看到protected切点,在任何地方都可以看到public切点,依此类推)。可见性并不影响切点的匹配。

共享命名切点定义

在处理企业级应用程序时,开发人员经常需要从多个角度引用应用程序的模块和特定的操作集。我们建议为此目的定义一个专门的类,用于捕获常用的命名切点表达式。这样的类通常类似于下面的CommonPointcuts示例(不过类的命名由你决定):

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() {}

}

在任何需要使用切点表达式的位置,你都可以通过引用该类的全限定名加上@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表达式的格式如下:

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

除了返回类型模式(前面代码片段中的 ret-type-pattern)、名称模式和参数模式之外,其他所有部分都是可选的。返回类型模式决定了方法必须具有何种返回类型才能与连接点(join point)匹配。* 是最常用的返回类型模式,它可以匹配任何返回类型。只有当方法返回指定的类型时,完全限定的类型名才会被匹配。名称模式用于匹配方法名。您可以将 * 通配符用作名称模式的全部或部分。如果您指定了声明类型模式,则需要在末尾添加一个点(.)来将其与名称模式组件连接起来。参数模式稍微复杂一些:()匹配不带参数的方法,而(..) 匹配任意数量(零个或多个)的参数。() 模式匹配接受任意类型参数的一个方法。(, 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..*)
  • 任何连接点(仅限 Spring AOP 中的方法执行),且代理实现了 AccountService 接口:

    this(com.xyz.service.AccountService)
    备注

    this 更常以绑定形式使用。请参阅 声明 Advice 部分,了解如何在建议体中访问代理对象。

  • 任何连接点(仅限 Spring AOP 中的方法执行),且目标对象实现了 AccountService 接口:

    target(com.xyz.service.AccountService)
    备注

    target 更常以绑定形式使用。请参阅 声明 Advice 部分,了解如何在建议体中访问目标对象。

  • 任何连接点(仅限 Spring AOP 中的方法执行),该方法接受单个参数,并且运行时传递的参数是 Serializable 类型:

    args(java.io.Serializable)
    备注

    args 更常以绑定形式使用。请参阅 声明 Advice 部分,了解如何在建议体中访问方法参数。

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

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

    @target(org.springframework.transaction.annotationTransactional)
    备注

    你也可以以绑定形式使用 @target。请参阅 声明 Advice 部分,了解如何在建议体中访问注解对象。

  • 任何连接点(仅限 Spring AOP 中的方法执行),且目标对象的声明类型带有 @Transactional 注解:

    @within(org.springframework.transaction.annotationTransactional)
    备注

    你也可以以绑定形式使用 @within。请参阅 声明 Advice 部分,了解如何在建议体中访问注解对象。

  • 任何连接点(仅限 Spring AOP 中的方法执行),且执行方法带有 @Transactional 注解:

    @annotation(org.springframework.transaction.annotationTransactional)
    备注

    你也可以以绑定形式使用 @annotation。请参阅 声明 Advice 部分,了解如何在建议体中访问注解对象。

  • 任何连接点(仅限 Spring AOP 中的方法执行),该方法接受单个参数,并且运行时传递的参数类型带有 @Classified 注解:

    @args(com.xyz.security.Classified)
    备注

    你也可以以绑定形式使用 @args。请参阅 声明 Advice 部分,了解如何在建议体中访问注解对象。

  • 在名为 tradeService 的 Spring bean 上的任何连接点(仅限 Spring AOP 中的方法执行):

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

    bean(*Service)

编写优秀的切点

在编译过程中,AspectJ 会处理切点(pointcuts)以优化匹配性能。检查代码并确定每个连接点(join point)是否(静态或动态地)与给定的切点匹配是一个耗时的过程。(动态匹配意味着无法通过静态分析完全确定匹配结果,因此需要在代码中加入测试来在运行时判断是否存在实际匹配。)首次遇到切点声明时,AspectJ 会将其重写为适合匹配过程的最佳形式。这意味着什么呢?基本上,切点会被重写为析取范式(Disjunctive Normal Form, DNF),并且切点的组成部分会被排序,以便先检查那些评估成本较低的组成部分。这样一来,你就无需担心理解各种切点标识符的性能了,在切点声明中可以以任何顺序提供这些标识符。

然而,AspectJ只能按照它所接收的指令来工作。为了获得最佳的匹配性能,你应该考虑自己想要实现的目标,并在定义中尽可能缩小匹配的范围。现有的限定符自然可以归为三类:类型限定(kinded)、作用域限定(scoping)和上下文限定(contextual):

  • 类型限定符选择特定类型的连接点:execution(执行)、get(获取)、set(设置)、call(调用)和handler(处理程序)。

  • 作用域限定符选择一组感兴趣的连接点(可能包含多种类型):within(在...范围内)和withincode(在代码内部)。

  • 上下文限定符根据上下文进行匹配(并可选择性地进行绑定):this(当前对象)、target(目标)和@annotation(注释)。

一个编写良好的切点(pointcut)至少应包含前两种类型(类型指定符和作用域指定符)。你还可以添加上下文指定符,以便根据连接点(join point)的上下文进行匹配,或将该上下文用于后续的处理建议中。仅使用类型指定符或仅使用上下文指定符也是可行的,但由于需要额外的处理和分析,可能会影响切点的执行性能(时间和内存消耗)。作用域指定符的匹配速度非常快,使用它们意味着AspectJ可以迅速排除那些不需要进一步处理的连接点组。如果可能的话,一个好的切点应该始终包含作用域指定符。