6.3. 基于Schema的AOP支持

Spring Framework

6.3. 基于Schema的AOP支持

  

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

  

使用本章所介绍的aop命名空间标签,你需要引入附录 A, XML Schema-based configuration中提及的spring-aop schema。 参见第 A.2.7 节 “The aop schema”了解如何在aop命名空间中引入标签。

  

在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的声明风格,可参考切入点表达式类型(@Aspects)中定义的命名切入点,   不过这个特性在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 节 “共享通用切入点定义”中 描述的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定义风格声明的切入点可以收集(collect) 连接点上下文。例如,下面的切入点收集'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中用起来非常不方便,所以关键字'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的定义风格中 命名切入点支持较之@AspectJ风格受到了很多的限制。

6.3.3. 声明通知

 

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

6.3.3.1. 前置通知

    

前置通知在匹配方法执行前运行。在<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. 后置通知

后置通知在匹配的方法完全执行后运行。和前置通知一样,可以在<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. 异常通知

异常通知在匹配方法抛出异常退出时执行。在<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) {...

6.3.3.4. 最终通知

最终通知无论如何都会在匹配方法退出后执行。使用after元素来声明它:

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

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

6.3.3.5. 环绕通知

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

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

<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属性来实现,它的处理和 在第 6.2.4.6.3 节 “确定参数名”中所描述的对通知注解中"argNames"属性的处理方式一样。 示例如下:

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

   Foo getFoo(String fooName, int age);
}

public class DefaultFooService implements FooService {

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

接下来看切面。注意profile(..)方法接受多个强类型参数, 首先连接点在方法调用时执行,这个参数指明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.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.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 节 “通知顺序”所述。切面的优先级通过给切面的支持bean增加 Order注解或者使切面的支持bean实现 Ordered接口来决定。   

6.3.4. 引入

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

引入的定义使用aop:aspect中的aop:declare-parents元素。 该元素用于声明所匹配的类型有一个新的父类型(所以有了这个名字)。 例如,给定接口UsageTracked, 以及这个接口的一个实现类 DefaultUsageTracked, 下面的切面声明所有实现service接口的类同时实现 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类型模式:任何匹配类型的bean都会实现  UsageTracked 接口。注意在上面前置通知的例子中, serevice bean可以直接用作UsageTracked接口的实现。 如果以编程形式访问一个bean,你可以这样来写:

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

6.3.5. 切面实例化模型

采用Schema风格来定义切面仅支持一种实例化模型就是singlton模型。其他的实例化模型或许以后的版本会支持。

6.3.6. Advisor

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

Spring 2.0通过<aop:advisor>元素来支持advisor概念。 你将会发现大多数情况下它会和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 属性来定义一个内联的切入点表达式。

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

6.3.7. 例子

让我们看看第 6.2.7 节 “例子”中并发锁失败重试的例子, 当使用schema重写它时是什么样子。

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

因为想要重试操作,我们需要使用环绕通知,这样就可以多次调用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 这个环绕通知方法中发生。 我们首先会尝试处理,如果得到一个OptimisticLockingFailureException 异常,我们仅仅重试直到耗尽所有预设的重试次数。

这个类跟我们在@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)"/>