JavaEE7-GlassFish4-应用服务器指南-全-
JavaEE7 GlassFish4 应用服务器指南(全)
原文:
zh.annas-archive.org/md5/ce4f768f28c26af59f024fab1a776597译者:飞龙
前言
Java 企业版 7,Java EE 的最新版本,在规范中添加了几个新特性。在这次规范的版本中,几个现有的 Java EE API 经历了重大改进;此外,还向 Java EE 添加了一些全新的 API。本书涵盖了最流行的 Java EE 规范的最新版本,包括 JavaServer Faces (JSF)、Java 持久化 API (JPA)、企业 JavaBeans (EJB)、上下文和依赖注入(CDI)、新的 Java JSON 处理 API (JSON-P)、WebSocket、完全重写的 Java 消息服务 API 2.0、Java XML Web 服务 API (JAX-WS)和 Java RESTful Web 服务 API (JAX-RS),以及 Java EE 应用程序的安全性。
GlassFish 应用服务器是 Java EE 的参考实现;它是市场上第一个支持 Java EE 7 的应用服务器。本书涵盖了最新版本的强大开源应用服务器 GlassFish 4.0。
本书涵盖内容
第一章,开始使用 GlassFish,解释了如何安装和配置 GlassFish。本书还解释了如何通过 GlassFish Web 控制台部署 Java EE 应用程序。最后,讨论了基本的 GlassFish 管理任务,例如通过添加连接池和数据源来设置域和数据库连接。
第二章,JavaServer Faces,涵盖了使用 JSF 开发 Web 应用程序,包括 HTML5 友好的标记和 Faces Flows 等新特性。它还涵盖了如何使用 JSF 的标准验证器以及通过创建自定义验证器或编写验证器方法来验证用户输入。
第三章,使用 JPA 进行对象关系映射,讨论了如何通过 Java 持久化 API 与关系数据库管理系统(RDBMS)如 Oracle 或 MySQL 交互开发代码。
第四章,企业 JavaBeans,解释了如何使用会话和消息驱动豆来开发应用程序。本章涵盖了主要 EJB 特性,如事务管理、EJB 定时服务和安全性。还涵盖了不同类型的企业 JavaBeans 的生命周期,包括解释如何在生命周期中的特定点自动由 EJB 容器调用 EJB 方法。
第五章,上下文和依赖注入,介绍了上下文和依赖注入(CDI)。本章涵盖了 CDI 命名豆、使用 CDI 进行依赖注入以及 CDI 限定符。
第六章, 使用 JSON-P 处理 JSON,介绍了如何使用新的 JSON-P API 生成和解析 JavaScript 对象表示法(JSON)数据。它还涵盖了处理 JSON 的两种 API:模型 API 和流式 API。
第七章,WebSockets,解释了如何开发基于 Web 的应用程序,这些应用程序在浏览器和服务器之间实现全双工通信,而不是依赖于传统的 HTTP 请求/响应周期。
第八章, Java 消息服务,介绍了如何在 GlassFish 中使用 GlassFish Web 控制台设置 JMS 连接工厂、JMS 消息队列和 JMS 消息主题。本章还讨论了如何使用完全重写的 JMS 2.0 API 开发消息应用程序。
第九章, 保护 Java EE 应用程序,介绍了如何通过提供的安全领域保护 Java EE 应用程序,以及如何添加自定义安全领域。
第十章, 使用 JAX-WS 的 Web 服务,介绍了如何通过 JAX-WS API 开发 Web 服务和 Web 服务客户端。还解释了使用 ANT 或 Maven 作为构建工具生成 Web 服务客户端代码。
第十一章, 使用 JAX-RS 开发 RESTful Web 服务,讨论了如何通过 Java API for RESTful Web services 开发 RESTful Web 服务,以及如何通过全新的标准 JAX-RS 客户端 API 开发 RESTful Web 服务客户端。它还解释了如何利用 Java API for XML Binding (JAXB)自动在 Java 和 XML 之间转换数据。
本书所需
需要安装以下软件才能遵循本书中的内容:
-
需要 Java 开发工具包(JDK)1.7 或更高版本
-
GlassFish 4.0
-
构建示例需要 Maven 3 或更高版本
-
一个 Java IDE,如 NetBeans、Eclipse 或 IntelliJ IDEA(可选,但推荐)。
本书面向的对象
本书假设读者熟悉 Java 语言。本书的目标市场是希望学习 Java EE 的现有 Java 开发人员以及希望将技能更新到最新 Java EE 规范的现有 Java EE 开发人员。
规范
在本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:"@Named类注解指定此 Bean 为 CDI 命名 Bean。"
代码块设置如下:
if (!emailValidator.isValid(email)) {
FacesMessage facesMessage = new FacesMessage(htmlInputText.
getLabel()
+ ": email format is not valid");
throw new ValidatorException(facesMessage);
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
<ejb>
<ejb-name>CustomerDaoBean</ejb-name>
<ior-security-config>
<as-context>
<auth-method>username_password</auth-method>
<realm>file</realm>
<required>true</required>
</as-context>
</ior-security-config>
</ejb>
任何命令行输入或输出都应如下所示:
$ ~/GlassFish/glassfish4/bin $ ./asadmin start-domain
Waiting for domain1 to start ........
新术语和重要词汇将以粗体显示。您在屏幕上看到的,例如在菜单或对话框中的文字,将以如下方式显示:“点击下一个按钮将您带到下一屏幕。”
注意
警告或重要提示将以如下框显示。
小贴士
小技巧和技巧将以如下方式显示。
读者反馈
我们始终欢迎读者的反馈。让我们知道您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。
如果您在某个领域有专业知识,并且对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已成为 Packt 图书的骄傲拥有者,我们有许多事情可以帮助您从购买中获得最大收益。
下载示例代码
您可以从您在www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进此书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误清单提交表单链接,并输入您的错误清单详情。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站,或添加到该标题的错误清单部分。任何现有的错误清单都可以通过从www.packtpub.com/support选择您的标题来查看。
侵权
在互联网上,版权材料的侵权问题是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供疑似侵权材料的链接。
我们感谢您在保护我们作者方面的帮助,以及我们为您提供有价值内容的能力。
问题
如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章. GlassFish 入门
在本章中,我们将讨论如何开始使用 GlassFish。以下是本章讨论的一些主题:
-
Java EE 和 GlassFish 概述
-
获取 GlassFish
-
安装和启动 GlassFish
-
解释 GlassFish 域的概念
-
部署 Java EE 应用程序
-
设置数据库连接
Java EE 和 GlassFish 概述
Java 企业版(Java EE,以前称为J2EE或Java 2 企业版)是一套用于服务器端 Java 开发的标准化技术。Java EE 技术包括JavaServer Faces(JSF)、企业 JavaBeans(EJBs)、Java 消息服务(JMS)、Java 持久化 API(JPA)、Java API for WebSocket、上下文与依赖注入(CDI)、Java API for XML Web Services(JAX-WS)、Java API for RESTful Web Services(JAX-RS)和Java API for JSON Processing(JSON-P)等。
存在着多个商业和开源应用服务器。Java EE 应用服务器允许开发者开发并部署 Java EE 兼容的应用程序,GlassFish 就是其中之一。其他开源 Java EE 应用服务器包括 Red Hat 的 WildFly(以前称为 JBoss)、Apache 软件基金会的 Geronimo 和 ObjectWeb 的 JOnAS。商业应用服务器包括 Oracle 的 WebLogic、IBM 的 WebSphere 和 Oracle 应用服务器。
GlassFish 是 Java EE 7 的参考实现;因此,它在市场上的任何其他应用服务器之前都实现了最新的 Java EE API。GlassFish 是开源的,并且可以免费获取,并且根据通用开发与分发许可证(CDDL)授权。
注意
您可以在opensource.org/licenses/CDDL-1.0了解更多关于 CDDL 许可证的信息。
与所有 Java EE 兼容的应用服务器一样,GlassFish 提供了必要的库,使我们能够开发并部署符合 Java EE 规范的 Java 应用程序。
Java EE 7 的新特性是什么?
Java EE 7 是 Java EE 规范的最新版本,它对该规范进行了多项改进和补充。以下各节列出了对规范的主要改进,这些改进对企业应用开发者来说很有兴趣:
JavaServer Faces (JSF) 2.2
Java EE 7 包括JavaServer Faces(JSF)规范的最新版本。JSF 2.2 包括以下显著的新特性:
-
JSF 2.2 具有 HTML5 友好的标记,也就是说,可以使用标准的 HTML 5 标记和 HTML 标签上的 JSF 特定属性来编写网页。
-
JSF 2.2 还包括 Faces Flows,它提供了一种封装相关页面并定义入口和出口点的方法。
-
资源库合约是 JSF 2.2 中引入的第三个主要 JSF 特性。资源库合约使我们能够轻松开发具有不同外观和感觉的 Web 应用程序,这些外观和感觉适用于不同的用户。
Java Persistence API (JPA) 2.1
JPA 被引入为 Java EE 规范第 5 版的标准部分。JPA 用作 Java EE 的标准对象关系映射框架,取代了实体 Bean。JPA 采用了来自第三方对象关系框架(如 Hibernate 和 JDO)的思想,并将它们作为标准的一部分。
JPA 2.1 引入了以下新特性:
-
JPA 2.1 引入了 转换器 的概念,它允许在数据库中存储的值和 Java 对象中存储的值之间进行自定义代码转换。例如,当处理数据库数据时,Java 代码中期望的值与数据库中存储的值通常不同。例如,值
1和0常常存储在数据库中以表示true和false。Java 有一个非常好的布尔类型,因此可以直接使用true和false。 -
JPA 查询 API 现在可以执行批量更新和删除。
-
JPA 2.1 现在支持存储过程。
-
JPA 2.1 引入了
@ConstructorResult注解,它允许从原生 SQL 查询中返回标准的 Java 类(但不是 JPA 实体)。
Java API for RESTful Web Services (JAX-RS) 2.0
JAX-RS 是一个用于开发 RESTful 网络服务的 Java API。RESTful 网络服务使用 表示状态转换(REST)架构。Java EE 6 将 JAX-RS 作为 Java EE 规范的官方部分。
JAX-RS 2.0 包含以下新特性:
-
JAX-RS 2.0 引入了一个新的客户端 API。虽然之前的 JAX-RS 版本使得开发 RESTful 网络服务变得容易,但每个实现都定义了自己的专有客户端 API。
-
扩展点、方法过滤器和实体拦截器也被引入到 JAX-RS 2.0 中。这些特性允许在开发 RESTful 网络服务时进行 面向方面编程(AOP)。
-
JAX-RS 2.0 还在服务器端和客户端 API 中引入了异步处理。
Java Message Service (JMS) 2.0
在 Java EE 7 中,Java 消息服务(JMS)API 已完全重写。之前的 JMS 版本需要大量的样板代码;使用新的重写的 JMS 2.0 API,这种情况不再存在。
Java API for JSON Processing (JSON-P) 1.0
JSON-P 是 Java EE 7 中引入的一个全新的 API。JSON-P 允许我们解析和生成 JSON(JavaScript 对象表示法)字符串。
Java API for WebSocket 1.0
传统的 Web 应用程序使用请求-响应模型,即客户端(通常是 Web 浏览器)请求资源,服务器提供响应。在这个模型中,通信始终由客户端发起。
WebSocket 作为 HTML5 规范的一部分被引入;它们在客户端和服务器之间提供全双工通信。
GlassFish 优势
在 Java EE 应用服务器中有这么多选项,为什么选择 GlassFish?除了 GlassFish 免费提供的明显优势外,它还提供以下好处:
-
Java EE 参考实现:GlassFish 是 Java EE 参考实现。这意味着其他应用服务器可能使用 GlassFish 来确保其产品符合规范。理论上,GlassFish 可以用来调试其他应用服务器。如果一个在另一个应用服务器上部署的应用程序表现不正常,但在 GlassFish 上部署时表现正常,那么很可能是不正常行为是由于其他应用服务器中的错误。
-
支持 Java EE 规范的最新版本:由于 GlassFish 是参考 Java EE 规范,它通常在市场上任何其他应用服务器之前实现最新规范。事实上,在撰写本文时,GlassFish 是市场上唯一支持完整 Java EE 7 规范的应用服务器。
获取 GlassFish
GlassFish 可以从glassfish.java.net下载。
注意
GlassFish 4.0 也捆绑了 7.4 或更新的 NetBeans IDE 版本。
一旦到达那里,你将看到一个如下截图所示的窗口:

点击下载链接将带我们到一个提供多个选项的向导页面,以下载 GlassFish,如下截图所示:

下载页面有多个选项;我们可以获取完整的 Java EE 平台或 Web 配置文件。我们还可以下载 GlassFish 作为压缩的 ZIP 文件或作为我们选择的操作系统的本地安装程序。
为了能够跟随本书中的所有示例,我们需要下载 GlassFish 的完整 Java EE 平台版本。我们将下载压缩的 ZIP 文件版本,因为安装说明在所有操作系统上都非常相似;如果您更喜欢,可以自由下载特定平台的安装程序。
安装 GlassFish
我们将使用 ZIP 安装程序来演示安装过程。此安装过程适用于所有主流操作系统。
安装 GlassFish 是一个简单的过程;然而,GlassFish 假设您的系统中存在某些依赖项。
GlassFish 依赖项
为了安装 GlassFish 4,必须在您的工作站上安装一个较新的Java 开发工具包(JDK)(需要 JDK 1.7 或更高版本),并且 Java 可执行文件必须在您的系统 PATH 中。最新的 JDK 可以从 www.oracle.com/technetwork/java/javase/downloads/index.html 下载。请参考 docs.oracle.com/javase/7/docs/webnotes/install/index.html 中您特定平台的 JDK 安装说明。
执行安装
一旦安装了 JDK,就可以通过以下截图所示简单地提取下载的压缩文件来开始 GlassFish 的安装:

注意
所有现代操作系统,包括 Linux、Windows 和 Mac OS X,都自带了提取压缩 ZIP 文件的支持;有关详细信息,请参阅您的操作系统文档。
解压 ZIP 文件后,将创建一个名为glassfish4的新目录。这个新目录包含我们的 GlassFish 安装。
启动 GlassFish
要从命令行启动 GlassFish,请将您的目录更改为 [glassfish 安装目录]/glassfish4/bin 并执行以下命令:
./asadmin start-domain domain1
注意
前面的命令以及本章中显示的大多数命令都假设使用 Unix 或 Unix-like 操作系统,如 Linux 或 Mac OS。对于 Windows 系统,初始的./是不必要的。
在执行前面的命令后不久,我们应该在终端底部看到类似以下的消息:
$ ~/GlassFish/glassfish4/bin $ ./asadmin start-domain
Waiting for domain1 to start ........
Successfully started the domain : domain1
domain Location: /home/heffel/GlassFish/glassfish4/glassfish/domains/domain1
Log File: /home/heffel/GlassFish/glassfish4/glassfish/domains/domain1/logs/server.log
Admin Port: 4848
Command start-domain executed successfully.
小贴士
下载示例代码
您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
然后,我们可以打开一个浏览器窗口,并在浏览器地址栏中输入以下 URL:
http://localhost:8080
如果一切顺利,我们应该看到一个页面,表明您的 GlassFish 服务器现在正在运行,如下面的屏幕截图所示:

小贴士
获取帮助
如果前面的任何步骤失败或需要关于 GlassFish 的一般帮助,一个很好的资源是 GlassFish 论坛 www.java.net/forums/glassfish/glassfish。
部署我们的第一个 Java EE 应用程序
为了进一步确认我们的 GlassFish 安装运行正常,我们将部署一个WAR(Web ARchive)文件,并确保文件能够正确部署和执行。在继续之前,请从本书的网站 www.packtpub.com 下载文件 simpleapp.war。
通过 Web 控制台部署应用程序
要部署simpleapp.war,打开浏览器并导航到http://localhost:4848。你应该会看到默认的 GlassFish 服务器管理页面,如下面的截图所示:

默认情况下,GlassFish 以开发模式安装。在这种模式下,访问 GlassFish 网络控制台不需要输入用户名和密码。在生产环境中,强烈建议配置网络控制台,使其受密码保护。
在这一点上,我们应该点击主屏幕下部署部分中的部署应用程序项。
要部署我们的应用程序,我们应该选择从 GlassFish 服务器可访问的本地打包文件或目录单选按钮,并输入我们的 WAR 文件的路径或通过点击浏览文件...按钮选择它。一旦完成,你将看到一个窗口,如下面的截图所示:

在我们选择了我们的 WAR 文件后,会显示一些输入字段,允许我们指定几个选项。对于我们的目的,所有默认值都很好。我们只需简单地点击页面右上角的确定按钮,如下面的截图所示:

一旦我们部署了应用程序,GlassFish 网络控制台将显示应用程序窗口,我们的应用程序作为已部署应用程序之一列出,如下面的截图所示:

要执行simpleapp应用程序,在浏览器地址栏中输入以下 URL:
http://localhost:8080/simpleapp/simpleservlet
生成的页面应如下面的截图所示:

就这样!我们已经成功部署了我们的第一个 Java EE 应用程序。
通过 GlassFish 管理控制台卸载应用程序
要卸载我们刚刚部署的应用程序,通过在浏览器中输入以下 URL 登录到 GlassFish 管理控制台:
http://localhost:4848
然后,要么在左侧导航面板中点击应用程序菜单项,要么在管理控制台主页上点击列出已部署应用程序项。
无论哪种方式,都应带我们到应用程序管理页面,如下面的截图所示:

应用程序可以通过简单地从已部署应用程序列表中选择simpleapp名称旁边的复选框,然后点击列表上方的卸载按钮来卸载。
一旦我们的应用程序被卸载,它将不再显示在应用程序管理页面上,如下面的截图所示:

通过命令行部署应用程序
前面的命令必须在[glassfish 安装目录]/glassfish4/bin路径下执行。
asadmin可执行文件也可以通过发出如下命令来卸载应用程序:
现在我们已经卸载了simpleappWAR 文件,我们准备使用命令行来部署它。要以这种方式部署应用程序,只需将simpleapp.war复制到[glassfish 安装目录]/glassfish4/glassfish/domains/domain1/autodeploy。只需将文件复制到该目录,应用程序就会自动部署。
我们可以通过查看服务器日志来验证应用程序是否已成功部署。可以通过输入[glassfish 安装目录]/glassfish4/glassfish/domains/domain1/logs/server.log来找到服务器日志。该文件上的最后几行应该看起来像以下内容:
[2013-08-02T10:57:45.387-0400] [glassfish 4.0] [INFO] [NCLS-DEPLOYMENT-00027] [javax.enterprise.system.tools.deployment.autodeploy] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1375455465387] [levelValue: 800] [[
Selecting file /home/heffel/GlassFish/glassfish4/glassfish/domains/domain1/autodeploy/simpleapp.war for autodeployment]]
[2013-08-02T10:57:45.490-0400] [glassfish 4.0] [INFO] [] [javax.enterprise.system.tools.deployment.common] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1375455465490] [levelValue: 800] [[
visiting unvisited references]]
[2013-08-02T10:57:45.628-0400] [glassfish 4.0] [INFO] [AS-WEB-GLUE-00172] [javax.enterprise.web] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1375455465628] [levelValue: 800] [[
Loading application [simpleapp] at [/simpleapp]]]
[2013-08-02T10:57:45.714-0400] [glassfish 4.0] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1375455465714] [levelValue: 800] [[
simpleapp was successfully deployed in 302 milliseconds.]]
[2013-08-02T10:57:45.723-0400] [glassfish 4.0] [INFO] [NCLS-DEPLOYMENT-00035] [javax.enterprise.system.tools.deployment.autodeploy] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1375455465723] [levelValue: 800] [[
[AutoDeploy] Successfully autodeployed : /home/heffel/GlassFish/glassfish4/glassfish/domains/domain1/autodeploy/simpleapp.war.]]
我们当然也可以通过导航到与通过 Web 控制台部署时相同的 URL 来验证部署:http://localhost:8080/simpleapp/simpleservlet。
一旦到达这里,应用程序应该能够正确执行。
以这种方式部署的应用程序可以通过简单地从autodeploy目录中删除工件(在我们的例子中是 WAR 文件)来卸载。删除文件后,我们应该在服务器日志中看到类似于以下的消息:
[2013-08-02T11:01:57.410-0400] [glassfish 4.0] [INFO] [NCLS-DEPLOYMENT-00026] [javax.enterprise.system.tools.deployment.autodeploy] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1375455717410] [levelValue: 800] [[
Autoundeploying application: simpleapp]]
[2013-08-02T11:01:57.475-0400] [glassfish 4.0] [INFO] [NCLS-DEPLOYMENT-00035] [javax.enterprise.system.tools.deployment.autodeploy] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1375455717475] [levelValue: 800] [[
[AutoDeploy] Successfully autoundeployed : /home/heffel/GlassFish/glassfish4/glassfish/domains/domain1/autodeploy/simpleapp.war.]]
asadmin 命令行实用程序
通过命令行部署应用程序的另一种方法是使用以下命令:
asadmin deploy [path to file]/simpleapp.war
注意
前面的命令必须在[glassfish 安装目录]/glassfish4/bin路径下执行。
我们应该在命令行终端看到以下确认信息,以告知我们文件已成功部署:
Application deployed with name simpleapp.
Command deploy executed successfully.
服务器日志文件应显示类似于以下的消息:
[2013-08-02T11:05:34.583-0400] [glassfish 4.0] [INFO] [AS-WEB-GLUE-00172] [javax.enterprise.web] [tid: _ThreadID=37 _ThreadName=admin-listener(5)] [timeMillis: 1375455934583] [levelValue: 800] [[
Loading application [simpleapp] at [/simpleapp]]]
[2013-08-02T11:05:34.608-0400] [glassfish 4.0] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=37 _ThreadName=admin-listener(5)] [timeMillis: 1375455934608] [levelValue: 800] [[
simpleapp was successfully deployed in 202 milliseconds.]]
可以使用asadmin可执行文件通过发出类似以下命令来卸载应用程序:
asadmin undeploy simpleapp
以下消息应在终端窗口的底部显示:
Command undeploy executed successfully.
请注意,文件扩展名不用于卸载应用程序,asadmin undeploy的参数应该是应用程序名称,默认情况下是 WAR 文件名(不带扩展名)。
通过命令行部署应用程序有两种方式——可以通过将我们要部署的工件复制到autodeploy目录,或者使用 GlassFish 的asadmin命令行实用程序来完成。
警惕的读者可能已经注意到autodeploy目录位于domains/domain1子目录下。GlassFish 有一个域的概念。域允许将相关应用程序一起部署。可以同时启动多个域。GlassFish 域的行为类似于单独的 GlassFish 实例;在安装 GlassFish 时,会创建一个默认域,称为domain1。
创建域
可以通过在命令行中发出以下命令来从命令行创建额外的域:
asadmin create-domain domainname
上述命令接受多个参数来指定域名将监听的服务(HTTP、Admin、JMS、IIOP、Secure HTTP 等)的端口号。在命令行中输入以下命令以查看这些参数:
asadmin create-domain --help
如果我们想在同一服务器上同时运行多个域名,这些端口号必须仔细选择,因为为不同的服务(或甚至跨域的相同服务)指定相同的端口号将阻止其中一个域名正常工作。
默认domain1域名的默认端口号列在以下表中:
| 服务 | 端口号 |
|---|---|
| Admin | 4848 |
| HTTP | 8080 |
| Java Messaging System (JMS) | 7676 |
| Internet Inter-ORB Protocol (IIOP) | 3700 |
| Secure HTTP (HTTPS) | 8181 |
| Secure IIOP | 3820 |
| Mutual Authorization IIOP | 3920 |
| Java Management Extensions (JMX) administration | 8686 |
请注意,在创建域名时,只需指定管理端口号。如果未指定其他端口号,将使用前面表中列出的默认端口号。在创建域名时必须小心,因为如上所述,如果任何服务在相同的端口上监听连接,则同一服务器上的两个域名不能同时运行。
创建域名的另一种方法,无需为每个服务指定端口号,是执行以下命令:
asadmin create-domain --portbase [port number] domainname
--portbase参数的值决定了域名的基准端口号;不同服务的端口号将是给定端口号的偏移量。以下表列出了分配给所有不同服务的端口号:
| 服务 | 端口号 |
|---|---|
| Admin | 端口号 + 48 |
| HTTP | 端口号 + 80 |
| Java Messaging System (JMS) | 端口号 + 76 |
| Internet Inter-ORB Protocol (IIOP) | 端口号 + 37 |
| Secure HTTP (HTTPS) | 端口号 + 81 |
| Secure IIOP | 端口号 + 38 |
| Mutual Authorization IIOP | 端口号 + 39 |
| Java Management Extensions (JMX) administration | 端口号 + 86 |
当然,在为--portbase选择值时必须小心,确保分配的端口号不会与其他任何域名冲突。
小贴士
通常情况下,使用大于 8000 且能被 1000 整除的端口号创建域名,应该能创建出不会相互冲突的域名。例如,使用端口号 9000 创建一个域名,另一个使用端口号 10000,依此类推,应该是安全的。
删除域名
删除域名非常简单。可以通过在命令行中执行以下命令来完成:
asadmin delete-domain domainname
我们应该在终端窗口看到以下信息:
Command delete-domain executed successfully.
小贴士
请谨慎使用上述命令。一旦删除了域名,它就不能轻易地被重新创建(所有已部署的应用程序以及任何连接池、数据源等都将消失)。
停止域名
可以通过执行以下命令来停止正在运行的域名:
asadmin stop-domain domainname
上述命令将停止名为domainname的域名。
如果只有一个域正在运行,则domainname参数是可选的,也就是说,我们可以简单地通过以下命令停止正在运行的域:
asadmin stop-domain
注意
本书将假设读者正在使用默认域domain1和默认端口进行工作。如果不是这种情况,给出的说明需要修改以匹配适当的域和端口。
设置数据库连接
任何非平凡的 Java EE 应用程序都将连接到关系数据库管理系统(RDBMS)。支持的 RDBMS 系统包括 Java DB、Oracle、Derby、Sybase、DB2、PointBase、MySQL、PostgreSQL、Informix、Cloudscape 和 SQL Server。在本节中,我们将演示如何设置 GlassFish 与 MySQL 数据库通信。对于其他 RDBMS 系统,该过程类似。
注意
GlassFish 附带一个名为 JavaDB 的 RDBMS。这个 RDBMS 基于 Apache Derby。为了限制本书代码所需的下载和配置,所有需要 RDBMS 的示例都将使用捆绑的 JavaDB RDBMS。本节中的说明是为了说明如何将 GlassFish 连接到第三方 RDBMS。
设置连接池
打开和关闭数据库连接是一个相对缓慢的操作。出于性能考虑,GlassFish 和其他 Java EE 应用程序服务器会保持一个打开的数据库连接池;当部署的应用程序需要数据库连接时,就会从池中提供一个;当应用程序不再需要数据库连接时,该连接就会返回到池中。
设置连接池的第一步是复制包含我们 RDBMS JDBC 驱动的 JAR 文件到域的lib目录中(查阅你的 RDBMS 文档以获取有关获取此 JAR 文件的信息)。如果我们想添加连接池的 GlassFish 域在复制 JDBC 驱动程序时正在运行,则必须重新启动域以使更改生效。可以通过执行以下命令来重新启动域:
asadmin restart-domain domainname
一旦 JDBC 驱动程序已复制到适当的目录,并且应用程序服务器已重新启动,请通过将浏览器指向http://localhost:4848来登录管理控制台。
然后,导航到资源 | JDBC | JDBC 连接池。浏览器现在应该类似于以下截图所示:

点击新建...按钮。在输入我们 RDBMS 的适当值后,页面主区域应类似于以下截图:

点击下一步按钮后,我们应该看到类似于以下截图的页面:

在前面截图显示的页面顶部大部分默认值都是合理的。滚动到页面底部,并输入我们 RDBMS(至少包括用户名、密码和 URL)的适当属性值。然后,点击屏幕右上角的完成按钮。
属性名称取决于我们使用的 RDBMS,但通常有一个 URL 属性,我们应该在其中输入数据库的 JDBC URL,以及用户名和密码属性,我们应该在其中输入数据库的认证凭据。
我们新创建的连接池现在应该可以在连接池列表中看到,如下面的截图所示:

在某些情况下,在设置新的连接池后,可能需要重新启动 GlassFish 域。
我们可以通过点击其池名称,然后在结果页面上的Ping按钮上启用,来验证我们的连接池是否成功设置,如下面的截图所示:

我们的数据源现在已准备好供我们的应用程序使用。
设置数据源
Java EE 应用程序不直接访问连接池;它们通过数据源访问,该数据源指向连接池。要设置新的数据源,点击左侧网页控制台下的资源菜单项下的JDBC图标,然后点击JDBC 连接池选项卡,然后点击新建...按钮。填写我们新数据源的相关信息后,网页控制台的主要区域应该看起来像以下截图所示:

点击确定按钮后,我们可以在以下截图所示中看到我们新创建的数据源:

摘要
在本章中,我们讨论了如何下载和安装 GlassFish。我们还讨论了通过 GlassFish 网页控制台、通过asadmin命令以及通过将文件复制到autodeploy目录来部署 Java EE 应用程序的几种方法。我们还讨论了基本的 GlassFish 管理任务,如设置域和通过添加连接池和数据源来设置数据库连接。在下一章中,我们将介绍如何使用 JSF 开发 Web 应用程序。
第二章:JavaServer Faces
在本章中,我们将介绍 Java EE 平台的标准组件框架 JavaServer Faces(JSF)。Java EE 7 包含了 JSF 2.2,这是 JSF 的最新版本。JSF 非常依赖于约定优于配置。如果我们遵循 JSF 约定,那么我们就不需要编写很多配置。在大多数情况下,我们甚至不需要编写任何配置。这一事实,加上 web.xml 自 Java EE 6 以来一直是可选的,意味着在许多情况下,我们可以编写完整的 Web 应用程序,而无需编写任何一行 XML 配置。
JSF 简介
JSF 2.0 引入了许多增强功能,使得 JSF 应用程序开发更加容易。在接下来的几节中,我们将解释其中的一些特性。
注意
对于不熟悉 JSF 早期版本的读者来说,可能无法完全理解以下几节。不用担心,到本章结束时,一切都会非常清晰。
Facelets
JSF 现代版本与早期版本之间一个明显的区别是,现在 Facelets 是首选的视图技术。JSF 的早期版本使用 JSP 作为它们的默认视图技术。由于 JSP 技术早于 JSF,有时使用 JSP 与 JSF 感觉不自然或产生问题。例如,JSP 的生命周期与 JSF 的生命周期不同;这种不匹配为 JSF 1.x 应用程序开发者引入了一些问题。
JSF 从一开始就被设计成支持多种视图技术。为了利用这一功能,Jacob Hookom 为 JSF 编写了一种特定的视图技术。他将自己的视图技术命名为 Facelets。Facelets 非常成功,以至于它成为了 JSF 的实际标准。JSF 专家组认识到 Facelets 的流行,并在 JSF 规范的 2.0 版本中将 Facelets 定为官方视图技术。
可选的 faces-config.xml
传统的 J2EE 应用程序遭受了一些人认为过度的 XML 配置。
Java EE 5 采取了一些措施来显著减少 XML 配置。Java EE 6 进一步减少了所需的配置,使得在 JSF 2.0 中 faces-config.xml JSF 配置文件成为可选的。
在 JSF 2.0 及更高版本中,可以通过 @ManagedBean 注解配置 JSF 管理器豆,从而无需在 faces-config.xml 中配置它们。Java EE 6 引入了 上下文和依赖注入(CDI)API,它提供了一种替代方法来实现通常使用 JSF 管理器豆实现的功能。截至 JSF 2.2,CDI 命名豆比 JSF 管理器豆更受欢迎。
此外,JSF 导航有一个约定。如果一个 JSF 2 命令链接或命令按钮的 action 属性值与 facelet(去掉 XHTML 扩展名)的名称匹配,那么按照约定,应用程序将导航到与动作名称匹配的 facelet。这个约定允许我们避免在 faces-config.xml 中配置应用程序的导航。
对于许多现代 JSF 应用程序,只要遵循已建立的 JSF 约定,faces-config.xml 就完全没有必要。
标准资源位置
JSF 2.0 引入了标准的资源位置。资源是页面或 JSF 组件需要正确渲染的工件,例如 CSS 样式表、JavaScript 文件和图像。
在 JSF 2.0 及更高版本中,资源可以放置在 resources 文件夹下的子目录中,这个文件夹位于 WAR 文件的根目录下或 META-INF 下。按照约定,JSF 组件知道它们可以从这两个位置之一检索资源。
为了避免资源目录杂乱,资源通常放置在子目录中。这个子目录通过 JSF 组件的 library 属性来引用。
例如,我们可以在 /resources/css/ 下放置一个名为 styles.css 的 CSS 样式表。
在我们的 JSF 页面中,我们可以使用 <h:outputStylesheet> 标签来检索此 CSS 文件,如下所示:
<h:outputStylesheet library="css" name="styles.css"/>
library 属性的值必须与我们的样式表所在的子目录匹配。
同样,我们可以在 /resources/scripts/ 下有一个名为 somescript.js 的 JavaScript 文件,并且我们可以使用以下代码来访问它:
<h:outputScript library="scripts" name="somescript.js"/>
我们可以在 /resources/images/ 下放置一个名为 logo.png 的图像,并且我们可以使用以下代码来访问这个资源:
<h:graphicImage library="images" name="logo.png"/>
注意,在每种情况下,library 属性的值都与 resources 目录下的相应子目录名称匹配,而 name 属性的值与资源的文件名匹配。
开发我们的第一个 JSF 应用程序
为了说明基本的 JSF 概念,我们将开发一个简单的应用程序,该应用程序由两个 Facelets 页面和一个名为 CDI 的命名豆组成。
Facelets
如我们在本章引言中提到的,JSF 2 的默认视图技术是 Facelets。Facelets 需要使用标准的 XML 编写。开发 Facelets 页面最流行的方式是结合使用 XHTML 和 JSF 特定的 XML 命名空间。以下示例显示了典型的 Facelets 页面看起来像什么:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
>
<h:head>
<title>Enter Customer Data</title>
</h:head>
<h:body>
<h:outputStylesheet library="css" name="styles.css"/>
<h:form id="customerForm">
<h:messages></h:messages>
<h:panelGrid columns="2"
columnClasses="rightAlign,leftAlign">
<h:outputLabel for="firstName" value="First
Name:">
</h:outputLabel>
<h:inputText id="firstName"
label="First Name"
value="#{customer.firstName}"
required="true">
<f:validateLength minimum="2" maximum="30">
</f:validateLength>
</h:inputText>
<h:outputLabel for="lastName" value="Last Name:">
</h:outputLabel>
<h:inputText id="lastName"
label="Last Name"
value="#{customer.lastName}"
required="true">
<f:validateLength minimum="2" maximum="30">
</f:validateLength>
</h:inputText>
<h:outputLabel for="email" value="Email:">
</h:outputLabel>
<h:inputText id="email"
label="Email"
value="#{customer.email}">
<f:validateLength minimum="3" maximum="30">
</f:validateLength>
</h:inputText>
<h:panelGroup></h:panelGroup>
<h:commandButton action="confirmation"
value="Save">
</h:commandButton>
</h:panelGrid>
</h:form>
</h:body>
</html>
以下截图说明了我们的示例页面在浏览器中的渲染方式:

当然,前面的截图是在每个文本字段输入一些数据之后拍摄的;最初,每个文本字段都是空的。
几乎任何 Facelets JSF 页面都会包括示例中展示的两个命名空间。第一个命名空间()用于渲染 HTML 组件的标签;按照约定,当使用此标签库时,使用前缀 `h`(代表 HTML)。
java The second namespace (``) is the core JSF tag library; by convention, the prefix `f` (for faces) is used when using this tag library.``
java ``The first JSF-specific tags we see in our example are the `<h:head>` and the `<h:body>` tags. These tags are analogous to the standard HTML `<head>` and `<body>` tags and are rendered as such when the page is displayed in the browser.``
java ``The `<h:outputStylesheet>` tag is used to load a CSS style sheet from a well-known location. (JSF standardizes the locations of resources such as CSS style sheets and JavaScript files; this will be discussed in detail later in the chapter.) The value of the `library` attribute must correspond to the directory where the CSS file resides (this directory must be under a `resources` directory). The `name` attribute must correspond to the name of the CSS style sheet we wish to load.``
java ``The next tag that we see is the `<h:form>` tag. This tag generates an HTML form when the page is rendered. As can be seen in the example, there is no need to specify an `action` or a `method` attribute for this tag; as a matter of fact, there is neither an `action` attribute nor a `method` attribute for this tag. The `action` attribute for the rendered HTML form will be generated automatically, and the `method` attribute will always be `"post".` The `id` attribute of `<h:form>` is optional; however, it is a good idea to always add it since it makes debugging JSF applications easier.``
java ``The next tag we see is the `<h:messages>` tag. As its name implies, this tag is used to display any messages. As we will see shortly, JSF can automatically generate validation messages. These will be displayed inside this tag. Additionally, arbitrary messages can be added programmatically via the `addMessage()` method defined in `javax.faces.context.FacesContext`.``
java ``The next JSF tag we see is `<h:panelGrid>`. This tag is roughly equivalent to an HTML table, but it works a bit differently. Instead of declaring rows and columns, the `<h:panelGrid>` tag has a `columns` attribute; the value of this attribute indicates the number of columns in the table rendered by this tag. As we place components inside this tag, they will be placed in a row until the number of columns defined in the `columns` attribute is reached, and then the next component will be placed in the next row. In the example, the value of the `columns` attribute is two. Therefore, the first two tags will be placed in the first row, the next two will be placed in the second row, and so forth.``
java ``Another interesting attribute of `<h:panelGrid>` is the `columnClasses` attribute. This attribute assigns a CSS class to each column in the rendered table. In the example, two CSS classes (separated by a comma) are used as the value for this attribute. This has the effect of assigning the first CSS class to the first column and the second one to the second column. Had there been three or more columns, the third one would have gotten the first CSS class, the fourth one would have gotten the second one, and so on, alternating between the first one and the second one. To clarify how this works, the next code snippet illustrates a portion of the source of the HTML markup generated by our example page:``
`<table> <tbody> <tr> **<td class="rightAlign">** **<label for="customerForm:firstName">** **First Name:** **</label>** **</td>** **<td class="leftAlign">** **<input id="customerForm:firstName" type="text"** **name="customerForm:firstName" />** **</td>** </tr> <tr> **<td class="rightAlign">** **<label for="customerForm:lastName">** **Last Name:** **</label>** **</td>** **<td class="leftAlign">** **<input id="customerForm:lastName" type="text"** **name="customerForm:lastName" />** **</td>** </tr> <tr> **<td class="rightAlign">** **<label for="customerForm:lastName">** **Email:** **</label>** **</td>** **<td class="leftAlign">** **<input id="customerForm:email" type="text"** **name="customerForm:email" />** **</td>** </tr> <tr> **<td class="rightAlign"></td>** **<td class="leftAlign">** **<input type="submit" name="customerForm:j_idt12"** **value="Save" />** **</td>** </tr> </tbody> </table>`
java ``Note how each `<td>` tag has an alternating CSS tag of "`rightAlign`" or "`leftAlign`". We achieved this by assigning the value "`rightAlign,leftAlign`" to the `columnClasses` attribute of `<h:panelGrid>`. The CSS classes we have used in our example are defined in the CSS style sheet we loaded via the `<h:outputStylesheet>` tag we discussed earlier. The IDs of the generated markup are a combination of the ID we gave to the `<h:form>` component plus the ID of each individual component. We didn't assign an ID to the `<h:commandButton>` component near the end of the page, so the JSF runtime assigned one automatically.``
java ``At this point in the example, we start adding components inside `<h:panelGrid>`. These components will be rendered inside the table rendered by `<h:panelGrid>`. As we have mentioned before, the number of columns in the rendered table is defined by the columns attribute of `<h:panelGrid>`. Therefore, we don't need to worry about columns (or rows); we have to just start adding components, and they will be inserted in the right place.``
java ``The next tag we see is the `<h:outputLabel>` tag. This tag renders an HTML `label` element. Labels are associated with other components via the `for` attribute, whose value must match the ID of the component that the label is for.``
java ``Next, we see the `<h:inputText>` tag. This tag generates a text field in the rendered page; its `label` attribute is used for any validation messages. It lets the user know which field the message refers to.``
`提示`
java ``Although it is not required for the value of the `label` attribute of `<h:inputText>` to match the label displayed on the page, it is highly recommended to use this value. In case of an error, this will let the user know exactly which field the message is referring to.``
java ``Of particular interest is the tag's `value` attribute. What we see as the value for this attribute is a **value-binding expression**. This means that this value is tied to a property of one of the application's named beans. In the example, this particular text field is tied to a property called `firstName` in a named bean called `customer`. When a user enters a value for this text field and submits the form, the corresponding property in the named bean is updated with this value. The tag's `required` attribute is optional, and valid values for it are `true` and `false`. If this attribute is set to `true`, the container will not let the user submit the form until the user enters some data in the text field. If the user attempts to submit the form without entering a required value, the page will be reloaded and an error message will be displayed inside the `<h:messages>` tag. The following screenshot shows the error message:``
``
java ``The preceding screenshot illustrates the default error message shown when the user attempts to save the form in the example without entering a value for the customer's first name. The first part of the message (**First Name**) is taken from the value of the `label` attribute of the corresponding `<h:inputTextField>` tag. You can customize the text as well as the style of the message (font, color, and so on). We will cover how to do this later in this chapter.``
`项目阶段`
java ``Having an `<h:messages>` tag on every JSF page is a good idea; without it, the user might not see the validation messages and will have no idea why the form submission is not going through. By default, JSF validation messages do not generate any output in the GlassFish log. A common mistake new JSF developers make is that they fail to add an `<h:messages>` tag to their pages. Without the tag, if the validation fails, then the navigation seems to fail for no reason. (The same page is rendered if the navigation fails, and without an `<h:messages>` tag, no error messages are displayed in the browser.)``
`为了避免前一段描述的情况,JSF 2.0 引入了 **项目阶段** 的概念。`
`以下是在 JSF 2 中定义的项目阶段:`
-
`生产` -
`开发` -
`单元测试` -
`系统测试`
java ``We can define the project stage as an initialization parameter to the faces servlet in the `web.xml` file or as a custom JNDI resource. Since `web.xml` is now optional and altering it makes it relatively easy to use the wrong project stage if we forget to modify it when we move our code from one environment to another, the preferred way of setting the project stage is through a custom JNDI resource.``
`使用 GlassFish,我们可以通过登录到 Web 控制台,导航到 **JNDI** | **自定义资源**,然后点击 **新建...** 按钮。出现的页面如图所示:`
``
`在生成的页面中,我们需要输入以下信息:`
| JNDI 名称 | javax.faces.PROJECT_STAGE |
|---|---|
| 资源类型 | java.lang.String |
java ``After you enter the preceding two values, the **Factory Class** field will be automatically populated with the value `org.glassfish.resources.custom.factory.PrimitivesAndStringFactory`.``
`输入值后,我们需要添加一个新的属性,其名称为阶段,其值对应于我们希望使用的项目阶段。`
`设置项目阶段允许我们在特定阶段运行程序时执行一些逻辑。例如,在我们的一个命名豆中,我们可能有如下代码:`
`FacesContext facesContext = FacesContext.getCurrentInstance(); Application application = facesContext.getApplication(); if (application.getProjectStage().equals( ProjectStage.Production)) { //do production stuff } else if (application.getProjectStage().equals( ProjectStage.Development)) { //do development stuff } else if (application.getProjectStage().equals( ProjectStage.UnitTest)) { //do unit test stuff } else if (application.getProjectStage().equals( ProjectStage.SystemTest)) { //do system test stuff }`
java ``As we can see, project stages allow us to modify our code's behavior for different environments. More importantly, setting the project stage allows the JSF engine to behave a bit differently based on the project stage setting. Relevant to our discussion, setting the project stage to Development results in additional logging statements in the application server log. Therefore, if we forget to add an `<h:messages>` tag to our page—our project stage is Development—and validation fails, a validation error will be displayed on the page even if we omit the `<h:messages>` component. The following screenshot shows the validation error message:``
``
`在默认的生产阶段,此错误消息不会在页面上显示,使我们困惑于为什么我们的页面导航似乎不起作用。`
`验证`
`JSF 提供了内置的输入验证功能。`
java ``In the previous section's example, note that each `<h:inputField>` tag has a nested `<f:validateLength>` tag. As its name implies, this tag validates that the entered value for the text field is between a minimum and maximum length. The minimum and maximum values are defined by the tag's `minimum` and `maximum` attributes. `<f:validateLength>` is one of the standard validators included in JSF. Just like with the `required` attribute of `<h:inputText>`, JSF will automatically display a default error message when a user attempts to submit a form with a value that does not validate.``
``
`同样,默认消息和样式可以被覆盖;我们将在本章后面的 *自定义 JSF 的默认消息* 部分介绍如何做到这一点。`
java ``In addition to `<f:validateLength>`, JSF includes other standard validators, which are listed in the following table:``
| 验证标签 | 描述 |
|---|---|
<f:validateBean> |
使用注解在命名豆中验证命名豆值,而无需在我们的 JSF 标签中添加验证器,这允许我们根据需要微调 Bean 验证。这些标签允许我们根据需要微调 Bean 验证。 |
<f:validateDoubleRange> |
此标签验证输入是否为在标签的 minimum 和 maximum 属性指定的两个值之间的有效 Double 值,包括这些值。 |
<f:validateLength> |
此标签验证输入的长度是否在标签的 minimum 和 maximum 值之间,包括这些值。 |
<f:validateLongRange> |
此标签验证输入是否为在标签的 minimum 和 maximum 属性指定的值之间的有效 Long 值,包括这些值。 |
<f:validateRegex> |
此标签验证输入是否与标签的 pattern 属性中指定的正则表达式模式匹配。 |
<f:validateRequired> |
此标签验证输入是否不为空。此标签等同于在父输入字段中将 required 属性设置为 true。 |
java ``Note that in the description for `<f:validateBean>`, we briefly mentioned Bean Validation. The Bean Validation JSR aims to standardize JavaBean validation. JavaBeans are used across several other API's that, up until recently, had to implement their own validation logic. JSF 2.0 adopted the Bean Validation standard to help validate named bean properties.``
`如果我们想利用 Bean 验证,我们只需要使用适当的 Bean 验证注解注释所需的字段,而无需显式使用 JSF 验证器。`
`注意`
java ``For the complete list of Bean Validation annotations, refer to the `javax.validation.constraints` package in the Java EE 7 API at [`docs.oracle.com/javaee/7/api/`](http://docs.oracle.com/javaee/7/api/).``
`组件分组`
java `` `<h:panelGroup>` is the next new tag in the example. Typically, `<h:panelGroup>` is used to group several components together so that they occupy a single cell in a `<h:panelGrid>` tag. This can be accomplished by adding components inside `<h:panelGroup>` and adding `<h:panelGroup>` to `<h:panelGrid>`. As can be seen in the example, this particular instance of `<h:panelGroup>` has no child components. In this particular case, the purpose of `<h:panelGroup>` is to have an "empty" cell and have the next component, `<h:commandButton>`, align with all other input fields in the form. ``
`表单提交`
java `` `<h:commandButton>` renders an HTML submit button in the browser. Just like with standard HTML, its purpose is to submit the form. Its `value` attribute simply sets the button's label. This tag's `action` attribute is used for navigation. The next page shown is based on the value of this attribute. The `action` attribute can have a `String` constant or a **method binding** **expression**, meaning that it can point to a method in a named bean that returns a `String` value. ``
java ``If the base name of a page in our application matches the value of the `action` attribute of an `<h:commandButton>` tag, then we navigate to this page when clicking on the button. This JSF feature frees us from having to define navigation rules, as we used to do in the older versions of JSF. In our example, our confirmation page is called `confirmation.xhtml`; therefore, by convention, this page will be shown when the button is clicked since the value of its `action` attribute ("`confirmation`") matches the base name of the page.``
`注意`
`尽管按钮的标签读作**保存**,但在我们的简单示例中,点击按钮实际上不会保存任何数据。`
`命名 bean`
`有两种类型的 JavaBeans 可以与 JSF 页面交互:JSF 管理 bean 和 CDI 命名 bean。JSF 管理 bean 自 JSF 规范的第一版以来就存在了,并且只能在 JSF 上下文中使用。CDI 命名 bean 在 Java EE 6 中引入,可以与其他 Java EE API(如企业 JavaBeans)交互。因此,CDI 命名 bean 比 JSF 管理 bean 更受欢迎。`
java ``To make a Java class a CDI named bean, all we need to do is make sure that the class has a public, no-argument constructor (one is created implicitly if there are no other constructors declared, which is the case in our example), and add the `@Named` annotation at the class level. The following code snippet is the managed bean for our example:``
`package net.ensode.glassfishbook.jsf; import javax.enterprise.context.RequestScoped; import javax.inject.Named; **@Named** @RequestScoped public class Customer { private String firstName; private String lastName; private String email; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }`
java ``The `@Named` class annotation designates this bean as a CDI named bean. This annotation has an optional `value` attribute that we can use to give our bean a logical name to use in our JSF pages. However, by convention, the value of this attribute is the same as the class name (`Customer`, in our case) with its first character switched to lowercase. In our example, we retain this default behavior; therefore, we access our bean's properties via the `customer` logical name. Notice the `value` attribute of any of the input fields in our example page to see this logical name in action.``
java ``Notice that other than the `@Named` and `@RequestScoped` annotations, there is nothing special about this bean. It is a standard JavaBean with private properties and corresponding getter and setter methods. The `@RequestScoped` annotation specifies that the bean should live through a single request.``
`命名 bean 始终有一个作用域。命名 bean 作用域定义了 bean 的生命周期,它由类级别的注解定义。下表列出了所有有效的命名 bean 作用域:`
| 命名 bean 作用域注解 | 描述 |
|---|---|
@ApplicationScoped |
应用程序作用域的命名 bean 的同一实例对所有应用程序的客户端都是可用的。如果一个客户端修改了应用程序作用域管理 bean 的值,该更改将在所有客户端中反映出来。 |
@SessionScoped |
每个会话作用域的命名 bean 实例都被分配给我们的应用程序的每个客户端。会话作用域的命名 bean 可以用来在请求之间保持客户端特定的数据。 |
@RequestScoped |
请求作用域的命名 bean 只存在于单个请求中。 |
@Dependent |
依赖作用域的命名 bean 被分配给它们注入的 bean 相同的范围。如果没有指定,这是默认的作用域。 |
@ConversationScoped |
会话作用域可以跨越多个请求,通常比会话作用域短。 |
`导航`
java ``As can be seen on our input page, when we click on the **Save** button in the `customer_data_entry.xhtml` page, our application will navigate to a page called `confirmation.xhtml`. This happens because we are taking advantage of the JSF's convention over configuration feature, in which if the value of the `action` attribute of a command button or link matches the base name of another page, then the navigation takes us to this page.``
`提示`
`**当你点击一个应该导航到另一个页面的按钮或链接时,页面会重新加载吗?**`
java ``When JSF does not recognize the value of the `action` attribute of a command button or command link, it will, by default, navigate to the same page that was displayed in the browser when the user clicked on a button or link that was meant to navigate to another page.``
java ``If navigation does not seem to be working properly, chances are there is a typo in the value of this attribute. Remember that by convention, JSF will look for a page whose base name matches the value of the `action` attribute of a command button or link.``
java ``The source for `confirmation.xhtml` looks as follows:``
`<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html > <h:head> <title>Customer Data Entered</title> </h:head> <h:body> <h:panelGrid columns="2" columnClasses="rightAlign,leftAlign"> <h:outputText value="First Name:"></h:outputText> <h:outputText value="#{customer.firstName}"></h:outputText> <h:outputText value="Last Name:"></h:outputText> <h:outputText value="#{customer.lastName}"></h:outputText> <h:outputText value="Email:"></h:outputText> <h:outputText value="#{customer.email}"></h:outputText> </h:panelGrid> </h:body> </html>`
java ``The `<h:outputText>` tag is the only tag on this page that we haven't covered before. This tag simply displays the value of its `value` attribute to the rendered page; its `value` attribute can be a simple string or a value binding expression. Since the value binding expressions in our `<h:outputText>` tags are the same expressions that were used in the previous page for the `<h:inputText>` tags, their values will correspond to the data that the user entered.``
``
java ``In traditional (that is, non-JSF) Java web applications, we defined URL patterns to be processed by specific servlets. Specifically for JSF, the suffixes `.jsf` or `.faces` were commonly used; another commonly used URL mapping for JSF was the `/faces` prefix. By default, GlassFish automatically adds the `/faces` prefix to the faces servlet; therefore, we don't have to specify any URL mappings at all. If, for any reason, we need to specify a different mapping, then we need to add a `web.xml` configuration file to our application. However, the default will suffice in most cases.``
java ``The URL we used for the pages in our application was the name of our Facelets page, prefixed by `/faces`. This takes advantage of the default URL mapping.``
java`` ````# 自定义数据验证 除了提供标准验证器外,JSF 允许我们创建自定义验证器。这可以通过两种方式完成:创建自定义验证器类或将验证方法添加到我们的命名豆中。 ## 创建自定义验证器 除了标准验证器外,JSF 允许我们通过创建实现 `javax.faces.validator.Validator` 接口的 Java 类来创建自定义验证器。 以下类实现了一个电子邮件验证器,我们将使用它来验证客户数据输入屏幕中的电子邮件文本输入字段。 java package net.ensode.glassfishbook.jsfcustomval; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.component.html.HtmlInputText; import javax.faces.context.FacesContext; import javax.faces.validator.FacesValidator; import javax.faces.validator.Validator; import javax.faces.validator.ValidatorException; import org.apache.commons.lang.StringUtils; @FacesValidator(value = "emailValidator") public class EmailValidator implements Validator { @Override public void validate(FacesContext facesContext, UIComponent uiComponent, Object value) throws ValidatorException { org.apache.commons.validator.EmailValidator emailValidator = org.apache.commons.validator.EmailValidator.getInstance(); HtmlInputText htmlInputText = (HtmlInputText) uiComponent; String email = (String) value; if (!StringUtils.isEmpty(email)) { if (!emailValidator.isValid(email)) { FacesMessage facesMessage = new FacesMessage(htmlInputText. getLabel() + ": email format is not valid"); throw new ValidatorException(facesMessage); } } } } `@FacesValidator` 注解将我们的类注册为 JSF 自定义验证器类。其 `value` 属性的值是 JSF 页面可以使用的逻辑名称。 如示例所示,在实现 `Validator` 接口时,我们只需要实现一个名为 `validate()` 的方法。此方法接受三个参数:`javax.faces.context.FacesContext` 的一个实例、`javax.faces.component.UIComponent` 的一个实例和一个对象。通常,应用程序开发人员只需关注后两个参数。第二个参数是我们正在验证数据的组件,第三个参数是实际值。在示例中,我们将 `uiComponent` 强制转换为 `javax.faces.component.html.HtmlInputText`;这样,我们可以访问其 `getLabel()` 方法,我们可以将其用作错误消息的一部分。 如果输入的值不是有效的电子邮件地址格式,则创建一个新的 `javax.faces.application.FacesMessage` 实例,将要在浏览器中显示的错误消息作为其构造函数参数传递。然后我们抛出一个新的 `javax.faces.validator.ValidatorException` 异常。错误消息随后在浏览器中显示。 ### 小贴士 **Apache Commons Validator** 我们的自定义 JSF 验证器使用 Apache Commons Validator 进行实际验证。此库包括许多常见的验证,如日期、信用卡号码、ISBN 号码和电子邮件。在实现自定义验证器时,值得调查是否已经存在我们可以使用的验证器。 为了在我们的页面上使用我们的验证器,我们需要使用 `<f:validator>` JSF 标签。以下 Facelets 页面是客户数据输入屏幕的修改版本。此版本使用 `<f:validator>` 标签来验证电子邮件。 java <h:head>
注意到 `<f:validator>` 标签的 `validatorId` 属性值与我们的自定义验证器中 `@FacesValidator` 注解的 `value` 属性值相匹配。 在编写我们的自定义验证器并修改我们的页面以利用它之后,我们可以在以下屏幕截图中看到我们的验证器在行动:  ## 验证器方法 另一种实现自定义验证的方法是将验证方法添加到应用程序的一个或多个命名豆中。以下 Java 类说明了 JSF 验证中使用验证器方法的使用: java package net.ensode.glassfishbook.jsfcustomval; import javax.enterprise.context.RequestScoped; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.component.html.HtmlInputText; import javax.faces.context.FacesContext; import javax.faces.validator.ValidatorException; import javax.inject.Named; import org.apache.commons.lang.StringUtils; @Named @RequestScoped public class AlphaValidator { public void validateAlpha(FacesContext facesContext, UIComponent uiComponent, Object value) throws ValidatorException { if (!StringUtils.isAlphaSpace((String) value)) { HtmlInputText htmlInputText = (HtmlInputText) uiComponent; FacesMessage facesMessage = new FacesMessage(htmlInputText. getLabel() + ": only alphabetic characters are allowed."); throw new ValidatorException(facesMessage); } } } 在此示例中,该类仅包含验证器方法。我们可以给我们的验证器方法起任何名字;然而,它的返回值必须是 void,并且它必须按照示例中所示按顺序接受三个参数。换句话说,除了方法名外,验证器方法的签名必须与在 `javax.faces.validator.Validator` 接口中定义的 `validate()` 方法的签名相同。 如我们所见,我们的验证器方法的主体几乎与我们的自定义验证器的 `validate()` 方法的主体相同。我们检查用户输入的值以确保它只包含字母字符和/或空格。如果不满足这些条件,则抛出 `ValidatorException`,传递一个包含适当的 `String` 错误消息的 `FacesMessage` 实例。 ### 小贴士 **StringUtils** 在示例中,我们使用了 `org.apache.commons.lang.StringUtils` 来执行实际的验证逻辑。除了示例中使用的该方法外,此类还包含几个用于验证字符串是否为数字或字母数字的方法。此类是 Apache `commons-lang` 库的一部分,在编写自定义验证器时非常有用。 由于每个验证器方法都必须在命名豆中,因此我们需要确保包含我们的验证器方法的类被 `@Named` 注解标记,如我们的示例所示。 为了使用我们的验证器方法,我们需要通过标签的 `validator` 属性将其绑定到我们的组件。执行此操作的代码如下: java <h:head> 由于姓名和姓氏字段都不会接受除字母字符或空格之外的内容,我们将我们的自定义验证器方法添加到这两个字段。 注意到 `<h:inputText>` 标签的 `validator` 属性值是一个 JSF 表达式语言表达式,它使用包含我们的验证方法的命名豆的默认名称。`alphaValidator` 是我们的命名豆的名称,`validateAlpha` 是我们的验证器方法的名称。 在修改我们的页面以使用自定义验证器之后,我们现在可以按照以下方式看到它在行动:  注意到对于 **First Name** 字段,我们的自定义验证器消息和标准长度验证器都执行了。 实现验证器方法的优点是您不需要创建一个仅用于单个验证器的整个类。(我们的示例就是这样做的,但在许多情况下,验证器方法被添加到包含其他方法的现有命名豆中。)验证器方法的缺点是每个组件只能由单个验证器方法进行验证。当使用验证器类时,可以在要验证的标签内部嵌套多个 `<f:validator>` 标签;因此,可以对字段执行多个验证,包括自定义和标准验证。 # 自定义 JSF 的默认消息 如我们之前提到的,可以自定义 JSF 默认验证消息的样式(字体、颜色、文本等)。此外,还可以修改默认 JSF 验证消息的文本。在以下部分中,我们将解释如何修改错误消息的格式和文本。 ## 自定义消息样式 可以通过 **Cascading Style Sheets** (**CSS**)来自定义消息样式。这可以通过使用 `<h:message>` 样式或 `styleClass` 属性来完成。当我们要声明内联 CSS 样式时,使用 `style` 属性。当我们要在 CSS 样式表中或在我们的页面中的 `<style>` 标签内使用预定义的样式时,使用 `styleClass` 属性。 以下标记说明了使用 `styleClass` 属性来更改错误消息样式的用法。这是我们在上一节中看到的输入页面的修改版本。 java <h:head> 与上一个页面相比,唯一的区别是使用了 `<h:messages>` 标签的 `styleClass` 属性。如前所述,`styleClass` 属性的值必须与在层叠样式表中定义的 CSS 样式的名称相匹配。 在我们的情况下,我们在 `style.css` 中为消息定义了一个 CSS 样式,如下所示: java .errorMsg { color: red; } 然后,我们将此样式用作 `<h:messages>` 标签的 `styleClass` 属性的值。 以下屏幕截图说明了在实施此更改后验证错误消息的外观:  在这个特定的例子中,我们只是将错误消息文本的颜色设置为红色,但我们只受 CSS 功能的限制来设置错误消息的样式。 ## 自定义消息文本 有时可能希望覆盖 JSF 的默认验证错误。默认验证错误在名为 `Messages.properties` 的资源包中定义。此文件位于 `[glassfish 安装目录]/glassfish/modules` 下的 `javax.faces.jar` 文件中。它位于 JAR 文件中的 `javax/faces` 文件夹内。该文件包含多个消息,但在此阶段我们只对验证错误感兴趣。默认验证错误消息如下定义: java javax.faces.validator.DoubleRangeValidator.MAXIMUM={1}: Validation Error: Value is greater than allowable maximum of "{0}" javax.faces.validator.DoubleRangeValidator.MINIMUM={1}: Validation Error: Value is less than allowable minimum of ''{0}'' javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE={2}: Validation Error: Specified attribute is not between the expected values of {0} and {1}. javax.faces.validator.DoubleRangeValidator.TYPE={0}: Validation Error: Value is not of the correct type javax.faces.validator.LengthValidator.MAXIMUM={1}: Validation Error: Value is greater than allowable maximum of ''{0}'' javax.faces.validator.LengthValidator.MINIMUM={1}: Validation Error: Value is less than allowable minimum of ''{0}'' javax.faces.validator.LongRangeValidator.MAXIMUM={1}: Validation Error: Value is greater than allowable maximum of ''{0}'' javax.faces.validator.LongRangeValidator.MINIMUM={1}: Validation Error: Value is less than allowable minimum of ''{0}'' javax.faces.validator.LongRangeValidator.NOT_IN_RANGE={2}: Validation Error: Specified attribute is not between the expected values of {0} and {1}. javax.faces.validator.LongRangeValidator.TYPE={0}: Validation Error: Value is not of the correct type. javax.faces.validator.NOT_IN_RANGE=Validation Error: Specified attribute is not between the expected values of {0} and {1}. javax.faces.validator.RegexValidator.PATTERN_NOT_SET=Regex pattern must be set. javax.faces.validator.RegexValidator.PATTERN_NOT_SET_detail=Regex pattern must be set to non-empty value. javax.faces.validator.RegexValidator.NOT_MATCHED=Regex Pattern not matched javax.faces.validator.RegexValidator.NOT_MATCHED_detail=Regex pattern of ''{0}'' not matched javax.faces.validator.RegexValidator.MATCH_EXCEPTION=Error in regular expression. javax.faces.validator.RegexValidator.MATCH_EXCEPTION_detail=Error in regular expression, ''{0}'' javax.faces.validator.BeanValidator.MESSAGE={0} 为了覆盖默认错误消息,我们需要创建自己的资源包,使用与默认资源包相同的键,但更改值以适应我们的需求。以下是我们应用程序的非常简单的自定义资源包: java javax.faces.validator.LengthValidator.MINIMUM={1}: minimum allowed length is ''{0}'' 在此资源包中,我们覆盖了由 `<f:validateLength>` 标签验证的字段输入的值小于允许的最小值时的错误消息。为了让我们的应用程序知道我们有一个包含消息属性的自定义资源包,我们需要按照以下方式修改应用程序的 `faces-config.xml` 文件: java 如我们所见,我们只需对应用程序的 `faces-config.xml` 文件进行修改,添加一个 `<message-bundle>` 元素,指示包含我们的自定义消息的资源包的名称和位置。 ### 注意 自定义错误消息文本定义是少数几种我们仍然需要为现代 JSF 应用程序定义 `faces-config.xml` 文件的情况之一。然而,请注意我们的 `faces-config.xml` 文件是多么简单;它与 JSF 1.x 的典型 `faces-config.xml` 文件大相径庭,JSF 1.x 的典型 `faces-config.xml` 文件通常包含命名豆定义、导航规则、JSF 验证器定义等。 在添加我们的自定义消息资源包并修改应用程序的 `faces-config.xml` 文件后,我们可以在以下屏幕截图中看到我们的自定义验证消息在行动:  如屏幕截图所示,如果我们没有覆盖验证消息,则默认值仍然会显示。在我们的资源包中,我们只覆盖了最小长度验证错误消息;因此,我们的自定义错误消息显示在 **First Name** 文本字段中。由于我们没有覆盖其他标准 JSF 验证器的错误消息,因此对于每个验证器,都显示了默认错误消息。电子邮件验证器是我们在本章前面开发的自定义验证器。由于它是一个自定义验证器,因此其错误消息不受影响。 # 启用 JSF 应用程序的 Ajax JSF 的早期版本没有包含本机 Ajax 支持。自定义 JSF 库供应商被迫以自己的方式实现 Ajax。不幸的是,这种状态引入了 JSF 组件库之间的不兼容性。JSF 2.0 通过引入 `<f:ajax>` 标签来标准化 Ajax 支持。 以下页面说明了 `<f:ajax>` 标签的典型用法: java <h:head> JSF Ajax Demo
<h:form> <h:messages/> <h:panelGrid columns="2"> <h:outputText value="Echo input:"/> <h:inputText id="textInput" value="#{controller.text}"> <f:ajax render="textVal" event="keyup"/> </h:inputText> <h:outputText value="Echo output:"/> <h:outputText id="textVal" value="#{controller.text}"/> </h:panelGrid><h:panelGrid columns="2"> <h:panelGroup/> <h:panelGroup/> <h:outputText value="First Operand:"/> <h:inputText id="first" value="#{controller.firstOperand}" size="3"/> <h:outputText value="Second Operand:"/> <h:inputText id="second" value="#{controller.secondOperand}" size="3"/> <h:outputText value="Total:"/> <h:outputText id="sum" value="#{controller.total}"/> <h:commandButton actionListener="#{controller.calculateTotal}" value="Calculate Sum"> <f:ajax execute="first second" render="sum"/> </h:commandButton> </h:panelGrid> </h:form> </h:body>
在部署我们的应用程序后,我们的页面渲染如下所示:  此示例页面说明了 `<f:ajax>` 标签的两个用途。在页面的顶部,我们使用了此标签来实现一个典型的 Ajax Echo 示例,其中 `<h:outputText>` 组件使用输入文本组件的值来更新自己。每次在输入字段中输入字符时,`<h:outputText>` 组件的值都会自动更新。 为了实现上一段中描述的功能,我们在 `<h:inputText>` 标签内放置了一个 `<f:ajax>` 标签。`<f:ajax>` 标签的 `render` 属性的值必须与我们在 Ajax 请求完成后希望更新的组件的 ID 相对应。在我们的示例中,我们希望使用 ID 为 "`textVal`" 的 `<h:outputText>` 组件进行更新;因此,我们将此值用作 `<f:ajax>` 标签的 `render` 属性的值。 ### 注意 在某些情况下,我们可能需要在 Ajax 事件完成后渲染多个 JSF 组件;为了适应这种情况,我们可以将多个 ID 作为 `render` 属性的值,我们只需用空格将它们分开即可。 我们在此实例中使用的另一个 `<f:ajax>` 属性是 `event` 属性。此属性指示触发 Ajax 事件的 JavaScript 事件。在这种情况下,我们需要在用户在输入字段中键入时释放任何键时触发事件;因此,要使用的事件是 `keyup`。 以下表格列出了所有支持的 JavaScript 事件: | 事件 | 描述 | | --- | --- | | `blur` | 组件失去焦点。 | | `change` | 组件失去焦点,其值已修改。 | | `click` | 在组件上单击。 | | `dblclick` | 在组件上双击。 | | `focus` | 组件获得焦点。 | | `keydown` | 在组件具有焦点时按下键。 | | `keypress` | 在组件具有焦点时按下或保持按下的键。 | | `keyup` | 在组件具有焦点时释放键。 | | `mousedown` | 在组件具有焦点时按下鼠标按钮。 | | `mousemove` | 鼠标指针在组件上移动。 | | `mouseout` | 鼠标指针离开组件。 | | `mouseover` | 鼠标指针放在组件上。 | | `mouseup` | 在组件具有焦点时释放鼠标按钮。 | | `select` | 选择组件的文本。 | | `valueChange` | 等同于 `change`;组件失去焦点,其值已修改。 | 我们再次使用 `<f:ajax>` 标签在页面下方较远的位置来启用命令按钮组件的 Ajax。在这种情况下,我们希望根据两个输入组件的值重新计算一个值。为了在服务器上使用最新的用户输入更新值,我们使用了 `<f:ajax>` 的 `execute` 属性;此属性接受一个空格分隔的组件 ID 列表,用作输入。然后我们像以前一样使用 `render` 属性来指定在 Ajax 请求完成后需要重新渲染哪些组件。 注意到我们使用了 `<h:commandButton>` 的 `actionListener` 属性。此属性通常用于在单击按钮后不需要导航到另一个页面时使用。此属性的值是我们在一个命名豆中编写的动作监听器方法。动作监听器方法必须返回 void,并接受一个 `javax.faces.event.ActionEvent` 实例作为其唯一参数。 我们应用程序的命名豆如下所示: java package net.ensode.glassfishbook.jsfajax; import javax.faces.event.ActionEvent; import javax.faces.view.ViewScoped; import javax.inject.Named; @Named @ViewScoped public class Controller { private String text; private int firstOperand; private int secondOperand; private int total; public Controller() { } public void calculateTotal(ActionEvent actionEvent) { total = firstOperand + secondOperand; } public String getText() { return text; } public void setText(String text) { this.text = text; } public int getFirstOperand() { return firstOperand; } public void setFirstOperand(int firstOperand) { this.firstOperand = firstOperand; } public int getSecondOperand() { return secondOperand; } public void setSecondOperand(int secondOperand) { this.secondOperand = secondOperand; } public int getTotal() { return total; } public void setTotal(int total) { this.total = total; } } ``` 注意到我们不需要在我们的命名豆中做任何特殊的事情来在我们的应用程序中启用 Ajax。一切都在页面的 <f:ajax> 标签中控制。 如此示例所示,启用 JSF 应用程序的 Ajax 非常简单。我们只需使用一个标签来启用我们的页面,而无需编写任何 JavaScript、JSON 或 XML 代码。 # JSF 2.2 HTML5 支持 HTML 5 是 HTML 规范的最新版本。它包括比之前版本的 HTML 多几个改进。JSF 2.2 通过引入 <f:ajax> 标签来包含对 HTML5 的支持。 ## HTML5 友好标记 通过使用透传元素,我们可以使用 HTML 5 标签开发页面,并将它们视为 JSF 组件。为此,我们需要使用 `http://xmlns
第三章。使用 JPA 进行对象关系映射
任何非平凡的 Java EE 应用程序都将数据持久化到关系型数据库。在本章中,我们将介绍如何连接到数据库并执行CRUD操作(创建、读取、更新、删除)。
Java 持久化 API(JPA)是标准的 Java EE 对象关系映射(ORM)工具。在本章中,我们将详细讨论此 API。
本章涵盖的一些主题包括:
-
通过 JPA 从数据库中检索数据
-
通过 JPA 将数据插入数据库
-
通过 JPA 在数据库中更新数据
-
通过 JPA 在数据库中删除数据
-
通过 JPA Criteria API 编程构建查询
-
通过 JPA 2.0 的 Bean Validation 支持自动化数据验证
CustomerDB 数据库
本章中的示例将使用名为CUSTOMERDB的数据库。该数据库包含跟踪虚构商店客户和订单信息的表。该数据库使用 JavaDB 作为其关系型数据库管理系统(RDBMS),因为它与 GlassFish 捆绑在一起。
本书代码下载中包含一个脚本,用于创建此数据库并预先填充其中的一些表。如何执行该脚本以及如何添加连接池和数据源以访问它的说明也包含在下载中。CUSTOMERDB数据库的模式如下图所示:

如前图所示,数据库包含存储客户信息(如姓名、地址和电子邮件地址)的表。它还包含存储订单和项目信息的表。
ADDRESS_TYPES表将存储诸如“Home”、“Mailing”和“Shipping”之类的值,以区分ADDRESSES表中的地址类型;同样,TELEPHONE_TYPES表存储“Cell”、“Home”和“Work”等值。这两个表在创建数据库时以及US_STATES表一起预先填充。
注意
为了简单起见,我们的数据库只处理美国地址。
介绍 Java 持久化 API
JPA 是在 Java EE 规范的第五版中引入的。正如其名称所暗示的,它用于将数据持久化到关系型数据库管理系统(RDBMS)。JPA 是 J2EE 中使用的实体 Bean 的替代品。JPA 实体是常规的 Java 类;Java EE 容器将这些类识别为 JPA 实体。让我们看看以下代码中CUSTOMERDB数据库的CUSTOMER表的实体映射:
package net.ensode.glassfishbook.jpaintro.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "CUSTOMERS")
public class Customer implements Serializable
{
@Id
@Column(name = "CUSTOMER_ID")
private Long customerId;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
private String email;
public Long getCustomerId()
{
return customerId;
}
public void setCustomerId(Long customerId)
{
this.customerId = customerId;
}
public String getEmail()
{
return email;
}
public void setEmail(String email)
{
this.email = email;
}
public String getFirstName()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName = firstName;
}
public String getLastName()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName = lastName;
}
}
在前面的代码中,@Entity注解让 GlassFish(或者,更确切地说,任何符合 Java EE 规范的其他应用服务器)知道这个类是一个实体。
@Table(name = "CUSTOMERS") 注解让应用服务器知道将实体映射到哪个表。name 元素的值包含实体映射到的数据库表的名称。此注解是可选的;如果类的名称与数据库表的名称相同,则不需要指定实体映射到的表。
@Id 注解表示 customerId 字段是我们实体(Entity)的主键(唯一标识符)。
@Column 注解将每个字段映射到表中的一列。如果字段的名称与数据库列的名称匹配,则不需要此注解。这就是为什么 email 字段没有被注解的原因。
EntityManager 类(实际上是一个接口;每个 Java EE 兼容的应用服务器都提供自己的实现)用于将实体持久化到数据库。以下示例说明了其用法:
package net.ensode.glassfishbook.jpaintro.namedbean;
import javax.annotation.Resource;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import net.ensode.glassfishbook.jpaintro.entity.Customer;
@Named
@RequestScoped
public class JpaDemoBean {
@PersistenceContext
private EntityManager entityManager;
@Resource
private UserTransaction userTransaction;
public String updateDatabase() {
String retVal = "confirmation";
Customer customer = new Customer();
Customer customer2 = new Customer();
Customer customer3;
customer.setCustomerId(3L);
customer.setFirstName("James");
customer.setLastName("McKenzie");
customer.setEmail("jamesm@notreal.com");
customer2.setCustomerId(4L);
customer2.setFirstName("Charles");
customer2.setLastName("Jonson");
customer2.setEmail("cjohnson@phony.org");
try {
userTransaction.begin();
entityManager.persist(customer);
entityManager.persist(customer2);
customer3 = entityManager.find(Customer.class, 4L);
customer3.setLastName("Johnson");
entityManager.persist(customer3);
entityManager.remove(customer);
userTransaction.commit();
} catch (HeuristicMixedException |
HeuristicRollbackException |
IllegalStateException |
NotSupportedException |
RollbackException |
SecurityException |
SystemException e) {
retVal = "error";
e.printStackTrace();
}
return retVal;
}
}
上一段代码中的 CDI 命名豆(bean)通过依赖注入获取了一个实现了 javax.persistence.EntityManager 接口类的实例。这是通过在 EntityManager 变量上装饰 @PersistenceContext 注解来完成的。
然后,通过 @Resource 注解注入实现了 javax.transaction.UserTransaction 接口的一个实例。这个对象是必要的,因为没有它,调用将实体持久化到数据库的操作会导致代码抛出 javax.persistence.TransactionRequiredException 异常。
EntityManager 类执行许多数据库相关任务,例如在数据库中查找实体、更新它们以及删除它们。
由于 JPA 实体是 普通的 Java 对象(POJOs),它们可以通过 new 操作符进行实例化。
注意
POJOs 是不需要扩展任何特定父类或实现任何特定接口的 Java 对象
调用 setCustomerId() 方法利用了自动装箱(autoboxing),这是在 JDK 1.5 中添加到 Java 语言的一个特性。请注意,该方法接受一个 java.lang.Long 类型的实例作为参数,但我们使用的是 long 基本类型。多亏了这个特性,代码能够正确编译和执行。
在 EntityManager 上调用 persist() 方法的调用必须在事务中进行;因此,需要通过调用 UserTransaction 的 begin() 方法来启动一个事务。
然后,我们通过在 entityManager 上调用 persist() 方法,为之前在代码中填充的两个 Customer 类实例插入两个新的行到 CUSTOMERS 表中。
在将customer和customer2对象中的数据持久化后,我们通过在entityManager上调用find()方法在CUSTOMERS表中搜索具有主键4的行。此方法接受我们正在搜索的实体的类作为其第一个参数,以及对应于我们想要获取的对象的行的主键。此方法大致等同于实体 Bean 的 home 接口上的findByPrimaryKey()方法。
我们为customer2对象设置的主键是4;因此,我们现在拥有这个对象的副本。当我们将这位客户的数据最初插入数据库时,他的姓氏拼写错误;我们现在通过在customer3上调用setLastName()方法来纠正约翰逊先生的姓氏,然后通过调用entityManager.persist()来更新数据库中的信息。
然后,我们通过调用entityManager.remove()并传递customer对象作为参数来删除customer对象的信息。
最后,我们通过在userTransaction上调用commit()方法将更改提交到数据库。
为了使我们的代码按预期工作,必须在包含JPADemoBean的 WAR 文件中部署一个名为persistence.xml的 XML 配置文件。此文件必须放置在 WAR 文件内WEB-INF/classes/META-INF/目录中。此文件的内容对应于我们的代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="customerPersistenceUnit">
<jta-data-source>jdbc/__CustomerDBPool</jta-data-source>
</persistence-unit>
</persistence>
persistence.xml文件必须至少包含一个<persistence-unit>元素。每个<persistence-unit>元素必须为其name属性提供一个值,并且必须包含一个<jta-data-source>子元素,其值是用于持久化单元的数据源的 JNDI 名称。
由于应用程序可能访问多个数据库,因此允许有多个<persistence-unit>元素。对于应用程序将要访问的每个数据库,都需要一个<persistence-unit>元素。如果应用程序定义了多个<persistence-unit>元素,那么用于注入EntityManager的@PersistenceContext注解必须为其unitName元素提供一个值。此元素的值必须与persistence.xml中对应<persistence-unit>元素的name属性匹配。
小贴士
无法持久化分离对象异常
应用程序通常会通过EntityManager.find()方法检索 JPA 实体,然后将此实体传递到业务或用户界面层,在那里它可能会被修改。稍后,将更新与实体对应的数据库数据。在这种情况下,调用EntityManager.persist()将导致异常。为了以这种方式更新 JPA 实体,我们需要调用EntityManager.merge()。此方法接受 JPA 实体的一个实例作为其单个参数,并使用其中存储的数据更新数据库中的对应行。
实体关系
在上一节中,我们看到了如何从数据库中检索、插入、更新和删除单个实体。实体很少是孤立的;在绝大多数情况下,它们与其他实体相关联。
实体可以有一对一、一对多、多对一和多对多的关系。
例如,在CustomerDB数据库中,LOGIN_INFO表和CUSTOMERS表之间存在一对一的关系。这意味着每个客户在LOGIN_INFO表中恰好对应一行。CUSTOMERS表和ORDERS表之间存在一对一的关系。这是因为一个客户可以下很多订单,但每个订单只属于一个客户。此外,ORDERS表和ITEMS表之间存在多对多关系。这是因为一个订单可以包含多个项目,而一个项目可以出现在多个订单中。
在接下来的几节中,我们将讨论如何建立 JPA 实体之间的关系。
一对一关系
当一个实体的实例可以对应另一个实体的零个或一个实例时,就会发生一对一关系。
一对一的实体关系可以是双向的(每个实体都了解这种关系)或单向的(只有其中一个实体了解这种关系)。在CustomerDB示例数据库中,LOGIN_INFO表和CUSTOMERS表之间的一对一映射是单向的。这是因为LOGIN_INFO表有一个指向CUSTOMERS表的外键,但反过来则不然。正如我们很快就会看到的,这个事实并不会阻止我们在Customer实体和LoginInfo实体之间创建一个双向的一对一关系。
下面可以看到映射到LOGIN_INFO表的LoginInfo实体源代码:
package net.ensode.glassfishbook.entityrelationship.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Table;
@Entity
@Table(name = "LOGIN_INFO")
public class LoginInfo
{
@Id
@Column(name = "LOGIN_INFO_ID")
private Long loginInfoId;
@Column(name = "LOGIN_NAME")
private String loginName;
private String password;
@OneToOne
@JoinColumn(name="CUSTOMER_ID")
private Customer customer;
public Long getLoginInfoId()
{
return loginInfoId;
}
public void setLoginInfoId(Long loginInfoId)
{
this.loginInfoId = loginInfoId;
}
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
public String getLoginName()
{
return loginName;
}
public void setLoginName(String userName)
{
this.loginName = userName;
}
public Customer getCustomer()
{
return customer;
}
public void setCustomer(Customer customer)
{
this.customer = customer;
}
}
这个实体代码与Customer实体代码非常相似;它定义了映射到数据库列的字段。每个名称与数据库列名称不匹配的字段都装饰有@Column注解;除此之外,主键还装饰有@Id注解。
这段代码在customer字段的声明中变得有趣。如代码所示,customer字段被装饰了@OneToOne注解。这使应用程序服务器(GlassFish)知道这个实体与Customer实体之间存在一对一的关系。customer字段也被装饰了@JoinColumn注解。这个注解让容器知道LOGIN_INFO表中的哪一列是对应于CUSTOMER表上主键的外键。由于LoginInfo实体映射的表LOGIN_INFO有一个指向CUSTOMER表的外键,因此LoginInfo实体拥有这个关系。如果这个关系是单向的,我们就不需要对Customer实体做任何修改。然而,由于我们希望这两个实体之间有一个双向关系,我们需要在Customer实体中添加一个LoginInfo字段,以及相应的 getter 和 setter 方法,如下面的代码所示:
package net.ensode.glassfishbook.entityrelationship.entity;
import java.io.Serializable;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
@Entity
@Table(name = "CUSTOMERS")
public class Customer implements Serializable
{
@Id
@Column(name = "CUSTOMER_ID")
private Long customerId;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
private String email;
@OneToOne(mappedBy = "customer")
private LoginInfo loginInfo;
public Long getCustomerId()
{
return customerId;
}
public void setCustomerId(Long customerId)
{
this.customerId = customerId;
}
public String getEmail()
{
return email;
}
public void setEmail(String email)
{
this.email = email;
}
public String getFirstName()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName = firstName;
}
public String getLastName()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName = lastName;
}
public LoginInfo getLoginInfo()
{
return loginInfo;
}
public void setLoginInfo(LoginInfo loginInfo)
{
this.loginInfo = loginInfo;
}
为了使一对一关系双向,我们只需要对Customer实体进行一个修改,即向其中添加一个LoginInfo字段,以及相应的 setter 和 getter 方法。LoginInfo字段被装饰了@OneToOne注解。由于Customer实体不拥有这个关系(它映射的表没有对应表的外键),@OneToOne注解的mappedBy元素需要被添加。这个元素指定了对应实体中哪个字段有关系的另一端。在这个特定的情况下,LoginInfo实体中的customer字段对应于这个一对一关系的另一端。
下面的 Java 类展示了前面实体的使用:
package net.ensode.glassfishbook.entityrelationship.namedbean;
import javax.annotation.Resource;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import net.ensode.glassfishbook.entityrelationship.entity.Customer;
import net.ensode.glassfishbook.entityrelationship.entity.LoginInfo;
@Named
@RequestScoped
public class OneToOneRelationshipDemoBean {
@PersistenceContext
private EntityManager entityManager;
@Resource
private UserTransaction userTransaction;
public String updateDatabase() {
String retVal = "confirmation";
Customer customer;
LoginInfo loginInfo = new LoginInfo();
loginInfo.setLoginInfoId(1L);
loginInfo.setLoginName("charlesj");
loginInfo.setPassword("iwonttellyou");
try {
userTransaction.begin();
customer = entityManager.find(Customer.class, 4L);
loginInfo.setCustomer(customer);
entityManager.persist(loginInfo);
userTransaction.commit();
} catch (NotSupportedException |
SystemException |
SecurityException |
IllegalStateException |
RollbackException |
HeuristicMixedException |
HeuristicRollbackException e) {
retVal = "error";
e.printStackTrace();
}
return retVal;
}
}
在这个例子中,我们首先创建了一个LoginInfo实体的实例,并用一些数据填充它。然后,我们通过调用EntityManager的find()方法从数据库中获取Customer实体的一个实例(这个实体的数据在之前的某个例子中已插入到CUSTOMERS表中)。接着,我们在LoginInfo实体上调用setCustomer()方法,将客户对象作为参数传递。最后,我们调用EntityManager.persist()方法将数据保存到数据库中。
在幕后,LOGIN_INFO表的CUSTOMER_ID列被填充了CUSTOMERS表中相应行的主键。这可以通过查询CUSTOMERDB数据库轻松验证。
注意
注意到调用EntityManager.find()以获取客户实体是在调用EntityManager.persist()的同一事务中进行的。这必须是这样;否则,数据库将无法成功更新。
一对多关系
JPA 的一对多实体关系可以是双向的(一个实体包含一个多对一关系,而相应的实体包含一个反向的一对多关系)。
使用 SQL,一对一关系通过一个表中的外键来定义。关系的“多”部分是包含对关系的“一”部分的外键的那个部分。在 RDBMS 中定义的一对多关系通常是单向的,因为使它们双向通常会导致数据规范化。
就像在 RDBMS 中定义单向的一对多关系一样,在 JPA 中,关系的“多”部分是具有对关系的“一”部分的引用的那个部分;因此,用于装饰适当 setter 方法的注解是@ManyToOne。
在CUSTOMERDB数据库中,客户和订单之间存在单向的一对多关系。我们通过以下代码在Order实体中定义此关系:
package net.ensode.glassfishbook.entityrelationship.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "ORDERS")
public class Order
{
@Id
@Column(name = "ORDER_ID")
private Long orderId;
@Column(name = "ORDER_NUMBER")
private String orderNumber;
@Column(name = "ORDER_DESCRIPTION")
private String orderDescription;
@ManyToOne
@JoinColumn(name = "CUSTOMER_ID")
private Customer customer;
public Customer getCustomer()
{
return customer;
}
public void setCustomer(Customer customer)
{
this.customer = customer;
}
public String getOrderDescription()
{
return orderDescription;
}
public void setOrderDescription(String orderDescription)
{
this.orderDescription = orderDescription;
}
public Long getOrderId()
{
return orderId;
}
public void setOrderId(Long orderId)
{
this.orderId = orderId;
}
public String getOrderNumber()
{
return orderNumber;
}
public void setOrderNumber(String orderNumber)
{
this.orderNumber = orderNumber;
}
}
如果我们要在Orders实体和Customer实体之间定义单向的多对一关系,我们就不需要修改Customer实体。为了在这两个实体之间定义双向的一对多关系,需要在Customer实体中添加一个带有@OneToMany注解的新字段,如下面的代码所示:
package net.ensode.glassfishbook.entityrelationship.entity;
import java.io.Serializable;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "CUSTOMERS")
public class Customer implements Serializable
{
@Id
@Column(name = "CUSTOMER_ID")
private Long customerId;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
private String email;
@OneToOne(mappedBy = "customer")
private LoginInfo loginInfo;
@OneToMany(mappedBy="customer")
private Set<Order> orders;
public Long getCustomerId()
{
return customerId;
}
public void setCustomerId(Long customerId)
{
this.customerId = customerId;
}
public String getEmail()
{
return email;
}
public void setEmail(String email)
{
this.email = email;
}
public String getFirstName()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName = firstName;
}
public String getLastName()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName = lastName;
}
public LoginInfo getLoginInfo()
{
return loginInfo;
}
public void setLoginInfo(LoginInfo loginInfo)
{
this.loginInfo = loginInfo;
}
public Set<Order> getOrders()
{
return orders;
}
public void setOrders(Set<Order> orders)
{
this.orders = orders;
}
}
与之前的Customer实体版本相比,唯一的区别是添加了orders字段和相关 getter 和 setter 方法。特别值得注意的是装饰此字段的@OneToMany注解。mappedBy属性必须与对应实体中“多”部分对应字段的名称匹配。简单来说,mappedBy属性的值必须与在关系另一端的 bean 中用@ManyToOne注解装饰的字段名称匹配。
以下示例代码说明了如何将一对多关系持久化到数据库中:
package net.ensode.glassfishbook.entityrelationship.namedbean;
import javax.annotation.Resource;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import net.ensode.glassfishbook.entityrelationship.entity.Customer;
import net.ensode.glassfishbook.entityrelationship.entity.Order;
@Named
@RequestScoped
public class OneToManyRelationshipDemoBean {
@PersistenceContext
private EntityManager entityManager;
@Resource
private UserTransaction userTransaction;
public String updateDatabase() {
String retVal = "confirmation";
Customer customer;
Order order1;
Order order2;
order1 = new Order();
order1.setOrderId(1L);
order1.setOrderNumber("SFX12345");
order1.setOrderDescription("Dummy order.");
order2 = new Order();
order2.setOrderId(2L);
order2.setOrderNumber("SFX23456");
order2.setOrderDescription("Another dummy order.");
try {
userTransaction.begin();
customer = entityManager.find(Customer.class, 4L);
order1.setCustomer(customer);
order2.setCustomer(customer);
entityManager.persist(order1);
entityManager.persist(order2);
userTransaction.commit();
} catch (NotSupportedException |
SystemException |
SecurityException |
IllegalStateException |
RollbackException |
HeuristicMixedException |
HeuristicRollbackException e) {
retVal = "error";
e.printStackTrace();
}
return retVal;
}
}
上述代码与之前的示例非常相似。它创建了两个Order实体实例,用一些数据填充它们;然后,在一个事务中定位Customer实体实例,并将其用作两个Order实体实例的setCustomer()方法的参数。然后,我们通过为每个Order实体调用EntityManager.persist()来持久化这两个Order实体。
就像处理一对一关系一样,在幕后,CUSTOMERDB数据库中ORDERS表的CUSTOMER_ID列被填充了对应于CUSTOMERS表中相关行的主键。
由于关系是双向的,我们可以通过在Customer实体上调用getOrders()方法来获取与客户相关的所有订单。
多对多关系
在 CUSTOMERDB 数据库中,ORDERS 表和 ITEMS 表之间存在多对多关系。我们可以通过向 Order 实体添加一个新的 Collection<Item> 字段并使用 @ManyToMany 注解来映射这种关系,如下面的代码所示:
package net.ensode.glassfishbook.entityrelationship.entity;
import java.util.Collection;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "ORDERS")
public class Order
{
@Id
@Column(name = "ORDER_ID")
private Long orderId;
@Column(name = "ORDER_NUMBER")
private String orderNumber;
@Column(name = "ORDER_DESCRIPTION")
private String orderDescription;
@ManyToOne
@JoinColumn(name = "CUSTOMER_ID")
private Customer customer;
@ManyToMany
@JoinTable(name = "ORDER_ITEMS",
joinColumns = @JoinColumn(name = "ORDER_ID",
referencedColumnName = "ORDER_ID"),
inverseJoinColumns = @JoinColumn(name = "ITEM_ID",
referencedColumnName = "ITEM_ID"))
private Collection<Item> items;
public Customer getCustomer()
{
return customer;
}
public void setCustomer(Customer customer)
{
this.customer = customer;
}
public String getOrderDescription()
{
return orderDescription;
}
public void setOrderDescription(String orderDescription)
{
this.orderDescription = orderDescription;
}
public Long getOrderId()
{
return orderId;
}
public void setOrderId(Long orderId)
{
this.orderId = orderId;
}
public String getOrderNumber()
{
return orderNumber;
}
public void setOrderNumber(String orderNumber)
{
this.orderNumber = orderNumber;
}
public Collection<Item> getItems()
{
return items;
}
public void setItems(Collection<Item> items)
{
this.items = items;
}
}
如前述代码所示,除了被 @ManyToMany 注解装饰外,items 字段还被 @JoinTable 注解装饰。正如其名称所暗示的,这个注解让应用服务器知道哪个表被用作连接表来创建两个实体之间的多对多关系。这个注解有三个相关元素:name 元素,它定义了连接表的名字;以及 joinColumns 和 inverseJoinColumns 元素,它们定义了作为连接表外键指向实体主键的列。joinColumns 和 inverseJoinColumns 元素的值是另一个注解,即 @JoinColumn 注解。这个注解有两个相关元素:name 元素,它定义了连接表中的列名;以及 referencedColumnName 元素,它定义了实体表中的列名。
Item 实体是一个简单的实体,映射到 CUSTOMERDB 数据库中的 ITEMS 表,如下面的代码所示:
package net.ensode.glassfishbook.entityrelationship.entity;
import java.util.Collection;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
@Entity
@Table(name = "ITEMS")
public class Item
{
@Id
@Column(name = "ITEM_ID")
private Long itemId;
@Column(name = "ITEM_NUMBER")
private String itemNumber;
@Column(name = "ITEM_SHORT_DESC")
private String itemShortDesc;
@Column(name = "ITEM_LONG_DESC")
private String itemLongDesc;
@ManyToMany(mappedBy="items")
private Collection<Order> orders;
public Long getItemId()
{
return itemId;
}
public void setItemId(Long itemId)
{
this.itemId = itemId;
}
public String getItemLongDesc()
{
return itemLongDesc;
}
public void setItemLongDesc(String itemLongDesc)
{
this.itemLongDesc = itemLongDesc;
}
public String getItemNumber()
{
return itemNumber;
}
public void setItemNumber(String itemNumber)
{
this.itemNumber = itemNumber;
}
public String getItemShortDesc()
{
return itemShortDesc;
}
public void setItemShortDesc(String itemShortDesc)
{
this.itemShortDesc = itemShortDesc;
}
public Collection<Order> getOrders()
{
return orders;
}
public void setOrders(Collection<Order> orders)
{
this.orders = orders;
}
}
就像一对一和一对多关系一样,多对多关系可以是单向的或双向的。由于我们希望 Order 和 Item 实体之间的多对多关系是双向的,我们添加了一个 Collection<Order> 字段并用 @ManyToMany 注解装饰它。由于 Order 实体中相应的字段已经定义了连接表,因此在这里不需要再次定义。包含 @JoinTable 注解的实体被称为拥有关系。在多对多关系中,任一实体都可以拥有关系。在我们的例子中,Order 实体拥有它,因为它的 Collection<Item> 字段被 @JoinTable 注解装饰。
就像一对一和一对多关系一样,双向多对多关系非拥有方的一侧的 @ManyToMany 注解必须包含一个 mappedBy 元素,指明在拥有实体中定义关系的哪个字段。
现在我们已经看到了在 Order 和 Item 实体之间建立双向多对多关系所需的更改,我们可以在下面的示例中看到这种关系在实际中的应用:
package net.ensode.glassfishbook.entityrelationship.namedbean;
import java.util.ArrayList;
import java.util.Collection;
import javax.annotation.Resource;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import net.ensode.glassfishbook.entityrelationship.entity.Item;
import net.ensode.glassfishbook.entityrelationship.entity.Order;
@Named
@RequestScoped
public class ManyToManyRelationshipDemoBean {
@PersistenceContext
private EntityManager entityManager;
@Resource
private UserTransaction userTransaction;
public String updateDatabase() {
String retVal = "confirmation";
Order order;
Collection<Item> items = new ArrayList<Item>();
Item item1 = new Item();
Item item2 = new Item();
item1.setItemId(1L);
item1.setItemNumber("BCD1234");
item1.setItemShortDesc("Notebook Computer");
item1.setItemLongDesc("64 bit Quad core CPU, 4GB memory");
item2.setItemId(2L);
item2.setItemNumber("CDF2345");
item2.setItemShortDesc("Cordless Mouse");
item2.setItemLongDesc("Three button, infrared, "
+ "vertical and horizontal scrollwheels");
items.add(item1);
items.add(item2);
try {
userTransaction.begin();
entityManager.persist(item1);
entityManager.persist(item2);
order = entityManager.find(Order.class, 1L);
order.setItems(items);
entityManager.persist(order);
userTransaction.commit();
} catch (NotSupportedException |
SystemException |
SecurityException |
IllegalStateException |
RollbackException |
HeuristicMixedException |
HeuristicRollbackException e) {
retVal = "error";
e.printStackTrace();
}
return retVal;
}
}
前面的代码创建了两个 Item 实体实例,并用一些数据填充它们。然后,将这些实例添加到一个集合中。接着开始一个事务。将这两个 Item 实体实例持久化到数据库中。然后,从数据库中检索 Order 实体实例。然后调用 Order 实体实例的 setItems() 方法,传递包含两个 Item 实体实例的集合作为参数。然后,将 Customer 实体实例持久化到数据库中。此时,在 ORDER_ITEMS 表(ORDERS 和 ITEMS 表的连接表)背后创建了两个行。
复合主键
CUSTOMERDB 数据库中的大多数表都有一个列,这个列的存在目的仅仅是为了作为主键(这种类型的主键有时被称为代理主键或人工主键)。然而,一些数据库并不是这样设计的;相反,数据库中有一个列,已知它在行之间是唯一的,被用作主键。如果没有列的值在行之间不是保证唯一的,那么就使用两个或更多列的组合作为表的主键。可以使用主键类将这种类型的主键映射到 JPA 实体。
CUSTOMERDB 数据库中有一个表没有代理主键;这个表是 ORDER_ITEMS 表。这个表作为 ORDERS 表和 ITEMS 表的连接表。除了为这两个表有外键之外,ORDER_ITEMS 表还有一个额外的列,称为 ITEM_QTY,用于存储订单中每个项目的数量。由于这个表没有代理主键,映射到它的 JPA 实体必须有一个自定义的主键类。在这个表中,ORDER_ID 和 ITEM_ID 列的组合必须是唯一的。因此,这是一个复合主键的好组合,如下面的代码示例所示:
package net.ensode.glassfishbook.compositeprimarykeys.entity;
import java.io.Serializable;
public class OrderItemPK implements Serializable
{
public Long orderId;
public Long itemId;
public OrderItemPK()
{
}
public OrderItemPK(Long orderId, Long itemId)
{
this.orderId = orderId;
this.itemId = itemId;
}
@Override
public boolean equals(Object obj)
{
boolean returnVal = false;
if (obj == null)
{
returnVal = false;
}
else if (!obj.getClass().equals(this.getClass()))
{
returnVal = false;
}
else
{
OrderItemPK other = (OrderItemPK) obj;
if (this == other)
{
returnVal = true;
}
else if (orderId != null && other.orderId != null
&& this.orderId.equals(other.orderId))
{
if (itemId != null && other.itemId != null
&& itemId.equals(other.itemId))
{
returnVal = true;
}
}
else
{
returnVal = false;
}
}
return returnVal;
}
@Override
public int hashCode()
{
if (orderId == null || itemId == null)
{
return 0;
}
else
{
return orderId.hashCode() ^ itemId.hashCode();
}
}
}
自定义主键类必须满足以下要求:
-
该类必须是
public -
它必须实现
java.io.Serializable -
它必须有一个不接受任何参数的
public构造函数 -
它的字段必须是
public或protected -
它的字段名和类型必须与实体的匹配
-
它必须重写
java.lang.Object类中定义的默认hashCode()和equals()方法
前面的代码中的 OrderItemPK 类满足所有这些要求。它还有一个方便的构造函数,接受两个 Long 对象来初始化其 orderId 和 itemId 字段。这个构造函数是为了方便而添加的,并不是作为主键类使用的必要条件。
当实体使用自定义主键类时,它必须用 @IdClass 注解装饰。由于 OrderItem 类使用 OrderItemPK 作为其自定义主键类,因此它必须用该注解装饰,如下面的代码示例所示:
package net.ensode.glassfishbook.compositeprimarykeys.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
@Entity
@Table(name = "ORDER_ITEMS")
@IdClass(value = OrderItemPK.class)
public class OrderItem
{
@Id
@Column(name = "ORDER_ID")
private Long orderId;
@Id
@Column(name = "ITEM_ID")
private Long itemId;
@Column(name = "ITEM_QTY")
private Long itemQty;
public Long getItemId()
{
return itemId;
}
public void setItemId(Long itemId)
{
this.itemId = itemId;
}
public Long getItemQty()
{
return itemQty;
}
public void setItemQty(Long itemQty)
{
this.itemQty = itemQty;
}
public Long getOrderId()
{
return orderId;
}
public void setOrderId(Long orderId)
{
this.orderId = orderId;
}
}
与我们之前看到的实体相比,此实体有两个区别。第一个区别是此实体用 @IdClass 注解装饰,表示与其对应的 primary key 类。第二个区别是此实体有多个字段用 @Id 注解装饰。由于此实体具有复合主键,因此主键中的每个字段都必须用此注解装饰。
获取具有复合主键的实体引用与获取具有单字段主键的实体引用没有太大区别。以下示例演示了如何做到这一点:
package net.ensode.glassfishbook.compositeprimarykeys.namedbean;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import net.ensode.glassfishbook.compositeprimarykeys.entity.OrderItem;
import net.ensode.glassfishbook.compositeprimarykeys.entity.OrderItemPK;
@Named
@RequestScoped
public class CompositePrimaryKeyDemoBean {
@PersistenceContext
private EntityManager entityManager;
private OrderItem orderItem;
public String findOrderItem() {
String retVal = "confirmation";
try {
orderItem = entityManager.find(OrderItem.class, new OrderItemPK(1L, 2L));
} catch (Exception e) {
retVal = "error";
e.printStackTrace();
}
return retVal;
}
public OrderItem getOrderItem() {
return orderItem;
}
public void setOrderItem(OrderItem orderItem) {
this.orderItem = orderItem;
}
}
如此例所示,使用复合主键定位实体与使用单字段主键的实体之间的唯一区别是,必须将自定义主键类的实例作为 EntityManager.find() 方法的第二个参数传递;对于此实例的字段必须填充与主键中每个字段相应的适当值。
介绍 Java 持久化查询语言
我们迄今为止从数据库获取实体的所有示例都方便地假设实体的主键在事先已知。我们都知道,这种情况通常并不成立。每当我们需要通过除实体主键之外的字段搜索实体时,我们必须使用 Java 持久化查询语言(JPQL)。
JPQL 是一种类似于 SQL 的语言,用于在数据库中检索、更新和删除实体。以下示例说明了如何使用 JPQL 从 CUSTOMERDB 数据库中的 US_STATES 表中检索状态子集:
package net.ensode.glassfishbook.jpql.namedbean;
import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import net.ensode.glassfishbook.jpql.entity.UsState;
@Named
@RequestScoped
public class SelectQueryDemoBean {
@PersistenceContext
private EntityManager entityManager;
private List<UsState> matchingStatesList;
public String findStates() {
String retVal = "confirmation";
try {
Query query = entityManager
.createQuery(
"SELECT s FROM UsState s WHERE s.usStateNm "
+ "LIKE :name");
query.setParameter("name", "New%");
matchingStatesList = query.getResultList();
} catch (Exception e) {
retVal = "error";
e.printStackTrace();
}
return retVal;
}
public List<UsState> getMatchingStatesList() {
return matchingStatesList;
}
public void setMatchingStatesList(List<UsState> matchingStatesList) {
this.matchingStatesList = matchingStatesList;
}
}
上述代码调用了 EntityManager.createQuery() 方法,传递一个包含 JPQL 查询字符串的参数。此方法返回一个 javax.persistence.Query 实例。查询检索所有名称以 "New" 开头的 UsState 实体。
如前述代码所示,JPQL 与 SQL 类似。然而,有一些差异可能会让了解 SQL 的读者感到困惑。代码中查询的等效 SQL 代码将是:
SELECT * from US_STATES s where s.US_STATE_NM like 'New%'
JPQL 与 SQL 之间的第一个区别是,在 JPQL 中,我们始终使用实体名称,而在 SQL 中使用表名。JPQL 查询中实体名称后面的 s 是实体的别名。在 SQL 中,表别名是可选的,但在 JPQL 中,实体别名是必需的。记住这些区别,JPQL 查询现在应该不会那么令人困惑。
查询中的:name参数是一个命名参数;命名参数的目的是用实际值替换。这是通过在调用EntityManager.createQuery()返回的javax.persistence.Query实例上调用setParameter()方法来实现的。JPQL 查询可以有多个命名参数。
要实际运行查询并从数据库中检索实体,必须在从EntityManager.createQuery()获得的javax.persistence.Query实例上调用getResultList()方法。此方法返回一个实现java.util.List接口的类的实例。此列表包含符合查询条件的实体。如果没有实体符合条件,则返回一个空列表。
如果我们确定查询将返回恰好一个实体,那么可以在Query上调用getSingleResult()方法作为替代;此方法返回一个必须转换为适当实体的对象。
我们的示例使用LIKE运算符来查找以字符串"New"开头的实体。这是通过将查询的命名参数替换为值"New%"来实现的。参数值末尾的百分号表示"New"之后的任意数量的字符都将与表达式匹配。百分号可以在参数值中的任何位置使用;例如,值"%Dakota"将匹配任何以"Dakota"结尾的实体,而值"A%a"将匹配任何以大写字母"A"开头并以小写字母"a"结尾的州。参数值中可以有多个百分号。下划线符号(_)可以用来匹配单个字符;所有关于百分号的规则也适用于下划线。
除了LIKE运算符之外,还有其他运算符可以用来从数据库中检索实体,如下所示:
-
=运算符将检索左侧操作符字段与右侧操作符值完全匹配的实体 -
>运算符将检索左侧操作符字段大于右侧操作符值的实体 -
<运算符将检索左侧操作符字段小于右侧操作符值的实体 -
>=运算符将检索左侧操作符字段大于或等于右侧操作符值的实体 -
<=运算符将检索左侧操作符字段小于或等于右侧操作符值的实体
所有的前面运算符与 SQL 中的等效运算符工作方式相同。就像在 SQL 中一样,前面的运算符可以与AND和OR运算符结合使用。与AND运算符结合的条件如果两个条件都为真则匹配,而与OR运算符结合的条件如果至少有一个条件为真则匹配。
如果我们打算多次使用查询,可以将查询存储在命名查询中。可以通过在相关实体类上装饰 @NamedQuery 注解来定义命名查询。此注解有两个元素,一个用于设置查询名称的名称元素,一个用于定义查询本身的查询元素。要执行命名查询,必须在 EntityManager 实例上调用 createNamedQuery() 方法。此方法接受一个类型为 String 的字符串,其中包含查询名称作为其唯一参数,并返回一个 javax.persistence.Query 实例。
除了检索实体外,JPQL 还可以用来修改或删除实体。然而,实体修改和删除也可以通过 EntityManager 接口以编程方式进行;这样做生成的代码通常比使用 JPQL 时更易于阅读。因此,我们不会介绍通过 JPQL 进行实体修改和删除。对编写修改和删除实体的 JPQL 查询感兴趣的读者,以及对 JPQL 感兴趣的读者,应查阅 Java Persistence 2.1 规范。该规范可以从 jcp.org/en/jsr/detail?id=338 下载。
引入 Criteria API
JPA 在第 2.0 版本中的一个主要新增功能是引入了 Criteria API。Criteria API 是作为 JPQL 的补充而设计的。
虽然 JPQL 非常灵活,但它有一些问题使得使用它比必要的更困难。首先,JPQL 查询以字符串形式存储,编译器无法验证 JPQL 语法。此外,JPQL 不是类型安全的;我们可能编写一个 JPQL 查询,其中 where 子句可能有一个字符串值用于数值属性,而我们的代码编译和部署仍然可以正常进行。
为了绕过前一段中描述的 JPQL 限制,JPA 在规范的第 2.0 版本中引入了 Criteria API。Criteria API 允许我们以编程方式编写 JPA 查询,而无需依赖于 JPQL。
以下代码示例说明了如何在我们的 Java EE 应用程序中使用 Criteria API:
package net.ensode.glassfishbook.criteriaapi.namedbean;
import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.SingularAttribute;
import net.ensode.glassfishbook.criteriaapi.entity.UsState;
@Named
@RequestScoped
public class CriteriaApiDemoBean {
@PersistenceContext
private EntityManager entityManager;
private List<UsState> matchingStatesList;
public String findStates() {
String retVal = "confirmation";
try {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<UsState> criteriaQuery = criteriaBuilder.
createQuery(UsState.class);
Root<UsState> root = criteriaQuery.from(UsState.class);
Metamodel metamodel = entityManager.getMetamodel();
EntityType<UsState> usStateEntityType = metamodel.entity(
UsState.class);
SingularAttribute<UsState, String> usStateAttribute
=
usStateEntityType.getDeclaredSingularAttribute(
"usStateNm",
String.class);
Path<String> path = root.get(usStateAttribute);
Predicate predicate = criteriaBuilder.like(
path, "New%");
criteriaQuery = criteriaQuery.where(predicate);
TypedQuery typedQuery = entityManager.createQuery(
criteriaQuery);
matchingStatesList = typedQuery.getResultList();
} catch (Exception e) {
retVal = "error";
e.printStackTrace();
}
return retVal;
}
public List<UsState> getMatchingStatesList() {
return matchingStatesList;
}
public void setMatchingStatesList(List<UsState> matchingStatesList) {
this.matchingStatesList = matchingStatesList;
}
}
这个示例与我们在本章前面看到的 JPQL 示例等效。然而,这个示例利用了 Criteria API 而不是依赖于 JPQL。
当使用 Criteria API 编写代码时,我们首先需要获取一个实现 javax.persistence.criteria.CriteriaBuilder 接口的类的实例。正如我们在示例中所看到的,我们需要通过在 EntityManager 实例上调用 getCriteriaBuilder() 方法来获取这个实例。
从我们的CriteriaBuilder实现中,我们需要获取一个实现javax.persistence.criteria.CriteriaQuery接口的类的实例。我们通过在CriteriaBuilder实现中调用createQuery()方法来完成此操作。请注意,CriteriaQuery是泛型化的。泛型类型参数决定了我们的CriteriaQuery实现执行后将返回的结果类型。通过这种方式利用泛型,Criteria API 允许我们编写类型安全的代码。
一旦我们获得了CriteriaQuery实现,我们可以从中获取一个实现javax.persistence.criteria.Root接口的类的实例。Root实现决定了我们将从哪个 JPA 实体进行查询。它与 JPQL(和 SQL)中的FROM查询类似。
在我们的示例中的下一行和下一行,我们利用了 JPA 规范中的另一个新特性,即Metamodel API。为了利用 Metamodel API,我们需要通过在EntityManager实例上调用getMetamodel()方法来获取javax.persistence.metamodel.Metamodel接口的实现。
从我们的Metamodel实现中,我们可以获取一个泛型类型的javax.persistence.metamodel.EntityType接口的实例。泛型类型参数指示我们的EntityType实现对应的 JPA 实体。EntityType接口实现允许我们在运行时浏览我们的 JPA 实体的持久属性,这正是我们在示例中的下一行所做的事情。在我们的例子中,我们正在获取一个SingularAttribute的实例,它映射到我们 JPA 实体中的一个简单、单一属性。EntityType接口实现有方法可以获取映射到集合、集合、列表和映射的属性。获取这些类型的属性与获取SingularAttribute非常相似;因此,我们不会深入探讨这些。有关更多信息,请参阅 Java EE 7 API 文档docs.oracle.com/javaee/7/api/。
如我们示例中所示,SingularAttribute包含两个泛型类型参数。第一个参数决定了我们正在处理的 JPA 实体,第二个参数指示属性的类型。我们通过在EntityType接口实现上调用getDeclaredSingularAttribute()方法并传递属性名称(如我们在 JPA 实体中声明的那样)作为字符串来获取我们的SingularAttribute实现。
一旦我们获得了我们的SingularAttribute实现,我们需要通过在Root实例上调用get()方法并传递SingularAttribute作为参数来获取一个javax.persistence.criteria.Path实现。
在我们的例子中,我们将获取一个列表,其中包含所有以字符串 "New" 开头的美国各州名称。当然,这是一个 like 条件的工作。我们可以通过在 CriteriaBuilder 实现上调用 like() 方法,使用标准 API 来完成这项工作。like() 方法将我们的 Path 实现作为其第一个参数,将搜索值作为其第二个参数。
CriteriaBuilder 接口实现有许多与 SQL 和 JPQL 子句类似的方法,如 equals()、greaterThan()、lessThan()、and()、or() 等(有关完整列表,请参阅 Java EE 7 文档docs.oracle.com/javaee/7/api/)。这些方法可以通过 Criteria API 组合起来创建复杂的查询。
CriteriaBuilder 中的 like() 方法返回一个 javax.persistence.criteria.Predicate 接口的实现,我们需要将其传递给 CriteriaQuery 实现中的 where() 方法。此方法返回一个新的 CriteriaBuilder 实例,我们将其分配给我们的 CriteriaBuilder 变量。
到目前为止,我们已经准备好构建我们的查询。当使用 Criteria API 时,我们处理 javax.persistence.TypedQuery 接口,这可以被视为与 JPQL 一起使用的 Query 接口的安全版本。我们通过在 EntityManager 中调用 createQuery() 方法并传递我们的 CriteriaQuery 实现来获取 TypedQuery 的实例。
要将查询结果作为列表获取,我们只需在 TypedQuery 实现上调用 getResultList()。值得注意的是,Criteria API 是类型安全的;因此,尝试将 getResultList() 的结果分配给错误类型的列表会导致编译错误。
使用 Criteria API 更新数据
当 JPA Criteria API 首次添加到 JPA 2.0 时,它仅支持从数据库中选择数据。不支持修改现有数据。
JPA 2.1,在 Java EE 7 中引入,通过 CriteriaUpdate 接口增加了对更新数据库数据的支持;以下示例说明了如何使用它:
package net.ensode.glassfishbook.criteriaupdate.namedbean;
//imports omitted for brevity
@Named
@RequestScoped
public class CriteriaUpdateDemoBean {
@PersistenceContext
private EntityManager entityManager;
@Resource
private UserTransaction userTransaction;
private int updatedRows;
public String updateData() {
String retVal = "confirmation";
try {
userTransaction.begin();
insertTempData();
CriteriaBuilder criteriaBuilder =
entityManager.getCriteriaBuilder();
CriteriaUpdate<Address> criteriaUpdate = criteriaBuilder.createCriteriaUpdate(Address.class);
Root<Address> root = criteriaUpdate.from(Address.class);
criteriaUpdate.set("city", "New York");
criteriaUpdate.where(criteriaBuilder.equal(
root.get("city"), "New Yorc"));
Query query = entityManager.createQuery(criteriaUpdate);
updatedRows = query.executeUpdate();
userTransaction.commit();
} catch (Exception e) {
retVal = "error";
e.printStackTrace();
}
return retVal;
}
public int getUpdatedRows() {
return updatedRows;
}
public void setUpdatedRows(int updatedRows) {
this.updatedRows = updatedRows;
}
private void insertTempData() throws NotSupportedException,
SystemException, RollbackException, HeuristicMixedException,
HeuristicRollbackException {
//body omitted since it is not relevant to the discussion at hand
//full source code available as part of this book's code download
}
这个例子实际上是在寻找所有城市名称为 "New Yorc"(一个拼写错误)的数据库行,并将其替换为正确的拼写 "New York"。
就像上一个例子一样,我们通过在 EntityManager 实例上调用 getCriteriaBuilder() 方法来获取实现 CriteriaBuilder 接口的一个类的实例。
然后,我们通过在 CriteriaBuilder 实例上调用 createCriteriaUpdate() 来获取实现 CriteriaUpdate 类的实例。
下一步是获取一个通过在 CriteriaUpdate 实例上调用 from() 方法实现 Root 类的实例。
然后,我们在CriteriaUpdate上调用set()方法来指定更新后我们的行将具有的新值。set()方法的第一参数必须是一个与Entity类中的属性名称匹配的字符串,第二个参数必须是新值。
在这一点上,我们通过在CriteriaUpdate上调用where()方法,并传递由在CriteriaBuilder中调用的equal()方法返回的Predicate来构建where子句。
然后,我们通过在EntityManager上调用createQuery()并传递我们的CriteriaUpdate实例作为参数来获取Query实现。
最后,我们通过在Query实现上调用executeUpdate()来执行我们的查询,就像往常一样。
使用 Criteria API 删除数据
除了通过 Criteria API 添加对数据更新的支持外,JPA 2.1 还添加了使用新的CriteriaDelete接口批量删除数据库行的能力。以下代码片段说明了其用法:
package net.ensode.glassfishbook.criteriadelete.namedbean;
//imports omitted
@Named
@RequestScoped
public class CriteriaDeleteDemoBean {
@PersistenceContext
private EntityManager entityManager;
@Resource
private UserTransaction userTransaction;
private int deletedRows;
public String deleteData() {
String retVal = "confirmation";
try {
userTransaction.begin();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaDelete<Address> criteriaDelete = criteriaBuilder.createCriteriaDelete(Address.class);
Root<Address> root = criteriaDelete.from(Address.class);
criteriaDelete.where(criteriaBuilder.or(criteriaBuilder.equal(
root.get("city"), "New York"),
criteriaBuilder.equal(root.get("city"), "New York")));
Query query = entityManager.createQuery(criteriaDelete);
deletedRows = query.executeUpdate();
userTransaction.commit();
} catch (Exception e) {
retVal = "error";
e.printStackTrace();
}
return retVal;
}
public int getDeletedRows() {
return deletedRows;
}
public void setDeletedRows(int updatedRows) {
this.deletedRows = updatedRows;
}
}
要使用CriteriaDelete,我们首先像往常一样获取一个CriteriaBuilder实例,然后在我们的CriteriaBuilder实例上调用createCriteriaDelete()方法来获取CriteriaDelete的实现。
一旦我们有一个CriteriaDelete的实例,我们就像通常使用 Criteria API 那样构建where子句。
一旦我们构建了where子句,我们就获取Query接口的实现,并像往常一样在它上面调用executeUpdate()。
Bean Validation 支持
JPA 2.0 引入的另一个特性是对 JSR 303,Bean Validation 的支持。Bean Validation 支持允许我们使用 Bean Validation 注解注解我们的 JPA 实体。这些注解使我们能够轻松验证用户输入并执行数据清理。
利用 Bean Validation 非常简单。我们只需要用javax.validation.constraints包中定义的任何验证注解注解我们的 JPA 实体字段或 getter 方法。一旦我们的字段按照需要注解,EntityManager将阻止未验证的数据被持久化。
以下代码示例是本章前面看到的Customer JPA 实体的修改版本。它已经被修改以利用其某些字段中的 Bean Validation。
net.ensode.glassfishbook.beanvalidation.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Entity
@Table(name = "CUSTOMERS")
public class Customer implements Serializable
{
@Id
@Column(name = "CUSTOMER_ID")
private Long customerId;
@Column(name = "FIRST_NAME")
@NotNull
@Size(min=2, max=20)
private String firstName;
@Column(name = "LAST_NAME")
@NotNull
@Size(min=2, max=20)
private String lastName;
private String email;
public Long getCustomerId()
{
return customerId;
}
public void setCustomerId(Long customerId)
{
this.customerId = customerId;
}
public String getEmail()
{
return email;
}
public void setEmail(String email)
{
this.email = email;
}
public String getFirstName()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName = firstName;
}
public String getLastName()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName = lastName;
}
}
在这个例子中,我们使用了@NotNull注解来防止我们的实体firstName和lastName以null值持久化。我们还使用了@Size注解来限制这些字段的最低和最大长度。
这就是我们利用 JPA 中的 Bean Validation 所需要做的全部。如果我们的代码尝试持久化或更新一个未通过声明的验证的实体实例,将抛出一个类型为javax.validation.ConstraintViolationException的异常,并且实体将不会被持久化。
如我们所见,Bean Validation 在很大程度上自动化了数据验证,使我们免于手动编写验证代码。
除了前一个示例中讨论的两个注解之外,javax.validation.constraints包还包含一些额外的注解,我们可以使用这些注解来自动化 JPA 实体的验证。请参阅 Java EE 7 API 文档docs.oracle.com/javaee/7/api/以获取完整列表。
最后的注意事项
在本章的示例中,我们展示了如何通过作为控制器的 CDI 命名豆直接访问数据库。我们这样做是为了阐明观点,而不被细节所困扰。一般来说,从控制器直接访问数据库不是一种好的做法。数据库访问代码应该封装在数据访问对象(DAOs)中。
注意
关于 DAO 设计模式的更多信息,请参阅www.oracle.com/technetwork/java/dao-138818.html。
命名豆在采用模型-视图-控制器(MVC)设计模式时通常扮演控制器和/或模型的角色:这种做法如此普遍,以至于已成为 Java EE 应用程序的事实标准。
注意
关于 MVC 设计模式的更多信息,请参阅www.oracle.com/technetwork/java/mvc-140477.html。
此外,我们选择不在示例中展示任何用户界面代码,因为这与当前主题无关;然而,本章的代码下载包括 JSF 页面,这些页面调用本章中的命名豆并显示确认页面,一旦命名豆调用完成。
摘要
本章介绍了如何通过 JPA 访问数据库中的数据。
我们介绍了如何通过使用@Entity注解来标记 Java 类为 JPA 实体。此外,我们还介绍了如何通过@Table注解将实体映射到数据库表,以及如何通过@Column注解将实体字段映射到数据库列,以及如何通过@Id注解声明实体的主键。
使用javax.persistence.EntityManager接口查找、持久化和更新 JPA 实体也已被涵盖。
本章还涵盖了在 JPA 实体之间定义单向和双向的一对一、一对多和多对多关系。
此外,我们还介绍了如何通过开发自定义主键类来使用 JPA 组合主键。
然后,我们继续介绍如何使用 JPQL 从数据库中检索实体。
我们还讨论了额外的 JPA 功能,例如 Criteria API,它允许我们通过编程方式构建 JPA 查询;Metamodel API,它允许我们在使用 JPA 时利用 Java 的类型安全性;以及 Bean Validation,它允许我们通过简单地注解 JPA 实体字段来轻松验证输入。
在下一章中,我们将介绍企业 JavaBeans(EJBs)。
第四章:企业 JavaBeans
企业 JavaBeans 是封装应用程序业务逻辑的服务端组件。企业 JavaBeans 通过自动管理事务管理和安全性来简化应用程序开发。企业 JavaBeans 有两种类型:执行业务逻辑的会话 Bean,以及充当消息监听器的消息驱动 Bean。
熟悉 J2EE 的读者可能会注意到,在前一段落中没有提到实体 Bean。在 Java EE 5 中,实体 Bean 被废弃,取而代之的是Java 持久化 API(JPA)。尽管实体 Bean 仍然支持向后兼容,但执行对象关系映射的首选方式是通过 JPA。
本章将涵盖以下主题:
-
会话 Bean
-
一个简单的会话 Bean
-
更现实的例子
-
使用会话 Bean 实现 DAO 设计模式
-
单例会话 Bean
-
-
消息驱动 Bean
-
企业 JavaBeans 中的事务
-
容器管理的交易
-
Bean 管理的交易
-
-
企业 JavaBeans 生命周期
-
状态与会话 Bean 的生命周期
-
无状态会话 Bean 的生命周期
-
消息驱动 Bean 的生命周期
-
EJB 定时器服务
-
EJB 安全性
-
会话 Bean 简介
如我们之前提到的,会话 Bean 通常封装业务逻辑。在 Java EE 中,创建会话 Bean 只需要创建一个或两个工件,即 Bean 本身和可选的业务接口。这些工件需要用适当的注解装饰,以便让 EJB 容器知道它们是会话 Bean。
注意
J2EE 要求应用程序开发者创建几个工件以创建会话 Bean。这些工件包括 Bean 本身、本地或远程接口(或两者),本地 Home 或远程 Home 接口(或两者),以及 XML 部署描述符。正如我们将在本章中看到的,Java EE 极大地简化了 EJB 开发。
开发一个简单的会话 Bean
以下示例说明了一个非常简单的会话 Bean:
package net.ensode.glassfishbook;
import javax.ejb.Stateless;
@Stateless
public class SimpleSessionBean implements SimpleSession
{
private String message =
"If you don't see this, it didn't work!";
public String getMessage()
{
return message;
}
}
@Stateless注解让 EJB 容器知道这个类是一个无状态会话 Bean。有三种类型的会话 Bean:无状态、状态与会话和单例。在我们解释这些类型会话 Bean 之间的区别之前,我们需要明确 EJB 实例是如何提供给 EJB 客户端应用程序的。
当无状态或状态与会话 Bean 部署时,EJB 容器为每个会话 Bean 创建一系列实例。这通常被称为 EJB 池。当 EJB 客户端应用程序获取 EJB 的一个实例时,应用程序服务器(在我们的例子中是 GlassFish)将池中的一个实例提供给客户端应用程序。
有状态会话 bean 和无状态会话 bean 之间的区别在于,有状态会话 bean 与客户端保持会话状态,而无状态会话 bean 则不保持。简单来说,这意味着当一个 EJB 客户端应用程序获取一个有状态会话 bean 的实例时,我们可以保证 bean 中任何实例变量的值在方法调用之间是一致的。在有状态会话 bean 上修改任何实例变量都是安全的,因为它们将在下一次方法调用中保留其值。EJB 容器通过钝化有状态会话 bean 来保存会话状态,并在 bean 被激活时检索该状态。会话状态是有状态会话 bean 的生命周期比无状态会话 bean 和消息驱动 bean 复杂一些的原因(EJB 生命周期将在本章后面讨论)。
当 EJB 客户端应用程序请求一个无状态会话 bean 的实例时,EJB 容器可能会从池中提供任何 EJB 实例。由于我们不能保证每次方法调用都使用相同的实例,因此在无状态会话 bean 中设置的任何实例变量的值可能会“丢失”(它们实际上并没有丢失;修改发生在池中 EJB 的另一个实例中)。
除了被@Stateless注解装饰之外,之前的类并没有什么特别之处。请注意,它实现了一个名为SimpleSession的接口。这个接口是 bean 的业务接口。SimpleSession接口在下面的代码中展示:
package net.ensode.glassfishbook;
import javax.ejb.Remote;
@Remote
public interface SimpleSession
{
public String getMessage();
}
这个接口唯一特殊的地方是它被@Remote注解装饰。这个注解表示这是一个远程业务接口。这意味着该接口可能位于调用它的客户端应用程序不同的 JVM 中。远程业务接口甚至可以在网络上被调用。
业务接口也可以被@Local接口装饰。这个注解表示业务接口是一个本地业务接口。本地业务接口的实现必须与调用其方法的客户端应用程序在同一个 JVM 中。
由于远程业务接口可以从与客户端应用程序相同的 JVM 或不同的 JVM 中调用,乍一看,我们可能会倾向于将所有业务接口都做成远程的。在这样做之前,我们必须提醒自己,远程业务接口提供的灵活性伴随着性能上的代价,因为方法调用是在假设它们将在网络上进行的情况下进行的。事实上,大多数典型的 Java EE 应用程序由充当 EJB 客户端应用程序的 Web 应用程序组成;在这种情况下,客户端应用程序和 EJB 运行在同一个 JVM 上,因此本地接口比远程业务接口使用得更多。
一旦我们编译了会话 Bean 及其对应的企业接口,我们需要将它们放入一个 JAR 文件中并部署它们。就像处理 WAR 文件一样,部署 EJB JAR 文件最简单的方法是将它复制到 [glassfish 安装目录]/glassfish/domains/domain1/autodeploy。
既然我们已经看到了会话 Bean 及其对应的企业接口,让我们看一下一个客户端示例应用程序:
package net.ensode.glassfishbook;
import javax.ejb.EJB;
public class SessionBeanClient
{
@EJB
private static SimpleSession simpleSession;
private void invokeSessionBeanMethods()
{
System.out.println(simpleSession.getMessage());
System.out.println("\nSimpleSession is of type: "
+ simpleSession.getClass().getName());
}
public static void main(String[] args)
{
new SessionBeanClient().invokeSessionBeanMethods();
}
}
前面的代码只是声明了一个类型为 net.ensode.SimpleSession 的实例变量,这是我们的会话 Bean 的企业接口。这个实例变量被 @EJB 注解所装饰。@EJB 注解让 EJB 容器知道这个变量是一个会话 Bean 的企业接口。然后 EJB 容器注入一个企业接口的实现供客户端代码使用。
由于我们的客户端是一个独立的应用程序(而不是像 WAR 文件或另一个 EJB JAR 文件这样的 Java EE 艺术品),为了使其能够访问服务器上部署的代码,它必须被放入一个 JAR 文件中并通过 appclient 工具执行。appclient 工具是 GlassFish 特定的工具,允许独立 Java 应用程序访问部署到应用服务器的资源。这个工具可以在 [glassfish 安装目录]/glassfish/bin/ 找到。假设这个目录在 PATH 环境变量中,并且我们已经将我们的客户端代码放入了一个名为 simplesessionbeanclient.jar 的 JAR 文件中,我们将在命令行中键入以下命令来执行前面的客户端代码:
appclient -client simplesessionbeanclient.jar
执行前面的命令会产生以下控制台输出:
If you don't see this, it didn't work!
SimpleSession is of type: net.ensode.glassfishbook._SimpleSession_Wrapper
这是 SessionBeanClient 类的输出。
注意
我们正在使用 Maven 来构建我们的代码。在这个例子中,我们使用了 Maven Assembly 插件 (maven.apache.org/plugins/maven-assembly-plugin/) 来构建一个包含所有依赖项的客户端 JAR 文件;这使我们免去了在 appclient 工具的 -classpath 命令行选项中指定所有依赖 JAR 文件的麻烦。要构建这个 JAR 文件,只需在命令行中调用 mvn assembly:assembly 即可。
输出的第一行仅仅是我们在会话 Bean 中实现的 getMessage() 方法的返回值。输出的第二行显示了实现企业接口的类的完全限定名称。请注意,类名不是我们所写的会话 Bean 的完全限定名称;相反,实际上提供的是由 EJB 容器在幕后创建的企业接口的实现。
一个更实际的例子
在上一节中,我们看到了一个非常简单、类似于“Hello world”的示例。在本节中,我们将展示一个更实际的示例。会话 Bean 通常用作数据访问对象(DAO)。有时,它们用作 JDBC 调用和其他调用(例如获取或修改 JPA 实体)的包装器。在本节中,我们将采用后一种方法。
以下示例说明了如何在会话 Bean 中实现 DAO 设计模式。在查看 Bean 实现之前,让我们看看它对应的业务接口:
package net.ensode.glassfishbook;
import javax.ejb.Remote;
@Remote
public interface CustomerDao
{
public void saveCustomer(Customer customer);
public Customer getCustomer(Long customerId);
public void deleteCustomer(Customer customer);
}
如我们所见,之前的代码是一个实现三个方法的远程接口:the saveCustomer()方法将客户数据保存到数据库中,getCustomer()方法从数据库中获取客户数据,而deleteCustomer()方法从数据库中删除客户数据。其中两个方法以我们在第三章中开发的Customer实体实例作为参数。第三个方法getCustomer(),它接受一个Long值,代表我们希望从数据库中检索的Customer对象的 ID。
现在我们来看看实现先前业务接口的会话 Bean。正如我们将在下面的代码中看到的,在会话 Bean 中实现 JPA 代码的方式和在普通 Java 对象中实现的方式之间有一些区别:
package net.ensode.glassfishbook;
import javax.ejb.Stateful;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Stateful
public class CustomerDaoBean implements CustomerDao {
@PersistenceContext
private EntityManager entityManager;
public void saveCustomer(Customer customer) {
if (customer.getCustomerId() == null) {
saveNewCustomer(customer);
} else {
updateCustomer(customer);
}
}
private void saveNewCustomer(Customer customer) {
entityManager.persist(customer);
}
private void updateCustomer(Customer customer) {
entityManager.merge(customer);
}
public Customer getCustomer(Long customerId) {
Customer customer;
customer = entityManager.find(Customer.class, customerId);
return customer;
}
public void deleteCustomer(Customer customer) {
entityManager.remove(customer);
}
}
我们会话 Bean 与之前的 JPA 示例之间的主要区别在于,JPA 调用被包裹在UserTransaction.begin()和UserTransaction.commit()调用之间。我们必须这样做的原因是 JPA 调用需要被包裹在事务中;如果它们没有被包裹在事务中,大多数 JPA 调用将抛出TransactionRequiredException。在这种情况下,我们不需要像之前示例中那样显式地包裹 JPA 调用在事务中,因为会话 Bean 方法隐式地是事务性的;我们不需要做任何事情来使它们成为那样。这种默认行为被称为容器管理事务。容器管理事务将在本章后面详细讨论。
注意
如第三章中所述,使用 JPA 进行对象关系映射,当从一笔交易中检索 JPA 实体并将其更新到另一笔交易时,需要调用EntityManager.merge()方法来更新数据库中的数据。在这种情况下调用EntityManager.persist()将导致Cannot persist detached object异常。
从 Web 应用程序调用会话 Bean
通常,Java EE 应用程序由充当 EJB 客户端的 Web 应用程序组成。在 Java EE 6 之前,部署由 Web 应用程序和一个或多个会话 Bean 组成的 Java EE 应用程序的最常见方式是将 Web 应用程序的 WAR 文件和 EJB JAR 文件打包成一个 EAR(企业存档)文件。
Java EE 6 简化了由 EJB 和 Web 组件组成的应用程序的打包和部署。
在本节中,我们将开发一个 JSF 应用程序,其中包含一个 CDI 命名的 Bean 作为我们之前章节中讨论的 DAO 会话 Bean 的客户端。
为了使这个应用程序充当 EJB 客户端,我们将开发一个名为CustomerController的 Bean,以便将保存新客户到数据库的逻辑委托给我们在上一节中开发的CustomerDaoBean会话 Bean。我们将开发一个名为CustomerController的 Bean,如下面的代码所示:
package net.ensode.glassfishbook.jsfjpa;
//imports omitted for brevity
@Named
@RequestScoped
public class CustomerController implements Serializable {
@EJB
private CustomerDaoBean customerDaoBean;
private Customer customer;
private String firstName;
private String lastName;
private String email;
public CustomerController() {
customer = new Customer();
}
public String saveCustomer() {
String returnValue = "customer_saved";
try {
populateCustomer();
customerDaoBean.saveCustomer(customer);
} catch (Exception e) {
e.printStackTrace();
returnValue = "error_saving_customer";
}
return returnValue;
}
private void populateCustomer() {
if (customer == null) {
customer = new Customer();
}
customer.setFirstName(getFirstName());
customer.setLastName(getLastName());
customer.setEmail(getEmail());
}
//setters and getters omitted for brevity
}
如我们所见,我们只需声明一个CustomerDaoBean会话 Bean 的实例,并用@EJB注解装饰它,以便注入相应的 EJB 实例,然后调用 EJB 的saveCustomer()方法。
注意,我们直接将会话 Bean 的实例注入到我们的客户端代码中。我们可以这样做的原因是 Java EE 6 引入的一个特性。当使用 Java EE 6 或更高版本时,我们可以去掉本地接口,并在客户端代码中直接使用会话 Bean 实例。
现在我们已经修改了我们的 Web 应用程序以作为我们的会话 Bean 的客户端,我们需要将其打包成 WAR(Web 存档)文件并部署以使用它。
单例会话 Bean 简介
Java EE 6 中引入的一种新型会话 Bean 是单例会话 Bean。每个单例会话 Bean 在应用程序中只存在一个实例。
单例会话 Bean 对于缓存数据库数据很有用。在单例会话 Bean 中缓存常用数据可以提高性能,因为它大大减少了访问数据库的次数。常见的模式是在我们的 Bean 中有一个用@PostConstruct注解装饰的方法;在这个方法中,我们检索我们想要缓存的数据。然后我们提供一个 setter 方法供 Bean 的客户端调用。以下示例说明了这种技术:
package net.ensode.glassfishbook.singletonsession;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import net.ensode.glassfishbook.entity.UsStates;
@Singleton
public class SingletonSessionBean implements
SingletonSessionBeanRemote {
@PersistenceContext
private EntityManager entityManager;
private List<UsStates> stateList;
@PostConstruct
public void init() {
Query query = entityManager.createQuery(
"Select us from UsStates us");
stateList = query.getResultList();
}
@Override
public List<UsStates> getStateList() {
return stateList;
}
}
由于我们的 Bean 是单例的,所以所有客户端都会访问同一个实例,避免了多次查询数据库。此外,由于它是单例的,可以安全地指定实例变量,因为所有客户端都访问同一个 Bean 的实例。
异步方法调用
有时进行一些异步处理是有用的,也就是说,调用一个方法调用并立即将控制权返回给客户端,而无需让客户端等待方法完成。
在 Java EE 的早期版本中,调用 EJB 方法异步的唯一方法是使用消息驱动豆(将在下一节中讨论)。尽管消息驱动豆编写起来相对简单,但它们在使用之前确实需要一些配置,例如设置 JMS 消息队列或主题。
EJB 3.1 引入了@Asynchronous注解,它可以用来标记会话豆中的方法为异步。当 EJB 客户端调用异步方法时,控制权立即返回客户端,无需等待方法完成。
异步方法只能返回 void 或java.util.concurrent.Future接口的实现。Future接口是在 Java 5 中引入的,表示异步计算的最终结果。以下示例说明了这两种情况:
package net.ensode.glassfishbook.asynchronousmethods;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
@Stateless
public class AsynchronousSessionBean implements
AsynchronousSessionBeanRemote {
private static Logger logger = Logger.getLogger(
AsynchronousSessionBean.class.getName());
@Asynchronous
@Override
public void slowMethod() {
long startTime = System.currentTimeMillis();
logger.info("entering " + this.getClass().getCanonicalName()
+ ".slowMethod()");
try {
Thread.sleep(10000); //simulate processing for 10 seconds
} catch (InterruptedException ex) {
Logger.getLogger(AsynchronousSessionBean.class.getName()).
log(Level.SEVERE, null, ex);
}
logger.info("leaving " + this.getClass().getCanonicalName()
+ ".slowMethod()");
long endTime = System.currentTimeMillis();
logger.info("execution took " + (endTime - startTime)
+ " milliseconds");
}
@Asynchronous
@Override
public Future<Long> slowMethodWithReturnValue() {
try {
Thread.sleep(15000); //simulate processing for 15 seconds
} catch (InterruptedException ex) {
Logger.getLogger(AsynchronousSessionBean.class.getName()).
log(Level.SEVERE, null, ex);
}
return new AsyncResult<Long>(42L);
}
}
当我们的异步方法返回 void 时,我们只需要用@Asynchronous注解装饰方法,然后像往常一样从客户端代码中调用它。
如果我们需要返回值,则此值需要包装在jav.util.concurrent.Future接口的实现中。Java EE API 以javax.ejb.AsyncResult类的形式提供了一个便利的实现。Future接口和AsyncResult类都使用泛型,因此我们需要指定我们的返回类型作为这些实体的类型参数。
Future接口有几种我们可以用来取消异步方法执行、检查方法是否完成、获取方法的返回值以及检查方法是否被取消的方法。以下表格列出了这些方法:
| 方法 | 描述 |
|---|---|
cancel(boolean mayInterruptIfRunning) |
此方法将取消方法执行。如果布尔参数为true,则此方法将尝试取消方法执行,即使它已经在运行。 |
get() |
此方法将返回方法的“未包装”返回值;它将是方法返回的Future接口实现类型参数。 |
get(long timeout, TimeUnit unit) |
此方法将尝试获取方法的未包装返回值;返回值将是方法返回的Future接口实现类型参数。此方法将阻塞指定的时间参数。等待时间的单位由第二个参数确定,TimeUnit枚举具有 NANOSECONDS、MILLISECONDS、SECONDS、MINUTES 等常量。有关完整列表,请参阅其 Javadoc 文档。 |
isCancelled() |
如果方法已被取消,则此方法返回true;否则返回false。 |
isDone() |
如果方法已执行完成,则此方法返回true;否则返回false。 |
如我们所见,@Asynchronous 注解使得在不设置消息队列或主题的开销下进行异步调用变得非常容易。这无疑是 EJB 规范中的一个受欢迎的补充。
消息驱动 Bean
Java 消息服务(JMS)是一个用于不同应用程序之间异步通信的 Java EE API。JMS 消息存储在消息队列或消息主题中。
消息驱动 Bean 的目的是从 JMS 队列或 JMS 主题(根据所使用的消息域)消费消息(参见第八章,Java 消息服务)。消息驱动 Bean 必须使用 @MessageDriven 注解进行装饰。此注解的 mappedName 属性必须包含 Bean 将从中消费消息的 JNDI 名称的 JMS 消息队列或 JMS 消息主题。以下示例演示了一个简单的消息驱动 Bean:
package net.ensode.glassfishbook;
import javax.ejb.MessageDriven;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
@MessageDriven(mappedName = "jms/GlassFishBookQueue")
public class ExampleMessageDrivenBean implements MessageListener
{
public void onMessage(Message message)
{
TextMessage textMessage = (TextMessage) message;
try
{
System.out.print("Received the following message: ");
System.out.println(textMessage.getText());
System.out.println();
}
catch (JMSException e)
{
e.printStackTrace();
}
}
}
建议消息驱动 Bean 实现 javax.jms.MessageListener 接口,但这不是必需的。然而,消息驱动 Bean 必须有一个名为 onMessage() 的方法,其签名与前面的示例相同。
客户端应用程序永远不会直接调用消息驱动 Bean 的方法。相反,它们将消息放入消息队列或主题,然后 Bean 消费这些消息并相应地执行。前面的示例只是将消息打印到标准输出,因为消息驱动 Bean 在 EJB 容器中执行;标准输出被重定向到日志。要查看 GlassFish 服务器日志中的消息,请打开 [GlassFish 安装目录]/glassfish/domains/domain1/logs/server.log 文件。
企业 JavaBean 中的事务
如我们本章前面提到的,默认情况下,所有 EJB 方法都会自动被事务包装。这种默认行为被称为容器管理事务,因为事务是由 EJB 容器管理的。应用程序开发者也可以选择自己管理事务;这可以通过使用 Bean 管理事务来实现。这两种方法将在以下章节中讨论。
容器管理事务
由于 EJB 方法默认是事务性的,当从已经处于事务中的客户端代码调用 EJB 方法时,我们会遇到一个有趣的困境。EJB 容器应该如何表现?它应该挂起客户端事务,在一个新的事务中执行其方法,然后恢复客户端事务?或者它不应该创建新的事务,而是将方法作为客户端事务的一部分执行?或者它应该抛出异常?
默认情况下,如果 EJB 方法被已经在事务中的客户端代码调用,EJB 容器将简单地执行会话 bean 方法作为客户端事务的一部分。如果这不是我们需要的操作,我们可以通过使用@TransactionAttribute注解来改变它。这个注解有一个value属性,它决定了当会话 bean 方法在现有事务中调用以及在外部任何事务中调用时,EJB 容器将如何行为。value属性的值通常是定义在javax.ejb.TransactionAttributeType枚举中的常量。
以下表格列出了@TransactionAttribute注解的可能值:
| @TransactionAttribute value | 描述 |
|---|---|
TransactionAttributeType.MANDATORY |
强制方法作为客户端事务的一部分被调用。如果这个方法在任何事务之外被调用,它将抛出TransactionRequiredException异常 |
TransactionAttributeType.NEVER |
该方法永远不会在事务中执行。如果它作为客户端事务的一部分被调用,将抛出RemoteException异常。如果没有在客户端事务中调用该方法,则不会创建任何事务。 |
TransactionAttributeType.NOT_SUPPORTED |
该方法作为客户端事务的一部分被调用,客户端事务被挂起,方法在任意事务之外执行。方法执行完毕后,客户端事务将恢复。如果没有在客户端事务中调用该方法,则不会创建任何事务。 |
TransactionAttributeType.REQUIRED |
该方法作为客户端事务的一部分被调用,它作为该事务的一部分执行。如果方法在事务外部被调用,将为该方法创建一个新的事务。这是默认行为。 |
TransactionAttributeType.REQUIRES_NEW |
该方法作为客户端事务的一部分被调用,该事务被挂起,并为该方法创建一个新的事务。一旦方法执行完毕,客户端事务将恢复。如果方法在事务外部被调用,将为该方法创建一个新的事务。 |
TransactionAttributeType.SUPPORTS |
该方法作为客户端事务的一部分被调用,它作为该事务的一部分执行。如果方法在事务外部被调用,不会为该方法创建新的事务。 |
尽管在大多数情况下默认事务属性是合理的,但如果需要,能够覆盖这个默认设置是很好的。例如,事务会有性能影响。因此,能够关闭不需要事务的方法的事务是有益的。对于这种情况,我们可以在以下代码片段中看到,我们会对方法进行装饰:
@TransactionAttribute(value=TransactionAttributeType.NEVER)
public void doitAsFastAsPossible()
{
//performance critical code goes here.
}
可以通过在TransactionAttributeType枚举中用相应的常量注解方法来声明其他事务属性类型。
如果我们希望一致地覆盖会话 Bean 中所有方法的默认交易属性,我们可以用 @TransactionAttribute 注解装饰会话 Bean 类;其 value 属性的值将应用于会话 Bean 中的每个方法。
容器管理的交易在 EJB 方法中抛出异常时将自动回滚。此外,我们可以通过在对应于要讨论的会话 Bean 的 javax.ejb.EJBContext 实例上调用 setRollbackOnly() 方法来程序化地回滚容器管理的交易。以下是一个示例,展示了我们在本章前面看到的会话 Bean 的新版本,修改后可以在必要时回滚交易:
package net.ensode.glassfishbook;
//imports omitted
@Stateless
public class CustomerDaoRollbackBean implements CustomerDaoRollback
{
@Resource
private EJBContext ejbContext;
@PersistenceContext
private EntityManager entityManager;
@Resource(name = "jdbc/__CustomerDBPool")
private DataSource dataSource;
public void saveNewCustomer(Customer customer)
{
if (customer == null || customer.getCustomerId() != null)
{
ejbContext.setRollbackOnly();
}
else
{
customer.setCustomerId(getNewCustomerId());
entityManager.persist(customer);
}
}
public void updateCustomer(Customer customer)
{
if (customer == null || customer.getCustomerId() == null)
{
ejbContext.setRollbackOnly();
}
else
{
entityManager.merge(customer);
}
}
//Additional methods omitted for brevity.
}
在这个 DAO 会话 Bean 的版本中,我们删除了 saveCustomer() 方法,并将 saveNewCustomer() 和 updateCustomer() 方法设置为公共。现在,这些方法中的每一个都会检查我们试图执行的操作的 customerId 字段是否设置正确(对于插入为 null,对于更新则不为 null)。它还会检查要持久化的对象是否不为 null。如果任何检查导致无效数据,方法将简单地通过在注入的 EJBContext 实例上调用 setRollBackOnly() 方法来回滚交易,并且不会更新数据库。
Bean 管理的交易
正如我们所见,容器管理的交易使得编写包含在交易中的代码变得极其容易。毕竟,我们不需要做任何特别的事情来使它们变得这样;事实上,一些开发者在开发会话 Bean 时有时甚至没有意识到他们正在编写将具有交易性质的代码。容器管理的交易涵盖了我们将遇到的典型情况的大部分。然而,它们确实有一个限制:每个方法最多只能包含一个交易。使用容器管理的交易,无法实现生成多个交易的交易,这可以通过如以下代码所示的 Bean 管理的交易来完成:
package net.ensode.glassfishbook;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.persistence.EntityManager;
//imports omitted
@Stateless
@TransactionManagement(value = TransactionManagementType.BEAN)
public class CustomerDaoBmtBean implements CustomerDaoBmt
{
@Resource
private UserTransaction userTransaction;
@PersistenceContext
private EntityManager entityManager;
@Resource(name = "jdbc/__CustomerDBPool")
private DataSource dataSource;
public void saveMultipleNewCustomers(
List<Customer> customerList)
{
for (Customer customer : customerList)
{
try
{
userTransaction.begin();
customer.setCustomerId(getNewCustomerId());
entityManager.persist(customer);
userTransaction.commit();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
private Long getNewCustomerId()
{
Connection connection;
Long newCustomerId = null;
try
{
connection = dataSource.getConnection();
PreparedStatement preparedStatement =
connection.prepareStatement("select " +
"max(customer_id)+1 as new_customer_id " +
"from customers");
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet != null && resultSet.next())
{
newCustomerId = resultSet.getLong("new_customer_id");
}
connection.close();
}
catch (SQLException e)
{
e.printStackTrace();
}
return newCustomerId;
}
}
在这个示例中,我们实现了一个名为 saveMultipleNewCustomers() 的方法。此方法仅接受一个客户 List 作为其唯一参数。此方法的目的是在 ArrayList 中尽可能多地保存元素。保存实体时发生异常不应阻止方法尝试保存剩余元素。使用容器管理的交易无法实现此行为,因为如果在保存实体时抛出异常,则整个交易将回滚。实现此行为的唯一方法是使用 Bean 管理的交易。
如前例所示,我们通过在类上添加@TransactionManagement注解,并使用TransactionManagementType.BEAN作为其value属性的值(此属性的另一个有效值是TransactionManagementType.CONTAINER,但由于这是默认值,因此没有必要指定它)来声明会话 Bean 使用 Bean 管理的交易。
为了能够以编程方式控制事务,我们注入一个javax.transaction.UserTransaction的实例,然后在saveMultipleNewCustomers()方法内的for循环中使用它来开始和提交每个循环迭代的交易。
如果我们需要回滚一个 Bean 管理的交易,我们可以通过在适当的javax.transaction.UserTransaction实例上调用rollback()方法来实现。
在继续之前,值得注意的是,尽管本节中的所有示例都实现为会话 Bean,但这些概念同样适用于消息驱动 Bean。
企业 JavaBean 生命周期
企业 JavaBean 在其生命周期中会经历不同的状态。每种类型的 EJB 都有不同的状态。每种类型 EJB 的特定状态将在下一节中讨论。
有状态会话 Bean 的生命周期
对于熟悉 J2EE 先前版本的读者来说,可能会记得在先前版本的规范中,会话 Bean 必须实现javax.ejb.SessionBean接口。此接口提供了在会话 Bean 生命周期中特定点执行的方法。由SessionBean接口提供的方法包括:
-
ejbActivate() -
ejbPassivate() -
ejbRemove() -
setSessionContext(SessionContext ctx)
前三个方法旨在在 Bean 的生命周期中的特定点执行。在大多数情况下,这些方法的实现中没有什么可做的。这一事实导致了绝大多数会话 Bean 实现了这些方法的空版本。幸运的是,从 Java EE 5 开始,不再需要实现SessionBean接口,然而,如果需要,我们仍然可以编写将在 Bean 生命周期中的特定点执行的方法。我们可以通过使用特定的注解来装饰方法来实现这一点。
在解释可用于实现生命周期方法的可用的注解之前,有必要简要说明会话 Bean 的生命周期。有状态会话 Bean 的生命周期与无状态会话 Bean 的生命周期不同。
有状态会话 Bean 的生命周期包含三个状态:不存在、就绪和被动,如下所示:

在有状态会话 bean 部署之前,它处于不存在状态。部署成功后,EJB 容器为 bean 执行任何必需的依赖注入,bean 进入就绪状态。此时,bean 准备好被客户端应用程序调用其方法。
当一个有状态会话 bean 处于就绪状态时,EJB 容器可能会决定将其钝化,即将其从主内存移动到二级存储。当这种情况发生时,bean 进入被动状态。如果一段时间内没有访问有状态会话 bean 的实例,EJB 容器将把 bean 设置为不存在状态。默认情况下,GlassFish 将在 90 分钟的非活动状态后将有状态会话 bean 发送到不存在状态。此默认值可以通过以下步骤进行更改:
-
登录到 GlassFish 管理控制台。
-
展开左侧树中的配置节点。
-
展开server-config节点。
-
点击EJB 容器节点。
-
将页面滚动到页面底部并修改移除超时文本字段的值。
-
点击保存按钮,如下面的截图所示:

此技术为所有有状态会话 bean 设置超时值。如果我们需要修改特定会话 bean 的超时值,我们需要在包含会话 bean 的 JAR 文件中包含一个glassfish-ejb-jar.xml部署描述符。在这个部署描述符中,我们可以将超时值设置为<removal-timeout-in-seconds>元素的值,如下面的代码所示:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-ejb-jar PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 EJB 3.1//EN" "http://glassfish.org/dtds/glassfish-ejb-jar_3_1-1.dtd">
<glassfish-ejb-jar>
<enterprise-beans>
<ejb>
<ejb-name>MyStatefulSessionBean</ejb-name>
<bean-cache>
<removal-timeout-in-seconds>
600
</removal-timeout-in-seconds>
</bean-cache>
</ejb>
</enterprise-beans>
</glassfish-ejb-jar>
尽管我们不再需要为我们的会话 bean 创建ejb-jar.xml文件(这在 J2EE 规范的前版本中是必须的),但如果我们愿意,我们仍然可以创建一个。glassfish-ejb-jar.xml部署描述符中的<ejb-name>元素必须与ejb-jar.xml中同名元素的值匹配。如果我们选择不创建ejb-jar.xml文件,此值必须与 EJB 类的名称匹配。有状态会话 bean 的超时值必须是<removal-timeout-in-seconds>元素的值;正如元素名称所暗示的,时间单位是秒。在先前的例子中,我们将超时值设置为 600 秒,即 10 分钟。
在有状态会话 bean 中装饰了@PostActivate注解的任何方法将在有状态会话 bean 激活后立即被调用。这相当于在 J2EE 的早期版本中实现ejbActivate()方法。同样,任何装饰了@PrePassivate注解的方法将在有状态会话 bean 钝化之前被调用;这相当于在 J2EE 的早期版本中实现ejbPassivate()方法。
当一个处于就绪状态的有状态会话豆超时并被发送到“不存在”状态时,任何被 @PreDestroy 注解装饰的方法都会执行。如果会话豆在被动状态下超时,则不会执行被 @PreDestroy 注解装饰的方法。此外,如果状态会话豆的客户执行了一个被 @Remove 注解装饰的方法,则所有被 @PreDestroy 注解装饰的方法都会执行,并且豆将被标记为垃圾回收。用 @Remove 注解装饰一个方法等同于在 J2EE 规范的前版本中实现 ejbRemove() 方法。
@PostActivate、@PrePassivate 和 @Remove 注解仅适用于有状态会话豆,而 @PreDestroy 和 @PostConstruct 注解适用于有状态会话豆、无状态会话豆和消息驱动豆。
无状态会话豆的生命周期
无状态会话豆的生命周期只包含不存在和就绪状态,如下面的图所示:

无状态会话豆永远不会钝化。无状态会话豆的方法可以被 @PostConstruct 和 @PreDestroy 注解装饰。就像有状态会话豆一样,任何被 @PostConstruct 注解装饰的方法将在无状态会话豆从“不存在”状态变为“就绪”状态时执行,任何被 @PreDestroy 注解装饰的方法将在无状态会话豆从“就绪”状态变为“不存在”状态时执行。无状态会话豆永远不会钝化,任何无状态会话豆中的 @PrePassivate 和 @PostActivate 注解都将被 EJB 容器简单地忽略。
就像有状态会话豆一样,我们可以通过管理控制台来控制 GlassFish 如何管理无状态会话豆(和消息驱动豆)的生命周期,如下面的屏幕截图所示:

-
初始和最小池大小指的是池中的最小豆数
-
最大池大小指的是池中的最大豆数
-
池大小调整数量指的是当池空闲超时值到期时,将从池中移除多少豆
-
池空闲超时指的是在从池中移除豆之前需要经过的不活动秒数
之前的设置会影响所有 poolable EJB,例如无状态会话豆和无状态消息驱动豆。就像有状态会话豆一样,这些设置可以通过添加一个 GlassFish 特定的 glassfish-ejb-jar.xml 部署描述符来逐个案例覆盖,如下面的代码所示:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-ejb-jar PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 EJB 3.1//EN" "http://glassfish.org/dtds/glassfish-ejb-jar_3_1-1.dtd">
<glassfish-ejb-jar>
<enterprise-beans>
<ejb>
<ejb-name>MyStatelessSessionBean</ejb-name>
<bean-pool>
<steady-pool-size>10</steady-pool-size>
<max-pool-size>60</max-pool-size>
<resize-quantity>5</resize-quantity>
<pool-idle-timeout-in-seconds>
900
</pool-idle-timeout-in-seconds>
</bean-pool>
</ejb>
</enterprise-beans>
</glassfish-ejb-jar>
-
<steady-pool-size>行对应于 GlassFish 控制台中的初始和最小池大小 -
<max-pool-size>行对应于 GlassFish Web 控制台中的最大池大小。 -
<resize-quantity>行对应于 GlassFish Web 控制台中的池调整数量。 -
<pool-idle-timeout-in-seconds>行对应于 GlassFish Web 控制台中的池空闲超时。
消息驱动 Bean 生命周期
就像无状态会话 Bean 一样,消息驱动 Bean 只存在于“不存在”和“就绪”状态,如下所示:

上面的图像与之前的图像完全相同。消息驱动 Bean 与无状态会话 Bean 具有相同的生命周期。因此,用于说明生命周期的图像被重新使用。
消息驱动 Bean 可以有被@PostConstruct和@PreDestroy方法装饰的方法。被@PostConstruct方法装饰的方法将在 Bean 进入就绪状态之前执行。被@PreDestroy注解装饰的方法将在 Bean 进入不存在状态之前执行。
EJB 计时器服务简介
无状态会话 Bean 和消息驱动 Bean 可以有一个在固定时间间隔定期执行的方法。这可以通过使用EJB 计时器服务来实现。以下示例说明了如何利用此功能:
package net.ensode.glassfishbook;
//imports omitted
@Stateless
public class EjbTimerExampleBean implements EjbTimerExample
{
private static Logger logger = Logger.getLogger(EjbTimerExampleBean.class
.getName());
@Resource
TimerService timerService;
public void startTimer(Serializable info)
{
Timer timer = timerService.createTimer
(new Date(), 5000, info);
}
public void stopTimer(Serializable info)
{
Timer timer;
Collection timers = timerService.getTimers();
for (Object object : timers)
{
timer = ((Timer) object);
if (timer.getInfo().equals(info))
{
timer.cancel();
break;
}
}
}
@Timeout
public void logMessage(Timer timer)
{
logger.info("This message was triggered by :" +
timer.getInfo() + " at "
+ System.currentTimeMillis());
}
}
在我们的示例中,我们通过使用@Resource注解装饰该类型的实例变量,注入了javax.ejb.TimerService接口的实现。然后,我们通过调用TimerService实例的createTimer()方法创建了一个计时器。
createTimer()方法有几个重载版本。我们选择使用的方法接受一个java.util.Date实例作为其第一个参数;此参数用于指示计时器应该何时首次到期(关闭)。在示例中,我们选择使用一个新的Date类实例,这实际上使得计时器立即到期。createTimer()方法的第二个参数是在计时器再次到期之前要等待的时间,以毫秒为单位。在我们的示例中,计时器被设置为每五秒到期一次。createTimer()方法的第三个参数可以是实现java.io.Serializable接口的任何类的实例。由于单个 EJB 可以同时执行多个计时器,因此此第三个参数用于唯一标识每个计时器。如果我们不需要标识计时器,可以将null作为此参数的值传递。
小贴士
调用TimerService.createTimer()方法的 EJB 方法必须来自 EJB 客户端。将此调用放置在带有@PostConstruct注解的 EJB 方法中,以便在 Bean 处于就绪状态时自动启动计时器,将会导致抛出IllegalStateException异常。
我们可以通过调用其 cancel() 方法来停止计时器。无法直接获取与 EJB 关联的单个计时器。我们需要做的是在 TimerService 实例上调用 getTimers() 方法,该实例与 EJB 相关联;此方法将返回一个包含与 EJB 关联的所有计时器的集合。然后我们可以遍历集合,通过调用其 getInfo() 方法来取消正确的计时器。此方法将返回我们传递给 createTimer() 方法的 Serializable 对象。
最后,任何带有 @Timeout 注解的 EJB 方法在计时器到期时都会执行。带有此注解的方法必须返回 void 并接受一个类型为 javax.ejb.Timer 的单个参数。在我们的例子中,该方法只是将一条消息写入服务器日志。
下面的类是之前 EJB 的独立客户端:
package net.ensode.glassfishbook;
import javax.ejb.EJB;
public class Client
{
@EJB
private static EjbTimerExample ejbTimerExample;
public static void main(String[] args)
{
try
{
System.out.println("Starting timer 1...");
ejbTimerExample.startTimer("Timer 1");
System.out.println("Sleeping for 2 seconds...");
Thread.sleep(2000);
System.out.println("Starting timer 2...");
ejbTimerExample.startTimer("Timer 2");
System.out.println("Sleeping for 30 seconds...");
Thread.sleep(30000);
System.out.println("Stopping timer 1...");
ejbTimerExample.stopTimer("Timer 1");
System.out.println("Stopping timer 2...");
ejbTimerExample.stopTimer("Timer 2");
System.out.println("Done.");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
之前的例子只是启动了一个计时器,等待几秒钟,然后启动第二个计时器。然后它睡眠 30 秒,然后停止两个计时器。部署 EJB 并执行客户端后,我们应该在服务器日志中看到一些如下所示的条目:
[2013-08-26T20:44:55.180-0400] [glassfish 4.0] [INFO] [] [net.ensode.glassfishbook.EjbTimerExampleBean] [tid: _ThreadID=147 _ThreadName=__ejb-thread-pool1] [timeMillis: 1377564295180] [levelValue: 800] [[
This message was triggered by :Timer 1 at 1377564295180]]
[2013-08-26T20:44:57.203-0400] [glassfish 4.0] [INFO] [] [net.ensode.glassfishbook.EjbTimerExampleBean] [tid: _ThreadID=148 _ThreadName=__ejb-thread-pool2] [timeMillis: 1377564297203] [levelValue: 800] [[
This message was triggered by :Timer 2 at 1377564297203]]
[2013-08-26T20:44:58.888-0400] [glassfish 4.0] [INFO] [] [net.ensode.glassfishbook.EjbTimerExampleBean] [tid: _ThreadID=149 _ThreadName=__ejb-thread-pool3] [timeMillis: 1377564298888] [levelValue: 800] [[
This message was triggered by :Timer 1 at 1377564298888]]
[2013-08-26T20:45:01.156-0400] [glassfish 4.0] [INFO] [] [net.ensode.glassfishbook.EjbTimerExampleBean] [tid: _ThreadID=150 _ThreadName=__ejb-thread-pool4] [timeMillis: 1377564301156] [levelValue: 800] [[
This message was triggered by :Timer 2 at 1377564301156]]
每当其中一个计时器到期时,都会创建这些条目。
基于日历的 EJB 计时器表达式
上一个示例中的例子有一个缺点:会话 beans 中的 startTimer() 方法必须从客户端调用才能启动计时器。这种限制使得计时器在 bean 部署后立即启动变得困难。
Java EE 6 引入了基于日历的 EJB 计时器表达式。基于日历的表达式允许我们的会话 beans 中的一个或多个方法在特定的日期和时间执行。例如,我们可以配置我们的一个方法在每晚 8:10 p.m. 执行,这正是以下示例所做的那样:
package com.ensode.glassfishbook.calendarbasedtimer;
import java.util.logging.Logger;
import javax.ejb.Stateless;
import javax.ejb.LocalBean;
import javax.ejb.Schedule;
@Stateless
@LocalBean
public class CalendarBasedTimerEjbExampleBean {
private static Logger logger = Logger.getLogger(
CalendarBasedTimerEjbExampleBean.class.getName());
@Schedule(hour = "20", minute = "10")
public void logMessage() {
logger.info("This message was triggered at:"
+ System.currentTimeMillis());
}
}
正如您在这个例子中所看到的,我们通过 javax.ejb.Schedule 注解设置方法执行的时间。在这个特定的例子中,我们通过将 @Schedule 注解的 hour 属性设置为 "20",并将其分钟属性设置为 "10" 来设置我们的方法在晚上 8:10 p.m. 执行。hour 属性是基于 24 小时的;20 点相当于晚上 8:00。
@Schedule 注解有几个其他属性,允许在指定方法何时执行时具有很大的灵活性;例如,我们可以让一个方法在每月的第三个星期五执行,或者月底的最后一天,等等。
下表列出了 @Schedule 注解中所有允许我们控制注解方法何时执行的属性:
| 属性 | 描述 | 示例值 | 默认值 |
|---|---|---|---|
dayOfMonth |
月份中的某一天。 | "3":月份的第三天"Last":月底的最后一天"-2":月底前两天"1st Tue":月份的第一个星期二 |
"*" |
dayOfWeek |
周中的某一天 | "3":每周三"Thu":每周四 |
"*" |
hour |
一天中的小时(基于 24 小时制) | "14":下午 2 点 |
"0" |
minute |
小时的分钟 | "10":小时后的十分钟 |
"0" |
month |
年份的月份 | "2":二月"March":三月 |
"*" |
second |
分钟的第二位 | "5":分钟后的五秒 |
"0" |
timezone |
时区 ID | "America/New York" |
"" |
year |
四位数的年份 | "2010" |
"*" |
除了单个值之外,大多数属性接受星号(`"*"”)作为通配符,这意味着被注释的方法将定期执行(每天、每小时等)。
此外,我们可以通过逗号分隔值来指定多个值,例如,如果我们需要一种方法在每个星期二和星期四执行,我们可以将该方法注释为@Schedule(dayOfWeek="Tue, Thu")。
我们也可以指定一个值的范围;第一个和最后一个值由连字符(-)分隔。要执行从星期一到星期五的方法,我们可以使用@Schedule(dayOfWeek="Mon-Fri")。
此外,我们可以指定方法需要每 n 个时间单位执行一次(例如,每天、每 2 小时、每 10 分钟等)。要执行类似的操作,我们可以使用@Schedule(hour="*/12"),这将使方法每 12 小时执行一次。
如我们所见,@Schedule注解在指定方法执行时间方面提供了很大的灵活性。此外,它还提供了我们不需要客户端调用即可激活调度的优势。它还具有使用类似于 cron 的语法的优势;因此,熟悉此 Unix 工具的开发者将感到使用此注解非常得心应手。
EJB 安全
企业 JavaBeans 允许我们声明性地决定哪些用户可以访问其方法。例如,某些方法可能仅供具有特定角色的用户使用。一个典型的场景是,只有具有管理员角色的用户才能添加、删除或修改系统中的其他用户。
以下示例是本章前面看到的 DAO 会话 bean 的略微修改版本。在这个版本中,一些之前是私有的方法被改为公开。此外,会话 bean 被修改为只允许具有特定角色的用户访问其方法。
package net.ensode.glassfishbook;
// imports omitted
@Stateless
@RolesAllowed("appadmin")
public class CustomerDaoBean implements CustomerDao
{
@PersistenceContext
private EntityManager entityManager;
@Resource(name = "jdbc/__CustomerDBPool")
private DataSource dataSource;
public void saveCustomer(Customer customer)
{
if (customer.getCustomerId() == null)
{
saveNewCustomer(customer);
}
else
{
updateCustomer(customer);
}
}
public Long saveNewCustomer(Customer customer)
{
entityManager.persist(customer);
return customer.getCustomerId();
}
public void updateCustomer(Customer customer)
{
entityManager.merge(customer);
}
@RolesAllowed(
{ "appuser", "appadmin" })
public Customer getCustomer(Long customerId)
{
Customer customer;
customer = entityManager.find(Customer.class, customerId);
return customer;
}
public void deleteCustomer(Customer customer)
{
entityManager.remove(customer);
}
}
如我们所见,我们使用@RolesAllowed注解声明哪些角色可以访问方法。此注解可以接受单个字符串或字符串数组作为其参数。当使用单个字符串作为此注解的参数时,只有具有该参数指定的角色的用户可以访问该方法。如果使用字符串数组作为参数,则具有数组元素中指定的任何角色的用户都可以访问该方法。
可以使用@RolesAllowed注解来装饰 EJB 类,在这种情况下,其值适用于 EJB 中的所有方法,或者适用于一个或多个方法。在后一种情况下,其值仅适用于被注解的方法。如果,像我们之前的例子一样,EJB 类及其一个或多个方法都被@RolesAllowed注解装饰,则方法级别的注解具有优先权。
应用程序角色需要映射到安全域的组名称(有关详细信息,请参阅第九章[Securing Java EE Applications],Securing Java EE Applications)。此映射以及要使用的域设置在glassfish-ejb-jar.xml部署描述符中,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-ejb-jar PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 EJB 3.1//EN" "http://glassfish.org/dtds/glassfish-ejb-jar_3_1-1.dtd">
<glassfish-ejb-jar>
<security-role-mapping>
<role-name>appuser</role-name>
<group-name>appuser</group-name>
</security-role-mapping>
<security-role-mapping>
<role-name>appadmin</role-name>
<group-name>appadmin</group-name>
</security-role-mapping>
<enterprise-beans>
<ejb>
<ejb-name>CustomerDaoBean</ejb-name>
<ior-security-config>
<as-context>
<auth-method>username_password</auth-method>
<realm>file</realm>
<required>true</required>
</as-context>
</ior-security-config>
</ejb>
</enterprise-beans>
</glassfish-ejb-jar>
glassfish-ejb-jar.xml中的<security-role-mapping>元素执行应用程序角色与安全域组的映射。<role-name>子元素的值必须包含应用程序角色;此值必须与@RolesAllowed注解中使用的值匹配。<group-name>子元素的值必须包含 EJB 使用的安全域中的安全组名称。在我们的示例中,我们将两个应用程序角色映射到安全域中的相应组。尽管在这个特定的例子中应用程序角色的名称和安全组的名称匹配,但这并不一定需要如此。
小贴士
自动匹配角色到安全组
可以自动将任何应用程序角色匹配到安全域中具有相同名称的安全组。这可以通过登录到 GlassFish Web 控制台,点击配置节点,然后点击安全,接着点击标记为默认主体到角色映射的复选框,并保存此配置更改来实现。
如我们的示例所示,用于身份验证的安全域定义在<as-context>元素的<realm>子元素中。此子元素的值必须与应用服务器中有效安全域的名称匹配。<as-context>元素的其它子元素包括<auth-method>,此元素的唯一有效值是username_password,以及<required>,其唯一有效值是true和false。
客户端身份验证
如果访问受保护 EJB 的客户端代码是某个已经通过其用户界面登录(即用户通过 Web 界面登录)的 Web 应用程序的一部分,那么用户的凭据将用于确定用户是否应该被允许访问他们试图执行的方法。
独立客户端必须通过appclient实用程序执行。以下代码演示了之前受保护的会话 Bean 的典型客户端:
package net.ensode.glassfishbook;
import javax.ejb.EJB;
public class Client
{
@EJB
private static CustomerDao customerDao;
public static void main(String[] args)
{
Long newCustomerId;
Customer customer = new Customer();
customer.setFirstName("Mark");
customer.setLastName("Butcher");
customer.setEmail("butcher@phony.org");
System.out.println("Saving New Customer...");
newCustomerId = customerDao.saveNewCustomer(customer);
System.out.println("Retrieving customer...");
customer = customerDao.getCustomer(newCustomerId);
System.out.println(customer);
}
}
如我们所见,代码中并没有执行任何用于验证用户的操作。会话 Bean 通过 @EJB 注解简单地注入到代码中,并按常规使用。这是因为 appclient 工具在通过 appclient 工具调用客户端代码后负责验证用户,如下所示:
appclient -client ejbsecurityclient.jar
当 appclient 工具尝试在 EJB 上调用安全方法时,它会向用户展示一个登录窗口,如下面的截图所示:

假设凭证正确且用户具有适当的权限,EJB 代码将执行,我们应该从 Client 类看到预期的输出:
Saving New Customer...
Retrieving customer...
customerId = 29
firstName = Mark
lastName = Butcher
email = butcher@phony.org
摘要
在本章中,我们介绍了如何通过无状态和有状态会话 Bean 实现业务逻辑。此外,我们还介绍了如何实现消息驱动 Bean 以消费 JMS 消息。
我们还解释了如何利用 EJB 的事务性来简化实现数据访问对象(DAO)模式。
此外,我们解释了容器管理事务的概念以及如何使用适当的注解来控制它们。我们还解释了在容器管理事务不足以满足我们的要求时,如何实现 Bean 管理事务。
我们还涵盖了不同类型的企业 JavaBeans 的生命周期,包括如何让 EJB 容器在生命周期中的特定点自动调用 EJB 方法。
我们还解释了如何利用 EJB 定时器服务让 EJB 容器定期调用 EJB 方法。
最后,我们解释了如何通过注释 EJB 类和/或方法以及添加适当的条目到 glassfish-ejb-jar.xml 部署描述符中,以确保 EJB 方法只被授权用户调用。
在下一章中,我们将介绍上下文和依赖注入。
第五章。上下文和依赖注入
上下文和依赖注入(CDI)在 Java EE 6 中添加到 Java EE 规范中。它为 Java EE 开发者提供了之前不可用的几个优点,例如允许任何 JavaBean 用作JavaServer Faces(JSF)管理 Bean,包括无状态和有状态会话 Bean。正如其名所示,CDI 简化了 Java EE 应用程序中的依赖注入。
在本章中,我们将涵盖以下主题:
-
命名豆
-
依赖注入
-
范围
-
标准化
命名豆
CDI 通过@Named注解为我们提供了命名 Bean 的能力。命名 Bean 允许我们轻松地将我们的 Bean 注入到依赖它们的其他类中(参见依赖注入部分),并且可以通过统一表达式语言轻松地从 JSF 页面引用它们。
以下示例展示了@Named注解的实际应用:
package net.ensode.cdidependencyinjection.beans;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named
@RequestScoped
public class Customer {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
如我们所见,我们只需要用@Named注解装饰我们的类来命名我们的类。默认情况下,Bean 的名称将是类名,其首字母转换为小写;在我们的例子中,Bean 的名称将是customer。如果我们想使用不同的名称,我们可以通过设置@Named注解的value属性来实现。例如,如果我们想在前面的例子中使用customerBean作为我们的 Bean 名称,我们可以通过修改@Named注解如下所示:
@Named(value="customerBean")
或者,我们也可以简单地使用以下代码:
@Named("customerBean")
由于value属性名称不需要指定,如果我们不使用属性名称,则隐含value。
CDI 名称可用于通过统一表达式语言从 JSF 页面访问我们的 Bean,如下面的代码所示:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
>
<h:head>
<title>Enter Customer Information</title>
</h:head>
<h:body>
<h:form>
<h:panelGrid columns="2">
<h:outputLabel for="firstName" value="First Name"/>
<h:inputText id="firstName"
value="#{customer.firstName}"/>
<h:outputLabel for="lastName" value="Last Name"/>
<h:inputText id="lastName"
value="#{customer.lastName}"/>
<h:panelGroup/>
</h:panelGrid>
</h:form>
</h:body>
</html>
如我们所见,命名 Bean 从 JSF 页面访问的方式与标准 JSF 管理 Bean 完全相同。这允许 JSF 访问任何命名 Bean,将 Java 代码与 JSF API 解耦。
当部署和执行时,我们的简单应用程序看起来如下截图(在用户输入一些数据后显示):

依赖注入
依赖注入是一种用于向 Java 类提供外部依赖的技术。Java EE 5 通过@Resource注解引入了依赖注入;然而,此注解仅限于注入资源,如数据库连接、JMS 资源等。CDI 包括@Inject注解,可用于将 Java 类的实例注入到任何依赖对象中。
JSF 应用程序通常遵循模型-视图-控制器(MVC)设计模式。因此,一些 JSF 管理 Bean 经常在模式中扮演控制器的角色,而其他 Bean 则扮演模型的角色。这种方法通常要求控制器管理 Bean 能够访问一个或多个模型管理 Bean。
由于前一段描述的模式,最常被问到的 JSF 问题之一是如何从一个管理 bean 访问另一个。有不止一种方法可以实现这一点;然而,在 CDI 之前,没有一种方法简单直接。在 CDI 之前,最简单的方法是在控制器管理 bean 中声明一个管理属性,这需要修改应用程序的faces-config.xml文件;另一种方法是使用如下代码:
ELContext elc = FacesContext.getCurrentInstance().getELContext();
SomeBean someBean
= (SomeBean) FacesContext.getCurrentInstance().getApplication()
.getELResolver().getValue(elc, null, "someBean");
在这个示例中,someBean是faces-config.xml文件中指定的 bean 的名称。正如我们所看到的,这两种方法都不简单,也不容易记住。幸运的是,由于 CDI 的依赖注入功能,像这样的代码现在不再需要了,如下面的代码所示:
package net.ensode.cdidependencyinjection.ejb;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Named;
@Named
@RequestScoped
public class CustomerController {
private static final Logger logger = Logger.getLogger(
CustomerController.class.getName());
@Inject
private Customer customer;
public String saveCustomer() {
logger.info("Saving the following information \n" + customer.
toString());
//If this was a real application, we would have code to save
//customer data to the database here.
return "confirmation";
}
}
注意,我们初始化客户实例所需要做的只是用@Inject注解装饰它。当 bean 由应用服务器构建时,一个Customer bean 的实例会自动注入到这个字段中。注意,注入的 bean 在saveCustomer()方法中被使用。正如我们所看到的,CDI 使得从一个 bean 访问另一个 bean 变得非常简单,与之前 Java EE 规范版本中必须使用的代码相比,差距甚远。
使用 CDI 限定符
在某些情况下,我们希望注入到代码中的 bean 的类型可能是一个接口或 Java 超类,但我们可能对注入子类或实现该接口的类感兴趣。对于这种情况,CDI 提供了限定符,我们可以使用它们来指明我们希望注入到代码中的特定类型。
CDI 限定符是一个必须用@Qualifier注解装饰的注解。这个注解可以用来装饰我们希望限定的特定子类或接口实现。此外,客户端代码中的注入字段也需要用限定符装饰。
假设我们的应用程序可能有一种特殊的客户类型;例如,常客可能会被赋予高级客户的身份。为了处理这些高级客户,我们可以扩展我们的Customer命名 bean,并用以下限定符装饰它:
package net.ensode.cdidependencyinjection.qualifiers;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.inject.Qualifier;
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Premium {
}
如我们之前提到的,限定符是标准的注释;它们通常具有运行时保留功能,可以针对方法、字段、参数或类型进行标注,正如前一个示例中通过@Retention注解的值所展示的那样。限定符与标准注释之间的唯一区别是,限定符被@Qualifier注解所装饰。
一旦我们设置了限定符,我们需要用它来装饰特定的子类或接口实现:
package net.ensode.cdidependencyinjection.beans;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import net.ensode.cdidependencyinjection.qualifiers.Premium;
@Named
@RequestScoped
@Premium
public class PremiumCustomer extends Customer {
private Integer discountCode;
public Integer getDiscountCode() {
return discountCode;
}
public void setDiscountCode(Integer discountCode) {
this.discountCode = discountCode;
}
}
一旦我们装饰了需要限定的特定实例,我们就可以在客户端代码中使用我们的限定符来指定所需的精确依赖类型:
package net.ensode.cdidependencyinjection.beans;
import java.util.Random;
import java.util.logging.Logger;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import net.ensode.cdidependencyinjection.qualifiers.Premium;
@Named
@RequestScoped
public class CustomerController {
private static final Logger logger = Logger.getLogger(
CustomerController.class.getName());
@Inject
@Premium
private Customer customer;
public String saveCustomer() {
PremiumCustomer premiumCustomer = (PremiumCustomer) customer;
premiumCustomer.setDiscountCode(generateDiscountCode());
logger.info("Saving the following information \n"
+ premiumCustomer.getFirstName() + " "
+ premiumCustomer.getLastName()
+ ", discount code = "
+ premiumCustomer.getDiscountCode());
//If this was a real application, we would have code to save
//customer data to the database here.
return "confirmation";
}
public Integer generateDiscountCode() {
return new Random().nextInt(100000);
}
}
由于我们使用@Premium限定符来装饰客户字段,因此将PremiumCustomer类的实例注入到该字段中,因为这个类也被装饰了@Premium 限定符。
就我们的 JSF 页面而言,我们像往常一样通过名称访问我们的命名 bean:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
>
<h:head>
<title>Enter Customer Information</title>
</h:head>
<h:body>
<h:form>
<h:panelGrid columns="2">
<h:outputLabel for="firstName" value="First Name"/>
<h:inputText id="firstName"
value="#{premiumCustomer.firstName}"/>
<h:outputLabel for="lastName" value="Last Name"/>
<h:inputText id="lastName"
value="#{premiumCustomer.lastName}"/>
<h:outputLabel for="discountCode" value="Discount Code"/>
<h:inputText id="discountCode"
value="#{premiumCustomer.discountCode}"/>
<h:panelGroup/>
<h:commandButton value="Submit"
action="#{customerController.saveCustomer}"/>
</h:panelGrid>
</h:form>
</h:body>
</html>
在这个例子中,我们使用 bean 的默认名称,即类名,首字母小写。
从用户的角度来看,我们的简单应用程序渲染和操作就像一个普通的(即不使用 CDI)JSF 应用程序一样。请看下面的屏幕截图:

命名 bean 作用域
就像 JSF 管理的 bean 一样,CDI 命名 bean 也有作用域。这意味着 CDI bean 是上下文对象。当需要命名 bean 时,无论是由于注入还是因为它被 JSF 页面引用,CDI 都会在该 bean 所属的作用域中查找 bean 的实例,并将其注入到依赖代码中。如果没有找到实例,就会创建一个并存储在适当的作用域中供将来使用。不同的作用域是 bean 存在的上下文。
下表列出了不同的有效 CDI 作用域:
| 作用域 | 注解 | 描述 |
|---|---|---|
| 请求 | @RequestScoped |
请求作用域的 bean 在单个请求的持续期间共享。单个请求可能指的是 HTTP 请求、对 EJB 方法的方法调用、Web 服务调用或向消息驱动 bean 发送 JMS 消息。 |
| 会话 | @ConversationScoped |
会话作用域可以跨越多个请求,但它通常比会话作用域短。 |
| 会话 | @SessionScoped |
会话作用域的 bean 在 HTTP 会话的所有请求之间共享。每个应用程序用户都获得自己的会话作用域 bean 实例。 |
| 应用 | @ApplicationScoped |
应用作用域的 bean 在整个应用程序生命周期中存在。此作用域中的 bean 在用户会话之间共享。 |
| 依赖 | @Dependent |
依赖作用域的 bean 不共享;每次注入依赖作用域的 bean 时,都会创建一个新的实例。 |
如我们所见,CDI 包括了 JSF 支持的所有作用域;它还增加了一些自己的作用域。CDI 的请求作用域与 JSF 的请求作用域不同,其中请求不一定指的是 HTTP 请求;它可能只是一个对 EJB 方法的调用、一个 Web 服务调用或向消息驱动 bean 发送 JMS 消息。
JSF 中不存在会话作用域。这个作用域比请求作用域长,但比会话作用域短,通常跨越三页或更多。希望访问会话作用域 bean 的类必须注入javax.enterprise.context.Conversation实例。在我们想要开始会话的点,必须在这个对象上调用begin()方法。在我们想要结束会话的点,必须在这个对象上调用end()方法。
CDI 的会话作用域的行为与其 JSF 对应物相同。会话作用域 bean 的生命周期与 HTTP 会话的生命周期绑定。
CDI 的应用程序作用域的行为也与其 JSF 中的等效作用域相同。应用程序作用域 bean 与应用程序的生命周期绑定。每个应用程序都有一个应用程序作用域 bean 的单例存在,这意味着相同的实例对所有 HTTP 会话都是可访问的。
就像对话作用域一样,CDI 的依赖作用域在 JSF 中不存在。每次需要时都会实例化一个新的依赖作用域 bean,通常是在将其注入依赖于它的类时。
假设我们想要让用户输入一些将被存储在单个命名 bean 中的数据;然而,这个 bean 有几个字段,因此,我们希望将数据输入分成几个页面。这是一个相当常见的情况,并且使用 JSF 的先前版本(JSF 2.2 添加了 Faces Flows 来解决这个问题;请参阅第二章,JavaServer Faces)或 servlet API 处理起来并不容易。这种情况不容易管理的原因是,我们只能将类放在请求作用域中,在这种情况下,类在每次请求后都会被销毁,从而丢失其数据;或者放在会话作用域中,在这种情况下,类在所需之后很长时间仍然留在内存中。
对于此类情况,CDI 的对话作用域是一个很好的解决方案,如下面的代码所示:
package net.ensode.conversationscope.model;
import java.io.Serializable;
import javax.enterprise.context.ConversationScoped;
import javax.inject.Named;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
@Named
@ConversationScoped
public class Customer implements Serializable {
private String firstName;
private String middleName;
private String lastName;
private String addrLine1;
private String addrLine2;
private String addrCity;
private String state;
private String zip;
private String phoneHome;
private String phoneWork;
private String phoneMobile;
//getters and setters omitted for brevity
@Override
public String toString() {
return ReflectionToStringBuilder.reflectionToString(this);
}
}
我们通过使用@ConversationScoped注解来装饰我们的 bean,声明我们的 bean 是会话作用域的。会话作用域 bean 还需要实现java.io.Serializable。除了这两个要求外,我们的代码没有特别之处。它是一个简单的 JavaBean 代码,具有私有属性和相应的 getter 和 setter 方法。
注意
我们在我们的代码中使用 Apache commons-lang库来轻松实现 bean 的toString()方法。commons-lang库有多个这样的实用方法,实现了频繁需要的、编写起来繁琐的功能。commons-lang可在中央 Maven 仓库中找到,网址为commons.apache.org/lang。
除了注入我们的作用域为对话的 bean 外,我们的客户端代码还必须注入一个javax.enterprise.context.Conversation实例,如下面的示例所示:
package net.ensode.conversationscope.controller;
import java.io.Serializable;
import javax.enterprise.context.Conversation;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import net.ensode.conversationscope.model.Customer;
@Named
@RequestScoped
public class CustomerInfoController implements Serializable {
@Inject
private Conversation conversation;
@Inject
private Customer customer;
public String customerInfoEntry() {
conversation.begin();
System.out.println(customer);
return "page1";
}
public String navigateToPage1() {
System.out.println(customer);
return "page1";
}
public String navigateToPage2() {
System.out.println(customer);
return "page2";
}
public String navigateToPage3() {
System.out.println(customer);
return "page3";
}
public String navigateToConfirmationPage() {
System.out.println(customer);
conversation.end();
return "confirmation";
}
}
对话可以是长运行的或短暂的。短暂的对话在请求结束时结束。长运行对话跨越多个请求。在大多数情况下,我们将使用长运行对话来在 Web 应用程序中跨多个 HTTP 请求保持对作用域为对话的 bean 的引用。
长运行对话在注入的对话实例中调用begin()方法时开始,并在我们调用同一对象的end()方法时结束。
JSF 页面可以像访问 JSF 管理 bean 一样访问我们的 CDI bean:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
>
<h:head>
<title>Customer Information</title>
</h:head>
<h:body>
<h3>Enter Customer Information (Page 1 of 3)</h3>
<h:form>
<h:panelGrid columns="2">
<h:outputLabel for="firstName" value="First Name"/>
<h:inputText id="firstName"
value="#{customer.firstName}"/>
<h:outputLabel for="middleName" value="Middle Name"/>
<h:inputText id="middleName"
value="#{customer.middleName}"/>
<h:outputLabel for="lastName" value="Last Name"/>
<h:inputText id="lastName" value="#{customer.lastName}"/>
<h:panelGroup/>
<h:commandButton value="Next"
action="#{customerInfoController.navigateToPage2}"/>
</h:panelGrid>
</h:form>
</h:body>
</html>
当我们从一页导航到下一页时,我们保持我们的会话作用域 bean 的同一实例。因此,所有用户输入的数据都保持不变。当在会话 bean 上调用end()方法时,会话结束,我们的会话作用域 bean 被销毁。
将我们的 bean 保持在会话作用域中简化了实现向导式用户界面的任务,如图所示,数据可以在多个页面中输入:

在我们的示例中,在第一页上点击下一步按钮后,我们可以在 GlassFish 日志中看到我们的部分填充的 bean:
INFO: HYPERLINK "mailto:net.ensode.conversationscope.model.Customer@6e1c51b4"net.ensode.conversationscope.model.Customer@6e1c51b4[firstName=Daniel,middleName=,lastName=Jones,addrLine1=,addrLine2=,addrCity=,state=AL,zip=<null>,phoneHome=<null>,phoneWork=<null>,phoneMobile=<null>]
到目前为止,我们的简单向导的第二页显示如下:

点击下一步,我们可以看到在我们的会话作用域 bean 中填充了额外的字段。
INFO: net.ensode.conversationscope.model.Customer@6e1c51b4[firstName=Daniel,middleName=,lastName=Jones,addrLine1=123 Basketball Ct,addrLine2=,addrCity=Montgomery,state=AL,zip=36101,phoneHome=<null>,phoneWork=<null>,phoneMobile=<null>]
当我们在向导的第三页提交时,与该页面上字段对应的额外 bean 属性被填充,如图所示:

当我们到达不再需要记住客户信息的地方时,我们需要在注入到我们的代码中的会话 bean 上调用end()方法。这正是我们在显示确认页面之前在代码中所做的:
public String navigateToConfirmationPage() {
System.out.println(customer);
conversation.end();
return "confirmation";
}
在完成显示确认页面的请求后,我们的会话作用域 bean 被销毁,因为我们已经在注入的Conversation类中调用了end()方法。
摘要
在本章中,我们介绍了上下文和依赖注入。我们介绍了 JSF 页面如何将 CDI 命名 bean 作为 JSF 管理 bean 来访问。我们还介绍了 CDI 如何通过@Inject注解使将依赖项注入到我们的代码中变得简单。此外,我们还解释了如何使用限定符来确定要注入到我们的代码中的依赖项的具体实现。最后,我们介绍了 CDI bean 可以放置的所有作用域,包括所有 JSF 作用域的等效作用域,以及 JSF 中未包含的两个额外作用域,即会话作用域和依赖作用域。
在下一章中,我们将介绍如何使用新的 JSON-P API 处理 JavaScript 对象表示法(JSON)格式的数据。
第六章:JSON-P 处理
JSON,或称JavaScript Object Notation,是一种人类可读的数据交换格式。正如其名所示,JSON 源自 JavaScript。Java EE 7 引入了 JSON-P,即 Java API for JSON Processing 作为Java Specification Request(JSR)353。
传统上,XML 一直是不同系统之间数据交换的首选格式。虽然 XML 无疑非常流行,但近年来,JSON 作为一种可能更简单的数据交换格式,其地位正在上升。存在几个 Java 库可以从 Java 代码中解析和生成 JSON 数据。Java EE 通过 Java API for JSON Processing(JSON-P)标准化了这一功能。
JSON-P 包括两个用于处理 JSON 的 API——Model API 和 Streaming API;这两个 API 将在本章中介绍。
在本章中,我们将介绍以下主题:
-
JSON-P 模型 API
-
使用 Model API 生成 JSON 数据
-
使用 Model API 解析 JSON 数据
-
-
JSON-P 流式 API
-
使用流式 API 生成 JSON 数据
-
使用流式 API 解析 JSON 数据
-
JSON-P 模型 API
JSON-P 模型 API 允许我们生成一个预加载的、完全可遍历的、内存中的 JSON 对象表示。与本章中讨论的流式 API 相比,此 API 更灵活。然而,JSON-P 模型 API 较慢且需要更多内存,这在处理大量数据时可能成为问题。
使用 Model API 生成 JSON 数据
JSON-P 模型 API 的核心是JsonObjectBuilder类。此类有几个重载的add()方法,可用于向生成的 JSON 数据添加属性及其对应值。
以下代码示例说明了如何使用 Model API 生成 JSON 数据:
packagepackagenet.ensode.glassfishbook.jsonpobject;
//other imports omitted for brevity.
importimportjavax.inject.Named;
importimportjavax.json.Json;
importimportjavax.json.JsonObject;
importimportjavax.json.JsonReader;
importimportjavax.json.JsonWriter;
@Named
@SessionScoped
public class JsonpBean implements Serializable{
private String jsonStr;
@Inject
private Customer customer;
public String buildJson() {
JsonObjectBuilderjsonObjectBuilder =
Json.createObjectBuilder();
JsonObjectjsonObject = jsonObjectBuilder.
add("firstName", "Scott").
add("lastName", "Gosling").
add("email", "sgosling@example.com").
build();
StringWriterstringWriter = new StringWriter();
try (JsonWriter jsonWriter = Json.createWriter(stringWriter))
{
jsonWriter.writeObject(jsonObject);
}
setJsonStr(stringWriter.toString());
return "display_json";
}
//getters and setters omitted for brevity
}
注意
我们的例子是一个 CDI 命名 bean,对应于一个更大的 JSF 应用程序;应用程序的其他部分没有显示,因为它们与讨论无关。完整的示例应用程序包含在本书的示例代码下载中。
如前例所示,我们通过在JsonObjectBuilder上调用add()方法来生成一个JsonObject实例。在我们的例子中,我们看到如何通过在JsonObjectBuilder上调用add()方法将String值添加到我们的JsonObject中。add()方法的第一参数是生成的 JSON 对象属性名,第二个参数对应于该属性的值。add()方法的返回值是另一个JsonObjectBuilder实例;因此,可以对add()方法进行链式调用,如示例所示。
一旦添加了所有所需的属性,我们需要调用JsonObjectBuilder的build()方法,该方法返回实现JsonObject接口的类的实例。
在许多情况下,我们可能希望生成我们创建的 JSON 对象的字符串表示形式,以便它可以被其他进程或服务处理。我们可以通过创建一个实现 JsonWriter 接口的类的实例;调用 Json 类的静态 createWriter() 方法,并将 StringWriter 的实例作为其唯一参数传递。一旦我们有了 JsonWriter 实现的实例,我们需要调用其 writeObject() 方法,并将我们的 JsonObject 实例作为其唯一参数传递。
到目前为止,我们的 StringWriter 实例将包含我们 JSON 对象的字符串表示形式作为其值。因此,调用其 toString() 方法将返回一个包含我们的 JSON 对象的字符串值。
我们的特定示例将生成如下外观的 JSON 字符串:
{"firstName":"Scott","lastName":"Gosling","email":
"sgosling@example.com"}
尽管在我们的示例中,我们只向 JSON 对象添加了 String 对象,但我们并不局限于这种类型的值。JsonObjectBuilder 有几个 add() 方法的重载版本,允许我们向 JSON 对象添加多种不同类型的值。
以下表格总结了所有可用的 add() 方法版本:
| JsonObjectBuilder 方法 | 描述 |
|---|---|
add(String name, BigDecimal value) |
此方法将一个 BigDecimal 值添加到我们的 JSON 对象中。 |
add(String name, BigInteger value) |
此方法将一个 BigInteger 值添加到我们的 JSON 对象中。 |
add(String name, JsonArrayBuilder value) |
此方法将一个数组添加到我们的 JSON 对象中。JsonArrayBuilder 实现允许我们创建 JSON 数组。 |
add(String name, JsonObjectBuilder value) |
此方法将另一个 JSON 对象添加到我们的原始 JSON 对象中(JSON 对象的属性值可以是其他 JSON 对象)。添加的 JsonObject 实现是从提供的 JsonObjectBuilder 参数构建的。 |
add(String name, JsonValue value) |
此方法将另一个 JSON 对象添加到我们的原始 JSON 对象中(JSON 对象的属性值可以是其他 JSON 对象)。 |
add(String name, String value) |
此方法将一个 String 值添加到我们的 JSON 对象中。 |
add(String name, boolean value) |
此方法将一个 boolean 值添加到我们的 JSON 对象中。 |
add(String name, double value) |
此方法将一个 double 值添加到我们的 JSON 对象中。 |
add(String name, int value) |
此方法将一个 int 值添加到我们的 JSON 对象中。 |
add(String name, long value) |
此方法将一个 long 值添加到我们的 JSON 对象中。 |
在所有情况下,add() 方法的第一个参数对应于我们 JSON 对象中的属性名称,第二个参数对应于属性的值。
使用 Model API 解析 JSON 数据
在上一节中,我们看到了如何使用 Model API 从我们的 Java 代码生成 JSON 数据。在本节中,我们将了解如何读取和解析现有的 JSON 数据。以下代码示例说明了如何进行此操作:
packagepackagenet.ensode.glassfishbook.jsonpobject;
//other imports omitted
importimportjavax.json.Json;
importimportjavax.json.JsonObject;
importimportjavax.json.JsonReader;
importimportjavax.json.JsonWriter;
@Named
@SessionScoped
public class JsonpBean implements Serializable{
private String jsonStr;
@Inject
private Customer customer;
public String parseJson() {
JsonObjectjsonObject;
try (JsonReaderjsonReader = Json.createReader(
new StringReader(jsonStr))) {
jsonObject = jsonReader.readObject();
}
customer.setFirstName(
jsonObject.getString("firstName"));
customer.setLastName(
jsonObject.getString("lastName"));
customer.setEmail(jsonObject.getString("email"));
return "display_parsed_json";
}
//getters and setters omitted
}
要解析现有的 JSON 字符串,我们需要创建一个StringReader对象,将包含要解析的 JSON 数据的String对象作为参数传递。然后,我们将生成的StringReader实例传递给Json类的静态createReader()方法。此方法调用将返回一个JsonReader实例。然后,我们可以通过调用readObject()方法来获取JsonObject的实例。
在我们的示例中,我们使用了getString()方法来获取 JSON 对象中所有属性的值;此方法的第一和唯一参数是我们希望检索的属性名称。不出所料,返回值是属性的值。
除了getString()方法之外,还有其他几个类似的方法可以用来获取其他类型的数据值。以下表格总结了这些方法:
| JsonObject 方法 | 描述 |
|---|---|
get(Object key) |
此方法返回实现JsonValue接口的类的实例。 |
getBoolean(String name) |
此方法返回与给定键对应的boolean值。 |
getInt(String name) |
此方法返回与给定键对应的int值。 |
getJsonArray(String name) |
此方法返回与给定键对应的实现JsonArray接口的类的实例。 |
getJsonNumber(String name) |
此方法返回与给定键对应的实现JsonNumber接口的类的实例。 |
getJsonObject(String name) |
此方法返回与给定键对应的实现JsonObject接口的类的实例。 |
getJsonString(String name) |
此方法返回与给定键对应的实现JsonString接口的类的实例。 |
getString(String Name) |
此方法返回与给定键对应的String。 |
在所有情况下,方法的String参数对应于键名,返回值是我们希望检索的 JSON 属性值。
JSON-P Streaming API
JSON-P Streaming API 允许从流(java.io.OutputStream的子类或java.io.Writer的子类)中顺序读取 JSON 对象。它比 Model API 更快、更节省内存。然而,它的缺点是功能更有限,因为 JSON 数据需要顺序读取,我们无法像 Model API 那样直接访问特定的 JSON 属性。
使用 Streaming API 生成 JSON 数据
JSON Streaming API 有一个JsonGenerator类,我们可以使用它来生成 JSON 数据并将其写入流。这个类有几个重载的write()方法,可以用来向生成的 JSON 数据中添加属性及其对应的值。
以下代码示例说明了如何使用 Streaming API 生成 JSON 数据:
packagepackagenet.ensode.glassfishbook.jsonpstreaming;
//other imports omitted
import javax.json.Json;
import javax.json.stream.JsonGenerator;
import javax.json.stream.JsonParser;
import javax.json.stream.JsonParser.Event;
@Named
@SessionScoped
public class JsonpBean implements Serializable {
private String jsonStr;
@Inject
private Customer customer;
public String buildJson() {
StringWriterstringWriter = new StringWriter();
try (JsonGeneratorjsonGenerator =
Json.createGenerator(stringWriter)) {
jsonGenerator.writeStartObject().
write("firstName", "Larry").
write("lastName", "Gates").
write("email", "lgates@example.com").
writeEnd();
}
setJsonStr(stringWriter.toString());
return "display_json";
}
//getters and setters omitted
}
我们通过调用Json类的createGenerator()静态方法来创建一个JsonGenerator实例。JSON-P 流式 API 提供了createGenerator()方法的两个重载版本;一个接受一个扩展java.io.Writer类(例如我们示例中使用的StringWriter)的类的实例,另一个接受一个扩展java.io.OutputStream类的类的实例。
在我们开始向生成的 JSON 流添加属性之前,我们需要在JsonGenerator上调用writeStartObject()方法。此方法写入 JSON 开始对象字符(在 JSON 字符串中由一个开括号{表示)并返回另一个JsonGenerator实例,允许我们将write()调用链式添加到我们的 JSON 流中。
JsonGenerator中的write()方法允许我们向生成的 JSON 流添加属性。它的第一个参数是一个String,对应于我们添加的属性的名称,第二个参数是属性的值。
在我们的示例中,我们只向创建的 JSON 流添加String值;然而,我们并不局限于Strings。JSON-P 流式 API 提供了几个重载的write()方法,允许我们向 JSON 流添加多种不同类型的数据。以下表格总结了所有可用的write()方法版本:
| JsonGenerator write()方法 | 描述 |
|---|---|
write(String name, BigDecimal value) |
此方法将一个BigDecimal值写入我们的 JSON 流。 |
write(String name, BigInteger value) |
此方法将一个BigInteger值写入我们的 JSON 流。 |
write(String name, JsonValue value) |
此方法将一个 JSON 对象写入我们的 JSON 流(JSON 流的属性值可以是其他 JSON 对象)。 |
write(String name, String value) |
此方法将一个String值写入我们的 JSON 流。 |
write(String name, boolean value) |
此方法将一个boolean值写入我们的 JSON 流。 |
write(String name, double value) |
此方法将一个double值写入我们的 JSON 流。 |
write(String name, int value) |
此方法将一个int值写入我们的 JSON 流。 |
write(String name, long value) |
此方法将一个long值写入我们的 JSON 流。 |
在所有情况下,write()方法的第一个参数对应于我们添加到 JSON 流中的属性的名称,第二个参数对应于属性的值。
一旦我们完成向我们的 JSON 流添加属性,我们需要在JsonGenerator上调用writeEnd()方法;此方法添加 JSON 结束对象字符(在 JSON 字符串中由一个闭合花括号}表示)。
在这个阶段,我们的流或读取器已经包含了我们生成的 JSON 数据;我们如何处理它取决于我们的应用程序逻辑。在我们的例子中,我们简单地调用了 StringReader 类的 toString() 方法来获取我们创建的 JSON 数据的 String 表示形式。
使用流式 API 解析 JSON 数据
在上一节中,我们看到了如何使用流式 API 从我们的 Java 代码中生成 JSON 数据。在本节中,我们将看到如何读取和解析我们从流中接收到的现有 JSON 数据。以下代码示例说明了如何做到这一点:
package net.ensode.glassfishbook.jsonpstreaming;
//other imports omitted
import javax.json.Json;
import javax.json.stream.JsonGenerator;
import javax.json.stream.JsonParser;
import javax.json.stream.JsonParser.Event;
@Named
@SessionScoped
public class JsonpBean implements Serializable {
private String jsonStr;
@Inject
private Customer customer;
public String parseJson() {
StringReaderstringReader = new StringReader(jsonStr);
JsonParserjsonParser = Json.createParser(stringReader);
Map<String, String> keyValueMap = new HashMap<>();
String key = null;
String value = null;
while (jsonParser.hasNext()) {
JsonParser.Event event = jsonParser.next();
if (event.equals(Event.KEY_NAME)) {
key = jsonParser.getString();
} else if (event.equals(Event.VALUE_STRING)) {
value = jsonParser.getString();
}
keyValueMap.put(key, value);
}
customer.setFirstName(keyValueMap.get("firstName"));
customer.setLastName(keyValueMap.get("lastName"));
customer.setEmail(keyValueMap.get("email"));
return "display_parsed_json";
}
//getters and setters omitted
}
为了使用流式 API 读取 JSON 数据,我们首先需要通过在 Json 类上调用静态 createJsonParser() 方法来创建一个 JsonParser 实例。createJsonParser() 方法有两种重载版本;一个接受一个扩展 java.io.InputStream 的类的实例,另一个接受一个扩展 java.io.Reader 的类的实例。在我们的例子中,我们使用后者,通过传递一个 java.io.StringReader 的实例来实现,它是 java.io.Reader 的一个子类。
下一步是遍历 JSON 数据以获取要解析的数据。我们可以通过在 JsonParser 上调用 hasNext() 方法来实现这一点,如果还有更多数据要读取,则返回 true,否则返回 false。
然后,我们需要读取流中的下一份数据。JsonParser.next() 方法返回一个 JsonParser.Event 实例,该实例指示我们刚刚读取的数据类型。在我们的例子中,我们只检查键名(即,“firstName”,“lastName”和“email”)以及相应的字符串值。我们可以通过将 JsonParser.next() 返回的事件与在 JsonParser 中定义的 Event 枚举中定义的几个值进行比较来检查我们刚刚读取的数据类型。
以下表格总结了 JsonParser.next() 可以返回的所有可能的常量:
| JsonParser 事件常量 | 描述 |
|---|---|
Event.START_OBJECT |
此常量表示 JSON 对象的开始。 |
Event.END_OBJECT |
此常量表示 JSON 对象的结束。 |
Event.START_ARRAY |
此常量表示数组的开始 |
Event.END_ARRAY |
此常量表示数组的结束。 |
Event.KEY_NAME |
此常量表示读取的 JSON 属性的名称。我们可以通过在 JsonParser 上调用 getString() 来获取键名。 |
Event.VALUE_TRUE |
此常量表示读取了一个 boolean 值为 true。 |
Event.VALUE_FALSE |
此常量表示读取了一个 boolean 值为 false。 |
Event.VALUE_NULL |
此常量表示读取了一个 null 值。 |
Event.VALUE_NUMBER |
此常量表示读取了一个数值。 |
Event.VALUE_STRING |
此常量表示读取了一个字符串值。 |
如示例所示,可以通过在 JsonParser 上调用 getString() 来检索 String 值。数值可以以几种不同的格式检索;以下表格总结了 JsonParser 中可以用来检索数值的方法:|
| JsonParser 方法 | 描述 |
|---|---|
getInt() |
此方法检索数值作为 int 类型的值。 |
getLong() |
此方法检索数值作为 long 类型的值。 |
getBigDecimal() |
此方法检索数值作为 java.math.BigDecimal 类型的实例。 |
JsonParser 还提供了一个方便的 isIntegralNumber() 方法,如果数值可以安全地转换为 int 或 long 类型,则返回 true。|
我们对从流中获取的值所采取的操作取决于我们的应用程序逻辑。在我们的示例中,我们将它们放入 Map 中,然后使用该 Map 来填充一个 Java 类。|
摘要 |
在本章中,我们介绍了 Java API for JSON Processing (JSON-P)。我们介绍了 JSON-P 的两个主要 API:模型 API 和流式 API。|
我们展示了如何通过 JSON-P 的模型 API 生成 JSON 数据,特别是 JsonBuilder 类。我们还介绍了如何通过 JsonReader 类通过 JSON-P 的模型 API 解析 JSON 数据。|
此外,我们解释了如何通过使用 JsonGenerator 类来生成 JSON 数据,通过 JSON-P 的流式 API。|
最后,我们介绍了如何通过 JSON-P 的流式 API 解析 JSON 数据,特别是通过 JsonParser 类。|
在下一章中,我们将介绍 Java API for WebSocket。
第七章:WebSocket
传统上,Web 应用程序是使用 HTTP 协议之后的请求/响应模型开发的。在这个模型中,请求始终由客户端发起,然后服务器将响应返回给客户端。
服务器从未有过独立向客户端发送数据的方式(无需等待浏览器请求),直到现在。WebSocket 协议允许客户端(浏览器)和服务器之间全双工、双向通信。
Java EE 7 引入了 Java API for WebSocket,允许我们在 Java 中开发 WebSocket 端点。Java API for WebSocket 是 Java EE 标准中的全新技术。
注意
套接字是一个双向管道,其存活时间比单个请求长。应用于符合 HTML5 规范的浏览器,这将允许与 Web 服务器进行连续通信,而无需加载新页面(类似于 AJAX)。
在本章中,我们将涵盖以下主题:
-
开发 WebSocket 服务器端点
-
在 JavaScript 中开发 WebSocket 客户端
-
在 Java 中开发 WebSocket 客户端
开发 WebSocket 服务器端点
WebSocket 服务器端点是一个部署到应用服务器的 Java 类,用于处理 WebSocket 请求。
我们可以通过两种方式使用 Java API for WebSocket 实现 WebSocket 服务器端点:要么通过编程开发端点,在这种情况下,我们需要扩展 javax.websocket.Endpoint 类,要么通过使用 WebSocket 特定的注解装饰 Plain Old Java Objects(POJOs)。这两种方法非常相似;因此,我们将详细讨论注解方法,并在本节稍后简要解释第二种方法,即通过编程开发 WebSocket 服务器端点。
在本章中,我们将开发一个简单的基于 Web 的聊天应用程序,充分利用 Java API for WebSocket。
开发注解 WebSocket 服务器端点
以下 Java 类代码演示了如何通过注解 Java 类来开发 WebSocket 服务器端点:
package net.ensode.glassfishbook.websocketchat.serverendpoint;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint("/websocketchat")
public class WebSocketChatEndpoint {
private static final Logger LOG = Logger.getLogger(WebSocketChatEndpoint.class.getName());
@OnOpen
public void connectionOpened() {
LOG.log(Level.INFO, "connection opened");
}
@OnMessage
public synchronized void processMessage(Session session, String message) {
LOG.log(Level.INFO, "received message: {0}", message);
try {
for (Session sess : session.getOpenSessions()) {
if (sess.isOpen()) {
sess.getBasicRemote().sendText(message);
}
}
} catch (IOException ioe) {
LOG.log(Level.SEVERE, ioe.getMessage());
}
}
@OnClose
public void connectionClosed() {
LOG.log(Level.INFO, "connection closed");
}
}
类级别的 @ServerEndpoint 注解表示该类是一个 WebSocket 服务器端点。服务器端点的 URI(统一资源标识符)是注解后面的括号中指定的值(在这个例子中是 "/websocketchat")——WebSocket 客户端将使用此 URI 与我们的端点进行通信。
@OnOpen 注解用于装饰一个方法,每当任何客户端打开 WebSocket 连接时,都需要执行此方法。在我们的示例中,我们只是向服务器日志发送一些输出,但当然,任何有效的服务器端 Java 代码都可以放在这里。
任何被@OnMessage注解的方法都会在我们服务器端点从客户端接收到消息时被调用。由于我们正在开发一个聊天应用,我们的代码只是简单地将接收到的消息广播给所有已连接的客户端。
在我们的示例中,processMessage()方法被@OnMessage注解,并接受两个参数:一个实现了javax.websocket.Session接口的类的实例,以及一个包含接收到的消息的String参数。由于我们正在开发一个聊天应用,我们的 WebSocket 服务器端点简单地广播接收到的消息给所有已连接的客户端。
Session接口上的getOpenSessions()方法返回一个表示所有打开会话的会话对象集合。我们遍历这个集合,通过在每个会话实例上调用getBasicRemote()方法,然后调用前一个方法返回的RemoteEndpoint.Basic实现上的sendText()方法,将接收到的消息广播给所有已连接的客户端。
Session接口上的getOpenSessions()方法在调用时返回所有打开的会话。在方法调用后,可能有一个或多个会话已经关闭;因此,在尝试向客户端返回数据之前,建议在Session实现上调用isOpen()方法。如果我们尝试访问已关闭的会话,可能会抛出异常。
最后,我们需要使用@OnClose注解来装饰一个方法,以防我们需要处理客户端从服务器端点断开连接的事件。在我们的示例中,我们只是简单地将一条消息记录到服务器日志中。
在我们的示例中,还有一个我们没有使用的额外注解——@OnError注解;它用于装饰一个在发送或接收客户端数据时出现错误时需要调用的方法。
如我们所见,开发注解 WebSocket 服务器端点很简单。我们只需要添加一些注解,应用程序服务器就会根据需要调用我们的注解方法。
如果我们希望以编程方式开发 WebSocket 服务器端点,我们需要编写一个扩展javax.websocket.Endpoint的 Java 类。这个类有onOpen()、onClose()和onError()方法,这些方法在端点生命周期中的适当时间被调用。没有与@OnMessage注解等效的方法来处理来自客户端的消息。需要在会话中调用addMessageHandler()方法,传递一个实现了javax.websocket.MessageHandler接口(或其子接口)的类的实例作为其唯一参数。
注意
通常,与它们的编程对应物相比,开发注解 WebSocket 端点更容易、更直接。因此,我们建议尽可能使用注解方法。
开发 WebSocket 客户端
大多数 WebSocket 客户端都是作为 HTML5 网页实现的,利用 JavaScript WebSocket API。因此,它们必须使用 HTML5 兼容的网页浏览器(大多数现代网页浏览器都是 HTML5 兼容的)。
Java WebSocket API 提供了客户端 API,允许我们开发作为独立 Java 应用程序的 WebSocket 客户端。我们将在后面的部分介绍如何做到这一点,在 Java 中开发 WebSocket 客户端。
开发 JavaScript 客户端 WebSocket 代码
在本节中,我们将介绍如何开发客户端 JavaScript 代码来与我们在上一节中开发的 WebSocket 端点进行交互。
我们的 WebSocket 示例客户端页面是使用 HTML5 友好标记实现的 JSF 页面(如第二章第二章。JavaServer Faces 中所述)。
我们的客户端页面包括一个文本区域,我们可以看到我们应用程序的用户在说什么(毕竟,这是一个聊天应用程序),以及一个输入文本,我们可以用它向其他用户发送消息,如下面的截图所示:

我们的客户端页面的标记如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html >
<head>
<title>WebSocket Chat</title>
<meta name="viewport" content="width=device-width"/>
<script type="text/javascript">
var websocket;
function init() {
websocket = new WebSocket('ws://localhost:8080/websocketchat/websocketchat');
websocket.onopen = function(event) {
websocketOpen(event)
};
websocket.onmessage = function(event) {
websocketMessage(event)
};
websocket.onerror = function(event) {
websocketError(event)
};
}
function websocketOpen(event) {
console.log("webSocketOpen invoked");
}
function websocketMessage(event) {
console.log("websocketMessage invoked");
document.getElementById('chatwindow').value += '\r' + event.data;
}
function websocketError(event) {
console.log("websocketError invoked");
}
function sendMessage() {
var userName =
document.getElementById('userName').value;
var msg =
document.getElementById('chatinput').value;
websocket.send(userName + ": " + msg);
}
function closeConnection(){
websocket.close();
}
window.addEventListener("load", init);
</script>
</head>
<body>
<form jsf:prependId="false">
<input type="hidden" id="userName" value="#{user.userName}"/>
<table border="0">
<tbody>
<tr>
<td>
<label for="chatwindow">
Chat Window
</label>
</td>
<td>
<textArea id="chatwindow" rows="10"/>
</td>
</tr>
<tr>
<td>
<label for="chatinput">
Type Something Here
</label>
</td>
<td>
<input type="text" id="chatinput"/>
<input id="sendBtn" type="button" value="Send" onclick="sendMessage()"/>
</td>
</tr>
<tr>
<td></td>
<td>
<input type="button" id="exitBtn" value="Exit" onclick="closeConnection()"/>
</td>
</tr>
</tbody>
</table>
</form>
</body>
</html>
我们 JavaScript 代码的最后一行(window.addEventListener("load", init);)将我们的 JavaScript init()函数设置为在页面加载时执行。
在init()方法中,我们初始化一个新的 JavaScript websocket对象,将我们的服务器端点 URI 作为参数传递。这告诉我们的 JavaScript 代码服务器端点的位置。
JavaScript 的websocket对象具有多种函数类型,用于处理不同的事件,例如打开连接、接收消息和处理错误。我们需要将这些类型设置为我们自己的 JavaScript 函数,以便我们可以处理这些事件,这正是我们在调用 JavaScript websocket对象构造函数后立即在init()方法中做的。在我们的例子中,我们分配给websocket对象的函数只是将它们的功能委托给独立的 JavaScript 函数。
每次 WebSocket 连接打开时,都会调用我们的websocketOpen()函数。在我们的例子中,我们只是向浏览器 JavaScript 控制台发送一条消息。
每次浏览器从我们的 WebSocket 端点接收到 WebSocket 消息时,都会调用webSocketMessage()函数。在我们的例子中,我们更新了id为chatWindow的文本区域内容和消息内容。
每次发生与 WebSocket 相关的错误时,都会调用websocketError()函数。在我们的例子中,我们只是向浏览器 JavaScript 控制台发送一条消息。
JavaScript 的sendMessage()函数将消息发送到 WebSocket 服务器端点,包含用户名和文本输入的id为chatinput的内容。当用户点击id为sendBtn的按钮时,会调用此函数。
closeConnection() JavaScript 函数关闭与我们的 WebSocket 服务器端点的连接。当用户点击具有 id 为 exitBtn 的按钮时,会调用此函数。
从这个例子中我们可以看出,编写客户端 JavaScript 代码与 WebSocket 端点交互相当简单。
使用 Java 开发 WebSocket 客户端
虽然目前开发基于 Web 的 WebSocket 客户端是最常见的方式,但 Java API for WebSocket 提供了一个客户端 API,我们可以使用它来在 Java 中开发 WebSocket 客户端。
在本节中,我们将使用 Java API for WebSocket 的客户端 API 来开发一个简单的 WebSocket 客户端。最终产品如图所示:

然而,我们不会在本节中介绍 GUI 代码(使用 Swing 框架开发),因为它与本次讨论无关。示例的完整代码(包括 GUI 代码)可以从 Packt Publishing 网站下载,网址为 www.packtpub.com。
就像 WebSocket 服务器端点一样,Java WebSocket 客户端可以以编程方式或使用注解的方式开发。再次强调,我们只会介绍注解方法:编程客户端的开发方式与编程服务器端点非常相似,即编程客户端必须扩展 javax.websocket.Endpoint 并重写适当的方法。
不再赘述,以下是我们 Java WebSocket 客户端的代码:
package net.ensode.websocketjavaclient;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.websocket.ClientEndpoint;
import javax.websocket.CloseReason;
import javax.websocket.ContainerProvider;
import javax.websocket.DeploymentException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
@ClientEndpoint
public class WebSocketClient {
private String userName;
private Session session;
private final WebSocketJavaClientFrame webSocketJavaClientFrame;
public WebSocketClient(WebSocketJavaClientFrame webSocketJavaClientFrame) {
this.webSocketJavaClientFrame = webSocketJavaClientFrame;
try {
WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer();
webSocketContainer.connectToServer(this, new URI("ws://localhost:8080/websocketchat/websocketchat"));
}
catch (DeploymentException | IOException | URISyntaxException ex) {
ex.printStackTrace();
}
}
@OnOpen
public void onOpen(Session session) {
System.out.println("onOpen() invoked");
this.session = session;
}
@OnClose
public void onClose(CloseReason closeReason) {
System.out.println("Connection closed, reason: "+ closeReason.getReasonPhrase());
}
@OnError
public void onError(Throwable throwable) {
System.out.println("onError() invoked");
throwable.printStackTrace();
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("onMessage() invoked");
webSocketJavaClientFrame.getChatWindowTextArea().setText(webSocketJavaClientFrame.getChatWindowTextArea().getText() + "\n" + message);
}
public void sendMessage(String message) {
try {
System.out.println("sendMessage() invoked, message = " + message);
session.getBasicRemote().sendText(userName + ": " + message);
}
catch (IOException ex) {
ex.printStackTrace();
}
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
类级别的 @ClientEndPoint 注解表示我们的类是一个 WebSocket 客户端——所有 Java WebSocket 客户端都必须使用此注解。
建立与 WebSocket 服务器端点连接的代码位于我们的类构造函数中。首先,我们需要调用 ContainerProvider.getWebSocketContainer() 来获取 javax.websocket.WebSocketContainer 的一个实例。然后,通过在 WebSocketContainer 实例上调用 connectToServer() 方法来建立连接;然后我们将一个带有 @ClientEndpoint 注解的类作为第一个参数传递(在我们的例子中,由于连接代码位于我们的 WebSocket Java 客户端代码中,我们使用 this);然后我们传递一个包含 WebSocket 服务器端点 URI 的 URI 对象作为第二个参数。
连接建立后,我们就可以准备响应 WebSocket 事件了。细心的读者可能已经注意到,我们用来开发服务器端点的确切相同的注解再次在我们的客户端代码中使用。
带有 @OnOpen 注解的任何方法都会在连接到 WebSocket 服务器端点时自动调用。该方法必须返回 void,并且可以有一个可选的 javax.websocket.Session 类型的参数。在我们的例子中,我们向控制台发送一些输出,并用接收到的 Session 实例初始化一个类变量。
带有@OnClose注解的方法会在 WebSocket 会话关闭时被调用。被注解的方法可以包含可选的javax.websocket.Session和CloseReason类型的参数。在我们的示例中,我们选择只使用CloseReason可选参数,因为它的类有一个方便的getReasonPhrase()方法,可以提供会话关闭的简要解释。
@OnError注解用于装饰在发生错误时被调用的任何方法。带有@OnError注解的方法必须有一个类型为java.lang.Throwable(java.lang.Exception的父类)的参数,并且可以有一个可选的类型为Session的参数。在我们的示例中,我们只是将Throwable参数的堆栈跟踪发送到stderr。
带有@OnMessage注解的方法会在接收到传入的 WebSocket 消息时被调用。@OnMessage方法可以根据接收到的消息类型以及我们希望如何处理它来具有不同的参数。在我们的示例中,我们使用了最常见的情况:接收文本消息。在这种情况下,我们需要一个String参数来保存消息的内容,以及一个可选的Session参数。
注意
有关如何处理其他类型消息的信息,请参阅@OnMessage的 JavaDoc 文档,网址为docs.oracle.com/javaee/7/api/javax/websocket/OnMessage.html。
在我们的示例中,我们简单地更新了聊天窗口文本区域,将接收到的消息追加到其内容中。
要发送 WebSocket 消息,我们在Session实例上调用getBasicRemote()方法,然后在这个调用返回的RemoteEndpoint.Basic实现上调用sendText()方法(如果这看起来很熟悉,那是因为我们在 WebSocket 服务器端点代码中做了完全相同的事情)。在我们的示例中,我们在sendMessage()方法中这样做。
关于 Java API for WebSocket 的更多信息
在本章中,我们介绍了 Java API for WebSocket 提供的功能的大部分。有关更多信息,请参阅 Tyrus 用户指南,它是 Java API for WebSocket 的参考实现,网址为tyrus.java.net/documentation/1.3.1/user-guide.html。
摘要
在本章中,我们介绍了 Java API for WebSocket,这是一个新的 Java EE API,用于开发 WebSocket 服务器端点和客户端。
我们首先看到了如何通过利用 Java API for WebSockets 来开发 WebSocket 服务器端点。我们专注于开发基于注解的 WebSocket 端点。
然后,我们介绍了如何使用 JavaScript 和 JavaScript 内置的 WebSocket API 开发基于 Web 的 WebSocket 客户端。
最后,我们解释了如何通过@ClientEndpoint注解在 Java 中开发 WebSocket 客户端应用程序。
在下一章中,我们将介绍Java 消息服务(JMS)。
第八章. Java 消息服务
Java 消息服务(JMS)API为 Java EE 应用程序之间发送消息提供了一个机制。Java EE 7 引入了 JMS 2.0,这是 JMS 的一个新版本,它极大地简化了涉及消息功能的应用程序的开发。
JMS 应用程序不直接通信;相反,消息生产者将消息发送到目的地,而消息消费者从目的地接收该消息。
当使用点对点(PTP)消息域时,消息目的地是一个消息队列,当使用发布/订阅(pub/sub)消息域时,消息目的地是一个消息主题。
在本章中,我们将涵盖以下主题:
-
设置 GlassFish 以支持 JMS
-
使用消息队列
-
使用消息主题
设置 GlassFish 以支持 JMS
在我们可以开始编写代码以利用 JMS API 之前,我们需要配置一些 GlassFish 资源。具体来说,我们需要设置一个 JMS 连接工厂、一个消息队列和一个消息主题。
注意
Java EE 7 要求所有符合标准的应用服务器提供默认的 JMS 连接工厂。GlassFish 是一个完全符合 Java EE 7 的应用服务器(也是 Java EE 7 的参考实现),符合这一要求;因此,严格来说,我们实际上并不需要设置连接工厂,然而,在许多情况下,我们可能需要设置一个。因此,在下一节中,我们将说明如何进行设置。
设置 JMS 连接工厂
设置 JMS 连接工厂最简单的方法是通过 GlassFish 的网页控制台。回想一下第一章,“开始使用 GlassFish”,网页控制台可以通过在命令行中输入以下命令来访问我们的域:
asadmin start-domain domain1
然后,转到http://localhost:4848并登录。

可以通过展开位于网页控制台左侧的资源节点来添加连接工厂;为此,展开JMS 资源节点,点击连接工厂节点,然后在网页控制台的主要区域中点击新建...按钮。

对于我们的目的,我们可以保持大多数默认设置;我们唯一需要做的是在JNDI 名称字段中输入一个池名称,并为我们的连接工厂选择一个资源类型。
注意
在命名 JMS 资源时,始终使用以"jms/"开头的池名称是一个好主意。这样,在浏览 JNDI 树时可以轻松识别 JMS 资源。
在标记为JNDI 名称的字段中输入jms/GlassFishBookConnectionFactory。本章后面的代码示例将使用此 JNDI 名称来获取对连接工厂的引用。
资源类型下拉菜单有以下三个选项:
-
javax.jms.TopicConnectionFactory:此选项用于创建一个连接工厂,该工厂为使用发布/订阅消息域的 JMS 客户端创建 JMS 主题
-
javax.jms.QueueConnectionFactory:此选项用于创建一个连接工厂,该工厂为使用 PTP 消息域的 JMS 客户端创建 JMS 队列
-
javax.jms.ConnectionFactory:此选项用于创建一个连接工厂,该工厂可以创建 JMS 主题或 JMS 队列
在我们的示例中,我们将选择javax.jms.ConnectionFactory;这样,我们可以为所有示例使用相同的连接工厂,包括使用 PTP 消息域和发布/订阅消息域的示例。
在JNDI 名称字段中输入我们的连接工厂的池名称,选择连接工厂类型,并可选地输入我们的连接工厂的描述后,我们需要单击确定按钮以使更改生效。
我们应该会看到新创建的连接工厂(jms/GlassFishBookConnectionFactory)在 GlassFish Web 控制台的主区域中列出,如下面的截图所示:

设置 JMS 队列
可以通过以下步骤添加 JMS 队列:
-
在 Web 控制台左侧的树中展开资源节点。
-
在JNDI 名称字段中输入一个值。
-
在物理目标名称字段中输入一个值。
-
在资源类型字段中选择javax.jms.Queue。
-
单击确定按钮。
![设置 JMS 队列]()
在我们的示例中,消息队列的 JNDI 名称为jms/GlassFishBookQueue。消息队列的资源类型必须是javax.jms.Queue。此外,必须在物理目标名称字段中输入一个值。在我们的示例中,我们将GlassFishBookQueue用作此字段的值。
单击确定按钮后,我们应该会看到新创建的队列,如下面的截图所示:

设置 JMS 主题
在 GlassFish 中设置 JMS 主题与设置消息队列非常相似。执行以下步骤:
-
展开 JMS 资源节点。
-
单击目的地节点。
-
在 Web 控制台的主区域中单击新建...按钮。
-
在JNDI 名称字段中为我们的主题输入一个名称。
-
在物理目标名称字段中为我们的主题输入一个物理目标名称。
-
从资源类型下拉菜单中选择javax.jms.Topic。
-
单击以下截图所示的确定按钮:

我们的示例将在JNDI 名称字段中使用jms/GlassFishBookTopic。由于这是一个消息主题,资源类型必须设置为javax.jms.Topic。描述字段是可选的。物理目标名称属性是必需的;在我们的示例中,我们将使用GlassFishBookTopic作为此属性的值。
点击确定按钮后,我们可以看到我们新创建的消息主题如下:

现在我们已经设置了一个连接工厂、一个消息队列和一个消息主题,我们就可以开始使用 JMS API 编写代码了。
与消息队列一起工作
正如我们之前提到的,当我们的 JMS 代码使用点对点(PTP)消息域时,会使用消息队列。对于 PTP 消息域,通常有一个消息生产者和一个消息消费者。消息生产者和消息消费者不需要同时运行以进行通信。消息生产者放入消息队列中的消息将保留在那里,直到消息消费者执行并从队列中请求消息。
向消息队列发送消息
向 JMS 队列发送消息包括将一些资源注入到我们的代码中,并执行一些简单的 JMS API 调用。
以下示例说明了如何向消息队列添加消息:
package net.ensode.glassfishbook;
import javax.annotation.Resource;
import javax.jms.ConnectionFactory;
import javax.jms.JMSContext;
import javax.jms.JMSProducer;
import javax.jms.Queue;
public class MessageSender {
@Resource(mappedName = "jms/GlassFishBookConnectionFactory")
private static ConnectionFactory connectionFactory;
@Resource(mappedName = "jms/GlassFishBookQueue")
private static Queue queue;
public void produceMessages() {
JMSContext jmsContext = connectionFactory.createContext();
JMSProducer jmsProducer = jmsContext.createProducer();
String msg1 = "Testing, 1, 2, 3\. Can you hear me?";
String msg2 = "Do you copy?";
String msg3 = "Good bye!";
System.out.println("Sending the following message: "
+ msg1);
jmsProducer.send(queue, msg1);
System.out.println("Sending the following message: "
+ msg2);
jmsProducer.send(queue, msg2);
System.out.println("Sending the following message: "
+ msg3);
jmsProducer.send(queue, msg3);
}
public static void main(String[] args) {
new MessageSender().produceMessages();
}
}
在深入研究代码的细节之前,细心的读者可能已经注意到MessageSender类是一个独立的 Java 应用程序,因为它包含一个主方法。由于这个类是独立的,它是在应用程序服务器之外执行的;然而,我们可以看到一些资源被注入到其中,特别是连接工厂对象和队列。我们可以将资源注入到这个代码中,即使它是在应用程序服务器之外运行的,这是因为 GlassFish 包含一个名为appclient的实用程序。
此实用程序允许我们“包装”一个可执行的 JAR 文件,并允许它访问应用程序服务器资源。假设我们的代码打包在一个名为jmsptpproducer.jar的可执行 JAR 文件中,我们将在命令行中输入以下内容:
appclient -client jmsptpproducer.jar
在一些 GlassFish 日志条目之后,我们将在控制台上看到以下输出:
Sending the following message: Testing, 1, 2, 3\. Can you hear me?
Sending the following message: Do you copy?
Sending the following message: Good bye!
appclient可执行脚本可以在[GlassFish 安装目录]/glassfish/bin下找到;我们的示例假设此目录在PATH变量中,如果不是,必须在命令行中输入appclient脚本的完整路径。
在解决这个问题之后,我们现在可以解释代码。
produceMessages()方法执行所有必要的步骤以将消息发送到消息队列。
此方法首先执行的操作是通过在注入的javax.jms.ConnectionFactory实例上调用createContext()方法来创建一个javax.jms.JMSContext实例。请注意,装饰连接工厂对象的@Resource注解的mappedName属性与我们在 GlassFish Web 控制台中设置的连接工厂的 JNDI 名称相匹配。在幕后,使用此名称进行 JNDI 查找以获取连接工厂对象。
接下来,我们通过在刚刚创建的JMSContext实例上调用createProducer()方法来创建一个javax.jms.JMSProducer实例。
获取JMSProducer实例后,代码通过调用其send()方法发送一系列文本消息;该方法将消息目的地作为其第一个参数,将包含消息文本的字符串作为其第二个参数。
在JMSProducer中,send()方法有几个重载版本;我们示例中使用的是一种便利方法,该方法创建一个javax.jms.TextMessage实例,并将其文本设置为方法调用中提供的第二个参数的字符串。
虽然我们的示例只向队列发送文本消息,但我们并不局限于只发送此类消息。JMS API 提供了多种类型的消息,这些消息可以被 JMS 应用程序发送和接收。所有消息类型都在javax.jms包中定义为接口。
以下表格列出了所有可用的消息类型:
| 消息类型 | 描述 |
|---|---|
BytesMessage |
允许发送字节数组作为消息。JMSProducer有一个便利的send()方法,它将字节数组作为其参数之一;在发送消息时,该方法会动态创建一个javax.jms.BytesMessage实例。 |
MapMessage |
允许发送java.util.Map的实现作为消息。JMSProducer有一个便利的send()方法,它将Map作为其参数之一;在发送消息时,该方法会动态创建一个javax.jms.MapMessage实例。 |
ObjectMessage |
允许发送实现java.io.Serializable接口的任何 Java 对象作为消息。JMSProducer有一个便利的send()方法,它将实现java.io.Serializable接口的类的实例作为其第二个参数;在发送消息时,该方法会动态创建一个javax.jms.ObjectMessage实例。 |
StreamMessage |
允许发送字节数组作为消息。与BytesMessage不同,它存储添加到流中的每个原始类型的类型。 |
TextMessage |
允许发送java.lang.String作为消息。正如我们的示例所示,JMSProducer有一个便利的send()方法,它将String类型作为其第二个参数;在发送消息时,该方法会动态创建一个javax.jms.TextMessage实例。 |
关于所有 JMS 消息类型的更多信息,请参阅docs.oracle.com/javaee/7/api/的 JavaDoc 文档。
从消息队列中检索消息
当然,如果没有任何接收者,从队列中发送消息是没有意义的。以下示例说明了如何从 JMS 消息队列中检索消息:
package net.ensode.glassfishbook;
import javax.annotation.Resource;
import javax.jms.ConnectionFactory;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.Queue;
public class MessageReceiver {
@Resource(mappedName = "jms/GlassFishBookConnectionFactory")
private static ConnectionFactory connectionFactory;
@Resource(mappedName = "jms/GlassFishBookQueue")
private static Queue queue;
public void getMessages() {
String message;
boolean goodByeReceived = false;
JMSContext jmsContext = connectionFactory.createContext();
JMSConsumer jMSConsumer = jmsContext.createConsumer(queue);
System.out.println("Waiting for messages...");
while (!goodByeReceived) {
message = jMSConsumer.receiveBody(String.class);
if (message != null) {
System.out.print("Received the following message: ");
System.out.println(message);
System.out.println();
if (message.equals("Good bye!")) {
goodByeReceived = true;
}
}
}
}
public static void main(String[] args) {
new MessageReceiver().getMessages();
}
}
就像之前的示例一样,使用@Resource注解注入了javax.jms.ConnectionFactory和javax.jms.Queue的实例。
在我们的代码中,我们通过调用ConnectionFactory的createContext()方法来获取javax.jms.JMSContext实例,就像在之前的示例中一样。
在此示例中,我们通过在 JMSContext 实例上调用 createConsumer() 方法来获取 javax.jms.JMSConsumer 的实例。
通过在 JMSConsumer 的实例上调用 receiveBody() 方法来接收消息。此方法接受我们期望的消息类型作为其唯一参数(在我们的示例中是 String.class)。此方法返回其参数指定的类型的对象(在我们的示例中是一个 java.lang.String 实例)。
在这个特定的示例中,我们将这个方法调用放在了一个 while 循环中,因为我们期望一个消息会告诉我们没有更多的消息到来。具体来说,我们正在寻找包含文本 "Good bye!" 的消息。一旦我们收到这个消息,我们就退出循环并继续进行进一步的处理。在这个特定的案例中,没有更多的处理要做,因此,在退出循环后执行结束。
就像在先前的示例中一样,使用 appclient 工具允许我们将资源注入到代码中,并防止我们不得不将任何库添加到 CLASSPATH 中。通过 appclient 工具执行代码后,我们应该在命令行中看到以下输出:
appclient -client target/jmsptpconsumer.jar
Waiting for messages...
Received the following message: Testing, 1, 2, 3\. Can you hear me?
Received the following message: Do you copy?
Received the following message: Good bye!
先前的示例在队列上放置了一些消息。此示例检索这些消息。如果先前的示例尚未执行,则没有要检索的消息。
异步从消息队列接收消息
JMSConsumer.receiveBody() 方法有一个缺点:它会在接收到队列中的消息之前阻塞执行。在先前的示例中,我们通过在接收到特定消息("Good bye!")后退出循环来绕过这个限制。
我们可以通过实现 javax.jms.MessageListener 接口来异步接收消息,从而防止我们的 JMS 消费者代码阻塞执行。
javax.jms.MessageListener 接口包含一个名为 onMessage 的单方法,它接受一个实现 javax.jms.Message 接口的类的实例作为其唯一参数。以下示例说明了此接口的典型实现:
package net.ensode.glassfishbook;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
public class ExampleMessageListener implements MessageListener {
@Override
public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.print("Received the following message: ");
System.out.println(textMessage.getText());
System.out.println();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
在这种情况下,onMessage() 方法只是将消息文本输出到控制台。回想一下,在幕后,当我们使用 String 作为第二个参数调用 JMSProducer.send() 时,JMS API 会创建 javax.jms.TextMessage 的实例;我们的 MessageListener 实现将接收到的 Message 实例作为参数转换为 TextMessage,然后获取由 JMSProducer 变量发送的 String message,调用其 getText() 方法。
我们的主代码现在可以将消息检索委托给我们的自定义 MessageListener 实现:
package net.ensode.glassfishbook;
import javax.annotation.Resource;
import javax.jms.ConnectionFactory;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.Queue;
public class AsynchMessReceiver {
@Resource(mappedName = "jms/GlassFishBookConnectionFactory")
private static ConnectionFactory connectionFactory;
@Resource(mappedName = "jms/GlassFishBookQueue")
private static Queue queue;
public void getMessages() {
try {
JMSContext jmsContext = connectionFactory.createContext();
JMSConsumer jMSConsumer = jmsContext.createConsumer(queue);
jMSConsumer.setMessageListener(
new ExampleMessageListener());
System.out.println("The above line will allow the "
+ "MessageListener implementation to "
+ "receiving and processing messages"
+ " from the queue.");
Thread.sleep(1000);
System.out.println("Our code does not have to block "
+ "while messages are received.");
Thread.sleep(1000);
System.out.println("It can do other stuff "
+ "(hopefully something more useful than sending "
+ "silly output to the console. :)");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new AsynchMessReceiver().getMessages();
}
}
与上一节中的示例相比,唯一的区别在于本例中,我们在从 JMS 上下文获取的javax.jms.JMSConsumer实例上调用setMessageListener()方法。我们将我们的自定义javax.jms.MessageListener实现实例传递给此方法;每当队列中有等待的消息时,其onMessage()方法会自动被调用。使用这种方法,主代码在等待接收消息时不会阻塞执行。
执行前面的示例(当然,使用 GlassFish 的appclient实用程序),将产生以下输出:
appclient -client target/jmsptpasynchconsumer.jar
The above line will allow the MessageListener implementation to receive and process messages from the queue.
Received the following message: Testing, 1, 2, 3\. Can you hear me?
Received the following message: Do you copy?
Received the following message: Good bye!
Our code does not have to block while messages are received.
It can do other stuff (hopefully something more useful than sending silly output to the console. :)
注意,在主线程执行过程中,消息是如何被接收和处理的。我们可以通过观察MessageListener类onMessage()方法的输出位于主类中System.out.println()调用之间来判断这一点。
浏览消息队列
JMS 提供了一种在不实际从队列中删除消息的情况下浏览消息队列的方法。以下示例说明了如何进行此操作:
package net.ensode.glassfishbook;
import java.util.Enumeration;
import javax.annotation.Resource;
import javax.jms.ConnectionFactory;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.QueueBrowser;
import javax.jms.TextMessage;
public class MessageQueueBrowser {
@Resource(mappedName = "jms/GlassFishBookConnectionFactory")
private static ConnectionFactory connectionFactory;
@Resource(mappedName = "jms/GlassFishBookQueue")
private static Queue queue;
public void browseMessages() {
try {
Enumeration messageEnumeration;
TextMessage textMessage;
JMSContext jmsContext = connectionFactory.createContext();
QueueBrowser browser = jmsContext.createBrowser(queue);
messageEnumeration = browser.getEnumeration();
if (messageEnumeration != null) {
if (!messageEnumeration.hasMoreElements()) {
System.out.println("There are no messages "
+ "in the queue.");
} else {
System.out.println(
"The following messages are "
+ "in the queue");
while (messageEnumeration.hasMoreElements()) {
textMessage = (TextMessage) messageEnumeration.nextElement();
System.out.println(textMessage.getText());
}
}
}
} catch (JMSException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new MessageQueueBrowser().browseMessages();
}
}
如我们所见,浏览消息队列的流程非常直接。我们以通常的方式获取 JMS 连接工厂、JMS 队列和 JMS 上下文,然后在 JMS 上下文对象上调用createBrowser()方法。此方法返回一个实现javax.jms.QueueBrowser接口的实例,其中包含一个getEnumeration()方法,我们可以调用它来获取包含队列中所有消息的Enumeration。要检查队列中的消息,我们只需遍历这个枚举并逐个获取消息。在我们的示例中,我们简单地调用了队列中每个消息的getText()方法。
与消息主题一起工作
当我们的 JMS 代码使用发布/订阅(pub/sub)消息域时,会使用消息主题。当使用此消息域时,相同的消息可以发送到主题的所有订阅者。
向消息主题发送消息
向 JMS 主题发送消息与向队列发送消息非常相似;只需注入所需的资源并执行一些简单的 JMS API 调用即可。
以下示例说明了如何向消息主题发送消息:
package net.ensode.glassfishbook;
import javax.annotation.Resource;
import javax.jms.ConnectionFactory;
import javax.jms.JMSContext;
import javax.jms.JMSProducer;
import javax.jms.Topic;
public class MessageSender {
@Resource(mappedName = "jms/GlassFishBookConnectionFactory")
private static ConnectionFactory connectionFactory;
@Resource(mappedName = "jms/GlassFishBookTopic")
private static Topic topic;
public void produceMessages() {
JMSContext jmsContext = connectionFactory.createContext();
JMSProducer jmsProducer = jmsContext.createProducer();
String msg1 = "Testing, 1, 2, 3\. Can you hear me?";
String msg2 = "Do you copy?";
String msg3 = "Good bye!";
System.out.println("Sending the following message: "
+ msg1);
jmsProducer.send(topic, msg1);
System.out.println("Sending the following message: "
+ msg2);
jmsProducer.send(topic, msg2);
System.out.println("Sending the following message: "
+ msg3);
jmsProducer.send(topic, msg3);
}
public static void main(String[] args) {
new MessageSender().produceMessages();
}
}
如我们所见,这个示例几乎与我们在讨论点对点消息时看到的MessageSender类相同。事实上,唯一不同的代码行是那些被突出显示的。JMS API 就是这样设计的,以便应用程序开发者不需要学习两个不同的 API 来处理 PTP 和 pub/sub 域。
由于代码几乎与使用消息队列部分中的相应示例相同,我们只解释两个示例之间的差异。在这个例子中,我们不是声明一个实现javax.jms.Queue的类的实例,而是声明一个实现javax.jms.Topic的类的实例。然后我们将这个javax.jms.Topic的实例作为JMSProducer对象的send()方法的第一参数传递,以及我们希望发送的消息。
从消息主题接收消息
就像向消息主题发送消息几乎与向消息队列发送消息相同一样,从消息主题接收消息几乎与从消息队列接收消息相同,如下例所示:
package net.ensode.glassfishbook;
import javax.annotation.Resource;
import javax.jms.ConnectionFactory;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.Topic;
public class MessageReceiver {
@Resource(mappedName = "jms/GlassFishBookConnectionFactory")
private static ConnectionFactory connectionFactory;
@Resource(mappedName = "jms/GlassFishBookTopic")
private static Topic topic;
public void getMessages() {
String message;
boolean goodByeReceived = false;
JMSContext jmsContext = connectionFactory.createContext();
JMSConsumer jMSConsumer = jmsContext.createConsumer(topic);
System.out.println("Waiting for messages...");
while (!goodByeReceived) {
message = jMSConsumer.receiveBody(String.class);
if (message != null) {
System.out.print("Received the following message: ");
System.out.println(message);
System.out.println();
if (message.equals("Good bye!")) {
goodByeReceived = true;
}
}
}
}
public static void main(String[] args) {
new MessageReceiver().getMessages();
}
}
再次强调,此代码与 PTP 消息的相应代码之间的差异很简单。我们不是声明一个实现javax.jms.Queue的类的实例,而是声明一个实现javax.jms.Topic的类。我们使用@Resource注解,通过在 GlassFish web 控制台创建它时使用的 JNDI 名称,将此类的实例注入到我们的代码中。然后我们像以前一样获取JMSContext和JMSConsumer的实例,然后通过在JMSConsumer上调用receiveBody()方法从主题接收消息。
如本节所示,使用发布/订阅消息域的优点是消息可以发送到多个消息消费者。这可以通过同时执行本节开发的MessageReceiver类的两个实例来轻松测试,然后执行上一节开发的MessageSender类。我们应该看到每个实例的控制台输出,表明两个实例都接收到了所有消息。
就像从消息队列中检索消息一样,可以从消息主题异步检索消息。执行此操作的步骤与消息队列版本非常相似,因此我们将不展示示例。要将本章前面展示的异步示例转换为使用消息主题,只需将javax.jms.Queue变量替换为javax.jms.Topic的一个实例,并使用"jms/GlassFishBookTopic"作为装饰javax.jms.Topic实例的@Resource注解的mappedName属性的值来注入适当的实例。
创建持久订阅者
使用发布/订阅消息域的缺点是,当消息被发送到主题时,消息消费者必须正在运行。如果消息消费者当时没有运行,它将不会收到消息;而在 PTP 中,消息将保存在队列中,直到消息消费者运行。幸运的是,JMS API 提供了一种使用发布/订阅消息域并保持消息在主题中直到所有已订阅的消息消费者运行并接收消息的方法。这可以通过为 JMS 主题创建持久订阅者来实现。
为了能够为持久订阅者提供服务,我们需要设置我们的 JMS 连接工厂的ClientId属性。每个持久订阅者都必须有一个唯一的客户端 ID;因此,必须为每个潜在的持久订阅者声明一个唯一的连接工厂。
小贴士
InvalidClientIdException?
只有一个 JMS 客户端可以连接到特定客户端 ID 的主题。如果有多个 JMS 客户端尝试使用相同的连接工厂获取 JMS 连接,将抛出一个JMSException,指出客户端 ID 已被使用。解决方案是为每个将接收持久主题消息的潜在客户端创建一个连接工厂。
正如我们之前提到的,通过 GlassFish 网页控制台添加连接工厂的最简单方法是按照以下步骤进行:
-
展开位于网页控制台左侧的资源节点。
-
展开JMS 资源节点。
-
点击连接工厂节点。
-
点击页面主区域中的新建...按钮。
我们接下来的示例将使用以下截图显示的设置:

在点击确定按钮之前,我们需要滚动到页面底部,点击添加属性按钮,并输入一个名为ClientId的新属性。我们的示例将使用ExampleId作为此属性的值,如下截图所示:

现在我们已经设置了 GlassFish 以能够提供持久订阅,我们准备好编写一些代码来利用它们:
package net.ensode.glassfishbook;
import javax.annotation.Resource;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSConsumer;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
public class MessageReceiver {
@Resource(mappedName
= "jms/GlassFishBookDurableConnectionFactory")
private static ConnectionFactory connectionFactory;
@Resource(mappedName = "jms/GlassFishBookTopic")
private static Topic topic;
public void getMessages() {
String message;
boolean goodByeReceived = false;
JMSContext jmsContext = connectionFactory.createContext();
JMSConsumer jMSConsumer =
jmsContext.createDurableConsumer(topic, "Subscriber1");
System.out.println("Waiting for messages...");
while (!goodByeReceived) {
message = jMSConsumer.receiveBody(String.class);
if (message != null) {
System.out.print("Received the following message: ");
System.out.println(message);
System.out.println();
if (message.equals("Good bye!")) {
goodByeReceived = true;
}
}
}
}
public static void main(String[] args) {
new MessageReceiver().getMessages();
}
}
如我们所见,此代码与之前用于检索消息的示例代码没有太大区别,其目的是检索消息。与之前的示例相比,只有两个不同之处:我们注入的ConnectionFactory实例是我们在本节中较早设置的,用于处理持久订阅,并且我们不是在 JMS 上下文对象上调用createConsumer()方法,而是调用createDurableConsumer()。createDurableConsumer()方法接受两个参数:一个用于检索消息的 JMS 主题对象和一个指定此订阅名称的String。此第二个参数必须在该持久主题的所有订阅者中是唯一的。
摘要
在本章中,我们介绍了如何使用 GlassFish 网页控制台在 GlassFish 中设置 JMS 连接工厂、JMS 消息队列和 JMS 消息主题。
我们还介绍了如何通过javax.jms.JMSProducer接口向消息队列发送消息。
此外,我们还介绍了如何通过javax.jms.JMSConsumer接口从消息队列接收消息。我们还介绍了如何通过实现javax.jms.MessageListener接口异步地从消息队列接收消息。
我们还看到了如何使用这些接口在 JMS 消息主题之间发送和接收消息。
我们还介绍了如何通过javax.jms.QueueBrowser接口在消息队列中浏览消息,而无需从队列中移除它们。
最后,我们学习了如何设置和与 JMS 主题的持久订阅进行交互。
在下一章中,我们将介绍如何确保 Java EE 应用程序的安全性。
第九章:保护 Java EE 应用程序
在本章中,我们将介绍如何通过利用 GlassFish 的内置安全功能来保护 Java EE 应用程序。
Java EE 安全性依赖于 Java 身份验证和授权服务(JAAS)API。正如我们将看到的,保护 Java EE 应用程序大部分情况下不需要编写太多代码。通过在应用程序服务器中设置用户和安全组到安全实体,然后配置我们的应用程序依赖于特定的安全实体以进行身份验证和授权,从而实现应用程序的保护。
本章我们将涵盖的一些主题包括:
-
管理实体
-
文件实体
-
证书实体
-
创建自签名安全证书
-
JDBC 实体
-
自定义实体
安全实体
安全实体本质上是由用户和相关安全组组成的集合。用户可以属于一个或多个安全组。用户所属的组定义了系统将允许用户执行哪些操作。例如,一个应用程序可以有普通用户,他们只能使用基本应用程序功能,还可以有管理员,除了可以使用基本应用程序功能外,还可以向系统中添加其他用户。
安全实体存储用户信息(用户名、密码和安全组)。因此,应用程序不需要实现此功能,只需配置即可从安全实体获取此信息。一个安全实体可以被多个应用程序使用。
预定义安全实体
GlassFish 默认配置了三个预定义的安全实体:admin-realm、file 和 certificate。admin-realm 用于管理用户对 GlassFish 网络控制台的访问,不应用于其他应用程序。file 实体将用户信息存储在文件中。certificate 实体查找客户端证书以验证用户。
以下截图显示了 GlassFish 网络控制台中的预定义实体:

除了预定义的安全实体外,我们可以轻松地添加额外的实体。我们将在本章后面介绍如何做到这一点,但首先让我们讨论 GlassFish 的预定义安全实体。
admin 实体
admin 实体有一个预定义的用户名为 admin,它属于一个预定义的组名为 asadmin。
为了说明如何向实体添加用户,让我们向 admin 实体添加一个新用户。这将允许额外的用户登录到 GlassFish 网络控制台。为了向 admin 实体添加用户,请登录到 GlassFish 网络控制台,并展开左侧的 配置 节点。然后展开 server-config 节点,接着是 安全 节点。然后展开 实体 节点并点击 admin-realm。页面主要区域应如下截图所示:

要将用户添加到文件域,请单击左上角标有管理用户的按钮。页面主区域现在应该看起来像以下截图:

要将新用户添加到域中,只需单击屏幕左上角的新建...按钮,然后输入新用户信息,如下截图所示:

如此截图所示,我们添加了一个名为root的新用户,将该用户添加到asadmin组,并输入了该用户的密码。
注意
GlassFish 网络控制台将只允许asadmin组中的用户登录。未能将我们的用户添加到这个安全组将阻止他/她登录到控制台。
我们现在可以在管理员域用户列表中看到我们新创建的用户,如下截图所示:

我们已成功为 GlassFish 网络控制台添加了新用户。我们可以通过使用新用户的凭据登录控制台来测试这个新账户。
文件域
GlassFish 的第二个预定义域是文件域。该域以加密文本文件的形式存储用户信息。将用户添加到该域与将用户添加到管理员域非常相似。我们可以通过导航到配置 | 服务器配置 | 安全 | 域来添加用户。在域节点下,单击文件,然后单击管理用户按钮,最后单击新建...按钮。页面主区域应该看起来像以下截图:

由于此域旨在用于我们的应用程序,我们可以创建自己的组。组对于给多个用户赋予相同的权限非常有用。例如,所有需要管理权限的用户都可以添加到管理员组(组名当然是任意的)。
在本例中,我们将用户 ID 为peter的用户添加到了appuser和appadmin组。
单击确定按钮应保存新用户并带我们到该域的用户列表,如下截图所示:

单击新建...按钮允许我们向域中添加更多用户。以下截图显示了如何添加名为joe的额外用户,他仅属于appuser组:

如本节所示,将用户添加到文件域非常简单。现在我们将说明如何通过文件域进行用户认证和授权。
通过文件域进行基本认证
在上一节中,我们介绍了如何将用户添加到文件域以及如何将这些用户分配到组中。在本节中,我们将说明如何保护一个网络应用程序,以确保只有经过适当认证和授权的用户才能访问它。这个网络应用程序将使用文件域进行用户访问控制。
应用程序将包含几个非常简单的 JSF 页面。所有认证逻辑都由应用服务器处理;因此,为了确保应用程序的安全,我们只需要在其部署描述符中做出修改,即web.xml和glassfish-web.xml。我们首先讨论web.xml,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.jsf</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsf</welcome-file>
</welcome-file-list>
<security-constraint>
<web-resource-collection>
<web-resource-name>Admin Pages</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>AllPages</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>file</realm-name>
</login-config>
</web-app>
<security-constraint>元素定义了谁可以访问与特定 URL 模式匹配的页面。页面的 URL 模式在<url-pattern>元素中定义,如示例所示,它必须嵌套在<web-resource-collection>元素内部。允许访问页面的角色在<role-name>元素中定义,它必须嵌套在<auth-constraint>元素内部。
在我们的示例中,我们定义了两组需要保护的页面。第一组页面是那些 URL 以/admin开头的页面。只有属于 admin 组的用户可以访问这些页面。第二组页面是其余的页面,由/*的 URL 模式定义。只有具有user角色的用户可以访问这些页面。值得注意的是,第二组页面是第一组页面的超集,也就是说,任何 URL 匹配/admin/*的页面也匹配/*。在这种情况下,最具体的案例获胜。在这个特定的情况下,具有user角色(但没有admin角色)的用户将无法访问任何以/admin开头的页面。
为了保护我们的页面,我们需要在web.xml中添加的下一个元素是<login-config>元素。此元素必须包含一个<auth-method>元素,它定义了应用程序的授权方法。此元素的合法值包括BASIC、DIGEST、FORM和CLIENT-CERT。
BASIC表示将使用基本认证。这种认证方式会导致浏览器生成一个弹出窗口,提示用户输入用户名和密码,以便在用户第一次尝试访问受保护页面时显示。除非使用 HTTPS 协议,否则在使用基本认证时,用户的凭据将以 Base64 编码,而不是加密。攻击者解码这些凭据相对容易;因此,不建议使用基本认证。
DIGEST认证值与基本认证类似,不同之处在于它使用 MD5 摘要来加密用户凭据,而不是以 Base64 编码的形式发送。
FORM认证值使用包含用户名和密码字段的 HTML 或 JSP 自定义页面。然后,表单中的值将与安全领域进行核对,以进行用户认证和授权。除非使用 HTTPS,否则在使用基于表单的认证时,用户凭据将以明文形式发送;因此,建议使用 HTTPS,因为它可以加密数据。我们将在本章后面介绍如何设置 GlassFish 以使用 HTTPS。
CLIENT-CERT认证值使用客户端证书来验证和授权用户。
<login-config>中的<realm-name>元素指示用于身份验证和授权用户的安全域。在这个特定示例中,我们使用的是文件域。
我们在本节中讨论的所有web.xml元素都可以与任何安全域一起使用;它们并不局限于文件域。将我们的应用程序与文件域联系在一起的是<realm-name>元素的值。还需要注意的是,并非所有身份验证方法都受所有域支持。文件域仅支持基本和基于表单的身份验证。
在我们能够成功验证用户之前,我们需要将web.xml中定义的用户角色与域中定义的组链接起来。我们通过以下方式在glassfish-web.xml部署描述符中完成此操作:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
<context-root>/filerealmauth</context-root>
<security-role-mapping>
<role-name>admin</role-name>
<group-name>appadmin</group-name>
</security-role-mapping>
<security-role-mapping>
<role-name>user</role-name>
<group-name>appuser</group-name>
</security-role-mapping>
<class-loader delegate="true"/>
</glassfish-web-app>
如前例所示,glassfish-web.xml部署描述符可以有一个或多个<security-role-mapping>元素。对于web.xml中每个<auth-constraint>标签中定义的每个角色,都需要这些元素中的一个。<role-name>子元素指示要映射的角色。其值必须与web.xml中相应的<role-name>元素的值匹配。《group-name>`子元素必须与用于在应用程序中身份验证用户的安全域中的安全组值匹配。
在这个示例中,第一个<security-role-mapping>元素将应用程序的web.xml部署描述符中定义的admin角色映射到我们在本章前面添加用户到文件域时创建的appadmin组。第二个<security-role-mapping>元素将web.xml中的user角色映射到文件域中的appuser组。
如前所述,我们不需要在我们的代码中进行任何操作以进行身份验证和授权用户。我们只需要修改本节中描述的应用程序的部署描述符。由于我们的应用程序只是几个简单的页面,因此我们将不会展示它们的源代码。我们应用程序的结构如下截图所示:

根据我们在部署描述符中设置的应用程序方式,具有user角色的用户将能够访问应用程序根目录下的两个页面(index.xhtml和random.xhtml)。只有具有admin角色的用户才能访问admin文件夹下的任何页面,在这个特定情况下是一个名为index.xhtml的单页。
在打包和部署我们的应用程序并将浏览器指向其任何页面的 URL 之后,我们应该看到一个弹出窗口,要求输入用户名和密码,如下截图所示:

在输入正确的用户名和密码后,我们将被引导到我们试图查看的页面,如下所示:

在这一点上,用户可以导航到应用程序中他/她被允许访问的任何页面,无论是通过跟随链接还是通过在浏览器中输入 URL,而无需重新输入用户名和密码。
注意,我们以用户joe的身份登录;此用户仅属于user角色。因此,他没有访问以/admin开头的任何页面的权限。如果joe尝试访问这些页面之一,他将收到一个显示HTTP 状态 403-禁止访问的 HTTP 错误,如下面的截图所示:

只有属于admin角色的用户才能看到截图中显示的 URL 匹配的页面。当我们向文件域添加用户时,我们添加了一个名为peter的用户,他拥有这个角色。如果我们以peter的身份登录,我们将能够看到请求的页面。对于基本身份验证,退出应用程序的唯一可能方法是关闭浏览器。因此,要登录为peter,我们需要关闭并重新打开浏览器。一旦以 Peter 的身份登录,我们将看到如下所示的窗口:

如前所述,我们在本例中使用的基本身份验证方法的一个缺点是登录信息没有加密。一种解决方法是通过使用 HTTPS(SSL 上的 HTTP)协议。当使用此协议时,浏览器和服务器之间的所有信息都会被加密。
使用 HTTPS 最简单的方法是修改应用程序的web.xml部署描述符,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<security-constraint>
<web-resource-collection>
<web-resource-name>Admin Pages</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>AllPages</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>file</realm-name>
</login-config>
</web-app>
如我们所见,为了让应用程序仅通过 HTTPS 访问,我们只需在每个想要加密的页面集中添加一个包含嵌套的<transport-guarantee>元素的<user-data-constraint>元素。需要保护的页面集在web.xml部署描述符中的<security-constraint>元素中声明。
现在,当我们通过(不安全的)HTTP 端口(默认为 8080)访问应用程序时,请求将被自动转发到(安全的)HTTPS 端口(默认为 8181)。
在我们的例子中,我们将<transport-guarantee>元素的值设置为CONFIDENTIAL。这会加密浏览器和服务器之间的所有数据。此外,如果请求是通过未加密的 HTTP 端口发出的,它将被自动转发到安全的 HTTPS 端口。
<transport-guarantee>元素的另一个有效值是INTEGRAL。当使用此值时,浏览器和服务器之间数据的完整性得到保证。换句话说,数据在传输过程中不能被更改。当使用此值时,通过 HTTP 发出的请求不会自动转发到 HTTPS。如果用户尝试在设置此值时通过 HTTP 访问安全页面,浏览器将拒绝请求并返回 403(访问被拒绝)错误。
<transport-guarantee>元素的第三个也是最后一个有效值是NONE。当使用此值时,不对数据的完整性和机密性做出任何保证。NONE值是在<transport-guarantee>元素未出现在应用程序的web.xml部署描述符中时使用的默认值。
在对web.xml部署描述符进行前面的修改后,重新部署应用程序,并将浏览器指向应用程序中的任何页面,当我们在 Firefox 上访问我们的应用程序时,我们应该看到以下警告页面:

在展开我了解风险节点并单击标有添加异常...的按钮后,我们应该看到一个类似于以下截图的窗口:

在单击标有确认安全异常的按钮后,我们会提示输入用户名和密码。在输入适当的凭据后,我们被允许访问请求的页面,如下面的截图所示:

我们看到此警告的原因是,为了使服务器能够使用 HTTPS 协议,它必须有一个 SSL 证书。通常,SSL 证书由证书颁发机构(CA)如 Verisign 或 Thawte 颁发。这些证书颁发机构对证书进行数字签名。通过这样做,他们证明服务器属于它声称属于的实体。
从这些证书颁发机构之一获得的数字证书通常花费约 400 美元,并在一年后到期。由于这些证书的成本可能对开发或测试目的来说过高,GlassFish 预先配置了一个自签名的 SSL 证书。由于此证书未由证书颁发机构签名,当我们尝试通过 HTTPS 访问受保护页面时,浏览器会显示警告窗口。
注意截图中的 URL。协议设置为 HTTPS,端口号为 8181。我们指向浏览器的 URL 是http://localhost:8080/filerealmauthhttps,因为我们修改了应用程序的web.xml部署描述符,请求被自动重定向到这个 URL。当然,用户可以直接输入安全的 URL,它将没有问题工作。
通过 HTTPS 传输的所有数据都是加密的,包括在浏览器生成的弹出窗口中输入的用户名和密码。使用 HTTPS 允许我们安全地使用基本身份验证。然而,基本身份验证有一个缺点,那就是用户注销应用程序的唯一方式是关闭浏览器。如果我们需要允许用户在不关闭浏览器的情况下注销应用程序,我们需要使用基于表单的身份验证。
基于表单的身份验证
我们需要修改应用的web.xml部署描述符,以便使用基于表单的认证,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.jsf</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsf</welcome-file>
</welcome-file-list>
<security-constraint>
<web-resource-collection>
<web-resource-name>Admin Pages</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>AllPages</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
<user-data-constraint>
<description/>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>file</realm-name>
<form-login-config>
<form-login-page>/login.jsf</form-login-page>
<form-error-page>/loginerror.jsf</form-error-page>
</form-login-config>
</login-config>
</web-app>
当使用基于表单的认证时,我们只需在web.xml中将<auth-method>元素的值设置为FORM。使用此认证方法时,我们需要提供一个登录页面和一个登录错误页面。我们将登录和登录错误页面的 URL 分别作为<form-login-page>和<form-error-page>元素的值。正如我们在示例中所看到的,这些元素必须嵌套在<form-login-config>元素内部。
我们应用的登录页面标记如下所示:
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html >
<h:head>
<title>Login</title>
</h:head>
<h:body>
<p>Please enter your username and password to access the application
</p>
<form method="POST" action="j_security_check">
<table cellpadding="0" cellspacing="0" border="0">
<tr>
<td align="right">Username: </td>
<td>
<input type="text" name="j_username"/>
</td>
</tr>
<tr>
<td align="right">Password: </td>
<td>
<input type="password" name="j_password"/>
</td>
</tr>
<tr>
<td></td>
<td>
<input type="submit" value="Login"/>
</td>
</tr>
</table>
</form>
</h:body>
</html>
请注意,尽管我们的登录页面是一个 JSF 页面,但它使用的是标准的<form>标签,而不是 JSF 特定的<h:form>标签。这样做的原因是表单的action属性值必须是j_security_check,而在 JSF 的<h:form>标签中无法设置此属性。同样,表单中的输入字段是标准的 HTML 字段,而不是它们的 JSF 特定对应字段。
使用基于表单认证的应用的登录页面必须包含一个方法为POST且行为为j_security_check的表单。我们不需要实现认证代码,因为这是由应用服务器提供的。
登录页面中的表单必须包含一个名为j_username的文本字段。这个文本字段是用来存放用户名的。此外,表单还必须包含一个名为j_password的密码字段,用于存放密码。当然,表单必须包含一个提交按钮,以便将数据提交到服务器。
对于登录页面,唯一的要求是它必须有一个与我们的示例中属性匹配的表单,以及前一段中描述的j_username和j_password输入字段。
对于错误页面,没有特殊要求。当然,它应该显示一个错误消息,告诉用户登录失败。然而,它可以包含任何我们希望的内容。我们应用的错误页面只是告诉用户登录时出现了错误,并链接回登录页面,给用户一个重新登录的机会。
除了登录页面和登录错误页面之外,我们还向应用中添加了一个 CDI 命名 bean。这允许我们实现注销功能,这是我们使用基本认证时无法实现的。实现注销功能的代码如下:
package net.ensode.glassfishbook;
import javax.enterprise.context.RequestScoped;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.inject.Named;
import javax.servlet.http.HttpSession;
@Named
@RequestScoped
public class LogoutManager {
public String logout() {
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
HttpSession session = (HttpSession) externalContext.getSession(true);
session.invalidate();
return "index?faces-redirect=true";
}
}
登出方法的前几行是为了获取HttpSession对象的引用。一旦我们获得这个对象,我们只需要调用它的invalidate()方法来使会话无效。在我们的代码中,我们将响应重定向到首页。由于此时会话已经无效,安全机制会自动将用户重定向到登录页面。
我们现在可以测试基于表单的身份验证了。在构建我们的应用程序、部署它并将浏览器指向其任何页面后,我们应该在浏览器中看到我们的登录页面,如下面的截图所示:

如果我们提交无效凭据,我们将自动转发到登录错误页面,如下面的截图所示:

我们可以点击重试链接再次尝试。在输入有效凭据后,我们被允许进入应用程序。以下截图显示了成功登录后的屏幕:

如我们所见,我们在页面上添加了一个注销链接。此链接将用户导向我们的 CDI 命名 bean 的logout()方法,正如之前提到的,这仅仅是无效化会话。从用户的角度来看,此链接将简单地让他们注销并导向登录屏幕。
证书领域
证书领域使用客户端证书进行身份验证。就像服务器端证书一样,客户端证书通常从证书颁发机构如 Verisign 或 Thawte 获取。这些证书颁发机构验证证书确实属于它所说的所有者。
从证书颁发机构获取证书需要付费并花费一些时间。在我们开发和/或测试应用程序时,从证书颁发机构之一获取证书可能并不实用。幸运的是,我们可以为测试目的创建自签名证书。
创建自签名证书
我们可以使用包含在Java 开发工具包(JDK)中的keytool实用程序轻松创建自签名证书。
注意
我们将简要介绍一些 keytool 实用程序的关键功能,特别是创建和将自签名证书导入 GlassFish 和浏览器所必需的功能。要了解更多关于 keytool 实用程序的信息,请参阅docs.oracle.com/javase/7/docs/technotes/tools/solaris/keytool.html。
您可以在命令行中输入以下命令来生成自签名证书:
keytool -genkey -v -alias selfsignedkey -keyalg RSA -storetype PKCS12 -keystore client_keystore.p12 -storepass wonttellyou -keypass wonttellyou
此命令假设 keytool 实用程序在系统路径中。此工具可以在 JDK 安装目录下的bin目录中找到。
将-storepass和-keypass参数的值替换为您自己的密码。这两个密码必须相同,才能成功使用证书对客户端进行身份验证。您可以为-alias参数选择任何值。您也可以为-keystore参数选择任何值。但是,值必须以.p12结尾,因为此命令生成的文件需要导入到网络浏览器中,并且如果没有.p12扩展名,则不会被识别。
在命令行中输入上述命令后,keytool 将提示以下信息:
What is your first and last name?
[Unknown]: David Heffelfinger
What is the name of your organizational unit?
[Unknown]: Book Writing Division
What is the name of your organization?
[Unknown]: Ensode Technology, LLC
What is the name of your City or Locality?
[Unknown]: Fairfax
What is the name of your State or Province?
[Unknown]: Virginia
What is the two-letter country code for this unit?
[Unknown]: US
Is CN=David Heffelfinger, OU=Book Writing Division, O="Ensode Technology, LLC", L=Fairfax, ST=Virginia, C=US correct?
[no]: y
在每个提示中输入数据后,keytool 将生成证书。它将被存储在当前目录中,文件名将是用于 -keystore 参数的值(例如示例中的 client_keystore.p12)。
为了能够使用此证书来验证我们自身,我们需要将其导入到浏览器中。虽然过程类似,但每个浏览器的具体步骤可能不同。在 Firefox 中,可以通过转到首选项菜单,点击出现的弹出窗口顶部的高级图标,然后点击证书选项卡来完成,如下面的截图所示:

我们需要导航到出现的窗口中的查看证书 | 你的证书 | 导入。然后导航并从创建它的目录中选择我们的证书。此时,Firefox 将要求我们输入用于加密证书的密码;在我们的例子中,我们使用 wonttellyou 作为密码。输入密码后,我们应该看到一个弹出窗口确认我们的证书已成功导入。然后我们应该在证书列表中看到它,如下面的截图所示:

我们已经将我们的证书添加到 Firefox 中,以便它可以用来验证我们。如果你使用的是其他浏览器,则过程将类似。请查阅浏览器文档以获取详细信息。
在上一步中创建的证书需要导出为 GlassFish 可以理解的格式。我们可以通过运行以下命令来完成此操作:
keytool -export -alias selfsignedkey -keystore client_keystore.p12 -storetype PKCS12 -storepass wonttellyou -rfc -file selfsigned.cer
-alias、-keystore 和 -storepass 参数的值必须与之前命令中使用的值匹配。对于 -file 参数,你可以选择任何值,但建议以 .cer 扩展名结尾。
由于我们的证书不是由证书颁发机构签发的,GlassFish 默认情况下不会将其识别为有效证书。GlassFish 根据创建它们的证书颁发机构知道哪些证书可以信任。这是通过将这些不同机构的证书存储在名为 cacerts.jks 的密钥库中实现的。此密钥库位于 [glassfish 安装目录]/glassfish/domains/domain1/config/cacerts.jks。
为了让 GlassFish 接受我们的证书,我们需要将其导入到 cacerts 密钥库中。这可以通过从命令行发出以下命令来完成:
keytool -import -file selfsigned.cer -keystore [glassfish installation directory]/glassfish/domains/domain1/config/cacerts.jks -keypass changeit -storepass changeit
在此阶段,keytool 将在命令行中显示以下证书信息,并询问我们是否信任它:
Owner: CN=David Heffelfinger, OU=Book Writing Division, O="Ensode Technology, LLC", L=Fairfax, ST=Virginia, C=US
Issuer: CN=David Heffelfinger, OU=Book Writing Division, O="Ensode Technology, LLC", L=Fairfax, ST=Virginia, C=US
Serial number: 7a3bca0
Valid from: Sun Oct 27 17:00:18 EDT 2013 until: Sat Jan 25 16:00:18 EST 2014
Certificate fingerprints:
MD5: 46:EA:41:ED:12:8A:EC:CE:8C:BE:F2:49:D5:71:00:ED
SHA1: 32:C2:D4:20:87:22:95:25:5D:B0:AC:35:43:0D:60:35:94:27:44:58
SHA256: 8C:2E:56:F4:98:45:AC:46:FD:20:27:38:D2:7D:BF:D8:2D:56:D3:91:B7:78:AA:ED:FA:93:30:27:77:7F:F9:03
Signature algorithm name: SHA256withRSA
Version: 3
Extensions:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: E8 75 1D 12 2F 18 D0 4B E5 84 C4 79 B6 C0 98 80 .u../..K...y....
0010: 33 84 E7 C0 3...
]
]
Trust this certificate? [no]: y
Certificate was added to keystore
一旦我们将证书添加到 cacerts.jks 密钥库中,我们需要重新启动域以使更改生效。
我们实际上在这里做的是将自己添加为 GlassFish 将信任的证书颁发机构。当然,这不应该在生产系统中进行。
-file 参数的值必须与我们导出证书时使用的此参数的值匹配。
注意
注意,changeit 是 -keypass 和 -storepass 参数的 cacerts.jks 仓库的默认密码。此值可以通过以下命令更改:
[glassfish installation directory]/glassfish/bin/asadmin change-master-password --savemasterpassword=true
此命令将提示输入现有密码和新密码。–savemasterpassword=true 参数是可选的;它将主密码保存到域根目录下名为 master-password 的文件中。如果我们更改主密码时不使用此参数,那么每次我们想要启动域时都需要输入主密码。
现在我们已经创建了一个自签名证书,将其导入到我们的浏览器中,并确立了自己作为 GlassFish 将信任的证书颁发机构,我们已准备好开发一个将使用客户端证书进行认证的应用程序。
配置应用程序使用证书领域
由于我们正在利用 Java EE 安全功能,我们不需要修改任何代码即可使用安全领域。我们只需要修改应用程序的配置,即其部署描述符 web.xml 和 glassfish-web.xml,如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">
<security-constraint>
<web-resource-collection>
<web-resource-name>AllPages</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>users</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<login-config>
<auth-method>CLIENT-CERT</auth-method>
<realm-name>certificate</realm-name>
</login-config>
</web-app>
与上一节中我们看到的 web.xml 部署描述符相比,这个 web.xml 部署描述符的主要区别在于 <login-config> 元素的内容。在这种情况下,我们声明 CLIENT-CERT 为授权方法,并将 certificate 作为用于认证的领域。这将导致 GlassFish 在允许用户进入应用程序之前要求浏览器提供一个客户端证书。
当使用客户端证书认证时,请求必须始终通过 HTTPS 进行。因此,将 <transport-guarantee> 元素添加到 web.xml 部署描述符中,并设置其值为 CONFIDENTIAL 是一个好主意。回想一下,上一节中提到这会将任何请求通过 HTTP 端口转发到 HTTPS 端口。如果我们不将此值添加到 web.xml 部署描述符中,任何通过 HTTP 端口的请求都会失败,因为客户端证书认证不能通过 HTTP 协议完成。
注意,我们声明只有具有 user 角色的用户可以访问系统中的任何页面。我们通过将 user 角色添加到 web.xml 部署描述符中 <security-constraint> 元素的 <auth-constraint> 元素内部的 <role-name> 元素来做到这一点。为了允许授权用户访问,我们需要将他们添加到这个角色中。这通过以下方式在 glassfish-web.xml 部署描述符中完成:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
<context-root>/certificaterealm</context-root>
<security-role-mapping>
<role-name>user</role-name>
<principal-name>CN=David Heffelfinger, OU=Book Writing Division, O="Ensode Technology, LLC", L=Fairfax, ST=Virginia, C=US</principal-name>
</security-role-mapping>
<class-loader delegate="true"/>
</glassfish-web-app>
此分配是通过将主要用户映射到glassfish-web.xml部署描述符中的<security-role-mapping>元素中的角色来完成的;其<role-name>子元素必须包含角色名称,而<principal-name>子元素必须包含用户名。此用户名来自证书。
如果您不确定要使用的名称,可以使用 keytool 实用工具从证书中获取,如下所示:
keytool -printcert -file selfsigned.cer
Owner: CN=David Heffelfinger, OU=Book Writing Division, O="Ensode Technology, LLC", L=Fairfax, ST=Virginia, C=US
Issuer: CN=David Heffelfinger, OU=Book Writing Division, O="Ensode Technology, LLC", L=Fairfax, ST=Virginia, C=US
Serial number: 7a3bca0
Valid from: Sun Oct 27 17:00:18 EDT 2013 until: Sat Jan 25 16:00:18 EST 2014
Certificate fingerprints:
MD5: 46:EA:41:ED:12:8A:EC:CE:8C:BE:F2:49:D5:71:00:ED
SHA1: 32:C2:D4:20:87:22:95:25:5D:B0:AC:35:43:0D:60:35:94:27:44:58
SHA256: 8C:2E:56:F4:98:45:AC:46:FD:20:27:38:D2:7D:BF:D8:2D:56:D3:91:B7:78:AA:ED:FA:93:30:27:77:7F:F9:03
Signature algorithm name: SHA256withRSA
Version: 3
Extensions:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: E8 75 1D 12 2F 18 D0 4B E5 84 C4 79 B6 C0 98 80 .u../..K...y....
0010: 33 84 E7 C0 3...
]
]
作为<principal-name>使用的值是Owner:之后的行。请注意,<principal-name>的值必须与其开放和关闭元素(<principal-name>和</principal-name>)在同一行。如果值前后有换行符或回车符,它们将被解释为值的一部分,验证将失败。
由于我们的应用程序只有一个用户和一个角色,我们已经准备好部署它。如果我们有更多用户,我们必须在glassfish-web.xml部署描述符中添加额外的<security-role-mapping>元素,至少每个用户一个。如果我们有属于多个角色的用户,那么我们将为用户所属的每个角色添加一个<security-role-mapping>元素,使用与每个用户的证书对应的<principal-name>值。
现在,我们已经准备好测试我们的应用程序。在我们部署它并将浏览器指向应用程序中的任何页面后,我们应该看到如下屏幕(假设浏览器尚未配置为在服务器请求证书时提供默认证书):

点击确定按钮后,我们就可以访问应用程序,如下面的截图所示:

在允许访问应用程序之前,GlassFish 会检查颁发证书的证书颁发机构(由于我们自签了证书,证书所有者和证书颁发机构是相同的),并对照受信任的证书颁发机构列表进行检查。由于我们将自己作为受信任的权威机构通过将我们的自签名证书导入cacerts.jks密钥库中,GlassFish 识别证书颁发机构为有效的。然后它从证书中获取主体名称,并将其与应用程序glassfish-web.xml文件中的条目进行比较。由于我们将自己添加到这个部署描述符中,并给自己分配了一个有效角色,因此我们被允许进入应用程序。
定义额外的域
除了我们在上一节中讨论的三个预配置的安全域之外,我们还可以为应用程序身份验证创建额外的域。我们可以创建与文件域或 admin 域行为完全相同的域。我们还可以创建与证书域行为相似的域。此外,我们还可以创建使用其他身份验证方法的域。我们可以对 LDAP 数据库和关系数据库进行用户身份验证,当 GlassFish 安装在 Solaris 服务器上时,在 GlassFish 中使用 Solaris 身份验证。此外,如果预定义的身份验证机制不符合我们的需求,我们可以实现自己的。
定义额外的文件域
在管理控制台中,展开配置节点,然后是server-config节点,接着是安全节点。点击域节点,然后在 Web 控制台主区域的结果页面上的新...按钮。
我们现在应该看到如下屏幕:

我们要创建一个额外的域,只需在名称字段中为其输入一个唯一的名称,为类名字段选择com.sun.enterprise.security.auth.realm.file.FileRealm,并为JAAS 上下文和密钥文件字段输入值;密钥文件字段的值必须是存储用户信息的文件的绝对路径,对于文件域,JAAS 上下文字段的值必须始终是fileRealm。
在输入所有必要的信息后,我们可以点击确定按钮,我们的新域将被创建。然后我们可以像使用预定义的文件域一样使用它。希望对此新域进行身份验证的应用程序必须使用其名称作为应用程序的web.xml部署描述符中<realm-name>元素的价值。
或者,可以通过asadmin实用程序从命令行添加自定义文件域,通过执行以下命令:
asadmin create-auth-realm --classname com.sun.enterprise.security.auth.realm.file.FileRealm --property file=/home/heffel/additionalFileRealmKeyFile:jaas-context=fileRealm newFileRealm
create-auth-realm参数告诉asadmin我们想要创建一个新的安全域。--classname参数的值对应于安全域类名。请注意,它与我们在 Web 控制台上选择的值相匹配。--property参数允许我们传递属性及其值;此参数的值必须是属性及其值的冒号(:)分隔列表。此命令的最后一个参数是我们希望给我们的安全域起的名字。
注意
虽然通过 Web 控制台设置安全域更容易,但通过asadmin命令行工具进行操作的优势在于它易于脚本化,允许我们将此命令保存在脚本中,并轻松配置多个 GlassFish 实例。
定义额外的证书域
要定义一个额外的证书领域,我们只需在名称字段中输入其名称,并将com.sun.enterprise.security.auth.realm.certificate.CertificateRealm作为类名字段的值,然后点击确定创建我们的新领域,如下面的屏幕截图所示:

希望使用这个新领域进行身份验证的应用程序必须在web.xml部署描述符中的<realm-name>元素中使用其名称,并将其<auth-method>元素的值指定为CLIENT-CERT。当然,客户端证书必须存在并按照配置应用程序以使用证书领域部分中所述进行配置。
或者,可以通过asadmin实用程序在命令行上创建自定义证书领域,执行以下命令:
asadmin create-auth-realm --classname com.sun.enterprise.security.auth.realm.certificate.CertificateRealm newCertificateRealm
在这种情况下,我们不需要传递任何属性,就像我们创建自定义文件领域时必须做的那样。因此,我们只需要传递适当的值给--classname参数,并指定新的安全领域名称。
定义 LDAP 领域
我们可以轻松设置一个领域以对LDAP(轻量级目录访问协议)数据库进行身份验证。为此,我们除了输入领域名称这一明显步骤外,还需要将com.sun.enterprise.security.auth.realm.ldap.LDAPRealm作为新领域的类名值。然后,我们需要在目录字段中输入目录服务器的 URL,以及用作搜索用户数据的基础 DN字段的值,如下面的屏幕截图所示:

注意
在撰写本文时,GlassFish 存在一个错误,阻止从 Web 管理控制台成功添加 LDAP 领域。在本节中,我们解释了应该发生什么,而不是实际发生了什么。希望在你阅读本文时,问题已经得到解决。
如本节稍后所述,从命令行添加 LDAP 领域的方法是正确的。
创建 LDAP 领域后,应用程序可以使用它来对 LDAP 数据库进行身份验证。领域的名称需要用作应用程序web.xml部署描述符中<realm-name>元素的值。<auth-method>元素的值必须是BASIC或FORM。
LDAP 数据库中的用户和角色可以使用本章前面讨论的<principal-name>、<role-name>和<group-name>元素映射到应用程序的glassfish-web.xml部署描述符中的组。
要从命令行创建 LDAP 领域,我们需要使用以下语法:
asadmin create-auth-realm --classname com.sun.enterprise.security.auth.realm.ldap.LDAPRealm --property "jaas-context=ldapRealm:directory=ldap\://127.0.0.1\:1389:base-dn=dc\=ensode,dc\=com" newLdapRealm
注意,在这种情况下,--property 参数的值在引号之间。这是必要的,因为我们需要转义其值中的某些字符,例如所有冒号和等号。为了转义这些特殊字符,我们只需在它们前面加上反斜杠(\)。
定义 Solaris 领域
当 GlassFish 安装在 Solaris 服务器上时,它可以通过 Solaris 领域利用操作系统的认证机制。此类领域没有特殊属性;我们只需为它选择一个名称,并将 com.sun.enterprise.security.auth.realm.solaris.SolarisRealm 作为 类名 字段的值,将 solarisRealm 作为 JAAS 上下文 字段的值,如以下截图所示:

JAAS 上下文 字段必须设置为 solarisRealm。添加领域后,应用程序可以使用基本认证或基于表单的认证对其进行认证。操作系统组和用户可以通过应用程序的 glassfish-web.xml 部署描述符中的 <principal-name>、<role-name> 和 <group-name> 元素映射到应用程序中定义的应用程序角色。
可以通过执行以下命令从命令行创建 Solaris 领域:
asadmin create-auth-realm --classname com.sun.enterprise.security.auth.realm.solaris.SolarisRealm --property jaas-context=solarisRealm newSolarisRealm
定义 JDBC 领域
我们可以创建的另一种领域类型是 JDBC 领域。这种类型的领域使用存储在数据库表中的用户信息进行用户认证。
为了说明如何对 JDBC 领域进行认证,我们需要创建一个数据库来存储用户信息。以下实体关系图显示了我们可以用来对 JDBC 领域进行认证的示例数据库:

我们的数据库由三个表组成。一个是存储用户信息的 USERS 表,另一个是存储组信息的 GROUPS 表。由于 USERS 和 GROUPS 之间存在多对多关系,我们需要添加一个连接表以保持数据规范化。这个第三个表的名称是 USER_GROUPS。
注意,USERS 表的 PASSWORD 列是 CHAR(32) 类型。我们选择这种类型而不是 VARCHAR 的原因是我们将使用 MD5 散列算法来散列密码,而这些散列总是 32 个字符。
可以通过使用 JDK 包含的 java.security.MessageDigest 类轻松地将密码以预期格式加密。以下示例代码将接受明文密码并从中创建一个 MD5 散列:
package net.ensode.glassfishbook;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class EncryptPassword {
public static String encryptPassword(String password)throws NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] bs;
messageDigest.reset();
bs = messageDigest.digest(password.getBytes());
StringBuilder stringBuilder = new StringBuilder();
//hex encode the digest
for (int i = 0; i < bs.length; i++) {
String hexVal = Integer.toHexString(0xFF & bs[i]);
if (hexVal.length() == 1) {
stringBuilder.append("0");
}
stringBuilder.append(hexVal);
}
return stringBuilder.toString();
}
public static void main(String[] args) {
String encryptedPassword = null;
try {
if (args.length == 0) {
System.err.println("Usage: java "+ "net.ensode.glassfishbook.EncryptPassword "+ "cleartext");
} else {
encryptedPassword = encryptPassword(args[0]);
System.out.println(encryptedPassword);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
上述类的主要功能在其 encryptPassword() 方法中定义。它基本上接受一个明文字符串,并使用 java.security.MessageDigest 实例的 digest() 方法使用 MD5 算法对其进行散列。然后,它将散列编码为一系列十六进制数字。这种编码是必要的,因为 GlassFish 默认期望 MD5 散列的密码是十六进制编码的。
当使用 JDBC 域时,应用程序用户和组不是通过 GlassFish 控制台添加到域中的。相反,它们是通过向适当的表中插入数据来添加的。
一旦我们设置了将保存用户凭据的数据库,我们就可以创建一个新的 JDBC 域。
我们可以通过在 GlassFish 网络控制台的新域表单的名称字段中输入其名称来创建一个 JDBC 域,然后选择 com.sun.enterprise.security.auth.realm.jdbc.JDBCRealm 作为类名字段的值,如下截图所示:

我们还需要为我们的新 JDBC 域设置一些其他属性,如下截图所示:

JAAS 上下文字段必须设置为 jdbcRealm 以用于 JDBC 域。JNDI属性值必须是包含域用户和组数据的数据库对应的数据源的 JNDI 名称。用户表属性值必须是包含用户名和密码信息的表的名称。
注意
注意,在前面的截图中,我们使用了 V_USER_ROLE 作为用户表属性的值。V_USER_ROLE 是一个包含用户和组信息的数据库视图。我们没有直接使用 USERS 表,因为 GlassFish 假设用户表和组表都包含一个包含用户名的列。这样做会导致数据重复。为了避免这种情况,我们创建了一个视图,我们可以将其用作用户表和组表(稍后讨论)属性的值。
用户名列属性必须包含在用户表属性中包含用户名的列。密码列属性值必须是用户表属性中包含用户密码的列的名称。组表属性值必须是包含用户组的表的名称。组名列属性必须包含在组表属性中包含用户组名称的列的名称。
所有其他属性都是可选的,在大多数情况下留空。特别值得注意的是摘要算法属性。此属性允许我们指定用于散列用户密码的消息摘要算法。此属性的合法值包括 JDK 所支持的所有算法。这些算法包括 MD2、MD5、SHA-1、SHA-256、SHA-384 和 SHA-512。此外,如果我们希望以明文形式存储用户密码,我们可以通过为此属性使用值 none 来实现。
注意
MD2、MD5 和 SHA-1 并不是很安全,在大多数情况下不应使用。
一旦我们定义了我们的 JDBC 域,我们需要通过其 web.xml 和 glassfish-web.xml 部署描述符来配置我们的应用程序。配置应用程序以依赖 JDBC 域进行授权和认证的方式与使用任何其他类型的域相同。
除了声明我们将依赖 JDBC 域进行认证和授权之外,就像使用其他类型的域一样,我们还需要将 web.xml 部署描述符中定义的角色映射到安全组名称。这可以在 glassfish-web.xml 部署描述符中完成。
可以通过执行以下命令从命令行创建 JDBC 域:
asadmin create-auth-realm --classname com.sun.enterprise.security.ee.auth.realm.jdbc.JDBCRealm
--property jaas-context=jdbcRealm:datasource-jndi=jdbc/__UserAuthPool:user-table=V_USER_ROLE:user-name-column=USERNAME:password-column=PASSWORD:group-table=V_USER_ROLE:group-name-column=GROUP_NAME newJdbcRealm
定义自定义域
尽管预定义的域类型应该涵盖绝大多数情况,但如果我们提供的类型不符合我们的需求,我们可以创建定制的域类型。这样做需要编写定制的 Realm 和 LoginModule 类。让我们首先讨论定制的 Realm 类如下:
package net.ensode.glassfishbook;
import java.util.Enumeration;
import java.util.Vector;
import com.sun.enterprise.security.auth.realm.IASRealm;
import com.sun.enterprise.security.auth.realm.InvalidOperationException;
import com.sun.enterprise.security.auth.realm.NoSuchUserException;
public class SimpleRealm extends IASRealm {
@Override
public Enumeration getGroupNames(String userName)throws InvalidOperationException, NoSuchUserException {
Vector vector = new Vector();
vector.add("appuser");
vector.add("appadmin");
return vector.elements();
}
@Override
public String getAuthType() {
return "simple";
}
@Override
public String getJAASContext() {
return "simpleRealm";
}
public boolean loginUser(String userName, String password) {
boolean loginSuccessful = false;
if ("glassfish".equals(userName) && "secret".equals(password)) {
loginSuccessful = true;
}
return loginSuccessful;
}
}
我们的定制 Realm 类必须扩展 com.sun.enterprise.security.auth.realm.IASRealm。此类位于 security.jar 文件中,因此必须在我们的 Realm 类成功编译之前将此 JAR 文件添加到 CLASSPATH 中。
注意
security.jar 文件可以在 [glassfish 安装目录]/glassfish/modules 下找到。
当使用 Maven 或 Ivy 依赖管理时,此 JAR 文件可以在以下存储库中找到:
download.java.net/maven/glassfish
组 ID 是 org.glassfish.security,而工件 ID 是 security。
我们必须重写一个名为 getGroupNames() 的方法。该方法接受一个字符串作为参数,并返回一个 Enumeration 参数。String 参数用于尝试登录域的用户的用户名。Enumeration 参数必须包含一个字符串集合,指示用户属于哪些组。在我们的简单示例中,我们只是硬编码了这些组。在实际应用程序中,这些组将从某种持久存储(数据库、文件等)中获取。
我们必须重写的下一个 Realm 类方法的是 getAuthType() 方法。该方法必须返回一个包含此域所使用的认证类型描述的 String。
getGroupNames() 和 getAuthType() 方法在 IASRealm(父)类中被声明为抽象的。尽管 getJAASContext() 方法不是抽象的,但我们仍然应该重写它,因为返回的值用于确定从应用程序服务器的 login.conf 文件中使用的认证类型。此方法的返回值用于将域映射到相应的登录模块。
最后,我们的 Realm 类必须包含一个用于认证用户的方法。我们可以自由地给它起任何名字。此外,我们可以使用任何类型的任意数量的参数。我们的简单示例将单个用户名和密码的值硬编码。再次强调,一个真实的应用程序会从某种持久存储中获取有效的凭证。这个方法是从相应的登录模块类中调用的,如下所示:
package net.ensode.glassfishbook;
import java.util.Enumeration;
import javax.security.auth.login.LoginException;
import com.sun.appserv.security.AppservPasswordLoginModule;
import com.sun.enterprise.security.auth.realm.InvalidOperationException;
import com.sun.enterprise.security.auth.realm.NoSuchUserException;
public class SimpleLoginModule extends AppservPasswordLoginModule {
@Override
protected void authenticateUser() throws LoginException {
Enumeration userGroupsEnum = null;
String[] userGroupsArray = null;
SimpleRealm simpleRealm;
if (!(_currentRealm instanceof SimpleRealm)) {
throw new LoginException();
} else {
simpleRealm = (SimpleRealm) _currentRealm;
}
if (simpleRealm.loginUser(_username, _password)) {
try {
userGroupsEnum = simpleRealm.getGroupNames(_username);
} catch (InvalidOperationException e) {
throw new LoginException(e.getMessage());
} catch (NoSuchUserException e) {
throw new LoginException(e.getMessage());
}
userGroupsArray = new String[2];
int i = 0;
while (userGroupsEnum.hasMoreElements()) {
userGroupsArray[i++] = ((String) userGroupsEnum.nextElement());
}
} else {
throw new LoginException();
}
commitUserAuthentication(userGroupsArray);
}
}
我们的登录模块类必须扩展 com.sun.appserv.security.AppservPasswordLoginModule。这个类也位于 security.jar 文件中。我们的登录模块类只需要重写一个方法,即 authenticateUser()。这个方法不接受任何参数,如果用户认证失败,必须抛出 LoginException 异常。_currentRealm 变量在父类中定义,其类型为 com.sun.enterprise.security.auth.realm。Realm 类是所有 Realm 类的父类。这个变量在执行 authenticateUser() 方法之前被初始化。LoginModule 类必须验证这个类是否为预期的类型(在我们的例子中是 SimpleRealm)。如果不是,必须抛出 LoginException 异常。
在执行 authenticateUser() 方法之前定义并初始化的两个其他变量是 _username 和 _password。这些变量包含用户在登录表单(对于基于表单的认证)或弹出窗口(对于基本认证)中输入的凭证。在我们的例子中,我们简单地将这些值传递给 Realm 类,以便它可以验证用户凭证。
在成功认证后,authenticateUser() 方法必须调用父类的 commitUserAuthentication() 方法。这个方法接受一个包含用户所属组的字符串对象数组。在我们的例子中,我们简单地调用 Realm 类中定义的 getGroupNames() 方法,并将它返回的 Enumeration 参数的元素添加到一个数组中;然后将这个数组传递给 commitUserAuthentication()。
GlassFish 对我们的自定义领域和登录模块类不存在认知。我们需要将这些类添加到 GlassFish 的 CLASSPATH 中。最简单的方法是将包含我们的自定义领域和登录模块的 JAR 文件复制到 [glassfish 安装目录]/glassfish/domains/domain1/lib。
在我们可以对我们的自定义领域进行应用程序认证之前,我们需要遵循的最后一步是将我们的新自定义领域添加到域的 login.conf 文件中,如下所示:
fileRealm {
com.sun.enterprise.security.auth.login.FileLoginModule required;
};
ldapRealm {
com.sun.enterprise.security.auth.login.LDAPLoginModule required;
};
solarisRealm {
com.sun.enterprise.security.auth.login.SolarisLoginModule required;
};
jdbcRealm {
com.sun.enterprise.security.auth.login.JDBCLoginModule required;
};
jdbcDigestRealm {
com.sun.enterprise.security.auth.login.JDBCDigestLoginModule required;
};
pamRealm {
com.sun.enterprise.security.ee.auth.login.PamLoginModule required;
};
simpleRealm {
net.ensode.glassfishbook.SimpleLoginModule required;
};
开括号前的值必须与 Realm 类中定义的 getJAASContext() 方法的返回值匹配。在这个文件中,Realm 和 LoginModule 类被链接在一起。GlassFish 域需要重新启动才能使这个更改生效。
现在我们已经准备好使用我们的自定义领域在我们的应用程序中验证用户。我们需要通过 GlassFish 的管理控制台添加我们创建的新领域类型,如下截图所示:

要创建我们的领域,像往常一样,我们需要给它一个名称。而不是从下拉菜单中选择类名,我们需要将其输入到文本字段中。我们的自定义领域没有任何属性;因此,在这个例子中我们不需要添加任何属性。如果有,它们将通过点击添加属性按钮并输入属性名称和相应的值来添加。然后,我们的领域将通过覆盖其父类的init()方法来获取属性。此方法具有以下签名:
protected void init(Properties arg0) throws BadRealmException, NoSuchRealmException
它作为参数接受的java.util.Properties实例将预先填充截图所示页面中输入的属性(我们的自定义领域没有任何属性,但对于那些有属性的情况,属性是在截图所示的页面中输入的)。
一旦我们为我们的新自定义领域添加了相关信息,我们就可以像使用任何预定义领域一样使用它。应用程序需要指定其名称作为应用程序web.xml部署描述符中<realm-name>元素的值。在应用程序级别不需要做任何特别的事情。
就像标准领域一样,可以通过asadmin命令行实用程序添加自定义领域,例如,对于我们的自定义领域,我们将执行以下命令:
asadmin create-auth-realm --classname net.ensode.glassfishbook.SimpleRealm newCustomRealm
摘要
在本章中,我们介绍了如何使用 GlassFish 的默认领域来验证我们的 Web 应用程序。我们介绍了文件领域,它将用户信息存储在平面文件中,以及证书领域,它要求客户端证书进行用户身份验证。
然后,我们介绍了如何使用 GlassFish 包含的领域类创建行为类似于默认领域的额外领域。
我们还介绍了如何使用 GlassFish 中包含的额外Realm类来创建针对 LDAP 数据库、关系数据库进行身份验证的领域,以及如何创建与 Solaris 服务器身份验证机制集成的领域。
最后,我们介绍了如何为那些内置的领域不满足我们需求的情况创建自定义Realm类。
在下一章中,我们将介绍使用 JAX-WS 的 SOAP Web 服务。
第十章。使用 JAX-WS 的网络服务
Java EE 规范将 JAX-WS API 作为其技术之一。JAX-WS 是在 Java 平台上开发 简单对象访问协议 (SOAP) 网络服务的标准方式。它代表 Java API for XML Web Services。JAX-WS 是一个高级 API;通过 JAX-WS 调用网络服务是通过远程过程调用完成的。JAX-WS 是 Java 开发者非常自然的 API。
网络服务是可以远程调用的应用程序编程接口。网络服务可以从任何编程语言编写的客户端调用。
我们将涵盖的一些主题包括:
-
使用 JAX-WS API 开发网络服务
-
使用 JAX-WS API 开发网络服务客户端
-
在网络服务调用中添加附件
-
将 EJB 作为网络服务公开
-
保护网络服务
使用 JAX-WS API 开发网络服务
JAX-WS 是一个高级 API,它简化了基于 SOAP 的网络服务的开发。通过 JAX-WS 开发网络服务包括编写一个具有公开方法的类,这些方法将被公开为网络服务。该类需要使用 @WebService 注解进行装饰。类中的所有公开方法都将自动公开为网络服务,它们可以选择使用 @WebMethod 注解进行装饰。以下示例说明了这个过程:
package net.ensode.glassfishbook;
import javax.jws.WebMethod;
import javax.jws.WebService;
@WebService
public class Calculator {
@WebMethod
public int add(int first, int second) {
return first + second;
}
@WebMethod
public int subtract(int first, int second) {
return first - second;
}
}
前面的类将其两个方法公开为网络服务。add() 方法简单地将它接收的两个 int 基本参数相加并返回结果。subtract() 方法从其两个参数中减去并返回结果。
我们通过使用 @WebService 注解来装饰类,表明该类实现了网络服务。任何我们希望公开为网络服务的方法都可以使用 @WebMethod 注解进行装饰;然而,这不是必需的,因为所有公开方法都自动公开为网络服务。
为了部署我们的网络服务,我们需要将其打包成 .war 文件。在 Java EE 6 之前,所有有效的 .war 文件都必须在其 WEB-INF 目录中包含一个 web.xml 部署描述符。正如我们在前面的章节中已经讨论过的,在 Java EE 6(及以后)中,这个部署描述符是可选的,并且在这个环境中部署网络服务时不需要。
如果我们选择添加 web.xml 部署描述符,为了成功部署我们的网络服务,不需要在 .war 文件的 web.xml 中添加任何内容。只需在部署描述符中有一个空的 <web-app> 元素就足够成功部署我们的 WAR 文件,如下面的代码所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
</web-app>
编译、打包和部署代码后,我们可以通过登录 GlassFish 管理员 Web 控制台并展开左侧的 应用程序 节点来验证它是否成功部署。我们应该在这个节点下看到我们新部署的网络服务,如下面的截图所示:

在前面的截图中,请注意页面右下角有一个 查看端点 链接。点击该按钮将带我们到以下截图所示的 Web 服务端点信息 页面,其中包含有关我们的 Web 服务的某些信息:

注意前面的截图中的链接标签为 Tester:;点击此链接将带我们到一个自动生成的页面,允许我们测试我们的 Web 服务。此页面看起来如下面的截图所示:

要测试方法,我们只需在文本框中输入一些参数,然后点击相应的按钮。例如,在 add 方法的文本框中输入值 2 和 3,然后点击 add 按钮将产生以下输出:

JAX-WS 在幕后使用 SOAP 协议在 Web 服务客户端和服务器之间交换信息。通过向下滚动前面的页面,我们可以看到我们的测试生成的 SOAP 请求和响应,如下面的截图所示:

作为应用程序开发者,我们不需要过多关注这些 SOAP 请求,因为它们由 JAX-WS API 自动处理。
Web 服务客户端需要一个 Web 服务定义语言 (WSDL) 文件,以便生成可执行代码,他们可以使用这些代码来调用 Web 服务。WSDL 是一种基于 XML 的标准接口定义语言,它定义了 Web 服务的功能。
WSDL 文件通常放置在 Web 服务器上,并通过其 URL 由客户端访问。当部署使用 JAX-WS 开发的 Web 服务时,会自动为我们生成一个 WSDL 文件。我们可以通过点击 Web 服务端点信息 页面上的 查看 WSDL 链接来查看它及其 URL,如下面的截图所示:

注意浏览器位置文本框中的 WSDL URL。在为我们 Web 服务开发客户端时,我们需要这个 URL。
开发 Web 服务客户端
如前所述,需要从 Web 服务的 WSDL 生成可执行代码。然后,Web 服务客户端将调用此可执行代码来访问 Web 服务。
GlassFish 包含一个用于从 WSDL 生成 Java 代码的实用工具。该实用工具的名称是 wsimport。它可以在 [glassfish 安装目录]/glassfish/bin/ 下找到。wsimport 的唯一必需参数是 WSDL 的 URL,它对应于 Web 服务,例如,wsimport http://localhost:8080/calculatorservice/CalculatorService?wsdl。
前一个屏幕截图中的命令将生成以下编译后的 Java 类,允许客户端应用程序访问我们的 Web 服务:
-
Add.class -
AddResponse.class -
Calculator.class -
CalculatorService.class -
ObjectFactory.class -
package-info.class -
Subtract.class -
SubtractResponse.class
注意
保留生成的源代码
默认情况下,生成的类文件的源代码会自动删除;可以通过传递-keep参数给wsimport来保留它。
这些类需要添加到客户端的 CLASSPATH 中,以便客户端代码可以访问它们。
除了命令行工具之外,GlassFish 还包括一个自定义的 ANT 任务,可以从 WSDL 生成代码。以下 ANT 构建脚本说明了其用法:
<project name="calculatorserviceclient" default="wsimport" basedir=".">
<target name="wsimport">
<taskdef name="wsimport"
classname="com.sun.tools.ws.ant.WsImport">
<classpath path="/opt/glassfish-4.0/glassfish/modules/webservices-osgi.jar"/>
<classpath path="/opt/glassfish-4.0/glassfish/modules/jaxb-osgi.jar"/>
<classpath path="/opt/glassfish-4.0/glassfish/lib/javaee.jar"/>
</taskdef>
<wsimport wsdl=" HYPERLINK "http://localhost:8080/calculatorservice/CalculatorService?wsdl"http://localhost:8080/calculatorservice/CalculatorService?wsdl"xendorsed="true"/>
</target>
</project>
上述示例是一个非常简单的 ANT 构建脚本,它仅说明了如何设置自定义的<wsimport> ANT 目标。实际上,项目的 ANT 构建脚本会有几个其他目标,用于编译、构建.war文件等。
由于<wsimport>是一个自定义的 ANT 目标,并且它不是标准的,我们需要在我们的 ANT 构建脚本中添加一个<taskdef>元素。我们需要设置name和classname属性,如示例所示。此外,我们还需要通过嵌套的<classpath>元素将以下.jar文件添加到任务的 CLASSPATH 中:
-
webservices-osgi.jar -
jaxb-osgi.jar -
javaee.jar
webservices-osgi.jar和jaxb-osgi.jar文件可以在[glassfish 安装目录]/glassfish/modules目录下找到。javaee.jar文件包含所有 Java EE API,可以在[glassfish 安装目录]/glassfish/lib下找到。
一旦我们通过<taskdef>元素设置了自定义的<wsimport>任务,我们就可以使用它了。我们需要通过其wsdl属性来指定 WSDL 的位置。一旦这个任务执行,就会生成访问由 WSDL 定义的 Web 服务所需的 Java 代码。
JDK 1.6 捆绑了 JAX-WS 2.1。如果我们使用这个版本的 JDK,我们需要告诉 ANT 使用 GlassFish 中包含的 JAX-WS 2.2 API。这可以通过将自定义wsimport ANT 任务的xendorsed属性设置为true轻松完成。
使用 Maven 构建项目的读者可以利用 Maven 的AntRun插件在构建代码时执行wsimport ANT 目标。这种方法在以下pom.xml文件中得到了说明。
<?xml version="1.0" encoding="UTF-8" ?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.ensode.glassfishbook</groupId>
<artifactId>calculatorserviceclient</artifactId>
<packaging>jar</packaging>
<name>Simple Web Service Client</name>
<version>1.0</version>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<id>maven2-repository.dev.java.net</id>
<name>Java.net Repository for Maven 2</name>
<url>http://download.java.net/maven/2/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>calculatorserviceclient</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>generate-sources</phase>
<configuration>
<tasks>
<property name="target.dir" value="target" />
<delete dir="${target.dir}/classes/com/testapp/ws/client" />
<delete dir="${target.dir}/generated-sources/main/java/com/testapp/ws/client" />
<mkdir dir="${target.dir}/classes" />
<mkdir dir="${target.dir}/generated-sources/main/java" />
<taskdef name="wsimport" classname="com.sun.tools.ws.ant.WsImport">
<classpath path="/home/heffel/sges-v3/glassfish/modules/webservices-osgi.jar" />
<classpath path="/home/heffel/sges-v3/glassfish/modules/jaxb-osgi.jar" />
<classpath path="/home/heffel/sges-v3/glassfish/lib/javaee.jar" />
</taskdef>
<wsimport wsdl="http://localhost:8080/calculatorservice/CalculatorService?wsdl" destdir="${target.dir}/classes" verbose="true" keep="true" sourceDestDir="${target.dir}/generated-sources/main/java" xendorsed="true" />
</tasks>
<sourceRoot>${project.build.directory}/generated-sources/main/java</sourceRoot>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>net.ensode.glassfishbook.CalculatorServiceClient</mainClass>
<addClasspath>true</addClasspath>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
在pom.xml文件的<configuration>标签内部,对应于AntRun插件,我们放置任何需要执行的 ANT 任务。不出所料,我们示例中这个标签的正文几乎与刚刚讨论的 ANT 构建文件相同。
现在我们知道了如何使用 ANT 或 Maven 构建我们的代码,我们可以开发一个简单的客户端来访问我们的 Web 服务,使用以下代码:
package net.ensode.glassfishbook;
import javax.xml.ws.WebServiceRef;
public class CalculatorServiceClient {
@WebServiceRef(wsdlLocation = "http://localhost:8080/calculatorservice/CalculatorService?wsdl")
private static CalculatorService calculatorService;
public void calculate() {
Calculator calculator = calculatorService.getCalculatorPort();
System.out.println("1 + 2 = "
+ calculator.add(1, 2));
System.out.println("1 - 2 = "
+ calculator.subtract(1, 2));
}
public static void main(String[] args) {
new CalculatorServiceClient().calculate();
}
}
@WebServiceRef 注解将 Web 服务的实例注入到我们的客户端应用程序中。它的 wsdlLocation 属性包含我们正在调用的 Web 服务的 WSDL 的 URL。
注意,Web 服务类是名为 CalculatorService 的类的实例。这个类是在我们调用 wsimport 工具时创建的。wsimport 工具总是生成一个类,其名称是我们实现的类的名称加上 Service 后缀。我们使用这个服务类来获取我们开发的 Web 服务类的实例。在我们的例子中,我们通过在 CalculatorService 实例上调用 getCalculatorPort() 方法来实现这一点。一般来说,获取我们 Web 服务类实例的方法遵循 getNamePort() 的模式,其中 Name 是我们编写的实现 Web 服务的类的名称。一旦我们获取了 Web 服务类的实例,我们就可以像使用任何常规 Java 对象一样调用它的方法。
注意
严格来说,服务类的 getNamePort() 方法返回一个实现由 wsimport 生成的接口的类的实例。这个接口被赋予我们的 Web 服务类的名称,并声明了我们声明的所有作为 Web 服务的方法。对于所有实际用途,返回的对象与我们的 Web 服务类等效。
回顾我们之前的讨论,为了在独立客户端(未部署到 GlassFish)中使资源注入工作,我们需要通过 appclient 工具执行它。假设我们将客户端打包在一个名为 calculatorserviceclient.jar 的 .jar 文件中,执行命令如下:
appclient -client calculatorserviceclient.jar
在命令行中输入前面的命令后,我们应该在控制台上看到客户端的以下输出:
1 + 2 = 3
1 - 2 = -1
在这个例子中,我们传递了原始类型作为参数和返回值。当然,也可以将对象作为参数和返回值传递。不幸的是,并非所有标准 Java 类或原始类型都可以在调用 Web 服务时用作方法参数或返回值。这是因为幕后,方法参数和返回类型会被映射到 XML 定义,并且并非所有类型都可以正确映射。
可以在 JAX-WS Web 服务调用中使用的有效类型如下所示:
-
java.awt.Image -
java.lang.Object -
Java.lang.String -
java.math.BigDecimal -
java.math.BigInteger -
java.net.URI -
java.util.Calendar -
java.util.Date -
java.util.UUID -
javax.activation.DataHandler -
javax.xml.datatype.Duration -
javax.xml.datatype.XMLGregorianCalendar -
javax.xml.namespace.QName -
javax.xml.transform.Source
此外,以下原始类型也可以使用:
-
boolean -
byte -
byte[] -
double -
float -
int -
long -
short
我们还可以将我们自己的自定义类用作方法参数和/或返回值,但我们的类的成员变量必须是前面列表中列出的类型之一。
此外,数组可以作为方法参数或返回值使用,然而,在执行wsimport时,这些数组被转换为Lists,导致 Web 服务中的方法签名与客户端调用的方法调用之间产生不匹配。因此,更倾向于使用Lists作为方法参数和/或返回值,因为这同样是有效的,并且不会在客户端和服务器之间产生不匹配。
注意
JAX-WS 内部使用Java Architecture for XML Binding(JAXB)从方法调用创建 SOAP 消息。我们允许用于方法调用和返回值的类型是 JAXB 支持的类型。您可以在jaxb.dev.java.net/上获取有关 JAXB 的更多信息。
向 Web 服务发送附件
除了发送和接受前面各节中讨论的数据类型外,Web 服务方法还可以发送和接受文件附件。以下示例说明了如何做到这一点:
package net.ensode.glassfishbook;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.activation.DataHandler;
import javax.jws.WebMethod;
import javax.jws.WebService;
@WebService
public class FileAttachment {
@WebMethod
public void attachFile(DataHandler dataHandler) {
FileOutputStream fileOutputStream;
try {
// substitute "/tmp/attachment.gif" with
// a valid path, if necessary.
fileOutputStream = new FileOutputStream(
"/tmp/attachment.gif");
dataHandler.writeTo(fileOutputStream);
fileOutputStream.flush();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
为了编写一个接收一个或多个附件的 Web 服务方法,我们只需要为方法将接收的每个附件添加一个类型为javax.activation.DataHandler的参数。在前面的示例代码中,attachFile()方法接受一个此类参数并将其简单地写入文件系统。
就像任何标准 Web 服务一样,前面的代码需要打包成 WAR 文件并部署。一旦部署,WSDL 将自动生成。然后我们需要执行wsimport实用程序来生成我们的 Web 服务客户端可以用来访问 Web 服务的代码。正如之前讨论的,wsimport实用程序可以直接从命令行或通过自定义 ANT 目标调用。
一旦我们执行了wsimport来生成访问 Web 服务的代码,我们可以编写和编译我们的客户端代码如下:
package net.ensode.glassfishbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import javax.xml.ws.WebServiceRef;
public class FileAttachmentServiceClient {
@WebServiceRef(wsdlLocation = "http://localhost:8080/fileattachmentservice/"+ "FileAttachmentService?wsdl")
private static FileAttachmentService fileAttachmentService;
public static void main(String[] args) {
FileAttachment fileAttachment = fileAttachmentService.
getFileAttachmentPort();
File fileToAttach = new File("src/main/resources/logo.gif");
byte[] fileBytes = fileToByteArray(fileToAttach);
fileAttachment.attachFile(fileBytes);
System.out.println("Successfully sent attachment.");
}
static byte[] fileToByteArray(File file) {
byte[] fileBytes = null;
try {
FileInputStream fileInputStream;
fileInputStream = new FileInputStream(file);
FileChannel fileChannel = fileInputStream.getChannel();
fileBytes = new byte[(int) fileChannel.size()];
ByteBuffer byteBuffer = ByteBuffer.wrap(fileBytes);
fileChannel.read(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
return fileBytes;
}
}
需要向 Web 服务发送一个或多个附件的 Web 服务客户端首先像往常一样获取 Web 服务的实例。然后,它创建一个java.io.File实例,将附件文件的路径作为其构造函数的参数。
一旦我们有一个包含我们希望附加的文件的java.io.File实例,我们接下来需要将文件转换为字节数组,并将这个字节数组传递给期望附件的 Web 服务方法。
注意,当客户端调用期望附件的方法时使用的参数类型与在 Web 服务器代码中该方法使用的参数类型不同。Web 服务器代码中的方法期望每个附件都是一个 javax.activation.DataHandler 实例。然而,由 wsimport 生成的代码期望每个附件都是一个字节数组。这些字节数组在 wsimport 生成的代码背后被转换为正确的类型(javax.activation.DataHandler)。作为应用程序开发者,我们不需要关心为什么会出现这种情况的细节。我们只需要记住,当向 Web 服务方法发送附件时,Web 服务代码和客户端调用中的参数类型将不同。
将 EJB 作为 Web 服务公开
除了在上一节中描述的创建 Web 服务之外,无状态会话 Bean 的公共方法可以很容易地公开为 Web 服务。以下示例说明了如何做到这一点:
package net.ensode.glassfishbook;
import javax.ejb.Stateless;
import javax.jws.WebService;
@Stateless
@WebService
public class DecToHexBean {
public String convertDecToHex(int i) {
return Integer.toHexString(i);
}
}
正如我们所见,要公开无状态会话 Bean 的公共方法,我们只需要用 @WebService 注解装饰其类声明。由于该类是一个无状态会话 Bean,它还需要用 @Stateless 注解进行装饰。
正如常规无状态会话 Bean 一样,那些方法被公开为 Web 服务的需要部署在一个 .jar 文件中。一旦部署,我们可以在 GlassFish 管理员 Web 控制台的 应用程序 节点下看到新的 Web 服务。点击应用程序节点,我们可以在 GlassFish 控制台中看到一些详细信息,如下面的屏幕截图所示:

注意,我们新 Web 服务的 类型 列表中的值是 StatelessSessionBean。这使我们能够一眼看出该 Web 服务是以 企业 JavaBean(EJB)的形式实现的。
正如标准 Web 服务一样,EJB Web 服务在部署时自动生成一个 WSDL,供其客户端使用;可以通过点击 查看端点 链接来访问它。
EJB Web 服务客户端
以下类说明了从客户端应用程序访问 EJB Web 服务方法的步骤:
package net.ensode.glassfishbook;
import javax.xml.ws.WebServiceRef;
public class DecToHexClient {
@WebServiceRef(wsdlLocation = "http://localhost:8080/DecToHexBeanService/DecToHexBean?wsdl")
private static DecToHexBeanService decToHexBeanService;
public void convert() {
DecToHexBean decToHexBean = decToHexBeanService.getDecToHexBeanPort();
System.out.println("decimal 4013 in hex is: "
+ decToHexBean.convertDecToHex(4013));
}
public static void main(String[] args) {
new DecToHexClient().convert();
}
}
正如我们所见,当从客户端访问 EJB Web 服务时,不需要做任何特殊的事情。过程与标准 Web 服务相同。
由于前面的示例是一个独立的应用程序,它需要通过以下方式通过 appclient 应用程序执行:
appclient -client ejbwsclient.jar
前一个命令的结果如下:
decimal 4013 in hex is: fad
保护 Web 服务
正如常规 Web 应用程序一样,Web 服务可以被安全地设置,以便只有授权用户才能访问它们。这可以通过修改 Web 服务的 web.xml 部署描述符来实现,如下面的代码所示:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<security-constraint>
<web-resource-collection>
<web-resource-name>Calculator Web Service</web-resource-name>
<url-pattern>/CalculatorService/*</url-pattern>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>file</realm-name>
</login-config>
</web-app>
在这个例子中,我们修改了我们的计算器服务,使其只能被授权用户访问。请注意,为了保护 Web 服务所需的修改与保护任何常规 Web 应用程序所需的修改没有区别。用于 <url-pattern> 元素的 URL 模式可以通过点击对应于我们服务的 查看 WSDL 链接来获取。在我们的例子中,链接的 URL 是:
http://localhost:8080/calculatorservice/CalculatorService?wsdl
用于 <url-pattern> 的值是紧接在上下文根(在我们的例子中是 /CalculatorService)之后,并在问号之前,后面跟着一个斜杠和一个星号。
注意
注意,前面的 web.xml 部署描述符仅保护 HTTP POST 请求。这样做的原因是 wsimport 使用 GET 请求获取 WSDL 并生成相应的代码。如果 GET 请求受到保护,wsimport 将会失败,因为它将无法访问 WSDL。
以下代码演示了一个独立客户端如何访问受保护的 Web 服务:
package net.ensode.glassfishbook;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.WebServiceRef;
public class CalculatorServiceClient {
@WebServiceRef(
wsdlLocation = "http://localhost:8080/securecalculatorservice/CalculatorService?wsdl")
private static CalculatorService calculatorService;
public void calculate() {
//add a user named "joe" with a password of "password"
//to the file realm to successfuly execute the web service.
//"joe" must belong to the group "appuser".
Calculator calculator = calculatorService.getCalculatorPort();
((BindingProvider) calculator).getRequestContext().put(
BindingProvider.USERNAME_PROPERTY, "joe");
((BindingProvider) calculator).getRequestContext().put(
BindingProvider.PASSWORD_PROPERTY, "password");
System.out.println("1 + 2 = " + calculator.add(1, 2));
System.out.println("1 - 2 = " + calculator.subtract(1, 2));
}
public static void main(String[] args) {
new CalculatorServiceClient().calculate();
}
}
前面的代码是我们在本章前面看到的 Calculator 服务独立客户端的修改版本。这个版本被修改为访问服务的安全版本。从代码中可以看出,要访问受保护的服务版本,我们只需要在请求上下文中放入一个用户名和一个密码。用户名和密码必须是对用于验证 Web 服务的域有效的。
我们可以通过将我们的 Web 服务端点类转换为 javax.xml.ws.BindingProvider 并调用其 getRequestContext() 方法来将用户名和密码添加到请求上下文中。此方法返回一个 java.util.Map 实例。然后我们可以简单地通过调用 Map 的 put 方法,并使用在 BindingProvider 中定义的常量 USERNAME_PROPERTY 和 PASSWORD_PROPERTY 作为键,相应的 String 对象作为值来添加用户名和密码。
保护 EJB Web 服务
就像标准 Web 服务一样,作为 Web 服务公开的 EJB 可以被保护,以便只有授权客户端可以访问它们。这可以通过通过以下方式配置 EJB 实现:通过 glassfish-ejb-jar.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-ejb-jar PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 EJB 3.1//EN" "http://glassfish.org/dtds/glassfish-ejb-jar_3_1-1.dtd">
<glassfish-ejb-jar>
<ejb>
<ejb-name>SecureDecToHexBean</ejb-name>
<webservice-endpoint>
<port-component-name>
SecureDecToHexBean
</port-component-name>
<login-config>
<auth-method>BASIC</auth-method>
<realm>file</realm>
</login-config>
</webservice-endpoint>
</ejb>
</glassfish-ejb-jar>
如前所述的部署描述符所示,将 EJB 作为 Web 服务公开与使用标准 EJB 相比,其安全设置不同。对于作为 Web 服务公开的 EJB,安全配置是在 glassfish-ejb-jar.xml 文件的 <webservice-endpoint> 元素内部完成的。
<port-component-name> 元素必须设置为我们要公开为 Web 服务的 EJB 的名称。此名称在 EJB 的 <ejb-name> 元素中定义。
<login-config> 元素与网络应用程序的 web.xml 部署描述符中相应的元素非常相似。<login-config> 元素必须包含一个授权方法,由其 <auth-method> 子元素定义,以及用于身份验证的领域。领域由 <realm> 子元素定义。
注意
不要为打算公开为网络服务的 EJB 使用 @RolesAllowed 注解。此注解旨在 EJB 方法通过其远程或本地接口访问时使用。如果一个 EJB 或其一个或多个方法被此注解装饰,那么调用该方法将因安全异常而失败。
一旦我们为身份验证配置了 EJB 网络服务,我们就将其打包成 .jar 文件,然后像往常一样部署它。现在,EJB 网络服务已经准备好供客户端访问了。
以下代码示例说明了 EJB 网络服务客户端如何访问安全的 EJB 网络服务:
package net.ensode.glassfishbook;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.WebServiceRef;
public class DecToHexClient {
@WebServiceRef(
wsdlLocation = "http://localhost:8080/SecureDecToHexBeanService/SecureDecToHexBean?wsdl")
private static SecureDecToHexBeanService secureDecToHexBeanService;
public void convert() {
SecureDecToHexBean secureDecToHexBean = secureDecToHexBeanService.
getSecureDecToHexBeanPort();
((BindingProvider) secureDecToHexBean).getRequestContext().put(
BindingProvider.USERNAME_PROPERTY, "joe");
((BindingProvider) secureDecToHexBean).getRequestContext().put(
BindingProvider.PASSWORD_PROPERTY, "password");
System.out.println("decimal 4013 in hex is: "
+ secureDecToHexBean.convertDecToHex(4013));
}
public static void main(String[] args) {
new DecToHexClient().convert();
}
}
如前例所示,访问作为网络服务公开的 EJB 的过程与访问标准网络服务相同。网络服务的实现与客户端无关。
摘要
在本章中,我们介绍了如何通过 JAX-WS API 开发网络服务和网络服务客户端。我们解释了在使用 ANT 或 Maven 作为构建工具时,如何将网络服务客户端的代码生成集成到网络服务中。我们还涵盖了可以通过 JAX-WS 进行远程方法调用的有效类型。此外,我们还讨论了如何向网络服务发送附件。我们还介绍了如何将 EJB 方法公开为网络服务。最后,我们介绍了如何确保网络服务不被未经授权的客户端访问。
在下一章中,我们将介绍使用 JAX-RS 的 RESTful 网络服务。
第十一章:使用 JAX-RS 开发 RESTful Web 服务
表征状态转移(REST)是一种架构风格,其中将网络服务视为资源,并且可以通过统一资源标识符(URIs)进行识别。
使用 REST 开发的 Web 服务被称为 RESTful Web 服务。
Java EE 6 通过添加 Java API for RESTful Web Services (JAX-RS) 的支持来支持 RESTful 网络服务。JAX-RS 之前已经作为一个独立的 API 存在了一段时间,在规范的第 6 版中它成为了 Java EE 的一部分。在本章中,我们将介绍如何通过 JAX-RS.API 开发 RESTful 网络服务。
本章将涵盖以下主题:
-
RESTful Web 服务与 JAX-RS 简介
-
开发一个简单的 RESTful 网络服务
-
开发 RESTful 网络服务客户端
-
路径参数
-
查询参数
介绍 RESTful 网络服务和 JAX-RS
RESTful 网络服务非常灵活。RESTful 网络服务可以消费多种不同的 MIME 类型,尽管它们通常被编写为消费和/或生成 XML 或 JSON(JavaScript 对象表示法)。
Web 服务必须支持以下四种 HTTP 方法之一或多个:
-
GET:按照惯例,GET请求用于检索现有资源 -
POST:按照惯例,POST请求用于更新现有资源 -
PUT:按照惯例,PUT请求用于创建一个新的资源 -
DELETE:按照惯例,DELETE请求用于删除现有的资源
我们通过创建一个带有注解方法的类来开发一个使用 JAX-RS 的 RESTful 网络服务,这些方法会在我们的网络服务接收到上述 HTTP 请求方法之一时被调用。一旦我们开发和部署了我们的 RESTful 网络服务,我们就需要开发一个客户端来向我们的服务发送请求。JAX-RS 2.0 引入了一个标准的客户端 API,我们可以使用它来开发 RESTful 网络服务客户端。
开发一个简单的 RESTful 网络服务
使用 JAX-RS 开发 RESTful 网络服务既简单又直接。我们每个 RESTful 网络服务都需要通过其唯一的资源标识符(URI)来调用。这个 URI 由 @Path 注解指定,我们需要使用它来装饰我们的 RESTful 网络服务资源类。
在开发 RESTful 网络服务时,我们需要开发当我们的网络服务接收到 HTTP 请求时将被调用的方法。我们需要实现处理 RESTful 网络服务所处理的四种请求类型之一或多个的方法:GET、POST、PUT和/或DELETE。
JAX-RS API 提供了四个我们可以用来装饰我们 Web 服务中方法的注解;这些注解的命名非常恰当,分别是 @GET、@POST、@PUT 和 @DELETE。在我们的 Web 服务中使用这些注解之一来装饰方法,将使其能够响应相应的 HTTP 方法。
此外,我们服务中的每个方法都必须生成和/或消耗一个特定的 MIME 类型。
小贴士
多用途互联网邮件扩展(MIME)是在互联网上传输非 ASCII 文本的标准。MIME 最初是为了通过电子邮件发送非文本数据而开发的,但后来其用途扩展到了包括其他形式的数据传输,如 RESTful Web 服务。
需要通过@Produces注解指定将要生成的 MIME 类型;同样,将要消耗的 MIME 类型必须通过@Consumes注解指定。
小贴士
请注意,这个示例实际上并没有做任何事情;它的目的是说明如何在我们的 RESTful Web 服务资源类中使不同的方法响应不同的 HTTP 方法。
以下示例说明了我们刚刚解释的概念:
package com.ensode.jaxrsintro.service;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@Path("customer")
public class CustomerResource {
@GET
@Produces("text/xml")
public String getCustomer() {
//in a "real" RESTful service, we would retrieve data from a database
//then return an XML representation of the data.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".getCustomer() invoked");
return "<customer>\n"
+ "<id>123</id>\n"
+ "<firstName>Joseph</firstName>\n"
+ "<middleName>William</middleName>\n"
+ "<lastName>Graystone</lastName>\n"
+ "</customer>\n";
}
/**
* Create a new customer
* @param customer XML representation of the customer to create
*/
@PUT
@Consumes("text/xml")
public void createCustomer(String customerXML) {
//in a "real" RESTful service, we would parse the XML
//received in the customer XML parameter, then insert
//a new row into the database.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".createCustomer() invoked");
System.out.println("customerXML = " + customerXML);
}
@POST
@Consumes("text/xml")
public void updateCustomer(String customerXML) {
//in a "real" RESTful service, we would parse the XML
//received in the customer XML parameter, then update
//a row in the database.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".updateCustomer() invoked");
System.out.println("customerXML = " + customerXML);
}
@DELETE
@Consumes("text/xml")
public void deleteCustomer(String customerXML) {
//in a "real" RESTful service, we would parse the XML
//received in the customer XML parameter, then delete
//a row in the database.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".deleteCustomer() invoked");
System.out.println("customerXML = " + customerXML);
}
}
注意,这个类被@Path注解标注;这个注解指定了我们的 RESTful Web 服务的统一资源标识符(URI)。我们服务的完整 URI 将包括协议、服务器名称、端口、上下文根、REST 资源路径(见下一小节)以及传递给此注解的值。
假设我们的 Web 服务已部署到名为example.com的服务器上,使用 HTTP 协议在 8080 端口,上下文根为"jaxrsintro",REST 资源路径为resources,那么我们服务的完整 URI 将是http://example.com:8080/jaxrsintro/resources/customer。
小贴士
由于 Web 浏览器在指向 URL 时会生成 GET 请求,我们可以通过将浏览器指向我们服务的 URI 来测试服务的 GET 方法。
注意,我们类中的每个方法都被@GET、@POST、@PUT或@DELETE注解之一标注。这些注解使我们的方法能够响应它们对应的 HTTP 方法。
此外,如果我们的方法需要向客户端返回数据,我们声明数据的 MIME 类型应该通过@Produces注解返回。在我们的示例中,只有getCustomer()方法向客户端返回数据;我们希望以 XML 格式返回数据,因此,我们将@Produces注解的值设置为text/xml。同样,如果我们的方法需要从客户端消耗数据,我们需要指定要消耗的数据的 MIME 类型;这是通过@Consumes注解完成的。我们服务中的所有方法(除了getCustomer())都消耗数据;在所有情况下,我们期望数据以 XML 格式,因此,我们再次指定text/xml作为要消耗的 MIME 类型。
配置应用程序的 REST 资源路径
如前一小节简要提到的,在成功部署使用 JAX-RS 开发的 RESTful Web 服务之前,我们需要为我们的应用程序配置 REST 资源路径。我们可以通过开发一个扩展javax.ws.rs.core.Application的类并使用@ApplicationPath注解来装饰它来实现这一点。
通过@ApplicationPath 注解进行配置
如前几章所述,Java EE 6 向 Java EE 规范添加了几个新功能,因此在许多情况下,编写 web.xml 部署描述符是不必要的。JAX-RS 也不例外。我们可以通过注解在 Java 代码中配置 REST 资源路径。
要配置我们的 REST 资源路径,而无需依赖于 web.xml 部署描述符,我们只需编写一个扩展 javax.ws.ApplicationPath 的类,并用 @ApplicationPath 注解装饰它;传递给此注解的值是我们服务的 REST 资源路径。
以下代码示例说明了这个过程:
package com.ensode.jaxrsintro.service.config;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("resources")
public class JaxRsConfig extends Application {
}
注意,该类不需要实现任何方法。它只需扩展 javax.ws.rs.Application 并用 @ApplicationPath 注解装饰。该类必须是公共的,可以具有任何名称,并且可以放在任何包中。
测试我们的网络服务
正如我们之前提到的,网络浏览器会向它们指向的任何 URL 发送 GET 请求;因此,测试我们对服务发送的 GET 请求的最简单方法就是将浏览器指向我们的服务 URI,如下面的截图所示:

网络浏览器仅支持 GET 和 POST 请求。要通过浏览器测试 POST 请求,我们必须编写一个包含具有我们的服务 URI 的 action 属性值的 HTML 表单的 Web 应用程序。虽然对于单个服务来说这很简单,但对我们开发的每个 RESTful 网络服务都这样做可能会变得繁琐。
幸运的是,有一个名为 curl 的开源命令行实用工具,我们可以用它来测试我们的网络服务。curl 命令包含在大多数 Linux 发行版中,并且可以轻松地下载到 Windows、Mac OS X 和其他几个平台。您可以在 curl.haxx.se/ 下载 curl 实用工具。
curl 可以向我们的服务发送所有四种请求方法(GET、POST、PUT 和 DELETE)。我们的服务器响应将简单地显示在命令行控制台上。curl 使用 -X 命令行选项,允许我们指定要发送的请求方法;要发送 GET 请求,我们只需在命令行中输入以下内容:
curl -XGET http://localhost:8080/jaxrsintro/resources/customer
这导致以下输出:
<customer>
<id>123</id>
<firstName>Joseph</firstName>
<middleName>William</middleName>
<lastName>Graystone</lastName>
</customer>
这,不出所料,是我们指向我们的服务 URI 时看到的相同输出。
curl 的默认请求方法是 GET,因此,我们之前示例中的 -X 参数是多余的;我们可以通过从命令行调用以下命令来达到相同的结果:
curl HYPERLINK "http://localhost:8080/jaxrsintro/resources/customer"http://localhost:8080/jaxrsintro/resources/customer
在提交任何前两个命令并检查 GlassFish 日志后,我们应该看到我们添加到 getCustomer() 方法的 System.out.println() 语句的输出。
INFO: --- com.ensode.jaxrsintro.service.CustomerResource.getCustomer() invoked
对于所有其他请求方法类型,我们需要向我们的服务发送一些数据。这可以通过 curl 命令的 --data 命令行参数来实现,如下面的代码所示:
curl -XPUT -HContent-type:text/xml --data "<customer><id>321</id><firstName>Amanda</firstName><middleName>Zoe</middleName><lastName>Adams</lastName></customer>" http://localhost:8080/jaxrsintro/resources/customer
如此示例所示,我们需要通过 curl 的-H命令行参数指定 MIME 类型,格式如示例所示。
我们可以通过检查 GlassFish 日志来验证之前的命令是否按预期工作,如下面的代码所示:
INFO: --- com.ensode.jaxrsintro.service.CustomerResource.createCustomer() invoked
INFO: customerXML = <customer><id>321</id><firstName>Amanda</firstName><middleName>Zoe</middleName><lastName>Adams</lastName></customer>
我们可以通过执行以下代码轻松测试其他请求方法类型:
curl -XPOST -HContent-type:text/xml --data "<customer><id>321</id><firstName>Amanda</firstName><middleName>Tamara</middleName><lastName>Adams</lastName></customer>" http://localhost:8080/jaxrsintro/resources/customer
GlassFish 日志显示了相应的输出:
INFO: --- com.ensode.jaxrsintro.service.CustomerResource.updateCustomer() invoked
INFO: customerXML = <customer><id>321</id><firstName>Amanda</firstName><middleName>Tamara</middleName><lastName>Adams</lastName></customer>
我们可以通过执行以下命令来测试delete方法:
curl -XDELETE -HContent-type:text/xml --data "<customer><id>321</id><firstName>Amanda</firstName><middleName>Tamara</middleName><lastName>Adams</lastName></customer>" http://localhost:8080/jaxrsintro/resources/customer
再次,GlassFish 日志显示了相应的输出:
INFO: --- com.ensode.jaxrsintro.service.CustomerResource.deleteCustomer() invoked
INFO: customerXML = <customer><id>321</id><firstName>Amanda</firstName><middleName>Tamara</middleName><lastName>Adams</lastName></customer>
使用 JAXB 在 Java 和 XML 之间转换数据
在我们之前的例子中,我们处理了原始的 XML 数据。在实际应用中,我们更有可能解析从客户端接收到的 XML 数据,并使用它来填充 Java 对象。此外,我们需要返回给客户端的任何 XML 数据都必须从 Java 对象构建。
将数据从 Java 转换为 XML 以及反向转换是一个如此常见的用例,以至于 Java EE 规范提供了一个 API 来完成它。这个 API 是Java API for XML Binding(JAXB)。
JAXB 使得将数据从 Java 转换为 XML 变得透明且简单。我们只需要用@XmlRootElement注解装饰我们希望转换为 XML 的类。以下代码示例说明了如何做到这一点:
package com.ensode.jaxrstest.entity;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Customer implements Serializable {
private Long id;
private String firstName;
private String middleName;
private String lastName;
public Customer() {
}
public Customer(Long id, String firstName,
String middleInitial, String lastName) {
this.id = id;
this.firstName = firstName;
this.middleName = middleInitial;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getMiddleName() {
return middleName;
}
public void setMiddleName(String middleName) {
this.middleName = middleName;
}
@Override
public String toString() {
return "id = " + getId() + "\nfirstName = " + getFirstName()
+ "\nmiddleName = " + getMiddleName() + "\nlastName = "
+ getLastName();
}
}
如此示例所示,除了类级别的@XmlRootElement注解外,上述 Java 类没有其他不寻常之处。
一旦我们使用@XmlRootElement注解装饰了一个类,我们需要将我们的 Web 服务的参数类型从 String 更改为我们的自定义类,如下面的代码所示:
package com.ensode.jaxbxmlconversion.service;
import com.ensode.jaxbxmlconversion.entity.Customer;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@Path("customer")
public class CustomerResource {
private Customer customer;
public CustomerResource() {
//"fake" the data, in a real application the data
//would come from a database.
customer = new Customer(1L, "David",
"Raymond", "Heffelfinger");
}
@GET
@Produces("text/xml")
public Customer getCustomer() {
//in a "real" RESTful service, we would retrieve data from a database
//then return an XML representation of the data.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".getCustomer() invoked");
return customer;
}
@POST
@Consumes("text/xml")
public void updateCustomer(Customer customer) {
//in a "real" RESTful service, JAXB would parse the XML
//received in the customer XML parameter, then update
//a row in the database.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".updateCustomer() invoked");
System.out.println("---- got the following customer: "
+ customer);
}
@PUT
@Consumes("text/xml")
public void createCustomer(Customer customer) {
//in a "real" RESTful service, we would insert
//a new row into the database with the data in the
//customer parameter
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".createCustomer() invoked");
System.out.println("customer = " + customer);
}
@DELETE
@Consumes("text/xml")
public void deleteCustomer(Customer customer) {
//in a "real" RESTful service, we would delete a row
//from the database corresponding to the customer parameter
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".deleteCustomer() invoked");
System.out.println("customer = " + customer);
}
}
如我们所见,我们这个版本的 RESTful Web 服务与之前的版本之间的区别在于,所有参数类型和返回值都已从String更改为Customer。JAXB 负责将我们的参数和返回类型适当地转换为 XML。当使用 JAXB 时,我们的自定义类对象会自动用客户端发送的 XML 数据填充,返回值也会类似地透明地转换为 XML。
开发 RESTful Web 服务客户端
虽然curl允许我们快速测试 RESTful Web 服务,并且是一个开发者友好的工具,但它并不完全用户友好;我们不应该期望用户在命令行中输入curl命令来使用我们的 Web 服务。因此,我们需要为我们的服务开发一个客户端。JAX-RS 2.0 引入了一个标准客户端 API,我们可以使用它轻松地开发 RESTful Web 服务客户端。
以下示例说明了如何使用 JAX-RS 客户端 API:
package com.ensode.jaxrsintroclient;
import com.ensode.jaxbxmlconversion.entity.Customer;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
public class App {
public static void main(String[] args) {
App app = new App();
app.insertCustomer();
}
public void insertCustomer() {
Customer customer = new Customer(234L, "Tamara", "A",
"Graystone");
Client client = ClientBuilder.newClient();
client.target(
"http://localhost:8080/jaxbxmlconversion/resources/customer").
request().put(
Entity.entity(customer, "text/xml"),
Customer.class);
}
}
我们需要做的第一件事是通过在javax.ws.rs.client.ClientBuilder类上调用静态newClient()方法来创建javax.ws.rs.client.Client的一个实例。
然后,我们在我们的Client实例上调用target()方法,将我们的 RESTful 网络服务的 URI 作为参数传递。target()方法返回实现javax.ws.rs.client.WebTarget接口的类的实例。
在这一点上,我们在我们的WebTarget实例上调用request()方法;此方法返回javax.ws.rs.client.Invocation.Builder接口的实现。
在这个特定的例子中,我们正在向我们的 RESTful 网络服务发送一个HTTP PUT请求;因此,在这个时候,我们调用我们的Invocation.Builder实现中的put()方法。put()方法的第一参数是javax.ws.rs.client.Entity的实例。我们可以通过在Entity类上调用静态entity()方法即时创建javax.ws.rs.client.Entity的实例。此方法的第一参数是我们希望传递给我们的 RESTful 网络服务的对象,第二个参数是我们将传递给 RESTful 网络服务的数据的 MIME 类型的字符串表示。put()方法的第二个参数是客户端期望从服务中获得的响应类型。在调用put()方法后,向我们的 RESTful 网络服务发送一个HTTP PUT请求,并调用我们用@Put注解装饰的方法(在我们的例子中是createCustomer())。我们还可以调用类似的get()、post()和delete()方法来向我们的 RESTful 网络服务发送相应的 HTTP 请求。
处理查询和路径参数
在我们之前的示例中,我们一直在使用一个 RESTful 网络服务来管理单个customer对象。在现实生活中,这显然不会很有帮助。常见的情况是开发一个 RESTful 网络服务来处理一组对象(在我们的例子中是客户)。为了确定我们正在处理集合中的哪个特定对象,我们可以向我们的 RESTful 网络服务传递参数。我们可以使用两种类型的参数:查询和路径。
查询参数
我们可以向将处理我们的网络服务中的 HTTP 请求的方法添加参数。带有@QueryParam注解的参数将从请求 URL 中检索。
以下示例说明了如何在我们的 JAX-RS RESTful 网络服务中使用查询参数:
package com.ensode.queryparams.service;
import com.ensode.queryparams.entity.Customer;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
@Path("customer")
public class CustomerResource {
private Customer customer;
public CustomerResource() {
customer = new Customer(1L, "Samuel",
"Joseph", "Willow");
}
@GET
@Produces("text/xml")
public Customer getCustomer(@QueryParam("id") Long id) {
//in a "real" RESTful service, we would retrieve data from a database
//using the supplied id.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".getCustomer() invoked, id = " + id);
return customer;
}
/**
* Create a new customer
* @param customer XML representation of the customer to create
*/
@PUT
@Consumes("text/xml")
public void createCustomer(Customer customer) {
//in a "real" RESTful service, we would parse the XML
//received in the customer XML parameter, then insert
//a new row into the database.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".createCustomer() invoked");
System.out.println("customer = " + customer);
}
@POST
@Consumes("text/xml")
public void updateCustomer(Customer customer) {
//in a "real" RESTful service, we would parse the XML
//received in the customer XML parameter, then update
//a row in the database.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".updateCustomer() invoked");
System.out.println("customer = " + customer);
System.out.println("customer= " + customer);
}
@DELETE
@Consumes("text/xml")
public void deleteCustomer(@QueryParam("id") Long id) {
//in a "real" RESTful service, we would invoke
//a DAO and delete the row in the database with the
//primary key passed as the "id" parameter.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".deleteCustomer() invoked, id = " + id);
System.out.println("customer = " + customer);
}
}
注意,我们唯一需要做的就是用@QueryParam注解装饰参数。此注解允许 JAX-RS 检索与注解值匹配的任何查询参数,并将其值分配给参数变量。
我们可以向网络服务的 URL 添加一个参数,就像我们向任何 URL 传递参数一样:
curl -XGET -HContent-type:text/xml http://localhost:8080/queryparams/resources/customer?id=1
通过 JAX-RS 客户端 API 发送查询参数
JAX-RS 客户端 API 提供了一个简单直接的方法来向 RESTful 网络服务发送查询参数。以下示例说明了如何做到这一点:
package com.ensode.queryparamsclient;
import com.ensode.queryparamsclient.entity.Customer;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
public class App {
public static void main(String[] args) {
App app = new App();
app.getCustomer();
}
public void getCustomer() {
Client client = ClientBuilder.newClient();
Customer customer = client.target(
"http://localhost:8080/queryparams/resources/customer").
queryParam("id", 1L).
request().get(Customer.class);
System.out.println("Received the following customer information:");
System.out.println("Id: " + customer.getId());
System.out.println("First Name: " + customer.getFirstName());
System.out.println("Middle Name: " + customer.getMiddleName());
System.out.println("Last Name: " + customer.getLastName());
}
}
如我们所见,我们只需调用我们的Client实例上target()方法返回的javax.ws.rs.client.WebTarget实例上的queryParam()方法,就可以传递参数。此方法的第一参数是参数名称,它必须与 Web 服务中@QueryParam注解的值匹配。第二个参数是我们需要传递给 Web 服务的值。如果我们的 Web 服务接受多个参数,我们可以通过链式调用queryParam()方法,为我们的 RESTful Web 服务期望的每个参数使用一个。
路径参数
我们向 RESTful Web 服务传递参数的另一种方式是通过路径参数。以下示例说明了如何开发一个接受路径参数的 JAX-RS RESTful Web 服务:
package com.ensode.pathparams.service;
import com.ensode.pathparams.entity.Customer;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
@Path("/customer/")
public class CustomerResource {
private Customer customer;
public CustomerResource() {
customer = new Customer(1L, "William",
"Daniel", "Graystone");
}
@GET
@Produces("text/xml")
@Path("{id}/")
public Customer getCustomer(@PathParam("id") Long id) {
//in a "real" RESTful service, we would retrieve data from a database
//using the supplied id.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".getCustomer() invoked, id = " + id);
return customer;
}
@PUT
@Consumes("text/xml")
public void createCustomer(Customer customer) {
//in a "real" RESTful service, we would parse the XML
//received in the customer XML parameter, then insert
//a new row into the database.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".createCustomer() invoked");
System.out.println("customer = " + customer);
}
@POST
@Consumes("text/xml")
public void updateCustomer(Customer customer) {
//in a "real" RESTful service, we would parse the XML
//received in the customer XML parameter, then update
//a row in the database.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".updateCustomer() invoked");
System.out.println("customer = " + customer);
System.out.println("customer= " + customer);
}
@DELETE
@Consumes("text/xml")
@Path("{id}/")
public void deleteCustomer(@PathParam("id") Long id) {
//in a "real" RESTful service, we would invoke
//a DAO and delete the row in the database with the
//primary key passed as the "id" parameter.
System.out.println("--- " + this.getClass().getCanonicalName()
+ ".deleteCustomer() invoked, id = " + id);
System.out.println("customer = " + customer);
}
}
任何接受路径参数的方法都必须用@Path注解装饰。此注解的value属性必须格式化为"{paramName}/",其中paramName是方法期望接收的参数。此外,方法参数必须用@PathParam注解装饰。@PathParam注解的值必须与方法@Path注解中声明的参数名称匹配。
我们可以通过调整我们的 Web 服务的 URI 来从命令行传递路径参数;例如,要将1的"id"参数传递给getCustomer()方法(该方法处理HTTP GET请求),我们可以从命令行如下操作:
curl -XGET -HContent-type:text/xml http://localhost:8080/pathparams/resources/customer/1
这将返回getCustomer()方法返回的Customer对象的 XML 表示形式的预期输出,如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><customer><firstName>William</firstName><id>1</id><lastName>Graystone</lastName><middleName>Daniel</middleName></customer>
通过 JAX-RS 客户端 API 发送路径参数
通过 JAX-RS 客户端 API 向 Web 服务发送路径参数既简单又直接;我们只需添加几个方法调用,以指定路径参数及其值。以下示例说明了如何进行此操作:
package com.ensode.pathparamsclient;
import com.ensode.pathparamsclient.entity.Customer;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
public class App {
public static void main(String[] args) {
App app = new App();
app.getCustomer();
}
public void getCustomer() {
Client client = ClientBuilder.newClient();
Customer customer = client.target(
http://localhost:8080/pathparams/resources/customer").
path("{id}").
resolveTemplate("id", 1L).
request().get(Customer.class);
System.out.println("Received the following customer information:");
System.out.println("Id: " + customer.getId());
System.out.println("First Name: " + customer.getFirstName());
System.out.println("Middle Name: " + customer.getMiddleName());
System.out.println("Last Name: " + customer.getLastName());
}
}
在此示例中,我们调用client.target()返回的WebTarget实例上的path()方法。此方法将指定的路径追加到我们的WebTarget实例;此方法的价值必须与我们的 RESTful Web 服务中@Path注解的值匹配。
在我们的WebTarget实例上调用path()方法后,我们需要调用resolveTemplate();此方法的第一参数是参数的名称(不带大括号),第二个参数是我们希望作为参数传递给我们的 RESTful Web 服务的值。
如果我们需要向我们的 Web 服务中的一个传递多个参数,我们只需在方法级别的@Path参数中使用以下格式:
@Path("/{paramName1}/{paramName2}/")
然后使用以下方式对相应的方法参数进行@PathParam注解:
public String someMethod(@PathParam("paramName1") String param1,
@PathParam("paramName2") String param2)
然后,可以通过修改 Web 服务的 URI 来调用 Web 服务,将参数按@Path注解中指定的顺序传递。例如,以下 URI 将传递paramName1和paramName2的值1和2:
http://localhost:8080/contextroot/resources/customer/1/2
之前的 URI 无论是从命令行还是通过我们用 JAX-RS 客户端 API 开发的网络服务客户端都可以使用。
摘要
在本章中,我们讨论了如何使用 JAX-RS 开发 RESTful 网络服务,这是 Java EE 规范的新增内容。
我们介绍了如何通过在我们的代码中添加一些简单的注解来开发 RESTful 网络服务。我们还解释了如何利用 Java API for XML Binding (JAXB) 自动在 Java 和 XML 之间转换数据。
我们还讨论了如何通过 JAX-RS 客户端 API 开发 RESTful 网络服务客户端。
最后,我们介绍了如何通过 @PathParam 和 @QueryParam 注解将参数传递给我们的 RESTful 网络服务。



浙公网安备 33010602011771号