跳到主要内容

基于 Schema 的 AOP 支持

ChatGPT-4o-mini 中英对照 Schema-based AOP Support

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

要使用本节中描述的 aop 命名空间标签,您需要导入 spring-aop 模式,如 XML Schema-based configuration 中所述。有关如何导入 aop 命名空间中的标签,请参见 the AOP schema

在你的 Spring 配置中,所有的 aspect 和 advisor 元素必须放在 <aop:config> 元素内(在应用上下文配置中可以有多个 <aop:config> 元素)。一个 <aop:config> 元素可以包含 pointcut、advisor 和 aspect 元素(注意这些元素必须按照这个顺序声明)。

注意

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

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

声明切点

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

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

<aop:config>

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

</aop:config>
xml

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

<aop:config>

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

</aop:config>
xml

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

<aop:config>

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

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

...
</aop:aspect>

</aop:config>
xml

与 @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>
xml

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

public void monitor(Object service) {
// ...
}
java

在组合切入点子表达式时,&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

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

声明建议

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

前置通知

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

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

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

...

</aop:aspect>
xml

在上面的例子中,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>
xml

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

如同 @AspectJ 风格,您可以在通知体内获取返回值。为此,使用 returning 属性指定返回值应传递给的参数名称,如以下示例所示:

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

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

...
</aop:aspect>
xml

doAccessCheck 方法必须声明一个名为 retVal 的参数。该参数的类型以与 @AfterReturning 中描述的方式限制匹配。例如,您可以将方法签名声明如下:

public void doAccessCheck(Object retVal) {...
java

抛出异常后的通知

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

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

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

...
</aop:aspect>
xml

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

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

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

...
</aop:aspect>
xml

doRecoveryActions 方法必须声明一个名为 dataAccessEx 的参数。该参数的类型限制了匹配方式,和 @AfterThrowing 中描述的方式相同。例如,方法签名可以声明如下:

public void doRecoveryActions(DataAccessException dataAccessEx) {...
java

After (最终) 建议

在(最终)建议运行后,无论匹配的方法执行如何退出。您可以通过使用 after 元素来声明它,如下例所示:

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

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

...
</aop:aspect>
xml

环绕通知

最后一种建议是 环绕 建议。环绕建议在匹配的方法执行 "周围" 运行。它有机会在方法运行之前和之后进行工作,并确定何时、如何,甚至是否该方法实际上会运行。环绕建议通常在需要以线程安全的方式在方法执行前后共享状态时使用 - 例如,启动和停止计时器。

提示

始终使用满足您需求的最低级别建议。

例如,如果 before 建议足以满足您的需求,则不要使用 around 建议。

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

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

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

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

...
</aop:aspect>
xml

doBasicProfiling 建议的实现可以与 @AspectJ 示例完全相同(当然,去掉注解),如下例所示:

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
java

建议参数

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

<aop:before
pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" // <1>
method="audit"
arg-names="auditable" />
xml

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

以下这个稍微复杂一些的基于 XSD 的方法示例展示了一些与多个强类型参数结合使用的建议:

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);
}
}
java

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

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

最后,以下示例 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>
xml

考虑以下驱动脚本:

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);
}
}
java

有了这样的 Boot 类,我们将在标准输出中获得类似以下的输出:

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

建议排序

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

备注

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

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

作为一般经验法则,如果您发现自己在同一个 <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>
xml

支持 usageTracking bean 的类将包含以下方法:

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

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

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

方面实例化模型

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

Advisors

“顾问”的概念源自 Spring 中定义的 AOP 支持,并且在 AspectJ 中没有直接的对应物。顾问就像一个小型的自包含切面,具有单一的建议。建议本身由一个 bean 表示,并且必须实现 Advice Types in 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>
xml

除了在前面的示例中使用的 pointcut-ref 属性外,您还可以使用 pointcut 属性在线定义切入点表达式。

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

AOP 模式示例

本节展示了来自 An AOP Example 的并发锁定失败重试示例在使用架构支持时的重写效果。

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

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

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;
}
}
java

请注意,方面实现了 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>
xml

请注意,目前我们假设所有业务服务都是幂等的。如果情况并非如此,我们可以改进该方面,使其仅重试真正的幂等操作,通过引入一个 Idempotent 注解,并使用该注解来标注服务操作的实现,如以下示例所示:

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

将重试仅限于幂等操作的方面的更改涉及到精炼切入点表达式,以便仅匹配 @Idempotent 操作,如下所示:

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