IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope:Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Java technology | Web development  >

编写自己的 secret Santa Web 应用程序,第 1 部分: bean

对工具、技术、设计和实现的分步指导

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Merlin Hughes (merlin@merlin.org), 密码专家, Betrusted, Inc.

2003 年 12 月 22 日

在圣诞来临之际,圣诞老人的助手 Merlin Hughes ―― 他在现实生活中的另一身份是Java 开发人员 ―― 展示了一个基于 J2EE 的 secretSantaWeb 应用程序的设计和实现,并讨论了可以用来为这种应用程序的开发提供方便的工具和技术。本文(还有 第2 部分第 3 部分)提供了对如何用一些最新的工具和框架从头开始建立 J2EE 应用程序的一个全面综述,并详细说明了如何让这些不同的技术共同工作以得到最终结果。但是这几篇文章并不准备对每一个技术作详细分析,而是要为用J2EE 开发一个 Web 应用程序提供一个指南。第一篇文章侧重于 bean、它们的设计和实现,以及用 XDoclet 加速它们的开发和部署。

应用程序概述

对于没有经验的人(就像不久前的我)来说, secret Santa是大家庭中圣诞礼物过多问题的一个解决方案。不是让家庭中的每位成员给家里的所有其他人都送一件礼物,而是每个人从帽子中挑一个名字,并匿名送给这个接受者一件礼物。因此每个人都只送出并接受一件礼物。

本文展示了 secret Santa 的 J2EE 实现。secret Santa 应用程序的生命周期如下:

  1. 一开始,某个人注册到 secret Santa 应用程序。他收到一封自动生成的电子邮件作为响应,其中包含了使他可以继续这个过程的密码。这个电子邮件认证对于滥用系统提供了一定的保护。
  2. 然后该注册者为 secret Santa 过程提供准则、所有家庭成员的名字和电子邮箱,以及他们的父母(不会指定成员给他或者她的父母送礼物)。
  3. 然后激活 secret Santa,向每位成员发送一封包含注册信息的电子邮件。
  4. 每位成员再注册到系统上,提供一个代码数(一个 0 到 9 之间的数),并提出自己希望得到什么样的礼物以及建议送给其他组成员什么样的礼物。系统使用的随机数生成器用这个代码数作为种子:当对结果的合法性出现争议时,每位成员可以拿出他或者她的代码数,从而可以模拟系统重新生成结果。
  5. 当所有成员都注册后,就向最初的注册者发送一封电子邮件。之后他就运行 secret Santa 分配操作,每位成员都会分配到一位接受者并收到一封电子邮件,其中包括了对送给这个人的礼物的所有建议。
不要错过本系列的其余部分

第 2 部分详细介绍了基于 Struts 的类的设计和实现,这些类构成了应用程序的动作和表单。
第 3 部分 详细介绍了基于 JSP 的表示页,以及如何用 Struct 和 JSTL 简化并增强它们的实现。

其他的功能包括支持当密码丢失时重新发送它们,并在增加了新的礼物建议时自动发送电子邮件更新。





回页首


涉及到的工具和技术

让我们快速考察一下在我们的 secret Santa 工作中用到的技术:

J2EE (当然了!)
这是使得像我们的 secret Santa 应用程序这样快速的、可移植的、健壮的、可伸缩的解决方案成为可能的底层技术。
实体 bean
实体 bean(entity bean)为 secret Santa 解决方案提供了一个基于对象的模型,它将数据库支持的数据与相关的业务方法相结合。由于这个应用程序的需求有限,它只提供了对 bean 的本地接口访问。
会话 bean
会话 bean(session bean)用于提供对系统中一些服务的访问。这个特定的应用程序对会话 bean 使用的不多。会话 bean 可以将系统用户与其实现分离,将业务逻辑从实体 bean 中抽取出来,并提供数据的本地缓存,这使得对性能要求更高的更实际的应用程序会从中受益。
值对象
值对象用于捕获实体 bean 的状态快照,从而将显示从实现中分离出来,并提高了性能。
容器管理持久性(CMP)
让容器可以管理持久性对于实现解决方案所需要的工作以及性能都有好处,因为容器可以执行不同的优化。
容器管理关系(CMR)
让容器管理实体 bean 之间的关系有与 CMP 类似的好处。
JavaServer Pages (JSP) 技术
JSP 技术让应用程序的表示(或者用 MVC 的说法:视图)与其实现分离。JSP 页包含 Web 应用程序的所有 HTML 格式编排,使得 secret Santa 应用程序可以不对底层代码做任何改变就重新构造,以适合不同的 Web 站点风格。除了提供 secret Santa 应用程序基于 Web 的视图,还可以用 JSP 页生成由应用程序发送的文本和 HTML 电子邮件,使表示的所有方面都可以容易和动态地配置。
SP 标准标签库(JSTL)
这个标签库实质上消除了在 JSP 页中嵌入任何 Java 代码的需要,标签用于迭代数组和集合、给出具有格式的数据、设置和访问不同上下文中的变量和属性,等等。
Jakarta Struts
这个公开源代码框架提供了支持 MVC 设计模式的控制器基础设施,使配置文件用脚本控制 secret Santa 应用程序。这个文件定义了用户可以请求什么操作、调用什么代码执行这些操作、从这些操作得到什么视图结果,以及错误路径是什么。而且,这个框架提供了 HTML 表单的模型、简化了应用程序对表单值的访问、还提供了在客户和服务器端自动化、脚本化的验证。
Java Mail
这个 API 提供了对 MIME 邮件的简单访问,使这个应用程序可以向 secret Santa 的参与者发送电子邮件,它结合了针对基本邮件阅读程序的纯文本格式和针对其他程序的 HTML 格式。
XDoclet
这个不可缺少的工具是代码生成引擎。根据您所编写的代码中的属性(专门标记的注释),它自动生成相关的类和配置文件。
Ant
Apache Ant 是事实上的 Java 编译工具。它是灵活的、可扩展的,并已经对 XDoclet 提供了广泛支持,使它成为管理 J2EE 应用程序编译的理想工具。

注意:本系列假设您对这些技术有足够的了解,能跟得上我们的讨论。对于其中任何一种技术的更深入了解请参阅 参考资料

这个清单并不完整,所列出的一些技术显然是另一些技术的组成部分,但是它甚至少可以让您对这个应用程序的构建元素有一个印象。





回页首


介绍企业 bean

图 1 是 secret Santa 应用程序中的实体 bean 的一个伪 UML(这里没有足够的地方放下真正的 UML)图。


图 1. 实体 bean
实体 bean
Family
Family bean 表示一个 secret Ssanta 组,它有不同的属性(如名字、电子邮箱地址、密码)和增加和删除成员并计算 secret Santa 分配(如果所有成员都注册了)的方法。每一个 Family 都由三个或者更多 Santa bean 组成(不过,在设置时成员可能少于此数)。姓就是 bean 的主键。
Santa
Santa bean 表示 secret Santa Family 的一位成员。每一个 Santa bean 都有一个名字、电子邮箱地址、PIN 等。此外, Santa bean 拥有对零个或者更多伙伴(不会把这位成员分配给他的伙伴)、一位身份为 secret Santa(开始时未设置)的成员,以及它所给出的或者所有对它做出的 Suggestion bean 的引用。用一个非语义标识符作为主键。
Suggestion
Suggestion bean 捕获由一个 Santa bean 对其他 Santa bean 给出的建议。每一个 Suggestion bean 都有一个描述、一个可选的 URL 和对建议目标的引用。用一个非语义标识符作为主键。

Santa bean 类和部署描述符

在介绍帮助简化 EJB 开发的工具之前,让我们很快地看一下在实现一个企业 bean 时涉及到的所有类和文件。图 2 是只与 Santa bean 有关的类的一个伪 UML 图(在这里只对这个 bean 做详细讨论,其他 bean 遵循这种模式)。


图 2. Santa bean 类
Santa bean 类
SantaLocal
这个接口描述 Santa bean 的本地接口,声明属性访问、关系访问和业务方法。
SantaLocalHome
这个接口描述创建和发现 Santa bean 的本地 API。
SantaUtil
这个实用工具类提供了获得一个 SantaLocalHome 引用的方法,它带有缓存以提高效率以及用于生成主键标识符。
SantaBean
这是 bean 的实现类,它提供了业务方法、EJB 生命周期方法、创建方法等的实现。这些方法的签名必须与在本地和本地 home 接口中的相应声明相匹配。
SantaCMP
在 EJB 1.1 中,这个类提供 bean 属性访问器的实现,它将值存储在本地变量中。在 EJB 2.0 中,容器管理持久性已经有了改变并且不再需要这个类。
SantaValue
这个类是一个值对象,它提供 Santa 的属性和关系的简单非远程、非 EJB 封装。这个类使客户可以用一个 EJB 方法调用获得 Santa bean 当前状态的快照。
SantaLiteValue
这个类是一个轻量级值对象,它提供 Santa bean 的部分属性的简单非远程、非 EBJ 封装。在不需要 bean 状态的完整快照时使用这个类。
*.xml
EJB 部署描述符和特定于容器的文件必须包含对应于 bean 的 CMP 和 CMR 字段、finder 方法等的不同字段。

在这个特定的例子中,这个 bean 有关的有 7 个 Java 类和 3 个 XML 配置文件。使用远程接口、一个自定义主键类和多种目标平台时,还会有更多。而且,这些类中存在很多重复,需要保持所有这些不同文件的协调,对一个文件做出改变时,常常需要将改变传播到许多其他文件中。

使用 XDoclet

XDoclet 是一个非常有价值的工具,它(诸多功能中的一个)允许从一个包含业务方法实现和适当属性注释的源文件( SantaBean 类)中生成与一个企业 bean 有关的所有类和文件(上述所有类和文件)。这意味着节省大量时间,特别是当您开发一个快速的系统原型时,但是在应用程序的整个生命周期同样会有回报,包括维护和增强。

当然,XDoclet 的使用不应该代替好的设计。如果 bean 的接口和关系设计得当,那么许多类、接口和配置文件就不会随着时间改变,这减少了让文件保持同步的负担。不过 XDoclet 与好的设计一点也不抵触,将 bean 合并到一些源代码文件中仍然意味着大量的节省,并且您可以自由挑选 XDoclet 中您所需要的方面、忽略那些您不需要的方面、并定制那些需要定制的方面。并且,XDoclet 对不同 EJB 容器和不同版本的 EJB 的自动目标支持为应用程序提供了巨大的灵活性和发展空间。

我们将在本文的后面详细讨论如何使用 XDoclet。

主键

Santa bean 的主键是一个非语义标识符,它是由一个实用工具类自动生成的。一个明显的替代方法是由姓和成员名组成复合主键。虽然这种方法有自动保证在一个族中名字保持惟一的好处,但是缺点远远超过了这个小小的一次性好处:

  • 姓已经是 Santa bean 架构的隐式部分: Santa bean 与 Family bean 是一种一对多的容器管理的关系。因此在默认情况下,在 Santa bean 架构中姓(主键)将是一个外键 CMR 列,就是说, Santa bean 数据库表已经有了包含姓的 FAMILY_FK 列(参见 清单 12Santa bean 中 family-santas 关系的声明)。不过,CMR 字段不能成为 CMP 主键的一部分,因而在 Santa bean 架构中姓是重复的 ―― 一次是在主键列中,一次是在 CMR 列中。这种重复是差的架构设计。
  • Santa bean 的每一次关系引用(在这里为 santa-partnerssanta-assignedanta-suggestions santa-ideas关系)都需要两个架构列,这样就比单列键更大和更慢。
  • 主键字段是不可变的,改变它们是一种重大和昂贵的操作。因此,像改变成员的名字这样的操作就变得不可能了。
  • 这种设计选择妨碍了在这个应用程序的未来版本中对重名(在某些文化中这是一个很常见的情况)的支持。

因为这些以及其他原因,我倾向于用非语义标识符作为实体 bean 的主键。EJB 技术支持复合主键,XDoclet 使它们更容易了,有时甚至单独的语义值看起来也像主键。但是最终,除非有特别的理由这样做,复合主键所带来的问题比它所解决的问题更多。将意义与数据库的机械关系分离开来是一种很好的设计决定,通过索引数据库中重要的列可以克服所有性能问题(OK,这样 Family bean 的主键就是它的名字,但是我不能在所有时候都遵守我自己的规则)。

在使用非语义主键时,一个明显的问题是如何给它们赋值。有不同的解决方案可以使用。一种选择是使用数据库自动索引机制,另一种是使用键生成企业 bean(有关这种 bean 的讨论的链接,请参阅 参考资料)。不过,我所选择的解决方案是使用一个好的随机数生成器并结合一些本地状态,这很有效,而且在统计上不可能出现冲突。

值对象

一个值对象是实体 bean 的一些或者所有状态的快照。值对象很重要,因为它们使客户可以不用反复访问 bean 就可以获取实体 bean 的状态(例如,在生成 bean 的 JSP 表示时)。这种方法从设计的角度(使显示与实现分离)和性能的角度看都很好。如果远程访问一个 bean,它的好处是明显的:一个远程调用会返回全部对象状态,消除了每个字段的开销。即使对于本地引用,也会有性能上的好处:对实体 bean 的访问应该在事务中进行,不管它是本地还是远程的。获得值对象只需要设置一个事务,而分别访问字段会对每一个字段都发生事务设置成本。

XDoclet 可以自动生成实体 bean 值对象和很多相关的基础设施(如相应的访问器方法实现、用 bean 的状态填充值对象以及用值对象填充 bean 的状态,以及添加相关实体 bean 的方法)。您可以生成多个值对象并配置在每一个值对象中包括哪些字段(例如,提供一个完整的值对象和一个只包含几个字段的轻量级值对象)。此外,您可以定义如何将实体关系映射到值对象,例如,值对象是否应当包括相关的实体 bean 的值对象的集合。

不过,对于 XDoclet 值对象有一些注意事项。特别是,它们 需要 公共 bean 接口以公开某些方法,并且它们将值对象访问器直接构建到实体 bean 中。当您的设计不能满足 XDoclet 要求时,前者就成为一个问题,后者在许多应用程序中是禁止的。例如,考虑两个伙伴, Jane 和 Mary。如果我调用 Jane 的值对象访问器,那么结果将包括她的搭档的值对象 Mary。这样就会调用 Mary 的值对象访问器。而 Mary 的值对象将包括她的伙伴 Jane,因此又要调用回 Jane 的值对象访问器。对于大多数 bean 来说,这会导致重新进入(re-entrance)异常,对于重入(reentrant) bean,它会导致一个无限循环。我选择的模式是一个提供可以正确解析这种引用循环的值对象工厂方法的会话 bean。

注意:非重入意味着如果一个 bean 调用另一个 bean 的方法,并且那个方法调用最后导致方法回调同一事务中原来的调用者,那么就会出现一个错误。标记为非重入很重要,因为对这种情况正确编码非常难,重入方法可能看到对象的一个非预期内部状态。XDoclet 默认为将 bean 标记为非重入。

我对于 XDoclet 的值对象的评论不应当视为批评。事实上,它们突出体现了 XDoclet 的一个长处 :所有自动生成的文件都由 .xdt 脚本驱动,值对象也不例外(参阅 xdoclet-ejb-module-x.y.jar 中的 xdoclet/modules/ejb/entity/resources/valueobject.xdt 和 xdoclet/modules/ejb/entity/resources/entity-value.xdt)。如果所提供的值对象模式不适用于您的应用程序,那么您只需提供您自己的,并将您所创建的所有值对象与 XDoclet 结合使用。

对于这个应用程序,我使用生成支持值对象工厂会话 bean 模式的值对象的不同 .xdt 脚本。请参阅源代码 zip 文件(在 参考资料 中提供)的 Readme.txt 中使用这些脚本的说明。我将不详细说明这里的改变,如果您希望编写自己的 .xdt 脚本,那么您需要分析现有的脚本、分析 XDoclet 的 ejb 模块的源代码并试验,指出这一点就足够了。用现有的标记可以实现我所需要的结果,但是有许多限制。更完整的实现实际上需要在 XDoclet 中的 .xdt 语言中增加内容。已经有了源代码,所以这种改变很容易。图 3 显示了得到的值对象类。注意在这个特定的应用程序中没有引用循环,因为我使用了轻量级值对象表示相关的 bean,轻量级值对象不包括相关 bean 的关系。


图 3. 值对象
值对象

在这个应用程序中,值对象也用在实体 bean 的创建方法中:向实体 bean 的构造函数传递的不是多个参数而是单个值对象。再用这个值对象的一些字段填充实体 bean 的初始状态。我决定这样设计是因为一个带有大量参数的创建方法是丑陋的,并且调用很有可能在错误的地方传递错误的参数。使用值对象使调用者使用 指定的参数:用显式方法调用(例如 setName(name) )在值对象中设置参数,并将填充的对象以及所有必要的相关 bean 引用传递给创建方法。这种技术消除了这种参数错误的可能性,也让您可以不用改变创建 API 就可在 bean 中增加字段。对于老客户传递的值,您的 bean 可以使用适当的默认值,而不用维护多个向后兼容的创建方法。





回页首


逐步介绍企业 bean

在本节,我们将逐步了解组成这个应用程序的企业 bean 的重要部分,驱动 XDoclet 的 comment 属性和所生成的代码。

Family 实体 bean

Family bean 封装了 secret Santa 组的成员,并提供了创建、管理和执行 secret Santa 过程的业务方法。

清单 1 显示了 Family bean 类的 import 语句。这些包括标准 Java 类和接口以及应用程序相关的类。不过要注意,接口、工具类和值对象还不存在,它们将由 XDoclet 从这个源文件中自动生成。

注意:要让 XDoclet 正常工作,您必须用名字显式导入所有自动生成的类,或者必须在引用它们时使用完全限定名。我认为使用完全限定的导入更方便。自动生成的类的名字遵守一种以 bean 的名字为基础的标准(可配置)模式,就像它们的 API 一样,所以您可以在它们实际存在之前写入它们和它们的 API。


清单 1. Family bean 导入
package org.merlin.santa.beans;
import java.security.SecureRandom;
import java.sql.Timestamp;
import java.util.*;
import javax.ejb.*;
import javax.naming.NamingException;
import org.merlin.santa.SantaException;
import org.merlin.santa.SantaMessages;
import org.merlin.santa.interfaces.FamilyState;
import org.merlin.santa.interfaces.SantaLocal;
import org.merlin.santa.interfaces.SantaLocalHome;
import org.merlin.santa.interfaces.SuggestionLocal;
import org.merlin.santa.utils.SantaUtil;
import org.merlin.santa.values.FamilyValue;
import org.merlin.santa.values.SantaValue;

清单 2 展示了这个类的声明和第一个 XDoclet 属性。XDoclet 属性驱动通用的和特定于容器的部署描述符、支持类、公共接口等的生成。在 Javadoc 注释中它们看上去是类似的,它们是在 /** ... */ 注释中出现的,并带有前缀符号 @ 。每一个属性都是由 XDoclet 模块名、一个句号和标记名组成,其后是一组形式为 name="value" 的参数。这些参数可能出现在一行中,也可能各占一行。XDoclet 网站有一个组织得很好的标签参考,描述了这些模块、标签以及它们的使用,有关链接请参阅 参考资料

第一个属性 @ejb.bean 定义了一般 bean 属性:在这里,就是 bean 的名字、类型(容器管理持久性的实体 bean)、重入性(大多数 bean 是非重入的)、它公开的接口(在这里为 local only,如果只有远程接口则指定“remote”,如果两种情况都有则使用“both”)、本地 JNDI 名、主键字段、抽象架构名和 CMP 版本。下一个属性 @ejb.transaction 定义 bean 的默认事务性需求。所有方法都必须在事务的上下文中执行,这对于维护这个 CMP bean 的一致性视图是很重要的。 @ejb.persistence 属性定义这个 bean 的 SQL 表名。 @ejb.finder 属性定义所有应该在 home 接口中公开的其他发现方法。 @ejb.ejb-ref 属性定义这个 bean 对其他 bean 所做出的所有引用,在这里,这个 bean 将对 Santa bean 做出本地引用。 @ejb.value-object 属性控制自动生成的值对象,在这里,它声明所有字段都应该包括在值对象中并指定这个值对象所继承的基类。使用基类使您可以容易地在值对象中添加业务方法,在超类中实现的所有业务方法都会为值对象本身所继承。 清单 7 展示了这个超类的实现。最后, @jboss.persistence 属性配置 JBoss 持久层,以便在部署或者反部署该 bean 时创建但不删除表。

这些 XDoclet 标签与各个方法中的标签共同驱动 FamilyLocalFamilyLocalHome 接口、 FamilyCMP、 FamilyUtil FamilyValue 类和 ejb-jar.xml、jboss.xml 和 jbosscmp-jdbc.xml 部署描述符的生成。XDoclet 的作用是明显的:只用几行代码,我们就可以生成大量相互关联的、并且通常是重复的类和文件,只要再加几行代码我们就可以添加新的值对象或者目标容器,而所有这些信息都以非常近似 bean 的业务逻辑的方式维护的。


清单 2. Family bean 类声明
/**
 * Family entity bean.
 *
 * @ejb.bean
 *   name="Family"
 *   type="CMP"
 *   reentrant="false"
 *   view-type="local"
 *   local-jndi-name="ejb/FamilyLocal"
 *   primkey-field="name"
 *   schema="Families"
 *   cmp-version="2.x"
 * @ejb.transaction
 *   type="Required"
 * @ejb.persistence
 *   table-name="FAMILIES"
 * @ejb.finder
 *   signature="Collection findAll()"
 * @ejb.ejb-ref
 *   ejb-name="Santa"
 *   view-type="local"
 *   ref-name="ejb/SantaLocal"
 * @ejb.value-object
 *   name="Family"
 *   match="*"
 *   extends="org.merlin.santa.values.FamilyValueBase"
 * @jboss.persistence
 *   create-table="true"
 *   remove-table="false"
 */ 
public abstract class FamilyBean
    implements EntityBean {

清单 3 展示了 bean 的一些 CMP 字段。bean 容器会自动将这些字段持久化到一个数据库中。 @ejb.persistence 属性表示 CMP 字段,它让您可以指定数据库列名和 SQL 数据类型。 @ejb.interface-method 标签表明这个方法应该在公共 bean 接口中公开。这个 bean 对它的所有 CMP 字段提供了公共 getter,但是只对那些需要公开的字段提供 setter。没有公共 setter 的字段只能由这个 bean 中的业务方法设置。只要根据 CMP 方法签名和 comment 属性,XDoclet 就会自动生成相应的 public 接口方法、值对象字段、CMP的实现和部署描述符字段。

@ejb.persistence 属性将主键字段标记为从事务内部或者事务外部可访问(所有其他字段继承了“Requires”行为)。这之所以可能,是因为它是一个不可变的主键字段,所以不用事务保护就可以安全地访问。


清单 3. Family bean CMP 字段
  /**
   * The family name.
   *
   * @ejb.persistence
   *   column-name="NAME"
   * @ejb.interface-method
   * @ejb.transaction
   *   type="Supports"
   */
  public abstract String
  getName
  ();
  
  public abstract void
  setName
  (String name);
  [...]
 
  /**
   * The family state.
   *
   * @see org.merlin.santa.interfaces.FamilyState
   *
   * @ejb.persistence
   *   column-name="STATE"
   * @ejb.interface-method
   */
  public abstract String
  getState
  ();
  
  /**
   * @ejb.interface-method
   */
  public abstract void
  setState
  (String state);

清单 4 展示了 bean 的 CMR 字段。容器将自动负责维护数据库外键列或者表,用于表示 bean 关系。对于一对多关系的“一”端,我们所需要就是一个 @ejb.relation 属性,用于指定关系和角色名。目标 bean 将包括更多的关系信息,使 XDoclet 和容器可以管理这个关系(请参阅 清单 12)。注意在这里,我们在 public bean 接口中只公开了 getSantas() 方法。

在默认情况下,关系不包括在 XDoclet 值对象中。要在值对象中包括一个关系,必须提供一个指定如何表示关系的 @ejb.value-object 属性。为相关的 bean 指定值对象类、值对象访问器方法和相关的 bean 接口和名字,以及几个额外的参数。对于这个特定的例子,值对象 FamilyValue 将包含返回对应于相关的 Santa bean 的 SantaLiteValue 对象的 Collection getSantas() 方法。


清单 4. Family bean CMR 字段
  /**
   * Family santas.
   *
   * @ejb.relation
   *   name="family-santas"
   *   role-name="family-has-santas"
   * @ejb.interface-method
   * @ejb.value-object
   *   compose="org.merlin.santa.values.SantaLiteValue"
   *   compose-name="Santa"
   *   members="org.merlin.santa.interfaces.SantaLocal"
   *   members-name="Santa"
   *   relation="external"
   *   type="Collection"
   */
  public abstract Collection
  getSantas
  ();
  
  public abstract void
  setSantas
  (Collection secretSantas);

清单5 展示了 bean 的业务方法,它们都使用 @ejb.interface-method 来标记,这样它们就在 bean 的 public 接口中公开。

addSanta() 方法添加一个新的 Santa 。因为我们不使用复合主键(姓、成员名),所以我们不会得到数据库的自动双重保护。相反,使用一个两阶段过程:这个方法首先创建新的 Santa ,XDoclet 生成的 SantaUtil 方法 getLocalHome() 返回一个对 Santa bean 的本地 home 接口的引用, EntityContext 方法 getEJBLocalObject() 返回一个对这个 Family bean 的引用以传递给 creator 方法。 Santa bean ejbCreate() 方法自动将自己添加到 family-santas 关系中。这个过程完成后,就由调用者提交新的 Santa 事务,然后在 family-santas 关系中搜索以检查是否有一个同名的 Santa bean 存在。如果找到一个重复的,那么就会删除新的 Santa 并抛出一个异常,否则,新的 Santa 就可标记为有效。这是一个优化的实现,它在预期的、成功的情况中是效率最高的。有必要在创建 Santa bean 以后提交事务,以便保证对其他事务的可见性。

removeSanta() 方法删除一个 Santa ,您可以指定目标 Santa 的主键或者名字,提供了对从一个外部 Family 中删除一个 Santa 的基本保护。我们这样做是为了完整性检查而不是安全性目的,这个应用程序目前没有提供安全性。

getSantaList() 方法返回一个按字母排列的各 Santa 的快照,只返回那些标记为有效的,这可防止返回没有验证为惟一的 Santa bean。CMR getSantas() 方法与 getSantaList() 的另一个区别是前者返回关系的一个活表示。向结果 collection 中添加一个 Santa 或者从其删除一个 Santa ,实际上会从数据库中的 relationship 中添加或者删除它。在 清单 14 中,您将看到一个 Santa bean 通过调用其 CMR setFamily() 方法将自己添加到关系中。如果没有被 Family bean 调用,它也可以将自己 add()Family bean 的 getSantas() 集合。因为这个集合是活的,所以它只能在事务中访问,如果客户试图从事务外面访问这个集合,那么就会出现一个错误。从 getSantaList() 得到的排序列表是一个简单的快照,因而可以从事务上下文的外面访问。


清单 5. Family bean 业务方法
  /**
   * Add a secret Santa.
   *
   * @param value The new Santa value. Only the name and e-mail address 
   * are used.
   *
   * @ejb.interface-method
   */
  public SantaLocal
  addSanta
  (SantaValue value)
  throws CreateException, NamingException,
         RemoveException, SantaException {
    SantaLocalHome home = SantaUtil.getLocalHome ();
    FamilyLocal family = 
      (FamilyLocal) getEntityContext ().getEJBLocalObject ();
    return home.create (family, value);
  }
  
  /**
   * Remove a secret Santa.
   *
   * @param value The santa value. Either the primary key or name are used.
   *
   * @ejb.interface-method
   */
  public void
  removeSanta
  (SantaValue value)
  throws FinderException, NamingException,
         RemoveException, SantaException {
    SantaLocal removeSanta = null;
    if (value.getPrimaryKey () != null) {
      SantaLocalHome home = SantaUtil.getLocalHome ();
      removeSanta = home.findByPrimaryKey (value.getPrimaryKey ());
      FamilyLocal family = 
        (FamilyLocal) getEntityContext ().getEJBLocalObject ();
      if (!family.isIdentical (removeSanta.getFamily ()))
        throw new SantaException (SantaMessages.ERROR_NOT_PERMITTED);
    } else {
      String name = value.getName ();
      for (Iterator i = getSantas ().iterator (); i.hasNext ();) {
        SantaLocal santa = (SantaLocal) i.next ();
        if (santa.getIsValid () && name.equals (santa.getName ())) {
          removeSanta = santa;
        }
      }
      if (removeSanta == null)
        throw new SantaException 
          ("That santa name does not exist: " + name);
    }
    removeSanta.remove ();
  }
  /**
   * Get the santas sorted alphabetically.
   *
   * @ejb.interface-method
   */
  public List
  getSantaList
  () {
    TreeMap sorted = new TreeMap ();
    for (Iterator i = getSantas ().iterator (); i.hasNext ();) {
      SantaLocal santa = (SantaLocal) i.next ();
      if (santa.getIsValid ()) {
        sorted.put (santa.getName (), santa);
      }
    }
    return new ArrayList (sorted.values ());
  }
  
  /**
   * Assign a secret santa to each family member.
   *
   * @ejb.interface-method
   */
  public void
  assignSecretSantas
  ()
  throws SantaException {
    [...]
  }

清单 6 显示 bean 的生命周期方法。 ejbCreate() 方法根据 FamilyValue 参数的一些字段创建一个新的 Family bean,其他字段,如创建时间和密码是自动赋值的。然后声明一个 abstract getEntityContext() 方法,它将检索实体上下文。这个方法以及其他的生命周期方法( ejbPostCreate()、 ejbLoad()、 setEntityContext() 等)的实际实现将自动由 FamilyCMP 子类中的 XDoclet 提供,消除了手工编写它们的需要。就像值对象一样,这个 getEntityContext() 方法实际上是由一个改编过的 XDoclet .cdt 脚本生成的(更多细节请参阅源代码压缩文档中的 Readme.txt 以及 xdoclet/modules/ejb/entity/resources/entity-body.xdt)。


清单 6. Family bean 生命周期方法
  /**
   * Create. Only the name and email address are used.
   *
   * @ejb.create-method
   */
  public String
  ejbCreate
  (FamilyValue value)
  throws CreateException, SantaException {
    setName (value.getName ());
    setEmailAddress (value.getEmailAddress ());
    String password = String.valueOf ((int) (Math.random () * 99999));
    setPassword ("00000".substring (password.length ()) + password);
    setDescription (null);
    setCreationTime (new Timestamp (System.currentTimeMillis ()));
    setState (FamilyState.CREATED);
    return null;
  }
  /**
   * Get the entity context.
   */
  public abstract EntityContext
  getEntityContext
  ();
}

清单 7 显示 Family bean 值对象的超类。这个类声明一个重复的 bean 的 getSantaList() 方法,这个方法返回 Santa bean 值对象的一个按字母顺序排列的列表。要让超类访问以后由值对象子类中 XDoclet 所生成的方法,需要声明与 XDoclet 要生成的 API (如 getSantas() )匹配的抽象方法。通常这些方法匹配原来 bean 类中的方法签名。


清单 7. Family bean 值对象基
package org.merlin.santa.values;
import java.util.*;
import org.merlin.santa.values.SantaValue;
/**
 * Family value object superclass.
 */
public abstract class FamilyValueBase {
  /**
   * Get the alphabetically sorted Santa values.
   */
  public List
  getSantaList
  () {
    TreeMap sorted = new TreeMap ();
    for (Iterator i = getSantas ().iterator (); i.hasNext ();) {
      SantaLiteValue value = (SantaLiteValue) i.next ();
      if (value.getIsValid ()) {
        sorted.put (value.getName (), value);
      }
    }
    return new ArrayList (sorted.values ());
  }
  /**
   * Get the Santa values. Will be implemented by the XDoclet-generated
   * subclass.
   */
  public abstract Collection
  getSantas
  ();
}

清单 8 展示了一个定义 Family bean 状态的有效常量的接口。


清单 8. Family bean 状态接口
package org.merlin.santa.interfaces;
public interface FamilyState {
  /** Family just created. */
  public static final String
  FAMILY_CREATED
  = "created";
  /** Family activated. */
  public static final String
  FAMILY_ACTIVATED
  = "activated";
  
  /** Secret santas assigned. */
  public static final String
  FAMILY_ASSIGNED
  = "assigned";
}

到这里就完成了对 Family bean 必须手工编写的代码。我们分析了声明 Family bean 的 CMP/CMR 字段的 FamilyBean 类,它实现了其业务方法,并包括特别的 comment 属性,它驱动 XDoclet 生成一系列的支持类和部署描述符。在我们运行 XDoctlet(通过使用 Ant 项目编译器)后,在目录 ../build/ejb/gen 中会自动生成下面的类和接口:

org.merlin.santa.interfaces.FamilyLocal
这个本地接口公开所有的 CMP、CMR 和标记为 @ejb.interface-method 的业务方法。下面是生成的代码的一部分,方法签名和文档直接匹配原来的 FamilyBean 代码:
/*
 * Generated by XDoclet - Do not edit!
 */
package org.merlin.santa.interfaces;
/**
 * Local interface for Family.
 */
public interface FamilyLocal
   extends javax.ejb.EJBLocalObject
{
   /**
    * The family name.
    */
   public java.lang.String getName(  ) ;
   [...]
   /**
    * Assign a secret Santa to each family member.
    */
   public void assignSecretSantas(  ) 
     throws org.merlin.santa.SantaException;
}


org.merlin.santa.interfaces.FamilyLocalHome
这个 home 接口公开了标记为 @ejb.create-method 的 creator 方法、用 @ejb.finder 指定的 finder 方法,并提供一些标识这个 bean 绑定的 JNDI 名字的常量。生成的代码如下所示:
/*
 * Generated by XDoclet - Do not edit!
 */
package org.merlin.santa.interfaces;
/**
 * Local home interface for Family.
 */
public interface FamilyLocalHome
   extends javax.ejb.EJBLocalHome
{
   public static final String COMP_NAME=
     "java:comp/env/ejb/FamilyLocal";
   public static final String JNDI_NAME=
     "ejb/FamilyLocal";
   public org.merlin.santa.interfaces.FamilyLocal 
     create(org.merlin.santa.values.FamilyValue value) throws
       javax.ejb.CreateException,org.merlin.santa.SantaException;
   public java.util.Collection findAll()
     throws javax.ejb.FinderException;
   public org.merlin.santa.interfaces.FamilyLocal 
     findByPrimaryKey(java.lang.String pk)
       throws javax.ejb.FinderException;
}


org.merlin.santa.beans.FamilyCMP
这个类提供 CMP 相关的方法、生命周期方法和各种 XDoclet 实用工具方法。它还将包含标准的值对象方法,但是我已经替换了 XDoclet 的这一方面。下面显示生成的代码的一部分:
/*
 * Generated by XDoclet - Do not edit!
 */
package org.merlin.santa.beans;
/**
 * CMP layer for Family.
 */
public abstract class FamilyCMP
   extends org.merlin.santa.beans.FamilyBean
   implements javax.ejb.EntityBean
{
   /**
    * Generated ejbPostCreate for corresponding ejbCreate method.
    *
    * @see #ejbCreate(org.merlin.santa.values.FamilyValue value)
    */
   public void 
     ejbPostCreate(org.merlin.santa.values.FamilyValue value)
   {
   }
   [...]
   public javax.ejb.EntityContext getEntityContext()
   {
      return this.ctx;
   }
   [...]
}


org.merlin.santa.utils.FamilyUtil
这个工具类提供一种获得对 Family 本地 home 接口的引用,以及另一个对生成作为主键使用的 GUID 的接口的引用的方便方法,尽管 Family bean 不使用这个方法。下面是生成的代码的一部分:
/*
 * Generated file - Do not edit!
 */
package org.merlin.santa.utils;
/**
 * Utility class for Family.
 */
public class FamilyUtil
{
   [...]
   public static org.merlin.santa.interfaces.FamilyLocalHome 
     getLocalHome()
       throws javax.naming.NamingException
   [...]
   public static final String generateGUID(Object o)
   [...]
}


org.merlin.santa.values.FamilyValue
这个值对象包含由 @ejb.value-object 标签标识的实体 bean 的字段和关系的快照。我提供的自定义值对象模式还包含一个会话 bean ,用来创建值对象实例的 static getInstance() 方法。下面是生成的代码的一部分:
/*
 * Generated by XDoclet - Do not edit!
 */
package org.merlin.santa.values;
import java.util.*;
/**
 * Value object for Family.
 *
 */
public class FamilyValue
   extends org.merlin.santa.values.FamilyValueBase
   implements java.io.Serializable
{
   private java.lang.String name;
   [...]
   public java.lang.String getName()
   {
          return this.name;
   }
   public void setName( java.lang.String name )
   {
          this.name = name;
          pk = name;
   }
   [...]
   public static org.merlin.santa.values.FamilyValue
   getInstance
   (javax.ejb.EJBLocalObject beanObject)
   [...]
}

Family bean 是一个相当基本的例子(我们甚至没有展示部署描述符),但是它展示了 EJB 和 XDoclet 技术的部分能力:以相对少的代码,我们有了一个完整的企业组件。CMP 自动处理 bean 状态的持久性,CMP 自动处理 bean 关系,而 XDoclet 自动生成相关的接口、工具类和部署描述符。

Santa 实体 bean

再往下, Santa bean 描述了 secret Santa Family 的一位成员,封装了有关这位成员的信息以及对这位成员所做的和由这位成员做出的 Suggestion bean,并提供了添加和删除 Suggestion 的业务方法。这个 bean 与 Family bean 的模式在很大程度上是相同的,我们不再做进一步的讨论,但是有一些重要的不同之处,在遇到时我将对它们给予讨论。

清单 9 展示了 Santa bean 类的 import 语句。


清单 9. Santa bean 的 import 语句
package org.merlin.santa.beans;
import java.util.*;
import javax.ejb.*;
import javax.naming.NamingException;
import org.merlin.santa.SantaException;
import org.merlin.santa.SantaMessages;
import org.merlin.santa.interfaces.FamilyLocal;
import org.merlin.santa.interfaces.SantaLocal;
import org.merlin.santa.interfaces.SuggestionLocal;
import org.merlin.santa.interfaces.SuggestionLocalHome;
import org.merlin.santa.utils.SuggestionUtil;
import org.merlin.santa.utils.SantaUtil;
import org.merlin.santa.values.SantaValue;
import org.merlin.santa.values.SuggestionValue;

清单 10 展示了类声明和类范围的 XDoclet 属性。 Santa bean 有两个值对象: SantaValueSantaLiteValue 。前者包括这个 bean 的所有字段和关系,后者只包括主键、名字、电子邮箱地址和有效性。出于效率方面的考虑,当提取一个 Santa bean 值对象时,只包括所有相关的 Santa bean 的轻量级值对象。另一方面,返回相关 bean 的完整值对象效率将很低,因为每一个值对象都包括每一个伙伴的值对象和分配的 Santa bean,以及它们的伙伴的值对象和分配的 Santa bean,依此类推。在许多情况下,这将包括 Family 中每一个 Santa bean 的值对象。在 XDoclet @ejb.value-object 属性中指定值对象 SantaLiteValue ,参数 match="lite" 表明只有用名字“lite”标识的 bean 字段才加入到这个值对象中。


清单 10. Santa bean 类声明
/**
 * Santa entity bean.
 *
 * @ejb.bean
 *   name="Santa"
 *   type="CMP"
 *   reentrant="false"
 *   view-type="local"
 *   local-jndi-name="ejb/SantaLocal"
 *   primkey-field="key"
 *   schema="Santas"
 *   cmp-version="2.x"
 * @ejb.transaction
 *   type="Required"
 * @ejb.persistence
 *   table-name="SANTAS"
 * @ejb.ejb-ref
 *   ejb-name="Suggestion"
 *   view-type="local"
 *   ref-name="ejb/SuggestionLocal"
 * @ejb.value-object
 *   name="Santa"
 *   match="*"
 * @ejb.value-object
 *   name="SantaLite"
 *   match="lite"
 * @jboss.persistence
 *   create-table="true"
 *   remove-table="false"
 */ 
public abstract class SantaBean
    implements EntityBean {

清单 11 展示了 Santa 类的一些 CMP 字段。对于每一个字段,XDoctlet 属性控制列名、数据类型以及在公共接口中是否公开 getter 和 setter。在这个类中,在 XDoclet 属性中没有显式设置数据类型,但是设置了列名。如果没有显式指定,XDoclet 将根据字段类型和名字选择默认的数据类型和列名。在这里,我使用显式列名以匹配原来的数据库架构。

只有用参数 match="lite" 标记 @ejb.value-object 属性的字段会加入到轻量级值对象中。主值对象包含所有 CMP 字段。


清单 11. Santa bean CMP 字段
  /**
   * The primary key.
   *
   * @ejb.persistence
   *   column-name="KEY"
   * @ejb.interface-method
   * @ejb.transaction
   *   type="Supports"
   * @ejb.value-object
   *   match="lite"
   */
  public abstract String
  getKey
  ();
  
  public abstract void
  setKey
  (String key);
  [...]
  /**
   * Whether there are new suggestions.
   *
   * @ejb.persistence
   *   column-name="NEWSUG"
   * @ejb.interface-method
   */
  public abstract boolean
  getHasNewSuggestions
  ();
  
  /**
   * @ejb.interface-method
   */
  public abstract void
  setHasNewSuggestions
  (boolean newSuggestions);

清单 12 展示了 Santa bean 的 CMR 字段。这个 bean 参与 Family bean 之外的许多关系,有单向和双向的、一对一的、一对多的和多对多的。XDoclet 呈现这些关系的声明特别容易,而 EJB 的 CMR 完全透明地呈现它们的实现。

family-santas 声明完成了在 Family bean 中开始的关系(请参阅 清单 4)。 @ejb.relation 属性包含 XDoclet 为这个关系生成相应代码和部署描述符需要的所有其他信息: name 参数必须与关系的双方相匹配、 role-name 提供了关系的这一侧的名字、 cascade-delete 参数表明如果删除了一个 Family bean,那么它包含的所有 Santa bean 都应该被删除。这是双向一对多关系中“多”的一侧,这样 Santa bean 架构将包含拥有 Family bean 的主键的列。 @jboss.relation 属性声明主键字段以及列名。如果外键是复合的,那么会有多个 @jboss.relation 字段。缺少 @ejb.value-object 属性意味着这个字段将不会在 Santa bean 的值对象中公开。

santa-partners 关系是单向的多对多关系,标识每一个 Santa bean 的“伙伴”。一个 Santa bean 将不会被指定为它的任何一个伙伴的 secret Santa。因为这是单向的关系,所以对于这一个方法是在 XDoclet comment 中提供它两端的信息, name 参数命名这个关系, role-nametarget-role-name 参数定义源和目标角色名, target-ejb 参数标识目标 bean(它是另一个 Santa bean),而 target-multiple 参数指定目标可能是多个 Santa 的伙伴。

关系的源一侧的多重性是从 getPartners() 返回一个集合这一事实推断出来的。多对多关系在数据库中是由一个单独的关系表所表示的, @jboss.relation@jboss.target-relation @jboss.relation-table 属性定义了这个表的结构,以及它的列如何映射到 Santa bean 键。最后, @ejb.value-object 属性声明这个关系将公开为由值对象的 getPartners() 方法返回的 SantaLiteValue 对象的一个 Collection

其他的关系有类似的规律: Santa-assigned 是单向的一对一关系,标识每一个 Santa bean 指派给了谁, Santa bean 数据库架构将包含一个 ASSIGNED_KEY 列,它引用目标 Santa bean 的主键。 Santa-suggestions 是一个双向的一对多关系,从 Santa bean 到它所给出的 Suggestion bean,Suggestion 数据库架构将包含一个引用源 Santa bean 的主键的 MAKER_KEY 列。

最后, santa-ideasSanta bean 与对它们所做出的 Suggestion bean 之间的一个双向的一对多关系,这个关系的声明是在 Suggestion bean 完成的。 target-cascade-delete 参数(和在 santa-suggestions 关系中的目标中的 cascade-delete 参数)保证如果删除了一个 Santa bean,那么由这个 Santa 所做出的和对这个 Santa 所做的所有 Suggestion bean 也会自动删除,无需在应用程序中编写任何代码。


清单 12. Santa bean CMR 字段
  /**
   * Family santas.
   *
   * @ejb.relation
   *   name="family-santas"
   *   role-name="santa-has-family"
   *   cascade-delete="yes"
   * @ejb.interface-method
   * @jboss.relation
   *   related-pk-field="name"
   *   fk-column="FAMILY_FK"
   *   fk-constraint="true"
   */
  public abstract FamilyLocal
  getFamily
  ();
  
  public abstract void
  setFamily
  (FamilyLocal family);
  
  /**
   * Partners.
   *
   * @ejb.relation
   *   name="santa-partners"
   *   role-name="santa-has-partners"
   *   target-ejb="Santa"
   *   target-role-name="partner-has-santas"
   *   target-multiple="yes"
   * @ejb.interface-method
   * @ejb.value-object
   *   aggregate="org.merlin.santa.values.SantaLiteValue"
   *   aggregate-name="Partner"
   *   members="org.merlin.santa.interfaces.SantaLocal"
   *   members-name="Partner"
   *   relation="external"
   *   type="Collection"
   * @jboss.relation
   *   related-pk-field="key"
   *   fk-column="PARTNER_KEY"
   *   fk-constraint="true"
   * @jboss.target-relation
   *   related-pk-field="key"
   *   fk-column="RENTRAP_KEY"
   *   fk-constraint="true"
   * @jboss.relation-table
   *   table-name="PARTNERS"
   */
  public abstract Collection
  getPartners
  ();
  /**
   * @ejb.interface-method
   */
  public abstract void
  setPartners
  (Collection partners);
  
  /**
   * Assigned.
   *
   * @ejb.relation
   *   name="santa-assigned"
   *   role-name="santa-has-assigned"
   *   target-ejb="Santa"
   *   target-role-name="assigned-has-santa"
   * @ejb.interface-method
   * @ejb.value-object
   *   aggregate="org.merlin.santa.values.SantaLiteValue"
   *   aggregate-name="Assigned"
   *   members="org.merlin.santa.interfaces.SantaLocal"
   *   members-name="Assigned"
   *   relation="external"
   * @jboss.relation
   *   related-pk-field="key"
   *   fk-column="ASSIGNED_KEY"
   *   fk-constraint="true"
   */
  public abstract SantaLocal
  getAssigned
  ();
  
  public abstract void
  setAssigned
  (SantaLocal assigned);
  /**
   * Suggestions made by this santa.
   *
   * @ejb.relation
   *   name="santa-suggestions"
   *   role-name="santa-made-suggestions"
   * @ejb.interface-method
   * @ejb.value-object
   *   aggregate="org.merlin.santa.values.SuggestionValue"
   *   aggregate-name="Suggestion"
   *   members="org.merlin.santa.interfaces.SuggestionLocal"
   *   members-name="Suggestion"
   *   relation="external"
   *   type="Collection"
   */
  public abstract Collection
  getSuggestions
  ();
  
  public abstract void
  setSuggestions
  (Collection suggestions);
  /**
   * Suggestions made for this santa.
   *
   * @ejb.relation
   *   name="santa-ideas"
   *   role-name="santa-has-suggestions"
   * @ejb.interface-method
   * @ejb.value-object
   *   aggregate="org.merlin.santa.values.SuggestionValue"
   *   aggregate-name="Idea"
   *   members="org.merlin.santa.interfaces.SuggestionLocal"
   *   members-name="Idea"
   *   relation="external"
   *   type="Collection"
   */
  public abstract Collection
  getIdeas
  ();
  
  public abstract void
  setIdeas
  (Collection ideas);

清单 13 展示了 Santa bean 的业务方法。它们与添加和删除 Suggestion bean 有关,并遵循与 Family bean 的对应方法基本相同的逻辑。在添加新的 Suggestion 时,向 Suggestion bean 创建方法传递对提出者和接收者 bean 的引用,然后它自动将自己添加到适当的关系中。在删除一个 Suggestion 时,由进行删除的 Santa bean 进行检查或者对这个 Santa bean 进行检查。


清单 13. Santa bean 业务方法
  /**
   * Add a suggestion.
   *
   * @param value The new suggestion value. Only the recipient name,
   * description and URL are used.
   *
   * @ejb.interface-method
   */
  public SuggestionLocal
  addSuggestion
  (SuggestionValue value)
  throws CreateException, NamingException, SantaException {
    SuggestionLocalHome home = SuggestionUtil.getLocalHome ();
    SantaLocal myself =
     (SantaLocal) getEntityContext ().getEJBLocalObject ();
    String name = value.getRecipient ().getName ();
    SantaLocal recipient = null;
    if (name.equals (getName ())) {
      recipient = myself;
    } else {
      Iterator i = getFamily ().getSantas ().iterator ();
      while (i.hasNext ()) {
        SantaLocal santa = (SantaLocal) i.next ();
        if (!santa.isIdentical (myself) && santa.getIsValid ()
            && name.equals (santa.getName ()))
          recipient = santa;
        if (recipient == null)
          throw new SantaException
            (SantaMessages.ERROR_MEMBER_UNKNOWN, name);
      }
    }
    SuggestionLocal newSuggestion =
      home.create (myself, recipient, value);
    if (recipient == myself) {
      setHasNewSuggestions (true);
    } else {
      recipient.setHasNewSuggestions (true);
    }
    return newSuggestion;
  }
  /**
   * Remove a suggestion.
   *
   * @ejb.interface-method
   */
  public void
  removeSuggestion
  (SuggestionValue value)
  throws FinderException, NamingException, RemoveException, 
    SantaException {
    SuggestionLocalHome home = SuggestionUtil.getLocalHome ();
    SuggestionLocal removeSuggestion = 
      home.findByPrimaryKey (value.getKey ());
    SantaLocal santa = 
      (SantaLocal) getEntityContext ().getEJBLocalObject ();
    if (!santa.isIdentical (removeSuggestion.getRecipient ()) &&
        !getSuggestionsMade ().contains (removeSuggestion))
      throw new SantaException (SantaMessages.ERROR_NOT_PERMITTED);
    removeSuggestion.remove ();
  }

清单 14 展示了 Santa bean 的生命周期方法。惟一要注意的重要一点是在创建 bean 时将它添加到关系中的代码:不能在 ejbCreate() 方法完成,而是要在 ejbPostCreate() 方法中完成,为这两种方法提供的参数是相同的。要将 bean 添加到单个关系中,调用 set Relation() 方法,要将 bean 添加到多重关系中,使用由 get Relation() 方法返回的集合的 add() 方法。对于将自己添加到另一个 bean 的关系中的 bean,可以使用 EntityContext getEJBLocalObject() 方法,如 清单 5所示,然后对目标 bean 调用适当的关系访问器。


清单 14. Santa bean 生命周期方法
  /**
   * Create a new Santa bean.
   *
   * @param family The family to which this belongs.
   * @param value The santa value. Only the name and email address are used.
   *
   * @ejb.create-method
   */
  public String
  ejbCreate
  (FamilyLocal family, SantaValue value)
  throws CreateException {
    setKey (SantaUtil.generateGUID (this));
    setName (value.getName ());
    setEmailAddress (value.getEmailAddress ());
    String pin = String.valueOf ((int) (Math.random () * 9999));
    setPIN ("0000".substring (pin.length ()) + pin);
    setCode (-1);
    setWishList (null);
    setLicenseAgreed (false);
    setHasNewSuggestions (false);
    return null;
  }
  /**
   * Santa bean post-create.
   */
  public void
  ejbPostCreate
  (FamilyLocal family, SantaValue value)
  throws CreateException {
    setFamily (family);
  }
  /**
   * Get the entity context.
   */
  public abstract EntityContext
  getEntityContext
  ();
}

Santa bean 自动生成的类和文件形式与 Family bean 一样,只是有两个而不是一个自动生成的值对象。

Suggestion 实体 bean

Suggestion bean 遵从与 FamilySanta bean 一样的模式,没有必要详细解释。在 参考资料 中提供了源代码。

ValueFactory 会话 bean

ValueFactory bean 是一个提供创建对应于 FamilySantaSuggestion 实体 bean 的值对象的方法。正如在前面讨论的,它的模式与与标准 XDoclet 值对象不同,它具有自动处理引用循环和消除对实体 bean 本身的 API 的所有影响的好处。

清单 15 展示了 ValueFactory bean 类的 import 语句。


清单 15. ValueFactory bean 的 import 语句
package org.merlin.santa.beans;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import org.merlin.santa.interfaces.FamilyLocal;
import org.merlin.santa.interfaces.SantaLocal;
import org.merlin.santa.interfaces.SuggestionLocal;
import org.merlin.santa.values.FamilyValue;
import org.merlin.santa.values.SantaValue;
import org.merlin.santa.values.SuggestionValue;

清单 16 展示了 ValueFactory bean 类声明和 bean 范围的 XDoclet 属性。这里惟一的改变是 @ejb.bean 属性有参数 type="Stateless" ,表明这是一个无状态会话 bean,省略了许多与实体 bean 相关的属性。


清单 16. ValueFactory bean 类声明
/**
 * Value factory bean.
 *
 * @ejb.bean
 *     name="ValueFactory"
 *     type="Stateless"
 *     view-type="local"
 *     local-jndi-name="ejb/ValueFactoryLocal"
 * @ejb.transaction
 *      type="Required"
 */
public class ValueFactoryBean
    implements SessionBean {

清单 17 展示了 ValueFactory bean 业务方法。它们是简单值对象构造方法,它们是用 XDoclet 属性 @ejb.interface-method 标记的,表明它们应该在本地和远程接口中公开,并且它们继承 bean 范围的事务要求,它们取一个实体 bean 引用作为参数,并返回相应的值对象作为结果。构造值对象的所有逻辑,包括对引用循环的处理,都包含在自动生成的值对象类的 getInstance() 方法中。结果,这个 bean 的业务方法特别简单,困难的工作已经在一个 XDoclet xdt 脚本中写过一次了,每一个值对象创建时就会自动重复使用它。

注意这个 bean 不进行缓存,它是一个提取实体 bean 的当前状态的简单无状态 bean。在部署涉及对实体 bean 的远程访问的应用程序时,这个 bean 将与实际的实体 bean 放在一起,而缓存将由与 servlet 前端在同一位置的有状态 bean 进行。


清单 17. ValueFactory bean 业务方法
  /**
   * Get a family value object.
   *
   * @ejb.interface-method
   */
  public FamilyValue
  getFamilyValue
  (FamilyLocal family) {
    return FamilyValue.getInstance (family);
  }
  /**
   * Get a santa value object.
   *
   * @ejb.interface-method
   */
  public SantaValue
  getSantaValue
  (SantaLocal santa) {
    return SantaValue.getInstance (santa);
  }
  /**
   * Get a suggestion value object.
   *
   * @ejb.interface-method
   */
  public SuggestionValue
  getSuggestionValue
  (SuggestionLocal suggestion) {
    return SuggestionValue.getInstance (suggestion);
  }

清单 18 展示了这个 bean 的生命周期方法。作为一个无状态会话 bean,这些方法并不做什么真正的工作。


清单 18. ValueFactory bean 生命周期方法
  /**
   * Create this bean.
   */
  public void
  ejbCreate
  () {
  }
  /**
   * Remove this bean.
   */
  public void
  ejbRemove
  () {
  }
  /**
   * Activate this bean.
   */
  public void
  ejbActivate
  () {
  }
  /**
   * Passivate this bean.
   */
  public void
  ejbPassivate
  () {
  }
  /**
   * The session context.
   */
  private SessionContext
  _sessionContext;
  /**
   * Set the session context.
   */
  public void
  setSessionContext
  (SessionContext sessionContext) {
    _sessionContext = sessionContext;
  }
  /**
   * Get the session context.
   */
  public SessionContext
  getSessionContext
  () {
    return _sessionContext;
  }
  /**
   * Unset the session context.
   */
  public void
  unsetSessionContext
  () {
    _sessionContext = null;
  }
}





回页首


结束语

这第一篇文章介绍了 secret Santa 应用程序。我们探讨了实现这个应用程序所用的工具和技术,并完成了模型实现的过程,包括封装了其状态、关系和一些业务逻辑的实体 bean。在开发 J2EE 应用程序时,您可以从头开始构建它们,也可以使用许多当前可用的工具以尽可能地提高您的生产率。后一种方法不仅可以加速您的开发时间,而且得到的解决方案通常会更健壮和有更好的伸缩性,因为它可以从推动开发这些支持工具的大量经验中受益,您会有更多时间设计和对结果进行测试。除了底层 J2EE 技术,我们的 secret Santa 应用程序模型的实现由于使用 XDoclet 而得到了巨大好处,只用稍微多于一千行注释的代码就可以得到一个体积四倍于它的应用程序。这个代码的大部分都是自动生成的代码,它们都经过部署、使用和验证,因此应该只有很少或者没有错误。XDoclet 有许多好处,其中一条是它支持对不同应用程序设计的定制,在这里使用的自定义值对象模式就体现了这一点。

第 2 部分我们将探讨这个应用程序的控制器方面,以及使用 servlet、JavaMail 和 Struts 以支持其开发。在 第 3 部分 中,我们将步入基于 JSP 的表示页面,以及如何使用 Struts 和 JSTL 来简化和增强它们的实现。最后,您将看到如何构建和部署得到的应用程序。不过,如果有人等不及,附带的源代码 zip 文件(请参阅 参考资料)包含这个应用程序的全部可部署的源代码和编译工具。



参考资料



关于作者

Merlin 是全球电子安全公司 Betrusted, Inc 的密码专家和首席技术讲师。作为不断增长的 J2EE 家族成员之一,在传统的随机编码技术(hat-based technology)无法胜任时,他用 J2EE 开发了圣诞礼物问题的解决方案。他住在纽约州纽约市(一个非常美的城市,人们给它取了两次名字),可以通过 merlin@merlin.org 与他联系。




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?




回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款