6.3. Schema-based AOP support

Spring Framework

6.3. Schema-based AOP support

如果你无法使用Java 5,或者你比较喜欢使用XML格式,Spring2.0也提供了使用新的"aop"命名空间来定义一个切面。 和使用@AspectJ风格完全一样,切入点表达式和通知类型同样得到了支持,因此在这一节中我们将着重介绍新的 语法 和回顾前面我们所讨论的如何写一个切入点表达式和通知参数的绑定(第 6.2 节 “@AspectJ支持”)。

使用本章所介绍的aop命名空间标签(aop namespace tag),你需要引入附录 A, XML Schema-based configuration中提及的spring-aop schema。 参见第 A.2.6 节 “The aop schema”

在Spring的配置文件中,所有的切面和通知器都必须定义在 <aop:config> 元素内部。 一个application context可以包含多个 <aop:config>。 一个 <aop:config> 可以包含pointcut,advisor和aspect元素(注意它们必须按照这样的顺序进行声明)。

警告

<aop:config>风格的配置使得对Spring auto-proxying 机制的使用变得很笨重。如果你已经通过BeanNameAutoProxyCreator或类似的东西使用显式的auto-proxying将会引发问题 (例如通知没有被织入)。推荐的使用模式是只使用<aop:config>风格或只使用 AutoProxyCreator风格

6.3.1. 声明一个切面

有了schema的支持,切面就和常规的Java对象一样被定义成application context中的一个bean。 对象的字段和方法提供了状态和行为信息,XML文件则提供了切入点和通知信息。

切面使用<aop:aspect>来声明,backing bean(支持bean)通过 ref 属性来引用:

<aop:config>
  <aop:aspect id="myAspect" ref="aBean">
	...
  </aop:aspect>
</aop:config>

<bean id="aBean" class="...">
  ...
</bean>

切面的支持bean(上例中的"aBean")可以象其他Spring bean一样被容器管理配置以及依赖注入。

6.3.2. 声明一个切入点

切入点可以在切面里面声明,这种情况下切入点只在切面内部可见。切入点也可以直接在<aop:config>下定义,这样就可以使多个切面和通知器共享该切入点。

一个描述service层中表示所有service执行的切入点可以如下定义:

<aop:config>

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

</aop:config>

注意切入点表达式本身使用了 第 6.2 节 “@AspectJ支持” 中描述的AspectJ 切入点表达式语言。 如果你在Java 5环境下使用基于schema的声明风格,可参考切入点表达式类型中定义的命名式切入点,不过这在JDK1.4及以下版本中是不被支持的(因为依赖于Java 5中的AspectJ反射API)。 所以在JDK 1.5中,上面的切入点的另外一种定义形式如下:

<aop:config>

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

</aop:config>

假定你有 第 6.2.3.3 节 “共享常见的切入点(pointcut)定义”中说描述的 SystemArchitecture 切面。

在切面里面声明一个切入点和声明一个顶级的切入点非常类似:

<aop:config>

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

当需要连接子表达式的时候,'&'在XML中用起来非常不方便,所以关键字'and', 'or' 和 'not'可以分别用来代替'&', '||' 和 '!'。

注意这种方式定义的切入点通过XML id来查找,并且不能定义切入点参数。在基于schema的定义风格中命名切入点支持较之@AspectJ风格受到了很多的限制。

6.3.3. 声明通知

和@AspectJ风格一样,基于schema的风格也支持5种通知类型并且两者具有同样的语义。

6.3.3.1. 通知(Advice)

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中。 在一个数据访问操作执行之前(执行连接点和切入点表达式匹配),切面中的"doAccessCheck"会被调用。

6.3.3.2. 返回后通知(After returning advice)

After returning通知在匹配的方法完全执行后运行。和Before通知一样,可以在<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) {...

6.3.3.3. 抛出异常后通知(After throwing advice)

After throwing通知在匹配方法抛出异常退出时执行。在 <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"
	  thowing="dataAccessEx"
	  method="doRecoveryActions"/>

	...

</aop:aspect>

doRecoveryActions方法必须声明一个名字为 dataAccessEx 的参数。 参数的类型强制匹配,和先前我们在@AfterThrowing中讲到的一样。例如:方法签名可以如下这般声明:

public void doRecoveryActions(DataAccessException dataAccessEx) {...

6.3.3.4. 后通知(After (finally) advice)

After (finally)通知在匹配方法退出后执行。使用 after 元素来声明:

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

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

	...

</aop:aspect>

6.3.3.5. 通知

Around通知是最后一种通知类型。Around通知在匹配方法运行期的“周围”执行。 它有机会在目标方法的前面和后面执行,并决定什么时候运行,怎么运行,甚至是否运行。 Around通知经常在需要在一个方法执行前或后共享状态信息,并且是线程安全的情况下使用(启动和停止一个计时器就是一个例子)。 注意选择能满足你需求的最简单的通知类型(i.e.如果简单的before通知就能做的事情绝对不要使用around通知)。

Around通知使用 aop:around 元素来声明。 通知方法的第一个参数的类型必须是 ProceedingJoinPoint 类型。 在通知的主体中,调用 ProceedingJoinPointproceed() 方法来执行真正的方法。 proceed 方法也可能会被调用并且传入一个 Object[] 对象 - 该数组将作为方法执行时候的参数。 参见 第 6.2.4.5 节 “环绕通知(Around Advice)” 中提到的一些注意点。

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

6.3.3.6. 通知参数

Schema-based声明风格和@AspectJ支持一样,支持通知的全名形式 - 通过通知方法参数名字来匹配切入点参数。 参见 第 6.2.4.6 节 “通知参数(Advice parameters)” 获取详细信息。

如果你希望显式指定通知方法的参数名(而不是依靠先前提及的侦测策略),可以通过 arg-names 属性来实现。示例如下:

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

The arg-names attribute accepts a comma-delimited list of parameter names.

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

请看下面这个基于XSD风格的更复杂一些的实例,它展示了关联多个强类型参数的环绕通知的使用。

首先,服务接口及它的实现将被通知:

package x.y.service;

public interface FooService {

   Foo getFoo(String fooName, int age);
}

// the attendant implementation (defined in another file of course)

public class DefaultFooService implements FooService {

   public Foo getFoo(String name, int age) {
      return new Foo(name, age);
   }
}

下一步(无可否认的)是切面。注意实际上profile(..)方法 接受多个强类型(strongly-typed)参数,第一个参数是方法调用时要执行的连接点,该参数指明了 profile(..)方法被用作一个环绕通知:

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 http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

   <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
   <bean id="fooService" class="x.y.service.DefaultFooService"/>

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

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

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

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

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

</beans>

如果使用下面的驱动脚本,我们将在标准输出上得到如下的输出:

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

public final class Boot {

   public static void main(final String[] args) throws Exception {
      BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
      FooService foo = (FooService) ctx.getBean("fooService");
      foo.getFoo("Pengo", 12);
   }
}
StopWatch 'Profiling for 'Pengo' and '12'': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)

6.3.3.7. 通知顺序

当同一个切入点(执行方法)上有多个通知需要执行时,执行顺序规则在 第 6.2.4.7 节 “通知(Advice)顺序” 已经提及了。 切面的优先级通过切面的支持bean是否实现了Ordered接口来决定。

6.3.4. 引入

Intrduction (在AspectJ中成为inter-type声明)允许一个切面声明一个通知对象实现指定接口,并且提供了一个接口实现类来代表这些对象。

aop:aspect 内部使用 aop:declare-parents 元素定义Introduction。 该元素用于用来声明所匹配的类型有了一个新的父类型(所以有了这个名字)。 例如,给定接口 UsageTracked, 以及这个接口的一个实现类 DefaultUsageTracked, 下面声明的切面所有实现service接口的类同时实现 UsageTracked 接口。(比如为了通过JMX暴露statistics。)

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

  <aop:declare-parents
	  types-matching="com.xzy.myapp.service.*+",
	  implement-interface="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类型模式:- 任何匹配类型的bean会实现 UsageTracked 接口。 注意在Before通知的例子中,srevice bean可以用作 UsageTracked 接口的实现。 如果编程形式访问一个bean,你可以这样来写:

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

6.3.5. 切面实例化模型

Schema-defined切面仅支持一种实例化模型就是singlton模型。其他的实例化模型或许在未来版本中将得到支持。

6.3.6. Advisors

"advisors"这个概念来自Spring1.2对AOP的支持,在AspectJ中是没有等价的概念。 advisor就像一个小的自包含的切面,这个切面只有一个通知。 切面自身通过一个bean表示,并且必须实现一个通知接口, 在 第 7.3.2 节 “Spring里的通知类型” 中我们会讨论相应的接口。Advisors可以很好的利用AspectJ切入点表达式。

Spring 2.0 通过 <aop:advisor> 元素来支持advisor 概念。 你将会发现它大多数情况下会和transactional advice一起使用,transactional advice在Spring 2.0中有自己的命名空间。格式如下:

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

为了定义一个advisord的优先级以便让通知可以有序,使用 order 属性来定义 advisor的值 Ordered

6.3.7. 例子

让我们来看看在 第 6.2.7 节 “例子” 提过并发锁失败重试的例子,如果使用schema对这个例子进行重写是什么效果。

因为并发锁的关系,有时候business services可能会失败(例如,死锁失败)。 如果重新尝试一下,很有可能就会成功。对于business services来说,重试几次是很正常的(Idempotent操作不需要用户参与,否则会得出矛盾的结论) 我们可能需要透明的重试操作以避免让客户看见 PessimisticLockingFailureException 例外被抛出。 很明显,在一个横切多层的情况下,这是非常有必要的,因此通过切面来实现是很理想的。

因为我们想要重试操作,我们会需要使用到环绕通知,这样我们就可以多次调用proceed()方法。 下面是简单的切面实现(只是一个schema支持的普通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 这个环绕通知中发生。 请注意这个时候我们所有的 businessService() 方法都会使用这个重试策略。 我们首先会尝试处理,然后如果我们得到一个 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>

请注意我们现在假设所有的bussiness services都是idempotent。如果不是这样,我们可以改写切面,加上 Idempotent 注解,让它只调用idempotent:

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

并且对service操作的实现进行注解。这样如果你只希望改变切面使得idempotent的操作会尝试多次,你只需要改写切入点表达式,这样只有 @Idempotent 操作会匹配:

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