声明切点(Pointcut)
切点(Pointcuts)决定了我们感兴趣的连接点(join points),从而让我们能够控制通知(advice)何时执行。Spring AOP仅支持针对Spring Bean的方法执行连接点,因此可以将切点视为与Spring Bean上的方法执行相匹配的机制。切点声明包含两部分:一部分是签名(signature),由名称和任何参数组成;另一部分是切点表达式(pointcut expression),用于精确指定我们感兴趣的方法执行。在AOP的@AspectJ注解风格中,切点签名通过常规的方法定义来提供,而切点表达式则通过使用@Pointcut注解来表示(作为切点签名的方法必须具有void返回类型)。
一个例子可能有助于澄清切点签名(pointcut signature)和切点表达式(pointcut expression)之间的区别。以下示例定义了一个名为 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注解值的点切表达式(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中运行的方法)带有给定注解的切点。
由于Spring AOP仅将匹配限制在方法执行连接点(join points)上,因此前面关于切点设计器(pointcut designators)的讨论所给出的定义,比你在AspectJ编程指南中找到的定义要狭隘。此外,AspectJ本身具有基于类型的语义(type-based semantics),在方法执行连接点上,this和target都指向同一个对象:即执行该方法的对象。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 是非常自然且直接的。
组合切点表达式
您可以使用 &&、|| 和 ! 来组合切点表达式。您也可以通过名称来引用切点表达式。以下示例展示了三个切点表达式:
- 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
}
publicMethod匹配表示执行任何公共方法的情况。inTrading匹配方法执行发生在交易模块中的情况。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
}
publicMethod匹配表示执行任何公共方法的情况。inTrading匹配方法执行发生在交易模块中的情况。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>元素在基于模式的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可以迅速排除那些不需要进一步处理的连接点组。如果可能的话,一个好的切点应该始终包含作用域指定符。