|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
声明切入点
切入点(Pointcuts)用于确定我们感兴趣的连接点(join points),从而让我们能够控制通知(advice)的执行时机。Spring AOP 仅支持针对 Spring Bean 的方法执行连接点,因此你可以将切入点理解为匹配 Spring Bean 上方法的执行。切入点声明包含两个部分:一个由名称和任意参数组成的签名,以及一个切入点表达式,该表达式精确地指定了我们感兴趣的方法执行。在基于 @AspectJ 注解风格的 AOP 中,切入点签名通过一个常规的方法定义来提供,而切入点表达式则通过使用 @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 注解值的切点表达式是一个标准的 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 中即正在执行的方法)具有指定注解的连接点上。
由于 Spring AOP 将匹配限制在方法执行连接点(join points)上,因此上述关于切入点(pointcut)指示符的讨论比 AspectJ 编程指南中的定义更为狭窄。此外,AspectJ 本身具有基于类型的语义,在方法执行连接点处,this 和 target 都指向同一个对象:即正在执行该方法的对象。而 Spring AOP 是一个基于代理的系统,它区分代理对象本身(绑定到 this)和代理背后的目标对象(绑定到 target)。
|
由于 Spring AOP 框架基于代理的特性,目标对象内部的方法调用按照定义是不会被拦截的。对于 JDK 代理,只有通过代理对公共接口方法的调用才能被拦截。而使用 CGLIB 时,通过代理对公共(public)和受保护(protected)方法的调用会被拦截(如有必要,包可见(package-visible)的方法也会被拦截)。然而,通过代理进行的常规交互应始终通过公共方法签名来设计。 请注意,切入点(pointcut)定义通常会与任何被拦截的方法进行匹配。 如果某个切入点严格限定仅适用于公共(public)方法,即使在 CGLIB 代理场景下可能存在通过代理进行的非公共(non-public)交互,也需要相应地进行定义。 如果你的拦截需求包括目标类中的方法调用甚至构造函数,请考虑使用 Spring 驱动的原生 AspectJ 编织(weaving),而不是 Spring 基于代理的 AOP 框架。这构成了一种具有不同特性的 AOP 使用模式,因此在做出决定之前,请务必先熟悉编织(weaving)的相关知识。 |
Spring AOP 还支持一个名为 bean 的额外切入点指示符(PCD)。该 PCD 允许你将连接点的匹配限制为特定名称的 Spring Bean,或一组使用通配符指定的 Spring Bean。bean PCD 的形式如下:
bean(idOrNameOfBean)
idOrNameOfBean 标记可以是任意 Spring Bean 的名称。系统提供了有限的通配符支持,使用 * 字符,因此,如果你为 Spring Bean 建立了一些命名约定,就可以编写一个 bean 切入点表达式(PCD)来选择它们。与其他切入点指示符(pointcut designator)一样,bean PCD 也可以与 &&(与)、||(或)和 !(非)操作符一起使用。
|
|
组合切点表达式
你可以使用 &&, || 和 ! 来组合切入点(pointcut)表达式。你也可以通过名称引用切入点表达式。以下示例展示了三个切入点表达式:
-
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 在方法执行代表交易模块中的任意公共方法时匹配。 |
如上所示,将较小的命名切入点(named pointcuts)组合起来构建更复杂的切入点表达式是一种最佳实践。在通过名称引用切入点时,适用常规的 Java 可见性规则(例如,可以在同一类型中访问private切入点,在继承层次结构中访问protected切入点,以及在任何地方访问public切入点等)。可见性不会影响切入点的匹配。
共享命名切点定义
在开发企业级应用程序时,开发者经常需要在多个切面(aspect)中引用应用程序的某些模块以及特定的操作集合。
为此,我们建议定义一个专用的类,用于封装常用的命名切入点(named pointcut)表达式。
此类通常类似于下面的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 支持中讨论。事务相关元素在事务管理中讨论。
<h1>示例</h1>
Spring AOP 用户最常使用的是 execution 切入点指示符。
execution 表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
除了返回类型模式(前文代码片段中的 ret-type-pattern)、名称模式和参数模式之外,其余部分都是可选的。返回类型模式决定了方法的返回类型必须是什么,才能匹配一个连接点。* 是最常用的返回类型模式。它匹配任意返回类型。只有当方法返回指定类型时,完全限定的类型名称才会匹配。名称模式匹配方法名。您可以使用通配符*作为名称模式的全部或部分。如果指定了声明类型模式,请在名称模式组件后添加一个尾随的.。参数模式稍微复杂一些:() 匹配不带参数的方法,而(..) 匹配任意数量(零个或多个)的参数。
The (*) pattern 匹配一个接受任意类型参数的方法。
(*,String) 匹配一个接受两个参数的方法。第一个可以是任何类型,而第二个必须是 String。咨询
语言语义 部分的 AspectJ 编程指南 以获取更多详细信息。
以下示例展示了一些常见的切入点表达式:
-
任何公共方法的执行:
execution(public * *(..))
-
任何方法名以
set开头的方法的执行:execution(* set*(..))
-
由
AccountService接口定义的任何方法的执行:execution(* com.xyz.service.AccountService.*(..))
-
service包中定义的任何方法的执行:execution(* com.xyz.service.*.*(..))
-
服务包或其任意子包中定义的任何方法的执行:
execution(* com.xyz.service..*.*(..))
-
服务包内的任意连接点(在 Spring AOP 中仅限方法执行):
within(com.xyz.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更常用于绑定形式。有关如何在通知体中使目标对象可用,请参见声明通知部分。 -
任何连接点(在 Spring AOP 中仅限方法执行),该连接点接受单个参数,并且在运行时传入的参数是
Serializable类型:args(java.io.Serializable)
args更常用于绑定形式。有关如何在通知(advice)体中使用方法参数,请参见声明通知一节。请注意,本示例中给出的切入点与
execution(* *(java.io.Serializable))不同。args 版本在运行时传入的参数是Serializable类型时匹配,而 execution 版本则在方法签名声明了一个类型为Serializable的参数时匹配。 -
任何连接点(在 Spring AOP 中仅限方法执行),其中目标对象具有
@Transactional注解:@target(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用 @target。有关如何在通知体中使注解对象可用,请参见声明通知部分。 -
任何连接点(在 Spring AOP 中仅限方法执行),其目标对象的声明类型具有
@Transactional注解:@within(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用 @within。有关如何在通知体中使注解对象可用,请参见声明通知部分。 -
任何连接点(在 Spring AOP 中仅限方法执行),其执行的方法带有
@Transactional注解:@annotation(org.springframework.transaction.annotation.Transactional)
你也可以在绑定形式中使用 @annotation。有关如何在通知体中使注解对象可用,请参见声明通知部分。 -
任何连接点(在 Spring AOP 中仅限方法执行),该连接点接受单个参数,并且所传递参数的运行时类型具有
@Classified注解:@args(com.xyz.security.Classified)
你也可以在绑定形式中使用 @args。请参阅声明通知一节, 了解如何在通知体中使注解对象可用。 -
Spring Bean 名为
tradeService的任意连接点(在 Spring AOP 中仅限方法执行):bean(tradeService)
-
任何连接点(在 Spring AOP 中仅限方法执行)在名称匹配通配符表达式
*Service的 Spring Bean 上:bean(*Service)
编写良好的切点
在编译期间,AspectJ 会处理切入点(pointcut),以优化匹配性能。检查代码并确定每个连接点(join point)是否匹配(静态或动态地)给定的切入点是一个代价高昂的过程。(动态匹配是指无法仅通过静态分析完全确定匹配结果,而需要在代码中插入测试逻辑,在运行时判断是否存在实际匹配。)当 AspectJ 首次遇到一个切入点声明时,会将其重写为匹配过程的最优形式。这意味着什么?基本上,切入点会被重写为析取范式(DNF, Disjunctive Normal Form),并且切入点的各个组成部分会被排序,使得计算成本较低的组件优先被检查。因此,您无需担心理解各种切入点指示符(pointcut designator)的性能差异,可以在切入点声明中以任意顺序提供它们。
然而,AspectJ 只能根据所告知的内容进行工作。为了实现最佳的匹配性能,你应该仔细考虑自己的目标,并在定义中尽可能缩小匹配的搜索范围。现有的指示符自然可以分为以下三类:类型(kinded)、作用域(scoping)和上下文(contextual):
-
种类化指示符用于选择特定类型的连接点:
execution、get、set、call和handler。 -
作用域指示符用于选择一组感兴趣的连接点(可能包含多种类型):
within和withincode -
上下文指示符根据上下文进行匹配(并可选择性地绑定):
this、target和@annotation
一个编写良好的切入点(pointcut)应至少包含前两种类型(kinded 和 scoping)。你可以加入上下文指示符(contextual designators),以便根据连接点(join point)的上下文进行匹配,或将该上下文绑定以在通知(advice)中使用。仅提供 kinded 指示符或仅提供 contextual 指示符虽然也能工作,但由于需要额外的处理和分析,可能会影响织入(weaving)性能(时间和内存消耗)。Scoping 指示符的匹配速度非常快,使用它们可以让 AspectJ 快速排除那些无需进一步处理的连接点组。因此,如果可能的话,一个好的切入点应始终包含一个 scoping 指示符。