Chapter 25. 注解和源代码级的元数据支持

25.1. 简介

源代码级的元数据通常是对类或方法这样的程序元素的属性注解的补充。

举例来说,我们可以象下面这样给一个类添加元数据:

/**
 * Normal comments here
 * @@org.springframework.transaction.interceptor.DefaultTransactionAttribute()
 */
public class PetStoreImpl implements PetStoreFacade, OrderService {

我们也可以像下面这样为一个方法添加元数据:

/**
 * Normal comments here
 * @@org.springframework.transaction.interceptor.RuleBasedTransactionAttribute()
 * @@org.springframework.transaction.interceptor.RollbackRuleAttribute(Exception.class)
 * @@org.springframework.transaction.interceptor.NoRollbackRuleAttribute("ServletException")
 */
public void echoException(Exception ex) throws Exception {
    ....
}

这两个例子都使用了Jakarta Commons Attributes的语法。

源代码级的元数据随着XDoclet(在Java世界中)和Microsoft的.NET平台的发布被引入主流,它使用了源代码级的属性来控制事务、缓冲(pooling)和一些其他的行为。

J2EE社区已经认识到了这种方法的价值。举例来说,跟EJB中专用的传统XML部署描述文件比起来它要简单很多。与人们乐意做的把一些东西从程序源代码中提取出来的做法相反, 一些重要的企业级设置 - 特别是事务特性 - 应该属于程序代码。并不像EJB规范中设想的那样,调整一个方法的事务特性基本没有什么意义(尽管像事务超时这样的参数可能改变)。

虽然元数据属性主要用于框架的基础架构来描述应用程序的类需要的服务,但是它也可以在运行时被查询。 这是它与XDoclet这样的解决方案的关键区别,XDoclet主要把元数据作为生成代码的一种方式,比如生成EJB类。

下面有几种解决方案,包括:

  • 标准Java注解:标准Java元数据实现作为JSR-175被开发,可在 Java 5中找到。Spring已经在事务划分、JMX和切面(准确地说它们是AspectJ的注解)中支持Java 5注解。 但是,我们在Java 1.4甚至是1.3中也需要一个解决方案。Spring元数据支持就提供了这样一个方案。

  • XDoclet:成熟的解决方案,主要用于代码生成

  • 多种不同的针对Java 1.3和1.4的开源元数据属性实现,在它们当中Commons Attributes看起来是最完整的实现。所有的这些实现都需要一个特定的前编译或后编译的步骤。

25.2. Spring的元数据支持

为了与Spring提供的其他重要概念的抽象相一致,Spring提供了一个对元数据实现的门面(facade), 以org.springframework.metadata.Attributes接口的形式来实现。 这个门面因以下几个原因而显得很有价值:

  • 尽管Java 5提供了语言级的元数据支持,但提供这样一个抽象还是能带来价值:

    • Java 5的元数据是静态的。它是在编译时与一个类关联,而且在部署环境下是不可改变的 (注解的状态可以通过反射在运行时改变,但这并不是一个很好的实践)。 这里会需要多层次的元数据,以支持在部署时重载某些属性的值 - 举例来说,在一个XML文件 中定义用于覆盖的属性。

    • Java 5的元数据是通过Java反射API返回的。这使得在测试时无法模拟元数据。 Spring提供了一个简单的接口来允许这种模拟。

    • 在未来至少两年内仍有在1.3和1.4应用程序中支持元数据的需要。Spring着眼于提供现在可以工作的解决方案; 强迫使用Java 5不是在这个重要领域中的明智之举。

  • 当前的元数据API,例如Commons Attributes(在Spring 1.0-1.2中使用)很难测试。Spring提供了一个简单的易于模拟的元数据接口。

Spring的Attributes接口是这个样子的:

public interface Attributes {

    Collection getAttributes(Class targetClass);

    Collection getAttributes(Class targetClass, Class filter);

    Collection getAttributes(Method targetMethod);

    Collection getAttributes(Method targetMethod, Class filter);

    Collection getAttributes(Field targetField);

    Collection getAttributes(Field targetField, Class filter);
}

这是个再普通不过的命名者接口。JSR-175能提供更多的功能,比如定义在方法参数上的属性。 在1.0版本时,Spring着眼于提供所需元数据的一个子集,使得能在Java 1.3+上提供像EJB或.NET 一样的有效的声明式企业级服务。Spring 1.2还支持Java 5的注解作为Commons Attributes的替代品。

要注意到该接口像.NET一样提供了Object属性。这使得它区别 于一些仅提供String属性的元数据属性系统,比如Nanning Aspects。 支持Object属性有一个显著的优点。它使属性能参与到类层次中, 还可以使属性能够灵活的根据它们的配置参数起作用。

对于大多数属性提供者来说,属性类的配置是通过构造方法参数或JavaBean的属性完成的。Commons Attributes同时支持这两种方式。

同所有的Spring抽象API一样,Attributes是一个接口。 这使得在单元测试中模拟属性的实现变得容易起来。

25.3. 注解

Spring有很多自定义的(Java5+)注解。

25.3.1. @Required

org.springframework.beans.factory.annotation包 中的@Required注解能用来标记 属性,标示为'需要设置'(例如,一个类中的被注解的(setter) 方法必须配置一个用来依赖注入的值),否则一个Exception 必须(并且将会)在运行时被容器抛出。

演示这个注解用法的最好办法是给出像下面这样的范例:

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on the MovieFinder
    private MovieFinder movieFinder;

    // a setter method so that the Spring container can 'inject' a MovieFinder
    @Required
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
    
    // business logic that actually 'uses' the injected MovieFinder is omitted...
}

还好上面的类定义看起来比较简单。基本上(在Spring IoC容器的上下文中), 所有针对SimpleMovieLister类的BeanDefinitions 一定要提供一个值(用Spring API的话来讲就是那个属性一定要设置一个PropertyValue)。

让我们看一个能通过验证的XML配置范例。

<bean id="movieLister" class="x.y.SimpleMovieLister">
    <!-- whoops, no MovieFinder is set (and this property is @Required) -->
</bean>

运行时Spring容器会生成下面的消息(追踪堆栈的剩下部分被删除了)。

Exception in thread "main" java.lang.IllegalArgumentException:
    Property 'movieFinder' is required for bean 'movieLister'.

现在先停一下……还有最后一点(小的)Spring配置需要用来'开启'这个行为。 简单注解一下你类的'setter'属性不足以实现这个行为。 你还需要能发现@Required注解并能适当地处理它的东西。

进入RequiredAnnotationBeanPostProcessor类。 这是一个由Spring提供的特殊的BeanPostProcessor实现, @Required-aware能提供'要求属性未被设置时提示'的逻辑。 它容易配置;只要简单地把下列bean定义放入你的Spring XML配置中。

<bean class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor"/>

最后,你能配置一个RequiredAnnotationBeanPostProcessor类的实例 来查找另一个Annotation类型。 如果你已经有自己的@Required风格的注解这会是件很棒的事。 简单地把它插入一个RequiredAnnotationBeanPostProcessor的定义中就可以了。

看个例子,让我们假设你(和你的组织/团队)已经定义了一个叫做@Mandatory 的属性。你能建一个如下的RequiredAnnotationBeanPostProcessor实例 @Mandatory-aware:

<bean class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor">
    <property name="requiredAnnotationType" value="your.company.package.Mandatory"/>
</bean>

25.3.2. Spring中的其它@Annotations

注解也被用于Spring中的其他地方。相比在这里说明,那些注解在他们各自的章节或与他们相关的章节中予以说明。

25.4. 集成Jakarta Commons Attributes

虽然为其他元数据提供者来说,提供org.springframework.metadata.Attributes 接口的实现很简单,但是目前Spring只是直接支持Jakarta Commons Attributes。

Commons Attributes 2.1(http://jakarta.apache.org/commons/attributes/)是一个功能很强的元数据属性解决方案。 它支持通过构造方法参数和JavaBean属性来配置属性,这提供了更好的属性定义的文档。(对JavaBean属性的支持是在Spring小组的要求下添加的。)

我们已经看到了两个Commons Attributes的属性定义的例子。大体上,我们需要解释一下:

  • 属性类的名称。这可能是一个完整的名字(fully qualified name, FQN),就像上面的那样。如果相关的属性类已经被导入,就不需要FQN了。你也可以在属性编译器的设置中指定属性的包名。

  • 任何必须的参数化。可以通过构造方法参数或者JavaBean属性完成。

Bean的属性如下:

/**
 * @@MyAttribute(myBooleanJavaBeanProperty=true)
 */

可以把构造方法参数和JavaBean属性结合在一起(就像在Spring IoC中一样)。

由于Common Attributes没有像Java 1.5中的属性那样和Java语言本身结合起来,因此需要运行一个特定的属性编译步骤作为整个构建过程的一部分。

为了在整个构建过程中运行Commmons Attributes,你需要做以下的事情:

1. 复制一些必要的jar包到$ANT_HOME/lib。有四个必须的jar包,它们包含在Spring的发行包里:

  • Commons Attributes编译器的jar包和API的jar包。

  • 来自于XDoclet的xjavadoc.jar

  • 来自于Jakarta Commons的commons-collections.jar

2. 把Commons Attributes的ant任务导入到你的项目构建脚本中去,像下面这样:

<taskdef resource="org/apache/commons/attributes/anttasks.properties"/>

3. 接下来,定义一个属性编译任务,它将使用Commons Attributes的attribute-compiler任务来“编译”源代码中的属性。这个过程将生成额外的代码至destdir属性指定的位置。在这里我们使用了一个临时目录来保存生成的文件:

<target name="compileAttributes">

  <attribute-compiler destdir="${commons.attributes.tempdir}">
    <fileset dir="${src.dir}" includes="**/*.java"/>
  </attribute-compiler>

</target>

运行javac命令编译源代码的编译目标任务应该依赖于属性编译任务,还需要编译属性时生成至目标临时目录的源代码。 如果在属性定义中有语法错误,通常都会被属性编译器捕获到。但是,如果属性定义在语法上似是而非,却使用了一些非法的类型或类名,生成属性类的编译可能会失败。 在这种情况下,你可以看看所生成的类来确定错误的原因。

Commons Attributes也提供对Maven的支持。请参考Commons Attributes的文档得到进一步的信息。

虽然属性编译的过程可能看起来复杂,实际上它是一次性的花销。一旦被创建后,属性的编译是递增式的,所以通常它不会明显减慢整个构建过程。 一旦编译过程建立起来后,你可能会发现本章中描述的属性的使用将节省在其他方面的时间。

如果需要属性索引支持(目前只在Spring的以属性为目标的web控制器中需要,下面会讨论到),你需要一个额外的步骤,执行在包含编译后的类的jar文件上。 在这步可选的步骤中,Commons Attributes将生成一个所有在你源代码中定义的属性的索引,以便在运行时进行有效的查找。 该步骤如下:

<attribute-indexer jarFile="myCompiledSources.jar">
    
  <classpath refid="master-classpath"/>

</attribute-indexer>
可以到Spring jPetStore例程下的/attributes目录下察看它的构建过程。你可以使用它里面的构建脚本,并修改该脚本以适应你自己的项目。

如果你的单元测试依赖于属性,尽量使它依赖于Spring Attributes抽象,而不是Commons Attributes。 这不仅仅为了更好的移植性 - 举例来说,你的测试用例将来仍可以工作如果你转换至Java 1.5的属性 - 它也简化了测试。另外,Commons Attributes是静态的API,而Spring提供的是一个容易模拟的元数据接口。

25.5. 元数据和Spring AOP自动代理

元数据属性最有用的就是与Spring AOP联合使用。这提供了一个类似.NET的编程模型:声明式服务会自动提供给声明了元数据的属性。 这些元数据属性可以被框架支持,比如声明式事务管理,同时也能定制。

25.5.1. 基本原理

基于Spring AOP的自动代理功能,配置可能如下所示:

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
  <property name="transactionInterceptor" ref="txInterceptor" />
</bean>

<bean id="txInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
  <property name="transactionManager" ref="transactionManager" />
  <property name="transactionAttributeSource">
    <bean class="org.springframework.transaction.interceptor.AttributesTransactionAttributeSource">
      <property name="attributes" ref="attributes" />
    </bean>
  </property>
</bean>

<bean id="attributes" class="org.springframework.metadata.commons.CommonsAttributes" />

这里的基本原理与AOP章节关于自动代理的讨论类似。

最重要的bean定义是自动代理的creator和advisor。注意实际的bean名称并不重要,重要的是它们的类。

org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator的bean定义会根据相应的advisor实现自动通告(自动代理)所有当前工厂的bean实例。 这个类不了解任何属性,但依赖于相应advisor的切入点,而切入点了解这些属性。

因此我们只需要一个能提供基于属性的声明式事务管理的AOP advisor。

这样同时能够添加自定义的advisor实现,它们能够被自动测试和应用。(如果有必要,你也可以使用带有与自动代理配置之外的标准属性相匹配的切入点的advisor。)

最后,属性bean是Commons Attributes中的Attributes的实现。把它替换为其它的org.springframework.metadata.Attributes接口实现,可以从另外的源获得属性。

25.5.2. 声明式事务管理

源码级属性的常见应用就是提供声明式事务管理。一旦有了前面的bean定义,你就可以定义任意多的需要声明式事务的应用对象。 只有定义了事务属性的类或者方法会被赋予事务通知。你唯一要做的就是定义需要的事务属性。

请注意你可以在类或方法级别指定事务属性。如果指定了类级别的属性,它将会被所有方法“继承”。方法级属性则会整体覆盖任意的类级别属性。

25.5.3. 缓冲

你还可以通过类级别属性提供缓冲行为。Spring能将这个行为赋予任何POJO。你只需要像下面这样指定一个被缓冲的业务对象的缓冲属性。

 /** 
 * @@org.springframework.aop.framework.autoproxy.target.PoolingAttribute(10)
 */
public class MyClass {

你将会需要常用的自动代理基础设施配置。然后你需要像下面这样指定一个缓冲的TargetSourceCreator。因为缓冲会影响目标的构造, 所以我们不能使用常规的通知。请注意如果一个类有一个缓冲属性,即使没有适合这个类的advisor,这个缓冲也会被应用。

<bean id="poolingTargetSourceCreator"
  class="org.springframework.aop.framework.autoproxy.metadata.AttributesPoolingTargetSourceCreator">
  <property name="attributes" ref="attributes" />
</bean>

相关的自动代理bean定义需要指定一组“custom target source creators”,包括Pooling target source creator。我们可以修改上面的示例来包含这个属性,如下所示:

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
  <property name="customTargetSourceCreators">
    <list>
      <ref bean="poolingTargetSourceCreator" />
    </list>
  </property>
</bean>

通常在Spring中使用元数据是一次性开销:一旦安装好了,其它业务对象就能非常方便地使用缓存。

因为一般对缓存的需要很少,所以很少有缓存很多业务对象的需要。所以这个特性并不被经常使用。

请参考Javadoc的org.springframework.aop.framework.autoproxy包以获得更详细的资料。可以通过很少的自定义代码来使用Commons Pool之外的另一个缓存实现。

25.5.4. 自定义元数据

因为潜在的自动代理基础设施的灵活性,我们甚至比.NET的元数据属性更强大。

我们能自定义属性来提供任何类型的申明式行为。要达到这些,你需要:

  • 定义你的自定义属性类。

  • 定义一个带有关注此自定义属性的切入点的Spring AOP Advisor

  • 通过普通的自动代理基础设施将这个Advisor作为bean定义加入到应用上下文中。

  • 给你的POJO加入属性.

有几个你可能需要这样做的潜在场所,比如自定义声明式安全管理,或者可能是缓存。

这是一个能在很多项目中显著降低配置开销的有效机制。但是要记住,这在底层依赖于AOP。你使用了越多的advisor,你运行时的配置复杂度就越高。 (如果你想知道哪个通知被对象使用,可以尝试创建一个org.springframework.aop.framework.Advised的引用。这样你能查看这些advisor。)

25.6. 使用属性来减少MVC web层配置

Spring元数据从1.0开始的另一个主要用处就是提供简化Spring MVC web层配置的方案。

Spring MVC提供了处理类映射的灵活性:将输入的请求映射到控制器(或者其它处理类)的实例上。 通常处理类映射是在相关的SpringDispatcherServletxxxx-servlet.xml 文件中配置的。

将这些映射保存在DispatcherServlet配置文件中通常是一个好主意。它提供了最大的灵活性,特别是:

  • 控制器实例通过XML bean定义由Spring IoC显式管理。

  • 因为映射在控制器之外,所以同一个控制器实例可以在一个DispatcherServlet上下文中获得多个映射,或者在不同的配置中重用。

  • Spring MVC可以支持基于任何标准的映射,而不仅仅是其它很多框架中支持的请求URL到控制器的映射。

然而,这的确意味着对于每一个控制器,我们都同时需要一个控制器映射(通常在一个控制器映射XML的bean定义中)和一个控制器自己的XML映射。

Spring提供了一种基于源码级属性的简单方式,这在很简单的场景中是很引人注目的选择。

本节描述的方式最适合简单的MVC相关场景。这也牺牲了一些Spring MVC的能力,比如在不同的映射中使用相同的控制器的能力,和基于请求URL之外的其它映射的能力。

这种方式中,控制器标识了一个或多个类级别的元数据属性,每一个都指定一个它们会被映射到的URL。

下面的例子展示了这种方式。在每个例子中,我们都会有一个依赖于Cruncher 类型的业务对象的控制器。 同样,这个依赖通过依赖注入解决。这个Cruncher需要通过相关的DispatcherServlet XML文件或上级上下文的bean定义中获取。

我们给这个控制器类绑定了一个指定需要映射的URL的属性。我们将这种依赖通过一个JavaBean属性或构造器参数来传递。 这个依赖一定要能够通过自动配置来解决:也就是说,在上下文中一定正好有一个Cruncher类型的业务对象。

/**
 * Normal comments here
 *
 * @@org.springframework.web.servlet.handler.metadata.PathMap("/bar.cgi")
 */
public class BarController extends AbstractController {

    private Cruncher cruncher;

    public void setCruncher(Cruncher cruncher) {
        this.cruncher = cruncher;
    }

    protected ModelAndView handleRequestInternal (
            HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("Bar Crunching c and d =" + cruncher.concatenate("c", "d"));
        return new ModelAndView("test");
    }
}

要让这个自动映射能生效,我们需要将下面的内容加到相关的 xxxx-servlet.xml 文件中,以指定属性处理器的映射关系。 这个特定的处理器映射能够处理任意多的带有上文的属性的控制器。这个bean的id("commonsAttributesHandlerMapping")并不重要,类型才是关键:

<bean id="commonsAttributesHandlerMapping"      
    class="org.springframework.web.servlet.handler.metadata.CommonsPathMapHandlerMapping"/>

在上面的例子中,我们现在不需要一个Attributes bean定义。这是因为这个类直接通过Commons Attributes API运行,而不是通过Spring的元数据抽象。

我们现在不需要为每个控制器指定XML配置。控制器会被自动映射到指定的URL。它们通过Spring的自动匹配能力从IoC中获益。 例如,上面展示的简单控制器的"cruncher" bean属性中的依赖,就是在当前的web应用上下文中自动获取的。Setter和Constructor依赖注入都可以实现零配置。

一个支持多个URL路径的构造器注入的例子:

/**
 * Normal comments here
 *
 * @@org.springframework.web.servlet.handler.metadata.PathMap("/foo.cgi")
 * @@org.springframework.web.servlet.handler.metadata.PathMap("/baz.cgi")
 */
public class FooController extends AbstractController {

    private Cruncher cruncher;

    public FooController(Cruncher cruncher) {
        this.cruncher = cruncher;
    }

    protected ModelAndView handleRequestInternal (
            HttpServletRequest request, HttpServletResponse response) throws Exception {
        return new ModelAndView("test");
    }
}

这个方式有下面一些好处:

  • 显著的减少了配置工作量。每次增加一个控制器,我们需要增加XML配置。通过属性驱动的事务管理,一旦建立了基本的基础设施,就非常容易增加更多的应用类。

  • 我们保留了很多Spring IoC的能力来配置控制器。

这个方式有以下一些局限性:

  • 一个更复杂的构建进程中的一次性开销。我们需要一个属性编译步骤和一个属性索引步骤。然而,一旦构建好了,这就不应该成为问题。

  • 现在只支持Commons Attributes,虽然将来可能会增加对其它属性提供者的支持。

  • 这种控制器只支持"根据类型自动匹配"的依赖注入。即使这样,它们也比Struts Action(框架中没有IoC支持)和WebWork Action(只有原始的IoC支持)要先进得多。

  • 依靠IoC自动解析可能容易引起混乱。

因为根据类型自动匹配意味着必须要有一个依赖于特定类型的bean,如果我们使用AOP一定要小心。例如,在使用TransactionProxyFactoryBean的常见情形中, 我们碰到两个Cruncher这样的业务接口的实现:原始的POJO定义和事务AOP代理。因为应用上下文不能清晰地解析依赖的类型, 这肯定不能运行。解决方案是使用AOP自动代理,构建好自动代理基础设施保证只定义了一个Cruncher 的实现,这个实现被自动通知。从而这个 方式能够象上文描述的那样与声明式面向属性的服务良好协作。这样也很容易构建,因为属性编译步骤必须恰当的去管理web控制器目标。

与其它的元数据功能不同的是,目前只有一个可用的Commons Attributes实现:org.springframework.web.servlet.handler.metadata.CommonsPathMapHandlerMapping。 这个局限是因为我们不仅仅需要属性编译,也需要属性索引:从属性API获得所有带有PathMap属性的类。 org.springframework.metadata.Attributes抽象接口目前还没有提供 索引功能, 也许将来会提供。(如果你希望增加对另外的属性实现的支持 - 它一定要支持索引 - 你 可以方便地扩展CommonsPathMapHandlerMapping类的父类 AbstractPathMapHandlerMapping,然后实现2个protected abstract方法,以使用你感兴趣的属性API)

总的来说,我们在构建过程中需要两个额外的步骤:属性编译和属性索引。前面已经讲解了属性索引任务的使用。请注意,Commons Attributes目前需要一个jar文件作为索引的输入。

25.7. 元数据属性的其它用法

元数据属性的其它一些用法正在逐渐流行。和Spring 1.2中一样,通过Commons Attributes(基于JDK 1.3+)和JSR-175注解(基于JDK 1.5),支持用于JMX暴露的元数据属性。

25.8. 增加对额外元数据API的支持

如果你希望提供对其它元数据API的支持,这很容易实现。

简单实现org.springframework.metadata.Attributes接口,并把它作为你的元数据API的门面。你就可以像前文所示那样在你的bean定义中包含这个对象。

所有象AOP元数据驱动自动代理这样的使用元数据的框架服务,都自动能使用你新建的元数据提供者。