声明一个切入点
切入点决定了感兴趣的连接点,从而使我们能够控制通知何时运行。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 Programming Guide(以及扩展部分,请参阅 AspectJ 5 Developer’s Notebook)或有关 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 中运行的方法)具有给定的注解。
由于 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 也可以与 &&
(和)、||
(或)和 !
(非)运算符一起使用。
bean
PCD 仅在 Spring AOP 中受支持,而不在原生 AspectJ 编织中受支持。它是对 AspectJ 定义的标准 PCD 的 Spring 特定扩展,因此不适用于 @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
)、名称模式和参数模式之外,所有部分都是可选的。返回类型模式决定了方法的返回类型必须是什么,以便匹配连接点。*
最常用作返回类型模式,它匹配任何返回类型。完全限定的类型名称仅在方法返回给定类型时匹配。名称模式匹配方法名称。你可以使用 *
通配符作为名称模式的全部或一部分。如果你指定了声明类型模式,请在其后加上 .
以将其连接到名称模式组件。参数模式稍微复杂一些:()
匹配不带参数的方法,而 (..)
匹配任意数量(零个或多个)参数。(*)
模式匹配带有一个任意类型参数的方法。(*,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..*)
-
代理实现
AccountService
接口的任何连接点(在 Spring AOP 中仅限方法执行):this(com.xyz.service.AccountService)
备注this
更常用于绑定形式。请参阅 声明通知 部分了解如何在通知体中使用代理对象。 -
目标对象实现
AccountService
接口的任何连接点(在 Spring AOP 中仅限方法执行):target(com.xyz.service.AccountService)
备注target
更常用于绑定形式。请参阅 声明通知 部分了解如何在通知体中使用目标对象。 -
接受单个参数且在运行时传递的参数为
Serializable
的任何连接点(在 Spring AOP 中仅限方法执行):args(java.io.Serializable)
备注args
更常用于绑定形式。请参阅 声明通知 部分了解如何在通知体中使用方法参数。请注意,此示例中给出的切入点与
execution(* *(java.io.Serializable))
不同。args
版本在运行时传递的参数为Serializable
时匹配,而execution
版本在方法签名声明单个类型为Serializable
的参数时匹配。 -
目标对象具有
@Transactional
注解的任何连接点(在 Spring AOP 中仅限方法执行):@target(org.springframework.transaction.annotation.Transactional)
备注你也可以在绑定形式中使用
@target
。请参阅 声明通知 部分了解如何在通知体中使用注解对象。 -
目标对象的声明类型具有
@Transactional
注解的任何连接点(在 Spring AOP 中仅限方法执行):@within(org.springframework.transaction.annotation.Transactional)
备注你也可以在绑定形式中使用
@within
。请参阅 声明通知 部分了解如何在通知体中使用注解对象。 -
执行方法具有
@Transactional
注解的任何连接点(在 Spring AOP 中仅限方法执行):@annotation(org.springframework.transaction.annotation.Transactional)
备注你也可以在绑定形式中使用
@annotation
。请参阅 声明通知 部分了解如何在通知体中使用注解对象。 -
接受单个参数且运行时传递的参数类型具有
@Classified
注解的任何连接点(在 Spring AOP 中仅限方法执行):@args(com.xyz.security.Classified)
备注你也可以在绑定形式中使用
@args
。请参阅 声明通知 部分了解如何在通知体中使用注解对象。 -
在名为
tradeService
的 Spring bean 上的任何连接点(在 Spring AOP 中仅限方法执行):bean(tradeService)
-
在名称匹配通配符表达式
*Service
的 Spring beans 上的任何连接点(在 Spring AOP 中仅限方法执行):bean(*Service)
编写良好的切入点
在编译期间,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个代价高昂的过程。(动态匹配意味着无法通过静态分析完全确定匹配,并且在代码中放置了一个测试,以便在代码运行时确定是否存在实际匹配)。在首次遇到切入点声明时,AspectJ 会将其重写为匹配过程的最佳形式。这意味着什么?基本上,切入点被重写为 DNF(析取范式),并且切入点的组件被排序,使得评估成本较低的组件优先被检查。这意味着你不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。
然而,AspectJ 只能处理被告知的内容。为了实现匹配的最佳性能,你应该考虑你想要实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的限定符自然分为三组:类型限定、范围限定和上下文限定:
-
种类限定符选择特定种类的连接点:
execution
、get
、set
、call
和handler
。 -
作用域限定符选择一组感兴趣的连接点(可能是多种类的):
within
和withincode
-
上下文限定符根据上下文进行匹配(并可选择性地绑定):
this
、target
和@annotation
一个编写良好的切入点应至少包含前两种类型(类型化和范围化)。你可以包含上下文限定符,以便根据连接点上下文进行匹配或绑定该上下文以在通知中使用。仅提供类型化限定符或仅提供上下文限定符是可行的,但可能会影响织入性能(时间和内存使用),因为需要额外的处理和分析。范围化限定符的匹配速度非常快,使用它们意味着 AspectJ 可以非常迅速地排除不应进一步处理的连接点组。如果可能的话,一个好的切入点应始终包含一个范围化限定符。