实体 Bean

作者:Reuven M. Lerner

上个月,我们开始了解 Enterprise JavaBeans (EJB),它是 Sun 的 J2EE(Java 2 企业版)服务器端 Web 应用程序标准的中心。虽然 Java 语言和 J2EE 规范都不是开放标准,但越来越多的 Linux 倡导者已开始使用它们来编写服务器端 Web 应用程序。J2EE 似乎是微软 .NET 框架的唯一主流替代方案,这一事实使其对许多人更具吸引力。

Enterprise JavaBeans 分为两种基本类型,称为会话 Bean 和实体 Bean。会话 Bean 通常模拟进程并且没有任何状态,这使我们可以将业务逻辑放在 EJB 中,而不是放在 Servlet 或 JavaServer Pages (JSP) 中。我们上个月设计的计算器对象(允许我们将两个数字相乘)是一个简单示例,它是一个具有单个方法的会话 Bean。

实体 Bean 旨在包含状态,甚至可能是复杂的状态。此状态通常反映关系数据库中行的内容,Bean 管理其自己的对象关系映射(Bean 管理的持久性或 BMP),或者允许 EJB 改为处理此任务(容器管理的持久性或 CMP)。EJB 容器还提供事务,在我们的对象以及数据库中为我们提供原子操作。

本月,我们编写一个简单的实体 Bean,将其连接到数据库,并通过 Java 应用程序中的会话 Bean 访问它。我们将使用开源 JBoss EJB 容器(根据 GNU 较宽松公共通用许可证(又名 LGPL)发布),但该代码应该可以通过少量修改在任何支持 EJB 的 J2EE 服务器上工作。

创建实体 Bean

正如我们上个月所见,编写会话 Bean 实际上意味着编写三个 Java 类

  • Bean 类执行实际工作。

  • 远程接口具有与 Bean 类上的方法匹配的方法,并且是我们 Bean 的代理。

  • Home 接口帮助我们创建 Bean 类的新实例,以及搜索与特定条件匹配的 Bean。

我们需要为实体 Bean 实现所有这三个类。此外,我们通常需要实现第四个“主键”类。虽然本月的示例不需要定义主键类,但为了完整性,我们将这样做。

大多数 EJB 应用程序最终将至少使用一个实体 Bean(用于对数据建模)和一个会话 Bean(用于实现业务逻辑)。鉴于面向对象编程的核心思想是将数据和代码放在一个包中,以这种方式拆分实体 Bean 和会话 Bean 似乎有点奇怪。但是,这种策略似乎总体上有效,并且一旦规范达成一致,就可以相对容易地在多人之间分配工作。

使用 JAWS

J2EE 是一项规范;该规范的实际实现取决于编写服务器的人员。J2EE 应用程序服务器最重要的部分之一是对象关系映射器,它可以透明地将 Java 类转换为关系数据库表的行(反之亦然)。对象关系映射器应尽可能保持不可见,从而使我们能够在不修改 Java 代码的情况下将后端存储从 Oracle 更改为 MySQL。

JBoss 对象关系映射系统称为 JAWS,通常只需要很少的配置。但是,查看 JAWS 配置文件 (standardjaws.xml,位于 JBoss conf/default 目录中) 以了解幕后发生的事情可能是有益的。

standardjaws.xml 顶部的定义设置了整个 JBoss 服务器的参数。通过这种方式,我们指示要使用的数据库;HyperSonic 数据库包含在 JBoss 中,我们将在本月的示例中使用它。

standardjaws.xml 的核心是多个 <type-mapping>(单数)部分,它们将每个 <java-type> 连接到每个数据库的 <jdbc-type> 和 <sql-type>。由于我们的 EJB 不创建表或显式编写 SQL,因此这些值必须准确非常重要。您可以通过修改这些值来提高应用程序的效率或灵活性。但是,请记住,在已将数据插入数据库后修改 JAWS 可能会导致混淆、损坏或错误。

如果您只想开始使用 EJB,那么您根本不需要修改 standardjaws.xml。相反,您需要修改 jboss.jcml,这是一个 XML 文件,用于定义 JBoss 用于系统配置和控制的不同托管 Bean (MBean)。

jboss.jcml 文件包括对 HyperSonic 和 InstantDB 的支持;为了使其与 HyperSonic 配合使用,我必须从 jboss.jcml 中删除对 InstantDB 的任何引用。我通过编辑 jboss.jcml 的“JDBC”部分来完成此操作,从 JdbcProvider MBean 的“Drivers”属性中删除对 org.enhydra.instantdb.jdbc.idbDriver 的提及,并删除了整个 XADataSourceLoader <mbean>,其服务是 XADataSource,服务名称是 InstantDB。

从 jboss.jcml 中删除所有提及 InstantDB 的内容后,启动 JBoss

cd $JBOSS_DIST/bin
sh run.sh
编写我们的实体 Bean

我们的实体 Bean 对一本书进行建模,其中每本书都有标题、作者、出版社和价格。为了简单和节省空间,我们将忽略一本书可能有多位作者或出版社的可能性。我们还将避免规范化数据,这将意味着拥有本身就是实体 Bean 的实例变量。

我们首先实现 BookBean 类,如列表 1 所示 [可在 ftp.linuxjournal.com/pub/lj/listings/issue93/5577.tgz 获取]。BookBean 是容器管理的实体 Bean 的典型简单 Bean 类定义;它为我们要跟踪的数据库中的每一列定义一个字段,包括一个整数“id”字段,该字段充当主键。

我们必须定义 ejbCreate() 方法以匹配 Home 接口上 create() 方法的签名。每次有人在 Home 接口上调用 create() 时,EJB 容器都会使用相同的参数在我们的 Bean 类上调用 ejbCreate()。ejbCreate() 是真正的创建操作发生的地方;虽然 CMP 实体 Bean 不需要担心处理对象关系映射,但它确实需要将其实例变量设置为适当的值。

除了 ejbCreate() 之外,我们必须编写的唯一方法是每个字段的“getter”和“setter”方法,以便其他对象可以检索或修改字段的值。在我们的示例中,每个方法都非常简单,返回或修改实例变量的值。

我们的远程接口(如列表 2 所示)名为 Book.java,其 API 与 Bean 类几乎相同。应用程序通常会与远程接口对话;如果出现问题,它会抛出 RemoteException。

列表 2. 远程接口 Book.java 的源代码—此类镜像了我们的 BookBean 类定义的公共方法。

我们还定义了一个 Home 接口(如列表 3 所示),其中包含一个 create() 方法,该方法在收到书籍的所有详细信息时创建 Book 的新实例(并隐式地在我们的数据库表中创建新行)。如果我们愿意,我们可以为用户提供多个版本的 create(),每个版本都将采用不同数量的参数。

列表 3. 定义 Home 接口

请注意,我们的 create() 方法如何要求我们提供显式主键。经验丰富的数据库程序员知道,主键应该隐藏起来,并且大多数数据库都有自动化此操作的方法;PostgreSQL 的 SERIAL 类型、MySQL 的 AUTO INCREMENT 和 Oracle 的序列是解决此问题的常用方法。不幸的是,在 EJB 中没有简单的方法来使用这种自动生成的主键。因此,我们必须显式设置它(如本月的示例中所示)或使用外部值,例如 ISBN,它可以是 String。这是我在 EJB 中发现的最令人惊讶的事情之一,我希望未来版本的规范能够纠正这种情况。

findBy...() 方法允许我们查找和检索 Book 的实例。调用 findByPrimaryKey(5) 返回主键为 5 的 Book 实例。所有 findBy...() 方法都由 EJB 容器实现,从而使我们不必自己这样做。findAll() 方法返回此类型的所有对象的集合(即数据库表中的所有行),允许您迭代它们。

不幸的是,自动定义的 findAll...() 方法使用简单的相等性检查。我们不能使用正则表达式或其他技术来搜索作者字段以 O 开头的书籍,或者出版社以 A 开头并以 M 结尾的书籍。相反,我们必须使用 findAll(),迭代返回的集合并根据需要进行过滤。

最后,我们的主键类 (BookPK.java),如列表 4 所示,定义了一个充当主键的单个实例变量 (id)。equals() 方法指示 BookPK 的两个实例是否相同,允许系统比较 Book 的两个实例。hashCode() 方法必须为每个实例返回一个唯一值,在本例中可以是 id。toString() 方法必须返回主键的字符串版本,在我们的类中,它只返回 String.valueOf(id)。

列表 4. BookPK.java,我们的实体 Bean 的主键类

由于所有这四个类都在 il.co.lerner.book 包中,因此我将所有四个源文件(Book.java、BookBean.java、BookHome.java 和 BookPK.java)放在目录 $BOOK/il/co/lerner/book 中,其中 BOOK 是此项目的根目录。

部署描述符

现在我们已经定义了实体 Bean,我们需要向 EJB 容器描述它。我们的部署描述符(名为 ejb-jar.xml 的文件)如列表 5 所示。我们将 ejb-jar.xml 放在 $BOOK/il/co/lerner/book/ 中,与将构成我们实体 Bean 的 Java 类放在一起;当我们使用 Ant 构建 Bean 时,它将被放置在名为 META-INF 的子目录中。

列表 5. ejb-jar.xml,我们的实体 Bean 的部署描述符

实体 Bean 的 ejb-jar.xml 中最有趣的部分是 <persistence-type> 部分(对于 CMP 设置为“Container”),<prim-key-field> 和 <prim-key-class> 部分(我们在其中命名主键的类)以及 <cmp-field> 部分(向 JBoss 描述容器管理的字段)。

部署描述符是 EJB 的标准部分,应在所有 EJB 服务器和容器中工作。但是,它并未解决所有运行时配置问题。为了使 JBoss 正常工作,因此我们包含一个名为 jboss.xml 的文件,该文件告诉服务器我们可以在网络上的何处找到 Bean。jboss.xml 的副本(我们将其与 ejb-jar.xml 一起放在 $BOOK/il/co/lerner/book/ 中)如下所示

<?xml version="1.0" encoding="UTF-8"?>
<jboss>
  <enterprise-beans>
    <entity>
      <ejb-name>Book</ejb-name>
      <jndi-name>Book</jndi-name>
    </entity>
  </enterprise-beans>
</jboss>
应用程序

我们的简单测试应用程序 UseBook.java 的源代码如列表 6 所示 [可在 ftp.linuxjournal.com/pub/lj/listings/issue93/5577.tgz 获取],它演示了您可以用很少的代码做多少事情。它仅定义了一个 main() 方法,并立即开始使用 EJB。首先,它获取 JNDI 上下文并查找我们已定义的 Book Bean。这使其可以创建 BookHome 的实例,进而使我们能够创建 Book Bean 的新实例

Book book = home.create(testPrimaryKey, "Book title",
                        "AuthorFirst AuthorLast",
                        "PublisherName", 10.50);

如您所见,我们已经硬编码了将添加到数据库中的值,包括主键。这在现实世界中显然是不可接受的;一个非示例应用程序将从文件、命令行、环境变量、Web 上的 HTML 表单中获取主键(和其他值),或者自动生成它们。

请注意,我们永远不必创建 SQL 表、插入行或检索它们。我们的后端持久存储大概是一个关系数据库,但我们的 Java 客户端既不知道也不关心。

一旦将新行插入表中,UseBook 就会修改其某些值(通过设置实例变量值),然后检索数据库中 Book 的所有实例 (使用 findAll())。在此过程中,它会将状态消息发送到 System.out,这些消息会打印在控制台上。

Ant 配置文件

我们使用 Ant(标准 make 程序的 Java 替代品)来编译和安装我们的程序。列表 7 [可在 ftp.linuxjournal.com/pub/lj/listings/issue93/5577.tgz 获取] 显示了我们的 build.xml 文件,该文件编译了 $BOOK/il/co/lerner/book 中的每个 .java 源文件,将它们与部署描述符和 JBoss 运行时配置文件一起转换为 jar 文件,然后将文件安装到 JBOSS 目录中。如果 Ant 安装在 ANT 中,则可以使用以下命令编译所有文件,将它们安装到 JBoss 的“deploy”目录中,并调用 UseBook.main():

$ANT/bin/ant use-book-ejb

一旦 JBoss 注意到新的(或更新的)jar 文件,您将在屏幕上看到指示服务器正在执行的操作的输出。相比之下,Ant 的输出将显示从 main() 内发送到 System.out 的所有项目,包括列表 6 中的状态消息,这些消息指示了我们所做的事情。每次使用新的 ID 值编译和运行 main() 时,都会将新行插入到数据库表中。

EJB 是未来吗?

Sun 希望您相信 EJB 是所有服务器端 Web 应用程序的未来。当然,微软正在尽最大努力说服您 .NET 才是真正的未来。独立开发者在这场巨头之战中表现如何?自由软件倡导者应该怎么做?

好消息是,J2EE 是一种出色的架构和理念。它既不是最优雅的,也不是最灵活的,并且它确实将您锁定在单一语言中。但总的来说,我对 J2EE 印象深刻,并认为它是 Web 应用程序编程世界中的一个重要里程碑。

不幸的是,每次我使用 J2EE 编写应用程序时,都会出现许多问题。第一个问题是 Java 和 J2EE 都不是开源的,尽管 Java 是免费的,并且 JBoss 是根据 LGPL 许可的。Sun 通常是一个诚实的参与者,但他们是一家具有自身商业利益的商业公司。开源倡导者不应感到惊讶,如果 Sun 限制代码或规范的使用。

此外,Enhydra Enterprise 的退出似乎使 JBoss 成为市场上唯一的开源 J2EE 应用服务器。但是,JBoss 没有获得 J2EE 合规性官方认证,因为 JBoss 团队无法支付官方认证费用。在我看来,这显得 Sun 有些目光短浅;也许他们可以为根据 GPL 或 LGPL 发布的服务器提供免费或廉价的认证,这(与 Berkeley 许可证不同)确保没有任何公司可以将经过认证的服务器变成专有产品。J2EE 官方认证对我来说并不重要,但有许多 CEO 和 CTO 确实需要它,这意味着 JBoss 经常被无缘无故地忽略。

Java 和 EJB 很复杂,程序员需要时间来学习它们。但是,与 Java 和 EJB 编程的复杂性相比,我所经历的是令人困惑的配置文件、新术语、环境变量以及 Java 独有的其他项目。更好的文档肯定会有所帮助,但在我看来,Java 类似于 CPAN 的工具(这将使服务器端 Java 配置更容易)将使程序员能够专注于编程,而不是系统维护问题。

最后,.NET 中真正吸引我的一部分是它对不同编程语言的相对开放性。根据定义,J2EE 需要 Java,这通常是正确的选择,但不应是唯一的选择。SOAP 和 XML-RPC 使弥合语言之间的差距成为可能,但没有 EJB 为表带来的良好事务处理和对象关系映射。目前,Python 与 EJB 通信的唯一方法似乎是通过 SOAP 或 XML-RPC(或 Jython),但我希望将来看到其他可能性。

结论

EJB 是一项令人印象深刻的技术,它比简单的对象关系映射器 Alzabo 和 DODS 做得更多。根据我的经验,使用 EJB 更多的是管理和后勤方面的难题,而不是技术难题。对于所有 Web 应用程序开发人员来说,学习 EJB 都是一个好主意;很明显,此标准正在行业中取得重大进展,并且将来将使用 EJB 构建许多重要的应用程序。拥有经过认证的开源实现将使程序员更容易尝试 EJB,我鼓励 Sun 尽快朝着这个方向发展。

下个月,我们将转换方向,开始研究 Zope,这是一种主要用 Python 编写的非常不同的 Web 应用程序框架。Zope 在过去几年中变得非常流行,并且通常被视为将 Python 推向编程语言前沿的杀手级应用。我们将了解 Zope,并开始研究如何使用它来编写我们自己的应用程序。

资源

Reuven M. Lerner 拥有一家小型咨询公司,专门从事 Web 和互联网技术。他与妻子 Shira 和女儿 Atara Margalit 一起住在以色列莫迪因。您可以通过 reuven@lerner.co.il 或 ATF 主页 www.lerner.co.il/atf 与他联系。

加载 Disqus 评论