基于 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.*.*(..)) &amp;&amp; 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 文档中使用 &amp;&amp; 不方便,因此您可以使用 andornot 关键字来代替 &amp;&amp;||!。例如,前面的 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>)的 命名 Pointcutid(参见声明一个 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 接口来确定。

与在同一 @Aspect 类中定义的 Advice 方法的优先级规则相反,当同一 <aop:aspect> 元素中定义的两个 Advice 都需要在同一连接点运行时,优先级由 Advice 元素在包含的 <aop:aspect> 元素内的声明顺序决定,从高到低。

例如,给定在同一 <aop:aspect> 元素中定义的 around Advice 和 before Advice,它们都适用于同一连接点,为了确保 around Advice 具有比 before Advice 更高的优先级,<aop:around> 元素必须在 <aop:before> 元素之前声明。

一般来说,如果您发现在同一 <aop:aspect> 元素中定义了多个适用于同一连接点的 Advice,请考虑将这些 Advice 方法合并到每个 <aop:aspect> 元素中每个连接点的一个 Advice 方法中,或者将这些 Advice 重构为单独的 <aop:aspect> 元素,以便您可以在 Aspect 级别进行排序。

引入 (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)

Aspect 实例化模型

Schema 定义的 Aspect 唯一支持的实例化模型是单例模型。其他实例化模型可能会在将来的版本中支持。

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 接口,以便我们可以将切面的优先级设置得高于事务通知(我们希望每次重试时都有一个新的事务)。maxRetriesorder 属性都由 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)"/>