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注解。 不过, 既然Spring也支持Java 1.4,我们仍需要一个JVM不同版本间的解决方案。Spring元数据支持就提供了这样一个方案。

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

  • 在多种不同的针对Java 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提供了比这更多的功能,比如定义在方法参数上的属性。

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

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

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

25.3. 注解

Spring有很多自定义的Java 5+注解。

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

希望上面的类定义看起来还算简单。你必须为所有SimpleMovieLister类的BeanDefinitions 提供一个值。

让我们看一个能通过验证的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类。 这是一个由特殊的BeanPostProcessor实现, 能感知@Required并提供'要求属性未被设置时提示'的逻辑。 它容易配置;只要简单地把下列bean定义放入你的Spring XML配置中。

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

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

看个例子,让我们假设你(或你的组织/团队)已经定义了一个叫做@Mandatory的属性。 你能用如下方法让一个RequiredAnnotationBeanPostProcessor实例感知@Mandatory

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

这是@Mandatory注解的源代码。 请确保你的自定义注解类型本身针对目标(target)和运行时保持策略(runtime retention policy)使用了合适的注解。

package your.company.package;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Mandatory {
}

25.4. Jakarta Commons Attributes集成

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

Commons Attributes 2.2(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实现来自动通知("auto-proxy")当前工厂中的所有bean实例。 这个类对属性一无所知,只是依赖于advisor匹配的切入点,而切入点了解这些属性。

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

这样还能添加自定义的advisor实现,它们能被自动运算并应用。 (如果有必要,你也可以使用这样的advisor,它的切入点还能匹配那些相同自动代理配置中除属性以外的条件。)

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

25.5.2. 声明式事务管理

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

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