<aop:config>

6.3. Schema-based AOP support

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

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

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

[Warning] Warning

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

注意切入点表达式本身使用了 Section 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>

假定你有 Section 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[] 对象 - 该数组将作为方法执行时候的参数。 参见 Section 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支持一样,支持通知的全名形式 - 通过通知方法参数名字来匹配切入点参数。 参见 Section 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.xsdhttp://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. 通知顺序

当同一个切入点(执行方法)上有多个通知需要执行时,执行顺序规则在 Section 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:beforepointcut="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表示,并且必须实现一个通知接口, 在 Section 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. 例子

让我们来看看在 Section 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)"/>

6.4. AOP声明风格的选择

当你确定切面是实现一个给定需求的最佳方法时,你如何选择是使用Spring AOP还是AspectJ,以及选择 Aspect语言(代码)风格、@AspectJ声明风格或XML风格?这个决定会受到多个因素的影响,包括应用的需求、 开发工具和小组对AOP的精通程度。

6.4.1. Spring AOP还是完全用AspectJ?

做能起作用的最简单的事。Spring AOP比完全使用AspectJ更加简单,因为它不需要引入AspectJ的编译器/织入器到你开发和构建过程中。 如果你仅仅需要在Spring bean上通知执行操作,那么Spring AOP是合适的选择。如果你需要通知domain对象或其它没有在Spring容器中 管理的任意对象,那么你需要使用AspectJ。如果你想通知除了简单的方法执行之外的连接点(如:调用连接点、字段get或set的连接点等等), 也需要使用AspectJ。

当使用AspectJ时,你可以选择使用AspectJ语言(也称为“代码风格”)或@AspectJ注解风格。 如果切面在你的设计中扮演一个很大的角色,并且你能在Eclipse中使用AspectJ Development Tools (AJDT), 那么首选AspectJ语言 :- 因为该语言专门被设计用来编写切面,所以会更清晰、更简单。如果你没有使用 Eclipse,或者在你的应用中只有很少的切面并没有作为一个主要的角色,你或许应该考虑使用@AspectJ风格 并在你的IDE中附加一个普通的Java编辑器,并且在你的构建脚本中增加切面织入(链接)的段落。

6.4.2. Spring AOP中使用@AspectJ还是XML?

如果你选择使用Spring AOP,那么你可以选择@AspectJ或者XML风格。总的来说,如果你使用Java 5, 我们建议使用@AspectJ风格。显然如果你不是运行在Java 5上,XML风格是最佳选择。XML和@AspectJ 之间权衡的细节将在下面进行讨论。

XML风格对现有的Spring用户来说更加习惯。它可以使用在任何Java级别中(参考连接点表达式内部的命名连接点,虽然它也需要Java 5) 并且通过纯粹的POJO来支持。当使用AOP作为工具来配置企业服务时(一个好的例子是当你认为连接点表达式是你的配置中的一部分时, 你可能想单独更改它)XML会是一个很好的选择。对于XML风格,从你的配置中可以清晰的表明在系统中存在那些切面。

XML风格有两个缺点。第一是它不能完全将需求实现的地方封装到一个位置。DRY原则中说系统中的每一项知识都必须具有单一、无歧义、权威的表示。 当使用XML风格时,如何实现一个需求的知识被分割到支撑类的声明中以及XML配置文件中。当使用@AspectJ风格时就只有一个单独的模块 -切面- 信息被封装了起来。 第二是XML风格同@AspectJ风格所能表达的内容相比有更多的限制:仅仅支持"singleton"切面实例模型,并且不能在XML中组合命名连接点的声明。 例如,在@AspectJ风格中我们可以编写如下的内容:

  @Pointcut(execution(* get*()))  public void propertyAccess() {}  @Pointcut(execution(org.xyz.Account+ *(..))  public void operationReturningAnAccount() {}  @Pointcut(propertyAccess() && operationReturningAnAccount())  public void accountPropertyAccess() {}

在XML风格中能声明开头的两个连接点:

  <aop:pointcut id="propertyAccess"       expression="execution(* get*())"/>  <aop:pointcut id="operationReturningAnAccount"       expression="execution(org.xyz.Account+ *(..))"/>

但是不能通过组合这些来定义accountPropertyAccess连接点

@AspectJ风格支持其它的实例模型以及更丰富的连接点组合。它具有将将切面保持为一个模块单元的优点。 还有一个优点就是@AspectJ切面能被Spring AOP和AspectJ两者都理解 - 所以如果稍后你认为你需要AspectJ 的能力去实现附加的需求,那么你非常容易转移到基于AspectJ的途径。总而言之,我们更喜欢@AspectJ风格只要你有切面 去做超出简单的“配置”企业服务之外的事情。

6.5. 混合切面类型

我们完全可以混合使用以下几种风格的切面定义:使用自动代理的@AspectJ 风格的切面,schema-defined <aop:aspect> 的切面,和用 <aop:advisor> 声明的advisor,甚至是使用Spring 1.2风格的代理和拦截器。 由于以上几种风格的切面定义的都使用了相同的底层机制,因此可以很好的共存。

6.6. 代理机制

Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象创建代理。(建议尽量使用JDK的动态代理)

如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。

如果你希望强制使用CGLIB代理,(例如:希望代理目标对象的所有方法,而不只是实现自接口的方法)那也可以。但是需要考虑以下问题:

  • 无法通知(advise)Final 方法,因为他们不能被覆写。

  • 你需要将CGLIB 2二进制发行包放在classpath下面,与之相较JDK本身就提供了动态代理

强制使用CGLIB代理需要将 <aop:config>proxy-target-class 属性设为true:

<aop:config proxy-target-class="true">...</aop:config>

当需要使用CGLIB代理和@AspectJ自动代理支持,请按照如下的方式设置 <aop:aspectj-autoproxy>proxy-target-class 属性:

<aop:aspectj-autoproxy proxy-target-class="true"/>

6.7. 编程方式创建@AspectJ代理

除了在配置文件中使用 <aop:config> 或者 <aop:aspectj-autoproxy> 来声明切面。 同样可以通过编程方式来创建代理通知(advise)目标对象。关于Spring AOP API的详细介绍,请参看下一章。这里我们重点介绍自动创建代理。

org.springframework.aop.aspectj.annotation.AspectJProxyFactory 可以为@AspectJ切面的目标对象创建一个代理。该类的基本用法非常简单,示例如下。请参看Javadoc获取更详细的信息。

// create a factory that can generate a proxy for the given target objectAspectJProxyFactory factory = new AspectJProxyFactory(targetObject); // add an aspect, the class must be an @AspectJ aspect// you can call this as many times as you need with different aspectsfactory.addAspect(SecurityManager.class);// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspectfactory.addAspect(usageTracker);// now get the proxy object...MyInterfaceType proxy = factory.getProxy();

6.8. 在Spring应用中使用AspectJ

到目前为止本章讨论的一直是纯Spring AOP。 在这一节里面我们将介绍如何使用AspectJ compiler/weaver来代替Spring AOP或者作为它的补充,因为有些时候Spring AOP单独提供的功能也许并不能满足你的需要。

Spring提供了一个小巧的AspectJ aspect library (你可以在程序发行版本中单独使用 spring-aspects.jar 文件,并将其加入到classpath下以使用其中的切面)。 Section 6.8.1, “在Spring中使用AspectJ来为domain object进行依赖注入”Section 6.8.2, “Spring中其他的AspectJ切面” 讨论了该库和如何使用该库。 Section 6.8.3, “使用Spring IoC来配置AspectJ的切面” 讨论了如何对通过AspectJ compiler织入的AspectJ切面进行依赖注入。 最后Section 6.8.4, “在Spring应用中使用AspectJ Load-time weaving(LTW)”介绍了使用AspectJ的Spring应用程序如何装载期织入(load-time weaving)。

6.8.1. 在Spring中使用AspectJ来为domain object进行依赖注入

Spring容器对application context中定义的bean进行实例化和配置。 同样也可以通过bean factory来为一个已经存在且已经定义为spring bean的对象应用所包含的配置信息。 spring-aspects.jar中包含了一个annotation-driven的切面,提供了能为任何对象进行依赖注入的能力。 这样的支持旨在为 脱离容器管理 创建的对象进行依赖注入。 Domain object经常处于这样的情形:它们可能是通过 new 操作符创建的对象, 也可能是ORM工具查询数据库的返回结果对象。

org.springframework.orm.hibernate.support 中的类 DependencyInjectionInterceptorFactoryBean 可以让Spring为Hibernate创建并且配置prototype类型的domain object(使用自动装配或者确切命名的bean原型定义)。 当然,拦截器不支持配置你编程方式创建的对象而非检索数据库返回的对象。 其他framework也会提供类似的技术。仍是那句话,Be Pragramatic选择能满足你需求的方法中最简单的那个。 请注意前面提及的类 没有 随Spring发行包一起发布。 如果你希望使用该类,需要从Spring CVS Respository上下载并且自行编译。 你可以在Spring CVS respository下的 'sandbox' 目录下找到该文件。

@Configurable 注解标记了一个类可以通过Spring-driven方式来配置。 在最简单的情况下,我们只把它当作标记注解:

package com.xyz.myapp.domain;import org.springframework.beans.factory.annotation.Configurable;@Configurablepublic class Account {   ...}

当只是简单地作为一个标记接口来使用的时候,Spring将采用和该已注解的类型(比如Account类)全名 (com.xyz.myapp.domain.Account)一致的bean原型定义来配置一个新实例。 由于一个bean默认的名字就是它的全名,所以一个比较方便的办法就是省略定义中的id属性:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">  <property name="fundsTransferService" ref="fundsTransferService"/>  ...</bean>

如果你希望明确的指定bean原型定义的名字,你可以在注解中直接定义:

package com.xyz.myapp.domain;import org.springframework.beans.factory.annotation.Configurable;@Configurable("account")public class Account {   ...}

Spring会查找名字为"account"的bean定义,并使用它作为原型定义来配置一个新的Account对象。

你也可以使用自动装配来避免手工指定原型定义的名字。 只要设置 @Configurable 注解中的autowire属性就可以让Spring来自动装配了: @Configurable(autowire=Autowire.BY_TYPE) 或者 @Configurable(autowire=Autowire.BY_NAME,这样就可以按类型或者按名字自动装配了。

最后,你可以设置 dependencyCheck 属性,通过设置,Spring对新创建和配置的对象的对象引用进行校验 (例如:@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true) )。 如果这个属性被设为true,Spring会在配置结束后校验除了primitives和collections类型的所有的属性是否都被赋值了。

仅仅使用注解并没有做任何事情。但当注解存在时,spring-aspects.jar中的 AnnotationBeanConfigurerAspect 就起作用了。 实质上切面做了这些:当初始化一个有 @Configurable 注解的新对象时,Spring按照注解中的属性来配置这个新创建的对象。 要实现上述的操作,已注解的类型必须由AspectJ weaver来织入 - 你可以使用一个 build-time ant/maven任务来完成 (参见AspectJ Development Environment Guide) 或者使用load-time weaving(参见 Section 6.8.4, “在Spring应用中使用AspectJ Load-time weaving(LTW)”)。

AnnotationBeanConfigurerAspect 本身也需要Spring来配置(获得bean factory的引用,使用bean factory配置新的对象)。 为此Spring AOP命名空间定义了一个非常方便的标签。如下所示,可以很简单的在application context配置文件包含这个标签中。

<aop:spring-configured/>

如果你使用DTD代替Schema,对应的定义如下:

<bean  class="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"  factory-method="aspectOf"/>

在切面配置完成 之前 创建的@Configurable对象实例会导致在log中留下一个warning,并且任何对于该对象的配置都不会生效。 举一个例子,一个Spring管理配置的bean在被Spring初始化的时候创建了一个domain object。 对于这样的情况,你需要定义bean属性中的"depends-on"属性来手动指定该bean依赖于configuration切面。

<bean id="myService"  class="com.xzy.myapp.service.MyService"  depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">  ...</bean>

6.8.1.1.  @Configurable object的单元测试

提供 @Configurable 支持的一个目的就是使得domain object的单元测试可以独立进行,不需要通过硬编码查找各种倚赖关系。 如果 @Configurable 类型没有通过AspectJ织入, 则在单元测试过程中注解不会起到任何作用,测试中你可以简单的为对象的mock或者stub属性赋值,并且和正常情况一样的去使用该对象。 如果 @Configurable 类型通过AspectJ织入, 我们依然可以脱离容器进行单元测试,不过每次创建一个新的 @Configurable 对象时都会看到一个warning标示该对象不受Spring管理配置。

6.8.1.2. 多application context情况下的处理

AnnotationBeanConfigurerAspect 通过一个AspectJ singleton切面来实现对 @Configurable 的支持。 一个singleton切面的作用域和一个静态变量的作用域是一样的,例如,对于每一个classloader有一个切面来定义类型。 这就意味着如果你在一个classloader层次结构中定义了多个application context的时候就需要考虑在哪里定义 <aop:spring-configured/> bean和在哪个classpath下放置Srping-aspects.jar。

考虑一下典型的Spring web项目,一般都是由一个父application context定义大部分business service和所需要的其他资源,然后每一个servlet拥有一个子application context定义。 所有这些context共存于同一个classloader hierarchy下,因此对于全体context,AnnotationBeanConfigurerAspect 仅可以维护一个引用。 在这样的情况下,我们推荐在父application context中定义 <aop:spring-configured/> bean: 这里所定义的service可能是你希望注入domain object的。 这样做的结果是你不能为子application context中使用@Configurable的domain object配置bean引用(可能你也根本就不希望那么做!)。

当在一个容器中部署多个web-app的时候,请确保每一个web-application使用自己的classloader来加载spring-aspects.jar中的类(例如将spring-aspects.jar放在WEB-INF/lib目录下)。 如果spring-aspects.jar被放在了容器的classpath下(因此也被父classloader加载),则所有的web application将共享一个aspect实例,这可能并不是你所想要的。

6.8.2. Spring中其他的AspectJ切面

除了 @Configurable 支持,spring-aspects.jar包含了一个AspectJ切面可以用来为那些使用了 @Transactional annotation 的类型和方法驱动Spring事务管理(参见 Chapter 9, 事务管理)。 提供这个的主要目的是有些用户希望脱离Spring容器使用Spring的事务管理。

解析@Transactional annotations的切面是AnnotationTransactionAspect。 当使用这个切面时,你必须注解这个实现类(和/或这个类中的方法),而不是这个类实现的接口(如果有)。 AspectJ允许在接口上注解的Java规则 不被继承

类之上的一个@Transactional注解为该类中任何public操作的执行指定了默认的事务语义。

类内部方法上的一个@Transactional注解会覆盖类注解(如果存在)所给定的默认的事务语义。 具有public、protected和default修饰符的方法都可以被注解。直接注解protected和default方法是让这个操作的执行 获得事务划分的唯一途径。

对于AspectJ程序员,希望使用Spring管理配置和事务管理支持,不过他们不想(或者不能)使用注解,spring-aspects.jar也包含了一些抽象切面供你继承来提供你自己的切入点定义。 参见 AbstractBeanConfigurerAspectAbstractTransactionAspect 的Javadoc获取更多信息。 作为一个例子,下面的代码片断展示了如何编写一个切面,然后通过bean原型定义中和类全名匹配的来配置domian object中所有的实例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {  public DomainObjectConfiguration() {    setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());  }  // the creation of a new bean (any object in the domain model)  protected pointcut beanCreation(Object beanInstance) :    initialization(new(..)) &&    SystemArchitecture.inDomainModel() &&     this(beanInstance);      }

6.8.3. 使用Spring IoC来配置AspectJ的切面

当在Spring application中使用AspectJ的时候,很自然的会想到用Spring来管理这些切面。 AspectJ runtime自身负责切面的创建,这意味着通过Spring来管理AspectJ 创建切面依赖于切面所使用的AspectJ instantiation model(per-clause)。

大多数AspectJ切面都是 singleton 切面。 管理这些切面非常容易,和通常一样创建一个bean定义引用该切面类型就可以了,并且在bean定义中包含 'factory-method="aspectOf"' 这个属性。 这确保Spring从AspectJ获取切面实例而不是尝试自己去创建该实例。示例如下:

<bean id="profiler" class="com.xyz.profiler.Profiler"  factory-method="aspectOf">  <property name="profilingStrategy" ref="jamonProfilingStrategy"/></bean>

对于non-singleton的切面,最简单的配置管理方法是定义一个bean原型定义并且使用@Configurable支持,这样就可以在切面被AspectJ runtime创建后管理它们。

如果你希望一些@AspectJ切面使用AspectJ来织入(例如使用load-time织入domain object) 和另一些@AspectJ切面使用Spring AOP,而这些切面都是由Spring来管理的,那你就需要告诉Spring AOP @AspectJ自动代理支持那些切面需要被自动代理。 你可以通过在 <aop:aspectj-autoproxy> 声明中使用一个或多个 <include/>。 每一个指定了一种命名格式,只有bean命名至少符合其中一种情况下才会使用Spring AOP自动代理配置:

<aop:aspectj-autoproxy>  <include name="thisBean"/>  <include name="thatBean"/></aop:aspectj-autoproxy>

6.8.4. 在Spring应用中使用AspectJ Load-time weaving(LTW)

Load-time weaving(LTW)指的是在虚拟机载入字节码文件时动态织入AspectJ切面。 关于LTW的详细信息,请查看 LTW section of the AspectJ Development Environment Guide。 在这里我们重点来看一下Java 5环境下Spring应用如何配置LTW。

LTW需要定义一个 aop.xml,并将其置于META-INF目录。 AspectJ会自动查找所有可见的classpath下的META-INF/aop.xml文件,并且通过定义内容的合集来配置自身。

一个基本的META-INF/aop.xml文件应该如下所示:

<!DOCTYPE aspectj PUBLIC  "-//AspectJ//DTD//EN""http://www.eclipse.org/aspectj/dtd/aspectj.dtd"><aspectj>   <weaver> <include within="com.xyz.myapp..*"/>   </weaver></aspectj>

'<include/>'的内容告诉AspectJ那些类型需要被纳入织入过程。使用包名前缀并加上"..*"(表示该子包中的所有类型)是一个不错的默认设定。 使用include元素是非常重要的,不然AspectJ会查找每一个应用里面用到的类型(包括Spring的库和其它许多相关库)。通常你并不希望织入这些类型并且不愿意承担AspectJ尝试去匹配的开销。

希望在日志中记录LTW的活动,请添加如下选项:

<!DOCTYPE aspectj PUBLIC      "-//AspectJ//DTD//EN"    "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">    <aspectj>       <weaver  options="-showWeaveInfo              -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler"> <include within="com.xyz.myapp..*"/>   </weaver></aspectj>

最后,如果希望精确的控制使用哪些切面,可以使用 aspects。 默认情况下所有定义的切面都将被织入(spring-aspects.jar包含了META-INF/aop.xml,定义了配置管理和事务管理切面)。 如果你在使用spring-aspects.jar,但是只希望使用配制管理切面而不需要事务管理的话,你可以像下面那样定义:

<!DOCTYPE aspectj PUBLIC  "-//AspectJ//DTD//EN""http://www.eclipse.org/aspectj/dtd/aspectj.dtd"><aspectj>   <aspects>  <include within="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"/>   </aspects>   <weaver    options="-showWeaveInfo -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">    <include within="com.xyz.myapp..*"/>   </weaver></aspectj>

在Java 5平台下,LTW可以通过虚拟机的参数来启用。

-javaagent:<path-to-ajlibs>/aspectjweaver.jar

6.9. 其它资源

更多关于AspectJ的信息可以查看 AspectJ home page

Eclipse AspectJ by Adrian Colyer et. al. (Addison-Wesley, 2005)全面介绍并提供了AspectJ语言参考。

AspectJ in Action by Ramnivas Laddad (Manning, 2003)是一本非常出色介绍AOP的书籍;全书着重介绍了AspectJ,但也对一些通用的AOP场景进行了比较深入的研究。

posted @ 2010-11-14 09:52  七郎  Views(65134)  Comments(0Edit  收藏  举报