对于最新的稳定版本,请使用 Spring Framework 7.0.6!spring-doc.cadn.net.cn

基于模式的 AOP 支持

如果你更喜欢基于XML的格式,Spring还提供了使用aop命名空间标签定义切面的支持。与使用@AspectJ风格时相同的支持点切点表达式和通知类型。因此,在本节中,我们将重点介绍该语法,并请读者参考前一节(@AspectJ支持)以了解编写点切点表达式和绑定通知参数的相关内容。spring-doc.cadn.net.cn

要使用本节中描述的aop命名空间标签,您需要导入spring-aop模式,如基于XML模式的配置中所述。有关如何在aop命名空间中导入标签的信息,请参见AOP模式spring-doc.cadn.net.cn

在您的Spring配置中,所有切面和通知元素必须放置在<aop:config>元素内(您可以在应用程序上下文配置中拥有多个<aop:config>元素)。一个<aop:config>元素可以包含切入点、通知和切面元素(请注意,这些元素必须按此顺序声明)。spring-doc.cadn.net.cn

The <aop:config> 风格的配置大量使用了Spring的 自动代理 机制。这可能会导致问题(例如,建议未被编织)如果你已经通过使用 BeanNameAutoProxyCreator 或类似的方式显式地使用自动代理。推荐的使用模式是只使用 <aop:config> 风格或只使用 AutoProxyCreator 风格,不要混合使用它们。

声明一个切面

当你使用模式支持时,切面是一个在你的Spring应用程序上下文中定义为bean的常规Java对象。对象的状态和行为通过其字段和方法来捕捉,而切入点和通知信息则通过XML来捕捉。spring-doc.cadn.net.cn

你可以通过使用<aop:aspect>元素来声明一个切面,并通过使用ref属性来引用支持的bean,如下例所示:spring-doc.cadn.net.cn

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

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

支持该切面的bean(在这种情况下为aBean)当然可以像其他任何Spring bean一样进行配置和依赖注入。spring-doc.cadn.net.cn

声明切入点

您可以在 <aop:config> 元素内声明一个命名切点,使得切点定义能够跨多个切面和通知器共享。spring-doc.cadn.net.cn

一个表示服务层中任何业务服务执行的切入点可以如下定义:spring-doc.cadn.net.cn

<aop:config>

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

</aop:config>

注意,切入点表达式本身使用的是与@AspectJ支持中描述的相同的AspectJ切入点表达式语言。如果采用基于schema的声明风格,在切入点表达式中还可以引用@Aspect类型中定义的命名切入点。因此,定义上述切入点的另一种方式如下所示:spring-doc.cadn.net.cn

<aop:config>

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

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

在切面内部声明切入点与声明顶级切入点非常相似,如下示例所示:spring-doc.cadn.net.cn

<aop:config>

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

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

		...
	</aop:aspect>

</aop:config>

以类似于@AspectJ切面的方式,使用基于模式的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点将this对象作为连接点上下文收集并传递给通知:spring-doc.cadn.net.cn

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

建议必须声明接收收集的连接点上下文,方法是包含匹配名称的参数,如下所示:spring-doc.cadn.net.cn

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

在组合切入点子表达式时,&amp;&amp; 在 XML 文档中显得笨拙,因此您可以使用 andornot 关键字分别代替 &amp;&amp;||!。例如,之前的切入点可以更好地写成如下:spring-doc.cadn.net.cn

<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风格提供的支持更为有限。spring-doc.cadn.net.cn

声明通知

基于模式的AOP支持使用与@AspectJ风格相同的五种通知类型,并且它们具有完全相同的语义。spring-doc.cadn.net.cn

前置通知

在匹配的方法执行之前运行的前置通知。它通过使用<aop:before>元素在<aop:aspect>中声明,如下例所示:spring-doc.cadn.net.cn

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

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

	...

</aop:aspect>

在上面的例子中,dataAccessOperation 是定义在顶层(<aop:config>)的命名切点id(参见声明切点)。spring-doc.cadn.net.cn

正如我们在讨论@AspectJ风格时提到的,使用命名切点可以显著提高代码的可读性。详情请参见共享命名切点定义

若要改为内联定义切入点,请将 pointcut-ref 属性替换为 pointcut 属性,如下所示:spring-doc.cadn.net.cn

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

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

	...

</aop:aspect>

The method 属性标识了一个方法 (doAccessCheck),该方法提供了通知的主体。此方法必须为包含通知的 aspect 元素所引用的 bean 定义。在执行数据访问操作之前(通过切入点表达式匹配的方法执行连接点),将调用 aspect bean 上的 doAccessCheck 方法。spring-doc.cadn.net.cn

返回后通知

在返回后通知会在匹配的方法正常执行完毕后运行。它与前置通知一样,声明在一个<aop:aspect>中。以下示例展示了如何声明它:spring-doc.cadn.net.cn

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

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

	...
</aop:aspect>

正如@AspectJ风格中那样,你可以在通知体中获取返回值。 为此,请使用returning属性指定应将返回值传递给的参数名称,如下例所示:spring-doc.cadn.net.cn

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

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

	...
</aop:aspect>

The doAccessCheck 方法必须声明一个名为 retVal 的参数。此参数的类型以与描述 @AfterReturning 相同的方式约束匹配。例如,您可以如下声明方法签名:spring-doc.cadn.net.cn

public void doAccessCheck(Object retVal) {...
fun doAccessCheck(retVal: Any) {...

在抛出建议后

在抛出建议(after throwing advice)在匹配的方法执行通过抛出异常退出时运行。它是在<aop:aspect>中使用after-throwing元素声明的,如下例所示:spring-doc.cadn.net.cn

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

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

	...
</aop:aspect>

正如@AspectJ风格中那样,你可以在通知体中获取抛出的异常。 为此,请使用throwing属性来指定应将异常传递给的参数名称,如下例所示:spring-doc.cadn.net.cn

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

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

	...
</aop:aspect>

The doRecoveryActions 方法必须声明一个名为 dataAccessEx 的参数。 该参数的类型以与描述 @AfterThrowing 相同的方式约束匹配。 例如,方法签名可以如下声明:spring-doc.cadn.net.cn

public void doRecoveryActions(DataAccessException dataAccessEx) {...
fun doRecoveryActions(dataAccessEx: DataAccessException) {...

最后通知

在匹配的方法执行退出后,无论以何种方式,都会运行 (finally) advice。 你可以通过使用 after 元素来声明它,如下例所示:spring-doc.cadn.net.cn

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

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

	...
</aop:aspect>

环绕通知

最后一种建议是“环绕”建议。环绕建议会在匹配方法执行的“周围”运行。它有机会在方法执行前后执行操作,并决定方法是否实际运行、何时以及如何运行。如果需要以线程安全的方式在方法执行前后共享状态,通常会使用环绕建议——例如,启动和停止计时器。spring-doc.cadn.net.cn

始终使用满足您需求的最弱形式的建议。spring-doc.cadn.net.cn

例如,如果您的需求只需要之前通知,则不要在周围使用通知。spring-doc.cadn.net.cn

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

以下示例显示了如何在 XML 中声明环绕通知:spring-doc.cadn.net.cn

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

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

	...
</aop:aspect>

实现 doBasicProfiling 通知可以与 @AspectJ 示例完全相同(当然,除了注解),如下例所示:spring-doc.cadn.net.cn

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

参数建议

基于模式的声明风格以与@AspectJ支持相同的方式支持完全类型化的通知——通过按名称匹配切入点参数和通知方法参数。有关详细信息,请参阅通知参数。如果您希望显式指定通知方法的参数名称(不依赖于之前描述的检测策略),可以通过使用通知元素的arg-names属性来实现,该属性的处理方式与通知注解中的argNames属性相同(如确定参数名称中所述)。以下示例展示了如何在XML中指定参数名称:spring-doc.cadn.net.cn

<aop:before
	pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" (1)
	method="audit"
	arg-names="auditable" />
1 引用在组合切点表达式章节中定义的名为publicMethod的切点。

The arg-names 属性接受以逗号分隔的参数名称列表。spring-doc.cadn.net.cn

以下是一个稍微复杂一些的基于XSD的方法示例,展示了在多个强类型参数结合使用时的一些环绕通知:spring-doc.cadn.net.cn

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通知,如下例所示:spring-doc.cadn.net.cn

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配置实现了对特定连接点执行前面所述的建议:spring-doc.cadn.net.cn

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

考虑以下驱动脚本:spring-doc.cadn.net.cn

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类,我们将在标准输出中获得类似以下的结果:spring-doc.cadn.net.cn

StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)

建议排序

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

与在同一个@Aspect类中定义的建议方法的优先级规则不同,当在同一<aop:aspect>元素中定义的两个建议都需要在同一连接点运行时,优先级由这些建议元素在包含的<aop:aspect>元素中的声明顺序决定,从最高优先级到最低优先级。spring-doc.cadn.net.cn

例如,给定一个 around 建议和一个 before 建议,它们在同一个 <aop:aspect> 元素中定义,并且适用于同一个连接点,为了确保 around 建议的优先级高于 before 建议,<aop:around> 元素必须在 <aop:before> 元素之前声明。spring-doc.cadn.net.cn

作为一个通用的经验法则,如果你发现同一个<aop:aspect>元素中定义了多个适用于同一连接点的切面通知,考虑将这些通知方法合并为每个<aop:aspect>元素中的每个连接点一个通知方法,或者将这些通知重构为可以按切面级别排序的单独<aop:aspect>元素。spring-doc.cadn.net.cn

介绍

介绍(在AspectJ中称为类型间声明)允许一个切面声明被通知的对象实现给定的接口,并为这些对象提供该接口的实现。spring-doc.cadn.net.cn

你可以通过在aop:aspect内使用aop:declare-parents元素来制作介绍。 你可以使用aop:declare-parents元素声明匹配的类型有一个新的父类(因此得名)。 例如,给定一个名为UsageTracked的接口和该接口的一个实现名为DefaultUsageTracked,以下切面声明所有服务接口的实现者也实现了UsageTracked接口。(例如,为了通过JMX暴露统计信息。)spring-doc.cadn.net.cn

<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的类将包含以下方法:spring-doc.cadn.net.cn

public void recordUsage(UsageTracked usageTracked) {
	usageTracked.incrementUseCount();
}
fun recordUsage(usageTracked: UsageTracked) {
	usageTracked.incrementUseCount()
}

要实现的接口由implement-interface属性确定。types-matching属性的值是一个AspectJ类型模式。任何匹配类型的bean都实现了UsageTracked接口。请注意,在前面示例的前置通知中,服务bean可以直接用作UsageTracked接口的实现。要程序化地访问一个bean,你可以编写如下代码:spring-doc.cadn.net.cn

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

切面实例化模型

唯一支持的模式定义方面实例化模型是单例模型。未来版本可能会支持其他实例化模型。spring-doc.cadn.net.cn

通知器

"顾问器"(advisors)的概念源于Spring中定义的AOP支持,在AspectJ中没有直接对应的概念。顾问器类似于一个自包含的小型切面,仅包含一条通知。通知本身由bean实现,且必须实现Spring中的通知类型中描述的某个通知接口。顾问器可利用AspectJ切点表达式。spring-doc.cadn.net.cn

Spring 支持带有 <aop:advisor> 元素的顾问概念。你最常见的是在与事务建议结合使用时看到它,事务建议在 Spring 中也有自己的命名空间支持。以下示例展示了一个顾问:spring-doc.cadn.net.cn

<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 属性来内联定义切点表达式。spring-doc.cadn.net.cn

要定义顾问的优先级,以便建议可以参与排序,请使用order属性来定义顾问的Ordered值。spring-doc.cadn.net.cn

AOP模式示例

本节展示了如何将并发锁定失败重试示例(来自一个AOP示例)使用模式支持重写。spring-doc.cadn.net.cn

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

因为我们想要重试操作,所以我们需要使用环绕通知,以便我们可以多次调用proceed。以下示例展示了基本的切面实现(这是一个使用模式支持的常规Java类):spring-doc.cadn.net.cn

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,我们将再次尝试,除非我们已经用尽了所有重试机会。spring-doc.cadn.net.cn

这个类与在 @AspectJ 示例中使用的类相同,但去掉了注解。

对应的Spring配置如下:spring-doc.cadn.net.cn

<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 注解,并使用该注解对服务操作的实现进行标注,如下例所示:spring-doc.cadn.net.cn

@Retention(RetentionPolicy.RUNTIME)
// marker annotation
public @interface Idempotent {
}
@Retention(AnnotationRetention.RUNTIME)
// marker annotation
annotation class Idempotent

将 切面更改为仅重试幂等操作涉及细化切入点表达式,以便仅匹配@Idempotent操作,如下所示:spring-doc.cadn.net.cn

<aop:pointcut id="idempotentOperation"
		expression="execution(* com.xyz.service.*.*(..)) and
		@annotation(com.xyz.service.Idempotent)"/>