JavaEE8-应用开发-全-

JavaEE8 应用开发(全)

原文:zh.annas-archive.org/md5/b55dc4e93eb6e00274e03326ae09f441

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Java 企业版 8,Java EE 规范的最新版本,在该规范中添加了几个新特性。几个现有的 Java EE API 在本规范版本中得到了重大改进,并且一些全新的 API 被添加到 Java EE 中。本书涵盖了最流行的 Java EE 规范的最新版本,包括 JavaServer Faces(JSF)、Java 持久化 API(JPA)、企业 JavaBeans(EJB)、上下文和依赖注入(CDI)、Java 处理 JSON 的 API(JSON-P)、新的 Java JSON 绑定 API(JSON-B)、Java WebSocket API、Java 消息服务(JMS)API 2.0、Java XML Web 服务 API(JAX-WS)和 Java RESTful Web 服务 API(JAX-RS)。它还涵盖了通过全新的 Java EE 8 安全 API 保护 Java EE 应用程序。

本书涵盖内容

第一章,Java EE 简介,简要介绍了 Java EE,解释了它是如何作为一个社区努力开发的。它还澄清了一些关于 Java EE 的常见误解。

第二章,JavaServer Faces,涵盖了使用 JSF 开发 Web 应用程序,包括 HTML5 友好标记和 Faces Flows 等功能。

第三章,使用 JPA 进行对象关系映射,讨论了如何通过 Java 持久化 API(JPA)与关系数据库管理系统(RDBMS)如 Oracle 或 MySQL 交互。

第四章,企业 JavaBeans,解释了如何使用会话和消息驱动 bean 开发应用程序。涵盖了主要 EJB 功能,如事务管理、EJB 定时服务和安全性。

第五章,上下文和依赖注入,讨论了 CDI 命名的 bean、使用 CDI 的依赖注入和 CDI 限定符,以及 CDI 事件功能。

第六章,使用 JSON-B 和 JSON-P 进行 JSON 处理,解释了如何使用 JSON-P API 和新的 JSON-B API 生成和解析 JavaScript 对象表示法(JSON)数据。

第七章,WebSocket,解释了如何开发基于浏览器和服务器之间全双工通信的 Web 应用程序,而不是依赖于传统的 HTTP 请求/响应周期。

第八章,Java 消息服务,讨论了如何使用完全重写的 JMS 2.0 API 开发消息应用程序。

第九章,保护 Java EE 应用程序,涵盖了如何通过新的 Java EE 8 安全 API 保护 Java EE 应用程序。

第十章,使用 JAX-RS 开发 RESTful Web 服务,讨论了如何通过 Java API for RESTful Web Services 开发 RESTful Web 服务,以及如何通过全新的标准 JAX-RS 客户端 API 开发 RESTful Web 服务客户端。本章还涵盖了 Java EE 8 中引入的新特性服务器端事件。

第十一章,使用 Java EE 开发微服务,解释了如何通过利用 Java EE 8 API 来开发微服务。

第十二章,使用 JAX-WS 开发 Web 服务,解释了如何通过 Java API for XML Web Services 开发基于 SOAP 的 Web 服务。

第十三章,Servlet 开发和部署,解释了如何通过 Servlet API 在 Java EE 应用程序中开发服务器端功能。

附录,配置和部署到 GlassFish,解释了如何配置 GlassFish,以便我们可以使用它来部署我们的应用程序,以及我们可以用于将我们的应用程序部署到 GlassFish 的各种方法。

您需要这本书的内容

遵循这本书的材料需要以下软件:

  • Java 开发工具包(JDK)1.8 或更高版本

  • 一个符合 Java EE 8 的应用服务器,如 GlassFish 5、Payara 5 或 OpenLiberty

  • 需要 Maven 3 或更高版本来构建示例

  • A 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);
      }

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

@FacesValidator(value = "emailValidator")

任何命令行输入或输出将如下所示:

/asadmin start-domain domain1

新术语重要词汇将以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下显示:"点击 Next 按钮将您移动到下一屏幕。"

警告或重要注意事项如下所示。

技巧和窍门如下所示。

读者反馈

我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发您真正能从中获得最大收益的标题。

要发送给我们一般反馈,请简单地发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书籍的标题。

如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在,您已经是 Packt 图书的骄傲拥有者,我们有多种方式可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com的账户下载此书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“SUPPORT”标签上。

  3. 点击“代码下载 & 勘误”。

  4. 在“搜索”框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击“代码下载”。

文件下载完成后,请确保您使用最新版本的以下软件解压或提取文件夹:

  • 适用于 Windows 的 WinRAR / 7-Zip

  • 适用于 Mac 的 Zipeg / iZip / UnRarX

  • 适用于 Linux 的 7-Zip / PeaZip

书籍的代码包也托管在 GitHub 上github.com/PacktPublishing/Java-EE-8-Application-Development。我们还有其他来自我们丰富图书和视频目录的代码包可供选择,请访问github.com/PacktPublishing/。查看它们吧!

勘误

尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现了错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进此书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的“勘误”部分下的现有勘误列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support并在搜索框中输入书籍名称。所需信息将在“勘误”部分显示。

盗版

在互联网上侵犯版权材料是一个跨所有媒体持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 copyright@packtpub.com 与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面所提供的帮助。

问题

如果您对本书的任何方面有问题,您可以通过 questions@packtpub.com 与我们联系,我们将尽力解决问题。

第一章:Java EE 简介

Java Platform, Enterprise Edition (Java EE) 由一组用于开发服务器端、企业 Java 应用程序的 应用程序编程接口 (API) 规范组成。在本章中,我们将提供一个 Java EE 的高级概述。

我们将在本章中介绍以下主题:

  • Java EE 简介

  • 一个标准,多个实现

  • Java EE、J2EE 和 Spring 框架

Java EE 简介

Java 平台,企业版 (Java EE) 是一组 API 规范的集合,旨在在开发服务器端、企业 Java 应用程序时协同工作。Java EE 是一个标准;Java EE 规范有多个实现。这一事实防止了供应商锁定,因为针对 Java EE 规范开发的代码可以部署到任何 Java EE 兼容的应用服务器,只需进行最小或没有修改。

Java EE 在 Java Community Process (JCP) 下开发,这是一个负责 Java 技术发展的组织。JCP 成员包括 Oracle(Java 平台的当前监护人)以及整个 Java 社区。

Java community process

Java Community Process (JCP) 允许感兴趣的相关方协助开发 Java 技术的标准技术规范。公司和个人都可以成为 JCP 的成员,并为任何他们感兴趣的技术规范做出贡献。每个 Java EE API 规范都是作为 Java Specification Request (JSR) 的一部分开发的。每个 JSR 都分配了一个唯一的编号。例如,JavaServer Faces (JSF) 2.3 是作为 JSR 372 开发的。

由于 Java EE 在 JCP 下开发,没有一家公司能够完全控制 Java EE 规范,因为,如前所述,JCP 对整个 Java 社区开放,包括软件供应商和感兴趣的个人。

不同的 JCP 成员有不同的兴趣,并为不同的 Java EE 规范做出贡献;结果是 Java EE 由 Java 社区的各个成员共同开发。

Java EE API

如前所述,Java EE 是一组 API 规范的集合,旨在在开发服务器端企业 Java 应用程序时协同工作。Java EE 8 API 包括:

  • JavaServer Faces (JSF) 2.3

  • Java 持久性 API (JPA) 2.2

  • 企业 JavaBeans (EJB) 3.2

  • Java EE 平台上下文和依赖注入 (CDI) 2.0

  • Java API for JSON Processing (JSON-P) 1.1

  • Java API for JSON Binding (JSON-B) 1.0

  • Java API for WebSocket 1.0

  • Java 消息服务 (JMS) 2.0

  • Java EE 安全 API 1.0

  • Java API for RESTful Web Services (JAX-RS) 2.1

  • Java API for XML Web Services (JAX-WS) 2.2

  • Servlet 4.0

  • 表达式语言 (EL) 3.0

  • JavaServer Pages (JSP) 2.3

  • Java 命名和目录接口 (JNDI) 1.2

  • Java 事务 API (JTA) 1.2

  • Java 事务服务 (JTS) 1.0

  • JavaMail 1.5

  • Java EE 连接器架构 (JCA) 1.7

  • Java Architecture for XML Binding (JAXB) 2.2

  • Java Management Extensions (JMX) 1.2

  • JavaServer Pages (JSTL) 标准标签库 1.2

  • Bean Validation 2.0

  • 管理 Bean 1.0

  • 拦截器 1.2

  • Java EE 并发实用工具 1.0

  • Java 平台批处理应用程序 1.0

上述列表是一个规范列表,应用服务器供应商或开源社区需要为每个 Java EE API 规范提供实现。应用服务器供应商然后将一组 Java EE API 实现捆绑在一起,作为其应用服务器产品的一部分。由于每个实现都符合相应的 Java EE JSR,针对一个实现开发的代码可以在任何其他实现上无修改地运行,从而避免供应商锁定。|

由于时间和空间限制,本书不会涵盖每个 Java EE API 规范,而是专注于最流行的 Java EE API。以下表格总结了我们将要覆盖的 API:

Java EE API 描述
JavaServer Faces (JSF) 2.3 JSF 是一个组件库,极大地简化了 Web 应用程序的开发。
Java Persistence API (JPA) 2.2 JPA 是 Java EE 标准的对象关系映射 (ORM) API。它使得与关系数据库交互变得容易。
Enterprise JavaBeans (EJB) 3.2 EJB 允许我们轻松地为 Java EE 应用程序添加企业功能,如事务和可伸缩性。
Contexts and Dependency Injection (CDI) 2.0 CDI 允许我们轻松定义 Java 对象的生命周期,并提供将依赖项轻松注入 Java 对象的能力;它还提供了一个强大的事件机制。
Java API for JSON Processing (JSON-P) 1.1 JSON-P 是一个允许在 Java 中处理 JSON 字符串的 API。
Java API for JSON Binding (JSON-B) 1.0 JSON-B 提供了从 JSON 流中轻松填充 Java 对象以及反向操作的能力。
Java API for WebSocket 1.0 WebSocket 是一个标准的 Java EE 实现,实现了互联网工程任务组 (IETF)的 WebSocket 协议,它允许通过单个 TCP 连接进行全双工通信。
Java Message Service (JMS) 2.0 JMS 是一个标准 API,允许 Java EE 开发者与面向消息的中间件 (MOM)交互。
Java EE Security API 1.0 Java EE 安全 API 旨在标准化和简化保护 Java EE 应用程序的任务。
Java API for RESTful Web Services (JAX-RS) 2.1 JAX-RS 是一个用于创建 RESTful 网络服务端点和客户端的 API。
Java API for XML Web Services (JAX-WS) 2.2 JAX-WS 是一个允许创建简单对象访问协议 (SOAP)网络服务的 API。
Servlet 4.0 Servlet API 是一个用于在 Web 应用程序中实现服务器端逻辑的低级 API。

我们还将介绍如何利用标准 Java EE API 开发微服务。微服务是一种现代、流行的架构风格,其中应用程序被分割成独立部署的小模块,通过网络相互交互,通常通过利用 RESTful 网络服务来实现。

我们还应该注意,除了关于微服务章节外,本书的每个章节都是独立的;您可以随意按任何顺序阅读这些章节。

现在我们已经介绍了 Java EE 提供的不同 API,值得重申的是,Java EE 是一个单一标准,有多种实现,其中一些是商业的,一些是开源的。

一个标准,多种实现

在其核心,Java EE 是一个规范——如果你愿意的话,可以说是一张纸。Java EE 规范的实现需要被开发,这样应用开发者才能根据 Java EE 标准实际开发服务器端的企业 Java 应用。每个 Java EE API 都有多种实现;例如,流行的 Hibernate 对象关系映射工具就是 Java EE 的 Java 持久化 API (JPA) 的一个实现。然而,它绝不是唯一的 JPA 实现;其他 JPA 实现包括 EclipseLink 和 OpenJPA。同样,每个 Java EE API 规范都有多种实现。

Java EE 应用通常部署到应用服务器;一些流行的应用服务器包括 JBoss、Websphere、Weblogic 和 GlassFish。每个应用服务器都被视为一个 Java EE 实现。应用服务器供应商要么开发自己的一些 Java EE API 规范的实现,要么选择包含现有的实现。

应用开发者通过不受特定 Java EE 实现的约束而受益。只要应用是针对标准 Java EE API 开发的,它应该非常易于跨应用服务器供应商移植。

Java EE、J2EE 和 Spring 框架

Java EE 早在 2006 年就被引入;Java EE 的第一个版本是 Java EE 5。Java EE 取代了 J2EE;J2EE 的最后一个版本是 J2EE 1.4,于 2003 年发布。尽管 J2EE 可以被认为是一种已死的技术,在 11 年前就被 Java EE 取代,但 J2EE 这个术语却拒绝消失。时至今日,许多人仍然将 Java EE 称为 J2EE;许多公司在他们的网站和招聘板上宣传他们正在寻找“J2EE 开发者”,似乎没有意识到他们所指的已经是一种存在了几年的过时技术。正确的术语,并且长期以来一直是,Java EE。

此外,术语 J2EE 已经成为任何服务器端 Java 技术的“万能”术语;通常 Spring 应用程序也被称作 J2EE 应用程序。Spring 并非 J2EE,也从未是 J2EE;事实上,Spring 是由 Rod Johnson 在 2002 年作为 J2EE 的替代品而创建的。就像 Java EE 一样,Spring 应用程序也经常被错误地称为 J2EE 应用程序。

摘要

在本章中,我们提供了 Java EE 的介绍,并列出了 Java EE 中包含的几个技术和应用程序编程接口(API)。

我们还介绍了 Java EE 是如何通过 Java 社区过程由软件供应商和整个 Java 社区共同开发的。

此外,我们解释了 Java EE 标准有多个实现,这一事实避免了供应商锁定,并使我们能够轻松地将我们的 Java EE 代码从一个应用程序服务器迁移到另一个。

最后,我们澄清了 Java EE、J2EE 和 Spring 之间的混淆,解释了尽管 J2EE 已经是一种过时的技术好几年了,Java EE 和 Spring 应用程序通常仍被称为 J2EE 应用程序。

第二章:JavaServer Faces

在本章中,我们将介绍 JavaServer FacesJSF),它是 Java EE 平台的标准组件框架。Java EE 8 包含了 JSF 2.3,这是 JSF 的最新版本。JSF 非常依赖于约定优于配置——如果我们遵循 JSF 的约定,那么我们就不需要编写很多配置。在大多数情况下,我们甚至不需要编写任何配置。这一事实,加上从 Java EE 6 开始 web.xml 是可选的,意味着在许多情况下,我们可以编写完整的 Web 应用程序,而无需编写任何一行 XML。

本章我们将涵盖以下主题:

  • Facelets

  • JSF 项目阶段

  • 数据验证

  • 命名 Bean

  • 导航

  • 启用 JSF 应用程序的 Ajax 功能

  • JSF HTML5 支持

  • Faces 流

  • JSF 艺术品注入

  • JSF WebSocket 支持

  • JSF 组件库

介绍 JSF

JSF 2.0 引入了许多增强功能,使 JSF 应用程序开发更加容易。在接下来的几节中,我们将探讨这些功能的一些。

对于不熟悉 JSF 早期版本的读者来说,可能无法完全理解以下几节。不用担心,到本章结束时,一切都会变得非常清晰。

Facelets

现代版本的 JSF 与早期版本之间一个显著的差异是,Facelets 现在是首选的视图技术。早期版本的 JSF 使用 Java Server PagesJSP)作为其默认的视图技术。由于 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 配置文件 faces-config.xml 在 JSF 2.0 中成为可选的。

在 JSF 2.0 及更高版本中,可以通过 @ManagedBean 注解配置 JSF 管理 Bean,从而无需在 faces-config.xml 中配置它们。Java EE 6 引入了 上下文和依赖注入CDI)API,它提供了一种实现通常使用 JSF 管理 Bean 实现的功能的替代方法。截至 JSF 2.2,CDI 命名 Bean 优于 JSF 管理 Bean;JSF 2.3 更进一步,废弃了特定的 JSF 管理 Bean,转而使用 CDI 命名 Bean。

此外,JSF 导航有一个惯例:如果一个 JSF 2.0 命令链接或命令按钮的动作属性的值与一个 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 组件知道它们可以从这两个位置之一检索资源。

为了避免使 resources 目录杂乱,通常将 资源 放置在子目录中。这个子目录由 JSF 组件的 library 属性引用。

例如,我们可以在 /resources/css/styles.css 下放置一个名为 styles.css 的 CSS 样式表。

在我们的 JSF 页面中,我们可以使用 <h:outputStylesheet> 标签检索这个 CSS 文件,如下所示:

<h:outputStylesheet library="css"  name="styles.css"/> 

library 属性的值必须与我们的 stylesheet 所在的子目录相匹配。

同样,我们可以在 /resources/scripts/somescript.js 下放置一个 JavaScript 文件,在 /resources/images/logo.png 下放置一个图片,并且我们可以按照以下方式访问这些 资源

<h:graphicImage library="images" name="logo.png"/>

以及:

<h:outputScript library="scripts" name="somescript.js"/> 

注意,在每种情况下,library 属性的值与 resources 目录下的相应子目录名称相匹配,而 name 属性的值与资源的文件名相匹配。

开发我们的第一个 JSF 应用程序

为了说明基本的 JSF 概念,我们将开发一个由两个 Facelet 页面和一个名为 CDI 的单个命名 bean 组成的简单应用程序。

Facelets

正如我们在本章引言中提到的,JSF 2.0 及更高版本的默认视图技术是Facelets。Facelets 需要使用标准 XML 编写。开发 Facelet 页面的最流行方式是结合使用 XHTML 和 JSF 特定的 XML 命名空间。以下示例显示了典型的 Facelet 页面看起来是什么样子:

<?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> 

以下截图说明了前一个页面在浏览器中的渲染效果:

当然,前面的截图是在每个文本字段输入一些数据之后拍摄的;最初每个文本字段都是空的。

几乎任何 Facelet JSF 页面都会包含示例中展示的两个命名空间。第一个命名空间(h:)用于渲染 HTML 组件的标签;按照惯例,当使用这个 标签 库时,使用前缀 h(代表 HTML)。

第二个命名空间(``)是核心 JSF tag库,按照惯例,当使用这个tag库时,前缀f(代表 faces)被使用。`

在前面的示例中,我们首先看到的与 JSF 相关的标签是<h:head><h:body>标签。这些标签与标准的 HTML <head><body>标签类似,当页面在浏览器中显示时,它们会被这样渲染。

<h:outputStylesheet>标签用于从一个已知位置(JSF 标准化了资源的位置,如 CSS 样式表和 JavaScript 文件;这将在本章后面详细讨论)加载 CSS 样式表。library属性的值必须与 CSS 文件所在的目录相对应(这个目录必须在resources目录下)。name属性的值必须与我们要加载的 CSS 样式表的名称相对应。

下一个我们看到的标签是<h:form>标签。当页面渲染时,这个标签会生成一个 HTML 表单。正如示例所示,不需要为这个标签指定actionmethod属性;实际上,这个标签没有actionmethod属性。渲染的 HTML 表单的action属性将自动生成,而method属性始终是post<h:form>id属性是可选的;然而,始终添加它是一个好主意,因为它使得调试 JSF 应用程序变得更加容易。

下一个我们看到的标签是<h:messages>标签。正如其名所示,这个标签用于显示任何消息。正如我们很快就会看到的,JSF 可以自动生成验证消息,并在该标签内显示。此外,可以通过javax.faces.context.FacesContext中定义的addMessage()方法以编程方式添加任意消息。

接下来的 JSF 标签是<h:panelGrid>。这个标签大致相当于 HTML 表格,但它的工作方式略有不同。不是通过声明行(<tr>)和单元格(<td>),<h:panelGrid>标签有一个columns属性;这个属性的值表示由这个标签渲染的表格中的列数。当我们在这个标签内放置组件时,它们将按行放置,直到达到columns属性定义的列数,此时下一个组件将被放置在下一行。在示例中,columns属性的值是2,因此前两个标签将被放置在第一行,接下来的两个将被放置在第二行,以此类推。

<h:panelGrid> 的另一个有趣属性是 columnClasses 属性。这个属性为渲染表格中的每一列分配一个 CSS 类。在示例中,使用两个 CSS 类(用逗号分隔)作为此属性的值。这会产生将第一个 CSS 类分配给第一列,第二个分配给第二列的效果。如果有三列或更多列,第三列将获得第一个 CSS 类,第四列获得第二个,依此类推,交替进行。为了阐明这是如何工作的,下面的代码片段展示了前面页面生成的 HTML 标记源的一部分:

<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> 

注意到每个 <td> 标签都有一个交替的 CSS 标签——"rightAlign""leftAlign";我们通过将值 "rightAlign,leftAlign" 分配给 <h:panelGrid>columnClasses 属性来实现这一点。我们应该注意,我们在示例中使用的 CSS 类是在我们之前讨论的 <h:outputStylesheet> 加载的 CSS 样式表中定义的。生成的标记的 ID 是我们分配给 <h:form> 组件的 ID 以及每个单独组件的 ID 的组合。注意,我们在页面末尾附近没有为 <h:commandButton> 组件分配 ID,因此 JSF 运行时自动分配了一个。

在示例的这个阶段,我们开始在 <h:panelGrid> 内部添加组件。这些组件将被渲染在由 <h:panelGrid> 渲染的表格内。正如我们之前提到的,渲染表格的列数由 <h:panelGrid>columns 属性定义。因此,我们不需要担心列(或行),我们只需开始添加组件,它们就会被放置在正确的位置。

我们接下来看到的标签是 <h:outputLabel> 标签。这个标签渲染为一个 HTML label 元素。标签通过 for 属性与其他组件关联,其值必须与标签所针对的组件的 ID 匹配。

接下来,我们看到 <h:inputText> 标签。这个标签在渲染的页面中生成一个文本字段。它的 label 属性用于任何验证消息;它让用户知道消息指的是哪个字段。

虽然 <h:inputText>label 属性的值与页面上显示的标签匹配不是强制性的,但强烈建议使用此值。在出现错误的情况下,这将使用户确切知道消息指的是哪个字段。

特别值得注意的是该标签的value属性。我们看到的这个属性的值是一个值绑定表达式。这意味着这个值与一个应用命名 bean 的属性相关联。在示例中,这个特定的文本字段与名为customer的命名 bean 中的firstName属性相关联。当用户为这个文本字段输入值并提交表单时,命名 bean 中相应的属性会更新为这个值。标签的required属性是可选的;它的有效值是truefalse。如果这个属性设置为true,容器将不允许用户提交表单,直到用户为文本字段输入一些数据。如果用户尝试不输入必需的值提交表单,页面将被重新加载,并在<h:messages>标签内显示错误消息:

图片

以下截图展示了当用户尝试在示例中保存表单而没有输入客户的首名值时显示的默认错误消息。消息的第一部分(First Name)来自相应<h:inputTextField>标签的label属性值。消息的文本可以自定义,以及其样式(字体、颜色等)。我们将在本章后面介绍如何进行这些操作。

项目阶段

在每个 JSF 页面上放置一个<h:messages>标签是个好主意;没有它,用户可能看不到验证消息,并且不知道为什么表单提交没有成功。默认情况下,JSF 验证消息不会在应用服务器日志中生成任何输出。新 JSF 开发者常见的错误是未能将<h:messages>标签添加到他们的页面上;如果没有它,如果验证失败,则导航似乎没有原因失败(如果导航失败,则渲染同一页面,如果没有<h:messages>标签,浏览器中不会显示错误消息)。

为了避免这种情况,JSF 2.0 引入了项目阶段的概念。

在 JSF 2.0 及更高版本中定义了以下项目阶段:

  • 生产

  • 开发

  • 单元测试

  • 系统测试

我们可以将项目阶段定义为web.xml中 Faces servlet 的初始化参数,或者作为一个自定义的 JNDI 资源。设置项目阶段的首选方式是通过自定义 JNDI 资源。

将全局 JNDI 资源映射到组件资源的流程是应用服务器特定的;当使用 GlassFish 时,需要修改应用的web.xml文件,并且我们需要使用 GlassFish 特定的部署描述符。

设置自定义 JNDI 资源是应用服务器特定的,请查阅您的应用服务器文档以获取详细信息。如果我们使用 GlassFish 来部署我们的应用,可以通过登录到 Web 控制台,导航到 JNDI | 自定义资源,然后点击新建...按钮来设置自定义 JNDI:

在生成的页面中,我们需要输入以下信息:

JNDI 名称 javax.faces.PROJECT_STAGE
资源类型 java.lang.String

输入这两个值后,工厂类字段将自动填充为值:org.glassfish.resources.custom.factory.PrimitivesAndStringFactory

输入这些值后,我们需要添加一个名为 value 的新属性,其值对应于我们想要使用的项目阶段(在先前的屏幕截图中为 Development)。

设置项目阶段允许我们在特定阶段运行时执行一些逻辑。例如,在我们的一个命名 bean 中,我们可能有如下代码:

    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 
    } 

如我们所见,项目阶段允许我们根据不同的环境修改我们代码的行为。更重要的是,设置项目阶段允许 JSF 引擎根据项目阶段设置以略有不同的方式运行。与我们的讨论相关,将项目阶段设置为 Development 会导致在应用程序服务器日志中生成额外的日志语句。因此,如果我们忘记在我们的页面上添加<h:messages>标签,我们的项目阶段是 Development,验证失败;即使我们省略了<h:messages>组件,页面上也会显示验证错误:

在默认的Production阶段,此错误消息不会在页面上显示,这让我们对我们的页面导航似乎不起作用感到困惑。

验证

注意到每个<h:inputField>标签都有一个嵌套的<f:validateLength>标签。正如其名称所暗示的,此标签验证输入的文本字段值是否在最小和最大长度之间。最小和最大值由标签的minimummaximum属性定义。《f:validateLength》是 JSF 附带的标准验证器之一。就像<h:inputText>required属性一样,当用户尝试提交一个值未通过验证的表单时,JSF 将自动显示默认错误消息:

再次,默认消息和样式可以被覆盖;我们将在本章后面介绍如何做到这一点。

除了<f:validateLength>之外,JSF 还包括其他标准验证器,这些验证器在以下表中列出:

验证标签 描述
<f:validateBean> Bean 验证允许我们通过在命名bean中使用注解来验证命名bean的值,而无需向我们的 JSF 标签添加验证器。此标签允许我们在必要时微调 bean 验证。
<f:validateDoubleRange> 验证输入是否是有效的Double值,该值在标签的minimummaximum属性指定的两个值之间,包括这两个值。
<f:validateLength> 验证输入的长度是否在标签的minimummaximum值之间,包括这两个值。
<f:validateLongRange> 验证输入是否为在标签的minimummaximum属性指定的值之间的有效Double值,包括这些值。
<f:validateRegex> 验证输入是否与标签的pattern属性中指定的正则表达式模式匹配。
<f:validateRequired> 验证输入是否不为空。此标签等同于在父输入字段中将required属性设置为true

注意,在<f:validateBean>的描述中,我们简要提到了 bean 验证。bean 验证 JSR 旨在标准化 JavaBean 验证。JavaBeans 被用于多个其他 API 中,直到最近,它们必须实现自己的验证逻辑。JSF 2.0 采用了 bean 验证标准,以帮助验证命名 bean 属性。

如果我们要利用 bean 验证,我们只需要使用适当的 bean 验证注解注释所需的字段,而无需显式使用 JSF 验证器。

要获取完整的 bean 验证注解列表,请参考 Java EE 8 API 中的javax.validation.constraints包,链接为javaee.github.io/javaee-spec/javadocs/.

分组组件

<h:panelGroup>是示例中的下一个新标签。通常,<h:panelGroup>用于将多个组件组合在一起,以便它们在<h:panelGrid>中占据单个单元格。这可以通过在<h:panelGroup>内添加组件并将<h:panelGroup>添加到<h:panelGrid>中来实现。如示例所示,这个特定的<h:panelGroup>实例没有子组件。在这种情况下,<h:panelGroup>的目的是有空单元格,并使下一个组件<h:commandButton>与表单中的所有其他输入字段对齐。

表单提交

<h:commandButton>在浏览器中渲染一个 HTML 提交按钮。就像标准 HTML 一样,它的目的是提交表单。它的value属性简单地设置按钮的标签。此标签的action属性用于导航;要显示的下一页基于此属性的值。action属性可以有一个String常量或一个方法绑定表达式,这意味着它可以指向一个名为 bean 中的method,该方法返回一个字符串。

如果我们应用程序中页面的基本名称与<h:commandButton>标签的action属性值匹配,那么当我们点击按钮时,我们将导航到这个页面。这个 JSF 特性使我们免于定义导航规则,就像我们在 JSF 的旧版本中必须做的那样。在我们的示例中,我们的确认页面名为confirmation.xhtml;因此,按照惯例,当点击按钮时,将显示此页面,因为其action属性的值(confirmation)与页面的基本名称匹配。

即使按钮的标签显示为保存,在我们的简单示例中,点击按钮实际上并不会保存任何数据。

命名 Bean

有两种 Java Bean 可以与 JSF 页面交互:JSF 管理 Bean 和 CDI 命名 Bean。JSF 管理 Bean 自 JSF 规范的第一版以来就存在了,并且只能在 JSF 上下文中使用。CDI 命名 Bean 是在 Java EE 6 中引入的,并且可以与其他 Java EE API(如企业 JavaBeans)交互。因此,CDI 命名 Bean 比 JSF 管理 Bean 更受欢迎。

要使一个 Java 类成为 CDI 命名 Bean,我们只需要确保该类有一个公共的无参数构造函数(如果没有声明其他构造函数,则会隐式创建一个,正如我们的例子所示),并在类级别添加 @Named 注解。以下是我们的示例中的管理 Bean:

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; 
  } 
} 

@Named 类注解指定这个 Bean 是一个 CDI 命名 Bean。这个注解有一个可选的 value 属性,我们可以用它给我们的 Bean 一个用于 JSF 页面的逻辑名称。然而,按照惯例,这个属性的值与类名(在我们的例子中是 Customer)相同,其第一个字符被转换为小写。在我们的例子中,我们让这个默认行为发生,因此我们通过 customer 逻辑名称访问我们的 Bean 属性。注意,在我们的示例页面中任何输入字段的 value 属性,以查看这个逻辑名称的实际应用。

注意,除了 @Named@RequestScoped 注解之外,这个 Bean 没有特别之处。它是一个标准的 JavaBean,具有私有属性和相应的 gettersetter 方法。@RequestScoped 注解指定 Bean 应该存在于单个请求中。我们 JSF 应用程序中可用的不同命名 Bean 作用域将在下一节中介绍。

命名 Bean 作用域

管理 Bean 总是有一个作用域。管理 Bean 的作用域定义了应用程序的生命周期。管理 Bean 的作用域由类级别的注解定义。下表列出了所有有效的管理 Bean 作用域:

命名 Bean 作用域注解 描述
@ApplicationScoped 应用程序作用域的命名 Bean 的同一实例对所有我们的应用程序客户端都是可用的。如果一个客户端修改了应用程序作用域管理 Bean 的值,则更改将在客户端之间反映出来。
@SessionScoped 每个会话作用域的命名 Bean 实例都被分配给我们的应用程序的每个客户端。会话作用域的命名 Bean 可以用来在请求之间保持客户端特定的数据。
@RequestScoped 请求作用域的命名 Bean 只存在于单个 HTTP 请求中。
@Dependent 依赖作用域的命名 Bean 被分配给它们注入到的 Bean 相同的范围。
@ConversationScoped 会话作用域可以跨越多个请求,但通常比会话作用域短。

导航

如我们的输入页面所示,当在 customer_data_entry.xhtml 页面中点击保存按钮时,我们的应用程序将导航到名为 confirmation.xhtml 的页面。这是因为我们正在利用 JSF 的约定优于配置功能;如果命令按钮或链接的 action 属性值与另一个页面的基本名称匹配,则此导航将带我们到该页面。

点击按钮或链接时是否会导致同一页面的重新加载?当 JSF 未能识别命令按钮或命令链接的 action 属性值时,它将默认导航到用户点击按钮或链接时在浏览器中显示的同一页面。该按钮或链接原本是用来导航到另一个页面的。

如果导航似乎没有正常工作,那么很可能是此属性值的拼写错误。记住,按照惯例,JSF 将寻找一个基本名称与命令按钮或链接的 action 属性值匹配的页面。

confirmation.xhtml 的来源看起来是这样的:

<?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> 

<h:outputText> 是我们之前没有介绍过的唯一标签。这个标签简单地将其 value 属性的值显示到渲染的页面,其 value 属性可以是简单的字符串或值绑定表达式。由于我们 <h:outputText> 标签中的值绑定表达式与之前页面中 <h:inputText> 标签使用的相同表达式,它们的值将对应于用户输入的数据:

图片

在传统的(即非 JSF)Java web 应用程序中,我们定义 URL 模式以供特定的 servlet 处理。对于 JSF,通常使用 .jsf.faces 后缀;另一个常用的 JSF URL 映射是 /faces 前缀。在满足某些条件下,现代应用程序服务器会自动将所有三个映射添加到 faces servlet,如果这些条件得到满足,我们根本不需要指定任何 URL 映射。

如果满足以下任何条件,则 FacesServlet 将自动映射:

  • 在我们 web 应用程序的 WEB-INF 目录中有一个 faces-config.xml 文件

  • 在我们 web 应用程序的依赖项之一的 META-INF 目录中有一个 faces-config.xml 文件

  • 在我们 web 应用程序的依赖项之一的 META-INF 目录中有一个以 .faces-config.xml 结尾的文件名

  • 我们在 web.xml 或依赖项中的一个 web-fragment.xml 中声明一个名为 javax.faces.CONFIG_FILES 的上下文参数

  • 在调用 ServletContextInitializeronStartup() 方法时,传递一个非空的类集

当上述条件都不满足时,我们需要在我们的 web.xml 部署描述符中显式映射 Faces servlet,如下所示:

<?xml version="1.0" encoding="UTF-8"?> 
<web-app  

         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" 
         version="3.1"> 
 <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>/faces/*</url-pattern> </servlet-mapping> 
</web-app> 

我们应用中页面的 URL 是我们的 Facelets 页面的名称,前面加上 /faces 前缀。

自定义数据验证

除了提供标准验证器供我们使用外,JSF 允许我们创建 custom validators。这可以通过两种方式之一完成——通过创建一个 custom validator 类或通过向我们的命名豆中添加 validation 方法。

创建自定义验证器

除了标准验证器外,JSF 允许我们通过创建一个实现 javax.faces.validator.Validator 接口的 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.lang3.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.routines.EmailValidator emailValidator = 
        org.apache.commons.validator.routines.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 实例,将要在浏览器中显示的错误消息作为其 constructor 参数传递。然后我们抛出一个新的 javax.faces.validator.ValidatorException。错误消息随后在浏览器中显示;它通过 JSF API 在幕后到达。

Apache Commons Validator:我们的自定义 JSF 验证器使用 Apache Commons Validator 进行实际验证。这个库包括许多常见的验证,如日期、信用卡号码、ISBN 号码和电子邮件。在实现自定义验证器时,值得调查这个库是否已经有一个我们可以使用的验证器。

为了在我们的页面上使用我们的验证器,我们需要使用 <f:validator> JSF 标签。以下 Facelets 页面是客户数据输入屏幕的修改版本。这个版本使用 <f:validator> 标签来验证电子邮件:

<?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> 
      <h:messages></h:messages> 
      <h:panelGrid columns="2" 
       columnClasses="rightAlign,leftAlign"> 
        <h:outputText value="First Name:"> 
        </h:outputText> 
        <h:inputText label="First Name" 
         value="#{customer.firstName}" 
         required="true"> 
          <f:validateLength minimum="2" maximum="30"> 
          </f:validateLength> 
        </h:inputText> 
        <h:outputText value="Last Name:"></h:outputText> 
        <h:inputText label="Last Name" 
         value="#{customer.lastName}" 
         required="true"> 
          <f:validateLength minimum="2" maximum="30"> 
          </f:validateLength> 
        </h:inputText> 
        <h:outputText value="Email:"> 
        </h:outputText> 
 <h:inputText label="Email" value="#{customer.email}">           
          <f:validator validatorId="emailValidator" /> </h:inputText> 
        <h:panelGroup></h:panelGroup> 
        <h:commandButton action="confirmation" value="Save"> 
        </h:commandButton> 
      </h:panelGrid> 
    </h:form> 
  </h:body> 
</html>

在编写我们的自定义验证器并修改我们的页面以利用它之后,我们可以看到我们的 validator 在行动:

图片

验证器方法

我们可以实施自定义验证的另一种方式是通过向应用程序的一个或多个命名豆中添加 validation 方法。以下 Java 类说明了使用 validator 方法进行 JSF 验证的使用:

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.lang3.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); 
    } 
  } 
} 

在这个示例中,该类只包含validator方法,但这并不总是必须的。我们可以给我们的validator方法起任何我们想要的名称;然而,它的返回值必须是void,并且它必须按照示例中所示顺序接受三个参数。换句话说,除了方法名外,验证器方法的签名必须与在javax.faces.validator.Validator接口中定义的validate()方法的签名相同。

如我们所见,前面的validator方法体几乎与我们的custom validator类中的validate()方法体相同。我们检查用户输入的值以确保它只包含字母字符和/或空格;如果不满足,则抛出一个包含适当错误消息StringValidatorExceptionStringUtils

在示例中,我们使用了org.apache.commons.lang3.StringUtils来执行实际的验证逻辑。除了示例中使用的方法外,这个类还包含了一些用于验证String是否为数字或字母数字的方法。这个类是Apache Commons Lang库的一部分,在编写自定义验证器时非常有用。

由于每个validator方法都必须在命名 bean 中,我们需要确保包含我们的validator方法的类被注解了@Named注解,正如我们在示例中所展示的那样。

我们需要做的最后一件事是将我们的validator方法绑定到我们的组件,通过标签的validator属性:

<?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> 
      <h:messages></h:messages> 
      <h:panelGrid columns="2" 
                   columnClasses="rightAlign,leftAlign"> 
        <h:outputText value="First Name:"> 
        </h:outputText> 
 <h:inputText label="First Name" value="#{customer.firstName}" required="true" validator="#{alphaValidator.validateAlpha}"> 
          <f:validateLength minimum="2" maximum="30"> 
          </f:validateLength> 
 </h:inputText> 
        <h:outputText value="Last Name:"></h:outputText> 
 <h:inputText label="Last Name"         value="#{customer.lastName}" required="true" validator="#{alphaValidator.validateAlpha}"> 
          <f:validateLength minimum="2" maximum="30"> 
          </f:validateLength> 
 </h:inputText> 
        <h:outputText value="Email:"> 
        </h:outputText> 
        <h:inputText label="Email" value="#{customer.email}"> 
          <f:validateLength minimum="3" maximum="30"> 
          </f:validateLength> 
          <f:validator validatorId="emailValidator" /> 
        </h:inputText> 
        <h:panelGroup></h:panelGroup> 
        <h:commandButton action="confirmation" value="Save"> 
        </h:commandButton> 
      </h:panelGrid> 
    </h:form> 
  </h:body> 
</html> 

由于“姓氏”和“名字”字段都不应接受除字母字符和空格之外的内容,我们已将我们的自定义验证器方法添加到这两个字段。

注意到<h:inputText>标签的validator属性的值是一个 JSF 表达式语言;它使用包含我们的validation方法的 bean 的默认命名 bean 名称。alphaValidator是我们 bean 的名称,而validateAlpha是我们validator方法的名称。

在修改我们的页面以使用我们的custom validator之后,我们现在可以看到它的实际应用:

图片

注意到对于“姓氏”字段,我们的自定义验证消息和标准的长度验证器都已被执行。

实现验证器方法的优势是无需创建一个仅用于单个validator方法的整个类(我们的示例就是这样做的,但在许多情况下,验证器方法被添加到包含其他方法的现有命名 bean 中);然而,缺点是每个组件只能由单个验证器方法进行验证。当使用验证器类时,可以在要验证的标签内部嵌套多个<f:validator>标签,因此可以在字段上执行多个验证,包括自定义和标准验证。

自定义 JSF 的默认消息

正如我们之前提到的,可以自定义 JSF 默认验证消息的样式(字体、颜色、文本等)。此外,还可以修改默认 JSF 验证消息的文本。在接下来的章节中,我们将解释如何修改错误消息的格式和文本。

自定义消息样式

可以通过层叠样式表CSS)来自定义消息样式。这可以通过使用<h:message>stylestyleClass属性来实现。当我们要声明内联 CSS 样式时,使用style属性。当我们要在 CSS 样式表中或在我们页面的<style>标签中使用预定义样式时,使用styleClass属性。

下面的标记示例展示了如何使用styleClass属性来改变错误消息的样式;这是我们在上一节中看到的输入页面的一个修改版本:

<?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> 
 <h:messages styleClass="errorMsg"></h:messages> 
      <h:panelGrid columns="2" 
                   columnClasses="rightAlign,leftAlign"> 
        <h:outputText value="First Name:"> 
        </h:outputText> 
        <h:inputText label="First Name" 
         value="#{customer.firstName}" 
         required="true" validator="# 
         {alphaValidator.validateAlpha}"> 
          <f:validateLength minimum="2" maximum="30"> 
          </f:validateLength> 
        </h:inputText> 
        <h:outputText value="Last Name:"></h:outputText> 
        <h:inputText label="Last Name" 
         value="#{customer.lastName}" 
         required="true" 
         validator="#{alphaValidator.validateAlpha}"> 
          <f:validateLength minimum="2" maximum="30"> 
          </f:validateLength> 
        </h:inputText> 
        <h:outputText value="Email:"> 
        </h:outputText> 
        <h:inputText label="Email" value="#{customer.email}"> 
          <f:validator validatorId="emailValidator" /> 
        </h:inputText> 
        <h:panelGroup></h:panelGroup> 
        <h:commandButton action="confirmation" value="Save"> 
        </h:commandButton> 
      </h:panelGrid> 
    </h:form> 
  </h:body> 
</html> 

与前一页相比,唯一的区别是使用了<h:messages>标签的styleClass属性。正如我们之前提到的,styleClass属性的值必须与我们在级联样式表中定义的 CSS 样式的名称匹配。

在我们的案例中,我们为消息定义了如下 CSS 样式:

.errorMsg { 
  color: red; 
} 

然后我们将这个样式用作我们<h:messages>标签的styleClass属性的值。

下面的截图展示了实施此更改后验证错误消息的外观:

图片

在这个特定案例中,我们只是将错误消息文本的颜色设置为红色,但我们仅限于 CSS 的能力来设置错误消息的样式。

几乎任何标准的 JSF 组件都同时具有stylestyleClass属性,可以用来改变其样式。前者用于预定义的 CSS 样式,后者用于内联 CSS。

自定义消息文本

有时可能需要覆盖 JSF 默认验证错误的文本。默认验证错误定义在名为Messages.properties的资源包中。这个文件通常可以在应用程序服务器的 JSF JAR 文件中找到。例如,GlassFish 将其包含在[glassfish 安装目录]/glassfish/modules下的javax.faces.jar文件中。该文件包含多个消息;目前我们只对验证错误感兴趣。默认验证错误消息定义如下:

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: Length is greater than allowable maximum of ''{0}'' 
javax.faces.validator.LengthValidator.MINIMUM={1}: Validation Error: Length 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} 

为了覆盖默认的错误消息,我们需要创建自己的资源包,使用与默认资源包相同的键,但修改值以适应我们的需求。以下是我们应用程序的一个非常简单的自定义资源包示例。例如,要覆盖最小长度验证的消息,我们将在我们的自定义资源包中添加以下属性:

javax.faces.validator.LengthValidator.MINIMUM={1}: minimum allowed length is ''{0}'' 

在这个资源包中,我们覆盖了当使用 <f:validateLength> 标签验证的字段输入值小于允许的最小值时的错误信息。为了让我们的应用程序知道我们有一个自定义的资源包用于消息属性,我们需要修改应用程序的 faces-config.xml 文件:

<?xml version='1.0' encoding='UTF-8'?> 
<faces-config version="2.0" 

      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
      http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"> 
 <application> <message-bundle>net.ensode.Messages</message-bundle> </application> 
</faces-config> 

如我们所见,我们只需要对应用程序的 faces-config.xml 文件进行修改,添加一个 <message-bundle> 元素,指定包含我们的自定义消息的资源包的名称和位置。

自定义错误消息文本定义是我们仍然需要为现代 JSF 应用程序定义 faces-config.xml 文件的不多几个情况之一。然而,请注意我们的 faces-config.xml 文件是多么简单,与典型的 JSF 1.x 的 faces-config.xml 相去甚远,后者通常包含命名 Bean 定义、导航规则和 JSF 验证器定义。

在添加我们的自定义消息资源包并修改应用程序的

faces-config.xml 文件后,我们可以看到我们的自定义验证消息在实际中的应用:

如截图所示,如果我们没有覆盖验证消息,默认消息仍然会显示。在我们的资源包中,我们只覆盖了最小长度验证错误消息,因此我们的自定义错误消息显示在“姓氏”文本字段中。由于我们没有在其他标准 JSF 验证器中覆盖错误消息,因此每个验证器都会显示默认错误消息。电子邮件验证器是我们在本章中之前开发的自定义验证器;由于它是一个自定义验证器,其错误消息不受影响。

启用 Ajax 的 JSF 应用程序

JSF 允许我们通过简单地使用 <f:ajax> 标签和 CDI 命名 Bean,轻松地将 Ajax异步 JavaScript 和 XML)功能实现到我们的 Web 应用程序中,无需实现任何 JavaScript 代码或解析 JSON 字符串来实现带有 JSF 的 Ajax。

下图展示了 <f:ajax> 标签的典型用法:

<?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>JSF Ajax Demo</title> 
  </h:head> 
  <h:body> 
    <h2>JSF Ajax Demo</h2> 
    <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> 
      <hr/> 
      <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> 
</html> 

在部署我们的应用程序后,前一页渲染的结果如下截图所示:

前一页展示了 <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 属性。此属性通常在我们点击按钮后不需要导航到另一个页面时使用。此属性的值是我们在一个命名豆中编写的 action listener 方法。Action listener 方法必须返回 void,并接受一个 javax.faces.event.ActionEvent 实例作为其唯一参数。

我们应用程序的命名 bean 看起来像这样:

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;
  }
}

注意,我们不需要在我们的命名 bean 中做任何特殊的事情来在我们的应用程序中启用 Ajax。所有这些都由页面上的<f:ajax>标签控制。

从这个例子中我们可以看出,启用 Ajax 的 JSF 应用程序非常简单;我们只需使用一个标签就可以将页面 Ajax 化,无需编写任何 JavaScript、JSON 或 XML 代码。

JSF HTML5 支持

HTML5 是 HTML 规范的最新版本,并在之前版本的 HTML 上包含了一些改进。JSF 的现代版本包括几个特性,使得 JSF 页面能够很好地与 HTML5 协同工作。

HTML5 友好标记

通过使用透传元素,我们可以使用 HTML5 开发页面,并将它们视为 JSF 组件。为此,我们需要指定至少一个元素属性使用xmlns.jcp.org/jsf 命名空间。以下示例展示了这种方法在实际中的应用:

<!DOCTYPE html>
<html 
      >
 <head jsf:id="head">
 <title>JSF Page with HTML5 Markup</title>
 <link jsf:library="css" jsf:name="styles.css" rel="stylesheet"
 type="text/css" href="resources/css/styles.css"/>
    </head>
    <body jsf:id="body">
 <form jsf:prependId="false">
            <table style="border-spacing: 0; border-collapse:  
             collapse">
                <tr>
                     <td class="rightAlign">
                      <label jsf:for="firstName">First  
                       Name</label>
                    </td>
                    <td class="leftAlign">
                      <input type="text" jsf:id="firstName"
 jsf:value="#{customer.firstName}"/>
                    </td>
                </tr>
                <tr>
                    <td class="rightAlign">
                      <label jsf:for="lastName">
                       Last Name</label>
                    </td>
                    <td class="leftAlign">
                      <input type="text" jsf:id="lastName"
                       jsf:value="#{customer.lastName}"/>
                       </td>
                </tr>
                <tr>
                    <td class="rightAlign">
                      <label jsf:for="email">Email  
                      Address</label>
                    </td>
                    <td class="leftAlign">
 <input type="email" jsf:id="email" 
 jsf:value="#{customer.email}"/></td>
                </tr>
                <tr>
                    <td></td>
                  <td>
                    <input type="submit"  
                     jsf:action="confirmation"
                     value="Submit"/>
                  </td>
                </tr>
            </table>
        </form>
    </body>
</html>

我们首先应该注意的关于前面例子的是,在页面顶部附近带有jsf前缀的 XML 命名空间。这个命名空间允许我们在 HTML5 页面上添加 JSF 特定的属性。当 JSF 运行时遇到页面任何标签上以jsf为前缀的属性时,它会自动将 HTML5 标签转换为等效的 JSF 组件。JSF 特定的属性与常规 JSF 页面中的属性相同,只是它们以jsf为前缀,因此,在这个阶段,它们应该是自解释的,不会详细讨论。

前面的例子将渲染和表现就像本章中的第一个例子一样。

本节中描述的技术如果我们的团队中有经验丰富的 HTML 网页设计师,他们更喜欢对页面外观有完全控制权时非常有用。这些页面使用标准的 HTML5 和 JSF 特定的属性来开发,以便 JSF 运行时可以管理用户输入。

如果我们的团队主要由有限的 CSS/HTML 知识的 Java 开发者组成,那么最好使用 JSF 组件来开发我们的 Web 应用程序的 Web 页面。HTML5 引入了几个在之前 HTML 版本中不存在的属性。因此,JSF 2.2 引入了向 JSF 组件添加任意属性的能力;这种 JSF/HTML5 集成技术将在下一节中讨论。

透传属性

JSF 允许定义任何任意属性(不被 JSF 引擎处理);这些属性在浏览器中显示的生成的 HTML 上简单地以原样渲染。以下示例是本章早期示例的新版本,修改后以利用 HTML5 透传属性:

<?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: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"
                 p:placeholder="First Name"> <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"
                 p:placeholder="Last Name"> <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}"
                 p:placeholder="Email Address"> <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>

我们首先应该注意的关于这个例子的是添加了http://xmlns.jcp.org/jsf/passthrough命名空间,这允许我们向我们的 JSF 组件添加任何任意属性。

在我们的例子中,我们在页面上所有的输入文本字段中添加了 HTML5 的placeholder属性;正如我们所看到的,它需要以应用顶部定义的命名空间的前缀(在我们的例子中是p)为前缀。placeholder HTML 属性简单地为输入字段添加一些占位文本,一旦用户开始在输入字段中输入,这些文本就会自动删除(在 HTML5 之前,这种技术通常通过 JavaScript 手动实现)。

以下截图显示了我们的更新页面在实际操作中的效果:

图片

JSF 2.2 Faces flows

JSF 2.2 引入了 Faces flows,它定义了一个可以跨越多个页面的作用域。当用户进入流程(一组网页)时创建流作用域的 bean,当用户离开流程时销毁。

Faces flows 遵循 JSF 的约定优于配置原则。在开发使用 faces flows 的应用程序时,通常使用以下约定:

  • 流程中的所有页面都必须放置在一个以流程名称命名的目录中。

  • 必须在包含流程页面的目录内存在一个以目录名称命名并后缀为-flow 的 XML 配置文件(文件可能为空,但必须存在)。

  • 流程中的第一个页面必须以包含流程的目录名称命名。

  • 流程中的最后一个页面不能位于包含流程的目录内,并且必须以目录名称命名,后缀为-return。

以下截图说明了这些约定:

图片

在前面的例子中,我们定义了一个名为customerinfo的流程;按照惯例,这些文件位于名为customerinfo的目录中,流程的第一个页面命名为customerinfo.xhtml(流程中其他页面的名称没有限制)。当我们退出流程时,我们将导航到一个名为flowname-return.xml的页面;在我们的例子中,由于我们的流程名为customerinfo,所以相关页面的名称是customerinfo-return.xhtml,它遵循命名约定并使我们退出流程。

页面的标记没有展示我们之前没有见过的内容;因此,我们不会展示它。所有示例代码都作为本书代码下载包的一部分提供。

所有的前一个页面都将数据存储在一个名为Customer的命名 bean 中,它具有流程作用域。

@Named 
@FlowScoped("customerinfo") 
public class Customer implements Serializable { 
   //class body omitted 
} 

@FlowScoped注解有一个值属性,它必须与 bean 打算与之一起工作的流程的名称匹配(在这个例子中是customerinfo)。

此示例创建了一组向导风格的页面,用户可以在流程的多个页面中输入用户数据。

在第一页,我们输入姓名信息:

图片

在第二页,我们输入地址信息:

图片

在下一页,我们输入电话号码信息:

图片

最后,我们显示一个确认页面:

如果用户验证信息正确,我们将导航到customerinfo-return.xhtml页面外,否则我们将返回流程中的第一页,以便用户进行任何必要的更正。

注入 JSF 组件

JSF 规范早于 CDI。因此,许多 JSF 组件,如FacesContextExternalContext,必须通过static entry方法获取;这导致了难以阅读的样板代码。JSF 2.3 引入了通过 CDI 的@Inject注解注入 JSF 组件的能力,如下例所示:

package net.ensode.javaee8book.jsfarbitrarymess; 

import java.io.Serializable; 
import javax.faces.application.FacesMessage; 
import javax.faces.context.FacesContext; 
import javax.faces.view.ViewScoped; 
import javax.inject.Inject; 
import javax.inject.Named; 

@Named 
@ViewScoped 
public class ArbitraryMessageController implements Serializable { 

 @Inject FacesContext facesContext; 

    public void saveData() { 
        FacesMessage facesMessage = new  
         FacesMessage(FacesMessage.SEVERITY_INFO, 
         "Data saved successfully", "All Data successfully  
          saved."); 
        facesContext.addMessage(null, facesMessage); 
    } 
} 

在这个例子中,我们需要一个FacesContext的实例,以便我们可以向一个<h:messages>组件发送任意消息;从 JSF 2.3 开始,我们可以简单地使用 CDI 的@Inject注解来注解我们的FacesContext实例。

第五章,上下文和依赖注入,详细介绍了 CDI。

为了能够成功将 JSF 组件注入我们的 CDI 命名 bean 中,我们需要在我们的 WAR 文件的WEB-INF目录中添加一个 CDI beans.xml部署描述符,确保将其<beans>标签的bean-discovery-mode属性设置为 all:

<?xml version="1.0" encoding="UTF-8"?> 
<beans  

       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee  
                           http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" 
 bean-discovery-mode="all"> 
</beans> 

此外,我们还需要在我们的 WAR 文件中有一个被@FacesConfig注解的类(我们使用此注解来指定我们正在使用 JSF 2.3):

package net.ensode.javaee8book.jsfarbitrarymess.config; 

import javax.faces.annotation.FacesConfig; 

@FacesConfig(version = FacesConfig.Version.JSF_2_3) 
public class ConfigurationBean { 

} 

如前例所示,包含@FacesConfig注解的类不需要有任何代码。我们通过将FacesConfig.Version.JSF_2_3作为注解版本属性的值来指定我们正在使用 JSF 2.3。

除了说明如何注入 JSF 组件外,此示例还说明了我们之前未看到的 JSF 功能——通过FacesContextaddMessage()方法向<h:messages>组件发送任意消息的能力。接下来,我们将展示与前面 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>JSF Arbitrary Messages Demo</title> 
    </h:head> 
    <h:body> 
        <h:outputStylesheet library="css" name="styles.css"/> 
 <h:messages id="messages" /> 
        <h:form> 
            <h:panelGrid columns="2" 
             columnClasses="rightAlign,leftAlign"> 
                 <!-- Irrelevant markup omitted for brevity --> 
 <h:commandButton    
                actionListener="#     
                {arbitraryMessageController.saveData()}"                             
                 value="Save"> <f:ajax render="messages"/> </h:commandButton> 
            </h:panelGrid> 
        </h:form> 
    </h:body> 
</html> 

当用户点击由<h:commandButton>组件生成的按钮时,我们的 CDI 命名 bean 的saveData()方法被调用,这反过来创建了一个FacesMessage实例并将其传递给FacesContextaddMessage()方法,从而在浏览器中显示消息。

如果不明显,这个简单的示例实际上并没有保存任何数据;我们在这里展示的只是如何将任意消息传递给 JSF <h:messages>组件。

以下截图假设用户已经点击了保存按钮。顶部消息是我们对FacesContextaddMessage()方法的调用结果:

JSF WebSocket 支持

在典型的 Web 应用程序中,服务器总是响应来自浏览器的请求;服务器没有不响应请求就向客户端浏览器发送数据的方法。WebSocket技术提供了浏览器和服务器之间的全双工通信,允许服务器独立地向客户端发送数据,而无需响应请求。WebSocket 技术使得可以为 Web 开发出无数新的应用程序,包括更新股票行情、多人在线游戏和聊天应用程序。

尽管一些这类 Web 应用程序是在 WebSocket 出现之前开发的,但它们依赖于一些技巧来绕过 HTTP 协议的限制。有了 WebSocket,这些技巧就不再必要了。

传统上,编写利用 WebSocket 协议的应用程序需要大量的 JavaScript 代码。JSF 2.3 引入了 WebSocket 支持,并抽象出了大部分 JavaScript 底层代码,使我们能够专注于开发应用程序的业务逻辑。

以下示例展示了使用 JSF 2.3 WebSocket 支持开发的简单聊天应用程序。

首先,让我们看看一个名为 bean 的应用范围 CDI,它负责向所有浏览器客户端发送消息:

package net.ensode.javaee8book.jsfwebsocket; 

import java.io.Serializable; 
import javax.enterprise.context.ApplicationScoped; 
import javax.faces.push.Push; 
import javax.faces.push.PushContext; 
import javax.inject.Inject; 
import javax.inject.Named; 

@Named 
@ApplicationScoped 
public class JsfWebSocketMessageSender implements Serializable { 

    @Inject 
    @Push 
    private PushContext pushContext; 

    public void send(String message) { 
        System.out.println("Sending message: " + message); 
 pushContext.send(message); 
    } 
} 

如前例所示,为了通过 WebSockets 向客户端发送数据,我们需要注入一个实现javax.faces.push.PushContext接口的实例,并用@Push注解标注它。要实际向客户端发送消息,我们需要调用注入的PushContext实现中的send()方法;在我们的例子中,这是在 CDI named bean 的send()方法中完成的。

在我们的例子中,有一个会话范围的 CDI named bean 从用户那里获取输入,并将其传递给前面提到的应用范围 CDI named bean 的send()方法。我们的会话范围 CDI bean 如下所示:

package net.ensode.javaee8book.jsfwebsocket; 
import java.io.Serializable; 
import javax.enterprise.context.SessionScoped; 
import javax.inject.Inject; 
import javax.inject.Named; 

@Named 
@SessionScoped 
public class JsfWebSocketController implements Serializable { 

 @Inject private JsfWebSocketMessageSender jsfWebSocketMessageSender; 

    private String userName; 
    private String message; 

 public void sendMessage() { jsfWebSocketMessageSender.send(String.format("%s: %s",            
       userName, message)); } 

    //setters getters and less relevant methods omitted for brevity. 
} 

前面的类中的sendMessage()方法调用我们之前讨论的应用范围 CDI bean 的send()方法,传递用户名和要向所有浏览器广播的消息。上述sendMessage()方法是通过 Ajax 在用户点击相应页面上的按钮时调用的,如下所示:

    <h:body> 
 <f:websocket channel="pushContext" onmessage="socketListener" /> 

        <h:form prependId="false"> 
            <h:panelGrid columns="2"> 
                <h:outputLabel for="chatWindow" value="Chat  
                 Window:"/> 
                <textarea id="chatWindow" rows="10"/> 
                <h:outputLabel for="chatInput" value="Type 
                 something here:"/> 
                <h:inputText id="chatInput" 
                 value="#{jsfWebSocketController.message}"/> 
                <h:panelGroup/> 
                <h:commandButton 
                 actionListener="# 
                 {jsfWebSocketController.sendMessage()}"  
                  value="Send message"> 
                    <f:ajax execute="chatInput"  
                     render="chatWindow"/> 
                </h:commandButton> 
            </h:panelGrid> 
        </h:form> 

 <script type="text/javascript"> function socketListener(message, channel, event) { var textArea = document.getElementById('chatWindow'); var textAreaValue = textArea.value; if (textAreaValue.trim() !== '') { textAreaValue += "\n"; } textAreaValue += message; textArea.value = textAreaValue; textArea.scrollTop = textArea.scrollHeight; } </script> 
    </h:body> 

在前面的标记中,<f:websocket>标签是启用页面 WebSocket 支持所必需的。其channel属性的值将页面链接到服务器上相应的PushContext实例(在我们的例子中,它定义在应用范围的JsfWebSocketMessageSender CDI named bean 中)。按照惯例,channel属性的值必须与相应 CDI named bean 上的变量名匹配(在我们的例子中是pushContext)。

我们只展示了示例中最相关的部分;完整的示例可以从本书的 GitHub 仓库中下载,网址为github.com/dheffelfinger/Java-EE-8-Application-Development-Code-Samples

在构建和部署我们的应用程序之后,我们可以看到它的实际运行情况:

其他 JSF 组件库

除了标准的 JSF 组件库之外,还有许多第三方 JSF 标签库可供选择。以下表格列出了其中一些最受欢迎的:

标签库 发行商 许可 URL
ICEfaces ICEsoft MPL 1.1 www.icefaces.org
RichFaces Red Hat/JBoss LGPL www.jboss.org/richfaces
Primefaces Prime Technology Apache 2.0 www.primefaces.org

摘要

在本章中,我们介绍了如何使用 JavaServer Faces(Java EE 平台的标准组件框架)开发基于 Web 的应用程序。我们探讨了如何通过使用 Facelets 作为视图技术以及 CDI 命名豆来创建页面来编写一个简单的应用程序。我们还介绍了如何使用 JSF 的标准验证器、创建自定义验证器或编写validator方法来验证用户输入。此外,我们还介绍了如何自定义标准的 JSF 错误消息,包括消息文本和消息样式(字体、颜色等)。还介绍了如何开发启用 Ajax 的 JSF 页面,以及如何集成 JSF 和 HTML5。

第三章:使用 Java 持久化 API 进行对象关系映射

任何非平凡的 Java EE 应用程序都会将数据持久化到关系数据库中。在本章中,我们将介绍如何连接到数据库并执行 CRUD(创建、读取、更新、删除)操作。

Java 持久化 APIJPA)是标准的 Java EE 对象关系映射ORM)工具。我们将在本章中详细讨论这个 API。

本章涵盖的一些主题包括:

  • 通过 JPA 从数据库中检索数据

  • 通过 JPA 在数据库中插入数据

  • 通过 JPA 在数据库中更新数据

  • 通过 JPA 在数据库中删除数据

  • 通过 JPA Criteria API 编程构建查询

  • 通过 JPA 2.0 的 Bean Validation 支持自动化数据验证

客户数据库

本章中的示例将使用名为CUSTOMERDB的数据库。该数据库包含跟踪虚构商店客户和订单信息的表。该数据库使用 JavaDB 作为其 RDBMS,因为它与 GlassFish 捆绑在一起,但它可以轻松地适应任何其他 RDBMS。

本书代码下载中包含一个脚本,用于创建此数据库并预先填充其中的一些表。如何执行脚本以及如何添加连接池和数据源以访问它的说明也包含在下载中。

CUSTOMERDB数据库的模式在以下图中表示:

图片

如图中所示,数据库包含存储客户信息(如姓名、地址和电子邮件地址)的表。它还包含存储订单和项目信息的表。

ADDRESS_TYPES表将存储“家庭”、“邮寄”和“运输”等值,以区分ADDRESSES表中的地址类型;同样,TELEPHONE_TYPES表存储“手机”、“家庭”和“工作”等值。这两个表在创建数据库时预先填充,以及US_STATES表。

为了简单起见,我们的数据库只处理美国地址。

Java 持久化 API

Java 持久化 API (JPA)是在 Java EE 规范的第 5 版中引入的。正如其名称所暗示的,它用于将数据持久化到关系数据库管理系统。JPA 是 J2EE 中使用的实体 Bean 的替代品。Java EE 实体是常规的 Java 类;Java EE 容器知道这些类是实体,因为它们被@Entity注解装饰。让我们看看CUSTOMERDB数据库中CUSTOMER表的实体映射:

package net.ensode.javaee8book.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注解让任何其他 Java EE 兼容的应用程序服务器知道这个类是一个 JPA 实体。

@Table(name = "CUSTOMERS")注解让应用服务器知道将实体映射到哪个表。name元素的值包含实体映射到的数据库表的名称。此注解是可选的;如果类的名称映射到数据库表的名称,则不需要指定实体映射到的表。

@Id注解表示customerId字段映射到主键。

@Column注解将每个字段映射到表中的一列。如果字段的名称与数据库列的名称匹配,则不需要此注解。这就是为什么email字段没有被注解的原因。

EntityManager类(这实际上是一个接口;每个 Java EE 兼容的应用服务器都提供自己的实现)用于将实体持久化到数据库。以下示例说明了其用法:

package net.ensode.javaee8book.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.javaee8book.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接口的类的实例。这是通过使用@PersistenceContext注解装饰EntityManager变量来实现的。

然后,通过@Resource注解注入实现javax.transaction.UserTransaction接口的类的实例。这个对象是必要的,因为没有它,在数据库中持久化实体时,代码将抛出javax.persistence.TransactionRequiredException异常。

EntityManager执行许多数据库相关任务,例如在数据库中查找实体、更新它们或删除它们。

由于 JPA 实体是普通的 Java 对象POJOs),它们可以通过new运算符进行实例化。

调用setCustomerId()方法利用了自动装箱,这是 Java 语言在 JDK 1.5 中添加的一个特性。请注意,该方法接受一个java.lang.Long实例作为其参数,但我们使用的是long原始类型。多亏了这个特性,代码能够正确编译和执行。

EntityManager上对persist()方法的调用必须在事务中进行,因此需要通过调用UserTransaction上的begin()方法来启动一个事务。

然后,我们通过在entityManager上对两个先前在代码中填充的Customer类的实例调用persist()方法,向CUSTOMERS表插入两行新数据。

在将customercustomer2对象中的数据持久化后,我们通过在entityManager上调用find()方法在数据库中搜索具有四个主键的CUSTOMERS表中的行。这是通过调用我们想要获取的对象对应行的实体类作为其第一个参数来完成的。此方法大致等同于实体 bean 的 home 接口上的findByPrimaryKey()方法。

我们为customer2对象设置的键是 4,因此我们现在有一个该对象的副本。当我们将这位客户的数据最初插入数据库时,他的姓氏被拼错了。现在我们可以通过在customer3上调用setLastName()方法来纠正约翰逊先生的姓氏,然后我们可以通过调用entityManager.persist()来更新数据库中的信息。

然后,我们通过调用entityManager.remove()并传递customer对象作为参数来删除customer对象的信息。

最后,我们通过在userTransaction上调用commit()方法将更改提交到数据库。

为了使前面的代码按预期工作,必须在包含先前命名的 bean 的 WAR 文件中部署一个名为persistence.xml的 XML 配置文件。此文件必须放置在 WAR 文件内的WEB-INF/classes/META-INF/目录中。此文件的前面代码内容如下:

<?xml version="1.0" encoding="UTF-8"?> 
<persistence version="2.2"   xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.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表之间存在一对一的关系。这意味着每个客户在登录信息表中恰好对应一行。CUSTOMERS表和ORDERS表之间存在一对多关系。这是因为一个客户可以下多个订单,但每个订单只属于一个客户。此外,ORDERS表和ITEMS表之间存在多对多关系。这是因为一个订单可以包含多个项目,而一个项目可以出现在多个订单中。

在接下来的几节中,我们将讨论如何建立 JPA 实体之间的关系。

一对一关系

当一个实体的实例可以对应零个或一个另一个实体的实例时,就会发生一对一关系。

一对一实体关系可以是双向的(每个实体都了解这种关系)或单向的(只有其中一个实体了解这种关系)。在CUSTOMERDB数据库中,LOGIN_INFO表和CUSTOMERS表之间的一对一映射是单向的,因为LOGIN_INFO表有一个指向CUSTOMERS表的外键,但反之则不然。正如我们很快就会看到的,这个事实并不会阻止我们在这两个实体之间创建一个双向的一对一关系。

这里可以看到映射到LOGIN_INFO表的LoginInfo实体的源代码:

package net.ensode.javaee8book.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注解;这使应用服务器知道这个实体与Customer实体之间存在一对一的关系。customer字段还装饰了@JoinColumn注解。这个注解让容器知道LOGIN_INFO表中的哪一列是对应CUSTOMER表主键的外键。由于LOGIN_INFO表(LoginInfo实体映射到的表)有一个指向CUSTOMER表的外键,因此LoginInfo实体拥有这个关系。如果这个关系是单向的,我们就不需要对Customer实体做任何修改。然而,由于我们希望这两个实体之间有一个双向关系,我们需要在Customer实体中添加一个LoginInfo字段,以及相应的 getter 和 setter 方法。

正如我们之前提到的,为了使CustomerLoginInfo实体之间的一对一关系双向,我们需要对Customer实体进行一些简单的修改:

package net.ensode.javaee8book.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 LoginInfo getLoginInfo() { return loginInfo; } public void setLoginInfo(LoginInfo loginInfo) { this.loginInfo = loginInfo; } 
   //Additional setters and getters omitted for brevity 
} 

为了使Customer实体中的一对一关系双向,我们只需要在该实体中添加一个LoginInfo字段,以及相应的 setter 和 getter 方法。loginInfo字段被@OneToOne注解装饰。由于Customer实体不拥有这个关系(它映射的表没有对应表的键外键),@OneToOne注解的mappedBy元素需要被添加。此元素指定对应实体中哪个字段有关系的另一端。在这个特定的情况下,LoginInfo实体中的客户字段对应于这个一对一关系的另一端。

以下 Java 类说明了前面实体使用的情况:

package net.ensode.javaee8book.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.javaee8book.entityrelationship.entity.Customer; 
import net.ensode.javaee8book.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实体的实例,并用一些数据填充它。然后,我们通过调用EntityManagerfind()方法从数据库中获取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.javaee8book.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.javaee8book.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字段和相关联的gettersetter方法。特别值得注意的是装饰此字段的@OneToMany注解。mappedBy属性必须与对应实体中关系many部分的相应字段名称匹配。简单来说,mappedBy属性值必须与关系另一端的 bean 中用@ManyToOne注解装饰的字段名称匹配。

以下示例代码说明了如何将一对多关系持久化到数据库中:

package net.ensode.javaee8book.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.javaee8book.entityrelationship.entity.Customer; 
import net.ensode.javaee8book.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.javaee8book.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注解装饰。正如其名称所暗示的那样,这个注解让应用程序服务器知道哪个表被用作连接表以在两个实体之间创建多对多关系。这个注解有三个相关元素:名称元素,它定义了连接表的名字,以及joinColumnsinverseJoinColumns元素,它们定义了作为连接表中指向实体主键的外键的列。joinColumnsinverseJoinColumns元素的值又是另一个注解:@JoinColumn注解。这个注解有两个相关元素:名称元素,它定义了连接表中的列名,以及referencedColumnName元素,它定义了实体表中的列名。

Item实体是一个简单的实体,映射到CUSTOMERDB数据库中的ITEMS表:

package net.ensode.javaee8book.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 Collection<Order> getOrders() { return orders; } public void setOrders(Collection<Order> orders) { this.orders = orders; } 

  //additional setters and getters removed for brevity 
} 

就像一对一和多对一关系一样,多对多关系可以是单向的也可以是双向的。由于我们希望OrderItem实体之间的多对多关系是双向的,所以我们添加了一个Collection<Order>字段,并用@ManyToMany注解装饰它。由于Order实体中相应的字段已经定义了连接表,因此在这里不需要再次定义。包含@JoinTable注解的实体被称为拥有关系;在多对多关系中,任一实体都可以拥有关系。在我们的例子中,Order实体拥有它,因为它的Collection<Item>字段被@JoinTable注解装饰。

就像一对一和多对一关系一样,双向多对多关系非拥有方上的@ManyToMany注解必须包含一个mappedBy元素,以指示拥有实体中定义关系的哪个字段。

现在我们已经看到了在OrderItem实体之间建立双向多对多关系所必需的更改,我们可以在下面的示例中看到这种关系是如何运作的:

package net.ensode.javaee8book.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.javaee8book.entityrelationship.entity.Item; 
import net.ensode.javaee8book.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表创建了两个行,这是ORDERSITEMS表之间的连接表。

组合主键

CUSTOMERDB数据库中的大多数表都有一个列,其唯一目的是作为主键(这种类型的主键有时被称为代理主键或人工主键)。然而,一些数据库并不是这样设计的;相反,数据库中一个已知在行之间是唯一的列被用作主键。如果没有列的值在行之间不是保证唯一的,那么两个或更多列的组合被用作表的唯一主键。可以通过使用primary key类将此类主键映射到 JPA 实体。

CUSTOMERDB数据库中有一个表没有代理主键:ORDER_ITEMS表。除了作为ORDERSITEMS表的连接表,以及这两个表的外键之外,这个表还有一个额外的列,称为ITEM_QTY;这个列存储每个订单中每个项目的数量。由于这个表没有代理主键,映射到它的 JPA 实体必须有一个自定义的primary key类。在这个表中,ORDER_IDITEM_ID列的组合必须是唯一的,因此这是一个复合主键的好组合:

package net.ensode.javaee8book.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(); 
    } 
  } 
} 

自定义primary key类必须满足以下要求:

  • 该类必须是公开的

  • 它必须实现java.io.Serializable

  • 它必须有一个不接受任何参数的公开构造函数

  • 它的字段必须是publicprotected

  • 它的字段名称和类型必须与实体的匹配

  • 它必须重写java.lang.Object类中定义的默认hashCode()equals()方法

前面的OrderPK类满足所有这些要求。它还有一个方便的构造函数,该构造函数接受两个Long对象来初始化其orderIditemId字段。这个构造函数是为了方便而添加的;这不是将此类用作主键类的先决条件。

当实体使用自定义primary key类时,它必须被@IdClass注解装饰。由于OrderItem类使用OrderItemPK作为其自定义primary key类,它必须被上述注解装饰:

package net.ensode.javaee8book.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注解装饰,表示对应的主键类。第二个不同之处在于,前面的实体有多个字段被@Id注解装饰。由于这个实体有一个复合主键,每个作为主键一部分的字段都必须被这个注解装饰。

获取具有复合主键的实体引用与获取由单个字段组成的主键的实体引用没有太大区别。以下示例演示了如何做到这一点:

package net.ensode.javaee8book.compositeprimarykeys.namedbean; 

import javax.enterprise.context.RequestScoped; 
import javax.inject.Named; 
import javax.persistence.EntityManager; 
import javax.persistence.PersistenceContext; 
import net.ensode.javaee8book.compositeprimarykeys.entity.OrderItem; 
import net.ensode.javaee8book.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.javaee8book.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.javaee8book.jpql.entity.UsState; 

@Named 
@RequestScoped 
public class SelectQueryDemoBean { 

    @PersistenceContext 
    private EntityManager entityManager; 

    private Stream<UsState> matchingStatesStream; 
    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%"); matchingStatesStream = query.getResultStream(); if (matchingStatesStream != null) { matchingStatesList =
              matchingStatesStream.collect(Collectors.toList()); } 
        } 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 查询的String作为参数。此方法返回一个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()方法作为替代;此方法返回一个必须转换为适当实体的Object

上述示例使用 LIKE 操作符来查找以单词 "New" 开头的实体。这是通过将查询的命名参数替换为值 "New%" 来实现的。参数值末尾的百分号表示 "New" 字词之后的任意数量的字符都将与表达式匹配。百分号可以在参数值中的任何位置使用,例如,值为 "%Dakota" 的参数将匹配任何以 "Dakota" 结尾的实体,值为 "A%a" 的参数将匹配任何以大写字母 "A" 开头并以小写字母 "a" 结尾的州。参数值中可以有多个百分号。下划线符号(_)可以用来匹配单个字符;所有关于百分号的规则也适用于下划线。

除了 LIKE 操作符之外,还有其他操作符可以用来从数据库中检索实体:

  • = 操作符将检索操作符左侧字段值与操作符右侧值完全匹配的实体

  • > 操作符将检索操作符左侧字段值大于操作符右侧值的实体

  • < 操作符将检索操作符左侧字段值小于操作符右侧值的实体

  • >= 操作符将检索操作符左侧字段值大于或等于操作符右侧值的实体

  • <= 操作符将检索操作符左侧字段值小于或等于操作符右侧值的实体

所有的上述操作符与 SQL 中的等效操作符工作方式相同。就像在 SQL 中一样,这些操作符可以与 ANDOR 操作符结合使用。与 AND 操作符结合的条件如果两个条件都为真则匹配;与 OR 操作符结合的条件如果至少有一个条件为真则匹配。

如果我们打算多次使用查询,它可以被存储在命名查询中。命名查询可以通过在相关实体类上使用 @NamedQuery 注解来定义。此注解有两个元素:一个 name 元素用于设置查询的名称,一个 query 元素用于定义查询本身。要执行命名查询,必须在 EntityManager 实例上调用 createNamedQuery() 方法。此方法接受一个包含查询名称的 String 作为其唯一参数,并返回一个 javax.persistence.Query 实例。

除了检索实体外,JPQL 还可以用来修改或删除实体。然而,实体修改和删除可以通过 EntityManager 接口以编程方式完成;这样做产生的代码通常比使用 JPQL 时更易于阅读。因此,我们不会介绍通过 JPQL 进行实体修改和删除。对编写修改和删除实体的 JPQL 查询感兴趣的读者,以及对 JPQL 感兴趣的读者,应鼓励查阅 Java Persistence 2.2 规范。该规范可从 jcp.org/en/jsr/detail?id=338 下载。

Criteria API

2.0 规范中 JPA 的主要新增功能之一是引入了 Criteria API。Criteria API 是作为 JPQL 的补充而设计的。

虽然 JPQL 非常灵活,但它有一些问题使得使用它比必要的更困难。首先,JPQL 查询以字符串形式存储,编译器无法验证 JPQL 语法。此外,JPQL 不是类型安全的:我们可能编写一个 JPQL 查询,其中我们的 where 子句可能有一个字符串值用于数值属性,而我们的代码编译和部署仍然很好。

为了克服前一段所述的 JPQL 限制,规范在 2.0 版本中引入了 Criteria API 到 JPA。Criteria API 允许我们以编程方式编写 JPA 查询,而无需依赖于 JPQL。

以下代码示例说明了如何在我们的 Java EE 应用程序中使用 Criteria API:

package net.ensode.javaee8book.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.javaee8book.criteriaapi.entity.UsState; 

@Named 
@RequestScoped 
public class CriteriaApiDemoBean { 

    @PersistenceContext 
    private EntityManager entityManager; 

    private List<UsState> matchingStatesList; 
    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); matchingStatesStream = typedQuery.getResultStream(); if (matchingStatesStream != null) { matchingStatesList =
             matchingStatesStream.collect(Collectors.toList()); } 

        } 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 实体的 persistent 属性。这正是我们在示例中的下一行所做的事情。在我们的例子中,我们获取了一个 SingularAttribute 的实例,它映射到我们 JPA 实体中的一个简单、单一属性。EntityType 有方法可以获取映射到集合、集合、列表和映射的属性。获取这些属性类型与获取 SingularAttribute 非常相似,因此我们不会直接介绍这些。有关更多信息,请参阅 Java EE 8 API 文档,网址为 javaee.github.io/javaee-spec/javadocs/

正如我们在示例中所看到的,SingularAttribute 包含两个泛型类型参数。第一个参数指定了我们正在处理的 JPA 实体,第二个参数表示属性的类型。我们通过在我们的 EntityType 实现上调用 getDeclaredSingularAttribute() 方法并传递属性名称(如在我们 JPA 实体中声明的那样)作为字符串来获取我们的 SingularAttribute

一旦我们获得了我们的 SingularAttribute 实现,我们需要通过在我们的 Root 实例中调用 get() 方法,并传递我们的 SingularAttribute 作为参数,来获取一个导入的 javax.persistence.criteria.Path 实现。

在我们的例子中,我们将获取所有新的美国州列表;也就是说,所有以 New 开头的州。当然,这是一个“like”条件的任务。我们可以通过在我们的 CriteriaBuilder 实现上调用 like() 方法来使用 criteria API 完成此操作。like() 方法将我们的 Path 实现作为其第一个参数,并将要搜索的值作为其第二个参数。

CriteriaBuilder 有许多与 SQL 和 JPQL 子句类似的方法,例如 equals()greaterThan()lessThan()and()or()(有关完整列表,请参阅 Java EE 8 文档,网址为 javaee.github.io/javaee-spec/javadocs/)。这些方法可以通过 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.javaee8book.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的类的实例。

下一步是获取一个实现Root的类的实例,通过在我们的CriteriaUpdate实例上调用from()方法来实现。

我们随后在CriteriaUpdate上调用set()方法来指定更新后我们的行将具有的新值;set()方法的第一个参数必须是一个与Entity类中属性名称匹配的String,第二个参数必须是新值。

在这一点上,我们通过在CriteriaUpdate上调用where()方法并传递由CriteriaBuilder中调用的equal()方法返回的Predicate来构建 where 子句。

然后,我们通过在EntityManager上调用createQuery()并传递我们的CriteriaUpdate实例作为参数来获取Query实现。

最后,我们通过在Query实现上调用executeUpdate()来执行我们的查询。

使用 Criteria API 删除数据

除了通过 Criteria API 添加对数据更新的支持外,JPA 2.1 还增加了使用新的CriteriaDelete接口批量删除数据库行的能力。以下代码片段说明了其用法:

package net.ensode.javaee8book.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.javaee8book.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注解来防止我们的实体firstNamelastName在持久化时带有null值。我们还使用了@Size注解来限制这些字段的长度范围。

这就是我们利用 JPA 中的 Bean Validation 所需要做的全部。如果我们的代码尝试持久化或更新一个未通过声明的验证的实体实例,将抛出一个类型为javax.validation.ConstraintViolationException的异常,并且实体不会被持久化。

如我们所见,Bean Validation 几乎自动化了数据验证,使我们免于手动编写验证代码。

除了前一个例子中讨论的两个注解之外,javax.validation.constraints包还包含几个额外的注解,我们可以使用它们来自动化 JPA 实体的验证。请参阅 Java EE 8 API 文档javaee.github.io/javaee-spec/javadocs/以获取完整的列表。

最后的注意事项

在本章的示例中,我们展示了从作为控制器的 CDI 命名 bean 直接执行数据库访问。我们这样做是为了清晰地传达观点,而不陷入细节;然而,通常情况下,这并不是一个好的做法。数据库访问代码应该封装在数据访问对象DAOs)中。

关于 DAO 设计模式的更多信息,请参阅www.oracle.com/technetwork/java/dao-138818.html.

在使用 模型-视图-控制器MVC)设计模式时,命名豆通常扮演控制器和/或模型的角色,这种做法非常普遍,已经成为 Java EE 应用的事实标准。

关于 MVC 设计模式的更多信息,请参阅 www.oracle.com/technetwork/java/mvc-140477.html.

此外,我们选择不在示例中展示任何用户界面代码,因为这与当前的主题无关。然而,本章的代码下载包括调用本章中命名豆的 JSF 页面,并在命名豆调用完成后显示确认页面。

摘要

本章介绍了如何通过 Java 持久化 API(JPA)访问数据库中的数据。

我们还介绍了如何通过使用 @Entity 注解将 Java 类标记为 JPA 实体,以及如何通过 @Table 注解将实体映射到数据库表。我们还介绍了如何通过 @Column 注解将实体字段映射到数据库列,以及通过 @Id 注解声明实体的主键。

使用 javax.persistence.EntityManager 接口查找、持久化和更新 JPA 实体也包含在内。

同时也涵盖了在 JPA 实体之间定义单向和双向的一对一、一对多和多对多关系。

此外,我们还探讨了如何通过开发自定义主键类来使用 JPA 组合主键。

此外,我们还介绍了如何使用 Java 持久化查询语言(JPQL)从数据库中检索实体。

我们讨论了额外的 JPA 功能,例如 Criteria API,它允许我们通过编程方式构建 JPA 查询;Metamodel API,它允许我们在使用 JPA 时利用 Java 的类型安全;以及 Bean Validation,它允许我们通过简单地注解我们的 JPA 实体字段来轻松验证输入。

第四章:企业 JavaBeans

企业 JavaBeans 是封装应用程序业务逻辑的服务端组件。企业 JavaBeansEJB)通过自动处理事务管理和安全性来简化应用程序开发。企业 JavaBean 有两种类型:会话 Bean,执行业务逻辑,和消息驱动 Bean,充当消息监听器。

熟悉 J2EE 先前版本的读者会注意到,在上一段中并未提及实体 Bean。在 Java EE 5 中,为了支持 Java 持久化 API(JPA),实体 Bean 已被弃用。尽管如此,为了向后兼容,实体 Bean 仍然得到支持;然而,进行对象关系映射(ORM)的首选方式是通过 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 接口(或两者都有),以及一个 XML 部署描述符。正如我们将在本章中看到的,Java EE 极大地简化了 EJB 开发。

一个简单的会话 Bean

以下示例演示了一个非常简单的会话 Bean。:

package net.ensode.javaeebook;

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 实例时,池中的一个实例会被提供给这个客户端应用程序。

有状态会话 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.javaeebook; 

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 文件中并部署它们。如何部署 EJB JAR 文件取决于我们使用的是哪种应用服务器。然而,大多数现代应用服务器都有一个autodeploy目录;在大多数情况下,我们可以简单地复制我们的 EJB JAR 文件到这个目录。请查阅您的应用服务器文档以找到其autodeploy目录的确切位置。

实现 EJB 客户端代码

现在我们已经看到了会话 Bean 及其相应的业务接口,让我们看看一个客户端示例应用程序:

package net.ensode.javaeebook; 

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 容器知道这个变量是一个会话 Bean 的业务接口。然后 EJB 容器注入一个业务接口的实现供客户端代码使用。

由于我们的客户端是一个独立的应用程序(而不是像 WAR 文件或另一个 EJB JAR 文件这样的 Java EE 工件),它实际上并没有部署到应用服务器。为了使其能够访问部署到服务器的代码,它必须能够访问应用服务器的客户端库。如何实现这一点的步骤因应用服务器而异。当使用 GlassFish 时,我们的客户端代码必须放入一个 JAR 文件中并通过appclient实用程序执行。这个实用程序可以在[glassfish 安装目录]/glassfish/bin/中找到。假设这个目录在 PATH 环境变量中,并且假设我们将我们的客户端代码放入一个名为simplesessionbeanclient.jar的 JAR 文件中,我们将在命令行中键入以下命令来执行前面的客户端代码:

appclient -client simplesessionbeanclient-jar-with-dependencies.jar

执行上述命令会产生以下控制台输出:

If you don't see this, it didn't work!

SimpleSession is of type: net.ensode.javaeebook._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.javaeebook; 

import javax.ejb.Remote; 

@Remote 
public interface CustomerDao 
{ 
  public void saveCustomer(Customer customer); 

  public Customer getCustomer(Long customerId); 

  public void deleteCustomer(Customer customer); 
} 

如我们所见,这是一个实现三个方法的远程接口;saveCustomer()方法将客户数据保存到数据库中,getCustomer()方法从数据库中获取客户数据,而deleteCustomer()方法从数据库中删除客户数据。所有这些方法都接受我们第三章中开发的Customer实体实例作为参数,使用 JPA 进行对象关系映射

现在我们来看看实现上述业务接口的会话 Bean。正如我们即将看到的,在会话 Bean 和普通的Java对象中实现 JPA 代码的方式之间有一些区别:

package net.ensode.javaeebook; 

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 方法隐式地是事务性的,我们不需要做任何事情来使它们成为那样。这种默认行为被称为容器管理事务。容器管理事务将在本章后面详细讨论。

如第三章中所述,使用 Java 持久化 API 进行对象关系映射,当 JPA 实体在一个事务中检索并在另一个事务中更新时,需要调用EntityManager.merge()方法来更新数据库中的数据。在这种情况下调用EntityManager.persist()将导致“无法持久化分离对象”异常。

从 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:

package net.ensode.javaeebook.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 的客户端,我们需要将其打包成 WARWeb 归档)文件并部署,以便使用它。

单例会话 bean

Java EE 6 中引入的一种新的会话 bean 类型是单例会话 bean。每个单例会话 bean 在应用服务器中只存在一个实例。

单例会话 bean 对于缓存数据库数据很有用。在单例会话 bean 中缓存常用数据可以提高性能,因为它大大减少了访问数据库的次数。常见的模式是在我们的 bean 中有一个带有 @PostConstruct 注解的方法;在这个方法中,我们检索我们想要缓存的数据。然后我们提供一个设置方法供 bean 的客户端调用。以下示例说明了这种技术:

package net.ensode.javaeebook.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.javaeebook.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 方法异步的唯一方法是使用消息驱动 bean(将在下一节中讨论)。尽管消息驱动 bean 编写起来相对简单,但它们在使用之前确实需要一些配置,例如设置 JMS 消息队列或主题。

EJB 3.1 引入了 @Asynchronous 注解,它可以用来标记会话 bean 中的方法为异步。当 EJB 客户端调用异步方法时,控制权立即返回客户端,无需等待方法完成。

异步方法只能返回voidjava.util.concurrent.Future接口的实现。以下示例说明了这两种情况:

package net.ensode.javaeebook.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注解装饰该方法,然后像往常一样从客户端代码中调用它。

如果我们需要返回值,则此值需要包装在java.util.concurrent.Future接口的实现中。Java EE API 以javax.ejb.AsyncResult类的形式提供了一个便利的实现。Future接口和AsyncResult类都使用泛型;我们需要将这些实物的返回类型指定为类型参数。

Future接口有几个我们可以用来取消异步方法执行、检查方法是否完成、获取方法的返回值或检查方法是否被取消的方法。以下表格列出了这些方法:

方法 描述
cancel(boolean mayInterruptIfRunning) 取消方法执行。如果boolean参数为 true,则此方法将尝试取消方法执行,即使它已经在运行。
get() 将返回方法的“未包装”返回值;返回值将是方法返回的Future接口实现中type参数的类型。
get(long timeout, TimeUnit unit) 将尝试方法的“未包装”返回值;返回值将是方法返回的Future接口实现中type参数的类型。此方法将阻塞由第一个参数指定的时间量。等待时间的单位由第二个参数确定;TimeUnit枚举具有 NANOSECONDS、MILLISECONDS、SECONDS、MINUTES 等常量。有关完整列表,请参阅其 Javadoc 文档。
isCancelled() 如果方法已被取消,则返回 true,否则返回 false。
isDone() 如果方法已执行完成,则返回 true,否则返回 false。

如我们所见,@Asynchronous注解使得在不设置消息队列或主题的开销下进行异步调用变得非常容易。这无疑是 EJB 规范中的一个受欢迎的补充。

消息驱动 bean

消息驱动 bean 的目的是根据所使用的消息传递域从 Java 消息服务(JMS)队列或 JMS 主题中消费消息(请参阅第八章,Java 消息服务)。消息驱动 bean 必须用@MessageDriven注解装饰;此注解的mappedName属性必须包含 bean 将从中消费消息的 JMS 消息队列或 JMS 消息主题的Java 命名和目录接口JNDI)名称。以下示例说明了一个简单的消息驱动 bean:

package net.ensode.javaeebook; 

import javax.ejb.MessageDriven; 
import javax.jms.JMSException; 
import javax.jms.Message; 
import javax.jms.MessageListener; 
import javax.jms.TextMessage; 

@MessageDriven(mappedName = "jms/JavaEE8BookQueue") 
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 必须装饰 @MessageDriven 注解。它们监听在 @MessageDriven 接口的 mappedName 属性中定义的队列或主题上的消息(在这个例子中是 jms/JavaEEBookQueue)。

建议但不是必须的消息驱动 Bean 实现接口 javax.jms.MessageListener;然而,消息驱动 Bean 必须有一个名为 onMessage() 的方法,其签名与前面的示例相同。

客户端应用程序永远不会直接调用消息驱动 Bean 的方法;相反,它们将消息放入消息队列或主题,然后 Bean 消费这些消息并相应地执行。前面的示例只是将消息打印到标准输出;由于消息驱动 Bean 在 EJB 容器内执行,标准输出被重定向到日志。如果使用 GlassFish,服务器日志文件可以在 [GlassFish 安装目录]/glassfish/domains/domain1/logs/server.log 中找到。

企业 JavaBeans 中的事务

正如我们在本章前面提到的,默认情况下,所有 EJB 方法都会自动包装在一个事务中。这种默认行为被称为容器管理事务,因为事务是由 EJB 容器管理的。应用程序开发者也可以选择自己管理事务,这可以通过使用 Bean 管理事务来实现。这两种方法将在以下章节中讨论。

容器管理的事务

由于 EJB 方法默认是事务性的,当从已经在事务中的客户端代码调用 EJB 方法时,我们会遇到一个有趣的困境。EJB 容器应该如何表现?它应该挂起客户端事务,在一个新的事务中执行其方法,然后恢复客户端事务吗?或者它不应该创建新的事务,而是将方法作为客户端事务的一部分执行?或者它应该抛出异常?

默认情况下,如果 EJB 方法被已经在事务中的客户端代码调用,EJB 容器将简单地执行会话 Bean 方法作为客户端事务的一部分。如果这不是我们需要的操作,我们可以通过在方法上装饰 @TransactionAttribute 注解来改变它。这个注解有一个 value 属性,它决定了当会话 Bean 方法在现有事务中调用或在外部任何事务中调用时,EJB 容器的行为。value 属性的值通常是定义在 javax.ejb.TransactionAttributeType 枚举中的常量。

下表列出了 @TransactionAttribute 注解的可能值:

描述
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.javaeebook; 

//imports omitted 

@Stateless 
public class CustomerDaoRollbackBean implements CustomerDaoRollback 
{ 
  @Resource     
  private EJBContext ejbContext; 

  @PersistenceContext 
  private EntityManager entityManager; 

  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 method omitted for brevity. 

} 

在这个版本的 DAO 会话 bean 版本中,我们删除了saveCustomer()方法,并将saveNewCustomer()updateCustomer()方法设置为公共。现在,每个这些方法都会检查我们试图执行的操作的customerId字段是否设置正确(对于插入是null,对于更新不是null)。它还会检查要持久化的对象不是null。如果任何检查导致无效数据,该方法将简单地通过在注入的EJBContext实例上调用setRollBackOnly()方法来回滚交易,并且不更新数据库。

Bean 管理的交易

正如我们所见,容器管理的交易使得编写被交易包裹的代码变得极其简单。我们不需要做任何特别的事情来达到这种效果;事实上,一些开发者在开发会话 bean 时甚至可能没有意识到他们正在编写具有交易性质的代码。容器管理的交易涵盖了我们将遇到的大部分典型用例。然而,它们确实有一个限制;每个方法可以包裹在一个单独的交易中,或者不包裹在交易中。使用容器管理的交易,无法实现生成多个交易的方法,但可以通过使用 bean 管理的交易来完成:

package net.ensode.javaeebook; 

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; 
import javax.persistence.PersistenceContext; 
import javax.sql.DataSource; 
import javax.transaction.UserTransaction; 

@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 生命周期

我们可以在会话 Bean 中注解方法,以便 EJB 容器在 Bean 生命周期的特定点自动调用它们。例如,我们可以在 Bean 创建后立即调用一个方法,或者在其销毁前调用一个方法。

在解释可用于实现生命周期方法的注解之前,有必要简要说明会话 Bean 的生命周期。有状态会话 Bean 的生命周期与无状态或单例会话 Bean 的生命周期不同。

有状态会话 Bean 的生命周期包含三个状态:不存在、就绪和被动:

在有状态会话 Bean 部署之前,它处于不存在状态。在成功部署后,EJB 容器会对 Bean 进行任何所需的依赖注入,然后进入就绪状态。此时,Bean 准备好被客户端应用程序调用其方法。

当有状态会话 Bean 处于就绪状态时,EJB 容器可能会决定将其钝化,即将其从主内存移动到二级存储,当这种情况发生时,Bean 进入被动状态。

如果有状态会话 Bean 的一段时间内没有被访问,EJB 容器将把 Bean 设置为不存在状态。Bean 在内存中保持多长时间才被销毁因应用服务器而异,通常是可以配置的。默认情况下,GlassFish 将在 90 分钟的不活动后把有状态会话 Bean 发送到不存在状态。当将我们的代码部署到 GlassFish 时,这个默认值可以通过访问 GlassFish 管理控制台;展开右侧树中的配置节点;展开服务器配置节点;点击 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 分钟。

装饰了@PostActivate注解的有状态会话 Bean 中的任何方法将在有状态会话 Bean 被激活后立即被调用。同样,装饰了@PrePassivate注解的任何方法将在有状态会话 Bean 被钝化之前被调用。

当一个处于“就绪”状态的有状态会话 Bean 超时并被发送到“不存在”状态时,任何装饰了@PreDestroy注解的方法将被执行。如果会话 Bean 处于“钝化”状态并且超时,则不会执行装饰了@PreDestroy注解的方法。此外,如果会话 Bean 的客户端执行了任何装饰了@Remove注解的方法,则执行装饰了@PreDestroy注解的方法,并且 Bean 将被标记为垃圾回收。

@PostActivate@PrePassivate@Remove注解仅对有状态会话 Bean 有效。@PreDestroy@PostConstruct注解对有状态会话 Bean、无状态会话 Bean 和消息驱动 Bean 都有效。

无状态和单例会话 Bean 的生命周期

无状态或单例会话 Bean 的生命周期只包含“不存在”和“就绪”状态:

图片

无状态和单例会话 Bean 永远不会被钝化。无状态或单例会话 Bean 的方法可以装饰@PostConstruct@PreDestroy注解。就像在有状态会话 Bean 中一样,任何装饰了@PostConstruct注解的方法将在会话 Bean 从“不存在”状态转换为“就绪”状态时执行,任何装饰了@PreDestroy注解的方法将在无状态会话 Bean 从“就绪”状态转换为“不存在”状态时执行。由于无状态和单例会话 Bean 永远不会被钝化,因此无状态会话 Bean 中的任何@PrePassivate@PostActivate注解都将被 EJB 容器简单地忽略。

大多数应用服务器允许我们配置在销毁空闲的无状态或单例会话 Bean 之前等待多长时间。如果使用 GlassFish,我们可以通过管理 Web 控制台来控制无状态会话 Bean(以及消息驱动 Bean)的生命周期管理:

前面的截图显示的设置允许我们控制无状态会话豆的生命周期:

  • 初始和最小池大小指的是池中最少的豆的数量

  • 最大池大小指的是池中豆的最大数量

  • 池大小调整数量指的是在池空闲超时到期时从池中移除多少个豆。

  • 池空闲超时指的是在从池中移除豆之前等待的不活动秒数

前面的设置影响所有池化(无状态会话豆和消息驱动豆)的 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> 

glassfish-ejb-jar.xml包含与 Web 控制台中相应设置等效的 XML 标签:

  • <steady-pool-size>对应于 GlassFish Web 控制台中的初始和最小池大小

  • <max-pool-size>对应于 GlassFish Web 控制台中的最大池大小

  • <resize-quantity>对应于 GlassFish Web 控制台中的池大小调整数量

  • <pool-idle-timeout-in-seconds>对应于 GlassFish Web 控制台中的池空闲超时

消息驱动豆生命周期

就像无状态会话豆一样,消息驱动豆只包含“不存在”和“就绪”状态:

消息驱动豆可以有被@PostConstruct@PreDestroy方法装饰的方法。被@PostConstruct装饰的方法在豆进入就绪状态之前执行。被@PreDestroy注解的方法在豆进入不存在状态之前执行。

EJB 计时器服务

无状态会话豆和消息驱动豆可以有一个在固定时间间隔定期执行的方法。这可以通过使用 EJB 计时器服务来实现。以下示例说明了如何利用此功能:

package net.ensode.javaeebook; 

//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实例作为其第一个参数;此参数用于指示定时器应该到期的第一次时间("trfgo off")。在示例中,我们选择使用一个新的Date类实例,这使得定时器立即到期。createTimer()方法的第二个参数是在定时器再次到期之前等待的时间,以毫秒为单位。在上面的示例中,定时器每五秒到期一次。createTimer()方法的第三个参数可以是实现java.io.Serializable接口的任何类的实例。由于单个 EJB 可以同时执行多个定时器,因此此第三个参数用于唯一标识每个定时器。如果我们不需要标识定时器,可以将 null 作为此参数的值。

调用TimerService.createTimer()的 EJB 方法必须从 EJB 客户端调用。将此调用放置在 EJB 方法中(使用@PostConstruct注解以在 bean 处于 Ready 状态时自动启动定时器)将导致抛出IllegalStateException异常。

我们可以通过调用其cancel()方法来停止定时器。无法直接获取与 EJB 关联的单个定时器;我们需要做的是在链接到 EJB 的TimerService实例上调用getTimers()方法。此方法将返回一个包含与 EJB 关联的所有定时器的集合;然后我们可以遍历集合,通过调用其getInfo()方法来取消正确的定时器。此方法将返回我们传递给createTimer()方法的Serializable对象。

最后,任何被@Timeout注解装饰的 EJB 方法将在定时器到期时执行。被此注解装饰的方法必须返回 void 类型,并接受一个类型为javax.ejb.Timer的单个参数。在我们的例子中,该方法只是将一条消息写入服务器日志。

下面的类是前面 EJB 的独立客户端:

package net.ensode.javaeebook; 

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.javaeebook.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.javaeebook.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.javaeebook.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.javaeebook.EjbTimerExampleBean] [tid: _ThreadID=150 _ThreadName=__ejb-thread-pool4] [timeMillis: 1377564301156] [levelValue: 800] [[

      This message was triggered by :Timer 2 at 1377564301156]]

每当定时器中的一个到期时,都会创建这些条目。

基于日历的 EJB 定时器表达式

上一个示例有一个缺点:会话 bean 中的startTimer()方法必须由客户端调用才能启动定时器。这种限制使得定时器在 bean 部署后立即启动变得困难。

Java EE 6 引入了基于日历的 EJB 定时器表达式。基于日历的表达式允许我们的会话 beans 中的一个或多个方法在特定的日期和时间执行。例如,我们可以配置我们的一个方法在每晚 8:10 pm 执行,这正是我们下一个示例所做的事情:

package com.ensode.javaeebook.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"(因为小时属性是基于 24 小时的,小时 20 相当于晚上 8:00),来设置我们的方法在晚上 8:10 pm 执行。

@Schedule 注解有几个其他属性,允许在指定方法何时执行时具有很大的灵活性。例如,我们可以有一个方法在每月的第三个星期五执行,或者在月底执行,等等。

下表列出了 @Schedule 注解中所有允许我们控制注解方法何时执行的属性:

属性 描述 示例值 默认值
dayOfMonth 月份中的日期 "3": 月份的第三天 "Last": 月份的最后一天"-2": 月份结束前两天"1st Tue": 月份的第一个星期二 "*"
dayOfWeek 周中的日期 "3": 每周三"Thu": 每周四 "*"
hour 一天中的小时(基于 24 小时制) "14": 下午 2:00 "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.javaeebook; 

// 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 注解装饰,则方法级别的注解具有优先权。

创建角色的过程因应用服务器而异;当使用 GlassFish 时,应用角色需要映射到安全域的组名(有关详细信息,请参阅第九章 [31a037d6-63a1-415d-a27f-dbec1ccff2cb.xhtml],Java EE 应用程序的安全性)。此映射以及要使用的域设置在 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 时,可以将任何应用程序角色自动匹配到安全域中同名安全组。这可以通过登录到 GlassFish Web 控制台,点击配置节点,点击安全,点击标记为默认主体到角色映射的复选框,并最终保存此配置更改来实现。

如示例所示,用于认证的安全域在<as-context>元素的<realm>子元素中定义。此子元素的值必须与应用服务器中有效安全域的名称匹配。<as-context>元素的其它子元素包括<auth-method>;此元素的唯一有效值是username_password,以及<required>,其唯一有效值是truefalse

客户端认证

如果访问受保护 EJB 的客户端代码是 Web 应用程序的一部分,并且该用户已经进行了认证,那么将使用用户的凭据来确定用户是否应该被允许访问他们试图执行的方法。

当使用 GlassFish 作为我们的应用服务器时,独立客户端必须通过appclient实用程序执行。以下代码演示了一个典型的安全会话 Bean 客户端:

package net.ensode.javaeebook; 

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注解注入到代码中,并像往常一样使用。这种处理方式取决于我们使用的是哪种应用服务器。GlassFish 的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 管理事务。

介绍了不同类型的企业 JavaBean 的生命周期,包括如何让 EJB 容器在生命周期中的特定点自动调用 EJB 方法。

我们还介绍了如何利用 EJB 定时器服务,让 EJB 方法被 EJB 容器定期调用。

最后,我们探讨了如何通过在 EJB 类和/或方法上添加适当的 EJB 安全注解,确保 EJB 方法只被授权用户调用。

第五章:上下文和依赖注入

上下文依赖注入CDI)在 Java EE 6 中被添加到 Java EE 规范中。Java EE 8 包括 CDI 的新版本,它添加了新的功能,如异步事件和事件排序。CDI 为 Java EE 开发者提供了之前不可用的几个优势,例如允许任何 JavaBean 用作 JSF 管理 Bean,包括无状态和有状态的会话 Bean。正如其名称所暗示的,CDI 简化了 Java EE 应用程序中的依赖注入。

在本章中,我们将涵盖以下主题:

  • 命名 Bean

  • 依赖注入

  • 范围

  • 标准化

  • CDI 事件

命名 Bean

CDI 通过@Named注解为我们提供了命名我们的 Bean 的能力。命名 Bean 允许我们轻松地将我们的 Bean 注入依赖于它们的其他类(参见下一节),并且可以通过统一表达式语言从 JSF 页面轻松地引用它们。

以下示例显示了@Named注解的作用:

package net.ensode.javaee8book.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属性来实现。例如,如果我们想为上面的 Bean 使用名称customerBean,我们可以通过修改@Named注解如下:

@Named(value="customerBean") 

或者简单地:

@Named("customerBean") 

由于value属性名称不需要指定,如果我们不使用属性名称,则隐含value

这个名称可以用来通过统一表达式语言从 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 管理 Bean 的访问方式完全相同。这允许 JSF 访问任何命名 Bean,将 Java 代码与 JSF API 解耦。

当部署和执行时,我们的简单应用程序看起来像这样:

图片

依赖注入

依赖注入是一种向 Java 类提供外部依赖的技术。Java EE 5 通过@Resource注解引入了依赖注入,然而,这个注解仅限于注入资源,如数据库连接、JMS 资源等。CDI 包括@Inject注解,它可以用来将 Java 类的实例注入到任何依赖对象中。

JSF 应用程序通常遵循模型-视图-控制器MVC)设计模式。因此,通常一些 JSF 管理 Bean 在模式中扮演控制器的角色,而其他则扮演模型的角色。这种方法通常要求控制器管理 Bean 能够访问一个或多个模型管理 Bean。CDI 的依赖注入功能使得将 Bean 注入到彼此中非常简单,如下面的示例所示:

package net.ensode.javaee8book.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"; 
  } 
} 

注意,我们初始化Customer实例所需要做的只是用@Inject注解来装饰它。当 Bean 由应用服务器构建时,Customer Bean 的实例会自动注入到这个字段中。注意,注入的 Bean 在saveCustomer()方法中被使用。

限定符

在某些情况下,我们希望注入到我们的代码中的 Bean 类型可能是一个接口或 Java 超类,但我们可能对注入特定的子类或实现接口的类感兴趣。对于这种情况,CDI 提供了我们可以用来指示我们希望注入到我们的代码中的特定类型的限定符。

CDI 限定符是一个必须用@Qualifier注解装饰的注解。这个注解然后可以用来装饰我们希望限定的特定子类或接口实现。此外,客户端代码中的注入字段也需要用限定符来装饰。

假设我们的应用程序可能有一种特殊的客户类型;例如,常客可能会被赋予高级客户的地位。为了处理这些高级客户,我们可以扩展我们的Customer命名 Bean 并用以下限定符装饰它:

package net.ensode.javaee8book.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 { 
} 

正如我们之前提到的,限定符是标准注解;它们通常具有运行时保留,可以针对方法、字段、参数或类型,如上例所示。限定符和标准注解之间的唯一区别是限定符被装饰了@Qualifier注解。

一旦我们设置了限定符,我们需要用它来装饰特定的子类或接口实现:

package net.ensode.javaee8book.cdidependencyinjection.beans; 

import javax.enterprise.context.RequestScoped; 
import javax.inject.Named; 
import net.ensode.javaee8book.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; 
  } 
} 

一旦我们装饰了需要限定的特定实例,我们就可以在client代码中使用我们的限定符来指定确切的依赖类型:

package net.ensode.javaee8book.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.javaee8book.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 的默认名称,即类名首字母小写。

从用户的角度来看,我们的简单应用程序渲染和操作就像一个“普通”的 JSF 应用程序:

命名 Bean 作用域

就像 JSF 管理 Bean 一样,CDI 命名 Bean 也有作用域。这意味着 CDI Bean 是上下文对象。当需要命名 Bean 时,无论是由于注入还是因为从 JSF 页面引用,CDI 都会在其所属的作用域中寻找 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 中不存在。这个作用域类似于 JSF 的流程作用域,因为它比请求作用域长,但比会话作用域短,通常跨越三个或更多页面。希望访问对话作用域 bean 的类必须注入javax.enterprise.context.Conversation的实例。在我们想要开始对话的点,必须在这个对象上调用begin()方法。在我们想要结束对话的点,必须在这个对象上调用end()方法。

CDI 的会话作用域与 JSF 的对应作用域行为相同。会话作用域 bean 的生命周期与 HTTP 会话的生命周期绑定。

CDI 的应用作用域也像 JSF 中的等效作用域一样行为。应用作用域 bean 与应用程序的生命周期绑定。每个应用作用域 bean 在每个应用程序中只有一个实例,这意味着相同的实例对所有 HTTP 会话都是可访问的。

就像对话作用域一样,CDI 的依赖作用域在 JSF 中不存在。每次需要时,都会实例化新的依赖作用域 bean;通常是在将其注入依赖于它的类时。

假设我们想要让用户输入一些将被存储在单个命名 bean 中的数据,但该 bean 有几个字段。因此,我们希望将数据输入分成几个页面。这是一个相当常见的情况,而且使用之前版本的 Java EE(JSF 2.2 添加了 Faces Flows 来解决这个问题;请参阅 第二章,JavaServer Faces),或者就 servlet API 而言,这种情况并不容易管理。这种情况之所以难以管理,是因为你可以将一个类放在请求作用域中,在这种情况下,该类会在每次请求后销毁,从而在过程中丢失其数据,或者放在会话作用域中,在这种情况下,该类会在需要之后长时间留在内存中。对于这种情况,CDI 的会话作用域是一个很好的解决方案:

package net.ensode..javaee8book.conversationscope.model; 

import java.io.Serializable; 
import javax.enterprise.context.ConversationScoped; 
import javax.inject.Named; 
import org.apache.commons.lang3.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; 

    public String getAddrCity() { 
        return addrCity; 
    } 

    public void setAddrCity(String addrCity) { 
        this.addrCity = addrCity; 
    } 

    public String getAddrLine1() { 
        return addrLine1; 
    } 

    public void setAddrLine1(String addrLine1) { 
        this.addrLine1 = addrLine1; 
    } 

    public String getAddrLine2() { 
        return addrLine2; 
    } 

    public void setAddrLine2(String addrLine2) { 
        this.addrLine2 = addrLine2; 
    } 

    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 String getMiddleName() { 
        return middleName; 
    } 

    public void setMiddleName(String middleName) { 
        this.middleName = middleName; 
    } 

    public String getPhoneHome() { 
        return phoneHome; 
    } 

    public void setPhoneHome(String phoneHome) { 
        this.phoneHome = phoneHome; 
    } 

    public String getPhoneMobile() { 
        return phoneMobile; 
    } 

    public void setPhoneMobile(String phoneMobile) { 
        this.phoneMobile = phoneMobile; 
    } 

    public String getPhoneWork() { 
        return phoneWork; 
    } 

    public void setPhoneWork(String phoneWork) { 
        this.phoneWork = phoneWork; 
    } 

    public String getState() { 
        return state; 
    } 

    public void setState(String state) { 
        this.state = state; 
    } 

    public String getZip() { 
        return zip; 
    } 

    public void setZip(String zip) { 
        this.zip = zip; 
    } 

    @Override 
    public String toString() { 
        return ReflectionToStringBuilder.reflectionToString(this); 
    } 
}  

我们通过使用 @ConversationScoped 注解来声明我们的 bean 是会话作用域的。会话作用域的 bean 还需要实现 java.io.Serializable。除了这两个要求之外,我们的代码没有特别之处;它是一个简单的 JavaBean,具有私有属性和相应的 gettersetter 方法。

我们在代码中使用 Apache commons-lang 库来轻松实现我们 bean 的 toString() 方法。commons-lang 包含几个这样的 utility 方法,它们实现了频繁需要的、编写起来繁琐的功能。commons-lang 可在中央 Maven 仓库和 commons.apache.org/lang 获取。

除了注入我们的会话作用域 bean 之外,我们的客户端代码还必须注入一个 javax.enterprise.context.Conversation 的实例,如下例所示:

package net.ensode.javaee8book.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.javaee8book.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 的引用。

当在注入的 Conversation 实例上调用 begin() 方法时,开始一个长时间运行的会话,并且它在我们对该对象调用 end() 方法时结束。

JSF 页面像往常一样访问我们的 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 保持会话作用域简化了实现 向导式 用户界面的任务,其中数据可以跨多个页面输入:

在我们的例子中,在第一页上点击“下一步”按钮后,我们可以在应用服务器日志中看到我们的部分填充的 bean:

INFO: net.ensode..javaee8book.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.javaee8book.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 属性。

当我们到达不需要在内存中保留客户信息的时候,我们需要调用注入到我们代码中的Conversation bean 上的end()方法。这正是我们在显示确认页面之前在代码中所做的:

public String navigateToConfirmationPage() { 
        System.out.println(customer); 
        conversation.end(); 
        return "confirmation"; 
    } 

在完成显示确认页面的请求后,我们的会话作用域 bean 被销毁,因为我们调用了注入的Conversation类中的end()方法。

我们应该注意,自从对话以来,范围需要注入一个javax.enterprise.context.Conversation实例,因此这个范围要求用于在页面之间导航的命令按钮或链接中的操作是一个解析为命名 bean 方法的表达式。使用静态导航将不会工作,因为Conversation实例不会被注入到任何地方。

CDI 事件

CDI 提供了事件处理功能。事件允许不同 CDI bean 之间的松散耦合通信。一个 CDI bean 可以触发一个事件,然后一个或多个事件监听器处理该事件。

触发 CDI 事件

以下示例是我们在上一节中讨论的CustomerInfoController类的新版本。该类已被修改为每次用户导航到新页面时触发一个事件:

package net.ensode.javaee8book.cdievents.controller; 

import java.io.Serializable; 
import javax.enterprise.context.Conversation; 
import javax.enterprise.context.RequestScoped; 
import javax.enterprise.event.Event; 
import javax.inject.Inject; 
import javax.inject.Named; 
import net.ensode.javaee8book.cdievents.event.NavigationInfo; 
import net.ensode.javaee8book.cdievents.model.Customer; 

@Named 
@RequestScoped 
public class CustomerInfoController implements Serializable { 

    @Inject 
    private Conversation conversation; 
    @Inject 
    private Customer customer; 
    @Inject 
    private Event<NavigationInfo> navigationInfoEvent; 

    public String customerInfoEntry() { 
        conversation.begin(); 
        NavigationInfo navigationInfo = new NavigationInfo(); 
        navigationInfo.setPage("1"); 
        navigationInfo.setCustomer(customer); 

        navigationInfoEvent.fire(navigationInfo); 
        return "page1"; 
    } 

    public String navigateToPage1() { 
        NavigationInfo navigationInfo = new NavigationInfo(); 
        navigationInfo.setPage("1"); 
        navigationInfo.setCustomer(customer); 

        navigationInfoEvent.fire(navigationInfo); 

        return "page1"; 
    } 

    public String navigateToPage2() { 
        NavigationInfo navigationInfo = new NavigationInfo(); 
        navigationInfo.setPage("2"); 
        navigationInfo.setCustomer(customer); 

        navigationInfoEvent.fire(navigationInfo); 
        return "page2"; 
    } 

    public String navigateToPage3() { 
        NavigationInfo navigationInfo = new NavigationInfo(); 
        navigationInfo.setPage("3"); 
        navigationInfo.setCustomer(customer); 

        navigationInfoEvent.fire(navigationInfo); 
        return "page3"; 
    } 

    public String navigateToConfirmationPage() { 
        NavigationInfo navigationInfo = new NavigationInfo(); 
        navigationInfo.setPage("confirmation"); 
        navigationInfo.setCustomer(customer); 

        navigationInfoEvent.fire(navigationInfo); 
        conversation.end(); 
        return "confirmation"; 
    } 
} 

正如我们所见,要创建一个事件,我们需要注入一个javax.enterprise.event.Event实例。这个类使用泛型,因此我们需要指定其类型;Event类的类型可以是实现java.io.Serializable接口的任何类。在我们的例子中,我们传递了一个简单的 POJO 实例作为类型参数。我们的 POJO 叫做NavigationInfo,有两个属性:一个Customer类型和一个包含用户正在导航到的页面的String。回想一下,在前面的章节中,我们CustomerInfoController类上的每个方法都会触发应用程序中一个页面到另一个页面的导航。在这个控制器的这个版本中,每次我们导航到新页面时都会触发一个 CDI 事件。在每种情况下,我们创建一个新的NavigationInfo实例,填充它,然后通过在我们的javax.enterprise.event.Event实例上调用fire()方法来触发事件。

处理 CDI 事件

要处理 CDI 事件,处理事件的 CDI 实例需要实现一个 observer 方法。observer 方法接受一个参数类型,即用于触发事件的类型,即创建触发事件的泛型类型。在我们的示例中,事件泛型类型是一个名为 NavigationInfo 的类的实例,如前一小节中我们事件声明所示。为了处理事件,观察者方法需要注解相应的参数为 @Observes 注解,以下示例进行了说明:

package net.ensode.javaee8book.cdievents.eventlistener; 

import java.io.Serializable; 
import javax.enterprise.context.SessionScoped; 
import javax.enterprise.event.Observes; 
import net.ensode.javaee8book.cdievents.event.NavigationInfo; 

@SessionScoped 
public class NavigationEventListener implements Serializable { 

    public void handleNavigationEvent(
         @Observes NavigationInfo navigationInfo) { 
        System.out.println("Navigation event fired"); 
        System.out.println("Page: " + navigationInfo.getPage()); 
        System.out.println("Customer: " +                     
         navigationInfo.getCustomer()); 
    } 
} 

在本例的事件处理器中,handleNavigationEvent() 方法接受一个 NavigationInfo 实例作为参数。请注意,此参数被注解为 @Observes;这会导致 CDI 在每次触发 NavigationInfo 类型事件时自动调用该方法。

在我们的示例中,我们只有一个事件监听器,但在实际应用中,我们可以根据需要拥有任意多个事件监听器。

异步事件

CDI 2.0 引入了异步触发事件的能力。异步触发事件可以帮助提高性能,因为各种观察者方法可以并发调用。异步触发事件与同步触发事件非常相似,唯一的区别是,我们不是在 Event 实例中调用 fire() 方法,而是调用其 fireAsync() 方法。以下示例说明了如何实现这一点:

public class EventSource{ 
  @Inject Event<MyEvent> myEvent; 
  public void fireEvent(){ 
   myEvent.fireAsync(myEvent); 
  } 
} 

处理异步事件的观察者方法与其同步对应者相同。

事件排序

CDI 2.0 中引入的另一个新功能是能够指定 observer 方法处理 CDI 事件的顺序。这可以通过 @Priority 注解实现,以下示例进行了说明:

import javax.annotation.Priority; 
import javax.enterprise.context.SessionScoped; 
import javax.enterprise.event.Observes; 
import javax.interceptor.Interceptor; 

@SessionScoped 
public class EventHandler{ 
    void handleIt (
      @Observes @Priority(Interceptor.Priority.APPLICATION)
       MyEvent me){ 
    //handle the event 
  } 
} 

@Priority 注解接受一个类型为 int 的参数。此参数指定了 observer 方法的优先级。最高优先级由 Interceptor.Priority 类中定义的 APPLICATION 常量定义。这是我们给示例中的 observer 方法指定的优先级。较低的优先级值具有优先权;默认优先级是 Interceptor.Priority.APPLICATION + 100

摘要

在本章中,我们介绍了上下文和依赖注入(CDI)。我们介绍了 JSF 页面如何访问 CDI 命名豆,就像它们是 JSF 管理豆一样。我们还介绍了 CDI 如何通过 @Inject 注解使代码注入依赖变得简单。此外,我们解释了如何使用限定符来确定要将哪个特定依赖项的实现注入到我们的代码中。最后,我们介绍了 CDI 豆可以放置的所有作用域,这些作用域包括所有 JSF 作用域的等效作用域,另外还有两个不在 JSF 中包含的作用域,即会话作用域和依赖作用域。

第六章:使用 JSON-P 和 JSON-B 进行 JSON 处理

JSON,或称JavaScript 对象表示法,是一种人类可读的数据交换格式。正如其名所示,JSON 源自 JavaScript。Java EE 7 引入了JSON-P,即 Java JSON 处理 API。Java EE 8 引入了一个额外的 JSON API,即 Java API for JSON 绑定JSON-B)。在本章中,我们将涵盖 JSON-P 和 JSON-B。

JSON-P 包括两个用于处理 JSON 的 API,即模型 API流式 API,这两个 API 都将在本章中介绍。JSON-B 可以透明地从 JSON 字符串填充 Java 对象,以及轻松地从 Java 对象生成 JSON 字符串。

在本章中,我们将涵盖以下主题:

  • JSON-P 模型 API:

    • 使用模型 API 生成 JSON 数据

    • 使用模型 API 解析 JSON 数据

  • JSON-P 流式 API:

    • 使用流式 API 生成 JSON 数据

    • 使用流式 API 解析 JSON 数据

  • 使用 JSON-B 从 JSON 填充 Java 对象

  • 使用 JSON-B 从 Java 对象生成 JSON 字符串

JSON-P 模型 API

JSON-P 模型 API 允许我们生成一个 JSON 对象的内存表示。这个 API 比本章后面讨论的流式 API 更灵活,然而,它速度较慢且需要更多的内存,这在处理大量数据时可能是一个问题。

使用模型 API 生成 JSON 数据

JSON-P 模型 API 的核心是JsonObjectBuilder类。这个类有几个重载的add()方法,可以用来向生成的 JSON 数据添加属性及其对应的值。

以下代码示例说明了如何使用模型 API 生成 JSON 数据:

    package net.ensode.javaee8book.jsonpobject; 

    //other imports omitted for brevity. 
    import javax.inject.Named; 
    import javax.json.Json; 
    import javax.json.JsonObject; 
    import javax.json.JsonReader; 
    import javax.json.JsonWriter; 

    @Named 
    @SessionScoped 
    public class JsonpBean implements Serializable{ 

      private String jsonStr; 

      @Inject 
      private Customer customer; 

      public String buildJson() { 
        JsonObjectBuilder jsonObjectBuilder =  
            Json.createObjectBuilder(); 
        JsonObject jsonObject = jsonObjectBuilder. 
            add("firstName", "Scott"). 
            add("lastName", "Gosling"). 
            add("email", "sgosling@example.com"). 
            build(); 

        StringWriter stringWriter = new StringWriter(); 

        try (JsonWriter jsonWriter = Json.createWriter(stringWriter)) 
         { 
           jsonWriter.writeObject(jsonObject); 
         }    

        setJsonStr(stringWriter.toString()); 

        return "display_json"; 

     } 
    //getters and setters omitted for brevity 
    }  

如前例所示,我们通过在JsonObjectBuilder的实例上调用add()方法来生成JsonObject的实例。在前例中,我们看到如何通过在JsonObjectBuilder上调用add()方法将String值添加到我们的JsonObject中。add()方法的第一参数是生成的 JSON 对象的属性名,第二个参数对应于该属性的值。add()方法的返回值是另一个JsonObjectBuilder的实例,因此,add()方法的调用可以像前例那样链式调用。

上述示例是一个对应于更大 JSF 应用的 CDI 命名 bean。应用的其他部分没有显示,因为它们与讨论无关。完整的示例应用可以作为本书示例代码下载的一部分获得。

一旦我们添加了所有需要的属性,我们需要调用JsonObjectBuilderbuild()方法,它返回一个实现JsonObject接口的类的实例。

在许多情况下,我们可能希望生成我们创建的 JSON 对象的 String 表示形式,以便它可以被另一个进程或服务处理。我们可以通过创建一个实现 JsonWriter 接口的类的实例,通过调用 Json 类的静态 createWriter() 方法并传递一个 StringWriter 实例作为其唯一参数来实现这一点。一旦我们有了 JsonWriter 实现的实例,我们需要调用它的 writeObject() 方法,并将我们的 JsonObject 实例作为其唯一参数传递。

到这一点,我们的 StringWriter 实例将包含我们 JSON 对象的 String 表示形式作为其值,因此调用其 toString() 方法将返回一个包含我们 JSON 对象的 String

我们的特定示例将生成一个看起来像这样的 JSON 字符串:

    {"firstName":"Scott","lastName":"Gosling","email":"
    sgosling@example.com "}

尽管在我们的示例中,我们只向我们的 JSON 对象添加了 String 对象,但我们并不局限于这种类型的值;JsonObjectBuilder 有几个重载的 add() 方法版本,允许我们向我们的 JSON 对象添加几种不同类型的值。

下表总结了所有可用的 add() 方法版本:

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 对象中的属性名,第二个参数对应于属性的值。

使用模型 API 解析 JSON 数据

在上一节中,我们看到了如何使用对象模型 API 从我们的 Java 代码中生成 JSON 数据。在本节中,我们将了解如何读取和解析现有的 JSON 数据。以下代码示例说明了如何进行此操作:

    package net.ensode.javaee8book.jsonpobject; 

    //other imports omitted 
    import javax.json.Json; 
    import javax.json.JsonObject; 
    import javax.json.JsonReader; 
    import javax.json.JsonWriter; 

    @Named 
    @SessionScoped 
    public class JsonpBean implements Serializable{ 

      private String jsonStr; 

      @Inject 
      private Customer customer; 

      public String parseJson() { 
        JsonObject jsonObject; 
        try (JsonReader jsonReader = 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()方法之外,还有其他几个类似的方法可以获取其他类型的数据值。以下表格总结了这些方法:

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 数据:

    package net.ensode.javaee8book.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() { 
        StringWriter stringWriter = new StringWriter(); 
        try (JsonGenerator jsonGenerator = 
          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 提供了此方法的两个重载版本:一个接受一个扩展 java.io.Writer 类的实例(例如我们例子中使用的 StringWriter),另一个接受一个扩展 java.io.OutputStream 类的实例。

在我们开始向生成的 JSON 流添加属性之前,我们需要在 JsonGenerator 上调用 writeStartObject() 方法。此方法写入 JSON 开始对象字符(在 JSON 字符串中表示为开括号({)),并返回另一个 JsonGenerator 实例,允许我们将 write() 调用链式添加到我们的 JSON 流中。

JsonGenerator 上的 write() 方法允许我们向正在生成的 JSON 流中添加属性。它的第一个参数是一个 String,对应于我们添加的属性名称,第二个参数是属性的值。

在我们的例子中,我们只向创建的 JSON 流添加 String 值,但我们并不局限于字符串;JSON-P Streaming API 提供了几个重载的 write() 方法,允许我们向 JSON 流添加多种不同类型的数据。以下表格总结了所有可用的 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 数据。我们如何处理它取决于我们的应用程序逻辑。在我们的例子中,我们简单地调用了 StringReadertoString() 方法,以获取我们创建的 JSON 数据的字符串表示形式。

使用 Streaming API 解析 JSON 数据

在本节中,我们将介绍如何解析我们从流中接收到的 JSON 数据。请参考以下代码:

    package net.ensode.javaee8book.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() { 

        StringReader stringReader = new StringReader(jsonStr); 

        JsonParser jsonParser = 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 

    } 

使用 Streaming API 读取 JSON 数据的第一步是创建一个 JsonParser 实例,通过在 Json 类上调用静态方法 createJsonParser() 实现。createJsonParser() 方法有两种重载版本:一个接受一个扩展 java.io.InputStream 类的类的实例,另一个接受一个扩展 java.io.Reader 类的类的实例。在我们的示例中,我们使用后者,传递一个 java.io.StringReader 的实例,它是 java.io.Reader 的子类。

下一步是循环遍历 JSON 数据以获取要解析的数据。我们可以通过在 JsonParser 上调用 hasNext() 方法来实现这一点,如果还有更多数据要读取,则返回 true,否则返回 false

接下来,我们需要读取流中的下一份数据。JsonParser.next() 方法返回一个 JsonParser.Event 实例,它表示我们刚刚读取的数据类型。在我们的示例中,我们只检查键名(即 firstNamelastNameemail),以及相应的字符串值。我们通过将 JsonParser.next() 返回的事件与在 JsonParser 中定义的 Event 枚举中定义的几个值进行比较来检查我们刚刚读取的数据类型。

以下表格总结了 JsonParser.next() 可以返回的所有可能的事件:

Event.START_OBJECT 表示 JSON 对象的开始。
Event.END_OBJECT 表示 JSON 对象的结束。
Event.START_ARRAY 表示数组的开始。
Event.END_ARRAY 表示数组的结束。
Event.KEY_NAME 表示读取了一个 JSON 属性的名称;我们可以通过在 JsonParser 上调用 getString() 来获取键名。
Event.VALUE_TRUE 表示读取了一个 true 的布尔值。
Event.VALUE_FALSE 表示读取了一个 false 的布尔值。
Event.VALUE_NULL 表示读取了一个 null 值。
Event.VALUE_NUMBER 表示读取了一个 numeric 值。
Event.VALUE_STRING 表示读取了一个 string 值。

如示例所示,可以通过在 JsonParser 上调用 getString() 来检索 String 值。数值可以以几种不同的格式检索;以下表格总结了 JsonParser 中可以用来检索数值的方法:

getInt() int 的形式检索数值。
getLong() long 的形式检索数值。
getBigDecimal() java.math.BigDecimal 实例的形式检索数值。

JsonParser 还提供了一个方便的 isIntegralNumber() 方法,如果数值可以安全地转换为 intlong,则返回 true

我们对从流中获取的值所做的事情取决于我们的应用程序逻辑。在我们的示例中,我们将它们放置在一个 Map 中,然后使用这个 Map 来填充一个 Java 类。

JSON 指针

Java EE 8 中引入的 JSON-P 1.1 引入了对 JSON Pointer 的支持。JSON Pointer 是一个 互联网工程任务组 (IETF) 标准,它定义了一种字符串语法,用于在 JSON 文档中标识特定的值,类似于 XPath 为 XML 文档提供的功能。

JSON Pointer 的语法很简单,例如,假设我们有以下 JSON 文档:

 { 
  "dateOfBirth": "1997-03-03",
  "firstName": "David",
  "lastName": "Heffelfinger",
  "middleName": "Raymond",
  "salutation": "Mr" 
 }

如果我们想要获取文档中 lastName 属性的值,要使用的 JSON Pointer 表达式将是 "/lastName"

如果我们的 JSON 文档由一个数组组成,那么我们必须在属性前加上数组中的索引作为前缀,例如,要获取以下 JSON 数组中第二个元素的 lastName 属性,我们需要这样做:

    [  
     {  
       "dateOfBirth": "1997-01-01", 
       "firstName": "David", 
       "lastName": "Delabassee", 
       "salutation": "Mr"  
      }, 
     {  
       "dateOfBirth": "1997-03-03", 
       "firstName": "David", 
       "lastName": "Heffelfinger", 
       "middleName": "Raymond", 
       "salutation": "Mr" 
     } 
    ] 

要这样做,JSON Pointer 表达式将是 "/1/lastName"。表达式开头的 "/1" 指的是数组中的元素索引。就像在 Java 中一样,JSON 数组是 0 索引的,因此,在这个例子中,我们正在获取数组中第二个元素的 lastName 属性的值。现在让我们看看如何使用新的 JSON-P JSON Pointer API 来执行此任务的示例:

    package net.ensode.javaee8book.jsonpointer; 
    //imports omitted 

    @Path("jsonpointer") 
    public class JsonPointerDemoService { 

      private String jsonString; //initialization omitted 

      @GET 
      public String jsonPointerDemo() { 
        initializeJsonString(); //method body omitted for brevity 
        JsonReader jsonReader = Json.createReader
 (new StringReader(jsonString)); 
        JsonArray jsonArray = jsonReader.readArray(); 
        JsonPointer jsonPointer = Json.createPointer("/1/lastName"); 

        return jsonPointer.getValue(jsonArray).toString(); 
      } 
    } 

上述代码示例是一个使用 Java EE 的 JAX-RS API 编写的 RESTful 网络服务(有关详细信息,请参阅第十章 [4987ac18-1f2a-410c-9613-530174a64bad.xhtml],使用 JAX-RS 的 RESTful 网络服务)。为了从 JSON 文档中读取属性值,我们首先需要通过在 javax.json.Json 上调用静态的 createReader() 方法来创建一个 javax.json.JsonReader 的实例。createReader() 方法接受任何实现 java.io.Reader 接口类的实例作为参数。在我们的例子中,我们即时创建了一个新的 java.io.StringReader 实例,并将我们的 JSON 字符串作为参数传递给其构造函数。

JSON.createReader() 有一个重载版本,它接受任何实现 java.io.InputStream 类的实例。

在我们的例子中,我们的 JSON 文档由一个对象数组组成,因此,我们通过在创建的 JsonReader 对象上调用 readArray() 方法来填充 javax.json.JsonArray 的一个实例(如果我们的 JSON 文档由一个单独的 JSON 对象组成,我们则会调用 JsonReader.readObject())。

现在我们已经填充了我们的 JsonArray 变量,我们创建了一个 javax.json.JsonPointer 的实例,并用我们想要使用的 JSON Pointer 表达式初始化它。记住,我们正在寻找数组中第二个元素的 lastName 属性的值,因此,适当的 JSON Pointer 表达式是 /1/lastName

现在我们已经创建了一个带有适当 JSON Pointer 表达式的 JsonPointer 实例,我们只需调用它的 getValue() 方法,并将我们的 JsonArray 对象作为参数传递,然后对结果调用 toString()。这个调用的返回值将是 JSON 文档中 lastName 属性的值(在我们的示例中是 "Heffelfinger")。

JSON Patch

JSON-P 1.1 还引入了对 JSON Patch 的支持,这是另一个 互联网工程任务组 标准,它提供了一系列可以应用于 JSON 文档的操作。JSON Patch 允许我们对 JSON 对象进行部分更新。

JSON Patch 支持以下操作:

JSON Patch 操作 描述
add 向 JSON 文档中添加一个元素。
remove 从 JSON 文档中删除一个元素。
replace 用新值替换 JSON 文档中的一个值。
move 将 JSON 文档中的一个值从其在文档中的当前位置移动到新位置。
copy 将 JSON 文档中的一个值复制到文档中的新位置。
test 验证 JSON 文档中特定位置的值是否等于指定的值。

JSON-P 支持所有上述 JSON Patch 操作,这些操作依赖于 JSON Pointer 表达式来定位 JSON 文档中的源和目标位置。

以下示例说明了我们如何使用 JSON-P 1.1 与 JSON Patch:

    package net.ensode.javaee8book.jsonpatch; 

    //imports omitted for brevity 

    @Path("jsonpatch") 
    public class JsonPatchDemoService { 

      private String jsonString; 

      @GET 
      public Response jsonPatchDemo() { 
        initializeJsonString(); //method declaration omitted 
        JsonReader jsonReader = Json.createReader( 
            new StringReader(jsonString)); 
        JsonArray jsonArray = jsonReader.readArray(); 
        JsonPatch jsonPatch = Json.createPatchBuilder() 
                .replace("/1/dateOfBirth", "1977-01-01") 
                .build(); 
        JsonArray modifiedJsonArray =jsonPatch.apply(jsonArray); 

        return Response.ok(modifiedJsonArray.toString(), 
        MediaType.APPLICATION_JSON).build(); 
      } 
    } 

在本例中,让我们假设我们正在处理与之前示例相同的 JSON 文档:一个包含两个单独 JSON 对象的数组,每个对象都有一个 dateOfBirth 属性(以及其他属性)。

在我们的示例中,我们创建了一个 JsonArray 实例,就像之前一样,然后修改数组中第二个元素的 dateOfBirth 属性。为了做到这一点,我们通过 javax.json.Json 类中的静态 createPatchBuilder() 方法创建了一个 javax.json.JsonPatchBuilder 实例。在我们的示例中,我们用一个新值替换了一个属性的值。我们使用 JsonPatch 实例的 replace() 方法来完成这个操作;方法中的第一个参数是一个 JSON Pointer 表达式,指示我们要修改的属性的定位,第二个参数是属性的新值。正如其名称所暗示的,JsonPatchBuilder 遵循 Builder 设计模式,这意味着它的大多数方法都返回另一个 JsonPatchBuilder 实例;这允许我们在 JsonPatchBuilder 的结果实例上链式调用方法(在我们的示例中,我们只执行一个操作,但这不必是这种情况)。

一旦我们指定了要在我们的 JSON 对象上执行的操作,我们就通过在 JsonPatchBuilder 上调用 build() 方法来创建一个 javax.json.JsonPatch 实例。

一旦我们创建了补丁,我们就通过调用其 patch() 方法并将其作为参数传递 JSON 对象(在我们的示例中是一个 JsonArray 实例)来将其应用到我们的 JSON 对象上。

在我们的示例中,如何通过 JSON-P 1.1 中的 JSON Patch 支持替换 JSON 属性的值,JSON-P 支持当前 JSON Patch 所支持的所有操作。API 是直接的。有关如何使用 JSON-P 中的其他 JSON Patch 操作的详细信息,请参阅 Java EE 8 API 文档,网址为 javaee.github.io/javaee-spec/javadocs/

使用 JSON-B 从 JSON 填充 Java 对象

填充 Java 对象从 JSON 字符串是一个常见的编程任务。这是一个如此常见的任务,以至于已经创建了几个库来透明地填充 Java 对象从 JSON,从而让应用程序开发者免于手动编写此功能。有一些非标准的 Java 库可以完成这个任务,例如 Jackson (github.com/FasterXML/jackson)、JSON-simple (github.com/fangyidong/json-simple) 和 Gson (github.com/google/gson)。Java EE 8 引入了一个提供此功能的新 API,即 Java API for JSON Binding (JSON-B)。在本节中,我们将介绍如何透明地从 JSON 字符串填充 Java 对象。

以下示例展示了使用 Java API for RESTful Web Services (JAX-RS) 编写的 RESTful 网络服务。该服务在其 addCustomer() 方法中响应 HTTP POST 请求。此方法接受一个 String 参数,并且这个字符串预期包含有效的 JSON。请参考以下代码:

    package net.ensode.javaee8book.jaxrs21example.service; 

    import net.ensode.javaee8book.jaxrs21example.dto.Customer; 
    import java.util.logging.Level; 
    import java.util.logging.Logger; 
    import javax.json.bind.Jsonb; 
    import javax.json.bind.JsonbBuilder; 
    import javax.ws.rs.POST; 
    import javax.ws.rs.Consumes; 
    import javax.ws.rs.Path; 
    import javax.ws.rs.core.MediaType; 
    import javax.ws.rs.core.Response; 

    @Path("/customercontroller") 
    public class CustomerControllerService { 

    private static final Logger LOG = 
    Logger.getLogger(CustomerControllerService.class.getName()); 

    @POST 
    @Consumes(MediaType.APPLICATION_JSON) 
    public Response addCustomer(String customerJson) { 
      Response response; 
      Jsonb jsonb = JsonbBuilder.create(); 
 Customer customer = jsonb.fromJson(customerJson, 
          Customer.class); 
      LOG.log(Level.INFO, "Customer object populated from JSON"); 
      LOG.log(Level.INFO, String.format("%s %s %s %s %s", 
      customer.getSalutation(), 
      customer.getFirstName(), 
      customer.getMiddleName(), 
      customer.getLastName(), 
      customer.getDateOfBirth())); 
      response = Response.ok("{}").build(); 
      return response; 
     } 

    } 

我们的应用服务器提供的 JSON-B 实现提供了一个实现 JsonbBuilder 接口的类的实例。这个类提供了一个静态的 create() 方法,我们可以使用它来获取 Jsonb 的实例。

一旦我们有了 Jsonb 的实例,我们可以使用它来解析 JSON 字符串并自动填充 Java 对象。这是通过其 fromJson() 方法完成的。fromJson() 方法接受一个包含我们需要解析的 JSON 数据的 String 作为其第一个参数,以及我们希望填充的对象的类型作为其第二个参数。在我们的例子中,我们正在填充一个包含 firstNamemiddleNamelastNamedateOfBirth 等字段的简单 Customer 类。JSON-B 将寻找与 Java 对象中的属性名称匹配的 JSON 属性名称,并自动用相应的 JSON 属性填充 Java 对象。这再简单不过了。

一旦我们填充了我们的 Java 对象,我们就可以用它做我们需要的任何事情。在我们的例子中,我们只是记录 Java 对象的属性,以验证它是否正确填充。

使用 JSON-B 从 Java 对象生成 JSON 字符串

除了从 JSON 数据填充 Java 对象之外,JSON-B 还可以从 Java 对象生成 JSON 字符串。以下示例说明了如何做到这一点:

    package net.ensode.javaee8book.jsonbjavatojson.service; 

    //imports omitted for brevity 

    @Path("/customersearchcontroller") 
    public class CustomerSearchControllerService { 
      private final List<Customer> customerList = new ArrayList<>(); 

      @GET 
      @Path("{firstName}") 
      public Response getCustomerByFirstName(@PathParam("firstName")   
      String firstName) { 
        List<Customer> filteredCustomerList; 
        String jsonString; 
        initializeCustomerList(); //method declaration omitted 

        Jsonb jsonb = JsonbBuilder.create(); 

        filteredCustomerList = customerList.stream().filter( 
                customer -> customer.getFirstName().equals(firstName)). 
                collect(Collectors.toList()); 

        jsonString = jsonb.toJson(filteredCustomerList); 

        return Response.ok(jsonString).build(); 
     } 
   } 

在这个例子中,我们将 Customer 对象的 List 转换为 JSON。

我们选择 List 作为示例来说明 JSON-B 支持此功能,但当然,我们也可以将单个对象转换为它的 JSON 表示形式。

就像之前一样,我们通过调用静态方法 javax.json.bind.JsonbBuilder.create() 来创建一个 javax.json.bind.Jsonb 实例。一旦我们有了 Jsonb 实例,我们只需调用它的 toJson() 方法,将对象列表转换为等价的 JSON 表示形式。

摘要

在本章中,我们介绍了 Java API for JSON Processing (JSON-P)。我们展示了如何通过 JSON-P 的模型和流式 API 生成和解析 JSON 数据。此外,我们还介绍了新的 JSON-P 1.1 功能,例如对 JSON Pointer 和 JSON Patch 的支持。最后,我们介绍了如何无缝地将 Java 对象从 JSON 中填充,以及如何通过新的 JSON-B API 简单地生成 Java 对象的 JSON 字符串。

第七章:WebSocket

传统上,Web 应用程序是使用 HTTP 协议之后的请求/响应模型开发的。在这个传统的请求/响应模型中,请求始终由客户端发起,然后服务器将响应发送回客户端。

服务器从未有过独立向客户端发送数据的方式,也就是说,无需等待请求,直到现在。WebSocket 协议允许客户端(浏览器)和服务器之间进行全双工、双向通信。

Java EE 7 引入了 Java API for WebSocket,它允许我们在 Java 中开发 WebSocket 端点。

在本章中,我们将涵盖以下主题:

  • 开发 WebSocket 服务器端点

  • 在 JavaScript 中开发 WebSocket 客户端

  • 在 Java 中开发 WebSocket 客户端

开发 WebSocket 服务器端点

我们可以通过两种方式使用 Java API for WebSocket 实现 WebSocket 服务器端点。我们既可以程序化地开发端点,在这种情况下,我们需要扩展javax.websocket.Endpoint类,也可以通过 WebSocket 特定注解装饰普通 Java 对象POJOs)。这两种方法非常相似,因此,我们将在本章中详细讨论注解方法,并在稍后简要解释如何程序化地开发 WebSocket 服务器端点。

在本章中,我们将开发一个简单的基于 Web 的聊天应用程序,充分利用 Java API for WebSocket。

开发一个带注释的 WebSocket 服务器端点

以下 Java 类说明了我们如何通过注解 Java 类来开发 WebSocket 服务器端点:

package net.ensode.javaeebook.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()方法返回一个表示所有打开会话的Session对象集合。我们遍历这个集合,通过在每个Session实例上调用getBasicRemote()方法,然后调用此调用返回的RemoteEndpoint.Basic实现上的sendText()方法,将接收到的消息广播回所有已连接的客户端。

Session接口上的getOpenSessions()方法在调用该方法时返回所有打开的会话。在方法调用后,可能有一个或多个会话已经关闭,因此,在尝试向客户端发送数据之前,建议在Session实现上调用isOpen()方法。

最后,如果我们需要处理客户端从服务器端点断开连接的事件,我们需要用@OnClose注解装饰一个方法。在我们的例子中,我们只是简单地将一条消息记录到服务器日志中。

我们在示例中没有使用的一个额外注解是@OnError注解,它用于装饰在发送或接收数据到或从客户端发生错误时需要调用的方法。

如我们所见,开发带有注释的 WebSocket 服务器端点是直接的;我们只需添加几个注解,应用程序服务器就会根据需要调用我们的注解方法。

如果我们希望以程序化的方式开发 WebSocket 服务器端点,我们需要编写一个扩展javax.websocket.Endpoint的 Java 类。这个类有onOpen()onClose()onError()方法,这些方法在端点生命周期中的适当时间被调用。没有与@OnMessage注解等效的方法来处理来自客户端的消息;需要在Session中调用addMessageHandler()方法,传递一个实现javax.websocket.MessageHandler接口(或其子接口)的类的实例作为其唯一参数。

通常情况下,与程序化方式相比,开发带有注释的 WebSocket 端点更容易且更直接,因此,在可能的情况下,我们推荐使用注释方法。

开发 WebSocket 客户端

大多数 WebSocket 客户端都是作为利用 JavaScript WebSocket API 的网页实现的。我们将在下一节中介绍如何做到这一点。

Java WebSocket API 提供了客户端 API,允许我们开发作为独立 Java 应用程序的 WebSocket 客户端。我们将在本章后面介绍这一功能。

开发 JavaScript 客户端 WebSocket 代码

在本节中,我们将介绍如何开发与我们在上一节中开发的 WebSocket 端点交互的客户端 JavaScript 代码。

我们 WebSocket 示例的客户端页面是一个使用 HTML5 友好标记的 JSF 页面(如第二章[9059bc2f-04fb-43df-a5c5-8b2cce80792e.xhtml]中所述,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 函数,以便我们可以处理这些事件,这正是我们在 init() 函数中做的,紧随 JavaScript WebSocket 对象构造函数的调用之后。在我们的示例中,我们分配给 WebSocket 对象的函数只是将它们的功能委托给独立的 JavaScript 函数。

我们的 websocketOpen() 函数会在 WebSocket 连接打开时被调用。在我们的示例中,我们只是简单地向浏览器的 JavaScript 控制台发送一条消息。

我们的 webSocketMessage() 函数会在浏览器从我们的 WebSocket 端点接收到 WebSocket 消息时被调用。在我们的示例中,我们使用消息的内容更新具有 ID chatWindow 的文本区域的内容。

我们的 websocketError() 函数会在发生与 WebSocket 相关的错误时被调用。在我们的示例中,我们只是简单地向浏览器的 JavaScript 控制台发送一条消息。

我们的 JavaScript sendMessage() 函数将消息发送到 WebSocket 服务器端点,包含用户名和具有 ID chatinput 的文本输入的内容。当用户点击具有 ID sendBtn 的按钮时,会调用此函数。

我们的 closeConnection() JavaScript 函数关闭与我们的 WebSocket 服务器端点的连接。当用户点击具有 ID exitBtn 的按钮时,会调用此函数。

如此示例所示,编写客户端 JavaScript 代码以与 WebSocket 端点交互相当简单。

在 Java 中开发 WebSocket 客户端

虽然目前开发基于 Web 的 WebSocket 客户端是开发 WebSocket 客户端最常见的方式,但 Java API for WebSocket 提供了一个客户端 API,我们可以使用它来在 Java 中开发 WebSocket 客户端。

在本节中,我们将使用 Java API for WebSocket 的客户端 API 开发一个简单的 WebSocket 客户端。最终产品看起来像这样:

然而,我们不会涵盖 GUI 代码(使用 Swing 框架开发的),因为它与讨论无关。示例的完整代码,包括 GUI 代码,可以从 Packt Publishing 网站下载。

就像 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 客户端代码中),第二个参数是一个包含 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的父类),并且可以有一个可选的type session参数。在我们的例子中,我们只是将Throwable参数的堆栈跟踪发送到stderr

被注解为@OnMessage的方法会在接收到任何传入的 WebSocket 消息时被调用。@OnMessage方法可以根据接收到的消息类型以及我们希望如何处理它来具有不同的参数。在我们的例子中,我们使用了最常见的情况,即接收一个文本消息。在这种情况下,我们需要一个String参数,它将保存消息的内容,以及一个可选的Session参数。

有关如何处理其他类型消息的信息,请参阅@OnMessage的 JavaDoc 文档,链接为docs.oracle.com/javaee/7/api/javax/websocket/OnMessage.html

在我们的例子中,我们只是简单地更新了聊天窗口的文本区域,将接收到的消息追加到其内容中。

要发送 WebSocket 消息,我们在Session实例上调用getBasicRemote()方法,然后在该R.remoteEndpoint上调用sendText()方法。此调用返回一个基本实现(如果这看起来很熟悉,那是因为我们在 WebSocket 服务器端点代码中做了完全相同的事情)。在我们的例子中,我们在sendMessage()方法中这样做。

关于 Java WebSocket API 的附加信息

在本章中,我们介绍了 Java WebSocket API 提供的功能的大部分。有关更多信息,请参阅 Tyrus 用户指南,它是 Java WebSocket API 的参考实现,链接为tyrus.java.net/documentation/1.3.1/user-guide.html

摘要

在本章中,我们介绍了 Java WebSocket API,这是一个 Java EE API,用于开发 WebSocket 服务器端点和客户端。我们首先看到了如何利用 Java WebSocket API 开发 WebSocket 服务器端点。然后,我们介绍了如何使用 JavaScript 开发基于 Web 的 WebSocket 客户端。最后,我们解释了如何在 Java 中开发 WebSocket 客户端应用程序。

第八章:Java 消息服务

Java 消息 APIJMS)为 Java EE 应用程序之间发送消息提供了一个机制。Java EE 7 引入了 JMS 2.0,它极大地简化了涉及消息功能的应用程序的开发。

JMS 应用程序不直接通信;相反,消息生产者向目的地发送消息,而消息消费者从该目的地接收消息。

当使用 点对点PTP)消息域时,消息目的地是一个消息队列;当使用发布/订阅(pub/sub)消息域时,消息目的地是一个消息主题。

在本章中,我们将涵盖以下主题:

  • 与消息队列一起工作

  • 与消息主题一起工作

大多数应用程序服务器都需要进行配置,以便 JMS 应用程序能够正常工作。附录 配置和部署到 GlassFish(e125af24-9bf3-427d-a71c-22461bbf34fc.xhtml)中包含了配置 GlassFish 的说明。请查阅您的应用程序服务器文档,以获取配置其他 Java EE 8 兼容应用程序服务器的说明。

消息队列

如我们之前提到的,当我们的 JMS 代码使用点对点(PTP)消息域时,会使用消息队列。对于 PTP 消息域,通常有一个消息生产者和一个消息消费者。消息生产者和消息消费者不需要同时运行才能进行通信。消息生产者放入消息队列中的消息将保持在消息队列中,直到消息消费者执行并从队列中请求这些消息。

向消息队列发送消息

以下示例说明了如何向消息队列添加消息:

package net.ensode.javaee8book.jmsptpproducer; 

import java.util.logging.Level; 
import java.util.logging.Logger; 
import javax.annotation.Resource; 
import javax.enterprise.context.RequestScoped; 
import javax.inject.Named; 
import javax.jms.ConnectionFactory; 
import javax.jms.JMSContext; 
import javax.jms.JMSProducer; 
import javax.jms.Queue; 

@Named 
@RequestScoped 
public class MessageSender { 

 @Resource private ConnectionFactory connectionFactory; @Resource(mappedName = "jms/JavaEE8BookQueue") private Queue queue; 

    private static final Logger LOG = 
        Logger.getLogger(MessageSender.class.getName()); 

    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!"; 

        LOG.log(Level.INFO, "Sending the following message: {0}",  
            msg1); 
 jmsProducer.send(queue, msg1); 
        LOG.log(Level.INFO, "Sending the following message: {0}",  
            msg2); 
 jmsProducer.send(queue, msg2); 
        LOG.log(Level.INFO, "Sending the following message: {0}", 
        msg3); 
 jmsProducer.send(queue, msg3); 

    } 
} 

MessageSender 类中的 produceMessages() 方法执行将消息发送到消息队列所需的所有必要步骤。

此方法首先通过在注入的 javax.jms.ConnectionFactory 实例上调用 createContext() 方法来创建一个 javax.jms.JMSContext 实例。请注意,装饰连接工厂对象的 @Resource 注解的 mappedName 属性与我们在 GlassFish 网络控制台中设置的连接工厂的 JNDI 名称相匹配。在幕后,使用此名称进行 JNDI 查找以获取连接工厂对象。

接下来,我们通过在刚刚创建的 JMSContext 实例上调用 createProducer() 方法来创建一个 javax.jms.JMSProducer 实例。

获得一个 JMSProducer 实例后,代码通过调用其 send() 方法发送一系列文本消息。此方法将消息目的地作为其第一个参数,将包含消息文本的 String 作为其第二个参数。

JMSProducer 中的 send() 方法有几个重载版本。我们在示例中使用的是一种方便的方法,它创建一个 javax.jms.TextMessage 实例并将其文本设置为方法调用中提供的第二个参数的 String

虽然上面的例子只向队列发送文本消息,但我们并不局限于这种类型的消息。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 实例。

关于上述所有消息类型的更多信息,请参阅 JavaDoc 文档,链接为 javaee.github.io/javaee-spec/javadocs/

从消息队列中检索消息

当然,如果没有任何东西要接收消息,从队列中发送消息是没有意义的。以下示例说明了如何从 JMS 消息队列中检索消息:

package net.ensode.javaee8book.jmsptpconsumer; 

import java.io.Serializable; 
import java.util.logging.Level; 
import java.util.logging.Logger; 
import javax.annotation.Resource; 
import javax.enterprise.context.RequestScoped; 
import javax.inject.Named; 
import javax.jms.ConnectionFactory; 
import javax.jms.JMSConsumer; 
import javax.jms.JMSContext; 
import javax.jms.Queue; 

@Named 
@RequestScoped 
public class MessageReceiver implements Serializable{ 

 @Resource private ConnectionFactory connectionFactory; @Resource(mappedName = "jms/JavaEE8BookQueue") private Queue queue; 
    private static final Logger LOG =   
     Logger.getLogger(MessageReceiver.class.getName()); 

    public void receiveMessages() { 
        String message; 
        boolean goodByeReceived = false; 

 JMSContext jmsContext = connectionFactory.createContext(); JMSConsumer jMSConsumer = jmsContext.createConsumer(queue); 

        LOG.log(Level.INFO, "Waiting for messages..."); 
        while (!goodByeReceived) { 
 message = jMSConsumer.receiveBody(String.class); 

            if (message != null) { 
                LOG.log(Level.INFO,
                 "Received the following message: {0}", message); 
                if (message.equals("Good bye!")) { 
                    goodByeReceived = true; 
                } 
            } 
        } 
    } 
} 

就像上一个例子一样,使用 @Resource 注解注入 javax.jms.ConnectionFactoryjavax.jms.Queue 的实例。

在我们的代码中,我们通过调用 ConnectionFactorycreateContext() 方法来获取 javax.jms.JMSContext 的实例,就像上一个例子一样。

在这个例子中,我们通过在 JMSContext 实例上调用 createConsumer() 方法来获取 javax.jms.JMSConsumer 的实例。

通过在我们的JMSConsumer实例上调用receiveBody()方法来接收消息。此方法接受我们期望的消息类型作为其唯一参数(在我们的例子中是String.class)并返回其参数指定的类型的对象(在我们的例子中是一个java.lang.String实例)。

在这个特定的例子中,我们将这个方法调用放在了一个while循环中,因为我们期望一个消息会告诉我们没有更多的消息到来。具体来说,我们正在寻找包含文本Good bye !的消息。一旦我们收到这个消息,我们就跳出循环并继续处理。在这种情况下,没有更多的处理要做,因此,在跳出循环后执行结束。

执行代码后,我们应该在服务器日志中看到以下输出:

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!

当然,这假设了之前的示例已经执行并将消息放入了消息队列。

本节讨论的将 JMS 消息处理作为缺点之一是消息处理是同步的。在 Java EE 环境中,我们可以通过使用消息驱动豆(message-driven beans),如第四章所述,来异步处理 JMS 消息,企业 JavaBeans

浏览消息队列

JMS 提供了一种在不实际从队列中删除消息的情况下浏览消息队列的方法。以下示例说明了如何做到这一点:

package net.ensode.javaee8book.jmsqueuebrowser; 

import java.util.Enumeration; 
import java.util.logging.Level; 
import java.util.logging.Logger; 

import javax.annotation.Resource; 
import javax.enterprise.context.RequestScoped; 
import javax.inject.Named; 
import javax.jms.ConnectionFactory; 
import javax.jms.JMSContext; 
import javax.jms.JMSException; 
import javax.jms.Queue; 
import javax.jms.QueueBrowser; 
import javax.jms.TextMessage; 

@Named 
@RequestScoped 
public class MessageQueueBrowser { 

 @Resource private ConnectionFactory connectionFactory; @Resource(mappedName = "jms/JavaEE8BookQueue") private Queue queue; 
    private static final Logger LOG =  
    Logger.getLogger(MessageQueueBrowser.class.getName()); 

    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()) { 
                    LOG.log(Level.INFO, "There are no messages " 
                            + "in the queue."); 
                } else { 
                    LOG.log(Level.INFO, 
                            "The following messages are in the    
                             queue:"); 
 while (messageEnumeration.hasMoreElements()) { textMessage = (TextMessage)  
                         messageEnumeration.nextElement(); LOG.log(Level.INFO, textMessage.getText()); } 
                } 
            } 
        } catch (JMSException e) { 
            LOG.log(Level.SEVERE, "JMS Exception caught", e); 
        } 
    } 

    public static void main(String[] args) { 
        new MessageQueueBrowser().browseMessages(); 
    } 
} 

如我们所见,在消息队列中浏览消息的程序很简单。我们以通常的方式获取一个 JMS 连接工厂、一个 JMS 队列和一个 JMS 上下文,然后在 JMS 上下文对象上调用createBrowser()方法。此方法返回javax.jms.QueueBrowser接口的实现。此接口包含一个getEnumeration()方法,我们可以调用它来获取包含队列中所有消息的Enumeration。要检查队列中的消息,我们只需遍历这个枚举并逐个获取消息。在上面的例子中,我们简单地调用了队列中每个消息的getText()方法。

消息主题

当我们的 JMS 代码使用发布/订阅(pub/sub)消息域时,使用消息主题。当使用此消息域时,相同的消息可以发送给所有订阅该主题的订阅者。

向消息主题发送消息

以下示例说明了如何向消息主题发送消息:

package net.ensode.javaee8book.jmspubsubproducer; 

import java.util.logging.Level; 
import java.util.logging.Logger; 
import javax.annotation.Resource; 
import javax.enterprise.context.RequestScoped; 
import javax.inject.Named; 
import javax.jms.ConnectionFactory; 
import javax.jms.JMSContext; 
import javax.jms.JMSProducer; 
import javax.jms.Topic; 

@Named 
@RequestScoped 
public class MessageSender { 

    @Resource 
    private ConnectionFactory connectionFactory; 
 @Resource(mappedName = "jms/JavaEE8BookTopic") 
    private Topic topic; 
    private static final Logger LOG = 
        Logger.getLogger(MessageSender.class.getName()); 

    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!"; 

        LOG.log(Level.INFO, "Sending the following message: {0}", 
            msg1); 
 jmsProducer.send(topic, msg1); 
        LOG.log(Level.INFO, "Sending the following message: {0}",  
            msg2); 
 jmsProducer.send(topic, msg2); 
        LOG.log(Level.INFO, "Sending the following message: {0}", 
            msg3); 
 jmsProducer.send(topic, msg3); 

    } 
} 

如我们所见,前面的代码几乎与我们在讨论点对点消息时看到的MessageSender类相同。实际上,唯一不同的代码行是那些被突出显示的。JMS API 就是这样设计的,以便应用程序开发者不需要学习两个不同的 API 来处理 PTP 和 pub/sub 域。

由于代码几乎与消息队列部分中的相应示例相同,我们只解释两个示例之间的差异。在这个例子中,我们不是声明一个实现javax.jms.Queue类的实例,而是声明一个实现javax.jms.Topic类的实例。然后,我们将这个javax.jms.Topic的实例作为我们JMSProducer对象的send()方法的第一参数传递,同时传递我们希望发送的消息。

从消息主题接收消息

正如向消息主题发送消息几乎与向消息队列发送消息相同一样,从消息主题接收消息几乎与从消息队列接收消息相同:

package net.ensode.javaee8book.jmspubsubconsumer; 

import java.util.logging.Level; 
import java.util.logging.Logger; 
import javax.annotation.Resource; 
import javax.enterprise.context.RequestScoped; 
import javax.inject.Named; 
import javax.jms.ConnectionFactory; 
import javax.jms.JMSConsumer; 
import javax.jms.JMSContext; 
import javax.jms.Topic; 

@Named 
@RequestScoped 
public class MessageReceiver { 

    @Resource 
    private ConnectionFactory connectionFactory; 
 @Resource(mappedName = "jms/JavaEE8BookTopic") private Topic topic; 
    private static final Logger LOG = 
      Logger.getLogger(MessageReceiver.class.getName()); 

    public void receiveMessages() { 
        String message; 
        boolean goodByeReceived = false; 

        JMSContext jmsContext = connectionFactory.createContext(); 
 JMSConsumer jMSConsumer = jmsContext.createConsumer(topic); 

        LOG.log(Level.INFO, "Waiting for messages..."); 
        while (!goodByeReceived) { 
 message = jMSConsumer.receiveBody(String.class); 

            if (message != null) {
                System.out.print(
                "Received the following message: ");
                LOG.log(Level.INFO, message);
                if (message.equals("Good bye!")) {
                    goodByeReceived = true;
                }
            }
        } 
    } 
} 

再次强调,此代码与 PTP 对应代码之间的差异微乎其微。我们不是声明一个实现javax.jms.Queue类的实例,而是声明一个实现javax.jms.Topic类的类,并使用@Resource注解将此类的实例注入到我们的代码中,使用我们在配置应用服务器时使用的 JNDI 名称。然后,我们像之前一样获取JMSContextJMSConsumer的实例,然后通过在JMSConsumer上调用receiveBody()方法从主题接收消息。

如本节所示,使用 pub/sub 消息域的优势在于可以将消息发送到多个消息消费者。这可以通过同时执行本节中开发的MessageReceiver类的两个实例来轻松测试,然后执行上一节中开发的MessageSender类。我们应该看到每个实例的控制台输出,表明两个实例都接收到了所有消息。

创建持久订阅者

使用 pub/sub 消息域的缺点是,消息消费者必须在消息发送到主题时正在执行。如果消息消费者在那时没有执行,它将不会收到消息,而在 PTP 中,消息将保留在队列中,直到消息消费者执行。幸运的是,JMS API 提供了一种使用 pub/sub 消息域并保留消息在主题中直到所有已订阅的消息消费者执行并接收消息的方法。这可以通过为 JMS 主题创建持久订阅者来实现。

为了能够服务持久订阅者,我们需要设置我们的 JMS 连接工厂的ClientId属性。每个持久订阅者都必须有一个唯一的客户端 ID,因此,必须为每个潜在的持久订阅者声明一个唯一的连接工厂。

设置 JMS 连接工厂的 ClientId 属性的步骤取决于所使用的应用服务器。附录,配置和部署到 GlassFish 中有关于在 GlassFish 上设置此属性的说明。当使用其他 Java EE 兼容的应用服务器时,请查阅您的应用服务器文档以获取设置此属性的说明。

无效的 ClientId 异常?只有一个 JMS 客户端可以连接到特定客户端 ID 的主题。如果有多个 JMS 客户端尝试使用相同的连接工厂获取 JMS 连接,将会抛出一个表示客户端 ID 已经被使用的 JMSException。解决方案是为每个可能接收来自持久主题消息的潜在客户端创建一个连接工厂。

一旦我们设置了我们的应用服务器以能够提供持久订阅,我们就可以编写一些代码来利用它们:

package net.ensode.javaee8book.jmspubsubdurablesubscriber; 

import java.util.logging.Level; 
import java.util.logging.Logger; 
import javax.annotation.Resource; 
import javax.enterprise.context.ApplicationScoped; 
import javax.inject.Named; 
import javax.jms.ConnectionFactory; 
import javax.jms.JMSConsumer; 
import javax.jms.JMSContext; 
import javax.jms.Topic; 

@Named 
@ApplicationScoped 
public class MessageReceiver { 

 @Resource(mappedName = "jms/JavaEE8BookDurableConnectionFactory") private ConnectionFactory connectionFactory; 
    @Resource(mappedName = "jms/JavaEE8BookTopic") 
    private Topic topic; 
    private static final Logger LOG = 
      Logger.getLogger(MessageReceiver.class.getName()); 

    public void receiveMessages() { 
        String message; 
        boolean goodByeReceived = false; 

        JMSContext jmsContext = connectionFactory.createContext(); 
 JMSConsumer jMSConsumer = jmsContext.createDurableConsumer(topic, "Subscriber1"); 

        LOG.log(Level.INFO, "Waiting for messages..."); 
        while (!goodByeReceived) { 
            message = jMSConsumer.receiveBody(String.class); 

              if (message != null) {
                LOG.log(Level.INFO, 
                  "Received the following message: {0}", message);
                if (message.equals("Good bye!")) {
                    goodByeReceived = true;
                }
            }
        } 

    } 

} 

如我们所见,前面的代码与之前用于检索消息的示例没有太大区别。与之前的示例相比,只有两个不同之处:我们注入的 ConnectionFactory 实例被设置为处理持久订阅,并且我们不是在 JMS 上下文对象上调用 createConsumer() 方法,而是在调用 createDurableConsumer()createDurableConsumer() 方法接受两个参数:一个用于检索消息的 JMS Topic 对象,以及一个指定此订阅名称的 String。这个第二个参数对于所有订阅持久主题的订阅者必须是唯一的。

摘要

在本章中,我们介绍了如何在 GlassFish 中使用 GlassFish 网络控制台设置 JMS 连接工厂、JMS 消息队列和 JMS 消息主题。我们还介绍了如何通过 javax.jms.JMSProducer 接口向消息队列发送消息。此外,我们还介绍了如何通过 javax.jms.JMSConsumer 接口从消息队列接收消息。我们还介绍了如何通过实现 javax.jms.MessageListener 接口异步地从消息队列接收消息,并展示了如何使用上述接口向和从 JMS 消息主题发送和接收消息。我们还探讨了如何通过 javax.jms.QueueBrowser 接口浏览消息队列中的消息而不从队列中删除这些消息。最后,我们看到了如何设置和与 JMS 主题的持久订阅进行交互。

第九章:保护 Java EE 应用程序

Java EE 8 引入了一个新的安全 API,该 API 标准化了所有 Java EE 8 兼容应用程序服务器的应用程序安全。该 API 包括对身份存储的标准化访问,允许以统一的方式从关系或 LDAP 数据库检索用户凭证,并允许我们实现自定义身份存储的访问。新的 Java EE 8 API 包括对认证机制的支持,允许我们以标准方式验证用户。支持多种认证机制,例如基本 HTTP 认证、客户端证书、HTML 表单等。

在本章中,我们将介绍以下主题:

  • 身份存储

  • 认证机制

身份存储

身份存储提供对持久化存储系统的访问,例如关系数据库或LDAP(轻量级目录访问协议)数据库,其中存储用户凭证。Java EE 安全 API 直接支持关系和 LDAP 数据库,并且允许我们在必要时与自定义身份存储集成。

在关系数据库中设置存储的身份存储

要对存储在关系数据库中的凭证进行身份验证,例如 Servlet 或 JAX-RS RESTful Web 服务,我们需要在应用程序范围的 CDI bean 上使用@DatabaseIdentityStoreDefinition注解,如下面的示例所示。

package net.ensode.javaee8book.httpauthdatabaseidentitystore.security; 

import javax.enterprise.context.ApplicationScoped; 
import javax.security.enterprise.identitystore.DatabaseIdentityStoreDefinition; 

@DatabaseIdentityStoreDefinition( 
        dataSourceLookup = "jdbc/userAuth", 
        callerQuery = "select password from users where name = ?", 
        groupsQuery = "select g.GROUP_NAME from " 
                + "USER_GROUPS ug, users u, " 
                + "GROUPS g where u.USERNAME=? " 
                + "and ug.USER_ID = u.user_id " 
                + "and g.GROUP_ID= ug.GROUP_ID" 
) 
@ApplicationScoped 
public class ApplicationConfig { 

} 

在我们的示例中,包含用户凭证的关系数据库的 JNDI 名称为jdbc/userAuth,这是我们提供给@DatabaseIdentityStoreDefinition注解的dataSourceLookup属性的值。

@DatabaseIdentityStoreDefinitioncallerQuery参数用于指定用于检索我们正在验证的用户的用户名和密码的 SQL 查询。从数据库检索的值必须与用户提供的值匹配(通过稍后在本章中介绍的认证机制)。

大多数受保护的应用程序都有不同类型的用户,这些用户被分为不同的角色,例如,一个应用程序可能有“普通”用户和管理员。管理员将能够执行普通用户无法执行的操作。例如,管理员可以重置用户密码并从系统中添加或删除用户。@DatabaseIdentityStoreDefinitiongroupsQuery属性允许我们检索用户的全部角色。

在 LDAP 数据库中设置存储的身份存储

要保护存储在 LDAP 数据库中的凭证的资源,我们需要使用@LdapIdentityStoreDefinition注解来注解要保护的资源(例如 servlet 或 JAX-RS RESTful Web 服务),以下示例说明了如何进行此操作:

package net.ensode.javaee8book.httpauthdatabaseidentitystore.servlet; 
import java.io.IOException; 
import javax.security.enterprise.identitystore.
LdapIdentityStoreDefinition; 
import javax.servlet.ServletException; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse;  

@LdapIdentityStoreDefinition( 
        url = "ldap://myldapserver:33389/", 
        callerBaseDn = "ou=caller,dc=packtpub,dc=com", 
        groupSearchBase = "ou=group,dc=packtpub,dc=com") 
@WebServlet(name = "ControllerServlet", urlPatterns =   
 {"/controller"}) 
public class ControllerServlet extends HttpServlet { 

    @Override 
    protected void doGet(
    HttpServletRequest req, HttpServletResponse res)  
            throws ServletException, IOException { 
        System.out.println("doGet() invoked"); 
    } 
} 

@LdapIdentityStoreDefinitionurl属性用于指定包含我们应用程序用户凭据的 LDAP 服务器的 URL,其callerBaseDn属性用于指定用于验证用户提供的用户凭据的 LDAP 基本区分名称。最后,其groupSearchBase属性用于检索用户的角色。

自定义身份存储

在某些情况下,我们可能需要将我们的应用程序安全性与安全 API 未直接支持的身份存储进行集成,例如,我们可能需要集成现有的商业安全产品。对于此类情况,Java EE 安全 API 允许我们自定义身份存储定义。

要处理自定义身份存储,我们需要创建一个应用程序范围的 CDI Bean;该 Bean 必须实现IdentityStore接口,如下面的示例所示:

package net.ensode.javaee8book.security.basicauthexample; 

import java.util.Arrays; 
import java.util.HashSet; 
import java.util.Set; 
import javax.annotation.PostConstruct; 
import javax.enterprise.context.ApplicationScoped; 
import javax.security.enterprise.credential.Credential; 
import javax.security.enterprise.credential.UsernamePasswordCredential; 
import javax.security.enterprise.identitystore.CredentialValidationResult; 
import javax.security.enterprise.identitystore.IdentityStore; 

@ApplicationScoped 
public class DummyIdentityStore implements IdentityStore { 

  Set<String> adminRoleSet; 
  Set userRoleSet; 
  Set userAdminRoleSet; 

  @PostConstruct 
  public void init() { 
    adminRoleSet = new HashSet<>(Arrays.asList("admin")); 
    userRoleSet = new HashSet<>(Arrays.asList("user")); 
    userAdminRoleSet = new HashSet<>(Arrays.asList("user",  
     "admin")); 
  } 

  @Override 
  public CredentialValidationResult validate(Credential credential)
 { 
    UsernamePasswordCredential usernamePasswordCredential =  
            (UsernamePasswordCredential) credential; 

    CredentialValidationResult credentialValidationResult; 

    if (usernamePasswordCredential.compareTo( 
            "david", "secret")) { 
      credentialValidationResult =  
              new CredentialValidationResult("david", 
              adminRoleSet); 
    } 
    else if (usernamePasswordCredential.compareTo("alan",
 "iforgot")) { 
      credentialValidationResult =  
              new CredentialValidationResult("alan",
 userAdminRoleSet); 
    } 
    else { 
      credentialValidationResult = 
        CredentialValidationResult.INVALID_RESULT; 
    } 

    return credentialValidationResult; 
  } 
} 

validate()方法是在安全 API 提供的IdentityStore接口中定义的;在我们的示例中,我们实现此方法以便我们可以为我们的应用程序实现自定义验证。

在我们的示例中,我们将有效的凭据硬编码到代码中,但请勿在实际应用程序中这样做!

IdentityStore接口中定义的validate()方法接受一个实现Credential接口的类的实例作为其唯一参数。在我们的方法体中,我们将其向下转换为UserNamePasswordCredential,然后调用其compareTo()方法,传递预期的用户名和密码。如果提供的凭据与预期的任何一组凭据匹配,则允许用户成功登录;我们通过返回一个包含用户名和一个包含用户在我们应用程序中所有角色的SetCredentialValidationResult实例来完成此操作。

如果提供的凭据与预期的任何凭据都不匹配,则通过返回CredentialValidationResult.INVALID_RESULT来阻止用户登录。

身份验证机制

身份验证机制为用户提供了一种提供其凭据的方式,以便它们可以与身份存储进行身份验证。

Java EE 8 安全 API 提供了对基本 HTTP 身份验证的支持,这是大多数网络浏览器支持的标准身份验证机制,以及表单身份验证,其中用户通过 HTML 表单提供他们的凭据。默认情况下,表单身份验证将表单提交给 Java EE 实现提供的安全 servlet。如果我们需要更多的灵活性或更好地与其他 Java EE 技术对齐,安全 API 还提供了自定义表单身份验证,这允许我们作为应用程序开发者,对尝试访问我们应用程序的用户如何进行身份验证有更多的控制。

基本身份验证机制

通过使用 @BasicAuthenticationMechanismDefinition 注解来注解要安全化的资源(即,一个 servlet 或 JAX-RS RESTful Web 服务),可以实现基本身份验证机制:

package net.ensode.javaee8book.security.basicauthexample; 

import java.io.IOException; 
import javax.annotation.security.DeclareRoles; 
import javax.security.enterprise.authentication.mechanism.http.BasicAuthenticationMechanismDefinition; 
import javax.servlet.ServletException; 
import javax.servlet.annotation.HttpConstraint; 
import javax.servlet.annotation.ServletSecurity; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@BasicAuthenticationMechanismDefinition( 
        realmName = "Book Realm" 
) 
@WebServlet(name = "SecuredServlet", 
     urlPatterns = {"/securedServlet"}) 
@DeclareRoles({"user", "admin"}) 
@ServletSecurity( 
        @HttpConstraint(rolesAllowed = "admin")) 
public class SecuredServlet extends HttpServlet { 

    @Override 
    protected void doGet(HttpServletRequest request, 
         HttpServletResponse response) 
            throws ServletException, IOException { 
        response.getOutputStream().
             print("Congratulations, login successful."); 
    } 
} 

@BasicAuthenticationMechanismDefinition 注解的 realmName 属性值将发送到浏览器的 WWW-Authenticate 响应头中。

使用基本身份验证会导致浏览器弹出一个窗口,要求输入用户名和密码:

图片

一旦用户输入正确的凭据,则允许访问受保护的资源:

图片

表单身份验证机制

我们可以用来验证用户的一种方法是开发一个 HTML 表单来收集用户的凭据,然后将身份验证委托给 Java EE 安全 API。采用这种方法的第一步是开发一个 HTML 页面,用户可以在其中登录到应用程序,如下例所示:

<!DOCTYPE html> 
<html> 
    <head> 
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
        <title>Login</title> 
    </head> 
    <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> 
    </body> 
</html> 

如示例所示,用于登录的 HTML 表单必须提交一个 HTTP POST 请求,其 action 属性的值必须是 j_security_checkj_security_check 映射到 Java EE 安全 API 提供的 servlet,我们不需要自己开发任何验证逻辑。表单必须包含几个输入字段,一个用于用户名,一个用于密码,这些字段的名称必须分别是 j_usernamej_password;Java EE API 提供的安全 servlet 将检索这些值并自动验证用户。

此外,我们还需要提供一个 HTML 页面,如果登录失败,用户将被重定向到该页面。该页面可以有任何有效的 HTML 标记;在我们的示例中,我们只是提供了一个错误消息和一个链接,将用户重定向回登录页面,以便他们可以再次尝试登录:

<!DOCTYPE html> 
<html> 
    <head> 
        <meta http-equiv="Content-Type" content="text/html; 
         charset=UTF-8"> 
        <title>Login Error</title> 
    </head> 
    <body> 
        There was an error logging in. 
        <br /> 
        <a href="login.html">Try again</a> 
    </body> 
</html> 

在服务器端,我们所需做的就是使用 @FormAuthenticationMechanismDefinition 注解注解受保护的资源,这将让 Java EE 安全 API 知道我们正在使用基于表单的身份验证,以及要使用的登录或登录失败时显示的 HTML 页面:

package net.ensode.javaee8book.httpauthdbidentitystore; 

import java.io.IOException; 
import javax.annotation.security.DeclareRoles; 
import javax.security.enterprise.authentication.mechanism.http.FormAuthenticationMechanismDefinition; 
import javax.security.enterprise.authentication.mechanism.http.LoginToContinue; 
import javax.security.enterprise.identitystore.DatabaseIdentityStoreDefinition; 
import javax.security.enterprise.identitystore.Pbkdf2PasswordHash; 
import javax.servlet.ServletException; 
import javax.servlet.annotation.HttpConstraint; 
import javax.servlet.annotation.ServletSecurity; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@FormAuthenticationMechanismDefinition( 
        loginToContinue = @LoginToContinue( 
                loginPage = "/login.html", 
                errorPage = "/loginerror.html" 
        ) 
) 

@DatabaseIdentityStoreDefinition( 
        dataSourceLookup = "java:global/authDS", 
        callerQuery = "select password from users where USERNAME =     
         ?", 
        groupsQuery = "select g.GROUP_NAME from USER_GROUPS ug, 
        users u, GROUPS g where ug.USER_ID = u.user_id and  
        g.GROUP_ID= ug.GROUP_ID and u.USERNAME=?", 
        hashAlgorithm = Pbkdf2PasswordHash.class, 
        hashAlgorithmParameters = { 
            "Pbkdf2PasswordHash.Iterations=3072", 
            "Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512", 
            "Pbkdf2PasswordHash.SaltSizeBytes=64" 
        } 
) 
@DeclareRoles({"user", "admin"}) 
@WebServlet("/securedServlet") 
@ServletSecurity( 
        @HttpConstraint(rolesAllowed = {"admin"})) 
public class SecuredServlet extends HttpServlet { 

    @Override 
    protected void doGet(HttpServletRequest request, 
     HttpServletResponse response) 
            throws ServletException, IOException { 
        response.getWriter().write("Congratulations, login  
        successful."); 
    } 
} 

@FormAuthenticationMechanismDefinition 注解有一个必需的 loginToContinue 属性;此属性的值必须是 @LoginToContinue 注解的一个实例。@LoginToContinue 有两个必需的属性,loginPageerrorPage;这些属性的值必须分别指示登录页面的路径和在身份验证失败时显示的页面的路径。

在构建和部署我们的代码后,尝试访问受保护的资源,用户将被自动重定向到我们的登录页面:

图片

如果用户输入正确的凭据,则允许访问受保护的资源:

图片

如果输入了无效的凭证,则用户将被重定向到我们的错误页面:

图片

自定义表单认证机制

在我们的应用程序中,我们还可以通过使用自定义表单认证机制来验证用户;当我们需要将应用程序与一个 Web 框架,如 JSF 集成时,这种认证机制非常有用。在接下来的示例中,我们将展示如何做到这一点,通过自定义表单认证将 Java EE 安全 API 与 JSF 集成。

要在我们的应用程序中使用自定义表单认证,我们需要使用名为@CustomFormAuthenticationMechanismDefinition的注解,如下面的示例所示:

package net.ensode.javaee8book.httpauthdbidentitystore; 

import java.io.IOException; 
import javax.annotation.security.DeclareRoles; 
import javax.security.enterprise.authentication.mechanism.http.CustomFormAuthenticationMechanismDefinition; 
import javax.security.enterprise.authentication.mechanism.http.LoginToContinue; 
import javax.security.enterprise.identitystore.DatabaseIdentityStoreDefinition; 
import javax.security.enterprise.identitystore.Pbkdf2PasswordHash; 
import javax.servlet.ServletException; 
import javax.servlet.annotation.HttpConstraint; 
import javax.servlet.annotation.ServletSecurity; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@CustomFormAuthenticationMechanismDefinition( 
    loginToContinue = @LoginToContinue( 
        loginPage="/faces/login.xhtml", 
        errorPage="" 
    ) 
) 

@DatabaseIdentityStoreDefinition( 
        dataSourceLookup = "java:global/authDS", 
        callerQuery = "select password from users where USERNAME =  
         ?", 
        groupsQuery = "select g.GROUP_NAME from USER_GROUPS ug, " 
                + "users u, GROUPS g where ug.USER_ID = u.user_id " 
                + "and g.GROUP_ID= ug.GROUP_ID and u.USERNAME=?", 
                 hashAlgorithm = Pbkdf2PasswordHash.class, 
        hashAlgorithmParameters = { 
            "Pbkdf2PasswordHash.Iterations=3072", 
            "Pbkdf2PasswordHash.Algorithm=PBKDF2WithHmacSHA512", 
            "Pbkdf2PasswordHash.SaltSizeBytes=64" 
        } 
) 
@DeclareRoles({"user", "admin"}) 
@WebServlet("/securedServlet") 
@ServletSecurity( 
        @HttpConstraint(rolesAllowed = {"admin"})) 
public class SecuredServlet extends HttpServlet { 

    @Override 
    protected void doGet(HttpServletRequest request,  
     HttpServletResponse response) 
            throws ServletException, IOException { 
        response.getWriter().write("Congratulations, login  
        successful."); 
    } 
} 

就像之前看到的@FormAuthenticationMechanismDefinition注解一样,@CustomFormAuthenticationMechanismDefinition注解有一个loginToContinue属性,它接受一个@LoginToContinue注解的实例作为其值。在这种情况下,由于我们正在与 JSF 集成,@LoginToContinueloginPage属性值必须指向用户登录所使用的 Facelets 页面的路径。当使用 JSF 进行用户认证时,预期登录页面会在认证失败时显示错误消息,因此我们需要将@LoginToContinueerrorPage属性留空。

我们的登录页面是一个标准的 Facelets 页面,它收集用户凭证并将重定向到一个充当控制器的 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>Login</title> 
    </h:head> 
    <h:body> 
        <h:form> 
            <h:messages/> 
            <h:panelGrid columns="2"> 
                <h:outputLabel for="userName"  
                   value="User Name:"/> 
                  <h:inputText id="userName"
 value="#{user.userName}"/> 
                <h:outputLabel for="password"  
                  value="Password: "/> 
                  <h:inputSecret id="password" 
                    value="#{user.password}"/> 
                <h:panelGroup/> 
               <h:commandButton  
                  action="#{loginController.login()}" 
 value="Login"/> 
            </h:panelGrid> 
        </h:form> 
    </h:body> 
</html> 

我们的登录页面有用户名和密码的输入字段;它通过值绑定表达式将这些值存储到一个 CDI 命名 bean 中(未显示,因为它很 trivial)。当用户点击登录按钮时,控制权转移到执行实际认证的LoginController CDI 命名 bean:

package net.ensode.javaee8book.httpauthdbidentitystore.customauth; 

import javax.enterprise.context.RequestScoped; 
import javax.faces.application.FacesMessage; 
import javax.faces.context.ExternalContext; 
import javax.faces.context.FacesContext; 
import javax.inject.Inject; 
import javax.inject.Named; 
import javax.security.enterprise.AuthenticationStatus; 
import javax.security.enterprise.SecurityContext; 
import javax.security.enterprise.authentication.mechanism.http.AuthenticationParameters; 
import javax.security.enterprise.credential.UsernamePasswordCredential; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@Named 
@RequestScoped 
public class LoginController { 

    @Inject 
    private SecurityContext securityContext; 

    @Inject 
    private User user; 

    public void login() { 
        FacesContext facesContext = 
        FacesContext.getCurrentInstance(); 
        ExternalContext externalContext =
             facesContext.getExternalContext(); 
        HttpServletRequest httpServletRequest = 
             (HttpServletRequest) externalContext.getRequest(); 
        HttpServletResponse httpServletResponse = 
             (HttpServletResponse) externalContext.getResponse(); 
       UsernamePasswordCredential uNamePasswordCredential = 
 new UsernamePasswordCredential(user.getUserName(),
 user.getPassword()); 

        AuthenticationParameters authenticationParameters =
 AuthenticationParameters.withParams().credential(
 uNamePasswordCredential); 

        AuthenticationStatus authenticationStatus =
 securityContext.authenticate(httpServletRequest,
 httpServletResponse, authenticationParameters); 

        if (authenticationStatus.equals(
 AuthenticationStatus.SEND_CONTINUE)) { 
            facesContext.responseComplete(); 
        } else if (authenticationStatus.equals(
 AuthenticationStatus.SEND_FAILURE)) { 
            FacesMessage facesMessage = new FacesMessage(
 "Login error"); 
            facesContext.addMessage(null, facesMessage); 
        } 

    } 
} 

在我们的LoginController类中,我们需要注入一个javax.security.enterprise.SecurityContext的实例,因为我们将在认证时需要它。我们的login()方法是实现认证逻辑的地方。首先我们需要做的是创建一个UsernamePasswordCredential的实例,将用户输入的用户名和密码作为参数传递给其构造函数。

我们首先通过在AuthenticationParameters上调用静态的withParams()方法,然后在该AuthenticationParameters的实例上调用credential()方法,创建一个javax.security.enterprise.authentication.mechanism.http.AuthenticationParameters的实例。然后,我们将刚刚创建的UserNamePasswordCredential实例作为参数传递;这会返回另一个AuthenticationParameters的实例,我们可以使用它来实际验证用户输入的凭证。

我们通过在SecurityContext实例上调用authenticate()方法来验证用户输入的凭据,将 HTTP 请求和响应对象作为参数传递,以及包含用户输入凭据的AuthenticationParameters实例。这个方法调用将返回一个AuthenticationStatus实例,我们需要检查返回的实例以确定用户是否输入了有效的凭据。

如果SecurityContext.authenticate()返回AuthenticationStatus.SEND_CONTINUE,则用户输入的凭据是有效的,我们可以允许用户访问请求的资源。如果该方法返回AuthenticationStatus.SEND_FAILURE,则用户输入的凭据是无效的,我们需要阻止用户访问受保护的资源。

在部署和运行我们的应用程序后,当用户尝试访问受保护的资源时,他们将被自动重定向到登录页面,在这种情况下,由于我们使用自定义表单认证,它是通过 JSF 实现的:

图片

输入正确的凭据将直接将用户导向受保护的资源(未显示),而输入错误的凭据则将用户重定向回登录页面,该页面应显示适当的错误消息:

图片

摘要

在本章中,我们介绍了 Java EE 8 中引入的新安全 API。我们介绍了如何访问不同类型的身份存储来检索用户凭据,例如关系数据库或 LDAP 数据库。我们还介绍了安全 API 如何提供与自定义身份存储集成的能力,以防我们需要访问一个直接不支持的身份存储。

此外,我们看到了如何使用不同的认证机制来允许访问我们的受保护 Java EE 应用程序。这包括如何实现所有网络浏览器提供的基本认证机制,以及如何实现基于表单的认证机制,其中我们提供用于认证的自定义 HTML 页面。此外,我们还看到了如何使用安全 API 提供的自定义表单认证机制,以便我们可以将我们的应用程序安全性与 JSF 等网络框架集成。

第十章:使用 JAX-RS 的 RESTful 网络服务

表征状态转移REST)是一种架构风格,在这种风格中,网络服务被视为资源,并且可以通过统一资源标识符URIs)来识别。

使用这种风格开发的网络服务被称为 RESTful 网络服务。

JAX-RS 在 Java EE 规范的第 6 版中成为 Java EE 的一部分,尽管在此之前它已经作为一个独立的 API 提供。在本章中,我们将介绍如何通过 JAX-RS API 开发 RESTful 网络服务。

本章将涵盖以下主题:

  • RESTful 网络服务和 JAX-RS 简介

  • 开发一个简单的 RESTful 网络服务

  • 开发 RESTful 网络服务客户端

  • 路径参数

  • 查询参数

  • 服务器端发送事件

RESTful 网络服务和 JAX-RS 简介

RESTful 网络服务非常灵活。它们可以消费多种不同类型的 MIME 类型,尽管它们通常被编写为消费和/或生成 XML 或JSONJavaScript 对象表示法)。

网络服务必须支持以下四种 HTTP 方法之一:

  • GET - 按照惯例,GET请求用于检索现有资源

  • POST - 按照惯例,POST请求用于更新现有资源

  • PUT - 按照惯例,PUT请求用于创建新资源

  • DELETE - 按照惯例,DELETE请求用于删除现有资源

我们通过创建一个带有注解方法的类来开发一个 RESTful 网络服务,这些方法在收到上述 HTTP 请求之一时将被调用。一旦我们开发和部署了我们的 RESTful 网络服务,我们需要开发一个客户端来向我们的服务发送请求。JAX-RS 包括一个标准的客户端 API,我们可以使用它来开发 RESTful 网络服务客户端。

开发一个简单的 RESTful 网络服务

在本节中,我们将开发一个简单的网络服务来展示我们如何使服务中的方法响应不同的 HTTP 请求方法。

使用 JAX-RS 开发 RESTful 网络服务简单直接。我们的每个 RESTful 网络服务都需要通过其唯一资源标识符URI)来调用。这个 URI 由@Path注解指定,我们需要使用它来装饰我们的 RESTful 网络服务资源类。

在开发 RESTful 网络服务时,我们需要开发当我们的网络服务接收到 HTTP 请求时将被调用的方法。我们需要实现方法来处理 RESTful 网络服务处理的四种类型之一或多种请求:GETPOSTPUT和/或DELETE

JAX-RS API 提供了四个注解,我们可以使用这些注解来装饰我们的网络服务中的方法。这些注解的名称分别是@GET@POST@PUT@DELETE。在我们的网络服务中用其中一个注解装饰方法,将使其能够响应相应的 HTTP 方法。

此外,我们服务中的每个方法都必须产生和/或消费特定的 MIME 类型。需要产生的 MIME 类型需要使用@Produces注解进行指定。同样,需要消费的 MIME 类型必须使用@Consumes注解进行指定。

以下示例说明了我们刚刚解释的概念:

请注意,这个示例实际上并没有做什么;示例的目的是说明如何使我们的 RESTful Web 服务资源类中的不同方法响应不同的 HTTP 方法。

package com.ensode.javaee8book.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; 
import javax.ws.rs.core.MediaType; 

@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(MediaType.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 将是:

example.com:8080/jaxrsintro/resources/customer

由于 Web 浏览器在指向 URL 时会生成一个GET请求,因此我们可以通过将浏览器指向我们服务的 URI 来简单地测试我们服务的 GET 方法。

注意,我们课程中的每个方法都使用@GET@POST@PUT@DELETE中的一个注解进行标注。这些注解使得我们的方法能够响应相应的 HTTP 方法。

此外,如果我们的方法需要向客户端返回数据,我们将在@Produces注解中声明返回数据的 MIME 类型。在我们的示例中,只有getCustomer()方法向客户端返回数据。我们希望以 XML 格式返回数据,因此我们将@Produces注解的值设置为 JAX-RS 提供的常量MediaType.TEXT_XML,其值为"text/xml"。同样,如果我们的方法需要从客户端消费数据,我们需要指定要消费的数据的 MIME 类型。这是通过@Consumes注解完成的。在我们的服务中,除了getCustomer()之外的所有方法都消费数据。在所有情况下,我们期望数据以 XML 格式存在,因此我们再次指定MediaType.TEXT_XML作为要消费的 MIME 类型。

为我们的应用程序配置 REST 资源路径

如前一小节简要提到的,在成功部署使用 JAX-RS 开发的 RESTful Web 服务之前,我们需要为我们的应用程序配置 REST 资源路径。我们可以通过开发一个扩展javax.ws.rs.core.Application的类,并用@ApplicationPath注解来装饰它来实现这一点。

通过@ApplicationPath注解进行配置

在开发针对 Java EE 现代版本的程序时,在许多情况下,编写web.xml部署描述符并不是必需的;JAX-RS 也不例外。我们可以通过注解在 Java 代码中配置 REST 资源路径。

要配置我们的 REST 资源路径,而无需依赖于web.xml部署描述符,我们只需要编写一个扩展javax.ws.ApplicationPath的类,并用@ApplicationPath注解装饰它。传递给此注解的值是我们服务的 REST 资源path

以下代码示例说明了这个过程:

package com.ensode.javaee8book..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注解装饰。该类必须是公共的,可以具有任何名称,并且可以放在任何包中。

测试我们的 Web 服务

正如我们之前提到的,网络浏览器会将GET请求发送到我们指向的任何 URL。因此,测试我们对服务发出的GET请求的最简单方法就是直接将浏览器指向我们的服务 URI:

网络浏览器只支持GETPOST请求。要通过浏览器测试POST请求,我们必须编写一个包含具有我们的服务 URI 作为 action 属性值的 HTML 表单的 Web 应用程序。虽然对于一个单一的服务来说这很简单,但对我们开发的每个 RESTful Web 服务都这样做可能会变得繁琐。

幸运的是,有一个名为 curl 的开源命令行工具,我们可以用它来测试我们的 Web 服务。curl 包含在大多数 Linux 发行版中,并且可以轻松地下载到 Windows、mac OS X 和几个其他平台,详情请访问curl.haxx.se/

curl 可以向我们的服务发送所有四种请求方法(GETPOSTPUTDELETE)。我们的服务器响应将简单地显示在命令行控制台上。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 http://localhost:8080/jaxrsintro/resources/customer

提交上述两个命令之一并检查应用程序服务器日志后,我们应该看到我们添加到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 类型,格式如前例所示。

我们可以通过检查应用服务器日志来验证前面的命令是否按预期工作:

INFO: --- com.ensode.jaxrsintro.service.CustomerResource.createCustomer() invoked
INFO: customerXML = <customer><id>321</id><firstName>Amanda</firstName><middleName>Zoe</middleName><lastName>Adams</lastName></customer>

我们可以同样容易地测试其他request方法类型:

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

这将在应用服务器日志中产生以下输出:

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

结果在应用服务器日志中产生以下输出:

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。在实际应用中,我们更有可能解析从客户端接收到的 XML,并使用它来填充一个 Java 对象。此外,我们需要返回给客户端的任何 XML 都必须从一个 Java 对象构建。

将数据从 Java 转换为 XML 然后再转换回来是一个非常常见的用例,Java EE 规范提供了一个 API 来完成这个任务。这个 API 就是Java API for XML BindingJAXB)。

JAXB 使得将数据从 Java 转换为 XML 变得透明且简单;我们所需做的只是用@XmlRootElement注解装饰我们希望转换为 XML 的类。以下代码示例说明了如何进行此操作:

package com.ensode.javaee8book.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; 
  } 

  //getters and setters omitted for brevity 

  @Override 
  public String toString() { 
    return "id = " + getId() + "\nfirstName = " + getFirstName() 
        + "\nmiddleName = " + getMiddleName() + "\nlastName = " 
        + getLastName(); 
  } 
} 

如我们所见,除了在类级别上的@XmlRootElement注解之外,上述 Java 类并没有什么异常之处。

一旦我们有一个装饰了@XmlRootElement注解的类,我们需要将我们的 Web 服务的参数类型从String更改为我们的自定义类:

package com.ensode.javaee8book.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 包括一个标准的客户端 API,我们可以使用它来轻松地开发 RESTful Web 服务客户端。

以下示例说明了如何使用 JAX-RS 客户端 API:

package com.ensode.javaee8book.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 Web 服务的 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()方法来创建一个。此方法的第一参数是我们希望传递给我们的 RESTful 网络服务的对象,第二个参数是我们将传递给 RESTful 网络服务的数据的 MIME 类型的字符串表示。put()方法的第二个参数是客户端期望从服务中得到的响应类型。在调用put()方法后,一个 HTTP PUT请求被发送到我们的 RESTful 网络服务,并且带有@Put注解的方法(在我们的例子中是createCustomer())被调用。还有类似的get()post()delete()方法,我们可以调用以向我们的 RESTful 网络服务发送相应的 HTTP 请求。

查询和路径参数

在我们之前的例子中,我们正在使用一个 RESTful 网络服务来管理单个客户对象。在现实生活中,这显然不会很有帮助。一个常见的案例是开发一个 RESTful 网络服务来处理一组对象(在我们的例子中是客户)。为了确定我们在集合中处理的是哪个特定对象,我们可以向我们的 RESTful 网络服务传递参数。我们可以使用两种类型的参数,查询路径参数。

查询参数

我们可以在处理我们的网络服务中的 HTTP 请求的方法中添加参数。带有@QueryParam注解的参数将从请求 URL 中检索。

以下示例说明了如何在我们的 JAX-RS RESTful 网络服务中使用查询参数:

package com.ensode.javaee8book.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.javaee8book.queryparamsclient; 

import com.ensode.javaee8book.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()方法来传递参数。此方法的第一参数是参数名称,必须与网络服务上的@QueryParam注解的值匹配。第二个参数是我们需要传递给网络服务的值。如果我们的网络服务接受多个参数,我们可以链式调用queryParam()方法,每个参数使用一个queryParam()方法。

路径参数

我们还可以通过path参数将参数传递给我们的 RESTful 网络服务。以下示例说明了如何开发一个接受path参数的 JAX-RS RESTful 网络服务:

package com.ensode.javaee8book.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参数的方法都必须用@Path注解装饰。此注解的值属性必须格式化为"{paramName}/",其中paramName是方法期望接收的参数。此外,方法参数必须用@PathParam注解装饰。此注解的值必须与方法上@Path注解中声明的参数名称匹配。

我们可以通过调整我们的网络服务的 URI 来从命令行传递path参数。例如,要将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 将路径参数发送到网络服务既简单又直接;我们只需要添加几个方法调用来指定路径参数及其值。以下示例说明了如何做到这一点:

package com.ensode.javaee8book..pathparamsclient; 

import com.ensode.javaee8book.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()方法。此方法将指定的path追加到我们的WebTarget实例。此方法的价值必须与我们的 RESTful 网络服务中@Path注解的值匹配。

在我们的WebTarget实例上调用path()方法后,我们需要调用resolveTemplate()。此方法的第一参数是参数的名称(不带花括号),第二个参数是我们希望作为参数传递给我们的 RESTful 网络服务的值。

如果我们需要将多个参数传递给我们的某个网络服务,我们只需在方法级别的@Path参数中使用以下格式:

@Path("/{paramName1}/{paramName2}/") 

然后,用@PathParam注解标注相应的方法参数:

public String someMethod(@PathParam("paramName1") String param1, 
@PathParam("paramName2") String param2) 

然后,通过修改网络服务的 URI 来传递参数,按照@Path注解中指定的顺序调用网络服务。例如,以下 URI 将传递paramName1paramName2的值12

http://localhost:8080/contextroot/resources/customer/1/2

上述 URI 既可以通过命令行使用,也可以通过我们使用 JAX-RS 客户端 API 开发的 Web 服务客户端使用。

服务器端发送事件

通常,Web 服务和其客户端之间的每次交互都是由客户端发起的;客户端发送一个请求(GETPOSTPUTDELETE),然后从服务器接收响应。服务器端发送事件技术允许 RESTful Web 服务“主动”向客户端发送消息,即发送不是响应客户端请求的数据。服务器端发送事件对于向客户端连续发送数据非常有用,例如股票行情、新闻源、体育比分等。

JAX-RS 2.1 引入了服务器端发送事件支持。以下示例演示了如何将此功能实现到我们的 JAX-RS RESTful Web 服务中:

package net.ensode.javaee8book.jaxrs21sse; 

import java.util.List; 
import java.util.concurrent.Executor; 
import java.util.concurrent.Executors; 
import java.util.concurrent.TimeUnit; 
import java.util.stream.Collectors; 
import java.util.stream.Stream; 
import javax.ws.rs.GET; 
import javax.ws.rs.Path; 
import javax.ws.rs.Produces; 
import javax.ws.rs.core.Context; 
import javax.ws.rs.core.MediaType; 
import javax.ws.rs.sse.OutboundSseEvent; 
import javax.ws.rs.sse.Sse; 
import javax.ws.rs.sse.SseEventSink; 

@Path("serversentevents") 
public class SseResource { 

    List<Float> stockTickerValues = null; 
    Executor executor = Executors.newSingleThreadExecutor(); 

    @GET 
    @Produces(MediaType.SERVER_SENT_EVENTS) 
 public void sendEvents(@Context SseEventSink sseEventSink,       
      @Context Sse sse) { 
        initializeStockTickerValues(); 
        executor.execute(() -> { 
            stockTickerValues.forEach(value -> { 
                try { 
                    TimeUnit.SECONDS.sleep(5); 
                    System.out.println(String.format( 
                      "Sending the following value: %.2f", value)); 
 final OutboundSseEvent outboundSseEvent = sse.newEventBuilder() .name("ENSD stock ticker value") .data(String.class,     
                      String.format("%.2f", value)) .build(); sseEventSink.send(outboundSseEvent); 
                } catch (InterruptedException ex) { 
                    ex.printStackTrace(); 
                } 

            }); 

        }); 
    } 

    private void initializeStockTickerValues() { 
        stockTickerValues = Stream.of(50.3f, 55.5f, 62.3f,  
         70.7f, 10.1f, 5.1f).collect(Collectors.toList()); 
    } 
} 

上述示例模拟了向客户端发送虚构公司的股票价格。为了向客户端发送服务器端发送的事件,我们需要利用SseEventSinkSse类的实例,正如我们在示例中所展示的那样。这两个类都通过@Context注解注入到我们的 RESTful Web 服务中。

要发送一个事件,我们首先需要通过Sse实例的newEventBuilder()方法构建一个OutboundSseEvent实例。此方法创建一个OutboundSseEvent.Builder实例,然后用于创建必要的OutboundSseEvent实例。

我们通过在OutboundSseEvent.Builder实例上调用name()方法给我们的事件命名,然后通过其data()方法设置要发送给客户端的数据。data()方法接受两个参数:第一个是我们发送给客户端的数据类型(在我们的例子中是String),第二个是我们实际发送给客户端的数据。

一旦我们通过相应的方法设置了事件名称和数据,我们就通过在OutboundSseEvent.Builder上调用build()方法来构建一个OutboundSseEvent实例。

一旦我们构建了OutboundSseEvent实例,我们就通过将其作为参数传递给SseEventSinksend()方法将其发送到客户端。在我们的示例中,我们遍历模拟的股票价格并将其发送到客户端。

JavaScript 服务器端发送事件客户端

到目前为止,我们所有的客户端示例要么使用了 curl 命令行工具,要么使用了 JAX-RS RESTful Web 服务客户端 API。使用在浏览器上运行的 JavaScript 代码作为 RESTful Web 服务客户端是非常常见的,因此,在本节中,我们将采用这种方法。以下示例演示了一个 HTML/JavaScript 客户端接收服务器端发送的事件:

<!DOCTYPE html> 
<html> 
    <head> 
        <title>Stock Ticker Monitor</title> 
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
    </head> 
    <body onload="getStockTickerValues()"> 
        <h2>Super fancy stock ticker monitor</h2> 
        <table cellspacing="0" cellpadding="0"> 
            <tr> 
                <td>ENSD Stock Ticker Value: </td> 
                <td> <span id="stickerVal"></span></td> 
            </tr> 
            <tr> 
                <td></td><td><button>Buy!</button></td> 
            </tr> 
        </table> 
        <script> 
 function getStockTickerValues() { var source = new                   
                 EventSource("webresources/serversentevents/"); source.addEventListener('ENSD stock ticker value', function (event) { document.getElementById("stickerVal").
                      innerHTML = event.data; }, false); } 
        </script> 
    </body> 
</html> 

getStockTickerValues() JavaScript 函数创建了一个 EventSource 对象。这个构造函数接受一个表示发送事件的服务器 URL 的 String 作为参数。在我们的例子中,我们使用了一个相对 URL,因为前面的 HTML/JavaScript 代码托管在与服务器代码相同的服务器上。如果不是这种情况,我们就需要使用一个完整的 URL。

我们通过向我们的 EventSource 实例添加事件监听器来执行客户端接收到事件时要执行的功能。这个函数接受事件名称(注意,该值与我们在 Java 代码中为我们的 RESTful 网络服务发送的名称相匹配),以及一个在接收到事件时要执行的功能。在我们的例子中,我们只是简单地更新一个 <span> 标签的内容,以显示接收到的消息数据。

摘要

在本章中,我们讨论了如何使用 JAX-RS,Java EE 规范的新增内容,轻松地开发 RESTful 网络服务。

我们通过在我们的代码中添加几个简单的注解来开发 RESTful 网络服务。我们还解释了如何利用 Java API for XML BindingJAXB)自动在 Java 和 XML 之间转换数据。

此外,我们还介绍了如何通过 @PathParam@QueryParam 注解将参数传递给我们的 RESTful 网络服务。

最后,我们讨论了如何开发能够向所有客户端发送服务器端事件的网络服务,利用新的 JAX-RS 2.1 服务器端事件支持。

第十一章:使用 Java EE 进行微服务开发

微服务是一种将代码部署在小而粒度化的模块中的架构风格。微服务架构减少了耦合并增加了内聚。通常,微服务被实现为 RESTful Web 服务,通常使用 JSON 通过调用 HTTP 方法(GETPOSTPUTDELETE)在彼此之间传递数据。由于微服务之间的通信是通过 HTTP 方法完成的,因此用不同编程语言编写的微服务可以相互交互。在本章中,我们将介绍如何使用 Java EE 实现微服务。

在本章中,我们将介绍以下主题:

  • 微服务简介

  • 微服务架构的优势

  • 微服务架构的劣势

  • 使用 Java EE 开发微服务

微服务简介

将应用程序设计为一系列微服务相对于传统设计应用程序有一些优势,但也存在一些劣势。在考虑为我们的应用程序采用微服务架构时,我们必须在做出决定之前仔细权衡利弊。

微服务架构的优势

将应用程序作为一系列微服务开发具有比传统设计应用程序的多个优势:

  • 更小的代码库:由于每个微服务都是一个小的、独立的单元,因此微服务的代码库通常比传统设计的应用程序更小,更容易管理。

  • 微服务鼓励良好的编码实践:微服务架构鼓励松耦合和高内聚。

  • 更高的容错性:传统设计的应用程序作为一个单点故障;如果应用程序的任何组件出现故障或不可用,整个应用程序将不可用。由于微服务是独立的模块,一个组件(即一个微服务)出现故障并不一定导致整个应用程序不可用。

  • 可伸缩性:由于作为一系列微服务开发的应用程序由多个不同的模块组成,因此可伸缩性变得更容易;我们只需关注可能需要扩展的服务,而无需在不需要扩展的应用程序部分上浪费精力。

微服务架构的劣势

无论使用哪种编程语言或应用程序框架来开发应用程序,开发并部署遵循微服务架构的应用程序都会带来其自身的挑战:

  • 额外的操作和工具开销:每个微服务实现都需要其自己的(可能是自动化的)部署、监控系统等。

  • 调试微服务可能比调试传统的企业应用程序更复杂:如果最终用户报告了他们应用程序的问题,并且该应用程序内部使用了多个微服务,那么并不总是清楚哪个微服务可能是罪魁祸首。如果涉及的微服务是由不同团队开发,且优先级不同,这可能会特别困难。

  • 分布式事务可能是一个挑战:涉及多个微服务的回滚事务可能很难。一种常见的解决方案是尽可能地将微服务隔离,将它们视为单一单元,然后为每个微服务进行本地事务管理。例如,如果微服务 A 调用了微服务 B,如果微服务 B 存在问题,微服务 B 的本地事务将回滚,然后它将返回 HTTP 状态码500(服务器错误)给微服务 A。微服务 A 可以使用这个 HTTP 状态码作为信号来启动补偿事务,使系统恢复到初始状态。

  • 网络延迟:由于微服务依赖于 HTTP 方法调用来进行通信,性能有时会因网络延迟而受到影响。

  • 潜在的复杂依赖性:虽然独立的微服务往往很简单,但它们相互依赖。微服务架构可能创建一个复杂的依赖图。如果我们的某些服务依赖于其他团队开发的微服务,而这些团队可能有冲突的优先级(例如,如果我们发现他们的微服务中存在一个错误,然而,修复这个错误可能不是其他团队的优先事项),这种情况可能会令人担忧。

  • 容易受到分布式计算谬误的影响:按照微服务架构开发的应用程序可能会做出一些不正确的假设,例如网络可靠性、零延迟、无限带宽等。

微服务与 Java EE

有些人可能认为 Java EE 对于微服务开发来说“过于重量级”,但这根本不是事实。正因为这种误解,有些人可能会认为 Java EE 可能不适合微服务架构,然而实际上,Java EE 非常适合微服务开发。在过去,Java EE 应用程序被部署到“重量级”的应用服务器上。如今,大多数 Java EE 应用服务器供应商都提供轻量级的应用服务器,这些服务器使用的内存或磁盘空间非常少。这些 Java EE 兼容的轻量级应用服务器的例子包括 IBM 的 Open Liberty、Red Hat 的 WildFly Swarm、Apache TomEE 和 Payara Micro。

使用 Java EE 开发微服务涉及编写标准的 Java EE 应用程序,同时将自己限制在 Java EE API 的某个子集——通常是 JAX-RS 和 JSON-P 或 JSON-B,以及一些其他,如 CDI,如果与关系数据库交互,则是 JPA。Java EE 开发者可以在开发微服务时利用他们现有的专业知识。主要要求是使用 JAX-RS 开发 RESTful 网络服务。然后,将这些网络服务打包在 WAR 文件中,并像往常一样部署到轻量级应用程序服务器上。

当使用现代、可嵌入的 Java EE 应用程序服务器时,通常每个应用程序服务器实例只部署一个应用程序,在某些情况下,可以说“形势逆转”,应用程序服务器只是一个应用程序作为依赖项使用的库。使用这些现代应用程序服务器,通常会在服务器上部署多个应用程序服务器实例,这使得现代 Java EE 特别适合微服务开发。许多现代、轻量级的 Java EE 应用程序服务器是可嵌入的,允许创建一个“超级 jar”,它包含应用程序代码和应用程序服务器库。然后,将这个“超级 jar”传输到服务器上并作为独立应用程序运行。除了“超级 jar”之外,现代应用程序服务器还可以添加到容器镜像(如 Docker)中,然后应用程序可以作为瘦 war 部署,通常只有几 KB 大小;这种方法具有非常快速部署的优势,通常在 2 秒以内。

通过部署到符合当代 Java EE Web Profile 的应用程序服务器(或,如前一段所述,创建一个“超级 jar”),Java EE 开发者当然可以利用他们现有的专业知识来开发符合微服务规范的应用程序。

使用 Java EE 开发微服务

现在我们已经对微服务进行了简要介绍,我们准备查看一个使用 Java EE 编写的示例微服务应用程序。我们的示例应用程序对大多数 Java EE 开发者来说应该非常熟悉。它是一个简单的 CRUD创建、读取、更新、删除)应用程序。作为一系列微服务的开发,该应用程序将遵循熟悉的 MVC 设计模式,其中“视图”和“控制器”作为微服务进行开发。该应用程序还将利用非常常见的 DAO 模式,我们的 DAO 也作为微服务进行开发。

实际上,示例代码不是一个完整的 CRUD 应用程序。为了简单起见,我们决定只实现 CRUD 应用程序的“创建”部分。

我们将使用 Payara Micro 来部署我们的示例代码。Payara Micro 是从 GlassFish 派生出的轻量级 Java EE 应用程序服务器,它是开源的,可以免费获取,并支持 Java EE 网络配置文件,该配置文件包括所有 Java EE 规范的一个子集,即安全、Bean 验证、CDI、EJB Lite(提供完整 EJB 功能的一个子集)、统一表达式语言、JAX-RS、JDBC、JNDI、JPA、JSF、JSON-P、JSP、Servlets 和 WebSockets。

可以在www.payara.fish/downloads.下载 Payara Micro。

我们的应用程序将开发为三个模块:首先是一个微服务客户端,其次是 MVC 设计模式中的控制器微服务实现,然后是实现为微服务的 DAO 设计模式。

开发微服务客户端代码

在深入开发我们的服务之前,我们首先将开发一个微服务客户端,其形式为一个 HTML5 页面,使用流行的 Twitter Bootstrap CSS 库以及无处不在的 jQuery JavaScript 库。前端服务中的 JavaScript 代码将调用控制器微服务,传递用户输入数据的 JSON 表示。然后,控制器服务将调用持久化服务并将数据保存到数据库中。每个微服务都将返回一个 HTTP 状态码,指示成功或错误条件。

我们客户端代码中最相关的部分是 HTML 表单以及提交表单到我们的控制器微服务的 jQuery 代码。

我们在这里只展示代码的小片段。示例应用的完整代码可以在以下位置找到:

github.com/dheffelfinger/Java-EE-8-Application-Development-Code-Samples.

我们 HTML5 页面中的表单标记如下所示:

<form id="customerForm"> 
    <div class="form-group"> 
        <label for="salutation">Salutation</label><br/> 
        <select id="salutation" name="salutation" 
            class="form-control" style="width: 100px !important;"> 
            <option value=""> </option> 
            <option value="Mr">Mr</option> 
            <option value="Mrs">Mrs</option> 
            <option value="Miss">Miss</option> 
            <option value="Ms">Ms</option> 
            <option value="Dr">Dr</option> 
        </select> 
    </div> 
    <div class="form-group"> 
        <label for="firstName">First Name</label> 
        <input type="text" maxlength="10" class="form-control"
         id="firstName" name="firstName"  placeholder="First Name"> 
    </div> 
    <div class="form-group"> 
        <label for="middleName">Middle Name</label> 
        <input type="text" maxlength="10" class="form-control"
         id="middleName" name="middleName" placeholder="Middle 
         Name"> 
    </div> 
    <div class="form-group"> 
        <label for="lastName">Last Name</label> 
        <input type="text" maxlength="20" class="form-control"
         id="lastName" name="lastName" placeholder="Last Name"> 
    </div> 
    <div class="form-group"> 
        <button type="button" id="submitBtn" 
         class="btn btn-primary">Submit</button> 
    </div> 
</form> 
As we can see, this is a standard HTML form using Twitter Bootstrap CSS classes. Our page also has a script to send form data to the controller microservice. 
<script> 
  $(document).ready(function () { 
      $("#submitBtn").on('click', function () { 
          var customerData = $("#customerForm").serializeArray(); 
          $.ajax({ 
              headers: { 
                  'Content-Type': 'application/json' 
              }, 
              crossDomain: true, 
              dataType: "json", 
              type: "POST", 
              url:   
          "http://localhost:8180/CrudController/webresources/customercontroller/", 
              data: JSON.stringify(customerData) 
            }).done(function (data, textStatus, jqXHR) { 
              if (jqXHR.status === 200) { 
                  $("#msg").removeClass(); 
                  $("#msg").toggleClass("alert alert-success"); 
                  $("#msg").html("Customer saved successfully."); 
              } else { 
                  $("#msg").removeClass(); 
                  $("#msg").toggleClass("alert alert-danger"); 
                  $("#msg").html("There was an error saving 
                   customer data."); 
              } 
          }).fail(function (data, textStatus, jqXHR) { 
              console.log("ajax call failed"); 
              console.log("data = " + JSON.stringify(data)); 
              console.log("textStatus = " + textStatus); 
              console.log("jqXHR = " + jqXHR); 
              console.log("jqXHR.status = " + jqXHR.status); 
          }); 
      }); 
  }); 
</script> 

当页面上的提交按钮被点击时,将调用脚本。它使用 jQuery 的serializeArray()函数收集用户输入的表单数据,并创建一个带有该数据的 JSON 格式数组。serializeArray()函数创建一个 JSON 对象数组。数组中的每个元素都有一个与 HTML 标记上的名称属性匹配的名称属性,以及一个与用户输入值匹配的value属性。

例如,如果用户在问候下拉菜单中选择了"Mr",在名字字段中输入了"John",中间名留空,并以"Doe"作为姓氏,生成的 JSON 数组将如下所示:

[{"name":"salutation","value":"Mr"},{"name":"firstName","value":"John"},{"name":"middleName","value":""},{"name":"lastName","value":"Doe"}] 

注意,上述 JSON 数组中每个"name"属性的值与 HTML 表单中的"name"属性匹配;相应的"value"属性与用户输入的值匹配。

由于生成的 HTTP 请求将被发送到 Payara Micro 的不同实例,即使我们将所有微服务部署到同一服务器(或在我们的情况下,部署到我们的本地工作站),我们也需要将Ajax设置对象的crossDomain属性设置为true

注意,Ajax设置对象的url属性值端口为8180,当我们部署时,我们需要确保我们的控制器微服务正在监听这个端口。

我们可以从命令行将我们的 View 微服务部署到 Payara Micro,如下所示:

java -jar payara-micro-4.1.2.173.jar --noCluster --deploy /path/to/CrudView.war

Payara micro 以可执行的 JAR 文件形式分发,因此我们可以通过java -jar命令启动它。JAR 文件的准确名称将取决于你使用的 Payara Micro 版本。

默认情况下,运行在同一服务器上的 Payara Micro 实例会自动形成一个集群。对于我们的简单示例,我们不需要这个功能,因此我们使用了--noCluster命令行参数。

--deploy命令行参数用于指定我们想要部署的工件。在我们的例子中,它是一个包含作为我们示例应用程序用户界面的 HTML5 页面的 WAR 文件。

我们可以通过检查 Payara Micro 的输出以确保我们的应用程序已成功部署:

    **[2017-10-21T12:00:35.196-0400] [] [INFO] [AS-WEB-GLUE-00172] [javax.enterprise.web] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1508601635196] [levelValue: 800] Loading application [CrudView] at [/CrudView]** 

    **[2017-10-21T12:00:35.272-0400] [] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1508601635272] [levelValue: 800] CrudView was successfully deployed in 1,332 milliseconds.** 

    **[2017-10-21T12:00:35.274-0400] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1508601635274] [levelValue: 800] Deployed 1 archive(s)** 

现在,我们可以将我们的浏览器指向我们的 CrudView 应用程序 URL(在我们的例子中是http://localhost:8080/CrudView)。输入一些数据后,页面将看起来如下截图所示:

截图

当用户点击提交按钮时,客户端将用户输入数据的 JSON 表示传递给控制器服务。

控制器服务

控制器服务是 MVC 设计模式中控制器的一个标准 RESTful Web 服务实现,使用 JAX-RS 实现:

package net.ensode.javaee8book.microservices.crudcontroller.service; 
//imports omitted for brevity 
@Path("/customercontroller") 
public class CustomerControllerService { 

    public CustomerControllerService() { 
    } 

    @OPTIONS 
    public Response options() { 
        return Response.ok("") 
 .header("Access-Control-Allow-Origin", "http://localhost:8080") 
                .header("Access-Control-Allow-Headers", "origin," +  
                        "content-type, accept, authorization") 
                .header("Access-Control-Allow-Credentials", "true") 
                .header("Access-Control-Allow-Methods",  
                        "GET, POST, PUT, DELETE, OPTIONS, HEAD") 
                .header("Access-Control-Max-Age", "1209600") 
                .build(); 
    } 

    @POST 
    @Consumes(MediaType.APPLICATION_JSON) 
    public Response addCustomer(String customerJson) { 
        Response response; 
        Response persistenceServiceResponse; 
        CustomerPersistenceClient client =  
          new CustomerPersistenceClient(); 
        Customer customer = jsonToCustomer(customerJson); 
        persistenceServiceResponse = client.create(customer); 
        client.close(); 

        if (persistenceServiceResponse.getStatus() == 201) { 
            response = Response.ok("{}"). 
                    header("Access-Control-Allow-Origin", 
                           "http://localhost:8080").build(); 

        } else { 
            response = Response.serverError(). 
                    header("Access-Control-Allow-Origin",
 "http://localhost:8080").build(); 
        } 
        return response; 
    } 

    private Customer jsonToCustomer(String customerJson) { 
        Customer customer = new Customer(); 
        JsonArray jsonArray; 
        try (JsonReader jsonReader = Json.createReader( 
                new StringReader(customerJson))) { 
            jsonArray = jsonReader.readArray(); 
        } 

        for (JsonValue jsonValue : jsonArray) { 
            JsonObject jsonObject = (JsonObject) jsonValue; 
            String propertyName = jsonObject.getString("name"); 
            String propertyValue = jsonObject.getString("value"); 

            switch (propertyName) { 
                case "salutation": 
                    customer.setSalutation(propertyValue); 
                    break; 
                case "firstName": 
                    customer.setFirstName(propertyValue); 
                    break; 
                case "middleName": 
                    customer.setMiddleName(propertyValue); 
                    break; 
                case "lastName": 
                    customer.setLastName(propertyValue); 
                    break; 
                default: 
                    LOG.log(Level.WARNING, String.format( 
                            "Unknown property name found: %s", 
                             propertyName)); 
                    break; 
            } 
        } 
        return customer; 
    } 
} 

带有javax.ws.rs.OPTIONS注解的options()方法是必要的,因为浏览器在调用包含我们服务器主要逻辑的实际POST请求之前会自动调用它。在这个方法中,我们设置了一些头部值以允许跨源资源共享CORS),简单来说就是允许我们的服务从不同于我们服务运行的服务器上调用。在我们的例子中,客户端部署到了 Payara Micro 的不同实例上,因此被视为不同的源。这些头部值是必要的,以便允许我们的客户端代码和控制器服务相互通信。注意,我们明确允许来自http://localhost:8080的请求,这是我们的客户端代码部署的主机和端口。

我们控制器服务的主要逻辑位于addCustomer()方法中。此方法接收客户端发送的 JSON 字符串作为参数。在这个方法中,我们创建了一个CustomerPersistenceClient()实例,这是一个使用 JAX-RS 客户端 API 实现的持久化服务客户端。

然后,我们通过调用jsonToCustomer()方法创建一个Customer类的实例。此方法接收客户端发送的 JSON 字符串,并使用标准的 Java EE JSON-P API,用 JSON 字符串中的对应值填充Customer类的实例。

Customer 类是一个简单的 数据传输对象 (DTO),包含一些属性,与客户端表单中的输入字段相匹配,以及相应的 getter 和 setter。这个类非常简单,所以我们决定不展示它。

然后,我们的 addCustomer() 方法通过在 CustomerPersistenceClient 上调用 create() 方法来调用持久化服务,检查持久化服务返回的 HTTP 状态码,然后向客户端返回相应的状态码。

让我们现在看看我们的 JAX-RS 客户端代码的实现:

package net.ensode.javaee8book.microservices.crudcontroller.restclient; 
//imports omitted 
public class CustomerPersistenceClient { 

    private final WebTarget webTarget; 
    private final Client client; 
 private static final String BASE_URI = "http://localhost:8280/CrudPersistence/webresources"; 

    public CustomerPersistenceClient() { 
 client = javax.ws.rs.client.ClientBuilder.newClient(); webTarget = client.target(BASE_URI).path("customerpersistence"); 
    } 

    public Response create(Customer customer) throws 
      ClientErrorException { 
 return webTarget.request( javax.ws.rs.core.MediaType.APPLICATION_JSON). post(javax.ws.rs.client.Entity.entity(customer, javax.ws.rs.core.MediaType.APPLICATION_JSON), Response.class); 
    } 

    public void close() { 
        client.close(); 
    } 
} 

我们的控制器服务只使用了两个标准的 Java EE API,即 JAX-RS 和 JSON-P。正如我们所见,我们的客户端代码是一个相当简单的类。利用 JAX-RS 客户端 API,我们声明一个包含我们正在调用的服务的基础 URI 的常量(我们的持久化服务)。在其构造函数中,我们创建一个新的 javax.ws.rs.client.ClientBuilder 实例,然后设置其基础 URI 和路径,匹配我们持久化服务的适当值。我们的客户端类有一个单一的方法,该方法向持久化服务提交一个 HTTP POST 请求,然后返回它发送回来的响应。

我们可以从命令行将我们的控制器服务部署到 Payara Micro,如下所示:

java -jar payara-micro-4.1.2.173.jar --noCluster --port 8180 --deploy /path/to/CrudController.war

通过检查 Payara Micro 的输出,我们可以看到我们的代码部署成功:

[2017-10-21T12:04:06.505-0400] [] [INFO] [AS-WEB-GLUE-00172] [javax.enterprise.web] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1508601846505] [levelValue: 800] Loading application [CrudController] at [/CrudController]

[2017-10-21T12:04:06.574-0400] [] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1508601846574] [levelValue: 800] CrudController was successfully deployed in 1,743 milliseconds.

[2017-10-21T12:04:06.576-0400] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1508601846576] [levelValue: 800] Deployed 1 archive(s)

现在我们已经成功部署了我们的控制器服务,我们准备检查我们应用程序的最后一个组件,即持久化服务:

package net.ensode.javaee9book.microservices.crudpersistence.service; 

//imports omitted for brevity 
@ApplicationScoped 
@Path("customerpersistence") 
public class CustomerPersistenceService { 
    @Context 
    private UriInfo uriInfo; 
    @Inject 
    private CrudDao customerDao; 

    @POST 
    @Consumes(MediaType.APPLICATION_JSON) 
    public Response create(Customer customer) { 
        try { 
            customerDao.create(customer); 
        } catch (Exception e) { 
            return Response.serverError().build(); 
        } 
        return Response.created(uriInfo.getAbsolutePath()).build(); 
    } 
} 

在这种情况下,由于调用我们服务的客户端代码是用 Java 开发的,所以我们不需要将接收到的 JSON 字符串转换为 Java 代码;这是在幕后自动完成的。我们的 create() 方法在控制器服务向持久化服务发送 HTTP POST 请求时被调用。该方法简单地在一个实现 DAO 设计模式的类上调用 create() 方法。我们的持久化服务返回 HTTP 响应 201(已创建)。如果一切顺利,如果 DAO 的 create() 方法抛出异常,那么我们的服务将返回 HTTP 错误 500(内部服务器错误)。

我们的 DAO 作为 CDI 管理的 Bean 实现,使用 JPA 将数据插入到数据库中:

package net.ensode.microservices.crudpersistence.dao; 
//imports omitted for brevity 
@ApplicationScoped 
@Transactional 
public class CrudDao { 
    @PersistenceContext(unitName = "CustomerPersistenceUnit") 
    private EntityManager em; 

    public void create(Customer customer) { 
        em.persist(customer); 
    } 
} 

我们的 DAO 实现非常简单;它实现了一个方法,该方法在注入的 EntityManager 实例上调用 persist() 方法。

在我们的持久化服务项目中,Customer 类是一个简单的 JPA 实体。

我们现在像往常一样将我们的持久化服务部署到 Payara Micro:

java -jar payara-micro-4.1.2.173.jar --port 8280 --noCluster --deploy /path/to//CrudPersistence.war 

检查 Payara Micro 的输出,我们可以看到我们的持久化服务已成功部署:

[2017-10-21T15:15:17.361-0400] [] [INFO] [AS-WEB-GLUE-00172] [javax.enterprise.web] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1508613317361] [levelValue: 800] Loading application [CrudPersistence] at [/CrudPersistence]

[2017-10-21T15:15:17.452-0400] [] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1508613317452] [levelValue: 800] CrudPersistence was successfully deployed in 4,201 milliseconds.

[2017-10-21T15:15:17.453-0400] [] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1508613317453] [levelValue: 800] Deployed 1 archive(s)

现在我们已经部署了应用程序的所有三个组件,我们准备看到它在实际中的应用。

一旦用户输入一些数据并点击提交按钮,我们应该在我们的页面顶部看到一条 成功 消息:

如果我们查看数据库,我们应该看到用户输入的数据已成功持久化:

如我们的示例代码所示,在 Java EE 中遵循微服务架构开发应用程序非常简单;它不需要任何特殊知识。微服务使用标准的 Java EE API 进行开发,并部署到轻量级的应用服务器。我们所有的示例代码都使用了之前章节中介绍的标准 Java EE API。

摘要

如我们所见,Java EE 特别适合微服务开发。Java EE 开发者可以利用他们现有的知识来开发微服务架构并将其部署到现代、轻量级的应用服务器。传统的 Java EE 应用程序可以很好地与微服务交互,并且在有意义的场合可以迭代重构为微服务架构。无论是开发遵循微服务架构的新应用程序,重构现有应用程序为微服务,还是修改现有应用程序以与微服务交互,Java EE 开发者都可以利用他们现有的技能来完成这项任务。

第十二章:使用 JAX-WS 的 Web 服务

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 开发网络服务

JAX-WS 是一个高级 API,它简化了基于 SOAP 的网络服务的开发。JAX-WS 代表 Java API for XML Web Services。通过 JAX-WS 开发网络服务包括编写一个公共方法以供作为网络服务公开的类。该类需要用 @WebService 注解进行装饰。类中的所有公共方法都会自动公开为网络服务。它们可以选择性地用 @WebService 注解进行装饰。以下示例说明了这个过程:

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 原始参数相加并返回结果;substract() 方法从其两个参数中减去并返回结果。

我们通过用 @WebService 注解装饰类来表示该类实现了网络服务。任何我们希望公开为网络服务的方法都可以用 @WebMethod 注解进行装饰,但这不是必需的;所有公共方法都会自动公开为网络服务。我们仍然可以使用 @WebMethod 注解以提高清晰度,但这并不是部署我们的网络服务所必需的;我们只需像往常一样将其打包到 WAR 文件中即可。

网络服务客户端需要一个 WSDLWeb 服务定义语言)文件来生成它们可以用来调用网络服务的可执行代码。WSDL 文件通常放置在 Web 服务器上,并通过客户端的 URL 访问。

当部署使用 JAX-WS 开发的网络服务时,会自动为我们生成一个 WSDL。生成的 WSDL 的确切 URL 依赖于我们使用的 Java EE 8 应用服务器。当使用 GlassFish 时,JAX-WS WSDL 的 URL 格式如下:

[http|https]://[server]:[port]/[context root]/[service name]?wsdl

在我们的示例中,当我们的网络服务(部署到 GlassFish)的 WSDL 的 URL 为 http://localhost:8080/calculatorservice/CalculatorService?wsdl(假设 GlassFish 在我们的本地工作站上运行,并且 GlassFish 在其默认的 8080 端口上监听 HTTP 连接)。

开发网络服务客户端

如我们之前提到的,需要从网络服务的 WSDL 生成可执行代码。然后,网络服务客户端将调用这个可执行代码来访问网络服务。

Java 开发工具包JDK)包括一个从 WSDL 生成 Java 代码的工具。这个工具的名称是 wsimport。它可以在 $JAVA_HOME/bin 下找到。wsimport 的唯一必需参数是与网络服务对应的 WSDL 的 URL,例如:

wsimport http://localhost:8080/calculatorservice/CalculatorService?wsdl

上述命令将生成多个编译后的 Java 类,允许客户端应用程序访问我们的网络服务:

  • Add.class

  • AddResponse.class

  • Calculator.class

  • CalculatorService.class

  • ObjectFactory.class

  • package-info.class

  • Subtract.class

  • SubtractResponse.class

保持生成的源代码:默认情况下,生成的类文件的源代码会被自动删除;可以通过传递 -keep 参数给 wsimport 来保留它。

这些类需要添加到客户端的 CLASSPATH 中,以便它们可以被客户端代码访问。

如果我们使用 Apache Maven 来构建我们的代码,我们可以利用 JAX-WS Maven 插件在构建客户端代码时自动调用 wsimport。这种方法在下面的 pom.xml 文件中得到了说明:

<?xml version="1.0" encoding="UTF-8"?> 
<project   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 
    <modelVersion>4.0.0</modelVersion> 

    <groupId>net.ensode.javaee8book</groupId> 
    <artifactId>calculatorserviceclient</artifactId> 
    <version>1.0</version> 
    <packaging>war</packaging> 

    <name>calculatorserviceclient</name> 

    <properties> 
        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir> 
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
    </properties> 

    <dependencies> 
        <dependency> 
            <groupId>javax</groupId> 
            <artifactId>javaee-api</artifactId> 
            <version>7.0</version> 
            <scope>provided</scope> 
        </dependency> 
    </dependencies> 

    <build> 
        <plugins> 
        <plugin> 
                <groupId>org.codehaus.mojo</groupId> 
                <artifactId>jaxws-maven-plugin</artifactId> 
                <version>2.4.1</version> 
                <executions> 
                    <execution> 
                        <goals> 
                            <goal>wsimport</goal> 
                        </goals> 
                        <configuration> 
                            <vmArgs> 
                            <vmArg>-Djavax.xml.accessExternalSchema=all</vmArg> 
                            </vmArgs> 
                            <wsdlUrls> 
                               <wsdlUrl> 
                     http://localhost:8080/calculatorservice/CalculatorService?wsdl 
                              </wsdlUrl> 
                            </wsdlUrls> 
                            <keep>true</keep> 
                        </configuration> 
                    </execution> 
                </executions> 
            </plugin> 
           <!-- additional plugins removed for brevity --> 
        </plugins> 
    </build> 
</project> 

上述 pom.xml Maven 构建文件将在我们通过 mvn packagemvn install 命令构建代码时自动调用 wsimport 工具。

到目前为止,我们已经准备好开发一个简单的客户端来访问我们的网络服务。我们将实现我们的客户端作为一个 JSF 应用程序。我们客户端应用程序源代码中最相关的部分如下所示:

package net.ensode.javaee8book.calculatorserviceclient;
import javax.enterprise.context.RequestScoped;
import javax.faces.event.ActionEvent;
import javax.inject.Inject;
import javax.inject.Named;
import javax.xml.ws.WebServiceRef;
import net.ensode.javaee8book.jaxws.Calculator;
import net.ensode.javaee8book.jaxws.CalculatorService;
@Named
@RequestScoped
public class CalculatorClientController { @WebServiceRef(wsdlLocation =
          "http://localhost:8080/calculatorservice/CalculatorService?wsdl") private CalculatorService calculatorService;  
    @Inject   
    private CalculatorServiceClientModel
    calculatorServiceClientModel;   
    private Integer sum;  
    private Integer difference;   
    public void add(ActionEvent actionEvent) { Calculator calculator = 
         calculatorService.getCalculatorPort();     
        sum = calculator.add(calculatorServiceClientModel.getAddend1(),                           
        calculatorServiceClientModel.getAddend2());   
    }    
    public void subtract(ActionEvent actionEvent) { Calculator calculator = 
         calculatorService.getCalculatorPort();   
        difference = 
         calculator.subtract(calculatorServiceClientModel.getMinuend(),                             
         calculatorServiceClientModel.getSubtrahend());   
    } 
    public Integer getSum() {  
        return sum;   
    }   
    public void setSum(Integer sum) { 
        this.sum = sum;  
    }    
    public Integer getDifference() {    
        return difference; 
    }  
    public void setDifference(Integer difference) {       
        this.difference = difference; 
    } 
} 

@WebServiceRef 注解将一个网络服务实例注入到我们的客户端应用程序中。它的 wsdlLocation 属性包含我们正在调用的网络服务的 WSDL 的 URL。

注意,这个网络服务类是名为 CalculatorService 的类的实例。这个类是在我们调用 wsimport 工具时创建的,因为 wsimport 总是生成一个类,其名称是我们实现的类名加上服务后缀。我们使用这个服务类来获取我们开发的网络“服务”类的实例。在我们的例子中,我们通过在 CalculatorService 实例上调用 getCalculatorPort() 方法来实现这一点。一般来说,获取我们网络服务类实例的方法遵循 getNamePort() 的模式,其中 Name 是我们编写的实现网络服务的类的名称。一旦我们获取了我们的网络服务类实例,我们就可以像使用任何常规 Java 对象一样调用它的方法。

严格来说,服务类的getNamePort()方法返回一个实现由wsimport生成的接口的类的实例。这个接口被赋予我们的 Web 服务类的名称,并声明了我们声明的所有作为 Web 服务的方法。从所有实际目的来看,返回的对象等同于我们的 Web 服务类。

我们简单客户端应用程序的用户界面是使用 Facelets 开发的,这在开发 JSF 应用程序时是惯例:

<?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>Calculator Service Client</title> 
    </h:head> 
    <h:body> 
        <h3>Simple JAX-WS Web Service Client</h3> 
        <h:messages/> 
        <h:form> 
            <h:panelGrid columns="4"> 
                <h:inputText id="addend1"
                   value="#{calculatorServiceClientModel.addend1}"/> 
                <h:inputText id="addend2" 
                  value="#{calculatorServiceClientModel.addend2}"/> 
                <h:commandButton value="Add" 
                  actionListener="#  
                   {calculatorClientController.add}"> 
                    <f:ajax execute="addend1 addend2" 
                     render="sum"/> 
                </h:commandButton> 
                <h:panelGroup> 
                    Total: <h:outputText id="sum"
                       value="#{calculatorClientController.sum}"/> 
                </h:panelGroup> 
            </h:panelGrid> 
            <br/> 
            <h:panelGrid columns="4"> 
                <h:inputText id="minuend" 
                   value="#{calculatorServiceClientModel.minuend}"/> 
                <h:inputText id="subtrahend" 
                  value="# 
                   {calculatorServiceClientModel.subtrahend}"/> 
                <h:commandButton value="Subtract" 
                   actionListener="#
                    {calculatorClientController.subtract}"> 
                    <f:ajax execute="minuend subtrahend"  
                     render="difference"/> 
                </h:commandButton> 
                <h:panelGroup> 
                    Difference: <h:outputText id="difference" 
                      value="#
                       {calculatorClientController.difference}"/> 
                </h:panelGroup> 
            </h:panelGrid> 
        </h:form> 
    </h:body> 
</html> 

用户界面使用 Ajax 调用CalculatorClientController CDI 命名豆上的相关方法(有关详细信息,请参阅第二章,JavaServer Faces)。

在部署我们的代码后,我们的浏览器应按以下方式渲染我们的页面(在输入一些数据并点击相应的按钮后显示):

在此示例中,我们传递了Integer对象作为参数和返回值。当然,也可以将原始类型作为参数和返回值传递。不幸的是,当通过 JAX-WS 实现基于 SOAP 的 Web 服务时,并非所有标准 Java 类或原始类型都可以用作方法参数或返回值。这是因为,在幕后,方法参数和返回类型被映射到 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

我们还可以使用我们自己的自定义类作为 Web 服务方法的参数和/或返回值,但我们的类的成员变量必须是列出的类型之一。

此外,使用数组作为方法参数和返回值也是合法的;然而,在执行wsimport时,这些数组会被转换为列表,导致 Web 服务中的方法签名与客户端调用的方法调用之间产生不匹配。因此,更倾向于使用列表作为方法参数和/或返回值,因为这同样是合法的,并且不会在客户端和服务器之间产生不匹配。

JAX-WS 内部使用 Java Architecture for XML Binding (JAXB)来从方法调用创建 SOAP 消息。我们允许用于方法调用和返回值的类型是 JAXB 支持的类型。有关 JAXB 的更多信息,请参阅github.com/javaee/jaxb-v2

向 Web 服务发送附件

除了发送和接受上一节讨论的数据类型外,web service 方法还可以发送和接受文件附件。以下示例说明了如何做到这一点:

package net.ensode.javaeebook.jaxws; 

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 { 
 fileOutputStream = 
              new FileOutputStream("/tmp/logo.png");
            dataHandler.writeTo(fileOutputStream); 

            fileOutputStream.flush(); 
            fileOutputStream.close(); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 

    } 
} 

为了编写一个接收一个或多个附件的 Web 服务方法,我们只需要为方法将接收到的每个附件添加一个类型为 javax.activation.DataHandler 的参数。在先前的示例中,attachFile() 方法接受一个此类参数并将其简单地写入文件系统。

到目前为止,我们需要将我们的代码打包到 WAR 文件中,并按常规部署。一旦部署,就会自动生成 WSDL。然后,我们需要执行 wsimport 工具来生成我们的 Web 服务客户端可以使用以访问 Web 服务的代码。如前所述,wsimport 可以直接从命令行或通过 Apache Maven 插件调用。

执行 wsimport 生成访问 Web 服务的代码后,我们可以编写和编译我们的客户端代码:

package net.ensode.javaee8book.fileattachmentserviceclient; 

import java.io.ByteArrayOutputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.net.URL; 
import javax.enterprise.context.RequestScoped; 
import javax.inject.Named; 
import javax.xml.ws.WebServiceRef; 
import net.ensode.javaeebook.jaxws.FileAttachment; 
import net.ensode.javaeebook.jaxws.FileAttachmentService; 

@Named 
@RequestScoped 
public class FileAttachmentServiceClientController { 

    @WebServiceRef(wsdlLocation = "http://localhost:8080/fileattachmentservice/" 
            + "FileAttachmentService?wsdl") 
    private FileAttachmentService fileAttachmentService; 

    public void invokeWebService() { 
        try { 
            URL attachmentUrl = new URL( 
        "http://localhost:8080/fileattachmentserviceclient/resources/img/logo.png"); 

            FileAttachment fileAttachment = fileAttachmentService. 
                    getFileAttachmentPort(); 

            InputStream inputStream = attachmentUrl.openStream(); 

 byte[] fileBytes = inputStreamToByteArray(inputStream); fileAttachment.attachFile(fileBytes); 
        } catch (IOException ioe) { 
            ioe.printStackTrace(); 
        } 
    } 

    private byte[] inputStreamToByteArray(InputStream inputStream) throws 
      IOException { 
        ByteArrayOutputStream byteArrayOutputStream =  
          new ByteArrayOutputStream(); 
        byte[] buffer = new byte[1024]; 
        int bytesRead = 0; 
        while ((bytesRead = inputStream.read(buffer, 0, buffer.length)) != -1) { 
            byteArrayOutputStream.write(buffer, 0, bytesRead); 
        } 
        byteArrayOutputStream.flush(); 
        return byteArrayOutputStream.toByteArray(); 
    } 
} 

Web 服务附件需要以 byte 数组的形式发送到 Web 服务,因此,Web 服务客户端需要将需要附加的文件转换为这种类型。在我们的示例中,我们发送一个图像作为附件,通过创建一个 java.net.URL 实例并将图像的 URL 作为参数传递给其构造函数来将图像加载到内存中。然后,我们通过在 URL 实例上调用 openStream() 方法来获取与图像对应的 InputStream 实例,将我们的 InputStream 实例转换为字节数组,然后将这个字节数组传递给期望附件的 web service 方法。

注意,与传递标准参数不同,当客户端调用期望附件的方法时使用的参数类型与 Web 服务器代码中方法的参数类型不同。Web 服务器代码中的方法期望每个附件都有一个 javax.activation.DataHandler 实例;然而,由 wsimport 生成的代码期望每个附件都是一个字节数组。这些字节数组在 wsimport 生成的代码背后被转换为正确的类型(javax.activation.DataHandler)。作为应用程序开发者,我们不需要关心为什么会发生这种情况;我们只需要记住,当向 Web 服务方法发送附件时,Web 服务代码和客户端调用中的参数类型将不同。

将 EJBs 公开为 Web 服务

除了创建上一节中描述的 Web 服务外,可以通过简单地在 EJB 类上添加注解,轻松地将无状态会话 Bean 的公共方法公开为 Web 服务。以下示例说明了如何做到这一点:

package net.ensode.javaee8book.ejbws; 

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 服务的 Bean 需要部署在一个 JAR 文件中。

正如标准 Web 服务一样,EJB Web 服务的 WSDL URL 取决于所使用的应用服务器。请查阅您的应用服务器文档以获取详细信息。

EJB Web 服务客户端

以下类展示了从客户端应用程序访问 EJB Web 服务方法的步骤:

package net.ensode.javaee8book.ejbwsclient; 

import javax.enterprise.context.RequestScoped; 
import javax.inject.Inject; 
import javax.inject.Named; 
import javax.xml.ws.WebServiceRef; 
import net.ensode.javaee8book.ejbws.DecToHexBeanService; 

@Named 
@RequestScoped 
public class EjbClientController { 

    @WebServiceRef(wsdlLocation =  
    "http://localhost:8080/DecToHexBeanService/DecToHexBean?wsdl") 
    private DecToHexBeanService decToHexBeanService; 

    @Inject 
    private EjbClientModel ejbClientModel; 

    private String hexVal; 

    public void convertIntToHex() { 
        hexVal = decToHexBeanService.getDecToHexBeanPort(). 
                convertDecToHex(ejbClientModel.getIntVal()); 
    } 

    public String getHexVal() { 
        return hexVal; 
    } 

    public void setHexVal(String hexVal) { 
        this.hexVal = hexVal; 
    } 
} 

正如我们所见,当从客户端访问 EJB Web 服务时,不需要做任何特别的事情。过程与标准 Web 服务相同。

前面的类是一个 CDI(上下文依赖注入)命名 Bean,下面的截图展示了使用前面的类来调用我们的 Web 服务的简单 JSF 基于 Web 的用户界面:

图片

摘要

在本章中,我们介绍了如何通过 JAX-WS API 开发 Web 服务和 Web 服务客户端。我们解释了在使用 ANT 或 Maven 作为构建工具时如何将 Web 服务客户端的代码生成集成到 Web 服务中。我们还介绍了可以通过 JAX-WS 进行远程方法调用时可以使用的有效类型。此外,我们还讨论了如何向 Web 服务发送附件。我们还介绍了如何将 EJB 的方法公开为 Web 服务。

第十三章:Servlet 开发和部署

在本章中,我们将讨论如何开发和部署 Java servlet。Servlet 允许我们作为应用程序开发者,在 Java Web 和企业应用程序中实现服务器端逻辑。

涵盖的一些主题包括:

  • 解释什么是 servlet

  • 开发、配置、打包和部署我们的第一个 servlet

  • HTML 表单处理

  • 转发 HTTP 请求

  • 重定向 HTTP 响应

  • 在 HTTP 请求之间持久化数据

  • 通过注解初始化 servlet

  • Servlet 过滤器

  • Servlet 监听器

  • Servlet 的可插拔性

  • 以编程方式配置 Web 应用程序

  • 异步处理

  • HTTP/2 服务器推送支持

什么是 servlet?

Servlet 是一个 Java 类,用于扩展托管服务器端 Web 应用程序的服务器的功能。Servlet 可以响应请求并生成响应。所有 servlet 的基类是javax.servlet.GenericServlet,定义了一个通用的、协议无关的 servlet。

到目前为止,最常见类型的 servlet 是 HTTP servlet。这种类型的 servlet 用于处理 HTTP 请求并生成 HTTP 响应。HTTP servlet 是一个扩展了javax.servlet.http.HttpServlet类的类,它是javax.servlet.GenericServlet的子类。

Servlet 必须实现一个或多个方法来响应特定的 HTTP 请求。这些方法是从父类HttpServlet中重写的。如下表所示,这些方法的命名方式使得知道使用哪个方法是很直观的:

HTTP 请求 HttpServlet 方法
GET doGet(HttpServletRequest request, HttpServletResponse response)
POST doPost(HttpServletRequest request, HttpServletResponse response)
PUT doPut(HttpServletRequest request, HttpServletResponse response)
DELETE doDelete(HttpServletRequest request, HttpServletResponse response)

这些方法都接受相同的两个参数,即实现javax.servlet.http.HttpServletRequest接口的类的实例和实现javax.servlet.http.HttpServletResponse接口的类的实例。这些接口将在本章后面详细讨论。

应用程序开发者永远不会直接调用前面的方法,它们会在应用程序服务器接收到相应的 HTTP 请求时自动调用。

在前面列出的四种方法中,doGet()doPost()是最常用的。

当用户在浏览器中输入 servlet 的 URL、点击指向 servlet URL 的链接,或者使用GET方法提交 HTML 表单(表单的 action 指向 servlet 的 URL)时,都会生成一个 HTTP GET请求。在这些情况下,servlet 的doGet()方法中的代码都会被执行。

当用户使用POST方法提交 HTML 表单并指向 Servlet 的 URL 的动作时,通常会生成一个 HTTP POST请求。在这种情况下,doPost()方法中的 Servlet 代码将被执行。

编写我们的第一个 Servlet

在本节中,我们将开发一个简单的 Servlet 来展示如何使用 Servlet API。我们的 Servlet 代码如下所示:

package net.ensode.javaee8book.simpleapp; 

import java.io.IOException; 
import java.io.PrintWriter; 

import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@WebServlet(urlPatterns = {"/simpleservlet"}) 
public class SimpleServlet extends HttpServlet { 

    @Override 
    protected void doGet(HttpServletRequest req,    
     HttpServletResponse res) { 
        try { 
            res.setContentType("text/html"); 
            PrintWriter printWriter = res.getWriter(); 
            printWriter.println("<h2>"); 
            printWriter 
                    .println("Hello servlet world!"); 
            printWriter.println("</h2>"); 
        } catch (IOException ioException) { 
            ioException.printStackTrace(); 
        } 
    } 
} 

@WebServlet注解指定我们的类是一个 Servlet;其urlPatterns属性指定了我们的 Servlet 的相对 URL。

Servlet 也可以通过web.xml部署描述符进行配置;然而,由于 Java EE 6 基于注解的配置更受欢迎。

由于这个 Servlet 旨在用户在浏览器窗口中输入其 URL 时执行,我们需要重写父类HttpServlet中的doGet()方法。正如我们之前解释的,这个方法接受两个参数,一个是实现javax.servlet.http.HttpServletRequest接口的类的实例,另一个是实现javax.servlet.http.HttpServletResponse接口的类的实例。

尽管HttpServletRequestHttpServletResponse是接口,但应用程序开发者通常不会编写实现它们的类。当控制从 HTTP 请求传递到 Servlet 时,应用程序服务器会提供实现这些接口的对象。

我们的doGet()方法首先设置HttpServletResponse对象的内容类型为"text/html"。如果我们忘记这样做,将使用默认的内容类型"text/plain",这意味着 HTML 标签将在页面上显示,而不是由浏览器解释。

然后我们通过调用HttpServletResponse.getWriter()方法来获取java.io.PrintWriter的实例。然后我们可以通过调用PrintWriter.print()PrintWriter.println()方法(前一个示例仅使用println())将文本输出到浏览器。由于我们设置了内容类型为"text/html",浏览器会正确地解释任何 HTML 标签。

测试 Web 应用程序

为了验证 Servlet 是否已正确部署,我们需要将我们的浏览器指向应用程序的 URL,例如,http://localhost:8080/simpleapp/simpleservlet。完成此操作后,我们应该看到如下截图所示的页面:

图片

处理 HTML 表单

Servlet 很少通过直接在浏览器中输入它们的 URL 来访问。Servlet 最常见的使用是处理用户在 HTML 表单中输入的数据。在本节中,我们将展示这个过程。

包含我们应用程序表单的 HTML 文件看起来如下所示:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
<html> 
    <head> 
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
        <title>Data Entry Page</title> 
    </head> 
    <body> 
 <form method="post" action="formhandlerservlet"> 
            <table cellpadding="0" cellspacing="0" border="0"> 
                <tr> 
                    <td>Please enter some text:</td> 
                    <td>
                     <input type="text" name="enteredValue" />
                    </td>
                </tr> 
                <tr> 
                    <td></td> 
 <td><input type="submit" value="Submit"></td> 
                </tr> 
            </table> 
        </form> 
    </body> 
</html> 

表单的action属性值必须与 Servlet 的@WebServlet注解中的urlPatterns属性值匹配。由于表单的method属性值为"post",当表单提交时,我们的 Servlet 的doPost()方法将被执行。

现在我们来看看我们的 Servlet 代码:

package net.ensode.javaee8book.formhandling; 

import java.io.IOException; 
import java.io.PrintWriter; 
import javax.servlet.annotation.WebServlet; 

import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@WebServlet(urlPatterns = {"/formhandlerservlet"}) 
public class FormHandlerServlet extends HttpServlet { 

    @Override 
    protected void doPost(HttpServletRequest request,   
     HttpServletResponse response) { 
        String enteredValue; 

 enteredValue = request.getParameter("enteredValue"); 

        response.setContentType("text/html"); 

        PrintWriter printWriter; 
        try { 
            printWriter = response.getWriter(); 

            printWriter.println("<p>"); 
            printWriter.print("You entered: "); 
 printWriter.print(enteredValue); 
            printWriter.print("</p>"); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    } 
} 

如此例所示,我们通过调用request.getParameter()方法来获取用户输入的值的引用。此方法仅接受一个String对象作为其唯一参数,并且此字符串的值必须与 HTML 文件中输入字段的名称匹配。在这种情况下,HTML 文件有一个名为"enteredValue"的文本字段:

    <input type="text" name="enteredValue" />

因此 servlet 中有一行相应的代码:

    enteredValue = request.getParameter("enteredValue");

这一行用于获取用户输入的文本并将其存储在名为enteredValueString变量中(变量的名称不需要与输入字段名称匹配,但这样命名是一种良好的实践;这使得记住变量所持有的值变得容易)。

在将前面三个文件打包成名为formhandling.war的 WAR 文件后,然后部署 WAR 文件,我们可以在浏览器中输入类似以下 URL 来查看渲染的 HTML 文件(确切的 URL 将取决于所使用的 Java EE 应用服务器):http://localhost:8080/formhandling

图片

用户在文本字段中输入一些文本并提交表单(无论是按“Enter”键还是点击提交按钮)后,我们应该看到 servlet 的输出:

图片

HttpServletRequest.getParameter()方法可以用来获取任何只能返回一个值的 HTML 输入字段的值(文本框、文本区域、单选下拉列表、单选按钮、隐藏字段等)。获取这些字段值的过程是相同的;换句话说,servlet 不在乎用户是在文本字段中键入了值,还是从一组单选按钮中选择它,等等。只要输入字段的名称与传递给getParameter()方法的值匹配,前面的代码就会生效。

当处理单选按钮时,所有相关的单选按钮必须具有相同的名称。调用HttpServletRequest.getParameter()方法并传递单选按钮的名称将返回所选单选按钮的值。

一些 HTML 输入字段,如复选框和多重选择框,允许用户选择多个值。对于这些字段,除了使用HttpServletRequest.getParameter()方法外,还使用HttpServletRequest.getParameterValues()方法。此方法也接受一个包含输入字段名称的String作为其唯一参数,并返回一个包含用户所选所有值的字符串数组。

以下示例说明了这种情况。我们新的 HTML 标记的相关部分如下所示:

<form method="post" action="multiplevaluefieldhandlerservlet"> 
<p>Please enter one or more options.</p> 
<table cellpadding="0" cellspacing="0" border="0"> 
  <tr> 
 <td><input name="options" type="checkbox" value="option1" /> 
    Option 1</td> 
  </tr> 
  <tr> 
 <td><input name="options" type="checkbox" value="option2" /> 
    Option 2</td> 
  </tr> 
  <tr> 
 <td><input name="options" type="checkbox" value="option3" /> 
    Option 3</td> 
  </tr> 
  <tr> 
    <td><input type="submit" value="Submit" /></td> 
  </tr> 
</table> 
</form> 

新的 HTML 文件包含一个简单的表单,有三个复选框和一个提交按钮。注意每个复选框的name属性都有相同的值。正如我们之前提到的,用户点击的任何复选框都将被发送到 servlet。

让我们现在看看将处理前面 HTML 表单的 servlet:

package net.ensode.javaee8book.multiplevaluefields; 

import java.io.IOException; 
import java.io.PrintWriter; 
import javax.servlet.annotation.WebServlet; 

import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@WebServlet(urlPatterns = {"/multiplevaluefieldhandlerservlet"}) 
public class MultipleValueFieldHandlerServlet extends HttpServlet { 

    @Override 
    protected void doPost(HttpServletRequest request, 
        HttpServletResponse response) { 
 String[] selectedOptions =  
        request.getParameterValues("options"); 

        response.setContentType("text/html"); 

        try { 
            PrintWriter printWriter = response.getWriter(); 

            printWriter.println("<p>"); 
            printWriter.print("The following options were
            selected:"); 
            printWriter.println("<br/>"); 

 if (selectedOptions != null) { for (String option : selectedOptions) { printWriter.print(option); printWriter.println("<br/>"); } } else { printWriter.println("None"); } 
            printWriter.println("</p>"); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    } 
} 

前面的代码调用 request.getParameterValues() 方法并将它的返回值赋给 selectedOptions 变量。在 doPost() 方法的更下方,代码遍历 selectedOptions 数组并在浏览器中打印选定的值。

如果没有点击复选框,request.getParameterValues() 方法将返回 null,因此在进行遍历此方法返回值之前检查 null 是一个好主意。

在将我们的新 servlet 打包到 WAR 文件并部署后,我们可以在浏览器窗口中输入其 URL 来查看实际的变化。对于大多数应用程序服务器,URL 将是 http://localhost:8080/formhandling/

图片

提交表单后,控制权转到我们的 servlet,浏览器窗口应类似于以下内容:

图片

当然,浏览器窗口中实际看到的消息将取决于用户点击了哪些复选框。

请求转发和响应重定向

在许多情况下,一个 servlet 处理表单数据,然后转移到另一个 servlet 或 JSP 进行更多处理或显示屏幕上的确认消息。有两种方法可以实现这一点,即请求可以转发或响应可以重定向到另一个 servlet 或页面。

请求转发

注意,上一节示例中显示的文本与被点击的复选框的 value 属性值匹配,而不是上一页上显示的标签。这可能会让用户感到困惑。让我们修改 servlet 以更改这些值,使它们与标签匹配,然后转发请求到另一个 servlet,该 servlet 将在浏览器中显示确认消息。

MultipleValueFieldHandlerServlet 的新版本如下所示:

package net.ensode.javaee8book.formhandling; 

import java.io.IOException; 
import java.util.ArrayList; 

import javax.servlet.ServletException; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@WebServlet(urlPatterns = {"/multiplevaluefieldhandlerservlet"}) 
public class MultipleValueFieldHandlerServlet extends HttpServlet { 

    protected void doPost(HttpServletRequest request,     
    HttpServletResponse response) { 
        String[] selectedOptions =  
        request.getParameterValues("options"); 
        ArrayList<String> selectedOptionLabels = null; 

        if (selectedOptions != null) { 
            selectedOptionLabels = new ArrayList<String>  
            (selectedOptions.length); 

            for (String selectedOption : selectedOptions) { 
                if (selectedOption.equals("option1")) { 
                    selectedOptionLabels.add("Option 1"); 
                } else if (selectedOption.equals("option2")) { 
                    selectedOptionLabels.add("Option 2"); 
                } else if (selectedOption.equals("option3")) { 
                    selectedOptionLabels.add("Option 3"); 
                } 
            } 
        } 

 request.setAttribute("checkedLabels", 
         selectedOptionLabels); 

        try { 
 request.getRequestDispatcher("confirmationservlet").
              forward(
              request, response); 
        } catch (ServletException e) { 
            e.printStackTrace(); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    } 
} 

此版本的 servlet 遍历选定的选项,并将相应的标签添加到字符串的 ArrayList 中。然后通过调用 request.setAttribute() 方法将此字符串附加到 request 对象。此方法用于将任何对象附加到请求,以便任何其他代码在稍后转发请求时都可以访问它。

在将 ArrayList 附加到请求后,我们在以下代码行中将其转发到新的 servlet:

    request.getRequestDispatcher("confirmationservlet").forward(
     request, response);

此方法的 String 参数必须与 servlet 的 @WebServlet 注解中的 urlPatterns 标签的值匹配。

到目前为止,控制权转到我们的新 servlet。此新 servlet 的代码如下所示:

package net.ensode.javaee8book.requestforward; 

import java.io.IOException; 
import java.io.PrintWriter; 
import java.util.List; 
import javax.servlet.annotation.WebServlet; 

import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@WebServlet(urlPatterns = {"/confirmationservlet"}) 
public class ConfirmationServlet extends HttpServlet { 

    @Override 
    protected void doPost(HttpServletRequest request,  
    HttpServletResponse response) { 
        try { 
            PrintWriter printWriter; 
            List<String> checkedLabels = (List<String>) request 
                    .getAttribute("checkedLabels"); 

            response.setContentType("text/html"); 
            printWriter = response.getWriter(); 
            printWriter.println("<p>"); 
            printWriter.print("The following options were  
            selected:"); 
            printWriter.println("<br/>"); 

            if (checkedLabels != null) { 
                for (String optionLabel : checkedLabels) { 
                    printWriter.print(optionLabel); 
                    printWriter.println("<br/>"); 
                } 
            } else { 
                printWriter.println("None"); 
            } 
            printWriter.println("</p>"); 
        } catch (IOException ioException) { 
            ioException.printStackTrace(); 
        } 
    } 
} 

此代码获取先前由 servlet 附加到请求的 ArrayList。这是通过调用 request.getAttribute() 方法实现的;此方法的参数必须与用于将对象附加到请求的值匹配。

一旦前面的 servlet 获取到选项标签列表,它将遍历该列表并在浏览器中显示它们:

图片

如前所述,将请求转发仅适用于与执行转发的代码处于同一上下文的其他资源(servlets 和 JSP 页面)。简单来说,我们想要转发的 servlet 或 JSP 必须打包在与调用request.getRequestDispatcher().forward()方法的代码相同的 WAR 文件中。如果我们需要将用户重定向到另一个上下文中的页面(或者在同一个服务器上的另一个 WAR 文件中部署,或者在另一个服务器上部署),我们可以通过重定向响应对象来实现。

响应重定向

前一节中描述的请求转发的缺点是,请求只能转发到同一上下文中的其他 servlets 或 JSPs。如果我们需要将用户重定向到不同上下文中的页面(部署在同一个服务器上的另一个 WAR 文件中或在不同的服务器上部署),我们需要使用HttpServletResponse.sendRedirect()方法。

为了说明响应重定向,让我们开发一个简单的 Web 应用程序,该程序要求用户选择他们最喜欢的搜索引擎,然后引导用户到他们选择的搜索引擎。此应用程序的 HTML 页面看起来如下所示:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
<html> 
    <head> 
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
        <title>Response Redirection Demo</title> 
    </head> 
    <body> 
 <form method="post" action="responseredirectionservlet"> 
            <p>Please indicate your favorite search engine.</p> 
            <table> 
 <tr> <td><input type="radio" name="searchEngine" value="http://www.google.com">Google</td> </tr> <tr> <td><input type="radio" name="searchEngine" value="http://www.bing.com">Bing</td> </tr> <tr> <td><input type="radio" name="searchEngine" value="http://www.yahoo.com">Yahoo!</td> </tr> 
                <tr> 
                    <td><input type="submit" value="Submit" /></td> 
                </tr> 
            </table> 
        </form> 
    </body> 
</html> 

上一标记代码中的 HTML 表单包含三个单选按钮,每个按钮的值对应于用户选择的搜索引擎的 URL。注意每个单选按钮的name属性值相同,即"searchEngine"。servlet 将通过调用request.getParameter()方法并传递字符串"searchEngine"来获取所选单选按钮的值,如下面的代码所示:

package net.ensode.javaee8book.responseredirection; 

import java.io.IOException; 
import java.io.PrintWriter; 
import javax.servlet.annotation.WebServlet; 

import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@WebServlet(urlPatterns = {"/responseredirectionservlet"}) 
public class ResponseRedirectionServlet extends HttpServlet { 

    @Override 
    protected void doPost(HttpServletRequest request,  
    HttpServletResponse response) 
            throws IOException { 
 String url = request.getParameter("searchEngine"); 

        if (url != null) { 
 response.sendRedirect(url); 
        } else { 
            PrintWriter printWriter = response.getWriter(); 

            printWriter.println("No search engine was selected."); 
        } 
    } 
} 

通过调用request.getParameter("searchEngine"),前面的代码将所选搜索引擎的 URL 分配给url变量。然后(在检查null之后,以防用户在未选择搜索引擎的情况下点击提交按钮),通过调用response.sendRedirect()并将url变量作为参数传递,将用户重定向到所选的搜索引擎。

此应用程序的web.xml文件应该相当简单,此处未显示(它是本书代码下载的一部分)。

在打包代码并部署后,我们可以在浏览器中输入类似以下 URL 来查看其效果:http://localhost:8080/responseredirection/

图片

点击提交按钮后,用户将被引导到他们最喜欢的搜索引擎。

应该注意的是,如前所述的重定向响应会创建一个新的 HTTP 请求到我们要重定向到的页面,因此任何请求参数和属性都会丢失:

图片

在请求之间持久化应用程序数据

在上一节中,我们看到了如何通过调用HttpRequest.setAttribute()方法将对象存储在请求中,以及稍后如何通过调用HttpRequest.getAttribute()方法检索此对象。这种方法仅在请求被转发到调用getAttribute()方法的 servlet 时才有效。如果不是这种情况,getAttribute()方法将返回 null。

有可能在请求之间持久化对象。除了将对象附加到请求对象之外,还可以将对象附加到会话对象或 servlet 上下文。这两种之间的区别在于,附加到会话的对象对不同的用户不可见,而附加到 servlet 上下文的对象是可见的。

将对象附加到会话和 servlet 上下文与将其附加到请求非常相似。要将对象附加到会话,必须调用HttpServletRequest.getSession()方法;此方法返回一个javax.servlet.http.HttpSession实例。然后我们调用HttpSession.setAttribute()方法将对象附加到会话。以下代码片段说明了这个过程:

protected void doPost(HttpServletRequest request, HttpServletResponse response) 
{ 
  . 
  . 
  . 
  Foo foo = new Foo(); //theoretical object 
 HttpSession session = request.getSession(); session.setAttribute("foo", foo); 
  . 
  . 
  . 
} 

然后,我们可以通过调用HttpSession.getAttribute()方法从会话中检索对象:

protected void doPost(HttpServletRequest request, HttpServletResponse response) 
{ 
 HttpSession session = request.getSession(); 
  Foo foo =
  (Foo)session.getAttribute("foo");
} 

注意session.getAttribute()的返回值需要转换为适当类型。这是必要的,因为此方法的返回值是java.lang.Object

将对象附加和从 servlet 上下文中检索对象的过程非常相似。servlet 需要调用getServletContext()方法(定义在一个名为GenericServlet的类中,它是HttpServlet的父类,而HttpServlet又是我们 servlet 的父类)。此方法返回一个javax.servlet.ServletContext实例,它定义了一个setAttribute()和一个getAttribute()方法。这些方法与它们的HttpServletRequestHttpSessionResponse对应方法的工作方式相同。

以下代码片段说明了将对象附加到 servlet 上下文的过程:

protected void doPost(HttpServletRequest request, HttpServletResponse response) 
{ 
  //The getServletContext() method is defined higher in 
  //the inheritance hierarchy. 
 ServletContext servletContext = getServletContext(); 

  Foo foo = new Foo(); 
  servletContext.setAttribute("foo", foo); 
  . 
  . 
  . 
} 

以下代码将foo对象附加到 servlet 上下文;此对象将可用于我们应用程序中的任何 servlet,并且将在会话之间保持相同。可以通过调用ServletContext.getAttribute()方法检索它,如下面的代码所示:

protected void doPost(HttpServletRequest request, HttpServletResponse response) 
{ 
 ServletContext servletContext = getServletContext();
  Foo foo = (Foo)servletContext.getAttribute(“foo”);
  . 
  . 
  . 
} 

此代码从请求上下文中获取foo对象;再次,由于ServletContext.getAttribute()方法,就像其对应方法一样,返回一个java.lang.Object实例,因此需要类型转换。

附加到 servlet 上下文的对象被称为具有应用范围。同样,附加到会话的对象被称为具有会话范围,而附加到请求的对象被称为具有请求范围。

通过注解将初始化参数传递给 servlet

有时将一些初始化参数传递给 servlet 是有用的;这样我们就可以确保 servlet 根据发送给它的参数以不同的方式行为。例如,我们可能希望配置 servlet 在开发和生产环境中以不同的方式行为。

在过去,servlet 初始化参数是通过web.xml中的<init-param>参数发送的。从 servlet 3.0 开始,初始化参数可以作为@WebServlet注解的initParams属性的值传递给 servlet。以下示例说明了如何做到这一点:

package net.ensode.javaee8book.initparam; 

import java.io.IOException; 
import java.io.PrintWriter; 
import javax.servlet.ServletConfig; 
import javax.servlet.ServletException; 
import javax.servlet.annotation.WebInitParam; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@WebServlet(name = "InitParamsServlet", urlPatterns = { "/InitParamsServlet"}, initParams = { @WebInitParam(name = "param1", value = "value1"), @WebInitParam(name = "param2", value = "value2")}) 
public class InitParamsServlet extends HttpServlet { 

  @Override 
  protected void doGet(HttpServletRequest request, 
          HttpServletResponse response) 
          throws ServletException, IOException { 
 ServletConfig servletConfig = getServletConfig(); String param1Val = servletConfig.getInitParameter("param1"); String param2Val = servletConfig.getInitParameter("param2"); 
    response.setContentType("text/html"); 
    PrintWriter printWriter = response.getWriter(); 

    printWriter.println("<p>"); 
    printWriter.println("Value of param1 is " + param1Val); 
    printWriter.println("</p>"); 

    printWriter.println("<p>"); 
    printWriter.println("Value of param2 is " + param2Val); 
    printWriter.println("</p>"); 
  } 
} 

如我们所见,@WebServlet注解的initParams属性的值是一个@WebInitParam注解的数组。每个@WebInitParam注解有两个属性——name,对应于参数名称,和value,对应于参数值。

我们可以通过在javax.servlet.ServletConfig类上调用getInitParameter()方法来获取我们参数的值。该方法接受一个String参数作为参数,对应于参数名称,并返回一个对应于参数值的String

每个 servlet 都有一个对应的ServletConfig实例分配给它。如本例所示,我们可以通过调用getServletConfig()方法来获取此实例,这是一个从javax.servlet.GenericServlet继承的方法,HttpServletHttpServlet的父类,我们扩展了 servlet。

在我们将 servlet 打包到 WAR 文件并部署到我们选择的 Java EE 8 应用服务器之后,我们将在浏览器中看到以下页面渲染:

图片

如我们所见,渲染的值对应于我们在每个@WebInitParam注解中设置的值。

Servlet 过滤器

过滤器是在 servlet 规范的第 2.3 版中引入的。过滤器是一个对象,可以在请求被 servlet 处理之前动态拦截请求并操纵其数据。过滤器还可以在 servlet 的doGet()doPost()方法完成后,但在输出发送到浏览器之前操纵响应。

在早期的 servlet 规范中配置过滤器的唯一方法是使用web.xml中的<filter-mapping>标签。servlet 3.0 引入了通过@WebFilter注解配置 servlet 的能力。

以下示例说明了如何做到这一点:

package net.ensode.javaee8book.simpleapp; 

import java.io.IOException; 
import java.util.Enumeration; 
import javax.servlet.Filter; 
import javax.servlet.FilterChain; 
import javax.servlet.FilterConfig; 
import javax.servlet.ServletContext; 
import javax.servlet.ServletException; 
import javax.servlet.ServletRequest; 
import javax.servlet.ServletResponse; 
import javax.servlet.annotation.WebFilter; 
import javax.servlet.annotation.WebInitParam; 

@WebFilter(filterName = "SimpleFilter", initParams = { @WebInitParam(name = "filterparam1", value = "filtervalue1")}, urlPatterns = {"/InitParamsServlet"}) 
public class SimpleFilter implements Filter { 

  private FilterConfig filterConfig; 

  @Override 
  public void init(FilterConfig filterConfig) throws 
          ServletException { 
    this.filterConfig = filterConfig; 
  } 

  @Override 
  public void doFilter(ServletRequest servletRequest, 
          ServletResponse servletResponse, FilterChain filterChain) 
          throws 
          IOException, ServletException { 
    ServletContext servletContext = 
     filterConfig.getServletContext(); 
    servletContext.log("Entering doFilter()"); 
    servletContext.log("initialization parameters: "); 
    Enumeration<String> initParameterNames = 
            filterConfig.getInitParameterNames(); 
    String parameterName; 
    String parameterValue; 

    while (initParameterNames.hasMoreElements()) { 
      parameterName = initParameterNames.nextElement(); 
      parameterValue = 
       filterConfig.getInitParameter(parameterName); 
      servletContext.log(parameterName + " = " + parameterValue); 
    } 

    servletContext.log("Invoking servlet..."); 
    filterChain.doFilter(servletRequest, servletResponse); 
    servletContext.log("Back from servlet invocation"); 

  } 

  @Override 
  public void destroy() { 
    filterConfig = null; 
  } 
} 

如示例所示,@WebFilter注解有几个我们可以用来配置过滤器的属性。其中特别重要的是urlPatterns属性。该属性接受一个String对象的数组作为其值,数组中的每个元素对应于我们的过滤器将拦截的 URL。在我们的示例中,我们拦截了一个单个的 URL 模式,这对应于我们在上一节中编写的 servlet。

@WebFilter注解中的其他属性包括可选的filterName属性,我们可以使用它来为我们的过滤器命名。如果我们没有为我们的过滤器指定名称,那么过滤器名称将默认为过滤器的类名。

如前例所示,我们可以向过滤器发送初始化参数。这与我们向 servlet 发送初始化参数的方式相同。@WebFilter注解有一个initParams属性,它接受一个@WebInitParam注解数组作为其值。我们可以通过在javax.servlet.FilterConfig上调用getInitParameter()方法来获取这些参数的值,如示例所示。

我们的过滤器相当简单,它只是在 servlet 被调用前后向服务器日志发送一些输出。部署我们的应用程序后检查服务器日志,并将浏览器指向 servlet 的 URL,应该会揭示我们的过滤器输出:

    [2017-05-31T20:02:46.044-0400] [glassfish 5.0] [INFO] [] [javax.enterprise.web] [tid: _ThreadID=112 _ThreadName=http-listener-1(5)] [timeMillis: 1496275366044] [levelValue: 800] [[
      WebModule[/servletfilter] ServletContext.log():Entering doFilter()]]

    [2017-05-31T20:02:46.045-0400] [glassfish 5.0] [INFO] [] [javax.enterprise.web] [tid: _ThreadID=112 _ThreadName=http-listener-1(5)] [timeMillis: 1496275366045] [levelValue: 800] [[
      WebModule[/servletfilter] ServletContext.log():initialization parameters: ]]

    [2017-05-31T20:02:46.045-0400] [glassfish 5.0] [INFO] [] [javax.enterprise.web] [tid: _ThreadID=112 _ThreadName=http-listener-1(5)] [timeMillis: 1496275366045] [levelValue: 800] [[
      WebModule[/servletfilter] ServletContext.log():filterparam1 = filtervalue1]]

    [2017-05-31T20:02:46.045-0400] [glassfish 5.0] [INFO] [] [javax.enterprise.web] [tid: _ThreadID=112 _ThreadName=http-listener-1(5)] [timeMillis: 1496275366045] [levelValue: 800] [[
      WebModule[/servletfilter] ServletContext.log():Invoking servlet...]]

    [2017-05-31T20:02:46.046-0400] [glassfish 5.0] [INFO] [] [javax.enterprise.web] [tid: _ThreadID=112 _ThreadName=http-listener-1(5)] [timeMillis: 1496275366046] [levelValue: 800] [[
      WebModule[/servletfilter] ServletContext.log():Back from servlet invocation]]

Servlet 过滤器当然有很多实际用途。它们可以用于分析 Web 应用程序、应用安全性和压缩数据,以及其他许多用途。

Servlet 监听器

在典型 Web 应用程序的生命周期中,会发生许多事件,例如 HTTP 请求的创建或销毁、请求或会话属性的增加、删除或修改,等等。

Servlet API 提供了一系列我们可以实现的监听器接口,以便对这些事件做出反应。所有这些接口都在javax.servlet包中,以下表格总结了它们:

监听器接口 描述
ServletContextListener 包含用于处理上下文初始化和销毁事件的方法。
ServletContextAttributeListener 包含用于对 servlet 上下文(应用程序范围)中添加、删除或替换的任何属性做出反应的方法。
ServletRequestListener 包含用于处理请求初始化和销毁事件的方法。
ServletRequestAttributeListener 包含用于对请求中添加、删除或替换的任何属性做出反应的方法。
HttpSessionListener 包含用于处理 HTTP 会话初始化和销毁事件的方法。
HttpSessionAttributeListener 包含用于对 HTTP 会话中添加、删除或替换的任何属性做出反应的方法。

要处理前表中所描述的接口处理的所有事件,我们只需实现前述接口之一,并用@WebListener接口注解它,或者通过<listener>标签在web.xml部署描述符中声明它。不出所料,使用注解注册监听器的功能是在 Servlet 规范的第 3.0 版中引入的。

所有上述接口的 API 都相当简单直观。我们将展示前述接口中的一个示例,其他接口将非常相似。

所有上述接口的 JavaDoc 可以在以下位置找到:javaee.github.io/javaee-spec/javadocs/

以下示例说明了如何实现ServletRequestListener接口,该接口可以在 HTTP 请求创建或销毁时执行操作:

    package net.ensode.javaee8book.listener;
    import javax.servlet.ServletContext;
    import javax.servlet.ServletRequestEvent;
    import javax.servlet.ServletRequestListener;
    import javax.servlet.annotation.WebListener;
    @WebListener() 
    public class HttpRequestListener implements ServletRequestListener   
   {

    @Override
    public void requestInitialized(ServletRequestEvent  
      servletRequestEvent) { 

    ServletContext servletContext =

    servletRequestEvent.getServletContext();

    servletContext.log("New request initialized");

    }

    @Override
    public void requestDestroyed(ServletRequestEvent   
      servletRequestEvent) { 

    ServletContext servletContext =

    servletRequestEvent.getServletContext();

    servletContext.log("Request destroyed");

    }
    }

如我们所见,要激活我们的监听器类,只需使用@WebListener注解即可。我们的监听器还必须实现我们之前列出的监听器接口之一。在我们的例子中,我们选择实现javax.servlet.ServletRequestListener接口;该接口有方法,当 HTTP 请求初始化或销毁时自动调用。

ServletRequestListener接口有两个方法,requestInitialized()requestDestroyed()。在我们之前的简单实现中,我们只是向日志发送了一些输出,但当然我们可以在我们的实现中做任何需要做的事情。

将我们之前开发的简单 Servlet 与监听器一起部署,我们可以在应用服务器日志中看到以下输出:

    [2017-05-31T20:15:57.900-0400] [glassfish 5.0] [INFO] [] [javax.enterprise.web] [tid: _ThreadID=109 _ThreadName=http-listener-1(2)] [timeMillis: 1496276157900] [levelValue: 800] [[ 
    WebModule[/servletlistener] ServletContext.log():New request initialized]] 

    [2017-05-31T20:15:58.013-0400] [glassfish 5.0] [INFO] [] [javax.enterprise.web] [tid: _ThreadID=109 _ThreadName=http-listener-1(2)] [timeMillis: 1496276158013] [levelValue: 800] [[ 

WebModule[/servletlistener] ServletContext.log():Request destroyed]] 

实现其他监听器接口同样简单直接。

可插拔性

当原始的 Servlet API 在 20 世纪 90 年代末发布时,编写 Servlet 是 Java 编写服务器端 Web 应用的唯一方式。从那时起,在 Servlet API 之上构建了几个标准 Java EE 和第三方框架。这些标准框架的例子包括 JSP 和 JSF,第三方框架包括 Struts、Wicket、Spring Web MVC 以及几个其他框架。

现在,极少数(如果有的话)Java Web 应用是直接使用 Servlet API 构建的;相反,绝大多数项目都利用了几个可用的 Java Web 应用框架之一。所有这些框架都在“幕后”使用 Servlet API,因此设置一个应用以使用这些框架之一始终涉及到在应用的web.xml部署描述符中进行一些配置。在某些情况下,一些应用使用多个框架,但这往往会使web.xml部署描述符变得相当大且难以维护。

Servlet 3.0 引入了可插拔性的概念。Web 应用框架的开发者现在不仅仅有一种,而是有两种方式来避免应用开发者必须修改web.xml部署描述符才能使用他们的框架。框架开发者可以选择使用注解而不是web.xml来配置他们的 Servlet;完成这一步后,要使用框架只需要将框架开发者提供的库 JAR 文件包含在应用 WAR 文件中即可。或者,框架开发者可以选择将web-fragment.xml作为 JAR 文件的一部分包含在应用框架中。

web-fragment.xml几乎与web.xml相同,主要区别在于web-fragment.xml的根元素是<web-fragment>,而不是<web-app>。以下示例演示了一个示例web-fragment.xml

<?xml version="1.0" encoding="UTF-8"?> 
<web-fragment version="3.0"  

  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd"> 
  <servlet> 
    <servlet-name>WebFragment</servlet-name> 
    <servlet-class> 
      net.ensode.glassfishbook.webfragment.WebFragmentServlet 
    </servlet-class> 
  </servlet> 
  <servlet-mapping> 
    <servlet-name>WebFragment</servlet-name> 
    <url-pattern>/WebFragment</url-pattern> 
  </servlet-mapping> 
</web-fragment> 

如我们所见,web-fragment.xml几乎与典型的web.xml相同。在这个简单的示例中,我们只使用了<servlet><servlet-mapping>元素,但所有其他通常的web.xml元素,如<filter><filter-mapping><listener>,也都是可用的。

如我们在web-fragment.xml中指定的,我们的 servlet 可以通过其 URL 模式/WebFragment来调用,因此一旦作为 Web 应用程序的一部分部署,执行我们的 servlet 的 URL 将是http://localhost:8080/webfragmentapp/WebFragment。当然,主机名、端口号和上下文根必须根据需要进行调整。

对于任何 Java EE 兼容的应用程序服务器,我们只需要将文件放置在我们打包 servlet、过滤器或/和监听器的库的META-INF文件夹中,然后将我们的库的 JAR 文件放置在包含我们的应用程序的 WAR 文件的lib文件夹中,就可以抓取web-fragment.xml中的设置。

以编程方式配置 Web 应用程序

除了允许我们通过注解和web-fragment.xml配置 Web 应用程序之外,Servlet 3.0 还允许我们在运行时以编程方式配置我们的 Web 应用程序。

ServletContext类有新的方法来以编程方式配置 servlet、过滤器监听器。以下示例演示了如何在运行时以编程方式配置 servlet,而不需要使用@WebServlet注解或 XML:

package net.ensode.javaee8book.servlet; 

import javax.servlet.ServletContext; 
import javax.servlet.ServletContextEvent; 
import javax.servlet.ServletContextListener; 
import javax.servlet.ServletException; 
import javax.servlet.ServletRegistration; 
import javax.servlet.annotation.WebListener; 

@WebListener() 
public class ServletContextListenerImpl implements 
        ServletContextListener { 

  @Override 
  public void contextInitialized( 
          ServletContextEvent servletContextEvent) { 
    ServletContext servletContext = servletContextEvent. 
            getServletContext(); 
    try { 
 ProgrammaticallyConfiguredServlet servlet = servletContext. createServlet(ProgrammaticallyConfiguredServlet.class); servletContext.addServlet(
       "ProgrammaticallyConfiguredServlet", servlet); ServletRegistration servletRegistration = servletContext. getServletRegistration( "ProgrammaticallyConfiguredServlet"); servletRegistration.addMapping( "/ProgrammaticallyConfiguredServlet"); 
    } catch (ServletException servletException) { 
      servletContext.log(servletException.getMessage()); 
    } 
  } 

  @Override 
  public void contextDestroyed( 
          ServletContextEvent servletContextEvent) { 
  } 
} 

在本例中,我们调用ServletContextcreateServlet()方法来创建我们即将配置的 servlet。此方法接受一个与我们的 servlet 类对应的java.lang.Class实例。此方法返回一个实现javax.servlet.Servlet或其任何子接口的类。

一旦我们创建了我们的 servlet,我们需要在我们的ServletContext实例上调用addServlet()来将我们的 servlet 注册到 servlet 容器。此方法接受两个参数,第一个是一个与 servlet 名称对应的String,第二个是通过调用createServlet()返回的 servlet 实例。

一旦我们注册了我们的 servlet,我们需要为它添加一个 URL 映射。为了做到这一点,我们需要在我们的ServletContext实例上调用getServletRegistration()方法,并将 servlet 名称作为参数传递。此方法返回 servlet 容器的javax.servlet.ServletRegistration实现。从这个对象中,我们需要调用其addMapping()方法,传递我们希望我们的 servlet 处理的 URL 映射。

我们的示例 servlet 非常简单,它只是在浏览器中显示一条文本消息:

package net.ensode.javaee8book.servlet; 

import java.io.IOException; 
import javax.servlet.ServletException; 
import javax.servlet.ServletOutputStream; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

public class ProgrammaticallyConfiguredServlet extends HttpServlet { 

  @Override 
  protected void doGet(HttpServletRequest request, 
          HttpServletResponse response) 
          throws ServletException, IOException { 
    ServletOutputStream outputStream = response.getOutputStream(); 

    outputStream.println( 
            "This message was generated from a servlet that was " 
            + "configured programmatically."); 
  } 
} 

在将我们的代码打包成 WAR 文件,部署到 GlassFish,并将浏览器指向适当的 URL(即,http://localhost:8080/programmaticservletwebapp/ProgrammaticallyConfiguredServlet,假设我们将应用程序打包成名为programmaticservletwebapp.war的 WAR 文件,并且没有覆盖默认上下文根),我们应该在浏览器中看到以下消息:

This message was generated from a servlet that was configured programmatically.

ServletContext接口有创建和添加 servlet 过滤器监听器的方法,它们的工作方式与addServlet()createServlet()方法非常相似,因此我们不会详细讨论它们。有关详细信息,请参阅 Java EE API 文档:javaee.github.io/javaee-spec/javadocs/

异步处理

传统上,servlet 在 Java Web 应用程序中为每个请求创建一个单独的线程。请求处理完毕后,线程会被释放供其他请求使用。这种模型对于传统 Web 应用程序来说效果相当不错,因为 HTTP 请求相对较少且间隔较远。然而,大多数现代 Web 应用程序都利用 Ajax(异步 JavaScript 和 XML)技术,这种技术使得 Web 应用程序比传统 Web 应用程序更加响应。Ajax 的一个副作用是生成比传统 Web 应用程序更多的 HTTP 请求,如果其中一些线程长时间阻塞等待资源就绪,或者执行任何需要长时间处理的事情,那么我们的应用程序可能会遭受线程饥饿。

为了缓解前一段描述的情况,Servlet 3.0 规范引入了异步处理。使用这种新功能,我们不再受限于每个请求一个线程的限制。现在我们可以创建一个单独的线程,并将原始线程返回到线程池,以便其他客户端重用。

以下示例说明了如何使用 Servlet 3.0 引入的新功能来实现异步处理:

package net.ensode.javaee8book.asynchronousservlet; 

import java.io.IOException; 
import java.util.logging.Level; 
import java.util.logging.Logger; 
import javax.servlet.AsyncContext; 
import javax.servlet.ServletException; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

@WebServlet(name = "AsynchronousServlet", urlPatterns = {
 "/AsynchronousServlet"}, asyncSupported = true) 
public class AsynchronousServlet extends HttpServlet { 

  @Override 
  protected void doGet(HttpServletRequest request, 
          HttpServletResponse response) 
          throws ServletException, IOException { 
    final Logger logger = 
            Logger.getLogger(AsynchronousServlet.class.getName()); 
    logger.log(Level.INFO, "--- Entering doGet()"); 
 final AsyncContext ac = request.startAsync(); 
    logger.log(Level.INFO, "---- invoking ac.start()"); 
 ac.start(new Runnable() { 

      @Override 
      public void run() { 
        logger.log(Level.INFO, "inside thread"); 
        try { 
          //simulate a long running process. 
          Thread.sleep(10000); 
        } catch (InterruptedException ex) { 
          Logger.getLogger(AsynchronousServlet.class.getName()). 
                  log(Level.SEVERE, null, ex); 
        } 
        try { 
 ac.getResponse().getWriter(). println("You should see this after a brief wait"); ac.complete(); 
        } catch (IOException ex) { 
          Logger.getLogger(AsynchronousServlet.class.getName()). 
                  log(Level.SEVERE, null, ex); 
        } 
      } 
    }); 
    logger.log(Level.INFO, "Leaving doGet()"); 
  } 
} 

为了确保我们的异步处理代码按预期工作,我们首先需要将@WebServlet注解的asyncSupported属性设置为 true。

要实际启动一个异步过程,我们需要在我们的 servlet 中的doGet()doPost()方法中调用接收到的HttpServletRequest实例上的startAsync()方法。此方法返回一个javax.servlet.AsyncContext实例。这个类有一个start()方法,它接受一个实现java.lang.Runnable接口的类的实例作为唯一参数。在我们的示例中,我们使用匿名内部类在行内实现了Runnable;当然,也可以使用实现Runnable接口的标准 Java 类。

当我们调用AsyncContextstart()方法时,会启动一个新的线程,并执行Runnable实例的run()方法。此线程在后台运行,doGet()方法立即返回,请求线程立即可用于服务其他客户端。重要的是要注意,尽管doGet()方法立即返回,但响应直到启动的线程完成后才提交。它可以通过在AsyncContext上调用complete()方法来表示它已完成处理。

在前面的示例中,我们向应用程序服务器日志文件发送了一些条目,以更好地说明正在发生的情况。通过观察我们的 servlet 执行后的应用程序服务器日志,我们应该注意到所有日志条目都在彼此之间的一瞬间被写入日志;消息“您应该在稍等片刻后看到此信息”不会在浏览器中显示,直到表示我们正在离开doGet()方法的日志条目被写入日志。

HTTP/2 服务器推送支持

HTTP/2 是 HTTP 协议的最新版本。它相对于 HTTP 1.1 提供了几个优点。例如,在 HTTP/2 中,浏览器和服务器之间只有一个连接,并且此连接在用户导航到另一个页面之前保持打开状态。HTTP/2 还提供了多路复用,这意味着允许浏览器向服务器发送多个并发请求。此外,HTTP/2 具有服务器推送功能,这意味着服务器可以在浏览器没有特别请求的情况下向浏览器发送资源。

HTTP/2 服务器推送支持在版本 4.0 的 servlet 规范中添加,作为 Java EE 8 的一部分发布。在本节中,我们将了解如何编写代码以利用 HTTP/2 的服务器推送功能。以下示例说明了如何实现这一点:

package net.ensode.javaee8book.servlet; 

import java.io.IOException; 
import java.io.PrintWriter; 
import javax.servlet.ServletException; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 
import javax.servlet.http.PushBuilder; 

@WebServlet(name = "ServletPushDemoServlet", urlPatterns = {"/ServletPushDemoServlet"}) 
public class ServletPushDemoServlet extends HttpServlet { 
    @Override 
    protected void doPost(HttpServletRequest request, 
     HttpServletResponse response) 
            throws ServletException, IOException { 
 PushBuilder pushBuilder = request.newPushBuilder(); 

        if (pushBuilder != null) { 
            //We know the browser is going to need the image 
            //so we push it before it even requests it. 
            //We could do the same for Javascript files, CSS, etc. 
 pushBuilder.path("images/david_heffelfinger.png"). addHeader("content-type", "image/png"). push(); 
            response.sendRedirect("response.html"); 
        } else { 
           //Gracefully handle the case when the browser does not  
           support HTTP/2\. 
        } 
    } 
} 

我们可以通过在版本 4 的 servlet 规范中引入的新PushBuilder接口将资源推送到浏览器。我们可以通过在我们的doPost()方法中作为参数获得的HttpServletRequest实例上调用新的PushBuilder()方法来获取实现PushBuilder的类的实例。

如其名称所示,PushBuilder接口实现了 Builder 模式,这意味着其大多数方法都返回一个新的PushBuilder实例,我们可以使用它,允许我们方便地将方法调用链接在一起。

我们通过从PushBuilder调用名为path()的方法来指示我们希望推送到浏览器的资源路径。此方法接受一个表示要推送的资源路径的单个String参数。以正斜杠(/)开头的路径表示绝对路径,所有其他路径表示相对于我们应用程序上下文根的路径。

一旦我们指定了资源的路径,我们可以选择性地设置一些 HTTP 头;在我们的例子中,我们正在推送一个 PNG 格式的图像,因此我们设置了适当的内容类型。

最后,我们在PushBuilder实例上调用push()方法,实际上将我们的资源推送到浏览器。

我们通过示例实现了在浏览器提交对该资源的请求之前将其推送到浏览器;在 HTTP/2 协议发布之前,这项任务是不可能的。

摘要

本章涵盖了如何开发、配置、打包和部署 servlet。我们还介绍了如何通过访问 HTTP 请求对象来处理 HTML 表单信息。此外,还涵盖了将 HTTP 请求从一个 servlet 转发到另一个 servlet,以及将 HTTP 响应重定向到不同服务器。

我们讨论了如何通过将对象附加到 servlet 上下文和 HTTP 会话来在请求之间持久化对象。我们还涵盖了 servlet API 的附加功能,包括通过注解配置 Web 应用程序,通过web-fragment.xml进行可插拔性配置,程序化 servlet 配置和异步处理。最后,我们介绍了支持 HTTP/2 服务器推送的新 Servlet 4.0 API。

第十四章:配置和部署到 GlassFish

在撰写本文时,GlassFish 5 是唯一发布的符合 Java EE 8 规范的应用服务器,因此,所有示例代码都是针对 GlassFish 进行测试的,然而,它们应该可以在任何符合 Java EE 8 规范的应用服务器上运行。

想要使用 GlassFish 运行示例代码的读者可以遵循本附录中的说明来设置它。

获取 GlassFish

可以从 javaee.github.io/glassfish/download. 下载 GlassFish。

Java EE 规范有配置文件的概念,Web Profile 实现了 Java EE 规范的子集,并缺少一些功能,例如 JMS 和一些 EJB 功能。为了能够成功部署本书中的所有示例,我们应该通过点击标有“GlassFish 5.0 - Full Platform”的链接来下载实现完整 Java EE 8 规范的 GlassFish 版本。

安装 GlassFish

GlassFish 5.0 以 zip 文件的形式分发;安装 GlassFish 与将 zip 文件解压缩到我们选择的目录一样简单。

GlassFish 假设系统中存在一些依赖项。

GlassFish 依赖项

为了安装 GlassFish 5,必须在您的工作站上安装一个较新的 Java SE 版本(需要 Java SE 8),并且 Java 可执行文件必须在您的系统 PATH 中。Java SE 8 可在 www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 下载。

进行安装

一旦 JDK 已经安装,可以通过简单地解压缩下载的压缩文件来开始 GlassFish 的安装:

所有现代操作系统,包括 Linux、Windows 和 macOS,都自带支持提取压缩 ZIP 文件的功能,有关详细信息,请参阅您的操作系统文档。

解压缩 zip 文件后,将创建一个名为 glassfish5 的新目录,这个新目录包含我们的 GlassFish 安装。

启动 GlassFish

要从命令行启动 GlassFish,请将目录更改为 [glassfish 安装目录]/glassfish5/bin,并执行以下命令:

./asadmin start-domain

上一条命令以及本章中显示的大多数命令都假设使用的是 Unix 或类 Unix 操作系统,如 Linux 或 macOS。对于 Windows 系统,初始的 ./ 是不必要的。

在执行前面的命令后不久,我们应该在终端底部看到类似以下的消息:

Waiting for domain1 to start .............. 
    Successfully started the domain : domain1 
    domain  Location: /home/heffel/glassfish-5/glassfish5/glassfish/domains/domain1 
    Log File: /home/heffel/glassfish- 
 5/glassfish5/glassfish/domains/domain1/logs/server.log 
    Admin Port: 4848 

Command start-domain executed successfully.

然后,我们可以打开一个浏览器窗口,并在浏览器地址栏中输入以下 URL:http://localhost:8080

如果一切顺利,我们应该看到一个页面,表明您的 GlassFish 服务器现在正在运行:

部署我们的第一个 Java EE 应用程序

为了进一步测试我们的 GlassFish 安装是否运行正常,我们将部署一个 WAR(Web 归档)文件,并确保它正确部署和执行。在继续之前,请从本书的代码包中下载simpleapp.war文件。

通过 Web 控制台部署应用程序

要部署simpleapp.war,打开浏览器并导航到以下 URL:http://localhost:4848。你应该会看到默认的 GlassFish 服务器管理页面:

默认情况下,GlassFish 以开发模式安装,在此模式下,访问 GlassFish Web 控制台无需输入用户名和密码。在生产环境中,强烈建议配置 Web 控制台,使其受密码保护。

在这一点上,我们应该点击主屏幕上的部署部分下的“部署应用程序”项。

要部署我们的应用程序,我们应该选择“从 GlassFish 服务器可访问的本地打包文件或目录”单选按钮,并输入我们的 WAR 文件的路径或通过点击“浏览文件...”按钮选择它:

在我们选择了我们的 WAR 文件后,会显示一些输入字段,允许我们指定几个选项。就我们的目的而言,所有默认设置都很好,我们只需简单地点击页面右上角的“确定”按钮:

一旦我们部署了应用程序,GlassFish Web 控制台会显示应用程序窗口,我们的应用程序被列为已部署应用程序之一:

要执行simpleapp应用程序,请在浏览器地址栏中输入以下 URL:http://localhost:8080/simpleapp-1.0/simpleServlet。生成的页面应该看起来像这样:

就这样!我们已经成功部署了我们的第一个 Java EE 应用程序。

通过 GlassFish 管理控制台卸载应用程序

要卸载我们刚刚部署的应用程序,请在浏览器中输入以下 URL 以登录 GlassFish 管理控制台:http://localhost:4848

然后,要么点击左侧导航面板中的“应用程序”菜单项,要么点击管理控制台主页上的“列出已部署应用程序”项。

无论哪种方式,都会带我们到应用程序管理页面:

可以通过从已部署应用程序列表中选择应用程序并点击列表上方“卸载”按钮来简单地卸载应用程序:

通过命令行部署应用程序

有两种方式可以通过命令行部署应用程序:一种是将我们想要部署的工件复制到 autodeploy 目录,另一种是使用 GlassFish 的 asadmin 命令行工具。

自动部署目录

现在我们已经卸载了 simpleapp.war 文件,我们准备使用命令行来部署它。要以这种方式部署应用程序,只需将 simpleapp.war 复制到 [glassfish 安装目录]/glassfish4/glassfish/domains/domain1/autodeploy。只需将应用程序复制到这个目录,它就会自动部署。

我们可以通过查看服务器日志来验证应用程序是否已成功部署。服务器日志位于 [glassfish 安装目录]/glassfish4/glassfish/domains/domain1/logs/server.log。该文件的最后几行应该看起来像这样:

    [2017-11-22T19:02:41.206-0500] [glassfish 5.0] [INFO] [] [javax.enterprise.system.tools.deployment.common] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1511395361206] [levelValue: 800] [[
      visiting unvisited references]]

    [2017-11-22T19:02:41.237-0500] [glassfish 5.0] [INFO] [AS-WEB-GLUE-00172] [javax.enterprise.web] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1511395361237] [levelValue: 800] [[
      Loading application [simpleapp-1.0] at [/simpleapp-1.0]]]

    [2017-11-22T19:02:41.246-0500] [glassfish 5.0] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1511395361246] [levelValue: 800] [[
      simpleapp-1.0 was successfully deployed in 50 milliseconds.]]

    [2017-11-22T19:02:41.247-0500] [glassfish 5.0] [INFO] [NCLS-DEPLOYMENT-02035] [javax.enterprise.system.tools.deployment.autodeploy] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1511395361247] [levelValue: 800] [[

  [AutoDeploy] Successfully autodeployed : /home/heffel/glassfish-5/glassfish5/glassfish/domains/domain1/autodeploy/simpleapp-1.0.war.]] 

我们当然也可以通过访问应用程序的 URL 来验证部署,这个 URL 将与我们在通过 Web 控制台部署时使用的 URL 相同,即 http://localhost:8080/simpleapp/simpleservlet,应用程序应该能够正常运行。

以这种方式部署的应用程序可以通过简单地从 autodeploy 目录中删除工件(在我们的例子中是 WAR 文件)来卸载。删除文件后,我们应该在服务器日志中看到类似以下的消息:

    [2017-11-22T19:04:23.198-0500] [glassfish 5.0] [INFO] [NCLS-DEPLOYMENT-02026] [javax.enterprise.system.tools.deployment.autodeploy] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1511395463198] [levelValue: 800] [[
      Autoundeploying application:  simpleapp-1.0]]

    [2017-11-22T19:04:23.218-0500] [glassfish 5.0] [INFO] [NCLS-DEPLOYMENT-02035] [javax.enterprise.system.tools.deployment.autodeploy] [tid: _ThreadID=91 _ThreadName=AutoDeployer] [timeMillis: 1511395463218] [levelValue: 800] [[
      [AutoDeploy] Successfully autoundeployed : /home/heffel/glassfish-5/glassfish5/glassfish/domains/domain1/autodeploy/simpleapp-1.0.war.]]

asadmin 命令行工具

通过命令行部署应用程序的另一种替代方法是使用以下命令:

asadmin deploy [path to file]/simpleapp-1.0.war

上述命令必须在 [glassfish 安装目录]/glassfish4/bin 下运行。

我们应该在命令行终端上看到确认消息,告知我们文件已成功部署:

Application deployed with name simpleapp-1.0.

Command deploy executed successfully.

服务器日志文件应该显示类似以下的消息:

    [2017-11-22T19:06:36.342-0500] [glassfish 5.0] [INFO] [AS-WEB-GLUE-00172] [javax.enterprise.web] [tid: _ThreadID=128 _ThreadName=admin-listener(6)] [timeMillis: 1511395596342] [levelValue: 800] [[
      Loading application [simpleapp-1.0] at [/simpleapp-1.0]]]

    [2017-11-22T19:06:36.349-0500] [glassfish 5.0] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=128 _ThreadName=admin-listener(6)] [timeMillis: 1511395596349] [levelValue: 800] [[

 simpleapp-1.0 was successfully deployed in 51 milliseconds.]] 

可以通过发出如下命令来使用 asadmin 可执行文件卸载应用程序:

asadmin undeploy simpleapp-1.0

以下消息应该在终端窗口的底部显示:

[2017-11-22T19:06:36.349-0500] [glassfish 5.0] [INFO] [] [javax.enterprise.system.core] [tid: _ThreadID=128 _ThreadName=admin-listener(6)] [timeMillis: 1511395596349] [levelValue: 800] [[
simpleapp-1.0 was successfully deployed in 51 milliseconds.]]

请注意,文件扩展名不用于卸载应用程序,asadmin 卸载的参数应该是应用程序名称,默认为 WAR 文件名(减去扩展名)。

GlassFish 域

警惕的读者可能会注意到 autodeploy 目录位于 domains/domain1 子目录下。GlassFish 有一个名为 domains 的概念。域允许将相关应用程序一起部署。可以同时启动多个域。GlassFish 域的行为类似于单独的 GlassFish 实例,在安装 GlassFish 时会创建一个默认域 domain1

创建域

可以通过发出以下命令从命令行创建额外的域:

asadmin create-domain domainname 

上述命令需要几个参数来指定域将监听的服务(HTTP、Admin、JMS、IIOP、安全 HTTP 等)的端口;在命令行中输入以下命令以查看其参数:

asadmin create-domain --help 

如果我们想在同一服务器上同时运行多个域,这些端口必须仔细选择,因为为不同的服务(或甚至跨域的同一服务)指定相同的端口将阻止其中一个域正常工作。

默认 domain1 域的默认端口列在以下表格中:

服务 端口
Admin 4848
HTTP 8080
Java 消息系统 (JMS) 7676
互联网间对象请求代理协议 (IIOP) 3700
安全 HTTP (HTTPS) 8181
安全 IIOP 3820
互认授权 IIOP 3920
Java 管理扩展 (JMX) 管理 8686

请注意,在创建域时,只需指定管理端口,如果未指定其他端口,则将使用表中列出的默认端口。在创建域时必须小心,因为,如上所述,如果任何服务在相同的端口上监听连接,则同一服务器上的两个域不能同时运行。

创建域而不必为每个服务指定端口的另一种方法是发出以下命令:

asadmin create-domain --portbase [port number] domainname

--portbase 参数的值决定了域的基本端口;不同服务的端口将是给定端口号的偏移量。以下表格列出了分配给所有不同服务的端口:

服务 端口
Admin 端口基础 + 48
HTTP 端口基础 + 80
Java 消息系统 (JMS) 端口基础 + 76
互联网间对象请求代理协议 (IIOP) 端口基础 + 37
安全 HTTP (HTTPS) 端口基础 + 81
安全 IIOP 端口基础 + 38
互认授权 IIOP 端口基础 + 39
Java 管理扩展 (JMX) 管理 端口基础 + 86

当然,在选择端口基础值时必须小心,确保分配的端口不会与其他域冲突。

根据经验法则,使用大于 8000 且能被 1000 整除的端口基础数字创建的域不会相互冲突,例如,使用端口基础为 9000 创建一个域是安全的,另一个使用端口基础为 10000,依此类推。

删除域

删除域非常简单,可以通过在命令行中发出以下命令来完成:

asadmin delete-domain domainname 

我们应该在终端窗口看到如下信息:

Command delete-domain executed successfully.

请谨慎使用前面的命令;一旦删除了一个域,它就无法轻易重新创建(所有已部署的应用程序都将消失,以及任何连接池、数据源等)。

停止域

可以通过发出以下命令来停止正在运行的域:

asadmin stop-domain domainname 

此命令将停止名为 domainname 的域。

如果只有一个域正在运行,domainname 参数是可选的。

本书假设读者正在使用默认域 domain1 和默认端口进行工作。如果不是这种情况,给出的说明需要修改以匹配适当的域和端口。

设置数据库连接

任何非平凡的 Java EE 应用程序都将连接到关系型数据库管理系统(RDBMS)。支持的 RDBMS 系统包括 JavaDB、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 目录中(有关获取此 JAR 文件的信息,请参阅您的 RDBMS 文档)。如果我们想要添加连接池的 GlassFish 域在复制 JDBC 驱动程序时正在运行,则必须重新启动该域以使更改生效。可以通过执行 asadmin restart-domain 来重新启动域。

一旦将 JDBC 驱动程序复制到适当的位置并且应用程序服务器已重新启动,请通过将浏览器指向 http://localhost:4848 来登录管理控制台。

然后,点击资源 | JDBC | JDBC 连接池,浏览器现在应该看起来像这样:

点击新建...按钮;在输入我们 RDBMS 的适当值后,页面主区域应该看起来像这样:

点击下一步按钮后,我们应该看到如下页面:

此页面上方的默认值大部分都是合理的。滚动到页面底部,并输入我们 RDBMS 的适当属性值(至少包括用户名、密码和 URL),然后点击屏幕右上角的完成按钮。

属性名称取决于我们使用的 RDBMS,但通常,有一个 URL 属性,我们应该在其中输入我们数据库的 JDBC URL,以及用户名和密码属性,我们应该在其中输入我们数据库的认证凭据。

我们新创建的连接池现在应该出现在连接池列表中:

在某些情况下,在设置新的连接池后,可能需要重新启动 GlassFish 域。

我们可以通过点击其“池名称”,然后在结果页面上点击“Ping”按钮来验证我们的连接池是否成功设置:

我们的联系池现在已准备好供我们的应用程序使用。

设置数据源

Java EE 应用程序不直接访问连接池,而是访问指向连接池的数据源。要设置新的数据源,请点击 Web 控制台左侧的“JDBC 资源”菜单项,然后点击“新建...”按钮。

在填写完我们新数据源的相关信息后,Web 控制台的主要区域应该看起来像这样:

点击“确定”按钮后,我们可以看到我们新创建的数据源:

设置 JMS 资源

在我们开始编写代码以利用 JMS API 之前,我们需要配置一些 GlassFish 资源。具体来说,我们需要设置一个JMS 连接工厂、一个消息队列和一个消息主题

Java EE 7 和 Java EE 8 要求所有合规的应用服务器提供默认的 JMS 连接工厂。作为完全合规的 Java EE 8 应用服务器(以及 Java EE 8 参考实现),GlassFish 符合这一要求,因此,严格来说,我们实际上并不需要设置连接工厂。在许多情况下,我们可能需要设置一个,因此在下文中,我们将说明如何进行设置。

设置 JMS 连接工厂

设置 JMS 连接工厂的最简单方法是通过 GlassFish 的 Web 控制台。正如之前在第一章“Java EE 简介”中提到的,我们可以通过在命令行中输入以下命令来启动我们的域,从而访问 Web 控制台:

asadmin start-domain domain1 

然后,我们可以将浏览器指向http://localhost:4848并登录:

可以通过展开 Web 控制台左侧的树中的“资源”节点,展开“JMS 资源”节点,然后点击“连接工厂”节点,再点击 Web 控制台主区域的“新建...”按钮来添加一个连接工厂。

对于我们的目的,我们可以使用大多数默认设置;我们唯一需要做的是输入一个池名称并为我们的连接工厂选择一个资源类型。

在选择 JMS 资源名称时,始终使用以jms/开头的池名称是一个好主意。这样,在浏览 JNDI 树时可以轻松识别 JMS 资源。

在标有“JNDI 名称”的文本字段中,输入jms/GlassFishBookConnectionFactory

资源类型下拉菜单有三个选项:

  • javax.jms.TopicConnectionFactory:用于创建一个连接工厂,该工厂为使用 pub/sub 消息域的 JMS 客户端创建 JMS 主题

  • javax.jms.QueueConnectionFactory:用于创建一个连接工厂,该工厂为使用 PTP 消息域的 JMS 客户端创建 JMS 队列

  • javax.jms.ConnectionFactory:用于创建一个连接工厂,该工厂可以创建 JMS 主题或 JMS 队列

在我们的示例中,我们将选择javax.jms.ConnectionFactory;这样我们就可以为所有示例使用相同的连接工厂,无论是使用 PTP 消息域的示例,还是使用 pub/sub 消息域的示例。

在输入我们的连接工厂的池名称、选择连接工厂类型以及可选地输入我们的连接工厂的描述后,我们必须点击“确定”按钮以使更改生效:

图片

我们应该能够在 GlassFish Web 控制台的主区域看到我们新创建的连接工厂。

设置 JMS 消息队列

可以通过展开 Web 控制台左侧树中的“资源”节点,然后展开“JMS 资源”节点,点击“目的地资源”节点,接着在 Web 控制台主区域点击“新建...”按钮来添加一个 JMS 消息队列:

图片

在我们的示例中,消息队列的 JNDI 名称为jms/GlassFishBookQueue。消息队列的资源类型必须是javax.jms.Queue。此外,必须输入物理目的地名称。在先前的示例中,我们使用GlassFishBookQueue作为此字段的值。

在点击“新建...”按钮、输入我们的消息队列的适当信息并点击“确定”后,我们应该能看到新创建的队列:

图片

设置 JMS 消息主题

在 GlassFish 中设置 JMS 消息主题与设置消息队列非常相似。

在 GlassFish Web 控制台中,展开左侧树中的“资源”节点,然后展开“JMS 资源”节点,点击“目的地”节点,然后在 Web 控制台主区域点击“新建...”按钮:

图片

我们的示例将使用 JNDI 名称为jms/GlassFishBookTopic。由于这是一个消息主题,资源类型必须是javax.jms.Topic。描述字段是可选的。物理目的地名称属性是必需的;在我们的示例中,我们将使用GlassFishBookTopic作为名称属性的值。

点击“确定”按钮后,我们可以看到我们新创建的消息主题:

图片

现在我们已经设置了一个连接工厂、一个消息队列和一个消息主题,我们就可以开始使用 JMS API 编写代码了。

配置持久订阅者

正如我们之前提到的,通过 GlassFish Web 控制台添加连接工厂是最简单的方法。回想一下,要通过 GlassFish Web 控制台添加 JMS 连接工厂,我们需要展开左侧的“资源”节点,然后展开“JMS 资源”节点,点击“连接工厂”节点,然后在页面主区域点击“新建...”按钮。我们的下一个示例将使用以下截图显示的设置:

图片

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

图片

摘要

在本附录中,我们讨论了如何下载和安装 GlassFish。我们还讨论了通过 GlassFish Web 控制台、通过asadmin命令以及通过将文件复制到autodeploy目录来部署 Java EE 应用程序的几种方法。我们还讨论了基本的 GlassFish 管理任务,例如设置域和通过添加连接池和数据源来设置数据库连接。

posted @ 2025-09-10 15:08  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报