Java-和-Quarkus-云原生应用实用指南-全-
Java 和 Quarkus 云原生应用实用指南(全)
原文:
zh.annas-archive.org/md5/1162524e617fbea9bbf254c1fa1d09f0译者:飞龙
前言
尽管 Java 已经存在多年,但它仍然是开发者中最受欢迎的选择之一,经历了近 20 年的持续改进,并为企业系统开发了一套完整的库。在我们生活在一个快节奏的行业中,我们不能否认,在容器、微服务、反应式应用程序和云平台引入的过去几年里,许多事情都发生了变化。为了在这个不断变化的世界中保持一流的地位,Java 需要一次提升。而我们认为,这次提升可以是 Quarkus!
Quarkus 是从头开始设计的,旨在成为一个 Kubernetes 原生 Java 框架,非常适合创建具有最小内存占用和快速执行速度的微服务应用程序。同时,Quarkus 并没有丢弃那些大多数开发者熟悉的丰富库集,例如 Hibernate 和 REST 服务。相反,它们是更大图景的一部分,包括颠覆性的 MicroProfile API、反应式编程模型如 Vert.x 以及许多其他可以轻松集成到您的服务中的功能。
由于 Quarkus 是针对这些考虑而设计的,因此它为在无服务器、微服务、容器、Kubernetes、函数即服务(FaaS)和云中运行 Java 提供了一个有效的解决方案。它的以容器优先的方法将命令式和反应式编程范式统一于微服务开发中,并提供了一组可扩展的基于标准的 企业 Java 库和框架,结合了极端的开发者生产力,承诺将彻底改变我们用 Java 开发的方式。
本书面向对象
这本书是为对学习构建可靠和健壮应用程序的非常有前途的微服务架构感兴趣的 Java 开发者和软件架构师而编写的。假设读者对 Java、Spring 和 REST API 有一定的了解。
本书涵盖内容
第一章,Quarkus 核心概念简介,解释了以容器优先(最小内存占用的 Java 应用程序在容器中运行是最优的)、云原生(Quarkus 接受了 Kubernetes 等环境中的 12 因素架构)和微服务优先(Quarkus 为 Java 应用程序带来了闪电般的启动时间和代码周转时间)的方法。我们将检查可用于开发 Quarkus 应用程序的各种工具。为此,我们将安装 IDE 和 GraalVM,这是原生应用程序所必需的。
第二章,使用 Quarkus 开发您的第一个应用程序,将带您通过使用 Quarkus 构建您的第一个应用程序。我们将看到如何使用 Maven/Gradle 插件来引导应用程序。您将学习如何将应用程序导入您的 IDE,以及如何运行和测试应用程序。接下来,我们将讨论如何从您的 Java 项目中创建原生应用程序。
第三章,创建应用程序容器镜像,探讨了如何构建应用程序的 Docker 镜像,如何在 Docker 上运行应用程序,如何安装 OpenShift 的单节点集群,以及如何在 Minishift 上运行应用程序。
第四章,Web 应用程序开发,探讨了将使用 REST 和 CDI 堆栈以及 Web 前端的一个客户商店应用程序的使用案例。我们将看到如何部署应用程序,并查看更改而不需要重启 Quarkus。
第五章,使用 Quarkus 管理数据持久性,讨论了 Quarkus 的数据持久性。我们将看到如何将持久性添加到客户商店示例中,以及如何设置数据库(PostgreSQL)以运行示例。然后,我们将把应用程序带到云端。最后,我们将向您展示 Hibernate Panache 如何简化应用程序开发。
第六章,使用 MicroProfile API 构建应用程序,教您如何将我们已讨论的企业 API 与 Eclipse MicroProfile 的完整规范栈相补充(microprofile.io/)。
第七章,保护应用程序安全,将探讨如何使用内置的安全 API(如 Elytron 安全堆栈、Keycloak 扩展和 MicroProfile JWT 扩展)保护我们的示例应用程序。
第八章,高级应用程序开发,探讨了高级应用程序开发技术,如高级应用程序配置管理、生命周期事件和触发计划任务。
第九章,统一命令式和响应式,通过 Quarkus 和 Vert.x 编程模型的一个示例应用程序,带您了解非阻塞编程模型。我们还将探讨如何利用 Vert.x 的响应式 SQL API 构建非阻塞数据库应用程序。
第十章,使用 Quarkus 进行响应式消息传递,解释了如何使用 CDI 开发模型和 Kafka 以及 AMQP 作为代理来实现响应式数据流应用程序。我们还将解释如何通过在 OpenShift 上部署我们的应用程序来实现云中的完整流式架构。
为了充分利用这本书
《使用 Java 和 Quarkus 进行实战云原生应用开发》是一本完整的端到端开发指南,它将为您提供在无服务器环境中构建 Kubernetes 原生应用的实战经验。为了最大限度地利用本书,我们建议您使用集成了 Apache Maven(如 IntelliJ IDEA 或 Eclipse)的开发环境,并将其导入我们的示例代码文件中。这将帮助您逐步跟踪我们的项目,并在需要时进行调试。
下载示例代码文件
您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择支持选项卡。
-
点击代码下载。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载完成后,请确保使用最新版本的以下软件解压或提取文件夹:
-
Windows 版的 WinRAR/7-Zip
-
Mac 版的 Zipeg/iZip/UnRarX
-
Linux 版的 7-Zip/PeaZip
本书代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还提供来自我们丰富的书籍和视频目录中的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781838821470_ColorImages.pdf。
代码实战
请访问以下链接查看代码实战视频:bit.ly/2LKFbY1
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“我们的项目中提供了一个index.html页面作为标记,如图所示的项目层次结构。”
代码块设置如下:
// Create new JSON for Order #1
objOrder = Json.createObjectBuilder()
.add("id", new Long(1))
.add("item", "mountain bike")
.add("price", new Long(100))
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
@POST
@RolesAllowed("admin")
public Response create(Customer customer)
任何命令行输入或输出都应如下编写:
$ tree src
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“一旦您有一些数据,其他操作(如编辑和删除)将可用。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并将邮件发送至customercare@packtpub.com.
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并附上材料的链接。
如果您想成为一名作者:如果您在某个领域有专业知识,并且对撰写或参与一本书籍感兴趣,请访问authors.packtpub.com.
评论
请留下评论。一旦您阅读并使用过这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问packt.com.
第一部分:Quarkus 入门
在本节中,我们将了解 Quarkus 架构的基础,安装我们开发应用程序所需的工具,并开发我们的第一个云原生应用程序。
本节包括以下章节:
-
第一章, Quarkus 核心概念简介
-
第二章, 使用 Quarkus 开发您的第一个应用程序
-
第三章, 创建您的应用程序的容器镜像
第一章:Quarkus 核心概念简介
Java 早在 20 多年前就被引入开源社区。从那时起,我们无法想到有任何一家大型 IT 公司或组织不使用 Java。因此,Java 常常被视为一种企业级语言,这本身并不是坏事:Java 是企业标准,它是一个极其成熟的语言,周围有一个庞大的工具和库生态系统,并且仍然是全球开发者使用最多的语言。
然而,在 IT 行业 20 年是一个相当长的时间。从开始,Java 就经历了一系列优化,同时承担着与早期版本保持向后兼容性的负担。然而,如今,随着云、容器、微服务和响应式编程等新标准的兴起,IT 格局已经发生了显著变化。我们是否还需要使用 Java 来应对最新的应用程序架构,并达到更高的生产力和效率水平?是的!本书承诺在向您介绍Quarkus的同时做到这一点,Quarkus 是一个 Kubernetes 原生框架,它将把超音速、亚原子级的 Java 提升到新的高度!
在本书的第一部分,我们将学习如何使用简单的工具创建 Quarkus 应用程序,同时使用开发环境进行编码、执行和调试。完成所有绿色条目后,我们将专注于高级主题,向您展示如何结合多个 Quarkus 扩展来构建无服务器基础设施。
就本章而言,我们将通过涵盖以下主题来快速浏览 Quarkus 技术:
-
IT 格局概述,展示云原生应用程序和微服务的优势
-
Quarkus 架构的基本原理
-
安装所需的软件(GraalVM 用于本地编译代码和开发环境)
技术要求
你可以在 GitHub 上找到该项目的源代码,请参阅本章中的github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter01。
从大数据炒作到 Kubernetes
大约 10 年前,IT 行业最大的炒作是“大数据”这个术语。每个主要企业都在竞相利用庞大的、但据说可管理的、神秘的数据孤岛的力量。有了大数据,没有任何问题是不可以克服的,所有的预测都会实现。
但最近,这些预测似乎已经消退,IT 行业最不为人知的秘密是大数据已经死亡——至少是我们所知道的大数据。这并不意味着数据的量或增长已经崩溃——或者相反。这只是底层技术发生了变化,这意味着使用大数据的应用程序架构也发生了变化。
以 Hadoop 为例,它一直是大数据炒作的标志。它基于一系列假设设计,这些假设在短时间内发生了巨大变化。其中之一是,为了处理大量数据,网络延迟是邪恶的,云原生存储根本不是一种选择。当时,大多数 IT 行业数据都在本地,因此重点是避免移动大量信息。这意味着数据需要集中存储以便高效计算。
今天,这种场景已经发生了很大变化:大多数应用程序仍然使用大量数据,但现在数据是实时处理的。也就是说,我们现在流式传输数据而不是多次处理整个数据集。
此外,网络延迟障碍对云服务提供商来说已经不再是问题,甚至有多个云源可供选择。此外,公司现在可以选择在自己的本地部署自己的私有云,从而产生新的场景,如混合云。
因此,重点是真正发生了什么变化:今天,大数据不仅仅意味着大量数据集的大量,而是大量数据的灵活存储选项。
这就是容器和,特别是 Kubernetes 的适用之处。简而言之,你可以把容器想象成一个打包的应用程序,它只包含运行它所需的库,而 Kubernetes 就像是一个编排系统,确保所有容器都有适当的资源,同时管理它们的生命周期。
Kubernetes 使用Docker运行镜像并管理容器。然而,Kubernetes 也可以使用其他引擎(例如,rkt)。由于我们将在 Kubernetes 之上构建我们的应用程序,因此我们将在下一节中简要概述其架构。
Kubernetes 架构概述
Kubernetes 的架构侧重于服务发现的概念,这是一种松散耦合且灵活的机制。像大多数其他分布式中间件平台一样,一个 Kubernetes 集群由一个或多个主节点和多个计算节点组成。以下图表展示了 Kubernetes 集群的高级视图:

Kubernetes 主节点本质上构成了集群的大脑。它们负责管理整个集群,暴露 API,以及调度部署。Kubernetes 节点(前一张图的右侧)包含运行在 Pod 组件中的应用所需的服务。
每个主节点包含以下组件:
-
API 服务器:此服务器同步并验证 Pod 和服务的运行信息。
-
etcd:这为集群数据提供一致且高度可用的存储。你可以把
etcd看作是大脑的共享内存。 -
控制器管理器服务器:此服务器检查
etcd服务的更改,并使用其 API 强制执行所需状态。 -
HAProxy:在配置 HA 主节点以在多个主端点之间平衡负载时可以添加此组件。
Kubernetes 节点(简称节点)可以被视为 Kubernetes 集群的驮马。每个节点向您的应用程序提供一组资源(例如计算、网络和存储)。节点还包含用于服务发现、监控、日志记录和可选附加组件的附加组件。在基础设施方面,您可以在云环境中或数据中心运行的裸机服务器上以虚拟机(VM)的形式运行节点。
每个节点包含以下组件:
-
Pod: 这使我们能够逻辑地将容器和我们的应用程序堆栈的各个部分组合在一起。Pod 充当具有共享资源和上下文的此类容器的逻辑边界。Pod 可以通过创建副本集在运行时进行扩展。这反过来又确保了部署始终运行所需数量的 Pod。
-
Kubelet:这是一个在 Kubernetes 集群中的每个节点上运行的代理。它确保容器在 Pod 中运行。
-
Kube-Proxy:这维护节点上的网络规则,以允许 Pod 之间的网络通信。
-
容器运行时:这是负责运行容器的软件。Kubernetes 支持多种容器运行时(如 Docker、
containerd、cri-o和rktlet)。
现在我们已经介绍了 Kubernetes 架构的基本知识,让我们来看看它可以为您的组织带来的主要优势。
使用 Kubernetes 的好处
Kubernetes 可以为您的组织带来的优势如下:
-
Kubernetes 极大地简化了容器管理。如您所知,在使用 Kubernetes 时,无需直接管理容器。相反,您只需管理 Pod。为了使您的应用程序在 Pod 中可用,Kubernetes 引入了一个称为服务的抽象。它定义了一个具有其 IP 地址的逻辑 Pod 集合。这种抽象级别提高了容错性并减少了停机时间,通过在多台机器上启动容器来实现。
-
Kubernetes 通过支持广泛的编程语言(如 Java、Go、Python 等)和提供高级部署功能(如自动化部署和回滚、金丝雀部署等)来加速构建、测试和发布软件的过程。这使得为您的软件配置有效的持续集成/持续交付(CI/CD)管道变得容易得多。
-
Kubernetes 为您的 Pod 提供最快且成本最低的水平扩展,因此当您的应用程序的用户数量增加时,您可以配置复制服务以触发新的 Pod,并在它们之间平衡负载以避免停机。
-
值得注意的是,Kubernetes 能够管理无状态和有状态的应用程序,因为它允许临时存储和持久卷。它还支持多种存储类型,如 NFS、GlusterFS 和云存储系统。此外,持久卷(PV)的生命周期不依赖于任何使用它的 Pod,因此您可以保留数据直到您需要它。
在您的行业中使用 Kubernetes 作为服务编排器的优势显而易见,但下一个问题是,我们如何编写我们的服务以充分利用这种架构?我们是否还可以使用我们在过去几年中学到的相同标准来编写我们的应用程序?下一节将解决这个困境。
从 Java EE 到 MicroProfile
Java 企业版(EE)已经达到了一个卓越的成熟度,并在 IT 企业中有巨大的采用率。一个 Java EE 应用程序通常被打包成一个单体应用程序,并在应用程序服务器中部署,它可以托管多个这样的应用程序。
可以将单体应用程序视为一个自包含的应用程序,它包括运行应用程序所需的用户界面和业务组件。
这种方法已经广泛使用了多年。原因是简单的:单体应用程序在概念上简单易开发打包,因为所有内容都包含在一个包中,并且可以使用单个 IDE 进行编辑。此外,扩展单体应用程序很简单:您只需要扩展单个组件。
因此,传统的企业应用程序编码方式产生了一大批应用程序,这些应用程序应该作为长时间运行的过程提供,并需要一个应用程序服务器来管理它们的高可用性(HA)。反过来,在出现故障的情况下,需要一些其他工具来管理服务器重启并检查系统的整体健康状况。
随着基于服务器的单体应用程序的持续增长,几个缺点变得明显,如下所述:
-
难以维护:这是由于应用程序的大小,这使得为它们创建补丁变得复杂。
-
有限的扩展性:您可以扩展整个应用程序,而不是单个服务。
-
较长的发布周期:对代码所做的任何更改都需要我们部署整个应用程序,这在多个团队在同一应用程序上工作时会使得事情变得复杂。
-
隔离性较差:在应用程序服务器中部署多个应用程序可能导致整个系统因单个应用程序的异常行为而失败。
-
启动速度较慢:完整的单体堆栈的启动时间以慢而闻名,尤其是在多个应用程序同时部署并可能竞争相同资源的情况下。
-
复杂的监控:监控和调整单个单体应用程序的活动更加困难,因为它提供了大量的指标。
-
更复杂的 CI/CD:为多个单体应用配置 CI/CD 管道同样困难。
在这种情况下,一个名为微服务的新范式围绕着简单但并非全新的想法出现。微服务背后的主要主题是,对于某些类型的应用,一旦它们被分割成更小且可组合的部分,它们就更容易构建和维护。在基于服务的架构中,我们不再需要用周或月来衡量我们应用的正常运行时间,因为我们可以在需要时激活我们的服务。因此,时间因素可以少到分钟或秒。
在这种架构中,每个组件都有自己的生命周期,从开发到测试,而最终的应用只是所有这些单个组件的组合。这种方法与将所有内容作为一个单一单元构建和测试的单体应用相比,是一个合理的转变。
将应用构建为一系列较小的模块化组件,这些组件更容易理解,更容易测试/调试,并且在应用生命周期中更容易维护。微服务架构通过减少将改进部署到生产所需的时间,利用了公司的敏捷性。这种方法经过尝试和测试,并且以下原因使其优于其他方法:
-
增加的弹性:微服务架构通过启动另一个组件来提高系统的整体能力,以承受任何类型的意外故障或组件或网络的故障,即使剩余的应用继续运行。
-
开发者独立性:通过并行工作在较小的团队中,你可以加快正在进行的工作,特别是对于由地理和文化上多元化的团队组成的大型企业应用来说。
-
可伸缩性:较小的组件需要的资源更少。这意味着我们可以轻松地将它们扩展以满足特定组件不断增加的需求。
-
CI/CD 生命周期自动化:单个组件可以无缝地融入 CD 管道和具有复杂部署的场景。
-
与业务更简单的映射:由于微服务架构在组织内具有不断增加的独立性和透明度,因此它们更容易与业务领域逻辑进行映射。
要从我们的软件即服务(SaaS)中获得最佳结果,需要一种方法论。在下一节中,我们将讨论由开发者推荐的十二要素应用方法论,该方法论旨在通过关注微服务,实现应用的平稳运行和交付。
十二要素应用方法论
在 2011 年,Heroku 的创始人 Adam Wiggins 发布了十二要素应用方法论,这个方法论很快成为了基于他们自身经验的构建软件即服务(SaaS)的关键参考。这个方法论不局限于任何编程语言,与微服务架构兼容,并基于容器和 CI/CD 管道。让我们来看看这 12 个因素:
-
代码库: 你应该在单个代码库的基础上构建你的应用程序,该代码库由版本控制系统(VCS)跟踪。你应该依赖一个基础仓库来简化单个应用程序的持续集成/持续部署(CI/CD)流程。因此,部署应该是自动的,这样一切都可以在不同的环境中运行,而不需要做任何事情。
-
依赖项: 不要将任何依赖项推送到你的项目代码库。相反,使用包管理器,这样你将能够在你的环境中同步所有依赖项,以确保你可以重现相同的行为。
-
配置: 将你的配置存储在环境变量中。配置应该与代码分离,以便配置根据应用程序部署的位置而变化。
-
支持服务: 服务应该易于互换,这样你就可以将支持服务作为附加资源来管理。你必须能够轻松地从一家提供商交换到另一家提供商的支持服务,而无需更改你的代码。这最大化了可移植性,并有助于维护你的系统。
-
构建、运行、发布: 构建、发布和运行阶段之间应该有明确和严格的分离。你可以通过分配唯一的发布 ID 并允许发布回滚来实现这一点。这些阶段之间的自动化应该尽可能简单。
-
无状态进程: 这个因素是微服务架构的核心。你不应该在你的服务中引入状态。任何需要持久化的数据都必须存储在支持服务中,通常是数据库或其他存储。
-
端口绑定: 通过这个因素,你的应用程序应该是完全自包含的。它不应该依赖于执行环境中的网络服务器的启动来创建前端服务。Web 应用应该通过将服务绑定到端口,将 HTTP 应用作为服务提供。
-
并发性: 你应该将你的应用程序分解成更小的部分。更小、定义良好的应用程序允许你根据需要扩展以处理不同的负载。你应该能够单独扩展单个组件。
-
可丢弃性: 你应该通过编写快速启动和优雅关闭的应用程序来最大化你系统的鲁棒性。这意味着你应该能够处理意外的故障。一个推荐的方法是使用健壮的异步后端,当发生故障时返回通知。
-
开发/生产一致性:你应该努力保持开发、测试和生产阶段相似和一致,以限制偏差和错误。这也隐含地鼓励了一种 DevOps 文化,其中软件开发和运维是统一的。
-
日志记录:日志记录是调试和监控应用程序整体健康状况的关键因素。日志存储的位置不应成为开发者的担忧。相反,这些日志应被视为一个连续的流,由一个服务分别捕获和存储。
-
管理流程:在许多情况下,开发者执行一次性管理或维护任务,例如数据库迁移、应用程序修补或为应用程序执行一次性的脚本。在类似于应用程序常规长期运行环境的环境中运行一次性管理流程是至关重要的。
虽然一些先前的模式一开始看起来可能微不足道,但随着服务开始增长,它们成为基本构建块变得至关重要。因此,在设计微服务应用程序时,请记住,大多数挑战不仅与编码相关,而且与基本错误有关。事实上,即使优秀的团队在没有拥抱 DevOps 文化和关键构建块(如 Twelve-Factor App 方法)的情况下,也会在微服务方面失败。
MicroProfile 倡议
在讨论了微服务的方法论之后,我们现在将涵盖一些与特定 API 相关的方面,这些 API 可以用来开发微服务。
虽然乍一看,Java 和微服务似乎并不真正匹配,但放弃整个 Java EE 生态系统(更名为Jakarta EE并转移到 Eclipse 基金会)的想法是错误的。已经投入了大量努力来重用 Java EE 进行微服务编码。
实际上,包括 IBM、Red Hat 和 Payara 在内的许多主要厂商已经提供了一种轻量级且可扩展的运行时环境来支持微服务和云部署。他们的个人努力自然地被MicroProfile.io倡议下的开放协作所跟随。
MicroProfile 组件建立在 Java EE 模型之上,因此使过渡到微服务开发变得自然。这意味着你将能够重用多年来积累的 Java EE 宝贵知识,灵活地使用多个供应商规范来定义应用程序需求。
在其初始版本中,MicroProfile 倡议仅包含 Java EE API 的小部分裁剪(JAX-RS 2.0、CDI 1.2 和 JSON-P 1.0)。
然而,在很短的时间内,新的 MicroProfile 项目已经被添加。仅 2018 年,我们就看到了 MicroProfile 1.3、1.4、2.0 和 2.1 的问世,以及它们包含的项目。MicroProfile 倡议的当前版本通过添加不属于 Java EE 的功能(如配置、弹性、监控、健康检查和分布式跟踪)来扩展标准。
以下图表展示了根据最新规范(本书编写时)的 MicroProfile 项目的构建块:

然而,仅凭 MicroProfile 本身在开发复杂的企业应用时通常是不够的。例如,它不包括持久化、事务或安全套接字层(SSL)管理的 API。因此,我们需要一个框架,它利用 MicroProfile API 的可扩展功能,并且可以被 Kubernetes 编排,从管理角度来看,Kubernetes 将成为新的应用服务器。
Quarkus – 一个 Kubernetes 原生 Java 框架
微服务架构的主要挑战之一是,除非你有有效的框架来编排它们,否则服务的激增可能会增加你系统的复杂性。此外,如果没有集中的认证、数据管理和 API 网关功能,微服务架构的优势就会被这些挑战所抵消。
在这种意义上,Kubernetes 的到来是 IT 模式的一次真正革命。借助基于 Kubernetes 的编排,你可以通过以动态方式管理和调度你的微服务来提高效率和资源利用率。这也增加了高级的弹性级别。你可以继续在需求变化时运行,而不用担心容器故障。为了闭合这个循环并统一所有组件,我们需要一个专门为这种架构设计的框架。让我们来认识一下Quarkus。
当涉及到管理云原生企业应用时,Quarkus 作为一个一等公民出现,并且拥有许多令人惊叹的功能,这些功能可以实现之前不可能的场景。正如你将在接下来的章节中看到的那样,Quarkus 能够从 Java 类构建轻量级的原生代码,并从中创建容器镜像,你可以在 Kubernetes 或 OpenShift 上运行这些镜像。Quarkus 还利用了你多年来一直在使用的 Java 库中的最佳库,如 RESTEasy、Hibernate、Apache Kafka、Vert.x 等等。让我们更详细地看看这个框架的亮点。
原生代码执行
在 Java 漫长的历史中,原生代码执行尝试过多次,但开发者对其的采用率始终不高。首先,它需要一些外部工具,因为这些工具并不是平台供应商提供的。对于单体应用来说,原生执行的优势微乎其微,因为从长远来看,由于 Hot Spot 技术的进步,Java 的速度可以接近原生执行(前提是你愿意为更慢的应用启动付费)。
尽管如此,在微服务场景中,启动大量原生服务起着至关重要的作用,甚至优化几秒或一秒钟的时间也能产生巨大的差异。同样,如果你旨在达到最高的内存密度要求,最大请求吞吐量以及一致的 CPU 性能,Quarkus 的原生执行方式完美地融入了这一场景。
另一方面,您可以使用纯 Java 字节码平稳地过渡到 Quarkus,仍然可以交付具有高内存密度要求、卓越的 CPU 原生性能、先进的垃圾收集策略、大量需要标准 JDK 的库或监控工具,以及无处不在的编译一次,运行在任何地方。
以下表格总结了在开发 Quarkus 时选择原生应用程序和 Java 应用程序的一些典型用例:
| Quarkus 原生应用程序 | Quarkus Java 应用程序 |
|---|---|
| 最高内存密度要求 | 高内存密度要求 |
| 更一致的 CPU 性能 | 最佳原生性能(CPU) |
| 最快的启动时间 | 快速启动时间 |
| 简单的垃圾收集 | 高级垃圾收集 |
| 最高吞吐量 | 仅与 JDK 兼容的大量库和工具 |
| 无 JIT 峰值 | 编译一次,运行在任何地方 |
正如这一图景所示,Quarkus 是一个突破性的技术,因为它利用了原生代码执行,同时保留了您使用 OpenJDK 运行服务的能力,并在需要时使用 Hot Spot 丰富的动态代码执行能力。
容器优先
如预期的那样,Quarkus 最有希望的功能之一是能够自动从您的应用程序中生成容器镜像。原生应用程序的最小占用空间已优化,以便在容器内运行。
生成您原生应用程序的容器镜像也解决了与原生执行相关的一个常见陷阱,即当使用不同的操作系统构建时可能出现的潜在冲突或错误。由于容器封装了您选择的操作系统,您可以在不遇到崩溃转储或臭名昭著的蓝屏场景的风险下,提供容器安全的原生应用程序构建。
统一命令式和响应式编程模型
大多数 Java 开发者都熟悉命令式编程模型,它转化为一系列用于修改对象状态的指令。另一方面,由于固有的复杂性和缺乏传播异步更改的可靠模式,异步编程一直是 Java 开发者的挑战。在这种情况下,一种称为响应式编程的范式因其能够将异步编程模式与数据流和变化的传播相结合而受到欢迎。
Quarkus 从头开始设计,旨在统一同一平台上的两种模型,以便你可以利用两种编程模型的好处,并在你的 IT 组织中使用它们。
令人愉悦的编码
即使是最强大的框架,如果使用过于复杂,需要大量编码和配置才能完成即使是微小的功能,也不会得到广泛的应用。
正如我们从 Spring Boot 的成功中学到的那样,开发者在使用框架时不需要花费大量时间在其设置或配置上,会更加高效。开箱即用,Quarkus 提供了以下功能:
-
一个统一的配置,可以轻松地在一个属性文件中维护
-
一系列默认设置,这样你实际上可以编写应用程序,甚至无需任何配置
此外,你还可以拥有以下非凡的功能:
-
无需任何第三方插件即可进行应用程序的实时重新加载
-
使用本地可执行文件直接生成容器
-
通过添加专门为 Quarkus 构建的测试扩展来简化测试
最佳 Java 库和标准
使开发堆栈成功的标准是各种事物的组合,例如活跃的贡献者数量、顶级工业参与者的高度认可和使用、遵守知名标准以及强大和活跃的标准验证者。
在这个问题上,Quarkus 通过利用你已熟悉的最佳库,将它们自动连接起来以生成最终工件,提供了一个统一的、全栈的框架。Quarkus 扩展包括完整的 Eclipse MicroProfile Stack、持久化 API(JPA)、事务管理器(Narayana)、响应式框架(Vert.x)、异步事件驱动网络应用程序框架(Netty)等等。
Quarkus 还包括一个扩展框架,第三方组件作者可以利用它来扩展框架。Quarkus 扩展框架大大降低了第三方框架在 Quarkus 上运行和编译为本地二进制的复杂性。
Quarkus 架构
既然我们已经了解了 Quarkus 的一些亮点,让我们更深入地看看这个框架的架构。
在 Quarkus 的核心,有一个核心组件在构建阶段完成重写我们的应用程序的繁重工作,以便生成超级优化的本地可执行文件和 Java 可运行的应用程序。为此,Quarkus 核心需要一组工具的合作:
-
Jandex: 这是一个空间高效的 Java 注解索引器和离线反射库,能够将一组类的所有运行时可见 Java 注解和类层次结构索引到一个内存高效的表示中。
-
Gizmo: 这是一个字节码生成库,Quarkus 用它来生成 Java 字节码。
-
GraalVM:这是一个组件集合。每个组件都有特定的功能,例如编译器、用于集成 Graal 语言和配置原生图像的 SDK API,以及基于 JVM 的语言的运行环境。
-
SubstrateVM:这是 GraalVM 的一个子组件,允许对 Java 应用程序进行 提前编译(AOT)。
接下来,我们将转向可用的 Quarkus 扩展列表。首先,Quarkus 完全实现了 MicroProfile 规范。Quarkus 还包括一套用于处理持久化的 Hibernate ORM 扩展,一个事务管理器(Narayana),一个连接池管理器(Agroal),以及更多其他扩展,例如 Apache Kafka API、Camel 路由以及运行反应式应用程序的能力(Vert.X)。
以下图总结了 Quarkus 架构的核心组件,尽管由于篇幅限制,可用的扩展列表可能无法详尽无遗:

在介绍了 Quarkus 架构的基础之后,我们不再拖延,现在将转向安装我们构建和运行 Quarkus 应用程序所需的工具。我们的待办事项列表并不长,并将很快解决。在下一节中,我们将安装 GraalVM 和我们的应用程序的开发环境。
开始使用 GraalVM
要将 Java 代码编译成原生可执行文件,你需要一个虚拟机的扩展,称为 GraalVM。更准确地说,GraalVM 是一个通用虚拟机,它简化了各种语言(如 Python、JavaScript、Ruby 等)的字节码编译。除此之外,它还允许在同一个项目中集成这些语言。它还有一些其他特性,其中之一是提供 Substrate VM,这是一个允许对用各种语言编写的应用程序进行 AOT 编译的框架。它还允许我们将 JVM 字节码编译成原生可执行文件。
GraalVM 与其他供应商提供的任何其他 JDK 类似,除了它支持基于 Java 的 JVM 编译器接口(JVMCI),并且它使用 Graal 作为其默认 JIT 编译器。因此,它不仅能执行 Java 代码,还能执行 JS、Python 和 Ruby 等语言。这可以通过一个名为 Truffle 的语言抽象语法树解释器来完成,它是 Oracle 与 GraalVM 协作开发的。
以下图展示了 GraalVM 栈的高级视图:

所有这些听起来都很棒,但 GraalVM 也有其代价。Java 的动态性质受到严重限制;例如,默认的反射机制将不会工作,除非类/成员已被显式注册为反射。此外,运行时类加载、动态代理和静态初始化器至少需要一些更改/解决方案才能工作。
Quarkus 是如何克服这些限制的呢?秘诀在于尽可能地在构建时移动框架初始化。在这个阶段,Quarkus 能够通过元数据发现(如注解)来发现哪些类需要在运行时进行反射。Quarkus 使用一系列工具,如 Jandex 来优化注解处理和字节码生成。此外,为了克服 GraalVM 的其他限制,Quarkus 使用单遍、单类加载器,并通过编程方式提供编译提示,以实现广泛的死代码消除,从而大大减小可执行文件的大小。
要开始,我们将从www.graalvm.org/安装 GraalVM。正如您可以从下载页面看到的那样,社区版和企业版 GraalVM 都可用。在这本书中,我们将使用社区版,因此请下载适合您操作系统的社区版本。
安装 GraalVM
有几种方法可以开始使用 GraalVM。您可以选择下载适合您操作系统的 zip 包二进制文件,或者从源代码构建它。为了本书的目的,我们将选择前者。安装步骤如下:
-
导航到下载页面,选择社区版。您将被重定向到 GitHub 项目,该项目在那里托管。下载与操作系统匹配的存档。请注意,与 Quarkus 1.0.0.final 一起推荐的 GraalVM 版本是 19.2.1 版本。实际上,较新的 19.3.0 版本不符合 Quarkus 1.0.0.final 的要求。
-
将存档提取到您的文件系统中。
您应该在提取 GraalVM 的文件夹中拥有以下顶级结构:
graalvm-ce-19.2.1/
├── 3rd_party_licenses.txt
├── ASSEMBLY_EXCEPTION
├── bin
├── GRAALVM-README.md
├── include
├── jre
├── lib
├── LICENSE
├── man
├── release
├── sample
├── src.zip
如您所见,GraalVM 的顶级结构相当类似于 JDK。在bin文件夹中,您将找到许多 JDK 工具的实用程序和替代品。值得注意的是,当您在 GraalVM 中使用java命令时,它会运行 JVM 和默认编译器,即 Graal。javac可用于编译您的代码。除此之外,以下命令在利用 GraalVM 的本地和多语言功能时是必不可少的:
-
js:如果我们传递一组选项和 JavaScript 文件名作为参数,则可以使用此命令执行纯 JavaScript 代码。 -
node:此命令可用于运行基于 Node.js 的应用程序。它依赖于npm命令来安装 Node.js 模块。 -
native-image: 此命令将您的 Java 类(们)构建为 AOT 编译的可执行文件或共享库。它默认不包括在大多数最新的 GraalVM 安装中,您需要gu工具来安装它。 -
npm:这是 Node.js 的包管理器。它将模块放置在适当的位置,以便node可以找到它们,并智能地管理依赖冲突。 -
lli: 这是一个可以执行在托管环境中 LLVM 位码的 LLVM 位码解释器。 -
gu: 此工具可用于安装 Python、R 和 Ruby 的语言包,以及native-image工具。
我们要做的第一件事是将 GraalVM 已安装的路径导出到我们的环境中:
export GRAALVM_HOME=/path/to/graal
此外,建议在安装过程中将 GraalVM 的bin文件夹添加到您的操作系统PATH中。例如,在 Linux 上,我们会这样做:
export PATH=$PATH:$GRAALVM_HOME
可选地,您可以通过设置JAVA_HOME环境变量来解析到 GraalVM 安装目录,如下所示:
export JAVA_HOME=$GRAALVM_HOME
所有前面的环境设置都应该添加到初始化您的 shell 的脚本中。对于大多数 Linux 发行版,这意味着将它们放入.bashrc文件中。
在您设置了PATH环境变量之后,使用 GraalVM 的启动器检查语言版本就非常简单了:
$ java -version
openjdk version "1.8.0_232"
OpenJDK Runtime Environment (build 1.8.0_232-20191008104205.buildslave.jdk8u-src-tar--b07)
OpenJDK 64-Bit GraalVM CE 19.2.1 (build 25.232-b07-jvmci-19.2-b03, mixed mode)
$ node -v
v10.16.3
$ lli --version
LLVM (GraalVM CE Native 19.2.1)
GraalVM 中所有语言运行时所属的可执行文件都模拟了语言默认运行时的行为。为了运行您的应用程序,您只需在PATH环境变量的开头包含 GraalVM 即可。
使用 GraalVM 运行 Java 应用程序
为了测试您的 GraalVM 环境是否正确工作,我们将添加一个最小的 Java 类并运行它。打开一个编辑器并创建以下Main类:
public class Main {
public static void main(String[] args) {
System.out.println("Hello GraalVM!");
}
}
将此类编译成字节码,然后在 GraalVM 上使用以下命令运行:
$ javac Main.java
$ java Main
这将给出以下输出:
Hello GraalVM!
构建原生图像
现在我们已经测试了 Java 字节码的编译和执行,我们将把字节码转换为原生可执行文件,以实现应用程序更快地启动和更小的占用空间。为了做到这一点,我们需要native-image工具,它允许我们将 Java 代码 AOT 编译成独立的可执行文件。我们可以按照以下方式安装native-image工具:
${GRAALVM_HOME}/bin/gu install native-image
在成功安装后,我们可以使用native-image工具针对我们创建的相同的HelloWorld Java 类进行操作。运行以下命令来构建原生图像:
$ native-image Main
您将看到以下输出:
Build on Server(pid: 7874, port: 38225)*
[main:7874] classlist: 1,156.93 ms
[main:7874] (cap): 859.74 ms
[main:7874] setup: 1,940.59 ms
[main:7874] (typeflow): 2,415.87 ms
[main:7874] (objects): 680.43 ms
[main:7874] (features): 124.69 ms
[main:7874] analysis: 3,517.35 ms
[main:7874] universe: 179.14 ms
[main:7874] (parse): 413.36 ms
[main:7874] (inline): 728.98 ms
[main:7874] (compile): 3,642.72 ms
[main:7874] compile: 5,219.51 ms
[main:7874] image: 304.89 ms
[main:7874] write: 97.09 ms
[main:7874] [total]: 12,891.94 ms
这将构建一个可执行文件,大小仅为 2 MB,命名为main,位于当前工作目录中:
$ ls -al main
-rwxrwxr-x. 1 francesco francesco 2481352 Apr 16 10:06 main
调用它将执行Main类的本地编译代码,如下所示:
$ ./main
Hello GraalVM!
一旦我们验证了我们的 GraalVM 安装正常工作,我们就可以安装一个开发环境,这将需要运行本书中包含的示例。
安装开发环境
随着我们远离单体开发,不再需要大量插件来构建应用层之间的复杂交互,选择开发环境的重要性就降低了。因此,我们可以选择任何能够原生导入/导出 Maven 或 Gradle 项目的 IDE,以及一套不错的功能来加速我们的代码或重构它。我们将使用 IntelliJ IDEA,我们可以从 www.jetbrains.com/idea/ 下载它。
从下载页面 (www.jetbrains.com/idea/download/) 可以看到, Ultimate 和 Community 版本都可用。我们将使用后者,它可以免费下载。选择下载适用于您操作系统的最新二进制文件。然后,将其解压缩到您偏好的文件夹中(例如,在您的 Home 文件夹中):
tar xvzf ideaIC-2019.1.tar.gz -C $HOME
接下来,进入安装目录下的 bin 文件夹,并使用以下命令执行:
./idea.sh
让我们对开发环境的视觉元素进行简要概述。
IntelliJ IDEA 的简要概述
尽管我们不会专注于特定的开发环境来学习 Quarkus,但我们将提供 IntelliJ IDEA 组成元素的简要概述,以便您能够更快、更轻松地了解可以执行的操作。如图所示,这些是 IntelliJ IDEA 界面的主要元素:

让我们看看各种高亮部分:
-
菜单栏:菜单栏包括我们可以使用的选项,用于创建或导入项目,以及其他与项目相关的关键操作,如代码重构、构建、运行、调试、版本控制选项等。
-
工具栏:工具栏包含一些常见执行操作的快捷键,如编译、调试和运行。您也可以根据需求自定义它。
-
导航栏:导航栏允许在项目内部进行源之间的导航。随着代码库的增长,这个功能将非常有用。
-
工具栏:工具栏包含一些常见执行操作的快捷键,如编译、调试和运行。您也可以根据需求自定义它。
-
项目视角:项目视角窗口包含您项目的所有元素,如包、模块、类、外部库等。
-
编辑窗口:这是您在 IntelliJ IDEA 中使用高级功能(如语法高亮、智能完成、快速修复建议等)编辑代码的地方。
太好了!在下一章中,我们将创建一个简单的 Quarkus 应用程序,并将其导入 IntelliJ IDEA。
安装 IntelliJ IDEA 的 Quarkus 插件
在结束本章之前,值得一提的是,IntelliJ IDEA 在其插件市场中包含了一个用于引导 Quarkus 应用程序的插件。您可以通过文件 | 设置 | 插件顶菜单选项安装它。一旦您选择了插件选项,在市场文本字段中搜索“quarkus”。
一旦找到它,点击以下图片所示的安装按钮:

重新启动 IDE 以使更改生效。一旦插件安装完成,您可以直接从 IDE 中添加新的 Quarkus 项目。以下是更新后的项目列表快照:

项目向导将引导您选择您项目的 Maven 坐标以及您想要包含在内的扩展:

在驶向未知水域之前,我们将稍作停留,简要回顾本章所学内容。然后,泡上一杯茶,准备出发吧!
摘要
在本章中,我们概述了当前 IT 行业的格局。正如我们所学的,Kubernetes 通过提供一套新的分布式服务和创建跨多个节点分布式的系统运行环境,为传统的基于语言的构建块添加了一个全新的维度。尽管创建容器化应用程序的核心原则并不严格要求您将单体应用程序分解为单个服务,但在隔离、可扩展性、团队独立性、监控、弹性和生命周期自动化方面,这样做显然具有优势。
之后,我们通过介绍 Quarkus,一个令人惊叹的框架,其中我们可以创建无服务器、原生应用程序,而不会失去作为 Java 开发者所学的技能,讨论了可以原生运行的实际应用。
现在我们已经安装了开始使用 Quarkus 所需的工具,在下一章中,我们将编写我们的第一个示例应用程序。
第二章:使用 Quarkus 开发你的第一个应用程序
在本章中,我们将使用我们可用的工具来创建我们的第一个 Quarkus 应用程序。正如你很快就会看到的,这是一个相当简单的流程,可以从命令行启动,不需要你下载任何外部工具。通过使用这个流程,我们将能够将应用程序编译成原生可执行文件,并证明当 Quarkus 将其转换为原生代码时,Java 应用程序可以有多快和多轻。
在本章中,我们将涵盖以下主题:
-
使用 Quarkus Maven 插件启动我们的项目
-
启动项目的替代方法(Quarkus CLI)
-
创建和执行我们的第一个 Quarkus 应用程序
-
从我们的 IDE 调试应用程序
-
使用 JUnit 测试框架的扩展测试应用程序
-
将我们的应用程序转换为原生代码
技术要求
你可以在 GitHub 上找到本章项目的源代码,地址为github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter02。
使用 Quarkus Maven 插件入门
为了构建我们的第一个 Quarkus 应用程序,我们将使用 Maven,这是最常见的一种软件和发布管理工具。它被各种开发者所使用,主要是因为它提供了以下功能:
-
所有项目的标准结构
-
依赖项的集中和自动管理
Maven 以几种格式分发给用户方便使用。你可以从maven.apache.org/download.cgi下载它。
下载 Maven 后,执行以下操作:
- 将分发存档(例如,
apache-maven-3.6.1-bin.zip)解压缩到你想安装 Maven 的目录中(例如,在你的$HOME/apache文件夹中):
$ mkdir $HOME/apache
$ unzip $HOME/Downloads/apache-maven-3.6.1-bin.zip -d $HOME/apache
- 将 Maven 库添加到你的系统路径中,如下面的代码所示。这将更新
PATH环境变量:
$ export PATH=$PATH:$HOME/apache/apache-maven-3.6.1/bin
- 完成安装后,你需要检查 Maven 是否已正确安装。运行
mvn --version来验证这一点:
$ mvn --version
Apache Maven 3.6.1
Maven home: /home/francesco/apache/apache-maven-3.6.1
Java version: 1.8.0_191, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b13-0.fc29.x86_64/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "4.18.16-300.fc29.x86_64", arch: "amd64", family: "unix"
如果你得到了前面的输出,那么你刚刚验证了 Maven 已经安装在你的系统上。
启动 Quarkus Maven 插件
现在 Maven 已经设置好了,我们可以通过其 Maven 插件启动我们的第一个 Quarkus 应用程序。Maven 插件提供了一组目标,可以执行以编译和构建我们的工件或扩展我们的项目以添加一些功能。每个插件,就像每个 Maven 组件一样,基于以下坐标:
-
groupId:项目组的 ID。这通常与包根目录的 ID 相匹配。 -
artifactId:工件 ID。这通常与最终工件名称相匹配。 -
version:指定组下工件版本。
您可以通过指定 <groupId>:<artifactId> 坐标从命令行引用 Maven 插件。对于 Quarkus,<groupId>:<artifactId> 组合是 io.quarkus:quarkus-maven-plugin。您可以使用以下命令检查可用的目标和最新版本:
$ mvn -Dplugin=io.quarkus:quarkus-maven-plugin help:describe
您将看到以下输出:
Name: Quarkus - Maven Plugin
Description: Build parent to bring in required dependencies
Group Id: io.quarkus
Artifact Id: quarkus-maven-plugin
Version: 1.0.0.Final
Goal Prefix: quarkus
This plugin has 11 goals:
quarkus:add-extension
Description: (no description available)
quarkus:add-extensions
Description: Allow adding an extension to an existing pom.xml file.
Because you can add one or several extension in one go, there are 2
mojos:
add-extensions and add-extension. Both supports the extension and
extensions parameters.
quarkus:analyze-call-tree
Description: (no description available)
quarkus:build
Description: (no description available)
quarkus:create
Description: This goal helps in setting up Quarkus Maven project with
quarkus-maven-plugin, with sensible defaults
quarkus:create-example-config
Description: (no description available)
quarkus:dev
Description: The dev mojo, that runs a quarkus app in a forked process
quarkus:help
Description: Display help information on quarkus-maven-plugin.
Call mvn quarkus:help -Ddetail=true -Dgoal=<goal-name> to display parameter
details.
quarkus:list-extensions
Description: (no description available)
quarkus:native-image
Description: (no description available)
quarkus:remote-dev
Description: The dev mojo, that connects to a remote host
我们第一个应用程序的源代码可以位于本书 GitHub 存储库的 Chapter02/hello-rest 文件夹中。为了参考,我们使用 Maven 插件创建了应用程序,并配置了以下参数集:
$ mvn io.quarkus:quarkus-maven-plugin:1.0.0.Final:create \
-DprojectGroupId=com.packt.quarkus.Chapter02 \
-DprojectArtifactId=hello-rest \
-DclassName="com.packt.quarkus.Chapter02.SimpleRest" \
-Dpath="/helloworld"
由于前面的命令,hello-rest 文件夹中已生成以下目录结构:
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ ├── docker
│ │ ├── Dockerfile.jvm
│ │ └── Dockerfile.native
│ ├── java
│ │ └── com
│ │ └── packt
│ │ └── quarkus
│ │ └── Chapter02
│ │ └── SimpleRest.java
│ └── resources
│ ├── application.properties
│ └── META-INF
│ └── resources
│ └── index.html
└── test
└── java
└── com
└── packt
└── quarkus
└── Chapter02
├── NativeSimpleRestIT.java
└── SimpleRestTest.java
在本章的下一节中,我们将学习如何将项目导入 IntelliJ IDEA(尽管在任意 IDE 中步骤大致相同)。现在,让我们继续查看项目的前一个树视图,并查看包含在本项目中的文件:
-
一个 项目对象模型 (
pom.xml),包含项目配置 -
一个名为
SimpleRest.java的示例 REST 服务以及为其创建的测试类SimpleRestTest.java,以及一个名为NativeSimpleRestIT.java的包装类,用于在原生可执行应用程序上执行测试 -
配置文件占位符(
application.properties) -
一个
index.html文件,指示我们可以添加静态网页内容 -
一个
Dockerfile,以便我们可以从我们的应用程序创建容器 -
Maven 包装文件(
mvnw/mvnw.cmd),允许我们在不预先安装 Maven 的情况下执行 Maven 目标
pom.xml 文件将被添加到项目的根目录。在那里,您将找到一个上层的 dependencyManagement 部分,它导入 Quarkus 的 物料清单。这允许我们自动链接每个 Quarkus 扩展的确切版本。
在 Quarkus 的 1.0.0.Final 版本中,您将引用名为 quarkus-universe-bom 的 artifactId,它属于 groupId io.quarkus。
在这里,已经包含了 quarkus-maven-plugin,以便您可以对您的应用程序进行打包并生成原生可执行文件:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</build>
进入依赖关系部分,您将看到添加的唯一运行时依赖项如下,它允许您执行基本的 REST 应用程序:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
RESTEasy 是 JAX-RS 规范的可移植实现,默认包含在 WildFly 应用服务器中(www.wildfly.org)。您可以使用它通过标准 HTTP 方法使用无状态通信来提供您服务的表示。
除了 quarkus-resteasy 之外,一些其他库也被包含在您的 pom.xml 文件中,目的是测试您的应用程序。这将在 测试 Quarkus 应用程序 部分中更详细地讨论。
要向您的项目添加额外的库,除了编辑 pom.xml 文件外,您还可以使用 add-extension,这在 Quarkus 的 Maven 插件中可以找到。一个例子是 $ mvn quarkus:add-extension -Dextensions="io.quarkus:quarkus-jsonp,io.quarkus:quarkus-smallrye-health"。
以下 SimpleRest 类已经为您自动生成在 src/main/java/com/packt/quarkus/Chapter02:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/helloworld")
public class SimpleRest {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
如您所见,这是一个非常简单的 REST 端点,它利用 JAX-RS API 在 /helloworld GET 请求落在默认端口时生成 TEXT_PLAIN 资源。
比 JAX-RS 更简单!
如我们之前提到的,Quarkus 简化了代码开发以提供合理的默认值。然而,我们不再需要声明一个 ApplicationScoped 类来启动 REST 服务,因为我们将会以默认选项获得它。
运行应用程序
现在,我们已经准备好运行我们的应用程序。执行 compile 和 quarkus:dev 目标来构建并运行它:
$ mvn compile quarkus:dev
几秒钟后,应用程序将被编译并执行,如下面的日志所示:
[INFO] Scanning for projects...
. . . .
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello-rest ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ hello-rest ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /home/francesco/git/packt/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/chapter2/hello-rest/target/classes
[INFO]
[INFO] --- quarkus-maven-plugin:1.0.0.Final:dev (default-cli) @ hello-rest ---
Listening for transport dt_socket at address: 5005
2019-11-11 13:10:34,493 INFO [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
2019-11-11 13:10:35,078 INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 585ms
2019-11-11 13:10:35,395 INFO [io.quarkus] (main) Quarkus 1.0.0.CR1 started in 1.079s. Listening on: http://0.0.0.0:8080
2019-11-11 13:10:35,397 INFO [io.quarkus] (main) Profile dev activated. Live Coding activated.
2019-11-11 13:10:35,397 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
现在,您可以使用浏览器或像 curl 这样的工具请求提供的端点:
$ curl http://localhost:8080/helloworld
hello
您可以使用 Ctrl + C 停止应用程序,尽管我们建议保持其运行,因为我们很快将测试 热重载 功能!
使用 Maven 插件生成 Gradle 项目
尽管名为 Quarkus Maven 插件,但它相当中立。实际上,您也可以使用它来生成 Gradle 项目。这两个工具的比较超出了本书的范围;然而,许多开发者更喜欢 Gradle 作为构建工具,因为它以最基本的方式是可扩展的,并且具有出色的性能。
话虽如此,您可以通过将 buildTool 选项设置为 gradle 来简单地生成 Gradle 项目,否则默认为 maven。以下是您如何使用 Gradle 生成项目的方法:
mvn io.quarkus:quarkus-maven-plugin:1.0.0.Final:create \
-DprojectGroupId=com.packt.quarkus.Chapter02 \
-DprojectArtifactId=hello-rest \
-DclassName="com.packt.quarkus.Chapter02.SimpleRest" \
-Dpath="/helloworld" \
-DbuildTool=gradle
生成的 build.gradle 文件定义了可用的存储库和依赖项集合,并设置了核心项目属性,如 quarkusPlatformGroupId、quarkusPlatformArtifactId 和 quarkusPlatformVersion 作为变量:
buildscript {
repositories {
mavenLocal()
}
dependencies {
classpath "io.quarkus:quarkus-gradle-
plugin:${quarkusPluginVersion}"
}
}
plugins {
id 'java'
}
apply plugin: 'io.quarkus'
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation enforcedPlatform("${quarkusPlatformGroupId}:
${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")
implementation 'io.quarkus:quarkus-resteasy'
testImplementation 'io.quarkus:quarkus-junit5'
testImplementation 'io.rest-assured:rest-assured'
nativeTestImplementation 'io.quarkus:quarkus-junit5'
nativeTestImplementation 'io.rest-assured:rest-assured'
}
group 'org.acme'
version '1.0.0-SNAPSHOT'
compileJava {
options.compilerArgs << '-parameters'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
所有前面的变量都是从位于项目根目录的 gradle.properties 文件中检索的。
如您从配置中看到的,默认项目中还包括了一个插件,这样您就可以轻松构建您的应用程序并在开发模式下启动,如下所示:
./gradlew quarkusDev
最后,值得一提的是,Gradle 扩展仍在开发中,因此您可能会在下一个 Quarkus 版本中看到一些更改或更新。
现在,我们将学习如何轻松地使用在线 Quarkus 项目生成器启动我们的项目(无论是 Maven 还是 Gradle)。
使用 Quarkus 在线应用程序启动应用程序
启动您的 Quarkus 应用程序的另一种选项是使用在线应用程序,该应用程序可在以下地址找到:code.quarkus.io/。
通过访问该页面,您将能够生成一个具有初始端点和您在用户界面中检查的所有扩展的基本项目:

如前一个截图所示,默认情况下,仅选中了RESTEasy扩展。从界面的左上角,您可以配置您的项目坐标(groupId,artifactId)和构建工具,这可以是 Maven 或 Gradle。更多选项可通过“更多配置选项”面板获得,该面板允许您配置包名和项目的版本。
通过滚动查看可用扩展列表,您还可以选择尝试使用其他语言,如 Kotlin 或 Scala,来开发您的 Quarkus 应用程序。这些选项仍在开发中,因此请考虑,随着扩展的成熟,它们的 API 和/或配置可能会发生变化。然而,Quarkus 团队非常感谢您对任何预览扩展的测试反馈。
当您设置完选项后,只需单击“启动新应用程序”即可将工件作为压缩文件夹下载:

现在,您只需将其解包并导入您喜欢的 IDE 中。我们将在下一节中这样做。
在您的 IDE 中测试实时重新加载
在本节中,我们将使用 Quarkus 的实时重新加载功能。为此,我们将项目导入到我们的 IDE 中,以便我们可以应用一些更改。
导航到文件 | 打开,并指向您创建 Maven 项目的文件夹。它将自动导入到您的 IDE 中。以下是您的 Java 类的文件标签视图:

现在,让我们看看 Quarkus 的实时重新加载是如何工作的。为此,让我们对我们的代码进行简单的修改。在这里,我们已修改了hello方法的返回值,如下所示:
public class SimpleRest {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello changed!";
}
}
希望您没有停止您的服务器。现在,再次尝试调用该服务:
$ curl http://localhost:8080/helloworld
hello changed!
如您所见,在开发模式下运行时,您可以实时重新加载您的应用程序。令人惊叹,不是吗?
实时重新加载也适用于资源文件,如网页或配置属性文件。请求服务将触发工作区扫描,如果检测到任何更改,Java 文件将被重新编译,应用程序将被重新部署。然后,您的请求将由重新部署的应用程序处理。
应用程序调试
在开发模式下运行时,Quarkus 将自动监听端口5005上的调试器。您可以使用以下基本 shell 命令检查调试是否激活:
$ netstat -an | grep 5005
tcp 0 0 0.0.0.0:5005 0.0.0.0:* LISTEN
现在,让我们在hello方法中撤销这些更改,并包含另一个hello方法,该方法接收一个要检查的输入参数:
package com.packt.quarkus.chapter2;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.MediaType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Path("/helloworld")
public class SimpleRest {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/{name}")
public String hello(@PathParam("name") String name) {
log.info("Called with "+name);
return "hello "+name;
}
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
通过在我们的 REST 服务中使用 @PathParam 表达式,我们将能够从我们的 IDE 中作为方法变量调试这个表达式的值。现在,放置一个断点在日志语句上,如下截图所示:

接下来,为了将 IntelliJ IDEA 连接到调试器,你必须连接到调试器的端口。在 IntelliJ IDEA 中,你可以通过多种方式做到这一点。最简单的方法是选择 Run | Attach to Process。你的应用程序的可运行进程将被检测,如下截图所示:

选择它并检查你是否成功附加到了它。你可以从调试器控制台这样做:

现在,通过在末尾添加一个额外的参数来调用应用程序,以便你能够触发断点:
$ curl http://localhost:8080/helloworld/frank
从调试器提示符,你可以从其控制台检查类和方法变量。你也可以通过点击调试器控制台左侧的按钮来控制执行路径(Step Over,Step Into,Stop 等):

如果你想在启动 Quarkus 应用程序之前等待调试器附加,你可以在命令行上传递 -Ddebug。一旦你的 IDE 的调试器连接,Quarkus Augmentor 将启动,你的应用程序将被执行。另一方面,如果你根本不需要调试器,你可以使用 -Ddebug=false。
测试 Quarkus 应用程序
除了示例端点外,Maven 插件还自动包含了一个用于我们 REST 服务的测试类:
package com.packt.quarkus.chapter2;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
public class SimpleRestTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/helloworld")
.then()
.statusCode(200)
.body(is("hello"));
}
}
在幕后,这个测试类使用 JUnit 作为核心测试框架和 REST Assured 库:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
REST Assured 是一个 Java 库,可以用来使用灵活的 领域特定语言(DSL)编写强大的 REST API 测试。REST Assured 中可用的流畅 API 支持来自 行为驱动开发(BDD)的标准模式,使用 Given/When/Then 语法。生成的测试易于阅读,并且可以包含我们构建测试所需的全部步骤,只需一行代码即可。
现在,我们可以验证响应体的内容并检查 HTTP 响应状态码是否为 200。我们可以通过运行以下命令来验证测试的执行:
$ mvn clean test
你应该在控制台看到以下输出:
[INFO] Running com.packt.quarkus.chapter2.SimpleRestTest
2019-05-16 11:04:21,166 INFO [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
2019-05-16 11:04:21,832 INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 669ms
2019-05-16 11:04:22,108 INFO [io.quarkus] (main) Quarkus 1.0.0.Final started in 0.265s. Listening on: http://[::]:8081
2019-05-16 11:04:22,109 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.958 s - in com.packt.quarkus.chapter2.SimpleRestTest
2019-05-16 11:04:23,263 INFO
[io.quarkus] (main) Quarkus stopped in 0.005s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] -----------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] -----------------------------------------------------------------------
如你所见,测试在本地 IP 地址的端口 8081 上启动了 Quarkus 运行时。因此,它不会干扰默认在端口 8080 上运行的开发/生产环境。
你可以通过使用 and() 方法将多个条件混合匹配在你的测试中。这个方法作为一个简单的语法糖,也就是说,它有助于使代码更易读。以下是一个如何包含对头部 Content-Length 的检查的示例:
@Test
public void testHelloEndpointHeader() {
given()
.when().get("/helloworld")
.then()
.statusCode(200)
.body(is("hello"))
.and()
.header("Content-Length","6");
}
通过使用参数化测试,您可以在单个方法中通过提供不同的记录集来测试多个场景。REST Assured 支持两种不同类型的参数:
- 查询参数:这些参数可以附加在 RESTful API 端点的末尾,并且通过它们前面的问号来识别。以下是一个示例:
@Test
public void testHelloEndpointQueryParam() {
given()
.param("name","Frank")
.when().get("/helloworld")
.then()
.statusCode(200)
.body(is("hello"));
}
如您所见,使用查询参数只需要我们通过连接 param() 方法来指定它们的名称和值。
- 路径参数:这些参数以类似的方式指定,即通过包含
pathParam()方法以及参数名称/值组合:
@Test
public void testHelloEndpointPathParam() {
given()
.pathParam("name", "Frank")
.when().get("/helloworld/{name}")
.then()
.statusCode(200)
.body(is("hello Frank"));
}
最后,值得一提的是,由于 Quarkus 致力于最高性能,您还可以根据响应时间验证您的测试。这可以通过将 time() 连接到条件来实现。以下是一个示例,当返回响应时将时间设置为小于一秒:
@Test
public void testTimedHelloEndpointPathParam() {
given()
.pathParam("name", "Frank")
.when().get("/helloworld/{name}")
.then()
.time(lessThan(1000L))
.body(is("hello Frank"));
}
在本节中,我们介绍了我们可以使用 REST Assured API 构建的常见测试场景。如果您想查看更多高级模式,我们建议查看其 Wiki,它可在 github.com/rest-assured/rest-assured/wiki/usage 找到。
选择不同的端口进行测试
您可以通过在 src/main/resources/application.properties 文件中设置适当的值来更改 Quarkus 用于测试的默认端口(8081),这是 Quarkus 的通用配置文件。例如,要将测试端口更改为 9081,您需要在 application.properties 中添加以下信息:
quarkus.http.test-port=9081
作为替代,您也可以在启动时通过传递 -Dquarkus.http.test-port=9081 标志来使用相同的属性。
将您的应用程序转换为原生可执行文件
现在,是时候检查 Quarkus 如何将我们的字节码转换为原生可执行文件了。这种魔法是通过一个名为 native 的 Maven 配置文件在幕后完成的,该配置文件在您搭建应用程序时默认包含:
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemProperties>
<native.image.path>${project.build.directory}
/${project.build.finalName}-runner
</native.image.path>
</systemProperties>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
此外,maven-failsafe-plugin 已经自动配置为运行 integration-test goal,因为我们已经设置了构建原生图像的路径作为系统属性。
在构建您的可执行文件之前,请确保您已经按照前一章所述在环境中设置了 GRAALVM_HOME。
接下来,通过执行以下命令创建一个原生可执行文件:
$ mvn package -Pnative
插件将开始分析您应用程序中使用的类和打包,以及调用树。生成的输出将是一个超级精简的可执行文件,它只包含一个薄薄的 JVM 层(足够窄,仅用于执行应用程序)以及应用程序本身。
您应该在输出的末尾看到类似以下的内容:
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 60485ms
除了包含您应用程序压缩字节码的 JAR 文件外,以下可执行文件将在 target 文件夹中生成:
Nov 11 14:49 hello-rest-1.0-SNAPSHOT-runner
您应用程序的实际名称可以通过在pom.xml文件中设置native.image.path环境变量来改变,默认值为${project.build.directory}/${project.build.finalName}-runner。
如您所见,我们有一个大约 20 MB 的可执行应用程序运行时,它包含所有库以及从 JVM 运行我们的应用程序所需的一切。您可以使用以下命令执行它:
$ target/hello-rest-1.0-SNAPSHOT-runner
仅仅 0.006 秒,我们就让我们的服务启动并运行。这可以在控制台日志中看到:
2019-11-11 14:53:38,619 INFO [io.quarkus] (main) hello-rest 1.0-SNAPSHOT (running on Quarkus 1.0.0.CR1) started in 0.014s. Listening on: http://0.0.0.0:8080
2019-11-11 14:53:38,619 INFO [io.quarkus] (main) Profile prod activated.
2019-11-11 14:53:38,619 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
让我们通过执行ps命令来检查这个紧凑型应用程序的内存使用情况:
$ ps -o pid,rss,command -p $(pgrep -f hello-rest)
这里是我从我的笔记本电脑收集到的输出:
PID RSS COMMAND
27919 18720 target/hello-rest-1.0-SNAPSHOT-runner
尽管输出可能因您的环境而异,但常驻集大小(RSS)显示该进程正在使用大约 18 MB 的内存,这仅是 Java 应用程序所需的最小内存大小的一小部分。
现在,让我们执行它以检查结果:
$ curl http://localhost:8080/helloworld
hello
如您所见,当我们将我们的应用程序转换为本地应用程序时,结果并没有改变。
对本地可执行文件执行集成测试
有趣的是,本地可执行代码可以测试。当您生成示例项目时,测试文件夹中包含了一个名为Native<project>Test的类名。这个类与 Java 测试不同,因为它被注解为@NativeImageTest注解。
由于 Maven failsafe 插件的配置,所有以IT结尾或用@NativeImageTest注解的 rest 都将针对本地可执行文件运行。
在代码方面,无需进行任何更改,因为它使用继承来执行来自我们的SimpleRestTest类的本地可执行测试:
@NativeImageTest
public class NativeSimpleRestIT extends SimpleRestTest {
// Execute the same tests but in native mode.
}
verify目标用于测试本地可执行文件。在此之前,请确保您已将 GraalVM 安装路径导出到您的环境中:
export GRAALVM_HOME=/path/to/graal
现在,你可以运行verify目标来测试本地可执行应用程序:
$ mvn verify -Pnative
请检查结果是否与本章前面测试 Quarkus 应用程序部分中产生的结果相同:
[INFO] Running com.packt.quarkus.chapter2.SimpleRestTest
2019-05-16 11:35:22,509 INFO [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
2019-05-16 11:35:23,084 INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 575ms
2019-05-16 11:35:23,419 INFO [io.quarkus] (main) Quarkus 1.0.0.Final started in 0.319s. Listening on: http://[::]:8081
2019-05-16 11:35:23,419 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
2019-05-16 11:35:24,354 INFO [com.pac.qua.cha.SimpleRest] (XNIO-1 task-1) Called with Frank
2019-05-16 11:35:24,598 INFO [com.pac.qua.cha.SimpleRest] (XNIO-1 task-1) Called with Frank
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.215 s - in com.packt.quarkus.chapter2.SimpleRestTest
太好了!我们刚刚成功测试了我们的示例应用程序在两种场景(JVM 和本地可执行)下。
摘要
在本章中,我们完成了我们的第一个概念验证 Quarkus 项目,该项目是通过quarkus-maven-plugin生成的。默认应用程序是一个具有所有最小功能和一个我们逐步丰富Test类的 REST 服务的原型。在本章的第二部分,我们看到了如何使用quarkus-maven-plugin的适当本地配置将 Java 应用程序代码转换为瘦型本地可执行文件。
到目前为止,我们只是触及了我们可以用 Quarkus 做到的事情的表面。现在,是时候继续前进,学习如何从我们的本地应用程序创建容器镜像,并在 Kubernetes 环境中部署它。这就是我们将在下一章中讨论的内容。
第三章:创建您的应用程序的容器镜像
在上一章中,我们通过运行传统的 JVM 应用程序并将其转换为原生构建,一瞥了 Quarkus 应用程序的力量。然而,Quarkus 不仅仅有精简的可执行文件和低资源使用,因此,在本章中,我们将继续学习如何创建我们的应用程序的容器镜像,然后可以部署到 Kubernetes 原生环境中。为此,我们的待办事项列表包括安装 Docker 工具和 OpenShift 的社区版,这被称为Origin Community Distribution of Kubernetes,或简称OKD。然后,我们将学习如何扩展我们的应用程序,以便我们可以进一步提高其响应时间。
在本章中,我们将涵盖以下主题:
-
在您的环境中设置 Docker
-
在容器中启动 Quarkus 应用程序
-
在容器中运行原生可执行文件
-
在 OpenShift 上部署您的容器镜像
-
扩展我们的应用程序以提高其吞吐量
技术要求
您可以在 GitHub 上找到本章节的项目源代码,链接为github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter03。
设置 Docker
Docker 是一个工具,它让我们能够简化在我们环境中创建和执行容器。每个容器反过来又把一个应用程序及其依赖项封装成一个单一的标准化单元,该单元包括它运行所需的一切,即系统工具、代码和其他所需的库。这保证了您的应用程序将始终以相同的方式执行,通过共享一个简单的容器镜像。Docker 有两种版本:
-
社区版(CE):我们将在这本书中使用 Docker CE,它非常适合寻求快速开始使用 Docker 和基于容器的应用程序的开发人员和小型团队。
-
企业版(EE):EE 具有额外的功能,例如认证的基础设施、镜像管理和镜像安全扫描。
虽然我们将使用 Docker 的社区版,但这并不会降低您应用程序的完整潜力,因为我们将通过原生 Kubernetes 平台利用高级容器功能,这对于在生产环境中大规模运行业务关键应用程序是一个理想的解决方案。
Docker 的安装过程在docs.docker.com/install/上进行了全面文档化。简而言之,您可以根据自己的需求遵循几种安装策略:
-
从中期角度来看,你可能希望简化 Docker 的升级。大多数用户选择设置 Docker 的仓库,并从那里安装和升级(
docs.docker.com/install/linux/docker-ce/fedora/#install-using-the-repository)。 -
另一个选项,如果你在离线机器上安装 Docker,证明非常实用,需要手动安装 RPM 软件包,并手动处理升级(
docs.docker.com/install/linux/docker-ce/fedora/#install-from-a-package)。 -
最后,为了快速方便地安装,你可以使用自动化脚本,该脚本将检测你的操作系统并相应地安装 Docker。为了简化,我们将选择此选项。
让我们按照以下步骤继续安装 Docker:
- 自动化脚本可以从
get.docker.com/下载,如下所示:
$ curl -fsSL https://get.docker.com -o get-docker.sh
- 现在,使用以下命令执行它:
$ sh get-docker.sh
重要!就像任何其他 shell 脚本一样,在执行之前验证其内容!其内容需要与位于 github.com/docker/docker-install 的 install.sh 脚本相匹配。如果不匹配,请通过访问 Docker 安装页面来验证自动化脚本是否仍在维护。
- 如果你希望以非特权用户身份运行 Docker,你应该考虑通过执行以下命令将你的用户添加到
docker组:
$ sudo usermod $(whoami) -G docker -a
- 为了使其生效,你需要注销并重新登录。我们可以通过检查以下命令的输出来确认我们的用户现在是否在 Docker 组中:
$ groups $(whoami)
- 输出应包括
docker在组列表中。现在,你可以验证是否可以在非 root 用户(或sudo)的情况下运行 Docker 命令:
$ docker run hello-world
- 上述命令将从 Docker 仓库拉取
hello-world测试镜像,并在容器中运行它。当测试镜像启动时,它会打印一条信息性消息然后退出:
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
此消息表明你的安装似乎工作正常。
在容器中运行 Quarkus 应用程序
一旦安装了 Docker,你就可以准备从你的 Java 或本地可执行应用程序构建 Docker 镜像了。为此,我们将快速构建另一个简单的应用程序,该应用程序检查一些环境变量以确定应用程序运行的容器 ID。
本章的源代码位于本书 GitHub 仓库的 Chapter03/hello-okd 文件夹中。我们建议在继续之前将项目导入到你的 IDE 中。
让我们从 REST 端点类 (HelloOKD) 开始,该类从 Contexts and Dependency Injection (CDI) 服务返回一些信息:
package com.packt.quarkus.chapter3;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.inject.Inject;
@Path("/getContainerId")
public class HelloOKD {
@Inject
ContainerService containerService;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "You are running on " +
containerService.getContainerId();
}
}
以下代码是 ContainerService 类的代码,该类被注入到 REST 端点:
package com.packt.quarkus.chapter3;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ContainerService {
public String getContainerId() {
return System.getenv().getOrDefault("HOSTNAME", "unknown");
}
}
此示例展示了 CDI @ApplicationScoped 注解在注入对象中的应用。定义为 @ApplicationScoped 的对象将在应用程序的生命周期内创建一次。在我们的例子中,它返回 HOSTNAME 环境变量,默认为 Docker 容器 ID。
为了测试我们的简单 REST 服务,以下 HelloOKDTest 已包含在 src/test/java 路径下的项目中。通过其 testHelloEndpoint 方法,我们验证 REST 调用的状态码是否成功:
package com.packt.quarkus.chapter3;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
public class HelloOKDTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/getContainerId")
.then()
.statusCode(200);
}
}
在我们开始 Docker 之旅之前,让我们检查前面的测试是否通过。测试阶段将在我们运行项目的 install 目标时自动启动:
$ mvn install
成功的测试应该产生以下日志:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.174 s - in com.packt.quarkus.chapter3.HelloOKDTest
2019-11-17 19:15:16,227 INFO [io.quarkus] (main) Quarkus stopped in 0.041s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
现在,让我们继续看看 Docker。如果你查看 src/main/docker 文件夹,你会注意到一些文件已经被自动添加到你的项目中:
$ tree src/main/docker
src/main/docker
├── Dockerfile.jvm
└── Dockerfile.native
列表中的第一个文件 Dockerfile.jvm 是为 JVM 环境专门编写的 Dockerfile。其内容如下:
FROM fabric8/java-alpine-openjdk8-jre
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV AB_ENABLED=jmx_exporter
COPY target/lib/* /deployments/lib/
COPY target/*-runner.jar /deployments/app.jar
EXPOSE 8080
# run with user 1001 and be prepared for be running in OpenShift too
RUN adduser -G root --no-create-home --disabled-password 1001 \
&& chown -R 1001 /deployments \
&& chmod -R "g+rwX" /deployments \
&& chown -R 1001:root /deployments
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]
Dockerfile 是一个包含一系列命令的纯文本文件,我们可以使用这些命令来组装一个镜像,以便它可以由 Docker 执行。Dockerfile 需要与特定格式和一组已在 Dockerfile 参考中记录的指令相匹配 (docs.docker.com/engine/reference/builder/)。
在我们的例子中,Dockerfile 包含了使用 Fabric8 Java Base Image 构建 Java 环境的指令,并使 JMX 导出器 (github.com/prometheus/jmx_exporter) 能够暴露进程指标。现在,我们将构建我们的容器镜像,如下所示:
$ docker build -f src/main/docker/Dockerfile.jvm -t quarkus/hello-okd .
在你的控制台中,你可以验证 Docker 拉取过程将被触发,并且 Dockerfile 中的所有命令都贡献于构建 quarkus/hello-okd 容器镜像的中间层:
Step 1/9 : FROM fabric8/java-alpine-openjdk8-jre
Trying to pull repository docker.io/fabric8/java-alpine-openjdk8-jre ...
sha256:b27090f384b30f0e3e29180438094011db1fa015bbf2e69decb921bc2486604f: Pulling from docker.io/fabric8/java-alpine-openjdk8-jre
9d48c3bd43c5: Pull complete
. . . . . . .
Status: Downloaded newer image for docker.io/fabric8/java-alpine-openjdk8-jre:latest
---> fe776eec30ad
Step 2/9 : ENV JAVA_OPTIONS "-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
---> Running in c5d31bae859e
---> 01c99aac17db
Removing intermediate container c5d31bae859e
Step 3/9 : ENV AB_ENABLED jmx_exporter
---> Running in c867300baaf0
---> 52deadd505bc
Removing intermediate container c867300baaf0
Step 4/9 : COPY target/lib/* /deployments/lib/
---> aa11b2b30f16
Removing intermediate container dcbd13a3ae0f
Step 5/9 : COPY target/*-runner.jar /deployments/app.jar
---> 2f2e1218eff8
Removing intermediate container 4b3861ba33d9
Step 6/9 : EXPOSE 8080
---> Running in 93eebaee5495
---> 4008a4fdbb9c
Removing intermediate container 93eebaee5495
Step 7/9 : RUN adduser -G root --no-create-home --disabled-password 1001 && chown -R 1001 /deployments && chmod -R "g+rwX" /deployments && chown -R 1001:root /deployments
---> Running in 2a86b3aeaeae
---> b21be209f09e
Removing intermediate container 2a86b3aeaeae
Step 8/9 : USER 1001
---> Running in fac8d64b8793
---> 94077bb5396a
Removing intermediate container fac8d64b8793
Step 9/9 : ENTRYPOINT /deployments/run-java.sh
---> Running in 7bacd02dd631
---> 9f269b2041d3
Removing intermediate container 7bacd02dd631
Successfully built 9f269b2041d3
现在,让我们通过执行 docker images 命令来检查镜像是否在你的本地 Docker 仓库中可用:
$ docker images | grep hello-okd
你应该看到以下输出:
quarkus/hello-okd latest 9f269b2041d3 2 minutes ago 98.9 MB
如你所见,本地缓存的镜像现在可用在你的本地 Docker 仓库中。你可以使用以下命令运行它:
$ docker run -i --rm -p 8080:8080 quarkus/hello-okd
在 run 命令中,我们包含了一些额外的标志,例如 --rm,它会在容器退出后自动删除容器。-i 标志将容器连接到终端。最后,-p 标志将端口 8080 外部暴露,从而映射到主机机器上的端口 8080。
由于我们将导出主机上的服务到端口,即 8080,请检查没有其他服务正在占用该端口!你应该能够在控制台收集此输出,这是一个代理启动的日志,底部是我们的 hello-okd 服务的日志:
exec java -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -javaagent:/opt/agent-bond/agent-bond.jar=jmx_exporter{{9779:/opt/agent-bond/jmx_exporter_config.yml}} -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40 -XX:+ExitOnOutOfMemoryError -cp . -jar /deployments/app.jar
2019-11-11 10:29:12,505 INFO [io.quarkus] (main) hello-okd 1.0-SNAPSHOT (running on Quarkus 1.0.0.Final) started in 0.666s. Listening on: http://0.0.0.0:8080
2019-11-11 10:29:12,525 INFO [io.quarkus] (main) Profile prod activated.
2019-11-11 10:29:12,525 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]
Docker 进程现在正在运行,可以通过以下命令进行确认。此命令将显示运行容器的Image名称:
$ docker ps --format '{{.Image}}'
以下是运行前面命令的输出:
quarkus/hello-okd
您可以使用以下命令测试应用程序是否在容器中运行:
$ curl http://localhost:8080/getContainerId
您应该能够在输出中看到与docker ps命令打印的相同容器 ID:
You are running on a333f52881a1
现在,让我们重新构建我们的容器镜像,以便我们可以使用原生可执行文件。
在容器中运行原生可执行进程
正如我们所见,Quarkus Maven 插件还生成了src/main/docker/Dockerfile.native,可以用作模板,这样我们就可以在容器中运行我们的原生可执行文件。以下是该文件的内容:
FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
由于我们不需要使用 JDK 层来启动我们的应用程序,我们的容器的基础层将是一个精简的 RHEL 镜像,称为ubi-minimal。
红帽通用基础镜像(UBI)是符合 OCI 规范的容器操作系统镜像,包括免费重新分发的附加运行时语言和其他包。
在构建 Docker 镜像之前,通过包含-Dnative-image.docker-build选项来打包您的应用程序:
$ mvn package -Pnative -Dnative-image.docker-build=true
检查构建是否成功,然后使用以下命令构建镜像:
$ docker build -f src/main/docker/Dockerfile.native -t quarkus/hello-okd-native .
在控制台中,您将看到容器将以与 Java 应用程序相同的方式创建,但使用不同的初始镜像(ubi-minimal):
Sending build context to Docker daemon 32.57 MB
Step 1/6 : FROM registry.access.redhat.com/ubi8/ubi-minimal
---> 8c980b20fbaa
Step 2/6 : WORKDIR /work/
---> Using cache
---> 0886c0b19e07
Step 3/6 : COPY target/*-runner /work/application
---> 7e66ae6447ce
Removing intermediate container 2ddc91992af5
Step 4/6 : RUN chmod 775 /work
---> Running in e8d6ffbbc14e
---> 780f6562417d
Removing intermediate container e8d6ffbbc14e
Step 5/6 : EXPOSE 8080
---> Running in d0d48475565f
---> 554f79b4cbb2
Removing intermediate container d0d48475565f
Step 6/6 : CMD ./application -Dquarkus.http.host=0.0.0.0
---> Running in e0206ff3971f
---> 33021bdaf4a4
Removing intermediate container e0206ff3971f
Successfully built 33021bdaf4a4
让我们检查该镜像是否在 Docker 仓库中可用:
$ docker images | grep hello-okd-native
您应该看到以下输出:
quarkus/hello-okd-native latest 33021bdaf4a4 59 seconds ago 113 MB
quarkus/hello-okd-native镜像现在可用。现在,使用以下命令运行容器镜像:
$ docker run -i --rm -p 8080:8080 quarkus/hello-okd-native
控制台不会显示额外的 JVM 层。在这里,我们可以看到我们的服务仅用了几毫秒就启动了:
2019-11-11 11:59:46,817 INFO [io.quarkus] (main) hello-okd 1.0-SNAPSHOT (running on Quarkus 1.0.0.CR1) started in 0.005s. Listening on: http://0.0.0.0:8080
2019-11-11 11:59:46,817 INFO [io.quarkus] (main) Profile prod activated.
2019-11-11 11:59:46,817 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]y]
验证当请求getContainerId URI 时,应用程序返回容器 ID:
curl http://localhost:8080/getContainerId
在我们的情况下,输出如下:
You are running on ff6574695d68
太棒了!我们刚刚成功地将一个原生应用程序作为 Docker 镜像运行。我们的下一个任务将是将我们的镜像部署到 Kubernetes 原生环境中。
在 Kubernetes 原生平台上部署 Quarkus 应用程序
现在我们已经验证了在容器中运行 Quarkus 应用程序是多么简单,我们将部署我们的应用程序到 Kubernetes 原生环境中。即使 Kubernetes 本身足以编排您的服务,您也可以通过安装 OpenShift 来极大地扩展其功能。除了利用 Kubernetes 功能外,OpenShift 还提供以下功能:
-
通过使用镜像流更好地管理容器镜像,它将实际镜像与应用程序解耦
-
高级 CI/CD 功能,使整个 CI/CD 工作流程变得更加容易,还包括一个 Jenkins 认证的镜像
-
更简单的构建过程,因为通过
BuildConfig组件在 OpenShift 内部构建 Docker 镜像更容易,该组件可以执行自动镜像构建并将它们推送到其内部仓库 -
丰富的经过认证的插件,例如存储/网络/监控插件
-
通过 资源调度器组件支持多租户,该组件将确定 Pod 的运行位置
-
一系列经过认证的数据库和中间件产品
-
一个更简单的 UI 网络应用程序,您可以从其中轻松管理您的服务集群并创建新应用程序
OpenShift 有几种版本:
-
Red Hat OpenShift 容器平台(需要订阅):这是一个受支持的 Kubernetes 平台,让您可以一致地在云和本地基础设施上构建、部署和管理基于容器的应用程序。
-
Red Hat OpenShift 专用(需要订阅):这提供了一种受支持的、私有的、高可用性的 Red Hat OpenShift 集群,托管在亚马逊网络服务或谷歌云平台上。
-
Red Hat OpenShift 在线(提供多种计划):它提供按需访问 Red Hat OpenShift,以便您可以管理容器化应用程序。
-
Kubernetes 的原始社区发行版(OKD):这是您可以在任何环境中自由使用的 Red Hat OpenShift 容器平台的社区版本。
为了本书的目的,我们将安装 Minishift,这是 OKD 的简化版本,以便在虚拟机内部启动单个节点集群。这是开始并尝试在本地机器上使用 OpenShift 的最简单方法。
Minishift 的当前版本基于 Openshift 的 3.x 版本发布。强烈建议迁移到 Openshift 4.x 平台,以便使用大多数高级示例,例如在本书最后一章讨论的基于云的响应式应用程序开发。
Minishift 的安装相当简单:您需要做的只是下载并解压缩其最新发行版。然而,确实存在一些先决条件,因为您需要通过安装虚拟机管理程序来准备您的系统,这是启动 OKD 提供的虚拟环境所必需的。
安装 Minishift
在本节中,我们将学习如何在运行 Fedora 的机器上安装 Minishift。如果您在机器上不运行 Fedora,您可以查看您操作系统的先决条件,请参阅docs.okd.io/latest/minishift/getting-started/preparing-to-install.html。
首先,您需要安装两个内核模块(libvirt 和 qemu**-**kvm),这些模块是管理各种虚拟化平台所需的。它们符合 基于内核的虚拟机(KVM)技术。按照以下步骤进行操作:
- 从 shell 中,执行以下命令:
$ sudo dnf install libvirt qemu-kvm
- 然后,为了使用您的用户运行虚拟化平台,将其添加到
libvirt组中:
$ sudo usermod -a -G libvirt $(whoami)
- 接下来,使用当前登录的用户配置组成员资格:
$ newgrp libvirt
- 最后,您需要下载并使 Docker 机器的 KVM 驱动程序可执行。作为 root 用户,执行以下命令:
$ sudo curl -L https://github.com/dhiltgen/docker-machine-kvm/releases/download/v0.10.0/docker-machine-driver-kvm-centos7 -o /usr/local/bin/docker-machine-driver-kvm
$ sudo chmod +x /usr/local/bin/docker-machine-driver-kvm
- 一旦您的用户设置完成,从官方 GitHub 仓库下载并解压最新的 Minishift 发布包:
github.com/minishift/minishift/releases。在撰写本文时,这是可以下载的 Minishift 的最新版本:
$ wget https://github.com/minishift/minishift/releases/download/v1.33.0/minishift-1.33.0-linux-amd64.tgz
- 下载完成后,将
.tar文件解压到目标文件夹中。例如,要将它解压到您的家目录(~),请执行以下命令:
$ tar xvf minishift-1.33.0-linux-amd64.tgz -C ~
在此包中,您将找到minishift可执行文件,可用于启动您的 Minishift 环境。
- 接下来,我们将运行
minishift命令以启动安装过程:
$ ./minishift start
- 完成后,您应该在终端中看到类似以下的消息:
-- Starting profile 'minishift'
-- Check if deprecated options are used ... OK
-- Checking if https://github.com is reachable ... OK
-- Checking if requested OpenShift version 'v3.11.0' is valid ... OK
-- Checking if requested OpenShift version 'v3.11.0' is supported ... OK
-- Checking if requested hypervisor 'kvm' is supported on this platform ... OK
-- Checking if KVM driver is installed ...
. . . .
OpenShift server started.
The server is accessible via web console at:
https://192.168.42.103:8443/console
The server is accessible at:
https://192.168.42.190:8443
You are logged in as: User: developer Password:
To log in as administrator:
oc login -u system:admin
就这样!Minishift 已安装到您的环境中!
建议您将以下文件夹包含在$PATH环境变量中:
-
您解压
minishift工具的文件夹。 -
存放
oc客户端工具的文件夹。此工具是一个命令行实用程序,您可以使用它来管理您的 Minishift 集群。一旦启动集群,此工具将被复制到~/.minishift/cache/oc/<oc-version>/linux。
因此,例如,如果您在家目录中解压了 Minishift,请将oc-version替换为您的工具版本,并执行以下命令:
export PATH=$PATH:~/minishift-1.33.0-linux-amd64:~/.minishift/cache/oc/<oc-version>/linux
您可以通过在默认浏览器中打开 OpenShift 网络控制台(在我们的案例中,https://192.168.42.190:8443)或通过将console参数传递给minishift工具来验证这一点:
$ minishift console
由于控制台在安全连接上运行,您将收到警告,表示在您的浏览器中未找到已签名的证书。向您的浏览器添加安全异常,以便您登录到登录页面:

使用developer/developer登录以进入仪表板:

恭喜!您已安装 Minishift 并验证了它。下一步将是将其部署到我们的示例应用程序上。
在 OKD 上构建和部署 Quarkus 应用程序
Minishift 的仪表板包含一组模板,可以快速构建我们的应用程序。在撰写本文时,还没有 Quarkus 模板;然而,我们可以轻松构建和部署我们的镜像,作为一个二进制构建,它传达了我们已经测试过的 Dockerfile。
二进制构建是一种功能,允许开发者从二进制源上传工件,而不是从 Git 仓库 URL 拉取源代码。
为了这个目的,我们将使用oc客户端工具,这是用于配置 OpenShift 及其对象的瑞士军刀。
以下命令集包含在本书 GitHub 仓库的Chapter03目录中的deploy-openshift.sh文件中。如果您迫不及待想看到您的应用程序在云中运行,只需执行脚本并检查输出是否与本文中所述相符。
我们首先需要做的是为我们的项目创建一个命名空间,该命名空间将在我们的当前 OpenShift 命名空间中创建。你可以使用以下命令创建quarkus-hello-okd命名空间:
$ oc new-project quarkus-hello-okd
我们首先需要做的是使用oc new-build命令定义一个二进制构建对象:
$ oc new-build --binary --name=quarkus-hello-okd -l app=quarkus-hello-okd
之前的命令将生成一个图像二进制构建,并将其推送到 Minishift 的内部注册表。以下输出描述了为此目的创建的资源:
* A Docker build using binary input will be created
* The resulting image will be pushed to image stream tag "quarkus-hello-okd:latest"
* A binary build was created, use 'start-build --from-dir' to trigger a new build
--> Creating resources with label app=quarkus-hello-okd ...
imagestream.image.openshift.io "quarkus-hello-okd" created
buildconfig.build.openshift.io "quarkus-hello-okd" created
--> Success
现在构建配置已经创建,我们可以通过查询bc别名(代表构建配置)来检查其可用性:
$ oc get bc
你应该看到以下输出:
NAME TYPE FROM LATEST
quarkus-hello-okd Docker Binary 0
就目前而言,二进制构建不包含对 Dockerfile 的任何引用。我们可以使用oc patch命令添加此信息,这是一个有用的快捷方式,我们可以用它来编辑资源。在我们的例子中,我们需要设置dockerfilePath属性,使其指向 Dockerfile 的位置。从你的 Quarkus 项目根目录开始,执行以下命令:
$ oc patch bc/quarkus-hello-okd -p '{"spec":{"strategy":{"dockerStrategy":{"dockerfilePath":"src/main/docker/Dockerfile.native"}}}}'
将返回以下输出:
buildconfig.build.openshift.io/quarkus-hello-okd patched
如果你检查二进制构建描述,你会看到 Dockerfile 路径已被包含:
$ oc describe bc/quarkus-hello-okd
输出有些冗长;然而,它应该包含以下信息:
Strategy: Docker
Dockerfile Path: src/main/docker/Dockerfile.native
现在,我们已经准备好开始构建过程,该过程将以项目根文件夹(.)作为输入,并将结果上传到你的 Minishift 环境。执行以下命令:
$ oc start-build quarkus-hello-okd --from-dir=. --follow
输出将通知你图像已被构建并推送到 Minishift 注册表:
Uploading finished
build.build.openshift.io/quarkus-hello-okd-1 started
Receiving source from STDIN as archive ...
Caching blobs under "/var/cache/blobs".
Pulling image registry.access.redhat.com/ubi8/ubi-minimal ...
. . . .
Writing manifest to image destination
Storing signatures
STEP 1: FROM registry.access.redhat.com/ubi8/ubi-minimal
STEP 2: WORKDIR /work/
2d94c8983e7ec259aa0e0207c66b0e48fdd9544f66e3c17724a10f838aaa50ab
STEP 3: COPY target/*-runner /work/application
a6d9d0a228023d29203c20de7bcfd9019de00f62b4a4c3fdc544648f8f61988a
STEP 4: RUN chmod 775 /work
e7bf7467d2191478f0cdccd9b480d10ebd19eeae254a7fa0608646cba28c5a97
STEP 5: EXPOSE 8080
29e6198d99e27508aa75cd073290f66fbca605b8ff95cbb6287b4ecbfc1807e5
STEP 6: CMD ["./application","-Dquarkus.http.host=0.0.0.0"]
6c44a4a542ea96450ed578afd4c1859564926405036f61fbbf2e3660660e5f5e
STEP 7: ENV "OPENSHIFT_BUILD_NAME"="quarkus-hello-okd-1" "OPENSHIFT_BUILD_NAMESPACE"="myproject"
9aa01e4bc2585a05b37e09a10940e326eec6cc998010736318bbe5ed1962503b
STEP 8: LABEL "io.openshift.build.name"="quarkus-hello-okd-1" "io.openshift.build.namespace"="myproject"
STEP 9: COMMIT temp.builder.openshift.io/myproject/quarkus-hello-okd-1:d5dafe08
5dafe14a20fffb50b151efbfc4871218a9b1a8516b618d5f4b3874a501d80bcd
Pushing image image-registry.openshift-image-registry.svc:5000/myproject/quarkus-hello-okd:latest ...
. . .
Successfully pushed image-registry.openshift-image-registry.svc:5000/myproject/quarkus-hello-okd@sha256:9fc48bb4b92081c415342407e8df41f38363a4bf82ad3a4319dddced19eff1b3
Push successful
作为概念验证,让我们检查默认项目中可用的图像流列表,使用其别名is:
$ oc get is
你应该看到以下输出:
NAME IMAGE
quarkus-hello-okd image-registry.openshift-image-registry.svc:5000/myproject/quarkus-hello-okd
你的ImageStream现在可用。我们只需创建一个使用ImageStream quarkus-hello-okd作为输入的应用程序。这可以通过以下命令完成:
$ oc new-app --image-stream=quarkus-hello-okd:latest
现在,将创建资源。这将通过以下输出得到确认:
--> Found image 5dafe14 (2 minutes old) in image stream "myproject/quarkus-hello-okd" under tag "latest" for "quarkus-hello-okd:latest"
Red Hat Universal Base Image 8 Minimal
--------------------------------------
The Universal Base Image Minimal is a stripped down image that uses microdnf as a package manager. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly.
Tags: minimal rhel8
* This image will be deployed in deployment config "quarkus
-hello-okd"
* Port 8080/tcp will be load balanced by service "quarkus
-hello-okd"
* Other containers can access this service through the hostname
"quarkus-hello-okd"
* WARNING: Image "myproject/quarkus-hello-okd:latest" runs as
the 'root' user which may not be permitted by your cluster
administrator
--> Creating resources ...
deploymentconfig.apps.openshift.io "quarkus-hello-okd" created
service "quarkus-hello-okd" created
--> Success
Application is not exposed. You can expose services to the
outside world by executing one or more of the commands below:
'oc expose svc/quarkus-hello-okd'
Run 'oc status' to view your app.
现在,我们的应用程序已准备好被使用。为了允许外部客户端访问它,我们需要通过路由对象将其公开,如下所示:
$ oc expose svc/quarkus-hello-okd
路由将被公开,并显示以下日志:
route.route.openshift.io/quarkus-hello-okd exposed
我们可以使用以下命令验证路由地址,该命令使用 JSON 模板显示quarkus-hello-okd路由的虚拟主机地址:
$ oc get route quarkus-hello-okd -o jsonpath --template="{.spec.host}"
在我们的例子中,路由可通过以下地址访问:
quarkus-hello-okd-myproject.192.168.42.5.nip.io
请注意,实际的路由 IP 地址是由虚拟机管理程序根据你的网络配置确定的,所以如果它与示例中暴露的地址不同,请不要感到惊讶。
你应该能够从 Web 控制台确认此信息,这表明应用程序正在运行,并且已启动一个 Pod:

如果你访问分配给你的路由主机/端口(在我们的例子中,http://quarkus-hello-okd-myproject.192.168.42.5.nip.io),你会看到以下欢迎屏幕:

这是一个简单的静态页面,它已经被包含在 src/main/resources/META-INF/resources/index.html 中,以显示你的应用程序是可用的,并包含一些有关你可以放置静态资源和配置的有用信息。另一方面,你的 REST 服务仍然可以通过 REST URI 访问:
$ curl quarkus-hello-okd-myproject.192.168.42.5.nip.io/getContainerId
由于应用程序正在运行在 quarkus-hello-okd-1-84xwq Pod 上,预期的输出如下:
You are running on quarkus-hello-okd-1-7k2t8
现在,让我们学习如何通过添加一些应用程序的副本来扩展我们的 Quarkus 服务。
扩展我们的 Quarkus 服务
到目前为止,你已经学会了如何在 Minishift 上部署 Quarkus 应用程序。该应用程序在一个 Pod 中运行,它分配了自己的内部 IP 地址,相当于运行容器的机器。在我们的例子中,应用程序在一个 OpenShift 节点的一个 Pod 上运行。这足以保证我们应用程序的可用性,因为一些存活性和就绪性探针会定期执行。如果你的 Pods 停止响应,OpenShift 平台将自动重启它们。
另一方面,你的应用程序可能需要满足一个最低吞吐量。除非请求量相当低,否则通常无法仅通过一个 Pod 来满足这一要求。在这种情况下,最简单的策略是水平 Pod 扩缩,这将提高在请求到达路由器时自动平衡的可用资源数量。
在扩展我们的应用程序之前,我们需要为它定义一个上限内存限制,以减少它对集群在系统资源方面的冲击。由于我们的 Quarkus 应用程序不需要大量的内存,我们将设置 50 MB 作为上限,这相当合理,并且肯定比一个平均的 Java 应用程序要薄。
执行以下命令将内存限制设置为 50 MB。这将更新你应用程序的 部署配置:
$ oc set resources dc/quarkus-hello-okd --limits=memory=50M
部署配置(在命令行中的别名简单为 dc)描述了应用程序特定组件的状态,作为一个 Pod 模板。当你更新部署配置时,会发生部署过程,以缩小应用程序的规模,并使用新的部署配置和新的应用程序复制控制器来扩大规模。
应该返回以下输出:
deploymentconfig.apps.openshift.io/quarkus-hello-okd resource requirements updated
作为概念验证,你可以通过 describe 命令验证部署配置:
$ oc describe dc/quarkus-hello-okd
describe 命令的输出有点冗长;然而,你应该能够在 Limits 部分看到以下设置:
Limits:
memory: 50M
现在,让我们将我们的应用程序扩展到 10 个实例。这将会非常快,因为我们已经为每个 Pod 消耗的资源设置了内存限制:
$ oc scale --replicas=10 dc/quarkus-hello-okd
以下是我们预期的输出:
deploymentconfig.apps.openshift.io/quarkus-hello-okd scaled
转到 Web 控制台,在概览面板中,我们将看到我们的应用程序已扩展到 10 个 Pod:

现在我们有大量的可用 Pod,让我们尝试对我们的应用程序进行负载测试:
for i in {1..100}; do curl quarkus-hello-okd-myproject.192.168.42.5.nip.io/getContainerId ; echo ""; done;
现在,你应该能够在你的控制台中看到由 REST 应用程序产生的响应。这显示了执行请求的 Pod 的 ID(输出已被截断以节省空间):
You are running on quarkus-hello-okd-2-jzvp2\n
You are running on quarkus-hello-okd-2-fc7h9\n
You are running on quarkus-hello-okd-2-lj67f\n
You are running on quarkus-hello-okd-2-qwm9j\n
You are running on quarkus-hello-okd-2-n6kn6\n
You are running on quarkus-hello-okd-2-bbk84\n
You are running on quarkus-hello-okd-2-d5bj6\n
You are running on quarkus-hello-okd-2-skc2h\n
You are running on quarkus-hello-okd-2-bw5f9\n
You are running on quarkus-hello-okd-2-p24jl\n
You are running on quarkus-hello-okd-2-jzvp2\n
...
尽管测量我们应用程序的性能超出了本书的范围,但你仍然可以继续测量在相同集群中运行等效 Java 应用程序所需的时间。你将注意到在时间和内存消耗方面有不同的响应!
那是我们本章的最后一个任务。当你完成这个示例,并想要清理我们在项目中创建的资源时,只需执行以下命令,这将执行资源的批量清理:
oc delete all --all
输出可能会根据可用 Pod 的数量而变化。然而,它应该类似于以下内容:
pod "quarkus-hello-okd-1-7k2t8" deleted
pod "quarkus-hello-okd-1-build" deleted
pod "quarkus-hello-okd-1-deploy" deleted
replicationcontroller "quarkus-hello-okd-1" deleted
service "quarkus-hello-okd" deleted
deploymentconfig.apps.openshift.io "quarkus-hello-okd" deleted
buildconfig.build.openshift.io "quarkus-hello-okd" deleted
imagestream.image.openshift.io "quarkus-hello-okd" deleted
route.route.openshift.io "quarkus-hello-okd" deleted
前面的日志确认所有已删除的资源都已成功驱逐。
摘要
在本章中,我们在 Docker 容器中运行了一个简单的 REST 应用程序,然后在 Kubernetes 原生环境中运行它,即 Minishift。我们看到了如何通过利用 OKD 集成发行版的特性,使我们的应用程序具有高可用性和良好的吞吐量。
现在,是时候给我们的应用程序添加更多功能了。在下一章中,我们将学习如何配置 Undertow 扩展,它可以添加到我们的应用程序中提供 Web 服务器功能。它还包括一些 UI 资产,我们将在简要介绍中查看。
第二部分:使用 Quarkus 构建应用程序
在本节中,我们将学习如何使用 Quarkus 的所有核心扩展,学习如何利用 Hibernate ORM/Panache,并查看 MicroProfile API 和安全问题。
本节包括以下章节:
-
第四章,为 Quarkus 服务添加 Web 接口
-
第五章,使用 Quarkus 管理数据持久性
-
第六章,使用 MicroProfile API 构建应用程序
-
第七章,保护应用程序
第四章:将网络界面添加到 Quarkus 服务中
到目前为止,我们已经学习了如何使用 Quarkus 构建一个简单的 REST 应用程序,并介绍了构建、测试和部署我们的应用程序到 Kubernetes 环境中所需采取的操作。
我们可以在这里停下来,对我们所取得的成就感到满意;然而,还有许多里程碑需要达到。例如,我们还没有使用任何网络界面来访问 Quarkus 服务。正如您在本章中将要看到的,Quarkus 具有一些扩展,允许我们重用标准的企业级 API,如 Servlet 和 WebSocket。同时,您可以使用更轻量级的 JavaScript/HTML 5 框架作为服务的用户界面。我们将在本章中探讨这两种方法。
在本章中,我们将涵盖以下主题:
-
将网页内容添加到 Quarkus 应用程序中
-
在 Minishift 上运行我们的应用程序
-
将企业级网络组件(如 Servlet 和 WebSocket)添加到我们的应用程序中
技术要求
您可以在 GitHub 上找到本章项目的源代码,链接为 github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter04。
将网页内容添加到 Quarkus 应用程序中
在我们讨论的示例中,我们通过添加 RESTful 服务来测试了 Quarkus 的网络服务器功能。在底层,Quarkus 使用以下核心组件来处理网络请求:
-
Vert.x 网络服务器:它是 Quarkus 中提供 RESTful 服务以及实时(服务器推送)网络应用的核心网络组件。我们将在本书的第九章 统一命令式和响应式编程的 Vert.x 中更详细地讨论 Vert.x。
-
Undertow 网络服务器:它是一个灵活的产品,通过组合不同的小型单一用途处理器构建而成,在 Quarkus 中用于交付
WebSocket应用程序时发挥作用。
如前所述,我们可以通过将静态网页内容(HTML、JavaScript、图像)包含在项目的 resources/META-INF/resources 文件夹下,将静态网页内容添加到我们的应用程序中。在微服务风格的程序中拥有静态网页内容的目的是什么?实际上,静态内容可以在多个上下文中使用,包括微服务。例如,我们可以为服务本身提供辅助页面。我们还可以将 Quarkus 与现有的框架(如 Swagger UI)混合使用,以测试我们的 REST 端点,而无需编写复杂用户界面。
在这个前提下,我们将演示如何构建一个使用 JSON 消费和生成数据的 创建、读取、更新、删除(CRUD)应用程序。然后,我们将通过一个基于 JavaScript 的网络框架构建的轻量级网络界面来丰富我们的应用程序。
构建 CRUD 应用程序
在本章的 GitHub 源文件夹中,您将找到两个示例。第一个位于 Chapter04/customer-service/basic 文件夹中,将在本节中讨论。我们建议在继续之前将项目导入到您的 IDE 中。
如果您查看项目结构,您将看到它由三个主要组件组成:
- 首先,有一个模型类用于记录客户条目:
package com.packt.quarkus.chapter4;
public class Customer {
private Integer id;
private String name;
private String surname;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
}
Customer 类是 Customer 记录的最小定义。它被定义为应该存储在内存中的普通旧 Java 对象。
- 接下来,看一下
CustomerRepository类,它包含我们将用于管理我们的模型的核心功能:
package com.packt.quarkus.chapter4;
import javax.enterprise.context.ApplicationScoped;
import java.util.ArrayList;
import java.util.List;
@ApplicationScoped
public class CustomerRepository {
List<Customer> customerList = new ArrayList();
int counter;
public int getNextCustomerId() {
return counter++;
}
public List<Customer> findAll() {
return customerList;
}
public Customer findCustomerById(Integer id) {
for (Customer c:customerList) {
if (c.getId().equals(id)) {
return c;
}
}
throw new CustomerException("Customer not found!");
}
public void updateCustomer(Customer customer) {
Customer customerToUpdate =
findCustomerById(customer.getId());
customerToUpdate.setName(customer.getName());
customerToUpdate.setSurname(customer.getSurname());
}
public void createCustomer(Customer customer) {
customer.setId(getNextCustomerId());
findAll().add(customer);
}
public void deleteCustomer(Integer customerId) {
Customer c = findCustomerById(customerId);
findAll().remove(c);
}
}
如您所见,它只是一个简单的存储和检索我们数据的模式,作为存储和检索数据的模板。在接下来的章节中,我们将添加其他功能,如持久存储和异步行为。因此,从无服务器的示例开始是很好的。
- 通过
CustomerEndpoint类完成客户服务的实现,其实现如下:
package com.packt.quarkus.chapter4;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.util.List;
@Path("customers")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
public class CustomerEndpoint {
@Inject CustomerRepository customerRepository;
@GET
public List<Customer> getAll() {
return customerRepository.findAll();
}
@POST
public Response create(Customer customer) {
customerRepository.createCustomer(customer);
return Response.status(201).build();
}
@PUT
public Response update(Customer customer) {
customerRepository.updateCustomer(customer);
return Response.status(204).build();
}
@DELETE
public Response delete(@QueryParam("id") Integer customerId) {
customerRepository.deleteCustomer(customerId);
return Response.status(204).build();
}
}
如您所见,CustomerEndpoint 是在 CustomerRepository 类之上的一个薄薄的 REST 层,并为每个 CRUD 操作包含一个方法,其中它将每个操作映射到适当的 HTTP 方法。当使用这种方法时,对于整个应用程序来说,只需要一个 REST 路径(/customers)就足够了,因为 REST 引擎将根据 HTTP 请求方法调用适当的方法。
为我们的客户服务添加用户界面
正如我们在 第一章 中提到的,Quarkus 核心概念简介,您可以在 src/main/resources/META-INF/resources 文件夹中包含静态资源,如 HTML 页面、JavaScript、CSS 或图像。index.html 页面作为我们的项目中的一个标记提供,如图所示的项目层次结构:
$ tree src
src
├── main
│ ├── docker
│ ├── java
│ │ └── com
│ │ └── packt
│ │ └── quarkus
│ │ └── chapter4
│ │ ├── CustomerEndpoint.java
│ │ ├── Customer.java
│ │ ├── CustomerRepository.java
│ └── resources
│ ├── application.properties
│ └── META-INF
│ └── resources
│ ├── index.html
为了连接到我们的 REST 端点,我们将在 index.html 页面的头部部分包含一个名为 AngularJS 的 JavaScript 框架和一些 CSS 样式:
<link rel="stylesheet" type="text/css" href="stylesheet.css" media="screen" />
<script src="img/angular.min.js"></script>
此外,在 index.html 页面的头部部分,我们将包含 AngularJS 控制器,其中包含一个我们可以用来访问 REST 端点方法的函数。我们将传递 HTML 表单数据作为参数,我们将在下一节中讨论:
<script type="text/javascript">
var app = angular.module("customerManagement", []);
angular.module('customerManagement').constant('SERVER_URL',
'/customers');
//Controller Part
app.controller("customerManagementController", function
($scope, $http, SERVER_URL) {
//Initialize page with default data which is blank in this
//example
$scope.customers = [];
$scope.form = {
id: -1,
name: "",
surname: ""
};
//Now load the data from server
_refreshPageData();
//HTTP POST/PUT methods for add/edit customers
$scope.update = function () {
var method = "";
var url = "";
var data = {};
if ($scope.form.id == -1) {
//Id is absent so add customers - POST operation
method = "POST";
url = SERVER_URL;
data.name = $scope.form.name;
data.surname = $scope.form.surname;
} else {
//If Id is present, it's edit operation - PUT operation
method = "PUT";
url = SERVER_URL;
data.id = $scope.form.id;
data.name = $scope.form.name;
data.surname = $scope.form.surname;
}
$http({
method: method,
url: url,
data: angular.toJson(data),
headers: {
'Content-Type': 'application/json'
}
}).then(_success, _error);
};
//HTTP DELETE- delete customer by id
$scope.remove = function (customer) {
$http({
method: 'DELETE',
url: SERVER_URL+'?id='+customer.id
}).then(_success, _error);
};
//In case of edit customers, populate form with customer
// data
$scope.edit = function (customer) {
$scope.form.name = customer.name;
$scope.form.surname = customer.surname;
$scope.form.id = customer.id;
};
/* Private Methods */
//HTTP GET- get all customers collection
function _refreshPageData() {
$http({
method: 'GET',
url: SERVER_URL
}).then(function successCallback(response) {
$scope.customers = response.data;
}, function errorCallback(response) {
console.log(response.statusText);
});
}
function _success(response) {
_refreshPageData();
_clearForm()
}
function _error(response) {
alert(response.data.message || response.statusText);
}
//Clear the form
function _clearForm() {
$scope.form.name = "";
$scope.form.surname = "";
$scope.form.id = -1;
}
});
</script>
</head>
AngularJS 的深入讨论超出了本书的范围;然而,简而言之,Angular 应用程序依赖于控制器来管理它们的数据流。每个控制器都接受 $scope 作为参数。该参数指的是控制器需要处理的模块或应用程序。
我们控制器的目的是使用不同的 HTTP 方法(GET、POST、PUT 和 DELETE)来访问我们的 REST 应用程序。
index.html 页面的最后一部分包含表单数据,可用于插入新客户和编辑现有客户:
<body ng-app="customerManagement" ng-controller="customerManagementController">
<div class="divTable blueTable">
<h1>Quarkus CRUD Example</h1>
<h2>Enter Customer:</h2>
<form ng-submit="update()">
<div class="divTableRow">
<div class="divTableCell">Name:</div>
<div class="divTableCell"><input type="text"
placeholder="Name" ng-model=
"form.name" size="60"/></div>
</div>
<div class="divTableRow">
<div class="divTableCell">Surname:</div>
<div class="divTableCell"><input type="text"
placeholder="Surname" ng-model="form.surname"
size="60"/>
</div>
</div>
<input type="submit" value="Save"/>
</form>
<div class="divTable blueTable">
<div class="divTableHeading">
<div class="divTableHead">Customer Name</div>
<div class="divTableHead">Customer Address</div>
<div class="divTableHead">Action</div>
</div>
<div class="divTableRow" ng-repeat="customer in customers">
<div class="divTableCell">{{ customer.name }}</div>
<div class="divTableCell">{{ customer.surname }}</div>
<div class="divTableCell"><a ng-click="edit( customer )"
class="myButton">Edit</a>
<a ng-click="remove( customer )"
class="myButton">Remove</a></div>
</div>
</div>
</body>
</html>
现在我们完成了 index.html 页面,我们可以为我们的应用程序编写一个测试类。
测试我们的应用程序
在测试我们的应用程序之前,值得一提的是,为了通过 REST 端点生成 JSON 内容以及在实际测试类中程序化创建 JSON 对象,我们已经在这个项目中包含了quarkus-jsonb依赖项。以下是我们已在pom.xml文件中包含的依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb</artifactId>
</dependency>
以下是我们CustomerEndpointTest类,它可以用来验证Customer应用程序:
@QuarkusTest
public class CustomerEndpointTest {
@Test
public void testCustomerService() {
JsonObject obj = Json.createObjectBuilder()
.add("name", "John")
.add("surname", "Smith").build();
// Test POST
given()
.contentType("application/json")
.body(obj.toString())
.when()
.post("/customers")
.then()
.statusCode(201);
// Test GET
given()
.when().get("/customers")
.then()
.statusCode(200)
.body(containsString("John"),
containsString("Smith"));
obj = Json.createObjectBuilder()
.add("id", "0")
.add("name", "Donald")
.add("surname", "Duck").build();
// Test PUT
given()
.contentType("application/json")
.body(obj.toString())
.when()
.put("/customers")
.then()
.statusCode(204);
// Test DELETE
given()
.contentType("application/json")
.when()
.delete("/customers?id=0")
.then()
.statusCode(204);
}
}
让我们转换一下思路,更仔细地看看测试类。这里的大部分内容你应该都很熟悉,除了Json.createObjectBuilder API,这是一个方便的工厂方法,我们可以用它流畅地创建 JSON 对象。在我们的代码中,我们用它来生成两个javax.json.JsonObject实例。第一个已经被序列化为字符串并通过 HTTP POST调用发送到我们的CustomerEndpoint。第二个被用来通过 HTTP PUT调用更新客户。
你可以使用以下命令打包和测试应用程序:
$ mvn package
输出将显示测试结果,应该成功:
[INFO] Results:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
testCustomerService 方法成功完成。现在我们有一个经过测试的 REST 应用程序,我们将学习如何让我们的应用程序在浏览器中运行。
运行示例
现在项目已经完全在我们手中,让我们看看它的实际效果!你可以使用以下命令启动应用程序:
$ mvn quarkus:dev
然后,转到主页http://localhost:8080。你应该能够看到以下 UI,其中你可以添加新的客户:

正如你所知,嵌入的 Vert.x 服务器将在根上下文中提供服务。如果你想改变这一点,你可以在application.properties中配置quarkus.http.root-path键来设置上下文路径。
一旦你有一些数据,其他操作(如编辑和删除)将可用:

太棒了!你可以尝试编辑和删除数据来验证所有 REST 方法是否正常工作。现在,我们将学习如何在 Minishift 上部署我们的应用程序。
在 Minishift 上运行我们的应用程序
按照常规启动你的 Minishift 环境,并执行以下命令来构建应用程序的本地可执行 Docker 镜像,并将其部署到一个 Pod 中:
$ mvn package -Pnative -Dnative-image.docker-build=true
构建应用程序的本地镜像需要大约一分钟的时间。接下来,我们将作为二进制构建将应用程序上传到 Minishift 命名空间。你应该已经熟悉这些步骤,所以我们只包括要执行的脚本,以及一些内联注释。执行每一行,并验证所有命令的输出是否成功:
#Create a new Project named quarkus-customer-service
$ oc new-project quarkus-customer-service
# Binary Build definition
$ oc new-build --binary --name=quarkus-customer-service -l app=quarkus-customer-service
# Add the dockerfilePath location to our Binary Build
$ oc patch bc/quarkus-customer-service -p '{"spec":{"strategy":{"dockerStrategy":{"dockerfilePath":"src/main/docker/Dockerfile.native"}}}}'
# Uploading directory "." as binary input for the build
$ oc start-build quarkus-customer-service --from-dir=. --follow
# Create a new application using as source the Binary Build
$ oc new-app --image-stream=quarkus-customer-service:latest
# Create a Route for external clients
$ oc expose svc/quarkus-customer-service
现在,您应该能够在概述面板中看到您的应用程序正在运行的 Pod,该面板可以通过“路由 - 外部流量”链接访问:http://quarkus-customer-service-quarkus-customer-service.192.168.42.53.nip.io(实际的路由地址取决于分配给您环境的 IP 地址):

点击“路由 - 外部流量”链接后,您将能够验证您的应用程序是否在 Kubernetes 环境中正常工作,就像您的本地副本一样。
在 Quarkus 中配置跨源资源共享
在这一章中,我们使用 JavaScript 将请求驱动到 Quarkus 的服务中。在更复杂的场景中,您的 JavaScript 代码部署在其自己的服务上,位于不同的主机或上下文中,您将需要实现 跨源资源共享(CORS)以使其工作。简而言之,CORS 允许 Web 客户端向托管在不同源上的服务器发送 HTTP 请求。通过 源,我们指的是 URI 方案、主机名和端口号的组合。
这对于客户端语言,如 JavaScript,尤其具有挑战性,因为所有现代浏览器都要求脚本语言遵循同源策略。
要使这生效,我们需要让我们的服务器应用程序负责决定谁可以发起请求以及允许哪些类型的请求,这通过使用 HTTP 头部来实现。在实践中,当服务器从不同的源接收到请求时,它可以回复并声明哪些客户端被允许访问 API,哪些 HTTP 方法或头部被允许,以及最后是否允许在请求中包含 cookies。
这如何转化为 Quarkus 配置?正如您可能猜到的,配置必须应用于 application.properties 文件中的 quarkus.http.cors 命名空间下。以下是一个允许所有域名、所有 HTTP 方法以及所有常见头部的示例配置:
quarkus.http.cors=true
quarkus.http.cors.origins=*
quarkus.http.cors.methods=GET,PUT,POST,DELETE, OPTIONS
quarkus.http.cors.headers=X-Custom,accept, authorization, content-type, x-requested-with
quarkus.http.cors.exposed-headers=Content-Disposition
在现实世界的场景中,您可能会将允许的源列表设置为请求远程连接的域名,如下所示:
quarkus.http.cors.origins=http://custom.origin.com
既然我们已经澄清了这一点,我们可以看看另一个示例,我们将使用 Java 企业组件,如 WebSocket,来访问我们的 Quarkus 服务。
添加企业级 Web 组件
在我们的客户服务示例中,前端应用程序使用 JavaScript 结构化框架(AngularJS)来测试我们的应用程序。现在,我们将考虑一个不同的用例:一个新的外部服务将使用不同的协议栈连接到我们的应用程序。除了 JAX-RS 端点外,Quarkus 还原生支持在嵌入式 Undertow Web 服务器上运行的 WebSocket 技术。因此,在这个示例中,我们将向现有的应用程序添加一个 WebSocket 端点。这将与另一个在不同应用程序中运行的 WebSocket 配对。
介绍 WebSockets
首先,让我们简要介绍我们应用程序的新组件。根据其企业规范,WebSocket是一个 API,它在一个浏览器和服务器端点之间建立套接字连接。由于客户端和服务器之间有持久的连接,双方可以随时开始发送数据,所以它与标准 TCP 套接字非常相似。
通常,你只需在 JavaScript 代码中调用WebSocket构造函数来打开一个WebSocket连接:
var connection = new WebSocket('ws://localhost:8080/hello');
注意WebSocket连接的 URL 模式(ws:)。我们还有wss:用于安全的WebSocket连接,它以与https:相同的方式用于安全的 HTTP 连接。
我们可以将一些事件处理器附加到连接上,以帮助我们确定连接状态是打开的、正在接收消息,还是发生错误。
我们可以通过在服务器端使用@ServerEndpoint注解来声明一个 Java 类WebSocket服务器端点。端点部署的 URI 也需要指定,如下面的示例所示:
@ServerEndpoint(value = "/hello")
public class WebSocketEndpoint {
@OnOpen
public void onOpen(Session session) throws IOException {
// Establish connection
}
@OnMessage
public void onMessage(Session session, Message message) throws
IOException {
// Handle Websocket messages
}
@OnClose
public void onClose(Session session) throws IOException {
}
@OnError
public void onError(Session session, Throwable throwable) {
}
}
在下一节中,我们将向我们的现有项目添加一个WebSocket层,然后创建另一个轻量级项目以远程访问WebSocket并添加新客户。
构建使用 Websockets 的项目
你将在本书 GitHub 仓库的Chapter04/customer-service/websockets文件夹中找到两个不同的项目:
-
随附
WebSocket端点的更新版customer-service项目 -
一个名为
customer-service-fe的项目,它为我们的WebSocket应用程序提供了一个最小化的 JavaScript 前端
在继续之前,你应该将这两个项目导入到你的集成开发环境(IDE)中。
首先,让我们讨论一下customer-service项目。我们添加的主要增强功能是一个WebSocket端点,该端点负责插入新的客户(使用CustomerRepository组件)并返回我们客户的表格视图。以下是WebsocketEndpoint类的代码内容:
@ServerEndpoint(value="/customers", encoders = {MessageEncoder.class})
public class WebsocketEndpoint {
@Inject
CustomerRepository customerRepository;
public List<Customer> addCustomer(String message, Session
session) {
Jsonb jsonb = JsonbBuilder.create();
Customer customer = jsonb.fromJson(message, Customer.class);
customerRepository.createCustomer(customer);
return customerRepository.findAll();
}
@OnOpen
public void myOnOpen(Session session) {
System.out.println("WebSocket opened: " + session.getId());
}
@OnClose
public void myOnClose(CloseReason reason) {
System.out.println("Closing a due to " +
reason.getReasonPhrase());
}
@OnError
public void error(Throwable t) {
}
}
这里有两个需要注意的地方,如下所示:
-
带有
@OnMessage注解的方法接收以 JSON 格式输入要添加的客户,并返回更新后的客户列表。 -
这个类使用一个编码器来定制返回给客户端的消息。编码器接收一个 Java 对象并产生其序列化表示,然后可以将其传输到客户端。例如,编码器通常负责生成 JSON、XML 和二进制表示。在我们的例子中,它以 JSON 格式编码客户列表。
现在,让我们看一下MessageEncoder类:
public class MessageEncoder implements Encoder.Text<java.util.List<Customer>> {
@Override
public String encode(List<Customer> list) throws EncodeException {
JsonArrayBuilder jsonArray = Json.createArrayBuilder();
for(Customer c : list) {
jsonArray.add(Json.createObjectBuilder()
.add("Name", c.getName())
.add("Surname", c.getSurname()));
}
JsonArray array = jsonArray.build();
StringWriter buffer = new StringWriter();
Json.createWriter(buffer).writeArray(array);
return buffer.toString();
}
@Override
public void init(EndpointConfig config) {
System.out.println("Init");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
如你所见,一个Encoder必须实现以下接口之一:
-
Encoder.Text<T>用于文本消息 -
Encoder.Binary<T>用于二进制消息
在我们的例子中,List<Customer>作为泛型类型在encode方法中接收,并转换为 JSON 字符串数组。
要进行编译,我们的项目需要quarkus-undertow-websockets扩展,这可以通过手动将其添加到pom.xml文件中来实现。或者,您可以使用以下命令让 Maven 插件为您完成:
$ mvn quarkus:add-extension -Dextensions="quarkus-undertow-websockets"
您将在控制台看到以下输出,这确认了扩展已被添加到我们的配置中:
Adding extension io.quarkus:quarkus-undertow-websockets
服务器项目现在已完成。您可以使用以下命令编译和运行它:
$ mvn compile quarkus:dev
现在,让我们创建一个新的前端项目,它包含一个瘦的WebSocketJavaScript 客户端。
创建 WebSocket 客户端项目
WebSocket客户端,就像它们的服务器对应物一样,可以用许多不同的语言编写。由于现代浏览器对WebSocket有原生支持,我们将编写一个简单的 JavaScript 客户端,这样我们就不需要安装任何额外的工具或 SDK 来运行我们的示例。
在customer-service-fe文件夹中,您将找到可以用来访问我们的WebSocket示例的前端项目。
我们的项目包含一个名为index.html的着陆页,当请求我们应用程序的根 Web 上下文时,将提供此页面。在这个页面中,我们包含了一个 HTML 表单和一个表格来显示客户列表:
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=ISO-
8859-1">
<link rel="stylesheet" type="text/css" href="stylesheet.css"
media="screen" />
<script src="img/jquery.min.js"></script>
<script src="img/functions.js"></script>
</head>
<meta charset="utf-8">
<body>
<h1 style="text-align: center;">Connect to Quarkus Websocket Endpoint</h1>
<br>
<div>
<form id="form1" action="">
<div><h3>Enter Customer</h3></div>
<div class="divTableRow">
<div class="divTableCell">Name:</div>
<div class="divTableCell"><input type="text"
placeholder="Name" name="name" size="60"/></div>
</div>
<div class="divTableRow">
<div class="divTableCell">Surname:</div>
<div class="divTableCell"><input type="text"
placeholder="Surname" name="surname"
size="60"/></div>
</div>
<br/>
<input onclick="send_message()" value="Insert" type="button"
class="myButton">
</form>
<br/>
</div>
<table id="customerDataTable" class="blueTable" />
<div id="output"></div>
</body>
</html>
WebSocket端点的连接发生在名为function.js的外部 JavaScript 文件中(您可以在本书 GitHub 仓库的customer-service-fe/src/main/resources/META-INF/resources文件夹中找到此文件)。以下是该文件的内容:
var wsUri = "ws://localhost:8080/customers";
function init() {
output = document.getElementById("output");
}
function send_message() {
websocket = new WebSocket(wsUri);
websocket.onopen = function(evt) {
onOpen(evt)
};
websocket.onmessage = function(evt) {
onMessage(evt)
};
websocket.onerror = function(evt) {
onError(evt)
};
}
function onOpen(evt) {
doSend(name.value);
}
function onMessage(evt) {
buildHtmlTable('#customerDataTable', evt.data);
}
function onError(evt) {
writeToScreen('<span style="color: red;">ERROR:</span>
' + evt.data);
}
function doSend(message) {
var json = toJSONString(document.getElementById("form1"));
websocket.send(json);
}
如您所见,一旦建立连接,就有几个回调方法(onOpen、onMessage、onError)与服务器事件相关联。在这里,我们将通过doSend方法添加一个新的客户,该客户以 JSON 字符串序列化,而onMessage回调方法将接收由我们的WebSocket编码器生成的客户列表。这些数据最终将包含在一个 HTML 表格中。
您可以使用以下命令运行项目:
$ mvn compile quarkus:dev -Dquarkus.http.port=9080 -Ddebug=6005
如您所见,我们将 HTTP 和调试端口移动了1000个偏移量,以避免与customer-service项目冲突。
浏览到http://localhost:9080将带您进入WebSocket客户端应用程序。添加一些示例数据以验证客户是否可以包含在表格中:

验证相同的数据是否也显示在可用的 AngularJS 前端中,该前端位于http://localhost:8080。
添加 AJAX 处理程序
当涉及到测试我们的WebSocket示例时,我们的 JavaScript 客户端是必需的。然而,您在这个项目中还会发现一个增强功能,那就是一个 Java Servlet,它将允许您删除任何硬编码的后端链接,这样在将示例移动到不同的机器或端口时,两个服务仍然可以通信。
以下 Servlet 通过使用名为CUSTOMER_SERVICE的环境变量和字符串ws://localhost:8080/customers来确定服务器端点信息:
@WebServlet("/AjaxHandler")
public class AjaxHandler extends HttpServlet {
public AjaxHandler() {
super();
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
String endpoint = System.getenv("CUSTOMER_SERVICE")
!= null ? System.getenv("CUSTOMER_SERVICE") :
"ws://localhost:8080/customers";
PrintWriter out = response.getWriter();
out.println(endpoint);
out.flush();
}
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws
ServletException, IOException {
doGet(request, response);
}
}
此更改需要在我们的 JavaScript 客户端中反映出来,以便它不使用硬编码的端点来访问我们的 WebSocket。在 function.js 文件的最终版本中,你会找到一个以下 JavaScript 函数,该函数通过 AJAX 查询我们的 Servlet:
var wsUri = "";
function callAjax() {
httpRequest = new XMLHttpRequest();
if (!httpRequest) {
console.log('Unable to create XMLHTTP instance');
return false;
}
httpRequest.open('GET', 'AjaxHandler');
httpRequest.responseType = 'text';
httpRequest.send();
httpRequest.onreadystatechange = function() {
if (httpRequest.readyState === XMLHttpRequest.DONE) {
if (httpRequest.status === 200) {
wsUri = httpRequest.response;
} else {
console.log('Something went wrong..!!');
}
}
}
}
当 HTML 页面加载时,此函数会被调用:
<body onload="callAjax()">
现在,从相同的 shell 启动服务器,以便它读取环境变量:
$ mvn quarkus:dev
现在,转到 http://localhost:9080 并验证由 WebSocket 请求产生的输出是否与服务器端点地址静态定义时相同。
你可以通过在 customer-service 应用程序中更改 quarkus.http.port 来将此示例进一步扩展。例如,你可以将其设置为 8888:
$ mvn quarkus:dev -Dquarkus.http.port=8888
customer-service-fe 将能够连接到 WebSocket 端点,一旦你相应地设置了 CUSTOMER_SERVICE 环境变量:
$ export CUSTOMER_SERVICE=ws://localhost:8888/customers
太好了!在本节中,我们从客户端应用程序中移除了任何静态硬编码的信息,现在它使用环境变量来联系客户服务。
摘要
在本章中,我们探讨了我们可以采取的不同路径来将网络内容添加到我们的 Quarkus 应用程序中。首先,我们学习了如何创建一个 CRUD 内存应用程序来管理一组 Java 对象。然后,该示例应用程序通过一个 JavaScript 层(AngularJS)和一些特殊的 API 来访问,这些 API 用于处理 REST 调用。我们还探讨了在 Quarkus 项目中启用 CORS 时所需的某些配置参数。接下来,我们添加了一个 WebSocket 层,以在初始项目和客户端前端之间引入全双工通信。
通过完成本章,你现在知道如何使用嵌入式 Vert.x 和 Undertow 服务器来利用 REST API (quarkus-resteasy) 和 WebSocket/Servlet API (quarkus-undertow-websockets)。
在下一章中,我们将使用 Hibernate ORM 和 Hibernate Panache 扩展为我们的应用程序添加数据库存储。
第五章:使用 Quarkus 管理数据持久化
到目前为止,我们已经使用内存结构开发了一些基本的应用程序,这些结构可以通过 REST 通道访问。但这只是开始。在现实世界的例子中,你不仅仅依赖于内存数据;相反,你将数据结构持久化在关系型数据库或其他地方,例如 NoSQL 存储。因此,在本章中,我们将利用构建 Quarkus 应用程序并将数据持久化到关系型数据库所需的基本技能。我们还将学习如何使用 对象关系映射(ORM)工具,如 Hibernate ORM,将数据库映射为存储,以及如何使用 Hibernate ORM with Panache 扩展简化其使用。
在本章中,我们将涵盖以下主题:
-
在客户服务中添加 ORM 层
-
配置和运行应用程序以连接到 RDBMS
-
将服务(应用程序和数据库)同时迁移到云端
-
在你的应用程序上添加 Hibernate ORM with Panache 以简化 ORM
技术要求
你可以在 GitHub 上找到本章项目的源代码,链接为 github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter05。
在我们的应用程序中添加 ORM 层
如果你之前曾参与过企业项目,你会知道几乎每个 Java 应用程序都使用 ORM 工具来映射外部数据库。使用 Java 对象映射数据库结构的优势如下:
-
数据库中立性:你的代码将不是针对特定数据库的,因此你不需要将你的代码适应特定的数据库 SQL 语法,这些语法可能在供应商之间有所不同。
-
开发者友好的工作流程:你不需要编写复杂的 SQL 结构来访问你的数据 - 你只需要引用 Java 字段。
另一方面,编写原生 SQL 语句确实可以使你真正了解你的代码正在做什么。此外,在大多数情况下,通过编写直接 SQL 语句,你可以实现最大的性能提升。因此,大多数 ORM 工具都包括执行原生 SQL 语句的选项,以绕过标准的 ORM 逻辑。
在 Quarkus 工具包中,你可以使用 quarkus-hibernate-orm 扩展将你的 Java 类映射为实体对象。Hibernate ORM 位于 Java 应用程序数据访问层和关系型数据库之间。你可以使用 Hibernate ORM API 执行查询、删除、存储和域数据等操作。
首先,让我们定义我们应用程序的领域模型。我们将从简单的Customer对象开始,因为我们已经知道它是什么。为了使我们的例子更有趣,我们将添加另一个对象,称为Orders,它与我们的Customer对象相关。更准确地说,我们将声明一个Customer和其Orders之间的一对一关系:

为了开始,让我们检查本章的第一个示例,它位于本书 GitHub 仓库的Chapter05/hibernate文件夹中。我们建议在继续之前将项目导入到您的 IDE 中。
如果您检查这个项目的pom.xml文件,您将发现其中包含了许多新的扩展。
-
quarkus-hibernate-orm:这个扩展是我们使用 Hibernate 的 ORM 工具在应用程序中所需的核心依赖项。 -
quarkus-agroal:这个扩展为我们购买了 Agroal 连接池,它将为我们处理 JDBC 连接管理。 -
quarkus-jdbc-postgresql:这个扩展包含我们连接到 PostgreSQL 数据库所需的 JDBC 模块。 -
quarkus-resteasy-jsonb:这个扩展是必需的,这样我们就可以在运行时创建 JSON 项并生成 JSON 响应。
以下代码显示了作为 XML 元素的附加依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-agroal</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
现在我们已经查看了项目的配置,让我们检查构成我们应用程序的单个组件。
定义实体层
我们首先需要检查映射数据库表的实体对象列表。第一个是Customer @Entity类,如下所示:
@Entity
@NamedQuery(name = "Customers.findAll",
query = "SELECT c FROM Customer c ORDER BY c.id",
hints = @QueryHint(name = "org.hibernate.cacheable", value =
"true") )
public class Customer {
@Id
@SequenceGenerator(
name = "customerSequence",
sequenceName = "customerId_seq",
allocationSize = 1,
initialValue = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"customerSequence")
private Long id;
@Column(length = 40)
private String name;
@Column(length = 40)
private String surname;
@OneToMany(mappedBy = "customer")
@JsonbTransient
public List<Orders> orders;
// Getters / Setters omitted for brevity
}
让我们逐一检查我们在实体类中包含的单个注解:
-
@Entity注解使这个类有资格进行持久化。它可以与@Table注解结合使用,以将相应的数据库表映射到映射。如果没有包含,就像在我们的例子中一样,它将映射具有相同名称的数据库表。 -
@NamedQuery注解(放置在类级别)是一个静态定义的 SQL 语句,包含一个查询字符串。在您的代码中使用命名查询可以提高代码的组织方式,因为它将 JPA 查询语言与 Java 代码分开。它还避免了将字符串字面量直接嵌入 SQL 中的不良做法,从而强制使用参数。 -
@Id注解指定了实体的主键,它将为每条记录都是唯一的。 -
@SequenceGenerator注解用于委托创建一个序列作为主键的唯一标识符。您需要检查您的数据库是否能够处理序列。另一方面,尽管这不是默认选项,但这被认为是一个更安全的替代方案,因为标识符可以在执行INSERT语句之前生成。 -
@Column注解用于告诉 Hibernate ORM,Java 字段映射了一个数据库列。请注意,我们还在列的大小方面指定了一个约束。由于我们将让 Hibernate ORM 从 Java 代码创建我们的数据库结构,因此 Java 类中声明的所有约束都将有效地转换为数据库约束。 -
最后,我们不得不在
orders字段上应用两个注解:-
@OneToMany注解定义了与Orders表(即一个客户关联多个订单)的一对多关系。 -
@JsonbTransient注解阻止将字段映射到 JSON 表示形式(由于这个关系的反向映射包含在Orders类中,将此字段映射到 JSON 将导致StackOverflow错误)。
-
在我们的代码示例中,为了简洁起见,我们省略了 getter/setter 方法。然而,Hibernate ORM 需要这些方法来对数据库执行实体读取和写入。在本章后面的 使用 Hibernate Panache 简化数据持久性 部分,我们将学习如何通过扩展 PanacheEntity API 来使我们的代码更加精简和整洁。
Customer 实体反过来又引用了以下 Orders 类,它提供了单一到多注解的另一端:
@Entity
@NamedQuery(name = "Orders.findAll",
query = "SELECT o FROM Orders o WHERE o.customer.id = :customerId ORDER BY o.item")
public class Orders {
@Id
@SequenceGenerator(
name = "orderSequence",
sequenceName = "orderId_seq",
allocationSize = 1,
initialValue = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"orderSequence")
public Long id;
@Column(length = 40)
public String item;
@Column
public Long price;
@ManyToOne
@JoinColumn(name = "customer_id")
@JsonbTransient
public Customer customer;
// Getters / Setters omitted for brevity
}
值得注意的是,这个类的命名查询稍微详细一些,因为 Orders.findAll NamedQuery 使用一个参数来过滤特定客户的订单。
由于 Customer 结构和 Orders 结构构成一个双向关联,我们需要将相应的 Customer 字段映射到 @javax.persistence.ManyToOne 注解。
我们还包含了 @javax.persistence.JoinColumn 注解,以表明这个实体是关系的所有者。在数据库术语中,这意味着相应的表有一个外键列,用于引用表。现在我们已经有一个将存储数据的类,让我们检查 Repository 类,它用于从 RDBMS 访问数据。
编写仓库类
为了访问我们的 Customer 数据,我们仍然依赖于 CustomerRepository 类,这需要调整。首先,我们注入了 EntityManager 接口的一个实例,以便管理实体实例的持久性:
@ApplicationScoped
public class CustomerRepository {
@Inject
EntityManager entityManager;
}
一旦我们有了 EntityManager 的引用,我们就可以使用它来对类中的其余部分执行 CRUD 操作:
public List<Customer> findAll() {
return entityManager.createNamedQuery("Customers.findAll",
Customer.class)
.getResultList();
}
public Customer findCustomerById(Long id) {
Customer customer = entityManager.find(Customer.class, id);
if (customer == null) {
throw new WebApplicationException("Customer with id of " +
id + " does not exist.", 404);
}
return customer;
}
@Transactional
public void updateCustomer(Customer customer) {
Customer customerToUpdate = findCustomerById(customer.
getId());
customerToUpdate.setName(customer.getName());
customerToUpdate.setSurname(customer.getSurname());
}
@Transactional
public void createCustomer(Customer customer) {
entityManager.persist(customer);
}
@Transactional
public void deleteCustomer(Long customerId) {
Customer c = findCustomerById(customerId);
entityManager.remove(c);
}
重要的是要注意,我们已将所有执行写操作的方法标记为 @javax.transaction.Transactional 注解。这是在 Quarkus 应用程序中划分事务边界的最简单方法,就像我们在 Java 企业应用程序中做的那样。在实践中,如果存在调用者的上下文事务,则 @Transactional 方法将在该事务的上下文中运行。否则,它将在运行方法之前启动一个新事务。
接下来,我们创建了一个Repository类,该类也用于管理订单。OrderRepository类基本上等同于CustomerRepository类,除了findAll方法会过滤特定客户的订单:
@ApplicationScoped
public class OrderRepository {
@Inject
EntityManager entityManager;
public List<Orders> findAll(Long customerId) {
return (List<Orders>)
entityManager.createNamedQuery("Orders.findAll")
.setParameter("customerId", customerId)
.getResultList();
}
public Orders findOrderById(Long id) {
Orders order = entityManager.find(Orders.class, id);
if (order == null) {
throw new WebApplicationException("Order with id of " + id
+ " does not exist.", 404);
}
return order;
}
@Transactional
public void updateOrder(Orders order) {
Orders orderToUpdate = findOrderById(order.getId());
orderToUpdate.setItem(order.getItem());
orderToUpdate.setPrice(order.getPrice());
}
@Transactional
public void createOrder(Orders order, Customer c) {
order.setCustomer(c);
entityManager.persist(order);
}
@Transactional
public void deleteOrder(Long orderId) {
Orders o = findOrderById(orderId);
entityManager.remove(o);
}
}
现在我们已经讨论了Repository和实体类,让我们来看看 REST 端点,它使应用程序具有响应性。
定义 REST 端点
我们的应用程序为每个Repository类定义了一个 REST 端点。我们在上一章中已经编写了CustomerEndpoint,它对是否使用存储一无所知。因此,一半的工作已经完成。我们在这里只添加了OrderEndpoint,它相应地映射 CRUD HTTP 操作:
@Path("orders")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
public class OrderEndpoint {
@Inject OrderRepository orderRepository;
@Inject CustomerRepository customerRepository;
@GET
public List<Orders> getAll(@QueryParam("customerId") Long
customerId) {
return orderRepository.findAll(customerId);
}
@POST
@Path("/{customer}")
public Response create(Orders order, @PathParam("customer") Long
customerId) {
Customer c = customerRepository.findCustomerById(customerId);
orderRepository.createOrder(order,c);
return Response.status(201).build();
}
@PUT
public Response update(Orders order) {
orderRepository.updateOrder(order);
return Response.status(204).build();
}
@DELETE
@Path("/{order}")
public Response delete(@PathParam("order") Long orderId) {
orderRepository.deleteOrder(orderId);
return Response.status(204).build();
}
}
我们的OrderEndpoint稍微复杂一些,因为它需要在getAll方法中通过Customer ID 过滤每个订单操作。我们还使用了@PathParam注解在代码中,以将Customer和Orders数据从客户端移动到 REST 端点。
连接到数据库
数据库连接是通过 Quarkus 的主配置文件(application.properties)进行的,该文件至少需要包含数据库的 JDBC 设置。我们将使用 PostgreSQL 作为存储,因此 JDBC URL 和驱动程序应符合 PostgreSQL JDBC 的规范。以下配置将用于访问quarkusdb数据库,该数据库使用quarkus/quarkus凭据:
quarkus.datasource.url=jdbc:postgresql://${POSTGRESQL_SERVICE_HOST:localhost}:${POSTGRESQL_SERVICE_PORT:5432}/quarkusdb
quarkus.datasource.driver=org.postgresql.Driver
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
注意,我们正在使用两个环境变量(POSTGRESQL_SERVICE_HOST和POSTGRESQL_SERVICE_PORT)来定义数据库主机和端口。如果它们未被定义,它们将被设置为localhost和5432。当我们将应用程序从本地文件系统切换到云时,此配置将非常有用。
接下来,我们配置了 Hibernate ORM 在启动时使用删除并创建策略。这对于开发或测试应用程序来说非常理想,因为它会在每次启动应用程序时从 Java 实体中删除并重新生成模式和数据表:
quarkus.hibernate-orm.database.generation=drop-and-create
此外,我们还包括了 Agroal 连接池设置,以定义池的初始大小、在内存中保持可用的最小连接数以及可以同时打开的最大连接数:
quarkus.datasource.initial-size=1
quarkus.datasource.min-size=2
quarkus.datasource.max-size=8
最后,为了测试目的,在我们的模式中预先插入一些行可能很有用。因此,我们使用以下属性设置了脚本(import.sql)的位置:
quarkus.hibernate-orm.sql-load-script=import.sql
import.sql脚本中的以下内容向Customer表添加了两行:
INSERT INTO customer (id, name, surname) VALUES ( nextval('customerId_seq'), 'John','Doe');
INSERT INTO customer (id, name, surname) VALUES ( nextval('customerId_seq'), 'Fred','Smith');
上述 SQL 脚本可以在src/main/resources文件夹中找到。
现在我们已经检查了我们的服务,我们将检查测试类,该类会自动验证 CRUD 操作。然后,我们将查看 Web 界面,以便我们可以通过浏览器测试代码。
编写测试类
我们的基本 Test 类假设我们已经有几个 Customer 对象可用。因此,一旦我们通过 GET 请求验证它们的数量,我们将在 Orders 从属实体上测试所有 CRUD 操作,如下所示:
// Test GET
given()
.when().get("/customers")
.then()
.statusCode(200)
.body("$.size()", is(2));
// Create a JSON Object for the Order
JsonObject objOrder = Json.createObjectBuilder()
.add("item", "bike")
.add("price", new Long(100))
.build();
// Test POST Order for Customer #1
given()
.contentType("application/json")
.body(objOrder.toString())
.when()
.post("/orders/1")
.then()
.statusCode(201);
// Create new JSON for Order #1
objOrder = Json.createObjectBuilder()
.add("id", new Long(1))
.add("item", "mountain bike")
.add("price", new Long(100))
.build();
// Test UPDATE Order #1
given()
.contentType("application/json")
.body(objOrder.toString())
.when()
.put("/orders")
.then()
.statusCode(204);
// Test GET for Order #1
given()
.when().get("/orders?customerId=1")
.then()
.statusCode(200)
.body(containsString("mountain bike"));
// Test DELETE Order #1
given()
.when().delete("/orders/1")
.then()
.statusCode(204);
在这个阶段,这个测试类不应该太复杂。我们基本上是在测试使用 org.hamcrest.CoreMatchers.is 构造在数据库中可用的两个客户。然后,我们通过创建一个项目、更新它、查询它,最后删除它,对 Orders 实体执行一轮完整的操作。
在运行测试之前,我们需要一个可用的数据库,其中数据将被持久化。如果您还没有活动的 PostgreSQL 实例,建议您使用以下 shell 启动 docker 镜像:
$ docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_test -e POSTGRES_USER=quarkus -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=quarkusdb -p 5432:5432 postgres:10.5
请注意,除了数据库用户、密码和数据库设置外,我们还在 --ulimit memlock=-1:-1 设置中强制执行我们的容器,以进行无限制的内存锁定,防止交换。我们还将数据库的地址和端口转发到本地机器上可用的所有 IPv4/IPv6 地址。
当启动 docker 进程时,将发出以下输出:
2019-07-09 14:05:56.235 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
2019-07-09 14:05:56.235 UTC [1] LOG: listening on IPv6 address "::", port 5432
2019-07-09 14:05:56.333 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-07-09 14:05:56.434 UTC [60] LOG: database system was shut down at 2019-07-09 14:05:56 UTC
2019-07-09 14:05:56.516 UTC [1] LOG: database system is ready to accept connections
现在,您可以使用以下命令启动测试类:
$ mvn compile test
预期的输出应确认测试已成功运行:
[INFO] Running com.packt.quarkus.chapter5.CustomerEndpointTest
. . . .
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 11.846 s - in com.packt.quarkus.chapter5.CustomerEndpointTest
现在,我们将检查我们添加的项目中的静态网页,以便我们可以访问和管理我们的服务。
为我们的应用程序添加网络界面
我们的服务包括两个静态网页来管理客户服务和每个客户的订单。正如您从我们之前的章节中了解到的,默认情况下,静态页面位于您项目的 src/main/resources/META-INF/resources 文件中。我们可以重用上一章中的相同 index.html 页面,这将成为我们应用程序的着陆页。不过,您会发现一个名为“添加订单”的操作,它将用户重定向到 order.html 页面,并传递一个包含 Customer 信息的查询参数:
<div class="divTable blueTable">
<div class="divTableHeading">
<div class="divTableHead">Customer Name</div>
<div class="divTableHead">Customer Address</div>
<div class="divTableHead">Action</div>
</div>
<div class="divTableRow" ng-repeat="customer in customers">
<div class="divTableCell">{{ customer.name }}</div>
<div class="divTableCell">{{ customer.surname }}</div>
<div class="divTableCell">
<a ng-href="/order.html?customerId={{ customer.id
}}&customerName={{ customer.name }}&
customerSurname={{ customer.surname }}"
class="myButton">Orders</a>
<a ng-click="edit( customer )" class="myButton">Edit</a>
<a ng-click="remove( customer )"
class="myButton">Remove</a>
</div>
</div>
</div>
order.html 页面有自己的 AngularJS 控制器,负责显示所选 Customer 的 Orders 集合,使我们能够读取、创建、修改或删除现有订单。以下为 Angular 控制器的第一部分,它定义了模块和控制器名称,并收集表单参数:
var app = angular.module("orderManagement", []);
angular.module('orderManagement').constant('SERVER_URL', '/orders');
//Controller Part
app.controller("orderManagementController", function($scope, $http, SERVER_URL) {
var customerId = getParameterByName('customerId');
var customerName = getParameterByName('customerName');
var customerSurname = getParameterByName('customerSurname');
document.getElementById("info").innerHTML = customerName + " " + customerSurname;
$scope.orders = [];
$scope.form = {
customerId: customerId,
isNew: true,
item: "",
price: 0
};
//Now load the data from server
reloadData();
在我们 JavaScript 代码的第二部分中,我们包含了 $scope.update 函数来插入/编辑新的 Orders,一个 $scope.remove 函数来删除现有订单,以及一个 reloadData 函数来检索该 Customer 的 Orders 列表,如下所示:
//HTTP POST/PUT methods for add/edit orders
$scope.update = function() {
var method = "";
var url = "";
var data = {};
if ($scope.form.isNew == true) {
// add orders - POST operation
method = "POST";
url = SERVER_URL + "/" + customerId;
data.item = $scope.form.item;
data.price = $scope.form.price;
} else {
// it's edit operation - PUT operation
method = "PUT";
url = SERVER_URL;
data.item = $scope.form.item;
data.price = $scope.form.price;
}
if (isNaN(data.price)) {
alert('Price must be a Number!');
return false;
}
$http({
method: method,
url: url,
data: angular.toJson(data),
headers: {
'Content-Type': 'application/json'
}
}).then(_success, _error);
};
//HTTP DELETE- delete order by id
$scope.remove = function(order) {
$http({
method: 'DELETE',
url: SERVER_URL + "/" + order.id
}).then(_success, _error);
};
//In case of edit orders, populate form with order data
$scope.edit = function(order) {
$scope.form.item = order.item;
$scope.form.price = order.price;
$scope.form.isNew = false;
};
/* Private Methods */
//HTTP GET- get all orders collection
function reloadData() {
$http({
method: 'GET',
url: SERVER_URL,
params: {
customerId: customerId
}
}).then(function successCallback(response) {
$scope.orders = response.data;
}, function errorCallback(response) {
console.log(response.statusText);
});
}
function _success(response) {
reloadData();
clearForm()
}
function _error(response) {
alert(response.data.message || response.statusText);
}
//Clear the form
function clearForm() {
$scope.form.item = "";
$scope.form.price = "";
$scope.form.isNew = true;
}
});
为了简洁起见,我们没有包括完整的 HTML 页面,但您可以在本书的 GitHub 仓库中找到它(正如我们在本章开头的 技术要求 部分中提到的)。
运行应用程序
该应用程序可以从我们运行测试的同一 shell 中执行(这样我们仍然保留DB_HOST环境变量):
mvn quarkus:dev
您应该在控制台中期望以下输出:
Listening for transport dt_socket at address: 5005
2019-07-14 18:41:32,974 INFO [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
2019-07-14 18:41:33,789 INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 815ms
2019-07-14 18:41:35,153 INFO [io.quarkus] (main) Quarkus 0.19.0 started in 2.369s. Listening on: http://[::]:8080
2019-07-14 18:41:35,154 INFO [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, jdbc-postgresql, narayana-jta, resteasy, resteasy-jsonb]
现在,使用以下 URL 转到登录页面:http://localhost:8080。在这里,您将看到一个预先填充的客户列表:

通过点击订单按钮添加一些客户订单。您将被带到以下 UI,在这里您可以读取、修改、删除并为每个客户存储新的订单:

太好了!应用程序按预期工作。它是否可以进一步改进?从性能的角度来看,如果我们缓存频繁访问的数据,应用程序的吞吐量可以得到提高。下一节将向您展示如何使用 Hibernate ORM 的缓存机制实现这一点。
缓存实体数据
在 Hibernate ORM 中,可以通过其高级缓存机制轻松配置实体缓存。默认情况下,有三种类型的缓存可用:
-
一级缓存是事务级别的缓存,用于跟踪当前会话期间实体的状态。默认情况下启用。
-
二级缓存用于在各个 Hibernate ORM 会话之间缓存实体。这使得它成为
SessionFactory级别的缓存。 -
查询缓存用于缓存 Hibernate ORM 查询及其结果。
由于二级缓存和查询缓存可能会消耗大量内存,因此默认情况下未启用。要使实体有资格缓存其数据,您可以使用@javax.persistence.Cacheable注解对其进行注解,如下所示:
@Cacheable
@Entity
public class Customer {
}
在这种情况下,客户的字段值被缓存,除了与其他实体的集合和关系外。这意味着一旦实体被缓存,就可以通过其主键进行搜索,而无需查询数据库。
HQL 查询的结果也可以被缓存。当您想对主要进行读操作的实体对象执行查询时,这非常有用。使 HQL 查询可缓存的简单方法是在@NamedQuery中添加一个@javax.persistence.QueryHint注解,将org.hibernate.cacheable属性设置为true,如下所示:
@Cacheable
@Entity
@NamedQuery(name = "Customers.findAll",
query = "SELECT c FROM Customer c ORDER BY c.id",
hints = @QueryHint(name = "org.hibernate.cacheable", value =
"true") )
public class Customer {
}
您可以通过在application.properties文件中开启 SQL 日志来轻松验证前面的断言,如下所示:
quarkus.hibernate-orm.log.sql=true
然后,如果您运行应用程序,您应该能够在控制台中看到一条单个SQL 语句,无论您请求页面多少次,都可以用它来查询Customer列表:
Hibernate:
select
customer0_.id as id1_0_,
customer0_.name as name2_0_,
customer0_.surname as surname3_0_
from
Customer customer0_
order by
customer0_.id
太好了!您已经达到了第一个里程碑,即在本地文件系统上运行应用程序,并在 Hibernate ORM 的二级缓存(2LC)中缓存常用 SQL 语句。现在,是时候将我们的应用程序迁移到云端了!
将应用程序迁移到云端
在通过本地 JVM 测试了应用程序之后,现在是时候将其原生地引入云中了。这个过程有趣的部分将是将 Quarkus 应用程序与 OpenShift 上的 PostgreSQL 应用程序连接起来,而无需修改任何一行代码!让我们看看我们如何实现这一点:
- 启动你的 Minishift 实例,并创建一个名为
quarkus-hibernate的新项目:
oc new-project quarkus-hibernate
- 接下来,我们将向我们的项目中添加一个 PostgreSQL 应用程序。默认情况下,
openshift命名空间中包含一个 PostgreSQL 镜像流,你可以使用以下命令进行检查:
oc get is -n openshift | grep postgresql
你应该在控制台中看到以下输出:
postgresql 172.30.1.1:5000/openshift/postgresql latest,10,9.2 + 3 more... 6 hours ago
要创建 PostgreSQL 应用程序,需要设置以下配置变量:
-
POSTGRESQL_USER:要创建的 PostgreSQL 账户的用户名 -
POSTGRESQL_PASSWORD:用户账户的密码 -
POSTGRESQL_DATABASE:数据库名称
我们将使用在 application.properties 文件中定义的相同参数,以便我们可以使用以下命令启动我们的应用程序:
oc new-app -e POSTGRESQL_USER=quarkus -e POSTGRESQL_PASSWORD=quarkus -e POSTGRESQL_DATABASE=quarkusdb postgresql
在你的控制台日志中,检查以下输出是否已生成:
--> Creating resources ...
imagestreamtag.image.openshift.io "postgresql:10" created
deploymentconfig.apps.openshift.io "postgresql" created
service "postgresql" created
--> Success
Application is not exposed. You can expose services to the
outside world by executing one or more of the commands below:
'oc expose svc/postgresql'
Run 'oc status' to view your app.
让我们查看可用的服务列表(oc get services),以验证 postgresql 是否可用:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postgresql ClusterIP 172.30.154.130 <none> 5432/TCP 14m
如你所见,服务现在在集群 IP 地址 172.30.154.130 上处于活动状态。幸运的是,我们不需要在我们的应用程序代码中硬编码此地址,因为我们将会使用服务名称 postgresql,它就像集群地址的别名一样工作。
现在,我们将创建我们项目的二进制构建,以便它可以部署到 Minishift。对于没有耐心的用户,可以直接执行 GitHub 上此章节根目录中的 deploy-openshift.sh 脚本。在其中,你可以找到以下带注释的命令列表:
# Build native application
mvn package -Pnative -Dnative-image.docker-build=true -DskipTests=true
# Create a new Binary Build named "quarkus-hibernate"
oc new-build --binary --name=quarkus-hibernate -l app=quarkus-hibernate
# Set the dockerfilePath attribute into the Build Configuration
oc patch bc/quarkus-hibernate -p '{"spec":{"strategy":{"dockerStrategy":{"dockerfilePath":"src/main/docker/Dockerfile.native"}}}}'
# Start the build, uploading content from the local folder:
oc start-build quarkus-hibernate --from-dir=. --follow
# Create a new Application, using as Input the "quarkus-hibernate" image stream:
oc new-app --image-stream=quarkus-hibernate:latest
# Expose the Service through a Route:
oc expose svc/quarkus-hibernate
在此过程结束时,你应该能够通过 oc get routes 命令看到以下路由可用:
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
quarkus-hibernate quarkus-hibernate-quarkus-hibernate.192.168.42.30.nip.io quarkus-hibernate 8080-tcp None
你可以从你项目的网络控制台中检查应用程序的整体状态:

你现在可以导航到应用程序的外部路由(实际的路由地址将根据你的网络配置而变化。在我们的例子中,是 http://quarkus-hibernate-quarkus-hibernate.192.168.42.30.nip.io),并检查应用程序在云上是否运行顺畅。
使用 Panache API 使数据持久性更容易
Hibernate ORM 是将数据库结构映射到 Java 对象的标准方式。使用 ORM 工具的主要缺点是,即使是简单的数据库结构也需要大量的样板代码(例如 getter 和 setter 方法)。此外,你必须在你的仓库类中包含基本的查询方法,这使得工作相当重复。在本节中,我们将学习如何使用 Hibernate Panache 来简化并加速我们应用程序的开发。
要开始使用带有 Panache 的 Hibernate ORM,让我们检查本章的第二个示例,该示例位于本书 GitHub 存储库的Chapter05/hibernate-panache文件夹中。我们建议在继续之前将项目导入到你的 IDE 中。
如果你查看项目的配置,你会看到我们在pom.xml文件中包含了quarkus-hibernate-orm-panache:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
这是我们需要使用的唯一 Hibernate Panache 配置。现在到了有趣的部分。将 Panache 连接到你的实体的策略有两种:
-
扩展
io.quarkus.hibernate.orm.panache.PanacheEntity类:这是最简单的方法,因为你将获得一个自动生成的 ID 字段。 -
扩展
io.quarkus.hibernate.orm.panache.PanacheEntityBase:如果你需要自定义 ID 策略,可以使用此选项。
由于我们为 ID 字段使用SequenceGenerator策略,我们将使用后者选项。以下是被重写的Customer类,它扩展了PanacheEntityBase:
@Entity
@NamedQuery(name = "Customers.findAll",
query = "SELECT c FROM Customer c ORDER BY c.id" )
public class Customer extends PanacheEntityBase {
@Id
@SequenceGenerator(
name = "customerSequence",
sequenceName = "customerId_seq",
allocationSize = 1,
initialValue = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"customerSequence")
public Long id;
@Column(length = 40)
public String name;
@Column(length = 40)
public String surname;
@OneToMany(mappedBy = "customer")
@JsonbTransient
public List<Orders> orders;
}
如你所见,代码已经大大减少,因为我们没有使用 getter/setter 字段。相反,一些字段已被公开为public,以便可以直接由类访问。Orders实体已经使用相同的模式重写:
@Entity
@NamedQuery(name = "Orders.findAll",
query = "SELECT o FROM Orders o WHERE o.customer.id = :id ORDER BY o.item")
public class Orders extends PanacheEntityBase {
@Id
@SequenceGenerator(
name = "orderSequence",
sequenceName = "orderId_seq",
allocationSize = 1,
initialValue = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"orderSequence")
public Long id;
@Column(length = 40)
public String item;
@Column
public Long price;
@ManyToOne
@JoinColumn(name = "customer_id")
@JsonbTransient
public Customer customer;
}
到目前为止,我们已经看到了 Hibernate Panache 提供的某些好处。另一个值得注意的方面是,通过扩展PanacheEntityBase(或PanacheEntity),你将能够直接在你的实体上使用一组静态方法。以下是一个包含你可以在实体上触发的最常见方法的表格:
| 方法 | 描述 |
|---|---|
count |
从数据库中计算此实体(可选查询和参数)的数量 |
delete |
如果该实体已经持久化,则从数据库中删除此实体。 |
flush |
将所有挂起的更改刷新到数据库 |
findById |
通过 ID 查找此类型的实体 |
find |
使用可选参数和排序策略的查询查找实体 |
findAll |
查找此类型的所有实体 |
list |
find().list()的快捷方式 |
listAll |
findAll().list()的快捷方式 |
deleteAll |
删除此类型的所有实体 |
delete |
使用可选参数的查询删除实体 |
persist |
持久化所有给定实体 |
下面的示例显示了利用Customer实体中可用的新字段和方法的重写的CustomerRepository类:
public class CustomerRepository {
public List<Customer> findAll() {
return Customer.listAll(Sort.by("id"));
}
public Customer findCustomerById(Long id) {
Customer customer = Customer.findById(id);
if (customer == null) {
throw new WebApplicationException("Customer with id
of " + id + " does not exist.", 404);
}
return customer;
}
@Transactional
public void updateCustomer(Customer customer) {
Customer customerToUpdate = findCustomerById(customer.id);
customerToUpdate.name = customer.name;
customerToUpdate.surname = customer.surname;
}
@Transactional
public void createCustomer(Customer customer) {
customer.persist();
}
@Transactional
public void deleteCustomer(Long customerId) {
Customer customer = findCustomerById(customerId);
customer.delete();
}
}
最明显的优势是,你不再需要EntityManager作为代理来管理你的实体类。相反,你可以直接调用实体中可用的静态方法,从而大大减少Repository类的冗长性。
为了完整性,让我们看看OrderRepository类,它已经被修改为使用 Panache 对象:
public class OrderRepository {
public List<Orders> findAll(Long customerId) {
return Orders.list("id", customerId);
}
public Orders findOrderById(Long id) {
Orders order = Orders.findById(id);
if (order == null) {
throw new WebApplicationException("Order with id of
" + id + " does not exist.", 404);
}
return order;
}
@Transactional
public void updateOrder(Orders order) {
Orders orderToUpdate = findOrderById(order.id);
orderToUpdate.item = order.item;
orderToUpdate.price = order.price;
}
@Transactional
public void createOrder(Orders order, Customer c) {
order.customer = c;
order.persist();
}
@Transactional
public void deleteOrder(Long orderId) {
Orders order = findOrderById(orderId);
order.delete();
}
}
自从切换到 Hibernate Panache 以来,您的应用程序在 REST 端点和 Web 界面方面完全透明,没有任何其他更改。使用以下命令按常规构建和运行应用程序:
mvn compile quarkus:dev
在控制台上,您应该看到应用程序已启动,并且已添加了两个初始客户:
Hibernate:
INSERT INTO customer (id, name, surname) VALUES ( nextval('customerId_seq'), 'John','Doe')
Hibernate:
INSERT INTO customer (id, name, surname) VALUES ( nextval('customerId_seq'), 'Fred','Smith')
2019-11-28 10:44:02,887 INFO [io.quarkus] (main) Quarkus 1.0.0.Final started in 2.278s. Listening on: http://[::]:8080
2019-11-28 10:44:02,888 INFO [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, jdbc-postgresql, narayana-jta, resteasy, resteasy-jsonb]
现在,享受由 Hibernate ORM 和 Panache 驱动的简化 CRUD 应用程序吧!
摘要
在本章中,我们探讨了数据持久性,并介绍了广为人知的 Hibernate ORM 框架。如果您在企业编程方面有一些年的经验,您应该不会觉得将相同的概念应用到 Quarkus 上有挑战性。现在,您的整体技能包括使用 Hibernate 及其简化的范式 Panache 配置基于 RDBMS 的应用程序。我们还学习了如何在 OpenShift 集群上部署和连接 RDBMS 以及我们的应用程序。
总结来说,我们已经掌握了企业编程的主要支柱(从 REST 服务到 servlets、CDI 和数据持久性)。
在下一章中,我们将学习如何在 Quarkus 中用 MicroProfile API 补充标准企业 API。
第六章:使用 MicroProfile API 构建应用程序
到目前为止,您应该已经很好地理解了如何在 Quarkus 应用程序中使用最常用的 Java API(CDI、REST、JSON、JPA)。在本章中,我们将添加一大堆被称为 MicroProfile 规范的 API。通过掌握本章中的主题,您将能够构建基于 Java EE 核心功能的组件,这允许在实现微服务时获得直接的开发体验,提高应用程序的健壮性,并减少过度设计和重复创造相同模式的风险。您将学习到的话题包括如何为您的服务添加容错性和健康检查,如何检查您的服务指标,如何跟踪和记录它们,以及如何为您的端点创建精简的 REST 客户端。其他功能,如配置、安全和反应式消息传递,将在后续章节中介绍。
在本章中,我们将涵盖以下主题:
-
MicroProfile API 概述及其如何补充企业 API
-
MicroProfile API 如何适应您的 Quarkus 项目
-
一些关于如何在云中运行 MicroProfile API 的介绍
技术要求
您可以在 GitHub 上的本章中找到项目的源代码:github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter06.
开始使用 MicroProfile API
Java 企业 API 是构建应用程序的一套优秀技术,但它历史上缺乏一些如果您想将应用程序迁移到云中所需的特性。例如,没有特定的 API 来处理可以注入到您的服务中的配置属性,也没有正式的方式来描述客户端如何与 REST 端点交互。此外,包括一些功能来监控应用程序的健康状况或负载均衡请求肯定会有所帮助;这些目前由供应商使用自定义技术管理。
Eclipse MicroProfile 项目是一个由顶级应用程序供应商推动的合作倡议,旨在优化 Java 应用程序的企业 API,包括我们在此处提到的所有功能。
从 3.2 版本的角度来看,Eclipse MicroProfile 规范的鸟瞰图显示了该环境是多么丰富:

在本章中,我们将深入探讨 MicroProfile 规范的以下领域:
-
Eclipse MicroProfile 配置:提供了一种统一的方式,通过注入来自静态文件或环境变量的配置数据来配置您的服务。
-
Eclipse MicroProfile Health Check:提供了探测服务状态的能力;例如,它是否正在运行,是否缺少磁盘空间,或者是否存在数据库连接问题。
-
Eclipse MicroProfile Fault Tolerance:允许您在服务失败的情况下定义策略,例如配置超时、重试策略、回退方法和断路器处理。
-
Eclipse MicroProfile Metrics:为 MicroProfile 服务提供了一种标准方式,可以将监控数据导出到外部代理。度量还提供了一个公共 Java API,用于公开其遥测数据。
-
Eclipse MicroProfile OpenAPI:提供了一套 Java 接口,以标准方式记录您的服务。
-
Eclipse MicroProfile OpenTracing:提供了一套用于跟踪组件(如 JAX-RS 和 CDI)的仪器库。
-
Eclipse MicroProfile Rest Client:它基于 JAX-RS API,提供了一种类型安全的、统一的方法来通过 HTTP 调用 RESTful 服务。
虽然本章没有讨论,但 Quarkus 也支持 MicroProfile JWT RBAC,它概述了一个使用基于OpenID Connect(OIDC)的JSON Web Tokens(JWTs)在您的服务端点进行基于角色的访问控制(RBAC)的提案。在下一章,关于安全性的章节中,我们将更详细地介绍这个主题。
开始使用 MicroProfile 项目
要了解单个 MicroProfile API,您需要以下项目,这些项目可以在本书 GitHub 仓库的Chapter06文件夹中找到:
-
fault-tolerance:一个展示如何使用 MicroProfile Fault Tolerance API 的项目 -
health:一个专注于 MicroProfile Health Check API 的项目 -
openapi-swagger:一个实现 OpenAPI 接口的项目 -
opentracing:一个实现 OpenTracing API 的项目 -
rest-client:一个专注于 MicroProfile REST Client API 的项目
大多数前面的项目都是源自我们在第五章“使用 Quarkus 管理数据持久性”中讨论的客户服务Hibernate 应用程序。因此,一个基本的要求是必须有一个运行中的 PostgreSQL 数据库,这样我们才能运行我们的项目。我们提醒您,这可以通过一条简单的脚本完成:
docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_test -e POSTGRES_USER=quarkus -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=quarkusdb -p 5432:5432 postgres:10.5
接下来,我们建议将整个Chapter06文件夹导入到您的 IDE 中,这样您就可以在继续本章内容时轻松访问所有项目。话虽如此,我们将从讨论 MicroProfile Health Check API 开始。
Eclipse MicroProfile Health Check
在云环境中,允许服务向定义的端点报告并最终发布整体健康状况是至关重要的。这可以通过 MicroProfile 健康检查实现,它允许服务在可用时报告整体状态为 "UP",在不可用时报告为 "DOWN"。这些信息可以被服务编排器收集,然后使用健康报告来做出决策。
让我们通过 Chapter06/health 示例将这些概念付诸实践。首先,为了使用健康扩展,我们在 pom.xml 文件中包含了以下依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
一旦前面的库可用,我们就可以添加 org.eclipse.microprofile.health.HealthCheck 接口的实现,这允许我们检查服务状态。以下是一个 DBHealthCheck 类,它验证 PostgreSQL 数据库连接的状态:
@Health
@ApplicationScoped
public class DBHealthCheck implements HealthCheck {
@ConfigProperty(name = "db.host")
String host;
@ConfigProperty(name = "db.port")
Integer port;
@Override
public HealthCheckResponse call() {
HealthCheckResponseBuilder responseBuilder =
HealthCheckResponse.named("Database connection
health check");
try {
serverListening(host,port);
responseBuilder.up();
} catch (Exception e) {
// cannot access the database
responseBuilder.down()
.withData("error", e.getMessage());
}
return responseBuilder.build();
}
private void serverListening(String host, int port) throws
IOException
{
Socket s = new Socket(host, port);
s.close();
}
}
这个类包含了 MicroProfile 规范的两个核心实现:
-
首先,我们有
@Health注解,它与@ApplicationScopedCDI 上下文结合使用,在每次收到对http://localhost:9080/health的请求时返回健康状态检查。 -
这个类还使用 MicroProfile 配置 API 将 PostgreSQL 数据库的主机和端口注入到 bean 中。以下是从
application.properties文件中的摘录:
db.host=${POSTGRESQL_SERVICE_HOST:localhost}
db.port=${POSTGRESQL_SERVICE_PORT:5432}
quarkus.datasource.url=jdbc:postgresql://${db.host}:${db.port}/postgres
如您所见,如果 POSTGRESQL_SERVICE_HOST 和 POSTGRESQL_SERVICE_PORT 环境变量未设置,则使用默认值(localhost 和 5432)并在 db.host 和 db.port 变量中存储。
目标主机和端口通过 TCP 套接字连接,在成功尝试后,将返回 responseBuilder.up()。否则,responseBuilder.down() 将指示失败。
您可以使用以下命令启动 Quarkus 项目:
$ mvn compile quarkus:dev
然后,假设数据库正在运行,让我们尝试访问 http://localhost:9080/health 端点:
curl http://localhost:8080/health
{
"status": "UP",
"checks": [
{
"name": "Database connection health check",
"status": "UP"
},
{
"name": "File system Readiness check",
"status": "UP"
}
]
}
响应确认了数据库连接的状态。让我们也验证数据库不可用的情况。从 PostgreSQL 命令行界面简单地进行 Ctrl + C 将会发送适当的信号来停止进程。你应该在控制台上看到以下输出:
2019-07-27 09:47:25.564 UTC [54] LOG: shutting down
2019-07-27 09:47:25.601 UTC [1] LOG: database system is shut down
现在,再次通过 /health 端点检查数据库连接的状态:
{
"status": "DOWN",
"checks": [
{
"name": "Database connection health check",
"status": "DOWN",
"data": {
"error": "Connection refused (Connection refused)"
}
}
]
}
如您从前面的输出中看到的,返回的 JSON 将状态更改为 "DOWN" 并在错误字段中设置了错误消息。这个例子设定了我们的第一个里程碑:检查应用程序的健康状况。我们可以通过使用存活性和就绪性检查来进一步细化我们的健康检查策略,这些将在下一节中讨论。
使用存活性和就绪性检查
根据最新的 MicroProfile 规范,健康检查现在应使用更具体的模型来帮助我们确定潜在问题的原因。因此,建议您将遗留的@HealthCheck迁移到以下检查之一:
-
可用性检查:此检查可以指示服务暂时无法处理流量。这可能是因为,例如,应用程序可能正在加载一些配置或数据。在这种情况下,您不希望关闭应用程序,但同时也不要向其发送请求。可用性检查旨在涵盖这种场景。
-
存活检查:24/7 运行的服务有时可能会过渡到损坏状态,例如,因为它们遇到了
OutOfMemoryError。因此,除非重新启动,否则它们无法恢复。然而,您可以通过定义一个探测服务存活的存活检查来通知此场景。
为了实现这两个检查,您只需将@org.eclipse.microprofile.health.HealthCheck注解替换为更具体的注解,例如@org.eclipse.microprofile.health.Liveness和@org.eclipse.microprofile.health.Readiness。
在以下示例中,我们实现了一个@Readiness检查来验证是否存在锁文件(例如,由于挂起的任务)并在检测到该文件时发出"DOWN"状态:
@Readiness
@ApplicationScoped
public class ReadinessHealthCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
HealthCheckResponseBuilder responseBuilder =
HealthCheckResponse.named("File system Readiness check");
boolean tempFileExists =
Files.exists(Paths.get("/tmp/tmp.lck"));
if (!tempFileExists) {
responseBuilder.up();
}
else {
responseBuilder.down().withData("error", "Lock file
detected!");
}
return responseBuilder.build();
}
}
可用性检查通过"/health/ready" URI 进行验证。您可以通过请求以下 URL 来检查:http://localhost:8080/health/ready:
$ curl http://localhost:8080/health/ready
如果没有检测到文件,您将看到以下类似的输出:
{
"status": "UP",
"checks": [
{
"name": "File system Readiness check",
"status": "UP"
}
]
}
现在,让我们学习如何将存活检查添加到我们的服务中。我们将检查运行服务所需的空闲内存量,并根据我们设置为 1GB 的特定内存阈值返回存活检查:
@Liveness
@ApplicationScoped
public class MemoryHealthCheck implements HealthCheck {
long threshold = 1024000000;
@Override
public HealthCheckResponse call() {
HealthCheckResponseBuilder responseBuilder =
HealthCheckResponse.named("MemoryHealthCheck
Liveness check");
long freeMemory = Runtime.getRuntime().freeMemory();
if (freeMemory >= threshold) {
responseBuilder.up();
}
else {
responseBuilder.down()
.withData("error", "Not enough free memory!
Please restart application");
}
return responseBuilder.build();
}
}
现在,您可以使用 cURL 验证服务的存活状态,如下所示:
curl http://localhost:8080/health/live
由于默认的 Quarkus JVM 设置不允许我们设置的阈值内存量,因此服务的状态将指示"DOWN",如下所示:
{
"status": "DOWN",
"checks": [
{
"name": "MemoryHealthCheck Liveness check",
"status": "DOWN",
"data": {
"error": "Not enough free memory! Please restart
application"
}
}
]
}
在我们继续检查清单中的下一个 API 之前,值得检查如何在云环境中使用 Kubernetes 探针检查触发健康检查。我们将在下一节中学习如何做到这一点。
让 OpenShift 管理不健康的服务
在迄今为止的示例中,我们已经看到了如何检测不同的健康检查场景。运行 Kubernetes 原生环境的一个最大优点是您可以自动对应用程序状态的变化做出反应。更具体地说,可以通过应用程序的部署描述符来探测以下检查:
-
存活探针:Kubernetes 提供了一个存活探针来确定配置在其上的容器是否仍在运行。如果存活探针失败,
kubelet代理将终止并重新启动容器。 -
就绪性探针:Kubernetes 提供了就绪性探针来指示应用程序暂时无法处理流量,例如,因为正在加载配置。在这种情况下,您不希望停止应用程序,但也不希望允许任何请求。
正如您所看到的,前面的探针与在最新规范中定义的 MicroProfile 健康检查相匹配。作为一个概念验证,我们将以二进制构建的形式将我们的示例应用程序部署到 MiniShift 中。像往常一样,我们将从 shell(或如果您更喜欢这种方式,可以从 Web 控制台)创建一个新的项目:
oc new-project quarkus-microprofile
如您可能记得,我们需要将一个 PostgreSQL 应用程序添加到我们的项目中,这样我们的检查就可以找到它:
oc new-app -e POSTGRESQL_USER=quarkus -e POSTGRESQL_PASSWORD=quarkus -e POSTGRESQL_DATABASE=quarkusdb postgresql
然后,您可以将我们刚刚构建的 Quarkus MicroProfile 健康应用程序推送到云上。以下脚本将用于此目的:
# Build native application
mvn package -Pnative -Dnative-image.docker-build=true -DskipTests=true
# Create a new Binary Build named "quarkus-microprofile"
oc new-build --binary --name=quarkus-microprofile -l app=quarkus-microprofile
# Set the dockerfilePath attribute into the Build Configuration
oc patch bc/quarkus-microprofile -p '{"spec":{"strategy":{"dockerStrategy":{"dockerfilePath":"src/main/docker/Dockerfile.native"}}}}'
# Start the build, uploading content from the local folder:
oc start-build quarkus-microprofile --from-dir=. --follow
# Create a new Application, using as Input the "quarkus-microprofile" Image Stream:
oc new-app --image-stream=quarkus-microprofile:latest
# Expose the Service through a Route:
oc expose svc/quarkus-microprofile
前面的脚本对您来说应该没有什么新意,所以让我们继续转到 OpenShift 控制台,在那里我们可以检查我们项目的状态:

现在,检查您项目的部署配置并选择编辑健康检查:

在健康检查 UI 中,您可以选择要添加哪个健康检查:

让我们从就绪性探针开始。通过选择它,您将被带到以下用户界面:

要选择的键参数是路径,它应该匹配我们的 MicroProfile 就绪性 URI(health/ready)。除此之外,您还可以配置以下属性:
-
initialDelaySeconds:容器启动后,在启动存活性或就绪性探针之前经过的秒数。 -
timeoutSeconds:探针超时的秒数。默认为 1 秒。最小值为1。
现在,让我们配置存活性探针:

除了路径,它将是health/live之外,我们可以保留其他默认值。按原样保存您的更改。现在,让我们尝试破坏一些东西。例如,我们将在应用程序运行所在的 Pod 中创建一个锁文件。这将立即触发就绪性探针的失败。让我们使用以下命令从 shell 中检查 Pod 列表:
$ oc get pods
返回的输出如下:
NAME READY STATUS RESTARTS AGE
quarkus-microprofile-1-build 0/1 Completed 0 20m
quarkus-microprofile-1-rxp4r 1/1 Running 0 20m
好的,我们现在将对这个正在运行的 Pod 运行一个远程 shell:
$ oc rsh quarkus-microprofile-1-rxp4r
我们已经进入。现在,创建一个名为/tmp/tmp.lck的文件
sh-4.4$ touch /tmp/tmp.lck
在几秒钟内(取决于初始延迟设置),您的 Pod 将不再可用。您可以从概览面板中看到这一点:

这个变化也将反映在系统事件中,这些事件可以通过oc get events命令捕获,如下所示:
$ oc get events
quarkus-microprofile-3-mzl6f.15b54cfc42ddb728 Pod spec.containers{quarkus-microprofile} Warning Unhealthy kubelet, localhost
Readiness probe failed: HTTP probe failed with statuscode: 503
最后,值得一提的是,我们的应用程序还包括一个存活检查,该检查验证可用内存量是否大于某个特定阈值。无论你是否达到了阈值,存活探测都取决于 MiniShift 启动时允许的内存量。关于 OpenShift 应用程序内存大小的深入探讨将超出本书的范围,但通过查看官方文档了解更多信息是值得的:docs.openshift.com/container-platform/3.9/dev_guide/application_memory_sizing.html。
维护应用程序的状态,使其保持健康是设计应用程序时需要考虑的关键要素。另一方面,使你的服务能够对故障或性能退化做出反应同样重要。不过,别担心——下一节将教你如何使用 MicroProfile 容错 API 来处理故障。
Eclipse MicroProfile 容错 API
容错规范是一个基本的 API,可以通过支持一组可以提高应用程序弹性的策略来处理你的微服务的不可用性。以下可用的容错策略有:
-
超时:为服务调用的执行定义超时时间
-
回退:在发生故障时提供应急解决方案
-
重试:允许你根据标准重试执行
-
隔离舱:在系统其余部分仍能工作的情况下隔离部分服务故障
-
断路器:定义了自动快速失败的准则,以防止系统因过载而退化
-
异步:允许我们异步调用操作
让我们通过使用Chapter06/fault-tolerance示例来实际看看这些概念。首先,为了使用容错扩展,我们在pom.xml文件中包含了以下依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>io.quarkus:quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
让我们从超时、回退和重试策略开始,这些策略通常一起使用,因为它们相互补充。
使用超时、回退和重试创建具有弹性的服务
简而言之,@org.eclipse.microprofile.faulttolerance.Timeout注解可以用来指定方法返回响应允许的最大时间(以毫秒为单位)。以下是一个在 250 毫秒后超时的findAll方法示例:
@Timeout(250)
public List<Customer> findAll() {
randomSleep();
return entityManager.createNamedQuery("Customers.findAll", Customer.class)
.getResultList();
}
private void randomSleep() {
try {
Thread.sleep(new Random().nextInt(400));
} catch (java.lang.InterruptedException e) {
e.printStackTrace();
}
}
由查找方法触发的随机睡眠可以用来允许一些偶尔的执行失败。
为了减轻超时或其他失败,你可以用@Fallback策略装饰你的方法,这样你就可以在失败的情况下指定一个替代执行路径:
@Timeout(250)
@Fallback(fallbackMethod = "findAllStatic")
public List<Customer> findAll() {
randomSleep();
return entityManager.createNamedQuery("Customers.findAll",
Customer.class)
.getResultList();
}
private List<Customer> findAllStatic() {
LOGGER.info("Building Static List of Customers");
return buildStaticList();
}
在此示例中,如果findAll方法中发生任何故障,我们将执行重定向到findAllStatic方法。findAllStatic方法将返回一个静态的Customer对象列表(请查看本章的源代码示例以查看此实现的实现)。
将重试策略应用于你的故障
有时,你的方法执行中的故障是由临时问题,如网络拥塞引起的。如果我们有信心问题可以按照我们的业务 SLA 解决,我们可以包含一个@Retry注解,以允许我们重新执行失败的特定次数。
例如,通过添加@Retry(maxRetries = 3)注解,我们将在使用静态客户列表之前尝试从数据库加载数据三次:
@Timeout(250)
@Fallback(fallbackMethod = "findAllStatic")
@Retry(maxRetries = 3)
public List<Customer> findAll() {
randomSleep();
return entityManager.createNamedQuery("Customers.findAll",
Customer.class)
.getResultList();
}
值得注意的是,@Retry注解可以配置为仅重试特定异常的子集。这可以在以下示例中看到,其中我们使用@Retry覆盖RuntimeException和TimeoutException:
@Retry(retryOn = {RuntimeException.class, TimeoutException.class}, maxRetries = 3)
现在,让我们学习如何将名为断路器的容错模式应用于我们的服务。
断路器
断路器是创建弹性服务的一个核心模式。它可以用来通过立即拒绝新请求来防止重复异常。MicroProfile 容错 API 使用@CircuitBreaker注解来控制传入请求。软件断路器与电路断路器类似,因为它具有以下状态:
-
关闭状态:闭合电路表示一个完全功能且可供其客户端使用的系统。
-
半开电路:当检测到某些故障时,状态可以变为半开。在此状态下,它会检查失败的组件是否已恢复。如果是,则关闭电路。否则,它将移动到开路状态。
-
开路状态:开路状态意味着服务暂时不可用。经过检查后,你可以验证是否可以安全地切换到半开路状态。
这里有一个示例:
@CircuitBreaker(successThreshold = 5, requestVolumeThreshold = 4, failureRatio=0.75,
delay = 1000)
public List<Orders> findAll(Long customerId) {
possibleFailure();
return (List<Orders>)
entityManager.createNamedQuery("Orders.findAll")
.setParameter("customerId", customerId)
.getResultList();
}
private void possibleFailure() {
if (new Random().nextFloat() < 0.5f) {
throw new RuntimeException("Resource failure.");
}
在前面的示例中,@CircuitBreaker策略应用于OrderRepository类的findAll方法。因此,如果在最后四次调用中,有 75%失败,则电路将过渡到开路状态。电路将保持开路状态 1,000 毫秒。当电路处于开路状态时,将抛出CircuitBreakerOpenException而不是实际调用方法。
请注意,与重试方法一样,@CircuitBreaker也允许我们通过failon注解参数定义失败标准。这可以在以下示例中看到:
@CircuitBreaker(failOn={RuntimeException.class}, successThreshold = 5, requestVolumeThreshold = 4, failureRatio=0.75, delay = 1000)
在前面的示例中,如果在方法中抛出RuntimeException,则CircuitBreaker将其计为一个失败;否则,计为一个成功。
现在我们已经了解了核心容错 API 的背景,让我们学习如何通过 bulkhead 和异步模式进一步增强我们应用程序的健壮性。
使用异步和 Bulkhead 策略
异步编程对于企业开发者来说并不是一种新模式。然而,当与 BulkHead 策略结合使用时,你可以为你的微服务实现一个强大的容错模式。简而言之,如果你用 @Asynchronous 注解一个方法,它将在一个单独的线程上异步执行。
在以下示例中,我们在 createOrder 方法中执行一些逻辑,通过 writeSomeLogging 方法在单独的线程中生成一些调试信息,该方法返回一个 CompletableFuture 实例:
public void createOrder(Orders order, Customer c) {
order.setCustomer(c);
entityManager.persist(order);
writeSomeLogging(order.getItem());
}
@Asynchronous
private Future writeSomeLogging(String item) {
LOGGER.info("New Customer order at: "+new java.util.Date());
LOGGER.info("Item: {}", item);
return CompletableFuture.completedFuture("ok");
}
当与 @Bulkhead 一起使用 @Asynchronous 时,将采用线程池隔离方法。线程池方法允许我们配置最大并发请求以及一定的队列大小,就像一个信号量。以下是更新后的示例,其中包含了 @Bulkhead 策略:
// maximum 5 concurrent requests allowed, maximum 10 requests allowed in the waiting queue
@Asynchronous
@Bulkhead(value = 5, waitingTaskQueue = 10)
private Future writeSomeLogging(String item) {
LOGGER.info("New Customer order at: "+new java.util.Date());
LOGGER.info("Item: {}", item);
return CompletableFuture.completedFuture("ok");
}
这是对 MicroProfile API 中可用的容错策略的快速浏览。让我们继续到下一节,该节是关于捕获服务指标。
Eclipse MicroProfile Metrics API
MicroProfile Metrics 规范为我们提供了一种统一的方式,将您的服务监控数据导出到管理代理。这有助于我们执行对一些关键统计指标的前瞻性检查,例如服务被请求的次数和速率、每次请求的持续时间等等。
让我们开始编码。在这里,我们将关注 Chapter06/metrics 示例。首先,为了使用指标扩展,我们在 pom.xml 文件中包含了以下依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>io.quarkus:quarkus-smallrye-metrics</artifactId>
</dependency>
现在,我们将概述添加到我们的 REST 服务之上的指标注解。让我们从 @Counted 注解开始,它跟踪请求被调用的次数:
@GET
@Counted(description = "Customer list count", absolute = true)
public List<Customer> getAll() {
return customerRepository.findAll();
}
在 @Counted 注解中,我们提供了一个描述并设置了 absolute 标志为 true,这意味着类的包名将不会添加到指标名称前。
让我们编译并运行应用程序:
$ mvn compile quarkus:dev
现在,让我们重新加载主页,这将触发客户列表。接下来,我们将收集一些指标。我们的指标有两个入口点:
-
http://localhost:8080/metrics:此端点将返回所有指标,包括应用程序运行时的系统指标。 -
http://localhost:8080/metrics/application:此端点将仅返回已部署的应用程序发出的指标。
我们将选择后面的选项,如下所示:
$ curl http:/localhost:8080/metrics/applications
由于我们已经加载了主页两次,预期的输出应该如下所示:
# HELP application:get_all Customer list count
# TYPE application:get_all counter
application:get_all 2.0
下一个注解是 @Timed 注解,它跟踪事件的持续时间。让我们将其应用到 getAll 方法上:
@Timed(name = "timerCheck", description = "How much time it takes to load the Customer list", unit = MetricUnits.MILLISECONDS)
public List<Customer> getAll() {
return customerRepository.findAll();
}
你应该能够检索关于前述方法调用频率的详细报告(包括每秒、每分钟、每 5 分钟的速率以及统计分位数指标)。为了简洁起见,以下是其中的一段摘录:
# TYPE application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_rate_per_second
application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_rate_per_second 0.04980015712212517
# TYPE application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_one_min_rate_per_second
application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_one_min_rate_per_second 0.09447331054820299
# TYPE application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_five_min_rate_per_second
application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_five_min_rate_per_second 0.17214159528501158
. . . .
application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_seconds{quantile="0.999"} 0.004191615
另一方面,如果你只需要一个记录单个数据单元的基本度量,你可以使用@Gauge注解:
@Gauge(name = "peakOfOrders", unit = MetricUnits.NONE, description = "Highest number of orders")
public Number highestNumberOfOrders() {
return orderRepository.countAll();
}
在两个请求落在前面的方法上之后,Gauge 度量将显示以下度量:
# HELP application:com_packt_quarkus_chapter6_order_endpoint_peak_of_orders Highest number of orders
# TYPE application:com_packt_quarkus_chapter6_order_endpoint_peak_of_orders gauge
application:com_packt_quarkus_chapter6_order_endpoint_peak_of_orders 2.0
在对 MicroProfile 度量进行快速介绍之后,让我们学习如何使用 OpenAPI 和 Swagger 来记录我们的端点资源。
配置 OpenAPI 和 Swagger UI
OpenAPI 规范旨在提供一组 Java 接口和编程模型,可以从 JAX-RS 服务中原生生成 OpenAPI v3 文档。Quarkus 中的默认 OpenAPI 实现为所有通过/openapi端点暴露的服务提供开箱即用的标准文档。
尽管如此,你可以通过使用特定的注解进一步增强 JAX-RS 服务,以提供更多关于端点、其参数和响应的见解。接下来,我们将关注Chapter06/openapi-swagger示例的代码。正如你可以从其配置中检查到的,我们向项目中添加了以下扩展:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
由于我们项目中有几个可用的 REST 端点,我们可以在http://localhost:8080/openapi检查生成的 OpenAPI 文档。以下是我们的客户服务应用的(截断)输出:
$ curl http://localhost:8080/openapi
---
openapi: 3.0.1
info:
title: Generated API
version: "1.0"
paths:
/customers:
get:
responses:
200:
description: OK
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
format: int64
type: integer
name:
type: string
orders:
type: array
items:
type: object
properties:
id:
format: int64
type: integer
item:
type: string
price:
format: int64
type: integer
surname:
type: string
如你所见,我们付出了最小的努力就生成了一份描述我们服务功能的 JSON 文档,而不需要直接访问底层源代码或任何其他文档。
除了这个之外,OpenAPI 可以用作构建如Swagger这样的强大 UI 的基础,Swagger 是一个可视化并交互 API 的出色工具。它的 UI 是自动从你的 OpenAPI 规范生成的。
为了开始使用 Swagger,你只需指向http://localhost:8080/swagger-ui/。这样做后,你将到达 Swagger 主页:

从那里,你可以轻松地通过展开并点击“尝试一下”按钮测试任何可用的操作:

将生成默认的响应体。根据你的需求进行调整,然后点击执行。结果,你将在响应体文本区域看到操作返回的值(如果有):

可选地,你可以点击下载按钮将响应保存到本地。
自定义 OpenAPI 的输出
根据 OpenAPI 规范(swagger.io/specification/),可以自定义由/openapi Servlet 返回的对象的完整模式。这可以通过向你的端点类和方法添加特定的注解来实现。尽管这些注解都不是强制的,但我们将提到一些可以改进你的 OpenAPI 模式可读性的常见注解。
例如,可以使用@org.eclipse.microprofile.openapi.annotations.tags.Tag注解作为限定符来描述与端点相关的特定操作组。此注解可以应用于类级别。为了描述单个资源方法,可以使用org.eclipse.microprofile.openapi.annotations.Operation标签,该标签可以应用于方法级别。然后,可以使用org.eclipse.microprofile.openapi.annotations.parameters.Parameter标签包含操作参数的描述。最后,org.eclipse.microprofile.openapi.annotations.responses.APIResponse标签描述了 API 操作的单一响应。您可以将多个APIResponse注解附加到单个方法上,以控制每个响应代码的响应。
以下示例展示了在实际中应用于CustomerEndpoint类的自定义设置:
@Tag(name = "OpenAPI Example", description = "Quarkus CRUD Example")
public class CustomerEndpoint {
@Inject CustomerRepository customerRepository;
@Operation(operationId = "all", description = "Getting All
customers")
@APIResponse(responseCode = "200", description = "Successful
response.")
@GET
public List<Customer> getAll() {
return customerRepository.findAll();
}
@POST
public Response create( @Parameter(description = "The new
customer.", required = true) Customer customer) {
customerRepository.createCustomer(customer);
return Response.status(201).build();
}
@PUT
public Response update(@Parameter(description = "The customer to
update.", required = true) Customer customer) {
customerRepository.updateCustomer(customer);
return Response.status(204).build();
}
@DELETE
public Response delete(@Parameter(description = "The customer to
delete.", required = true) @QueryParam("id") Long customerId) {
customerRepository.deleteCustomer(customerId);
return Response.status(204).build();
}
}
为了简洁起见,我们仅使用 OpenAPI 注解标记了CustomerEndpoint服务。我们将更新OrderEndpoint服务的任务留给你,以便你可以验证你的新技能。
Eclipse MicroProfile OpenTracing API
分布式跟踪在微服务时代扮演着关键角色,因为它允许您跟踪请求在不同服务之间的流动。为了实现微服务跟踪,我们可以对我们的服务进行配置,以便将消息记录到分布式跟踪服务器,该服务器可以收集、存储并以各种格式显示这些信息。
OpenTracing 规范没有指定哪个分布式系统负责收集跟踪数据,但广泛采用的全端到端开源解决方案是Jaeger (www.jaegertracing.io/),它完全实现了 OpenTracing 标准。
让我们通过切换到Chapter06/opentracing示例来查看 OpenTracing 的实际应用。首先,为了使用 opentracing 扩展,必须在您的项目中添加以下依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>io.quarkus:quarkus-smallrye-opentracing</artifactId>
</dependency>
实际上,在添加此扩展时,将为您的应用程序提供一个io.opentracing.Tracer对象的实现。这意味着您所有的 HTTP 请求都将自动进行跟踪。
在配置方面,我们需要提供有关 Jaeger 端点的某些详细信息。这可以通过application.properties文件或使用环境变量来完成。以下展示了我们如何配置application.properties文件以向运行在本地主机并监听端口14268的 Jaeger 端点发送跟踪通知:
quarkus.jaeger.service-name=quarkus-service
quarkus.jaeger.sampler-type=const
quarkus.jaeger.sampler-param=1
quarkus.jaeger.endpoint=http://localhost:14268/api/traces
在前面的配置中,我们还定义了服务名称(quarkus-service)和采样器类型。在采样器类型定义中,"总是对所有跟踪做出相同的决定。它要么采样所有跟踪(sampler-param=1),要么不采样任何跟踪(sampler-param=2)。
现在,我们可以启动 Jaeger 服务。最简单的方法是以 Docker 容器的方式运行它。以下命令将启动jaegertracing/all-in-one容器镜像,将 Docker 容器的 UDP/TCP 端口转发到 localhost:
docker run -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14268:14268 -p 9411:9411 jaegertracing/all-in-one:latest
现在,我们可以开始使用我们的客户服务应用程序并执行一些操作。然后,我们可以登录到 Jaeger 控制台,它位于http://localhost:16686:

如前一张截图所示,在 Jaeger UI 的左侧面板中,你可以看到一个名为“服务”的组合框,其中包含可用于追踪的服务列表。你应该在其中看到默认的 jaeger 查询服务,它允许我们追踪查询服务。假设你已经配置了你的 Quarkus 应用程序以发出通知,你应该能够看到 quarkus-service 被列出。选择它,然后检查下一个组合框,即“操作”。这个组合框包含为该特定服务追踪的所有操作。以下是包含组合框的 UI 的部分视图:

如果你选择全部,你应该能在屏幕上看到所有针对quarkus-service的所有 HTTP 请求的追踪,如下面的截图所示:

从那里,你可以通过点击它来选择收集有关单个追踪的更多详细信息。你将看到一个包含执行时间、远程调用者和报告的错误等详细信息的综合时间线:

如果你想要下载并详细说明追踪文件,你也可以选择通过在右上角选择“追踪 JSON”将你的操作作为 JSON 进行追踪。
使用 Jaeger 追踪你的应用程序有很多可能性。如果你想在追踪微服务方面成为忍者,我们建议参考www.jaegertracing.io/。
Eclipse MicroProfile REST 客户端 API
在本章中,我们将讨论的最后一个是 MicroProfile 的 REST 客户端扩展。这个 API 的目标是提供一个类型安全的方式来在微服务架构中调用 REST 服务。
不要混淆 MicroProfile REST 客户端 API 与 JAX-RS 客户端 API!它们实现了不同的标准:JAX-RS 客户端 API 是根据 JSR 370(www.jcp.org/en/jsr/detail?id=370)实现的,而 MicroProfile REST 客户端 API 遵循此处指定的标准:microprofile.io/project/eclipse/microprofile-rest-client。
为了了解 REST 客户端 API,我们将将其作为Chapter06/rest-client应用程序的模板。这个项目不过是我们客户服务的简化版本,它只包含接口而不是服务实现。在配置方面,我们在rest-client项目的pom.xml文件中添加了以下依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
接下来,我们将具体的服务实现替换为两个接口:一个名为CustomerEndpointItf,另一个名为OrdersEndpointItf。以下是CustomerEndpointItf:
@RegisterRestClient
@Path("customers")
@Produces("application/json")
@Consumes("application/json")
public interface CustomerEndpointItf {
@GET
List<Customer> getAll();
@POST
Response create(Customer customer);
@PUT
Response update(Customer customer);
@DELETE
Response delete(Long customerId);
}
下面是OrdersEndpointItf:
@RegisterRestClient
@Path("orders")
@Produces("application/json")
@Consumes("application/json")
public interface OrderEndpointItf {
@GET
List<Orders> getAll(@QueryParam("customerId") Long customerId);
@POST
@Path("/{customer}")
Response create(Orders order, @PathParam("customer") Long
customerId);
@PUT
Response update(Orders order);
@DELETE
@Path("/{order}")
Response delete(@PathParam("order") Long orderId);
}
注意@org.eclipse.microprofile.rest.client.inject.RegisterRestClient注解,它使得 REST 客户端可以通过@org.eclipse.microprofile.rest.client.inject.RestClient注解通过 CDI 进行注入。让我们在CustomerEndpoint中学习如何实际操作:
public class CustomerEndpoint {
@Inject @RestClient
CustomerEndpointItf customer;
@GET
public List<Customer> getAll() {
return customer.getAll();
}
@POST
public Response create(Customer c) {
return customer.create(c);
}
@PUT
public Response update(Customer c) {
return customer.update(c);
}
@DELETE
public Response delete(Long customerId) {
return customer.delete(customerId);
}
}
如您所见,我们已经通过将执行委托给已注册为 REST 客户端的接口来替换了 REST 客户端实现。在此阶段,您可能会想知道 REST 客户端是如何知道远程端点的。这是一个好问题,答案包含在application.properties文件中:
com.packt.quarkus.chapter6.restclient.CustomerEndpointItf/mp-rest/url=http://localhost:8080
com.packt.quarkus.chapter6.restclient.CustomerEndpointItf/mp-rest/scope=java.inject.Singleton
com.packt.quarkus.chapter6.restclient.OrderEndpointItf/mp-rest/url=http://localhost:8080
com.packt.quarkus.chapter6.restclient.OrderEndpointItf/mp-rest/scope=java.inject.Singleton
如您从第一行所见,所有对 REST 客户端接口的请求都将导致调用远程端点的基本 URL,该 URL 使用以下表达式进行限定:
<Fully Qualified REST Client Interface>/mp-rest/url=<Remote REST base URL>
此外,REST 客户端接口的默认作用域已被配置为单例,这指示 Quarkus 一次性实例化单例,在注入期间将其引用传递给其他对象。其他支持的作用域值包括@Dependent、@ApplicationScoped和@RequestScoped,后者是默认值。有关不同作用域的更多详细信息,请参阅 CDI 规范(www.cdi-spec.org/)。
为了运行测试,我们需要一个通过http://localhost:8080/customers端点返回Customers列表并通过http://localhost:8080/orders端点返回Orders列表的应用程序。为此,我们可以启动任何版本的实现上述端点的客户服务应用程序,如下所示:
cd Chapter05/hibernate
$ mvn quarkus:dev
让我们回到我们的例子:
cd Chapter06/rest-client
现在,我们可以使用以下命令运行 REST 客户端测试:
$ mvn compile test
您应该在控制台中看到以下输出:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.988 s - in com.packt.quarkus.chapter6.restclient.CustomerEndpointTest
2019-08-04 19:29:43,592 INFO [io.quarkus] (main) Quarkus stopped in 0.003s
[INFO] Results:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
这意味着您成功地对远程客户端点执行了完整的 CRUD 操作。作为证明,您应该能够在服务控制台中看到执行的 SQL 语句:
select
orders0_.id as id1_1_0_,
orders0_.customer_id as customer4_1_0_,
orders0_.item as item2_1_0_,
orders0_.price as price3_1_0_,
customer1_.id as id1_0_1_,
customer1_.name as name2_0_1_,
customer1_.surname as surname3_0_1_
from
Orders orders0_
left outer join
Customer customer1_
on orders0_.customer_id=customer1_.id
where
orders0_.id=?
请注意,前面的日志要求您已启用 SQL 跟踪,如第五章所述,使用 Quarkus 管理数据持久性。
摘要
在本章中,我们全面概述了 MicroProfile 规范以及如何将其与 Quarkus 应用程序集成。
我们从 MicroProfile API 的概述开始,讨论了它如何融入基于云的微服务整体图景。然后,我们介绍了主要的 MicroProfile 规范。
首先,我们探讨了 Health API 以及它如何报告你服务的存活状态和就绪状态。然后,我们介绍了容错 API,它可以用来设计具有弹性的服务。接下来,我们讨论了应用程序的遥测数据以及如何使用 Metrics API 收集这些数据。我们还涉及了另一个关键方面,即服务的文档化和跟踪请求流,这可以通过 OpenAPI 和跟踪规范来实现。最后,我们学习了如何创建 REST 客户端以简化我们与远程服务的交互。
到目前为止,你应该已经对如何设计一个完整的 Quarkus 企业应用程序有了清晰的认识,尽管我们还没有掌握一个关键方面:Quarkus 应用程序安全。这正是我们将在下一章中学习的内容。
第七章:保护应用程序
安全性是每个企业系统的一个关键要求。在本章中,我们将学习如何使用各种方法有效地保护 Quarkus 服务。我们将首先实践的方法是在我们的服务中嵌入安全层。这仍然可以被视为快速应用程序开发和测试的有效解决方案。另一方面,当我们的服务投入生产时,我们需要避免这种极端的集中化。因此,我们将学习的下一个策略是 Quarkus 服务如何连接到分布式安全系统,如 Keycloak。本章的最后一个主题是关于通过一些简单的配置步骤加密 HTTP 通道。
在本章中,我们将涵盖以下主题:
-
保护我们的客户服务
-
使用 Elytron 保护 Quarkus 服务
-
使用 Keycloak 保护 Quarkus 服务
-
使用 MicroProfile JWT 保护 Quarkus 服务
-
使用 HTTPS 与 Quarkus
技术要求
您可以在 GitHub 上找到本章项目的源代码,地址为github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter07。
保护我们的客户服务
Quarkus 安全基础设施源自标准的Java 企业版(Java EE)规范,该规范基于简单的基于角色的安全模型。通过使用它,您可以通过注解和配置文件指定您的安全约束。
在 Java 注解方面,以下注解可以用来指定可以应用于单个方法或类的安全约束:
-
@javax.annotation.security.RolesAllowed:这是最常见的注解,因为它指定了一个或多个被授权调用特定方法或类的角色。 -
@javax.annotation.security.RunAs:此注解在方法或类的调用期间动态分配一个角色。如果我们需要临时允许执行某些方法,这可以是一个方便的选项。 -
@javax.annotation.security.PermitAll:此注解允许我们从方法中释放安全约束。在某些场景中,如果您还没有确定哪个角色将有权调用方法,这可能很有用。 -
@javax.annotation.security.DenyAll:此注解与@PermitAll正好相反,因为它拒绝访问带有此注解的方法或类。
为了保持简单,我们将为我们的客户服务应用程序定义一个简单的安全策略。这包括两个角色:
-
用户 角色:此角色将有权执行只读操作,例如查询
客户列表。 -
管理员 角色:此角色将有权执行所有可用的操作,包括创建、更新和删除。
以下是我们 CustomerEndpoint 类的代码,该类已被 @RolesAllowed 安全注解装饰:
public class CustomerEndpoint {
@Inject CustomerRepository customerRepository;
@GET
@RolesAllowed("user")
public List<Customer> getAll() {
return customerRepository.findAll();
}
@POST
@RolesAllowed("admin")
public Response create(Customer customer) {
customerRepository.createCustomer(customer);
return Response.status(201).build();
}
@PUT
@RolesAllowed("admin")
public Response update(Customer customer) {
customerRepository.updateCustomer(customer);
return Response.status(204).build();
}
@DELETE
@RolesAllowed("admin")
public Response delete(@QueryParam("id") Long customerId) {
customerRepository.deleteCustomer(customerId);
return Response.status(204).build();
}
}
为了简洁起见,我们这里不包括已更新的 OrderEndpoint 类,它同样被更新以保护带有 用户 角色的读取方法和带有 管理员 角色的写入方法。
在定义了我们的安全策略之后,我们现在可以选择将哪个安全提供者应用于我们的服务。这需要我们在 application.properties 中添加正确的设置,并在我们项目的 pom.xml 文件中包含依赖项。
我们将从 Elytron 安全提供者开始,它不需要我们安装任何外部应用程序或工具来保护我们的服务。
使用 Elytron 保护 Quarkus 服务
Elytron 是一个安全框架,它被创建出来以统一 WildFly 和 JBoss 的 企业应用平台(EAP)的安全方面。因此,这个框架最初是为单体应用程序设计的,以便覆盖安全方面的每一个方面。在 Quarkus 这样的容器就绪的本地平台上使用 Elytron 有什么优势?
虽然这可能看起来是一个过于简化的保护资产的方法,但它可以在开发或测试包含安全角色的应用程序时证明是有益的。开箱即用,Quarkus 提供了一个基于文件的 安全域 实现,以提供最小配置要求下的 基于角色的访问控制(RBAC)。
在撰写本书时,我们有三个可用的 Elytron 扩展,我们可以使用它们来保护我们的应用程序:
-
quarkus-elytron-security-properties-file:通过属性文件提供基本身份验证的支持。 -
quarkus-elytron-security-jdbc:通过 JDBC 提供数据库身份验证的支持。 -
quarkus-elytron-security-oauth2:提供 OAuth2 身份验证的支持。这个扩展可能在 Quarkus 的未来版本中被弃用,并可能被一个反应式 Vert.x 版本所取代。
由于我们的客户应用程序已经使用数据库作为后端,我们将向您展示如何使用数据库身份验证。在继续之前,请检查 Chapter07/elytron-demo 文件夹中的源代码。
如 pom.xml 文件所示,我们已经向我们的项目中添加了以下扩展:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-jdbc</artifactId>
</dependency>
为了配置身份验证,我们需要指定哪个表包含用户和角色的列表。我们还需要添加几个具有不同角色的用户。为此,我们在 src/main/resources 文件夹中的 import.sql 脚本中包含了以下 SQL 语句:
CREATE TABLE quarkus_user (
id INT,
username VARCHAR(255),
password VARCHAR(255),
role VARCHAR(255)
);
INSERT INTO quarkus_user (id, username, password, role) VALUES (1, 'admin', 'password123', 'admin');
INSERT INTO quarkus_user (id, username, password, role) VALUES (2, 'frank','password123', 'user');
现在,在 application.properties 文件中,我们需要通过提供一些基本的配置参数来激活 JDBC 身份验证。以下是添加的属性列表:
quarkus.security.jdbc.enabled=true
quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM quarkus_user u WHERE u.username=?
quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=true
quarkus.security.jdbc.principal-query.clear-password-mapper.password-index=1
quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2
quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups
让我们快速讨论这些属性:
-
当
quarkus.security.jdbc.enabled属性设置为 true 时,启用 JDBC 认证。 -
quarkus.security.jdbc.principal-query.sql属性用于指定将检查有效用户名/密码组合的 SQL 语句。 -
当
quarkus.security.jdbc.principal-query.clear-password-mapper.enabled属性设置为 true 时,配置了一个将 SQL 查询返回的列映射到明文密码键类型的映射器。 -
quarkus.security.jdbc.principal-query.clear-password-mapper.password-index属性设置了来自明文认证查询的列索引。 -
最后,
quarkus.security.jdbc.principal-query.attribute-mappings.0.index和quarkus.security.jdbc.principal-query.attribute-mappings.0.to用于将认证查询中的第二个字段(index=2)绑定到主体的角色(组)。
这就是您使用 Elytron 安全域来保护服务所需的所有内容。当所有部件都放在正确的位置时,我们将使用一个测试类来验证对 REST 端点的认证。
创建一个执行基本身份验证的测试类
我们的测试类需要调整,以便它可以适应新的受保护场景。实际上,我们需要在 HTTP 请求中发送带有用户凭证的头部文件,以授权服务的执行。多亏了 Fluent RestAssured API,将 auth() 方法插入我们的 HTTP 请求相当简单,如下面的代码所示:
@Test
public void testCustomerService() {
// Test GET for Customer size
given()
.auth()
.preemptive()
.basic("frank", "password123")
.when().get("/customers")
.then()
.statusCode(200)
.body("$.size()", is(2));
JsonObject objOrder = Json.createObjectBuilder()
.add("item", "bike")
.add("price", new Long(100))
.build();
// Test POST Order for Customer #1
given()
.auth()
.preemptive()
.basic("admin", "password123")
.contentType("application/json")
.body(objOrder.toString())
.when()
.post("/orders/1")
.then()
.statusCode(201);
// Create new JSON for Order #1
objOrder = Json.createObjectBuilder()
.add("id", new Long(1))
.add("item", "mountain bike")
.add("price", new Long(100))
.build();
// Test UPDATE Order #1
given()
.auth()
.preemptive()
.basic("admin", "password123")
.contentType("application/json")
.body(objOrder.toString())
.when()
.put("/orders")
.then()
.statusCode(204);
// Test GET for Order #1
given()
.auth()
.preemptive()
.basic("admin", "password123")
.when().get("/orders?customerId=1")
.then()
.statusCode(200)
.body(containsString("mountain bike"));
// Test DELETE Order #1
given()
.auth()
.preemptive()
.basic("admin", "password123")
.when().delete("/orders/1")
.then()
.statusCode(204);
}
请注意,我们必须使用预防性基本身份验证。这意味着认证详情会立即发送在请求头中,无论服务器是否已经挑战了认证。没有这个,当前在 Quarkus 中的 Vert.x 实现将返回一个未授权的响应。
假设 PostgreSQL 数据库已经启动,我们的测试类准备就绪,可以执行:
mvn compile test
您应该看到所有的 CRUD 操作都已完成成功:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.799 s - in com.packt.quarkus.chapter7.CustomerEndpointTest
2019-08-16 15:30:12,281 INFO [io.quarkus] (main) Quarkus stopped in 0.012s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
在下一节中,我们将介绍如何使用 Keycloak 在我们的应用程序中利用 OpenID (openid.net/) 安全标准。
使用 Keycloak 保护 Quarkus 服务
Keycloak (www.keycloak.org/) 是一个基于 WildFly 应用服务器的开源访问管理解决方案。您可以在您的架构中采用它,以利用以下各种功能:
-
客户端适配器
-
单点登录 (SSO)
-
身份管理和社交登录
-
标准协议(OpenID Connect 或 SAML)
-
丰富的管理控制台
-
用户账户管理控制台
多亏了这些功能和连接到现有身份标准的能力,Keycloak 已成为许多大型组织的既定标准。它的一个受支持的版本,被称为红帽单点登录(RH-SSO:access.redhat.com/products/red-hat-single-sign-on),也适用于企业客户。
安装完成后,Keycloak 将成为您网络中应用程序的主要安全端点。因此,您的应用程序不需要添加登录表单来验证用户并存储其凭证。相反,应用程序被配置为指向 Keycloak,它支持 OpenID 或 SAML 等协议标准,以保护您的端点。
简而言之,客户端应用将从其域名重定向到 Keycloak 的身份服务器,在那里它们展示其凭证。这样,您的服务与您的安全策略和用户凭证完全隔离。相反,服务被授予一个数字签名的身份令牌或断言。这些令牌包含身份信息(如姓名或电子邮件地址),也可以包含有关授权执行业务操作的角色的信息。在下一节中,我们将学习如何配置 Keycloak,以便我们可以发行可以用来访问我们的示例客户服务的令牌。
将 Keycloak 扩展添加到我们的服务中
在Chapter07/keycloak-demo文件夹中,您将找到我们客户服务应用程序的另一个版本,该版本使用 Keycloak 来保护 REST 端点。为了使用 Keycloak 授权和 OpenID 扩展,我们在pom.xml文件中包含了以下依赖项集:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
现在所有必需的库都已就绪,让我们学习如何安装 Keycloak 身份服务器并在其中加载一个安全领域。
设置 Keycloak
在本节中,我们将使用 Docker 快速掌握 Keycloak 的控制权。首先,我们需要拉取 Keycloak 镜像并启动一个容器实例。以下命令将在后台启动 Keycloak 服务器,并将8180作为 HTTP 端口,并本地暴露主机和端口:
docker run --rm \
--name keycloak \
-e KEYCLOAK_USER=admin \
-e KEYCLOAK_PASSWORD=admin \
-p 8180:8180 \
-it quay.io/keycloak/keycloak:7.0.1 \
-b 0.0.0.0 \
-Djboss.http.port=8180 \
-Dkeycloak.profile.feature.upload_scripts=enabled
在控制台中,你应该看到服务器已经成功启动:
10:33:15,519 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:9990/management
10:33:15,519 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:9990
10:33:15,519 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 6.0.1 (WildFly Core 8.0.0.Final) started in 15991ms - Started 672 of 937 services (652 services are lazy, passive or on-demand)
现在,Keycloak 已经启动并运行,让我们加载一个领域,它包含我们将应用于我们的服务的有效安全配置。
定义安全领域
Keycloak 配置的一个关键方面是安全领域,它包含与一个安全上下文相关的所有配置(用户、角色、客户端策略等)。当您第一次启动 Keycloak 时,它将包含一个领域:主领域。这是领域层次结构中的顶级。您不应该使用此领域来配置您组织中的用户和服务。相反,考虑使用主领域为负责定义您组织中的其他领域的管理员。
我们本章的 GitHub 仓库包含一个名为 Quarkus 领域的应用程序领域,这对我们的目的很有用。我们将向您展示如何导入它,然后我们将逐步介绍其配置,以便您可以根据此模板创建新的领域。让我们按照以下步骤进行:
- 首先连接到 Keycloak 控制台,该控制台位于
http://localhost:8180。将显示身份验证挑战。使用admin/admin登录:

- 现在,从左上角的面板中选择添加一个新的领域,如下面的截图所示:

- 选择导入一个领域,并指向包含 Quarkus 领域导出的 JSON 文件(
quarkus-realm.json):

- 点击创建以继续。现在,让我们简要概述一下领域的选项。
在领域设置窗口中,您将能够定义一些核心设置,例如域名和您的登录设置,预定义您的令牌有效期和超时时间,等等:

为了我们的学习路径,我们将不会更改这些设置。在领域面板中,您将能够验证我们已经包含了相同的角色(管理员和用户),以便它们与我们的现有安全约束相匹配:

在这里,我们重新创建了我们在基于文件的 Elytron 领域中测试过的相同用户列表:

在“角色映射”选项卡中,您可以检查测试用户是否是用户角色的成员:

另一方面,管理员用户将被分配给管理员和用户角色:

现在我们已经查看了用户和角色,让我们讨论领域客户端配置的一个关键方面,如下面的 UI 所示:

让我们看看客户端配置的一些关键方面。首先,我们必须定义客户端协议。有两种类型的客户端身份验证协议:
-
OpenID Connect(OIDC)是一种身份验证系统,客户端请求一个访问令牌,该令牌用于代表经过身份验证的用户调用其他服务。
-
SAML身份验证要求 Keycloak 为您的组织用户提供单点登录(SSO)。
对于我们的需求,基于 OIDC 令牌的身份验证是我们需要授予我们的服务访问权限的。我们还需要选择我们将使用标准流程还是隐式流程进行身份验证。
默认选项(启用标准流程)涉及将浏览器重定向到/从 OIDC 提供商(Keycloak)进行用户认证和同意。然后,需要一个第二个回传请求来检索 ID 令牌。此流程提供最佳安全性,因为令牌不会暴露给浏览器,并且客户端可以安全地认证。
让我们以序列图的形式描述这个流程:

为了提供更好的性能,Keycloak 还支持 隐式流程,这发生在访问令牌在成功认证后立即发送时。虽然这个选项可能具有更好的可扩展性(因为不需要额外的请求来交换代码以获取令牌),但你将负责监控令牌何时过期,以便你可以发放一个新的令牌。
由于我们选择了使用标准流程,我们将指定一个适当的有效重定向 URI,这需要设置为运行在 Quarkus 上的应用程序的默认 HTTP 端口。
对于访问类型,我们将其配置为机密类型,这要求客户端应用程序提供密钥以获取 ID 令牌。当你将访问类型设置为机密时,你将能够从凭据选项卡中选择客户端认证器,它定义了你将用于客户端及其密钥的凭据类型。为该客户端 ID 定义的密钥是 mysecret,如下面的截图所示:

如果你想要更改默认密钥,只需点击“重新生成密钥”按钮,并相应地更新你的客户端应用程序。
现在我们已经配置了 Keycloak,我们将使用这个域来执行一个使用携带令牌认证的测试。然而,在那之前,我们将相应地配置我们的 Quarkus 服务。
配置 Quarkus 以支持 Keycloak
信不信由你,一旦我们配置了身份服务器,在我们的 Quarkus 应用程序中就没有太多工作要做。我们只需要将 Keycloak URL 和客户端设置提供给我们的应用程序的 application.properties 配置文件:
keycloak.url=http://localhost:8180
quarkus.oidc.enabled=true
quarkus.oidc.auth-server-url=${keycloak.url}/auth/realms/quarkus-realm
quarkus.oidc.client-id=quarkus-client
quarkus.oidc.credentials.secret=mysecret# Enable Policy Enforcement
quarkus.keycloak.policy-enforcer.enable=true
quarkus.http.cors=true
在前面的配置中,我们提供了连接到 Keycloak 身份服务器的必需设置。我们还添加了一个名为 keycloak.url 的属性来定义 Keycloak 的 IP 地址和端口。在下面的表中,我们添加了一些关于每个参数的详细信息:
| 参数 | 描述 |
|---|---|
quarkus.oidc.enabled |
当设置为 true 时,将启用 OIDC 扩展。 |
quarkus.oidc.auth-server-url |
Keycloak 认证客户端请求的根 URL。 |
quarkus.oidc.client-id |
客户端 ID。 |
quarkus.oidc.credentials.secret |
客户端密钥。 |
quarkus.keycloak.policy-enforcer.enable |
通过启用策略执行器,即使没有与该资源关联的策略,也不允许请求进入。 |
除了这个之外,如果您打算在不同于应用程序的机器上运行 Keycloak,建议启用 HTTP CORS,这样您就可以跨域访问 Keycloak(有关此内容的更多详细信息,请参阅第五章,使用 Quarkus 管理数据持久性)。
现在,让我们深入了解将用于执行通过 Keycloak 身份服务器授权的 CRUD 操作的测试类。
编写测试类
我们的测试类由两个部分组成。在第一个部分中,我们为test用户和admin用户检索令牌。然后,我们使用这两个令牌来测试应用程序。更具体地说,属于用户角色的test令牌将用于GET请求。另一方面,admin令牌将用于授权POST、PUT和DELETE请求。
这是测试类的第一个部分:
@ConfigProperty(name = "keycloak.url")
String keycloakURL;
@Test
public void testHelloEndpoint() {
RestAssured.baseURI = keycloakURL;
Response response = given().urlEncodingEnabled(true)
.auth().preemptive().basic("quarkus-client",
"mysecret")
.param("grant_type", "password")
.param("client_id", "quarkus-client")
.param("username", "test")
.param("password", "test")
.header("Accept", ContentType.JSON.getAcceptHeader())
.post("/auth/realms/quarkus-realm/protocol/openid-
connect/token")
.then().statusCode(200).extract()
.response();
JsonReader jsonReader = Json.createReader(new
StringReader(response.getBody().asString()));
JsonObject object = jsonReader.readObject();
String userToken = object.getString("access_token");
response = given().urlEncodingEnabled(true)
.auth().preemptive().basic("quarkus-client",
"mysecret")
.param("grant_type", "password")
.param("client_id", "quarkus-client")
.param("username", "admin")
.param("password", "test")
.header("Accept", ContentType.JSON.getAcceptHeader())
.post("/auth/realms/quarkus-realm/protocol/openid-
connect/token")
.then().statusCode(200).extract()
.response();
jsonReader = Json.createReader(new
StringReader(response.getBody().asString()));
object = jsonReader.readObject();
String adminToken = object.getString("access_token");
// Test CRUD Methods here
}
此代码向我们的 Keycloak 域的认证 URL 发出POST请求。请求包含用户名和密码,以及客户端 ID(quarkus_client)及其密钥(mysecret)作为参数。RESTAssured API 验证返回状态码为 200,并返回响应对象。然后,我们从 JSON 响应中提取了包含在access_token键下的令牌。
如果您想调试令牌声明的底层细节,您可以使用像curl这样的工具来检查 Keycloak 返回的响应。例如,如果您要为test用户请求令牌,那么以下是一个简单的curl命令,可以完成这项工作:
curl -X POST http://localhost:8180/auth/realms/quarkus-realm/protocol/openid-connect/token \
--user quarkus-client:mysecret \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=test&password=test&grant_type=password'
在代码的第二部分中,我们通过在每个 REST 调用中包含用户的令牌来访问我们的服务。我们使用oauth2方法来做这件事:
RestAssured.baseURI = "http://localhost:8081";
given().auth().preemptive()
.oauth2(userToken)
.when().get("/customers")
.then()
.statusCode(200)
.body("$.size()", is(2));
JsonObject objOrder = Json.createObjectBuilder()
.add("item", "bike")
.add("price", new Long(100))
.build();
// Test POST Order for Customer #1
given().auth()
.oauth2(adminToken)
.contentType("application/json")
.body(objOrder.toString())
.when()
.post("/orders/1")
.then()
.statusCode(201);
// Create new JSON for Order #1
objOrder = Json.createObjectBuilder()
.add("id", new Long(1))
.add("item", "mountain bike")
.add("price", new Long(100))
.build();
// Test UPDATE Order #1
given().auth()
.oauth2(adminToken)
.contentType("application/json")
.body(objOrder.toString())
.when()
.put("/orders")
.then()
.statusCode(204);
// Test GET for Order #1
given().auth()
.oauth2(adminToken)
.when().get("/orders?customerId=1")
.then()
.statusCode(200)
.body(containsString("mountain bike"));
// Test DELETE Order #1
given().auth()
.oauth2(adminToken)
.when().delete("/orders/1")
.then()
.statusCode(204);
测试可以通过以下命令执行:
$ mvn compile test
您应该期望它能够成功完成,如下所示:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.84 s - in com.packt.quarkus.chapter7.CustomerEndpointTest
2019-08-24 12:28:28,056 INFO [io.quarkus] (main) Quarkus stopped in 0.011s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
恭喜!您已经成功为您的服务搭建了一个企业级的安全基础设施。
在运行时收集主体和角色信息
在继续到下一个认证方案之前,值得注意的是,您可以通过注入表示当前登录用户的SecurityIdentity接口来在运行时确定主体名称和角色。在这段简短的摘录中,我们将学习如何检索和记录已连接的用户、他们用于注册的姓名/姓氏以及他们有权获得的角色集:
@Inject SecurityIdentity securityContext;
@Inject CustomerRepository customerRepository;
@GET
@RolesAllowed("user")
public List<Customer> getAll() {
LOGGER.info("Connected with User
"+securityContext.getPrincipal().getName());
Iterator<String> roles = securityContext.getRoles().iterator();
while (roles.hasNext()) {
LOGGER.info("Role: "+roles.next());
}
return customerRepository.findAll();
}
在这种情况下,当访问客户列表时,服务将记录以下信息:
Connected with User test
Role: offline_access
Role: uma_authorization
Role: user
您可以通过查看其源代码来检查SecurityIdentity接口中所有可用的方法,该源代码可在github.com/quarkusio/quarkus-security/blob/master/src/main/java/io/quarkus/security/identity/SecurityIdentity.java找到。
使用 MicroProfile JWT 保护 Quarkus 服务
在前面的示例中,我们介绍了如何使用 Keycloak 通过携带令牌进行身份验证和授权请求。然而,仅携带令牌本身是一个简化的安全模式,因为它基于交换一个可能任意字符串。
任何拥有有效携带令牌的客户都可以使用它来获取相关资源,而无需证明其身份,因为身份只能通过加密密钥进行验证。为了填补这一空白,我们将学习如何使用JSON Web Tokens(JWTs),这是一种用于令牌的编码标准,它使用可以签名和加密的 JSON 数据负载。JWT 包括以下部分:
- 头部:这是一个 Base64 编码的字符串,由两部分组成:令牌的类型,即 JWT,以及正在使用的哈希算法,例如 HMAC SHA256 或 RSA。以下是一个示例解码头部 JWT:

- 负载:这也是一个 Base64 编码的字符串,包含声明。声明是关于实体(用户或组)及其附加元数据的陈述。以下是我们服务返回的示例负载:

- 签名:签名用于验证消息在传输过程中未被更改。对于使用私钥签名的令牌,它还可以断言 JWT 的发送者是其所声称的人:

JWT 令牌也可以由 Keycloak 提供,因此我们无需更改领域配置即可使用 JWT。另一方面,我们必须包含组声明,以便 JWT 令牌将令牌主体的组成员映射到服务中定义的应用程序级别角色。
此信息已通过 Token Claim Name 字段包含在我们的领域(客户端配置的 Mappers 部分)中:

现在,让我们配置我们的服务,使其能够使用此身份验证方案。
配置我们的服务以使用 JWT
我们的证明概念项目可以位于Chapter07/jwt-demo文件夹中。我们建议您将其导入到您的 IDE 中,以便与其他章节中的项目进行比较。
首先,我们将 Keycloak 授权和 OpenID 扩展替换为quarkus-smallrye-jwt扩展:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
然后,我们为我们的项目包含了一组不同的属性。以下是application.properties配置文件的代码,它针对同一 Keycloak 服务器,并提供了公钥位置和身份验证机制的详细信息:
keycloak.url=http://localhost:8180
# MP-JWT Config
mp.jwt.verify.publickey.location=${keycloak.url}/auth/realms/quarkus-realm/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/auth/realms/quarkus-realm
quarkus.smallrye-jwt.realmName=quarkus-realm
以下表格简要描述了每个属性:
| 参数 | 描述 |
|---|---|
mp.jwt.verify.publickey.location |
存储提供者公钥的位置。它可以是相对路径或 URL。 |
mp.jwt.verify.issuer |
指定 JWT 的 iss(代表 issuer)声明的值,服务器将接受该值为有效。 |
quarkus.smallrye-jwt.realmName |
用于认证的安全域。 |
现在,我们已经准备好使用 JWT 认证机制来执行我们的测试类。
运行我们的测试
我们的 CustomerEndpointTest 类包含我们用来验证 Keycloak 认证的相同代码。然而,在幕后,它将执行以下步骤:
-
请求访问令牌。
-
验证访问令牌字段。
-
使用领域提供的 RSA 公钥执行签名验证,该公钥位于
mp.jwt.verify.publickey.location系统属性定义的位置。
测试可以通过以下命令执行:
$ mvn compile test
您应该看到它成功完成,如下所示:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.463 s - in com.packt.quarkus.chapter7.CustomerEndpointTest
2019-08-24 15:37:29,879 INFO [io.quarkus] (main) Quarkus stopped in 0.006s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
至于纯 Keycloak 认证,让我们学习如何从 JWT 上下文中获取更多信息。
注入 JWT 声明和令牌信息
要以编程方式管理 JWT 中包含的信息,我们可以将相关的 API 注入到我们的服务中。主类 org.eclipse.microprofile.jwt.JsonWebToken 可以通过以下命令直接注入:
@Inject
JsonWebToken jwt;
此类提供了我们可以用来检索令牌本身、其主题以及令牌中包含的声明的函数。更多详细信息可以在源代码中找到,源代码位于 github.com/eclipse/microprofile-jwt-auth/blob/master/api/src/main/java/org/eclipse/microprofile/jwt/JsonWebToken.java。
另一方面,可以通过 @Claim 注解来获取特定的声明,它包括其 standard 属性和 Claim 的名称。使用以下代码注入令牌声明中的组和用户名:
@Inject
@Claim(standard = Claims.groups)
Optional<JsonString> groups;
@Inject
@Claim(standard = Claims.preferred_username)
Optional<JsonString> currentUsername;
那就是本章关于 Keycloak 的最后一个主题。现在,让我们剖析另一个安全主题,即使用 SSL 进行 HTTP 通信。
使用 Quarkus 的 HTTPS
本章的最后一节专门讨论了在 Quarkus 中加密 HTTP 通信。为了做到这一点,您需要在配置中提供一个有效的(无论是自签名还是由 CA 签名)密钥库或 PEM 证书。
首先,让我们学习如何生成一个自签名的 PEM 密钥和证书对。最简单的方法是使用 OpenSSL 工具,如下所示:
$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
上述命令将在当前目录下生成一个名为 cert.pem 的证书和一个相关名为 key.pem 的密钥文件。接下来,在您的 application.properties 中配置证书和密钥文件的文件系统路径:
quarkus.http.ssl.certificate.file=/path/cert.pem
quarkus.http.ssl.certificate.key-file=/path/key.pem
另一方面,您还可以生成并使用一个已经包含默认条目和证书的密钥库。您可以使用 keytool 工具生成它,并为它提供一个密码:
$ keytool -genkey -keyalg RSA -alias quarkus -keystore keystore.jks -storepass password -validity 365 -keysize 2048
Enter key password for <quarkus>
(RETURN if same as keystore password):
在我们的案例中,唯一将被创建的文件将是 keystore.jks;我们可以在配置中包含它,如下所示:
quarkus.http.ssl.certificate.key-store-file=/path/keystore.jks
quarkus.http.ssl.certificate.key-store-password=password
最后,我们可以指定 Undertow 服务器使用的端口来绑定 HTTPS 协议:
quarkus.http.ssl-port=8443
作为概念验证,你可以构建并运行位于 Chapter07/https 中的应用程序,以验证它可以通过 SSL 端口访问:

浏览器栏中可以看到的警告只是意味着正在使用的 SSL 证书不是由受信任的机构签发的。
在本节中,我们已经介绍了我们需要遵循的基本配置步骤,以确保我们的应用程序在传输层的安全性。现在,我们不再通过明文通道与客户服务进行通信;相反,我们使用 安全套接字层 (SSL) 来保护我们的连接。
摘要
我们本章开始时讨论了可以应用于 Quarkus 服务的安全策略。默认情况下,你可以通过使用 Elytron 扩展提供基于文件的认证和授权。然后,我们更详细地探讨了 Keycloak,它可以通过支持 OpenID 标准来提供企业级的安全标准。我们介绍了使用载体令牌的基本示例,以及一个更复杂的示例,使用数字签名的令牌,两者都符合 JWT 规范。最后,我们发现了如何生成和配置证书,以使用 HTTPS 保护对 Quarkus 终端的访问。
在下一章中,我们将介绍一些高级策略,这些策略可以提升 Quarkus 服务的潜在能力!
第三部分:高级开发策略
在本节中,我们将涵盖高级配置概念。然后,我们将学习如何使用 Vert.x 实现响应式和非阻塞 API,以及如何使用 Apache Kafka 和 ActiveMQ 构建流式平台。
本节包括以下章节:
-
第八章,高级应用开发
-
第九章,统一命令式和响应式
-
第十章,使用 Quarkus 进行响应式消息传递
第八章:高级应用程序开发
在本章中,我们将探讨一些 Quarkus 的高级功能,这些功能将帮助您设计和编写前沿的 Quarkus 应用程序。我们将学习的内容将涵盖 Quarkus API 的不同领域,从高级配置选项到控制 Quarkus 应用程序的生命周期以及使用 Quarkus 调度器触发基于时间的事件。
到本章结束时,您将能够利用以下高级功能:
-
使用高级 MicroProfile 配置选项
-
控制您服务的生命周期事件
-
在您的服务中安排周期性任务
技术要求
您可以在本章中找到项目的源代码,在 GitHub 上的 github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter08。
使用高级配置选项
如我们所学的,Quarkus 依赖于 MicroProfile Config 规范来将配置属性注入到我们的应用程序中。到目前为止,我们已使用默认配置文件(命名为 application.properties)为应用程序的初始设置提供初始值。
让我们通过一个基本示例来回顾如何注入属性,包括为属性提供默认值:
@Inject
@ConfigProperty(name="tempFileName", defaultValue="file.tmp")
String fileName;
在前面的代码中,我们将一个应用程序属性注入到 fileName 变量中。请注意,属性名称应仔细规划,因为 Quarkus 随带一套广泛的系统属性,可用于管理其环境。幸运的是,您不需要手头有文档来检查所有可用的系统属性。事实上,您可以使用 Maven 的 generate-config 命令列出所有内置的系统属性,基于您当前安装的扩展:
mvn quarkus:generate-config
此命令将在 src/main/resources 文件夹下创建一个名为 application.properties.example 的文件。如果您打开此文件,您将看到它包含一个注释列表,列出了所有可用的配置选项,这些选项位于 quarkus 命名空间下。以下是其中的一段摘要:
# The name of the application.
# If not set, defaults to the name of the project.
#
#quarkus.application.name=
# The version of the application.
# If not set, defaults to the version of the project
#
#quarkus.application.version=
作为旁注,您可以通过添加 -Dfile=<filename> 选项来为 generate-command 选择不同的文件名。
在接下来的章节中,我们将学习一些使用本书 GitHub 存储库中 Chapter08/advanced-config 文件夹中的示例作为参考的高级配置练习。我们建议在继续之前将项目导入到您的 IDE 中。
多个配置源
当涉及到设置应用程序属性时,application.properties 文件并非唯一的选择。根据 MicroProfile 的 Config 规范,您还可以使用以下选项:
- Java 系统属性:可以通过
System.getProperty()和System.setProperty()API 以编程方式读取/写入 Java 系统属性。作为替代,您可以使用-D选项在命令行上设置属性,如下所示:
java -Dquarkus.http.port=8180 app.jar
- 环境变量:这需要为属性设置一个环境变量,如下所示:
export QUARKUS_HTTP_PORT=8180
如您可能已注意到的,匹配的环境变量名称已被设置为 uppercase,并且点号已被下划线替换。
请注意,在 Quarkus 的当前版本中,还必须在 application.properties 中定义该变量,以便它可以被环境变量覆盖。
最后,我们还可以通过向我们的应用程序添加一个新的配置源来从外部收集我们的配置。下一节将展示我们如何做到这一点。
配置自定义配置源
在我们迄今为止创建的所有示例中,我们假设应用程序配置是从 src/main/resources/application.properties 文件中获取的,这是 Quarkus 应用程序的默认设置。尽管如此,由于 Quarkus 完全支持 MicroProfile Config 规范,从另一个来源加载配置(例如外部文件系统、数据库或任何可以被 Java 应用程序加载的东西)是完全可能的!
为了做到这一点,您必须实现 org.eclipse.microprofile.config.spi.ConfigSource 接口,该接口公开了一组用于加载属性(getProperties)、检索属性名称(getPropertyNames)和检索相应的值(getValue)的方法。
作为概念验证,请查看以下实现,该实现可在 Chapter08/advanced-config 项目中找到:
public class FileConfigSource implements ConfigSource {
private final String CONFIG_FILE = "/tmp/config.properties";
private final String CONFIG_SOURCE_NAME = "ExternalConfigSource";
private final int ORDINAL = 900;
@Override
public Map getProperties() {
try(InputStream in = new FileInputStream( CONFIG_FILE )){
Properties properties = new Properties();
properties.load( in );
Map map = new HashMap();
properties.stringPropertyNames()
.stream()
.forEach(key-> map.put(key,
properties.getProperty(key)));
return map;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public Set getPropertyNames() {
try(InputStream in = new FileInputStream( CONFIG_FILE )){
Properties properties = new Properties();
properties.load( in );
return properties.stringPropertyNames();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public String getValue(String s) {
try(InputStream in = new FileInputStream( CONFIG_FILE )){
Properties properties = new Properties();
properties.load( in );
return properties.getProperty(s);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public String getName() {
return CONFIG_SOURCE_NAME;
}
}
如果您熟悉 java.io API,代码本身相当简单。FileConfigSource 类尝试从您的文件系统的 /tmp/config.properties 路径加载外部配置。值得一提的是,已经设置了一个 ORDINAL 变量来指定此 ConfigSource 类的顺序,以防从多个来源加载了多个属性。
ConfigSource 的默认值设置为 100,在属性在多个来源中定义的情况下,具有最高序数值的来源具有优先级。以下是可用配置源的排名:
| 配置源 | 值 |
|---|---|
application.properties |
100 |
| 环境变量 | 300 |
| 系统属性 | 400 |
由于我们在示例中将 ORDINAL 变量设置为 900,如果存在其他配置源,它将覆盖其他配置源。
一旦自定义 ConfigSource 在项目中可用,我们需要为此类进行注册。为此,我们在项目的 resources/META-INF/services 文件夹下添加了一个名为 org.eclipse.microprofile.config.spi.ConfigSource 的文件。以下是项目的树视图,位于 resources 文件夹下:
│ └── resources
│ └── META-INF
│ └── services
│ ├── org.eclipse.microprofile.config.spi.ConfigSource
在此文件中,我们指定了 ConfigSource 的完全限定名。在我们的情况下,如下所示:
com.packt.chapter8.FileConfigSource
现在,一旦应用程序启动,自定义的 ConfigSource 将被加载,并且其属性将覆盖其他潜在的同名属性副本。
在您的项目中的 AdvancedConfigTest 类中,您将找到一个断言,该断言验证了一个属性已从外部的 FileConfigSource 类加载:
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("custom greeting"));
关于 AdvancedConfigTest 类的更多细节将在本章后面讨论。
在配置中使用转换器
要讨论配置转换器,让我们以这个简单的配置示例为例:
year=2019
isUser=true
在这里,将前面的属性注入到我们的代码中是完全可以接受的:
@ConfigProperty(name = "year", defaultValue = "2020")
Integer year;
@ConfigProperty(name = "isUser", defaultValue = "false")
Boolean isUser;
在底层,MicroProfile Config API 为不仅仅是普通字符串的值提供了类型安全的转换。
此外,请注意,我们可以为属性提供一个默认值,如果属性在我们的配置中未定义,则将使用该默认值。
这是通过在配置模型中提供转换器来实现的。默认情况下,MicroProfile Config API 已经提供了一些内置的转换器。以下是一些内置转换器的列表:
-
boolean和java.lang.Boolean。以下值被转换为布尔值(不区分大小写):true、YES、Y、1和ON。任何其他值都将为false。 -
byte和java.lang.Byte。 -
short和java.lang.Short。 -
int和java.lang.Integer。 -
long和java.lang.Long。 -
float和java.lang.Float。点.用于分隔小数位。 -
double和java.lang.Double。点.用于分隔小数位。 -
char和java.lang.Character。 -
java.lang.Class。这是基于Class.forName的结果。
数组、列表和集合也受到支持。为了将其中一个集合注入到类的变量中,您可以使用逗号(,)字符作为分隔符,\ 作为转义字符。例如,考虑以下配置:
students=Tom,Pat,Steve,Lucy
以下代码将前面的配置注入到 java.util.List 元素中:
@ConfigProperty(name = "students")
List<String> studentList;
以类似的方式,您可以使用内置转换器从值列表生成 Array。看看以下配置示例:
pets=dog,cat,bunny
以下配置可以注入到字符串数组中如下:
@ConfigProperty(name = "pets")
String[] petsArray;
类也可以作为配置的一部分进行注入:
myclass=TestClass
在运行时,该类将由类加载器搜索,并使用 Class.forName 构造创建。我们可以在代码中这样放置它:
@ConfigProperty(name = "myclass")
TestClass clazz;
最后,值得一提的是,您可以将整个 Config 对象注入,并在需要时检索单个属性:
@Inject
Config config;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
Integer y = config.getValue("year", Integer.class);
return "Year is " +y;
}
现在,让我们探索一些更高级的策略来创建类型转换器。
添加自定义转换器
如果内置转换器的列表不足以满足需求,你仍然可以通过实现通用接口,即org.eclipse.microprofile.config.spi.Converter来创建自定义转换器。接口的Type参数是字符串转换成的目标类型:
public class MicroProfileCustomValueConverter implements Converter<CustomConfigValue> {
public MicroProfileCustomValueConverter() {
}
@Override
public CustomConfigValue convert(String value) {
return new CustomConfigValue(value);
}
}
以下代码是为目标Type参数编写的,它从一个我们已包含在配置中的普通 Java 字符串派生而来:
public class CustomConfigValue {
private final String email;
private final String user;
public CustomConfigValue(String value) {
StringTokenizer st = new StringTokenizer(value,";");
this.user = st.nextToken();
this.email = st.nextToken();
}
public String getEmail() {
return email;
}
public String getUser() {
return user;
}
你必须在名为resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter的文件中注册你的转换器。包括自定义实现的完全限定类名。例如,在我们的案例中,我们添加了以下行:
com.packt.chapter8.MicroProfileCustomValueConverter
现在,让我们学习如何在实践中使用我们的自定义转换器。为此,我们将向application.properties文件中添加以下行,该行使用CustomConfigValue类构造函数中编码的模式:
customconfig=john;johnsmith@gmail.com
现在,自定义转换器可以作为类属性注入到我们的代码中:
@ConfigProperty(name = "customconfig")
CustomConfigValue value;
@Path("/email")
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getEmail() {
return value.getEmail();
}
尽管前面的例子并没有做什么特别的事情,但它展示了我们如何根据类定义创建一个自定义属性。
测试高级配置选项
在本章的Chapter08/advanced-config/src/test文件夹中,你会找到一个名为AdvancedConfigTest的测试类,它将验证我们迄今为止学到的关键概念。
要成功运行所有这些测试,请将customconfig.properties文件复制到你的驱动器的/tmp文件夹中,否则AdvancedConfigTest类中包含的一个断言将失败:
cp Chapter08/customconfig.properties /tmp
然后,简单地运行install目标,这将触发测试的执行:
mvn install
你应该能看到包含在AdvancedConfigTest中的所有测试都通过了。
配置配置文件
我们刚刚学习了如何使用内置转换器和,对于最复杂的情况,使用自定义转换器来创建复杂的配置。如果我们需要在不同配置之间切换,例如,从开发环境迁移到生产环境时,该怎么办?在这里,你可以复制你的配置。然而,配置文件的激增在 IT 项目中并不总是受欢迎。让我们学习如何使用配置配置文件来处理这个担忧。
简而言之,配置配置文件允许我们在配置中为我们的配置文件指定命名空间,这样我们就可以将每个属性绑定到同一文件中的特定配置文件。
默认情况下,Quarkus 提供了以下配置配置文件:
-
dev:在开发模式下运行时触发(即quarkus:dev)。 -
test:当运行测试时触发。 -
prod:当我们不在开发或测试模式下运行时被选中。
除了前面的配置文件之外,你还可以定义自己的自定义配置文件,这些配置文件将根据我们在激活配置文件部分中指定的规则被激活。
你可以使用以下语法将配置参数绑定到特定的配置文件:
%{profile}.config.key=value
为了看到这个策略的实际示例,我们将通过这本书 GitHub 仓库中Chapter08/profiles文件夹的源代码来讲解。我们建议在继续之前将项目导入到您的 IDE 中。
让我们先检查其application.properties配置文件,它定义了多个配置文件:
%dev.quarkus.datasource.url=jdbc:postgresql://localhost:5432/postgresDev
%test.quarkus.datasource.url=jdbc:postgresql://localhost:6432/postgresTest
%prod.quarkus.datasource.url=jdbc:postgresql://localhost:7432/postgresProd
quarkus.datasource.driver=org.postgresql.Driver
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
quarkus.datasource.initial-size=1
quarkus.datasource.min-size=2
quarkus.datasource.max-size=8
%prod.quarkus.datasource.initial-size=10
%prod.quarkus.datasource.min-size=10
%prod.quarkus.datasource.max-size=20
在先前的配置中,我们为我们的数据源连接指定了三个不同的 JDBC URL。每个 URL 都绑定到不同的配置文件。我们还为生产配置文件设置了一个特定的连接池设置,以便提供更多的数据库连接。在下一节中,我们将学习如何激活每个单独的配置文件。
激活配置文件
让我们以先前的配置中的prod配置文件为例,学习如何激活特定的配置文件。首先,我们需要启动一个名为postgresProd的 PostgreSQL 实例,并将其绑定到端口7432:
docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_Prod -e POSTGRES_USER=quarkus -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=postgresProd -e PGPORT=7432 -p 7432:7432 postgres:10.5
然后,我们需要在package阶段提供配置文件信息,如下所示:
mvn clean package -Dquarkus.profile=prod -DskipTests=true
当运行应用程序时,它将选择在package阶段指定的配置文件:
java -jar target/profiles-demo-1.0-SNAPSHOT-runner.jar
作为一种替代方法,您也可以使用QUARKUS_PROFILE环境变量来指定配置文件,如下所示:
export QUARKUS_PROFILE=dev
java -jar target/profiles-demo-1.0-SNAPSHOT-runner.jar
最后,值得一提的是,相同的策略也可以用来定义非标准配置文件。例如,假设我们想要为需要在生产前进行检查的应用程序添加一个预发布配置文件:
%staging.quarkus.datasource.url=jdbc:postgresql://localhost:8432/postgresStage
在这里,我们可以应用我们为其他配置文件所使用的相同策略,即我们可以在应用程序启动时使用 Java 系统属性(quarkus-profile)指定配置文件,或者将必要的信息添加到QUARKUS_PROFILE环境变量中。
自动配置文件选择
为了简化开发和测试,dev和test配置文件可以通过 Maven 插件自动触发。例如,如果您正在以开发模式执行 Quarkus,最终将使用dev配置文件:
mvn quarkus:dev
以类似的方式,当执行测试时,例如在install生命周期阶段,将激活test配置文件:
mvn quarkus:install
当您执行 Maven 的test目标时,将激活test配置文件。此外,值得注意的是,您可以通过maven-surefire-plugin的系统属性为您的测试设置不同的配置文件:
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<quarkus.test.profile>custom-test</quarkus.test.profile>
<buildDirectory>${project.build.directory}</buildDirectory>
</systemPropertyVariables>
</configuration>
在本节中,我们讲解了应用程序配置文件。在下一节中,我们将学习如何控制我们的 Quarkus 应用程序的生命周期。
控制应用程序生命周期
控制应用程序的生命周期是您服务能够启动外部资源或验证组件状态的一个常见要求。一个简单的策略,借鉴自 Java 企业 API,是包含Undertow扩展(或任何上层,如 REST 服务),这样您就可以利用ServletContextListener,当创建或销毁 Web 应用程序时,它会收到通知。以下是它的最小实现示例:
public final class ContextListener implements ServletContextListener {
private ServletContext context = null;
public void contextInitialized(ServletContextEvent event) {
context = event.getServletContext();
System.out.println("Web application started!");
}
public void contextDestroyed(ServletContextEvent event) {
context = event.getServletContext();
System.out.println("Web application stopped!");
}
}
虽然在 Quarkus web 应用程序中重用此策略是完全可以的,但建议为任何类型的 Quarkus 服务使用此方法。这可以通过观察io.quarkus.runtime.StartupEvent和io.quarkus.runtime.ShutdownEvent事件来实现。此外,在 CDI 应用程序中,你可以通过@Initialized(ApplicationScoped.class)限定符观察一个事件,该事件在应用程序上下文初始化时触发。这对于启动资源(如数据库)特别有用,这些资源在 Quarkus 读取配置之前是必需的。
要查看这个的实际例子,请检查这本书 GitHub 仓库中Chapter08/lifecycle文件夹中可用的源代码。像往常一样,建议在继续之前将项目导入到你的 IDE 中。这个例子的目的是向你展示如何在我们的客户服务中用 H2 数据库替换 PostgreSQL 数据库(www.h2database.com/)。
从配置开始,生命周期项目不再包含 PostgreSQL JDBC 依赖项。为了替换它,以下内容已被包含:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
为了测试我们的客户服务,我们包含了两个 H2 数据库配置配置文件:一个绑定到dev配置文件,另一个绑定到test配置文件:
%dev.quarkus.datasource.url=jdbc:h2:tcp://localhost:19092/mem:test
%test.quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test
quarkus.datasource.driver=org.h2.Driver
要在应用程序上下文启动之前绑定 H2 数据库,我们可以使用以下DBLifeCycleBean类:
@ApplicationScoped
public class DBLifeCycleBean {
protected final Logger log =
LoggerFactory.getLogger(this.getClass());
// H2 Database
private Server tcpServer;
public void observeContextInit(@Observes
@Initialized(ApplicationScoped.class) Object event) {
try {
tcpServer = Server.createTcpServer("-tcpPort",
"19092", "-tcpAllowOthers").start();
log.info("H2 database started in TCP server
mode on Port 19092");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
void onStart(@Observes StartupEvent ev) {
log.info("Application is starting");
}
void onStop(@Observes ShutdownEvent ev) {
if (tcpServer != null) {
tcpServer.stop();
log.info("H2 database was shut down");
tcpServer = null;
}
}
}
此类能够拦截以下事件:
-
上下文启动:这是通过
observeContextInit方法捕获的。数据库在这个方法中启动。 -
应用程序启动:这是通过
onStart方法捕获的。我们只是在事件触发时执行一些日志记录。 -
应用程序关闭:这是通过
onStop方法捕获的。我们在这个方法中关闭数据库。
现在,你可以使用以下命令以dev配置文件启动 Quarkus,就像平常一样:
mvn quarkus:dev
当应用程序启动时,我们将收到通知,H2 数据库已经启动:
INFO [com.pac.qua.cha.DBLifeCycleBean] (main) H2 database started in TCP server mode on Port 19092
然后,我们将在应用程序启动时收到另一个通知,我们可以包括一些额外的任务来完成:
[com.pac.qua.cha.DBLifeCycleBean] (main) Application is starting
最后,当我们停止应用程序时,资源将被取消,如下面的控制台日志所示:
[com.pac.qua.cha.DBLifeCycleBean] (main) H2 database was shut down
在关闭数据库之前,你可以享受使用一个小型内存数据库层运行你的客户服务示例。
激活数据库测试资源
作为额外提示,我们将向你展示如何在测试生命周期中激活 H2 数据库。这可以通过向你的测试类添加一个标记为@QuarkusTestResource的类,并将H2DatabaseTestResource类作为属性传递来实现。
这里有一个例子:
@QuarkusTestResource(H2DatabaseTestResource.class)
public class TestResources {
}
H2DatabaseTestResource基本上执行了与DBLifeCycleBean相同的操作,在我们启动测试之前。请注意,以下依赖项已被添加到项目中以运行前面的测试类:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-h2</artifactId>
<scope>test</scope>
</dependency>
现在,您可以使用以下命令安全地针对test配置文件运行测试:
mvn install
注意,在我们执行测试之前,以下日志将确认 H2 数据库已经在我们的可用 IP 地址之一上启动:
[INFO] H2 database started in TCP server mode; server status: TCP server running at tcp://10.5.126.52:9092 (only local connections)
外部资源引导确实是生命周期管理器的常见用例。另一个常见用例是在应用程序启动阶段安排事件。在下一节中,我们将讨论如何使用 Quarkus 的调度器触发事件。
使用 Quarkus 调度器触发事件
Quarkus 包含一个名为scheduler的扩展,可用于安排单次或重复执行的任务。我们可以使用 cron 格式来指定调度器触发事件的次数。
以下示例的源代码位于本书 GitHub 存储库的Chapter08/scheduler文件夹中。如果您检查pom.xml文件,您将注意到以下扩展已被添加到其中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId>
</dependency>
我们的示例项目每 30 秒生成一个随机令牌(为了简单起见,使用随机字符串)。负责生成随机令牌的类是以下TokenGenerator类:
@ApplicationScoped
public class TokenGenerator {
private String token;
public String getToken() {
return token;
}
@Scheduled(every="30s")
void generateToken() {
token= UUID.randomUUID().toString();
log.info("New Token generated");
}
}
现在,我们可以将我们的令牌注入到内置的 REST 端点中,如下所示:
@Path("/token")
public class Endpoint {
@Inject
TokenGenerator token;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getToken() {
return token.getToken();
}
}
使用以下命令以常规方式启动应用程序:
mvn quarkus:dev
您将注意到,每 30 秒,以下消息将在控制台日志中打印:
[INFO] New Token generated
然后,通过请求/token URL,将返回随机生成的字符串:
curl http://localhost:8080/token
3304a8de-9fd7-43e7-9d25-6e8896ca67dd
使用 cron 调度器格式
除了使用时间表达式(s=seconds,m=minutes,h=hours,d=days)外,您还可以选择更紧凑的 cron 调度器表达式。因此,如果您想每秒触发事件,则可以使用以下 cron 表达式:
@Scheduled(cron="* * * * * ?")
void generateToken() {
token= UUID.randomUUID().toString();
log.info("New token generated");
}
检查 cron 主页面以获取有关 cron 格式的更多信息:man7.org/linux/man-pages/man5/crontab.5.html.
触发一次性事件
如果您需要执行一次性事件,则可以直接将io.quarkus.scheduler.Scheduler类注入到您的代码中,并使用startTimer方法,该方法将在单独的线程中触发动作的执行。这可以在以下示例中看到:
@Inject
Scheduler scheduler;
public void oneTimeEvnt() {
scheduler.startTimer(300, () -> oneTimeAction());
}
public void oneTimeAction() {
// Do something
}
在这个简短的摘录中,我们可以看到单个事件,该事件将在oneTimeAction()方法中执行,将在300毫秒后触发一次性动作。
摘要
在本章中,我们介绍了一些我们可以使用的高级技术,以使用转换器和配置配置文件来管理我们的配置。我们还演示了如何注入不同的配置源,并优先于标准配置文件。在本章的第二部分,我们探讨了如何捕获应用程序生命周期的事件以及如何安排未来任务的执行。
为了使我们的应用更加可扩展,在下一章中,我们将讨论如何构建响应式应用,这些应用是事件驱动的且非阻塞的。请系好安全带!
第九章:使用 Vert.x 统一命令式和反应式
对于企业应用来说,最大的挑战之一传统上一直是将本质上是同步的业务操作与异步和事件驱动的操作结果调度相结合。在本章中,我们将学习 Vert.x 工具包如何通过结合标准命令式编程和可以在运行时创建、更改或组合的异步数据流来应对 Quarkus 应用中的这一挑战。到本章结束时,你应该能够熟练地在 JVM 上使用 Vert.x 编写反应式应用程序。
在本章中,我们将涵盖以下主题:
-
反应式编程和 Vert.x 工具包的简介
-
在 Quarkus 中可用的 Vert.x API 模型
-
使用 Vert.x 管理反应式 SQL 客户端
技术要求
你可以在 GitHub 上本章中找到项目的源代码:github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter09。
揭秘反应式编程和 Vert.x
命令式编程是大多数程序员每天编写代码的方式。等等——命令式编程是什么意思?用一个简洁的声明来说,我们可以这样说,命令式编程意味着代码行按顺序、逐条执行,如下面的示例所示:
URL url = new URL("http://acme.com/");
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
String line;
while ((line = in.readLine()) != null) {
System.out.println(line);
}
in.close();
如你所见,命令式编程可以使用循环或条件语句跳转到代码的不同部分。不过,不要被这个迷惑。只要你的调试器清楚地指向你的代码中的某个语句(因此很明显下一个将执行哪一行),你就肯定是在使用命令式编程。
虽然命令式编程模型显然更容易理解,但随着连接数量的增加,它可能会严重影响到可伸缩性。事实上,系统线程的数量将相应增加,这将导致你的操作系统花费大量的 CPU 周期仅仅是为了线程调度管理。这就是 Vert.x 发挥作用的地方。
让我们从定义开始:Vert.x 究竟是什么?Vert.x 不是一个应用服务器或框架,而仅仅是一个工具包,或者如果你愿意,是一组可以添加到你的项目中的普通 JAR 文件。简单易懂。你也不需要特定的开发环境或插件来使用 Vert.x 开发应用程序。
在其核心,Vert.x 是一个满足反应式宣言(www.reactivemanifesto.org/)要求的反应式工具包。这些要求可以总结为以下几点:
-
响应性: 一个反应式系统需要能够以合理的时间处理请求。
-
弹性: 一个反应式系统必须被设计成能够处理故障,并且适当地处理它们。
-
弹性:反应式系统必须能够根据负载进行扩展和缩减,而不会影响系统的响应性。
-
消息驱动:反应式系统的组件通过交换异步消息相互交互。
根据前面的观点,很明显,Vert.x 推动了一种新的设计和构建分布式系统的方法,同时将异步性、可扩展性和反应性注入到应用程序的核心。因此,关于我们之前的示例,它可以以反应式的方式重写,如下所示:
vertx.createHttpClient().getNow(80, "acme.com", "", response -> {
response.bodyHandler(System.out::println);
});
与前面的示例不同,使用 Vert.x 时,在建立与 HTTP 服务器的连接期间,运行线程被释放。然后,当收到响应时,一个作为 Lambda 表达式编写的处理器(docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html)被调用来处理响应。
在前面的示例中,每次你扩展 Vert.x 的基本部署单元,即所谓的Verticle时,都可以在你的代码中使用vertx字段。本质上,Verticle 通过事件循环处理传入的事件,为异步编程模型奠定基础。Verticle 可以用各种语言编写,而不仅仅是 Java,因此你可以将不同的环境作为更大反应式系统的一部分进行混合。
允许不同的 Verticle 之间相互通信的主要工具被称为事件总线,通信通过异步消息传递进行。以下图表显示了事件总线如何融入此架构:

事件总线不对你使用的数据格式有任何限制,尽管 JSON 是首选的交换格式,因为它是一种流行的数据结构化选项,允许用不同语言编写的 Verticle 进行通信。事件总线支持以下通信模式:
-
点对点消息传递,意味着消息被路由到在该地址注册的处理器之一。
-
请求-响应消息传递,与点对点消息传递类似,但它在发送消息时可以指定一个可选的回复处理器,以便接收者可以决定是否回复消息。如果他们这样做,回复处理器将被调用。
-
发布-订阅,允许你使用发布函数广播消息。在这种情况下,事件总线将消息路由到所有在该地址注册的处理器。
由于存在多种通信模式,已经为 Vert.x 设计了多个 API 模型,并且所有这些模型都围绕通过回调以异步方式执行流程的概念。下一节将讨论 Quarkus 中可用的各种 Vert.x API 模型。
Quarkus 中的 Vert.x API 模型
Vert.x 提供了一整套集成到 Quarkus 中的反应式 API。更具体地说,Quarkus 通过提供一个单一依赖项到你的应用程序来使用 Vert.x 作为反应式引擎:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx</artifactId>
</dependency>
这允许你通过简单的代码注入访问一个管理的 Vert.x 实例:
@Inject io.vertx.core.Vertx vertx;
Vertx对象是 Vert.x 应用程序的控制中心。它是你通往 Vert.x 世界的通行证,允许你创建异步和非阻塞的客户端和服务器,获取 Event Bus 的引用,以及许多其他功能。
然而,当在 Quarkus 中使用 Vert.x API 时,没有Vertx对象供你访问。事实上,Quarkus 提供了三个不同的 Vert.x API:
-
io.vertx.core.Vertx:这是 Vert.x 核心 API 的入口点,允许你使用回调实现异步和非阻塞的客户端和服务器。 -
io.vertx.reactivex.core.Vertx:这个 API 允许我们在 Vert.x 应用程序中可以使用流或异步结果的地方使用可观察模式。此外,它还允许我们在流上使用大量数据转换操作符。 -
io.vertx.axle.core.Vertx:这个 API 专门设计用于与 Quarkus 的 MicroProfile 模式集成,为发送和接收异步消息提供了一个坚实的基础,从而在服务之间强制松耦合。
为了了解 Vert.x 的三个不同变体,我们在本书 GitHub 仓库的Chapter09文件夹中提供了相同数量的示例。让我们详细看看它们。
管理 Vert.x 核心 API
为了学习 Vert.x 核心 API,我们将使用我们之前讨论的基本客户服务应用程序的修改版,该应用程序在第四章中介绍,将 Web 接口添加到 Quarkus 服务中。你可以在本书 GitHub 仓库的Chapter09/core/customer-service文件夹中找到这个示例的源代码。我们建议你将项目导入到你的 IDE 中。
现在,让我们直接进入代码。由于 Vert.x 核心 API 基于回调机制,为了利用异步和非阻塞 API,在我们的客户服务示例中,我们添加了两个函数,用于从文件系统中以 JSON 格式读取和写入客户列表。我们的客户列表应该写在哪里?答案是写入application.properties文件,它定义了一个名为file.path的属性,客户列表将写入该位置:
file.path=/tmp/customer.json
现在,让我们看看代码。负责提供客户数据的核心类是CustomerRepository。在这里,我们将注入一个io.vertx.core.Vertx实例。我们还将注入数据存储的路径:
public class CustomerRepository {
@Inject io.vertx.core.Vertx vertx;
@ConfigProperty(name = "file.path" )
String path;
现在来到了有趣的部分,那就是编写一个使用vertx实例在文件系统中展平我们的客户列表的方法:
public CompletionStage<String> writeFile( ) {
JsonArrayBuilder jsonArray = javax.json.Json.createArrayBuilder();
for (Customer customer:customerList) {
jsonArray.add(javax.json.Json.createObjectBuilder().
add("id", customer.getId())
.add("name", customer.getName())
.add("surname", customer.getSurname()).build());
}
JsonArray array = jsonArray.build();
CompletableFuture<String> future = new CompletableFuture<>();
vertx.fileSystem().writeFile(path, Buffer.buffer(array
.toString()), handler -> {
if (handler.succeeded()) {
future.complete("Written JSON file in " +path);
} else {
System.err.println("Error while writing in file:
" + handler.cause().getMessage());
}
});
return future;
}
你可能首先注意到的是 CompletionStage 方法的签名。如果你已经编写过异步 Java 代码,你可能对 java.util.concurrent.Future API 很熟悉。它用于以下操作:
-
通过
isDone()方法检查执行是否已完成 -
使用
cancel()方法取消执行 -
使用阻塞的
get()方法获取执行结果
这种方法的重大局限性是调用者无法手动完成任务,也不能链式调用多个 Future 执行。
另一方面,CompletionStage 基于阶段的概念,这些阶段被视为多个中间计算,可能是异步的,也可能不是。无论如何,我们必须在达到最终结果之前完成它们。这些中间计算被称为 完成阶段。
通过使用 CompletionStage 阶段,你可以通过以下方式轻松解决 java.util.concurrent.Future API 的局限性:
-
使用
complete(T value)手动完成CompletableStage -
在一个块中链式调用多个
CompletableStage
让我们回到我们的例子。一旦我们从客户列表中创建出 JsonArray,我们就可以使用 Vert.x 核心 API 访问我们的 FileSystem。我们还可以注册一个处理程序,负责在文件成功写入后立即完成我们的 CompletionStage。
让我们来看看负责读取包含客户列表的文件的 readFile 方法:
public CompletionStage<String> readFile() {
CompletableFuture<String> future = new CompletableFuture<>();
long start = System.nanoTime();
// Delay reply by 100ms
vertx.setTimer(100, l -> {
// Compute elapsed time in milliseconds
long duration = MILLISECONDS.convert(System.nanoTime() -
start, NANOSECONDS);
vertx.fileSystem().readFile(path, ar -> {
if (ar.succeeded()) {
String response = ar.result().toString("UTF-8");
future.complete(response);
} else {
future.complete("Cannot read the file: " +
ar.cause().getMessage());
}
});
});
return future;
}
readFile 方法故意设计得稍微复杂一些。事实上,我们已经将两个不同的阶段链入其中。第一个阶段执行一个一次性定时器,将在 100 毫秒后触发下一次执行。定时器是 Vert.x 的核心结构,应该在任何需要延迟某些代码执行或重复执行代码的地方使用:
vertx.setPeriodic(1000, l -> {
// This code will be called every second
System.out.println("timer fired!");
});
在 Vert.x 术语中,无论在何种情况下,定时器都是你可以用来延迟执行而不是其他机制(如 Thread.sleep),后者会阻塞事件循环,因此 绝对 不应该在 Vert.x 上下文中使用。
如果你忘记了我们的温和警告,Vert.x 将会在你每次尝试在 Vert.x 上下文中使用阻塞代码时提醒你,日志消息类似于 Thread vertx-eventloop-thread-1 has been blocked for 22258 ms。
readFile 方法的剩余部分与 writeFile 方法的操作正好相反;也就是说,它读取 JSON 文件并在文件读取完成后立即完成阶段。
为了将此功能暴露给客户端应用程序,我们在 CustomerEndpoint 类中添加了两个包装方法,以便通过 REST API 暴露函数:
@GET
@Path("writefile")
@Produces("text/plain")
public CompletionStage<String> writeFile() {
return customerRepository.writeFile();
}
@GET
@Path("readfile")
public CompletionStage<String> readFile() {
return customerRepository.readFile();
}
值得注意的是,writeFile 方法产生文本信息,因为它应该返回一个简单的文本消息给调用者。另一方面,readFile 方法依赖于类的默认 application/json 格式来显示 JSON 文本文件。
现在,让我们转向客户端。我们可以使用两个额外的 AngularJS 处理程序轻松捕获CompletionStage事件,这些处理程序将立即捕获结果:
$scope.writefile = function () {
$http({
method: 'GET',
url: SERVER_URL+'/writefile'
}).then(_successStage, _error);
};
scope.readfile = function () {
$http({
method: 'GET',
url: SERVER_URL+'/readfile'
}).then(_successStage, _error);
};
function _successStage(response) {
_clearForm()
$scope.jsonfile = JSON.stringify(response.data);
}
通过在主页上添加两个简单的按钮,这两个函数将被触发:
<a ng-click="writefile()" class="myButton">Write File</a>
<a ng-click="readfile()" class="myButton">Read File</a>
除了做这件事,我们还向我们的 HTML 架构中添加了一个div部分,信息将在这里显示:
<div ng-app="displayfile" >
<span ng-bind="jsonfile"></span>
</div>
不再拖延,让我们使用以下命令构建和运行应用程序:
mvn install quarkus:dev
以下是我们新的 UI,包括读取文件和写入文件的按钮。我们已经保存了一组Customer对象,如下面的截图所示:

相反,如果我们点击“读取文件”按钮,其内容将以 JSON 格式显示在页面下方的div中:

我们已经完成了第一轮的 Vert.x 核心。现在,让我们继续前进,看看如何使用ReactiveX(RxJava)与 Vert.x 结合使用。
管理 Vert.x API for RxJava
RxJava (github.com/ReactiveX/RxJava) 是一个 Java 库,它允许你使用 Java VM 的Observable序列创建异步和基于事件的程序。为了理解这个框架的核心特性,我们需要定义 ReactiveX 的核心角色,如下所示:
-
Observables:这些代表要发出数据的数据源。一旦订阅者开始监听,Observable 就开始提供数据。Observable 可以发出可变数量的项目,并最终以成功或错误结束。
-
订阅者:这些监听由 Observable 发出的事件。一个 Observable 可以有零个或多个订阅者。
以下图表显示了这两个组件之间的关系:

根据发出的项目数量和对项目流的控制,我们可以区分不同类型的 Observables:
| Observable 类型 | 描述 |
|---|---|
Flowable<T> |
发出0或n个项目,并以成功或错误事件结束。支持背压,这允许我们控制源发射的速率。 |
Observable<T> |
发出0或n个项目,并以成功或错误事件结束。 |
Single<T> |
发出单个值或错误通知。 |
Maybe<T> |
发出一个项目、没有项目或错误事件。可选调用的响应式版本。 |
Completable |
包裹延迟计算,没有值,但仅作为完成或异常的指示。 |
让我们提供一个简约的例子。以下是一个Observable发出单个项目的Hello world示例:
Observable.just("Hello world!").subscribe(System.out::println);
当订阅者收到项目时,它只是简单地将其打印到输出流中。下面的代码略有不同,因为它使用了一个 Flowable 可观察对象来控制在高速率推送数据时项目的流动,这可能会使你的订阅者过载:
Flowable.just("Hello world!").subscribe(System.out::println);
RxJava 编程的一个重要概念是操作符;操作符是一个函数,它定义了 Observable 以及它如何以及何时应该发射数据流。我们已经遇到过一个,那就是 just 操作符,它允许你将一个对象或一组对象转换为 Observable。在我们的第一个例子中,对象是 Hello world 字符串。
有许多其他的操作符,所有这些都可以在 RxJava 的文档中找到(reactivex.io/documentation/operators.html)。例如,你可以使用 distinct 操作符在数据流中抑制重复项:
Observable.just(2, 3, 4, 4, 2, 1)
.distinct()
.subscribe(System.out::println);
在这种情况下,订阅者预期的输出如下:
2,3,4,1
你还可以链式添加另一个操作符来过滤掉不符合模式的项,如下所示:
Observable.just(1, 2, 3, 4, 5, 6)
.distinct()
.filter(x -> x % 2 == 0)
.subscribe(System.out::println);
如你所猜,输出将进一步限制如下:
2,4
尽管我们对 RxJava 的强大功能只是浅尝辄止,但我们已经具备了将这些概念应用到我们的示例应用程序中的基本背景。
使用 Quarkus 与 RxJava
为了了解 RxJava,我们将通过本书 GitHub 仓库中 Chapter09/rx2java/customer-service 文件夹中的示例进行学习。
你首先应该意识到的是,为了使用 RxJava 与 Quarkus,你必须添加一个 Vertx 实例,该实例位于 io.vertx.reativex.core 命名空间下:
@Inject io.vertx.reactivex.core.Vertx vertx;
话虽如此,将 ReactiveX 包含到我们的项目中的一大主要优势是它将极大地增强在可观察对象和订阅者之间流动的数据的转换能力。
例如,让我们看看以下用例:
-
我们想要生成一个包含要导入电子表格的客户列表的文件。因此,我们将从我们的客户列表中创建一个普通的 CSV 文件。
-
然后,我们希望将 CSV 文件转换为客户的
toString方法中编码的任何其他格式。
让我们学习如何对 CustomerRepository 类进行适当的修改。正如我们之前提到的,第一个修改是将 io.vertx.core.Vertx 实例替换为相应的 io.vertx.reativex.core.Vertx。然后,我们将对 writeFile 和 readFile 方法进行一些修改。让我们首先从 writeFile 方法开始:
public CompletionStage<String> writeFile() {
CompletableFuture<String> future = new CompletableFuture<>();
StringBuffer sb = new StringBuffer("id,name,surname");
sb.append(System.lineSeparator());
Observable.fromIterable(customerList)
.map(c -> c.getId() + "," + c.getName() + "," +
c.getSurname() + System.lineSeparator())
.subscribe(
data -> sb.append(data),
error -> System.err.println(error),
() -> vertx.fileSystem().writeFile(path,
Buffer.buffer(sb.toString()), handler -> {
if (handler.succeeded()) {
future.complete("File written in "+path);
} else {
System.err.println("Error while
writing in file: " + handler.cause()
.getMessage());
}
}));
return future;
}
如果你觉得我们对可观察对象的介绍直观易懂,那么前面的代码尽管使用了大量的 Lambda 表达式,看起来也不会过于复杂。在这里,我们添加了一系列操作符以产生所需的结果。
首先,我们通过使用Observable.fromIterable运算符遍历客户列表产生了一组可观察对象。由于我们需要生成一个 CSV 文件,我们需要将单个客户字段映射到 CSV 格式,该格式使用逗号(,)来分隔值。我们为此使用了map运算符。然后,我们完成了转换,结果将是一个列表,其中包含我们选择格式的可观察对象。
对于一个观察者(或订阅者)要看到由Observable发出的项目,以及来自Observable的错误或完成通知,它必须使用subscribe运算符订阅该Observable。简而言之,subscribe运算符是连接订阅者与Observable的粘合剂。
当有新项目添加时,我们的订阅者将收到通知,以便它们可以被追加到已经初始化了 CSV 标题的StringBuffer中。在出现错误的情况下,订阅者也会收到通知,最终,当项目流完成时,通过()处理程序。在这种情况下,CSV 文件将使用writeFile函数写入文件系统,该函数也存在于io.vertx.reativex.core.Vertx文件系统上下文中。
然后,readFile方法需要将我们已写入的 CSV 文件反转成Customer对象表示的形式,如它的toString方法所提供的。代码如下:
public CompletionStage<String> readFile() {
CompletableFuture<String> future = new CompletableFuture<>();
StringBuffer sb = new StringBuffer();
vertx.fileSystem().rxReadFile(path)
.flatMapObservable(buffer ->
Observable.fromArray(buffer.toString().split(System.
lineSeparator())))
.skip(1)
.map(s -> s.split(","))
.map(data-> new Customer(Integer.
parseInt(data[0]),data[1],data[2]))
.subscribe(
data -> sb.append(data.toString()),
error -> System.err.println(error),
() -> future.complete(sb.toString()));
return future;
}
在这里,我们需要熟悉一些更多的运算符。由于我们想要逐行读取和处理文件,我们使用flatMapObservable运算符来产生我们的多个Observable实例数组。在实践中,这个运算符允许我们产生一组Observable实例,这是由我们 CSV 文件中的单行发出的函数产生的结果。
我们方便地使用字符串类的split方法将文件分割成一个数组。然后,我们使用skip运算符跳过了第一个项目,即 CSV 标题。之后,我们对数据应用了两个map转换:
-
第一个操作符创建了一个字符串对象数组,从 CSV 行中出来,使用逗号(
,)作为分隔符 -
接下来,我们使用从字符串数组中到达的数据创建了一个
Customer对象的实例
现在我们已经收集了我们的目标数据,即一个Customer对象,我们准备流式传输这些数据,这些数据最终将被订阅者收集。订阅者反过来接收每个项目,并将从它那里得到的toString()输出添加到StringBuffer中。你可以在你的toString()方法中包含任何格式,但为了简单起见,我们已经让我们的 IDE(IntelliJ IDEA)自动生成它:
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", surname='" + surname + '\'' +
'}';
}
我们将要做的最后一件事是设置readFile的媒体类型,使其与我们的toString数据格式保持一致。由于我们正在生成简单的文本,它将如下所示:
@GET
@Path("readfile")
@Produces("text/plain")
public CompletionStage<String> readFile() {
return customerRepository.readFile();
}
现在,您只需运行应用程序并检查新的结果。以下是在添加了一些客户并点击了“写入文件”按钮后,您的 UI 应该看起来像什么:

然后,通过点击“读取文件”按钮,下方的 HTML div 将包含每个客户的 toString 数据:

如您所见,尽管 UI 日志保持了极简风格,但在底层仍有大量工作在进行,以管理不同格式数据之间的转换。
那是我们对 Vert.x 和 Quarkus 的第二次实现。我们仍然需要处理第三个“野兽”,即 io.vertx.axle.core.Vertx。
使用 Vert.x 轴承库解耦事件
通常,我们希望将我们的服务入口点(适配器)与业务逻辑分开,后者是应用程序的一部分。一种常见的模式是将服务保存在一个独立的容器中,该容器被注入到我们的服务 REST 入口点。然而,当我们接近响应式编程时,我们可以通过引入 Vert.x Event Bus 来进一步解耦我们的组件。
在这种架构中,组件通过向虚拟地址发送消息来相互通信。为了管理消息的分配,以下组件可用:
-
EventBus:这是一个轻量级的分布式消息系统,允许以松耦合的方式在应用程序的不同部分之间进行通信。
-
消息:这包含从 Event Bus 在处理程序中接收到的数据。消息有一个体和一个头,两者都可以为 null。通过在消息中添加回复处理程序,可以将请求-响应模式应用于通信。
让我们通过使用 Chapter09/axle/customer-service 文件夹中可用的示例应用程序来学习如何通过使用简单的消息模式进行仪表化。
将 EventBus 层添加到 Quarkus 应用程序中
要在我们的应用程序中包含分布式对等消息模式,我们需要将 EventBus 实例注入到一个 CDI 容器中,该容器将充当接收者:
@Inject EventBus bus;
在我们的情况下,我们将 EventBus 添加到 CustomerEndpoint 类中。
请注意,每个 Vert.x 实例只有一个 Event Bus 实例。
现在,在同一个类中,让我们创建一个新的端点方法,它将负责分发消息:
@GET
@Path("/call")
@Produces("text/plain")
public CompletionStage<String> call(@QueryParam("id") Integer customerId) {
return bus.<String>send("callcustomer",
customerRepository.findCustomerById(customerId))
.thenApply(Message::body)
.exceptionally(Throwable::getMessage);
我们通过 "callcustomer" 地址在总线上传递消息。消息体包含 Customer 对象,该对象通过 findCustomerById 方法检索。在发生错误的情况下,将抛出一个包含错误 getMessage 内容的可抛出对象。
现在,我们需要一个消息消费者,因此我们将添加另一个名为 CustomerService 的类,其中包含一个被 @ConsumeEvent 注解的方法:
@ApplicationScoped
public class CustomerService {
@ConsumeEvent("callcustomer")
public String reply(Customer c) {
return "Hello! I am " + c.getName() + " "
+c.getSurname() + ". How are you doing?";
}
}
在@ConsumeEvent注解中,我们指定了消息被消费的地址。最终,我们只是返回一个包含客户消息的响应。
为了完成循环,我们需要进行以下更改:
- 我们需要将一个按钮添加到
index.html页面:
<a ng-click="call( customer )" class="myButton">Call</a>
- 我们需要添加一个额外的 AngularJS 控制器来处理响应,该控制器将在(一个弹窗中)显示以下消息:
$scope.call = function (customer) {
$http({
method: 'GET',
url: SERVER_URL+'/call/?id='+customer.id
}).then(_callCustomer, _error);
};
function _callCustomer(response) {
window.alert(response.data);
}
现在我们已经添加了一切,让我们运行我们的应用程序。
收敛应用程序
当所有更改都到位后,你应该能够看到呼叫按钮已经添加到每个客户的行中,如下面的截图所示:

当你点击呼叫按钮时,将通过事件总线发送一条消息。一旦被消费,你应该看到以下响应:

除了点对点消息之外,你还可以使用 Vert.x 轴 API 来流式传输服务器端事件(SSEs)。
使用 Vert.x 流式传输 SSE
传统上,Web 应用程序能够向服务器发送请求以接收响应;这就是标准范式。然而,通过服务器发送事件,服务器应用程序可以在任何时候通过向网页推送事件(消息)来向网页发送新数据。这些传入的消息被作为网页内部的事件与数据结合处理。
现在,让我们演示如何使用 Vert.x 轴 API 在 Quarkus 中流式传输 SSE。以下被包含在我们项目中的类,负责每两秒向主页发送一个 SSE:
@Path("/streaming")
public class StreamingEndpoint {
@Inject io.vertx.axle.core.Vertx vertx;
@Inject CustomerRepository customerRepository;
@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
public Publisher<String> stream() {
return
ReactiveStreams.fromPublisher
(vertx.periodicStream(2000).
toPublisher())
.map(l -> String.format
("Number of Customers %s .
Last one added: %s %n",customerRepository.
findAll().size(),
customerRepository.findAll().size()
> 0 ?
(customerRepository.findAll().
get(customerRepository.findAll().
size() -1)).toString() : "N/A"))
.buildRs();
}
}
首先,请注意我们正在使用io.vertx.axle.core.Vertx的一个实例来处理事件的流式传输。然后,我们的 REST 方法,绑定到"/streaming"URI,被注解为不同的媒体类型,即SERVER_SENT_EVENTS。该方法返回一个发布者类型,这是发布 Reactive Streams 所必需的。
通过使用ReactiveStreams.fromPublisher方法,我们根据vert.xperiodicStream指定的频率推送流事件。在我们的例子中,消息将每两秒分发一次。在发送实际事件之前,内容将通过map操作符进行转换,这将创建一个包含一些Customer统计信息(如客户数量和最后添加的客户)的消息。通过使用三元操作符,我们成功地将这个逻辑压缩成只有一句话,但牺牲了稍微复杂一点的可读性。
服务器端你需要的就是这些。在客户端,我们做了一些其他的适配:
- 我们添加了一个额外的按钮来触发 SSE:
<a ng-click="stats()" class="myButton">Stats</a></div>
- 我们在 JavaScript 中添加了一个回调方法来处理接收的事件:
$scope.stats = function () {
var eventSource = new EventSource("/streaming");
eventSource.onmessage = function (event) {
var container = document.getElementById("divcontainer");
var paragraph = document.createElement("p");
paragraph.innerHTML = event.data;
container.appendChild(paragraph);
};
- 我们添加了一个
div,消息将在其中显示:
<div id="divcontainer" style="width: 800px; height: 200px; overflow-y: scroll;">
当我们运行更新后的应用程序时,预期的结果是包含底部 Stats 按钮的 UI:

较低级别的 div 将每两秒更新一次,基于客户列表中的数据。
取消事件
值得注意的是,可以通过保持对 Subscription 对象的引用来取消 SSE 订阅,这样你就可以在任何时候取消你的订阅:
publisher
.subscribe(new Subscriber<String>() {
volatile Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
}
@Override
public void onNext(String s) {
// when no more event is needed
subscription.cancel();
}
@Override
public void onError(Throwable throwable) {
// handle error
}
@Override
public void onComplete() {
// handle complete
}
});
在前面的代码片段中,当事件发出时,观察者的 onNext 方法会使用项目被调用,紧接着调用 onComplete 方法。另一方面,当回调是失败时,观察者的 onError 方法会被调用。在任何回调方法中,我们都可以使用订阅对象的 cancel 方法来取消订阅。
这是我们与 Reactive Events 的最后一次合作,但不是与 Vert.x 的最后一次。我们还有一件事要讲:Quarkus 的 Reactive SQL 客户端。这是一个专注于具有最小开销的可扩展 JDBC 连接的 API。
管理反应式 SQL 客户端
反应式 SQL 客户端是一个 API,允许你使用 Vert.x 的反应式和非阻塞特性来访问关系数据库。这在访问数据的方式上带来了一些变化。让我们把成本和收益列出来:
-
一方面,你需要使用 SQL 语句来启用你的 RDBMS 访问数据,而不是抽象的 HQL。由于 Hibernate 不再适用,Java 类和数据库之间的自动映射也不再可用。
-
另一方面,你将能够使用完全事件驱动、非阻塞、轻量级的替代方案来流式传输 SQL 语句的结果。
根据你的需求,你可以继续使用 Hibernate 的 API 或切换到反应式 SQL 客户端。假设你很勇敢,想要切换到反应式 SQL。为此,你需要配置你的应用程序,使其能够使用 PostgreSQL 反应式客户端 API。
配置应用程序以使用 PostgreSQL 反应式客户端
为了深入了解反应式客户端 API,请参阅本书 GitHub 仓库中 Chapter09/pgpool 文件夹中的示例。由于此示例不会使用 PostgreSQL JDBC 驱动程序,以下依赖项已被添加为替代品:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
我们添加的另一个配置是 JDBC URL,它需要以下格式:
vertx-reactive:postgresql://<Host>:<Port>/<DBName>
因此,在我们的示例中,我们将在 application.properties 中添加此设置:
quarkus.datasource.url=vertx-reactive:postgresql://localhost:5432/quarkusdb
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
现在,让我们看看我们应用程序中的变化。为了尽可能保持简单,我们将示例分解,使其仅使用 CustomerEndpoint 和 Customer POJO 类。
让我们从 CustomerEndpoint 开始,它需要访问 io.vertx.axle.pgclient.PgPool 和 io.vertx.core.Vertx:
public class CustomerEndpoint {
@Inject PgPool client;
@Inject Vertx vertx;
在同一个类中,我们添加了一个 init 方法,在启动时创建一些数据:
@PostConstruct
private void initdb() {
client.query("DROP TABLE IF EXISTS CUSTOMER")
.thenCompose(r -> client.query("CREATE SEQUENCE IF
NOT EXISTS customerId_seq"))
.thenCompose(r -> client.query("CREATE TABLE CUSTOMER
(id SERIAL PRIMARY KEY, name TEXT NOT NULL,surname
TEXT NOT NULL)"))
.thenCompose(r -> client.query("INSERT INTO CUSTOMER
(id, name, surname) VALUES ( nextval('customerId
_seq'), 'John','Doe')"))
.thenCompose(r -> client.query("INSERT INTO CUSTOMER
(id, name, surname) VALUES ( nextval('customerId
_seq'), 'Fred','Smith')"))
.toCompletableFuture()
.join();
}
Pgpool 的query方法返回一个包含数据RowSet的CompletionStage对象,这是查询的结果。请注意我们如何将多个语句链接起来以生成一个CompletableFuture,它将在另一个线程中启动执行。在这个简单的方法中,你可以体验一下当涉及到创建事件驱动的、非阻塞的 SQL 执行时,反应式 SQL 客户端有多么强大。你最终将通过在CompletableFuture的末尾执行join方法来获得所有语句的合并结果。
CustomerEndpoint的其他方法使用相同的组合模式将 CRUD 语句的执行委托给Customer类:
@GET
public CompletionStage<Response> getAll() {
return Customer.findAll(client).thenApply(Response::ok)
.thenApply(ResponseBuilder::build);
}
@POST
public CompletionStage<Response> create(Customer customer) {
return customer.create(client).thenApply(Response::ok)
.thenApply(ResponseBuilder::build);
}
@PUT
public CompletionStage<Response> update(Customer customer) {
return customer.update(client)
.thenApply(updated -> updated ? Status.OK :
Status.NOT_FOUND)
.thenApply(status -> Response.status(status).build());
}
@DELETE
public CompletionStage<Response> delete(@QueryParam("id") Long customerId) {
return Customer.delete(client, customerId)
.thenApply(deleted -> deleted ? Status.NO_CONTENT :
Status.NOT_FOUND)
.thenApply(status -> Response.status(status).build());
}
在Customer类中,我们编写了所有执行 CRUD 操作所需的方法。第一个方法create通过使用PreparedStatement在CUSTOMER表中执行一个INSERT操作,将包含姓名和姓氏的元组作为参数:
public CompletionStage<Long> create(PgPool client) {
return client.preparedQuery("INSERT INTO CUSTOMER (id, name,
surname) VALUES ( nextval('customerId_seq'), $1,$2)
RETURNING (id)", Tuple.of(name,surname))
.thenApply(pgRowSet -> pgRowSet.iterator()
.next().getLong("id"));
}
同样地,update方法通过PreparedStatement执行UPDATE操作,并将客户的元组数据作为参数应用:
public CompletionStage<Boolean> update(PgPool client) {
return client.preparedQuery("UPDATE CUSTOMER SET name = $1,
surname = $2 WHERE id = $3", Tuple.of(name, surname, id))
.thenApply(pgRowSet -> pgRowSet.rowCount() == 1);
}
要删除客户,delete方法执行PreparedStatement,它使用客户id作为参数:
public static CompletionStage<Boolean> delete(PgPool client, Long id) {
return client.preparedQuery("DELETE FROM CUSTOMER WHERE
id = $1", Tuple.of(id))
.thenApply(pgRowSet -> pgRowSet.rowCount() == 1);
}
最后,使用findAll方法从数据库查询客户列表,并将它们作为 Java 列表返回:
public static CompletionStage<List<Customer>> findAll(PgPool client) {
return client.query("SELECT id, name, surname FROM CUSTOMER
ORDER BY name ASC").thenApply(pgRowSet -> {
List<Customer> list = new ArrayList<>(pgRowSet.size());
for (Row row : pgRowSet) {
list.add(from(row));
}
return list;
});
}
我们已经完成了应用程序的编码。让我们让它运行起来!
运行示例
在运行示例之前,请确保你已经初始化了 PostgreSQL 数据库;否则,当应用程序部署时,初始语句将失败:
$ docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_test -e POSTGRES_USER=quarkus -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=quarkusdb -p 5432:5432 postgres:10.5
然后,使用以下命令按常规运行应用程序:
mvn install quarkus:dev
UI 隐藏了我们从普通对象切换到真实数据库的事实,尽管你可以从页面标题中推断出这一点,现在标题是 Quarkus Vert.X PgPool Example:

然而,如果你登录到数据库容器,你可以确认Customer表已经创建并包含其项。让我们找到用于此目的的容器 ID:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6b1b13b0547f postgres:10.5 "docker-entrypoint..." 2 minutes ago Up 2 minutes 0.0.0.0:5432->5432/tcp quarkus_test
现在,让我们使用docker exec命令进入 PostgreSQL 容器的 bash shell:
$ docker exec -it 6b1b13b0547f /bin/bash
root@6b1b13b0547f:/# psql -U postgres
psql (10.5 (Debian 10.5-2.pgdg90+1))
Type "help" for help.
你可以使用\dt快捷键检查关系列表:
postgres=# \dt;
List of relations
Schema | Name | Type | Owner
--------+----------+-------+----------
public | customer | table | postgres
(1 row)
我们也可以按照以下方式查询Customer表的行:
postgres=# select * from customer;
id | name | surname
----+------+---------
5 | John | Doe
6 | Fred | Smith
(2 rows)
太好了!我们已经完成了我们的第一个使用 Quarkus 的反应式 SQL 应用程序。这也标志着我们进入 Vert.x 世界的旅程的结束。
摘要
通过对响应式编程的快速浏览,你应该已经熟练掌握了在 JVM 上编写响应式应用程序。你的编程技能现在包括如何使用 Vert.x 核心 API 编写异步和非阻塞服务。你还学习了如何使用 Vert.x 响应式 API 将Observable模式与流或异步结果相结合。然后,我们迅速探索了最后一个 Vert.x 范式,Vert.x Axle,它允许不同的 bean 通过异步消息进行交互并强制松耦合。最后,我们应用了响应式 API,通过 Vert.x 的 PostgreSQL 客户端扩展来访问关系数据库。
尽管你已经掌握了响应式编程 API,请注意,其大部分功能只有在构建实时数据管道和流式数据时才能充分发挥。我们将在下一章中介绍这些内容。
第十章:使用 Quarkus 进行响应式消息传递
在本章中,我们将学习 SmallRye 响应式消息传递的细节,它可以在 Quarkus 中用于实现 Eclipse MicroProfile 响应式消息传递规范。到本章结束时,您将拥有一个坚实的数据流应用程序开发模型,并了解如何连接到 Apache Kafka 和 ActiveMQ 等流平台。
在本章中,我们将涵盖以下主题:
-
开始使用响应式消息传递
-
使用 Apache Kafka 流式传输消息
-
使用 高级消息队列协议(AMQP)流式传输消息
技术要求
您可以在 GitHub 上的本章中找到项目的源代码:github.com/PacktPublishing/Hands-On-Cloud-Native-Applications-with-Java-and-Quarkus/tree/master/Chapter10。
开始使用响应式消息传递
响应式流是一个旨在提供跨异步边界交换数据流标准的倡议。同时,它保证接收方不会被强制缓冲任意数量的数据。
有几种可用的响应式流实现,我们已经学习了如何在 Vert.x 中实现响应式编程。在本章中,我们将使用 SmallRye 响应式消息传递实现来补充我们的知识,向您展示如何通过最小配置更改将其与 Apache Kafka 或 ActiveMQ 等流平台或消息代理集成。
为了熟悉 MicroProfile 响应式消息传递,我们需要了解一些关键概念。首先,MicroProfile 响应式消息传递是一个使用 CDI 容器来驱动消息流向特定通道的规范。
消息是一个基本接口,包含要流式传输的有效负载。Message 接口是参数化的,以便描述它包含的有效负载类型。此外,Message 接口还包含特定于用于消息交换的代理(例如 Kafka 或 AMQ)的属性和元数据。
另一方面,通道是一个字符串,指示使用哪个消息的源或目的地。有两种类型的通道:
-
内部通道位于应用程序内部,用于实现消息的多步骤处理过程。
-
远程通道通过连接器连接到远程代理(例如 Apache Kafka 或 AMQ)。
由于 MicroProfile 响应式消息传递完全受 CDI 模型管理,因此使用两个核心注解来指示方法是否是消息的生产者或消费者:
@Incoming: 该注解用于方法上,表示它从指定的通道消费消息。通道的名称作为属性添加到注解中。以下是一个示例:
@Incoming("channel")
public void consume(Message<String> s) {
// Consume message here:
}
将此注解放置在方法上的效果是,每次向该通道发送消息时都会调用该方法。从用户的角度来看,它对传入消息是否来自本地 CDI 实例或远程代理是完全透明的。然而,您可能决定明确指出该方法消费特定类型的消息,例如 KafkaMessage(它继承自 Message)。以下是一个示例:
@Incoming("channel")
public void consume(KafkaMessage<String> s) {
// Consume message here:
}
@Outgoing:这个注解表示一个方法向一个通道发布消息。在许多方面,通道的名称都在注解的属性中声明:
@Outgoing("channel")
public Message<String> produce() {
// Produce and return a Message implementation
}
在注解了 @Outgoing 的方法中,我们返回 Message 接口的具体实现。
注意,您只能为单个通道使用 @Outgoing 注解一个方法。如果您尝试在多个 @Outgoing 注解的方法中使用相同的通道,则在部署时将发出错误。
您还可以使用 @Incoming 和 @Outgoing 同时注解一个方法,使其表现得像一个消息处理器,它转换消息数据的内容:
@Incoming("from")
@Outgoing("to")
public String translate(String text) {
return MyTranslator.translate(text);
}
从前面的示例中,我们可以看到消息从 @Outgoing 流生产者流向 @Incoming 流消费者,并且反应式消息透明地连接了这两个端点。为了解耦 Producer 和 Consumer 消息,您可以使用 MicroProfile API 提供的连接器添加一个组件,如 Apache Kafka。在下一节中,我们将介绍使用 Apache Kafka 的第一个反应式消息示例。
使用 Apache Kafka 进行流消息
Apache Kafka (kafka.apache.org/) 是一个分布式数据流平台,可以用于以惊人的速度实时发布、订阅、存储和处理来自多个源的数据流。
Apache Kafka 可以集成到流数据管道中,这些管道在系统之间分配数据,也可以集成到消费这些数据的系统和应用程序中。由于 Apache Kafka 减少了数据共享的点对点集成需求,因此它非常适合需要高吞吐量和可扩展性的各种用例。
此外,一旦将 Kafka 与 Kubernetes 结合使用,您将获得 Kafka 的所有优势,以及 Kubernetes 的以下优势:
-
可扩展性和高可用性:您可以使用 Kubernetes 轻松地进行资源的扩展和缩减,这意味着您可以在保证 Kafka 集群高可用性的同时,自动确定 Apache Kafka 将与其他应用程序共享的资源池。
-
可移植性:通过在 Kubernetes 上运行 Kafka,您的 Kafka 节点集群可以跨越本地和公有、私有或混合云,甚至可以使用不同的操作系统。
要管理 Kafka 环境,你需要一个名为 ZooKeeper 的软件,它负责命名和配置数据,以便在分布式系统中提供灵活且强大的同步。ZooKeeper 控制着 Kafka 集群节点的状态,并跟踪 Kafka 主题、分区以及你需要的所有 Kafka 服务。虽然在本章中不会详细介绍 ZooKeeper 的管理细节,但值得一提的是它在 Kafka 管理员职位中的角色,因为你需要掌握它才能胜任这份工作。
为了展示 Apache Kafka 和 MicroProfile Streaming 在 Quarkus 上的强大组合,我们将设计一个简单的应用程序,该程序模拟实时更新的股票交易行情,通过购买和销售进行更新。准备好并开始营业吧!
组装我们的股票交易应用程序
让我们从我们的股票交易应用程序的架构开始。为了设置一个具有最小复杂度的应用程序,我们将创建以下通道:
-
一个绑定到 "stock-quote" 通道的 流出 生产者,其中包含股票订单的消息将被写入名为 "stocks" 的主题
-
一个绑定到 "stocks" 通道的 流入 消费者,它读取 "stocks" 主题中可用的消息
-
一个绑定到 "in-memory-stream" 通道的 流出 生产者,它将新的股票报价广播给所有内部可用的订阅者
-
一个绑定到 "in-memory-stream" 通道的 流入 消费者,它读取新的股票报价并将其作为 SSE 发送给客户端
以下图表描述了我们将用于示例的基本消息流:

示例应用程序可以在本书 GitHub 存储库的 Chapter10/kafka 文件夹中找到。我们建议在继续之前将项目导入到您的 IDE 中。
如您从本项目的 pom.xml 文件中看到的,我们已包含以下扩展,以便我们可以将消息流式传输到 Apache Kafka 服务器:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
</dependency>
在我们深入代码之前,我们需要满足一些要求才能在容器中运行 Kafka。正如我们之前提到的,Kafka 需要 ZooKeeper 来管理其集群,因此我们需要启动这两个服务。在开发或测试环境中,你可以使用一个实用的解决方案,即 Docker Compose,这是一个用于管理和同步单个 YAML 格式配置文件中多个容器应用的工具。
Docker Compose 的安装细节可以在其文档页面上找到 (docs.docker.com/compose/install/),但对于 Linux 机器,你可以使用以下 shell 命令安装其稳定版本:
sudo curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
完成后,请对 docker-compose 工具申请适当的权限:
chmod a+x /usr/local/bin/docker-compose
现在,你可以按照以下方式验证已安装的版本:
docker-compose --version
你应该看到以下输出:
docker-compose version 1.24.1, build 1110ad01
现在我们完成了初步需求,是时候添加一些代码行了!
编码 bean 类
我们将添加的第一个类是 QuoteGenerated,它是一个 ApplicationScoped CDI bean,每两秒为一家公司生成随机报价。以下是这个类的代码:
@ApplicationScoped
public class QuoteGenerator {
private Random random = new Random();
@Outgoing("stock-quote")
public Flowable<String> generate() {
return Flowable.interval(2, TimeUnit.SECONDS)
.map(tick -> generateOrder(random.nextInt(2),
random.nextInt(5), random.nextInt(100)));
}
private String generateOrder(int type, int company, int amount) {
Jsonb jsonb = JsonbBuilder.create();
Operation operation = new Operation(type, Company.values()
[company], amount);
return jsonb.toJson(operation);
}
}
这个类通过 "stock-quote" 通道生成将被写入 Kafka 的消息。消息包含通过三个参数随机生成的股票订单:
-
订单类型(卖出/买入)
-
公司名称
-
卖出/买入的股票数量
到了最后,generate 方法将生成一个包含 JSON 字符串的消息,类似于以下内容:
{"amount":32,"company":"Soylent","type":0}
为了更好地理解辅助组件,以下是 Company 枚举,其中包含以下公司集合:
public enum Company {
Acme, Globex, Umbrella, Soylent, Initech
}
我们还需要 Operation 类的核心部分,它是一个 Java POJO,用于存储每个股票订单的数据:
public class Operation {
public static final int SELL = 0;
public static final int BUY = 1;
private int amount;
private Company company;
private int type;
public Operation(int type, Company company, int amount) {
this.amount = amount;
this.company = company;
this.type = type;
}
// Getters/Setters method omitted for brevity
}
现在,简要介绍一下华尔街 101:每个股票订单将决定一家公司的报价变化。简单来说,通过卖出股票,公司的价格会下降,而买入订单会使股票需求增加,这意味着价格会上涨。卖出的/买入的股票数量最终将决定价格上升和下降的幅度。
以下 QuoteConverter 类将负责将股票订单转换为交易涉及的 Company 的新报价:
@ApplicationScoped
public class QuoteConverter {
HashMap<String,Double> quotes;
private Random random = new Random();
@PostConstruct
public void init() {
quotes = new HashMap<>();
for (Company company: Company.values())
quotes.put(company.name(), new Double(random.nextInt
(100) + 50));
}
@Incoming("stocks")
@Outgoing("in-memory-stream")
@Broadcast
public String newQuote(String quoteJson) {
Jsonb jsonb = JsonbBuilder.create();
Operation operation = jsonb.fromJson(quoteJson,
Operation.class);
double currentQuote =
quotes.get(operation.getCompany().name());
double newQuote;
double change = (operation.getAmount() / 25);
if (operation.getType() == Operation.BUY) {
newQuote = currentQuote + change;
}
else {
newQuote = currentQuote - change;
}
if (newQuote < 0) newQuote = 0;
quotes.replace(operation.getCompany().name(), newQuote);
Quote quote = new Quote(operation.getCompany().name(),
newQuote);
return jsonb.toJson(quote);
}
}
这个类的 init 方法只是用一些随机值启动每个 Company 的初始报价。
newQuote 方法是我们交易系统的核心。通过读取 JSON 文件中包含的操作数据,使用基本算法生成一个新的报价:对于任何交易的 25 只股票,将对股票的价值产生一个点的影响。返回的 JSON 字符串封装了 Quote 类,通过方法顶部的 @Broadcast 注解,将消息广播到 "in-memory-stream" 通道的所有匹配订阅者。
为了完整性,我们还将包括 Quote Java 类,它将被作为 JSON 发送到客户端:
public class Quote {
String company;
Double value;
public Quote(String company, Double value) {
this.company = company;
this.value = value;
}
// Getters Setters method omitted for brevity
}
在我们的示例中,我们有一个 "in-memory-stream" 通道的以下订阅者,其中 Quote 被发布:
@Path("/quotes")
public class QuoteEndpoint {
@Inject
@Channel("in-memory-stream")
Publisher<String> quote;
@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
@SseElementType("text/plain")
public Publisher<String> stream() {
return quote;
}
}
QuoteEndpoint 是我们的 REST 端点。在这个端点中,我们使用 @Channel 限定符将 "in-memory-stream" 通道注入到 bean 中。这正是反应式世界(由流控制)与命令式世界(CDI bean,按顺序执行代码)统一的地方。简单来说,这就是我们的 bean 能够检索由反应式消息管理的通道的地方。
所有的先前组件都需要一个经纪人,这是发布股票报价并读取它们的地方。以下是 application.properties 文件,它将这些组件组合在一起:
#Kafka destination
mp.messaging.outgoing.stock-quote.connector=smallrye-kafka
mp.messaging.outgoing.stock-quote.topic=stocks
mp.messaging.outgoing.stock-quote.value.serializer=org.apache.kafka.common.serialization.StringSerializer
#Kafka source
mp.messaging.incoming.stocks.connector=smallrye-kafka
mp.messaging.incoming.stocks.topic=stocks
mp.messaging.incoming.stocks.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
第一个块与 Kafka 目标相关,在流式通信中通常称为sink,这是我们写入由QuoteGenerator产生的股票报价的地方。为了在类的节点之间复制数据,有必要序列化其内容。字节流是操作系统用于 I/O 的标准语言。在我们的情况下,由于数据是 JSON 格式,我们使用StringSerializer。
在第二个块中,我们配置了源主题和连接器,其中我们以 JSON 序列化流的形式读取股票报价。
现在,我们只需要添加一个能够捕获 SSE 并将其文本以格式化的表格形式显示的客户端应用程序。为了简洁起见,我们只添加收集 SSE 的核心 JavaScript 函数:
<script>
var source = new EventSource("/quotes/stream");
source.onmessage = function (event) {
var data = JSON.parse(event.data);
var company = data['company'];
var value = data['value'];
document.getElementById(company).innerHTML = value;
};
</script>
上述代码将被包含在index.html页面中,该页面位于本章的源代码中。让我们看看它的实际效果!在构建应用程序之前,使用以下命令启动 Kafka/ZooKeeper 容器:
docker-compose up
Docker Compose 工具将搜索根目录中的docker-compose.yaml文件。在这里,我们已配置 Kafka 和 ZooKeeper 容器以便它们启动。成功的启动将在控制台底部产生以下输出:
kafka_1 | [2019-10-20 07:05:36,276] INFO Kafka version : 2.1.0 (org.apache.kafka.common.utils.AppInfoParser)
kafka_1 | [2019-10-20 07:05:36,277] INFO Kafka commitId : 809be928f1ae004e (org.apache.kafka.common.utils.AppInfoParser)
kafka_1 | [2019-10-20 07:05:36,279] INFO [KafkaServer id=0] started (kafka.server.KafkaServer)
你可以通过执行docker ps命令来验证 Kafka 和 ZooKeeper 容器是否正在运行:
docker ps --format '{{.Names}}'
上述命令将显示以下活动进程:
kafka_kafka_1
kafka_zookeeper_1
现在,使用以下命令以常规方式启动应用程序:
mvn install quarkus:dev
应用程序的欢迎页面(可在http://localhost:8080访问)将显示正在运行的股票报价行情,如下截图所示:

列表中的每家公司都将开始于一个 N/A 报价,直到对其执行随机操作。最后,你会看到前面的页面每两秒更新一次,这是我们配置在QuoteGenerator类中的。很酷,不是吗?
当你完成这个示例后,使用以下命令停止所有正在运行的容器:
docker stop $(docker ps -a -q)
一旦docker-compose进程终止,上述命令将显示所有已停止的容器层列表:
6a538738088f
f2d97de3520a
然后,通过再次执行docker ps命令来验证 Kafka 和 ZooKeeper 容器是否已停止:
docker ps --format '{{.Names}}'
上述命令不应产生任何输出,这意味着没有挂起的 Docker 进程正在运行。
我们刚刚开始使用 Docker Compose 工具。现在,让我们继续并在 OpenShift 上部署完整的应用程序堆栈。
在云中向 Kafka 发送流消息
为了完成我们接下来的挑战,我们强烈建议使用最新的 OpenShift 4.X 版本。事实上,为了编排多个服务,如 Kafka 和 ZooKeeper,使用基于Operator概念的 OpenShift 4 版本要简单得多。Kubernetes Operator 是在集群上的 Pod 中运行的软件片段,它通过自定义资源定义(CRDs)引入新的对象类型。CRD 不过是 Kubernetes 中的一个扩展机制,它允许您为用户定义接口;例如,您可以定义一个用于 Kafka 服务器的 CRD,这为我们提供了一个更简单的方法来配置和在我们的集群中运行它。
此外,Operator 已经有一个公共目录(operatorhub.io/),您可以在其中找到现有的 Operator 或添加您自己的。
您可以通过访问www.openshift.com/trial/来评估 OpenShift 4。在那里,您可以找到评估 OpenShift 的几种选择,无论是在云中还是在您的机器上。在本章中,我们假设您已经完成了注册流程,并且 OpenShift 4 已经启动并运行。
对于下一个项目,请参考Chapter10/kafka-openshift目录,在那里您将找到为 OpenShift 配置的股票交易应用程序以及设置和配置 Kafka 集群的 YAML 文件。
在 OpenShift 上安装 Kafka
在 OpenShift 集群上安装和管理工作 Apache Kafka 集群的最简单方法是使用Strimzi项目(strimzi.io/),该项目可以作为 OpenShift Operator 安装。
首先创建一个名为kafka-demo的新 OpenShift 项目。您可以从管理员控制台创建它,或者使用oc命令行工具,如下所示:
oc new-project kafka-demo
返回的输出将确认项目命名空间已创建在您的虚拟地址中:
Now using project "kafka-demo" on server "https://api.fmarchioni-openshift.rh.com:6443".
服务器名称将根据您在登录时选择的账户名称而有所不同。
我们建议从 OpenShift web-console 继续操作。从左侧管理员面板中选择 OperatorHub,如下截图所示:

OperatorHub 目录将在主 OpenShift 仪表板中显示。选择Strimzi Operator,如下截图所示:

然后,在以下 UI 中,选择安装 Operator:

接下来,您将能够选择是否要在集群的所有可用命名空间中安装 Operator,或者只在特定的项目中安装。由于我们不会在其他项目中使用此 Operator,只需选中“集群上的特定命名空间”选项并选择您的项目。您的选择应如下所示:

几秒钟后,在主面板中,您将收到通知,Operator 已安装,以及所有提供的 API:

现在您已经有了 Strimzi Operator,安装 Kafka 集群将变得轻而易举!在Chapter10/kafka-openshift/strimzi文件夹中,您将找到以下文件:
-
kafka-cluster-descriptor.yaml:此文件包含基于 Strimzi Operator 的 Kafka 集群定义。 -
kafka-topic-queue-descriptor.yaml:此文件定义了一个资源(一个 Kafka 主题),我们需要在我们的 Kafka 集群中进行配置。
您可以使用oc命令安装它们两个。让我们从集群开始:
oc create -f strimzi/kafka-cluster-descriptor.yaml
前一个命令的输出如下:
kafka.kafka.strimzi.io/my-kafka created
现在,等待几秒钟,直到 Kafka 集群启动并运行。您可以使用以下命令检查当前项目中 Pod 的状态:
oc get pods
然后,等待直到所有 Pod 都在运行,如下所示:
NAME READY STATUS RESTARTS AGE
my-kafka-entity-operator-58d546cf6c-dw85n 3/3 Running 0 5m50s
my-kafka-kafka-0 2/2 Running 1 6m27s
my-kafka-kafka-1 2/2 Running 1 6m27s
my-kafka-kafka-2 2/2 Running 0 6m27s
my-kafka-zookeeper-0 2/2 Running 0 7m5s
my-kafka-zookeeper-1 2/2 Running 0 7m5s
my-kafka-zookeeper-2 2/2 Running 0 7m5s
strimzi-cluster-operator-v0.14.0-59744f8569-d7j44 1/1 Running 0 7m47s
一个成功的集群设置将包括以下组件:
-
三个 Kafka 集群节点处于运行状态
-
三个 ZooKeeper 集群节点也处于运行状态
集群的名称(my-kafka)已经在kafka-cluster-descriptor.yaml文件中指定,如下所示:
apiVersion: kafka.strimzi.io/v1beta1
kind: Kafka
metadata:
name: my-kafka
现在,让我们通过添加一个名为stock的队列来继续,该队列在kafka-topic-queue-descriptor.yaml文件夹中定义。您可以使用以下命令创建它:
oc create -f strimzi/kafka-topic-queue-descriptor.yaml
您将看到以下输出:
kafkatopic.kafka.strimzi.io/stocks created
如果您想对 Kafka 集群有一些了解,您可以检查主题是否可用。为此,使用oc rsh登录到任何可用的 Kafka 节点:
oc rsh my-kafka-kafka-0
通过这样做,您将能够访问该容器的终端。从那里,执行以下命令:
sh-4.2$ ./bin/kafka-topics.sh --list --zookeeper localhost:2181
控制台中最小的输出是stocks,这是我们的主题名称:
stocks
要连接到 Kafka 集群,我们不会使用 IP 地址或 Pod 名称(这些名称在重启后会发生变化)。相反,我们将使用服务名称,这将让您通过别名访问集群。您可以使用以下命令检查可用的服务名称:
oc get services -o=name
前一个命令的输出将仅限于name列。在我们的情况下,它将如下所示:
service/my-kafka-kafka-bootstrap
service/my-kafka-kafka-brokers
service/my-kafka-zookeeper-client
service/my-kafka-zookeeper-nodes
我们感兴趣的服务名称是my-kafka-kafka-bootstrap,我们很快将其添加到我们的 Quarkus 项目中。
为原生云执行准备我们的项目
要在 OpenShift 上运行我们的项目,我们将对配置文件进行一些最小更改,以便我们可以访问我们刚刚确定的 Kafka 服务名称。在以下代码中,我们已突出显示必须应用到application.properties文件中的更改:
mp.messaging.outgoing.stock-quote.connector=smallrye-kafka
mp.messaging.outgoing.stock-quote.topic=stocks
mp.messaging.outgoing.stock-quote.value.serializer=org.apache.kafka.common.serialization.StringSerializer
mp.messaging.outgoing.stock-quote.bootstrap.servers=my-kafka-kafka-bootstrap:9092
mp.messaging.incoming.stocks.connector=smallrye-kafka
mp.messaging.incoming.stocks.topic=stocks
mp.messaging.incoming.stocks.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
mp.messaging.incoming.stocks.bootstrap.servers=my-kafka-kafka-bootstrap:9092
如您所见,在先前的配置中,我们使用了bootstrap.servers属性来指定 Kafka 服务器列表(host:port)。
在配置中,您可以通过逗号分隔每个条目来添加多个服务器。
在本例的源代码中,你也会发现所有在消息流中序列化的 POJO 类都标注了 @io.quarkus.runtime.annotations.RegisterForReflection,如下所示:
@RegisterForReflection
public class Quote { . . . }
实际上,在构建原生可执行文件时,GraalVM 会做一些假设以删除所有未直接在代码中使用的类、方法和字段。通过反射使用到的元素不是调用树的一部分,因此它们是消除原生可执行文件时的候选元素。由于 JSON 库严重依赖反射来完成其工作,我们必须使用 @RegisterForReflection 注解明确告诉 GraalVM 不要排除它们。
这是我们为了将其发布到云端所做的微小更改。现在,使用以下命令构建和部署原生应用程序:
#Build the native application
mvn clean package -Pnative -Dnative-image.docker-build=true
#Create a new build for it
oc new-build --binary --name=quarkus-kafka -l app=quarkus-kafka
#Patch the Docker.native file
oc patch bc/quarkus-kafka -p "{\"spec\":{\"strategy\":{\"dockerStrategy\":{\"dockerfilePath\":\"src/main/docker/Dockerfile.native\"}}}}"
#Deploy the application in the build
oc start-build quarkus-kafka --from-dir=. --follow
# To instantiate the image as new app
oc new-app --image-stream=quarkus-kafka:latest
# To create the route
oc expose service quarkus-kafka
请注意,你可以找到前面的脚本,即 deploy-openshift.sh,在 Chapter10/kafka-openshift 文件夹中。
一旦执行了前面的脚本,请使用以下命令验证 quarkus-kafka Pod 是否已启动并运行:
oc get pods
输出将确认这一点:
NAME READY STATUS RESTARTS AGE
kafka-demo-1-deploy 0/1 Completed 0 30s
kafka-demo-1-p9qdr 1/1 Running 0 36s
你可以按照以下方式检查路由地址:
oc get routes
路由将在 HOST/PORT 列输出:
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
quarkus-kafka quarkus-kafka-kafka-demo.apps.fmarchio-qe.qe.rh-ocs.com quarkus-kafka 8080-tcp None
如果你只想通过点击就能访问你的应用程序,请转到管理控制台并选择网络 | 路由。然后,点击路由位置,如下面的截图所示:

一旦配置的输出引言超时时间结束,你将在 OpenShift 上看到股票交易应用程序的实际运行情况:

我们已经到达了 Apache Kafka 流的辉煌旅程的终点!在下一节中,我们将学习如何接近另一个候选解决方案,即基于 AMQP 协议的流消息。
使用 AMQP 进行消息流
如果你只是在多年的 Java 企业社区之后刚刚发现 Quarkus,你将已经熟悉消息代理,这些代理用于允许不同的 Java 应用程序使用 JMS 作为标准协议进行通信。尽管 JMS 是实现消息系统的健壮和成熟的解决方案,但它的主要局限性之一是它仅专注于 Java。在微服务世界中,使用不同的语言来构建整体系统架构相当常见,因此需要一个平台无关的解决方案。在这种情况下,AMQP 提供了一系列优势,使其成为在分布式系统中的微服务实现反应式流 API 的完美选择。
简而言之,以下是一些 AMQP 协议的主要功能:
-
它提供了一个平台无关的底层消息协议,允许跨多种语言和平台进行互操作性。
-
它是一个底层协议,数据作为字节流通过网络发送。
-
它在低级别字节流工作的情况下可以实现高性能。
-
它支持长连接消息和经典消息队列。
-
它支持如轮询(负载在服务器之间均匀分配)和存储转发(消息在发送方侧存储在持久存储中,然后转发到接收方侧)等分发模式。
-
它支持事务(跨消息目的地),以及使用通用标准(XA、X/Open、MS DTC)的分布式事务。
-
它支持使用 SASL 和 TLS 协议进行数据加密。
-
它允许我们通过元数据控制消息流。
-
它提供消息流控制以控制背压。
为了让我们的应用程序与 AMQP 交互,我们需要一个支持此协议的代理。Java 企业中常用的解决方案是Apache Artemis ActiveMQ (activemq.apache.org/components/artemis/),它也与 Java 企业面向消息的中间件(MOM)兼容。在下一节中,我们将学习如何在我们的股票报价应用程序中启动和配置它。
配置 AMQP 代理
为了尽可能快地启动我们的应用程序,我们将使用 Docker Compose 脚本。这将下载一个合适的消息代理版本,并设置一些必要的环境变量,以便我们可以访问代理。
只需使用以下命令启动amqp文件夹中的docker-compose.yaml文件:
docker-compose up
如果启动成功,你应该会看到以下输出:
artemis_1 | 2019-10-26 17:20:47,584 INFO [org.apache.activemq.artemis] AMQ241001: HTTP Server started at http://0.0.0.0:8161
artemis_1 | 2019-10-26 17:20:47,584 INFO [org.apache.activemq.artemis] AMQ241002: Artemis Jolokia REST API available at http://0.0.0.0:8161/console/jolokia
artemis_1 | 2019-10-26 17:20:47,584 INFO [org.apache.activemq.artemis] AMQ241004: Artemis Console available at http://0.0.0.0:8161/console
你可以通过执行docker ps命令来验证 Kafka 和 ZooKeeper 容器是否正在运行:
docker ps --format '{{.Names}}'
上述命令将显示以下活动进程:
amqp_artemis_1
现在,让我们配置我们的应用程序,使其可以使用 ActiveMQ。你可以在本书 GitHub 仓库的Chapter10/amqp文件夹中找到更新后的应用程序。首先,我们需要将 Kafka 的响应式消息传递依赖项替换为 AMQP 响应式消息传递依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-reactive-messaging-amqp</artifactId>
</dependency>
在应用配置方面,我们需要对application.properties文件进行一些修改。首先,我们需要包含在docker-compose.yaml中设置的用户名和密码(quarkus/quarkus):
amqp-username=quarkus
amqp-password=quarkus
然后,我们需要配置 AMQP 连接器,以便我们可以写入stock-quote队列,通过指定队列是持久的(例如,持久化到磁盘并在代理重启后存活):
mp.messaging.outgoing.stock-quote.connector=smallrye-amqp
mp.messaging.outgoing.stock-quote.address=stocks
mp.messaging.outgoing.stock-quote.durable=true
相反,我们需要配置 AMQP 连接器,以便它可以读取stocks队列:
mp.messaging.incoming.stocks.connector=smallrye-amqp
mp.messaging.incoming.stocks.durable=true
现在,我们可以使用以下命令像往常一样引导应用程序:
mvn install quarkus:dev
应用程序的欢迎页面(可在http://localhost:8080访问)将显示股票报价的实时行情,现在它使用 ActiveMQ 作为其代理。正如你所见,UI(仅标题)进行了最小调整,但它准确地掩盖了底下的变化:

您可以通过登录到 AMQ 管理控制台了解更多关于此过程的信息,该控制台位于 http://localhost:8161/console。一旦您使用配置的凭据(quarkus/quarkus)登录,您可以在可用地址列表中检查是否已创建目标队列:

通过选择 stocks 目标,您可以在管理控制台的主面板中检查任何进一步的详细信息,如下面的截图所示:

完成后,使用以下命令停止所有正在运行的容器:
docker stop $(docker ps -a -q)
上述命令将显示已停止的所有容器层的列表,如下所示:
6a538738088f
f2d97de3520a
然后,再次执行 docker ps 命令以验证 ActiveMQ 容器已被停止:
docker ps --format '{{.Names}}'
上述命令不应产生任何输出。现在,让我们在云端测试相同的应用程序堆栈。
在云端向 AMQ 流式传输消息
我们将要做的最后一件事是在使用 AMQ 作为消息代理的同时将 Quarkus 应用程序部署到云端。为此,我们将之前测试过的 ActiveMQ Docker 镜像插入到 OpenShift 中(有关此镜像的更多详细信息可在 GitHub 上找到:github.com/vromero/activemq-artemis-docker)。
首先,创建一个名为 amq-demo 的新项目:
oc new-project amq-demo
输出将确认项目命名空间已创建在您的虚拟地址中:
Now using project "amq-demo" on server "https://api.fmarchioni-openshift.rh.com:6443"
接下来,使用以下命令将 AMQ 服务器部署到您的项目中,这将设置用户名和密码,以便您可以访问代理:
oc new-app --name=artemis vromero/activemq-artemis:2.9.0-alpine -e ARTEMIS_USERNAME=quarkus -e ARTEMIS_PASSWORD=quarkus -e RESTORE_CONFIGURATION=true
注意 RESTORE_CONFIGURATION=true 环境变量。这是必需的,因为 OpenShift 自动挂载所有声明的空卷。由于此行为影响此镜像的 /etc 文件夹,而配置存储在该文件夹中,因此我们需要将 RESTORE_CONFIGURATION 环境变量设置为 true。
执行 new-app 命令后,将显示以下输出:
--> Found container image 2fe0af6 (10 days old) from Docker Hub for "vromero/activemq-artemis:2.9.0-alpine"
* An image stream tag will be created as "artemis:2.9.0-alpine" that will track this image
* This image will be deployed in deployment config "artemis"
* Ports 1883/tcp, 5445/tcp, 5672/tcp, 61613/tcp, 61616/tcp, 8161/tcp, 9404/tcp will be load balanced by service "artemis"
* Other containers can access this service through the hostname "artemis"
* This image declares volumes and will default to use non-persistent, host-local storage.
You can add persistent volumes later by running 'oc set volume dc/artemis --add ...'
--> Creating resources ...
imagestream.image.openshift.io "artemis" created
deploymentconfig.apps.openshift.io "artemis" created
service "artemis" created
--> Succes
您可以使用 oc 命令检查 Pods 的状态:
oc get pods
以下输出确认 artemis Pod 正在运行状态:
NAME READY STATUS RESTARTS AGE
artemis-1-deploy 0/1 Completed 0 80s
artemis-1-p9qdr 1/1 Running 0 76s
最后,让我们检查服务名称,它将是 artemis:
oc get services -o name
确认返回的输出与这里显示的输出匹配:
service/artemis
现在,让我们进行最后的终结操作:我们将部署位于 Chapter10/amqp-openshift 目录中的应用程序。在这个文件夹中,您将找到配置为在 AMQ 上流式传输消息的股票交易应用程序。
这里是更新后的 application.properties 文件,其中包含 AMQ 用户名和密码,以及服务运行的主机和端口:
amqp-username=quarkus
amqp-password=quarkus
# Configure the AMQP connector to write to the `stocks` address
mp.messaging.outgoing.stock-quote.connector=smallrye-amqp
mp.messaging.outgoing.stock-quote.address=stocks
mp.messaging.outgoing.stock-quote.durable=true
mp.messaging.outgoing.stock-quote.host=artemis
mp.messaging.outgoing.stock-quote.port=5672
# Configure the AMQP connector to read from the `stocks` queue
mp.messaging.incoming.stocks.connector=smallrye-amqp
mp.messaging.incoming.stocks.durable=true
mp.messaging.incoming.stocks.host=artemis
mp.messaging.incoming.stocks.port=5672
接下来,我们将部署与同一文件夹(Chapter10/amqp-openshift)中的应用程序到 OpenShift。为了方便,你可以简单地运行deploy-openshift.sh脚本,该脚本位于同一目录下。以下是脚本的内容,这应该对你来说相当熟悉:
#Build native image of the project
mvn clean package -Pnative -Dnative-image.docker-build=true
# Create a new binary build
oc new-build --binary --name=quarkus-amq -l app=quarkus-amq
# Patch the native file
oc patch bc/quarkus-amq -p "{\"spec\":{\"strategy\":{\"dockerStrategy\":{\"dockerfilePath\":\"src/main/docker/Dockerfile.native\"}}}}"
# Add project to the build
oc start-build quarkus-amq --from-dir=. --follow
# To instantiate the image
oc new-app --image-stream=quarkus-amq:latest
# To create the route
oc expose service quarkus-amq
然后,检查quarkus-amq Pod 是否处于运行状态:
oc get pods
你将收到的输出确认了这一点:
NAME READY STATUS RESTARTS AGE
artemis-1-deploy 0/1 Completed 0 9m9s
artemis-1-p9qdr 1/1 Running 0 9m5s
quarkus-amq-1-deploy 0/1 Completed 0 14s
quarkus-amq-1-zbvrl 1/1 Running 0 10s
现在,你可以通过点击路由地址来验证应用程序是否工作。只需在控制台中的“网络 | 路由”路径下进行操作:

输出将几乎相同,只是路由名称会庆祝你在本书中的最后一次成就:

好了,朋友们!
摘要
在本章中,我们学习了如何使用 CDI Bean 根据反应式消息规范来产生、消费和处理消息。我们还学习了如何启动和配置 Apache Kafka 和 Active MQ 的代理,使其作为我们的 CDI Bean 的分布式流平台。为了将我们的新技能付诸实践,我们创建了一个示例股票交易应用程序,该应用程序最初以开发模式运行,然后作为原生镜像部署到 OpenShift 上。
现在,我们已经到达了本书的结尾,在这里我们了解了 Java 企业应用程序从单体到在云中运行的本地微服务的逐步更新故事。这是一次激动人心的旅程,确实设立了一个里程碑,但这并不是我们辛勤工作的结束——这只是这个故事的一个结束。


浙公网安备 33010602011771号