j2ee详解指南


 

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) {
         ...
      }
   }
   ...
%>
<html>
<head>
    <title>Converter</title>
</head>

<body bgcolor="white">
<h1><center>Converter</center></h1>
<hr>
<p>Enter an amount to convert:</p>
<form method="get">
<input type="text" name="amount" size="25">
<br>
<p>
<input type="submit" value="Submit">
<input type="reset" value="Reset">
</form>
<%
   String amount = request.getParameter("amount");
   if ( amount != null && amount.length() > 0 ) {
      BigDecimal d = new BigDecimal (amount);
%>
   <p><%= amount %> dollars are 
      <%= converter.dollarToYen(d) %>  Yen.
   <p><%= amount %> Yen are
      <%= converter.yenToEuro(d) %>  Euro.
<%
    }
%>
</body>
</html>
编译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客户端。<host>表示运行J2EE服务器的机器名,如果你的J2EE服务器和访问浏览器在同一机器上,可以用localhost代替作为机器名。
http://<host>:8000/converter
在显示页面的输入框中输入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) <name>EJB AccountEJB
EJB的存档文件JAR命名(DD) <name>JAR AccountJAR
企业Bean主类命名 <name>Bean AccountBean
Home接口命名 <name>Home AccountHome
Remote接口命名 <name> Account
Local home接口命名 Local<name>Home LocalAccountHome
Local接口命名 Local<name> LocalAccount
抽象数据模式命名(DD) <name> Account
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组合框中选择<none>。
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://<host>:8000/catalog/index.html
将从文档根处得到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://<host>:8000/contextroot/aliaspath
在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://<host>:8000/booktore1/enter
处理错误
 这部分介绍常见错误及解决方法(尤指网络客户运行错误),并列出了一些为什么网络客户连接失败的原因。此外,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/<host>/web,并以catalina.<date>.log命名。
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://<host>:<port><request path>?<query string>
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页面可能包含下面的连接:
<a href="/bookstore1/catalog?Add=101">Add to Cart</a>
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("<html>"+"<head><title>+messages.getString("TitleBookDescription")+
</title></head>");
  //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("<h2>"+bd.getTitle()+"</h2>"
   }catch(BookNotFoundException ex){};
  }
  out.println("</body></html>");
  out.close();
 }
过滤请求及响应
 过滤是一个可传送头或内容或者二者的请求及响应的对象。过滤对象不同于网络组件之处在于它不是自己产生响应,它提供了能依附其它网络资源的功能。因此,过滤对象不能独立于网络资源,它不只可以与一种网络资源组合。它的主要功能是:
 .查询请求并作相应的处理。
 .阻止请求响应对进一步通过。
 .修改请求头及数据。通过提供一个自定义的请求
 .修改响应头及数据。通过提供一个自定义的响应。
 .域外部资源交互。
 过滤的应用包含授权、日志、图像转换,数据压缩,加密,表示流,及XML转换。
 总之,使用过滤的任务包含以下方面:
 .编写filter
 .编写自定义请求及响应
 .为每个网络资源指定过滤链。
编写Filter
 过滤API由Filter,FilterChain及FilterConfig接口定义在javax.servlet包中。你通过实现Filter接口定义一个filter。这个接口中最重要的方法是doFilter,它传递request,response,filter.这个方法可以实现下面的动作:
 .检查请求头。
 .如果希望修改请求头或数据可以自定义请求体
 .如果希望修改响应头或数据可以自定义响应体
 .如果当前的filter在链中是最后一个以网络资源或静态资源的filter,可以调用过滤链中的下一个实体,下一个实体是链尾的资源;另外,它是下一个在WAR中配置的filter。它通过调用链体中的doFilter来调用下一个实体。如果要做出选择,可以选择不通过调用下一实体来阻止请求。在下面的例子中,filter负责响应。
 .在调用链中的下一个filter之后检查响应头。
 .抛出异常以说明正在处理错误。
 除了doFilter,你必须实现init, destroy方法。当filter实例化后,Init方法由容器自己调用。如果你希望传递初始参数到filter,你可以通过FilterConfig对象传递到init方法。
 在Duke 's Bookstore程序中使用了HitCounterFilter及Orderfilter来增加,日志计数值。
 在doFilter方法中,两个filter都从filter配置体中获得servlet context.所以它们可以访问作为context属性的计数值。在filter完成特定应用处理后,它们调用filter chain中的doFilter方法传递到原来的doFilter方法。
Public final class HitCounterFilter implements Filter
{
 private FilterConfig filterConfig=null;
 public void init(FilterConfig filterConfig)
    throws ServletException  {this.filterConfig=filterConfig; }
 public void destroy()
 {
  this.filterConfig=null;
 }
 public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
    throws IOException,ServletException
 {
  if(filterConfig==null)
   return;
  StringWriter sw=new StringWriter();
  PrintWriter writer=new PrintWriter(sw);
  Counter counter=(Counter)filterConfig.getServletContext().getAttribute("hitCounter");
  Writer.println();
  Writer.println("==============");
  Writer.println("The number of hits is:"+counter.incCounter());
  Writer.println("==============");
 //log the resulting string
  writer.flush();
  filterConfig.getServletContext().log(sw.getBuffer().toString());
   ……..
  chain.doFilter(request,wrapper);
   ……..
 }
}
编写自定义的请求与响应
 filter有很多种方法修改请求及响应。例如,filter可以可以增加一个属性到请求中,或插入数据到响应中。在Duke's Bookstore这个例子中,HitCounterFilter插入counter的值到响应中。
 Filter在修改响应必须在它返回到客户端时捕获响应,方法就是传递替代流到产生响应的servlet,替代流防止servlet关闭源响应。
 为传递这替代流到servlet,filter创建一个响应包取代getWriter,getOutputStream方法以返回这替代流。这个替代流传递到filter链的doFilter方法。下面的部分描述了hit counter filter是如何描述早期及其它类型filter使用包的。 
 为重载请求方法,你可以包装请求到一个扩展ServletRequestWrapper或DttpServletWrapper对象。要重载响应方法,可以包装响应到扩展了ServletResponseWrapper或HttpServletResponseWrapper.
 HitCounterFilter在CharResponseWrapper中包装了响应。包装的响应被传递到filter链中的下一个对象。BookStoreServlet将响应写到由CharResponseWrapper产生的流中。当chain.doFilter返回时,HitCounterFilter从PrintWriter得到响应,并写入到缓存。Filter插入counter的值到缓存,重设响应头的内容长度,最后将缓存中的内容写到响应流。
PrintWriter out=response.getWriter();
CharResponseWrapper wrapper=new CharResponseWrapper(
 (HttpServletResponse)response);
chain.doFilter(request,wrapper);
CharArrayWriter caw=new CharResponseWrapper();
Caw.write(wrapper.toString().substring(0,wrapper.toString().indexOf("</body>")-1));
Caw.write("<p>/n<center>"+messages.getString("Visitor")+"<font color='red'>"+counter.getCounter()+"</font></center>");
Caw.write("/n</body></html>");
Response.setContentLength(caw.toString().length());
Out.writer(caw.toString());
Out.close();
Public class CharResponseWrapper extends
 HttpServletResponseWrapper{
 Private CharArrayWriter output;
 Public String toString(){
  Return output.toString();}
 Public CharResponseWrapper(HttpServletResponse response){
 Super(response);
 Output=new CharArrayWriter();
 }
 public PrintWriter getWriter()
 {
  return new PrintWriter(output);
 }
}
下图展示了进入Duke's Bookstore页面:
 
指定filter映射
 Web容器使用filter映射来决定如何将filter应用到网络资源。Filter映射通过命名将一个filter与网络组件匹配,或者通过URL映射到网络资源。在WAR中列出的filter映射,这些filters将被按顺序调用。你在部署工具中为WAR指定一个filter映射列表。
 下图显示了过滤器F1映射到servlets S1,S2,S3,filter F2映射到Servlet S2,filter F3映射到servletsS1,S2.
 
调用其它网络资源
 网络组件可以通过两种方法调用其它网络资源:间接的和直接的。当网络组件嵌入在内容返回到一个指向其它网络组件的客户URL时,网络组件间接的调用其它组件。在Duke's bookstore这个应用程序中,多数网络组件包含嵌入的志向其它网络组件的URL,例如,ShowCartServlet通过嵌入的URL--/bookstore1/catalog间接的调用CatalogServlet。
 当网络组件在值形式它也可以直接调用其它网络组件。有两种可能:它可以包含另一个网络资源,或者重定向到另一资源。
 要调用在服务器上运行网络组件的可用资源,你必须首先通过getRequestDispatcher方法获得RequestDispatcher对象。
 你可以通过请求或者Web context取得RequestDispatcher对象,这两个方法稍有不同。请求方法把请求资源的URL作为一个参数,请求可以使用相对路径。但Web context需要一个绝对路径。如果一个资源不可用,或服务器没有为那类资源实现RequestDispatcher对象,getRequestDispatcher方法将返回null.你的servlet应该处理这类情况。
响应中包含其它资源
 包含另一个网络资源通常非常有用。例如,在响应中从网络组件返回的横幅内容或版权信息,要包含其它资源,可以调用RequestDispatcher的include方法。
如果资源是静态的,include方法使能服务端编程。如果资源事网络组件,结果是该方法发送请求到被包含的网络资源,并执行网络组件机返回结果到该包含的servlet中。一个被包含的网络组件可以访问请求对象,但它在响应对象方面却受到一定限制:
 . 它可以写响应内容级提交一个响应。
 . 她不能设置头信息级调用任何影响头的方法。
 Duke's bookstore应用程序的横幅是由BannerServlet产生的。注意doGet,doPost方法都实现了,这是因为BannerServlet可以从调用的servlet中分派。
 Public class BannerServlet extends HttpServlet
 {
  public void doGet (HttpServletRequest request,HttpServletResponse response)
       throws ServletException,IOException
   {
    PrintWriter out=response.getWriter();
    Out.println("<body bgcolor=/"#ffffff/:">"+"<center>"+"<hr><br>&nbsp;"
+"<h1>"+"<font size=/"+3/" color=/"#3cc0066/">Bookstore</font>"+Duke's </font>"+<img src=/""+request.getContextPath()+"/duke.books.gif/">"+"<font size=/"+3/" color=/black/">Bookstore</font>"+"</h1>"+"</center>"+"<br>&nbsp;<hr><br>
  }
  public void doPost(httpServletRequest request,HttpServletResponse response)
      throws ServletException,IOException
  {
   PrintWriter out=response.getWriter();
   Out.println("<body bgcolor=/"#ffffff/:">"+"<center>"+"<hr><br>&nbsp;"
+"<h1>"+"<font size=/"+3/" color=/"#3cc0066/">Bookstore</font>"+Duke's </font>"+<img src=/""+request.getContextPath()+"/duke.books.gif/">"+"<font size=/"+3/" color=/black/">Bookstore</font>"+"</h1>"+"</center>"+"<br>&nbsp;<hr><br>
Duke's Bookstore中的每个servlet都包含有BannerServlet返回的结果,代码如下:
 RequestDispatcher dispatcher=getServletContext().getRequestDispatcher("/banner");
 If(dispatcher!=null)
  Dispatcher.include(request,response);
 }
转移控制到其它网络组件
 在有些程序中,你也需要一个网络组件事先处理一个请求让另外组件产生响应。例如,你也许想部分处理一个请求接着转送到其它组件。
 转送控制到另外的网络组件,你可以调用RequestDispatcher的forward方法,当一个请求转送时,请求URL送到请求页面。如果源URL要求处理,你可以把它作为请求属性保存它。
访问Web Context
 网络组件中执行的上下文是实现了ServletContext接口的对象。你可以通过getServletContext方法返回上下文对象,Web context提供了下列方法供访问:
 。初始参数
 。与Web context相关的资源
 。对象值属性
 。日志能力
 在Duke's Bookstores程序中web context被filter:filters.HitCounterFilter和OrderFilter使用。
Filters存储了一个作为上下文属性的计数器。通过控制并发访问共享资源来回调counter的访问方法,防止并发运行的servlet产生不符的操作。Filter通过context的getAttribute方法返回counter对象。计数器增加的值由context的log方法记录。
Public final class HitCounterFilter implements Filter
{
 private FilterConfig filterConfig=null;
 public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
   throws IOException,ServletException
 {
   ……
  StringWriter sw=new StringWriter();
  PrintWriter writer=new PrintWriter(sw);
  ServletContext context=filterConfig.getServletContext();
  Counter counter=(Counter)context.getAttribute("hitCounter");
  …….
  Writer.println("The number of hits is:"+counter.incCounter());
  …….
  Context.log(sw.getBuffer().toString());
  ……
 }
}

维持客户端状态
 很多应用程序需要一系列的来自客户端的相关的请求。例如,在Duke's Bookstore应用程序中保存用户的购物车中的状态。基于网络的程序要负责维护这样的称之为session的状态,因为HTTP协议是无状态的。为了支持应用程序维持状态,Java servlet技术为管理会话提供了一个API,并允许几种机制实现会话。
访问一个会话
 会话由HttpSession对象来代表。你可以通过调用request的getSession方法来获得一个会话。该方法返回与请求相关的当前会话对象,如果不存在则创建一个新的。由于getSession方法可能修改响应头信息(如果用cookies实现会话跟踪机制),它需要在返回Printwriter或ServletOutputStream之前调用。
将属性与会话绑定
 你可以将一个对象值属性通过命名与会话绑定。这些属性可以被任何同一网络上下文的,而且是同一会话的请求部分的网络组件访问。
 Duke's Bookstore应用程序通过会话来存储一个客户的购物车信息。这样可以在个请求之间保存购物车信息,并且servlet也可以访问购物车。CatalogServlet增加一个项目到购物车;ShowCartServlet显示,删除购物车中的内容,并清空购物车;CashierServlet返回购物车中所有的图书。
 public class CashierServlet extends HttpServlet
 {
  public void doGet (HttpServletRequest request,HttpServletResponse response)
      throws ServletException,IOException
  {
   //Get the user's session and shopping cart
   HttpSession session=request.getSession();
   ShoppingCart cart=(ShoppingCart)session.getAttribute("cart");
    ……..
   //Determine the total price of the user's books
   double total=cart.getTotal();
与会话相关联的通知对象
 重新调用你的应用程序中以通知网络上下文及会话监听对象的servlet生命周期事件。你也可以通知某一与会话相关的对象,例如下面的例子:
 。当该对象重会话中增加或删除。为接收这个通知,你的对象必须实现java.http.HttpSessionBindListener接口。
 。当对象绑定的会话将钝化或激活时。当一个会话在虚拟机之间转移时,或保存到持久存储器时,会话将相应的激活与钝化。为接收通知,你的对象必须实现该接口--java.http.HttpSessionActivationListener.
会话管理
 由于没有办法向HTTP Client示意不再需要会话,每一个会话都有一个相关联的超时设置,以让它的资源回收。超时设置可通过session的[get|set]MaxInactiveInterval方法实现。也可以通过部署工具来设置:
1、 选择相应的WAR;
2、 选择General标签。
3、 在Advanced box中输入超时时间
为确保一个处于活动状态的会话不超时,你需通过service方法周期性的访问会话,因为这样重置了时间计数器。
当特定的客户结束交互时,你使用会话的invalidate方法让会话失效,并删掉所有相关数据。
会话跟踪
 网络容器可以使用一些方法将一个用户与会话相关联,所有的都在服务端与客户端传递一个标志,该标志可以在客户端通过cookie或URL中包含该标志的网络组件来返回到客户端。
 如果你的应用程序使用了会话对象,任何时候你必须重写URL以确保会话可以跟踪,哪怕客户停止使用cookie。你可以通过对servlet返回的所有URL调用response的encodeURL(URL)方法。只有当cookie失效时该方法在URL中包含会话ID,否则它返回URL,并不改变它。
下面是ShowCartServlet的doGet方法对购物车下的显示页码的三个URL进行编码:
 out.println("<p>&nbsp;<p><strong><a href=/""+response.encodeURL(request.getContextPath()
+"/catalog")+"/">"+messages.getString("ContinueShopping")+"</a>&nbsp;&nbsp;"+"<a href=/""
+response.encodeURL(request.getContextPath()+"/cashier")+"/"+message.getString("Checkout")
+"</a>&nbsp;&nbsp;"+"<a href=/""+response.encodeURL(request.getContextPath()+
"/showCart?Clear=clear")+"/">"+messages.getString("ClearCart")+"</a></strong>");
如果cookie关闭了,会话被编码如下:
http://localhost:8080/bookstore1/cashier;
 jsessionid=c0o7fszeb1
如果cookie没有关闭,URL如下:
http://localhost:8080/bookstore1/cashier
结束servlet
 当servlet容器决定一个servlet需从service中删除时,容器调用servlet的destroy方法来实现。在这个方法中,可以释放servlet中使用的任何资源,及保存持久状态。下面的destroy方法释放了在init方法中创建的数据库对象:
 public void destroy()
 {
  bookDB=null;
 }
 当一个servlet移出时,servlet的所有的service方法都应当结束。只有当所有service的请求都返回或过了服务器指定的宽限期,服务器调用destroy方法以确保完全结束。
 如果servlet潜在的长时间运行服务请求,使用下面的技术来实现:
 。明了当前有多少线程在运行service方法。
 。提供一个彻底的关闭方法destroy以通知长时间运行的关闭线程并等待它们结束。
 。让长时间运行的方法周期性的验证关闭,如果必要,停止服务,接着返回。
跟踪服务请求
 跟踪服务请求,包括servlet的一个变量来计运行着的服务方法的次数。改变两需要一致访问方法以增加,减少,然后返回其值。
 Public class ShutdownExample extends HttpServlet
 {
  private int serviceCounter=0;
  ……
  //Access methods for serviceCounter
  protected synchronize void enteringServiceMethod()
  {
   serviceCounter++;
  }
  ptotected synchronized void leavingServiceMethod()
  {
   serviceCounter--;
  }
  protected synchronized void leavingServiceMethod()
  {
   return serviceCounter;
  }
 }
 当服务方法每进入一次,service应增加服务次数,当方法返回时就减少一次。
通知方法Shut Down
 为确保关闭,destroy方法需在所有的请求都结束时释放所有的资源。其中一部分就是检查服务次数,另一部分就是通知长时间运行的方法停止。为了实现通知,需要另外一个变量,该变量有通常的访问方法:
public class ShutdownExample extends HttpServlet
{
 private boolean shuttingDown;
 ……
 //Access methods for ahuttingDown
 protected synchronized void setShuttingDown(boolean flag)
 {
  shuttingDown=flag;
 }
 protected synchronized boolean isShuttingDown()
 {
  return shuttingDown;
 }
}

一个destroy方法使用变量来提供彻底关闭的例子:
public void destroy()
{
 /*check to see whether there are still service methods*/
 /*running ,and if there are ,tell them to stop.*/
 if(numServices()>0)
 {
  setShuttingDown(true);
 }
 /*wait for the service methods to stop.*/
 while (numServices()>0)
 {
  try
  {
   Thread.sleep(interval);
  }catch(InterruptedException e){}
 }
}
创建长时间运行的方法
 为实现彻底关闭的最后一步就是让任何长时间运行的方法温柔的运行。可能长时间运行的方法应该检查通知关闭的变量的值,如果有必要可以中断工作。
Public void doPost()
{
 ……
 for(I=0;((I<lotsOfStuffToDo)&&lisShuttingDown());I++)
 {
  try
  {
   partOfLongRunningOperation(i);
  }catch(InterruptedException e){}
 }
}

 

 

 

 

 

 

 

 

 

 

 
第11章 JSP技术
JSP技术可以让你轻松的创建静态及动态的网络内容。JSP技术设计了所有Java Servlet技术的动态能力而且还提供了更自然的途径创建静态内容,JSP主要特点如下:
。有专门的语言开发jsp页面,这些都是基于文本的来描述如何处理请求及产生响应。
。为访问服务端对象进行构造。
。有定义扩展语言的机制。
JSP技术也包含网络容器的应用程序接口(API),这些API供开发人员使用,这章节不讨论API。
什么是JSP页面
 JSP是基于文本的文件,它包含两种类型的文本:静态模板数据--任何基于文本格式的都可以被表示,例如HTML,SVG,WML,和XML;动态内容由JSP元素构成。
下面是一个表单页面,让你选择位置,并显示当地的时间:
 
 下面的例子包含了以下构成元素:
 。指令(<%@ page…%>)引用包含的类
 。jsp:useBean 元素创建一个包含对象集的场所,并初始一个变量指向该对象。
 。Scriptlets(<%…%>)或的本地的请求参数值,重申本地名称集,有条件的插入HTML文本到输出。
 。表达式(<%=….%>)插入本地值到输出中。
 。jsp:include 元素发送一个请求到另一个页面,也包含调用页面中响应的响应。
<%@ page import="java.util.*,MyLocales" %>
<%@ page contentType="text/html;charset=ISO8859_1" %>
<html>
<head><title>Localized Dates</title></head>
<body bgcolor="white">
<jsp:userBean id="locales" scope="application" class="MyLocales"/>
<form name="localeForm" action="index.jsp" method="post">
<b>Locale:</b>
<select name=locale>
<%
 String selectedLocale=request.getParameter("locale");
 Iterator I=locales.getLocaleNames().iterator();
 While(i.hasNext())
 {
  String locale=(String)i.next();
  If(selectedLocal.equals(locale))
  {
%>
   <option selected><%=locale%></option>
<%
  }else
  {
%>
   <option><%=locale%></option>
<%
  }
 }
%>
</selected>
<input type="submit" name="submit" value="Get Date">
</form>
<jsp:include page="date.jsp"/>
</body>
</html>
JSP页面的例子
 为了展示JSP技术,这章重写了Duke's Bookstore里的每个servlet,把这些servlet改写为JSP。下表列出了各功能所对应的JSP页面:
Function JSP Page
进入书店 Bookstore.jsp
创建书店横幅 Banner.jsp
浏览供销售的图书 Catalog.jsp
将一本书放到购物车中 Catalog.jsp and bookdetails.jsp
过的特定的书的详细信息 Bookdetails.jsp
显示购物车的内容 Showcart.jsp
删除购物车中的书籍 showcashier.jsp
购买购物车中的书籍 Cashier.jsp
收到确认信息 Recipt.jsp
书店应用程序中的数据保存在数据库中。然而,数据库的帮助类database.BookDB有两处改变:
1、 数据库帮助类可以重写以确定JavaBean的设计模式。这样,jsp页面可以通过jsp元素来访问特定的JavaBean组件。
2、 帮助对象可以通过enterprise bean来访问数据库,从而代替直接访问数据库。使用企业Bean的好处在于帮助类不再负责连接数据库;这项工作由企业Bean代替。而且,因为EJB容器负责维护数据库连接池,企业Bean获得联接的速度要比帮助类的要快。数据库帮助类的实现如下,该bean有两个变量:当前的图书及一个企业Bean的引用。
Public class BookDB
{
  private String bookId="0";
  private BookDBEJB database=null;
 
  public BookDB() throws Exception
  { }
  public void setBookId(String bookId)
  {
   this.bookId=bookId;
  }
  public void setDatabase(bookDBEJB database)
  {
   this.database=database;
  }
  public BookDetails getBookDetails() throws Exception
  {
   try
   {
    return (BookDetails)database.getBookDetails(bookId);
   }catch (BookNotFoundException ex){throw ex;}
  }
 ……
}
你可以按照下面的步骤来编译、部署,运行该程序。
1、 找到j2eetutorial/examples,通过运行ant并编译它;
2、 启动j2ee服务器
3、 启动部署工具;
4、 通过运行cloudscape -start来启动Cloudscape数据库;
5、 如果你还没有创建书店数据库,运行ant create web-db;
6、 创建J2EE应用程序并命名为Bookstore2App
a. 选择File'New'Application,
b. 在文件选择中,找到j2eetutorial/examples/src/web/bookstore2,
c. 在文件名框中输入Bookstore2App,
d. 单击New Application
e. 单击OK
7、 将Bookstore2WAR加入到Bookstore2App应用程序中。
a. 选择File'Add'Web WAR
b. 在Add Web WAR对话框中,找到j2eetutorial/examples/build/web/bookstore2,选择bookstore2.war。单击Add Web WAR
8、 把BookDBEJB企业Bean加入到应用程序中
a. 选择File'New Enterprise Bean;
b. 从Create New JARFile In Application 复合框中选择Bookstore2App;
c. 在显示名称的框中输入BookDBJAR;
d. 单击Edit来增加内容文件;
e. 在编辑框中,导航到j2eetutorial/examples/build/web/ejb/目录,并增加database和exception包,单击Next.
f. 为Enterprise Bean选择Session和Stateless状态。
g. 为Enterprise Bean选择database.BookDBEJBImp1
h. 在远程接口框中,为远程主接口(Remote Home)选择database.BookDBEJBHome,为database.BookDBEJB选择Remote Interface.
i. 为Enterprise Bean的名称输入BookDBEJB.
j. 单击Next,接着单击Finish.
9、 增加一个数据库资源引用到BookDBEJB
a. 选择Enterprise Bean BookDBEJB
b. 选择资源引用标签
c. 单击Add
d. 从类型栏中选择javax.sql.DataSource
e. 在jndi名称中输入jdbc/BookDB
10、 保存BookDBEJB
a. 选择BookDBJAR
b. 选择File'Save as
c. 导航到目录examples/build/web/ejb
d. 在文件名框中输入bookDB.jar
e. 单击Save EJB JAR As
11、 增加引用到Enterprise Bean BookDBEJB
a. 选择Bookstore2WAR
b. 选择EJB引用标签
c. 单击增加
d. 在编码命名框中输入ejb/BookDBEJB
e. 选择类型框中选择Session类型
f. 选择远程接口
g. 在主接口列中输入database.BookDBEJBHome
h. 在Local/Remote接口列中输入database.BookDBEJB
12、 指定JNDI名称
a. 选择Bookstore2App
b. 在Application表中,找到EJB组件并在JNDI名框中输入BookDBEJB
c. 在引用表中,找到EJB Ref并在JNDI名框中输入BookDBEJB
d. 在引用表中找到资源组件,并在JNDI名框中输入jdbc/Cloudscape.
13、 输入上下文根目录
a. 选择标签Web Context
b. 输入bookstore2
14、 部署应用程序
a. 选择Tools'Deploy
b. 单击finish
15、 打开书店URL http://<host>:8000/bookstore2/enter.

JSP页面的生命周期
 一个JSP页面的服务请求是作为servlet来执行的。这样,JSP页面的生命周期及各种能力就取决于servlet技术。
 当一个请求映射到一个Jsp页面时,由一个特殊的servlet来处理,该servlet首先检查一下对应的JSP页面有没有改动,如果有变动则将该JSP页面转换为servlet类并编译这个类,在开发过程中,JSP的优点在于执行过程都是自动运行的。
转换与编译
 在转换期间,JSP页面的每种数据类型都区别对待,模板数据都转换为代码。JSP元素分为以下几类:
 。 指令用来控制网络容器是如何解释并执行JSP页面的。
 。 脚本元素是插入到Jsp页面中的servlet类。
 。 形如<jsp:XXX/>的元素用来调用JavaBean组件或调用Java Servlet API。
 在JSP页面第一次获得请求时,解释与编译期间都可能产生错误。当错误发生在页面被解释时,服务器将返回ParseException,servlet类源文件将唯恐或不完全。最后不完全的行将产生一个指针指向错误JSP元素。
 如果错误发生在JSP页面被编译期间(例如,有个语法错误发生在脚本中),服务器将返回JasperException,并给出出错点的JSP页面的servlet和行。
 一旦页面被解释并执行,JSP页面的servlet的生命周期大部分与servlet类似:
1、 如果JSP页面的servlet实例不存在,容器将:
a) 载入JSP的servlet class
b) 实例化一个servlet class
c) 通过调用jspInit 方法实例化servlet
2、 调用_jspService方法,传递请求及响应对象。
3、 如果容器需要移除JSP页面的servlet,就调用jspDestroy方法。
异常
 你可以控制各种JSP页面的页面指令使用的执行参数。这些指令适合于缓存输出及处理错误。其它指令将在这章中讲到。
缓存
 当一个JSP页面被执行时,写到响应对象中的输出自动缓存。你可以通过下面得也面指令来设置缓存大小:
 <%@ page buffer="none|xxxkb" %>
在任何数据返回到客户端之前,一个较大的缓存允许写更多的内容,这样为JSP页面提供了更多的时间来设置正确的状态嘛及头信息或重定向到另一网络资源。一个较小的缓存减少了服务器的内存使用,能让客户更快的接收数据。
处理错误
 当JSP页面执行时,任何数量的错误都有可能发生。为了指定表明,容器应在异常发生时定向到一个错误页面,在JSP页面的开头可以包含下面的指令:
 <%@ page errorPage="file_name"%>
初始化及结束一个JSP页面
 你可以自定义初始化过程以允许JSP页面读取持久数据,初始化资源,而且通过重载jspInit方法可以做任何其他一次性动作。你也可以通过jspDestroy方法释放资源。这些方法使用Jsp申明来定义。
书店例子页面initdestroy.jsp定义了jspInit方法来返回或创建一个企业bean,database.BookDBEJB用来访问书店数据库;initdestroy.jsp存储了一个bean的引用。
 Private BookDBEJB bookDBEJB;
 Public void  jspInit()
 {
  bookDBEJB=(BookDB)getServletContext().getAttribute("bookDBEJB");
  if(bookDBEJB==null)
  {
   try
   {
    InitialContext ic=new InitialContext();
    Object objRef=ic.lookup("java:comp/env/ejb/BookDBEJB");
    BookDBEJBHome home=(BookDBEJBHome)PortableRemoteObject.narrow(objRef,database.BookDBEJBHome.class);
    BookDBEJB=home.create();
    GetServletContext().setAttribute("bookDBEJB",bookDBEJB);
   }catch(RemoteException ex){System.out.println("Couldn't create database bean."+ex.getMesage());
   }catch(CreateException ex){System.out.println("Couldn't create database bean.");
   }catch(NamingException ex){System.out.println("Unable to lookup home.");}
  }
}
当jsp页面从服务方法中移出时,jspDestroy方法释放变量BookDBEJB.
 Public void jspDestroy()
 {
  bookDBEJB=null;
 }
 由于企业Bean在各jsp页面间共享,当一个程序运行时,应将企业bean初始化。Java servlet技术提供了应用程序生命周期事件及监听类。作为练习,你可以将负责生成EJB的代码移动到context listener类。
生成静态内容
 你可以在jsp页面中生成静态内容,静态的内容可以通过基于文本的格式如HTML,WML及XML来表示。缺省方式为HTML,如果你想用除HTML外的格式,可以通过包含page指令的属性contentType来设置。例如,你想一个页面包含无线标记语言(WML),可以这样:
 <%@ page contentType="text/vnd.wap.wml"%>
生成动态内容
 你可以通过访问脚本元素中的Java programming language对象来生成动态内容。
在JSP中使用对象
 你可以在JSP页面中访问一个对象变量,包括企业bean及JavaBean组件。JSP技术自动产生一些对象变量,也可以生成访问应用程序特定的对象。
隐式对象
 隐式对象由网络容器生成,它包含一些特定请求,页面,应用的相关信息。许多对象通过Java Servlet技术定义。下表列出了隐式对象:
变量 类 描述
Application Javax.servlet.ServletContext 该上下文是为在同一个应用中的Jsp servlet及网络组件
Config Javax.servlet.ServletConfig 为Jsp页面初始化信息
Exception Java.lang.Throwable 来自error页的可以访问
Out Javax.servlet.jsp.JspWriter 输出流
Page Java.lang.Object Jsp页面处理当前请求的实例,页面所有者一般不使用
PageContext Javax.servlet.jsp.PageContext Jsp页面的上下文。
Request Javax.servlet.ServletRequest 触发执行Jsp页面的请求。
Response Javax.servlet.ServletResponse 返回到客户端的响应
Session Javax.servlet.http.HttpSession 客户端的会话对象
特定应用程序对象
 只要可能,应用对象的行为应封装到对象中,好让页面设计人员能集中精力在表现问题上。对像可以由精于程序设计的人员来实现访问数据库及其他服务。有四种方法可以在JSP页面中创建使用对象:
 。 在声明中创建的JSP页面的servlet类的实例及类变量,并在scriptlets和expresssion中访问;
 。 JSP页面的servlet类本地变量的创建,在scriptlets及expressions中使用;
 。 scope对象的属性在scriptlets及expressions中创建并使用;
 。 通过JSP元素使用JavaBean组件。也可以在声明中及scriptlet中创建JavaBean组件并调用JavaBean组件的方法
共享对象
 作为一个多线程的servlets,条件影响并发访问共享对象。可以通过下面的方法页面指令来让网络容器分派多客户请求:
 <%@ page isThreadSafe="true|false"%>
当isThreadSafe设置为true时,网络容器会分派多并发客户请求到JSP页面。这是缺省设置。如果使用true,你必须确保正确地同步地访问在页面及定义的共享对象,page scope的JavaBean组件,以及page scope对象的属性。
 如果isThreadSafe设置为false,将按照接收顺序,一次只分派一次请求,访问页面级对象不需要控制,然而,你仍然要确保对对象具有application scope及session scope属性的及JavaBean组件具有application scope及session scope属性的访问要正确地并发运行。
JSP scripting 元素
 JSP scripting 元素用来创建并访问对象,定义方法,及管理控制流。 由于JSP技术的一个目标就是尽量将静态的模板数据统动态的内容相分离,所以推荐少使用JSP scripting。许多必需的工作可以使用自定义的标签库来减少使用scripts。
 JSP技术允许容器支持任何可以调用Java对象的脚本语言。如果你想要使用除了缺省外的脚本语言,你必须在page指令中指定:
 <%@ page language="scripting language"%>
由于脚本元素都已在JSP page's servlet类中转换成编程语言声明,所以你必须在JSP页面中包含要用到的类及包。如果页面语言为java,使用page指令包含一个类及包:
 <%@ page import="packagename.*,fully_qualified_classname"%>
例如,在bookstore例子showcart.jsp:
 <%@ page import="java.util.*,cart.*" %>
声明
 JSP声明是用来声明在页面脚本中使用的变量及方法,声明的语法如下:
 <%! Scripting language declaration %>
当脚本语言是Java时,JSP页面中声明部分的变量及方法成为JSP页面servlet类的声明部分。
在书店例子中的initdestroy.jsp页面定义了一个实例变量--bookDBEJB及两个方法jspInit和jspDestroy:
 <%!
  Private BookDBEJB bookDBEJB;
  Public void jspInit()
  {
   ……
  }
  public void jspDestroy()
  {
   ……
  }
 %>
脚本(scriptlets)
 JSP脚本使用来包含代码片断的,语法如下:
<%
 scripting language statements
%>
当脚本语言设置为java时,脚本就转换成为java语句片断并插入到JSP servlet中的service方法中。在scriptlet中声明的变量在jsp页面中都可以访问。
JSP页面showcart.jsp包含了一个从collection返回迭代器的scriptlet。在循环中,JSP页面从book对象中取出其属性,并用HTML格式化输出。例子如下:
<%
 Iterator I=cart.getItems().iterator();
 While(i.hasNext())
 {
  ShoppingCartItem  item=(ShoppingCartItem)i.next();
  BookDetails bd=(BookDetails)item.getItem();
%>
 <tr>
  <td align="right" bgcolor="#ffffff">
  <%=item.getQuantity()%>
  </td>
  <td bgcolor="#ffffaa">
  <strong><a href="<%=request.getContextPath()%>/bookdetails?bookId=<%=bd.getBookId()%>">
<%=bd.getTitle()%></a></strong>
  </td>
 ……..
<%
 <tr>
  <td align="right" bgcolor="#ffffff">
  <%=item.getQuantity()%>
  </td>
  <td bgcolor="#ffffaa">
  <strong><a href="<%=request.getContextPath()%>/bookdetails?bookId=<%=bd.getBookId()%>">
<%=bd.getTitle()%></a></strong>
  </td>
  …
<%
 }
%>
输出结果见下图:
 
表达式
 JSP表达式使用来插入脚本语言表达式的值,并转换为字符串返回到客户端。当脚本语言为java时,表达式转换为一个将表达式的纸转换为字符串对象的语句并插入到隐含对象out。
语法如下:
 <%= scripting language expression %>
注意分号不能出现在JSP表达式中,即使是你在scriptlet中使用相同的表达式之间加分号。
下面的例子在脚本中返回了购物车中项的数量:
<%
 int num=cart.getNumberOfItems();
 if(num>0){  %>
表达式接着将num的值插入到输出流中:
<font size="+2">
 <%=messages.getString("CartContents")%><%=num%>
 <%=(num==1?<%=messages.getString("CartItem")%>;
 <%=messages.getString("CartItems"))%></font>
在JSP页面中包含内容
 在JSP页面中包含其他组件有两种机制:include指令和jsp:include元素。
Include指令是当JSP页面编译为servlet类时来处理的。效果就是插入另一文件中的文本--静态的内容或其他JSP页面到JSP页面中。
也许使用include指令来包含页头内容,版权信息,或其他需要在另一页面中重用的大块内容。
语法如下:
<%@ include file="filename: %>
因为要静态的将包含指令放到每个重用资源引用的文件中,这种方法有它的局限性。为了更灵活的建立页面内容而除去大量的快,可以使用标签库。
Jsp:include元素是在当JSP页面执行时来处理的。该动作允许在JSP页面中包含静态及动态的资源。包含静态与动态的内容的结果是不同的。如果资源是静态的,内容直接插入到调用JSP文件。如果为动态的,则请求发送到被包含的资源,被包含的页面被执行,结果在调用页面中反映出来。语法如下:
<jsp:include page="includepage" />
转换控制到另一网络组件
 转换控制到另一网络组件的机制使用了Java Servlet API提供的功能。可以通过jsp:forward元素来实现该功能:
<jsp:forward page="main.jsp"/>
注意:任何数据如果已经返回到客户端,jsp:forward元素将会抛出异常IllegalStateException。
参数元素
 当include或forward元素被调用时,最初的请求对象提供给目标页面。如果要提供额外的数据到目标页面,可以通过jsp:param元素来增加参数到请求对象。
<jsp:include page="…">
 <jsp:param name="param1" value="value1"/>
</jsp:include>
包含Java小程序(applet)
 可以在jsp页面中通过使用jsp:plugin元素来加入applet及JavaBean组件。该元素产生包含适当的依赖于客户浏览器的结构的HTML。它下载Java插件并顺序执行客户端组件。该元素语法如下:
 <jsp:plugin type="bean|applet" code="objectCode" codebase="objectCodebase"
  { align="alignment"}  {archive="archiveList"} { height="height"} { hspace="hspace"}
  {jreversion="jreversion"} {name="componentName"}{vspace="vspace:"}
  {width="width"} {nspluginurl="url"} {iepluginur="url"}>
  {<jsp:params>
   {<jsp:param name="paramName" value="paramValue"/>}+
  {</jsp:params>}
  {<jsp:fallback>arbitrary_text</jsp:fallback>}
 </jsp:plugin> 
jsp:plugin标签被客户的请求转换为<object>或<embed>标签。Jsp:plugin的属性为表现层提供配置数据,如同插件需要的版本一样。属性nspluginrul和iepluginurl指定了下载插件的URL。
 Jsp:param元素指定了传递到applet或JavaBean的参数。Jsp:fallback元素指明了在客户浏览器不支持插件的情况下使用的内容。
 如果插件可以运行但applet或JavaBean不能,一个特定的消息将发送到用户,就像弹出窗口报告ClassNotFoundException.
 在书店应用中有一页面banner.jsp创建了一个横幅用来显示由DigitalClock产生的动态数字钟。如下图:
 
Jsp:plugin元素用来下载下面的applet:
<jsp:plugin type="applet" code="DigitalClock.class"  codebase="/bookstore2" jreversion="1.3"
 align="center" height="25" width="300"
nspluginurl="http://java.sun.com/products/plugin/1.3.0_01/plugin-install.html"
iepluginurl="http://java.sun.com/products/plugin/1.3.0_01/jinstall-130_01-win32.cab#Version=1.3.0.1">
<jsp:params>
 <jsp:param name="language" value="<%=request.getLocale().getCountry()%>"/>
 <jsp:param name="country" value="<%=request.getLocale().getCountry()%>" />    
 <jsp:param name="bgcolor" value="FFFFFF" />
 <jsp:param name="fgcolor" value="CC0066" />
 </jsp:params>
  <jsp:fallback>
  <p>Unable to start plugin.</p>
  </jsp:fallback>
</jsp:plugin>
扩展JSP语言
 你可以通过与脚本衔接的JavaBean组件执行广泛的多样的动态处理任务,包括访问数据库,使用企业服务例如电子邮件,目录,管理流控制等。然而使用脚本的缺陷是难于维护页面。在这种情况下JSP技术提供了自定义标签技术,它可以让你通过扩喊来访问封装动态功能的对象。自定义标签给JSP页面提供了另一组件级技术。
例子, 重调用来循环显示购物车内容的脚本如下:
<%
 Iterator I=cart.getItems().iterator();
 While(i.hasNext())
 {
  ShoppingCartItem item=(ShoppingCartItem)i.next();
  ……
%>
<tr>
 <td align="center" bgcolor="#ffffff" >
 <%=item.getQuantity()%>
 </td>
 ……
<%
 }
%>
 迭代自定义标签取出了代码逻辑并管理脚本中的变量item:
<logic:iterate id="item"
  collection="<%=cart.getItems()%>">
  <tr>
  <td align="right" bgcolor="#ffffff"><%=item.getQuantity()%>
  </td>
  ……
</logic:iterate>
自定义标签被打包并发布在称之为标签库的单元里。自定义标签库的语法如同使用JSP元素,类似<prefix:tag>,然而,对于自定义标签,prefix为被用户定义标签库。
 
第12章 JSP页面中的JavaBean组件

JavaBean组件是可以轻松重用并集成到应用程序中的java类。任何继承了特定习惯的java类都可以为JavaBean组件。
 JSP技术直接支持JavaBean组件,你可以轻松的创建并初始化bean并set/get属性值。本章提供了JavaBean组件的基本信息及在JSP页面下访问JavaBean组件。
JavaBean组件设计约定
 JavaBean组件设计约定管理类属性及管理访问属性的公共方法。
 一个JavaBean组件属性可以:
 。read/write,read-only,或write-only
 。简单的只含一个值,带索引的代表一个数组值。
没有必要通过实例变量实现属性;属性可以通过使用公共方法来访问:
 。对于每一个可读的属性,该bean必须有一个方法的形式如PropertyClass getProperty(){…}
 。对于可写的属性,bean唏嘘由一个方法形如:setProperty(PropertyClass pc){…}
 除了属性方法,JavaBean组件必须定义一个不带参数的构造器。
 在书店应用程序中的JSP页面enter.jsp,bookdetails.jsp,catalog.jsp,showcart.jsp使用database.BookDB,database.BookDetails JavaBean组件.BookDB提供了一个Bean组件,和一个企业Bean,这两个Bean都扩展使用了面向bean的自定义标签。JSP页面showcart.jsp,cashier.jsp使用了cart.ShoppingCart莱显示顾客的购物车。
 JSP页面catalog.jsp,showcart.jsp及cashier.jsp使用了util.Currency JavaBean组件通过本地敏感的方式格式化货币。该bean由两个可写的属性--locale和amount,及一个只读属性--format。该属性不与任何实例对象对应,但返回变量locale及amount属性的功能。
Public class Currency
{
 private Locale locale;
 private double amount;
 public Currency()
 {
  locale=null;
  amount=0.0;
 }
 public void setLocale(Locale l)
 {
  locale=l;
 }
 public void setAmount(double a)
 {
  amount=a;
 }
 public Stirng getFormat()
 {
  NumberFormat nf=NumberFormat.getCurrencyInstance(locale);
  Return nf.format(amount);
 }
}
为什么要使用JavaBean组件  
 JSP页面可以在声明或脚本中创建使用任何类型的Java编程语言对象。下面的脚本创建了书店购物及保存的一个会话属性:
<%
 ShoppingCart cart={ShoppingCart}session.getAttribute("cart");
 If(cart==null)
 {
  Cart=new ShoppingCart();
  Session.setAttribute("cart",cart);
}
%>
 如果购物车对象符合JavaBean约定,JSP页面可以使用JSP元素来创建并访问对象。例如,在Duke's Bookstore页面中有bookdetails.jsp,catalog.jsp及showcart.jsp替代脚本:
 <jsp:useBean id="cart" class="cart.ShoppingCart" scope="session"/>

创建并使用JavaBean组件
 通过下面的格式可以在JSP中声明使用JavaBean组件:
 <jsp:useBean id="beanName" class="fully_qualified_classname" scope="scope"/>
或者
 <jsp:useBean id="beanName" class="fully_qualified_classname" scope="scope">
 <jsp:setProperty …/>
 </jsp:useBean>
第二种形式是当你想初始化bean的属性时使用的。
Jsp:usebean元素标明在该页面中将使用一个存储在特定域内的可以访问的bean,它的域可以是application,session,request,page。如果没有这样的bean存在,该语句创建该bean,并把它作为域对象属性保存。属性id的值决定了bean在域中的的名字,identifier用来在其它JSP元素及脚本中引用bean。
 下面的元素创建了一个Currency的实例,如果不存在则把它作为session对象属性存储起来,使得bean在整个会话期间可用。
<jsp:useBean id="currency" class="util.Currency" scope="session"/>
设置JavaBean组件属性
 在JSP页面中有两种方法设置JavaBean组件属性:使用jsp:setProperty元素或脚本。
 <% beanName.setPropName(value);%>
jsp:setProperty元素的语法取决于属性值。下表总结了各种方法来设置JavaBean的属性:
Value Source Element Syntax
String <jsp:setProperty name="beanName" property="propName'" value="string constant"/>
Request parameter <jsp:setProperty name="beanName" property="propName"
paramName"/>
Request parameter name matches bean property <jsp:setProperty name="beanName" property="propName"/>
<jsp:setProperty name="beanName" property="*"/>
Expression <jsp:setProperty name="beanName" property="propName"
value="<%=expression%>"/>
1、 beanName must be the same as that specified for the id attribute in a useBean element.
2、 There must be a setPropName method in the JavaBean component
3、 ParamName must be a request parameter name.
你可以使用运行时表达式来设置属性类型为复合型的值。重新调用表达式--该表达式用来插入脚本语言表达式的值,转换成String,在到输出流返回到客户端。在setProperty元素中使用时,表达式仅仅返回其值;不会自动的转换。其结果,返回的类型必须转换成属性匹配的类型。
 在Duke's Bookstore应用中展示了如何使用setProperty元素及脚本来为数据库帮助类设置当前的书,如下例:
 <jsp:setProperty name="bookDB" property="bookId"/>
然而,bookstore2/bookdetails.jsp使用下面的形式:
<% bookDB.setBookId(bookId);%>
 下面的代码片断来自bookstore3.jsp一显示如何初始化currency bean由于第一次初始化是嵌套在jsp:useBean元素中,只有当bean创建后才执行。
 <jsp:useBean id="currency" class="util..Currency" scope="session">
  <jsp:setProperty name="currency" property="locale" value="<%=request.getLocale()%>"/>
 </jsp:useBean>
 <jsp:setProperty name="currency" property="amount" value="<%=cart.getTotal()%>"/>
返回JavaBean组件属性
 有一些方法可以返回JavaBean组件属性,两种方法(jsp:setProperty元素及表达式)转换属性的值为String并插入到当前的隐含对象out:
 。 <jsp:getProperty name="beanName" property="propName"/>
 。 <%=beanName.getPropName() %>
 对于这两个方法,beanName必须与在useBean元素中的属性id指定的一样,而且必须有方法getPropName在JavaBean组件中。
如果返回属性只是不必转换类型则必须使用脚本:
<% Object o =beanName.getPropName();%>
下图总结了各类对象的存储,以及那些对象如何通过JSP页面访问:
 
 
第13章 在JSP页面中自定义标签
 标准JSP标签是用来调用JavaBean组件的操作,处理定向请求以简化JSP页面开发与维护。JSP技术提供了一种封装其它动态类型的机制--自定义标签,它扩展了JSP语言。自定义标签通常发布在标签库中,该库定义了一个自定义标签集并包含实现标签的对象。
 一些功能可以通过自定义标签来实现,包括对隐含对象的操作,处理表单,访问数据库集其它企业级服务,如e-mail,目录服务,处理流控制。JSP标签库由精通Java语言的开发者及精于访问数据机器它服务的专家来创建,由网络应用设计者来使用,以集中精力来解决表现而不是如何访问企业服务。业就是鼓励分开库的开发者与使用者相分离,自定义标签通过封装实现了生产率的提高。
 标签库在JSP技术中受到广泛注意。要更多的了解标签库可以访问以下网址:
http://java.sun.com/products/jsp/taglibraries.html
什么是自定义标签?
 自定义标签是用户定义的JSP语言元素。当JSP页面包含一个自定义标签时被转化为servlet,标签转化为对称为tag handler的对象的操作。接着当servlet执行时Web container调用那些操作。
 自定义标签有着丰富的特点,它们可以:
1、 可以通过调用页面传递的属性进行自定义;
2、 可以访问对于JSP页面可能的所有对象;
3、 可以修改由调用页面产生的响应。
4、 可以相互间通信。你可以创建并初始化一个JavaBean组件,创建一个变量引用标签中的bean,接着在其它的标签中引用该bean.
5、 可以在一个标签中嵌套另一个,可以在JSP页面中进行复杂的交互。
JSP页面的例子
 这章描述的任务包括使用及定义标签。
 标签库Struts为建立一个国际化的网络应用提供了一个实现设计模式为Model-View-Control的框架。该标签库包括全面的功能用于处理:
。 HTML forms
。 Templates
。 JavaBeans components
。 Logic processing
 Duke's Bookstore应用程序使用了这些标签:Struts bean和logic sublibraries.
 标签库tutorial-template定义了一系列标签集来创建应用模板。该模板是带有占位符的JSP页面用来在每个页面显示需要改变的部分。每一个占位符作为模板的一个parameter而引用。例如,一个简单的模板可以包含一个标题参数及体参数来引用JSP页面中部分自定义的内容。模板由一系列嵌套的标签。
下图显示了通过Duke'sBookstroe的网络组件发出请求的流程:
 
 Duke's Bookstore3应用的源代码位于j2eetutorial/examples/src/web/bookstore3目录下。要编译、部署并运行该例子,进行一下操作步骤:
1、 到目录j2eetutorial/examples下编译该应用程序。
2、 从http://jakarta.apache.org/builds/jakarta-struts/release/v1.0下载并解压Struts(版本为1.0),将struts-bean.tld,struts-logic.tld和struts.jar拷到jakarta-struts-1.0/lib to examples/build/web/bookstore3下面
3、 运行j2ee server;
4、 运行部署工具deploytool
5、 运行Cloudscape database;
6、 如果你还没有创建bookstore database,运行ant create web-db.
7、 创建J2EE应用程序--Bookstore3App.
a. 选择File-'New Application.
b. 在文件选择框中,找到j2eetutorial/examples/src/web/bookstore3.
c. 在文件名框中输入Bookstore3App;
d. 单击New Application.
e. OK.
8、 创建WAR文件并将DispatcherServlet 网络组建及所有的Duke's Bookstore加入到Bookstore3App.
a. 选择File-'New-'Web Component.
b. 单击创建New WAR文件,从复合框中选择Bookstore3App,输入Bookstore3WAR。
c. 单击Edit来增加文件。在编辑内容对话框中,导航到j2eetutorial/examples/build/web/bookstore3.选择Dispatcher.class并单击Add.增加banner.jsp,bookstore.jsp,bookdetails.jsp,catalog.jsp,showcart.jsp,cashier.jsp,receipt.jsp,initdestroy.jsp,template.jsp,screendefinitions.jsp,errorpage.jsp.增加duke.books.fig,struts-bean.tld,struts-logic.tld,tutorial-template.tld,struts.jar..增加cart,database,message,taglib,util packages.
d. 单击Next
e. 选择servlet单选按钮,
f. 单击Next,
g. 从Servlet类复选框中选择Dispatcher
h. 单击两次Next.
i. 在组件别名框中,单击增加接着输入别名。
j. 单击Finish.
9、 增加BookDBEJB企业Bean
a. 选择File-'Add-'EJB JAR.
b. 找到目录examples/build/web/ejb.
c. 选择bookDB.jar,
d. 单击Add EJBJAR.
10、 给BookDBEJB增加一个引用。
a. 选择Bookstore3WAR,
b. 选择EJB Refs标签,
c. 单击Add,
d. 在代码名称栏中输入ejb/BookDBEJB,
e. 在类型栏中输入Session.
f. 在接口列中选择Remote,
g. 在Home接口列中输入database.BookDBEJBHome,
h. 在Local/Remote接口栏中输入database.BookDBEJB.
11、 增加标签库URI映射
a. 选择File Refs标签,
b. 在JSP标签库德自框中单击Add按钮
c. 在引用栏中输入相对路径URI /tutorial-template,
d. 在标签栏中输入绝对路径/WEB-INF/tutorial-template.tld,
e. 对/struts-bean,struts-logic页执行上面的操作。
12、 指定JNDI名。
a. 选择Bookstore3App,
b. 在应用表中,找到EJB组件并在JNDI名称栏中输入BookDBEJB,
c. 在引用表中,定位到EJB Ref并在JNDI名称栏中输入BookDBEJB,
d. 在引用表中,定位到资源组件并在JNDI名称栏中输入jdbc/Cloudscape.
13、 输入context根
a. 选择Web Coontext 标签
b. 输入bookstore3.
14、 部署应用程序
a. 选择Tools-'Deploy,
b. 单击Finish.
15、 打开bookstore URL http://<host>:8000/bookstore3/enter.

使用标签
 这部分描述页面作者如何使用标签库指定JSP页面并介绍不同类型的标签。
声明标签
 你可以通过在页面中使用下面的指令来声明在JSP中使用标签库:
<%@ taglib uri="WEB-INF/tutorial-template.tld" prefix="tt" %>
 该uri属性引用了唯一识别的标签库描述符(TLD),该URI可以是直接的也可以是间接的。Prefix属性定义了区分其它标签的方法。
 标签库描述文件必须以后缀为.tld命名。TLD文件保存在WEB-INF目录中,你可以直接或间接的引用TLD。
 下面的标签库指令直接引用了TLD文件:
<%@ taglib uri="/WEB-INF/tutorial-template.tld" prefix="tt" %>
 下面的标签库指令使用了短的逻辑名来间接引用TLD:
<%@ taglib uri="/tutorial-template" prefix="tt" %>
 逻辑名必须映射到Webapplication部署描述符中的绝对路径。为映射到逻辑名/tutorial-template到绝对路径/WEB-INF/tutorial-template.tld:
1、 选择Bookstore3WAR,
2、 选择File Refs标签,
3、 在JSP标签库的子栏中单击Add按钮,
4、 在代码引用域中输入相对路径URI /tutorial-template.
5、 在标签库域中输入绝对路径/WEB-INF/tutorial-template.tld.
标签种类
 JSP自定义标签使用XML语法。它们有起始标签及结束标签,并有可能有体:
 <tt:tag> body </tt:tag>
 没有体的标签如下:
 <tt:tag />
简单标签
 一个简单标签没有体及属性:
 <tt:simple />
带有属性的标签
 自定义标签可以含有属性。属性列于起端标签,有这样的语法:attr="value".属性值用于自定义标签的行为,就像方法中定义的参数一样。
 你可以给属性设置一字符串常量货运形时表达式。这个转换过程介于常量与运行时表达式之间,且属性类型遵循为JavaBean组件属性描述的规则。
 Struts的属性 logic:present决定标签的体是否求值。在下面的例子中,一个属性制订了一个请求参数:Clear:
 <logic:present parameter="Clear">
 在Duke's Bookstore application页面catalog.jsp使用了运行表达式来设置属性值:
<logic:iterate collection="<%=bookDB.getBooks()%>" id="book" type="database.BookDetails">
带体的标签
 自定义标签可以包含自定义的核心标签,脚本元素,HTML文本,以及标签依赖的体内容。
 在下面的例子中,Duke's Bookstore application页面showcart.jsp使用了Struts logic:present 标签来清除购物撤兵打印消息如果请求中包含参数为Clear的话:
<logic:present parameter="Clear">
 <% cart.clear(); %>
 <font color="#ff0000" size="+2"><strong>
 You just cleared your shopping cart!</strong><br>&nbsp;<br></font>
</logic:present>
在传递信息时选择属性还是体
 正如在后两节中讲到的,把一个给定的数据作为标签的属性或者体来传递是可能的。通常,无论是字符串或可以计算的简单表达式最好作为属性来传递。
定义脚本变量的标签
 自定义标签可以定义在脚本中使用的变量。下面的例子展示了如何定义并使用包含从JNDIlookup返回的对象的脚本变量。这个例子包含企业bean,事务处理,数据库,环境入口等。
<tt:lookup id="tx" type="UserTransaction" name="java:comp/UserTransaction" />
<% tx.begin(); %>
 在Duke'sBookstore应用中,有些页面使用Struts的面向bean的标签。例如,bookdetails.jsp使用了bean:parameter标签来创建biikId变量并给它设置请求传递的参数。Jsp:setProperty语句也将bookDB的属性bookId设置为请求来的参数。标签bean:define返回属性bookDetails的值:
<bean:parameter id="bookId" name="bookId" />
<jsp:setProperty name="bookDB" property="bookId" />
<bean:define id="book" name="bookDB" property="bookDetails" type="database.BookDetails" />
<h2><jsp:getProperty name="book" property="title"></h2>
标签的相互协作
 顾客标签可以通过共享对象与其它标签相互协作。在下面的例子中,标签tag1创建了一个称为obj1的对象并重用tag2:
 <tt:tag1 attr1="obj1" value="value" />
 <tt:tag2 attr1="obj1" />
在下一个例子中,一组嵌套的封闭标签创建了一个对象,所有内部标签都可以使用。由于对象未命名,这样减少了潜在的名字冲突。该例子展示了嵌套标签如何在jsp页面中协调工作的。
<tt:outerTag>
 <tt:innerTag />
</tt:outerTag>

 

定义标签
 要定义一个标签,需要一下准备工作:
 。 为标签开发一个标签处理累计帮助类
 。 在标签库中声明标签描述符
标签处理器
 标签处理器是由网络容器调用的,用来处理运行的包含标签的JSP页面。标签处理器必须实现Tag或BodyTag接口。这些接口可以携带Java对象并产生一个标签处理器。对新创建的处理器,你可以使用TagSupport及BodyTagSupport类作为基类,这些类基接口在javax.servlet.jsp.tagext包中。
 有Tag及BodyTag接口定义的处理器方法由JSP页面的servlet任何时候调用。当遇到标签的起始处时,JSP页面的servlet调用方法来初始化处理器接着调用处理器的doStartTag方法,当遇到结束点时,处理器的方法doEndTag被调用。另外的一些方法在标签相互通信时使用。
 标签处理器有API接口来与jsp页面通信。其API入口点是page context(javax.servlet.jsp.PageContext)。通过API,处理器可以返回其它隐含对象(request,session,application).
 隐含对象有些属性,它们可以通过使用[set|get]方法来调用。
 如果标签嵌套,标签处理器也要访问相应的标签。
标签库描述符
 标签库描述符是XML格式的文档。TLD包含库的所有信息及库中的每个标签。TLD由Web容器使用来验证标签并由JSP页面开发工具来使用。
 TLD文件必须以扩展名.tld为后缀。这类文件保存在WEB-INFO目录中或它的子目录中,当你使用部署工具将TLD加到WAR中时,它会自动的加入到WEB-INFO中。
 TLD文件必须以下面的格式开头:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems,Inc.//DTD JSP Tag Library 1.2//EN" http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd>
 J2EE SDK1.3版可以识别1.1,1.2版的DTD。然而由于本章文档使用的是1.2版,所以必须使用更新的标签库。Struts库遵循1.1版的DTD,其中有些元素的名称有些改变。
 TLD的根元素是taglib,子元素列表如下:
Element Description
tlib-version The tag library's version
Jsp-version The JSP specification version that the tag library requires
Short-name Optional name that could be used by a JSP page authoring tool to create names with a numeric value
Uri A URI that uniquely identifies the tag library
Display-name Optional name intended to be displayed by tools
Small-icon Optional small icon that can be used by tools
Large-icon Optional large icon that can be used by tools
Description Optional tag-specific information
Listener See listener Element
Tag  See tag Element

元素listener
 标签库可以指定一些类--事件监听类。(参照处理servlet生命周期事件)。这些监听类都作为listener元素列于TLD中,网络容器将实例化监听类并通过与在WAR中定义的监听类类似的方法来注册。不像WAR级的监听类,,标签库中注册的监听类没有定义顺序。Listener元素的唯一子元素是listener-class,它必须包含监听类名的全称。
元素tag
 库中的每个标签都有一个给定的名称及该标签的处理器来描述,标签创建的脚本变量信息,标签属性信息。脚本变量信息可以直接在TLD中给出或通过标签的额外信息获取。每一属性都包含说明是否需要该属性,它的值是否可以通过请求时表达式来决定,及其属性类型。
下表列出了标签的子元素信息:
Element 描述
Name 标签唯一名称
Tag-class 标签处理器类名
Tei-class 可选子类 javax.servlet.jsp.tagext.TagExtraInfo
Body-content 内容类型
Display-name 可选的用来显示的名字
Small-icon 可由工具使用的可选的 small-icon
Large-icon 可由工具使用的可选的large-icon
Description 可选的描述信息
variable 可选的脚本变量信息
attribute 标签属性信息
下面的部分描述了需要为每种类型标签开发的方法几TLD元素。
简单标签
标签处理器
 简单标签处理器必须实现doStartTag及doEndTag两个标签接口的方法。方法doStartTag在开始遇到时被调用,并返回SKIP_BODY,因为标签没有体。方法doEndTag在标签结束时被调用,如果其它页面需要使用则返回EVAL_PAGE,否则返回SKIP_PAGE.
在第一部分提到的标签:<tt:simple />将由下面的标签处理器来实现:
public SimpleTag extends TagSupport {
 public int doStartTag() throws JspException {
  try{
   pageContext.getOut().print("hello.");
  }catch(Exception e) {throw new JspTagException("SimpleTag:"+e.getMessage());}
  return SKIP_BODY;
 }
 public int doEndTag() {
  return EVAL_PAGE;
 }
}
元素body-content
 没有体的标签必须用body-content元素来声明体内容为空:
<body-content>empty</body-contnent>
标签属性
 在标签处理器中定义属性
 对于每一个标签属性,你必须在标签处理器中定义一个属性及get/set方法来遵循JavaBean的结构。例如,下面的处理器处理Struts logic:present tag.
<logic :present parameter="Clear">
包含下面的声明及方法:
protected String parameter=null;
public String getParameter() {
 return (this.parameter);
}
public void setParameter(String parameter) {
 this.parameter=parameter;
}
注意:如果属性命名为id,标签处理器继承了TagSupport类,你不需要定义该属性及set/get方法,因为已经由TagSupport定义了。
 一个属性值类型为String的标签可以命名一隐含对象供标签处理器使用。隐含对象属性可以通过传递标签属性值到隐含对象的[get/set]Attribute方法来访问。这是一个好方法来传递脚本变量到标签处理器。
元素attribute
 对于每一个标签属性,你必须指定该属性是否必须,该标签的知是否可以通过表达式确定,属性类型在元素attribute中。对于静态类型数值通常为java.lang.String。如果元素rtexprvalue是true或者是yes,元素类型决定了返回值类型。
<attribute>
 <name>attr1</name>
 <required>true|false|yes|no</required>
 <rtexprvalue>true|false|yes|no</rtexprvalue>
 <type>fully_qualified_type</type>
<attribute>
 如果属性不必须,标签处理器应该指定一个默认值。
标签元素logic:present声明了一个属性parameter不必须,且它的值可以有一个表达式来赋值。
<tag>
 <name>present</name>
 <tag-class>org.apache.struts.taglib.logic.PresentTag</tag-class>
 <body-content>JSP</body-content>
 ……
 <attribute>
  <name>parameter</name>
  <required>false</required>
  <rtexprvalue>true</rtexprvalue>
 </attribute>
 ……
</tag>
属性验证
 标签库的文档应该为标签属性描述合法的值。当JSP页面被运行时,网络容器将加强在TLD元素中的每一个属性的制约。
 传递到标签的属性也可以在传递时间由一个继承于TagExtraInfo的类的方法isValid来验证。该类也用来提供标签定义的脚本变量的信息。
 方法isValid传递对象TagData的属性信息,该对象包含了标签每一属性的属性值。由于验证过程在传输期间进行,所以属性值在请求时开始计算并送到TagData.REQUEST_TIME_VALUE.
 标签<tt:twa attr1="value1" />有下面的TLD属性元素:
<attribute>
 <name>attr1</name>
 <required>true</required>
 <rtexprvalue>true</a>
</attribute>
 该声明显示attr1的止可以在运行时决定。下面的方法isValid检查attr1的值是合法的Boolean值。注意,由于attr1的治可以在运行时计算,所以isValid必须检查标签的使用者是否提供了运行时值。
Public class TwaTEI extends TagExtraInfo {
 Public boolean isValid(Tagdata data) {
  Object o=data.getAttribute("attr1");
  If(o!=null&& o!=TagData.REQUEST_TIME_VALUE) {
   o.toLowerCase().equals("true")||o.toLowerCase().equals("false"))
   return true;
  else
   return false;
 }
 else
  return true;
 }
}

标签体
标签处理器
 标签处理器在处理还标签体的标签时实现有所不同,主要取决于处理器是否需要与体交互。如果交互则标签处理器读取或修改体的内容。
标签处理器不与体交互
 如果处理器不与体交互,标签处理器需要实现Tag接口(或继承TagSupport)。如果标签的体需要计算,方法doStartTag需要返回EVAL_BODY_INCLUDE;否则,则返回SKIP_BODY.
 如果标签处理器需要重复计算体,则该标签需要实现接口IterationTag或者继承TagSupport。如果标签决定体需要再计算一次则从doStartTag和doAfterBody返回EVAL_BODY_AGAIN。
标签处理器与体交互
 如果标签处理器需要与体交互,标签处理器必须实现BodyTag,这样的处理器实现了方法doInitBody,doAfterBody。这些方法与传递到标签处理器的体内容交互。
 体内容支持一些方法来读写它的内容。标签处理器可以使用体内容的getString/getReader方法从体中获取信息,并通过writeOut方法来将体内容写到输出流。写提供writeOut方法,该方法可以通过使用标签处理器的方法getPreviousOut来获取。该方法确保标签处理器的结果对封闭的标签处理器可行。
 如果标签的体需要计算,方法doStartTag需要返回EVAL_BODY_BUFFERED,否则,返回SKIP_BODY。
 方法doInitBody
 该方法在体内容设置之前求值之后被调用。通常使用该方法初始化体内容。
 方法doAfterBody
 该方法在体内容求值之后被调用。像方法doStartTag一样,doAfterBody方法必须返回指示是否继续计算体。这样,如果体需要再计算一次,就像实现了迭代标签一样,doAfterBody方法应返回EVAL_BODY_BUFFERED,否则返回SKIP_BODY.
 方法release
 标签处理器应该重新设置它的状态并通过方法release释放任何私有资源。
 下面的例子从体中读取内容并传递到一个执行查询的对象。由于体不需要重复计算,所以doAfterBody返回SKIP_BODY。
 Public class QueryTag extend BodyTagSupport
 {
  public int doAfterBody() throws JspTagException
  {
   BodyContent bc =getBodyContent();
   String query=bc.getString();
   Bc.clearBody();
   Try
   {
    Statement stm=connection.createStatement();
    Result=stm.executeQuery(query);
  }catch(SQLException e){ throw new JspTagException("QueryTag:"+e.getMessage());}
 return SKIP_BODY;
 }
}
元素body-content
 对于有体的标签,必须用下面的方法指定体内容的类型:
 <body-content>JSP|tagdependent</body-content>
 体内容包含自定义及核心标签,脚本元素,HTML文本都统一为JSP。这就是为Struts描述的logic:present标签。其它类型体内容,例如,SQL 语句传递查询标签,将会标以tagdependent.
 注意,body-content元素的值不会影响体处理器的解释。
定义脚本变量的标签
标签处理器
 标签处理器通过页面上下文设置的脚本变量负责创建、设置一个对象引用。它通过使用pageContext.setAttribute(name,value,scope)或pageContext.getAttribute(name,value)方法来实现。典型的,属性传递到自定义标签指定的脚本变量对象名;该对象可以通过调用属性的get方法返回。
 如果脚本变量的值依赖于标签处理器上下文的一个对象。可以通过使用pageContext.getAttribute(name,scope)方法返回。
 一般过程是标签处理器返回脚本变量,对对象进行处理,接着使用pageContext.setAttribute(name,object)方法给变量赋值。
 对象受作用域总结如下表,作用于限制了对象的可访问性及对象的生命周期。
Name Accessible From Lifetime
Page Current page Until the response has been sent back to the user or the request is passed to a new page
Request Current page and any included or forwarded pages Until the response has been sent back to the user
Session Current request and any subsequent request from the same browser The file of the user's session
Application Current and any future request from the same Web application The life of the application
提供脚本变量的信息
 在标签定义脚本变量的例子中,定义了一个脚本变量book用来访问book信息。
<bean:define id="book" name="bookDB" property="bookDetails" type="database.BookDetails"/>
<font color="red" size="+2">
 <%=messages.getString("CartRemoved")%>
 <strong><jsp:getProperty name="book" property="title"/></strong>
 <br>&nbsp;<br>
</font>
当JSP页面包含该标签并转换时,网络容器产生代码以与脚本变量保持同不。要产生代码,网络容器需要脚本变量的一些信息:
 。变量名
 。变量类
 。该变量是否引用一个新的或存在的对象
 。该变量可访问
有两种方法可以提供这些信息:指定变量TLD子元素或定义标签的额外的信息类并在TLD中包含tei-class元素。使用变量元素很方便,但是不够灵活。
元素variable
 元素variable有下面的一些子元素:
name-given:变量名作为一个常量
name-from-attribute:属性名,它的解释时间将传给变量名。
上面两者之一是必需的,下面的子元素是可选的:
variable-class:该变量的全名,缺省类型为java.lang.String.
declare:该变量是否引用一个新对象。缺省值为True.
scope:脚本变量的作用范围。
下表描述了变量的可用性及变量必须设置的方法
Value Availability Method
NESTED Between the start tag and the end tag In doInitBody Tag;otherwise,in doStartTag
AT_BEGIN From the start tag until the end of the page In doInitBody ,doAfterBody,and doEndTag for a tag handler implementing BodyTag,otherwise,in doStartTag and doEndTag
AT_END After the end tag until the end of the page In doEndTag
类TagExtraInfo
 你可以通过扩展类javax.servlet.jsp.TagExtraInfo来定义一个额外标签信息类。TagExtraInfo必须实现方法getVariableInfo来返回一个VariableInfo对象数组,包含下面的一些信息:
 。变量名
 。变量类
 。该变量是否引用一个新对象
 。该变量的可访问性
 网络容器传递一个名为data的参数到方法getVariableInfo,这些属性用来提供对象VariableInfo。
 Struts标签库提供了由bean:define创建的在标签额外信息类DefineTei中的脚本变量信息。由于脚本变量的名称及类作为标签的属性来传递,可以使用方法data.getAttributeString来返回,并填充构造函数VariableInfo。为了脚本变量book能在页面的余下部分使用,book的域应该设置为AT_BEGIN.
 Public class DefineTei extends TagExtraInfo {
  Public VariableInfo[] getVariableInfo(TagData data) {
  String type=data.getAttributeString("type");
   If(type==null)
    Type="java.lang.Object";
   Return new VariableInfo[] {
   New VariableInfo(data.getAttributeString("id"),,type,true,VariableInfo.AT_BEGIN
  };
 }
}
为脚本变量定义的标签额外信息类必须在TLD中声明,这样元素tei-class将会如下:
<tei-class>org.apache.struts.taglib.bean.DefineTagTei</tei-class>
与标签协作
 通过共享对象实现标签的协作JSP技术支持两种类型的对象共享。第一种类型需要共享对象命名、存储在page context中。为了让其它标签访问,标签处理器使用pageContext.getAttribute(name,scope)方法。
 第二种方法是由封闭的标签处理器创建的对象对内部的标签处理器是可访问的。这种形式的对象共享有个优点,它使用了对象私有的名字空间,这样减少了潜在的命名冲突。
 为访问闭合标签创建的对象,标签处理器必须首先通过静态方法TagSupport.findAncestorWithClass(from,class)或TagSupport.getParent方法获得封闭标签。前一个方法应该在特定的嵌套的标签处理器不能保证时使用。一旦ancestor返回,标签处理器就能访问任何静态或动态创建的对象。静态创建的对象是父类的成员。私有对象也可以动态的创建。这样的对象可以通过方法setValue存储在标签处理器中,可以通过getValue方法返回。
 下面的例子展示了一个标签处理器支持命名及私有对象访问共享对象的例子。在该例子中,,处理器为查询标签检验一个名称为connection的属性是否以设置到方法doStartTag中。如果connection属性已经设置,处理器将从page context对象返回connection对象,否则,标签处理器先返回封闭标签的处理器然后从处理器返回connection对象。
Public class QueryTag extends BodyTagSupport {
 Private String connectionId;
 Public int doStartTag() throws JspException {
  String cid=getConnection();
  If(cid!=null){
   Connection=(Connection)pageContext.getAttribute(cid);
   }
   else
    connectionTag ancestorTag=(ConnectionTag)findAncestorWithClass(this,
   ConnectionTag.class);
   If(ancestorTag==null) {
    Throw new JspTagException("A quety without a connection attribute must be nested within a connection tag.");
   Connection =ancestorTag.getConnection();
  }
 }
}
 由标签处理器实现的查询标签可以在下面的方法中使用:
<tt:connection id="con01" …..>…..</tt:connection>
<tt:query id="balances" connection="con01">
 SLECT account,balance FROM acct_table
   Where customer_number =<%=request.getCustno()%>
</tt:query>
<tt:connection …>
 <x:query id="balances">
  SELECT account,balance FROM acct_table
    Where customer_number=<%=request.getCustno()%>
 </x:query>
</tt:conncetion>
在TLD中必须指出属性connection是可选的:
<tag>
 …
 <attribute>
  <name>connection</name>
  <required>false</required>
 </attribute>
</tag>
例子
 自定义标签展示了在开发JSP应用中两种常见问题的解决方案:最小化在JSP页面中的编码量及确保良好的表现形式。为了达到这样的目的,在本章的前面几节展示了各类标签。
迭代标签
 构造动态页面内容经常需要使用流控制语句。流控制标签可以减少这类语句的数量。
Struts的logic:iterate标签返回一个存储在JavaBean组建中的对象。并把它赋值给脚本变量。从脚本变量种返回的标签体信息
JSP页面
 两个Duke's应用页面,catalog.jsp和showcart.jsp.使用了logic:iterate标签来列出collection中的对象。下面展示了catalog.jsp的一段引用,这个JSP页面初始化了iterate标签,该标签设置了脚本变量book,代码片断如下:
<logic:iterate name="bookDB" property="books" id="book" type="database.BookDetails">
 <bean:define id="bookId" name="book" property="bookId" type="java.lang.String"/>
 <tr>
 <td bgcolor="#ffffaa">
 <a href="<%=requet.getContextPath()%>/bookdetails?bookId=<%=bookId%>">
 property="title"/>&nbsp;</strong></a></td>
 <td bgcolor="#ffffaa" rowspan=2>
 <jsp:setProperty name="currency" property="amount" value="<%=book.getPrice()%>"/>
 <jsp:getProperty name="currency" property="format"/>&nbsp;</td>
 <td bgcolor="#ffffaa" rowspan=2>
 <a href="<%=request.getContextPath()%>/catalog?Add=<%=bookId%>">&nbsp;</a></td></tr>
 <tr>
 <td bgcolor="#ffffff">
 &nbsp;&nbsp;<%=messages.getString("By")%><em>
 <jsp:getProperty name="book" property="firstName"/>&nbsp;
 <jsp:getProperty name="book" property="surname"/></em></td></tr>
</logic:iterate>
标签处理器
 标签logic:iterate的实现遵循JSP1.1规范,JSP1.2规范增加了一些新特性。下面将进行讨论:
标签logic:iterate支持几种方法来初始化collection:作为标签属性或者是作为bean或bean的属性。
标签额外信息类
 在IterateTei标签的额外信息类中提供了脚本变量的信息。脚本变量的名称及类传递到构造器VariableInfo。
 Public class InterateTei extends TagExtraInfo {
  Public VariableInfo[] getVariableInfo(TagaData data) {
  String type =data.getAttributeString("type");
  If(type==null)
   Type="java.lang.Object";
   Return new VariableInfo[] {
    New VariableInfo(data.getAttributeString("id"),type,true,VariableInfo.AT_BEGIN)
   };
  }
 }
模板标签库
 模板提供了一种方法分离一般元素的方法。把所有的元素放到文件中使得易于维护且看起来很棒的方法。这样也使得个人开发变得容易,因为设计者可以专注于表现部分。
 模板是JSP页面中每屏都需要变动的部分,每部分都作为一个参数来引用模板。例如,一个简单的模板能包容顶部的标题参数及体参数。
 模板使用了一个嵌套的标签。
JSP页面
 在Duke's Bookstore例子中使用的模板template.jsp如下,该页面包括了创建一个产生屏定义并使用insert标签插入参数的JSP页面。
<%@ taglib uri="/tutorial-template.tld" prefix="tt" %>
<%@ page errorPage="errorpage.jsp"%>
<%@ include file="screendefinitions.jsp" %>
<html>
<head>
 <title>
  <tt:insert definition="bookstore" parameter="title"/>
 </title>
</head>
<tt:insert definition="bookstore" parameter="banner"/>
<tt:insert difinition="bookstore" parameter="body"/>
 </body>
</html>
页面screendefinitions.jsp创建了一个基于请求属性的页面定义
<tt:definition name="bookstore" screen="<%=(String)request.getAttribute("/"selectedScreen/")%>">
<tt:screen id="/enter">
 <tt:parameter name="title" value="Duke's Bookstore" direct="true"/>
 <tt:parameter name="banner.jsp" direct="false"/>
 <tt:parameter name="body" value="/bookstore.jsp" direct="false"/>
</tt:screen>
<tt:screen id ="/catalog">
 <tt:parameter name="title" value="<%=messages.getString("TitleBookCatalog")%>" direct="true"/>
</tt:definition>
模板由Dispatcher servlet实例化,Dispatcher首先获得请求页面并将它作为一个请求属性存储起来。这是必需的,因为当请求定向到template.jsp时,请求URL不包含原始请求板反映了定向路径,最后servlet将请求重定向到template.jsp
public class Dispatcher extends HttpServlet{
 public void doGet(HttpServletRequest request,HttpServletResponse response){
  request.setAttribute("selsectedScreen",request.getServletPath());
  RequestDispatcher dispatcher=request.getRequestDispatcher("/template.jsp");
  If(dispatcher!=null)
  Dispatcher.forward(request.response);
 }
 public void doPost(HttpServletrequest request,HttpServletResponse response) {
  request.setAttribute"selectedScreen",request.getServletPath());
  RequestDispatcher dispatcher=request.getRequestDispatcher("/template.jsp");
  If(dispatcher!=null)
   Dispatcher.forward(rquest,response);
  }
 }
标签处理器
 模板标签库包含四个标签处理器--DefinitionTag,ScreenTag,ParameterTag,InsertTag,DefinitionTag,ScreenTag,ParameterTag组成了一系列嵌套的标签处理器,他们可以共享共有的及私有的对象。DefinitionTag创建了一个共有的对象definition供InsertTag使用。
Public int doStartTag() {
 HashMap screens=null;
 Screens=(HashMap)pagecontext.getAttribute("screens",pageContext.APPLICATION_SCOPE);
 If(screens==null)
  PageContext.setAttribute("screens",new HashMap(),pageContext.APPLICATION_SCOPE);
 Return EVAL_BODY_INCLUDE;
}
下面的表格是关于Duke's Bookstore应用的哈西表
Screen Id Title Banner Body
/enter Duke's bookstore /banner.jsp /bookstore.jsp
/catalog Book catalog /banner.jsp /catalog.jsp
/bookdetails Book description  /banner /bookdetails.jsp
/showcart Your shopping cart /banner.jsp /showcart.jsp
/cashier Cashier /banner.jsp /cashier.jsp
/receipt Receipt /banner.jsp /receipt..jsp

 在doEndTag中,DefinitionTag创建了一个公有的对象Definition:
public int doEndTag() throws JspTagException {
 try {
  Definition definition=new Definition();
  Hashtable screens=null;
  ArrayList params=null;
  TagSupport screen=null;
  Screens=(HashMap)pageContext.getAttribute("screens",pageContext.APPLICATION_SCOPE);
 If(screens!=null)
  Params=(ArrayList)screens.get(screenId);
  Else
   ……
 if(params==null)
   ……
 Iterator ir=null;
 If(params!=null)
  Ir=params.iterator();
 While((ir!=null)&&ir.hasNext())
  Definition.setParam((Parameter) ir.next());
  //put the difinition in the page context.
  PageContext.setAttribute(
   DefinitionName,definition);
  }catch(Exception ex) {
   ex.printStackTrace();
  }
  return EVAL_PAGE;
 }
如果传递给请求的URL是/enter,Definition包含下面的一些项:
Title Banner Body
Duke's bookstore /banner.jsp /bookstore.jsp
URL/enter的定义如下表
Parameter name Parameter value IsDirect
Title /banner.jsp True
Banner /banner.jsp False
Body /bookstore.jsp false
InsertTag使用Definition插入一个参数到响应中。在方法doStartTag中,它从page context返回对象definition.
Public int doStartTag()
{
 //get the definition from the page context
 definition =(Definition) pageContext.getAttribute(definitionName);
 //get the parameter
 if(parameterName!=null)&&definition!=null)
  parameter=(Parameter)definition.getParam(parameterName);
 if(parameter!=null)
  directInclude=parameter.isDirect();
 return SKIP_BODY;
}
方法doEndTag插入了一个参数值,如果该参数是直接参数,它将直接传到响应对象,否则请求将送到该参数,且响应对象动态的包含到所有的响应中。
 Public int doTndTag() throws JspTagException {
  Try{
   If(directInclude&&parameter!=null)
    PageContext.getOut().print(parameter.getValue());
   Else{
    
    If((parameter!=null)&&(parameter..getValue()!=null))
     PageContext..include(parameter.getValue());
    }   
}catch(Exception e){
 throw new JspTagException(e.getMessage());
}
return EVAL_PAGE;
}
标签处理器是如何被调用的?
 标签接口定义了位于标签处理器和JSP页面servlet之间的基础协议。它定义了在遇到标签起始及结束处是要调用的方法。
 JSP servlet调用setPageContext,setParent及在调用doStartTag方法之前调用属性设置方法。JSP servlet也负责在结束页面之前调用release以释放资源。
 下面是一个典型的标签处理器方法调用顺序的例子:
ATag t =new Atag();
t.setPageContext(…);
t.setParent(…);
t.setAttribute(value1);
t.setAttribute2(value2)
t.doStartTag();
t.doEndTag();
t.release();
接口BodyTag扩展自Tag,它允许标签处理器访问body。该接口提供了三个新方法:
 .setBodyContent
 .doInitBody
 .doAfterBody
一个典型的调用顺序如下:
t.doStartTag();
out=pageContext.pushBody();
t.setBodyContent(out);
//perform any initialization needed after body content is set
t.doInitBody();
t.doAfterBody();
//while doAfterBody returns EVAL_BODY_BUFFERED we iterate body evaluation
……
t.doAfterBody();
t.doEndTag();
t.pageContext.popBody();
t.release();                          
 
第14章 事务
Dale Green著
JSP WU 译
    一个典型的企业应用程序在一个或多个数据库中访问和存储信息。因为这些信息对于商业操作非常重要,它必须精确、实时、可靠。如果允许多个程序同时更新相同的数据,就会破坏数据的完整性。如果在一个商业交易处理过程中,部分数据被更新后系统崩溃也将破坏数据完整性。事务通过预防以上情况的发生确保数据的完整性。事务控制多个应用程序对数据库的并发操作。如果发生系统崩溃,事务确保恢复的数据崩溃前将保持一致。
本章内容:
什么是事务
  容器管理事务
   事务的属性
   回滚容器管理事务
   同步会话bean实例变量
   容器管理事务中不允许使用的方法
Bean 管理事务
   JDBC事务
   JTA  事务
   非提交返回事务
   在Bean管理事务中不允许使用的方法
企业Bean事务摘要
  事务超时
  隔离级别
  更新多个数据库
  Web 组件事务

一.什么是事务
   模拟一个商业交易,应用程序需要完成几个步骤。例如,一个财物应用程序,可能会将资金从经常性帐户(checking account)转到储蓄性账户(saving account),该交易的伪码表示如下:
              begin transaction
   debit checking account
   credit savings account
   update history log
commit transaction
三个步骤要么全部完成,要么一个都不做。否则数据完整性将被破坏。因为事务中的所有步骤被看作一个统一的整体,所以事务一般被定义为一个不可分割的工作单元。
结束事务有两种方法:提交或者回滚。当一个事务提交,数据修改被保存。如果事务中有一个步骤失败,事务就回滚,这个事务中的已经执行的动作被撤销。例如在上面的伪码中,如果在处理第二步的时候硬盘驱动器崩溃,事务的第一步将被撤销。尽管事务失败,数据的完整性不会被破坏,因为帐目仍然保持平衡。
前面伪码中,begin和commit标明了事务的界限。当设计一个企业Bean的时候,你要决定怎样通过容器管理或bean管理事务来指定事务界限。
二.容器管理事务

在容器管理事务的企业Bean中,EJB容器来设定事务界线。你能够在任何企业Bean中使用容器管理事务:会话Bean、实体Bean或者 Message-driven Bean。容器管理事务简化了开发,因为企业Bean不用编码来显式制定事务界限。代码不包括开始结束事务的语句。典型的,容器会在一个企业Bean的方法被调用前立即开始一个事务,在这个方法退出以前提交这个事务。每个方法都关联一个事务。在一个方法中不允许嵌套或多个的事务存在。容器管理事务不需要所有的方法都关联事务。当部署一个Bean时,通过设定部署描述符中的事务属性来决定方法是否关联事务和如何关联事务。

 事务的属性
一个事务的属性控制了事务的使用范围。图 14-1说明了为什么控制事务的范围很重要。图中,method-A开始一个事务然后调用Bean-2中的method-B.它运行在method-A开始的事务中还是重新执行一个新的事务?结果要看method-B中的事务属性。
 
Figure 14-1 Transaction Scope
一个事务属性可能有下面的属性之一:
" Required
" RequiresNew
" Mandatory
" NotSupported
" Supports
" Never
Required
          如果客户端正在一个运行的事务中调用一个企业Bean的方法,这个方法就在这个客户端的事务中执行。如果客户端不关联一个事务,这个容器在运行该方法前开始一个新的事务。
          Required属性在许多事务环境中可以很好的工作,因此你可以把它作为一个默认值,至少可以在早期开发中使用。因为事务的属性是在部署描述符中声明的,在以后的任何时候修改它们都很容易。
RequiresNew
           如果客户端在一个运行的事务中调用企业Bean的方法,容器的步骤是:
1. 挂起客户端的事务
2. 开始一个新的事务
3. 代理方法的调用
4. 方法完成后重新开始客户端的事务
           如果客户端不关联一个事务,容器运行这个方法以前同样开始一个新的事务。如果你想保证该方法在任何时候都在一个新事物中运行,使用RequiresNew属性。
Mandatory
          如果客户端在一个运行的事务中调用企业Bean的方法,这个方法就在客户端的事务中执行。如果客户端不关联事务,容器就抛出TransactionRequiredException 异常。
          如果企业Bean的方法必须使用客户端的事务,那么就使用Mandatory属性。
NotSupported
          如果客户端在一个运行的事务中调用企业Bean的方法,这个容器在调用该方
法以前挂起客户端事务。方法执行完后,容器重新开始客户端的事务。
   如果客户端不关联事务,容器在方法运行以前不会开始一个新的事务。为不需要事务的方法使用NotSupported属性。因为事务包括整个过程,这个属性可以提高性能。

Supports
          如果客户端在一个运行的事务中调用企业Bean的方法,这个方法在客户端的事务中执行,如果这个客户端不关联一个事务,容器运行该方法前也不会开始一个新的事务。因为该属性使方法的事务行为不确定,你应该谨慎使用Supports属性。

Never
          如果客户端在一个运行的事务中调用企业Bean的方法,容器将抛出RemoteException异常。如果这个客户端不关联一个事务,容器运行该方法以前不会开始一个新的事务。

Summary of Transaction Attributes(事务属性概要)
表 14-1 列出了事务属性的影响。事务T1和T2都被容器控制。T1是调用企业Bean方法的客户端的事务环境。在大多数情况下,客户端是其它的企业Bean。T2是在方法执行以前容器启动的事务。在表 14-1中,"None"的意思是这个商业方法不在容器控制事务中执行。然而,该商业方法中的数据库操作可能在DBMS管理控制的事务中执行。

Setting Transaction Attributes (设定事务属性)
            因为事务属性被保存在配置描述符中,他们会在J2EE应用程序开发的几个阶段被改变:创建企业Bean,应用程序装配和部署。然而, 当创建这个Bean的时候指定它的属性是企业Bean开发者的责任。只有将该组件装配到一个更大的应用程序时可以由开发者修改该属性,而不要期待J2EE应用程序部署者来指定该事务属性。

表 14-1 事物属性和范围
事务属性 客户端事务 商业方法事务
Required None T2
 T1 T1
RequiresNew None T2
 T1 T2
Mandatory None error
 T1 T1
NotSupported None None
 T1 None
Supports None None
 T1 T1
Never None None
 T1 Error
你可以为整个企业Bean或者单个方法指定事务属性。如果你为整个企业Bean和它某个方法各指定一个事务属性,为该方法指定的事务属性优先。当为单个方法指定事务属性时,不同类型企业Bean的要求也不同。会话Bean需要为商业方法定义属性属性,但create方法不需要定义事务属性。实体Bean需要为商业方法、create方法、remove方法和查找(finder)方法定义事务属性。Message-driven Bean需要为onMessage方法指定属性事务,而且只能是Required和NotSupported其中之一。

容器管理事务的回滚
  
在以下两中情况下,事务将回滚。第一,如果产生一个系统异常,容器将自动回滚该事务。第二,通过调用EJBContext接口SetRollbackOnly方法,Bean方法通知容器回滚该事务。如果Bean抛出一个应用异常,事务将不会自动回滚,但可以调用SetRollbackOnly回滚。对于一个系统和应用的异常,参考第5章的处理异常一节。
下面这个例子的代码在j2eetorial/examples/src/bank目录下。在命令行窗口下进入j2eetutorial/examples目录执行ant bank命令编译这些代码,执行 ant create-bank-table命令创建要用到的表。一个BankApp.ear样本文件在j2eetutorial/examples/ears目录下。通过BnankEJB 实例的transferToSaving方法来说明setRollbackOnly方法的用法。如果余额检查出现负数,那么transferToSaving调用setRollBackOnly回滚事务并抛出一个应用程序异常(InsufficientBalanceException)。updateChecking和updateSaving 方法更新数据表。如果更新失败,这两个方法抛出SQLException异常而transgerToSaving方法抛出EJBException异常。因为EJBException是一个系统异常,它使容器事务自动回滚事务。TransferTuSaving 方法的代码如下:
public void transferToSaving(double amount) throws
   InsufficientBalanceException  {

   checkingBalance -= amount;
   savingBalance += amount;

   try {
      updateChecking(checkingBalance);
      if (checkingBalance < 0.00) {
         context.setRollbackOnly();
         throw new InsufficientBalanceException();
      }
      updateSaving(savingBalance);
   } catch (SQLException ex) {
       throw new EJBException
          ("Transaction failed due to SQLException: "
          + ex.getMessage());
   }
}
   当一个容器回滚一个事务,它总是会撤消事务中已执行的SQL语句造成的数据改动。然而,仅仅在实体Bean中容器回滚才会改变Bean的实例变量(与数据库状态有关的字段)。(这是因为容器会自动调用实体Bean的ejbLoad方法,该方法从数据库中读入实例变量的值。)当发生回滚,会话Bean必须显式重新设置所有被事务改动过的实例变量。重设会话Bean的实例变量最简单的方法是实现SessionSynchronization接口。
同步会话Bean的实例变量
   SessionSynchronization接口是可选的,它允许你在Bean的实例变量和它们在数据库中的相应值之间保持同步。容器会在事务的几个主要阶段调用SessionSynchronization接口的对应方法-afterBegin、beforeCompletion和afterCompletion。
AfterBegin方法通知Bean实例一个新的事务已经开始。容器在调用商业方法以前立即调用afterBegin方法。afterBegin方法是从数据库中读入实例变量值的最佳位置。例如,在BanBean类中,在afterBegin方法中从读入了CheckingBalance和savingBalance变量的值:
  public void afterBegin() {

   System.out.println("afterBegin()");
   try {
      checkingBalance = selectChecking();
      savingBalance = selectSaving();
   } catch (SQLException ex) {
       throw new EJBException("afterBegin Exception: " +
           ex.getMessage());
   }
}
商业方法方法完成以后,容器调用beforeCompletion方法,不过仅仅是在事务提交以前。BeforeCompletion方法是会话Bean回滚事务的最后时机(通过调用setRollbackOnly方法).如果会话Bean还没有实例变量的值更新数据库,就在beforCompletion方法里实现。
afterCompletion方法指出事务已经完成。它只有一个布尔型的参数,true表示事务被正确提交false表示事务回滚。如果事务回滚,会话Bean可以在该方法中从数据库中重新读取它的实例变量值:
public void afterCompletion(boolean committed) {

   System.out.println("afterCompletion: " + committed);
   if (committed == false) {
      try {
         checkingBalance = selectChecking();
         savingBalance = selectSaving();
      } catch (SQLException ex) {
          throw new EJBException("afterCompletion SQLException:
         " + ex.getMessage());
      }
   }
}
容器管理事务中不允许使用的方法
    你不应该调用可能干扰容器设置的事务界线的方法,下面列出了所有禁止的方法:
" java.sql.Connection接口的commit、setAutoCommit和rollback方法
" javax.ejb.EJBContext 接口的getUserTransaction方法
" javax.transaction.UserTransaction接口的所有方法
   然而你可以在Bean管理事务中使用这些方法设置事务界限。
三.Bean管理事务
   在一个Bean管理事务中,会话Bean或者Message-driven Bean是用代码显式设置事务界线的。实体Bean不能使用Bean管理事务,只能使用容器管理的事务。虽然容器管理事务Bean需要较少的代码,但它也有一个局限:方法执行时,它只能关联一个事务或不关联任何事务。如果这个局限使你Bean编码困难,你应该考虑使用Bean管理事务。(译者:实际上J2EE服务器不支持嵌套事物,那么Bean管理事务唯一的优点就是可以在一个方法中一次启动多个事务)
   下面的伪码很好说明了Bean管理事对商业逻辑的紧密控制。通过检查各种条件,伪码决定是否在商业方法中启动或停止不同的事务。
begin transaction
...
update table-a
...
if (condition-x)
   commit transaction
else if (condition-y)
   update table-b
   commit transaction
else
   rollback transaction
   begin transaction
   update table-c
   commit transaction
当为会话Bean或Message-driver Bean的Bean管理事务编码时,你必须决定是使用jdbc或者JTA事务。下面的内容论述了两种事务类型。
JDBC 事务
JDBC事务通过DBMS事务管理器来控制。你可能会为了使用会话Bean中的原有代码而采用JDBC事务将这些代码封装到一个事务中。使用JDBC事务,要调用java.sql.Connection接口的commit和rollback方法。事务启动是隐式的。一个事务的从最近的提交、回滚或连接操作后的第一个SQL的语句开始。(这个规则通常是正确的,但可能DBMS厂商的不同而不同)
代码资源
下面的例子在j2eetutorial/examples/src/ejb/warehouse目录下。在命令行窗口中进入j2eetutorial/examples目录执行ant bank命令编译这些源文件,执行ant create-warehouse-table命令创建要用到的表,一个样本WarehouseApp.ear文件在j2eetutorial/example/ears 目录下。
下面的代码来自WarehouseEJB例子,一个会话Bean通过使用Connection接口的方法来划定Bean管理事务界限。ship方法以调用名为con的连接对象的setAutoCommit方法开始,该方法通知DBMS不要自动提交每个SQL语句。接下来ship 方法更新order_item和inventory数据表。如果更新成功,这个事务就会被提交。如果出现异常,事务就回滚。
public void ship (String productId, String orderId, int
quantity) {

   try {
      con.setAutoCommit(false);
      updateOrderItem(productId, orderId);
      updateInventory(productId, quantity);
      con.commit();
   } catch (Exception ex) {
       try {
          con.rollback();
          throw new EJBException("Transaction failed: " +
             ex.getMessage());
       } catch (SQLException sqx) {
           throw new EJBException("Rollback failed: " +
              sqx.getMessage());
       }
   }
}
 
JTA 事务
     JTA是Java Transaction API 的缩写。这些API 允许你用独立于具体的事务管理器实现的方法确定事务界限。J2EE SDK 事务管理器通过Java事务服务(Java Transaction Service, JTS)实现。但是你的代码并不直接调用JTS中的方法,而是调用JTA方法来替代,JTA方法会调用底层的JTS实现。
JTA事务被J2EE 事务管理器管理。你可能需要使用一个JTA事务,因为它能够统一操作不同厂商的数据库。一个特定DBMS的事务管理器不能工作在不同种类的数据库上。然而J2EE事务管理器仍然有一个限制--它不支持嵌套事务。就是说,它不能在前一个事务结束前启动另一个事务。
下面例子的源代码在j2eetutorial/examples/src/ejb/teller目录下,在命令行窗口进入j2eetutorial/examples目录,执行ant teller命令编译这些源文件,执行ant create-bank-teller命令创建要用到的表。一个样本TellerApp.ear文件在j2eetutorial/examples/ears目录下。
要自己确定事务界限,可以调用javax.transaction.UserTransaction接口的begin、commit和rollback方法来确定事务界限(该接口只能在SessionBean中使用,实体Bean不允许使用用户自定义的)。下面选自TellerBean类的代码示范了UserTransaction的用法。begin和commit方法确定了数据库操作的事务界限,如果操作失败则调用rollback回滚事务并抛出EJBException异常。
public void withdrawCash(double amount) {

   UserTransaction ut = context.getUserTransaction();

   try {
      ut.begin();
      updateChecking(amount);
      machineBalance -= amount;
      insertMachine(machineBalance);
      ut.commit();
   } catch (Exception ex) {
       try {
          ut.rollback();
       } catch (SystemException syex) {
           throw new EJBException
              ("Rollback failed: " + syex.getMessage());
       }
       throw new EJBException
          ("Transaction failed: " + ex.getMessage());
    }
}
非提交返回事务
使用Bean管理事务的无状态会话Bean在事务返回前必须提交或者返回事务,而有状态的会话Bean没有这个限制。
对于使用JTA事务的有状态会话Bean,Bean实例和事务的关联越过大量用户调用被保持,甚至被调用的每个商业方法都打开和关闭数据库连接,该市无关联也不断开,直到事务完成(或回滚)。
对于使用JDBC事务的有状态会话Bean,JDBC连接越过用户调用保持Bean和事务之间的关联。连接关闭,事务关联将被释放。
在Bean管理事务中不允许使用的方法
在Bean管理的事务中不能调用EJBContext接口的getRollbackOnly和setRollbackOnly方法,这两个方法只能在容器管理事务中被调用。在Bean管理事务中,应调用UserTransaction接口的getStatus和rollback方法。
四.企业Bean事务摘要
如果你不能确定怎么在企业Bean中使用事务,可以用这个小技巧:在Bean的部署描述符中,制定事务类型为容器管理,把整个Bean(所有方法)的事务属性设置为Required。大多数情况下,这个配置可以满足你的事务需求。
表14-2列出了不同类型的企业Bean所允许使用的事务类型。实体Bean只能使用容器管理事务,但可以在部署描述符中配置事务属性,并可以调用EJBContext接口的setRollbackOnly方法来回滚事务。
表 14-2 企业Bean允许的事务类型
企业Bean类型 容器管理事务 Bean管理事务
  JTA JDBC
实体Bean Y N N
会话Bean Y Y Y
Message-driven Y Y Y
 
会话Bean既可以使用容器管理事务也可以使用Bean管理事务。Bean管理事务又有两种类型:JDBC事务和JTA事务。JDBC事务使用Connection接口的commit和rollback方法来划分事务界限。JTA事务使用UserTransaction接口的begin、commit和rollback方法来划分事务界限。
在Bean管理事务的会话Bean中,混合使用JTA事务和JDBC事务是可能的。但是我不推荐这样使用,因为这样会造成代码的调试和维护都很困难。
Message-driver Bean和会话Bean一样既可以使用容器管理事务也可以使用Bean管理事务。
五.事务超时
对于容器管理事务,事务超时间隔是通过设置default.properties文件中ransaction.timeout属性的值来确定的,该文件在J2EE SDK安装目录的config子目录下。如下例将事务超时间隔设置为5秒钟:
transaction.timeout=5
这样,当事务在5秒钟内还没有完成,容器将回滚该事务。
J2EE SDK安装后,超时间隔的缺省值为0,表示不计算超时,无论事务执行多长时间,除非异常出错回滚,一直等待事务完成。
只有使用容器管理事务的企业Bean才会受到transaction.timeout属性值的影响。Bean管理的JTA事务使用UserTransaction接口的setTransactionTimeout方法来设置事务超时间隔。
六.隔离级别
事务不仅保证事务界限内的数据库操作全部完成(或回滚)同时还隔离数据库更新语句。隔离级别描述被修改的数据对其他事物的可见度。
假如一个应用程序在事务中修改一个顾客的电话号码,在事务结束前另一个应用程序要读取该条记录的电话号码。那么第二个应用程序是读取修改过但还没提交的数据,还是读取未修改前的老数据呢?答案就取决于事务的隔离级别。如果事务允许其他程序读取未提交的数据,会因为不用等待事务结束而提高性能,同时也有一个缺点,如果事务回滚,其他应用程序读取的将是错误的数据。
容器管理持久性(CMP)的实体Bean的事务级别无法修改,它们使用DBMS的默认个理解别,通常是READ_COMMITTED。
Bean管理持久性(BMP)的实体Bean和两种会话Bean都可以通过在程序中调用底层DBMS提供的API来设置事务级别。例如,一个DBMS可能允许你如下调用setTransactionIsolation方法将隔离级别设置成可读取未提交数据:
Connection con;
...
con.setTransactionIsolation(TRANSACTION_READ_UNCOMMITTED);
不要在事务执行期间更改隔离级别,通常隔离级别的更改会引起DBMS产生一次隐式提交。因为隔离级别的控制会跟具体的DBMS厂商不同而不同,具体的信息请参考DBMS的文档。J2EE平台规范不包括隔离级别标准。
七.更新多个数据库
J2EE事务管理器控制着除了Bean管理的JDBC事务以外的所有企业Bean事务,它允许企业Bean在同一个事务中更新多个数据库。下面示范在单个事务中更新多个数据库的两个应用。
图14-2中,客户端调用Bean-A的商业方法,商业方法启动一个事务,更新数据库X和Y,Bean-A的商业方法有调用Bean-B的商业方法,Bean-B的商业方法更新数据库Z然后返回事务的控制权给Bean-A的商业方法,由Bean-A提交该事务。三个数据库的更新都在同一个事务中发生。
 
图 14-2 更新多个数据库
 图14-3中,客户端调用Bean-A的商业方法,该商业方法启动一个事务并更新数据库X,然后调用另一个J2EE服务器中的Bean-B的方法,该方法更新数据库Y。J2EE服务器保证两个数据库的更新都在同一个事务中进行(笔者认为应该是第一个J2EE服务器的事务管理器管理整个事物)。
 
图 14-3 跨越J2EE服务器更新多个数据库
八.Web 组件事务
 Web组件中划分事务界限可以使用java.sql.Connection接口和javax.transaction.UserTransaction接口中的任意一个。跟Bean管理事务的会话Bean使用一样的两个接口。这两个接口的使用方法参考前面几节的内容。Web组件事务的例子在第10章Servlet技术第四节共享信息的访问数据库小节讲述过。

 
第15章 安全
Eric Jendrock著
Iceshape Zeng译

J2EE平台提供了与具体应用程序安全实现机制无关的编程模型,这种无关性提高了应用程序的可移植性,使应用程序可以部署在不同的安全环境中。本章假定读者已经了解了基本的安全概念,如果读者还需要学习这些概念,我们强烈建议读者学习The Java Tutorial一书的Security部分(http://java.sun.com/docs/books/tutorial/security1.2/index.html)。
本章内容:
纵览
安全角色
 声明和连接角色引用
 映射角色到J2EE用户和组
Web层安全
 保护Web资源
 控制Web资源访问
 验证Web资源用户
 使用编程安全机制
 不受保护的Web资源
EJB层安全
 声明方法许可
 使用编程安全机制
 不受保护的EJB层资源
应用程序客户端层安全
 确定应用程序客户端的回调处理机制
EIS(Enterprise Information System)层安全
 配置契约
 容器管理的契约
 组件管理的契约
 配置资源适配器安全
传递安全身份
 配置组件使用的传递安全身份
 配置客户端验证机制
J2EE用户、域和组
 管理J2EE用户和组
安装服务器证书
一.纵览
J2EE平台定义了应用程序组件的开发与装配者和运行环境中的应用程序配置者之间的声明性约定(The J2EE platform defines declarative contracts between those who develop and assemble application components and those who configure applications in operational environments.)在应用程序的安全上下文中,应用程序提供者要提供在能完全满足应用程序配置要求的声明机制。在应用程序中声明性安全机制都被表示成部署描述符中的声明语法。应用程序部署者使用各种J2EE服务器的不同工具将部署描述符描述的应用程序安全需求映射到J2EE容器实现的安全机制。J2EE SDK用deploytool工具实现这个功能。
编程安全机制是指用安全敏感的程序来实现安全控制。当声明性安全无法满足应用程序的安全模型的表达时,编程安全就很有用。例如,应用程序可能需要实现基于当天某个时间、方法调用参数、企业Bean或者Web组件的内部状态等的安全授权机制,或者根据数据库中的用户信息约束访问权限等就需要用编程安全机制。
J2EE应用程序都是由可以部署在不同容器中的各种组件组成的。这些组件通常用来创建多层企业级应用程序。J2EE安全体系的目的是在实现各层安全的基础上实现各层相关联的应用程序整体安全。
每一层都有受保护资源和非保护资源。通常你要确认受保护资源只有授权用户可以访问,授权机制提供受保护资源的访问控制。授权机制基于识别和验证,它通常作为允许访问系统资源的先决条件。识别是能够"认出"系统实体的过程。验证是校验计算机系统的用户、设备或者其他实体的身份的过程。
访问非保护资源不需要授权机制。因此访问非保护资源也不需要验证。访问系统资源而不需要授权被称作未验证或匿名访问。
二.安全角色
当你设计一个企业Bean或者Web组件的时候,你总是会考虑什么样的用户可以访问这个组件。例如,一个Account企业Bean可以被客户、银行出纳员和分行经理访问。这些用户类型中的每一种都被称作安全角色--一个由应用程序组装者定义的抽象逻辑用户组。当应用程序部署时,部署者将把这些角色映射到运行环境的安全实体。
一个J2EE用户组也代表一个用户分类,但是它和角色有不同的范围。J2EE用户组被指定到整个J2EE服务器,而角色只覆盖一个特定的应用程序。
你可以为应用程序中的JAR或者WAR文件声明角色来创建一个角色。下面是一个用deploytool部署工具创建角色的步骤的示例:
1. 选中企业Bean的JAR文件或者Web组件的WAR文件
2. 在Roles页签里,点击Add按钮
3. 在表格中输入Name和Description列的值
声明和连接角色引用
一个安全角色引用允许一个企业Bean或者Web组件引用一个已经存在的安全角色。一个安全角色是一个应用程序相关的逻辑用户组,他们按照如消费水平或者职务等共同特性被分类。当应用程序被部署时,角色被映射到运行环境中的principals(作为验证结果赋予用户的安全身份的集合)或者用户组这样的安全实体。(When an application is deployed, roles are mapped to security identities, such as principals (identities assigned to users as a result of authentication) or groups, in the operational environment.)基于它,一个有特定角色的用户就有了相应的J2EE应用程序访问权。连接指明被引用安全角色的实际名称(The link is the actual name of the security role that is being referenced.)。
在应用程序装配时,装配者为应用程序创建安全角色,并把这些安全角色关联到可用的安全机制中。装配者再通过连接单个的Servlet或者JSP到应用程序定义的角色来分配角色引用。
安全角色引用定义了角色名(程序可引用名)和应用程序定义的安全角色名之间的映射,通过该引角色名,在Web组件中可以调用isUserInRole(String name)(见Web层安全一节的使用编程安全机制)函数,在EJB组件中可以调用isCallerInRole(String name)(见EJB层安全一节的是用编程安全机制)函数。例如:映射安全角色引用名cust到名为bankCustomer的安全角色的步骤为:
1. 选中要引用安全角色的Web组件或者企业Bean
2. 选择Security页签
3. 如果cust并没有出现在Role Names Referenced In Code板块,点击Add按钮
4. 在Coded Name列输入cust
5. 在Role Name列的下拉列表中选择bankCustomer
如果在下拉列表中没有bankCustomer选项,点击Edit Roles按钮添加该角色
6. 点击最后一列的纸页图标再弹出的对话框中为cust添加描述文字,点击OK。
配置好后,isUserInRole("bankCustomer")和isUserInRole("cust")都将返回true,并对Method Permissions面板中描述的方法都有访问权限。
因为代码中的引用名是通过连接映射到角色名的,所以你可以随时更改角色名而不需要修改代码。例如你要修改bankCustomer的角色名,你不必修改编码中的cust名,而只需要将cust的连接重新映射到bankCustomer的新角色名。
映射角色到J2EE用户和组
当你在开发一个J2EE应用程序的时候,你应该知道用户的角色,但是可能你并不知道确切的用户是谁。这被很好的体现在J2EE的安全体系中,在你的组件被发布后,J2EE服务器的系统管理员会将安全角色映射到缺省域的用户或者组。在AccountBean的例子中,管理员可能会将管理员的角色赋给用户Sally,而只给用户Bob、Ted和Clara三人Teller的角色。
管理员在deploytool工具中通过如下步骤将角色映射到J2EE用户和组:
1. 选中J2EE应用程序
2. 在安全页签中,在Role Name列表中选中相应的角色
3. 点击Add按钮
4. 在弹出的Users对话框中选择属于该角色的用户和组(用deploytool创建用户和组将在J2EE用户、域和组一节的管理J2EE用户和组部分中讲述)
三.Web层安全
本节讲述Web层的受保护资源和验证用户。
保护Web资源
你可以通过制定安全约束来保护Web资源。安全约束决定谁通过验证可以访问受保护的Web资源,这些资源是URL和HTTP请求的列表。安全约束可以在deploytool部署工具中设置,具体操作将在后面介绍。
如果你是还没有通过验证的用户,当你访问受保护的Web资源的时候,Web容器将验证你的访问权限。容器只有在你被证明具有该资源的访问权限的时候才接受你的请求。
控制Web资源访问
根据一下步骤,在deploytool中可以设置对Web资源的访问控制
1. 选中存放Web资源的WAR节点
2. 选中Security页签
3. 在Security Contraints中点击Add按钮
4. 点击Web Resource Collection域的Add按钮添加将受安全约束的Web资源及和名称,点击下面的Edit按钮配置该安全约束的Web资源集合。
5. 点击Authorized Roles域的Edit按钮添加角色到该安全约束,这些角色才被允许访问这些受保护的Web资源
Web资源访问的用户验证
当你试图访问一个受保护的Web资源的时候,Web容器将激活为该资源设置好的验证机制。你可以配置以下几种验证机制:
  HTTP原始验证
  基于FORM的验证
  客户端证书验证
原始验证
如果使用原始验证,Web服务器将通过从客户端获得的用户名和密码验证该用户。
基于Form的验证
如果使用基于基于Form的验证,可以在HTTP浏览器为最终用户自定义登录页面和错误提示页。
不管是原始验证还是基于Form的验证都不能确保安全。在基于Form的验证中用户会话通过普通文本传递,而且目标服务器是没有通过验证的(In form-based authentication, the content of the user dialog box is sent as plain text, and the target server is not authenticated.)。原始验证使用UUENCODE编码在Internet上传递用户名和密码,但是没有加密。这种基于Form的验证使用Base64编码,仍然很容易暴露你的用户名和密码,除非你的所有连接都在SSL上进行。如果有人中途截取这些数据,解码得到用户名和密码是再简单不过的事。
客户端证书验证
客户端证书验证是比上两种方法更安全的验证方法。它使用基于SSL的HTTP(HTTP over SSL ,HTTPS),在此,服务器和客户端(可选)使用公匙证书验证每一个用户。SSL(Secure Sockets Layer 安全套接层)提供数据加密、服务器验证、消息完整性验证和可选的客户端TCP/IP连接验证。你可以把公匙证书看作数字护照,它由可信任的被称为证书授权机构(CA)的组织发给,并给信息发送者提供安全证明。如果使用客户端正书验证,Web服务器将使用X.509证书来验证客户端,该证书跟X.509公匙下属机构定义的标准公匙证书一致。
配置Web资源的验证机制
配置WAR文件中的Web资源步骤:
1. 选中WAR节点
2. 选中Security页签
3. 在User Authentication Method下拉列表中选择None、Basic、客户端证书和基于Form的验证机制中的一种
a) 如果使用基于From的验证,必须点击Settings按钮并再弹出的对话框中设置相应的选项:Realm Name、Login Page和Error Page。用户无法通过登录验证时将显示Error Page。
b) 如果使用原始验证,必须在Settings对话框中将Realm Name设置为Defailt
使用SSL增强原始验证和基于Form验证安全性
原始验证和基于Form的验证都无法保证密码的机密性,为了克服这个缺陷,可以使该验证协议运行在SSL保护会话中并确定所有的消息内容都受到该协议保护。
以下步骤配置SSL上的安全验证:
1. 选中Web组件(WAR节点)
2. 在Security页签中确定在User Authentication Method下来列表中选中Basic或者Form Based
3. 在Security Constraint域中点击Add按钮
4. 选中刚才添加的Security Constraint实体
5. 在Network Security Requirement下拉列表中选中CONFIDENTIAL
使用编程安全机制
编程安全机制是在声明性安全机制无法有效的表达应用程序的安全模型时使用安全敏感的应用程序的方法。编程安全机制由HttpServletRequest接口的以下几个方法组成:
  getRemoteUser
  isUserInRole
  getUserPrincipal
getRemoteUser方法被用来确定客户端被验证的用户名,isUserInRole方法被用来确定用户是否具有指定的安全角色,getUserPrincipal方法返回一个java.security.Principal对象。
这些API允许servlet基于远程用户的逻辑角色决定商业逻辑,而且允许servlet确定当前用户的principal。
不受保护的Web资源
很多应用程序包含不受保护的Web内容,任何用户都可以不通过验证访问这些资源。在Web层,只需要不配置验证机制就可以支持无限制的自由访问。
四.EJB层安全
本节介绍针对EJB层应受保护资源的声明性安全机制和编程安全机制。受保护资源包括被应用程序客户端、Web组件和其他企业Bean调用的企业Bean方法。
通过一下方法可以保护EJB层资源:
  声明方法许可
  映射角色到J2EE用户和组
声明方法许可
定义角色后,就可以定义企业Bean的方法许可。方法许可指示什么角色被允许调用什么方法。
在deploytool部署工具中通过映射角色到方法指定方法许可:
1. 选中企业Bean节点
2. 选中Security页签
3. 在Method Permissions表格中,在Availability列的下拉列表中选中Sel Roles
4. 在该列后的对应被允许调用该方法的角色列中选中它的复选框(没有角色Availability列后就没有列了)
使用编程安全机制
编程安全机制由getCallerPrincipal方法和isCallerInRole方法构成。GetCallerPrincipal方法决定企业Bean的调用者,isCallerInRole方法获得调用者的角色。
GetCallerPrincipal方法在EJBContext接口中定义,该方法返回标志企业Bean调用者身份的java.security.Principal对象,(在这种情况下,一个principal就等同于一个用户),下面的例子中getUser方法通过调用该方法返回调用它的J2EE用户名。
public String getUser() {
   return context.getCallerPrincipal().getName();
}
 调用isCallerInRole方法可以确定企业Bean的调用者是否属于某个特定角色:
boolean result = context.isCallerInRole("Customer");
不受保护的EJB层资源
缺省情况下,J2EE SDK将ANYONE角色赋给方法。不被信任的用户guest属于ANYONE角色。然而如果你不为方法绑定角色,任何用户将都可以访问企业Bean的方法。
五.应用程序客户端层安全
J2EE应用程序客户端的验证需求和其他J2EE组件的验证需求一样,访问受保护资源需要通过用户验证,非保护资源则不需要。
应用程序客户端可以使用Java验证和授权服务(Java Authentication and Authorization Service ,JAAS)来建立验证机制。JAAS实现了Java版的标准可插入安全模块(Pluggable Authentication Module ,PAM)体系,该体系使应用程序独立于底层的安全技术实现。你可以为应用程序插入一个新的或者更换原有的验证技术而不需要对应用程序本身做任何修改。应用程序通过实例化一个LoginContext对象来激活验证处理,该对象轮流引用决定验证技术的验证配置或者用来执行验证功能的登录模块。
一个典型的登录模块提示用户输入用户名和密码并作校验。其他模块甚至可以读取和校验声音或者指纹样本。
有些情况下,登录模块需要和用户通信以获得验证信息。它使用javax.security.auth.callback.CallbackHandler接口来达到该目的,应用程序实现该接口并将其对象传递给登录上下文,该上下文直接将收到的对象交给底层的登录模块。登录模块用回调处理机制收集用户输入(例如密码和智能卡PIN号码等)也给用户返回信息(如状态信息)。通过允许应用程序定义回调处理,可以使底层登录模块和不同的应用程序与用户交互的方法保持独立。
例如回调处理可以被实现为GUI应用程序显示一个窗口要求用户输入,或者被实现为一个命令行工具简单的提示用户直接在命令行中输入。
登录模块传递适当的回调实现的数组给回调处理器的handle方法(例如为用户名提供的NameCallback实现和为密码提供的PasswordCallback实现),回调处理器被请求用户的交互并给回调设置相应的值。例如,对于NameCallback,CallbackHandler也许提示需要用户名,并从用户获取,然后调用NameCallback的setName方法存放该用户名。
确定应用程序客户端的回调处理机制
在deploytool部署工具中按一下步骤配置应用程序的回调处理机制:
1. 选中应用程序客户端的JAR节点
2. 选中General页签
3. 在CallbackHandler Class下来列表中选定用来收集用户验证数据的CallbackHandler实现类
六.EIS(Enterprise Information System)层安全
在EIS层,一个应用程序组件请求一个EIS资源的连接,EIS需要对该资源的一个契约作为该连接的一部分。应用程序组件提供者有两种方式来设计EIS连接契约:
  容器管理的契约,应用程序让容器来负责配置和管理EIS契约。容器保存建立EIS连接的用户名和密码。
  组件管理的契约,应用组件编码管理EIS契约,包括编码实现EIS契约建立过程
组件提供者可以使用deploytool部署工具来选择企业类型
配置契约
按一下步骤在deploytool部署工具中配置契约类型:
1. 选择需要使用EIS连接的组件
2. 选中Resource Refs页签
3. 点击Add按钮
4. 如果实容器管理的契约,在Authenticatication下拉列表中选中Container。如果是组件管理的契约则选中Application。
容器管理的契约
使用容器的契约,应用程序组件不需要传递任何安全验证信息给连接该资源的getConnection()方法。安全验证信息由容器提供,如下面的例子:
// Business method in an application component
Context initctx = new InitialContext();
 
// perform JNDI lookup to obtain a connection factory
javax.resource.cci.ConnectionFactory cxf =
     (javax.resource.cci.ConnectionFactory)initctx.lookup(
      "java:comp/env/eis/MainframeCxFactory");
 
// Invoke factory to obtain a connection. The security
// information is not passed in the getConnection method
javax.resource.cci.Connection cx = cxf.getConnection();
...
组件管理的契约
使用组件管理的契约,应用程序组件负责安全验证信息给连接资源的getConnection方法。安全验证信息一般是用户名和密码,如下面的例子:
// Method in an application component
Context initctx = new InitialContext();
 
// perform JNDI lookup to obtain a connection factory
javax.resource.cci.ConnectionFactory cxf =
     (javax.resource.cci.ConnectionFactory)initctx.lookup(
       "java:comp/env/eis/MainframeCxFactory");
 
// Invoke factory to obtain a connection
com.myeis.ConnectionSpecImpl properties = //..
 
// get a new ConnectionSpec
properties.setUserName("...");
properties.setPassword("...");
javax.resource.cci.Connection cx =
  cxf.getConnection(properties);
...
配置资源适配器安全
除了为EIS资源配置契约,还要配置资源适配器的安全机制。步骤如下:
1. 选中资源适配器RAR(Resource Adapter Archive)节点
2. 选中Security页签,在验证机制方框中选择该资源适配器支持的验证机制
  Password:EIS连接需要验证用户名和密码
  Kerberos Version 5.0:该资源适配器支持Kerberos验证机制(参考RFC-1510,The Kerberos Network Authentication Service(v5),该规范可以在http://www.ietf.org/rfc/rfc1510.txt.找到)
也可以选择0到多种验证机制。如果没有选择任何验证机制,则不支持任何安全验证。
3. 如果资源适配器支持队以存在的EIS连接进行再次验证,选中Reauthentication Supported。应用服务器在和当前连接的安全上下文环境不同的上下文中调用getConnection()方法时也将使用再次验证功能。
4. 在Security Permissions方各种,点击Add按钮添加运行环境中资源适配器需要访问系统资源的安全许可。这里只需要指定缺省安全许可以外的许可,要了解缺省安全许可请查看J2EE连接体系规范1.0(J2EE Connector Architecture Specification 1.0)的11.2节的表2。
5. 对每一个安全许可,点击最后一列的纸页图标为该许可输入描述文字。
删除许可只需选中该许可,然后点击Delete按钮。
七.传递安全身份
当你部署企业Bean或者Web组件的时候,可以指定在企业Bean和调用它的组件间可传递的安全身份。如图15-1:
 
图15-1 安全身份的传递
 你可以选择下列传递方式之一:
  中介组件(调用者)的安全身份被传递给目的企业Bean(被调用者)。只有目的容器信任中介容器才能使用该技术。
  一个特定安全身份被传递给目的企业Bean。当目的容器期望访问通过一个特定的安全身份时使用该技术。
配置组件使用的传递安全身份
 使用deploytool部署工具选择从企业Bean或者Web组件传递的安全身份。
配置企业Bean或者Web组件可传递组件运行时的调用者安全身份(调用其他组件时的安全身份)的步骤:
1. 选中该组件
2. 选中Security页签
3. 在Security Indentity方框中,选中Use Caller ID单选钮(表示组件将使用调用者的安全身份ID)
配置组件传递特定安全身份步骤:
1. 选中组件
2. 选中Security页签
3. 在Security Indentity方框中,选中Run As Specified Role单选钮
4. 在下拉列表中选中指定的角色
5. 点击Deployment Settings指定该角色的一个用户
6. 在Run As Specified User列表中选择用来调用企业Bean的用户名
7. 点击OK确定
配置客户端验证机制
当应用程序组件在客户端容器中需要访问受保护的Bean方法的时候使用客户端验证。
在deploytool部署工具中配置客户端验证机制的步骤如下:
1. 选择目的企业Bean
2. 选中Security页签
3. 点击Deployment Settings弹出Security Deployment Settings对话框
4. 选中SSL Required复选框激活SSL
5. 在Client Authentication方框中,选中Certificate。该选项使客户端如期望的为服务器作自我验证。(select Certificate as the method by which the server expects the client to authenticate itself to the server.)
6. 点击OK按钮确定。
容器间的信任机制
当企业Bean被设计成不管是用原始调用者的身份或者指定的身份调用目的Bean时,目的Bean只能得到被传递的安全身份时,它将无法得到任何验证数据。
我们没有方法是目的容器验证被传递的安全身份。然而,因为安全身份被用来做授权检查(例如方法许可和isCallerInRole方法),所以安全身份的可信度至关重要。又由于传递的安全身份没有可用的验证数据,所以目的组件必须信任调用者容器传递了一个已通过验证的安全身份。
缺省情况下,J2EE SDK服务器的配置信任来自各种不同容器的安全身份。因此,不必要用另外的步骤来设置信任关系。
八.J2EE用户、域和组
一个J2EE用户类似一个操作系统用户。很明显,两种用户都表示人。然而这两种用户并不相同,J2EE的验证服务并不知道你登录操作系统时的用户名和密码,因为该服务并不连接操作系统的安全机制。这两种安全服务管理属于不同域的用户。
域是指被相同的安全验证规则约束的用户集合。J2EE验证服务在两个域中管理用户:证书和缺省域。
证书通常在HTTPS协议中被用来验证Web浏览器客户端。为了在证书域中验证一个用户的身份,验证服务检验该用户的X.509证书。具体操作参考下一节安装服务器证书。X.509证书的公用名字段被用来作为principal名。
大多数情况下,J2EE验证服务通过缺省域验证用户身份,缺省域用来验证除Web浏览器客户端之外的所有用户。Web浏览器客户端使用HTTPS协议和证书验证。
一个缺省域的J2EE用户可以属于J2EE组(证书域的用户不能属于该组)。J2EE组是按职务或者消费水平等共有特点分类的用户类别。例如电子商务应用的大多数顾客可能都属于CUSTOMER用户组,而大客户却属于PREFERRED用户组。将用户分类到用户组使控制大量用户的访问权变的更简单。(EJB层安全一节讲述了如何控制企业Bean的用户访问。)
管理J2EE用户和组
本小节介绍如何使用deploytool部署工具实现如下功能:
  在缺省域中显示所有用户
  向缺省域中添加一个用户
  向证书域中添加一个用户
  删除一个用户
  为缺省域添加一个用户组(证书域不能添加用户组)
  从缺省域中删除一个用户组
显示缺省域或者证书域中的所有用户:
1. 中要添加用户或者组的目的服务器
2. 选择Tool'Server Configuration菜单显示Configuration Instrallation窗口
3. 在J2EE树节点下选中Users子节点
4. 在下拉列表中选择域类型(缺省Default或者证书Certificate)
向缺省域添加一个用户:
1.点击Add User按钮
2.输入用户名和密码
3.在Group Membership方框中从Available Groups列表中选择该用户将属于的组点击Add按钮添加到Groups列表中
4.点击OK按钮确定
为缺省域添加用户组:
1. 在Add User对话框中点击Edit Croups按钮
2. 在弹出的Groups窗口中点击Add
3. 在新行中输入用户组名
4. 点击OK确定
删除一个用户组只需在上述的Groups窗口中选中要删除的组点击Delete按钮,然后OK确定。
向证书域中添加一个用户
1. 选择Certificate证书域
2. 点击Add User按钮
3. 选择证书存放的目录
4. 选中证书文件名
5. 点击OK确定
九.安装服务器证书
证书被用来在HTTPS协议中验证验证Web客户端。除非服务器证书被安装,否则J2EE服务器不会激活HTTPS服务。安装服务器证书步骤如下:
1. 生成一对密匙和自签名证书
你可以用keytool工具创建证书。J2EE SDK携带的keytool工具和J2SE SDK携带的keytool工具用法相同。不过J2EE SDK版的keytool增加了执行RSA算法得Java加密扩展(Java Cryptogra Extension)支持。该功能允许你引入RSA签名的证书。
使用如下命令创建证书(用你的证书别名代替命令中的<certificate-alias>,用你的密匙存放文件名代替<keystore-filename>):
keytool -genkey -keyalg RSA -alias <certificate-alias> -keystore <keystore-filename>
2. keytool工具会提示你如下信息(我试运行了一下发现提示是中文的,这就是国际化了):
a) 输入keystore密码(Keystore password):输入密码(你可以使用changeit作为密码以和J2EE SDK的keystore密码保持一致)
b) 您的姓氏和名字是什么?(First and last name)[unknown]:输入你的服务器的有效全名,包括主机名和域名。
c) 您的组织单位名称是什么?(Organizational unit)[unknown]:输入组织单位名称
d) 您的组织名称是什么?(Organizational)[unknown]:输入组织名称
e) 您所在的城市或区域名称是什么?(City or locality)[unknown]:jinhua
f) 您所在的州或省份名称是什么?(State or province)[unknown]:zhengjiang
g) 该单位的两字母国家代码是什么(Two-letter country code)[unknown]:cn
h) 以上输入结束会让你确认上述输入是否正确,……[否]:输入y确认
i) 输入<certificate-alias>的主密码(如果和keystore密码相同,按回车)(key password for alias):password
3. 导入证书
如果你的证书用CA而不用Verisign签名,你必须导入CA证书(用Verisign Test CA签名也必须导入证书)。否则可以省略该步骤。导入证书步骤:
a) 从你的CA处申请CA证书,并保存到文件。
b) 用如下命令将该CA证书安装到Java 2平台标准版(Java 2 Platform, Standard Edition)(你必须有修改$JAVA_HOME/jre/lib/security/cacerts文件的权限):
keytool -import -trustcacerts -alias <ca-cert-alias> -file <ca-cert-filename>
4. 如果你想让你的证书具有CA的数字签名,步骤如下:
a) 生成一个证书签名请求(Certificate Signing Request ,CSR):
keytool -certreq -sigalg MD5withRSA -alias <cert-alias> -file <csr-filename>
b) 将<csr-filename>的内容发去签名。如果你使用Verisign CA,发送到http://digitalid.verisign.com/。Verisign将通过电子邮件发回签名证书。将该证书保存到文件。
c) 将电子邮件收到的签名证书导入服务器:
keytool -import -alias <cert-alias> -file <signed-cert-file>
 
第16章 资源连接
Dale Green著
Iceshape Zeng译

企业Bean和Web组件(WAR)都可以访问多种资源,包括数据库、邮件服务、JMS对象和URL资源等等。J2EE平台提供了访问这些资源的共同机制。本章介绍了J2EE平台下几种资源的连接方式,虽然都是一企业Bean为例,但是对Web组件也是一样的。
本章内容:
JNDI名和资源引用
 在deploytool中配置资源引用
数据库连接
 获得连接
 连接池
邮件会话连接
 运行ConfirmerEJB例子
URL资源连接
 运行HTMLReaderEJB例子

一.JNDI名和资源引用
JNDI是Java命名和目录接口的首字母缩写。J2EE平台通过JDNI名来定位提供服务(资源访问)的对象。
JNDI名是由J2EE服务器提供的命名和目录服务绑定到特定对象的用户友好的访问名称,(当然,是不是用户友好的取决于你的命名。)因为J2EE组件通过JNDI API来访问这些服务,所以我们通常称这些对象访问名为JNDI名。例如jdbc/Cloudscape是Cloudscape数据库的JNDI名,设置好后,J2EE服务器在启动的时候从配置文件里读取该信息,并自动将该JNDI名添加到名字空间。
我们并不是直接通过JNDI查找到资源访问对象的。查找得到的是连接工厂。连接工厂"生产"出资源访问对象。数据库资源的连接工厂是javax.sql.DataSource对象,它可以创建java.sql.Connection数据库连接对象。
在代码中,我们并不是接通过JNDI名来查找资源,而是资源引用。具体地说就是通过资源引用来查找资源工厂。资源引用是资源查找中lookup方法的实际参数(当然JNDI名也可以),它在部署描述符中指定。例如下面将提到的例子中的资源引用名为:jdbc/SavingsAccountDB(对应lookup方法的参数为java:comp/env/jdbc/SavingsAccountDB)。
JNDI名和资源引用名是不相同的,所以需要你建立两者之间的映射关系。但是它可以降低组件和资源之间的耦合,这样当组件需要访问不同的资源时,可以不用改变资源引用名。(这里的好处实际上并没有想象的那么好,不过如果两种资源是相同类型的资源,如都是上面的javax.sql.DataSource类型,则完全可以不更改代码,单是如果是不相同的类型,即使资源引用名不改变夜毫无意义,因为无疑你需要更改代码。)这种灵活性是你更容易在已有组件的基础上装配应用程序。(因为引用名是属于使用资源的组件的部署描述符元素,而JNDI名是资源的部署描述符元素,实际上这种做法避免了JNDI命名冲突。)
在deploytool中配置资源引用
这里使用的是第5章的SavingsAccountEJB的例子,详细信息参考第5章第一节。
指定资源引用
1. 在树中选中SavingsAccountEJB节点
2. 选择Resource Refs页签
3. 点击Add按钮
4. 在Coded Name列输入jdbc/SavingsAccountDB。在SavingsAccountBean中数据库的引用编码为:
private String dbName =
       "java:comp/env/jdbc/SavingsAccountDB";
java:comp/env是组件的JNDI上下文的名字(实际上这个上下文也作为一种资源来处理了,资源查找的过程可以是这样:jndictxt = ctxt.lookup("java:comp/env")然后用这个jndictxt来查找资源,ref = jndictxt.lookup("jdbc/SavingsAccountDB")。)jdbc/SavingsAccountDB是资源引用的JNDI名(The jdbc/SavingsAccountDB string is the JNDI name for the resource reference,这句话可能意味着资源引用实际上也跟资源一样处理成一种JNDI绑定对象了,但是实际上应该不是这样,因为在部署描述符中它是引用名元素。因为译者也不是高手,所以这里的具体实现细节有待读者自己研究了:)所以JDBC的DataSource对象的JNDI名就存储在java:comp/env/jdbc的上下文子对象中。(组件运行环境的上下文层次需要进一步了解)
5. 在Type列中选择javax.sql.DataSource。前面说过它是数据库连接工厂。
6. 在Authentication列选择Container。
7. 如果允许其他数据从DataSource中得到连接,选中Sharable复选框
配置完成,如图16-1
 
图 16-1 SavingsAccountEJB的Resource Refs 页签
映射资源引用名和JNDI名
1. 在树中选这SavingsAccountApp节点
2. 选择JNDI Names页签
3. 在References表里,选择资源引用的一行(Ref Type为Resource)。
4. 在JNDI Name列中输入JNDI名,本例为jdbc/Cloudscape。
这些工作也可以在SavingsAccountJAR节点的JNDI Names页签里完成。配置结束,如图16-2
 
图 16-2 SavingsAccountApp的JNDI Names 页签
二.数据库连接
企业Bean持久性机制决定了你是否要为处理数据库连接编写代码。不是采用CMP的企业Bean都必须自己编写连接处理的代码,包括BMP实体Bean和会话Bean。对于CMP实体Bean,deploytool工具会自动产生连接处理的代码。
获得连接
如何获得连接
以SaingsAccountBean为例:
1. 确定数据库名:
private String dbName =
      "java:comp/env/jdbc/SavingsAccountDB";
2. 获得数据库连接工厂:
InitialContext ic = new InitialContext();
DataSource ds = (DataSource) ic.lookup(dbName);
3. 从工厂获得连接:
Connection con =  ds.getConnection();
获得连接的时机
编写企业Bean时,你需要决定连接保持的时间。通常有两种选择:持续连接,在企业Bean的生命周期中一直保持连接或者在每一个数据库操作期间保持连接。临时连接,仅在每一个数据操作期间保持连接。这两种连接方式将决定数据操作方法的实现。
持续连接
你可以设计一个企业Bean,在它的整个生命周期中保持数据库连接。因为它连接数据库和断开数据库连接都只进行一次,所以它的方法得到连接都很简单。但是它有一个缺点:其他组件在该Bean的生命周期内就不能使用该连接(它独占了一个连接资源)。会话Bean和实体Bean在不同的方法中获得持续连接。
会话Bean
EJB容器在会话Bean生命周期开始的时候调用ejbCreate方法在结束的时候调用ejbRemove方法。所以会话Bean的持续连接在ejbCreate方法中获得,并在ejbRemove方法中断开。如果是有状态会话Bean你也必须在ejbActive中取得连接并在ejbPassivate中断开连接。有状态会话Bean之所以需要这些额外的连接和断开动作,是因为EJB容器在它的生命周期内可能会钝化它。在钝化期间,该有状态会话Bean被保存在二级存储器(硬盘)中,但是数据库连接不能保存在这里。而无状态会话Bean不会被钝化,所以它不需要在ejbActive和ejbPassivate方法中获得和断开数据库连接。关于激活和钝化的更多信息请参考有状态会话Bean的生命周期(第3章第8节)。在j2eetutorial/examples/ejb/teller目录下的TellerBean.java是使用持续连接的有状态会话Bean的例子。
BMP实体Bean
当实例化一个实体Bean并把它放入Bean池中的时候EJB容器调用setEntityContext方法。相对地,在当实体Bean从Bean池中被移除并成为垃圾收集器的目标时EJB容器调用unsetEntityContext方法。所以BMP实体Bean的持续连接在setEntityContext方法中获得并在unsetEntityContext方法中断开。实体Bean的生命周期参考图3-5(第3章第8节)。在j2eetutorial/examples/ejb/savingsaccount目录下的SavingsAccountBean.java是使用持续连接的实体Bean的例子。
临时连接
只在使用时暂时占有连接可以使许多组件共用同一个资源连接。因为EJB容器管理一个数据库连接池,企业Bean可以快速地获得和释放连接。例如,一个商业方法可以得到连接,插入一行数据,然后释放连接。在会话Bean中,商业方法连接数据库会启动一个事务以保证数据完整性。
用deploytool指定数据库用户名和密码
下面的方法不适合CMP实体Bean,关于CMP实体Bean的数据库连接方法请参考第6章CMP例子中的相关内容。这种Claudscape数据库连接不需要指定用户名和密码,因为权限验证已被分开成为另一项服务。更多信息请参考第15章。
然而,很多其它类型的数据库系统却需要提供用户名和密码来获得连接。对于这些数据库系统如果调用无参数的getConnection方法,那么必须在deploytool中指定数据库的用户名和密码:
1. 在树视图中选中企业Bean节点
2. 选择Resource Refs页签
3. 在Resource Factories Referenced in Code表格中选择对应的行,然后在页签下面的输入框中分别输入数据库的用户名和密码。
如果你想在程序中指定用户名和密码,就不需要deploytool来指定了,只是在或的数据库连接的时候要用它们作为getConnection方法的参数:
con = dataSource.getConnection(dbUser, dbPassword);
连接池
EJB容器管理数据库连接池,连接池对企业Bean是透明的。当企业Bean请求一个连 接时,容器从连接池中取出一个连接给企业Bean。因为连接早就建立好了,所以企业Bean可以更快的得到一个连接。因为企业Bean这样可以快速得到连接,所以可以在每次数据库操作结束后就马上释放连接。企业Bean占用一个连接只是很短的时间,因而同样的连接可以被很多的企业Bean使用。
三.邮件服务连接
如果你在某个网站上订购一件商品,就会收到一封确认订单的e-mail。本节的例子ConfirmBean类说明了如何用企业Bean来发送e-mail。该例子的源文件放在j2eetutorial/examples/src/ejb/confirmer目录下。进入j2eetutorial/examples目录用ant confirmer命令编译该例子,一个样本文件放在j2eetutorial/examples/ears目录下。
在ConfirmerBean的sendNotice方法中,lookup方法返回一个邮件会话对象。和数据库连接一样,邮件会话也是一种资源,和别的资源一样,需要将编码引用名(TheMailSession)关联到一个JNDI名。用该会话对象作为参数,sendNotice方法创建了一个空的Message对象,在调用Message对象的一些set方法后,sendNotice方法调用Transport类的send方法送这个消息对象上路了。源代码如下:
public void sendNotice(String recipient) {

   try {
       Context initial = new InitialContext();
       Session session =
         (Session) initial.lookup(
         "java:comp/env/TheMailSession");
      
       Message msg = new MimeMessage(session);
       msg.setFrom();

       msg.setRecipients(Message.RecipientType.TO,
          InternetAddress.parse(recipient, false));

       msg.setSubject("Test Message from ConfirmerBean");
 
       DateFormat dateFormatter =
         DateFormat.getDateTimeInstance(
         DateFormat.LONG, DateFormat.SHORT);

       Date timeStamp = new Date();
     
       String messageText = "Thank you for your order." + '/n' +
          "We received your order on " +
          dateFormatter.format(timeStamp) + ".";

       msg.setText(messageText);
       msg.setHeader("X-Mailer", mailer);
       msg.setSentDate(timeStamp);

       Transport.send(msg);

   } catch(Exception e) {
       throw new EJBException(e.getMessage());
   }
}
运行ConfirmerEJB例子
部署该应用程序
1. 在deploytool部署工具中打开j2eetutorial/examples/ears/ConfirmerApp.ear文件
2. 在Resource Refs页签中根据如下表格确定资源连接各项属性的值:
表16-1 Resource Refs页签属性值
属性 值
Coded Name TheMailSession
Type javax.mail.Session
Authentication Application
From (你的email地址)
Host 邮件服务器地址
User Name 连接邮件服务器的用户名

3. 部署该应用程序(Tools'Deploy菜单)。在Introduction对话框中确定Return Client JAR复选框被选中。
运行客户端
1.在终端窗口中进入j2eetutorial/examples/ears目录
2.将APPCPATH环境变量设置为ConfirmerAppClient.jar所在目录
3.运行如下命令(将<recipient>替换为邮件的目的地址):
  runclient -client ConfirmerApp.ear -name ConfirmerClient -textauth <recipient>
4. 用户名:guest。密码:guest123。
错误处理
如果应用程序无法连接邮件服务器将抛出如下异常:
javax.mail.MessagingException: Could not connect to SMTP host
 如果出现这个错误,请检查邮件服务器是否运行和邮件服务器地址是否正确(Resource Refs页签)。
四.URL资源连接
URL表明一个资源在Web中的位置。HTMLReaderBean类示例如何在企业Bean内部连接一个URL资源。该例子的源文件在j2eetutorial/examples/src/ejb/htmlreader目录下,进入j2eetutorial/examples目录执行ant htmlreader命令编译该例子。一个样本文件j2eetutorial/examples/ears在目录下。
HTMLReaderBean类的getContents方法返回包含HTML文件内容的字符串,该方法先查找跟url/MyURL引用名关联的java.net.URL对象,打开一个到该对象的连接,然后从InputStream对象中读出文件内容。在部署该应用程序之前,必须先将引用名(url/MyURL)和帮定该对象的JNDI名关联起来。GetContents方法的源代码如下:
public StringBuffer getContents() throws HTTPResponseException
{

   Context context;
   URL url;
   StringBuffer buffer;
   String line;
   int responseCode;
   HttpURLConnection connection;
   InputStream input;
   BufferedReader dataInput;
 
   try {
      context = new InitialContext();
      url = (URL)context.lookup("java:comp/env/url/MyURL"); 
      connection = (HttpURLConnection)url.openConnection();
      responseCode = connection.getResponseCode();
   } catch (Exception ex) {
       throw new EJBException(ex.getMessage());
   }

   if (responseCode != HttpURLConnection.HTTP_OK) {
      throw new HTTPResponseException("HTTP response code: " +
         String.valueOf(responseCode));
   }

   try {
      buffer = new StringBuffer();
      input = connection.getInputStream();
      dataInput =
          new BufferedReader(new InputStreamReader(input));
      while ((line = dataInput.readLine()) != null) {
         buffer.append(line);
         buffer.append('/n');
      } 
   } catch (Exception ex) {
       throw new EJBException(ex.getMessage());
   }
   return buffer;
}
运行HTMLReaderEJB例子
部署应用程序
1. 在deploytool部署工具中打开j2eetutorial/examples/ears/HTMLReaderApp.ear文件
2. 部署HTMLReaderApp应用程序。确定在Introduction对话框中选中了Return Client JAR复选框。
运行客户端
1. 在终端窗口中进入j2eetutorial/examples/ears目录
2. 设置APPCPATH环境变量为HTMLReaderAppClient.jar所在目录
3. 执行如下命令:
runclient -client HTMLReaderApp.ear -name HTMLReaderClient -textauth
4. 用户名:guest。密码:guest123。
5. 运行结果在J2EE SDK安装目录的public_html子目录下的index.html文件中

 
第17章 DUKE的银行应用程序
malongfa@etang.com
这一章我们讨论DUKE的银行应用程序,一个在线的银行应用程序.他有两个客户端,一个让管理员管理顾客和账号的j2ee应用程序客户端,一个让顾客访问账号历史和执行的交易信息的web客户端。顾客通过实体bean访问存储在数据库中的顾客,账号,和交易信息。DUKE银行应用程序向我们展示了我们在这本书中介绍的所有的组件-EJB,j2ee应用程序客户端和web组件是如何在一起协同工作以组成一个简单但又功能丰富的应用程序的。
 下面的图片是一个在高层次上的组件交互图。在这一章我们将详细讨论他们的类型,包括他们是如何编译,部署,和运行的。
 
   图   17-1 duke 银行应用程序

 

 

 

 

EJB
下图展示了客户端,EJB和数据库表之间的访问路径。正如下图所示,客户端应用程序仅仅只访问会话BEAN,在EJB之间的关系中,会话BEAN是实体BEAN的客户端。在应用程序的末端,实体BEAN通过访问数据库中的表存储实体的状态。

这些EJB的原代码位于j2eetutorial/bank/src/com/sun/ebank/ejb子目录。
 
   图:  17-2 duke 银行应用程序中的EJB
会话BEAN
DUKE的应用程序有三个会话BEAN,AccountControllerEJB,CustomerControllerEJB和TxControllerEJB(Tx代表一个业务交易,比如银行转账)这些会话BEAN向客户端提供了一个应用程序业务逻辑的视图。它们隐藏了服务器端执行业务逻辑,访问数据库,管理关系和检查错误的细节。
AccountControllerEJB
AccountControllerEJB的业务方法根据执行的任务可以分为几类:生成和删除实体BEAN,管理顾客和账号之间的关系,获得账号的信息。
 下面的两个方法生成和删除实体BEAN。
  createAccount
  removeAccount
AccountControllerEJB会话BEAN的这两个方法调用AccountEJB实体BEAN的create和remove方法。如果参数错误,createAccount和removeAccount方法将抛出应用程序级的异常。如果参数的类型不是Checking,Savings,Credit和Money Market, createAccount方法将抛出IllegalAccountTypeException异常。createAccount方法也通过调用CustomerEJB实体BEAN的方法findByPrimaryKey来确定特定的顾客是否存在,如果顾客不存在,createAccount方法抛出CustomerNotFoundException异常。
下面的方法管理账号和顾客之间的关系。

addCustomerToAccount
removeCustomerFromAccount
AccountEJB和CustomerEJB实体BEAN之间有着多对多的关系。一个账号可以被多个顾客使用,一个顾客也可以有多个账号。因为实体BEAN使用BMP(Bean 管理持久性关系),所以有多种方法处理这种关系。
在Duke的银行应用程序中,AccountControllerEJB会话BEAN的使用addCustomerToAccount和removeCustomerFromAccount方法管理账号和客户之间的关系。例如addCustomerToAccount方法开始先确定一个顾客是否存在。为了实现这种多对多的关系,addCustomerToAccount方法向数据库表customer_account_xref插入一行,在这个交叉引用的表中,每一行都包括相关实体的customerId和accountId字段。为了删除这种关系,removeCustomerFromAccount方法从customer_account_xref表中删除一行。
下面的方法得到有关账号的信息。
  getAccountsOfCustomer
  getDetails
AccountControllerEJB会话BEAN有两个get方法,getAccountsOfCustomer方法通过调用AccountEJB实体BEAN的findByCustomer方法返回一个给定顾客的所有账号,为了取代对AccountEJB的每一个变量(即与数据库表相对应的字段)都执行get方法,AccountControllerEJB会话BEAN通过一个getDetails方法返回一个封装了AccountEJB实体BEAN状态的对象(AccountDetails对象)。
CustomerControllerEJB
因为AccountControllerEJB会话BEAN管理顾客和账户之间的关系,所以CustomerControllerEJB会话BEAN相对简单一些。客户端通过调用CustomerControllerEJB会话BEAN的方法createCustomer创建一个顾客,通过调用removeCustomer删除一个顾客,它不仅调用CustomerEJB实体BEAN的remove 方法,还删除customer_account_xref表中包含相应顾客的所有行。

CustomerControllerEJB会话BEAN中有两个方法返回多个顾客,getCustomersOfAccount和getCustomersOfLastName,这两个方法调用CustomerEJB实体BEAN的相应的finder方法findByAccountId和findByLastName。

TxControllerEJB
TxControllerEJB会话BEAN处理银行交易。除了他的get方法getTxsOfAccount和getDetails,他还有几个方法用于改变一个账号中的余额。
  withdraw
  deposit
  makeCharge
  makePayment
  transferFunds
这些方法通过访问AccountEJB实体BEAN来确定账号的类型和设置账号中的余额。withdraw和deposit用于非信用卡的账号。makeCharge和makePayment用于信用卡账号。如果账号的类型不符合,这些方法抛出IllegalAccountTypeException异常。如果在取款后,账号中的余额为负数,withdraw则抛出InsufficientFundsException异常。在用信用卡支付中,如果超过了信用卡中的上限,makeCharge方法抛出InsufficientCreditException异常。

transferFunds方法不仅检查账号的类型还检查账号中的余额。如果需要,它抛出和withdraw,makeCharge方法相同的异常。transferFunds必须检查一个账号上的余额,并把它加到另一个账号上,这两步必须完成,因此transferFunds需要事务支持,如果其中的一步失败了,事务回滚,余额保持不变。

实体BEAN
在我们简单的小银行中,每一个业务实体在duke银行应用程序中都有一个对应的实体BEAN
  AccountEJB
  CustomerEJB
  TxEJB
这些实体BEAN的目的是为了提供account,customer,tx这几个数据库表的对象视图,对数据库表中的每一行,都有一个实体BEAN的实例变量与之对应。因为这些实体BEAN使用BMP,所以他们包含访问这些数据库表的SQL语句。例如CustomerEJB实体BEAN的create方法调用SQL语句的INSERT命令。

不像会话BEAN,这些实体BEAN的方法不验证参数,除了ejbCreate方法的主键参数。在设计阶段,我们决定在会话BEAN中验证参数,并抛出应用程序级的异常,例如CustomerNotInAccountException和IllegalAccountTypeException异常。因此,假如其他的应用程序使用这些实体BEAN,它的会话BEAN仍然必须验证方法的参数。
帮助类
在EJB的jar文件中包含了几个被EJB使用的帮助类,这些帮助类的源代码位于j2eetutorial/bank/src/com/sun/ebank/util目录下。下面的表格简单的表述了这些帮助类。

类名 描述
AccountDetails 封装了AccountEJB的实例状态,被AccountEJB和AccountControllerEJB的getDetails方法返回。
CodedNames 定义了在调用lookup方法中使用的字符串的逻辑名称,例如java:comp/env/ejb/account,EJBGetter类引用这些字符串。
CustomerDetails 封装了CustomerEJB的实例状态,被CustomerEJB和CustomerControllerEJB的getDetails方法返回
DBHelper 提供一些产生下一个主键的方法。例如getNextAccountId方法
Debug 提供一些简单的方法打印EJB的编译信息。如果j2ee server使用-verbose选项运行,这些信息出现在server的控制台上
DomainUtil 包含一些验证方法,例如getAccountTypes,checkAccountType,isCreditAccount。
EJBGetter 包含一些方法(通过调用lookup方法)定位并返回HOME接口。例如getAccountControllerHome
TxDetails 封装了TxEJB的实例状态,被TxEJB和TxControllerEJB的getDetails方法返回

       表      17-1 duke 应用程序EJB的帮助类
数据库表
在duke的银行应用程序中,数据库的表可根据他们的目的分类,一类代表业务实体,一类管理产生下一个主键。
代表业务实体的表
下图展示了数据库表之间的关系。customer和account 表之间有一个多对多的关系。一个顾客可能有多个账号,一个账号也可能被多个顾客所拥有。这个多对多的关系通过交叉表
customer_account_xref来实现。account和tx表有一个一对多的关系。在一个账号上可以进行多次业务交易,但是一次业务交易只能引用一个账号。
 
  图:          17-3 duke 应用程序中的数据库表

在图中我们使用了几个简写。PK代表主键(primary key)它的值唯一确定了数据库表中的一行。FK是外键的简写,这个字段是被引用的数据库表中的主键。Tx代表一个业务过程。例如取款和存款。
管理下一个主键的表
这些表有下面几个:
  next_account_id
  next_customer_id
  next_account_id
  next_tx_id
这些表的每一个中都有一个单独的列叫做id,他的值被传给实体BEAN的create方法。例如,在创建一个AccountEJB实体BEAN之前,AccountControllerEJB会话BEAN必须通过调用DBHelper类的getNextAccountId方法获得一个唯一的值。getNextAccountId从next_account_id表中读出id的值,并在数据库表中增加id的值,返回id。
保护EJB
在j2ee平台,你可以建立访问EJB方法的角色,相应的角色访问EJB相应的方法。在duke的银行应用程序中,根据他们的操作类型定义了两种角色,银行顾客和银行管理员。属于银行管理员角色的用户,可以执行管理功能:创建和删除一个账户,给一个账户增加或者删除顾客,设置信用卡的上限,设置初始账号的余额。属于银行顾客角色的用户,可以存款取款,转账等功能。注意:两个角色可执行的功能上不会有重叠。

通过在CustomerControllerEJb,AccountControllerEJB和TxControllerEJB会话BEAN的特定的方法上设置访问允许权,限制角色对这些方法的访问。例如,可以允许只有属于银行管理员角色的用户可以访问AccountControllerEJB的createAccount方法,可以拒绝属于银行顾客角色或其他角色的用户创建账号。为了查看是否设置了方法的允许权,在deploytool中的树状视图中找到CustomerControllerEJB,AccountControllerEJB和TxControllerEJb。对其中的每一个选择安全标签(Security tab)检查方法的允许权。
应用程序客户端
有时候企业应用程序有一个单独的应用程序客户端来处理一些例如系统和应用程序管理的任务。例如在duke的银行应用程序中通过一个j2ee应用程序客户端来手工管理顾客和账号。这样做在站点因为某种原因不能使用或者客户喜欢通过电话来交流事情,例如,改变账号的某些信息时,是十分有用处的。

一个j2ee应用程序客户端是一个通过命令行或者桌面启动的单独的应用程序。它访问运行在j2ee服务器上的EJB。

J2ee客户端应用程序通过一个swing用户界面来管理顾客和账号。如下图所示:银行管理员可以通过选择菜单执行下面的功能:
客户管理
  查看顾客信息
  增加新顾客
  更新顾客信息
  查找顾客的id(标志)
 
            图 :         17-4 应用程序客户端界面
账号管理
  增加一个新账号。
  给一个存在的账号增加新顾客
  查看账号的信息。
  删除账号
错误和一些信息出现在左边的application message watch(上图)面板的下面。数据显示在右边的面板上。
类和他们之间的关系
j2ee客户端应用程序被分为三个类BankAdmin,EventHandle,DataModel,这三个类之间的关系如下图(下一页)所示:

BankAdmin对象建立初始化的用户界面,创建EventHandle对象,并为EventHandle和DataModel对象提供调用的方法更新用户界面。

EventHandle对象监听用户按下的按钮,并根据按钮作相应的处理。创建DataModel对象,并调用DataModel的方法从底层的数据库中读写数据,并在处理结束时调用BankAdmin的方法更新用户界面。

DataModel对象从用户界面中检索数据,执行数据检查,并向数据库中写有效数据,或者从数据库中读数据。当数据库的读写成功时,根据对数据库的读写,调用BankAdmin类的方法更新用户界面。
BankAdmin类
创建用户界面的BankAdmin类,带有main方法,并提供一些受保护的方法供BankAdmin应用程序的其他类调用。
 
  图:             17-5 类之间关系图
main 方法
main方法创建BankAdmin和EventHandle类的实例,传递给main方法的参数用于确定相应的地区(即次应用程序在中国使用汉语运行,还是在英国使用英语运行),并被传递到BankAdmin的构造方法中。

public static void main(String args[]) {
   String language, country;
   if(args.length == 1) {
      language = new String(args[0]);
      currentLocale = new Locale(language, "");
   } else if(args.length == 2) {
      language = new String(args[0]);
      country = new String(args[1]);
      currentLocale = new Locale(language, country);
   } else
      currentLocale = Locale.getDefault();
   frame = new BankAdmin(currentLocale);
   frame.setTitle(messages.getString("CustAndAccountAdmin"));
   WindowListener l = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
         System.exit(0);
      }
   };
   frame.addWindowListener(l);
   frame.pack();
   frame.setVisible(true);
   ehandle = new EventHandle(frame, messages);
   System.exit(0);
   }
}
构造方法
构造方法用于建立初始的用户界面,包含一个菜单条和两个面板。菜单条包括customer和account菜单,左边的面板包含一个消息区,右边的面板是显示或者更新数据区。
类的方法
BankAdmin提供了一些更新用户界面的方法。这些方法的描述如下:
  clearMessages:清除出现在左边面板上的应用程序的消息。
  resetPanelTwo:当用户在点击ok表示显示或者更新数据结束时调用,重新设置右边的面板。
  createPanelTwoActLabels:当显示或者更新账号信息时为账号的字段创建标签。
  createActFields:当显示或者更新账号信息时创建账号字段。
  createPanelTwoCustLabels:当显示或者更新顾客信息时为顾客的字段创建标签。
  createCustFields:当显示或者更新顾客信息时创建顾客的字段。
  addCustToActFields:当在一个账号上添加顾客时创建标签和字段
  makeRadioButtons:当创建一个新的账号时创建单选按钮,选择创建账号的类型
  getDescription:创建单选按钮的标签用于描述账号的类型信息
EventHandle类
EventHandle执行ActionListener接口,这是一个用于处理行为事件的方法接口。像其他的用java 语言写的接口一样,ActionListener接口定义了一个方法集,但并没有实现它们。你必须根据应用程序的具体行为实现他们。
构造方法
构造方法检索ResourceBundle 和BankAdmin类的实例,并把他们赋值给自己的私有变量,这样子EventHandle就可以访问用户界面上的本地化后的文本并根据需要更新用户界面。
最后EventHandle的构造方法调用hookupEvents方法创建一个内在类监听和处理行为事件。

public EventHandle(BankAdmin frame, ResourceBundle messages) {
    this.frame = frame;
    this.messages = messages;
    this.dataModel = new DataModel(frame, messages);
    //Hook up action events
    hookupEvents();
}
 
actionPerformed方法
ActionListener接口只有一个方法actionPerformed方法。这个方法处理当用户创建一个新账号时,用户界面产生的行为事件。确切地讲:当银行管理员选择用户账号类型的单选按钮时,它设置用户账号类型的描述,当管理员按初始余额上的返回键时,它设置新账号的初始余额。
hookupEvents方法
hookupEvents方法使用内在类处理菜单和按钮的按下事件。一个内在类是一个类嵌在或者定义在另一个类中。使用内在类使代码更加模块化,更加容易阅读和维护。EventHandle的内在类管理下列应用程序客户端的操作。
  查看顾客信息
  创建新顾客
  更新顾客信息
  通过顾客的lastName查询顾客的Id
  查看账号信息
  创建新账号
  给一个账号增加一个客户
  删除账号
  当cancle按钮被按下时清除数据。
  当ok按钮被按下时处理数据。
DataModel类
DataModel类提供一些方法从数据库中读写数据,从用户界面检索数据,并在数据写入数据库之前检查数据的正确性。
构造方法
构造方法检索BankAdmin类的实例变量并把它赋值给自己的私有变量,所以当BankAdmin的checkActData, checkCustData,和 writeData方法检查到错误时,就可以在用户界面的面板上显示错误信息。他也收到一个ResourceBundle类的实例并把它赋值给自己的私有变量,以使它能够收到应用程序客户的本地化后的文本。

因为DataModel类和数据库进行交互,所以构造方法中也有一些代码用于建立和CustomerControllerEJB和AccountControllerEJB会话BEAN的远程接口的连接,并通过它们的远程接口创建它们的实例。
//Constructor
public DataModel(BankAdmin frame, ResourceBundle messages) {
   this.frame = frame;
   this.messages = messages;
//Look up and create CustomerController bean
    try {
      CustomerControllerHome customerControllerHome =
         EJBGetter.
         getCustomerControllerHome();
      customer = customerControllerHome.create();
    } catch (Exception NamingException) {
      NamingException.printStackTrace();
    }
//Look up and create AccountController bean
    try {
      AccountControllerHome accountControllerHome =
         EJBGetter.getAccountControllerHome();
      account = accountControllerHome.create();
    } catch (Exception NamingException) {
      NamingException.printStackTrace();
    }
}
方法
getData方法从用户界面的文本字段中检索数据,并使用String.trim方法除掉数据中的多余的控制字符,例如空格和回车字符。他有一个JTextFiled类型的参数,所以任何JTextField类的实例都可以被传送并处理。
private String getData(JTextField component) {
    String text, trimmed;
    if(component.getText().length() > 0) {
      text = component.getText();
      trimmed = text.trim();
      return trimmed;
    } else {
      text = null;
      return text;
    }
}
checkCustData方法存储从getData方法中得到的顾客的数据,但是它首先检查所有要求的字段必须有数据,中间的大写不能超过一个字符,状态不能超过两个字符。当一切都检查完毕,它调用writeData 方法。如果有错误,错误信息被打印在BankAdmin对象的用户界面上。checkActData使用类似的方法检查和存储账号的数据。

createCustInf和createActInf方法被EventHandle类调用,在查看,更新,创建事件中刷新面板2的显示信息。
创建顾客信息
  在查看和更新事件中,createCustInf方法从数据库中读出特定顾客的信息,并把他们传递给BankAdmin类的createCustFields方法。一个布尔型的变量被用来确定createCustFields方法是创建查看事件中的只读字段还是更新事件中的可写字段。
  在创建事件中,createCustInf方法通过空数据和一个布尔型的变量调用BankAdmin类的createCustFields创建一些空的可编辑的字段,供用户输入顾客的数据。
创建账号信息
  在查看和更新事件中,createActInf方法从数据库中读出特定账号的信息,并把他们传递给BankAdmin类的createActFields方法。一个布尔型的变量被用来确定createActFields方法是创建查看事件中的只读字段还是更新事件中的可写字段。
  在创建事件中,createActInf方法通过空数据和一个布尔型的变量调用BankAdmin类的createActFields创建一些空的可编辑的字段,供用户输入账号的数据。
  在一个账号上增加或者删除一个顾客,不需要创建任何用户界面组件,直接在数据库上操作。
Web客户端
在duke的银行应用程序中,顾客通过web 客户端访问账号信息并在账号上进行操作。下面的表格显示了web 客户端的功能和使用这些功能须访问的URL,以及执行这些功能的组件。
功能 URL的别名 Jsp页面 JavaBea组件
主页 /main main.jsp 
登陆和离开页面 /logon
/logonError
/logoff logon.jsp
logonError.jsp
logoff.jsp 
列出账号 /accountList accountList.jsp 
列出账号的历史 /accountHist accountHist.jsp AccountHistoryBean
在账号之间转账 /transferFunds
/transferAck transferFunds.jsp
transferAck.jsp TransferBean
取款存款 /atm
/atmAck atm.jsp
atmAck.jsp ATMBean
错误处理 /error error.jsp 
      
表:    17-2 web客户端

 


下面是显示账号历史的页面视图
 

      图    17-6  账号历史
设计策略
在duke的银行应用程序中,Jsp页面的主要工作是显示。一种开发可维护的Jsp页面的策略是减少嵌入Jsp页面的脚本,为了达到这个目的,许多动态的处理任务都由EJB,自定义标记(tag)和JavaBean组件完成。

在duke的银行应用程序中,Jsp页面使用EJB处理和数据库的交互。而且在和EJB交互的时候Jsp主要依赖JavaBean组件。在duke的书店应用程序中(参看第十章和十三章),JavaBean组件BookDB扮演了通向数据库的前端或者说一个通向EJB提供的接口的通道。在duke的银行应用程序中,TransferBean扮演着同样的角色。然而其他的JavaBean组件有着丰富的功能。ATMBean调用EJB的方法并根据顾客确定的输入设置字符串。AccountHistoryBean保存着从EJB中得到的顾客想看到的数据的信息。

Web客户端通过使用一个由自定义标记实现的模板机制维护着一个通用的几乎覆盖所有的Jsp页面的视图。模板机制由以下三个组件组成:
  template.jsp:定义了每一个屏幕的结构。他使用insert 标记插入子组件组成一个屏幕。
  screenDefinitions.jsp:定义了组成被每一个屏幕使用的子组件,每一个屏幕有相同的行为,但是有不同的标题和主体部分。
  dispatcher :一个servlet,用于处理请求并将请求转发给template.jsp。
最后,web客户端使用了下列三种逻辑标记iterate, equal, 和 notEqual--来自Struts 标记库。
Web客户端的生命循环
初始化web客户端的组件
BeanManager类担负着管理客户端使用的EJB的责任。它创建顾客,账号和控制EJB,并提供检索EJB的方法。

当初始化以后,BeanManager类从帮助类EJBGetter中检索每个EJb的home 接口,并通过调用home 接口的create方法创建他们的实例。因为这是一个应用程序级别的功能,所以当客户端初始化时,BeanManager类自身被ContextListener类创建并作为一个Context的属性保存。
public class BeanManager {
   private CustomerController custctl;
   private AccountController acctctl;
   private TxController txctl;
   public BeanManager() {
      if (custctl == null) {
         try {
            CustomerControllerHome home =
               EJBGetter.getCustomerControllerHome();
            custctl = home.create();
         } catch (RemoteException ex) {
            System.out.println("...");
         } catch (CreateException ex) {
            System.out.println();
         } catch (NamingException ex) {
            System.out.println();
      }
   }
   public CustomerController getCustomerController() {
      return custctl;
   }
   ...
}

public final class ContextListener
   implements ServletContextListener {
   private ServletContext context = null;
   ...
   public void contextInitialized(ServletContextEvent event) {
      this.context = event.getServletContext();
      context.setAttribute("beanManager",
         new BeanManager());
      context.log("contextInitialized()");
   }
   ...
}
 
请求处理
所有请求的URLs列在表18-2中,这些请求被映射到dispatcher web组件上,dispatcher web组件由dispatcher servlet 实现:
public class Dispatcher extends HttpServlet {
   public void doPost(HttpServletRequest request,
      HttpServletResponse response) {
   ...
   String selectedScreen = request.getServletPath();

   request.setAttribute("selectedScreen", selectedScreen);
   BeanManager beanManager = getServletContext().getAttribute(
      "beanManager");
   ...
   if (selectedScreen.equals("/accountHist")) {
      ...
   } else if (selectedScreen.equals("/transferAck")) {
      String fromAccountId =
         request.getParameter("fromAccountId");
      String toAccountId =
         request.getParameter("toAccountId");
      if ( (fromAccountId == null) || (toAccountId == null)) {
         request.setAttribute("selectedScreen", "/error");
         request.setAttribute("errorMessage",
            messages.getString("AccountError"));
      } else {
         TransferBean transferBean = new TransferBean();
         request.setAttribute("transferBean",
         transferBean);
         transferBean.setMessages(messages);
         transferBean.setFromAccountId(fromAccountId);
         transferBean.setToAccountId(toAccountId);
         transferBean.setBeanManager(beanManager);
         try {
            transferBean.setTransferAmount(new
               BigDecimal(request.
                  getParameter("transferAmount")));
            String errorMessage = transferBean.populate();
            if (errorMessage != null) {
               request.setAttribute("selectedScreen", "/error");
               request.setAttribute("errorMessage",
                  errorMessage);
            }
         } catch (NumberFormatException e) {
            request.setAttribute("selectedScreen", "/error");
            request.setAttribute("errorMessage",
               messages.getString("AmountError"));
         }
      }
      ...
      try {
      request.getRequestDispatcher("/template.jsp").
         forward(request, response);
      } catch(Exception e) {
      }
   }
}
当一个请求被送出的时候,dispatcher 执行下列步骤:
1. 在请求的selectedScreen 属性中存储请求的URL,只所以这么做是因为在以后请求被传送到应用程序的模板页的时候,URL可能会被修改。
2. 创建一个JavaBean组件,并把它作为请求的一个属性存储。
3. 解析验证请求参数,如果一个请求参数无效,dispatcher将重新设置请求的别名到错误处理页,否则,它初始化JavaBean组件。
4. 调用JavaBean的populate的方法,这个方法根据顾客的选择从EJB中检索并处理数据。
5. 将请求发送到template.jsp页面

正如早前提到的,template.jsp通过包含一些响应的子组件产生响应,如果请求是一个Get方法,子组件通常直接从EJB中检索数据,否则它从被dispatcher初始化的JavaBean组件中检索数据。

下图(18-7)显示了这些组件的交互:
 

   图:     17-7 web组件交互图
保护web资源
在j2ee平台中,在匿名访问web资源的时候,可以设置通过设置可以访问资源的安全角色保护资源。这就是所谓的安全限制,web容器授权只有那些属于在安全限制中的特定角色的用户才能访问资源。为了使容器使用安全限制的功能,应用程序必须确定一种验证用户身份的方式,web容器必须支持将一个角色映射到用户上。

在duke银行应用程序的web客户端,列在表18-2所有的URLs,都被限制只有属于BankCustomer安全角色的用户才能访问。应用程序通过基于表单(form-based)的登陆机制验证用户的身份。Web容器显示了基于表单验证登陆的URL   /logon,它被映射到logon.jsp,这个页面包含了一个要求用户输入身份标志(ID)和密码的表单,web容器检索这些信息,并把它们映射到安全角色上,验证用户所属的角色是否符合容器的安全限制。注解:为了使容器能够检查验证信息的有效性和执行映射功能,当在部署应用程序是必须执行以下两步:

1. 在web容器默认的域(realm)中增加顾客的组,ID(身份标志)和密码。
2. 映射BankCustomer角色到顾客或者顾客所在的组
一旦顾客通过验证,用户的身份标志(ID)将作为一个值去确定顾客的账号,顾客的身份标志从请求中按照下面的方法被检索到:
<% ArrayList accounts =
beanManager.getAccountController().getAccountsOfCustomer(
   request.getUserPrincipal().getName()); %>

国际化
duke 银行应用程序的j2ee应用程序客户端和web客户端是国际化的,所有显示在用户界面的字符串都是从资源绑定(resource bundles)中检索出来的。管理员使用的资源绑定是:AdminMessages_*.properties(属性文件,*号可以是cn-代表中国,en-代表英国等),web顾客使用的资源绑定是WebMessages_*.properties,客户端使用英语和西班牙语两种资源绑定。

应用程序客户端从命令行检查地区信息(即代表的国家),例如,使用西班牙语资源绑定,可以通过像下面的命令方式调用应用程序:

 runclient -client BankApp.ear -name BankAdmin es
应用程序客户端使用一个从命令行传递过来的Locale 类型的参数创建一个资源绑定:

//Constructor
public BankAdmin(Locale currentLocale) {
   //Internationalization setup
   messages = ResourceBundle.getBundle("AdminMessages",
      currentLocale);
web客户端Dispatcher 组件从请求中检索Locale 类的实例(即用户浏览器显示页面使用的语言),打开资源绑定,并把它作为会话(Session)的一个属性保存.

ResourceBundle messages = (ResourceBundle)session.
   getAttribute("messages");
   if (messages == null) {
      Locale locale=request.getLocale();
      messages = ResourceBundle.getBundle("WebMessages",
         locale);
      session.setAttribute("messages", messages);
   }

在web客户端,每一个Jsp页面通过会话(session)检索资源绑定:
<% ResourceBundle messages =
   (ResourceBundle)session.getAttribute("messages"); %>
并从中查找需要绑定的字符串,例如,下面是accountHist.jsp如何生成业务表的表头的
<td><b><%=messages.getString("TxDate")%></b></td>
<td><b><%=messages.getString("TxDescription")%></b></td>
<td><b><%=messages.getString("TxAmount")%></b></td>
<td><b><%=messages.getString("TxRunningBalance")%></b></td>
编译,打包,部署,运行应用程序:
为了编译应用程序,你必须下载和解压在前面下载例子中描述的tutorial bundle(tutorial文件包),当你安装了文件包后,duke银行应用程序的资源被放在j2eetutorial 下的下列目录结构中:
/bank
   /dd - deployment descriptors
      account-ejb.xml
      app-client.xml
      customer-ejb.xml
      runtime-ac.xml
      runtime-app.xml
      tx-ejb.xml
      web.xml
   /src
      /com - component classes
         /sun/ebank/appclient
         /sun/ebank/ejb
         /sun/ebank/web
      /web - JSP pages, images
   /sql - database scripts
      create-table.sql
      insert.sql
为了简化编译,打包和部署应用程序,tutorial文件包包含了部署描述,源代码,和一个包含可自动执行ant任务build.xml文件。如果你还没有运行过ant ,请看 如何编译和运行例子(How to build and run the Examples)

当你编译了源代码文件之后,产生的类文件放在j2eetutorial/bank/build子目录下。当你把组件和应用程序打包后,产生的归档(archive)文件放在j2eetutorial/bank/jar子目录下。
给域增加组和用户
为了运行j2ee应用程序客户端和web客户端,你必须给默认的安全域增加用户和组。按照下列步骤在deploytool中创造Customer和Admin组,增加用户200 到Customer组,增加用户 admin 到Admin 组:
1. 选择Tools Server Configuration
2. 在树中选择User 节点
3. 确定在域的组合匡中选定default
4. 点击Add User
5. 点击 Edit Groups
6. 点击Add.
7. 输入 Customer.
8. 点击Add.
9. 输入 Admin
10. 点击OK
11. 输入用户名200,密码j2ee
12. 从可选得组中选择Customer
13. 点击Add.
14. 点击Apply
15. 输入用户名admin,密码j2ee
16. 从可选得组中选择Admin
17. 点击Add.
18. 点击OK.

你也可以使用realmtool命令完成同样的功能:
1. realmtool -addGroup Customer
2. realmtool -add 200 j2ee Customer
3. realmtool -addGroup Admin
4. realmtool -add admin j2ee Admin

启动j2ee 服务器,部署工具和数据库
j2ee 服务器
启动j2ee 服务器

 j2ee -verbose

部署工具
在j2ee服务器启动报告结束时,运行部署工具
 1.如果部署工具还没有运行,运行下列命令
          deploytool
        2.如果部署工具已经在运行,连接到j2ee 服务器上
  a .选择File-〉Add Server
   b .在Add Server 的对话框中的Server Name一栏输入localhost
  c .点击OK
CloudScape
启动cloudscape数据库服务器

 cloudscape -start
编译EJB
在另外一个窗口,到turorial 的j2eetutorial/bank目录下,运行下列命令

 ant compile-ejb

给EJB打包
给EJB打包,运行下列命令

 ant package-ejb
这个命令将把类文件和部署描述打包成下面的EJB jar 文件,这些文件位于j2eetutorial/bank/jar 目录下
 
 account-ejb.jar
 customer-ejb.jar
 tx-ejb.jar

当给这些组件打包的时候,ant 可能会报告它找不到一个文件(例如account-ejb.jar)去删除,你可以忽略这些信息。
编译web客户端
为了编译web客户端,到j2eetutorial/bank目录,运行下面的命令

 ant compile-web

给web 客户端打包
web 客户端使用了struts标记库,在打包之前,必须从下面网址下载并安装Struts version 1.0:
http://jakarta.apache.org/builds/jakarta-struts/release/v1.0/

从jakarta-struts-1.0/lib目录下拷贝struts-logic.tld和struts.jar,到j2eetutorial/bank/jar目录下:并切换到j2eetutorial/bank目录运行命令:

 ant package-web

这个命令将servlet 类,jsp页面,JavaBean组件,标记库和web部署描述打包到web-client.war,并把它放在j2eetutorial/bank/jar 目录下.

编译j2ee应用程序客户端
为了编译j2ee应用程序客户端,到j2eetutorial/bank目录,运行下面命令:

 ant compile-ac

给j2ee 应用程序客户端打包
1. 到j2eetutorial/bank目录下,运行下面命令
             ant package-ac
这个命令在j2eetutorial/bank/jar目录下创建app-client.jar文件
2. 在同样的目录下,运行
             ant setruntime-ac
这个命令给app-client.jar增加一个运行时的部署描述文件(j2eetutorial/bank/dd/runtime-ac.xml)
给企业归档(archive)文件打包
1. 为了创建duke银行的企业归档文件,到j2eetutorial/bank目录,运行下面命令:
              ant assemble-app
这个命令在j2eetutorial/bank/jar目录下创建DukesBankApp.ear文件
2. 在同样的目录,运行命令:
               ant setruntime-app
这个命令给DukesBankApp.ear增加一个运行时的部署描述文件(j2eetutorial/bank/dd/runtime-app.xml)
打开企业归档文件
在部署(deploytool)工具中,按照下列步骤打开EAR 文件:
1. 选择 File Open.
2. 到j2eetutorial/bank/jar目录
3. 选择 DukesBankApp.ear.
4. 单机 Open
你将在部署工具中看到如图18-8的界面:
 
  图:      17-8应用结构和组件
检查jndi 名字
选中DukesBankApp,单击JNDI名字面板,每一列JNDI的名字如图18-9所示(下图):在顺序上可能有一点小小的不同:
           
 
             图:  17-9 JNDI 名字
JNDI名字是j2ee服务器寻找EJb和资源的名字。当你查找EJB的时候,你提供和下面类似的代码:实际的查询在代码的第三行调用com.sun.ebank.utilEJBGetter类的getCustomerControllerHome方法。EJBGetter是一个从com.sun.ebank.util.CodedNames类中检索JNDI名字的帮助类。在这个例子中,应用程序客户端查找CustomerController远程接口的编码名字(Coded Name)(在代码中使用的ejb JNDI的引用名字)。
try {
   customerControllerHome =
      EJBGetter.getCustomerControllerHome();
   customer = customerControllerHome.create();
} catch (Exception NamingException) {
   NamingException.printStackTrace();
}

public static CustomerHome getCustomerHome() throws
NamingException {
   InitialContext initial = new InitialContext();
   Object objref = initial.lookup(
      CodedNames.CUSTOMER_EJBHOME);
BankAdmin类引用ejb/customerController,这是在CodedNames类中定义的CustomerController远程接口的编码名字
JNDI名字存储在j2ee应用程序的部署描述中,j2ee服务器使用它来查找CustomerControllerEJB。在图18-9中,你看到CustomerControllerEJB的JNDI名字被映射为:ejb/customerController(即引用名字)。JNDI的名字是什么并不重要。但是一个EJB的JNDI的名字必须唯一(Coded Name,编码名字,可以有多个)。所以,看看表格你可以说应用程序客户端(BankAdmin)使用JNDI名字MyCustomerController来查找CustomerController远程接口。J2ee服务器使用MyCustomerController JNDI名字找到相对应的CustomerControllerEJB对象。

表的其他的行映射到其他的EJB,所有的这些BEAN都存储在Jar文件中,他们使用编码名字查找其他的BEAN和数据库的驱动。

数据库驱动的JNDI名字是jdbc/Cloudscape,这个名字在你安装j2eeSDK的时候配置文件中的默认的名字。

映射安全角色到组
为了映射BankAdmin角色到Admin组,BankCustomer角色到Customer 组:
1. 在 部署工具中(deploytool),选择 DukesBankApp
2. 在安全面板(security tab)中,从角色名字的列表中选择BankAdmin
3. 点击Add
4. 在用户组的对话窗口中,从组的名字列表中选择Admin
5. 点击OK
6. 在安全面板(security tab)中,从角色名字的列表中选择BankCustomer
7. 点击Add
8. 在用户组的对话窗口中,从组的名字列表中选择Customer
9. 点击OK
10. 从主菜单中选择File-〉Save
图18-10显示了被选中的BankCustomer角色和被映射到它上的Customer组。
部署duke银行应用程序
为了部署应用程序
1. 选择 DukesBankApp应用程序
2. 选择tool-〉deploy
3. 选中返回客户端Jar (return client jar)的复选框,默认的情况下返回Jar文件的目录和存储EAR文件的目录相同。默认的返回Jar文件的名字是应用程序的名字加上Client.jar,在这里是DukesBankAppClient.jar。
4. 点击结束(finish)

 
   图:17-10 BankCustomer 角色映射到Customer组
创建银行应用程序数据库
你必须创建并写入一些数据进数据库的表中,这样子EJB才能从中读写数据,为了创建和插入数据到数据库表,在终端的窗口(命令行窗口)中到j2eetutorial/bank目录,运行命令:
1. ant db-create-table
2. ant db-insert
运行j2ee应用程序客户端
为了运行j2ee应用程序客户端:
1. 在终端窗口(命令行窗口)中,到j2eetutorial/bank/jar目录下
2. 为DukesBankAppClient.jar设置APPCPATH环境变量。
3. 执行下面的命令,运行英语版的客户端
runclient -client DukesBankApp.ear -name BankAdmin
4. 执行下面命令运行西班牙版本,包括英语语言代码
runclient -client DukesBankApp.ear -name BankAdmin es
5. 参数DukesBankApp.ear是j2ee应用程序ear文件的名字,参数BankAdmin是客户端显示的名字,在login的提示符下,输入用户的名字admin,和密码j2ee,你看到的应用程序的界面如图18-11所示:
 
   图 17-11 BankAdmin j2ee应用程序客户端
运行web客户端
为了运行web客户端:
1. 在浏览器中输入http://<host>:8000/bank/main,如果你的j2ee服务器和浏览器在同一台机子上,请用localhost 代替<host>.为了看西班牙版本的应用程序,选择你的浏览器使用的语言为Spanish
2. 应用程序将显示登陆页面,在Customer Id中输入200,在Password 中输入j2ee,点击submit(提交)
3. 选择应用程序的功能Account List(账号列表), Transfer Funds(转账), ATM,或者Logoff(退出)。如果你有一系列的账号,通过选择账号的连接你可以得到账号的历史。
注解:当你第一次选择一个新页时,可能会碰到一个复杂的像账号历史一样的页面。显示的时候可能会花一些时间,因为j2ee服务器必须把这些页面转换成servlet类,编译,并装载他们
如果你选择Account List(账号列表),你将会看到像图18-12 一样的页面:
 
                图: 17-12账号列表

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值