JavaEE7-Wildfly-开发指南-全-

JavaEE7 Wildfly 开发指南(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

WildFly,作为最新的 JBoss 应用程序服务器版本,为开发者提供了 Java EE 7 平台的完整实现。它建立在 JBoss AS 7 引入的模块化架构的坚实基础之上,但在灵活性和性能方面有所提升。最新版本的 Java 企业版专注于提高开发者的生产力,WildFly 也是如此。

本书将向 Java 开发者介绍企业应用程序的世界。我们将使用在现代实际项目中经过实战检验的现代开发技术和工具。我们还将利用 WildFly 平台提供的功能,如安全、缓存、测试和集群。最后,您将学习如何使用为 WildFly 专门创建的工具来管理您的服务器。

学习过程将集中在票务预订应用程序上,这是一个示例项目,每个章节都会增加更多功能(有时是完全不同的用户界面)。

本书涵盖的内容

第一章, 开始使用 WildFly,是 Java EE 平台和新的 Java EE 7 版本规范的介绍。它还侧重于介绍 WildFly 的新特性、开发者环境设置和基本服务器管理。

第二章, 在 WildFly 上创建您的第一个 Java EE 应用程序,描述了 WildFly 服务器使用的基础知识,提供了部署您第一个应用程序所需的信息。

第三章, 介绍 Java EE 7 – EJBs,介绍了 Java EE 中的企业对象,称为企业 Java Bean。在本章中,我们为票务预订应用程序奠定了基础。

第四章, 学习上下文和依赖注入,涵盖了连接您应用程序构建块的 CDI 技术。

第五章, 将持久性与 CDI 结合,探讨了数据库世界和 Java EE 中的对象映射。

第六章, 使用 JBoss JMS Provider 开发应用程序,深入探讨了使用 JCA 的企业系统集成和 HornetQ。

第七章, 将 Web 服务添加到您的应用程序中,不仅讨论了旧式的 SOAP Web 服务,还介绍了基于 JAX-RS(REST)的现代且流行的方法。我们还将探讨如何将 Java EE 7 后端与 AngularJS 浏览器应用程序集成。

第八章,“添加 WebSocket”,介绍了 Java EE 7 平台的一个全新的功能:WebSocket。我们将在我们的 AngularJS 示例中查看它们。

第九章,“管理应用程序服务器”,讨论了 WildFly 的管理功能。

第十章,“保护 WildFly 应用程序”,专注于服务器和你的应用程序的安全相关方面。

第十一章,“集群 WildFly 应用程序”,讨论了使 Java EE 应用程序具有高可用性和可扩展性的方法。

第十二章,“长期任务执行”,描述了企业 Java 批处理应用程序和服务器上的并发管理的新领域。

第十三章,“测试你的应用程序”,展示了在介绍最重要的 Java EE 技术之后,我们如何使用 Arquillian 为我们的应用程序编写集成测试。

附录,“使用 JBoss Forge 快速开发”,涵盖了 JBoss Forge 工具。它展示了如何使用这个应用程序通过其代码生成功能来加速基于 Java EE 的项目开发。

你需要这本书的内容

本书是面向 Java EE 和 WildFly 世界的代码导向指南。为了充分利用本书,你需要访问互联网以下载所有必需的工具和库。需要具备 Java 知识。我们还将使用 Maven 作为构建自动化工具,但由于其冗长性,本书中提供的示例都是自解释的。

尽管我们将使用 AngularJS,但不需要额外的 JS 知识。本书中所有框架的示例都将基于展示,其目的是展示在典型场景中不同方如何与 Java EE 交互。

这本书面向的对象

如果你是一名想要学习 Java EE 基础知识的 Java 开发者,或者你已经是一名 Java EE 开发者,想要了解 WildFly 或 Java EE 7 的新特性,这本书就是为你准备的。本书涵盖了所述技术的基础知识,并确保为那些已有一定知识的人提供一些更有趣、更高级的主题。

习惯用法

在这本书中,你会找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“在 MDB 实例的onMessage()方法返回后,请求完成,实例被放回空闲池中。”

代码块设置如下:

<jms-destinations>
   <jms-queue name="TicketQueue">
      <entry name="java:jboss/jms/queue/ticketQueue"/>
         <durable>false</durable>
   </jms-queue>
</jms-destinations>

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

@Stateless
public class SampleEJB {

 @Resource(mappedName = "java:/ConnectionFactory")
 private ConnectionFactory cf; 
}

任何命令行输入或输出都应如下编写:

CREATE DATABASE ticketsystem;
CREATE USER jboss WITH PASSWORD 'jboss';
GRANT ALL PRIVILEGES ON DATABASE ticketsystem TO jboss;

新术语重要词汇以粗体显示。您在屏幕上、菜单或对话框中看到的单词,例如,在文本中如下所示:“例如,Eclipse 的文件菜单包括一个从表创建 JPA 实体的选项,一旦设置了与数据库的连接,就可以将您的 DB 模式(或其部分)反转成 Java 实体。”

注意

警告或重要注意事项以如下框的形式出现。

提示

技巧和窍门看起来像这样。

读者反馈

我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大价值的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。

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

客户支持

现在您是 Packt 书籍的骄傲所有者,我们有多种方法可以帮助您从您的购买中获得最大价值。

下载示例代码

您可以从您在www.packtpub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

勘误

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

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

盗版

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

如果您发现了疑似盗版材料,请通过<copyright@packtpub.com>联系我们,并提供链接。

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

问题和建议

如果您在书籍的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章. 开始使用 WildFly

Java 企业版提供了一种开发企业软件的标准,但允许开发者选择其特定的实现。对于 Java EE(企业版)规范中包含的每一项技术,都有一个参考实现;一个满足所有要求的开源库或组件。公司和组织可以创建自己版本的组件,这意味着没有所有人都使用的中央 Java EE 平台。取而代之的是,我们得到了多种实现规范的方法,针对特定情况进行了改进和优化。在撰写本文时,大约有 20 个 Java EE 6 的认证(完整)实现和三个 Java EE 7 的实现。

应用服务器是一个提供所有 Java EE 组件的运行时环境。Glassfish 是由 Oracle 赞助的参考实现,但从版本 4(为 Java EE 7 创建)开始,它不再提供商业支持。在这本书中,你将学习如何在 WildFly 应用服务器上开发应用程序,之前被称为 JBoss 应用服务器。

JBoss 是红帽公司的一个部门,旨在为企业管理开发提供开发者友好的开源生态系统。目前,该公司支持多个项目(大约 100 个),其中一些是 Java EE 规范的实现。企业元素被整合到 JBoss 自己的应用服务器 WildFly 中。

值得注意的是,从 JBoss AS 更名为 WildFly 是为了将应用服务器与公司和其他子项目区分开来。这个名字是通过公开投票选出的(更多信息可在jbossas.jboss.org/rename/vote找到)。

新版本推出了一个可扩展且性能高的 Web 服务器 Undertow,它支持 HTTP 升级机制和 WebSocket 协议。更重要的是,新版本的容器甚至比 JBoss 应用服务器 7 更快,并提供了一个统一的配置机制。然而,最新版本的核心是 Java EE 7 兼容性,这允许开发者使用 Java EE 规范最新版本的技术。

这本书的重点是应用开发;因此,我们首先需要收集所有交付应用程序所需的资源。在本章中,我们将详细介绍以下主题:

  • Java EE 和 WildFly 概述

  • 准备您的环境以安装软件

  • 下载和安装 WildFly

  • 验证 WildFly 的安装

  • 安装开发所需的其他资源

Java EE 和 WildFly 概述

Java EE(以前称为 J2EE)是一个包含用于服务器端 Java 开发的标准技术集合的规范。Java EE 技术包括 Java Servlet、JavaServer PagesJSPs)、JavaServer FacesJSFs)、企业 JavaBeansEJB)、上下文和依赖注入CDI)、Java 消息服务JMS)、Java 持久化 APIJPA)、Java API for XML Web ServicesJAX-WS)和 Java API for RESTful Web ServicesJAX-RS)等。Java EE 的最新版本通过提供对批处理应用程序、并发实用工具、JSON 处理JSON-P)和 WebSocket 的支持,进一步扩展了可用技术的范围。存在多个商业和开源应用服务器,允许开发者运行符合 Java EE 的应用程序;WildFly(以前称为 JBoss AS)是开发者采用的主要开源解决方案,尽管很难用精确的术语来衡量,但它可能是市场上使用最广泛的应用服务器。

与所有符合 Java EE 的应用服务器一样,WildFly 随带所有必需的库,使我们能够开发并部署基于 Java EE 平台构建的 Java 应用程序。

WildFly 和企业应用平台

WildFly 和之前的 JBoss 应用服务器以可下载的二进制包(针对主要版本)或可构建的源代码(针对错误修复版本)的形式免费提供给社区。这些版本被称为社区发布版,可用于开发和生产。

JBoss 还发布了社区构建软件的更稳定和加固版本,这些版本被称为 企业应用平台EAP),这是一个由 Red Hat 提供支持服务的商业产品。Red Hat 将项目之间的这种关系称为上游/下游。社区构建是下游变化和创新的来源,代码是下游。商业版本编号与社区版本不同,但它是社区发布的扩展变体(例如,EAP 6.1.0 是基于 JBoss 7.2.0 构建的,仅以可构建源代码的形式在 GitHub 上提供;EAP 6.2.0 和 JBoss 7.3.0 也是如此)。EAP 构建具有更复杂的许可系统;使用条款取决于构建的成熟度,如下所示:

  • EAP 预览版对开发者和生产使用都是免费的,因为它们相当于包含可选修复的标准社区版本。相应的社区二进制文件可能不可下载,因为它们将与 EAP 预览版类似。

  • EAP 测试版对开发者免费提供(在注册订阅计划后),但不能用于生产。

  • EAP Final 也对开发者免费提供,但除此之外,只有付费订阅中才有新的安全补丁。

JBoss 提出的分发模型允许开发者免费使用与生产环境中相同的版本。这是一个巨大的好处,特别是由于来自 Oracle 的竞争解决方案(Glassfish:Java EE 服务器参考实现)不再有带商业支持的版本。

欢迎使用 Java EE 7

Java EE 7 包含了对现有版本的几个改进和新增功能。新版本专注于三个主题:开发者生产力、HTML5 和提供企业应用程序所需的新功能。以下章节列出了对企业应用程序开发者感兴趣的主要规范改进。

如果您刚开始 Java EE 的冒险之旅,请随意跳过本节。以下章节将更详细地介绍后续章节中描述的技术。

JavaServer Faces 2.2 – JSR 344

Java EE 7 包含了 JSF 规范的新版本,它不像 2.0 那样革命性,但仍然为开发者提供了一些吸引人的新增功能。JSF 2.2 提供的关键特性如下:

  • 现在通过使用透传元素和属性支持 HTML5 标记。以前,每个组件都需要一个扩展渲染器来支持自定义属性。

  • 通过 @FlowScoped 引入了流程作用域,这使得创建向导(具有多个步骤的对话框)变得更加容易。

  • 基于 Ajax 的文件上传现在已直接支持。

  • 此外,还提出了无状态视图作为提高性能的方法。

企业 JavaBeans 3.2 – JSR 345

与 EJB 3.1 相比,3.2 版本是对现有版本的微小更新。它主要集中于标记一些较旧的功能为过时(它们现在是可选的,这意味着并非每个 Java EE 7 兼容的应用服务器都将支持它们)。这些可选功能与基于 EJB 2.1 和 JAX-RPC 的 Web 服务持久性相关。新规范提供的主要增强如下:

  • 有状态会话 Bean 的生命周期方法现在可以是事务性的。

  • 定时器服务 API 现在允许您访问当前 EJB 模块中的所有活动定时器。

  • 已引入了一个新的容器提供角色(**),它可以用来表示任何经过身份验证的用户(不考虑其实际角色)。

  • 现在可以禁用有状态会话 Bean 的钝化。

EJB 规范的事务部分已被提取并用于 Java EE 平台的其他部分(事务支持已放置在 JTA 1.2 规范中)。例如,由于引入了 @Transactional 注解,现在可以在 CDI Bean 中使用事务行为。

Java 持久性 API 2.1 – JSR 338

JPA 被引入为 Java EE 规范第 5 版的标准部分。JPA 的目的是作为 Java EE 的默认对象关系映射框架来取代实体 Bean。JPA 采纳了来自第三方对象关系框架(如 Hibernate 和 JDO)的想法,并将它们作为标准版本的一部分。

JPA 2.1 相比 JPA 2.0 是一个改进,因为它为开发者提供了以下几项便利:

  • 它提供了一个标准化的模式生成机制,这得益于扩展的注解集和 persistence.xml 属性。

  • 通过引入 @Converter 注解添加了对类型转换的支持。

  • 实体管理器 API 现在支持存储过程,因此不再需要使用 SQL 查询机制

  • 通过批更新和删除扩展了 Criteria API

  • 可以将注入到实体监听器类中,同时使用生命周期回调方法。

  • 现在可以在运行时创建命名查询。

  • JPA 查询语言JPQL)已扩展了新的数据库函数

此外,Java EE 7 兼容的容器现在必须支持预配置的数据源(以及其他资源),这些资源可以由 JPA 实体即时使用。

WildFly 使用 Hibernate 作为其 JPA 提供程序,并附带一个可立即使用的 H2 内存数据库。默认数据源指向应用程序服务器内部托管的一个 H2 实例。

Java EE 1.1 的上下文和依赖注入 – JSR 346

上下文和依赖注入CDI)的 1.1 版本提供了对 Java EE 6 中引入 CDI 后识别出的问题的改进。简化编程模型的过程从 1.0 版本开始,现在正在继续。更新涵盖的领域如下:

  • CDI 默认启用(无需将 bean.xml 文件添加到部署中),并且可以指定所需的组件扫描模式。

  • 开发者现在可以通过使用 @Vetoed 注解和 beans.xml 中的类或包过滤器来获得对 Bean 发现机制的更细粒度控制。现在可以使用 @Priority 注解全局启用整个应用程序的拦截器、装饰器和替代方案,而不是为每个模块启用。

  • 当处理 CDI 事件时,现在可以检查事件元数据。

  • 拦截器已增强,可以围绕构造函数调用执行。

  • 最后,新版本包含了对可移植扩展开发的大量增强。

Weld 是 WildFly 内部使用的 CDI 实现。

Java Servlet API 3.1 – JSR 340

新版本的 Java Servlet API 明确关注新特性。其中最重要的是 HTTP 升级机制,它允许客户端和服务器在 HTTP 1.1 中开始对话,并协商后续请求的另一个协议。该特性被用于在 Java EE 7 中实现 WebSocket 机制。规范新版本的其他特性如下:

  • 为 Servlets 提供了非阻塞 I/O API,以改善 Web 应用程序的可伸缩性

  • 引入了多项安全改进;其中最值得注意的是可以为所有 HTTP 方法设置默认的安全语义

JAX-RS,Java API for RESTful Web Services 2.0 – JSR 339

在 Java EE 7 中,JAX-RS 规范增加了一些长期期待的功能。由于新规范带来的改进具有重大影响,版本已从 1.1 更改为 2.0。以下是最重要的特性列表:

  • 客户端 API 现在是规范的一部分,因此不再需要第三方库。规范的实施必须提供一个符合通用 API 的 REST 客户端。

  • 现在支持异步请求,这样客户端就不必被动等待任务的完成。

  • 引入了过滤器和处理程序作为为开发者提供扩展点的通用机制。它们可用于跨切面关注点,如审计和安全。

  • Bean Validation 已集成到 JAX-RS 中,使得约束注解可用于请求参数。

WildFly 随带 RESTEasy,这是 JAX-RS 2.0 的一个实现。

Java 消息服务 2.0 – JSR 343

JSR 343 是十多年来 JMS 规范的第一个更新。更新再次以简化 API 为主题。新的 API 大大减少了程序员必须编写的样板代码量,同时仍然保持向后兼容。其他新特性如下:

  • 现在支持异步消息发送,因此应用程序不必在收到服务器的确认之前被阻塞

  • 现在可以设置延迟交付的消息

HornetQ 是由 JBoss 使用和开发的 JMS 提供商。它可以在 WildFly 之外作为独立的消息代理使用。

Bean Validation 1.1 – JSR 349

更新 Java EE 7 中的 Bean Validation 的过程集中在两个主要特性上:

  • 方法验证,允许开发者验证参数和返回值

  • 更紧密的 CDI 集成,这改变了验证框架元素的生命周期,允许开发者在其自己的 ConstraintValidator 实现中使用依赖注入

Java EE 1.0 的并发实用工具 – JSR 236

并发实用工具是一个新的功能包,用于在 Java EE 应用组件中使用多线程。新的规范提供了ManagedExecutorService(Java SE 中已知的ExecutorService的容器感知版本),可以用来将任务的执行委托给一个单独的线程。这些管理任务可以使用大多数适用于应用组件的功能(例如 EJB 或 Servlet)。还可以使用新的ManagedScheduledExecutorService来安排周期性或延迟的任务。这些平台的新增功能填补了 Java EE 的功能空白,这在早期架构中是非常难以克服的。

Java 平台 1.0 的批处理应用程序 – JSR 352

批处理作业是另一个企业级应用开发领域,在 Java EE 的早期版本中没有涉及。新的批处理框架被用来提供一个通用的解决方案来运行无需用户交互的任务。Java EE 为开发者提供了以下选项:

  • 批处理作业执行运行时

  • 基于 XML 的作业描述语言

  • 批处理任务业务逻辑实现的 Java API

  • jBeret,这是 WildFly 中使用的批处理框架

JSON 处理的 Java API 1.0 – JSR 353

Java EE 7 现在自带了开箱即用的 JSON 处理功能,因此开发者不再被迫使用外部库来完成这项任务。新的 API 允许 JSON 处理使用两种方法:对象模型(基于 DOM)和流式处理(基于事件)。

WebSocket 1.0 的 Java API – JSR 356

为了完全支持基于 HTML5 的应用程序的开发,Java EE 7 需要一种标准化的技术来实现服务器与用户浏览器之间的双向通信。WebSocket API 允许开发者定义服务器端端点,这些端点将为每个连接到它们的客户端维护一个 TCP 连接(例如,使用 JavaScript API)。在新规范之前,开发者必须使用供应商特定的库和非便携式解决方案来实现相同的目标。

WildFly 的新特性

WildFly 的第八次发布基于在之前版本 JBoss AS 7 中引入的模块化架构。它在多个关键点进行了改进,包括性能和管理领域。对于开发者来说,最重要的变化是这次发布完全实现了 Java EE 7 标准。其中一些最显著的改进包括以下内容:

  • WildFly 8 实现了 Java EE 7 规范中提出的所有标准,这些标准也在本章中进行了描述。

  • 网络服务器模块在名称为 Undertow 的情况下被完全重写。它支持阻塞和非阻塞操作。早期的性能测试(例如,www.techempower.com/benchmarks/#section=data-r6&hw=i7&test=plaintext)显示了 HTTP 请求处理方面的重大性能提升。Undertow 也可以作为一个独立的项目使用,并且可以在不使用 WildFly 的情况下使用。

  • 最终的 WildFly 版本减少了使用的端口号数量。现在,它只使用两个端口,一个(9990)用于管理、JMX 和网络管理,另一个(8080)用于标准服务,包括 HTTP、WebSockets、远程 JNDI 和 EJB 调用。

  • 现在,可以使用管理基于角色的访问控制RBAC)来限制用户的管理权限。所有配置更改都可以通过审计日志进行跟踪。

  • 对于之前的版本,任何升级操作都需要完全新的服务器安装。WildFly 带来了打补丁的功能,允许使用管理协议安装和回滚模块。

在下一节中,我们将描述安装并启动一个新的应用程序服务器所需的所有步骤。

安装服务器和客户端组件

了解应用程序服务器的第一步是在您的机器上安装所有必要的组件以运行它。应用程序服务器本身只需要安装 JDK 环境。

就硬件要求而言,您应该知道,在撰写本文时,服务器发行版需要大约 130 MB 的硬盘空间,并为独立服务器分配了最小 64 MB 和最大 512 MB。

为了开始,我们需要检查以下清单:

  • 在 WildFly 将运行的地方安装 JDK

  • 安装 WildFly

  • 安装 Eclipse 开发环境

  • 安装 Maven 构建管理工具

在本章结束时,您将拥有启动应用程序服务器所需的所有工具。

安装 Java SE

第一项强制性要求是安装 JDK 8 环境。Java SE 下载网站可在 www.oracle.com/technetwork/java/javase/downloads/index.html 找到。

选择 Java SE 8 的最新版本并安装它。如果您不知道如何安装,请参阅docs.oracle.com/javase/8/docs/technotes/guides/install/install_overview.html

测试安装

完成安装后,从命令提示符运行 java -version 命令以验证其是否正确安装。以下是来自 Windows 机器的预期输出:

C:\>java –version
java version "1.8.0_11"
Java(TM) SE Runtime Environment (build 1.8.0_11-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.11-b03, mixed mode)

安装 WildFly

JBoss WildFly 应用程序服务器可以从 wildfly.org/downloads/ 免费下载。

如以下截图所示,在撰写本书时,WildFly 的最新稳定版本为 8.1.0.Final,它具有经过认证的 Java EE 7 全功能配置:

安装 WildFly

一旦您选择了适当的服务器发行版,您将被告知此下载是社区发布的一部分,因此不受支持。

注意

如果您需要为您的应用程序提供企业级支持,可以选择 Red Hat Enterprise Application Platform。

与社区版本相比,EAP 经过了不同的质量测试,可能在功能/打包方面有所不同。有关 EAP 和社区版本之间差异的更多信息,请参阅本章开头。然而,在撰写本书时,EAP 还不支持 Java EE 7,且没有公开的路线图。

安装 WildFly 简单易行;除了解压缩 wildfly-8.1.0.Final.zip 归档之外,不需要其他任何东西。

Windows 用户可以使用任何解压缩工具,例如内置的压缩文件夹(在新版 Windows 中),WinZip,WinRAR 或 7-Zip,注意选择不包含空格和空白的文件夹名称。Unix/Linux 应使用 $ unzip wildfly-8.1.0.Final.zip 解压缩 shell 命令来展开归档。

注意

安全警告

Unix/Linux 用户应知道 WildFly 不需要 root 权限,因为 WildFly 使用的默认端口都不低于 1024 的特权端口范围。为了降低用户通过 WildFly 获得 root 权限的风险,请以非 root 用户安装并运行 WildFly。

启动 WildFly

安装 WildFly 后,进行简单的启动测试以验证您的 Java VM /操作系统组合没有出现重大问题是明智的。要测试您的安装,请转到您的 JBOSS_HOME 目录的 bin 目录(您已解压缩应用程序服务器的路径)并执行以下命令:

standalone.bat    # Windows users
$ ./standalone.sh   # Linux/Unix users

以下是一个示例 WildFly 启动控制台的截图:

启动 WildFly

上述命令启动了一个 WildFly 独立实例,这与使用早期 JBoss AS 版本中使用的 run.bat/run.sh 脚本启动应用程序服务器等效。您将注意到新版本的应用程序服务器启动速度之快令人惊讶;这归功于 JBoss AS 7 版本中引入的模块化架构,该架构只启动加载应用程序所需的应用程序服务器容器中的必要部分。

如果您需要自定义应用程序服务器的启动属性,请打开 standalone.conf 文件(或 Windows 用户的 standalone.conf.bat),其中已声明 Wildfly 的内存需求。以下是该文件的 Linux 核心部分:

if [ "x$JAVA_OPTS" = "x" ]; then
    JAVA_OPTS="-Xms64m -Xmx512m -XX:MaxPermSize=256m
       -Djava.net.preferIPv4Stack=true"
    JAVA_OPTS="$JAVA_OPTS
        -Djboss.modules.system.pkgs=$JBOSS_MODULES_SYSTEM_PKGS
        -Djava.awt.headless=true"
else
    echo "JAVA_OPTS already set in environment; overriding default settings with values: $JAVA_OPTS"

因此,默认情况下,应用服务器以至少 64 MB 的堆空间和最多 512 MB 的最大堆空间启动。这足以开始;然而,如果你需要在上面运行更健壮的 Java EE 应用程序,你可能至少需要 1 GB 的堆空间,或者根据应用程序类型,可能需要 2 GB 或更多。一般来说,32 位机器无法执行超过 2 GB 空间的过程;然而,在 64 位机器上,实际上没有进程大小的限制。

你可以通过将浏览器指向应用服务器的欢迎页面来验证服务器是否可以从网络访问,该页面可以通过众所周知的地址http://localhost:8080访问。WildFly 的欢迎页面如下所示:

启动 WildFly

使用命令行界面连接到服务器

如果你之前使用过应用服务器的早期版本,你可能听说过 twiddle 命令行实用程序,该实用程序查询应用服务器上安装的 MBeans。这个实用程序已被一个更复杂的界面所取代,称为命令行界面(CLI);它可以在JBOSS_HOME/bin中找到。

只需启动jboss-cli.bat脚本(或 Linux 用户的jboss-cli.sh),你就可以通过 shell 界面管理应用服务器。这将在以下截图展示:

使用命令行界面连接到服务器

我们启动了一个交互式 shell 会话,也可以使用命令行补全(通过按Tab键)来匹配部分输入的命令名称。不再需要搜索以找到命令的确切语法!

注意

在上一张截图,我们使用connect命令连接到服务器;它默认使用回环服务器地址,并连接到端口9990

命令行界面在第九章中进行了深入讨论,管理应用服务器,这是关于服务器管理界面的全部内容;然而,我们将在下一节中初步了解其基本功能,以便让你熟悉这个强大的工具。

停止 WildFly

停止 WildFly 最简单的方法是通过发送中断信号Ctrl + C

然而,如果你的 WildFly 进程是在后台启动的,或者更确切地说,是在另一台机器上运行,你可以使用 CLI 界面发出立即关闭命令:

[disconnected /] connect
Connected to localhost:9990
[standalone@localhost:9990 /] :shutdown

定位关闭脚本

实际上还有一个选项可以关闭应用服务器,这在需要从脚本中关闭服务器时非常有用。此选项包括将--连接选项传递给管理 shell,从而关闭交互模式,如下所示命令行:

jboss-cli.bat --connect command=:shutdown      # Windows
./jboss-cli.sh --connect command=:shutdown       # Unix / Linux

在远程机器上停止 WildFly

关闭在远程机器上运行的 应用服务器只需将服务器的远程地址提供给 CLI,出于安全原因,还需要用户名和密码,以下代码片段显示了这一点(参见下一章了解有关用户创建的更多信息):

[disconnected /] connect 192.168.1.10
Authenticating against security realm: ManagementRealm
Username: admin1234
Password:
Connected to 192.168.1.10:9990
[standalone@192.168.1.10:9990 / ] :shutdown

然而,你必须记住你需要访问一个特定的端口,因为通常,它可能会被防火墙阻止。

重启 WildFly

命令行界面包含许多有用的命令。其中最有趣的一个选项是使用 reload 命令重新加载 AS 配置或其部分。

当在 AS 服务器根节点路径上执行时,reload 命令可以重新加载服务的配置:

[disconnected /] connect
Connected to localhost:9990
[standalone@localhost:9990 /] :reload

安装 Eclipse 环境

本书所使用的开发环境是 Eclipse,这是全球 Java 开发者所熟知的,它包含了一整套插件来扩展其功能。除此之外,Eclipse 是第一个兼容新应用服务器的 IDE。

那么,让我们转到 Eclipse 的下载页面,它位于 www.eclipse.org

从这里下载最新的企业版(在撰写本书时,版本为 4.4,也称为 Luna)。压缩包中包含了已经安装的所有 Java EE 插件。以下截图显示了这一过程:

安装 Eclipse 环境

一旦解压之前下载的文件,你将看到一个名为 eclipse 的文件夹。在这个文件夹中,你可以找到 Eclipse 应用程序(一个大的蓝色圆点)。建议你在桌面上创建一个快捷方式以简化 Eclipse 的启动。请注意,与 WildFly 一样,Eclipse 没有安装过程。一旦解压文件,你就完成了!

安装 JBoss Tools

下一步将是安装 JBoss AS 插件,它是名为 JBoss Tools 的插件套件的一部分。在 Eclipse 中安装新插件非常简单;只需按照以下步骤操作:

  1. 从菜单中,导航到 帮助 | 安装新软件

  2. 然后,点击 添加 按钮,在这里你将输入 JBoss Tools 的下载 URL(以及描述),download.jboss.org/jbosstools/updates/development/luna/。以下截图显示了这一过程:安装 JBoss Tools

  3. 如前一个截图所示,你需要检查 JBossAS Tools 插件,然后继续到下一个选项以完成安装过程。

    注意

    在过滤器字段中输入 JBossAS 以快速在大量的 JBoss Tools 中找到 JBoss AS 工具插件。

  4. 完成后,根据提示重新启动进程。

    注意

    你还可以将 JBoss Tools 作为单独的 zip 文件下载以进行离线安装。请参阅 tools.jboss.org/downloads/ 上的 JBoss Tools 下载。

  5. 现在,您应该能够通过从菜单中选择新建 | 服务器并展开JBoss Community选项来在新服务器中看到 WildFly,如图所示:安装 JBoss Tools

在 Eclipse 中完成服务器安装相当简单,只需指向您的服务器发行版所在的文件夹即可;因此,我们将把这个留给你作为实践练习来实现。

替代开发环境

由于这本书全部关于开发,我们也应该考虑一些可能更适合您的编程风格或公司标准的其他替代方案。因此,另一个有效的替代方案是 IntelliJ IDEA,可在www.jetbrains.com/idea/index.html找到。

IntelliJ IDEA 是一个以代码为中心的 IDE,专注于提高开发者的生产力。编辑器能够很好地理解您的代码,并在您需要时提供出色的建议,并且始终准备帮助您塑造代码。

该产品有两种版本——社区版和终极版,需要许可证。为了使用 Java EE 和 WildFly 插件,您需要从www.jetbrains.com/idea/download/index.html下载终极版,然后使用安装向导简单地安装它。

一旦安装了终极版,您可以通过转到文件 | 设置并选择IDE 设置选项来开始使用 WildFly 开发应用程序。在这里,您可以选择添加新的应用程序服务器环境。这在上面的屏幕截图中显示:

替代开发环境

另一个在开发者中相当受欢迎的开发选项是 NetBeans (netbeans.org),它在 7.4 和 8.0 版本中支持 WildFly,但需要在 NetBeans 插件注册表中安装额外的插件。

安装 Maven

除了图形工具之外,强烈建议您学习 Maven,这是一个流行的构建和发布管理工具。通过使用 Maven,您将享受到以下好处:

  • 所有项目的标准结构

  • 依赖项的集中和自动管理

Maven 以几种格式分发,为了您的方便,可以从maven.apache.org/download.html下载。

下载完成后,将发行版存档(例如,Windows 上的 apache-maven-3.1.1-bin.zip)解压缩到您希望安装 Maven 3.1.0(或最新可用版本)的目录中,例如,C:\\Programs\apache-maven-3.1.1。一些操作系统(如 Linux 或 OS X)在其应用程序仓库中提供 Maven 软件包。

完成后,将 M2_HOME 环境变量添加到您的系统中,使其指向 Maven 解包的文件夹。

接下来,通过将 Maven 二进制文件添加到系统路径中来更新 PATH 环境变量。例如,在 Windows 平台上,你应该包含 %M2_HOME%/bin 以便在命令行上使用 Maven。

一些额外的 Maven 学习材料以免费书籍的形式可在 Sonatype 网站上找到;请参阅 www.sonatype.com/resources/books

测试安装

一旦您完成安装,运行 mvn 版本来验证 Maven 是否已正确安装。请参考以下代码片段以验证正确的安装:

> mvn –version
Apache Maven 3.1.1 (0728685237757ffbf44136acec0402957f723d9a; 2013-09-17 17:22:22+0200)
Maven home: C:\Programs\Dev\apache-maven-3.1.1
Java version: 1.8.0_11, vendor: Oracle Corporation
Java home: C:\Programs\Java\jdk1.8.0\jre
Default locale: en_US, platform encoding: Cp1250
OS name: "windows 8.1", version: "6.3", arch: "amd64", family: "dos"

注意

下载示例代码

您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

摘要

在本章中,我们在应用服务器开发的道路上迈出了第一步。我们介绍了应用服务器的最新功能,并对 Java 平台企业版(版本 7)进行了概述,也称为 Java EE 7。

接下来,我们讨论了 WildFly AS 的安装以及所有核心组件的安装,包括 JDK 和一系列开发工具,如 Eclipse 和 Maven,这些工具将是您在这段旅程中的伴侣。

在下一章中,我们将总结所有应用服务器的功能,特别关注交付应用程序所需的组件和命令,这是本书的主要目标。

第二章. 在 WildFly 上创建您的第一个 Java EE 应用程序

本章将为您提供关于新应用程序服务器的快速入门课程,以便您能够在下一章中创建我们第一个 Java EE 7 应用程序的部署框架。更具体地说,我们将涵盖以下主题:

  • WildFly 8 核心概念的介绍

  • WildFly 8 文件系统的结构

  • 可用管理工具的介绍

  • 发布您的第一个 Hello World 应用程序

WildFly 8 核心概念

现在我们已经下载并安装了 WildFly 8,花几分钟时间熟悉一些基本概念是值得的。架构和大多数核心思想直接来自 JBoss AS 7;尽管如此,也有一些新机制是在最新版本中引入的(例如,管理系统的基于角色的安全性、使用的端口数量减少以及新的补丁系统)。就像 JBoss AS 7 一样,WildFly 可以以两种模式运行:独立模式和域模式。

独立 模式下,每个 WildFly 实例都是一个独立的过程(类似于之前的 JBoss AS 版本,如版本 4、版本 5、版本 6 以及版本 7 的独立模式)。独立配置文件位于应用程序服务器的 standalone/configuration 目录下。

模式下,您可以从一个中心点运行多个应用程序服务器并管理它们。一个域可以跨越多个物理(或虚拟)机器。在每台机器上,我们可以安装几个受主机控制器进程控制的 WildFly 实例。域模式下的配置文件位于应用程序服务器的 domain/configuration 文件夹下。

从进程的角度来看,一个域由三个元素组成:

  • 域控制器: 域控制器是您域的管理控制点。在域模式下运行的 WildFly 实例最多只有一个进程实例充当域控制器。域控制器持有集中式配置,该配置由属于该域的节点实例共享。

  • 主机控制器: 这是负责协调服务器进程生命周期以及从域控制器到服务器实例部署分布的过程。

  • 应用程序服务器节点: 这些是映射应用程序服务器实例的常规 Java 进程。每个服务器节点反过来又属于一个服务器组。当讨论域配置文件时,将详细介绍域组。

此外,当启动域时,您将在您的机器上看到另一个 JVM 进程正在运行。这是进程控制器。这是一个非常轻量级的进程,其主要功能是生成服务器进程和主机控制器进程,并管理它们的输入/输出流。由于进程控制器不可配置,我们不会进一步讨论它。

下面的图显示了典型的域部署配置:

WildFly 8 核心概念

正如你在前面的图中可以看到的,一个主机(Host1)充当专用域控制器。这是在域管理的服务器中采用的一种常见做法,目的是为了在逻辑上和物理上将管理单元与托管应用程序的服务器分开。

其他主机(Host2Host3)包含域应用程序服务器,这些服务器分为两个服务器组:main-server-groupother-server-group。服务器组是一组逻辑上的服务器实例,它们将一起被管理和配置。每个服务器组都可以配置不同的配置文件和部署;例如,在前面提到的域中,你可以使用 main-server-group 提供一些服务,而使用 other-server-group 提供其他服务。

这有一些优点。例如,当你不想为了新版本而关闭你的应用程序时,你可以一次只重新部署一个服务器组。当一个服务器不完全运行时,请求可以被第二个服务器处理。

详细了解域配置超出了本书的范围;然而,到本章结束时,我们将看到如何使用 WildFly 中可用的命令行界面在域中部署应用程序单元。

WildFly 8 目录结构

独立和域之间的区别在以下图中显示了应用程序服务器的目录结构:

WildFly 8 目录结构

正如你在前面的图中可以看到的,WildFly 的目录结构分为两个主要部分:第一个与独立服务器模式相关,另一个是针对域服务器模式的。两种服务器模式都有的共同点是 modules 目录,这是应用程序服务器的心脏。

WildFly 基于 JBoss Modules 项目,该项目提供了一个模块化(非层次化)的类加载和 Java 执行环境实现。换句话说,而不是一个加载所有 JAR 到平坦类路径的单个类加载器,每个库都成为一个模块,它只链接到它所依赖的确切模块,不再链接其他任何内容。它实现了一个线程安全、快速且高度并发的委托类加载器模型,并配有一个可扩展的模块解析系统。这些结合在一起形成了一个独特、简单且强大的应用程序执行和分发系统。

下表详细说明了 JBOSS_HOMEroot 目录中每个文件夹的内容:

文件夹 描述
bin 这个文件夹包含启动脚本、启动配置文件以及各种命令行实用程序,例如 vault、add-user 和适用于 Unix 和 Windows 环境的 Java 诊断报告
bin/client 此文件夹包含一个客户端 Jar 文件,用于远程 EJB、CLI 以及不使用任何具有自动依赖管理功能的构建系统(如 Maven、Ivy 的 Ant 或 Gradle)的客户。
bin/init.d WildFly 新增功能,此文件夹包含用于 Red Hat Linux 和 Debian 的脚本,这些脚本将 WildFly 注册为 Linux 服务。
bin/service WildFly 新增功能,此文件夹包含一个脚本,允许将 WildFly 注册为 Windows 服务。
docs/examples 此文件夹包含一些示例独立配置,例如最小化独立配置(standalone-minimalistic.xml)。
docs/schema 此文件夹包含 XML 模式定义文件。
domain 此文件夹包含由此安装的域模式进程使用的配置文件、部署内容和可写区域。
modules 此文件夹包含在应用程序服务器上安装的所有模块。
standalone 此文件夹包含由此安装的单个独立服务器使用的配置文件、部署内容和可写区域。
appclient 此文件夹包含由此安装的应用程序客户端容器使用的配置文件、部署内容和可写区域。
welcome-content 此文件夹包含默认欢迎页内容。

深入研究独立模式树,我们可以找到与独立独立进程相关的文件夹。如果你有早期服务器版本的经验,你会发现这些文件夹对你来说非常直观:

目录 描述
configuration 此目录包含从此安装运行的独立服务器的配置文件。所有运行服务器的配置信息都位于此处,并且是独立服务器配置修改的唯一位置。
data 此目录包含服务器写入的持久信息,以便在服务器重启后继续存在。
deployments 最终用户部署内容可以放置在此目录中,以便自动检测并将该内容部署到服务器的运行时。
lib/ext 此目录是安装的库 Jar 文件的存放位置,由使用扩展列表机制的应用程序引用。
log 此目录包含独立服务器日志文件。
tmp 此目录包含服务器写入的临时文件的位置。

domain目录结构与独立模式类似,但有一个重要区别。如以下表格所示,deployments文件夹不存在,因为域模式不支持基于扫描文件系统部署内容。我们需要使用 WildFly 管理工具(CLI 和 Web 管理控制台)来部署应用程序到域。 |

目录 描述
configuration 此目录包含域主机控制器和在此安装上运行的任何服务器的配置文件。域内管理的所有服务器的配置信息都位于此处,并且是配置信息的唯一位置。
data/content 此目录是主机控制器的一个内部工作区域,它控制这个安装。这是它内部存储部署内容的地方。此目录不打算由最终用户操作。它是在第一次服务器启动后创建的。
log 此目录是主机控制器进程写入其日志的位置。进程控制器,一个实际产生其他主机控制器进程和任何应用服务器进程的小型、轻量级进程,也在这里写入日志。它是在第一次服务器启动后创建的。

| servers | 此目录是每个从该安装运行的应用服务器实例的可写区域。每个应用服务器实例将有一个自己的子目录,在服务器第一次启动时创建。在每个服务器的子目录中,将存在以下子目录:

  • data: 这是服务器写入的、需要服务器重启后仍然存在的信息。

  • log: 这是服务器的日志文件。

  • tmp: 这是服务器写入的临时文件的位置。此文件夹在第一次服务器启动后创建。

|

tmp 此目录包含服务器写入的临时文件的位置。

管理应用服务器

WildFly 提供了三种不同的方式来配置和管理服务器:一个网页界面、一个命令行客户端和一组 XML 配置文件。无论你选择哪种方法,配置总是同步到不同的视图,并最终持久化到 XML 文件中。使用网页界面保存更改后,你将立即在你的服务器配置目录中看到更新的 XML 文件。

使用网页界面管理 WildFly 8

备注

WildFly 8 默认是安全的,默认的安全机制基于用户名或密码,并使用 HTTP 摘要。默认情况下保护服务器的理由是,如果管理接口意外地暴露在公共 IP 地址上,则需要认证才能连接。因此,分发中没有默认用户。

用户存储在mgmt-users.properties属性文件中,该文件位于独立配置或域配置下,具体取决于服务器的运行模式。此文件包含用户名信息以及预先计算的哈希值,以及域和用户的密码。

为了操作文件和添加用户,服务器已提供如add-user.shadd-user.bat之类的实用程序来添加用户并生成散列。所以只需执行脚本并遵循引导过程。这在上面的屏幕截图中显示:

使用 Web 界面管理 WildFly 8

为了创建新用户,您需要提供以下信息:

  • 用户类型:用户类型将是管理用户,因为它将管理应用程序服务器。

  • :这必须与配置中使用的域名称匹配,除非您已更改配置以使用不同的域名称,否则请将此设置为ManagementRealm

  • 用户名:这是您要添加的用户的用户名。

  • 密码:这是用户的密码。

  • 用户组:这是一个逗号分隔的组列表,应分配给新创建的用户;它们用于 WildFly 中引入的角色基于访问控制和审计系统。用户组的信息存储在mgmt-groups.properties文件中。

如果验证成功,您将被要求确认是否要添加用户;只有在这种情况下,properties文件才会被更新。

最后一个问题(这个新用户是否将被用来连接一个 AS 进程到另一个?)可以用来添加从属主机控制器,这些控制器将验证主域控制器。这反过来又需要在从属主机的配置中添加密钥,以便与主域控制器进行验证。(有关域配置的更多信息,请访问docs.jboss.org/author/display/WFLY8/Admin+Guide#AdminGuide-ManagedDomain。)

启动 Web 控制台

现在我们已经添加了至少一个用户,我们可以在默认地址http://<host>:9990/console启动 Web 控制台(请注意,您必须首先启动服务器,例如使用standalone.batstandalone.sh)。

将会提示登录界面。在用户名密码字段中输入数据,这是我们之前创建的。这在上面的屏幕截图中显示:

启动 Web 控制台

登录后,您将被重定向到 Web 管理主屏幕。当以独立模式运行时,Web 控制台将分为三个主要标签页:配置运行时管理。这在上面的屏幕截图中显示:

启动 Web 控制台

配置标签页包含所有作为服务器配置一部分的单个子系统。因此,一旦您在左侧框架中选择配置标签页,您就可以访问所有子系统并编辑它们的配置(在之前的屏幕截图中,我们看到了数据源子系统)。

另一个名为运行时的标签页可以用于两个主要目的:管理应用程序的部署和检查服务器指标。这在上面的屏幕截图中显示:

启动 Web 控制台

WildFly 和 Red Hat JBoss EAP 6.2 引入了管理标签页,目前它仅包含与基于角色的访问控制相关的选项。您现在可以限制管理用户的权限,例如,这样不是每个管理员都可以使用 Web 控制台卸载应用程序。默认情况下,此功能是禁用的。您必须手动使用 CLI 机制启用它。这在上面的屏幕截图中显示:

启动 Web 控制台

一旦您学会了如何访问 Web 控制台,现在是时候尝试您的第一个应用程序示例了。

将您的第一个应用程序部署到 WildFly 8

为了测试启动我们的第一个应用程序,我们将使用 Eclipse 创建一个HelloWorld网络项目。主要部分是一个servlet类,用于生成 HTML 标记。因此,启动 Eclipse,通过导航到文件 | 新建 | 动态网络项目来创建一个新的网络项目。这在上面的屏幕截图中显示:

将您的第一个应用程序部署到 WildFly 8

为您的应用程序选择一个名称,如果您想在 Eclipse 工作空间同一位置创建项目,请勾选使用默认位置复选框。如果您已在 Eclipse 中正确配置了新的 WildFly 服务器,您应该看到默认选中了WildFly 8.0 运行时选项,并且在配置框中预选了WildFly 8.0 运行时目标运行时默认配置

选择3.1作为动态网络模块版本,这将通过使用 Servlet 3.1 规范使开发变得容易,并且也保留EAR 成员资格将项目添加到工作集复选框未选中。

点击完成继续。

现在,让我们向我们的项目中添加一个典型的简单 servlet,它仅将一个Hello World消息作为 HTML 页面输出。从文件菜单,转到新建 | Servlet,为您的 servlet 输入一个有意义的名称和包名,例如名称为TestServlet,包名为com.packtpub.wflydevelopment.chapter2。这在上面的屏幕截图中显示:

将您的第一个应用程序部署到 WildFly 8

向导将生成一个基本的 servlet 骨架,需要通过以下代码行进行增强:

@WebServlet("/test")
public class TestServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private static final String CONTENT_TYPE = 
      "text/html;charset=UTF-8";
    private static final String MESSAGE = "<!DOCTYPE html><html>" +
            "<head><title>Hello!</title></head>" +
            "<body>Hello World WildFly</body>" +
            "</html>";

    @Override
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType(CONTENT_TYPE);
        try (PrintWriter out = response.getWriter()) {
            out.println(MESSAGE);
        }
    }
}

该 servlet 将对针对其 URL 地址发出的每个 GET HTTP 请求响应一个静态 HTML 页面(我们定义的内容类型为 Text/HTML,字符集为 UTF-8)。

注意

注意到 TextServlet 带有 @WebServlet 注解,这是由 Servlet 3.0 API 引入的,它允许在不使用 web.xml 配置文件的情况下注册一个 servlet。在我们的例子中,我们使用它来自定义 servlet URL 绑定,使用 /test,否则 Eclipse 会将其默认为类名。

我们将通过创建一个名为 jboss-web.xmlJBoss 文件描述符,位于 /WebContent/WEB-INF/ 目录中,来完成应用程序;尽管这不是强制性的,但它可以用来重新定义上下文根,如下面的代码片段所示:

<jboss-web>
    <context-root>/hello</context-root>
</jboss-web>

注意

jboss-web.xml 的模式定义文件命名为 jboss-web_8_0.xsd,可以位于 JBOSS_HOME/docs/schema 文件夹中。

请记住,创建 jboss-web.xml 使得应用程序无法在其他 Java EE 应用服务器上移植。当没有定义此类文件时,默认的应用程序路径是应用程序名称和版本的连接,例如,对于名为 TestServlet 且版本为 1.0 的应用程序,它将是 TestServlet-1.0。

现在,我们将通过在 Eclipse 服务器选项卡上右键单击并选择添加和移除来将 Web 应用程序添加到已部署资源的列表中。这如图所示:

将您的第一个应用程序部署到 WildFly 8

接下来,点击添加将项目添加到服务器上配置的资源列表中,如图所示:

将您的第一个应用程序部署到 WildFly 8

如果你已经在 Eclipse 内启动了 WildFly,资源将通过检查标志来自动部署,以查看服务器是否已启动,并立即发布更改。

另一方面,如果你在外部启动了应用程序服务器,那么你可以通过在应用程序上右键单击并选择完全发布来完全发布你的资源,如图所示:

将您的第一个应用程序部署到 WildFly 8

现在,转到浏览器并检查应用程序是否在配置的 URL 上响应,如图所示:

将您的第一个应用程序部署到 WildFly 8

此示例也以 Maven(将在下一章介绍)项目的形式存在于您的 Packt Publishing 账户中。

高级 Eclipse 部署选项

如此一来,Eclipse 已经在 JBOSS_HOME/standalone/deployments 中发布了 HelloWorld.war 文件夹。

注意

你可能已经注意到,Eclipse 还添加了一个名为 HelloWorld.war.dodeploy 的标记文件。这一步是必要的,因为默认情况下,WildFly 中的展开部署不会自动部署。展开内容的自动部署默认是禁用的,因为部署扫描器可能会尝试部分部署复制的目录,这会导致许多错误。可以通过名为 application.[jar/war/ear].dodeploy 的标记文件手动触发展开存档的部署。

一旦应用程序部署完成,应用程序服务器将用部署的HelloWorld.war文件替换.dodeploy标记文件,或者在部署失败的情况下,用HelloWorld.war.failed文件替换。

您可以通过双击 WildFly 8.0(在服务器选项卡中),然后选择部署选项卡来更改默认的部署选项,如下截图所示:

高级 Eclipse 部署选项

部署选项卡中,您可以通过勾选使用自定义部署文件夹选项并在相应的文本框中输入适当的值来选择将您的应用程序部署到自定义部署文件夹。

请注意,自定义部署文件夹也必须在 WildFly 中定义;有关更多信息,请参阅下一节。

此外,请注意将项目作为压缩存档部署选项,在某些情况下可能很有用,例如,如果您通过其他工具(如 CLI)分发应用程序,这些工具只能部署压缩存档。

使用 Web 控制台管理部署

使用 Eclipse 部署应用程序是一个简单的任务,并且可能是您开发应用程序时的首选选项。我们将在此处了解如何使用 Web 控制台部署应用程序,这可以成为您箭袋中的另一支箭。

注意

此示例的一个典型场景可能是您正在以域模式运行 AS,或者简单地将应用程序部署到远程 WildFly 实例。

启动 Web 控制台并单击运行时选项卡。从左侧面板中,转到服务器 | 管理部署,如下截图所示:

使用 Web 控制台管理部署

在中央面板中,我们可以使用添加删除/禁用更新按钮来管理部署。选择添加按钮以添加新的部署单元。在下一屏幕中,从您的本地文件系统中选择您想要部署的文件(例如,HelloWorld.war工件,可以通过在 Eclipse 测试项目中导航到文件 | 导出 | Web | WAR 文件来创建),如下截图所示:

使用 Web 控制台管理部署

通过验证部署的名称并单击保存来完成向导,如下截图所示:

使用 Web 控制台管理部署

现在,部署已列在部署表中。然而,默认情况下它并未启用。单击/禁用按钮以启用应用程序的部署,如下截图所示:

使用 Web 控制台管理部署

修改部署扫描器属性

如我们之前所见,以独立模式运行的应用程序默认在 deployments 文件夹中进行扫描。你可以通过点击 配置 选项卡并从左侧菜单导航到 子系统 | 核心 | 部署扫描器 来更改此行为(以及部署扫描器的属性)。这如下面的截图所示:

更改部署扫描器属性

部署扫描器 中,你可以设置核心部署的属性。你可以点击 编辑 按钮来为这些属性定义新值。其中大部分是自解释的;然而,以下表格总结了它们:

属性 描述
name 这是部署扫描器的名称(默认情况下,提供名称 default)。
path 这是部署扫描扫描的绝对路径。如果设置了 相对于路径 属性,则将其附加到相对路径定义上。
启用 此属性确定部署扫描器是否启用。
相对于路径 如果包含,此属性必须指向用于构建相对路径表达式的系统路径。
扫描间隔 这是部署扫描的频率(以毫秒为单位)。
自动部署压缩包 将此设置为 true 将启用压缩应用的自动部署。其默认值是 true
自动部署展开 将此设置为 true 将启用展开应用的自动部署。其默认值是 true
部署超时 这指的是部署操作标记为 失败 之后的超时时间。

使用命令行界面部署应用程序

部署应用程序的另一种方式是通过 WildFly 命令行界面CLI),可以从 jboss-cli.bat(或 Linux 用户的 jboss-cli.sh)启动。不要害怕使用文本界面来管理你的应用程序服务器;事实上,控制台提供了内置的自动完成功能,你可以通过简单地按 Tab 键在任何时候显示可用的命令,如下面的截图所示:

使用命令行界面部署应用程序

如你所猜,为了部署一个应用程序,你需要发出 deploy shell 命令。当不带参数使用时,deploy shell 命令提供当前已部署的应用程序列表。请参考以下代码:

[standalone@localhost:9990 /] deploy
ExampleApp.war

如果你将一个资源存档,例如 .war,传递给 shell,它将立即在独立服务器上部署它,如下面的命令行所示:

[standalone@localhost:9990 /] deploy ../HelloWorld.war 

如您从前面的命令行中看到的,CLI 使用您实际启动部署的文件夹的初始位置,默认情况下为JBOSS_HOME/bin。然而,当指定存档的位置时,您可以使用绝对路径;CLI 的扩展功能(使用Tab键)使此选项相当简单。以下命令行展示了这一点:

[standalone@localhost:9990 /] deploy c:\deployments\HelloWorld.war

命令执行后没有错误消息;因此,应用程序已部署并激活,用户可以访问它。如果您只想执行应用程序的部署并将激活推迟到以后,您必须添加--disabled开关,如下所示:

[standalone@localhost:9990 /] deploy ../HelloWorld.war --disabled 

为了激活应用程序,只需发出另一个不带--disabled开关的deploy shell 命令,如下所示:

[standalone@localhost:9990 /] deploy -–name=HelloWorld.war 

重新部署应用程序需要为deploy shell 命令添加一个额外的标志。使用-f参数强制应用程序重新部署,如下所示:

[localhost:9990 /] deploy -f ../HelloWorld.war

使用undeploy命令可以卸载应用程序,该命令将已部署的应用程序作为参数。以下命令行展示了这一点:

[localhost:9990 /] undeploy HelloWorld.war

部署应用程序到域

在域模式下运行时部署应用程序与在独立模式下进行此操作略有不同。这种差异归结为应用程序可以仅部署到单个服务器组或所有服务器组。实际上,您可能将域拆分为不同的服务器组的原因之一可能是您计划为每个服务器组提供不同类型的服务(因此是应用程序)。

因此,为了将您的HelloWorld.war应用程序部署到所有服务器组,请发出以下命令:

[domain@localhost:9990 /] deploy HelloWorld.war --all-server-groups

另一方面,如果您想从属于域的所有服务器组中卸载应用程序,您必须发出undeploy命令,如下所示:

[domain@localhost:9990 /] undeploy HelloWorld.war --all-relevant-server-groups

您也可以通过指定一个或多个服务器组(用逗号分隔)并使用--server-groups参数,将应用程序仅部署到域的某个服务器组,如下所示:

[domain@localhost:9990 /] deploy HelloWorld.war --server-groups=main-server-group

您可以使用 Tab 补全功能来完成为部署选择的--server组列表的值。

现在,假设我们希望仅从单个服务器组中卸载应用程序。可能有两种情况。如果应用程序仅在该服务器组中可用,您只需将服务器组传递给--server-groups标志,如下所示:

[domain@localhost:9990 /] undeploy HelloWorld.war --server-groups=main-server-group

另一方面,如果您的应用程序在其他服务器组上也有可用,您需要提供额外的--keep-content标志;否则,CLI 将抱怨它无法删除由其他服务器组引用的应用程序,如下所示:

[domain@localhost:9990 /] undeploy HelloWorld.war --server-groups=main-server-group --keep-content

摘要

在本章中,我们进行了一次关于应用服务器的快速课程,重点关注可用的管理工具:Web 界面和命令行界面。然后我们看到了如何使用这些工具将一个示例应用程序部署到独立环境和域环境。

在下一章中,我们将深入探讨 Java EE 7 组件,从企业 JavaBeans 开始,它在 Java 企业应用程序的发展场景中仍然扮演着重要的角色。

第三章. 介绍 Java EE 7 – EJBs

在上一章中,你学习了如何设置和部署 Hello World 应用程序在 WildFly 上的基础知识。在本章中,我们将更深入地学习如何创建、部署和组装企业 JavaBeans,它们是大多数企业应用程序的核心。此外,你还将学习如何使用 Maven,这是一个流行的构建工具,可以简化我们的 Bean 的打包过程。

更详细地说,以下是本章你将学习的内容:

  • 新的 EJB 3.2 规范引入了哪些变化

  • 如何创建一个 Java EE 7 Maven 项目

  • 如何开发一个单例 EJB

  • 如何创建无状态和有状态的 Enterprise JavaBeans

  • 如何向你的应用程序添加和管理调度器和定时器

  • 如何在 EJB 项目中使用异步 API

EJB 3.2 – 概述

根据企业 JavaBeansEJB)规范,企业 JavaBeans 是通常实现 Java 企业版应用程序(对于 Java EE,请注意,Oracle 建议不要使用 JEE 作为 Java 企业版的缩写;有关 Java 相关技术缩写的更多信息,请访问java.net/projects/javaee-spec/pages/JEE)。由于它们的事务性,EJBs 也常用于许多应用程序的数据访问层构建。然而,在新版规范中,容器管理的交易不再是企业 JavaBeans 的专属,并且可以在 Java EE 平台的其它部分重用。

基本上,Enterprise JavaBeans 有三种类型:

  • 会话 Bean:这是最常用的 EJB 类型。容器管理每个被定义为会话 Bean 的类的多个实例(单例除外,它只有一个实例)。当一个由 EJB 实现的操作必须执行时(例如,因为用户请求更新数据库中的实体),容器为特定用户分配一个会话 Bean 实例。然后,代表调用客户端执行此代码。容器负责为会话 Bean 提供多个系统级服务,例如安全、事务或 Bean 的分布。

  • 消息驱动 BeanMDB):MDBs 是能够异步处理任何 JMS 生产者发送的消息的企业 Bean。(我们将在第六章,使用 JBoss JMS 提供程序开发应用程序中讨论 MDBs。)

  • 实体对象:EJB 用于在数据库中表示实体。最新版本的规范使这种类型的 Enterprise JavaBeans 成为可选的,因此它们可能不在所有容器中得到支持(在 WildFly 中也已取消支持)。实体对象将在 Java EE 8 中从规范中删除。目前,在 Java EE 7 中,主要的持久化技术是 Java 持久化 API。我们将在第五章,将持久化与 CDI 结合中讨论 JPA。

此外,会话 Bean 可以根据其特性和使用场景分为三种子类型。

  • 无状态会话 BeanSLSB):这些对象的实例与调用它们操作的客户端没有对话状态。这意味着当它们不服务客户端时,所有这些 Bean 实例都是相同的,并且容器可以为它们准备一个池来并行处理多个请求。因为它们不存储任何状态,所以它们的性能开销相当低。无状态服务负责从数据库检索对象的场景是 SLSB 的一个常见用法。

  • 有状态会话 BeanSFSB):SFSB 支持与紧密耦合的客户端进行对话式服务。有状态会话 Bean 为特定客户端完成一项任务,并且不能在多个调用者之间共享。它在客户端会话期间维护状态。会话完成后,状态不会保留。容器可能会决定钝化(序列化并存储以供将来使用)一个过期的 SFSB。这样做是为了节省应用服务器的资源,或者在某些情况下,为了在应用服务器域中支持 SFSB 故障转移机制(这是 JBoss AS 7 和 WildFly 的情况)。从 EJB 3.2 开始,可以禁用特定 SFSB 的钝化,尽管这可能会影响服务器的稳定性和故障转移能力。购物车可以作为 SFSB 的一个简单用例。

  • 单例 EJB:这本质上与无状态会话 bean 相似;然而,它使用单个实例来服务客户端请求。因此,你可以保证在调用之间使用相同的实例。单例可以使用更丰富的生命周期来处理一系列事件,以及控制 bean 初始化时间的可能性。此外,可以强制执行更严格的锁定策略来控制对实例的并发访问,以便多个客户端可以使用单例 bean 的共享状态。如果应用程序在域的多个节点上分布式部署,那么每个运行的 JVM 都将有自己的单例 bean 实例。我们将在第十一章中进一步讨论这个问题,WildFly 应用程序的集群。由于它们的特殊特性,单例可以用来保存应用程序的状态、缓存或在应用程序启动时初始化一些资源。

如我们之前提到的,容器管理 bean 的实例,但客户端应该通过业务接口来调用它们。会话 bean 有三种类型的视图可用:

  • 本地业务接口:当 bean 及其客户端位于同一容器中时使用此会话 bean。它使用按引用传递的语义,因此返回值和方法参数基于引用,而不是对象的副本。

  • 远程业务接口:在这个会话 bean 中,客户端和 bean 的位置是独立的(客户端可能位于另一个容器中,或者根本不需要容器,例如作为一个独立的应用程序)。每个参数和返回值都被序列化和复制。

  • 无接口视图:这个会话 bean 是本地业务视图的一个变体,它不需要单独的接口,也就是说,bean 类的所有public方法都会自动暴露给调用者。

自 EJB 3.1 以来,可以使用异步方法。这些方法能够异步处理客户端请求,就像消息驱动 bean 一样,除了它们提供了一个类型化的接口,并采用更复杂的方法来处理客户端请求。可以使用两种方法来实现这种行为:

  • 客户端调用的异步 void 方法,即“fire-and-forget”异步方法

  • 返回类型为Future<?>的异步方法,即“检索结果稍后”方法

在继续之前,你还需要了解哪些关于 EJB 的知识?当你开发企业 JavaBean 时,你必须遵循一些通用规则,如下所示:

  • 避免使用非 final 静态字段

  • 不要手动创建线程(我们将在第十二章中更深入地讨论这个主题,长期任务执行

  • 不要使用同步原语(除了在 bean 管理的并发中的单例)

  • 禁止在文件系统上手动执行文件操作和监听套接字

  • 原生库不应被加载

违反这些规则可能会导致 EJB 容器出现安全和稳定性问题。可以在www.oracle.com/technetwork/java/restrictions-142267.html找到不允许的活动列表,以及一些特定点的解释。

由于用实际例子更容易理解概念,在下一节中,我们将提供一个具体的应用程序示例,介绍本节中描述的一些特性。

开发单例 EJB

如其名所示,javax.ejb.Singleton 是一个会话 Bean,它保证应用程序中最多只有一个实例。

注意

此外,单例 EJB 填补了 EJB 应用程序中的一个知名空白,即当应用程序启动和停止时能够通知 EJB。因此,你可以用 EJB 做许多以前(在 EJB 3.1 之前)只能用启动时加载的 servlet 做的事情。EJB 还为你提供了一个可以存放与整个应用程序及其所有用户相关数据的地方,而不需要静态类字段。

为了将你的 EJB 转换为单例,你只需要在它上面应用 @javax.ejb.Singleton 注解。

注意

单例 Bean 类似于有状态的 Bean,因为状态信息在方法调用之间保持。然而,每个服务器 JVM 只有一个单例 Bean,它被应用程序的所有 EJB 和客户端共享。这种类型的 Bean 提供了一种方便的方式来维护应用程序的整体状态。但是,如果应用程序分布在多个机器(因此多个 JVM)上,那么每个机器上的单例都是唯一的。任何应用程序状态必须在节点之间同步。

值得学习的一个注释是 @javax.ejb.Startup,它会在应用程序启动时由容器实例化该 Bean。如果你在 EJB 中定义了带有 @javax.annotation.PostConstruct 注解的方法,它将调用该方法。

现在我们已经有了足够的信息来理解我们的第一个 EJB 示例。创建 Java Enterprise 项目有多种选择。在早期章节中,我们展示了如何从一个基于 Eclipse Java EE(一个动态 Web 项目)的项目开始,稍后将其绑定到 WildFly 运行时安装。这显然是最简单的选择,你可以很容易地使用这种模式运行本书中的示例;然而,当涉及到企业解决方案时,几乎每个项目现在都使用某种类型的构建自动化工具。对于本书,我们将提出 Apache Maven,因为它是最受欢迎的选择之一,但并非唯一。Gradle 是一个类似的项目,它使用 Groovy 语言来描述项目结构、依赖关系和构建工作流程。

当你转向 Maven 项目时,你将获得的某些好处包括定义良好的依赖结构、项目构建最佳实践的传统,以及项目模块化设计,仅举几例。此外,当你有一个自动化的构建过程时,你可以使用持续集成工具(如 Jenkins)来安排应用程序的自动化测试和部署。

所有主要的 IDE 都有内置的 Maven 支持。这包括 Eclipse Java EE Luna 版本。

因此,让我们直接从 Eclipse 创建我们的第一个 Maven 项目。导航到 文件 | 新建 | 其他 | Maven | Maven 项目。这将在以下屏幕截图中显示:

开发单例 EJB

点击 下一步;你将被带到以下中间屏幕:

开发单例 EJB

Maven 允许在创建新项目时使用原型。它们定义了一个项目的基本依赖、资源、结构等等。例如,你可以使用一个网络应用程序原型来获取一个空的项目骨架,然后你可以直接构建和部署。不幸的是,原型通常过时,你仍然需要根据你的需求进行调整。为了使用一些 Java EE 7 原型,你必须首先定义一个仓库和你想要使用的原型,然后你才能创建一个项目。在现实生活中,你可能只是通过查看你之前的那些项目来创建每一个新项目,而不使用任何原型。因此,这里我们将展示如何从头开始创建一个项目。你可能还对一些额外的 Java EE 相关工具感兴趣,例如 JBoss Forge,你将在附录中找到其描述,Rapid Development Using JBoss Forge

在可见的屏幕上,勾选 创建一个简单项目 复选框。使用此选项,我们将跳过原型选择。你可以点击 下一步。现在,你必须完成一些基本的项目信息。我们正在创建一个服务器端 EJB 应用程序,它还有一个独立的客户端。这两个项目可以共享一些共同信息,例如关于依赖及其版本的信息。因此,我们想要创建一个 Maven 多模块项目。在这个第一步中,让我们创建一个具有 POM 打包的父项目。POM 是 Maven 用来描述项目及其模块结构的一种约定。更多关于这个的信息可以在我们之前章节中提到的 Sonatype 免费书籍中找到。

你可以通过输入一些包特定信息来完成向导,如下面的屏幕截图所示:

开发单例 EJB

对于 组 ID(一个具有类似 Java 包角色的抽象标识符),您可以使用 com.packtpub.wflydevelopment.chapter3。对于 工件 ID(我们项目的简化名称),只需使用 ticket-agency。将 打包 字段设置为 pom,并且可以保留项目 版本 字段的默认选择。点击 完成 以完成向导。

看看我们新创建的项目。目前,它只包含 pom.xml,这将是新模块的基础。再次导航到 文件 | 新建 | 其他 | Maven,但现在选择 新建 Maven 模块。您现在可以看到以下截图:

开发单例 EJB

再次,我们想跳过存档选择,因此请检查 创建简单项目 选项。在 父项目 下,点击 浏览 并选择我们之前创建的父项目。在 模块名称 下,输入 ticket-agency-ejb。点击 下一步。您将看到以下屏幕。

开发单例 EJB

现在,让我们讨论打包类型。Java EE 部署有几种可能的存档类型:

  • EJB 模块:此模块通常包含 EJB 的类,打包为 .jar 文件。

  • Web 模块:此存档可以包含额外的 Web 元素,如 servlet、静态 Web 文件、REST 端点等。它打包为 .war 文件(Web 存档)。

  • 资源适配器模块:此存档包含与 JCA 连接器相关的文件(在第六章开发使用 JBoss JMS 提供程序的应用程序中描述),打包为 .rar 文件。

  • 企业存档:此存档聚合了多个 Java EE 模块(EJB、Web)及其相关描述符。它打包为 .ear 文件。

在这里,我们只想部署 EJB 而不包含任何 Web 元素,因此将打包设置为 EJB(如果它在 Eclipse 下拉菜单中不可见,只需手动输入即可)并点击 完成

按照相同的步骤添加第二个模块,模块名为 ticket-agency-ejb-client,打包类型为 JAR。这将是一个简单的客户端,用于在 ticket-agency-ejb 中公开的服务。

现在,看看我们的父项目 pom.xml。它应该定义两个最近创建的模块,如下所示:

    <modules>
        <module>ticket-agency-ejb</module>
        <module>ticket-agency-ejb-client</module>
    </modules>

这些操作的预期结果应与以下截图相匹配,该截图是从项目资源管理器视图获取的:

开发单例 EJB

如您所见,ticket-agency-ejbticket-agency-ejb-client 项目已按标准 Maven 项目组织:

  • src/main/java 将包含我们的源代码

  • src/main/resources 用于配置(包含一个用于 EJB 项目的裸骨 ejb-jar.xml 配置文件)

  • src/test/java 用于存储测试类

目前,我们将关注主文件 pom.xml,它需要知道 Java EE 依赖项。

配置 EJB 项目对象模块(pom.xml)

在深入研究代码之前,首先你需要进一步配置 Maven 的 pom.xml 配置文件。这个文件相当冗长,所以我们在这里只展示理解我们的示例所必需的核心元素,完整的列表留到本书的代码示例包中。

我们将在属性部分之后添加对 Java EE 7 API 的引用,如下所示:

<dependencies>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>7.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

这个依赖项将添加所有 Java EE 7.0 API 的定义。范围设置为 provided,这意味着依赖项在目标环境中(在我们的例子中,是应用程序服务器)可用,并且不需要包含在构建的存档中。这个依赖项是通用的,应该与所有兼容 Java EE 7.0 的应用程序服务器一起工作,而不仅仅是与 WildFly。

我们还希望添加第二个依赖项,即 JBoss 日志 API。将此定义放在相同的 <dependencies> </dependencies> 标签中,例如在 javaee-api 下方,如下所示:

<dependency>
    <groupId>org.jboss.logging</groupId>
    <artifactId>jboss-logging</artifactId>
    <version>3.1.4.GA</version>
    <scope>provided</scope>
</dependency>

注意

提供的范围包括企业依赖项,相当于将库添加到编译路径。因此,它期望 JDK 或容器在运行时提供依赖项。除了依赖项之外,我们还想配置构建过程。创建的项目指定了 EJB 打包,但构建使用的是与 JDK 1.5 兼容的级别和一个旧的 EJB 版本。这就是为什么我们想在 pom.xml 中添加一个额外的代码块,如下所示:

<build>
    <plugins>
        <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
                <!-- enforce Java 8 -->
                <source>1.8</source>
                <target>1.8</target>
           </configuration>
        </plugin>
        <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-ejb-plugin</artifactId>
           <version>2.3</version>
           <configuration>
                <ejbVersion>3.2</ejbVersion>
                <!-- Generate ejb-client for client project -->
                <generateClient>true</generateClient>
           </configuration>
        </plugin>
    </plugins>
</build>

此代码块执行以下两项操作:

  • maven-compiler-plugin 配置强制使用 Java 8

  • maven-ejb-plugin 配置定义了使用 EJB 3.2 版本,并启用了为 EJB 客户端应用程序生成 EJB 客户端(默认禁用)包

此外,检查 src/main/resources/META-INF/ejb-jar.xml 文件。它可能包含来自 EJB 2.1 的配置。相反,使用以下代码:

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

         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee"
         version="3.2">
    <display-name>ticket-agency-ejb</display-name>
    <ejb-client-jar>ticket-agency-ejbClient.jar</ejb-client-jar>
</ejb-jar>

到目前为止,你将能够编译你的项目;因此,我们将开始添加类,但在部署你的工件时,我们将回到 pom.xml 文件。

编写我们的 EJB 应用程序

创建 EJB 类不需要与复杂的向导混淆;你只需要添加裸 Java 类。因此,从文件菜单,转到新建 | Java 类,并将 TheatreBox 作为类名,com.packtpub.wflydevelopment.chapter3.control 作为包名输入。

我们将在类中添加以下实现:

@Singleton
@Startup
@AccessTimeout(value = 5, unit = TimeUnit.MINUTES)
public class TheatreBox {

    private static final Logger logger = Logger.getLogger(TheatreBox.class);

    private Map<Integer, Seat> seats;

    @PostConstruct
    public void setupTheatre() {
        seats = new HashMap<>();
        int id = 0;
        for (int i = 0; i < 5; i++) {
            addSeat(new Seat(++id, "Stalls", 40));
            addSeat(new Seat(++id, "Circle", 20));
            addSeat(new Seat(++id, "Balcony", 10));
        } 
        logger.info("Seat Map constructed.");
    }

    private void addSeat(Seat seat) {
        seats.put(seat.getId(), seat);
    }

    @Lock(READ)
    public Collection<Seat> getSeats() {
        return Collections.unmodifiableCollection(seats.values());
    }

    @Lock(READ)
    public int getSeatPrice(int seatId) throws NoSuchSeatException {
        return getSeat(seatId).getPrice();
    }

    @Lock(WRITE)
    public void buyTicket(int seatId) throws SeatBookedException, NoSuchSeatException {
        final Seat seat = getSeat(seatId);
        if (seat.isBooked()) {
            throw new SeatBookedException("Seat " + seatId + " already booked!");
        }
        addSeat(seat.getBookedSeat());
    }

    @Lock(READ)
    private Seat getSeat(int seatId) throws NoSuchSeatException {
        final Seat seat = seats.get(seatId);
        if (seat == null) {
            throw new NoSuchSeatException("Seat " + seatId + " does not exist!");
        }
        return seat;
    }
}

让我们详细看看我们的应用程序代码;void 方法 setupTheatre 在应用程序部署时被调用,负责组装剧院座位,创建一个简单的 Seat 对象映射。座位标识符是这个映射的关键因素。这发生在部署之后,因为我们的 bean 被注解为 @Singleton@Startup,这强制容器在启动时初始化 bean。每个 Seat 对象都是使用一组三个字段构造函数构建的,包括座位 ID、其描述和价格(预订字段最初设置为 false)。这在下述代码中给出:

public class Seat {
    public Seat(int id, String name, int price) {
        this(id, name, price, false);
    }
    private Seat(int id, String name, int price, boolean booked) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.booked = booked;
    }
    public Seat getBookedSeat() {
        return new Seat(getId(), getName(), getPrice(), true);
    }
    // Other Constructors, Fields and Getters omitted for brevity
}

注意,我们的 Seat 对象是不可变的。在创建实例后,我们将无法更改其状态(字段的值,所有字段都是最终的,并且没有暴露设置器)。这意味着当我们向客户端(本地或远程)返回一个 Seat 对象时,它只能用于读取。

接下来,单例 bean 公开了四个公共方法;getSeats 方法返回一个不可修改的 Seat 对象集合,它将返回有关它们是否已被预订的信息给用户。该集合必须是不可修改的,因为我们的 Singleton 公开了一个无接口视图,这意味着我们正在使用引用传递语义。如果我们不保护集合,那么对返回集合中任何元素的更改都将在我们的缓存上完成。更重要的是,客户端可以向我们的内部集合添加或删除元素!

getSeatPrice 方法是一个实用方法,它将获取座位价格并将其作为 int 返回,因此可以用来验证用户是否有能力购买门票。

getSeat 方法返回一个给定 ID 的不可变 Seat 对象。再次强调,我们返回一个不可变的 Seat 对象,因为我们不希望客户端在不使用 TheatherBox bean 的情况下更改对象。

最后,buyTicket 方法是实际购买门票的方法,因此将门票设置为已预订。我们不能更改不可变对象的值,但我们可以用包含另一个值的新对象来替换它。新创建的对象被放置在 hashmap 中,而不是旧的一个。

控制 bean 并发

如您可能已经注意到的,bean 在管理我们的 Seat 对象集合的方法上包含了一个 @Lock 注解。这种类型的注解用于控制单例的并发。

默认情况下,对单例 EJB 的并发访问由容器控制。对单例的读写访问一次仅限于一个客户端。然而,通过使用注解,可以提供更细粒度的并发控制。这可以通过使用 @Lock 注解来实现,其参数决定了允许的并发访问类型。

通过使用类型为 javax.ejb.LockType.READ@Lock 注解,将对 bean 的多线程访问将允许。这在下述代码中显示:

 @Lock(READ)
    public Collection<Seat> getSeats() {
        return Collections.unmodifiableCollection(seats.values());
    }

另一方面,如果我们应用javax.ejb.LockType.WRITE,将强制执行单线程访问策略,如下面的代码所示:

 @Lock(WRITE)
    public void buyTicket(int seatId) throws SeatBookedException, NoSuchSeatException {
        final Seat seat = getSeat(seatId);
        if (seat.isBooked()) {
            throw new SeatBookedException("Seat " + seatId + " already booked!");
        }
        addSeat(seat.getBookedSeat());
    }

通用思路是在只从缓存中读取值的READ类型锁方法上使用READ类型锁,对于更改缓存中元素值的WRITE类型锁方法使用WRITE类型锁。请记住,WRITE类型锁会阻塞所有带有READ类型锁的方法。确保单例对其状态的修改具有独占控制权至关重要。缺乏适当的封装与引用传递语义(在 EJB 的本地和无接口视图中使用)混合可能导致难以发现的并发错误。将不可变对象作为单例的返回值是一种解决这类问题的好策略。另一种策略是只返回我们对象的副本或切换到值传递语义。最后一种策略可以通过在单例中切换到远程业务接口来实现。

TheatreBox代码中,你可能已经注意到了一个@AccessTimeout注解(值为5,单位为TimeUnit.MINUTES)。当你对一个带有@Lock (WRITE)的方法执行查询,并且如果有其他线程正在访问它,那么在等待 5 秒后,你会得到一个超时异常。为了改变这种行为(例如,通过延长允许的等待时间),你可以在方法或类级别指定一个@javax.ejb.AccessTimout注解。

使用 bean 管理的并发

另一个可能的选择是使用由@javax.ejb.ConcurrencyManagement注解(带有参数ConcurrencyManagementType.BEAN)实现的 bean 管理的并发策略。这个注解将禁用我们之前使用的@Lock注解的效果,将确保单例缓存不被损坏的责任放在开发者身上。

因此,为了确保我们的预订得到保留,我们将在buyTicket方法上使用一个众所周知的同步关键字,如下所示:

@Singleton
@Startup
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
public class TheatreBox {
. . . .
  public synchronized void buyTicket(int seatId) {
    final Seat seat = getSeat(seatId);
    if (seat.isBooked()) {
        throw new SeatBookedException("Seat " + seatId + " already booked!");
    }
    addSeat(seat.getBookedSeat());
}

当一个线程进入同步块时,并发访问被限制,因此在此期间不允许其他方法访问该对象。使用同步块相当于拥有容器管理的并发性,所有方法默认使用类型为WRITE的锁。这是 Java EE 中少数几个开发者可以使用同步原语而不影响容器稳定性的地方之一。

编写会话 Bean

我们的单一 EJB 配备了处理我们的剧院座位库存的方法。现在我们将向我们的项目中添加几个会话 Bean 来管理业务逻辑,一个无状态会话 Bean 将提供剧院座位的视图,以及作为我们系统支付网关行为的会话 Bean。

注意

将我们的信息系统拆分为两个不同的 Bean 的选择并不属于特定的设计模式,而是服务于不同的目的。那就是,我们希望展示如何从远程客户端查找这两种类型的 Bean。

添加无状态的 Bean

因此,我们将首先创建的 Bean 是com.packtpub.wflydevelopment.chapter3.boundary.TheatreInfo,它几乎只包含查找剧院座位列表的逻辑。实际上,这个 Bean 充当了我们单例 Bean 的代理,如下面的代码所示:

@Stateless
@Remote(TheatreInfoRemote.class)
public class TheatreInfo implements TheatreInfoRemote {
 @EJB
 private TheatreBox box;

    @Override
    public String printSeatList() {
        final Collection<Seat> seats = box.getSeats();
        final StringBuilder sb = new StringBuilder();
        for (Seat seat : seats) {
            sb.append(seat.toString());
            sb.append(System.lineSeparator());
        }
        return sb.toString();
    }
}

由于我们计划从远程客户端调用这个 EJB,我们使用@Remote(TheatreInfoRemote.class)注解为其定义了一个远程接口。

接下来,看看@EJB TheatreBox box,它可以用来安全地将 EJB 注入到您的类中,而无需手动 JNDI 查找。这种做法可以提高应用程序在不同应用服务器之间的可移植性,在这些服务器中可能存在不同的 JNDI 规则。

您的 Bean 的远程接口将像以下代码一样简单:

public interface TheatreInfoRemote {
    String printSeatList();
}

注意

如果您计划仅将您的 EJB 暴露给本地客户端(例如,给一个 servlet),您可以省略远程接口定义,只需简单地用@Stateless注解您的 Bean 即可。应用服务器将创建一个无接口视图的会话 Bean,它可以安全地注入到您的本地客户端,如 servlet 或其他 EJB。请注意,这也改变了方法参数和返回值的语义。对于远程视图,它们将被序列化并通过值传递。

添加有状态的 Bean

为了跟踪我们的客户口袋里有多少钱,我们需要一个会话感知组件。将一个 Java 类转换成一个有状态的会话 Bean 只需在其上添加一个@Stateful注解,就像我们示例中的com.packtpub.wflydevelopment.chapter3.boundary.TheatreBooker类一样。这在上面的代码中有所展示:

@Stateful
@Remote(TheatreBookerRemote.class)
@AccessTimeout(value = 5, unit = TimeUnit.MINUTES)
public class TheatreBooker implements TheatreBookerRemote {
    private static final Logger logger = Logger.getLogger(TheatreBooker.class);

    @EJB
    private TheatreBox theatreBox;
    private int money;

    @PostConstruct
    public void createCustomer() {
        this.money = 100;
    }

    @Override
    public int getAccountBalance() {
        return money;
    }

    @Override
    public String bookSeat(int seatId) throws SeatBookedException, NotEnoughMoneyException, NoSuchSeatException {
        final int seatPrice = theatreBox.getSeatPrice(seatId);
        if (seatPrice > money) {
            throw new NotEnoughMoneyException("You don't have enough money to buy this " + seatId + " seat!");
        }

 theatreBox.buyTicket(seatId);
        money = money - seatPrice;

        logger.infov("Seat {0} booked.", seatId);
        return "Seat booked.";
    }
}

如您所见,之前的 Bean 带有@PostConstruct注解,用于初始化一个会话变量(money),该变量将用于检查客户是否有足够的钱购买票。在使用 EJB 时,我们不使用构造器析构器来对一个对象执行动作以创建或销毁。原因是该对象可能还没有注入它所依赖的所有对象。带有@PostConstruct注解的方法是在对象创建完成后执行的,也就是说,所有对象都已经注入到它中。还有一个与 EJB 生命周期相关的第二个注解,@PreDestroy,它在对象销毁之前执行。

此外,我们的 SFSB 的最终目的是在执行一些业务检查后调用我们的单例的buyTicket方法。

如果业务检查未通过,应用程序将抛出一些异常。例如,如果座位已经被预订或者如果客户没有足够的钱购买票,就会出现这种情况。为了保持我们的对话进行,我们的异常必须是通用Exception类的扩展。有关更多信息,请参考以下代码:

public class SeatBookedException extends Exception {
  // some code 
}

如果我们使用运行时异常(例如,EJBException),则将丢弃 bean 实例,并且远程客户端与服务器之间的通信将被切断。因此,在处理 EJB 时,始终要小心选择适当的异常类型——如果您处理的是不可恢复的场景(与企业信息系统之间的连接被切断),则选择抛出运行时异常。这种异常被称为系统异常。另一方面,如果您处理的是业务类型的异常;例如,如果预订的座位已经被占用,则考虑抛出检查异常(或者简单地不抛出异常)。可恢复异常被称为应用程序异常。

使用@ApplicationException注解,还可以将运行时异常(通常为系统异常)标记为可恢复异常。您甚至可以使用@ApplicationException(带有rollback true)在异常类上或业务方法内的EJBContext.setRollbackOnly语句中决定当前事务是否应该回滚(系统异常的默认行为)。是否回滚事务的决定权在开发者手中,在大多数情况下,这取决于业务场景。

部署 EJB 应用程序

按照现状,您应该能够通过执行以下 Maven 目标并从项目根目录启动命令行提示符来打包您的 EJB 项目:

mvn package

上述命令将编译并打包需要复制到应用程序服务器deployments文件夹中的应用程序。这是可以的;然而,通过仅安装几个插件,我们可以期待 Maven 提供更多功能。在我们的例子中,我们将通过添加以下部分来配置我们的项目以使用 Maven 的 WildFly 插件:

<build>
 <finalName>${project.artifactId}</finalName>
    <plugins>
 <!-- WildFly plugin to deploy the application -->
 <plugin>
 <groupId>org.wildfly.plugins</groupId>
 <artifactId>wildfly-maven-plugin</artifactId>
 <version>1.0.2.Final</version>
 <configuration>
 <filename>${project.build.finalName}.jar</filename>
 </configuration>
 </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
                <!-- enforce Java 8 -->
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-ejb-plugin</artifactId>
            <version>2.3</version>
            <configuration>
                <ejbVersion>3.2</ejbVersion>
                <!-- Generate ejb-client for client project -->
                <generateClient>true</generateClient>
            </configuration>
        </plugin>
    </plugins>
</build>

在 XML 片段的第一部分,我们指定了项目的finalName属性,这将决定打包构件的名称(在我们的例子中,项目的名称与我们的项目构件 ID 相对应,因此它将被命名为ticket-agency-ejb.jar)。

命名为wildfly-maven-plugin的构件 ID 将实际触发用于部署我们项目的 WildFly Maven 插件。

因此,一旦您配置了 WildFly 插件,您就可以通过从项目根目录进入来自动部署应用程序。这可以通过在控制台中键入以下命令来完成:

mvn wildfly:deploy

由于部署对于开发者来说是一项重复性任务,因此从 Eclipse 环境中执行此操作将非常方便。您只需要从菜单中选择运行 | 运行配置来创建一个新的运行配置设置。

进入项目的基目录(提示:浏览工作区...实用程序可以帮助您从项目列表中选择项目)并在目标文本框中输入您的 Maven 目标,如下面的屏幕截图所示:

部署 EJB 应用程序

完成此操作后,请确保您的 WildFly 实例正在运行。单击应用以保存您的配置,然后单击运行以执行应用程序的部署。Maven 插件将激活,一旦验证所有类都已更新,它将开始使用远程 API 将应用程序部署到 WildFly。请注意,您不需要传递任何用户名或密码进行部署。这是因为您是从安装 WildFly 的同一台机器上部署应用程序的。在幕后进行本地用户身份验证,这样程序员就不需要在他们的开发机器上处理这个问题。

在执行命令后,你应该在 Maven 控制台上看到成功消息,如下面的代码所示:

INFO: JBoss Remoting version 4.0.3.Final
[INFO] ---------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ---------------------------------------------------------------

另一方面,在 WildFly 控制台上,您会看到相当冗长的输出,指出一些重要的 EJB JNDI 绑定(我们将在下一分钟回到它),并告知我们应用程序已正确部署。如下面的代码所示:

09:09:32,782 INFO  [org.jboss.as.server] (management-handler-thread - 1) JBAS018562: Deployed "ticket-agency-ejb.jar"

尽管我们正在开发 WildFly,但我们经常可以在控制台上看到来自 JBoss AS 子系统的信息。这是因为 WildFly 是直接基于 JBoss AS 7 代码库构建的,因此无需担心。

创建远程 EJB 客户端

为 WildFly 应用程序服务器创建远程 EJB 客户端与 AS7 非常相似。在 AS6 和较新版本之间可以明显看出差异。

实际上,WildFly 的早期版本(7.x 之前的 JBoss AS 版本)使用 JBoss 命名项目作为 JNDI 命名实现,因此开发者熟悉使用jnp:// PROVIDER_URL与应用程序服务器通信。

从 AS7 开始,JNP 项目不再使用——无论是在服务器端还是客户端。JNP 项目的客户端现在已被 jboss-remote-naming 项目取代。JNP 客户端被 jboss-remote-naming 项目取代的原因有很多,其中之一是 JNP 项目在与 JNDI 服务器通信时不允许进行细粒度的安全配置。jboss-remote-naming 项目由 jboss-remoting 项目支持,这允许对安全进行更多和更好的控制。

除了 AS7 和 WildFly 中的新命名实现外,不再支持将自定义 JNDI 名称绑定到 EJB。因此,这些豆总是绑定到规范要求的java:globaljava:appjava:module命名空间。因此,通过注解或配置文件设置会话豆元素的 JNDI 名称不再受支持。

那么,将使用什么 JNDI 名称来调用无状态的会话豆?这里就是:

ejb:<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>

这有点啰嗦,不是吗?然而,以下表格将帮助你理解:

元素 描述
app-name 这是企业应用程序名称(不带ear),如果你的 EJB 被打包在一个 EAR 中
module-name 这是模块名称(不带.jar.war),你的 EJB 已经被打包在这个模块中
distinct-name 使用此选项,你可以为每个部署单元设置一个不同的名称
bean-name 这是豆的类名
fully-qualified-classname-of-the-remote-interface 这是远程接口的完全限定类名

因此,你的TheatreInfo EJB对应的 JNDI 绑定,打包在一个名为ticket-agency-ejb.jar的文件中,将是:

ejb:/ticket-agency-ejb//TheatreInfo! com.packtpub.wflydevelopment.chapter3.boundary.TheatreInfoRemote

另一方面,有状态的 EJB 将在 JNDI 字符串底部包含一个额外的属性,?stateful;这将导致以下 JNDI 命名结构:

ejb:<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>?stateful

此外,这里还有TheatreBooker类的对应绑定:

ejb:/ticket-agency-ejb//TheatreBooker! com.packtpub.wflydevelopment.chapter3.boundary.TheatreBookerRemote?stateful

注意

如果你注意服务器日志,你将看到一旦你的应用程序部署,一组 JNDI 绑定将在服务器控制台上显示。例如:

java:global/ticket-agency-ejb/TheatreInfo!com.packtpub.wflydevelopment.chapter3.boundary.TheatreInfoRemote
java:app/ticket-agency-ejb/TheatreInfo!com.packtpub.wflydevelopment.chapter3.boundary.TheatreInfoRemote
java:module/TheatreInfo!com.packtpub.wflydevelopment.chapter3.boundary.TheatreInfoRemote
java:jboss/exported/ticket-agency-ejb/TheatreInfo!com.packtpub.wflydevelopment.chapter3.boundary.TheatreInfoRemote

一些这些绑定反映了 Java EE 规范的标准绑定以及 JBoss 自定义绑定(java:/jboss)。这些信息本身对我们来说并不相关,但可以通过用ejb:/替换 Java EE(或 JBoss 特定前缀)来构建我们的 EJB 客户端查找字符串。例如,将java:/global替换为ejb:,这样你就可以避免引用 EJB 查找字符串的麻烦。

一旦我们完成了 JNDI 绑定字符串的解码,我们就会编写我们的 EJB 客户端。我们已经在本章开头为它创建了一个单独的子项目(ticket-agency-ejb-client),但我们在开始编码之前必须完成其配置。

配置客户端的项目对象模块

配置客户端依赖项(在pom.xml中)基本上需要连接和传输数据到服务器的所有库,以及所需的 EJB 客户端依赖项。我们将添加的第一件事,就像我们为服务器项目所做的那样,是 EJB 客户端依赖项的 BOM,如下代码片段所示:

<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>org.wildfly</groupId>
         <artifactId>wildfly-ejb-client-bom</artifactId>
         <version>8.1.0.Final</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
   </dependencies>
</dependencyManagement>

接下来,我们将添加一组必要的依赖项,用于解析 EJB 接口(ticket-agency-ejb artifact),JBoss 的交易 API(因为 EJB 是具有交易感知能力的组件,所以需要),jboss-ejb-apiejb-client API,org.jboss.xnioorg.jboss.xnio API(提供低级输入/输出实现),org.jboss.remoting3 API(核心传输协议),它反过来又需要 org.jboss.sasl(以安全传输),最后是 org.jboss.marshalling API(用于序列化发送到和从服务器接收的对象)。这在上面的代码片段中显示:

<dependencies>
   <dependency>
      <groupId>com.packtpub.wflydevelopment.chapter3</groupId>
      <artifactId>ticket-agency-ejb</artifactId>
      <type>ejb-client</type>
      <version>${project.version}</version>
   </dependency>

   <dependency>
      <groupId>org.jboss.spec.javax.transaction</groupId>
      <artifactId>jboss-transaction-api_1.2_spec</artifactId>
      <scope>runtime</scope>
   </dependency>

   <dependency>
      <groupId>org.jboss.spec.javax.ejb</groupId>
      <artifactId>jboss-ejb-api_3.2_spec</artifactId>
      <scope>runtime</scope>
   </dependency>

   <dependency>
      <groupId>org.jboss</groupId>
      <artifactId>jboss-ejb-client</artifactId>
      <scope>runtime</scope>
   </dependency>

   <dependency>
      <groupId>org.jboss.xnio</groupId>
      <artifactId>xnio-api</artifactId>
      <scope>runtime</scope>
   </dependency>
   <dependency>
      <groupId>org.jboss.xnio</groupId>
      <artifactId>xnio-nio</artifactId>
      <scope>runtime</scope>
   </dependency>

   <dependency>
      <groupId>org.jboss.remoting3</groupId>
      <artifactId>jboss-remoting</artifactId>
         <version>3.3.3.Final</version>
         <scope>runtime</scope>
   </dependency>

   <dependency>
      <groupId>org.jboss.sasl</groupId>
      <artifactId>jboss-sasl</artifactId>
      <scope>runtime</scope>
   </dependency>

   <dependency>
      <groupId>org.jboss.marshalling</groupId>
      <artifactId>jboss-marshalling-river</artifactId>
      <scope>runtime</scope>
   </dependency>
</dependencies>

许多这些依赖项使用运行时作用域。这意味着它们提供的类不是直接由我们的代码使用;它们不需要包含在我们的应用程序包中,但在运行时是必需的。

编写 EJB 客户端

我们完成了配置。我们最终将添加一个新的 Java 类 com.packtpub.wflydevelopment.chapter3.client.TicketAgencyClient,它将与票务预订机的 EJB 应用程序进行通信。这在上面的代码片段中显示:

public class TicketAgencyClient {

    private static final Logger logger = Logger.getLogger(TicketAgencyClient.class.getName());

    public static void main(String[] args) throws Exception {
 Logger.getLogger("org.jboss").setLevel(Level.SEVERE);  [1]
        Logger.getLogger("org.xnio").setLevel(Level.SEVERE);

        new TicketAgencyClient().run();
    }
    private final Context context;
    private TheatreInfoRemote theatreInfo;
    private TheatreBookerRemote theatreBooker;

    public TicketAgencyClient() throws NamingException {
 final Properties jndiProperties = new Properties(); [2]
        jndiProperties.setProperty(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
        this.context = new InitialContext(jndiProperties);
    }

    private enum Command { [3]
        BOOK, LIST, MONEY, QUIT, INVALID;

        public static Command parseCommand(String stringCommand) {
            try {
                return valueOf(stringCommand.trim().toUpperCase());
            } catch (IllegalArgumentException iae) {
                return INVALID;
            }
        }
    }

    private void run() throws NamingException {
 this.theatreInfo = lookupTheatreInfoEJB();  [4]
 this.theatreBooker = lookupTheatreBookerEJB();  [5]

 showWelcomeMessage(); [6]

        while (true) {
            final String stringCommand = IOUtils.readLine("> ");
 final Command command = Command.parseCommand(stringCommand); [7]
            switch (command) {
                case BOOK:
                    handleBook();
                    break;
                case LIST:
                    handleList();
                    break;
                case MONEY:
                    handleMoney();
                    break;
                case QUIT:
                    handleQuit();
                    break;

                default:
                    logger.warning("Unknown command " + stringCommand);
            }
        }
    }

    private void handleBook() {
        int seatId;

        try {
            seatId = IOUtils.readInt("Enter SeatId: ");
        } catch (NumberFormatException e1) {
            logger.warning("Wrong SeatId format!");
            return;
        }

        try {
            final String retVal = theatreBooker.bookSeat(seatId);
            System.out.println(retVal);
        } catch (SeatBookedException | NotEnoughMoneyException | NoSuchSeatException e) {
            logger.warning(e.getMessage());
            return;
        }
    }

    private void handleList() {
        logger.info(theatreInfo.printSeatList());
    }

    private void handleMoney() {
        final int accountBalance = theatreBooker.getAccountBalance();
        logger.info("You have: " + accountBalance + " money left.");
    }

    private void handleQuit() {
        logger.info("Bye");
        System.exit(0);
    }
    private TheatreInfoRemote lookupTheatreInfoEJB() throws NamingException {
        return (TheatreInfoRemote) context.lookup("ejb:/ticket-agency-ejb//TheatreInfo!com.packtpub.wflydevelopment.chapter3.boundary.TheatreInfoRemote");
    }

    private TheatreBookerRemote lookupTheatreBookerEJB() throws NamingException {
        return (TheatreBookerRemote) context.lookup("ejb:/ticket-agency-ejb//TheatreBooker!com.packtpub.wflydevelopment.chapter3.boundary.TheatreBookerRemote?stateful");
    }

    private void showWelcomeMessage() {
        System.out.println("Theatre booking system");
        System.out.println("=====================================");
        System.out.println("Commands: book, list,money, quit");
    }
}
jboss-ejb-client.properties file in the client's classpath.

注意

在 Maven 中,大多数资源文件(如提到的属性)的适当位置是 src/main/resources 目录。

jboss-ejb-client.properties 文件的内容如下:

remote.connections=default
remote.connection.default.host=localhost
remote.connection.default.port=8080

还有一个 remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED 属性,它启用了 XNIO 连接的加密;否则,将使用明文。(第十章,保护 WildFly 应用程序,我们将讨论使用 SSL 保护客户端和服务器之间的连接。)

可以将 remote.connections 属性设置为定义一组逻辑名称,这些名称将由 remote.connection.[name].hostremote.connection.[name].port 属性用于连接目的。如果您定义了多个连接,如以下示例所示,连接将分布在各种目的地之间,如以下代码片段所示:

remote.connections=host1,host2 
remote.connection.host1.host=192.168.0.1
remote.connection.host2.host=192.168.0.2
remote.connection.host1.port=8080
remote.connection.host2.port=8080

远程框架使用的默认端口是 8080

您可能会想知道 EJB 远程如何在与 HTTP 协议相同的端口上工作。从 WildFly 开始,远程使用 HTTP 协议升级机制。第一个连接是在 8080 端口(通过 HTTP)完成的,然后升级到 EJB 远程,并切换到另一个端口(由 WildFly 选择)。

运行客户端应用程序

为了运行您的客户端应用程序,最后一个要求将是添加所需的 Maven 插件,这些插件是运行远程 EJB 客户端所必需的。这在上面的代码片段中给出:

<build>
   <finalName>${project.artifactId}</finalName>
   <plugins>
      <!-- maven-compiler-plugin here -->

      <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>exec-maven-plugin</artifactId>
         <version>1.2.1</version>
         <executions>
            <execution>
               <goals>
                  <goal>exec</goal>
               </goals>
            </execution>
         </executions>
         <configuration>
            <executable>java</executable>
            <workingDirectory>${project.build.directory}/exec-working-directory</workingDirectory>
            <arguments>
               <argument>-classpath</argument>
               <classpath />
            <argument>com.packtpub.wflydevelopment.chapter3.client.TicketAgencyClient</argument>
            </arguments>
         </configuration>
      </plugin>
   </plugins>
</build>
maven-compiler-plugin configuration that we omitted for the sake of brevity (we discussed it in the server project), we have included exec-maven-plugin, which adds the ability to execute Java programs using the exec goal.

一旦所有插件都到位,您可以通过以下 Maven 目标编译和执行您的项目:

mvn package exec:exec

前一个命令可以从 shell(位于项目的root文件夹中)或从您的 Eclipse 运行时配置中执行,如下面的截图所示:

运行客户端应用程序

如果从 Eclipse 环境中执行,您应该能够看到以下 GUI 截图:

运行客户端应用程序

目前,我们的应用程序提供了三个功能:一本书来预订座位,一个列表来列出所有剧院座位,以及金钱来检索账户余额。在下一节中,我们将通过添加更多命令来丰富我们的应用程序。

添加用户身份验证

如果您从位于应用程序服务器同一台机器上的客户端运行此示例,远程框架将静默允许客户端和您的 EJB 类之间的通信。另一方面,对于位于远程系统上的客户端,您将需要为您的请求提供身份验证。为了添加应用程序用户,启动位于JBOSS_HOME/binadd-user.sh(或add-user.bat)脚本。

这里是一个用户创建示例的记录:

What type of user do you wish to add?
 a) Management User (mgmt-users.properties)
 b) Application User (application-users.properties)
(a): b

Enter the details of the new user to add.
Using realm 'ApplicationRealm' as discovered from the existing property files.
Username : ejbuser
Password requirements are listed below. To modify these restrictions edit the add-user.properties configuration file.
 - The password must not be one of the following restricted values {root, admin, administrator}
 - The password must contain at least 8 characters, 1 alphanumeric character(s), 1 digit(s), 1 non-alphanumeric symbol(s)
 - The password must be different from the username
Password :
Re-enter Password :
What groups do you want this user to belong to? (Please enter a comma separated list, or leave blank for none)[  ]:
About to add user 'ejbuser' for realm 'ApplicationRealm'
Is this correct yes/no? yes
Added user 'ejbuser' to file 'C:\Programs\Dev\Servers\wildfly-8.0.0.Final\standalone\configuration\application-users.properties'
Added user 'ejbuser' to file 'C:\Programs\Dev\Servers\wildfly-8.0.0.Final\domain\configuration\application-users.properties'
Added user 'ejbuser' with groups  to file 'C:\Programs\Dev\Servers\wildfly-8.0.0.Final\standalone\configuration\application-roles.properties'
Added user 'ejbuser' with groups  to file 'C:\Programs\Dev\Servers\wildfly-8.0.0.Final\domain\configuration\application-roles.properties'
Is this new user going to be used for one AS process to connect to another AS process?
e.g. for a slave host controller connecting to the master or for a Remoting connection for server to server EJB calls.
yes/no? no
Press any key to continue . . .

定义的用户将自动添加到位于您的configuration文件夹中的application-user.properties文件中。

此文件包含名为ApplicationRealm的默认安全域。此安全域使用以下格式存储密码:

username=HEX( MD5( username ':' realm ':' password))

使用您刚刚输入的密码,文件将包含以下条目:

ejbuser=dc86450aab573bd2a8308ea69bcb8ba9

现在,将用户名和密码信息插入到jboss-ejb-client.properties文件中:

remote.connection.default.username=ejbuser
remote.connection.default.password=ejbuser123

现在,所有先前信息都已放置在正确的位置,您将能够从不在与服务器同一台机器上的客户端连接到您的 EJB 应用程序。

您还可以通过在本地机器上添加以下行到jboss-ejb-client属性中,强制执行正常的身份验证过程:

remote.connection.default.connect.options.org.xnio.Options.SASL_DISALLOWED_MECHANISMS=JBOSS-LOCAL-USER

使用 EJB 计时器服务

模拟业务工作流程的应用程序通常依赖于定时通知。企业 Bean 容器中的计时器服务允许您为所有类型的企业 Bean(除了有状态的会话 Bean)安排定时通知。您可以根据日历计划在特定时间、时间段的持续时间后或定时间隔发生定时通知。

EJB 计时器主要有两种类型:编程式计时器和自动计时器。编程式计时器是通过显式调用TimerService接口的计时器创建方法来设置的。自动计时器是在企业 Bean 成功部署时创建的,该 Bean 包含带有java.ejb.Schedulejava.ejb.Schedules注解的方法。让我们在以下章节中查看这两种方法。

编程式计时器创建

要创建一个计时器,Bean 将调用TimerService接口的create方法之一。这些方法允许创建单次动作、间隔或基于日历的计时器。

获取TimerService实例的最简单方法是使用资源注入。例如,在TheatreBox单例 EJB 中,我们将使用@Resource注解注入一个TimerService对象,如下面的代码片段所示:

@Resource
TimerService timerService;

private static final long DURATION = TimeUnit.SECONDS.toMillis(6);

持续时间指定了单次计时器触发的时间(以毫秒为单位)。将触发计时器的那个方法将使用TimerService实例来调用createSingleActionTimer方法,并将持续时间以及TimerConfig类的实例作为参数传递,该实例可以包含一些基本信息(例如计时器的描述)。这在上面的代码片段中有所展示:

public void createTimer(){
    timerService.createSingleActionTimer(DURATION, new TimerConfig());
}

接下来,我们将创建一个名为timeout的回调方法,并在方法上方使用@Timeout注解。在timeout方法中,例如,我们可以通过调用setupTheatre方法来重新初始化我们的单例。没有什么花哨的;然而,这应该能给你一个如何使用单次动作计时器的想法。有关更多信息,请参考以下代码:

@Timeout
public void timeout(Timer timer){
    logger.info("Re-building Theatre Map."); 
    setupTheatre();
}

计划计时器事件

 explains this:
@Stateless
public class AutomaticSellerService {

    private static final Logger logger = Logger.getLogger(AutomaticSellerService.class);

    @EJB
    private TheatreBox theatreBox;

 @Resource
 private TimerService timerService;  [1]

 @Schedule(hour = "*", minute = "*/1", persistent = false)  [2]
    public void automaticCustomer() throws NoSuchSeatException {
        final Optional<Seat> seatOptional = findFreeSeat();
        if (!seatOptional.isPresent()) {
            cancelTimers();
            logger.info("Scheduler gone!");
            return; // No more seats
        }

        final Seat seat = seatOptional.get();

        try {
 theatreBox.buyTicket(seat.getId());   [3]
        } catch (SeatBookedException e) {
            // do nothing, user booked this seat in the meantime
        }

        logger.info("Somebody just booked seat number " + seat.getId());
    }

    private Optional<Seat> findFreeSeat() {
        final Collection<Seat> list = theatreBox.getSeats();
        return list.stream()
            .filter(seat -> !seat.isBooked())
            .findFirst();
    }
    private void cancelTimers() {  [4]
        for (Timer timer : timerService.getTimers()) {
 timer.cancel();
        }
    }
}

我们首先应该考虑的是Timer对象的资源注入[1],它将在TheatreBox单例的cancelTimers方法[4]中用于取消所有调度,当剧院完全订满时。请注意,timerService.getTimers()方法检索仅与当前 Bean 关联的所有活动计时器。为了从您的应用程序模块中获取所有计时器,您必须使用最近在 EJB 3.2 中添加的timerService.getAllTimers()方法。

接下来,请注意我们使用的Schedule注解[2],它将每分钟触发一个非持久计时器。

注意

持久计时器(默认选项)可以在应用程序和服务器崩溃后存活。当系统恢复时,任何持久计时器都将被重新创建,并且丢失的回调事件将被执行。

当不希望重放丢失的计时器事件时,应使用非持久计时器,如前例所示。

当一个动作被触发时,automaticCustomer方法开始扫描剧院座位以寻找一个可用的座位。(没有什么太复杂的;findSeat从第一个可用的座位开始查找。)

最后,如果还有座位可用,TheatreBox单例的buyTicket方法[3]将被用来短路购买座位(显然,我们不需要检查自动客户的金钱)。

将异步方法添加到我们的 EJB 中

在 EJB 3.1 规范之前,向企业应用提供异步功能的方法是使用消息驱动 bean 配方。这仍然是一个最佳实践,我们将在第六章中深入讨论,使用 JBoss JMS 提供程序开发应用程序;然而,在某些情况下,可能希望(并且更容易)从遵循经典请求-响应模式的组件中使用这些异步功能。

您可以通过简单地使用@Asynchronous注解标记 EJB 的方法来使其异步。每次调用此方法时,它将立即返回,无论该方法实际上需要多长时间才能完成。

这可以通过两种方式使用:

  • 第一种技术是一种“点火并忘记”的方式,其中请求由 EJB 发起,客户端不关心请求的成功或失败。

  • 第二种操作方式调用方法但不等待方法完成。方法返回一个Future对象。该对象用于稍后确定请求的结果。

使用“点火并忘记”的异步调用

如果您不关心异步结果,您的async方法可以简单地返回 void。为此,我们将向TheatreBooker添加一个名为bookSeatAsync的新方法,并简单地将其标记为@Asynchronous。这在下述屏幕截图中显示:

 @Asynchronous
 public void bookSeatAsync(int seatId) throws NotEnoughMoneyException, NoSuchSeatException, SeatBookedException {
        bookSeat(seatId);
}

如您所见,这种方法不返回任何内容;它只是执行我们的同步bookSeet方法。我们需要使用其他工具来检查交易是否成功完成。例如,我们可以从剧院列表中检查座位是否已成功预订。

向客户端返回 Future 对象

另一个可用的选项是返回一个java.util.concurrent.Future对象,该对象可以稍后由我们的客户端检查,以便他们知道交易的结果。这在下述代码片段中显示:

@Asynchronous
@Override
public Future<String> bookSeatAsync(int seatId) {
        try {
            Thread.sleep(10000);
            bookSeat(seatId);
            return new AsyncResult<>("Booked seat: " + seatId + ". Money left: " + money);
        } catch (NoSuchSeatException | SeatBookedException | NotEnoughMoneyException | InterruptedException e) {
            return new AsyncResult<>(e.getMessage());
        }
    }

在这种情况下,对异步bookSeatAsync方法的调用在幕后简单地导致创建一个RunnableCallable Java对象,该对象封装了您提供的方法和参数。这个Runnable(或可调用)对象被交给一个Executor对象,它只是一个附加到线程池的工作队列。

在将工作添加到队列后,方法的代理版本返回一个与Runnable相关联的 Future 实现,该Runnable现在正在队列中等待。

Runnable最终执行bookSeatAsync方法时,它将返回值设置到Future中,使其对调用者可用。

当处理Future对象时,客户端代码需要做出调整。实际上,在标准的同步调用中,我们使用异常来拦截一些事件,例如当客户没有足够的钱来完成交易时。当使用Future调用时,这种范式发生了变化。对异步方法的调用与客户端分离;然而,我们有选项通过在 Future 返回值上发出isDone方法来检查Future工作是否已完成。

为了这个目的,让我们向TicketAgencyClient添加一个bookasync命令,该命令将执行异步预订,并模拟通过电子邮件读取结果的邮件命令,如下面的代码片段所示:

private final List<Future<String>> lastBookings = new ArrayList<>(); [1]
 // Some code
    case BOOKASYNC:
 handleBookAsync();
 break;
 case MAIL:
 handleMail();
 break; 
// Some code
private void handleBookAsync() {
    String text = IOUtils.readLine("Enter SeatId: ");
    int seatId;

    try {
        seatId = Integer.parseInt(text);
    } catch (NumberFormatException e1) {
        logger.warning("Wrong seatId format!");
        return;
    }

 lastBookings.add(theatreBooker.bookSeatAsync(seatId));  [2]
    logger.info("Booking issued. Verify your mail!");
}

private void handleMail() {
    boolean displayed = false;
    final List<Future<String>> notFinished = new ArrayList<>();
    for (Future<String> booking : lastBookings) {
 if (booking.isDone()) {  [3]
            try {
                final String result = booking.get();
                logger.info("Mail received: " + result);
                displayed = true;
            } catch (InterruptedException | ExecutionException e) {
                logger.warning(e.getMessage());
            }
        } else {
            notFinished.add(booking);
        }
    }

    lastBookings.retainAll(notFinished);
    if (!displayed) {
        logger.info("No mail received!");
    }
}
booking [2] and add Future<?> to lastBookings list [1]. On the EJB side, we introduced a pause of 10 seconds to complete the booking so that later on, we can check if the work has been completed by checking the isDone method [3] of the lastBookings list elements object.

下面是我们更丰富的客户端应用程序的截图:

返回给客户端的 Future 对象

摘要

在本章中,我们通过一个简单的实验室示例,逐步丰富了 EJB 基础知识以及 EJB 3.2 的变化。这个示例展示了如何在 Eclipse 环境中使用 Maven 项目来协助你组装包含所有必要依赖项的项目。

到目前为止,我们只为我们的应用程序编写了一个远程独立客户端。在下一章中,我们将看到如何使用上下文和依赖注入将 Web 前端添加到我们的示例中,以弥合 Web 层和企业层之间的差距。

第四章。学习上下文和依赖注入

我们看到,第三章,“介绍 Java EE 7 – EJBs”,由于我们必须涵盖大量的内容,包括 Java 企业增强和 Maven 特定的配置,因此具有挑战性。在本章中,我们将讨论上下文和依赖注入CDI),它在 Java EE 6(从 JSR 299 开始)中添加到 Java EE 规范中。它为 Java EE 开发者提供了许多缺失的好处,例如允许任何 JavaBean 作为 JSF 管理 Bean 使用,包括无状态和有状态会话 Bean。你可以在www.cdi-spec.org/上找到有关 CDI 和规范最新版本(JSR 346)的更多信息。

本章将涵盖的一些主题如下:

  • 上下文和依赖注入是什么以及它与 EJB 的关系

  • 如何重写我们的票务预订示例以使用 CDI 和 JavaServer Faces 技术

  • 如何使用 Maven 运行项目

本章假设读者熟悉JavaServer FacesJSF),它将被用来为我们提供图形界面。如果你正在寻找 JSF 的入门指南,网上有多个优秀的资源,包括官方 Java EE 7 教程中的相关部分docs.oracle.com/javaee/7/tutorial/doc/jsf-develop.htm#BNATX

介绍上下文和依赖注入

CDI(上下文和依赖注入)对于 Java EE 平台引入了一套标准的组件管理服务。作为 Java EE 7 的一个组成部分,CDI 在许多方面是对 Spring 中长时间酝酿的概念的标准化,例如依赖注入和拦截器。实际上,CDI 和 Spring 3 共享许多相似的功能。对于开发者来说,也有其他更轻量级且在 Java SE 环境中更容易使用的依赖注入框架。Google Guice (github.com/google/guice) 是一个显著的例子。为独立的 Java SE 应用程序提供对 CDI 容器的全面支持,并从应用服务器中分离出来,是即将到来的 CDI 2.0 规范的目标之一。这将允许开发者在使用客户端和服务器两端的通用编程模型。

CDI 通过所谓的松耦合和强类型来解耦关注点。这样做,它几乎提供了一种从日常 Java 编程的平凡中解放出来的感觉,允许注入其对象并控制它们的生命周期。

小贴士

为什么 CDI 对 Java EE 是必需的?

如果你使用 Java EE 5 进行编程,你可能会争辩说它已经具有资源注入的功能。然而,这种注入只能用于容器已知的资源(例如,@EJB@PersistenceContext@PersistenceUnit@Resource)。另一方面,CDI 提供了一种通用的依赖注入方案,它可以用于任何组件。

CDI 的基本单元仍然是 Bean。与 EJB 相比,CDI 具有不同、更灵活的 Bean 类型,这通常是放置业务逻辑的好地方。这两种方法之间最重要的区别之一是 CDI Bean 是上下文相关的;也就是说,它们存在于一个定义良好的作用域中。

考虑以下代码片段:

public class HelloServlet extends HttpServlet {

    @EJB
    private EJBSample ejb;

    public void doGet (HttpServletRequestreq,
                       HttpServletResponse res)
                throws ServletException, IOException {
        try(PrintWriter out = res.getWriter()) {
            out.println(ejb.greet());
        }
    }
}

在这里,注入的 EJB 代理(让我们假设它是一个带有@Stateless注解的 POJO 类)仅指向一个无状态实例池(或对于有状态的 Bean,是一个单个 Bean 实例)。HTTP 请求或 HTTP 会话与给定的 EJB 实例之间没有自动关联。

对于 CDI Bean 来说,情况正好相反,它们存在于定义良好的作用域中。例如,以下 CDI Bean 存在于RequestScoped中;也就是说,它将在请求结束时被销毁:

@RequestScoped
public class Customer {

    private String name;
    private String surname;

    public String getName(){
        return name;
    }

    public String getSurname(){
        return surname;
    }
}

前面的 CDI Bean 可以安全地注入到我们之前的 servlet 中;在 HTTP 会话或 HTTP 请求结束时,所有与这个作用域相关的实例都会自动销毁,从而进行垃圾回收:

public class HelloServlet extends HttpServlet {

    @Inject
    private Customer customer;

    public void doGet (HttpServletRequest req,
                       HttpServletResponse res)
                throws ServletException, IOException {
        // some code
    }
}

命名 Bean

在前面的章节中,我们遇到了@Named注解。命名 Bean 允许我们轻松地将我们的 Bean 注入到依赖它们的其他类中,并通过统一表达式语言UEL)从 JSF 页面中引用它们。回想一下前面的例子:

@RequestScoped
@Named 
public class Customer {

    private String name;
    private String surname;

    public String getName(){
        return name;
    }

    public String getSurname(){
        return surname;
    }
}

这个类,被@Named注解装饰,然后可以从 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:body>
      <h:form>
         <h:panelGrid columns="2">
            <h:outputLabel for="name" value="Name" />
            <h:inputText id="name" value="#{customer.name}" />
            <h:outputLabel for="lastName" value="Surname" />
            <h:inputText id="surname" value="#{customer.surname}" />
            <h:panelGroup />
         </h:panelGrid>
      </h:form>
   </h:body>
</html>

注意

默认情况下,Bean 的名称将是类名,其首字母转换为小写;因此,Customer Bean 可以被称为customer

如果你想为你的 Bean 使用不同的命名策略,你可以像下面这样使用@Named注解:

@Named(value="customNamed")

这样,我们将能够使用指定的customNamed值来引用我们的 CDI Bean。

我们可以不用两个@RequestScoped@Named注解,而只需使用聚合它们的@Model注解。

CDI 作用域

CDI Bean 附带一组预定义的作用域和注解,每个 CDI Bean 都有由其所属的作用域确定的独特生命周期。以下表格描述了内置的 CDI 作用域和设置这些作用域所需的注解:

作用域 描述
@RequestScoped @RequestScoped Bean 在单个请求的长度内共享。这可能是一个 HTTP 请求、远程 EJB 调用、Web 服务调用,或者发送到消息驱动 Bean(MDB)的消息。这些 Bean 在请求结束时被销毁。
@ConversationScoped @ConversationScoped作用域的 Bean 在同一个 HTTP 会话中的多个请求间共享,但仅当存在一个活跃的会话时。通过javax.enterprise.context.Conversation Bean 支持 JSF 请求的会话。
@SessionScoped @SessionScoped作用域的 Bean 在同一个 HTTP 会话中的所有请求间共享,并在会话销毁时被销毁。
@ApplicationScoped @ApplicationScoped作用域的 Bean 将在应用程序运行期间存在,并在应用程序关闭时被销毁。
@Dependent @Dependent作用域的 Bean 永远不会在注入点之间共享。任何对依赖 Bean 的注入都是一个新实例,其生命周期绑定到被注入的对象的生命周期。

其他 Java EE 部分可以扩展可用作用域的列表。在 Java EE 7(Java 事务 API 规范)中,引入了一个新的作用域:@TransactionScoped。它将 Bean 的生命周期与当前事务绑定。当然,也可以引入自己的自定义作用域。

在本章的示例中,我们将使用RequestScopedSessionScoped Bean 来驱动我们的简单票务预订系统。在下一章中,我们将使用ConversationScoped Bean 进一步增强我们的示例,这些 Bean 是 CDI Bean 的一个特殊作用域。对所有命名 Bean 作用域的详细解释超出了本书的范围。然而,您可以通过查看 CDI 参考实现(JBoss Weld)文档来满足您对知识的渴望,文档地址为docs.jboss.org/weld/reference/latest/en-US/html/scopescontexts.html

WildFly CDI 实现

Weld 是 CDI 参考实现,最初作为 Seam 3 项目的一部分(www.seamframework.org/)。Weld 提供了一个完整的 CDI 实现,它可以成为 Java EE 7 容器(如 WildFly)的一部分。

因此,为了在 WildFly 上运行基于 CDI 的应用程序,您不需要下载任何额外的库,因为 Weld 是服务器模块的一部分,并且如以下扩展所述,它包含在所有服务器配置中。

<extension module="org.jboss.as.weld"/>

然而,安装了您的模块并不意味着您可以在应用程序中盲目使用它。一般规则是,在 WildFly 上,每个应用程序模块都是与其他模块隔离的;这意味着默认情况下,它对 AS 模块没有可见性,AS 模块对应用程序也没有可见性。

为了准确起见,我们可以将所有 WildFly 模块分为以下三个类别:

  • 隐式添加到您的应用程序中的模块:此类别包括最常用的 API,如javax.activationjavax.annotationjavax.securityjavax.transactionjavax.jmsjavax.xml。使用这些模块不需要额外的工作,因为如果您的应用程序中引用了它们,WildFly 会自动为您添加它们。

  • 根据条件添加的模块:此类别包括javax.ejborg.jboss.resteasyorg.hibernateorg.jboss.as.web以及最后的org.jboss.as.weld。所有这些模块将在您提供其核心注解(例如 EJB 的@Stateless)或其核心配置文件(例如 Web 应用程序的web.xml)的条件下添加。

  • 需要由应用程序部署者显式启用的模块:这包括所有其他模块,例如您的自定义模块,您可以将其添加到应用程序服务器中。允许您查看这些模块的最简单方法是向您的META-INF/MANIFEST.MF文件添加一个显式依赖项。例如,如果您想触发log4j依赖项,您必须按照以下方式编写您的清单文件:

    Dependencies: org.apache.log4j
    

此外,还有一个自定义描述符文件可用,WildFly 使用该文件来解析依赖关系——jboss-deployment-structure.xml。它允许开发者以细粒度的方式配置所需的依赖项。该文件位于顶级部署文件中的META-INF目录(或 Web 存档的WEB-INF目录)。XML 文件的示例内容(以及 XSD 模式)可在docs.jboss.org/author/display/WFLY8/Class+Loading+in+WildFly找到。

因此,如果您仔细遵循了我们的清单,您将知道为了让 Weld 库启动并自动发现您的 CDI Bean,您应该添加其核心配置文件,即beans.xml。此文件可以放置在您的应用程序中的以下位置:

  • 如果您正在开发 Web 应用程序,请将其放置在WEB-INF文件夹中

  • 如果您正在部署 JAR 存档,请将其放置在META-INF文件夹中

beans.xml文件基于以下模式引用:

 <beans 

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

然而,在正确的位置放置一个空的beans.xml文件是完全合法的;如果您这样做,CDI 将在您的应用程序中启用。如果您没有放置beans.xml文件,那么只有被标注为 Bean 的类子集将被考虑。在这种情况下,容器只为带有 CDI 相关注解的类创建 Bean,并忽略其他类。大多数情况下,这不是我们期望的行为,并且与 Java EE 6 的默认模式(当时需要beans.xml文件)不同。

您可能已经注意到,在 beans.xml 文件中,bean-discovery-mode 属性被设置为 all。这允许我们配置上一段中讨论的 CDI 发现模式。它表示我们存档中的每个可读类都将被视为一个托管 Bean。您可以在一个类上放置一个 @Vetoed 注解来将其从 Bean 发现过程中过滤掉。也可以将发现模式设置为 annotated,这样您就可以为每个您希望用作 Bean 的类放置一个作用域注解。这是最新 CDI 版本的默认值(在没有 beans.xml 的情况下也是如此),所以请确保在我们的所有示例中都将其设置为开启。

重新思考您的票务系统

一旦您学会了 CDI 的基础知识,我们将开始使用 CDI Bean 重新设计票务预订系统。我们将通过删除一些不需要的项,如远程接口或异步方法,将其转变为一个更精简的应用程序。通过这样做,您将能够专注于 Web 应用程序中实际使用的组件。

让我们创建一个新的 Maven 项目,就像我们在上一章所做的那样:

  1. 文件 菜单,转到 新建 | Maven 项目;按照我们之前的方式遵循向导(记得勾选 创建一个简单项目 选项)。

  2. 在下一屏中,输入 com.packtpub.wflydevelopment.chapter4 作为 Group Idticket-agency-cdi 作为 Artifact Id,并将打包设置为 war重新思考您的票务系统

  3. 点击 完成。Eclipse 的 Maven 插件将为您生成一个您在上一章中熟悉的项目结构。

  4. 唯一的不同之处在于,除了标准的 java(用于 Java 类)和 resources(用于配置文件)文件夹外,还有一个名为 webapp 的新目录,它将托管 Web 应用程序的视图。

添加所需的依赖项

为了编译和运行项目,我们的 Maven 的 pom.xml 文件将需要上一章中已知的一组依赖项:

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.logging</groupId>
            <artifactId>jboss-logging</artifactId>
            <version>3.1.4.GA</version>
            <scope>provided</scope> 
        </dependency>
     </dependencies>

我们还将需要上一章中的两个插件(注意我们将文件扩展名从 jar 改为了 war):

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- WildFly plugin to deploy the application -->
            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-maven-plugin</artifactId>
                <version>1.0.2.Final</version>
                <configuration>
 <filename>${project.build.finalName}.war</filename>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <!-- enforce Java 8 -->
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

如果您在 POM 配置文件中遇到任何问题,请确保您检查这本书附带的源代码以及上一章的材料。

创建 Bean

一旦您的项目配置正确,我们就可以开始建模我们的 Bean。我们将首先升级的 Bean 是 TheatreBooker,它将驱动用户会话,从我们的 TheatreBox Bean 访问票务列表:

package com.packtpub.wflydevelopment.chapter4.controller;

import com.packtpub.wflydevelopment.chapter4.boundary.TheatreBox;
import org.jboss.logging.Logger;

import javax.annotation.PostConstruct;
import javax.enterprise.context.SessionScoped;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.Serializable;

@Named [1]
@SessionScoped [2]
public class TheatreBooker implements Serializable {

    @Inject
 private Logger logger; [3]

    @Inject
 private TheatreBox theatreBox; [4]

    @Inject
 private FacesContext facesContext; [5]

    private int money;

    @PostConstruct
    public void createCustomer() {
        this.money = 100;
    }

    public void bookSeat(int seatId) {
        logger.info("Booking seat " + seatId);
        int seatPrice = theatreBox.getSeatPrice(seatId);

        if (seatPrice > money) {
 FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Not enough Money!", "Registration unsuccessful"); [6]
            facesContext.addMessage(null, m);
            return;
        }

        theatreBox.buyTicket(seatId);

        FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_INFO, "Booked!", "Booking successful");
        facesContext.addMessage(null, m);
        logger.info("Seat booked.");

        money = money - seatPrice;
    }

    public int getMoney() {
        return money;
    }
}

如您所见,该 Bean 已被标记为 Named [1],这意味着它可以直接在我们的 JSF 页面中引用。该 Bean 是 SessionScoped [2],因为它在其会话期间存储客户可用的金额。

我们还希望注入logger [3]FacesContextFacexContexts [5],而不是手动定义它们。为此,我们需要注册一个生成日志记录器的 Bean,这些日志记录器以类的名称作为参数。我们将在稍后介绍生成 Bean 的过程。

最后,请注意,我们可以安全地使用Inject [4]注解将 EJB 注入我们的 CDI Bean 中。同样,反向注入也是完全合法的,即,将 CDI Bean 注入 EJB 中。

与我们早期的项目相比,当客户无法负担票价时,我们这里不会抛出 Java 异常。由于应用程序是基于 Web 的,我们只需使用JSF Faces Messages [6]向客户端显示警告消息。

我们在应用程序中仍然使用的另一个 Bean 是TheatreInfo,它已被移动到controller包中,因为它实际上会为应用程序提供可用座位的列表:

package com.packtpub.wflydevelopment.chapter4.controller;

import com.google.common.collect.Lists;
import com.packtpub.wflydevelopment.chapter4.boundary.TheatreBox;
import com.packtpub.wflydevelopment.chapter4.entity.Seat;

import javax.annotation.PostConstruct;
import javax.enterprise.event.Observes;
import javax.enterprise.event.Reception;
import javax.enterprise.inject.Model;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Collection;

@Model [1]
public class TheatreInfo {

    @Inject
    private TheatreBox box;

    private Collection<Seat> seats;

    @PostConstruct
    public void retrieveAllSeatsOrderedByName() {
        seats = box.getSeats();
    }

 @Produces [2]
    @Named
    public Collection<Seat> getSeats() {
        return Lists.newArrayList(seats);
    }
    public void onMemberListChanged(@Observes(notifyObserver = Reception.IF_EXISTS) final Seat member) {
        retrieveAllSeatsOrderedByName(); [3]
    }
}

首先,看看@Model注解[1],它是对两个常用注解@Named@RequestScoped的别名(我们称这种注解为类型化注解)。因此,此 Bean 将被命名为我们的 JSF 页面,并将携带请求作用域。

接下来,注意getSeats方法。此方法返回一个座位列表,将其作为producer方法[2]公开。

注意

producer方法允许您控制依赖对象的生成。作为一个 Java 工厂模式,它们可以用作对象源,其实现在运行时可能不同,或者如果对象需要一些不在构造函数中执行的定制初始化。

它可以用来提供任何类型的具体类实现;然而,它特别适用于将 Java EE 资源注入您的应用程序。

使用@Producer注解为getSeats方法的一个优点是,其对象可以直接通过 JSF 的表达式语言EL)公开,正如我们稍后将看到的。

最后,CDI 在此示例中释放的另一个特性是观察者。正如其名所示,观察者可以用来观察事件。每当创建、删除或更新对象时,观察者方法都会被通知。在我们的示例中,它允许在需要时刷新座位列表。

注意

严格来说,在我们的示例中,我们使用了一个条件观察者,表示为表达式notifyObserver = Reception.IF_EXISTS。这意味着在实践中,只有当组件的实例已经存在时,才会调用observer方法。如果没有指定,默认选项(ALWAYS)将是观察者方法始终被调用。(如果不存在实例,它将被创建。)

在最新的 CDI 版本中,通过向观察者的方法添加EventMetadata参数,可以在观察者中获取有关已触发事件的额外信息。

每当我们的座位列表发生变化时,我们将使用javax.enterprise.event.Event对象来通知观察者关于这些变化。这将在我们的单例 Bean 中完成,该 Bean 注入了座位的[1]事件,并在座位预订时通过触发事件来通知观察者[2]

package com.packtpub.wflydevelopment.chapter4.boundary;
import javax.enterprise.event.Event;

@Singleton
@Startup
@AccessTimeout(value = 5, unit = TimeUnit.MINUTES)
public class TheatreBox {

 @Inject [1]
 private Event<Seat> seatEvent;

    @Lock(WRITE)
    public void buyTicket(int seatId) {
        final Seat seat = getSeat(seatId);
        final Seat bookedSeat = seat.getBookedSeat();
        addSeat(bookedSeat);

 seatEvent.fire(bookedSeat); [2]
    }  
    // Rest of the code stays the same, as in the previous chapter
}

之前我们提到,如果 Bean 请求,应该向其注入一个预配置的记录器。我们将创建一个简单的记录器生产者,它将使用注入点(请求记录器的 Bean)的信息来配置一个实例:

package com.packtpub.wflydevelopment.chapter4.util;

import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;
import org.jboss.logging.Logger;

public class LoggerProducer {

    @Produces
    public Logger produceLogger(InjectionPoint injectionPoint) {
        return Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName());
    }
}

我们还允许注入FacesContext而不是使用标准的FacesContext.getCurrentInstance()静态方法。此上下文用于,例如,显示指定的错误消息:

package com.packtpub.wflydevelopment.chapter4.util;

import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Produces;
import javax.faces.context.FacesContext;

public class FacesContextProducer {

    @Produces
    @RequestScoped
    public FacesContext produceFacesContext() {
        return FacesContext.getCurrentInstance();
    }
}

我们将在项目中包含的最后一个类是Seat实体,正如前一章所提到的,它将作为我们的模型使用,无需任何修改(请记住,使用适当的包将其包含在你的项目中)。

构建视图

一旦我们编码了示例的服务器端,创建前端将相当容易,因为我们已经通过 CDI Beans 提供了所有资源。

与本书的一些早期版本相比,一个显著的不同之处在于Facelets现在是 JSF 的首选视图技术。JSF 的早期版本使用JavaServer PagesJSP)作为它们的默认视图技术。由于 JSP 技术在 JSF 之前,使用 JSP 与 JSF 有时感觉不自然或造成问题。例如,JSP 的生命周期与 JSF 的生命周期不同。

注意

与 JSP 生命周期所基于的更简单的请求-响应范式相比,JSF 生命周期要复杂得多,因为 JSF 的核心是 MVC 模式,这有几个含义。在 JSF 生成的视图中,用户操作发生在没有与服务器建立永久连接的客户端。用户操作或页面事件的传递将延迟,直到建立新的连接。JSF 生命周期必须处理事件和事件处理之间的延迟。此外,JSF 生命周期必须确保在渲染之前视图是正确的,并且 JSF 系统包括一个阶段来验证输入,并在所有输入通过验证后更新模型。

大多数时候,Facelets 用于使用 HTML 风格的模板和组件树构建 JavaServer Faces 视图。模板是 Facelets 提供的一个有用功能,允许你创建一个将作为应用程序中其他页面模板的页面(类似于 Struts Tiles)。想法是获取可重用代码的部分,而无需在不同的页面上重复相同的代码。

因此,这里的主要应用程序结构包含一个名为 default.xhtml 的模板页面,该页面通过页面组成元素的模板属性被视图引用。模板包含两个主要的 HTML div 元素,将用于包含主要应用程序面板(content)和一个页脚 divfooter),它几乎只输出应用程序标题。

为了首先添加模板,将一个新的 JSF 页面添加到您的应用程序的 WEB-INF/templates 文件夹中,并将其命名为 default.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>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <h:outputStylesheet name="style.css"/>
</h:head>
<h:body>
    <div id="container">
        <div id="content">
 <ui:insert name="content">
 [Template content will be inserted here]
 </ui:insert>
        </div>
        <div id="footer">
            <p>
                <em>WildFly Development Ticket Booking example.</em><br/>
            </p>
        </div>
    </div>
</h:body>
</html>

接下来,我们将添加主页面视图,它将被嵌入到您的模板中。为此,将一个名为 index.xhtml 的 JSF 页面添加到您的 Maven 项目的 webapp 文件夹中:

<?xml version="1.0" encoding="UTF-8"?>
<ui:composition 

                template="/WEB-INF/templates/default.xhtml"> [1]
    <ui:define name="content">
        <h1>TicketBooker Machine</h1>
        <h:form id="reg">
 <h3>Money: $ #{theatreBooker.money}</h3> [2]
            <h:messages errorClass="error" infoClass="info"
                        globalOnly="true"/>
            <h:panelGrid columns="1" border="1" styleClass="smoke">
 <h:dataTable var="_seat" value="#{seats}" [3]
 rendered="#{not empty seats}" styleClass="simpletablestyle">

                    <h:column>
                        <f:facet name="header">Id</f:facet>
                        #{_seat.id}
                    </h:column>

                    <h:column>
                        <f:facet name="header">Name</f:facet>
                        #{_seat.name}
                    </h:column>
                    <h:column>
                        <f:facet name="header">Price</f:facet>
                        #{_seat.price}$
                    </h:column>
                    <h:column>
                       <f:facet name="header">Booked</f:facet>
                       #{_seat.booked}
                    </h:column>
                    <h:column>
                       <f:facet name="header">Action</f:facet>
                       <h:commandButton id="book"
 action="#{theatreBooker.bookSeat(_seat.id)}" [4]
                       disabled="#{_seat.booked}"
                       value="#{_seat.booked ? 'Reserved' : 'Book'}" />
                    </h:column>

                </h:dataTable>
            </h:panelGrid>
        </h:form>
    </ui:define>
</ui:composition>

ui:composition 元素是一个模板标签,它将内容包装以包含在另一个 Facelet 中。具体来说,它将被包含在 default.xhtml[1] 模板中。

视图的创建分为三个步骤。首先,我们将显示客户的金钱 [2],它绑定到名为 money 的会话变量。

注意

注意我们如何直接从 JSF 表达式中引用 CDI Bean(例如,TheatreBooker),就像我们过去使用 JSF 管理 Bean 一样。

清单上的下一件事是打印所有由应用程序通过 messages 元素产生的 JSF 消息 [3]

这个视图的主要任务是生成所有票务的视图,并允许用户购买它们。这是通过一个 dataTable 对象 [3] 实现的,它可以用来生成对象的表格列表,这些对象通常存储在您的 beans 中的 java.util.List

注意 dataTable 对象的值属性:

<h:dataTable var="_seat" value="#{seats}" 
rendered="#{not empty seats}" styleClass="simpletablestyle">

在这种情况下,我们不是直接引用 CDI Bean,而是引用一个由 CDI Bean 生成的对象。更准确地说,它是由 TheatreInfo 生成的,正如我们所看到的,它在我们座位列表上有 @Produces@Named 注解:

private List<Seat> seats;

@Produces
@Named
public List<Seat>getSeats() {
   return seats;
}

只有当 dataTable 对象中包含一些数据时(如 not empty seats EL 表达式所规定的),它才会显示。在 dataTable 的一个列中,我们添加了一个 commandButton [4],它将用于预订显示在该行上的座位。注意这里的一个 JSF 2 精美之处,因为我们通过传递一个参数(即 seatId 字段)调用 TheatreBookerbookSeat 方法。

JSF 2 面板建议

通过在您的项目配置中启用 JSF 2 面板,您可以在设计视图时享受一些额外的优势。

启用 JSF 2 项目面板需要半分钟。右键单击您的项目,导航到 属性 | 项目面板。然后,选择 JSF 2.2 项目面板 复选框,并单击 确定 按钮:

JSF 2 面板建议

注意

一旦启用 JSF 面板,Eclipse 将通知您缺少 JSF 库配置;只需禁用 Maven 负责的 JSF 库配置即可。

一旦配置了 JSF 2 角色组件,如果您在引用字段或方法之前按下 Ctrl + 空格键,一个建议弹出窗口将允许您选择您想要引用的 Bean 的方法或属性。

准备运行应用程序

好的,现在您的应用程序几乎准备好了。我们只需要在 web.xml 文件中配置一个 JSF 映射,如下所示:

<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>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

这将随后运行 /faces/* URL 上所有页面的 FacesServlet Servlet。

最后,正如之前所述,为了激活我们的 war 文件作为显式的 Bean 存档,我们需要在应用程序的 WEB-INF 文件夹中添加一个空的 beans.xml 文件。

因此,如果您遵循本章中使用的相同命名约定,您将得到以下项目结构:

准备运行应用程序

到目前为止,您应该已经熟悉了使用 Eclipse 或 shell 构建和部署您的 Maven 应用程序。假设您正在使用 shell 管理您的应用程序,请先使用以下命令构建项目:

mvn package

然后,使用我们在上一章中使用的 WildFly Maven 插件发布它。

如果 WildFly 服务器已启动,您可以执行以下命令:

mvn wildfly:deploy

如果 WildFly 服务器没有启动,您可以执行以下命令,然后 WildFly Maven 插件将自动启动一个实例:

mvn wildfly:run

应用程序将在 http://localhost:8080/ticket-agency-cdi 上可用。

然后,为了执行一个独特的命令,您可以执行以下操作:

mvn clean package wildfly:deploy

经过这么多工作,您将很高兴看到您的应用程序在浏览器上运行:

准备运行应用程序

现在,您将能够预订预算($ 100)内定义在您的 SessionScoped 实例中的票务。所以,享受 JSF 和 CDI 的第一次尝试吧。

当然,在本章中,我们只是对 JSF 特性进行了初步探讨。JSF 2.2 中还引入了一种新的高级方法,可用于基于流程的场景,例如购物车。这个新特性被称为 FacesFlow,并附带一个 @FlowScoped 注解。然而,我们现在将专注于向我们的当前应用程序添加一些其他功能。

将调度器集成到我们的应用程序中

到目前为止,我们还没有将负责模拟其他客户请求票务的调度器包含到我们的应用程序中。这并不是疏忽;实际上,在 Web 应用程序中引入外部系统会带来一些挑战。例如,如果调度器更新了应用程序使用的一些数据,用户将如何得知?

有几种策略可以解决这个需求;然而,它们都归结为在您的客户端应用程序中使用一些智能。例如,如果您熟悉网络脚本语言,您可以使用流行的 jQuery API 轮询服务器以获取一些更新。JSF 2.2 的最新版本对 HTML5 和 JavaScript 框架提供了很好的支持,这要归功于自定义数据属性和透传元素。这些是简单的机制,允许 JSF 的渲染套件渲染页面的一部分而无需任何进一步更改,以便自定义标签可以被浏览器(或 JavaScript 框架)解释。

由于并非所有 Java EE 开发者都擅长 JavaScript,我们更愿意展示一种简单而有效的方法来使用RichFaces库(www.jboss.org/richfaces)来满足我们的需求,这些库提供了高级 Ajax 支持以及一系列现成的组件。

安装 RichFaces

安装 RichFaces 需要一组核心库,这些库通常可在 RichFaces 下载页面上找到。

此外,您还需要提供一组由 RichFaces API 使用的第三方依赖项。没关系,这正是 Maven 的作用!首先,在上面的依赖管理部分添加RichFaces API 的最新物料清单BOM):

<dependencyManagement>
  ...
    <dependency>
        <groupId>org.richfaces</groupId>
        <artifactId>richfaces-bom</artifactId>
        <version>4.3.5.Final</version>
        <scope>import</scope>
        <type>pom</type>
    </dependencies>
</dependencyManagement>

然后,只需添加丰富的 UI 库和核心 API:

<dependency>
    <groupId>org.richfaces.ui</groupId>
    <artifactId>richfaces-components-ui</artifactId>
</dependency>
<dependency>
    <groupId>org.richfaces.core</groupId>
    <artifactId>richfaces-core-impl</artifactId>
</dependency>

使您的应用程序丰富

一旦我们安装了 RichFaces 库,我们只需在项目中的每个 XHTML 页面上引用它们。以下是使用 RichFaces 命名空间的新的index.xhtml页面:

<ui:composition 

                template="/WEB-INF/templates/default.xhtml">
    <ui:define name="content">
        <f:view>

            <h:form>
                <a4j:poll id="poll" interval="2000"
                 enabled="#{pollerBean.pollingActive}"
                 render="poll,grid,bookedCounter"/>
                <rich:panel header="TicketBooker Machine" 
                            style="width:350px">

                    <h2>Book your Ticket</h2>

                    <h3>Money: $ #{theatreBooker.money}</h3>
                    <h:messages errorClass="error" infoClass="info" globalOnly="true"/>

 <rich:dataTable id="grid" var="_seat" 
 value="#{seats}" 
 rendered="#{not empty seats}" 
 styleClass="simpletablestyle">

                        <h:column>
                            <f:facet name="header">Id</f:facet>
                            #{_seat.id}
                        </h:column>

                        <h:column>
                            <f:facet name="header">Name</f:facet>
                            #{_seat.name}
                        </h:column>
                        <h:column>
                            <f:facet name="header">Price</f:facet>
                            #{_seat.price}
                        </h:column>
                        <h:column>
                            <f:facet name="header">Booked</f:facet>
                            #{_seat.booked}
                        </h:column>
                        <h:column>
                            <f:facet name="header">Action</f:facet>
                            <h:commandButton id="book"
                        action="#{theatreBooker.bookSeat(_seat.id)}"
                        disabled="#{_seat.booked}"
                        value="#{_seat.booked ? 'Not Available' : 'Book'}"/>
                        </h:column>
                    </rich:dataTable>
 <h:outputText value="Booked seats on this page: #{bookingRecord.bookedCount}"  id="bookedCounter" />
                </rich:panel>
            </h:form>
        </f:view>
    </ui:define>
</ui:composition>

我们已经突出显示了添加到本页的核心增强功能。起初,正如我们所说的,我们需要在 XHTML 页面的顶部引用 RichFaces 库。

接下来,我们添加了一个丰富的 Ajax 组件,a4j:poll,它通过简单地轮询服务器以获取更新,允许重新渲染我们的组件——grid(包含主数据表),poller(检查是否应该继续运行),以及bookedCounter

此外,此组件引用了一个名为Poller的 CDI Bean,它充当我们的轮询器的开/关标志。我们预计在所有座位售罄后关闭轮询:

package com.packtpub.wflydevelopment.chapter4.controller;

import java.util.Optional;

import javax.enterprise.inject.Model;
import javax.inject.Inject;

import com.packtpub.wflydevelopment.chapter4.boundary.TheatreBox;
import com.packtpub.wflydevelopment.chapter4.entity.Seat;

@Model
public class Poller {

    @Inject
    private TheatreBox theatreBox;

    public boolean isPollingActive() {
        return areFreeSeatsAvailable();
    }

    private boolean areFreeSeatsAvailable() {
        final Optional<Seat> firstSeat = theatreBox.getSeats().stream().filter(seat -> !seat.isBooked()).findFirst();
        return firstSeat.isPresent();
    }
}

我们的销售服务与上一章几乎相同(唯一的区别是logger注入):

package com.packtpub.wflydevelopment.chapter4.control;

import com.packtpub.wflydevelopment.chapter4.boundary.TheatreBox;
import com.packtpub.wflydevelopment.chapter4.entity.Seat;
import org.jboss.logging.Logger;

import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.Schedule;
import javax.ejb.Stateless;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.inject.Inject;
import java.util.Collection;
import java.util.Optional;

@Stateless
public class AutomaticSellerService {

    @Inject
    private Logger logger;

    @Inject
    private TheatreBox theatreBox;

    @Resource
    private TimerService timerService;

    @Schedule(hour = "*", minute = "*", second = "*/30", persistent = false)
    public void automaticCustomer() {
        final Optional<Seat> seatOptional = findFreeSeat();

        if (!seatOptional.isPresent()) {
            cancelTimers();
            logger.info("Scheduler gone!");
            return; // No more seats
        }

        final Seat seat = seatOptional.get();

        theatreBox.buyTicket(seat.getId());

        logger.info("Somebody just booked seat number " + seat.getId());
    }

    private Optional<Seat> findFreeSeat() {
        final Collection<Seat> list = theatreBox.getSeats();
        return list.stream().filter(seat -> !seat.isBooked()).findFirst();
    }

    private void cancelTimers() {
        for (Timer timer : timerService.getTimers()) {
            timer.cancel();
        }
    }
}

最后,我们将添加一个预订记录,它将使用视图作用域与当前视图绑定。它的作用是计算用户在当前视图中完成的预订数量(单个浏览器标签页被视为单个视图):

package com.packtpub.wflydevelopment.chapter4.controller;

import com.packtpub.wflydevelopment.chapter4.entity.Seat;

import java.io.Serializable;

import javax.enterprise.event.Observes;
import javax.faces.view.ViewScoped;
import javax.inject.Named;

@Named
@ViewScoped
public class BookingRecord implements Serializable {

    private int bookedCount = 0;

    public int getBookedCount() {
        return bookedCount;
    }

    public void bookEvent(@Observes Seat bookedSeat) {
        bookedCount++;
    }
}

您可以通过在浏览器中的两个单独标签页尝试预订票务来实验预订计数器。

你可能已经注意到我们在豆子上放置了两个注解:@Named@ViewScoped。如果你想要定义多个带有特定 CDI 注解的 bean,创建一个包含所需注解的自定义注解是个好主意。这种结构被称为模式。可以包含以下元素:

  • 默认作用域

  • 可选,拦截器绑定

  • 可选,一个 @Named 注解

  • 可选,一个 @Alternative 注解

要创建一个模式,你需要添加所需的注解以及 @Stereotype 注解:

@ViewScoped
@Named
@Stereotype
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface NamedView {

}

现在,你可以按照以下方式定义 BookinRecord bean:

@NamedView
public class BookingRecord implements Serializable {
    //Some code here
}

@Model 模式注解在 CDI 中默认可用。它定义了一个请求作用域的命名 bean,你可以直接在你的 bean 上使用它。

运行应用程序

在所有库就绪后,你现在可以测试运行你的新丰富应用程序。正如你所见,每 30 秒就有一张票售罄,按钮实时变为不可用

运行应用程序

创建拦截器

这里还有一个值得提到的 CDI 特性,即拦截器。有时,应用程序包含逻辑并跨越多个层;最简单的例子是日志记录。在 Java EE 平台上,可以使用拦截器来实现。首先,我们需要创建一个新的注解:

@Inherited
@InterceptorBinding [1]
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface Logged {
    // empty
}

这个注解定义了一个拦截器绑定。它可以用来指定你想要拦截的方法。绑定也可以用于类型;在这种情况下,对该类型的每个方法调用都会被拦截。这个定义最重要的部分是 @InterceptorBinding [1] 注解。务必添加它!

然后,我们必须创建拦截器定义本身:

@Interceptor
@Logged [1]
public class LoggingInterceptor implements Serializable {

 @AroundInvoke [2]
    public Object log(InvocationContext context) throws Exception {
        final Logger logger = Logger.getLogger(context.getTarget().getClass());
        logger.infov("Executing method {0}", context.getMethod().toString());
        return context.proceed() [3];
    }
}

我们首先声明我们的类是 @Interceptor,它将使用我们之前定义的拦截器绑定(@Logged [1])。接下来,我们创建一个名为 log 的方法,它将在注解类上的每个方法执行(@AroundInvoke [2])周围执行。在其中,我们将调用 context.proceed() 方法,它基本上会将调用转发到原始接收者。请注意,拦截器可以基于某些安全逻辑(例如)决定是否应该丢弃调用。它甚至可以分析或更改返回值。

最后,我们必须在 beans.xml 文件中启用它,通过添加以下代码:

<interceptors>
  <class>com.packtpub.wflydevelopment.chapter4.util.LoggingInterceptor</class>
</interceptors>

现在,让我们转到仅使用 @Logged 注解来记录的注解类或方法。例如,参考以下内容:

@Named
@SessionScoped
@Logged
public class TheatreBooker implements Serializable {
    // Some code
}

现在,所有对 TheatreBooker 公共方法的调用都将记录到控制台:

21:02:11 INFO  [com.packtpub.wflydevelopment.chapter4 .controller.TheatreBooker$Proxy$_$$_WeldSubclass] (default task-8) Executing method public int com.packtpub.wflydevelopment.chapter4.controller. TheatreBooker.getMoney()

在多个拦截器的情况下,它们的执行顺序由@Interceptor.Priority注解确定。优先级最低的拦截器将被首先调用。请确保检查Priority注解中定义的常量。您自己的拦截器的优先级应该在APPLICATIONLIBRARY_AFTER作用域之间。

此外,还有一些有趣的 CDI 机制,我们在这本书中不会涉及,但绝对值得探索:装饰器和替代方案。装饰器基本上是强类型拦截器,专注于您应用程序的业务逻辑。替代方案可以用来为特定 Bean 提供替代实现。

EJBs 和 JSF 管理 Bean 已经过时了吗?

在本章结束时,我们希望就开发者提出的一个常见问题给出我们诚实的意见,即 EJB、JSF 管理 Bean 和 CDI 如何交互以及它们之间的边界在哪里。它们之间有冗余吗?由于现在 Java EE 中提供了多个组件模型,这确实有点令人困惑。

JSF 管理 Bean 长期以来一直是应用程序视图和业务方法之间的实际粘合剂。自从 JSF 2.0 版本发布以来,您可以通过注解声明 JSF 管理 Bean,并且作用域通过视图作用域和创建自定义作用域的能力得到了扩展。然而,对于 JSF 管理 Bean 来说,仍然很少有活动。它的大多数功能都可以由 CDI Bean 替代,CDI Bean 更加灵活,并允许您更好地与其他 Java EE 组件集成。即使在 JSF 最新版本中,视图作用域也被实现为一个 CDI 自定义作用域(javax.faces.view.ViewScoped),这取代了旧的javax.faces.bean.ViewScoped(注意包名;这是一个常见的错误,混淆了它们)。

另一方面,尽管 EJBs 使用的是不太灵活的注入机制,但它们仍然保持了一些独特的功能,如可调度定时器、异步操作和池化,这对于节流和确保应用程序提供良好的服务质量至关重要。从 Java EE 7 开始,EJBs 不再是唯一具有事务性的组件。新的@Transactional注解允许您通过简单地将它放在选定的方法上,在 CDI Bean 中使用声明式事务。

尽管如此,EJBs 可能不会从我们的代码中消失,而是很可能(并且也是所希望的)它们将继续被用于一些独特的功能。然而,对于剩余的部分,其功能将通过 CDI 而不是 EJBs 自己的注解(如@Stateless@EJB)来公开。

摘要

在本章中,我们介绍了 CDI。我们涵盖了 JSF 页面如何访问 CDI 命名的 bean,就像它们是 JSF 管理 bean 一样。我们还介绍了 CDI 如何通过@Inject注解使将依赖项注入我们的代码变得简单。此外,我们还解释了如何添加 JBoss 生态系统中的另一个库(RichFaces),仅揭露了其潜在性的一方面。

到目前为止,我们一直在处理内存中的数据,因此现在是时候介绍用于我们的 CDI 应用程序的存储,即使用 Java 持久化 API,这是下一章的主题。

第五章:将持久性与 CDI 结合

在前面的章节中,我们讨论了 Java EE,结合了 CDI 等几种技术。然而,到目前为止的示例都是基于一个错误的假设,即所有信息都可以存储在内存中。在本章中,我们将展示如何以标准的关系数据库的形式为我们的应用程序使用持久性数据存储。

Enterprise JavaBeansEJB)3.2 规范包含对持久性规范Java Persistence APIJPA)的引用。它是一个用于创建、删除和查询称为实体的 Java 对象的 API,这些实体可以在符合 EJB 3.x 容器的标准和 Java SE 环境中使用。在 Java EE 7 中,它已更新到 2.1 版本。您可以在 JSR 338(jcp.org/en/jsr/detail?id=338)中查看当前版本的规范。

我们需要提醒您,在本章中,您有很多东西要学习,因此概念将从各个方向向您袭来。然而,在结束时,您将能够确切地欣赏如何创建和部署一个完整的 Java EE 7 应用程序。

具体来说,我们将涵盖以下主题:

  • JPA 的关键特性

  • 如何创建实体和数据库模式

  • 如何使用 CDI Beans 和 EJBs 操作实体

  • 使用 JSF 和 Facelets 技术为我们应用程序提供前端层

数据持久性符合标准

基于简单 Java 对象(POJO)开发模型的 Enterprise Java Persistence 标准的到来,在 Java EE 平台中填补了一个重要的空白。之前的尝试(EJB 2.x 规范)偏离了目标,并创造了一个不便于开发且对许多应用程序来说过于沉重的 EJB 实体 bean 的刻板印象。因此,它从未在许多行业的许多领域得到广泛采用或普遍认可。

软件开发者知道他们想要什么,但很多人在现有的标准中找不到,所以他们决定另寻他处。他们找到的是轻量级持久性框架,包括商业和开源领域。

与 EJB 2.x 实体 bean 相比,EJB 3.0 的Java Persistence APIJPA)是一种元数据驱动的 POJO 技术,也就是说,为了将存储在 Java 对象中的数据保存到数据库中,我们的对象不需要实现接口、扩展类或符合框架模式。

JPA 的另一个关键特性是称为Java Persistence Query LanguageJPQL)的查询语言,它提供了一种以可移植的方式定义查询的方法,独立于在企业环境中使用的特定数据库。JPA 查询在语法上类似于 SQL 查询,但操作的是实体对象而不是直接与数据库表交互。

使用 JPA 进行操作

受 ORM 框架如 Hibernate 的启发,JPA 使用注解将对象映射到关系数据库。JPA 实体是 POJO,不扩展任何类也不实现任何接口。您甚至不需要映射的 XML 描述符。实际上,JPA API 由注解和一些类和接口组成。例如,我们将 Company 类标记为 @Entity,如下所示:

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Company {

    // Some code

    @Id
    private String companyName;

    public Company () {  }

    // Some code

}

上一段代码显示了使一个类持久化的最小要求,如下所示:

  • 它必须使用 @javax.persistence.Entity 注解标识为实体

  • 它必须使用 @javax.persistence.Id 注解的标识属性

  • 它必须在至少受保护的范围内有一个无参数的构造函数

由于通过示例学习效果更好,我们将在下一节中展示如何在 WildFly 中创建和部署一个示例 JPA 应用程序。

将持久化添加到我们的应用程序中

为了持久化数据,JPA 需要一个关系数据库;我们将使用在开发者中相当流行的 PostgreSQL 数据库,可以从 www.postgresql.org/download/ 免费下载。建议下载 PostgreSQL 9.x 的最新稳定版本,并使用简单的安装向导进行安装。如果您不需要完整的数据库,请记住,稍后我们还将向您展示如何使用 WildFly 提供的内存数据库,这在开发期间可能是一个非常有用的替代方案。

设置数据库

我们将创建一个名为 ticketsystem 的数据库;然后我们将添加一个名为 jboss 的用户,并授予他在模式上的所有权限。

在您的 PostgreSQL 安装目录下的 bin 文件夹中打开一个 shell 并运行可执行文件 psql –U postgres。一旦使用安装密码登录,执行以下命令:

CREATE DATABASE ticketsystem;
CREATE USER jboss WITH PASSWORD 'jboss';
GRANT ALL PRIVILEGES ON DATABASE ticketsystem TO jboss;

我们简单的模式将由两个表组成:包含剧院中所有可用座位列表的 SEAT 表,以及用于对座位类型进行分类的 SEAT_TYPE 表。这两个表之间是 1-n 的关系,SEAT 表包含一个外键,与 SEAT_TYPE 表的 ID 相关联。然而,我们将让 JPA 根据我们的类层次结构为我们生成模式,我们将在稍后建模。

在 WildFly 中安装 JDBC 驱动程序

数据库连接是通过 JDBC 驱动程序在 Java 中实现的,这些驱动程序可以直接在您的应用程序中使用,或者在 JPA 的幕后使用。可以从 jdbc.postgresql.org/download.html 免费下载 PostgreSQL JDBC 驱动程序。

下载完成后,将 postgresql-9.X-X.jdbc41.jar 文件放置在文件系统中的一个方便位置。现在我们将看到如何在 WildFly 中安装 JDBC 驱动程序。

在 JBoss AS 5 和 6 中,您通常在服务器分发的 common/lib 文件夹中安装 JDBC 驱动程序。在新的模块化服务器架构(在 JBoss AS 7 中引入)中,您有多个选项来安装您的 JDBC 驱动程序。推荐的方法是将驱动程序作为模块安装。

安装新模块的流程需要在 JBOSS_HOME/modules 下创建一个模块路径,并将 .jar 库和 module.xml 文件(声明模块名称及其依赖项)放置在那里。

在我们的示例中,我们将向我们的文件系统添加以下单元:

  • JBOSS_HOME/modules/org/postgresql/main/postgresql-9.3-1101.jdbc41.jar

  • JBOSS_HOME/modules/org/postgresql/main/module.xml

首先在您的 WildFly 安装中(JBOSS_HOME 变量指向的位置)创建所需的目录,并将下载的 JAR 文件复制到其中。

现在,在主文件夹中添加一个名为 module.xml 的文件。此文件包含实际的模块定义;其中最有趣的部分是模块名称(org.postgresql),它与数据源中定义的模块属性相对应。

接下来,您需要声明 JDBC 驱动程序资源路径和模块依赖项,如下所示:

<module  name="org.postgresql"> 
  <resources>
    <resource-root path="postgresql-9.3-1101.jdbc41.jar"/>
  </resources>
  <dependencies>
    <module name="javax.api"/>
    <module name="javax.transaction.api"/>
  </dependencies>
</module>

我们已经完成了模块的安装。现在我们需要在我们的配置中定义一个数据源,该数据源将使用此模块并在我们的 PostgreSQL 数据库中维护一个连接池。为了做到这一点,您可以编辑 standalone.xml/domain.xml,向数据源的子系统添加一个驱动元素(确保将此配置与您配置中现有的任何数据源合并):

<subsystem >
 <datasources>
  <datasource jta="false" 
      jndi-name="java:jboss/datasources/wflydevelopment" 
      pool-name="wflydevelopment" enabled="true">
          <connection-url>
           jdbc:postgresql://localhost:5432/ticketsystem
          </connection-url>
          <driver-class>org.postgresql.Driver</driver-class>
          <driver>postgresql</driver>
          <security>
               <user-name>jboss</user-name>
               <password>jboss</password>
          </security>
  </datasource>
  <drivers>
         <driver name="postgresql" module="org.postgresql"/>
  </drivers>
 </datasources>
</subsystem>

如您所见,新的配置文件从早期的 JBoss AS 配置中借用了相同的 XML 架构定义,因此迁移到新架构应该不会很困难。基本上,您将使用 connection-url 字符串和 driver 部分的 JDBC 驱动程序类来定义数据库的连接路径。

注意

自从 JBoss AS 7.1.0 以来,数据源绑定到 java:/java:jboss/ JNDI 命名空间是强制性的。这将标准化开发人员之间的资源定义,避免奇特的 JNDI 绑定。

使用命令行界面创建新的数据源

应用程序服务器提供了多种方法来将数据源添加到您的配置中。我们只提及命令行界面方法,这可能会非常有用,尤其是如果您计划使用脚本文件修改您的配置。

启动 jboss-cli.sh 脚本(或 jboss-cli.bat)并连接到应用程序服务器,如下所示:

[disconnected /] connect
[standalone@localhost:9990 /]

现在,执行以下命令,该命令实际上创建了一个新的数据源,实现了我们通过编辑配置文件所获得的目标:

/subsystem=datasources/data-source=wflydevelopment:add(jndi-name=java:jboss/datasources/wflydevelopment, driver-name=postgresql, connection-url= jdbc:postgresql://localhost:5432/ticketsystem,user-name="jboss",password="jboss")

如果一切顺利,CLI 应该会响应一个 success 消息。

创建 Maven 项目

本章我们将要创建的应用程序只需要我们使用标准的 Java EE 7 API。有了前面章节的知识,你应该能够自己设置本章的项目!只需使用你喜欢的 IDE,创建一个war类型的 Maven 项目。记得包括 Java SE 8 的配置,以及beans.xmlfaces-config.xml文件。如果你遇到任何问题,请记住,本书中提供的代码示例包含一个基于此示例的完整项目。

添加 Maven 配置

现在你已经设置了 Maven 骨架,我们将包括所需的依赖项,以便 Eclipse 能够在你编码时编译你的类。你将需要的唯一依赖项是javaee-api

    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>7.0</version>
        <scope>provided</scope>
    </dependency>

烹饪实体

现在我们已经完成了配置部分,我们将把我们的实体添加到项目中。有一些很有价值的选项可以自动生成我们的实体,从数据库模式开始。例如,Eclipse 的文件菜单包括一个选项从表生成 JPA 实体,一旦在数据库中设置了连接,它允许你将你的 DB 模式(或其一部分)反向生成到 Java 实体。

如果你愿意尝试这个选项,请记住,你需要在你的项目中激活Eclipse JPA特性,从项目属性中,如图所示:

烹饪实体

在附录中提到了另一个选项,即使用 JBoss Forge 进行快速开发,该附录讨论了 JBoss Forge,这是一个强大的、针对 Java EE 的快速应用程序开发(旨在 Java EE)和项目理解工具。

在本章中,我们将专注于从 Java 类生成 SQL 脚本。无论你的策略是什么,预期的结果都需要符合以下实体。以下是第一个实体,SeatType,它映射到SEAT_TYPE表:

@Entity [1]
@Table(name="seat_type") [2]
public class SeatType implements Serializable {

    @Id  [3]
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    private String description;

    private int price;

    private int quantity;

    //bi-directional many-to-one association to Seat
 @OneToMany(mappedBy="seatType", fetch=FetchType.EAGER) [4]
 private List<Seat> seats;

    // Getters and Setters omitted for brevity
}

第一个有意义的注解是@Entity [1],它声明了Entity类。@Table [2]注解用于将 bean 类映射到数据库表。

@Id注解,[3],是必须的;它描述了表的主键。与@Id一起,还有@GeneratedValue注解。这个注解用于声明数据库负责生成值。你可以查看这个类的 Javadoc 来探索其他值生成策略。

继续前进,@OneToMany注解[4]定义了一个一对一的关联。实际上,SeatType类有许多座位。相应的Seat引用包含在一个列表集合中。我们定义mappedBy属性来设置拥有关系的多边字段。

fetch属性定义了每当从数据库加载seat类型时,JPA 应该检索座位列表。对于关系的延迟配置会导致在第一次调用该字段时检索列表。

最后,请注意,为了简洁起见,我们没有在此处包含生成的字段获取器和设置器。

让我们来看看Seat实体:

@Entity
public class Seat implements Serializable {

    private static final long serialVersionUID = 89897231L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    private boolean booked;

    //bi-directional many-to-one association to SeatType
 @ManyToOne [1]
 @JoinColumn(name="seat_id") [2]
    private SeatType seatType;

    // Getters and Setters omitted for brevity

}

如您所见,Seat实体具有相应的@ManyToOne [1]注解,这自然补充了@OneToMany关系。@JoinColumn [2]通知 JPA 引擎seatType字段是通过数据库座位 ID 的外键进行映射的。

添加 Bean Validation

**Bean Validation (JSR-303)**是 Java EE 6 平台的一部分提供的验证模型。新的 1.1 版本(JSR-349)是 Java EE 7 的一部分。Bean Validation 模型通过在 JavaBeans 组件的字段、方法或类上放置的注解形式的约束得到支持,例如管理 Bean。

在我们的示例中,SeatType实体将通过输入表单创建;因此,我们需要验证用户输入的数据。

在我们的示例中,我们将在SeatType表单的每个字段上放置@javax.validation.constraints.NotNull约束,并在description字段上放置一个更复杂的约束,这将设置座位描述的最大长度为25@javax.validation.constraints.Size约束)并允许其中仅包含字母和空格(@javax.validation.constraints.Pattern约束):

@Entity
@Table(name="seat_type)
public class SeatType implements Serializable {

    private static final long serialVersionUID = 3643635L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

 @NotNull
 @Size(min = 1, max = 25, message = "You need to enter a Seat Description (max 25 char)")
 @Pattern(regexp = "[A-Za-z ]*", message = "Description must contain only letters and spaces")
    private String description;

 @NotNull
    private Integer price;

 @NotNull
    private Integer quantity;

 private SeatPosition position;

    // Getters/Setters here
}

如您所见,我们还可以在约束条件上放置描述,这可以用于在数据未能通过约束条件时向 JSF 层提供自定义的错误消息。您可以通过 Oracle 文档查看可用的完整约束列表,网址为docs.oracle.com/javaee/7/tutorial/doc/bean-validation001.htm#GIRCZ

我们还为我们座位类型添加了座位位置信息。它是一个简单的枚举:

public enum SeatPosition {
    ORCHESTRA("Orchestra", "orchestra"), BOX("Box", "box"), BALCONY("Balcony", "balcony");

    private final String label;
    private final String dbRepresentation;

    private SeatPosition(String label, String dbRepresentation) {
        this.label = label;
        this.dbRepresentation = dbRepresentation;

    }

    public String getDatabaseRepresentation() {
        return dbRepresentation;
    }

    public String getLabel() {
        return label;
    }
}

当我们在数据库中保存SeatType实体时,我们还将存储与之相关的枚举值。JPA 的早期版本提供了两种自动处理它的选项(除了手动管理它们的状态),@Enumerated(EnumType.STRING)@Enumerated(EnumType.ORDINAL);两者都有其缺陷。第一个对枚举重命名敏感;数据库中的实体将存储枚举的全名(这有时也是存储空间的浪费)。第二个在枚举值的顺序改变时可能会出现问题(因为它存储了枚举值的索引)。从 JPA 2.1 开始,我们可以创建一个转换器,它将自动将我们的枚举属性转换为数据库中的特定条目。我们只需要创建一个实现了AttributeConverter接口的注解类:

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class SeatPositionConverter implements AttributeConverter<SeatPosition, String> {

    @Override
    public String convertToDatabaseColumn(SeatPosition attribute) {
        return attribute.getDatabaseRepresentation();
    }

    @Override
    public SeatPosition convertToEntityAttribute(String dbData) {
        for (SeatPosition seatPosition : SeatPosition.values()) {
            if (dbData.equals(seatPosition.getDatabaseRepresentation())) {
                return seatPosition;
            }
        }
        throw new IllegalArgumentException("Unknown attribute value " + dbData);
    }
}

就这些,不需要额外的配置。将autoApply属性设置为true表示 JPA 将负责处理实体中的所有SeatPosition枚举。

配置持久化

实体 API 看起来很棒,并且非常直观,但服务器如何知道应该存储/查询实体对象的是哪个数据库?位于您项目src/main/resources/META-INF下的persistence.xml文件是标准的 JPA 配置文件。通过配置此文件,您可以轻松地从一种持久化提供者切换到另一种,从而也可以从一种应用程序服务器切换到另一种(信不信由你,这向应用程序服务器兼容性迈出了巨大的一步)。

persistence.xml文件中,我们基本上需要指定持久化提供者和底层的数据源。只需在src/main/resources/persistence.xml下创建以下文件:

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

             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
 <persistence-unit name="primary">
 <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
        <class>com.packtpub.wflydevelopment.chapter5.entity.Seat</class>
        <class>com.packtpub.wflydevelopment.chapter5.entity.SeatType</class>
        <properties>
 <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
        </properties>
    </persistence-unit>
</persistence>

我们在persistence.xml中突出了最重要的属性。name属性是一个必需的元素,它将被用来从我们的企业 JavaBeans 中引用持久化单元。

在示例代码中,我们使用 WildFly 内置的内存 H2 数据库([www.h2database.com/](http://www.h2database.com/)),默认情况下在`java:jboss/datasources/ExampleDS`处可用(这样就可以在不进行任何设置的情况下运行示例)。然而,您也可以在这里使用配置好的 PostgreSQL 连接,即java:jboss/datasources/wflydevelopment,这是我们之前创建的。在 Java EE 7 中,甚至可以省略整个jta-data-source标签。现在每个容器都必须为应用程序提供默认数据源。对于 WildFly 来说,将是上述的 H2 数据库。

我们还定义了应被视为实体的类。这是一个可选步骤;如果实体与persistence.xml文件在同一存档中,它们将被自动发现。

在之前的 JPA 版本中,几乎每个配置都需要一些提供者特定的属性。在 JPA 2.1 中,添加了一些标准属性,例如展示的javax.persistence.schema-generation.database.action。可以使用drop-and-create值在每次部署应用程序时创建和删除您的数据库表。如果您希望每次部署应用程序时都从一个干净的数据存储开始,这可能是一个优点。

然而,也可以指示 JPA 为您生成 SQL 脚本,这样您可以手动将它们应用到数据库中。只需将以下条目添加到您的persistence-unit标签中:

<property name="javax.persistence.schema-generation-target" value="scripts"/>
<property name="javax.persistence.ddl-create-script-target" value="createSeats.sql"/>
<property name="javax.persistence.ddl-drop-script-target" value="dropSeats.sql"/>

如果您不通过指定一个额外的属性来指定位置,那么生成的脚本将被放置在JBOSS_HOME/bin目录中,名称与配置中提供的名称相同。名称可以是绝对路径,这样您可以将脚本放置在文件系统的任何位置(当然,如果 WildFly 被允许写入那里的话)。

添加生产者类

生产类在早期章节中被引入,作为通过 CDI 为我们应用程序提供一些资源的一种方式。在这个例子中,我们将使用它来生成许多资源,例如 JPA 实体管理器和传递给 JSF 视图的对象列表。因此,我们提供了包含一些通用资源以及SeatProducerSeatTypeProducer类的单例的LoggerProducerFacesContextProducerEntityManagerProducer类,这些类将用于生成实体集合。

下面是三个基本生产类的内容:

public class LoggerProducer {

    @Produces
    public Logger produceLoger(InjectionPoint injectionPoint) {
        return Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName());
    }
} 

public class FacesContextProducer {

    @Produces
    @RequestScoped
    public FacesContext produceFacesContext() {
        return FacesContext.getCurrentInstance();
    }
} 

public class EntityManagerProducer {

    @Produces
    @PersistenceContext
    private EntityManager em;
}

如您所见,这些类将是以下三种资源的工厂:

  • EntityManager:这将解析主持久化单元,因为只定义了一个持久化单元

  • java.util.Logger:这将记录服务器控制台上的某些信息

  • FacesContext:这将用于在屏幕上输出一些 JSF 消息

提示

生产者与 Java EE 5 @Resource 注入的比较

如果你之前从未使用过依赖注入框架,你可能会想知道添加一个额外的层来生成一些容器资源有什么好处。一旦你需要更改一些配置元素,例如持久化单元,原因就会变得明显。使用较旧的 Java EE 5 方法,你将被迫在所有使用的地方更改@Resource注入的细节;然而,使用生产方法将使资源创建集中化,使更改变得简单。

接下来,我们将添加一些实体生产者;让我们添加SeatTypeProducerSeatProducer类:

@javax.enterprise.context.RequestScoped
public class SeatTypeProducer {

    @Inject
    private SeatTypeDao seatTypeDao;

    private List<SeatType> seatTypes;

    @PostConstruct
    public void retrieveAllSeatTypes() {
        seatTypes = seatTypeDao.findAll();
    }

    @Produces
    @Named
    public List<SeatType> getSeatTypes() {
        return seatTypes;
    }

    public void onListChanged(@Observes(notifyObserver = Reception.IF_EXISTS) final SeatType member) {
        retrieveAllSeatTypes();
    }
}

如果您已经通过了我们第四章的例子第四章,您在这里将找不到任何新内容;如您所见,该类仅生成一个标记为@NamedseatTypes集合,以便可以从 JSF EL 中访问。此外,该类包含一个observer处理方法(onListChanged),当集合中的数据发生变化时,该方法将被触发。

集合数据是通过SeatTypeDao CDI Bean 的retrieveAllSeatTypes方法(在类构造时首次和唯一加载)填充的。我们将在稍后定义这个 bean;现在,我们将添加这个例子中使用的最后一个生产类,即SeatProducer bean:

@javax.enterprise.context.RequestScoped
public class SeatProducer implements Serializable {

    @Inject
    private SeatDao seatDao;

    private List<Seat> seats;

    @PostConstruct
    public void retrieveAllSeats() {
        seats = seatDao.findAllSeats();
    }

    @Produces
    @Named
    public List<Seat> getSeats() {
      return seats;
    }

    public void onMemberListChanged(@Observes(notifyObserver = Reception.IF_EXISTS) final Seat member) {
      retrieveAllSeats();
    }
}

前面的 bean 将被用来生成实际可用于预订的Seat对象列表。

为您的应用程序编写查询代码

如您从前面的代码中看到的,生产类使用了名为SeatDaoSeatTypeDao的 bean 来填充它们的数据集合。这些 bean 对SeatSeatType对象执行一些简单的查找,如下面的代码所示:

@Stateless
public class SeatDao extends AbstractDao<Seat> {

    public SeatDao() {
        super(Seat.class);
    }
}

@Stateless
public class SeatTypeDao extends AbstractDao<SeatType> {

    public SeatTypeDao() {
        super(SeatType.class);
    }
}

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public abstract class AbstractDao<T extends Serializable> implements Serializable {

    private final Class<T> clazz;

    @Inject
    private EntityManager em;

    public AbstractDao(Class<T> clazz) {
        this.clazz = clazz;
    }

    public T find(Object id) {
        return em.find(clazz, id);
    }

    public void persist(final T entity) {
        em.persist(entity);
    }

    public List<T> findAll() {
        final CriteriaQuery<T> criteriaQuery = em.getCriteriaBuilder().createQuery(clazz);
        criteriaQuery.select(criteriaQuery.from(clazz));
        return em.createQuery(criteriaQuery).getResultList();
    }

    public void deleteAll() {
        final CriteriaDelete<T> criteriaDelete = em.getCriteriaBuilder().createCriteriaDelete(clazz);
        criteriaDelete.from(clazz);
        em.createQuery(criteriaDelete).executeUpdate();
    }
}

如您所见,SeatDaoSeatTypeDao这两个 bean 都扩展了通用的AbstractDao类。它封装了EntityManager,并使用 JPA Criteria API 提供了基本的类型安全 CRUD 操作,例如findAllpersist等。JPA 允许执行以下三种类型的查询:

  • 原生 SQL:这些查询使用标准 SQL 语言。在使用此类查询时,你必须记住查询在迁移到不同数据库时可能会不兼容。

  • Java 持久化查询语言JPQL):这些查询可以使用类似于 SQL 的特殊语言形成。在实践中,如果没有良好的 IDE 支持,这种方法通常很难维护,尤其是在重构期间。这些查询也可以在启动时编译,这意味着它们不会被多次解析。最后,它们可以通过缓存机制用于频繁调用的查询,以避免不必要的数据库操作。你可以在@NamedQuery(name="…", query="…")注解中定义查询及其名称。

  • Criteria API:这些查询可以通过简单地执行 Java 方法和使用适当的对象来形成。自 JPA 2.1 以来,可以通过此 API 执行批量更新和删除。

让我们用一个例子简单比较这三种方法。我们只想获取给定类型的所有对象。使用原生 SQL,这个查询看起来是这样的:

entityManager.createNativeQuery("SELECT * from seat_type").getResultList()

如您所见,它使用字符串形式的标准 SQL。现在让我们看看 JPQL:

entityManager.createQuery("select seatType from SeatType seatType").getResultList(); 

很容易注意到它与 SQL 的相似性,但略有不同。例如,它使用类名而不是表名。然而,再次强调,它是一个字符串形式的查询。最后一个例子是 Criteria API:

final CriteriaQuery<SeatType> criteriaQuery = 
                em.getCriteriaBuilder().createQuery(SeatType.class);
criteriaQuery.select(criteriaQuery.from(SeatType.class));
em.createQuery(criteriaQuery).getResultList(); 

初看起来,它似乎是最复杂的,但它有一些优势,即它不使用任何字符串(通常是易出错的且难以重构)。JPQL 和 Criteria API 在最新版本的 JPA 中都有许多改进,包括使用on条件进行连接操作、数据库函数支持和算术子查询。

你可能会问自己,“我应该使用哪一个?”这是一个难题,因为它们都有其优缺点,所以这取决于具体的情况。基本上,Criteria 查询和命名查询通常是安全的赌注。原生 SQL 应该有很好的理由,因为它通常在不同供应商之间不可移植,并且在执行之前不能由 JPA 验证。

将服务添加到您的应用程序中

到目前为止,我们通过应用程序屏幕可见的所有信息都是我们编写的。显然,这里缺少的是所有最终转化为插入数据或更新现有数据的业务逻辑。因此,我们现在将添加两个类;第一个位于 com.packtpub.wflydevelopment.chapter5.control 包下,第二个位于 com.packtpub.wflydevelopment.chapter5.controller 包下。第一个是 TicketService,这是一个无状态的 EJB,将被用来执行此应用程序的核心业务逻辑,第二个是我们的 stateful EJB 对应的 BookerService 类。让我们从无状态的 EJB 开始:

@Stateless
public class TicketService {

    @Inject
    private Logger log;

    @Inject
    private Event<SeatType> seatTypeEventSrc;

    @Inject
    private Event<Seat> seatEventSrc;

    @Inject
    private SeatDao seatDao;

    @Inject
    private SeatTypeDao seatTypeDao;

    public void createSeatType(SeatType seatType) throws Exception {
        log.info("Registering " + seatType.getDescription());
        seatTypeDao.persist(seatType);
        seatTypeEventSrc.fire(seatType);
    }

    public void createTheatre(List<SeatType> seatTypes) {
        for (SeatType type : seatTypes) {
            for (int ii = 0; ii < type.getQuantity(); ii++) {
                final Seat seat = new Seat();
                seat.setBooked(false);
                seat.setSeatType(type);
                seatDao.persist(seat);
            }
        }
    }

    public void bookSeat(long seatId) {
        final Seat seat = seatDao.find(seatId);
        seat.setBooked(true);
        seatDao.persist(seat);
        seatEventSrc.fire(seat);
    }

    public void doCleanUp() {
        seatDao.deleteAll();
        seatTypeDao.deleteAll();
    }
}

小贴士

为什么这个组件被编码为 EJB 而不是 CDI Bean?

使用 EJB 的一大主要优势是它们本质上是事务性组件。然而,在 Java EE 7 中,我们可以使用带有额外 @Transactional 注解的 CDI Beans。现在的选择取决于开发者,但 EJB 在某些情况下仍然可能很有用,即使是对于本地调用;例如,我们可以轻松地为它们划分安全权限(我们将在未来的章节中这样做)。

此服务由四个方法组成。第一个是 createSeatType 方法,它将在第一个应用程序屏幕中用来向我们的剧院添加一个新的 SeatType 对象。下一个方法 createTheatre 将在我们完成剧院设置后调用;因此,我们在下一个屏幕中创建可供预订的座位列表。

接下来是列表中的 bookSeat 方法,正如你可能猜到的,它将被用来预订座位。最后,doCleanUp 方法实际上用于执行清理操作,如果你想要重新启动应用程序的话。

我们拼图中的最后一部分是 BookerService 类,它为您的应用程序添加了一个微小的会话层:

@Named
@javax.faces.view.ViewScoped
public class BookerService implements Serializable {

    private static final long serialVersionUID = -4121692677L;

    @Inject
    private Logger logger;

    @Inject
    private TicketService ticketService;

    @Inject
    private FacesContext facesContext;

    private int money;

    @PostConstruct
    public void createCustomer() {
        this.money = 100;
    }

    public void bookSeat(long seatId, int price) {
        logger.info("Booking seat " + seatId);

        if (price > money) {
            final FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Not enough Money!",
                    "Registration successful");
            facesContext.addMessage(null, m);
            return;
        }

        ticketService.bookSeat(seatId);

        final FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_INFO, "Registered!", "Registration successful");
        facesContext.addMessage(null, m);
        logger.info("Seat booked.");

        money = money - price;
    }

    public int getMoney() {
        return money;
    }
}

前面的类使用了视图作用域,我们已经在之前的章节中描述过。

添加控制器来驱动用户请求

持久化层和用户视图之间的链接落在 TheatreSetupService 实例上,它将驱动对应用程序暴露的实际服务的请求。由于此实例将被绑定到 RequestScope,并且我们需要将其暴露给我们的视图(使用 @Named),我们可以使用方便的 @Model 注解,它是以下两个属性的组合:

@Model
public class TheatreSetupService {

    @Inject
    private FacesContext facesContext;

    @Inject
    private TicketService ticketService;

    @Inject
    private List<SeatType> seatTypes;

 @Produces [1]
 @Named
 private SeatType newSeatType;

    @PostConstruct
    public void initNewSeatType() {
        newSeatType = new SeatType();
    }

    public String createTheatre() {
        ticketService.createTheatre(seatTypes);
        return "book";
    }

    public String restart() {
        ticketService.doCleanUp();
        return "/index";  [4]
    }

    public void addNewSeats() throws Exception {
        try {
            ticketService.createSeatType(newSeatType);

            final FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_INFO, "Done!", "Seats Added");
            facesContext.addMessage(null, m);
            initNewSeatType();
        } catch (Exception e) {
            final String errorMessage = getRootErrorMessage(e);
            FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_ERROR, errorMessage, "Error while saving data");
            facesContext.addMessage(null, m);
        }
    }

    private String getRootErrorMessage(Exception e) {
        // Default to general error message that registration failed.
        String errorMessage = "Registration failed. See server log for more information";
        if (e == null) {
            // This shouldn't happen, but return the default messages
            return errorMessage;
        }

        // Start with the exception and recurse to find the root cause
        Throwable t = e;
        while (t != null) {
            // Get the message from the Throwable class instance
            errorMessage = t.getLocalizedMessage();
            t = t.getCause();
        }
        // This is the root cause message
        return errorMessage;
    }

    public List<SeatPosition> getPositions() {
        return Arrays.asList(SeatPosition.values());
    }

}

TheatreSetupService 类预计要完成以下任务:

  1. 最初,TheatreSetupService 类生成一个 SeatType 对象 [1] 并使用 @Named 注解将其暴露给 JSF 视图层。

    小贴士

    这种技术是 CDI 提供的伟大补充,因为它消除了创建一个样板对象 SeatType 来从视图传输信息到服务的需要。SeatType 对象由控制器生成,将由 JSF 视图填充并由 TheatreSetupService 类持久化。

  2. 它通过返回主页 [4] 来驱动用户在应用程序屏幕之间的导航。

  3. 我们已经完成了 Java 类。你现在应该检查确保你的项目结构与以下截图匹配:添加控制器以驱动用户请求

编写 JSF 视图

现在我们已经完成了中间层,我们只需要在我们的 Web 应用的views文件夹中添加几个 JSF 视图。第一个视图,命名为setup.xhtml,将设置我们的剧院,第二个视图,命名为book.xhtml,将用于预订票务,其中一些代码借鉴了前面的章节。

然而,这次我们希望使我们的应用程序更具图形吸引力。为了保持简单,我们将使用Bootstrap,一个非常流行的前端框架,它将很好地与我们的 JSF 视图集成。它严重依赖于 JavaScript 和 CSS,但我们在应用程序中只需要使用基本的 HTML 就能让它启动并运行。将严格的前端框架纳入我们的应用程序将是一个展示如何使用 Java EE 7 与最新 Web 技术的机会。

你可以从getbootstrap.com/获取 Bootstrap 的最新版本,并将所有文件放置在资源目录中;然而,我们在这里不会这样做。我们将使用 WebJars,它们只是打包客户端 Web 库的 JAR 文件。你可以在www.webjars.org/找到依赖项,添加到你的pom.xml文件后,它们将像手动添加静态文件到项目中一样工作。然而,多亏了 WebJars,我们可以让 Maven 控制我们的版本,并且不需要担心外部代码污染我们的代码库。

现在,我们需要 Bootstrap 和 jQuery,所以我们将添加以下依赖项:

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>1.11.0</version>
</dependency>

现在,当我们已经将 Bootstrap 的库放置到位后,我们必须将它们链接到我们的代码中。我们将它们添加到我们的WEB-INF/templates/default.xhtml文件中,以及一个简单的导航栏:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html 

      >
<h:head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>#{app.applicationName}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="" />
    <meta name="author" content="" />

    <h:outputStylesheet name="/webjars/bootstrap/3.2.0/css/bootstrap.css " />
    <h:outputStylesheet name="/webjars/bootstrap/3.2.0/css/bootstrap-theme.css " />

    <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
    <!--[if lt IE 9]>
      <script src="img/html5.js"></script>
    <![endif]-->

<style>
    body {
        padding-top: 60px;
    }
    </style>
</h:head>
<h:body>
    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
    <div class="navbar-header">
        <h:link outcome="/index" class="navbar-brand" value="Ticket Agency" />
    </div>
    <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
        <li class="#{view.viewId =='/views/setup.xhtml' ? 'active':''}">
<h:link outcome="/views/setup" value="Theatre setup" /></li>
        <li class="#{view.viewId =='/views/book.xhtml' ? 'active':''}">
<h:link  outcome="/views/book" value="Book tickets" /></li>
        </ul>
    </div>
    </div>
    </div>

    <div class="container">
        <ui:insert name="content">
            [Template content will be inserted here]
        </ui:insert>
        <hr />
        <footer>
        <p class="text-muted">&copy; Company 2014</p>
        </footer>
    </div>
    <h:outputScript name="/webjars/jquery/1.11.0/jquery.js" />
    <h:outputScript name="/webjars/bootstrap/3.2.0/js/bootstrap.js " />
</h:body>
</html>

接下来,我们将转向setup.xhtml中的内容:

<ui:composition 

                template="/WEB-INF/templates/default.xhtml"
                >
<div class="jumbotron">
    <h1>Theatre Setup</h1>
    <p>Enter the information about Seats</p>
</div>

<div class="row">
<div class="col-md-6">
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">Add seats</h3>
        </div>
        <div class="panel-body">
            <h:form id="reg" role="form">
        <div class="form-group has-feedback #{!desc.valid? 'has-error' : ''}">
            <h:outputLabel for="desc" value="Description"
                        styleClass="control-label" />
            <h:inputText id="desc" value="#{newSeatType.description}"
            p:placeholder="Enter a description here" class="form-control"
                        binding="#{desc}" />
            <span class="#{!desc.valid ? 'glyphicon glyphicon-remove form-control-feedback' : ''}" />
            <h:message for="desc" errorClass="control-label has-error" />
        </div>
        <div class="form-group  #{!price.valid and facesContext.validationFailed? 'has-error' : ''}">
<h:outputLabel for="price" value="Price:"
                        styleClass="control-label" />
            <div class="input-group  has-feedback">
            <span class="input-group-addon">$</span>
            <h:inputText id="price" value="#{newSeatType.price}"
            class="form-control" p:placeholder="Enter a price"
                            binding="#{price}" />
            <span class="#{!price.valid ? 'glyphicon glyphicon-remove input-group-feedback input-group-addon' : ''}" />
            </div>
            <h:message for="price" errorClass="control-label has-error" />
        </div>
        <div class="form-group has-feedback #{!quantity.valid and facesContext.validationFailed? 'has-error' : ''}">
            <h:outputLabel for="quantity" value="Number of Seats:"
                    styleClass="control-label" />
            <h:inputText id="quantity" value="#{newSeatType.quantity}"
                class="form-control" p:placeholder="Enter quantity"
                        binding="#{quantity}" />
            <span class="#{!quantity.valid ? 'glyphicon glyphicon-remove form-control-feedback' : ''}" />
            <h:message for="quantity" errorClass="control-label has-error" />
        </div>
        <div class="form-group">
            <h:outputLabel for="position" value="Position:"
                styleClass="control-label" />
            <h:selectOneMenu value="#{newSeatType.position}" id="position"
                class="form-control">
            <f:selectItems value="#{theatreSetupService.positions}"
                var="pos" itemValue="#{pos}" itemLabel="#{pos.label}" />
            </h:selectOneMenu>
        </div>

            <div class="form-group">
<h:commandButton id="Add" action = "#{theatreSetupService. addNewSeats}" value="Add styleClass="btn btn-primary" />
            </div>
            <h:messages styleClass="messages" style="list-style: none; padding:0; margin:0;" errorClass="alert alert-error" infoClass="alert alert-success"
            warnClass="alert alert-warning" globalOnly="true" />
            </h:form>
        </div>
    </div> 
</div>
// some code
</div>
</ui:define>
</ui:composition>

如你所见,前面的视图在顶部部分包含一个表单,用于输入新的座位类型。高亮显示的输入文本实际上会将数据传递给SeatType对象,该对象将被传输到TheatreSetupService CDI Bean,并在用户点击添加按钮时最终持久化。

你可能也会注意到标签上有很多class属性。这些属性指的是 Bootstrap 定义的 CSS 类;我们使用它们来可视化我们的验证。如果用户在表单输入中放置了一些无效数据,就会给它分配一个合适的 CSS 类(在我们的例子中是 Bootstrap 的has-error方法)。然而,这只是一个严格的前端相关添加。JSF 验证消息将随着h:messages标签和在本章前面部分定义的 Bean Validation 约束一起显示或隐藏。

一个有趣的补充是,我们使用了 JSF 2.2 的一个特性,它简化了与 HTML5 前端框架的集成,即pass-through属性。通过在p:placeholder中使用xmlns:p=http://xmlns.jcp.org/jsf/passthrough命名空间,我们指示 JSF 忽略一个未知属性并将其直接传递给渲染器。然后,Bootstrap 的内部机制可以解释该属性,并为我们的输入控件提供占位符文本,该文本在控件获得焦点后消失。

setup.xhtml文件的下一部分在以下代码中可用:

<div class="col-md-6">
    <div class="panel panel-default">
        <div class="panel-heading">
            <h2 class="panel-title">Seats List</h2>
        </div>
        <div class="panel-body">
            <h:form id="reg2">
                <h:commandButton id="Finish"
                    action="#{theatreSetupService.createTheatre}"
                    value="Finalize the theatre setup"
                    styleClass="btn btn-default  btn-block" />
            </h:form>
        </div>
        <h:panelGroup rendered="#{empty seatTypes}">
            <em>No Seats Added.</em>
        </h:panelGroup>
        <h:dataTable var="seatType" value="#{seatTypes}"
            rendered="#{not empty seatTypes}"
            styleClass="table table-hover table-striped ">
            <h:column>
                <f:facet name="header">Id</f:facet>
                 #{seatType.id}
             </h:column>
            <h:column>
                <f:facet name="header">Name</f:facet>
                 #{seatType.description}
             </h:column>
            <h:column>
                <f:facet name="header">Position</f:facet>
                 #{seatType.position.label}
             </h:column>
            <h:column>
                <f:facet name="header">Price</f:facet>
                 $ #{seatType.price}
             </h:column>
            <h:column>
                <f:facet name="header">Quantity</f:facet>
                 #{seatType.quantity}
             </h:column>
        </h:dataTable>

    </div>
</div>
</div>
</ui:define>

每次你为你的剧院添加一个新的座位块时,屏幕下方的dataTable方法将被更新。当你完成设置后,点击完成按钮,这将调用TheatreSetupService CDI Bean 的finish方法,创建座位列表。

此操作还将你重定向到下一个视图,名为book.xhtml,用于预订座位:

    <ui:define name="content">
    <div class="page-header">
        <h2>TicketBooker Machine</h2>
    </div>

    <h3>Money: $ #{bookerService.money}</h3>

    <h:form id="reg">
        <h:messages styleClass="messages"
            style="list-style: none; padding:0; margin:0;"
            errorClass="alert alert-error" infoClass="alert alert-success"
            warnClass="alert alert-warning" globalOnly="true" />

        <h:commandButton id="restart" action="#{theatreSetupService.restart}"
            value="Restart Application" class="btn btn-default" />

        <h:dataTable var="seat" value="#{seats}"
            rendered="#{not empty seats}"
            styleClass="table table-hover table-striped ">

            <h:column>
                <f:facet name="header">Id</f:facet>
                    #{seat.id}
            </h:column>

            <h:column>
                <f:facet name="header">Description</f:facet>
                    #{seat.seatType.description}
            </h:column>
            <h:column>
                <f:facet name="header">Price</f:facet>
                    #{seat.seatType.price}$
            </h:column>
            <h:column>
                <f:facet name="header">Position</f:facet>
                    #{seat.seatType.position.label}
            </h:column>
            <h:column>
                <f:facet name="header">Booked</f:facet>
                <span class="glyphicon glyphicon-#{seat.booked ? 'ok' :'remove'}"></span>
            </h:column>
            <h:column>
                <f:facet name="header">Action</f:facet>
                <h:commandButton id="book" 
                    action="#{bookerService.bookSeat(seat.id, seat.seatType.price)}" 
                    disabled="#{seat.booked}" class="btn btn-primary"   
                    value="#{seat.booked ? 'Reserved' : 'Book'}" />
            </h:column>
        </h:dataTable>
    </h:form>
</ui:define>

这是项目的快照,展开到webapp级别(正如你所见,我们还包括了一个基本的index.html屏幕和一个index.xhtml屏幕,用于将用户重定向到初始屏幕setup.xhtml):

编写 JSF 视图

运行示例

部署应用程序通常需要使用以下 Maven 目标进行打包:

mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------------------------------------------------
[INFO] Building ticket-agency-jpa 1.0 
[INFO] ---------------------------------------------------------------
[INFO] Building war: C:\chapter5\ticket-agency-jpa\target\ticket-agency-jpa.war
. . . . 
[INFO] ---------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ---------------------------------------------------------------
[INFO] Total time: 1.799s

最后,假设你已经安装了 WildFly Maven 插件,你可以使用以下命令部署你的应用程序:

mvn wildfly:deploy

一旦部署成功完成,请访问http://localhost:8080/ticket-agency-jpa/以查看应用程序的欢迎页面,如下面的截图所示:

运行示例

恭喜!你已经完成了。点击设置剧院链接,你可以在setup.xhtml页面开始创建地点。请随意尝试输入一些字母到价格框或数字到描述中,如下面的截图所示:

运行示例

一旦你点击了完成剧院设置按钮,你将被重定向到最后一个屏幕,在该屏幕上在book.xhtml进行座位预订:

运行示例

摘要

新的 Java 持久化 API 的目标是简化持久化实体的开发。它通过一个简单的基于 POJO 的持久化模型来实现这一目标,该模型减少了所需的类和接口的数量。

在本章中,我们从使用 Eclipse 的 JBoss 工具插件逆向工程数据库模式开始,覆盖了大量的内容。接下来,我们编写了应用程序的一部分层(生产者、服务和控制器),以及 JSF Facelets。

在下一章中,我们将通过介绍使用新简化的 Java EE 7 API 的消息驱动 Bean 的示例来讨论使用JBoss 消息提供程序HornetQ)开发应用程序。

第六章:使用 JBoss JMS 提供程序开发应用程序

消息传递是软件组件和应用程序之间的一种通信方法。Java 消息服务(JMS)是一个 Java API——最初由 Sun 设计——允许应用程序创建、发送、接收和读取消息。API 的新 2.0 版本随着 JSR 343(jcp.org/en/jsr/detail?id=343)被引入。

消息传递与其他标准协议(如远程方法调用(RMI)或超文本传输协议(HTTP))在两个方面有所不同。首先,对话是通过消息服务器介导的,因此不是对等体之间的双向对话。其次,发送者和接收者需要知道使用什么消息格式和目标。这与需要应用程序了解远程应用程序方法的紧密耦合技术(如 RMI)形成对比。

在本章中,我们将涵盖以下内容:

  • 消息导向系统的简要介绍

  • JBoss 消息子系统的构建块

  • 设置概念验证编程示例

  • 如何使用 JMS 和资源适配器与外部系统集成

JMS 简述

JMS 定义了一套供应商中立的(但 Java 特定的)编程接口,用于与异步消息系统交互。消息传递实现了松散耦合的分布式通信。整个消息交换是一个两步过程,其中组件向目的地发送消息,然后由 JMS 服务器介导的接收者检索。在 JMS 中,有两种类型的目的地:主题和队列。它们有不同的语义,将在下面解释。

在点对点模型中,消息通过队列从生产者发送到消费者。一个特定的队列可能有多个接收者,但只有一个接收者能够消费每条消息。只有第一个请求消息的接收者会得到它,而其他人则不会,如下面的图像所示:

JMS 简述

相反,发送到主题的消息可能被多个当事人接收。发布在特定主题上的消息会被所有已注册(订阅)接收该主题消息的消息消费者接收。订阅可以是持久的非持久的。非持久订阅者只能接收在其活动期间发布的消息。非持久订阅不能保证消息的投递;它可能会多次投递相同的消息。另一方面,持久订阅保证消费者恰好接收一次消息,如下面的图像所示:

JMS 简述

就消息消费而言,尽管 JMS 本质上是异步的,但 JMS 规范允许以以下两种方式之一消费消息:

  • 同步:订阅者或接收者通过调用任何 MessageConsumer 实例的 receive() 方法显式地从目的地获取消息。receive() 方法可以阻塞,直到有消息到达,或者如果消息在指定的时间限制内没有到达,可以超时。

  • 异步:在异步模式下,客户端必须实现 javax.jms.MessageListener 接口并重写 onMessage() 方法。每当有消息到达目的地时,JMS 提供者通过调用监听器的 onMessage 方法来投递消息,该方法作用于消息的内容。

一个 JMS 消息由一个报头、属性和主体组成。消息报头提供一组固定的元数据字段,用于描述消息,例如消息的去向和接收时间。属性是一组键值对,用于特定应用目的,通常用于在接收时快速过滤消息。最后,主体包含发送到消息中的任何数据。

JMS API 支持两种消息投递模式,用于指定在 JMS 提供者失败的情况下,消息是否会丢失,以下常量表示:

  • 持久投递模式,这是默认模式,指示 JMS 提供者要格外小心,确保在 JMS 提供者失败的情况下,消息在传输过程中不会丢失。使用此投递模式发送的消息在发送时会被记录到稳定的存储中。

  • 非持久投递模式不需要 JMS 提供者存储消息或保证在提供者失败的情况下不会丢失。

JMS 的基本构建块

任何 JMS 应用程序的基本构建块包括以下内容:

  • 管理对象——连接工厂和目的地

  • 连接

  • 会话

  • 消息生产者

  • 消息消费者

  • 消息

让我们更详细地看看它们:

  • 连接工厂:此对象封装了一组由管理员定义的连接配置参数。客户端使用它来与 JMS 提供者建立连接。连接工厂隐藏了特定于提供者的细节,并将管理信息抽象为 Java 编程语言中的对象。

  • 目的地:这是客户端用于指定其产生的消息的目标和其消费的消息的来源的组件。在 点对点PTP)消息域中,目的地被称为队列;在 发布/订阅pub/sub)消息域中,目的地被称为主题。

  • 连接:这封装了一个与 JMS 提供程序的虚拟连接。一个连接可以代表客户端和提供程序服务之间的一个打开的 TCP/IP 套接字。您使用连接来创建一个或多个会话。

  • 会话:这是一个用于生产和消费消息的单线程上下文。您使用会话来创建消息生产者、消息消费者和消息。会话序列化消息监听器的执行,并提供一个事务上下文,可以将一系列发送和接收操作组合成一个原子的工作单元。

  • 消息生产者:这是一个由会话创建的对象,用于向目的地发送消息。消息生产者的 PTP 形式实现了QueueSender接口。发布/订阅形式实现了TopicPublisher接口。从 JMS 2.0 开始,可以仅依赖于JMSProducer接口。

  • 消息消费者:这是一个由会话创建的对象,用于接收发送到目的地的消息。消息消费者允许 JMS 客户端向 JMS 提供程序注册对目的地的兴趣。JMS 提供程序管理从目的地到已注册消费者目的地的消息传递。消息消费者的 PTP 形式实现了QueueReceiver接口。发布/订阅形式实现了TopicSubscriber接口。最新的 JMS 版本支持新的JMSConsumer API。

JBoss 消息子系统

JBoss AS 在其各个版本中使用了不同的 JMS 实现,例如 JBoss MQ 和 JBoss Messaging。自 JBoss AS 6.0 以来,默认的 JMS 提供程序是HornetQwww.jboss.org/hornetq),它提供了一个多协议、可嵌入、高性能且可集群的消息系统。

在其核心,HornetQ 被设计成一套简单的普通 Java 对象POJOs),对外部 JAR 文件的依赖很少。实际上,唯一的 JAR 依赖是 Netty 库,它利用 Java 新输入输出NIO)API 来构建高性能网络应用程序。

由于其易于适应的架构,HornetQ 可以嵌入到您自己的项目中,或者在任何依赖注入框架中实例化,例如 JBossMicrocontainer、Spring 或 Google Guice。

在本书中,我们将介绍一个场景,其中 HornetQ 作为模块集成到 WildFly 子系统,如下所示。此图展示了 JCA 适配器和 HornetQ 服务器如何在整体图中定位:

The JBoss messaging subsystem

创建和使用连接工厂

连接工厂封装连接参数的职责是创建新的 JMS 连接。连接工厂绑定到Java 命名目录索引JNDI),并且只要它们提供正确的环境参数,本地和远程客户端都可以查找。由于连接工厂可以在你的代码中多次重用,它是一种可以被远程客户端或消息驱动豆方便地缓存的类型。

连接工厂实例的定义包含在fullfull-ha服务器配置中。你可以使用–c命令参数选择任一服务器配置,例如,standalone.bat –c standalone-full.xml。我们将在第九章中深入讨论配置配置文件,管理应用服务器。现在,只需记住,每次你需要 JMS 时,都要使用完整配置配置文件启动你的服务器。

你可以通过浏览管理控制台并导航到配置 | 消息目的地 | 连接工厂来检查整体 JMS 配置中的连接工厂,如下面的截图所示:

创建和使用连接工厂

如前一个截图所示,有以下两个内置的连接工厂定义:

  • InVmConnectionFactory: 这个连接工厂在java:/ConnectionFactory条目下进行绑定,并且当服务器和客户端是同一进程的一部分时(即,它们在同一个 JVM 上运行)使用。

  • RemoteConnectionFactory: 这个连接工厂在java:jboss/exported/jms/RemoteConnectionFactory条目下进行绑定,正如其名称所暗示的,当由远程服务器提供 JMS 连接时,可以使用 Netty 作为连接器来使用。

如果你想要更改连接工厂的 JNDI 绑定,最简单的方法是通过服务器配置文件(例如,对于独立模式,standalone-full.xml):

<connection-factory name="InVmConnectionFactory">
      <connectors>
        <connector-ref connector-name="in-vm"/>
   </connectors>
   <entries>
 <entry name="java:/ConnectionFactory"/>
   </entries>
</connection-factory>
<connection-factory name="RemoteConnectionFactory">
   <connectors>
     <connector-ref connector-name="http-connector"/>
   </connectors>
   <entries>
 <entry name="java:jboss/exported/jms/RemoteConnectionFactory"/>
   </entries>
</connection-factory>
<pooled-connection-factory name="hornetq-ra">
<transaction mode="xa"/>
   <connectors>
      <connector-ref connector-name="in-vm"/>
   </connectors>
   <entries>
 <entry name="java:/JmsXA"/>
 <entry name="java:jboss/DefaultJMSConnectionFactory"/>
    </entries>
</pooled-connection-factory>

连接工厂可以像任何其他 Java EE 资源一样注入;以下代码片段显示了无状态 EJB 如何注入默认连接工厂:

@Stateless
public class SampleEJB {

 @Resource(mappedName = "java:/ConnectionFactory")
 private ConnectionFactory cf; 
}

注意

为了使用消息子系统,你必须使用 Java EE 完整配置启动 WildFly,该配置包括消息子系统。例如,如果你想启动一个具有 JMS 感知能力的独立服务器实例,你可以简单地使用以下代码:

standalone.sh –c standalone-full.xml

使用 JMS 目的地

除了连接工厂的定义外,你还需要了解如何配置 JMS 目的地(队列和主题)。

这可以通过各种工具实现。由于我们已经开始处理 Web 控制台,只需导航到配置选项卡,并从左侧面板中选择消息子系统。选择目的地并单击查看中央链接。

从那里,你可以使用包含一组选项的上层菜单标签,其中第一个选项——命名为队列/主题——可以用来配置你的 JMS 目的地,如下面的截图所示:

使用 JMS 目的地

现在点击添加按钮。你应该会看到以下对话框:

使用 JMS 目的地

输入您目的地的必填名称及其 JNDI。您可以选择性地定义您的 JMS 目的地为以下任一选项:

  • 持久性:此选项允许 JMS 服务器在订阅者暂时不可用的情况下保留消息。

  • 选择器:此选项允许对 JMS 目的地进行过滤(我们将在本章后面更详细地介绍)。

点击保存按钮并验证队列是否已列入 JMS 目的地之中。

前面的更改将在服务器配置文件中反映如下:

<jms-destinations>
   <jms-queue name="TicketQueue">
      <entry name="java:jboss/jms/queue/ticketQueue"/>
         <durable>false</durable>
   </jms-queue>
</jms-destinations>

值得注意的是,JMS 配置通常在每个应用程序服务器上都有所不同。在本章中,我们将只介绍在 WildFly 中使用的方案,但不同供应商之间的关键概念保持相同。

将消息驱动 Bean 添加到您的应用程序

一旦我们完成配置,我们就可以开始编写 JMS 消息消费者,例如消息驱动 Bean。

消息驱动 Bean(MDB)是无状态的、服务器端和事务感知的组件,用于处理异步 JMS 消息。

消息驱动 Bean 最重要的一个方面是它们可以并发地消费和处理消息。这种能力在传统的 JMS 客户端上提供了显著的优势,因为传统的 JMS 客户端必须被定制构建以管理多线程环境中的资源、事务和安全。MDB 容器自动管理并发,这样 Bean 开发者就可以专注于处理消息的业务逻辑。一个 MDB 可以从各种应用程序接收数百条 JMS 消息,并且可以同时处理它们,因为它的多个实例可以在容器中并发执行。

从语义角度来看,消息驱动 Bean(MDB)被归类为企业 Bean,就像会话或实体 Bean 一样,但有一些重要的区别。首先,消息驱动 Bean 没有组件接口。这是因为消息驱动 Bean 不能通过 Java RMI API 访问;它只响应异步消息。

正如实体和会话 Bean 有明确的生命周期一样,MDB Bean 也是如此。MDB 实例的生命周期有两个状态,不存在方法就绪池,如下面的图像所示:

将消息驱动 Bean 添加到您的应用程序

当接收到消息时,EJB 容器会检查池中是否有可用的 MDB 实例。如果免费池中有可用的 Bean,JBoss 将使用该实例。一旦 MDB 实例的onMessage()方法返回,请求就完成了,并将实例放回免费池。这导致最佳响应时间,因为请求是在不等待创建新实例的情况下得到服务的。

注意

另一方面,如果池中的所有实例都忙碌,新的请求将被序列化,因为可以保证同一实例不会被允许同时为多个客户端提供服务。此外,如果客户端向服务器发送包含 MDB 的多个消息,不能保证每个消息都使用相同的 MDB 实例,或者消息将按照客户端发送的顺序处理。这意味着应用程序应该设计为处理乱序到达的消息。

池中 MDB 的数量在 EJB 池中配置,可以通过控制台导航到配置 | 容器 | EJB 3 | Bean Pools来访问,如下面的截图所示:

将消息驱动的 Bean 添加到你的应用程序

Bean 池的配置包含在 Bean 池中心标签中,其中包含无状态和 MDB 池配置。MDB 的最大池大小默认值为 20 个单位。

还可以覆盖特定 Bean 的池。你可以使用 JBoss 特定的org.jboss.ejb3.annotation.Pool注释或jboss-ejb3.xml部署描述符。有关覆盖所选 Bean 的池的更多信息,请访问docs.jboss.org/author/display/WFLY8/EJB3+subsystem+configuration+guide

如果没有可用的 Bean 实例,请求将被阻塞,直到一个活动的 MDB 完成方法调用或事务超时。

消息驱动的 Bean 配置

我们现在将从前一章的应用程序中添加一个消息驱动的 Bean,该 Bean 将用于在预订新票时拦截消息。为了我们的示例,我们只需跟踪 JMS 消息是否已接收;然而,你也可以用它来实现更复杂的目的,例如通知外部系统。

创建一个新的 Java 类,例如BookingQueueReceiver,并将包名输入为com.packtpub.wflydevelopment.chapter6.jms

完成后,让我们通过注释添加 MDB 配置,如下所示:

package com.packtpub.wflydevelopment.chapter6.jms;

import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.inject.Inject;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import java.util.logging.Logger;

@MessageDriven(name = "BookingQueueReceiver", activationConfig = {
 @ActivationConfigProperty(propertyName = "destinationLookup",
 propertyValue = "java:jboss/jms/queue/ticketQueue"),  [1]
 @ActivationConfigProperty(propertyName = "destinationType",
 propertyValue = "javax.jms.Queue"),}
)
public class BookingQueueReceiver implements MessageListener {

    @Inject
    private Logger logger;

    @Override
    public void onMessage(Message message) {
        try {
            final String text = message.getBody(String.class);
            logger.info("Received message " + text);
        } catch (JMSException ex) {
            logger.severe(ex.toString());
        }
    }
}

在这里,我们已经将 MDB 连接到了我们的ticketQueue目标 [1],该目标绑定在java:jboss/jms/queue/ticketQueue。该组件的目的将是通过java.util.Logger跟踪消息接收。

Java EE 7 引入了队列定义的另一种方式。现在,你不必从应用服务器管理工具中添加队列。你可以使用一些基本注解在代码中定义队列及其属性:

package com.packtpub.wflydevelopment.chapter6.jms;

import javax.jms.JMSDestinationDefinition;

@JMSDestinationDefinition(
        name = BookingQueueDefinition.BOOKING_QUEUE,
        interfaceName = "javax.jms.Queue"
)
public class BookingQueueDefinition {

    public static final String BOOKING_QUEUE = "java:global/jms/bookingQueue";
}

然后,在BookingQueueReceiver中,你只需将propertyValue = "java:jboss/jms/queue/ticketQueue"更改为propertyValue = BookingQueueDefinition.BOOKING_QUEUE

添加 JMS 生产者

一旦我们完成 JMS 消费者,我们需要一个组件来处理发送 JMS 消息。为此,我们将添加一个应用范围 CDI Bean,比如BookingQueueProducer,它被注入到 JMS 资源中:

package com.packtpub.wflydevelopment.chapter6.jms;

import javax.annotation.Resource;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.jms.JMSContext;
import javax.jms.Queue;

@ApplicationScoped
public class BookingQueueProducer {

    @Inject
    private JMSContext context;
    @Resource(mappedName = BookingQueueDefinition.BOOKING_QUEUE)
    private Queue syncQueue;

    public void sendMessage(String txt) {
        context.createProducer().send(syncQueue, txt);
    }
}

这可能对那些使用过 JMS 先前版本的人来说有些震惊。对于那些还没有使用过的人来说,在下面的代码中,我们展示了 JMS 1.1 中此代码的等效版本:

package com.packtpub.wflydevelopment.chapter6.jms;

Import javax.annotation.Resource;
Import javax.enterprise.context.ApplicationScoped;
Import javax.jms.*;
Import java.util.logging.Logger;

@ApplicationScoped
public class BookingQueueProducer {

    @Inject
    private Logger logger;

    @Resource(mappedName = "java:/ConnectionFactory")
    private ConnectionFactorycf;

    @Resource(mappedName = BookingQueueDefinition.BOOKING_QUEUE)
    private Queue queueExample;  

    public void sendMessage(String txt) {
        try {
            final Connection connection = cf.createConnection();
            Session session = connection
                  .createSession(false, Session.AUTO_ACKNOWLEDGE);

            final MessageProducer publisher = 
                session.createProducer(queueExample);

            connection.start();

            final TextMessage message = 
                session.createTextMessage(txt);
            publisher.send(message);
        }
        catch (Exception exc) {
           logger.error("Error ! "+exc);
        }
        finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (JMSException e) { 
                    logger.error(e); 
                } 
            }
        }
    }
}

代码量变化令人印象深刻。API 简化是新 JMS 版本的主要特性之一,规范作者在这方面做得很好。

现在,你可以使用你的服务来通知一些特定于应用程序的操作。例如,我们将BookingQueueProducer注入到BookerServiceBean 中,并在用户注册时发送消息:

public class BookerService implements Serializable {

 @Inject
 private BookingQueueProducer bookingQueueProducer;

    // Some code

    public void bookSeat(long seatId, int price) {
        logger.info("Booking seat " + seatId);

        if (price > money) {
            final FacesMessage m = 
                  new FacesMessage(FacesMessage.SEVERITY_ERROR, 
                    "Not enough Money!",
                    "Registration successful");
            facesContext.addMessage(null, m);
            return;
        }

        ticketService.bookSeat(seatId);

        final FacesMessage m = 
              new FacesMessage(FacesMessage.SEVERITY_INFO, 
                  "Registered!", 
                  "Registration successful");
        facesContext.addMessage(null, m);
        logger.info("Seat booked.");

        money = money - price;

 bookingQueueProducer.sendMessage("[JMS Message] User registered seat " + seatId);
    }
    // Some code

}

从 JMS 2.0 开始,消息可以异步发送,但此时重要的是要控制操作是否成功。为此,我们必须创建一个实现CompletionListener接口的对象,如下所示:

@ApplicationScoped
public class BookingCompletionListener implements CompletionListener {

    @Inject
    private Logger logger;

 @Override
 public void onCompletion(Message message) {
        try {
            final String text = message.getBody(String.class);
            logger.info("Send was successful: " + text));
        } catch (Throwable e) {
            logger.severe("Problem with message format");
        }
    }

 @Override
 public void onException(Message message, Exception exception) {
        try {
            final String text = message.getBody(String.class);
            logger.info("Send failed..." + text);
        } catch (Throwable e) {
            logger.severe("Problem with message format");
        }
    }
}

send操作期间,我们必须指定异步并使用此listener对象。为此,将BookingCompletionListener注入到BookingQueueProducer中,并使用更新后的调用发送消息:

public void sendMessage(String txt) {
    context.createProducer()
        .setAsync(bookingCompletionListener).send(syncQueue, txt);
}

现在,当消息send完成或失败时,将执行适当的监听器方法:

[com.packtpub.wflydevelopment.chapter6.jms.BookingCompletionListener] (Thread-3 (HornetQ-client-global-threads-269763340)) Send was successful: [JMS Message] User registered seat 2 

编译和部署应用程序

我们的代码基于上一章中的 JPA 应用程序。多亏了javaee-api,你不需要添加任何新的项目依赖项来使用 JMS!你唯一需要做的是使用例如standalone-full.xml的标准完整配置来启动 WildFly 的全配置模式:

standalone.sh –c standalone-full.xml

注意

记住,当切换到另一个服务器配置时,你需要重新创建所有最初为你的独立配置设置的资源,例如数据源。

现在你可以使用 Eclipse 的服务器视图或 Maven 部署你的应用程序,并通过http://localhost:8080/ticket-agency-jms/访问应用程序。

一切都应该像早期的 JPA 项目一样工作;然而,在你的应用服务器控制台中,你应该注意到确认座位已预订的消息。

使用选择器指定要接收的消息

消息选择器允许 MDB 对从特定主题或队列接收的消息有更多的选择性。消息选择器使用消息属性作为条件表达式的标准。消息选择器基于的消息属性是可以分配给消息的附加头信息。它们给应用程序开发者提供了附加更多信息的可能性。这些信息可以使用几个原始值(booleanbyteshortintlongfloatdouble)或作为String存储。

例如,假设我们想要使用同一个队列处理两种类型的消息:

  • 一个指示用户已预订座位的跟踪消息

  • 一个指示发生错误的警告消息

因此,我们的sendMessage方法可以稍作修改,包括一个可以附加到消息上的String属性:

@ApplicationScoped
public class BookingQueueProducer {

    @Inject
    private JMSContext context;

    @Inject
    private BookingCompletionListener bookingCompletionListener;

    @Resource(mappedName = BookingQueueDefinition.BOOKING_QUEUE)
    private Queue syncQueue;

    public void sendMessage(String txt, Priority priority) {
        context.createProducer()
                .setAsync(bookingCompletionListener)
                .setProperty("priority", priority.toString())
                .send(syncQueue, txt);
    }
} 

public enum Priority {
    LOW, HIGH
}

现在,在我们的应用程序上下文中,我们可能会使用sendMessage方法,当用户注册时附加一个LOW优先级值:

bookingQueueProducer.sendMessage("[JMS Message] User registered seat " + seatId, Priority.LOW);

另一方面,当发生错误时,我们可以附加一个HIGH优先级:

bookingQueueProducer.sendMessage("Error during Transaction", Priority. HIGH);

从 MDB 的角度来看,你只需要在ActivationConfigProperty类中包含消息选择器,如下所示,以便过滤消息:

@MessageDriven(name = "BookingQueueReceiver", activationConfig = {
        @ActivationConfigProperty(propertyName = "destinationLookup",
                propertyValue = BookingQueueDefinition.BOOKING_QUEUE),
        @ActivationConfigProperty(propertyName = "destinationType",
                propertyValue = "javax.jms.Queue"),
        @ActivationConfigProperty(propertyName = "messageSelector",
 propertyValue = "priority = 'HIGH'"),}
)
public class BookingQueueReceiver implements MessageListener {

    // Some code 
}

同时,你可以部署另一个负责消费带有LOW优先级消息的 MDB:

@MessageDriven(name = " LowPriorityBookingQueueReceiver", activationConfig = {
        @ActivationConfigProperty(propertyName = "destinationLookup",
                propertyValue = BookingQueueDefinition.BOOKING_QUEUE),
        @ActivationConfigProperty(propertyName = "destinationType",
                propertyValue = "javax.jms.Queue"),
 @ActivationConfigProperty(propertyName = "messageSelector",
 propertyValue = "priority = 'LOW'"),}
)
public class LowPriorityBookingQueueReceiver implements MessageListener {

    // Some code
} 

当谈到过滤时,我们必须说几句关于性能的话。在 HornetQ 队列和主题中,过滤消息发生在不同的阶段。在队列的情况下,当属性已经被监听器接收时,它们会被过滤,而在主题中,它们在添加之前被过滤。请记住,这并不由 JMS 规范保证(因为规范描述了 API),并且可能在其他实现中有所不同。JMS 提供者中有许多可以调整的性能选项;然而,大多数配置必须为每个项目专门选择。请确保在 HornetQ 文档中查看额外的调整提示,链接为docs.jboss.org/hornetq/2.4.0.Final/docs/user-manual/html_single/#perf-tuning

事务和确认模式

为了控制异步消息系统的整体性能和可靠性,我们需要考虑两个因素:消息的持久化和确认。让我们看看这些特性。

系统的可靠性集中在能够精确地发送消息一次的能力。这意味着没有消息丢失,也没有重复。对于大多数系统来说,不遗漏或重复任何订单(如电子商务网站上的订单)是一个强烈的要求。然而,通常错过股市更新并不是问题,因为更新的信息很快就会覆盖它。当然,额外的功能如可靠性是有代价的,在 JMS 的情况下,代价是性能。系统越可靠,其消息吞吐量就越低。

当处理消息时,它只能保存在内存中或在磁盘上的某个位置。保存在内存中的消息在失败或消息服务停止时丢失。持久化的消息在服务重启后可以从磁盘检索,因此至少被投递给消费者一次(但仍没有任何关于确认的保证)。如果没有这种机制,消息可能会因为失败发生在它们被投递之前而丢失。然而,存储它们的开销可能会对系统的性能特性产生严重影响。

确认对于通知 JMS 服务消息确实被接收和处理非常重要。可以使用不同级别的确认来避免重复或触发 JMS 再次发送消息,可能发送给另一个消费者。JMS 提供者将确保已确认的消息只被投递一次。应用程序负责正确处理已重新投递的回滚消息(这些消息带有JMSRedelivered头)。

如果消费者会话在事务中处理,则只有在事务提交时才会确认消息。然而,可以选择禁用事务性消息驱动豆并手动处理确认。在这种情况下,有以下三种确认选项:

  • AUTO_ACKNOWLEDGE:使用此选项,消费的消息将被自动确认

  • DUPS_OK_ACKNOWLEDGE:使用此选项,已发送的消息将被延迟确认;这意味着客户端可能会接收到一些重复的消息

  • CLIENT_ACKNOWLEDGES:使用此选项,客户端通过acknowledge方法手动确认接收到的消息

可以在从连接工厂检索JMSContext时设置模式:

JMSContext context = connectionFactory.createContext(JMSContext.CLIENT_ACKNOWLEDGE)

第一个参数是一个整数标志,接受之前提到的值以及SESSION_TRANSACTED条目(这是 JTA 管理的消息驱动豆的标准模式)。

使用 JMS 与外部系统集成

在本章的开头,我们提到 JCA 适配器负责处理应用服务器和 HornetQ 服务器之间的通信。

实际上,执行企业应用集成EAI)的一种可能方式是通过Java 连接器架构JCA),它可以用来驱动 JMS 的入站和出站连接。

最初,Java 连接器旨在以同步请求/回复模式访问大型机上的遗留事务服务器,这就是大多数连接器最初是如何工作的。该标准目前正在向更异步和双向连接发展;这正是 JMS 通信的情况,它本质上是异步的(但也提供了模拟同步请求/回复模式的能力)。在下一节中,我们将向您展示如何使用 Java 资源适配器来启用 JBoss HornetQ 消息系统与 Apache ActiveMQ 代理(例如,由非 Java EE 应用程序使用)之间的通信。

小贴士

JMS/JCA 集成与 Web 服务

如果我们在讨论 EAI,我们不可避免地要谈到 Web 服务之间的区别,这是异构系统集成的既定标准。

使用 JMS/JCA 集成的一个优点是它提供了对资源适应性的支持,将 Java EE 安全、事务和通信池映射到相应的 EIS 技术。这使得这项技术相当有吸引力,尤其是如果你正在尝试连接一些现有的、稳固的、同质化的系统(记住,如果你使用 JMS 作为驱动,你将绑定到 Java 到 Java 的交互)。

另一方面,如果你计划连接不同的商业伙伴(例如,Java 和.NET 应用)或者从头开始构建一个没有明确交互定义的新系统,使用 Web 服务进行传输和连接会更好。

我们将在第七章中了解更多关于 Web 服务的内容,向您的应用程序添加 Web 服务,这应该会为你提供一个相当完整的 EAI 替代方案概述。

一个现实世界的例子——HornetQ 和 ActiveMQ 集成

在本节中,我们将提供一个示例场景,其中包括一个外部组件,例如完全实现Java 消息服务 1.1JMS)的 Apache ActiveMQ(Apache 2.0 开源许可)消息代理。另一个应用可能是使用此代理与我们的票务系统进行通信,但在我们的示例中,我们将使用 ActiveMQ 管理控制台来模拟外部系统。

为了运行此示例,我们需要选择 ActiveMQ 资源适配器 activemq-rar-5.9.0.rar,可以从 Maven 仓库 repo1.maven.org/maven2/org/apache/activemq/activemq-rar/5.9.0/ 下载。你还需要 ActiveMQ 代理,你可以从 activemq.apache.org/activemq-590-release.html 下载。只需提取二进制发行版并运行 /apache-activemq-5.9.0/bin/activemq.bat 文件来启动代理。

安装 ActiveMQ 资源适配器

资源适配器(.rar)可以使用 WildFly 管理工具或通过将资源适配器复制到独立服务器的部署目录中来部署。在这样做之前,我们需要在服务器配置中配置资源适配器。这可以通过将配置添加到 JCA 子系统或(建议选择)创建外部资源的 JCA 描述符来完成。

JCA 描述符可以通过使用 JBoss JCA 实现中包含的一个实用工具创建,该工具名为 IronJacamar (www.jboss.org/ironjacamar). 在 IronJacamar 1.1 或更高版本的发行版(可在 www.jboss.org/ironjacamar/downloads 访问)中,你可以找到一个资源适配器信息工具 (rar-info.bat),它可以用来通过生成包含所有必要信息的报告文件来创建资源适配器部署描述符。

rar-info.bat 工具可以在你的 IronJacamar 发行版的 doc/as 文件夹中找到。所以让我们转到这个文件夹:

$ cd doc/as

现在执行以下命令,假设你已经将你的资源适配器保存在 /usr/doc 文件夹中:

rar-info.bat /usr/doc/activemq-rar-5.9.0.rar

提示

解决 rar-info 命令行问题

rar-info 命令行包括一组库,用于执行主实用程序类。然而,为了检查 JMS 适配器,你需要手动编辑命令行文件,并将 jboss-jms-api_2.0_spec-1.0.0.Finaljboss-transaction-api_1.2_spec-1.0.0.Final.jar 添加到类路径中。这些 JAR 文件位于 JBOSS_HOME/modules/system/layers/base/javax/jms/api/JBOSS_HOME/modules/system/layers/base/javax/transaction/api/ 的主文件夹中。只需在 rar-info.bat 文件中添加它们的路径(用字符分隔);例如,参考以下内容(假设 JAR 文件与 rar-info.bat 在同一目录中):

java -classpath ironjacamar-as.jar;..\..\lib\ironjacamar-common-spi.jar;..\..\lib\jboss-logging.jar;..\..\lib\jboss-common-core.jar;..\..\lib\ironjacamar-spec-api.jar;..\..\lib\jandex.jar;..\..\lib\ironjacamar-common-impl.jar;..\..\lib\ironjacamar-common-api.jar;..\..\lib\ironjacamar-core-impl.jar;..\..\lib\ironjacamar-core-api.jar;..\..\lib\ironjacamar-validator.jar;..\..\lib\jandex.jar;..\..\lib\validation-api.jar;..\..\lib\hibernate-validator.jar;jboss-jms-api_2.0_spec-1.0.0.Final.jar;jboss-transaction-api_1.2_spec-1.0.0.Final.jar org.jboss.jca.as.rarinfo.Main %*

这将生成一个名为 activemq-rar-5.9.0-report.txt 的文件,它将为你提供构建自己的 JBoss JCA 配置文件所需的信息,该文件需要命名为 ironjacamar.xml。请随意查看其内容。

在以下代码中,你可以找到一个示例 ironjacamar.xml 文件,它定义了一个新的队列 (java:jboss/activemq/queue/TicketQueue):

<ironjacamar>
     <connection-definitions>
        <connection-definition class-name="org.apache.activemq.ra.ActiveMQManagedConnectionFactory" jndi-name="java:jboss/activemq/TopicConnectionFactory" pool-name="TopicConnectionFactory">
     <pool>
        <min-pool-size>1</min-pool-size>
        <max-pool-size>200</max-pool-size>
        <prefill>false</prefill>
      </pool>
      <security>
        <application />
      </security>
      <timeout>
        <blocking-timeout-millis>30000</blocking-timeout-millis>
        <idle-timeout-minutes>3</idle-timeout-minutes>
      </timeout>
      <validation>
        <background-validation>false</background-validation>
        <use-fast-fail>false</use-fast-fail>
      </validation> 
   </connection-definition>
   <connection-definition class-name="org.apache.activemq.ra.ActiveMQManagedConnectionFactory" jndi-name="java:jboss/activemq/QueueConnectionFactory" pool-name="QueueConnectionFactory">
      <pool>
        <min-pool-size>1</min-pool-size>
        <max-pool-size>200</max-pool-size>
        <prefill>false</prefill>
      </pool>
      <security>
        <application />
      </security>
      <timeout>
        <blocking-timeout-millis>30000</blocking-timeout-millis>
        <idle-timeout-minutes>3</idle-timeout-minutes>
      </timeout>
      <validation>
        <background-validation>false</background-validation>
        <use-fast-fail>false</use-fast-fail>
      </validation>
    </connection-definition>
    </connection-definitions>
     <admin-objects>
 <admin-object class-name="org.apache.activemq.command.ActiveMQQueue" jndi-name="java:jboss/activemq/queue/TicketQueue">
 <config-property name="PhysicalName">
 activemq/queue/TicketQueue
 </config-property>
    </admin-object>
    </admin-objects>
</ironjacamar>

如您所见,此文件包含 ActiveMQ 连接工厂的定义以及 JMS 管理对象的映射,这些将被资源适配器导入。ironjacamar.xml文件需要复制到activemq-rar-5.9.0.rarMETA-INF文件夹中(您可以使用您选择的压缩文件管理器打开 RAR 文件,例如 7-Zip)。

小贴士

资源适配器的附加配置要求

除了ironjacamar.xml文件外,还有一个配置文件包含在您的activemq-rar-5.9.0.rar文件的META-INF文件夹中。ra.xml文件是标准的 JCA 配置文件,描述了资源适配器相关属性的类型及其部署属性。然而,对于我们的基本示例,我们不需要修改其内容。

现在我们已经完成了配置,让我们将资源适配器(activemq-rar-5.9.0.rar)部署到我们的 WildFly 中,并检查 JCA 工厂和对象是否已正确绑定到应用服务器。部署后,您应该在 WildFly 的控制台中看到以下类似的消息:

19:52:51,521 INFO  [org.jboss.as.connector.deployment] (MSC service thread 1-5) JBAS010401: Bound JCA AdminObject [java:jboss/activemq/queue/TicketQueue]
19:52:51,521 INFO  [org.jboss.as.connector.deployment] (MSC service thread 1-5) JBAS010401: Bound JCA ConnectionFactory [java:jboss/jms/TopicConnectionFactory]
19:52:51,521 INFO  [org.jboss.as.connector.deployment] (MSC service thread 1-8) JBAS010401: Bound JCA ConnectionFactory [java:jboss/jms/ConnectionFactory]
19:52:51,542 INFO  [org.jboss.as.server] (DeploymentScanner-threads - 1) JBAS018559: Deployed "activemq-rar-5.9.0.rar" (runtime-name : "activemq-rar-5.9.0.rar")

消费 ActiveMQ 消息

干得好!最困难的部分已经完成。现在,为了消费 ActiveMQ 代理发送的 JMS 消息,我们将在消息驱动 Bean 上添加一个@ResourceAdapter注解。这个消息驱动 Bean 将拦截来自 ActiveMQ 代理的预订。为了能够使用@ResourceAdapter注解,我们需要在我们的pom.xml中添加一个 JBoss 特定的依赖项:

         <dependency>
            <groupId>org.jboss.ejb3</groupId>
            <artifactId>jboss-ejb3-ext-api</artifactId>
            <version>2.1.0</version>
            <scope>provided</scope>
        </dependency>

我们的新注解消息 Bean 如下所示(注意,属性destinationType现在是目标):

@MessageDriven(name = "MDBService", activationConfig = {
 @ActivationConfigProperty(propertyName = "destination",
 propertyValue = "java:jboss/activemq/queue/TicketQueue"),
        @ActivationConfigProperty(propertyName = "destinationType",
                propertyValue = "javax.jms.Queue"),}
)
@ResourceAdapter(value="activemq-rar-5.9.0.rar")
public class BookingQueueReceiver implements MessageListener {

    @Inject
    private Logger logger;

    @Override
    public void onMessage(Message message) {
        try {
             final String text = message.getBody(String.class);
            logger.info("Received message " + text);
        } catch (JMSException ex) {
            logger.severe(ex.toString());
        }
    }
}

一旦收到消息,它就会被写入控制台。这意味着是时候部署我们的应用程序了。如果您的 ActiveMQ 代理正在运行,您应该在部署阶段看到以下类似的消息:

19:59:59,452 INFO  [org.apache.activemq.ra.ActiveMQEndpointWorker] (ServerService Thread Pool -- 65) Starting
19:59:59,458 INFO  [org.apache.activemq.ra.ActiveMQEndpointWorker] (default-threads - 1) Establishing connection to broker [tcp://localhost:61616]
19:59:59,573 INFO  [javax.enterprise.resource.webcontainer.jsf.config] (MSC service thread 1-5) Initializing Mojarra 2.2.5-jbossorg-3 20140128-1641 for context '/ticket-agency-jms'
19:59:59,618 INFO  [org.apache.activemq.ra.ActiveMQEndpointWorker] (default-threads - 1) Successfully established connection to broker [tcp://localhost:61616]
20:00:00,053 INFO  [org.wildfly.extension.undertow] (MSC service thread 1-5) JBAS017534: Registered web context: /ticket-agency-jms
20:00:00,081 INFO  [org.jboss.as.server] (DeploymentScanner-threads - 1) JBAS018559: Deployed "ticket-agency-jms.war" (runtime-name : "ticket-agency-jms.war")

现在是时候使用 ActiveMQ 控制台测试我们的连接了,它将直接向 ActiveMQ 代理发送消息。ActiveMQ 5.9.0 配备了捆绑的hawt.io控制台。这是一个可插拔的 Web 仪表板,可以配置来管理各种应用程序。其中之一是 ActiveMQ。使用此控制台的一个好处是,您几乎可以在任何基于 JVM 的容器上部署它,包括 WildFly。查看hawt.io/以及 ActiveMQ 插件([hawt.io/plugins/activemq/](http://hawt.io/plugins/activemq/))以获取更多信息。

注意

从版本 5.10.0 开始,ActiveMQ 不再预捆绑 hawt.io。您可以通过遵循http://hawt.io/getstarted/index.html上的指南来准备自己的 hawt.io 控制台;安装 ActiveMQ 插件;或者(我们强烈推荐)在您的示例中使用 5.9.0 版本,它已经方便地预配置了。

前往http://localhost:8161/hawtio/并使用admin/admin凭据登录:

消费 ActiveMQ 消息

登录后,你应该会看到 hawt.io 网络控制台。值得注意的是,它是使用 Twitter Bootstrap 创建的,这是我们应用程序中使用的相同的前端框架。

选择第一个标签页(ActiveMQ),你应该会看到一个表示代理当前配置的树形结构。找到节点localhost/Queue/。当你展开它时,你应该会看到我们在资源适配器中定义的队列:java_jboss/activemq/queue/TicketQueue。选择它后,你可以在右侧选择发送标签页。你应该会看到一个类似于以下屏幕的界面:

消费 ActiveMQ 消息

在中心的较大文本区域中输入所需的消息,然后点击发送消息按钮。切换到我们的 WildFly 控制台后,我们应该会看到一个日志条目,其中包含我们传递给 ActiveMQ 代理的消息,如下面的截图所示:

消费 ActiveMQ 消息

恭喜!如果你已经成功完成了这个示例,那么你已经掌握了一个真实世界的集成场景。为了让这个示例更加真实,你可以改进消息 bean,使其在消息包含所需信息时预订票务(例如,消息55,10将预订 ID 为 55 的座位,价格为 10 美元)。请随意尝试!

摘要

在本章中,我们讨论了 JBoss 的消息中间件,它允许你将异构系统松散耦合在一起,同时通常提供可靠性、事务和其他许多功能。

我们看到了如何使用网络控制台配置 JMS 目的地并创建一些消息驱动 bean,这是在 EJB 容器内消费消息的标准方式。

我们现在将转向另一个组件,它通常用于集成异构系统——Web 服务。

第七章。将网络服务添加到您的应用程序中

在上一章中,我们讨论了 Java 消息服务 API,它通常用于开发松散耦合的应用程序,以及 Java 到 Java 系统的通用集成模式。在本章中,你将了解由 W3C 定义的作为软件系统的网络服务,以及它们旨在支持通过网络进行可互操作机器到机器交互的设计。

使网络服务与其他分布式计算形式不同的因素是,信息仅通过简单和非专有协议进行交换。这意味着服务可以无论位置、平台或编程语言如何相互通信。本质上,网络服务协议提供了一种平台无关的方式来执行远程过程调用RPCs)。

本章的重点将集中在两个主要的网络服务标准上,即JAX-WSJSR 224)和JAX-RSJSR 339),以及它们在 WildFly 中的实现方式。正如你所想象的那样,这里有很多内容需要覆盖,因此我们将快速进入以下主题:

  • 基于 SOAP 的网络服务的简要介绍

  • 创建、部署和使用 JBoss JAX-WS 实现(Apache CXF)

  • 对 REST 网络服务的快速概述

  • 如何使用 JBoss JAX-RS 实现(RESTEasy)创建、部署和使用服务

  • 将 JAR-RS 与外部非 Java 应用程序集成

开发基于 SOAP 的网络服务

正如所述,网络服务基于使用非专有协议消息的消息交换。这些消息本身不足以定义网络服务平台。我们实际上需要一个包括以下内容的标准化组件列表:

  • 一种用于以不依赖于它运行的平台或实现它的编程语言的方式定义网络服务提供的接口的语言

  • 一种在 web 服务提供者和 web 服务消费者之间交换消息的通用标准格式

  • 一个可以放置服务定义的注册表

网络服务描述语言WSDL),也称为WSDL,(www.w3.org/TR/wsdl)是提供对客户端公开的网络服务合同描述的事实标准。特别是,一个 WSDL 文档描述了一个提供操作的 web 服务,以及每个操作所需的输入数据类型和可以以结果形式返回的数据类型。

服务提供者与服务消费者之间的通信是通过依赖于 SOAP 规范的 XML 消息来实现的。

一个基本的 SOAP 消息由一个可能包含任意数量头部的信封和一个主体组成。这些部分由称为envelopeheaderbody的 XML 元素界定,这些元素属于由 SOAP 规范定义的命名空间。以下图展示了 SOAP 消息的基本结构:

开发基于 SOAP 的网络服务

构建基于 SOAP 的网络服务的策略

正如我们刚才讨论的,服务描述是由一个常用的文档接口 WSDL 提供的,它使用 XML 格式将服务作为一组网络、端点和端口公开。

你可能会逻辑上倾向于认为,在服务合同的开始处声明相应的编程接口,然后生成它们是必要的。

实际上,你可以遵循两种方法来开发你的 SOAP 网络服务:

  • 自上而下:这种开发策略涉及从 WSDL 文件创建网络服务。当从头开始创建网络服务时,可能会使用自上而下的方法。这是纯网络服务工程师的首选选择,因为它是业务驱动的,也就是说,合同是由业务人员定义的,因此软件的设计是为了适应网络服务合同。

  • 自下而上:这种方法要求由编程接口生成 WSDL 文件。当我们希望将现有应用程序作为网络服务公开时,可能会使用这种方法。由于这种方法不需要对 WSDL 语法的深入了解,如果你想把 Java 类或 EJB 转换为网络服务,它是最容易的选择。

由于本书的受众主要由对 WSDL 基础知识了解甚少或没有的 Java 开发者组成,我们将主要关注自下而上的方法。

另一方面,设计自上而下的网络服务将需要你将本章提供的基本网络服务概念与对 WSDL 标准的全面了解相结合。

JBoss 基于 SOAP 的网络服务栈

目前在 WildFly 上提供的所有 JAX-WS 功能都是通过将 JBoss 网络服务栈与 Apache CXF 项目的大部分功能适当集成来提供的。

Apache CXF 是一个开源的网络服务框架,它提供了一个易于使用的、基于标准的编程模型来开发 SOAP 和 REST 网络服务。集成层(以下简称 JBossWS-CXF)允许我们执行以下操作:

  • 在 WildFly 应用服务器上使用标准的网络服务 API(包括 JAX-WS);这是通过利用 Apache CXF 内部执行的,无需用户处理。

  • 在 WildFly 应用服务器上利用 Apache CXF 的高级原生特性,无需用户处理运行该容器中的应用所需的所有集成步骤

因此,下一节的重点将是使用内置的 Apache CXF 配置开发 JAX-WS 网络服务。如果你想进一步扩展你对 Apache CXF 原生特性的知识,你可以参考官方文档,该文档可在cxf.apache.org/找到。

简要了解 JAX WS 架构

当客户端发送的 SOAP 消息进入网络服务运行环境时,它被一个名为服务器端点监听器的组件捕获,该组件随后使用调度器模块将 SOAP 消息传递到该服务。

在这一点上,HTTP 请求被内部转换为 SOAP 消息。消息内容从传输协议中提取出来,并通过为该网络服务配置的处理程序链进行处理。

SOAP 消息处理程序用于拦截 SOAP 消息,在它们从客户端到端点服务以及相反方向传输的过程中。这些处理程序拦截了网络服务的请求和响应中的 SOAP 消息。

下一步是将 SOAP 消息反序列化为 Java 对象。此过程由 WSDL 到 Java 映射和 XML 到 Java 映射控制。前者由 JAX-WS 引擎执行,它确定从 SOAP 消息中调用哪个端点。后者由 JAXB 库执行,它将 SOAP 消息反序列化,以便它可以调用端点方法。

最后,反序列化的 SOAP 消息到达实际的网络服务实现,并调用该方法。

一旦调用完成,过程就会逆转。网络服务方法返回的值使用 JAX-WS WSDL 到 Java 映射和 JAXB 2.0 XML 到 Java 映射打包成一个 SOAP 响应消息。

注意

JAXB 提供了一种快速便捷的方式将 XML 模式与 Java 表示绑定,使得 Java 开发者能够轻松地将 XML 数据和过程函数集成到 Java 应用程序中。作为此过程的一部分,JAXB 提供了将 XML 实例文档反序列化为 Java 内容树的方法,然后将 Java 内容树反序列化为 XML 实例文档的方法。JAXB 还提供了一种从 Java 对象生成 XML 模式的方法。

接下来,出站消息在返回给调度器和端点监听器之前,由处理程序进行处理,这些调度器和端点监听器将消息作为 HTTP 响应传输。

以下图表描述了数据如何从网络服务客户端流向网络服务端点,并返回:

简要了解 JAX WS 架构

使用 WildFly 编写 SOAP 网络服务代码

在第一个交付成果中,我们将展示如何轻松地将一个普通的 Java 类转换为网络服务。然后,使用基于 Eclipse 的简单测试 GUI 对该新创建的服务进行测试。本节第二部分将关注如何通过增强您的票务应用程序以网络服务的方式公开 EJB 作为网络服务端点。

开发 POJO 网络服务

我们将开始开发 Web 服务,使用我们的项目从第四章,学习上下文和依赖注入 (ticket-agency-cdi)作为基础。现在我们将省略基于 JSF 的当前 Web 层。您可以安全地删除所有与 JSF 相关的类和配置。如果您遇到任何问题,请记住,在本章完成后,您将在代码示例中找到一个完全工作的项目。

我们的第一类将与我们的票务应用程序无关,但它将仅演示如何从一个名为CalculatePowerWebServicePOJO类创建 Web 服务。这个类有一个名为calculatePower的方法,它返回一个参数的幂,如下面的高亮代码所示:

package com.packtpub.wflydevelopment.chapter7.boundary;

public class CalculatePowerWebService {

    public double calculatePower(double base, double exponent) {
        return Math.pow(base, exponent);
    }
}

现在,我们将通过添加必需的@WebService注解将这个简单的类转换成 Web 服务:

package com.packtpub.wflydevelopment.chapter7.webservice;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService(targetNamespace = "http://www.packtpub.com/",
 serviceName = "CalculatePowerService")
@SOAPBinding(style = SOAPBinding.Style.RPC)
public class CalculatePowerWebService {

    @WebMethod
    @WebResult(name = "result")
    public double calculatePower(@WebParam(name = "base") double base,
                                 @WebParam(name = "exponent") double exponent) {
        return Math.pow(base, exponent);
    }
}

@WebService注解内部,您可以指定额外的元素,例如声明由 Web 服务生成的 WSDL 元素的命名空间的targetNamespace元素。如果您不指定此元素,Web 服务容器将使用 Java 包名来生成默认的 XML 命名空间。

您还可以使用serviceName元素来指定服务名称。使用serviceName指定的名称用于生成 WSDL 接口中服务元素的名称属性。如果您不指定serviceName元素,服务器将使用默认值生成它,默认值是 bean 类名后追加的服务。

在下一行中,我们使用@javax.jws.SOAPBinding注解声明 Web 服务是远程过程调用类型。可能的值是DOCUMENTRPC,第一个是默认值。

注意

RPC 和文档风格之间的选择归结为我们可以使用这两种风格构建服务的不同方式。RPC 风格 SOAP 消息的体是以特定方式构建的,这在 SOAP 标准中定义。这是基于您想要像调用应用程序代码中的普通函数或方法一样调用 Web 服务的假设。

因此,RPC 更紧密地耦合,因为如果您在消息结构中做出任何更改,您需要更改所有处理此类消息的客户端和服务器。

另一方面,文档风格的 Web 服务对 SOAP 体如何构建没有限制。它允许您包含任何想要的 XML 数据以及该 XML 的模式。因此,文档风格可能更灵活,但实现 Web 服务和客户端的努力可能稍微多一点。

最后,改变的可能性是选择是否使用 RPC 或文档风格 Web 服务时必须考虑的一个因素。

@WebMethod 属性附加到公共方法上表示您希望将方法公开作为网络服务的一部分。

@WebParam 注解用于指定需要在 WSDL 中展示的参数名称。您应该始终考虑使用 WebParam 注解,尤其是在使用多个参数时,否则 WSDL 将使用默认的参数(在这种情况下,arg0),这对网络服务消费者来说是没有意义的。

@WebResult 注解在意义上与 @WebParam 非常相似,因为它可以用来指定由 WSDL 返回的值的名称。

您的网络服务现在已完成。为了部署您的网络服务,运行以下 Maven 目标,这将打包并部署您的网络服务到您的运行 WildFly 实例:

mvn package wildfly:deploy 

WildFly 将在控制台上提供最小输出;这将通知您网络服务项目已部署,WSDL 文件已生成:

14:25:37,195 INFO  [org.jboss.weld.deployer] (MSC service thread 1-11) JBAS016005: Starting Services for CDI deployment: ticket-agency-ws.war
14:25:37,198 INFO  [org.jboss.ws.cxf.metadata] (MSC service thread 1-11) JBWS024061: Adding service endpoint metadata: id=com.packtpub.wflydevelopment.chapter7.boundary.CalculatePowerWebService
 address=http://localhost:8080/ticket-agency-ws/CalculatePowerService
 implementor=com.packtpub.wflydevelopment.chapter7.boundary.CalculatePowerWebService
 serviceName={http://www.packtpub.com/}CalculatePowerService
 portName={http://www.packtpub.com/}CalculatePowerWebServicePort
 annotationWsdlLocation=null
 wsdlLocationOverride=null
 mtomEnabled=false

从简短的日志中,您可以获取一些有用的信息。例如,第一行表明网络服务已在端点注册表中绑定为 {http://www.packtpub.com/}CalculatePowerService。接下来是有关网络上下文路径的信息,默认情况下,它与您的项目名称相同,即 ticket-agency-ws。最后一条信息是关于网络服务地址的,它是 http://localhost:8080/ticket-agency-ws/CalculatePowerService。通过在地址末尾附加 ?wsdl 后缀,您可以检查网络服务合约。

注意

data 目录包含所有生成的 WSDL 的版本化列表。因此,您可能会在 JBOSS_HOME/standalone/data/wsdl/ticket-agency-ws.war 中找到由 ticket-agency-ws 发布的网络服务的整个历史记录。

从控制台检查网络服务

您可以通过转到网络管理控制台并导航到 Runtime | Status | Subsystems | Web Services 来检查网络服务子系统。

在这里,您可以收集有关已部署服务的有用信息。实际上,最有用的选项是可用的端点合约列表,这在开发我们的客户端时是必需的。以下截图显示了从控制台查看的网络服务端点:

从控制台检查网络服务

尤其是在屏幕的下半部分,你可以阅读包含网络应用程序上下文名称和注册的网络服务名称的网络服务端点地址。在我们的例子中,它是 http://localhost:8080/ticket-agency-ws/CalculatePowerService?wsdl

测试我们的简单网络服务

由于我们的第一个网络服务尚未连接到我们的票务系统,我们将使用外部客户端应用程序来测试我们的网络服务。测试网络服务最好的工具之一是 SoapUI

SoapUI 是一个免费、开源、跨平台的函数测试解决方案,具有易于使用的图形界面和企业级功能。此工具允许您轻松快速地创建和执行自动化、功能、回归、合规性和负载测试。SoapUI 还可用作 Eclipse 插件。

在这里,我们将使用 SoapUI 独立应用程序。运行它,创建一个新的 SOAP 项目,提供服务的 WSDL URL,如图下截图所示:

测试我们的简单网络服务

之后,您将看到一个包含几个窗口的视图。其中最重要的窗口显示请求日志和导航窗口中的项目视图,如图下截图所示:

测试我们的简单网络服务

如您所见,您的服务操作已被自动发现。双击请求 1树元素;SoapUI 请求窗口将出现,您可以在其中输入命名参数。输入网络服务的两个参数,如图下截图所示:

测试我们的简单网络服务

点击工具栏上的提交按钮,并在 SOAP 响应窗口中检查结果:

测试我们的简单网络服务

EJB3 无状态会话 Bean(SLSB)网络服务

JAX-WS 编程模型支持与 EJB3 无状态会话 Bean 在 POJO 端点上的相同一组注解。既然我们已经有一些网络服务经验,我们将构建本书中介绍的一个示例。

我们的主要网络服务类将被命名为DefaultTicketWebService,并使用我们在第三章中描述的一些核心类,例如TheatreBox,它将在内存中保留票务预订和Seat类作为模型。我们的网络服务业务方法将由一个名为服务端点接口SEI)的TicketWebService来描述:

package com.packtpub.wflydevelopment.chapter7.boundary;

import javax.jws.WebService;
import java.util.List;

@WebService
public interface TicketWebService {

    List<SeatDto> getSeats();

    void bookSeat(int seatId);
}

注意

编写服务接口始终是一个好习惯,因为它为我们提供了服务方法的适当客户端视图。然后实现类可以实现对接口中定义的方法。

我们现在将通过在DefaultTicketWebService类中提供业务逻辑到接口方法来实现该接口:

package com.packtpub.wflydevelopment.chapter7.boundary;

import javax.inject.Inject;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;

@WebService(targetNamespace = "http://www.packtpub.com/", serviceName = "TicketWebService")
public class DefaultTicketWebService implements TicketWebService, Serializable {

    @Inject
    private TheatreBox theatreBox;

    @WebMethod
 @WebResult(name = "listSeats")
    public List<SeatDto> getSeats() {
        return theatreBox.getSeats()
                         .stream()
                         .map(SeatDto::fromSeat)
                         .collect(Collectors.toList());
    }

 @WebMethod
    public void bookSeat(@WebParam(name = "seatId") int seatId) {
        theatreBox.buyTicket(seatId);
    }
}

如您所见,实现类包含getSeats方法,该方法返回当TheatreBox对象初始化时自动生成的座位列表。bookSeat方法将为您的网络服务客户端预订座位。

现在部署您的网络服务,并在控制台上验证它是否已正确注册:

00:43:12,033 INFO  [org.jboss.ws.cxf.metadata] (MSC service thread 1-13) JBWS024061: Adding service endpoint metadata: id=com.packtpub.wflydevelopment.chapter7.boundary.DefaultTicketWebService
 address=http://localhost:8080/ticket-agency-ws/TicketWebService
 implementor=com.packtpub.wflydevelopment.chapter7.boundary.DefaultTicketWebService
 serviceName={http://www.packtpub.com/}TicketWebService
 portName={http://www.packtpub.com/}DefaultTicketWebServicePort
 annotationWsdlLocation=null
 wsdlLocationOverride=null
 mtomEnabled=false

开发网络服务消费者

TicketWebService类的 Web 服务消费者将使用标准的 Java SE 类进行编码。我们在这里想展示如何使用这些标准 API。因此,你只需在你的当前项目或单独的项目中添加一个名为TicketWebServiceTestApplication的类到包com.packtpub.wflydevelopment.chapter7.webservice

package com.packtpub.wflydevelopment.chapter7.webservice;

import com.packtpub.wflydevelopment.chapter7.boundary.SeatDto;
import com.packtpub.wflydevelopment.chapter7.boundary.TicketWebService;

import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;

public class TicketWebServiceTestApplication {

    private static final Logger logger = Logger.getLogger(TicketWebServiceTestApplication.class.getName());

    public static void main(String[] args) throws MalformedURLException {
        final int seatId = 1;
        logger.info("TEST SOAP WS Service");
        final URL wsdlURL = new URL("http://localhost:8080/ticket-agency-ws/TicketWebService?wsdl");
        final QName SERVICE_NAME = new QName("http://www.packtpub.com/", "TicketWebService");
        final Service service = Service.create(wsdlURL, SERVICE_NAME);
        final TicketWebService infoService = service.getPort(TicketWebService.class);

        logger.info("Got the Service: " + infoService);

        infoService.bookSeat(seatId);
        logger.info("Ticket Booked with JAX-WS Service");

        final List<SeatDto> list = infoService.getSeats();

        dumpSeatList(list);
    }

    private static void dumpSeatList(Collection<SeatDto> list) {
        logger.info("================= Available Ticket List ================");
        list.stream().forEach(seat -> logger.info(seat.toString()));
    }
}

需要服务 WSDL URL 和名称来检索Service对象。最后,getPort方法将返回一个代理到你的 Web 服务,你可以用它来测试两个基本操作:预订座位和从Seat列表中检查座位是否已被预留。

这个小型独立类展示了从客户端视角使用基于 SOAP 的服务是如何可能的。

然而,最有趣的部分是在 Maven 输出的底部,其中在预订一个座位后,Ticket列表被输出,如下面的命令行所示:

apr 01, 2014 1:08:44 AM com.packtpub.wflydevelopment.chapter7.webservice.TicketWebServiceTestApplication main
INFO: TEST SOAP WS Service
apr 01, 2014 1:08:44 AM com.packtpub.wflydevelopment.chapter7.webservice.TicketWebServiceTestApplication main
INFO: Got the Service: JAX-WS RI 2.2.9-b130926.1035 svn-revision#8c29a9a53251ff741fca1664a8221dc876b2eac8: Stub for http://localhost:8080/ticket-agency-ws/TicketWebService
apr 01, 2014 1:08:44 AM com.packtpub.wflydevelopment.chapter7.webservice.TicketWebServiceTestApplication main
INFO: Ticket Booked with JAX-WS Service
apr 01, 2014 1:08:44 AM com.packtpub.wflydevelopment.chapter7.webservice.TicketWebServiceTestApplication dumpSeatList
INFO: ================= Available Ticket List ================
apr 01, 2014 1:08:44 AM com.packtpub.wflydevelopment.chapter7.webservice.TicketWebServiceTestApplication lambda$dumpSeatList$0
INFO: SeatDto [id=1, name=Stalls, price=40, booked=true]
apr 01, 2014 1:08:44 AM com.packtpub.wflydevelopment.chapter7.webservice.TicketWebServiceTestApplication lambda$dumpSeatList$0
INFO: SeatDto [id=2, name=Stalls, price=40, booked=false]
…

开发基于 REST 的 Web 服务

JAX-RS 2.0(JSR-339 可以在jcp.org/en/jsr/detail?id=339找到)是一个 JCP 规范,它为 HTTP 协议中的 RESTful Web 服务提供了 Java API。这是从旧版本 1.1 的重大更新。一些新特性包括客户端 API、HATEOAS 支持以及异步调用。

在最简单的形式中,RESTful Web 服务是网络应用程序,它操作系统资源的状态。在这个上下文中,资源操作意味着资源的创建、检索、更新和删除(CRUD)。然而,RESTful Web 服务并不仅限于这四个基本的数据操作概念。相反,RESTful Web 服务可以在服务器级别执行逻辑,但请记住,每个结果都必须是域的资源表示。

与 SOAP Web 服务的主要区别是,REST 要求开发者显式地使用 HTTP 方法,并且与协议定义保持一致。这个基本的 REST 设计原则在 CRUD 操作和 HTTP 方法之间建立了一个一对一的映射。

因此,有了对资源和表示的明确角色划分,我们现在可以将我们的 CRUD 动作映射到 HTTP 方法POSTGETPUTDELETE,如下所示:

动作 HTTP 协议等效
RETRIEVE GET
CREATE POST
UPDATE PUT
DELETE DELETE

访问 REST 资源

正如我们所说,REST 资源可以通过映射等效 HTTP 请求的动作来访问。为了简化 REST 应用程序的开发,你可以使用简单的注解来映射你的动作;例如,为了从你的应用程序中检索一些数据,你可以使用以下类似的方法:

@Path("/users")
public class UserResource {

 @GET
    public String handleGETRequest() { . . .}

 @POST
    public String handlePOSTRequest(String payload) { . . . }
}

在我们的示例中,第一个注解@Path用于指定分配给此 Web 服务的 URI。后续的方法都有其特定的@Path注解,这样你就可以根据请求的 URI 提供不同的响应。

然后,我们有一个 @GET 注解,它映射 HTTP GET 请求,还有一个 @POST 注解,它处理 HTTP POST 请求。所以,在这个例子中,如果我们请求绑定到 example 网络上下文的 Web 应用程序,对 URL http://host/example/users 的 HTTP GET 请求将触发 handleGETRequest 方法;另一方面,对同一 URL 的 HTTP POST 请求将相反地调用 handlePOSTRequest 方法。

JBoss REST 网络服务

在理解了 REST 服务的基础知识之后,让我们看看如何使用 WildFly 开发一个 RESTful 网络服务。应用程序服务器包含一个开箱即用的 RESTEasy 库,它是 JSR-339 规范的可移植实现。RESTEasy 可以在任何 Servlet 容器中运行;然而,它与 WildFly 完美集成,因此在那个环境中为用户提供更好的体验。

除了服务器端规范之外,在以前,RESTEasy 通过 RESTEasy JAX-RS 客户端框架将 JAX-RS 带到客户端方面进行了创新。然而,JAX-RS 规范的最新版本附带了一个客户端 API,我们可以在每个 JAX-RS 实现中使用它。

激活 JAX-RS

RESTEasy 与 WildFly 打包在一起,因此你几乎不需要做任何努力就可以开始使用。你有两个选择。第一个是在扩展 javax.ws.rs.core.Application 的类中使用 @ApplicationPath 注解:

@ApplicationPath("/rest")
public class JaxRsActivator extends Application {

}

第二种选择不太受欢迎,它用于通过 web.xml 文件配置应用程序:

<?xml version="1.0" encoding="UTF-8"?>
<web-app  
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_3_0.xsd" version="3.0">
    <servlet>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>javax.ws.rs.core.Application</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>

这仅仅意味着,如果我们部署之前的示例,HTTP GET 方法,http://host/example/rest/users 将触发我们的 getUser 业务方法,而相同的 URL 将通过 POST 请求通过 handlePOSTRequest 方法发送请求。

将 REST 添加到我们的票务示例中

在所有配置就绪后,我们现在可以向我们的 Ticket Web Service 项目添加一个简单的 REST 网络服务,它将提供与我们的 SOAP 网络服务相同的功能。

所以在你的项目中添加一个新类,并将其命名为 SeatsResource。这个类的代码如下:

package com.packtpub.wflydevelopment.chapter7.boundary;

@Path("/seat")
@Produces(MediaType.APPLICATION_JSON)
@RequestScoped
public class SeatsResource {

    @Inject
    private TheatreBooker theatreBooker;

    @Inject
    private TheatreBox theatreBox;

 @GET
    public Collection<SeatDto> getSeatList() {
        return theatreBox.getSeats()
                 .stream()
                 .map(SeatDto::fromSeat)
                 .collect(Collectors.toList());
    }

 @POST
    @Path("/{id}")
    public Response bookPlace(@PathParam("id") int id) {
        try {
            theatreBooker.bookSeat(id);
            return Response.ok(SeatDto.fromSeat(theatreBox.getSeat(id)))
             .build();
        } catch (Exception e) {
            final Entity<String> errorMessage = Entity
                                         .json(e.getMessage());
            return Response.status(Response.Status.BAD_REQUEST)
                       .entity(errorMessage).build();
        }
    }
}

如果你已经很好地理解了我们前面的部分,这段代码对你来说几乎会直观易懂。我们在这里包含了两个方法,就像 SOAP 的对应物一样;前者命名为 getSeatList,它与 HTTP GET 请求绑定并生成 Seats 列表。列表使用 JSON 表示法返回,这在将 Java 对象返回给客户端时非常常见。

注意

JSON 对象的语法很简单,需要将数据定义和数据值分组;如下所示:

  • 元素被花括号 ({}) 包围

  • 元素的值以 name:value 的结构成对出现,并且用逗号分隔

  • 数组被方括号 ([]) 包围

这就是全部内容(对于完整的 JSON 语法描述,请访问 www.json.org/)。

该类中包含的第二个方法是bookPlace,它将用于调用我们的 EJB 的相应bookSeat类。另一方面,此方法绑定到以下 HTTP POST方法:

@POST
@Path("/{id}")
public Response bookPlace(@PathParam("id") int id)

你可能认为这个Path表达式看起来有点奇怪,但它所做的只是将 URI 参数(包含在Path表达式中)映射到方法参数。简而言之,包含在 URL 中的参数将通过ID变量传递给方法。

之前的方法还返回一个使用 Jackson(默认情况下,你可以创建自己的消息体提供者!)编码和解码的 JSON 格式的字符串,这是一个将 POJO 转换为 JSON(反之亦然)的库。

在我们继续之前,我们需要通过一个新的资源账户扩展我们的样本,这将使我们能够检查现金状态并可选择重置它:

package com.packtpub.wflydevelopment.chapter7.boundary;

@Path("/account")
@Produces(MediaType.APPLICATION_JSON)
@RequestScoped
public class AccountResource {

    @Inject
    private TheatreBooker theatreBooker;

    @GET
    public AccountDto getAccount() {
        return AccountDto
                 .fromAccount(theatreBooker.getCurrentAccount());
    }

    @POST
    public Response renew() {
        theatreBooker.createCustomer();
        return Response
        .ok(AccountDto.fromAccount(theatreBooker.getCurrentAccount()))
        .build();
    }
}

账户表示如下所示:

package com.packtpub.wflydevelopment.chapter7.entity;

public class Account {

    private final int balance;

    public Account(int initialBalance) {
        this.balance = initialBalance;
    }

    public Account charge(int amount) {
        final int newBalance = balance - amount;
        if (newBalance < 0) {
            throw new IllegalArgumentException("Debit value on account!");
        }
        return new Account(newBalance);
    }

    public int getBalance() {
        return balance;
    }

    @Override
    public String toString() {
        return "Account [balance = " + balance + "]";
    }
}

最后一步是将我们的TheatreBooker类更新为使用我们新的账户表示:

    private Account currentAccount;

    @PostConstruct
    public void createCustomer() {
        currentAccount = new Account(100);
    }

    public void bookSeat(int seatId) {
        logger.info("Booking seat " + seatId);
        final int seatPrice = theatreBox.getSeatPrice(seatId);

        if (seatPrice > currentAccount.getBalance()) {
            throw new IllegalArgumentException("Not enough money!");
        }

        theatreBox.buyTicket(seatId);
        currentAccount = currentAccount.charge(seatPrice);

        logger.info("Seat booked.");
    }

    public Account getCurrentAccount() {
        return currentAccount;
    }

注意

JAX-RS 的最新版本也支持服务器端异步响应。多亏了@Suspended注解和AsyncResponse类,你可以使用一个单独的(可能是延迟的)线程来处理请求调用。

添加过滤器

JAX-RS 允许我们为客户端和服务器定义过滤器和拦截器。它们允许开发者处理横切关注点,如安全、审计或压缩。基本上,你可以将过滤器和拦截器视为扩展点。

过滤器主要用于请求和响应的头部。例如,你可以根据其头部字段阻止请求或仅记录失败的请求。相反,拦截器处理消息体,例如,你可以对消息进行签名或压缩。拦截器也有两种类型:一种是读取(当消息被转换为 POJO,例如 JSON 到SeatDto时执行)和一种是写入(用于 POJO 到消息的转换)。

我们可以通过创建以下类向我们的应用程序添加一个简单的服务器端日志过滤程序:

package com.packtpub.wflydevelopment.chapter7.controller;

import java.io.IOException;
import java.util.logging.Logger;

import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;

@Provider
public class LoggingRestFilter implements ContainerRequestFilter, ContainerResponseFilter {

    @Inject
    private Logger logger;

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
            throws IOException {
        logger.info(responseContext.getStatusInfo().toString());
    }

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        logger.info(requestContext.getMethod() + " on " + requestContext.getUriInfo().getPath());
    }
}

如您所见,我们实现了两个相当直接的接口:ContainerRequestFilterContainerResponseFilter。我们只是记录一些关于 HTTP 请求和响应的信息。要激活过滤器,我们使用@Provider注解;在没有额外配置的情况下,过滤器将适用于我们应用程序中的每个 REST 资源。此外,如果我们想在过滤器中拒绝请求,有一个requestContext.abortWith方法。

客户端有两个相应的接口:ClientRequestFilterClientResponseFilter。然而,实现必须手动注册。

现在,REST 服务已经完成,我们可以按照常规方式开始部署它:

mvn package wildfly:deploy

如果你已经遵循了迄今为止的所有步骤,那么由你的浏览器发出的http://localhost:8080/ticket-agency-ws/rest/seat GET方法应该会打印出可用座位的列表:

[{"id":0,"name":"Stalls","price":40,"booked":false},{"id":1,"name":"Stalls","price":40,"booked":false},{"id":2,"name":"Stalls","price":40,"booked":false},{"id":3,"name":"Stalls","price":40,"booked":false},{"id":4,"name":"Stalls","price":40,"booked":false},
. . . . . .

访问http://localhost:8080/ticket-agency-ws/rest/account将导致:

{"balance":100}

你还应该在控制台中看到一些来自我们的过滤器的日志语句,例如:

19:52:45,906 INFO  [com.packtpub.wflydevelopment.chapter7.controller.LoggingRestFilter] (default task-10) GET on /seat
19:52:45,909 INFO  [com.packtpub.wflydevelopment.chapter7.controller.LoggingRestFilter] (default task-10) OK
20:29:04,275 INFO  [com.packtpub.wflydevelopment.chapter7.controller.LoggingRestFilter] (default task-14) GET on /account
20:29:04,313 INFO  [com.packtpub.wflydevelopment.chapter7.controller.LoggingRestFilter] (default task-14) OK

消费我们的 REST 服务

连接到 RESTful Web 服务所需的工作量与直接通过 HTTP 连接连接到服务的工作量相当。因此,你可以使用大量的 API 来访问你的 REST 服务,例如 JDK 的URLConnection类或 Jakarta Commons HttpClient API,因为我们有标准化的客户端可用在 JAX-RS 中。

如果你想从你的 REST 服务中检索Seats列表,你的代码应该看起来像这样:

Client restclient = ClientBuilder.newClient();
WebTarget seatResource = restclient.target(APPLICATION_URL + "seat");
Collection<SeatDto> seats = seatResource.request().get(new GenericType<Collection<SeatDto>>() {});

之前的代码将简单地执行对作为ticket-agency-ws Web 应用程序一部分部署的 REST 服务的GET操作。RESTEasy(使用 Jackson)将转换 JSON 对象。

以下独立示例将从账户和座位资源获取数据,并尝试预订所有可用的座位:

public class RestServiceTestApplication {
    private static final String APPLICATION_URL = "http://localhost:8080/ticket-agency-ws/rest/";

    private WebTarget accountResource;
    private WebTarget seatResource;

    public static void main(String[] args) {
        new RestServiceTestApplication().runSample();
    }

    public RestServiceTestApplication() {
        Client restclient = ClientBuilder.newClient();

        accountResource = restclient.target(APPLICATION_URL + "account");
        seatResource = restclient.target(APPLICATION_URL + "seat");
    }

    public void runSample() {
        printAccountStatusFromServer();

        System.out.println("=== Current status: ");
        Collection<SeatDto> seats = getSeatsFromServer();
        printSeats(seats);

        System.out.println("=== Booking: ");
        bookSeats(seats);

        System.out.println("=== Status after booking: ");
        Collection<SeatDto> bookedSeats = getSeatsFromServer();
        printSeats(bookedSeats);

        printAccountStatusFromServer();
    }

    private void printAccountStatusFromServer() {
 AccountDto account = accountResource.request().get(AccountDto.class);
        System.out.println(account);
    }

    private Collection<SeatDto> getSeatsFromServer() {
 return seatResource.request().get(new GenericType<Collection<SeatDto>>() { });
    }

    private void printSeats(Collection<SeatDto> seats) {
        seats.forEach(System.out::println);
    }

    private void bookSeats(Collection<SeatDto> seats) {
        for (SeatDto seat : seats) {
            try {
                String idOfSeat = Integer.toString(seat.getId());
                seatResource.path(idOfSeat).request().post(Entity.json(""), String.class);
                System.out.println(seat + " booked");
            } catch (WebApplicationException e) {
                Response response = e.getResponse();
                StatusType statusInfo = response.getStatusInfo();
                System.out.println(seat + " not booked (" + statusInfo.getReasonPhrase() + "):" response.readEntity(JsonObject.class). getString("entity"));
            }
        }
    }
}

在高亮显示的片段中,你可以看到用于检索数据和预订座位的 REST 调用。我们的post调用需要指定一个 ID;我们通过使用request构建器的path方法来完成。也可以使用async方法和Future对象来异步执行调用:

Future<Collection<SeatDto>> future = seatResource.request()
        .async().get(new GenericType<Collection<SeatDto>>() {});

我们可以使用 Java 8 中的新CompletableFuture类来通知请求的完成:

CompletableFuture.<Collection<SeatDto>> supplyAsync(() -> {
    try {
        return future.get();
    } catch (Exception e) {
        e.printStackTrace();
        throw new IllegalArgumentException(e);
    }
}).thenAccept(seats -> seats.forEach(System.out::println));

收到数据后,我们只需将其打印出来。另一个选项是简单地创建一个InvocationCallback类,并将其作为第二个参数传递给get方法。

编译我们的票务示例

我们的示例可以位于一个单独的 Maven 模块中,或者你可以将其与服务器内容一起保留(尽管这不是一个好的做法)。为了编译我们的客户端项目与 REST Web 服务,我们需要导入包含在应用程序服务器库中的 JAX-RS API。在我们的独立应用程序中,我们需要以下依赖项:

<properties>
   . . .
    <version.resteasy-client>3.0.6.Final</version.resteasy-client>
</properties>

<dependencies>
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-client</artifactId>
        <version> ${version.resteasy-client}</version>
    </dependency>

    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-json-p-provider</artifactId>
        <version> ${version.resteasy-client}</version>
    </dependency>

    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jackson-provider</artifactId>
        <version> ${version.resteasy-client}</version>
    </dependency>

    <dependency>
        <groupId>com.packtpub.wflydevelopment.chapter7</groupId>
        <artifactId>ticket-agency-ws</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

如果你创建 POM 文件时遇到任何问题,你可以查看本书附带提供的示例。

现在简单地运行你的应用程序,你应该会看到以下类似的控制台输出:

AccountDto [balance=100]
=== Current status: 
SeatDto [id=1, name=Stalls, price=40, booked=false]
SeatDto [id=2, name=Stalls, price=40, booked=false]
SeatDto [id=3, name=Stalls, price=40, booked=false]
SeatDto [id=4, name=Stalls, price=40, booked=false]
SeatDto [id=5, name=Stalls, price=40, booked=false]
SeatDto [id=6, name=Circle, price=20, booked=false]
SeatDto [id=7, name=Circle, price=20, booked=false]
…

添加 AngularJS

我们的 REST 集成示例并不非常引人注目。然而,因为我们通过 REST API 公开了我们的应用程序功能,所以很容易创建一个非 Java GUI,它可以用来控制应用程序。

要创建一个仅使用 REST API 与我们的 Java 后端通信的 GUI,我们将使用一个流行的 JavaScript 框架:AngularJS (angularjs.org/)。我们不会深入探讨 JavaScript 代码的细节。对我们来说,最有趣的部分是使用我们的 REST API,我们目前只在 Java 应用程序中消费它。

如 第五章 所见,我们将使用 WebJars。这次,除了 Bootstrap 之外,我们还需要 AngularJS(最好是 3.x 版本)和 Angular UI Bootstrap 包 (angular-ui.github.io/bootstrap/):

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>angularjs</artifactId>
    <version>1.3.0-rc.1</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>angular-ui-bootstrap</artifactId>
    <version>0.11.0-2</version>
</dependency>

注意

记住,所有运行此示例所需的文件都包含在这本书附带的代码中。

我们需要一个 index.html 文件来开始我们的工作,以及一个空的 scripts 目录来存储我们的逻辑。我们的目录结构目前应该如下所示:

添加 AngularJS

index.html 文件中,我们需要添加所有必需的库以及我们熟知的 Bootstrap 结构:

<!doctype html>
<html lang="en" ng-app="ticketApp">
<head>
    <meta charset="utf-8">
    <title>Ticket Service</title>
    <link rel="stylesheet" href=""webjars/bootstrap/3.2.0/css/bootstrap.css">
    <link rel="stylesheet" href=""webjars/bootstrap/3.2.0/css/bootstrap-theme.css">
    <style>
        body {
            padding-top: 60px;
        }
    </style>
</head>
<body>

<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
</div>

<div class="container" ng-controller="SeatCtrl">
    <footer>
        <p class="text-muted">&copy; Packt Publishing 2014</p>
    </footer>
</div>

<script src="img/angular.js"></script>
<script src="img/angular-resource.js"></script>
<script src="img/angular-route.js"></script>
<script src="img/ui-bootstrap-tpls.js"></script>

<script src="img/app.js"></script>
<script src="img/seat.js"></script>
<script src="img/seatservice.js"></script>
<script src="img/accountservice.js"></script>
</body>
</html>

你可能还注意到了 html 标签中的两个看起来很奇怪的属性:ng-appng-controller。这些是 AngularJS 指令,指向的网页是一个 AngularJS 应用程序,并且容器 div 将使用 SeatCtrl 控制器。

现在,我们需要在 scripts 目录中放置以下文件。第一个是初始化文件 app.js

'use strict';
angular.module('ticketApp', [ 'ngResource', 'ngRoute', 'ui.bootstrap' ])
    .config(function ($routeProvider) {
        $routeProvider.when('/', {
            controller: 'SeatCtrl'
        }).otherwise({
            redirectTo: '/'
        });
    });

接下来,我们将在 scripts/services/seatservice.js 中初始化座位资源的地址:

'use strict';
angular.module('ticketApp').service('SeatService',
    function SeatService($resource) {
        return $resource('rest/seat/:seatId', {
            seatId: '@id'
        }, {
 query: {
 method: 'GET',
 isArray: true
 },
 book: {
 method: 'POST'
 }
        });
    });

如你所见,我们将我们的 REST URL 与 JavaScript 代码以及两个 HTTP 方法:GETPOST 映射在一起。它们将由控制器调用以与服务器通信;我们的账户资源也是如此,如下面的代码所示:

'use strict';
angular.module('ticketApp').service('AccountService',
    function AccountService($resource) {
        return $resource('rest/account', {}, {
 query: {
 method: 'GET',
 isArray: false
 },
 reset: {
 method: 'POST'
 }
        });
    });

最后,我们创建一个简单的控制器,将我们的逻辑放在 scripts/controllers/seat.js

'use strict';
angular.module('ticketApp').controller(
    'SeatCtrl',
    function ($scope, SeatService, AccountService) {
 $scope.seats = SeatService.query();
 $scope.account = AccountService.query();

        $scope.alerts = [];

 $scope.bookTicket = function (seat) {
 seat.$book({}, function success() {
                $scope.account.$query();
            }, function err(httpResponse) {
                $scope.alerts.push({
                    type: 'danger',
                    msg: 'Error booking ticket for seat '
                        + httpResponse.config.data.id + ': '
                        + httpResponse.data.entity
                });
            });
        };
        $scope.closeAlert = function (index) {
            $scope.alerts.splice(index, 1);
        };
        $scope.clearWarnings = function () {
            $scope.alerts.length = 0;
        };
        $scope.resetAccount = function () {
 $scope.account.$reset();
        };
    });

突出的代码部分是我们之前定义的服务调用。例如,$scope.seats = SeatService.query() 将发出一个 GET 请求以获取座位列表的 JSON 格式。对于 seat.$book 的情况类似;它将发出一个 POST 请求以预订特定的座位。

我们的全部 JavaScript 逻辑现在都已就绪。最后的步骤是将一些与它绑定的 HTML 代码放在我们的 index.html 文件中。在 index.html 文件中,在 content div 内插入以下代码:

   <alert ng-repeat="alert in alerts" type="alert.type"
           close="closeAlert($index)">{{alert.msg}}
    </alert>

    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">Ticket booking</h3>
        </div>
        <div class="panel-body">
            <p>
 Remaining money: <span class="badge">{{account.balance}}</span>
            </p>
            <br/>

            <button type="button" class="btn btn-primary btn-xs"
                    ng-click="clearWarnings()">Clear warnings
            </button>

            <button type="button" class="btn btn-warning btn-xs"
                    ng-click="resetAccount()">Reset account
            </button>
        </div>
        <table class="table table-hover table-striped">
            <thead><th>ID</th><th>Name</th><th>Price</th><th>Booked</th>       <th>Book</th></thead>
            <tbody>
            <tr ng-repeat="seat in seats">
 <td>{{seat.id}}</td>
 <td>{{seat.name}}</td>
 <td>${{seat.price}}</td>
                <td><span
                        class="glyphicon glyphicon-{{seat.booked ? 'ok' :'remove'}}"></span></td>
                <td>
                    <button type="button"
                            class="btn btn-primary {{seat.booked? 'disabled' :''}} btn-xs" ng-click="bookTicket(seat)">Book
                    </button>
                </td>
            </tr>
            </tbody>
        </table>

代码与我们在早期章节中创建的 JSF 表类似。对我们来说重要的是,{{ }} 符号被 AngularJS 用于将显示的数据与控制器中的一个变量绑定,实际上这是一个我们 REST 端点的表示。

此外,ng-click 指令绑定到控制器中适当的方法。bookTicket 方法发出一个 seat.$book 调用,这作为一个 POST 请求传播到我们的后端。

我们现在可以将我们的应用程序部署到服务器上。在浏览器中访问 http://localhost:8080/ticket-agency-ws/index.html 后,你应该能看到你的应用程序正在运行,如下面的截图所示:

添加 AngularJS

您可以使用 Chrome(或 Mozilla Firefox 中的 FireBug)的开发者工具来检查针对服务器的rest调用;只需按下F12并切换到网络标签页:

添加 AngularJS

恭喜!您刚刚创建了一个现代互联网应用,并将其与之前由独立控制台应用使用的 REST API 相结合!

选择 SOAP 和 REST 服务

采用 SOAP 而不是 REST 的选择取决于您的应用需求。SOAP 网络服务使用它们自己定义良好的协议,并专注于公开应用逻辑的片段作为服务。因此,如果您的需求是消费使用定义良好且经过协商的合同(在服务消费者和服务提供者之间)公开的业务服务,SOAP 网络服务是一个完美的匹配。

另一方面,如果您需要使用无状态的 HTTP 调用访问某些服务器资源,并且尽可能少地使用浏览器导航栏,那么您可能应该选择 RESTful 网络服务。

话虽如此,仍有一些场景可能适合这两种选项,您可以根据自己的需求自由选择最适合的 Web 服务。最近,由于其互操作性,REST 变得流行。我们只使用 HTTP 协议和 JSON,这几乎每种语言都能处理。因此,使用 Java EE 开发的 REST API 可以由各种客户端以及移动设备使用。通常,当设计系统时,这个特性是一个决定性的因素。

摘要

在本章中,我们介绍了基本网络服务概念,以便您在使用它们来增强您的票务应用之前熟悉这些技术。

然后,我们讨论了基于 WSDL 文件定义的服务和客户端之间合同的基础上的 SOAP 网络服务。当您使用标准 XML 文件公开了定义良好、抽象的操作时,SOAP 网络服务是集成系统的绝佳选择。

然后,我们讨论了 REST 服务。REST 方法的关键是使用一个已知且广泛使用的接口编写网络服务:URI。这里的转折点是识别关键系统资源(这可以是实体、集合或设计师认为值得拥有自己的 URI 的任何其他东西),并使用映射到标准方法的标准方法公开它们。在这种情况下,HTTP 动词被映射到资源特定的语义。

我们创建了两个使用我们的 REST API 的应用程序:一个基于控制台,另一个完全使用 JavaScript 编写,使用 AngularJS。这两个都使用相同的 REST 端点,第二个只知道 JSON;它对底层的 Java 类(甚至 Java 本身)一无所知。

我们讨论了很多应用服务器资源。在下一章中,我们将探讨客户端-服务器通信的另一种方法:WebSocket。

第八章。添加 WebSocket

WebSocket是 Java EE 7 中最大的新增功能之一。在本章中,我们将探讨它们为开发者提供的新可能性。在我们的票务预订应用程序中,我们已经使用了多种方法来通知客户端服务器端发生的事件。以下是一些方法:

  • JSF 轮询

  • Java 消息服务(JMS)消息

  • REST 请求

  • 远程 EJB 请求

除了 JMS 之外,所有这些方法都基于客户端将负责询问服务器应用程序状态的假设。在某些情况下,例如在我们与应用程序交互时检查是否有人预订了票,这是一种浪费的策略;服务器在需要时可以通知客户端。更重要的是,感觉开发者必须修改 HTTP 协议才能从服务器获取通知发送到客户端。这是一个大多数 Web 应用程序都必须实现的要求,因此,值得有一个标准化的解决方案,开发者可以在多个项目中轻松应用,而无需付出太多努力。

WebSockets 正在改变开发者的游戏规则。它们取代了客户端始终通过双向消息系统发起通信的请求-响应范式。在初始连接之后,只要会话保持活跃,双方就可以相互发送独立的消息。这意味着我们可以轻松创建那些会自动使用来自服务器的最新数据刷新其状态的 Web 应用程序。你可能已经在 Google Docs 或新闻网站的现场直播中见过这种行为。现在,我们可以以比 Java 企业版早期版本更简单、更高效的方式实现相同的效果。在本章中,我们将尝试利用 Java EE 7 中随着 WebSocket 带来的这些新、令人兴奋的功能,这得益于 JSR 356 (jcp.org/en/jsr/detail?id=356) 和 HTML5。

在本章中,你将学习以下主题:

  • WebSocket 是如何工作的

  • 如何在 Java EE 7 中创建 WebSocket 端点

  • 如何创建一个 HTML5/AngularJS 客户端,该客户端将接受部署在 WildFly 上的应用程序的推送通知

WebSocket 概述

客户端和服务器之间的 WebSocket 会话建立在标准的 TCP 连接之上。尽管 WebSocket 协议有自己的控制帧(主要用于创建和维持连接),这些控制帧由互联网工程任务组在 RFC 6455 中编码(tools.ietf.org/html/rfc6455),但对等方并不强制使用任何特定的格式来交换应用数据。你可以使用纯文本、XML、JSON 或其他任何东西来传输你的数据。你可能还记得,这与基于 SOAP 的 WebServices 大不相同,后者有膨胀的交换协议规范。同样,这也适用于 RESTful 架构;我们不再有 HTTP 预定义的动词方法(GET、PUT、POST 和 DELETE)、状态码以及整个 HTTP 请求的语义。

这种自由意味着与到目前为止我们所使用的相比,WebSocket 相当低级,但正因为如此,通信开销最小。该协议比 SOAP 或 RESTful HTTP 更简洁,这使我们能够实现更高的性能。然而,这也有代价。我们通常喜欢使用高级协议的功能(如水平扩展和丰富的 URL 语义),而使用 WebSocket,我们需要手动编写它们。对于标准的 CRUD-like 操作,使用 REST 端点比从头开始创建一切要容易得多。

与标准 HTTP 通信相比,我们从 WebSocket 获得了什么?首先,两个对等体之间的直接连接。通常,当你连接到 Web 服务器(例如,可以处理 REST 端点)时,每次后续调用都是一个新的 TCP 连接,你的机器每次请求时都被视为一个不同的机器。当然,你可以使用 cookie 模拟有状态的行为(这样服务器将在不同的请求之间识别你的机器),并通过在短时间内为特定客户端重用相同的连接来提高性能,但这基本上是一个解决 HTTP 协议限制的权宜之计。

一旦在服务器和客户端之间建立 WebSocket 连接,你可以在整个通信过程中使用相同的会话(以及底层的 TCP 连接)。双方都清楚这一点,并且可以以全双工方式独立发送数据(双方可以同时发送和接收数据)。使用纯 HTTP,服务器在没有来自其侧的任何请求的情况下,无法自发地向客户端发送数据。更重要的是,服务器知道所有连接的 WebSocket 客户端,甚至可以在它们之间发送数据!

当前解决方案包括尝试使用 HTTP 协议模拟实时数据传输,这会给 Web 服务器带来很大压力。轮询(询问服务器更新)、长轮询(延迟请求完成直到更新准备好)和流(基于 Comet 的解决方案,具有始终打开的 HTTP 响应)都是通过黑客协议来实现它未设计的功能,并且各自都有局限性。由于消除了不必要的检查,WebSocket 可以大幅减少 Web 服务器需要处理的 HTTP 请求数量。由于我们只需要通过网络进行一次往返即可获取所需信息(由服务器立即推送),因此更新以更小的延迟传递给用户。

所有这些特性使 WebSocket 成为 Java EE 平台的一个很好的补充,它填补了完成特定任务所需的空白,例如发送更新、通知和编排多个客户端交互。尽管有这些优势,WebSocket 并不打算取代 REST 或 SOAP Web 服务。它们在水平扩展方面表现不佳(由于它们的状态性,难以分发),并且缺乏大多数在 Web 应用程序中使用的功能。URL 语义、复杂的安全、压缩以及许多其他功能仍然最好使用其他技术来实现。

WebSockets 是如何工作的

要启动 WebSocket 会话,客户端必须发送一个带有Upgrade: websocket头字段的 HTTP 请求。这通知服务器对等客户端已请求服务器切换到 WebSocket 协议。

注意

你可能会注意到,在 WildFly 的远程 EJB 中也会发生同样的事情;初始连接是通过 HTTP 请求建立的,后来通过Upgrade机制切换到远程协议。标准的Upgrade头字段可以用来处理任何协议,除了 HTTP,这是客户端和服务器双方都接受的。在 WildFly 中,这允许你重用 HTTP 端口(80/8080)用于其他协议,从而最小化需要配置的端口号数量。

如果服务器能够“理解”WebSocket 协议,客户端和服务器随后将进入握手阶段。它们协商协议版本,交换安全密钥,如果一切顺利,对等方可以进入数据传输阶段。从现在开始,通信仅使用 WebSocket 协议进行。使用当前连接无法交换任何 HTTP 帧。整个连接的生命周期可以总结如下图所示:

WebSockets 是如何工作的

一个 JavaScript 应用程序向 WildFly 服务器发送的示例 HTTP 请求看起来可能如下所示:

GET /ticket-agency-websockets/tickets HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: localhost:8080
Origin: http://localhost:8080Pragma: no-cache
Cache-Control: no-cache
Sec-WebSocket-Key: TrjgyVjzLK4Lt5s8GzlFhA==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits, x-webkit-deflate-frame
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36
Cookie: [45 bytes were stripped]

我们可以看到,客户端在/ticket-agency-websockets/tickets URL 上请求一个升级连接,目标协议为 WebSocket。它还传递了请求的版本和密钥信息。

如果服务器支持请求协议并且客户端传递了所有必要的数据,那么它将响应以下帧:

HTTP/1.1 101 Switching Protocols
X-Powered-By: Undertow 1
Server: Wildfly 8
Origin: http://localhost:8080
Upgrade: WebSocket
Sec-WebSocket-Accept: ZEAab1TcSQCmv8RsLHg4RL/TpHw=
Date: Sun, 13 Apr 2014 17:04:00 GMT
Connection: Upgrade
Sec-WebSocket-Location: ws://localhost:8080/ticket-agency-websockets/tickets
Content-Length: 0

响应的状态码是101(切换协议),我们可以看到服务器现在将开始使用 WebSocket 协议。最初用于 HTTP 请求的 TCP 连接现在是 WebSocket 会话的基础,并可用于传输。如果客户端尝试访问仅由另一个协议处理的 URL,则服务器可以要求客户端进行升级请求。在这种情况下,服务器使用426(需要升级)状态码。

注意

初始连接创建有一些开销(因为对等体之间交换的 HTTP 帧),但完成后,新消息只有 2 个字节的额外头部。这意味着当我们有大量的小消息时,WebSocket 将比 REST 协议快一个数量级,仅仅是因为传输的数据更少!

如果你在想浏览器的 WebSocket 支持情况,你可以在caniuse.com/websockets上查找。目前所有主流浏览器的所有新版本都支持 WebSocket;据估算(在撰写本文时),总覆盖率为 74%。你可以在以下屏幕截图中看到这一点:

WebSocket 是如何工作的

在这个理论介绍之后,我们准备好采取行动。我们现在可以创建我们的第一个 WebSocket 端点!

创建我们的第一个端点

让我们从简单的例子开始:

package com.packtpub.wflydevelopment.chapter8.boundary;

import javax.websocket.EndpointConfig;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

@ServerEndpoint("/hello")
public class HelloEndpoint {

    @OnOpen
    public void open(Session session, EndpointConfig conf) throws IOException {
        session.getBasicRemote().sendText("Hi!");
    }
}

Java EE 7 规范考虑了开发者的友好性,这在给定的示例中可以清楚地看到。为了定义你的 WebSocket 端点,你只需要在普通 Java 对象POJO)上添加几个注解。第一个注解@ServerEndpoint("/hello")定义了端点的路径。现在是讨论端点完整地址的好时机。我们将这个示例放在名为ticket-agency-websockets的应用程序中。在部署应用程序时,你可以在 WildFly 日志中找到有关端点创建的信息,如下所示:

02:21:35,182 INFO  [io.undertow.websockets.jsr] (MSC service thread 1-7) UT026003: Adding annotated server endpoint class com.packtpub.wflydevelopment.chapter8.boundary.FirstEndpoint for path /hello
02:21:35,401 INFO  [org.jboss.resteasy.spi.ResteasyDeployment] (MSC service thread 1-7) Deploying javax.ws.rs.core.Application: class com.packtpub.wflydevelopment.chapter8.webservice.JaxRsActivator$Proxy$_$$_WeldClientProxy
02:21:35,437 INFO  [org.wildfly.extension.undertow] (MSC service thread 1-7) JBAS017534: Registered web context: /ticket-agency-websockets

端点的完整 URL 是ws://localhost:8080/ticket-agency-websockets/hello,这仅仅是服务器和应用程序地址与适当协议上的端点路径的连接。

第二个使用的注解@OnOpen定义了客户端连接打开时端点的行为。这不是 WebSocket 端点唯一的行为相关注解。让我们看看以下表格:

标注 描述
@OnOpen 连接已打开。使用这个注解,我们可以使用 SessionEndpointConfig 参数。第一个参数代表与用户的连接,允许进一步的通信。第二个参数提供一些与客户端相关的信息。
@OnMessage 当客户端的消息被接收时,这个注解会被执行。在这样的方法中,你只需要有 Session 和例如 String 参数,其中 String 参数代表接收到的消息。
@OnError 当发生错误时,会有一些不好的时候。使用这个注解,你可以除了标准的 Session 之外检索一个 Throwable 对象。
@OnClose 当连接关闭时,可以以 CloseReason 类型对象的形式获取一些关于此事件的数据。

在我们的 HelloEndpoint 中还有一条有趣的线。使用 Session 对象,我们可以与客户端进行通信。这清楚地表明,在 WebSocket 中,双向通信是很容易实现的。在这个例子中,我们决定通过只发送一条文本消息 Hi! (sendText (String)) 来同步响应已连接的用户 (getBasicRemote())。当然,也可以异步通信,例如,使用你自己的二进制带宽节省协议发送二进制消息。我们将在下一个示例中展示一些这些过程。

扩展我们的客户端应用程序

是时候展示你如何在现实生活中利用 WebSocket 的特性了。在前一章中,第七章,将 Web 服务添加到您的应用程序中,我们基于 REST API 和 AngularJS 框架创建了票务预订应用程序。显然,它缺少一个重要的功能:应用程序没有显示有关其他用户票务购买的信息。这是一个完美的 WebSocket 应用场景!

由于我们只是在添加之前应用程序的一个功能,所以我们只描述我们将要引入到其中的更改。

在这个例子中,我们希望能够通知所有当前用户关于其他购买的信息。这意味着我们必须存储有关活动会话的信息。让我们从注册类型对象开始,它将为此目的服务。我们可以使用一个 Singleton 会话豆来完成这个任务,如下面的代码所示:

@Singleton
public class SessionRegistry {

    private final Set<Session> sessions = new HashSet<>();

    @Lock(LockType.READ)
    public Set<Session> getAll() {
        return Collections.unmodifiableSet(sessions);
    }

    @Lock(LockType.WRITE)
    public void add(Session session) {
        sessions.add(session);
    }

    @Lock(LockType.WRITE)
    public void remove(Session session) {
        sessions.remove(session);
    }
}

我们可以使用标准 Java 库中的 Collections.synchronizedSet,但这是一个很好的机会来回忆我们在 第三章 中描述的内容,介绍 Java EE 7 – EJBs,关于基于容器的并发。在 SessionRegistry 中,我们定义了一些基本方法来添加、获取和删除会话。为了在检索期间保证集合的线程安全,我们返回一个不可修改的视图。

我们定义了注册,现在我们可以转向端点定义。我们需要一个 POJO,它将使用我们新定义的注册,如下所示:

@ServerEndpoint("/tickets")
public class TicketEndpoint {

 @Inject
 private SessionRegistry sessionRegistry;

 @OnOpen
    public void open(Session session, EndpointConfig conf) {
        sessionRegistry.add(session);
    }

 @OnClose
    public void close(Session session, CloseReason reason) {
        sessionRegistry.remove(session);
    }

    public void send(@Observes Seat seat) {
 sessionRegistry.getAll().forEach(session -> session.getAsyncRemote().sendText(toJson(seat)));
    }

    private String toJson(Seat seat) {
 final JsonObject jsonObject = Json.createObjectBuilder()
 .add("id", seat.getId())
 .add("booked", seat.isBooked())
 .build();
        return jsonObject.toString();
    }
}

我们的定义端点在/tickets地址。我们向端点注入了SessionRepository。在@OnOpen期间,我们将Sessions添加到注册表中,在@OnClose期间,我们只需移除它们。消息发送是在 CDI 事件(@Observers注解)上执行的,该事件已经在我们的代码中通过TheatreBox.buyTicket(int)触发。在我们的send方法中,我们从SessionRepository检索所有会话,并对每个会话异步发送关于已预订座位的消息。我们并不真的需要所有Seat字段的信息来实现这个功能。这就是为什么我们在这里没有使用从上一章了解到的自动 JSON 序列化的原因。相反,我们决定使用一个简约的JSON对象,它只提供所需的数据。为此,我们使用了新的 Java API for JSON Processing (JSR-353)。使用类似流畅的 API,我们能够创建一个JSON对象并向其中添加两个字段。然后,我们只需将 JSON 转换为字符串,通过文本消息发送。

因为在我们的例子中,我们是在响应 CDI 事件时发送消息,所以在事件处理器中,我们没有对任何会话的现成引用。我们必须使用我们的sessionRegistry对象来访问活动会话。然而,如果我们想在例如@OnMessage方法中做同样的事情,那么只需执行session.getOpenSessions()方法就可以获取所有活动会话。

这些都是在后端执行所需的所有更改。现在,我们必须修改我们的 AngularJS 前端以利用新增的功能。好消息是 JavaScript 已经包含了可以用来执行 WebSocket 通信的类!我们只需要在seat.js文件中定义的模块内添加几行代码,如下所示:

var ws = new WebSocket("ws://localhost:8080/ticket-agency-websockets/tickets");
ws.onmessage = function (message) {
    var receivedData = message.data;
    var bookedSeat = JSON.parse(receivedData);

    $scope.$apply(function () {
        for (var i = 0; i < $scope.seats.length; i++) {
            if ($scope.seats[i].id === bookedSeat.id) {
                $scope.seats[i].booked = bookedSeat.booked;
                break;
            }
        }
    });
};

代码非常简单。我们只需使用端点的 URL 创建WebSocket对象,然后在该对象中定义onmessage函数。在函数执行期间,接收到的消息会自动从 JSON 解析为 JavaScript 对象。然后,在$scope.$apply中,我们只需遍历我们的座位,如果 ID 匹配,我们更新预订状态。我们必须使用$scope.$apply,因为我们正在从 Angular 世界之外(onmessage函数)触摸 Angular 对象。对$scope.seats进行的修改会自动在网站上可见。有了这个,我们只需在两个浏览器会话中打开我们的票务预订网站,就可以看到当一个用户购买票时,第二个用户几乎立即看到座位状态已更改为已预订

我们可以稍微增强我们的应用程序,以便在 WebSocket 连接真正工作的情况下通知用户。让我们只为这个目的定义onopenonclose函数:

ws.onopen = function (event) {
    $scope.$apply(function () {
        $scope.alerts.push({
            type: 'info',
            msg: 'Push connection from server is working'
        });
    });
};
ws.onclose = function (event) {
    $scope.$apply(function () {
        $scope.alerts.push({
            type: 'warning',
            msg: 'Error on push connection from server '
        });
    });
};

为了通知用户连接的状态,我们推送不同类型的警报。当然,我们再次从外部触及 Angular 世界,因此我们必须在 $scope.$apply 函数上执行所有操作。

运行所描述的代码会导致通知,这在下面的屏幕截图中可见:

扩展我们的客户端应用程序

然而,如果在打开网站后服务器失败,你可能会得到以下屏幕截图所示的错误:

扩展我们的客户端应用程序

将 POJO 转换为 JSON

在我们当前的示例中,我们手动将 Seat 对象转换为 JSON。通常,我们不希望这样做;有许多库会为我们完成转换。其中之一是来自 Google 的 GSON。此外,我们还可以为 WebSocket 端点注册一个 encoder/decoder 类,该类将自动执行转换。让我们看看我们如何重构当前的解决方案以使用编码器。

首先,我们必须将 GSON 添加到我们的类路径中。所需的 Maven 依赖项如下:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.3</version>
</dependency>

接下来,我们需要提供一个 javax.websocket.Encoder.Text 接口的实现。也有 javax.websocket.Encoder.Text 接口的二进制和流式数据版本(对于二进制和文本格式)。对于解码器(javax.websocket.Decoder)也有相应的接口层次结构。我们的实现相当简单。如下代码片段所示:

public class JSONEncoder implements Encoder.Text<Object> {

    private Gson gson;

    @Override
    public void init(EndpointConfig config) {
 gson = new Gson(); [1]
    }

    @Override
    public void destroy() {
        // do nothing
    }

    @Override
    public String encode(Object object) throws EncodeException {
 return gson.toJson(object); [2]
    }
}

首先,我们在 init 方法中创建 GSON 的实例;这个动作将在端点创建时执行。接下来,在每次调用的 encode 方法中,我们通过端点发送一个对象。我们使用 JSON 命令从对象创建 JSON。当我们想到这个小程序的可重用性时,这相当简洁。如果你想在 JSON 生成过程中有更多控制,你可以在创建之前使用 GsonBuilder 类来配置 Gson 对象。我们已经有了编码器。现在,是时候修改我们的端点了:

@ServerEndpoint(value = "/tickets", encoders={JSONEncoder.class})[1]
public class TicketEndpoint {

    @Inject
    private SessionRegistry sessionRegistry;

    @OnOpen
    public void open(Session session, EndpointConfig conf) {
        sessionRegistry.add(session);
    }

    @OnClose
    public void close(Session session, CloseReason reason) {
        sessionRegistry.remove(session);
    }

    public void send(@Observes Seat seat) {
        sessionRegistry.getAll().forEach(session -> session.getAsyncRemote().sendObject(seat)); [2]
    }
}

第一个更改是在 @ServerEndpoint 注解上进行的。我们必须定义一个支持编码器的列表;我们只需将我们的 JSONEncoder.class 包裹在数组中传递。此外,我们必须使用 value 属性传递端点名称。

之前,我们使用 sendText 方法传递一个包含手动创建的 JSON 的字符串。现在,我们想要发送一个对象,并让编码器处理 JSON 生成;因此,我们将使用 getAsyncRemote().sendObject() 方法。就这样。我们的端点已经准备好使用。它将像早期版本一样工作,但现在我们的对象将被完全序列化为 JSON,因此它们将包含每个字段,而不仅仅是 idbooked

在部署服务器后,你可以使用 Chrome 扩展程序之一连接到 WebSocket 端点,例如,Chrome 商店中的Dark WebSocket终端(使用ws://localhost:8080/ticket-agency-websockets/tickets地址)。当你使用 Web 应用程序预订票务时,WebSocket 终端应该显示类似于以下截图所示的内容:

将 POJO 转换为 JSON

当然,除了 JSON 之外,还可以使用其他格式。如果你想获得更好的性能(尤其是在序列化时间和负载大小方面),你可能想尝试二进制序列化器,例如Kryo (github.com/EsotericSoftware/kryo)。它们可能不被 JavaScript 支持,但如果你也想为其他客户端使用 WebSocket,它们可能很有用。Tyrus (tyrus.java.net/)是 Java 的 WebSocket 标准的参考实现;你可以在你的独立桌面应用程序中使用它。在这种情况下,除了用于发送消息的编码器之外,你还需要创建一个解码器,它可以自动转换传入的消息。

WebSocket 的替代方案

本章中我们提供的示例可以使用一个较老、不太知名的技术来实现,该技术名为服务器发送事件SSE)。SSE 允许通过 HTTP 从服务器到客户端进行单向通信。它比 WebSocket 简单得多,但内置了对自动重连和事件标识符等事物的支持。WebSocket 确实更强大,但并非传递事件的唯一方式,所以当你需要从服务器端实现一些通知时,记得 SSE。

另一个选择是探索围绕 Comet 技术的机制。有多种实现方式,其中大多数使用不同的传输方法来实现目标。一个全面的比较可以在cometdaily.com/maturity.html找到。

摘要

在本章中,我们成功介绍了新的低级别通信类型。我们介绍了它是如何工作的,以及与上一章中介绍的 SOAP 和 REST 相比。我们还讨论了新的方法如何改变 Web 应用程序的开发。

我们的票务预订应用程序得到了进一步增强,使用类似推送的通知向用户显示座位的变化状态。当我们考虑到我们能够用它们实现多少时,新的添加在现有项目中只需要很少的代码更改。Java EE 7 中 WebSocket 的流畅集成与 AngularJS 应用程序是 Java EE 平台新版本带来的灵活性的另一个绝佳展示。

在下一章中,你将学习更多关于 WildFly 管理和配置的知识,以便我们可以在接下来的章节中探索 Java EE 7 的更多系统级特性。

第九章。管理应用程序服务器

到目前为止,我们已经涵盖了多个 Java Enterprise 示例,并将它们部署到应用程序服务器上。现在,我们将一头扎入管理应用程序服务器的丰富多样的工具海洋。本章的目的是教会你如何使用这些工具来管理和监控应用程序服务器上所有可用的资源。

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

  • WildFly 命令行界面CLI)简介

  • 如何使用 CLI 创建脚本

  • 如何使用脚本语言和 WildFly 客户端 API 编程式地管理服务器资源

  • 如何强制执行管理员基于角色的安全性

进入 WildFly CLI

CLI 是一个完整的管理工具,可以用来启动和停止服务器,部署和卸载应用程序,配置系统资源,并执行其他管理任务。其中的操作可以以原子方式或批量模式执行,允许你将多个任务作为一个组运行。

启动 CLI

如果你使用的是 Windows,你可以通过命令提示符从JBOSS_HOME/bin文件夹中输入以下命令来启动 CLI:

jboss-cli.bat

如果你使用的是 Linux,则可以输入以下命令:

./jboss-cli.sh

一旦 CLI 启动,你可以使用connect命令连接到托管的服务器实例,默认情况下连接到localhost9990端口:

[disconnected /] connect
[standalone@localhost:9990 /]

如果你想要连接到另一个地址或端口,你可以简单地将其传递给connect命令,如下所示:

[disconnected /] connect 192.168.1.1
[standalone@192.168.1.1:9990 /]

还可以在连接模式下启动 CLI;这允许它自动连接,并可能指定要执行的命令。例如,以下shell命令自动连接到 WildFly 实例并发出shutdown命令:

> jboss-cli.bat --connect command=:shutdown
{"outcome" => "success"}

命令行界面(CLI)对于自动化软件开发流程特别有用——持续集成CI)和生产环境管理系统可以使用 Chef (www.getchef.com/) 或 Puppet (puppetlabs.com/) 等工具自动控制应用服务器的生命周期。如果你希望最小化部署应用程序所需的手动任务数量,这将非常有用。

从远程主机连接

从应用程序服务器的 7.1.0 Beta 版本开始,默认情况下在 AS 管理接口上启用了安全功能,以防止未经授权的远程访问应用程序服务器。尽管应用程序服务器的本地客户端仍然允许未经任何身份验证地访问管理接口,但远程客户端需要输入用户名/密码对才能访问 CLI。以下是一个成功连接到 IP 地址为10.13.2.255的远程主机的示例会话:

[disconnected /] connect 10.13.2.255
Authenticating against security realm: ManagementRealm
Username: administrator
Password:
[standalone@10.13.2.255:9990 /]

请参阅第二章,在 WildFly 上创建第一个 Java EE 应用程序,以获取有关使用add-user.sh shell 命令创建用户的更多信息。

在图形模式下使用 CLI

命令行界面可用的一项有趣选项是图形模式,可以通过在 shell 脚本中添加--gui参数来激活:

jboss-cli.bat --gui

这就是 CLI 在图形模式下的样子:

在图形模式下使用 CLI

如标签所述,当您点击文件夹时,资源将展开;另一方面,如果您右键单击节点,您可以在其上执行操作。图形模式可能有助于探索可能的配置值,或者如果您不是特别喜欢控制台工具的话。

下一个部分讨论了如何构建 CLI 命令,这些命令可以在终端模式或图形模式下执行。

构建 CLI 命令

所有 CLI 操作请求都允许您与服务器管理模型进行低级别交互。它们提供了一种受控的方式来编辑服务器配置。一个操作请求由三部分组成:

  • /为前缀的地址

  • 前缀为:的操作名称

  • 包含在()内的可选参数集

确定资源地址

服务器配置以可寻址资源的分层树的形式呈现。每个资源节点提供一组不同的操作。地址指定要执行操作的资源节点。地址使用以下语法:

/node-type=node-name

以下是对这些记号的解释:

  • node-type:这是资源节点类型。这映射到服务器配置中的一个元素名称。

  • node-name:这指定了资源节点名称。这映射到服务器配置中元素的名称属性。

使用斜杠(/)分隔资源树的每一级。例如,以下 CLI 表达式识别在数据源子系统注册的 ExampleDS 数据源:

/subsystem=datasources/data-source=ExampleDS

对资源执行操作

一旦您已识别资源,您就可以对该资源执行操作。一个操作使用以下语法:

:operation-name

因此,在前面的示例中,您可以通过在末尾添加read-resource命令来查询您节点的可用资源列表:

/subsystem=datasources/:read-resource 
{
    "outcome" => "success",
    "result" => {
        "data-source" => {"ExampleDS" => undefined},
        "jdbc-driver" => {"h2" => undefined},
        "xa-data-source" => undefined
    }
}

如果您想查询节点的一个特定属性,您可以使用read-attribute操作。例如,以下代码显示了如何从数据源中读取启用属性:

/subsystem=datasources/data-source=ExampleDS/:read-attribute(name=enabled) 
{
    "outcome" => "success",
    "result" => false
}

注意

除了对特定资源执行的操作之外,您还可以执行一组在您的 WildFly 子系统每个路径上都可用的命令,例如cdls命令。这些命令几乎等同于它们的 Unix shell 对应命令,并允许您在 WildFly 子系统中导航。其他重要的新增功能是deployundeploy命令,正如您可能猜到的,这些命令允许您管理应用程序的部署。这些关键命令在本章的使用 CLI 部署应用程序部分进行了讨论。

CLI(命令行界面)不仅仅是查询 WildFly 子系统的属性;您还可以设置属性或创建资源。例如,如果您要设置 HTTP 连接器的 HTTP 端口,您将不得不使用 HTTP 套接字绑定接口上的相应write属性,如下所示:

/socket-binding-group=standard-sockets/socket-binding=http/:write-attribute(name=port,value=8080)
{
    "outcome" => "success","response-headers" => {
        "operation-requires-reload" => true,
        "process-state" => "reload-required"
    }
}

除了我们迄今为止看到的可以在您的子系统中的每个资源上执行的操作之外,还可以执行一些只能针对单个资源执行的特殊操作。例如,在命名子系统中,您将能够执行一个jndi-view操作,该操作将显示 JNDI 绑定的列表,如下面的代码片段所示:

/subsystem=naming/:jndi-view
{
    "outcome" => "success",
    "result" => {"java: contexts" => {
        "java:" => {
            "TransactionManager" => {
                "class-name" => "com.arjuna.ats.jbossatx.jta.TransactionManagerDelegate","value" => "com.arjuna.ats.jbossatx.jta.TransactionManagerDelegate@afd978"
            },
     . . .
}

使用 Tab 补全助手

了解 CLI 中所有可用命令是一项相当困难的任务;这个管理界面包括一个基本功能,即 Tab 补全。假设光标位于一个空行的开头;现在如果您输入/并按Tab键,您将获得以下所有可用节点类型的列表:

[standalone@localhost:9990 /] /
core-service           extension              socket-binding-group
deployment             interface              subsystem
deployment-overlay     path                   system-property

在选择节点类型后,您想进入资源树,因此请输入=并再次按Tab键。这将显示所有以下节点名称的列表,这些名称适用于所选节点类型:

[standalone@localhost:9990 /] /subsystem=
batch                jdr                  resource-adapters
datasources          jmx                  sar
deployment-scanner   jpa                  security
ee                   jsf                  threads
ejb3                 logging              transactions
infinispan           mail                 undertow
io                   naming               webservices
jaxrs                pojo                 weld
jca                  remoting

在完成节点路径后,在节点路径末尾添加冒号(:)并按Tab键将显示所选节点的所有可用操作名称,如下所示:

[standalone@localhost:9990 /] /subsystem=deployment-scanner/scanner=default:
add                          read-resource
read-attribute              read-resource-description
read-children-names          remove
read-children-resources      resolve-path
read-children-types          undefine-attribute
read-operation-description   whoami
read-operation-names         write-attribute

要查看add操作的所有参数(在操作名称之后),请按Tab键:

[standalone@localhost:9990 /] /subsystem=deployment-scanner/scanner=default:read-attribute(
include-defaults=   name=

选择您想要的参数,并在=之后指定其值:

[standalone@localhost:9990 /] /subsystem=deployment-scanner/scanner=default:read-attribute(name=
runtime-failure-causes-rollback   scan-enabled
relative-to                       scan-interval
path                              auto-deploy-zipped
auto-deploy-exploded              deployment-timeout
auto-deploy-xml

最后,当所有参数都已指定后,添加)并按Enter键以执行以下命令:

[standalone@localhost:9990 /] /subsystem=deployment-
scanner/scanner=default:read-attribute(name=scan-enabled)
{
 "outcome" => "success",
 "result" => true
}

使用 CLI 部署应用程序

使用 CLI 部署应用程序(在独立模式下)可以通过将应用程序的存档复制到服务器发行版的deployment文件夹中轻松完成。这是一个相当方便的选项;然而,我们想强调使用 CLI 的优势,它提供了广泛的附加选项,在部署时使用,同时也提供了远程部署应用程序的机会。

部署应用程序存档所需的一切只是一个连接到管理实例(无论是本地还是远程),以及发出 deploy shell 命令。当不使用参数时,deploy 命令会提供一个当前已部署的应用程序列表,如下所示:

[disconnected /] connect
[standalone@localhost:9990 /] deploy ExampleApp.war

如果你将资源存档(如 WAR 文件)喂给 shell,它将立即在独立服务器上部署它,如下所示:

[standalone@localhost:9990 /] deploy ../MyApp.war 

默认情况下,CLI 使用 JBOSS_HOME/bin 文件作为你的部署存档的来源。然而,你可以使用绝对路径来指定存档的位置;CLI 扩展功能(使用 Tab 键)使此选项相当简单:

[standalone@localhost:9990 /] deploy c:\deployments\MyApp.war

重新部署应用程序需要在 deploy 命令中添加一个额外的标志。使用 -f 参数强制应用程序重新部署:

[standalone@localhost:9990 /] deploy -f ../MyApp.war

通过 undeploy 命令卸载应用程序可以通过将已部署的应用程序作为参数来实现。如下所示:

[standalone@localhost:9990 /] undeploy MyApp.war

通过检查 WildFly 配置文件(例如,standalone.xmldomain.xml),你会注意到你的应用程序的部署元素已被移除。

部署应用程序到 WildFly 域

当你使用域模式部署应用程序时,你必须指定部署关联的服务器组。CLI 允许你在以下两个选项之间进行选择:

  • 部署到所有服务器组

  • 部署到单个服务器组

我们将在两个单独的部分中讨论这些选择。

部署到所有服务器组

如果选择此选项,应用程序将被部署到所有可用的服务器组。可以使用 --all-server-groups 标志来实现此目的。例如,参考以下代码:

[domain@localhost:9990 /] deploy ../application.ear --all-server-groups

如果你想从属于某个域的所有服务器组中卸载应用程序,你将必须按照以下命令发出 undeploy 命令:

[domain@localhost:9990 /] undeploy application.ear --all-relevant-server-groups

注意

你可能已经注意到,undeploy 命令使用的是 --all-relevant-server 组而不是 --all-server- 组。这种差异的原因是部署可能并未在所有服务器组中启用;因此,通过使用此选项,你实际上会从所有已启用部署的服务器组中卸载它。

部署到单个服务器组

另一个选项允许你仅在你指定的服务器组上执行应用程序的选择性部署:

[domain@localhost:9990 /] deploy application.ear --server-groups=main-server-group

你不仅限于单个服务器组,并且可以使用逗号(,)分隔多个服务器组。例如,参考以下代码:

[domain@localhost:9990 /] deploy application.ear --server-groups=main-server-group,other-server-group
Successfully deployed application.ear

选项卡补全功能将帮助你完成为部署所选的 --server-groups 列表指定的值。

现在,假设我们只想从单个服务器组中卸载应用程序。在这种情况下,可能会有两种可能的结果。如果应用程序仅在该服务器组中可用,您将成功完成卸载,如下面的命令所示:

[domain@localhost:9990 /] undeploy wflyproject.war --server-groups=main-server-group

另一方面,如果您的应用程序在其他服务器组中可用,CLI 将返回以下错误:

Undeploy failed: {"domain-failure-description" => {"Composite operation failed and was rolled back. Steps that failed:" => {"Operation step-3" => "Cannot remove deployment wflyproject.war from the domain as it is still used by server groups [other-server-group]"}}}

看起来出了点问题。实际上,当您从服务器组中删除应用程序时,域控制器会验证没有其他服务器组会引用该应用程序;否则,之前的命令将失败。

然而,您可以指示域控制器卸载应用程序而不删除内容。以下命令显示了这一点:

[domain@localhost:9990 /] undeploy application.ear --server-groups=main-server-group --keep-content

创建 CLI 脚本

作为程序开发者,您可能想知道 CLI 可以通过将它们添加到文件中来以非交互方式执行命令,就像 shell 脚本一样。为了执行脚本,您可以使用以下示例(对于 Windows)中的 --file 参数启动 CLI:

jboss-cli.bat --file=test.cli

对于 Unix 用户,等效的命令如下:

./jboss-cli.sh --file=test.cli

在下一节中,我们将探讨一些可以添加到您的管理员工具箱中的有用脚本。

将应用程序部署到多个 WildFly 节点

早期的 JBoss AS 版本通常会附带一个 farm 文件夹,该文件夹会触发将部署到 JBoss 集群中的所有节点。这个选项在 JBoss AS7 和 WildFly 中不再包含,但恢复 farm 部署只需遵循几个 CLI 指令。

在以下示例中,我们将应用程序部署到默认服务器地址(127.0.0.1 和端口 9990)以及绑定到相同地址但端口为 10190 的另一个服务器实例:

connect
deploy /usr/data/example.war
connect 127.0.0.1:10190
deploy /usr/data/example.war

在域中重启服务器

域管理员的一个常见需求是在更新某些服务器库时重新启动应用程序服务器节点。CLI 提供了一个方便的快捷方式来停止和启动属于服务器组的所有服务器:

connect
/server-group=main-server-group:start-servers
/server-group=main-server-group:stop-servers

如果您更喜欢更细粒度的方法,您可以根据以下示例启动单个服务器节点,该示例显示了如何在您的 CLI 脚本中应用条件执行逻辑:

connect
if (result == "STARTED") of /host=master/server-config=server-one:read-attribute(name=status)
/host=master/server-config=server-one:stop
end-if

if (result == "STARTED") of /host=master/server-config=server-two:read-attribute(name=status)
/host=master/server-config=server-two:stop
end-if
/host=master/server-config=server-one:start

/host=master/server-config=server-two:start

在代码的 if end-if 部分,我们正在检查服务器的状态属性。如果状态是 STARTED,则应用程序服务器将被停止并重新启动。

将数据源作为模块安装

在 WildFly 中,您可以使用 module 命令来安装一个新的模块。我们已经在 第五章 中做了类似的事情,将持久性与 CDI 结合。现在,您可以像以下示例中所示那样完全自动化数据源创建:

connect

module add --name=org.postgresql --resources= postgresql-9.3-1101.jdbc41.jar  --dependencies=javax.api,javax.transaction.api

/subsystem=datasources/jdbc-driver=postgresql:add(driver-module-name=org.postgresql,driver-name=postgresql,driver-class-name=org.postgresql.Driver)

/subsystem=datasources/data-source=PostgreSQLDS:add(jndi-name=java:jboss/datasources/PostgreSQLDS , driver-name=postgresql, connection-url=jdbc:postgresql://localhost:5432/ticketsystem,user-name=jboss,password=jboss)

脚本的第 1 行,在连接之后,在你的服务器模块目录中安装了一个名为org.postgresql的新模块,包括 PostgreSQL JDBC 驱动和所需的依赖项。

第 2 行将org.postgresql模块的 JDBC 驱动安装到datasources/jdbc-driver子系统。

最后,使用所需的 URL 和凭据将数据源添加到jndi java:jboss/datasources/PostgreSQLDS

添加 JMS 资源

添加一个新的 JMS 目标非常简单,因为它不需要一系列冗长的命令。然而,有时你的应用程序需要设置许多 JMS 目标才能工作,那么为什么不为此创建一个脚本呢?以下是一个简单的脚本,用于将 JMS 队列添加到服务器配置中:

connect
jms-queue add  --queue-address=queue1 --entries=queues/queue1 

以下是你可以使用来创建 JMS 主题的相应脚本:

connect
jms-topic add  --topic-address=topic1 --entries=topics/topic1

使用高级语言创建强大的 CLI 脚本

到目前为止,我们已经学习了如何编写 CLI shell 命令来管理应用程序服务器的资源。这种方法的优势在于,你可以轻松快速地访问每个服务器资源,这得益于内置的自动完成功能。另一方面,如果你想围绕你的命令执行一些复杂的逻辑,那么你需要寻找其他替代方案。

如果你是一个 shell 高手,你可能会很容易地求助于一些 bash 脚本,以便捕获 CLI 的输出并使用丰富的 Unix/Linux 工具执行一些管理操作。

提供 bash 功能的简要概述可能是一项有趣的练习;然而,如果我们这样做,我们就会偏离本书的范围。我们将记录一些内置功能,如下所示:

  • 在第一部分,我们将展示如何在 Python 脚本中使用 CLI 远程客户端 API。

  • 在下一节中,我们将使用原始管理 API 在 Java 应用程序中执行 CLI 命令。

在许多情况下,JBoss CLI 脚本可能非常有用。脚本可以用来配置开发人员的机器、测试环境,或者作为生产环境的初始配置。在许多情况下,启动一个完整的企业应用程序所需的配置可能相当复杂;你可能需要使用特定的端口配置来集群测试或你的安全域。你可能还需要你的持续集成服务器为你完成这项工作。除此之外,拥有一个自动配置脚本比每次都手动设置配置要好,这既浪费时间,又可能成为错误源。

使用脚本语言包装 CLI 执行

JBoss AS 7 引入了一个新的 CLI 远程 API,它充当 CLI 公共 API 的代理。这两个 API 之间的桥梁是scriptsupport.CLI类,该类包含在JBOSS_HOME/bin/client/jboss-cli-client.jar文件中。

多亏了这个 API,您可以使用多种不同的语言执行 CLI 命令,例如 Jython、Groovy 或 JavaScript。由于 Jython 也是其他应用程序服务器的实际管理标准,例如 Oracle、WebLogic 和 WebSphere,我们将使用它来执行一些基本的管理任务。

注意

Jython 是为 JVM 实现的 Python。Jython 非常有用,因为它在 JVM 上运行时提供了成熟脚本语言的生产力特性。与 Python 程序不同,Jython 程序可以在支持 JVM 的任何环境中运行。

Jython 使用 jython 脚本调用,这是一个简短的脚本,用于调用您的本地 JVM,运行 Java 类文件 org.python.util.jython

为了开始,您需要做的第一件事是下载 Jython 安装程序,从 www.jython.org/downloads.html

使用以下命令运行安装程序:

java -jar jython-installer-2.5.3.jar

接下来,将 JYTHON_HOME/bin 文件夹(例如,C:\jython2.5.3\bin)添加到系统路径,并将 jboss-cli-client.jar 文件添加到系统的 CLASSPATH 中。例如,在 Windows 中,使用以下命令:

set JYTHON_HOME= C:\jython2.5.3
set PATH=%PATH%;%JYTHON_HOME%\bin

set CLASSPATH=%CLASSPATH%;%JBOSS_HOME%\bin\client\jboss-cli-client.jar;.

这里是 Linux 的相同命令:

export PATH=$PATH:/usr/data/jython2.5.3/bin
export CLASSPATH=$CLASSPATH$:%JBOSS_HOME%/bin/client/jboss-cli-client.jar

好的,现在我们将创建第一个脚本,该脚本将基本上返回我们的应用程序服务器的 JNDI 视图。

注意

请注意,Jython 与 Python 一样,使用缩进来确定代码结构,而不是使用大括号或关键字。因此,不要随意使用它们。IDE 可能会帮助您完成这项工作——例如,对于 Python,您可以使用 Vim 的 python-mode (github.com/klen/python-mode) 或带有 PyDev 扩展的 Eclipse (pydev.org/)。

创建一个名为 script.py 的文件,包含以下代码:

from org.jboss.as.cli.scriptsupport import CLI

cli = CLI.newInstance()
cli.connect()

cli.cmd("cd /subsystem=naming")

result = cli.cmd(":jndi-view")
response = result.getResponse()

print 'JNDI VIEW ======================= '
print response
cli.disconnect()

现在按照以下代码执行脚本:

jython script.py

如您所见,代码非常直观;我们正在导入 org.jboss.as.cli.scriptsupport.CLI 类,该类用于发送命令并读取响应。然后,我们连接到本地 WildFly 实例并发出 :jndi-view 命令。

注意

可以使用 connect 命令通过添加以下参数连接到远程 WildFly 主机:connect (String controllerHost, int controllerPort, String username, String password)

响应变量是 org.jboss.dmr.ModelNode。这可以通过以下示例进一步检查,该示例深入探讨了平台 MBeans,以获取一些内存统计信息:

from org.jboss.as.cli.scriptsupport import CLI

cli = CLI.newInstance()
cli.connect()

cli.cmd("cd /core-service=platform-mbean/type=memory/")

result = cli.cmd(":read-resource(recursive=false,proxies=false,include-runtime=true,include-defaults=true)")

response = result.getResponse()
enabled = response.get("result").get("heap-memory-usage")

used = enabled.get("used").asInt()

if used > 512000000:
    print "Over 1/2 Gb Memory usage "
else:
    print 'Low usage!'

cli.disconnect()

在前面的示例中,我们跟踪了 /core-service=platform-mbean/type=memory 中包含的资源。然而,可用的资源实际上是两种可用堆内存区域(heap-memory-usagenon-heap-memory-usage)的子资源,如下面的代码所示:

[standalone@localhost:9990 /] /core-service=platform-mbean/type=memory:read-resource(recursive=false,proxies=false,include-runtime=true,include-defaults=true)
{
    "outcome" => "success","result" => {
        "heap-memory-usage" => {"init" => 67108864L,"used" => 59572256L,"committed" => 170852352L,"max" => 477233152L},
        "non-heap-memory-usage" => {
            "init" => 24313856L,"used" => 90491328L,"committed" => 90701824L,"max" => 369098752L},
        "object-pending-finalization-count" => 0,"verbose" => false
    }
}

仅使用 ModelNode 对象的 get 命令,您就可以引用内存类型的子资源并访问所有单个属性。一旦您获得了属性,就可以使用 ModelNode 对象的 asInt() 函数将它们转换为整数,并使用酷炫的 Python 构造来提醒您的管理员。

使用原始管理 API 管理应用程序服务器

如果您不想学习脚本语言来管理应用程序服务器,您仍然可以在 Java 类中使用原始管理 API。不要受我们将其作为最后一个选项的事实的影响;实际上,使用原生管理 API 并不难,因为它基于非常少的类,并且对 WildFly API 的编译时间和运行时依赖性很小。

因此,您也可以通过简单地将以下依赖项添加到应用程序的 META-INF/MANIFEST.MF 文件中来从任何 Java EE 应用程序中使用管理 API:

Dependencies: org.jboss-as-controller-client,org.jboss.dmr

命名为 未类型化的管理 API 的核心 API 非常简单;主要类是 org.jboss.dmr.ModelNode,我们已经在 Jython 部分提到了它。ModelNode 类本质上只是一个值的包装器;该值通常是可以通过 ModelNodegetType() 方法检索的基本 JDK 类型。

除了 jboss-dmr API 之外,用于连接到管理 API 的另一个模块是 jboss-as-controller-client

注意

您不需要下载这些库,因为这两个模块自 7 版本以来就包含在应用程序服务器中了。

通过原始管理 API 读取管理模型描述

使用 未类型化的管理 API 与其脚本语言对应物并没有太大的不同;最初,您需要创建一个管理客户端,该客户端可以连接到目标进程的本地管理套接字(这可能是一个独立的服务器,或者在一个域模式环境中,是域控制器):

ModelControllerClient client = ModelControllerClient.Factory.create(InetAddress.getByName("localhost"), 9990);

接下来,您需要使用 org.jboss.dmr.ModelNode 类创建一个操作请求对象,如下面的命令所示:

final ModelNode operation = new ModelNode();
operation.get("operation").set("jndi-view");

final ModelNode address = operation.get("address");
address.add("subsystem", "naming");

operation.get("recursive").set(true);
operation.get("operations").set(true);

final ModelNode returnVal = client.execute(operation);
logger.info(returnVal.get("result").toString());

如您所见,ModelNode 对象可以被链起来以到达一个操作(在示例中是 JNDI 视图),该操作在节点路径(在我们的例子中是命名子系统)上可用。

一旦您添加了 ModelNode 属性,您就可以在您的节点上发出 execute 命令,这将反过来返回 ModelNode,其中将存储操作的结果。

在示例中,您可以找到一个包含这些管理示例的完整工作项目。

使用未类型化 API 创建您的资源监视器

现在你已经学习了去类型化管理 API的基础知识,我们将通过一个具体的例子来展示;我们的目标将是使用 EJB 监控服务器资源(数据源的活动 JDBC 连接数)。你可以使用这种模式创建自己的服务器监控,并将其集成到你的应用环境中。以下代码片段展示了这一点:

package com.packtpub.wflydevelopment.chapter9;

import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.dmr.ModelNode;

import javax.ejb.Schedule;
import javax.ejb.Stateless;
import java.io.Closeable;
import java.net.InetAddress;
import java.util.logging.Level;
import java.util.logging.Logger;

@Stateless
public class WatchMyDB {

    private final static Logger logger = Logger.getLogger(WatchMyDB.class.getName());

    @Schedule(dayOfWeek = "*", hour = "*", minute = "*", second =
            "*/30", year = "*", persistent = false)
    public void backgroundProcessing() {
        ModelControllerClient client = null;
        try {
            client = ModelControllerClient.Factory.create(InetAddress.getByName("localhost"), 9990);
            final ModelNode operation = new ModelNode();
            operation.get("operation").set("read-resource");
            operation.get("include-runtime").set(true);
            final ModelNode address = operation.get("address");
            address.add("subsystem", "datasources");
            address.add("data-source", "ExampleDS");
            address.add("statistics", "pool");
            final ModelNode returnVal = client.execute(operation);

            final ModelNode node2 = returnVal.get("result");
            final String stringActiveCount = node2.get("ActiveCount").asString();

            if (stringActiveCount.equals("undefined")) {
                return; // Connection unused
            }
            int activeCount = Integer.parseInt(stringActiveCount);

            if (activeCount > 50) {
                alertAdministrator();
            }
        } catch (Exception exc) {
            logger.log(Level.SEVERE, "Exception !", exc);
        } finally {
            safeClose(client);
        }
    }

    public void safeClose(final Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (Exception e) {
                logger.log(Level.SEVERE, "Exception closing the client! ", e);
            }
        }
    }

    private void alertAdministrator() {
        // Implement it !
    }
}

我们不会重新讨论 EJB 定时器的基本概念,这些概念已经在第三章 介绍 Java EE 7 – EJBs 中讨论过。我们建议你查看代码中突出显示的部分,它展示了如何链接你的 ModelNode 对象,以便达到我们要监控的属性(ExampleDS 数据源的 activeCount 属性)。

一旦你获得了 activeCount 属性的值,我们留给你的想象力去设想所有可能采取的行动!

值得注意的是,还有其他方法可以监控 WildFly。其中之一是使用 JBoss 的 hawt.io 插件(hawt.io/plugins/jboss/)。当我们在开发 MessageBeans 时,我们已经尝试了这种方法。另一个工具是 Jolokia(www.jolokia.org/),它通过 HTTP 暴露 JMX 对象。所以,如果你不打算编写自己的监控程序,还有其他值得探索的选项。

基于角色的安全

在 JBoss 7 中,登录管理员对运行服务器每个配置方面的控制权是无限制的。在多用户可以访问服务器执行不同任务的生产环境中,这可能会成为一个问题。一个用户可能只对部署新应用程序感兴趣,另一个可能只能重启服务器,还可能有一个用户不应该能够更改任何内容(例如,发送应用程序执行数据的监控代理)。

为了支持这些类型的请求,WildFly 提供了两种访问控制策略:

  • 简单来说,这是从 JBoss AS 7 和 EAP 6.2 版本之前的所有或无的访问方法(每个经过身份验证的管理员都有对应用程序服务器的完全访问权限)。这是默认策略。

  • 基于角色的访问控制(RBAC),允许你将管理员用户分配到特定的管理角色。

让我们导航到 http://localhost:8080/console 并使用我们的管理员密码登录。顶部菜单包含一个名为管理的标签页。这用于配置访问控制机制。一旦你点击它(你应该看到一个消息框告诉你 RBAC 尚未启用),我们将看到三个子标签页:用户角色。让我们逐一查看这些对象。

用户使用JBOSS_HOME/bin目录中的add-user.bat.sh)脚本定义。我们已经在第一次访问 JBoss 控制台之前定义了一个。然而,创建的用户需要一些额外的信息来确定其安全级别。最简单的方法是将它们组织到组中。可以通过用户创建脚本或通过 WildFly 配置目录中的mgmt-groups.properties文件来完成分配。另一种方法是定义一个连接到外部源(例如 LDAP 服务器)的安全领域。我们将在下一章中更多地讨论安全领域。现在,您可以创建一个分配给名为TestGroup的组的用户。

一个组映射到一组安全角色以提供特定权限。例如,我们可以为开发人员和初级管理员创建用户组,并将它们映射到所需角色的子集。用户可以是多个组的成员,因此也有可能为特定组排除一个角色,这样其他组就不能授予它。

最后,我们有涵盖服务器功能多个领域的角色。每个角色都分配了一组权限,其中一些权限有额外的限制(例如,允许您仅配置特定子系统(如数据源)的修改)。以下表格中提供了内置角色的列表:

角色 权限 敏感数据(密码和审计)
监控员 只读访问配置和运行时状态。 无访问权限。
操作员 所有监控权限。此角色可以重启服务器,控制 JMS 目的地和数据库连接池。不能修改配置。 无访问权限。
维护员 所有操作员权限。此角色可以修改配置(包括部署新应用程序)。 无访问权限。
部署者 所有维护员权限,但限制部署新应用程序(不能更改服务器的配置)。 无访问权限。
管理员 所有维护员权限。 读写访问。无审计系统访问权限。
审计员 所有监控权限。 只读访问。完全访问审计系统。
超级用户 允许一切。从 JBoss AS 7 中了解的管理员以及 WildFly 中的简单策略。这也是本地用户(从 localhost 连接)的默认角色。 完全访问权限。

除了依赖组-角色映射机制外,您还有另一种将用户分配到角色的方法。您可以使用管理控制台中的管理/用户屏幕直接将用户分配给角色(请确保选择包含作为类型)。现在使用添加按钮将SuperUser角色分配给当前用户。此外,您还可以使用管理/组将新创建的TestGroup添加到,例如监控员角色。

我们现在的配置已经就绪;试着检查一下。要切换到基于角色的访问控制(RBAC)策略,我们需要使用 CLI 界面执行以下命令:

/core-service=management/access=authorization:write-attribute(name=provider, value=rbac)

重新加载服务器,并使用您设计的SuperUser账户再次登录到 Web 控制台。

注意

我们正在测试 Web 控制台,但 RBAC 机制也适用于 CLI。请注意,只要您的安全域中允许了$local用户,CLI 将允许您从localhost访问它:

  <security-realm name="ManagementRealm">
    <authentication>
 <local default-user="$local" allowed-users="*"/>
     <properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
    </authentication>
    <authorization map-groups-to-roles="false">
     <properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
    </authorization>
 </security-realm>

如果您想禁用它,只需删除这一行。

如果您想知道您的当前角色是什么,您可以点击屏幕右上角的用户名。在以下屏幕截图中,您应该会看到有关当前登录管理员的少量信息:

基于角色的安全

在前面的屏幕截图中,我们可以看到用户 root 以SuperUser角色登录。除了可以对应用服务器做任何事情外,SuperUser角色还有一个额外功能。它可以使用**运行为…**来模仿其他角色,这在您想检查另一个角色的限制时可能很有用。现在您可以随意检查它们。例如,作为Monitor角色,您不应该能够在管理控制台中更改任何设置。

您也可以使用您之前创建的用户重新登录,该用户被分配到TestGroup。它应该在屏幕右上角显示Monitor角色。

审计管理操作

WildFly 引入了一个审计日志功能,允许管理员跟踪在服务器上所做的配置更改。该功能最初是禁用的,但在某些场景下可能很有用,所以让我们简要了解一下。

审计日志配置由三部分组成:

  • 格式化器: 这用于格式化日志输出。默认情况下,它基于 JSON 格式。

  • 处理器: 这处理输出。默认情况下,它是一个基于文件的处理器,但也可以使用 TCP 或 UDP 将日志发送到远程服务器。

  • 记录器: 这控制着登录过程。

详细配置可以在官方 WildFly 文档中找到,链接为docs.jboss.org/author/display/WFLY8/Audit+logging

默认情况下,审计日志是禁用的。要启用它,我们必须执行以下 CLI 命令:

/core-service=management/access=audit/logger=audit-log:write-attribute(name=enabled, value=true)

现在,您可以使用 Web 控制台尝试执行任何管理操作(例如,禁用数据源)。之后,您应该在JBOSS_HOME/standalone/data/audit-log.log中找到它的痕迹(以及关于开启审计日志的信息)。

补丁正在运行实例

JBoss 应用服务器的最新版本附带了一个补丁工具,允许您自动使用新版本更新服务器的一部分。目前,补丁是通过命令行界面(CLI)进行的。任何补丁都可以撤销,管理员能够追踪补丁的历史记录。

可以通过简单地调用patch apply <文件路径> (不使用-)命令来应用补丁。一个互补的命令是patch rollback --patch-id = id,即补丁回滚命令。要获取有关已安装补丁的信息,只需调用patch info。补丁由负责特定 WildFly 子系统的团队分发。如果您需要特定模块的补丁,请访问他们的网站。

摘要

在本章中,我们从开发者的角度介绍了应用服务器的管理 API,这将使您能够编写自己的脚本来监控应用服务器的健康状况。

监控应用服务器的最有效工具是命令行界面。然而,如果您想通过一些典型的编程逻辑来增加趣味性,您可以选择其他一些替代方案,例如脚本语言或原始的管理 API。

我们还探讨了 WildFly 引入的一些新、高级功能。您现在知道如何限制对管理控制台的访问以及如何审计对配置所做的更改。

我们现在已经完成了对管理的回顾。在下一章中,我们将讨论集群,这是关键应用程序部署的环境。

第十章。保护 WildFly 应用程序

在上一章中,我们描述了如何管理您的应用服务器。我们旅程的下一个目的地将是学习安全,这是任何企业应用程序的关键元素。您必须能够控制并限制谁被允许访问您的应用程序以及用户可以执行的操作。

Java 企业版规范为 Enterprise JavaBeans 和 Web 组件定义了一个简单的基于角色的安全模型。WildFly 安全的实现由Picketbox框架(以前称为 JBoss Security)提供,它是应用服务器的一部分,并为 Java 应用程序提供认证、授权、审计和映射功能。

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

  • Java 安全 API 简介

  • WildFly 安全子系统的基石

  • 定义并应用登录模块以保护 Java EE 应用程序

  • 使用安全套接字层SSL)协议加密流量

接近 Java 安全 API

Java EE 安全服务提供了一种强大且易于配置的安全机制,用于认证用户并授权访问应用程序功能和相关数据。为了更好地理解与安全相关的主题,我们首先应该列出一些基本定义:

  • 认证:这是验证当前执行应用程序的用户身份的过程,无论它是 EJB 还是 servlet(等等)。认证通常通过包含在 Web/独立应用程序中的Login模块来完成。Java EE 规范仅提供了所有兼容容器必须满足的一般要求。这意味着每个应用服务器都提供自己的认证机制,这在应用程序的可移植性和配置方面会带来问题。

  • 授权:这是验证用户是否有权(权限)访问系统资源或调用某些操作的过程。因此,授权假设认证已经发生;如果你不知道用户是谁,就无法授予任何访问控制。Java EE 规范提供了授权用户操作的手段。授权声明通常在不同应用服务器之间是可移植的。认证和授权之间的区别在以下图中展示:接近 Java 安全 API

在 Java EE 中,容器负责提供应用程序安全。容器基本上提供两种类型的安全:声明性和程序性。让我们来看看这两种类型:

  • 声明式安全:通过部署描述符表达应用程序组件的安全需求。因为部署描述符信息包含在外部文件中,所以可以在不修改源代码的情况下进行更改。

    例如,企业 JavaBeans 组件使用 EJB 部署描述符,其名称必须是 ejb-jar.xml,并放置在 EJB JAR 文件的 META-INF 文件夹中。

    Web 组件使用名为 web.xml 的 Web 应用程序部署描述符,位于 WEB-INF 目录中。

    小贴士

    自 Java EE 5 发布以来,你可以通过注解的方式应用声明式安全,就像我们对其他关键 API(EJB、Web 服务等)所做的那样。注解在类文件中指定,当应用程序部署时,应用程序服务器会内部转换这些信息。

  • 程序性安全:嵌入在应用程序中,用于做出安全决策。当仅使用声明式安全不足以表达应用程序的安全模型时,可以使用它。Java EE 安全 API 允许开发者使用以下调用测试当前用户是否有权访问特定角色:

    • isUserInRole() 用于 servlets 和 JSPs(在 javax.servlet.http.HttpServletRequest 中采用)

    • isCallerInRole() 用于 EJBs(在 javax.ejb.SessionContext 中采用)

    此外,还有其他 API 调用可以提供对用户身份的访问,如下所示:

    • getUserPrincipal() 用于 servlets 和 JSPs(在 javax.servlet.http.HttpServletRequest 中采用)

    • getCallerPrincipal() 用于 EJBs(在 javax.ejb.SessionContext 中采用)

    使用这些 API,你可以开发任意复杂的授权模型。

WildFly 安全子系统

WildFly 安全性作为应用程序服务器的扩展,默认情况下包含在独立服务器和域服务器中,以下代码所示:

<extension module="org.jboss.as.security"/>

WildFly 使用两个术语定义安全策略:安全领域和安全域。安全领域是映射到外部连接器的配置集(例如,EJB 远程和管理接口)。它们允许每种连接类型都有其适当的身份验证和授权属性定义。例如,管理和应用程序领域定义了两个单独的文件,存储允许的用户名。此外,应用程序领域包含一个指向定义用户角色的文件的引用。

然后将安全域中定义的配置传递给已部署应用程序请求的安全域。安全域定义了一组负责检查用户凭据并创建代表客户端的安全主体(以及请求者的角色集)的登录模块。

以下是从服务器配置文件中提取的默认安全子系统,其中包含将在下一节中用于保护 Ticket 示例应用程序的RealmDirect登录:

<subsystem >
    <security-domains>
        <security-domain name="other" cache-type="default">
            <authentication>
                <login-module code="Remoting" flag="optional">
                    <module-option name="password-stacking" value="useFirstPass"/>
                </login-module>
 <login-module code="RealmDirect" flag="required">
 <module-option name="password-stacking" value="useFirstPass"/>
 </login-module>
            </authentication>
        </security-domain>
        <security-domain name="jboss-web-policy" cache-type="default">
            <authorization>
                <policy-module code="Delegating" flag="required"/>
            </authorization>
        </security-domain>
        <security-domain name="jboss-ejb-policy" cache-type="default">
            <authorization>
                <policy-module code="Delegating" flag="required"/>
            </authorization>
        </security-domain>
    </security-domains>
</subsystem>

使用以下代码在安全域中定义配置文件:

<security-realm name="ApplicationRealm">
    <authentication>
        <local default-user="$local" allowed-users="*"/>
            <properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
     </authentication>
    <authorization>
        <properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
   </authorization>
</security-realm>

如您所见,配置相当简短,因为它在很大程度上依赖于默认值,特别是对于像安全管理区域这样的高级结构。通过定义自己的安全管理选项,例如,您可以覆盖默认的认证/授权管理器,使用您的实现。由于您可能不需要覆盖这些接口,我们将更关注security-domain元素,这是 WildFly 安全的核心方面。

安全域可以被视为外国人的海关办公室。在请求越过 WildFly 边界之前,安全域会执行所有必要的身份验证和授权检查,并最终通知他/她是否可以继续。

安全域通常在服务器启动时配置,并随后绑定到java:/jaas/键下的 JNDI 树中。在安全域内,您可以配置登录认证模块,以便您可以简单地通过更改其login-module元素来轻松更改您的认证提供者。

默认情况下,提供了几个登录模块的实现;显然,这里没有足够的空间来详细描述每个模块的功能,尽管我们将提供一些流行选项的全面描述,例如:

  • RealmDirect登录模块,可用于基于基本文件的身份验证

  • Database登录模块,该模块将用户凭据与关系数据库进行核对

注意

如果您需要有关登录模块的更多信息,请查看 WildFly 文档:

设置您的第一个登录模块

在以下部分,我们将演示如何使用之前介绍的RealmDirect安全域来保护应用程序。RealmDirect登录模块基于以下两个文件:

  • application-users.properties:此文件包含用户名和密码列表

  • application-roles.properties:此文件包含用户与其角色之间的映射

这些文件位于应用程序服务器配置文件夹中,每次您通过add-user.sh/add-user.cmd脚本添加新用户时,它们都会被更新。为了我们的目的,我们将创建一个名为demouser的新应用程序用户,该用户属于Manager角色,如下面的截图所示:

设置您的第一个登录模块

用户添加后,application-users.properties 文件将包含用户名和密码的 MD5 编码,如下所示:

demouser=9e21f32c593ef5248e7d6b2aab28717b

相反,application-roles.properties 文件将包含登录后授予 demouser 用户名的角色:

demouser=Manager

在 Ticket 网络应用程序中使用登录模块

我们现在可以在 第四章 中描述的 Ticket 网络应用程序中应用 RoleDirect 登录模块,学习上下文和依赖注入(如果你喜欢,也可以从其他章节选择版本)。我们首先将展示如何提供基本的 Web 身份验证,然后我们将展示一个使用基于表单的身份验证的稍微复杂一些的例子。

注意

BASIC 访问身份验证是在通过浏览器进行请求时提供用户名和密码的最简单方式。

它通过发送包含用户凭据的编码字符串来工作。这个 Base64 编码的字符串被传输并解码,结果是冒号分隔的用户名和密码字符串。当涉及到安全性时,基本身份验证通常不是最佳解决方案。密码可能在传输过程中被盗,因此 SSL 是必须的,以保护它。

启用 Web 身份验证需要在 Web 应用程序配置文件(web.xml)中定义 security-constraints 元素,如下面的代码片段所示:

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

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>HtmlAuth</web-resource-name>
            <description>application security constraints
            </description>
            <url-pattern>/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>Manager</role-name>
        </auth-constraint>
    </security-constraint>
    <login-config>
        <auth-method>BASIC</auth-method>
    </login-config>

    <security-role>
        <role-name>Manager</role-name>
    </security-role>
</web-app>

此配置将在 Web 应用程序中的任何 JSP/servlet 上添加一个安全约束,将限制对具有 Manager 角色的已验证用户的访问。前面部分中显示的所有登录模块都定义了此角色,因此你可以使用最适合你需求的登录模块。

从 Java EE 7 开始,有另外两种方法来表示你的安全约束。首先,你可以使用一个新的容器提供的角色:**。这表示你正在引用任何已认证的用户,不考虑其角色。

第二个是 deny-http-uncovered-methods 标签,可以在 web.xml 文件中使用,以禁止访问未由单独安全约束覆盖的任何 HTTP 方法。

下一个配置调整需要在 JBoss 网络部署的描述符 WEB-INF/jboss-web.xml 上执行。你需要在这里声明安全域,它将被用来验证用户。由于我们使用的是 RealmDirect,它是其他内置登录模块的一部分,因此我们需要包含 java:/jaas/other 上下文信息:

<jboss-web> 
 <security-domain>java:/jaas/other</security-domain>
</jboss-web>

下面的图示说明了应用于 Database 登录模块的整个配置序列:

在 Ticket 网络应用程序中使用登录模块

一旦部署了你的应用程序,结果应该是一个阻塞的弹出窗口,要求用户进行身份验证。在不同的浏览器上,窗口的外观可能会有所不同,其外观无法更改。

使用 demouser 用户名和有效的密码登录将授予 Manager 角色的应用程序访问权限。

切换到基于 FORM 的安全

基于 FORM 的身份验证允许开发者自定义身份验证用户界面,例如,适应贵公司的标准。在您的应用程序中配置它需要您基本上只修改 web.xml 文件安全部分的 login-config 段。在其中,我们将定义一个登录着陆页(login.xhtml)和一个错误页(error.xhtml),以防登录失败。相应的代码片段如下:

<login-config>
    <auth-method>FORM</auth-method>
    <form-login-config>
      <form-login-page>/faces/login.xhtml</form-login-page>
      <form-error-page>/faces/error.xhtml</form-error-page>
    </form-login-config>
</login-config>

登录表单必须包含用于输入用户名和密码的字段。这些字段分别命名为 j_usernamej_password。认证表单应将这些值提交到 j_security_check 逻辑名称。所有以 j_ 开头的名称都由 Java Servlet 规范标准化——我们只需遵循约定,以便让自动机制工作。以下是一个简单的 login.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 
      >
<head>
    <title>FORM based Login</title>
</head>
<body>
<form method="post" action="j_security_check" name="loginForm">
    <h:panelGrid columns="2">
        <h:outputLabel id="userNameLabel" for="j_username" value="Username:"/>
        <h:inputText id="j_username" autocomplete="off"/>
        <h:outputLabel id="passwordLabel" for="j_password" value="Password:"/>
        <h:inputSecret id="j_password" autocomplete="off"/>

        <div/>
        <h:panelGroup>
            <h:commandButton type="submit" value="Login"/>
            <h:commandButton type="reset" value="Clear"/>
        </h:panelGroup>
    </h:panelGrid>
</form>
</body>
</html>

为了简洁起见,我们不会包括错误页,它将简单地提醒用户输入了错误的用户名和密码组合。预期的结果是以下登录屏幕,它将拦截所有用户对您的应用程序的访问,如果 usernamepassword 凭据正确,则授予对默认主页的访问权限。

创建数据库登录模块

UserRoles 登录模块是学习如何组合所有用于保护 Web 应用程序所需的组件的好起点。在实际情况下,有更好的替代方案来保护您的应用程序,例如 Database 登录模块。数据库安全域遵循之前示例中公开的逻辑;它只是在数据库中存储凭据。为了运行此示例,我们将参考在 第五章 中定义的数据源,将持久性与 CDI 结合(绑定在 JNDI 名称 java:jboss/datasources/wflydevelopment),需要在应用程序服务器上部署:

<security-domain name="dbdomain" cache-type="default">
    <authentication>
        <login-module code="Database" flag="required">
            <module-option name="dsJndiName" value=" java:jboss/datasources/wflydevelopment"/>
            <module-option name="principalsQuery" value="select passwd from USERS where login=?"/>
            <module-option name="rolesQuery" value="select role 'Roles' from USER_ROLES where login=?"/>
        </login-module>
    </authentication>
</security-domain>

为了使此配置生效,您必须首先创建所需的表,并使用以下查询在表中插入一些示例数据:

CREATE TABLE USERS(login VARCHAR(64) PRIMARY KEY, passwd VACHAR(64));
CREATE TABLE USER_ROLES(login VARCHAR(64), role VARCHAR(32));
INSERT into USERS values('admin', 'admin');
INSERT into USER_ROLES values('admin', 'Manager');

如您所见,admin 用户将再次映射到 Manager 角色。此配置的一个注意事项是它使用数据库中的明文密码;因此,在将此模块投入生产之前,您应该考虑在登录模块中添加额外的安全性。让我们在下一节中看看如何做。

加密密码

将密码以明文字符串的形式存储在数据库中不被认为是良好的做法;事实上,数据库甚至比常规文件系统有更多的潜在安全漏洞。例如,想象一下,如果数据库管理员为某些表添加了一个公共同义词,忘记了其中一张表包含了敏感信息,如应用程序密码,如下面的截图所示!您随后需要确保没有任何潜在的攻击者能够执行以下查询。

加密密码

幸运的是,保护应用程序密码相对简单;您可以在登录模块中添加一些额外的选项,指定存储的密码使用消息摘要算法进行加密。例如,在Database登录模块中,您应该在底部添加以下突出显示的选项:

<login-module code="Database" flag="required">
     <module-option name="dsJndiName" value="java:jboss/datasources/wflydevelopment"/>
     <module-option name="principalsQuery" value="select passwd from USERS where login=?"/>
     <module-option name="rolesQuery" value="select role, 'Roles' from USER_ROLES where login=?"/>
 <module-option name="hashAlgorithm" value="SHA-256"/>
 <module-option name="hashEncoding" value="BASE64"/>
</login-module>

在这里,我们指定密码将与 SHA 哈希算法进行散列;或者,您可以使用 JCA 提供程序允许的任何其他算法。

注意

对于对哈希算法的优秀介绍,请参阅www.unixwiz.net/techtips/iguide-crypto-hashes.html

为了完整性,我们包括以下小应用程序,它生成要插入到Database中的 Base64 散列密码:

public class Hash {

    public static void main(String[] args) throws Exception{
       String password = args[0];
       MessageDigest md = MessageDigest.getInstance("SHA-256");
       byte[] passwordBytes = password.getBytes();
       byte[] hash = md.digest(passwordBytes);
 String passwordHash = 
            Base64.getEncoder().encodeToString(hash);

       System.out.println("password hash: "+passwordHash);
}

使用admin作为参数运行主程序将生成哈希jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg=。这个哈希将是您的更新后的密码,您需要将其更新到您的数据库中,如下面的截图所示。使用以下代码更新密码:

UPDATE USERS SET PASSWD =  'jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg=' WHERE LOGIN = 'admin';

您可以使用您选择的任何 SQL 客户端来更新它。

加密密码

在您的应用程序中使用 Database 登录模块

完成登录模块配置后,不要忘记通过 JBoss web 部署的描述符WEB-INF/jboss-web.xml引用它:

<jboss-web> 
 <security-domain>java:/jaas/dbdomain</security-domain>
</jboss-web>

保护 EJBs

TheatreBooker SFSB, which we discussed in Chapter 4, *Learning Context and Dependency Injection*:
@RolesAllowed("Manager")
@SecurityDomain("dbdomain")
@Stateful
@Remote(TheatreBooker.class) 
public class TheatreBooker implements TheatreBooker {

}

注意

请注意!有多个SecurityDomain API 可用。您必须包含org.jboss.ejb3.annotation.SecurityDomain。另一方面,@RolesAllowed注解需要导入javax.annotation.security.RolesAllowed

JBoss 特定的注解可以在以下 maven 依赖项中找到:

<groupId>org.jboss.ejb3</groupId>
<artifactId>jboss-ejb3-ext-api</artifactId>
<version>2.0.0</version>
<scope>provided</scope>

注解也可以应用于方法级别;例如,如果我们只想保护TheatreBookerBean类的bookSeat对象,我们将如下标记bookSeat方法:

@RolesAllowed("Manager")
@SecurityDomain("dbdomain")
public String bookSeat(int seatId) throws SeatBookedException {

}

如果您不想使用注解来建立安全角色怎么办?例如,如果您有一个被所有 EJB 应用程序跨域使用的安全角色,可能使用普通的旧 XML 配置而不是给所有 EJB 添加注解会更简单。在这种情况下,您必须在通用的META-INF/ejb-jar.xml文件中首先声明安全约束,如下所示:

<ejb-jar  
  version="3.2"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_2.xsd">
  <assembly-descriptor>
    <method-permission>
      <role-name>Manager</role-name>
      <method>
        <ejb-name>*</ejb-name>
        <method-name>*</method-name>
      </method>
    </method-permission>
  </assembly-descriptor>
</ejb-jar>

然后,在META-INF/jboss-ejb3.xml配置文件中,只需添加对您的安全域的引用:

<?xml version="1.0" encoding="UTF-8"?>
<jboss:ejb-jar 

   version="3.1" impl-version="2.0">
  <assembly-descriptor>
    <s:security>
      <ejb-name>*</ejb-name>
      <s:security-domain>dbdomain</s:security-domain>
    </s:security>
  </assembly-descriptor>
</jboss:ejb-jar>

下面是一个展示 EJB 文件角色配置的快照:

保护 EJB

注意

如果您想通过 EJB 远程使用登录模块,您必须使用以下代码中的 JAAS 条目相应地配置您的安全领域:

<security-realm name="ApplicationRealm">
  <authentication>
   <jaas name="dbdomain"/>
  </authentication>
</security-realm>

此外,您应该在 jbossyourjboss-ejb-client-properties 中放置以下条目:

remote.connection.default.username=admin
remote.connection.default.password=admin
remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false
remote.connection.default.connect.options.org.xnio.Options.SASL_POLICY_NOPLAINTEXT=false
remote.connection.default.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=true

这些条目将确保(除了传递凭证之外),传输的密码不会被远程机制额外散列。

保护网络服务

根据我们是否处理基于 POJO 的网络服务或基于 EJB 的网络服务,基本可以通过两种方式执行 Web 服务授权。安全更改与我们对 servlets/JSP 介绍的安全更改相同,一致地定义 web.xml 中的 security-constraints 元素和 jboss-web.xml 中的登录模块。

如果您使用 Web 客户端来访问您的网络服务,这将是您需要的一切以进行认证。如果您使用的是独立客户端,您需要在 JAX-WS 工厂中指定凭证。以下是如何访问在 第七章 中描述的受保护 CalculatePowerService 实例的示例,该实例在 将 Web 服务添加到您的应用程序 中进行了描述:

JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();

factory.getInInterceptors().add(new LoggingInInterceptor());
factory.getOutInterceptors().add(new LoggingOutInterceptor());

factory.setServiceClass(CalculatePowerWebService.class);
factory.setAddress("http://localhost:8080/pojoService");
factory.setUsername("admin");
factory.setPassword("admin");
CalculatePowerWebService client = (CalculatePowerWebService) factory.create();

那么基于 EJB 的网络服务呢?配置略有不同;由于在 Web 描述符中没有指定安全域,我们必须通过注解来提供它:

@Stateless
@WebService(targetNamespace = "http://www.packtpub.com/", serviceName = "TicketWebService") 
@WebContext(authMethod = "BASIC",
 secureWSDLAccess = false)
@SecurityDomain(value = "dbdomain")
@RolesAllowed("Manager")
public class TicketSOAPService implements TicketSOAPServiceItf, Serializable {

   . . . . 
}

正如你所见,@org.jboss.ws.api.annotation.Webcontext 注解基本上反映了与基于 POJO 的网络服务相同的配置选项,包括基本身份验证和无限制的 WSDL 访问。

注意

@WebContext 注解可以在以下依赖项中找到:

    <dependency>
      <groupId>org.jboss.ws</groupId>
      <artifactId>jbossws-api</artifactId>
      <version>1.0.2.Final</version>
      <scope>provided</scope>
    </dependency>

@org.jboss.ejb3.annotation.SecurityDomain 注解应该对你来说很熟悉,因为我们曾用它来演示如何保护 EJB。正如你所见,它是 jboss-web.xml 文件中包含信息的替代品,除了安全域是直接通过 dbdomain(而不是 java:/jaas/dbdomain)引用的。

注意

如果您更喜欢使用标准配置文件,之前的网络安全配置也可以通过 META-INF/ejb-jar.xmlMETA-INF/jboss-ejb3.xml 文件来指定。

要将您的登录凭证传递给网络服务,您可以使用 RequestContext 对象:

final TicketWebService infoService = service.getPort(TicketWebService.class);
Map<String, Object> requestContext = ((BindingProvider) infoService).getRequestContext();
requestContext.put(BindingProvider.USERNAME_PROPERTY, "admin");
requestContext.put(BindingProvider.PASSWORD_PROPERTY, "admin");

用户名和密码值将被传递到安全域中定义的登录模块,就像在其他任何认证方法中一样。

保护传输层

如果你仅凭至今所学的基础概念来创建一个关键任务应用,你将面临各种安全威胁。例如,如果你需要设计一个支付网关,其中信用卡信息通过 EJB 或 servlet 传输,仅使用授权和认证栈是远远不够的,因为敏感信息仍然通过网络传输,可能被黑客泄露。

为了防止关键信息被未经授权的个人或系统泄露,你必须使用一种提供信息加密的协议。加密是将数据转换成未经授权的人无法理解的形式的过程。相反,解密是将加密数据转换回原始形式以便理解的过程。

用于保护通信的协议是 SSL 和 TLS,后者被认为是较老 SSL 的替代品。

提示

这两种协议之间的差异很小,且非常技术性。简而言之,TLS 使用更强的加密算法,并且能够在不同的端口上工作。在本章的其余部分,我们将对这两种协议都使用 SSL。有关更多信息,请查看维基百科:en.wikipedia.org/wiki/Transport_Layer_Security

加密信息有两种基本技术:对称加密(也称为密钥加密)和非对称加密(也称为公钥加密)。

对称加密是最古老且最广为人知的技巧。它基于一个密钥,该密钥应用于消息文本以改变内容。只要发送者和接收者都知道这个密钥,他们就可以加密和解密使用此密钥的所有消息。这些加密算法通常运行速度快,非常适合一次性加密大量消息。

对称算法的一个显著问题是需要安全的行政组织来向用户分发密钥。这通常会导致行政方面的开销增加,同时密钥仍然容易受到未经授权的泄露和潜在滥用的威胁。

因此,关键任务的企业系统通常依赖于非对称加密算法,这些算法通常更容易部署、管理和使用,最终也更加安全。

非对称密码学,也称为公钥密码学,基于这样一个概念:用于加密的密钥与用于解密消息的密钥不同。在实践中,每个用户都持有几对密钥:分发给其他方的公钥和作为秘密保留的私钥。每条消息都使用接收者的公钥进行加密,并且只能用接收者的私钥(如以下图所示)进行解密:

保护传输层

使用非对称加密,你可以确信你的消息不会被泄露给第三方。然而,仍然存在一个漏洞。

假设你想与一个商业伙伴交换一些有价值的信息,为此你通过电话或电子邮件请求他的公钥。一个欺诈用户拦截了你的电子邮件或简单地监听你的对话,并迅速发送给你一封带有他公钥的伪造邮件。现在,即使你的数据传输是加密的,它也会被错误地发送给错误的人!

为了解决这个问题,我们需要一份文件来验证公钥属于特定的个人。这份文件被称为数字证书或公钥证书。数字证书由一个格式化的数据块组成,其中包含证书持有者的名称(可能是用户名或系统名)和持有者的公钥,以及认证机构CA)的数字签名以进行认证。CA 证实发送者的名称与文件中公钥关联。

保护传输层

公钥证书通常用于确保与网站的交互安全。默认情况下,网络浏览器附带一组预定义的 CA;它们用于验证当你输入一个安全网站时,提供给浏览器的公钥证书确实是由网站所有者签发的。简而言之,如果你将浏览器连接到https://www.abc.com,并且你的浏览器没有给出任何证书警告,你可以安全地与负责该网站的实体进行交互,除非该网站或你的浏览器已被黑客攻击。然而,这又是另一个故事。

注意

简单认证和客户端认证

在前面的例子中,我们描述了一个简单的认证(也称为服务器认证)。在这个场景中,唯一需要证明其身份的方是服务器。

然而,SSL 能够执行相互认证(也称为客户端或双向认证);在这里,服务器在网络 SSL 握手期间也会请求客户端证书。

客户端认证需要从 CA 获取的 x.509 格式的客户端证书。x.509 格式是 SSL 证书的行业标准格式。在下一节中,我们将探讨哪些工具可用于生成数字证书,以及如何让你的证书由 CA 签名。

在 WildFly 上启用安全套接字层

WildFly 使用Java 安全套接字扩展JSSE),它是 Java SE 的一部分,用于利用 SSL/TLS 通信。

企业应用程序可以在两个不同的位置进行安全设置:Web 应用的 HTTP 级别,以及使用 EJB 的应用程序的 RMI 级别。HTTP 通信由standalone.xml/domain.xml文件中的 Web 子系统处理。另一方面,保护 RMI 传输并不是您应用程序的强制要求。实际上,在大多数生产环境中,WildFly 都放置在防火墙后面。

如您从以下图中可以看出,这意味着您的 EJBs 不是直接暴露在不信任的网络中,这些网络通常通过放置在非军事区的 Web 服务器连接:

在 WildFly 上启用安全套接字层

为了开始使用 WildFly 和 SSL,我们需要一个工具来生成用于 SSL 服务器套接字的公钥/私钥对,形式为 x.509 证书。这将在下一节中介绍。

证书管理工具

可以用来设置数字证书的一个工具是keytool,这是一个 Java SE 附带的关键和证书管理实用程序。它使用户能够管理自己的公钥/私钥对及其相关证书,用于自我认证(用户向其他用户或服务进行身份验证)或数据完整性和认证服务,使用数字签名。它还允许用户缓存其通信对等方的公钥(以证书的形式)。

keytool 将密钥和证书存储在称为密钥库的文件中,这是一个用于识别客户端或服务器的证书存储库。通常,密钥库包含客户端或服务器的身份,由密码保护。让我们看看密钥库生成的示例:

keytool -genkey -keystore wildfly.keystore -storepass mypassword -keypass mypassword -keyalg RSA -validity 180  -alias wflyalias   -dname "cn=John Smith,o=PackPub,c=GB"

此命令在当前工作目录中创建名为wildfly.keystore的密钥库,并为其分配密码mypassword。它为具有通用名John Smith、组织PacktPub和两位字母国家代码GB的唯一名称的实体生成公钥/私钥对。

此操作的输出将是一个自签名证书(使用 RSA 签名算法),它包括公钥和唯一名称。此证书将有效期为 180 天,并与密钥库条目中引用的别名wflyalias关联的私钥相关联。

小贴士

自签名证书是一个未经 CA 验证的证书,因此,您容易受到经典的中间人攻击。自签名证书仅适用于内部使用或测试,直到您真正的证书到达。

使用自签名证书保护 HTTP 通信

现在让我们看看如何使用此密钥库文件来保护您的 WildFly Web 通道。打开您的服务器配置文件,定位 Web 子系统。

在 Web 子系统中,您必须首先更改默认的 http-listenersocket-bindinghttps-listener"https",并将 security-realm 元素添加到其中。接下来,您必须在其中插入一个 ssl 子句,其中包含您的 keystore 对象的详细信息(在我们的示例中,我们将文件 jboss.keystore 放入服务器配置目录中):

<subsystem >
            <buffer-caches>
                <buffer-cache name="default" buffer-size="1024" buffers-per-region="1024" max-regions="10"/>
            </buffer-caches>
            <server name="default-server">
 <https-listener name="default" socket-binding="https" security-realm="EJBRealm"/>
                <host name="default-host" alias="localhost">
                    <location name="/" handler="welcome-content"/>
                    <filter-ref name="server-header"/>
                    <filter-ref name="x-powered-by-header"/>
                </host>
            </server>
            <servlet-container name="default" default-buffer-cache="default" stack-trace-on-error="local-only">
                <jsp-config/>
            </servlet-container>
            // some more code
        </subsystem>

如您所见,我们在配置中引用了 EJBRealm,但我们仍然需要定义它。我们将在下一节中这样做。

生成服务器和客户端证书

首先,为具有通用名 John Smith、组织 PacktPub 和两字母国家代码 GB 的实体的公私钥对进行生成。

keytool -genkey -v -alias wflyAlias -keyalg RSA -keysize 1024 -keystore wfly.keystore -validity 180 -keypass mypassword -storepass mypassword -dname "cn=John Smith,o=PacktPub,c=GB"

接下来,将服务器的公钥导出到一个名为 sslPublicKey.cer 的证书中,该证书使用密码 mypassword

keytool -export -keystore jboss.keystore -alias wflyAlias -file sslPublicKey.cer -keypass mypassword -storepass mypassword

现在我们已经完成了服务器的配置,我们也将为客户端生成一个密钥对。我们将通过使用别名 ejbclientalias 和与服务器 keystore 对象相同的属性来完成此操作:

keytool -genkey -v -alias ejbclientalias -keyalg RSA -keysize 1024 -keystore jbossClient.keystore -validity 180 -keypass clientPassword -storepass clientPassword -dname "cn=John Smith,o=PacktPub,c=GB"

客户端公钥也将导出到一个名为 clientPublicKey.cer 的证书中。

keytool -export -keystore jbossClient.keystore -alias ejbclientalias -file clientPublicKey.cer -keypass clientPassword -storepass clientPassword

现在,为了成功完成 SSL 握手,我们首先需要将客户端的公钥导入服务器的 truststore 对象中:

keytool -import -v -trustcacerts -alias ejbclientalias -file clientPublicKey.cer -keystore jboss.keystore -keypass mypassword -storepass mypassword

服务器证书也需要被客户端信任。您有两个可用的选项来解决此问题,如下所示:

  • 将服务器证书导入客户端的 JDK 证书包中

  • 创建一个客户端信任的证书新存储库(truststore

将服务器证书导入客户端 JDK 意味着执行一个证书导入到客户端的认证机构中。

keytool -import -v -trustcacerts -alias wflyAlias -file sslPublicKey.cer -keystore C:\Java\jdk1.8.0_20\jre\lib\security\cacerts 

我们只需将我们使用的路径替换为实际的 JDK 路径,并使用客户端存储库的密码(默认值为 changeit)来完成此操作。

否则,如果您想将证书导入到一个新创建的 truststore 对象中,只需将 cacerts 目标替换为您的客户端 truststore 对象。

keytool -import -v -trustcacerts -alias wflyAlias -file sslPublicKey.cer -keystore jbossClient.keystore -keypass clientPassword -storepass  clientPassword

注意

如果您选择后者,您需要将以下属性添加到您的客户端 JDK 参数中,这将覆盖默认 JDK 的 truststore 对象:

java -Djavax.net.ssl.trustStore=<truststorefile>
-Djavax.net.ssl.trustStorePassword=<password>

创建一个支持 SSL 的安全域

在 WildFly 中,安全域用于保护对管理接口、HTTP 接口和远程 JNDI 及 EJB 访问的访问。在安全域内,还可以为服务器定义一个身份;此身份可以用于服务器的外部连接和服务器正在建立的内部连接。

因此,为了启用我们的 EJB 通信和 HTTP 的 SSL 通信,我们将定义一个名为 EJBRealm 的安全域,该域绑定到一个服务器身份,该身份引用服务器的 keystore 对象,如下所示:

<security-realm name="EJBRealm">
<server-identities>
      <ssl>
 <keystore path="jboss.keystore" relative-to="jboss.server.config.dir" keystore-password="mypassword"/>
      </ssl>
   </server-identities>
   <authentication>
 <jaas name="ejb-security-domain"/>
   </authentication>
</security-realm>

除了包含存储 SSL 证书的位置外,这个安全域还包含你的 EJB 使用的身份验证策略,该策略由名为ejb-security-domain的 JAAS 安全域定义。

以下是一个安全域定义,它是一个简单的基于文件的包含用户凭据和角色的安全域,分别在ejb-users.propertiesejb-roles.properties文件中:

<security-domain name="ejb-security-domain" cache-type="default">
<authentication>
  <login-module code="Remoting" flag="optional">
    <module-option name="password-stacking" value="useFirstPass"/>
  </login-module>
  <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule" flag="required">
    <module-option name="defaultUsersProperties" value="${jboss.server.config.dir}/ejb-users.properties"/>
    <module-option name="defaultRolesProperties" value="${jboss.server.config.dir}/ejb-roles.properties"/>
    <module-option name="usersProperties" value="${jboss.server.config.dir}/ejb-users.properties"/>
    <module-option name="rolesProperties" value="${jboss.server.config.dir}/ejb-roles.properties"/>
    <module-option name="password-stacking" value="useFirstPass"/>
  </login-module>
</authentication>
</security-domain>

正如你所想象的那样,你需要创建两个属性文件,每个文件中都有一些值。例如,以下是将放置在服务器配置文件夹中的ejb-user.properties文件:

adminUser=admin123

以下是对应的ejb-roles.properties文件,它将ejbRole角色授予adminUser角色:

adminUser=ejbRole

最后的配置工作是在你的remoting连接器的security-realm属性中指定它:

<subsystem >
    <endpoint worker="default"/>
    <http-connector name="http-remoting-connector" 
    connector-ref="default" 
 security-realm="EJBRealm"/>
</subsystem>

让我们检查我们的工作成果。首先,我们将尝试 HTTPS 连接。

你必须重新启动 WildFly 以激活更改。你应该在你的控制台底部看到以下日志,它通知你新的 HTTPS 通道正在端口 8443 上运行:

[org.wildfly.extension.undertow] (MSC service thread 1-9) JBAS017519: Undertow HTTP listener default listening on /127.0.0.1:8443

如果你尝试使用安全通道(例如,https://localhost:8443/ticket-agency-cdi)访问 Ticket 示例,以下屏幕将是 Internet Explorer(不要在家尝试)浏览器将显示的内容(其他浏览器如 Firefox 和 Google Chrome 将以不同格式显示相同类型的错误消息):

创建一个支持 SSL 的安全域

发生了什么?一旦你与 Web 服务器建立了安全连接,服务器证书将被发送到浏览器。由于证书没有被任何已识别的 CA 签发,浏览器安全沙盒会警告用户潜在的威胁。

这是一个内部测试,因此我们可以通过选择继续访问此网站来安全地进行。这就是你为了使用自签名证书激活安全套接字层所需做的全部事情。

使用由 CA 签名的证书保护 HTTP 通信

让你的证书被签发需要向 CA 发出一个证书签名请求CSR),CA 将返回一个已签名的证书以安装到你的服务器上。这表明你的组织将产生一定的成本,这取决于你请求的证书数量、加密强度和其他因素。

首先,使用新创建的keystore和密钥条目生成 CSR:

keytool -certreq -keystore jboss.keystore -alias wflyalias -storepass mypassword -keypass mypassword  -keyalg RSA  -file certreq.csr

这将创建一个名为certreq.csr的新证书请求,具有以下格式:

-----BEGIN NEW CERTIFICATE REQUEST-----
. . . . . .
-----END NEW CERTIFICATE REQUEST-----

需要将之前的证书传输给 CA。在注册阶段结束时,CA 将返回一个已签名的证书,需要将其导入到你的密钥链中。以下代码假设你将 CA 证书保存在名为signed_ca.txt的文件中:

keytool -import -keystore jboss.keystore -alias testkey1 -storepass mypassword -keypass mypassword -file signed_ca.txt

现在,你的网络浏览器将识别你的新证书是由 CA 签发的,因此它不会因为无法验证证书而抱怨。

保护 EJB 通信

EJB 客户端使用 RMI-IIOP 协议与企业 EJB 层交互。RMI-IIOP 协议是由 Sun 开发的,旨在将 RMI 编程模型与 IIOP 基础传输相结合。

对于具有严格安全策略的应用程序,需要保护 EJB 传输,这些策略不能使用明文传输执行。为了做到这一点,我们需要确保完成以下步骤:

  1. 首先,生成 SSL 证书,然后将客户端的公钥存储在服务器的 keystore 对象中,并将服务器的公钥存储在客户端的 truststore 中;我们已经为此准备了 HTTPS 连接器。

  2. 接下来,我们需要创建一个支持 SSL 的安全域,该域将被 remoting 传输使用。我们可以使用为 HTTPS 通信创建的那个。

  3. 最后,我们需要对我们的 EJB 应用程序进行一些更改,以便它实际上使用 SSL 安全通道。我们将在下一小节中介绍这一点。

连接到支持 SSL 的安全域

正如你在第三章中看到的,介绍 Java EE 7 – EJBs,RMI-IIOP 连接属性在 jboss-ejb-client.properties 文件中指定,需要稍作调整以启用 SSL 连接:

remote.connections=node1 
remote.connection.node1.host=localhost
remote.connection.node1.port = 4447
remote.connection.node1.username=adminUser
remote.connection.node1.password=admin123
remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=true
remote.connection.node1.connect.options.org.xnio.Options.SSL_STARTTLS=true
remote.connection.node1.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=true

SSL_ENABLED 选项设置为 true 时,启用 remoting 连接器的 SSL 通信。

STARTTLS 选项指定在启动时或需要时是否使用 Tunneled Transport Layer SecurityTTLS)模式。

SASL_POLICY_NOANONYMOUS 选项指定是否允许 Simple Authentication and Security LayerSASL)机制,这些机制接受匿名登录。

最后,由于我们的安全域还包括一个身份验证安全域,我们可以选择通过指定一个 @RolesAllowed 注解来限制对某些方法的访问,该注解需要 ejbRole 角色:

@RolesAllowed("ejbRole")
public String bookSeat(int seatId)  throws SeatBookedException {
 . . . .
}

为了在您的 EJB 上激活安全域,我们需要在 jboss-ejb3.xml 文件的组装描述符中提及它:

<jboss:ejb-jar>
  <assembly-descriptor>
    <s:security>
      <ejb-name>*</ejb-name>
 <s:security-domain>ejb-security-domain</s:security-domain>
    </s:security>
  </assembly-descriptor>
</jboss:ejb-jar>

现在,按照 第三章中包含的说明重新部署 Ticket EJB 示例应用程序,并执行客户端。

如果连接成功,那么你已经配置了一个完全工作且安全的远程连接。

摘要

我们在本章开始时讨论了安全的基本概念以及身份验证和授权之间的区别。

WildFly 使用位于 Java Authentication and Authorization ServiceJAAS)之上的 PicketBox 框架,该框架保护应用程序中运行的所有 Java EE 技术。安全子系统的核心部分包含在执行所有必需的 授权身份验证检查的 security-domain 元素中。

然后,我们更仔细地研究了登录模块,这些模块用于存储用户凭据及其相关角色。特别是,你学习了如何应用基于文件的UserRoles登录模块和Database登录模块。每个登录模块都可以由企业应用程序以编程方式或声明方式使用。虽然编程安全可以提供细粒度的安全模型,但你应考虑使用声明式安全,这允许业务层和安全策略之间有一个清晰的分离。

最后,在本章的最后部分,我们介绍了如何使用安全套接字层和由keytool Java 实用程序生成的证书来加密通信通道。

在下一章中,我们将讨论集群,这是关键应用程序部署的环境。

第十一章。WildFly 应用程序集群

在前面的章节中,我们探讨了开发 Java 企业应用程序最有趣的方面。一旦您准备好推出您的应用程序,确保您的客户获得响应迅速且容错的环境是非常重要的。这一需求可以通过应用服务器集群来实现。

WildFly 集群不是单一库或规范的产物,而是一系列技术的融合。在本章中,我们将首先介绍一些集群编程的基本知识。然后,我们将快速进入集群配置及其设置,这是部署一些集群应用程序所必需的。

以下列表是本章将要涵盖的主题预览:

  • 集群是什么以及 WildFly 如何实现它

  • 在独立和域模式下设置集群

  • 开发集群 Java EE 7 应用以实现负载均衡和高可用性

集群基础知识

应用服务器集群由多个同时运行并协同工作以提供增强的可扩展性和可靠性的服务器实例(集群节点)组成。构成集群的节点可以位于同一台机器或不同的机器上。从客户端的角度来看,这并不重要,因为集群看起来就像一个单独的服务器实例。

在您的应用程序中引入集群将产生以下好处:

  • 水平扩展(向外扩展):向集群中添加一个新节点应该允许整个系统服务比简单基本配置提供的更高的客户端负载。理想情况下,只需通过添加适当数量的服务器或机器就可以服务任何给定的负载。

  • 负载均衡:在集群环境中,构成集群的各个节点应该各自处理整体客户端负载的公平份额。这可以通过在多个服务器之间分配客户端请求来实现,这也被称为负载均衡。

  • 高可用性:当服务器实例失败时,运行在集群中的应用程序可以继续运行。这是通过将应用程序部署在集群的多个节点上实现的,因此如果服务器实例失败,另一个部署了该组件的服务器实例可以继续进行应用程序处理。

WildFly 集群

WildFly 中自带集群功能。没有处理集群的单一库,而是一组覆盖不同方面的库。

以下图显示了 WildFly 采纳的基本集群架构:

WildFly 集群

JBoss 集群的骨干是 JGroups 库,它通过多播传输在集群成员之间提供通信。

注意

多播是一种协议,数据同时传输到已加入适当多播组的多个主机。你可以将多播想象成一种广播或电视流,只有调谐到特定频率的接收者才能接收到流。

下一个构建块是Infinispan,它通过一个复制和事务性的 JSR-107 兼容缓存来处理应用程序在集群中的一致性。

注意

JSR-107指定了 Java 对象临时内存缓存的 API 和语义,包括对象创建、共享访问、排队、失效和跨 JVM 的一致性。

在深入探讨一些集群示例之前,我们首先需要描述如何使用两个可用的节点设置一个 WildFly 节点的集群:独立集群域集群。如果你不记得独立模式之间的区别,或者核心元素是什么,你可以复习第二章的内容,在 WildFly 上创建你的第一个 Java EE 应用程序

启动独立节点集群

独立服务器以单个 JVM 进程启动;因此,我们需要使用standalone.bat/standalone.sh命令启动每个服务器,传递所有必需的参数。在以下示例中,我们正在启动两个不同机器上的两个服务器节点集群,分别绑定到 IP 地址192.168.1.10192.168.1.11

./standalone.sh -c standalone-ha.xml -b 192.168.1.10
./standalone.sh -c standalone-ha.xml -b 192.168.1.11

-c参数指定要使用的服务器配置;默认情况下,应用程序服务器包括两个独立的集群配置:standalone-ha.xmlstandalone-full-ha.xml。后者还包括 Java EE 完整配置的消息子系统和其他元素;因此,它被称为完整配置。

另一个参数(-b)应该对老版本的 JBoss 用户来说很熟悉,因为它仍然用于指定服务器绑定地址,该地址需要是唯一的,以避免端口冲突。

在这个其他示例中,我们正在同一台机器上启动另一个由两个节点组成的集群,使用一些额外的参数以避免端口冲突:

./standalone.sh -c standalone-ha.xml -Djboss.node.name=node1 
./standalone.sh -c standalone-ha.xml -Djboss.node.name=node2 -Djboss.socket.binding.port-offset=200

如你所见,我们不得不指定两个额外的参数:jboss.node.name,以便为每个节点分配一个唯一的服务器名称,以及一个套接字绑定端口,它使用200的偏移量。例如,第二个节点将响应 HTTP 通道上的端口8280而不是端口8080

注意

如果你在服务器控制台上没有看到任何关于集群的消息,请不要感到惊讶。集群模块是按需激活的,所以首先你需要部署一个集群感知的应用程序。在几分钟内,我们将向你展示如何做。

启动域节点集群

为了配置在服务器节点域上运行的集群,您需要配置域控制器的domain.xml主文件。然后,对于集群中的每个 WildFly 主机,您需要提供一个host.xml配置文件,该文件描述了单个服务器分布的配置。

域控制器配置

domain.xml文件位于JBOSS_HOME/domain/configuration/。它包括主域配置,该配置由所有服务器实例共享。在domain.xml文件中,我们将定义服务器组配置,指定一个与集群兼容的配置文件。默认情况下,WildFly 域附带四个不同的配置文件:

  • default:此配置文件支持 Java EE Web Profile 和一些扩展,例如 RESTful Web 服务,或支持企业 JavaBeansEJB)3 远程调用

  • full:此配置文件支持默认配置文件中包含的所有默认子系统以及消息子系统

  • ha:此配置文件对应于扩展了集群功能的default配置文件

  • full-ha:这是具有集群功能的full配置文件

因此,首先在您的domain.xml文件中为您的服务器组指定一个集群感知配置文件。在我们的示例中,我们为服务器组采用了full-ha配置文件,这样您就可以在所有域服务器上运行完整的 Java EE 堆栈:

<server-groups>
 <server-group name="main-server-group" profile="full-ha">
<jvm name="default">
<heap size="64m" max-size="512m"/>
</jvm>
<socket-binding-group ref="full-ha-sockets"/>
</server-group>
<server-group name="other-server-group" profile="full-ha">
<jvm name="default">
<heap size="64m" max-size="512m"/>
</jvm>
<socket-binding-group ref="full-sockets"/>
</server-group>
</server-groups>

当使用full-ha配置文件时,您需要配置 HornetQ 集群安全性。您可以简单地禁用它,或者您还需要为 JMS 集群设置一个完全随机的用户凭据。在domain.xml中找到配置设置,并将以下代码添加到消息子系统:

<subsystem >
    <hornetq-server>
 <cluster-user>randomUser</cluster-user>
 <cluster-password>randomPassword</cluster-password>
           . . . 
    </hornetq-server>
</subsystem>

除了domain.xml文件外,您还需要检查您的域控制器的host.xml文件是否包含对本地主机的引用,如下面的代码片段所示:

<host name="master" >
    ...
    <domain-controller>
 <local/>
    </domain-controller>
    ...
</host>

local部分表示该主机控制器将扮演域控制器的角色。对于所有其他主机控制器,您必须指定远程域控制器的主机和端口(在本例中,我们添加了一些变量作为占位符)。我们将在下一节中介绍它们。

最后,您需要创建一个管理用户,该用户将用于在从节点和域控制器之间建立连接。为此,启动位于您分发JBOSS_HOME/bin目录中的add-user.sh/add-user.cmd脚本:

What type of user do you wish to add?
 a) Management User (mgmt-users.properties)
 b) Application User (application-users.properties)
(a): a

Enter the details of the new user to add.
Using realm 'ManagementRealm' as discovered from the existing property files.
Username : admin1234
Password recommendations are listed below. To modify these restrictions edit the add-user.properties configuration file.
 - The password should not be one of the following restricted values {root, admin, administrator}
 - The password should contain at least 8 characters, 1 alphabetic character(s), 1 digit(s), 1 non-alphanumeric symbol(s)
 - The password should be different from the username
Password :
Re-enter Password :
What groups do you want this user to belong to? (Please enter a comma separated list, or leave blank for none)[  ]:
About to add user 'admin1234' for realm 'ManagementRealm'
Is this correct yes/no? yes
Added user 'admin1234' to file 'D:\Dev\Servers\wildfly-8.1.0.Final\standalone\configuration\mgmt-users.properties'
Added user 'admin1234' to file 'D:\Dev\Servers\wildfly-8.1.0.Final\domain\configuration\mgmt-users.properties'
Added user 'admin1234' with groups  to file 'D:\Dev\Servers\wildfly-8.1.0.Final\standalone\configuration\mgmt-groups.properties'
Added user 'admin1234' with groups  to file 'D:\Dev\Servers\wildfly-8.1.0.Final\domain\configuration\mgmt-groups.properties'
Is this new user going to be used for one AS process to connect to another AS process?
e.g. for a slave host controller connecting to the master or for a Remoting connection for server to server EJB calls.
yes/no? yes
To represent the user add the following to the server-identities definition <secret value="c2xvZHppYWsxMjM0" />
Press any key to continue . . .

如前所述,您必须通过指定用户名和密码来创建一个管理用户。您应该用yesy回答前面的问题,以表示该用户将用于从主机控制器连接到域控制器。生成的密钥值是新创建用户的 Base64 编码密码。

现在,我们可以通过指定用于公共和管理接口的地址(在我们的示例中为192.168.1.10)来启动域控制器,以下命令:

domain.sh –host-config=host-master.xml -b 192.168.1.10 -Djboss.bind.address.management=192.168.1.10

我们已将物理网络的绑定地址设置为具有jboss.bind.address.management属性的宿主配置。管理接口必须对所有域中的主机可访问,以便与域控制器建立连接。

主机配置

在配置并启动域控制器之后,下一步是设置将连接到域控制器的其他主机。在每个主机上,我们还需要安装 WildFly,我们将配置host.xml文件。(作为替代,您可以根据需要命名主机文件,并通过-host-config参数启动域,例如,./domain.sh -host-config=host-slave.xml。)

首先是为我们域中的每个主机选择一个唯一名称,以避免名称冲突。否则,默认为服务器的主机名。

<host name="server1" >
    ...
</host>

此外,您还必须为另一个主机选择一个唯一名称:

<host name="server2" >
    ...
</host>

接下来,我们需要指定主机控制器将连接到远程域控制器。我们不会指定域控制器的实际 IP 地址,而是将其留为名为jboss.domain.master.address的属性。

此外,我们还需要指定用于连接域控制器的用户名。因此,让我们添加在域控制器机器上创建的用户admin1234

<domain-controller>
       <remote host="${jboss.domain.master.address}"      port="${jboss.domain.master.port:9999}"
 username="admin1234" 
       security-realm="ManagementRealm"/>
</domain-controller>

最后,我们需要指定包含在remote元素中的服务器身份的 Base64 密码:

<management>
   <security-realms>
      <security-realm name="ManagementRealm">
         <server-identities>
            <secret value="QWxlc3NhbmRybzIh" />
         </server-identities>
         <authentication>
            <properties path="mgmt-users.properties" relative-to="jboss.domain.config.dir" />
         </authentication>
      </security-realm>
      <security-realm name="ApplicationRealm">
         <authentication>
            <properties path="application-users.properties" relative-to="jboss.domain.config.dir" />
         </authentication>
      </security-realm>
   </security-realms>
   <management-interfaces>
      <native-interface security-realm="ManagementRealm">
         <socket interface="management" port="${jboss.management.native.port:9999}" />
      </native-interface>
   </management-interfaces>
</management>

最后一步是在两个主机上的host.xml文件中配置服务器节点。因此,在第一个主机上,我们将配置server-oneserver-two以将它们添加到main-server-group

<servers>
        <server name="server-one" group="main-server-group"/>
        <server name="server-two" group="main-server-group" auto-start="false"> 
            <socket-bindings port-offset="150"/>
        </server>
</servers>

在第二个主机上,我们将配置server-threeserver-four以将它们添加到other-server-group

<servers>
     <server name="server-three" group="other-server-group"/>
     <server name="server-four" group="other-server-group"> auto-start="false">
            <socket-bindings port-offset="150"/>
     </server>
</servers>

请注意,auto-start标志的值表示如果主机控制器启动,服务器实例将不会自动启动。

对于server-twoserver-four,已配置port-offset值为150以避免端口冲突。好吧,现在我们已经完成了我们的配置。假设第一个主机的 IP 地址为192.168.1.10,我们可以使用以下代码片段启动第一个主机:

domain.sh \
-host-conifg=host.xml
-b 192.168.1.10  \
-Djboss.domain.master.address=192.168.1.1 \
-Djboss.bind.address.management=192.168.1.10

第二个主机(192.168.1.11)可以使用以下代码片段启动:

domain.sh \
-host-conifg=host.xml
-b 192.168.1.11 \
-Djboss.domain.master.address=192.168.1.1 \
-Djboss.bind.address.management=192.168.1.11 

部署集群应用程序

如果您尝试启动您的独立或域集的集群节点,您可能会惊讶地发现您的服务器日志中完全没有关于集群的信息。相信我,这不是一个错误,而是一个特性!WildFly 的一个关键特性是只启动最小的一组服务;因此,为了看到集群的实时演示,您需要部署一个集群感知的应用程序。为了在您的应用程序中触发集群库,您可以遵循两种方法:

  • 如果您的应用程序使用企业 JavaBeans,您不需要做任何事情。这个区域带来了 WildFly 的一些重要变化。现在,默认情况下,所有状态会话 Bean 的数据都在 HA 配置文件中进行复制,所有无状态 Bean 都是集群化的。如果您的应用程序部署在以standalone-ha.xml配置启动的容器上,所有远程无状态会话 BeanSLSB)默认支持故障转移功能。

  • 如果您的应用程序包含一个 Web 应用程序存档,您可以在web.xml文件中使用可移植的<distributable />元素。

让我们来看看两种方法,从集群 EJB 开始。

创建高可用性(HA)状态会话 Bean

集群状态会话 BeanSFSB)具有内置的故障转移能力。这意味着@Stateful EJB 的状态在集群节点之间进行复制,以便如果集群中的某个节点发生故障,其他节点将能够接管指向它的调用。可以通过使用@Stateful(passivationCapable=false)注解来为特定 Bean 禁用此功能。

以下图展示了 EJB 客户端应用程序和远程 EJB 组件之间典型的信息交换:

创建高可用性状态会话 Bean

如您所见,通过Java 命名和目录接口JNDI)成功查找 SFSB 后,会返回一个代理给客户端用于后续的方法调用。

注意

由于 EJB 是集群化的,它将返回一个会话 ID,以及与它一起的会话的亲和力,即状态会话 Bean 在服务器端所属的集群名称。这种亲和力将有助于 EJB 客户端将代理上的调用适当地路由到集群中的特定节点。

在此会话创建请求进行的同时,NodeA也会发送一个包含集群拓扑结构的异步消息。JBoss EJB 客户端实现将注意这个拓扑信息,并在以后使用它来创建与集群内节点的连接,并在必要时将调用路由到这些节点。

现在让我们假设NodeA发生故障,客户端应用程序随后在代理上调用。在这个阶段,JBoss EJB 客户端实现将了解集群拓扑;因此,它知道集群有两个节点:NodeANodeB。现在当调用到达时,它检测到NodeA已关闭,因此它使用选择器从集群节点中获取一个合适的节点。此交换在以下图中显示:

创建高可用性状态会话 Bean

如果找到合适的节点,JBoss EJB 客户端实现将创建到该节点的连接(在我们的案例中是NodeB),并从中创建一个 EJB 接收器。在此过程结束时,调用现在已被有效地故障转移到集群内的另一个节点。

集群票务示例

在第三章,介绍 Java EE 7 – EJBs中,我们讨论了我们的票务系统示例,该示例围绕以下内容构建:

  • 用于存储会话数据的具有状态的 EJB

  • 用于存储数据缓存的单例 EJB

  • 用于执行一些业务方法的无状态 EJB

让我们看看如何应用必要的更改以在集群上下文中启动我们的应用程序。

无状态和有状态的 Bean 已准备好进行集群化——不需要额外的代码;然而,存在一个陷阱。实际上,用于存储座位缓存的单例 EJB 将在集群中的每个 JVM 中实例化一次。这意味着如果服务器发生故障,缓存中的数据将会丢失,并且将使用新的数据(不一致的数据)。

在集群环境中设置缓存有几种替代方案:

  • 使用 JBoss 专有解决方案,该解决方案部署了SingletonService的集群版本,该版本公开了org.jboss.msc.service.Service的 HA 单例(此方法的示例包含在 WildFly 快速入门演示中,github.com/wildfly/quickstart/tree/master/cluster-ha-singleton

  • 将您的缓存移动到持久存储,这意味着使用 JPA 从缓存中存储和读取数据(参见第五章
    public class TheatreBox {

    private static final Logger logger =
    Logger.getLogger(TheatreBox.class);
    private Map<Integer, Seat> seats;

@Resource(lookup = "java:jboss/infinispan/tickets")
private EmbeddedCacheManager container;

@PostConstruct
public void setupTheatre() {
    try {
        this.cache = container.getCache();
        logger.info("Got Infinispan cache");

        int id = 0;
        for (int i = 0; i < 5; i++) {
            addSeat(new Seat(++id, "Stalls", 40));
            addSeat(new Seat(++id, "Circle", 20));
            addSeat(new Seat(++id, "Balcony", 10));
        }
        logger.info("Seat Map constructed.");
    } catch (Exception e) {
        logger.info("Error! " + e.getMessage());
    }
}

private void addSeat(Seat seat) {
    seats.put(seat.getId(), seat);
}

@Lock(READ)
public Collection<Seat> getSeats() {
    return Collections.unmodifiableCollection(seats.values());
}

@Lock(READ)
public int getSeatPrice(int seatId) throws NoSuchSeatException {
    return getSeat(seatId).getPrice();
}

@Lock(WRITE)
public void buyTicket(int seatId) throws SeatBookedException, NoSuchSeatException {
    final Seat seat = getSeat(seatId);
    if (seat.isBooked()) {
        throw new SeatBookedException("Seat " + seatId + " already booked!");
    }
    addSeat(seat.getBookedSeat());
}

@Lock(READ)
private Seat getSeat(int seatId) throws NoSuchSeatException {
    final Seat seat = cache.get(seatId);
    if (seat == null) {
        throw new NoSuchSeatException("Seat " + seatId + " does not exist!");
    }
    return seat;
}

}


我们首先想强调的是`@Resource`注解,它注入一个`EmbeddedCacheManager`实例。当 WildFly 部署器遇到这个注解时,您的应用程序将包含对请求的缓存容器的依赖。因此,缓存容器将在部署期间自动启动,并在您的应用程序卸载期间停止(包括所有缓存)。

随后,当 EJB 被实例化时(请参阅标记为`@PostConstruct`的`start`方法),使用`EmbeddedCacheManager`作为工厂创建`org.infinispan.Cache`。这个缓存将用于存储我们高度可用的数据集。

对分布式缓存执行的操作相当直观:`put`方法用于将`Seat`对象的实例存储在缓存中,相应的`get`方法用于从中检索元素,这正是您使用普通 hashmap 所做的事情。唯一的区别是,在我们的集群缓存中,每个条目都必须是可序列化的。请确保将`Seat`标记为`Serializable`并为它创建一个默认构造函数。

在应用部署方面,您需要明确声明对 Infinispan API 的依赖,因为在 WildFly 的类加载策略中,它并未作为隐式依赖项包含。这可以通过将以下行添加到您的应用程序的`META-INF/MANIFEST.MF`文件中来实现:

```java
Dependencies: org.infinispan export 

我们还需要将新的缓存容器添加到我们的domain.xml文件中适当的配置文件(在 Infinispan 子系统内):

<cache-container name="tickets" default-cache="default" jndi-name="java:jboss/infinispan/tickets" module="deployment.ticket-agency-cluster.jar">
<transport lock-timeout="60000"/>
<replicated-cache name="default" batching="true" mode="SYNC">
<locking isolation="REPEATABLE_READ"/>
</replicated-cache>
</cache-container>

注意

在我们的示例中,我们使用seats.values()调用从我们的分布式映射(实际上是org.infinispan.Cache的一个实例)中获取所有元素。这种操作在分布式缓存(非复制)中通常是不推荐的,并且有其自身的限制。有关此方法的更多信息,请查看docs.jboss.org/infinispan/6.0/apidocs/org/infinispan/Cache.html#values()的 Javadoc。然而,对于 Infinispan 的最新版本来说,情况已经不再是这样了:infinispan.org/infinispan-7.0/

编写集群感知的远程客户端

远程 EJB 客户端不需要进行任何特定的更改,以便能够实现高可用性。

我们只需要准备一个jboss-ejb-client.properties文件,该文件将包含我们的客户端应用程序最初将通过远程通信联系到的服务器列表:

remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false
remote.connections=node1,node2
remote.connection.node1.host=localhost
remote.connection.node1.port = 8080
remote.connection.node1.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
remote.connection.node2.host=localhost
remote.connection.node2.port = 8280
remote.connection.node2.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false

如您从本文件中可以看到,我们假设您正在使用localhost地址运行一个双节点集群:第一个节点使用默认端口设置,第二个节点使用偏移量200(正如在“启动独立节点集群”部分的第二段中所示)。

如果您的服务器节点和客户端在不同的机器上运行,请将remote.connection.nodeX.host变量的值替换为实际的 IP 或主机。

部署和测试高可用性

将应用程序部署到集群可以通过几种方式实现;如果您更喜欢自动化而不是手动将每个存档复制到 deployments 文件夹,您可以重用前一章中包含的 CLI 部署脚本。

或者,如果您正在使用 WildFly Maven 插件进行部署,您可以参数化其配置,包括主机名和端口号作为变量,这些变量将被传递到命令行:

<plugin>
      <groupId>org.wildfly.plugins</groupId>
      <artifactId>wildfly-maven-plugin</artifactId>
      <version>1.0.2.Final</version>
      <configuration>
          <filename>${project.build.finalName}.jar</filename>
 <hostname>${hostname}</hostname>
 <port>${port}</port> 
      </configuration>
</plugin>

因此,您将使用以下 shell 编译包并在第一个节点上部署应用程序:

mvn install wildfly:deploy –Dhostname=localhost –Dport=9999

对于第二个节点,您将使用以下内容:

mvn install wildfly:deploy –Dhostname=localhost –Dport=10194

注意

在域节点上部署应用程序与前面示例中提到的相同,除了您需要将 domain 标签添加到您的配置中,并且需要指定至少一个服务器组。有关更多信息,请访问 docs.jboss.org/wildfly/plugins/maven/latest/examples/deployment-example.html

一旦您在服务器节点上部署了这两个应用程序,您应该能够在服务器控制台日志中看到集群视图,并且可以看到 Infinispan 缓存已启动并已发现集群中的其他节点。您应该在其中一个节点上看到以下类似内容:

部署和测试高可用性

在您启动应用程序之前,更新 Maven 的 exec 插件信息,现在它应该引用以下代码片段中突出显示部分所示我们的远程 EJB 客户端应用程序。

<plugin>
   <groupId>org.codehaus.mojo</groupId>
   <artifactId>exec-maven-plugin</artifactId>
   <version>${version.exec.plugin}</version>
   <executions>
     <execution>
         <goals>
            <goal>exec</goal>
         </goals>
     </execution>
   </executions>
   <configuration>
     <executable>java</executable>
     <workingDirectory>${project.build.directory}/exec-working-directory</workingDirectory>
     <arguments>
       <argument>-classpath</argument>
       <classpath>
       </classpath>
 <argument>com.packtpub.wflydevelopment.chapter11.client.TicketAgencyClient</argument>
     </arguments>
   </configuration>
</plugin>

您可以使用以下命令运行它:

mvn exec:exec

客户端的第一部分将显示我们已经成功完成第一笔交易的证据。在客户端控制台,您将看到预订交易的返回值和 Seat 列表,如下面的屏幕截图所示:

部署和测试高可用性

以下屏幕截图显示了我们的 EJB 客户端到达的服务器节点:

部署和测试高可用性

现在关闭前面的服务器节点(如果您以前台进程启动,则按 Ctrl + C 即可)并在客户端应用程序上按 Enter(或在 Mac 上按 Return)。

如您从以下屏幕截图中所见,您应该看到会话在幸存节点上继续运行并正确显示会话值(剩余金额)。您的客户端窗口也应显示更新的缓存信息。

部署和测试高可用性

Web 应用程序集群

Web 应用集群涉及两个方面:设置 HTTP 负载均衡器和告诉 WildFly 使应用程序的用户会话具有高可用性。如何做前者取决于你选择的负载均衡器(我们建议选择mod_cluster——它是预配置的,并且与 WildFly 无缝集成);后者非常简单——只需将<distributable/>标签添加到你的应用程序的web.xml文件中。每当一个节点失败时,用户的 HTTP 会话将由另一个节点处理。如果一切顺利,最终用户将不知道发生了故障——所有的事情都会在幕后处理。

让我们具体看看如何执行这两个步骤。

负载均衡您的 Web 应用

你有几种选择可以实现 HTTP 请求的负载均衡。你可以选择一个硬件负载均衡器,它位于你的服务器集群之前,或者你可以从许多可用的 WildFly 软件解决方案中选择,包括以下内容:

  • 使用 Apache Tomcat 的mod_jk模块将你的请求路由到你的节点

  • 使用 Apache mod_proxy配置 Apache 作为代理服务器并将请求转发到 WildFly 节点

  • 使用 WildFly 的内置解决方案mod_cluster来实现请求的动态负载均衡

在这里,我们将说明如何开始使用mod_cluster——Apache HTTP 服务器的模块。使用mod_cluster相对于其他选项的优势可以总结如下要点:

  • 动态集群配置

  • 服务器端可插拔负载度量

  • 应用程序状态的生命周期通知

实际上,当使用标准负载均衡器如mod_jk时,你必须提供一个静态节点列表,该列表用于分配负载。这是一个非常限制性的因素,特别是如果你需要通过添加或删除节点来升级你的配置;或者,你可能只需要升级单个节点使用的软件。除此之外,使用平面集群配置可能会很繁琐,并且容易出错,尤其是当集群节点数量很多时。

当使用mod_cluster时,你可以动态地向你的集群中添加或删除节点,因为集群节点是通过广告机制发现的。

实际上,HTTP 侧的mod_cluster库在多播组上发送 UDP 消息,该组由 WildFly 节点订阅。这允许当发送应用程序生命周期通知时,WildFly 节点自动发现 HTTP 代理。

下一个图表更好地说明了这个概念:

负载均衡您的 Web 应用

安装 mod_cluster

mod_cluster模块作为 WildFly 的核心模块实现,它是分发的一部分。在 HTTP 方面,它作为一组安装在 Apache Web 服务器上的库提供。

在 WildFly 方面,您可以在集群配置文件中找到已捆绑的mod_cluster模块的子系统。您可以在standalone-ha.xml文件或standalone-full-ha.xml(当然在domain.xml文件)配置文件中找到它:

<subsystem >
  <mod-cluster-config advertise-socket="modcluster" connector="ajp">
   <dynamic-load-provider>
       <load-metric type="cpu"/>
   </dynamic-load-provider>
  </mod-cluster-config>
</subsystem>

子系统仅包含一个裸骨配置,通过advertise-socket元素引用其套接字绑定:

<socket-binding name="modcluster" port="0" multicast-address="224.0.1.105" multicast-port="23364"/>

在 Apache 网络服务器方面,我们必须安装用于与mod_cluster交互的核心库。这是一个非常简单的步骤;只需将浏览器指向最新的mod_cluster发布版,在www.jboss.org/mod_cluster/downloads。请确保选择适合您的操作系统和架构(x86 或 x64)的版本。

一旦下载了二进制文件,将存档提取到一个文件夹中;然后,导航到提取的文件夹。mod_cluster的二进制文件基本上是一个捆绑了所有必需库的 Apache 网络服务器。为了预配置您的安装,请确保运行\httpd-2.2\bin\installconf.bat文件。

注意

您可以使用自己的 Apache web 服务器 2.2 安装;只需从mod_cluster捆绑中提取模块并将它们复制到您的 Apache 网络服务器的modules文件夹中。

如果您选择使用自己的 Apache 网络服务器而不是捆绑的版本,您必须将以下库加载到您的httpd.conf文件中(与捆绑的 Apache HTTP 使用相同的集合):

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_cluster_module modules/mod_proxy_cluster.so
LoadModule manager_module modules/mod_manager.so
LoadModule slotmem_module modules/mod_slotmem.so
LoadModule advertise_module modules/mod_advertise.so

这些模块中的每一个都涵盖了负载均衡的重要方面,如下所示:

  • mod_proxymod_proxy_httpmod_proxy_ajp:这些是核心模块,它们使用 HTTP/HTTPS 或 AJP 协议将请求转发到集群节点

  • mod_manager:此模块从 AS 7 读取信息,并与mod_slotmem一起更新共享内存信息

  • mod_proxy_cluster:此模块包含mod_proxy的均衡器

  • mod_advertise:这是一个附加模块,允许 HTTP 通过多播数据包(IP 和端口)进行广告,其中mod_cluster模块正在监听

我们需要添加的配置的下一部分是核心负载均衡配置:

Listen 192.168.10.1:8888

<VirtualHost 192.168.10.1:8888>
<Location />
    Order deny,allow
    Deny from all
    Allow from 192.168.10.
</Location>
  KeepAliveTimeout 60
  MaxKeepAliveRequests 0
  ManagerBalancerName mycluster
  ServerAdvertise On
</VirtualHost>

基本上,您必须将192.168.10.1 IP 地址替换为您 Apache 网络服务器监听请求的地址,将8888端口号替换为您想要与 WildFly 通信的端口号。

就目前而言,Apache 虚拟主机允许您从子网络192.168.10接收传入请求。

KeepAliveTimeout指令允许您在 60 秒内重用相同的连接。由于我们将MaxKeepAliveRequests设置为0,因此每个连接的请求数量是无限制的。ManagerBalancerName指令提供了您集群的均衡器名称(默认为mycluster)。

对我们来说最重要的是设置为 OnServerAdvertise 指令,它使用广告机制来告诉 WildFly 它应该将集群信息发送给谁。

默认情况下,此选项在捆绑的服务器中已禁用。请确保在 httpd.conf 文件中取消注释 ServerAdvertise 指令。

现在,重新启动 Apache Web 服务器和单个应用程序服务器节点。如果您已正确配置 HTTP 侧的模式集群,您将看到每个 WildFly 节点将开始从 mod_cluster 接收 UDP 多播消息。

注意

如果您正在 Windows 机器上运行,请确保以管理员身份运行您的 Web 服务器。

如果一切顺利,您可以通过访问 http://127.0.0.1:6666/mod_cluster_manager 来查看您的负载均衡器和互联节点的情况。请确保不要使用 Google Chrome,因为它将 6666 端口视为不安全(默认情况下,它是一个 IRC 端口)。您应该在简单的网页上看到以下信息:

mod_cluster/1.2.6.Final
Auto Refresh show DUMP output show INFO output 
Node michal-pc (ajp://localhost:8009): 
Enable Contexts Disable ContextsBalancer: mycluster,LBGroup: ,Flushpackets: Off,Flushwait: 10000,Ping: 10000000,Smax: 65,Ttl: 60000000,Status: OK,Elected: 0,Read: 0,Transferred: 0,Connected: 0,Load: 100

如果您现在没有运行的 WildFly 实例,请确保使用其中一个全高可用性配置文件启动它。在服务器启动后,刷新 Apache 的配置网页。

集群化您的 Web 应用程序

集群化 Web 应用程序需要开发者投入最少的努力。正如我们刚才讨论的,要在 Web 应用程序中启用集群,您只需将以下指令添加到 web.xml 描述符中:

<web-app>
 <distributable/>
</web-app>

一旦您的应用程序包含可分发的配置段落,集群将启动,并且只要您正确设计了会话层,它也将是负载均衡和容错的。

您可以通过将浏览器指向您的 HTTP 代理来检查它。对于默认设置,它将是 http://localhost:6666/your_web_application/

实现高可用性的编程注意事项

为了支持 HTTP 会话状态的内存复制,所有 servlet 和 JSP 会话数据都必须是可序列化的。

注意

序列化是将对象转换为一系列字节的过程,以便对象可以轻松地保存到持久存储或通过通信链路传输。然后,该字节流可以被反序列化,将流转换为原始对象的副本。

此外,在实现 javax.servlet.http.HttpSession 的 HTTP servlet 中,您需要使用 setAttribute 方法来更改会话对象中的属性。如果您使用 setAttribute 在会话对象中设置属性,则默认情况下,对象及其属性将使用 Infinispan API 进行复制。每次对会话中的对象进行更改时,都应该调用 setAttribute 来更新集群中的该对象。

同样,您需要使用 removeAttribute 来从会话对象中删除属性。

在 JSF 应用程序中实现高可用性(HA)

在本书包含的应用程序中,我们使用了 JSF 和 CDI API 来管理 Web 会话。在这种情况下,我们透明地将其他服务器节点复制到标记为@SessionScoped的 Bean。

注意

如果您正在处理由 SFSB 创建的 HTTP 和 EJB 会话,则对基于 JSF 的应用程序进行集群需要特别注意。在早期的以 servlet 为中心的框架中,通常的方法是将状态会话 Bean 的引用存储在javax.servlet.http.HttpSession中。当处理高级 JSF 和 CDI Bean 时,向您的应用程序提供一个@SessionScoped控制器至关重要,该控制器被注入到 SFSB 引用中;否则,您将在每次请求时创建一个新的状态会话 Bean。

以下是如何将您的 Ticket CDI 应用程序(在第四章中描述,学习上下文和依赖注入)适配到集群环境的一个示例。起初,正如我们所说的,我们需要在您的web.xml文件中包含可分发段以触发集群模块:

<web-app>
    <distributable/>
</web-app>

接下来,将我们在将您的缓存转换为分布式缓存部分中描述的相同更改应用到TheatreBox单例:

@Singleton
@Startup
public class TheatreBox {

 @Resource(lookup="java:jboss/infinispan/container/cluster")
 private CacheContainer container;

    // Apply the same changes described in
    // "Turning your Cache into a distributed cache section
}

由于我们的控制器组件绑定到@SessionScoped状态,您不需要进行任何更改即可在服务器节点之间传播您的会话:

@Named
@SessionScoped
public class TheatreBooker implements Serializable {
}

最后,请记住在您的META-INF/MANIFEST.MF中包含 Infinispan 依赖项:

Dependencies: org.infinispan export

一旦您的应用程序部署在集群的节点上,您可以通过访问 Apache web 服务器(在我们的示例中为http://localhost:6666/ticket-agency-cluster)来测试它并开始预订票务:

在 JSF 应用程序中实现高可用性

由于mod_cluster子系统默认配置为使用粘性 Web 会话,因此来自同一客户端的所有后续请求都将被重定向到同一服务器节点。因此,通过关闭粘性服务器节点,你可以获得已创建新的集群视图的证据,并且可以在其他服务器节点上继续购物。

在 JSF 应用程序中实现高可用性

摘要

本章全部关于集群应用程序的世界。在这里,我们向您介绍了 WildFly 的强大集群功能,并将其应用于本书中讨论的一些示例。

与集群相关的主题数量可能需要扩展以涵盖一本完整的书籍;然而,我们决定只强调一些功能。特别是,我们学习了如何集群 EJB 并实现服务器拓扑变化时的容错。

接下来,我们讨论了集群 Web 应用程序以及与负载均衡解决方案(如 Apache web 服务器和mod_cluster)的集成。

在下一章中,我们将关注 Java EE 7 中添加的一些与长期任务执行相关的新主题:批处理和并发实用工具的使用。

第十二章。长期任务执行

到目前为止,我们的应用程序专注于与用户的交互。这可能是你未来项目最重要的方面,但有一些场景需要不同的方法。维护任务、导入大量数据或耗时的计算通常以批处理模式而不是交互式方式处理。通常,这类作业不是标准操作的一部分,而应在服务器负载最低或定期时调用。

在 Java EE 7 之前,没有标准化的方式来实现批处理作业(不需要用户交互的操作)。随着 JSR 352 (jcp.org/en/jsr/detail?id=352) 和批处理框架的引入,这一情况发生了变化,该框架使用 XML 语言来定义作业。

当涉及到处理器密集型任务时,自然想到的是并行化。现代 CPU 具有多个核心,这些核心可以很容易地被 JVM 利用。唯一的问题是,在 Java EE 中,使用来自 Java SE 的并发原语是不被鼓励的。程序员可能会损害整个容器的稳定性和可用性。

再次强调,新的 JSR 236 (jcp.org/en/jsr/detail?id=236) 提供了克服这种架构障碍的新方法。新的规范 ManagedExecutorService 是 Java SE 中已知的 ExecutorService 的容器感知版本。

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

  • 如何创建和执行批处理作业

  • 不同批处理作业类型之间的区别是什么

  • 如何在 Java EE 容器内创建我们的自定义工作线程

批处理框架概述

步骤是基本的工作单元,也是我们主要关注的领域。批处理框架定义了两种类型的步骤,如下所示:

  • 分块步骤:这些步骤在三个阶段上对数据块进行处理:读取、处理和写入(对于每个阶段,都创建一个单独的类)。这些块可以配置为在一个事务中应该处理元素的数量。

  • 任务步骤:这些执行程序员创建的特定代码块,没有任何特殊约束。它们用于大多数非数据处理任务。

此外,批处理框架允许监听器为整个作业或特定任务阶段进行注册。

现在我们已经涵盖了基本词汇,最好直接进入编码。

我们的第一个批处理作业

WildFly 提供了一个名为 JBeret 的 JSR 352 实现 (github.com/jberet/jsr352). 这意味着我们可以通过简单地实现所需的接口轻松地将批处理作业扩展到我们的票务应用程序中;不需要额外的依赖项。所有 API 都已经在我们当前的示例中就绪,所以我们只需要创建一些类和一个 XML 文件来指定作业流程。

作为本章开发的基线,最好使用第五章中的代码,将持久性与 CDI 结合。持久化层将允许我们编写一个示例导入批处理作业。为了保持简单,让我们首先定义一个人工的外部服务,它将为我们提供应预订的票证的 ID。我们可以将其作为应用程序的一部分或作为单独的 WAR 文件部署。此示例基于 REST 端点,因此请确保在部署中配置 JAX-RS(有关详细信息,请参阅第七章,将 Web 服务添加到您的应用程序)。以下是一个代码片段:

@Singleton
@Startup
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@Path("/external")
@Produces(MediaType.TEXT_PLAIN)
public class PendingSeats {

    private final Queue<Integer> seats = 
                               new ConcurrentLinkedQueue<>();

    @PostConstruct
    private void setUp() {
        for (int i = 5; i < 10; i++) {
            seats.add(i);
        }
    }

 @GET
    public Integer getNextSeat() {
 return seats.poll();
    }
}
5 to 9, and on every GET request, it will provide the ID as the output. When all IDs are emitted, a null value will be returned. This endpoint will serve as a model of a reservation system. For simplicity, it produces plaintext values instead of JSON. Of course, a flat file or any other source of data could also be used for integration.

创建基于块的批处理步骤

我们的集成场景将非常直接。我们需要从外部系统读取所有预订 ID,以从我们的数据库中获取相应的座位,并将更改写回数据库。记录导入操作日志也会很好。让我们从项目读取器开始:

package com.packtpub.wflydevelopment.chapter12.batching;

import java.io.Serializable;
import javax.batch.api.chunk.AbstractItemReader;
import javax.inject.Named;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;

@Named
public class ExternalSystemReader extends AbstractItemReader {

    private WebTarget target;

    @Override
    public void open(Serializable checkpoint) throws Exception {
        final Client restclient = ClientBuilder.newClient();
        this.target = restclient.target("http://localhost:8080/ticket-agency-longterm/rest/external");
    }

    @Override
    public Object readItem() throws Exception {
        return target.request().get(String.class);
    }
}

我们的读者扩展了AbstractItemReader类,这样我们就不必实现javax.batch.api.chunk.ItemReader接口的所有方法。我们感兴趣的只有两个方法:openreadItem。第一个方法初始化 REST 客户端,将从服务器获取数据。实现是可选的,因为并非每个读取器都需要初始化逻辑。请注意,一个检查点参数被传递给该方法。它可以用来从特定点重新启动批处理作业。然而,我们将省略这个功能。

readItem方法从外部服务请求数据,并将单个项目返回给批处理框架。null 值是一个指示没有更多数据的标志。ItemReader接口的其他方法负责检查点处理和读取器的关闭。

当我们为批处理作业定义 XML 规范时,我们必须使用管理 bean 的名称来引用我们想要的读取器、处理器或写入器(就像在 JSF 中一样)。因此,我们需要@Named注解来提供一个基于字符串的限定符;默认情况下,它将是放置注解的类的名称的小写。对于ExternalSystemReaderbean,我们将使用externalSystemReader名称。

在读取一个项目后,我们可能会处理它。我们的SeatProcessor类如下代码片段:

package com.packtpub.wflydevelopment.chapter12.batching;

import javax.batch.api.chunk.ItemProcessor;
import javax.inject.Inject;
import javax.inject.Named;
import com.packtpub.wflydevelopment.chapter12.control.SeatDao;
import com.packtpub.wflydevelopment.chapter12.entity.Seat;

@Named
public class SeatProcessor implements ItemProcessor {

 @Inject
 private SeatDao dao;

    @Override
    public Object processItem(Object id) throws Exception {
        Seat seat = dao.find(Long.parseLong((String) id));
        if (seat != null) {
            if (seat.getBooked() == true) {
                return null;
            }
            seat.setBooked(true);
        }
        return seat;
    }
}

我们的处理器从读取器检索 ID,并在数据库中找到相应的条目。为了找到实体,我们重用了上一章中已知的SeatDao类。因为我们有 CDI 在批处理框架上工作,所以我们只需注入我们的 EJB,无需关心事务处理。

如果找到座位,我们检查它是否已被预订。如果是,我们可以简单地返回一个 null 值,以从进一步处理中省略此项目。

最后一步是 SeatWriter。以下是一个代码片段:

package com.packtpub.wflydevelopment.chapter12.batching;

import javax.batch.api.chunk.AbstractItemWriter;
import javax.batch.runtime.context.JobContext;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Named
public class SeatWriter extends AbstractItemWriter {

    public static final String FILENAME_PARAM = "logFile";

 @Inject
 private JobContext jobContext;

    @PersistenceContext
    private EntityManager em;

    private BufferedWriter writer;

    @Override
    public void open(Serializable ckpt) throws Exception {
        Properties jobProperties = jobContext.getProperties();
        String fileName = jobProperties.getProperty(FILENAME_PARAM);

        writer = new BufferedWriter(new FileWriter(fileName));
        writer.write("Importing...");
        writer.newLine();
    }

    @Override
 public void writeItems(List<Object> items) throws Exception {
        writer.write("Chunk size: " + items.size());
        writer.newLine();

        for (Object obj : items) {
            em.persist(obj);
            writer.write("Persisted: " + obj);
            writer.newLine();
        }
    }

    @Override
    public void close() throws Exception {
        writer.write("Import finished");
        writer.newLine();
        writer.close();
    }
}

我们的 ItemWriter 类首先定义了一个 open 方法,该方法获取一个用于写入的文件。新创建的日志文件名取自作业属性。我们关于当前批处理作业的信息来源是注入的 JobContext 类(还有一个 StepContext 对象,它提供了有关特定步骤的信息)。它为我们提供了获取作业定义的属性、当前 ID、状态和附加瞬态数据的机会。

我们写入器的核心当然是 writeItem 方法。它接收要写入的项目列表(在我们的例子中是座位),其责任是持久化它们。此方法可以在没有更多数据要写入之前多次调用。您可以配置每个块中要处理元素的数量。更重要的是,每个块都在自己的事务中运行。

最后,当最后一个块写入完成后,close 方法会写入摘要并关闭文件。

所有元素现在都已就绪,因此我们需要创建一个批处理作业规范。externalSystem.xml 文件应放置在您的项目中 src/main/resources/META-INF/batch-jobs 目录下。内容如下:

<job id="externalSystem" 
    version="1.0"> [1]
    <properties>
        <property name="logFile" value="log.txt" /> [2]
    </properties>
    <step id="processData">
        <chunk item-count="2"> [3]
            <reader ref="externalSystemReader" /> [4]
            <processor ref="seatProcessor" />
            <writer ref="seatWriter" />
        </chunk>
    </step>
</job>

结构非常简单。首先,我们定义一个与文件名 [1] 匹配的作业 ID。接下来,在属性部分,我们设置一个名为 logFile 的属性,其值为 log.txt [2],我们在 SeatWriter 中使用它来创建输出文件 [3]。然后,我们定义一个包含数据块的分步操作。item-count 属性定义了我们在一个事务中处理的项目数量。最后,我们在相应的标签 [4] 中引用我们的读取器、处理器和写入器。

现在,当我们的作业定义完成后,是时候启动它了。为此,我们需要使用 BatchRuntime 的静态方法 getJobOperator。为了简化解决方案,我们将使用 REST 端点的 GET 方法来调用我们的代码:

package com.packtpub.wflydevelopment.chapter12.batching;

import java.util.Properties;
import javax.batch.runtime.BatchRuntime;
import javax.ejb.Stateless;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Stateless
@Path("/job")
public class JobStarter {

    @GET
    public String start() {
 long jobId = BatchRuntime.getJobOperator()
 .start("externalSystem", new Properties());
        return Long.toString(jobId);
    }
}

JobOperator start 方法返回一个作业 ID,它是正在进行的批处理过程的表示。我们需要提供定义批处理作业的文件名(不带 XML 扩展名)和一组运行时参数。

注意

在运行时提供的属性与我们之前使用的不同!这类属性不是绑定到特定作业的(与在 XML 文件中定义的属性相反),但可以从作业执行中访问。批处理框架称它们为参数。如果您需要在您的应用程序中实现这类逻辑,您只需在作业启动期间传递它们,并使用作业执行 ID 来访问它们:

JobOperator operator = BatchRuntime.getJobOperator();
Properties properties = new Properties();
properties.put("propertyName", "propertyValue");

long jobId = operator.start("externalSystem", properties);

JobExecution execution = operator.getJobExecution(jobId);
Properties jobParameters = execution.getJobParameters();

您可以将浏览器指向 http://localhost:8080/ticket-agency-longterm/rest/job 并启动您的批处理作业!在运行作业之前,请务必设置您的座位(控制台位于 http://localhost:8080/ticket-agency-longterm/faces/views/setup.xhtml)。

在您的 WildFly 的 bin 目录中,一个示例输出文件可能看起来如下:

Importing...
Chunk size: 2
Persisted: Seat [id=5, booked=true, seatType=com.packtpub.wflydevelopment.chapter12.entity.SeatType@a55bb6e]
Persisted: Seat [id=6, booked=true, seatType=com.packtpub.wflydevelopment.chapter12.entity.SeatType@a55bb6e]
Chunk size: 2
Persisted: Seat [id=7, booked=true, seatType=com.packtpub.wflydevelopment.chapter12.entity.SeatType@440a007]
Persisted: Seat [id=8, booked=true, seatType=com.packtpub.wflydevelopment.chapter12.entity.SeatType@440a007]
Chunk size: 1
Persisted: Seat [id=9, booked=true, seatType=com.packtpub.wflydevelopment.chapter12.entity.SeatType@307124b7]
Import finished

当然,你还可以在应用程序中的特定事件之后或作为传入 JMS 消息的效果使用 Java EE 定时器启动批处理作业。你还可以使用检索到的作业 ID 来监控已运行的作业或根据需要终止它们。批处理框架 API 在作业管理领域提供了许多可能性,而不会带来太多的复杂性。

创建基于作业的批处理步骤

我们基于分块的作业非常适合处理大数据集。然而,如果我们只想执行一个特定的任务怎么办?除了创建分块,我们还可以定义将简单地调用特定类的过程方法的步骤。这些类必须实现 Batchlet 接口(或扩展 AbstractBatchlet 类)。

在我们的示例中,让我们尝试联系一个外部 API 来询问当前的比特币汇率(一种去中心化、虚拟货币)。然后,我们将把我们的票价的当前价格存储在一个简单的平面文件中。我们的批处理单元将如下所示:

@Named
public class BitcoinTask extends AbstractBatchlet { 

    private static final String EXTERNAL_API = "https://api.bitcoinaverage.com/exchanges/USD";
    public static final String FILENAME_PARAM = "bitcoinFile";

    @Inject
    private SeatTypeDao seatTypeDao;

    @Inject
    private JobContext jobContext;

    @Override
    public String process() throws Exception { // [1]
        WebTarget api = ClientBuilder.newClient().target(EXTERNAL_API);
        Response response = api.request().get();
 JsonObject entity = response.readEntity(JsonObject.class); // [2]

        double averageValue = entity.getJsonObject("btce").getJsonObject("rates").getJsonNumber("bid").doubleValue(); 

        Map<SeatType, Double> pricesInBitcoins = calculeteBitcoinPrices(averageValue, seatTypeDao.findAll()); // [3]

 writeToFile(pricesInBitcoins); // [4]

        return "OK";
    }

    private Map<SeatType, Double> calculeteBitcoinPrices(double averageValue, List<SeatType> findAll) {
        return findAll.stream().collect(
                Collectors.toMap(seatType -> seatType, seatType -> seatType.getPrice() / averageValue));
    }

    private void writeToFile(Map<SeatType, Double> pricesInBitcoins) throws Exception {
 Properties jobProperties = jobContext.getProperties(); // [5]
        String fileName = jobProperties.getProperty(FILENAME_PARAM);
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
            writer.write(pricesInBitcoins.toString());
            writer.newLine();
        }
    }
}

流程方法 [1] 是我们进入批处理单元的入口点。我们将首先向外部 API [2] 发起一个 REST 请求,并使用响应来计算我们的比特币价格 [3]。最后,我们将尝试将收集到的数据写入文件。正如你所见,又一次,我们使用 JobContext 从批处理框架(在这种情况下是文件名)获取配置属性。

你可能会想知道,process 方法中的返回类型有什么意义?它仅仅表示作业的状态,如果它已经成功完成或者没有。

那就是我们想要做的,我们通过单个批处理步骤就实现了它:读取、处理和写入。在面向分块的方法中,我们将有三种独立的机制来完成这个任务。让我们将我们的新步骤添加到 externalSystem.xml 中,该文件位于 src/main/resources/META-INF/batch-jobs

<job id="externalSystem" 
    version="1.0">
    <properties>
        <property name="logFile" value="log.txt" />
        <property name="bitcoinFile" value="bitcoins.txt" /> [1]
    </properties>
    <step id="processData" next="checkBitcoins"> [2]
        <chunk item-count="2">
            <reader ref="externalSystemReader" />
            <processor ref="seatProcessor" />
            <writer ref="seatWriter" />
        </chunk>
    </step>
    <step id="checkBitcoins"> [3]
        <batchlet ref="bitcoinTask" />
    </step>
</job>

在 XML 文件中有三个新的需要注意的地方。首先,我们添加了一个新的属性,我们之前在我们的批处理单元 [1] 中引用过。接下来,我们定义了在分块处理步骤之后,我们希望调用另一个步骤,即 checkBitcoins [2]。最后,我们创建了一个新的步骤,在其中我们引用了我们的 batchlet 类。

你可以再次启动你的作业,完成后,一个 bitcoins.txt 文件应该出现在 WildFly 的 bin 目录中。

我们已经涵盖了批处理框架的基础,它允许你满足企业应用中定义的大多数常见需求。然而,规范中还有更多内容,例如拆分、分区以及与工作流相关的元素(状态和决策),如果你正在实施的业务流程需要更复杂的机制,你可以探索这些内容。

我们下一步是使用新的并发工具在 Java EE 容器内提供一些并行性。

在 Java EE 中使用并发工具

在 Java EE 6(特别是在 EJB 容器中),不建议创建新线程,因为应用程序服务器将无法控制平台的稳定性,也无法保证任何事务功能。这可能对希望有效使用 CPU 并并行执行多个任务的应用程序造成问题。可以通过使用 JCA 适配器来克服这一点,但实现它们需要额外的努力。

幸运的是,JSR 236 引入了 ManagedExecutorService(以及 ManagedScheduledExecutorService),这是 Java SE 中使用的 ExecutorService 的容器感知版本。已知的 API 已移植到 Java EE 平台,为 EJB 容器中的并发操作提供了顺畅的工作流程。新的托管执行器服务相对于标准服务有以下优点:

  • 它们依赖于容器提供的线程池。这意味着服务器控制着可以由所有部署的应用程序生成的许多线程,你可以调整配置以确保所需的服务质量。

  • 线程配置完全独立于代码,因此可以在不更改应用程序本身的情况下更改它。

  • 可以将调用者上下文传播到创建的线程中。例如,可以使用启动新线程的用户请求的安全主体。

  • 应用程序服务器允许监控当前线程数。

  • 由托管执行器启动的线程可以为业务组件创建新的事务;然而,它们不能参与来自其他组件的事务。

并发实用工具的主要部分在以下表中描述:

组件 描述
ManagedExecutorService 这用于以异步方式执行提交的任务。开发者可以提交 CallableRunnable 函数,并使用返回的 Future 在结果可用时检查结果。容器上下文将由容器传播。该接口扩展了标准的 ExecutorService 接口。
ManagedScheduledExecutorService 这与 ManagedExecutorService 类似,但它用于在特定时间执行任务(循环、计划或延迟)。该接口扩展了标准的 ScheduleExecutorService,但还提供了触发功能;创建一个动态对象的可能性,该对象可以决定何时触发特定事件(请参阅 javax.enterprise.concurrent.Trigger)。
ContextService 这用于捕获容器的上下文;然后可以在提交作业到执行器服务时使用它。
ManagedThreadFactory 这用于由容器创建线程。开发者可以提供自己的线程工厂以满足特定的用例(例如,在创建的对象上设置特定的属性)。

可以通过 JNDI 查找或@Resource注入来获取这些组件的实例。Java EE 7 规范要求每个容器提供一组默认资源,这些资源可以在没有任何额外配置的情况下注入。因此,在 WildFly 中,获取它们的简单方法就是输入以下代码:

@Resource
private ManagedExecutorService executorService;

@Resource
private ManagedScheduledExecutorService scheduledExecutorService;

@Resource
private ContextService contextService;

您还可以在standalone.xml文件中找到任何额外的执行服务以及默认执行服务的配置(以及其他配置文件变体)。以下展示了相关子系统的一部分配置:

<subsystem >
    <spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
    <concurrent>
        (…)
        <managed-executor-services>
            <managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-threshold="60000" core-threads="5" max-threads="25" keepalive-time="5000"/>
        </managed-executor-services>
        (…)
    </concurrent>
</subsystem>

如您所见,standalone.xml文件包含了默认ManagedExecutorService的配置。您可以使用另一个名称和 JNDI 路径添加一个新的自定义配置;您也可以为每个部署的应用程序创建一个单独的配置。

注意

注意,默认的 ManagedExecutorService 有两个 JNDI 名称:配置文件中的那个和 Java EE 规范中定义的那个(java:comp/DefaultManagedExecutorService)。您可以通过standalone.xml文件中的 default-bindings 标签切换到默认的执行服务(以及其他组件)。

让我们更详细地查看一些执行服务的属性:

  • core-threads:这定义了在所有时间都应该在线程池中保持活跃的线程数量(即使这些线程是空闲的,服务器也没有处理用户请求)。

  • max-threads:这表示服务器在必要时可以启动多少个线程(包括核心线程),例如,在负载很重的情况下。

  • keepalive-time:这定义了线程在服务器将其杀死之前可以空闲多少毫秒(仅当运行的线程数多于指定的 core-threads 参数时适用)。此配置值定义了服务器在不再需要额外线程时将保留这些线程的时间长度。

  • hung-task-threshold:这定义了服务器将在多少毫秒后将一个线程标记为挂起。如果设置为0,则线程永远不会被标记为挂起(线程将没有执行时间限制)。

通过使用这些配置属性和创建额外的执行服务,服务器管理员可以精细控制服务器在特定时间可以处理的最大负载。确保在应用程序性能调整期间仔细查看它们。

对于开发而言,默认配置非常适合我们,因此现在是时候通过并发工具的示例用法来深入代码了!

将线程引入企业 Bean

当我们与批处理框架一起工作时,我们联系了一个 REST 端点,该端点在我们的示例中模拟了一个外部系统。现在,我们将向其中添加一些并发性。

外部系统可能从多个来源汇总预订请求。如果每个请求都需要大量时间,同时发出所有请求可能是个好主意。让我们从创建 Callable 开始,它将返回应该预订的座位 ID 列表。如下面的代码片段所示:

package com.packtpub.wflydevelopment.chapter12.external;

import java.util.concurrent.Callable;
import javax.enterprise.concurrent.ManagedTask;
import javax.enterprise.concurrent.ManagedTaskListener;
import javax.enterprise.inject.Instance;

public class GenerateSeatRequestFromArtificial implements Callable<List<Integer>>, ManagedTask [1] {

    @Inject
    private Logger logger;

 @Inject
 private Instance<TaskListener> taskListener; [2]

    @Override
    public ManagedTaskListener getManagedTaskListener() {
        return taskListener.get(); [3]
    }

    @Override 
    public Map<String, String> getExecutionProperties() {
 return new HashMap<>(); [4]
    }

    @Override
    public List<Integer> call() throws Exception {
        logger.info("Sleeping...");
 Thread.sleep(5000); [5]
        logger.info("Finished sleeping!");

        return Arrays.asList(4, 5, 6);
    }
}

我们的任务实现了 [1] 两个接口:CallableManagedTaskManagedExecutorService 需要一个满足 Java SE 中 CallableRunnable 接口契约的对象。

ManagedTask 接口是可选的,但它允许我们与任务本身一起注册 ManagedTaskListener 并从任务返回额外的属性。任务监听器有一组生命周期回调,这些回调在任务执行期间被调用。我们将使用它来记录有关我们任务的额外信息。为了创建任务监听器的实例,我们使用了 Instance<T>[2]。它用于按需创建 CDI 实例。我们从 ManagedTask 接口的方法中返回 ManagedTaskListener [3]。我们不需要任何额外的属性;因此,我们从 ManagedTask 接口的第二个方法中返回一个空对象 [4]

最后,我们实现了 call 方法;线程将被挂起 5 秒钟(以模拟长时间工作)并返回一个预定义的 ID 列表。

我们的任务监听器只是一个带有记录器的 bean,它将获取有关任务生命周期的所有信息。如下面的代码片段所示:

public class TaskListener implements ManagedTaskListener {

    @Inject
    private Logger logger;

    @Override
    public void taskSubmitted(Future<?> future, ManagedExecutorService executor, Object task) {
        logger.info("Submitted " + task);
    }

    @Override
    public void taskAborted(Future<?> future, ManagedExecutorService executor, Object task, Throwable exception) {
        logger.log(Level.WARNING, "Aborted", exception);
    }

    @Override
    public void taskDone(Future<?> future, ManagedExecutorService executor, Object task, Throwable exception) {
        logger.info("Finished task " + task);
    }

    @Override
    public void taskStarting(Future<?> future, ManagedExecutorService executor, Object task) {
        logger.info("Starting " + task);
    }
}

如您所见,大多数实现的方法都是获取执行器服务、未来和任务本身作为参数。我们简单地使用注入的记录器记录当前状态。

因此,我们已经创建了一个任务,它相当静态。现在,让我们尝试创建另一个任务,该任务将联系数据库。和之前一样,我们需要一个 Callable 实现来返回一个整数列表。如下面的代码片段所示:

public class GenerateSeatRequestsFromDatabase implements Callable<List<Integer>> {

    private static final int SEATS_TO_RETURN = 3;

    @Inject
 private SeatDao dao; // [1]

    @Inject
    private Logger logger;

    @Override
    public List<Integer> call() throws Exception {
        logger.info("Sleeping...");
 Thread.sleep(5000); // [4]
        logger.info("Finished sleeping!");

 List<Seat> databaseSeats = dao.findAll(); // [2]

 List<Integer> freeSeats = databaseSeats.stream()
 .filter(seat -> !seat.getBooked())
 .limit(SEATS_TO_RETURN)
 .map(seat -> seat.getId().intValue())
 .collect(Collectors.toList()); // [3]

        if (freeSeats.isEmpty()) {
            logger.info("No seats to book");
        } else {
            logger.info("Requesting booking for " + freeSeats);
        }
        return freeSeats;
    }
}

与上一个任务相比,主要区别在于我们注入了一个 EJB [1],这将启动一个底层事务。在 call 方法中,发出一个数据库请求 [2]。然后,返回的座位列表被过滤并转换成一个座位 ID 列表 [3]

此外,如前所述,我们将停止线程 5 秒钟,以便我们可以在稍后观察执行情况 [4]

我们已经准备好了构建块。现在,是时候将它们组合成一个工作示例了。我们可以从本章开头提到的 PendingSeats 类开始,如下面的代码所示:

package com.packtpub.wflydevelopment.chapter12.external;

@Singleton
@Startup
public class PendingSeats {

    private final Queue<Long> seats = 
                                new ConcurrentLinkedQueue< >();

 @Resource
 private ManagedExecutorService executorService; // [1]

 @Inject  // [2]
 private Instance<GenerateSeatRequestsFromDatabase> databaseCollector; 

 @Inject
 private Instance<GenerateSeatRequestFromArtificial> artificalCollector;

    @Inject
    private Logger logger;

    @PostConstruct
    private void setUp() {
        try {
 List<Future<List<Integer>>> futures = executorService.invokeAll(Arrays.asList(
 databaseCollector.get(), artificalCollector.get()
)); // [3]

 List<Integer> requestedIds = futures.stream().flatMap(future -> get(future).stream()).distinct()
 .collect(Collectors.toList()); // [4]

            logger.info(requestedIds.toString());
        } catch (InterruptedException e) {
            logger.log(Level.SEVERE, e.getMessage(), e);
        }

    }

    private List<Integer> get(Future<List<Integer>> future) {
        try {
            return future.get();
        } catch (InterruptedException | ExecutionException e) {
            logger.log(Level.SEVERE, e.getMessage(), e);
            return new ArrayList<>();
        }
    }
}

我们首先使用@Resource注解[1]获取ManagedExecutorService的实例。接下来,使用 CDI 的Instance<T>类模式[2]注入先前创建的任务。多亏了这一点,它们是管理 Bean,并且它们的依赖项被注入。有了依赖项,我们使用executorServiceinvokeAll方法[3]一次性启动所有任务(我们也可以使用多次调用submit方法)。返回值代表一组未来结果,可以在数据准备好时用于检索收集的数据。

到目前为止,我们的任务已经开始运行,因此我们可以在未来结果上简单地执行阻塞的get调用并等待数据[4]。当它准备好时,我们删除任何重复项,并使用flatMap操作将结果收集到一个单独的列表中。如您所记得,我们之前的两个任务各自等待了 5 秒钟。由于它们是同时执行的,我们预计它们将在 5 秒后同时完成。

因为我们的 Bean 是一个带有启动注解的单例,所以整个流程将在我们的应用程序部署期间被调用。现在就试试看吧!

当然,数据库任务需要在seats表中有一些数据,否则将返回空结果(对我们来说这不是一个大问题)。如果您希望应用程序自动将一些数据种入数据库,您可以创建另一个单例 Bean,例如:

@Startup
public class DatabaseInitializer {

    @PersistenceContext
    private EntityManager em;

    @PostConstruct
    public void setup() {
        SeatType seatType = new SeatType();
        seatType.setPosition(SeatPosition.BALCONY);
        seatType.setDescription("Test Data");
        seatType.setQuantity(10);
        seatType.setPrice(10);
        em.persist(seatType);

        Seat seat = new Seat();
        seat.setSeatType(seatType);
        em.persist(seat);

    }
}

请确保在PendingSeatsBean 上添加@DependsOn("DatabaseInitializer")注解,以便初始化器在我们数据库收集器之前运行。

如果一切顺利,您应该在控制台看到如下内容:

23:42:48,455 INFO  [TaskListener] (ServerService Thread Pool -- 54) Submitted GenerateSeatRequestFromArtificial@4256cb0c
23:42:48,456 INFO  [GenerateSeatRequestsFromDatabase] (EE-ManagedExecutorService-default-Thread-1) Sleeping... (1)
23:42:48,456 INFO  [TaskListener] (EE-ManagedExecutorService-default-Thread-2) Starting GenerateSeatRequestFromArtificial@4256cb0c
23:42:48,456 INFO  [GenerateSeatRequestFromArtificial] (EE-ManagedExecutorService-default-Thread-2) Sleeping... (2)
23:42:53,457 INFO  [GenerateSeatRequestsFromDatabase] (EE-ManagedExecutorService-default-Thread-1) Finished sleeping!
23:42:53,461 INFO  [GenerateSeatRequestFromArtificial] (EE-ManagedExecutorService-default-Thread-2) Finished sleeping!
23:42:53,461 INFO  [TaskListener] (EE-ManagedExecutorService-default-Thread-2) Finished task GenerateSeatRequestFromArtificial@4256cb0c
23:42:53,617 INFO  [GenerateSeatRequestsFromDatabase] (EE-ManagedExecutorService-default-Thread-1) Requesting booking for [1]
23:42:53,621 INFO  [PendingSeats] (ServerService Thread Pool -- 54) [1, 4, 5, 6] (3)

如您所见,两个任务同时在两个不同的线程中启动(1 和 2),注意日志中的EE-ManagedExecutorService-default-Thread-1…-Thread-2条目)。最终结果在大约 5 秒后产生,它包含来自两个收集器的数据,并且还收集在最初提交任务的线程中(ServerService Thread Pool -- 54)。

您还可以使用 Java VisualVM 工具来可视化应用程序服务器中的线程。该工具位于您的 JDK 安装的bin目录中(jvisualvm可执行文件)。运行它后,您应该在左侧树中看到 JBoss,并在点击 JBoss 节点后看到线程选项卡。这在上面的屏幕截图中显示:

向企业 Bean 介绍线程

如果您在应用程序部署期间切换到线程选项卡,您将看到一个图表,如以下屏幕截图所示:

向企业 Bean 介绍线程

紫色表示休眠线程,时间轴上带有紫色部分的两个突出显示的线程是执行过程中的我们的任务。您可以使用详细的线程视图来进一步检查您的工作线程。这在上面的屏幕截图中显示:

向企业 Bean 介绍线程

Java VisualVM 为每个开发者提供了许多有用的功能,例如虚拟机的资源监控、分析器、采样器等,这些功能都作为专用插件实现。务必查看它们!

在本节中,我们实现了一个在 Java EE 之前版本中难以适当覆盖的用例。得益于提供给开发者的高级 API,我们能够用更少的代码完成这项工作。

摘要

在本章中,你学习了如何使用新的批处理框架以两种不同的方式创建批处理应用程序。接下来,我们尝试了一些并发工具提供的机制。我们的探索从用户交互转向了中间件层的内部。

在下一章中,我们将填补 Java EE 开发者工具箱中的最后一个空白,即使用 Arquillian 进行集成测试。

第十三章。测试你的应用程序

在前面的章节中,我们介绍了 Java EE 平台最重要的技术。然而,每个专业开发者都知道,软件开发应该从编写测试开始。起初,能够验证 EJBs、数据库相关代码或 REST 服务等的执行正确性似乎并不容易,但使用正确的工具时,这看起来非常直接!在本章中,我们将介绍用于 Java EE 应用程序测试的基本测试框架:Arquillian。此外,我们还将探讨其扩展和相关库。

在本章中,我们将重点关注以下主题:

  • 从模拟对象到 Arquillian 框架的企业级测试简介

  • 如何将 Arquillian 测试用例集成到我们的票务机应用程序中

  • 如何使用 Eclipse IDE 和 Maven shell 运行 Arquillian 测试

  • 最重要的 Arquillian 扩展及其使用方法

测试类型

“测试”这个词可以有多种解释。最常见的是,测试执行应用程序需求的验证和验证。测试可以在多个级别上执行,从单个方法到整个业务功能。测试还可以涵盖非功能性方面,如安全或性能。

首先,让我们介绍验证功能性需求的测试类别。Mike Cohn 提出了一个测试金字塔的概念,如下所示:

测试类型

正如你所见,应用程序中的大多数测试通常是覆盖代码单元的测试。一个单元可以是一个单独的方法,这是最基本的功能。正因为这个范围,这类测试被称为单元测试。它们可以被定义为程序员编写的测试,以验证相对较小的功能是否按预期工作。由于单元相对较小,这些测试的数量会迅速增加,因此它们成为金字塔最低层的应用测试基础。

下一种类型的测试关注更大的代码区域。它们覆盖整个服务或业务功能。这意味着它们覆盖多个代码单元,涉及不同的模块和库。这种测试的数量将低于单元测试的数量。这种类型的测试通常被称为集成测试。集成测试的目的是证明系统的不同部分可以协同工作;由于它们覆盖整个应用程序,因此需要投入更多的努力来构建。例如,它们通常需要为它们分配数据库实例和硬件等资源。集成测试在证明系统如何工作方面做得更有说服力(尤其是对非程序员来说);至少在集成测试环境与生产环境相似的程度内。

最后一种测试类型是 UI 测试,也可以称为验收测试。它们在项目中的数量最少;它们通常是最难编写的,以模拟用户与应用程序的交互。它们覆盖整个需求和功能。

让我们暂时放下非功能性测试的话题。目前,你只需要记住它们可以涵盖与性能、安全等相关的话题。

用于测试的仪器

如你所想,每种测试类型都使用不同的方法,通常需要不同的测试库。

当你编写单元测试时,你只需提供一些方法输入参数,并验证它们的输出是否符合预期。在 Java 中,你可能已经使用了JUnitTestNGSpock。当你从更大的代码部分转向测试整个服务时,可能会出现一些问题。通常很难分离你想要测试的代码,使其可测试而不需要运行所有其他服务。你通常会创建一些模拟对象来模拟你不想包含在测试中的模块的行为。如果你有一个你想要测试的对象,并且如果这些方法依赖于另一个对象,你可以创建一个依赖项的模拟而不是依赖项的实际实例。这允许你在隔离的情况下测试你的对象。

例如,一个常见的用例可能是在数据库应用程序中,你想要测试,例如,用户注册过程,但你不想运行整个数据库(这意味着你将设置其模式,一些初始数据,并在测试完成后手动清理其状态)。你可以模拟数据库交互并定义某些方法执行的行为,例如,你的存根将始终返回四个用户,这些用户将被硬编码在你的测试代码中。

这种方法虽然很容易理解并付诸实践,但有几个局限性。首先,它让你陷入一个人工环境,你经常会对该环境的行為和稳定性做出无效的假设。

其次,你可能会得到一段难以维护的模拟代码,这会让你的测试通过,并给你一种完成了出色工作的温暖感觉。

第三,有时很难隔离你想要测试的服务,并且模拟所有交互的代码可能比有意义的测试代码还要长。

因此,即使模拟对象可能对启动系统提供一些好处,尤其是在你没有某个特定子系统的完整实现时,最好尽可能接近代码预期运行的靶环境。在某个时刻,无模拟运动(非仅模拟运动)被发起,指出模拟通常花费太多时间,并让你专注于编写模拟而不是编写测试。

Arquillian试图解决这些问题。它是一个简化 Java 中间件集成测试的平台。它处理所有容器管理、部署和框架初始化的底层工作,这样你就可以专注于编写测试任务——真正的测试。Arquillian 通过覆盖测试执行周围的方面来最小化对开发者的负担;以下是一些方面:

  • 管理容器的生命周期(启动/停止)

  • 将测试类及其依赖的类和资源捆绑成一个可部署的存档

  • 增强测试类(例如,解决@Inject@EJB@Resource注入)

  • 将存档部署到测试应用程序(部署/卸载),并捕获结果和失败

Arquillian 还有扩展,可以增强其功能,例如,允许它执行 UI 测试或一些非功能性测试。

在下一节中,我们将讨论运行你的集成测试所需的仪器。

开始使用 Arquillian

虽然 Arquillian 不依赖于特定的构建工具,但它通常与 Maven 一起使用;它提供依赖管理,因此简化了将 Arquillian 库包含到应用程序中的任务,因为它们分布在 Maven 中央仓库中。

根据你用于生成的存档类型,你的项目可能会有不同的文件夹结构;这不是问题。真正重要的是,你需要在你的src文件夹下提供以下结构:

  • main/java/: 将所有应用程序 Java 源文件放在这里(在 Java 包下)

  • main/resources/: 将所有应用程序配置文件放在这里

  • test/java/: 将所有测试 Java 源文件放在这里(在 Java 包下)

  • test/resources/: 将所有测试配置文件放在这里(例如,persistence.xml

因此,到目前为止,我们将在test/java下工作,这是我们放置第一个 Arquillian 测试类的地方。

编写 Arquillian 测试

如果你一直在使用 JUnit (www.junit.org),你将找到一个类似的 Arquillian 测试,其中包含一些额外的特色。

为了做到这一点,我们将使用 Eclipse 和 Maven,就像我们迄今为止所做的那样。如果你即将向你的项目添加测试类,显然没有必要为此创建一个新的项目。然而,为了学习目的,我们在这个单独的项目中提供了这个示例,这样你可以确切地看到为了运行 Arquillian 测试需要添加什么。

为了避免从头开始重新创建整个项目,你可以简单地克隆ticket-agency-jpa项目并将其命名为ticket-agency-test,将根包从com.packtpub.wflydevelopment.chapter5移动到com.packtpub.wflydevelopment.chapter13。如果这仍然看起来工作量太大,你可以直接从书中的示例导入Chapter13项目。

配置 pom.xml 文件

运行 Arquillian 测试所必需的第一件事是包含junit依赖项,这是运行我们的单元测试所必需的:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
    <version>4.11</version>
</dependency>

在前面的章节中,我们介绍了术语物料清单BOM)。现在,我们将使用 Arquillian BOM 来导入所有与 Arquillian 相关的依赖项的版本:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.jboss.arquillian</groupId>
            <artifactId>arquillian-bom</artifactId>
            <version>1.1.5.Final</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

我们使用 Arquillian 与 JUnit(如前所述,其他可能性有TestNGSpockJBehaveCucumber),因此我们需要包含适当的依赖项:

<dependency>
    <groupId>org.jboss.arquillian.junit</groupId>
    <artifactId>arquillian-junit-container</artifactId>
    <scope>test</scope>
</dependency>

在完成基本依赖项之后,我们现在必须指定测试将运行的容器。对于更重要的 Java EE 应用程序服务器(WildFly、Glassfish、WebLogic 和 WebSphere)以及一些 Servlet 容器(如 Tomcat 或 Jetty),都提供了容器适配器。在这里,我们想使用 WildFly,因此我们将使用适当的容器适配器。然而,我们有几种可能的选择。容器适配器可以分为三个基本组:

  • 嵌入式:这是在测试运行的同一 JVM 实例上运行容器的模式。通常,在这种模式下运行的容器不是原始的,而是打包到一个单一的 JAR 文件中,具有有限的版本。

  • 托管:在这种模式下,真实的应用程序服务器在单独的 JVM 上运行。正如其名所示,可以管理容器状态,启动它,停止它等。默认情况下,当你运行测试时,服务器启动,测试针对它运行,然后停止。然而,可以配置 Arquillian 在已运行的实例上运行测试。

  • 远程:在这种模式下,我们只是连接到某个现有的服务器实例并对其运行测试。

运行测试的最通用选择是托管容器。测试在真实服务器上运行,与生产环境相同,此外,还可以管理其状态,允许进行一些更高级的测试,例如测试与高可用性或运行在不同实例上的两个应用程序之间的通信相关的功能。现在,我们需要将适当的容器适配器添加到我们的pom.xml文件中。为此,我们将创建一个 Maven 配置文件:

<profile>
    <id>arq-wildfly-managed</id>
    <dependencies>
        <dependency>
            <groupId>org.wildfly</groupId>
            <artifactId>wildfly-arquillian-container-managed</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</profile>

可能会有一些情况,你希望针对不同的应用程序服务器运行测试。你可以定义几个 Maven 配置文件,并多次运行测试,每次激活其他配置文件。记住,一些应用程序服务器不提供所有类型的适配器。

有一个与容器相关的话题。我们的 Arquillian 测试使用一个协议与应用程序服务器上的微部署进行通信。如果我们不指定协议,容器将选择默认的协议。为了手动指定它,我们需要添加org.jboss.arquillian.protocol依赖项(这样命名是为了与 Servlet 3.0 规范兼容):

<dependency>
    <groupId>org.jboss.arquillian.protocol</groupId>
    <artifactId>arquillian-protocol-servlet</artifactId>
    <scope>test</scope>
</dependency>

编写你的第一个 Arquillian 测试

一旦配置完成,我们最终将编写我们的测试代码。因此,在包com.packtpub.wflydevelopment.chapter13.test下创建一个名为TicketTest的 Java 类。你将首先添加到这个类的以下注解,告诉 JUnit 使用 Arquillian 作为测试控制器:

@RunWith(Arquillian.class)
public class TicketServiceTest {

}

Arquillian 随后会寻找一个带有@Deployment注解的静态方法;它创建一个包含所有指定类和资源的微部署(而不是部署整个应用程序),允许只测试系统的一部分:

@Deployment
public static Archive<?> createTestArchive() {
    return ShrinkWrap.create(WebArchive.class)
         addPackage(SeatType.class.getPackage())
        .addPackage(TicketService.class.getPackage())
        .addPackage(LoggerProducer.class.getPackage())
        .addAsResource("META-INF/persistence.xml")
        .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
}

ShrinkWrap项目提供的流畅 API(www.jboss.org/shrinkwrap)使得使用create方法实现这种技术成为可能,该方法接受部署单元的类型(WebArchive)作为参数,并将所有资源包含在这个归档中。在我们的例子中,我们不是包含所有单个类,而是使用addPackage实用方法添加包含在类包中的所有类(例如,通过添加SeatType.class.getPackage()方法,我们将包括与SeatType类在同一包中的所有类)。我们的项目使用 JPA,所以我们还添加了持久化配置;在这里,我们指定.xml文件的路径,这样我们就可以指向使用其他非生产数据库的某些其他测试配置。当然,我们还需要添加空的beans.xml文件以启用 CDI。

最后,我们注入我们想要测试的服务(是的,可以将服务注入到测试类中)并添加一个测试方法:

@Inject
TicketService ticketService;

@Test
public void shouldCreateSeatType() throws Exception {
    // given
    final SeatType seatType = new SeatType();
    seatType.setDescription("Balcony");
    seatType.setPrice(11);
    seatType.setQuantity(5);

    // when
    ticketService.createSeatType(seatType);

    // then
    assertNotNull(seatType.getId());
}

在这里,shouldCreateSeatType方法将使用TicketService类的createSeatType方法创建一个新的SeatType属性。注意我们如何像在服务器端运行此代码一样注入TicketService

我们的第一个测试用例现在准备好了。我们只需要在我们的项目src/test/resources下添加一个名为arquillian.xml的 Arquillian 配置文件:

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

            xsi:schemaLocation="http://jboss.org/schema/arquillian
        http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

    <container qualifier="jboss-managed" default="true">
        <!-- Additional configuration -->
    </container>

</arquillian>

你必须配置容器适配器。在这个例子中,我们假设你已经将JBOSS_HOME环境变量设置为 WildFly 主目录。在这种情况下,不需要更多的配置。然而,如果你想运行非标准的东西,例如连接到具有更改的管理端口的远程容器,那么这个文件是修改这个设置的适当位置。当你没有指定JBOSS_HOME时,你可以使用property如下设置 WildFly 的位置:

<container qualifier="jboss-managed" default="true">
    <configuration>
        <property name="jbossHome">C:\wildfly</property>
    </configuration>
</container>

然而,当有多个人在项目上工作时,这个方法可能很难维护。为了避免问题,你可以使用系统属性解析,例如${jbossHome}

如果你配置了远程容器,配置看起来就像这样:

<container qualifier="jboss-remote" default="true">
    <configuration>
        <property name="managementAddress">localhost</property>
        <property name="managementPort">9999</property>
    </configuration>
</container>

运行 Arquillian TicketTest

你可以从 Maven 和你的 IDE 中运行 Arquillian 测试。你必须记住我们在 Maven 配置文件中声明了容器适配器,因此为了运行完整的构建,你必须运行以下命令行:

mvn clean package –Parquillian-wildfly-managed

如果你想要从 Eclipse 运行测试,你必须导航到项目属性并选择 Maven 属性。在 活动 Maven 配置文件 字段中,输入 arquillian-wildfly-managed(如以下截图所示),这是我们之前在 pom.xml 文件中声明的:

运行 Arquillian TicketTest

现在你只需要右键单击你的 TicketServiceTest 类并选择 运行 As JUnit Test。Arquillian 引擎将启动,在 JUnit 视图中产生测试结果(你可以通过导航到 菜单 | 窗口 | 显示视图 | JUnit)来使其可见)。

恭喜!JUnit 控制台记录了第一个成功运行的测试。

如果你只想在测试中使用一个容器,那么在 pom.xml 文件中添加以下行以设置默认 Maven 配置文件是一个好主意:

<activation>
    <activeByDefault>true</activeByDefault>
</activation>

使用 Spock 运行 Arquillian 测试

Arquillian 不仅限于仅使用 JUnit。正如我们之前提到的,已经有容器,例如 TestNG 和 Spock;让我们专注于第二个。

Spock 是一个用 Groovy 编写的现代测试框架,它使用了一些 Groovy 语言特性来使你的测试更易于阅读和编写。Spock 的主要目标是测试 Groovy 代码,但它非常适合编写各种 Java 代码的测试。Spock 通过其 领域特定语言 (DSL) 引入了一些额外的语义,以便使测试更加简单和开发者友好。

让我们使用 Spock 重新编写之前的测试示例:

@RunWith(ArquillianSputnik.class)
class TicketServiceTest extends Specification {

    @Deployment
    def static WebArchive createTestArchive() {
        return ShrinkWrap.create(WebArchive.class)
               .addPackage(SeatType.class.getPackage())
               .addPackage(TicketService.class.getPackage())
               .addPackage(LoggerProducer.class.getPackage())
               .addAsResource('META-INF/persistence.xml')
               .addAsWebInfResource(EmptyAsset.INSTANCE, 'beans.xml');
    }

    @Inject
    TicketService ticketService;

    def "should create SeatType"() {
        given:
        def seatType = new SeatType(description: "Balcony", 
                                    price: 11, quantity: 6)

        when:
        ticketService.createSeatType(seatType);

        then:
        seatType.getId() != null
    }
}

你可以注意到一些差异。首先,它确实是 Groovy!其次,测试使用了一个不同的运行器,ArquillianSputnik。更重要的是,你在这里已经可以看到一些 Spock DSL,例如来自 行为驱动开发 (BDD) 的 givenwhenthen 构造。given 构造预期将系统置于特定状态,when 描述一个动作,而 then 包含验证动作结果的断言。

这个完整的 Spock 示例,包括完整的 pom.xml 配置,可以在本章的示例项目 ticket-agency-spock 中找到。有关 Arquillian Spock 测试运行器、其功能和用法说明的更多信息,可以在 GitHub 上找到:github.com/arquillian/arquillian-testrunner-spock。有关 Spock 的更多信息也可以在 GitHub 上找到:github.com/spockframework/spock

ShrinkWrap 解析器

几乎在每一个 Arquillian 测试中,你可能会使用 ShrinkWrap 来创建微部署。在使用它一段时间后,你可能会注意到一些不足之处。你可能想知道当你有一个依赖于某些外部库的测试时会发生什么;你是否需要添加该库的所有包?答案是:不需要。ShrinkWrap 解析器提供了与 Maven 的集成,并且基本 Gradle 支持也是可用的。你只需在测试中写下你想要包含在存档中的依赖项,它将与微部署一起部署。

让我们看看 ShrinkWrap 解析器 Maven 集成的基本示例:

Maven.resolver().resolve("G:A:V").withTransitivity().asFile();

前面的行意味着我们想要从 Maven 的中心仓库中解析具有给定组 ID、工件 ID 和版本(规范形式的 Maven 坐标)的工件,包括所有依赖项,并将其转换为文件列表。

然而,在这个例子中,你必须同时在测试代码和构建文件中维护工件版本。你可以改进这一点!只需从你的pom.xml文件中导入一些依赖项数据,这样 ShrinkWrap 解析器就可以解析与主项目使用相同版本的工件:

Maven.resolver().loadPomFromFile("/path/to/pom.xml").
resolve("G:A").withTransitivity().asFile();

因此,首先,加载pom.xml数据,包括所有依赖管理部分和工件版本。此外,工件坐标不必包含版本。

这些是最基本的功能。你可以手动完全配置解析器,你想要使用的存储库,要应用 Maven 配置文件,以及更多。现在让我们抓取一个例子。

假设你正在使用 JUnit 和一些花哨的断言库来测试你的项目。AssertJFEST断言的继任者)是一个流畅的断言库,它允许你以更易于阅读的形式编写你的项目:

assertThat(frodo.getName()).isEqualTo("Frodo");

在每个测试中使用这样的库意味着你必须将它包含在每个微部署中。还有另一件事你总是需要的:beans.xml文件。所以让我们创建一些实用类:

public class ArquillianWarUtils {

    private static final String BEANS_XML = "beans.xml";
    private static final String ASSERTJ_COORDINATE =
                                  "org.assertj:assertj-core";

    private static File[] ASSERTJ_ARTIFACT = Maven.resolver()
     .loadPomFromFile("pom.xml").resolve(ASSERTJ_COORDINATE)
     .withTransitivity().asFile();

    public static WebArchive getBasicWebArchive() {
        return ShrinkWrap.create(WebArchive.class)
            .addAsLibraries(ASSERTJ_ARTIFACT)
            .addAsWebInfResource(EmptyAsset.INSTANCE, BEANS_XML);
    }
}

此外,现在在每一个测试用例中,你只需编写以下代码:

    @Deployment
    public static WebArchive createDeployment() {
        return ArquillianWarUtils.getBasicWebArchive()
                    .addPackage(SomePackage.class.getPackage();
    }

在某个时候,你可能还想做另一件事;而不是手动添加所有库,你可以在运行时依赖项中导入它们:

Maven.resolver().loadPomFromFile("pom.xml")
               .importRuntimeDependencies().resolve()
               .withTransitivity().asFile();

有一些不幸的情况,其中无法将项目的一部分隔离以进行微部署。你只是不断地向其中添加更多和更多的类,而且没有尽头。这意味着你的项目可能设计得不好,但假设你想要在某个现有的遗留项目中引入 Arquillian,而你对其结构没有影响力。在这种情况下,你可能希望将整个项目导入到集成测试中。有些人会玩一些小把戏;他们只是使用基本的 ShrinkWrap,并使用ZipImporter ShrinkWrap 导入.jar.war文件:

ShrinkWrap
    .create(ZipImporter.class)
    .importFrom(new File("/target/myPackage.war"))
    .as(WebArchive.class);

问题在于这个存档中到底有什么?你很可能导入了在之前的构建过程中创建的存档,因为它是测试完成后创建的!更重要的是,当你只是从 IDE 中工作而不运行完整的 Maven 构建时,它甚至可能不存在!这是你可以使用 MavenImporter 类的地方。请参考以下代码:

ShrinkWrap.create(MavenImporter.class)
    .loadPomFromFile("/path/to/pom.xml")
    .importBuildOutput()
    .as(WebArchive.class);

就这样!内部运行简化构建,收集编译后的类和资源,并将其打包到存档中。它不会在完整的 Maven 构建中使用某个嵌入实例运行,因为那样会慢得多。你可能想将此类方法添加到你的测试工具中:

public class ArquillianWarUtils {

    // already presented code

    public static WebArchive getBasicWebArchive() { . . . }

    public static WebArchive importBuildOutput() {
        return ShrinkWrap.create(MavenImporter.class)
                  .loadPomFromFile("pom.xml")
                  .importBuildOutput()
                  .as(WebArchive.class);
    }
}

自 ShrinkWrap Resolver 2.2.0-alpha-1 以来,Gradle 项目也存在一个类似的功能。然而,它内部使用 Gradle 工具 API:

ShrinkWrap.create(EmbeddedGradleImporter.class)
    .forProjectDirectory("/path/to/dir")
    .importBuildOutput()
    .as(WebArchive.class);

在某个时候,你可能会惊讶地发现最后一个例子没有工作。原因可能是 arquillian-bom 没有包含这个 ShrinkWrap 解析器版本。然而,可以通过另一个 BOM 覆盖 BOM 导入的版本。这很简单;只需首先插入更重要的 BOM:

<dependencyManagement>
     <!-- shrinkwrap resolvers import must be before arquillian bom! -->
    <dependency>
        <groupId>org.jboss.shrinkwrap.resolver</groupId>
        <artifactId>shrinkwrap-resolver-bom</artifactId>
        <version>${version.shrinkwrap-resolver}</version>
        <scope>import</scope>
        <type>pom</type>
    </dependency>
    <dependency>
        <groupId>org.jboss.shrinkwrap</groupId>
        <artifactId>shrinkwrap-bom</artifactId>
        <version>${version.shrinkwrap}</version>
        <scope>import</scope>
        <type>pom</type>
    </dependency>
</dependencyManagement>

更多关于 ShinkWrap 解析器的信息可以在其 GitHub 仓库github.com/shrinkwrap/resolver中找到。

ShrinkWrap 描述符

ShrinkWrap 家族还有一个项目。它相对不太受欢迎,很多人都不了解,它被称为 ShrinkWrap 描述符。它的目标是提供一个流畅的 API,用于创建你通常创建并插入到你的微部署中的描述符资源。

让我们从例子开始。假设你正在编写一个持久化框架扩展。在这样做的时候,你使用了大量的 persistence.xml 文件,如下面的代码所示:

<persistence>
   <persistence-unit name="myapp">
      <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <jta-data-source>java:/DefaultDS</jta-data-source>
      <properties>
         <property name="hibernate.dialect" 
                  value="org.hibernate.dialect.HSQLDialect"/>
         <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
      </properties>
   </persistence-unit>
</persistence>

使用 ShrinkWrap 描述符,你不必将这些文件全部放在 src/test/resources 中,然后再从特定的测试中引用它们,你只需在测试本身中放入一些代码:

final PersistenceDescriptor persistence = Descriptors
  .create(PersistenceDescriptor.class)
            .createPersistenceUnit()
               .name("myapp")
               .provider("org.hibernate.ejb.HibernatePersistence")
               .jtaDataSource("java:/DefaultDS")
               .getOrCreateProperties()
                  .createProperty().name("hibernate.dialect")
                     .value("org.hibernate.dialect.HSQLDialect").up()
                  .createProperty().name("hibernate.hbm2ddl.auto")
                     .value("create-drop").up()
               .up().up()

这样的 PersistenceDescriptor 类可以导出为 String,或者直接添加到 ShrinkWrap 存档中。

默认情况下,项目包含适用于 Java EE 平台所有最重要的 .xml 文件的描述符。然而,它还允许使用 XSD 和 DTD 文件进行代码生成。请注意,它仍处于 alpha 阶段。它是稳定的,但 API 可能已经更改。

持久化测试

真正的挑战开始于你必须将其他系统包含到测试过程中时。即使只是测试与关系型数据库的交互,也可能引发问题。在第五章中,将持久化与 CDI 结合,我们介绍了 JPA。现在,是时候描述如何测试与它的交互了。

在测试数据库相关代码时,必须考虑以下几个问题:

  • 如何验证数据是否真的被插入到数据库中?

  • 如何在测试之间维护数据库状态,以及如何自动清理它?

Arquillian 持久性扩展允许您测试这两件事。在运行测试之前,您可以从.xml.xls.yaml.json或自定义 SQL 脚本中为数据库播种。这是通过仅使用@UsingDataSet("path-to-seeding-file")注解来注解测试用例来完成的。在测试执行后,您可以使用@ShouldMatchDataSet("path-to-dataset")注解将数据库状态与另一个文件进行比较。让我们看一个例子:

@Test
@UsingDataSet("datasets/seats.yml")
@ShouldMatchDataSet("datasets/expected-seats.yml")
public void shouldMakeACleanup() throws Exception {
    // given
    // from annotation

    // when
    ticketService.doCleanUp();

    // then
    // from annotation
}

seats.ymlexpected-seats.xml文件只是简单的 YAML 文件,放置在/src/test/resources/datasets中。第一个文件包含SeatType

Seat_Type:
  - description: test
    position: "box"
    price: 10
    quantity: 10

第二个文件包含:

Seat_Type:

由于我们正在执行清理。请注意,这里使用的名称和值是 SQL 名称,而不是 JPA 名称。

JPA 允许您使用二级缓存来提高操作性能。有了这个,并非所有操作都会立即反映在数据库状态上。在测试运行期间,您可能对@JpaCacheEviction注解感兴趣,该注解在每次测试运行后使缓存失效。每个测试也被封装在一个单独的事务中,这样它就不会影响其他测试的执行

当然,您需要一些依赖项才能使此扩展正常工作。恰好有三个,如下所示:

<dependency>
    <groupId>org.jboss.arquillian.extension</groupId>
    <artifactId>arquillian-persistence-dbunit</artifactId>
    <version>1.0.0.Alpha7</version>
    <scope>test</scope>
</dependency>

Arquillian 1.1.4.Final 版本有一个 bug,即使不应该通过所有持久性测试,它也会通过。1.1.5.Final 版本工作正常。

本章的完整配置示例项目命名为ticket-agency-test-ape

Arquillian Persistence Extension 的手册可在 GitHub 上找到:github.com/arquillian/arquillian-extension-persistence

Arquillian Warp

我们在这里将要讨论的最后一个 Arquillian 扩展是Warp。作者表示,它允许您编写断言服务器端逻辑的客户端测试。更具体地说,它允许执行客户端请求,然后执行服务器端测试。这填补了客户端和服务器端测试之间的空白。

为了全面理解 Warp,我们必须引入@RunAsClient注解。它可以放置在测试类或测试方法上,并指出测试将在客户端执行,而不是在服务器端执行。第二个重要的注解是@Deployment,您已经在创建存档的方法中遇到过它。然而,它可以接受一些布尔值可测试的参数。如果可测试的值为false,它也会在客户端执行,而不是重新打包部署等等。然而,Warp 混合了这两种模式,并需要@Deployment(testable=true)@RunAsClient注解。测试类必须额外注解为@WarpTest

@RunWith(Arquillian.class)
@WarpTest
@RunAsClient
public class BasicWarpTest {

    @Deployment(testable = true)
    public static WebArchive createDeployment() {
       ...
    }

    @Test
    public void test() {
        // Warp test
    }
}

每个 Warp 测试都使用以下结构:

Warp
    .initiate(Activity)
    .inspect(Inspection);

活动是客户端部分,它发起请求。检查是服务器端断言。也可以通过观察者的额外指定来过滤一些请求:

Warp
    .initiate(Activity)
    .observer(Observer)
    .inspect(Inspection);

例如,观察者可以过滤 HTTP 地址。

让我们看看一些更具体的内容。Arquillian Warp 也有一些扩展。目前,所有这些扩展都是面向 HTTP 的;然而,可以将 Warp 扩展以覆盖非 HTTP 用例。这些扩展为测试添加了一些特殊类:

  • JSF

  • JAX-RS

  • Spring MVC

现在我们将查看 JAX-RS 部分。为此,我们将使用来自第七章,将 Web 服务添加到您的应用程序的代码。我们想要测试我们的 REST 服务。首先,我们需要添加所有标准 Arquillian 相关依赖项和arquillian.xml文件。对于 Warp 本身,我们需要以下依赖项:

<dependency>
    <groupId>org.jboss.arquillian.extension</groupId>
    <artifactId>arquillian-warp</artifactId>
    <version>1.0.0.Alpha7</version>
    <type>pom</type>
    <scope>test</scope>
</dependency>

对于 JAX-RS 扩展,我们需要以下依赖项:

<dependency>
    <groupId>org.jboss.arquillian.extension</groupId>
    <artifactId>arquillian-rest-warp-impl-jaxrs-2.0</artifactId>
    <version>1.0.0.Alpha2</version>
    <scope>test</scope>
</dependency>

此外,我们将使用 JAX-RS 客户端:

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-client</artifactId>
    <version>3.0.9.Final</version>
    <scope>test</scope>
</dependency>

我们的测试将如下所示:

@RunWith(Arquillian.class)
@WarpTest
@RunAsClient
public class SeatsResourceTest {

    @Deployment(testable = true)
    public static WebArchive deployment() {
        return ShrinkWrap.create(MavenImporter.class)
                         .loadPomFromFile("pom.xml")
                         .importBuildOutput()
                         .as(WebArchive.class);
    }

    @ArquillianResource
 private URL contextPath;            // [1]

    private ResteasyWebTarget target;

    @Before
    public void setUp() {
        final ResteasyClient client = 
                   new ResteasyClientBuilder().build();
        this.target = client.target(contextPath + "rest/seat");
    }

    @Test
    public void testasd() {
        Warp.initiate(new Activity() {
            @Override
            public void perform() {
                final String response = target
                 .request(MediaType.APPLICATION_JSON_TYPE)
 .get(String.class);  // [2]
 assertNotNull(response);              // [3]
            }
        }).inspect(new Inspection() {

            private static final long serialVersionUID = 1L;

            @ArquillianResource
            private RestContext restContext;

            @AfterServlet
            public void testGetSeats() {
                assertEquals(200, restContext.getHttpResponse().getStatusCode());
                assertEquals(MediaType.APPLICATION_JSON, restContext.getHttpResponse().getContentType());
 assertNotNull(restContext.getHttpResponse().getEntity());  // [4]
            }
        });
    }
}

首先,你可以看到之前提到的所有注解。在这里,我们使用ShrinkWrap Resolver MavenImporter类来获取整个部署项目。[1]对象是应用程序 URL 的注入。在[2]中,我们执行一个客户端请求以获取座位,在[3]中,我们进行一些基本的客户端断言。在[4]中,我们测试服务器端,以检查是否返回了适当的 HTTP 代码等。在更复杂的场景中,我们可以执行一些 bean 逻辑以确认服务器端执行了适当的状态更改。这一点将 Arquillian Warp 与客户端模式(@RunAsClient注解)运行测试以及使用ResteasyWebTarget进行断言区分开来。

关于这个扩展的更多信息可以在github.com/arquillian/arquillian-extension-warp找到。

WebSocket 测试

我们在前面章节中介绍了 WebSocket 的话题。现在让我们看看如何测试它们。要在纯 Java 中完成这个任务,我们需要一个 WebSocket 客户端实现;请确保将Tyrus添加到您的pom.xml文件中:

<dependency>
    <groupId>org.glassfish.tyrus.bundles</groupId>
    <artifactId>tyrus-standalone-client</artifactId>
    <scope>test</scope>
    <version>1.8.3</version>
</dependency>

在这个例子中,我们将使用来自第八章,添加 WebSocket的 Tyrus 作为基础代码。我们的测试实现了一个简单场景。使用 REST API,我们预订一个座位,作为 WebSocket 客户端,我们等待广播有关新预订的信息的消息。让我们看看代码:

@RunAsClient
@RunWith(Arquillian.class)
public class TicketServiceTest {

    private static final String WEBSOCKET_URL = "ws://localhost:8080/ticket-agency-test-websockets/tickets";
    private static final String SEAT_RESOURCE_URL = "http://localhost:8080/ticket-agency-test-websockets/rest/seat";

    @Deployment
 public static Archive<?> createTestArchive() { // [1]
        return ShrinkWrap.create(MavenImporter.class).loadPomFromFile("pom.xml").importBuildOutput()
            .as(WebArchive.class);
    }

    @Test
    public void shouldReceiveMessageOnBooking() throws Exception {
        // given
        final int seatNumber = 4;
 final Deque<JsonObject> messages = new ConcurrentLinkedDeque<>(); // [2]
 final CountDownLatch messageLatch =new CountDownLatch(1); // [3]
 final MessageHandler.Whole<String> handler = // [4]
          new MessageHandler.Whole<String>() {
            @Override
 public void onMessage(String message) {
                messages.add(Json
                 .createReader(new StringReader(message))
                 .readObject());
                messageLatch.countDown();
            }
        };

 ContainerProvider.getWebSocketContainer()  // [5]
                         .connectToServer(new Endpoint() {
            @Override
 public void onOpen(Session session, 
 EndpointConfig endpointConfig) {
                session.addMessageHandler(handler);
            }
        }, new URI(WEBSOCKET_URL));

        // when
 RestAssured.when()
 .post(SEAT_RESOURCE_URL + "/" + seatNumber)
 .then().statusCode(200); // [6]
 messageLatch.await(10, TimeUnit.SECONDS); // [7]

 // then [8]
        assertThat(messages.size(), equalTo(1));
        final JsonObject message = messages.poll();
        assertThat(message.getInt("id"), equalTo(seatNumber));
    }
}

测试按照本章描述的客户端模式运行,并使用 Tyrus:底层的 WebSocket 客户端参考实现。对于这个测试来说,完美的部署是我们的整个应用程序,因此我们将使用MavenImporter [1]。在测试中,我们声明了一个并发双端队列来收集接收到的消息[2]和一个门闩[3],我们将使用它们在[7]中等待。为了处理客户端的 WebSocket,我们必须声明一个处理器[4],它指定了接收消息时的行为。在这里,我们只是将消息添加到我们的双端队列中,并执行门闩倒计时。在[5]中,我们必须注册处理器,以便它将在一个打开的会话中使用。REST 调用使用 rest-assured 库执行,它提供了一个流畅的 API 来测试 REST API。最后,在[8]中,我们执行一些关于接收到的消息的基本断言。

完全配置的pom.xml文件和整个工作项目可以在ticket-agency-test-websockets下找到。

提高你的 Arquillian 测试

你可能已经注意到,我们故意只创建了所需的集成测试的一部分。我们没有达到最后一英里,也就是说,创建座位并预定一个。实际上,如果你记得的话,我们的票务应用程序使用ConversationScope来跟踪用户的导航。因此,我们需要将ConversationScope绑定到我们的测试中。

幸运的是,Weld容器通过org.jboss.weld.context.bound.BoundConversationContext提供了你所需要的一切,它需要注入到你的测试类中:

@Inject BoundConversationContext conversationContext;

@Before
public void init() {
      conversationContext.associate(
      new MutableBoundRequest(new HashMap<String, Object>(),
                              new HashMap<String, Object>()));
      conversationContext.activate();
}

注意

注意,@Before注解在每个测试方法之前和注入发生之后被调用。在我们的情况下,它用于在conversationContext.activate激活之前将conversationContextMutableBoundRequest关联。这是在 Arquillian 测试环境中模拟会话行为所必需的。

为了完整性,你必须知道BoundRequest接口是在 Weld API 中定义的,用于保持跨越多个请求的会话,但比会话短。

这里是完整的TicketTest类,它包含在testTicketAgency方法中的剧院创建和座位预订:

@RunWith(Arquillian.class)
public class TicketTest {

    @Inject BoundConversationContext conversationContext;

    @Before
    public void init() {
        conversationContext.associate(
        new MutableBoundRequest(new HashMap<String, Object>(),
            new HashMap<String, Object>()));
        conversationContext.activate();
    }

    @Deployment
    public static Archive<?> createTestArchive() {
        return ShrinkWrap.create(WebArchive.class, "ticket.war")
           .addPackage(SeatProducer.class.getPackage())
           .addPackage(Seat.class.getPackage())
           .addPackage(TicketService.class.getPackage())
           .addPackage(DataManager.class.getPackage())
           .addAsResource("META-INF/persistence.xml")
           .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
    }

    @Inject
    TicketService ticketService;

    @Inject
    BookerService bookerService;

    @Inject
    Logger log;

    @Test
    public void testTicketAgency () throws Exception {

        SeatType seatType = new SeatType();
        seatType.setDescription("Balcony");
        seatType.setPrice(20);
        seatType.setQuantity(5);

        ticketService.createSeatType(seatType);
        log.info("Created Seat Type");
        assertNotNull(seatType.getId());

        List<SeatType> listSeats = new ArrayList();
        listSeats.add(seatType);
        ticketService.createTheatre(listSeats);

        log.info("Created Theatre");
        log.info(seatType.getDescription() + " was persisted with id " + seatType.getId());

        bookerService.bookSeat(new Long(seatType.getId()), seatType.getPrice());
        log.info("Created Theatre");
        log.info("Money left: " +bookerService.getMoney());
        assertTrue(bookerService.getMoney() <100); 
    }
}

其他信息

Arquillian 项目是一个不断发展的框架,有许多其他有趣的主题。然而,描述所有其扩展超出了本书的范围。然而,其他值得关注的领域是DroneGraphene,它们将WebDriverPage Object模式引入 Arquillian 测试。

在某个时候,你可能会发现自己在每个测试用例中创建单独的部署方法。你可以通过使用Arquillian Suite Extension来改变这种行为,它允许为一系列测试用例指定部署。

Arquillian 是完全开源的,因此您可以从arquillian.org/提供的在线文档中了解更多关于它的信息。当您需要帮助或有关于新特性的绝佳想法时,您可以在论坛或 IRC 上联系 Arquillian 社区(arquillian.org/community/)。记住,如果您发现了一个 bug,不要抱怨;只需在 JBoss JIRA 上提交一个问题issues.jboss.org

Arquillian 的贡献者之一 John D. Ament 已经出版了一本关于这个主题的书,名为《Arquillian 测试指南》,由 Packt Publishing 出版。

摘要

在本章中,我们探讨了企业系统的一个关键部分:集成测试。从历史上看,Java EE 的一个主要缺点是其可测试性,但 Arquillian 确实在很大程度上解决了这个问题。

作为 JUnit 框架的扩展,Arquillian 在检查暴露在企业 Java 应用程序中的业务逻辑的集成层方面表现出色。

Arquillian 挂钩到您的测试框架中,以管理容器的生命周期。它还将test类捆绑到一个可部署的归档中,其中包含依赖的类和资源。

这是最后一章,涵盖了基本的 Java EE 和 WildFly 特性。我们从一个会话 Bean 开始,最终到达了 web sockets、异步消息系统、RESTful API,甚至还有一点 JavaScript。在本书的整个过程中,我们看到了 Java EE 的最新版本为我们提供了创建现代和可扩展应用程序的工具。该平台的目标是帮助开发者专注于业务逻辑。这意味着通过整个应用程序堆栈从后端到视图层移除样板代码。在大多数领域,我们只涵盖了 Java EE 提供的多种技术的最重要的特性。还有很多东西可以探索!

在附录中,我们将了解一些关于 JBoss Forge 工具的知识,该工具可以极大地提高使用 Java EE 进行工作时的工作效率。

附录 A. 使用 JBoss Forge 快速开发

在本书的附录中,我们将为您概述 JBoss Forge,这是一个强大的、快速的应用程序开发(针对 Java EE)和项目理解工具。使用 Forge,您可以通过几个命令从头开始启动一个新项目并生成您应用程序的骨架。然而,它也可以通过额外的插件对现有项目进行增量增强。

安装 Forge

为了安装 Forge,您需要执行以下步骤:

  1. forge.jboss.org/下载并解压 Forge 到您的硬盘上的一个文件夹中;这个文件夹将是您的FORGE_HOME

  2. FORGE_HOME/bin添加到您的路径中(Windows、Linux 和 Mac OS X)。

在基于 Unix 的操作系统上,这通常意味着编辑您的~/.bashrc~/.profile;您需要输入以下代码片段:

export FORGE_HOME=~/forge/
export PATH=$PATH:$FORGE_HOME/bin

在 Windows 系统中,您需要打开控制面板窗口,然后导航到系统属性 | 高级 | 环境变量,并可视地添加这两个条目。建议为 Forge 设置用户变量,除非您已将未压缩的发行版放置在所有用户都可以访问的文件夹中。

如果出现任何问题,请查看可在线获取的安装指南,链接为 forge.jboss.org/document/installation

启动 Forge

为了启动 Forge,有一个名为 forge.bat(或等效的 Unix Forge)的脚本。运行以下脚本:

forge.bat

这将启动 Forge 控制台,如下面的截图所示:

启动 Forge

控制台接受大量命令,例如用于导航和操作文件系统、创建新项目、操作 Forge 环境和 UI 生成以及脚手架命令的命令。它还提供了自动完成功能。

为了学习当前上下文中可用的以下命令,请按 Tab 键两次:

[bin]$
alias                                   echo
unalias                                 edit
export                                  exit
about                                   git-clone
addon-build-and-install                 grep
addon-install                           less
addon-install-from-git                  ls
addon-list                              man
addon-remove                            mkdir
archetype-add                           more
archetype-list                          open
archetype-remove                        pl-cmil-forge-ecore-ui
cat                                     project-new
cd                                      pwd
clear                                   rm
command-list                            run
config-clear                            system-property-get
config-list                             system-property-set
config-set                              touch
connection-create-profile               track-changes
connection-remove-profile               transaction-start
cp                                      version
date                                    wait

注意

除了标准命令外,您还可以通过附加组件来丰富 Forge 命令行的语法,这为您的项目创建增加了高级功能。您可以在 forge.jboss.org/addons 上找到可用的插件列表。例如,我们将使用 angular-js 插件来为我们的应用程序创建一个图形用户界面。

在下一节中,我们将演示如何使用一些可用的命令来创建一个 Java EE 7 应用程序。

使用 JBoss Forge 创建您的第一个 Java EE 7 应用程序

因此,Forge 安装相当简单;然而,创建您的第一个 Java EE 7 应用程序将会更快!尽管我们可以使用 Forge 创建相当高级的应用程序,但为了学习的目的,我们只会使用一个包含用户表的简单模式,该模式可以使用以下命令构建:

CREATE TABLE users (
  id serial PRIMARY KEY,
  name varchar(50),
  surname varchar(50),
  email varchar(50)
);

我们需要做的第一件事是使用 project-new 命令创建一个新的项目。请在 Forge shell 中执行以下命令:

[bin]$ project-new --named forge-demo --topLevelPackage com.packtpub.wflydevelopment.appendix –projectFolder forge-demo

现在,您有一个新的 Forge 项目,它基于 Maven 项目结构。可以说,生成新项目并不是 Forge 的最大价值所在——这同样可以通过 Maven 架构师实现。Forge 的美妙之处在于,现在您可以在项目生成后交互式地定义自己的应用程序骨架。这意味着您可以使用 Maven 架构师首先创建项目,然后使用 Forge 的直观建议进行扩展。

当项目创建完成后,您可以从 shell 中进入命令列表,如下面的截图所示,其中列出了您可以在 Forge 2.12.1 Final 中使用的所有基本命令:

使用 JBoss Forge 创建您的第一个 Java EE 7 应用程序

如果您想了解更多关于单个命令的信息,可以使用 man 后跟命令名称,如下面的屏幕截图所示:

使用 JBoss Forge 创建您的第一个 Java EE 7 应用程序

当您学会了如何使用 Forge 获取帮助后,让我们回到我们的应用程序。

在第一步,我们需要指定我们想要使用的 Java 和 Java EE 版本:

[forge-demo]$ project-set-compiler-version --sourceVersion 1.8 --targetVersion 1.8
[forge-demo]$ javaee-setup --javaEEVersion 7
***SUCCESS*** JavaEE 7 has been installed.

到目前为止,我们的项目已经包含了 Java EE 7 API 依赖项。现在,由于我们需要将数据库表反向工程到 Java 实体中,下一步将是为您的应用程序配置 Java 持久性 APIJPA)层。此应用程序将基于基于 Hibernate 提供程序的 WildFly JPA 实现,引用名为 Forge 的数据库。此数据库可通过名为 java:jboss/datasources/PostgreSqlDSJava 命名和目录接口JNDI)访问。如下所示:

[forge-demo]$ jpa-setup --jpaVersion 2.1 --provider HIBERNATE --container WILDFLY --dbType POSTGRES  --dataSourceName java:jboss/datasources/PostgreSqlDS

persistence.xml 文件已生成,当前控制台指示我们正在编辑它。我们可以使用 cat 命令来检查其内容:

[persistence.xml]$ cat .
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<persistence  xmlns:xsi="http://w
ww.w3.org/2001/XMLSchema-instance" version="2.1" xsi:schemaLocation="http://xmln
s.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence
_2_1.xsd">
 <persistence-unit name="forge-demo-persistence-unit" transaction-type="JTA">
 <description>Forge Persistence Unit</description>
 <provider>org.hibernate.ejb.HibernatePersistence</provider>
 <jta-data-source>java:jboss/datasources/PostgreSqlDS</jta-data-source>
 <exclude-unlisted-classes>false</exclude-unlisted-classes>
 <properties>
 <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
 <property name="hibernate.show_sql" value="true"/>
 <property name="hibernate.format_sql" value="true"/>
 <property name="hibernate.transaction.flush_before_completion" value="true
"/>
 <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL
Dialect"/>
 </properties>
 </persistence-unit>
</persistence>

接下来,我们将使用 jpa-generate-entities-from-tables 命令来生成您的 Entity 类。您需要提供以下 Java 数据库连接JDBC)连接信息:

  • JDBC URL

  • 用户名和密码

  • SQL 方言

  • JDBC 驱动程序类名

  • JDBC 驱动程序在文件系统中的路径

  • 将生成实体的包

您可以在一行命令中指定所有参数,或者通过交互式完成。最终的命令将看起来像这样(添加换行以提高可读性):

[persistence.xml]$ jpa-generate-entities-from-tables  \
--jdbcUrl jdbc:postgresql://localhost:5432/forge \
--hibernateDialect org.hibernate.dialect.PostgreSQLDialect \
--userName jboss \
--userPassword jboss \
--driverLocation c:\\forge\\postgresql-9.3-1101.jdbc41.jar \
--driverClass org.postgresql.Driver \
--databaseTables users 

在完成持久层之后,我们现在将使用 scaffold 命令创建 GUI 应用程序,该命令可以与多个提供程序相关联,例如 AngularJS。首先,让我们使用以下 shell 命令安装插件(请注意,它应该在您的系统 shell 中执行,而不是在 Forge CLI 中):

forge --install org.jboss.forge.addon:angularjs

安装完成后,我们需要发出三个更多命令。首先,我们将准备脚手架框架:

[forge-demo]$ scaffold-setup --provider AngularJS

我们的应用程序现在是一个带有 AngularJS 库的 Web 应用程序。接下来,我们将定义我们想要为 Users 实体生成 UI:

[forge-demo]$ scaffold-generate --provider AngularJS --targets com.packtpub.wflydevelopment.appendix.model.Users

最后,我们创建一个 JAX-RS 端点:

[forge-demo]$ rest-generate-endpoints-from-entities --targets com.packtpub.wflydevelopment.appendix.model.Users

完成了!应用程序现在是一个完整的 Java EE 应用程序,具有 REST 端点、JPA 和 AngularJS UI。

注意

当这些示例被编写时,JBoss Forge 并未完全支持所有 Java EE 7 依赖项。这可以通过手动修改生成的项目的 pom.xml 文件来修复。您只需删除以下代码片段中显示的所有依赖项之外的所有依赖项:

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>7.0</version>
    <scope>provided</scope>
</dependency>

此外,如果您的 Users 实体在其 ID 字段中没有 @javax.persistence.GeneratedValue 注解,请确保您手动添加它(JPA 插件中目前存在一个错误):

   @Id
   @Column(name = "id", unique = true, nullable = false)
   @GeneratedValue(strategy=GenerationType.IDENTITY)
   public int getId() {
      return this.id;
   }

构建和部署应用程序

现在,是时候使用build命令构建您的应用程序了,这个命令将会编译并打包您的应用程序为一个网络应用程序归档(forge-demo.war):

[forge-demo]$ build
***SUCCESS*** Build Success 

Maven 构建命令在您的项目target文件夹中创建了一个名为forge-demo-1.0.0-SNAPSHOT.war的归档。现在,您可以选择手动将归档复制到应用程序服务器的deployments文件夹,或者使用management接口。

记住服务器应该定义了java:jboss/datasources/PostgreSqlDS数据源!

您的 Forge-demo 应用程序运行中的截图

您可以在默认 URL http://localhost:8080/ forge-demo-1.0.0-SNAPSHOT/ 访问您的应用程序。

主要应用程序屏幕将包含左侧菜单中的实体列表。如果您选择用户选项,那么您应该看到已添加的用户列表,一个搜索按钮,可以用来过滤用户,以及一个创建按钮,显然它将插入一些数据。这将在以下屏幕截图中显示:

您的 Forge-demo 应用程序运行中的截图

点击创建按钮,您将被带到允许向数据库插入新用户的屏幕(记住我们已经配置了这个应用程序以针对PostgreSQL数据库运行)。这将在以下屏幕截图中显示:

您的 Forge-demo 应用程序运行中的截图

这样,我们就创建了一个基于 Java EE 的基本 AngularJS 应用程序。它可以作为您项目的基石,或者只是一个沙盒,您可以在其中尝试新想法。请确保查看其他可用的插件,并记住您始终有创建自己的插件的可能性!

posted @ 2025-09-10 15:08  绝不原创的飞龙  阅读(103)  评论(0)    收藏  举报