Schema-based AOP Support

如果您更喜欢基于 XML 的格式,Spring 还提供使用 aop 名称空间标签定义方面的支持。在使用 @AspectJ 样式时,支持完全相同的切入点表达式和建议类型。因此,在本节中,我们重点关注该语法,并将读者转到前一节(@AspectJ support)中的讨论,以了解书写切入点表达式和建议参数绑定。

If you prefer an XML-based format, Spring also offers support for defining aspects using the aop namespace tags. The exact same pointcut expressions and advice kinds as when using the @AspectJ style are supported. Hence, in this section we focus on that syntax and refer the reader to the discussion in the previous section (@AspectJ support) for an understanding of writing pointcut expressions and the binding of advice parameters.

要使用本部分中描述的 aop 命名空间标记,您需要导入 spring-aop 架构,如 XML Schema-based configuration 中所述。有关如何在 aop 命名空间中导入标记,请参阅 the AOP schema

To use the aop namespace tags described in this section, you need to import the spring-aop schema, as described in XML Schema-based configuration . See the AOP schema for how to import the tags in the aop namespace.

在 Spring 配置中,所有切面和通知元素都必须放在 <aop:config> 元素内(你可以在应用程序上下文配置中有多个 <aop:config> 元素)。一个 <aop:config> 元素可以包含切入点、通知和切面元素(请注意,它们必须按照此顺序声明)。

Within your Spring configurations, all aspect and advisor elements must be placed within an <aop:config> element (you can have more than one <aop:config> element in an application context configuration). An <aop:config> element can contain pointcut, advisor, and aspect elements (note that these must be declared in that order).

<aop:config> 样式的配置大量使用了 Spring 的 auto-proxying 机制。如果你已经通过使用 BeanNameAutoProxyCreator 这样的显式自动代理,这可能会导致问题(例如不织入建议)。建议的使用模式是仅使用 <aop:config> 样式或仅使用 AutoProxyCreator 样式,并且永远不要将它们混合在一起。

The <aop:config> style of configuration makes heavy use of Spring’s auto-proxying mechanism. This can cause issues (such as advice not being woven) if you already use explicit auto-proxying through the use of BeanNameAutoProxyCreator or something similar. The recommended usage pattern is to use either only the <aop:config> style or only the AutoProxyCreator style and never mix them.

Declaring an Aspect

当您使用模式支持时,切面是在 Spring 应用程序上下文中定义为 bean 的常规 Java 对象。状态和行为被捕获在该对象的字段和方法中,切入点和建议信息被捕获在 XML 中。

When you use the schema support, an aspect is a regular Java object defined as a bean in your Spring application context. The state and behavior are captured in the fields and methods of the object, and the pointcut and advice information are captured in the XML.

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

You can declare an aspect by using the <aop:aspect> element, and reference the backing bean by using the ref attribute, as the following example shows:

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

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

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

The bean that backs the aspect (aBean in this case) can of course be configured and dependency injected just like any other Spring bean.

Declaring a Pointcut

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

You can declare a named pointcut inside an <aop:config> element, letting the pointcut definition be shared across several aspects and advisors.

可以按照如下方式定义一个切点来表示服务层中任何业务服务的执行:

A pointcut that represents the execution of any business service in the service layer can be defined as follows:

<aop:config>

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

</aop:config>

请注意,切入点表达式本身使用与 @AspectJ support 中描述的相同 AspectJ 切入点表达式语言。如果您使用基于模式的声明样式,您还可以引用 @Aspect 类型中 named pointcuts 定义的在切入点表达式中。因此,另一种定义上述切入点的方法如下:

Note that the pointcut expression itself uses the same AspectJ pointcut expression language as described in @AspectJ support. If you use the schema based declaration style, you can also refer to named pointcuts defined in @Aspect types within the pointcut expression. Thus, another way of defining the above pointcut would be as follows:

<aop:config>

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

</aop:config>
1 References the businessService named pointcut defined in Sharing Named Pointcut Definitions.

如以下示例所示,在切面内声明一个连接点与声明一个顶级连接点非常相似:

Declaring a pointcut inside an aspect is very similar to declaring a top-level pointcut, as the following example shows:

<aop:config>

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

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

		...
	</aop:aspect>

</aop:config>

与使用基于模式定义样式声明的 @AspectJ 方面的方式相同,切入点可以通过收集连接点上下文。例如,以下切入点收集“this”对象作为连接点上下文并将它传递给建议:

In much the same way as an @AspectJ aspect, pointcuts declared by using the schema based definition style can collect join point context. For example, the following pointcut collects the this object as the join point context and passes it to the 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>

若要接收收集的联接点上下文,必须声明建议,方法是包括匹配名称的参数,如下所示:

The advice must be declared to receive the collected join point context by including parameters of the matching names, as follows:

  • Java

  • Kotlin

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

在组合切点子表达时,XML 文档中的 && 使用起来很不方便,因此您可以使用 and, ornot 关键字来分别代替 &&,||!。例如,前一个切点可以写得更好如下:

When combining pointcut sub-expressions, && is awkward within an XML document, so you can use the and, or, and not keywords in place of &&, ||, and !, respectively. For example, the previous pointcut can be better written as follows:

<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 样式所提供的支持更有限。

Note that pointcuts defined in this way are referred to by their XML id and cannot be used as named pointcuts to form composite pointcuts. The named pointcut support in the schema-based definition style is thus more limited than that offered by the @AspectJ style.

Declaring Advice

基于模式的 AOP 支持使用与 @AspectJ 样式相同的 5 种建议,并且它们具有完全相同的语义。

The schema-based AOP support uses the same five kinds of advice as the @AspectJ style, and they have exactly the same semantics.

Before Advice

建议在匹配的方法执行前执行。它通过使用 <aop:before>元素在<aop:aspect>中声明,如以下示例所示:

Before advice runs before a matched method execution. It is declared inside an <aop:aspect> by using the <aop:before> element, as the following example shows:

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

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

	...

</aop:aspect>

在上面的示例中,dataAccessOperation 是在顶部(<aop:config>)级别定义的 named pointcutid(请参阅 Declaring a Pointcut)。

In the example above, dataAccessOperation is the id of a named pointcut defined at the top (<aop:config>) level (see Declaring a Pointcut).

正如我们在对 @AspectJ 样式的讨论中指出的,使用 named pointcuts 可以大大提高代码的可读性。详情请参阅 Sharing Named Pointcut Definitions

As we noted in the discussion of the @AspectJ style, using named pointcuts can significantly improve the readability of your code. See Sharing Named Pointcut Definitions for details.

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

To define the pointcut inline instead, replace the pointcut-ref attribute with a pointcut attribute, as follows:

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

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

	...

</aop:aspect>

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

The method attribute identifies a method (doAccessCheck) that provides the body of the advice. This method must be defined for the bean referenced by the aspect element that contains the advice. Before a data access operation is performed (a method execution join point matched by the pointcut expression), the doAccessCheck method on the aspect bean is invoked.

After Returning Advice

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

After returning advice runs when a matched method execution completes normally. It is declared inside an <aop:aspect> in the same way as before advice. The following example shows how to declare it:

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

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

	...
</aop:aspect>

与 @AspectJ 样式相同,您可以在通知主体中获取返回值。为此,请使用 returning 属性指定应将返回值传递给的参数的名称,如下例所示:

As in the @AspectJ style, you can get the return value within the advice body. To do so, use the returning attribute to specify the name of the parameter to which the return value should be passed, as the following example shows:

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

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

	...
</aop:aspect>

doAccessCheck 方法必须声明一个名为 retVal 的参数。此参数的类型对匹配的约束与为 @AfterReturning 描述的相同。例如,您可以将方法签名声明如下:

The doAccessCheck method must declare a parameter named retVal. The type of this parameter constrains matching in the same way as described for @AfterReturning. For example, you can declare the method signature as follows:

  • Java

  • Kotlin

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

After Throwing Advice

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

After throwing advice runs when a matched method execution exits by throwing an exception. It is declared inside an <aop:aspect> by using the after-throwing element, as the following example shows:

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

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

	...
</aop:aspect>

与 @AspectJ 样式相同,您可以在通知主体中获取抛出的异常。为此,请使用 throwing 属性指定应将异常传递给的参数的名称,如下例所示:

As in the @AspectJ style, you can get the thrown exception within the advice body. To do so, use the throwing attribute to specify the name of the parameter to which the exception should be passed as the following example shows:

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

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

	...
</aop:aspect>

doRecoveryActions 方法必须声明一个名为 dataAccessEx 的参数。此参数的类型对匹配的约束与为 @AfterThrowing 描述的相同。例如,可以将方法签名声明如下:

The doRecoveryActions method must declare a parameter named dataAccessEx. The type of this parameter constrains matching in the same way as described for @AfterThrowing. For example, the method signature may be declared as follows:

  • Java

  • Kotlin

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

After (Finally) Advice

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

After (finally) advice runs no matter how a matched method execution exits. You can declare it by using the after element, as the following example shows:

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

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

	...
</aop:aspect>

Around Advice

最后一种通知是 around 通知。Around 通知在匹配方法的执行周围运行。它有机会在方法运行之前和之后都进行工作,并确定方法何时、如何,甚至是否真正运行。如果您需要在多线程方式下在方法执行之前和之后共享状态(例如,启动和停止计时器),则经常使用 around 通知。

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.

例如,如果 before 通知足以满足您的需要,请勿使用 around 通知。

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

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

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[].

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

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>

doBasicProfiling 通知的实现与 @AspectJ 示例完全相同(当然,减去注释),如下例所示:

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

Advice Parameters

基于模式的声明样式通过按名称对比切点参数和通知方法参数,以与 @AspectJ 支持相同的方式支持完全类型的通知。有关详细信息,请参阅 Advice Parameters。如果您希望明确指定通知方法的参数名称(不依赖于前述检测策略),您可以使用通知元素的 arg-names 属性来执行此操作,该属性的处理方式与通知注释中的 argNames 属性相同(如 Determining Argument Names 所述)。以下示例展示了如何在 XML 中指定参数名称:

The schema-based declaration style supports fully typed advice in the same way as described for the @AspectJ support — by matching pointcut parameters by name against advice method parameters. See Advice Parameters for details. If you wish to explicitly specify argument names for the advice methods (not relying on the detection strategies previously described), you can do so by using the arg-names attribute of the advice element, which is treated in the same manner as the argNames attribute in an advice annotation (as described in Determining Argument Names). The following example shows how to specify an argument name in XML:

<aop:before
	pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" 1
	method="audit"
	arg-names="auditable" />
1 References the publicMethod named pointcut defined in Combining Pointcut Expressions.

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

The arg-names attribute accepts a comma-delimited list of parameter names.

以下是基于 XSD 的方法的一个稍微复杂一些的示例,展示了一些与许多强类型参数结合使用的 around 通知:

The following slightly more involved example of the XSD-based approach shows some around advice used in conjunction with a number of strongly typed parameters:

  • Java

  • Kotlin

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);
	}
}
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”建议,如下例所示:

Next up is the aspect. Notice the fact that the profile(..) method accepts a number of strongly-typed parameters, the first of which happens to be the join point used to proceed with the method call. The presence of this parameter is an indication that the profile(..) is to be used as around advice, as the following example shows:

  • Java

  • Kotlin

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());
		}
	}
}
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 配置会对特定连接点执行前一个建议:

Finally, the following example XML configuration effects the execution of the preceding advice for a particular join point:

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

考虑以下驱动器脚本:

Consider the following driver script:

  • 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,我们会在标准输出上获得类似以下的输出:

With such a Boot class, we would get output similar to the following on standard output:

StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0

ms     %     Task name

00000 ? execution(getFoo)

Advice Ordering

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

When multiple pieces of advice need to run at the same join point (executing method) the ordering rules are as described in Advice Ordering. The precedence between aspects is determined via the order attribute in the <aop:aspect> element or by either adding the @Order annotation to the bean that backs the aspect or by having the bean implement the Ordered interface.

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

In contrast to the precedence rules for advice methods defined in the same @Aspect class, when two pieces of advice defined in the same <aop:aspect> element both need to run at the same join point, the precedence is determined by the order in which the advice elements are declared within the enclosing <aop:aspect> element, from highest to lowest precedence.

例如,假设在同一 <aop:aspect> 元素中定义了适用于同一连接点的“around”建议和“before”建议,为确保“around”建议比“before”建议具有更高的优先级,则必须在 <aop:before> 元素之前声明 <aop:around> 元素。

For example, given an around advice and a before advice defined in the same <aop:aspect> element that apply to the same join point, to ensure that the around advice has higher precedence than the before advice, the <aop:around> element must be declared before the <aop:before> element.

一个普遍的经验法则,如果你发现你有多条建议在同一 <aop:aspect> 元素中定义并且适用于同一连接点,则考虑将此类建议方法折叠到每个 <aop:aspect> 元素中的每个连接点的一个建议方法中,或重构这些建议成为独立的 <aop:aspect> 元素,以便你可以在方面级别对它们进行排序。

As a general rule of thumb, if you find that you have multiple pieces of advice defined in the same <aop:aspect> element that apply to the same join point, consider collapsing such advice methods into one advice method per join point in each <aop:aspect> element or refactor the pieces of advice into separate <aop:aspect> elements that you can order at the aspect level.

Introductions

引入(在 AspectJ 中称为跨类型声明)允许一个方面声明所建议对象实现给定接口,代表这些对象提供此接口的实现。

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.

你可以在 aop:aspect 中使用 aop:declare-parents 元素进行引入。你可以使用 aop:declare-parents 元素声明匹配类型有新的父级(这就是其名称的由来)。例如,给定一个名为 UsageTracked 的接口和一个名为 DefaultUsageTracked 的该接口的实现,以下方面声明服务接口的所有实现者也实现 UsageTracked 接口。(例如,为了通过 JMX 公开统计信息。)

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>

提供 usageTracking bean 的类随后将包含以下方法:

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

要实现的接口由 implement-interface 属性决定。types-matching 属性的值是 AspectJ 类型模式。任何匹配类型的 bean 都实现了 UsageTracked 接口。请注意,在前一个示例的建议之前,服务 bean 可以直接用作 UsageTracked 接口的实现。若要以编程方式访问 bean,你可以编写以下内容:

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

“顾问” 的概念源自 Spring 中定义的 AOP 支持,并且在 AspectJ 中没有直接等效项。顾问就像一个小型的、独立的方面,只包含一条通知。通知本身由 Bean 表示,并且必须实现 Advice Types in Spring 中描述的一个通知接口。顾问可以利用 AspectJ 切点表达式。

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 通过 <aop:advisor> 元素支持顾问概念。你最常见地看到它与事务建议结合使用,事务建议在 Spring 中也有自己的命名空间支持。以下示例显示了一个顾问:

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>

除了前一个示例中使用的 pointcut-ref 属性之外,你还可以使用 pointcut 属性来内联定义切入点表达式。

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.

若要定义顾问的优先级以便建议能参与排序,请使用 order 属性来定义顾问的 Ordered 值。

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.

An AOP Schema Example

本节展示了当使用模式支持重新编写时,An AOP Example 中的并发锁定失败重试示例是如何表现的。

This section shows how the concurrent locking failure retry example from An AOP Example looks when rewritten with the schema support.

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

The execution of business services can sometimes fail due to concurrency issues (for example, a deadlock loser). If the operation is retried, it is likely to succeed on the next try. For business services where it is appropriate to retry in such conditions (idempotent operations that do not need to go back to the user for conflict resolution), we want to transparently retry the operation to avoid the client seeing a PessimisticLockingFailureException. This is a requirement that clearly cuts across multiple services in the service layer and, hence, is ideal for implementing through an aspect.

因为我们想要重试该操作,因此我们需要使用around建议,以便我们可以多次调用`proceed`。以下清单显示了基本切面实现(这是一个使用模式支持的常规Java类):

Because we want to retry the operation, we need to use around advice so that we can call proceed multiple times. The following listing shows the basic aspect implementation (which is a regular Java class that uses the schema support):

  • 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`接口,以便我们可以将切面的优先级设置得高于事务建议(我们希望每次重试都会有一个新的事务)。`maxRetries`和`order`属性都由Spring配置。主操作发生在around建议方法`doConcurrentOperation`中。我们尝试继续。如果我们因`PessimisticLockingFailureException`失败,我们会重新尝试,除非我们已经用尽了所有重试尝试。

Note that the aspect implements the Ordered interface so that we can set the precedence of the aspect higher than the transaction advice (we want a fresh transaction each time we retry). The maxRetries and order properties are both configured by Spring. The main action happens in the doConcurrentOperation around advice method. We try to proceed. If we fail with a PessimisticLockingFailureException, we try again, unless we have exhausted all of our retry attempts.

此类与在 @AspectJ 示例中使用的类相同,但已删除注释。

This class is identical to the one used in the @AspectJ example, but with the annotations removed.

相应的Spring配置如下:

The corresponding Spring configuration is as follows:

<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`注释并使用该注释注释服务操作的实现,如下例所示:

Notice that, for the time being, we assume that all business services are idempotent. If this is not the case, we can refine the aspect so that it retries only genuinely idempotent operations, by introducing an Idempotent annotation and using the annotation to annotate the implementation of service operations, as the following example shows:

  • Java

  • Kotlin

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

仅重试幂等操作的切面变化涉及优化切入表达式,以便仅匹配`@Idempotent`操作,如下所示:

The change to the aspect to retry only idempotent operations involves refining the pointcut expression so that only @Idempotent operations match, as follows:

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