J2EE指南
序:
1993年8月,我加入了太阳公司--被称之为第一人的小公司。我之所以知道这个公司是因为我的一些同事跳槽到了太阳公司。我加入太阳公司的主要原因是因为我喜欢他卡通似的用户界面。太阳公司正在发展,其界面有一个昵称--Duke。
"第一人"的首次样品演示名曰:七星(Star 7),它是一个能让房主通过触摸屏来实现远程控制的产品。在我到哪儿的时候,他们正在为视频点播做演示。
这个妙极的视频点播演示是由加利福尼亚的一个称之为巨大图片工作室做的。演示产品使用当时称之为Oak的编程语言做的。我的第一个任务就是帮助Oak语言的创造者--James Gosling 写语言规范。然而我真正想做的是对象为普通程序员的面向任务的文档。到了1994年7月,"第一人"陷入混乱,失去了向一些有线网络公司说明视频点播方案是消费者需求的说服力。
面向Internet的编程
1994年秋天我返回工作时,这个公司的景象已经完全改变。他们决定Oak语言--跨平台的、安全的、易传输的代码--时理想的面向Internet的语言。同时他们在制作名为WebRunner的浏览器,它具有在Internet上传输Oak代码--他们称之为Applet--的能力。
我开始写一些指南来帮助程序员使用applets。1995年,当WebRunner浏览器首次出版时,指南只是包含浏览器的一小部分,这个指南就是J2EE Tutorial的前身。该指南时第一次包含applets的文档,看起来有点像The Java Tutorial。事实上,The Java Tutorial也许仍有些能在最初出版的指南中找到。由于没有HTML工具,因此不得不手工完成。告诉你,为文档做手工代码连接是无趣的,哪怕是一份小文档。这个编程语言的名称由Oak变为Java,浏览器的名称由WebRunner换为HotJava
Mary的加盟
1995年早些时候,我们招收了一个合同编制员--Mary Campione。她和我在后来的工作互助中相识。Mary的工作是帮助程序员使用平台特色如线程。我们很快意识到我们的工作如此相似,我们很快就在一起为Java平台程序员指南工作。
1995年5月18日,Mary Campione和我出版了第一版指南,我们称之为The Java Programmer's Guide。它是一个不完全的草本,但它提供了为程序员在Java平台胜编程的一些信息。紧接着第二个星期,太阳公司正式在一个展示会上宣布了称之为SunWorld的Java平台。
最棒的是NetScape同意在他们的浏览器上支持applets。
在接下来的几个月里,Mary和我继续增加、校订《编程指南》,我们密切的一起工作。
到了1995年底,第一批Java系列的书籍完成。这一系列书籍主要由原"第一人"的成员编写,Addison-Wesley出版。
The J2EE Tutorial
现在又出现了一个新的平台,新的指南。Java2企业版的成功是显著的。开发者们都强烈要求提供有关使用Java平台编写服务端应用程序的资料。同原来的指南一样,这是一个多实例、易使用的并作为J2EE平台开发的参考。
目录
准备工作 8
第1章 总 括 9
一.分布式得多层应用程序 9
二.J2EE容器 11
三.打包J2EE组件 12
四.开发者角色 13
五.本书所用的软件 14
第2章 动手做一个EJB 17
一.准备工作 18
二.创建J2EE应用程序 19
三.创建企业Bean 19
四.创建J2EE应用程序客户端 22
五.创建Web客户端 25
六.设置企业Bean的JNDI名 27
七.部署J2EE应用程序 28
八.运行J2EE应用程序客户端 29
九.运行Web客户端 29
十.修改J2EE应用程序 30
十一。常见问题和解决方法 31
第3章 企业Bean 35
1,企业Bean概述 36
2,会话Bean 36
3,EntityBean 38
4,Message-Driven Bean 40
5.定义客户端访问接口 42
6,企业Bean的"内容" 45
7,企业Bean的命名约定 46
8,企业Bean的生存周期 46
第4章 有状态会话Bean示例 51
1.购物车会话Bean CartEJB 51
二 其他的企业Bean特性 58
第5章 BMP的例子 62
一.SavingsAccountEJB 62
二.用deploytool部署BMP实现的实体Bean 74
三.为BMP映射表间关系 74
四.BMP的主键 85
五.异常处理 88
第6章 CMP的例子 89
一 RosterApp应用概述 90
二 layerEJB代码分析 90
三.RosterApp配置说明 95
四 RosterApp中的方法调用 102
五 运行RosterApp应用程序 109
六 用deploytool工具部署CMP实现的实体Bean 110
七 CMP的主键 110
第7章 一个消息驱动Bean的例子 113
一.例子应用程序介绍 113
二.J2EE应用程序客户端 114
三.消息驱动Bean类 115
四.运行本例子 116
五.用deploytool部署消息驱动Bean 117
六.用deploytool配置JMS客户端 118
第8章 EJB查询语言 120
一.术语 120
二.简单语法 121
三.查询例子 121
四.全部语法 124
五.EJB QL的限制 137
第9章 网络客户端及组件 139
第11章 JSP技术 165
第12章 JSP页面中的JavaBean组件 178
第13章 在JSP页面中自定义标签 182
第14章 事务 202
一.什么是事务 202
二.容器管理事务 203
三.Bean管理事务 208
四.企业Bean事务摘要 211
五.事务超时 211
六.隔离级别 212
七.更新多个数据库 212
八.Web 组件事务 214
第15章 安全 215
一.纵览 215
二.安全角色 216
三.Web层安全 217
四.EJB层安全 219
五.应用程序客户端层安全 220
六.EIS(Enterprise Information System)层安全 221
七.传递安全身份 223
八.J2EE用户、域和组 224
九.安装服务器证书 225
第16章 资源连接 227
一.JNDI名和资源引用 227
二.数据库连接 230
三.邮件服务连接 232
四.URL资源连接 234
第17章 DUKE的银行应用程序 236
第一部分 介绍
准备工作
J2EE指南对于广大的Java程序员来说是一份不可或缺的资料了。这篇导论对于初次碰到J2EE的程序员来说有着同样的作用。它与Java指南一样都是一例子为中心。
谁应该使用这指南
这篇指南是为爱好开发和部署J2EE应用程序的程序员准备的。它包括了组成J2EE平台的技术以及描述如何开发J2EE组件并部署在J2EE软件开发包上。
这篇指南不是为J2EE服务器及工具供应商准备的,它没有解释如何实现J2EE结构,也没有解释J2EE软件包。J2EE规范描述了J2EE结构并可以从下面的网址下载:
http://java.sun.com/j2ee/docs.html#specs
关于例子
这篇指南包含很多完整的,可运行的例子。你可以看看例子列表(P445)。
理解例子的准备工作
为了理解这些例子,你需要熟练Java语言,SQL,及关系型数据库的概念,这些非常重要的主题在Java指南中,下面的表格已列了出来。
主题 Java指南
JDBC http://java.sun.com/docs/books/tutorial/jdbc
Threads http://java.sun.com/docs/books/tutorial/essential/threads
JavaBeans http://java.sun.com/docs/books/tutorial/javabean
Security http://java.sun.com/docs/books/tutorial/security1.2
例子下载
如果你一再线看了这些文档,并且你向变异并运行这些例子,可以从下面的网址下载:
http://java.sun.com/j2ee/download.html#tutorial
如果你安装了这些捆绑的例子,例子的源代码在目录j2eetutorial/examples/src下,子目录ejb下是EJB的例子。这些捆绑的例子也包括j2ee EAR,位于目录j2eetutorial/examples/ears下。
如何编译并运行例子
这篇指南文档是J2EE SDK 1.3版,要编译、运行例子你需要J2EE SDK 1.3及Java2标准版,你可以从下面的网址下载J2EE1.3:
http://java.sun.com/j2ee/download.html#sdk
下载J2SE1.3.1的网址:
http://java.sun.com/j2se/1.3/
这些例子附有一个配置文件--ant1.3,一个方便的运行工具。这个工具是由Apache 软件公司Jakarta项目组开发的。可以从下面的网址下载:
http://jakarta.apache.org/builds/jakarta-ant/release/v1.3/bin
要编译例子,可按照下面的步骤来:
1. 下载并安装J2SE SDK1.3.1、J2EE SDK1.3及ant.
2. J2SE SDK1.3.1、J2EE SDK1.3及ant的安装说明如何配置环境变量。对照下表和对环境变量的配置情况。
环境变量 值
JAVA_HOME J2SE SDK的安装路径
J2EE_HOME J2EE SDK的安装路径
ANT_HOME ANT的安装路径
PATH 应该包括J2EE SDK,J2SE SDK&ANT的安装路径
3. 转到目录j2eetutorial/example.
4. 运行ant target.
相关信息
这篇指南提供了如何使用主要的组件技术在J2EE平台上运用的简明概括,要了解更多的信息可参考下面的网址:
EJB: http://java.sun.com/products/ejb
Java Servlet: http://java.sun.com/products/servlets
JSP: http://java.sun.com/products/jsp
第1章 总 括
今天,越来越多的开发人员都想编写分布式的,事务型的企业及应用程序,以及平衡速度、安全及服务器方可靠度的技术。如果你已经在这个领域工作,你应该知道在当今的快速变换及需求的电子商务及信息技术的世界里,企业应用程序需要设计、编译、产生低价位的、高速的、占用少量资源的程序。
为了缩减开发成本,快速跟踪企业应用的设计和开发,J2EE技术提供了基于组件的设计方法,开发、集成、部署应用程序。J2EE平台提供了多层分布式应用模式,使具有重用的能力,并集成了基于XML的数据交换--一个统一的安全模式及灵活的事务控制。
一.分布式得多层应用程序
J2EE平台使用多层分布式的应用模式。应用逻辑根据其功能分成多个组件,各种不同的应用组件构成分布在不同的依赖于层的机器上的J2EE程序。下面列出了位于不同层的组件
. 运行在客户机上的客户层组件
. 运行在J2EE服务器上的网络层
. 运行在J2EE服务器上的逻辑层
. 运行在EIS服务器上的企业信息层
尽管J2EE应用程序可以由三层或四层构成,J2EE应用程序通常由三层构成,因为他们分布于三个不同的位置:客户及,服务器,后台数据库服务器。通过这种方式运行的三层应用模式拓展了基于客户/服务的两层模式。
J2EE组件
J2EE应用程序由组件构成。一个J2EE组件是自包含的,与其相关的语气它组件通信的类及文件集成到J2EE应用程序的功能软件单元。J2EE规范定义了下面一些组件:
。 运行在客户端的应用客户程序及小程序。
。 运行于服务器网络的Servlet&Jsp组件。
。 运行于服务端的企业逻辑组件--EJB。
J2EE组件用Java语言编写,通过相同的方法编译。J2EE组件与标准Java类的不同之处在于J2EE组件集成到了应用程序中,证明能很好的组成,与J2EE规范兼容,并部署到负责运行、管理的J2EE服务器上。
J2EE客户端
J2EE客户端可以使网络浏览器也可以是桌面应用程序。
网络浏览器
网络客户程序由两部分组成:动态网页包含各种标记语言(HTML,XML等),它由运行于网络层的网络组件产生,浏览器从服务器接受信息并反馈到页面上。
网络客户端又称为瘦客户。瘦客户端通常不运行像查询数据库,执行复杂的业务规则,或连到合法的应用程序。当你使用瘦客户时,重量级的操作都载入到运行于J2EE服务器上的企业Bean,它能够均衡安全,速度,服务及可靠性。
小程序
网页可以包含小程序。小程序是一个较小的用java语言编写的程序,并能通过安装在浏览器上的虚拟机运行
网络组件首选API,它可以创建网络客户层,因为在客户系统中它不需要插件或安全策略文件,宁外,网络组件能提供更干净的,模块化的应用设计,因为它将程序设计与页面设计相分离。这样,页面设计人员可以进行页面设计而不需要懂Java程序设计。
应用客户端
J2EE应用客户端运行在客户上,它为用户处理任务提供了比标记语言丰富的接口。典型的是它拥有通过Swing&AWTAPI建立的图形用户界面,基于命令行的接口也是可以的。
应用客户端可以直接调用业务逻辑层的企业bean。
JavaBean组件结构
服务端及客户端也可以包含基于JavaBean组件来管理客户端与运行于服务端的组件间的数据流或服务端组件与数据库间的数据流。J2EE规范没有认为JavaBean为j2EE组件。
JavaBean组件有实例变量和get,set方法来设置、获取变量值。
网络组件
J2EE网络组件可以是servlet或jsp。Servlet是java类,它能动态处理请求及响应。Jsp页面是基于文档的,能像servlet一样执行的能允许更多的静态页面内容。
静态HTML页面及applets域网络组件绑在一起,但J2EE规范没有认为这些为网络组件。
网络层可以包含JavaBean组件来管理用户输入、发送输入道逻辑层的EJB以处理。
业务逻辑组件
业务逻辑代码是解决、达到特定业务领域的需求,如银行、零售、金融,又EJB处理的业务逻辑层。
企业Bean可以重新从存储器找回数据,如果必要并处理它,然后发送到客户程序。
总共有三种EJB:会话bean,实体bean,消息驱动bean。
会话bean代表短暂的与客户的会话,当客户结束执行时,会话bean及它的数据就消失了。与会话bean相比,实体bean代表存储在数据库的表,如果客户结束程序或服务器关闭,潜在的服务方法会将数据存储。
二.J2EE容器
瘦客户端的多层应用程序总是很难开发,因为它包括各个层的事务处理、状态管理、多线程、资源池和其他复杂底层细节等等的错综复杂的编码。但是基于组件和平台独立的J2EE平台使J2EE应用程序容易开发,因为商业逻辑被封装在可重用的组件(EJB)中。另外J2EE服务器以容器的形式为所有组件提供底层服务,因此你不必再为这些底层服务二伤脑筋,而可以专注于解决商业问题。
容器服务
容器(Container)是组件和支持组件功能的底层特定平台(如数据库)之间的接口。在运行Web组件、企业Bean或者J2EE应用程序客户端之前,你必须将它们装配到一个J2EE应用程序中,并部署它们到容器中。
装配的过程包括为J2EE应用程序的每个组件 和J2EE应用程序本身设置容器的配置信息。这些配置信息定制J2EE服务器支持的底层服务,包括安全,事务管理,Java命名和目录接口(JNDI)查找和远程连接等。下面使这些服务的精简描述:
J2EE安全模型让你配置Web组件或者企业Bean以使系统资源只被授权用户访问
J2EE事务模型让你指定属于同一个事务的多个方法以使这些方法作为一个原子操作被执行
JNDI查找服务为企业应用中的多种命名和目录服务提供统一接口使应用程序组件可以统一访问这些命名和目录服务。
J2EE远程连接模型管理客户端和企业Bean之间的底层通信。企业Bean被创建后,客户端调用它的方法就像在本地虚拟机中的调用一样。
事实上,J2EE体系结构提供可配置服务意味着同一个J2EE应用程序中的组件可以根据不同的部署环境而有不同的行为。例如,一个企业Bean的安全配置可以使它在一种产品环境中有一个级别的数据库数据访问权限,而在另一种产品环境中有不同的数据库数据访问权限。
容器也管理着很多不可配置的服务,如企业Bean和Servlet的生命周期,数据库连接池,数据持久化机制和J2EE平台API的访问权等等。尽管数据持久化机制是不可配置服务,但是J2EE体系结构允许你忽略容器管理的持久性(Container-Managed Persistence,CMP)机制在企业Bean实现中加入合适的代码,当然除非你确实需要比容器管理持久性机制提供的更多的应用程序控制权,否则使用容器管理的持久性。例如你可以用Bean管理的持久性(Bean-Managed Persistence,BMP)机制来实现自己的查找方法或者创建一个定制的数据库缓冲池。
容器类型
部署过程将J2EE应用程序安装到J2EE容器中。图1-5展示了组件在容器中的情况:
图 1-5 J2EE 服务器和容器(Container)
J2EE server(J2EE服务器)
J2EE产品的运行时服务部分。一个J2EE服务器提供EJB容器(EJB Container)和Web容器(Web Container)。
EJB容器
管理J2EE应用程序中企业Bean的运行。企业Bean和它们的容器在J2EE服务其中运行。
Web容器
管理J2EE应用程序中JSP页面和Servlet组件的运行。Web组件和容器也在J2EE服务其中运行。
Application client container(应用程序客户端容器)
管理应用程序客户端组件的运行。应用程序客户端和它的容器运行在客户机。
Applet container(Applet容器)
管理Applet的运行。由在客户端运行的浏览器和Java插件组成。
三.打包J2EE组件
J2EE组件都分开打包然后组装成一个J2EE应用程序来部署。每一个组件用到的文件如GIF、HTML文件或者服务器端的实用类文件等,再加上一个部署描述符文件都被装配到一个模块然后加入到J2EE应用程序中。一个J2EE应用程序由一个或多个企业Bean组件模块、Web组件模块和应用程序客户端组件模块组成。最终企业解决方案会根据设计需要由一个或者多个J2EE应用程序组成。
J2EE应用程序和它的每一个组成模块都有自己的部署描述符文件。部署描述符文件是描述组件部署配置信息的XML文件。例如,一个企业Bean的部署描述符文件声明了企业Bean的事物属性和安全授权。由于这种简单的声明形式,可以在不修改Bean的源代码的情况下修改这些信息。运行时,J2EE服务器读取部署描述符并根据读取信息在组件上执行相应动作。
J2EE应用程序和它的所有模块都被打包到一个EAR(Enterprise Archive)文件中。EAR文件是后缀为.ear的标准JAR(Java Archive)文件。(在J2EE SDK提供的GUI版的应用程序部署工具中,需要先创建一个EAR文件然后将JAR和WAR(Web Archive)文件加到EAR中。而命令版本的部署工具正好相反。)下面是这些打包文件的描述:
EJB JAR文件包含一个部署描述符文件,企业Bean文件和用到的其他文件
应用程序客户端JAR文件包含一个部署描述符文件,客户端类文件和其他用到的文件
WAR文件包含一个部署描述符文件,Web组件(JSP和Servlet)文件和用到的其他文件
使用模块和EAR文件的打包方式,使用相同的组件装配成不同的应用程序成为可能,而且不需要额外的编码,只需要将用到的J2EE模块的任意组合装配成一个J2EE EAR文件。
四.开发者角色
可重用的的模块使将应用程序的开发和部署分配给不同的角色成为可能,因此不同的个人或者团队可以更好的分工合作。
在整个过程中,最先的两个角色提供J2EE产品和工具,搭建开发平台。平台OK之后,由应用程序组件提供者开发J2EE组件,然后应用程序装配者为特定应用装配需要的组件,最后由应用程序部署者来部署应用程序。在大的组织中,这些角色都由不同的个人或者团队来执行。这样的分工使得上一个角色的产品成为下一个角色的原料,更重要的是每个环节的产品都很轻便(portable)以方便下一环节的工作。例如在应用程序开发阶段,企业Bean开发者提供EJB JAR文件,而作为应用程序装配者的其他开发者将这些EJB JAR文件组装成J2EE应用程序并保存在EAR文件中,作为应用程序部署者的消费者站点的系统管理员用EAR文件将J2EE应用程序安装到J2EE服务器中。
当然不同的角色并不总是由不同的人来执行。例如你在一个小公司开发一个原型系统,你可能同时充当多种角色。
J2EE产品提供商
J2EE产品提供商设计并实现J2EE规范定义的J2EE平台、API和其他特性。典型的J2EE产品提供商如操作系统、数据库系统、应用服务器、Web服务器厂商,它们根据Java2平台企业版规范实现J2EE平台。
工具提供商
工具提供商是那些提供开发、装配和打包工具的组织或个人。组件开发者、装配者和部署者使用这些工具来工作。
应用程序组件开发者
应用程序组件开发者是开发J2EE应用程序可使用的企业Bean、Web组件、Applet和应用程序客户端组件的组织或个人。
企业Bean开发者
企业Bean开发者提供企业Bean的EJB JAR文件,他的工作步骤如下:
编写并编译源文件
配置部署描述符文件
将编译后的类文件和部署描述符文件打包为一个EJB JAR文件
Web组件开发者
Web组件开发者的工作任务是提供WAR文件:
编写并编译servlet源文件
编写JSP和HTML文件
配置部署描述符文件
将.class、.jsp、.html和部署描述符文件打包为一个WAR文件
J2EE应用程序客户端开发者
应用程序客户端开发者也提供一个JAR文件:
编写并编译源文件
配置部署描述符文件
将.class类文件和部署描述符文件打包进一个JAR文件
应用程序组装者
应用程序组装者将从组件开发者获得的组件文件装配成一个J2EE应用程序EAR文件。组装者可以编辑部署描述符文件。组装者的任务:
组装EJB JAR和WAR文件到一个J2EE应用程序EAR文件
配置J2EE应用程序的部署描述符文件
确认EAR文件的内容符合J2EE规范
应用程序部署者和系统管理员
应用程序部署者和系统管理员配置和部署J2EE应用程序,在程序运行时管理计算机和网络结构,并且监控运行时环境。包括设置事务控制、安全属性和指定数据库连接。
部署者和系统管理员的任务如下:
将J2EE应用程序EAR文件添加到J2EE服务器
修改J2EE应用程序的部署描述符为特定运行环境配置应用程序
部署J2EE应用程序到J2EE服务器
五.本书所用的软件
本书使用J2EE SDK,它是Sun公司教学用J2EE平台,包括J2EE应用服务器,Web服务器,关系数据库,J2EE API和一整套开发部署工具。从以下网址可以下载:
http://java.sun.com/j2ee/download.html#sdk
数据库访问
J2EE SDK并不支持所有的数据库,而且版本不同支持的数据库也不同。
J2EE API
J2EE1.3 API包括EJB2.0、JDBC API2.0、Servlet2.3、JSP1.2、JMS1.0、JNDI1.2、JTA1.0、JavaMail API1.2、JAF1.0、JAXP1.1、JCA1.0和JAAS1.0。
简化系统集成
J2EE平台的平台独立特性和完整的系统集成解决方案建立了一个开放的市场是用户可以使用任何厂商的产品。这样厂商就必须提共更有利于客户的产品和服务以争取用户。J2EE API通过提供以下功能简化应用程序集成:
企业Bean的统一应用程序访问接口
JSP和Servlet的单一请求和应答机制
JAAS的可靠安全模型
JAXP提供基于XML的数据交换集成
JCA提供的简单互操作能力
JDBC API提供的简单数据库连接能力
消息驱动Bean、JMS、JTA和JNDI技术提供企业应用集成
以下网址由关于集成的J2EE平台应用程序集成的详细信息:
http://java.sun.com/j2ee/inpractice/aboutthebook.html
工具
J2EE SDK提供应用程序部署工具和一组组装、检验和部署J2EE应用程序和管理开发环境的脚本。
应用程序部署工具
该工具提供组装、检验和部署J2EE应用程序的功能。它还提供以下向导:
打包、配置和部署J2EE应用程序向导
打包和配置企业Bean向导
打包和配置Web组件向导
打包和配置应用程序客户端向导
打包和配置资源适配器向导
同时这些配置信息也可以在组件和模块的相应选项页里设置。
脚本
表1-1列出了J2EE SDK提供的脚本:
表1-1 J2EE SDK 脚本
脚本命令 功能描述
j2ee 启动(-verbose或者不要参数)和停止(-stop)J2EE服务器
cloudscape 启动和停止Cloudscape数据库
j2eeadmin 添加JDBC驱动、JMS目标和各种资源工厂
keytool 创建公钥和私钥并生成X509的自签名证书
realmtool 到如证书文件,向J2EE应用程序的授权列表添加或者删除用户
packager 打包J2EE应用程序组件到EAR、EJB JAR、应用程序客户端JAR、WAR文件
verifier 检验EAR、EJB JAR、应用程序客户端JAR和WAR文件是否符合J2EE规范
runclient 运行J2EE应用程序客户端
cleanup 删除J2EE服务其中部署的所有应用程序
第2章 动手做一个EJB
Dale Green著
Iceshape Zeng译
本章一个简单的客户端/服务器应用程序为例子描述了J2EE应用的开发、部署和运行的整个过程。这个例子由三部分组成:一个货币对换企业Bean,一个J2EE应用程序客户端和一个JSP页面组成的Web客户端。
本章内容:
准备工作
获得例子代码
获得编译工具
启动J2EE服务器
启动deploytool部署工具
创建J2EE应用程序
创建企业Bean
编写企业Bean代码
编译源文件
打包企业Bean
创建J2EE应用程序客户端
编写应用程序客户端代码
打包客户端
指定应用程序客户端的企业Bean引用
创建Web客户端
编写Web客户端代码
编译
打包Web客户端
指定Web客户端的企业Bean引用
设置企业Bean的JNDI名
部署J2EE应用程序
运行J2EE应用程序客户端
运行Web客户端
修改J2EE应用程序
修改类文件
添加文件
更改部署设置
常见问题和解决方法
无法启动J2EE服务器
编译出错
部署出错
J2EE应用程序客户端运行时错误
Web客户端运行时错误
用检验工具检查问题
比较你的EAR文件和样本EAR文件
其它异常
一.准备工作
在你部署例子应用程序之前请先阅读本节。本节介绍我们使用的工具和如何使用它们。
获得例子代码
本章例子的源文件在j2eetutorial/examples/src/ejb/converter目录下,是你解压缩本指南的目标目录的相对路径。
获得编译工具
要编译例子程序的源文件,你需要安装J2EE SDK和ant(它是一个轻便的编译工具)。详细信息请参考前言的编译和运行例子一节。
检查环境变量
J2EE和ant的安装说明解释了怎么设置需要的环境变量。确定这些环境变量的设置成了下表中描述的值:
表2-1 环境变量设置
环境变量 值
JAVA_HOME J2SE SDK的安装目录
J2EE_HOME J2EE SDK的安装目录
ANT_HOME ANT的安装目录(或解压缩目录)
PATH 包括上面三个工具的安装目录的bin子目录
启动J2EE服务器
在终端窗口中执行如下命令启动J2EE服务器:
j2ee -verbose
虽然verbose不是必需的,但是它对调试很有用。
停止服务器用如下命令:
j2ee -stop
启动deploytool部署工具
deploytool部署工具有两种运行模式:命令模式和GUI图形用户接口模式。本章介绍的是指GUI模式。在终端窗口中执行下面的命令启动deploytool的GUI模式:
deploytool
要查看部署工具的上下文帮助,按F1键。
二.创建J2EE应用程序
在建立例子应用程序的三个组成部分前,你需要创建一个J2EE应用程序,命名为ConverterApp,指定保存应用程序的EAR文件名为ConverterApp.ear。
1. 在deploytool部署工具中,选择菜单File/New/Appliction新建应用程序
2. 再出现的对话框中点击Browse
3. 在选择文件对话框中定位到路径:j2eetutorial/examples/src/ejb/converter
4. 在File Name域中输入ConverterApp.ear
5. 点击New Application按钮
6. 点击OK
三.创建企业Bean
企业Bean是包含应用程序商业逻辑的服务器端组件。运行时,客户端调用企业Bean的方法来处理商业逻辑。本例的企业Bean是一个叫做ConverterEJB的无状态会话Bean,(企业Bean的分类将在下一章讨论。)它的源文件放在j2eetutorial/examples/src/ejb/converter目录下。
编写企业Bean代码
本例的企业Bean需要以下三类代码:
Remote接口
Home接口
企业Bean类
编写Remote接口
Remote接口定义客户端可以访问的商业方法。这些商业方法都在企业Bean类里实现。本例的Remote接口Coverter的代码:
import javax.ejb.EJBObject;
import java.rmi.RemoteException;
import java.math.*;
public interface Converter extends EJBObject {
public BigDecimal dollarToYen(BigDecimal dollars)
throws RemoteException;
public BigDecimal yenToEuro(BigDecimal yen)
throws RemoteException;
}
编写Home接口
Home接口定义客户端可以调用来创建、查找和删除企业Bean的方法。本例的Home接口ConverterHome只有一个create方法,该方法返回企业Bean的远程接口类型。下面是ConverterHome接口的代码:
import java.io.Serializable;
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;
public interface ConverterHome extends EJBHome {
Converter create() throws RemoteException, CreateException;
}
编写企业Bean类
本例的企业Bean类是ConverterBean类。它实现了Remote接口Converter定义的两个商业方法:dollarToYen和yenToEuro。ConverterBean的代码如下:
import java.rmi.RemoteException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import java.math.*;
public class ConverterBean implements SessionBean {
BigDecimal yenRate = new BigDecimal("121.6000");
BigDecimal euroRate = new BigDecimal("0.0077");
public BigDecimal dollarToYen(BigDecimal dollars) {
BigDecimal result = dollars.multiply(yenRate);
return result.setScale(2,BigDecimal.ROUND_UP);
}
public BigDecimal yenToEuro(BigDecimal yen) {
BigDecimal result = yen.multiply(euroRate);
return result.setScale(2,BigDecimal.ROUND_UP);
}
public ConverterBean() {}
public void ejbCreate() {}
public void ejbRemove() {}
public void ejbActivate() {}
public void ejbPassivate() {}
public void setSessionContext(SessionContext sc) {}
}
编译源文件
现在你可以编译Remote接口(Converter.java)、Home接口(ConverterHome.java)和企业Bean类的源文件了。
1. 在终端窗口中进入j2eetutorial/examples directory目录
2. 执行命令:ant converter
这个命令编译本例企业Bean和J2EE应用程序客户端的源文件,编译后的类文件放在j2eetutorial/examples/build/ejb/converter目录下(不是src目录)。关于编译的更多信息参考前言部分的编译和运行例子。
注意:在前述ant编译源文件时,它将j2ee.jar文件加在每个编译任务的classpath中。j2ee.jar文件存放在J2EE SDK安装目录的lib子目录下。如果你使用其它工具来编译这些源文件,需要把j2ee.jar文件加入classpath。或者可以直接将该文件加入操作系统的classpath环境变量中。
打包企业Bean
我们使用deploytool部署工具的新建企业Bean向导来打包企业Bean。在这个过程中向导完成如下工作:
1. 创建企业Bean的部署描述符文件
2. 将部署描述符文件和企业Bean的类文件打包进同一个EJB JAR文件
3. 把生成的EJB JAR文件加入应用程序的ConverterApp.ear文件
打包成功后你可以用Tools/Descriptor Viewer菜单查看部署描述符文件内容
用File/New/Enterprise Bean菜单打开新建企业Bean向导,该向导包含以下对话框:
1. Introduction对话框
a) 阅读该向导的说明
b) 点击Next
2. EJB JAR对话框
a) 选择Create New JAR File In Application单选按钮
b) 在下拉框中选择ConverterApp
c) 在JAR Display Name域输入ConverterJAR
d) 点击Edite按钮
e) 在Available Files下的树中定位到j2eetutorial/examples/build/ejb/converter目录。(如果converter目录在树中的层次太深,你可以通过在Starting Directory域中输入converter的整个或者部分路径来简化在树中的目录展开动作)
f) 在Available Files树中选中converter目录下的Converter.class、ConverterBean.class和ConverterHome.class文件点击Add按钮。(你也可以使用鼠标拖放操作来将这些文件加入Contents文本域)
g) 点击OK
h) 点击Next
3. General对话框
a) 在Bean Type下选中Session单选按钮
b) 选中Stateless单选按钮
c) 在Enterprise Bean Class下拉框中选择ConverterBean
d) 在Enterprise Bean Name域输入ConverterEJB
e) 在Remote Home Interface下拉框中选择ConverterHome
f) 在Remote Interface下拉框中选择Converter
g) 点击Next
4. Transaction Management对话框
a) 后面的步骤对本例并不重要,点击Finish完成向导
四.创建J2EE应用程序客户端
J2EE应用程序客户端是用Java语言编写的应用程序。在运行时,客户端应用程序可以运行在和J2EE服务器不同的Java虚拟机中。
本例的J2EE应用程序客户端需要两个不同的JAR文件。第一个JAR文件存放J2EE客户端组件,它包含客户端的部署描述符和类文件。当你使用新建应用程序客户端向导创建该JAR文件时,deploytool会自动将生成的JAR文件加入应用程序的EAR文件中。因为在J2EE规范中被定义,这个JAR文件可以轻易适应不同的J2EE服务器。
第二个JAR文件包含客户端运行时需要的一些存根类文件。这些存根文件使客户端可以访问运行在J2EE服务器上的企业Bean。因为J2EE规范没有定义这个存根JAR文件,它的实现根据服务器的不同而不同,本例中的这个JAR文件只适用于J2EE SDK。
本例的J2EE应用程序客户端源文件为:j2eetutorial/examples/src/ejb/converter/ConverterClient.java。它已经在创建企业Bean一节中被编译过。
编写J2EE应用程序客户端代码
ConverterClient.java源文件展示了企业Bean客户端要完成的基本任务:
查找Home接口
创建一个企业Bean实例
调用商业方法
查找Home接口
ConverterHome接口定义了像create等的企业Bean生命周期方法。在ConverterClient能调用create方法之前,它必须先查找到一个ConverterHome类型的实例对象。以下是查找过程:
1. 创建一个初始命名上下文(initial naming context)对象:
Context initial = new InitialContext();
Context接口是Java命名和目录接口(Java Naming and Directory Interface ,JNDI)的一部分。一个命名上下文是一个名字到对象绑定的集合,上下文中已绑定的名字是对象的JNDI名。
一个InitialContext(该类实现Context接口)对象提供名字方案的访问入口。所有的命名操作都关联一个上下文。
2. 获得客户端的环境命名上下文对象:
Context myEnv = (Context)initial.lookup("java:comp/env");
Java:comp/env是绑定到ConverterClient组件环境命名上下文对象的名字。
3. 获得绑定到名字ejb/simpleConverter的对象
Object objref = myEnv.lookup("ejb/SimpleConverter");
名字ejb/SimpleConverter绑定到一个企业Bean的引用,该引用是企业Bean的Home接口的逻辑名。这样名字ejb/SimpleConverter就等于引用一个ConverterHome对象。企业Bean的名字应该存放在java:com/env/ejb的字上下文中。
4. 将得到的引用造型为ConverterHome类型的对象
ConverterHome home =
(ConverterHome)PortableRemoteObject.narrow(objref,ConverterHome.class);
创建一个企业Bean实例
可端调用ConverterHome类型的对象的create方法来创建一个企业Bean实例,该方法返回一个Converter(本例企业Bean的远程接口)类型的对象。Converter接口定义了客户端可调用的企业Bean商业方法。当客户端调用上述的create方法,容器实例化一个企业Bean然后调用ConverterBean.ejbCreate方法。客户端调用代码如下:
Converter currencyConverter = home.create();
调用商业方法
调用商业方法十分简单,就是调用上面获得的Converter类型对象的方法(该接口中定义的方法都是商业方法)。EJB容器会调用运行在服务器中的ConverterEJB实例的对应方法。客户端调用dollarToYen商业方法的代码如下:
BigDecimal param = new BigDecimal ("100.00");
BigDecimal amount = currencyConverter.dollarToYen(param);
下面是客户端ConverterClient的完整代码:
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import java.math.BigDecimal;
public class ConverterClient {
public static void main(String[] args) {
try {
Context initial = new InitialContext();
Object objref = initial.lookup
("java:comp/env/ejb/SimpleConverter");
ConverterHome home =
(ConverterHome)PortableRemoteObject.narrow(objref,
ConverterHome.class);
Converter currencyConverter = home.create();
BigDecimal param = new BigDecimal ("100.00");
BigDecimal amount =
currencyConverter.dollarToYen(param);
System.out.println(amount);
amount = currencyConverter.yenToEuro(param);
System.out.println(amount);
System.exit(0);
} catch (Exception ex) {
System.err.println("Caught an unexpected exception!");
ex.printStackTrace();
}
}
}
打包J2EE应用程序客户端
客户端的源文件已经在创建企业Bean时同时被编译过了,这里直接打包。
运行deploytool部署工具的新建应用程序客户端向导,该向导完成如下工作:
创建应用程序客户端的部署描述符文件
将部署描述符文件加入客户端的JAR文件
将生成的JAR文件加入ConverterApp.ear文件
之后你可以通过Tools/Descrptor Viewer菜单查看部署描述符文件内容。
从File/New/Application Client菜单打开该向导,它有以下对话框:
1. Introduction对话框
a) 阅读向导说明
b) 然后Next
2. JAR File Contents对话框
a) 在下拉框中选择ConverterApp
b) 点击Edit按钮
c) 将j2eetutorial/examples/build/ejb/converter/ConverterClient.class文件加入Contents Of树中
d) 确定然后Next
3. General对话框
a) 在Main Class下拉框中选择ConverterClient
b) 确定Display Name域中输入了ConverterClient
c) 在Callback Handler Class下拉框中选择Use container-managed authentication
d) Next后点击Finish完成
指定应用程序客户端的企业Bean引用
当客户端ConverterClient调用lookup方法,它传递企业Bean的Home接口的引用绑定名字给方法:
Object objref = myEnv.lookup("ejb/SimpleConverter");
设置该引用名字的步骤如下:
1. 在deploytool工具左边的树视图中选择ConverterClient节点
2. 选择右边的EJB Refs选项页
3. 点击Add按钮
4. 在Coded Name列输入ejb/SimpleConverter
5. 在Type列选择Session
6. 在Interfaces列选择Remote
7. 在Home Interface列输入ConverterHome
8. 在Local/Remote Interface列输入Converter
五.创建Web客户端
Web客户端包含在JSP文件j2eetutorial/examples/src/ejb/converter/index.jsp中。JSP文件是一个基于文本的文档,它有两种类型的内容组成:静态模板数据,它们可以是像HTML、WML和XML等等的任意基于文本格式的表示;JSP元素,它是动态内容。
编写Web客户端代码
下面代码的粗体部分和应用程序客户端的工作相似,它查找企业Bean的Home接口,创建企业Bean实例,然后调用商业方法。仅有的不同是lookup调用时的参数,为什么这么用在下一节介绍。
客户端需要的类都在JSP命令(<%@ %>中包括的代码)中导入(import)。因为查找查找Home接口和创建企业Bean实例只执行一次,所以这些代码放在JSP声明(<%! %>中包括的代码)中,声明中的代码将在JSP的初始化方法jspInit中被执行。接着用HTML标记创建包含一个输入框的表单。接着的scriptlet(<% %>中包含的部分)代码从request内值对象中读出参数并将它转换为BigDecimal对象,最后JSP表达式(<%= %>中包含的表达式)调用企业Bean的商业方法并将结果写入送到客户端(浏览器)的页面输出流传。
<%@ page import="Converter,ConverterHome,javax.ejb.*,
javax.naming.*, javax.rmi.PortableRemoteObject,
java.rmi.RemoteException" %>
<%!
private Converter converter = null;
public void jspInit() {
try {
InitialContext ic = new InitialContext();
Object objRef = ic.lookup("
java:comp/env/ejb/TheConverter");
ConverterHome home =
(ConverterHome)PortableRemoteObject.narrow(
objRef, ConverterHome.class);
converter = home.create();
} catch (RemoteException ex) {
...
}
}
...
%>
Enter an amount to convert:
<%= amount %> dollars are
<%= converter.dollarToYen(d) %> Yen.
<%= amount %> Yen are
<%= converter.yenToEuro(d) %> Euro.
<%
}
%>
编译Web客户端
J2EE服务器自动编译Web客户端的JSP页面,如果是servlet实现,你需要先编译它。
打包Web客户端
运行deploytool工具的新建Web客户端向导,它完成如下工作:
1. 创建Web客户端的部署描述符
2. 将Web客户端的文件打包进一个WAR文件中
3. 将该WAR文件加入应用程序的ConverterApp.ear文件中
可以通过Tools/Descriptor菜单在打包结束后查看部署描述符文件的内容。
用File/New/Web Component菜单打开该向导,它有如下对话框组成:
1. Introduction对话框。阅读向导说明
2. WAR File对话框
a) 选中Create New WAR File In Application
b) 在下拉框中选择ConverterApp
c) 在WAR Display域输入ConverterWAR
d) 点击Edit
e) 将j2eetutorial/examples/build/ejb/converter/index.jsp文件加入Contents Of树中
f) 确定
3. Choose Component Type对话框。选中JSP单选按钮
4. Component General Properties对话框。在JSP Filename下拉框中选择index.jsp。点击Finish完成
指定Web客户端的企业Bean引用
Web客户端的企业Bean的Home接口的引用名为:ejb/TheConverter。Lookup调用:
Object objRef = ic.lookup("java:comp/env/ejb/TheConverter");
设置该引用名步骤如下:
1. 在树视图中选择ConverterWAR节点
2. 选择EJB Refs选项页
3. 点击Add按钮
4. 在Coded Name列中输入ejb/TheConverter
5. 在Type列选择Session
6. 在Interfaces列选择Remote
7. 在Home Interface列输入ConverterHome
8. 在Local/Remote Interface列输入Converter
六.设置企业Bean的JNDI名
虽然J2EE应用程序客户端和Web客户端访问的是同一个企业Bean,但是它们对企业Bean的Home接口的引用命名不同。J2EE应用程序客户端用ejb/SimpleConverter查找Home接口引用,而Web客户端用ejb/TheConverter。这些引用名都作为lookup调用是的参数。为了让lookup方法能够通过它们找到企业Bean的Home接口类型对象,你必须映射这些引用名到企业Bean的JNDI名。虽然这个映射增减了一个间接访问层,但是它降低了客户端和企业Bean之间的耦合,使J2EE组件更容易装配成应用程序。(这种引用查找在EJB1.1时出现,主要是避免在代码中写死JNDI名来查找企业Bean可能造成的装配时JNDI名冲突)。
以下步骤建立引用名和JNDI名的映射:
1. 在树视图中选择ConverterApp节点
2. 选择JNDI Name选项页
3. 在Application表格中选中ConverterEJB组件,在JNDI Name列中输入MyConverter作为ConverterEJB的JNDI名
4. 在References表格中每一行的JNDI Name列中都输入上面的JNDI名
图2-1显示了设置的JNDI名:
图 2-1 ConverterApp JNDI 名选项页
七.部署J2EE应用程序
前面我们已经将本例中的组件全部加入到应用程序中了,现在开始部署。
1. 选择ConverterApp应用程序(在树中)
2. 选择菜单Tools/Deploy
3. 在Introduction对话框中确定Object To Deploy选择的是ConverterApp,Target Server选择的是localhost
4. 选中Return Client Jar复选框
5. 在Client JAR File Name域中输入j2eetutorial/examples/src/ejb/converter/ConverterAppClient.jar(可以使用Browse按钮),该JAR文件包含可以远程访问ConverterEJB的存根类。下一步
6. 在JNDI Names对话框中,检验是不是你前面输入的名字。下一步
7. 在WAR Context Root对话框中,为Context Root域输入converter。当你运行Web客户端时,converter将是访问Web的URL的一部分。下一步
8. 在Rewiew对话框中点及Finish完成
9. 在Deployment Progress对话框部署完成后确定退出部署。
八.运行J2EE应用程序客户端
按以下步骤运行应用程序客户端:
1. 在终端窗口中进入j2eetutorial/examples/src/ejb/converter目录
2. 确认ConverApp.ear和ConverterAppClient.jar文件在该目录下
3. 设置APPCPATH环境变量为ConvertAppClient.jar所在目录
4. 执行下面的命令:
runclient -client ConverterApp.ear -name ConverterClient -textauth
5. 在出现登录提示时输入用户名:guest。密码:guest123。
6. 终端窗口中显示结果如下:
Binding name:'java:comp/env/ejb/SimpleConverter'
12160.00
0.77
Unbinding name:'java:comp/env/ejb/SimpleConverter'
九.运行Web客户端
在浏览器中输入下面的URL访问Web客户端。
http://
在显示页面的输入框中输入100后点击Submit提交,会看到图2-2所示的结果。
图 2-2 Converter Web客户端
十.修改J2EE应用程序
因为J2EE SDK仅供试验用,所以它支持重复部署。如果你修改了一个J2EE应用程序,你必须重新部署它。
修改类文件
要修改企业Bean的一个类文件,先修改源文件,然后重新编译,最后重新部署应用程序。例如,如果你想修改ConverterBean类的dollarToYen商业方法的汇率,执行如下步骤:
1. 编辑ConverterBean.java文件
2. 用ant converter命令重新编译ConverterBean.java源文件
3. 在deploytool中选择菜单:Tools/Update Files
4. 在Update Files对话框中,如果你修改的文件已经列在对话框上面的列表中,点击OK转到第6步。如果列在下面的列表中,它们的修改还没有被发现。选择这些文件中的一个点击Edit Search Paths按钮
5. 在Edit Search Paths对话框中指定Update Files对话框查找修改过的文件的路径
a) 在Search Root域中输入查找开始的全路径
b) 在Path Directory列表中,为你想搜索的每一个目录加入一行,不用全路径名,这里的路径都是用对Search Root的相对路径
c) 确定
6. 用Tools/Deploy菜单重新部署。在第一步的对话框中确定选中了Save Object Before Deploying复选框。如果你不想马上部署,可以用Tools/Save菜单先保存第5步的设置。
有一种快捷方式来重新部署,就是用菜单Tools/Update And Redeploy。除非文件找不到,否则不会弹出Update Files对话框。
改变WAR文件的内容步骤相同。Update Files操作会检查包括HTML和JSP的任意文件的改动,如果你改动了index.jsp文件,仍然要执行ant converter命令,它会将index.jsp文件从src目录拷贝到build目录。
添加文件
以下步骤可以向EJB JAR或者WAR文件中加入新文件:
1. 在deploytool的树视图中选择目标JAR或者WAR文件
2. 选择General选项页
3. 点击Edit按钮
4. 添加需要的文件确定
5. 在主窗口中选择菜单:Tools/Update And Redeploy
修改部署设置
要修改ConverterApp的部署设置,先选择特定的选项页然后编辑对应的输入框中内容最后重新部署使修改生效。例如一下步骤将ConverterBean的JNDI名从MyConverter改为ATypo:
1. 在deploytool中选中ConverterApp节点
2. 选择JNDI Names选项页
3. 将两个表中所有JNDI Name列的内容改为Atypo
4. 在主窗口中选择File/Save菜单
5. 用Tools/Update And Redeploy菜单重新部署
十一。常见问题和解决方法
无法启动J2EE服务器
命名和目录服务端口冲突
症状:在你用-verbose参数启动J2EE服务器时,显示如下信息:
J2EE server listen port: 1050
RuntimeException: Could not initialize server...
解决办法:其他应用程序占用了1050端口。停止J2EE服务器,将J2EE SDK安装目录下的config/orb.properies文件里的默认端口1050改为别的端口。默认端口的更多信息清参考J2EE SDK的下载包的配置说明。
Web服务端口冲突
症状:
LifecycleException: HttpConnector[8000].open:
java.net.BindException: Address in use...
解决办法:端口8000被占用,编辑config/web.properties文件更改该端口
不正确的XML解析器
症状:启动J2EE服务器时出现如下错误:
Exception in thread "main"
javax.xml.parsers.FactoryConfigurationError:
org.apache.xerces.jaxp.SAXParserFactoryImpl at ...
解决办法:删除J2SE安装目录下的jre/lib/jaxp.properties文件
编译出错
ant找不到Build文件
症状:执行ant converter命令时,出现如下错误:
Buildfile: build.xml does not exist!
Build failed.
解决办法:在执行ant命令前,进入j2eetutorial/examples./src目录。如果你想在当前目录下执行ant命令,必须在命令里指定Build文件。例如:
ant -buildfile C:/j2eetutorial/examples/src/build.xml converter
编译器不能解析符号
症状:执行ant converter命令时,编译器报告如下错误:
cannot resolve symbol
...
BUILD FAILED
...
Compile failed, messages should have been provided
解决办法:确定你的J2EE_HOME环境变量设置正确(参考本章第一节)
ant1.4在你运行客户端后不编译例子
症状:ant1.4显示如下错误:
The filename, directory name, or volume label syntax is
incorrect.
解决办法:使用ant1.3。1.4版的ant.bat脚本和J2EE SDK的脚本都使用JAVACMD环境变量。例如SDK的runclient.bat脚本设置JAVACMD的值会引起ant.bat执行错误。
部署出错
Classpath里不正确的XML解析器
症状:错误提示如下:
...
[]java.rmi.RemoteException:Error saving/opening
Deployment Error:Bad mapping of key{0} class{1},
not found: com.sum.enterprise.deployment.xml.ApplicationNode
解决办法:在J2SE安装目录的jre/lib/ext子目录下删除jaxp.jar文件。这个文件里的XML解析器跟J2EE服务器不兼容。如果你没有jaxp.jar文件,或许你的classpath指向Tomcat的XML解析器。这种情况你需要将该路径从classpath环境变量中删除。
Remote Home接口被设置成Local Home接口
症状:错误提示如下:
LocalHomeImpl must be declared abstract.
It does not define javax.ejb.HomeHandle getHomeHandle()
from interface javax.ejb.EJBHome.
解决办法:从EAR文件中将企业Bean删除(Edit/Delete菜单),然后用新建企业Bean向导重新创建该企业Bean。在向导的General对话框中为Remote Home Interface和Remote Interface选择值。
J2EE应用程序客户端运行时错误
客户端抛出NoClassDefFoundError
症状:错误提示如下:
java.lang.NoClassDefFoundError:converter.ConverterHome
解决办法:该错误出现在当客户端找不到ConverterAppClient.jar里的类时。确定你是按照运行J2EE应用程序客户端一节的步骤做的。
客户端找不到ConverterApp.ear
症状:错误提示如下:
IOException: ConverterApp.ear does not exist
解决办法:确定ConverterApp.ear文件存在并且你在命令中用-client参数指示正确:
runclient -client ConverterApp.ear -name ConverterClient
参考创建J2EE应用程序和运行J2EE应用程序客户端两节。
客户端找不到ConverterClient组件
症状:错误提示如下:
No application client descriptors defined for: ...
解决办法:检查你是否已经创建了ConverterClient组件,并确定在runclient命令中用-name参数指定了该组件。
参考打包J2EE应用程序客户端一节。
登录失败
症状:登录时出现如下错误提示:
Incorrect login and/or password
解决办法:确定用户名输入的是:guest。密码输入的是:guest123。
J2EE应用程序还没有部署
症状:客户端提示出错:
NameNotFoundException. Root exception is org.omg.CosNaming...
解决办法:部署应用程序。参考部署J2EE应用程序一节。
JNDI名错误
症状:客户端报告如下错误:
NameNotFoundException. Root exception is org.omg.CosNaming...
解决办法:在ConverterApp的JNDI Names选项页中确定ConverterBean的JNDI名和客户端引用ejb/SimpleConverter引用的JNDI名一致。如果不一致,修改后重新部署。
Web客户端运行时错误
URL中的Web上下文不正确
症状:浏览器报告找不到这个页面(HTTP 404)
解决办法:检查URL里的Web上下文(converter)跟你在Conponent General Properties对话框中指定的一致,它是大小写敏感的。参考打包Web客户端一节。
J2EE应用程序还没有部署
症状:浏览器报告找不到这个页面(HTTP 404)
解决办法:部署
JNDI名错误
症状:当你在Web页中点击Submit提交,浏览器报告如下错误:
A Servlet Exception Has Occurred.
解决办法:在ConverterApp的JNDI Names选项页中确定ConverterBean的JNDI名和Web客户端引用的JNDI名一致。如果不一致,修改后重新部署。
用检验工具检查问题
检验工具(verifier)可以检查部署描述符和方法签名不一致的地方。这些不一致通常导致部署错误或运行时错误。在deploytool里用菜单Tools/Verifier可以启动GUI版的verifer检验工具。你也可以启动单独的GUI版本或者命令行版本。详见附录B。
比较你的EAR文件和样本EAR文件
对大部分例子(包括本章的例子),该指南的下载包里提供了样本J2EE应用程序EAR文件,它们存放在j2eetutorial/examples/ears directory目录下。
其它异常
如果以上的提示都不能解决出现的问题,你可以卸载应用程序,并用cleanup脚本清空J2EE服务器的储藏库。服务器也需要关闭后重新启动:
j2ee -stop
cleanup
j2ee -verbose
第二部分 EJB技术
第3章 企业Bean
Dale Green
翻译 IceShape Zeng
企业Bean是实现EJB技术的J2EE组件。企业Bean在俄EJB容器中运行,运行时环境由服务器建立(图1-5)。虽然EJB容器对开发这是透明的,但它为运行其中的企业Bean提供项事务处理等的系统级服务。这些服务是你可以快速的建立和部署企业Bean,而这些企业Bean正是构成和新业务处理的J2EE应用。
本章内容:
1,企业Bean概述
企业Bean的优点
何时需要使用企业Bean
企业Bean的分类
2,会话Bean
状态管理模式
何时需要会话Bean
3,EntityBean
EntityBean的构造
和会话Bean的不同之处
容器管理的持久性
何时需要EntityBean
4,Message-Driven Bean
Message-DrivenBean的构造
和会话Bean、EntityBean的不同之处
何时需要Message-DrivenBean
5,定义客户端访问接口
远程访问
本地访问
本地接口和容器管理的关系
选择接口类型
访问的性能
访问的参数
6,企业Bean的内容
7,企业Bean的命名约定
8,企业Bean的生存周期
有状态会话Bean的生存周期
无状态会话Bean的生存周期
EntityBean的生存周期
Message-DrivenBean的生存周期
1,企业Bean概述
用Java语言编写,企业Bean就是一个应用中封装了商务逻辑的服务器端组件。这些商务逻辑是实现应用程序目标的代码。例如在一个存货控制的应用程序里,企业Bean也许在checkInventoryLevel和orderProduct方法中实现了商务逻辑,通过调用这两个方法,远程客户端就可以访问应用程序提供的存货管理的服务。
企业Bean的优点
由于以下的原因,企业Bean大大简化了分布是应用的开发。首先EJB容器给企业Bean提供了系统级服务,使Bean开发者可以专注于商务问题的解决。是EJB容器而不是开发者负责项事务处理和安全授权等系统级服务的管理。其次因为企业Bean而不是客户端实现商务逻辑,客户端开发者就可以致力于客户端表述的开发,而不必为实现商务规则或者数据库访问的日常处理而编码了。结果使客户端"瘦"了许多,很明显,这个有点对于在小设备上运行的客户端来说是很重要的。最后,因为企业Bean是可移植的,应用程序组装者可以用现有的企业Bean建立新的应用程序。这些应用程序可以在任何兼容的J2EE服务器上运行。
何时需要使用企业Bean
如果你的应用程序符合以下的任一条件,你就应该考虑使用企业Bean:
" 你的应用程序需要不断的升级。为了适应不断增长的用户,你可能需要将你的应用程序组件分布在多台不同的机器上运行。虽然并不仅仅是企业Bean可以在不同的机器上运行,但企业Bean的运行位置对于客户端始终是透明的。
" 需要用事务机制来保证数据完整性。企业Bean支持事务机制以提供对共享资源并发访问的管理。
" 应用程序需要支持众多不同类型的客户端。只需要极少的几行代码,远程客户端就可以很容易的访问到企业Bean。这些客户都可以很"瘦"并且在理论上可以是任意数量不同类型的客户端。
企业Bean的分类
表3-1列出了三种不同类型的企业Bean。接下来的几节将详细介绍每一种企业Bean.
企业Bean类型 作用
会话Bean 完成客户端请求的动作
EntityBean 描述持久存储的商业实体对象
MessageDrivenBean 作为JMS(Java Message Service Java消息服务) API的监听者异步处理监听到的的消息
2,会话Bean
会话Bean表现连接到J2EE服务器的一个单独的客户端。客户端通过调用会话Bean的方法来访问部署在服务器上的应用程序。会话Bean完成客户端的请求,从而对客户端隐藏了服务器内复杂商务逻辑的执行过程。
正如会话Bean的名称所表示的,它代表一次会话。单独的会话Bean对象是不可以共享的,它只能有一个客户端访问,同样一次会话也只能有一个用户。和一次会话一样,会话Bean对象不是持久性对象。(就是说它的数据并不保存到数据库。)当客户端结束运行,对应的会话Bean也跟着结束并断开与客户端的联系(不保留特定客户端的任何信息)。
第四章将会介绍会话Bean的编码实例。
状态管理模式
根据状态管理模式的不同会话Bean可以分为两类:有状态(stateful)会话Bean和无状态(stateless)会话Bean。
有状态会话Bean。
一个对象的状态由它的成员变量(数据成员)的状态决定。有状态会话Bean的成员变量描述一个唯一的客户端-会话Bean的关联状态。因为客户端要与对应的会话Bean进行对话,所以这种状态通常被叫做会话状态。
会话状态在整个会话期间被保留。如果客户端运行结束或者删除对应的会话Bean,这个会话就结束同时状态被清除。这种状态的短暂性并不是问题,相反,如果客户端和会话Bean的对话结束就不必要在保留会话的状态了。
无状态会话Bean
无状态会话Bean并不为客户端保留会话状态。在客户端掉用无状态会话Bean的方法时,对应会话Bean的数据成员会描述这个调用状态,但仅仅只在该方法调用期间保持这个状态。当方法调用结束,状态就被清除。除了在方法调用期间,所有同一个无状态会话Bean实例是等价的,可以被容器分配给任一客户端。
因为无状态会话Bean可以同时支持多个客户端,所以能更好的支持应用程序的可数的大量客户端。很明显,对支持相同数量的客户端的应用程序,需要的无状态会话Bean会比有状态会话Bean要少。
有时,EJB容器会在没有请求的时候把有状态会话Bean保存在内存(二级存储器Second Storage)中。不管什么时候,没有请求时无状态会话Bean都不会被保存中。所以,无状态会话Bean会比有状态会话Bean有更高的性能。
何时需要会话Bean
通常,在出现以下几种情况时你需要用会话Bean:
.在任何给定时间,只有一个客户端访问这个Bean的实例。
. Bean的状态并不需要持久保存,只在一个时间段(可能是几小时)内保持。
在以下情况下,建议采用有状态会话Bean:
. Bean需要描述一个于特定客户端的会话状态
. Bean需要在客户端的多个方法调用之间保存调用信息
. Bean作为应用程序的其他组件和客户端的中介者,呈现一个简单化的视图给客户端
.在调用接口里,Bean管理很多企业Bean的工作流(如18章的AccountControllerEJB的会话Bean例子)。
如果你的应用符合以下特性,为了得到更高的性能你应该选择无状态会话Bean:
.Bean的状态不包含客户端相关的数据
.在一个单一方法调用中,Bean已经可以为客户端完成所需要的工作。例如你可以用无状态会话Bean发一封邮件确认网络订单。
.Bean需要从数据库获取一些客户端经常访问的只读数据。你可以用这样的Bean来访问数据表中代表这个月已经卖出的产品的行。
3,EntityBean
一个EntityBean描述一个持久存储备的商业对象。商业对象的例子如:消费者,订单和产品等。在J2EE SDK中持久存储设备是一个关系型数据库。最典型的情况是一个EntityBean又一个在地层数据库中有一个表相对应,而EntityBean的每一个实例对应表中的一行数据。在第5和第6章有示例代码。
EntityBean和会话Bean的不同之处
EntityBean和会话Bean有很多不同之处。EntityBean是持久性的,允许共享访问,拥有主键并且会参与和其他EntityBean的关联。
持久性
因为EntityBean的状态保存在存储设备中,所以它具有持久性。持久性是指EntityBean的状态跨越应用程序和J2EE服务器处理过程的生存期,就是说应用程序结束或者服务器终止EntityBean的状态仍然保留。如果你是用数据库,你就是在使用持久性数据。数据库中的数据是持久性的,应为就算你关闭数据库服务器或者相应的应用程序,他们仍然是存在的。
EntityBean有两种持久性管理机制:BMP(bean-managed persistence Bean管理的持久性)和CMP(container-managed persistence 容器管理的持久性)。对于BMP,必须在EntityBean中手工编写访问数据库的代码。对于CMP,容器会自动生成访问数据库的代码,为开发者节省了为数据库访问编码。详细信息在下节容器管理的持久性中介绍。
共享访问
EntityBean可以被多客户端所共享。由于多个客户端可能同时去修改同一数据,所以在调用过程中事务机制非常重要。典型情况下EJB容器都支持事务机制。在这种情况下,可以在Bean的部署描述符中确定它的事务属性。开发者不必为事务界限编码--容器会自动划分事务界限。14章将详细描述Bean的事务机制。
主键
每一个EntityBean实例都有一个唯一对象标识。例如一个特定的EntityBean实例可能用一个特定的数字来标识。这个唯一标识就是主键,可以让客户端找到对应的EntityBean实例。更多信息请查看主键和Bean管理的持久性一节。
关系
象关系数据库中的一个表一样,EntityBean之间也会有关系。例如在一个学校登记系统中,表示学生的StudentEJB和表示课程的CourseEJB因为学生必须登记上课而产生关系。
EntityBean关系的实现方法对于BMP和CMP是不同的。BMP需要编码来实现关系,而CMP是由容器来处理关系的(开发者必须在部署描述符中定义关系)。因此,EntityBean的关系通常是指采用CMP的关系。
容器管理的持久性(CMP)
容器管理的持久性(CMP)是指EJB容器负责处理所有的数据库访问。EntityBean的代码不包含任何数据库访问语句(SQL)。所以Bean的代码不会受到低层存储机制(数据库)的约束。由于这样的灵活性,即使把EntityBean部署到使用不同数据库的不同的服务器上,也不需要修改代码和重新编译。简而言之,CMP大大提高了EntityBean的可移植性。
为了可以自动生成数据库访问代码,容器需要知道EntityBean所代表数据的抽象规则。
抽象数据模式(Abstract Schema)
作为EntityBean的部署描述符的一部分,抽象数据模式定义了EntityBean的持久性字段和关系。抽象数据模式不同于底层数据库的物理表述。抽象是将它和底层的数据库物理模式区分开来(这里指的是把数据库的表和字段直接映射倒EntityBean)。比如关系型数据库的物理规划是指由表和列组成的结构。
你需要在部署描述符中指定抽象数据模式的名称,这个名称会在用EJB QL(Enterptise JavaBean Query Language)写查询的时候被引用。对于CMP,你需要为除了findByPrimaryKey的所有查找方法定义一个对应的EJB QL查询。这个查询将在该查找方法被调用的时候有容器执行。第8章将详细介绍EJB QL。
在你想要编码之前了解一下抽象数据模式的梗概是很有帮助的。下图是一个描绘3个EntityBean之间关系的简单规划。这些关系将在后续章节深入讨论。
图3-1
持久性字段(Persistent Fields)
EntityBean的持久性字段都存储在地层的数据存储设备中。它们共同组成了Entity Bean的状态。在运行时,EJB容器自动地在数据库和EntityBean之间同步这些状态。在部署的时候,典型情况容器会把EntityBean映射为数据库中的一张对应表而持久性字段映射为表的字段(column列)。
例如:一个EntityBean CustomerEJB可能有firstName,lastName,phone和emailAddress等持久性字段。在CMP中它们都是虚拟的,它们都在抽象数据模式中以访问方法(getters和setters)的形式声明,不需要像BMP一样在EntityBean中把它们声明为实例变量。
关系字段(Relationship Fields)
一个关系字段就像数据库的一个外键,它识别一个关联的Bean。和持久性字段一样,关系字段在CMP中也是虚拟的并以访问方法形式定义。但是关系字段并不表示Entity Bean的状态。关系字段会在CMR的方向一节进一步讨论。
CMR(Container-ManagedRelationships)分类
CMR可以分为四类:
一对一:一个EntityBean的实例对应另一个EntityBean的单个实例。例如,对于一个存储箱存放一个小部件的物资仓库模型,StorageBinEJB和WidgetEJB的关系就是一对一。
一对多:一个EntityBean实例对应另一个EntityBean的多个实例。例如,一张订单可以有很多的明细项目,在order应用中,OrderEJB和LineItemEJB的关系就是一对多。
多对一:呵呵,把上面的例子再读一边就好了:)
多对多:两个EntityBean中任一EntityBean的单个实例都可能对应另一个的多个实例。例如:在学校里,一门课有很多学生上,而每一个学生都不会只上一门课。因此在enrollment应用中,CourseEJB和StudentEJB的关系就是多对多。
CMR的方向
只有两种可用的方向:单向或者双向(呵呵:)。在双向的关系中,每一个EntityBean都有对另一个EntityBean引用的关系字段。通过关系字段,EntityBean可以访问相关的EntityBean对象。如果一个EntityBean有关系字段,我们通常会说它"知道"它的关联对象。就像OrderEJB"知道"它的明细项LineItemEJB,如果同时LineItemEJB也"也知道"自己所属的订单OrderEJB,那么它们的关系就是双向的。
在单向的关系中,只有一个EntityBean中有对另一个EntityBean引用的关系字段。像订单明细LineItemEJB中有一个产品的关系字段引用ProductEJB,而ProductEJB中并没有对LineItemEJB引用的关系字段。就是说LineItemEJB"知道"它要卖哪个ProductEJB而ProductEJB却还是SB一样被谁卖了都不晓得。
EJB QL查询通常需要通过这些关系取得数据。关系的方向决定了查询可以从哪个EJB向另一个相关的EJB取得数据。例如一个查询可以通过LineItemEJB取得对应的ProductEJB代表的产品数据,但却不能从ProductEJB查询到有哪些订单明细项出卖过它。而对于双向关系想怎么玩就怎么玩吧,大家都是成年人嘛,只要不是太过分哦:)
何时需要EntityBean
Bean代表一个商务实体而不是一个过程。例如表示信用卡的CreditCardEJB要做成EntityBean,而信用卡核实的VerifierEJB就只能做成会话Bean。
Bean的状态是需要持久存储的。如果Bean的实力结束了或者J2EE服务器关闭,它的状态依然存在,只是回到向数据库这样的存储设备睡觉去了。
4,Message-Driven Bean
注:因为Message-DrivenBean依赖于JMS(Java Message Serviece,Java消息服务)技术,所以本节包含The Java Message Service Tutorial的一些内容。如果要全面了解它们的工作原理请参考上书,下在地址:
http://java.sun.com/products/jms/tutorial/index.html
Message-DrivenBean的构造
Message-DrivenBean是一种可以让应用程序一部处理消息的企业Bean。它以JMS消息监听者的方式工作,很像一个事件监听者,只是用消息代替了事件。消息的发送者可以是任意J2EE构件--应用程序客户端、别的企业Bean或者Web应用--或者一个JMS应用程序或者别的非J2EE系统。
Message-DrivenBean现在只能处理JMS消息,不过将来一定可以处理任意类型的消息。
第7章会详细讨论Message-DrivenBean。
与会话Bean、EntityBean的不同之处
Message-DrivenBean与另外两种企业Bean最明显的区别是客户端访问Message-DrivenBean不需要通过接口(接口定义将在下一节介绍)。就是说它只需要一个Bean类文件。
Message-DrivenBean在有些方面和无状态会话Bean相似:
它的实例不保持数据或者与特定客户端的会话状态
一个Message-DrivenBean的所有实例都是等价的,容器可以把消息分给任何一个实例处理。容器可以通过实例池实现同时处理多个消息流。
单个Message-DrivenBean可以处理多个客户端发送的消息
Message-DrivenBean并不是任何状态都保持,在处理客户端发送的消息期间它也通过实例变量保持一些状态,例如:JMS连接,数据库连接或者对企业Bean的引用等。
当一个消息发送到J2EE服务器端,容器调用Message-DrivenBean的onMessage方法来处理该消息。该方法通常把收到的消息造型为五种JMS消息之一然后根据该应用的商业逻辑处理收到的消息。该方法也可以调用别的辅助方法或者调用一个会话Bean或者EntityBean的方法来处理消息中的信息或把消息存储到数据库。
消息也许和事务上下文一起发送给Message-DrivenBean,这样onMessage方法中的所有操作都会被当作同一个事务或其中的一部分来处理。如果处理过程被回滚,消息就必须重发。详见第7章
何时需要Message-DrivenBean
用会话Bean和EntityBean也可以发送和接收JMS消息,但它们是同步的。而很多时候同步并不是必要的,这时候同步反而会强占很多服务器资源,这样我们可以采用异步方式来处理以减少资源消耗。需要异步消息处理就是用Message-DrivenBean。
5.定义客户端访问接口
注:本节内容不适用于Message-DrivenBean,因为它不需要通过接口访问:)
客户端只能通过会话Bean或者EntityBean的接口中定义的方法来访问它们。接口就相当于一个企业Bean的客户端视图。而企业Bean的方法实现、部署描述符设置、抽象数据模式和数据库访问对客户端都是透明的。设计优良的接口可以使J2EE应用程序的开发和维护更简单。优雅的接口不仅避免了客户端了解EJB层的复杂性,同时它们也使EJB内部实现的修改不会影响到客户端。甚至你把原来用BMP实现的EntityBean改为用CMP实现也不需要改变客户端的代码。但是如果你修改了接口中的方法声明,那么没办法客户端也只有作相应的修改了。就向地下工作者的联系暗号,如果上线的暗号变了,而下线还用旧暗号是不可能在联系上的了。因此,为了尽量使你的客户端不受EJB更改的影响,必须谨慎的设计接口。
在设计J2EE应用程序的时候,你一开就应该要讨论问题之一就是企业Bean允许客户端访问的方式:远程或者本地访问。
远程访问
一个企业Bean的远程客户端有以下特征:
它可以运行在一个与它访问的企业Bean不同的机器和一个不同的Java虚拟机(Java virtual machine JVM)环境中。但并不是必须的。
它可以是一个Web应用或者一个J2EE的应用程序客户端,也可以是其他的企业Bean。
对于远程客户端,企业Bean的位置是透明的。
要创建一个可以远程访问的企业Bean你必须为它编写一个Remote接口和一个Home接口。Remote接口定义商业方法,不同的企业Bean有不同的商业方法(这个是废话,因为企业Bean是根据商业逻辑划分的实体或者处理过程)。如BankAccountEJB有两个名字为debit(借)和credit(贷)的商业方法。Home接口定义企业Bean的生命周期方法create和remove方法。对EntityBean,Home接口还定义查找方法(finder)和家族(home)方法。查找方法用来定位EntityBean。家族方法是被调用以操作所有的EntityBean实例的,就是说这些方法的调用对于对应EntityBean的实力都起作用。下图是由接口组成的企业Bean的客户端视图。
图3-2
本地接口
企业Bean的本地客户端特征:
它必须和被调用的企业Bean在同一个java虚拟机环境中。
它可以是Web应用或者其他的企业Bean。
对于本地客户端,企业Bean的位置是不透明的。
它们通常是访问CMP的其他EntityBean。(一般是会话Bean。在J2EE设计模式一书中描述的会话外观模式就是这种情况,用会话Bean调用EntityBean,以免客户端反复调用EntityBean的细粒度数据方法。)
要创建一个允许本地访问的企业Bean,你必须编写一个Local接口和一个Local Home接口。相应的,Local接口定义商业方法,Local Home接口定义企业Bean的生命周期和查找方法(没有家族方法?)。
Local接口和CMR(Container-Managed RelationShips)
如果一个企业Bean是一个CMR的靶子,那么它必须有Local接口。关系的方向决定企业Bean是不是该关系的靶子。例如在图3-1中,ProductEJB就是它和LineItemEJB的单向关系中的靶子,应为LineItemEJB在本地访问ProductEJB,所以ProductEJB必须有Local接口。而LineItemEJB和OrderEJB也必须实现Local接口,因为它们的关系是双向的,也就是说都是该关系的靶子。
因为参与CMR的企业Bean(只有CMP可以参与CMR所以实际上这里只可能是CMP)是本地访问关系的靶子,所以它们必须被打包在同一个EJB JAR文件里。本地访问的重要好处是提高了性能--本地调用同长比远程调用要快得多。
两种访问方式的抉择
我们可以根据一下因素来决定是选用远程访问还是本地访问:
CMR:如果一个企业Bean是CMR的靶子,那么它必须实现本地访问。
企业Bean之间的关系是紧耦合还是松耦合:紧耦合的企业Bean互相依赖。例如一个订单必须有一条或者多条商品条目,这些条目脱离订单就毫无意义。表示它们的EntityBean OrderEJB和LineItemEJB是典型的紧耦合模型。紧耦合关系的企业Bean一般都在同一个商业逻辑单元里,而且他们通常会用频繁的相互调用。所以最好考虑在紧耦合的企业Bean之间使用本地访问,会大大的提高性能。
客户端的类型:如果企业Bean是被J2EE应用程序客户端访问,那么它必须允许远程访问。在生产环境中,客户端大多数情况下运行在和J2EE服务器不同的机器中。如果客户端是Web应用或者其他的企业Bean,那么它们也可以和被访问的企业Bean部署在同一个环境中,访问方法也取决于如何部署应用程序。
组件部署:J2EE应用程序是可升级的,因为服务器端的组件可以本分布在多个不同的机器中。例如一个分布式应用程序的Web应用可以运行在与它调用的企业Bean不同的服务器中。在这种分布式场景下,企业Bean必须允许远程访问。
如果你还不能确定使用哪种访问方式,那就选择远程访问。它可以让你的应用程序更灵活--在以后你可以任意分布部署你的应用程序组件以适应不断增长的需求。
虽然很少这样做,但企业Bean也可以两种访问方式同时与允许,这样它就要同时实现Remote和Local两组接口。
性能和访问方式
因为诸如网络反应时间之类的因素,远程调用会比本地调用慢。另一方面,如果在不同的服务器上分布组件,又可以提高应用程序的整体性能。这两种描述都是概念性的,实际情况中性能在不同的运行环境中会出现很大的变化。然而你应该谨记你的设计会给应用程序的性能造成什么样的影响。
方法参数和访问方式
访问方式会影响客户端调用的企业Bean方法的参数和返回值的类型。
隔离
远程调用中的参数是传值的,它们是对象的拷贝。但是本地调用的参数是传引用的,和一般的Java方法调用一样。
远程调用的形式参数核实参事相互隔离的。在调用过程中,客户端和企业Bean对不同对象拷贝操作。如果客户端改变了对象,企业Bean中的对应对象并不会跟着改变。这个隔离层可以保护企业Bean不会被客户端以外的修改数据。(这也造成了值对象模式的一些弊端,如不可以同步刷新而可能造成脏数据,见J2EE核心模式,值对象模式。可见有其利必有其弊:)
在本地调用的过程中,因为引用同一个对象,客户端和企业Bean都可能修改都操作同一个对象。但是一般情况下,请不要以来这种效果来实现什么功能,因为可能以后有一天你要分布部署你的组件,而需要用远程调用来替换本地调用。
数据访问粒度
因为远程调用会比本地调用慢,远程方法的参数应该设计成粗粒度对象。由于粗粒度对象比细粒度对象包含更多的数据,所以也减少了调用次数。(这也是值对象模式的初衷,下面的CustomerDetials对象就是值对象的一个例子)
例如,假设CustomerEJB是通过远程访问的。那么它就可能只有一个getter方法用来返回一个CustomerDetails对象。但是如果它是通过本地访问的,那么它就可以为每一个企业Bean的字段提供一个getter方法:getFirstName、getLastName、getPhoneNumber等等。因为本地调用要比远程调用快很多,这些多次getter方法的调用并不会明显的影响性能。(注意这里说的这些getter方法都是指在Remote或者Local接口里声明的客户端可访问的方法)
6,企业Bean的"内容"
要创建一个企业Bean,你必须提供一下这些文件:
部署描述符文件:一个描述企业Bean的持久性类型和事务属性等信息的XML文件。如果你是按照新建企业Bean向导的步骤来创建的话,Deploytool工具可以有效的创建部署描述符。
企业Bean的类文件:实现节口中定义的方法。
接口:如上节所述,对于远程调用需要实现Remote和Home接口,而本地调用需要实现Local和Local Home接口。而这些Message-DrivenBean是例外,它不需要任何接口。
辅助类:企业Bean类需要的其他类文件,像异常类和工具类等等。
你必须把这些文件打包进保存企业Bean的模块一个EJB JAR文件中。一个EJB JAR文件是可移植的并且可以在多个J2EE应用程序中使用。要装配拟议J2EE应用程序,你需要将一个或多个像EJB JAR文件一样的模块打包一个保存整个应用程序的存档EAR文件中。当你部署了这个EAR文件,企业Bean也同时被部署到J2EE服务器中。
7,企业Bean的命名约定
因为企业Bean是有多个部分组成,让应用程序遵循一定的命名约定是很有用的。下表示一个例子:
表 3-2 企业名的命名约定
项目 约定 实例
企业Bean命名(DD)
EJB的存档文件JAR命名(DD)
企业Bean主类命名
Home接口命名
Remote接口命名
Local home接口命名 Local
Local接口命名 Local
抽象数据模式命名(DD)
DD表示该项目是部署描述符文件里的项目。
其实命名约定指是一个习惯问题,不是什么要求或者必须遵守的技术规范。
8,企业Bean的生存周期
企业Bean的生命周期可以划分为几个阶段,不过不同类型的企业Bean如会话Bean,EntityBean和Message-DrivenBean都有不同的生命周期。下面的描述提到的方法都是围绕着接下来两章的示例代码展开的。如果你没有做个企业Bean的开发,可以跳过本节看一下示例代码先。
有状态会话Bean的生命周期:
图3-3显示了一个有状态会话Bean的生命周期中的三个阶段:从不存在到准备就绪到钝化,然后原路返回。客户端调用create方法(该方法在Home或者Local Home节口中声明)就开始了一个有状态会话Bean的生命周期(在此之前是Does Not Exist,不存在阶段)。EJB容器产生一个Bean的实例然后调用Bean类的setSessionContext和ejbCreate(和Home或者Local Home节口中被调用的create方法对应的ejbCreate)方法。这时该Bean实例进入准备就绪状态(Ready),它的商业方法可以被客户端调用了。
图 3-3 有状态会话Bean的生命周期
在就绪状态,EJB容器可能会把该Bean实例从内存中移出到外存以钝化该实例(EJB容器一般采用最近最少使用原则来钝化内存中的Bean实例)。其实就是把内存中EJB容器认为暂时不会用到的Bean实例转移到外存中,以提高内存使用效率,这好像是支持多线程操作系统的内存管理。在钝化Bean实例之前,EJB容器会先调用ejbPassivate方法,所以你如果想在钝化之前做什么动作的话就可以在该方法里实现。如果钝化期间有客户端调用该Bean实例的商业方法,EJB容器将该实例读入内存激活它回到就绪组状态(很有点线程的状态变化哦),同时调用该Bean类的ejbActivate方法。所以你要在Bean实例被激活的时候做点什么动作的话,就在这个方法里动点手脚就OK了:)
在企业Bean的生命周期最后,客户端调用remove(同样是Home或者Local Home接口里的)方法然后容器调用Bean类的ejbRemove方法结束了一个实例的生命。该实例就乖乖的等待垃圾收集器的召唤了,不知道等待黑白无常的心情如何,哎你认命吧,Bean兄弟。
在这些生命周期方法中,你可以在客户端编码调用的只有两个:create和remove方法(呵呵,上面说过一遍了),如图3-3中所示的其他方法都是由EJB容器来调用的。例如,ejbCreate方法在Bean类里,允许你在Bean类实例化后执行一些特定的操作,比如你想让它自己实例化后就报个到,喊一声"我来了",那把代码塞在这个方法里面就没错了。或者你是想让它跟要访问的数据库打个招呼,建个连接也在这里。
无状态会话Bean的生命周期
因为无状态会话Bean不需要保存任何状态信息,所以它不需要钝化,在不用的时候直接干掉就好了。所以它的生命周期只有两个阶段:不存在(Does Not Exist)和就绪(Ready)如下图3-4
图 3-4 无状态会话Bean的生命周期
EntityBean的生命周期
图3-5显示了EntityBean生命周期中的三个阶段:不存在、池状态和就绪。EJB容器创建企业Bean实例后调用它的setEntityContext方法,该方法将实体上下文(Entity Context)传递给Bean实例。然后该实例就被放入同类实例的Bean池中。在这个状态中,实例并没有和特定的EJB对象身份标志关联,池中所有实例都是等价的。EJB容器在把实例变为就绪状态时才为它指定一个特定的标志。(实体Bean映射商业实体,具有分辨实体的唯一主键标志)
有两种方法让实例从池状态到就绪状态。第一种方法,客户端调用create方法,然后EJB容器调用ejbCreate和ejbPostCreate方法。第二种方法,EJB容器调用ejbActive方法。只用在就绪状态下,企业Bean才可以为客户端提供服务。
图 3-5 Entity Bean的生命周期
在EntityBean的生命周期的最后,EJB容器从池中删除Bean实例并调用unsetEntityContext方法。
企业Bean实例在Bean池中并不和任何EJB实体标志关联。以BMP为例,在EJB容器把实例从池状态转到就绪状态时,它并不自动设置实例的主键,所以ejbCreate和ejbActivate方法必须对主键赋值。如果主键不正确,ejbLoad和ejbStore方法(这两个方法在实体Bean种执行和数据库同步数据操作,在BMP中要自己编码实现,BMP一章将详细介绍)就不能正确的在Bean字段和数据库数据之间执行同步。在SavingAccountEJB的例子中,ejbCreate方法通过自身的参数对主键赋值。EjbActivate方法用下面的语句对主键(id)赋值:
id = (String)context.getPrimaryKey();
在池状态下,企业Bean的字段的值是不需要的,你可以在ejbPassivate方法里把这些值置空(赋值为null)一边垃圾收集器可以收集它们。
Message-DrivenBean的生命周期
图3-6显示了Message-DrivenBean生命周期中的两个阶段:不存在和就绪。
EJB容器通常会创建一个Message-DrivenBean的实例池。对其中每一个实例EJB容器完成如下工作:
1. 调用setMessageDrivenContext方法传递上下文对象给实例。
2. 调用ejbCreate方法
图 3-6 Message-Driven Bean的生命周期
和无状态会话Bean一样,Message-DrivenBean也不会被钝化,只有两种状态:不存在和就绪(可以接收消息)。在生命周期的最后容器调用ejbRemove方法,实例就开始等待垃圾收集器的召唤了。
第4章 有状态会话Bean示例
会话Bean功能强大,因为它把客户端的区域扩展到了服务器(在服务器端保存客户端某些特定数据),然而它们仍然很容易创建。在第二章你已经建立了一个无状态会话Bean的例子ConvertEJB。这一章我们将创建一个购物车的有状态会话Bean CartEJB。
本章内容:
购物车会话Bean CartEJB
会话Bean类
Home接口
Remote接口
辅助类
运行该例子
其他企业Bean特征
访问环境实体
企业Bean的比较
传递企业Bean的对象引用
1.购物车会话Bean CartEJB
CartEJB描述一个在线书店的购物车。它的客户端的动作可能有:增加一本书,减少一本书,浏览购物车中存放的所有书。要建立该企业Bean,需要以下文件:
会话Bean类文件(CartBean.java)
Home接口(CartHome.java)
Remote接口(Cart.java)
所有的会话Bean都需要一个会话Bean类,所有允许远程访问的企业Bean都必须有一个Home接口和一个Remote接口(好像在哪说过了,不知道可不可以有多个:)。因为特定应用程序的需要,企业Bean可能也需要一些辅助类。本例的购物车用到两个辅助类:BookException和IdVerifier。这两个类将在后续节里介绍。
本例的原文件在j2eetutorial/examples/src/ejb/cart 目录下。要编译这些文件进入j2eetutorial/examples 目录执行如下命令:
ant cart
一个CartApp.ear的样本文件可以在j2eetutorial/examples/ears 目录下找到。
会话Bean类
本例的会话Bean类是CartBean类。以CartBean为例,下面列出所有会话Bean都具有的特征:
它们都实现SessionBean接口(呵呵,它什么也不做,只是继承了Serializable接口而已)
会话Bean类必须声明为public
会话Bean类不能被定义为abstract或者final(就是说它不能是抽象类,而且允许被继承)
至少实现一个ejbCreate方法
实现特定的商业方法
包含一个无参数的构造函数
不能定义finalize方法
以下是CartBean.java的代码:
import java.util.*;
import javax.ejb.*;
public class CartBean implements 会话Bean {
String customerName;
String customerId;
Vector contents;
public void ejbCreate(String person)
throws CreateException {
if (person == null) {
throw new CreateException("Null person not allowed.");
}
else {
customerName = person;
}
customerId = "0";
contents = new Vector();
}
public void ejbCreate(String person, String id)
throws CreateException {
if (person == null) {
throw new CreateException("Null person not allowed.");
}
else {
customerName = person;
}
IdVerifier idChecker = new IdVerifier();
if (idChecker.validate(id)) {
customerId = id;
}
else {
throw new CreateException("Invalid id: "+ id);
}
contents = new Vector();
}
public void addBook(String title) {
contents.addElement(title);
}
public void removeBook(String title) throws BookException {
boolean result = contents.removeElement(title);
if (result == false) {
throw new BookException(title + "not in cart.");
}
}
public Vector getContents() {
return contents;
}
public CartBean() {}
public void ejbRemove() {}
public void ejbActivate() {}
public void ejbPassivate() {}
public void setSessionContext(SessionContext sc) {}
}
SessionBean接口
SessionBean接口继承EnterpriseBean接口,后者又继承Serializable接口。SessionBean接口声明了ejbRemove,ejbActivate,ejbPassivate和setSessionContext方法。虽然本例中CartBean没有用到这些方法,但也要实现它们,也为它们在SessionBean节口中声明过。因此在CartBean中被实现为空方法。大家可能都注意到了,该接口没有声明ejbCreate方法,该方法因为形式太自由(有状态会话Bean在实现的时候可能会有不确定个的参数,实体Bean的ejbCreate方法就更不用说了,不过可惜的是实体Bean并不实现该接口:),所以该方法无法作为固定类型的方法在接口中声明,该方法在被EJB容器调用时也只有以方法名作为标志,这可不太符合面向对象的原则。随后介绍什么时候使用这些方法。
ejbCreate方法
因为企业Bean运行在EJB容器中,所以客户端不可能直接创建一个企业Bean的实例,只有EJB容器有能力这么做。下面是本例创建CartEJB实例的过程:
1. 客户端调用Home对象的create方法
Cart shoppingCart = home.create("Duke DeEarl","123");
2. EJB容器创建一个企业Bean(本例中是CartEJB)的实例
3. EJB容器调用企业Bean相应的ejbCreate方法,CartEJB中对应的方法:
public void ejbCreate(String person, String id)
throws CreateException {
if (person == null) {
throw new CreateException("Null person not allowed.");
}
else {
customerName = person;
}
IdVerifier idChecker = new IdVerifier();
if (idChecker.validate(id)) {
customerId = id;
}
else {
throw new CreateException("Invalid id: "+ id);
}
contents = new Vector();
}
一般地,ejbCreate方法初始化企业Bean的状态。例如上述的ejbCreate方法就通过方法参数初始化customerName和customerId两个变量。
企业Bean必须至少实现一个ejbCreate方法(这句话好像重复了很多遍了,没办法这个确实很重要嘛:)。该方法签名必须符合以下要求:
方法的访问权修饰必须是public
返回值类型必须是void
如果该企业Bean允许远程访问,那么方法的参数必须是符合Java远程方法调用API(Java Remote Method Invocation,Java RMI)规则的合法类型,就是说必须是实现了Serializable接口或它的字接口的类的对象。
方法不可以是static或final的
throws子句必须包含javax.ejb.CreateException异常。EjbCreate方法遇到非法的参数也会抛出该异常。
商业方法
会话Bean的主要作用就是执行客户端要求的商业逻辑。客户端调用Home对象的create方法返回的远程对象引用中的商业方法,在客户端透视图中,这些商业方法好像是在本地运行,实际上它们确实在远程会话Bean中运行。下面的代码片断展示了CartClient客户端如何调用商业方法:
Cart shoppingCart = home.create("Duke DeEarl", "123");
...
shoppingCart.addBook("The Martian Chronicles");
shoppingCart.removeBook("Alice In Wonderland");
bookList = shoppingCart.getContents();
CartBean类中商业方法的实现如下
public void addBook(String title) {
contents.addElement(new String(title));
}
public void removeBook(String title) throws BookException {
boolean result = contents.removeElement(title);
if (result == false) {
throw new BookException(title + "not in cart.");
}
}
public Vector getContents() {
return contents;
}
商业方法必须遵循以下规则:
方法名不能和EJB体系定义的方法冲突,例如不可以命名为ejbCreate或者ejbActivate
访问权修饰符必须是public
如果企业Bean允许远程访问,参数和返回值必须符合JavaRMI API调用规则
不可以用statci和final修饰符(和ejbCreate一样)
throws子句可以包含任意类型的异常,不过要有意义。例如removeBook方法就会在购物车中没有要删除的书是抛出BookException异常。
为了可以指出像无法连接数据库这样的系统错误,商业方法的应该在这时抛出javax.ejb.EJBException异常。当商业方法抛出该异常,容器会将其包装到一个RemoteException异常中,后者会被客户端捕获。容器不会擅自包装应用程序自定义异常,如本例中的BookException异常。因为EJBException异常是RutimeException(该异常类型系统会自动处理)异常类的子类,所以你不需要把它也加入到商业方法的throws子句中。
Home接口
企业Bean的Home接口继承javax.ejb.EJBHome接口。对于会话Bean,Home接口的目标是要定义客户端可访问create方法。如本例的CartClient客户端:
Cart shoppingCart = home.create("Duke DeEarl", "123");
Home接口中的每一个create方法都对应一个企业Bean类中的ejbCreate方法。CartBean的ejbCreate方法声明如下:
public void ejbCreate(String person) throws CreateException
...
public void ejbCreate(String person, String id)
throws CreateException
对比一下CartHome接口中的create方法声明:
import java.io.Serializable;
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;
public interface CartHome extends EJBHome {
Cart create(String person) throws
RemoteException, CreateException;
Cart create(String person, String id) throws
RemoteException, CreateException;
}
以上两个文件中的ejbCreate和create方法声明很相似,但是也有重要的不同。下面是Home接口的create方法声明规则:
参数个数和类型必须和对应的ejbCreate方法一样,就是说要为企业Bean的每一个ejbCreate方法(永远不会用到的除外:)在Home接口中声明一个对应的参数个数类型和顺序一模一样的create方法
参数和返回值类型必须符合RMI调用规则
create方法的返回值类型是企业Bean的Remote接口类型(对应的ejbCreate方法返回void)
throws子句中必须包含java.rmi.RemoteException和javax.ejb.CreateException异常
Remote接口
Remote接口继承EJBObject接口,它定义客户端调用的商业方法。下面是本例中的Remote接口Cart.java的代码:
import java.util.*;
import javax.ejb.EJBObject;
import java.rmi.RemoteException;
public interface Cart extends EJBObject {
public void addBook(String title) throws RemoteException;
public void removeBook(String title) throws
BookException, RemoteException;
public Vector getContents() throws RemoteException;
}
Remote接口中的方法定义规则如下:
每一个商业方法声明必须在企业Bean类里实现有一个对应的商业方法
必须与对应的企业Bean类实现的商业方法的声明签名一样,就是说参数类型个数顺序和返回值类型都必须一样。
参数和返回值类型也必须符合RMI调用规则
throws子句必须包含RemoteException异常,就是在对应的企业Bean商业方法的throws子句的基础上加一个RemoteException组成Remote接口中相应方法的throws子句。
辅助类
本例有两个辅助类:BookException和IdVerifier。BookException异常类在removeBook方法找不到要删除的书时被抛出。IdVerifier在ejbCreate方法中被调用来验证custormerId的有效性。辅助类文件必须和调用它们的企业Bean类文件打包到同一个EJB JAR文件中。
运行本例
1. 启动J2EE服务器和deploytool工具
2. 在deploytool工具中打开j2eetutorial/examples/ears/CartApp.ear 文件,你可以在图4-1中看到本例的CartApp应用程序
3. 部署CartApp应用程序(Tools'Deploy)。在进入的对话框中确认你已经选择了Return Client JAR复选框
4. 运行
a) 在终端窗口中进入j2eetutorial/examples/ears 目录
b) 将环境变量APPCPATH设置为CartAppClient.jar文件的目录
c) 执行如下命令:
runclient -client CartApp.ear -name CartClient -textauth
d) 出现登录提示符后,输入用户名:guest,密码:guest123
图 4-1 CartApp 应用程序的General选项面板
二 其他的企业Bean特性
下面的这些特性是会话Bean和实体Bean的公有特性。
访问环境变量
在部署描述符中存储的环境变量是你不改变企业Bean的源代码也可以定制商业逻辑的名字-值对。例如一个计算商品折扣的企业Bean,用一个环境变量Discount Percent来存储折扣比例。在应用程序部署前,你可以用deploytool在Env Entries页里给Discount Percent赋值0.05。(如图4-2)当你运行该程序,企业Bean从它的运行环境中得到0.05的折扣比例。
Figure 4-2 CheckerBean的Env.Entries页
在下面的示例代码中,applyDiscount方法根据购买数量用环境变量计算折扣。首先,它以java:comp/env参数调用lookup方法来定位环境命名上下文对象,然后用环境上下文对象的lookup方法得到Discount Level和Discount Percent环境变量的值。这里将把0.05赋给discountPercent变量。applyDiscount方法是CheckerBean类的方法,该类的源文件放在j2eetutorial/examples/src/ejb/checker 目录下,对应的样本CheckerApp.ear文件放在j2eetutorial/examples/ears目录下。
public double applyDiscount(double amount) {
try {
double discount;
Context initial = new InitialContext();
Context environment =
(Context)initial.lookup("java:comp/env");
Double discountLevel =
(Double)environment.lookup("Discount Level");
Double discountPercent =
(Double)environment.lookup("Discount Percent");
if (amount >= discountLevel.doubleValue()) {
discount = discountPercent.doubleValue();
}
else {
discount = 0.00;
}
return amount * (1.00 - discount);
} catch (NamingException ex) {
throw new EJBException("NamingException: "+
ex.getMessage());
}
}
企业Bean的比较
客户端可以调用isIdentical方法来判断两个有状态会话Bean是否等价:
bookCart = home.create("Bill Shakespeare");
videoCart = home.create("Lefty Lee");
...
if (bookCart.isIdentical(bookCart)) {
// true ... }
if (bookCart.isIdentical(videoCart)) {
// false ... }
因为无状态会话Bean的对象都是等价的,所以用isIdentical方法比较他们时总是返回真。
实体Bean的等价判断也可以用isIdentical方法,不过这里还有另外一种方法,就是比较它们的主键:
String key1 = (String)accta.getPrimaryKey();
String key2 = (String)acctb.getPrimaryKey();
if (key1.compareTo(key2) == 0)
System.out.println("equal");
访问企业Bean的远程对象引用
有时你的企业Bean需要提供一个自己的引用给其他企业Bean。例如你可以通过引用使企业Bean可以调用另一个企业Bean的方法。你无法访问this引用因为它指向在EJB容器中运行的Bean实例,只有容器可以直接调用Bean实例的方法。客户端通过远程接口实现对象间接调用Bean的方法,通过这些对象(远程接口实现对象)的引用企业Bean可以互相访问。
会话Bean调用SessionContext接口定义的getEJBObject方法获得它的远程接口对象引用。而实体Bean调用的是EntityContext接口定义的getEJBObject方法。这两个借口提供企业Bean访问EJB容器管理的上下文对象。典型情况下,企业Bean通过setSessionContext方法保存它的上下文对象。下面的代码片断说明了会话Bean如何使用这些方法:
public class WagonBean implements SessionBean {
SessionContext context;
...
public void setSessionContext(SessionContext sc) {
this.context = sc;
}
...
public void passItOn(Basket basket) {
...
basket.copyItems(context.getEJBObject());
}
...
第5章 BMP的例子
Dale Green 著
IceShape Zeng 译
数据是大多数商业应用程序的核心(这个好像是废话,没有数据程序也只能玩玩了)。在J2EE应用程序中,实体Bean表示存储在数据库中的商业实体。如果用BMP实现实体Bean,你必须自己编写数据库访问代码。虽然写这些代码是附加的责任,但是因此你可以更灵活的控制实体Bean访问数据库的行为(当然这得你自己愿意才行)。
本章讨论BMP实现实体Bean的编码技术。关于实体Bean的概念请参考第3章企业Bean中实体Bean一节。
本章内容:
1.SavingsAccountEJB
实体Bean类
Home接口
Remote接口
运行该例子
2.用deploytool部署BMP实现的实体Bean
3.为BMP映射表间关系
一对一关系
一对多关系
多对多关系
4.BMP的主键
主键类
实体Bean中的主键
获取主键
5.处理异常
一.SavingsAccountEJB
本例中的实体Bean表示一个样本银行账号。SavingsAccountEJB的状态信息保存在一个关系数据库的savingaccount表中。创建该表的SQL语句如下:
CREATE TABLE savingsaccount
(id VARCHAR(3)
CONSTRAINT pk_savingsaccount PRIMARY KEY,
firstname VARCHAR(24),
lastname VARCHAR(24),
balance NUMERIC(10,2));
SavingsAccountEJB由三个文件组成:
实体Bean类(SavingsAccountBean)
Home接口(SavingsAccountHome)
Remote接口(SavingsAccount)
本例应用程序还包括下面的两个类:
一个异常类InsufficientBalanceException
一个客户端类SavingsAccountClient
该例子的源代码文件在j2eetutorial/examples/src/ejb/savingsaccount 目录下,可以在j2eetutorial/examples 目录下用ant savingsaccount 来编译这些代码。一个样本SavingsAccountApp.ear文件放在j2eetutorial/examples/ears 目录下。
实体Bean类(SavingsAccountBean)
SavingsAccount是本例中的实体Bean类。从它的代码可以看出BMP实现实体Bean的基本要求。首先实现一下接口和方法:
EntityBean接口
大于等于零对的ejbCreate和ejbPostCreate方法(实体Bean可以没有ejbCreate方法,只有查找方法,但是不能两者都没有)
查找(Finder)方法
商业方法
Home方法(这里的Home方法不好理解,如果是指生命周期方法,应该包含ejbcreate等方法)
另外它还有如下的一些特征:
该类访问属性为public
该类不可以被定义为abstract或者final
该类包含一个空构造函数
该类不可以实现finalize方法
EntityBean接口
该接口也继承至EnterpriseBean接口(EnterpriseBean接口是SessionBean和EntityBean共同的父接口,它继承至Serializable接口,没有任何方法)。EntityBean定义一些方法,如ejbActive、ejbLoad和ejbStore等等,你必须在实体Bean类里实现它们。EntityBean接口的定义如下:
package javax.ejb;
import java.rmi.RemoteException;
// Referenced classes of package javax.ejb:
// EnterpriseBean, EJBException, RemoveException, EntityContext
public interface EntityBean
extends EnterpriseBean
{
public abstract void setEntityContext(EntityContext entitycontext)
throws EJBException, RemoteException;
public abstract void unsetEntityContext()
throws EJBException, RemoteException;
public abstract void ejbRemove()
throws RemoveException, EJBException, RemoteException;
public abstract void ejbActivate()
throws EJBException, RemoteException;
public abstract void ejbPassivate()
throws EJBException, RemoteException;
public abstract void ejbLoad()
throws EJBException, RemoteException;
public abstract void ejbStore()
throws EJBException, RemoteException;
}
ejbCreate方法
当客户端调用create方法后,EJB容器调用对应的ejbCreate方法。实体Bean中典型的ejbCreate方法完成如下工作:
将实体状态(表述实体Bean的属性字段)插入数据库
初始化实例变量,就是对实体Bean的属性字段赋值
返回主键
本例中SavingsAccountBean的ejbCreate方法调用私有方法insertRow来将实体状态插入数据库,insertRow方法向数据库发出一条INSERT的SQL命令。下面是ejbCreate方法的代码:
public String ejbCreate(String id, String firstName,
String lastName, BigDecimal balance)
throws CreateException {
if (balance.signum() == -1) {
throw new CreateException
("A negative initial balance is not allowed.");
}
try {
insertRow(id, firstName, lastName, balance);
} catch (Exception ex) {
throw new EJBException("ejbCreate: " +
ex.getMessage());
}
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.balance = balance;
return id;
}
虽然SavingsAccountBean类只有一个ejbCreate方法,但是一个企业Bean可以有多个ejbCreate方法。例如前一章CartEJB的例子。
编写实体Bean的ejbCreate方法许要遵循如下规则:
访问权修饰符必须是public
返回值类型必须是主键类
参数类型必须符合RMI调用规则
该方法不可以声明为final或者static
throws子句要包含javax.ejb.CreateException异常,通常遇到非法参数ejbCreate方法会抛出该异常。如果ejbCreate方法因为存在使用相同主键的其他实体而无法创建实体,将抛出javax.ejb.DuplicateKeyException异常,它是CreateException的子类。如果客户端捕获CreateException或者DuplicateKeyException异常,就表示实体创建失败。
实体Bean的状态数据可能被J2EE服务器不知道的其他应用程序直接插入到数据库里。例如,你可以直接用SQL语句在数据库工具中插入一行到savingsaccount表里。虽然该行对应的实体Bean没有被ejbCreate方法创建,但客户端仍然可以找到该行对应的实体Bean(不用说,这里当然是用ejbFinder方法了,下面会介绍)。
EjbPostCreate方法
对每一个ejbCreate方法,你都必须在实体Bean中写一个对应的ejbPostCreate方法。因为EJB容器在调用ejbCreate方法后接着就调用ejbPostCreate方法。跟ejbCreate方法不同的是,ejbPostCreate方法可以调用EntityContext接口的getPrimaryKey和getEJBObject方法(在前一章的传递企业Bean对象的引用一节讨论过)。EjbPostCreate方法大部分情况下什么事也不干。
EjbPostCreate方法声明必须符合一下要求:
参数数量类型声明顺序必须跟对应的ejbCreate方法相同
必须声明为public
不能声明为final或者static
返回值必须是void
throws子句要包含javax.ejb.CreateException异常。
ejbRemove方法
客户端调用remove方法来删除实体Bean。该调用会引发EJB容器调用ejbRemove方法,ejbRemove方法从数据库中删除实体状态对应的数据。SavingsAccountBean的ejbRemove方法调用deleteRow私有方法向数据库发送一条DELETE的SQL命令。代码如下:
public void ejbRemove() {
try {
deleteRow(id);
catch (Exception ex) {
throw new EJBException("ejbRemove: " +
ex.getMessage());
}
}
如果ejbRemove方法遇到系统问题就抛出javax.ejb.EJBException异常。如果遇到一个应用程序异常就怕抛出javax.ejb.RemoveException异常。要区分系统异常和应用程序异常请参考异常处理一节。
实体Bean状态数据也可能被直接从数据库中删除。当用SQL语句删除数据库中实体Bean状态数据对应的行时,实体Bean也会被删除。
ejbLoad和ejbStore方法
如果EJB容器要同步实体Bean的属性子段和数据库中对应的数据就调用这两个方法。顾名思义,ejbLoad方法从数据库中取出数据并刷新属性子段的值,ejbStore方法把属性子段值写入数据库。客户端不能调用这两个方法。
如果商业方法在一个事务环境中执行,EJB容器会在该方法执行前调用ejbLoad,并在该方法执行完后立即调用ejbStore方法。这样你不必在商业方法中调用这两个方法来刷新和存储实体Bean数据。SavingsAccountBean依赖容器同步实体和数据库的数据,所以商业方法应该在事务环境中执行。
如果ejbLoad和ejbStore方法不能在数据库中找到实体的数据就会抛出javax.ejb.NosuchEntityException异常,它是EJBException的子类。因为它的父类是RutimeException的子类,所以你不必将该异常加到throws子句中。该异常在返回客户端前被容器封装进RemoteException的一个实例。
在SavingsAccountBean类中,ejbLoad调用LoadRow私有方法,后者向数据库发送一条SELECT的SQL命令并将读出的数据赋给SavingsAccountBean的属性字段。EjbStore调用storeRow私有方法,后者用UPDATE的SQL命令将SavingsAccountBean的属性字段值存入数据库。下面是这两个方法的代码:
public void ejbLoad() {
try {
loadRow();
} catch (Exception ex) {
throw new EJBException("ejbLoad: " +
ex.getMessage());
}
}
public void ejbStore() {
try {
storeRow();
} catch (Exception ex) {
throw new EJBException("ejbStore: " +
ex.getMessage());
}
}
查找方法(Finder)
查找方法允许客户端查找实体Bean。SavingsAccountClient可以通过三个查找方法查找实体Bean:
SavingsAccount jones = home.findByPrimaryKey("836");
...
Collection c = home.findByLastName("Smith");
...
Collection c = home.findInRange(20.00, 99.00);
对每一个客户端可用的查找方法,实体Bean类必须事先一个对应的ejbFind为前缀的方法。SavingsAccountBean中对应上面findByLastName方法的ejbFindByLastNamef方法代码如下:
public Collection ejbFindByLastName(String lastName)
throws FinderException {
Collection result;
try {
result = selectByLastName(lastName);
} catch (Exception ex) {
throw new EJBException("ejbFindByLastName " +
ex.getMessage());
}
return result;
}
查找方法的实现细节用应用程序决定,例如上例的ejbFindByLastName和ejbFindInRange方法的命名都是很随意的。但是ejbFindByPrimaryKey方法命名不能随意,就像它的名字所暗示的,该方法有一个用来查找实体Bean的主键类型的参数。SavingsAccountBean类中,主键是id子段。下面是SavingsAccountBean的ejbFindByPrimaryKey方法的实现代码:
public String ejbFindByPrimaryKey(String primaryKey)
throws FinderException {
boolean result;
try {
result = selectByPrimaryKey(primaryKey);
} catch (Exception ex) {
throw new EJBException("ejbFindByPrimaryKey: " +
ex.getMessage());
}
if (result) {
return primaryKey;
}
else {
throw new ObjectNotFoundException
("Row for id " + primaryKey + " not found.");
}
}
上面的实现也许对你来说有点陌生,因为它的参数和返回值都是主键类。不过,记住客户端并不直接调用ejbFindByPrimaryKey方法,而是由EJB容器代劳的。客户端只能调用在Home接口中声明的findByPrimaryKey方法。
下面总结一下BMP实现实体Bean的查找方法的规则:
必须实现ejbFindByPrimaryKey方法
方法名必须用ejbFind做前缀
方法不能用final或者static方法
如果BMP实现Remote接口组中的方法,则方法的参数和返回值类型必须是符合RMI API调用的合法类型
返回类型必须是主键类或者主键类的集合
Throws字句要包括javax.ejb.FinderException。如果查找方法返回一个主键类对象但是被查找的实体不存在,该方法将抛出javax.ejb.ObjectNotFoundException,该异常是javax.ejb.FinderException的子类。如果查找方法返回主键类对象集合,而且也没有符合要求的实体,则该方法返回空集合。
商业方法
商业方法处理你想封装在实体Bean中的商业逻辑。一般商业方法并不访问数据库,以使你可以把商业逻辑和数据库访问代码分离。SavingsAccountBean包含以下一些商业方法:
public void debit(BigDecimal amount)
throws InsufficientBalanceException {
if (balance.compareTo(amount) == -1) {
throw new InsufficientBalanceException();
}
balance = balance.subtract(amount);
}
public void credit(BigDecimal amount) {
balance = balance.add(amount);
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public BigDecimal getBalance() {
return balance;
}
SavingsAccountClient客户端这样吊用这些商业方法:
BigDecimal zeroAmount = new BigDecimal("0.00");
SavingsAccount duke = home.create("123", "Duke", "Earl",
zeroAmount);
...
duke.credit(new BigDecimal("88.50"));
duke.debit(new BigDecimal("20.25"));
BigDecimal balance = duke.getBalance();
会话Bean和实体Bean的商业方法的签名规则是相同的:
方法名不能和EJB体系定义的方法名冲突,比如商业方法不能命名为:ejbCreate或者ejbActivate
访问修饰必须是public
Remote接口租中定义的方法的参数和返回值必须是RMI API规范的合法类型
Throws子句没有特殊要求。例如debit方法抛出InsufficientBalaceException自定义异常。为了捕获系统异常,商业方法可以抛出javax.ejb.EJBException。
Home方法
Home方法包含那些应用于一个特定企业Bean类的所有实例的商业逻辑。正好和只应用于由主键标志的企业Bean单个实例的商业方法相反。Home方法调用期间,企业Bean实例不仅没有唯一的标志主键,也不具备表示一个商业实体的状态。因此,Home方法不可以访问企业Bean的持久性字段(企业Bean中的实例变量)。(对于CMP,Home方法也不能访问关系字段。)
Home方法查找到企业Bean的实例的集合然后通过集合的迭代器调用商业方法是实现Home方法的典型做法。SavingsAccountBean类的ejbHomeChargeForLowBalance就是这样做的。该方法对余额少于某个特定数值的账户收取服务费,首先调用findInRange方法得到符合条件所有的账户,它返回一个SavingAccount实现类实例的集合,然后通过集合的迭代器访问这些远程接口对象,在检查了余额之后调用debit商业方法收取费用。下面是代码:
public void ejbHomeChargeForLowBalance(
BigDecimal minimumBalance, BigDecimal charge)
throws InsufficientBalanceException {
try {
SavingsAccountHome home =
(SavingsAccountHome)context.getEJBHome();
Collection c = home.findInRange(new BigDecimal("0.00"),
minimumBalance.subtract(new BigDecimal("0.01")));
Iterator i = c.iterator();
while (i.hasNext()) {
SavingsAccount account = (SavingsAccount)i.next();
if (account.getBalance().compareTo(charge) == 1) {
account.debit(charge);
}
}
} catch (Exception ex) {
throw new EJBException("ejbHomeChargeForLowBalance: "
+ ex.getMessage());
}
}
该方法在Home接口中对应的定义为chargeForLowBalance(稍后的Home方法定义将具体介绍)。客户端可以通过该接口访问Home方法:
SavingsAccountHome home;
...
home.chargeForLowBalance(new BigDecimal("10.00"),
new BigDecimal("1.00"));
综上所述,实体Bean类中Home方法的实现要遵循以下规则:
方法名必须以ejbHome开头
访问修饰符必须是public
方法不可以是static方法
根据应用程序逻辑确定throws子句,throws子句中不能包含java.rmi.RemoteException异常。
数据库访问
表5-1列出了SavingsAccountBean的数据库访问操作。商业方法并不在表中,因为它们不访问数据库,它们只是更新企业Bean的持久性子段,这些字段的值会在EJB容器调用ejbStore方法时被写入数据库。但是这并不是强制的规则,你也可以在商业方法中直接访问数据库来存取数据,这要根据你的应用程序的具体需要来决定。
访问数据库前你必须先得到数据库的连接,关于数据库连接的更多信息将在16章讨论。
表 5-1 SavingsAccountBean中的SQL语句
方法 SQL 语句
ejbCreate INSERT
ejbFindByPrimaryKey SELECT
ejbFindByLastName SELECT
ejbFindInRange SELECT
ejbLoad SELECT
ejbRemove DELETE
ejbStore UPDATE
Home接口
Home接口定义了让客户端创建和查找实体Bean的方法。本例中SavingsAccountHome接口的实现如下:
import java.util.Collection;
import java.math.BigDecimal;
import java.rmi.RemoteException;
import javax.ejb.*;
public interface SavingsAccountHome extends EJBHome {
public SavingsAccount create(String id, String firstName,
String lastName, BigDecimal balance)
throws RemoteException, CreateException;
public SavingsAccount findByPrimaryKey(String id)
throws FinderException, RemoteException;
public Collection findByLastName(String lastName)
throws FinderException, RemoteException;
public Collection findInRange(BigDecimal low,
BigDecimal high)
throws FinderException, RemoteException;
public void chargeForLowBalance(BigDecimal minimumBalance,
BigDecimal charge)
throws InsufficientBalanceException, RemoteException;
}
定义create方法
create方法的定义规则:
必须和企业Bean类中对应的ejbCreate方法有相同的参数数量、类型和排列顺序。就是要为每个Home中的定义的可用create方法在企业Bean中实现对应的ejbCreate方法。
返回企业Bean的远程接口类型
throws子句包括对应的ejbCreate方法和ejbPostCreate方法的throws子句中出现的所有异常,另外还要包括javax.ejb.CreateException
如果方法是在Home接口而不是在LocalHome接口中定义的则throws子句必须包括java.rmi.RemoteException异常
定义查找方法
跟create的规则相似,Home接口中的每个查找方法都对应一个企业Bean类中的一个查找方法。Home接口中的查找方法必须以find开头,企业Bean类中对应的方法以ejbFind开头。本例中SavingsAccountHome接口定义的findByLastName对应的SavingsAccountBean类的方法为ejbFindByLastName方法。总结一下Home接口中的方法必须符合一下条件:
参数个数类型和顺序必须和对应的ejbFind方法相同
返回实体Bean远程接口类型,或是远程接口类型的集合
throws字据除了包括对应的ejbFind方法的throws子句中出现的异常外,还要包括javax.ejb.FinderException
不是在Local Home接口中定义的方法throws子句还要包括java.rmi.RemoteExceotion异常
定义Home方法
Home接口中的每个Home方法都在实体Bean类中有一个对应的方法,这些方法的命名没有特殊的前缀,相反对应的实体Bean类中的方法要以ejbHome开头。如本例中SavingsAccountBean类中前面提到的ejbHomeChargeForLowBalance方法,在Home接口中的对应方法名为chargeForLowBalance。
除了Home方法不抛出FinderException,它的签名规则和查找方法是一样的。
Remote接口
Remote接口继承javax.ejb.EJBObject接口,定义远程客户端访问的商业方法。本例中SavingsAccount远程接口定义如下:
import javax.ejb.EJBObject;
import java.rmi.RemoteException;
import java.math.BigDecimal;
public interface SavingsAccount extends EJBObject {
public void debit(BigDecimal amount)
throws InsufficientBalanceException, RemoteException;
public void credit(BigDecimal amount)
throws RemoteException;
public String getFirstName()
throws RemoteException;
public String getLastName()
throws RemoteException;
public BigDecimal getBalance()
throws RemoteException;
}
会话Bean和实体Bean的远程方法的定义规则是相同的,这里再重复一下:
每个方法在企业Bean类中都必须有对应的方法
方法签名必须和企业Bean类中的对应方法相同
参数和返回值必须是符合RMI规范的类型
throws子句企业Bean类对应方法的throws子句的基础上添加java.rmi.RemoteException
而Local接口有所不同:
参数和返回值不需要是RMI合法类型
throws子句不需要包括java.rmi.RemoteException
运行本例子
配置数据库
本例使用Cloundscape数据库,该数据库软件被包括在J2EE SDK包里。
1. 启动数据库。在命令方式下执行如下命令
cloudscape -start
关闭命令为:cloudscape -stop
2. 创建savingsaccount表
a) 进入j2eetutorial/examples 目录
b) 执行ant create-savingsaccount-table 命令
你也可以用其他数据库来运行本例(要是J2EE服务器支持的数据库)。在其他数据库中创建该表要执行j2eetutorial/examples/sql/savingsaccount.sql 脚本文件。
部署应用程序
1. 用deploy工具打开j2eetutorial/examples/ears/SavingsAccountApp.ear 文件
2. 执行Tools/Deploy菜单命令部署。确定Introduction对话中你选中了Return Client JAR复选框
运行客户端
1. 在命令方式下进入j2eetutorial/examples/ears 目录
2. 设置APPPATH环境变量为SavingsAccountAppClient.jar 所在目录
3. 执行以下命令(只有一条命令,有点长):
runclient -client SavingsAccountApp.ear -name SavingsAccountClient -textauth
4. 在登录提示符下输入用户名guest,密码guest123
5. 这一步什么也不做,看看结果:
balance = 68.25
balance = 32.55
456: 44.77
730: 19.54
268: 100.07
836: 32.55
456: 44.77
4.00
7.00
二.用deploytool部署BMP实现的实体Bean
第4章介绍了创建一个会话Bean包的步骤,创建实体Bean包的步骤相似,但是有以下不同:
1. 在新建企业Bean向导(New Enterprise Bean)中,指定企业Bean类型和持久性管理类型
a) 在General对话框中,选定Entity单选项
b) 在Entity Settings对话框中,选定Bean-Managed Persistence
2. 在Resource Refs页,指定企业Bean引用的资源工厂。这些设置使企业Bean可以访问数据库。具体的设置信息参考用deploytool工具配置资源引用一节
3. 部署前,检查你的JNDI名是否都是对的
a) 从树中选择要部署的应用程序
b) 选择JNDI Names页
三.为BMP映射表间关系
在关系数据库中,数据表可以通过共同的列建立关系。数据库的关系影响了他们对应的实体Bean的设计。本节分以下几类讨论实体Bean如何映射数据库中的表间关系
一对一
一对多
多对多
一对一关系
一对一关系中一个表1的一行数据只对应于表2的一行数据,表2的一行数据也只对应表1的一行数据。例如:一个仓库应用程序中,储藏箱(storagebin)表和小物件(widget)表就可能是一对一关系。这个应用程序要为物理仓库中每个储藏箱只装一个小物件并且每个小物件也只能装在一个储藏箱中建立逻辑模型。
图5-1说明了两个表的关系。因为storagebinid字段可以唯一确定storagebin表中的一行,它是这个表的主键。widgetid是widget表的主键。storagebin表也有一个widgetid字段来关联两张表的数据。通过在storagebin表中引用widget表的主键,可以确定一个物件存储在仓库中的哪个储存箱中。因为storagebin中widget字段引用其他表的主键,所以它是一个外键。(本章的图例中用PK表示主键,FK表示外键。)
图 5-1 一对一关系
一般子表包含一个匹配父表数据的外键。子表storagebin中的外键widgetid的值依赖于父表widget的主键值。如果storagebin表中有一行的widgetid值为344,那么widget表中一定有一行数据的widgetid也是344。
在设计数据库应用程序时,你必须保证子表和父表之间的依赖关系正确无误。有两种方法可以实现这种保证:在数据库中定义参照约束或者在应用程序中编码检查。本例中storagebin表定义了一个参照约束fk_widgetid:
CREATE TABLE storagebin
(storagebinid VARCHAR(3)
CONSTRAINT pk_storagebin PRIMARY KEY,
widgetid VARCHAR(3),
quantity INTEGER,
CONSTRAINT fk_widgetid
FOREIGN KEY (widgetid)
REFERENCES widget(widgetid));
下面讲到的例子的源文件可以在j2eetutorial/examples/src/ejb/storagebin目录下找。在j2eetutorial/examples目录下执行storagebin命令编译这些文件。StorageBinApp.ear样本文件放在j2eetutorial/examples/ears目录下。
StorageBinBean和WidgetBean类文件实现了storagebin表和widget表之间的一对一关系。StorageBinBean类为storagebin表中包括外键widgetid的所有列生命了对应的实例变量:
private String storageBinId;
private String widgetId;
private int quantity;
ejbFindByWidgetId方法返回跟传入参数widgetId值匹配的storageBinId值:
public String ejbFindByWidgetId(String widgetId)
throws FinderException {
String storageBinId;
try {
storageBinId = selectByWidgetId(widgetId);
} catch (Exception ex) {
throw new EJBException("ejbFindByWidgetId: " +
ex.getMessage());
}
if (storageBinId == null) {
throw new ObjectNotFoundException
("Row for widgetId " + widgetId + " not found.");
}
else {
return storageBinId;
}
}
ejbFindByWidgetId方法通过调用selectByWidgetId方法来得到数据库的查询结果(这种做法的好处是将商业方法和数据库操作方法分开,以使商业逻辑尽量的独立于数据库操作):
private String selectByWidgetId(String widgetId)
throws SQLException {
String storageBinId;
String selectStatement =
"select storagebinid " +
"from storagebin where widgetid = ? ";
PreparedStatement prepStmt =
con.prepareStatement(selectStatement);
prepStmt.setString(1, widgetId);
ResultSet rs = prepStmt.executeQuery();
if (rs.next()) {
storageBinId = rs.getString(1);
}
else {
storageBinId = null;
}
prepStmt.close();
return storageBinId;
}
客户端通过调用findByWidgetId方法(该方法在Home接口中定义,它对应与Bean类中的ejbFindByWindgetId方法)来找到物件存放的储藏箱:
String widgetId = "777";
StorageBin storageBin =
storageBinHome.findByWidgetId(widgetId);
String storageBinId = (String)storageBin.getPrimaryKey();
int quantity = storageBin.getQuantity();
运行StorageBinApp应用程序
1. 创建storagebin数据表
a) 进入j2eetutorial/examples目录
b) 执行Type ant create-storagebin-table命令
2. 部署StorageBinApp.ear文件(在j2eetutorial/examples/ears directory目录下)
3. 运行客户端
a) 进入j2eetutorial/examples/ears目录
b) 将APPCPATH环境变量设置为StoreageBinAppClient.jar所在目录
c) 执行如下命令(只有一条命令)
runclient -client StorageBinApp.ear -name StorageBinClient -textauth
d) 在登录提示符下输入用户名guest和密码guest123
一对多关系
如果父表的一个主键值在子表中匹配多条纪录,就是一对多关系。一对多关系在数据库应用程序中经常出现。例如运动社团的应用中会有一个team(组)表和一个player(运动员)表。每个组中不会只有一个运动员,但每个运动员只属于一个组(这里只是假设情况,老外怎么可以这样开玩笑呢?只有当他根据不同应用灵活处理了)。在子表player中有一个外键标志它所属的组,这个外键匹配父表team中的主键。
对于一对多关系,在设计实体Bean的时候,最好考虑一下到底是将关系的双方都实现为实体Bean还是只将父表映射到实体Bean。(在《J2EE核心模式》中有详细论述)下面的例子用实体Bean实现一对多的关系。
将子表实现为辅助类
并不是数据库中所有的表都必须映射到相应的实体Bean。如果一个表并不表示一个商业实体,或者它存储的信息是某个商业实体的部分信息,那么它只需要实现为一个辅助类(这样做的好处是显而易见的,参考《J2EE核心模式》)。例如一个在线商店的应用程序中,用户提交的每一个订单都有多行明细条目。该应用程序中存储订单和订单条目信息的数据表如下图5-2:
图 5-2 一对多关系:订单和订单条目
订单条目不仅从属于订单,而且它的存在也依赖于订单的存在。所以lineitems表应该被表示成一个辅助类而不是一个实体Bean。当然在这里用辅助并不是必需的,但是它可以提高性能,因为一个辅助类比实体Bean使用的资源要少。(《J2EE核心模式》一书关于辅助类有详细描述)。
下面讲的例子的源文件在j2eetutorial/examples/src/ejb/order目录下。在j2eetutorial/examples目录下执行ant order命令可以编译它们。一个样本OrderApp.ear文件存放在目录下。
这个例子用LineItem和OrderBean两个类展示了如何用辅助类实现一对多关系。LineItem类的数据成员对应lineitems表中的列,itemNo表示lienitems表的主键,orderId表示表的外键。LineItem类的代码如下:
public class LineItem implements java.io.Serializable {
String productId;
int quantity;
double unitPrice;
int itemNo;
String orderId;
public LineItem(String productId, int quantity,
double unitPrice, int itemNo, String orderId) {
this.productId = productId;
this.quantity = quantity;
this.unitPrice = unitPrice;
this.itemNo = itemNo;
this.orderId = orderId;
}
public String getProductId() {
return productId;
}
public int getQuantity() {
return quantity;
}
public double getUnitPrice() {
return unitPrice;
}
public int getItemNo() {
return itemNo;
}
public String getOrderId() {
return orderId;
}
}
OrderBean类有一个ArrayList类型的数据成员lineItems,lienItems里的每一个元素都是一个LineItem对象。lineItems变量通过OrderBean的ejbCreate方法传入,该方法先向orders表里插入一行,然后为lineItems里的每一个LineItem对象向lineitems表中插入一行。下面是ejbCreate方法的代码:
public String ejbCreate(String orderId, String customerId,
String status, double totalPrice, ArrayList lineItems)
throws CreateException {
try {
insertOrder(orderId, customerId, status, totalPrice);
for (int i = 0; i < lineItems.size(); i++) {
LineItem item = (LineItem)lineItems.get(i);
insertItem(item);
}
} catch (Exception ex) {
throw new EJBException("ejbCreate: " +
ex.getMessage());
}
this.orderId = orderId;
this.customerId = customerId;
this.status = status;
this.totalPrice = totalPrice;
this.lineItems = lineItems ;
return orderId;
}
OrderClient客户端程序创建一个ArrayList,并用LineItem对象填充它。在调用create方法是将它传给实体Bean:
ArrayList lineItems = new ArrayList();
lineItems.add(new LineItem("p23", 13, 12.00, 1, "123"));
lineItems.add(new LineItem("p67", 47, 89.00, 2, "123"));
lineItems.add(new LineItem("p11", 28, 41.00, 3, "123"));
...
Order duke = home.create("123", "c44", "open",
totalItems(lineItems), lineItems);
OrderBean的其它方法也同时访问两个表。例如ejbRemove方法不仅要删除orders表的一行还要同时删除lineitems表中的所有对应行。EjbLoad和ejbStore方法就更不用说了,它们必须访问两个表以保证所有的数据跟数据库中是同步的。
客户端可以通过调用ejbFindByProductId方法来查找包括特定产品的所有订单。这个方法查询lineitems表中特定productId值确定的所有纪录,并返回一个Order对象的集合。OrderClient客户端程序遍历集合并在屏幕上打印出每个订单的主键值:
Collection c = home.findByProductId("p67");
Iterator i=c.iterator();
while (i.hasNext()) {
Order order = (Order)i.next();
String id = (String)order.getPrimaryKey();
System.out.println(id);
}
运行OrderEJB应用程序
1. 创建用到的表
a) 进入j2eetutorial/examples directory.目录
b) 执行create-order-table命令
2. 部署OrderApp.ear(j2eetutorial/examples/ears)
3. 运行客户端程序
a) 进入j2eetutorial/examples/ears目录
b) 设置APPCPATH环境变量为OrderAppClient.jar所在目录
c) 执行如下命令(有点长,但只是一条命令):
runclient -client OrderApp.ear -name OrderClient -textauth
d) 用户名:guest;密码:guest123。
将子表实现为实体Bean
在一下这些情况下你可以将一个子表实现为实体Bean:
子表存储的信息并不依赖于父表
子表代表的商业实体可以独立于父表存在
其它应用程序可能不访问父表而只访问子表(考虑可重用性)
考虑下面的场景:一个公司里,一个销售代表负责为很多客户服务,而一个客户只跟一个销售代表联系。公司用一个数据库应用程序跟踪销售代表的业绩。数据库需要两张表,父表salsrep和子表cunstomer,salesrep表中的一行对应customer表中的多行。它们的关系如图5-3:
图 5-3 一对多关系:销售代表和客户
实体Bean类SalesRepBean和CunstomerBean实现了sales表和customer表之间的一对多关系。
这个例子的源文件在j2eetutorial/examples/src/ejb/salesrep目录下,要编译它们请在j2eetutorial/examples目录下执行ant salesrep命令。样本文件SalesRepApp.ear存放在j2eetutorial/examples/ears directory目录下。
SalesRepBean类有一个数据成员customerIds,它是一个String对象填充的ArrayList,这些String元素代表跟特定销售代表联系的客户。因为customerIds反映了两个实体之间的关系,所以SalesRepBean必须保证它们是最新的可用数据(如果某个客户和销售代表的关系改变而customerIds并没有变,那么根据它度出来的数据就是错的)。
SalesRepBean类在setEntityContext方法而不是ejbCreate方法中给customerIds一个实例对象,容器只在创建实体Bean实例时调用setEntityContext方法一次以保证只对customerIds实例化一次。因为在实体Bean的生命周期中一个实体Bean实例可以有不同的身份(主键被重新赋值),在ejbCreate方法中实例化customerIds会造成多次不必要的实例化。因此SalesRepBean类的在setEnetityContext方法中实例化customerIds:
public void setEntityContext(EntityContext context) {
this.context = context;
customerIds = new ArrayList();
try {
makeConnection();
Context initial = new InitialContext();
Object objref =
initial.lookup("java:comp/env/ejb/Customer");
customerHome =
(CustomerHome)PortableRemoteObject.narrow(objref,
CustomerHome.class);
} catch (Exception ex) {
throw new EJBException("setEntityContext: " +
ex.getMessage());
}
}
在ejbLoad方法中,会调用私有方法loadCustomerIds来更新customerIds数据成员。有两种实现loadCustomerIds方法的办法:从数据库中读出或者从CustomerEJB实体Bean得到。第一种方法会比第二肿快,但是会使SalesRepBean类的代码依赖于CustomerEJB的底层数据表的实现细节(何种数据库,字段数量等等)。以后可能你想要修改CustomerEJB对应的表(把应用程序移植到另外的J2EE平台服务器上),这时需要修改SalesRepBean类的代码。但是从CustomerEJB实体Bean得到数据就没有这些问题了。这两种方法体现了应用程序中如何在性能和可移植性之间平衡考虑。SalesRepEJB选择了可移植性调用CustomerEJB的findSalesByRep和getPrimaryKey方法来获得cunstomerIds的值(可以用DAO模式来解决这个冲突,见《J2EE核心模式》):
private void loadCustomerIds() {
customerIds.clear();
try {
Collection c = customerHome.findBySalesRep(salesRepId);
Iterator i=c.iterator();
while (i.hasNext()) {
Customer customer = (Customer)i.next();
String id = (String)customer.getPrimaryKey();
customerIds.add(id);
}
} catch (Exception ex) {
throw new EJBException("Exception in loadCustomerIds: " +
ex.getMessage());
}
}
如果客户的销售代表改变了,客户端调用CustomerBean的setSalsRepId方法来更新数据库。SalesRepBean下一个商业方法调用时,loadCustomerIds方法因ejbLoad方法被调用而被调用来更新customerIds数据成员。(为保证ejbLoad方法在每个商业方法前被调用,把所有商业方法的事物属性设置为:Required。)例如:SalesRepClient客户端程序将客户Mary Jackson的销售代表改变的代码如下:
Customer mary = customerHome.findByPrimaryKey("987");
mary.setSalesRepId("543");
543是销售代表Janice Martin的salesRepId值。要列出Janice的所有客户,客户端调用getCunstomerIds方法,然后遍历返回的集合对每一个集合中得到的CustomerEJB实体Bean实例调用findByPrimaryKey方法:
SalesRep janice = salesHome.findByPrimaryKey("543");
ArrayList a = janice.getCustomerIds();
i = a.iterator();
while (i.hasNext()) {
String customerId = (String)i.next();
Customer customer =
customerHome.findByPrimaryKey(customerId);
String name = customer.getName();
System.out.println(customerId + ": " + name);
}
运行SalesRepEJB的例子
1. 创建用到的表
a) 在命令模式下进入j2eetutorial/examples/src directory目录
b) 执行命令:create-salesrep-table
2. 部署SalesRepApp.ear文件(j2eetutorial/examples/ears)
3. 运行客户端
a) 进入j2eetutorial/examples/ears目录
b) 设置环境变量APPCPATH为SalesRepAppClient.jar所在目录
c) 执行命令:
runclient -client SalesRepApp.ear -name SalesRepClient -textauth
d) 在登录提示符下输入用户名:guest。密码:guest123
多对多关系
对于多对多关系,每个实体都对应关系另一方的多个实体。例如学生选课的例子,每门课程都有很多学生来上,而每个学生也不可能只上一门课,在数据库中,这种关系被表示成一个由外键(关系中的两张表的主键)组成的关系引用表。图5-4表enrollment是一个关系引用表。这些表分别被StudentBean、CustomerBean和EnrollerBean类访问。
图 5-4 多队多关系:学生和课程
这个例子的源文件在j2eetutorial/examples/src/ejb/enroller目录下,要编译这些源文件在j2eetutorial/examples目录下执行ant enroller命令。样本文件EnrollerApp.ear方在j2eetutorial/examples/ears目录下。
StudentBean和CourseBean类都有一个以外键对象为元素的ArrayList数据成员。StudentBean类中是:courseIds,记录学生参加的课程。CourseBean类中是studentIds。
StudentBean类的ejbLoad方法调用私有方法loadCourseIds将课程元素加入ArrayList中,后者从EnrollerEJB会话Bean得到课程实体的主键。LoadCourseIds的代码如下:
private void loadCourseIds() {
courseIds.clear();
try {
Enroller enroller = enrollerHome.create();
ArrayList a = enroller.getCourseIds(studentId);
courseIds.addAll(a);
} catch (Exception ex) {
throw new EJBException("Exception in loadCourseIds: " +
ex.getMessage());
}
}
被上面方法调用的EnrollerBean类的getCourseIds方法直接查询数据库:
select courseid from enrollment
where studentid = ?
只有EnrollerBean类访问enrollment表,因此这各类管理着学生-课程之间的关系。当一个学生选择一门课,客户端调用enroll商业方法,在enrollment表中插入一行:
insert into enrollment
values (studentid, courseid)
如果学生退选一门课,则调用unEnroll方法删除一行:
delete from enrollment
where studentid = ? and courseid = ?
如果一个学生离开了学校,则调用deleteStudent方法删除和该学生相关的所有纪录:
delete from enrollment
where student = ?
EnrollerBean类不删除student表中的对应纪录,这个工作由StudentBean类的ejbRemove方法来完成。为了保证这两个表的删除作为一个单一操作完成,它们必须在同一个事务中。关于事务参考14章。
运行EnrollerEJB的例子
1. 创建用到的表
a) 进入j2eetutorial/examples目录
b) 执行命令:ant create-enroller-table
2. 部署EnrollerApp.ear文件(j2eetutorial/examples/ears)
3. 运行客户端:
a) 进入j2eetutorial/examples/ears目录
b) 设置APPCPATH环境变量为EnrollerAppClient.jar文件的路径
c) 执行如下命令
runclient -client EnrollerApp.ear -name EnrollerClient -textauth
d) 在登录时输入用户名:guest。密码:guest123
四.BMP的主键
在部署实体Bean时,你必须在部署描述符指定主键类型。很多时候,主键类型可能是String、Integer或者其他J2SE或J2EE标准库中的类。但是有很多实体Bean的主键是复合字段,你必须自己定义主键类。
主键类
下面的主键类是一个复合组建类,productId和vendorId字段共同标志一个唯一的实体:
public class ItemKey implements java.io.Serializable {
public String productId;
public String vendorId;
public ItemKey() { };
public ItemKey(String productId, String vendorId) {
this.productId = productId;
this.vendorId = vendorId;
}
public String getProductId() {
return productId;
}
public String getVendorId() {
return vendorId;
}
public boolean equals(Object other) {
if (other instanceof ItemKey) {
return (productId.equals(((ItemKey)other).productId)
&& vendorId.equals(((ItemKey)other).vendorId));
}
return false;
}
public int hashCode() {
return productId.concat(vendorId).hashCode();
}
}
对BMP,主键类规则如下:
1. 主键类必须是公有(public)类
2. 所有数据成员都是共有(public)的
3. 有一个共有(public)的缺省构造函数
4. 重载hashCode()和equals(Object other)方法
5. 可序列化
实体Bean类中的主键
在BMP中,ejbCreate方法将传入参数赋值给对应字段并返回主键类
public ItemKey ejbCreate(String productId, String vendorId,
String description) throws CreateException {
if (productId == null || vendorId == null) {
throw new CreateException(
"The productId and vendorId are required.");
}
this.productId = productId;
this.vendorId = vendorId;
this.description = description;
return new ItemKey(productId, vendorId);
}
ejbFindByPrimaryKey核实传入主键值在数据库中是否存在对应纪录:
public ItemKey ejbFindByPrimaryKey(ItemKey primaryKey)
throws FinderException {
try {
if (selectByPrimaryKey(primaryKey))
return primaryKey;
...
}
private boolean selectByPrimaryKey(ItemKey primaryKey)
throws SQLException {
String selectStatement =
"select productid " +
"from item where productid = ? and vendorid = ?";
PreparedStatement prepStmt =
con.prepareStatement(selectStatement);
prepStmt.setString(1, primaryKey.getProductId());
prepStmt.setString(2, primaryKey.getVendorId());
ResultSet rs = prepStmt.executeQuery();
boolean result = rs.next();
prepStmt.close();
return result;
}
获取主键
客户端可以调用EJBObject(远程接口)对象的getPrimaryKey方法得到实体Bean得主键值:
SavingsAccount account;
...
String id = (String)account.getPrimaryKey();
实体Bean调用EntityContext对象的getPrimaryKey方法找回自己的主键值:
EntityContext context;
...
String id = (String) context.getPrimaryKey();
五.异常处理
企业Bean抛出的异常有两类:系统异常和应用程序异常。
系统异常是指应用程序的支撑服务出现异常。这样的异常例子有:不能得到数据库连接,数据库满造成SQL语句插入失败,一个lookup方法找不到需要的对象等等。如果企业Bean遇到一个系统级的问题,它将抛出javax.ejb.EJBException,容器会把该异常包装到一个RemoteException异常中返回给客户端。因为EJBException是RutimeException的子类,所以你不需要将它列在throws子句中。当一个系统异常发生时,EJB容器可能会销毁企业Bean实例,因此系统异常不能被客户端处理,它需要系统管理员的干涉。
应用程序异常表示一个企业Bean的动作违反特定的商业逻辑。应用程序异常又分为两类:自定义异常和系统预定异常。自定义异常是自己编码的表示商业逻辑违例的异常类,例如SavingsAccountEJB例子的debit方法抛出的InsufficentBalanceException异常。同时javax.ejb包还为处理一些通用问题预定义了很多异常。例如ejbCreate方法在输入参输非法的情况下抛出的CreateException异常。容器不对企业Bean抛出的应用程序异常做手脚,客户端可以处理捕获的任何应用程序异常。
如果在事务中出现系统异常,EJB容器将回滚该事务。然而容器不为应用程序异常回滚事务。
表5-2列出了javax.ejb中定义的异常,除了NoSuchEntityException和EJBException是系统异常,其余的都是应用程序异常。
表5-2 异常
方法 抛出异常 异常原因
ejbCreate CreateExcetption 非法参数
EjbFindByPrimaryKey(所有返回单个对象的查找方法) ObjectNotFoundException(FinderException的子类) 被查询的实体在数据库中不存在
ejbRemove RemoveException 从数据库中删除对应实体数据失败
ejbLoad NoSuchEntityException 要查找的记录数据库中没有
ejbStore NoSuchEntityException 要更新的实体数据数据库中没有
所有方法 EJBException 出现系统异常
第6章 CMP的例子
Dale Green 著
Iceshape Zeng 译
CMP实体Bean技术给开发者带来了很多重要的好处。首先,EJB容器处理了所有数据库的访问操作。其次,容器管理了实体Bean之间的关系。因为这些服务,你不需要为数据库访问而编写任何代码,而只用在部署描述符里指定配置。这种方法不仅可以节省开发者的时间,更重要的是它使企业Bean在访问不同的数据库时有更好的可移植性。
本章将重点介绍CMP实现的实体Bean应用程序RosterApp这个例子的代码和部署描述符设置。如果你对本章提到的术语和概念不太熟悉,请参考第4章企业Bean的CMP部分。
本章内容:
RosterApp应用概述
PlayerEJB代码分析
实体Bean类
Local Home接口
Local接口
RosterApp设置说明
RoseterApp应用程序设置
RosterClient客户端设置
RosterJAR设置
TeamJAR设置
RosterApp中的方法调用
创建一个Player实体
将Player加入一个Team实体
删除一个Player
从Team中删除一个Player
查询一个Team中所有的Player
得到Team中所有Player的副本
查询特定位置的Player
查询一个Player参加的Sports
运行RosterApp应用程序
启动用到的程序
部署
运行客户端
用deploytool工具部署CMP实现的实体Bean
指定企业Bean类型
选择持久性字段和抽象模式名
为查找方法和Select方法编写EJB QL查询
生成SQL语句和指定表创建机制
设置数据库的JNDI名,用户名和密码
定义关系
CMP主键
主键类
实体Bean的主键
产生主键值
一 RosterApp应用概述
RosterApp应用程序维护运动社团中运动员分组的花名册。它有五个组成部分,RosterAppClient是通过Remote接口访问会话Bean RosterEJB的J2EE客户端,RosterEJB通过Local接口访问三个实体Bean:PlayerEJB,TeamEJB和LeagueEJB。
这些实体Bean用容器管理的持久性和关系。TeamEJB和PlayerEJB之间是双向的多对多关系。双向关系中每一个Bean都有一个关系字段来确定相关联另一个Bean实例。多对多关系是指:可以参加多个运动项目的运动员(Player)可以加入多个组(team),而每个组又有多个运动员。LeagueEJB和TeamEJB之间是一对多的双向关系:一个社团可以有多个组,而一个组只能属于一个社团。
图6-1描述了该应用程序各组件和它们之间的关系。虚线箭头表示通过调用JNDI lookup方法的访问。
图 6-1 RosterApp应用程序
二 layerEJB代码分析
PlayerEJB表示运动社团中的运动员实体。本例中,它需要以下三各个类:
1. 实体Bean类(PlayerBean)
2. Local Home接口(LocalPlayerHome)
3. Local接口(LocalPlayer)
你可以在本例的源代码文件存放目录j2eetutorial/examples/src/ejb/cmproster中找到以上代码的源文件,要编译这些源文件,在命令方式下进入j2eetutorial/examples目录,执行ant cmproster命令。RosterApp.ear的样本文件存放在j2eetutorial/examples/ears目录下。
实体Bean类
CMP实现的实体Bean必须符合CMP的语法规则。首先Bean类必须定义为公有(public)和抽象(abstract)的。其次要实现一下内容:
1. EntityBean接口
2. 零对或多对ejbCreate和ejbPostCreate方法
3. 持久性字段和关系字段的get和set方法定义为abstract
4. 所有的select方法定义为abstract
5. 商业方法
CMP的实体Bean类不能实现如下方法
1. 查找方法
2. finalize方法
CMP和BMP实现实体Bean的代码比较
因为CMP不需要编写数据库访问的代码,所以CMP实现的实体Bean比BMP实现的实体Bean的代码少得多。例如本章中讨论的PlayerBean.java源文件要比第5章的代码文件SavingsAccountBean.java小得多。下表比较了两种不同类型实体Bean实现代码的不同:
表6-1两种持久性机制的编码比较
不同点 CMP BMP
企业Bean类定义 抽象 非抽象
数据库访问代码 由工具产生 开发者编码
持久性状态 虚拟持久字段表示 代码中的实例变量表示
持久性字段和关系字段的访问方法 必须 不是必须
findByPrimaryKey方法 由容器处理 开发者编码
其他查找方法 容器根据开发者定义的EJB QL查询自动处理 开发者编码
select方法 容器处理 不需要
ejbCreate方法的返回值 应该为null 必须是主键类
注意:对于两种持久性机制,商业方法和Home方法的实现规则都是一样的。参考第5章的商业方法和Home方法两节。
访问(get和set)方法
CMP实现的实体Bean中持久性字段和关系字段都是虚拟的,你不能把它们在Bean类中写成实例变量,而应该在部署描述符里列出它们。要访问这些字段,你需要在实体Bean类中定义抽象的get和set方法。
持久性字段的访问方法
EJB容器根据部署描述符信息自动为持久性字段实现存储到数据库和从数据库读取操作。(具体的操作过程可能是在执行部署的同时,部署工具就根据部署描述符的信息生成了对应的数据库访问代码)本例中PlayerEJB的部署描述符中定义了以下持久性字段:
playerId(primary key)
name
position
salary
PlayerBean类中以上字段的访问方法定义如下:
public abstract String getPlayerId();
public abstract void setPlayerId(String id);
public abstract String getName();
public abstract void setName(String name);
public abstract String getPosition();
public abstract void setPosition(String position);
public abstract double getSalary();
public abstract void setSalary(double salary);
访问方法名以get或者set开头,后跟头字母大写的对应持久性字段名或者关系字段名。例如字段salary的访问方法命名为:getSalary和setSalary。这种命名约定和JavaBean组件的相同。
关系字段的访问方法
在RosterApp应用程序中,因为一个运动员可以属于多个组,一个PlayerEJB实例可以关联多个TeamEJB实例。为了说明这个关系,部署描述符中定义了一个名叫teams的关系字段。在PlayerBean类中,teams的访问方法定义如下:
public abstract Collection getTeams();
public abstract void setTeams(Collection teams);
select方法
select方法和查找方法有很多相同的地方:
select方法可以返回Local或者Remote接口或者它们之一的集合
select方法访问数据库
部署描述符为每个select方法指定EJB QL查询
实体Bean类并不实现select方法(只是声明或者说定义)
当然,select方法和查找方法还有一些显著的区别:
1. 一个查找方法可以返回相关联实体Bean的一个持久性字段或者字段的集合。而查找方法值可以返回所属实体Bean的Local或者Remote接口或者它们之一的集合。
2. 因为select方法在Local或者Remote接口中没有被定义,所以它们不能被客户端访问,而只能被所属实体Bean中实现的方法调用(用点像类的私有方法)。select方法通常被商业方法调用。
3. select方法在实体Bean类里定义。BMP的查找方法也在实体Bean类中定义,但是CMP的查找方法不在实体Bean类里定义。CMP中的查找方法在Home或者Local Home接口中定义,在部署描述符中定义对应的EJB QL查询。
PlayerBean类定义了下面这些select方法:
public abstract Collection ejbSelectLeagues(LocalPlayer player)
throws FinderException;
public abstract Collection ejbSelectSports(LocalPlayer player)
throws FinderException;
select的方法签名规则:
1. 方法名要以ejbSelect开头
2. 访问修饰符必须是public
3. 方法必须声明为抽象的
4. throws子句必须包括javax.ejb.FinderException
商业方法
因为客户端不能调用select方法,所以PlayerBean类将它们封装(wraps)在商业方法getLeagues和getSports中:
public Collection getLeagues() throws FinderException {
LocalPlayer player =
(team.LocalPlayer)context.getEJBLocalObject();
return ejbSelectLeagues(player);
}
public Collection getSports() throws FinderException {
LocalPlayer player =
(team.LocalPlayer)context.getEJBLocalObject();
return ejbSelectSports(player);
}
实体Bean方法
因为处理CMP实体Bean的持久性,PlayerBean的生命周期方法都接近于空方法了。ejbCreate方法将参数赋值给持久性字段以初始化实体Bean实例。ejbCreate方法调用结束后,容器向数据库对应表中插入一行。ejbCreate方法实现:
public String ejbCreate (String id, String name,
String position, double salary) throws CreateException {
setPlayerId(id);
setName(name);
setPosition(position);
setSalary(salary);
return null;
}
ejbPostCreate方法必须和对应的ejbCreate方法有相同的参数签名和返回值。如果你想在初始化Bean实例时设置关系字段值,可以实现ejbPostMethod方法,而不可以在ejbCreate方法中设置关系字段值。
除了一个调试语句,PlayerBean的ejbRemove方法就是一个空方法了。容器在删除数据库表中对应行之前调用ejbRemove方法。
容器自动同步实体Bean状态和数据库数据。容器从数据库中读取实体状态后调用ejbLoad方法,同样的方式,在将实体状态存入数据库前调用ejbStore方法。(这句话有些费解,因为在BMP中数据库的同步是在ejbLoad和ejbStore这两个方法里实现的,难道部署工具或者容器为CMP生成的实现子类不使用这两个方法来实现数据库同步的?)
Local Home接口
Local Home接口定义本地客户端调用的create方法、查找方法和Home方法。
create方法的语法规则如下:
1. 方法名以create开头
2. 和对应的实体Bean类里定义的ejbCreate方法有相同的参数签名
3. 返回实体Bean的Local接口
4. throws子句包括对应ejbCreate方法throws子句中出现的所有异常加上javax.ejb.CreateException异常
查找方法的规则:
1. 方法名以find开头
2. 返回实体Bean的Local接口或它的集合
3. throws子句包括javax.ejb.FinderException异常
4. 必须定义findByPrimaryKey方法
下面是LocalPlayerHome的部分代码:
package team;
import java.util.*;
import javax.ejb.*;
public interface LocalPlayerHome extends EJBLocalHome {
public LocalPlayer create (String id, String name,
String position, double salary)
throws CreateException;
public LocalPlayer findByPrimaryKey (String id)
throws FinderException;
public Collection findByPosition(String position)
throws FinderException;
...
public Collection findByLeague(LocalLeague league)
throws FinderException;
...
}
Local接口
该接口定义了本地客户端调用的商业方法和字段访问方法。PlayerBean类实现了两个商业方法getLeagues和getSports,同时还为持久性字段和关系字段定义了很多get和set访问方法。但是客户端不能调用set方法,因为LocalPlayer接口中没有定义这些set方法。但是get方法客户端可以访问。下面是LocalPlayer的实现代码:
package team;
import java.util.*;
import javax.ejb.*;
public interface LocalPlayer extends EJBLocalObject {
public String getPlayerId();
public String getName();
public String getPosition();
public double getSalary();
public Collection getTeams();
public Collection getLeagues() throws FinderException;
public Collection getSports() throws FinderException;
}
三.RosterApp配置说明
本节将引导你为CMP实现的实体Bean配置部署描述符。在这个过程中,还将讨论deplouytool工具中出现的重要的选项页和对话框。
请先运行deploytool并打开j2eetutorial/examples/ears目录下的RosterApp.ear文件。
RosterApp应用程序配置
在属性视图中选中RosterApp节点以查看应用程序的部署信息。
General页(RosterApp)
Contents域显示了RosterApp.ear中包含的文件,包括两个EJB JAR文件(team-ejb.jar和roster-ejb.jar)和J2EE应用程序客户端JAR文件(roster-ac.jar)。如图6-2:
图 6-2 RosterApp 的General 页
JNDI Names页(RosterApp)
Application表列出了RosterApp应用程序中的企业Bean的JNDI名。
References表有两个条目,EJB Ref条目为RosterClient客户端映射RosterEJB会话Bean的引用名(ejb/SimpleRoster)。Resource条目指定TeamJAR模块中的实体Bean访问的数据库的JNDI名。
RosterClient客户端配置
展开RosterApp节点,选中RosterClient节点,将显示客户端配置信息。
JAR File页(RosterClient)
Contents域显示了roster-ac.jar包含的文件:两个XML文件(部署描述符文件)和一个类文件(RosterClient.class)。
EJB Refs页(RosterCliet)
RosterClient客户端访问一个企业Bean:RosterEJB会话Bean。因为是远程访问,Interfaces列选择Remote,Local/Remote列填写会话Bean的Remote接口(roster.Roster)。
RosterJAR的设置
在树视图中选中RosterJAR节点。该JAR文件包含RosterEJB会话Bean。
General页(RosterJAR)
Contents域列出了三个包:roster包包含RosterEJB需要的类文件--会话Bean类,Remote接口和Home接口;team包包含RosterEJB要访问的实体Bean的Local接口;util包里是应用程序用到的一些实用类(utility classes)。
RosterEJB
展开RosterJAR节点点选RosterEJB节点。
General页(RosterEJB)
本例中General选项页显示RosterEJB是一个远程访问的有状态会话Bean。因为它不支持本地访问,所以Local Interface域为空。
EJB Refs页(RosterEJB)
RosterEJB会话Bean访问三个实体Bean:PlayerEJB、TeamEJB和LeagueEJB。因为这些访问都是本地访问,这些引用条目中的Interfaces列中都定为Local,Home Interface列显示的是这些实体Bean的Local Home接口。Local/Remote Interfaces列显示的是这些实体Bean的Local接口。
选中表中的一行,可以查看运行时部署信息。例如当你选择Coded Name列为ejb/SimpleLeague的一行时,LeagueEJB就显示在Enterprise Bean Name域里。Enterpri Bean Name域需要设置成被引用的企业Bean的名字(在左边树视图中显示的名字)。
TeamJAR配置
点选树视图中的TeamJAR节点。该JAR文件包含三个相互关联的实体Bean:LeagueEJB、TeamEJB和PlayerEJB。
General页(TeamJAR)
Contents域显示了该JAR文件中的两个包:team和util。team包包含三个实体Bean的类文件、Local接口文件和Local Home接口文件。Util包就不再介绍了。
Relationships页(TeamJAR)
如图6-3,CMP实体Bean之间的关系在这个选项页中定义。
图 6-3 TeamJAR 的Relationships页
Container Managed Relationships表中定义了两个关系:TeamEJB-PlayerEJB和LeagueEJB-TeamEJB。在TeamEJB-PlayerEJB关系中,TeamEJB被当作EJB A而PlayerEJB被当作EJB B(当然你也可以交换它们的位置,这并不影响关系本身)。
关系编辑对话框(TeamJAR)
在上图所示的Relationship页中选择表中的一行点击Edit按钮,就会弹出关系编辑对话框。(当然Add按钮也可以,只不过不需要选择已有关系,因为……)。如图6-4是TeamEJB-PlayerEJB关系的编辑对话框,下面详细介绍它们的关系。
TeamEJB-PlayerEJB关系
Multiplicity组合框提供四种可选的关系类型(因为这里是单向从A到B,所以一对多和多对一是两种类型,其实可以在下面的EJB组合框中调换关系双方的位置而让它们合成一种)。TeamEJB-PlayerEJB是多对多关系(many to many(*:*)选项)。
Enterprise Bean A框里描述了TeamEJB在关系中的信息。Field Referencing Bean B组合框里选择了TeamEJB中的关系字段(players),这个字段对应TeamBean.java文件中定义的以下两个访问方法:
public abstract Collection getPlayers();
public abstract void setPlayers(Collection players);
图 6-4 TeamJAR 的关系编辑对话框
FieldType组合框选择的是java.util.Collection以匹配为访问方法中player字段的类型。因为在对TeamEJB在关系中PlayerEJB是"多"的一方(在多对多关系中双方都是"多"),所以player字段的类型是多值对象(集合)。
TeamEJB-PlayerEJB的关系是双向的--关系的双方都有一个关系字段标志关联的实体Bean的实例。如果关系不是双向的,没有关系字段的一方在Field Referenceing组合框中选择
LeagueEJB-TeamEJB关系
在关系编辑对话框里,LeagueEJB-TeamEJB关系的Multiplicity组合框选择的是One to Many(因为一个社团可以有多个组,而且这里选定后,Enterprise Bean A就只能是LeagueEJB了)。
LeagueEJB中用关系字段teams来表示该社团里所有的组。因为TeamEJB是关系中"多"的一方,所以teams字段是一个集合。而LeagueEJB是"一"的一方,所以TeamEJB中league是单个的LocalLeaguer类型对象。TeamBean.java通过如下访问方法定义关系字段league:
public abstract LocalLeague getLeague();
public abstract void setLeague(LocalLeague players);
在LeagueEJB-TeamEJB关系中定义了关联删除,TeamEJB中Delete When A Is Deleted符选框被选中。这样当一个LeagueEJB实例被删除时,与它相关联的TeamEJB实例也将自动地被删除,这就是关联删除。LeagueEJB中相同的符选框并没有选中,不能删除一个组就删除整个社团,因为还有其他组在社团中。一般在关系中"多"的一方才会被关联删除,另一方不会被自动删除。
PlayerEJB
在树视图中选择PlayerEJB节点(TeamJAR的字节点之一)。
General页(PlayerEJB)
这一页显示企业Bean类和EJB接口。因为PlayerEJB是容器管理关系的靶子,所以它有本地接口(Local和Local Home接口),同时它不需要允许远程访问,所以没有远程接口(Remote和Home接口)。
Entity页(PlayerEJB)
如图6-5,在这一页上面的单选按钮组定义企业实体Bean的持久性类型。为PlayerEJB选定的是container-managed persistence(2.0),就是CMP2.0。因为CMP1.0不提供容器管理的关系服务,所以不推荐使用。(这些版本号是EJB规范的版本号,而不是J2EE SDK的版本号)
Fields To Be Persisted列表框列出了PlayerBean.java中定义了访问方法的所有持久性字段和关系字段。持久性字段前面的选择框必须被选中,而关系字段不能不选中。就是说关系字段(外键)不在本实体Bean中被持久化(这里或许是因为数据库表本身是用参照约束实现的外键关联,所以关系字段不需要在这里持久化,如果数据表关系是用应用程序编码检查来实现,那么这里就应该让关系字段也被持久化,但是这时这个字段还是不是关系字段呢?)。PlayerEJB实体Bean的关系字段只有一个:teams。
abstract schema name是Player,这个名字代表PlayerEJB实体Bean的所有持久性字段和关系字段(抽象模式名标志一个定义持久性字段和关系字段的抽象模式)。这个抽象模式名将在为PlayerEJB定义的EJB QL 查询中被引用。EJB QL将在第8章论述。
图 6-5 PlayerEJB的Entity页
Finder/Select Methods对话框(PlayerEJB)
在Entity页点击Finder/Select Methods按钮会打开Finder/Select Methods对话框,如图6-6。你可以在这个对话框里查看和编辑为CMP实体Bean的查找和Select方法写的EJB QL查询。
图 6-6 Finder/Select Methods 对话框
Entity Deployment Settings对话框(PlayerEJB)
在Entity页点击Deployment Settings按钮打开该对话框,该对话框用来设置CMP实体Bean的运行时配置信息。这些信息是J2EE SDK特有的,其他的J2EE平台可能有些不同。在J2EE SDK中,实体Bean的持久性字段存储在关系数据库中,在Database Table框中你可以确定是否让服务器自动创建和删除数据库中的表。如果你想把数据保存在数据库中,就不选中Delete table on undeploy复选框,否则当你从服务器卸载实体Bean时,表将被删除。
J2EE服务器通过SQL语句访问数据库,用CMP实现实体Bean时你不需要自己编写这些SQL语句,点击General Default SQL按钮deploytool工具会自动生成这些语句。然后在Method表格里选中任意一个方法,会在右边的SQL Query域里看到为这个方法生成的SQL语句,你可以在这里修改这些生成的语句。
对查找和Select方法,对应的EJB QL查询也会显示在下面的EJB QL Query域里。在你点击General Default SQL按钮时,deploytool把EJB QL查询转换成SQL语句,如果你修改了EJB QL查询,你应该让deploytool重新生成SQL语句。
点击Container Methods单选按钮,可以看到为容器管理方法生成的SQL语句。如createTable方法的创建表SQL语句,当然还有删除表的SQL语句。
当容器创建一个PlayerEJB实例时,它生成一条插入数据(INSERT)的SQL语句,要查看这条语句,在Method表各种选择createRow方法,这条语句的VALUES子句中的变量是Home接口中create方法的对应参数。
Database Deployment Settings对话框(PlayerEJB)
在Entity Deployment Settings对话框中点击Database Settings按钮打开该对话框。在这里你可以为数据库指定JNDI名,这是非常重要的,因为没有JNDI名你将无法访问数据库。本例中的数据库JNDI名为jdbc/Cloudscape,用户名和密码为空。
四 RosterApp中的方法调用
为了说明组件之间如何相互协作,本节描述实现特定功能时的方法调用顺序。这些组件的源文件在j2eetutorial/examples/src/ejb/cmproster目录下。
"新建"一个运动员
1.RosterClient客户端程序
RosterClient客户端程序调用RosterEJB会话Bean的createPlayer商业方法。在下面的代码中myRoster对象是Roster类型(RosterEJB的远程接口),参数是PlayerDetails对象,该对象封装了一个运动员的所有信息。(PlayerDetails是一个值对象,用来在远程调用间传递实体信息,关于值对象模式的更多信息,参考《J2EE核心模式》一书)。
myRoster.createPlayer(new PlayerDetails("P1", "Phil Jones", "goalkeeper", 100.00));
2.RosetEJB
会话Bean的createPlayer方法创建一个PlayerEJB实体Bean实例。因为PlayerEJB只有本地接口,所以create方法在Local Home接口LocalPlayerHome中定义。下面的代码中playerHome是LocalPlayerHome类型的对象:
public void createPlayer(PlayerDetails details) {
try {
LocalPlayer player = playerHome.create(details.getId(),
details.getName(), details.getPosition(),
details.getSalary());
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
3.PlayerEJB
ejbCreate方法用set访问方法将参数值赋给持久性字段,该方法调用后容器生成一个INSERT SQL语句将持久性字段写入数据库:
public String ejbCreate (String id, String name,
String position, double salary) throws CreateException {
setPlayerId(id);
setName(name);
setPosition(position);
setSalary(salary);
return null;
}
将运动员加入组
1.RosterClient客户端
客户端调用RosterEJB的addPlayer方法(参数P1和T1分别代表Player和TeamEJB实例的主键):
myRoster.addPlayer("P1", "T1");
2.RosterEJB
addPlayer方法分两个步骤完成工作:首先它调用findByPrimaryKey查找PlayerEJB和TeamEJB实例;然后直接调用TeamEJB的addPlayer方法。代码如下:
public void addPlayer(String playerId, String teamId) {
try {
LocalTeam team = teamHome.findByPrimaryKey(teamId);
LocalPlayer player =
playerHome.findByPrimaryKey(playerId);
team.addPlayer(player);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
3.TeamEJB
TeamEJB有一个关系字段players,它是属于这个队伍的所有运动员的集合(Collection),它的访问方法如下:
public abstract Collection getPlayers();
public abstract void setPlayers(Collection players);
addPlayer方法县调用getPlayers方法得到关联的LocalPlayer对象的集合,然后调用集合的add方法新加入一个运动员:
public void addPlayer(LocalPlayer player) {
try {
Collection players = getPlayers();
players.add(player);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
"删除"一个运动员
1.RosterClient
删除运动员P4,客户端调用RosterEJH的removePlayer方法:
myRoster.removePlayer("P4");
2.RosterEJB
removePlayer方法调用findByPrimaryKey方法定位倒要删除的PlayerEJB实例然后调用实例的remove方法。这个调用会通知容器在数据库中删除对应的纪录,容器还同时删除相关联的TeamEJB的player关系字段中该实例的引用以更新TeamEJB-PlayerEJB之间的关系。下面是RosterEJB的removePlayer方法:
public void removePlayer(String playerId) {
try {
LocalPlayer player =
playerHome.findByPrimaryKey(playerId);
player.remove();
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
从组中开除运动员
1.RosterClient
将运动员P2从组T1中删除的客户端调用:
myRoster.dropPlayer("P2", "T1");
2.RosterEJB
dropPlayer方法的调用和addPlayer很像,她先找到PlayerEJB和TeamEJB的实例,然后调用TeamEJB的dropPlayer方法:
public void dropPlayer(String playerId, String teamId) {
try {
LocalPlayer player =
playerHome.findByPrimaryKey(playerId);
LocalTeam team = teamHome.findByPrimaryKey(teamId);
team.dropPlayer(player);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
3.TeamEJB
dropPlayer方法会更新TeamEJB-PlayerEJB关系。首先它得到关系字段players 对应的LocalPlayer对象集合,然后调用集合的remove方法删除目标运动员。代码如下:
public void dropPlayer(LocalPlayer player) {
try {
Collection players = getPlayers();
players.remove(player);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
获得一个组里的所有运动员
1.RosterClient
客户段调用RosterEJB的getPlayersOfTeam方法来获得某个组的成员,该方法返回包含PlayerDetails对象的ArrayList。PlayerDetails是PlayerEJB的值对象(《J2EE核心模式》),包含的数据成员playerId、name、salary都是PlayerEJB的持久性字段。客户端调用代码如下:
playerList = myRoster.getPlayersOfTeam("T2");
2.RosterEJB
RosterEJB的getPlayersOfTeam方法先调用TeamEJB的findByPrimaryKey方法找到对应的实例,然后调用TeamEJB的getPlayers方法,最后调用copyPlayerToDetails方法将每一个运动员的数据拷贝到PlayDetails中组成返回集合。代码如下:
public ArrayList getPlayersOfTeam(String teamId) {
Collection players = null;
try {
LocalTeam team = teamHome.findByPrimaryKey(teamId);
players = team.getPlayers();
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
return copyPlayersToDetails(players);
}
copyPlayerToDetails方法的实现如下:
private ArrayList copyPlayersToDetails(Collection players) {
ArrayList detailsList = new ArrayList();
Iterator i = players.iterator();
while (i.hasNext()) {
LocalPlayer player = (LocalPlayer) i.next();
PlayerDetails details =
new PlayerDetails(player.getPlayerId(),
player.getName(), player.getPosition(),
player.getSalary());
detailsList.add(details);
}
return detailsList;
}
3.TeamEJB
TeamEJB的getPlayers方法是关系字段players的访问方法:
public abstract Collection getPlayers();
对本地客户可用,因为它是在Local接口中被定义:
public Collection getPlayers();
该方法向本地客户返回关系字段的引用,如果客户接着修改了得到的结果,实体Bean中的关系字段也跟着被修改(因为是引用,所以操作的是同一个对象)。例如本地客户可以用如下方法开除一个运动员:
LocalTeam team = teamHome.findByPrimaryKey(teamId);
Collection players = team.getPlayers();
players.remove(player);
下面讲到的方法描述如何避免这种危险。
获取组成员的副本
这一小节讨论下面两项技术:
过滤返回给远程客户端的信息
防止本地客户直接修改关系字段
1.RosterClient
如果你想在返回给客户端的结果中过滤掉运动员的薪水信息,你应该调用RosterEJB的getPlayersOfTeamCopy方法,该方法跟getPlayersOfTeam的唯一区别就是它把每个运动员的薪水都设置为0。客户端调用代码:
playerList = myRoster.getPlayersOfTeamCopy("T5");
2.RosterEJB
getPlayersOfTeamCopy方法并不像getPlayersOfTeam方法一样调用getPlayers访问方法,它调用LocalTeam接口中定义的getCopyOfPlayers商业方法。从getPlayersOfTeamCopy方法得到的返回值不能修改TeamEJB的关系字段players。代码如下:
public ArrayList getPlayersOfTeamCopy(String teamId) {
ArrayList playersList = null;
try {
LocalTeam team = teamHome.findByPrimaryKey(teamId);
playersList = team.getCopyOfPlayers();
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
return playersList;
}
3.TeamEJB
TeamEJB的getCopyOfPlayers方法返回包含PlayeDetails对象的ArrayList。为了创建这个ArrayList,它必须遍历关系字段players集合并将每一个元素的信息拷贝到一个PlayerDetails对象中,因为要过滤薪水字段,所以只拷贝了salary除外的字段,而salary被直接置为0。当客户端调用getPlayerOfTeamCopy方法时,隐藏了运动员的薪水信息。代码如下:
public ArrayList getCopyOfPlayers() {
ArrayList playerList = new ArrayList();
Collection players = getPlayers();
Iterator i = players.iterator();
while (i.hasNext()) {
LocalPlayer player = (LocalPlayer) i.next();
PlayerDetails details =
new PlayerDetails(player.getPlayerId(),
player.getName(), player.getPosition(), 0.00);
playerList.add(details);
}
return playerList;
}
根据位置查询运动员
1.RosterClient
客户端调用RosterEJB的getPlayersByPosition方法:
playerList = myRoster.getPlayersByPosition("defender");
2.RosterEJB
RosterEJB的getPlayersByPosition方法调用PlayerEJB的findByPosition方法得到特定位置得运动员集合:
public ArrayList getPlayersByPosition(String position) {
Collection players = null;
try {
players = playerHome.findByPosition(position);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
return copyPlayersToDetails(players);
}
3.PlayerEJB
LocalPlayerHome接口定义了findByPosition方法:
public Collection findByPosition(String position)
throws FinderException;
因为PlayerEJB是CMP实现的实体Bean,所以实体Bean类PlayerBean并不实现查找方法,而是在部署描述符中为每一个查找方法指定EJB QL查询。下面是findByPosition方法的EJB QL查询:
SELECT DISTINCT OBJECT(p) FROM Player p
WHERE p.position = ?1
Deploytool工具会将上面的EJB QL查询转换成对应的SELECT语句。在容器调用findByPositiong方法的时候,SELECT语句也将被执行。
查询运动员的运动项目
1.RosterClient
客户段调用RosterEJB的getSportsOfPlayer方法:
sportList = myRoster.getSportsOfPlayer("P28");
2.RosterEJB
RosterEJB的getSportsOfPlayer方法返回一个运动员可以参与的运动项目的String类型的ArrayList,它直接调用PlayerEJB的getSports方法得到这个ArrayList。代码如下:
public ArrayList getSportsOfPlayer(String playerId) {
ArrayList sportsList = new ArrayList();
Collection sports = null;
try {
LocalPlayer player =
playerHome.findByPrimaryKey(playerId);
sports = player.getSports();
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
Iterator i = sports.iterator();
while (i.hasNext()) {
String sport = (String) i.next();
sportsList.add(sport);
}
return sportsList;
}
3.PlayerEJB
PlayerEJB的getSports方法只是简单的调用ejbSelectSports方法。因为ejbSelectSports方法的参数是LocalPlayer类型,所以要传递一个实体Bean实例的引用给方法。代码如下:
public Collection getSports() throws FinderException {
LocalPlayer player =
(team.LocalPlayer)context.getEJBLocalObject();
return ejbSelectSports(player);
}
ejbSelectSports方法的代码:
public abstract Collection ejbSelectSports(LocalPlayer player)
throws FinderException;
ejbSelectSports的EJB QL查询语句:
SELECT DISTINCT t.league.sport
FROM Player p, IN (p.teams) AS t
WHERE p = ?1
在部署PlayerEJB前,你要用deploytool来产生对应的SELECT语句。当容器调用ejbSelectSports方法时,也同时执行SELECT语句。
五 运行RosterApp应用程序
启动用到的软件
1. 在命令模式下执行cloudscape -start命令启动Cloudscape数据库服务器。
2. 执行j2ee -verbose命令启动J2EE服务器。
3. 执行deploytool命令运行deploytool部署工具。
部署该应用程序
1. 在deploytool中打开RosterApp.ear(j2eetutorial/examples/ears directory)文件
2. 部署
a) 选定树视图中的RosterApp节点
b) 执行Tools/Deploy菜单命令
c) 在Introduction对话框中,选中Return Client JAR复选框
d) 在Client JAR File域中输入文件名(或者用Browse按钮设置):j2eetutorial/examples/ears/RosterAppClient.jar
e) 一路Next直到Finish
运行客户端
1. 在命令模式下进入j2eetutorial/examples/ears目录
2. 设置环境变量APPCPATH为RosterAppClient.jar所在目录
3. 执行如下命令:
runclient -client RosterApp.ear -name RosterClient -textauth
4. 用户名:guest。密码:guest123。
六 用deploytool工具部署CMP实现的实体Bean
在第2章讲述了打包和部署企业Bean的基本步骤,这一节将介绍deploytool部署CMP实现的实体Bean时的不同之处,图例参考RosterApp配置说明一节。
指定企业Bean类型
在新建企业Bean向导(New/Enterprise Bean菜单)中指定企业Bean类型和持久性管理机制。
1.在EJB JAR对话框中点击Edit按钮打开Edite Contents对话框,添加实体Bean和关联的实体Bean需要的类
2.在General对话框中,选择Bean类型为:Entity
3.在同一个对话框中指定Bean类和用到的接口类(远程或者本地或者两者都有)
4.在Entity Settings对话框中选择持久性机制Container managed persistence(2.0)
选择持久性字段和抽象模式名
可以在上面提到的Entity Settings对话框中设置。这里我们在Entity选项页中设置(在树视图中选中上面新建的实体Bean节点)。见图6-5
1. 在Fields To Be Persisted类表中,选中需要存储到数据库中的字段。这些字段名是根据命名规范从定义的访问方法中读出的。
2. 指定主键类合主键字段,主键字段唯一标志一个实体Bean实例
3. 在Abstract Schema Name域中输入一个抽象模式名,该名字在EJB QL查询中被引用
为查找方法和Select方法定义EJB QL查询
在Finder/Select Mothods对话框中定义EJB QL查询,见图6-6。
1. 在Entity页中点击Finder/Select Methods按钮
2. 在Method表各种选择要定义EJB QL查询的方法,在EJB-QL域中输入语句
产生SQL、指定表创建机制(Deploy Settings对话框),指定数据库JNDI名、访问用户名和密码(Database Settings对话框),定义关系(EJB JAR节点的Relationship页,图6-3,Edit Relationships对话框,图6-4)都请参考RosterApp配置说明一节的对应内容。
七 CMP的主键
主键类并不一定是J2SE或者J2EE的标准类库中的类(特别是你的主键是组合字段的时候),这是你就要自己新建主键类并把它和实体Bean打包在一起。
主键类
下面的例子中,PurchaseOrderKey类为PurchaseOrderEJB实现一个组合主键,该主键由有两个数据成员productModel和vendorId对应实体Bean的两个持久性字段:
public class PurchaseOrderKey implements java.io.Serializable {
public String productModel;
public String vendorId;
public PurchaseOrderKey() { };
public String getProductModel() {
return productModel;
}
public String getVendorId() {
return vendorId;
}
public boolean equals(Object other) {
if (other instanceof PurchaseOrderKey) {
return (productModel.equals(
((PurchaseOrderKey)other).productModel) &&
vendorId.equals(
((PurchaseOrderKey)other).vendorId));
}
return false;
}
public int hashCode() {
return productModel.concat(vendorId).hashCode();
}
}
对于CMP实体Bean的主键类有以下要求:
该类必须是public公有类
数据成员是实体Bean的持久性字段的子集
有一个public的缺省构造函数(没有实现任何构造函数或者自己实现一个无参构造函数和其他构造函数)
重载hashCode和equals(Object other)方法(继承自Object类)
可序列化(实现Serializble接口)
实体Bean类中的主键
在PurchaseBean类中,主键类对应字段(vendorId和productModel)的访问方法定义如下:
public abstract String getVendorId();
public abstract void setVendorId(String id);
public abstract String getProductModel();
public abstract void setProductModel(String name);
下面是PurchaseOrderBean类ejbCreate方法的代码,它的返回类型是主键类但是返回语句缺返回null。虽然不是必需的,null是CMP机制推荐的返回值。这样可以节省资源,因为实体Bean并不需要产生主键类的实例并返回。(This approach saves overhead because the bean does not have to instantiate the primary key class for the return value.这句话有些疑义,它的意思应该是说CMP机制会自动处理这些动作,所以我们不必自己多事浪费时间)。
public PurchaseOrderKey ejbCreate (String vendorId,
String productModel, String productName)
throws CreateException {
setVendorId(vendorId);
setProductModel(productModel);
setProductName(productName);
return null;
}
产生主键值
一些实体Bean的主键值对于商业实体有特殊意义。例如:一个表示向呼叫中心打入的电话呼叫的实体Bean,主键应该包括呼叫被接收时的时间戳。也有很多实体Bean的主键值是任意的,只要它们是唯一的。对CMP实现的实体Bean,容器可以自动为实体Bean生成主键值,不过它对实体Bean有一些额外的要求:
在部署描述符中,定义主键类被为java.lang.Object,不指定主键字段
在Home接口中,findByPrimaryKey方法的参数是java.lang.Object
实体Bean类中,ejbCreate的返回值类型是java.lang.Object
这些实体Bean的主键值保存在只有容器可以访问的内部字段中,你不能让它和持久性字段或者任何其他数据成员产生联系,然而你还是可以通过调用getPrimaryKey方法得到主键值,然后你可以调用findByPrimaryKey来定位实体Bean实例了。
第7章 一个消息驱动Bean的例子
Dale Green,Kim Haase著
Iceshape Zeng译
因为消息驱动Bean建立在Java消息服务(Java Message Service,JMS)技术的基础上,所以在学习本章之前,你应该已经熟悉了如消息和消息服务等的JMS概念。你可以通过Java Message Tutorial来学习JMS,在以下网址可以找到这本书:
http://java.sun.com/products/jms/tutorial/index.html
本章讨论一个消息驱动Bean的例子,如果你没有读过第3章的什么是消息驱动Bean一节,那么再回头看一下这些基础概念。
本章内容:
例子应用程序介绍
J2EE应用程序客户端
消息驱动Bean类
onMessage方法
ejbCreate和ejbRemove方法
运行该例子
启动J2EE服务器
创建消息队列
部署该应用程序
运行客户端
用deploytool部署消息驱动Bean
指定Bean类型和事务处理机制
配置消息驱动Bean的特有属性
用deploytool配置JMS客户端
配置资源引用
配置资源环境引用
设置JNDI名
一.例子应用程序介绍
这各应用程序有两个组成部分(消息驱动Bean没有本地和远程接口):
SimpleMessageClient:向消息队列发送消息的J2EE应用程序客户端
SimpleMessageEJB:异步接收并处理消息队列中消息的消息驱动Bean
图7-1显示了这各应用程序的结构。客户端发送消息到消息队列,该消息队列是用j2eeadmin命令创建的。JMS服务提供者(这里是J2EE服务器)将消息传送给消息驱动Bean实例处理。
图 7-1 SimpleMessageApp结构
这个程序的源文件放在j2eetutorial/examples/src/ejb/simplemessage目录下,要编译这些源文件在j2eetutorial/examples目录下执行ant simplemessage命令。SimpleMessageApp样本文件放在j2eetutorial/examples/ears目录下。
二.J2EE应用程序客户端
SimpleMessageClient客户端程序发送消息到SimpleMessageBean监听的消息队列。首先它找到连接工厂和消息队列:
queueConnectionFactory = (QueueConnectionFactory)
jndiContext.lookup
("java:comp/env/jms/MyQueueConnectionFactory");
queue = (Queue)
jndiContext.lookup("java:comp/env/jms/QueueName");
然后创建到消息队列的连接、消息会话和消息发送器:
queueConnection =
queueConnectionFactory.createQueueConnection();
queueSession =
queueConnection.createQueueSession(false,
Session.AUTO_ACKNOWLEDGE);
queueSender = queueSession.createSender(queue);
最后发送几条消息到消息队列:
message = queueSession.createTextMessage();
for (int i = 0; i < NUM_MSGS; i++) {
message.setText("This is message " + (i + 1));
System.out.println("Sending message: " +
message.getText());
queueSender.send(message);
}
三.消息驱动Bean类
参照SimpleMessageEJB类我们先看一下消息驱动Bean类的要求:
实现MessageDrivenBean和MessageListener接口
定义为公有(public)类
不能定义成abstract和final类
实现onMessage方法
实现ejbCreate和ejbRemove方法
必须有一个无参构造函数
不能定义finalize方法
和会话Bean和实体Bean不同,消息驱动Bean没有本地接口和远程接口。客户端不必查找消息驱动Bean的实例,并在这些接口上调用方法。虽然消息驱动Bean也没有商业方法,但是它可以有辅助类并在onMessage方法例调用这些辅助类的方法。
OnMessage方法
当消息队列收到一条消息,EJB容器调用消息驱动Bean的onMessage方法。在SimpleMessageBean类中,onMessage方法将接收到的消息恢复造型为TextMessage消息并显示消息内容:
public void onMessage(Message inMessage) {
TextMessage msg = null;
try {
if (inMessage instanceof TextMessage) {
msg = (TextMessage) inMessage;
System.out.println
("MESSAGE BEAN: Message received: "
+ msg.getText());
} else {
System.out.println
("Message of wrong type: "
+ inMessage.getClass().getName());
}
} catch (JMSException e) {
e.printStackTrace();
mdc.setRollbackOnly();
} catch (Throwable te) {
te.printStackTrace();
}
}
ejbCreate和ejbRemove方法
这两个方法签名的规则:
必须是公有(public)方法
返回类型是void
不能是abstract和final方法
不能有throws子句
没有参数
本例SimpleMessageBean类中ejbCreate和ejbRemove方法都是空方法。
四.运行本例子
启动J2EE服务器
在命令模式下执行如下命令:
j2ee -verbose
创建消息队列
1. 创建:
2eeadmin -addJmsDestination jms/MyQueue queue
2. 确认消息队列已经创建:
j2eeadmin -listJmsDestination
部署该程序
1. 在deploytool工具中打开SimleMessageApp.ear文件
2. 部署。注意确认在Introduction对话框中选中Return Client JAR复选框
运行客户端
1. 在命令模式下进入j2eetutorial/examples/ears目录
2. 设置环境变量APPCPATH为SimpleMessageAppClient.jar所在目录
3. 运行客户端:
runclient -client SimpleMessageApp.ear -name SimpleMessageClient -textauth
4. 在登陆提示符后输入用户名:j2ee,密码:j2ee
5. 客户端显示结果:
Sending message: This is message 1
Sending message: This is message 2
Sending message: This is message 3
6. J2EE服务器终端(启动J2EE服务器的命令窗口)输出的信息:
MESSAGE BEAN: Message received: This is message 1
MESSAGE BEAN: Message received: This is message 2
MESSAGE BEAN: Message received: This is message 3
五.用deploytool部署消息驱动Bean
本章介绍部署消息驱动Bean和第2章部署企业Bean基础步骤的不同。
指定Bean类型和事务管理机制
打开新建企业Bean向导,创建消息驱动Bean(类文件加入都一样)
1. 在General对话框中,选中Message-Dirven单选按钮
2. 在Transaction Management对话框指定事务管理机制。可以是容器管理(Container-Managed)和Bean管理(Bean-Managed)中的任意一个。不过选择Bean管理的事务时,在第4不中就要指定应答(Acknowledgement)类型。
设置消息驱动Bean的特有属性
你可以在两个地方设置这些属性:
1. 上面提到的新建企业Bean向导的第4步
2. 消息驱动Bean的Message选项页(如图7-2)
需要设置的属性如下:
1. Desination Type通过两个单选按钮Queue(消息队列)和Topic(消息主题)来设置。消息队列使用点对点消息域,它只能有一个消息消费者。消息主题使用发布-订阅消息域,它可以有0个或多个消息消费者。
2. 在Destination下拉框中选择你用j2eeadmin命令创建的消息的JNDI名。目的地(destination)可以是Queue和Topic中的任意类型,它是消息转发服务的提供者(it represents the source of incoming messages and the target of outgoing messages)
3. Connection Factory下拉框,在QueueConnectionFactory和TopicConnectionFactory中选择合适的一个(其实这两个工厂在选择Desination Type就被过滤了一个了)。它们提供J2EE组件访问消息服务的连接。
4. 如果你设置的是Bean管理的事务,那么你也要选择应答(Acknowledgment)类型:Auto-Acknowledge或者Duplicates-OK。Auto-Acknowledge指示会话自动应答消息驱动Bean消费了消息。Duplicates-OK指示会话不必确保对发送消息的应答(The Duplicates-OK type instructs the session to lazily acknowledge the delivery of messages),它可能引起消息重复,但是降低了会话费用。
5. 在JMS Message Selector域中,你可定义过滤收到消息的语句。
图 7-2 SimpleMessageEJB的Message页
六.用deploytool配置JMS客户端
本节只是简要介绍JMS客户端的配置,要知道更多信息请参考Java Message Service Tutorail。
配置资源引用
1. 在树视图中选中客户端节点
2. 选择Resource Refs选项页
3. 点击Add按钮
4. 在Coded Name列输入和客户端调用lookup方法时用的参数对应的名字(当然你也可以在这里先设置了之后,再写客户端的lookup调用)。如本例客户端lookup方法调用的参数是java:comp/env/jms/MyQueueConnectionFactory,则Coded Name应该是:jms/QueueConnectionFactory
5. 在Type列选择和消息目的地(destination)类型一致的连接工厂类
6. 在Authentication列,大部分时候你应该选择Container。如果在程序中编码登录消息服务,你可以选择Application。
7. 在Sharable列中,确信复选框被选中。这样可以让容器优化连接。
8. 在User Name和Password域输入用户名和密码。J2EE SDK的验证服务将在客户端运行时提示你输入用户名和密码。
配置资源环境引用
1. 选择Resource Env. Refs选项页
2. 点击Add按钮
3. 在Coded Name列输入和调用lookup方法定位消息队列或者消息主题时的参数一致的名字。本例中lookup方法调用的参数为:java:comp/env/jms/QueueName,对应的Coded Name为:jms/QueueName
4. 在Type列选择和目的地类型一致的类型(一般工具会自动选择一个正确的类型)
设置JNDI名
1. 在树视图中选择应用程序节点
2. 选择JNDI Name选项页,设置用到资源的正确JNDI名。表7-1列出了本例中使用的JNDI:
表7-1 SimpleMessageApp中的JNDI名
组件或引用名 JNDI名
SimpleMessageEJB jms/MyQueue
jms/MyQueueConnectionFactory jms/QueueConnectionFactory
jms/QueueName Jms/MyQueue
第8章 EJB查询语言
Dale Green著
Iceshape Zeng译
企业JavaBean查询语言(EJB QL)定义CMP的查找和Select方法的查询动作。EJB QL在SQL92子集的基础上作了一些扩展,以允许在实体Bean抽象模式中定义的关系上定义查询,它的查询范围跨越同一个EJB JAR文件中的关联实体Bean的抽象模式。
EJB QL查询在实体Bean的部署描述符中被定义,通常部署工具会将这些查询转化为底层数据库可执行的目标SQL语句。因此CMP实体Bean有更好的可移植性--它的代码不依赖于特定的底层数据库实现。
本章的例子沿用第6章使用的例子。
本章内容:
术语
简单语法
查询例子
简单查找方法的查询
跨越实体Bean关系查找方法的查询
其它条件查找方法的查询
Select方法的查询
全部语法
BNF范式
EJB QL的BNF文法
FROM子句
Path表达式
WHERE子句
SELECT子句
EJB QL的限制
一.术语
下面列除了本章用到的术语:
抽象模式(Abstract schema):实体Bean部署描述符的一部分,定义持久性子段和关系子段
抽象模式名(Abstract schema name):EJB QL中引用的逻辑名字,每一个CMP实体Bean指定一个唯一的抽象模式名
抽象模式类型(Abstract schema type):所有的EJB QL表达式都归结到一定的类型。如果一个表达式是抽象模式名,缺省地它的类型是定义这个抽象模式名的实体Bean的Local接口。
BNF范式(Backus-Naur Form):描述高级语言语法的符号。本章的语法都用BNF范式表示。
关联查询(Navigation):在EJB QL表达式中跨越关系查询。关联操作符是一个句点
Path表达式(Path expression):一个可以根据路径找到关系实体Bean的表达式
持久性字段(Persistent field):CMP实体Bean中的一个虚拟字段,它存储在数据库中。
关系字段(Relationship field):CMP实体Bean中的一个虚拟字段,它标志关系的另一方实体Bean。
二.简单语法
本节简要的描述EJB QL的语法一遍你可以进入下一节查询例子。如果想全面了解语法可以转到第三节全部语法。
一个EJB QL查询有三个子句:SELECT、FROM和WHERE。SELECT和FROM子句是必需的,WHERE子句可选。下面是一个符合高级BNF范式语法的EJB QL查询:
EJB QL ::= select_clause from_clause [where_clause]
SELECT子句定义查询返回的对象或者值,返回类型是一下上种之一:Local接口、Remote接口和持久性字段。
FROM子句声明一个或多个同位变量来定义查询范围,这些变量会在SELECT和WHERE子句中被引用。一个同位变量表示一下元素之一:
实体Bean的抽象模式名
一个一对多关系中"多"方的集合成员
WHERE子句是约束查询返回对象和值的条件表达式。虽然可选但是大部分查询都有一个WHERE子句。
三.查询例子
本节用到的例子都来自第6章的RosterApp应用程序的PlayerEJB实体Bean。要查看RosterApp中个企业Bean之间的关系参考图6-1。
简单查找方法的查询
如果你不熟悉EJB QL语言,下面的简单查询将是一个很好的开始:
例1:
SELECT OBJECT(p)
FROM Player p
查询结果:所有的运动员
对应查找方法:findall()
解释:FROM子句声明一个同位变量p,省略可选关键字AS。上面语句的FROM子句也可以写成:
FROM Player AS p
元素Player是PlayerEJB的抽象模式名。因为findall方法在LocalPlayerHome接口中定义,所以这个查询返回LocalPlayer类型的对象。
关于同位变量的更多信息参考下一节。
例2:
SELECT DISTINCT OBJECT(p)
FROM Player p
WHERE p.position = ?1
查询结果:位置等于查找方法传入的位置参数运动员。
查找方法:findByPosition(String position)
解释:在SELECT子句中,OBJECT关键字必须在像p这样的一个单独的同位变量前。 DISTINCT关键字过滤调重复的数据。
WHERE子句检查找到的运动员的位置来约束返回结果。?1表示findByPosition方法的参数。
关于传入参数、DISTINCT关键字和OBJECT关键字的详细信息参考下一节。
例3:
SELECT DISTINCT OBJECT(p)
FROM Player p
WHERE p.position = ?1 AND p.name = ?2
返回结果:在给定位置并且叫给定名字的运动员
查找方法:findByPositionAndName(String position,String name)
解释:position和name都是PlayerEJB的持久性字段。WHERE子句比较这两个字段和findByPositionAndName方法的传入参数。EJB QL用一个问号后跟一个整数来表示方法的传入参数。第一个参数表示为:?1,第二个参数表示成:?2,以此类推。
跨越实体Bean关系查找方法的查询(关联查询)
在EJB QL语言中,一个表达式可以跨越实体Bean关系的另一方进行查询。这些表达式是EJB QL和SQL之间的主要区别,EJB QL跨越关联的实体Bean,而SQL连接多个表。
例4:
SELECT DISTINCT OBJECT(p)
FROM Player p, IN (p.teams) AS t
WHERE t.city = ?1
返回结果:属于在指定城市的组的运动员
查找方法:findByCity(String city)
解释:FROM子句声明了两个同位变量:p和t。p跟Player同义,表示PlayerEJB实体Bean。t表示关系中的另一方TeamEJB。t的声明引用了前面声明的p。IN关键字表明teams是一个TeamEJB的集合。p.teams表达式关联PlayerEJB和TeamEJB,表达式中的句点是关联操作。
在WHERE子句中,city前的句点是一个限定符,不是关联操作符。严格来讲,关联表达式只能使用关系字段,不能使用持久性字段。访问持久性字段时,表达式中的句点作为限定符使用。
关联表达式不可以越过(或更严格的限定)集合类型的关联字段。在表达式的语法中,一个集合值字段是一个终结符。因为teams字段是集合类型,在WHERE子句中不能写成p.teams.city,这是非法的表达式。
详细描述参考下一节的路径表达式。
例5
SELECT DISTINCT OBJECT(p)
FROM Player p, IN (p.teams) AS t
WHERE t.league = ?1
返回结果:属于特定社团(league)的所有运动员。
查找方法:findByLeague(LocalLeague league)
解释:这个查询跨越了两个关系。p.teams跨越PlayerEJB-TeamEJB关系,t.league跨越TeamEJB-LeagueEJB关系。
在其他例子中,传入参数都是String对象,而这个例子的参数是LocalLeague接口类型,它在WHERE的比较表达式中匹配league关系字段。
例6
SELECT DISTINCT OBJECT(p)
FROM Player p, IN (p.teams) AS t
WHERE t.league.sport = ?1
返回结果:参加了指定运动项目的运动员
查找方法:findBySport(String sport)
解释:sport是LeagueEJB的持久性字段。要访问sport字段,查询必须先关联PlayerEJB到TeamEJB(p.teams)然后关联TeamEJB到LeagueEJB(t.league)。因为league关系字段不是集合类型,它可以在后面接sport持久性字段。
其它条件查找方法的查询
每一个WHERE子句都必须指定一个条件表达式,这样的条件表达式有很多种。在前面的例子中,条件表达式都是检查相等的比较表达式。下面会给出一些其他类型的条件表达式。关于条件表达式的详尽描述请参考下一节的WHERE子句内容。
例7
SELECT OBJECT(p)
FROM Player p
WHERE p.teams IS EMPTY
返回结果:不属于任何组的所有运动员
查找方法:findNotOnTeam()
解释:PlayerEJB的关系字段teams是集合类型,如果运动员不属于任何组,则teams集合为空,而条件表达式返回TRUE。参考下一节的空集合比较表达式
例8
SELECT DISTINCT OBJECT(p)
FROM Player p
WHERE p.salary BETWEEN ?1 AND ?2
返回结果:薪水在指定范围内的运动员
查找方法:findBySalaryRange(double low,double high)
解释:BETWEEN表达式中有三个数学表达式:一个持久性字段(p.salary)和两个传入参数(?1和?2)。可以用下面的表达式替换BETWEEN表达式:
p.salary >= ?1 AND p.salary <= ?2
参考下一节的BETWEEN表达式。
例9
SELECT DISTINCT OBJECT(p1)
FROM Player p1, Player p2
WHERE p1.salary > p2.salary AND p2.name = ?1
返回结果:返回薪水高于指定名字的运动员的所有运动员
查找方法:findByHigherSalary(String name)
解释:FROM子句声明了Player类型的两个同位变量(p1和p2)。因为WHERE子句需要比较两个运动员(p1和p2)的薪水,所以这里需要两个同为变量。(实际上就是自连接查询)。参考下一节同位变量。
Select方法的查询
下面的例子是Select方法的查询,和查找方法不同,Select方法可以返回持久性字段和其他实体Bean。
例10
SELECT DISTINCT t.league
FROM Player p, IN (p.teams) AS t
WHERE p = ?1
返回结果:指定运动员加入的所有社团
Select方法:ejbSelectLeagues(LocalPlayer player)
解释:该查询的返回类型是LeagueEJB实体Bean的抽象模式,该抽象模式映射到LocalLeagueHome接口。因为t.league不是一个单独的同为变量,所以OBJECT关键字省略。参考下一节SELECT子句。
例11
SELECT DISTINCT t.league.sport
FROM Player p, IN (p.teams) AS t
WHERE p = ?1
返回结果:指定运动员参与的所有运动项目
Select方法:ejbSelectSports(LocalPlayer player
解释:该查询返回LeagueEJB的持久性字段sport的集合。
四.全部语法
本节讨论EJB规范定义的EJB QL语法。下面的大部分资料解释规范,有些还是直接引用规范的内容。
BNF范式
表8-1列出了本章用到的BNF符号
表8-1 BNF符号
符号 解释
::= 左边的元素由右边的元素构造
* 该符号前面的元素可以出现0或多次
{...} 花括号里的元素是同种语义效果
[...] 方括号里的元素是可选的
| 左右两个表达式任选其一(或的意思)
粗体单词 关键字(尽管它们都大写,实际上关键字是大小写不敏感的)
空白 可能是空格,退格或者接续行
EJB QL语法的BNF定义
下面是这些语法的定义:
EJB QL ::= select_clause from_clause [where_clause]
from_clause ::= FROM identification_variable_declaration
[, identification_variable_declaration]*
identification_variable_declaration ::=
collection_member_declaration |
range_variable_declaration
collection_member_declaration ::=
IN (collection_valued_path_expression) [AS] identifier
range_variable_declaration ::=
abstract_schema_name [AS] identifier
single_valued_path_expression ::=
{single_valued_navigation |
identification_variable}.cmp_field |
single_valued_navigation
single_valued_navigation ::=
identification_variable.[single_valued_cmr_field.]*
single_valued_cmr_field
collection_valued_path_expression ::=
identification_variable.[single_valued_cmr_field.]*
collection_valued_cmr_field
select_clause ::= SELECT [DISTINCT]
{single_valued_path_expression |
OBJECT(identification_variable)}
where_clause ::= WHERE conditional_expression
conditional_expression ::= conditional_term |
conditional_expression OR conditional_term
conditional_term ::= conditional_factor |
conditional_term AND conditional_factor
conditional_factor ::= [ NOT ] conditional_test
conditional_test ::= conditional_primary
conditional_primary ::=
simple_cond_expression | (conditional_expression)
simple_cond_expression ::=
comparison_expression |
between_expression |
like_expression |
in_expression |
null_comparison_expression |
empty_collection_comparison_expression |
collection_member_expression
between_expression ::=
arithmetic_expression [NOT] BETWEEN
arithmetic_expression AND arithmetic_expression
in_expression ::=
single_valued_path_expression
[NOT] IN (string_literal [, string_literal]* )
like_expression ::=
single_valued_path_expression
[NOT] LIKE pattern_value [ESCAPE escape-character]
null_comparison_expression ::=
single_valued_path_expression IS [NOT] NULL
empty_collection_comparison_expression ::=
collection_valued_path_expression IS [NOT] EMPTY
collection_member_expression ::=
{single_valued_navigation | identification_variable |
input_parameter}
[NOT] MEMBER [OF] collection_valued_path_expression
comparison_expression ::=
string_value { =|<>} string_expression |
boolean_value { =|<>} boolean_expression} |
datetime_value { = | <> | > | < } datetime_expression |
entity_bean_value { = | <> } entity_bean_expression |
arithmetic_value comparison_operator
single_value_designator
arithmetic_value ::= single_valued_path_expression |
functions_returning_numerics
single_value_designator ::= scalar_expression
comparison_operator ::=
= | > | >= | < | <= | <>
scalar_expression ::= arithmetic_expression
arithmetic_expression ::= arithmetic_term |
arithmetic_expression { + | - } arithmetic_term
arithmetic_term ::= arithmetic_factor |
arithmetic_term { * | / } arithmetic_factor
arithmetic_factor ::= { + |- } arithmetic_primary
arithmetic_primary ::= single_valued_path_expression |
literal | (arithmetic_expression) |
input_parameter | functions_returning_numerics
string_value ::= single_valued_path_expression |
functions_returning_strings
string_expression ::= string_primary | input_expression
string_primary ::= single_valued_path_expression | literal |
(string_expression) | functions_returning_strings
datetime_value ::= single_valued_path_expression
datetime_expression ::= datetime_value | input_parameter
boolean_value ::= single_valued_path_expression
boolean_expression ::= single_valued_path_expression |
literal | input_parameter
entity_bean_value ::=
single_valued_navigation | identification_variable
entity_bean_expression ::= entity_bean_value | input_parameter
functions_returning_strings ::=
CONCAT(string_expression, string_expression) |
SUBSTRING(string_expression, arithmetic_expression,
arithmetic_expression)
functions_returning_numerics::=
LENGTH(string_expression) |
LOCATE(string_expression,
string_expression[, arithmetic_expression]) |
ABS(arithmetic_expression) |
SQRT(arithmetic_expression)
FROM子句
FROM子句声明同位变量来定义查询的对象。语法:
from_clause ::= FROM identification_variable_declaration [, identification_variable_declaration]*identification_variable_declaration ::= collection_member_declaration | range_variable_declarationcollection_member_declaration ::= IN (collection_valued_path_expression) [AS] identifier range_variable_declaration ::= abstract_schema_name [AS] identifier
标识符
标识符是一个字符序列。头一个字符必须是符合Java变成语言(以下简称Java)的标识符规则的开始字符(字母、$和_),后面的字符要是Java标识符规则的非开始字符(字母、数字、$和_)。(参考J2SE文档中Character类的isJavaIdentifierStart和isJavaIdentifierPart方法的说明。)问号(?)是EJB QL的保留字,不能出现在标识符中。跟Java变量不同的是,EJB QL标识符大小写不敏感。
标识符不能是如下的EJB QL关键字(20个):
AND AS BETWEEN DISTINCT
EMPTY FALSE FROM IN
MEMBER NOT NULL OBJECT
OF OR SELECT TRUE
IS LIKE UNKNOWN WHERE
这些关键字也是SQL的保留字。以后EJB SQL关键字可能会扩展包含另一些SQL保留字,所以EJB规范建议不要使用其它的SQL保留字作为EJB QL的标识符。
同位变量
同位变量是FROM字句中声明的标识符,尽管SELECT和WHERE子句会引用这些变量,但不能声明它们。所有的同为变量都必须在FROM子句中声明。
因为同位变量也是标识符,所以它有和标识符一样的命名约定和约束。例如,同为变量也是大小写不敏感的也不能使用EJB QL关键字。另外,在给定的EJB JAR文件里,同位变量也不能是其实体Bean的名字和抽象模式名。
FROM子句可以声明多个同位变量,它们之间用逗号隔开。一个声明可以引用前面(左边)声明过的同位变量。如下变量t引用前面声明的p:
FROM Player p, IN (p.teams) AS t
同位变量就算不被WHERE子句引用,它的声明仍然可以影响查询结果。比较下面的两个例子:
1. SELECT OBJECT(p)
FROM Player p
该查询返回所有的运动员。
2.SELECT OBJECT(p)
FROM Player p, IN (p.teams) AS t
因为这里声明了同位变量t,它返回加入了组的运动员
下面的查询返回和上面2相同的结果,但是它的WHERE子句更容易读懂:
SELECT OBJECT(p)
FROM Player p
WHERE p.teams IS NOT EMPTY
同位变量总是表示一个引用,它的类型是声明中使用的表达式的类型。声明可以分为两种:范围变量和集合成员。
范围变量声明
你可以指定一个范围变量以将同位变量声明为一个抽象模式类型。就是说一个同位变量具有实体Bean的抽象模式类型。下面的例子中,同位变量p代表抽象模式Player:
FROM Player p
一个范围变量声明可以包含可选的AS操作符:
FROM Player AS p
大多是情况下,要获得目标对象,查询可以通过路径表达式跨域实体关系。但是不能通过跨越关系获得的目标对象,你只能用范围变量声明来表示一个查询起点(根)。
例如,查询要比较同一个抽象模式中的多个对象,FROM子句必须为该抽象模式声明多个对应的同位对象(上节例9):
FROM Player p1, Player p2
集合成员声明
在一对多关系中,"多"的一方在关系中由实体Bean的集合组成。一个同位变量可以代表集合的成员。为了访问集合成员,变量声明中的路径表达式跨越抽象模式中的实体关系。(路径表达式将在本节后面部分讲到。)因为路径表达式可以嵌套,所以这种关联可以跨越多个关系。(上节例6)
集合成员声明时必须包含IN操作符,但可以省略AS操作符。
下面的例子中,抽象模式名为Player的实体Bean有一个关系字段:teams,同位变量t代表teams集合中的单个成员:
FROM Player p, IN (p.teams) AS t
路径表达式
由于多种原因,使路径表达式成为EJB QL语法中的重要成员。首先它们定义跨越抽象模式中的关系的路径,这些路径定义不仅影响查询范围还同时影响查询结果。其次它们可以出现在EJB QL查询的三种主要子句(SELECT、WHERE和FROM)的任何一种子句中。最后尽管很多EJB QL是SQL的子集,但是路径表达式确实SQL里没有的一种扩展。
语法
路径表达式有两种类型:单值表达式和集合表达式。下面是这两种表达式的语法:
single_valued_path_expression ::=
{single_valued_navigation |
identification_variable}.cmp_field |
single_valued_navigation
single_valued_navigation ::=
identification_variable.[single_valued_cmr_field.]*
single_valued_cmr_field
collection_valued_path_expression ::=
identification_variable.[single_valued_cmr_field.]*
collection_valued_cmr_field
在上面的语法定义中,cmp_field表示持久性字段,cmr_field表示关系字段.。single_valued限定关系字段是一对一或者一对多关系单一值的一方。Collection_valued表示关系字段是集合值的一方。
句点(.)在路径表达式中有两种作用。如果句点出现在持久性字段前,那么它表示在字段和同位变量值键的限定符,如果在关系字段前,表示一个关联操作符。
例子
下面的这个查询中,WHERE子句包含一个单值路径表达式。P是一个同位变量,salary是Player的持久性字:
SELECT DISTINCT OBJECT(p)
FROM Player p
WHERE p.salary BETWEEN ?1 AND ?2
接着的这个例子中,WHERE子句也包含一个单值路径表达式,但跨越多个关系。t是一同位变量,league是一个单值关系字段(关系字段中单一值的一方),sport是league的持久性字段:
SELECT DISTINCT OBJECT(p)
FROM Player p, IN (p.teams) AS t
WHERE t.league.sport = ?1
最后的这个例子中,WHERE子句包含一个集合路径表达式。P是同位变量,teams表示集合关系字段:
SELECT DISTINCT OBJECT(p)
FROM Player p
WHERE p.teams IS EMPTY
表达式类型
表达式的类型是表达式结束元素代表的对象的类型。可以使一下任何一种:
持久性字段
单值关系字段
集合关系字段
例如,因为salary持久性字段的类型是double,则p.salary表达式的类型也是double。而p.teams表达式的结束元素是teams,它是一个集合关系字段,所以这个表达式的类型是Team的抽象模式类型集合,因为Team是TeamEJB实体Bean的抽象模式名,实际上这个类型映射为实体Bean的Local接口:LocalTeam。关于抽象模式的类型映射参考返回类型部分。
关联查询
路径表达式使查询可以跨越关系中的两个实体Bean。表达式的结束元素决定是否允许这种关联,如果一个表达式包含单值关系字段,那么查询可以继续跨越该字段的关联实体。然而查询不能跨越持久性字段和集合关系值段建立关联。例如p.teams.league.sport是一个非法表达式,因为teams是一个集合关系字段。要获得sport字段,可以在FROM子句中为teams声明一个同为变量t:
FROM Player AS p, IN (p.teams) t
WHERE t.league.sport = 'soccer'
WHERE子句
WHERE子句定义限制查询返回结果的条件表达式。查询返回从数据库中读出的使条件表达式为TRUE的所有值。尽管WHERE子句很常用,但它并不是必需的。如果WHERE子句被省略,查询就返回所有值。下面是WHERE子句的高级语法定义:
where_clause ::= WHERE conditional_expression
常量元素
WHERE子句有三种常量:字符串、数字和布尔型。
字符串常量
字符串常量使用单引号括起来的字符序列:
'Duke'
和Java字符串类(String)一样,EJB QL的字符串常量使用Unicode字符编码。
数字常量
WHERE子句支持两种类型的数字常量:精确数字和近似值。
一个精确的数字常量是没有小数点的整数,如65、-233、+12这些。用Java语法描述,精确数字常量支持Java的long类型的表示范围。
一个近似值数字常量是用科学计数法表示的数字,像57.、-85.7、+3.5这样的数字。用Java浮点数描述,近似值数字常量支持double类型的表示范围。
布尔常量
布尔常量只有两个:TRUE和FALSE。大小写不敏感。
传入参数
传入参数用问号(?)后跟一个整数表示。例如第一个参数表示为?1,第二个参数表示为:?2,依次类推。
以下是传入参数的使用规则:
只能在WHERE子句中使用传入参数
在条件表达式中限制只能和单值路径表达式比较
必须用从1开始的整数编号而且不能大于对应的查找或者Select方法中的参数个数
类型必须和相应查找或者Select方法中的对应参数类型匹配
条件表达式
WHERE子句由条件表达式组成,条件表达式符合一般的比较运算和布尔表达式的运算顺序,同优先级的从左到右运算,用括好可以改变默认的运算顺序。下面是条件表达式的语法定义:
conditional_expression ::= conditional_term |
conditional_expression OR conditional_term
conditional_term ::= conditional_factor |
conditional_term AND conditional_factor
conditional_factor ::= [ NOT ] conditional_test
conditional_test ::= conditional_primary
conditional_primary ::=
simple_cond_expression | (conditional_expression)
simple_cond_expression ::=
comparison_expression |
between_expression |
like_expression |
in_expression |
null_comparison_expression |
empty_collection_comparison_expression |
collection_member_expression
运算符和它们的优先级
表8-2按优先级递减的顺序列出了EJB QL的运算符:
表 8-2 EJB QL操作符
类型 预算符
关联查询 . (句点)
算术 + -(一元)
* / (乘除)
+ - (加减)
比较 =
>
>=
<
<=
<> (不等于)
逻辑 NOT
AND
OR
BETWEEN表达式
BETWEEN表达式判断一个算术表达式的值是否在某个范围内。语法定义如下:
between_expression ::=
arithmetic_expression [NOT] BETWEEN
arithmetic_expression AND arithmetic_expression
下面的两个表达式等价:
p.age BETWEEN 15 AND 19
p.age >= 15 AND p.age <= 19
下面的两个表达式也等价:
p.age NOT BETWEEN 15 AND 19
p.age < 15 OR p.age > 19
如果算术表达式有一个空值(NULL),BETWEEN表达式的值不可预料。
IN表达式
IN表达式判断一个字符串是否属于一个字符串常量的集合。语法如下:
in_expression ::=
single_valued_path_expression
[NOT] IN (string_literal [, string_literal]* )
其中的单值路径表达式必须是一个字符串类型。如果单值路径表达式的值为(NULL),IN表达式的值不可预料。
在下面的例子中,如果country是UK,IN表达式值是TRUE。如果country是Peru则表达式值为FALSE:
o.country IN ('UK', 'US', 'France')
LIKE表达式
LIKE表达式判断一个通配符模式和一个字符串是否匹配。语法如下:
like_expression ::=
single_valued_path_expression
[NOT] LIKE pattern_value [ESCAPE escape-character]
其中单值路径表达式是字符串类型,如果为空(NULL),LIKE表达式的值不可预料。Pattern_value是一个可以包含通配符的字符串常量。EJB QL的通配符是下划线(_)和百分号(%)。下划线表示任意单个字符,百分号表示任意字符序列(字符串)。ESCAPE子句指定要从pattern_value的通配符中排除的字符。
表8-3给出了一些LIKE表达式的例子。TRUE和FALSE列表示单值路径表达式的值是列内容时LIKE表达式的值是TRUE或者FALSE。
表 8-3 LIKE 表达式例子
表达式 TRUE FALSE
address.phone LIKE '12%3' '123'
'12993' '1234'
asentence.word LIKE 'l_se' 'lose' 'loose'
aword.underscored LIKE '/_%' ESCAPE '/' '_foo' 'bar'
address.phone NOT LIKE '12%3' 1234 '123'
'12993'
判断空值(NULL)表达式
此种表达式判断一个单值路径表达式是否是空值。通常用来判断一个单值关系字段是否被赋值。如果单值路径表达式运算是遇到空值则它的值为空。表达式语法:
null_comparison_expression ::=
single_valued_path_expression IS [NOT] NULL
判断集合为空表达式
此种表达式判断集合路径表达式中是否没有元素。就是说它检查一个集合关系字段是否被赋值。语法:
empty_collection_comparison_expression ::=
collection_valued_path_expression IS [NOT] EMPTY
如果集合路径表达式的值是空,则判断表达式得到一个空值。(If the collection-valued path expression is NULL, then the empty collection comparison expression has a NULL value.这句话显然在我们的预料之外,如果这个判断语句的值如果不是布尔型……)
判断集合成员表达式
表达式判断给定值是否是集合的成员。该值和集合成员必须是相同的类型。语法:
collection_member_expression ::=
{single_valued_navigation | identification_variable |
input_parameter}
[NOT] MEMBER [OF] collection_valued_path_expression
如果集合路径表达式的值未知,则该判断表达式的值也未知。如果集合路径表达式代表一个空集合,则判断表达式的值为FALSE。
函数表达式
EJB QL提供一些字符串和数学函数,表8-4和8-5列出了这些函数。其中start和length参数都是int类型,它们指示String参数中的位置.number参数可以是int、float和double中的任一类型。
表 8-4 字符串函数
函数声明 返回类型
CONCAT(String, String) String
SUBSTRING(String, start, length) String
LOCATE(String, String [, start]) int
LENGTH(String) int
表 8-5 数学函数
函数声明 返回类型
ABS(number) int, float, or double
SQRT(double) double
空值(NULL)
如果引用目标不在稳定的存储空间中,则该目标为空。对包含空值的条件表达式,EJB QL采用SQL92定义的语义,简要概括该语义如下:
如果一个比较操作或者算术操作有一个未知的值,则表达式为空
如果一个路径表达式有空值参与运算,则表达式为空
IS NULL表达式遇到NULL值返回TRUE,IS NOT NULL遇到空值返回FALSE
布尔运算符和条件表达式的值使用下面的表提供的三种逻辑值(T:TRUE,F:FALSE,U:UNKNOW)
表 8-6 AND 操作
AND T F U
T T F U
F F F F
U U F U
表 8-7 OR 操作
OR T F U
T T T T
F T F U
U T U U
表 8-8 NOT 操作
NOT
T F
F T
U U
表 8-9 条件判断
条件判断 T F U
表达式值是TRUE T F F
表达式值是FALSE F T F
表达式的值是U F F T
相等
EJB QL中只有同种类型的值可以进行比较。精确和近似数字例外,它们可以比较,在他们比较时,按照Java语言的数学运算进行隐式转换。
EJB QL把这些参加比较的值当作Java对象类型来对待,而不是它们在底层数据库中代表的类型。例如如果一个持久性字段只能是整形或者空值,则它必须被指派给一个Integer对象,而不是一个原始int类型。因为Java对象可以为空而原始类型不能为空,所以这种指派是必需的。
两个字符串只有在它们只包含相同的字符序列时才相等,例如'abc'和'abc '就不相等(后者包含一个空个结束)。
同一抽象模式类型的两个实体Bean只有它们的主键值相等时才相等。
SELECT子句
SELECT子句定义查询返回的对象或值的类型。语法:
select_clause ::= SELECT [DISTINCT]
{single_valued_path_expression |
OBJECT(identification_variable)}
返回类型
查询的返回类型必须匹配查询对应的查找或者Select方法的返回类型。
查找方法的查询,返回类型是定义查找方法的实体Bean的抽象模式类型,该抽象模式类型映射到实体Bean的Remote或者Local接口。如果Remote Home接口定义的该查找方法,则返回类型是Remote接口或它的集合);如果是Local Home接口定义的该查找方法,则返回类型是Local接口或它的集合。例如PlayerEJB的LocalPlayerHome接口定义了查找方法findall:
public Collection findAll() throws FinderException;
findall方法的EJB QL查询返回LocalPlayer接口的集合:
SELECT OBJECT(p)
FROM Player p
Select方法的查询,返回类型可能是以下之一:
Select方法所属实体Bean的抽象模式类型
实体关系中另一个实体Bean的抽象模式类型(缺省情况下抽象模式类型映射到实体Bean的Local接口,尽管不常用,你可以在部署描述符中指定映射到Remote接口。)
持久性字段
例如PlayerEJB实体Bean,包含ejbSelectSport方法,返回一个代表sport的String对象的集合,sport是LeagueEJB的持久性字段。(上节例11)
SELECT子句中不能出现集合表达式。如在下面的例子中,SELECT子句中使用p.teams是非法的,因为它是一个集合。而用t(IN(p.teams) AS t)就是合法的,因为它代表teams的单一成员:
SELECT t
FROM Player p, IN (p.teams) AS t
DISTINCT和OBJECT关键字
DISTINCT关键字排除返回值中的重复值。如果查询返回一个java.util.Collection的对象,它允许出现重复值,你可以使用DISTINCT关键字来排除重复值。而当一个方法返回java.util.Set的对象,则DISTINCT关键字就多余了,因为java.util.Set不允许有重复值。
OBJECT关键字必须在单独的同位变量前面,但是不能出现在一个单值路径表达式前。如果同位变量是单值路径表达式的一部分,那么它不是单独的。
五.EJB QL的限制
EJB QL由一些局限:
不支持注释
Date和Time的值都用Java的long类型表示成毫秒。一个为了产生毫秒值,你也需要使用java.util.Calendar类
通常CMP不支持继承,因此不同类型的实体Bean(CMP)无法比较
第三部分 Web技术
本部分翻译:Eward Ding
第9章 网络客户端及组件
当基于浏览器的网络客户端与J2EE应用程序通信时,它是通过服务方的称为网络组件的对象实现的。有两种类型的网络组件:Java Servlet 和JSP。Servlet是Java编程语言的类,它能够动态处理请求并响应。JSP页面是基于文本的能作为Servlet运行,但它能够以更自然的方式创建动态内容。尽管Servlet和JSP可以交互使用,但他们各有各的优点,Servlet适用于功能控制管理,例如,分派请求及处理非文本数据;JSP页面则适合于产生基于文本的标记语言如HTML,SVG,WMLXML。
这章描述了为网络客户端打包、配置、部署的过程,第10章及11章将讲述如何开发这些组件。许多JSP技术的特点取决于Servlet技术,因此你必须熟练那部分内容,哪怕你不打算写Servlet。
多数J2EE客户端都使用HTTP协议,支持HTTP是网络组件的主要部分。在附录A有关于HTTP协议的简单小结。
网络客户端的生命周期
网络客户端部分的服务端由网络组件组成,静态的资源文件如图像、帮助类、及库。J2EE平台提供了数多服务,如提高网络组件的能力以使程序易于开发。然而,因为考虑到这些服务,处理创建及运行网络客户端的过程不同于传统的单一的Java类。
网络组件运行在称为网络容器的环境中。网络容器提供如请求分派,安全,并发,生命周期管理的服务。它也为网络组件提供J2EE平台的API入口如:命名,事务及电子邮件。
在它运行之前,网络客户端必须打包为网络应用包(WAR),一各类似于JAR的包。
某些网络客户端的行为再它部署之后会被配置。配置信息包含在XML格式的称为网络应用部署描述符的文件。当你使用J2EE软件开发包部署工具创建网络客户及组件时,它会自动的通过部署工具输入的数据产生或更新部署描述符。你也可以根据Java Servlet规范手工创建部署符。
这个创建,部署及运行网络客户程序的过程可以总结如下:
1. 开发网络组件代码;
2. 打包这些网络组件及任何静态资源及组件引用的帮助类;
3. 部署应用程序;
4. 进入引用网络客户端的URL
开发网络组件代码在第10、11章中,步骤2到4将在下几节中展开,下面是一个Hello,world型程序,它让用户在一表单中输入名字,在提交后接着显示问候:
部署网络组件代码的部分将在后面的相关章节中介绍。
网络应用结构
网络客户端被打包到WAR,除了网络组件,一个WAR通常包含以下文件:
. 服务端的工具类(数据Bean,购物车等),这些类遵守JavaBean组件结构;
. 静态网页内容(HTML,image,sound files,etc.)
. 客户端类(applets和帮助类)。
网络组件及静态网页内容统称为网络资源。
一个WAR由一个特定的目录结构。WAR的顶级目录时应用程序的文档根。这个根是JSP,客户端类及包,静态网络资源的存放点。
根又包含有子目录WEB-INF,它包括以下文件及子目录:
. web.xml:网络应用部署符;
. 标签库描述文件;
. 类:包含服务方的类如:servlet,帮助类及JavaBean组件;
. 库:包含JAR库的一个目录。
你也可以在跟或WEB-INF/classes目录下创建特定应用程序的子目录
创建WAR文件
当你第一次把网络组件加到J2EE应用程序时,部署工具自动的创建了一个新的WAR文件。后面的部分描述了如何加入网络组件。
你也可以通过一下三种方式手动的创建WAR:
1、 使用J2EE SDK随带的打包工具。
2、 使用ant便携工具。
3、 使用随J2SE一起发布的JAR工具。如果你要安排应用程序开发目录以符合WAR格式,直接创建WAR格式即可,只需简单的在顶级目录执行下面的命令:
jar cvf archiveName.war
注意:要应用其中的任一方法,你必须手工创建正确格式的部署符。
将WAR文件加入到EAR文件中
如果你手工创建了一个WAR文件或者从合作者那儿获得了WAR文件,你可以通过下面的方法将它加入到一个存在的EAR文件中:
1、 选择一个J2EE程序;
2、 选择File'Add'Web WAR;
3、 选择要加入的WAR文件,单击Add Web WAR.
你也可以使用打包工具将一个WAR文件加入到J2EE程序。
增加一个Web组件到WAR文件中
下面的过程描述了如何在应用程序HelloApp中创建并加入一个Web组件到WAR。尽管当你加入一个组件时,Web组件想到会提示组件级配置信息,本章将描述如何在应用程序中增加组件及提供配置信息,WAR,Web组件检查:
1、 到目录j2eetutorial/examples,编译例子并运行 ant hello1。
2、 创建一个名为HelloApp的应用程序。
a. 选择 File 'New'Application;
b. 单击浏览;
c. 选择j2eetutorial/examples/src/web/hello1;
d. 在文件名框中输入HelloApp;
e. 单击New Application
f. 单击OK.
3、 创建WAR文件,增加GreetingServlet Web组件及所有的HelloApp应用程序内容。
a. 通过选择File'New'Web组件使用Web组件向导;
b. 在标注有Create New WAR文件的复选框中选择Hello1App.输入HelloWAR
c. 单击Edit以增加内容文件
d. 在编辑对话框中,选择j2eetutorial/examples/build/web/hello1.选择GreetingServlet.class,ResponseServlet.class.及duke.waving.gif,单击Add,点击OK
e. 单击Next;
f. 选择Servlet单选按钮;
g. 选择Next;
h. 从Servlet复选框中选择GreetingServlet
i. 单击Finish.
4、 增加ResponseServlet Web组件
a. 通过选择File'New'Web组件调用Web组件向导;
b. 在复选框标注 Add to Existing WAR File,选择HelloWAR.
c. 单击Next;
d. 选择Servlet单选按钮;
e. 单击Next;
f. 从Servlet类复选框中选择ResponseServlet;
g. 单击Finish.
配置Web客户
下面部分描述了经常需要指定的Web客户配置参数。配置参数在三级需要指定:application,WAR,及组件。一些安全参数可以在WAR及组件级应用。
应用级配置
Context root
Context root 是获得映射到文档根的Web客户。如果你的客户上下文的根是:catalog,那么请求URL
http://
将从文档根处得到index.html文件。
在部署工具中为HelloApp指定上下文的根
1、 选择HelloApp
2、 选择Web context tab
3、 在上下文根于内输入hello1.
WAR级的配置
下面的部分给出了指定WAR级配置信息的一般过程。
上下文参数
WAR中的Web组件共享一个代表网络上下文的对象。指定符合上下文的初始参数。
组件及配置
初始化参数
指定符合Web组件的初始化参数,
1、 选择Web组件
2、 选择Init.Parameters 标签
3、 单击Add以加入一个新的参数及值。
指定别名参数
当Web容器收到请求时,它必须决定哪一个Web组件来响应请求。这通过URL映射到Web组件。一个URL路径包含上下文根及别名路径:
http://
在servlet能够响应之前,Web容器必须有至少一个组件别名。别名路径必须以"/"开始,以字符串或以通配符表达式结尾(如*.jsp)。由于Web容器能自动的映射到以*.jsp结尾的别名路径,你不需要为JSP页面指定一个别名路径,除非你希望引用一个页面。
部署网络客户
在创建、打包、配置网络客户之后,下一步就是部署EAR文件。
运行网络客户
当浏览器指向一个映射到客户程序的一个组件容器的URL时网络客户就被执行。
更新网络客户
在部署期间,也许你经常需要将网络客户进行变动,通过修改servlet源文件,重新编译servlet类,更新WAR内的组件,并重新部署应用程序。
当你运行该程序时,响应就会发生变化:
国际化网络客户
国际化就是让一个应用程序支持各国语言的处理过程。本地化就是采用使国际化的应用程序支持指定的语言或本地语言。尽管客户界面都应当国际化和本地化,在网络客户中尤为重要,为了更好的了解国际化及本地化可以参看:
http://java.sun.com/docs/books/tutorial/i18n/index.html
在简化的国际化程序中,字符串从包含为使用语言进行翻译的资源包中读取数据。资源包映射一个显示给用户字符串程序的键,这样,不必在代码里直接创建字符串,你创建一个资源包(包含翻译且从包中使用响应的键读取翻译)。资源包可以使文本文件或一个包含映射的类。
第10章 Java Servlet技术
网络一开始提供服务,服务供应者们就意识到动态内容的需要。Applets--是最早的用于动态内容的java类,主要通过使用客户平台传送动态用户经验。与此同时,开发者们也研究使用服务方平台以达到这个目的。最初,公共网关接口(CGI)脚本是产生动态内容的主要的技术。
尽管被广泛的应用,CGI脚本有着许多缺点,如平台相关性,缺乏可升级性。为克服这些缺点,Java servlet技术作为一种以简单的方式提供动态的,面向用户的技术诞生了。
什么是Servlet?
Servlet是java语言类,用来拓展通过请求响应模式的服务端的能力。尽管servlets可以响应任何类型的响应,它们通常用于拓展基于Web的应用程序。在这中应用程序中,Java servlet技术定义了特定的HTTP servlet类。
包javax.servlet和javax.servlet.http提供了写servlets的接口和类。所有的servlets都必须实现servlet借口,它定义了生命周期方法。
当实现一个一般的服务时,你能使用或拓展GenericServlet类,该类提供了Java Servlet API,HttpServlet类提供了一些方法,如doGet及doPost,用来处理特定的http服务。
本章主要集中在编写产生响应HTTP请求的servlets。这里假设你以了解一些HTTP协议的知识;如果你不熟悉该协议,你可以参考附录A。
有关Servlets的例子
这一章使用了Duke's Bookstore程序来演示编写servlets的任务。下面的表格列出了所有的处理书店功能的servlets。每一任务都通过servlet展示。例如,BookDetailsServlet展示了如何处理HTTP GET请求,BookDetailsServlet及CatalogServlet显示了如何构造响应,CatalogServlet显示了如何跟踪会话信息。
Function Servlet
进入书店 BookStoreServlet
创建书店标语 BannerServlet
浏览书店目录 CatalogServlet
放书到购物车中 CatalogServlet,BookDetailsServlet
取得特定书籍的详细信息 BookDetailsServlet
显示购物车的情况 ShowCartServlet
从购物车中去掉一本或多本书籍 ShowCartServlet
购买书籍 CashierServlet
图书程序的数据保存在数据库中,数据库包也包含类BookDetails。购物车及购物车中的具体的项目由类Cart.ShopingCart及Cart.ShoppingCartItem来分别表示。
书店应用程序的源代码位于j2eetutorial/examples/src/web/bookstore1目录中,可以通过一下步骤来编译、部署、运行例子:
1、 到j2eetutorial/examples目录编译例子并运行ant bookstore1
2、 启动j2ee服务器
3、 打开部署工具
4、 启动Cloudscape数据库服务器
5、 通过运行ant create-web-db来装载数据到数据库
6、 创建名为Bookstore1App的J2EE应用程序
a. 选择File'New'Application.
b. 在文件选择中选择j2eetutorial/examples/src/web/bookstore1.
c. 在文件名域中,输入Bookstore1App.
d. 单击New Application
e. 单击OK.
7、 创建WAR并将BannerServlet Web组件及所有的Duke's书店内容加入到Bookstore1App程序中
a. 选择File'New'Web组件
b. 在应用程序单选按钮中单击创建新的WAR文件并从复选框中选择Bookstore1App.
c. 单击Edit以增加文件内容
d. 在编辑对话框中浏览到j2eetutorial/examples/build/web/bookstore1.选择BeanerServlet.class,CatalogServlet.class,ShowCartServlet.class,CashierServlet.class及ReceiptServlet.class.点击Add。增加errorpage.html,duke.books.gif,增加cart,database,exception,filters,listeners,messages和工具包,单击OK.
e. 单击Next
f. 选择servlet单选按钮
g. 单击Next
h. 选择BannerServlet
i. 连续单击Next两次
j. 在复选框别名栏中,单击Add,接着在别名域中输入/banner
k. 单击Finish.
8、增加下表列出的网络组件,对于每个Servlet,单击Add to Existing WAR File单选按钮并从复选框中选择Bookstore1WAR。
Web Component Name Servlet Class Component Alias
BookStoreServlet BookStoreServlet /enter
CatalogServlet CatalogServlet /catalog
BookDetailsServlet BookDetailsServlet /bookdetails
ShowCartServlet ShowCartServlet /showCart
CashierServlet CashierServlet /cashier
ReceiptServlet ReceiptServlet /receipt
9、 Cloudscape数据库增加资源引用。
a. 选择Bookstore1WAR.
b. 选择资源引用标签
c. 单击Add
d. 从类型栏里选择javax.sql.DataSource
e. 在代码名称域中输入jdbc/BookDB
f. 在JNDI域中输入jdbc/Cloudscape.
10、 增加监听类listeners.ContextListener
a. 选择事件监听标签
b. 单击Add
c. 在事件监听类栏的下拉框中选择listeners.ContextListener类
11、 增加一个错误页面
a. 选择文件引用标签
b. 在错误映射栏中,单击Add
c. 在错误/异常域中输入exception.BookNotFoundException
d. 在资源调用域中输入/errorpage.html
12、 增加filters filters.HitCounterFilter及filters.OrderFilter
a. 选择Filter Mapping
b. 单击Edit Filter List
c. 单击Add
d. 从Filter类栏中选择filters.HitCounterFilter
e. 单击Add
f. 从Filter类栏中选择filters.OrderFilter.部署工具会自动的在现实名称栏中输入OrderFilter.
g. 单击OK
h. 单击Add
i. 从Filter名称栏中选择HitCounterFilter
j. 从目标类型栏中选择servlet
k. 选择BookStoreServlet.
l. 对于OrderFilter也重复上面的步骤,目标类型是Servlet,目标是ReciptServlet
13、 输入上下文的根
a. 选择Bookstore1App
b. 选择网络上下文标签
c. 输入bookstore1
14、 部署应用程序
a. 选择工具'部署
b. 单击完成
15、 打开bookstore的URL http://
处理错误
这部分介绍常见错误及解决方法(尤指网络客户运行错误),并列出了一些为什么网络客户连接失败的原因。此外,Duke's Bookstore返回下面的异常:
1、 BookNotFoundException:如果图书不在书店数据库中将产生这个异常,
2、 BooksNotFoundException:如果书店数据不能返回将产生这个异常。
3、 UnavailableException:如果一个servlet不能返回代表书店的网络上下文属性信息。
由于我们已经指定了一个错误页面,你将会看到这样的信息:The application is unavailable.Please try later。如果没有指定错误页面,网络容器将产生一个默认的页面产生这样的信息:A ServletException Has Occurred and a stack trace that can help diagnose the cause of the exception.如果使用errorpage.html,你不得不堪网络容器的日志来判断产生异常的原因。网络日志在下面的目录中:$J2EE_HOME/logs/
Servlet的生命周期
部署了的Servlet的生命周期是由容器控制的。当一个请求映射到相应的servlet时,容器产生下面的步骤:
1.如果servlet的实例不存在,容器会
a. 载入servlet类
b. 创建一个servlet实例
c. 通过调用init方法初始化servlet实例
2.调用service方法,
如果容器需要删除servlet,可以通过调用destroy方法。
处理servlet生命周期事件
在servlet生命周期中,你可以通过定义一个监听对象来监听它的生命周期,当生命周期事件发生时,该监听对象就会被调用。要使用这些监听对象,你必须定义监听类并指定监听类。
定义监听类
定义一个监听类并实现监听接口。下面的表列出了能够被监听并必须实现的相应的接口:
Object Event Listener Interface and Event Class
Web context Initialization and destruction Javax.servlet.ServletContextListener and ServletContextEvent
Attribute added,remove,replace javax.servlet.ServletContextAttributeListener and ServletContextAttributeEvent
Session Creation,invalidation,timeout Javax.servlet.http.HttpSessionListener and HttpSessionEvent
Attribute added,remove,replace Javax.servlet.http.HttpSessionAttributeListener and HttpSessionBindingEvent
当一个监听方法被调用时,它恰当的传递一个包含信息的事件。例如,在HttpSessionListener接口中的方法传递一个包含HttpSession的HttpSessionEvent。
在Duke's Bookstore应用程序中,类listeners.ContextListener创建及删除一个数据库帮助和计数对象。这个方法重新从ServletContextEvent返回网络上下文对象,接着吧该对象做为servlet的上下文属性保存起来。
Import database.BookDB;
Import javax.servlet.*;
Import util.Counter;
Public final class ContextListener implements ServletContextListener
{
private ServletContext context=null;
try
{
BookDB bookDB=new BookDB();
Context.setAttribute("bookDB",bookDB);
}catch(Exception ex){System.out.println("Couldn't create database:"+ex.getMessage());}
Counter counter=new Counter();
Context.setAttribute("hitCounter",counter);
Context.log("Created hitCounter"+counter.getCounter());
Context.setAttribute("orderCounter",counter);
Context.log("Created orderCounter"+counter.getCounter());
}
public void contextDestroyed(ServletContextEvent event)
{
context=event.getServletContext();
BookDB bookDB=context.getAttribute("bookDB");
Context.removeAttribute("bookDB");
Context.removeAttribute("hitCounter");
Context.removeAttribute("orderCounter");
}
}
指定事件监听类
处理错误
当Servlet执行时任何数量的异常都有可能发生。网络容器将会产生一个默认的页面包含这样的信息:A Servlet Exception Has Occurred when an exception occurs,你也可以指定容器返回一个特定的异常到指定的错误页面中。
共享信息
网络组件像其它的对象一样,通常同其它的对象协调工作。它们通过这样一些方法实现。可以使用私有帮助对象,可以共享作为公共域属性的对象,可以使用数据库,也可以调用其它资源。Java Servlet技术机制是的网络组件可以调用其它网络资源。
使用域对象
共享信息通过做为4类域对象属性的对象来协调网络组件,这些属性通过类的get/set属性方法来展示域。下表列出了域对象:
Scope Object Class Accessible From
Web context Javax.servlet.ServletContext Web components within a Web context
Session Javax.servlet.http.HttpSession Web components handling a request that belongs to the session
Request Subtype of javax.servlet.ServletRequest Web components handling the request
Page Javax.servlet.jsp.PageContext The JSP page that creates the object
下图展示了由Duke's Bookstore应用维护的域属性:
控制共享资源的并发入口
在一个多线程的服务器中,共享资源并发使用是可能的。除了域对象属性外,共享资源包括进驻内存的数据,例如实例或类变量,以及外部对象如文件,数据库连接,网络连接。并发可在下面的一些情况下发生:
. 多数网络组件访问对象存储在网络上下文中
. 多数网络组件访问对象存储在会话中
. 网络组件中的多线程访问实例变量。网络容器将产生一个线程来处理每个请求。如果你想让servlet在某个时刻只处理一个请求,servlet可以实现接口SingleThreadModel。如果一个servlet实现了该接口,可以保证没有两个线程回并发调用servlet的service方法。网络容器可以通过同步访问单个servlet实例以实现这个保证,或者通过维护网络组件实例的池并分发新的请求到一个闲着的servlet。这个接口不阻止由于网络组件访问共享资源(例如静态类变量或外部对象)产生的同步问题。
当资源能并发访问的时候,它们可以以一种不一致的方式来使用。为了防止这种情况,你必须控制通过并发技术实现的访问。
在前面的部分里,我们展示了被servlet共享的五种域属性:bookDB,Cart,currency,hitCounter及orderCounter. bookDB将在下一部分讨论。Cart,counter可以被垛线程的servlet设置、读取。
为防止这些对象被不一致的读取,访问通过并发方法来控制。例如,下面是util.Counter类
public class Counter
{
private int counter;
public counter()
{
counter=0;
}
public synchronized int getCounter()
{
return counter;
}
public synchronized int setCounter(int c)
{
counter=c;
return counter;
}
public synchronized int incCounter()
{
return (++counter);
}
}
访问数据库
数据在各网络组件间共享,J2EE应用程序的数据调用通常通过数据库来维护。网络组件通过JDBC2.0API来访问关系型数据库。书店应用程序的数据存放在数据库里并通过帮助类database.BookDB来访问。例如,当顾客下单时ReceiptServlet调用BookDB.buyBooks方法来更新书的清单。这个方法对购物车中的每本书都可调用buyBook。为确保订单完整的处理,这个调用被封在一个单一的JDBC事务中。共享数据库的连接通过get/release方法实现同步。
Public void buyBooks(ShoppingCart cart) throws OrderException
{
Collection items=cart.getItems();
Iterator I=items.iterator();
Try
{
getConnection();
con.setAutoCommit(false);
while(i.hasNext())
{
shoppingCartItem sci=(shoppingCartItem)i.next();
BookDetails bd=(BookDetails)sci.getItem();
String id=bd.getBookID();
Int quantity=sci.getQuantity();
BuyBook(id,quantity);
}
con.commit();
con.setAutoCommit(true);
releaseConnection();
}catch(Exception ex)
{
try
{
con.rollback();
releaseConnection();
throw new OrderException("Transaction failed:"+ex.getMessage());
}catch(SQLException sqx)
{
releaseConnection();
throw new OrderException("Rollback failed:"+sqx.getMessage());
}
}
}
初始化Servlet
在网络容器载入并实例化Servlet之后,被客户请求之前,网络容器将初始化servlet。你可以通过改写init方法来自己定制这个过程来允许servlet读取持久配置数据,初始化资源,及处理其它一次性的事情。一个servlet不能完成其初始化过程将抛出UnavailableException.
所有的访问书店数据库的servlet都在init方法中初始化由网络上下文产生的指向数据库帮助类对象的变量:
public class CatalogServlet extends HttpServlet
{
private BookDB bookDB;
public void init() throws ServletException
{
boolDB=(BookDB)getServletContext().getAttribute("bookDB");
if(bookDB==null) throw new UnavailavleException("Couldn't get database.");
}
}
编写Service方法
servlet提供的service方法实现了GenericServlet类的service方法。HttpServlet的doMethod方法,或其它类定义的实现了Servlet接口的特定协议方法。在本章的下面几部分,service方法将在servlet中为客户提供服务。
一般的service方法模式通过request获取信息,访问外部资源,然后根据信息做出相应的响应。
对于httpServlet,正确的响应过程是首先填写响应头,接着从响应返回输出流,最后向输出流写上程序体内容。响应头必须在PrintWriter和ServletOutputStream之前设置,因为HTTP协议希望在程序体内容之前接受所有头信息。接着的两部分描述了如何从request获取信息并产生响应。
通过request取得信息
一个请求包含客户端与servlet之间的数据传递。所有的请求都实现了ServletRequest接口。该接口定义了访问一下信息的方法:
. 参数,典型的实用它来在客户端与Servlet之间传递信息。
. 对象属性,主要用来在servlet容器中的servlet间的信息传递。
. 使用通信协议的信息,以及请求调用中的客户与服务。
. 本地相关信息。
例如,在CatalogServlet中,客户想购买的书籍的标志作为请求参数。在下面的代码中显示了如何使用方法getParameter来取得标志:
String bookID=request.getParameter("Add");
If(bookID !=null)
{
BookDetails book=bookDB.getBookDetails(bookID);
}
你也可以从请求中获得输入流并解析数据。要读取字符数据流,可以使用由请求(request)方法getReader返回BufferedReader对象,要读取二进制数据,可以使用getInputStream方法返回的ServletInputStream对象。
HTTP servlet传递HTTP request对象HttpServletRequest,它包含requestURL,HTTP头,查询字符串,等等。
一个HTTP请求URL包含以下部分:
http://
request path由下面的几部分组成:
. Context path: J2EE应用程序的servlet的上下文根构成的一连串斜线(/)。
. Servlet path: 激发请求的组件路径部分,这部分以斜线开头。
. Path info : 请求路径部分,这部分不是上下文路径,也不是servlet路径。
如果上下文路径是/catalog,别名列于下表:
Pattern Servlet
/lawn/* LawnServlet
/*.jsp JSPServlet
下表给出了将URL分解的例子:
Request Path Servlet Path Path Info
/catalog/lawn/index.htnl /lawn /index.html
/catalog/help/feedback.jsp /help/feedback.jsp null
查询字符串由一系列参数和其值组成,参数分别从request的方法getParameter获得其值。有两种方法产生查询字符串:
. 查询字符串可以显示的出现在Web页面中。例如,由CatalogServlet产生的HTML页面可能包含下面的连接:
Add to Cart
CatalogServlet通过下面的方法分解出这个参数:
String bookId=request.getParameter("Add");
. 当一个表单通过GET HTTP提交时,查询字符串可以跟在URL后面。在Duke's Bookstore 应用程序中,CasherServlet产生一个表单,接着一个输入到表单中用户名跟到URL后面映射到ReceiptServlet,最后ReceiptServlet通过getParameter方法获得用户名。
构造Responses
一个响应包含数据在服务器域客户之间的传递。所有的响应都实现了ServletResponse接口。该接口的方法允许你做下面的事:
. 获得输出流用来向客户端输出数据。发送字符型数据可以使用由response's getWriter方法返回PrintWriter,要发送二进制数据,可以使用getOutputStream方法返回的ServletOutputStream要发送二者的混合数据,可以通过ServletOutputStream人工管理字符部分以创建一个多部分响应。
.指出内容类型(content type,例如text/html),
.指出是否缓冲输出。默认情况下,输出流中的任何内容都是立即送到客户端。在任何数据实际发送到客户之前,缓冲允许数据写入,这样允许servlet有更多的时间设置正确的状态码、头信息或定向到期它网络资源。
.设置本地信息。
HTTP响应对象--HttpServletResponse,有属性代表HTTP头,例如:
.状态码,用来线时请求失败的原因。
.Cookies,用来在客户端存储特定的应用信息。有时cookies也用来作为标志来跟踪用户的会话。
在Duke's书店应用中,BookDetailsServlet产生HTML页面以显示由servlet从数据库返回的有关书的信息。Servlet首先设置响应:响应的内容类型,缓冲大小;因为数据库访问可能产生异常,因此servlet缓存页面内容以重定向到错误页面。通过缓存响应,客户端不会看到连串的错误页面。DoGet方法由响应返回PrintWriter.
Servlet首先分发请求到BannerServlet用来产生公用旗帜以代替响应,接着,Servlet从请求参数获得图书标志,并使用该标志从数据库返回相关信息,最后,servlet产生HTML以描述图书信息,并通过调用PrintWreter的close方法执行响应。
Public class BookDetailsServlet extends HttpServlet
{
public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException
{
//set headers before accessing the Writer
response.setContentType("text/html");
response.setBufferSize(8192);
PrintWriter out=response.getWriter();
//then write the response
out.println(""+"
//get the dispatcher;it gets the banner to the user
RequestDispatcher dispatcher=getServletContext().getRequestDispatcher("/banner");
If(dispatcher!=null)
Dispatcher.include(request,response);
//get the identifier of the book to display
String bookId=request.getParameter("bookId");
If(bookId!=null)
{
try
{
BookDetails bd=bookDB.getBookDetails(bookId);
……
//print out the information obtained
out.println(""+bd.getTitle()+"
"
}catch(BookNotFoundException ex){};
}
out.println("