架构
爱因斯坦曾经说过:"每件事物都应该尽可能简单,而不是更简单"。的确,对科学真理的追求都是为了简化理论的根本假设,这样我们才能处理真正麻烦的问题。企业级软件的开发也是这样的。
简化企业级软件开发的关键是提供一个隐藏了复杂性(例如事务、安全性和永续性)的应用框架。良好设计的框架组件可以提升代码的重复使用(reuse)能力,提高开发效率,从而得到更好的软件质量。但是,目前J2EE 1.4中的EJB 2.1框架组件被人们普遍认为是设计较差的和过于复杂的。Java开发者对EJB 2.1很不满,他们已经试验了多种其它的用于中间件服务传送的方法。最引人注目的,下面两个框架组件已经引起开发者的巨大兴趣和积极的反映。它们很可能成为未来企业级Java应用程序可供选择的框架组件。
· Spring框架组件是一个流行的,但是非标准的开放源代码框架组件。它主要是由Interface21 Inc.公司开发和控制的。Spring框架组件的架构是基于依赖注入(DI)设计模式的。Spring可以单独地或者与现有的应用程序服务器一起工作,它大量地使用XML配置文件。
· EJB 3.0框架组件是一个标准的框架组件,由Java社区组织(JCP)定义,并受到所有主流的J2EE厂商支持。预发布的EJB 3.0规范的开放源代码和商业实现都可以在JBoss和Oracle上看到了。EJB 3.0大量使用Java注释(annotation)。
这两个框架组件的核心设计理念是相同的:两者的目标都是把中间件服务传递给松散耦合的简单旧式Java对象(POJO)。这些框架组件通过在运行时截取执行内容或向POJO注入服务对象,把应用程序服务与POJO捆绑在一起。POJO本身不关心捆绑的过程,并且对框架组件几乎没有依赖。其结果是,开发者可以聚焦于业务逻辑,个人可以在没有框架组件的情况下测试他们的POJO。此外,由于POJO不需要从框架组件中继承或实现框架组件接口,开发者建立继承结构和构建应用程序的时候都有高度的灵活性。
但是,尽管两者的设计理念是相同的,它们传递POJO服务时却采用了完全不同的方法。尽管目前已经出版了大量的图书和文章来把Spring或EJB 3.0与EJB 2.1进行对比,但是它们都没有对Spring与EJB 3.0之间的差异进行认真的研究。在本文中,我将研究Spring和EJB 3.0框架组件之间的关键差异,并讨论它们的优缺点。本文的主题也可以应用在其它一些名气稍小的企业级中间件框架组件上,因为它们都聚焦于"松散耦合的POJO"设计。我希望本文能够帮助你选择符合需求的最佳的框架组件。
厂商无关性(Independence)
开发者选择某种Java平台的一个最重要的理由就是该平台的厂商无关性。EJB 3.0是一个开放的、标准的、具有厂商无关性的平台。EJB 3.0规范是由企业级Java团体中所有主流开放源代码和商业厂商开发和支持的。EJB 3.0框架组件把开发人员与应用程序服务器实现(implementation)隔离开来了。例如,尽管JBoss的EJB 3.0实现是基于Hibernate的,而Oracle的EJB 3.0实现是基于TopLink的,但是开发人员并不需要学习Hibernate或TopLink的特殊API,就可以让他们的应用程序在JBoss和Oracle上运行。厂商无关性把EJB 3.0框架组件与其它的POJO中间件框架组件区分开来了。
但是,很多EJB 3.0的批评家迅速指出,在写这篇文章的时候,EJB 3.0规范还没有达到最终发表的版本。在EJB 3.0被所有主流的J2EE厂商采用之前可能还需要一到两年时间。但是,即使你的应用程序服务器还没有自然地(natively)支持EJB 3.0,你还是可以通过下载和安装一个"嵌入式的" EJB 3.0产品,在服务器上运行EJB 3.0应用程序。例如,JBoss嵌入式EJB 3.0产品是开放源代码的,可以在任何与J2SE-5.0兼容的环境中(例如,在Java应用程序服务器中)运行。它现在正在进行beta测试。其它的厂商也可能很快发布他们的嵌入式EJB 3.0产品,特别是用于规范的"数据永续性"部分的产品。
另一方面,Spring一直是非标准的技术,而且在可以预见的未来它仍然是这样的。尽管你可以把Spring框架组件与任何应用程序服务器一起使用,但是Spring应用程序都被"锁定"在Spring自身和你所选择的集成到Spring中的特定服务中了。
· 尽管Spring框架组件是一个开放源代码项目,但是它仍然拥有配置文件的专利XML格式和专利编程接口。当然,这类"锁定"发生在任何非标准的产品上,Spring也不例外。但是它却造成了:你的Spring应用程序的长期生存能力依赖于Spring项目本身(或Interface21 Inc公司,它雇佣了大多数Spring核心开放人员)。此外,如果你使用任何Spring特定的服务,例如Spring事务管理器或Spring MVC,你就被"锁定"在这些API中了。
· Spring应用程序需要知道后台的服务提供者。例如,对于数据持续(data persistence)服务来说,Spring框架组件为JDBC、Hibernate、iBatis和JDO使用了不同的DAO和模板辅助类。因此,如果你希望为Spring应用程序更换持续服务提供者(例如从JDBC切换到Hibernate),你就必须重构自己的应用程序代码,使用新的辅助类。
服务集成
从较高的层次看,Spring框架组件位于应用程序服务器和服务类库之上。其服务集成代码(例如数据访问模板和辅助类)位于框架组件之中,并暴露给应用程序开发者。与此不同的是,EJB 3.0框架组件被紧密地集成到应用程序服务器中,服务集成代码被封装在标准的接口中。
其结果是,EJB 3.0厂商可以积极地优化总体性能和开发者体验。例如,在JBoss的 EJB 3.0实现中,使用EntityManager保持实体Bean POJO的时候,下层Hibernate对话事务会自动地与该调用方法的JTA事务联系在一起,当JTA事务提交的时候,它也会提交。如果使用简单的PersistenceContext注释(本文后面有一个例子),你甚至于可以在有状态的(stateful)对话bean中把EntityManager和它的下层Hibernate事务捆绑到一个应用程序事务上。该应用程序事务在一个对话中跨越了多个线程,它在事务性的Web应用程序(例如多页面购物车)中是非常有效的。由于在JBoss中,EJB 3.0框架组件、Hibernate和Tomcat紧密集成,上述的简单和集成的编程接口才得以实现。Oracle的EJB 3.0框架组件和其下层Toplink持续服务之间的也实现了类似层次的集成。
EJB 3.0中集成服务的另一个例子是群集(clustering)支持。如果你在服务器群集中部署EJB 3.0应用程序,那么所有的失效接续(fail-over)、负载均衡、分布式缓存和状态复制服务都是可以自动地供应用程序使用的。下层群集服务都隐藏在EJB 3.0编程接口后面,它们对于EJB 3.0开发人员来说是完全透明的。
在Spring中,优化框架组件与服务之间的交互操作要困难得多。例如,为了使用Spring的宣告式事务服务来管理Hibernate事务,你必须在XML配置文件中显式地配置Spring TransactionManager和Hibernate SessionFactory对象。Spring应用程序开发者必须显式地管理跨多个HTTP请求的事务。此外,要在Spring应用程序中使用群集服务也没有简单的途径。
服务集成的灵活性
由于Spring中的服务集成代码是作为编程接口的一部分暴露的,应用程序开发者可以根据需要灵活地集成服务。这个特性允许你集成自己的"轻量级"应用程序服务器。Spring最普遍的使用方式是把Tomcat和Hibernate"粘合"在一起来提供简单的数据库驱动web应用程序。在这种情况下,Spring自身提供事务服务,Hibernate提供持续(persistence)服务--这种组织方式在Spring中建立了一个微型应用程序服务器。
EJB 3.0应用程序服务器没有赋予你挑选服务的灵活性。在大多数情况中,你得到一组事先包装好的特性,而你只需要其中的一部分。但是,如果应用程序服务器由模式化的内部设计主导(类似JBoss),那么你就可能把它分开,去掉一些不必要的特性。在任何情况下,定制成熟的应用程序服务器都不是一个简单的事情。
当然,如果应用程序的范围超越了单节点,那么你可能需要捆绑来自普通应用程序服务器的服务(例如资源缓冲池、消息队列和群集)。在总体的资源消耗方面,Spring解决方案与任何EJB 3.0解决方案一样,都是"重量级"的。
在Spring中,灵活的服务集成使得我们更容易把仿制(mock)对象(而不是实际的服务对象)捆绑到应用程序,用于在容器外部进行单元测试。在EJB 3.0应用程序中,大多数组件都是简单的POJO,我们可以很容易地在容器外部测试这些它们。但是对于测试那些涉及到容器服务的对象(例如持续EntityManager),我们推荐在容器内测试,因为比起仿制对象的方法,它们更简单、更牢固、更精确。 XML与注释的比较
从应用程序开发者的角度来看,Spring的编程接口主要是基于XML配置文件的,而EJB 3.0广泛使用了Java注释。XML文件可以表达复杂的关系,但是它们同时也很冗长、牢固程度也较低。注释简单明了,但是在注释中我们却很难表达复杂的或层次的结构。
Spring和EJB 3.0关于XML或注释的选择是依赖于这两个框架组件后面的架构的:由于注释只能保存相当少的配置信息,只有预先集成的框架组件(类似在框架组件中已经完成了大多数预备工作)可以广泛地把注释作为配置选项。我们已经讨论过了,EJB 3.0符合这种需求,而Spring作为一个通用的DI框架组件,不符合这个需求。
当然,EJB 3.0和Spring都在学习对方的最佳特性,它们都在某个程度上支持XML和注释。例如,在EJB 3.0中XML配置文件是一个可选的重载机制,可以用于改变注释的默认行为。注释也可以用于配置某些Spring服务。
认识XML和注释之间的区别的最好途径是通过示例。在下一部分,我们会看到Spring和EJB 3.0是如何为应用程序提供关键服务的。
宣告式服务(Declarative Services)
Spring和EJB 3.0都把运行时服务(例如事务、安全性、日志记录、消息和定制服务)捆绑到应用程序上。由于这些服务都没有直接地与应用程序的业务逻辑相关联,因此它们不由应用程序自身来管理。作为代替,这些服务是在运行时由服务容器(例如Spring或EJB 3.0)透明地应用在程序上的。开发者(或管理员)配置容器并告诉容器如何/什么时候应用服务。
EJB 3.0使用Java注释配置宣告式服务,而Spring使用XML配置文件。在大多数情况下,对于这类服务,EJB 3.0注释方法更加简单,更加优雅。下面是一个在EJB 3.0中给POJO方法应用事务服务的例子。
public class Foo {
TransactionAttribute(TransactionAttributeType.REQUIRED)
public bar () {
// 执行某些操作 ...
}
}
你也可以在一个代码片断中定义多个属性,应用多个服务。下面是一个在EJB 3.0中同时给POJO应用了事务和安全性服务的例子:
SecurityDomain("other")
public class Foo {
RolesAllowed({"managers"})
TransactionAttribute(TransactionAttributeType.REQUIRED)
public bar () {
// 执行某些操作 ...
}
}
使用XML指定代码属性和配置宣告式服务可能导致冗长的和不稳定的配置文件。下面是一个在Spring应用程序中利用XML元素给Foo.bar()方法应用一个非常简单的Hibernate事务服务的例子:
<!-- Setup the transaction interceptor -->
<bean id="foo"
class="org.springframework.transaction
.interceptor.TransactionProxyFactoryBean">
<property name="target">
<bean class="Foo"/>
</property>
<property name="transactionManager">
<ref bean="transactionManager"/>
</property>
<property name="transactionAttributeSource">
<ref bean="attributeSource"/>
</property>
</bean>
<!-- Setup the transaction manager for Hibernate -->
<bean id="transactionManager"
class="org.springframework.orm
.hibernate.HibernateTransactionManager">
<property name="sessionFactory">
<!-- you need to setup the sessionFactory bean in
yet another XML element -- omitted here -->
<ref bean="sessionFactory"/>
</property>
</bean>
<!-- Specify which methods to apply transaction -->
<bean id="transactionAttributeSource"
class="org.springframework.transaction
.interceptor.NameMatchTransactionAttributeSource">
<property name="properties">
<props>
<prop key="bar">
</props>
</property>
</bean>
如果你给同一个POJO添加多个Lan截器(interceptor,例如安全性Lan截器),那么XML的复杂程度会呈几何级数增长。Spring意识到了只使用XML配置文件的局限性,它现在支持在Java源代码中使用Apache通用元数据指定事务属性。在最新的Spring 1.2中,还支持JDK-1.5样式的注释。如果你要使用事务元数据,就需要把上面的transactionAttributeSource bean改变成AttributesTransactionAttributeSource示例,并增加与元数据Lan截器相关的额外配置。
<bean id="autoproxy"
class="org.springframework.aop.framework.autoproxy
.DefaultAdvisorAutoProxyCreator"/>
<bean id="transactionAttributeSource"
class="org.springframework.transaction.interceptor
.AttributesTransactionAttributeSource"
autowire="constructor"/>
<bean id="transactionInterceptor"
class="org.springframework.transaction.interceptor
.TransactionInterceptor"
autowire="byType"/>
<bean id="transactionAdvisor"
class="org.springframework.transaction.interceptor
.TransactionAttributeSourceAdvisor"
autowire="constructor"/>
<bean id="attributes"
class="org.springframework.metadata.commons
.CommonsAttributes"/>
当你拥有很多事务方法的时候,Spring元数据简化了transactionAttributeSource元素。但是它没有解决XML配置文件的基本问题--冗长和脆弱,还是需要使用事务Lan截器、transactionManager和transactionAttributeSource。
依赖注入(Dependency Injection)
中间件容器的关键优势在于它们允许开发者建立松散耦合的应用程序。服务的客户端只需要知道服务的接口。容器用具体的实现来初始化服务对象,并使客户端能够访问它们。这就允许了容器在不同的服务实现之间进行切换,而不需要改变接口或客户端代码。
依赖注入(DI)模式是实现松散耦合的应用程序的最好的方法之一。它比旧方法(例如通过JNDI的依赖查找或容器回调)更易于使用、更优雅。使用DI的时候,框架组件充当建立服务对象的对象工厂,并根据运行时配置,把这些服务对象注入应用程序POJO中。从应用程序开发者的角度来看,当客户端POJO需要使用某种服务对象的时候,它们会自动地获取该对象。
Spring和EJB 3.0都给DI模式提供了广泛的支持,但是它们之间有一些深刻的差异。Spring支持普通的、但是复杂的、基于XML配置文件的DI API;EJB 3.0通过简单的注释支持大多数通用服务对象(例如EJB和上下文关系对象)和JNDI对象的注入操作。
EJB 3.0 DI注释非常简洁,易于使用。Resource标签注入大多数通用服务对象和JNDI对象。下面的例子演示了如何把JNDI中的服务器的默认DataSource对象注入POJO的一个字段变量中。DefaultDS是JNDI用于表示DataSource的名称。在第一次使用myDb变量之前,会把正确的值自动地赋给它。
public class FooDao {
Resource (name="DefaultDS")
DataSource myDb;
// 使用 myDb 获取数据库的JDBC连接
}
作为对字段变量直接注入的补充,我们还可以使用EJB 3.0中的Resource注释,通过设置(setter)方法来注入对象。例如,下面的例子就注入了一个对话上下文关系(context)对象。应用程序一直没有显式调用设置方法--该方法在被其它的任何方法调用之前,会先被容器调用。
Resource
public void setSessionContext (SessionContext ctx) {
sessionCtx = ctx;
}
对于更加复杂的服务对象,已经定义了一些专用的注入注释。例如,EJB注释用于注入EJB stub,PersistenceContext注释用于注入EntityManager对象(它为EJB 3.0实体bean处理数据库访问)。下面的例子演示了如何向一个有状态的对话bean注入EntityManager对象。PersistenceContext注释的type属性指明被注入的EntityManager拥有扩展的事务上下文关系--它不会自动地与JTA事务管理器一起提交,因此它可以用于那些在一个对话中跨越多个线程的应用程序事务。
Stateful
public class FooBean implements Foo, Serializable {
PersistenceContext(
type=PersistenceContextType.EXTENDED
)
protected EntityManager em;
public Foo getFoo (Integer id) {
return (Foo) em.find(Foo.class, id);
}
}
EJB 3.0规范定义了可以通过注释注入的服务器资源。但是它不支持用户自定义的应用程序POJO的彼此相互注入。
在Spring中,你首先需要为POJO的服务对象定义一个设置方法(或者带参数的构造函数)。下面的例子显示POJO需要一个指向Hibernate对话工厂的指针。
public class FooDao {
HibernateTemplate hibernateTemplate;
public void setHibernateTemplate (HibernateTemplate ht) {
hibernateTemplate = ht;
}
// 使用 Hibernate 模板访问数据
public Foo getFoo (Integer id) {
return (Foo) hibernateTemplate.load (Foo.class, id);
}
}
接下来,你可以指定容器如何在运行时通过XML元素链获取服务对象并把它捆绑到POJO上。下面的例子演示了把数据源捆绑到Hibernate对话工厂,把对话捆绑到Hibernate模板对象,最后把模板对象捆绑到应用程序POJO的XML元素。这段Spring代码如此复杂的部分原因在于我们必须手动地注入下层Hibernate管道对象,而EJB 3.0 EntityManager是由服务器自动地管理和配置的。但是这又让我们回到了Spring没有像EJB 3.0那样与服务紧密集成的讨论中了。
<bean id="dataSource"
class="org.springframework
.jndi.JndiObjectFactoryBean">
<property name="jndiname">
<value>java:comp/env/jdbc/MyDataSource</value>
</property>
</bean>
<bean id="sessionFactory"
class="org.springframework.orm
.hibernate.LocalSessionFactoryBean">
<property name="dataSource">
<ref bean="dataSource"/>
</property>
</bean>
<bean id="hibernateTemplate"
class="org.springframework.orm
.hibernate.HibernateTemplate">
<property name="sessionFactory">
<ref bean="sessionFactory"/>
</property>
</bean>
<bean id="fooDao" class="FooDao">
<property name="hibernateTemplate">
<ref bean="hibernateTemplate"/>
</property>
</bean>
<!-- The hibernateTemplate can be injected
into more DAO objects -->
尽管在Spring中基于XML的依赖注入语法是复杂的,但是它却很强大。你可以把任何POJO(包括你在应用程序中定义的)注入另一个POJO。如果你真的希望在EJB 3.0应用程序中使用Spring的依赖注入能力,你可以通过JNDI把Spring bean工厂注入EJB中。在某些EJB 3.0应用程序服务器中,厂商可能定义了额外的非标准的API,以注入任意的POJO。其中一个很好的例子是JBoss MicroContainer,它甚至比Spring更普通,因为它处理了面向方面编程(AOP)的依赖性。
结论
尽管Spring和EJB 3.0的目标都是为松散耦合的POJO提供企业级服务,但是它们是使用截然不同的方法来达到这个目标的。在这两个框架组件中都大量地使用了依赖注入(DI)。
使用EJB 3.0的时候,基于标准的方法、注释的大量使用、以及与应用程序服务器的紧密集成形成了强大的厂商无关性和开发者的高效率。使用Spring的时候,一致地使用依赖注入和集中的XML配置文件,允许开发者构造更加灵活的应用程序,并在同一时刻使用多个应用服务。