【IT168 技术文章】
EJB 历史回顾
勿庸置疑,1997 年推出的 IBM® 原始 EJB 规范是 Java 技术领域最重要的开发成果之一。EJB 和包含 EJB 的 J2EE 应用服务器迅速地应用于企业开发。然而,对 EJB 的批评之声也正如 J2EE 的采用一样快速涌至。在这些批评之中,最主要的抱怨是 EJB 难于理解且开发起来繁琐乏味。
EJB 规范旨在解决一些复杂问题,比如分布式计算、事务管理和数据持久性。复杂问题通常会产生复杂的解决方案。原始 EJB 规范反映了其旨在解决的复杂问题。结果,开发人员在使用 EJB 时体验了很多痛苦的经历。当您了解 EJB 后,可以使用一些复杂的工具简化开发过程。但对于 EJB 新手而言仍然存在一个很重要的学习过程。
EJB 1.0 和 1.1 规范由 Sun Microsystems 公司开发和发布。后续所有的新规范都使用 Java Specification Requests(JSR)创建并使用 Java Community Process(JCP)认可。Java 社区的参与是促使 Java 技术演化的关键因素。JCP 中反映了 EJB 开发人员的痛苦。结果导致了 EJB 3.0 规范的诞生,其最后定案时间为 2006 年 5 月。
EJB 再也不是仅限于 “解决” 复杂问题,而是 “通过简单便捷的开发” 解决这些问题。EJB 3.0 重点关注的是使初始开发更加简单、使应用程序更易于维护。在本文中,您将了解到 EJB 3.0 相对 EJB 2.1 所带来的简化。首先,我们来考察一下 OpenEJB,它是一个 EJB 实现,也是 Geronimo 的基础之一。
OpenEJB —— EJB 1.1
OpenEJB 问世于 2000 年,其创建者是 David Blevins 和 Richard Monson-Haefel。Blevins 也是 Geronimo 的创建者之一,而 OpenEJB 是 Geronimo 中 EJB 实现的首选。OpenEJB 是 EJB 1.1 规范的第一批开源实现之一。它直接提供了一个远程会话 bean 的实现,并使用 Castor 作为它的容器管理持久性(CMP)实体 bean 的实现。在最开始时,OpenEJB 主要关注的是简化开发人员的工作。例如,OpenEJB 允许使用嵌入式容器(甚至是嵌入式数据库)以简化 EJB 单元测试的编写。大多数 EJB 实现对于 EJB 单元测试的处理都很繁琐,因而开发了一些复杂的框架以处理这个问题(JUnitEE 就是一个最著名的框架)。OpenEJB 是 EJB 1.1 的一个绝好的实现 —— 处理速度快而且用户界面友好。但是,它也需要做些自身改进以满足 Geronimo 的需要。
OpenEJB 和 Geronimo —— EJB 2.1
Geronimo 是 J2EE 1.4 规范的一个开源实现。此规范包含了 EJB 2.1 规范。OpenEJB 是一个 EJB 1.1 实现。EJB 2.1 相对于 1.1 增加了很多内容。这些内容包括:为会话 bean 和消息驱动 bean(MDBs)提供了本地接口、在实体 bean 查找器方法中加入了查询语言并支持将会话 bean 公开为 Web 服务。
所幸的是,Geronimo 团队(包括很多 OpenEJB 贡献者)是一个杰出的团队,他们致力于为 OpenEJB 实现所有的 EJB 2.1 特性,从而为 Geronimo 实现 J2EE 1.4 铺平了道路。在此期间,OpenEJB 接收了 EJB 规范的大量复杂内容。例如,可以很好地用于单元测试的可嵌入容器在寻求与 EJB 2.1 兼容时会带来间接的损害。对于所有的 Java 开发人员而言,所幸的是 EJB 3 即将出现。
EJB 3.0
那么 EJB 3.0 有什么惊人之处?了解 EJB 3.0 的最简单方法就是与 EJB 2.1 比较。我们分别使用二者构建一个简单的 EJB。在下面的章节中,我们将构建一个简单的无状态会话 bean,为您提供当前时间。首先,考察使用 EJB 2.1 时的操作方法。
Time EJB —— 近似 EJB 2.1
最好考察一下 EJB 中的复杂性,它们给有经验的开发人员和 EJB 的初学者都带来了很多痛苦。使用清单 1 中所示的简单接口可以描述一个假想的 EJB。
清单 1. 简单的 Time 接口
2
3 import java.util.Date;
4
5 public interface Time {
6 public Date currentTime();
7 }
8
现在将此接口打造成一个 EJB。首先需要重新定义接口,如清单 2 所示。
清单 2. Time EJB 接口
2
3 import java.rmi.RemoteException;
4 import java.util.Date;
5
6 import javax.ejb.EJBObject;
7
8 public interface Time extends EJBObject{
9 public Date currentTime() throws RemoteException;
10 }
11
非常相似,但是需要从根本上改变接口仍然会让人不安。扩展 EJBObject 好像并不坏,因为它只是一个典型的标记接口。最大的麻烦就是需要更改方法签名以声明抛出一个 RemoteException。EJB 2.0 通过添加一个可选的本地接口解决此问题,如清单 3 所示。
清单 3. Time 的本地接口
2
3 import java.util.Date;
4
5 import javax.ejb.EJBLocalObject;
6
7 public interface TimeLocal extends EJBLocalObject{
8 public Date currentTime();
9 }
10
不错,现在没有 RemoteException 了。当然,您仍然创建了两个接口描述同一个服务。您不能让远程接口扩展本地接口(这也许听起来很有道理),因为前者的 currentTime 方法抛出了一个 RemoteException 而后者则没有,原因诸如此类。接口声明似乎有些突兀,但其影响较下面将介绍到的实现内容而言还算轻微。
您可以想像原始接口的一个简单实现,如清单 4 所示。
清单 4. 原始接口的实现
2
3 import java.util.Date;
4
5 public class TimeBean implements Time {
6
7 public Date currentTime() {
8 return new Date();
9 }
10
11 public TimeBean() {
12 }
13 }
14
这对于 EJB 2.1 而言太过简单。清单 5 中给出的是具有相同功能的 EJB 2.1 版本。
清单 5. TimeBean EJB 实现
2
3 import java.rmi.RemoteException;
4 import java.util.Date;
5
6 import javax.ejb.EJBException;
7 import javax.ejb.SessionBean;
8 import javax.ejb.SessionContext;
9
10 public class TimeBean implements SessionBean {
11
12 public void ejbCreate() {
13 }
14
15 public Date currentTime() {
16 return new Date();
17 }
18
19 public void ejbActivate() throws EJBException, RemoteException {
20 }
21
22 public void ejbPassivate() throws EJBException, RemoteException {
23 }
24
25 public void ejbRemove() throws EJBException, RemoteException {
26 }
27
28 public void setSessionContext(SessionContext sessionContext) throws EJBException,
29 RemoteException {
30 }
31
32 public TimeBean() {
33 }
34 }
35
所有这些 ejbXYZ 方法是从哪儿来的?它们都是由 SessionBean 接口托管。它们是生命周期回调方法。在一些复杂场景中,当复杂会话 bean 的生命周期中需要执行大量的处理和簿记时,这些方法有很大的用处。但是,它是一个典型的边角条件(或最坏场景)编程。
EJB 2.1 的任务还未完成。还需要创建 Home 和 LocalHome 接口。开发人员必须提供这些接口给容器使用。例如,清单 6 给出了一个 Home 接口。
清单 6. Time Home 接口
2
3 import java.rmi.RemoteException;
4
5 import javax.ejb.CreateException;
6 import javax.ejb.EJBHome;
7
8
9 public interface TimeHome extends EJBHome{
10 public static final String COMP_NAME="java:comp/env/ejb/Time";
11 public static final String JNDI_NAME="Time";
12
13 public Time create() throws CreateException,RemoteException;
14
15 }
16
LocalHome 接口几乎完全相同。它扩展了 EJBLocalHome 而不是 EJBHome,并且其 Create 方法没有抛出 RemoteException。
还有更麻烦的,您还需要为 EJB 编写部署描述符。部署描述符将所有这些连接起来并进一步指示容器如何管理 EJB。清单 7 给出了示例 bean 的一个可能的部署描述符。
清单 7. 部署描述符
2
3 <ejb-jar id="ejb-jar_1" xmlns="http://java.sun.com/xml/ns/j2ee"
4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/
6 j2ee/ejb-jar_2_1.xsd" version="2.1">
7
8 <display-name>Time</display-name>
9
10 <enterprise-beans>
11
12 <!-- Session Beans -->
13 <session id="Session_Time">
14 <display-name>Time</display-name>
15 <ejb-name>Time</ejb-name>
16 <home>org.developerworks.time.TimeHome</home>
17 <remote>org.developerworks.time.Time</remote>
18 <local-home>org.developerworks.time.TimeLocalHome</local-home>
19 <local>org.developerworks.time.TimeLocal</local>
20 <ejb-class>org.developerworks.time.TimeSession</ejb-class>
21 <session-type>Stateless</session-type>
22 <transaction-type>Container</transaction-type>
23 </session>
24 </ejb-jar>
25
希望您现在已经很好地了解了 EJB 所带来的痛苦。EJB 3.0 是如何解决所有这些问题的呢?我们来看一下。
Time EJB —— 近似 EJB 3.0
我们使用 EJB 3.0 重新编写 Time EJB。跟前面一样,首先编写一个描述服务的接口。只是这次对接口执行较少的操作即可使之成为 EJB 接口。请观察一下清单 8。
清单 8. TimeBean 接口
2
3 import java.util.Date;
4
5 import javax.ejb.Remote;
6
7 @Remote
8 public interface Time {
9 public Date currentTime();
10 }
11
注意,它和 清单 1 何其相似。唯一添加的是一个注释,将其声明为远程接口。您无需扩展接口或抛出 RemoteException。EJB 3.0 规范允许您省略所有的注释,前提是对实现类进行正确的注释并且只实现一个接口。保留这些注释仍然是个不错的做法;可以使代码更易于维护。接下来看看接口的实现,如清单 9 所示。
清单 9. TimeBean EJB 实现
2
3 import java.util.Date;
4
5 import javax.ejb.Stateless;
6
7 @Stateless
8 public class TimeBean implements Time{
9
10 public Date currentTime(){
11 return new Date();
12 }
13 }
14
此类 EJB 通常称为普通旧式 Java 对象(POJO)。原因在于唯一能够证明它是 EJB 的是 @Stateless 注释。接口中再也没有实现回调函数。如果需要实现一个回调函数,只需添加一个方法并通过注释将其声明为回调函数即可。您可以随时调用该方法,因为不用实现接口。
现在来看看部署描述符。等等,还没有部署描述符!如果您先前没有了解到 EJB 3.0(可以推知 Java EE 5)的重要性,希望您现在可以有一个了解。对于简化开发而言这算得上是一个跨越。当然,其本质只是一个规范。您需要一个实现来使用这些较好的思想。Geronimo 2.0 和 OpenEJB 3.0 就应运而生了。
Geronimo 2 —— Java EE 5
您仅仅了解了一些表面信息,但是您可能知道 Java EE 5 与 J2EE 1.4 差别巨大。它们具有很多相同的概念。应用服务器所提供的服务也相同,比如分布式计算、事务管理和持久性。但是,这些服务的 API 却完全不同。EJB 的开发简化了很多,但是这就意味着 Java EE 5 应用服务器必须提供比以前更多的功能。因此,Geronimo 开发人员在实现 Java EE 5 规范时需要做大量的工作。对于 OpenEJB 而言这种情况表现得尤为明显,因为再次需要依靠它作为 Geronimo 的基础,而且它实现了这个全新的 EJB 3.0 规范。
在撰写本文时,Apache Geronimo 已经通过了 Java EE 5.0 TCK。开发人员终于尝到了胜利的果实 —— 他们可以享受使用新的 EJB 3 API 和将其更加整洁简单的代码部署到 Geronimo 上的乐趣。
孵化器状态
另外一个值得注意的有趣现象是,OpenEJB 项目现在已经成为一个 Apache Incubator 项目。Apache Geronimo 团队为 OpenEJB 的 EJB 2.1 实现作出了很大的贡献,因为后者是 Geronimo 的 J2EE 1.4 实现的关键。但是,OpenEJB 还没有准备好加入到 Apache 中。通常还需要一些文书工作,现在 OpenEJB 已成为 Incubator 的一部分。EJB 3.0 正作为一个 Apache 项目在实现,而它的成功实现将为帮助 OpenEJB 成为一个优异的 Apache 项目跨进一大步。
结束语
本文介绍了 J2EE 和 EJB 规范多年以来的发展历程。您不仅见识了该规范的强大功能,还了解了由此导致的复杂编程模型给 Java 编程人员所带来的痛苦。您已经体验到了 Java EE 5 和 EJB 3.0 规范如何旨在解决这些痛苦并极大地提高 Java 开发人员的生产率。它是一个非常简化的编程模型,不仅可以立刻提高生产率,而且所开发的应用程序更加易于维护。
Geronimo 的第一个版本实现了 J2EE 1.4 规范。OpenEJB 提供的 EJB 2.1 规范是 J2EE 1.4 的一部分。现在,Geronimo 正致力于实现 Java EE 5 规范,而 OpenEJB 则需要实现 EJB 3.0 规范。这些规范的实现已经取得了很大进展;鼓励您下载 Geronimo 的最新版本开始享受 Java EE 5 和 EJB 3.0 规范带来的好处。