5.5. 基于模式的 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样式并且从不混合使用它们。

5.5.1. 声明一个切面

当您使用模式支持时,切面是在 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>

支持切面的 bean(在这种情况下是aBean)当然可以像任何其他 Spring bean 一样进行配置和依赖注入。

5.5.2. 声明切入点

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

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

<aop:config>

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

</aop:config>

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

<aop:config>

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

</aop:config>

假设您有共享通用切入点定义中描述的CommonPointcuts切面。

然后在切面内声明切入点与声明顶级切入点非常相似,如以下示例所示:

<aop:config>

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

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

        ...
    </aop:aspect>

</aop:config>

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

<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 文档中很尴尬,因此您可以分别使用andornot关键字来代替&&||!。例如,前面的切入点可以更好地写成如下:

<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不能用作命名切入点来形成复合切入点。因此,基于模式的定义风格中的命名切入点支持比@AspectJ 风格提供的更有限。

5.5.3. 声明通知

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

前置通知

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

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

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

    ...

</aop:aspect>

这里,dataAccessOperation是在顶层 ( <aop:config>) 级别定义的切入点的id 。要改为内联定义切入点,请将pointcut-ref属性替换为pointcut属性,如下所示:

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

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

    ...
</aop:aspect>

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

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

返回通知

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

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

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

    ...
</aop:aspect>

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

<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) {...

异常通知

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

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

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

    ...
</aop:aspect>

与@AspectJ 风格一样,您可以在通知正文中获取抛出的异常。为此,请使用throwing属性指定应将异常传递到的参数的名称,如以下示例所示:

<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元素来声明它,如以下示例所示:

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

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

    ...
</aop:aspect>

环绕通知

最后一种通知是环绕通知。环绕通知“围绕”匹配方法的执行。它有机会在方法运行之前和之后进行工作,并确定该方法何时、如何以及是否真正开始运行。如果您需要以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器),则通常使用环绕通知。

始终使用满足您要求的最不强大的通知形式。例如,如果之前的通知足以满足您的需求,请不要使用环绕通知。

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

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

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

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

    ...
</aop:aspect>

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

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

通知参数

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

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

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

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

package x.y.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(..)方法接受许多强类型参数,其中第一个参数恰好是用于继续进行方法调用的连接点。此参数的存在表明 profile(..)将用作around通知,如以下示例所示:

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

使用这样的 Boot 类,我们将在标准输出中获得类似于以下内容的输出:

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

通知优先级

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

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

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

5.5.4. 切面说明

切面说明(在 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.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.CommonPointcuts.businessService()
            and this(usageTracked)"
            method="recordUsage"/>

</aop:aspect>

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

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

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

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

5.5.5. 切面实例化模型

唯一受支持的模式定义切面的实例化模型是单例模型。未来版本可能支持其他实例化模型。

5.5.6. 顾问

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

Spring 通过<aop:advisor>元素支持顾问概念。您最常看到它与事务通知一起使用,后者在 Spring 中也有自己的命名空间支持。以下示例显示了一个顾问:

<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属性内联定义切入点表达式。

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

5.5.7。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;
    }
}

请注意,切面实现了Ordered接口,以便我们可以将切面的优先级设置为高于事务通知(我们希望每次重试时都有一个新事务)。maxRetriesorder属性都是由 Spring 配置的。主要动作doConcurrentOperation发生在around 通知方法中。我们尝试继续。如果我们以 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注解来注解服务操作的实现,来细化切面,使其仅重试真正的幂等操作,如以下示例所示:

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

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

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

最后更新于