Pointcuts 确定感兴趣的连接点( join points),从而使我们能够控制 advice 的运行时间。Spring AOP 只支持 Spring Bean 的 方法执行连接点,所以你可以把 pointcut 看作是对 Spring Bean 上的 方法执行的匹配。一个切点声明有两个部分:一个由名称和任何参数组成的签名,以及一个切点表达式,它决定了我们到底对哪些方法的执行感兴趣。在 AOP 的 @AspectJ 注解式中,一个切点签名是由普通的方法定义提供的,而切点表达式是通过使用 @Pointcut 注解来表示的(作为切点签名的方法必须有一个 void 返回类型)。
一个例子可以帮助我们明确区分切点签名和切点表达式的区别。下面的例子定义了一个名为 anyOldTransfer 的切点,它匹配任何名为 transfer 的方法的执行。
@Pointcut("execution(* transfer(..))") // 切入点表达式
private void anyOldTransfer() {} // 切入点签名
构成 @Pointcut 注解的值的 pointcut 表达式是一个常规的 AspectJ pointcut 表达式。关于 Aspect J的 pointcut 语言的全面讨论,请参见《AspectJ 编程指南》(以及 AspectJ 5 Developer’s Notebook 的扩展部分)或关于 AspectJ 的书籍之一(如 Colye r等人的《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 中被运行的方法)具有给定注解的连接点。
切点表达式的类型还是比较多的,官方文档感觉描述也不是特别的清楚也有可能是翻译的问题,可以参考下这篇博客
:::info 其他 pointcut 类型
完整的 AspectJ pointcut 语言支持 Spring 不支持的其他 pointcut 指定符:call
, get
, set
, preinitialization
, staticinitialization
, initialization
, handler
, adviceexecution
, withincode
, cflow
, cflowbelow
, if
, @this
, @withincode
。在 Spring AOP 解释的 pointcut 表达式中使用这些 pointcut 指定符会导致抛出 IllegalArgumentException。
Spring AOP 支持的切点符号集可能会在未来的版本中扩展,以支持更多 AspectJ 的切点符号。 :::
因为 Spring AOP 将匹配限制在方法的执行连接点上,所以前面对切点代号的讨论给出的定义比你在 AspectJ 编程指南中找到的要窄。此外, AspectJ 本身有基于类型的语义,在执行连接点,this 和 target 都指同一个对象:执行方法的对象。Spring AOP 是一个基于代理的系统,区分了代理对象本身(与 this 绑定)和代理背后的目标对象(与 target 绑定)。
:::info 由于 Spring的AOP 框架是基于代理的,根据定义,目标对象内部的调用是不会被拦截的。对于 JDK 代理,只有代理上的公共接口方法调用可以被拦截。使用 CGLIB,代理上的 public 和 protected 的方法调用都可以被拦截(如果需要的话,甚至可以拦截包中的可见方法)。然而,通过代理的普通交互应该始终通过 public 签名来设计。
需要注意的是,pointcut 的定义通常会与任何被拦截的方法相匹配。如果一个切点是严格意义上的只公开的,即使是在 CGLIB 代理场景下,通过代理进行潜在的非公开交互,它也需要相应地被定义。
如果你的拦截需求包括目标类中的方法调用甚至构造函数,可以考虑使用 Spring 驱动的本地 AspectJ 编织,而不是 Spring 的基于代理的 AOP 框架。这构成了不同的 AOP 使用模式,具有不同的特点,所以在做决定之前一定要让自己熟悉织构。 :::
Spring AOP 还支持一个额外的名为 bean 的 PCD。这个 PCD 允许你将连接点的匹配限制在一个特定的命名的 Spring Bean 或命名的 Spring Bean 的集合(当使用通配符时)。bean PCD 有以下形式:
bean(idOrNameOfBean)
id 或 NameOfBean 标记可以是任何 Spring Bean 的名称。我们提供了使用 *
字符的有限通配符支持,因此,如果你为你的 Spring Bean 建立了一些命名惯例,你可以写一个 bean PCD 表达式来选择它们。就像其他的 pointcut 符号一样,Bean PCD 也可以和 &&
(和)、||
(或)、和 !
(否定)运算符一起使用。
:::info 仅在 Spring AOP 中支持 bean PCD,而在本地 AspectJ 织构中不支持。它是对 AspectJ 定义的标准 PCD 的一个特定的 Spring 扩展,因此不适用于 @Aspect 模型中声明的切面。
Bean PCD 在实例级(建立在 Spring Bean 名称概念的基础上)而不是仅在类型级(基于织构的 AOP 仅限于此)进行操作。基于实例的切点指定器是 Spring 基于代理的 AOP 框架的一种特殊能力,它与 Spring Bean 工厂紧密结合,通过名称来识别特定的 bean 是很自然和直接的。 :::
组合 pointcut 表达式
你可以通过使用 &&
、||
和 !
来组合 pointcut 表达式。你也可以通过名称来引用 pointcut 表达式。下面的例子显示了三个切点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
anyPublicOperation
:如果一个方法的执行连接点代表了任何 public 方法的执行,则与之匹配任何 public 方法的执行。inTrading
: 如果一个方法的执行是在 trading 模块中,则匹配。tradingOperation
:前面两个条件都满足这与之匹配
正如前面所显示的那样,从较小的命名组件中建立更复杂的 pointcut 表达式是一种最佳做法。当通过名字引用点切时,正常的 Java 可见性规则适用(你可以在同一类型中看到 private 切点,在层次结构中看到 protected 的切点,在任何地方看到 public 切点,等等)。可见性并不影响切点的匹配。
共享通用切入点定义
在处理企业应用程序时,开发人员经常想从几个切面来引用应用程序的模块和特定的操作集。我们建议定义一个 CommonPointcuts 切面,它可以为这个目的捕获常见的 pointcut 表达。这样的一个切面通常类似于下面的例子:
package cn.mrcode.study.springdocsread.aspect;
import org.aspectj.lang.annotation.Pointcut;
/**
* 公共切入点
*
* @author mrcode
*/
public class CommonPointcuts {
/**
* 如果方法是在 com.xyz.myapp.web 包或该包下的任何子包中的类型中定义的,则连接点位于 Web 层中。
*/
@Pointcut("within(com.xyz.myapp.web..*)")
public void inWebLayer() {
}
/**
* 如果方法在 com.xyz.myapp.service 包或该包下的任何子包中的类型中定义,则连接点位于 service 中。
*/
@Pointcut("within(com.xyz.myapp.service..*)")
public void inServiceLayer() {
}
/**
* 如果方法在 com.xyz.myapp.dao 包或该包下的任何子包中的类型中定义,则连接点位于数据访问(dao)层中。
*/
@Pointcut("within(com.xyz.myapp.dao..*)")
public void inDataAccessLayer() {
}
/**
* <pre>
* 业务服务是定义在服务接口上的任何方法的执行。这个定义假设接口放在 “service” 包中,实现类型放在子包中。
*
* 如果按功能区域对服务接口进行分组(例如,在包 com.xyz.myapp.abc.service 和 com.xyz.myapp.def.service
* 中),那么切入点表达式 “execution(* com.xyz.myapp..service .*.*(..))" 可以代替使用。
* 或者,您可以使用 “bean” PCD 编写表达式,例如 “bean(*Service)”。 (这假设您以一致的方式命名了 Spring 服务 bean。)
* </pre>
*/
@Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
public void businessService() {
}
/**
* 数据访问操作是定义在 dao 接口上的任何方法的执行。这个定义假定接口放在 “dao” 包中,实现类型放在子包中。
*/
@Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
public void dataAccessOperation() {
}
}
你可以在任何需要 pointcut 表达式的地方引用在这样一个切面定义的 pointcut 。例如,为了使服务层成为事务性的,你可以这样写:
<aop:config>
<aop:advisor
pointcut="com.xyz.myapp.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>
元素在 基于 xml 的 AOP 支持中讨论。事务元素在事务管理中讨论。
例子
Spring AOP 用户可能最常使用的是执行点指定器。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
除了返回类型模式(前面片段中的 ret-type-pattern)、name-pattern 和 param-pattern 之外的所有部分都是可选的。返回类型模式决定了方法的返回类型必须是什么,以使连接点被匹配。
- modifiers-pattern:方法的可见性,如 public,protected;
- ret-type-pattern / 方法返回值类型:
*
是最常被用作返回类型模式的。它匹配任何返回类型。一个完全限定的类型名称只在方法返回给定的类型时匹配。 - declaring-type-pattern / 方法所在类的全路径:如 com.spring.Aspect;
- name-pattern / 名称模式:匹配方法名称。你可以使用
*
通配符作为名称模式的全部或部分。如果你指定了一个 declaring-type-pattern ,请在它的尾部使用一个.
来链接 name-pattern 模式。 - param-pattern / 参数模式:稍微复杂一些
()
匹配一个不需要参数的方法,(...)
匹配任何数量(零或更多)的参数。(*)
模式匹配一个需要任何类型的参数的方法。(*,String)
匹配一个需要两个参数的方法。第一个参数可以是任何类型,而第二个参数必须是一个字符串。
更多信息请参考《AspectJ 编程指南》的语言语义部分。
下面的例子显示了一些常见的切入点表达式:
- 任何 public 方法
// 这里使用了 方法可见性 返回值类型 方法名称模式 参数模式
// 含义就是,任何返回值,任何方法,任何参数类型 的 public 方法
execution(public * *(..))
名称以 set 开头的任何方法
// 返回值类型 方法名称模式 参数模式
execution(* set*(..))
AccountService 接口中的任何方法
// 返回值类型 类路径 方法名称(类路径和方法名称后面使用 . 链接) 方法常数
execution(* com.xyz.service.AccountService.*(..))
service 包中定义的任何方法
// 返回值类型 类路径 方法名称 方法参数
// com.xyz.service.*.* 这里是表示 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 的部分。任何连接点(仅在 Spring AOP 中的方法执行),目标对象实现了 AccountService 接口
target(com.xyz.service.AccountService)
target
更常用于绑定形式。关于如何使代理对象在 advice 中可用,请参见声明 advice 的部分。任何连接点(仅在 Spring AOP 中的方法执行)都需要一个参数,并且在运行时传递的参数是可序列化的。
// 接受一个参数,并且是可序列化的方法
args(java.io.Serializable)
请注意,本例中给出的 pointcut 语句与
execution(* *(java.io.Serializable))
不同。如果在运行时传递的参数是 Serializable,则 args 版本匹配,如果方法签名声明了一个 Serializable 类型的单一参数,则执行版本匹配。任何连接点(仅在 Spring AOP 中的方法执行),目标对象有
@Transactional
注解。@target(org.springframework.transaction.annotation.Transactional)
任何连接点(仅在 Spring AOP 中的方法执行),其中目标对象的声明类型有一个 @Transactional 注解
@within(org.springframework.transaction.annotation.Transactional)
任何连接点(仅在 Spring AOP 中的方法执行),其中执行的方法有一个 @Transactional 注解。
@annotation(org.springframework.transaction.annotation.Transactional)
任何连接点(仅在 Spring AOP 中的方法执行),它需要一个参数,并且所传递的参数的运行时类型具有 @Classified 注解。
@args(com.xyz.security.Classified)
在一个名为 tradeService 的 Spring Bean 上的任何连接点(仅在 Spring AOP 中方法执行)
bean(tradeService)
在 Spring Bean 上的任何连接点(仅在 Spring AOP 中执行方法),其名称与通配符表达式
*Service
相匹配bean(*Service)
编写好的切入点
在编译过程中,AspectJ 处理 pointcuts,以优化匹配性能。检查代码并确定每个连接点是否匹配(静态或动态)一个给定的 pointcut 是一个昂贵的过程。(动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中放置一个测试,以确定在代码运行时是否存在实际的匹配)。在第一次遇到一个 pointcut 声明时,AspectJ 会将其改写成匹配过程的最佳形式。这是什么意思?基本上,pointcut 语句被重写成 DNF(Disjunctive Normal Form),并且 pointcut 语句的组件被排序,以便首先检查那些评估起来比较便宜的组件。这意味着你不需要担心理解各种切点指定器的性能,可以在切点声明中以任何顺序提供它们。
然而,AspectJ 只能用它被告知的东西来工作。为了获得最佳的匹配性能,你应该思考他们要实现的目标,并在定义中尽可能地缩小匹配的搜索空间。现有的指定器自然分为三组之一:种类、范围和上下文:
- 种类指示符选择一种特点的连接点:
execution(执行)
,get
,set
,call
,handler
. - 范围指定器选择了一组感兴趣的连接点(可能有许多种):
within
、withincode
- 上下文指定器根据上下文进行匹配(并可选择绑定):
this
、target
和@annotation
一个写得好的连接点应该至少包括前两种类型(种类和范围)。你可以包括上下文指定符,以便根据连接点的上下文进行匹配,或者将该上下文绑定在 advice 中使用。只提供种类指定符或只提供上下文指定符是可行的,但由于额外的处理和分析,可能会影响织造性能(所用时间和内存)。范围指定符的匹配速度非常快,使用它们意味着 AspectJ 可以非常迅速地排除那些不应该被进一步处理的连接点组。如果可能的话,一个好的连接点应该总是包括一个。