floyd marinescu floyd ejb second editionread.pudn.com/downloads74/ebook/272500/ejb.design... ·...

183
译者序 随着 J2EE 技术的日益流行,越来越多的企业都选择使用 J2EE 技术来构建它们的应用 系统,但是,EJB 技术是一项高端技术,如何构建高效、安全和可靠的基于 EJB 的企业应 用系统就成为了软件开发者要面临的一个重大挑战。很多有经验的 EJB 系统开发人员在他 们的实践过程中总结出了许多可以提高系统性能的用于 EJB 编程的可重用方法,从而形成 EJB 设计模式。这些设计模式可以帮助其他的 EJB 开发人员提高项目的质量,缩短项目 的开发周期。但是 EJB 设计模式种类众多,所针对的问题各不相同,在解决问题时的侧重 点也不尽相同。因此需要对它们进行分类整理,综合分析,使 EJB 开发人员能够在实际项 目中选择最合适的设计模式。 EJB 设计模式》一书正是以这种方法来介绍 EJB 设计模式的。这本书分为两大部份, 第一部介绍了 EJB 设计模式语言,包括系统架构设计模式、层内数据传输模式、事务和持 久性模式、客户端 EJB 交互模式以及主键生成策略等;第二部分介绍了从需求到模式驱动 的设计、用 Ant 编译与用 JUNIT 进行单元测试的 EJB 开发过程、实体 Bean 的替代物以及 众多的灵活小巧的设计技巧等。这本书通俗易懂,书中述及的 EJB 设计模式都从实际的 EJB 项目中抽取的,所以它们非常实用。通过使用这些设计模式,使得系统的质量得以提高,并 且更加易于被熟悉这些模式的开发者所理解。 本书的作者 Floyd Marinescu EJB 设计模式方面的著名专家,他领导了 EJB 设计模 式项目。并且和另外一名 EJB 领域的专家 Ed Roman (《精通 EJB》一书的作者)一起成立 了一个培训和咨询公司——中间件公司(MiddleWare),以帮助开发者掌握企业 Java 技术。 我们在翻译本书的过程中力求终于原著。对于本书中出现的大量的专业术语尽量遵循标 准的译法,并在有可能引起歧义之处著上了英文原文,以方便读者的对照理解。 全书的翻译由饶若楠、陈昊鹏和张保稳合作完成。由于我们水平有限,书中出现错误与 不妥之处在所难免,恳请读者批评指正。

Upload: others

Post on 05-Jun-2020

22 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

译者序 随着 J2EE 技术的日益流行,越来越多的企业都选择使用 J2EE 技术来构建它们的应用

系统,但是,EJB 技术是一项高端技术,如何构建高效、安全和可靠的基于 EJB 的企业应

用系统就成为了软件开发者要面临的一个重大挑战。很多有经验的 EJB 系统开发人员在他

们的实践过程中总结出了许多可以提高系统性能的用于 EJB 编程的可重用方法,从而形成

了 EJB 设计模式。这些设计模式可以帮助其他的 EJB 开发人员提高项目的质量,缩短项目

的开发周期。但是 EJB 设计模式种类众多,所针对的问题各不相同,在解决问题时的侧重

点也不尽相同。因此需要对它们进行分类整理,综合分析,使 EJB 开发人员能够在实际项

目中选择 合适的设计模式。 《EJB 设计模式》一书正是以这种方法来介绍 EJB 设计模式的。这本书分为两大部份,

第一部介绍了 EJB 设计模式语言,包括系统架构设计模式、层内数据传输模式、事务和持

久性模式、客户端 EJB 交互模式以及主键生成策略等;第二部分介绍了从需求到模式驱动

的设计、用 Ant 编译与用 JUNIT 进行单元测试的 EJB 开发过程、实体 Bean 的替代物以及

众多的灵活小巧的设计技巧等。这本书通俗易懂,书中述及的 EJB 设计模式都从实际的 EJB项目中抽取的,所以它们非常实用。通过使用这些设计模式,使得系统的质量得以提高,并

且更加易于被熟悉这些模式的开发者所理解。 本书的作者 Floyd Marinescu 是 EJB 设计模式方面的著名专家,他领导了 EJB 设计模

式项目。并且和另外一名 EJB 领域的专家 Ed Roman(《精通 EJB》一书的作者)一起成立

了一个培训和咨询公司——中间件公司(MiddleWare),以帮助开发者掌握企业 Java 技术。 我们在翻译本书的过程中力求终于原著。对于本书中出现的大量的专业术语尽量遵循标

准的译法,并在有可能引起歧义之处著上了英文原文,以方便读者的对照理解。 全书的翻译由饶若楠、陈昊鹏和张保稳合作完成。由于我们水平有限,书中出现错误与

不妥之处在所难免,恳请读者批评指正。

Page 2: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

前 言

许多具有良好体系结构的 EJB 项目都使用了设计模式,不管开发者当时是否意识到他在使

用设计模式。通常,开发者在项目开发过程中会考虑他们的 优方案,他们并没有意识到这些

优方案实际上就是设计模式――用于编程的可重用方法。设计模式对其他开发者的项目开发

也颇有益处。 这就是这本书中述及的 EJB 设计模式的妙处所在――它们是从真实的 EJB 项目中抽取的

实用的现实世界内的模式。J2EE 社区鼓励成员们去到 Serverside.com 上分享他们的模式。 Floyd Marinescu 是 EJB 世界里设计模式方面的带头的专家,他领导了本 EJB 设计模式项

目。Floyd 和我在过去几年中一直在一起工作。我们成立了中间件公司(MiddleWare)。该公司

是一个培训和咨询公司,帮助开发者掌握企业 Java 技术。 在中间件公司,我们为现实世界中的项目提供咨询,帮助提高设计模式的质量。我们也曾

经向诸位这样的开发者讲授 EJB 设计模式方面的培训课程,同时这些课程的反馈显著提高了本

书的质量。 在这本书中,Floyd 将向大家展示许多的 EJB 设计模式,它们可以用于提高 EJB 项目的质

量。在进行合理的判断后,通过使用这些设计模式,我们可以改善体系结构的质量,使得代码

更加雅致和可重用,使得系统易于被熟悉这些模式的开发者所理解。 关于这本书的很好的一点是它易于理解。Floyd选择了一种叫做Alexandiran形式的容易理解的

写作模式的形式。这使得任何懂得用EJB编写程序的人都可以容易地阅读本书。(如果你还不了

解EJB,你或许想读一下另一本拙作,《精通EJB(第二版)》(Mastering Enterprise JavaBeans, Second Edition,)。在书店和serverside均可找到它。)另一个选择是你也可以参加一个EJB的培

训课程,比如中间件公司提供的课程之一。 当你准备开始阅读时,你将获益匪浅。Floyd 用了整整一年时间解决 EJB 设计模式的概念

问题,这将对整个 EJB 团体都有益处。能和 Floyd 在这个项目上一起工作,我感到很荣幸。同

时我也学到很多东西。相信你也会的。 Ed Roman The Midlleware Company 的 CEO 精通 EJB 一书的作者。

Page 3: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

简 介

这本书关系到我们的生活质量。无论是你是一名开发者、系统架构师还是项目经理,在一

天结束的时候,我们都会为在构建和发布设计精良的应用时没有产生代价高昂的错误,没有工

作到深夜和没有经历那种长达数月的压力而感到高兴。我们都是普通人,都希望在一天结束的

时候看到项目在按进度进行,并且回家后有充足的时间去干任何我们喜欢干的事情。 遗憾的是,当使用诸如Java 2 Enterprise Edition(J2EE)这类正处在发展阶段的新技术时,

设计精良的应用并不容易实现。在相对较新的领域内,总是非常缺乏有关设计优良系统的知识,

开发者要么低效率地从头开始努力学习,要么每天都在项目上产生很多代价高昂的错误。如果

没有任何有条理的 优方法集可遵循,那么EJB开发者的工作将十分困难。学习优良的设计方

法在技术上对于初学者来说尤其困难,他们中的许多人在这之前从来就没有构建过分布式系统,

并且不理解影响分布式系统设计的 基本需求。 使事情变得更糟的是,EJB规范从一个版本到另一个版本之间的变化总是会给在设计优良

的EJB系统时所使用的设计方式带来显著的变化。特别是随着EJB2.0规范的引入,甚至在大多

数 近出版的有关EJB的书中所讨论的多年来积累的 优方法都将不再适用,或者达不到相同

的目的了,使用这样的 优方法只能导致设计出的系统非常拙劣。 这本书的内容(以及Middleware 公司的“EJB for Architects” 课程的内容,它被用于教

授这本书中所描述的模式)所关注的是传播精妙的设计思想,提高开发者设计的应用的质量,

从而提高开发者自己的生活质量,我们的 终目的是希望能够帮助你学习在当今业界中被使用

的顶尖的设计策略,使得你能够快速地设计出高效的、可扩展的和可维护的系统。 我们希望使用模式(Pattern)这种机制来给你传授设计知识。

什么是模式?

我喜欢将模式看作是对一个常见的重复性问题的 优方法解决方案。也就是说,一个模式

说明并解释了一个在设计和实现一个应用时所产生的重要的或者是具有挑战性的问题,并且讨

论了对这个问题的 优方法的解决方案。随着时间的推移,模式开始体现出产生它的业界的集

体知识和经验。例如,在这本书中的模式表示了成千上万的来自TheServerSide和业界的EJB开发者的知识,他们的意见和评论为这本书提供了素材。

模式的好处

模式当然有很多的用处,但是下面是一些可以帮助促进诸如J2EE这样的新软件平台成熟的

重要的好处: 帮助提供了一种为讨论设计问题而使用的高级语言。EJB开发者可以使用在这本书中的模式

名字来高效地共同讨论实现细节。想象一下描述一个应用是使用无状态会话façade(stateless session façade) 模式构建的比设法解释会话 bean 怎样包装实体 bean 的所

Page 4: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

有语义要快多少。 预先提供了许多设计工作。一个编写优良的模式详细讨论了需要解决的问题,并且通过正

反两方面的讨论,说明了问题应该如何被解决,以及其它应该被了解的问题。通过阅读一

个模式,可以预先讨论和考虑许多有挑战性的并且是潜在的、被隐藏的问题。 模式的组合有助于可复用的体系结构。模式总是在彼此之间互相引用和依赖。这种模式之

间的连接适合于创建一种被称为模式语言(pattern language)的东西:即一系列互联的模

式,作为一个整体,通常建议将其作为一个应用的完整的体系结构。这样,当阅读这本书

时,某些互联模式的集合将形成一个可以被反复应用于多个项目的可复用的体系结构。 特别是在这本书中,我们选取的焦点是与EJB相关的非常底层的模式,也就是说,不同于可

以被应用于多种技术的文档通用的抽象模式。我们将焦点放在怎样完成EJB开发上,讨论与具

体EJB相关的问题和与具体EJB相关的实现的复杂性。这样,在这本书中,我们与许多其它的

通常是说明一个模式的具体实现的模式书不同(那些模式不是与具体的项目相关的),我们的

目标是为您——EJB开发者/体系架构师提供在基于EJB/J2EE的应用中使用那些模式所必需的

所有信息。

模式的起源

对许多人来说,模式首先是通过里程碑式的书籍 design pattern:elements of reusable object-oriented software(Gamma,et al,1994)介绍给他们的。尽管这本书并不是第一本关于软件

模式的书,但是它做出了积极的努力,将模式的概念和使用引入到了软件开发的主流中。 模式的实际起源始于1994年出版 Design Patterns 的很久之前。模式是Christopher

Alexander第一个描述的,并且在20世纪70年代应用于城镇和建筑物的结构/体系中。在A Pattern Language(1977)这本书中,Alexander写道:“每一个模式都描述了一个在我们周围

的环境中一次又一次地重复发生的问题,并且描述了对这个问题的解决方案的核心内容,这样

你可以成百万次地使用这个解决方案,但是从来没有以相同的方式来使用它两次”。 模式是一种优秀的用来组织在生活中任何领域内的知识和解决方案的方法,而不仅仅只是

民用工程和软件设计领域。模式的结构和本性使它们能够很好地适用于对知识进行分类。好的

模式不仅说明了解决问题的方法,并且被构建在一个有助于解释问题及其解决方案的各个方面

的样式(style) 内。

在这本书中使用的模式风格

本书采用了一种与Alexander所使用的、称为Alexandrian 格式(Alexandrian form)的原

始样式相似的样式来描述模式。我使用的格式由一种以类似散文的格式编写的模式构成,它被

对一个问题及其解决方案的描述分割成如下的段落: 应用上下文(Context):几个描述有关模式应用上下文(context) 的句子。 问题(Problem):一个问题,用来说明这个设计模式将要解决的难题。 限制(Forces):详细解释context和problem的若干段落,它们解释了在需要一个解决

方案的工作中所存在的许多限制,这里读者将完全理解为什么需要这个模式。 解决方案(Solution):几个介绍性的句子,用于介绍这个作为上面所描述问题的解决

方案的模式。

Page 5: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

解决方法描述(solution description):描述这个解决方案的若干段落,包括对模式和

在EJB中的实现问题的从正反两方面、从高层到低层的解释。

相关联的模式

相关联的模式可能会交叉引用在这本书中的其它模式,否则将指出在其它资源中的相同或

相似模式,它们将在这本书的"参考(Refernces)"部分详细介绍。 本书使用散文式的而非点式(Point)的格式来构建模式数据,这使得在本书中用到的

Alexandrian格式和样式与大多数其它流行的软件模式书不同。这本书的目标之一就是使有经验

的架构师和初学者都可以读懂一个模式并且完全理解它。我努力实现这个目标的方法是把一个

模式编写为流动的散文式的格式(fluid,prose format),把读者由问题引导到解决方案,在所

有的必要点上都一步一步地解释,强调的是以一种简单而愉快的形式学习模式。 在使用非Alexandrian 格式时,模式会被分解为彼此互相分离的部分,其中每一部分都使

用点格式(point form)进行描述。我自己关于这些样式的观点是它们总是将注意力放在把尽可

能多的信息打包在每一个点中,这使得随意地阅读和学习变得很困难,因为这些信息总是被以

一种更适合于参考而非点对点阅读的方式来分类。这些模式书对于参考目的是很优秀的,但是

这本书的目标是将模式教授给有经验的架构师以及没有经验的开发者,所以必须选择一种有助

于学习的格式,Alexandrian 格式看起来 适合这个目的。

这本书是如何组织的

这本书在组织上可以分为两部分:第一部分,“EJB模式语言(EJB Pattern Language)”是EJB模式的目录,详细介绍了20种模式,第二部分,“EJB 设计和实现的 优方法(Best Practices for EJB Design and Implementation)”,提供了对教授其它主题的 优方法进行支持

的章节,例如,应用模式和实际实现基于EJB的系统。我们的目标不仅是要给读者一个模式的

目录,还要给读者掌握这些模式的知识和完成工作所必需的工具。在第二部分同时还包括一个

用于描述实体 Bean的替代物的章节,它给EJB开发者提供了使用java 数据对象(Java Data Object,JDO)来持久化一个对象模型的前景,还有一章描述了粒度更细的,因为其太小而难以

成为一个完整模式的设计技巧和策略。

谁应该读这本书?

为了让这本书能够在技术上深入讨论高级EJB话题,我们假设读者在读这本书之前已经对

EJB的基础知识有了很好的了解。我特别推荐Ed Roman的Mastering Enterprise JavaBeans,Second Edition(Wiley 2001),它对读者来说,是一本学习EJB基础知识和高级应用的优秀书籍。

EJB Design Patterns 初是Ed Roman的书中的一章,但是作为一章它的长度不断地增加,于

是我们决定将它独立成书。另一个获得这本书的背景知识的好方法是获取由Middleware公司提

供的Mastering EJB类,或者你可以通过参加Middleware公司的EJB for Architects课程来学习这

本书中所有的模式。 尽管这本书要求读者要了解有关EJB的知识,但它并不是仅针对有经验的架构师的。这本

书的风格和在模式中所使用的语言可以同时被入门级的读者和有经验的架构师所掌握。特别是,

Page 6: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

在这本书的撰写过程中遵从了下面三个原则: 1.某些刚刚读过Ed Roman的Mastering EJB或其它EJB书的读者应该能够在没有什么太多

困难的情况下就通读和掌握这些概念。 2.本书的内容是足够核心的而且在技术上是足够深入的,这使得本书即使对专家来说都是

吸引人的读物。 3.本书的每一部分所回答的问题都比它提出的问题要多得多。 这三条原则被当作这本书的灵魂,其中,原则3可以保证我们能够给您提供在解释一个令人

费解的话题时所必需的背景知识,而原则1还使得本书重复了一些对EJB的介绍性的文本,原则

2则保证我们将焦点聚集在讨论那些在别的书中通常没有提到的,然而却是 有用和 重要的话

题上。

在 Web 站点上有什么?

与这本书相伴的Web站点——www.theserverside.com/pattern/ejbpatterns——包含了这

本书中运行和编译的源代码例子,以及一个读者论坛。这个web站点将很有希望不断地发展下

去,因为随着时间的推移,读者团体会不断地向这个web站点加入更多的模式。

小结

这本书是一本与任何其它的模式书都不同的模式书。你将不止学习到很多有价值的、基础

的模式,它们可以帮助你提高你所编写的基于EJB的应用的质量,而且还将学习到怎样去接受

这些模式的知识并以用例驱动的方式应用它们。同时,你将学习到许多关于如何去实现已经完

成设计的应用的 优方法。 如果这本书对你的事业和你正在从事的项目的质量有所贡献的话,我将为此而感到高兴。

如果这本书能够通过提高你的项目的效率而使你的整个生活质量大受裨益,那么它就是成十倍

地完成了任务。

Page 7: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

致 谢

我要感谢 ED Roman 和 Agniesza Zdaniuk,没有他们的信任,本书不可能完成。 我要感谢 Florin 和 Carmen Marinescu,他们很早就教我什么是重要的。 特别感谢 Aravind Krishnaswamy 和 Mark TurnbUll,他们帮助我履行了生活中的其它一些

责任,使我可以为本书投入更多的时间。

文本贡献者

我要感谢 Randy Stafford,他在第七章关于开发过程方面的贡献,感谢 Craig Russell,他

为非实体 beans 的章节提供了 JDO 方面的材料。

代码和模式思想的贡献者

Ricahrd Monson——Hefel 提出了使用行集来进行数据传输。 Doug Bateman 提出了为自动生成关键字使用存储过程的初始建议。 Steve Woodcock 提出了 EJB 模式的 UUID 的概念和代码。 Stuart 提出了通用属性访问的思想。

模式指导

我要感谢 Markus Voelter,Ralph Johnson,并特别感谢 Bobby Woolf,没有他们早期关于

模式形式的建议,本书的内容将十分地混乱。 没有 Server.side.com J2EE 团体的评论、建议、修正、质疑,EJB 设计模式一书不可能完

成。他们用八个月的时间审阅了拙作。他们有: Alex Tyuin,Alex Wologodzew,Allan Schweitz,Alun R.Butler,Andre Cesta,Andre

Winssen,Andreas Kruger,Andy Stevens,Andy Turner,Ankur Kumar,Anthony Calafano,Anuj Vohra,Anup Kumar Maliyackel,Aparna Walawalkar,Ashim Chakraborty,Babur Begg,Ben Beazley,Bill Ennis,Billy Newport,Brain Dobby,Brian Weith,Carmine Scotto d'Antuono,Cecile Saint-Martin,Chad Vawter,Chandra Mora,Charles N.May,Colin He,Constatin Gonciulea,Cristina Belderrain,Curt Smith,Dan bereczki,DanZainea,Daniel F.Burke,Daniel Massey,Darrow Kirkpatrick,Dave Churchville,David E.Jones,David Ezzio,David

Page 8: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

Ziegler,Dimitri Rakitine,Dimitrios Varsos,Dion Almaer,Doal Miller,Don Schaefer,Donnie Hale,Eduard Skhisov,Emmanuel Valentin,Engstrom Anders,Erez nahir,Faisal,Aveed,Fernando Bellas Permuy,FM Spruzen Simon,Forslof Mats,Frank Robbins,Frank Sampson,Frank Stefan,Fried Hoeben,Gabriela Chiribau,Ganesh Ramani,Geert mergan,Gene Mckenna,Geoff Soutter,Gjino Bledar,Gunnar Eikman,Hai Hoang,heng Ngee Mok,Hildebrando Arguello,Hossein S.Attar,Howard katz,HUun-An Nguyen,Iain McCorquodale,J.D.Berton,James Hicks,James kelly,Janne Nykanen,Jean Safar,Jean-Pierre Belanger,Jeff Anderson ,Jerome Beau,Jesper Anderson,John Ipe,Jonathan Asbell,Jorg Winter,Joseph Sheinis,Juan-Francisco Borras-Correa,Julian Chee,Junaid Bhatra,Justin Leavesly,Justin Walsh,Ken Hoying,Ken Sizer,Krishnan Subramanian,Kristin Love,Kyle Brown,Lance hankins,Larry Yang,Laura Fang,Laurent Rieu,Leo Shuster,M Heling,Madhu Gopianathan,mark Buchner,Mark L.Stevens,martin Squicciarini,Matt Mikulicsfagerstrom,

Mohan Radhakrishnan,Mohit Sehgal,Muhammad farhat kaleem,Muller laszlo,Murray Knox,Nakamura Tadashi,nicholas Jackson,Nick Minutello,Nick Smith,Niklas Eriksson,Oliver kanmps,Oliver Brand,Partha Nageswaran,Patrick caulfield,Paul Wadmore,Paulo Fereira de Moura Jr.,Paulo Merson,Peter Miller,Pontus Hellgren,Raffaele Spazzoli,Rais Ahmed,Rajesh Jayaprakash,Reg Whitton,Richard Dedeyan,Rick Vogel,Robert McIntosh,Robert NIcholson,Robert O'Leary,Roger Rhoades,Roman Stepanenko,Sammuel Santiago,Sashi Guduri,Scot McPhee,Scott Chen,Scott Stirling,Scott W.Ambler,Sebastien Coutriaux,Sergey Oreshko,Shorn Tolley,Simon Brown,Simon harris,Simone milani,Stefan Piesche,Stefan Tilknov,Tarek Hammoud,Taylor Cowan,Terry Griffey,Thanh C.Bryan,Therese Hermansson,Thierry Januady,Thomas Bohn,Toby Reyelts,Tom Wood,Tracy Milburn,Trond Andersen,Tyler Jewell,Udaya Kumar,Vaheesan Selvarajah,Vincent Harcq,Yagiz Erkan,Yi Lin,和 Youself Syed。

后,我要感谢那些对我生活产生积极影响从而间接地为本书的出版作出贡献的人。他们

是: Nitin Bharti ,Sudeep Dutt,Morrissey, Calvin Broadus,George Kecskemeti,Johnny

marr,Geoff McGuire,Andre Young,Katerina Illievska,Chogyam Trungpa,Siddhartha Gautama,Nadia Staltieri,Dale Carnegie,Lao-Tzu,David Gahan,Bogdan 和 Andre Cristescu,Umar Sheikh,Robert Smith,Ursula 和 Suzanna Lipstajn,James McDonald,Jacob Murphy,Olivia Sasa Nikolic,Deanna Ciampa,Aravind Krishnaswamy,Mikola Michon,Mark Turnull,Laura Ilisie,Gregory Peres,Struart Charlton,和 Carlos Martinez。

Page 9: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

关于为本书作出贡献的人

Randy Stafford 是一个在软件开发方面很精通的专家,在金融服务、数据库管理

系统软件、医院、无线通信、交通、CASE 和航天以及国防系统等多个信息技术的领域有着 15年的经验。他有着作为咨询师或者永久雇员的广泛的阅历,这些公司大的如 Travelers Express,Oracle,AMS 和 Martin Marietta,小的如 Gemstone,SynXis,和 Ascent Logic 企业。作为

Smaltalker 的开发者,他自 1998 年起就专注于面向对象和分布式系统的开发,并且自 1995 年

起还加入了 Web 开发和电子业务的项目。 现在,作为 IQNaviagtor 公司的首席体系师,Stafford 先生自 1997 年来使用不同的 J2EE

应用服务器和Java ORBs开发了8个分布式产品的 java应用。他是Gemstone系统公司的J2EE范例 FoodSmart 的创始人和构造者。他也是 Gemstone 关于设计 J2EE 应用的模式语言的设计

者,是 J2EE 应用框架之一 GemStone 专业应用服务基础类的设计者。他曾在 2000 年夏回溯

他的后 5 个 J2EE 应用的 初发行版,用 Ant 构建了它们的自动版本。从 初发布 JUnit 的 1998年初开始,他一直在使用它,并且从 1995 年开始,他还使用了 JUnit 的前身 SmalltalkUnit。

Stafford 先生是 Colorado 州立大学的校友,获得过应用数学的理学学士学位和计算机系的

Coursework 研究生。他在面向对象的仿真和系统工程文献中赫赫有名。 Craig Russell 是 Sun 微系统公司的产品体系架构师,负责一个对象关系映射引擎

--透明持久性的体系结构。在过去的 30 年中,他从事过企业规模的分布式事务和数据库系统

的构造、设计和支持。 Craig 是Java Data Object的规范领导者,该规范是针对以Java为中心的持久性的,通过

Java Community Process 作为Java规范请求之一进行管理。

Page 10: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

第一章 EJB 层的体系结构模式

在最初着手设计 Enterprise JavaBean (EJB)系统时,选择一个恰当的体系结

构,或者是进行逻辑的划分,来满足项目所关注的诸如性能﹑可维护性﹑可移植性等方面

的要求,将是开发者要面对的最困难的问题之一。本章包括目前在业界使用的一些基本的

体系架构模式,具体的是:

会话表面层(Session Façade)

它是所有的 EJB 设计模式中被使用得最广泛的一种,会话表面层说明了在强制

用例在一个网络调用和一个事务中执行的情况下,怎样在你的系统中恰当地划分业务

逻辑以使其有助于将客户端(Client)与服务器(Server)之间的依赖降到最低。

消息表面层(Message Façade)

消息表面层模式讨论了何时和怎样为具有异步特征的用例划分逻辑。

EJB 命令(EJB Command)

它是会话 表面层的对立物,EJB 命令模式提倡把业务逻辑置于轻量级的、简单

的 Java 命令 bean(java command bean)之中。这个模式的主要好处是将客户

端与 EJB 自身以及用例在一个网络调用和事务中的执行过程完全分离开来。

数据传送对象工厂(Data Transfer Object Factory)

指出了将 DTO(data transfer object,数据传送对象)的创建/消费逻辑

置于实体 bean 中的旧惯例的缺点,并且描述了其解决方案:把数据传送对象的创建

和消费逻辑集中到一个单一层中(用会话 bean 或简单的 Java 工厂(factory)

来实现)。

通用属性访问(Generic Attribute Access)

这个模式讨论了何时和怎样为实体 bean 的属性提供一个通用的域接口,这个

接口是为了保证可维护性和性能而被创建的。

业务接口(Business Interface)

这个模式说明了怎样实现一个接口实现方案,它可以提供对 Remote/Local 接

口和 EJB bean 类中的方法签名的编译时刻的检查。

1

Page 11: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

会话表面层(会话 Façade)

为了完成具体用例,一个 EJB 的客户端需要去执行业务逻辑。

EJB 客户端怎样才可以只在一个事务和一个网络调用中执行用例的业务逻辑呢?

为了执行一个典型用例的业务逻辑,经常需要访问并可能会修改多个服务器端对象

(例如会话和实体 Bean)。问题是多个细粒度的对会话和实体 Bean 的调用在只增加了

少量可维护代码的同时也增加了多个网络调用(也可能是多个事务)的开销,这是因为数

据访问逻辑和工作流/业务逻辑需要被散布到各个客户端上。

考虑一个在线银行的实例,其中一个 servlet 接收代表 web 客户的把资金从一个账

户转移到另一个账户的请求。这个例子中(如图 1.1),servlet 必须检查用户身份以确

保他是被授权的用户,然后从一个银行账户的实体 bean 中提取资金,并且把它们存入另

一个账户的实体 bean 中去。

图 1.1 客户端在实体 bean 之间转移资金

当我们执行在实体 bean 的 home 和 remote 接口中的方法时,上述这种方式在负载

较大时是不具备可扩展性的,因为整个过程至少需要六个网络调用:三个用来查找恰当的

实体 bean,另外三个用来实际转移资金。而且,因为实体 bean 本质上是具有事务性

的,所以每一个对实体上的方法的调用都会请求一个在服务器端的独立事务,用来处理远

程实体与它底层的数据存储之间的同步,并且为应用服务器执行维护工作。

更糟的是,这种方式不能确保客户资金的安全。如果在存钱时发生了某种故障,而资

金已经从一个账户中划走了,那么这笔钱就将丢失。因为用户授权检查、资金存入和提取

都是互相完全独立地运行的,所以如果存钱操作失败,取钱操作将不会回滚,这将造成状

态的不一致。问题在于,当直接调用一个实体 bean 的方法时,每一个方法调用都是独立

的工作单元和独立的事务。

一种解决方案是在我们的实体 bean 中加入附加的逻辑,它可以为一个单一客户端调

用执行多个操作。这种方法会带来维护问题,因为随着时间的推移,我们的实体 bean 层

可能会以各种不同的方式被使用,如果我们在每次需要提高实体 bean 的性能时,都在其

中加入应用逻辑,那我们的实体 bean 就会迅速变得臃肿并且难以阅读、维护和重用。虽

然我们有效地合并了持久性逻辑(名词)和应用逻辑(动词),但这仍然只是一种拙劣的

2

Page 12: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

应用设计。

另一种方法是由客户端通过 Java Transaction API (JTA,Java 事务 API)来

划分一个聚合的、大型的事务,这将使若干个实体 bean 方法调用在同一个事务中以

all-or-nothing 的模式被执行。这样,如果存钱操作失败,那么取钱操作将回滚,用

户的钱也就变得安全了。但是,这种改进模式也有很多缺陷:

高昂的网络开销。我们仍然有六个网络调用要处理,这将延缓处理速度(除

非我们使用 local 接口)。

极差的并发性。如果客户端离服务器很远的话 (即一个 applet 或应用与

一个远程的 EJB 系统进行交互,甚至要通过因特网或防火墙),那么这个事

务会持续很长一段时间,这将引发过度上锁,增加了冲突和死锁的概率,并

且降低了其它客户端访问相同的实体 bean 实例的并发性。

过高的耦合性。我们的客户端是直接根据将客户端与实体 bean 紧密耦合在

一起的实体 bean API 编写的,如果实体 bean 层在将来需要改变的话,

那么必须同时修改我们的客户端。

极差的可重用性。执行转账的业务逻辑是直接嵌入在客户端的,因此嵌入它

的那个客户端可以有效地使用它,而其它类型的客户端(Java 应用,

applets, servlets 等)就无法重用这个业务逻辑。这种表示逻辑与业务

逻辑的混合对任何一个正式的发布而言都是一种拙劣的应用设计。

极差的可维护性。使用 Java 事务 API 将会使执行事务的中间件逻辑与应

用逻辑缠绕在一起,相比之下,通过声明式的事务将二者分离开将会显得更

清晰,因为这样我们就可以任意修改中间件而不会影响业务规则。

极差的开发角色划分。对于一个大型项目,一个通用的惯例是将表示逻辑程

序员(如 servlet/jsp 开发者)和业务逻辑/中间件程序员(EJB 开发

者)的开发任务分离。如果业务逻辑被编码在客户端/表示层,那么清晰的

角色分离将不可能实现,也就是说如果它们都被编码在表示层中,那么业务

逻辑和表示逻辑的程序员将无法区分开来。

特别要强调的一点是我们需要一个服务器端的抽象,作为对实体 bean 调用的一种介

质和缓存,而会话 bean 就是为此而设计的。

因此:

将实体 Bean 层包装在被称为会话表面层(session façade)的会话 Bean 层

中,客户端只能访问会话 Bean 而不是实体 Bean。

通过只提供给客户端一个单一的可访问点——会话 Bean 层,会话表面层模式将传统

的表面层模式的长处应用到了 EJB 中,它对客户端完全隐藏了存在于服务器端的对象模

型。图 1.2 向我们展示了采用这种方式后体系结构是怎样被改进的。会话 表面层还新增

加了如下优点:强制一个用例在一次网络调用中执行,并提供了一个封装了用来完成用例

的业务逻辑及工作流逻辑的层。会话表面层经常被作为一个无状态会话 Bean 层来实现

(虽然这部分也可以用有状态会话 Bean 来实现)。

3

Page 13: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

图 1.2 会话表面层在体系结构方面的优势

为了举例说明这个范例是怎么工作的,以及它的优势在哪里,让我们在来看看前面的

例子。我们的有关资金转帐的业务逻辑用例将被置于会话 Bean 中,它有一个方法称为

transfer-Funds(userpk, accountpk, accountpk, amount) , 这 样

BankTeller 会话 Bean 就可以在 User 和 Bank Account 上执行大量的操作,正如图

1.3 所示那样。

图 1.3 会话 表面层在性能方面的优势

因为 BankTeller 会话 Bean 是与 User 和 BankAccount 实体 bean 并置的,

所以它应该被硬编码为通过实体 bean 的 home 接口与实体 bean 进行通信,这样,执行

这个用例所需的网络开销就被减少到了只有一个调用(从客户端到 BankTeller 的调

用)。同时,通过在 BankTeller 的发布描述符中增加一个 TX_REQUIRED 的事务属性

设置,使得所有的实体 bean 层的更新都运行在由 BankTeller 启动的事务中。这样就把

整个用例有效地包装在一个事务中了,从而确保了所有实体 bean 的更新都运行于在执行

BankTeller 的 transfer-Funds 方法时所启动的事务中。

会话 表面层模式是当今正在使用的最基础的 EJB 模式(这也是为什么把它作为本书

的第一个模式的原因),它不但能提供性能上的好处,而且对你的 J2EE 应用的 EJB 系统

4

Page 14: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

划分也提出了一种标准的体系结构,其中,客户端和服务器之间的边界被一个会话 Bean

层分开,它的方法将映射为应用中的所有用例,并且包含了这些用例的业务逻辑。

进一步看看 BankTeller 的例子,在一个银行应用中显然涉及比简单的资金转账更多

的用例。使用会话表面层模式,可以在创建会话 bean 时将具有相同功能的用例聚集在同

一个 bean 中。这样,我们就可以在 BankTeller 中添加一些银行的其它附属操作(比如

withdrawFunds,depositFunds,getBalance())。在银行应用的其它方面,那些不

同目的的用例也将被聚合到一个会话 bean 中去。例如,每个银行都有信贷部(Loans

Department),这个需要对信贷部的操作建模的用例与 BankTeller 的用例就不相关;

因此,它们将被聚合到一个 LoanServices 会话 bean 中去。同样的,一个银行应用还

需要另一个会话 bean 来封装与投资有关的用例。在使用会话 表面层模式时,这个银行

应用的体系结构布局图如图 1.4 所示。

图 1.4 将用例聚合在会话 bean 中的体系结构布局

会话表面层模式工作得非常出色,但它容易被滥用。我们会发现在一个项目中误用会

话表面层模式是很常见的现象:

创建了一个会话 bean 的万能类(God-class)。开发者常常会将系统中

所有的用例都放在一个会话 bean 中,这将会导致产生一个臃肿的会话

bean,并且降低了开发效率,因为所有的开发者都需要访问这一个类。这样

的会话 bean 应该被划分为将相关的用例聚合在一起的若干个会话 bean。

把域逻辑放到了会话 bean 中。一个设计良好的面向对象的域模型应该包含

了你的应用中的所有业务/用例逻辑(Fowler 2001)。大部分会话 表面

层模式的方法应该只是简单地指代给恰当的实体 bean,除非这个用例所涉

及的工作流逻辑需要在多个不同的、可能非直接相关的 bean 上进行操作。

在表面层中重复了业务逻辑。随着项目开发的深入,在会话 bean 的方法中

5

Page 15: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

经常会包含重复的代码,比如 CheckCreditHistory 的执行逻辑,它可以

是任意数量的用例的工作流的一部分。解决方案是增加一个服务层(作为会

话 bean 或简单的 java 类来实现),它封装了那些可重用的且独立于用例

的业务逻辑,这个服务层对于客户端来说是不可见的。因为我们的项目越来

越大,所以把重复的逻辑提取出来置于一个经常使用的会话中是相当有用

的。

以下是会话表面层模式带来的好处:

较低的网络开销。在增加了一个方法调用需要穿越的、额外的会话 bean 层

的同时,客户端却只需一个而不是六个网络调用就可以完成转账了。在服务

器端,会话 bean 与实体 bean 通过其 local 接口互相通信,这不会增加

任何网络开销,甚至对于那些只使用 remote 接口的实体 bean,大部分应

用服务器都会优化它们这些并置的 EJB 之间的通信。

业务逻辑与表示层逻辑清晰而严格的分离。通过使用会话表面层模式,执行

业务逻辑所需的方法完全被包装在会话 bean 的方法之中,EJB 的客户端只

需要考虑表示层的问题就可以了,它为了完成一个工作单元而在一个 EJB 上

所执行的方法永远不会超过一个,这样业务逻辑和表示层逻辑就被严格地分

离开了。

事务完整性。我们的会话 bean 封装了在一个事务中完成银行转账所需的所

有逻辑。因此,这个会话 bean 起到了一个把事务都限制在服务器端,并使

其尽量简洁的事务性表面层(transactional façade)的作用。事务可

以在“会话 bean 方法”这一级别上被划分边界,也可以通过发布描述符进行

配置。

较低的耦合性。会话 bean 缓冲了客户端与实体 bean 之间的请求。在以

后实体 bean 层需要修改时,我们可以避免改变客户端,因为客户端是通过

会话 bean 层间接访问实体 bean 的。

良好的可重用性。我们的 BankTeller 逻辑被封装在一个模块化的会话

bean 中 , 它 可 以 被 任 何 类 型 的 客 户 端 访 问 ( JSP, servlet,

application, or applet)。在会话 bean 中封装应用逻辑意味着我们

的实体 bean 可以只包含数据及数据访问逻辑,使得它们可以被在相同的甚

至是不同的应用中的多个会话 bean 重用。

良好的可维护性。我们必须在 BankTeller 会话 bean 的发布描述符中声

明性地定义事务,而不是通过 JTA 编程来实现。这样可以把中间件和应用逻

辑清晰的分离开来,从而增加了可维护性并且降低了出错的可能性。

清晰的动词-名词(verb-noun)的分离。会话 bean 层对应用中具体的用

例建模,即我们的应用中的动词(verb);而实体 bean 层对业务对象建

模,即我们的应用中的名词(noun)。这种体系结构使得我们可以很容易地

从需求文档中将用例映射到实际的 EJB 体系结构上。

会话表面层模式是 EJB 开发中所使用的主要模式,它坚持高效及高度可重用的设计原

则,同时将表示逻辑(客户端)、业务逻辑(会话 表面层)和数据逻辑(实体 bean

等)清晰地分离开。会话 表面层描述了一个对实现任何类型的用例都很有用的体系结

构;然而,如果一个用例具有异步的特征,消息 表面层将提供另外一种更加具有可扩展

性的方法。

6

Page 16: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

相关联的模式:

消息表面层 (Message Façade)

数据传送对象(Data Transfer Object)

会话表面层(Session Façade)(Alur, et al., 2001)

会话表面层(Session Façade )(MartinFowler.com)

7

Page 17: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

消息表面层(Message Façade)

如果一个企业 Java bean 的客户端想调用在一个用例上下文(context)中的多

个 EJB 上的方法,但是并不要求服务器立即响应它的请求,那么:

一个 EJB 客户怎样在一个事务中调用多个会话或实体 bean 上的方法,而不需要阻塞

并等待每个 bean 的响应呢?

特别是在大型系统中,可扩展性规定一个用例的业务逻辑是独立于客户端的业务逻辑

而执行的,并不要求客户端等待执行的完成。这种类型的行为称为异步行为,它允许客户

端与用户界面(UI)以最大的响应时间进行交互,因为当它们所启动的用例在执行时,它

们不用等待。这种方式允许大型系统进行扩展,因为用例可以被排队和批处理,这个过程

对用户是透明的,用户可以立即移动到用户界面的下一部分。如果被排队积压的用例已经

开始开发了,那么系统中实际执行用例的部分也可以进行扩充和系统升级,而不会改变为

客户端提供的服务的质量与可用性。

考虑一个简单的基于 web 的航班订票系统,其中一个 servlet 接收为一个用户保留

某个特定航班座位的请求。在这种情况下,这个 servlet 必须在一个航班上注册一个用

户,确认是否有空座位,并且在有空座位的情况下,为客户保留一个座位。如图 1.5 所

示。

图 1.5 保留座位的用例

在这个例子中,一个客户端为执行一个用例要运行多个同步的、到服务器端的调

用。这个处理过程的每一步都需要一个独立的网络调用并且在客户端阻塞以等待响应,对

于一个象航空订票这样的大型应用系统来说,这个瓶颈显然是不能被接受的。而且,用这

样的方式执行逻辑降低了系统的可维护性和可重用性,并且无法为用例提供事务的一致性

和隔离性保障。

最常用的解决方案是使用会话表面层模式,在使用这种模式时,一个应用将创建一个

包含业务逻辑的会话 bean 层来完成业务用例。每一个会话 bean 都在一个块调用中,为

客户端在实体 bean 或其它服务器端资源上完成大量操作,就像会话表面层模式中图 1.3

所示的一样。遗憾的是,即使整个用例都被包装到一个会话表面层方法中,这种方式仍然

8

Page 18: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

有几个缺陷:

无法接受的响应时间。一个和 web 站点交互的用户不会逗留超过几秒钟,但执行

这个用例需要许多跨越不同航线系统的数据库的后台操作。因为这些到 EJB 层的

调用是同步的,所以客户端必须阻塞直到整个进程完成。

不可靠/无容错。这个用例有可能涉及的 EJB 将被分散到三个独立的 EJB 服务器

实例和三个独立的数据库(用户、航线、班次)之上。如果这些服务器中的任意

一个被关闭,那么整个处理过程将失败,而且客户的定座请求也将丢失。即使

servlet 层只和一个 EJB 服务器通信,在服务器被关闭的情况下处理过程也同

样失败。

使用会话表面层模式可以解决耦合性、性能、可维护性、可重用性和一致性问题,但

无法完全解决响应时间和可靠性的问题。在一个复杂而又费时的订票用例处于运行状态

时,客户端仍然需要阻塞,用例也同样会因为它们在被执行时,所依赖的 EJB 服务器或其

它系统资源并没有处于运行状态而出错。

特别要强调的一点是,当我们在一个调用和一个事务中执行用例时(使复杂的服务端

对象模型对客户端屏蔽),需要一种具有容错性的服务器端的抽象来作为调用介质,通过

它不需要客户端阻塞并等待用例完成,消息驱动 bean(Message-driven bean)就是

为此被设计的。

因此: 使用消息驱动 bean 创建一个具有容错性的、异步的表面层。客户端只能对消息驱

动 bean 而不是实体 bean 进行访问.

消息驱动 bean(MDB)作为一个 表面层对会话 表面层进行了改进,在其中增加了能

够以具有容错的和异步的方式执行用例的能力。当我们使用消息表面层模式时,在一个应

用中的每一个用例的业务逻辑都映射为它们自己的 MDB。

考虑前面的例子,我们的在一个航班上订座的业务逻辑将被置于 ReserveSeat 消息

驱动 bean 中的 onMessage()方法中。这个 MDB 的目的就是封装并异步地执行所有与航

班订座有关的业务/工作流逻辑,如图 1.6 所示。

图 1.6 使用消息 表面层的保留座位用例

这里我们有一个 servlet 客户,他创建一个符合 Java Message Service

(JMS,java 消息服务)格式的消息并向它传递必要的参数。这个 Servlet 构建一个包含

了所有必要的参数(用户的主键码,航班号,航线的主键码)的消息,并把这个消息发送

9

Page 19: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

给为定座用例建立的 JMS 目的地。当适当的目的地收到这个消息后,客户端就自由了并且

可以继续作其它的工作(显示下一个 web 页),这时,消息驱动 bean 容器试图将消息

传递到下一个可用的 ReserveSeat 消息驱动 bean 中去。如果在实例池中的所有

ReserveSeat MDB 在消息被接收时都处于被使用状态,那么 JMS 服务器应该等待直到

其中一个变为可用的为止。而通过会话 表面层执行这个用例时,如果会话 bean 实例池

中的 Bean 都处于被使用状态,那么它就会成为一个出错点,而客户端将不得不人为地重

新尝试执行这个用例。

一旦某个 MDB 变为可用的,容器将执行 onMessage()方法,这时 ReserveSeat

消息驱动 bean 将线性地经历执行用例的过程:向航线注册客户,查找是否有空座,然后

保留一个座位。在运行这个费时的过程时,终端用户可以在网上冲浪或干其它自己的事。

消息表面层模式超越会话表面层模式的一个重要优势就是它能保证异步地执行用例。

也就是说,如果事务在任何一点出错(可能航班系统瘫痪或产生其它系统错误),事务将

回滚,而且 JMS 消息将重新放回到队列。这个事务将在以后进行重试,但客户端并不知道

这一些。

这种 behind-the-scenes(幕后)行为也会产生一个问题。怎么通知客户端执行用

例是成功了还是失败了?例如,如果因为飞机座位已经被预订满了而无法完成定座请求,

就需要通知客户端。在同步模式中(用会话表面层),客户端可以立即知道,而在异步模

式中,客户端不再为查看用例是否被成功执行而等待,它需要以与具体的应用相关的形式

被告警,最通常的解决方法就是电子邮件,如果用例成功/失败了,系统将通过电子邮件

通知用户,有些公司可能会用打电话等方式来实现。如果应用需求允许,某些应用也可以

使用投票模式。也就是说,终端用户将被分配一个特定的位置,用来查询他们的请求所处

的状态,类似于现代信使服务(modern courier services)使用的跟踪号。

特别要强调的一点是,当我们使用消息表面层模式时,开发者必须设计一种新颖的方

式来将用例的执行结果告知客户端。

使用消息表面层模式的一个缺点是:现在,业务逻辑将被分散到消息驱动 bean(对

于消息表面层)和会话 bean (对于会话表面层)中。这可能对大多数应用来说并不是主

要关心的事情,但是在一个应用中将业务逻辑集中放置在一个地方仍然是一个好的做法。

一种灵巧的解决这个问题方法是在会话表面层上实现它自己的所有用例,而用消息表面层

将方法调用指代到会话表面层上。这样,在将逻辑限制在会话 bean 层的同时,还可以保

留使用具有容错的、异步的、诸如消息驱动 bean 这样的结构所带来的所有好处。

消息表面层模式的优点除了在会话表面层模式中描述的几点之外,还包括:

快速的响应时间/异步通信。当客户端发送一个 JMS 消息后,它可以自由地

继续它的处理过程,而不用等待服务器完成用例和产生应答。这样,可以在

一个冗长而复杂的用例被启动的同时,将控制流立即返回给用户。

消除单一的失败点。使用消息机制将保证应用即使在它所依赖的 EJB 服务器

或子系统被关闭时也能继续运行。例如,如果数据库被关闭,但是 MDB 的事

务还没有完成,那么 reserve seat(定座)的消息将仍然保留在队列中,

并在以后重试;而如果 EJB 容器被关闭,消息将被再次储存。这种容错能力

在使用同步模式时是不可能具备的。当然,如果你的 JMS 服务器没有集群功

能并且被关闭,还是会产生单一的失败节点,但是它至少大大降低了出错的

可能。

然而,作为使用消息驱动 bean 的副产品,消息表面层模式也有一些缺陷:

消息驱动 bean 具有弱类型的入口参数。消息驱动 bean 的任务是消费

JMS 消息,它们在编译时看起来都是一样的。这与会话/实体 bean 形成了

10

Page 20: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

鲜明的对比,为了在编译时刻捕捉常见的错误,会话/实体 bean 支持 Java

内置的对 local 或 remote 接口中的方法和参数的强类型要求。对于包含

了由消息发往的 MDB 所请求的适当内容的 JMS 消息,开发者在加载它们时

必须格外地小心。这个问题的解决方案之一是将 JMS 消息中所有的数据封装

到一个定制的数据传送对象(data transfer object)中,并且序列化

这个对象到 JMS 消息中。

消息驱动 bean 没有任何返回值。由于 MDB 调用是异步的,所以消息 表面

层对那些在执行完毕后要求一个返回值的用例就不适用了。这样使得使用消

息表面层模式与使用所有的会话 bean 方法都只是返回 void 的会话表面层

模式从表面上看起来很相似。然而,通过使用 JMS 作为传输机制,仍然可以

从消息驱动 bean 得到一个返回给消息创建者的应答,请参考 Mastering

Enterprise Java Beans, Second Edition 中对 JMS 这种机制的讨

论。

消息驱动 bean 不能将异常传播给客户端。与会话/实体 bean 不同,消息

驱 动 bean 不 能 从 它 们 的 任 何 方 法 中 抛 出 应 用 异 常 或 远 程 异 常

(RemoteException),因此,MDB 必须处理所有以与具体应用相关的格

式表示的异常(也就是说,如果出现某种错误,给用户发送电子邮件,或者

把错误记录到管理日志等)。

消息表面层是一种非常强有力的模式,它可以被用来构建互相分离的、高度可扩展的

应用。一个典型的 EJB 系统将可能使用一种会话表面层和消息表面层混合的模式。会话表

面层是诸如客户端要求从服务器获得某些数据或客户端需要明确地等待某个用例完成的

“读”类型操作的选择,而消息表面层模式是更新操作的选择,这时客户端不需要立即看到

更新的结果。

消息表面层模式相比会话表面层模式在可扩展性和容错性上的优势是很显著的。在性

能方面,一个基于消息的系统的扩展性要比一个群集的会话 bean 方式好,因为消息驱动

bean 是“拉(pull)”工作而不是将工作“推(push)”给它,当我们将运行环境

(box)集群在一起时,这种拉方式的扩展性更好,因为它对系统资源的使用进行了优

化。

开发者在设计每一个用例时都应该仔细评估,询问他们自己这个用例具有同步的还是

异步的特性,这是判断使用哪种模式的决定性因素。

相关联的模式 会话表面层(Session Façade)

11

Page 21: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

EJB 命令(EJB Command)

为了完成一个用例,一个 EJB 客户端必须执行业务逻辑。 开发者怎样才能用一种轻量级的方式来实现一个用例的业务逻辑,而且把客户端与

EJB 分离开来,并且在一个事务或一个网络调用中执行用例呢?

在设计一个EJB系统时,一个关键的有关体系结构方面的决策是把业务逻辑置于何

处。一个用例的业务逻辑要么是指代给你的域模型上的适当方法的逻辑,要么是在多个其

它的实体bean 与/或会话 bean上执行操作的逻辑(工作流逻辑)。

把业务逻辑置于客户端(servlet, applet,等)有严重的负面结果,它会影响性

能和可维护性,这在会话表面层模式中已经解释过了。这些问题可以通过使用会话 表面

层模式,将业务逻辑置于会话 bean 的方法中得到解决,会话 bean 上的每一个方法都映

射了一个特定的工作单元或用例,这样的话,服务器的对象模型对于客户端就是屏蔽的,

而且用例是在一个事务和一个网络调用中被执行的。

会话表面层模式是 EJB 开发中最主要的一种模式,但是它也有自己的缺点。直接从客

户端调用会话表面层将会因为与 EJB 耦合性过高,而造成在一个大型项目中客户端与服务

器互相依赖,并且会产生复杂的客户端代码,正如在业务代理(Business Delegate)

模式中讨论的一样。这些问题可以通过使用业务代理来缓解,这个方法增加了一个对象

层,它封装了所有的对 EJB 层的访问操作。业务代理有助于在保持简单的客户端代码的同

时,将客户端与服务器之间的依赖降到了最低。

会话表面层模式与业务代理模式的混合模式为使用这样一种方式来编写业务逻辑提供

了一种最优方法:客户端与服务器实现细节相分离,并且允许用例在一个网络调用和一个

事务中执行。当然,这是付出了代价的:

较慢的开发过程。因为用例逻辑是(它们是可以经常改变的)在会话 bean

中运行的,因此无论何时,当一个用例需要修改时(也就是说在某个方法中

增加一个调用参数或返回属性),实现这个用例的会话 bean 方法也需要修

改。修改一个会话 bean 的过程并不是小事情:一个修改通常需要编辑三个

不同的文件(接口,bean 类,发布描述符),同时还要重新发布

(redeployment)到 EJB 服务器上,而且可能还要重启服务器。另外,在

客户端封装了被修改的会话 bean 的业务代理也需要修改。

在大型项目中划分工作更难。由于要依赖于在一个项目的开发者之间划分工

作时所使用的策略,所以会话表面层经常会成为不同的组或不同的开发者互

相争吵的瓶颈,因为它作为项目开发的一个过程,可能会成为频繁变动的部

分。

在一家大型公司中服务器资源通常只被一个组控制。对于已经建立了被发布

的 EJB 集合并正工作与其上的大型公司来说,可能很难让某个组在其它任何

会导致对现有类文件进行修改的项目上工作。

简言之,用会话表面层和业务代理开发会造成很长的更改-发布-测试的循环过程,它

可能会成为大型项目的瓶颈。这个问题的症结在于业务逻辑被置于对开发来说是可能是相

当重量级的会话 EJB 层中了。

因此:

使用命令模式(Command pattern),将业务逻辑包装在轻量级的命令 bean 中,

这个命令 bean 可以将客户端与 EJB 分离,并且在一个网络调用中执行,而且它还

起到了像一个 EJB 层的表面层一样的作用。

一个命令 bean 只是一个带有 get、set 和一个执行(execute)方法的平凡

12

Page 22: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

(plain)Java 类,就像在原始命令模式(original Command pattern (Gamma,

et al., 1995))中描述的那样,将命令模式应用于 EJB 时,它提供了一个轻量级的解

决方案,这个解决方案能够达到会话表面层和业务代理模式一样的优点:隐藏了在 EJB 层

上的对象模型,在一个网络调用和一个事务中执行一个用例,完全将客户端与 EJB 分离

等。命令模式通过给客户端提供一个用于在本地交互但实际上是在远程 EJB 服务器上执行

的、对客户端透明的类来实现上述优点。

在一个应用中,命令被用来封装单独的工作单元,一个像 placeOrder,

transferFunds 这样的用例将把它的业务/工作流逻辑封装在一个只为这个用例创建的

特定命令中,就像图 1.7 所示。

图 1.7 转移资金命令的客户端视图

客户端与一个命令的交互很简单,一旦客户端得到一个命令(是直接创建还是通过工

厂得到依赖于具体实现),它只是在命令上设置(set)属性,直到这个命令包含了执行

用例所需的所有数据,这时,客户端可以调用命令的执行方法,然后只是在命令上执行获

取(get)方法,直到它得到由执行用例/命令所产生的新数据。

当客户端执行命令时,在幕后发生了一些有趣的事。这个命令并不是在本地执行,而

是被传送到了远程的 EJB 服务器上,在 EJB 服务器的 JVM 中执行,这样在执行用例的过

程中命令对 EJB 的所有调用都发生在 EJB 服务器自身中。当命令完全执行之后,它返回

客户端,客户端可以调用获取(get)方法获得数据。通过在 EJB 服务器内执行命令,一

个用例可以只在一个事务中执行,这种行为的实现机制会这个模式的后面部分讨论。

例如在转账的例子中,客户端可以设置资金转出账户、转入账户和转账金额的标识

(ID),在调用转账(TransferFunds)命令的 execute 方法之后,客户端可以获得

帐户的结余,如图 1.8 所示。

13

Page 23: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

图 1.8 使用一个资金转移命令

IBM 的命令框架(Command framework)可能是最全面的命令模式的实现之一,它

与 Websphere 捆绑在一起,是 IBM 电子商务的一部分。另外还有许多不同的方法来实现

EJB 命令模式,但它们都有相同的三个要素:

命令 Bean。它是一个具有 get、set 和包含了执行用例所需的业务逻辑的

execute 方法的平凡 Java 类。命令 bean 是命令模式中唯一需要应用开发

者编写的部分,下面介绍的其它组件都是可以在多个项目中重用的。

客户端路由逻辑。通常由一个类框架负责获取一个命令并把它发送到远程的

EJB 服务器。这个路由逻辑对客户端通常是不可见的,并且是通过调用命令

的 execute 方法触发的。路由逻辑/框架是一个通用的可以被多个项目重用

的类的集合。

远程命令服务器。命令服务器是一个只接受命令并执行它们的服务。被应用

于 EJB 时,命令服务器(CommandServer)类是一个无状态的会话

bean,它接收一个作为调用参数的命令,并且在本地执行它。命令服务器也

是通用的,可以在多个项目中重用。

客户端与这三个构成部分的交互如图 1.9 所示。在这个例子中,客户端在路由逻辑组

件上调用 executeCommand 方法。在 IBM 命令框架中,客户端只需要在命令自身上调用

execute 方法,因为这个方法调用实际上是被作为路由逻辑框架一部分的命令的父类所接

收的。

在 幕 后 , 命 令 执 行 者 ( CommandExecutor ) 将 这 个 调 用 代 理 给

EJBCommandTarget(它没有在图 1.9 中给出,因为它是路由逻辑的一部分),

EJBCommandTarget 与 EJB 的知识一起编码,并且知道如何将命令发送到命令服务器的

无状态会话 bean。接收到命令后,命令服务器只是调用命令上实现了业务逻辑的

execute 方法。

命令模式的好处是:

通过轻量级的开发/发布(dev/deploy)过程促进快速应用发布(Rapid

Application Development (RAD))。将一个用例编写成命令 bean

14

Page 24: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

比起将它编写成会话 bean 的方法来,在发布与测试方面要快捷而且容易得

多,因为频繁的修改可以只在平凡 Java 类而不是整个 EJB 上完成。

业务逻辑和表示逻辑互相分离。命令通过将业务逻辑封装在其内部,起到了

一个在服务器上的对象模型的表面层的作用,它只暴露一个简单的命令接口

给客户端使用。这种分离允许客户端和服务器端互相独立地进化发展。

强制在一个单一的网络调用内执行用例。因为命令实际上是在 EJB 服务器内

执行的,所以只需要一个网络调用(和事务)就可以完成一个复杂的用例。

将客户端与 EJB 分离。客户端完全与服务器的实现细节分离,它所能看到的

只是以一个本地类形式出现的命令 bean。

命令可以在本地执行或者产生哑数据。可以在一个项目开始时创建空命令或

伪命令,从而允许表示层的开发者以独立于业务逻辑/EJB开发组的方式编

写、编译和测试它们的代码。

图1.9 命令模式的交互

很多方面命令模式看起来都像是最终的解决方案,它结合了会话 表面层 和业务代

理模

以没

命令对象不能在执行它的会话bean中存储任何状态,因

只能抛出一个命令

式的优势,并且拥有一个轻量级的基础架构,然而,好处都是要付出代价的:

非常粗粒度的事务控制。因为命令只是平凡(plain)Java bean,所

有任何自动的方法来标识运行在一个特殊的事务设置或隔离级别之下的命

令,但是在会话bean的方法中这是可以实现的。命令只能以执行它们的命

服务器的事务设置来运行,可以在它的工作区中使用不同的jndi名字和事务

设置(在发布描述文件中配置)来发布多个命令服务器会话 bean,并且路

由逻辑组件需要配置向某个命令服务器发送某种命令的信息。也就是说,某

人可能希望向运行时不产生事务的会话bean发送所有的只读命令,而更新命

令可以在一个具有tx_requires属性和隔离级别为serializable的命令

服务器上执行。

命令是无状态的。

此,在EJB层中存储状态对命令模式来说是不可能的。

笨拙的错误处理。由于命令框架是通用的,所以从命令中

异常(CommandException)。这意味着像

15

Page 25: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

NoMoneyInAccountException这样的应用

中被捕获和包装,然后,客户端需要在命令对象内部查找确切的异常。因为

异常没有被明确地声明,所以客户端失去了在编译时进行错误处理检查的好

处。

在大型

程序异常必须在一个命令异常

项目中的命令会变得无法控制。一个大型项目会产生成千上万条命

是紧耦合的。因为命令 bean

令模式和会话表面层模式都提供了两个重要的好处:它们都起到了一个表面层的作

用,

关联的模式 (Gamma, et al., 1995)

令,其中许多都会重复业务逻辑中的某些部分,特别是当不同的项目组在后

台使用同一个域模型时这种情况尤其严重。这使得对业务逻辑层的维护变得

困难了许多,这与会话表面层模式相反,在会话表面层模式中,用例是作为

会话bean的方法来实现的,并被很好地组合在少量的会话bean中。类数量

的激增对大型项目而言是很严重的问题。

命令服务器的ejb-jar与命令bean及其它EJB

是在命令服务器会话bean的环境内执行的,所以为了使命令 bean能够被反

序列化并被执行,命令 bean类需要与命令服务器会话bean一起发布(在同

一个ejb-jar和EAR中)。这就意味着无论何时,如果命令bean被修改了,

而我们想要测试这些修改,那么命令服务器会话bean 的EAR 或 ejb-jar

就需要被重新发布(这样命令服务器的类加载器才可以读取所有命令的最新

版本),并且应用服务器有可能需要完全重启(如果你的应用服务器不支持

热发布)。而且,命令 bean必须了解任何在它们的业务逻辑中可能会用到

的home,remote,local home或local接口。这就要求被发布的命令服

务器与其它所有的被它的命令bean访问的EJB位于相同的EAR中,或者被访

问的EJB的接口被包装在命令服务器的ejb-jar中。

并且都在一个网络循环中执行。命令模式超越会话表面层模式的主要优势是它将客户

端与EJB相分离,当然这在业务代理与会话表面层联合的模式中也可以实现。那么,开发

者怎么在这两者中进行选择呢?把命令看作是更廉价的会话bean会有所帮助,它们更轻量

级,在可能更少地损害可维护性的情况下,能够更快地形成初始的开发过程。

命令(Command)

数据传送HashMap (Data Transfer HashMap

16

Page 26: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

数据传送对象工厂(Data Transfer Object Factory)

一个使用数据传送对象(DTO)的 J2EE 系统会发现它的 DTO 层经常倾向于发生改

变。 为了将由于 DTO 层的频繁改变而产生的对系统其它部分的影响降到最低,应该怎样实

现数据传送对象的创建和消费逻辑呢?

数据传送对象有经常被改变的趋势,无论何时,只要域对象改变了(对一个实体

bean 增加新属性等),那么域的 DTO 也要改变。定制的 DTO 被用于在网络中传送数据,

并且还是与具体用例相关的数据拥有者(holder),它们可以象你的应用表示视图一样

被频繁地修改。一个大中型应用可能会有数十个,甚至数百个不同的传送对象,其中每一

个都需要定制的逻辑来创建它。这样就产生了一个关键的问题:为了将系统的其它部分与

对数据传送对象的修改相分离,并保护它们免受影响,应该怎样和在哪里实现这个逻辑

呢?

在 EJB 1.X 应用中采用的一个通用的解决方案是将 getXXXDTO/setXXXDTO 方法

直接置于实体 bean 中。这种情况下,实体 bean 将负责组装(populate)这个数据传

送对象,并且基于所设置的 DTO 属性来更新它自己。这种方法的问题是数据传送对象层和

实体 bean 层高度耦合,也就是说,将与具体用例相关的数据传送对象的创建代码置于一

个实体 bean 中会导致一个大中型应用中的实体 bean 和客户端的严重依赖。每当一个

Web 页被修改并且需要一个不同的数据模型视图时,你必须在实体 bean 中添加新方法,

重新编译它,并且将你的 remote 接口重新发布到任何使用它们的客户端。

实体 bean 应该成为可重用的业务组件,它们可以被互相独立地组装成一个应用。如

果要构建真正可重用的业务组件,那么维持精确的应用逻辑与业务逻辑的分离,以允许它

们各自独立地发展进化就显得很重要了。创建和消费实体 bean 还需要某个其它的解决方

案,它可以将和 DTO 有关的逻辑与系统中的其它组件分离。

因此:

将创建和消费数据传送对象的职责置于数据传送对象工厂(data transfer

object factory)中。

数据传送对象工厂把与数据传送对象有关的逻辑(应用域的一部分)同系统中的其它

构件互相分离开,例如象实体 bean(业务域的一部分)这样的构件。当必须产生服务器

端数据的新视图或不同的子集时,可以将一个新的 DTO 创建方法添加到 DTO 工厂中,而

不是置于实体 bean 中。这些新方法将与实体 bean 层(或任何其它数据源,例如

connector,straight JDBC 等)通过调用 getter 方法来进行交互,必须通过产生

定制的数据传送对象或域的数据传送对象时所必需的关系来调用这些 getter 方法。这种

方法的好处是实体 bean 自身并不需要了解这些不同的数据视图,事实上,在实体 bean

中根本没有任何代码需要修改。

例如,考虑一个允许用户浏览汽车及其制造商信息的汽车应用,它的域模型包括一个

汽车实体 bean 和一个制造商实体 bean。这样一个应用的用户界面(UI)具有很多能让

用户浏览汽车及其制造商的不同属性的页面,包括汽车属性(引擎属性、车身属性、底盘

等)的不同子集,和跨越多个实体 bean 的数据(汽车及其制造商的信息等)。这些不同

的数据集合应该使用定制的 DTO 传送到客户端,创建这些 DTO 所需的 Java 方法不是置

于汽车或制造商实体 bean 中,而是置于像图 1.10 所示的 DTOFactory 中。

17

Page 27: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

图 1.10 CarDTOFactory

现在的 CarDTOFactory 成为了与具体用例相关的 DTO 逻辑的唯一的驻留点,它可

以帮助将客户端与域模型分离。而现在的域模型上的实体 bean 对于域对象则是自由的,

它们暴露给客户端的只是业务方法,而不是那些令人厌恶的、实际上与通过特定的域模型

所体现的业务概念(business concept)无关的 DTO 的 get/set 逻辑。

有两种基本的方法来实现 DTO 工厂模式,使用哪一种取决于工厂的客户端是一个会话

bean 还是一个象 servlet 这样的非 ejb 客户端。在使用会话 bean 表面层模式时,

DTO 工厂可以被作为一个平凡 Java 类来实现,在这个类的方法中存储了不同的数据传送

对象的创建和消费逻辑,这类工厂有助于提高可重用性,因为它所生成的数据传送对象可

以在不同的项目和不同的会话 bean 中重用。

当被非 ejb 客户端使用时,DTO 工厂应该被作为一个会话 bean 来实现。一个典型的

这类客户端和数据传送对象的交互如图 1.11 所示。这里,一个 servlet 的客户端想得

到一个被称为 CarAndManufacturerDTO 的定制的 DTO,所以它为这个对象查询一个

CarDTOFactory,然后 CarDTOFactory 通过 local 接口调用汽车实体 bean 和相关

的制造商实体 bean 上的 get 获取方法来创建和组装 DTO。

18

Page 28: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

图1.11 作为一个会话 bean来使用CarDTOFactory

数据传送对象工厂可以被用来轻松地创建任何类型的DTO,甚至可以创建复杂的聚合

的DTO的多层结构(包含其它DTO域的DTO域),这是通过将这个DTO的多层结构映射为在

服务器上的实体bean对象模型的不同片断来实现的。复杂的数据传送对象的多层结构可以

使用一个写逻辑来显式地创建,这个写逻辑知道怎样定位(和拷贝)一个与具体用例相关

的实体bean多层结构的片段。这些DTO多层结构都可以预先在服务器端创建,然后通过一

个网络调用传送到客户端。

这个方法的一个重要的好处是在我们的应用中的实体bean现在是完全可重用的了。例

如,设想在一个公司中的两个独立的小组正在开发两个独立的应用,这两个小组可以通过

使用独立的数据传送对象工厂来重用相同的实体bean业务组件。开发组可以通过维护自己

的DTO工厂来实现完全重用,这个工厂可以独立于其它组而传递出与具体用例相关的实体

bean 状态的DTO片断(“slices”)。通过维护自己的DTO工厂,他们可以彼此完全独立

地开发和发布自己的应用,如图1.12所示。

图1.12 使用数据传送对象工厂来实现实体bean的可重用性

要注意的是数据传送对象工厂模式并不意味着对每一个实体bean类都创建一个DTO工

厂。例如,你不是必须要为汽车实体bean创建一个CarDTOFactory,因为这将导致DTO

19

Page 29: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

工厂的激增。在需求允许的情况下,我们可以更直接地为整个实体bean集合和其它服务器

端数据只创建一个DTO工厂。

DTO工厂提供了一种从服务器端读取数据的方法,那么更新数据的情况又如何呢?更

新数据使用的是与从服务器端读取数据相似的技术。也就是说,客户端可以传递一个域

DTO或一个定制的DTO到服务器,在那里它可以在实体bean或其它存储在服务器端的数据

上依次执行创建(Create)、读取(Read)、更新(Update)、删除(Delete(CRUD))

操作。

对于域DTO(它们通常是易变的),客户端将在本地执行它的更新,然后通过将域

DTO传递到DTO工厂的updateXXXEntity方法来更新服务器,DTO工厂使用细粒度的在实

体bean的local接口中的set方法将DTO的属性复制到适当的实体bean上。客户端可以类

似地通过在本地组装域DTO并把它传递到DTO工厂的createXXXEntity方法来创建实体

bean。

再看前面的例子,如果应用管理员想更新某种汽车或制造商的信息,这些更新将由互

相独立的UI显示界面来完成(一个是汽车的显示界面,一个是制造商的显示界面)。当一

个汽车或一个制造商的域DTO需要在一个单一事务中更新时,它就被发送回服务器,并且

执行数据更新,如图1.13所示。

图 1.13 使用数据传送对象工厂来更新数据

为了执行任何类型的相对于对域对象的CRUD更新来说更复杂的更新,应该通过将定制

的DTO传递到会话或消息表面层来使服务器得到更新。记住,表面层应该包括了完成应用

中的用例所必需的所有业务逻辑,例如下一个订单到Amazon,或在银行中转账。对这些

类型的操作,客户端通常创建一个定制的DTO,它包含了所有完成更新所必需的数据,再

把这个DTO传递到表面层,在那里可以依次创建、更新和删除任何数量的服务器端资源。

数据传送对象工厂模式的好处数不胜数:

更好的可维护性。它将你的应用逻辑(用例)和数据对象模型(实体

bean)分离,使得它们可以独立地发展进化。当客户需求被改变时,实体

bean不再需要被改变和重新编译。

鼓励对实体bean的重用。因为不同的DTO工厂被编写为适合不同的应用需

20

Page 30: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

求,所以实体bean可以被不同的项目重用。 允许创建复杂的 DTO 图。通过预先编写 DTO 创建逻辑,开发者可以创建复

杂的 DTO 图/层次结构,它可以用来从复杂的实体 bean 层次结构中传送数

据,这些复杂的实体 bean 层次结构包括一对一关系、一对多关系、多对多

关系、循环关系和这些关系的混合关系。这给客户端提供了对实体 bean 数

据的哪些部分需要显示进行细粒度控制的能力。对于非 Web 客户端如 Java

应用程序和 applet,这种获取非表格式数据的能力尤为重要。

提高了性能。当 DTO 工厂作为会话表面层模式使用时,多个实体 bean 的属

性可以仅在一个网络调用中被传递到客户端。

数据传送对象工厂模式提供了一个简单而一致的用来创建任意复杂的数据传送对象,

并在一个网络块调用中把它们传递到客户端的方法,同时不会造成数据传送对象与 J2EE

系统的其它组件之间的互相依赖,所以使用它可以构建可维护的、灵活的系统。 相关联的模式 会话表面层 (Session Façade)

数据传送对象(Data Transfer Object)

值对象组装器(Value Object Assembler) (Alur, et al., 2001)

21

Page 31: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

通用属性访问(Generic Attribute Access)

一个实体 bean 客户需要访问实体 bean 的属性。

一个实体 bean 客户怎样才能以一种通用的块方式来高效地访问和操作一个实体

bean 的属性呢?

在通常的方法中,实体 bean 是通过 local 接口的 get/set 方法(EJB2.X 的实体

bean)或者通过发送到 remote 接口的大型的数据传送对象(EJB1.X 的实体 bean)被

访问的。对于前者,为了访问和操纵数据,会话 表面层或数据传送对象工厂上的方法通

过调用多个细粒度的 getter 和 setter 方法与实体 bean 进行交互,就像特定的用例

所要求的那样。对于后者,为了将与 remote 接口通信相关的网络调用降到最少,数据传

送对象被用来成批地访问和处理实体 bean 的状态。将 DTO 作为操纵实体 bean 的一种

机制来使用是优化与 EJB1.X 实体 bean 的通信的一种通用模式。

使用 DTO 来访问 EJB1.X 实体 bean 的代价是降低了实体 bean 层的可维护性(参

阅 DTO 工厂模式)。随着 EJB2.0 的出现,local 接口允许将 DTO 的创建和消费逻辑提

取出来并置于数据传送对象工厂中,DTO 工厂与实体 bean 通过其 local 接口上的细粒

度的 get/set 方法进行交互,从而缓解了使用 DTO 带来的一些问题。

遗憾的是,EJB1.X 实体 bean 没有 local 接口。这样带来的后果是 DTO 的创建和

消费逻辑不能从一个实体 bean 中提取出来,从而也就不能将其置于数据传送对象工厂中

(因为对 DTO 工厂来说,在一个实体 bean 的 remote 接口上产生多个细粒度的调用对

性能是有损的)。因此需要某种允许通过 remote 接口来大量访问实体 bean 的数据的

机制,而这种机制又不会因 DTO 的创建/消费逻辑而被打散。

即使是 local 接口,暴露多个细粒度的 get/set 方法也不是一个好主意:

不能很好地从小型实体 bean 扩展到大型实体 bean。想象一个财政应用中

的股票/债券实体 bean,这样的一个实体 bean 可能具有超过 200 个的属

性,为所有这些属性编写和暴露 getter/setter 方法将导致乏味的编码噩

梦和接口规模的激增。

导致紧耦合硬编码的客户端。实体 bean 客户端(例如会话表面层)需要与

实体 bean 紧密耦合,这使得它对可能是频繁发生的微小变化都很敏感,例

如象增加或删除一个属性这样的修改。

要强调的一点是需要某个其它的访问实体 bean 数据的机制,它允许一个 DTO 工厂

使用 remote 接口动态地在一个网络块调用中获取实体 bean 状态的不同子集,同时在使

用 local 接口时,可以帮助将实体 bean 客户端与实体 bean 的属性访问器分离。

因此:

将实体 bean 的属性访问逻辑抽象出来置于一个通用的属性访问接口中(Attribute

Access Interface),并使用 HashMap 在实体 bean 中传入和传出属性的键-值

对(key-value)。

这个属性访问接口由实体 bean 在 remote 或 local 接口上实现,像这样:

public interface AttributeAccess {

public Map getAttributes(Collection keysOfAttributes);

public Map getAllAttributes();

public void setAttributes(Map keyAndValuePairs);

}

22

Page 32: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

AttributeAccess 提供了一个允许从一个实体 bean 中动态地读取或设置任意的数

据集的通用接口。这个接口允许 EJB1.X 实体 bean 使用一个 DTO 工厂来提取 DTO 创建

逻辑并优化远程调用,同时允许 EJB2.X 实体 bean 通过移去对细粒度的 get/set 方法

的需求来简化它们的 local 接口。客户端和实体 bean 代码之间的唯一依赖是用来标识属

性的键的命名规则,这将在这个模式的后面描述。

会话表面层或 DTO 工厂可以通过属性访问接口来访问一个实体 bean 的属性。图

1.14 说明了这样一种典型的情况,这里,一个客户端对收集一个汽车实体 bean 的有关

它的引擎的数据子集感兴趣,它调用会话表面层上的 getCarEngineData 方法,这个方

法依次查询实体 bean 的关于轿车引擎的那部分属性,它首先创建一个包括感兴趣的属性

的键值集合(马力、气缸体积等),然后将这个集合传递给实体 bean 上的

getAttributes(collection)方法,getAttributes(collection)方法将用这个

属性子集的精确值返回一个 HashMap。

图 口 1.14 使用属性访问接

在接收到这个从汽车实体 bean 组装的 HashMap 之后,这个会话 bean 可以:

1. 将这个 HashMap 返回给一个远程客户。这个会话 bean 使用这个 HashMap 作为

在网络中传输的数据的可序列化的容器(就像在第二章中数据传送 HashMap 模

式所描述的那样)。

2. 将 HashMap 转换为一个 DTO 然后将它作为结果返回。作为一个 DTO 工厂,这个

会话 bean 可以提取 HashMap 的值,并且将它们添加到一个数据传送对象中,

然后将这个 DTO 返回给客户端。

选择哪一个方法,取决于开发者。作为数据传送的一种机制,HashMap 具备许多超越

数据传送对象的优势(就像在数据传送 HashMap 模式中描述的那样),但同时作为所付

出的代价,它明显地增加了复杂性。如果在 DTO 工厂模式下使用属性访问接口,那么键-

值对名字之间的依赖可以保持只驻留在服务器上,在那里会话 bean 无论如何都需要了解

实体 bean。

通过使用属性访问接口,一个会话 bean 可以在运行时刻动态地决定它需要实体

bean 数据的哪一个子集,而不需要在设计阶段对数据传送对象进行手工编程。

象接口本身一样,属性访问接口的实现是通用的。对 BMP 来说,一个实体 bean 可

23

Page 33: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

以将它所有的属性存储在一个私有的、内部的 HashMap 中,而不是像通常情况下对属性

进行显式的硬编码,这样实体 bean 可以得到更进一步地简化。对于大型的实体 bean,

这种做法可以大大简化其代码。通过使用这个内部 HashMap,对 AttributeAccess 中

的方法的实现将可以在多个 BMP 实体 bean 中完全通用并且它们是可重用的:

private java.util.HashMap attributes;

/**

* 返回实体bean 属性的 key/value 对

* @return java.util.Map

*/

public Map getAllAttributes()

{

return(HashMap)attributes.clone();

}

/**

* 被客户端用来获取他们感兴趣的属性

* @return java.util.Map

* @param keysofAttributes 客户端感兴趣的属性

*/

public Map getAttributes(Collection keysofAttributes)

{

Iterator keys = keysofAttributes.iterator();

Object aKey = null;

HashMap aMap = new HashMap();

while ( keys.hasNext() )

{

aKey = keys.next();

aMap.put( aKey, this.attributes.get(aKey));

}

//map 现在具有所有被请求的数据

return aMap;

}

/**

* 被客户端程序用来更新在实体bean 中的特定属性

* @param keyValuePairs java.util.Map

*/

public void setAttributes(Map keyValuePairs)

{

Iterator entries = keyValuePairs.entrySet().iterator();

Map.Entry anEntry = null;

while ( entries.hasNext() )

{

anEntry = (Map.Entry)entries.next();

this.attributes.put(anEntry.getKey(),

24

Page 34: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

anEntry.getValue());

}

}

在 CMP 中,使用一个内部的属性 Map 是不可能的,因为一个实体 bean 的实现类是

抽象的,容器负责产生 get/set 方法。当不可能使用内部的 map 时,属性访问接口可以

使 用 Java Reflection API ( java 反 射 API ) 来 实 现 。 也 就 是 说 , 在

setAttributes 中,可以在客户想要设置的属性的键-值对上执行反射。具体说,如果

键-值对是 XXX,那么 setAttribute 的实现将尝试调用这个实体 bean 上的 setXXX()

方法。类似地,在 getAttributes 方法中,反射可以被用来查找在一个实体 bean 上

的所有 get 方法,然后调用它们,并为客户端组装一个 map。如果开发者不想使用反射

API,那么就不可能为 CMP 通用地实现属性访问方法,开发者需要在他的

AttributeAccess 实现中插入调用在实体 bean 上硬编码的 get/set 方法的 IF 语

句,这些 IF 语句的执行结果取决于键-值对字符串的内容。

无论我们是使用反射方法还是内部属性 map 风格的 AttributeAccess 的实现,为

了使其实现代码可以在所有的实体 bean 中重用,可以使用一个完全通用的父类来实现接

口中的方法,所有想利用 Attribute Access Service(属性访问服务)的实体

bean 只需要从这个父类实现中扩展子类。这样就可以自动暴露这个统一接口给它们的属

性,而不需要任何额外的编码。

最后让人疑惑的一点是怎样命名标识实体 bean 属性的键。不仅属性需要通过键来标

识,而且通过属性访问接口来访问实体 bean 的逻辑和实体 bean 自身都需要对命名规则

达成一致,因此,在服务器和客户端之间需要某种形式的“契约(contract)”,下面讨

论几种可能的形式:

建立一致的、有良好的文件说明的命名规则

客户端和实体 bean 可以对属性的命名规则达成一致,这个命名规则具有有良好的文

件 说 明 。 对 于 Account 实 体 bean , “com.bank.Account.accountName” 或

“accountName”都是基于一致规则的例子。这种方法的缺点是这个契约只存在于开发者

的脑子里和纸上,这使得在开发时很容易拼写错属性名字,从而导致很难捕捉得到的代价

高昂的开发错误。

在实体 bean 的 remote 或 local 接口中定义静态的、最终的成员变量

一个实体 bean 客户端可以通过引用静态的(static)、最终的(final)变量来

产生对实体 bean 的调用,这个变量包含了获得一个属性时所必需的正确的键字符串。例

如,为了从一个 Employee 实体 bean 中读取属性,一个会话 bean 可以使用下面的代

码:

//查询employee 的个人属性

Collection aCollection = new ArrayList();

aCollection.add(Employee.NAME);

aCollection.add(Employee.EMAIL);

aCollection.add(Employee.SEX);

aCollection.add(Employee.SSN);

Map aMap = employee.getAttributes(aCollection);

实体 bean 的 local 接口包含下面的定义:

25

Page 35: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

public interface Employee extends EJBLocalObject, AttributeAccess

{

//因为属性是存储在实体bean 的一个hashmap中, 所以我们需要

//一个中心位置来存储用来引用属性的 key,这样,客户端程序和

//实体 bean 都不需要硬编码有关属性的 key 字符串的知识

public final static String ID = “EMPLOYEEID”;

public final static String NAME = “NAME”;

public final static String EMAIL = “EMAIL”;

public final static String AGE = “AGE”;

public final static String SSN = “SSN”;

public final static String SEX = “SEX”;

...

}

这个方法对 DTO 工厂方法很有用,在 DTO 工厂方法中,会话 bean 直接查询实体

bean 的属性,返回给客户端一个硬编码的数据传送对象,而不是一个 HashMap。其中,

只有这个会话 bean 和实体 bean 需要对属性的键名达成一致,这就使得

local/remote 接口成为了放置属性名字的理想位置。这个方法在使用数据传送

HashMap 模式时将无法使用,因为客户端虽然也需要知道键-值对的名字,但是它并不访

问实体 bean 的 remote/local 接口。

含有静态的、最终的成员变量的共享类

我们可以在硬编码一个最终的、静态的变量的情况下,创建一个客户端类和服务器类

共享的类,用来封装实际从 HashMap 中读取的和被用来组装 HashMap 的字符串,它对客

户端和服务器都是可访问的。例如,一个客户端可以象下面这样在一个 hashmap 中查询

一个属性: accountMap.get(Names.ACCOUNTBALANCE)

被称作 Names 的共享类如下:

public class Names {

public final static String ACCOUNTBALANCE = “BALANCE”;

...

}

这个方法的一个弊处是当键的映射需要升级或增添新内容时,新产生的类必须重新发

布到客户端和服务器(这样,它们的 JVM 也必须重启)。

将属性契约置于 JNDI 树中

在这个方法中,通过将一个包含了键的类置于 JNDI 树中来维护一个有关种类信息的

singleton(一个包含了它自己的一个静态实例的 java 类,这样,在一个应用中,这个

类将只有一个实例在运行),它对客户端和服务器都是可访问的。在键发生变化或升级

时,客户端和服务器端的代码都不需要重新编译和重新启动,因为在 JNDI 树中的中枢对

象将包含最新的键的副本。这种方法的缺点是无论何时需要键-值对,都将产生从 JNDI 树

中获得契约的开销。

通用的属性访问模式有许多优点:

26

Page 36: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

一个接口可被所有的实体 bean 使用。实体 bean 客户可以通过属性访问

接口来统一地操纵实体 bean,从而简化了客户端代码。实体 bean 也同时

得到了简化,因为属性访问操作可以被封装在一个父类中。

能够很好地扩展到大型实体 bean。无论一个实体 bean 具有 20 个还是

200 个属性,属性访问逻辑都被简化到只有几行代码。

随着时间的推移,只需要较低的维护代价。不需要任何服务器端编程就可以

创建服务器端数据的新视图。客户端可以动态地决定显示哪些属性。

允许在运行时刻动态增加属性。当使用 BMP 时,这种模式可以很容易地扩展

为允许动态地从一个实体 bean 中增加或移去属性,这可以通过添加

addAttribute 和 removeAttribute 方法到被用来在属性 HashMap 上

执行操作的接口中来实现。

象所有模式一样,使用通用属性访问模式也要付出代价:

每一个方法调用的附加开销。对每一个属性调用,客户端必须使用一个属性

键来标识属性。最终,属性在被从 HashMap 对象中提取出来之后需要被转

换(cast)为恰当的类型。

需要维护一个键-属性契约。因为属性是通过字符串被访问的,所以客户端

需要记住用来标识属性的键字符串。定义一个键-属性契约(象前面讨论的

那样)可以缓解这些依赖。

丢失了强类型/编译时刻检查的特性。当我们使用 DTO 时,传递到 get 或

set 的值总是恰当的类型,任何错误都将在编译时刻被发现。而当我们使用

通用属性访问模式时,属性访问必须由客户端在运行时刻进行管理,而且还

要将对象转换到它们恰当的类型,并要将恰当的属性类型和恰当的键相关

联。

总的来说,通用属性访问模式提供了一种通用的管理实体 bean 状态的方法,消除了

大量的与访问域关联的实体 bean 数据有关的重复代码。

相关模式 属性容器(Property Container) (Carey, et al.,2000)

数据传送 HashMap(Data Transfer HashMap)

27

Page 37: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

业务接口(Business Interface)

EJB 规范要求企业 bean 类要为在 remote 和 local 接口中声明的所有方法都提供

一个实现,但是 bean 并不直接实现这些接口。

怎样才能在编译时刻发现 remote/local 接口中的方法与企业 bean 实现之间的不

一致呢?

在 EJB 开发过程中出现的一个最常见的错误就是在 remote/local 接口中定义的业

务逻辑方法与它们在企业 bean 类中的实现之间缺乏一致。EJB 规范要求企业 bean 必

须完全实现在 remote/local 接口中定义的业务方法签名,但是并没有提供一种自动的

方法用来在编译时刻探测这个问题。许多类型的错误都是由这种接口定义和实现的分离所

引起的,包括方法名字、参数类型、异常的拼写错误和不一致的参数表等。因此,这些类

型的错误不能在编译时刻被探测到,EJB 的开发者必须手工地维护接口定义与 bean 实现

之间的一致。

这些错误只有在使用你的 EJB 服务器供应商所专有的 postcompilation 工具时才

能被探测到。这些工具通常被用来在包装和发布已编译的 java 类之前加载并测试它们是

否符合 EJB 规范。通常,这些 postcompilation 工具在使用时运行速度都很缓慢、不

易使用,而且很少能够适应不断增加的开发者用来尽早捕捉错误的编译方法,这样产生的

最终结果是开发错误只能在开发过程较晚的阶段才能被捕捉到。

图 1.15 EJBObject 和 EJBLocalObject 接口

解决方案之一是企业 bean 直接在 bean 类中实现 remote/local 接口。这样使用

任何标准的 java 编译器都可以强制实现方法定义与实现之间的一致。遗憾的是,EJB 规

范建议不要使用这种习惯,而且提出了一个很好的原因,remote 接口扩展了

javax.ejb.EJBObject 接口,local 接口扩展了 javax.ejb.EJBLocalObject 接

口 , 如 图 1.15 所 示 。 这 些 接 口 定 义 了 一 些 额 外 的 方 法 ( isIdentical、

getPrimaryKey、remove 等),这意味着它们要由 EJBObject 和 EJBLocalObject

的桩(stub)而不是企业 bean 类来实现。

28

Page 38: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

为了编译你的 bean,你不得不编写这些额外方法的哑实现,这样也就打乱了你的企

业 bean。而且,如果企业 bean 类直接实现 remote 或 local 接口,那么 bean 就可

以直接转换为这些接口中的一个,从而允许开发者传递一个 this 实例到客户端,但是这

种行为是 EJB 规范所不允许的。一个 bean 为了传递一个对它自己的引用就必须首先通过

调 用 在 SessionContext 或 EntityContext 接 口 中 的 getEJBObject 或

getEJBLocalObject 方法得到对它自己的引用。

EJB 开发者不应该在它们的企业 bean 类中直接实现 remote 或 local 接口,但是

开发者又需要一种机制来允许在编译时刻确认在 remote/local 接口中的方法定义和它

们在 bean 类中的实现之间的一致性。

因此:

创建一个定义了所有的业务方法的被称为业务接口(business interface)的父

接口。让 remote/local 接口和企业 bean 类都实现这个接口,从而强制实现编译

时刻的一致性检查。

一个业务接口是一个平凡(plain)java 接口,它为一个企业 bean 所选择的所有

要暴露的业务方法定义了方法签名。业务接口由 remote/local 接口和企业 bean 类实

现,如图 1.16 所示。通过创建这个父接口,在 remote/local 接口中与在企业 bean

类中的方法签名定义的一致性错误就可以在编译时刻捕获。

图 1.16 为 remote、local 接口和 bean 类设计的业务接口

业务接口并不实现 javax.ejb.EjbObject 或 javax.ejb.EJBLocalObject 接

口,所以 bean 类开发者不必实现哑方法。而且,开发者不能将 bean 类直接转换为它的

remote/local 接口,同时阻止了 bean 开发者将 this 传递到客户端。

业务接口模式有细微的差别,取决于企业 bean 是将它的业务方法在 local 接口还

是在 remote 接口中暴露。如果实体 bean 暴露 remote 接口,那么在业务接口中的所有

方 法 签 名 都 需 要 抛 出 java.rmi.RemoteException 异 常 ( 但 是 不 用 扩 展

java.rmi.Remote) 。 注 意 在 企 业 bean 类 中 的 方 法 实 现 不 应 该 抛 出

29

Page 39: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第一章 EJB 层的体系结构模式

RemoteException 异常,因为这在 EJB 规范中已经被摒弃了,相反地,业务方法在其

方法体中不需在 throws 子句中声明就可以抛出 EJBException,因为 EJBException

是 RuntimeException 的子类。

当使用带有一个 local 接口的业务接口时,业务接口不需要实现任何其它的接口,而

且编写业务方法签名没有任何特殊的规则。

使用业务接口模式有一个危险的负面效果,对于那些返回值是 bean 自己的

remote/local 接口的方法,实现业务接口将允许 bean 开发者返回 this 而不会产生任

何在编译时会被探测到的问题,这种情况是可能发生的,因为 bean 类和 remote/local

接口都实现了业务接口。返回 this 的错误在不使用业务接口模式时总是能够在编译时刻

被捕获到(因为 bean 类没有实现 remote/local 接口)。这样,bean 开发者必须在使

用业务接口模式时加倍小心,不要产生象返回 this 这样的错误,或其它可能会在运行时

刻产生的不可预知的错误。

业务接口模式是在 EJB 开发中的一个通用模式。它允许开发者在编译时刻捕捉常见的

编程错误,以确保业务方法定义与实现之间的一致性。

30

Page 40: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第二章 内层数据传送模式

内层数据传送模式回答了“怎样从服务端获得数据并且返回给客户端?”这个 基

本的问题;服务器端数据通过内层传送、拷贝的概念对于一个刚接触分布式系统开发的

初学者来说是很容易混淆的。因为,在 java 非分布式的应用开发中没有类似的范例。 本章涉及了以下三种模式: 数据传送对象(Data Transfer Object):基本的设计模式。讨论为什么,如何,和何

时通过网络调度一组数据――数据传送对象(DTO)。以下两种模式(域和定制 DTO)

将指导我们如何设计 DTO。 域数据传送对象:同域模型的交互在直觉意义上是可行的,然而遗憾的是,在 ejb

的世界里它是一个有损于系统性能的实践(参见会话表面层的解释)。而域 DTO 描述了

DTO 如何被用于调度域模型的拷贝到客户端,而不是域对象自身。 定制数据传送对象:与域 DTO 相反,定制 DTO 不代表任何服务端的域对象。他

们是根据客户端的需要而构建的。 数据传送哈希表: 讨论哈希图如何可以被用作内层通讯的方式,从而不必构造DTO

层。 数据传送行集:.当传送到客户端的数据是只读的表格数据,行集接口对这些表格数

据提供了一个很好的抽象,直接从 ResultSet 中将表格数据传送至客户。 数据传送对象: EJB 系统中的客户端需要一个种和服务端进行大量数据的传送的方式。

客户端如何和服务端进行大量数据交换而不需要进行多重、细粒度的网络调

用呢?

在任何分布式的应用程序中,客户需要和服务端相互作用影响主要有两个原因,

第一个原因是:为了实现某目标而从server端读取数据。第二个原因:通过创建,修改,

删除数据和服务端交换数据。在ejb情形下,这些类型的操作都涉及到客户((servlet, applet,等)和会话Bean,实体Bean,message Bean之间进行数据交换。

当有大量数据需要交换的时候,可以通过将参数传给方法调用(在服务端更新数

据时)来实现,或者通过进行多重,细粒度的网络调用给服务端重新获得数据(当客户

需要从服务端读取数据时),前面一种方法可以通过处理大量的参数而避免繁琐的操作,

后者可以成为“性能杀手”。 想象一下这种场景:客户端的界面需要显示一个宿主于服务端的属性的集合,这

些属性宿主于实体Bean 或者可以通过会话Bean访问到。一种方法就是客户可以通过多

重,细粒度的调用给服务端,获得他想要的数据(见图2.1)

Page 41: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

图2.1 一种从服务器获取数据的没有效率的方式

而这种方法实现的问题在于每个对服务器的调用都是一个网络调用,需要返回值

的序列化和解序列化,而ejb server会截获这些调用,完成事务处理和安全性检查以及所

需属性的收取,此时要阻塞客户端。而且,如果客户端没有用java 事务处理机制api,每个方法调用可能要在各自的单独的事务中运行。

以这种方式执行多重网络调用会造成性能的大幅度下降,我们需要一个更好一些

的选择,它允许将客户需要获得的所有数据放在一个调用里 生成一个被称为数据传送对象的java类,该对象是一个在网络传送的封装了

大量数据的数据包

一个数据传送对象是一个java类,这个类描述了server端数据的抽象属性的快照

(Snapshot),如以下所示 import java.io.Serializable;

public class SomeDTO implements Serializable {

private long attribute1;

private String attribute2;

private String attribute3;

...

public long getAttribute1();

public String getAttribute2();

public String getAttribute3();

...

}//SomeSTO

数据传送对象可以用来在分布式的系统中察看操作或者是修改操作。当客户需要在

服务端修改一些数据时候,它可以创建一个包含了所有服务需要修改数据时使用的信息

Page 42: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

的dto,并将它传给服务(通常是传给一个会话表面层).当然,也可以通过大量的细粒

度的参数来传送数据,但是这种方法非常脆弱。一旦属性被添加或删除,相应的方法就

需要变换格式。而通过将属性封装入dto,这些变换就仅限于dto本身了。

数据传送对象真正的应用是在读操作。当一个客户需要读取一些服务端的数据。客

户通过将数据封装入数据传送对象的方法,可以用一个网络调用获得所有他想获得的数

还是用前面那个例子,服务器端的ejb将创建一个dto,并装入客户需要的属性。

这个数据将以包的形式被返回到客户端--数据传送对象,数据传送对象可以被看成一个

“信封”,用来在j2ee系统中传递任何形式的数据。

当使用数据传送对象的时候,开发者面临的一个很普遍的问题就是选择何种粒度来

设计,具体地说就是,如何选择,选择多少数据装入dto? 根据什么决定DTO是必要的?

作为客户和服务端交换数据 主要的方法,数据传送对象构成了接口的一部分,将客户

开发者和服务端开发者隔离开来。在整个工程的开始,客户和服务端的开发者需要在数

据传送对象的设计上达成一致,同时,需要决定如何使用ejb的接口,即使不管这些需

要,在工程的一开始,数据传送对象的设计也会非常困难,因为开发者很难彻底,准确

的理解数据应以什么单位在客户和服务端之间传递。

刚开始设计数据传送对象,一个简单的方法是将其设计成一个服务器端实体

Bean(或者域对象)的拷贝,如DTO模式描述的那样,域 dto的设计比较简单,因为项

目组通常会对项目使用的域模型比较了解,因为这些需求都是在初始设计阶段形成的。

所以,以域 dto为单位构造客户和服务端之间的交换数据可以使一个团队得到更快的发

展。

图2.2 从服务器读取数据的一种有效率的方式

后,客户和服务端之间的数据交换的设计应该满足客户的需求,另外,当项目

的进展和客户的需求 后定稿的时候,由于交换单位的过于粗糙,而客户的要求相当精

细,所以域 DTO 经常变的相当大。而客户端需要可以简单的访问到数据,而不是被包

含在域 DTO 里。此时,开发者可以设计定制数据传送对象(custom data transfer objects),所以,数据传送对象可以包含了任何形式的数据,这是完全由客户不同的特殊的需求来

Page 43: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

驱动的。 总的说来,这两种设计范例的差异对应用程序的设计有着重要的影响。尽管他们

的实现途径完全相反,但它们却可以共存于任何 j2ee 应用中。 当你决定哪里该生成和使用数据传送对象的时候,数据传送对象的工厂模式提供

了一种令人信服的解决方法。无论是 DTOs,HashMaps 还是行集,一个主要问题,就是

任何数据在传送层的过程中,一旦数据到达了客户端,就有过期的可能性。第三章版本

号模式会告诉我们如何解决这些过期问题。 相关模式

State Holder Value Object (Alur, et al., 2001) Details Object 域数据传送对象(Domain data transfer object)

客户端需要从服务器端域对象模型访问和处理数据。 客户端如何从服务器端域对象模型访问,处理数据,而不需要执行上层远程的调用?

域模型,或者说域对象模型是指系统中与真实世界概念相对应的那一层对象,比如

一个人,一个银行帐户,或一辆汽车。在一个EJB框架中, 常见的域模型的例子就

是实体 Bean 的一个实际应用层。从这种意义上来说,一个域对象是一个具体的实体

Bean,例如 car 实体 Bean。一些其他的用于生成域模型的技术,也可以用于EJB框架

中,例如,java 数据对象(java data objects,)数据访问对象(data access objects),或者

由面向关系的 mapping tool 提供的私人域对象层(propietary domain object layer)。 在分布式的情形下,域对象模型完全位于服务器端。然而,根据所选择的执行技术,域

对象模型自身可以分为两种类型:一种可以使客户端通过网络远程的访问(实体Bean, JDO, 等),一种不能被远程访问(JDO, proprietary frameworks, 等)。对于后者,只

有通过其他位于于服务器端的事务组件才能访问到。 实体Bean可以远程被访问,客户端可以访问EJB对象(stub),这个对象保持着与

实体Bean的直接连接,而实体Bean位于服务器端。但是,正如会话表面层 pattern中描

述的,远程访问实体Bean不是一个有效的手段, 有效的访问方式不是从客户端访问实

体Bean,相反,客户端可以通过执行一个会话表面层上的方法,这个方法通过本地接口

可以和实体Bean直接交互,可以在网络调用中完成一个复杂的操作。当我们用了会话表

面层 pattern这种模式,就不需要从客户端远程访问实体Bean了。 这解决了一个关于直接从客户端使用实体Bean性能问题,但却将客户端置于非常

困难的位置:如果客户端不能访问实体Bean,(或任何域对象),怎么样才能够使在服务

器端上使用的同样的数据对象抽取(同样的域模型)在客户端也能发挥作用?怎样才能

使客户端读取,显示位于服务器端的特殊域对象的属性,在客户端使用这些具有相同的

面向对象语义的数据呢? 例如:考虑一个汽车经销的应用,在这个系统的事务需求中,定义了一辆汽车和一

个制造商的概念,以及在客户端实体Bean的实现。对这个应用的客户端部分来说, 直

观的事情就是显示,修改位于客户端的那些相似的汽车,制造商的数据抽象,一个客户

端将会从汽车,制造商那里读取数据,更新数据值,如果需要从其他实体Bean那里获得

数据,还和他们有横向联系。

Page 44: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

有关性能的考虑需要客户端不能直接访问实体Bean域对象模型,还有其他的一些域

模型对象甚至不可以被远程访问,但是,在客户端却期望能和域模型一起协作。这样,

就需要一种机制,它可以允许客户端详细研究和操纵服务器方的数据对象抽象。因此: 设计服务器端域对象的数据传送对象的拷贝。客户端只需对域对象的一份本

地DTO拷贝进行操作,包括进行大批读操作和更新操作 域数据传送对象是一个数据传送对象模式的特殊应用。数据传送对象模式只是简单

的说明了用dtos可以交换客户端和服务器端的数据。而域数据传送对象模式说明了数据

传送对象可以使客户端用 优方法当地访问服务器端域对象的dto拷贝。所以,域数据传

送对象可以使服务器端的域对象一对一的联系。例如,如果我们有一个账目实体Bean,就可以建立一个账目dto,如图2.3所示:

图2.3 帐户EJB和帐户域DTO

运用域数据传送对象提供了一个很简单的途径使得客户端和实体Bean对象模型在一

个分布式应用中进行交互: 显示实体Bean 数据:当一个客户端想要显示账目实体Bean的数据时,它可以

调用其上的一个getdto()方法,获取一个accountDTO,它包含了一个账目实体Bean的所有属性的拷贝。客户端可以执行多个get调用在本地的数据传送对象上,而不是

繁琐的网络调用。 修改实体Bean数据: 如果一个客户端想要修改账目实体Bean的内容,它必须

通过调用set()方法在本地的accountDTO的拷贝,来执行更新,然后将修改好的数

据传送到服务器端,覆盖原来的账目实体Bean。 显示相关的实体范围内的数据:实体Bean可以以一对一,一对多,或者多对多

的方式引用到其他一些实体Bean。通常一个客户端会从多个有关联的实体Bean中获

得数据,但是手工的通过这些实体Bean获得相互独立的数据传送对象拷贝会牵扯到

上层的网络调用,一个较好的解决方案是将那些相关联的实体Bean在服务器端组合

为一个数据传送对象的拷贝,再通过一次性的网络调用将其传递给客户端。这可以

通过一个被称为聚合传送对象(aggregate data transfer objects)的一个特殊的数据传

送对象来实现。聚合传送对象是用来包含相关实体Bean体系的数据传送对象的拷贝。

Page 45: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

客户端在这个本地的数据传送对象所含的体系结构中“漫游”就像“漫游”访问远

端的实体Bean一样。 创建实体Bean:为了创建一个账目,客户端可以在本地创建,装入一个

accountDTO,再将这个对象传递给会话façade.,它将在ejbcreate 方法重要到它,而

可维护性较差的一种选择就是将account所有的属性都作为方法的参数传给会话表面

层 和ejbcreat(),例如:下面哪一个更又可维护性呢?是ejbcreat(aDTO)还是

ejbcreat(attrib1,attrib2,attrib3,attrib4,attrib5, ...),?

关于域数据传送对象是易变的还是不变的,出现了许多争论。也就是说,允许

客户端修改域 DTO(通过调用 set 方法),还是 set()方法,使得域 DTO 只读?当使

用域 DTO 的时候,使其可变是有意义的。客户端知道这个是客户端的拷贝和实体

Bean 之间的交互,它能够读取,更新数据传送对象,就好象对实体 Bean 本身做了

同样的事情。既然客户知道数据传送对象是来自服务端的,一旦 DTO 被修改过,那

么他有责任将 DTO 传送回服务器端,这是合理的。那么不可变的数据传送对象的意

义在何处呢——当数据传送对象不代表服务器端的实体 Bean,就象定制数据传送对

象模式。在这里,数据传送对象仅仅代表一些只读数据的任意集合。

将域数据传送对象设计为服务器端的实体 Bean 的拷贝有以下这些好处: 域模型数据结构可以在一次网络调用中传递到客户端:实体 Bean 甚至是多重

实体 Bean 的拷贝可以在服务端组装,并在网络调用中传送给客户。而客户可以通过

本地数据传送对象读取,更新他们而不用进行上层的网络调用,这样客户就可以更

新通过传回数据传送对象来更新服务器。 易于快速建构一个功能性站点:在早期的开发过程中,客户对特定数据访问的

需要是不清楚的,易变的。尽管客户的用户界面是不明确的,应用中的实体 Bean对象模型通常已经建好。一个有功能性的应用可以通过应用实体 Bean 数据传送对象

这个客户和服务端之间的交换媒介来快速的建构。 客户端属性的校验:实体 Bean 的属性的系统的校验可以通过将校验逻辑嵌入域

数据传送对象的 set 方法中在客户端进行。这使得实体 Bean 编辑和创建时得错误在

客户端即可捕获,而不必通过网络调用,这只能会使服务器抛出异常。当然,属性

的语义/商业校验仍需要这样执行,但这些只在服务端被执行。

这个过程还需要权衡以下一些条件: 客户到服务器域对象模式的连接:在域数据传送对象模式中,客户端起的作用

是一个服务器端域对象(实体 Bean)的一个拷贝.所以,不管使用会话表面层与否 ,客户端是一个到位于服务端的对象模型的有效连接。如果一个实体 Bean 被改变了,

那么互相通信的数据传送对象也必须被改变,这样,所有用到这个数据传送对象的

客户端必须被重新编译。 不总是很好的满足客户端的需要:在服务端的实体 Bean 对象模型不总是能很好

的满足客户的需要。不同的用户接口可能需要不同的数据集合,而这些数据集并不

是实体 Bean 传送对象提供的一大堆数据的简单映射,一个客户可能想获得有一个

20 个属性的实体 Bean 其中的一个或两个属性,当用一个域 DTO 连传送 20 个属性

到客户,而客户却只需要 1 或 2 个的时候,对于网络资源来说是一种浪费。

Page 46: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

导致一个类似的层次:域 DTO 在域模型中复制对象将会导致属性,方法的重复 更新域对象的繁琐: 合并那些聚合数据传送对象(一个域对象包含了其他域对

象)中的变化是很困难,繁琐的。如果只有一个在树的深处的域数据传送对象发生

变化会发生什么?发现这需要编写冗长的代码。 读者可能已经注意到以上的例子表明了:域数据传送对象可以被实体 Bean 自身

创建,销毁。在 EJB1.X 时(在实体 Bean 有本地接口前),那时候实体 Bean 直接使

用 getDTO 和 setDTO 方法是很普遍的,而不是细粒度的 getAttribute/setAttribute 方

法,每个程序中的 entitiy Bean 包含了以下逻辑:创建一个数据传送对象自己的精确

拷贝(getDTO)和根据被更新值更新一个数据传送对象(setDTO),原因是所有对

实体 Bean 的调用默认都是远程调用,甚至是会话 Bean 或者是在相同服务端配置的

其他实体 Bean.数据传送对象模式就是为了优化调用而产生的,不管他们来自非 EJB客户或者会话和实体 Bean。随着 EJB2.0 本地接口的引入,会话 Bean 和其他一些实

体 Bean 不再需要用数据传送对象来优化实体 Bean 的访问。取而代之的是,只需要

用细粒度的 getAttribute/setAttribute 方法访问实体 Bean。现在数据传送对象主要用

于:客户和服务端之间的域对象拷贝的交换。 既然域数据传送对象不能被域对象自身创建,销毁,那么问题就出现了:应该

在何处创建,销毁数据传送对象呢?数据传送对象工厂模式提供了 好的实现代码,

另外一种相关的模式是定制数据传送对象模式,它从相反的角度考虑实体 Bean 数据

传送对象模式:数据传送对象应该是不变的,满足客户,而不是域模型的具体的需

求。

定制数据传送对象 Custom data transfer objects

客户端会发现域对象模型,以及相关的域数据传送对象已经不能很好的和他们

的需要相对应。 当域数据传送对象不合适的时候,数据传送对象应该怎么样设计呢? 数据传送对象模式引入了一种使用数据传送对象在客户和服务端之间传送大量

数据的概念,数据传送对象模式描述了一种设计数据传送对象的通用的方法——直

接映射到服务器端使用的对象模型,尽管这种设计数据传送对象的方法在以前的项

目中很有效,EJB 客户端需要更细化的数据访问。 例如,考虑一个汽车实体 Bean.一辆汽车应该潜在的由数百个属性来描述(颜色,

重量,型号,长度,宽度,高度,生产年份等),在 典型的情况下,一个客户可能

只对其中很小的一部分属性感兴趣。例如,考虑一个列出了一辆汽车的型号,年份,

类型的网页,为了导入这个网页,可能会传送一辆汽车的 carvalueobject(包含了所

有的属性)到客户,当客户只需要这三种典型的属性时,这是极大的浪费。 客户可能会有更复杂的需要,想象一个客户,它只需要从五个不同的相关实体

Bean 中获得一个或两个属性。为了优化网络调用,可以在服务端构造一个数据传送

对象的代表,这个服务将一个网络传送中需要的所有数据封装起来。一个解决方法

就是创建一个数据传送对象,它包含了到其他域数据传送对象的连接。这样,实体

Page 47: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

Bean 在服务端的那层将被拷贝到域数据传送对象的相应的层,但是这种方法在实现

的时候是极其繁琐的。如果客户在服务端每个的实体 Bean 中只需要一,两个属性,

将这些完整的域对象数据模型作为数据传送对象传送到客户端,既浪费时间又浪费

网络带宽。 另一个问题就是,客户端可能更需要来自数据源多种多样的数据,而不是服务

端的域对象,数据源,例如直接的JDBC调用和JAVA Connector Architecture适配器, 这些数据也需要被封装入数据传送对象,返回到客户端。因此:

设计定制数据传送对象,它包装了客户端需要的数据的任意组合,完全从服务

器端的域模型层中分离 定制数据传送对象就像其他一般的数据传送对象,区别在于他们是不变的,不

映射到任何服务端的数据结构(相对于可变得域数据传送对象),定制数据传送对

象提倡用例驱动的途径,这里面的数据传送对象是围绕客户需要来设计的。 从这个汽车的例子中可以想象,客户只想显示与发动机有关的一些属性,这种情

况,包含这些特殊属性的数据传送对象应该被创建,传送给客户端。这个定制数据

传送对象应该含有汽车属性的一个子集,如图2。4。 一般来说,如果一个EJB客户端需要属性x,y,z。那么应该创建一个包含,且仅

包含属性x,y,z的数据传送对象。用这种方法,数据传送对象可以看成一个约定,

当封装了服务端的数据,它提供了客户端所需要的数据。定制数据传送对象设计可

以很好的配合会话表面层模式,其中,实体Bean对象模型的细节被隐藏在会话Bean之中。在这方面,正确考虑数据传送对象的方法是将其当作数据,而不是所有服务

端的业务抽象,如实体Bean。如果所有的数据都来自同一个实体Bean,那当然很好,

否则,如何组装数据传送对象将是服务端的问题,而和客户端无关。

图2.4 封装了一个数据子集的定制DTO

一个典型的j2ee应用程序将会有越来越多的定制DTOs,以至于开发者情愿接受稍微

Page 48: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

粗糙点的DTOs(包含比需要多的属性),而不是重新再写一个新的定制DTOs.只要没有

太多的冗余数据被返回,这种方法还是很好的,对于任何一个模式,对于开发者来说,

都需要平衡在使用过程中的维护和性能之间的关系. 定制数据传送对象主要用于UI-specific 只读操作,而且是不变的,也就是说,

定制DTOs不能被改变,他们仅仅可以用来被显示。既然定制数据传送对象仅仅是一个

数据的集合,和任何服务端业务对象没有关系,那么对他进行更新是没有意义的。一般

来说,更新是通过实体Bean数据传送对象(因为他们代表了真正的事物对象,封装校验

逻辑(encapsulate validation logic))或者通过会话表面层的特殊用例方法. 定制数据传送对象几乎都是通过DTOfactory创建的(见数据传送对象制造模式),

是依赖于客户端的具体需求的。 数据传送哈希表(data transfer hashmap)

一个客户端需要以一种通用的方法和服务器端交换大量的数据。

怎样使人以数量的数据以一种通用的方法有效地在各层之间传递? 正如在数据传送对象和会话表面层模式中所讨论的,一个EJB系统中的执行是通

过减少实施给定用例所需要的网络调用的数量来实现的。特别是,数据传送对象提供了

一种在网络中传送大量数据的方法,使得客户端不需要执行多于一个的网络调用来传

送,接收数据。 使用DTOs的 普遍的方式是通过DTO工厂来使用定制DTOs。在这里,需要为系统中每

个新的用例写一个DTO, 提供给用户一个基于对象的包装器,它的作用是作为信封传

送用例所需的数据。尽管这种方法很简单,但是以这种方式使用DTOs的时候还是有一

些缺陷: 随时间发生变动的代价高:一个应用程序中的用例随着时间而改变,相对于初始化

的时候,不同的客户需要访问服务端数据的不同子集和视图(views),当我们运用定制

数据传送对象的方法时(甚至是DTO工厂),服务端的代码(比如新的DTOs和相关联

的创造逻辑)必须要满足客户端的数据访问需求的变化,一旦一个EJB项目被启动,如

同EJB的重部署过程一样,设计服务端访问部分的程序员的花费将变得昂贵。 需要创建一个数据传送对象层:数据传送对象创建了一个新的层次,一个大型应用

程序中这可以爆炸式的达到数以千计的对象。想象一个有30个实体Bean的分布式系统,

30个实体Bean中的每一个都需要一个域DTO在客户层之间来组织他们的状态,应用程序

的用例同样需要这些实体Bean中的数据用于不同的定制数据传送对象中。这样,一个中

等规模的系统就需要上百个定制的DTOs,每一个由一个特殊的factory method 来创建,

因为DTO层基本代表实体Bean层中的属性,实体Bean中属性的变化也会影响到多个DTO的变化,在大型的应用程序中,已经证实DTO层很难维护。

客户端UIs与服务端紧密连接:当使用定制DTOs,每个客户端UI都和它用于装填自

身的DTO紧密联系。当数据传送对象改变了,那么客户端需要重新编译,即使是这些变

化并没有影响到客户端。在一个典型的系统中有大量不同的UIs(想像一个有许多jsp组成

的大型网站),这也表明大量的依赖关系需要被维护。 如果一个应用程序的域相对比较简单,数据传送对象是一个很好的完成任务的方

Page 49: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

法。如果域还要复杂一点的话,数据传送对象需要一种选择方法。一个就是将正在被传

送的数据和包含数据的对象分离开,但是仍然允许通过层次访问,传送大量的数据。 因而:

用哈希表赖调度客户和EJB层之间的数据的任意组合。 JDK哈希表的平台提供了一种普遍的,容纳任意数据组合的可序列化的容器,这

可以替代一个完整的数据传送对象层。客户机和服务器端唯一要写的代码就是命名基于

键的规则,这将用于模式中今后的属性,描述的确认。 客户端通过会话表面层来请求哈希表,例如,在获得一个帐户的所有数据的用

例中,客户端将在会话Bean调用getaccountData这个方法,然后将返回一个封装了所有数

据的哈希表,见图2.5。更新数据可在本地相同的哈希表中进行。一旦更新完成,客户端

只需要通过将更新的哈希表返回到会话表面层来进行更新。

图2.5 使用数据传输哈希表

只需要将一个包含具体用例所需要的不同的数据组合的哈希表传给客户端,而

不需要为每个用例执行定制数据传送对象。例如,一个客户端需要比包含所有的账

目属性的哈希表更小的数据子集,会话表面层只需要返回一个包含更少属性的哈希

表。 使用哈希表而不是数据传送对象意味着增加额外执行的复杂性,因为现在客户

端需要明确的作为keys值的字符串,在哈希表中查询感兴趣的属性。另外,会话表

面层 和客户端需要对用于封装,读取哈希表的字符串(keys)达成一致,如果要进

一步对这个问题的解决方法进行讨论,参考Generic Attribute Access Pattern. 用哈希表来传递数据的好处在于: 完好的可维护性----消除了数据传送对象层。额外的域数据传送对象层和所有的

重复的数据传送对象创建逻辑现在都可以消除了,有利于表和访问属性借口的重用

Page 50: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

性。这表明了数以千计行的代码的减少,在和普遍的属性访问模式的连接中使用时

更是如此。 一个数据对象映射到所有的客户端: 属性的表从会话表面层到 JSPs 都是可重

用的,特别的,将一张表用作一个数据容器会有效的减少 JSP 代码的复杂性。因为

不需要再写那些和实体 Bean 层紧密联系的专门的用例的数据传送对象,

随时间的维护的代价低:一种新的服务端数据的观点可以不需要任何服务端的

程序即可创建。客户端动态决定显示的属性。 相应的,使用哈希表在网络中传送数据也有一些缺点:

需要为属性键值维护一张协议表:尽管属性的名字在一个 DTO 的 get 方法中是

一固定的代码,但是客户在使用哈希表的时候需要记住属性的键值。而且,客户和

服务端需要对键值进行统一,在客户和服务端之间创建额外的联系。好的和透彻的

文档也将有利于减小问题。

类型/编译时进行检查功能的丧失:当我们使用数据传送对象的时候,传递给 gets或 sets 方法的值总是正确的类型。任何错误都将在编译的时候通过。当我们用哈希

表的时候,属性的访问必须在运行时由客户管理,由他们正确的类型构造对象,将

正确的属性类型和正确的键值联系起来。

基本属性需要被封装:基本属性,类如 int,double,long 不能保存在表中,因为

他们不是对象的子类。在被放入表前这些基本属性需要人为的封装成合适的对象

(Integer 代表 ints,Long 代表 long,Double 代表 double 等等)。

每次读取需要类型转换(casting)。无论何时从哈希表读取数据,都需要将其从

对象类型转换成合适的类型,这将复杂客户断代码,降低性能。

使用哈希表来概括表示会带来突出的优势,同样也有缺点。大多数开发者发现他

们还是习惯使用合适的数据传送对象。他们能较好的理解使用的模式,而且对于客

户端开发者来说,也不容易发生错误。而数据传送哈希表是一把双刃剑,它既能解

决许多维护性问题,但同时也能产生一些问题。决定使用哪种方法需要基于项目的

需要好好判断,。

数据传送行集:(Data transfer 行集)

当用 JDBC 读取的时候,相关数据需要通过网络从会话表面层传到客户端。 相关的数据如何以一种通用的,表格的形式传到客户端? 这种用 JDBC 进行读取的模式,在执行一个普通的表格数据的只读查询时,(例

如在 HTML 页或 applets 导入数据表),提倡用会话 Bean 执行直接的 JDBC 调用访

Page 51: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

问数据库,(而不是通过实体 Bean 层的查询)。这种方式可以提高性能,如果你使用

BMP,或者你的 CMP 不支持登陆大量的实体 Bean,或者你不能利用实体 Bean 缓冲(见 JDBC 读取模式的更多信息)。 。 当会话表面层 执行 JDBC 调用时,问题随之而来:将数据整合,传给客户的 好

的方法是什么呢? 普遍的解决方法是用数据传送对象。例如,考虑雇员/部门例子,

用 JDBC 读取模式。这里我们想要获得一张数据表,列出了一个公司所有的雇员和

他们所在的部门。见图 2.6 数据传送对象可以用来获得这张表,通过创建一个定制雇员部门 DTO,如下所

示:

图2.6 雇员HTML表

这里,会话Bean将执行一个JDBC调用来获得一个包含雇员和它所属部门信息的

结果集,会话Bean将手工从结果集中提取出字段,调用必需的setters方法装填DTO,

结果集中的每一行都将被放入DTO,这个DTO将被加入一个集合。这个DTO的集合

形成一个网络传送包,被传送到客户端选用。 正如在数据传送哈希表模式中所描述的,用DTOs作为数据传送机制将会可维护

性的问题,因为需要经常创建很大的DTO层,当用JDBC读取的时候,DTOs还会遇

到其他的问题: 执行:从表格到对象然后再回到表格是冗余的。数据已经在结果集数据表中的

行中显示,那么将数据传送到一个对象集合中,在传回到由行和列组成的数据表(在

数据端UI)是多余的。 当用JDBC读取的时候,理想情况下,考虑到客户端和解析成客户UI的简洁性,

应该采用一种可以在以通用的方式进行传送时保持数据的表格性质的数据传送机

制。 所以:

Page 52: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

用行集来直接从EJB层到客户层的结果集中整合原始的相关数据。

接口 Javax.sql.行集在JDBC2.0中作为一个可选择的接口引入,它是一个

java.sql.resultset的子接口。将行集和ejb开发者联系起来的是,具体执行行集接口将

允许你封装结果数据,将其整合到客户层。如果他们在结果集中的话,客户可以直

接操做那些结果集中的行和字段。这允许开发者可以将表格数据分离于数据库之外,

使他们可以在客户层容易的修改数据表,而不需要手工将结果集中的数据映射成其

他形式(如数据传送对象)再用UI,JSP等传回数据表。 将数据传递给客户层,这种行集的实现类型必须是非连接的行集.就是说,行集

不需要保持与数据库的连接。SUN提供的这样一种实现被称作为缓冲行集

(cachedRowset)。一个缓冲行集允许在结果集中拷贝数据并将这些数据带至客户

层。因为cached行集与数据库是断开的。可选择性的,你可以创建一个自己定制的,

不连接的行集或结果集接口的实现,用他们整合表格数据到客户。 在我们的雇员和部门的例子中,用行集将允许我们获得一个对象中完整的雇员

部门的数据表,并将其传递给客户端。图2。7描述了行集的实现与数据传送对象的

实现不同的地方。 为了创建这个行集,会话表面层执行直接JDBC调用的方法需要写成如下形式: ...

ps = conn.prepareStatement(“select * from ...”);

ps.execute();

ResultSet rs = ps.getResultSet();

RowSet cs = new CachedRowSet();

cs.populate(rs);

return cs;

...

在客户层,行集的数据可以直接被映射到表的列和行。

Page 53: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

图2.7 数据传送对象和行集

行集提供了一种很清晰,实用的方法从JDBC结果集中整理数据,直接到客户端

UI,不用上层的将数据转成数据传送对象,在传回到客户端列表。 用行集作为一个整合数据的方法会带来许多好处: 行集提供一种对所有查询操作的通用的借口:用行集方法,所有的客户可以用

相同的接口满足所有的数据查询需要,不论是什么用例,或者返回什么数据,客户

端操作的接口总是相同的。这个与用上百个与具体用例定制DTOs紧密相连的客户端

UIs是截然相反的。即使是当客户端数据访问需要改变,而改变数据传送对象,行集

接口还是保持不变。 消除多余的数据转移.行集可以直接从JDBC结果集中创建,而不用一步步从结

果集中转移到DTO,在传回到客户端的表中。 允许自动操作。因为行集接口从不改变,它可以用来创建图形用户接口(GUI)

构造器,比如taglibs。这些构造器知道如何使用行集和在用例间反复使用他们。和

DTO实现相比,不同之处在于DTO需要定制代码来显示本身。 这里是相应的一些缺点:

客户端需要知道数据库表中每一列的名字。持久性模式对客户端应该是透明的,

Page 54: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

比如表中列的名字。使用行集时,一个客户端需要知道数据库中列的名字,才能获

得相关的属性。就如一般属性访问模式中描述的那样,这个问题可以通过一个保持

一种客户机和服务器端的协定(或者非常好的文档)来减缓,。 忽略域模型(非面向对象的特征).脱离对象范例的做法看起来有点背离一些基

于数据传送对象/实体Bean的大多数的j2ee模型。毕竟,将一大团数据装入一个通用

的表格对象显得很不具备OO的特征。当使用行集时候,我们没有试图反映出任何业

务的概念,数据本身作为一个业务的概念呈现在用户面前,而不是任何事务之间的

关系。 不可以在编译时检查查询结果。一个客户必须调用行集上的getString(“XXX”),

而不是调用数据传送对象上的getXXX()。这使得客户端的开发容易出现不能在编

译时捕获的错误,比如说客户想从行集获得的属性名字的拼写错误。 必须记住的重要的一点是,尽管一些行集接口的实现是可以被更改,并且与数

据库的变化保持同步,但是一个开发者绝不能在应用程序中使用这种手段进行更新。

更新必须通过在会话表面层 方法中传递参数实现,或者使用数据传送对象。 另一点需要考虑是,javax.sql的行集接口没有什么特别的地方,它只是官方JDBC规

则的一部分和存在的一种工作实现而已。开发者可以编写他们自己的功能和行集类

似的类(或者简单的包含一个cached行集),继承所有的好处。创建一个并非行集

接口的扩展的定制实现的原因是为了隐藏所有行集接口的变更类(插入/更新/删除)

方法,因为这些不能在客户层使用。 数据传送行集仅仅用在只读数据,与JDBC读取模式相结合。

Page 55: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

第三章 事务和持久性模式

这一章包含了各种不同模式的集合,它们所解决的问题涉及事务控制(Transaction

control)、持久性(persistence)和性能等方面,具体的模式包括:

本版号(Version Number) 当处理涉及多个事务和用户思考时间(user

think time)的用例时,使用它来将实体 bean 与能够保护数据库一致性

的乐观的并发检查逻辑结合在一起编程。

使用 JDBC读取数据(JDBC for reading)出于性能方面的考虑,在这个

有关性能提高模式的部分中,讨论了何时可以忽略实体 bean 层而选择直接

使用 JDBC来访问数据库,并且讨论了这样做所涉及的所有语义。

数据访问命令 bean(Data Access Command Bean) 提供了一种标准的将

一个企业 bean 与持久性逻辑和持久性存储的细节相隔离的方法,这使得编

写持久性逻辑变得相当容易。

双重持久性实体 bean(Dual Persistence Entity Bean) 这是一个专

为构件开发者设计的模式,双重持久性实体 bean 展示了如何编写一个这样

的实体 bean:它只需编译一次,就可以被发布到一个 CMP 或 BMP 引擎中,

需要的只是编辑不同的发布描述符。

Page 56: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

版本号(Version Number)

如果一个客户端在服务器端激发一个更新时所使用的数据是在前面的事务中被读取的,

那么这个更新所使用的数据可能是陈旧的。

你怎样才能判断用来更新服务器的数据是否是陈旧的呢?

事务允许开发者对他们要处理的数据做出某种假设,这些假设之一就是事务将与其它的

事务隔离执行,从而通过假设在事务中被读写的数据是 新的且一致的来允许开发者简化他

们的代码。

在一个 EJB context 中,这意味着当一个用例被执行时(通常作为在一个声明式事

务下运行的会话表面层的一个方法),它的代码可以基于下面的假设来更新一个实体 bean

集合:没有其它任何事务可以修改与它目前正在修改的实体 bean相同的 bean。

当一个用例可以只在一个事务中被执行时,事务隔离将运转良好,但是当用例跨越多个

事务时,事务隔离将被破坏。一个用户需要在执行服务器上的更新之前手工处理一个数据片

断的情况就是这样的一个典型用例,它们需要一个被称为用户思考时间(user think

time,也就是用户将更改输入到表格中的时间)的时间间隔。用户思考时间的问题是它太

长了,这使得把从服务器读取数据、用户思考、更新服务器的整个过程包装在一个事务中变

得不可行(而且在 EJB中也不可能实现)。数据通常在一个事务中从服务器中读出,由用户

处理它,然后在第二个事务中被更新到服务器。

这种方法的问题是我们再也不能保证与其它事务的修改相隔离。例如,我们来看一个消

息白板管理系统,多个用户使用其缓冲器(moderator)来访问一个消息论坛。一个常见

的用例是编辑一个发送给用户的诸如“断开联接”或“内容不正确”这样的消息的内容。在代

码级别上,这将涉及在一个事务中获取一个消息的数据,在用户思考时间内修改它,然后在

第二个事务中更新它。现在考虑当两个缓冲器 A 和 B 试图在同一时间编辑相同的消息时会

发生什么:

1. 缓冲器 A在一个事务中读取消息 X

2. 缓冲器 B在一个事务中读取消息 X

3. 缓冲器 A在它的消息副本中执行本地修改

4. 缓冲器 B在它的消息副本中执行本地修改

5. 缓冲器 A在一个事务中更新消息 X

6. 缓冲器 B在一个事务中更新消息 X

一旦步骤 6 发生,所有由缓冲器 A 执行的更新将被由缓冲器 B 所作的更新所覆盖。在

步骤 5中,缓冲器 A成功地更新了消息 X,这时,其它用户所持有的这个消息的所有副本都

成为了陈旧的数据,因为它们再也不能反映这个消息实体 bean 的当前状态了。这样,缓

冲器 B更新的消息就是基于陈旧数据的了。

在一个消息白板系统中,这样的问题可能不会引起人们特别的关心,但是想象一下相似

的问题如果发生在医疗或银行系统中,那将是多么严重的灾难。在这里,问题的症结是缓冲

器 A 和缓冲器 B 的行为没有彼此隔离开。因为使用了互相独立的事务来执行读取和更新步

骤,所有没有任何方法能够自动检查出用来更新服务器的数据是基于一个已经变得陈旧了的

数据读取操作的。

因此:

使用版本号来实现在实体 bean中的你自己的陈旧性检查。

一个版本号是一个作为成员属性添加到实体 bean(以及它底层的数据库表)中的简单

Page 57: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

的整数。插入这个整数的目的是标识实体 bean 在任意时间点上的状态,这可以通过每当

实体 bean被更新时,递增这个 bean的版本号来实现。通过使用下面的过程,版本递增将

允许你去探测基于陈旧数据的更新:

1. 在取读事务中将版本号连同其它数据从实体 bean 中读出。这通常是通过将一个

实体 bean 的版本号添加到任何用来将它的数据复制到客户端的数据传送对象中

来实现的。 2. 将版本号连同其它更新数据发送回实体 bean。到执行更新操作时,将原始的版本

号连同 新的更改过的数据发回,然后在执行任何更新之前,将这个版本号与这个

实体 bean中当前的版本号进行比较。 3. 当执行一个更新时,递增这个实体 bean 的版本号。如果这个实体 bean 当前的

版本号与从客户端发回的更新数据中的版本号相等,那么就更新这个实体 bean,

并递增版本号。

4. 如果版本号不匹配,则拒绝更新。如果一个更新所发回的版本号比实体 bean 当

前的版本号旧,那么这就意味着这个更新是基于陈旧数据的,所以将抛出一个异常。

以这种方式使用版本号可以保护实体 bean 不受在一个用例跨越多个事务时可能发生

的隔离问题的影响。考虑论坛缓冲器的例子,如果在步骤 1之前,消息 X的版本号是 4,那

么缓冲器 A 和缓冲器 B 将在他们的消息本地副本中获取这个版本号。在步骤 5,缓冲器 A

的更新将被成功执行,因为它发回的版本和在消息 X 中的版本是相匹配的。这时,消息 X

的版本号将从 4递增到 5。在步骤 6,缓冲器 B的更新将失败,因为这个缓冲器发回的版本

号是 4,而消息实体 bean X的当前版本号是 5,它们并不匹配。

当探测到一个陈旧的更新时,通用恢复过程(recovery procedure)将通知 终用

户已经有人先与他们进行了更新,并且要求他们在服务器端数据的 新副本的基础上重复他

们的修改操作。

版本号模式的实现将依据用来访问实体 bean 的机制的不同而有一些细微的差别。如

果我们使用数据传送对象来直接获取(get)和设置(set)在实体 bean中的大量数据(就

象在 EJB 1.X应用那样),那么版本号将添加到实体 bean的 getXXXDTO方法的 DTO中,

并且将在实体 bean的 setXXXDTO方法中与当前的版本号进行比较检查,就象下面的代码

块所示: public void setMessageDTO(MessageDTO aMessageDTO)

throws NoSuchMessageException

{

if (aMessageDTO.getVersion() != this.getVersion())

throw new NoSuchMessageException();

this.setSubject(aMessageDTO.getSubject());

this.setBody(aMessageDTO.getBody());

}

但是,就象在数据传送对象工厂(DTOFactory)模式中讨论的那样,在 EJB 2.0中,

使用 DTO 来作为直接访问实体 bean 的机制是一个不推荐使用的做法,作为它的替代物,

DTOFactory/会话表面层将负责通过使用实体 bean的 local接口直接调用 get/set方

法来从一个实体 bean中获取数据以及更新这个实体 bean。

在使用这个范例时,一个会话 bean 负责直接通过实体 bean 的 set 方法来更新它;

这样,这个实体 bean 就可以在更新它自己时再也不用自动地检查数据集的版本了,相对

应的是,开发者必须采用这样的编程惯例:要记住在更新过程之前总是要传递将要更新的数

Page 58: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

据集的版本号,就象下面的会话 bean的方法一样: public void updateMessage( MessageDTO aMessageDTO)

{

Message aMessage;

try //更新想要更新的消息 {

aMessage = this.messageHome.findByPrimaryKey(

aMessageDTO.getMessageID() );

aMessage.checkAndUpdateVersion(aMessageDTO.getVersion());

//更新消息 aMessage.setBody( aMessageDTO.getBody() );

aMessage.setSubject( aMessageDTO.getSubject() );

}

catch(IncorrectVersionException e)

{

this.ctx.setRollbackOnly();

throw new StaleUpdateException();

}

catch (...)

...

}

在 checkAndUpdateVersion调用中,这个消息实体 bean对作为参数传递的版本号

和 它 自 己 内 部 的 版 本 号 进 行 检 查 , 如 果 不 匹 配 , 将 抛 出 一 个

IncorrectVersionException 异常;如果版本号匹配,那么这个实体 bean 将递增它

自己的内部计数器,就象下面的代码块所示: public void checkAndUpdateVersion(long version)

throws IncorrectVersionException

{

int currentVersion = this.getVersion();

if( version != currentVersion)

throw new IncorrectVersionException();

else

this.setVersion( ++currentVersion );

}

这里描述的版本计数模式也同样可以作为你自己的乐观的并发处理的实现思想。不同于

对一个被长时间运行的用例所使用的实体 bean 进行相对于其它并发访问的锁定操作,我

们允许多个用户访问这个数据,只是在我们探测到一个更新使用了陈旧的数据作为它的修改

基础的时候,才拒绝这个更新。实现了乐观的并发处理的数据库使用类似的机制以允许多个

客户端读取数据,只是在发生冲突时才拒绝写操作。

类似的实现还可以在使用时间戳(timestamp)而不是版本号的系统中找到。这两种

实现基本相同,使用版本号要相对简单一些,并且可以保护系统不受下面这些不太可能发生

的事件的影响:服务器的时钟向回走,或者数据库的日期和时间有足够小的间隔以至于不能

Page 59: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

消除产生不合法的陈旧性检查的可能性。这些事件所产生的问题在使用时间戳的系统中是不

能解决的。

版本号模式确保了涉及多个事务的用例彼此之间的修改被正确隔离,同样,也确保了在

一个单一事务中执行的用例与其它事务的操作被互相隔离。然而,在一种很罕见的情况下,

即两个缓冲器试图在精确相等的同一时刻更新服务器(步骤5和步骤6),将会发生什么呢?

在这个例子中,两个包含有相同版本号的消息实体bean的实例将都被载入到了内存,这样,

两个实例对checkAndUpdateVersion 方法的调用都会成功。一旦第一个事务被提交,

那么问题就变为:在试图提交第二个事务时,将会发生什么?

答案是第二个事务将被正确回滚,因为这两个事务是同时发生的,二者具有相同的事务

隔离级别语义,这个事务隔离级别语义可以保护在一个事务中被执行的用例,因此它将保护

这个特殊操作避免冲突。它实现的方法依赖于你的数据库/应用服务器是如何进行并发处理

的:

READ_COMITTED的事务隔离级别和应用服务器CMP实现的更新认证。这里,应用

服务器将比较在消息实体 bean 中被修改的属性(包括版本号)和在提交前数据

库中的相应内容。如果这些内容不匹配(因为前一个事务递增了版本号并且修改

了其它属性),那么应用服务器将回滚这个事务。这是一个在应用服务器级别上

实现的乐观的并发检查,它允许你只使用READ_COMITTED的事务隔离级别,因

为应用服务器保证了数据一致性。 READ_COMITTED的事务隔离级别和在BMP中实现的更新认证。BMP开发者可以通

过比较在当前Bean中的版本号和在ejbstore中存储的在数据库中的版本号来手

工地实现对更新的认证。这可以通过修改SQL UPDATE 语句使其包括一个where

version =X 子句来实现。对于第二个事务,即使缓冲器A 的事务仅在几毫秒之

前更新了数据库,它的where 子句都将失败,开发者可以手工回滚这个异常。

SERIALIZABL的事务隔离级别和一个使用乐观的并发处理的DB。如果乐观的并

发处理没有在应用服务器级别上实现,那么必须使用SERIALABLE 的事务隔离级

别来确保数据的一致性。如果数据库本身实现了乐观的并发处理检查,那么它将

在探测到ejbstore正在试图覆盖由第一个事务插入的数据时自动地回滚缓冲器

B的事务。

SERIALIZABL的事务隔离级别和一个使用悲观的并发处理的DB。同样的,由于

应用服务器不会强制保持数据一致性,所以还是要使用事务的SERIALIZABLE隔

离级别。然而,由于数据库使用的是悲观的并发处理策略,它将对数据库中表示

消息 X 的行加锁,强制第二个事务的MessageEntity.ejbLoad()方法一直等

待,直到第一个事务的MessageEntity.ejbLoad()方法完成并提交。这意味

着当缓冲器B的事务调用checkAndUpdateVersion 时将失败,因为消息 X 没

有被执行ejbload()操作,直到缓冲器A的事务提交之后才能执行。

SERIALIZABL的事务隔离级别和一个SELECT FOR UPDATE操作。某些应用服

务器允许通过编辑发布描述符的设置,来把CMP引擎设置成在ejbLoad过程中产

生一个SELECT FOR UPDATE操作。这是为了强制那些使用乐观的并发处理的数

据库真正地锁定底层的行,这将使事务象在前面选项中描述的那样被执行。

这里要特别提出的一点是:在这类罕见的更新同时发生的实例中,必须要维护数据的一

致性,要么第二个事务将在调用checkAndUpdateVersion 时被探测到冲突,要么应用

服务器或数据库将探测到冲突并回滚这个事务,无论怎样,都要维护数据一致性。

在使用版本号模式时另外要考虑的很重要的一点是,当有一个遗产应用或非java应用

所更新的数据与你的EJB应用所使用的数据相同时可能会产生问题。遗产应用将有可能使用

Page 60: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

版本号,这将在EJB应用和遗产应用之间产生数据一致性问题。如果它处于你的控制之下,

那么就能够确保非java应用和遗产应用在执行更新时同样可以正确更新版本号;如果更改

遗产应用已经超出了你的控制范围,那么另一种解决方案就是在数据库中实现能够在数据库

中自动更新版本号的触发器(trigger)。如果你不使用这个方法,那么就不要忘记从你

的实体 bean中移去有关版本号递增的代码。

版本号模式 经常的是被作为一种保护方法来使用,它可以保护系统在使用数据传送对

象时发生陈旧更新后不受其影响。一旦使用一个DTO来从服务器上复制一些数据,那么这些

数据就潜在地有可能是陈旧的,版本号将帮助我们在更新时刻探测到这些陈旧数据。

Page 61: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

使用 JDBC 读取数据

在一个后端使用关系型数据库的EJB系统中,为了显示的目的,一个EJB客户端需要获

得一个对服务器端数据的列表用户接口(tabular user interface)。

一个会话表面层应该在何时执行直接数据库访问操作而不是通过实体 bean 层来访问呢?

可能在分布式应用中遇到的 普通的用例就是需要向客户端以表格的形式来显示静态

的服务器端的数据。列表UI的例子构成了Web页的大部分内容,在Web页中数据被列表为表

格或行,例如在一个目录中的条目的列表(与其相反的非列表UI的例子包括树形和循环UI

等)。而且,这些列表数据通常都是只读的,而且客户端想要浏览的比更新这些页面要更多。

一种常见的情形是:一个需要向用户表示大量只读数据的应用可能被构建为一个HTML

表格的形式。这个表格可能表述了一个大型订单中的若干条目、一个公司的所有雇员的信息

或一个公司生产的所有产品的特性等。

在图3.1中,在表格中的每一行对应于系统中的一个雇员以及他/她所在的部门。在服

务器端,我们将它建模为一个Employee和一个Department 实体 bean。一种获取表格

的方法是调用在会话表面层/数据传送对象工厂上的getEmployees()方法,它将通过调用

一个在EmployeeHome对象上的查找方法来查找与Department 实体 bean 相关的每一

个Employee,并且将创建一个定制的把从这两个实体bean中获取的数据和并在一起的数

据传送对象,然后会话 bean将返回一个EmployeeDepartmentDTO集合给客户端。

图3.1 employee的HTML表格

对于各种不同的EJB服务器和应用,使用这种方法都存在着很多问题:

N+1 个实体数据库调用问题。如果使用BMP和某种CMP的实现,那么从N个实

体bean中获取数据将需要N+1个数据库调用。尽管一个好的CMP实现将允许大

量数据的加载,开发者还是应该意识到这个可怕的问题。N+1个调用问题的描

述如下:为了从N个实体 bean中读取数据,必须首先调用一个查找方法(一

个数据库调用),然后容器要么直接在查找调用之后,要么在一个业务方法被

调用之前,在由查找方法返回的每一个实体bean 上分别执行 ejbLoad()方

法,这意味着将需要为每一个实体bean 调用ejbLoad()方法(将在一个数

据库调用中被执行)。这样,一个简单的通过实体 bean 层的数据库查询操

作将需要N+1个数据库调用!每一个这样的数据库调用都将在连接池中临时性

地锁定一个数据库连接,执行打开和关闭连接,打开和关闭结果集等操作。因

为大多数分布式系统都为数据库设置了一个独立的运行环境(box),所以到

这些数据库的每一个往返调用都需要一个网络调用,这就降低了每一个往返调

用的速度,并且对系统的其它部分锁定了很多有价值的数据库资源。对于我们

的Employee和Department的例子,运行它将实际需要2N+1个数据库调用

Page 62: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

(一个查找方法,N个Employee的ejbLoad(),和N个Department的

ejbLoad())。

远程调用的开销。如果要通过实体 bean的remote接口而不是local接口来

调用,那么这个方法将需要3N个远程调用来处理N行雇员和部门数据。这些远

程调用包括:

N个对每一个Employee 实体 bean上的getValueObject()方法的调

N个对每一个Employee 实体 bean上的getDepartment()方法的调用

N个对每一个Department 实体 bean上的getValueObject()方法的

调用

在获取了每一个值对象(value object)集后,这个会话 bean将这些值

对象组合在一起置于EmployeeProjectViewObject中。

对简单的连接(join)操作来说太麻烦。无论我们是使用BMP还是CMP,这

个典型用例都需要多个实体bean的实例化操作和用来查找它们之间的相互关

系的连接操作。想象一个稍微复杂一些的情形,一个表格需要列出来自

Employee和与其相关的Department、Project 和 Company 实体 bean

的数据,这不仅需要数十行的象意大利面条一样的代码,而且还会因数据库调

用、远程调用和在多个实体 bean的相互关系中执行连接操作时所引起的应用

服务器开销而明显地降低系统的速度。

当客户端主要是为了只读的列表(listing)目的而请求列表数据时,通过实体 bean

层来执行查询的好处将损失很多。使用local接口和一个好的CMP实现将明显地减少通过实

体bean来列表数据时所产生的性能问题,但是BMP的开发这就没有这么幸运了。在BMP中,

这些问题只能通过打开实体bean的缓存来得到缓解,这个缓存是很奢侈的,它通常只对单

一的(或非集群的)EJB服务器上的发布是可用的,在这些发布中,数据库从来都不会被EJB

应用之外的东西所修改。

BMP 开发者还面临着一个严重的性能问题,如果只是为了列表只读数据而通过实体

bean 层来执行查询操作将会引起令人无法接受的性能问题。

因此:

在BMP中,使用JDBC来执行在关系型数据库上的列表操作,而使用实体 bean来执行

更新操作。

如果客户端UI请求的数据主要是被用于列表的目的,那么使用JDBC来直接读取客户端

请求的行和列将比通过实体 bean层来读取要快速而高效得多。在前面的例子中,整个

employee和department表可以只在一个JDBC调用中从数据库中大量地读取,而不是象

在通过实体bean层来读取时,潜在地需要3N个远程调用和N+1个数据库调用。

数据在从结果集(ResultSet)中被读出后,就可以像前面的例子一样被添加到

EmployeeDepartmentDTO中了,或者用HashMap打包(marshalled)发送给客户端(就

象数据传送HashMap模式一样),还可以使用行集(RowSet)以表格的形式打包发送给客

户端(就象数据传送行集模式一样)。

作出使用直接JDBC方式而不是通过实体bean 来读取数据的决定对于大多数的开发者

来说都是很艰难的,自从实体 bean 出现以来,它就成为了人们激烈争论的话题。毕竟实

体 bean 提供了一个良好的数据与数据逻辑的封装,它们隐藏了诸如正在使用的数据库类

型等持久性方面的细节,并且对你的系统中的业务逻辑进行了建模,而且还利用了诸如实例

池、并发控制、事务管理等许多容器的特性,所以使用非面向对象的数据访问方法看起来象

是后退了一大步。而且,象其它的所有模式一样,它也有缺点。

Page 63: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

使用JDBC读取数据的好处如下:

对简单的查询操作将没有任何事务方面的开销。只读操作不需要使用事务,来自

关闭了事务管理的无状态会话bean 的数据库查询相比实体bean查询要显得更

轻量级一些。通常实体bean查询不使用事务是不可能的。

充分利用了DB内置的缓存。数据库都具有复杂而强有力的缓存,通过使用JDBC

来执行这些操作,我们可以更好地利用DB内置的缓存。这在执行跨越多个表的查

询时就显得很重要了,因为数据库可以缓存这个块查询的整个结果,而不是缓存

由实体bean的ejbLoad调用所产生的对单个表查询的结果。在下一次运行某个查

询时,将直接从数据库的缓存中执行这个块JDBC查询。

获得你的用例所请求的精确的数据。通过使用JDBC,你可以选取所需要的在任意

数量的表中的精确的列。这与使用实体 bean层时,客户端可能只需要从多个相

关联的实体 bean中获取少数几个属性形成了鲜明的对比,即使一个客户只需要

一个属性,那些实体 bean也需要从数据库中加载它们所有的属性。

在一个块读取操作中执行查询。一个客户端所请求的所有数据都是在一个块数据

库调用中获得的,这直接与由使用实体bean而产生的N+1个数据库调用问题形成

了鲜明的对比。

它的缺点是:

业务与持久性逻辑之间的紧耦合。当使用实体 bean时,开发者不知道其底层的

持久性机制是什么。但是使用这种模式,会话bean的数据查询逻辑就要与JDBC

API 耦合,这样也就和关系型数据库耦合了。然而,使用其它设计模式,例如使

用数据访问对象模式(Data Access Object pattern,在这本书中并没有阐

述)可以缓解这个问题。

容易出现Bug并且不易维护。如果使用这个模式,那么容易出现Bug的JDBC代码

就与会话bean层混合在一起,而不是被良好地封装在实体 bean中。更改数据库

结构时还将需要更改贯穿于会话表面层中的多个代码片断。同样,数据访问对象

模式对解决这个问题也有帮助。

后要说明一点,这个模式并不意味着实体 bean 应该被彻底放弃,而只是表明当客

户端只需要临时列表数据时,有其它更高效的选择。在这个模式中,JDBC被用于应用中的

列表行为,实体bean层被用于应用中的更新行为。

尽管当我们在客户端上列表数据时,业务/数据对象与它们和其它业务对象之间的关系

的集成显得并不是那么重要,但是这些观念在执行更新操作时就显得非常关键了。实体bean

(或其它任何数据对象框架)封装了数据和更改这些数据的规则,当更改一个实体bean的

属性时,这个实体bean可能需要在它的更改上执行验证逻辑,并且启动在应用中的其它实

体bean的更新操作。

例如,考虑一个书(Book)和章(Chapter)实体bean的应用。当我们修改了一个

Chapter 实体 bean的title属性时,这个Chapter需要在新的title上执行验证操作,

并且要在内部调用和修改它的Book bean以通知这个Book去修改它的表中的内容,然后

Book 实体bean需要修改其它的实体 bean,之后是诸如此类的一些操作。

在会话表面层中通过JDBC执行更新操作将强制开发者编写混合了业务逻辑和复杂的数

据处理逻辑的象意大利面条一样的代码。某个特定的业务概念(business concept)所

需的所有规则、关系和验证将不得不被剪裁为在行和表上执行更新的形式,这样,系统对于

更改应用的业务需求来说将变得非常脆弱。

Page 64: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

因此,在客户端UI 请求只读的列表数据并且不可能得到实体bean缓存的情况下,就

应该使用JDBC而不是通过实体bean层来从数据库中读取数据,而所有的更新操作仍然要通

过域对象(实体 bean)层来完成。

使用JDBC读取数据的模式往往出现在一个会话表面层或一个数据传送对象工厂的幕

后:这取决于用来向客户端传送结果集内容的对象类型。(DTO工厂意味着将返回给客户端

DTO,从会话表面层可以返回HashMap或RowSet)。

相关联的模式

快速通道读取器(Fast Lane Reader)(J2EE 蓝皮书,J2EE Blueprints)

Page 65: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

数据访问命令 Bean(Data Access Command Bean)

一个企业 Bean需要访问一个持久性的数据存储库。

怎样才能将持久性逻辑和持久性存储细节分离开,并且将它们与企业bean的业务逻辑

分开封装呢?

当使用一个直接访问数据库(没有任何实体bean)的会话 bean层来编程,或者是编

写BMP 实体 bean时,通常的做法是将持久性逻辑混合到会话 bean 或 实体 bean中。对

于会话 bean,这样做通常需要编写与具体的数据存储有关的访问代码(例如JDBC操作语

句),这些代码是与业务逻辑混合在一起的。对于实体 bean,标准的做法是在

ejbCreate()、ejbLoad()、ejbStore()和ejbRemove()方法中编写JDBC操作语句。

尽管这样可以完成我们所需的工作,但是这个方法也存在着若干缺点:

数据逻辑与业务逻辑相混合。将持久性逻辑与业务逻辑相混合会对可维护性带来

可怕的后果。业务逻辑变得很难与象意大利面条一样的持久性代码区分开,而且

持久性代码将被传播到整个业务层中,而不是指被局限在本地的一个层内。

与特定的持久性数据存储库(数据库)类型的紧耦合。如果将一个特定的持久性

API(例如JDBC)编码进在你的业务逻辑中,你的应用就和一个特定的数据存储

库类型(OODBMS、RDBMS和遗产系统)紧耦合在一起了,这使得切换数据存储库

类型变得很困难。而且,对于集成了遗产系统并包含一个更现代化的数据存储库

的项目,两种不同的持久性API 将与业务逻辑混合在一起,这使得代码更加令人

费解。

极易受数据模式改变的影响。数据库中较小的模式改变都将要求修改和重新编译

持久性逻辑和业务逻辑,因为它们这两种逻辑是紧耦合的。

逻辑的重复。JDBC编程需要在所有访问数据库的EJB中编写一些重复的编码(查

找数据源、获取连接、声明PreparedStatement 语句、解析结果集的结果、关

闭statement和连接等)。

上面描述的耦合问题和维护性问题使得编写真正的可重用的业务构件变得很困难。在许

多情况下,在不同的数据存储库类型之间的可重用性不并是很重要,只有那些应用需求指明

了要使用多种类型的数据存储库(RDBMS、ODBMS、LDAP等)的项目才需要关心持久性逻

辑和数据库实现细节之间的耦合性。然而,不管耦合不耦合,持久性逻辑与企业Bean的混

合总是会造成明显的可维护性方面的问题。

因此:

将持久性逻辑封装到能够将企业Bean与所有的持久性逻辑相分离的数据访问命令

bean(data access command bean)中。

一个数据访问命令bean(DACB)是一个平凡(plain)java bean 风格的对象,它暴

露一个简单的get/set/执行接口给企业bean的客户端。数据访问命令bean封装了持久性

逻辑和所有有关持久性数据存储库的细节,形成了一个存在于EJB层之下的独立的、分离的

持久性层。

数据访问命令bean模式与原始命令模式(Original Command pattern)(Gamma,et

al.,1994)相似,因为它只暴露了一个非常简单的接口给客户端(如图3.2)。一个客户端

所需做的所有工作是:创建一个数据访问命令bean,设置它执行任务所需的所有信息,然

后调用命令中的getter来获取数据,接下来如果这个命令返回了多个行,那么就调用next

方法。

Page 66: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

例如,考虑一个EmployeeServices 会话bean的情况,这个会话bean被用来处理所

有的有关公司中雇员的管理工作,EmployeeServices在一个组织中暴露其用来创建和搜

索雇员的方法,作为一个会话表面层的例子,这个bean不使用域模型,而是直接与数据库

进行交互。

为了将EmployeeServices与持久性逻辑分离开,将要创建两个数据访问命令bean,

一个负责处理创建一个雇员(employee)实体,另一个负责在所有的雇员中查找具有相同

名字的雇员。这些DACB的类图如图3.3所示:

图3.2 使用一个数据访问命令bean

图3.3 数据访问命令bean的例子

通过使用这些数据访问命令bean可以极大地简化EmployeeServices的代码,下面的

代码将说明了EmployeeServices是如何与InsertEmployeeCommand进行交互的:

Page 67: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

InsertEmployeeCommand insEmp = null;

try

{

insEmp = new InsertEmployeeCommand();

insEmp.setEmail(“[email protected]”);

insEmp.setId(id);

insEmp.setName(“Ed”);

} catch (DataCommandException e)

{

this.ctx.setRollbackOnly();

throw new EJBException(e.getMessage());

}

使用 QueryEmployeeByName 命令稍微有一点不同,因为这个命令潜在地可能会返

回具有相同名字的多个雇员实体: try

{

QueryEmployeeByNameCommand query =

new QueryEmployeeByNameCommand();

query.setName(name);

query.execute();

Vector employees;

EmployeeDTO anEmployee;

while (query.next())

{

anEmployee = new EmployeeDTO( query.getId(),

query.getName(),

query.getEmail());

employees.addElement(anEmployee);

}

return employees;

} catch (DataCommandException e)

{

this.ctx.setRollbackOnly();

throw new EJBException(e.getMessage());

}

注意数据访问命令bean抛出一个DataCommandException异常,这是一个普通的异

常,它服务于将会话bean客户端与数据库类型的细节完全分离的情况。

数据访问命令bean被实现为象图3.4那样的继承层次结构。每一个数据访问命令都是

从两个抽象类:BaseReadCommand 和 BaseUpdateCommand 继承而来的,这两个可重

用的类集中了在持久性逻辑中通用的设置(setup)、数据库执行(database execution)

和清理(cleanup)代码。

Page 68: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

图3.4 命令的继承实现

实现数据访问命令bean很简单,如果你要实现一个插入、更新或删除命令,那么这个

类必须从BaseUpdateCommand扩展而来;如果你要实现一个查询命令,那么这个类必须

从BaseReadCommand扩展而来。这两个抽象的父类从数据访问命令bean的开发者那里移

去了大多数的持久性细节,这使得他们只需要编写所使用的数据源的JNDI名字,和将要被

执行的与具体用例有关的SQL字符串,以及所有的与具体的用例有关的get/set: public class InsertEmployeeCommand extends BaseUpdateCommand

{

static String statement =

“insert into Employees (EMPLOYEEID,NAME,EMAIL) values (?,?,?)”;

static final String dataSourceJNDI = “bookPool”;

protected InsertEmployeeCommand() throws DataCommandException

{

super(dataSourceJNDI, statement);

}

public void setEmail(String anEmail) throws DataCommandException

{

try{

pstmt.setString(3, anEmail);

} catch (SQLException e) {

throw new DataCommandException(e.getMessage());

}

}

... //更多的设置

Page 69: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

}

数据访问命令Bean模式的优势是:

将持久性逻辑与业务逻辑相分离。所有乏味的、重复性的持久性逻辑都被封装在

一个平凡java bean/命令风格的接口中,业务逻辑不再需要担心结果集的解析、

驱动程序/语句声明的跟踪等。

创建了一个持久性层。将所有的持久性逻辑提取出来置于一个数据访问命令bean

的层中(在EJB层之下)将有助于使两个层能够彼此互相独立地被修改,从而有

助于将一个层所作的修改对另一个层的影响降到 低。

数据源的独立性。DACB可以访问关系型数据库管理系统(RDBMS)、面向对象的

数据库管理系统(OODBMS)、遗产系统适配器和其它任何持久性数据存储库,这

些对客户端来说都是透明的。事实上,在数据库之间的迁移将变得容易得多,因

为持久性逻辑被限制在一个层内。

在任何层上都可用。尽管这一章阐明的模式是在一个EJB应用上下文中应用的,

但是数据访问命令bean可以为任何应用环境(EJB、Servlet、JSP、TagLib

等)提供一种清晰的、健壮的持久性机制。

接口的一致性。命令风格的接口对所有的DACB都保持了一致性,这使得即使可以

支持多种数据存储库类型,对用户来说也是透明的。

这个模式的缺点是:

增加了一个额外的对象层。必须编写一个额外的命令bean层来将持久性逻辑和其

它层分离开。

不支持高级JDBC特性。诸如批处理更新这样的特性明显地不被数据访问命令

bean所支持。

数据访问命令bean模式提供了一种简单的、可扩展的、能够将持久性逻辑与企业bean

相分离的方法,这使得它成为了一种在会话表面层和BMP 实体 bean中处理持久性的有吸

引力的机制。DACB应该与通过JDBC读取数据(JDBC for reading)模式并列使用。可

以对命令Bean的接口进行细微的修改以支持在使用数据传送行集模式(Data Transfer

Rowset pattern)时直接返回行集(RowSet)。

相关模式

数据访问命令bean(Data Access Command Bean) (Matena and Stearns, 2001)

数据访问对象(Data Access Object) (J2EE Blueprints; Alur, et al., 2001)

Page 70: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

双重持久性实体Bean(Dual Persistent Entity Bean)

一个 EJB开发者需要编写同时支持 CMP和 BMP的实体 bean构件。

怎样才能将一个实体 bean设计为在发布时要么支持 CMP,要么支持 BMP呢?

一个实体 bean构件将要被发布的环境可能会因项目的不同而变化很大。 好的情况

是:一个开发组将访问一个具有良好的 CMP实现的应用服务器,他们可以使用这个 CMP实

现来获得明显的性能提高,而这些性能提高在使用 BMP时是不可能得到的。而通常的情况

是:一个开发组将使用一个只具有较差的 CMP支持或缺乏对他们的数据库的支持的应用服

务器,在这种情况下,就必须使用 BMP了。这把一个实体 bean的开发者推到了一个艰难

的境地,他们怎样才能提供一个能够适应两种情况的构件呢?

一种实现方法是对相同的实体 bean构件包装两个互相独立的版本。一个打包为 CMP

版本,另一个打包为 BMP版本。遗憾的是,这种方法要求构件开发者去维护两个代码互相

独立的构件,以及维护它们的测试和调试,这使得维护变得更困难了。

一个真正可移植的构件应该能够被发布到任何符合 J2EE规范的服务器上和大量不同

的环境配置中。可移植性意味着这个构件应该是不需要任何重编程或重编译就可以实现可定

制,而唯一需要修改的资源只是发布描述符。

因此:

为了构建可移植性更好的构件,将通过把业务逻辑分离出来置于一个符合 CMP规

范的父类中,并且把 BMP持久性逻辑置于一个子类中,来编写同时支持 CMP和

BMP的实体 bean。发布描述符的设置可以在发布时在二者之间进行选择。

通过将实体 bean 逻辑分开置于两个类中,可以使实体 bean同时支持 CMP和 BMP,

这两个类是:一个符合 CMP规范的父类和一个扩展了这个父类的 ejbstore、ejbLoad和

其它方法实现的子类。这个新的构件可以在发布时通过对标准的 ejb-jar.xml 做一些小

的修改来选择其持久性模式。

例如,考虑一个Account 实体 bean的例子,这个Account 实体 bean包含两个属

性:一个 account id 和一个 balance,同时它有三个业务方法:deposit、withdraw

和 balance,以及一个特殊的查找方法:findByBalance(int)。作为一个双重持久性

实体 bean,Account实体bean 将如图3.5所示:

Page 71: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

图3.5 一个双重持久性实体 bean

它的CMP 父类包含业务方法和抽象的get/set方法(抽象属性访问器需要EJB2.X

CMP的支持),以及所需的EJB方法的简单实现,例如set/unSetEntityContext 和

ejbCreate(),要注意的是,ejbLoad、ejbStore和ejbRemove的实现是空实现。查找

方法不需要在CMP类中实现,因为它们在发布描述符中被个别地声明。

它的BMP 子类提供了accountID和balance属性以及它们的get/set访问器的具体

实现,除了这些这个类所需要的额外逻辑外,还需要具体实现与持久性有关的方法,这些方

法包括:ejbCreate、ejbLoad、ejbStore和ejbRemove。查找方法也需要被实现,尽

管CMP父类依赖于在ejb-jar.xml文件中定义的查找方法。要注意的是BMP不需要重新实

现业务逻辑方法、set/unSetEntityContext和ejbActivate/Passivate方法,因为

这些方法是从父类那里继承来的。

在发布时,可以通过修改ejb-jar.xml文件来选择CMP和BMP类,即要求

<ejb-class>标签(tag)要么引用CMP父类,要么引用BMP子类。很明显,

<persistence-type>标签也需要在“container”和“bean managed”之间选择。如果

你选择CMP,ejb-jar.xml文件将需要用具体的CMP配置标签来添加一个模式(schema)、

属性、查找方法等。这个模式也需要被映射到一个底层数据存储库,这个底层数据存储库使

用了由Bean将要发布于其上的应用服务器所提供的专有机制。另一方面,如果你使用BMP

发布,ejb-jar.xml文件将可能需要使用<resource-ref>标签来添加一个SQL 数据源。

除了可以创建更加具有可移植性的实体bean之外,这个模式的另外一个用途是将BMP

实体 bean迁移到CMP上。许多EJB 2.0以前版本的应用都被编写成了BMP,因为由EJB1.X

规范提供的对CMP的支持对复杂系统的需求来说经常是不充分的,而且,当时在市场上能够

得到的许多CMP实现的性能都很差。所有的这些遗产EJB应用都将会因从EJB1.X BMP上迁

移到更新而且更复杂的CMP上而获得好处,遗憾的是,从BMP迁移到CMP的过程可能会非常

Page 72: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第三章 事务和持久性模式

棘手。一种解决方案是使用CMP完全重写构件,这个选择将需要许多更复杂的工作,而且从

本质上来说,这需要从一个实体bean 中将业务逻辑剪切和粘贴到其它的实体bean中,所

以它不是一个高效的将BMP bean转换成CMP bean 的方法。使用双重持久性实体 bean

模式,一个现有的BMP 实体 bean可以被转换为CMP bean,这可以通过创建一个父类并将

代码转移到其中,只在子类中保留属性、属性访问器和与持久性有关的方法来实现。这个新

的父类可以被测试和发布,如果需要的话这个子类也可以在稍后被移去。

Page 73: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

第四章 客户端 EJB 交互模式

决定 EJB 最佳的使用方法也许比写出这些 EJB 还要复杂。这一章的两个模式概述

了如何提高你的客户端 EJB 的性能,以及如何提高他们的易维护性。

EJBHOME 工厂(EJBHomeFactory) 提供了一个最好的和 EJB 层交互的方式:用一

个单独的工厂缓冲 Home 接口对象,这个代理还封装了所有有关查找 EJBHome 和错误处

理的复杂性。

业务代理(Business Delegate) 用来把客户层从会话或消息表面层层分隔出

来,把客户层处理 EJB 层的复杂操作抽象出来,把客户端和服务器端开发小组关注的事

务更好的分离开来。

·EJBHOME 工厂

一个EJB客户端需要查找一个EJBHOME对象,但是多次查找同一个Home接口是多余

的。

如何使得一个客户端应用程序在它的生命周期里只查找一次它的

EJBHOME?如何抽象出这次查找的细节?

* * *

对一个EJBHOME方法的JNDI查找,是接入一个远程EJB接口的第一步。为了接入这个

接口,客户端必须通过代码密集并且耗费巨大的过程以进入初始上下文,接下来就是真

正的查找EJBHOME,映射此页,处理例外,就像下面这些代码中描述的:

try // to get the initial context

{

Properties properties = new Properties();

// Get location of name service

properties.put(Javax.naming.Context.PROVIDER_URl,

“some providers url”);

// Get name of initial context factory

properties.put(Javax.naming.Context.INITIAL_CONTEXT_FACTORY,

“some name service”);

initContext = new InitialContext(properties);

}

catch (Exception e) { // Error getting the initial context ... }

try //to look up the home interface using the JNDI name

{

Object homeObject = initContext.lookup(“aHomeName”);

myHome = (MyHome) Javax.rmi.PortableRemoteObject.narrow(

homeObject, MyHome.class);

}

Page 74: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

catch (Exception e) { // Error getting the home interface ... }

//get EJBObject stub

MyEJB anEJB = myHome.create();

这个代码例子阐述了查找EJBHOME的复杂性及重复性。问题是一个典型的应用程序

用到很多EJBHOME引用――对应于客户端需要接入的每一个EJB。这样,为每一个EJBHOME

的写查找代码本质上就是复写代码。

此外,这些代码比较复杂,需要冗长单调的错误处理。( ClassCastExceptions,

NamingExceptions等等)。复制这些代码到所有的客户端是很糟糕的事情。

更糟糕的是,每次得到EJBHOME后,它只会被使用一次(用来得到EJB对象的开头)。

由于以下的原因,每次需要一个EJBHOME就执行一次JNDI查找的调用花费将是昂贵的:

·如果JNDI服务器在另一个不同的机器上那就需要一次网络调用。

如果这个客户端不是配置在JNDI服务器所在的机器上,那么调用JNDI就需要一个网

络调用。这是有可能发生的,比如,在集群的情形,它的Web server/servlet引擎

不在EJB服务器上而在一个不同的运行环境(box),而它的JNDI服务器往往就是EJB

服务器的一部分。

·如果 JNDI 服务器在同一个运行环境(Box)上就需要进程(IPC) 如果

客户端进程和 EJB 服务器进程运行在同一个运行环境(Box),但不是运行在同一

虚拟机(VM, virtual machine)上,那么查找 EJBHOME 时就需要 IPC 耗费。

即使客户端(比如一个servlet客户端)和JNDI服务器运行在同一台VM上,为每一个

Web请求查找EJBHOME也会损伤性能,因为在客户端应用程序的整个生命期内,其EJBHOME

都不会失效而且能够被重用。让我们设想一个流量很大的网站(比如TheServerSide。

com),它的某个页面可能每分钟被查看500次。而为这500个不同的客户端执行500次对

同一个对象的查找,其耗费是十分巨大的,并且是完全没必要的。

我们需要查找EJBHOME的更好的方法,允许抽象出查找代码,并且能够在客户端的

整个生命期内重用同一个EJBHOME实例。

因此:

把查找EJBHOME的查找代码抽象到一个可重用的EJBHOME工厂中去,它能在客户端应用程

序的整个生命期内缓存相关的EJBHOME。

EJBHOME工厂是一个单独实现的java类,如图4.1所示。 这个工厂封装了EJBHOME的

查找逻辑(使查找逻辑适用于每种类型的EJBHOME)并在内部缓存了Home接口,对于后来

的请求将缓存的Home接口传递给客户端。 EJBHOME工厂是通用的,同一类在任何应用程

序中都能重用。 拥有这种可重用性是因为它不包含任何特定领域的查找代码,比如

getAccountHome,或getXXXHome ,而是定义了一个单独的lookUpHome方法。

Page 75: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

图4.1 EJBHOME工厂的实现

Home接口工厂是为applets,serlets以及standalone应用程序这样的EJB客户端程

序所设计的,不过它同样能被EJB (EJB通常只是简单地把那些被请求的页面缓存在

setSession/Entity/MessageContext 方法中)用作封装和优化EJBHOME查找的方法。

EJBHOME工厂地使用是很简单的。客户端完全从Home接口查找和创建逻辑的工作中

抽象出来了,客户端的查找代码减少到了只有一行。例如,一个Account bean客户端只

要调用如下代码(不包括错误处理代码):

AccountHome accountHome = (AccountHome)EJBHomeFactory.getFactory()

.lookUpHome(AccountHome.class);

Home接口缓存和失效,集群问题 出现了这样一些问题,如这个模式是否使集群方法(clustering)失效或者缓存的Home接口EJBHOME能否在集群式或非集群式环境下失效。事实上集群式的服务器几乎总

是实现支持集群的Home接口部分(Weblogic和Websphere,至少,使用这种方法),这意味着在集群环境中某个Home接口不一定是和某个特定的服务器服务器可以崩溃

和重启,缓存的Home接口信息能够和集群群中的正常运行的和重启好了的服务器进

行通信。对于只有一个服务器的部署方,大多数服务器的Home接口信息也能保存下

来重新部署甚至服务启重启。但是,如果你的服务器允许失效的Home接口,那么你

得核实你的特定的服务器的语义,并且谨慎地编写Home接口工厂。

当客户端为了某个特定地Home接口对象,第一次调用EJBHOME工厂时,Home接口工

厂将通过JNDI查找这个Home接口,然后把Home接口对象缓存在内部的哈希表里面。对于

后来的调用,Home接口工厂将把在缓存的Home接口拷贝传送给调用者,完全优化了Home

接口调用。

注意客户端是以.class的形式传送AccountHome接口的,而不是用一个JNDI名字。。

Page 76: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

使用EJBHOME工厂后,客户端甚至被进一步从EJB的JNDI名字抽象出来了。 客户端所需

要知道的所有东西就是涉及的Home接口对象的接口(用来作为.class参数传递,然后返

回EJBHOME)。不管怎么样,由于客户端需要使用这个EJB的Home接口,并且以.class的

形式传递参数以查找Home接口,客户端需要知道的信息量就被减到最少了,这样就使客

户端代码变得简短。为了让Home接口工厂只是用一个类作为参数通过JNDI查找到一个

Home接口,下面三种情形之一须为真:

1. 被部署的EJB的JNDI名字必须同EJB的Home接口接口的完全合格的名字相一致.

如果你能控制你的应用程序中的EJB的部署属性,那就把EJB的Home接口的完全合法

的名字命名(就是com.xxx.xxx.xxxHome)用作这个EJB JNDI名字,采用这个命名协

定使你能独立地把xxxHome.class作为一个参数,以查找Home接口。对于很大的工

程,这样的要求可能有点过分。

2. 使用EJB-REF标记符分离JNDI名字.到目前为止,最精巧的办法就是在你的

web.xml文件中使用一个EJB-REF标记符,把com.xxx.xxx.xxxHome映射到你要用的

EJB的真正被部署的JNDI名字。注意这意味着EJBHOME工厂的这个class必须被部署

在你的应用程序的WEB-INF\lib目录下,以利用web.xml的ejb-ref映射。对于使用

EJBHOME工厂的EJB,在ejbjar.xml中使用EJB-REF也能达到这个目的(同样的,这个

Home接口工厂的class必须被打包到ejb-jar以利用定义在这层的ejbref映射)。

3. 从一个资源文件读入.class与JNDI名字的绑定关系.如果你使用的是旧版本的

应用程序服务器而不能使用EJBREFs,那么你可以在EJBHOME工厂中编写代码,以从

一个Home接口工厂文件中读入.class与JNDI名字的绑定关系。

到目前为止,最雅致和易移植的解决办法就是上面的第2个选择,使用EJB-REF标记

符来把EJB的Home接口的.class名字映射到真正的JNDI名字。这使得在EJB的JNDI名字是

它的Home接口接口的完全合法的名字这个假设之下,可以编写某个Home接口工厂代码,

因为开发者在部署时执行这个从Home接口class名字到JNDI名字的映射。下面举一个银

行帐户的例子说明ejb-ref映射时的工作机制。以下的这些ejb-ref标记符将被加到

web.xml文件中去,它们定义了把com.bankapp.AccountHome作为这个帐户的逻辑JNDI名

字:

<ejb-ref>

<ejb-ref-name>

com.bankapp.AcccountHome

</ejb-ref-name>

<ejb-ref-type>

Session

</ejb-ref-type>

<home>

com.bankapp.AcccountHome

</home>

<remote>

com.bankapp.Acccount

</remote>

</ejb-ref>

Page 77: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

这个把com.bangApp.AccountHome作为帐户的逻辑JNDI名字的声明将在部署阶段被

映射为真正的JNDI名字。 在Weblogic中,通过把下面这些代码放入weblogic.xml描述

符(用于WARS)实现这个目标:

<reference-descriptor>

<ejb-ref-name>

com.bankapp.AcccountHome

</ejb-ref-name>

<jndi-name>

AccountHomeActualJNDINAme

</jndi-name>

</reference-descriptor>

使用这个模式使EJBHOME工厂只要简单地把客户端传过来的Home接口.class的完全

合法的名字传递给lookUpHome以查找Home接口对象,而在其背后,servlet或EJB的包容

器将把这个字符串映射成ejb-ref标记符声明的真正的JNDI名字。

注意,把Home接口class用作请求一个Home接口的机制,这种选择是被设计用做简

化客户端和Home接口工厂的一种实现方式,但并非一定要这么做。你可以简单的加入一

个类和一个JNDI名字来改变查找方法,如下所示:

AccountHome accountHome = (AccountHome)EJBHomeFactory.getFactory()

.lookUpHome(“AccountHome”, AccountHome.class);

这个方法的缺点就是要查找的Home接口的编码复杂的JNDI名字对客户端是个负担,这使

EJBHOME工厂模式的易维护性降低了。

一个以SERVLET为中心的选择 Servlet开发者中间有一个惯例: 把Home接口EJBHOME初始化逻辑放在

Servlet.init(),把Home接口EJBHOME缓存在ServletContext 对象中,因

为它被整个应用程序共用。这个方法和EJB这也工厂有一样的好处(性能,简

易性),但是编码更复杂一点。通用表达层构件(比如java Bean帮助器)没有

访问ServletContext的权限,为了访问EJBHOme,必须传入一个Home接口。因为Home工厂是一个singleton,它可以在应用的任何一个地方存在并

简化代码。

EJBHOME工厂模式是一个简单高效的,以通用、可重用的方式把查找EJBHOME的复杂

性从客户端分离出来的方法。通过缓存EJBHOME,能消除耗费巨大的多余的Home接口查

找,显著提高性能。EJBHOME工厂提供了一个查找Home接口对象的统一接口,而且可以

在任何环境(applet,servlet,standalong,甚至在EJB中)下保持可重用性。

相关模式

Page 78: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

服务定位器(Alur,et al.,2001)

工厂(Gamma,et al.,1995)

Page 79: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

业务代理(Business Delegate)

当使用会话表面通信和(或)消息表面通信时,客户端和EJB层紧密地联结在一起,这样

就形成了客户端和服务器端的依赖关系,这种依赖会影响到开发、运行期间和工程管理涉及

的事务。

怎样创建一个客户端和会话表面之间的中间组件来帮助把客户端从EJB层分离出来?

* * *

在一个良好的EJB设计中,用例应该在会话层和(或)消息驱动bean上划分开来,就像在会

话表面通信模式和消息表面通信模式中分别描述的那样。一般的和这一层交互的方法是通过

在客户端代码中直接调用实现的。也就是说,你的表示层将同EJBHOME以及用于会话Bean的

EJB对象直接交互,并在和消息驱动bean通信时发送JMS消息。

具有讽刺意味的是,直接使用EJB APIs编程不一定是编写EJB应用程序的最好方法。其间

会出现各种问题,它们都和由客户层和EJB层紧密联系而产生的问题有关:

减少了客户端程序员和服务器端程序员之间的角色分离. 对于大型工程,快速而

高效的完成工程依靠客户层(就是servlet/JSP)开发者和服务器端EJB开发者独立

工作的能力。小组之间通常会产生的一个依赖就是完整的编译好的会话Bean层的可

用性。客户端程序员要依靠会话表面通信的实现来编译和测试他们的代码,这样就

在两个小组之间产生了一个可怕的瓶颈。

给客户端添加乐观并发性的恢复的责任.通常一个事务处理的失败是因为应用程序

服务器或数据库级上的一个乐观的并发性冲突,客户端能够通过

TransactionRolledBackException或者TransactionRolledBackLocalException这

两个例外(Exception)捕获它。对于某些类型的用例(比如等幂操作),并不一定需

要把错误信息传送到终端应用程序用户以让他们重新尝试操作(通常是在一个Web

表单上再次点击发送按钮)。事实上,客户端的代码应该自动地重新执行事务处理。

当直接使用EJB APIs编程时,客户端代码必须明确地捕获这些错误并重新尝试事务

处理,这给客户端开发者(他们可能并不完全理解用例实现的本质,因为他们并不

编写EJB层)加上了一个大责任,并且扰乱了他们的代码。

复杂的错误处理使客户端逻辑复杂化.客户端需要担负对在查找和使用EJB时出现

的无数错误的捕获和作出反应的责任。这些错误包括查找组件时抛出的错误,

RemoteExceptions,EJBException (使用逻辑接口时), 等等。 特别的

RemoteExceptions,EJBException的出现是由各种不同的原因产生的(比如上面描

述的乐观的并发性冲突),给客户端加上了实现用来分析错误并决定如何反应的混

乱代码的责任。

把客户端直接同EJB以及JMS APIs联结起来. 即使执行简单用例时,客户端也要装

载EJB-或JMS的特定代码,用来发现,创建,执行和恢复会话或消息表面层所实现

的事务逻辑。这样就产生了客户端代码的矛盾(不同的业务服务明确地由完全不同

的APIs执行),而且即使最简单地用例也会被复杂化,结果就导致了总体上的较低

的可维护性。

尽管会话/消息表面模式拥有性能和维护方面的很多好处,但是明确地从客户端使用这些

层会产生一个紧密地联结关系,它会影响工程开发和客户端代码的整体易维护性。

因此:

Page 80: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

创建一个业务代理层:一些普通的java类,他们通过封装用于发现,代理和对会话及

消息表面通信EJB层调用的恢复的代码,隐藏了EJB API的复杂性。

业务代理是一个普通的java类,作为客户端和服务器端之间的中间件。客户端在本地调用

业务代理的方法,然后代理给一个和会话表面层有相同签名的方法,或者产生一个JMS消息

并把它传送给消息表面层。

业务代理在会话表面层一一映射到会话Bean,并且可以编写代码在其中装入多个消息驱

动的bean。例如,考虑一个论坛消息板的应用程序。这里我们将展示ForumServices 会话Bean

上的用例(postMessage,addReply等等) ,或者使用分离消息驱动的bean异步地执行每个用

例。图4.2显示了业务代理如何映射到这两种架构。

图4.2 使用业务代理面向会话/消息表面层。

在这两种情况下,客户端代码都只和业务代理进行交互,显然APIs和各个过程都由业务

代理本身执行。当业务代理地一个方法被执行时,它能执行以下的功能:   代理对EJB的调用方法. 业务代理接受客户端传过来地所有参数然后简单地把这

个调用代理给会话表面层的一个方法,或者把这些参数包装到一个JMS消息中去再把

他们传送到一个消息驱动的bean。   隐藏EJB特定的系统例外. EJB特定的系统例外,比如RemoteException, EJBException, 或者JMS例外,在业务代理中被捕获然后被作为一个非EJB特定例外(比如一个业务代理例外)重新抛出到客户端。应用程序级的例外仍然被传递给客户端。   在本地缓存数据. 业务代理可以在本地缓存一个会话Bean方法调用返回的结果,

然后在以后的请求时把它们传送给客户端。   透明地重新尝试失败地事务处理. 业务代理可以实现复杂的错误处理代码,这些

代码用来判断事务处理失败的起因(比如上面描述的乐观的并发性冲突),然后通过重

新执行会话表面层的方法来重新尝试事务处理。业务代理对客户端隐藏了精妙而复杂

的处理过程。  在本地执行事务逻辑或者为客户端创建虚拟数据. 就像在把客户端和EJB APIs联

Page 81: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

结带来的第一个问题中提到的那样,客户端工程小组是依靠已经存在的会话表面层来

编译和测试他们的代码的。业务代理提供了一个让客户端程序员在没有已经存在的会

话表面层的情况下也能编写,编译和测试工作代码的方法。可以编写一个原型业务代

理,它只是简单地返回虚拟数据(在unit测试中非常有用),或者甚至在本地执行事务

逻辑(对于快速创建一个工作原型有用)。当服务器端地EJB被构建时,业务代理地类能

够被重构以处理EJB层,这些过程对于客户端地开发者都是透明的,这样客户端的开发

者就再也不需要依赖EJB工程小组了。

实现业务代理是很简单的。只要对应用程序里的每一个会话Bean,使用相同地方法签名

简单地创建一个本地java类。在其内部,业务代理能够在它地事务方法中执行上面概括地任

何工作。唯一需要编写的其他地代码段就是一个构造函数和业务代理所面向的会话Bean的一

个引用。在业务代理的构造函数中,需要调用一个EJBHOME工厂(见EJBHOME工厂模式)以获得

它所代表的会话Bean的Home接口并创建这个会话Bean的一个实例,并保存为一个本地成员变

量。对于后来的对业务代理的事务方法的调用,将被代理给存在内部的这个会话Bean的引用,

如下面的代码所示:

public class ForumServicesDelegate

{

ForumServices sb;

public ForumServicesDelegate() throws DelegateException

{

try

{

ForumServicesHome home = (ForumServicesHome)

EJBHomeFactory.getFactory().lookUpHome

(ForumServicesHome.class);

this.sb = home.create();

}catch(Exception e)

{

throw new DelegateException();

}

}

public long addForum(long categoryPK, String forumTitle,

String summary)

throws NoSuchCategoryException,DelegateException

{

try

{

return sb.addForum( categoryPK, forumTitle, summary);

}

catch(CreateException e)

{

throw new DelegateException();

//log errors, etc

Page 82: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

} catch(RemoteException e)

{

throw new DelegateException();

//log errors, etc

}

}

... //more similarly implemented business methods

}//ForumServicesDelegate

对于消息驱动的Bean,业务代理被创建,用来把相似的用例一起放到一个类里面。 (映射到不同的消息驱动的bean,如图4.2所示)。实现方法和上面的会话Bean的例子相似,不同

的是这里所有的方法都返回空(void)。 业务代理在客户端的视图是很简单的。当某个方法要被执行时,只是简单地创建一个新

的业务代理然后调用它的一个方法。在这背后,业务代理在它的构造函数中初始化自己(使用一个EJBHOME工厂),然后代理给特定的方法。由于EJBHOME被缓存在EJBHOME工厂

中,创建和使用业务代理相对来说耗费比较小。 使用一个业务代理的方法唯一的需要改变的时候就是当使用他们面向状态会话Bean。时。

在这种情况下,客户端不是对于每个请求创建一个业务代理,而是创建一次然后把它缓存在

本地,重用同一个业务代理(它在内部维护了对同一个状态会话Bean的引用)。在一个servlet应用程序中,业务代理是被缓存在ServletSession中的。为了在HTTPSession中支持状态业务

代理的存储,业务代理编写的方法要做一些改变:   业务代理必须可以序列化 由于业务代理是被存储在ServletSession中,它应被声明为

可以序列化,以支持钝化HTTPSessions的servlet引擎,或者支持分布式系统中的会话复

制。    必须使用一个EJB句柄来支持序列化 由于业务代理可以被序列化,它不能象前面

地代码例子中那样只简单地包含对一个EJB对象地引用,EJB对象并未实现序列化,因

此必须编写实物对象的代码以使用一个句柄对象,那样对于状态会话Bean的引用才能保

持在序列化后仍然完整无缺。 一个使用业务代理面向状态会话Bean的重要的副作用是这个类和它的方法可以保持同

步,这样就使得客户端无法并发地调用同一个状态会话Bean(这是EJB规范所不允许的,因为

EJB对象不是线程安全的)。这个问题在使用框架的网站(每个框架需要发送一个请求,并通

过同一个状态会话Bean结束)上会发生,但是可以通过使用一个业务代理透明地更正。 另一个非典型的业务代理模式的使用是把它作为一个整合EJB和非java应用程序的方法。

由于业务代理只是简单的java对象 ,使用JNI或者一些java-com桥梁可以很方便地把他们包

装起来。因为所有的J2EE javax接口都对客户端隐藏,你就不一定要提供他们的非java版本。

这个整合的方法消除了非java应用程序和应用程序服务器买主的ORB之间的依赖关系。由于

旧版本的ORB的互操作性问题,使非java应用程序通过业务代理同EJB通信保证了集群和安

全性正确地运作。 什么时候应该使用业务代理模式?对于那些客户端和服务器段代码由相同地开发者编写

地工程来说,把客户端代码同服务器APIs分离开的好处可能不是很大。但是,对于大型的工

程,Web小组同EJB小组是分开的,业务代理能够更好地分离客户端和服务器端开发者的工

Page 83: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第四章 客户端 EJB 交互模式

作。 相关模式:

业务代理(Alur, et al., 2001)

Page 84: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

第五章 主键生成策略

以可移植的、可扩展的和可靠的方式来生成主键(PK)是EJB应用中的一大挑战。许多应用

服务器都提供了它们专有的为实体 bean生成主键的方法,例如,Weblogic允许你通过透明

地使用你的数据库内置的序列/计数器来自动地产生一个主键。虽然在许多情况下这是个简单而

可行的解决方案,但是这个方法也带来了问题,那就是当把代码从一个应用服务器上移植到另

一个应用服务器上时,不同的CMP实现方案之间的PK产生机制可能并不兼容。唯一能够实现真

正具有可移植性的实体 bean主键生成方法的机制是调用一些外部的、用户创建的结构。 这一章将讨论三种用来创建实体bean的主键的主键生成 优方法:

序列块(Sequence Block):提供了一种只需要很少的数据库访问就可以生成

递增的整型主键的模式。这个模式在其解决方案中使用了一个无状态会话 bean和一个CMP 实体bean。

EJB的全局唯一标识符(UUID):提供了一个在内存中创建基于字符串的主键的PK

生成服务的算法和实现,在这个模式中并不需要数据库或应用程序方面的

singleton对象(一个包含了它自己的一个静态实例的java类,在一个应用中,

这个类将只有一个实例在运行)。

自动生成键的存储过程:描述了何时使用数据库内置的键生成服务,以及怎样在

一个BMP 实体 bean中以可移植的方式来使用它。

Page 85: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

序列块(Sequence Block)

实体 bean开发者需要一种能够以递增形式生成基于整型的主键的方法,这种方法并不在

乎键值之间可能出现的间距。 怎样才能以可移植的、高效的方式生成基于整型的递增式主键呢?

使用一个简单的递增数字作为主键是一种非常高效,并且具有很高的可维护性的生成主键

的解决方案。从数据库的观点来看,整数是高效的,因为它们比巨大的基于字符串的整数(比如

那些由UUID为EJB模式或High/Low模式产生的数) 可以被更容易而且更高效地索引。从开发

者的观点来看,整数是更容易维护的,因为主键是从0开始向上递增的,这样就可以产生简短的

键,数据库管理员(DBA)可以很容易地操作它们,从而快速方便地产生报表。 自动生成键的存储过程模式提供了一种很方便的、与RDBMS数据库中内置的自动递增键协

同工作的解决方案,但是这个模式只适用于BMP 实体 bean,并且需要一个支持自动递增键的

数据库,而且还需要在实体 bean和数据库之间编写一个额外的存储过程层。 我们真正需要的是一种能够生成整型主键序列的方法,而这个主键序列可以被CMP或BMP

实体 bean调用。其中一种解决方案是创建一个表示一个序列(Sequence)的实体 bean,也

就是说,创建一个只在数据库中存储一个整数,并在每一次客户端请求一个主键时对它进行递

增的实体 bean。图5.1显示了一个序列实体bean(Sequence entity bean)的例子,这

个实体bean的主键是一个代表它的名字的字符串,系统中允许存在多个序列,其中每一个序列

维护一个不同的currentKeyValue。

图 5.1 简单的、递增的 序列实体bean(Sequence entity bean)

其它的实体 bean能够通过它们的ejbCreate( )方法调用这个序列实体bean。例如,一

个Bank Account(银行帐户)实体 bean可以执行类似下面的伪代码的代码: int ejbCreate(attrib1, attrib2, ...) {

Sequence aSequence = SequenceHome.findByPrimaryKey(“Account”); this.id = aSequence.getNextKey() ...

}

这个方法存在许多问题: • 性能(Performance):Account的ejbCreate方法将产生四个数据库调用,

分解如下:前三个调用分别是SequenceHome.findBy、Sequence.ejbLoad 和 Sequence.ejbStore, 后一个调用Account 实体 bean的插入操作。为

了优化这个过程,可以把对“Account”序列的引用缓存在Account 实体bean里面,但是仍然会有三个数据库调用。

• 可扩展性(Scalability):如果getNextKey方法运行在一个隔离级别为

serializable的事务隔离级别上,将会导致可扩展性方面的令人难以接受的损

Page 86: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

失,因为可能会有成百上千的实体 bean在排队等待获得它们的主键。 • 需要在ejbCreate中编写乐观的并发性处理逻辑:如果应用服务器或者底层的数

据库使用了优化的并发策略,那么使用序列实体 bean的那些实体 bean将不得

不捕获TransactionRolledBack异常并重新尝试调用getNextKey(),这将

导致混乱的ejbCreate代码和许多重试操作,因为有许多实体 bean在竞争使用

同一个序列。 一个序列实体 bean可以提供一种简单的生成基于整形的主键的方法,但是让客户端直接

和这个实体 bean交互以使键递增将会导致较差的性能和较差的代码封装。所以需要一个更好

的机制,它允许实体 bean使用递增的整数作为主键,但是对生成那些数字的机制进行了优化。 因此: 在序列实体bean之前设置一个会话 bean,这个会话bean在某个时刻获得整数块并把它

们缓存在本地。客户端与这个会话bean进行交互,会话 bean把缓存的键传递出去并对

其它的实体bean客户端隐藏了所有的复杂性。

前面的使用一个序列实体 bean来递增一个计数器的方法是可以被改进的,这种改进是通

过修改序列实体 bean使它按整数块递增(而不是每次递增同一个数),并使用一个会话bean来处理这个实体 bean,以及将键块在本地缓存来实现的,如图5.2所示。这个会话 bean成为

了一个主键生成服务,它维护任意数量的不同序列的键块,并提供对内存中的新键的快速存取,

从而不必在每当实体 bean需要一个主键时都对序列实体 bean(也就是对数据库)进行访问。 当一个实体 bean的ejbCreate方法需要一个主键时,它将调用序列会话 bean的local

接口并要求获取下一个可用的键。在Bank Account的例子中,Account实体 bean的ejbCreate方法将包括下面的代码:

//home 和 sequence session 查询 //这只会被执行一次,然后缓存在 setEntityContext 中 SequenceSessionLocalHome ahome = (SequenceSessionLocalHome) (new InitialContext()).lookup(“SequenceSessionLocal”); SequenceSessionLocal aSequence = aHome.create(); //获取下一个键 this.id = aSequence.getNextNumberInSequence(“Account”));

下面是对getNextNumberInSequence方法的伪代码的描述: 1. 在本地的缓存块中检查与“Account”序列相对应的块。 2. 如果不存在任何键或者键块中的键已经用完,序列会话将调用序列实体,并获得下

一个“Account”序列可用的整数块。 3. 在获取了下一个块时,捕获所有的事务回滚操作(下面将解释),并且进行指定次

数的重试操作。 4. 从本地的键块中直接传递一个键到客户端的实体bean。这个会话 bean对于后面

的对键的请求,都会从本地键块中把键传递出去,直到键块中的键用光为止,这时

再重复步骤1。

序列实体 bean能够作为一个简单的CMP bean被实现,这个CMP bean的主键是一个与序

列名字相对应的字符串,它需要维护的唯一的其它值就是当前 大的键值。如图5.2所示,序

列块实体只有一个方法-getNextKeyAfterIncrementingBy(block- size)。这个方法

只是接受一个表示块的大小的值,然后把自己的长度增加这么一个尺寸,再把新的 大键值返

回给调用它的序列会话bean。所有的序列实体 bean都映射到数据库中的一列上,而数据库的

行对应于不同序列的当前值,如图5.3所示。

Page 87: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

图5.2 序列块体系结构布局

图5.3 把一个序列实体bean映射到一个数据库的表中

尽管这个CMP bean 终能够被简化,我们还是必须特别注意在发布描述符中将

getNextKeyAfterIncrementingBy方法标记为TRANSACTION_REQUIRES_NEW。如果没

有这个特殊的设置,对块进行递增的操作将会成为被客户端的实体 bean所初始化的事务的一

部分,而这个客户端实体 bean依据不同用例可能会是一个执行时间很长的实体 bean。为了

限制上锁和提高性能,获得一个新块的操作应该是一个尽可能简短的原子操作。 这个模式的事务和并发处理的语义取决于所使用的应用服务器和数据库。为了使序列会话

可以移植到不同系统中,它必须被编码为具有一个捕获

TransactionRolledBackLocalExceptions的try/catch 代码块,以捕获可能的乐观的

并发处理冲突。这种冲突的一个例子是:在集群中的两个Account 实体 bean 在它们各自的

服务器上同时请求一个主键,序列会话 bean需要在同一时刻获取下一个键块,如果没有正确

的设置,这两个序列实体 bean的实例 终可能会得到相同的键块。用来纠正这个问题的设置

取决于你所使用的数据库或应用服务器是如何处理并发操作的: 1. READ_COMITTED的事务隔离级别和应用服务器CMP实现的更新认证。在这种情况下,

应用服务器会在事务提交时刻之前对序列实体 bean的内容和数据库中的相应内容进行比

较。如果发现先前的某个事务已经得到了这个键块,就抛出一个例外。这是在应用服务器

级别上的乐观的并发性检查的实现,它允许你只使用READ_COMMITTED的事务隔离级别,

因为应用服务器会保证数据一致性。 2. SERIALIZABL的事务隔离级别和一个使用乐观的并发处理的DB。如果没有在应用服务

器级别上实现乐观的并发处理,那么就必须使用一个SERIALIZABLE的事务隔离级别来确

保数据一致性。如果数据库本身实现了乐观的并发处理检查,那么当它检测到第二个序列

实体bean的事务的ejbStore方法正在试图覆盖第一个事务插入的数据时,它将会自动地

回滚第二个事务。 3. SERIALIZABL的事务隔离级别和一个使用悲观的并发处理的DB。同样的,由于应用服

务器不会强制保持数据一致性,所以还是要使用事务的SERIALIZABLE隔离级别。但是,

由于数据库使用的是悲观的并发处理策略,所以它将对序列表中的“Account”行加锁,强

Page 88: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

制第二个事务的SequenceEntity.ejbLoad()方法一直等待,直到第一个事务的

SequenceEntity.ejbStore()方法完成并提交。 4. SERIALIZABL的事务隔离级别和一个SELECT FOR UPDATE操作。某些应用服务器允

许通过编辑发布描述符的设置,来把CMP引擎设置成在ejbLoad过程中能够产生一个

SELECT FOR UPDATE操作。这是为了强制那些使用乐观的并发处理的数据库真正地锁定

底层的行,就像选项3那样。 对于选项3和选项4,将保证每个序列会话都可以得到一个唯一的键块,因为在第一个事务

完成它的ejbLoad-递增块-ejbStore循环之前不允许第二个事务读相同的行。但是,对于选

项1和选项2,在序列会话中需要一个用来重试这个调用的try/catch程序段。这里讨论的要点

就是如果你在会话bean中保留了try/catch代码段,那么这些代码本身就可以移植到所有可

能的配置上,现在所需的只是在发布描述符中改变前面描述的事务隔离级别和可能有的与具体

的软件提供商有关的CMP选项。 序列块模式(Sequence Block pattern)的优势在于:

性能 尽管这个模式需要一个设置有很大的块的数据库,但是这个模式的性能接近于

EJB的UUID模式,因为大多数主键的生成都发生在内存中。 可扩展性 即使使用serializable 的事务隔离级别(在序列实体上),这个模式也

很容易扩展,因为getNextKeyAfter-IncrementingBy并不经常发生。 很容易被重用 序列块模式使用完全通用的代码,一旦被实现,这个模式就能够毫无问

题地在各个项目中重用。 简单性 需要用来实现这个模式的代码量很小,而且,CMP可以被可靠地用于序列实体

bean。 生成简单的键 这个模式生成简单的基于整数的键,它们允许数据库有效地索引主键

列,使DBA能够轻松地使用主键。 它的缺点是:

不保证键有序 如果使用大小为10的块,那么先后生成的四个不同的实体bean的主键

(通过两个序列会话bean的实例生成)可以分别是10,20,11,12。这是因为实例

池中不同的序列会话bean具有分配给它们的不同的块。 在实例池调整大小时键可能会丢失 如果使用无状态会话 bean来维护一个键的缓存,

那么当应用服务器决定移去一个会话 bean实例时,键将会丢失。大多数应用服务器

使用基于需求的会话 bean实例池——新的bean基于当前通信量被创建,然后当通信

量下降时被移去。实际上,丢失的键并不是我们所关注的(世界上有许多数字);它

不会影响传给实体 bean客户端的键的唯一性。

总的来说,序列块模式提供了一个简单的、集群安全的(cluster-safe)、以高效而可

移植的方式生成基于整数的主键的机制。

Page 89: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

EJB的 UUID(全局唯一标识符)

实体bean开发者需要一种能够生成基于字符串的在内存中全局唯一的主键的方法,而这种

方法不需要一个数据库或一个全局唯一的sigleton。 怎样才能不使用数据库或某个sigleton而又能够在内存中生成全局唯一的主键呢?

对于很多主键生成方案来说,数据库都被用来维护主键的状态和同步对键的访问,就如同

在EJB序列模式中描述的那样。当使用这些方案时,它们都需要数据库基础架构的事实使得它

们难以被实现,因为它们需要被编码为可以移植到各种不同的数据库上,这是非常困难的,因

为不同的数据库处理诸如对行加锁等问题的方法并不相同。 许多非数据库主键生成模式都需要使用一个singleton,也就是在整个应用中只存在一个

唯一的实例对象。它可以代替数据库来管理主键并可以作为所有请求主键的客户端(比如

entity bean)的同步节点。 这种方法的问题是很难创建一个贯穿整个J2EE应用的真正的单一实例。一个传统的java

的singleton(一个包含自身的同步静态实例的类)只保证每个classloader有唯一的实例,

而典型的J2EE服务器将在每个VM中包含有多个处于运行状态的classloader。另一种方法是

使用一个网络化的RMI对象的singleton,就是一个在应用程序中只存活于一个服务器上的可

以通过RMI来调用的对象,这样就可以使整个应用程序中只有一个实例。现在这个问题涉及到

了可扩展性:当你的潜在的服务器集群中的每一实体 bean都必须同步访问这个RMI对象时,

这可能会变成一个瓶颈,而且也是一个单一的失败节点。 另一个解决方案是使用JDK提供的java.rmi.server.UID类。通过这个类所生成的ID的

问题是它们在整个系统的运行环境不唯一,它们需要附加一个网络地址InetAddress来获得唯

一性。更重要的是,UID类的实现使用了Thread.sleep()方法,而它在EJB环境中是不允许

的。 更好的可以作为主键生成机制的方法将不需要关于数据库或全局singleton的同步。这样

一个机制需要被分散开来(因为没有同步节点),它允许其多个实例并行地生成唯一的主键。 因此: 通过创建一个全局唯一的标识符(UUID)来在内存中创建主键,这个UUID包含了足够的

系统信息使它在空间和时间上都是唯一的。 UUID是一个字符串主键,它被编码为包含了使生成的UUID在整个空间和时间上都完全唯

一所必需的系统信息集,不管这个UUID是何时何地被生成的。作为一个完全分散的算法,在集

群中甚至是在同一个服务器上可以有多个UUID的实例,从而可以快速而高效地生成主键。 原始的UUID规范可以在一个由Paul Leach 和 Rich Salz1撰写的(Network Working

Group Internet Draft)网络工作组Internet draft中得到,但是定义在那个原始版本

中的算法将不能在EJB应用环境中使用,因为他们描述的各种不同的实现都需要一个恰当的

singleton来访问一个同步共享资源(数据库),而且经常将IEEE 802网络地址硬编码到你

的服务器网卡中。这些特性在EJB 应用环境中都不可能存在,但是仍然可能在EJB中创建一个

等价的GUID,这也就是这个模式所关注的焦点。 UUID是一个基于字符串的主键,它由32位数字组成,编码为十六进制,如图5.4所示。这

个字符串组成如下: 1.精确到毫秒级的唯一 1-8位是System.currentTimeMillis()方法调用所产生的

十六进制编码的低32位。 2.在集群中的唯一 9-16位是表示底层IP地址的32位整数的十六进制编码(一个IP地址

被分为四个分离的字节,它们连在一起构成32位)。 3.精确到在JVM内部的对象中的唯一 17-24位是System.identityHashCode(this)

Page 90: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

方法调用所产生的十六进制编码,这个方法调用保证对JVM中的不同对象返回不同的整数。

即使在同一机器上存在多个VM,两个UUID生成器返回相同的UUID的情况也极不可能发生

(后面将解释)。 4.在一个对象的一个毫秒级别上的唯一 后的25-32位表示一个32位的、与每一个方法

调用相对应的随机整数,这个随机整数是通过使用java.security.SecureRandom类来

生成的。这样就可以保证在同一毫秒内对于同一个方法的多个调用都是彼此唯一的。

上述四段字符串组合在一起,可以确保用这个算法创建的UUID在一个集群的所有机器中,

以及在一台机器上的JVM内部的所有UUID生成器的实例中都保持唯一,它能够精确到毫秒级甚

至是一个毫秒内的单个方法调用的级别。

图5.4 EJB的UUID 的布局

在EJB应用环境中有两种实现UUID模式的方法:作为一个平凡(plain)java sinlgeton

类或者作为一个无状态的会话 bean来实现。两种实现方法的选择实际上取决于开发者,根据

他们的喜好而决定。不管有多少个实例在一个VM 内运行,UUID算法都是安全的。如果作为一

个无状态会话 bean来实现,EJB服务器将构建UUID生成器的实例池,并且不得不拦截调用请

求并执行诸如安全检查、会话 bean创建等通用的服务器处理。如果作为一个平凡java singleton来实现,则没有上述开销,实体 bean只是调用存活于它们的类加载器

(cloassloader)中的singleton类的实例。 下面提供一个作为无状态会话 bean来实现UUID的例子,它是基于Steve Woodcock的

实现的(www.activescript.co.uk):

public class UUIDBean implements javax.ejb.SessionBean { // 安全的随机数能够体提供不重复的seed private SecureRandom seeder; // 缓存的字符串中间部分的值 private String midValue; public void ejbCreate() throws CreateException {

try { // 获得 internet 地址 InetAddress inet = InetAddress.getLocalHost(); byte [] bytes = inet.getAddress(); String hexInetAddress = hexFormat(getInt(bytes),8); // 获得这个对象的 hashcode String thisHashCode = hexFormat(System.identityHashCode(this),8); // 设置中间值字符串 this.midValue = hexInetAddress + thisHashCode; // 加载随机数发生器 seeder = new SecureRandom(); int node = seeder.nextInt();

} catch (Exception e)

Page 91: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

{ throw new CreateException (“failure to create bean “ + e);

} } public String getUUID() throws RemoteException{

long timeNow = System.currentTimeMillis(); // 获得无符号的整形值 int timeLow = (int) timeNow & 0xFFFFFFFF; // 获得下一个随机值 int node = seeder.nextInt(); return (hexFormat(timeLow, 8) + mid + hexFormat(node, 8));

} } 当会话 bean第一次被创建时,将创建十六进制格式的系统的IP地址和hashcode以及安

全随机数(SecureRandom seeder),并把它们缓存起来。对于随后的对getUUID()的调

用,只需要把当前以毫秒为单位的时间和当前的随机数编码成十六进制并和缓存的IP和hashcode结合在一起,就可以在内存中高效地创建一个主键。

理论上,可能导致这个模式失败的一个问题是时钟倒退。如果出于某种原因,服务器的时

钟倒退了,而在服务器JVM 内的UUID生成器正好与某个在新的回退了的时间存在的生成器具

有相同的hashcode,并且在同一毫秒内这个生成器创建了和过去的副本相同的随机数,那么

就有可能产生重复的键。 这个模式的其它理论上的问题可能会在一个应用服务器集群处于同一台机器(每台机器都

有多个VM)上时发生。在多个JVM运行于Sun的JDK 1.3.x 和1.4的单机上时,如果应用服务

器在两个JVM中以完全相同的顺序创建两个对象,那么这两个对象的用作UUID字符串中间8位字符的对象标识符(通过System.identityHashCode(this)得到)就是重复的。但是,要

使UUID发生冲突,这两个对象需要在同一毫秒内被调用并产生相同的安全随机数,这使得UUID冲突只有在极端条件下才有可能发生。

EJB的UUID模式的优势在于: • 性能 主键是在内存中被生成的,不需要有关全局singleton 或数据库的任何同

步。 • 简易性 UUID模式不需要复杂的数据库访问和同步代码,而且可以作为一个老式的

平凡java singleton类被部署。 它的缺点是:

• 依赖于IP地址 在本地LAN上产生的UUID将与本地的192.168…地址一起被编码。

但是即使在本地LAN,所有的IP地址仍然是唯一的。 • 使用36位的字符串作为主键 UUID模式生成的这个长字符串在某些数据库上可能

会导致其索引能力的下降。这个长字符串也使得DBA很难对主键进行操作(例如进

行常规的维护任务、制作报表等)。

Page 92: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

自动生成键的存储过程(Stored Procedures for Autogenerated

Keys)

使用JDBC2.X或1.X驱动程序的BMP 实体 bean的开发者需要一种创建简单的基于整数

的主键的方法,而每个实体 bean的主键都应该是唯一的。大多数关系数据库都提供了它们专

有的内置的自动生成键的特性。 怎样才能以可移植的、高效的方式使用关系数据库内置的自动生成的键呢? 大多数数据库都提供了一个主键生成服务用来为新插入的行自动生成一个主键。这种工具

中 普通的就是自动递增计数器(通常叫做一个序列或一个标识列),它允许你只是通过从0开始递增一个数来创建主键。自动递增计数器会被请求以获得下一个可用的数字,它可以用来产

生一个数据库表的主键列的数字。通常,这个自动递增计数器直接和表中的主键列联系在一起,

能够自动地用序列的下一个数字为新插入的一行产生主键字段的值。对于BMP程序员,自动生

成键提供了一种简单的、强有力的、内置的主键生成机制。 EJB规范要求一个新创建的实体 bean的主键要作为ejbCreate方法的一个返回值而被传

递给容器。这就对想使用自动生成键的BMP开发者提出了一个问题:当执行一个JDBC插入操作

时,返回的结果集中只包含了一个插入行数的数值,而不是被插入行的主键,所以插入行这个

行为并没有将生成的主键提供给开发者。考虑到这个限制,开发者怎样才能通过编程获得插入

行的值呢? 这个问题在JDBC3.0(JDK1.4的一部分)中得到了解决。JDBC3.0扩展了statement接

口,在其中加入了使用标准的API方法返回任何插入行的主键的功能,如下面的代码示例: PrepartedStatement pstmt = conn.prepareStatement(); pstmt.executeUpdate(“insert Into sometable(field1 ,field2)” + “values (‘value1’, ‘value2’)”, Statement.RETURN_GENERATED_KEYS); ResultSet rs = pstmt.getGeneratedKeys(); if ( rs.next() ) {

int myPrimaryKey = rs.getInt(1); }

遗憾的是,如果开发者不能为他们的数据库使用JDBC3.X驱动程序,那么他们也就不能利

用这个标准的使用自动生成键的方法。 一种解决方案是将ejbCreate方法编码为在插入操作后立即执行一个SQL select语句以

得到新插入行的主键。这个方法的问题是可能没有任何能够只唯一地选取新插入行的方法,要

知道一行中只有主键是保证唯一的。如果其它插入的字段不能保证唯一,那么就不可能产生其

参数的唯一程度能够保证只选取新插入行的SQL select语句的where从句,也就是说,在表

中这些字段都相同的行不止一行。另一个问题是这个方法需要两个数据库调用(当数据库在与应

用服务器不同的机器上时就成了网络调用):一个用来执行插入操作,另一个用来获得 新插入

行的键。 许多支持序列数(自动递增计数器)的数据库允许创建不和任何特定表联系在一起的序列

数。通过这种方法,开发者可以在一个数据库调用(通常是通过在一个提供类似

nextval(“sequencename”)过程的DB上执行select操作)中获取下一个可用的数字,在这

个调用中递增了计数器,并立刻返回了下一个数,然后在插入(insert)调用中使用这个生成

的数把主键连同这个实体 bean中的其它内容一起插入,这样主键就可以通过ejbCreate方法

被返回了。这种方法需要两个数据库调用,不过它的主要问题是它不能在各种不同的数据库之

Page 93: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

间移植,而某些提供了递增计数器工具的数据库( 著名的是SQLServer)也不允许你创建不和

任何表联系在一起的序列计数器。这样在执行插入操作前就不可能获得下一个可用的主键。 自动生成键提供了一种用来创建主键的、强有力的内置机制,但是硬编码你的实体 bean

使它能通过一些与具体的DB有关的机制来访问生成的键,将限制代码的可移植性而且常常需要

在ejbCreate方法中使用多个数据库调用。一个实体 bean的持久性代码在理想情况下应该可

以移植到不同的程序服务器和数据库上。 因此:

使用存储过程在同一个调用中将数据插入数据库并且生成主键。实体 bean可以被编写

为使用标准的和可移植的JDBC的CallableStatement接口来调用一个存储过程。

存储过程是所有支持SQL的数据库都具有的特性。它们允许将复杂的数据访问逻辑直接编

写到数据库中,并将这些逻辑在数据库中进行编译以获得 佳的性能。在JDBC中,通过使用

CallableStatement接口可以实现用一种完全独立于数据库的方式来访问存储过程。这样,

存储过程就可以被用来作为一种独立于数据库的、在一个数据库调用中插入一行并得到自动生

成的主键的方法。通过将你的实体bean编写为使用标准的CallableStatement API,可以

获得在不同的数据库之间的可移植性,因为与数据库提供商有关的代码被存储在数据库而不是

EJB层中。因此,可以以一种标准的方法来使用任何支持自动生成键的关系数据库,而不用在

数据库需要改变时重新编写ejbCreate方法的代码。 通过使用一个存储过程,ejbCreate方法的代码可以将所有需要作为参数被插入的实体

bean属性传递给一个JDBC的CallableStatement,并执行这个CallableStatement。在

数据库端,存储过程将使用与具体的DB提供商有关的机制执行插入和获取(get)所生成的键

的操作,所有这些操作都在一个存储过程中实现。 对于银行帐户的例子,Account实体 bean的 ejbCreate方法应该类似这样:

public AccountPK ejbCreate(String ownerName, int balance) throws CreateException{

PreparedStatement pstmt = null; Connection conn = null; try (

this.ownerName = ownerName; this.balance = balance; conn = getConnection(); CallableStatement call = conn.prepareCall( “{call insertAccount(?, ?, ?)}”); call.setString(1, this.ownerName); call.setInt(2, this.balance); call.registerOutParameter(3, java.sql.Types.INTEGER); call.execute(); this.accountID = call.getInt(3); return new AccountPK(accountID);

} catch (Exception e) . . . 在上面的代码例子中,创建了一个CallableStatement来调用insert-Account存储过

程。所有传入ejbCreate方法的实体bean的属性接着被传递给用来插入的insertAccount存储过程。在数据库端,insertAccount过程把传入的数据插入到恰当的表中,并在同时用

与提供商有关的钩子函数(hook)产生自动生成的键,然后存储过程将通过把键置于输出(OUT)

Page 94: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第五章 主键生成策略

参数中(在这个例子中就是过程调用的 后一个问号)来将生成的键返回给客户端,从而允许

ejbCreate方法中java代码在调用执行完成后能够访问它。 这个方法的优势在于: • 简易性 如果你使用的是BMP 实体 bean和一个带有自动生成键工具的RDBMS,那就

没有必要实现诸如在EJB的UUID模式和序列块模式中描述的那些更复杂的主键生成

策略。 • 可移植性 CallableStatement接口是一个标准的、可以移植到任何数据库上的

JDBC API。

而这个方法的缺点是: • 增加的对数据库底层架构的维护 必须为应用中的每一个实体 bean创建一个新的

insertXXX存储过程,来为ejbCreate方法执行插入操作。每当在底层的表中加入

一列或在一个实体 bean中加入一个属性时,相关的插入存储过程也不得不跟着被更

新。不过这可能不会是主要问题,因为存储过程的创建代码通常同表的创建代码存储

在相同的脚本中,而表的创建代码在表被改变时总是要被更新的。 • 并不是所有的数据库都支持自动生成键 所有企业级的数据库都支持某种形式的自动

生成键机制。对于那些不支持自动生成键机制的数据库,如果必要的话可以用一个存

储过程来手工地操作一个简单的递增计数器。例如,在数据库中,有一个维护着被数

据库中所有其它的表所使用的、存储了它们各自的当前 大键值的表,为了维护这个

表,可以使用insertXXX过程来手动地递增那些位于这个表中行。

在编写BMP 实体 bean时,自动生成键的存储过程模式提供了一种快速的且可移植的方法

来使用RDBMS内置的生成键工具。在这一章中还论述了另外两个在BMP和CMP中都能使用的选

项:序列块模式和EJB的UUID模式。

Page 95: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第六章 从需求到模式驱动的设计

现在,你打算对应用实施本书的思想,同时,你也已经列出了应用必需支持的所有用例。

你应该如何准确地将你对业务问题的概念化的理解映射成为一个 J2EE 的应用设计?如何才

能将本书中所介绍的模式合理地运用其中? 已经构建有不同的流程和 佳的实践用于将业务问题转化为具体的设计。这一章我们不

会展示具体的流程和方法。相反的,我们将选用一些现实世界的需求来看一看它们是如何作

为模式驱动体系结构被实现的。 我建议你在阅读此章前浏览一遍第一章中的模式。你可能会在阅读此章时需要不时地参

考前面所述的这些模式。这章同时也是本书中所介绍的所有模式的回顾。因而,在阅读此章

后,你应当对如何将本书中的模式运用到你自己现实世界的项目中去有了很好的理解。 我们将要一起设计的应用是 The-ServerSide.com J2EE 社区的论坛子系统,这是一个有

关 J2EE 新闻、讨论、模式、应用服务回顾、文献的产业资源。注意这本书没有一个长期运

行的事例--TheServerSide 在这章中仅用来说明如何运用于现实世界的应用进行模式驱动的

设计。 始于 2000 年 5 月,TheServerSide.com 是 早部署的基于包含基于 J2EE 的网站之一,

有着 EJB 后端。网站由 Ed Roman(Mastering EJB(2002)的作者)任 CEO 的 Middleware 公

司投资创建,建立 TheServerSide 目的是为 J2EE 的开发者建立 Web 社区。在后端,社区本

质上仅是一个基于 EJB 的消息板风格的应用。事实上,第一个 TheServerSide 的版本在一个

简单的四个实体 Bean 的域模型前台只有一个会话 Bean。此后,该部分被定位成一个论坛

组件,其他的一些功能(例如调查、投票、电子邮件等等)作为不同的组件加入系统,会话

Bean 是系统与这些组件的接口。

TheServerSide 论坛信息系统用例 根据我们的经验,一个用例驱动的方式是在设计任何软件应用中所能采用的 为有效的

方式。为了构建一个成功的应用,我们需要理解用户们的需求(Carey,et al.,2000)。对于 TheServerSide 来说,这句话极为重要。因为,它能帮助我们专注于如何建

立一个简朴紧凑和实用的网站以及如何使之快速地投入运作,而不是将时间浪费在酷的特色

和后台的基础设施的设计上,而这些可能会延误项目的启动(这可以回溯到几个相似的 J2EE社区项目的启动情况)。 对于 TheServerSide 我们需要构建一个论坛/消息板系统,可以通过主题(论坛)组织的

方式,使开发者们互相传递消息。此外,回复的消息一定要同原始消息组织在一起,用来建

立对于同一话题的一系列讨论。TheServerSide 也必须支持管理功能,用以管理讨论(论坛)

的主题,管理发送的消息等等。 使用这个需求占首要位置的方式,我们给出了一套系统需要支持的用例,以实现系统目

的。图 6.1 中给出了这些用例的一个子集。要特别注意这些用例,因为在余下的章节中我们

作出设计决定时将要经常参考它们。

Page 96: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

图 6.1 论坛消息系统用例

余下的章节将会向你展示如何获取这些需求以及如何映射它们用以设计。每一个用例

都有自己的语义和详细说明的用户交互作用,用来确保所有的构建者和其他的项目成员(尤

其是客户)理解和认同每一个用例的含义。如图 6.2 中的标识地址用例图就是这样的一种用

以详细说明 TheServerSide 而使用的工具。

图 6.2 发送消息用例实现

在对系统所必须支持的用例有了可靠的理解之后,下一步是要分析用例模型和开始提出

具体的应用设计,包括域模型和其他的结构。我们会采用模式驱动的方式,使用这本书的模

式来将用例构映射为可能的 J2EE 的构架,而不是去实践整个的设计过程(这超出了本书的

范围)。

Page 97: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

但是开始的时候,我们需要了解一些必须的背景资料,这是每一个分布式系统的构建者

和开发者都需要理解的。

设计问题和术语的快速回顾 在讨论构建和设计的时候,为了可以用该主题所需的细节来处理它,我们对一些基本术

语在理解上需要达成一致。如果你还没有参加过大型的面向对象系统的设计和构建,我们强

烈地建议你在继续以前阅读这个部分,因为本章余下的部分(和第七章)广泛地运用了这部

分所介绍的概念。 什么是域模型 一个域模型表示一个业务问题中的所有名词:人物,地方,事情和观念。模型中的域对

象经常由应用中的用例推导所派生,或是通过向对业务问题有着深刻理解的领域专家(例:

那些资助你们项目的客户)进行咨询获得。 以 TheServerSide 的用例模型作为起点,我们可以派生所有应用中的域对象。例如,采

用发送消息用例,我们可以推出消息模型的存在的必要性。同样的,消息需要被发送到一些

分类的话题范畴,我们可以称之为论坛。采用添加回复消息模型,我们了解到我们需要一种

方式用来关联消息和它的回复体系。线程就是我们可以实现这些工作的一种机制,从而就有

了视图线程的用例。 后,消息需要与发送者的身份相关联(在 TheServerSide 中不允许匿

名发送),因而我们需要为应用中的主角--用户来设计模型。 理解 J2EE 系统的层次

一个基于 J2EE 的应用(或者任何此类事件的分布式系统)通常都可以分为如图 6.4 所

示的层次集合。在本章余下的部分和第七章将经常用到这些层次。

图 6.3 TheServerSide 的简单域模型

Page 98: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

图 6.4 一个 J2EE 系统的层次

分层如下: 表示:所有应用的实际 UI 部分,例如 HTML,JSP,Flash,Swing,或者 AWT 类。JSP 标

记库(仅仅用于格式化目的)也可以视作本层的一部分。 应用:应用层通过在表示层和服务层的组件间提供黏合剂和工作流来结合一个应用。总的

来说,这一层负责管理用户端状态(HTTPSessions),执行用户输入的一致性验证以及为业务

逻辑授权服务层。Taglib 如果对 EJB 层存在调用,也可以视作本层的一部分。 服务:服务层(会话 Bean)是 EJB 端事务的主要入口点,主要负责为应用层调用与特定的用

例相关的业务逻辑。服务层通常由会话表面层 pattern(第一章)实现。服务层的主要功能

是为用例(对于一个域对象)调用业务逻辑提供方法,控制底层运行用例的交互以及处理需

要实现用例的域对象间的授权和工作流。这里的一个关键区别是多个应用可以进入同一服务

层,正如一个网站和一个厚(thick)客户端都可以进入同一会话 Bean 层。 领域:领域层(例如,实体 Bean)是对业务员问题(域模型)进行面向对象分析而产生所有

对象的所在。服务层授权许多它从领域层获得的请求(Fowler and Mee,2001)。因而,领域

层就是业务问题的内容所在,独立于应用(可跨应用/项目重复使用)。 持久性:持久性层包含使你的域模型保持在数据源中所需的所有通道逻辑。对于 CMP 实

体 Bean,JDO 和 O/R,开发者不需要为这层编程,而且可以使用外部的工具为数据源映射

域对象。对于 BMP 实体 Bean 和会话 Bean,这一层可以通过使用第三章中的数据访问命令

Bean 模式实现。

Page 99: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

业务逻辑对比域逻辑 对业务逻辑与域逻辑的含义有许多的混淆,每种逻辑包含在前述的五层系统中。 域逻辑是一种作用于系统中的域对象的逻辑。大多数应用中的逻辑是作用于系统中的

域对象(应用中业务事件)的逻辑,因而属于域对象自身(如果你关心良好的 OO 原则,

例如封装)。然而,既然这个逻辑解决一个业务问题,许多人考虑使其符合业务逻辑的描

述。例如,用一个论坛对象的发送消息方式来实现发送消息用例,因为只有一个论坛才会

知道把消息发送回给自身。因而,在论坛中发送一条消息的逻辑是域逻辑且属于领域层。

但是,如果业务逻辑和域逻辑是同一事件,那么在服务层的会话 Bean 方法会是怎样

的呢?令人奇怪的是,这个逻辑也是业务逻辑的一种,但是如果考虑工作流的话,它又与

领域层的业务逻辑不同。更确切的说,服务层的业务逻辑是为了实现一个用例,在多种(可

能没有关系)域对象或者外部系统(通过 JMS 或者 Java 连接构建陈述的系统)中起作用

的逻辑。例如,在调用 Forum.postMessage(aMessage)前,会话 Bean 必须先创建正被讨论

的消息,然后再将它传递到论坛(一个工作流的小例子)。其他的业务/工作流逻辑的例子

是发送 JMS 消息到消息驱动的 Bean,电子邮件,更新记录,通过 Java 连接构建与遗产系

统等等。服务层的业务/工作流逻辑基本上是需要实现用例的任何种类的逻辑,它并不仅仅

符合域对象所表达的概念(因而不应在域对象后封装)。 这个原始两难的解决方案是使用两种业务逻辑,一个基本上是域逻辑,另一个包含不

属于域逻辑(因而运用于服务层)的工作流。

模式驱动 EJB 体系 现在在我们手头已经有了用例和必要的体系背景,我们准备将本书中的设计运用到现实

的应用设计中去。我们将要采用的方法是运用本书中 为基本的模式逐层地设计

TheServerSide,同时关注考虑这些模式允许的可选架构。 既然我们系统中所有的其他层次取决于领域层和持久性层,这就是开始的 佳位置。 领域层和持久性层模式 当我们设计我们系统终端的时候,我们采用的体系和模式会根据我们的应用含有领域层

(例如实体 Bean)或不含有领域层(例如使用 JDBC 的会话 Bean 或者存储过程)而变化。

接下来我们来尝试这两种方法:首先,我们设计一个不含有领域层的系统,然后再设计一个

含有领域层的。

Page 100: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

为什么直接连接数据库 一些选择直接数据库调用的非派别原因是: 使原型能够快速建立。直接数据库连接有助于拼凑那些没有长的生命期和不会经常变

动的快速原型应用。 提供轻巧的域模型。如果一个应用是很简单的,那么它可以很快地将它拼凑起来,而

不是将预先的时间花费在建立一个很漂亮的域模型上。例如,TheServerSide 的论坛子系统

是非常简单的,只有四个域对象。因而,如果不是将时间浪费在编写一个漂亮的

BMP-entity-Bean-based 域模型上,TheServerSide 很快就可以投入运行(当时只有一个论坛

子系统)。幸运的是,这样的行动并没有被采用。TheServerSide 随着时间而停止了成长与

变化,OO 域模型后端帮助减轻了维护的负担。 能够根据性能的原因来绕开域模型。 当实现一个本来就是只读的用例,例如 ViewThreadSumaries 或 ViewThread,开发者

可能希望至少可以避免域模型,由他们自己编写持久性逻辑。由于 JDBC 读取模式(第三

章)所表达的原因,对于这些类型操作,避免域模型和直接连接数据库会变得快捷很多。

不含有领域层的持久性层模式

当设计一个不含有领域层的系统时,Data Access Command Bean 模式(第三章)为构建

一个持久性层提供 好的实现。使用这个模式,PostMessage 和 ViewThreadSummaries 用例

的持久性层执行通过使用 data access command Bean 来实现,如图 6.5 所示。

图 6.5 含有领域层的持久性层模式

Page 101: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

有点类似地是,我们系统所有的用例会 终映射为处理持久性的 DACB。Data access command Bean 提供一个标准且有效的方式,完全将持久性逻辑封装在一组 command Bean后。它们有效地成为一个应用中的持久性层和领域层。 含有领域层的持久性层模式 如果你明智地决定将你的业务问题模拟为一个面向对象的域模型,那么运用哪个持久性

层模式取决于你使用那种技术实现你的域模型。这个域模型实现技术选择可以分为两种类

型:那些为你生成持久性代码和那些不生成的,说明如下:

使用生成的持久性逻辑 使用 CMP 实体 Bean,JDO 和对象/关系映射工具可以提供你一

种技术,使你可以实现你的域模型,而不必编写任何的持久性逻辑。它们让你编写你的领域

逻辑和提供一种工具自动保存你的域对象到一个数据库。既然持久性层已经生成了,就没有

空间让开发者使用任何模式。 做自己的 BMP Bean 是一种域模型技术的例子,它要求开发者编写所有的持久性逻辑。

尽管 BMP Bean 提供(而且鼓励)你直接编写持久性代码到域对象本身(在

ejbLoad/Store/Create/Find/Delete),开发者仍应考虑建立一个分离的持久性层,本书的设计模

式帮你解决这个问题。 一个持久性层可以使用 Data Access Command Bean pattern(第三章)在 BMP 实体 Bean 领域层下建立,这与图 6.5 所示的方式是很相似的。这将会使你 BMP 实体 Bean 脱离持久性

逻辑的束缚,将逻辑定位在它自己封装良好的类层上。另一个对此有用的模式是 Data Access Object pattern(Alur,et al.,2001)。 领域层的模式

在领域层上,有许多模式的应用,这取决于上下文环境和正在解决的问题。在这个章节,

我们要评定 TheServerSide 的需求和根据那些影响并发性,可移植性,可维护性,工具使用

和生成键码的需要来选择实现方法。

并发性 在图 6.1 中所概括的用例中,除非采用一些特殊的预防措施,EditMessage 用例

将使得基础数据库存在崩溃的潜在可能性。图 6.1 表明只有管理员可以编辑一条消息。但是,

当两个不同的管理员希望编辑同一条消息会发生什么事呢?正如 Version Number pattern(第

三章)所解释的那样,这里有潜在可能性使管理员互相覆盖了对方的变化。解决的方法是使

用 Version Number pattern,它可以使你添加可选的对域对象的并发性检查,这些域对象含有

可导致潜在数据库崩溃的用例。 工具 大多数的现代 EJB 开发工具自动完成许多使用 EJB 需要的乏味的开发任务,包括

维护在远端/本地接口和 Bean 类的一致性业务方式签名。当然,如果你发现你自己需要使用

一个文本编辑器(VI 是我 喜欢的)来编写你的 EJB,那么 Businees Interface pattern(第一

章)可以帮助你获得发生在在远端/本地接口和 Bean 类的不一致性业务方式签名错误。这个

模式可以应用于所有的 EJB,而不仅仅是实体 Bean。 可移植性 如果你正在设计一个实体 Bean 域模型,而这个模型潜在地可能在不同的和

不可预测的应用服务环境里使用,那么很可能不同的应用服务器需含有不同级别的对 CMP和 BMP 的支持。Dual Persistent Entity Bean 模式(第三章)提供解决这个问题的一个方案。

既然 The Middleware Company(TheServerSide.com 的建立者)致力于培训事业而不是组件

的零售事业,那么 TheServerSide 就不含有需要在 CMP 和 BMP 间可移植的用例,因而这个

模式就不会影响此章中的设计。

Page 102: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

可维护性 如果在 EJB 1.X 环境(不含有远程接口)下编程,而且你希望使用数据传送

对象模式,或者如果你要完成一个含有重量级的实体 Bean(含有很多属性)的应用, Generic Attribute Access 模式会说明如何提供你的实体 Bean 一个一般的,基于 HashMap 的接口,

这样就可以减轻简化你的实体 Bean 的实现和接口。 键码的生成 在一个分布式系统中的域对象需要使用键码以将一个对象实例与另一个区

分开来。特别的,当 ejbCreating 一个实体 Bean 时,实体 Bean 需要一个可以返回给容器的

键码。但是开发者如何才能为他们的实体 Bean 生成键码呢?第五章提供三个设计模式,它

们都没有改变实体 Bean 本身的构架,其中的一些可以提供恰当的创造性工具 EJB 来生成键

码。

服务层模式

当决定如何设计我们用例的服务层构件时,需要询问的第一个简单问题是这些用例在本

质上是同步的还是异步的。 异步用例 异步用例是那些客户端可以初始化但不需要等待回复的用例。也就是说,一旦客户端初

始化了用例,他或是她可以继续使用应用程序,与此同时,用例以并发的方式处理。开发者

可以识别出异步的用例,因为那些用例不要求返回任何种类的立即返回值或确认给用户。例

如,大多数的网上书店以异步的方式实现相当于 purchaseBooks 的用例。一旦用户输入了所

有的信息,点击了 终的提交按钮,他们不用坐等他们的信用卡结账或是将书本列入详细清

单和装运。而是用例在后端触发,然后用户就可以自由的浏览其它的网站。 对于 TheServerSide,PostMessage 和 AddReply 用例可以异步执行,因为一旦一条消息

被输入和提交给服务器,客户端不一定需要等它被加到论坛中去。对于这些用例,消息表面

层模式(第一章)提供一个构架来对这些用例异步地实现业务逻辑。消息表面层模式提倡使

用一个消息驱动 Bean 来为一个用例封装业务逻辑。因而,PostMessage 和 AddReply 用例可

以如图 6.6 实现服务层。

图 6.6 服务层和消息 façade 模式

同步用例 在用例执行的时候,同步用例需要用户停下来等待。所有从服务器读取数据的用例是同

步的,例如 CreateForum 或 EditMessage 用例,需要管理员了解用例是否成功地执行。 大多数的用例都是同步用例,同步用例可以用两种模式:会话表面层模式和 EJB Command 模式(第一章)来实现它们的服务层。

Page 103: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

会话表面层 会话表面层模式是使用 多的 EJB 设计模式,也是本书中其它模式的样板。事实上,

会话表面层模式是服务层。它提倡将 EJB 应用中的领域层隐藏在一层会话 Bean 后,从服务

层封装一个 J2EE 系统中所有的业务逻辑。 例如,如果我们需要改变 PostMessage 和 AddReply 用例的规范以使用户能够浏览他必

须提交的消息,然后在后端实际上将消息加到论坛的时候,用户必须停下来等待(因而这些

用例现在可以本质上成为同步)。 在服务层,这些用例作为无状态会话 Bean 上的两种方法:postMessage 和 addReply 来

实现。例如,postMessage 用例的业务逻辑会被封装在会话 Bean 方法上,由生成一条消息以

及将其传递给论坛上的 postMessage 方法组成。因而,域逻辑在域模型上维护,而要求实现

用例(创建消息以及将其传递给论坛)的工作流在会话 Bean 上维护,如图 6.7。 图 6.7 在会话表面层上的传递消息用例

图 6.7 在会话 Façade 上的发送消息用例

当我们使用会话表面层模式设计服务层的构架时,同一种类的用例和进程以同一会话 Bean(如果想要了解更多有关资料,请参看会话表面层模式)的方法分组。因而,我们应

用的服务层潜在地可以分为两个会话 Bean,一个包含所有与论坛消息板相关的用例,另一

个与用户和用户周围的用例相关,如图 6.8 所示。

Page 104: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

图 6.8 TheServerSide 的会话表面层服务层

实现服务层构架的唯一的方法是使用 EJB Command 模式来代替会话表面层。 EJB Command 模式 EJB Command 模式(第一章)是实现用例服务层业务逻辑的另一种可供选择的方法。

考虑 Command 模式 简单的方法是将其看作一个轻量的会话表面层,这种方法的另一个好

处是可以减弱客户端对EJB细节的要求(与在本章稍后介绍的Bussiness Delegate模式相似)。 使用 Command 模式,每一个用例的业务逻辑被封装成小的轻量的 command Bean,产

生了一个由许多构造合理的 commands(每个用例一个)组成的服务层,如图 6.9 所示。

图 6.9 含有 Command 模式的服务层

其它服务层模式 会话表面层,消息表面层和 Command 模式展示了如何在服务层上构造我们的用例业务

逻辑。然而,特定的用例需要对表面层内部的业务逻辑和 command 模式进行不同的设计,

以体现性能和可维护性之间的关系: 性能 正如在持久性层模式中提到的那样,对于实质上只读的用例,JDBC for Reading模式(第三章)可以通过避免实体 Bean 领域层来帮助改善性能,这有利于数据库的访问。

使用这个模式,ViewThread 和 ViewThreadSummaries 用例可使它们的服务层业务逻辑忽略

Forum 和 Thread 域对象,这有利于与持久性层(使用 Data Access Command Bean 模式)的

直接交互。 可维护性 数据传送对象模式提供在服务层如何构造数据传送对象和如何将其返回给客

Page 105: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

户端的特别的 佳实现。这个模式的实现存在于服务层,但是我们将在介绍完与我们样例有

关的数据传送对象模式后,在下一节来讨论它。

内层数据传送模式 我们应用中的每一个用例都包含了应用层和服务层之间的交流,这需要一种机制来将数

据从层与层之间进行传送。 数据传送模式和会话表面层模式的组合式 EJB 开发者中 常使用的构架。DTO 模式提

倡建立一个简单的,可序列化的 Java 类来起到封装的作用,把大量的数据(在一个网络调

用中)从服务端传到客户端或从客户端传到服务端。另一种可选的方法 HashMaps 也可以代

替 DTOs 来使用(下文说明)。 当传递的只读数据用以列表显示时,行列集也是 DTO 的一种很好的替换方法(下文说

明)。 数据传送对象 在 TheServerSide 中应用的 DTO 需要为几乎每一个用例创建,因为它们中的每一个(除

了对逻辑用例)都要在应用和服务层之间传递数据。使用 DTO 模式,会创建一层 DTO,如

图 6.10 所示。注意 ForumServices 类上的方法名称。

图 6.10 消息领域 DTO

在这里我们介绍两种类型的数据传送模式:域 DTOs 和定制 DTOs。消息 DTO 是一种

Page 106: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

域 DTO,因为它直接映射为一个消息域对象,被用来从客户端接受数据和将发送和更新版

本送回给服务器。另一方面,ThreadSummary DTO 是一种定制 DTO,包含的属性来自三个

域对象:Thread,User,Message。ThreadSummary DTO 完全是一个特定用户接口,用以显

示 thread 的一个概要,例如 TheServerSide.com 的主页。 数据传送对象工厂 数据传送对象模式提倡将 DTO 创建和消费逻辑封装在一个 DTOFactory 里,这样可以

将实体 Bean 同 DTO 创建和消费逻辑分离开。在我们例子中应用的 DTOFactory 将会从

ForumServices 会话 Bean 中提炼出有关 DTO 的方法,可以通过以另一个会话 Bean 的方式

来实现 DTOFactory 或者以一个 ForumServices 指派的普通 Java 类的方式,如图 6.11 所示。

图 6.11 DTOFactory 实现选项

注意使用数据传送对象模式的要点是不应该在服务层和域对象间使用,这在 EJB1.X 中

很普遍,而且,域对象不应了解 DTO 知识。 数据传送 HashMaps: DTO 的一种替代方法 为使这个章节在编写上更易管理,上面介绍的例子仅提供了有限数量的 DTO。但是通

常开发者需要处理急速增长的 DTO。数据传送 HashMap 模式(第二章)讨论一个通用的

HashMap 是如何被用来代替整个 DTO 层的。 数据传送行列集: 作为一个基于网络的站点,几乎每一个 TheServerSide 上的用户接口自然地都是表格形

式的,因为 HTML 表格是组织网页数据的主要方式。此外,作为一个消息论坛系统,站点

的用户只需要只读入口。因而,所有的用例包含浏览本质上只读站点(例如 ViewThread,

Page 107: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

ViewThreadSummaries)的功能而且将会在 TheServerSide 上以表格方式显示。因而,

ViewThreadSummaries 用例将会被实现如图 6.12。 使用行列集需要与数据库直接连接,因而,应该将它们与适合于 Reading 和 Data Access

Command Bean 模式的 JDBC 协作使用。

图 6.12 对只读用例使用 Rowsets

应用层模式

由于这本书是关于 EJB 设计模式的,它不包括任何可以在实质上改变应用层的构架的

模式(参阅 Core J2EE pattern[Alur,et al.,2001] for an excellent set of presentation and application layer patterns),但是,本书也包含了两种重要的模式,可以提供应用层与服务层

之间如何交互的 佳实现方法。 EJBHomeFactory 模式 这个模式提供一个整洁的封装好的高性能的机制,可以为应用

层 客 户 寻 找 EJBHome 。 例 如 , 用 例 可 以 简 化 地 调 用

EJBHomeFactory.getFactoryI().lookUpHome(ForumServicesHome.class),而不是查寻论坛服务

的 Home 接口。通过解除记忆 JNDI 名字(这些被抽象到 web.xml 的 ejb-tag 中)的负担,这

也进一步地简化了应用层逻辑。 业务代理 (Business Delegate)模式 业务代理提倡创建一个轻量普通 Java 类来隐藏

来自于应用逻辑的 EJB 的细节和复杂部分。使用这种模式,应用逻辑将只与业务代理交互,

如图 6.13 所示。注意可以使用 EJBHomeFactory 来优化业务代理。

Page 108: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

图 6.13 业务代理

在那些表示/应用层程序员和工作于服务/领域的 EJB 开发者之间分离的大型项目中,业

务代理模式 为有用,可以作为一种使表示/应用层程序员与 EJB 的细节屏蔽开的机制。

小结 到这里,我们已经为 TheServerSide.com 的消息论坛组件获取了用例,并研究了这本书

中的模式是如何来影响系统中每一层用例的实现。你可能感到不能肯定应该选择那个模式,

应为很多模式是互斥的。我推荐你要密切注意上下文的关系和每个模式所解决的问题,以及

以及它们的正反两方面。有一个符合你需要的 佳模式,你必须将其与其他可选方案放在一

起进行简单地评估,以发现哪一个才是能应用于你的特定用例的 佳模式。 幸运的是,模式可能与其他模式有着密切的关系,这会导致我们看到模式的特定组合在

项目中不断地被重复使用。我喜欢将那些模式组合的类型视作参照体系( reference architectures)。这就是说,特定模式组合引起在项目中一致的特定构架。 可能用来构建 J2EE系统的 常用的参考体系是数据传送对象 + 会话表面层模式组合,

这可以实现一个严格区分和整洁的服务层。在 TheServerSide,我们使用的参照体系是

EJBHomeFactory + DTO + 会话表面层 + DTOFactory + Sequence Blocks 模式组合(当然是

和域模型在一起的)。

Page 109: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第七章 EJB 的开发过程:用 ANT 和 UNIT 构建,用 JUNIT 测试 假设你头脑里已经有了一个待设计的系统的概念性设计,你知道它必须支持的用例及其

从用例到会话 Beans 的方法的映射,这些会话 Bean 来自你的服务层。并且你至少在概念意

义上知道系统操作涉及到的域模型,你还知道系统持久存储的方法。 现在出现的问题是如何对你所设计的系统进行 佳开发。哪句代码应当首先编写?你如

何配置你的系统,才使它能兼容 J2EE 环境?单元测试如何?总而言之,你应该用什么工具

和方法去支持开发、环境管理、测试等环节的过程? 本章将讲述一些有效且极其可行的方法来解决这些问题,指导你将一个概念上的应用框

架实现为一个可运作的 J2EE 系统。我们将考虑一些比较实际的问题,例如代码开发的规则,

这意味着如何管理 J2EE 的应用程序环境,单元测试的 佳方法,支持 J2EE 开发的工具和

方法等等。设想在你周围有许多具有潜在冲突的影响。你已经为你系统的运行选择了一个

J2EE 平台,并且已经设计得使它可以与行业中 好的实践模式(例如本书中提到的模式)

相协调。而且,有可能你已经提交了时刻表,因为用于保证开发进度得效率是很重要的。但

同时你又关心工作质量,因此在工作中自觉地去保持专业水平的的质量 (Hunt,et al., 1999)。 在这章中提出的解决方案将尽量平衡这些有冲突的影响因素。为了避免让任何人产生这

种印象:我们的建议只是极度理论说明性的,我们向你保证我们的目的绝对不是这样的。我

们希望每个开发者都能轻松地做到 好,能够创造性地地解决业务问题。我们不希望开发小

组变得没效率,以至于引起环境的不协调、先前工作方式的破坏以及可重复性的缺乏。相反,

我们相信养成本章中提到的习惯能使我们自由地发挥我们所有的潜能。

开发顺序 哪个代码你应该首先编写?你是否是整个项目的唯一开发者,或者是一个小组的成员,

而这个小组已经为组织和分配任务采用了一定的策略?这个问题是一定要面对的。幸运的

是,那些在你周围影响你的因素力量结合起来也暗示了一个简单自然的解决方案,它可以很

好地为不同大小和采用不同组织策略的开发小组工作服务。 在开发开始以前,我们假定你已经为你的系统设计了一个至少分了五层的模型,如图7.1所示。(可以参见第六章的理解J2EE的分层小节加以快速回顾)

图 7.1 分层体系

Page 110: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

这样分层的一个结果是你要编写的用来实现分层的java类型(类和接口)可以显示出向

下的依赖关系。那样,上层的类型可以在编译和运行的时候都依赖下层的类型。但是对于每

个行业的 优方法,相反的情况是不正确的,对象和分层不应该通过向上的类型依赖将客户

对象和分层所用到的知识编码进来。 而且,java编译器是通过类型依赖来驱动的。在编译一个给定的源文件时,java编译器

首先要为编译源文件引用的类型所对应的类寻找特定的类路径,如果必要和可能的话,用其

编译引用的类型。类型图的这种深度优先遍历意味着持久性类要比域类首先编译,域类要比

服务类先编译,以此类推。这种现象提示你要先编写底层的代码,再编写高层的代码。而且,

你也应该随着实际的一些变动而作一定的缓和调节。第一个变动就是你可以有一些类型(比

如,例外和工具类)跨越分层,可用于不同的分层上。因此,它们可以被首先编译。第二,

为了使用户接口的开发和持久性层开发相一致,尽可能快的实现一个工作的服务层是有帮助

的。实现这个的一种方法是一开始就忽略持久性层,返回“读”请求的例子数据,用“空操

作”实现“创建”“更新”“删除”请求。然后当依赖服务层的API开发用户接口时,真正

持久性层的执行便能被编码进来。第三,也是 后一点,假定你在一个迭代性的、不断增加

的过程中工作,因此你不必开发给定层次的全部,只需开发有必要的那部分——支持你为当

前反复所做的用例的那部分。 后的解决方法——也可能被称为“领域外开发(out of domain)”——将在接下来的部分详细说明。

独立于层的编码 如上所述,你可能提出一些 java 类型,它们不是很准确地属于任何 "层" —它们在多个

层面被用到。这些类型的两个主要例子是 "公用程序"(utilities) 和例外。预测你可能需要

什么"公用程序"是很困难的;它们一般在编写代码的过程中出现。一个普通的例子是一个类

为了字符串操作而实现静态外部方法。但是你至少需要在心里有一个主意——该到哪里收集

这样的方法, 和该把收集这些方法的类放在哪个包里?一种途径是创建一个命名为字符串

的类,目的是为了收集能处理字符串 并且能将字符串类放到有可能的包中的静态外部方法。

例外是另外一回事。你会妥善地用一些方法去处理那些你将会在你的 codebase 中使用的例

外类型和能包含你的例外类型的包。因为你不能编译那些类——它们的方法引用了你的例外

类型直到你对例外进行编码,所以这是一个令人遗憾但又必须的出发点。On Ward Cunningham’s Portland Pattern Repository,在 java 里有许多关于例外处理的讨论,还有许多

有价值的文献模式。我们建议你要特别注意那些命名为“不要抛出类例外”“例外传播”“使

例外一致”“转换例外”的模式。

首先是域

一旦你为例外处理准备好了包结构和方法,你就已经准备好开始编写你的领域层了。是

否要像实体Beans或一般java对象那样实现域模型将是你要做的第一个选择,因为每个选择

对于你将如何实现你的持久性层都有明显的意义。不管怎么样,领域层是一个恰当的出发点,

因为你不能在服务层编译会话Beans,直到你在领域层已经编译好了为止。此外,域对象可

以通过实现技术的任何一个选择来很直接地实现。

在这种情况下,你的主要目的应该是实现你项目中的叠代所用到的域对象类,而且要编

译他们。不用在前期太担心你的持久性域对象; 你一开始可以仅仅让

ejbLoad/Store/remove/find 方法为空即可。

Page 111: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

其次是持久性性

一旦你有了一个编译好的领域层,下一步将是开始实现持久性层。经验告诉我们实现持

久性层是非常耗时的, 尤其是当为保持域对象状态而用到相关的数据库时。要改变数据库模

式,使它与域模型相协调必须要花精力, 而且要把域模型的映射融入数据库模式也是很费力

的。当所有的开发都在进行的同时,你的用户接口开发者可能不象他们在别的情况下那样有

效率。即使他们能实现并且编译UI的类,他们也不能真实地测试UI,除非已经有某种特定持

久性机制可以进行工作。

选择java包结构

你将会在你的 EJB 应用开发过程中面对的另外一个决定是如何区分你的

类,将其归入不同的包中。 你将用到怎样的一组包?如何组织包结构?理想的

情况是,你想把逻辑上相关的类汇集起来放到一个包中,这样不同包之间的类

关联会减到 少。

你的分层体系提示了一个简单易懂的方法:为每层创建一个包,加上一组

独立于层的代码的附加包。这样就有了如下的一组包:

com.mycompany.mysystem.domain

com.mycompany.mysystem.exceptions

com.mycompany.mysystem.persistence

com.mycompany.mysystem.services

com.mycompany.mysystem.util

com.mycompany.mysystem.web (assuming a web-based

application layer)

另一个价值的方法是根据容器——在运行时包要在那儿部署——插入附

加的打包标准。 在这个方法中,我们必须允许有将要在多个容器或层中部署

的代码,因此有了“公共”包。这样就导致了如下的结构:

com.mycompany.mysystem.common.exceptions

com.mycompany.mysystem.common.util

com.mycompany.mysystem.common.datatransferobjects

com.mycompany.mysystem.ejbcontainer.domain

com.mycompany.mysystem.ejbcontainer.persistence

com.mycompany.mysystem.ejbcontainer.services

com.mycompany.mysystem.webcontainer.web

在每层的包中,你根据需要可以选择创建附加子包——如果在一层中你有

许多在概念上相关的类的分组,它们是相互不相同的,这样在一个层里增加一

个附加打包标准就变得有意义了。这样就导致了在整个系统里有了一个“垂直

分层的子系统”,它会分割层次并且从它们处理的域模型中的主要“主题”得到

它们的名字。

第三是服务

Page 112: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

可能令人惊讶的是前面提到的所有工作都是在为实现你的系统的基于会话Beans的服务

层作准备,但那的确是J2EE 开发的一个事实。然而,服务层也许是系统中 重要的层,而且

它的实现也明确象征了在 EJB 开发过程中一个顶点,一个里程碑。除了它定义了系统客户用

到的API(包括用户接口)以及检查了领域层和持久性层的因素以外,它之所以重要的一个

主要原因是它为你的单元测试组形成了一个 佳测试点——关于这一点我们将稍后会在本

章中继续说明。

因此,如果给了一编译过的领域层和一个可工作的持久性层,服务层的实现便能被完成。

因为服务层 “业务方法”通常是从系统需求模型的用例名字中提取出来的(或在用例情节

中的系统反应),所以由服务层“业务方法”——反应创建,读,更新,删除(CRUD)操作和域

对象——是很普通的。即使它看上去很一般,不过在系统中创建、更新和删除业务对象是用

户共同的目标——被转化为用例(Cockburn,2000)。典型地,这些业务方法的实现仅仅是表

示领域层或持久性层的CRUD API.

后是客户端

后, 有了一个真正持久性层支持的单元测试服务层之后,你就已准备好完成客户端层

的开发和整合的工作——包括用户接口 , 外部系统接口,等等。在一致确认,事务控制以及

响应协调等方面,对于整合所有这样的客户端和服务层在一起是有明显的优势的。毕竟,服

务层定义你的应用程序的操作边界。

正如它可能的那样,你的服务层的 重要的客户一般是你的用户接口。实际上,用户接

口需求经常通知一组服务层需要的事务方法,正如系统用例那样。服务层的存在是为了服务

系统的客户,如果主要客户是一个用户接口,那么服务层的API将被用来满足用户接口的需

求。所以当编译依赖因素对开发顺序起主要作用时,用户接口需求一定要结合服务层API设

计过程来考虑。正如服务层API形成了一个重要的测试点,应用程序层的API也是如此。假定

用户接口是基于Web的,用JSP和servlet实现的。就有可能JSP/Servlets用Javabean来获取

要显示的数据(如果不这样,他们应该这么做——先前提到的把系统任务分为五层的方法是

很好的——在行业内部建立一个模式)。事实上,JSP页所需要的数据为JSP页和它们用到的

Javabean之间的协议建立了一个基础,并且那组成的协议的签名方法应该只返回一个简单数

据类型而不是域对象。而且,有可能结构中的servlet将在应用程序层里表示控制类。这些

JavaBeans, servlets和控制类将有它们自己的API,它们将受单元测试组支配,目的是为了

用基于java的测试工具尽可能使还原测试自动化接近“透明(glass)” 。我们将在本章后面,

深入研究在环境中J2EE容器里的应用程序部署以及如何使它自动化以后,再回到单元测试的

讨论中。

自动化环境

用Ant管理

一旦开始为系统编写代码,你将需要一个方法来编译它,为了部署而把它打包,还要把

它部署到你的J2EE环境中的容器里。如何把这做得 好呢?我们的方法是使用Ant

(http://jakarta.apache.org/ant)。Ant是一个来自theApache Jakarta 计划的,跨平台

的,XML驱动的自动生成工具(http://jakarta.apache.org),它对于管理J2EE应用程序环境

是非常有用的。经过上述内容解释这些术语的含义之后,我们将说明如何用Ant来做这些事

情,而且还为管理J2EE应用程序环境提供Ant构建文件目标的模板。

一个J2EE应用程序环境是什么?

一个典型的 J2EE 项目在其生成的产品的生命周期中会用到多种应用程序环境。每个环

Page 113: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

境为一个不同的目的服务,从而使软件开发生命周期中不同活动变得容易。编码和数据有序

地从一个环境提取到接下来的一个环境中。一组典型的 小的J2EE项目环境构成如下:

开发环境 (典型的是每个开发者拥有一个或多个) QA 环境 生产环境

环境定义是很重要,因为它为定义卷入到管理一个环境的事务提供了一个场所。

正如这儿用到的,一个环境引用硬件/OS平台以及构成一个应用程序的运行安装程序的

进程、文件和环境变量的集合。对于具有两个或者多个层次体系的应用来说,在网络中的多

台主机里把这些进程,文件,变量划分开来,即便不是很可能的话,也是可能的。

基于J2EE的应用程序一般反应了J2EE平台定义的一个体系。这个应用的体系在很大程度

上决定了在这样一个应用中的任何环境里会出现的粗粒度的组件(例如:容器,服务器,系

统等等)。图7.2阐明了由JavaSoft's Blueprints小组(Kassem, 2000)建立的J2EE应用程

序体系结构的基本原理示意平台。

图像7.2 J2EE应用环境

管理一个J2EE应用程序环境的意义是什么?

在一个J2EE应用程序的部署过程中对管理各种类型的环境有着频繁的需求,这些环境

是:开发环境,QA环境,生产环境和其他类似的类型。

一个J2EE应用程序环境的管理由多个步骤组成。一般假定的起始点是一台新(裸机)的

机器和一个公共源代码的控制系统(我们假定你正在使用一个源代码控制系统;如果不是,

那么马上去弄一个)。目的是为了得到一个机器上的应用程序的正在运行的配置。步骤如下:

1. 安装环境组件 这个步骤包括安装应用程序和它的开发环境所依赖的第三方产品。在这个

过程中,它可能包括创造目录结构,环境变量等等。

2. 检验应用程序的代码库 这包括与公共源代码控制系统进行业务处理。为了符合项目模块

的规范以及翻译,事务需要参数化。

Page 114: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

3. 编译通过检验的代码库

4. 把应用程序和部署时所需要的代码装入要在其中运行的容器中 例如,这可能包含至少

一个jar文件的创建。

5. 把应用程序部署到容器中 这可能包括为应用程序配置容器。

6. 初始化或更新数据库 这个步骤包括初始化或更新任何持久性存储的东西或其他应用程

序可能依赖的资源。举例来说,这可能包括运行DDL脚本以及加载应用程序可能要求操作的

数据。

7. 开始运行应用程序

8. 测试安装,也许,使用一个单元测试组,也有可能甚至是一个加载测试机构

环境管理需要尽量可靠,可重复以及有效率。没有什么事情比在一些环境中花时间去找

明显的bug更使人垂头丧气,而这样作只是为了找出根本原因是配置不同或者配置充分。经

过脚本化的自动操作是实现这些目标的 好方法,并且除了第一步以外后面的都是可编写

的。但为了得到这样的好处,环境管理者包括开发者必须一致地使用和修改脚本,并且不同

的环境可能要使用稍有不同的配置(部分原因是开发者都倾向于喜欢用自己的方法做事)。

因此, 为了要适应不同的配置 ( 以及开发者的偏爱)当仍然通过使一些特定事物持久不变

来传递自动操作的好处时,自动操作脚本需要支持一定范围的变动,特别是在属性驱动的和

有独立储存单元的地方(考虑到已安装的第三方的产品和从公共源代码控制系统中调试的目

录)。为一个J2EE应用程序设计一个环境管理脚本需要考虑这些有冲突的目标。

使用Ant

Ant构建文件能支持这种程度的弹性变化,并且能说明那些有冲突的目标。诀窍是创建

一些事物使得你的Ant构建文件能使一些特定的事物保持不变——比如为了管理一个环境所

需的必要步骤,在程序中用到的目录结构,被依赖的第三方产品(和版本)的组合,以及编译

和运行你的应用程序所需的类路径,在这同时,通过在环境管理中使用属性文件和环境变量

来缓和配置不同和存储单元无关时,在同时 小化依赖关系和 小化在环境上的Ant构建文

件的期望时,基本上目标是尽可能多的把知识编码成有关涉及环境管理的Ant构建文件。为

了 有效的利用Ant来管理J2EE应用程序环境,必须要留意源代码控制系统和应用程序环境

的初始化安装,并且必须要在头脑中灵活地、一致地组织和实现Ant构建文件。接下来的部

分是关于这些话题的扩充和延伸。

初始化安装

Ant构建文件需要确定一组 小的、源代码控制系统和管理的环境中的主机文件系统上

的目录结构和文件的组织形式的设想。同样地,它也将需要确定一组 小的、环境里已安装

的第三方产品的存在的设想。因此,想要有效使用Ant来管理J2EE应用程序环境,第一步首

先要用特定目录结构来安装源代码控制系统,用特定的已安装的产品和环境变量来安装应用

程序。

源代码控制系统的安装

典型地,源代码控制系统是在一个客户端/服务器端的模式上运作。在服务器端,有一

个包含了代码库中所有文件的全部版本的仓库。这些文件被检出到客户端的文件系统中。在

服务器端的仓库中的文件被组织成生成的目录结构,在检出(check out)代码库到客户端

时将被保存起来。客户端通常拥有一个应用的环境(或者一部分),并且在管理环境的过程

中,Ant构建文件可能需要创建一些附加的目录(用来存放编译好的类文件,打包的jar文件,

第三方库文件,等等)。因此,出现的问题之一是如何在服务器端仓库中和客户端的文件系

Page 115: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

统中组织需要的目录结构。我们的答案见表7.1,我们相信这在行业的 好处理方法中是颇

具代表性的。我们假定在服务器端的存储器中有一个被引用为“project_dir”的 高层的项

目目录,它被编译为客户端文件系统的父目录。在 高层项目目录下面的结构是很重要的。

我们再假定在服务器端存储器中一个第三方目录结构的存在。这个目录结构的目的是为了在

源代码控制系统中组织和存储应用程序所依赖的第三方文件(jar文件和产品许可文件成为

主要例子)。在源代码控制中存储这些文件以及把它们编译到客户端的环境中是 实际有效

的方法,因为这样能减少原来必须指望在环境中存在的构建文件,而且这样也允许构建文件

把那些你管理的这个版本的应用程序所依赖的文件和版本编码到环境中。 另一个有效的途

径是用一个表示软件提供者、产品和第三方文件所说明的版本的三层模式来组织第三方目录

结构。这样做可以使以后不必根据文件大小和修改时间反过来去设计API的版本、从而使得

它和在某些环境存在的jar文件相一致。

表 7.1 项目目录结构

目录名 位置 内容和用途

<project_dir> 服务器端和客户端 高层目录在整个应用程序代码

库组织的下面。Ant构建文件就在

这个层上。

Bin 服务器端和客户端 你的应用程序所依赖的命令解释

器脚本和可执行程序

Build 客户端 一个在环境管理期间产生的目录,

用来保存编译好的类文件和为展

开配置而产生的 java 存档文件。

存档 存放创建的 jar 文件

类 编译目的目录

Conf 服务器端和客户端 应用程序或第三方产品所用到的

任何配置文件,这些文件也依赖于

这些应用程序或第三方产品。

Data 服务器端和客户端 你的应用程序所要求的任何数据

文件,例如,在数据库初始化时用

到的引用数据文件。

Doc 服务器端和客户端 与你的项目联系起来的文档文件,

例如,包层次的 java 文档文件。

Page 116: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

Lib 客户端 保存你的应用程序所依赖的第三

方库文件的一个目录。你的Ant构

建文件创建这个目录并且把正确

的第三方文件和保存在源代码控

制系统里的版本放到这个目录中。

为了编译和运行应用程序,它在类

途径中被用到。

Src 服务器端和客户端 在组织你的应用程序源代码的地

方的下面的 高层目录。

Java 你的 java 源代码树的根目录

Sql 包含任何用来初始化你的应用程

序的数据库的 DDL

Web 包含你的应用程序的所有 Web 内

容,包括 JSPs,静态 HTML,图形,

JavaScript 文档,等等。

Test 服务器端和客户端 包含所有实现测试(用于你的应用

程序)的代码。

Java 包含你的 JUnit 测试组

Third-party 服务器端 用于你的应用程序所依赖的第三

方产品文件的 高层目录结构。

<vendor> 命名第三方产品的提供者。

<product> 命名第三方产品。

<version> 包含与产品版本有关的文件。

标准环境设置

一些应用所依赖的第三部分产品(比如以jar文件发布的小型的APIs和产品)可以储存

在源代码控制系统中,而其他的第三部分产品如数据库管理系统、应用服务器和WEB服务器

则必须安装在所管理的环境中。

为了将应用部署到这些产品中,并调用它们发布的可执行部分,ANT构造文件需要了解

这些产品安装文件系统的位置。尽管存在这些因平台而异的为了安装位置需要遵循的规范

(例如,UNIX中的/USR/LOCAL,和WINDOUS中的/PROGRAM FILES ),如果ANT构造文件是对

预定路径硬编码的话,它将变的很不灵活。首选的传递安装路径给ANT构造文件的方式是由

经环境变量实现。ANT构造文件有权使用由当前运行的SHELL程序所定义的环境变量。因此,

应该为每一个安装过的第三部分产品定义一个环境变量,这些第三部分产品的路径是需要在

ANT构造文件中的(JAVA HOME就是一个例子)。这样一系列环境变量形成了在ANT构造文件

和正在使用的环境之间需要的几乎全部的契约。

ANT构造文件另外唯一需要的一点信息就是将项目目录从源码控制系统检出到用户文件

Page 117: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

系统的位置。所有的ANT构造文件所完成的计算都可以相对此位置进行。这些信息对于ANT

构造文件来说是可以通过其<project>标签的basedir属性访问的,它缺省的是含有构建文件

的目录。因为ANT构造文件是保存在工程目录结构的根层次的,所以检查路径就很一般的可

访问了。

构造文件组织和实施

一个如上所述的初始设置准许ANT构造文件的执行过程中创建大量的简化设想,从而可

以使J2EE应用环境的管理更加可靠、可重复、有效。

一致性也因ANT构造文件的强有力的组织原则得到了强化。

ANT构造文件是由“目标(target)”(在概念上和构造文件目标相似,但由XML定义)

所组成的,目标又分类为:“主要目标”和“子目标”。那么,什么样的目标集在完成J2EE

应用环境管理的ANT构造文件中是必要的呢? 答案,也就是构造原则,是一个主要的目标(可

能有包括支撑它的子目标),它在前面所列举的管理的每一个步骤中都有所涉及。跨越多个

工程的经验表明,这样的一系列步骤应该是相关而且相适应的,不管哪一个产品被选为源程

序控制、应用服务、或者是数据库管理。尽管,每个步骤的执行细节是因产品而异的,但在

这一些步骤上却是一致的。所以,现在让我们来按顺序来考察每一个步骤和它由经ANT的自

动操作。

检验目标

第一个可脚本化的环境管理过程的步骤是将应用的代码库检出到环境中去。当然,这可

以在ANT之外手动完成。但是用ANT自动完成它的一个目的是将第三方的文件的正确版本也自

动检出到客户的”lib”目录下,相比于预期每个文件在环境中都先期存在而言,这可以使得

管理过程更加“自足”(self-sufficient)。下面的代码给了一个检出目标实施的例子,

假设CVS是源代码控制系统,一个应用独立于JAXP。注意检出目标调用了CHECKOUT.LIB目标。

<!-- Checks out the application codebase from the CVS repository. If property tag is not defined, will check out head revisions of the main branch. 1 See the Ant User Manual at http://jakarta.apache.org/ant/manual/index.html. --> <target name=”checkout” description=”Checks out the application codebase”> <property name=”tag” value=””/> <cvs package=”<project_dir>” tag=”${tag}” dest=”${basedir}/..”/> <antcall target=”checkout.lib”/> </target> <!-- Checks out third-party jar files on which the application depends into a local “lib” directory which is then included in classpaths, deployable archive files, etc. --> <target name=”checkout.lib” description=”Checks out third-party jar files”> <delete dir=”${basedir}/lib”/>

Page 118: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

<mkdir dir=”${basedir}/lib”/> <!--- insert here: cvs export tasks for required jar files --> </target>

当然,用构造文件目标来校验代码库会造成一点纠缠不清的(Bootstrapping)问题,

因为构造文件的自身是保存在源代码的控制系统里。但是这个问题可以很容易用WINDOWS中

的命令宏和UNIX中的命令别名:只是设立命令符号或者命令别名唯一检验一些本地目录下的

构造文件,然后转换为发布检出的目录。

编译目标

ANT的JAVAC任务使得编译代码库变的相当容易。它通过下列方法来完成上述的任务:递

归的向下遍历任何给定的目录树,寻找那些有着陈旧的或者是不存在的类文件的JAVA源文

件,然后编译这些源文件到代理的目标目录中。这样,默认的状态下,它仅仅编译在 后编

译的过程中修改过的源文件:为了强制性的重新编译整个代码库,必须清理类文件的目标目

录。既然这两种编译的领域都是我们所期望的,我们在ANT构造文件中用两个目标(编译和

重新编译),就象下面的ANT脚本里所描述的一样。为了方便的清除类文件(还包括其他原

因),我们把类文件保存在和源文件分离开的目录树中。

<path id=”classpath.compilation”> <pathelement location=”${basedir}/build/classes”/> <pathelement location=”${basedir}/lib/jaxp.jar”/> <pathelement location=”${env.WL_HOME}/lib/weblogic.jar”/> </path> <target name=”compile” description=”Compiles only modified source files”> <mkdir dir=”${basedir}/build/classes”/> <javac classpathref=”classpath.compilation” srcdir=”${basedir}/src/java” destdir=”${basedir}/build/classes”/> <antcall target=”compile.resources”/> </target> <!-- Forces recompilation of the entire codebase by deleting the classes directory before invoking compile. --> <target name=”recompile” description=”Recompiles the entire codebase”> <delete dir=”${basedir}/build/classes”/> <antcall target=”compile”/> </target> <!-- “Compiles” (in the collection sense) any additional resources (besides java class files, i.e., product license files, application properties files, etc.) upon which the application depends, that need to be collocated with class files due to being accessed through class loaders. --> <target name=”compile.resources”>

Page 119: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

<copy todir=”${basedir}/build/classes” includes=”${basedir}/src/java/**/META-INF/**”/> </target>

注意到编译目标引用了一个〈path〉元素作为它的类途径。这就是运用ANT的很多好处

中的一个——在构造文件中定义的类途径作为依赖于代码库的精确的规范。还注意到编译资

源目标的调用,它们被用于复制那些运行的时候会被加载的类途径相关的资源到类的根部

目。

包目标

有很多把用于将J2EE应用打包部署到容器中的方法。在这个范围的一端,可以用单一的

企业存档(EAR)文件,包括单一的WEB存档(WAR)文件,和单一的包含EJB和支持编码的EJB

(JAR)文件。而在范围的另外一端,则可以部署一些庞杂的目标,在这样的情况下你的“打

包”步骤会变的琐碎。很可能对于不同的环境会运用一些不同的方法。取决于用的是什么样

的EJB容器,你可能不得不把合成的EJB存根和框架包括在你的存档文件中,也可能不必要。

这些变动性的来源使得很难提供一个用于打包的全局模板的ANT构造文件,但是还是可以用

下面的ANT脚本给出了一个把WAR文件的部署打包到Tomcat,和一个EAR文件(包括多个EJB

jar 文件和支持的代码)打包到WebLogic的例子。

<target name=”package” description=”Packages the application for deployment”> <delete dir=”${basedir}/build/archives”/> <antcall target=”package.ejb.jars”/> <antcall target=”package.ejb.ear”/> <antcall target=”package.web.war”/> </target> <target name=”package.ejb.jars”> <antcall target=”packageExampleEJB”/> <!-- insert calls for other EJBs here --> <antcall target=”generate.ejb.ear.DD”/> </target> <target name=”packageExampleEJB”> <antcall target=”package.ejb”> <param name=”package.ejb.ejbname” value=”Example”/> <param name=”package.ejb.directory” value=”com/mycompany/mysystem/services/example/”/> <param name=”package.ejb.implclass” value=”com/mycompany/mysystem/services/example/ExampleBean.class”/> <param name=”package.ejb.remoteIFclass” value=”com/mycompany/mysystem/services/example/Example.class”/> <param name=”package.ejb.homeIFclass” value=”com/mycompany/mysystem/services/example/ExampleHome.class”/> </antcall> </target> <target name=”package.ejb”> <mkdir dir=”${basedir}/build/archives/temp”/>

Page 120: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

<jar jarfile=”${basedir}/build/archives/temp/temp.jar” basedir=”${basedir}/build/classes” includes=”${package.ejb.implclass}, ${package.ejb.remoteIFclass}, ${package.ejb.homeIFclass}, ${package.ejb.directory}/META-INF/**” /> <java classpathref=”classpath.compilation” classname=”weblogic.ejbc” fork=”yes”> <arg line=”${basedir}/build/archives/temp/temp.jar ${basedir}/build/archives/${package.ejb.ejbname}.jar”/> </java> <delete dir=”${basedir}/build/archives/temp”/> </target> <target name=”generate.ejb.ear.DD”> <java classpath=”${basedir}/lib/weblogic.jar” classname=”weblogic.ant.taskdefs.ear.DDInit” fork=”yes”> <arg line=”${basedir}/build/archives”/> </java> </target> <target name=”package.ejb.ear” if=”isNonDevelopmentEnvironment”> <jar jarfile=”${basedir}/build/archives/myapp.ear” basedir=”${basedir}/build/archives” includes=”**” excludes=”myapp.ear, myapp.war” /> </target> <target name=”package.web.war” if=”isNonDevelopmentEnvironment”> <mkdir dir=”${basedir}/build/archives/temp/WEB-INF/classes”/> <mkdir dir=”${basedir}/build/archives/temp/WEB-INF/lib”/> <copy todir=”${basedir}/build/archives/temp/WEB-INF”> <fileset dir=”${basedir}/src/web” excludes=”**/CVS/**”/> </copy> <copy todir=”${basedir}/build/archives/temp/WEB-INF/classes”> <fileset dir=”${basedir}/build/classes”/> </copy> <copy todir=”${basedir}/build/archives/temp/WEB-INF/lib”> <fileset dir=”${basedir}/build/archives” includes=”*.jar” excludes=”*.ear”/> </copy> <java classpath=”${env.WL_HOME}/lib/weblogic.jar” classname=”weblogic.ant.taskdefs.war.DDInit” fork=”yes”>

Page 121: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

<arg line=”${basedir}/build/archives/temp”/> </java> <jar jarfile=”${basedir}/build/archives/myapp.war” basedir=”${basedir}/build/archives/temp” /> <delete dir=”${basedir}/build/archives/temp”/> </target>

部署目标

"部署" 的意义因容器而异。对于一些 EJB容器,部署主要是把文件复制到这个容器所

知道的目录里;对于其他容器,它主要使用供应商提供的部署器具或接口。而且,部署方法

可以不依赖管理的环境的类型。对于一个开发环境来说,你希望把你的应用程序部署成一个

有创意的形式,避免复制文件(从你的本地项目目录结构到容器的目录结构)。你可以通过

配置文件和虚拟机类途径把你的应用程序告知本地容器。之所以用这种方法来部署你的开发

环境是因为它很有效:当你在一个“开发循环”里,重复编写和测试代码,你会为了测试而不

时地需要重新部署你编写的代码到容器中。而且每次你做这些,你必须要停下来重新启动容

器来加载已编写的代码。看来进行这项工作的 有效的方法是象上面描述的那样部署你的代

码,在你的项目目录结构里编写源文件,如果有必要就编译,并且要重新启动容器。然而,

对于一个非开发的环境来说,效率不是重点。取而代之的是,知道部署在环境中的代码的准

确结构以及保护结构防止被修改显得更加重要。这些我们所关心的问题暗示:你应该从文档

文件所在的位置复制它们,然后把它们部署到非开发环境中。理想情况下,你的Ant构建文

件的的部署目标应该对当前正在执行的环境类型很敏感,并且为当前的环境类型做出正确的

响应。再次强调,提供一个通用的部署模板是很难的,因为各个容器之间有差异;但是下面

的Ant脚本给出了一个部署目标——假定Tomcat为Web容器,WebLogic 6.1为EJB容器:

<target name=”deploy” description=”Deploys the application to this host’s servers”> <antcall target=”deploy.ejbcontainer”/> <antcall target=”deploy.webcontainer”/> <antcall target=”deploy.webserver”/> </target> <target name=”deploy.ejbcontainer”> <antcall target=”create.weblogic.domain”/> <antcall target=”deploy.ejbcontainer.earfile”/> <antcall target=”touchRedeployFile”/> </target> <target name=”create.weblogic.domain”> <mkdir dir= “${env.WL_HOME}/config/${weblogic.admin.domain}/applications”/> <copy overwrite=”yes” filtering=”yes” file=”${basedir}/conf/WebLogic/config.xml” todir=”${env.WL_HOME}/config/${weblogic.admin.domain}” />

Page 122: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

<copy overwrite=”yes” file=”${basedir}/conf/WebLogic/fileRealm.properties” todir=”${env.WL_HOME}/config/${weblogic.admin.domain}” /> <copy overwrite=”yes” file=”${basedir}/conf/WebLogic/SerializedSystemIni.dat” \ todir=”${env.WL_HOME}/config/${weblogic.admin.domain}”/> </target> <target name=”deploy.ejbcontainer.earfile” if=”isNonDevelopmentEnvironment”> <copy todir= “${env.WL_HOME}/config/${weblogic.admin.domain}/applications”> <fileset dir=”${basedir}/build/archives” includes=”myapp.ear”/> </copy> </target> <target name=”deploy.webcontainer”> <copy filtering=”yes” file=”${basedir}/conf/Tomcat/tomcat.conf” todir=”${env.TOMCAT_HOME}/conf” /> <copy filtering=”yes” file=”${basedir}/conf/Tomcat/server.xml” todir=”${env.TOMCAT_HOME}/conf” /> <copy file=”${basedir}/conf/Tomcat/web.xml” todir=”${env.TOMCAT_HOME}/conf” /> <antcall target=”deploy.webcontainer.war”/> </target> <target name=”deploy.webcontainer.war” if=”isNonDevelopmentEnvironment”> <copy todir=”${env.TOMCAT_HOME}/webapps”> <fileset dir=”${basedir}/build/archives” includes=”myapp.war” /> </copy> </target>

开始/结束 目标

运用ANT构造文件目标来启动和停止环境中的服务器提供了另一个ANT将管理应用环境

所需的知识集中和编码的例子。容器的购买商一般会鼓励我们去借用他们提供的启动脚本,

Page 123: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

去添加我们自己的类途径等等。另一个更加易于管理的方法是把启动和停止目标包括在我们

的ANT构造文件中,重视提供商提供的启动/停止类和可执行类。通过这样的方法目标的执行

可以变得对相关上下文敏感,例如主机或者环境的类型,还可以对任何已知的包含在传递变

量给购买商提供类和可执行类的过程中的构造文件起到平衡作用。特别的,作为应用依赖性

的规范的类途径,可以对基于环境变量的储存单元的独立性和工程目录的lib子目录起到平

衡作用。下面的脚本说明了对于Apache, Tomcat, 和WebLogic的跨越WINDOWS和UINX平台的

启动/停止目标的执行。

<target name=”start.apache” description=”Starts the Apache Server in this environment”> <antcall target=”start.apache.unix”/> <antcall target=”start.apache.windows”/> </target> <target name=”start.apache.unix” if=”isUnix”> <exec executable=”${env.APACHE_HOME}/bin/httpd” dir=”${env.APACHE_HOME}/..”/> </target> <target name=”start.apache.windows” if=”isWindows”> <exec executable=”${env.APACHE_HOME}/apache.exe”/> </target> <target name=”stop.all” description=”Stops all servers in this environment”> <antcall target=”stop.apache”/> <antcall target=”stop.tomcat”/> <antcall target=”stop.weblogic”/> </target> <target name=”stop.apache” description=”Stops the Apache Server in this environment”> <antcall target=”stop.apache.unix”/> <antcall target=”stop.apache.windows”/> </target> <target name=”stop.apache.unix” if=”isUnix”> <echo file=”tempKill” message=”kill “/> <exec vmlauncher=”false” executable=”cat tempKill ${env.APACHE_HOME}/logs/httpd.pid” output=”tempKillApache”/> <chmod file=”tempKillApache” perm=”+x”/> <exec vmlauncher=”false” executable=”tempKillApache”/> <delete file=”tempKillApache”/> <delete file=”tempKill”/> </target> <target name=”stop.apache.windows” if=”isWindows”> <exec executable=”${env.APACHE_HOME}/apache.exe”> <arg line=”-k shutdown”/> </exec> </target>

Page 124: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

<target name=”start.tomcat” description=”Starts Tomcat in this environment”> <java classname=”org.apache.tomcat.startup.Tomcat” fork=”yes” dir=”${env.TOMCAT_HOME}”> <classpath> <pathelement location=”${basedir}/build”/> <pathelement location=”${basedir}/build/applications/myEJB.jar”/> <pathelement location=”${env.TOMCAT_HOME}/lib/webserver.jar”/> <pathelement location=”${env.TOMCAT_HOME}/lib/jasper.jar”/> <pathelement location=”${env.TOMCAT_HOME}/lib/xml.jar”/> <pathelement location=”${env.TOMCAT_HOME}/lib/servlet.jar”/> <pathelement location=”${env.TOMCAT_HOME}/classes”/> <pathelement location=”${env.JAVA_HOME}/lib/tools.jar”/> <pathelement location=”${env.WL_HOME}/lib/weblogic.jar”/> </classpath> <sysproperty key=”tomcat.home” value=”${env.TOMCAT_HOME}”/> </java> </target> <target name=”stop.tomcat” description=”Stops Tomcat in this environment”> <java classname=”org.apache.tomcat.startup.Tomcat” fork=”yes” dir=”${env.TOMCAT_HOME}”> <classpath> <pathelement location=”${env.TOMCAT_HOME}/lib/webserver.jar”/> <pathelement location=”${env.TOMCAT_HOME}/lib/jasper.jar”/> <pathelement location=”${env.TOMCAT_HOME}/lib/xml.jar”/> <pathelement location=”${env.TOMCAT_HOME}/lib/servlet.jar”/> <pathelement location=”${env.TOMCAT_HOME}/classes”/> <pathelement location=”${env.JAVA_HOME}/lib/tools.jar”/> </classpath> <sysproperty key=”tomcat.home” value=”${env.TOMCAT_HOME}”/> <arg line=”-stop”/> </java> </target> <target name=”start.weblogic” description=”Starts the WebLogic Server in this environment”>

Page 125: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

<java classname=”weblogic.Server” fork=”yes” dir=”${env.WL_HOME}”> <classpath> <pathelement location=”${basedir}/build/classes”/> <pathelement location=”${basedir}/lib/toplinkall.jar”/> <pathelement location=”${env.WL_HOME}/lib/weblogic.jar”/> </classpath> <sysproperty key=”bea.home” value=”${env.WL_HOME}/..”/> <sysproperty key=”weblogic.Domain” value=”${weblogic.admin.domain}”/> <sysproperty key=”weblogic.Name” value=”${weblogic.admin.server.name}”/> <sysproperty key=”weblogic.management.password” value=”${weblogic.admin.password}”/> </java> </target> <target name=”stop.weblogic” description=”Stops the WebLogic Server in this environment”> <java classpath=”${env.WL_HOME}/lib/weblogic.jar” classname=”weblogic.Admin”> <arg line=”-url localhost:${weblogic.admin.server.port} SHUTDOWN -username ${weblogic.admin.username} -password ${weblogic.admin.password}” /> </java> </target> 初始化数据库目标

当重新管理(readministrator)一个J2EE应用环境时,一般需要进行对环境里的数据

库重新初始化,或者可能是更新。或许你的应用是需要特定的“引用”参数数据来正确的运

行;或许你的开发过程会从在测试调试过程中总是可利用的特定例子数据,还有新的开发目

的中获益;或许你开发和测试对于引入的产品数据库的复制的新的功能性,并且在执行计划

中应用转移脚本和数据转移。所有的这些特定情形都需要一定环境里的数据库在应用可以运

行之前适当的做出初始化。设想一个RDBMS,它的数据库初始化概念性的由两个步骤构成。

对于迁移(migration)场景而言,这些步骤被引入到纪录的数据库中,运行迁移脚本。对

于非转移(升级)情节而言,这些步骤则是加载模式,加载引用/例子数据。对于两个情节

而言,这些步骤就可以被ANT构造文件目标来执行。下面的ANT脚本说明了假定ORACLE作为使

用的RDBMS的执行过程。:

<target name=”load.database” description=”Loads schema and generated data into DB”> <antcall target=”load.schema”/> <antcall target=”load.data”/> </target>

Page 126: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

<target name=”load.schema”> <exec dir=”${basedir}/src/sql” executable=”${env.ORA81_HOME}/bin/sqlplus”> <arg line=”-s ${database.username}/${database.password}@${database.name} @createtables.sql”/> </exec> </target> <target name=”load.data”> <java classname=”com.mycompany.mysystem.persistence.DataGenerator” fork=”yes”> <classpath> <pathelement location=”${basedir}/build”/> <pathelement location=”${basedir}/lib/toplinkall.jar”/> <pathelement location=”${basedir}/lib/weblogic.jar”/> </classpath> <arg line=”jdbc:weblogic:oracle:${database.name} ${database.username} ${database.password} weblogic.jdbc.oci.Driver”/> </java> </target> <target name=”export.database” description=”Exports the database to a dump file”> <exec dir=”.” executable=”${env.ORA81_HOME}/bin/exp”> <arg line=”${database.username}/${database.password}@${database.name} file=exp.${database.username}.${app.version}.${date}.dmp owner=${database.username} consistent=y buffer=102400 compress=n”/> </exec> </target> <target name=”import.database” if=”import.file” description=”Import DB dump (set import.file, import.fromuser)”> <property name=”import.fromuser” value=”${database.username}”/> <exec failonerror=”true” dir=”${basedir}/src/sql” executable=”${env.ORA81_HOME}/bin/sqlplus”> <arg line=”-s ${database.username}/${database.password}@${database.name} @droptables.sql”/> </exec> <exec executable=”${env.ORA81_HOME}/bin/imp”> <arg line=”${database.username}/ ${database.password}@${database.name} file=${import.file} fromuser=${import.fromuser}

Page 127: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

touser=${database.username} analyze=no”/> </exec> </target> <target name=”update.database” description=”Updates DB using appropriate update scripts”> <exec dir=”${basedir}/src/sql” executable=”${env.ORA81_HOME}/bin/sqlplus”> <arg line=”-s ${database.username}/${database.password}@${database.name} @migrate.sql”/> </exec> </target> 测试目标

后但并非 不重要的是,为了运行的的单元测试而在你的Ant构建文件中实现一个目

标是很方便的。这给了你一个确保为运行你的测试包而需要做的提前工作(例如开启服务器)

的机会。你的目标也同样要再次把一个要求的类途径编码——在这里,类途径是运行你的

高层的测试包。我们将会在接下来的部分更加详细地讨论单元测试;同时,接下来的例子给

出了一个针对于运行单元测试包的Ant构建文件目标模板,假定JUnit是单元测试的框架:

<path id=”classpath.compilation.test”> <pathelement location=”${basedir}/build/classes”/> <pathelement location=”${basedir}/lib/junit.jar”/> <pathelement location=”${basedir}/lib/toplinkall.jar”/> <pathelement location=”${env.WL_HOME}/lib/weblogic.jar”/> </path> <path id=”classpath.runtime.test”> <pathelement location=”${basedir}/build/classes”/> <pathelement location=”${basedir}/lib/junit.jar”/> <pathelement location=”${basedir}/lib/toplinkall.jar”/> <pathelement location=”${env.WL_HOME}/lib/weblogic.jar”/> </path> <target name=”compile.test”> <javac srcdir=”test/java” destdir=”build/classes” classpathref=”classpath.compilation.test” /> </target> <target name=”test” depends=”compile.test” description=”Runs the project’s test suite”> <java classname=”junit.textui.TestRunner” classpathref=”classpath.runtime.test”> <arg value=”com.mycompany.mysystem.MySystemTestSuite”/> </java>

Page 128: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

</target> 正如你所看到的,测试目标依赖测试包的编译,并且我们已经为了编译和运行测试包而

引进了类路径。细心的读者可能也注意到我们把测试包类编译到的目录和它们测试的代码的

所在目录是同一个。这也提出了一个问题:该在你的项目目录和你的java包组件的什么地方

放置测试包组件。这两点属于我们将在接下来的部分说到的关于使用JUnit的几个要点。

用Junit对Unit测试

Unit测试――这是我们所知的必须做的事之一(像进行JavaDoc一样),不过它往往在

节省开支之时被遗弃。遗憾的是这种仓促使我们容易招致错误的发生,缺乏一套unit测试是

一个工程和组织常遇到的“技术债”(”technical debt”,Brown,2000)的活标本。

我们都知道,开发,维护和不断行一套不错的unit测试包需要可观的时间和金钱。但是

看待这笔开销的适当方法就是把它当作一笔投资――为现在和将来你的开发机构。如果这么

做了,这笔投资会通过加强开发者,管理者和用户对于你的应用程序的codebase的正确性的

信心来不断给你带来利润。

能够展示一套服务层测试包的成功执行是新项目的重要的里程碑,因为这也确认了领域

和持久性层有效。更进一步,你每次把新的功能融入你的codebase时你的那套unit测试包就

会告诉你是否破坏了先前的运行功能。然后,任何时候你要融合那些被分开以支持并行开发

(象产品错误修正相对新特征,或next-release功能相对futurerelease功能)的代码行时,

你的unit测试包会告诉你是否正确融合。你将从看到你的unit测试包在这些环境下成功运行

获得的信心就是你投资于unit测试的回报。

与你的投资相关的风险是你将付出比为实现回报所需要的多得多,你将会花费太多的时

间和金钱用于开发,维护和运转unit测试系统。为了使风险 小化,你需要决定多少测试代

码是否已经充分和谁来写代码。你需要组织测试包的策略以使它容易理解和维护,以及在选

定粒度的间隔下快速运行。因为后面的原因你还需要在测试前有效地设置初始条件,和后面

用于清除的方法,特别是持久性存储相关之处。你需要知道何时运行测试,以及在什么间隔。

而且你需要知道在测试失败和发现错误时应作出什么处理,使得你的测试包(和codebase)

维持在很好的维护状态中。

我们将处理以下的所有问题。在继续之前,我们假定你选择JUnit(www.junit.org)作为

你单元测试的框架,为此我们将永远感谢Kent Beck(JUnit是一个1998Java端口,是Kent的

初的Smalltalk单元测试框架的,在1995年初次出现,Kent Beck和Erich Gamma编写)。我

们进一步假定你在开发一个分层结构的J2EE系统,就像本章前面所描述的。以下的部分依次

解决每个问题。

要测试多少?

在你构思单元测试时,大概你想到的第一个问题是你要写多少单元测试代码。多少是足

够的?我们甚至定义过“足够”吗?这套代码是要测试“任何能破坏的东西”(Jeffries, et

al., 2000)。不过按照它的字面意义会导致不容许的一大组单元测试。假定我们只相信 简

单的getter和setter方法不会破坏,我们不得不为codebase中的每个其他方法至少写一个单

元测试,而且为了使输入变化以覆盖所有条件路径和边界条件。这样很可能每个方法超过一

个单元测试,结果将会是一套数倍于codebase方法数的单元测试方法数。因而,我们要问,

逐渐缩小的回报的重点在哪里?

按照我们的经验, 有价值的测试是在重要的层边界的测试分界。例如,形成你的应用

程序的行为边界的服务层,是你的体系结构中 有价值的测试之一。原因如下:

Page 129: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

服务层界面被你的系统的许多客户端所依赖 用户界面、数据库装载器和它们之

中的外部系统界面。因此每个外露的服务层方式函数正确越过整个输入,条件路径和正常以

及例外环境范围是非常重要的。

服务层使用领域层和持久性层 因为服务层通过委托领域层和持久性层来实现它的

职责,对服务层的测试将间接测试它所依赖的较低的层。这个重要的任务使得服务层成为了

一个有重要意义的测试点。

超越了服务层的范围,接下来 重要的层次的边界是应用层了,它控制着用户和系统的

交互。应用层由servlet和它们用到的控制类(例如JSP通过<usebean>标签使用的javabean)

组成。在这层上的测试也起了很重要的杠杆作用,因为这层需要比服务层更多的在它下面的

层的支持。而且,在这层维持一个单元测试使得你更能反过来在JUnit中测试你的用户接口,

而不是使用一个UI测试工具。虽然在这层上的测试比在服务层上更集中但也变得不通用,但

它们仍然属于 有价值的测试。

持久性层边界,尽管要求更严格的正确性,比高层的边界提供了更少的平衡作用。假定

当将不同的CRUD的操作乘以对象间关系(关联对应聚集),乘以存在的场景(所有的对象都

预先存在,没有预先存在的对象,有些预先存在但有些不存在),乘以其他相关因子,持久

性必须正确地支持大约104种场景。拥有一套可以在每个场景展示持久性层的正确行为的测

试包极有价值。另一方面,这类测试包的开发代表着一笔重要的投资, 终需要正确运行的

场景是那些被服务层API用到(并被它的测试包测试的)的场景。对持久性层进行单元测试

的 佳途径可能是在极端之间的一种折中,随着时间越来越多的覆盖所有场景。

领域层不具有可写测试包的明显的边界。它由许多的小的对象组成,这些对象可能并没

有多少复杂的方法。因此,相对其他层的测试包的开发而言,在本层开发一套耗时的测试包

的需要并不急迫。但是,理想情况下,如果你需要完全的覆盖,就 好尽可能早地开始。

对框架代码进行测试将会怎样?如果应用依赖于自己独创地框架代码将会怎样?有些

情况下,就像配置框架、登录框架和其他措施框架一样,如何测试框架代码是很清除并且适

于去做的。然而另外一些情况下,测试框架代码是困难的,除非用应用的代码作为子类来代

替,并测试应用的代码。依照我们的经验,这样也是可以的。如果应用一如既往地按框架地

意图采用框架,并且应用通过了单元测试,得出框架正确支持应用地结论是合乎情理的。在

实践允许的范围内尽可能多的进行测试。

这样,一般的,我们建议将测试的精力集中于层的边界处,强调服务层和应用层,这样

可以得到 高的价值和均衡。但如果可以做到的话,不要忽视任何一层(或者独立于层的代

码和框架)。

体系的分层和层与层之间的依赖形成了组织单元测试包(Unit TestSuites)的基础。

而且,你希望在不同的粒度水平上运行单元测试:或许对单独的一个方法或者类,或许是整

个层,或者 后是整个应用。JUnit通过组合模式支持这种粒度的聚集(Gamma, Helm, Johnson,

and Vlissides,1995): 测试包可以回溯性地包含测试用例(TestCases),或者其他测试包,

直到测试用例的叶结点为止。

于是单元测试包的组织原则就是按体系的分层加以组织,在给定层内按类组织。实施本

原则的结构就是一个测试包的集成。同时,还需要决定将测试包和测试用例放入那一个包以

及放入项目目录结构(在源码控制系统中)的位置。

这些部分将在下面进行详细说明。

测试包的集成

Page 130: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

顶端的测试包的粒度自然应该是整个应用。它应由层相关的测试包合成,(除了表示层

外)每层一个。或许你想包含独立于层的代码的测试包。在给定的测试包中,你可能在类的

基础上引用一些测试包。

在对于一个包容性的测试包中的测试包进行排序时,记住两件事。第一,JUnit的一个

根本的原则是每个测试应该独立于其他测试,因此不能按某类可以造成一项测试依赖其 状

态的顺序构造。其次,在高水平粒度上,假定低层的错误可能在高层引起错误,你可能希望

从体系低层到高层进行排序。这可以使得察看高层测试包的输出时,你可以在多个错误中首

先检查根错误。下面代码块中的测试包类的模板反映了

这类测试包集成的方法。

package com.mycompany.mysystem;

import com.mycompany.mysystem.domain.DomainLayerTestSuite;

import

com.mycompany.mysystem.persistence.PersistenceLayerTestSuite;

import com.mycompany.mysystem.services.ServiceLayerTestSuite;

import com.mycompany.mysystem.web.ApplicationLayerTestSuite;

import junit.framework.Test;

import junit.framework.TestSuite;

/**

* The MySystemTestSuite is a TestSuite that is specialized for

* testing the behavior of My System.

*/

public class MySystemTestSuite

extends TestSuite {

//PROTOCOL: SUITE

/**

* Returns a suite of tests for My System.

*

* @return a suite of tests for My System

*/

public static Test suite()

{

MySystemTestSuite suite = new MySystemTestSuite();

suite.addTest(PersistenceLayerTestSuite.suite());

suite.addTest(DomainLayerTestSuite.suite());

suite.addTest(ServiceLayerTestSuite.suite());

suite.addTest(ApplicationLayerTestSuite.suite());

return suite;

}

}

package com.mycompany.mysystem.services;

import junit.framework.Test;

import junit.framework.TestSuite;

Page 131: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

/** The ServiceLayerTestSuite is a TestSuite that is specialized for

* testing the behavior of the services layer of My System.

*/

public class ServiceLayerTestSuite

extends TestSuite

{

//PROTOCOL: SUITE

/**

* Returns a suite of tests for the Back Office System’s

* services layer.

*

* @return a suite of tests for the Back Office System’s

* services layer

*/

public static Test suite()

{

ServiceLayerTestSuite suite = new ServiceLayerTestSuite();

suite.addTest(MyEJBTestCase.suite());

return suite;

}

}

打包和位置的考虑

细心的读者将注意到上面代码块中得测试包也反映出了我们测试放置的Java包的趣向。

但是从列表来看我们在项目路径中放置测试类的位置并不清楚。这两个问题在公共Java相关

的论坛中小有争议。

逻辑上看,应该将测试类放入那些同含有被测试代码的包相关的Java包。一种思想是让

你生成目标包的“测试”子包,将测试类放入测试子包中。然而,这只会使你不得不为测试

类的源文件加入大量的import语句,因此我们不喜欢它(我们是懒惰的)。我们更推崇测试

类直接放入含有目标类的包中,消除了额外的import语句的需要。

但是,请等一等,你说。现在,如何将测试代码和目标代码分离开来,尤其是出于部署

和发布的目的。这很容易。我们只需要简单地将测试代码放于源码控制系统的顶层项目路径

下的一个完全独立的目录下。因此,测试代码就可以不必打包进档案文件、和应用代码一起

部署。部署的时候,若码,当然可以如你想的去做。

命名测试用例的子类和测试方法

作为围绕测试包的组织的 后一个细节,我们鼓励你去认真思考如何命名你的测试用

例的子类和它们所含的测试方法。你的基本的启发要素是可以向任何后来要阅读的代码的

人清晰地传递你的想法。

JUnit用户的惯例,也是我们遵循的,是为每一个待测试的生成一个测试用例子类,

在目标类的名字后面添加“Test”或者“TestCase”,将TestCase子类命名。在这两者中,

我们推荐“TestCase”,因为它是父类的名字。

命名测试方法时,JUnit框架要求测试方法名以“test”前缀开头,接下来你可以自由

追加内容组成测试方法的名字。惯例是添加测试方法待测试的目标方法的名字。但是对同一

Page 132: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

个目标方法你常常需要有多个测试方法,来覆盖可能的输入范围,条件路径,正常和例外的

环境等等。我们建议带有意愿的后缀。例如,若你在测试一个名为“createCustomers”的

方法,你可以以“testCreateCustomersWithEmptyCollection”来命名测试方法。无论你怎

么做,不要仅仅在测试方法名后添加一个数字,这不利于传达你的意图。

测试用例的?

使用JUnit测试企业应用的的一个挑战就是设置测试某些功能所需的初始条件。通常企

业应用实施复杂的业务逻辑操纵复杂的域模型。业务逻辑实现工作流中的内在的责任,或者

应用支持的整个的业务过程。结果,某些部分的业务逻辑甚至可能并不相关,除非特定的工

作流进展到了某个阶段,在持久性存储中存在着某种业务对象的聚集体系,或者某些业务对

象进入了某种状态,等等。测试这部分的业务逻辑时必须确保持久性存储以适当的初始条件

设置。

测试用例应该以什么样的程度来假定持久性存储呢?Junit下的一个基本的原则是测试

应该彼此相互独立--一个测试用例应该不依赖于其他测试用例的结果,该用例被假定为在

某种序列中先期运行。但是另一方面,每个用例都在其设置函数和测试方法中从头开始设置

复杂的数据库状态,这种时间消耗将使得这样做行不通。于是,在测试序列中应该何时创建

表和装入代表初始状态的数据?我们的经验表明,一个合乎情理的折中是书写测试用例时假

定一个标准的数据集已经装入(和或许是一个标准的引用数据需要仔细设计数据集,使它包

含处于全部正确状态的对象、所有所需的聚集体系、关联等等,以支持应用的所有功能(和

它的测试)。当然,数据集及其所填充的模式在项目中会随着时间发生变动,因此应该随着

应用的扩充和变动对其进行维护和扩充。

有两种创建和维护测试集的一般性的方法。你可以书写程序来生成它或者通过应用的用

户界面输入数据到一个数据库中,就像你维护任何别的数据库一样(比如,产品数据库,培

训数据库和演示数据库)。两种方法各有优劣。但是在后一方法中你在承担维护和迁移一个

产品数据库以前有可能在数据库迁移中做一些重复性的工作。

测试方法的执行可能会造成含有测试数据集的数据库发生事务性的改变。为了和Junit

的理念保持一致,测试方法在完成时要进行清理,确保数据集恢复原貌。对应每一个测试的

目标类,给定的创建测试用例的方法,这意味着清理逻辑须从测试方法自身进行调用。一个

测试用例类的tearDown()方法不需要知道运行的是哪一个测试方法和测试的目标方法,也不

需要知道需要清理的内容。

谁应该编写这些测试?

编写那些待测试的代码的开发人员应该为代码编写测试。没有人比其对开发者在编写代

码的意图和测试的目的有着更好的了解。没有人比其对代码的内部机制和输入范围,条件路

径,正常,例外的情景有着更好的理解。

一些组织建议安排中级程序员为他人编写的代码书写单元测试。我们觉得这有违模式。

这种单元测试的结果很可能是不完备的甚至是错误的,因此,目标代码可能得不到充分的测

试。 后,单元测试的开发者不得不询问目标代码的开发者的开发意图,这会导致生产率的

下降。

开发者有义务书写它们自己的单元测试。这是成为专业的软件开发者的一般的要求之

Page 133: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

一。

谁进行测试?

或许一个更好的提问应当是“我们应当以何种频度来进行某种粒度的测试”。我们知道,

单元测试可以在不同的粒度水平上运行,从整个应用到体系的某一层,到层中的某个类甚至

到该类的某个方法。你有义务为体系中的某个给定的层或者从所有层中切割下来的一个垂直

的子系统进行测试,这取决于团队的组织和分工。随着开发的进展,你可能频繁进行方法或

者类粒度层次的单元测试。因为你需要为所在层添加新的方法或类,或者为子系统添加新功

能,你得在编码和编译后马上进行测试。每次你完成一个方法或类和开发任务得时候,你也

应该追加测试用例,对所完成得代码进行测试。偶尔,你也想运行整个层得测试用例或者,

选定得应用层或者服务层的测试用例的子类,它们可以测试一个特定的子系统。 后,成功

登入后,你或许想从删除本地地项目目录并从源码控制系统获取一个新的“检出”开始,完

整地重新管理开发环境。当然,重新管理环境的 后一步就是运行整个应用的单元测试用例。

这同样对非开发环境有效。每当重新管理环境时,均应该在新环境中运行整个应用的测试用

例。这样,当代码从开发上升到QA,Demo或者产品环境时,单元测试用例也将在相应环境中

运行。

测试失败时怎么办?

我们得进行某种修正。测试失败的原因仅有两个:或者是测试本身有问题,或者是被测

试代码有问题。两种情形均应引起你的注意。如果问题出在被测试的代码上,单元测试就起

到了它的作用,给你的投资带来部分的回报(另一部分它是告诉你一切正常)。

如果错误出在测试自身,修正该测试。随着应用的不断进展,测试用例易于失去维护。

不要仅仅是夸大它,--保持测试用例的更新是每项开发任务的一部分。一旦开了忽视坏的

测试的先例,测试用例的效用将开始消失。

发现了 Bug 怎么办?

在应用中发现先前未能察知(包括运行测试用例)的Bugs是不可避免的。或许这个Bug

并不在你的应用中,而是存在于你使用的某个提供商的第三方产品中。

两种情况下,你均应该做的第一件事就是创建一个可以演示该Bug的测试用例。否则你

怎能下结论你已经修正了它。只有当新创建的单元测试成功后,才修正了Bug,尽管原来它

们失败了。如果发现的Bug来自第三方产品,将展示该Bug的单元测试发送给提供商。如果他

们有些商业意识和专业精神的话,他们将对你帮助他们QA他们的产品非常感谢。

总结

本章我们覆盖了多个方面。希望你能对将概念设计转化为运行的,可行的J2EE的应用所

涉及的事务有所了解。你知道了开发代码的顺序,懂得了如何使用Ant管理应用使用的环境,

通晓了如何用JUnit对代码进行单元测试。我们打算用一个对软件开发过程的类比结束本章:

如同程序中的嵌套循环。

我们看到了三层嵌入。在 内层的循环中,每个开发人员在某个开发环境中不断地编写

和测试代码,或许进行领域外的新特征的开发。这可能不断执行某些环境管理的步骤。每次

Page 134: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

都是这么的频繁的进行,而后开发者完成一项任务并进入另一层嵌套的循环,并在其中他将

和源码控制系统打交道并重新进入内层循环。在和源码控制系统打交道时,开发者将运行单

元测试并重新管理开发环境。 后,在 外层的循环中,整个项目循环地进行一系列的迭代。

在每次迭代的结尾,将对其他(非开发)环境进行重新管理,运行测试用例。

Page 135: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

第八章 实体 Bean的替代物

实体 bean 从它出现以来就陷入了人们激烈的争论之中,有的人喜欢它,有的人厌恶

它。许多人声称实体 bean是性能的杀手,不适合处于黄金时期的企业应用;而另一些人

则声称在恰当的环境中使用实体 bean是一个好的决策。这一章将从正反两方面来讨论实

体 bean,并将介绍一种可以象实体 bean 一样被用于会话 表面层的实体 bean 的替代

物。

实体 Bean的特性

实体 bean提供了一个创建具有透明的持久性、由容器管理的、分布式的、安全的、

具有事务性的构件的标准。注意在上面这句话中所使用的词:

标准 实体 bean提供了一种标准的创建持久性域对象的方法。在实体 bean之前,人

们不得不编写他们自己的持久性框架或者依赖于专有的关系映射工具。

透明的持久性 这意味着业务逻辑可以被编写为在使用实体 bean 和实体 bean 上的

业务方法时并不需要了解任何有关底层的持久性机制(BMP 或 CMP;RDBMS 或

OODBMS)的知识。这使得业务逻辑可以成为纯粹的业务逻辑,而不会与有关持久性的

代码缠绕在一起。实体 bean对实体 bean的客户端代码隐藏了所有的持久性逻辑。

由容器管理的 当使用 CMP 实体 bean 时,它的主要好处就是容器可以为开发者处理

所有的持久性逻辑和 O/R映射,以及数据缓存。持久性逻辑长得令人生厌,并且很乏

味,而且还可能隐藏了许多 bug,所以使用 CMP对开发者来说是一个好的选择。

分布式的 实体 bean是分布式的对象,它们对于网络上的任何 java 或 CORBA客户

而言都是可以通过 RMI-IIOP来调用的。

安全的 象会话 bean 一样,实体 bean 的业务方法也可以被配置为是基于角色的安

全检查的,这是通过配置发布描述符来实现的。

具有事务性 实体 bean为开发者提供了细粒度的事务控制。通过提供自动的声明式的

控制,发布描述符可以被配置为对一个实体 bean 上的不同的业务方法赋予不同的事

务语义和隔离级别。

构件 实体 bean 被设计成为了构件。发布描述符和所有使实体 bean 成为可发布的

所必需的声明式的标签都是为了使实体 bean 成为一个自包含的、不需改变任何代码

就可以发布到任意的 J2EE应用服务器的构件。

这些特性体现出了实体 bean新增加的重要价值,而这些在其它的这个规模的分布式

框架中是从来都得不到的。那么既然具有这么丰富的特性集,为什么人们还会不愿意使用

实体 bean呢?

实体 Bean和认知的不一致

“认知的不一致是当我们做某件与我们先前的价值观、信仰和感觉相反的事情时所经历

的内部冲突,为了减轻压力,我们必须要么改变我们的信仰,要么解释清楚我们的行为” The Complete Idiot’s Guide to Psychology (Johnston, 2000)

Page 136: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

实体 bean被设计成为了分布式的构件(具有上面所描述的所有特性),但是开发者真

正需要的只是将实体 bean作为轻量级的域对象来使用,由此也就产生了认知的不一致。早

期版本的EJB规范的限制和为了支持EJB作为构件所需的性能和开发开销是为什么关于实

体 bean的观点是如此的混杂的主要原因。

到底哪些实体 bean特性在大多数真实世界的项目中实际上并没有被用到的呢?

1. 分布式 客户端直接和一个实体 bean通信的做法是性能和可维护性的杀手,请查看

会话表面层模式(第一章)中关于这个问题的深入解释。幸运的是,Local接口缓

解了实体 bean的性能问题,但是在Local接口出现之前,实体 bean是远程的这

个事实是许多项目决定不使用它们的实际原因。

部分原因是因为EJB1.X 实体 bean的远程本质,使得会话表面层模式成为了实际

上的在设计EJB系统时所使用的方法,它在提高性能的同时,也是一种设计更好的

系统的方法。在使用会话表面层模式时,实体 bean所提供的许多其它特性都变成

多余的了。

2. 安全性 因为会话表面层是为客户层提供的单一的访问节点,所以调用者的安全性检

查通常只是在被客户端调用的会话bean的方法内执行。这样容器或编程的安全性检

查在调用实体 bean时就已经完成了,所以任何实体的有关安全性的发布描述符都

是不需要的。

3. 事务性 同上面一样,使用会话表面层时,事务通常都是由容器管理的,并在会话

bean的发布描述符中被声明。事务在客户端调用一个会话bean的方法时启动,在

客户端完成这个方法调用时结束。这样,因为事务是在会话表面层中被划分边界的,

所以开发者不需要为一个实体 bean的任何业务方法声明事务(在发布描述符中),

而且容器也不需要在一个实体 bean的方法被调用时执行任何事务检查。

4. 构件 许多编写实体 bean所必需的复杂性和收集周围环境的工作都是在探测被设

计为能够使实体 bean 构件独立地发布到任何应用服务器上的基础架构,许多编写

发布描述符时所需的标签也都是为了这个目的。认知的不一致在这里发生了,因为

开发者并不把实体 bean 作为构件来使用,而是作为轻量级的域对象来使用。这里

的构件是与会话 bean一起被划分的,而且实体 bean只是被会话表面层作为简单

的对象模型来使用。象实体 bean的相互关系这样的东西也都只是在代码中而不是

在发布描述符中作了很简单的表示。

实体 bean 的另一个主要问题是N+1个数据库调用问题(查看通过JDBC读取数据模式

中关于这个问题的解释)。N+1个调用问题使得在对象模型很复杂而且客户端并不频繁地从

服务器上读取相同数据(这样,实体 bean数据的缓存将没有任何用处)的项目中使用实体

bean变得很困难。我们将在这一章的后面部分看到在这种情况下所使用的模式。

捍卫实体 Bean

随着 EJB2.0的发布,下面这些实体 bean的主要问题得到了解决:

远程性 这个问题随着 Local 接口的引入而获得了解决,它允许以一种细粒度的方式

来编写实体 bean,并且限制实体 bean只能被会话表面层所调用。

支持关系和对象建模 对 CMP 的性能提高考虑到了实体 bean 之间丰富的关系集,使

得我们可以将一个域模型直接映射到一个实体 bean 的层次关系上。例如,现在可以

把多个实体 bean编写为具有 1:N和 N:M的关系,它们可以交叉删除,还可以具有更

多的特性,从而可以节省大量的时间。

由于 EJB发布平台的成熟,另外两个 EJB的主要问题也得到了解决:

Page 137: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

N+1个调用问题 这个问题的解决方案是可以在一 JDBC块调用中加载一个实体 bean

集,这是许多现代化的应用服务器所支持的 CMP的一个特性。对于 BMP,Gene Chuang

的 Fatkey模式(查看 TheServerSide.com 模式部分)也解决了 N+1个调用的问

题。

复杂的、缓慢的开发时间 复杂性和开发时间的开销(在前面的部分中解释过)已经因

EJB工具的成熟而减少了很多。由 Sun产生的新的 JSR将使我们很快就可以在市场上

得到这样的工具:通过使用它,可以在不进行任何 XML和 home/构件接口编码的情况

下,完成对实体 bean建模、生成和发布到应用服务器中等任务。

尽管实体 bean的许多好处都是多余的(当我们在会话表面层的幕后使用它时),但是

实体 bean实际所能提供的好处(一个标准的、透明的持久性管理,容器管理的持久性)

还是很显著的。让我们来看一看这些好处对你的项目意味着什么:

减少了爬坡学习的时间 因为它是一个很好理解的标准,所以使用它可以减少爬坡学习

的时间并降低新的 EJB 项目的开发代价,因此接受过 EJB 培训或者已经掌握了一些

EJB技巧的人正在变得日益流行。

先进的在运行环境(box)范围之外的 O/R映射 EJB 的 CMP机制本质上是一个标准

的在运行环境范围之外持久性框架。这样,当我们使用 CMP 实体 bean时,就不需要

在第三方的 O/R映射产品上花费金钱和爬坡学习的时间。减少在一个软件应用中的可

移动部件的数量可以明显地减轻系统维护的负担。

可移植性 实体 bean被保证对任何 J2EE应用服务器都是可发布的,但是用第三方持

久性引擎(例如 O/R映射工具和 JDO引擎)开发的简单的平凡(plian)java对象

(plain ordinary Java object,POJO)只能运行在支持你的特有的 O/R映射

工具的应用服务器上。

EJB2.0 使实体 bean 成为了一种可行的用来在设计良好的会话表面层幕后构建可

移植的并可扩展的域模型的技术,同时,由于实体 bean 是构件这个事实,使得开发实体

bean与开发 POJO相比依然相当地复杂。

尽管这是一本有关 EJB设计模式的书,但是在这本书中许多应用于实体 bean的模式

也应用到了其它的域模型技术中。因为许多开发者到现在为止仍然不愿使用实体 bean,

所以这一章的剩余部分将讨论能够替代实体 bean以支持使用 POJO来构建服务器端的域

模型的 优方法。

实体 Bean的替代物

如果不使用实体 bean,那么就需要一些其它的持久性资源来支持在你的会话 表面层

幕后的所有的业务逻辑。大多数做出这种选择的动机都是希望能够不使用构件来构建域模

型,而是使用 POJO 来构建,从而实现系统的简化和性能的提高。POJO 与构件相比,它

实现起来更简单、更快速、更容易并且更加面向对象。我们将在下面的部分中分别阐述这

些其它的选择。

使用直接 JDBC操作/存储过程

这是一个非 POJO 方法,其中的会话 bean 被编码为直接与数据库进行交互以完成某

些操作。数据访问命令 Bean模式(第三章)提供了一种将会话 bean的业务逻辑与持久

性逻辑相分离的 优方法。然而,即使是使用 DACB 模式,在多个会话 bean 层和一个存

储过程层中划分业务逻辑也只能导致效果较差的划分和封装。关于使用不使用直接 JDBC

Page 138: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

操作/存储过程的争论可以归结为是使用关系型还是面向对象型设计的问题,这在其它的

出版物中已经有了深入的探讨。

使用一个第三方的 O/R映射产品

使用一个可以插入到你的 J2EE应用服务器中的第三方的 O/R映射工具是 通用的替

代实体 bean的方法。这些工具允许你使用它们来透明地将你的对象持久化,而不需要开

发者作任何附加工作,从而使得你的域模型可以被编写为平凡 java对象。O/R映射工具

是非常流行的,并被广泛应用的实体 bean的替代物。这种方法仅有的缺点是这些产品本

质上是专有的并且潜在地缺乏可移植性。使用这种方法意味着,我们宁可使用一个专有的

产品(它需要培训),也不使用一个象实体 bean 这样的很好理解的标准。同样,如果你

关心的是使你的 J2EE 应用可以在不同的应用服务器之间移植,那么你只能被限制在支持

你的 O/R工具的某一种或某几种特殊的应用服务器范围之内。

构件定制的持久性框架

在这里,开发者构建一个定制的持久性框架,它可以在幕后获取 POJO 并将它们持久

化。这种方式的好处是你不用为一个昂贵的第三方的 O/R映射工具支付费用,就可以使用

POJO了;而它的缺点是你需要自己去实现一个持久性层,这是一个复杂而棘手的过程。

使用 java数据对象

java 数据对象(JDO)是 Sun 的一个新 API,它的目的是为 POJO 提供透明的持久

性。JDO提供了实体 bean实际上所能提供的所有好处(一个标准,透明的持久性,由容

器管理),所有这些都被打包在一个非常轻量级的框架中,这个框架允许开发者使用简单的

POJO来编写复杂的域模型。

象实体 bean一样,java 数据对象想要表示持久性对象,与实体 bean不同的是,

java 数据对象可以被作为平凡 java 对象来开发,完全独立于任何的容器和 API(JDO

甚至不需要 JDO API)。作为一个标准,JDO看起来像是 有前途的实体 bean的替代物,

所以我们将在本章的剩余部分中来阐述它。

Page 139: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

Java数据对象的简介

JDO是一个有关透明的对象持久性的规范,它允许你创建 POJO的复杂的层次结构,

并且使它们所有的持久性细节被透明地处理。JDO定义了一个真正的面向对象的持久性机

制,它可以被用来将 JDO持久化到任意类型的数据库中(关系型数据库、对象数据库等)。

使用JDO,开发者不需要编写任何持久性代码,而且使用java数据对象的业务逻辑(和

数据对象自身)完全对底层的持久性机制隐藏了起来。Java数据对象是一个有吸引力的实

体 bean的替代物,它能够在会话 表面层 之后将java 对象持久化,它更加轻量级,并且

在维护作为一个标准的所有好处的同时,卸下了所有的开发实体 bean分布式构件的重负。

在写这部分内容的同时,它正在被Java Community Process作为JSR 12来开发,并且

已经处于1.0 提案的 后修改稿阶段(1.0 proposal final draft stage)。遗憾的

是,目前还没有在J2EE规范中纳入JDO的任何计划,可能是因为JDO与实体 bean在进行竞

争的缘故,因此,JDO将不可能成为J2EE规范的一部分,其结果是J2EE应用服务器不会提

供内置的对JDO的支持,因此,需要一个第三方的JDO引擎与J2EE服务器一起运行以使能

JDO。

这一节的剩余部分将从EJB开发者的角度来探讨JDO,以此说明一个EJB开发者将怎样

使用JDO,并将讨论一个EJB开发者应该意识到哪些问题。关于不同的JDO API的细节信息

超出了这一节的内容范围,我们将注意力放在说明JDO可以在一个EJB context中被如何

用来完成工作。

对类的要求和类之间的依赖

图 8.1 展示了一个简单的将银行账户(Bank Account)作为 CMP 实体 bean 和

JDO来实现的例子。

Page 140: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

图 8.1 简单的 Account 实体 bean与 JDO类以及它们的依赖图

图 8.1展示了开发者必须实现的类和在开发一个简单的银行账户时需要考虑的其它

的依赖关系(它们是实现 javax.ejb.EntityBean这样的接口所必需的)。在这个 EJB

用例中,需要编写三个类(home、local和 bean类),这三个类都与

javax.ejb.EntityBean之间存在着依赖关系,而且在 bean类中,还需要编写 10个

EJB回调方法(ejbLoad等)。相比之下,java 数据对象方法只需简单地编写一个

Account 的 java 类,而不依赖任何外部的系统 API,并且没有任何回调方法。对 JDO

的唯一要求就是编写主键类(AccountID),当我们试图查找一个 JDO时,就必须使用这

个类(将在本章稍后的部分进行解释)。

构建和发布过程

实体 bean和 JDO的构建和发布过程是很相似的,这一节将比较和对比这两种方法:

为对象编写一个 XML描述符。这项工作只需在对象开发过程的伊始进行一次就行

了,如果对 bean/对象做出了重大改变,例如在 bean/对象中添加了一个新属性,

那么其对应的 XML 文件通常也需要进行修改。对实体 bean 需要编写一个

ejb-jar.xml发布描述符(一个发布描述符对应一个单一的 bean或一个 bean

集合)。在 ejb-jar.xml 中,通过使用 XML 标签,集中了对类、事务、安全、

jndi、持久性映射、关系以及其它一些信息的描述。对 Java数据对象也需要编

写一个名字为<类名>.jdo 或<包名>.jdo 的 XML 文件(一个文件对应一个类或

一个包)。需要被类用来进行持久化的描述符的名称同时也提供了有关类的域和提

供商扩展的信息。

Page 141: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

使用标准的 java 编译器来编译文件。可以使用任何标准的编译器来编译实体

bean 的类和 JDO。使用 JDO 时,开发者可以选择在进行编译之前在他们的 JDO

java 源 代 码 文 件 上 运 行 一 个 源 代 码 后 处 理 器 ( source code

post-processor ), 这 个 后 处 理 器 对 在 .jdo XML 文 件 中 被 称 为

persistence-capable的持久性对象的源代码进行修改,将它们与持久化所需

的逻辑一起编码。如果开发者不想运行一个源代码后处理器,那么他们可以运行

一个在后面将要描述字节码增强器(byte code enhancer)。

一旦 JDO被编译,就可以开发测试脚本了,这些测试脚本实例化你的 JDO并测试

代码的所有方面(并没有对任何数据进行真正地持久化操作)。实体 bean不能以

这种方式被测试,因为它们依赖于 EJB容器。

使用提供商特有的工具来完成后编译(postcompile)工作。在这一步,使用一

个与具体的提供商有关的特有的后编译工具来完成实体 bean或 JDO的后编译工

作(除非已经使用了一个源代码后处理器),在执行这项工作时需要使用附加信息

的 XML描述符。要注意的是,在第一次执行这项工作时,可能对于实体 bean和

JDO,都需要通过使用一个提供商特有的工具来将其映射到一个底层数据存储上

(也就是说,把域映射到一个 RDBMS中的列上)。实体 bean是被一种类似 EJBC

的工具编译的,这种工具产生 CMP 实体的与具体的提供商有关的桩和持久性子

类。如果开发者不想使用源代码后处理器,那么 java 数据对象就需要使用字节

码增强器来完成后编译工作,字节码增强器要对 JDO的字节码进行修改,这些修

改类似于源代码后处理器所作的修改。在后编译/后处理之后,java 数据对象就

可以被完整地测试了(测试持久性映射以及运行时的行为等),这些测试都是在

JDO提供者的环境内进行的,而不需要使用应用服务器。

打包和发布到应用服务器。这里我们获取已经准备好被运行的代码,并将它们发

布到应用服务器中。通常情况下,实体 bean都与通过其 Local接口调用它们的

会话 bean打包在同一个 ejb-jar中。

Java数据对象也可以被打包在使用它们的会话 bean的 ejb-jar中,或者作为

一个库(library)添加到 J2EE ear中。

实体 bean与 JDO的构建和发布过程是十分相似的,不同之处在于它们的发布描述符

的复杂程度不同,以及在构建过程中,java 数据对象与实体 bean 相比,可以在更早的

阶段被测试,因为 java数据对象并不依赖于应用服务器的存在。

继承

继承对于实体 bean来说,从来都不可能真正实现,因为实体 bean并不是要成为域

对象,它们是要成为构件。可以通过在实体 bean的类之间或在 remote接口之间的继承

来实现基本的代码共享,但是整个构件并不能被继承。也就是说,由继承所带来的在运行

时刻的好处(也就是说,一个客户端不能向下或向上将一个实体 bean的 EJBObject 桩

转换为他的父类或子类)是不可能实现的。

Java 数据对象仅仅是一个 java 对象,所以它可以很容易地使用复杂的继承结构,

类与类之间可以很容易地实现彼此扩展,所使用的方法也与平凡 java 对象所使用的方法

一样。在运行时刻,客户端可以向上或向下转换 JDO 而不会有任何问题。这使得 JDO 比

带有 local 接口的实体 bean更适合被用来构建一个适当的域模型。

Page 142: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

客户端 API

实体 bean的入口点是 home 接口,通过它你可以查找、创建、删除和修改实体 bean

(通过 home 方法)。Home 对象与实体 bean 一样数量庞大,也就是说,对每一个实体

bean,都有一个不同的 home对象来访问它,需要通过 JNDI来分别查找这些 home 对象。

Java 数据对象定义了在一个应用中的所有 JDO 的单一入口点:持久性管理器(PM,

Persistence Manager)。PM暴露了查找 JDO、对 JDO持久化(创建)、删除 JDO的接

口,同时执行缓存管理、生命周期管理和事务管理功能。PM是一个比 EJBHome接口明显

要大得多而且更加复杂的接口。JDO 的开发者可能会希望将 PM 与他们自己的类包装在一

起,并且屏蔽掉 PM中不常用的方法。

动态与静态的查找机制

当我们使用 CMP bean 的 EJB 查找方法时,由查找方法实际执行的查询必须在开发

或发布时将其定义在发布描述符中,这限制了 CMP 实体 bean,使它不能在运行时刻使用

实体 bean的动态查询。

Java 数据对象使用零散输入的字符串来表示查询,这些字符串能够被动态地构建和

执行,这与被用来执行 JDBC查询的字符串相类似。这样,使用 JDO就有可能创建一个动

态查询引擎,这个引擎可以在运行时刻来查询基于不同标准的 java 数据对象的 JDO 引

擎。这为我们解决艰难的设计问题打开了一道方便之门,这些问题是不可能由实体 bean

所使用的、静态的、在开发时定义查找方法的机制所能够解决的。

Page 143: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

EJB开发者使用 JDO的指南

JDO和 EJB是互补的技术,JDO适合 EJB系统是因为它可以成为实体 bean 的替代

物,并可以作为一种你的应用中的域模型的实现技术。

JDO也可以被用来向BMP 实体 bean提供持久性,或者被你的应用服务器透明地用来使

CMP 实体 bean 持久化,但是这些方法并没有任何实际意义。因为JDO是一个轻量级的使

POJO持久化的机制,从设计和开发的观点来看,使用它们来作为实体 bean的实现细节将

不能提供任何实际的好处。

下面一节将讨论如何在会话表面层幕后使用JDO。

准备你的 EJB环境

JDO 的引导接口是 PersistenceManagerFactory,通过向它请求来获取一个

PersistenceManager实例——你的 JDO对象模型的主入口。使用 JDO的 EJB需要一种

方法来获得对这个工厂进行访问的能力,这样它才能与 JDO一起开始工作。推荐使用的在

你的应用服务器中使能 JDO的方法是将这个工厂置于 JNDI树中,这样,对于你的应用中

所 有 的 会 话 bean 来 说 , 它 都 是 被 可 访 问 的 。 一 个 被 命 名 的

PersistenceManagerFactory 的实例可以通过 JNDI 被查找到,这类似于查找一个

JDBC 数据源。PersistenceManagerFactory 的实例被用来表示数据存储库,其属性

可以配置,这些属性包括数据存储库的名字、用户名、密码和其它选项等。通常需要在 VM

中,为每一个数据存储库实例化一个 PersistenceManagerFactory实例。

开发者可以通过启动类或其它可得到的、与具体工具相关的机制来将 PMF 添加到

JNDI树中。

配置会话 bean

在 会 话 表 面 层 上 的 会 话 bean ( 查 看 会 话 表 面 层 模 式 ) 需 要 访 问

PersistenceManagerFactory,就象上面提到的那样。会话 bean的发布描述符需要

被配置为将要使用这项资源,这与它们怎样被配置为将要使用一个 JDBC 数据源相类似。

我们建议使用 java:comp/env/jdo作为 JDO的名字空间,当然,这不是必需的,但是

它是我们所推荐的。例如,在会话 bean的发布描述符中可能包含了如下内容: <resource-ref>

<description>The JDO PersistenceManagerFactory to access the Human

Resources database</description>

<res-ref-name>jdo/HumanResourcesPMF</res-ref-name>

<res-type>javax.jdo.PersistenceManagerFactory</res-type>

<res-auth>Container</res-auth>

<res-sharing-scope>Shareable</res-sharing-scope>

</resource-ref>

在会话 bean 自身的内部,需要一个成员变量与 JDO:a 一起用来缓存对

PersistenceManagerFactory 的引用。在 EJB 容器实例化一个会话 bean 之后,它

将调用 setSessionContext()方法。在这里,会话 bean 在其生命周期内应该对

Page 144: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

PersistenceManagerFactory的一个引用执行一次查找和缓存操作,就象在你使用实

体 bean时,需要缓存对 EJBHome的引用一样: ...

private PersistenceManagerFactory pmFactory = null;

public void setSessionContext (SessionContext ctx)

{

this.ctx = ctx;

InitialContext icx = new InitialContext();

pmFactory =

(PersistenceManagerFactory)icx.lookup(“aPMFactory”);

}

...

会话 bean将设置 SessionContext时获得对 PersistenceManagerFactory的

引用,可以通过使用在会话 bean 的 ejb-jar.xml 文件中的<ejb-ref>标签,将

PersistenceManagerFactory 实际的 JNDI 名字从”aPMFactory”映射到恰当的

JNDI名字上。

执行用例和事务管理

在会话 表面层上的方法通常映射为在你的应用设计中的单个用例,就象在使用实体

bean 时一样,这些用例通常需要在一个事务中运行。当从一个会话 bean 层中使用 JDO

时,开发者必须在容器管理的事务(CMT)和 bean 管理的事务(BMT)之间做出选择,就象

他们为用来查询实体 bean的会话 bean所作的选择一样。使用 BMT时,bean的开发者

可以选择何时启动和终止事务,并且可以执行事务内部或外部的方法。使用 CMT时,容器

通过在会话 bean的发布描述符中的事务设置来划分事务的边界。使用 JDO的语义和语法

将会依我们的不同选择而有所变化。

容器管理的事务

CMT是首选的事务模型,使用 CMT,容器将自动地启动一个事务、挂起一个现有的事

务或在派遣业务方法之前使用一个现有的事务,以及在业务方法执行完毕时自动地提交、

恢复或回滚事务。

使用 CMT 对 JDO 的影响是:每一个业务方法在开始执行时都必须要从

PersistenceManagerFactory处获得一个 PersistenceManager,并且在结束时要

关闭这个 PersistenceManager。当向 PersistenceManagerFactory 请求一个

PersistenceManager时,PersistenceManagerFactory自动地判断调用线程的事

务上下文( transaction context),并试图查找已经与事务上下文绑定的

PersistenceManager,如果没有找到,那么 PersistenceManagerFactory将从包

含了可用的 PersistenceManager实例的池中选择一个 PersistenceManager,并把

它 绑 定 到 事 务 上 下 文 上 , 以 后 的 从 这 个 相 同 的 事 务 上 下 文 而 来 的 向

PersistenceManagerFactory 请求 PersistenceManager 的所有调用都将返回这

个 绑 定 的 PersistenceManager。 在 事 务 完 成 ( 提 交 或 回 滚 ) 后 , 将 断 开

PersistenceManager与事务之间的关联,并将这个 PersistenceManager放回存放

Page 145: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

可用实例的池中。

这些自动的行为使得我们可以很容易地将不同的 bean 中的业务方法结合在一起放置

到一个单一的由容器划分边界的事务中。每一个支持事务的业务方法都将在适当的 JDO事

务中执行,而不需要 bean 开发者显示地编写任何逻辑。例如,在下面的例子中,

createAccount和 deposit方法(它们存活在会话 bean上)都使用了 CMT: void createAccount (acctId)

{

PersistenceManager pm = pmFactory .getpersistenceManager();

// 获取一个新的id Account acct = new Account (acctId);

pm.makePersistent (acct);

pm.close();

}

void deposit (long acctId, BigDecimal dep)

{

PersistenceManager pm = pmFactory .getpersistenceManager();

Account acct = pm.getObjectById ( new AccountId(acctId) );

acct.deposit (dep);

pm.close();

}

在使用 CMT时,bean的开发者可以选择在事务完成后得到一个通告,这可以通过声

明它实现了 SessioinSynchronization接口来实现。beforeCompletion方法将在

事务完成阶段被调用,类似地,所有注册了同步(synchronization)的其它构件也将

被调用,但是容器并不保证调用这些方法的顺序。而且,在 beforeCompletion回调的

过程中不能执行任何访问 JDO或访问实体 bean的操作。

Bean管理的事务

BMT赋予了 bean开发者很大的灵活性,其代价是使开发过程更加复杂了,并且要做

出更多的设计决策。开发者可以使用容器的 UserTransaction实例来启动和完成事务。

这种情况类似于CMT,因为在适当的事务上下文中也要获得PersistenceManager实例。

业务方法在方法开始执行时获得 PersistenceManager,在方法结束时关闭

PersistenceManager,虽然并不严格要求必须做到这一点,但是它是一个 优方法,

因为如果这样做的话,这些方法就可以任意地组合在一起以构成更大规模的事务。

例如,下面的方法将在 CMT一节中定义的方法调用结合在一起构成了一个事务: void openAccount (BigDecimal initialDeposit, int acctId)

{

UserTransaction ut=icx.lookup(“java:comp/UserTransaction”);

ut.begin();

createAccount(acctId);

deposit (acctId, initialDeposit);

ut.commit();

}

Page 146: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

另一项技术是直接使用PersitenceManager和javax.jdo.Transaction实例。这

样就可以允许Bean开发者获得一个单一的PersistenceManager,并在几个事务中使用

它,而不需把它放回到PersistenceManagerFactory 池中。在这种情况下,业务方法

不需要获得和关闭PersistenceManager。事务性的业务方法将象下面的例子一样来获得

PersistenceManager: void openAccount (BigDecimal initialDeposit, int acctId)

{

persistenceManager = pmFactory .getPersistenceManager();

Transaction tx = persistenceManager.currentTransaction()

tx.begin();

createAccount(acctId);

deposit (acctId, initialDeposit);

tx.commit();

// 其它事务可以在这里使用同一个PersistenceManager来被执行 ...

}

缓存/惰性加载和引用定位

在使用 JDO 时,一个与事务关联的 PersistenceManager 负责管理包含了在执行

事务期间获得的所有持久性对象的缓存。如果某些持久性对象包含了对其它持久性对象的

引用,那么它们就可以被透明地导航。例如,如果你有一个 Account 实例(它带有一个

对 Owner的引用),那么你就可以通过引用 Account中的 owner域来定位到 Owner。 class Account

{

long accountId;

BigDecimal balance;

Owner owner;

Owner getOwner()

{

return Owner;

}

}

class Owner {

String name;

}

在会话 bean中,你可以定义一个方法用来定位到 Owner实例并执行它上面的方法。 String getOwner (long accountId)

{

Account acct = persistenceManager.getObjectById (new

AccountId(id));

return acct.getOwner().getName();

}

Page 147: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

查找 java数据对象

可以使用两种技术来查找JDO:执行一个查询或者用JDO的ID(标识)来查找一个实例,

就像在动态发现与静态发现机制(DynamicDiscovery versus Static Discovery

Mechanisms)一节中提到的一样,JDO对实体 bean的查找方法进行了改进,因为它允许

在运行时刻动态地构建JDO查找方法。

使用ID查找

为了使用ID来查询,你必须为你想要查找的java 数据对象构建一个主键类(primary

key class)的等价物(被称为id实体 class)。尽管不是必需的,但是id实体类通常都

包含了这样的一个构造器,它接受一个字符串作为其唯一的参数。大多数id实体类还包含

有若干个便利的方法,它们接收的参数与类中的key域相对应,例如,AccountId类可能

被定义为下面的形式:

class AccountId

{

public long id;

public AccountId (String str)

{

id = Long.parseLong(str);

}

public AccountId (long id)

{

this.id = id;

}

public toString()

{

return Long.toString(id);

}

}

在使用这个key类定义时,获取一个Account实例的操作是通过构建一个AccountId

实例并请求PersistenceManager查找这个实例来实现的,象JNDI一样,

getObjectById返回的类型是Object,它必须被转换成适当的类型。

AccountId acctId = new AccountId (id);

Account acct = (Account) persistenceManager.getObjectById(acctId);

请注意,toString方法返回一个可以被用作为构造器参数的字符串,而这个构造器将

接受这个字符串作为其唯一的参数,这是对所有的key类的一个JDO建议。

使用查询(query)来查找

Page 148: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

另外一种查找实例的方法是使用JDO查询工具。PersistenceManager是一个查询工

厂,它指定了在数据存储库中的查找范围(Extent)和一个用来筛选感兴趣的实例的过滤

器(filter)。下面的这个方法签名表示了一个查找所有满足如下条件的Account的业务

方法:这些Account具有比某一个参数更大的结余(balance) ,并且它的owner的名字

与另一个参数所指定的名字相同: Collection getAccountByBalanceAndName (BigDecimal minBal,

String name);

Extent是一个实例,它表示了在数据库中的某一个指定的JDO类的所有实例,可能还

包括这个JDO类的子类的所有实例。PersistenceManager还是一个Extent的工厂,它

的第二个参数说明了在Extent中是否包括JDO的子类: Extent acctExtent = persistenceManager.getExtent

(Account.class, true);

Extent实例是一种特殊的实例,它可能并不实际包含一个持久性实例的集合,而只是

仅仅被用来持有一个类名和其子类标志。当查询被执行时,在Extent中的信息被JDO的实

现用来将适当的查询传递到数据库中。

Filter指定了一个Java的布尔表达式,对在Extent中的所有实例都使用这个表达式

来判断其是否符合查找条件,例如,为了查找所有的结余大于某个数并且其owner与指定的

名字相同的Account实例,需要执行下面的代码: String filter = “balance > balanceParameter & owner.name ==

ownerParameter”;

Query query = persistenceManager.newQuery (acctExtent, filter);

为了声明参数类型,我们遵从java语法来为方法声明正式的参数: query.declareParameters (“BigDecimal balanceParameter, String

ownerParameter”);

后,为了真正找到我们所希望查找的Account,我们将执行这个query来获得满足

filter的约束条件的实例集合: Collection queryResult = query.execute (minBal, name);

这个查询结果可以被迭代,iterator.next()的值是Account类型的持久性实例。

要注意的是,查询的过滤器和参数都是用字符串定义的,这些字符串可以在运行时刻被动态

地创建,这是对静态的实体 bean查找机制的一种改进。

交互层(inter-Tier)数据传送

对于使用实体 bean和 java数据对象的系统,数据传送对象模式都可以很好地应用

于其中。在使用这种模式时,DTO被创建用来从 JDO中复制数据并把它们发送到客户层。

(Client tier)。在客户层上,可能会产生更新操作并把 DTO发送回服务器,服务器将

使用这个 DTO的内容来更新 JDO。

JDO 提供了一个唯一的值,因为 JDO 实例本身可作为 DTO 来使用,只要它们遵循可

序列化对象的规则。事实上,由于允许将一个复杂的实例集传送到客户端,所以可以返回

一个 JDO实例的对象图。

如果你选择直接使用 JDO持久性实例作为值(value)实例,那么这个过程会稍微更

Page 149: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第八章 实体 Bean 的替代物

复杂一些,因为持久性实例都被绑定到了 PersisetenceManager 上。为了将它们与

PersistenceManager断开联系,需要使用 makeTransient方法,这个方法的作用是

从相关联的 PersistenceManager 中移去持久性实例。在调用了 makeTransient 之

后,这个实例就再也不能被用来引用持久性数据了,因此,任何相关联的持久性实例都必

须在这个调用之前被获取。

例如,为了直接返回给用户一个 Account 和一个与其相关联的 Owner,需要把

Account 定义成为一个返回值,并使 Account 和 Owner 类对应用中的客户端都是可用

的。Owner将在它被返回给客户端时,与 Account一起被序列化。 Account getAccount (long id)

{

Account acct = persistenceManager.getObjectById (new

AccountId(id));

Owner owner = acct.getOwner();

Object [] objs = new Object[] {acct, owner};

persistenceManager.retrieveAll(objs);

persistenceManager.makeTransientAll (objs);

return acct;

}

小结

自从实体 bean产生以来,它们受到了大量的各式各样的非议,但是随着 EJB工具市

场的成熟以及 EJB2.0所带来的大量改进,实体 bean已经变成了一个成熟的平台。尽管

这样,仍然有许多人不愿意使用实体 bean,因为它们仍然是重量级的构件,而不是轻量

级的域对象。因此这些人选择使用其它的允许你创建简单的 java 对象模型的技术,包括

定制构建的持久性框架、O/R映射器和 著名的 java数据对象(JDO)。

Page 150: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第九章 EJB 的设计策略、习惯用语和技巧

第九章 EJB的设计策略、习惯用语和技巧

这一章包括了一个细粒度的策略、习惯用语和技巧的集合,这个集合可以被用来设计和

实现高效的 EJB 应用。尽管它们可能太简单或粒度太细以至于不能被当作一个完整的模式

来撰写,但是它们仍然是很重要的,这使得我们必须把它们囊括到本书中。

不使用复合实体 Bean(Composite Entity Bean)模式

复合实体 Bean 模式(也被称为聚合实体 Bean(Aggregate entity bean)模式或

粗粒度实体 bean (coarse-grained entity bean)模式)是构建符合 1.X规范的 EJB

应用时所使用的一种通用模式,引入这个模式是为了解决有关通过 remote 接口与实体

bean 通信的性能问题。为了解决这些问题,这个模式建议创建一个被称为依赖对象

(dependent object)的新的实体类型,这是一个平凡(plain)java 类,它的生命

周期由实体 bean 来管理。依赖对象的问题是:使用你的应用服务器的 CMP 引擎来创建它

们是不可能的,而使用 BMP来实现也是极度地困难,因为管理在 BMP中的一个依赖对象集

的生命周期等同于编写你自己的持久性引擎。

实体 bean要对一个应用中的“实体”或域模型建模,随着 EJB2.0 CMP的增强,例如

引入了 local 接口,使得你可以使用实体 bean,以你想要的那种细粒度来对你的设计中

的域模型建模。这样,EJB2.0就使得依赖对象的概念和复合实体 bean模式都变得过时了。

如果你关心在调用一个实体 bean 时可能发生的事务和安全检查的开销,那么你不用为此

担心。实体 bean面对会话表面层时,只需要使用 tx_support设置,并且根本不用使用

安全检查,因为安全和事务都在会话 表面层中被声明和检查。在向大量的 J2EE 服务器提

供商求证后,我们发现一个共同的现象:如果在发布描述符中没有任何有关事务和安全方面

的设置,应用服务器将不执行事务和安全检查。

大概有百分之五的案例还是应该使用复合实体 bean 模式,在这些案例中,在底层数

据库中的数据不能被映射为一个实体 bean 图。如果我们在一个遗产系统上构建一个新系

统,而这个遗产系统又完全不能被映射到新系统的对象模型上,那么这种情况就属于上述案

例。

使用一个域命名规则以允许在 EJB2.0 CMP 实体 bean中

执行数据确认

从 EJB2.0 CMP出现时开始,实体 bean就可以被编写为抽象类了,因为 CMP引擎将

为开发者实现所有的持久性逻辑。这样做的一个副作用是开发者再也不能访问

getXXX/setXXX方法的实现了,因为它们必须被声明为抽象的,并且被容器来实现。对于

其它 EJB执行一个实体 bean上细粒度的 get/set方法的情形,将会由于 local接口的

引入而成为可以被接受的行为,所以一个开发者将愿意暴露这些在 local 接口中的

get/set方法。这样,问题就变成了:如果开发者不能够访问一个 set方法的实现,那么

他们怎样才能在要设置的数据上执行语法和业务确认?

Page 151: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第九章 EJB 的设计策略、习惯用语和技巧

解决方案是为 set 方法使用一个简单的命名规则和代理方案。我们并不是暴露一个在

local接口中由 CMP生成的 setXXX方法(为一个被称为 XXX的属性而产生的),而是暴

露一个在 local接口中被称为 setXXXField的方法。在 setXXXField方法内部,开发

者可以去实现适当的确认检查,然后将这个调用代理给由容器生成的 setXXX方法。

不要在实体 bean上获取和设置值/数据传送对象

另一个在 EJB1.X 时期产生的、已经过时的模式是使用值对象(更确切说是数据传送

对象)来获取和设置一个实体 bean的大型的数据集。这个模式最初是有助于性能提高的,

因为它限制了从客户端到实体 bean的 get/set远程调用的数量,这样,客户端只需获取

和设置一个包含了大量的实体 bean 属性集的 DTO。然而,这个模式会导致相当严重的实

体 bean的维护问题,使用 DTO作为一个访问实体 bean的接口作带来的问题在数据传送

对象模式一节(第二章)中已经有了深入的探讨。

幸运的是,local接口使在实体 bean上执行细粒度的 get/set方法的行为成为了可

以被接受的行为,这样,使用 DTO 来传入和传出实体 bean 的数据就变成了一种过时的模

式。开发者应该将数据传送对象看作是被用来在层与层之间进行通信(客户端和会话 表面

层之间)的数据封装。

如果能够被正确使用,就可以使用 Java 的 Singleton类

人们对在 EJB 中 singleton 所充当的角色存在着很多的顾虑、半信半疑和怀疑。最

初的 Singleton 模式(Gamma,et al.,1995)建议创建一个包含了它自己的一个静态

实例的 java类,这样,在一个应用中,这个类将只有一个实例在运行。EJB规范中描述到:

EJB既不应该使用静态域,也不应该使用有关同步的关键字(例如 synchronized关键字)。

许多开发者都由此推断出一个不正确的结论:这意味着一个 EJB 不能调用一个

singleton,因为 singleton它自身使用了一个静态域和 synchronized关键字。

这个结论是错误的,只要开发者不以读/写方式来使用 sinlgleton 类,那么使用

sinlgeton类将不会产生任何错误,因为在以读写方式来使用 sinlgleton类的情况下,

调用 sinlgleton 类的 EJB 线程可能需要被阻塞,而这种行为是 EJB 规范努力去防范的

行为。但是,在只读行为或其它任何类型的允许 EJB 彼此互相独立地访问 singleton 的

服务中使用 singleton都是可以的。

关于使用 Java 的 singleton 类的一个告诫是不可能创建一个经典意义上的

singleton——每一个应用中只有一个对象实例。在最少的情况下,任何被使用的

singleton在每一个服务器的 JVM中都有一个副本,并且通常每一个类加载器都有它的一

个实例(每一个被发布的 ejb-jar 都有一个它们自己独立的类加载器和它们自己的

singleton)。

如果你想要创建一个不阻塞的服务,而对于这个服务,你并不介意在同一个 VM中有数

个 singleton的副本,但是你又不想有那些在把这个服务作为无状态会话 bean来实现时

所必需的实例池和内存开销,那么你就可以使用 java 的 singleton类。例如,一个主键

生成器(诸如 EJB 的 UUID 和序列块模式)提供了一个轻量级的、比无状态会话 bean 更

高效的实现选择。

Page 152: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第九章 EJB 的设计策略、习惯用语和技巧

使用预定更新而不是实时计算

当构建基于 web 的应用时,如果计算是很耗时和耗资源的处理过程,而每一个象要求

计算一个需要被显示到用户接口(UI)的值这样的请求又都必须通过 EJB层来完成,那么

开发这个应用将是非常昂贵的。

例如,在 TheServerSide.com中,在主页右上方的表示 membership count的数

字对于每一个单一的 web 请求都将需要一个连接到数据库代理去执行一个 selece

count(*) from user查询。由于现在已经有超过 140,000个用户,在每一分钟内都有

很多个主页浏览请求,因此实时地执行这个查询将是一个很明显的性能瓶颈。

这个问题可以得到缓解,方法是:我们并不实时地执行计算,而是每隔一定的间隔,使

用一个预定调度工具(Scheduler),例如Unix Cron或J2EE Scheduler Flux来执行

计算,并将输出缓存到磁/硬盘上。在JSP中,只需在这个被缓存的文件上执行一个

jsp:include,而不需要将查询代理到服务器上。用这种方法可以实现性能的显著改善。

通常,你需要问一问自己将要显示的UI部分是否真正需要被实时实现。对大多数只读

的浏览类型的UI而言,每一个web请求都要通过EJB层可能并没有意义。因此,你应该使用

预定更新而不是实时计算。

使用一个被序列化的 java类来将编译器类型检查添加到与

消息驱动 Bean交互操作中

消息驱动 bean 要消费 JMS 消息,所有这些消息在编译时刻看起来都是一样的。这与

会话/实体 bean相反,会话/实体 bean支持 java内置的对 remote和 local接口中的

方法和参数的强类型要求,从而可以在编译时刻捕捉一些常见的错误。

一种解决方案是通过将 JMS 消息定义为可序列化的 java 对象来弥补这个缺陷。将应

用层和业务层之间的契约建立成为一个 java对象集,这个 java对象集只包含了所需的成

员变量、获取方法(getter)和设置方法(setter),然后在 JMS消息中使用这些对象而

不是使用任意形态的域集合,这样做可以重新使能编译器类型检查。将对象序列化所需的开

销不会有碍于性能,而且,这个操作还总是被异步执行的。使用这个方法时最好是给类起一

个能够表示其行为的名字,例如,当创建一个类来将数据组装(marshal)到一个下订单

(place an order)的消息驱动 bean中时,这个类将可以被称为 PlaceOrderAction,

或其它包括了这些意思的名字。

在发生应用异常时总是调用 setRollbackOnly

一个重要的但并没有被强调的事实是从 EJB抛出给客户端的应用异常(application

exception,开发者编写的异常)不会触发正在运行的事务自动地回滚,这与

EJBException相反,EJBException自动地触发当前的事务回滚。如果一个用例失败了

而它的事务并没有回滚,那么将会引起严重的数据一致性问题。

因此,一定要记住首先是捕获应用异常,然后在向用户端抛出或打包应用异常之前,调

用 ctx.setRollbackOnly() ( ctx 的 类 型 是 会 话 bean 的

javax.ejb.SessionContext)。

Page 153: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第九章 EJB 的设计策略、习惯用语和技巧

限制传递给 ejbCreate的参数

在构建实体 bean 时,开发者经常不正确地以为他们应该将实体 bean 的所有属性都

添加到 ejbCreate方法中。尽管这样做可以完成它应该完成的工作,但是事实也证明了这

样做会使得诸如增加或移去一个属性这样的对实体 bean 的修改变得很难实现。如果一个

属性被移去,那么实体 bean的 ejbCreate、ejbPostCreate和 home接口都需要被修

改,而且定义这三个方法和接口的所有方法签名必须保持同步。当增加一个属性时,如果所

有其它的属性都传递到了 ejbCreate中,那么为了保持一致性,这个新属性也需要被添加

进去,同时需要修改其它所有有关的方法签名。

我们可以采用一个经常被使用的惯例来减少在修改一个实体 bean属性时所需的开销,

这个惯例将传递给 ejbCreate 的参数限制为只包含那些创建实体 bean 时强制的或必需

的属性(我们假设这些强制性的属性不会被经常修改),这样,在创建实体 bean 的会话

bean中,不是将所有的属性都传递给 home.create方法,而是只传递一个属性子集,然

后调用实体 bean 上的 setter 方法,这些 setter 方法用所有必需的其它属性来生成这

个实体 bean。

不要在 ejbCreate中使用数据传送对象

在对一个实体 bean的 ejbCreate方法编程时,开发者容易犯的另一个错误是将一个

与实体 bean对应的域 DTO作为构造器参数传递进来[Brown,2000]。请考虑一下在第六

章中所描述的 J2EE五层体系结构,其中数据传送对象存在于应用层与服务层之间,这样,

传递一个 DTO到实体 bean的 ejbCreate方法中将产生域层与上层之间的依赖。

这可能会产生各种各样的问题。例如,一个域 DTO 包含的属性经常比在最初创建一个

实体 bean 时所使用的属性更多,甚至包含了在实体 bean 中并没有的属性(例如计算得

到的值)。传入一个包含空值的 DTO到 ejbCreate中做法只能被评价为:使用了错误的类

来完成错误的工作。

请记住上面描述的要限制传递到 ejbCreate的参数的提示,所以我们应该只传递基本

类型的参数到实体 bean的 ejbCreate方法中。

不要作为一个 DTO 机制来使用 XML 进行通信,除非你确实

是不得不使用它

XML是一种非常重要的集成(integration)技术,这里,集成这个关键词意味着 java

系统与非 java 系统之间的集成。当两个 java 系统之间互相通信时,使用 XML 并没有实

际意义,事实上,这将会导致不必要的性能开销和臃肿的代码。特别是,只有在你确实是不

得不使用的情况下,才应该将 XML作为一种在客户端与服务器之间传送数据的机制来使用。

也就是说,除非你确实需要在你的数据库中持久化 XML数据,否则,在 EJB层中生成 XML

并将它传递给客户层的做法与简单而快速的对数据传送对象的序列化相比,只能是一个性能

很差的选择。

如果你的表示层使用 XML来生成动态的 UI,那么在这一层就可以生成与具体的客户端

有关的 XML文件,这时你可以考虑在这一层将 DTO从服务器传送到客户端,并执行 DTO到

Page 154: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

第九章 EJB 的设计策略、习惯用语和技巧

XML 的转换。同时,你还可以考虑使用 JAXB API,它能够提供一种标准的方法来实现自

动的 DTO到 XML的转换。

Page 155: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

模式代码列表 这个附录的目的是为了给在这本书中需要用代码来做进一步解释的模式提供完整的示例代

码,而代码置于书中的后面部分是为了保持模式本身的描述文本整洁有序。 具有示例代码的模式都包括在这里,可以引用目录得到所有的示例代码的页数。 这本书的Web站点www.theserverside.com/patterns/ejbpatterns 包含了所有

列在这里的源代码的运行版本和编译版本,同时还为这本书的读者提供了一个论坛。

EJB 命令模式(EJB Command Pattern) 这里包含了一个非常简单的命令模式的实现,包括路由逻辑组件(CommandExecutor

和 EJBCommandTarget) 和一个命令服务器(CommandServerBean),它包含了一个银行

转账命令。 这个源代码是一个由IBM的命令框架(Command framework)而产生的高度简化的

版本,它只是用来说明命令模式的概念。使用它所产生的后果由你自己负责。

Transfer Funds Command(资金转帐命令) package examples.command; import examples.account.AccountHome; import examples.account.Account; import examples.account.ProcessingErrorException; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.rmi.PortableRemoteObject; import javax.ejb.FinderException; import java.rmi.RemoteException; public class TransferFundsCommand extends Command implements Serializable {

String withdrawAccountID; String depositAccountID; double transferAmount;

double withdrawAccountBalance; double depositAccountBalance;

public void execute() throws CommandException {

//at this point we are inside the EJB Server try {

InitialContext ctx = new InitialContext(); AccountHome home = (AccountHome)PortableRemoteObject.narrow

(ctx.lookup(“Account”), AccountHome.class);

//locate accounts and perform transfer Account account1 = home.findByPrimaryKey(withdrawAccountID); Account account2 = home.findByPrimaryKey(depositAccountID);

Page 156: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

account1.withdraw(this.transferAmount); account2.deposit(this.transferAmount); //populate command with final balances this.depositAccountBalance = account2.balance(); this.withdrawAccountBalance = account1.balance();

} catch (Exception e) {

//wrap the exception as a command exception and throw //to client for interception throw new CommandException(e);

} } public void setWithdrawAccountID(String withdrawAccountID) {

this.withdrawAccountID = withdrawAccountID; } public void setDepositAccountID(String depositAccountID) {

this.depositAccountID = depositAccountID; } public void setTransferAmount(double transferAmount) {

this.transferAmount = transferAmount; } public double getDepositAccountBalance() {

return depositAccountBalance; } public double getWithdrawAccountBalance() {

return withdrawAccountBalance; } public TransferFundsCommand() {}

}

Command Superclass(命令的父类) package examples.command; import java.io.Serializable; public abstract class Command implements Serializable {

public abstract void execute() throws CommandException; }

CommandServer Session Bean(命令服务器会话Bean) package examples.command; import javax.ejb.*; import java.rmi.RemoteException;

Page 157: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

import javax.naming.*; public class CommandServerBean implements SessionBean {

SessionContext ctx; public void CommandServer() {} public Command executeCommand(Command aCommand) throws CommandException {

try {

aCommand.execute(); } catch (CommandException e) {

ctx.setRollbackOnly(); throw e;

} return aCommand;

} public void ejbActivate() throws EJBException,

java.rmi.RemoteException {}

public void ejbCreate() throws CreateException {} public void ejbPassivate() throws EJBException,

java.rmi.RemoteException {}

public void ejbRemove() throws EJBException, java.rmi.RemoteException {}

public void setSessionContext(final SessionContext p1) throws EJBException, java.rmi.RemoteException

{ this.ctx = p1;

} }

CommandException(命令异常) package examples.command; public class CommandException extends Exception {

Exception wrappedException; public CommandException(){} public CommandException(Exception e) {

this.wrappedException = e; } Exception getWrappedException() {

return wrappedException; }

Page 158: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

public CommandException(String s) { super(s);

} }

CommandTarget Interface(命令目标接口) package examples.command; interface CommandTarget {

Command executeCommand(Command aCommand) throws CommandException;

}

EJBCommandTarget(EJB命令目标) package examples.command; import javax.rmi.PortableRemoteObject; import javax.ejb.CreateException; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import java.rmi.RemoteException; public class EJBCommandTarget implements CommandTarget {

private CommandServerHome serverHome; public EJBCommandTarget() {

try {

Context ctx = new InitialContext(System.getProperties()); Object obj = ctx.lookup(“CommandServer”); System.out.println(obj); this.serverHome = (CommandServerHome) PortableRemoteObject.narrow(obj, CommandServerHome.class );

} catch (NamingException e) {

e.printStackTrace(); } catch (ClassCastException e) {

e.printStackTrace(); }

} public Command executeCommand(Command aCommand)

throws CommandException {

try {

CommandServer aCommandServer = serverHome.create(); aCommand = aCommandServer.executeCommand(aCommand); return aCommand;

} catch (Exception e)

Page 159: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

{ throw new CommandException(e);

} }

}

CommandExecutor(命令执行器) package examples.command; public class CommandExecutor {

private static EJBCommandTarget ejbTarget = new EJBCommandTarget();

//execute command, overwriting memory reference of the passed //in command to that of the new one public static Command execute(Command aCommand)

throws CommandException {

//at this point,a real implementation would use a properties file //to determine which command target (EJB, Local, Corba, etc) to //use for this particular command, as well as which deployed //CommandServer to use (in order to run commands //under different transaction configurations) return ejbTarget.executeCommand(aCommand);

} }

数据访问命令 Bean(Data Access Command Bean) 提供了抽象父类BaseReadCommand 和BaseUpdateCommand的实现,以及

InsertEmployeeCommand 和QueryEmployeeByNameCommand 类的实现。要注意

BaseReadCommand 使用了一个行集(RowSet)来简化它的实现。行集是JDBC 2.0可选包

的一部分,在JDBC 3.0 中它已经被添加到了JDBC的核心中。这里的例子使用了Sun的RowSet接口的实现CachedRowSet。

BaseReadCommand.java(基本读取命令) package examples.datacommands; import javax.sql.*; import javax.naming.InitialContext; import javax.naming.NamingException; import java.sql.*; import sun.jdbc.rowset.CachedRowSet; /** * The Super class for any data command beans Read from the * database. */ abstract class BaseReadCommand {

Page 160: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

protected PreparedStatement pstmt; protected CachedRowSet rowSet = null; private Connection con; protected BaseReadCommand ( String jndiName, String statement ) throws DataCommandException {

InitialContext ctx = null; try {

ctx = new InitialContext(); DataSource ds = (javax.sql.DataSource) ctx.lookup(jndiName); con = ds.getConnection(); pstmt = con.prepareStatement(statement);

} catch (NamingException e) {

throw new DataCommandException(e.getMessage()); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} public void execute() throws DataCommandException {

try {

rowSet = new CachedRowSet(); rowSet.populate(pstmt.executeQuery()); rowSet.beforeFirst(); this.release();

} catch (SQLException e) { throw new DataCommandException(e.getMessage()); }

} public boolean next() throws DataCommandException {

try {

return rowSet.next(); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} private void release() throws SQLException {

if (pstmt != null) pstmt.close(); if (con != null) con.close();

} }

BaseUpdateCommand.java(基本更新命令)

Page 161: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

package examples.datacommands; import javax.sql.*; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.ejb.EJBException; import java.sql.*; /** * The Super class for any data command beans that Create, Update or * Delete. This class is reusable across projects, all proj. specific data * (Datasource JDNI and SQl String) are left to the subclasses */ abstract class BaseUpdateCommand {

protected PreparedStatement pstmt; private Connection con; protected BaseUpdateCommand ( String jndiName, String statement ) throws DataCommandException {

InitialContext ctx = null; try {

ctx = new InitialContext(); DataSource ds = (javax.sql.DataSource) ctx.lookup(jndiName); con = ds.getConnection(); pstmt = con.prepareStatement(statement);

} catch (NamingException e) {

throw new DataCommandException(e.getMessage()); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} public int execute() throws DataCommandException {

try {

//execute update, return the rowcount int updateCount = pstmt.executeUpdate(); this.release(); return updateCount;

} catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} private void release() throws SQLException {

if (pstmt != null) pstmt.close(); if (con != null) con.close();

} }

InsertEmployeeCommand.java(插入Employee的命令)

Page 162: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

package examples.datacommands; import java.sql.*; import javax.sql.*; /** * InsertEmployeeCommand, this class * is the usecase specific Command bean that * an application developer would write. */ public class InsertEmployeeCommand extends BaseUpdateCommand {

static String statement = “insert into Employees (EMPLOYEEID, NAME, EMAIL) values (?,?,?)”;

static final String dataSourceJNDI = “bookPool”; /** * Passes parent class the usecase specific sql statement to use */ protected InsertEmployeeCommand() throws DataCommandException {

super(dataSourceJNDI, statement); } public void setEmail(String anEmail) throws DataCommandException {

try {

pstmt.setString(3, anEmail); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} public void setId(int id) throws DataCommandException {

try {

pstmt.setInt(1, id); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} public void setName(String aName) throws DataCommandException {

try {

pstmt.setString(2, aName); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} }

QueryEmployeeByName.java(用名字查询Employee)

Page 163: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

package examples.datacommands; import java.sql.*; import javax.sql.*; /** * A usecase specific querying object **/ public class QueryEmployeeByNameCommand extends BaseReadCommand {

static final String statement = “select EMPLOYEEID, NAME, EMAIL from Employees where NAME = ?”;

static final String dataSourceJNDI = “bookPool”; protected QueryEmployeeByNameCommand() throws DataCommandException {

super(dataSourceJNDI, statement); } public String getEmail() throws DataCommandException {

try {

return rowSet.getString(3); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} public int getId() throws DataCommandException {

try {

return rowSet.getInt(1); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} public String getName() throws DataCommandException {

try {

return rowSet.getString(2); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} public void setName(String aName) throws DataCommandException {

try {

pstmt.setString(1, aName); } catch (SQLException e) {

throw new DataCommandException(e.getMessage()); }

} }

Page 164: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

Dual Persistent Entity Bean(双重持久性实体 Bean) 包括了银行账户实体 bean 的继承关系和发布描述符。这些类可以被编译,然后可以通

过交换发布描述符来以CMP或BMP的形式被发布。

Account Deployment Descriptor for CMP(Account的CMP发布描述符) <ejb-jar>

<enterprise-beans> <entity>

<ejb-name>dualPersistent</ejb-name> <home>examples.dualpersistent.AccountHome</home> <remote>examples.dualpersistent.Account</remote> <ejb-class>examples.dualpersistent.AccountCMPBean</ejb-class> <persistence-type>Container</persistence-type> <prim-key-class>java.lang.String</prim-key-class> <reentrant>False</reentrant> <cmp-version>2.x</cmp-version> <abstract-schema-name>AccountBean</abstract-schema-name> <cmp-field>

<field-name>accountId</field-name> </cmp-field> <cmp-field>

<field-name>balance</field-name> </cmp-field> <primkey-field>accountId</primkey-field> <query>

<query-method> <method-name>findBigAccounts</method-name> <method-params>

<method-param>double</method-param> </method-params>

</query-method> <ejb-ql>

<![CDATA[FROM AccountBean AS a WHERE a.balance> ?1]]> </ejb-ql>

</query>

</entity> </enterprise-beans> <assembly-descriptor>

<container-transaction> <method>

<ejb-name>dualPersistent</ejb-name> <method-intf>Remote</method-intf> <method-name>*</method-name>

</method> <trans-attribute>Required</trans-attribute>

</container-transaction> </assembly-descriptor>

</ejb-jar>

Page 165: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

Account Deployment Descriptor for BMP(Account的BMP发布描述符) <ejb-jar>

<enterprise-beans> <entity>

<ejb-name>dualPersistent</ejb-name> <home>examples.dualpersistent.AccountHome</home> <remote>examples.dualpersistent.Account</remote> <ejb-class>examples.dualpersistent.AccountBMPBean</ejb-class> <persistence-type>Bean</persistence-type> <prim-key-class>java.lang.String</prim-key-class> <reentrant>False</reentrant> <resource-ref>

<res-ref-name>jdbc/ejbPool</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth>

</resource-ref> </entity>

</enterprise-beans> <assembly-descriptor>

<container-transaction> <method>

<ejb-name>dualPersistent</ejb-name> <method-intf>Remote</method-intf> <method-name>*</method-name>

</method> <trans-attribute>Required</trans-attribute>

</container-transaction> </assembly-descriptor>

</ejb-jar>

Account Remote Interface(Account的Remote接口) package examples.dualpersistent; import java.rmi.RemoteException; import javax.ejb.EJBObject; public interface Account extends EJBObject {

public double balance() throws RemoteException; public double deposit(double amount) throws RemoteException; public double withdraw(double amount) throws

ProcessingErrorException, RemoteException; }

Account Home Interface(Account的Home接口) package examples.dualpersistent; import javax.ejb.*; import java.rmi.RemoteException; import java.util.Collection; public interface AccountHome extends EJBHome {

Page 166: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

public Account create(String accountId, double initialBalance) throws CreateException, RemoteException;

public Account findByPrimaryKey(String primaryKey) throws FinderException, RemoteException;

public Collection findBigAccounts(double balanceGreaterThan) throws FinderException, RemoteException;

}

Account CMP Bean Superclass(Account的CMP Bean的父类) package examples.dualpersistent; import java.io.Serializable; import java.util.Enumeration; import java.util.Vector; import javax.ejb.*; import javax.naming.*; import javax.sql.DataSource; abstract public class AccountCMPBean implements EntityBean {

protected EntityContext ctx; /** * container managed fields */ abstract public String getAccountId(); abstract public double getBalance(); abstract public void setAccountId(String val); abstract public void setBalance(double val); /** * Developer implemented Business Methods */ public double balance() {

return getBalance(); } public double deposit(double amount) {

setBalance(getBalance() + amount); return getBalance();

} public double withdraw(double amount)

throws ProcessingErrorException {

if (amount > getBalance()) {

throw new ProcessingErrorException(“Attempt to withdraw too much”);

} setBalance(getBalance() - amount); return getBalance();

} public String ejbCreate(String accountId, double initialBalance)

throws CreateException {

setAccountId(accountId);

Page 167: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

setBalance(initialBalance); return null;

} /** * Container required methods - implemented by the CMP engine */ public AccountCMPBean() {} public void ejbActivate() {} public void ejbLoad() {} public void ejbPassivate() {} public void ejbPostCreate(String accountId,double initialBalance){} public void ejbRemove() throws RemoveException {} public void ejbStore() {} /** * The usual Plumbing */ public void setEntityContext(EntityContext ctx) {

this.ctx = ctx; } public void unsetEntityContext() {

this.ctx = null; }

}

Account BMP Bean Subclass(Account的BMP Bean的父类) package examples.dualpersistent; import java.io.Serializable; import java.util.*; import javax.ejb.*; import javax.naming.*; import java.sql.*; import javax.sql.*; public class AccountBMPBean extends AccountCMPBean implements EntityBean

{ private String accountId; private double balance; public String getAccountId() {

return accountId; } public double getBalance() {

return balance; } public void setAccountId(String val) {

this.accountId = val; } public void setBalance(double val)

Page 168: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

{ this.balance = val;

} public String ejbCreate(String accountId, double initialBalance)

throws CreateException {

//delegate to super class for validation checks, etc. super.ejbCreate(accountId, initialBalance); Connection con = null; PreparedStatement ps = null; try {

con = getConnection(); ps = con.prepareStatement(“insert into Accounts (id,

balance) values (?, ?)”); ps.setString(1, accountId); ps.setDouble(2, balance); if (ps.executeUpdate() != 1) {

throw new CreateException(); } return accountId;

} catch (SQLException sqe) {

throw new CreateException(); } finally {

try {

if (ps != null) ps.close(); if (con != null) con.close();

} catch (Exception e) {

throw new EJBException(e); }

} } public Collection ejbFindBigAccounts(double balanceGreaterThan) {

Connection con = null; PreparedStatement ps = null; try {

con = getConnection(); ps = con.prepareStatement(“select id from Accounts

where balance > ?”); ps.setDouble(1, balanceGreaterThan); ps.executeQuery(); ResultSet rs = ps.getResultSet(); Vector v = new Vector(); String pk; while (rs.next()) {

pk = rs.getString(1); v.addElement(pk);

} return v;

} catch (SQLException e) {

throw new EJBException(e);

Page 169: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

} finally {

try {

if (ps != null) ps.close(); if (con != null) con.close();

} catch (Exception e) {

throw new EJBException(e); }

} } public String ejbFindByPrimaryKey(String pk)

throws ObjectNotFoundException {

Connection con = null; PreparedStatement ps = null; try {

con = getConnection(); ps = con.prepareStatement(“select balance from

Accounts where id = ?”); ps.setString(1, pk); ps.executeQuery(); ResultSet rs = ps.getResultSet(); if (rs.next())

balance = rs.getDouble(1); else

throw new ObjectNotFoundException(); } catch (SQLException sqe) {

throw new EJBException(sqe); } finally {

try {

if (ps != null) ps.close(); if (con != null) con.close();

} catch (Exception e) {

System.out.println(“Error closing JDBC resourcest: “ +e);

throw new EJBException(e); }

} return pk;

} public void ejbLoad() {

Connection con = null; PreparedStatement ps = null; accountId = (String) ctx.getPrimaryKey(); try {

con = getConnection(); ps = con.prepareStatement(“select balance from

Accounts where id = ?”); ps.setString(1, accountId); ps.executeQuery(); ResultSet rs = ps.getResultSet(); if (rs.next())

balance = rs.getDouble(1);

Page 170: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

else throw new NoSuchEntityException();

} catch (SQLException sqe) {

throw new EJBException(sqe); } finally {

try {

if (ps != null) ps.close(); if (con != null) con.close();

} catch (Exception e) {

System.out.println(“Error closing JDBC resourcest: “ +e);

throw new EJBException(e); }

} } public void ejbPostCreate(String accountId, double initialBalance) { } public void ejbRemove() {

Connection con = null; PreparedStatement ps = null; try {

con = getConnection(); accountId = (String) ctx.getPrimaryKey(); ps = con.prepareStatement(“delete from Accounts where

id =?”); ps.setString(1, accountId); if (!(ps.executeUpdate() > 0)) {

throw new NoSuchEntityException(); }

} catch (SQLException e) {

throw new EJBException(e); }

} public void ejbStore() {

Connection con = null; PreparedStatement ps = null; try {

con = getConnection(); ps = con.prepareStatement(“update Accounts set balance

= ? where id = ?”); ps.setDouble(1, balance); ps.setString(2, accountId); if (!(ps.executeUpdate() > 0))

throw new NoSuchEntityException(); } catch (SQLException sqe) {

throw new EJBException(sqe); } finally {

try

Page 171: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

{ if (ps != null) ps.close(); if (con != null) con.close();

} catch (Exception e) {

System.out.println(“Error closing JDBC resourcest: “ +e);

throw new EJBException(e); }

} } private Connection getConnection() throws SQLException {

InitialContext ctx = null; try {

ctx = new InitialContext(); DataSource ds = (javax.sql.DataSource) ctx.lookup(“ejbPool”); return ds.getConnection();

} catch (NamingException e) {

throw new EJBException(e); }

} }

Processing Error Exception(处理错误异常) package examples.dualpersistent; public class ProcessingErrorException extends Exception {

public ProcessingErrorException() {} public ProcessingErrorException(String message) {super(message);

}

EJB Home Factory(EJB Home 工厂) 这里表示的是一个EJB Home 工厂的例子。

Simple EJB Home Factory(简单的EJB Home工厂) package com.portal.util; import javax.ejb.*; import java.rmi.*; import javax.rmi.*; import java.util.*; import javax.naming.*; /** * EJB Home Factory, maintains a simple hashmap cache of EJBHomes * For a production implementations, exceptions such as NamingException * can be wrapped with a factory exception to futher simplify

Page 172: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

* the client. */ public class EJBHomeFactory {

private Map ejbHomes; private static EJBHomeFactory aFactorySingleton; Context ctx; /** * EJBHomeFactory private constructor. */ private EJBHomeFactory() throws NamingException {

ctx = new InitialContext(); this.ejbHomes = Collections.synchronizedMap(new HashMap());

} /* * Returns the singleton instance of the EJBHomeFactory * The sychronized keyword is intentionally left out the * as I don’t think the potential to intialize the singleton * twice at startup time (which is not a destructive event) * is worth creating a sychronization bottleneck on this * VERY frequently used class, for the lifetime of the * client application. * * Alternatively, you can sychronize this method, OR you can * simply Intialize the hashMap and factory using static Intializers. */ public static EJBHomeFactory getFactory() throws

HomeFactoryException {

try {

if ( EJBHomeFactory.aFactorySingleton == null ) {

EJBHomeFactory.aFactorySingleton = new EJBHomeFactory();

} } catch (NamingException e) {

throw new HomeFactoryException(e); } return EJBHomeFactory.aFactorySingleton;

} /** * Lookup and cache an EJBHome object using a home class. * Assumes that the JNDI name of the EJB Home being looked for * is the same as the fully qualified class name of the * same EJB Home. * If EJB-REF tags are being used externally, then the classname * of the EJB Home can be mapped to the actual JNDI name of the * deployed bean transaprently at deployment time. * If EJB-REF tags are not used, then the EJB’s must be deployed * with JNDI names equal to their fully qualified home interfaces. */ public EJBHome lookUpHome(Class homeClass)

throws HomeFactoryException {

EJBHome anEJBHome; anEJBHome = (EJBHome) this.ejbHomes.get(homeClass);

Page 173: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

try {

if(anEJBHome == null) {

anEJBHome = (EJBHome)PortableRemoteObject.narrow (ctx.lookup (homeClass.getName()), homeClass);

this.ejbHomes.put(homeClass, anEJBHome); }

} catch (ClassCastException e) {

throw new HomeFactoryException(e); } catch (NamingException e) {

throw new HomeFactoryException(e); } return anEJBHome;

} /** * Lookup and cache an EJBHome object. * This ‘alternate’ implementation delegates JNDI name knowledge * to the client. It is included here for example only. */ public EJBHome lookUpHome(Class homeClass, String jndiName)

throws HomeFactoryException {

EJBHome anEJBHome; anEJBHome = (EJBHome) this.ejbHomes.get(homeClass); try {

if(anEJBHome == null) {

System.out.println(“finding HOME for first time”);

anEJBHome =(EJBHome)PortableRemoteObject.narrow

( ctx.lookup (jndiName), homeClass);

this.ejbHomes.put(homeClass, anEJBHome); }

} catch (ClassCastException e) {

throw new HomeFactoryException(e); } catch (NamingException e) {

throw new HomeFactoryException(e); } return anEJBHome;

} }

Page 174: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

Business Delegate(业务代理) 包括了一个业务代理的实现,这个业务代理包装了一个有状态会话Bean。这些业务代理比

包装无状态会话Bean的业务代理稍微复杂一点,因为它们需要一个EJBObject的句柄

(handler),以避免因被这样的Servlet引擎序列化而造成的丢失:这个Servlet引擎可

能会钝化(passivate)它的HTTPSession缓存,或者可能会尝试序列化它的

HTTPSession的副本以支持在一个集群中的session复制。 在第8章的无状态代理的例子代码的基础上所修改和添加的代码用粗体表示。在业务代理

的SFSB版本中所作的主要修改只是在每一个对EJB的业务方法的调用之前使用一个getEJB()方法。这样做能够保证EJBObject在序列化后仍然存在而不会丢失,从而可以从句柄中重新

创建它。同时,客户端必须记住在使用完代理时,调用remove方法,这样SFSB就可以被移去

了。 public class ForumServicesDelegate implements Serializable {

private transient TestSession sb; private Handle remoteHandle; public ForumServicesDelegate() throws DelegateException {

try {

ForumServicesHome home = (ForumServicesHome) EJBHomeFactory.getFactory().lookUpHome (ForumServicesHome.class);

this.sb = home.create(); //store a handle incase we get serialized this.remoteHandle = sb.getHandle();

} catch(Exception e) {

throw new DelegateException(); }

} //business method public long addForum(long categoryPK, String forumTitle,

String summary) throws NoSuchCategoryException,DelegateException

{ try {

return getEJB().sb.addForum(categoryPK, forumTitle, summary); } catch(CreateException e) {

throw new DelegateException(); //log errors, etc

} catch(RemoteException e) {

throw new DelegateException(); //log errors, etc

} } private ForumServices getEJB() throws DelegateException

Page 175: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

{ //test if the delegate was serialized try {

if (sb == null) {

//if so, recreate session bean reference sb = (ForumServices) PortableRemoteObject.narrow

(remoteHandle.getEJBObject(),ForumServices.class); }

} catch (ClassCastException e) {

throw new DelegateException(); } catch (RemoteException e) {

throw new DelegateException(); } return sb;

} public void remove() throws DelegateException {

//once the client is done with the //stateful delegate, allow client to call //remove, so we can tell the EJB server to //remove the SFSB try {

getEJB().remove(); } catch (RemoteException e) {

throw new DelegateException(); } catch (RemoveException e) {

throw new DelegateException(); }

} }

Sequence Blocks(序列块) 包括一个序列块模式的完整实现,它是基于Borland 公司的Jonathan Weedon 的一个

子任务的。序列实体Bean(Sequence entity bean)只暴露local接口(它只被序列会话

Bean(Sequence Session Bean)调用),而序列会话Bean将同时暴露local和remote接口。这里还包括了Ejb-jar.xml 发布描述符。 Sequence Entity Bean Local Interface(序列实体Bean的Local接口)

Page 176: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

package examples.sequencegenerator; public interface Sequence extends javax.ejb.EJBLocalObject {

public int getNextKeyAfterIncrementingBy(int blockSize); } Sequence Entity Bean Local Home Interface(序列实体Bean的Local Home接口) package examples.sequencegenerator; public interface SequenceLocalHome extends javax.ejb.EJBLocalHome {

Sequence create(String name) throws javax.ejb.CreateException; Sequence findByPrimaryKey(String name) throws

javax.ejb.FinderException; }

Sequence Entity Bean Code(序列实体Bean的实现类代码) package examples.sequencegenerator; import javax.ejb.*; abstract public class SequenceBean implements EntityBean {

public int getNextKeyAfterIncrementingBy(int blockSize) {

this.setIndex(this.getIndex()+ blockSize); return this.getIndex();

} public String ejbCreate(String name) {

this.setName(name); this.setIndex(0); return name;

} abstract public int getIndex(); abstract public String getName(); abstract public void setIndex(int newIndex); abstract public void setName(java.lang.String newName); public void ejbActivate() {} public void ejbLoad() {} public void ejbPassivate() {} public void ejbPostCreate(String name) {} public void ejbRemove() {} public void ejbStore() {} public void setEntityContext(EntityContext unused) {} public void unsetEntityContext() {}

} Sequence Session Remote Interface(序列会话Bean的Remote接口)

Page 177: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

package examples.sequencegenerator; import java.rmi.*; public interface SequenceSession extends javax.ejb.EJBObject {

public int getNextNumberInSequence(String name) throws RemoteException;

}

Sequence Session Home Interface(序列会话Bean的Home接口) package examples.sequencegenerator; import javax.ejb.*; import java.rmi.*; public interface SequenceSessionHome extends javax.ejb.EJBHome {

SequenceSession create() throws CreateException, RemoteException; }

Sequence Session Local Interface(序列会话Bean的Local接口) package examples.sequencegenerator; public interface SequenceSessionLocal extends javax.ejb.EJBLocalObject {

public int getNextNumberInSequence(String name); }

Sequence Session Local Home Interface(序列会话Bean的Local Home接口) package examples.sequencegenerator; import javax.ejb.*; import javax.naming.*; public interface SequenceSessionLocalHome extends javax.ejb.EJBLocalHome {

SequenceSessionLocal create() throws CreateException; }

Sequence Session Bean Implementation(序列会话Bean的实现类) package examples.sequencegenerator; import javax.ejb.*; import javax.naming.*; public class SequenceSessionBean implements javax.ejb.SessionBean {

private class Entry {

Sequence sequence; int last;

Page 178: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

}; private java.util.Hashtable _entries = new java.util.Hashtable(); private int _blockSize; private int _retryCount; private SequenceLocalHome _sequenceHome; public int getNextNumberInSequence(String name) {

try {

Entry entry = (Entry) _entries.get(name); if (entry == null) {

// add an entry to the sequence table entry = new Entry(); try {

entry.sequence = _sequenceHome.findByPrimaryKey(name);

} catch (javax.ejb.FinderException e) {

//if we couldn’t find it, //then create it... entry.sequence =

_sequenceHome.create(name); } _entries.put(name, entry);

} if (entry.last % _blockSize == 0) {

for (int retry = 0; true; retry++) {

try {

entry.last =entry.sequence.getNextKeyAfterIncrementingBy(_blockSize);

break; } catch

(javax.ejb.TransactionRolledbackLocalException e)

{ if (retry < _retryCount) {

// we hit a concurrency // exception, so try again... continue;

} else {

// we tried too many times, //so fail... throw new

javax.ejb.EJBException(e); }

} }

} return entry.last++;

Page 179: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

} catch (javax.ejb.CreateException e) {

throw new javax.ejb.EJBException(e); }

} public void setSessionContext( javax.ejb.SessionContext

sessionContext) {

try { Context namingContext = new InitialContext(); _blockSize = ((Integer)namingContext.lookup

(“java:comp/env/blockSize”)).intValue(); _retryCount = ((Integer) namingContext.lookup

(“java:comp/env/retryCount”)).intValue(); _sequenceHome = (SequenceLocalHome)

namingContext.lookup(“SequenceLocalHome”); } catch(NamingException e) {

throw new EJBException(e); }

} public void ejbActivate() {} public void ejbCreate() {} public void ejbPassivate() {} public void ejbRemove() {}

}

Sequence Session and Entity ejb-jar.xml(序列会话和实体的ejb-jar.xml) <?xml version=”1.0”?> <!DOCTYPE ejb-jar PUBLIC ‘-//Sun Microsystems,

Inc.//DTD Enterprise JavaBeans 2.0//EN’ ‘http://java.sun.com/dtd/ejb-jar_2_0.dtd’>

<ejb-jar> <enterprise-beans>

<entity> <ejb-name>Sequence</ejb-name> <local-home>

examples.sequencegenerator.SequenceLocalHome</localhome> <local>examples.sequencegenerator.Sequence</local> <ejb-class>examples.sequencegenerator.SequenceBean</ejb-class> <persistence-type>Container</persistence-type> <prim-key-class>java.lang.String</prim-key-class> <reentrant>False</reentrant> <cmp-version>2.x</cmp-version> <abstract-schema-name>SequenceBean</abstract-schema-name> <cmp-field>

<field-name>index</field-name> </cmp-field> <cmp-field>

<field-name>name</field-name> </cmp-field> <primkey-field>name</primkey-field>

<env-entry>

<env-entry-name>datasourceName</env-entry-name> <env-entry-type>java.lang.String</env-entry-type> <env-entry-value>bookPool</env-entry-value>

</env-entry>

Page 180: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

<resource-ref>

<res-ref-name>jdbc/bookPool</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth>

</resource-ref> </entity> <session>

<ejb-name>SequenceSession</ejb-name> <home>examples.sequencegenerator.SequenceSessionHome</home> <remote>examples.sequencegenerator.SequenceSession</remote> <localhome>examples.sequencegenerator.SequenceSessionLocalHome<

/local-home> <local>examples.sequencegenerator.SequenceSessionLocal</local> <ejb-class>

examples.sequencegenerator.SequenceSessionBean</ejbclass> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> <env-entry>

<description /> <env-entry-name>retryCount</env-entry-name> <env-entry-type>java.lang.Integer</env-entry-type> <env-entry-value>5</env-entry-value>

</env-entry> <env-entry>

<description /> <env-entry-name>blockSize</env-entry-name> <env-entry-type>java.lang.Integer</env-entry-type> <env-entry-value>10</env-entry-value>

</env-entry> </session>

</enterprise-beans>

<assembly-descriptor> <container-transaction>

<method> <ejb-name>Sequence</ejb-name> <method-name>getNextKeyAfterIncrementingBy</method-name>

</method> <trans-attribute>RequiresNew</trans-attribute>

</container-transaction> <container-transaction>

<method> <ejb-name>SequenceSession</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute>

</container-transaction> </assembly-descriptor>

</ejb-jar>

Page 181: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

Stored Procedures for Auto-Generated Keys(自动生成键的存储过程)

在这里我们有一个存储过程的例子,它将在同一个数据库调用中向数据库中插入一行并返

回自动生成的主键。这个主键需要从ejbCreate方法中返回,就像在规范中强制规定的那样。

这个存储过程使用了一个被称为accountID 的 Oracle 序列(sequence)来生成主键。

InsertAccount Stored Procedure for Oracle(Oracle 的 InsertAccount 储存

过程) create or replace procedure insertAccount

(owner IN varchar, bal IN integer, newid OUT integer)

AS BEGIN

insert into accounts (id, ownername, balance) values (accountID.nextval, owner, bal) returning id into newid;

END;

Page 182: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

参 考 Web 站点 MartinFowler.com. “Information Systems Architecture.” Available at:

www.martinfowler.com/j2ee/blueprints/index.html Sun Microsystems. “J2EEBlueprints.” Available at:

http://java.sun.com/j2ee/blueprints/index.html TheServerSide.com. “Patterns Repository.” Available at:

www.theserverside.com/patterns

书与论文 Alexander, Christopher, Sara Ishikawa, and Murray Silverstein. 1977. A Pattern

Language: Towns, Buildings, Construction. Oxford University Press. Alur, D., J. Crupi, and D. Malks. 2001. Core J2EE Patterns. Prentice-Hall. Brown, K. January 26, 2000. “Limit Parameters for EJB Creates.” Portland

Pattern Repository. Available at: www.c2.com/cgi/wiki?LimitParametersForEjbCreates

Brown, K. May 5, 2001. “What’s a Controller Anyway?” Portland Pattern Repository.Available at: www.c2.com/cgi/wiki?WhatsaControllerAnyway

Brown, K., and B. Whitenack. 1995. “Crossing Chasms: A Pattern Language for Object-RDBMS Integration.” Pattern Languages of Program Design 2, Vlissedes, Coplien, and Kerth, eds. Addison-Wesley.

Carey, J., B. Carlson, and T. Graser. 2000. San Francisco Design Patterns. Addison-Wesley.

Coad, P. 1990. Object-Oriented Analysis. Yourdon Press. Cockburn, A. 1995. “Prioritizing Forces in Software Design.” Pattern Languages

of Program Design 2, Vlissedes, Coplien, and Kerth, eds. Addison-Wesley. Cockburn, A. 2000. Writing Effective Use Cases. Addison-Wesley. “Exception

Patterns.” Portland Pattern Repository. August 19, 2001. (Multiple contributors).Available at: www.c2.com/cgi/wiki?ExceptionPatterns

Fowler, M. 1999. Refactoring: Improving the Design of Existing Code. Addison-Wesley.

Fowler, M., and R. Mee. 2001. 2001 J2EE Summit. Crested Butte, CO. Fowler, M., with K. Scott. 1997. UML Distilled. Addison-Wesley. Gamma, E., R. Helm, R. Johnson, and J. Vlissides. 1995. Design Patterns:

Elements of Reusable Object-Oriented Software. Addison-Wesley. Gamma, E., et al. 1994. Design Patterns: Elements of Reusable Object-Oriented

Software. Addison-Wesley. Hunt, A., and D. Thomas. 1999. The Pragmatic Programmer. Addison-Wesley. Jeffries, R., et al. 2000. Extreme Programming Installed. Addison-Wesley. Johnston, J. 2000. The Complete Idiot’s Guide to Psychology. Alpha Books.

Page 183: Floyd Marinescu Floyd EJB Second Editionread.pudn.com/downloads74/ebook/272500/EJB.Design... · Floyd Marinescu Floyd EJB Second Edition ... 译者序

Kassem, N. 2000. Designing Enterprise Applications with the Java 2 Platform, Enterprise Edition. Addison-Wesley.

Maister, D. 1997. True Professionalism. Touchstone. Matena, V., and B. Stearns. 2001. Applying Enterprise Java Beans. Addison-

Wesley. Roman, E. 2002. Mastering EJB. John Wiley & Sons. Smith, D. August 8, 2001. “Technical Debt.” Portland Pattern Repository.

Available at: www.c2.com/cgi/wiki?TechnicalDebt

Stafford, Randy. September 2, 2001. “Object Relational Impedance Mismatch.” Portland Pattern Repository. Available at: www.c2.com/cgi/wiki?ObjectRelationalImpedanceMismatch