6.8. 在Spring应用中使用AspectJ

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

Spring提供了一个小巧的AspectJ aspect library (你可以在程序发行版本中单独使用 spring-aspects.jar 文件,并将其加入到classpath下以使用其中的切面)。 第 6.8.1 节 “在Spring中使用AspectJ来为domain object进行依赖注入”第 6.8.2 节 “Spring中其他的AspectJ切面” 讨论了该库和如何使用该库。 第 6.8.3 节 “使用Spring IoC来配置AspectJ的切面” 讨论了如何对通过AspectJ compiler织入的AspectJ切面进行依赖注入。 最后第 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;

@Configurable
public 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(参见 第 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事务管理(参见 第 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