基于 Schema 的 AOP 支持
如果您更喜欢基于 XML 的格式,Spring 也支持使用 aop
命名空间标签定义 Aspect。它支持与 @AspectJ 风格完全相同的 Pointcut 表达式和 Advice 类型。因此,在本节中,我们重点介绍该语法,关于编写 Pointcut 表达式和绑定 Advice 参数的理解,请读者参阅上一节(@AspectJ 支持)的讨论。
要使用本节描述的 aop 命名空间标签,您需要导入 spring-aop
schema,具体描述请参见基于 XML Schema 的配置。有关如何在 aop
命名空间中导入标签的信息,请参见AOP schema。
在您的 Spring 配置中,所有 aspect 和 advisor 元素都必须放置在 <aop:config>
元素内(在应用程序上下文配置中可以有多个 <aop:config>
元素)。<aop:config>
元素可以包含 pointcut、advisor 和 aspect 元素(注意,这些元素必须按此顺序声明)。
<aop:config> 风格的配置大量使用了 Spring 的自动代理机制。如果您已经通过使用 BeanNameAutoProxyCreator 或类似的方式使用了显式自动代理,这可能会导致问题(例如 Advice 未被织入)。推荐的使用模式是只使用 <aop:config> 风格或只使用 AutoProxyCreator 风格,切勿混合使用。 |
声明一个 Aspect
当您使用 Schema 支持时,Aspect 是在 Spring 应用程序上下文中定义为 Bean 的常规 Java 对象。状态和行为在对象的字段和方法中捕获,而 Pointcut 和 Advice 信息则在 XML 中捕获。
您可以使用 <aop:aspect>
元素声明一个 Aspect,并使用 ref
属性引用 backing Bean,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
当然,支持 Aspect 的 Bean(此处为 aBean
)可以像其他任何 Spring Bean 一样进行配置和依赖注入。
声明一个 Pointcut
您可以在 <aop:config>
元素内声明一个 命名 Pointcut,这样 Pointcut 定义就可以在多个 Aspect 和 Advisor 之间共享。
表示服务层中任何业务服务执行的 Pointcut 可以定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))" />
</aop:config>
请注意,Pointcut 表达式本身使用与@AspectJ 支持中描述的相同的 AspectJ Pointcut 表达式语言。如果您使用基于 Schema 的声明风格,您也可以在 Pointcut 表达式中引用 @Aspect
类型中定义的 命名 Pointcut。因此,定义上述 Pointcut 的另一种方法如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.CommonPointcuts.businessService()" /> (1)
</aop:config>
1 | 引用共享命名 Pointcut 定义中定义的 businessService 命名 Pointcut。 |
在 Aspect 内部 声明 Pointcut 与声明顶级 Pointcut 非常相似,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与 @AspectJ Aspect 的方式非常相似,使用基于 Schema 的定义风格声明的 Pointcut 可以收集连接点上下文。例如,以下 Pointcut 将 this
对象收集为连接点上下文并将其传递给 Advice:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
Advice 必须声明为通过包含匹配名称的参数来接收收集到的连接点上下文,如下所示:
-
Java
-
Kotlin
public void monitor(Object service) {
// ...
}
fun monitor(service: Any) {
// ...
}
组合 Pointcut 子表达式时,在 XML 文档中使用 &&
不方便,因此您可以使用 and
、or
和 not
关键字来代替 &&
、||
和 !
。例如,前面的 Pointcut 可以更好地写成如下形式:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
请注意,以这种方式定义的 Pointcut 通过其 XML id
进行引用,不能用作命名 Pointcut 来形成复合 Pointcut。因此,基于 Schema 的定义风格中对命名 Pointcut 的支持比 @AspectJ 风格提供的更为有限。
声明 Advice
基于 Schema 的 AOP 支持使用与 @AspectJ 风格相同的五种 Advice 类型,它们具有完全相同的语义。
前置 Advice (Before Advice)
前置 Advice 在匹配的方法执行之前运行。它使用 <aop:before>
元素在 <aop:aspect>
内部声明,如下例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
在上面的示例中,dataAccessOperation
是定义在顶级(<aop:config>
)的 命名 Pointcut 的 id
(参见声明一个 Pointcut)。
正如我们在讨论 @AspectJ 风格时所指出的,使用 命名 Pointcut 可以显著提高代码的可读性。详情请参见共享命名 Pointcut 定义。 |
要以内联方式定义 Pointcut,请将 pointcut-ref
属性替换为 pointcut
属性,如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
method
属性标识提供 Advice 主体的方法(doAccessCheck
)。此方法必须在包含 Advice 的 Aspect 元素所引用的 Bean 中定义。在执行数据访问操作(由 Pointcut 表达式匹配的方法执行连接点)之前,将调用 Aspect Bean 上的 doAccessCheck
方法。
后置返回 Advice (After Returning Advice)
后置返回 Advice 在匹配的方法执行正常完成时运行。它在 <aop:aspect>
内部声明,方式与前置 Advice 相同。以下示例显示如何声明它:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
与 @AspectJ 风格一样,您可以在 Advice 主体内获取返回值。为此,使用 returning
属性指定返回值应传递到的参数名称,如下例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut="execution(* com.xyz.dao.*.*(..))"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck
方法必须声明一个名为 retVal
的参数。此参数的类型约束匹配的方式与 @AfterReturning
的描述相同。例如,您可以如下声明方法签名:
-
Java
-
Kotlin
public void doAccessCheck(Object retVal) {...
fun doAccessCheck(retVal: Any) {...
后置抛出 Advice (After Throwing Advice)
后置抛出 Advice 在匹配的方法执行因抛出异常而退出时运行。它使用 after-throwing
元素在 <aop:aspect>
内部声明,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doRecoveryActions"/>
...
</aop:aspect>
与 @AspectJ 风格一样,您可以在 Advice 主体内获取抛出的异常。为此,使用 throwing
属性指定异常应传递到的参数名称,如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut="execution(* com.xyz.dao.*.*(..))"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions
方法必须声明一个名为 dataAccessEx
的参数。此参数的类型约束匹配的方式与 @AfterThrowing
的描述相同。例如,方法签名可以声明如下:
-
Java
-
Kotlin
public void doRecoveryActions(DataAccessException dataAccessEx) {...
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
后置 (Finally) Advice
后置 (Finally) Advice 无论匹配的方法执行如何退出都会运行。您可以使用 after
元素声明它,如下例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut="execution(* com.xyz.dao.*.*(..))"
method="doReleaseLock"/>
...
</aop:aspect>
环绕 Advice (Around Advice)
最后一种 Advice 类型是 环绕 Advice。环绕 Advice 在匹配方法的执行“周围”运行。它有机会在方法运行之前和之后执行工作,并决定方法何时、如何以及是否实际运行。如果您需要在方法执行之前和之后以线程安全的方式共享状态(例如,启动和停止计时器),通常会使用环绕 Advice。
始终使用满足您需求的最低权限 Advice 形式。 例如,如果 前置 Advice 足以满足您的需求,请不要使用 环绕 Advice。 |
您可以使用 aop:around
元素声明环绕 Advice。Advice 方法应声明 Object
作为其返回类型,并且该方法的第一个参数必须是 ProceedingJoinPoint
类型。在 Advice 方法的主体内,您必须在 ProceedingJoinPoint
上调用 proceed()
以便基础方法运行。不带参数调用 proceed()
将导致在调用基础方法时提供调用者的原始参数。对于高级用例,proceed()
方法有一个重载变体,它接受一个参数数组(Object[]
)。数组中的值将用作调用基础方法时的参数。有关调用带有 Object[]
参数的 proceed
的注意事项,请参阅环绕 Advice。
以下示例显示如何在 XML 中声明环绕 Advice:
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut="execution(* com.xyz.service.*.*(..))"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling
Advice 的实现可以与 @AspectJ 示例中的完全相同(当然,不包括注解),如下例所示:
-
Java
-
Kotlin
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any? {
// start stopwatch
val retVal = pjp.proceed()
// stop stopwatch
return pjp.proceed()
}
Advice 参数
基于 Schema 的声明风格支持完全类型化的 Advice,其方式与 @AspectJ 支持中所述相同 — 通过按名称匹配 Pointcut 参数与 Advice 方法参数。详情请参见Advice 参数。如果您希望显式指定 Advice 方法的参数名称(不依赖于先前描述的检测策略),您可以使用 Advice 元素的 arg-names
属性,其处理方式与 Advice 注解中的 argNames
属性相同(如确定参数名称中所述)。以下示例显示如何在 XML 中指定参数名称:
<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" (1)
method="audit"
arg-names="auditable" />
1 | 引用组合 Pointcut 表达式中定义的 publicMethod 命名 Pointcut。 |
arg-names
属性接受逗号分隔的参数名称列表。
以下稍微复杂一些的基于 XSD 的方法示例显示了与多个强类型参数结合使用的环绕 Advice:
-
Java
-
Kotlin
package com.xyz.service;
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
package com.xyz.service
interface PersonService {
fun getPerson(personName: String, age: Int): Person
}
class DefaultPersonService : PersonService {
fun getPerson(name: String, age: Int): Person {
return Person(name, age)
}
}
接下来是 Aspect。请注意 profile(..)
方法接受多个强类型参数,其中第一个参数恰好是用于继续方法调用的连接点。此参数的存在表明 profile(..)
将用作 around
Advice,如下例所示:
-
Java
-
Kotlin
package com.xyz;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
package com.xyz
import org.aspectj.lang.ProceedingJoinPoint
import org.springframework.util.StopWatch
class SimpleProfiler {
fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any? {
val clock = StopWatch("Profiling for '$name' and '$age'")
try {
clock.start(call.toShortString())
return call.proceed()
} finally {
clock.stop()
println(clock.prettyPrint())
}
}
}
最后,以下 XML 配置示例针对特定连接点执行了前面的 Advice:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="com.xyz.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="com.xyz.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* com.xyz.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
考虑以下驱动脚本:
-
Java
-
Kotlin
public class Boot {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
PersonService person = ctx.getBean(PersonService.class);
person.getPerson("Pengo", 12);
}
}
fun main() {
val ctx = ClassPathXmlApplicationContext("beans.xml")
val person = ctx.getBean(PersonService.class)
person.getPerson("Pengo", 12)
}
使用这样的 Boot
类,我们将在标准输出上得到类似于以下内容的输出:
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)
Advice 顺序
当多个 Advice 需要在同一个连接点(执行方法)运行时,排序规则如Advice 顺序中所述。Aspect 之间的优先级通过 <aop:aspect>
元素中的 order
属性确定,或者通过向支持 Aspect 的 Bean 添加 @Order
注解,或者通过让 Bean 实现 Ordered
接口来确定。
与在同一 例如,给定在同一 一般来说,如果您发现在同一 |
引入 (Introductions)
引入(在 AspectJ 中称为 inter-type declarations)允许 Aspect 声明被通知的对象实现给定的接口,并代表这些对象提供该接口的实现。
您可以使用 aop:declare-parents
元素在 aop:aspect
内部进行引入。您可以使用 aop:declare-parents
元素声明匹配的类型具有新的父类型(因此得名)。例如,给定一个名为 UsageTracked
的接口和一个名为 DefaultUsageTracked
的该接口的实现,以下 Aspect 声明所有服务接口的实现者也都实现 UsageTracked
接口。(例如,以便通过 JMX 公开统计信息。)
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xyz.service.*+"
implement-interface="com.xyz.service.tracking.UsageTracked"
default-impl="com.xyz.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="execution(* com.xyz..service.*.*(..))
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
支持 usageTracking
Bean 的类将包含以下方法:
-
Java
-
Kotlin
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
要实现的接口由 implement-interface
属性确定。types-matching
属性的值是一个 AspectJ 类型模式。任何匹配类型的 Bean 都实现 UsageTracked
接口。请注意,在前面示例的前置 Advice 中,服务 Bean 可以直接用作 UsageTracked
接口的实现。要以编程方式访问 Bean,您可以编写以下代码:
-
Java
-
Kotlin
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
val usageTracked = context.getBean("myService", UsageTracked.class)
Advisors
“Advisor”的概念来自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接对应的概念。Advisor 就像一个小的自包含的 Aspect,它包含一个单独的 Advice。Advice 本身由一个 Bean 表示,并且必须实现Spring 中的 Advice 类型中描述的一个 Advice 接口。Advisor 可以利用 AspectJ Pointcut 表达式。
Spring 通过 <aop:advisor>
元素支持 Advisor(通知器)概念。您最常见的使用场景是与事务通知结合,事务通知在 Spring 中也有自己的命名空间支持。以下示例展示了一个通知器
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice" />
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
除了前面示例中使用的 pointcut-ref
属性外,您还可以使用 pointcut
属性内联定义一个切入点表达式。
要定义通知器的优先级,以便通知可以参与排序,请使用 order
属性来定义通知器的 Ordered
值。
一个 AOP Schema 示例
本节展示了来自 一个 AOP 示例 的并发锁定失败重试示例在用 Schema 支持重写后的样子。
业务服务的执行有时会因为并发问题(例如,死锁失败)而失败。如果操作被重试,很可能在下一次尝试中成功。对于在这些条件下适合重试的业务服务(无需返回给用户解决冲突的幂等操作),我们希望透明地重试操作,以避免客户端看到 PessimisticLockingFailureException
。这是一个明显跨越服务层中多个服务的需求,因此非常适合通过切面来实现。
因为我们想重试操作,所以需要使用环绕通知,这样我们就可以多次调用 proceed
。以下清单显示了基本的切面实现(它是一个使用 Schema 支持的普通 Java 类)
-
Java
-
Kotlin
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
class ConcurrentOperationExecutor : Ordered {
private val DEFAULT_MAX_RETRIES = 2
private var maxRetries = DEFAULT_MAX_RETRIES
private var order = 1
fun setMaxRetries(maxRetries: Int) {
this.maxRetries = maxRetries
}
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any? {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException
do {
numAttempts++
try {
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
lockFailureException = ex
}
} while (numAttempts <= this.maxRetries)
throw lockFailureException
}
}
请注意,该切面实现了 Ordered
接口,以便我们可以将切面的优先级设置得高于事务通知(我们希望每次重试时都有一个新的事务)。maxRetries
和 order
属性都由 Spring 配置。主要操作发生在 doConcurrentOperation
环绕通知方法中。我们尝试继续执行。如果因 PessimisticLockingFailureException
失败,我们会再次尝试,除非我们已经耗尽了所有重试次数。
此类与 @AspectJ 示例中使用的类相同,但去掉了注解。 |
相应的 Spring 配置如下所示
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
请注意,目前我们假设所有业务服务都是幂等的。如果不是这种情况,我们可以细化切面,使其仅重试真正幂等的操作,方法是引入 Idempotent
注解并使用该注解来标注服务操作的实现,如下例所示
-
Java
-
Kotlin
@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
@Retention(AnnotationRetention.RUNTIME)
// marker annotation
annotation class Idempotent
要修改切面以仅重试幂等操作,需要细化切入点表达式,使其仅匹配 @Idempotent
操作,如下所示
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.service.*.*(..)) and
@annotation(com.xyz.service.Idempotent)"/>