基于模式的 AOP 支持

如果你更喜欢基于 XML 的格式,Infra 还支持使用 aop 命名空间标签定义切面。 使用 @AspectJ 风格时支持的切入点表达式和 advice 类型与此完全相同。 因此,在本节中,我们重点介绍该语法,并让读者参考上一节(@AspectJ 支持)中的讨论,以了解编写切入点表达式和 advice 参数的绑定。

要使用本节中描述的 aop 命名空间标签,你需要导入 infra-aop 模式,如 基于 XML 模式的配置 中所述。 有关如何在 aop 命名空间中导入标签,请参阅 AOP 模式

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

<aop:config> 配置风格大量使用 Infra 自动代理 机制。 如果你已经通过使用 BeanNameAutoProxyCreator 或类似的东西使用了显式自动代理,这可能会导致问题(例如 advice 未被织入)。 推荐的使用模式是仅使用 <aop:config> 风格或仅使用 AutoProxyCreator 风格,切勿混合使用。

声明 Aspect

当你使用模式支持时,切面是在 Infra 应用程序上下文中定义为 bean 的常规 Java 对象。 状态和行为在对象的字段和方法中捕获,而切入点和 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)当然可以像任何其他 Infra bean 一样进行配置和依赖注入。

声明 Pointcut

你可以在 <aop:config> 元素内声明一个 命名 pointcut,让 pointcut 定义在多个 aspect 和 advisor 之间共享。

代表服务层中任何业务服务执行的 pointcut 可以定义如下:

<aop:config>

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

</aop:config>

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

<aop:config>

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

</aop:config>
1 引用 共享命名 Pointcut 定义 中定义的 businessService 命名 pointcut。

在 aspect 内部 声明 pointcut 与声明顶级 pointcut 非常相似,如下例所示:

<aop:config>

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

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

    ...
  </aop:aspect>

</aop:config>

与 @AspectJ 切面非常相似,使用基于模式的定义风格声明的 pointcut 可以收集连接点上下文。 例如,以下 pointcut 收集 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>

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

  • Java

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

组合 pointcut 子表达式时,&amp;&amp; 在 XML 文档中很尴尬,因此你可以使用 andornot 关键字分别代替 &amp;&amp;||!。 例如,前面的 pointcut 可以更好地写成如下:

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

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

声明 Advice

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

前置 Advice

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

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

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

  ...

</aop:aspect>

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

正如我们在 @AspectJ 风格的讨论中指出的那样,使用 命名 pointcut 可以显着提高代码的可读性。 有关详细信息,请参阅 共享命名 Pointcut 定义

要改为内联定义 pointcut,请将 pointcut-ref 属性替换为 pointcut 属性,如下所示:

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

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

  ...

</aop:aspect>

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

返回后 Advice

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

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

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

  ...
</aop:aspect>

就像在 @AspectJ 风格中一样,你可以在 advice 主体内获取返回值。 为此,请使用 returning 属性指定应传递返回值的参数的名称,如下例所示:

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

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

  ...
</aop:aspect>

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

  • Java

public void doAccessCheck(Object retVal) {...

抛出后 Advice

抛出后 advice 在匹配的方法执行通过抛出异常退出时运行。它在 <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 描述的相同方式约束匹配。 例如,方法签名可以如下声明:

  • Java

public void doRecoveryActions(DataAccessException dataAccessEx) {...

After (Finally) Advice

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

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

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

  ...
</aop:aspect>

环绕 Advice

最后一种 advice 是 环绕 advice。环绕 advice 在匹配的方法执行“周围”运行。 它有机会在方法运行之前和之后做工作,并确定何时、如何以及即使方法实际上完全运行。 如果你需要以线程安全的方式在方法执行之前和之后共享状态(例如,启动和停止计时器),通常使用环绕 advice。

始终使用满足你要求的最不强大的 advice 形式。

例如,如果 前置 advice 足以满足你的需求,请不要使用 环绕 advice。

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

以下示例显示了如何在 XML 中声明环绕 advice:

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

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

  ...
</aop:aspect>

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

  • Java

public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
  // 启动秒表
  Object retVal = pjp.proceed();
  // 停止秒表
  return retVal;
}

Advice 参数

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

<aop:before pointcut="com.xyz.Pointcuts.publicMethod() and @annotation(auditable)" (1)
  method="audit" arg-names="auditable" />
1 引用 组合 Pointcut 表达式 中定义的 publicMethod 命名 pointcut。

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

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

  • Java

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

  • Java

package com.xyz;

import org.aspectj.lang.ProceedingJoinPoint;
import infra.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 配置对特定连接点执行前面的 advice:

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

  <!-- 这是将被 Infra AOP 基础设施代理的对象 -->
  <bean id="personService" class="com.xyz.service.DefaultPersonService"/>

  <!-- 这是实际的 advice 本身 -->
  <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>

考虑以下驱动程序脚本:

  • Java

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

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

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

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

作为一般经验法则,如果你发现在同一 <aop:aspect> 元素中定义了多个 advice 应用于同一连接点, 请考虑将此类 advice 方法折叠为每个 <aop:aspect> 元素中每个连接点的一个 advice 方法, 或者将 advice 重构为单独的 <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>

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

  • Java

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

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

  • Java

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

Aspect 实例化模型

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

Advisors

“advisors”的概念来自 Infra 中定义的 AOP 支持,在 AspectJ 中没有直接等效项。 Advisor 就像一个包含单个 advice 的小型独立切面。advice 本身由 bean 表示,并且必须实现 Infra 中的 Advice 类型 中描述的 advice 接口之一。 Advisors 可以利用 AspectJ 切入点表达式。

Infra 通过 <aop:advisor> 元素支持 advisor 概念。你最常看到它与事务 advice 结合使用,事务 advice 在 Infra 中也有自己的命名空间支持。 以下示例显示了一个 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 属性内联定义 pointcut 表达式。

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

AOP 模式示例

本节展示了当使用模式支持重写时,一个 AOP 示例 中的并发锁定失败重试示例是什么样子的。

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

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

  • 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 接口,以便我们可以将切面的优先级设置为高于事务 advice(我们希望每次重试都有一个新的事务)。 maxRetriesorder 属性均由 Infra 配置。 主要动作发生在 doConcurrentOperation 环绕 advice 方法中。我们尝试继续。 如果我们因 PessimisticLockingFailureException 而失败,我们会重试,除非我们已经用尽了所有重试尝试。

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

相应的 Infra 配置如下:

<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 注解并使用该注解来注解服务操作的实现,从而细化切面,使其仅重试真正的幂等操作,如下例所示:

  • Java

@Retention(RetentionPolicy.RUNTIME)
// 标记注解
public @interface Idempotent {
}

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

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