基于 Schema 的 AOP 支持

如果您更喜欢基于XML的格式,Spring还提供使用aop命名空间标签定义方面的支持。与使用@AspectJ样式时完全相同的切点表达式和通知类型都受支持。因此,在本节中,我们将重点介绍该语法,并将读者引导至上一节(@AspectJ支持)中的讨论,以了解如何编写切点表达式以及通知参数的绑定。

要使用本节中描述的aop命名空间标签,您需要导入spring-aop模式,如基于XML模式的配置中所述。请参阅AOP模式,了解如何在aop命名空间中导入标签。

在您的Spring配置中,所有方面和顾问元素都必须放置在<aop:config>元素内(您可以在应用程序上下文配置中拥有多个<aop:config>元素)。<aop:config>元素可以包含切点、顾问和方面元素(请注意,这些元素必须按此顺序声明)。

<aop:config>样式的配置大量使用了Spring的自动代理机制。如果您已经通过使用BeanNameAutoProxyCreator或类似方法使用显式自动代理,这可能会导致问题(例如,通知未编织)。建议的使用模式是仅使用<aop:config>样式或仅使用AutoProxyCreator样式,切勿混合使用它们。

声明方面

当您使用模式支持时,方面是在您的Spring应用程序上下文中定义为Bean的普通Java对象。状态和行为捕获在对象的字段和方法中,切点和通知信息捕获在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切点表达式语言。如果您使用基于模式的声明样式,您也可以在切点表达式中引用@Aspect类型中定义的命名切点。因此,定义上述切点的另一种方法如下

<aop:config>

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

</aop:config>
1 引用共享命名切点定义中定义的businessService命名切点。

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

<aop:config>

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

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

		...
	</aop:aspect>

</aop:config>

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

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

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

  • Java

  • Kotlin

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

当组合切点子表达式时,&amp;&amp;在XML文档中很笨拙,因此您可以使用andornot关键字分别代替&amp;&amp;||!。例如,前面的切点可以更好地编写如下

<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引用,不能用作命名切点来形成复合切点。因此,基于模式的定义样式中的命名切点支持比@AspectJ样式提供的支持更有限。

声明通知

基于 Schema 的 AOP 支持使用与 @AspectJ 风格相同的五种 Advice,并且它们具有完全相同的语义。

Before Advice

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

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

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

	...

</aop:aspect>

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

正如我们在 @AspectJ 风格的讨论中提到的,使用命名切点可以显著提高代码的可读性。有关详细信息,请参阅 共享命名切点定义

要内联定义切点,请用 pointcut 属性替换 pointcut-ref 属性,如下所示

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

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

	...

</aop:aspect>

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

After Returning Advice

After Returning Advice 在匹配的方法执行正常完成时运行。它在 <aop:aspect> 内部声明,方式与 Before 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) {...

After Throwing Advice

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

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

After (Finally) Advice

After (Finally) Advice 无论匹配的方法执行如何退出都会运行。您可以使用 after 元素声明它,如下例所示

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

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

	...
</aop:aspect>

Around Advice

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

始终使用满足您需求的最低功能的 Advice。

例如,如果Before Advice 满足您的需求,则不要使用Around Advice。

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

以下示例演示如何在 XML 中声明 Around 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 的声明风格以与 @AspectJ 支持中描述的方式相同的方式支持完全类型的 Advice,即通过按名称将切点参数与 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 引用 组合切点表达式 中定义的 publicMethod 命名切点。

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

以下基于 XSD 的方法的稍微复杂一些的示例显示了一些与多个强类型参数一起使用的 Around 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 属性确定,或者通过将 @Order 注释添加到支持 Aspect 的 Bean 或让 Bean 实现 Ordered 接口来确定。

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

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

作为一个一般的经验法则,如果您发现您在同一个 <aop:aspect> 元素中定义了多个应用于同一个连接点的 Advice,请考虑将这些 Advice 方法折叠成每个 <aop:aspect> 元素中每个连接点的一个 Advice 方法,或者将 Advice 重构到单独的 <aop:aspect> 元素中,您可以在 Aspect 级别对这些元素进行排序。

引入

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

您可以通过在 aop:aspect 内部使用 aop:declare-parents 元素进行引入。您可以使用 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 接口。请注意,在前面的示例的 Before Advice 中,服务 Bean 可以直接用作 UsageTracked 接口的实现。要以编程方式访问 Bean,您可以编写以下内容

  • Java

  • Kotlin

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

Aspect 实例化模型

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

Advisors

“Advisors”的概念来自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接的等价物。Advisor 就像一个小的自包含的 Aspect,它只有一个 Advice。Advice 本身由一个 Bean 表示,并且必须实现 Spring 中的 Advice 类型 中描述的 Advice 接口之一。Advisors 可以利用 AspectJ 切点表达式。

Spring 使用 <aop:advisor> 元素支持 Advisor 概念。您最常看到它与事务性 Advice 一起使用,事务性 Advice 在 Spring 中也有自己的命名空间支持。以下示例显示了一个 Advisor

<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 属性内联定义切点表达式。

要定义 Advisor 的优先级,以便 Advice 可以参与排序,请使用 order 属性定义 Advisor 的 Ordered 值。

AOP Schema 示例

本节展示了 AOP 示例 中的并发锁定失败重试示例在使用 Schema 支持重写时是什么样的。

业务服务的执行有时会由于并发问题而失败(例如,死锁失败者)。如果重试操作,则很可能在下一次尝试中成功。对于在这种情况下的适当重试的业务服务(不需要返回给用户进行冲突解决的幂等操作),我们希望透明地重试操作,以避免客户端看到 PessimisticLockingFailureException。这是一个明确跨越服务层中多个服务的需求,因此非常适合通过 Aspect 实现。

因为我们希望重试操作,所以我们需要使用 Around Advice,以便我们可以多次调用 proceed。以下清单显示了基本的 Aspect 实现(这是一个使用 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
	}
}

请注意,Aspect 实现了 Ordered 接口,以便我们可以将 Aspect 的优先级设置高于事务性 Advice(我们希望在每次重试时都进行新的事务)。maxRetriesorder 属性都由 Spring 配置。主要操作发生在 doConcurrentOperation Around Advice 方法中。我们尝试继续。如果我们因 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)"/>