基于模式的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应用程序上下文中定义为一个常规Java对象的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切入点表达式语言。如果您使用基于模式的声明风格,您还可以引用在@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) {
	// ...
}

在组合切入点子表达式时,在XML文档中使用&amp;&amp;会很麻烦,因此您可以使用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进行引用,不能用作命名切入点来形成复合切入点。因此,基于模式的定义风格中的命名切入点支持比@AspectJ风格提供的更有限。

声明通知

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

前置通知

前置通知在匹配的方法执行之前运行。它在<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-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) {...

抛出后通知

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

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

Around Advice

The last kind of advice is around advice. Around advice runs "around" a matched method’s execution. It has the opportunity to do work both before and after the method runs and to determine when, how, and even if the method actually gets to run at all. Around advice is often used if you need to share state before and after a method execution in a thread-safe manner – for example, starting and stopping a timer.

Always use the least powerful form of advice that meets your requirements.

For example, do not use around advice if before advice is sufficient for your needs.

You can declare around advice by using the aop:around element. The advice method should declare Object as its return type, and the first parameter of the method must be of type ProceedingJoinPoint. Within the body of the advice method, you must invoke proceed() on the ProceedingJoinPoint in order for the underlying method to run. Invoking proceed() without arguments will result in the caller’s original arguments being supplied to the underlying method when it is invoked. For advanced use cases, there is an overloaded variant of the proceed() method which accepts an array of arguments (Object[]). The values in the array will be used as the arguments to the underlying method when it is invoked. See Around Advice for notes on calling proceed with an Object[].

The following example shows how to declare around advice in XML:

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

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

	...
</aop:aspect>

The implementation of the doBasicProfiling advice can be exactly the same as in the @AspectJ example (minus the annotation, of course), as the following example shows:

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

通知参数

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

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

	<!-- 这是Spring的AOP基础设施将代理的对象 -->
	<bean id="personService" class="com.xyz.service.DefaultPersonService"/>

	<!-- 这是实际的通知本身 -->
	<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属性确定,或者通过将@Order注解添加到支持切面的bean,或者通过使bean实现Ordered接口来确定。

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

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

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

Introductions

Introductions (known as inter-type declarations in AspectJ) let an aspect declare that advised objects implement a given interface and provide an implementation of that interface on behalf of those objects.

You can make an introduction by using the aop:declare-parents element inside an aop:aspect. You can use the aop:declare-parents element to declare that matching types have a new parent (hence the name). For example, given an interface named UsageTracked and an implementation of that interface named DefaultUsageTracked, the following aspect declares that all implementors of service interfaces also implement the UsageTracked interface. (In order to expose statistics through JMX for example.)

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

The class that backs the usageTracking bean would then contain the following method:

  • Java

  • Kotlin

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

The interface to be implemented is determined by the implement-interface attribute. The value of the types-matching attribute is an AspectJ type pattern. Any bean of a matching type implements the UsageTracked interface. Note that, in the before advice of the preceding example, service beans can be directly used as implementations of the UsageTracked interface. To access a bean programmatically, you could write the following:

  • Java

  • Kotlin

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

Aspect Instantiation Models

The only supported instantiation model for schema-defined aspects is the singleton model. Other instantiation models may be supported in future releases.

Advisors

The concept of "advisors" comes from the AOP support defined in Spring and does not have a direct equivalent in AspectJ. An advisor is like a small self-contained aspect that has a single piece of advice. The advice itself is represented by a bean and must implement one of the advice interfaces described in Advice Types in Spring. Advisors can take advantage of AspectJ pointcut expressions.

Spring supports the advisor concept with the <aop:advisor> element. You most commonly see it used in conjunction with transactional advice, which also has its own namespace support in Spring. The following example shows an 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>

As well as the pointcut-ref attribute used in the preceding example, you can also use the pointcut attribute to define a pointcut expression inline.

To define the precedence of an advisor so that the advice can participate in ordering, use the order attribute to define the Ordered value of the advisor.

AOP模式示例

本节展示了如何使用模式支持重写来自AOP示例中的并发锁定失败重试示例。

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

因为我们想要重试操作,所以需要使用环绕通知,这样我们可以多次调用proceed。以下清单显示了基本的切面实现(这是一个使用模式支持的常规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)
// 标记注解
public @interface Idempotent {
}
@Retention(AnnotationRetention.RUNTIME)
// 标记注解
annotation class Idempotent

将切面更改为仅重试幂等操作涉及优化切入点表达式,以便仅匹配@Idempotent操作,如下所示:

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