第 8 章 源代码级的元数据支持
8.1. 源代码级的元数据
源代码级的元数据是对程序元素:通常为类和/或方法的 attribute 或者叫annotation的扩充。
举例来说,我们可以象下面一样给一个类添加元数据:
/**
* Normal comments
* @@org.springframework.transaction.interceptor.DefaultTransactionAttribute()
*/
public class PetStoreImpl implements PetStoreFacade, OrderService {
我们也可以添加元数据到一个方法上:
/**
* Normal comments
* @@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的格式。
源代码级的元数据随着Microsoft的.NET平台的发布被介绍给大众,它使用了源代码级的attribute 来控制事务,缓冲池(pooling)和一些其他的行为。
这种方法的价值已经被J2EE社区的人们认识到了。举例来说,跟EJB中清一色使用的传统的XML部署描述文 件比起来它要简单很多。XML描述文件适合于把一些东西从程序源代码中提取出来,一些重要的企业级设 定——特别是事务特性——本来属于程序代码。并不像EJB规范中设想的那样,调整一个方法的事务特性根本 没有什么意义。
虽然元数据attribute主要用于框架的基础架构来描述应用程序的类需要的业务, 但是也可以在运行时查询元数据attribute。这是与XDoclet这样的解决方案的关键区别,XDoclet 主要把元数据作为生成代码的一种方式,比如生成EJB类。
这一段包括了几个解决方案:
- JSR-175:标准的Java元数据实现,在Java 1.5中提供。 但是我们现在就需要一个解决方案,通常情况下可能还需要一个外观(facade)。
- XDoclet:成熟的解决方案,主要用于代码生成
- 其它不同的开源attribute实现,在JSR-175的发布 悬而未决的情况下,它们当中的Commons Attributes看来是最有前途的。所有的这些实现都需要一 个特定的前编译或后编译的步骤。
8.2. Spring的元数据支持
为了与它提供的其他重要概念的抽象相一致,Spring提供了一个对元数据实现的外观(facade), 以org.springframework.metadata.Attributes这个接口的形式来表示。
这样一个外观很有价值,因为下面几个原因:
- 目前还没有一个标准的元数据解决方案。 Java 1.5版本会提供一个,但是在Spring1.0版本的时候, Java 1.5仍是beta版本。而且,至少两年内还是需要对1.3和1.4版本的应用程序提供元数据支持。 现在Spring打算提供一些可以工作的解决方案: 在一个重要的环境下等待1.5,并不是个好的选择.
- 目前的元数据API,例如Commons Attributes(被Spring 1.0使用), 测试起来很困难。Spring提供了一个简单的更容易模拟的元数据接口。
- 即使当Java 1.5在语言级别提供了对元数据的支持时,提供了一个如此的抽象仍然是有价值的:
- JSR-175的元数据是静态的。它是在编译时与某一个类关联,而在部署环境下是不可改变的。 这里会需要多层次的元数据,以支持在部署时重载某些attribute的值--举例来说, 在一个XML文件中定义用于覆盖的attribute。
- JSR-175的元数据是通过Java反射API返回的。这使得在测试时无法模拟元数据。 Spring提供了一个简单的接口来允许这种模拟。
虽然Spring在Java 1.5达到它的GA版本之前将支持JSR-175,但仍会继续提供一个attribute抽象API。
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能提供更多的功能,比如定义在方法参数上的attributes。 在1.0版本时,Spring目的在于提供元数据的一个子集,使得能象EJB或.NET一样提供有效的声明式企业级服务。 1.0版本以后,Spring将提供更多的元数据方法。
注意到该接口像.NET一样提供了Object类型的attibute。 这使得它区别于一些仅提供String类的attribute的attribute系统, 比如Nanning Aspects和JBoss 4(在DR2版本时)。支持Object类型的attribute有一个显著的优点。 它使attribute含有类层次,还可以使attribute能够灵活的根据它们的配置参数起作用。
对于大多数attribute提供者来说,attribute类的配置是通过构造函数参数或JavaBean的属性完成的。Commons Attributes同时支持这两种方式。
同所有的Spring抽象API一样,Attributes是一个接口。 这使得在单元测试中模拟attribute的实现变得容易起来。
8.3. 集成Jakarta Commons Attributes
虽然为其他元数据提供者来说,提供org.springframework.metadata.Attributes
Commons Attributes 2.0 (http://jakarta.apache.org/commons/sandbox/attributes/) 是一个功能很强的attribute解决方案。它支持通过构造函数参数和JavaBean属性来配置attribute, 也提供了更好的attribute定义的文档。(对JavaBean属性的支持是在Spring team的要求下添加的。)
我们已经看到了两个Commons Attributes的attribute定义的例子。通常,我们需要解释一下:
- Attribute类的名称。 这可能是一个FQN,就像上面的那样。如果相关的attribute类已经被导入, 就不需要FQN了。你也可以在attibute编译器的设置中指定attribute的包名。
- 任何必须的参数化,可以通过构造函数参数或者JavaBean属性完成。
Bean的属性如下:
/** * @@MyAttribute(myBooleanJavaBeanProperty=true) */
把构造函数参数和JavaBean属性结合在一起也是可以的(就像在Spring IoC中一样)。
因为,并不象Java 1.5中的attribute一样,Common Attributes没有和Java语言本身结合起来, 因此需要运行一个特定的attribute编译的步骤作为整个构建过程的一部分。
为了在整个构建过程中运行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.接下来,定义一个attribute编译任务,它将使用Commons Attributes的attribute-compiler任务 来“编译”源代码中的attribute。这个过程将生成额外的代码至destdir属性指定的位置。 在这里我们使用了一个临时目录:
<target name="compileAttributes" > <attribute-compiler destdir="${commons.attributes.tempdir}" > <fileset dir="${src.dir}" includes="**/*.java"/> </attribute-compiler> </target>
运行javac命令编译源代码的编译目标任务应该依赖于attribute编译任务,还需要编译attribute时生成至 目标临时目录的源代码。如果在attribute定义中有语法错误,通常都会被attribute编译器捕获到。 但是,如果attribute定义在语法上似是而非,却使用了一些非法的类型或类名, 编译所生成的attribute类可能会失败。在这种情况下,你可以看看所生成的类来确定错误的原因。
Commons Attributes也提供对Maven的支持。请参考Commons Attributes的文档得到进一步的信息。
虽然attribute编译的过程可能看起来复杂,实际上是一次性的耗费。一旦被创建后,attribute的编译是递增式的, 所以通常它不会减慢整个构建过程。一旦编译过程完成后, 你可能会发现本章中描述的attribute的使用将节省在其他方面的时间。
如果需要attribute的索引支持(目前只在Spring的以attribute为目标的web控制器中需要,下面会讨论到), 你需要一个额外的步骤,执行在包含编译后的类的jar文件上。在这步可选的步骤中, Commons Attributes将生成一个所有在你源代码中定义的attribute的索引,以便在运行时进行有效的查找。 该步骤如下:
<attribute-indexer jarFile="myCompiledSources.jar"> <classpath refid="master-classpath"/> </attribute-indexer>
可以到Spring jPetStore例程下的attributes目录下察看关于该构建过程的例子。 你可以使用它里面的构建脚本,并修改该脚本以适应你自己的项目。
如果你的单元测试依赖于attribute,尽量使它依赖于Spring对于Attribute的抽象,而不是Commons Attributes。 这不仅仅为了更好的移植性——举例来说,你的测试用例将来仍可以工作如果你转换至Java 1.5的attributes—— 它也简化了测试。Commons Attributes是静态的API,而Spring提供的是一个容易模拟的元数据接口。
8.4. 元数据和Spring AOP自动代理
元数据attributes最重要的用处是和Spring AOP的联合。 提供类似于.NET风格的编程模式,声明式的服务会被自动提供给声明了元数据attribute的应用对象。 这样的元数据attribute可以像在声明式事务管理一样被框架直接支持,也可以是自定义的.
这就是AOP和元数据attribute配合使用的优势所在。
8.4.1. 基础
基于Spring AOP自动代理功能实现。配置可能象这样:
<bean id="autoproxy"
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
</bean>
<bean id="transactionAttributeSource"
class="org.springframework.transaction.interceptor.AttributesTransactionAttributeSource"
autowire="constructor">
</bean>
<bean id="transactionInterceptor"
class="org.springframework.transaction.interceptor.TransactionInterceptor"
autowire="byType">
</bean>
<bean id="transactionAdvisor"
class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor"
autowire="constructor" >
</bean>
<bean id="attributes"
class="org.springframework.metadata.commons.CommonsAttributes"
/>
这里的基本概念和AOP章节中关于自动代理的讨论一致。
最重要的bean的定义名称为autoproxy和 transactionAdvisor。要注意bean的实际名称并不重要; 关键是它们的类。
所定义的自动代理bean org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
因而我们只是需要一个AOP的advisor来提供基于attribute的声明式事务管理。
也可以添加任意的自定义Advisor实现,它们将被自动计算并应用。 (如果有必要的话,你可以使用切入点除了符合自动代理配置的attribute,而且满足一定规则的Advisor。)
最后,attributes bean使用的是Commons Attributes的实现。 也可以用org.springframework.metadata.Attributes的另一个实现代替。
8.4.2. 声明式事务管理
源代码级的attribute的最普通用法是提供了类似.NET的声明式事务管理。一旦定义好上面的bean定义, 你就能定义任意数目的需要声明式事务的应用对象了。只有那些拥有事务attribute的类或方法会被有事务的通知。 除了定义你需要的事务attribute以外,其他什么都不用做。
不象在.NET中那样,你可以在类或方法级别指定事务attribute。 如果指定了类级别的attribute,它的所有方法都会“继承”它。 方法中定义的attribute将完全覆盖类上定义的attribute。
8.4.3. 缓冲池技术
再者,象.NET中一样,你可以通过在类上指定attribute增加缓冲池行为。 Spring能把该行为应用到任何普通Java对象上。你只需在需要缓冲的业务对象上指定一个缓冲池的attribute,如下:
/** * @@org.springframework.aop.framework.autoproxy.target.PoolingAttribute (10) * * @author Rod Johnson */ public class MyClass {
你需要通常的自动代理的基本配置。 然后指定一个支持缓冲池的TargetSourceCreator,如下所示。 由于缓冲池会影响目标对象的创建,我们不能使用一个通常的advice。 注意如果一个类有缓冲池的attribute,即使没有任何advisors应用到该类,缓冲池也会对该类起作用。
<bean id="poolingTargetSourceCreator"
class="org.springframework.aop.framework.autoproxy.metadata.AttributesPoolingTargetSourceCreator"
autowire="constructor" >
</bean>
对应的自动代理bean定义需要指定一系列的“自定义的目标源生成器”,包括支持缓冲池的目标源生成者。 我们可以修改上面的例子来引入这个属性,如下:
<bean id="autoproxy"
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
>
<property name="customTargetSourceCreators">
<list>
<ref local="poolingTargetSourceCreator" />
</list>
</property>
</bean>
就像通常在Spring中使用元数据一样,这是一次性的耗费:一旦创建完成后, 在其它业务对象上使用缓冲池是非常容易的。
对于很少需要缓冲池技术的地方,很少需要缓冲大量业务对象,这是就值得斟酌了。 因此这个功能好像也不是经常使用。
详细请参考org.springframework.aop.framework.autoproxy包的Javadoc。 除了以最少的代码量使用Commons Pool以外,其它的缓冲池实现也是可以的。
8.4.4. 自定义的元数据
由于自动代理底层架构的灵活性,我们甚至还可以超越.NET元数据attribute的功能。
我们可以定义一些自定义的attribute来提供任何种类的行为。为了达到这个目的,你需要:
- 定义你的自定义attribute类
- 定义一个Spring AOP Advisor,自定义attributes的出现的时候触发它的切入点。
- 把Advisor当作一个bean的定义添加到一个包含普通自动代理基础架构的应用上下文中。
- 把attribute添加到普通Java对象上。
有几个潜在的领域中你可能想这么做,比如自定义的声明式安全管理,或者可能的缓存。
这是一个功能很强的机制,它能显著减少某些工程中的配置工作。但是,要记住它在底层是依赖AOP的。 你在应用的使用的Advisors越多,运行时配置将会越复杂。
(如果你想查看object上应用了哪些通知, 可以看一下关于org.springframework.aop.framework.Advised的参考。它使你能够检查相关的Advisors。)
8.5. 使用attribute尽可能减少MVC web层配置
Spring 1.0中的元数据的另一个主要用法是为简化Spring MVC web配置提供了一个选择。
Spring MVC提供了灵活的处理器映射: 将外来的请求映射到控制器(或其它的处理器)实例上。 通常上处理器映射被配置在相应的Spring DispatcherServlet的xxx-servlet.xml文件中。
把这些配置定义在DispatcherServlet的配置文件中通常是个不错的选择。它提供了最大的灵活性。 特别的是:
- Controller实例是显式的被Spring IoC通过XML bean定义来管理。
- 该映射位于controller的外面, 所以相同controller的实例可以在同一个DispatcherServlet中被赋予不同的映射或者在不同的配置中重用。
- Spring MVC能够支持在任何规则上的映射,而不是大多数其它框架中仅有的将请求URL映射到控制器。
然而,对于每个controller来说,我们同时需要一个处理器映射(通常是一个处理器映射的XML bean定义), 和一个控制器自身的一个XML映射描述。
Spring提供了一个基于源代码级attribute的更简单的方法,对于一些简单场景来说是一个吸引人的选择。
这一段描述的方法很适合一些简单的MVC场景。但它牺牲了一些Spring MVC的功能, 比如根据不同的映射使用相同的控制器,把映射基于除了请求的URL之外的规则。
在这种方法下,控制器上标记了一个或多个类级的元数据attribute, 每一个attribute指定了它们应该映射的一个URL。
下面的例子展示了这种方法。在每一种情况下,我们都有一个依赖于Cruncher类的业务对象控制器。 和往常一样,这种依赖性将通过依赖注射来解决。 Cruncher必须作为一个bean定义出现在相关的DispatcherServlet的XML文件中,或在一个父上下文中。
我们把一个attribute设置到控制器类上,并指定应该映射的URL。 我们可以通过JavaBean属性或构造函数参数来表达依赖关系。这种映射必须通过自动装配来解决: 那就是说,在上下文中必须只能有一个Crucher类型的业务对象。
/**
* Normal comments here
* @author Rod Johnson
* @@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 arg0, HttpServletResponse arg1)
throws Exception {
System.out.println("Bar Crunching c and d =" +
cruncher.concatenate("c", "d"));
return new ModelAndView("test");
}
}
为了使这个自动映射起作用,我们需要添加下面的配置到相关的xxxx-servlet.xml文件中, 这些配置是用来指定相关attribute的处理器映射的。 这个特殊的处理器映射可以处理任意数量的象上面一样带有attribute的控制器。 Bean的id(“commonsAttributesHandlerMapping”)并不重要。关键是它的类型:
<bean id="commonsAttributesHandlerMapping"
class="org.springframework.web.servlet.handler.metadata.CommonsPathMapHandlerMapping"
/>
现在我们并不需要一个象上面例子中的Attributes bean的定义, 因为这个类直接利用Commons Attributes API,而不是通过Spring的元数据抽象。
现在我们不再需要对每一个控制器提供XML配置。控制器被自动映射到指定的URL上。 控制器得益于IoC,使用了Spring的自动装配功能。举例来说, 上述的简单控制器中的“cruncher”属性上的依赖性会自动在当前的web应用中解决. Setter方法和构造函数依赖注射都是可以的,每一个都是零配置。
构造函数注册的一个例子,同时演示了多个URL路径:
/**
* Normal comments here
* @author Rod Johnson
*
* @@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 arg0, HttpServletResponse arg1)
throws Exception {
return new ModelAndView("test");
}
}
这种方法有着下列的好处:
- 显著的减少配置工作量。每一次我们增加一个控制器时,我们不需要XML的配置。 就像attribute驱动的事务管理一样,一旦有了基础架构后,添加更多的应用类将会很简单。
- 我们保留了Spring IoC配置控制器的能力。
不过该方法有如下的限制:
- 这个方法使构建过程更加复杂,不过开销是一次性的。 我们需要一个attribute编译步骤和一个建立attribute的索引步骤。 但是,一旦这些步骤就位后,这将不再是一个问题。
- 虽然将来会增加对其他attribute提供者的支持,但目前只支持Commons Attributes。
- 只有“根据类型的自动装配”的依赖注射才支持这样的控制器。但是, 这妨碍这些控制器对Structs Actions(在框架中并没有IoC的支持), 和值得斟酌的WebWork Actions(只有基本的IoC支持,并且IoC逐步受到关注)的领先。
- 依赖自动魔法的IoC解决方案可能是令人困惑的。
由于根据类型的自动装配意味着必须依赖一种指定类型,如果我们使用AOP就需要小心了。 在通常使用TransactionProxyFactoryBean的情况下,举例来说, 我们对一个业务接口例如Cruncher有两个实现: 一个为原始的普通Java对象的定义,另一个是带有事务的AOP代理。这将无法工作, 因为所有者应用上下文无法确切的解析这种类型依赖。解决方法是使用AOP的自动代理, 建立起自动代理的基本架构,这样就只有一个Crucher的实现, 同时该实现是自动被通知的。 因此这种方法和上面的以attribute为目标声明式服务能很好的一起工作。 因为attribute编译过程必须恰当处理web控制器的定位,所有这就很容易被创建
不象其它的元数据的功能,当前只有Commons Attributes的一种实现: org.springframework.web.servlet.handler.metadata.CommonsPathMapHandlerMapping。 这个限制是因为我们不仅需要attribute编译,还需要attribute索引: 根据attribute API来查询所有拥有PathMap attribute的类。虽然将来可能会提供, 但目前在org.springframework.metadata.Attributes的抽象接口上还没有提供索引。 (如果你需要增加另一个attribute实现的支持,并且必须支持索引, 你可以简单的继承AbstractPathMapHandlerMapping, CommonsPathMapHandlerMapping的父类, 用你选择的attribute API来实现其中两个protected的虚方法。)
因此我们在构建过程中需要两个额外的步骤:attribute编译和建立attribute索引。 上面已经展示了attributer索引建立器的使用方法。要注意到目前Commons Attributes仍需要一个Jar文件来建立索引。
如果你开始使用控制器元数据映射方法,你可以在任何一个地方转换至经典的Spring XML映射方法。 所以你不需要拒绝这种选择。正因为这个原因,我发现我经常在开始一个web应用时使用元数据映射.
8.6. 元数据attribute的其它使用
对元数据attribute的其它应用看来正在流行。到2004年3月时, 一个为Spring建立的基于attribute的验证包正在开发中。 当考虑到潜在的多次使用时,attribute解析的一次性开销看起来更加吸引人了.
8.7. 增加对其它的元数据API的支持