跳到主要内容
版本:7.0.3

基于模式的AOP支持

Hunyuan 7b 中英对照 Schema-based AOP Support

如果你更倾向于使用基于XML的格式,Spring也支持使用aop命名空间标签来定义切面。所支持的切点表达式和通知类型与使用@AspectJ风格时完全相同。因此,在本节中我们将重点介绍这种语法,并建议读者参考前一节的讨论(@AspectJ支持),以便更好地理解如何编写切点表达式以及如何绑定通知参数。

要使用本节中描述的aop命名空间标签,你需要导入spring-aop模式(schema),如基于XML模式的配置中所介绍的那样。有关如何导入aop命名空间中的标签,请参阅AOP模式

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

注意

<aop:config> 配置风格大量使用了 Spring 的 自动代理 机制。如果您已经通过使用 BeanNameAutoProxyCreator 或类似方式实现了显式的自动代理,那么使用 <aop:config> 可能会引发问题(例如通知(advice)无法正确编织)。推荐的用法模式是:要么仅使用 <aop:config> 风格,要么仅使用 AutoProxyCreator 风格,切勿将两者混用。

声明一个切面

当你使用模式支持时,一个“方面”(aspect)就是在你Spring应用程序上下文中被定义为Bean的普通Java对象。该对象的状态和行为被封装在其字段和方法中,而切点(pointcut)与建议(advice)的相关信息则被保存在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>元素内部声明一个命名切点(named pointcut),这样该切点的定义就可以在多个切面(aspect)和顾问(advisor)之间共享。

可以如下定义一个切点(pointcut),该切点代表服务层中任何业务服务的执行:

<aop:config>

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

</aop:config>

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

<aop:config>

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

</aop:config>

在切面(aspect)内部声明切入点(pointcut)与声明顶级切入点(top-level pointcut)非常相似,以下示例可以说明这一点:

<aop:config>

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

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

...
</aop:aspect>

</aop:config>

与@AspectJ方面(aspect)非常相似,使用基于模式的定义方式声明的切点(pointcut)也可以收集连接点上下文(join point context)。例如,以下切点会收集this对象作为连接点上下文,并将其传递给建议(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>

要接收收集到的连接点上下文,必须声明该建议,并包含匹配名称的参数,如下所示:

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

在组合点切子表达式时,&&在XML文档中使用起来不太方便,因此你可以分别使用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风格相同的五种类型的通知(advices),它们具有完全相同的语义。

事前建议

在匹配到的方法执行之前,会先执行前置顾问(before advice)。它通过使用 <aop:before> 元素在 <aop:aspect> 中进行声明,如下例所示:

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

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

...

</aop:aspect>

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

备注

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

相反,要内联定义切点(pointcut),请将 pointcut-ref 属性替换为 pointcut 属性,如下所示:

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

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

...

</aop:aspect>

method属性标识了一个方法(doAccessCheck),该方法提供了该建议(advice)的实现内容。这个方法必须为包含该建议的Aspect元素所引用的Bean定义。在执行数据访问操作之前(即由点切表达式(pointcut expression)匹配到的方法执行连接点(method execution join point)时),会调用Aspect Bean上的doAccessCheck方法。

返回后的建议

当匹配的方法执行正常完成后,返回通知(return advice)会执行。它的声明方式与之前的通知(advice)相同,也是放在 <aop:aspect> 标签内部。以下示例展示了如何声明它:

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

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

...
</aop:aspect>

与@AspectJ风格类似,你可以在建议体(advise body)中获取返回值。为此,使用returning属性来指定应传递返回值的参数名称,如下例所示:

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

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

...
</aop:aspect>

doAccessCheck 方法必须声明一个名为 retVal 的参数。该参数的类型约束方式与 @AfterReturning 所描述的相同。例如,你可以如下声明该方法的签名:

public void doAccessCheck(Object retVal) {...

抛出异常后的建议

当匹配到的方法执行因抛出异常而结束时,after-throwing建议会运行。如下例所示,它是在 <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 所描述的相同。例如,该方法签名可以如下声明:

public void doRecoveryActions(DataAccessException dataAccessEx) {...

最后的建议(Finally Advice)

无论匹配方法的执行如何结束,after(最终)回调总是会被执行。你可以通过使用after元素来声明它,如下例所示:

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

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

...
</aop:aspect>

周边建议

最后一种建议是“环绕式建议”(around advice)。这种建议会在匹配到的方法执行过程中“环绕”其执行流程。它有机会在方法执行前后进行操作,甚至可以决定该方法是否真的会执行。当需要在线程安全的方式下在方法执行前后共享某些状态时,通常会使用环绕式建议——例如,启动和停止定时器。

提示

始终使用满足你需求的最不强的建议形式。
例如,如果“在...之前”这种建议就足以满足你的需求,就不要使用“在...周围”这种建议了。

你可以使用 aop:around 元素来环绕建议(advice)代码。建议方法的返回类型应声明为 Object,并且该方法的第一个参数必须是 ProceedingJoinPoint 类型。在建议方法的主体内部,你必须对 ProceedingJoinPoint 调用 proceed() 方法,以便底层方法能够被执行。如果不带参数调用 proceed(),那么在底层方法被调用时,将会使用调用者的原始参数。对于更高级的用例,proceed() 方法还有一个重载版本,它可以接受一个参数数组(Object[])。该数组中的值将在底层方法被调用时作为其参数传递。有关如何使用 Object[] 调用 proceed() 的详细信息,请参阅 环绕建议

以下示例展示了如何在XML中声明围绕“建议”(advice)的内容:

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

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

...
</aop:aspect>

doBasicProfiling建议的实现完全可以与@AspectJ示例中的实现相同(当然,不需要注释),如下例所示:

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

建议参数

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

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

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

接下来是另一个方面。请注意,profile(..) 方法接受多个强类型参数,其中第一个参数正是用于执行方法调用的连接点(join point)。这个参数的存在表明 profile(..) 应该被用作 “around” 规范(advise),如下例所示:

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

最后,以下示例XML配置实现了对特定连接点(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>

考虑以下驱动脚本:

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

有了这样的Boot类,我们在标准输出上会得到类似以下的输出:

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

建议排序

当多条建议需要在同一个连接点(执行方法)同时被执行时,其排序规则遵循Advice Ordering中的描述。这些方面的优先级是通过 <aop:aspect> 元素中的 order 属性来确定的,或者通过给支持该方面的bean添加 @Order 注解来决定,又或者是让该bean实现 Ordered 接口来设定优先级。

备注

与在同一@Aspect类中定义的建议方法的优先级规则不同,当在同一<aop:aspect>元素中定义的两个建议都需要在同一个连接点(join point)上执行时,优先级是由这些建议元素在封闭的<aop:aspect>元素中的声明顺序决定的,从最高优先级到最低优先级排列。

例如,如果在同一个<aop:aspect>元素中定义了一个around建议和一个before建议,并且它们都适用于同一个连接点,为了确保around建议的优先级高于before建议,那么必须将<aop:around>元素的声明放在<aop:before>元素之前。

作为一个通用的经验法则,如果你发现在同一<aop:aspect>元素中定义了多个适用于同一连接点的建议,可以考虑将这些建议方法合并为每个连接点一个建议方法,或者将这些建议重构为单独的<aop:aspect>元素,这样你就可以在方面(aspect)级别上对这些建议进行排序了。

介绍

在AspectJ中,引入(introductions)被称为“类型间声明”(inter-type declarations),它允许一个切面(aspect)声明被通知的对象(advised objects)实现某个给定的接口,并代表这些对象提供该接口的实现。

你可以在 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>

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

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

要实现的接口由implement-interface属性确定。types-matching属性的值是一个AspectJ类型模式。任何匹配类型的Bean都会实现UsageTracked接口。请注意,在前面示例的“before”建议中,服务Bean可以直接用作UsageTracked接口的实现。要以编程方式访问一个Bean,你可以编写以下代码:

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

方面实例化模型

对于由模式定义的方面(schema-defined aspects),唯一支持的实例化模型是单例模型(singleton model)。在未来的版本中,可能会支持其他实例化模型。

顾问

“顾问”(advisors)的概念源自Spring中定义的AOP支持,在AspectJ中没有直接的等价概念。顾问(advisor)就像一个小型、自包含的切面(aspect),它包含一条单一的“建议”(advice)。这条“建议”本身由一个bean表示,且必须实现Spring中的建议类型中描述的某个建议接口(Advice Types in Spring)。顾问可以利用AspectJ的切点表达式(pointcut expressions)。

Spring通过 <aop:advisor> 元素支持顾问(advisor)的概念。你最常看到它与事务性顾问(transactional advice)一起使用,而事务性顾问在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>

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

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

AOP模式示例

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

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

因为我们需要重试该操作,所以我们需要使用 around 建议,以便我们可以多次调用 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;
}
}

请注意,该方面(aspect)实现了Ordered接口,这样我们就可以将这个方面的优先级设置得高于事务建议(advice)的优先级(我们希望在每次重试时都启动一个新的事务)。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注解,并使用该注解来标记服务操作的实现,从而优化这个方面,使其仅重试真正幂等的操作,如下例所示:

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

将重试范围改为仅针对幂等操作,需要优化切点表达式,以便只有标记为@Idempotent的操作才能被匹配,具体修改如下:

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