声明切入点
切入点决定了感兴趣的连接点,从而使我们能够控制通知何时运行。Spring AOP 只支持 Spring
bean 的方法执行连接点,因此你可以将切入点视为匹配 Spring
bean 上方法的执行。切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个精确决定我们感兴趣的方法
执行的切入点表达式。在 @AspectJ 注解风格的 AOP 中,切入点
签名由常规方法定义提供,切入点表达式则通过使用 @Pointcut 注解指定(作为切入点签名的
方法必须具有 void 返回类型)。
一个例子可能有助于明确切入点签名和切入点表达式之间的区别。以下示例定义了一个名为 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 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 中运行的方法)具有给定注解的连接点。
完整的 AspectJ 切入点语言支持 Spring 不支持的其他切入点指示符:call、get、set、preinitialization、
staticinitialization、initialization、handler、adviceexecution、withincode、cflow、
cflowbelow、if、@this 和 @withincode。在 Spring AOP 解释的切入点表达式中使用这些切入点指示符会导致抛出
IllegalArgumentException。
Spring AOP 支持的切入点指示符集可能会在未来的版本中扩展,以支持更多的 AspectJ 切入点指示符。
由于 Spring AOP 将匹配限制为仅方法执行连接点,因此前面关于切入点指示符的讨论
给出了比你在 AspectJ 编程指南中找到的更窄的定义。此外,AspectJ 本身具有基于类型的语义,并且在
执行连接点,this 和 target 都指向同一个对象:执行方法的对象。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 也可以与 &&(与)、||(或)和 !(非)运算符一起使用。
|
|
组合切入点表达式
你可以使用 &&、|| 和 ! 组合切入点表达式。你还可以按名称引用切入点表达式。以下示例显示了三个切入点表达式:
-
Java
-
Kotlin
package com.xyz;
public class Pointcuts {
@Pointcut("execution(public * *(..))")
public void publicMethod() {} [id="CO1-1"][id="CO1-1"][id="CO1-1"](1)
@Pointcut("within(com.xyz.trading..*)")
public void inTrading() {} [id="CO1-2"][id="CO1-2"][id="CO1-2"](2)
@Pointcut("publicMethod() && inTrading()")
public void tradingOperation() {} [id="CO1-3"][id="CO1-3"][id="CO1-3"](3)
}
| 1 | 如果方法执行连接点表示任何公共方法的执行,则 publicMethod 匹配。 |
| 2 | 如果方法执行在交易模块中,则 inTrading 匹配。 |
| 3 | 如果方法执行表示交易模块中的任何公共方法,则 tradingOperation 匹配。 |
package com.xyz
class Pointcuts {
@Pointcut("execution(public * *(..))")
fun publicMethod() {} [id="CO2-1"][id="CO1-4"][id="CO1-4"](1)
@Pointcut("within(com.xyz.trading..*)")
fun inTrading() {} [id="CO2-2"][id="CO1-5"][id="CO1-5"](2)
@Pointcut("publicMethod() && inTrading()")
fun tradingOperation() {} [id="CO2-3"][id="CO1-6"][id="CO1-6"](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 {
/**
* 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 支持 中讨论。
事务元素在 事务管理 中讨论。
示例
Spring AOP 用户最常使用的可能是 execution 切入点指示符。
执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
除返回类型模式(上述代码片段中的 ret-type-pattern)、名称模式和参数模式外,所有部分都是可选的。返回类型模式决定了
方法的返回类型必须是什么才能匹配连接点。
最常被用作返回类型模式。它匹配任何返回
类型。完全限定的类型名称仅在方法返回给定
类型时匹配。名称模式匹配方法名称。你可以使用 通配符作为名称模式的全部或部分。如果你指定了声明类型模式,
请包含一个尾随的 . 将其连接到名称模式组件。
参数模式稍微复杂一些:() 匹配一个不带参数的方法,而 (..) 匹配任意数量(零个或多个)的参数。
() 模式匹配一个带有一个任意类型参数的方法。
(,String) 匹配一个带两个参数的方法。第一个可以是任意类型,而第二个必须是 String。有关更多信息,请查阅
AspectJ 编程指南的 语言
语义 部分。
以下示例显示了一些常见的切入点表达式:
-
任何公共方法的执行:[indent="0",subs="verbatim"] execution(public * *(..))
-
任何名称以
set开头的方法的执行:[indent="0",subs="verbatim"] execution(* set*(..)) -
由
AccountService接口定义的任何方法的执行:[indent="0",subs="verbatim"] execution(* com.xyz.service.AccountService.*(..)) -
在
service包中定义的任何方法的执行:[indent="0",subs="verbatim"] execution(* com.xyz.service..(..)) -
在 service 包或其子包之一中定义的任何方法的执行:[indent="0",subs="verbatim"] execution(* com.xyz.service...(..))
-
服务包内的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] within(com.xyz.service.*)
-
服务包或其子包之一内的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] within(com.xyz.service..*)
-
代理实现
AccountService接口的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] this(com.xyz.service.AccountService)
|
|
-
目标对象实现
AccountService接口的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] target(com.xyz.service.AccountService)
|
|
-
接受单个参数且运行时传入的参数是
Serializable的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] args(java.io.Serializable)
|
|
请注意,此示例中给出的切入点与 execution(*
*(java.io.Serializable)) 不同。args 版本在运行时传入的参数是 Serializable 时匹配,而 execution 版本在方法签名声明单个
Serializable 类型参数时匹配。
* 目标对象具有 @Transactional 注解的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"]
@target(org.springframework.transaction.annotation.Transactional)
|
你也可以以绑定形式使用 |
-
目标对象的声明类型具有
@Transactional注解的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] @within(org.springframework.transaction.annotation.Transactional)
|
你也可以以绑定形式使用 |
-
执行方法具有
@Transactional注解的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] @annotation(org.springframework.transaction.annotation.Transactional)
|
你也可以以绑定形式使用 |
-
接受单个参数,并且传入参数的运行时类型具有
@Classified注解的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] @args(com.xyz.security.Classified)
|
你也可以以绑定形式使用 |
-
在名为
tradeService的 Spring bean 上的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] bean(tradeService) -
在名称与通配符表达式
*Service匹配的 Spring bean 上的任何连接点(仅限 Spring AOP 中的方法执行):[indent="0",subs="verbatim"] bean(*Service)
编写好的切入点
在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否匹配(静态或 动态)给定切入点是一个代价高昂的过程。(动态匹配意味着匹配无法通过静态分析完全确定,并且在代码中放置了测试以 确定代码运行时是否存在实际匹配)。首次遇到 切入点声明时,AspectJ 会将其重写为匹配过程的最佳形式。这意味着什么?基本上,切入点被重写为 DNF(析取范式),并且切入点的组件被排序,使得那些评估成本较低的组件 首先被检查。这意味着你不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。
然而,AspectJ 只能根据它被告知的内容工作。为了获得最佳的匹配性能,你应该考虑你想要实现什么,并在定义中尽可能地缩小匹配的搜索空间。现有的指示符 自然地分为三组:类型、范围和上下文:
-
类型指示符选择特定类型的连接点:
execution、get、set、call和handler。 -
范围指示符选择一组感兴趣的连接点(可能属于多种类型):
within和withincode -
上下文指示符根据上下文匹配(并可选地绑定):
this、target和@annotation
一个编写良好的切入点应至少包含前两种类型(类型和范围)。你可以包含上下文指示符以根据 连接点上下文进行匹配或绑定该上下文以在通知中使用。仅提供 类型指示符或仅提供上下文指示符也可以,但可能会影响织入性能(所用的时间和内存),因为会进行额外的处理和分析。范围指示符 匹配速度非常快,使用它们意味着 AspectJ 可以非常快速地 排除不应进一步处理的连接点组。一个好的 切入点如果可能的话,应该始终包含一个。