技术开发 频道

Spring PK EJB 3.0,我们该选择哪一个?



【IT168 专稿】在Spring 和 EJB 3.0 两种主流技术之间作出选择是非常艰难的。本文探讨了两者间的异同,并阐述了进行技术比较的关键环节。

一、简介

    如今的软件开发人员面临的各种技术选择令人眼花缭乱。涉及到具体项目案例时,很难说哪一种技术更优。在基于java平台的企业开发领域,针对Spring框架和EJB 3.0之间的辩论异常激烈。众说纷纭,莫衷一是。本文尽量从客观的视角比较两者的优劣。 
  
   
Spring 和 EJB 在概念上属于两个不同的范畴,Spring是一种技术实现,EJB则是技术规范。但两者确实存在交叉重叠,比如他们都提供了为java应用递送中间件服务的机制。Spring框架的推出正是针对EJB的,两者之间自然就存在着可比性。随着EJB最新版本3.0的发布,对于先前版本的缺欠做出了很大改进。那么Spring如何能持续地提供超越EJB的特性呢?

本文撷取了企业级应用开发环境内,Spring和EJB 3各自最重要的特性。文中以机票预定系统为案例阐述每个特性的要旨,涉及业务领域模型以及企业级应用开发中的并发控制、事务处理、消息处理、任务调度等范畴。

定义  

在深入比较分析之前,先看一下Spring 和EJB各自声称要实现的是什么。在Spring的网站上,Spring被定义为“基于Java/J2EE分层应用框架”。而EJB则将自己定义为“分布式面向对象企业级应用的组件架构”。

    首先,从定义可看出,二者都将自己的产品定位在企业级Java开发的领域。Spring进一步把自己声明为应用框架,而EJB则是声明为一种架构。架构的含义很广,一般来说指的是软件系统不轻易改变的重要策略或者软件结构。框架意味着技术支撑而架构强调的是策略。以上是Spring和EJB之间的细微差别。值得注意的是EJB推出了分布式组件开发的解决方案而Spring则没有提供类似的工具。

    持久化是企业级应用的关键组件。当然,Spring和EJB对于持久化都提供了强大的支持。Spring的设计策略是在产品中集成主流的持久化框架如JDBC、Hibernate、JDO、iBatis等。而在最新的EJB3.0规范中,实体bean被JPA(JAVA持久化API)取代;相应地在Spring 2.0中,也增加了对于JPA的支持。JPA旨在提供一个轻量级的对象-关系映射框架。在EJB 3.0中专门定义了操作持久化服务的接口和映射实体对象到关系数据库的方法。

持久化——功能对比

图一. 航班预订模型。模型涉及了对象-关系映射的诸多概念:组合,双向和三向关联等。

如下的单元测试用来验证机票对象生成、与航班关联、分配座位、最后结果存入数据库。

public void testSave() { Ticket ticket = TicketMother.create(); Flight outboundFlight = flightDAO.findById(1); Flight returnFlight = flightDAO.findById(2); ticket.setOutboundFlight(outboundFlight); ticket.setReturnFlight(returnFlight); ticket.getPassenger().setFlightDetails(outboundFlight, new PassengerFlightDetails(getSeatForFlight(outboundFlight, "2A"))); ticket.getPassenger().setFlightDetails(returnFlight, new PassengerFlightDetails(getSeatForFlight(returnFlight, "2B"))); ticketDAO.save(ticket); ticket = ticketDAO.findById(ticket.getId()); assertEquals("John", ticket.getPassenger().getFirstName()); assertEquals("2A", ticket.getPassenger().getDetailsForFlight (ticket.getOutboundFlight()) .getSeat().getNumber()); }

 

 

 

以上测试在Spring and EJB 3.0环境下都成功通过。这说明Spring 和 EJB 3.0在基本的ORM持久化方面功能相当,并且其实现方式非常相似。Hibernate是最流行的对象关系映射工具,以下是Spring/Hibernate组合的TicketDAO类的实现:

public class TicketSpringDAO extends HibernateDaoSupport implements TicketDAO { public Ticket save(Ticket ticket) { getHibernateTemplate().saveOrUpdate(ticket); return ticket; } ... } EJB 3.0的实现如下: @Stateless public class TicketEJBDAO implements TicketDAO { @PersistenceContext(unitName = "flightdb") private EntityManager em; public Ticket save(Ticket ticket) { em.persist(ticket); return ticket; } ... }

 

 

显然,从实现的角度来看,两个产品非常相似。Spring设计了HibernateDaoSupport类,将Spring与Hibernate间的交互操作抽象出来,通过HibernateDaoSupport类可访问Hibernate的诸多特性,以及Spring的Hibernate模版。而在EJB 3.0中,则是凭借容器来注入实体对象管理器。

Hibernate的Session和JPA的实体对象管理器功用相当,但其间细微的差别值得重视。Hibernate的Session是实体对象的缓存同时也是对象-关系映射引擎的接口。而在JPA中,持久化场景承担了缓存的角色,实体管理器则承担接口的角色。

在映射方式方,基于Spring/Hibernate的应用以XML映射文件的方式实现。

<hibernate-mapping package="org.jug.flight.domain" default-lazy="false"> <class name="Ticket"> <id name="id" column="id"> <generator class="native" /> </id> <property name="status" type="Status" /> <many-to-one name="outboundFlight" column="outbound_flight" /> <many-to-one name="returnFlight" column="return_flight" /> <many-to-one name="passenger" column="passenger_id" cascade="all" /> </class> </hibernate-mapping>

 

 

基于JPA的应用通常采用注释的方式,好处是减少了配置以及映射数据毗邻相关实体对象的工作量,从而提高了可视性。 

@Entity public class Ticket implements Serializable { @Id @GeneratedValue private long id; private Status status; private double price; @ManyToOne(cascade = CascadeType.PERSIST) private Passenger passenger; private Flight outboundFlight; private Flight returnFlight; ... }

     Spring/Hibernate 和 EJB 3.0/JPA在通过ORM实现持久化方面功能相似。



持久化——非功能性需求
 
表1. 支持JPA的应用服务器以及 JPA实现途径
应用服务器
JPA 实现
JBoss  
Hibernate
BEA        
Kodo (OpenJPA)
Oracle     
TopLink
Glassfish  
TopLink
 
 
从表中可看出,Spring和EJB都有可选的持久化服务提供程序。如果要在应用中使用超出JPA规范的某种产品的专有特性,可以在EJB应用中实现,但这是以牺牲跨平台兼容性为代价的。
 
ORM另一个重要的概念是缓存。若使某单元内的各个组件协同工作,必须使缓存在各组件间得以共享。基于Spring的应用一般通过线程内局部变量将session(以及事务)和线程绑定起来。尽管Spring内置了处理缓存的一系列类,仍需开发人员处理具体的环节。而EJB 3.0则在事务对象上自动生成持久化场景。对象模型通常很复杂,如果每当持久化实体对象从数据库中提取时都自动生成其所有的相关关系,那么处理效率会非常低。针对这一点,ORM工具提供了延迟初始化机制。这种机制只有在数据确实需要时,才执行相关的数据库操作。这样确实避免了不必要且昂贵的数据库访问操作,但也使应用逻辑变得复杂化。需要注意的是,当试图访问一个延迟初始化的对象集合而其缓存已经关闭时,则就会抛出异常。
 
Spring/Hibernate组合的处理方式是在视图生成前打开缓存,生成后立即将缓存关闭。Spring 中的OpenSessionInViewFilter类(和拦截器)用于简化以上操作,在使用前需要配置。EJB 3.0规范在持久化场景中增加了有趣的范畴,允许缓存生存周期与状态会话 bean的生命周期进行绑定。这样可简化对于长周期应用事务和延迟加载集合对象的处理。
 
持久化——小结
JPA支持可插入式的持久化提供程序,Spring也支持同样的程序。表二列出了两者主要的特性对比。
 
表2. Spring 和 EJB 3.0的持久化机制对比
  特性 
Spring 
EJB 3.0
Simple ORM Persistence
简单对象-关系映射
√
√
Implementation
实现
Hibernate,JPA,JDO, TopLink, iBatis
JPA (提供程序包括 Hibernate, Kodo and Toplink)
是否支持JDBC
 
√
--
 
映射
XML, 注释
注释, XML
Cache Propagation
缓存生成
本地线程
事务
扩展缓存范围
在视图中打开session
在扩展持久化场景中
 
标准化
√(是否使用JPA)
√
 


    以下以机票预订系统为例,阐述Spring和EJB 3.0提供的事务处理功能。



图二. 购票流程序列图。如果中间任一步骤失败,应确保系统状态的一致性。
 
如下的单元测试验证购票操作流程以事务方式运作:
 
 
public void testPurchaseTicket_DebitFailure() throws Exception { // 创建机票对象 Ticket ticket = createTicket(); // 保存数据库中机票数量 int count = ticketDAO.findAll().size(); // 设置信用卡授权 setupMockAuthorizer(true); try { bookingAgent.purchaseTicket(ticket, true); fail("System failure was not thrown as was intended"); } catch (InsufficientFundsException e) { // 纠正异常行为 } // 验证事务回滚 assertEquals(count, ticketDAO.findAll().size()); }
此测试在Spring 和 EJB 3.0中均能通过。下面是Spring的实例类:
 
public Ticket purchaseTicket(Ticket ticket) { creditAuthorizer.authorize(ticket.getPrice(), null); ticket.setStatus(Status.PURCHASED); ticketDAO.save(ticket); creditAuthorizer.debit(ticket.getPrice()); return ticket; } 以下是EJB 3.0的实例类 public Ticket purchaseTicket(Ticket ticket) { creditAuthorizer.authorize(ticket.getPrice(), null); ticket.setStatus(Status.PURCHASED); ticketDAO.save(ticket); creditAuthorizer.debit(ticket.getPrice()); return ticket; }
Spring和 EJB 3.0在处理方式上都不必直接和事务管理细节打交道。这说明了声明式编程的优势。事务管理可以模块化的方式在整个应用范围内重用,以避免业务逻辑混乱,从而使开发人员可将主要精力用在处理业务问题方面。


事务管理的输入层面包括定界、传播、隔离。将一组操作与事务作语义绑定称为定界。如果需多个组件在一组操作范围协作,其事务也应在这些组件间共享。隔离允许开发人员细致地调整事务间的隔离程度。
 
在Spring中,事务定界、传播和隔离均通过AOP工具来声明。在xml配置文件中可定义事务代理对象,或通过面向方面的语义来实现。事务代理的操作方法如下:
 
<bean id="bookingAgent" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager" ref="transactionManager" /> <property name="target" ref="bookingAgentSpring" /> <property name="transactionAttributes"> <props> <prop key="*">PROPAGATION_REQUIRED</prop> </props> </property> </bean> <bean id="bookingAgentSpring" class="org.jug.flight.booking.spring.BookingAgentSpring"> <property name="flightDAO" ref="flightDAO" /> <property name="ticketDAO" ref="ticketDAO" /> <property name="screener" ref="screener" /> </bean>
而EJB 3.0 中,所有会话 bean的公共方法自动应用了事务语义,不再需要专门的配置文件。并且通过注释或xml可微调事务传播级别。
 
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Ticket purchaseTicket(Ticket ticket)
{
 ...
    }
 
EJB 3.0规范将事务隔离声明为针对资源管理,没有标准的表述方式。Spring集成了各种事务管理API 如JDBC, Hibernate, and JTA。而EJB 3.0仅支持JTA。
 
对于API的选择主要有两个要点:首先,并非所有的Java容器支持JTA(如Tomcat就不支持),尽管所有的EJB容器支持JTA。其次,如果某一分布式的事务操作涉及各种资源(JDBC , JMS),则JTA开发环境的支持是必须的。如果容器本身不提供JTA的支持,而又必须JTA的时候,可考虑采用开源的JTA实现如JOTM。
 
事务管理——小结
 
Spring 和 EJB 3.0均提供了与各自持久化机制相兼容针对事务处理的集成管理方式。(见表3)
 
表3. Spring和EJB 3.0事务管理功能性比较
特性 
Spring 
EJB 3.0
声明式事务
√
√
可编程事务
√
√
定界
AOP
会话 bean 方法
支持的事务类别
JDBC, Hibernate, JTA
JTA
分布式事务的支持
√ (With JTA)
√
配置
XML
默认情况下支持事务,通过注释或xml微调
标准
√ (With JTA)
√


EJB对于应用程序的状态参数所扮演的重要角色给予充分关注,提供了较高级别的结构——SFSB(状态会话 bean),专门用来管理状态会话。在应用组件中采用SFSB模式之后,对于组件的调用将自动共享同一个bean实例以及相应的状态。SFSB在设计上充分考虑了伸缩性和可靠性,具体的实现方式由厂商决定。Spring没有和SFSB直接对应的结构,而是通过其它多种技术途径的组合来达到同样的目标。
 
状态管理——功能性比较

    图3通过机票预订流程的各个步骤,阐述了Spring 和 EJB 3.0管理状态的方式。


    图3. 机票预订序列图。旅客通过一系列与订票代理的交互过程进行订票操作,每个步骤都会在会话场景产生相应的状态。
 
public void testPurchaseTicket_UseCase() throws Exception { //开始购票会话 FlightCriteria flightCriteria = new FlightCriteria("DFW", "AUS", DateUtilities.parseDate("2006-01-15")); bookingAgent.bookRoundTripTicket(flightCriteria); //选择离港航班 List outboundFlights = bookingAgent.listOutboundFlights(); assertEquals(3, outboundFlights.size()); bookingAgent.selectOutboundFlight(selectFlight(outboundFlights, 1)); //选择返程航班 List<Flight> returnFlights = bookingAgent.listReturnFlights(); assertEquals(3, returnFlights.size()); bookingAgent.selectReturnFlight(selectFlight(returnFlights, 2)); //选择旅客 Passenger passenger = new Passenger("Rod", "Coffin"); bookingAgent.selectPassenger(passenger); //选择座位 bookingAgent.selectOutboundSeat(selectSeat (bookingAgent.retrieveCurrentTicket().getOutboundFlight(), "1B")); bookingAgent.selectReturnSeat(selectSeat (bookingAgent.retrieveCurrentTicket().getReturnFlight(), "1A")); //购票 Ticket ticket = bookingAgent.purchaseTicket(); assertTrue(ticket.getId() != 0); } 以下是基于Spring的实现: public class BookingAgentSpring implements BookingAgent { private FlightCriteria outboundCriteria; private FlightCriteria returnCriteria; private Ticket ticket; public Ticket bookRoundTripTicket(FlightCriteria flightCriteria) { outboundCriteria = flightCriteria; returnCriteria = reverseCriteria(flightCriteria); ticket = new Ticket(); ticketDAO.save(ticket); return ticket; } ... }
以下的单元测试验证订票代理对象以有状态的方式交互。
 
  
为了在Spring中实现bean的状态管理,在bean的调用对象之间不能共享bean实例。因此,状态bean的作用域的属性配置为“原型”级别,这意味着要从bean工厂每次生成一个新的bean实例。
 
<bean id="bookingAgentSpring" scope=”prototype”
    class="org.jug.flight.booking.spring.BookingAgentSpring">
    ...
</bean>
 
而EJB 3.0的处理方式如下:
 
@Stateful public class BookingAgentEJB implements BookingAgent { private FlightCriteria outboundCriteria; private FlightCriteria returnCriteria; private Ticket ticket; public Ticket bookRoundTripTicket(FlightCriteria flightCriteria) { outboundCriteria = flightCriteria; returnCriteria = reverseCriteria(flightCriteria); ticket = new Ticket(); ticketDAO.save(ticket); return ticket; } ... }
 
从上例可见,Spring 和 EJB 3.0在保存状态的具体处理方式上差异较大。


以上实例演示了Spring 和 EJB 3.0如何实现有状态的对象。尽管在功能方面相近,处理方式上则有明显的差异。



                                                    图4. 客户端的状态管理。Spring将状态组件存储在由web容器管理的HttpSession内。



图5. EJB的状态管理。客户端通过代理对象与EJB组件交互。这样组件的状态可由EJB容器管理。
 
EJB采用SFSB的方式有几个优点。第一,在设计上允许SFSB在内存和持久性存储之间交换,使应用服务器可以平衡资源分配以改善性能。第二:SFSB运行在单独的进程内,应用系统的各层可以独立地配置和维护。第三,SFSB可以实现SessionSynchronization接口,这样事务回滚事件发生时,可以返回到bean的初始状态。
 
对于SFSB的批评一般集中在:与无状态的架构相比,性能和伸缩性不够好。针对这一点,Spring框架设计为以无状态为主。对于很多应用来说,状态是个重要指标。通常保存状态的几种方式是:数据库,HTTP Session,或内存。在上例中,基于Spring的购票代理可采用任一种方式管理状态,见表4。
 
表4. Spring中状态会话 Bean的替代方案
 SFSB 的替代方案
优点
缺点
数据库
所有应用服务器共享
应用各层中最不灵活的层,如果设置二级缓存,则需复制数据。
HTTP Session
易用
必须复制数据,不能感知事务。
内存
所有应用服务器共享,效率高于数据库
必须复制数据,不能感知事务。
 
 
对于SFSB伸缩性问题的指责同样适用于表中的各方案,另外还需要重新开发SFSB已经具备的功能。在确定状态管理模式时,这些因素需要综合加以考虑。
 
Spring和EJB 3.0均提供了组件扩展点,使开发人员可将组件绑定到托管bean的生命周期。Spring提供了InitializingBean 和 DisposableBean接口,在bean初始化后和销毁前得到通知。EJB组件在创建后、销毁和钝化前、激活后,可通过设置获得通知。
 
最后,将bean的会话状态和事务同步是另一个要重点解决的问题。默认情况下,在事务回滚时,所有和事务相关的资源也相应回滚(如数据库和消息系统)。如果bean是有状态的,就会产生bean的状态同持久性数据存储不一致的问题。

EJB 3.0中SFSB基于容器管理的事务管理方式可实现SessionSynchronization接口以获取事务通知。基于Spring的方案则需以定制的方式实现相应的功能。
 
状态管理——小结
 
Spring 和EJB 3.0 都支持应用的状态管理机制。EJB 3.0采用SFSB实现状态管理,而Spring通过原型bean达到同一目标,这时性能和伸缩性是主要的考虑因素。两者的比较见图5。
 
图5. Spring 和 EJB 3.0.状态处理的比较
特性
Spring 
EJB 3.0
表示状态的结构
原型bean
状态会话 Bean
实例管理
单例、原型、请求、会话、全局会话
每次生成新实例
生命周期管理
√ (初始化/销毁)
√
(创建/销毁、激活、钝化)
事务感知
--
√(SessionSynchronization 接口)
标准化
没有
√
 
五、结论
本文对于Spring应用框架和EJB 3.0规范在持久化、事务处理、状态管理等方面做了技术上的比较。两者有很多相近之处,其主要的差异体现在:状态管理模式、配置方式、扩展性。如果要开发的应用在状态管理方面需求很高,应优先考虑EJB 3.0 SFSB的方案。如果应用中交互会话强度较高,SEAM(基于SFSBs 和 JSF)是好的选择。
 
Spring在应用开发的很多方面提供了比EJB更灵活的选择,这一点在持久化和事务管理提供程序方面尤为显著。另一方面,也无疑增加了配置的复杂性。EJB 3.0虽欠灵活,但以其紧凑的技术架构、基于注释的配置方式、异常处理的配置方案等特性简化了应用开发过程。
 
标准化问题:Spring框架中集成了很多行业标准,比如JTA, JDBC, and JMS等,但Spring还不是一个java标准。
 
非常幸运的是,Spring和EJB 3.0之间并不是相互排斥的。可以将两种技术结合起来以取长补短。
0
相关文章