【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则将自己定义为“分布式面向对象企业级应用的组件架构”。
持久化是企业级应用的关键组件。当然,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的应用通常采用注释的方式,好处是减少了配置以及映射数据毗邻相关实体对象的工作量,从而提高了可视性。
Spring/Hibernate 和 EJB 3.0/JPA在通过ORM实现持久化方面功能相似。
@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;
...
}
|
应用服务器
|
JPA 实现
|
|
JBoss
|
Hibernate
|
|
BEA
|
Kodo (OpenJPA)
|
|
Oracle
|
TopLink
|
|
Glassfish
|
TopLink
|
|
特性
|
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());
}
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;
}
<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>
![]()
|
特性
|
Spring
|
EJB 3.0
|
|
声明式事务
|
√
|
√
|
|
可编程事务
|
√
|
√
|
|
定界
|
AOP
|
会话 bean 方法
|
|
支持的事务类别
|
JDBC, Hibernate, JTA
|
JTA
|
|
分布式事务的支持
|
√ (With JTA)
|
√
|
|
配置
|
XML
|
默认情况下支持事务,通过注释或xml微调
|
|
标准
|
√ (With JTA)
|
√
|
图3通过机票预订流程的各个步骤,阐述了Spring 和 EJB 3.0管理状态的方式。

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


|
SFSB 的替代方案
|
优点
|
缺点
|
|
数据库
|
所有应用服务器共享
|
应用各层中最不灵活的层,如果设置二级缓存,则需复制数据。
|
|
HTTP Session
|
易用
|
必须复制数据,不能感知事务。
|
|
内存
|
所有应用服务器共享,效率高于数据库
|
必须复制数据,不能感知事务。
|
|
特性
|
Spring
|
EJB 3.0
|
|
表示状态的结构
|
原型bean
|
状态会话 Bean
|
|
实例管理
|
单例、原型、请求、会话、全局会话
|
每次生成新实例
|
|
生命周期管理
|
√ (初始化/销毁)
|
√
(创建/销毁、激活、钝化)
|
|
事务感知
|
--
|
√(SessionSynchronization 接口)
|
|
标准化
|
没有
|
√
|