基于 Schema 的 AOP 支持

如果你更喜欢基于 XML 的格式,Spring 也支持使用 aop 命名空间标签定义切面。支持的切入点表达式和通知类型与使用 @AspectJ 样式时完全相同。因此,在本节中,我们重点介绍该语法,并请读者参阅上一节(@AspectJ 支持)中的讨论,以了解如何编写切入点表达式和绑定通知参数。 要使用本节中描述的 aop 命名空间标签,你需要导入 spring-aop schema,如 基于 XML Schema 的配置 中所述。请参阅 AOP schema 了解如何导入 aop 命名空间中的标签。 在你的 Spring 配置中,所有切面和顾问元素都必须放在 <aop:config> 元素内(你可以在应用程序上下文配置中拥有多个 <aop:config> 元素)。一个 <aop:config> 元素可以包含切入点、顾问和切面元素(请注意,这些元素必须按此顺序声明)。

<aop:config> 样式的配置大量使用了 Spring 的 自动代理 机制。如果你已经通过使用 BeanNameAutoProxyCreator 或类似机制使用了显式自动代理,这可能会导致问题(例如通知未织入)。推荐的使用模式是只使用 <aop:config> 样式或只使用 AutoProxyCreator 样式,绝不要混用。

声明切面

当你使用 schema 支持时,切面是一个常规的 Java 对象,在你的 Spring 应用程序上下文中定义为 bean。状态和行为在对象的字段和方法中捕获,切入点和通知信息在 XML 中捕获。

你可以使用 <aop:aspect> 元素声明一个切面,并使用 ref 属性引用支持 bean,如下例所示:

<aop:config>
	<aop:aspect id="myAspect" ref="aBean">
		...
	</aop:aspect>
</aop:config>

<bean id="aBean" class="...">
	...
</bean>

支持切面的 bean(本例中为 aBean)当然可以像任何其他 Spring bean 一样进行配置和依赖注入。

声明切入点

你可以在 <aop:config> 元素内部声明一个_命名切入点_,让切入点定义在多个切面和顾问之间共享。

表示服务层中任何业务服务执行的切入点可以定义如下:

<aop:config>

	<aop:pointcut id="businessService"
		expression="execution(* com.xyz.service.*.*(..))" />

</aop:config>

请注意,切入点表达式本身使用与 @AspectJ 支持 中描述的相同的 AspectJ 切入点表达式语言。如果你使用基于 schema 的声明样式,你也可以在切入点表达式中引用在 @Aspect 类型中定义的_命名切入点_。因此,定义上述切入点的另一种方式如下:

<aop:config>

	<aop:pointcut id="businessService"
		expression="com.xyz.CommonPointcuts.businessService()" /> [id="CO1-1"]1

</aop:config>
<1>  引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-common-pointcuts[共享命名切入点定义] 中定义的 `businessService` 命名切入点。

在切面_内部_声明切入点与声明顶级切入点非常相似,如下例所示:

<aop:config>

	<aop:aspect id="myAspect" ref="aBean">

		<aop:pointcut id="businessService"
			expression="execution(* com.xyz.service.*.*(..))"/>

		...
	</aop:aspect>

</aop:config>

与 @AspectJ 切面非常相似,使用基于 schema 的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点将 this 对象作为连接点上下文收集并传递给通知:

<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>

通知必须声明为通过包含匹配名称的参数来接收收集到的连接点上下文,如下所示:

  • Java

  • Kotlin

public void monitor(Object service) {
	// ...
}
fun monitor(service: Any) {
	// ...
}

当组合切入点子表达式时,&& 在 XML 文档中不方便,因此你可以使用 andornot 关键字分别代替 &&||!。例如,前面的切入点可以更好地编写如下:

<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>

请注意,以这种方式定义的切入点通过其 XML id 引用,不能用作命名切入点来形成复合切入点。因此,基于 schema 的定义样式中提供的命名切入点支持比 @AspectJ 样式提供的更有限。

声明通知

基于 schema 的 AOP 支持使用与 @AspectJ 样式相同的五种通知,它们具有完全相同的语义。

前置通知

前置通知在匹配的方法执行之前运行。它通过使用 <aop:before> 元素在 <aop:aspect> 内部声明,如下例所示:

<aop:aspect id="beforeExample" ref="aBean">

	<aop:before
		pointcut-ref="dataAccessOperation"
		method="doAccessCheck"/>

	...

</aop:aspect>

在上面的示例中,dataAccessOperation 是在顶层 (<aop:config>) 定义的_命名切入点_的 id(参见 声明切入点)。

正如我们在讨论 @AspectJ 样式时所指出的,使用_命名切入点_可以显著提高代码的可读性。有关详细信息,请参阅 共享命名切入点定义

要以内联方式定义切入点,请将 pointcut-ref 属性替换为 pointcut 属性,如下所示:

<aop:aspect id="beforeExample" ref="aBean">

	<aop:before
		pointcut="execution(* com.xyz.dao.*.*(..))"
		method="doAccessCheck"/>

	...

</aop:aspect>

method 属性标识提供通知主体的方法 (doAccessCheck)。此方法必须为包含通知的切面元素引用的 bean 定义。在执行数据访问操作(与切入点表达式匹配的方法执行连接点)之前,将调用切面 bean 上的 doAccessCheck 方法。

后置返回通知

后置返回通知在匹配的方法执行正常完成时运行。它以与前置通知相同的方式在 <aop:aspect> 内部声明。以下示例显示了如何声明它:

<aop:aspect id="afterReturningExample" ref="aBean">

	<aop:after-returning
		pointcut="execution(* com.xyz.dao.*.*(..))"
		method="doAccessCheck"/>

	...
</aop:aspect>

与 @AspectJ 样式一样,你可以在通知主体中获取返回值。为此,请使用 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) {...

后置异常通知

后置异常通知在匹配的方法执行因抛出异常而退出时运行。它通过使用 after-throwing 元素在 <aop:aspect> 内部声明,如下例所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

	<aop:after-throwing
		pointcut="execution(* com.xyz.dao.*.*(..))"
		method="doRecoveryActions"/>

	...
</aop:aspect>

与 @AspectJ 样式一样,你可以在通知主体中获取抛出的异常。为此,请使用 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) {...

后置(最终)通知

后置(最终)通知无论匹配的方法执行如何退出都会运行。你可以使用 after 元素声明它,如下例所示:

<aop:aspect id="afterFinallyExample" ref="aBean">

	<aop:after
		pointcut="execution(* com.xyz.dao.*.*(..))"
		method="doReleaseLock"/>

	...
</aop:aspect>

环绕通知

最后一种通知是_环绕_通知。环绕通知在匹配方法的执行“周围”运行。它有机会在方法运行之前和之后执行工作,并确定方法何时、如何以及是否实际运行。如果你需要在方法执行之前和之后以线程安全的方式共享状态(例如,启动和停止计时器),通常会使用环绕通知。

始终使用满足你要求的最低功率的通知形式。例如,如果前置通知足以满足你的需求,请不要使用环绕通知。

你可以使用 aop:around 元素声明环绕通知。通知方法应声明 Object 作为其返回类型,并且方法的第一个参数必须是 ProceedingJoinPoint 类型。在通知方法的主体中,你必须在 ProceedingJoinPoint 上调用 proceed() 以便底层方法运行。调用不带参数的 proceed() 将导致在调用底层方法时向其提供调用者的原始参数。对于高级用例,proceed() 方法有一个重载变体,它接受一个参数数组 (Object[])。数组中的值将用作调用底层方法时的参数。有关使用 Object[] 调用 proceed 的注意事项,请参阅 环绕通知

以下示例显示了如何在 XML 中声明环绕通知:

<aop:aspect id="aroundExample" ref="aBean">

	<aop:around
		pointcut="execution(* com.xyz.service.*.*(..))"
		method="doBasicProfiling"/>

	...
</aop:aspect>

doBasicProfiling 通知实现可以与 @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()
}

通知参数

基于 schema 的声明样式以与 @AspectJ 支持描述的相同方式支持完全类型化的通知——通过按名称将切入点参数与通知方法参数匹配。有关详细信息,请参阅 通知参数。如果你希望显式指定通知方法的参数名称(不依赖于前面描述的检测策略),你可以通过使用通知元素的 arg-names 属性来做到这一点,该属性的处理方式与通知注解中的 argNames 属性相同(如 确定参数名称 中所述)。以下示例显示了如何在 XML 中指定参数名称:

<aop:before
	pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" [id="CO2-1"]1
	method="audit"
	arg-names="auditable" />
<1>  引用 xref:core/aop/ataspectj/pointcuts.adoc#aop-pointcuts-combining[组合切入点表达式] 中定义的 `publicMethod` 命名切入点。

arg-names 属性接受逗号分隔的参数名称列表。

以下是 XSD 方法的一个稍微复杂一些的示例,它显示了一些与多个强类型参数结合使用的环绕通知:

  • 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)
	}
}

接下来是切面。请注意 profile(..) 方法接受多个强类型参数,其中第一个恰好是用于继续方法调用的连接点。此参数的存在表明 profile(..) 将用作 around 通知,如下例所示:

  • 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 配置实现了特定连接点的前述通知的执行:

<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)

通知排序

当多个通知需要在同一个连接点(执行方法)上运行时,排序规则如 通知排序 中所述。切面之间的优先级通过 <aop:aspect> 元素中的 order 属性或通过向支持切面的 bean 添加 @Order 注解或让 bean 实现 Ordered 接口来确定。

与在同一个 @Aspect 类中定义的通知方法的优先级规则不同,当在同一个 <aop:aspect> 元素中定义的两个通知都需要在同一个连接点上运行时,优先级由通知元素在封闭的 <aop:aspect> 元素中声明的顺序决定,从高到低。 例如,给定在同一个 <aop:aspect> 元素中定义的 around 通知和 before 通知,它们都适用于同一个连接点,为了确保 around 通知比 before 通知具有更高的优先级,<aop:around> 元素必须在 <aop:before> 元素之前声明。 通常的经验法则是,如果你发现同一个 <aop:aspect> 元素中定义了多个适用于同一个连接点的通知,请考虑将这些通知方法合并为每个 <aop:aspect> 元素中每个连接点的一个通知方法,或者将通知重构为单独的 <aop:aspect> 元素,你可以在切面级别对它们进行排序。

引入

引入(在 AspectJ 中称为类型间声明)允许切面声明被通知的对象实现给定接口,并代表这些对象提供该接口的实现。

你可以通过在 aop:aspect 内部使用 aop:declare-parents 元素进行引入。你可以使用 aop:declare-parents 元素声明匹配类型具有新的父级(因此得名)。例如,给定一个名为 UsageTracked 的接口和一个名为 DefaultUsageTracked 的该接口的实现,以下切面声明所有服务接口的实现者也实现 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 接口。请注意,在前面示例的前置通知中,服务 bean 可以直接用作 UsageTracked 接口的实现。要以编程方式访问 bean,你可以编写以下内容:

  • Java

  • Kotlin

UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);
val usageTracked = context.getBean("myService", UsageTracked.class)

切面实例化模型

schema 定义的切面唯一支持的实例化模型是单例模型。其他实例化模型可能在未来的版本中得到支持。

顾问

“顾问”的概念来自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接的等价物。顾问就像一个小的、自包含的切面,它只有一段通知。通知本身由一个 bean 表示,并且必须实现 Spring 中的通知类型 中描述的一个通知接口。顾问可以利用 AspectJ 切入点表达式。

Spring 通过 <aop: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)"/>