Spring5.1.x官方参考指南中文版

 主页   资讯   文章   代码   电子书 

5.5基于Schema的AOP支持

如果你喜欢基于XML的格式,Spring还提供了使用新的AOP名称空间标记定义方面的支持。支持与使用@Aspectj样式时完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点介绍新的语法,并将读者引向上一节(@Aspectj support)中的讨论,以了解如何编写切入点表达式以及如何绑定通知参数。

要使用本节中描述的AOP命名空间标记,需要导入spring-aop schema,如基于XML模式的配置中所述。请参阅AOP模式以了解如何导入AOP命名空间中的标记。

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

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

5.5.1 定义一个Aspect

当使用schema支持时,一个方面是定义在你的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)当然可以像其他任何Springbean一样配置和注入依赖项。

5.5.2 定义Pointcut

你可以在<aop:config>元素中声明一个命名的切入点,让切入点定义在多个aspects和advisors之间共享。

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

<aop:config>

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

</aop:config>

注意,切入点表达式本身使用的是与@Aspectj支持中描述的相同的aspectj切入点表达式语言。如果使用基于schema的声明样式,则可以引用在切入点表达式中的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下:

<aop:config>

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

</aop:config>

假设你有一个SystemArchitecture方面,如共享公共切入点定义中所述。

然后在方面中声明一个切入点与声明一个顶级切入点非常相似,如下示例所示:

<aop:config>

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

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

        ...

    </aop:aspect>

</aop:config>

与@Aspectj方面非常相似,使用基于schema的定义样式声明的切入点可以收集连接点上下文。例如,以下切入点将此对象收集为连接点上下文,并将其传递给通知:

<aop:config>

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

        <aop:pointcut id="businessService"
            expression="execution(* com.xyz.myapp.service.*.*(..)) &amp;&amp; this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...

    </aop:aspect>

</aop:config>

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

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

当组合切入点子表达式时,&&在XML文档中很难使用,因此可以使用and、or和not关键字来代替&& || 和!。例如,前面的切入点可以更好地写如下:

<aop:config>

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

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

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...
    </aop:aspect>
</aop:config>

请注意,以这种方式定义的切入点由其XML ID引用,不能用作命名切入点以形成复合切入点。因此,schema-based定义样式中的命名切入点支持比@Aspectj样式提供的支持更为有限。

5.5.3 定义Advice

schema-based AOP 支持使用与@Aspectj样式相同的五种建议,它们具有完全相同的语义。

Before Advice

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

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

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

    ...

</aop:aspect>

这里dataAccessOperation是顶级(<aop:config>)中定义的切入点id,要以内联方式定义切入点,请使用pointcut属性替换pointcut-ref属性,如下所示:

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

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

    ...

</aop:aspect>

正如我们在讨论@Aspectj样式时所指出的,使用命名的切入点可以显著提高代码的可读性。

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

After Returning Advice

After Returning Advice,在匹配的方法执行正常完成时运行。它在一个<aop:aspect>中声明,方式与之前的通知相同。下面的示例演示如何声明它:

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

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

    ...

</aop:aspect>

正如在@Aspectj样式中一样,你可以在通知正文中获得返回值。为此,请使用返回属性来指定应将返回值传递到的参数的名称,如下示例所示:

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

    <aop:after-returning
        pointcut-ref="dataAccessOperation"
        returning="retVal"
        method="doAccessCheck"/>

    ...

</aop:aspect>

doAccessCheck方法必须要有声明名为retval的参数。此参数的类型以与@AfterReturning相同的方式约束匹配。例如,可以按如下方式声明方法签名:

public void doAccessCheck(Object retVal) {...

After Throwing Advice

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

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

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

正如在@Aspectj样式中一样,你可以在通知正文中得到抛出的异常。要执行此操作,请使用引发属性指定异常应传递到的参数的名称,如下示例所示:

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

    <aop:after-throwing
        pointcut-ref="dataAccessOperation"
        throwing="dataAccessEx"
        method="doRecoveryActions"/>

    ...

</aop:aspect>

doRecoveryActions方法必须有声明名为DataAccessEx的参数。此参数的类型约束匹配的方式与@AfterThrowing中描述的方式相同。例如,方法签名可以声明如下:

public void doRecoveryActions(DataAccessException dataAccessEx) {...

After (Finally) Advice

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

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

    <aop:after
        pointcut-ref="dataAccessOperation"
        method="doReleaseLock"/>

    ...

</aop:aspect>

Around Advice

最后一种advice是around advice的。around通知运行“around”匹配的方法执行。它有机会在方法执行之前和之后都进行工作,并确定何时、如何以及该方法真正开始执行。around advice通常用于以线程安全的方式(例如,启动和停止计时器)共享方法执行前后的状态。始终使用满足你要求的最不强大的建议形式。如果在before advice可以完成工作,不要使用around advice。

你可以使用aop:around元素来声明around advice。advice方法的第一个参数必须是ProceedingJoinPoint类型。在通知正文中,对ProceedingJoinPoint调用proceed()会导致执行基础方法。也可以使用Object[] 调用proceed方法。数组中的值在方法执行过程中用作参数。有关调用proceed使用Object[]的说明,请参阅Around advice。下面的示例演示如何在XML中声明around advice:

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

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...

</aop:aspect>

doBasicProfiling advice的实现可以与@Aspectj示例中的完全相同(当然,可以减去注解),如下示例所示:

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

Advice参数

schema-based的声明样式支持完全类型化的通知,方法与@Aspectj support中描述的方法相同,方法是按名称将切入点参数与通知方法参数匹配。有关详细信息,请参阅Advice 参数。如果你希望显式地为advice方法指定参数名(不依赖于前面描述的检测策略),可以使用advice元素的arg-names属性来指定参数名,该属性与advice注解中的argNames属性的处理方式相同。下面的示例演示如何以XML格式指定参数名:

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

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

下面这个基于XSD的方法的稍微复杂一些的示例显示了一些与强类型参数一起使用的around advice :

package x.y.service;

public interface PersonService {

    Person getPerson(String personName, int age);
}

public class DefaultFooService implements FooService {

    public Person getPerson(String name, int age) {
        return new Person(name, age);
    }
}

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

package x.y;

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配置示例将影响对特定连接点执行前面的建议:

<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="x.y.service.DefaultPersonService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config>
        <aop:aspect ref="profiler">

            <aop:pointcut id="theExecutionOfSomePersonServiceMethod"
                expression="execution(* x.y.service.PersonService.getPerson(String,int))
                and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
                method="profile"/>

        </aop:aspect>
    </aop:config>

</beans>

考虑下面的驱动脚本:

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import x.y.service.PersonService;

public final class Boot {

    public static void main(final String[] args) throws Exception {
        BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
        PersonService person = (PersonService) ctx.getBean("personService");
        person.getPerson("Pengo", 12);
    }
}

有了这样的引导类,我们将得到与标准输出类似的输出:

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

Advice Ordering

当需要在同一连接点(执行方法)执行多个通知时,排序规则如通知排序中所述。方面之间的优先级通过向支持方面的bean添加order注解或让bean实现Ordered接口来确定。

5.5.4 Introductions

介绍(在AspectJ中称为类型间声明)让Aspect声明建议的对象实现给定的接口,并代表这些对象提供该接口的实现。

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

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents
        types-matching="com.xzy.myapp.service.*+"
        implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
        default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before
        pointcut="com.xyz.myapp.SystemArchitecture.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

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

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

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

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

5.5.5 方面实例化模型

对于schema-defined 的方面,唯一支持的实例化模型是单例模型。在未来的版本中可能支持其他实例化模型。

5.5.6 Advisors

“Advisors”的概念来自于Spring中定义的AOP支持,在AspectJ中没有直接的等价物。Advisors就像一个独立的小方面,只有一条建议。通知本身由bean表示,并且必须实现在Spring的通知类型中描述的通知接口之一。Advisors可以利用AspectJ切入点表达式。

Spring使用<aop:advisor>元素支持Advisor概念。你通常会看到它与事务性advice结合使用,后者在Spring中也有自己的名称空间支持。以下示例展示了advisor:

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.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属性来定义内联的pointcut表达式。

要定义advisor的优先级以便通知可以参与排序,请使用order属性定义advisor的排序值。

5.5.7 一个AOP Schema例子

本节展示了使用Schema支持重写时,来自AOP示例的并发锁定失败重试示例。

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

因为我们想重试这个操作,所以我们需要使用around advice,这样我们可以多次调用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;
    }

}

注意,方面实现了Ordered接口,因此我们可以将方面的优先级设置为高于事务通知(我们希望每次重试时都有一个新的事务)。maxRetries和order属性都由spring配置。主要动作发生在围绕advice方法的doConcurrentOperation中。我们试着继续。如果我们因PessimisticLockingFailureException而失败,我们会再试一次,除非我们已经用尽了所有的重试尝试。

此类与@Aspectj示例中使用的类相同,但已删除注解。

相应的Spring配置如下:

<aop:config>

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

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

        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>

    </aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor"
    class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>

请注意,目前我们假定所有业务服务都是idempotent的。如果不是这样,我们可以通过引入一个Idempotent注解并使用它来注解服务操作的实现来改进方面,使它只重试真正的Idempotent操作,如下示例所示:

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

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

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