Quarkus-秘籍-全-

Quarkus 秘籍(全)

原文:zh.annas-archive.org/md5/9958122197e3eeeafd7a91f4277fedf5

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

我们很高兴你能加入我们一起学习和使用 Quarkus 的旅程!与传统的 Java 框架不同,它可能庞大、笨重、复杂,需要数月才能掌握,Quarkus 则建立在你已有的知识基础之上!它使用了 JPA、JAX-RS、Eclipse Vert.x、Eclipse MicroProfile 和 CDI 等多种你已经熟悉的技术。然后,Quarkus 将你的知识整合成一个紧凑、易于部署、完全优化于 Kubernetes 的容器,可选择使用 OpenJDK Hotspot 或 GraalVM。这使得你能够尽可能紧密地打包你的 Kubernetes 集群,充分利用每台机器上的资源,以应对需求的扩展。无论你在迁移到 Kubernetes 的哪个阶段,你都会在 Quarkus 中找到有用的东西,而本书将为你提供成功所需的工具和资源。

谁应该阅读本书

显然,我们希望每个人都能阅读这本书!但是,我们对读者做了一些假设:

  • 你已经熟悉 Java 和在该领域内的应用程序开发。

  • 你理解传统软件开发实践。

  • 你经常将服务部署到一组机器的集群或云中。

为什么要写这本书

Quarkus 是一个相对较新的框架,处于一个新的领域(本地 Java 和 GraalVM)。我们希望深入一些示例和操作指南,超出互联网上的内容。此外,我们希望本书尽可能地充实内容。这本书中没有需要理解或记住的大型应用程序。本书中的所有示例都是独立的,可以随时使用。我们希望你将其作为你所有 Quarkus 开发的参考!

导航本书

章节的组织比较松散,但基本上如下:

  • 第一章和第二章将带你了解 Quarkus,并设置你的基本项目。

  • 第三章至第六章介绍了 Quarkus 的主要部分:使用 CDI 和 Eclipse MicroProfile 构建的 RESTful 应用程序。这些章节还向你展示了如何打包你的应用程序。

  • 第七章至第十四章涉及更难,但同样重要的概念,如容错性、持久化、安全性以及与其他服务的交互。你还将了解 Quarkus 与 Kubernetes 的其他集成。

  • 第十五章和第十六章讨论了使用 Quarkus 进行响应式编程以及框架的一些额外功能,如模板化、调度和 OpenAPI。

本书中使用的约定

本书中使用以下排印约定:

Italic

表示新术语、网址、电子邮件地址、文件名和文件扩展名。

Constant width

用于程序清单,以及在段落内引用程序元素,例如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

Constant width bold

显示用户应该按字面意义键入的命令或其他文本。

Constant width italic

显示应由用户提供的值或由上下文确定的值的文本。

Tip

此元素表示提示或建议。

Note

此元素表示一般注释。

Warning

此元素指示警告或注意事项。

Important

此元素表示需要记住的重要事项。

使用代码示例

可以下载补充材料(代码示例、练习等)https://oreil.ly/quarkus-cookbook-code

如果您有技术问题或在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com

本书旨在帮助您完成工作。一般情况下,如果本书提供示例代码,您可以在自己的程序和文档中使用它。除非您复制了大量代码,否则无需征得我们的许可。例如,编写使用本书中几个代码块的程序不需要许可。出售或分发来自 O’Reilly 书籍的示例则需要许可。引用本书并引用示例代码来回答问题不需要许可。将本书的大量示例代码合并到产品文档中则需要许可。

我们感谢您的使用,但通常不需要署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Quarkus Cookbook by Alex Soto Bueno and Jason Porter (O’Reilly). Copyright 2020 Alex Soto Bueno and Jason Porter, 978-1-492-06265-3.”

如果您认为您使用的代码示例超出了合理使用范围或以上所给予的许可,请随时通过permissions@oreilly.com联系我们。

致谢

Jason Porter:在隔离期间你会做什么?当然是写一本书!感谢所有在医疗前线勇敢的人们。我要感谢 Quarkus 和 GraalVM 团队,为我们提供了一个令人惊叹的工具和有趣的开发体验。我从事软件开发已有 20 多年,而 Quarkus 让我重新感受到了当初学习软件开发时的乐趣。特别感谢 Georgios Andrianakis 和 Daniel Hinojosa 为本书提供的技术审查!你们的工作帮助我们创造了一些易于理解、有用且希望能给学习 Quarkus 的人带来乐趣的东西。我还要感谢 Red Hat 让我有机会撰写这本书。Alex,再次感谢你邀请我与你共同撰写书籍!最后,感谢我的五个孩子(Kaili、Emily、Zackary、Nicolas 和 Rebecca)和妻子 Tessie,尽管我说过不再写书,但你们仍然支持我。爱你们!

Alex Soto Bueno:这本书是在 COVID-19 大流行期间完成的,因此首先,我要感谢所有为我们所有人提供医疗服务的医护人员。我还要感谢 Red Hat Developers 团队,特别是 Burr Sutter,让我有机会撰写这本书。Jason,一如既往地与你共同撰写书籍是一种乐趣。最后,感谢我的父母;我的妻子 Jessica;以及我的女儿 Ada 和 Alexandra,在我写书的时候对我的耐心,因为没有两全其美的事情。非常感谢你们一切。

第一章:Quarkus 概述

Kubernetes 现在已经成为部署企业应用的事实标准平台。转向容器和 Kubernetes 导致了我们编写、部署和维护基于 Java 的应用程序的方式发生了变化。如果你在不采取适当措施的情况下容器化和运行 Java 应用程序,很容易遇到麻烦。Pods 中的容器(一个 Kubernetes 术语)是 Kubernetes 中的基本单位,因此正确理解如何正确地容器化基于 Java 的应用程序非常重要,以避免陷阱、浪费工作和额外的挫败时间。

Quarkus 是一个内置 Kubernetes 集成的云原生框架。它是一个开源堆栈,根据 Apache License 2.0 发布,帮助你创建专为 GraalVM 和 OpenJDK HotSpot 定制的 Kubernetes 本地应用程序。它建立在流行的库和技术之上,如 Hibernate、Eclipse MicroProfile、Kubernetes、Apache Camel 和 Eclipse Vert.x。

Quarkus 的好处包括与 Docker 和 Kubernetes 的轻松集成、快速启动时间、低 Resident Set Size(RSS)内存和提高开发者的生产力。在这个介绍性的章节中,我们将快速浏览 Quarkus——它是什么、它解决了哪些问题、它如何与 Kubernetes 集成、为什么开发者喜欢使用它以及它的一些最显著的特点。

开发者友好

Quarkus 允许你作为 Java 开发者更加高效,帮助你在微服务和基于云的应用程序快速发展的领域保持相关性。

Quarkus 能够使你的应用程序更好地扩展,更紧密地填充 Kubernetes 集群,利用更少的总资源,并利用开源 Java 社区几十年来的工作。

要开始使用 Quarkus 进行开发,你不需要学习新技术。如果你已经熟悉依赖注入、JAX-RS、Hibernate 和 Eclipse MicroProfile 的概念,这里没有什么新东西。你在职业生涯中建立的所有知识将直接映射到 Quarkus 中。相比于学习其他框架可能需要几周的时间,你可以立即开始使用 Quarkus,并在几天甚至几小时内提高工作效率。

Quarkus 设计成为下一代应用开发和部署的优化选择。它支持你从应用程序的脚手架搭建和开发模式下的实时重载(一种保存和刷新的工作流程),一直到在基于云的 Kubernetes 集群中的部署。作为开发者,Quarkus 将让你保持高效解决问题,而不是在无关紧要的事情上浪费时间。

与 Kubernetes 集成

我们说 Quarkus 是为了在 Kubernetes 内运行而设计的。听起来很棒,但我们知道许多东西都可以在 Kubernetes 中运行。将你的应用程序放入 Docker 容器中,它就可以在 Kubernetes 上运行。虽然这是真的,但传统上还有许多事情必须做才能正确地调整、大小和配置你的应用程序以在 Kubernetes 中有效运行。你还必须打开你喜欢的文本编辑器,并编写多个 YAML 文件——说实话,没有人真的喜欢做所有这些。

Quarkus 通过具有许多增强功能来部署和使用 Kubernetes 与你的应用程序来消除这项工作。当你启动一个 Quarkus 应用程序时,它会带有一些用于生成你的应用程序 Docker 容器的 Dockerfile 文件。这是一个很好的第一步。这些文件经过优化,可与 OpenJDK JVM 一起运行或使用 GraalVM 作为本地可执行文件运行。它们包含运行应用程序所必需的内容,从而尽可能地消除容器镜像中的重复和不必要的膨胀。

接下来,当你使用 Kubernetes 扩展时,Quarkus 可以为普通 Kubernetes 或 OpenShift 部署生成资源(YAML 文件)!不再需要深入研究 YAML 文件并确保你有正确的缩进。毕竟,你更愿意编写代码,而不是寻找那一行格式不正确的 YAML。Quarkus 还可以将你的镜像推送到注册表,然后部署到 Kubernetes 集群。所有这些应用程序镜像都可以通过 Quarkus 应用程序配置进一步增强和定制,你将在第四章中了解到这些内容。例如,在 Quarkus 1.4 及更高版本中,ConfigMapSecret 可以从 API 服务器中读取——你不需要在 Pod 中挂载任何文件!

内存和首次响应时间

Quarkus 被称为“超音速,亚原子”Java 框架。这可能会引起开发者的营销警报,但当你分解并理解 Quarkus 在做什么时,你会发现你确实获得了一个非常小、快速且高效的执行。使用 Quarkus,你可以部署一个针对 Kubernetes 进行优化的本地应用程序。例如,假设你想部署一个本地应用程序,该应用程序经过优化以在 Kubernetes 上运行,容器镜像大小约为 200 MB 或更小。在 Quarkus 中,该应用程序将在一小部分秒内启动并准备好接受请求,并且将使用少于 50 MB 的内存。

在部署到 Kubernetes 集群时,您希望尽可能多地打包您的应用实例,以便在遇到意外负载时能够扩展,并尽可能利用多个资源。在扩展时,您希望新的应用实例能够快速启动并运行,这就是原生可执行文件的优势所在。Quarkus 在原生可执行文件构建过程中尽可能多地预先加载您的应用程序和使用的框架。这有助于您的应用程序快速启动,并准备好处理请求,而无需进行额外的类加载、运行时扫描或其他 JVM 通常会执行的预热操作。

自然地,可用内存是有限资源。了解您的应用程序使用了多少内存,并在尝试保持该数字较低时不使 JVM 饥饿,这对于部署密度至关重要。Quarkus 通过小型且内存高效的原生可执行文件帮助您实现这一目标。

一个基本的 Quarkus 工作流程

在阅读本书并浏览示例时,您将被介绍到 Quarkus 生态系统。您将了解到扩展、集成和设计决策。您还将看到在整个过程中使用的基本工作流程,以帮助您提高工作效率。简而言之,这个工作流程如下所示:

  1. 创建脚手架

  2. 启动开发模式

  3. 代码

  4. 测试

  5. 打包

  6. 部署

构建应用程序的脚手架,或者对现有的启动进行扩展,为你提供了一个坚实的基础来构建。你将在第二章中熟悉这一过程。在完成脚手架后,你将被要求在开发模式下运行你的应用程序,这也在第二章中介绍。接下来,你将学习应用程序的典型任务:创建 RESTful 服务,完成基本的编程模型,并进行应用程序配置。开发模式将为你提供几乎即时的反馈,避免了你熟悉的编译、打包和部署的繁琐过程。在第五章中,你将了解如何测试旨在同时针对 JVM 和本机可执行文件的 Quarkus 应用程序,从而确保你的应用程序能够正确运行并符合你的标准。创建最终的交付成果在第六章中介绍,以及学习如何为特定的部署策略打包你的应用程序。工作流程的最后一部分——部署,在第十章中讨论。进一步探索时,你将学习如何使应用程序更加容错,如何与各种持久性引擎进行交互,以及如何与外部服务通信。我们还将解释额外的集成,帮助你利用来自其他库和编程范式的现有知识。我们将指导你设置必要的 Kubernetes 优化,以适应你的应用程序,并构建 Kubernetes 资源,并将其推向生产环境。

第二章:搭建

在本章中,您将了解如何创建 Quarkus 项目结构。Quarkus 提供了几种不同的方式来搭建项目。

您将学会如何做以下事情:

  • 以不同的方式搭建项目,从 Maven 到 VSCode IDE

  • 通过实时重载提升开发者体验

  • 使用 Quarkus 提供静态资源

2.1 使用 Maven 搭建 Quarkus 项目

问题

您可以通过生成一个简单的项目来快速开始 Quarkus。

解决方案

使用 Quarkus Maven 插件。

讨论

使用 Quarkus Maven 插件创建一个简单的项目,准备部署,并包含以下内容:

  • 包含最小 Quarkus 依赖项的pom.xml文件

  • 一个简单的 JAX-RS 资源

  • 一个用于 JAX-RS 资源的测试

  • 本地测试

  • 使用 Dockerfiles 生成一个容器

  • 空配置文件

我们假设您已经安装了Apache Maven。打开终端并执行以下命令:

mvn io.quarkus:quarkus-maven-plugin:1.4.1.Final:create \
 -DprojectGroupId=org.acme \
 -DprojectArtifactId=getting-started \
 -DclassName="org.acme.quickstart.GreetingResource" \
 -Dpath="/hello"

该项目具有以下结构:

├── mvnw ├── mvnw.cmd ├── pom.xml └── src
 ├── main │   ├── docker ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
 │   │   ├── Dockerfile.jvm │   │   └── Dockerfile.native │   ├── java │   │   └── org │   │       └── acme │   │           └── quickstart │   │               └── GreetingResource.java ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
 │   └── resources │       ├── META-INF │       │   └── resources │       │       └── index.html ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
 │       └── application.properties ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
 └── test └── java └── org └── acme └── quickstart ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
 ├── GreetingResourceTest.java └── NativeGreetingResourceIT.java

1

Dockerfiles

2

JAX-RS 资源

3

静态资源

4

配置文件

5

为 JAX-RS 资源自动生成的测试

2.2 使用 Gradle 搭建 Quarkus 项目

问题

您想要通过生成一个简单的项目来快速开始 Quarkus,您是 Gradle 用户。

解决方案

使用 Quarkus Maven 插件(是的,Maven 插件)。

讨论

您可以通过使用 Quarkus Maven 插件来搭建一个简单的 Quarkus 项目;您只需将输出设置为 Gradle 项目。生成的项目准备好部署,并包含以下内容:

  • 包含最小 Quarkus 依赖项的build.gradle文件

  • 一个简单的 JAX-RS 资源

  • 为 JAX-RS 资源编写的测试

  • 本地测试

  • 使用 Dockerfiles 生成一个容器

  • 空配置文件

我们假设您已经安装了Apache Maven。打开终端并执行以下命令:

mvn io.quarkus:quarkus-maven-plugin:1.4.1.Final:create \
 -DprojectGroupId=org.acme \
 -DprojectArtifactId=getting-started \
 -DclassName="org.acme.quickstart.GreetingResource" \
 -Dpath="/hello" \
 -DbuildTool=gradle
注意

不像在 Apache Maven 中那样,此命令将在当前目录中创建结构。

结果项目具有以下结构:

.
├── README.md
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
 ├── main
 │   ├── docker
 │   │   ├── Dockerfile.jvm
 │   │   └── Dockerfile.native
 │   ├── java
 │   │   └── org
 │   │       └── acme
 │   │           └── quickstart
 │   │               └── GreetingResource.java
 │   └── resources
 │       ├── META-INF
 │       │   └── resources
 │       │       └── index.html
 │       └── application.properties
 ├── native-test
 │   └── java
 │       └── org
 │           └── acme
 │               └── quickstart
 │                   └── NativeGreetingResourceIT.java
 └── test
 └── java
 └── org
 └── acme
 └── quickstart
 └── GreetingResourceTest.java

2.3 使用 Quarkus Start Coding 网站搭建 Quarkus 项目

问题

您可以通过生成一个简单的项目来快速开始 Quarkus,而无需安装 Maven 或 Gradle。

解决方案

使用 Quarkus Start Coding 网站通过https://code.quarkus.io生成一个简单的 Quarkus 项目。

讨论

撰写本文时,主页看起来像图 2-1 所示。

qucb 0201

图 2-1. https://code.quarkus.io 主页

页面加载后,点击“生成您的应用程序”按钮以下载包含生成项目的 ZIP 文件。

打开终端并解压生成的项目:

unzip code-with-quarkus.zip
cd code-with-quarkus/

脚手架项目与您在 Recipe 2.1 中生成的项目相同,具有以下元素:

  • pom.xml 文件具有最小的 Quarkus 依赖项

  • 一个简单的 JAX-RS 资源

  • JAX-RS 资源的测试

  • 本地测试

  • Dockerfile 用于生成容器

  • 一个空配置文件

参见

我们还没有讨论 Quarkus 扩展,但请注意,您可以使用注册的任何 Quarkus 扩展生成项目。您将在后续章节中了解更多有关扩展的信息。

可以通过在页面上的复选框中选择它们或使用搜索框来添加扩展。

2.4 使用 Visual Studio Code 搭建 Quarkus 项目

问题

您可以通过在 Visual Studio (VS) Code 中生成一个简单的 Quarkus 项目来快速入门 Quarkus。

解决方案

使用 Quarkus VS Code 扩展。

讨论

Quarkus 扩展为 Visual Studio Code IDE 开发,将一些 Quarkus 特性集成到 IDE 中。以下是其中一些特性:

  • 用命令搭建项目

  • 添加扩展的命令

  • 配置文件(属性和 YAML 格式)的自动完成代码段

要安装插件,请打开 VS Code 并点击“Extensions”按钮,如图 2-2 所示。

qucb 0202

图 2-2. 在 VS Code 中点击扩展按钮可以安装 Quarkus 扩展

然后搜索quarkus并选择由 Red Hat 提供的Quarkus Tools for Visual Studio Code。你的搜索结果应该类似于图 2-3。

qucb 0203

图 2-3. Quarkus 扩展可以从 VS Code 市场免费获取

安装完成后,请重新启动 IDE,然后即可开始使用这些扩展。

要生成一个新的 Quarkus 项目,打开命令面板并选择“生成一个 Quarkus 项目”。编写时的可用选项如图 2-4 所示。

qucb 0204

图 2-4. 从命令面板生成一个新的 Quarkus 项目

下一步是根据图 2-5 所示的提示,选择构建工具。还有关于groupIdartifactId等的一些问题。

qucb 0205

图 2-5. 选择您的构建工具

扩展可以通过控制台中的Add Extension命令进行添加,如图 2-6 所示。

qucb 0206

图 2-6. Add extension 命令

并选择您可能想要添加到项目中的任何 Quarkus 扩展。可见的可用扩展样本可见于图 2-7。

qucb 0207

图 2-7. 您可以看到可安装到应用程序的可用扩展列表

在接下来的图表中,您可以看到 Quarkus 扩展提供的一些功能。

图 2-8 显示了配置属性的自动完成,帮助您正确配置应用程序。

qucb 0208

图 2-8. 配置属性键的自动完成和类型信息

图 2-9 当您将鼠标悬停在配置属性上时,显示配置属性的描述。

qucb 0209

图 2-9. 将鼠标悬停在配置属性上,可以获得有关该属性的更多信息

图 2-10 演示如何检测应用程序中未使用的属性。

qucb 0210

图 2-10. 检测未使用的配置属性

图 2-11 显示了表示端点的 URL。如果单击它,将打开一个新的浏览器窗口,指向给定的 URL。

qucb 0211

图 2-11. VS Code 扩展为每个端点方法生成可点击的 URL 端点

参见

Quarkus 扩展适用于多个 IDE:

2.5 使用开发模式实现实时重新加载

问题

您希望在不必重新打包和重新部署完整应用程序的情况下查看项目中的更改结果。

解决方案

使用开发模式,它可以通过后台编译实现热部署,这意味着当您修改 Java 文件和/或资源文件并刷新浏览器时,这些更改将自动生效。

注意

每种脚手架方法默认使用 Java 11。您需要 Java 11 VM,或者您需要修改项目以使用不同版本的 JVM。

讨论

要在开发模式下启动应用程序,请在项目的根目录中运行quarkus:dev命令。例如,您正在使用 Recipe 2.1 中创建的项目:

./mvnw compile quarkus:dev

[INFO] Scanning for projects...
....
[INFO] --- quarkus-maven-plugin:1.4.1.Final:dev (default-cli) @
 getting-started ---
Listening for transport dt_socket at address: 5005
INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation
 completed in 946ms
INFO  [io.quarkus] (main) Quarkus 1.4.1.Final started in 1.445s.
 Listening on: http://[::]:8080
INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

当应用程序运行时,请打开新的终端窗口并运行以下命令:

curl http://localhost:8080/hello

hello

现在,在不停止 quarkus:dev 模式的情况下,在 org.acme.quickstart.GreetingResource.java 中进行下一个修改:

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    return "hola";
}

然后再次运行此命令:

curl http://localhost:8080/hello

hola

非常重要的一点是,您已经对源代码进行了更改,并且在不需要任何重新编译、重新打包或重新部署的情况下,这些更改已经自动应用到正在运行的实例中——无需任何特殊设置。

现在,不再是编写代码编译部署刷新重复,而是简化您的开发工作流程为编写代码刷新重复

开发模式检测 Java 文件、应用程序配置和静态资源的更改。

要停止开发模式,请转到quarkus:dev终端并按 Ctrl+C。

要在 Gradle 项目中运行开发模式,可以运行quarkusDev任务:

./gradlew quarkusDev
...

2.6 提供静态资源

问题

您想要提供静态资源,如 HTML、JavaScript、CSS 和图像。

解决方案

在 Quarkus 中,复制到src/main/resources/META-INF/resources的任何资源将从根路径提供服务。

在某些情况下,您可能希望为调用者提供静态资源。这些可以是静态可下载内容或者一个 HTML 页面。

默认情况下,Quarkus 带有一个index.html文件作为静态资源。

启动应用程序:

./mvnw compile quarkus:dev

打开浏览器并输入以下网址:http://localhost:8080/index.html

并且你将看到类似于图 2-12 所示的内容。

qucb 0212

图 2-12. Quarkus 为您的应用程序创建一个带有基本信息和下一步操作的占位符索引,以帮助您在脚手架后继续进行操作。
提示

实时重新加载也适用于静态资源。

第三章:开发 RESTful 服务

Quarkus 集成了 RESTEasy,一个 JAX-RS 实现,用于定义 REST API。在本章中,您将学习如何在 Quarkus 中开发 RESTful Web 服务。我们将涵盖以下主题:

  • 如何使用 JAX-RS 创建 CRUD 服务

  • 如何为请求其他域中的资源启用 CORS

  • 如何实现响应式路由

  • 如何实现过滤器以操纵请求和响应

3.1 创建一个简单的 REST API 端点

问题

您想要创建一个带有 CRUD 操作的 REST API 端点。

解决方案

使用先前生成的 JAX-RS GreetingResource 资源,并填充 JAX-RS 注解。

JAX-RS 是 Quarkus 中用于定义 REST 端点的默认框架。所有 JAX-RS 注解都已经正确地存在于您的类路径上。您将需要使用 HTTP 动词注解(@GET@POST@PUT@DELETE)来声明端点方法将监听的 HTTP 动词。当然,您还需要 @Path 注解来定义相对于应用程序其余部分的 URI。

打开 org.acme.quickstart.GreetingResource.java

package org.acme.quickstart;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class GreetingResource {
    @GET ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    @Produces(MediaType.TEXT_PLAIN) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    public String hello() {
        return "hello"; ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
    }
}

1

标识当前资源的 URI 路径

2

响应 HTTP GET 请求

3

定义返回的媒体类型(们)

4

返回纯文本

让我们创建用于创建、更新和删除消息的其余方法:

@POST ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@Consumes(MediaType.TEXT_PLAIN) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
public void create(String message) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    System.out.println("Create");
}

@PUT ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public String update(String message) {
    System.out.println("Update");
    return message;
}

@DELETE ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
public void delete() {
    System.out.println("Delete");
}

1

响应 HTTP POST 请求

2

定义接受的媒体类型(们)

3

请求的主体内容

4

响应 HTTP PUT 请求

5

响应 HTTP DELETE 请求

以下是有效的 HTTP 方法:@GET@POST@PUT@DELETE@PATCH@HEAD@OPTIONS

3.2 提取请求参数

问题

您想要使用 JAX-RS 提取请求参数。

解决方案

使用 JAX-RS 规范提供的一些内置注解。

打开 org.acme.quickstart.GreetingResource.java 类,并将 hello 方法与请求参数更改为以下提取内容:

public static enum Order {
    desc, asc;
}

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello(
                @Context UriInfo uriInfo, ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
                @QueryParam("order") Order order, ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
                @NotBlank @HeaderParam("authorization") String authorization ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
                ) {

    return String.format("URI: %s - Order %s - Authorization: %s",
                         uriInfo.getAbsolutePath(), order, authorization);
}

1

获取请求的 UriInfoUriInfo 是 JAX-RS 的一部分,允许您获取应用程序和请求 URI 信息

2

获取名为 order 的查询参数作为 Enum

3

获取名为 authorization 的头部参数,集成了 Bean 验证

通过打开新的终端窗口,启动 Quarkus 应用程序,并发送请求到GET方法来尝试:

./mvnw clean compile quarkus:dev

curl -X GET "http://localhost:8080/hello?order=asc" \
 -H "accept: text/plain" -H "authorization: XYZ"
URI: http://localhost:8080/hello - Order asc - Authorization: XYZ

curl -X GET "http://localhost:8080/hello?order=asc" \
 -H "accept: text/plain" -v
HTTP/1.1 400 Bad Request

其他请求参数可以使用注解来提取,例如表单参数(@FormParam),矩阵参数(@MatrixParam)或者 cookie 值(@CookieParam)。同时,使用@Context注解,可以注入与 JAX-RS 相关的其他元素,如javax.ws.rs.core.SecurityContextjavax.ws.rs.sse.SseEventSinkjavax.ws.rs.sse.Sse

讨论

在 Recipe 3.1 中,您看到了如何使用 JAX-RS 创建 REST API 端点,但通常需要从请求中提取更多信息而不仅仅是主体内容。

使用 Quarkus 和 JAX-RS 时需要考虑的一件重要事情是,在内部,Quarkus 默认使用 RESTEasy 直接与 Vert.x 一起工作,而不使用与Servlet规范相关的任何内容。

总体而言,对于开发 REST API 端点可能需要的一切都得到了很好的支持,而且在需要实现自定义Servlet过滤器或直接将 HTTP 请求直接编码到代码中时,Quarkus 提供了替代方案。

但是,如果有要求的话,可以通过添加quarkus-undertow扩展来配置 Quarkus,以在使用Servlet规范而不是 Vert.x 的情况下使用 RESTEasy:

./mvnw quarkus:add-extension -Dextensions="quarkus-undertow"

./gradlew addExtension --extensions="quarkus-undertow"

参见

要了解更多关于 JAX-RS 的信息,请访问以下网站:

3.3 使用语义化 HTTP 响应状态码

问题

您希望使用 HTTP 响应状态码正确反映请求的结果。

解决方案

JAX-RS 规范使用javax.ws.rs.core.Response接口来返回正确的 HTTP 响应状态码,以及设置任何其他必需的信息,如响应内容、cookies 或头部:

package org.acme.quickstart;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

@Path("/developer")
public class DeveloperResource {

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response createDeveloper(Developer developer) {
        developer.persist();
        return Response.created( ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
            UriBuilder
                .fromResource(DeveloperResource.class) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
                .path(Long.toString(developer.getId())) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
                .build()
            )
            .entity(developer) ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
            .build(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
    }

    public static class Developer {

        static long counter = 1;

        private long id;
        private String name;

        public long getId() {
            return id;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void persist() {
            this.id = counter++;
        }
    }
}

1

将响应状态码设置为 201 并将Location头设置为 URI,以创建的资源。

2

从资源类设置路径

3

Location头中设置开发者 ID。

4

将创建的开发者设置为响应内容

5

构建Response对象

注意

如果从你的端点返回 JSON,则在项目中需要quarkus-resteasy-jsonbquarkus-resteasy-jackson扩展。

通过打开新的终端窗口,启动 Quarkus 应用程序,并发送请求到GET方法来尝试:

./mvnw clean compile quarkus:dev

curl -d '{"name":"Ada"}' -H "Content-Type: application/json" \
 -X POST http://localhost:8080/developer -v

< HTTP/1.1 201 Created
< Content-Length: 21
< Content-Type: application/json
< Location: http://localhost:8080/developer/1
<
{"id":1,"name":"Ada"}

请注意,Location头包含一个有效的 URI,用于访问已创建的资源。

讨论

在定义 RESTful Web API 时,遵循底层技术提供的一些约定非常重要;对于 RESTful Web 服务来说,这是 HTTP 层。

定义 API 的另一个关键部分是使用正确的响应状态代码,这些代码将返回给客户端,以指示请求是否已完成。有五类状态代码:

  • 信息响应(100–199)

  • 成功的响应(200–299)

  • 重定向(300–399)

  • 客户端错误(400–499)

  • 服务器错误(500–599)

默认情况下,Quarkus 尝试提供正确的 HTTP 状态代码的响应。例如,在约束违规的情况下,它提供 400 Bad Request,在服务器异常的情况下,它提供 500 Internal Server Error。但有一种情况不会默认处理:在创建资源时,应该向客户端发送一个 HTTP 201 Created 状态响应代码,该代码应在消息体中带有新资源,并将新资源的 URL 设置在Location头中。

另请参阅

完整的 HTTP 响应状态码总结在以下网站:

3.4 绑定 HTTP 方法

问题

您希望将方法绑定到 HTTP 动词,但 JAX-RS 规范没有提供专用的注解。

解决方案

使用javax.ws.rs.HttpMethod注解创建您的 HTTP 方法注解。

JAX-RS 规范提供了七个注解来指定方法应该响应的 HTTP 方法。这些注解是@GET@POST@PUT@DELETE@PATCH@HEAD@OPTIONS。但还有许多其他 HTTP 方法,JAX-RS 提供了javax.ws.rs.HttpMethod注解来支持这些其他方法。

首先要做的是创建一个元注解。我们将使用在RFC-4918中定义的LOCK动词,该动词锁定访问或刷新资源的现有锁。我们的注解将命名为LOCK,并用@javax.ws.rs.HttpMethod注解:

package org.acme.quickstart;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.ws.rs.HttpMethod;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("LOCK") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@Documented
public @interface LOCK {
}

1

LOCK HTTP 方法绑定到注解

最后,将此注解用于资源方法,以将其绑定到LOCK HTTP 动词。

打开org.acme.quickstart.GreetingResource.java类并创建一个LOCK方法:

@LOCK ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@Produces(MediaType.TEXT_PLAIN)
@Path("{id}")
public String lockResource(@PathParam("id") long id) {
    return id + " locked";
}

1

绑定到LOCK HTTP 方法

通过打开新的终端窗口,启动 Quarkus 应用程序,并向LOCK方法发送请求来尝试它:

./mvnw clean compile quarkus:dev

curl -X LOCK http://localhost:8080/hello/1
1 locked

另请参阅

完整的 HTTP 方法列表可以在以下 GitHub 页面找到:

3.5 启用跨域资源共享(CORS)

问题

您希望从另一个域请求受限资源。

解决方案

使用quarkus.http.cors配置属性启用 CORS。

讨论

跨域资源共享(CORS)是一种机制,允许从提供第一个资源的域之外的另一个域请求受限资源。Quarkus 提供了一组配置属性来配置 CORS。

要在 Quarkus 中启用 CORS,您需要在 application.properties 文件中将 quarkus.http.cors 配置属性设置为 true

CORS 配置示例可能如下所示:

quarkus.http.cors=true
quarkus.http.cors.origins=http://example.com
quarkus.http.cors.methods=GET,PUT,POST,DELETE
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with

您可以使用 curl 查看输出和头部:

curl -d '{"name":"Ada"}' -H "Content-Type: application/json" \
 -X POST http://localhost:8080/developer \
 -H "Origin: http://example.com" --verbose

输出应显示 access-control-allow-origin 头部:

upload completely sent off: 14 out of 14 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 Created
< access-control-allow-origin: http://example.com
< access-control-allow-credentials: true
< Content-Length: 21
< Content-Type: application/json
< Location: http://localhost:8080/developer/5

参见

您可以在以下 Wikipedia 页面上找到有关 CORS 的更多信息:

3.6 使用响应式路由

问题

您希望使用响应式路由实现 HTTP 端点。

解决方案

使用 Vert.x io.vertx.ext.web.Router 路由器实例或 i⁠o⁠.⁠q⁠u⁠a⁠r⁠k⁠u⁠s​.⁠v⁠e⁠r⁠t⁠x⁠.⁠w⁠e⁠b⁠.⁠R⁠o⁠u⁠t⁠e 注解。

在 Quarkus 中有两种使用响应式路由的方式。第一种方式是直接注册路由,使用 io.vertx.ext.web.Router 类。

要在启动时检索 Router 实例,您需要使用上下文和依赖注入(CDI)观察对象的创建。

创建一个名为 org.acme.quickstart.ApplicationRoutes.java 的新类:

package org.acme.quickstart;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;

import io.quarkus.vertx.http.runtime.filters.Filters;
import io.quarkus.vertx.web.Route;
import io.vertx.core.http.HttpMethod;   ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
import io.vertx.ext.web.Router;         ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
import io.vertx.ext.web.RoutingContext; ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)

@ApplicationScoped ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class ApplicationRoutes {

    public void routes(@Observes Router router) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

        router
            .get("/ok") ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
            .handler(rc -> rc.response().end("OK from Route")); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

    }
}

1

将对象实例化到 CDI 容器中,作用域为 application

2

提供 Router 对象来注册路由

3

GET HTTP 方法绑定到 /ok

4

处理逻辑

5

稍后在示例中使用的导入

通过打开新的终端窗口,启动 Quarkus 应用程序,并向新方法发送请求来尝试它:

./mvnw clean compile quarkus:dev

curl http://localhost:8080/ok
OK from Route

使用第二种方式使用响应式路由是一种声明性方法,使用 i⁠o⁠.⁠q⁠u⁠a⁠r⁠k⁠u⁠s​.⁠v⁠e⁠r⁠t⁠x⁠.⁠w⁠e⁠b⁠.⁠R⁠o⁠u⁠t⁠e 注解。要访问此注解,您需要添加 quarkus-vertx-web 扩展:

./mvnw quarkus:add-extension -Dextensions="quarkus-vertx-web"

然后您可以使用 @Route 注释方法。这些方法必须在 CDI bean 中定义。

打开 org.acme.quickstart.ApplicationRoutes.java 类并定义一个路由:

@Route(path = "/declarativeok", methods = HttpMethod.GET) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public void greetings(RoutingContext routingContext) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    String name = routingContext.request().getParam("name"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

    if (name == null) {
        name = "world";
    }

    routingContext.response().end("OK " + name + " you are right"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
}

1

设置 HTTP 路径和方法

2

使用 RoutingContext 获取请求信息

3

获取查询参数

4

处理逻辑

通过打开新的终端窗口,启动 Quarkus 应用程序,并向新方法发送请求来尝试它:

./mvnw clean compile quarkus:dev

curl localhost:8080/declarativeok?name=Alex
OK Alex you are right

讨论

Quarkus HTTP 基于非阻塞和响应式引擎。在幕后它使用 Vert.x 和 Netty。当接收到请求时,它由 事件循环 管理,可以依靠工作线程(对于 servlet 或 JAX-RS),或使用 I/O 线程(对于响应式路由),来处理调用的逻辑。

需要注意的是,响应式路由必须是非阻塞的,或者显式声明为阻塞;否则,由于响应式事件循环的特性,会阻塞循环,从而导致无法处理进一步的循环,直到线程解除阻塞。

在同一个项目中,您可以毫无问题地混合使用 JAX-RS 端点和响应式路由。

参见

您可以在以下网页了解更多有关 Vert.x 中响应式路由的信息:

3.7 拦截 HTTP 请求

问题

您希望拦截 HTTP 请求以操纵请求或响应。

解决方案

有时候您需要在到达终端逻辑之前操纵请求(例如安全检查),或者在返回给调用者的响应发送之前操纵响应(例如压缩响应)。使用 Quarkus,您可以通过 Vert.x 的Filters或者 JAX-RS 的过滤器接口来拦截 HTTP 请求。

让我们看看如何使用io.quarkus.vertx.http.runtime.filters.Filters来实现过滤器。

要在启动时获取Filters实例,您需要观察使用 CDI 创建对象的过程。

打开org.acme.quickstart.ApplicationRoutes.java类,并添加一个名为filters的方法:

public void filters(@Observes Filters filters) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    filters
        .register(
            rc -> {
                rc.response() ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
                    .putHeader("V-Header", "Header added by VertX Filter"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
                rc.next(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
            },
            10); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
}

1

提供Filters对象来注册过滤器

2

修改响应

3

向响应添加新的头部

4

继续过滤器链

5

设置执行顺序

需要注意的是,这些过滤器适用于 Servlet、JAX-RS 资源和响应式路由。

通过打开新的终端窗口,启动 Quarkus 应用,并向新方法发送请求来尝试它:

./mvnw clean compile quarkus:dev

echo Reactive-Route
curl localhost:8080/ok -v
< V-Header: Header added by VertX Filter
< content-length: 13
OK from Route

echo JAX-RS
curl -X GET "http://localhost:8080/hello?order=asc" \
 -H "accept: text/plain" -H "authorization: XYZ" -v
< V-Header: Header added by VertX Filter
< content-length: 65
URI: http://localhost:8080/hello - Order asc - Authorization: XYZ

注意,注册的过滤器修改了两个请求(响应式路由和 JAX-RS 端点),并添加了一个新的头部。

也可以使用javax.ws.rs.container.ContainerRequestFilter/javax.ws.rs.container.ContainerResponseFilter接口来实现过滤器。

创建一个名为org.acme.quickstart.HeaderAdditionContainerResponseFilter.java的新类:

package org.acme.quickstart;

import java.io.IOException;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;

@Provider ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class HeaderAdditionContainerResponseFilter
                implements ContainerResponseFilter { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

    @Override
    public void filter(ContainerRequestContext requestContext,
                       ContainerResponseContext responseContext)
            throws IOException {
                responseContext.getHeaders()
                  .add("X-Header", "Header added by JAXRS Filter"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    }
}

1

将此类设置为扩展接口

2

应用响应中的更改

3

向响应添加新的头部

此过滤器仅适用于 JAX-RS 资源,不适用于响应式路由。

通过打开新的终端窗口,启动 Quarkus 应用,并向新方法发送请求来尝试它:

./mvnw clean compile quarkus:dev

echo Reactive-Route
curl localhost:8080/ok -v
< V-Header: Header added by VertX Filter
< content-length: 13
OK from Route

echo JAX-RS
curl -X GET "http://localhost:8080/hello?order=asc" \
 -H "accept: text/plain" -H "authorization: XYZ" -v
< V-Header: Header added by VertX Filter
< Content-Length: 65
< Content-Type: text/plain;charset=UTF-8
< X-Header: Header added by JAXRS Filter
URI: http://localhost:8080/hello - Order asc - Authorization: XYZ

讨论

注意,在反应式路由端点的情况下,只添加了V-Header头部,而没有添加X-Header头部。与此同时,在 JAX-RS 端点中,请求由两个过滤器修改,同时添加了这两个 HTTP 头部。

参见

要了解更多关于 JAX-RS 和 Vert.x 的信息,您可以访问以下网站:

3.8 使用 SSL 进行安全连接

问题

您希望通过安全连接来防止攻击者窃取敏感信息。

解决方案

启用 Quarkus 以使用 SSL 来保护连接。

在传输敏感信息(密码、账号、健康信息等)时,保护客户端和应用程序之间的通信非常重要。因此,使用 SSL 保护服务之间的通信至关重要。

要保护通信,必须提供两个元素:证书和关联的密钥文件。这两者可以分别提供,也可以作为一个密钥库的形式提供。

让我们配置 Quarkus 使用包含证书条目的密钥库:

quarkus.http.ssl-port=8443 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.http.ssl.certificate.key-store-file=keystore.jks ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.http.ssl.certificate.key-store-file-type=jks
quarkus.http.ssl.certificate.key-store-password=changeit ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

设置 HTTPS 端口

2

密钥库的类型和相对于src/main/resources的位置

3

打开密钥库的密码

启动应用程序并发送请求到 HTTPS 端点:

./mvnw clean compile quarkus:dev

curl --insecure https://localhost:8443/hello
hello

由于证书是自签名的,提供了--insecure标志以跳过证书验证。在证书不是自签名的情况下,不应提供insecure标志。在本例中为简单起见使用了该标志。

重要提示

在配置文件中以明文提供密码是一种不好的做法。可以通过环境变量QUARKUS_HTTP_SSL_CERTIFICATE_KEY_STORE_PASSWORD来提供,这是在引入 MicroProfile Config 规范时在本书开头介绍的。

讨论

对于忙碌的开发者,这是如何为 Quarkus 生成您自己的密钥证书的方法:

  1. 转到src/main/resources

  2. 执行以下命令:

    keytool -genkey -keyalg RSA -alias selfsigned \
     -keystore keystore.jks -storepass changeit \
     -validity 360 -keysize 2048
    

参见

要了解如何生成证书、密钥库和信任库,请参阅以下网页:

第四章:配置

在本章中,您将学习有关设置配置参数的以下内容:

  • 如何配置 Quarkus 服务

  • 如何在服务中注入配置参数

  • 如何根据环境应用值

  • 如何正确配置日志系统

  • 如何为配置系统创建自定义项

4.1 使用自定义属性配置应用程序

问题

您希望使用自定义属性配置 Quarkus 应用程序。

解决方案

Quarkus 利用了多个 Eclipse MicroProfile 规范之一。其中之一是配置规范;但是,为了简化配置,Quarkus 仅使用一个文件来进行所有配置,即application.properties,该文件必须放置在类路径的根目录下。

此文件可用于配置 Quarkus 属性,如日志或默认路径,Quarkus 扩展如数据源或 Kafka,或者您为应用程序定义的自定义属性。您将在本书中看到所有这些内容,但在本节中,您将看到后者。

打开src/main/resources/application.properties文件,并添加以下属性:

greeting.message=Hello World

您可以使用org.eclipse.microprofile.config.inject.ConfigProperty注解在字段中注入application.properties中定义的属性值。

打开org.acme.quickstart.GreetingResource.java并注入greeting.message属性值:

@ConfigProperty(name = "greeting.message") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
String message; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    return message; ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
}

1

注入greeting.message属性的值

2

将字段置于包保护范围内

3

返回配置的数值

提示

出于性能原因,在使用 GraalVM 和反射时,我们建议您在运行时注入的字段上使用protected-package范围。您可以在Quarkus CDI 参考指南中了解更多信息。

在新的终端窗口中,向/hello发出请求,查看输出消息是否为application.properties中配置的值:

curl http://localhost:8080/hello

Hello World

如果要使配置字段非强制且提供默认值,可以使用@ConfigProperty注解的defaultValue属性。

打开org.acme.quickstart.GreetingResource.java文件并注入greeting.upper-case属性值:

@ConfigProperty(name = "greeting.upper-case",
                defaultValue = "true") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
boolean upperCase;
@GET
@Path("/optional")
@Produces(MediaType.TEXT_PLAIN)
public String helloOptional() {
    return upperCase ? message.toUpperCase() : message;
}

1

greeting.upper-case属性的默认值设置为 true

然后在终端窗口,向/hello/optional发出请求,查看输出消息是否为大写:

curl http://localhost:8080/hello/optional

HELLO WORLD

支持多值属性——您只需将字段类型定义为Arraysjava.util.Listjava.util.Set之一,具体取决于您的需求/偏好。属性值的分隔符是逗号(,),转义字符是反斜杠(\)。

打开src/main/resources/application.properties文件,并添加具有三个值的以下属性:

greeting.suffix=!!, How are you???

打开 org.acme.quickstart.GreetingResource.java 并注入 greeting.suffix 属性值:

@ConfigProperty(name = "greeting.suffix")
List<String> suffixes;
@GET
@Path("/list")
@Produces(MediaType.TEXT_PLAIN)
public String helloList() {
    return message + suffixes.get(1);
}

并在终端窗口中发出 /hello/list 请求,以查看输出消息是否包含第二后缀:

curl http://localhost:8080/hello/list

Hello World How are you?

YAML 格式也支持配置应用程序。在这种情况下,文件名为 application.yamlapplication.yml

要开始使用 YAML 配置文件,您需要添加 config-yaml 扩展:

./mvnw quarkus:add-extension -Dextensions="config-yaml"

给定以下使用 properties 格式的配置文件:

greeting.message=Hello World

%staging.quarkus.http.port=8182

quarkus.http.cors=true
quarkus.http.cors.methods=GET,PUT,POST

YAML 格式中的等效格式如下:

greeting:
  message: Hello World ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
"%staging": ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
  quarkus:
    http:
      port: 8182
quarkus:
  http:
    cors:
      ~: true ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
      methods: GET,PUT,POST

1

简单属性被设置为一个结构

2

支持包含在引号中的配置文件

3

当存在子键时,使用 ~ 来引用无前缀部分

讨论

Eclipse MicroProfile 配置带有以下内置转换器,以将配置值映射为 Java 对象:

  • booleanjava.lang.Boolean;true 的值为 true1YESYON,其他任何值都被视为 false

  • bytejava.lang.Byte

  • shortjava.lang.Short

  • intjava.lang.Integer

  • longjava.lang.Long

  • floatjava.lang.Float

  • doublejava.lang.Double

  • charjava.lang.Character

  • 基于调用 Class.forName 的结果的 java.lang.Class

如果不存在内置转换器或自定义转换器,则在目标对象中检查以下方法。如果存在内置转换器或自定义转换器,则使用找到的方法实例化转换器对象,并将字符串参数传递进行转换:

  • 目标类型具有 public static T of(String) 方法

  • 目标类型具有 public static T valueOf(String) 方法

  • 目标类型具有带有 String 参数的公共构造函数

  • 目标类型具有 public static T parse(CharSequence) 方法

4.2 以编程方式访问配置属性

问题

您希望以编程方式访问配置属性,而不是使用 org.eclipse.microprofile.config.inject.ConfigProperty 注解进行注入。

解决方案

在您希望以编程方式访问属性的对象中注入 org.eclipse.microprofile.config.Config 类。

Eclipse MicroProfile 配置规范允许您注入 org.eclipse.microprofile.config.Config 以便以编程方式获取属性,而不是直接使用 ConfigProperty 进行注入。

打开 org.acme.quickstart.GreetingResource.java 并注入 Config 类:

@Inject ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
Config config;
@GET
@Path("/config")
@Produces(MediaType.TEXT_PLAIN)
public String helloConfig() {
    config.getPropertyNames().forEach( p -> System.out.println(p)); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

    return config.getValue("greeting.message", String.class); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
}

1

使用 Inject CDI 注解来注入实例

2

现在可以访问属性列表

3

需要将属性转换为最终类型

您可以通过调用 ConfigProvider.getConfig() 方法访问 Config 类,而无需使用 CDI。

4.3 在外部覆盖配置值

问题

你希望在运行时覆盖任何配置值。

解决方案

你可以通过将其设置为系统属性或环境变量来在运行时覆盖任何属性。

Quarkus 允许你通过将配置设置为系统属性(-Dproperty.name=value)和/或环境变量(export PROPERTY_NAME=value)来覆盖任何配置属性。系统属性比环境变量具有更高的优先级。

外部化这些属性的示例可以是数据库 URL、用户名或密码,因为它们仅在目标环境中知道。但是你需要知道这是一种权衡,因为可用的运行时属性越多,Quarkus 能够执行的构建时间预处理就越少。

让我们对配方 4.1 中使用的应用程序进行打包,并通过设置系统属性覆盖greeting.message属性:

./mvnw clean package -DskipTests

java -Dgreeting.message=Aloha -jar target/getting-started-1.0-SNAPSHOT-runner.jar

在新的终端窗口中运行以下命令验证属性已从Hello World覆盖为Aloha

curl localhost:8080/hello

Aloha

对于环境变量,针对给定属性名称支持三种命名约定。这是因为一些操作系统只允许字母字符和下划线(_),而不允许其他字符,如点(.)。为了支持所有可能的情况,使用以下规则:

  1. 完全匹配(greeting.message)。

  2. 将非字母数字字符替换为下划线(greeting_message)。

  3. 将非字母数字字符替换为下划线,并将其余字符转换为大写(GREETING_MESSAGE)。

这是application.properties文件:

greeting.message=Hello World

您可以使用以下任何环境变量名称来覆盖其值,因为它们都是等效的:

export greeting.message=Aloha
export greeting_message=Aloha
export GREETING_MESSAGE=Aloha

还有一个特殊的地方,你可以将application.properties文件放在应用程序之外,放在一个名为config的目录内,该目录位于应用程序运行的地方。该文件中定义的任何运行时属性都将覆盖默认配置。

重要提示

config/application.properties也适用于开发模式,但是您需要将其添加到构建工具的输出目录才能使其正常工作(在 Maven 的情况下,是target目录;在 Gradle 的情况下,是build),因此您需要注意在运行clean任务时重新创建它的需要。

除了环境变量和application.properties文件之外,您还可以将.env文件放在当前工作目录中,以覆盖配置值,遵循环境变量格式(GREETING_MESSAGE=Aloha)。

4.4 配置与配置文件

问题

你希望根据运行 Quarkus 的环境覆盖配置值。

解决方案

Quarkus 支持配置文件的概念。这允许您在同一文件中为同一属性具有多个配置值,并使不同的值适合您正在运行服务的环境。

配置文件的语法是%{profile}.config.key=value

讨论

Quarkus 带有三个内置配置文件。

开发

激活开发模式时(即,quarkus:dev)。

测试

在运行测试时激活。

生产

当不在开发或测试模式下运行时的默认配置文件;您不需要在 application.properties 中设置它,因为它会隐式设置。

打开 src/main/resources/application.properties 文件,并设置在开发模式下以端口 8181 启动 Quarkus:

%dev.quarkus.http.port=8181

在此更改后,启动服务以再次检查监听端口是否为 8181 而不是默认端口(8080):

./mvnw compile quarkus:dev

INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed
 in 671ms
INFO  [io.quarkus] (main) Quarkus 1.4.1 started in 1.385s. Listening on:
 http://0.0.0.0:8181
INFO  [io.quarkus] (main) Profile dev activated. Live Coding activated.
INFO  [io.quarkus] (main) Installed features:
 [cdi, hibernate-validator, resteasy]

注意,现在监听地址是http://0.0.0.0:8181,而不是默认地址。

最后,回滚到 8080 端口,在 application.properties 中删除 %dev.quarkus.http.port=8181 行,以与书中其他部分使用的端口对齐。

4.5 更改日志配置

问题

您希望更改默认的日志配置。

解决方案

Quarkus 使用统一的配置模型,其中所有配置属性都放在同一个文件中。对于 Quarkus 来说,这个文件就是 application.properties,您可以在其中配置许多日志方面。

例如,如果要更改日志级别,只需将 quarkus.log.level 设置为最低日志级别。

打开 src/main/resources/application.properties 并添加以下内容:

quarkus.log.level=DEBUG

现在启动应用程序以查看控制台中打印了大量新的日志消息:

./mvnw compile quarkus:dev

...
[INFO] --- quarkus-maven-plugin:0.22.0:dev (default-cli) @ getting-started ---
Listening for transport dt_socket at address: 5005
DEBUG [org.jbo.logging] (main) Logging Provider: \
 org.jboss.logging.JBossLogManagerProvider
INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
DEBUG [io.qua.run.con.ConverterSupport] (main) Populate SmallRye config builder
 with converter for class java.net.InetSocketAddress of priority 200
DEBUG [io.qua.run.con.ConverterSupport] (main) Populate SmallRye config builder
 with converter for class org.wildfly.common.net.CidrAddress of priority 200
注意

我们必须跨多行进行格式化以适应书籍;我们使用反斜杠来指示这一点。

您还可以通过使用 quarkus.log.file.enable 属性将日志存储到文件中。输出默认写入名为 quarkus.log 的文件中:

quarkus.log.file.enable=true
注意

在开发并从源目录工作时,您的日志文件将位于 target 目录中。

4.6 添加应用程序日志

问题

您希望向应用程序添加日志行。

解决方案

大多数时候,您的应用程序需要编写自己的日志消息,而不仅仅依赖于 Quarkus 提供的默认日志。应用程序可以使用任何支持的日志 API 进行日志记录,并且这些日志将被合并。

Quarkus 支持这些日志记录库:

  • JDK java.util.logging

  • JBoss 日志记录

  • SLF4J

  • Apache Commons Logging

让我们看看如何使用 JBoss Logging 记录内容。打开 org.acme.quickstart.GreetingResource.java 并在调用特定端点时记录消息:

private static org.jboss.logging.Logger logger =
                org.jboss.logging.Logger.getLogger(GreetingResource.class); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

@GET
@Path("/log") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
@Produces(MediaType.TEXT_PLAIN)
public String helloLog() {
    logger.info("I said Hello"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    return "hello";
}

1

创建日志记录器实例

2

终端子路径是 /log

3

信息级别的日志

现在启动应用程序:

./mvnw compile quarkus:dev

在新的终端窗口中,向 /hello/log 发出请求:

curl http://localhost:8080/hello/log

如果检查您启动 Quarkus 的终端,您会看到下一个日志行:

INFO  [org.acm.qui.GreetingResource] (executor-thread-1) I said Hello

讨论

日志是按类别进行的。适用于类别的配置也适用于该类别的所有子类别,除非有更具体的子类别配置。

分类由类的位置表示(即定义它们的包或子包)。例如,如果要将 Undertow 安全日志设置为跟踪级别,需要在application.properties中设置quarkus.log.category."io.undertow.request.security".level=TRACE属性。

跟随前面的示例,让我们限制来自org.acme.quickstart包(及其子类)的日志行,以确保最低日志级别为WARNING

quarkus.log.category."org.acme.quickstart".level=WARNING ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

1

引号是设置类别的必需部分

如果您重复请求http://localhost:8080/hello/log,则日志行不再被记录。

4.7 高级日志

问题

您希望将所有服务的日志集中记录。

解决方案

在处理微服务架构和 Kubernetes 时,日志记录是需要考虑的重要事项,因为每个服务都会单独记录日志;但作为开发人员或操作员,您可能希望将所有日志集中到一个位置,以便作为整体使用。

Quarkus 日志还支持 JSON 和 GELF 输出。

这些日志可以以 JSON 格式而不是纯文本形式编写,以供通过注册logging-json扩展进行机器处理:

./mvnw quarkus:add-extension -Dextensions="logging-json"

使用 GELF 扩展以生成 GELF 格式的日志,并使用 TCP 或 UDP 发送它们。

Graylog 扩展日志格式(GELF)现在被三个最常用的集中式日志系统所支持:

  • Graylog(MongoDB,Elasticsearch,Graylog)

  • ELK(Elasticsearch,Logstash,Kibana)

  • EFK(Elasticsearch,Fluentd,Kibana)

要开始以 GELF 格式记录日志,您只需添加logging-gelf扩展即可:

./mvnw quarkus:add-extension -Dextensions="logging-gelf"

日志代码不会更改,因此仍使用相同的接口:

private static org.jboss.logging.Logger logger =
                org.jboss.logging.Logger.getLogger(GreetingResource.class); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

@GET
@Path("/log") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
@Produces(MediaType.TEXT_PLAIN)
public String helloLog() {
    logger.info("I said Hello"); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    return "hello";
}

1

创建记录器实例

2

终端子路径为/log

3

info级别的日志

必须在application.properties中配置 GELF 处理程序:

quarkus.log.handler.gelf.enabled=true ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.log.handler.gelf.host=localhost ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.log.handler.gelf.port=12201 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

启用扩展

2

设置发送日志消息的主机

3

设置端点端口

重要

如果使用 Logstash(ELK),则需要启用能够理解 GELF 格式的输入插件:

input {
  gelf {
    port => 12201
  }
}
output {
  stdout {}
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
  }
}
重要

如果使用 Fluentd(EFK),则需要启用能够理解 GELF 格式的输入插件:

<source>
  type gelf
  tag example.gelf
  bind 0.0.0.0
  port 12201
</source>

<match example.gelf>
  @type elasticsearch
  host elasticsearch
  port 9200
  logstash_format true
</match>

讨论

Quarkus 日志还通过默认支持 syslog 格式,无需添加任何扩展。在 Fluentd 中,syslog 格式可用作 Quarkus 中 GELF 格式的替代:

quarkus.log.syslog.enable=true
quarkus.log.syslog.endpoint=localhost:5140
quarkus.log.syslog.protocol=udp
quarkus.log.syslog.app-name=quarkus
quarkus.log.syslog.hostname=quarkus-test
重要

您需要启用能够理解 Fluentd 中 syslog 格式的输入插件:

<source>
  @type syslog
  port 5140
  bind 0.0.0.0
  message_format rfc5424
  tag system
</source>

<match **>
  @type elasticsearch
  host elasticsearch
  port 9200
  logstash_format true
</match>

如果使用 Kubernetes,最简单的日志记录方式是记录到控制台,并在集群中安装一个中央日志管理器以收集所有日志行。

参见

要了解更多高级日志主题,请访问以下网站:

4.8 使用自定义配置文件配置

问题

您希望为您创建的自定义配置文件设置不同的配置值。

解决方案

到目前为止,您已经看到 Quarkus 具有内置的配置文件,因此您可以为同一属性设置不同的配置值,并使它们适应环境。但是,使用 Quarkus,您也可以设置自己的配置文件。

您唯一需要做的就是指定要启用的配置文件,要么使用quarkus.profile系统属性,要么使用QUARKUS_PROFILE环境变量。如果两者都设置了,系统属性优先于环境变量。

然后,您需要做的唯一一件事是创建带有配置文件名的属性,并将当前配置文件设置为该名称。让我们创建一个名为staging的新配置文件,该配置文件将覆盖 Quarkus 的侦听端口。

打开src/main/resources/application.properties文件,并设置在启用staging配置文件时将 Quarkus 启动到端口 8182:

%staging.quarkus.http.port=8182

然后使用staging配置文件启动应用程序:

./mvnw -Dquarkus.profile=staging compile quarkus:dev

INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed
 in 640ms
INFO  [io.quarkus] (main) Quarkus 0.23.2 started in 1.300s. Listening on:
 http://0.0.0.0:8182
INFO  [io.quarkus] (main) Profile staging activated. Live Coding activated.
INFO  [io.quarkus] (main) Installed features: [cdi, hibernate-validator,
 resteasy]

在这种情况下,使用系统属性方法,但您也可以使用QUARKUS_PROFILE环境变量设置它。

讨论

如果要在测试中设置运行配置文件,则只需要在构建脚本中将quarkus.test.profile系统属性设置为给定配置文件即可,例如,在 Maven 中:

<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
    <systemPropertyVariables>
        <quarkus.test.profile>foo</quarkus.test.profile>
        <buildDirectory>${project.build.directory}</buildDirectory>
    </systemPropertyVariables>
</configuration>

或者,在 Gradle 中:

test {
    useJUnitPlatform()
    systemProperty "quarkus.test.profile", "foo"
}

另外,您可以更改默认的生产配置文件。Quarkus 中内置的配置文件是prod,因此当您在不指定任何配置文件的情况下运行应用程序时,这是默认配置文件,其中包含的值被使用。但是,您可以在构建时更改该配置文件,以便在应用程序运行时不指定任何配置文件时,您的配置文件是默认配置文件。

你需要做的唯一一件事是使用quarkus.profile系统属性构建应用程序,并将要设置为默认值的配置文件命名为配置文件值:

./mvnw package -Pnative -Dquarkus.profile=prod-kubernetes` ./target/getting-started-1.0-runner ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

1

默认情况下,该命令将使用prod-kubernetes配置文件启用

4.9 创建自定义源

问题

您希望从除application.properties文件之外的任何其他源加载配置参数。

解决方案

Quarkus 使用 Eclipse MicroProfile Configuration 规范来实现有关配置的所有逻辑。该规范提供了org.eclipse.microprofile.config.spi.ConfigSource Java SPI 接口,用于实现一种自定义加载配置属性的方式,而不是使用 Quarkus 提供的默认方式。

例如,您可以从数据库、XML 文件或 REST API 加载配置属性。

让我们创建一个简单的内存配置源,该源在实例化时从Map中获取配置属性。创建一个名为org.acme.quickstart.InMemoryConfigSource.java的新类:

package org.acme.quickstart;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.microprofile.config.spi.ConfigSource;

public class InMemoryConfigSource implements ConfigSource {

    private Map<String, String> prop = new HashMap<>();

    public InMemoryConfigSource() { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
        prop.put("greeting.color", "red");
    }

    @Override
    public int getOrdinal() { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        return 500;
    }

    @Override
    public Map<String, String> getProperties() { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
        return prop;
    }

    @Override
    public String getValue(String propertyName) { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
        return prop.get(propertyName);
    }

    @Override
    public String getName() { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
        return "MemoryConfigSource";
    }

}

1

用属性填充映射

2

用于确定值的重要性;最高的序数优先于优先级较低的序数

3

获取所有属性作为 Map;在这种情况下,它是直接的

4

获取单个属性的值

5

返回此配置源的名称

然后,您需要将其注册为 Java SPI。在 src/main/resources/META-INF 下创建 services 文件夹。接下来,在 services 文件夹内创建一个名为 org.eclipse.microprofile.config.spi.ConfigSource 的文件,其内容如下:

org.acme.quickstart.InMemoryConfigSource

最后,您可以修改 org.acme.quickstart.GreetingResource.java 类来注入这个属性:

@ConfigProperty(name = "greeting.color") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
String color;

@GET
@Path("/color")
@Produces(MediaType.TEXT_PLAIN)
public String color() {
    return color;
}

1

注入在 InMemoryConfigSource 中定义的属性值

然后在终端窗口中对 /hello/color 发出请求,以查看输出消息是否为自定义源中配置的值:

curl http://localhost:8080/hello/color

red

讨论

每个 ConfigSource 都有一个指定的序数,用于在同一应用程序中对多个配置源定义的情况下设置值的重要性。如果有多个 ConfigSource,则使用较高的序数 ConfigSource 而不是具有较低值的 ConfigSource。使用以下列表中的默认值作为参考,系统属性将优先于一切,如果没有找到其他 ConfigSources,则使用 src/main/resources 目录中的 application.properties 文件:

  • 将系统属性设为 400

  • 将环境变量设为 300

  • config 目录下的 application.properties 至 260

  • 项目中的 application.properties 至 250

4.10 创建自定义转换器

问题

您想要实现一个自定义转换器。

解决方案

您可以通过实现 org.eclipse.microprofile.config.spi.Converter Java SPI 将属性从 String 转换为任何类型的对象。

Quarkus 使用 Eclipse MicroProfile Configuration 规范来实现所有关于配置的逻辑。该规范提供了 org.eclipse.microprofile.config.spi.Converter Java SPI 接口,用于将配置值转换为自定义类型。

例如,您可以将百分比值(即,15%)转换为 Percentage 类型,将百分比包装为 double 类型。

创建一个新的 POJO 类 org.acme.quickstart.Percentage.java

package org.acme.quickstart;

public class Percentage {

    private double percentage;

    public Percentage(double percentage) {
        this.percentage = percentage;
    }

    public double getPercentage() {
        return percentage;
    }

}

然后创建一个名为 org.acme.quickstart.PercentageConverter.java 的类,将 String 表示转换为 Percentage

package org.acme.quickstart;

import javax.annotation.Priority;

import org.eclipse.microprofile.config.spi.Converter;

@Priority(300) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class PercentageConverter implements Converter<Percentage> { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

    @Override
    public Percentage convert(String value) {

        String numeric = value.substring(0, value.length() - 1);
        return new Percentage (Double.parseDouble(numeric) / 100);

    }

}

1

设置优先级;在这种特定情况下可能是可选的

2

泛型类型,将其类型设置为要转换的类型

然后,您需要将其注册为 Java SPI。在 src/main/resources/META-INF/services 下创建 services 文件夹。接下来,在 services 文件夹内创建一个名为 org.eclipse.microprofile.config.spi.Converter 的文件,其内容如下:

org.acme.quickstart.PercentageConverter

然后,您可以修改 org.acme.quickstart.GreetingResource.java 类以注入此属性:

@ConfigProperty(name = "greeting.vat")
Percentage vat;

@GET
@Path("/vat")
@Produces(MediaType.TEXT_PLAIN)
public String vat() {
    return Double.toString(vat.getPercentage());
}

最后,您需要在 src/main/resources 目录中的 application.properties 文件中添加一个新属性:

greeting.vat = 21%

然后,在终端窗口中,发出请求 /hello/vat,以查看输出消息是否将增值税转换为双倍:

curl http://localhost:8080/hello/vat

0.21

讨论

默认情况下,如果在转换器上找不到 @Priority 注释,则将其注册为优先级为 100。Quarkus 转换器注册为优先级 200,因此如果要替换 Quarkus 转换器,应使用更高的值;如果不需要替换 Quarkus 转换器,则默认转换器完全合适。

Recipe 4.1 中展示了一些 Quarkus 核心转换器的列表。

4.11 分组配置值

问题

您希望避免一遍又一遍地设置配置属性的公共前缀。

解决方案

您可以使用 @⁠i⁠o​.⁠q⁠u⁠a⁠r⁠k⁠u⁠s⁠.⁠a⁠r⁠c⁠.⁠c⁠o⁠n⁠f⁠i⁠g⁠.⁠C⁠o⁠n⁠f⁠i⁠g⁠P⁠r⁠o⁠p⁠e⁠r⁠t⁠i⁠e⁠s 注释来分组共同的属性(具有相同的前缀)。

当您在应用程序中创建临时配置属性时,通常这些属性将具有相同的前缀(即 greetings)。要注入所有这些属性,您可以使用 @ConfigProperty 注释(如 Recipe 4.1 中所示),或者您可以使用 io.quarkus.arc.config.ConfigProperties 注释将属性组合在一起。

使用 application.properties 文件:

greeting.message=Hello World
greeting.suffix=!!, How are you???

让我们实现一个类,使用 io.quarkus.arc.config.ConfigProperties 注释将配置属性映射到 Java 对象中。创建一个新类 org.acme.quickstart.GreetingConfiguration.java

package org.acme.quickstart;

import java.util.List;
import java.util.Optional;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;

import io.quarkus.arc.config.ConfigProperties;

@ConfigProperties(prefix = "greeting") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class GreetingConfiguration {

    public String message; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    public String suffix = "!"; ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
}

1

将其设置为具有共同前缀的配置 POJO

2

映射 greeting.message 属性

3

如果未设置该属性,则 greeting.suffix 的默认值

在上述代码中需要注意的一点是,prefix 属性并非强制性。如果未设置,那么将由类名(去除 Configuration 后缀部分)确定要使用的前缀。在这种情况下,prefix 属性可以自动解析为 greeting

然后,您可以注入此配置 POJO 开始使用配置值。

您可以修改 org.acme.quickstart.GreetingResource.java 类以注入此类:

@Inject ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
GreetingConfiguration greetingConfiguration;

@GET
@Path("/configurations")
@Produces(MediaType.TEXT_PLAIN)
public String helloConfigurations() {
    return greetingConfiguration.message + greetingConfiguration.suffix;
}

1

配置通过 CDI @Inject 注释注入

然后,在终端窗口中,发出请求 /hello/configurations,以查看配置值在 Java 内部被填充,例如:

curl http://localhost:8080/hello/configurations

Hello World!!, How are you???

正如您现在所看到的,您无需通过使用 @ConfigProperty 注释每个字段,只需利用类定义即可获取属性名称或默认值。

讨论

此外,Quarkus 支持嵌套对象配置,因此您也可以使用内部类映射子类别。

假设我们在application.properties中添加了一个名为greeting.output.recipients的新属性:

greeting.output.recipients=Ada,Alexandra

您可以使用内部类将其映射到配置对象中。修改org.acme.quickstart.GreetingConfiguration.java类。然后添加一个代表子类别output的新内部类,并将其注册为字段:

public OutputConfiguration output; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

public static class OutputConfiguration {
    public List<String> recipients;
}

1

子类别的名称是字段名称(output

然后,您可以访问greetingConfiguration.output.recipients字段以获取该值。您还可以使用 Bean Validation 注解对字段进行注释,以验证所有配置值在启动时是否有效。如果它们无效,应用程序将无法启动,并将在日志中指示验证错误。

4.12 验证配置数值

问题

您希望验证配置数值是否正确。

解决方案

使用 Bean Validation 规范验证通过@ConfigProperty注解注入的属性值是否有效。

Bean Validation 规范允许您使用注解在对象上设置约束。Quarkus 将 Eclipse MicroProfile 配置规范与 Bean Validation 规范集成在一起,因此您可以一起使用它们验证配置值是否符合某些标准。此验证在启动时执行,如果有任何违规,控制台将显示错误消息,并且启动过程将中止。

您首先需要注册Quarkus Bean Validation依赖项。您可以通过手动编辑pom.xml或从项目根目录运行下一个 Maven 命令来执行此操作:

./mvnw quarkus:add-extension -Dextensions="quarkus-hibernate-validator"

在此之后,您需要创建一个配置对象,这是您在前一篇章节学到的。在下一个示例中,设置对greeting.repeat配置属性的约束,以便不能设置超出 1 到 3 范围之外的重复次数。

要验证整数范围,使用以下 Bean Validation 注解:j⁠a⁠v⁠a⁠x​.⁠v⁠a⁠l⁠i⁠d⁠a⁠t⁠i⁠o⁠n⁠.⁠c⁠o⁠n⁠s⁠t⁠r⁠a⁠i⁠n⁠t⁠s⁠.⁠M⁠a⁠xjavax.validation.constraints.Min。打开org.acme.quickstart.GreetingConfiguration.java并添加 Bean Validation 注解:

@Min(1) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@Max(3) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
public Integer repeat;

1

最小接受值

2

最大接受值

打开src/main/resources/application.properties文件,并将greeting.repeat配置属性设置为 7:

greeting.repeat=7

启动应用程序,您将看到一个错误消息,通知某个配置值违反了定义的约束之一:

./mvnw compile quarkus:dev

讨论

在这个例子中,你已经看到了 Bean Validation 规范的简要介绍,以及一些可以用来验证字段的注解。然而,Hibernate Validation 和使用的 Bean Validation 实现支持更多的约束,比如@Digits@Email@NotNull@NotBlank

第五章:编程模型

在本章中,您将学习与 Quarkus 编程模型相关的主题。Quarkus 在其遵循的编程模型方面独具一格。与其他一些框架不同,Quarkus 允许您混合使用命令式模型(使用 CDI)和反应式模型(使用 SmallRye Mutiny)。第十五章专门介绍使用反应式方法。有时,您可能需要同时使用这两种方法,因此了解如何利用每种方法是很有益处的。

在本章中,我们专注于命令式模型,因此您将学习以下内容:

  • 如何编组/解组 JSON 和 XML 文档

  • 如何验证请求参数

  • 如何使用 CDI 规范作为上下文和依赖注入解决方案

  • 如何为 Quarkus 服务编写测试

5.1 编组/解组 JSON

问题

您希望将 JSON 文档与 Java 对象进行编组/解组。

解决方案

使用JSON-B规范或 Jackson 项目来从/向 Java 对象编组和解组 JSON 文档。

当您创建 REST API 时,通常使用 JSON 作为交换信息的数据格式。到目前为止,您已经看到了仅返回简单纯文本响应的示例;但在本节中,您将学习如何开始使用 JSON 作为请求体和响应的数据格式。

首先要做的是在pom.xml中注册JSON-B扩展。打开一个终端窗口,从项目的根目录运行以下命令:

./mvnw quarkus:add-extension -Dextensions="quarkus-resteasy-jsonb"

[INFO] --- quarkus-maven-plugin:1.4.1.Final:add-extension (default-cli)
 @ custom-config ---
✅ Adding extension io.quarkus:quarkus-resteasy-jsonb

这实际上将io.quarkus:quarkus-resteasy-jsonb添加到构建工具中。

注意

在 Gradle 中,您可以使用./gradlew addExtension --extensions="quarkus-resteasy-jsonb"来添加扩展。

下一步是创建一个developer类,该类将在端点中进行编组和解组。创建一个名为org.acme.quickstart.Developer.java的新类:

package org.acme.quickstart;

public class Developer {

    private String name;
    private String favoriteLanguage;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getFavoriteLanguage() {
        return favoriteLanguage;
    }

    public void setFavoriteLanguage(String favoriteLanguage) {
        this.favoriteLanguage = favoriteLanguage;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

}

最后,您可以创建一个 Rest API 端点来实现developer操作。创建一个名为org.acme.quickstart.DeveloperResource.java的新类:

package org.acme.quickstart;

import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/developer")
public class DeveloperResource {

    private static final List<Developer> developers = new ArrayList<>();

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response addDeveloper(Developer developer) {
        developers.add(developer);
        return Response.ok().build();
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Developer> getDevelopers() {
        return developers;
    }
}

通过在新的终端窗口中打开并启动 Quarkus 应用程序,并发送POSTGET方法的请求来尝试它:

./mvnw clean compile quarkus:dev

curl -d '{"name":"Alex","age":39, "favoriteLanguage":"java"}' \
 -H "Content-Type: application/json" -X POST http://localhost:8080/developer

curl localhost:8080/developer
[{"age":39,"favoriteLanguage":"java","name":"Alex"}]

注意,每个 Java 字段直接映射到一个 JSON 字段。如果您想更改此映射,可以使用javax.json.bind.annotation.JsonbProperty注解来设置不同的映射名称:

@JsonbProperty("favorite-language")
String favoriteLanguage;

讨论

您可以使用 Jackson 项目来编组/解组 JSON 文档与 Java 对象,而不是 JSON-B。您需要注册Jackson-Extension以将其用作 JSON 解决方案:

./mvnw quarkus:add-extension -Dextensions="quarkus-resteasy-jackson"

默认情况下,提供了一个com.fasterxml.jackson.databind.ObjectMapper,但您可以使用 CDI 提供自定义的ObjectMapper

package org.acme.quickstart;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.jackson.ObjectMapperCustomizer;
import javax.inject.Singleton;

@Singleton
public class RegisterCustomModuleCustomizer
    implements ObjectMapperCustomizer {

    public void customize(ObjectMapper mapper) {
        objectMapper.configure(
            DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
    }
}

参见

你可以在以下网页上了解有关 JSON-B 和 Jackson 的更多信息:

5.2 编组/解组 XML

问题

您希望将 XML 文档与 Java 对象进行编组/解组。

解决方案

使用 JAX-B 规范将 XML 文档从/到 Java 对象编组和解组。

当您创建 REST API 时,可能希望使用 XML 作为请求体和响应的数据格式。到目前为止,您已经了解了如何在 JSON 格式中实现它,但在本节中,您将学习如何开始使用 XML 作为请求体和响应的数据格式。

首先,您需要在 pom.xml 中注册 JAX-B 扩展。打开一个终端窗口,并从项目的根目录运行以下命令:

./mvnw quarkus:add-extension -Dextensions="quarkus-resteasy-jaxb"

[INFO] --- quarkus-maven-plugin:1.4.1.Final:add-extension (default-cli)
 @ custom-config ---
✅ Adding extension io.quarkus:quarkus-resteasy-jaxb

这实际上将 io.quarkus:quarkus-resteasy-jaxb 添加到构建工具中。

注意

在 Gradle 中,您可以使用 ./gradlew addExtension --extensions="quarkus-resteasy-jaxb" 来添加扩展。

下一步是创建一个 computer 类,在端点中将其编组和解组。创建一个名为 org.acme.quickstart.Computer.java 的新类:

package org.acme.quickstart;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class Computer {

    private String brand;
    private String serialNumber;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getSerialNumber() {
        return serialNumber;
    }

    public void setSerialNumber(String serialNumber) {
        this.serialNumber = serialNumber;
    }

}

1

XmlRootElement 将其设置为 XML 文档

最后,您可以创建一个 REST API 端点以实现 computer 操作。创建一个名为 org.acme.quickstart.ComputerResource.java 的新类:

package org.acme.quickstart;

import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/computer")
public class ComputerResource {

    private static final List<Computer> computers = new ArrayList<>();

    @POST
    @Consumes(MediaType.APPLICATION_XML)
    public Response addComputer(Computer computer) {
        computers.add(computer);
        return Response.ok().build();
    }

    @GET
    @Produces(MediaType.APPLICATION_XML)
    public List<Computer> getComputers() {
        return computers;
    }

}

通过打开新的终端窗口,启动 Quarkus 应用程序,并发送 POSTGET 方法的请求进行测试:

./mvnw clean compile quarkus:dev

curl \
 -d '<computer><brand>iMac</brand>
 <serialNumber>111-111-111</serialNumber></computer>'
 -H "Content-Type: application/xml" -X POST http://localhost:8080/computer

curl localhost:8080/computer
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><collection><computer>
<brand>iMac</brand><serialNumber>111-111-111</serialNumber>
</computer></collection>

讨论

除了 @XmlRootElement,JAX-B 规范中还有其他重要的注解:

@XmlRootElement

设置根 XML 文档。您还可以用它来设置元素的名称或命名空间。

@XmlType

定义字段写入的顺序。

@XmlElement

定义实际的 XML 元素名称,以及 namespacenillablerequired 等其他属性。

@XmlAttribute

定义将字段映射为属性而不是元素。

@XmlTransient

表示不包括在 XML 中的字段。

参见

您可以在以下网页了解更多关于 JAX-B 的信息:

5.3 验证输入和输出值

问题

您希望验证 REST 和业务服务的输入和输出值。

解决方案

使用 Bean Validation 规范向模型添加验证。

通常,您的模型可能包含一些约束条件,无论模型是否语义上有效——例如,name 不为空,或者 email 是有效的电子邮件。Quarkus 通过注解集成 Bean Validation 来表达对对象模型的约束。

首先,您需要在 pom.xml 中注册 Bean Validation 扩展。打开一个终端窗口,并从项目的根目录运行以下命令:

./mvnw quarkus:add-extension -Dextensions="quarkus-hibernate-validator"

[INFO] --- quarkus-maven-plugin:1.4.1.Final:add-extension (default-cli)
 @ custom-config ---
✅ Adding extension io.quarkus:quarkus-resteasy-jsonb

这实际上将 io.quarkus:quarkus-hibernate-validator 添加到构建工具中。

注意

在 Gradle 中,您可以使用 ./gradlew addExtension --extensions="quarkus-hibernate-validator" 来添加扩展。

下一步是更新 developer 类,并对其进行一些字段注解。打开 org.acme.quickstart.Developer.java 类并对一些字段进行注解:

@Size(min = 4) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
private String name;

@NotBlank ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
private String favoriteLanguage;

1

字符串的最小长度为 4

2

此字段是必填的

注意

可用的包之一就足够了,但是如果您只想使用规范 API,请使用 javax 包。

最后,您需要使用 javax.validation.Valid 注释来验证必须验证的参数。打开 org.acme.quickstart.DeveloperResource.java 类并对 developer 参数进行注释:

@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addDeveloper(@Valid Developer developer) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    developers.add(developer);
    return Response.ok().build();
}

1

@Valid 是验证对象的必需注解

尝试通过打开新的终端窗口,启动 Quarkus 应用程序,并执行 POST 方法的请求来测试它:

./mvnw clean compile quarkus:dev

curl -d '{"name":"Ada","age":7, "favoriteLanguage":"java"}' \
 -H "Content-Type: application/json" \
 -X POST http://localhost:8080/developer -v

< HTTP/1.1 400 Bad Request
< Content-Length: 89
< validation-exception: true
< Content-Type: text/plain;charset=UTF-8

curl -d '{"name":"Alexandra","age":5, "favoriteLanguage":"java"}' \
 -H "Content-Type: application/json" \
 -X POST http://localhost:8080/developer -v

< HTTP/1.1 200 OK
< Content-Length: 0

需要注意的是,在第一次请求中,名称的大小不正确,因此返回了 400 Bad Request HTTP 状态码。在第二次请求中,因为请求体是正确的,方法按预期工作。

但请注意,如果出现错误,则响应不包含有关失败内容的任何信息。这没关系,因为重要的是以受控的方式显示内部而不是直接显示。

讨论

如果要提供更好的响应消息,可以提供 ExceptionMapper 的实现。

创建一个名为 org.acme.quickstart.BeanValidationExceptionMapper.java 的新类:

package org.acme.quickstart;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class BeanValidationExceptionMapper
  implements ExceptionMapper<ConstraintViolationException> { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

    @Override
    public Response toResponse(ConstraintViolationException exception) {
      return Response.status(Response.Status.BAD_REQUEST)
        .entity(createErrorMessage(exception))
        .type(MediaType.APPLICATION_JSON)
        .build();
    }

    private JsonArray createErrorMessage(ConstraintViolationException exc) {
      JsonArrayBuilder errors = Json.createArrayBuilder(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
      for (ConstraintViolation<?> violation : exc.getConstraintViolations()) { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
        errors.add(
            Json.createObjectBuilder() ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
            .add("path", violation.getPropertyPath().toString())
            .add("message", violation.getMessage())
            );
      }
      return errors.build();
    }
}

1

@Provider 设置一个扩展接口的实现,由 JAX-RS 运行时发现

2

javax.ws.rs.ext.ExceptionMapper 用于将异常转换为 javax.ws.rs.core.Response

3

创建一个约束违规的数组

4

遍历每个约束违规

5

创建一个 JSON 对象

现在您可以再次发送 POST 方法的请求:

curl -d '{"name":"Ada","age":7, "favoriteLanguage":"java"}' \
 -H "Content-Type: application/json" \
 -X POST http://localhost:8080/developer -v

< HTTP/1.1 400 Bad Request
< Content-Length: 90
< Content-Type: application/json

[{"path":"addDeveloper.developer.name",
 "message":"size must be between 4 and 2147483647"}]%

现在输出略有不同。错误代码仍然是相同的,400 Bad Request,但是现在响应体的内容包含了我们在异常映射器中创建的 JSON 文档。

您还可以通过在返回类型中添加 @Valid 注解来验证输出参数(发送回调用者的参数):

@GET
@Produces(MediaType.APPLICATION_JSON)
public @Valid List<Developer> getDevelopers() {
    return developers;
}

此外,有时您不希望在端点上添加验证规则,而是希望在业务服务层上添加。如果您使用 CDI,可以在业务服务中使用 Bean Validation。参见以下示例:

@ApplicationScoped
public class DeveloperService {
    public void promoteDeveloper(@Valid Developer developer) {
    }
}

参见

如果您想进一步了解 Bean Validation 和默认实施的约束(如 @Min@Max@AssertTrue@Email 等),可以在以下网站找到信息:

5.4 创建自定义验证

问题

您想要创建自定义验证。

解决方案

通过实现 javax.validation.ConstraintValidator 接口使用 Bean Validation 扩展模型。

有时,Bean 验证规范提供的默认约束不足以满足您的需求,您需要实现更符合业务模型的约束。Bean 验证允许您通过创建一个实现 javax.validation.ConstraintValidator 接口和用于注释要验证的字段的注释来实现这一点。

让我们验证您最喜欢的语言只能是基于 JVM 的语言。首先,您需要创建注释。创建一个名为 org.acme.quickstart.JvmLanguage.java 的新类:

package org.acme.quickstart;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
          ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { JvmLanguageValidator.class}) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public @interface JvmLanguage {
    String message() default "You need to provide a Jvm based-language";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}

1

将约束错误作为普通编译错误引发

然后,您需要创建检测任何约束违规的逻辑。这个新类必须实现 javax.validation.ConstraintValidator 接口。

接下来,创建一个名为 org.acme.quickstart.JvmLanguageValidator.java 的类:

package org.acme.quickstart;

import java.util.Arrays;
import java.util.List;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class JvmLanguageValidator
    implements ConstraintValidator<JvmLanguage, String> { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

        private List<String> favoriteLanguages = Arrays.asList("java",                                                             "groovy", "kotlin", "scala");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return favoriteLanguages.stream()
            .anyMatch(l -> l.equalsIgnoreCase(value)); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    }
}

1

在前面步骤中定义的注释

2

验证应用的对象类型

3

检查提供的喜爱语言(value)是否是基于 JVM 的语言

最后,您需要注释 org.acme.quickstart.Developer 类中的 favoriteLanguage 字段:

@JvmLanguage
@NotBlank
private String favoriteLanguage;

通过打开一个新的终端窗口,启动 Quarkus 应用程序,并向 POST 方法发送一些请求来尝试它:

./mvnw clean compile quarkus:dev

curl -d '{"name":"Alexadra","age":7, "favoriteLanguage":"python"}' \
 -H "Content-Type: application/json"
 -X POST http://localhost:8080/developer -v

< HTTP/1.1 400 Bad Request
< Content-Length: 89
< validation-exception: true
< Content-Type: text/plain;charset=UTF-8

curl -d '{"name":"Alexandra","age":5, "favoriteLanguage":"java"}' \
 -H "Content-Type: application/json"
 -X POST http://localhost:8080/developer -v

< HTTP/1.1 200 OK
< Content-Length: 106
< Content-Type: application/json
<

[{"path":"addDeveloper.developer.favoriteLanguage",
 "message":"You need to provide a Jvm based-language"}]

讨论

在您的 REST 端点、服务方法以及最终任何 CDI 作用域对象上遵循 Bean 验证规范的任何验证将在应用运行期间自动执行。如果您需要更多控制,请参阅下一个配方,配方 5.5,以获取验证对象的额外方法。

还值得注意的是,默认情况下,约束违规消息将使用系统语言环境返回。如果您想要更改此设置,可以在 application.properties 文件中设置 quarkus.default-locale 设置:

quarkus.default-locale=es-ES

对于 REST 端点,语言环境将基于 Accept-Language HTTP 头。您可以在 application.properties 文件中指定支持的语言环境列表:

quarkus.locales=en-US, es-ES

参见

有关更多信息,请访问以下网站:

5.5 验证对象的编程方式

问题

您希望以编程方式验证对象。

解决方案

使用 Bean 验证 javax.validation.Validator 类。

在某些情况下(例如非 CDI bean 中),您希望控制何时执行验证过程。因此,提供了 javax.validation.Validator 类。

让我们创建一个端点,使用 javax.validation.Validator 验证输入,而不是使用 @Valid 注解的声明方式。打开 org.acme.quickstart.DeveloperResource.java 类并注入 Validator 实例:

@Inject
Validator validator; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

@POST
@Path("/programmaticvalidation")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response addProgrammaticValidation(Developer developer) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    Set<ConstraintViolation<Developer>> violations =
      validator.validate(developer); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

    if (violations.isEmpty()) { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
        developers.add(developer);
        return Response.ok().build();
    } else {
        JsonArrayBuilder errors = Json.createArrayBuilder();
        for (ConstraintViolation<Developer> violation : violations) { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
            errors.add(
                Json.createObjectBuilder()
                .add("path", violation.getPropertyPath().toString())
                .add("message", violation.getMessage())
                );
        }

        return Response.status(Response.Status.BAD_REQUEST)
                       .entity(errors.build())
                       .build();
    }
}

1

从 Bean 验证规范中注入Validator

2

不需要@Valid

3

以编程方式验证对象

4

如果没有错误,请继续

5

如果有错误,则构建输出

通过打开新的终端窗口,启动 Quarkus 应用程序,并发送新的 POST 方法请求来尝试它:

./mvnw clean compile quarkus:dev

curl -d '{"name":"Ada","age":7, "favoriteLanguage":"java"}' \
 -H "Content-Type: application/json" \
 -X POST http://localhost:8080/developer/programmaticvalidation -v

< HTTP/1.1 400 Bad Request
< Content-Length: 89
< validation-exception: true
< Content-Type: text/plain;charset=UTF-8

讨论

Quarkus 将自动创建javax.validation.ValidatorFactory的实例。 您可以通过创建自己的替代 bean 稍微调整这一点。 您的应用程序中以下类型的实例将自动注入到ValidatorFactory中:

  • javax.validation.ClockProvider

  • javax.validation.ConstraintValidator

  • javax.validation.ConstraintValidatorFactory

  • javax.validation.MessageInterpolator

  • javax.validation.ParameterNameProvider

  • javax.validation.TraversableResolver

  • org.hibernate.validator.spi.properties.GetterPropertySelectionStrategy

  • org.hibernate.validator.spi.scripting.ScriptEvaluatorFactory

注意

在上述列表中,您可能只有一个特定类型的实例,并且类应声明为@ApplicationScoped

5.6 注入依赖项

问题

您想将依赖项注入到您的类中。

解决方案

使用上下文和依赖注入(CDI)。

讨论

Quarkus 中的依赖注入(DI)基于上下文和依赖注入 2.0 规范,非常标准,仅需对基本用例进行少量修改。

注意

Quarkus 实现了大部分规范,除了一些不会影响您的代码的边缘情况。 Quarkus 网站维护了一个支持的特性和限制列表,包括这本书中未涵盖的更高级特性。 您可以在Quarkus CDI 参考指南中找到这些列表。

注入的方式与使用 CDI 的任何其他应用程序一样符合预期:

package org.acme.quickstart;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello")
public class GreetingResource {
    @Inject                     ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    GreetingService service;    ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return service.getGreeting();
    }

1

需要使用@Inject注解

2

由于反射的限制,优选包私有注入字段

注入的服务非常标准,没有任何意外:

package org.acme.quickstart;

import java.util.Locale;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;

@ApplicationScoped                  ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class GreetingService {
    public String getGreeting() {
        return "Hello";
    }
}

1

如下所述,您应包括一个定义 bean 的注解,允许找到类

Quarkus 中的 Bean 发现遵循从标准 CDI 简化的过程。 简而言之,如果您的应用程序类没有定义 bean 的注解,它们将不会被 Quarkus 选择。

参见

要了解更多,请参阅以下网页:

5.7 创建工厂

问题

您想为对象创建一个工厂。

解决方案

使用 CDI 中的javax.enterise.inject.Produces概念。

CDI 有一个称为producers的概念,允许您执行必要的对象创建以将新 bean 或类添加到可解析实例列表中,如下所示:

package org.acme.quickstart;

import java.util.Locale;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Named;

@ApplicationScoped
public class LocaleProducer {
    @Produces
    public Locale getDefaultLocale() {
        return Locale.getDefault();
    }
}

讨论

Quarkus 进一步推广了生产者概念。Quarkus 通过添加@io.quarkus.arc.DefaultBean注解实现这一点。在 CDI 方面,这类似于启用默认的备选项。因为 Quarkus 不允许使用备选项,所以带有DefaultBean注解的类提供了创建 bean 的默认实例的方法。以下代码示例来自 Quarkus 网站:

@Dependent
public class TracerConfiguration {

    @Produces
    public Tracer tracer(Reporter reporter, Configuration configuration) {
        return new Tracer(reporter, configuration);
    }

    @Produces
    @DefaultBean
    public Configuration configuration() {
        // create a Configuration
    }

    @Produces
    @DefaultBean
    public Reporter reporter(){
        // create a Reporter
    }
}

以下节选允许您的应用程序或库在必要时注入跟踪器。它还允许通过创建新的生产者进行自定义:

@Dependent
public class CustomTracerConfiguration {

    @Produces
    public Reporter reporter(){
        // create a custom Reporter
    }
}

将此代码添加到您的应用程序中,从CustomTracerConfiguration类创建的Reporter将用于替代默认值。

参见

要了解更多信息,请访问以下网页:

5.8 执行对象生命周期事件

问题

你想在创建/销毁对象之前和/或之后执行逻辑。

解决方案

CDI 利用@javax.annotation.PostConstruct@javax.annotation.PreDestroy注解进行生命周期管理。使用这些注解标记的方法将在PostConstruct中对象创建后和PreDestroy中对象销毁之前被调用:

package org.acme.quickstart;

import java.util.Arrays;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class RecommendationService {
    List<String> products;

    @PostConstruct
    public void init() {
        products = Arrays.asList("Orange", "Apple", "Mango");
        System.out.println("Products initialized");
    }

    @PreDestroy
    public void cleanup() {
        products = null;
        System.out.println("Products cleaned up");
    }

    public List<String> getProducts() {
        return products;
    }
}

讨论

如果需要在调用构造函数后和所有注入发生之后执行逻辑,则应将其放入使用@PostConstruct注解标记的方法中。这保证在对象实例的生命周期中只调用一次。

类似地,如果需要在对象销毁之前执行逻辑,请将其放入使用@PreDestroy注解标记的方法中。这些想法包括关闭连接、清理资源和完成日志记录。

参见

要了解更多信息,请参阅 GitHub 上的以下页面:

5.9 执行应用程序生命周期事件

问题

您希望在应用程序启动时和/或应用程序关闭后执行逻辑。

解决方案

观察io.quarkus.runtime.StartupEventio.quarkus.runtime.ShutdownEvent。在应用程序启动期间,Quarkus 将触发StartupEvent;在关闭期间,触发ShutdownEvent,如下所示:

package org.acme.quickstart;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ApplicationScoped                                  ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class ApplicationEventListener {
    private static final Logger LOGGER =
            LoggerFactory.getLogger(ApplicationEventListener.class);

    void onStart(@Observes StartupEvent event) {    ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        LOGGER.info("Application starting...");
    }

    void onStop(@Observes ShutdownEvent event) {    ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
        LOGGER.info("Application shutting down...");
    }

}

1

您必须添加一个定义 bean 的注解

2

Quarkus 触发的启动事件

3

Quarkus 触发的关闭事件

这两个事件对象都不携带任何额外信息,因此没有其他内容可覆盖。

讨论

在 Quarkus(以及其他 CDI 框架)中,事件观察是一种非常强大的方式,可以以最小的开销解耦关注点。

参见

欲了解更多,请参见 Recipe 5.8。

5.10 使用命名限定符

问题

您希望使用名称对注入进行限定。

解决方案

使用@javax.inject.Named注解。

在 CDI 中,限定符 是指任何定义为 @Retention(RUNTIME) 并且带有 @javax.inject.Qualifier 注解的注解。通常会定义限定符,以便可以在需要时随处使用它们,例如 @Target({METHOD, FIELD, PARAMETER, TYPE})

CDI 配备了一个有用的限定符:@javax.inject.Named。不需要值,但使用@Named而没有实际名称是没有意义的。在解析注入点时,CDI 会查找包含相同限定符的正确类型的任何 Bean。对于@Named,注解的值部分也必须匹配。

如果您有多个类型的实例,但它们不是相同的对象,这将非常有用。CDI 不考虑对象的实际实例,因为这只有在创建时才知道,而且每次都会有所不同。为了解决这个问题,CDI 使用限定符:

    @Inject
    @Named("en_US")
    Locale en_US;

    @Inject
    @Named("es_ES")
    Locale es_ES;

    public String getGreeting(String locale) {
        if (locale.startsWith("en"))
            return "Hello from " + en_US.getDisplayCountry();

        if (locale.startsWith("es"))
            return "Hola desde " + es_ES.getDisplayCountry();

        return "Unknown locale";
    }

讨论

为了完整起见,这是生成命名 Bean 的一种方法:

package org.acme.quickstart;

import java.util.Locale;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Named;

@ApplicationScoped
public class LocaleProducer {
    @Produces
    public Locale getDefaultLocale() {
        return Locale.getDefault();
    }
    @Produces
    @Named("en_US")
    public Locale getEnUSLocale() {
        return Locale.US;
    }

    @Produces
    @Named("es_ES")
    public Locale getEsESLocale() {
        return new Locale("es", "ES");
    }
}

@Named限定符虽然弱——这是 CDI 试图避免的事情之一——但在集成过程中可能会是一个有用的技巧。我们建议在可能的情况下使用强类型的注解。

参见

欲获得更多信息,请访问以下网页:

5.11 使用自定义限定符

问题

您希望使用其他限定符注解对注入进行限定。

解决方案

开发并使用限定符注解。

在 Recipe 5.10 中,您已经了解到一个限定符的概念:

package org.acme.quickstart;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface SpainLocale {
}

生产豆子与您所期望的完全一样:

    @Produces
    @SpainLocale
    public Locale getSpainLocale() {
        return new Locale("es", "ES");
    }

然后,当然,注入新限定的实例也同样简单:

    @Inject
    @SpainLocale
    Locale spain;

讨论

使用限定符注解是在普通 CDI 应用程序和 Quarkus 中使用限定 CDI 注入的首选方式。

参见

欲获得更多信息,请访问以下网页:

5.12 限定和配置注解

问题

您希望使用注解对依赖项进行限定和配置。

解决方案

使用生成器中的InjectionPoint与限定符注解上的非绑定属性的组合,可以对 Bean 进行限定和配置。

这是限定符和生成器的一个有趣但不寻常的用例。请查看以下代码以查看它的实际应用:

package org.acme.quickstart;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.enterprise.util.Nonbinding;
import javax.inject.Qualifier;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface Quote {
    @Nonbinding String msg() default "";        ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    @Nonbinding String source() default "";
}

1

属性被列为非绑定的,因此注入实际上是有效的。

通常,限定符的属性被认为是注入的依据,因此如果属性不匹配,将不会注入限定对象:

    @Produces
    @Quote                                                          ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    Message getQuote(InjectionPoint msg) {
        Quote q = msg.getAnnotated().getAnnotation(Quote.class);    ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        return new Message(q.msg(), q.source());                    ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    }

1

只有生产者上的默认属性

2

获取限定符的实例以从属性中提取配置

3

返回新配置的对象

使用方式与任何其他限定符完全相同:

    @Quote(msg = "Good-bye and hello, as always.", source = "Roger Zelazny")
    Message myQuote;

参见

欲了解更多信息,请访问以下网页:

5.13 创建拦截器

问题

您希望实现横切关注点。

解决方案

横切关注点 是影响程序其他关注点的一个方面。其典型示例是 事务控制。它是一种影响程序中数据使用的行为,通常必须以相同或类似的方式处理。

创建带有相应拦截器绑定的 @javax.inject.AroundInvoke@javax.inject.AroundConstruct 拦截器。您还可以创建 CDI 类型化注解,以更好地组合关注点到单个注解中。

首先,创建一个带有 @javax.interceptor.InterceptorBinding 注解的注释。这将用于将实际的拦截器代码与要拦截的任何方法或类进行关联注释:

package org.acme.quickstart;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.interceptor.InterceptorBinding;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Inherited
@InterceptorBinding
@Retention(RUNTIME)
@Target({METHOD, TYPE})
public @interface LogEvent {
}

那里没有什么特别的。接下来,您需要创建拦截器:

package org.acme.quickstart;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;

@LogEvent
@Interceptor
public class LogEventInterceptor {
    static List<Event> events = new ArrayList<>();

    @AroundInvoke
    public Object logEvent(InvocationContext ctx) throws Exception {
        events.add(new Event(ctx.getMethod().getName(),
                             Arrays.deepToString(ctx.getParameters())));
        return ctx.proceed();
    }
}

这是一个相当牵强的例子,但很容易理解正在发生的事情。最后,您只需使用绑定注解对方法或类进行注释:

@LogEvent
public void executeOrder(Order order) {
    // ...
}

每次调用 executeOrder 方法时,都会在实际调用 executeOrder 方法之前调用带有 @javax.interceptor.AroundInvoke 注解的拦截器方法,本例中为 logEvent

讨论

在 Quarkus 中使用标准的 CDI 机制非常容易实现拦截器。这提供了在应用程序中定义和利用横切动作的简单方式。

面向切面编程(AOP)已经存在很长时间了,确切地说是自 1997 年以来。由 Gregor Kiczales 领导的 Xerox PARC 团队创造并称为横切和面向切面编程。有人声称 Microsoft Transaction Server 是第一个被广泛采纳的 AOP 实例。最终,企业 JavaBeans 开发了 AOP 方面。Java 生态系统中还有 Spring 和 AspectJ。

不过,我们在谈论 CDI 和 Quarkus。Quarkus ArC(Quarkus 中的依赖注入风格)的名字是对电弧焊接的一种戏仿,使用了相同的概念。

参见

欲了解更多信息,请查看以下内容:

5.14 编写行为测试

问题

您希望编写行为测试以验证服务的正确性,而不验证其内部。

解决方案

Quarkus 的测试解决方案基于JUnit 5,这是 Java 生态系统中的事实上的测试工具,并与REST-Assured测试框架紧密集成,用于验证 RESTful Web API。

重要提示

使用 REST-Assured 并非强制要求;这只是一个建议或最佳实践,因此您可以选择任何其他您喜欢的框架来测试端点。

Quarkus 测试框架的最重要部分是一个叫做QuarkusTest的注解。当您用这个注解标记一个测试类时,您实际上是在 Quarkus 测试框架内执行该测试,这指示测试要遵循以下生命周期:

  1. Quarkus 应用程序会自动启动一次。当应用程序启动并准备好开始处理请求时,测试执行就会开始。

  2. 每个测试都针对此运行实例执行。

  3. Quarkus 应用程序已停止。

警告

为了尽量减少性能方面运行测试的影响,Quarkus 应用程序仅启动一次,然后执行测试计划中定义的所有测试类针对此运行实例执行,因此不会为每个测试类的执行重新启动应用程序。

打开位于src/test/java目录下的org.acme.quickstart.GreetingResourceTest.java类:

package org.acme.quickstart;

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 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given() ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
          .when()
          .get("/hello") ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
          .then() ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
             .statusCode(200)
             .body(is("hello"));
    }
}

1

将此测试标记为 Quarkus 测试

2

REST-Assured 的静态方法开始验证

3

使用 GET HTTP 方法发送请求到/hello路径

4

开始断言部分

您也可以从您的 IDE 中运行测试,就像在图 5-1 中展示的那样。

qucb 0501

图 5-1. 具有 Java 集成的 Visual Studio Code

或者如果您想在终端窗口中运行测试,请运行以下命令:

./mvnw clean compile test 
[INFO] ------------------------------------------------------- [INFO]  T E S T S [INFO] ------------------------------------------------------- [INFO] Running org.acme.quickstart.GreetingResourceTest
 INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation INFO  [io.qua.resteasy] (build-13) Resteasy running without servlet container. INFO  [io.qua.resteasy] (build-13) - Add quarkus-undertow to run Resteasy within a servlet container INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 803ms INFO  [io.quarkus] (main) Quarkus 1.4.1.Final started in 0.427s. Listening on: http://0.0.0.0:8081 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
 INFO  [io.quarkus] (main) Profile test activated. ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.586 s
 - in org.acme.quickstart.GreetingResourceTest 2019-11-06 13:02:43,431 INFO  [io.quarkus] (main) Quarkus stopped in 0.053s [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

1

当运行测试时,Quarkus 监听 8081 端口

2

test配置文件已激活

如前面的例子所示,测试执行时使用的默认端口是 8081。

讨论

您可以通过将quarkus.http.test-port属性设置为不同的值来更改测试使用的端口:

quarkus.http.test-port=8083

因为 Quarkus 与 REST-Assured 有良好的集成,它会自动更新所使用的端口,因此在那部分不需要额外的配置。

提示

在某些场景下,您可能希望在随机端口而不是特定端口上运行测试。Quarkus 也支持这一点;您唯一需要做的是将quarkus.http.test-port属性设置为零(0):

quarkus.http.test-port=0
./mvnw clean compile test

INFO  [io.quarkus] (main) Quarkus 1.4.1.Final started in 0.442s.
 Listening on: http://0.0.0.0:49661
INFO  [io.quarkus] (main) Profile test activated.
INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

Quarkus 支持编写行为测试,这些测试验证服务的功能性,而无需了解或验证服务的内部。图 5-2 展示了行为测试的性质。

对于 REST API 和微服务的一般情况,您可以将行为测试理解为一种发送请求到运行中的服务并验证响应是否符合预期的测试形式。

qucb 0502

图 5-2. 行为测试

如果您使用本章第一章中解释的任何方法搭建项目,则应已具备完成的行为测试,包括在构建工具脚本中注册的必需依赖项。

参见

如果您想了解 Quarkus 测试框架使用的底层技术,请访问以下网站:

5.15 编写单元测试

问题

您希望编写单元测试来验证服务内部的正确性。

解决方案

使用基于JUnit 5及其与 CDI 集成的 Quarkus 测试解决方案。

Quarkus 允许您通过@Inject注解将 CDI bean 注入到您的测试中。事实上,在 Quarkus 中,测试就是一个 CDI bean,因此在 bean 中有效的一切在测试中也有效。

让我们创建一个使用 Bean Validation 验证其输入参数的 Greeting Service bean。记得添加quarkus-hibernate-validator扩展。创建一个名为org.acme.quickstart.GreetingService.java的新类:

package org.acme.quickstart;

import javax.enterprise.context.ApplicationScoped;
import javax.validation.constraints.Min;

@ApplicationScoped ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class GreetingService {

    public String greetingMessage(@Min(value = 16) int age) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        if (age < 19) {
            return "Hey boys and girls";
        } else {
            return "Hey ladies and gentlemen";
        }
    }

}

1

将服务设置为 CDI bean

2

在方法中添加验证

现在,您希望在下面的三种情况下测试 Greeting Service 是否按预期工作:

  • 当用户年龄少于 16 岁时,抛出异常。

  • 当用户年龄在 16 到 18 岁之间时,返回青少年的消息。

  • 当用户年龄大于 18 岁时,返回成年人的消息。

我们建议您使用AssertJ项目编写可读的断言。要使用它,您需要在构建脚本中注册 AssertJ 依赖项:

<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <version>3.14.0</version>
  <scope>test</scope>
</dependency>

src/test/java目录下创建一个名为org.acme.quickstart.GreetingService.java的新类:

package org.acme.quickstart;

import javax.inject.Inject;
import javax.validation.ConstraintViolationException;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class GreetingServiceTest {

  @Inject ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
  GreetingService greetingService;

  @Test ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
  public void testGreetingServiceForYoungers() {

    Assertions.assertThatExceptionOfType(ConstraintViolationException.class) ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
      .isThrownBy(() -> greetingService.greetingMessage(15));
  }

  @Test
  public void testGreetingServiceForTeenagers() {
    String message = greetingService.greetingMessage(18);
    Assertions.assertThat(message).isEqualTo("Hey boys and girls");
  }

  @Test
  public void testGreetingServiceForAdult() {
    String message = greetingService.greetingMessage(21);
    Assertions.assertThat(message).isEqualTo("Hey female and male");
  }

}

1

将此测试设置为 Quarkus 测试

2

注入GreetingService实例

3

使用由 CDI 容器创建的 Greeting Service 实例执行测试

4

使用 AssertJ 断言

尝试通过打开新的终端窗口并运行测试来执行它:

./mvnw clean compile test

[INFO] Running org.acme.quickstart.GreetingResourceTest
 INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
 INFO  [io.qua.resteasy] (build-3) Resteasy running without servlet container.
 INFO  [io.qua.resteasy] (build-3) - Add quarkus-undertow to run Resteasy
 within a servlet container
 INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed
 in 813ms
 INFO  [io.quarkus] (main) Quarkus 1.4.1.Final started in 0.715s.
 Listening on: http://0.0.0.0:51581
 INFO  [io.quarkus] (main) Profile test activated.
 INFO  [io.quarkus] (main) Installed features:
 [cdi, hibernate-validator, resteasy]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.614 s
 - in org.acme.quickstart.GreetingResourceTest
[INFO] Running org.acme.quickstart.GreetingServiceTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.086 s
 - in org.acme.quickstart.GreetingServiceTest
2019-11-06 16:16:11,503 INFO  [io.quarkus] (main) Quarkus stopped in 0.029s

请注意,Quarkus 应用程序启动一次,但两个测试类都会执行。

讨论

在 5.14 节中,您学习了如何使用行为方法编写测试,其中您只关心服务的请求和响应。然而,往往情况是您想要验证服务内部发生了什么,或者您想要验证某些部分在运行实例中的行为,而不必模拟环境。当您想要验证业务对象是否按预期工作时(包括与 Quarkus 提供的功能的集成,如 Bean Validation、CDI 等),通常需要这样做。

参见

要了解更多关于 AssertJ 的信息,请访问以下网页:

5.16 创建模拟对象

问题

您想要测试需要额外处理时间或需要与外部系统通信的类。

解决方案

使用 Quarkus 中的模拟支持提供模拟真实对象行为的 CDI 对象,以替换默认对象。

模拟对象是通过为方法调用提供某些固定答案来模拟真实对象行为的模拟对象。

让我们模拟 5.15 节中创建的问候服务。

src/test/java 目录下创建新的类 org.acme.quickstart.MockedGreetingService.java

package org.acme.quickstart;

import io.quarkus.test.Mock;

@Mock ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class MockedGreetingService
    extends GreetingService { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

        @Override
        public String greetingMessage(int age) {
            return "Hello World"; ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
        }

}

1

将 POJO 标记为 CDI 中的模拟类(替代类)

2

类必须扩展或实现基础服务

3

固定答案

讨论

创建模拟对象不仅是绕过外部服务和运行时间较长的进程的好方法,而且还是测试特定场景的简单方法。在前面的解决方案中,可能有两个测试:一个使用模拟对象,另一个使用实际对象。一个将通过服务展示预期的行为,另一个可能展示预期的失败。此技术对于测试外部服务失败特别有用。

5.17 使用 Mockito 创建模拟对象

问题

使用 Mockito,您想要测试需要额外处理时间或需要与外部系统通信的类。

解决方案

使用 Mockito 库提供模拟真实对象行为的 CDI 对象,以替换默认对象。

使用 Mockito,让我们模拟 5.15 节中创建的问候服务。

第一件事是添加 Quarkus Mockito 扩展:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-junit5-mockito</artifactId>
  <scope>test</scope>
</dependency>

src/main/java 目录下创建新的类 org.acme.quickstart.GreetingResourceTest.java

import io.quarkus.test.junit.mockito.InjectMock;
import org.junit.jupiter.api.BeforeEach;
import static org.mockito.Mockito.when;

@QuarkusTest
public class GreetingResourceTest {

    @InjectMock ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    GreetingService greetingService;

    @BeforeEach ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    public void prepareMocks() {
        when(greetingService.message())
                .thenReturn("Aloha from Mockito");
    }

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/greeting")
          .then()
             .statusCode(200)
             .body(is("Aloha from Mockito")); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    }

}

1

InjectMock 使此字段成为 Mockito 模拟

2

在每次测试执行之前,都会记录模拟期望

3

返回的消息是模拟的消息

5.18 将多个注解分组成一个注解

使用元注释

问题

您希望避免在应用程序中进行注解的填充。

解决方案

使用元注解将多个注解分组成一个。

您可以开发一个包含测试或应用程序其他部分所需的所有注解的元注解。例如,您可以创建一个TransactionalQuarkusTest注解,其中包含@QuarkusTest@Transactional注解,如果使用此新创建的注解,则默认情况下使 Quarkus 测试具有事务性。

src/test/java目录下创建一个名为org.acme.quickstart.TransactionalQuarkusTest.java的新类:

package org.acme.quickstart;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.enterprise.inject.Stereotype;
import javax.transaction.Transactional;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@Transactional
@Stereotype ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TransactionalQuarkusTest {
}

1

添加此元注解可能“继承”的注解

2

将注解设置为元注解(元注解)

如果然后将此注解应用于类,则会像应用了@QuarkusTest@Transactional注解一样:

@TransactionalQuarkusTest
public class DeveloperDAO {
}

注意现在测试更易读且注解可重复使用。

讨论

Quarkus 测试是 CDI bean,因此您可以应用 CDI 拦截器。例如,您可以使用事务拦截器使测试变得具有事务性。通过使用事务注释测试类,可以启用此拦截器@javax.transaction.Transactional。因此,事务性测试可能如下所示:

@QuarkusTest
@Transactional
public class DeveloperDAO {
}

当然,这是完全有效的,但有两种情况下,类上的多个注解可能会影响您的测试的可读性:

  1. 您的测试需要更多注解,例如,JUnit 5 注解(如@TestMethodOrder)用于定义测试的执行顺序,或者您需要为测试启用其他拦截器。在这些情况下,您可能会设置比代码更多的注解。

  2. 您有很多测试需要相同的注解,因此在大多数情况下,您会连续注释所有测试具有相同的注解。

5.19 在测试前或后执行代码

问题

在测试套件开始/结束前/后执行一些逻辑,以启动/停止/配置测试所需的资源。

解决方案

使用Quarkus Test Resource扩展机制来定义执行测试所需的资源。

Quarkus 提供了一种扩展机制,允许您在测试套件启动之前和测试套件完成之后执行 Java 代码。此外,它允许您以编程方式创建/覆盖配置属性,以便可以在测试资源类中设置测试资源所需的任何参数,而无需修改application.properties文件。

让我们编写一个简单的 Quarkus 测试资源,只需打印一些消息。

src/test/java目录下创建一个实现io.quarkus.test.common.QuarkusTestResourceLifecycleManager接口的名为org.acme.quickstart.HelloWorldQuarkusTestResourceLifecycleManager的新类:

package org.acme.quickstart;

import java.util.Collections;
import java.util.Map;

import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

public class HelloWorldQuarkusTestResourceLifecycleManager
    implements QuarkusTestResourceLifecycleManager { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

    @Override
    public Map<String, String> start() { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        System.out.println("Start Test Suite execution");
        return Collections.emptyMap(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    }

    @Override
    public void stop() { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
        System.out.println("Stop Test Suite execution");
    }

    @Override
    public void inject(Object testInstance) { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
        System.out.println("Executing " + testInstance.getClass().getName());
    }

    @Override
    public int order() { ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
        return 0;
    }

}

1

必须实现QuarkusTestResourceLifecycleManager

2

测试套件执行之前执行的方法

3

Map 对象用于系统属性的使用

4

测试套件执行之后执行的方法

5

对于每个测试类的执行,会调用此方法,并传递测试实例,以便注入特定字段

6

设置执行顺序,以防定义了多个资源

最后,您需要注册此扩展以在测试套件执行期间执行。为此,您需要在 src/test/java 目录下的任何类中使用 QuarkusTestResource 注解,并设置要启动的测试资源。虽然任何负责注册资源的测试类都可以,但我们建议您创建一个专门的空类来注册测试资源。

src/test/java 中创建一个新的类 org.acme.quickstart.HelloWorldTestResource,内容如下:

package org.acme.quickstart;

import io.quarkus.test.common.QuarkusTestResource;

@QuarkusTestResource(HelloWorldQuarkusTestResourceLifecycleManager.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class HelloWorldTestResource {
}

1

注册测试资源

然后在终端中运行测试,您会看到类似终端输出:

./mvnw clean compile test 
INFO] ------------------------------------------------------- [INFO]  T E S T S [INFO] ------------------------------------------------------- [INFO] Running org.acme.quickstart.GreetingResourceTest Start Test Suite execution ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
 INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 756ms INFO  [io.quarkus] (main) Quarkus 1.4.1.Final started in 0.381s. Listening on: http://0.0.0.0:8081 INFO  [io.quarkus] (main) Profile test activated. INFO  [io.quarkus] (main) Installed features: [cdi, resteasy] Executing org.acme.quickstart.GreetingResourceTest ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.058 s
 - in org.acme.quickstart.GreetingResourceTest Stop Test Suite execution ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
2019-11-08 16:57:01,020 INFO  [io.quarkus] (main) Quarkus stopped in 0.027s

1

在 Quarkus 启动前调用 start 方法

2

在运行 GreetingResourceTest 之前调用 inject 方法

3

在所有测试执行完毕后调用 stop 方法

此示例并不是非常实用;然而,它简单地解释了测试生命周期,但没有更多内容。

讨论

随着您继续前进,您的测试复杂性(集成测试、端到端测试等)及其运行所需的依赖项增加。例如,测试可能需要数据库实例、Kafka 代理、JMS 队列或类似 Keycloak 的身份提供者。

有了这个背景,让我们编写一个更有趣的测试资源,它使用 Docker 启动 MariaDB Docker 容器。

对于此示例,您将使用 Testcontainers 测试框架;在撰写本文时,最新版本为 1.14.3。

在运行测试之前,您需要在您的机器上安装 Docker,以便 Testcontainers 可以在本地启动 MariaDB 容器。

开发 Testcontainers Quarkus 测试资源的第一步是在构建工具中注册 Testcontainers 依赖项,以便在您的测试中使用 MariaDB Docker 容器:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>mariadb</artifactId>
  <version>${testcontainers.version}</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jdbc-mariadb</artifactId>
</dependency>

QuarkusTestResourceLifecycleManager 的实现如下所示:

package org.acme.quickstart;

import java.util.HashMap;
import java.util.Map;

import org.acme.quickstart.MariaDbTestResource.Initializer;
import org.testcontainers.containers.MariaDBContainer;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

@QuarkusTestResource(Initializer.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class MariaDbTestResource {

  public static class Initializer
      implements QuarkusTestResourceLifecycleManager { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

    private MariaDBContainer mariaDBContainer; ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

    @Override
    public Map<String, String> start() {

      this.mariaDBContainer = new MariaDBContainer<>("mariadb:10.4.4"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
      this.mariaDBContainer.start();![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)

      return getConfigurationParameters();
    }

    private Map<String, String> getConfigurationParameters() { ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
      final Map<String, String> conf = new HashMap<>();

      conf.put("quarkus.datasource.url", this.mariaDBContainer.getJdbcUrl());
      conf.put("quarkus.datsource.username", this.mariaDBContainer
                                                  .getUsername());
      conf.put("quarkus.datasource.password", this.mariaDBContainer
                                                    .getPassword());
      conf.put("quarkus.datasource.driver", this.mariaDBContainer
                                                  .getDriverClassName());

      return conf;
    }

    @Override
    public void stop() {
      if (this.mariaDBContainer != null) {
        this.mariaDBContainer.close(); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/7.png)
      }
    }
  }
}

1

注册测试资源

2

定义测试资源接口

3

设置 MariaDB 容器对象

4

使用所需的 Docker 镜像实例化 MariaDB 容器

5

启动容器并等待容器接受连接

6

覆盖 Quarkus 的配置,将数据库连接指向容器中的连接

7

停止容器

最后,在终端中运行测试。您将在终端输出中看到类似以下内容:

./mvnw clean test 
[INFO] ------------------------------------------------------- [INFO]  T E S T S [INFO] ------------------------------------------------------- [INFO] Running org.acme.quickstart.GreetingResourceTest
 ℹ︎ Checking the system... ✔ Docker version should be at least 1.6.0 ✔ Docker environment should have more than 2GB free disk space Start Test Suite execution
 INFO  [org.tes.doc.DockerClientProviderStrategy] (main) Loaded org.testcontainers.dockerclient .UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first ...
 INFO  [ߐ㠮4.4]] (main) Creating container for image: mariadb:10.4.4 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
 INFO  [ߐ㠮4.4]] (main) Starting container with ID: 0d07d45111b1103fd7e64ac2050320ee329ca14eb46a72d525f61bc5e433dc69 INFO  [ߐ㠮4.4]] (main) Container mariadb:10.4.4 is starting: 0d07d45111b1103fd7e64ac2050320ee329ca14eb46a72d525f61bc5e433dc69 INFO  [ߐ㠮4.4]] (main) Waiting for database connection to become available at jdbc:mariadb://localhost:32773/test using query 'SELECT 1' INFO  [ߐ㠮4.4]] (main) Container is started (JDBC URL: jdbc:mariadb://localhost:32773/test) INFO  [ߐ㠮4.4]] (main) Container mariadb:10.4.4 started ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
 INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 1461ms INFO  [io.quarkus] (main) Quarkus 1.4.1.Final started in 0.909s. Listening on: http://0.0.0.0:8081 INFO  [io.quarkus] (main) Profile test activated. INFO  [io.quarkus] (main) Installed features: [cdi, jdbc-mariadb, resteasy] Executing org.acme.quickstart.GreetingResourceTest [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 32.666 s
 - in org.acme.quickstart.GreetingResourceTest Stop Test Suite execution 2019-11-12 11:57:27,758 INFO  [io.quarkus] (main) Quarkus stopped in 0.043s

1

创建 MariaDB Docker 容器

2

容器已启动并准备好接收传入请求

提示

您可以在单独的项目中开发 Quarkus 测试资源,并将它们打包为外部 JAR 库。然后可以在任意数量的项目中重用它们。

Quarkus 提供以下默认的 Quarkus 测试资源实现:

H2DatabaseTestResource

用于启动/停止服务器模式下的 H2 数据库

DerbyDatabaseTestResource

用于启动/停止服务器模式下的 Derby 数据库

InfinispanEmbeddedTestResource

用于启动/停止嵌入式模式下的 Infinispan

InfinispanServerTestResource

用于启动/停止服务器模式下的 Infinispan

KeycloakTestResource

用于启动/停止 Keycloak 身份提供者

ArtemisTestResource

用于启动/停止嵌入式 ActiveMQ

KafkaTestResource

用于使用 Debezium 类启动/停止 Kafka 集群

KubernetesMockServerTestResource

用于启动/停止 Kubernetes Mock 服务器

参见

您可以在以下位置了解如何在本地机器上安装 Docker,并找到有关 Testcontainers 测试框架的更多示例:

5.20 测试本地可执行文件

问题

您希望测试本地可执行文件是否正确。

解决方案

使用NativeImageTest注解从本地文件而非 JVM 启动应用程序。

如果计划生成应用程序的本地可执行文件,则编写一些针对在本地可执行文件中运行的应用程序的行为测试始终是一个好主意。

Quarkus 提供NativeImageTest注解,从本地文件而非 JVM 启动应用程序。需要注意,在运行测试或使用quarkus.package.type系统属性生成本地可执行文件之前,必须先生成本地可执行文件。可以在 Recipe 6.4 中了解如何生成本地可执行文件。

如果项目是使用前述任何方法脚手架生成的,则已提供本地可执行文件测试。

警告

不可能在同一测试套件中混合 JVM 和本地镜像测试。JVM 测试必须在与本地测试不同的周期中运行(例如,在 Maven 中,这将是surefire用于 JVM 测试和failsafe用于本地测试)。

当项目通过前述任何方法搭建时,默认提供此配置:

<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> ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
            </goals>
            <configuration>
              <systemProperties>
                <native.image.path> ${project.build.directory}/
                  ${project.build.finalName}-runner </native.image.path> ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
              </systemProperties>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <properties>
    <quarkus.package.type> native </quarkus.package.type> ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
  </properties>
</profile>

1

本地测试在verify目标(./mvnw verify)中运行

2

生成的本地可执行文件的位置(该行不应在 pom 文件中拆分)

3

在运行测试之前生成一个本地可执行文件

打开位于src/test/java目录下的org.acme.quickstart.NativeGreetingResourceIT.java类:

package org.acme.quickstart;

import io.quarkus.test.junit.NativeImageTest;

@NativeImageTest ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class NativeGreetingResourceIT
    extends GreetingResourceTest { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

    // Execute the same tests but in native mode. }

1

启动位于native.image.path的本地可执行文件。

2

扩展 JVM 测试以使其针对本地可执行文件运行。这不是强制性的;您可以编写您的测试,但请记得用@QuarkusTest进行注解。这将在本地可执行文件上运行相同的测试。

警告

本节中展示的所有测试功能都是有效的,除了在测试中进行注入。

讨论

在编写本地测试时需要考虑一些事项:

  1. Quarkus 等待本地镜像启动的时间为 60 秒,然后自动失败本地测试。可以使用quarkus.test.native-image-wait-time属性进行更改(例如,./mvnw verify -Pnative -Dquarkus.test.native-image-wait-time=200)。

  2. 本地测试在prod配置文件中运行;如果要更改,可以使用quarkus.test.native-image-profile属性来设置替代配置文件。

  3. 可以通过使用io.quarkus.test.junit.DisabledOnNativeImage注解来禁用某些测试方法(或类),以便在本地测试中运行它们(例如,@DisabledOnNativeImage @Test public void n⁠o⁠n⁠N⁠a⁠t⁠i⁠v⁠e⁠T⁠e⁠s⁠t⁠(⁠)⁠{⁠})。

第六章:打包 Quarkus 应用程序

在本章中,您将学习如何将 Quarkus 服务打包成 JVM 或本地格式,以便分发和部署。如今,随着容器成为分发应用程序的标准方式,您需要了解如何将其容器化。

我们将讨论以下主题:

  • 如何将 Quarkus 应用程序打包以在 JVM 中运行

  • 如何将 Quarkus 应用打包成本地可执行文件

  • 如何将 Quarkus 应用程序容器化

6.1 在命令模式下运行

问题

您想创建一个 CLI 应用程序。

解决方案

使用 Quarkus,您还可以编写运行后可选择退出的应用程序。

要在 Quarkus 中启用命令模式,您需要创建一个实现 io.quarkus.runtime.QuarkusApplication 接口的类:

package org.acme.quickstart;

import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.QuarkusApplication;

public class GreetingMain implements QuarkusApplication { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

    @Override
    public int run(String... args) throws Exception { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        System.out.println("Hello World");
        Quarkus.waitForExit(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
        return 0;
    }

}

1

设置 Quarkus 以命令模式运行的接口

2

当调用 main 方法时执行的方法

3

不要退出,而是等待 Quarkus 进程停止

然后,您可以实现众所周知的 Java main 方法。其中一个要求是带有 main 方法的类必须用 @io.quarkus.runtime.annotations.QuarkusMain 注解进行注释:

package org.acme.quickstart;

import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.annotations.QuarkusMain;

@QuarkusMain ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class JavaMain {

    public static void main(String... args) {
        Quarkus.run(GreetingMain.class, args); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    }

}

1

将类设置为 main

2

启动进程

如果你想访问命令行参数,可以使用@io.quarkus.runtime.annotations.CommandLineArguments注解来注入它们:

package org.acme.quickstart;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.runtime.annotations.CommandLineArguments;

@Path("/hello")
public class GreetingResource {

    @CommandLineArguments ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    String[] args;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return args[0];
    }
}

1

注入命令行参数

最后,您可以构建项目并运行它:

./mvnw clean package -DskipTests

java -jar target/greeting-started-cli-1.0-SNAPSHOT-runner.jar Aloha

curl localhost:8080/hello
Aloha

讨论

可以使用两种不同的方法来实现退出的应用程序。我们在前一节中解释了第一种方法;第二种方法是通过将实现 io.quarkus.runtime.QuarkusApplication 接口的类用 @io.quarkus.runtime.annotations.QuarkusMain 注解进行注释。

第二种解决方案的缺点是无法从 IDE 中运行,这也是我们建议您使用前一种方法的原因。

正如您在示例中看到的那样,如果您希望在启动时运行一些逻辑,然后像普通应用程序一样运行它(即不退出),则应从主线程调用 Quarkus.waitForExit 方法。如果您不调用此方法,则 Quarkus 应用程序将启动然后终止,因此您的应用程序实际上会像任何其他 CLI 程序一样运行。

6.2 创建一个可运行的 JAR 文件

问题

您想创建一个可分发/容器化到安装了 JVM 的机器上的可运行 JAR 文件。

解决方案

使用Quarkus Maven 插件创建可运行的 JAR 包。

如果您使用之前提到的任何启动器生成了项目,Quarkus Maven 插件将会默认安装:

<plugin>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-maven-plugin</artifactId>
  <version>${quarkus.version}</version>
  <executions>
    <execution>
      <goals>
        <goal>build</goal>
      </goals>
    </execution>
  </executions>
</plugin>

然后运行 package 目标以构建 JAR 文件:

./mvnw clean package

target 目录包含以下内容:

target ├── classes ├── generated-sources ├── generated-test-sources ├── getting-started-1.0-SNAPSHOT-runner.jar ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
├── getting-started-1.0-SNAPSHOT.jar ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
├── lib ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
├── maven-archiver ├── maven-status ├── test-classes ├── transformed-classes └── wiring-classes

1

一个可执行 JAR(而不是超级 JAR)

2

依赖项的位置

3

应用依赖的 Lib 文件夹

如果要部署应用程序,重要的是将 可执行 JARlib 目录一起复制。

您可以通过运行以下命令来运行应用程序:

java -jar target/getting-started-1.0-SNAPSHOT-runner.jar

以 JVM 模式运行 Quarkus 被称为在 JVM 模式 下运行 Quarkus。这意味着您没有进行本地编译,而是在 JVM 内运行应用程序。

提示

如果您想将 JVM 模式下的 Quarkus 应用程序打包到容器中,我们建议使用这种方法,因为在容器构建阶段创建的层将被缓存以供将来重复使用。库通常不会改变,因此这个依赖层可能在未来的执行中被多次重用,加快容器构建时间。

讨论

使用 Gradle 创建可运行的 JAR 文件,您可以运行 quarkusBuild 任务:

./gradlew quarkusBuild

参见

如果您想了解如何创建超级 JAR 或如何将 Quarkus 应用程序容器化,请参阅 Recipe 6.3。

6.3 超级 JAR 打包

问题

您希望创建一个 Quarkus 应用程序的超级 JAR。

解决方案

Quarkus Maven 插件通过在 pom.xml 中设置 uberJar 配置选项来支持生成超级 JAR:

要创建一个包含您的代码 可运行类 和所有必需依赖项的超级 JAR,您需要在 application.properties 文件中相应地配置 Quarkus,将 quarkus.package.uber-jar 设置为 true

quarkus.package.uber-jar=true

6.4 构建本地可执行文件

问题

您希望将 Quarkus 应用程序构建为本地可执行文件。

解决方案

使用 Quarkus 和 GraalVM 构建适用于容器和无服务器负载的本地可运行文件。

本地可执行文件使 Quarkus 应用程序非常适合于容器和无服务器工作负载。Quarkus 依赖于 GraalVM 来将 Java 应用程序构建为本地可执行文件。

在构建原生可执行文件之前,请确保设置了 GRAALVM_HOME 环境变量,指向 GraalVM 19.3.1 或 20.0.0 的安装目录。

重要提示

如果您使用 macOS,该变量应指向 Home 子目录:export GRAALVM_HOME=<installation_dir>/Development/graalvm/Contents/Home/

当使用之前解释过的任何方法生成 Quarkus 项目时,它会注册一个名为 native 的默认 Maven 配置文件,可用于构建 Quarkus 本地可执行应用程序:

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

然后需要使用启用了 native 配置文件的项目构建:

./mvnw package -Pnative

几分钟后,target 目录中将出现一个本地可执行文件:

[INFO] --- quarkus-maven-plugin:1.4.1.Final:native-image (default) @
 getting-started ---
[INFO] [io.quarkus.creator.phase.nativeimage.NativeImagePhase] Running Quarkus
 native-image plugin on Java HotSpot(TM) 64-Bit Server VM
...
[getting-started-1.0-SNAPSHOT-runner:19]    classlist:  13,614.07 ms
[getting-started-1.0-SNAPSHOT-runner:19]        (cap):   2,306.78 ms
[getting-started-1.0-SNAPSHOT-runner:19]        setup:   4,793.43 ms
...
Printing list of used packages to
 /project/reports/used_packages_getting-started-1.0-SNAPSHOT-
 runner_20190927_134032.txt
[getting-started-1.0-SNAPSHOT-runner:19]    (compile):  42,452.12 ms
[getting-started-1.0-SNAPSHOT-runner:19]      compile:  62,356.07 ms
[getting-started-1.0-SNAPSHOT-runner:19]        image:   2,939.16 ms
[getting-started-1.0-SNAPSHOT-runner:19]        write:     696.65 ms
[getting-started-1.0-SNAPSHOT-runner:19]      [total]: 151,743.29 ms

target/getting-started-1.0-SNAPSHOT-runner

讨论

要在 Gradle 中构建本机可执行文件,可以使用 buildNative 任务:

./gradlew buildNative

6.5 为 JAR 文件构建 Docker 容器

问题

您希望构建一个包含在 Recipe 6.2 配方中构建的 JAR 的容器。

解决方案

使用提供的 Dockerfile.jvm 文件构建容器。

当使用之前解释的任何方法生成 Quarkus 项目时,会在 src/main/docker 中创建两个 Dockerfile:一个用于使用 JVM 模式下的 Quarkus 生成 Docker 容器,另一个用于本机可执行文件。

要生成一个用于在 JVM 中运行 Quarkus 的容器(非本机),您可以使用 Dockerfile.jvm 文件来构建容器。此 Dockerfile 添加了 lib 目录和可运行的 JAR 并暴露了 JMX。

要构建 Docker 镜像,您需要按照 Recipe 6.2 中所示的步骤对项目进行打包,然后构建容器:

./mvnw clean package

docker build -f src/main/docker/Dockerfile.jvm -t example/greetings-app .

容器可以通过运行以下命令启动:

docker run -it --rm -p 8080:8080 example/greetings-app

6.6 为本机文件构建 Docker 容器

问题

您希望构建一个本机可执行文件容器映像。

解决方案

要生成用于运行 Quarkus 本机可执行文件的容器,可以使用 Dockerfile.native 文件来构建容器。

要构建 Docker 镜像,您需要创建一个可以在 Docker 容器中运行的本机文件。因此,请勿使用本地 GraalVM 来构建本机可执行文件,因为结果文件将针对您的操作系统进行特定的处理,将无法在容器中运行。

要在终端中使用以下命令创建可以在容器中运行的可执行文件:

./mvnw clean package -Pnative -Dquarkus.native.container-build=true

此命令创建一个包含 GraalVM 安装的 Docker 镜像,用于从您的代码生成 64 位 Linux 可执行文件。

重要

您需要在 pom.xml 中定义 native 配置文件,如 Recipe 6.4 配方所述。

最后一步是使用在上一步中生成的本机可执行文件构建 Docker 镜像:

docker build -f src/main/docker/Dockerfile.native -t example/greetings-app .

然后可以通过运行以下命令来启动容器:

docker run -it --rm -p 8080:8080 example/greetings-app

讨论

默认情况下,Quarkus 使用 docker 来构建容器。可以使用 quarkus.native.container-runtime 属性更改容器运行时。在编写本书时,dockerpodman 是支持的选项:

./mvnw package -Pnative -Dquarkus.native.container-build=true \
               -Dquarkus.native.container-runtime=podman

6.7 构建和 Docker 化本机 SSL 应用程序

问题

在构建本机可执行文件时,您希望保护连接,以防止攻击者窃取敏感信息。

解决方案

在本机可执行文件中启用 Quarkus 使用 SSL 来保护连接。

如果你在 JVM 模式下运行 Quarkus 应用程序,则 SSL 支持没有任何问题,就像任何其他 JVM 应用程序一样。但是在本机可执行文件的情况下,SSL 并不是默认支持的,必须执行一些额外的步骤(特别是在 Docker 化应用程序时)来启用 SSL 支持。

通过在 application.properties 中添加 quarkus.ssl.native 配置属性来启用 Quarkus 在本机可执行文件中使用 SSL 保护连接:

quarkus.ssl.native=true

启用此属性允许 Graal VM 的native-image过程启用 SSL。使用以下命令创建本地可执行文件:

./mvnw clean package -Pnative -DskipTests -Dquarkus.native.container-build=true

...

docker run -v \
 gretting-started/target/gretting-started-1.0-SNAPSHOT-native-image-source-jar: \
 /project:z

下面是由进程自动添加以启用 SSL 的重要标志:

-H:EnableURLProtocols=http,https --enable-all-security-services -H:+JNI

要将此本地可执行文件 Docker 化,需要稍微修改与 SSL 支持相关的 Docker 脚本。

打开.dockeringnore并将keystore.jks文件添加为不可忽略的文件,以便将其添加到生成的容器中。这是必要的,因为 keystore 文件需要与可执行文件一起复制:

*
!target/classes/keystore.jks
!target/*-runner
!target/*-runner.jar
!target/lib/*

src/main/docker/Dockerfile.native文件还必须适应以下元素的打包:

  • SunEC 库

  • 收集用于验证应用程序中使用的证书的受信任证书颁发机构文件集合。

FROM quay.io/quarkus/ubi-quarkus-native-image:19.2.1 as nativebuilder ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
FROM quay.io/quarkus/ubi-quarkus-native-image:19.3.1-java11 as nativebuilder
RUN mkdir -p /tmp/ssl \
  && cp /opt/graalvm/lib/security/cacerts /tmp/ssl/

FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY --from=nativebuilder /tmp/ssl/ /work/ ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
COPY target/*-runner /work/application
RUN chmod 775 /work /work/application \ ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
  && chown -R 1001 /work \
  && chmod -R "g+rwX" /work \
  && chown -R 1001:root /work
EXPOSE 8080 8443 ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
USER 1001
CMD ["./application",
"-Dquarkus.http.host=0.0.0.0",
"-Djavax.net.ssl.trustStore=/work/cacerts"] ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)

1

从 GraalVM Docker 镜像获取 SunEC 库和cacerts

2

将自定义的keystore.jks复制到根工作目录

3

设置权限

4

暴露 HTTPS 端口

5

在运行应用程序时加载 SunEC 和cacerts

可通过运行以下命令构建容器镜像:

docker build -f src/main/docker/Dockerfile.native -t greeting-ssl .

讨论

安全和 SSL 现在很常见,始终使所有服务使用 SSL 进行通信是一个良好的做法。因此,当注册以下任何扩展时,Quarkus 会自动启用 SSL 支持:

  • Agroal 连接池

  • Amazon DynamoDB

  • Hibernate Search Elasticsearch

  • Infinispan 客户端

  • Jaeger

  • JGit

  • Keycloak

  • Kubernetes 客户端

  • Mailer

  • MongoDB

  • Neo4j

  • OAuth2

  • REST 客户端

只要在项目中有其中一个扩展,quarkus.native.ssl属性默认设置为true

第七章:持久化

Quarkus 使用的底层持久化策略应该已经很熟悉了。事务、数据源、Java Persistence API(JPA)等都是存在多年的标准。Quarkus 使用这些标准,并在某些情况下在其基础上进行扩展,以便更轻松地处理持久化存储。在本章中,您将学习如何在 Quarkus 中处理持久化存储。我们涵盖传统的关系型数据库管理系统(RDBMS)和 NoSQL 数据库。

如果您使用传统的关系型数据库管理系统或 MongoDB,则 Quarkus 通过 Panache 提供了一些额外的宝贝,Panache 是一种基于实体或活动记录的 API,具有明确的观点。Panache 简化了大部分标准 JPA 语法,使您的应用程序更易于阅读和维护,从而帮助您提高生产力!

在本章中,您将学习如何完成以下任务:

  • 配置数据源

  • 处理事务

  • 管理数据库模式迁移

  • 利用 Panache API

  • 与 NoSQL 数据存储交互

7.1 定义数据源

问题

您希望定义和使用数据源。

解决方案

使用 Agroal 扩展和 application.properties

讨论

Agroal 是 Quarkus 中首选的数据源和连接池实现。Agroal 扩展与安全性、事务管理和健康指标集成。虽然它有自己的扩展,但如果您使用 Hibernate ORM 或 Panache,则 Agroal 扩展会通过传递方式引入。您还需要一个数据库驱动扩展。目前,H2、PostgreSQL、MariaDB、MySQL、Microsoft SQL Server 和 Derby 都有支持的扩展。您可以使用 Maven 的 add-extension 添加正确的数据库驱动:

./mvnw quarkus:add-extension -Dextensions="jdbc-mariadb"

数据源的配置,就像 Quarkus 的所有其他配置一样,在 src/main/resources/application.properties 文件中完成:

quarkus.datasource.url=jdbc::mariadb://localhost:3306/test
quarkus.datasource.driver=org.mariadb.jdbc.Driver
quarkus.datasource.username=username-default
quarkus.datasource.min-size=3
quarkus.datasource.max-size=13
提示

敏感数据可以通过系统属性、环境属性、Kubernetes Secrets 或 Vault 传递,稍后章节将详细介绍。

如果您需要访问数据源,可以按以下方式注入它:

@Inject
DataSource defaultDataSource;
注意

您还可以使用 AgroalDataSource 类型,它是 DataSource 的子类型。

7.2 使用多个数据源

问题

当您的应用程序需要多个数据源时,您可以使用多个数据源。

解决方案

使用命名数据源。

Agroal 允许多个数据源。它们的配置与默认配置完全相同,只有一个显著的例外——名称:

quarkus.datasource.driver=org.h2.Driver
quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:default
quarkus.datasource.username=username-default
quarkus.datasource.min-size=3
quarkus.datasource.max-size=13

quarkus.datasource.users.driver=org.h2.Driver
quarkus.datasource.users.url=jdbc:h2:tcp://localhost/mem:users
quarkus.datasource.users.username=username1
quarkus.datasource.users.min-size=1
quarkus.datasource.users.max-size=11

quarkus.datasource.inventory.driver=org.h2.Driver
quarkus.datasource.inventory.url=jdbc:h2:tcp://localhost/mem:inventory
quarkus.datasource.inventory.username=username2
quarkus.datasource.inventory.min-size=2
quarkus.datasource.inventory.max-size=12

格式如下:quarkus.*datasource*.[*optional name*.][*datasource property*]

讨论

注入的工作方式相同;但是,您需要一个限定符(请参阅 Recipe 5.10 获取有关限定符的更多信息):

@Inject
AgroalDataSource defaultDataSource;

@Inject
@DataSource("users")
AgroalDataSource dataSource1;

@Inject
@DataSource("inventory")
AgroalDataSource dataSource2;

7.3 添加数据源健康检查

问题

您希望为数据源添加健康检查条目。

解决方案

同时使用 quarkus-agroalquarkus-smallrye-health 扩展。

讨论

当使用quarkus-smallrye-health扩展时,数据源的健康检查会自动添加。如果需要,可以通过application.properties中的quarkus.datasource.health.enabled属性(默认为true)禁用。要查看状态,请访问应用程序的/health/ready端点。该端点是从quarkus-smallrye-health扩展创建的。

参见

欲了解更多信息,请访问 GitHub 上的以下页面:

7.4 声明事务边界

问题

您希望使用注解定义事务边界。

解决方案

使用quarkus-narayana-jta扩展中的@javax.transaction.Transactional注解。

讨论

quarkus-narayana-jta扩展添加了@javax.transaction.Transactional注解,以及TransactionManagerUserTransaction类。该扩展会自动添加到任何持久性扩展中。当然,如果需要,也可以手动添加。

@Transactional注解可以添加到任何 CDI bean 的方法或类级别,使这些方法具有事务性,这也包括 REST 端点:

package org.acme.transaction;

import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/tx")
@Transactional
public class Transact {
}

7.5 设置事务上下文

问题

您需要一个不同的事务上下文。

解决方案

@Transactionalvalue属性允许设置事务的范围。

讨论

指定的事务上下文将传播到已注释的方法内的所有嵌套调用。除非在堆栈中抛出运行时异常,否则事务将在方法调用结束时提交:

package org.acme.transaction;

import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/tx")
@Transactional
public class Transact {
    @Inject
    NoTransact noTx;

    @GET
    @Path("/no")
    @Produces(MediaType.TEXT_PLAIN)
    public String hi() {
        return noTx.word();
    }
}
package org.acme.transaction;

import javax.enterprise.context.ApplicationScoped;
import javax.transaction.Transactional;

import static javax.transaction.Transactional.TxType.NEVER;

@ApplicationScoped
public class NoTransact {
    @Transactional(NEVER)
    public String word() {
        return "Hi";
    }
}

这可以通过@⁠T⁠r⁠a⁠n⁠s​a⁠c⁠t⁠i⁠o⁠n⁠a⁠ldontRollbackOnrollbackOn属性进行重写。如果需要手动回滚事务,还可以注入TransactionManager

这是可用事务上下文的列表:

@Transactional(REQUIRED)(默认)

如果没有启动事务,则启动一个;否则,继续使用现有的事务。

@Transactional(REQUIRES_NEW)

如果没有启动事务,则启动一个;如果已经启动了现有事务,则将其挂起,并在该方法的边界内启动一个新事务。

@Transactional(MANDATORY)

如果没有启动事务,则失败;否则,在现有事务中工作。

@Transactional(SUPPORTS)

如果已经启动了事务,则加入它;否则,在没有事务的情况下工作。

@Transactional(NOT_SUPPORTED)

如果已经启动了事务,则暂停它,并在方法边界内没有事务地工作;否则,在没有事务的情况下工作。

@Transactional(NEVER)

如果已经启动了事务,则会引发异常;否则,会在没有事务的情况下工作。

7.6 编程式事务控制

问题

您希望更精细地控制事务。

解决方案

注入UserTransaction并使用该类的方法。

UserTransaction类具有非常简单的 API:

  • begin()

  • commit()

  • rollback()

  • setRollbackOnly()

  • getStatus()

  • setTransactionTimeout(int)

前三种方法将是主要使用的方法。getStatus()有助于确定事务在其生命周期中的位置。最后,您可以为事务设置超时时间。

注意

如有必要,您还可以通过注入来使用javax.transaction.TransactionManager

参见

欲获取更多信息,请访问以下网页:

7.7 设置和修改事务超时时间

问题

您希望在一定时间后使事务超时并回滚。

解决方案

如果使用声明性事务,请使用@io.quarkus.narayana.jta.runtime.TransactionConfiguration注解;否则,可以使用事务 API 进行编程式事务控制。您还可以通过quarkus.transaction-manager.default-transaction-timeout属性更改全局事务超时时间,该属性以java.time.Duration格式指定。

使用@TransactionConfiguration注解非常容易修改事务的超时时间。使用timeout属性设置超时时间(以秒为单位)。

讨论

如果应用程序中的每个事务都需要更长或更短的时间,可以在application.properties中使用quarkus.transaction-manager.default-transaction-timeout属性。该属性接受java.time.Duration,可以作为可通过Duration#parse()解析的字符串指定。您也可以从整数开始持续时间。Quarkus 将自动在值前面添加PT,以创建正确的格式。

参见

欲获取更多信息,请访问以下网站:

7.8 使用 Persistence.xml 进行设置

问题

您希望使用带有persistence.xml文件的 JPA。

解决方案

您可以像平常一样使用 JPA;只需在application.properties中设置数据源即可。

讨论

在 Quarkus 中,JPA 的工作方式与其他设置完全相同,因此不需要进行任何更改。

注意

如果使用persistence.xml,则不能使用quarkus.hibernate-orm.*属性。如果使用persistence.xml,则只能使用在persistence.xml文件中定义的持久单元。

7.9 在没有 persistence.xml 的情况下进行设置

问题

您想使用 JPA,但不使用persistence.xml文件。

解决方案

添加quarkus-hibernate-orm扩展,为您的关系型数据库(RDBMS)配置 JDBC 驱动程序,通过application.properties进行配置,最后使用@Entity注解您的实体。

讨论

使用 Quarkus 和 JPA 并没有什么特别的设置,除了数据库连接。Quarkus 将做出一些主观选择,但不用担心——它们可能是您本来会选择的选项——然后继续使用您的实体。您可以像平常一样注入和使用EntityManager

简而言之,您可以像使用标准 JPA 一样继续进行,但不需要额外的 persistence.xml 配置。这是在 Quarkus 中使用 JPA 的首选方式。

使用来自不同 JAR 的实体

问题

您希望包含来自不同 jar 的实体。

解决方案

在包含实体的 jar 中包含一个空的 META-INF/beans.xml 文件。

讨论

Quarkus 依赖于实体的编译时字节码增强。如果这些实体定义在与应用程序的其余部分(jar)相同的项目中,则一切都会正常工作。

然而,如果其他类(如实体或其他 CDI bean)定义在外部库中,则该库必须包含一个空的 META-INF/beans.xml 文件,以便正确地被索引和增强。

使用 Panache 持久化数据

问题

您希望使用 Hibernate 和 Panache 持久化数据。

解决方案

PanacheEntity 上调用 persist 方法。

当然,您需要添加 quarkus-hibernate-orm-panache 扩展,并为您的数据存储添加相应的 JDBC 扩展。接下来,您需要定义一个实体。所有这些都包括创建一个类,用 @javax.persistence.Entity 注解,并扩展自 PanacheEntity

讨论

Panache 是建立在传统 JPA 之上的一种主张 API。它更多地遵循一种活动记录的方法来处理数据实体;然而,在底层,它是使用传统的 JPA 来实现的。

正如您在探索 Panache 时将会发现的那样,很多功能都通过 PanacheEntityPanacheEntityBase 父类传递给您的实体——persist 也不例外。PanacheEntityBase 包含 persist()persistAndFlush() 方法。虽然 flush 选项会立即将数据发送到数据库,但这不是推荐的持久化数据的方式。

正如您在以下所见,持久化是非常简单的:

    @POST
    public Response newLibrary(Library library) {
        library.persist();
        return Response.created(URI.create("/library/" + library.encodedName()))
                .entity(library).build();
    }

作为补充,这里是 Library 实体:

package org.acme.panache;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.panache.common.Parameters;

@Entity
public class Library extends PanacheEntity {
    public String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true,
               mappedBy = "library")
    public List<Inventory> inventory;
    public String encodedName() {
        String result;

        try {
            result = URLEncoder.encode(name, "UTF-8")
                    .replaceAll("\\+", "%20")
                    .replaceAll("\\%21", "!")
                    .replaceAll("\\%27", "'")
                    .replaceAll("\\%28", "(")
                    .replaceAll("\\%29", ")")
                    .replaceAll("\\%7E", "~");
        } catch (UnsupportedEncodingException e) {
            result = name;
        }

        return result;
    }
}

使用 Panache 的 listAll 方法来查找所有实体实例

问题

您想要找到实体的所有条目。

解决方案

使用 PanacheEntityBase 类的 listAll() 方法。

就像前一节中涵盖的 persist() 方法一样,listAll()PanacheEntityBase 类的一个方法。它并没有什么特别之处;它查询给定实体的所有条目。它以 List 的形式返回这些实体:

    @GET
    public List<Book> getAllBooks() {
        return Book.listAll();
    }
注意

这实际上是一个 findAll().list() 链的快捷方式。有关更多信息,请参阅 hibernate-orm-panache 代码库。

使用 Panache 的 findById 方法来查找单个实体对象

问题

我想要根据其 ID 从数据库中找到并加载一个实体。

解决方案

使用 PanacheEntityBase.findById(Object)

使用 findById(Object) 方法简化了通过 Panache 查找实体的过程。您只需传递对象的 ID,就能从数据库中返回正确的实例:

    @GET
    @Path("/byId/{id}")
    public Book getBookById(@PathParam(value = "id") Long id) {
        Book b = Book.findById(id);
        return b;
    }

使用 Panache 查找和列出实体的方法

问题

您希望根据其属性查询数据库中的特定实体。

解决方案

使用 PanacheEntityBase 的各种 findlist 方法的实例。

根据结果返回的需要,您将使用 PanacheEntityBaselistfind。在内部,list 使用 find,因此它们本质上是相同的:

    public static Book findByTitle(String title) {
        return find("title", title).firstResult();
    }

    public static List<Book> findByAuthor(String author) {
        return list("author", author);
    }

    public static List<Book> findByIsbn(String isbn) {
        return list("isbn", isbn);
    }

两种方法都有多个重载版本——它们根据排序的必要性和您希望发送参数的方式而变化。以下代码是使用 Hibernate 查询语言(HQL)(或 Java 持久化查询语言 [JPQL])和使用 Parameters 类的示例:

    public static Library findByName(String name) {
        return Library
                .find("SELECT l FROM Library l " +
                      "LEFT JOIN fetch l.inventory " +
                      "WHERE l.name = :name ",
                        Parameters.with("name", name)).firstResult();
    }

Parameters 类的覆盖可用于 findlist 方法。有关更多信息,请参考 API。

注意

您可能会问,为什么需要完整的 JPQL 查询?简而言之,这是为了避免与列表的序列化问题。通过左连接,我们能够获取图书馆及其所有库存。

7.15 使用 Panache 的 count 方法获取实体计数

问题

您想要获取资源的项目计数。

解决方案

使用 PanacheEntityBase 的各种 count 方法。

就像之前讨论的 find 方法一样,Panache 提供了各种 count 方法的重载,用于获取数据库中给定类型的实体数量:

Book.count()
Book.count("WHERE title = ?", )

7.16 使用 Panache 的 page 方法分页浏览实体列表

问题

您希望使用分页。

解决方案

Quarkus,特别是 Panache,内置了分页功能。PanacheQuery 对象上有许多支持分页的方法。

讨论

分页非常容易上手。第一步是获取 PanacheQuery 的实例。这与使用 find 方法一样简单:

PanacheQuery<Book> authors = Book.find("author", author);
authors.page(Page.of(3, 25)).list();                        ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

authors.page(Page.sizeOf(10)).list();

1

每页显示 25 个项目,从第 3 页开始

当然,还有其他方法,如 firstPage()lastPage()nextPage()previousPage()。还存在支持布尔的方法:hasNextPage()hasPreviousPage()pageCount()

参见

欲了解更多信息,请参阅 GitHub 上的以下页面:

7.17 通过 Panache 的 Stream 方法流式处理结果

问题

您希望使用数据流。

解决方案

在使用 Panache 时,所有的 list 方法都有对应的 stream 方法。下面您将看到它们的使用方式,与 list 方法的使用方式没有任何不同:

Book.streamAll();
...
Book.stream("author", "Alex Soto");

streamstreamAll 方法各自返回一个 java.util.Stream 实例。

注意

stream 方法需要事务才能正常工作。

参见

欲了解更多信息,请访问以下网站:

7.18 测试 Panache 实体

问题

您希望在测试中使用嵌入式数据库。

解决方案

Quarkus 提供了用于内存数据库 H2 和 Derby 的助手,以正确地将数据库作为单独的进程引导。

确保将io.quarkus:quarkus-test-h2:1.4.1.Finali⁠o​.⁠q⁠u⁠a⁠r⁠k⁠u⁠s⁠:⁠q⁠u⁠a⁠r⁠k⁠u⁠s⁠-⁠t⁠e⁠s⁠t⁠-⁠d⁠e⁠r⁠b⁠y⁠:⁠1⁠.⁠4⁠.⁠1⁠.⁠F⁠i⁠n⁠a⁠l这些构件添加到您的构建文件中。

下一步是对使用嵌入式数据库的任何测试进行注释,使用@⁠Q⁠u⁠a⁠r⁠k⁠u⁠s​T⁠e⁠s⁠t⁠R⁠e⁠s⁠o⁠u⁠r⁠c⁠e⁠(⁠H⁠2⁠D⁠a⁠t⁠a⁠b⁠a⁠s⁠e⁠T⁠e⁠s⁠t⁠R⁠e⁠s⁠o⁠u⁠r⁠c⁠e⁠.⁠c⁠l⁠a⁠s⁠s⁠)@QuarkusTestResource(DerbyDatabaseTestResource.class)。最后,请确保在src/test/resources/META-INF/application.properties中为所选数据库设置正确的数据库 URL 和驱动程序。

以下是 H2 的示例:

package my.app.integrationtests.db;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.h2.H2DatabaseTestResource;

@QuarkusTestResource(H2DatabaseTestResource.class)
public class TestResources {
}
quarkus.datasource.url=jdbc:h2:tcp://localhost/mem:test
quarkus.datasource.driver=org.h2.Driver
注意

此辅助程序仅将数据库添加到本机映像中,而不是客户端代码。但是,在 JVM 模式或本机映像模式下的应用程序测试中,可以自由使用此辅助程序。

参见

欲了解更多信息,请访问以下网站:

7.19 使用数据访问对象(DAO)或存储库模式

问题

欲使用 DAO 或存储库模式。

解决方案

Quarkus 不限于 Panache;您可以像以前描述的那样使用实体模式,DAO 或存储库模式。

要使用存储库,您需要理解PanacheRepositoryPanacheRepositoryBase这两个接口。只有在您的主键不是Long时,才需要基本接口。PanacheEntity上可用的所有操作在PanacheRepository上也都可以使用。存储库是一个 CDI bean,在使用时必须注入它。以下是一些基本示例:

package org.acme.panache;

import java.util.Collections;

import javax.enterprise.context.ApplicationScoped;

import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import io.quarkus.panache.common.Parameters;
import io.quarkus.panache.common.Sort;

@ApplicationScoped
public class LibraryRepository implements PanacheRepository<Library> {
    public Library findByName(String name) {
        return find("SELECT l FROM Library l " +
                    "left join fetch l.inventory where l.name = :name ",
                Parameters.with("name", name)).firstResult();
    }

    @Override
    public PanacheQuery<Library> findAll() {
        return find("from Library l left join fetch l.inventory");
    }

    @Override
    public PanacheQuery<Library> findAll(Sort sort) {
        return find("from Library l left join fetch l.inventory",
                sort, Collections.emptyMap());
    }
}

DAO 将按您的预期工作。您需要注入一个EntityManager并像平常一样查询。关于在 Java 中使用 DAO 的解决方案和示例非常多,可以在线或其他书籍中找到。所有这些示例在 Quarkus 中都将正常运行。

参见

欲了解更多信息,请访问以下网站:

7.20 使用 Amazon DynamoDB

问题

欲在 Quarkus 应用程序中使用 DynamoDB。

解决方案

使用 DynamoDB 扩展并设置配置。DynamoDB 扩展允许同步和异步客户端使用 Apache Amazon Web Service 软件开发工具包(AWS SDK)客户端。在项目中启用并配置此功能需要一些必要的设置。首先当然是依赖项:

 <dependency>
     <groupId>software.amazon.awssdk</groupId>
     <artifactId>url-connection-client</artifactId>
 </dependency>
注意

Quarkus 没有 AWS 连接客户端的扩展。

默认情况下,扩展使用 URLConnection HTTP 客户端。您需要向构建脚本添加正确的客户端(URLConnection,Apache 或 Netty NIO):

 <dependency>
     <groupId>software.amazon.awssdk</groupId>
     <artifactId>apache-client</artifactId>
     <exclusions>    ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
         <exclusion>
             <groupId>commons-logging</groupId>
             <artifactId>commons-logging</artifactId>
         </exclusion>
     </exclusions>
 </dependency>

1

您必须排除commons-logging以强制客户端使用 Quarkus 日志记录器

如果使用 Apache 客户端,则还需要对application.properties文件进行调整,因为url是默认值:

 quarkus.dynamodb.sync-client.type=apache

application.properties 中还有客户端的配置(请参阅属性引用以获取更多信息):

quarkus.dynamodb.endpoint-override=http://localhost:8000 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.dynamodb.aws.region=eu-central-1 ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.dynamodb.aws.credentials.type=static ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.dynamodb.aws.credentials.static-provider.access-key-id=test-key
quarkus.dynamodb.aws.credentials.static-provider.secret-access-key=test-secret

1

如果使用非标准端点(例如本地 DynamoDB 实例),则很有用

2

正确且有效的地区

3

staticdefault

default 凭证类型将按顺序查找凭证:

  • 系统属性 aws.accessKeyIdaws.secretKey

  • 环境变量 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY

  • 凭证配置文件位于默认位置($HOME/.aws/credentials

  • 通过 Amazon EC2 容器服务提供的凭据

  • 通过 Amazon EC2 元数据服务传递的实例配置凭据

讨论

下面的示例来自 Recipe 7.11,但使用 DynamoDB 作为持久存储。

下面是用于与 DynamoDB 通信并创建可注入服务以在 REST 端点中使用的两个类:

package org.acme.dynamodb;

import java.util.List;
import java.util.stream.Collectors;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

@ApplicationScoped
public class BookSyncService extends AbstractBookService {
    @Inject
    DynamoDbClient dynamoDbClient;

    public List<Book> findAll() {
        return dynamoDbClient.scanPaginator(scanRequest()).items().stream()
                .map(Book::from)
                .collect(Collectors.toList());
    }

    public List<Book> add(Book b) {
        dynamoDbClient.putItem(putRequest(b));
        return findAll();
    }

    public Book get(String isbn) {
        return Book.from(dynamoDbClient.getItem(getRequest(isbn)).item());
    }
}

下面的抽象类包含了与 DynamoDB 通信以及持久化和查询 Book 实例所需的样板代码:

package org.acme.dynamodb;

import java.util.HashMap;
import java.util.Map;

import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;

public abstract class AbstractBookService {

    public final static String BOOK_TITLE = "title";
    public final static String BOOK_ISBN = "isbn";
    public final static String BOOK_AUTHOR = "author";

    public String getTableName() {
        return "QuarkusBook";
    }

    protected ScanRequest scanRequest() {
        return ScanRequest.builder().tableName(getTableName()).build();
    }

    protected PutItemRequest putRequest(Book book) {
        Map<String, AttributeValue> item = new HashMap<>();
        item.put(BOOK_ISBN, AttributeValue.builder()
                            .s(book.getIsbn()).build());
        item.put(BOOK_AUTHOR, AttributeValue.builder()
                              .s(book.getAuthor()).build());
        item.put(BOOK_TITLE, AttributeValue.builder()
                             .s(book.getTitle()).build());

        return PutItemRequest.builder()
                .tableName(getTableName())
                .item(item)
                .build();
    }

    protected GetItemRequest getRequest(String isbn) {
        Map<String, AttributeValue> key = new HashMap<>();
        key.put(BOOK_ISBN, AttributeValue.builder().s(isbn).build());

        return GetItemRequest.builder()
                .tableName(getTableName())
                .key(key)
                .build();
    }
}

这个最后的类是表示 Book 实体的类:

package org.acme.dynamodb;

import java.util.Map;
import java.util.Objects;

import io.quarkus.runtime.annotations.RegisterForReflection;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

@RegisterForReflection      ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class Book {
    private String isbn;
    private String author;
    private String title;

    public Book() {         ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    }

    public static Book from(Map<String, AttributeValue> item) {
        Book b = new Book();
        if (item != null && !item.isEmpty()) {
            b.setAuthor(item.get(AbstractBookService.BOOK_AUTHOR).s());
            b.setIsbn(item.get(AbstractBookService.BOOK_ISBN).s());
            b.setTitle(item.get(AbstractBookService.BOOK_TITLE).s());
        }
        return b;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(isbn, book.isbn) &&
                Objects.equals(author, book.author) &&
                Objects.equals(title, book.title);
    }

    @Override
    public int hashCode() {
        return Objects.hash(isbn, author, title);
    }
}

1

在本机应用程序中必须具有反射

2

DynamoDB 客户端所需

大部分是标准的 DynamoDB 代码,唯一的例外是 Quarkus 注解为反射注册 Book 类,仅在创建本机镜像时才需要。

正如您所看到的,当使用 Quarkus 时,您以前使用 DynamoDB 时已经掌握的技能仍然可以在工作中使用,而无需进行太多修改,这有助于提高您的工作效率。

7.21 使用 MongoDB

问题

您希望使用 MongoDB 作为持久存储。

解决方案

Quarkus MongoDB 扩展使用了 MongoDB 驱动程序和客户端。

讨论

到目前为止,您应该对 RESTful 资源和 Quarkus 配置的基础知识很熟悉。在这里,我们将展示用于与本地 MongoDB 实例通信的代码和示例配置。

自然地,您需要将连接信息添加到您的应用程序中:

quarkus.mongodb.connection-string = mongodb://localhost:27017

Book 类是 MongoDB 中文档的表示:

package org.acme.mongodb;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import org.bson.Document;

public class Book {
    public String id;
    public String title;
    public String isbn;
    public Set<String> authors;

    // Needed for JSON-B
    public Book() {}

    public Book(String title) {
        this.title = title;
    }

    public Book(String title, String isbn) {
        this.title = title;
        this.isbn = isbn;
    }

    public Book(String title, String isbn, Set<String> authors) {
        this.title = title;
        this.isbn = isbn;
        this.authors = authors;
    }

    public Book(String id, String title, String isbn, Set<String> authors) {
        this.id = id;
        this.title = title;
        this.isbn = isbn;
        this.authors = authors;
    }

    public static Book from(Document doc) {
        return new Book(doc.getString("id"),
                        doc.getString("title"),
                        doc.getString("isbn"),
                        new HashSet<>(doc.getList("authors", String.class)));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(id, book.id) &&
                Objects.equals(title, book.title) &&
                Objects.equals(isbn, book.isbn) &&
                Objects.equals(authors, book.authors);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, title, isbn, authors);
    }
}

这个服务类充当 DAO,一种进入 MongoDB 实例的方式:

package org.acme.mongodb;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.Filters;
import org.bson.Document;

@ApplicationScoped
public class BookService {

    @Inject
    MongoClient mongoClient;

    public List<Book> list() {
        List<Book> list = new ArrayList<>();

        try (MongoCursor<Document> cursor = getCollection()
                                            .find()
                                            .iterator()) {
            cursor.forEachRemaining(doc -> list.add(Book.from(doc)));
        }

        return list;
    }

    public Book findSingle(String isbn) {
        Document document = Objects.requireNonNull(getCollection()
                .find(Filters.eq("isbn", isbn))
                .limit(1).first());
        return Book.from(document);
    }

    public void add(Book b) {
        Document doc = new Document()
                .append("isbn", b.isbn)
                .append("title", b.title)
                .append("authors", b.authors);
        getCollection().insertOne(doc);
    }

    private MongoCollection<Document> getCollection() {
        return mongoClient.getDatabase("book").getCollection("book");
    }
}

最后,RESTful 资源利用了前面两个类:

package org.acme.mongodb;

import java.util.List;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/book")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookResource {
    @Inject
    BookService service;

    @GET
    public List<Book> getAll() {
        return service.list();
    }

    @GET
    @Path("{isbn}")
    public Book getSingle(@PathParam("isbn") String isbn) {
        return service.findSingle(isbn);
    }

    @POST
    public Response add(Book b) {
        service.add(b);
        return Response.status(Response.Status.CREATED)
                .entity(service.list()).build();
    }
}

我们将其留给读者作为一个练习,来创建并使用 BSON 编解码器。MongoDB 扩展的另一个有用功能是,在使用 quarkus-smallrye-health 扩展时自动运行的自动健康检查。当然,可读性检查是可配置的。

Quarkus MongoDB 扩展还包括一个响应式客户端,将在 Recipe 15.12 中详细介绍。

7.22 使用 Panache 与 MongoDB

问题

您希望使用 Panache 与 MongoDB。

解决方案

添加mongodb-panache扩展,并使用PanacheMongoEntity的所有 Panache 能力。

MongoDB 的 Panache 与 Hibernate 的 Panache 工作方式相同,我们在 7.7 到 7.17 的示例中看到了。它显著简化了您的实体代码:

package org.acme.mongodb.panache;

import java.time.LocalDate;
import java.util.List;

import io.quarkus.mongodb.panache.MongoEntity;
import io.quarkus.mongodb.panache.PanacheMongoEntity;
import org.bson.codecs.pojo.annotations.BsonProperty;

@MongoEntity(collection = "book", database = "book")    ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class Book extends PanacheMongoEntity {          ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    public String title;
    public String isbn;
    public List<String> authors;

    @BsonProperty("pubDate")                            ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    public LocalDate publishDate;

    public static Book findByIsbn(String isbn) {
        return find("isbn", isbn).firstResult(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
    }

    public static List<Book> findPublishedOn(LocalDate date) {
        return list("pubDate", date);
    }

}

1

可选的@MongoEntity注解允许您自定义使用的数据库和/或集合。

2

必需部分——将您的字段添加为公共字段

3

使用@BsonProperty自定义序列化字段名。

4

使用 PanacheQL(JPQL 的子集)进行查询,就像使用 JPA 一样。

讨论

Panache MongoDB 扩展使用PojoCodecProvider将实体映射到 MongoDB 的Document。除了@BsonProperty,您还可以使用@BsonIgnore忽略字段。您还可以使用@BsonId设置自定义 ID,并扩展PanacheMongoEntityBase

当然,如果您需要编写访问器方法,Panache 并不会阻止您这样做;事实上,在构建时所有字段调用都会替换为相应的访问器/变异器调用。就像 Hibernate 的 Panache 一样,MongoDB 版本支持分页、排序、流和 Panache API 的其余部分。

您在前面的示例中看到的 PanacheQL 查询易于使用和理解;但如果您更喜欢使用常规的 MongoDB 查询,也是支持的,只要查询以{开头。

与 Hibernate 和 MongoDB Panache 变体之间的轻微差异在于 MongoDB 能够在find()方法的返回上使用查询投影。这允许您限制从数据库返回哪些字段。以下是我们的Book实体的一个非常基本的示例:

import io.quarkus.mongodb.panache.ProjectionFor;

@ProjectionFor(Book.class)
public class BookTitle {
    public String title;
}

PanacheQuery<BookTitle> query = Book.find("isbn", "978-1-492-06265-3")
                                    .project(BookTitle.class);

如果您具有投影类的层次结构,则父类(们)也需要使用@ProjectionFor进行注释。

7.23 使用 Neo4j 与 Quarkus

问题

您希望连接并使用 Neo4j。

解决方案

使用基于 Neo4j Java Driver 的 Quarkus Neo4j 扩展。

以下示例使用基于 JDK 的 CompletableFuture 的异步编程模型。驱动程序还使用类似于 JDBC 的阻塞模型和响应式模型。响应式模型仅适用于 Neo4j 4 及以上版本。

到目前为止,您已经看到如何向项目添加额外的扩展,因此我们这里不再详述。以下示例还管理书籍,就像之前的示例一样:

package org.acme.neo4j;

import java.util.HashSet;
import java.util.Set;
import java.util.StringJoiner;

import org.neo4j.driver.Values;
import org.neo4j.driver.types.Node;

public class Book {
  public Long id;
  public String title;
  public String isbn;
  public Set<String> authors;

  // Needed for JSON-B
  public Book() {}

  public Book(String title) {
    this.title = title;
  }

  public Book(String title, String isbn) {
    this.title = title;
    this.isbn = isbn;
  }

  public Book(String title, String isbn, Set<String> authors) {
    this.title = title;
    this.isbn = isbn;
    this.authors = authors;
  }

  public Book(Long id, String title, String isbn, Set<String> authors) {
    this.id = id;
    this.title = title;
    this.isbn = isbn;
    this.authors = authors;
  }

  public static Book from(Node node) {
    return new Book(node.id(),
        node.get("title").asString(),
        node.get("isbn").asString(),
        new HashSet<>(
          node.get("authors")
          .asList(Values.ofString())
          )
        );
  }

  public String toJson() {
    final StringJoiner authorString =
      new StringJoiner("\",\"", "[\"", "\"]");

    authors.forEach(authorString::add);

    return "{" +
      "\"title\":\"" + this.title + "\"," +
      "\"isbn\":\"" + this.isbn + "\"," +
      "\"authors\":" + authorString.toString() +
      "}";
  }
}

当然,您需要配置客户端。这可以很简单,设置quarkus.neo4j.uriquarkus.neo4j.authentication.usernamequarkus.neo4j.authentication.password属性。您可以查阅扩展以获取更多属性信息。

配置客户端的第一件事是 Neo4j 驱动程序。该扩展提供了一个可注入的实例:

@Inject
Driver driver;

接下来,创建一个新的 REST 资源并添加 Driver 注入点,然后添加基本的 CRUD 操作:

@GET
public CompletionStage<Response> getAll() {
    AsyncSession session = driver.asyncSession();   ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

    return session
            .runAsync("MATCH (b:Book) RETURN b ORDER BY b.title")   ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
            .thenCompose(cursor -> cursor.listAsync(record ->
                    Book.from(record.get("b").asNode()))) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
            .thenCompose(books -> session.
                    closeAsync().thenApply(signal -> books))  ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
            .thenApply(Response::ok)    ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
            .thenApply(Response.ResponseBuilder::build);
}

1

从驱动程序获取AsyncSession

2

执行 Cypher(Neo4j 的查询语言)来获取数据

3

检索游标,从节点创建Book实例

4

在完成后关闭会话

5

构建 JAX-RS 响应

类/代码的其余部分遵循相同的模式:

@POST
public CompletionStage<Response> create(Book b) {
    AsyncSession session = driver.asyncSession();
    return session
            .writeTransactionAsync(tx ->
                    {
                        String query = "CREATE (b:Book " +
                        "{title: $title, isbn: $isbn, authors: $authors})" +
                        " RETURN b";
                        return tx.runAsync(query,
                                Values.parameters("title", b.title,
                                        "isbn", b.isbn,
                                        "authors", b.authors))
                                .thenCompose(ResultCursor::singleAsync);
                    }
            )
            .thenApply(record -> Book.from(record.get("b").asNode()))
            .thenCompose(persistedBook -> session.closeAsync()
                    .thenApply(signal -> persistedBook))
            .thenApply(persistedBook -> Response.created(
                    URI.create("/book/" + persistedBook.id)).build());
}
@DELETE
@Path("{id}")
public CompletionStage<Response> delete(@PathParam("id") Long id) {
    AsyncSession session = driver.asyncSession();
    return session
            .writeTransactionAsync(tx -> tx
                    .runAsync("MATCH (b:Book) WHERE id(b) = $id DELETE b",
                            Values.parameters("id", id))
                    .thenCompose(ResultCursor::consumeAsync))
            .thenCompose(resp -> session.closeAsync())
            .thenApply(signal -> Response.noContent().build());
}

最后一个有点不同,因为它处理错误:

@GET
@Path("{id}")
public CompletionStage<Response> getById(@PathParam("id") Long id) {
    AsyncSession session = driver.asyncSession();
    return session.readTransactionAsync(tx ->
            tx.runAsync("MATCH (b:Book) WHERE id(b) = $id RETURN b",
                                Values.parameters("id", id))
                    .thenCompose(ResultCursor::singleAsync))
            .handle(((record, err) -> {
                if (err != null) {
                    Throwable source = err;
                    if (err instanceof CompletionException)
                        source = ((CompletionException) err).getCause();
                    Response.Status status = Response.Status.
                                                INTERNAL_SERVER_ERROR;
                    if (source instanceof NoSuchRecordException)
                        status = Response.Status.NOT_FOUND;

                    return Response.status(status).build();
                } else {
                    return Response.ok(Book.from(record.get("b")
                                        .asNode())).build();
                }
            }))
            .thenCompose(response -> session.closeAsync()
                                            .thenApply(signal -> response));
}

参见

Neo4j Cypher 手册在您学习和尝试 Cypher 的新功能时会很有帮助。

7.24 在启动时使用 Flyway

问题

您希望使用 Flyway 来迁移我的数据库架构。

解决方案

使用quarkus-flyway集成扩展。

讨论

Quarkus 对使用 Flyway 进行模式迁移具有一流支持。您需要做五件事来在应用程序启动时使用 Flyway 与 Quarkus:

  1. 添加 Flyway 扩展。

  2. 添加适用于你的数据库的 JDBC 驱动程序。

  3. 设置数据源。

  4. 将迁移添加到src/main/resources/db/migration

  5. quarkus.flyway.migrate-at-start设置为true

Flyway 迁移的默认命名模式是V.<version>__<description>.sql。其他所有事项都已处理。

您还可以使用多个数据源来使用 Flyway。任何需要为每个数据源配置的设置都以与数据源名称相同的模式命名:quarkus.flyway.*数据源名称*.*设置*。例如,可能是quarkus.flyway.users.migrate-at-start

7.25 以编程方式使用 Flyway

问题

您希望以编程方式使用 Flyway。有时,您可能希望控制模式何时迁移,而不是在应用程序启动时进行迁移。

解决方案

使用quarkus-flyway扩展并注入Flyway实例:

@Inject
Flyway flyway

讨论

这将注入针对默认数据源配置的默认org.flywaydb.core.Flyway实例。如果您有多个数据源和 Flyway 实例,可以使用@FlywayDataSource@Named注解来注入特定的实例。当使用@FlywayDataSource时,值是数据源的名称。如果改用@Named,则值应该是带有flyway_前缀的数据源名称:

@Inject
@FlywayDataSource("books")
Flyway flywayBooks;

@Inject
@Named("flyway_users")
Flyway flywayUsers;

自然而然,您将能够运行所有标准的 Flyway 操作,如cleanmigratevalidateinfobaselinerepair

第八章:容错

在本章中,您将了解为什么需要在微服务架构中接受失败,因为这种情况经常发生。其中一个原因是微服务架构在很大程度上依赖于网络进行功能运作,而网络作为一个关键部分并不总是可用(网络故障、线路饱和、拓扑结构变化、下游服务更新等)。

因此,构建能够容忍任何问题并提供优雅解决方案而不是简单传播错误的服务变得非常重要。

本章将包括以下任务的解决方案:

  • 实施不同的弹性策略

  • 提供一些在出现错误时的回退逻辑

  • 正确配置容错参数

8.1 实现自动重试

问题

如果出现错误,您希望执行自动重试以尝试从故障中恢复。

解决方案

MicroProfile Fault Tolerance 规范提供了一种在任何 CDI 元素上实现自动重试的方式,包括 CDI bean 和 MicroProfile REST Client。

可以实现几种策略来防止故障,并在最坏情况下提供一些默认逻辑而不是失败。假设您有一个根据读者喜好建议书籍的服务。如果此服务宕机,您可以缓存一份畅销书列表,以至少能够提供该列表而不是失败。因此,定义作为容错策略的重要部分之一的回退逻辑非常重要,以在没有可能的恢复情况下执行。

MicroProfile Fault Tolerance 专注于几种策略,使您的代码具备容错能力。让我们看看第一种策略,即执行自动重试,这是非常简单的一种方式。

您需要添加扩展来使用 MicroProfile Fault Tolerance 规范:

./mvnw quarkus:add-extension -Dextensions="quarkus-smallrye-fault-tolerance"

从网络故障中恢复的最简单且有时最有效的方法之一是对同一操作进行重试。如果是偶发性错误,那么可以通过几次重试来修复错误。

使用 @org.eclipse.microprofile.faulttolerance.Retry 注释的类或方法如果抛出异常,则执行自动重试。您可以设置不同的参数,如最大重试次数、最大持续时间或抖动;或者您可以指定应为其执行重试的异常类型。

此外,您可以通过在方法上注释 @org.eclipse.microprofile.faulttolerance.Fallback 来实现回退逻辑。作为回退执行的逻辑可以作为实现 org.eclipse.microprofile.faulttolerance.FallbackHandler 接口的类来实现:

    @Retry(maxRetries = 3, ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
            delay = 1000) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    @Fallback(RecoverHelloMessageFallback.class) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    public String getHelloWithFallback() {
        failureSimulator.failAlways();
        return "hello";
    }

    public static class RecoverHelloMessageFallback
        implements FallbackHandler<String> { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

        @Override
        public String handle(ExecutionContext executionContext) {
            return "good bye";
        }

    }

1

将最大重试次数设置为 3

2

重试之间有 1 秒的延迟

3

如果经过 3 次重试问题仍然存在,则添加回退逻辑

4

FallbackHandler 模板必须与恢复方法的返回类型相同

讨论

您可以通过配置文件覆盖这些属性中的任何一个。配置键遵循以下格式:*fully_qualified_class_name*/*method_name*/*fault_tolerant_annotation*/*parameter*

例如,您可以针对方法或类设置特定的参数,或者全局设置:

org.acme.quickstart.ServiceInvoker/getHelloWithFallback/Retry/maxDuration=30 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
org.acme.quickstart.ServiceInvoker/Retry/maxDuration=3000 ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
Retry/maxDuration=3000 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

方法级别的覆盖

2

类级别的覆盖

3

全局覆盖

参见

要获取更多信息,请访问 Eclipse MicroProfile 网站上的以下页面:

8.2 实现超时

问题

您希望防止执行永久等待。

解决方案

MicroProfile Fault Tolerance 规范提供了一种实现操作超时并防止执行无限等待的方式。

当调用外部服务时,建议确保此操作有关联的超时时间。这样,如果存在网络延迟或故障,流程不会长时间等待并最终失败,而是会快速失败,让您能够更早地解决问题。

使用 @org.eclipse.microprofile.faulttolerance.Timeout 注解的类或方法定义了超时。如果超时,则会抛出 org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException 异常:

    @Timeout(value = 2000) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    public String getHelloWithTimeout() {
        failureSimulator.longMethod();
        return "hello";
    }

1

将超时设置为 2 秒

您可以通过配置文件覆盖这些属性中的任何一个,如下所示:

org.acme.quickstart.ServiceInvoker/getHelloWithTimeout/Timeout/value=3000 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
org.acme.quickstart.ServiceInvoker/Timeout/value=3000 ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
Timeout/value=3000 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

方法级别的覆盖

2

类级别的覆盖

3

全局覆盖

您可以将 @Timeout 注解与 @Fallback 结合使用,在超时时实现一些恢复逻辑,或者使用 @Retry 在超时异常发生时执行自动重试(@Retry(retryOn=TimeoutException.class))。

参见

要了解有关 MicroProfile Fault Tolerance 中超时模式的更多信息,请访问 GitHub 上的以下页面:

8.3 使用舱壁模式避免过载

问题

您希望限制对服务的接受请求数量。

解决方案

MicroProfile Fault Tolerance 规范提供了 舱壁 模式的实现。

舱壁模式限制可以同时执行的操作,保持新请求等待,直到当前执行的请求完成。如果等待的请求在一定时间后无法执行,则被丢弃并抛出异常。

使用@org.eclipse.microprofile.faulttolerance.Bulkhead注解的类或方法会应用舱壁限制。如果存在同步调用(你将在 第十五章 中了解舱壁限制如何与异步调用配合工作),当达到并发执行限制时,将抛出org.eclipse.microprofile.faulttolerance.exceptions.BulkheadException异常,而不是排队请求:

    @Bulkhead(2) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    public String getHelloBulkhead() {
        failureSimulator.shortMethod();
        return "hello";
    }

1

将并发执行限制设置为两个

如果你使用siege工具模拟 4 个并发请求,输出将如下所示:

siege -r 1 -c 4 -v http://localhost:8080/hello/bulkhead 
** SIEGE 4.0.4 ** Preparing 4 concurrent users for battle. The server is now under siege... HTTP/1.1 500     0.47 secs:    2954 bytes ==> GET  /hello/bulkhead HTTP/1.1 500     0.47 secs:    2954 bytes ==> GET  /hello/bulkhead HTTP/1.1 200     2.46 secs:       5 bytes ==> GET  /hello/bulkhead HTTP/1.1 200     2.46 secs:       5 bytes ==> GET  /hello/bulkhead 
Transactions:		       2 hits Availability:		       50.00 % ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

1

只处理 2 个请求

此外,你可以通过配置文件覆盖任何这些属性:

org.acme.quickstart.ServiceInvoker/getHelloBulkhead/Bulkhead/value=10 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
org.acme.quickstart.ServiceInvoker/Bulkhead/value=10 ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
Bulkhead/value=10 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

方法级别的覆盖

2

类级别的覆盖

3

全局覆盖

讨论

当你处理(微)服务架构时,当另一个服务超载超过其一次能够消耗的调用时,问题可能会发生。如果超载继续,该服务可能会不堪重负并且无法在可接受的时间内处理请求。

你可以将@Bulkhead注解与之前演示的任何其他容错注解混合使用,以实现更具弹性的策略——例如,舱壁 + 带延迟重试

另请参阅

要了解 MicroProfile Fault Tolerance 中的舱壁模式更多信息,请参阅 GitHub 上的以下页面:

8.4 使用断路器模式避免不必要的调用

问题

你希望防止服务故障传播到其他服务,并消耗大量资源。

解决方案

MicroProfile Fault Tolerance 规范提供了 断路器 模式,以避免在出现错误时进行不必要的调用。

我们定义一个断路器,它在 4 个请求窗口中发生 3 次错误后被触发:

    @CircuitBreaker(requestVolumeThreshold = 4, ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
                    failureRatio = 0.75,  ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
                    delay = 2000) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    public String getHelloCircuitBreaker() {
        failureSimulator.fail4Consecutive();
        return "hello";
    }

1

定义滚动窗口

2

触发断路的阈值(4 × 0.75 = 3)

3

断路器打开的时间量

你可以通过配置文件覆盖任何这些属性:

org.acme.quickstart.ServiceInvoker/getHelloCircuitBreaker \
    /CircuitBreaker/failureRatio=0.75 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
org.acme.quickstart.ServiceInvoker/CircuitBreaker/failureRatio=3000 ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
Timeout/value=3000 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

方法级别的覆盖;这应该在同一行上

2

类级别的覆盖

3

全局覆盖

讨论

在处理(微)服务架构时,当与另一个服务的通信变得不可能时,可能会出现问题,无论是因为服务已宕掉还是因为延迟过高。 这种情况发生时,调用者在等待另一个服务响应时可能会消耗昂贵的资源,如线程或文件描述符。 如果这种情况持续下去,可能会导致资源耗尽,这意味着该服务无法处理更多请求,从而触发应用程序中其他服务的错误级联。

图 8-1 展示了一个服务中发生的失败如何通过其所有调用者进行传播。 这是级联故障的一个示例。

qucb 0801

图 8-1. 级联故障

断路器模式通过检测检测窗口内的连续故障数来修复级联故障。 如果超过定义的错误阈值,则会触发断路器,这意味着在一定时间内,所有尝试调用此方法的尝试都将立即失败,而不尝试执行它。 图 8-2 展示了断路器调用模式的图表。

qucb 0802

图 8-2. 断路器调用

一段时间后,断路器将变成半开状态,这意味着下一次调用不会立即失败,而是会再次尝试连接真实系统。 如果调用成功,则断路器将关闭;否则,它将保持打开状态。 断路器模式的所有可能状态如图 8-3 所示。

qucb 0803

图 8-3. 断路器生命周期

使用@org.eclipse.microprofile.faulttolerance.CircuitBreaker注释的类或方法为特定操作定义断路器。 如果断路器已打开,则会抛出org.eclipse.microprofile.faulttolerance.exceptions.CircuitBreakerOpenException异常。

您还可以将@CircuitBreaker@Timeout@Fallback@Bulkhead@Retry混合使用,但必须考虑以下内容:

  • 如果使用@Fallback,则如果抛出CircuitBreakerOpenException,将执行备用逻辑。

  • 如果使用@Retry,则每次重试都由断路器处理,并记录为成功或失败。

  • 如果使用@Bulkhead,则在尝试进入舱壁之前会检查断路器。

参见

要了解有关 MicroProfile Fault Tolerance 中断路器模式的更多信息,请参阅 GitHub 上的以下页面:

8.5 禁用容错

问题

您希望在某些环境中禁用容错。

解决方案

微配置 Fault Tolerance 规范提供了一个特殊参数,可以全局或单独地启用或禁用容错逻辑。

有些情况下,您可能希望禁用容错逻辑。MicroProfile 容错规范定义了一个特殊参数enabled,可以用来从配置文件中全局或个别地启用或禁用逻辑:

org.acme.quickstart.ServiceInvoker/getHelloCircuitBreaker/\
    CircuitBreaker/enabled=false ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
org.acme.quickstart.ServiceInvoker/CircuitBreaker/enabled=false ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
CircuitBreaker/enabled=false ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
MP_Fault_Tolerance_NonFallback_Enabled=false ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

1

在方法级别禁用;这应该在同一行

2

在类级别禁用

3

按类型全局禁用

4

禁用所有容错功能

第九章:可观察性

在本章中,您将了解可观察性及其在微服务架构中的重要性。可观察性通过观察一些参数(如错误代码、性能或任何类型的业务指标)来回答系统行为的问题。Quarkus 与用于可观察性的多种技术进行了原生集成。

本章将包含如何完成以下任务的示例:

  • 定义健康检查

  • 向监控系统提供指标

  • 配置分布式跟踪以了解网格内的请求概况

9.1 使用自动健康检查

问题

您想要检查服务是否正在运行并且能够正确处理请求。

解决方案

MicroProfile 健康规范提供了一个 API,用于从另一台机器(例如 Kubernetes 控制器)探查服务的状态。

要在 Quarkus 应用程序中启用 MicroProfile Health,您只需要注册 quarkus-smallrye-health 扩展:

./mvnw quarkus:add-extension -Dextensions="quarkus-smallrye-health"

有了类路径中的扩展,Quarkus 将自动注册默认的活跃性和可用性探针,当服务正在运行时,两者都返回 UP

./mvnw compile quarkus:dev 
curl localhost:8080/health/live ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

{
 "status": "UP", ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
 "checks":  ![3
 ] } 

curl localhost:8080/health/ready ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

{
 "status": "UP", "checks": [ ] }

1

活跃性 URL

2

状态为 UP

3

没有检查(只有默认值)

4

可用性 URL

讨论

MicroProfile 健康规范提供了两种健康检查:

活跃性

如果服务已启动,则返回 200 OK 结果为 UP,如果服务不可用,则返回 503 Service Unavailable 和 DOWN 结果,如果无法计算健康检查,则返回 500 Server Error。活跃性探针端点默认注册在 /health/live 端点。

可用性

如果服务准备好处理请求,则返回 200 OK 结果为 UP。这与活跃性探针不同,因为活跃性只意味着服务正在运行,但可能尚未能够处理任何请求(例如,因为正在执行数据库迁移)。如果服务尚不能接受任何请求,则返回 503 Service Unavailable 和 DOWN 结果,如果无法计算健康检查,则返回 500 Server Error。准备探针端点默认注册在 /health/ready 端点。

如果您正在配置 Quarkus 使用 SQL 数据库(JDBC),它将自动注册一个可用性健康检查(在 checks 部分),以验证与数据库的连接是否可行。

以下扩展提供了自动的可用性/活跃性探针:

数据源

用于检查数据库连接状态的准备探针。

Kafka

用于检查 Kafka 连接状态的准备探针。默认情况下禁用,需要通过将 quarkus.kafka.health.enabled 属性设置为 true 来启用。

MongoDB

用于检查 MongoDB 连接状态的准备探针。

Neo4j

用于检查 Neo4j 连接状态的准备探针。

Artemis

一个就绪探针检查 Artemis JMS 连接状态。

Kafka Streams

存活性(用于流状态)和就绪性(创建的主题)探针。

Vault

一个就绪探针检查 Vault 状态。

可以通过将quarkus.*component*.health.enabled设置为false来禁用探针的自动生成:

quarkus.kafka-streams.health.enabled=false
quarkus.mongodb.health.enabled=false
quarkus.neo4j.health.enabled=false

参见

要了解有关 MicroProfile Health 规范的更多信息,请参阅 GitHub 上的以下页面:

9.2 创建自定义健康检查

问题

您希望自定义如何检查服务是否正在运行并且能够正确处理请求。

解决方案

MicroProfile Health 规范使您能够创建自定义的存活性和就绪性健康检查。在某些情况下,可能需要针对存活性或就绪性探针的自定义健康检查逻辑。

MicroProfile Health 规范允许您通过创建一个使用@org.eclipse.microprofile.health.Liveness@org.eclipse.microprofile.health.Readiness注解的方法,并返回org.eclipse.microprofile.health.HealthCheck接口的实现来创建自定义健康检查。

创建一个新的类在org.acme.quickstart.LivenessCheck.java来实现一个自定义的存活性探针:

@ApplicationScoped ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@Liveness ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
public class LivenessCheck implements HealthCheck { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

    @Override
    public HealthCheckResponse call() {
        HealthCheckResponseBuilder checkResponseBuilder = HealthCheckResponse
        .named("custom liveness"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

        if(isUpAndRunning()) {
            return checkResponseBuilder.up().build(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
        } else {
            return checkResponseBuilder.down()
                .withData("reason", "Failed connection")
                .build(); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
        }

    }
}

1

需要是一个 CDI 类

2

将健康检查设置为存活性

3

实现HealthCheck作为一个要求

4

设置健康检查名称

5

将结果设置为上线

6

将结果设置为下线

让我们检查这个存活性探针是否按预期工作:

./mvnw compile quarkus:dev

curl localhost:8080/health/live

{
 "status": "UP",
 "checks": [
 {
 "name": "custom liveness",
 "status": "UP"
 }
 ]
}

讨论

因为健康检查是作为 CDI bean 注册的,您还可以在工厂对象中生成健康检查,如 Recipe 5.7 中所解释的。

创建一个新的工厂类来包含新的健康检查 - 在这种情况下,是一个就绪性检查:

@ApplicationScoped ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class CustomHealthCheck {

    @Produces ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    @Readiness ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    public HealthCheck ready() {
        if (isReady()) {
            return io.smallrye.health.HealthStatus.up("Custom readiness"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
        } else {
            return io.smallrye.health.HealthStatus.down("Custom readiness");
        }
    }
}

1

需要是一个 CDI 类

2

该方法生成一个健康检查

3

就绪探针

4

HealthStatus 是一个为您实现HealthCheck接口的实用类

让我们检查这个就绪探针是否按预期工作:

./mvnw compile quarkus:dev

curl localhost:8080/health/ready

{
 "checks": [
 {
 "name": "Custom readiness",
 "status": "UP"
 }
 ],
 "status": "UP"
}

参见

MicroProfile Health 规范非常适合定义 Kubernetes 存活性和就绪探针。你可以在以下网站了解更多信息:

9.3 暴露指标

问题

您希望通过公开服务指标来主动检查生产环境中服务的当前状态,以便尽快检测到任何错误行为。

解决方案

MicroProfile Metrics 规范提供了一种从应用程序构建和公开指标到监控工具(例如 Prometheus)的方式。

要在 Quarkus 应用程序中启用 MicroProfile Metrics,您只需注册quarkus-smallrye-metrics扩展:

./mvnw quarkus:add-extension -Dextensions="quarkus-smallrye-metrics"

在类路径中添加扩展后,Quarkus 默认提供监控参数,并以 Prometheus 格式在/metrics端点公开它们:

./mvnw compile quarkus:dev

curl localhost:8080/metrics

base_cpu_processCpuLoad_percent 0.0
base_memory_maxHeap_bytes 4.294967296E9
base_cpu_systemLoadAverage 2.580078125
base_thread_daemon_count 6.0
...
vendor_memoryPool_usage_max_bytes{name="Compressed Class Space"} 3336768.0
vendor_memory_usedNonHeap_bytes 3.9182104E7

通过在 HTTP Accept头中添加application/json类型,可以将输出格式更改为 JSON:

curl --header "Accept:application/json" localhost:8080/metrics

{
 "base": {
 "cpu.systemLoadAverage": 4.06201171875,
 "thread.count": 20,
 "classloader.loadedClasses.count": 4914,
 ...
 },
 "vendor": {
 "memoryPool.usage.max;name=G1 Survivor Space": 7340032,
 "memory.freePhysicalSize": 814391296,
 "memoryPool.usage.max;name=CodeHeap 'non-profiled nmethods'": 5773056,
 ...
 }
}

讨论

在微服务架构中了解服务的行为方式对于预测可能影响所有应用程序的任何问题至关重要。

对于单体应用程序,监控服务行为相对较容易,因为您只需监控三到四个元素;但是现在随着(微)服务架构的出现,您可能需要监控数百个元素。

有许多可能要监控的值,例如以下内容:

  • 内存

  • 磁盘空间

  • 网络

  • JVM 资源

  • 关键方法的性能

  • 业务指标(例如每秒付款的次数)

  • 集群整体健康状况

如果您仔细查看输出,您将看到参数分别以basevendor作为前缀。MicroProfile Metrics 将指标分类为三类:

base

服务器的核心信息。这些指标始终是必需的,因为它们在规范中指定。在/metrics/base路径下访问它们。

供应商

供应商特定的信息。每种实现可能提供不同的信息。在/metrics/vendor路径下访问它们。

应用程序

使用 MicroProfile Metrics 扩展机制为该服务定制了专门的信息。在/metrics/application路径下访问它们。

您可以通过将quarkus.smallrye-metrics.path属性设置为要公开指标的路径来配置指标的公开位置。默认情况下,此属性设置为/metrics

参见

要了解更多有关 MicroProfile Metrics 的信息,请访问 GitHub 上的以下页面:

9.4 创建指标

问题

您希望监控一些自定义指标,如性能指标或业务指标。

解决方案

MicroProfile Metrics 规范提供了不同的注解来注册不同类型的监控参数,如计数器、持续时间和仪表。通过这些注解,您可以创建可能与业务或性能参数相关的自定义指标,而不是像内存和 CPU 这样的物理值。

以下是 MicroProfile Metrics 的注解:

注解 描述

|

org.eclipse.microprofile.metrics.annotation.Counted

计数调用次数。

|

org.eclipse.microprofile.metrics.annotation.Timed

跟踪调用持续时间。

|

org.eclipse.microprofile.metrics.annotation.SimplyTimed

跟踪调用持续时间,不进行平均值和分布计算。Timed的简化版本。

|

org.eclipse.microprofile.metrics.annotation.Metered

跟踪调用频率。

|

org.eclipse.microprofile.metrics.annotation.Gauge

对一个带注解的字段或方法进行离散值采样。

|

org.eclipse.microprofile.metrics.annotation.ConcurrentGauge

计算并行调用次数的计量器。

|

org.eclipse.microprofile.metrics.annotation.Metric

|

用于注入指标。有效类型包括 MeterTimerCounterHistogramGauge 结合 Metric 只能在 CDI 生产者中使用。

|

让我们看看如何使用指标注解以及如何创建直方图指标。

计数器

一个 counter 在用 @Counted 注解的方法上增加调用次数,并且可以在方法或类级别使用。

在以下示例中,方法调用次数被计数:

@Counted( ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
        name = "number-of-transactions", ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        displayName = "Transactions", ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
        description = "How many transactions have been processed" ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
)
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response doTransaction(Transaction transaction) {
    return Response.ok().build();
}

1

注册计数器

2

计数器的名称

3

设置显示名称

4

计数器的描述

让我们来看看计数器监视器:

./mvnw compile quarkus:dev

curl -d '{"from":"A", "to":"B", "amount":2000}' \
 -H "Content-Type: application/json" \
 -X POST http://localhost:8080/tx

curl localhost:8080/metrics/application

application_org_acme_TransactionResource_number_of_transactions_total 1.0

计量器

一个 gauge 是一个简单的值,你希望公开以供测量,类似于汽车上的油表。要注册它,需要在字段或方法上注释 @Gauge,并且该值/返回值将自动公开:

private long highestTransaction = 0; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response doTransaction(Transaction transaction) {
    if (transaction.amount > highestTransaction) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        highestTransaction = transaction.amount;
    }
    return Response.ok().build();
}
@Gauge( ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
        name = "highest-gross-transaction", ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
        description = "Highest transaction so far.",
        unit= MetricUnits.NONE ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
)
public long highestTransaction() {
    return highestTransaction;
}

1

用于存储最高交易的字段

2

如果当前交易更高,则更新字段

3

将返回值设置为计量器

4

计量器的名称

5

该计量器的指标(例如,秒、百分比、每秒、字节等)

执行以下命令来运行应用程序,生成一些指标数据,并查看输出:

./mvnw compile quarkus:dev

curl -d '{"from":"A", "to":"B", "amount":2000}' \
 -H "Content-Type: application/json" \
 -X POST http://localhost:8080/tx

curl localhost:8080/metrics/application

application_org_acme_TransactionResource_highest_gross_transaction 2000.0

计量的

一个 metered 指标测量方法调用的速率。@Metered 注解可以在方法或类级别使用:

@Metered( ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
        name = "transactions",
        unit = MetricUnits.SECONDS, ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        description = "Rate of transactions"
)

1

注册计量指标

2

设置单位为秒

执行以下命令来运行应用程序,生成一些指标数据,并查看输出:

./mvnw compile quarkus:dev

curl -d '{"from":"A", "to":"B", "amount":2000}' \
 -H "Content-Type: application/json" \
 -X POST http://localhost:8080/tx

curl localhost:8080/metrics/application

application_org_acme_TransactionResource_transactions \
 _rate_per_second  0.09766473618811813
application_org_acme_TransactionResource_transactions \
 _one_min_rate_per_second  0.015991117074135343
application_org_acme_TransactionResource_transactions \
 _five_min_rate_per_second  0.0033057092356765017
application_org_acme_TransactionResource_transactions \
 _fifteen_min_rate_per_second  0.0011080303990206543

计时器

一个 timed 指标测量调用的持续时间。@Timed 注解可以在方法或类级别使用:

@Timed( ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    name = "average-transaction",
    unit = MetricUnits.SECONDS,
    description = "Average duration of transaction"
)

1

注册计时指标

执行以下命令来运行应用程序,生成一些指标数据,并查看输出:

./mvnw compile quarkus:dev

curl -d '{"from":"A", "to":"B", "amount":2000}' \
 -H "Content-Type: application/json" \
 -X POST http://localhost:8080/tx

curl localhost:8080/metrics/application

application_org_acme_TransactionResource_average_transaction \
 _rate_per_second 0.7080455375154214
application_org_acme_TransactionResource_average_transaction \
 _one_min_rate_per_second 0.0
application_org_acme_TransactionResource_average_transaction \
 _five_min_rate_per_second 0.0
application_org_acme_TransactionResource_average_transaction \
 _fifteen_min_rate_per_second 0.0
application_org_acme_TransactionResource_average_transaction \
 _min_seconds 1.0693E-5
application_org_acme_TransactionResource_average_transaction \
 _max_seconds 4.9597E-5
application_org_acme_TransactionResource_average_transaction \
 _mean_seconds 3.0145E-5
application_org_acme_TransactionResource_average_transaction \
 _stddev_seconds 1.9452E-5
application_org_acme_TransactionResource_average_transaction \
 _seconds_count 2.0
application_org_acme_TransactionResource_average_transaction \
 _seconds{quantile="0.5"} 4.9597E-5
application_org_acme_TransactionResource_average_transaction \
 _seconds{quantile="0.75"} 4.9597E-5
application_org_acme_TransactionResource_average_transaction \
 _seconds{quantile="0.95"} 4.9597E-5
application_org_acme_TransactionResource_average_transaction \
 _seconds{quantile="0.98"} 4.9597E-5
application_org_acme_TransactionResource_average_transaction \
 _seconds{quantile="0.99"} 4.9597E-5
application_org_acme_TransactionResource_average_transaction \
 _seconds{quantile="0.999"} 4.9597E-5

直方图

一个 histogram 测量值随时间的分布;它测量最小值、最大值、标准偏差或像中位数或 95 分位数这样的分位数。直方图没有适当的注解,但 org.eclipse.microprofile.metrics.Histogram 类用于更新指标:

@Metric(name = "transaction-evolution") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
Histogram transactionHistogram;

@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response doTransaction(Transaction transaction) {
    transactionHistogram.update(transaction.amount); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    return Response.ok().build();
}

1

注入带给定名称的直方图

2

使用新值更新直方图

执行以下命令来运行应用程序,存储一些指标数据,并查看输出:

./mvnw compile quarkus:dev

curl -d '{"from":"A", "to":"B", "amount":2000}' \
 -H "Content-Type: application/json" \
 -X POST http://localhost:8080/tx

curl localhost:8080/metrics/application

application_org_acme_TransactionResource_transaction_evolution_min 2000.0
application_org_acme_TransactionResource_transaction_evolution_max 2000.0
application_org_acme_TransactionResource_transaction_evolution_mean 2000.0
application_org_acme_TransactionResource_transaction_evolution_stddev 0.0
application_org_acme_TransactionResource_transaction_evolution_count 2.0
application_org_acme_TransactionResource_transaction_evolution \
 {quantile="0.5"}  2000.0
application_org_acme_TransactionResource_transaction_evolution \
 {quantile="0.75"}  2000.0
application_org_acme_TransactionResource_transaction_evolution \
 {quantile="0.95"}  2000.0
application_org_acme_TransactionResource_transaction_evolution \
 {quantile="0.98"}  2000.0
application_org_acme_TransactionResource_transaction_evolution \
 {quantile="0.99"}  2000.0
application_org_acme_TransactionResource_transaction_evolution \
 {quantile="0.999"}  2000.0

讨论

您可以通过使用OPTIONHTTP 方法查询到特定端点中的元数据信息。元数据在/metrics/*scope*/*metric_name*中公开,其中scopebasevendorapplicationmetric_name是指度量标准的名称(在应用程序中,设置在name属性中的名称)。

9.5 使用分布式跟踪

问题

您希望对整个应用程序进行性能分析和监控。

解决方案

MicroProfile OpenTracing 规范使用OpenTracing 标准 API来为分布式跟踪的微服务进行工具化。Quarkus 集成了 MicroProfile OpenTracing 规范作为分布式跟踪的解决方案。

分布式跟踪是一种用于分析和监控分布式系统的方法。它可以用于检测服务之间通讯的故障,确定哪些点是性能问题,或者记录网络网格内发生的所有请求和响应的日志记录。

在进行分布式跟踪之前,您必须了解 OpenTracing 中的五个重要概念:

跨度

代表已完成的工作单位(例如,执行的服务)的命名操作。一个 span 可能包含更多以子父形式的 span。

Span 上下文

从服务传播到服务的跟踪信息(例如,span ID)。

行李物品

从服务传播到服务的自定义键/值对。

标签

用户定义的键/值对被设置在 span 中,以便可以查询和过滤(例如,http.status_code)。

日志

与包含日志消息或其他重要信息的跨度相关联的键/值对。日志用于标识跨度中的特定时刻;与此同时,标签独立于时间应用于整个 span。

本示例中,使用Jaeger服务器来收集应用程序的所有跟踪,并使其可供消费或查询。图 9-1 显示了服务和 Jaeger 之间的交互。

qucb 0902

图 9-1. 微服务和 Jaeger

在前一段中解释的 Jaeger 概念在图 9-2 中有所说明。

qucb 0901

图 9-2. Jaeger 概念

使用jaegertracing/all-in-one容器映像,因为它包含所有 Jaeger 后端组件和 UI 的单个映像。这并不是用于生产,但为了简单起见,这是配方 9.5 中使用的图像:

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

要在 Quarkus 应用中启用 MicroProfile OpenTracing,您只需要注册quarkus-smallrye-opentracing扩展。

./mvnw quarkus:add-extension -Dextensions="quarkus-smallrye-opentracing"

在类路径中添加扩展后,Quarkus/MicroProfile OpenTracing 将默认的跟踪信息发送到 Jaeger 服务器。你只需配置 Jaeger 端点,即可发送所有跟踪信息。

默认收集的跟踪信息包括以下内容:

  • 硬件指标如 CPU、内存和可用处理器。

  • JVM 指标如内存堆和线程池。

MicroProfile OpenTracing 为每个传入请求创建一个新的 span。这个新 span 的默认名称是 *HTTP method*:*package name*.*class name*.*method name*

为传入请求创建的 spans 将包含以下标签及其正确的值:

  • Tags.SPAN_KIND = Tags.SPAN_KIND_SERVER

  • Tags.HTTP_METHOD 表示传入请求中使用的 HTTP 方法

  • Tags.HTTP_URL 表示传入端点的 URL

  • Tags.HTTP_STATUS 表示 HTTP 状态结果代码

  • Tags.COMPONENT = "jaxrs"

  • 如果发生服务器错误(5XX 错误代码),将 Tags.ERROR 设置为 true;如果异常有提供对象,则添加两个日志,一个带有 event=error,另一个带有 error.object=<error object instance>

对于出站请求,如果存在当前活动 span,则创建一个新的子 span。新 span 的默认名称是 <HTTP method>。为传出请求创建的 spans 将包含以下标签及其正确的值:

  • Tags.SPAN_KIND=Tags.SPAN_KIND_SCLIENT

  • Tags.HTTP_METHOD 表示传出请求中使用的 HTTP 方法

  • Tags.HTTP_URL 表示传出端点的 URL

  • Tags.HTTP_STATUS 表示 HTTP 状态结果代码

  • Tags.COMPONENT = "jaxrs"

  • 如果发生客户端错误(4XX 错误代码),将 Tags.ERROR 设置为 true;如果异常有提供对象,则添加两个日志,一个带有 event=error,另一个带有 error.object=<error object instance>

最后要做的事情是配置 Jaeger 参数:

quarkus.jaeger.service-name=shopping-cart ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.jaeger.sampler-type=const ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.jaeger.sampler-param=1 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.jaeger.endpoint=http://localhost:14268/api/traces ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

1

在 Jaeger 中识别的服务名称

2

设置一个采样器

3

需要采样的请求百分比(1 表示全部采样)

4

Jaeger 服务器位置

然后启动应用程序,并向服务中定义的一个端点发送一些请求。之后,访问 Jaeger UI 来检查所有的分布式跟踪信息。打开浏览器,访问 http://localhost:16686(Jaeger UI)以查看跟踪信息。

在初始页面中,可以通过多个参数进行筛选,但其中一个用于选择将用于查看已完成请求的服务。

Jaeger 的首页显示在 图 9-3 中。

qucb 0903

图 9-3. Jaeger 的首页

点击查找跟踪按钮以选择符合给定条件的所有跟踪,你将看到 图 9-4 中显示的图像。

qucb 0904

图 9-4. 寻找迹象

您将看到符合条件的所有请求。在本例中,请求都是涉及 shopping-cart 服务的所有追踪,如 图 9-5 所示。

qucb 0903

图 9-5. 查看追踪

如果单击任何请求,将显示特定请求的更多详细信息,如 图 9-6 所示。

qucb 0906

图 9-6. 请求的详细信息

在发生错误时,会添加一个新的日志条目,其中设置了错误消息,如 图 9-7 所示。

qucb 0907

图 9-7. 错误日志消息

禁用追踪

默认情况下,所有请求(传入或传出)都会被跟踪。通过使用 @org.eclipse.microprofile.opentracing.Traced 对其进行注释,可以禁用特定类或方法的跟踪:

@Traced(false)
public class TransactionResource {}

讨论

通过在 src/main/resources/application.properties 文件中设置选项或使用 第 9.6 节 中讨论的任何其他方法配置 OpenTracing。一些最重要的配置属性列在 表 9-1 中。

表 9-1. OpenTracing 配置属性

属性 描述

|

quarkus.jaeger.enabled

|

定义 Jaeger 扩展是否启用(默认为true)。这是一个构建属性,无法在运行时修改。

|

|

quarkus.jaeger.endpoint

|

追踪服务器端点。

|

|

quarkus.jaeger.auth-token

|

发送到端点的身份验证令牌。

|

|

quarkus.jaeger.user

|

发送作为身份验证的用户名到端点。

|

|

quarkus.jaeger.password

|

发送作为身份验证的一部分的密码。

|

|

quarkus.jaeger.sampler-type

|

采样器类型(constprobabilisticratelimitingremote)。

|

|

quarkus.jaeger.sampler-param

|

流量采样的百分比(0.0-1.0)。

|

|

quarkus.jaeger.service-name

|

服务名称。

|

|

quarkus.jaeger.tags

|

一个逗号分隔的键/值标签列表,这些标签将添加到所有跨度中。支持使用 ${environmentVar:default} 使用环境变量。

|

|

quarkus.jaeger.propagation

|

用于传播跟踪上下文的格式(默认为 jaeger)。可能的值为 jaegerb3

|

|

quarkus.jaeger.sender-factory

|

发送方工厂类名。

|

另请参阅

查看Quarkus 使用 OpenTracing 的指南以获取支持的属性的完整列表。

关于 MicroProfile OpenTracing 规范的更多信息可以在 GitHub 上的以下页面找到:

9.6 自定义分布式跟踪

问题

您想要在当前跟踪跨度中添加自定义信息。

解决方案

MicroProfile OpenTracing 规范使用 io.opentracing.Tracer 类来在当前跨度中添加新信息。

在某些情况下,需要创建一个新的子 span 或者在当前 span 中添加信息,比如新标签、日志信息或者装备项。为了添加这些信息,MicroProfile OpenTracing 会生成一个io.opentracing.Tracer类的实例来操作当前 span。

假设你想要标记所有由重要客户发出的请求。例如,重要客户的 ID 以1开头:

@Inject ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
Tracer tracer;
@POST
@Path("/add/{customerId}")
@Transactional
@Consumes(MediaType.APPLICATION_JSON)
public Response addItem(@PathParam("customerId") String customerId, Item item) {

    if (customerId.startsWith("1")) {
        tracer.activeSpan().setTag("important.customer", true); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    }
}

1

注入一个Tracer实例

2

在当前 span 中创建一个新标签

然后任何重要客户的请求都会相应地被标记。

自定义标签如图 Figure 9-8 所示。

qucb 0908

图 9-8. 自定义标签

讨论

Quarkus 支持 OpenTracing 的一种自定义方式来为 JDBC 提供工具,因此如果您想要监控 SQL 查询,无需自己定制当前 span;您可以使用以依赖形式提供的集成。

opentracing-jdbc工件注册到你的构建工具中:

<dependency>
    <groupId>io.opentracing.contrib</groupId>
    <artifactId>opentracing-jdbc</artifactId>
</dependency>

然后激活 JDBC 连接的跟踪。通过在 JDBC URL 中添加tracing来完成。因为 Quarkus 使用 JPA,您还需要配置数据源和 Hibernate 以使用专用跟踪驱动程序:

quarkus.datasource.url=jdbc:tracing:h2:mem:mydb ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.datasource.driver=io.opentracing.contrib.jdbc.TracingDriver ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.datasource.username=sa
quarkus.datasource.password=
quarkus.hibernate-orm.database.generation=update
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

使用tracing更新 JDBC URL

2

使用TracingDriver代替数据库驱动程序

3

配置真实数据库的方言

所有在请求中执行的查询也反映在 Jaeger UI 中。

JDBC 跟踪显示如图 Figure 9-9 所示。

qucb 0909

图 9-9. JDBC 跟踪

如果仔细观察截图,您会注意到有一个名为db.statement的新标签,反映了已被跟踪的查询。还请注意,有一个shopping-cart span 同时包含了其他六个更多的 spans,每个查询对应一个。

要忽略特定查询,可以设置(多次)ignoreForTracing属性,并提供要忽略的查询(例如,jdbc:tracing:h2:mem:test?ignoreForTracing=SELECT * FROM \"TEST\")。

第十章:集成 Kubernetes

到目前为止,您一直在学习如何在裸机上开发和运行 Quarkus 应用程序,但 Quarkus 真正发光的地方是在 Kubernetes 集群中运行时。

在本章中,您将了解 Quarkus 与 Kubernetes 之间的集成,以及如何使用多个扩展来帮助开发和部署 Quarkus 服务到 Kubernetes。

Kubernetes 正在成为部署应用程序的事实标准平台;因此,了解 Kubernetes 及如何在其上正确开发和部署应用程序至关重要。

在本章中,您将学习如何完成以下任务:

  • 构建并推送容器镜像。

  • 生成 Kubernetes 资源

  • 部署一个 Quarkus 服务

  • 开发 Kubernetes 运算符。

  • 在 Knative 中部署一个服务。

10.1 构建和推送容器镜像

问题

您希望构建并推送容器镜像。

解决方案

Kubernetes 中的工作单元是一个 pod。Pod 表示在同一主机上运行并共享 IP 和端口等资源的一组容器。要将服务部署到 Kubernetes,您需要创建一个 pod。由于 pod 由一个或多个容器组成,因此需要构建服务的容器镜像。

Quarkus 提供了用于构建和可选推送容器镜像的扩展。在撰写本文时,支持以下容器构建策略:

Jib

Jib 为您的 Java 应用程序构建 Docker 和 OCI 容器镜像,无需 Docker 守护程序(无 Docker)。这使得在容器内部运行进程时构建 Docker 镜像变得非常完美,因为您避免了 Docker-in-Docker (DinD) 进程的麻烦。此外,使用 Jib 与 Quarkus 将所有依赖项缓存到与实际应用程序不同的层中,使重建速度快且节省空间。这也改善了推送和构建时间。

Docker

使用 Docker 策略构建容器镜像,使用本地安装的 docker 二进制文件,默认情况下使用位于 src/main/docker 下的 Dockerfiles 来构建镜像。

S2I

Source-to-Image (S2I) 策略使用 s2i 二进制文件在 OpenShift 集群内执行容器构建。S2I 构建需要创建 BuildConfig 和两个 ImageStream 资源。这些资源的创建由 Quarkus Kubernetes 扩展来实现。

在本教程中,我们将使用 Jib 构建和推送容器;本教程的“讨论”部分将涉及 Docker 和 S2I。

要使用 Jib 构建并推送容器镜像,首先需要添加 Jib 扩展。

./mvnw quarkus:add-extensions -Dextensions="quarkus-container-image-jib"

然后,您可以定制容器镜像构建过程。您可以在 application.properties、系统属性或环境变量中设置这些属性,就像在 Quarkus 中设置任何其他配置参数一样:

quarkus.container-image.group=lordofthejars ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.container-image.registry=quay.io ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.container-image.username=lordofthejars ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
#quarkus.container-image.password= ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

1

设置镜像的组部分;默认情况下为 ${user.name}

2

镜像推送的注册表;默认情况下,镜像被推送到docker.io

3

登录容器注册表的用户名

4

登录容器注册表的密码

要为项目构建并推送容器镜像,您需要将quarkus.container-image.push参数设置为true,并且在package阶段期间,将创建并推送容器:

./mvnw clean package -Dquarkus.container-image.push=true 
... [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ greeting-jib --- [INFO] Building jar: /greeting-jib/target/greeting-jib-1.0-SNAPSHOT.jar [INFO] [INFO] --- quarkus-maven-plugin:1.3.0.CR2:build (default) @ greeting-jib --- [INFO] [org.jboss.threads] JBoss Threads version 3.0.1.Final [INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building thin jar:
 greeting-jib/target/greeting-jib-1.0-SNAPSHOT-runner.jar [INFO] [io.quarkus.container.image.jib.deployment.JibProcessor]
 Starting container image build ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
[WARNING] [io.quarkus.container.image.jib.deployment.JibProcessor]
 Base image 'fabric8/java-alpine-openjdk8-jre' does not use a specific image digest - build may not be reproducible [INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] LogEvent
 [level=INFO, message=trying docker-credential-desktop for quay.io] [INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] LogEvent
 [level=LIFECYCLE, message=Using credentials from Docker config ($HOME/.docker/config.json) for quay.io/lordofthejars/greeting-jib:1.0-SNAPSHOT] [INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] The base image
 requires auth. Trying again for fabric8/java-alpine-openjdk8-jre... [INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Using base
 image with digest: sha256:a5d31f17d618032812ae85d12426b112279f02951fa92a7ff8a9d69a6d3411b1 [INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Container
 entrypoint set to [java, -Dquarkus.http.host=0.0.0.0, -Djava.util.logging.manager=org.jboss.logmanager.LogManager, -cp, /app/resources:/app/classes:/app/libs/*, io.quarkus.runner.GeneratedMain] [INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Pushed container image quay.io/lordofthejars/greeting-jib:1.0-SNAPSHOT (sha256:e173e0b49bd5ec1f500016f46f2cde03a055f558f72ca8ee1d6cb034a385a657)![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed
 in 12980ms

1

容器镜像已构建

2

容器镜像被推送到 quay.io

讨论

除了 Jib 之外,还有两个其他选项可用于构建容器镜像;要使用它们,您只需注册该扩展:

Docker

quarkus-container-image-docker

S2I

quarkus-container-image-s2i

每个扩展都提供特定的配置参数来修改构建过程。这些参数允许您更改用于构建容器镜像的基础镜像,并允许您设置环境变量、传递给可执行文件的参数或 Dockerfile 的位置。

您还可以构建镜像,但不将其推送到注册表。为此,您需要将quarkus.container-image.build属性设置为true,并且不设置quarkus.container-image.push属性:

./mvnw clean package -Dquarkus.container-image.build=true
重要提示

如果使用了Jib并且将push设置为false,该扩展将创建一个容器镜像并将其注册到 Docker 守护程序。这意味着虽然不使用 Docker 构建镜像,但它仍然是必要的。

容器镜像扩展可以从build/output目录中的 JAR 包(用于 JVM 模式)或本地可执行文件创建容器,具体取决于目录中的内容。如果您希望创建一个可以在 Linux 容器中运行的本地可执行文件,并在其中创建包含生成的本地可执行文件的容器镜像,您可以运行以下命令:

./mvnw clean package -Dquarkus.container-image.push=true -Pnative \
 -Dquarkus.native.container-build=true

quarkus.native.container-build属性设置为true,可以在 Docker 容器内创建本地可执行文件。

参见

欲获取更多信息,请访问 GitHub 上的以下页面:

10.2 生成 Kubernetes 资源

问题

您希望自动生成 Kubernetes 资源。

解决方案

Quarkus 具有一个 Kubernetes 扩展,能够根据合理的默认值和可选的用户提供的配置自动生成 Kubernetes 资源。目前,该扩展可以为 Kubernetes 和 OpenShift 生成资源。

要启用生成 Kubernetes 资源的功能,您需要注册quarkus-kubernetes扩展:

./mvnw quarkus:add-extension -Dextensions="quarkus-kubernetes"

要生成 Kubernetes 资源,请在新的终端中执行 ./mvnw package。然后在 target 目录中,构建工具通常生成的文件中,会在 target/kubernetes 目录下生成两个新文件。这些新文件分别命名为 kubernetes.jsonkubernetes.yaml,每个文件都包含 DeploymentService 的定义:

{
  "apiVersion" : "v1",
  "kind" : "List",
  "items" :  {
    "apiVersion" : "v1",
    "kind" : "Service",
    "metadata" : {
      "labels" : {
        "app" : "getting-started", ![1        "version" : "1.0-SNAPSHOT", ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        "group" : "alex" ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
      },
      "name" : "getting-started"
    },
    "spec" : {
      "ports" : [ {
        "name" : "http",
        "port" : 8080,
        "targetPort" : 8080
      } ],
      "selector" : {
        "app" : "getting-started",
        "version" : "1.0-SNAPSHOT",
        "group" : "alex"
      },
      "type" : "ClusterIP"
    }
  }, {
    "apiVersion" : "apps/v1",
    "kind" : "Deployment",
    "metadata" : {
      "labels" : {
        "app" : "getting-started",
        "version" : "1.0-SNAPSHOT",
        "group" : "alex"
      },
      "name" : "getting-started"
    },
    "spec" : {
      "replicas" : 1,
      "selector" : {
        "matchLabels" : {
          "app" : "getting-started",
          "version" : "1.0-SNAPSHOT",
          "group" : "alex"
        }
      },
      "template" : {
        "metadata" : {
          "labels" : {
            "app" : "getting-started",
            "version" : "1.0-SNAPSHOT",
            "group" : "alex"
          }
        },
        "spec" : {
          "containers" : [ {
            "env" : [ {
              "name" : "KUBERNETES_NAMESPACE",
              "valueFrom" : {
                "fieldRef" : {
                  "fieldPath" : "metadata.namespace"
                }
              }
            } ],
            "image" : "alex/getting-started:1.0-SNAPSHOT",
            "imagePullPolicy" : "IfNotPresent",
            "name" : "getting-started",
            "ports" : [ {
              "containerPort" : 8080,
              "name" : "http",
              "protocol" : "TCP"
            } ]
          } ]
        }
      }
    }
  } ]
}

1

默认为项目名称

2

默认为版本字段

3

默认为操作系统用户名

讨论

您可以通过将这些属性添加到 application.properties 中,自定义生成清单中使用的组和名称:

quarkus.container-image.group=redhat
quarkus.application.name=message-app

Kubernetes 扩展允许向清单的不同部分提供用户自定义:

quarkus.kubernetes.replicas=3 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

quarkus.container-image.registry=http://my.docker-registry.net ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.kubernetes.labels.environment=prod ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

quarkus.kubernetes.readiness-probe.initial-delay-seconds=10 ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
quarkus.kubernetes.readiness-probe.period-seconds=30

1

设置副本数

2

添加 Docker 注册表以拉取镜像

3

添加新的标签

4

设置就绪性探针

您可以通过在 application.properties 文件中设置属性 quarkus.kubernetes.deployment-target 或作为系统属性来生成不同的资源。

该属性的默认值是 kubernetes,但在撰写时还支持以下值:kubernetesopenshiftknative

参见

以下网页列出了所有 Kubernetes 配置选项,以修改生成的文件:

10.3 使用健康检查生成 Kubernetes 资源

问题

您希望自动创建带有存活检查和就绪检查的 Kubernetes 资源。

解决方案

默认情况下,输出文件中不会生成健康检查探针,但如果存在 quarkus-smallrye-health 扩展(如 配方 9.1 中所述),则会自动生成就绪性和存活性探针部分:

"image" : "alex/getting-started:1.0-SNAPSHOT",
"imagePullPolicy" : "IfNotPresent",
"livenessProbe" : { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    "failureThreshold" : 3,
    "httpGet" : {
        "path" : "/health/live", ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        "port" : 8080,
        "scheme" : "HTTP"
        },
    "initialDelaySeconds" : 0,
    "periodSeconds" : 30,
    "successThreshold" : 1,
    "timeoutSeconds" : 10
},
"name" : "getting-started",
"ports" : [ {
    "containerPort" : 8080,
    "name" : "http",
    "protocol" : "TCP"
    } ],
"readinessProbe" : { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    "failureThreshold" : 3,
    "httpGet" : {
        "path" : "/health/ready", ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
        "port" : 8080,
        "scheme" : "HTTP"
    },
    "initialDelaySeconds" : 0,
    "periodSeconds" : 30,
    "successThreshold" : 1,
    "timeoutSeconds" : 10
}

1

定义存活性探针

2

路径由 MicroProfile Health 规范定义

3

定义就绪性探针

4

路径由 MicroProfile Health 规范定义

讨论

Kubernetes 使用探针来确定服务的健康状态,并自动采取行动解决任何问题。

Quarkus 自动为 Kubernetes 生成两个探针:

存活性检查

Kubernetes 使用存活性探针来确定是否必须重新启动服务。如果应用程序变得无响应,可能是因为死锁或内存问题,重新启动容器可能是解决问题的良好解决方案。

就绪性检查

Kubernetes 使用就绪探针来确定服务是否可以接受流量。有时,服务在接受请求之前可能需要执行一些操作。例如更新本地缓存系统、将更改应用到数据库架构、应用批处理过程或连接到像 Kafka Streams 这样的外部服务。

参见

欲了解更多信息,请访问以下网站:

10.4 在 Kubernetes 上部署服务

问题

您希望在 Kubernetes 上部署服务。

解决方案

使用kubectl和 Quarkus 提供的所有功能在 Kubernetes 上创建和部署服务。

使用以下步骤,Quarkus 使得创建并在 Kubernetes 上部署 Java 应用程序变得非常简单:

  1. 如 Recipe 6.6 中所述,生成企业应用程序的容器本地可执行文件。

  2. 提供Dockerfile.native文件以构建 Docker 容器

  3. 使用quarkus-kubernetes扩展生成 Kubernetes 资源文件,详见 Recipe 10.2

现在是时候看到所有这些步骤如何一起运作了。

若要创建可在容器内运行的本机可执行文件:

./mvnw package -DskipTests -Pnative -Dquarkus.native.container-build=true ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

docker build -f src/main/docker/Dockerfile.native \
 -t alex/geeting-started:1.0-SNAPSHOT . ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
docker push docker build -f src/main/docker/Dockerfile.native \
 -t alex/getting-started:1.0-SNAPSHOT . ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

kubectl apply -f target/kubernetes/kubernetes.json ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

kubectl patch svc getting-started --type='json' \
 -p '[{"op":"replace","path":"/spec/type","value":"NodePort"}]' ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)

curl $(minikube service getting-started --url)/hello ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)

1

在 Docker 容器内创建本机可执行文件

2

使用之前生成的本机可执行文件创建 Docker 镜像

3

将镜像推送到 Docker 注册表(在 minikube 中,这是eval $(minikube docker-env),因此无需推送。)

4

将应用程序部署到 Kubernetes

5

更改为NodePort

6

获取访问服务的 URL

请注意,仅因服务部署在 minikube 中,才需要执行第 5 和第 6 步。根据您用于部署服务的 Kubernetes 平台,您可能需要执行不同的操作。

讨论

如果使用多阶段 Docker 构建功能,步骤 1 和 2 可以简化为一个步骤。在第一阶段生成本机可执行文件,第二阶段创建运行时镜像:

FROM quay.io/quarkus/centos-quarkus-maven:19.2.1 AS build ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
USER root
RUN chown -R quarkus /usr/src/app
USER quarkus
RUN mvn -f /usr/src/app/pom.xml -Pnative clean package

FROM registry.access.redhat.com/ubi8/ubi-minimal ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
WORKDIR /work/
COPY --from=build /usr/src/app/target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

1

生成本机可执行文件

2

从上一阶段的输出创建运行时镜像

从项目根目录中删除.dockerignore文件(rm .dockerignore)。这是必要的,因为默认情况下会忽略src目录,而构建本机可执行文件需要src目录:

docker build -f src/main/docker/Dockerfile.multistage -t docker \
 build -f src/main/docker/Dockerfile.multistage -t ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

1

使用捆绑本机可执行文件创建运行时镜像

10.5 在 OpenShift 上部署服务

问题

您希望在 OpenShift 上部署服务。

解决方案

OpenShift 与前一个示例生成的资源完美配合,因此即使您在使用 OpenShift,仍然可以使用之前提供的所有内容。但是,如果您想使用 OpenShift 提供的某些功能,可以将kubernetes.deployment.target属性设置为openshift

生成的两个文件分别位于target/kubernetes/openshift.jsontarget/kubernetes/openshift.yaml

{
  "apiVersion" : "v1",
  "kind" : "List",
  "items" : [ {
    "apiVersion" : "v1",
    "kind" : "Service",
    "metadata" : {
      "labels" : {
        "app" : "getting-started",
        "version" : "1.0-SNAPSHOT",
        "group" : "alex"
      },
      "name" : "getting-started"
    },
    "spec" : {
      "ports" : [ {
        "name" : "http",
        "port" : 8080,
        "targetPort" : 8080
      } ],
      "selector" : {
        "app" : "getting-started",
        "version" : "1.0-SNAPSHOT",
        "group" : "alex"
      },
      "type" : "ClusterIP"
    }
  }, {
    "apiVersion" : "image.openshift.io/v1",
    "kind" : "ImageStream",
    "metadata" : {
      "labels" : {
        "app" : "getting-started",
        "version" : "1.0-SNAPSHOT",
        "group" : "alex"
      },
      "name" : "getting-started"
    }
  }, {
    "apiVersion" : "image.openshift.io/v1",
    "kind" : "ImageStream",
    "metadata" : {
      "labels" : {
        "app" : "getting-started",
        "version" : "1.0-SNAPSHOT",
        "group" : "alex"
      },
      "name" : "s2i-java"
    },
    "spec" : {
      "dockerImageRepository" : "fabric8/s2i-java"
    }
  }, {
    "apiVersion" : "build.openshift.io/v1",
    "kind" : "BuildConfig",
    "metadata" : {
      "labels" : {
        "app" : "getting-started",
        "version" : "1.0-SNAPSHOT",
        "group" : "alex"
      },
      "name" : "getting-started"
    },
    "spec" : {
      "output" : {
        "to" : {
          "kind" : "ImageStreamTag",
          "name" : "getting-started:1.0-SNAPSHOT"
        }
      },
      "source" : {
        "binary" : { }
      },
      "strategy" : {
        "sourceStrategy" : {
          "from" : {
            "kind" : "ImageStreamTag",
            "name" : "s2i-java:2.3"
          }
        }
      }
    }
  }, {
    "apiVersion" : "apps.openshift.io/v1",
    "kind" : "DeploymentConfig",
    "metadata" : {
      "labels" : {
        "app" : "getting-started",
        "version" : "1.0-SNAPSHOT",
        "group" : "alex"
      },
      "name" : "getting-started"
    },
    "spec" : {
      "replicas" : 1,
      "selector" : {
        "app" : "getting-started",
        "version" : "1.0-SNAPSHOT",
        "group" : "alex"
      },
      "template" : {
        "metadata" : {
          "labels" : {
            "app" : "getting-started",
            "version" : "1.0-SNAPSHOT",
            "group" : "alex"
          }
        },
        "spec" : {
          "containers" : [ {
            "env" : [ {
              "name" : "KUBERNETES_NAMESPACE",
              "valueFrom" : {
                "fieldRef" : {
                  "fieldPath" : "metadata.namespace"
                }
              }
            }, {
              "name" : "JAVA_APP_JAR",
              "value" : "/deployments/getting-started-1.0-SNAPSHOT.jar"
            } ],
            "image" : "",
            "imagePullPolicy" : "IfNotPresent",
            "name" : "getting-started",
            "ports" : [ {
              "containerPort" : 8080,
              "name" : "http",
              "protocol" : "TCP"
            } ],
          } ]
        }
      },
      "triggers" : [ {
        "imageChangeParams" : {
          "automatic" : true,
          "containerNames" : [ "getting-started" ],
          "from" : {
            "kind" : "ImageStreamTag",
            "name" : "getting-started:1.0-SNAPSHOT"
          }
        },
        "type" : "ImageChange"
      } ]
    }
  } ]
}

10.6 构建和自动部署容器镜像

问题

您希望自动构建、推送和部署容器镜像。

解决方案

Quarkus 通过container-image扩展提供了构建和推送容器镜像的功能,并通过kubernetes扩展提供了部署到 Kubernetes 的功能。

要构建、推送和部署容器镜像,首先需要添加所需的扩展:

./mvnw quarkus:add-extensions \
    -Dextensions="quarkus-container-image-jib, quarkus-kubernetes"

然后,您可以自定义容器镜像构建过程。您可以在application.properties、系统属性或环境变量中设置这些属性,就像 Quarkus 中的任何其他配置参数一样:

quarkus.container-image.group=lordofthejars ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.container-image.registry=quay.io ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.container-image.username=lordofthejars ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
#quarkus.container-image.password= ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

1

设置镜像的组部分;默认情况下为${user.name}

2

将镜像推送到的注册表;默认情况下,镜像推送到docker.io

3

登录容器注册表的用户名

4

登录容器注册表的密码

最后,使用以下命令将部署到 Kubernetes:

./mvnw clean package -Dquarkus.kubernetes.deploy=true

讨论

请注意,将quarkus.kubernetes.deploy设置为true会隐式地将quarkus.container-image.push属性设置为true,因此您无需手动设置它。

Kubernetes 扩展使用位于~/.kube/config的标准kubectl配置文件,以了解在哪里部署应用程序。

提示

您还可以使用-Pnative -Dquarkus.native.container-build=true标志使用本地编译创建和部署容器镜像。

10.7 从 Kubernetes 配置应用程序

问题

您希望通过 Kubernetes 而不是配置文件来配置应用程序。

解决方案

使用ConfigMaps配置在 Pod 内运行的应用程序。

在此示例中,您将使用ConfigMap和 Kubernetes 扩展配置服务。要启用在Pod中注入ConfigMap的 Kubernetes 资源生成,您需要注册quarkus-kubernetes扩展:

./mvnw quarkus:add-extension -Dextensions="quarkus-kubernetes"

当调用/hello端点时,服务返回greeting.message配置值:

@ConfigProperty(name = "greeting.message")
String message;

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    return "hello " + message;
}

创建包含键值对的ConfigMap资源:

apiVersion: v1
kind: ConfigMap ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
metadata:
    name: greeting-config
data:
    greeting: "Kubernetes" ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

定义ConfigMap类型

2

对于键greeting定义值为Kubernetes

然后,必须通过在终端窗口中运行以下命令将资源应用于 Kubernetes 集群:

kubectl apply -f src/main/kubernetes/config-greeting.yaml

最后,在application.properties中设置 Kubernetes 扩展属性,以便生成的 Kubernetes 部署文件包含将 config map 作为环境变量注入的部分:

greeting.message=local
quarkus.container-image.group=quarkus ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.container-image.name=greeting-app
quarkus.container-image.tag=1.0-SNAPSHOT
quarkus.kubernetes.env-vars.greeting-message.value=greeting ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.kubernetes.env-vars.greeting-message.configmap=greeting-config ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.kubernetes.image-pull-policy=if-not-present

1

配置 Docker 镜像

2

将环境变量设置为覆盖greeting.message属性

3

设置要加载的 config map 资源名称

生成的 Kubernetes 文件将包含一个新条目,定义容器定义中的键值对,称为configMapKeyRef

要部署应用程序,请打开新的终端窗口,打包应用程序,创建 Docker 容器,并应用生成的 Kubernetes 资源:

./mvnw clean package -DskipTests

docker build -f src/main/docker/Dockerfile.jvm \
 -t quarkus/greeting-app:1.0-SNAPSHOT .
kubectl apply -f target/kubernetes/kubernetes.yml

kubectl patch svc greeting-app --type='json' \
 -p '[{"op":"replace","path":"/spec/type","value":"NodePort"}]'
curl $(minikube service greeting-app --url)/hello

讨论

ConfigMap由 Kubernetes 注入到 pod 的容器中,以文件或环境变量的形式组成键值对,使应用程序可以读取并相应地进行配置。通过ConfigMaps,可以将应用程序的配置与业务逻辑解耦,从而使其在不同环境中具备可移植性。

重要

ConfigMaps用于存储和共享非敏感配置属性。

MicroProfile Config 规范允许通过等效的环境变量(大写并将点[.]改为下划线[_])来覆盖任何配置属性。ConfigMap包含配置属性。在application.properties中,配置了 Kubernetes 扩展以生成部署描述符,将这些属性设置为环境变量,因此当在 Kubernetes 集群内启动容器时,使用适用于该集群的特定配置。

参见

要了解更多关于 Kubernetes 中ConfigMaps的信息,请访问以下网站:

10.8 通过 Config 扩展从 Kubernetes 配置应用程序

问题

您希望通过 Kubernetes 而不是配置文件来配置您的应用程序,使用 MicroProfile Config 规范。

解决方案

Quarkus 提供了一个 Kubernetes 配置扩展,可以从 Kubernetes API 服务器中读取 secrets 和 config maps 元素,并使用@ConfigProperty注解进行注入。

要启用生成 Kubernetes 资源,请注册quarkus-kubernetes-config扩展。

该扩展支持注入ConfigMaps,可以是单个键/值形式,也可以是键为文件名的形式(仅支持application.propertiesapplication.yaml),其值是文件的内容。

让我们创建一个带有单个键/值的 config map:

apiVersion: v1
kind: ConfigMap
metadata:
    name: my-config ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
data:
    greeting: "Kubernetes"

1

对于扩展来说,配置名称很重要。

然后注册前述的 ConfigMap 资源:

kubectl apply -f src/main/kubernetes/my-config.yaml

对于此示例,还注册了一个名为 application.propertiesConfigMap 文件。

添加的配置文件包含以下内容:

some.property1=prop1
some.property2=prop2

然后将前述文件注册到名为 my-file-configConfigMap 中:

kubectl create configmap my-file-config \
 --from-file=./src/main/kubernetes/application.properties

在注入这些值之前的最后一步是配置扩展程序以从这些 ConfigMaps 中读取值:

quarkus.kubernetes-config.enabled=true ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.kubernetes-config.config-maps=my-config,my-file-config ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

启用扩展

2

设置 ConfigMap 的名称

这些配置值像其他配置值一样注入:

@ConfigProperty(name = "greeting") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
String greeting;

@ConfigProperty(name = "some.property1") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
String property1;

@ConfigProperty(name = "some.property2")
String property2;

1

注入简单键

2

也注入了 application.properties 文件中的键

要部署应用程序,请打开新的终端窗口,打包应用程序,创建 Docker 容器,并应用生成的 Kubernetes 资源:

./mvnw clean package -DskipTests

docker build -f src/main/docker/Dockerfile.jvm \
 -t quarkus/greeting-app:1.0-SNAPSHOT .
kubectl apply -f target/kubernetes/kubernetes.yml

kubectl patch svc greeting-app-config-ext --type='json' \
 -p '[{"op":"replace","path":"/spec/type","value":"NodePort"}]'

curl $(minikube service greeting-app-config-ext --url)/hello
Kubernetes

curl $(minikube service greeting-app-config-ext --url)/hello/p1
prop1

curl $(minikube service greeting-app-config-ext --url)/hello/p2
prop2

10.9 以编程方式与 Kubernetes 集群交互

问题

您希望以编程方式与 Kubernetes API 服务器进行交互。

解决方案

使用 kubernetes-client 扩展程序开始观察和响应 Kubernetes 资源的更改。

要添加kubernetes-extension,运行以下命令:

./mvnw quarkus:add-extension -Dextensions="kubernetes-client"

连接到 Kubernetes 集群的主要类是 io.fabric8.kubernetes.client.KubernetesClient。扩展程序生成此实例以便在代码中注入。可以使用各种属性配置客户端,将它们设置在 application.properties 中。

此处开发的示例是一个端点,返回给定命名空间上所有部署的 Pod 的名称:

package org.acme.quickstart;

import java.util.List;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.fabric8.kubernetes.client.KubernetesClient;

@Path("/pod")
public class PodResource {

    @Inject ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    KubernetesClient kubernetesClient;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{namespace}")
    public List<String> getPods(@PathParam("namespace") String namespace) {
        return kubernetesClient.pods() ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
                                .inNamespace(namespace) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
                                .list().getItems()
                                .stream()
                                .map(p -> p.getMetadata().getGenerateName()) ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
                                .collect(Collectors.toList());
    }
}

1

KubernetesClient 被注入,就像任何其他 CDI bean 一样

2

选择所有的 Pods

3

来自给定命名空间

4

仅获取 Pod 的生成名称

访问 Kubernetes 的 REST API 推荐的方式是使用 kubectl 代理模式,因为这样可以避免中间人攻击。

另一种方法是直接提供位置和凭据,但为了避免中间人攻击,可能需要导入根证书。

因为代理模式是推荐的方式,所以本示例中使用了此方法。

指向应用程序必须连接的集群的 kubectl,打开新的终端窗口并运行以下命令:

kubectl proxy --port=8090

此命令以反向代理方式运行 kubectl,在 http://localhost:8090 上公开远程 Kubernetes API 服务器。

配置 KubernetesClient 以通过 application.properties 中的 quarkus.kubernetes-client.master-url 属性连接到 http://localhost:8090

%dev.quarkus.kubernetes-client.master-url=http://localhost:8090 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

1

设置 Kubernetes API 服务器的 URL

最后,运行服务并请求/pod/default端点,以获取在默认命名空间中部署的所有 pod:

./mvnw compile quarkus:dev

curl http://localhost:8080/pod/default
["getting-started-5cd97ddd4d-"]

讨论

在某些情况下,您需要以编程方式创建新的 Kubernetes 资源或获取有关 Kubernetes 集群/资源的一些信息(已部署的 pod、配置参数、设置密码等)。kubernetes-client真正出色的地方在于在 Java 中实现Kubernetes Operator。由于 Quarkus 的能力可以生成本机可执行文件,这是在 Java 中实现 Kubernetes Operator 的绝佳方式。

在本例中,服务是在 Kubernetes 集群外部部署的,并且您使用 Kubernetes API 服务器连接到它。

如果部署了需要访问的 Kubernetes 集群中的服务,则必须将quarkus.kubernetes-client.master-url属性设置为https://kubernetes.default.svc

可以通过简单声明返回已配置KubernetesClient实例的 CDI 提供程序工厂方法来重写KubernetesClient的创建:

@ApplicationScoped
public class KubernetesClientProducer {

    @Produces
    public KubernetesClient kubernetesClient() {
        Config config = new ConfigBuilder()
                              .withMasterUrl("https://mymaster.com")
                              .build(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
        return new DefaultKubernetesClient(config); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    }
}

1

配置客户端

2

创建KubernetesClient的实例

在大多数情况下,要访问 Kubernetes API 服务器,需要ServiceAccountRoleRoleBinding。以下可能是在本节提供的示例中工作的起点:

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: greeting-started
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: greeting-started
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: greeting-started
  namespace: default
roleRef:
  kind: Role
  name: greeting-started
  apiGroup: rbac.authorization.k8s.io
subjects:
  - kind: ServiceAccount
    name: greeting-started
    namespace: default

参见

要了解有关 Fabric8 Kubernetes 客户端的更多信息,请访问 GitHub 上的以下页面:

10.10 测试 Kubernetes 客户端交互

问题

您想要测试 Kubernetes 客户端代码。

解决方案

Quarkus 实现了一个 Quarkus 测试资源,用于启动 Kubernetes API 服务器的模拟,并设置正确的配置以使 Kubernetes 客户端使用模拟服务器实例而不是application.properties中提供的值。此外,您可以设置模拟服务器以响应任何特定测试所需的任何 canned 请求:

package org.acme.quickstart;

import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
import io.fabric8.kubernetes.api.model.PodListBuilder;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.kubernetes.client.KubernetesMockServerTestResource;
import io.quarkus.test.kubernetes.client.MockServer;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
@QuarkusTestResource(KubernetesMockServerTestResource.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class PodResourceTest {

    @MockServer ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    KubernetesMockServer mockServer;

    @BeforeEach ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    public void prepareKubernetesServerAPI() {
        final Pod pod1 = new PodBuilder()
                .withNewMetadata()
                .withName("pod1")
                .withNamespace("test")
                .withGenerateName("pod1-12345")
                .and()
                .build(); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

        mockServer
                .expect()
                  .get()
                    .withPath("/api/v1/namespaces/test/pods")
                    .andReturn(200, new PodListBuilder()
                        .withNewMetadata()
                        .withResourceVersion("1")
                        .endMetadata()
                        .withItems(pod1).build()) ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
                .always();

    }

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/pod/test")
          .then()
             .statusCode(200)
             .body(is("[\"pod1-12345\"]"));
    }

}

1

设置 Kubernetes 测试资源模拟服务器

2

注入 Kubernetes 模拟服务器实例以记录任何交互

3

为了保持测试隔离性,在每次测试之前,再次记录交互

4

构建要返回的 pod

5

通过查询test命名空间中的所有 pod,返回一个 pod 作为结果

10.11 实现 Kubernetes 运算符

问题

您想要实现一个 Kubernetes 运算符,以使用自定义资源扩展 Kubernetes 以管理 Java 中的应用程序。

解决方案

使用kubernetes-client扩展和 Quarkus 在 Java 中实现 Kubernetes 运算符,并将其编译为本机可执行文件。

操作员的一个用例是创建一个模板(自定义资源),其中在创建时设置一些值。文件模板和操作员之间最大的区别在于,通用内容(在模板的情况下)是静态的,而在操作员中是以编程方式设置的,这意味着您可以自由地动态更改公共部分的定义。这被称为自定义资源,其中,您不是使用一个众所周知的 Kubernetes 资源,而是使用您自己的字段实现自己的自定义 Kubernetes 资源。

另一个用例可能是在集群内部发生某事时做出反应/操作。假设您在集群上部署了一些内存数据网格,并且其中一个实例停止运行。也许在这种情况下,您希望通知所有存活的实例,数据网格集群的一个元素已经停止。

正如您所看到的,这不仅仅是创建资源,还涉及到应用一些特定于您的应用程序的任务,这些任务需要在 Kubernetes 已经在执行的任务之上执行。

Kubernetes 操作员使用 Kubernetes API 来决定何时以及如何运行其中一些定制化内容。

从逻辑实现的角度来看,以下简单示例并没有太多意义,但它将帮助您理解编写 Kubernetes 操作员的基础知识。将其用作实现自己的 Kubernetes 操作员的起点。

要编写 Kubernetes 操作员,可能需要以下元素:

  1. 解析自定义资源的类。

  2. 注册并生成客户端以操作自定义资源的工厂方法。

  3. 当将自定义资源应用于集群时,反应的观察者。您可以将其视为操作员控制器或操作员实现。

  4. 具有所有先前代码的 Docker 镜像。

  5. 用于定义自定义资源(CustomResourceDefinition)的 YAML/JSON 文件。

  6. 部署文件以部署自定义操作员。

让我们实现一个简单的 Kubernetes 操作员,配置在容器中运行的命令,并使用此配置实例化 pod。

用于示例的基础镜像是 Whalesay,它基本上会在容器控制台中打印您在run命令中传递的消息,就像这样:

docker run docker/whalesay cowsay boo
 _____
< boo >
 -----
 \
 \
 \
 ##        .
 ## ## ##       ==
 ## ## ## ##      ===
 /""""""""""""""""___/ ===
 ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~
 \______ o          __/
 \    \        __/
 \____\______/

使用此镜像的 pod 资源的示例可能如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: whalesay
spec:
  containers:
  - name: whalesay
    image: docker/whalesay
    imagePullPolicy: "IfNotPresent"
    command: ["cowsay","Hello Alex"] ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

1

设置输出消息

此操作员的目标是只需提供要打印的消息。其余内容(例如 Docker 镜像、容器配置等)将由 Kubernetes 操作员自动设置。

要创建自定义操作员,需要 Kubernetes 客户端和 Jackson 依赖项:

./mvnw quarkus:add-extension \
-Dextensions="io.quarkus:quarkus-kubernetes-client, io.quarkus:quarkus-jackson"

首先要做的是定义自定义资源的外观。例如,对于这个示例,它看起来如下所示:

apiVersion: acme.org/v1alpha1
kind: Hello ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
metadata:
  name: example-hello
spec:
  message: Hello Alex ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

使用自定义 kind 架构(稍后在本文档中定义)

2

设置要打印的消息

需要对象模型来解析自定义资源。 在这种情况下,使用 Jackson 库将 YAML 映射到 Java 对象。 需要三个类,一个用于整个资源,另一个用于 spec 部分,另一个用于 status 部分,后者为空但是需要,因为集群可能会自动填充它。

org.acme.quickstart.cr 包内的 src/main/java 中创建它们:

package org.acme.quickstart.cr;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import io.fabric8.kubernetes.client.CustomResource;

@JsonDeserialize ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class HelloResource extends CustomResource { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

    private HelloResourceSpec spec; ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    private HelloResourceStatus status; ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

    public HelloResourceStatus getStatus() {
        return status;
    }

    public void setStatus(HelloResourceStatus status) {
        this.status = status;
    }

    public HelloResourceSpec getSpec() {
        return spec;
    }

    public void setSpec(HelloResourceSpec spec) {
        this.spec = spec;
    }

    @Override
    public String toString() {
        return "name=" + getMetadata().getName()
                + ", version=" + getMetadata().getResourceVersion()
                + ", spec=" + spec;
    }
}

1

将 POJO 设置为可反序列化

2

继承通用自定义资源字段,如 kindapiVersionmetadata

3

自定义 spec 部分

4

status 部分

spec 部分映射如下:

package org.acme.quickstart.cr;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

@JsonDeserialize
public class HelloResourceSpec {

    @JsonProperty("message") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return "HelloResourceSpec [message=" + message + "]";
    }

}

1

自定义规格仅包含一个 message 字段

空的 status 部分映射如下:

package org.acme.quickstart.cr;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

@JsonDeserialize
public class HelloResourceStatus {
}

从模型观点仍然需要两个类。

当不仅仅是将单个自定义资源(如前所示)应用于集群时,而是提供了自定义资源列表(使用items数组)时,使用 One class

package org.acme.quickstart.cr;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import io.fabric8.kubernetes.client.CustomResourceList;

@JsonDeserialize
public class HelloResourceList extends CustomResourceList<HelloResource> { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
}

1

CustomResourceList 继承了支持自定义资源列表的所有必需字段

另一个类用于从操作员实现中使自定义资源可编辑:

package org.acme.quickstart.cr;

import io.fabric8.kubernetes.api.builder.Function;
import io.fabric8.kubernetes.client.CustomResourceDoneable;

public class HelloResourceDoneable
    extends CustomResourceDoneable<HelloResource> { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    public HelloResourceDoneable(HelloResource resource, Function<HelloResource,
                                 HelloResource> function) {
        super(resource, function);
    }
}

1

CustomResourceDoneable 类使资源可编辑

接下来所需的另一件重要事情是一个 CDI 工厂 bean,它提供了操作员所需的所有机制。 在 src/main/javaorg.acme.quickstart 包中创建此类:

package org.acme.quickstart;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import javax.enterprise.inject.Produces;
import javax.inject.Named;
import javax.inject.Singleton;

import org.acme.quickstart.cr.HelloResource;
import org.acme.quickstart.cr.HelloResourceDoneable;
import org.acme.quickstart.cr.HelloResourceList;

import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.fabric8.kubernetes.internal.KubernetesDeserializer;

public class KubernetesProducer {

  @Produces
  @Singleton
  @Named("namespace")
  String findMyCurrentNamespace() throws IOException { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    return new String(Files.readAllBytes(
          Paths
            .get("/var/run/secrets/kubernetes.io/serviceaccount/namespace")));
  }

  @Produces
  @Singleton
  KubernetesClient makeDefaultClient(@Named("namespace") String namespace) {
    return new DefaultKubernetesClient().inNamespace(namespace); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
  }

  @Produces
  @Singleton
  MixedOperation<HelloResource,
                 HelloResourceList,
                 HelloResourceDoneable,
                 Resource<HelloResource, HelloResourceDoneable>>
  makeCustomHelloResourceClient(KubernetesClient defaultClient) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    KubernetesDeserializer
        .registerCustomKind("acme.org/v1alpha1",
                            "Hello", HelloResource.class); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
    CustomResourceDefinition crd = defaultClient.customResourceDefinitions()
                                      .list()
                                      .getItems()
                                      .stream()
                                      .findFirst()
      .orElseThrow(RuntimeException::new); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
    return defaultClient.customResources(crd, HelloResource.class,
        HelloResourceList.class,
        HelloResourceDoneable.class); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
  }
}

1

获取操作员正在运行的命名空间

2

配置 KubernetesClient 与当前命名空间;默认适用于 Kubernetes Operator 开发

3

MixedOperation 用于监视有关自定义资源的事件(例如,应用新的自定义资源时)

4

注册要由 org.acme.quickstart.cr.HelloResource 解析的 apiVersionKind

5

获取自定义资源的定义;因为只有一个(即我们正在开发的),可以使用 findFirst

6

注册客户端资源、解析器、列表解析器和 doneable

最后需要实现的 Java 类是控制器。这个控制器(或者观察者/操作者)负责检查集群内部的运行情况,并对订阅的事件做出响应,例如,一个新的 Pod 已经被创建或销毁,或者应用了一个名为Hello的自定义资源。

在这个实现中,控制器会监视当一个新的Hello资源被添加时。当自定义资源被应用时,然后从模型中检索消息,并使用 Kubernetes 客户端 API 提供的所有构建器创建 Pod 定义。最终,将 Pod 部署到 Kubernetes 集群中。

org.acme.quickstart包内的src/main/java中创建这个类:

package org.acme.quickstart;

import java.util.HashMap;
import java.util.Map;

import javax.enterprise.event.Observes;
import javax.inject.Inject;

import org.acme.quickstart.cr.HelloResource;
import org.acme.quickstart.cr.HelloResourceDoneable;
import org.acme.quickstart.cr.HelloResourceList;

import io.fabric8.kubernetes.api.model.ContainerBuilder;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
import io.fabric8.kubernetes.api.model.PodSpecBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.Watcher;
import io.fabric8.kubernetes.client.dsl.MixedOperation;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.quarkus.runtime.StartupEvent;

public class HelloResourceWatcher {

  @Inject
  KubernetesClient defaultClient; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

  @Inject
  MixedOperation<HelloResource,
    HelloResourceList,
    HelloResourceDoneable,
    Resource<HelloResource,
    HelloResourceDoneable>> crClient; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

  void onStartup(@Observes StartupEvent event) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    crClient.watch(new Watcher<HelloResource>() { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
      @Override
      public void eventReceived(Action action, HelloResource resource) {
        System.out.println("Received " + action
            + " event for resource " + resource);
        if (action == Action.ADDED) {
          final String app = resource.getMetadata().getName(); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
          final String message = resource.getSpec().getMessage();

          final Map<String, String> labels = new HashMap<>(); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
          labels.put("app", app);

          final ObjectMetaBuilder objectMetaBuilder =
            new ObjectMetaBuilder().withName(app + "-pod")
            .withNamespace(resource.getMetadata()
                .getNamespace())
            .withLabels(labels);

          final ContainerBuilder containerBuilder =
            new ContainerBuilder().withName("whalesay")
            .withImage("docker/whalesay")
            .withCommand("cowsay", message); ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/7.png)

          final PodSpecBuilder podSpecBuilder =
            new PodSpecBuilder()
            .withContainers(containerBuilder.build())
            .withRestartPolicy("Never");

          final PodBuilder podBuilder =
            new PodBuilder()
            .withMetadata(objectMetaBuilder.build())
            .withSpec(podSpecBuilder.build());

          final Pod pod = podBuilder.build(); ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/8.png)
          HasMetadata result = defaultClient
            .resource(pod)
            .createOrReplace(); ![9](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/9.png)

          if (result == null) {
            System.out.println("Pod " + pod
                + " couldn't be created");
          } else {
            System.out.println("Pod " + pod + " created");
          }
        }
      }

      @Override
      public void onClose(KubernetesClientException e) { ![10](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/10.png)
        if (e != null) {
          e.printStackTrace();
          System.exit(-1);
        }
      }
    });
  }
}

1

注入 KubernetesClient

2

注入特定于开发的自定义资源的操作

3

在应用程序启动时执行逻辑

4

监视任何涉及HelloResource的操作

5

获取自定义资源中提供的信息

6

程序化地开始创建 Pod 定义

7

设置自定义资源提供的消息

8

构建 Pod

9

将 Pod 添加到集群中

10

如果在关闭时出现任何关键错误,则停止容器

讨论

在 Java 端,这就是你需要做的一切;然而,还有一些未完成的部分,例如打包和容器化操作符,或者在集群中定义自定义操作符。

开发 Kubernetes Operator 时首要考虑的事项是与 Kubernetes API 服务器的通信通过 HTTPS 进行,并且这意味着如果默认情况下未提供加密库,则必须在 Docker 镜像中提供它们。

在撰写本文时,Quarkus 提供的Dockerfile.jvm文件中不包含与 Kubernetes 服务器通信所需的加密库。要解决这个问题,只需打开src/main/docker/Dockerfile.jvm文件并添加nss(Network Security Services)包:

FROM fabric8/java-alpine-openjdk8-jre

RUN apk add --no-cache nss

然后通过运行 Maven 和 Docker 将操作符容器化:

./mvnw clean package

docker build -f src/main/docker/Dockerfile.jvm \
 -t lordofthejars/quarkus-operator-example:1.0.0 .

然后将自定义资源定义注册到 Kubernetes 集群中,以便它了解新资源类型、自定义资源的范围或组名称等信息。

创建一个名为custom-resource-definition.yaml的文件,在src/main/kubernetes中定义所有集群注册新资源所需的信息:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: hellos.acme.org ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
spec:
  group: acme.org ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
  names:
    kind: Hello ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    listKind: HelloList ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
    plural: hellos ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
    singular: hello ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
  scope: Namespaced ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/7.png)
  subresources:
    status: {}
  version: v1alpha1 ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/8.png)

1

plural 加上 group

2

设置自定义资源的组(用于自定义资源的 apiVersion 字段)

3

kind 的名称

4

当种类是此自定义资源列表时的名称

5

复数名字

6

单数名字

7

资源的范围

8

资源的版本(用于自定义资源的 apiVersion 字段)

最后要创建的是部署操作者的部署文件。在 src/main/kubernetes 下创建一个名为 deploy.yaml 的新文件:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
metadata:
  name: quarkus-operator-example
rules:
- apiGroups:
  - ''
  resources:
  - pods ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - delete
  - patch
- apiGroups:
  - apiextensions.k8s.io
  resources:
  - customresourcedefinitions
  verbs:
  - list
- apiGroups:
  - acme.org ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
  resources:
  - hellos
  verbs:
  - list
  - watch
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: quarkus-operator-example
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: quarkus-operator-example
subjects:
- kind: ServiceAccount
  name: quarkus-operator-example
  namespace: default
roleRef:
  kind: ClusterRole
  name: quarkus-operator-example
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
metadata:
  name: quarkus-operator-example
spec:
  selector:
    matchLabels:
      app: quarkus-operator-example
  replicas: 1
  template:
    metadata:
      labels:
        app: quarkus-operator-example
    spec:
      serviceAccountName: quarkus-operator-example ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
      containers:
      - image: lordofthejars/quarkus-operator-example:1.0.0 ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
        name: quarkus-operator-example
        imagePullPolicy: IfNotPresent

1

为 Kubernetes 资源的基于角色的访问控制(RBAC)定义了一个集群角色

2

添加了获取、列表、观察、创建、更新、删除和修补 Pod 的权限

3

对于自定义资源(hellos.acme.org),所需的操作是 listwatch

4

操作者使用 Deployment 部署

5

设置与文件中定义的集群角色关联的服务帐户

6

设置包含操作者的容器镜像

在操作者启动之前的最后一步是应用所有这些创建的资源:

kubectl apply -f src/main/kubernetes/custom-resource-definition.yaml
kubectl apply -f src/main/kubernetes/deploy.yaml

kubectl get pods

NAME                                       READY   STATUS    RESTARTS   AGE
quarkus-operator-example-fb77dc468-8v9xk   1/1     Running   0          5s

操作者现已安装并运行。要测试操作者,只需创建一种 Hello 种类的自定义资源,并提供要显示的消息:

apiVersion: acme.org/v1alpha1
kind: Hello ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
metadata:
  name: example-hello
spec:
  message: Hello Alex ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

使用自定义 kind 模式

2

设置要打印的消息

然后按如下方式应用:

kubectl apply -f src/main/kubernetes/custom-resource.yaml

kubectl get pods

NAME                                       READY   STATUS      RESTARTS   AGE
example-hello-pod                          0/1     Completed   0          2m57s
quarkus-operator-example-fb77dc468-8v9xk   1/1     Running     0          3m24s

完成后,请检查 Pod 日志,验证消息是否已打印到控制台上:

kubectl logs example-hello-pod

 ____________
< Hello Alex >
 ------------
 \
 \
 \
 ##        .
 ## ## ##       ==
 ## ## ## ##      ===
 /""""""""""""""""___/ ===
 ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~
 \______ o          __/
 \    \        __/
 \____\______/

尽管操作者和自定义资源通常是相关的,但没有自定义资源定义的操作者仍然是可能的——例如,创建一个监视器类来拦截影响 Pod 的任何事件并应用一些逻辑。

参见

要了解更多关于操作者的信息,请查看以下网站:

10.12 使用 Knative 部署和管理无服务器工作负载

问题

您想要部署和管理无服务器工作负载。

解决方案

使用 Knative,基于 Kubernetes 的平台,部署和管理现代无服务器工作负载。

quarkus-kubernetes 扩展提供了自动生成 Knative 资源的支持,具有合理的默认设置和可选的用户提供的配置。

要启用 Kubernetes 资源的生成,您需要注册 quarkus-kubernetes 扩展:

./mvnw quarkus:add-extension \
 -Dextensions="quarkus-kubernetes, quarkus-container-image-docker"

对于本示例,使用 quarkus-container-image-docker 扩展来使用 docker 二进制文件构建容器镜像,因此镜像直接在 minikube 集群内构建并在内部注册表中注册,因此不需要外部注册表。

您需要运行 eval $(minikube docker-env) 来配置 docker 使用 minikube docker 主机。

然后,您需要将 quarkus.kubernetes.deployment-target 属性设置为 knative,并在打包阶段设置为构建 Docker 容器,以及其他有关容器镜像创建的配置属性:

quarkus.kubernetes.deployment-target=knative ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.container-image.build=true ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.container-image.group=lordofthejars
quarkus.container-image.registry=dev.local ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

将目标部署设置为 knative

2

使用 lordofthejars 组构建容器镜像

3

在部署本地容器镜像时设置为 dev.local

Knative 控制器将图像标签解析为摘要,以确保修订版本的不可变性。 当使用普通注册表时,这很有效; 但是,当与 minikube 和本地图像一起使用时,可能会导致问题。

默认情况下,Knative 控制器会跳过以 dev.localko.local 为前缀的图像的摘要。 如果您在 minikube 中运行此示例,则必须将注册表属性设置为其中的任何一个选项,以使 Knative 找到要部署的图像。

要生成 Kubernetes 资源,请在新终端中执行 ./mvnw package。 然后,在 target 目录中构建工具生成的常规文件中,会在 target/kubernetes 目录内创建两个名为 knative.jsonknative.yaml 的新文件,其中包含 Knative 服务定义:

{
  "apiVersion" : "v1",
  "kind" : "ServiceAccount",
  "metadata" : {
    "annotations" : {
      "app.quarkus.io/vcs-url" :
       "https://github.com/lordofthejars/quarkus-cookbook.git",
      "app.quarkus.io/build-timestamp" : "2020-03-10 - 22:55:08 +0000",
      "app.quarkus.io/commit-id" : "17b19a409c41cc933770b20009f635a65f69440e"
    },
    "labels" : {
      "app.kubernetes.io/name" : "greeting-knative",
      "app.kubernetes.io/version" : "1.0-SNAPSHOT"
    },
    "name" : "greeting-knative"
  }
}{
  "apiVersion" : "serving.knative.dev/v1alpha1",
  "kind" : "Service",
  "metadata" : {
    "annotations" : {
      "app.quarkus.io/vcs-url" :
        "https://github.com/lordofthejars/quarkus-cookbook.git",
      "app.quarkus.io/build-timestamp" : "2020-03-10 - 22:55:08 +0000",
      "app.quarkus.io/commit-id" : "17b19a409c41cc933770b20009f635a65f69440e"
    },
    "labels" : {
      "app.kubernetes.io/name" : "greeting-knative",
      "app.kubernetes.io/version" : "1.0-SNAPSHOT"
    },
    "name" : "greeting-knative"
  },
  "spec" : {
    "runLatest" : {
      "configuration" : {
        "revisionTemplate" : {
          "spec" : {
            "container" : {
              "image" :"dev.local/lordofthejars/greeting-knative:1.0-SNAPSHOT",
              "imagePullPolicy" : "IfNotPresent"
            }
          }
        }
      }
    }
  }
}

然后部署生成的 Knative 服务:

kubectl apply -f target/kubernetes/knative.json

serviceaccount/greeting-knative created
service.serving.knative.dev/greeting-knative created

kubectl get ksvc
NAME               URL                                                \
greeting-knative   http://greeting-knative.default.127.0.0.1.nip.io   \

LATESTCREATED            LATESTREADY              READY   REASON
greeting-knative-j8n76   greeting-knative-j8n76   True

Unknown 状态移动到 True 状态可能需要几秒钟的时间。 如果出现失败,也就是 ready 状态仍然为 false,您可以通过运行以下命令来检查原因和事件序列:

kubectl get events --sort-by=.metadata.creationTimestamp

要测试服务是否已正确部署,请打开一个新的终端窗口,并在本地机器和 Knative 网关之间进行端口转发:

kubectl port-forward --namespace kourier-system $(kubectl get pod \
 -n kourier-system -l "app=3scale-kourier-gateway" \
 --output=jsonpath="{.items[0].metadata.name}") \
 8080:8080 19000:19000 8443:8443

Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Forwarding from 127.0.0.1:19000 -> 19000
Forwarding from [::1]:19000 -> 19000
Forwarding from 127.0.0.1:8443 -> 8443
Forwarding from [::1]:8443 -> 8443
Handling connection for 8080
Handling connection for 8080

注意,这仅在服务部署在 minikube 中时才需要。 根据您部署服务的 Kubernetes 平台的不同,您可能需要执行不同的操作。

最后,您可以向服务发送请求:

curl -v -H "Host: greeting-knative.default.127.0.0.1.nip.io" \
 http://localhost:8080/greeting

hello

要取消部署示例,您需要运行以下命令:

kubectl delete -f target/kubernetes/knative.json

serviceaccount "greeting-knative" deleted
service.serving.knative.dev "greeting-knative" deleted

讨论

您可以将 container-imagekubernetes 扩展组合起来,自动构建容器镜像并将其推送到 Kubernetes,如配方 10.6 所示,因此无需手动步骤。

参见

要了解更多,请访问以下网页:

第十一章:身份验证和授权

在本章中,您将了解身份验证和授权如何在 Quarkus 应用程序中发挥作用,这是应用程序安全性的支柱。我们将讨论以下主题:

  • 基于文件的认证和授权方案

  • 基于数据库的身份验证和授权方案

  • 外部服务支持的认证和授权方案

Quarkus 安全性基础知识

在我们开始第一个配方之前,本节将向您展示 Quarkus 和安全性的基础知识,您将使用安全性扩展来加载身份验证源,以及如何使用基于角色的访问控制(RBAC)方法保护资源。

本节中显示的示例并不意味着可运行,但它们将是即将出现的配方的基础,其中我们将看到安全性扩展的实际操作。

下面是关于安全性的两个主要概念:

认证

验证您的凭据(即用户名/密码)以验证您的身份,以便系统知道您是谁。

授权

验证您对受保护资源的访问权。这发生在身份验证过程之后。

认证

Quarkus 为 HTTP 提供了两种认证机制,即众所周知的BASICFORM方法。这些机制可以由任何 Quarkus 扩展进行扩展,以提供自定义的身份验证方法。这些机制的示例以 Quarkus 扩展的形式提供,用于对抗 OpenID Connect 服务器(如 Keycloak)进行身份验证。我们将在本节中探讨如何做到这一点。

要使用身份验证,需要身份提供者来验证用户提供的凭据(即用户名/密码)。Quarkus 提供了以下开箱即用的身份提供者,但您也可以实现自己的身份提供者:

Elytron 属性文件

以属性文件的形式提供用户/密码/角色之间的映射。这些信息可以嵌入在application.properties文件中,也可以放在一个专门用于此目的的文件中。

Elytron JDBC

基于 JDBC 查询提供用户/密码/角色之间的映射。

JPA

提供通过 JPA 进行身份验证的支持。

SmallRye JWT

提供使用 JSON Web Tokens(JWT)规范进行身份验证。

OIDC

提供使用 OpenID Connect(OIDC)提供程序(如 Keycloak)进行身份验证。

Keycloak 授权

提供使用 Keycloak 授权服务的策略执行器支持。

基本身份验证

要使用基本访问认证进行身份验证,必须将quarkus.http.auth.basic配置属性设置为true

基于表单的身份验证

为了使用表单访问认证进行身份验证,必须将quarkus.http.auth.form.enabled配置属性设置为true

重要

Quarkus 不会将经过身份验证的用户存储在 HTTP 会话中,因为没有集群化的 HTTP 会话支持。相反,身份验证信息存储在加密的 cookie 中。

可以使用quarkus.http.auth.session.encryption-key属性来设置加密密钥,必须至少为 16 个字符长。该密钥使用 SHA-256 进行哈希,结果用作 AES-256 加密 Cookie 值的密钥。此 Cookie 包含作为加密值的一部分的到期时间,在使用会话时每分钟生成一个新的 Cookie,并更新到期时间。

授权

Quarkus 与Java EE 安全注解集成,以定义对 RESTful Web 端点和 CDI bean 的 RBAC。

此外,您可以使用配置文件(application.properties)而不是注解来定义 RESTful Web 端点的授权。

这两种方法可以在同一个应用程序中共存,但是配置文件检查会在任何注解检查之前执行,并且它们不是互斥的,这意味着在重叠的情况下,必须通过两个检查。

下面的代码段展示了如何使用 Java EE 安全注解来保护 JAX-RS 端点:

package org.acme.quickstart;

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;

import io.quarkus.security.Authenticated;

import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path("/hello")
public class GreetingResource {

    @GET
    @Path("/secured")
    @RolesAllowed("Tester")  ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    public String greetingSecured() {}

    @GET
    @Path("/unsecured")
    @PermitAll ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    public String greetingUnsecured() {}

    @GET
    @Path("/denied")
    @DenyAll ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    public String greetingDenied() {}

    @GET
    @Path("/authenticated")
    @Authenticated ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
    public String greetingAuthenticated() {}
}

1

要求具有Tester角色的经过身份验证的用户

2

未经身份验证的用户可以访问该方法

3

无论是否经过身份验证,都不能访问用户。

4

允许任何经过身份验证的用户访问;它是@RolesAllowed("*")的别名,由 Quarkus 提供,而不是规范。

可以使用javax.ws.rs.core.Context注解来注入javax.ws.rs.core.SecurityContext实例,以获取有关已验证用户的信息:

@GET
@Path("/secured")
@RolesAllowed("Tester")
public String greetingSecured(@Context SecurityContext sec) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    Principal user = sec.getUserPrincipal(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    String name = user != null ? user.getName() : "anonymous";
    return name;
}

1

为当前请求注入SecurityContext

2

获取当前登录的用户

重要提示

安全注解不仅限于 JAX-RS 资源。它们也可以用于 CDI bean 中来保护方法调用。

Quarkus 支持使用配置文件而不是注解来配置 RESTful Web 端点。可以通过配置文件表示等效的安全注解示例:

quarkus.http.auth.policy.role-policy1.roles-allowed=Tester ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

quarkus.http.auth.permission.roles1.paths=/hello/secured ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.http.auth.permission.roles1.policy=role-policy1 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.http.auth.permission.roles1.methods=GET ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

quarkus.http.auth.permission.deny1.paths=/hello/denied
quarkus.http.auth.permission.deny1.policy=deny ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)

quarkus.http.auth.permission.permit1.paths=/hello/unsecured
quarkus.http.auth.permission.permit1.policy=permit ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
quarkus.http.auth.permission.permit1.methods=GET

quarkus.http.auth.permission.roles2.paths=/hello/authenticated
quarkus.http.auth.permission.roles2.policy=authenticated
quarkus.http.auth.permission.roles2.methods=GET

1

定义应用程序的角色;role-policy1用作参考值

2

设置资源的权限;roles1是一个任意的名称,以避免重复的键。

3

设置角色策略

4

限制对GET方法的权限

5

拒绝访问

6

允许访问

需要注意的是paths属性支持逗号分隔的多个值,还支持*通配符来匹配任何子路径。比如,quarkus.http.auth.permission.permit1.paths=/public/_,/robots.txt设置了对位于/public及其子路径下的任意资源和文件/robots.txt的权限。

同样,methods属性允许用逗号分隔的多个值。

有两个配置属性影响 RBAC 的行为:

quarkus.security.jaxrs.deny-unannotated-endpoints

如果设置为true,则所有未标记安全注解的 JAX-RS 端点默认情况下将被拒绝访问。此属性默认值为false

quarkus.security.deny-unannotated-members

如果设置为true,那么所有未标记安全注解的 JAX-RS 端点和 CDI 方法默认情况下将被拒绝。此属性默认值为false

到目前为止,你已经看到在 Quarkus 中可以设置授权过程(基本的、表单的,或者其他扩展提供的)并使用安全注解或在配置文件中指定身份验证角色。

本章中的示例将探讨不同的 Quarkus 扩展以提供身份验证和授权身份提供者。

11.1 使用 Elytron 属性文件配置进行身份验证和授权

问题

你想通过存储身份来保护应用程序。

解决方案

Quarkus 安全性支持使用 Elytron 属性文件配置将身份存储在文件中作为身份提供者。

你已经看到了如何定义身份验证机制以及如何使用 RBAC 保护资源,无论是通过安全注解还是在application.properties中,但你还没有看到如何注册身份提供者以及如何存储用户信息,比如用户名、密码或角色。

现在让我们看看如何使用 Elytron 属性文件配置扩展定义身份信息。该扩展基于属性文件来定义所有身份信息,其主要目的是用于开发和测试。不推荐在生产环境中使用,因为密码只能以明文或 MD5 哈希的形式表示。

要启用 Elytron 属性文件配置,需要注册quarkus-elytron-security-properties-file扩展:

./mvnw quarkus:add-extension \
 -Dextensions="quarkus-elytron-security-properties-file"

该扩展支持使用属性文件的组合将用户映射到密码和用户映射到角色。

保护端点,只允许Tester角色访问资源:

@GET
@Produces(MediaType.TEXT_PLAIN)
@RolesAllowed("Tester")
public String hello() {
    return "hello";
}

要注册身份,需要两个属性文件,一个用于将用户和密码映射,另一个用于将用户和他们所属的角色列表映射。

用户配置属性文件定义了每一行用户和密码的配对,在系统中注册:

alex=soto

在用户属性文件中,关键部分是用户名,值部分是密码。

警告

注意密码为明文。您可以按照以下模式使用 MD5 对密码进行哈希:HEX(MD5(username:realm:password)

角色配置文件为每行定义了用户名和用户所属角色(用逗号分隔):

alex=Tester

在角色属性文件中,关键部分是用户名,值是分配给用户的角色。

最后,Elytron 安全属性文件扩展需要配置用户和角色属性文件的类路径位置:

quarkus.http.auth.basic=true ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

quarkus.security.users.file.enabled=true ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.security.users.file.plain-text=true ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.security.users.file.users=users.properties ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
quarkus.security.users.file.roles=roles.properties

1

启用基本认证方法。

2

启用具有属性文件扩展的安全性

3

设置未使用 MD5 哈希的密码

4

设置用户和角色属性文件的类路径位置。

运行生成的测试以验证端点的保护:

./mvnw clean test

...
INFO  [io.quarkus] (main) Installed features:
 [cdi, resteasy, security, security-properties-file]
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 8.485 s
 <<< FAILURE! - in org.acme.quickstart.GreetingResourceTest
[ERROR] testHelloEndpoint  Time elapsed: 0.076 s  <<< FAILURE!
java.lang.AssertionError:
1 expectation failed.
Expected status code <200> but was <401>.
 at org.acme.quickstart.GreetingResourceTest.testHelloEndpoint
 (GreetingResourceTest.java:17)

测试失败,显示 HTTP 401 未经授权的错误,因为测试未使用基本认证方法提供任何身份验证。修改测试以使用配置的用户名和密码进行身份验证:

@Test
public void testSecuredHelloEndpoint() {
    given()
            .auth() ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
            .basic("alex", "soto") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
            .when()
            .get("/hello")
            .then()
            .statusCode(200)
            .body(is("hello"));
}

1

设置认证部分。

2

使用给定的用户名和密码进行基本认证

现在,使用有效的认证参数,测试通过。

讨论

Elytron 属性文件配置扩展还支持在 Quarkus 配置文件(application.properties)中嵌入用户/密码/角色的映射,而不是使用不同的文件。

quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.alex=soto
quarkus.security.users.embedded.roles.alex=Admin,Tester

存储在文件中的密码可以使用公式HEX(MD5(username ":" realm ":" password))进行哈希。

可以通过使用表 11-1 中列出的属性来配置嵌入式 Elytron 属性文件。

表 11-1. 嵌入式 Elytron 属性

属性 描述

|

quarkus.security.users.embedded.realm-name

|

生成哈希密码时使用的域名(默认为Quarkus)。

|

|

quarkus.security.users.embedded.enabled

|

使用具有属性文件扩展的安全性(默认为false)。

|

|

quarkus.security.users.embedded.plain-text

|

设置密码是否为哈希形式。如果设为true,则哈希密码必须采用HEX(MD5(username:realm:password)的格式(默认为false)。

|

|

quarkus.security.users.embedded.users.<user>

|

用户信息。关键部分是用户名,值部分是密码。

|

|

quarkus.security.users.embedded.roles.<user>

|

角色信息。关键部分是用户名,值部分是密码。

|

11.2 使用 Elytron 安全性 JDBC 配置进行认证和授权

问题

您希望保护应用程序并将用户身份存储在数据库中。

解决方案

Quarkus 安全性提供支持,可以使用 Elytron 安全性 JDBC 配置将用户身份存储在数据源中作为身份提供者。

您已经看到如何使用 Elytron 属性文件配置扩展在属性文件中定义身份,详见 Recipe 11.1。然而,正如所述,此方法更适用于测试/开发目的,不应在生产环境中使用。

Elytron 安全性 JDBC 扩展可用于将用户身份存储在数据库中,支持使用 bcrypt 密码映射器进行密码加密,并且足够灵活,不会将您锁定在任何预定义的数据库模式中。

要启用 Elytron 安全性 JDBC 扩展,您需要注册quarkus-elytron-security-jdbc扩展,用于连接数据库的 JDBC 驱动程序,以及可选的 Flyway 来填充模式和一些默认用户:

./mvnw quarkus:add-extension \
 -Dextensions="quarkus-elytron-security-jdbc,quarkus-jdbc-h2,quarkus-flyway"

保护终端点,仅允许具有Tester角色的用户访问资源:

@GET
@RolesAllowed("Tester")
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    return "hello";
}

下一步是定义数据库模式以存储所有 RBAC 信息。为简单起见,此示例中使用了一个包含用户、密码和角色的简单表格:

CREATE TABLE test_user (
  id INT,
  username VARCHAR(255),
  password VARCHAR(255),
  role VARCHAR(255)
);

INSERT INTO test_user (id, username, password, role)
  VALUES (1, 'alex', 'soto', 'Tester');

最后,必须配置扩展以指定要执行的查询来验证用户并检索他们所属的角色:

quarkus.datasource.url=jdbc:h2:mem:mydb
quarkus.datasource.driver=org.h2.Driver
quarkus.datasource.username=sa
quarkus.datasource.password=

quarkus.flyway.migrate-at-start=true

quarkus.security.jdbc.enabled=true ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.security.jdbc.principal-query.sql=\
  SELECT u.password, u.role FROM test_user u WHERE u.username=? ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=true ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.security.jdbc.principal-query.clear-password-mapper\
  .password-index=1![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2 ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups

1

启用 Elytron 安全性 JDBC。

2

定义验证用户并获取角色的查询;查询必须包含一个参数(用户名),并且至少返回密码,值应与键在同一行。

3

密码以明文存储。

4

设置密码的索引;所有内容应在同一行上

5

设置角色的索引,并指定该字段为角色

重要提示

索引从 1 开始。

贴士

检索密码的查询(以及可选的角色)可以根据模型的要求复杂化(即,SQL 连接)。

现在,身份验证和授权数据是从数据库中检索,而不是从文件中。当提供用户名和密码(例如,使用基本的auth方法)时,将执行查询以检索身份验证过程所需的所有信息(将提供的密码与从数据库检索到的密码进行匹配),并获取授权过程中的角色。

记得更新测试(如果之前未完成)以使其通过:

@Test
public void testSecuredHelloEndpoint() {
    given()
      .auth().basic("alex", "soto")
    .when()
      .get("/hello")
    .then()
      .statusCode(200)
      .body(is("hello"));
}

讨论

在此示例中,您使用了明文密码,显然不应在生产环境中使用。该扩展提供了与bcrypt密码映射器集成,因此身份验证过程也适用于哈希密码。

您需要通过向配置文件添加一些额外参数来指示 Elytron Security JDBC 使用bcrypt加密密码,并且不应将其作为明文进行比较。

不再配置 clear-password-mapper,而是使用 bcrypt-password-mapper。以下是使用 bcrypt 的配置文件示例:

quarkus.security.jdbc.enabled=true
quarkus.security.jdbc.principal-query.sql=\
    SELECT u.password, u.role, u.salt, u.iteration \
    FROM test_user u WHERE u.username=?

quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=false
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabled=true ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-index=\
    1 ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.hash-encoding=\
    BASE64 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-index=\
    3 ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-encoding=\
    BASE64 ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.\
    iteration-count-index=4 ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)

quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2
quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups

1

启用 bcrypt

2

设置密码索引;这应该在同一行。

3

设置密码哈希编码;这应该在同一行。

4

设置盐索引;这应该在同一行。

5

设置盐编码;这应该在同一行。

6

设置迭代计数索引;这应该在同一行。

在此更改后,提供的密码与通过查询检索的密码不再以明文形式匹配。相反,提供的密码使用 bcrypt 进行哈希处理,然后与存储的密码进行比较:

quarkus.security.jdbc.enabled=true

quarkus.security.jdbc.principal-query.sql=\
    SELECT u.password FROM test_user u WHERE u.username=? ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
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.roles.sql=\
    SELECT r.role_name FROM test_role r, test_user_role ur \
    WHERE ur.username=? AND ur.role_id = r.id ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.security.jdbc.principal-query.roles.datasource=permissions ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
quarkus.security.jdbc.principal-query.roles.attribute-mappings.0.index=1
quarkus.security.jdbc.principal-query.roles.attribute-mappings.0.to=groups

1

默认数据源用于检索密码。

2

roles 用作标识第二个查询的名称;查询应该全部位于同一行。

3

从另一个查询获取角色。

4

角色查询针对名为permissions的数据源执行。

11.3 使用 MicroProfile JWT 进行授权

问题

您希望在 RESTful web 服务和通常的无状态服务中保存安全上下文。

解决方案

使用 JSON Web Tokens。

JWT(JSON Web Token)是根据 RFC-7519 规范制定的标准,用于在服务之间交换信息。JWT 的特殊之处在于,令牌内容以 JSON 格式而不是纯文本或任何其他二进制格式进行格式化。

Quarkus 与MicroProfile JWT 规范集成,以消费和验证 JWT 令牌并获取声明。

JWT 令牌由声明组成,这些声明是要传输的信息,例如用户名、令牌的过期时间或用户的角色。令牌经过数字签名,因此可以信任和验证其中包含的信息。

JWT 令牌由三个部分组成,所有部分均使用 Base64 编码:

头部

它包含一些元数据,例如用于签署令牌的算法;令牌的自定义信息,例如令牌类型;或者如果使用 JSON Web Encryption (JWE),则是未加密的声明。

声明

要存储在令牌中的信息。某些声明是强制性的,其他是可选的,还有一些是我们应用程序自定义的。

签名

令牌的签名。

然后将三个部分编码为 Base64 并用句点符号(.)连接,因此最终令牌看起来像是base64(Header).base64(Claims).base64(Signature)

对于本示例,使用以下 JWT 令牌:

{ ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
  "kid": "/privateKey.pem",
  "typ": "JWT",
  "alg": "RS256"
},
{ ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
  "sub": "jdoe-using-jwt-rbac",
  "aud": "using-jwt-rbac",
  "upn": "jdoe@quarkus.io",
  "birthdate": "2001-07-13",
  "auth_time": 1570094171,
  "iss": "https://quarkus.io/using-jwt-rbac", ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
  "roleMappings": {
    "group2": "Group2MappedRole",
    "group1": "Group1MappedRole"
  },
  "groups":  ![4
    "Echoer",
    "Tester",
    "Subscriber",
    "group2"
  ],
  "preferred_username": "jdoe",
  "exp": 2200814171,
  "iat": 1570094171,
  "jti": "a-123"
}

1

头部部分

2

Claim 部分

3

令牌的颁发者

4

属于令牌所有者的组(或角色)。

以下是相同令牌的序列化版本:

eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.
eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ
1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF
90aW1lIjoxNTcwMDk0MTcxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmcta
nd0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIs
Imdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3R
lciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZS
IsImV4cCI6MjIwMDgxNDE3MSwiaWF0IjoxNTcwMDk0MTcxLCJqdGkiOiJhLTEyMyJ9.
Hzr41h3_uewy-g2B-sonOiBObtcpkgzqmF4bT3cO58v45AIOiegl7HIx7QgEZHRO4PdUtR3
4x9W23VJY7NJ545ucpCuKnEV1uRlspJyQevfI-mSRg1bHlMmdDt661-V3KmQES8WX2B2uqi
rykO5fCeCp3womboilzCq4VtxbmM2qgf6ag8rUNnTCLuCgEoulGwTn0F5lCrom-7dJOTryW
1KI0qUWHMMwl4TX5cLmqJLgBzJapzc5_yEfgQZ9qXzvsT8zeOWSKKPLm7LFVt2YihkXa80l
Wcjewwt61rfQkpmqSzAHL0QIs7CsM9GfnoYc0j9po83-P3GJiBMMFmn-vg

注意部分如何用句点分隔。

当接收到请求时,MicroProfile JWT 规范执行以下操作:

  1. 从请求中提取安全令牌,通常从Authorization头部提取。

  2. 验证令牌以确保令牌有效。这些检查可能涉及验证签名以信任令牌或验证令牌未过期。

  3. 提取令牌信息。

  4. 创建一个带有身份信息的安全上下文,以便在授权(RBAC)时使用。

此外,MicroProfile JWT 规范设置了每个令牌必须提供的强制声明列表:

声明 描述

|

typ

|

令牌格式。必须是JWT

|

|

alg

|

标识用于保护令牌的加密算法。必须是RS256

|

|

kid

|

指示用于保护令牌的密钥。

|

|

iss

|

令牌颁发者。

|

|

sub

|

标识受令牌约束的主体。

|

|

aud

|

标识令牌所针对的接收者。

|

|

exp

|

设置过期时间。

|

|

iat

|

提供令牌发放的时间。

|

|

jti

|

令牌的唯一标识符。

|

|

upn

|

用于java.security.Principal接口中的用户主体名称。

|

|

groups

|

已分配给令牌主体的组名列表。它们是用户所属的角色。

|

这些是 MicroProfile JWT 规范所需的最小声明,但可以添加额外的声明,例如preferred_username或任何您的应用程序可能需要在服务之间传输的其他信息。

注册quarkus-smallrye-jwt扩展以开始使用 MicroProfile JWT 规范:

./mvnw quarkus:add-extension -Dextensions="quarkus-smallrye-jwt"

配置扩展以设置用于验证令牌是否未被修改以及服务器接受的令牌颁发者(iss)声明的公钥。

规范支持的公钥格式如下:

  • Public Key Cryptography Standards #8(PKCS#8)Privacy-Enhanced Mail(PEM)

  • JSON Web Key(JWK)

  • JSON Web Key Set(JWKS)

  • JSON Web Key(JWK)Base64 URL 编码

  • JSON Web Key Set(JWKS)Base64 URL 编码

对于本示例,我们选择 JSON Web Key Set(JWKS)格式来指定用于验证令牌的公钥。

包含公钥的 JWKS 文件放置在项目目录内:

{
    "keys": [
        {
            "kty": "RSA",
            "kid": "/privateKey.pem",
            "e": "AQAB",
            "n": "livFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm-ntyIv1
 p4kE1sPEQO73-HY8-Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRho
 r3kpM6ni2SPmNNhurEAHw7TaqszP5eUF_F9-KEBWkwVta-PZ37bwqSE4sCb
 1soZFrVz_UT_LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQ
 ZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npf
 v_3Lw50bAkbT4HeLFxTx4flEoZLKO_g0bAoV2uqBhkA9xnQ"
        }
    ]
}

指向此数据的配置文件如下:

mp.jwt.verify.publickey.location=quarkus.pub.jwk.json ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
mp.jwt.verify.issuer=https://quarkus.io/using-jwt-rbac ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

公钥的位置

2

服务所接受的发行者

除了处理令牌的验证过程外,MicroProfile JWT 还集成了现有的 Java EE 安全 API,提供来自令牌的数据。集成发生在以下注解中:

javax.ws.rs.core.SecurityContext.getUserPrincipal()
javax.ws.rs.core.SecurityContext.isUserInRole(String)
javax.servlet.http.HttpServletRequest.getUserPrincipal()
javax.servlet.http.HttpServletRequest.isUserInRole(String)
javax.ejb.SessionContext.getCallerPrincipal()
javax.ejb.SessionContext.isCallerInRole(String)
javax.security.jacc.PolicyContext
  .getContext("javax.security.auth.Subject.container")
javax.security.enterprise.identitystore.IdentityStore
  .getCallerGroups(CredentialValidationResult)
@javax.annotation.security.RolesAllowed

此外,MicroProfile JWT 规范提供了两个类,用于在 CDI 或 JAX-RS 类中存储 JWT 数据。

org.eclipse.microprofile.jwt.JsonWebToken

公开原始令牌并提供获取声明的方法的接口

@org.eclipse.microprofile.jwt.Claim

注释以在类中注入声明

例如:

package org.acme.quickstart;

import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

@Path("/hello")
@RequestScoped ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class GreetingResource {

    @Inject
    JsonWebToken callerPrincipal; ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

    @Claim(standard = Claims.preferred_username) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    String username;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello " + username;
    }
}

1

JWT 令牌基于请求范围;如果希望使用令牌,则类必须为RequestScoped,以避免在类中混合令牌。

2

注入代表完整 JWT 令牌的JsonWebToken接口

3

注入preferred_username声明

声明注解还支持注入私有声明名称。这些声明不是 RFC 提供的官方声明名称,而是特定于服务的声明(自定义声明)。要注入私有声明,请使用注解值作为声明的名称:@Claim("*my_claim*")。此外,在非强制性声明的情况下,可以使用java.util.Optional类来指示声明可为空:

@Claim(standard = Claims.birthdate)
Optional<String> birthdate;

更新测试以向定义的端点发送 bearer JWT 令牌:

@Test
public void testHelloEndpoint() {
    given().header("Authorization", "Bearer " + validToken) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
            .when().get("/hello").then().statusCode(200).body(is("hello jdoe"));
}

1

JWT 令牌作为Authorization头中的 bearer 令牌发送

使用当前解决方案,这些假设是正确的:

  • 如果提供了有效的令牌,则提取preferred_username

  • 如果提供了无效的令牌(过期、签名无效、被第三方修改等),则向调用者返回 401 未经授权错误代码。

  • 如果未提供令牌,则会处理请求,但preferred_username字段为空。

MicroProfile JWT 规范还通过集成@RolesAllowed注解支持授权流程。每当调用isCallerInRole()方法时,都会使用groups声明值,这实际上意味着groups中的任何值都可以作为应用程序中的角色使用。

在此示例中使用的 JWT 令牌中的groups声明包含以下值:"groups": ["Echoer", "Tester", "Subscriber", "group2"]。通过使用@RolesAllowed保护对/hello的调用,其中令牌中存在一个组值:

@GET
@Produces(MediaType.TEXT_PLAIN)
@RolesAllowed("Tester")
public String hello() {}

现在,您可以假设以下内容:

  • 如果提供了有效的令牌,并且groups声明包含Tester组,则提取preferred_username

  • 如果提供了有效的令牌并且groups声明不包含Tester组,则返回给调用者 403 禁止错误代码。

  • 如果提供了无效的令牌(过期、签名无效、被第三方修改等),则返回给调用者 401 未授权错误代码。

  • 如果未提供令牌,则返回给调用者 401 未授权错误代码。

讨论

在过去,安全上下文保存在 HTTP 会话中,这在服务规模扩展并变得越来越复杂时效果良好。为避免此问题,一种可能的解决方案是在所有调用中使用令牌传递此信息,特别是 JSON 令牌。

需要注意的是令牌是签名的而不是加密的,这意味着信息可以被任何人看到但不能被修改。可以使用 JSON Web Encryption 添加加密层,使声明不是明文而是加密的。

本节的目的不是让您精通 JWT,而是让您了解如何在 Quarkus 中使用它,因此我们假设您已经具有一些关于 JWT 的知识。我们还在下面的“参见”部分提供了一些链接,以帮助您更熟悉 JWT。

参见

要了解 JWT,请访问以下网页:

11.4 使用 OpenId Connect 进行授权和认证

问题

您希望使用 OpenId Connect 保护您的 RESTful Web API。

解决方案

使用由 OpenId Connect 签发的承载令牌进行授权。

在前一节中,您学习了如何使用 JWT 令牌保护资源,但未涵盖令牌的生成,因为令牌是预先生成并在文本文件中提供的。

在实际应用程序中,您需要一个发行令牌的身份提供者。用于分布式服务的事实上协议是 OpenId Connect 和 OAuth 2.0 以及符合该协议的授权服务器,如Keycloak

注册quarkus-oidc扩展以保护使用 OpenId Connect 的资源:

./mvnw quarkus:add-extension -Dextensions="quarkus-oidc"

配置 OpenId Connect 服务器的位置以验证令牌:

quarkus.oidc.auth-server-url=http://localhost:8180/auth/realms/quarkus ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.oidc.client-id=backend-service ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

OpenID Connect 服务器的基本 URL

2

每个应用程序都有一个客户端 ID 用于标识应用程序

使用@RolesAllowed注解保护端点:

@Inject
io.quarkus.security.identity.SecurityIdentity securityIdentity; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

@GET
@RolesAllowed("user")
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    return "hello " + securityIdentity.getPrincipal().getName();
}

1

Quarkus 接口,表示当前已登录的用户

测试必须更新以从 OpenId Connect 获取访问令牌并将其提供为承载令牌:

@Test
public void testHelloEndpoint() {
    System.out.println(accessToken);
    given()
      .auth().oauth2(accessToken)
      .when().get("/hello")
      .then()
         .statusCode(200)
         .body(is("hello alice"));
}

访问令牌由 OpenId Connect 服务器生成。要生成它,必须提供一些参数,例如用户名和密码,以访问服务器并生成代表用户的令牌:

package org.acme.quickstart;

import java.net.URI;
import java.net.URISyntaxException;

import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.response.Response;
import io.restassured.response.ResponseOptions;
import io.restassured.specification.RequestSpecification;

public class RestAssuredExtension {

  public static ResponseOptions<Response> getAccessToken(String url,
                                                         String clientId,
                                                         String clientIdPwd,
                                                         String username,
                                                         String password) {
    final RequestSpecification request = prepareRequest(url);
    try {
      return request
        .auth()
        .preemptive()
        .basic(clientId, clientIdPwd)
        .contentType("application/x-www-form-urlencoded; charset=UTF-8")
        .urlEncodingEnabled(true)
        .formParam("username", username)
        .and()
        .formParam("password", password)
        .and()
        .formParam("grant_type", "password")
        .post(new URI(url));
    } catch (URISyntaxException e) {
      throw new IllegalArgumentException(e);
    }
  }

  private static RequestSpecification prepareRequest(String url) {
    final RequestSpecBuilder builder = new RequestSpecBuilder();
    final RequestSpecification requestSpec = builder.build();
    return RestAssured.given().spec(requestSpec);
  }
}

代码本质上是使用 REST-Assured 实现下一个curl命令的实现:

curl -X POST \
    http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/token \
    --user backend-service:secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=alice&password=alice&grant_type=password'

现在,在运行测试时,显示的内容与 Recipe 11.3 完全不同。

首先,令牌(JWT 令牌)不是静态的;它是由 OpenID Connect(Keycloak)为alice用户名颁发的。

以下是针对alice的示例已颁发令牌:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "cfIADN_xxCJmVkWyN-PNXEEvMUWs2r68CxtmhEDNzXU"
},
{
  "jti": "cc54b9db-5f2f-4609-8a6b-4f76026e63ae",
  "exp": 1578935775,
  "nbf": 0,
  "iat": 1578935475,
  "iss": "http://localhost:8180/auth/realms/quarkus",
  "sub": "eb4123a3-b722-4798-9af5-8957f823657a",
  "typ": "Bearer",
  "azp": "backend-service",
  "auth_time": 0,
  "session_state": "5b674175-a2a9-4a45-a3da-394923125e55",
  "acr": "1",
  "realm_access": {
    "roles": [
      "user"
    ]
  },
  "scope": "email profile",
  "email_verified": false,
  "preferred_username": "alice"
}

其次,OpenID Connect 负责提供所有内容来验证令牌;不手动配置公钥。

当提供令牌时,Keycloak 执行以下验证:

  • 如果提供了有效令牌并且roles声明包含user组,则提取preferred_username

  • 如果提供了有效令牌并且roles声明不包含user组,则会向调用方返回 403 Forbidden 错误代码。

  • 如果提供了无效令牌(过期、签名无效、被第三方篡改等),则会向调用方返回 403 Forbidden 错误代码。

  • 如果未提供令牌,则会向调用方返回 401 Unauthorized 错误代码。

参见

要了解有关 OpenId Connect 协议的更多信息,请参阅以下网站:

11.5 使用 OpenId Connect 保护 Web 资源

问题

您希望保护您的 Web 资源。

解决方案

使用 OpenId Connect 和基于文件的角色定义来保护 Web 资源。

可以使用 OpenId Connect 协议和 Quarkus 保护 Web 资源。 OpenId Connect 扩展通过实现众所周知的授权码流程,使 Web 资源能够进行身份验证,如果尝试访问受保护资源的未经身份验证用户,则会将其重定向到 OpenId Connect 提供者网站进行身份验证。完成身份验证过程后,用户将被发送回应用程序。

注册quarkus-oidc扩展以保护 OpenId Connect 资源:

./mvnw quarkus:add-extension -Dextensions="quarkus-oidc"

配置 OpenId Connect 服务器的位置以验证令牌:

quarkus.oidc.auth-server-url=http://localhost:8180/auth/realms/quarkus ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.oidc.client-id=frontend ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.oidc.application-type=web-app ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.http.auth.permission.authenticated.paths=/* ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
quarkus.http.auth.permission.authenticated.policy=authenticated

1

OpenID Connect 服务器的基本 URL

2

每个应用程序都有一个客户端 ID 用于标识应用程序

3

启用 OpenID Connect 授权码流程

4

设置 Web 资源的权限

启动应用程序,打开浏览器,并输入以下网址:http://localhost:8080:

./mvnw clean compile quarkus:dev

默认的 index.html 页面不会显示,而是会重定向到 Keycloak 的认证页面。输入以下有效凭据(登录名:alice,密码:alice)即可访问 Web 资源。点击登录按钮后,页面将被重定向回登录页面。

第十二章:应用程序秘密管理

每个应用程序都有需要保密的信息。这些信息可能包括数据库凭据、外部服务身份验证或某些资源的位置。所有这些信息统称为秘密。您的应用程序需要一个安全的地方来存储这些秘密,在应用程序启动期间和静态时都是如此。在本章中,我们将讨论使用 Kubernetes 和 Vault 进行秘密管理。

12.1 使用 Kubernetes 秘密存储数据

问题

您希望以比直接在 Pod 或容器上更安全的方式在 Kubernetes 中存储秘密。

解决方案

使用 Kubernetes 秘密对象以明文形式存储和检索敏感数据,例如密码、令牌或 SSH 密钥。Kubernetes 提供了可以用来存储敏感数据的 secret 对象的概念。

将敏感数据存储在秘密对象中并不自动使其安全,因为 Kubernetes 不加密数据,而是默认使用 Base64 编码。使用秘密对象可以获得一些标准配置流程不提供的功能:

  • 您可以定义授权策略以访问秘密。

  • 您可以配置 Kubernetes 以加密敏感数据(称为静态加密)。

  • 您可以使用列表为特定容器实例授予权限。

重要

这些功能都不会默认启用,它们需要一些关于 Kubernetes 的知识。在本书中,我们仅解释了 Quarkus 如何与 Kubernetes 等其他工具集成;我们不解释工具的操作方面。

秘密可以作为环境变量或卷注入到容器中。环境变量方法较不安全,因为任何能够访问容器实例的人都可以轻松地转储内容。另一方面,当存在大量密钥时,卷方法会变得复杂,因为 Kubernetes 会为存储值内的每个密钥创建一个文件。

两种方法都显示了,因此您可以选择适合您用例的方法。

本示例涵盖了需要将 API 令牌(例如 GitHub 个人访问令牌)设置为应用程序中的秘密的用例。

要在 Pod 中启用 secrets 注入的 Kubernetes 资源生成,需要注册 quarkus-kubernetes 扩展:

./mvnw quarkus:add-extension -Dextensions="quarkus-kubernetes"

通过创建 Secret 类型的 Kubernetes 资源或使用 kubectl CLI 工具来创建秘密。打开新终端并运行以下命令,以注册具有 greeting-security ID 和键 github.api.key.token 的新秘密(此令牌无效,仅用于示例目的):

kubectl create secret generic greeting-security \
--from-literal=github.api.key.token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.\
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

现在秘密已创建,让我们看看如何将其设置为环境变量。

需要从环境变量获取属性的配置属性。在本例中,属性称为github.api.key.token,但当然你也可以直接使用System.getenv()来访问它。前一种方法更好,因为它依赖于 MicroProfile Config 规范来读取配置属性,而不是一些自定义解决方案:

@ConfigProperty(name = "github.api.key.token")
String githubToken;

application.properties中为 Kubernetes 扩展设置额外属性,以便生成的 Kubernetes 部署文件包含注入秘密作为环境变量所需的部分:

quarkus.container-image.group=quarkus ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.container-image.name=greeting-started-kubernetes-secrets
quarkus.container-image.tag=1.0-SNAPSHOT
quarkus.kubernetes.image-pull-policy=if-not-present
quarkus.kubernetes.env-vars.github-api-key-token.name=github.api.key.token ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.kubernetes.env-vars.github-api-key-token.secret=greeting-security ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

配置 Docker 镜像

2

设置环境变量以覆盖github.api.key.token属性

3

设置要加载的秘密名称

Kubernetes 文件的生成将在容器定义中包含一个名为secretKeyRef的新条目,该条目定义了所有的键/值对。

MicroProfile 配置规范允许使用等效的环境变量(大写并更改点[.]为下划线[_])覆盖任何配置属性。Secrets包含配置属性作为秘密。在application.properties中,Kubernetes 扩展被配置为生成一个设置这些秘密作为环境变量的部署描述符,这样当容器在 Kubernetes 集群内启动时,秘密会作为环境变量注入到容器中,并被 MicroProfile Config 读取为配置属性。

要部署该应用程序,请打开一个新的终端窗口,打包该应用程序,创建 Docker 容器,并应用生成的 Kubernetes 资源:

./mvnw clean package -DskipTests

docker build -f src/main/docker/Dockerfile.jvm \
 -t quarkus/greeting-started-kubernetes-secrets:1.0-SNAPSHOT .
kubectl apply -f target/kubernetes/kubernetes.yml

kubectl patch svc greeting-started-kubernetes-secrets \
 --type='json' \
 -p '[{"op":"replace","path":"/spec/type","value":"NodePort"}]'
curl $(minikube service greeting-started-kubernetes-secrets --url)/hello

但是秘密也可以挂载为卷,而不是设置为环境变量。在application.properties中设置 Kubernetes 扩展属性,使生成的 Kubernetes 部署文件包含挂载秘密文件作为卷所需的部分:

quarkus.kubernetes.mounts.github-token.path=/deployment/github ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.kubernetes.mounts.github-token.read-only=true ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.kubernetes.secret-volumes.github-token.secret-name=greeting-security![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
quarkus.kubernetes.secret-volumes.github-token.default-mode=420 ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)

1

使用github-token名称挂载卷

2

设置卷在容器内挂载的路径

3

将卷设置为只读

4

设置要加载的秘密名称

5

设置模式为从进程可读取

最后一步是从代码中读取秘密。由于秘密被挂载在文件系统中,因此需要像读取任何其他文件一样读取它:

@GET
@Path("/file")
@Produces(MediaType.TEXT_PLAIN)
public String ghTokenFile() throws IOException {
    final byte[] encodedGHToken = Files.readAllBytes(
        Paths.get("/deployment/github/github.api.key.token")); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    return new String(encodedGHToken);
}

1

秘密的位置是挂载路径加上秘密密钥

要部署该应用程序,打包它,创建 Docker 容器,并应用生成的 Kubernetes 资源:

./mvnw clean package -DskipTests

docker build -f src/main/docker/Dockerfile.jvm \
 -t quarkus/greeting-started-kubernetes-secrets:1.0-SNAPSHOT .
kubectl apply -f target/kubernetes/kubernetes.yml

kubectl patch svc greeting-started-kubernetes-secrets --type='json' \
 -p '[{"op":"replace","path":"/spec/type","value":"NodePort"}]'
curl $(minikube service greeting-started-kubernetes-secrets --url=/hello/file

讨论

Kubernetes secrets 存在一些需要通过外部方式解决的问题。以下是其中一些问题:

  • 机密默认情况下不是加密的,而只是以 Base64 编码。

  • 您需要使用 SSL 与 etcd 进行通信。这是存储机密的地方。

  • 需要加密磁盘,因为 etcd 可能会将数据存储在磁盘上。

  • 您需要正确定义 RBAC 以防止任何人访问机密。

参见

要了解有关 Kubernetes Secrets 的更多信息,请访问 Kubernetes 网站上的以下页面:

12.2 使用 Vault 安全地存储配置机密

问题

您希望安全地存储配置机密。

解决方案

使用 Quarkus Vault 扩展检索机密。

处理机密时的关键方面是存储方式,使其无法被禁止用户读取,并保护对其的访问,以便只有需要访问机密的服务可以访问它们。

Vault 是一个工具,通过提供统一接口来简化这些用例,用于存储和使用机密。

Vault 支持多种认证方法,用于对 Vault 服务进行认证并开始使用这些机密。在撰写本文时,Quarkus Vault 扩展支持以下认证方法:

token

直接传递用户令牌以绕过认证过程。

user/password

使用用户名和密码凭据认证 Vault。

approle

使用 role_idsecret_id 进行认证。此方法适用于自动化工作流程(机器和服务)。role_id 通常嵌入到 Docker 容器中,secret_id 由 Kubernetes 集群作为 cubbyhole 响应获取,包装(单次使用)并传递给目标服务。

kubernetes

使用 Kubernetes 服务账户令牌认证 Vault。

要开始使用 Vault,请注册 quarkus-vault 扩展。

./mvnw quarkus:add-extension -Dextensions="quarkus-vault"

Quarkus Vault 扩展与 MicroProfile 配置规范集成,可以使用 @ConfigProperty 注解注入秘密。配置应用程序以使用用户名和密码作为 Vault 的认证方法,并设置存储机密的基本路径:

quarkus.vault.url=http://localhost:8200 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.vault.authentication.userpass.username=alex ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.vault.authentication.userpass.password=alex

quarkus.vault.kv-secret-engine-version=2 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.vault.secret-config-kv-path=myapps/vault-service/config

1

Vault 服务器的基本 URL

2

用于认证的凭据

3

存储机密的路径

使用 @org.eclipse.microprofile.config.inject.ConfigProperty 注解访问 foo 键的秘密值:

@ConfigProperty(name = "foo") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
String foo;

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    return foo;
}

1

foo 键的秘密值注入

启动应用程序并向端点发送请求:

./mvnw clean compile quarkus:dev

curl http://localhost:8080/hello
secretbar

讨论

如果路径只在运行时已知,则还可以通过注入 io.quarkus.vault.VaultKVSecretEngine 接口来以编程方式检索秘密:

@Inject
VaultKVSecretEngine kvSecretEngine;

final Map<String, String> secrets = kvSecretEngine
  .readSecret("myapps/vault-service/config"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
final String fooSecret = secrets.get("foo");

1

提供存储在 Vault 键/值秘密引擎中的值

参见

要了解有关 Vault 的更多信息,请访问以下网站:

12.3 加密即服务

问题

您希望避免在所有服务中分散所有密码操作。

解决方案

使用 Vault 的 transit 引擎,将所有密码操作集中在同一位置执行。

在前一篇章节创建的 Vault 容器内打开一个 shell,以配置 Vault 并添加用于加密和签署消息的密钥:

docker exec -it dev-vault sh export VAULT_TOKEN=s.ty3QS2uNaxPdiFsSZpCQfjpc ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

vault secrets enable transit ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

vault write -f transit/keys/my_encryption ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
vault write transit/keys/my-sign-key type=ecdsa-p256 ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

1

设置访问令牌

2

启用 transit 引擎

3

创建类型为 AES-256-GCM96 的加密密钥

4

创建类型为 ECDSA-P256 的签名密钥

创建授予过境操作权限的策略:

cat <<EOF | vault policy write vault-service-policy -
path "transit/*" {
 capabilities = [ "create", "read", "update" ]
}
EOF

最后一步是启用凭证(userpass 引擎),以便从服务中访问秘密:

vault auth enable userpass vault write auth/userpass/users/alex password=alex \
 policies=vault-service-policy ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

1

创建 ID 为 alex、密码为 alex 的用户

注册 quarkus-vault 扩展以使用 Vault:

./mvnw quarkus:add-extension -Dextensions="quarkus-vault"

配置应用程序以将用户名和密码作为 Vault 的身份验证方法:

quarkus.vault.url=http://localhost:8200 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

quarkus.vault.authentication.userpass.username=alex ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.vault.authentication.userpass.password=alex

1

Vault 服务器的基本 URL

2

用于身份验证的凭证

注入 io.quarkus.vault.VaultTransitSecretEngine 实例以使用过境操作:

@Inject
VaultTransitSecretEngine transit; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

@GET
@Path("/encrypt")
@Produces(MediaType.TEXT_PLAIN)
public String encrypt(@QueryParam("text") String text) {
    return transit.encrypt("my_encryption", text); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
}

@GET
@Path("/decrypt")
@Produces(MediaType.TEXT_PLAIN)
public String decrypt(@QueryParam("text") String text) {
    return transit.decrypt("my_encryption", text).asString(); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
}

@GET
@Path("/sign")
@Produces(MediaType.TEXT_PLAIN)
public String sign(@QueryParam("text") String text) {
    return transit.sign("my-sign-key", text); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
}

1

过境操作接口

2

使用加密密钥加密

3

使用加密密钥解密

4

用给定签名对文本进行签名

启动应用程序并发送请求到端点:

./mvnw clean compile quarkus:dev

curl http://localhost:8080/hello/encrypt?text=Ada
vault:v1:iIunGAElLpbaNWWqZq1yf4cctkEUOFdJE1oRTaSI2g==

curl http://localhost:8080/hello/decrypt? \
 text=vault:v1:iIunGAElLpbaNWWqZq1yf4cctkEUOFdJE1oRTaSI2g==
Ada

curl http://localhost:8080/hello/sign?text=Alexandra
vault:v1:MEUCIGkgS5VY5KEU2yHqnIn9qwzgfBUv3O2H4bgNAFVrYCK3AiEAnQznfdEZI6b\
 /Xtko/wEl8WhZLuKZQ/arOYkfsnwBH3M=

讨论

在服务中通常需要执行加密、解密、签名或基于哈希的消息认证码(HMAC)等密码操作。这些操作通常在每个服务中实现,这意味着您也在每个服务中重复执行这些敏感逻辑以及密钥管理。

Vault Transit 引擎为您处理所有加密功能,而不存储结果数据。您可以将 Vault 视为 加密即服务 模型,在该模型中,数据被发送、操作并返回,而不存储在内部。

一切都由 Vault 在内部管理,使开发人员能够专注于实现重要的业务逻辑。

Vault 扩展支持以下操作:

encrypt

使用配置在传输秘密引擎中的 Vault 密钥对常规字符串进行加密。

decrypt

解密指定键的加密数据并返回未加密数据。

rewrap

使用最新的密钥版本对使用旧密钥版本加密获取的密文重新加密成新的密文。

sign

使用指定键对输入字符串进行签名。

verifySignature

检查签名是否来自使用指定键对输入进行签名的操作。

参见

欲知更多信息,请访问以下网站:

12.4 生成数据库密码作为秘密

问题

您希望安全地存储数据库密码。

解决方案

作为秘密读取数据库密码。

数据库密码是需要保护的内容,不应直接设置到配置文件中。Quarkus Vault 扩展集成了持久化配置,从 Vault 中读取数据库密码作为秘密。

在前一个配方中创建的 Vault 容器内部打开一个 shell,以配置 Vault 并将数据库密码添加为秘密:

docker exec -it dev-vault sh export VAULT_TOKEN=s.ty3QS2uNaxPdiFsSZpCQfjpc ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

vault kv put secret/myapps/vault-service/db password=alex ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

设置用于访问的令牌

2

使用键 password 和值 alex 创建一个新的秘密

创建一个授予对秘密读取权限的策略:

cat <<EOF | vault policy write vault-service-policy -
path "secret/data/myapps/vault-service/*" {
 capabilities = ["read"]
}
EOF

最后一步是启用凭据(userpass 引擎)以从服务中访问秘密:

vault auth enable userpass vault write auth/userpass/users/alex password=alex \
 policies=vault-service-policy ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

1

创建一个 ID 为 alex 并且密码为 alex 的用户

在本示例中,使用 PostgreSQL 服务器作为数据库。通过运行以下命令在新终端中启动一个新的 Docker 实例:

docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 \
 --name postgres-quarkus-hibernate -e POSTGRES_USER=alex \
 -e POSTGRES_PASSWORD=alex -e POSTGRES_DB=mydatabase \
 -p 5432:5432 postgres:10.5

注意,密码与 secret/myapps/vault-service/db 路径中设置的密码相同。

注册 quarkus-vault 和持久化扩展:

./mvnw quarkus:add-extension \
 -Dextensions="quarkus-vault, quarkus-hibernate-orm-panache, \
 quarkus-jdbc-postgresql, quarkus-resteasy-jsonb"

数据源配置与 第七章 中显示的配置略有不同。不是将密码硬编码到配置文件中,而是作为秘密从 Vault 中检索并用于建立连接。

除了 Vault 配置参数(如 URL 和认证方法(即用户/密码))外,您还需要定义 Vault 内部的键/值路径,其中存储了数据库配置。更具体地说,它是存储带有数据库密码的名为 password 的键的路径。在以下示例中,要将此信息设置到 Vault 中,您可以运行命令 vault kv put secret/myapps/vault-service/db password=alex,但如果您已经在配置 Vault 时完成了此操作。

还需要覆盖用于建立与数据库连接时使用的凭据提供程序,以指示密码来自 Vault 而不是作为配置属性。这通过使用 q⁠u⁠a⁠r⁠k⁠u⁠s⁠.⁠d⁠a⁠t⁠a⁠s⁠o⁠u⁠r⁠c⁠e⁠.⁠c⁠r⁠e⁠d⁠e⁠n⁠t⁠i⁠a⁠l⁠s⁠-⁠p⁠r⁠o⁠v⁠i⁠d⁠e⁠r 属性完成。

配置应用程序的数据源和 Vault 参数,并覆盖凭据提供程序:

quarkus.datasource.url=jdbc:postgresql://localhost:5432/mydatabase
quarkus.datasource.driver=org.postgresql.Driver
quarkus.datasource.username=alex
quarkus.datasource.credentials-provider=mydatabase ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.vault.credentials-provider.mydatabase\
  .kv-path=myapps/vault-service/db ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
quarkus.vault.url=http://localhost:8200 ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
quarkus.vault.authentication.userpass.username=alex
quarkus.vault.authentication.userpass.password=alex
quarkus.vault.kv-secret-engine-version=2
quarkus.hibernate-orm.database.generation=drop-and-create
%dev.quarkus.hibernate-orm.sql-load-script=import.sql
%dev.quarkus.hibernate-orm.log.sql=true

1

将凭据提供程序设置为自定义名称(mydatabase

2

设置保存 mydatabase 提供程序密码的键/值路径

3

配置 Vault 参数

需要注意的是,没有 quarkus.datasource.password 属性,因为密码从 Vault 获取。

当启动 Quarkus 应用程序时,执行以下步骤:

  1. 服务对接 Vault 服务进行认证。

  2. secret/myapps/vault-service/db 路径检索键/值。

  3. password 的值被用作数据库凭据的密码。

提示

可通过 kv-key 属性将密钥名称从 password 更改为任何其他密钥名称:quarkus.vault.credentials-provider.mydatabase.kv-key=pass

讨论

Vault 可以动态生成数据库凭据,并配置数据库实例使用它们作为凭据,而不是手动配置凭据并将其设置在 Vault 和/或需要访问数据库的服务中。这意味着在任何地方都不会硬编码凭据,因为它们是从 Vault 请求的。生成的用户名和密码遵循 Vault 的租赁机制,使得凭据在合理时间后失效。

配置 Vault 以动态生成数据库凭据,请按以下步骤操作:

  1. 启用数据库机密引擎。

  2. 设置连接参数到数据库,并设置供应商数据库(此时大多数 SQL 和 NoSQL 数据库都受支持)。

  3. 配置将名字在 Vault 中映射到 SQL 语句以创建数据库凭证的角色:

vault secrets enable database

cat <<EOF | vault policy write vault-service-policy -
path "database/creds/mydbrole" {
 capabilities = [ "read" ]
}
EOF

vault write database/config/mydb
 plugin_name=postgresql-database-plugin \
 allowed_roles=mydbrole \
 connection_url=postgresql://{{username}}:{{password}}\
 @localhost:5432/mydb?sslmode=disable \
 username=alex \
 password=alex

vault write database/roles/mydbrole \
 db_name=mydb \
 creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD \
 '{{password}}' VALID UNTIL '{{expiration}}'; \
 GRANT SELECT,INSERT, UPDATE, DELETE ON ALL \
 TABLES IN SCHEMA public TO \"{{name}}\"; \
 GRANT USAGE, SELECT ON ALL SEQUENCES IN \
 SCHEMA public to \"{{name}}\";" \
 default_ttl="1h" \
 revocation_statements="ALTER ROLE \"{{name}}\" NOLOGIN;" \
 renew_statements="ALTER ROLE \"{{name}}\" VALID UNTIL '{{expiration}}';" \
 max_ttl="24h"

Vault 扩展还支持通过 credentials-provider 上的 database-credentials-role 属性使用动态数据库凭据:

quarkus.vault.url=https://localhost:8200
quarkus.vault.authentication.userpass.username=alex
quarkus.vault.authentication.userpass.password=alex

quarkus.datasource.driver=org.postgresql.Driver
quarkus.datasource.url=jdbc:postgresql://localhost:6543/mydb
quarkus.datasource.username=postgres

quarkus.datasource.credentials-provider=dynamic-ds ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.datasource.credentials-provider-type=vault-credentials-provider
quarkus.vault.credentials-provider.dynamic-ds.database-credentials-role=\
  mydbrole ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

未设置密码

2

配置动态凭据

参见

要了解有关使用 Vault 的动态数据库凭据的更多信息,请访问以下网站:

12.5 使用 Vault Kubernetes Auth 验证服务

问题

您希望在不使用用户名/密码的情况下对接 Vault 认证服务。

解决方案

使用 Vault Kubernetes Auth 方法。

到目前为止,您已经使用了用户名/密码方法来对接 Quarkus 服务与 Vault 服务进行认证。在某些情况下(例如测试目的、内部应用程序等),这种方法可能很好,但请注意,您正在引入一个新的秘密(密码)以获取更多秘密。解决此问题的一种方法是使用 Kubernetes Secrets 设置 Vault 密码,例如使用 approle 认证方法。另一种方法是使用 Vault Kubernetes Auth,这使其非常适合用于认证部署在 Kubernetes 集群中的服务。

Vault Kubernetes auth 方法使用 Kubernetes 服务账户令牌和定义的角色来对接 Vault 服务进行认证。使用这种方法,Vault 不会存储凭据;它使用一个受信任的第三方(Kubernetes 集群)来验证它们。当包含服务的 Pod 实例化时,服务账户令牌会挂载在容器内,因此应用程序可以访问它。秘密令牌的默认挂载点是 /var/run/secrets/kubernetes.io/serviceaccount/token

然后应用程序尝试使用此令牌进行身份验证,将其发送到 Vault 服务器。然后,Vault 调用 Kubernetes API 来验证令牌的有效性。如果令牌有效,则返回内部 Vault 令牌,以便将来用于获取秘密的请求。该过程在 图 12-1 中进行了总结。

qucb 1201

图 12-1. Kubernetes 认证方法

要配置 Kubernetes 认证模式,您需要向 Vault 设置两个参数以连接到 Kubernetes API。第一个是访问令牌,第二个是用于验证 Vault 与 Kubernetes API 之间通信的证书颁发机构。这些值来自以 vault-token 开头的秘密。当为本示例首次设置 Vault 时,该值为 vault-token-mm5qx

要获取令牌并将其存储在文件中,请打开终端窗口并运行以下命令:

kubectl get secret vault-token-mm5qx -o jsonpath='{.data.token}' \
 | base64 --decode > jwt.txt ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

cat jwt.txt eyJhbGciOiJSUzI1NiIsImtpZCI6Inp0WWZBcl8weW1SaTI1bjRNYVNHNmtXOUhCWDV\ yczhYandVYkVETktzRHMifQ.

1

将秘密名称替换为以 vault-token 开头的您的秘密名称

2

秘密存储在 Base64 中,因此需要解码

要获取证书颁发机构并将其存储在文件中,请在终端中运行以下命令:

kubectl get secret vault-token-mm5qx -o jsonpath="{.data['ca\.crt']}" \
 | base64 --decode > ca.crt ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

cat ca.crt 
-----BEGIN CERTIFICATE----- MIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p -----END CERTIFICATE-----

1

Pod 的名称为 vault-0

2

vault-token 设置为一个秘密

在部署应用程序之前,您需要启用 Kubernetes auth 方法,进行配置,并插入一些秘密来进行测试。

将 Vault 服务暴露在 Kubernetes 集群外,以便可以从本地机器进行配置。打开一个新的终端窗口,并运行以下命令以将流量从 localhost:8200 转发到运行在 Kubernetes 集群内的 Vault 实例:

kubectl port-forward svc/vault 8200:8200

返回到运行命令获取令牌和 CA 的终端窗口,并运行以下命令插入一个秘密:

export VAULT_TOKEN=root ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
export VAULT_ADDR='http://localhost:8200' 
cat <<EOF | vault policy write vault-service-policy - ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
path "secret/data/myapps/vault-service/*" {
 capabilities = ["read"] } EOF 
vault kv put secret/myapps/vault-service/config foo=secretbar ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

配置 Vault 连接参数

2

创建名为 vault-service-policy 的策略来管理 myapps/vault-service/* 的秘密

3

设置一个新的秘密

最后一步是启用 Kubernetes auth 方法,并配置它使用 Kubernetes API 验证令牌。

执行以下命令:

vault auth enable kubernetes ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

vault write auth/kubernetes/config \ ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
 token_reviewer_jwt=@jwt.txt \ ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
 kubernetes_host=https://kubernetes.default.svc \ ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
 kubernetes_ca_cert=@ca.crt ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)

vault write auth/kubernetes/role/example \ ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
 bound_service_account_names=vault \ ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/7.png)
 bound_service_account_namespaces=default \ ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/8.png)
 policies=vault-service-policy ![9](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/9.png)

1

启用 Kubernetes auth 方法

2

配置 auth 方法

3

设置在前面的步骤中检索到的令牌文件

4

设置 Kubernetes API 主机

5

设置在前面的步骤中检索到的 CA 文件

6

创建一个新角色 (example) 以从应用程序进行身份验证

7

设置在我们的部署中使用的服务账号名称为 vault

8

设置服务运行的命名空间

9

将使用此方法进行身份验证的用户绑定到创建的策略

开发一个使用 Vault Kubernetes auth 方法进行认证并获取名为 foo 的秘密的 Quarkus 服务。

添加 Vault 和 Kubernetes 扩展:

./mvnw quarkus:add-extension -Dextensions="quarkus-vault, quarkus-kubernetes"
@ConfigProperty(name = "foo")
String foo;

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    return foo;
}

配置应用程序以使用 Kubernetes Vault auth 方法和 Kubernetes 扩展来生成正确的部署文件:

quarkus.vault.url=http://vault:8200 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.vault.kv-secret-engine-version=2
quarkus.vault.secret-config-kv-path=myapps/vault-service/config

quarkus.vault.authentication.kubernetes.role=example ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
kubernetes.service-account=vault ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

kubernetes.group=quarkus ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
kubernetes.name=greeting-app
kubernetes.version=latest

1

配置 Vault 的位置和秘密

2

设置 example 角色供用户使用(在上一步中创建)

3

设置要在生成的部署文件中设置的 serviceaccount 名称

4

设置 Docker 镜像的组名

注意,quarkus.vault.authentication.kubernetes.jwt-token-path 属性未设置。原因是默认值 (/var/run/secrets/kubernetes.io/serviceaccount/token) 与默认值完美配合。如果秘密挂载在不同的路径上,则应将此属性设置为新位置。

要部署应用程序,请打开一个新的终端窗口,打包应用程序,创建 Docker 容器,并应用生成的 Kubernetes 资源:

./mvnw clean package -DskipTests

docker build -f src/main/docker/Dockerfile.jvm \
 -t quarkus/greeting-started-vault-kubernetes-auth:1.0-SNAPSHOT .
kubectl apply -f target/kubernetes/kubernetes.yml

kubectl patch svc greeting-app --type='json' \
 -p '[{"op":"replace","path":"/spec/type","value":"NodePort"}]'
curl $(minikube service greeting-app --url)/hello

当 Pod 部署后,应用程序通过 Vault 进行身份验证,并且 Vault 使用 Kubernetes API 验证令牌是否有效。然后应用程序被验证,并可以从配置的路径获取秘密。

这个示例与之前的示例之间的主要区别在于,在这种情况下没有像 Vault 密码这样的秘密设置,这意味着可以安全地访问秘密,但无需添加任何新秘密。

讨论

我们的目的不是展示如何在生产环境中部署 Kubernetes 中的 Vault。因此,提供了一个部署文件,用于部署一个仅满足此示例运行所需的最小要求的 Vault 服务。

此部署文件位于src/main/kubernetes/vault-dev-deployment.yaml,提供以下元素:

  • 使用dev模式和根令牌设置为root的 Vault。

  • 在端口 8200 上公开 Vault。

  • ServiceAccount的名称设置为vault

  • ClusterRoleBindingClusterRole绑定到vault

  • 所有应用于default命名空间的资源。

通过运行以下命令部署 Vault 服务:

kubectl apply -f src/main/kubernetes/vault-dev-deployment.yaml 
kubectl get pods ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
NAME      READY   STATUS    RESTARTS   AGE vault-0   1/1     Running   0          44s 
kubectl get secrets NAME                  TYPE                                  DATA   AGE default-token-zdw8r   kubernetes.io/service-account-token   3      2d greeting-security     Opaque                                1      3h9m vault-token-mm5qx     kubernetes.io/service-account-token   3      8s ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

Pod 命名为vault-0

2

vault-token设置为一个秘密

要配置 Vault,您需要在计算机上本地安装 Vault CLI。Vault CLI 是一个单文件,可以从Vault下载,并设置在您的PATH变量中。

假设您已在本地安装并将 Vault 客户端添加到您的 PATH 变量中,您可以配置 Vault。

参见

要了解有关 Vault Kubernetes auth方法的更多信息,请访问以下网站:

第十三章:Quarkus REST 客户端

在第三章,您已经学习了如何开发 RESTful 服务,但在本章中,您将学习如何在 RESTful Web 服务之间进行通信。

使用任何基于服务的架构都意味着您需要与外部服务进行通信。这些服务可能是内部服务(您控制服务的生命周期,通常部署在同一集群中)或外部服务(第三方服务)。

如果这些服务实现为 RESTful Web 服务,则需要客户端与这些服务进行交互。Quarkus 提供了两种方法:JAX-RS Web 客户端,这是与 RESTful 服务通信的标准 Java EE 方式;以及 MicroProfile REST 客户端,这是与 RESTful 服务通信的新方式。

本章将包括以下示例:

  • 使用 JAX-RS 客户端与其他 RESTful 服务进行通信

  • 使用 MicroProfile Rest 客户端与其他 RESTful 服务进行通信

  • 安全地在 RESTful 服务之间进行通信

13.1 使用 JAX-RS Web 客户端

问题

您想要与另一个 RESTful Web 服务进行通信。

解决方案

使用 JAX-RS Web 客户端与其他 RESTful Web 服务进行通信。

让我们看看如何使用 JAX-RS 规范与其他 RESTful 服务进行通信。

我们将要连接的外部服务是世界时钟 API,它通过时区返回当前日期/时间。您需要获取由API公开的当前日期/时间。

您需要添加扩展来使用 REST 客户端以及 JAX-B/Jackson 用于 JSON 和 Java 对象的编组/解组。

./mvnw quarkus:add-extension -Dextensions="resteasy-jsonb, rest-client"

或者,如果您是从空目录创建,请运行以下命令:

mvn io.quarkus:quarkus-maven-plugin:1.4.1.Final:create \
 -DprojectGroupId=org.acme.quickstart \
 -DprojectArtifactId=clock-app \
 -DclassName="org.acme.quickstart.WorldClockResource" \
 -Dextensions="resteasy-jsonb, rest-client"
 -Dpath="/now"

您可以开始使用 JAX-RS REST 客户端与外部 REST API 进行通信。让我们看看与世界时钟服务的交互是什么样子的。

打开 org.acme.quickstart.WorldClockResource.java 并添加以下代码:

package org.acme.quickstart;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;

@Path("/now")
public class WorldClockResource {

  @ConfigProperty(name = "clock.host",
  defaultValue = "http://worldclockapi.com")
    String clockHost; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

  private Client client = ClientBuilder.newClient(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

  @GET
  @Path("{timezone}")
  @Produces(MediaType.APPLICATION_JSON)
  public WorldClock getCurrentTime(@PathParam("timezone") String timezone) {
    WorldClock worldClock = client.target(clockHost) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
      .path("api/json/{timezone}/now") ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
      .resolveTemplate("timezone", timezone) ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
      .request(MediaType.APPLICATION_JSON)
      .get(WorldClock.class); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png) ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/7.png)

    return worldClock;
  }
}

1

使服务主机可配置

2

创建新的 REST 客户端

3

设置主机

4

设置到服务的路径

5

timezone 占位符解析为所提供的时区

6

执行 GET HTTP 方法

7

将 JSON 输出转换为提供的 POJO

通过打开新的终端窗口,启动 Quarkus 应用程序,并发送请求到 GET 方法来尝试它:

./mvnw clean compile quarkus:dev

curl localhost:8080/now/cet
{"currentDateTime":"2019-11-13T13:29+01:00","dayOfTheWeek":"Wednesday"}%

讨论

以类似的方式,您可以对其他 HTTP 方法发出请求。例如,要执行 POST 请求,您调用 post 方法:

target(host)
    .request(MediaType.APPLICATION_JSON)
    .post(entity);

您还可以使用 javax.ws.rs.core.Response 来获取所有响应细节,而不仅仅是响应体:

  @GET
  @Path("{timezone}/raw")
  @Produces(MediaType.APPLICATION_JSON)
  public Response getCurrentTimeResponse(@PathParam("timezone")
      String timezone) {
    javax.ws.rs.core.Response responseWorldClock = client.target(clockHost)
      .path("api/json/{timezone}/now")
      .resolveTemplate("timezone", timezone)
      .request(MediaType.APPLICATION_JSON)
      .get(Response.class);

    System.out.println(responseWorldClock.getStatus());
    System.out.println(responseWorldClock.getStringHeaders());
    // ... more methods

    return responseWorldClock;
  }

另请参阅

您可以进一步在 Oracle 网站上的以下页面探索 JAX-RS REST 客户端:

13.2 使用 MicroProfile REST 客户端

问题

您希望与另一个 RESTful Web 服务通信,而不需了解低级细节。

解决方案

使用 MicroProfile REST 客户端与其他 RESTful Web 服务通信。

到目前为止,您已经了解到如何使用 JAX-RS Web Client 与其他 REST API 进行通信,但这并不是类型安全的,并且需要处理低级参数而不是专注于消息通信。

MicroProfile REST 客户端尽可能地使用 JAX-RS 2.0 规范,提供了一种类型安全的方法来通过 HTTP 调用 RESTful 服务。 REST 客户端定义为 Java 接口,使其类型安全,并使用 JAX-RS 注解提供网络配置。

我们将在此处继续使用在前一节中使用的相同的世界时钟 API。 记得获取当前日期/时间

创建负责与外部服务交互的 org.acme.quickstart.WorldClockService 接口:

package org.acme.quickstart;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@Path("/api") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@ApplicationScoped
@RegisterRestClient ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
public interface WorldClockService {

    @GET ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    @Path("/json/{timezone}/now") ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
    @Produces(MediaType.APPLICATION_JSON) ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
    WorldClock getNow(@PathParam("timezone") String timezone); ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)

}

1

全局路径

2

将接口设置为 REST 客户端

3

请求使用 GET HTTP 方法

4

具有路径参数的子路径

5

请求的媒体类型

6

使用传递的参数解析路径参数

打开 org.acme.quickstart.WorldClockResource.java 并添加以下代码:

@RestClient ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
WorldClockService worldClockService;

@GET
@Path("{timezone}/mp")
@Produces(MediaType.APPLICATION_JSON)
public WorldClock getCurrentTimeMp(@PathParam("timezone") String timezone) {
  return worldClockService.getNow(timezone); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
}

1

注入 REST 客户端

2

调用外部服务

您仍然需要设置外部服务的主机。 MicroProfile REST 客户端有一个配置属性用于设置它。

打开 application.properties

org.acme.quickstart.WorldClockService/mp-rest/url=http://worldclockapi.com

属性名称使用以下格式:*fully_qualified_name_rest_client*/mp-rest/url,其值为主机名(或 URL 的根):

./mvnw clean compile quarkus:dev

curl localhost:8080/now/cet/mp
{"currentDateTime":"2019-11-13T16:46+01:00","dayOfTheWeek":"Wednesday"}%

讨论

通过实现 org.eclipse.microprofile.rest.client.ex.ResponseExceptionMapper 接口,您还可以将状态码大于或等于 400 的响应转换为异常。 如果注册了多个映射器,则需要使用 javax.annotation.Priority 注解设置优先级。

创建以下 ResponseExecptionMapper 类以注册它,并使应用程序对 400 状态码抛出 IOExceptions

package org.acme.quickstart;

import java.io.IOException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;

public class CustomResponseExceptionMapper
                implements ResponseExceptionMapper<IOException> { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

    @Override
    public IOException toThrowable(Response response) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        return new IOException();
    }

    @Override
    public boolean handles(int status,
                            MultivaluedMap<String, Object> headers) { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
        return status >= 400 && status < 500;
    }

}

1

实现映射器接口

2

将响应转换为异常

3

默认情况下,会将状态码 ≥ 400 的任何响应转换,但您可以覆盖该方法以提供更小的范围。

ResponseExceptionMapper 是专门从 MicroProfile REST Client 规范中的扩展点,但您也可以使用 JAX-RS 规范提供的扩展模型:

ClientRequestFilter

调用请求发送到外部服务时触发的过滤器。

ClientResponseFilter

从外部服务接收到响应时调用的过滤器。

MessageBodyReader

调用后读取实体。

MessageBodyWriter

在支持主体的操作中编写请求主体。

ParamConverter

将资源中的参数转换为请求或响应中使用的格式。

ReadInterceptor

当从外部服务接收到响应时触发的监听器。

WriteInterceptor

请求发送到外部服务时触发的监听器。

您还可以使用@InjectMock@RestClient一起模拟WorldClockService接口:

@InjectMock
@RestClient
WorldClockService worldClockService;

参见

可以在以下网站找到 MicroProfile Rest Client 规范:

13.3 实现 CRUD 客户端

问题

您希望与另一个提供 CRUD 操作的 RESTful web 服务通信。

解决方案

使用 MicroProfile REST Client 和 JAX-RS 注解实现 CRUD 客户端。

到目前为止,您已经看到如何使用 MicroProfile REST Client 从外部服务获取信息。当服务是内部服务时,通常需要实现更多操作,如插入、删除或更新。

要实现这些操作,可以在 MicroProfile REST Client 上使用 JAX-RS 注解。让我们看一个例子:

package org.acme.quickstart;

import java.util.List;

import javax.ws.rs.BeanParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@Path("/developer")
@RegisterRestClient
@Consumes("application/json") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@Produces("application/json")
public interface DeveloperService {

  @HEAD ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
  Response head();

  @GET
  List<Developer> getDevelopers();

  @POST ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
  Response createDeveloper(
      @HeaderParam("Authorization") String authorization, ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
      Developer developer); ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)

  @DELETE ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png)
  @Path("/{userId}")
  Response deleteUser(@CookieParam("AuthToken") String authorization, ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/7.png)
      @PathParam("developerId") Long developerId);
}

1

请求和响应以 JSON 格式。

2

使用HEAD HTTP 方法。

3

使用POST HTTP 方法。

4

设置Authorization

5

开发人员内容作为主体发送

6

使用DELETE HTTP 方法。

7

设置AuthToken cookie

讨论

注意 JAX-RS 注解如何用于配置请求如何发送到其他服务。您无需编写任何程序代码。

这种方法对开发人员友好,有助于减少使用 JAX-RS Web 客户端时可能使用的样板代码。

当然,它也有一些缺点。例如,方法可能包含大量参数,因为有许多路径参数、要设置的标头和 cookie。为了解决此问题,请传递一个带有所有必需字段的 POJO(而不是在方法中设置它们)。

让我们为PUT需求创建一个 Java 类(即,授权头和路径参数):

package org.acme.quickstart;

import javax.ws.rs.HeaderParam;
import javax.ws.rs.PathParam;

public class PutDeveloper {

    @HeaderParam("Authorization") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    private String authorization;

    @PathParam("developerId") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    private String developerId;

    public String getAuthorization() {
        return authorization;
    }

    public void setAuthorization(String authorization) {
        this.authorization = authorization;
    }

    public String getDeveloperId() {
        return developerId;
    }

    public void setDeveloperId(String developerId) {
        this.developerId = developerId;
    }

}

1

设置Authorization头部

2

设置解析的path参数

使用前述类的interface方法如下:

@PUT
@Path("/{developerId}")
Response updateUser(@BeanParam PutDeveloper putDeveloper, ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    Developer developer);

1

BeanParam用于指示此类是参数聚合器

13.4 操纵头部

问题

您希望从传入请求中操纵和传播头部到传出服务(服务到服务认证)。

解决方案

使用 MicroProfile REST Client 功能操作头部。

当您需要与其他 RESTful Web 服务进行通信时,可能需要将一些请求头传递给传出/下游服务。其中一个典型情况是通过Authorization头部进行服务间认证。服务架构中的认证和授权通常通过传播令牌来解决,通常是 JWT 令牌,通过应用程序中所有服务进行传递。您可以在图 13-1 中看到这个想法。

qucb 1301

图 13-1. 服务到服务认证

MicroProfile REST Client 通过允许您使用注解在静态级别或通过实现ClientHeadersFactory接口在编程级别来传播和操作头部,简化了所有这些操作。

要在接口定义的一个方法或所有方法上设置头部,您可以使用org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam注解在方法级别或类级别设置具有静态值的头部:

@Path("/somePath")
@ClientHeaderParam(name="user-agent", value="curl/7.54.0") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
Response get();

1

user-agent设置为请求

value可以是方法调用,其中返回值将是头部的值:

@ClientHeaderParam(name="user-agent", value="{determineHeaderValue}") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
Response otherGet();

default String determineHeaderValue(String headerName) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    return "Hi-" + headerName;
}

1

设置要调用的方法

2

头部名称是方法的第一个参数

这些方法提供了基本的头部操作,但不会帮助传播从传入请求到传出服务的头部。还可以通过实现ClientHeadersFactory接口并将其注册到RegisterClientHeaders注解来添加或传播头部。

假设您的服务从上游服务的头部x-auth接收认证令牌,并且您的下游服务要求将此值设置为Authorization头部。让我们在 MicroProfile REST Client 中实现这些头部的重命名:

package org.acme.quickstart;

import java.util.List;

import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;

import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;

public class CustomClientHeadersFactory implements ClientHeadersFactory {

  @Override
  public MultivaluedMap<String, String> update(
      MultivaluedMap<String, String> incomingHeaders, ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
      MultivaluedMap<String, String> clientOutgoingHeaders) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

    final MultivaluedMap<String, String> headers =
      new MultivaluedHashMap<String, String>(incomingHeaders);
    headers.putAll(clientOutgoingHeaders); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

    final List<String> auth = headers.get("x-auth"); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
    headers.put("Authorization", auth);
    headers.remove("x-auth");

    return headers;
  }
}

1

来自入站 JAX-RS 请求的头部

2

客户端接口上指定的头部参数

3

添加所有头部

4

重命名头部值

最后,您需要使用R⁠e⁠g⁠i⁠s⁠t⁠e⁠r⁠C⁠l⁠i⁠e⁠n⁠t​H⁠e⁠a⁠d⁠e⁠r⁠s注解将此工厂注册到客户端:

@RegisterClientHeaders(CustomClientHeadersFactory.class) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public interface ConfigureHeaderServices {

1

为此客户端注册头部工厂

讨论

如果您只想按原样传播头部而不做任何修改,可以通过只用@RegisterClientHeaders注解 REST 客户端来实现。然后将使用默认头部工厂。

此默认工厂将从入站 JAX-RS 请求中传播指定的头部到出站请求。要配置要传播的头部,您需要将它们设置为逗号分隔的值,放置在org.eclipse.microprofile.rest.client.propagateHeaders属性下:

org.eclipse.microprofile.rest.client.propagateHeaders=Authorization,\
 MyCustomHeader

13.5 使用 REST 客户端处理多部分消息

问题

您希望发送多部分内容与需要其的 REST API 进行交互。

解决方案

使用 RESTEasy 多部分支持处理多部分消息。

有时,您需要连接的服务要求您发送多个内容主体嵌入到一个消息中,通常使用multipart/form-data MIME 类型。处理多部分 MIME 类型最简单的方法是使用 RESTEasy 多部分提供程序,该提供程序与 MicroProfile REST 客户端集成。

重要

此功能特定于 RESTEasy/Quarkus,不属于 MicroProfile REST 客户端规范。

在开始开发之前,您需要在构建工具中添加resteasy-multipart-provider依赖项:

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-multipart-provider</artifactId>
</dependency>

然后,您需要创建定义消息有效负载的模型对象。让我们定义一个包含两个部分的多部分消息,一个作为二进制内容,另一个作为字符串:

package org.acme.quickstart;

import java.io.InputStream;

import javax.ws.rs.FormParam;
import javax.ws.rs.core.MediaType;

import org.jboss.resteasy.annotations.providers.multipart.PartType;

public class MultipartDeveloperModel {

    @FormParam("avatar") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    @PartType(MediaType.APPLICATION_OCTET_STREAM) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    public InputStream file;

    @FormParam("name")
    @PartType(MediaType.TEXT_PLAIN)
    public String developerName;

}

1

JAX-RS 注解来定义包含在请求内部的表单参数

2

RESTEasy 注解来定义部分内容类型

最后,您需要声明一个新方法,该方法使用MultipartDeveloperModel对象作为参数,并注解为org.jboss.resteasy.annotations.providers.multipart.MultipartForm

@POST
@Consumes(MediaType.MULTIPART_FORM_DATA) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@Produces(MediaType.TEXT_PLAIN)
String sendMultipartData(@MultipartForm ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
                            MultipartDeveloperModel data); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

设置输出 MIME 类型为多部分

2

将参数定义为multipart/form-type MIME 类型

3

多部分数据

13.6 使用 REST 客户端配置 SSL

问题

您想要配置 REST 客户端以使用 SSL。

解决方案

MicroProfile REST 客户端提供了一种配置 SSL 以与其他服务通信的方式。

默认情况下,MicroProfile REST 客户端在使用 HTTPS 连接时使用 JVM 信任库来验证证书。但有时,特别是在内部服务的情况下,无法使用 JVM 信任库验证证书,因此需要提供自定义信任库。

MicroProfile REST Client 可以通过使用 trustStore 配置属性来设置自定义信任存储:

org.acme.quickstart.FruityViceService/mp-rest/trustStore= \
    classpath:/custom-truststore.jks ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
org.acme.quickstart.FruityViceService/mp-rest/trustStorePassword=acme ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
org.acme.quickstart.FruityViceService/mp-rest/trustStoreType=JKS ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

trustStore 设置信任存储位置;这可以是一个类路径资源(classpath:)或一个文件(file:

2

trustStorePassword 设置信任存储的密码

3

trustStoreType 设置信任存储的类型

Keystores 也提供了,在双向 SSL 连接中非常有用。

MicroProfile REST Client 可以通过使用 keyStore 配置属性来设置自定义密钥库。

org.acme.quickstart.FruityViceService/mp-rest/keyStore= \
    classpath:/custom-keystore.jks ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
org.acme.quickstart.FruityViceService/mp-rest/keyStorePassword=acme ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
org.acme.quickstart.FruityViceService/mp-rest/keyStoreType=JKS ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

1

keyStore 设置密钥存储的位置;这可以是一个类路径资源(classpath:)或一个文件(file:

2

keyStorePassword 设置信任存储的密码

3

keyStoreType 设置密钥存储的类型

最后,您可以实现 javax.net.ssl.HostnameVerifier 接口来覆盖当 URL 的主机名与服务器的识别主机名不匹配时的行为。然后,该接口的实现可以确定是否应允许此连接。

以下是主机名验证器的示例:

package org.acme.quickstart;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;

public class FruityHostnameVerifier implements HostnameVerifier {

    @Override
    public boolean verify(String hostname, SSLSession session) {
        if ("fruityvice.com".equals(hostname)) {
            return true;
        }

        return false;
    }

}

需要在配置文件中启用它:

org.acme.quickstart.FruityViceService/mp-rest/hostnameVerifier=\
org.acme.quickstart.FruityHostnameVerifier

讨论

大多数情况下,当您在本地运行测试时,可能没有安装所有连接到外部服务所需的信任存储或密钥存储。在这些情况下,您可能会针对服务的 HTTP 版本运行测试。然而,并非总是可能,在某些第三方服务中,只启用了 HTTPS 协议。

解决此问题的一个可能方案是配置 MicroProfile REST Client 以信任任何证书。为此,您需要配置客户端并提供自定义信任管理器:

package org.acme.quickstart;

import java.net.Socket;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.enterprise.context.ApplicationScoped;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509ExtendedTrustManager;

import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.eclipse.microprofile.rest.client.RestClientBuilder;

@ApplicationScoped ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class TrustAllFruityViceService {

  public FruityVice getFruitByName(String name) {
    FruityViceService fruityViceService = RestClientBuilder.newBuilder()
      .baseUri(URI.create("https://www.fruityvice.com/"))
      .hostnameVerifier(NoopHostnameVerifier.INSTANCE) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
      .sslContext(trustEverything()) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
      .build(FruityViceService.class);

    return fruityViceService.getFruitByName(name);
  }

  private static SSLContext trustEverything() { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)

    try {
      SSLContext sc = SSLContext.getInstance("SSL");
      sc.init(null, trustAllCerts(), new java.security.SecureRandom());
      HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
      return sc;
    } catch (KeyManagementException | NoSuchAlgorithmException e) {
      throw new IllegalStateException(e);
    }
  }

  private static TrustManager[] trustAllCerts() {
    return  new TrustManager[]{
      new X509ExtendedTrustManager(){

        @Override
        public X509Certificate[] getAcceptedIssuers() {
          return null;
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain,
                                       String authType)
          throws CertificateException {
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain,
                                       String authType)
          throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain,
                                      String authType,
                                      SSLEngine sslEngine)
          throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain,
                                       String authType,
                                       Socket socket)
          throws CertificateException {
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain,
                                       String authType,
                                       SSLEngine sslEngine)
          throws CertificateException {
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain,
                                       String authType,
                                       Socket socket)
          throws CertificateException {
        }
      }
    };
  }
}

1

创建一个 CDI bean;您需要使用 @Inject 而不是 @RestClient 来使用它

2

禁用主机验证

3

在不进行任何验证的情况下信任所有证书

4

使用空信任管理器自定义 SSLContext,有效地取消所有 SSL 检查

然后,如果您注入此实例而不是生产实例,则每个 HTTPS 请求都是有效的,不论外部服务使用的证书是什么。

第十四章:开发 Quarkus 应用程序使用 Spring API

到目前为止,您已经看到每个示例都是使用 CDI 注解(如 @Inject@Produces)、JAX-RS 注解或 Java EE 安全性注解开发的。但是 Quarkus 也为一些最常用的 Spring 库提供了兼容层,因此您可以使用关于 Spring Framework 的所有知识来开发 Quarkus 应用程序。

本章将包括以下几种方法:

  • Spring 依赖注入

  • Spring REST Web

  • Spring Data JPA

  • Spring Security

  • Spring Boot 配置

14.1 使用 Spring 依赖注入

问题

您希望使用 Spring 依赖注入(DI)API 来开发 Quarkus 应用程序。

解决方案

Quarkus 提供了一个 API 兼容层(使用扩展)来使用 Spring DI 注解。

虽然我们鼓励您使用 CDI 注解,但您也可以自由使用 Spring 注解,因为最终应用程序的行为方式完全相同。

开发了一个问候服务,就像书的开头一样。如果您熟悉 Spring Framework,很多东西看起来会很眼熟。

要添加 Spring DI 扩展,请运行以下命令:

./mvnw quarkus:add-extension -Dextensions="spring-di"

或者,您可以通过以下方式创建一个带有 Spring DI 扩展的项目:

mvn io.quarkus:quarkus-maven-plugin:1.4.1.Final:create \
 -DprojectGroupId=org.acme.quickstart \
 -DprojectArtifactId=spring-di-quickstart \
 -DclassName="org.acme.quickstart.GreeterResource" \
 -Dpath="/greeting" \
 -Dextensions="spring-di"

打开 application.properties 文件并添加一个新的属性:

greetings.message=Hello World

要将此配置值注入到任何类中,请使用 @org.springframework.beans.factory.annotation.Value Spring 注解:

package org.acme.quickstart;

import java.util.function.Function;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class AppConfiguration {

    @Bean(name = "suffix") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    public Function<String, String> exclamation() {
        return new Function<String, String>() { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
            @Override
            public String apply(String t) {
                return t + "!!";
            }
        };
    }
}

1

使用 @Configuration 注解将类定义为配置对象

2

创建一个向给定消息添加后缀的 bean

3

实现服务

请注意,在这两种情况下都使用了 Spring 注解。

可以使用 @org.springframework.beans.factory.annotation.Autowired@org.springframework.beans.factory.annotation.Qualifier 注解来注入 bean:

package org.acme.quickstart;

import org.springframework.stereotype.Service;

@Service ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class PrefixService {

    public String appendPrefix(String message) {
        return "- " + message;
    }

}

1

将此类设置为服务

并且可以使用构造函数而不是 @Autowired 进行注入:

private PrefixService prefixService;

public GreetingResource(PrefixService prefixService) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    this.prefixService = prefixService;
}

1

使用构造函数注入实例;请注意,不需要 @Autowired

最后,可以将所有这些操作结合起来产生以下输出:

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    String prefixed = prefixService.appendPrefix(message);
    return this.suffixComponent.apply(prefixed);
}

如果运行项目,您将能够看到所有对象都正确创建和注入,即使使用了 Spring DI 注解:

./mvnw compile quarkus:dev

curl http://localhost:8080/greeting

- Hello World!!

讨论

需要注意的是,Quarkus 不会启动一个 Spring 应用上下文实例。这是因为它的集成仅在 API 层面(注解、返回类型等),这意味着以下内容:

  • 使用任何其他 Spring 库将没有任何效果。稍后你会看到 Quarkus 提供了对其他 Spring 库的集成。

  • org.springframework.beans.factory.config.BeanPostProcessor 将不会被执行。

表 14-1 显示了 MicroProfile/CDI 与 Spring 之间的等效注解。

表 14-1. MicroProfile/CDI 与 Spring 的等效注解

Spring CDI / MicroProfile

|

@Autowired

|

@Injecct

|

|

@Qualifier

|

@Named

|

|

@Value

|

@ConfigProperty。典型用例的表达式语言是支持的。

|

|

@Component

|

@Singleton

|

|

@Service

|

@Singleton

|

|

@Repository

|

@Singleton

|

|

@Configuration

|

@ApplicationScoped

|

|

@Bean

|

@Produces

|

14.2 使用 Spring Web

问题

你希望使用 Spring Web API 在 Quarkus 中进行开发。

解决方案

Quarkus 提供了一个 API 兼容层(使用扩展)来使用 Spring Web 注解/类。

要添加 Spring Web 扩展,运行以下命令:

./mvnw quarkus:add-extension -Dextensions="spring-web"

仅使用 Spring Web 注解创建一个新的资源:

package org.acme.quickstart;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
@RequestMapping("/greeting") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
public class SpringController {

  @GetMapping ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
  public ResponseEntity<String> getMessage() { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
    return ResponseEntity.ok("Hello");
  }

  @GetMapping("/{name}")
  public String hello(@PathVariable(name = "name") String name) { ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png)
    return "hello " + name;
  }
}

1

REST 资源定义

2

映射根路径

3

设置 GET HTTP 方法

4

返回 Spring 的 ResponseEntity

5

获取路径信息

仅使用 Spring 注解和类来实现资源。没有 JAX-RS API 的痕迹。

讨论

你已经看到如何在 Quarkus 应用中使用 Spring 依赖注入注解。Quarkus 通过扩展与 Spring Web 集成。

虽然我们鼓励你使用 JAX_RS 注解,但你可以自由使用 Spring Web 类和注解。无论使用哪种,最终应用的行为都会是一样的。

Quarkus 仅支持 Spring Web 的与 REST 相关的特性。总之,所有 @RestController 的特性都得到了支持,除了与通用 @Controller 相关的那些特性。

支持以下 Spring Web 注解:@RestController@RequestMapping@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping@RequestParam@RequestHeader@MatrixVariable@PathVariable@CookieValue@RequestBody@ResponseStatus@ExceptionHandler(仅用于 @RestControllerAdvice 类)、以及 @RestControllerAdvice(仅支持 @ExceptionHandler 能力)。

默认支持的 REST 控制器返回类型包括:基本类型、String(作为文字,而非 MVC 支持)、POJO 和 org.springframework.http.ResponseEntity

默认支持的 REST 控制器方法参数包括:基本类型、String、POJO、javax.servlet.http.HttpServletRequestjavax.servlet.http.HttpServletResponse

默认支持的异常处理程序返回类型包括:org.springframework.http.ResponseEntityjava.util.Map

下列是默认支持的方法参数,用于异常处理:java.lang.Exception 或任何其他子类型,javax.servlet.http.HttpServletRequestjavax.servlet.http.HttpServletResponse

重要提示

要使用 javax.servlet 类,您需要注册 quarkus-undertow 依赖项。

表 14-2 显示了 JAX-RS 和 Spring Web 之间的等效注解。

表 14-2. JAX-RS 和 Spring Web 中的等效注解

Spring JAX-RS

|

@RequestController

|

@RequestMapping(path="/api")

|

@Path("/api")

|

|

@RequestMapping(consumes="application/json")

|

@Consumes("application/json")

|

|

@RequestMapping(produces="application/json")

|

@Produces("application/json")

|

|

@RequestParam

|

@QueryParam

|

|

@PathVariable

|

@PathParam

|

|

@RequestBody

|

@RestControllerAdvice

|

@ResponseStatus

|

使用 javax.ws.rs.core.Response

|

|

@ExceptionHandler

|

实现 javax.ws.rs.ext.ExceptionMapper 接口

|

14.3 使用 Spring Data JPA

问题

您想要使用 Spring Data JPA API 在 Quarkus 中开发持久层。

解决方案

Quarkus 提供了一个 API 兼容层(使用扩展)来使用 Spring Data JPA 类。

要添加 Spring Web 扩展,请运行以下命令:

./mvnw quarkus:add-extension -Dextensions="spring-data-jpa"

使用 Panache 或 Spring Data JPA 的主要区别在于,您的 repository 类必须实现 Spring Data 的 org.springframework.data.repository.CrudRepository 接口,而不是 io.quarkus.hibernate.orm.panache.PanacheRepository。但其他部分,如在 application.properties 中定义实体或配置数据源,完全相同。

创建一个名为 org.acme.quickstart.DeveloperRepository 的新类:

package org.acme.quickstart;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface DeveloperRepository extends CrudRepository<Developer, Long> { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    List<Developer> findByName(String name); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
}

1

定义一个 Spring Data JPA CRUD repository

2

派生查询方法

运行项目并发送一些请求来验证对象是否使用 Spring Data 接口持久化。要执行此操作,请在终端中运行以下命令:

./mvnw compile quarkus:dev

curl -d '{"name":"Ada"}' -H "Content-Type: application/json" \
 -X POST http://localhost:8080/developer -v

< HTTP/1.1 201 Created
< Content-Length: 0
< Location: http://localhost:8080/developer/1
<
* Connection #0 to host localhost left intact

讨论

在 第七章 中,您了解了如何使用 Quarkus 开发持久性代码,特别是使用 Panache 框架。Quarkus 还通过扩展与 Spring Data JPA 集成。

尽管我们鼓励您使用 Panache 框架,但您也可以自由地使用 Spring Data JPA 类,因为最终应用程序的行为完全相同。

Quarkus 支持 Spring Data JPA 的一部分功能,基本上是最常用的功能。

支持定义存储库的以下接口:

  • org.springframework.data.repository.Repository

  • org.springframework.data.repository.CrudRepository

  • org.springframework.data.repository.CrudRepository

  • org.springframework.data.repository.PagingAndSortingRepository

  • org.springframework.data.jpa.repository.JpaRepository

重要

更新数据库的方法会自动用 @Transactional 注释。如果您使用 @org.springframework.data.jpa.repository.Query,则需要使用 @org.springframework.data.jpa.repository.Modifying 注释方法使其具有事务性。

在撰写本文时,以下功能不受支持:

  • org.springframework.data.repository.query.QueryByExampleExecutor 的方法

  • QueryDSL 支持

  • 自定义代码库中所有存储库接口的基本存储库

  • java.util.concurrent.Future 或其子类作为存储库方法的返回类型

  • 使用 @Query 时的本机和命名查询

这些限制可能在不久的将来得到修复。

14.4 使用 Spring Security

问题

您想要使用 Spring Security API 来保护资源。

解决方案

Quarkus 提供了一个 API 兼容性层(使用扩展)来使用 Spring Security 类。

要添加 Spring Security 扩展(以及身份提供者),请运行以下命令:

./mvnw quarkus:add-extension \
 -Dextensions="quarkus-spring-security, quarkus-spring-web, \
 quarkus-elytron-security-properties-file"

从这一点开始,可以使用 Spring Security 注解 (org.springframework.security.access.annotation.Securedorg.springframework.security.access.prepost.PreAuthorize) 代替 Java EE 注解 (@javax.annotation.security.RolesAllowed) 来保护代码:

package org.acme.quickstart;

import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
public class GreetingController {
    @GetMapping
    @Secured("admin") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    public String helloAdmin() {
        return "hello from admin";
    }

    @PreAuthorize("hasAnyRole('user')") ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    @GetMapping
    @RequestMapping("/any")
    public String helloUsers() {
        return "hello from users";
    }
}

1

@Secured 注解

2

@PreAuthorize 注解以添加表达式支持

application.properties 文件中注册有效用户和角色,因为 Elytron 文件属性扩展已注册为身份提供者:

quarkus.security.users.embedded.enabled=true
quarkus.security.users.embedded.plain-text=true
quarkus.security.users.embedded.users.alexandra=aixa
quarkus.security.users.embedded.roles.alexandra=admin,user
quarkus.security.users.embedded.users.ada=dev
quarkus.security.users.embedded.roles.ada=user

只有 alexandra 可以访问两个端点,而 ada 只能访问用户端点。

讨论

在 第十一章 中,您学习了如何使用 Java EE 安全注解(@RolwsAllowed)保护 RESTful Web 服务。 Quarkus 通过扩展集成了 Spring Security,允许使用 Spring Security 注解。

需要注意的是,Spring 安全集成发生在 API 层面,像 Elytron 文件属性这样的身份提供者实现仍然是必需的。

Quarkus 支持 Spring Security @PreAuthorze 表达式语言的子集,基本上是最常用功能的集合:

@PreAuthorize("hasRole('admin')")
@PreAuthorize("hasRole(@roles.USER)") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

@PreAuthorize("hasAnyRole(@roles.USER, 'view')")

@PreAuthorize("permitAll()")
@PreAuthorize("denyAll()")

@PreAuthorize("isAnonymous()")
@PreAuthorize("isAuthenticated()")

1

其中 roles 是使用 @Component 注解定义的 bean,而 USER 是类的公共字段

还支持条件表达式:

@PreAuthorize("#person.name == authentication.principal.username")
public void doSomethingElse(Person person){}

@PreAuthorize("@personChecker.check(#person,
 authentication.principal.username)")
public void doSomething(Person person){}

@Component
public class PersonChecker {
    public boolean check(Person person, String username) {
        return person.getName().equals(username);
    }
}

@PreAuthorize("hasAnyRole('user', 'admin') AND #user == principal.username")
public void allowedForUser(String user) {}

14.5 使用 Spring Boot 属性

问题

您想要使用 Spring Boot 将配置属性映射到 Java 对象。

解决方案

Quarkus 提供了一个 API 兼容性层(使用扩展),用于使用 Spring Boot 配置属性。

Quarkus 与 Spring Boot 集成为一个扩展,因此可以使用 @org.springframework.boot.context.properties.ConfigurationProperties 注解将配置属性映射到 Java 对象。

要添加 Spring Boot 扩展(以及其他 Spring 集成),请运行以下命令:

./mvnw quarkus:add-extension \
 -Dextensions="quarkus-spring-di, quarkus-spring-web, \
 quarkus-spring-boot-properties,
 quarkus-hibernate-validator"

添加一些要绑定到 Java 对象的配置属性:

greeting.message=Hello World
greeting.configuration.uppercase=true

下一步是创建带有 getter/setter 的 POJOs,将文件中的配置属性绑定到 Java 对象。 需要注意的是,属性 uppercase 定义在名为 configuration 的子类别中,这影响到了 POJO 类的创建方式,因为每个子类别必须添加到自己的类中:

package org.acme.quickstart;

import javax.validation.constraints.Size;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "greeting") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class GreetingConfiguration {

    @Size(min = 2) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    private String message;
    private Configuration configuration; ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

    public void setMessage(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setConfiguration(Configuration configuration) {
        this.configuration = configuration;
    }

    public Configuration getConfiguration() {
        return configuration;
    }

}

1

使用 ConfigurationProperties 注解父类,并设置配置属性的前缀

2

支持 Bean 验证注释

3

子类别 configuration 映射到具有相同名称的字段中

子类别 POJO 只是具有 uppercase 属性的 Java 类。

package org.acme.quickstart;

public class Configuration {

    private boolean uppercase;

    public boolean isUppercase() {
        return uppercase;
    }

    public void setUppercase(boolean uppercase) {
        this.uppercase = uppercase;
    }
}

配置对象被注入到任何类中,就像使用 @Inject@Autowired 注解其他 bean 一样:

@Autowired ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
GreetingConfiguration greetingConfiguration;

@GetMapping
public String hello() {
    if (greetingConfiguration.getConfiguration().isUppercase()) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        return greetingConfiguration.getMessage().toUpperCase();
    }
    return greetingConfiguration.getMessage();
}

1

注入配置对象并将数据绑定到其中

2

配置属性会自动填充到 Java 对象中

第十五章: 使用响应式编程模型

我们都熟悉主导企业软件开发数十年的客户端-服务器架构。然而,最近我们的架构风格发生了变化。除了标准的客户端-服务器方法外,我们还有消息驱动的应用程序,微服务,响应式应用程序甚至无服务器!使用 Quarkus 可以创建所有这些类型的应用程序。在接下来的示例中,您将了解有关响应式编程模型,消息总线和流式传输的信息。

注意

Quarkus(以及本书!)使用 SmallRye Mutiny 作为其响应式库。您可以在 SmallRye Mutiny 上阅读更多信息。还支持 RxJava 和 Reactor,但它们不是首选选择。要使用它们中的任何一个,您需要使用从 Mutiny 转换器。

15.1 创建异步 HTTP 端点

问题

您希望创建异步 HTTP 端点。

解决方案

Quarkus 与 Java Streams,Eclipse MicroProfile 响应式规范和 SmallRye Mutiny 集成。这些集成使得支持异步 HTTP 端点变得非常容易。您首先需要确定希望使用哪些库。如果您希望使用原生 Streams 或 MicroProfile 响应式规范,您需要添加 quarkus-smallrye-reactive-streams-operators 扩展。如果您想使用 SmallRye Mutiny,则需要向项目添加 quarkus-resteasy-mutiny 扩展。

注意

从现在开始,Mutiny 将是 Quarkus 内所有响应式相关内容的首选库。

一旦扩展位于适当位置,您需要做的就是在您的 HTTP 端点中返回一个响应式类:

@GET
@PATH("/reactive")
@Produces(MediaType.TEXT_PLAIN) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public CompletionStage<String> reactiveHello() { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    return ReactiveStreams.of("h", "e", "l", "l", "o")
        .map(String::toUpperCase)
        .toList()
        .run()
        .thenApply(list -> list.toString());
}

1

自然地,任何有效的 MediaType 都是有效的;为简单起见,我们使用了纯文本

2

CompletionStage 来自 java.util.concurrent

对于 Mutiny,这个示例变成了以下内容:

@GET
@PATH("/reactive")
@Produces(MediaType.TEXT_PLAIN)
public Multi<String> helloMutiny() {
    return Multi.createFrom().items("h", "e", "l", "l", "o");
}

参见

欲了解更多信息,请访问以下网站:

15.2 异步流式处理数据

问题

您希望以异步方式流式传输数据。

解决方案

与创建异步 HTTP 端点非常相似,Quarkus 允许您使用服务器发送事件(SSE)或服务器端事件从应用程序中流式传输事件。在这种情况下,您需要返回一个 Publisher 并告诉 JAX-RS 您的端点生成 MediaType.SERVER_SENT_EVENTS。以下是每 500 毫秒流式传输 long 的示例:

@GET
@Path("/integers")
@Produces(MediaType.SERVER_SENT_EVENTS) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public Publisher<Long> longPublisher() { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    return Multi.createFrom()
            .ticks().every(Duration.ofMillis(500));
}

1

确保告诉 JAX-RS 你在使用 SSEs

2

方法的返回类型必须是来自 Reactive Streams 库的 org.reactivestream.Publisher

使用 Mutiny,一个 Multi 是一个 Publisher,这样做更加简单,只需返回一个 Multi

另请参见

更多信息,请访问以下网站:

15.3 使用消息传递来解耦组件

问题

你想使用消息传递来解耦组件。

解决方案

Quarkus 使用的一个底层/捆绑框架是 Vert.x。Vert.x 是一个用于构建异步、事件驱动、响应式应用的框架,类似于 Quarkus!Quarkus 利用 Vert.x 事件总线发送和接收事件/消息,类之间是解耦的。

要使用 Vert.x,就像许多 Quarkus 的特性一样,你需要将适当的扩展添加到你的应用中。Vert.x 扩展的名称是 vertx

处理事件/消息

我们将首先看一下监听或消费事件。 在 Quarkus 中消费事件的最简单方式是使用 io.quarkus.vertx.ConsumeEvent 注解。@ConsumeEvent 有一些属性,我们稍后会讲到,但让我们先看看它的实际应用:

package com.acme.vertx;

import javax.enterprise.context.ApplicationScoped;

import io.quarkus.vertx.ConsumeEvent;

@ApplicationScoped
public class GreetingService {
    @ConsumeEvent   ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    public String consumeNormal(String name) { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        return name.toUpperCase();
    }
}

1

如果没有设置值,事件的地址是 bean 的完全限定名;在这种情况下,应该是 com.acme.vertx.GreetingService

2

消费者的参数是消息体;如果方法返回任何内容,它会被封装为消息响应。

发送事件/消息

要发送事件,你将与 Vert.x 事件总线进行交互。你可以通过注入获取实例:@Inject io.vertx.axle.core.eventbus.EventBus bus。你将主要使用事件总线上两个方法:

send

发送消息并可选地期待回复

publish

向每个监听器发布消息

bus.send("address", "hello"); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
bus.publish("address", "hello"); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
bus.send("address", "hello, how are you?") ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    .thenAccept(msg -> {
    // do something with the message });

1

向特定地址发送消息,一个消费者接收后,便忽略响应。

2

向特定地址发布消息,所有消费者都接收该消息。

3

向特定地址发送消息,并期待回复。

你应该已经有足够的信息来使用 Vert.x 事件机制创建你自己的问候服务版本!

讨论

您还可以返回 CompletionStage 以异步方式处理事件。最后,如果希望使用 io.vertx.axle.core.eventbus.Message 作为方法参数,可以这样做,并在事件处理程序中访问消息的其余部分。

火而忘交互方式同样简单——只需从您的方法返回 void 即可。

警告

Vert.x 事件循环上调用消耗事件的方法。Vert.x 的第一个原则是永不阻塞事件循环。您的代码 必须 是非阻塞的。 如果 需要方法阻塞,将 @ConsumeEvent 上的 blocking 属性设置为 true

要配置事件处理程序的名称或地址,请使用 value 参数:

    @ConsumeEvent(value = "greeting")
    public String consumeNamed(String name) {
        return name.toUpperCase();
    }

参见

欲了解更多信息,请访问以下网站:

15.4 响应 Apache Kafka 消息

问题

您希望响应 Apache Kafka 消息。

解决方案

Quarkus 利用 Eclipse MicroProfile Reactive Messaging 与 Apache Kafka 进行交互。

反应式消息规范是建立在三个主要概念之上的:

  1. Message

  2. @Incoming

  3. @Outgoing

本配方专注于 Message@Incoming;如果您需要进行反向操作,请参阅配方 15.5。

消息

简而言之,Message 是围绕有效载荷的封套。封套还携带可选的元数据,尽管更多时候,您只关心有效载荷。

@Incoming 注解

此注解 (org.eclipse.microprofile.reactive.messaging.Incoming) 表示该方法消耗消息流。唯一的属性是流或主题的名称。方法以此方式注解是为了处理链的末端,也称为 sink。以下是在 Quarkus 中的几个用法:

package org.acme.kafka;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

import javax.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Message;

@ApplicationScoped
public class CharacterReceiver {
  @Incoming("ascii-char")
  public CompletionStage<Void> processKafkaChar(Message<String> character) {
    return CompletableFuture.runAsync(() -> {
      System.out.println("Received a message from Kafka "
          + "using CompletableFuture: '" + character.getPayload() + "'");
    });
  }

  @Incoming("ascii-char")
  public void processCharacter(String character) {
    System.out.println("Received a String from kafka: '" + character + "'");
  }
}

您可以看到两种方法都可以运行;但是,在 processKafkaCharacter 的情况下,它接收一个 Message 并返回一个 CompletionStage。如果您的方法接收 Message 作为参数,则 必须 返回 CompletionStage

如果您仅对有效载荷感兴趣,则无需担心其中任何内容,可以简单接受有效载荷的类型并返回 void,就像前一个代码中的 processCharacter 演示的那样。

配置

预期地,您需要配置应用程序以与 Apache Kafka 进行通信:

mp.messaging.incoming.ascii-char.connector=smallrye-kafka
mp.messaging.incoming.ascii-char.value.deserializer=org.apache.kafka.common\
 .serialization\
 .StringDeserializer
mp.messaging.incoming.ascii-char.broadcast=true

在前述代码中,我们有多个订阅者,因此需要使用 broadcast=truebroadcast 属性让 MicroProfile Reactive Messaging(SmallRye 是一个实现)知道接收到的消息可以分发给多个订阅者。

配置的语法如下:

mp.messaging.[outgoing|incoming].{channel-name}.property=value

channel-name 段中的值必须与 @Incoming 中设置的值匹配(以及在下一个配方中涵盖的 @Outgoing)。

您可以在 SmallRye Reactive Messaging: Apache Kafka 中看到一些合理的默认值。

讨论

要快速在开发环境中使用 Apache Kafka,您可以访问 “另请参阅” 中列出的网站,或者使用以下 docker-compose.yml 文件以及 docker-compose

version: '2'

services:

  zookeeper:
    image: strimzi/kafka:0.11.3-kafka-2.1.0
    command: [
      "sh", "-c",
      "bin/zookeeper-server-start.sh config/zookeeper.properties"
    ]
    ports:
      - "2181:2181"
    environment:
      LOG_DIR: /tmp/logs

  kafka:
    image: strimzi/kafka:0.11.3-kafka-2.1.0
    command: [
      "sh", "-c",
      "bin/kafka-server-start.sh config/server.properties
      --override listeners=$${KAFKA_LISTENERS}
      --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS}
      --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}"
    ]
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      LOG_DIR: "/tmp/logs"
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181

另请参阅

欲了解更多信息,请访问以下网址:

15.5 向 Apache Kafka 发送消息

问题

您想要发送消息到 Apache Kafka。

解决方案

首先,您需要将 quarkus-smallrye-reactive-messaging-kafka 扩展添加到您的项目中。在本示例中,我们还使用了 SmallRye Mutiny,因此还需添加 io.smallrye.reactive:mutiny 依赖项。

要将消息发送到 Apache Kafka,请使用来自 Eclipse MicroProfile Reactive Messaging 的 @Outgoing 注解。

当您生成要发送到 Apache Kafka 的数据时,您将使用 org.eclipse.microprofile.reactive.messaging.Outgoing 注解您的方法。您可以发送一系列消息或单个消息。如果您希望发布一系列消息,则必须返回 org.reactivestreams.Publisherorg.eclipse.microprofile.reactive.streams.operators.PublisherBuilder。如果您希望发布单个消息,请返回 org.eclipse.microprofile.reactive.messaging.Messagejava.util.concurrent.CompletionStage 或您消息负载的相应类型。

以下是一个基本示例,每秒创建一个新的 ASCII 字符并将其发送到 “letter-out” 通道:

package org.acme.kafka;

import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;

import io.smallrye.mutiny.Multi;
import org.eclipse.microprofile.reactive.messaging.Outgoing;
import org.reactivestreams.Publisher;

public class CharacterGenerator {
    @Outgoing("letter-out")
    public Publisher<String> generate() {
        return Multi.createFrom()
                .ticks().every(Duration.ofSeconds(1))
                .map(tick -> {
                    final int i = ThreadLocalRandom.current().nextInt(95);
                    return String.valueOf((char) (i + 32));
                });
    }
}

@Outgoing 的 value 属性是必需的,是出站通道的名称。在本示例中,我们使用了 SmallRye Mutiny,但您也可以使用任何返回 org.reactivestreams.Publisher 实例的内容;例如,来自 RXJava2 的 Flowable 也可以很好地工作。

还需要以下配置:

mp.messaging.outgoing.letter-out.connector=smallrye-kafka
mp.messaging.outgoing.letter-out.topic=ascii-char
mp.messaging.outgoing.letter-out.value.serializer=org.apache.kafka.common\
 .serialization\
 .StringSerializer

讨论

如果您发现自己需要以命令的方式发送消息,您可以在应用程序中注入一个 org.eclipse.microprofile.reactive.messaging.Emitter

@Inject @Channel("price-create") Emitter<Double> priceEmitter;

@POST
@Consumes(MediaType.TEXT_PLAIN)
public void addPrice(Double price) {
    priceEmitter.send(price);
}

另请参阅

欲了解更多信息,请查看以下内容:

15.6 将 POJOs 编组到/从 Kafka

问题

您希望将 POJOs 序列化/反序列化到 Kafka。

解决方案

Quarkus 具有处理 JSON Kafka 消息的功能;您需要选择 JSONB 或 Jackson 作为实现。所需的扩展名为 quarkus-resteasy-jsonbquarkus-resteasy-jackson,具体取决于您的偏好。

然后,您需要创建一个反序列化器。最简单的方法是扩展 JSONB 的 JsonDeserializer 或 Jackson 的 ObjectMapperDeserializer。这是 Book 类及其反序列化器:

package org.acme.kafka;

public class Book {
    public String title;
    public String author;
    public Long isbn;

    public Book() {
    }

    public Book(String title, String author, Long isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
    }
}

对于 JSONB,反序列化器如下所示:

package org.acme.kafka;

import io.quarkus.kafka.client.serialization.JsonbDeserializer;

public class BookDeserializer extends JsonbDeserializer<Book> {
    public BookDeserializer() {
        super(Book.class);
    }
}

对于杰克逊来说,这同样容易:

package com.acme.kafka;

import io.quarkus.kafka.client.serialization.ObjectMapperDeserializer;

public class BookDeserializer extends ObjectMapperDeserializer<Book> {
    public BookDeserializer() {
        super(Book.class);
    }
}

您需要做的最后一件事是将您的反序列化器和默认序列化器添加到 Quarkus 配置中:

# Configure the Kafka source (we read from it)
mp.messaging.incoming.book-in.connector=smallrye-kafka
mp.messaging.incoming.book-in.topic=book-in
mp.messaging.incoming.book-in.value.deserializer=com.acme\
 .kafka.BookDeserializer

# Configure the Kafka sink (we write to it)
mp.messaging.outgoing.book-out.connector=smallrye-kafka
mp.messaging.outgoing.book-out.topic=book-out
mp.messaging.outgoing.book-out.value.serializer=io.quarkus.kafka\
 .client.serialization\
 .JsonbSerializer

或者,对于 Jackson:

# Configure the Kafka source (we read from it)
mp.messaging.incoming.book-in.connector=smallrye-kafka
mp.messaging.incoming.book-in.topic=book-in
mp.messaging.incoming.book-in.value.deserializer=com.acme\
 .kafka.BookDeserializer

# Configure the Kafka sink (we write to it)
mp.messaging.outgoing.book-out.connector=smallrye-kafka
mp.messaging.outgoing.book-out.topic=book-out
mp.messaging.outgoing.book-out.value.serializer=io.quarkus.kafka.client\
 .serialization\
 .ObjectMapperSerializer

讨论

如果您使用 JSONB 并且不希望为每个通过电线发送的 POJO 创建反序列化器,您可以使用通用的io.vertx.kafka.client.serialization.JsonObjectDeserializer。返回的对象将是一个javax.json.JsonObject。在这里,我们选择使用默认的序列化器。

如果您需要比基本功能更多的东西,您还可以创建自己的序列化器。

15.7 使用 Kafka Streams API

问题

您希望使用 Kafka Streams API 来查询数据。

解决方案

Quarkus 中的 Apache Kafka 扩展(quarkus-smallrye-reactive-messaging-kafka)与 Apache Kafka Streams API 集成。这个例子有点深奥,需要一些额外的移动部件。当然,您需要一个运行中的 Apache Kafka 实例。如果您还没有可用的实例,我们建议您使用 Kubernetes 设置一个 Apache Kafka 实例。如果您只是需要开发用的东西,您可以使用以下docker-compose.yml文件:

version: '3.5'

services:
  zookeeper:
    image: strimzi/kafka:0.11.3-kafka-2.1.0
    command: [
      "sh", "-c",
      "bin/zookeeper-server-start.sh config/zookeeper.properties"
    ]
    ports:
      - "2181:2181"
    environment:
      LOG_DIR: /tmp/logs
    networks:
      - kafkastreams-network
  kafka:
    image: strimzi/kafka:0.11.3-kafka-2.1.0
    command: [
      "sh", "-c",
      "bin/kafka-server-start.sh config/server.properties
      --override listeners=$${KAFKA_LISTENERS}
      --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS}
      --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT}
      --override num.partitions=$${KAFKA_NUM_PARTITIONS}"
    ]
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      LOG_DIR: "/tmp/logs"
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_NUM_PARTITIONS: 3
    networks:
      - kafkastreams-network

此解决方案的下一部分是创建一个生成值并将这些生成的值发送到 Kafka 主题的生产者。我们将使用一个点唱机的概念。我们的点唱机将包含多首歌曲及其艺术家,以及歌曲播放次数。每个都将发送到不同的主题,然后由另一个服务聚合在一起:

package org.acme.kafka.jukebox;

import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

import javax.enterprise.context.ApplicationScoped;

import io.smallrye.mutiny.Multi;
import io.smallrye.reactive.messaging.kafka.KafkaRecord;
import org.eclipse.microprofile.reactive.messaging.Outgoing;
import org.jboss.logging.Logger;

@ApplicationScoped
public class Jukebox {
    private static final Logger LOG = Logger.getLogger(Jukebox.class);

    private ThreadLocalRandom random = ThreadLocalRandom.current();

    private List<Song> songs = Collections.unmodifiableList(
            Arrays.asList(
                new Song(1, "Confessions", "Usher"),
                new Song(2, "How Do I Live", "LeAnn Rimes"),
                new Song(3, "Physical", "Olivia Newton-John"),
                new Song(4, "You Light Up My Life", "Debby Boone"),
                new Song(5, "The Twist", "Chubby Checker"),
                new Song(6, "Mack the Knife", "Bobby Darin"),
                new Song(7, "Night Fever", "Bee Gees"),
                new Song(8, "Bette Davis Eyes", "Kim Carnes"),
                new Song(9, "Macarena (Bayside Boys Mix)", "Los Del Rio"),
                new Song(10, "Yeah!", "Usher")
            )
    );

    @Outgoing("song-values")
    public Multi<KafkaRecord<Integer, String>> generate() {
        return Multi.createFrom().ticks().every(Duration.ofMillis(500))
                .onOverflow().drop()
                .map(tick -> {
                   Song s = songs.get(random.nextInt(songs.size()));
                   int timesPlayed = random.nextInt(1, 100);

                   LOG.infov("song {0}, times played: {1,number}",
                           s.title, timesPlayed);
                   return KafkaRecord.of(s.id, Instant.now()
                                               + ";" + timesPlayed);
                });
    }

    @Outgoing("songs")
    public Multi<KafkaRecord<Integer, String>> songs() {
        return Multi.createFrom().iterable(songs)
                .map(s -> KafkaRecord.of(s.id,
                        "{\n" +
                        "\t\"id\":\""+ s.id + "\",\n" +
                        "\t\"title\":\"" + s.title + "\",\n" +
                        "\t\"artist\":\"" + s.artist + "\"\n" +
                        "}"
                        ));
    }

    private static class Song {
        int id;
        String title;
        String artist;

        public Song(int id, String title, String artist) {
            this.id = id;
            this.title = title;
            this.artist = artist;
        }
    }
}

每 500 毫秒,一个包含歌曲及其播放次数以及时间戳的新消息将发送到songs主题。我们将跳过额外的配置——您可以在食谱 15.5 中查看详细步骤。

接下来,我们需要构建管道。第一步是创建一些值持有者:

package org.acme.kafka.jukebox;

public class Song {
    public int id;
    public String title;
    public String artist;
}

现在我们需要一个播放计数器:

package org.acme.kafka.jukebox;

import java.time.Instant;

public class PlayedCount {
    public int count;
    public String title;
    public String artist;
    public int id;
    public Instant timestamp;

    public PlayedCount(int id, String title, String artist,
                       int count, Instant timestamp) {
        this.count = count;
        this.title = title;
        this.artist = artist;
        this.id = id;
        this.timestamp = timestamp;
    }
}

最后,对于值持有者,这是一个对象,用于在管道中处理消息时跟踪值的聚合:

package org.acme.kafka.jukebox;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Aggregation {
    public int songId;
    public String songTitle;
    public String songArtist;
    public int count;
    public int sum;
    public int min;
    public int max;
    public double avg;

    public Aggregation updateFrom(PlayedCount playedCount) {
        songId = playedCount.id;
        songTitle = playedCount.title;
        songArtist = playedCount.artist;

        count++;
        sum += playedCount.count;
        avg = BigDecimal.valueOf(sum / count)
                .setScale(1, RoundingMode.HALF_UP).doubleValue();
        min = Math.min(min, playedCount.count);
        max = Math.max(max, playedCount.count);

        return this;
    }
}

现在,进入魔法部分!拼图的最后一部分是流式查询实现。我们只需要定义一个作为 CDI 生产者的方法,返回一个 Apache Kafka Stream Topology。Quarkus 将负责配置,生命周期将处理 Kafka Streams 引擎:

package org.acme.kafka.jukebox;

import java.time.Instant;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;

import io.quarkus.kafka.client.serialization.JsonbSerde;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.Consumed;
import org.apache.kafka.streams.kstream.GlobalKTable;
import org.apache.kafka.streams.kstream.Materialized;
import org.apache.kafka.streams.kstream.Produced;
import org.apache.kafka.streams.state.KeyValueBytesStoreSupplier;
import org.apache.kafka.streams.state.Stores;

@ApplicationScoped
public class TopologyProducer {
    static final String SONG_STORE = "song-store";

    private static final String SONG_TOPIC = "songs";
    private static final String SONG_VALUES_TOPIC = "song-values";
    private static final String SONG_AGG_TOPIC = "song-aggregated";

    @Produces
    public Topology buildTopology() {
        StreamsBuilder builder = new StreamsBuilder();

        JsonbSerde<Song> songSerde = new JsonbSerde<>(Song.class);
        JsonbSerde<Aggregation> aggregationSerde =
                new JsonbSerde<>(Aggregation.class);

        KeyValueBytesStoreSupplier storeSupplier =
                Stores.persistentKeyValueStore(SONG_STORE);

        GlobalKTable<Integer, Song> songs = builder.globalTable(SONG_TOPIC,
                Consumed.with(Serdes.Integer(), songSerde));

        builder.stream(SONG_VALUES_TOPIC, Consumed.with(Serdes.Integer(),
                Serdes.String()))
                .join(
                        songs,
                        (songId, timestampAndValue) -> songId,
                        (timestampAndValue, song) -> {
                            String[] parts = timestampAndValue.split(";");
                            return new PlayedCount(song.id, song.title,
                                    song.artist,
                                    Integer.parseInt(parts[1]),
                                    Instant.parse(parts[0]));
                        }
                )
                .groupByKey()
                .aggregate(
                        Aggregation::new,
                        (songId, value, aggregation) ->
                                aggregation.updateFrom(value),
                        Materialized.<Integer, Aggregation> as(storeSupplier)
                            .withKeySerde(Serdes.Integer())
                            .withValueSerde(aggregationSerde)
                )
                .toStream()
                .to(
                        SONG_AGG_TOPIC,
                        Produced.with(Serdes.Integer(), aggregationSerde)
                );
        return builder.build();
    }
}

解释所有发生的事情超出了本教程的范围,但是 Kafka Streams 站点在“另请参阅”中链接了完整的教程和视频专门讨论此主题。简而言之,这将连接到先前的songssong-values主题,然后根据歌曲 ID 合并值。然后对播放计数值执行一些聚合操作,并将输出发送回 Apache Kafka 到一个新主题。

讨论

我们建议使用kafkacat工具来查看发送到主题的消息。

重要

在这两个 Apache Kafka 示例中,我们仅连接了单个客户端和机器。这不是 Quarkus 的限制,而是我们为了简化示例而做的。

另请参阅

欲知详情,请访问以下网站:

15.8 使用 AMQP 与 Quarkus

问题

您希望使用 AMQP(高级消息队列协议)作为消息系统。

解决方案

使用quarkus-smallrye-reactive-messaging-amqp扩展。

就像 Kafka 集成一样,Quarkus 使用 Eclipse MicroProfile Reactive Messaging 作为所有消息交互的外观。通过向您的项目添加quarkus-smallrye-reactive-messaging-amqp扩展,您将获得 SmallRye AMQP 连接器及其相关依赖项。这将使得@Outbound@Inbound@Broadcast和其他 Eclipse MicroProfile Reactive Messaging 注解和概念可以与 AMQP 一起工作。

警告

这些注解适用于 AMQP 1.0,不适用于 0.9.x。

你还需要在application.properties文件中将通道连接器设置为smallrye-amqp。请记住,这些配置的语法如下:

mp.messaging.[outgoing|incoming].[channel-name].property=value

您还可以通过以下方式全局设置 AMQP 连接的用户名和密码:

amqp-username=[my-username]
amqp-password=[my-secret-password]

或者,如果您需要与具有自己凭据的不同实例通信,可以在每个通道上设置这些凭据。详细属性请参阅 SmallRye 文档。

从 Recipe 15.5 中的代码,无论是用 AMQP 还是 Kafka,只要通道名称相同且 AMQP 设置和连接信息正确,都能正常工作。

另请参阅

欲知详情,请访问以下网站:

15.9 使用 MQTT

问题

您希望使用 MQTT(MQ Telemetry Transport)作为消息系统。

解决方案

使用quarkus-smallrye-reactive-messaging-mqtt扩展。

就像 Kafka 和 AMQP 集成一样,Quarkus 使用 Eclipse MicroProfile Reactive Messaging 作为所有消息交互的外观。通过向您的项目添加quarkus-smallrye-reactive-messaging-mqtt扩展,您将获得 SmallRye MQTT 连接器及其相关依赖项。这将使得@Outbound@Inbound@Broadcast和其他 Eclipse MicroProfile Reactive Messaging 注解和概念可以与 MQTT 一起工作。

你还需要在application.properties文件中将通道连接器设置为smallrye-mqtt。请记住,这些配置的语法如下:

mp.messaging.[outgoing|incoming].[channel-name].property=value

连接和凭据可以按通道设置。详细属性请参阅 SmallRye 文档。

配方 15.4 的代码与 MQTT 一样适用于 Kafka,假设通道名称相同,并且其余的 MQTT 设置及连接信息正确。

讨论

还支持作为 MQTT 服务器使用;但是,它不是一个完整功能的 MQTT 服务器。例如,它只处理发布请求及其确认;不处理订阅请求。

另请参阅

欲了解更多信息,请访问以下网站:

15.10 使用响应式 SQL 进行查询

问题

您希望使用 PostgreSQL 响应式客户端查询数据。

解决方案

Quarkus 集成了 Vert.x 响应式 SQL 客户端,可与 MySQL/MariaDB 和 PostgreSQL 配合使用。在这个示例中,我们将演示与 PostgreSQL 的集成;在下一个示例中,我们将使用 MariaDB。

自然地,您需要添加扩展以利用响应式 SQL 客户端。目前有两个扩展:quarkus-reactive-pg-clientquarkus-reactive-mysql-client,分别适用于这两个数据库。如果您正在使用 JAX-RS,则还需要确保项目中包含以下扩展:

  • quarkus-resteasy

  • quarkus-resteasy-jsonbquarkus-resteasy-jackson

  • quarkus-resteasy-mutiny

就像使用任何数据存储一样,您需要配置访问:

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.reactive.url=postgresql://localhost:5432/quarkus_test

现在您可以使用客户端:

package org.acme.pg;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.pgclient.PgPool;
import io.vertx.mutiny.sqlclient.Row;
import io.vertx.mutiny.sqlclient.Tuple;

public class Book {
    public Long id;
    public String title;
    public String isbn;

    public Book() {
    }

    public Book(String title, String isbn) {
        this.title = title;
        this.isbn = isbn;
    }

    public Book(Long id, String title, String isbn) {
        this.id = id;
        this.title = title;
        this.isbn = isbn;
    }

    public static Book from(Row row) {
        return new Book(row.getLong("id"),
                        row.getString("title"),
                        row.getString("isbn"));
    }

    public static Multi<Book> findAll(PgPool client) {
        return client.query("SELECT id, title, isbn " +
                            "FROM books ORDER BY title ASC") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
                .onItem().produceMulti(Multi.createFrom()::iterable) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
                .map(Book::from); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    }
}

1

查询数据库,返回Uni<RowSet<Row>>

2

一旦查询返回,创建Multi<Row>

3

将每一行映射为Book实例

要完成练习,您可以使用 RESTful 端点:

package org.acme.pg;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.pgclient.PgPool;
import org.eclipse.microprofile.config.inject.ConfigProperty;

@Path("/books")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookResource {

    @Inject
    PgPool client;
    @GET
    public Uni<Response> get() {
        return Book.findAll(client)
                .collectItems().asList()
                .map(Response::ok)
                .map(Response.ResponseBuilder::build);
    }
}

讨论

您还可以通过使用preparedQuery方法和Tuple类来使用预准备查询:

    public static Uni<Boolean> delete(PgPool client, Long id) {
        return client.preparedQuery("DELETE FROM books " +
                                    "WHERE id = $1", Tuple.of(id))
                .map(rowSet -> rowSet.rowCount() == 1); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    }

1

使用元数据从RowSet实例返回以验证已删除的行

另请参阅

底层实现可以在Vert.x: 响应式 PostgreSQL 客户端找到。

15.11 使用响应式 SQL 客户端进行插入

问题

您希望使用 MySQL 响应式客户端插入数据。

解决方案

与使用 PostgreSQL 的上一个示例类似,可以使用响应式 MySQL 客户端进行数据插入。需要使用相同的扩展来将quarkus-reactive-pg-client更改为quarkus-reactive-mysql-client

  • quarkus-resteasy

  • quarkus-resteasy-jsonbquarkus-resteasy-jackson

  • quarkus-resteasy-mutiny

当然,您需要设置数据源:

quarkus.datasource.db-kind=mysql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.reactive.url=mysql://localhost:3306/quarkus_test

您将在Book.save方法中看到与上一个示例中相似的主题:

    public Uni<Long> save(MySQLPool client) {
        String query = "INSERT INTO books (title,isbn) VALUES (?,?)";
        return client.preparedQuery(query, Tuple.of(title, isbn))
                .map(rowSet -> rowSet
                        .property(MySQLClient.LAST_INSERTED_ID)); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    }

1

使用RowSet的属性获取插入的 ID。

到目前为止,您应该能够为BookResource端点编写适当的 POST 方法,调用从用户接收到的新Book实例的save方法。

参见

欲了解更多信息,请访问以下网站:

15.12 使用反应式 MongoDB 客户端

问题

您想要使用反应式 MongoDB 客户端。

解决方案

MongoDB Quarkus 扩展还包括反应式 MongoDB 客户端。如配方 7.21 所示,您需要添加quarkus-mongodb-client。您还需要向项目添加以下扩展:

quarkus-resteasy-mutiny

为了返回和与 Mutiny 交互以进行端点返回。

quarkus-smallrye-context-propagation

这允许像注入和事务这样的事情与异步代码一起工作。

集成的其余部分非常简单。这里是从先前的 MongoDB 配方中的服务和资源类的版本,但以反应式方式编写:

package org.acme.mongodb;

import java.util.List;
import java.util.Objects;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import com.mongodb.client.model.Filters;
import io.quarkus.mongodb.reactive.ReactiveMongoClient;
import io.quarkus.mongodb.reactive.ReactiveMongoCollection;
import io.smallrye.mutiny.Uni;
import org.bson.Document;

@ApplicationScoped
public class ReactiveBookService {
    @Inject
    ReactiveMongoClient mongoClient;

    public Uni<List<Book>> list() {
        return getCollection().find()
                .map(Book::from).collectItems().asList();
    }

    public Uni<Void> add(Book b) {
        Document doc = new Document()
                .append("isbn", b.isbn)
                .append("title", b.title)
                .append("authors", b.authors);

        return getCollection().insertOne(doc);
    }

    public Uni<Book> findSingle(String isbn) {
        return Objects.requireNonNull(getCollection()
                .find(Filters.eq("isbn", isbn))
                .map(Book::from))
                .toUni();
    }

    private ReactiveMongoCollection<Document> getCollection() {
        return mongoClient.getDatabase("book")
                .getCollection("book");
    }
}

除了导入和从命令式转变为反应式使用 Mutiny,没有什么改变。同样适用于 REST 终点:

package org.acme.mongodb;

import java.util.List;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import io.smallrye.mutiny.Uni;

@Path("/reactive_books")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ReactiveBookResource {
    @Inject
    ReactiveBookService service;

    @GET
    public Uni<List<Book>> getAll() {
        return service.list();
    }

    @GET
    @Path("{isbn}")
    public Uni<Book> getSingle(@PathParam("isbn") String isbn) {
        return service.findSingle(isbn);
    }

    @POST
    public Uni<Response> add(Book b) {
        return service.add(b).onItem().ignore()
                .andSwitchTo(this::getAll)
                .map(books -> Response.status(Response.Status.CREATED)
                                      .entity(books).build());
    }
}

参见

欲了解更多信息,请访问以下网站:

15.13 使用反应式 Neo4j 客户端

问题

您想要使用反应式 Neo4j 客户端。

解决方案

Neo4j Quarkus 扩展支持反应式驱动程序。

您需要使用 4 版或更高版本的 Neo4j 完全反应式。您还需要向项目添加quarkus-resteasy-mutiny扩展。从配方 7.23 继续,除了使用驱动程序的RxSession和使用 Mutiny 外,没有太多改变:

package org.acme.neo4j;

import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Record;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.reactive.RxResult;
import org.reactivestreams.Publisher;

@Path("/reactivebooks")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ReactiveBookResource {
    @Inject
    Driver driver;

    @GET
    @Produces(MediaType.SERVER_SENT_EVENTS) ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    public Publisher<Response> getAll() {
        return Multi.createFrom().resource( ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
                driver::rxSession,
                rxSession -> rxSession.readTransaction(tx -> { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
                    RxResult result = tx.run("MATCH (b:Book) RETURN " +
                                             "b ORDER BY b.title");
                    return Multi.createFrom().publisher(result.records())
                            .map(Record::values)
                            .map(values -> values.stream().map(Value::asNode)
                                                          .map(Book::from)
                                                          .map(Book::toJson))
                            .map(bookStream ->
                                    Response.ok(bookStream
                                            .collect(Collectors.toList()))
                                    .build());
                }))
                .withFinalizer(rxSession -> { ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
                    return Uni.createFrom().publisher(rxSession.close());
                });
    }

    @POST
    public Publisher<Response> create(Book b) {
        return Multi.createFrom().resource(
                driver::rxSession,
                rxSession -> rxSession.writeTransaction(tx -> {
                    String query = "CREATE " +
                                   "(b:Book {title: $title, isbn: $isbn," +
                                   " authors: $authors}) " +
                                   "RETURN b";
                    RxResult result = tx.run(query,
                            Values.parameters("title", b.title,
                                    "isbn", b.isbn, "authors", b.authors));
                    return Multi.createFrom().publisher(result.records())
                            .map(record -> Response.ok(record
                                    .asMap()).build());
                })
        ).withFinalizer(rxSession -> {
            return Uni.createFrom().publisher(rxSession.close());
        });
    }
}

1

从驱动程序获取RxSession

2

使用 Mutiny 与 ReactiveStreams Publisher 进行交互

3

将结果流式传输回用户

4

在最后关闭会话

第十六章:附加的 Quarkus 特性

本章包含 Quarkus 的一些特性,这些特性不适合其他章节。当然,这并不会使它们变得不那么有用!在本章中,您将了解以下主题:

  • Quarkus 的模板解决方案,Qute

  • OpenAPI 集成

  • 发送电子邮件

  • 调度功能

  • 应用数据缓存

16.1 使用 Qute 模板引擎创建模板

问题

您希望创建模板并使用特定数据渲染它们。

解决方案

使用 Qute 模板引擎。

Qute 是一个专门设计以满足 Quarkus 的需求,最小化反射使用并支持命令式和响应式编码风格的模板引擎。

Qute 可以作为一个独立库使用(生成报告到磁盘或生成电子邮件正文消息),也可以与 JAX-RS 结合以提供 HTML 内容。

要开始使用 JAX-RS 的 Qute,请添加 resteasy-qute 扩展:

./mvnw quarkus:add-extension -Dextensions="quarkus-resteasy-qute"

默认情况下,模板存储在 src/main/resources/templates 目录及其子目录中。

下面可能是一个简单的文本文件模板:

Hello {name}!

模板是一个简单的句子,使用 name 参数进行参数化。

要使用具体数据渲染模板,只需注入 io.quarkus.qute.Template 实例并提供模板参数:

@Inject
io.quarkus.qute.Template hello; ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

@GET
@Produces(MediaType.TEXT_PLAIN)
public TemplateInstance hello() { ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
    final String name = "Alex";
    return hello.data("name", name); ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
}

1

Template 实例定义了模板中的操作。

2

默认情况下,使用字段名来定位模板;在这种情况下,模板路径为 src/main/resources/templates/hello.txt

3

无需渲染,因为 RESTEasy 集成了 TemplateInstance 对象以渲染内容。

4

data 方法用于设置模板参数。

如果运行项目,您将能够看到模板如何渲染:

./mvnw compile quarkus:dev

curl http://localhost:8080/hello

Hello Alex!

讨论

Qute 支持更多语法(如 includeinsert 片段、直接注入 CDI bean 或变体模板)以及与其他 Quarkus 部分(如电子邮件或计划任务)的集成。

参见

访问以下网站了解更多关于 Qute 的信息:

16.2 使用 Qute 渲染 HTML

问题

使用 Qute 渲染 HTML。

解决方案

Qute 将 HTML 渲染得如此简单,就像文本一样。唯一需要发生的是 Quarkus 找到与您的注入匹配的模板。模板的实际内容并不重要。

让我们使用模板渲染一个包含更复杂结构的 HTML 页面。在这种情况下,将渲染一个简单的 HTML 报告。创建一个包含报告参数的 POJO 类:

package org.acme.quickstart;

import java.util.ArrayList;
import java.util.List;

import io.quarkus.qute.TemplateData;

@TemplateData ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public class Movie {

    public String name;
    public int year;
    public String genre;
    public String director;
    public List<String> characters = new ArrayList<>();
    public float ratings;

    public int getStars() { ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        return Math.round(ratings);
    }
}

1

此注释允许 Quarkus 避免在运行时使用反射访问对象

2

自定义方法

讨论

下面是值得解释的 HTML 模板的一些详细信息。

首先要查看的部分是一个可选的头部,你可以将其放在任何模板中,以帮助 Quarkus 在编译时验证所有表达式:

{@org.acme.quickstart.Movie movie} ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
<!DOCTYPE html>
<html>

1

参数声明;这不是强制性的,但有助于 Quarkus 验证模板的类型安全性

支持基本的语法,如条件语句或循环:

<div class="col-sm-12">
    <dl> {#if movie.year == 0} ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
            <dt>Year:</dt> Not Known
        {#else} ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
            <dt>Year:</dt> {movie.year}
        {/if}
        {#if movie.genre is 'horror'} ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)
        <dt>Genre:</dt> Buuuh
        {#else} <dt>Genre:</dt> {movie.genre}
        {/if} <dt>Director:</dt> {movie.director ?: 'Unknown'} ![4](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/4.png)
        <dt>Main Characters:</dt> {#for character in movie.characters} ![5](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/5.png) {character} ![6](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/6.png) {#if hasNext} ![7](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/7.png) -
            {/if}
        {/for} <dt>Rating:</dt>
        <font color="red"> {#for i in movie.stars} ![8](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/8.png)
            <span class="fas fa-xs fa-star"></span> {/for} </font>
    </dl>
</div>

1

数字类型的条件语句

2

否则部分

3

使用 is 运算符的字符串类型的条件语句

4

Elvis 操作符;如果参数是 null,则使用默认值

5

遍历所有字符

6

显示字符信息

7

hasNext 是一个特殊属性,用于检查是否还有更多元素。

8

在 POJO 中定义的方法调用;按照调用中定义的次数进行迭代

提示

在循环内部,可以使用以下隐式变量:hasNextcountindexoddeven

警告

目前只能使用 IterableMap.EntrySetIntegerStream

16.3 更改 Qute 模板的位置

问题

你想要改变 Qute 查找模板的位置。

解决方案

你可以通过使用 io.quarkus.qute.api.ResourcePath 注解,自定义模板位置(仍然在 src/main/resources/templates 中,并将输出到应用程序部署目录中的 templates 目录):

@ResourcePath("movies/detail.html") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
Template movies;

1

设置模板路径为 src/main/resources/templates/movies/detail.html

再次运行应用程序(或者如果已经在运行,请让实时重载完成其工作),然后打开浏览器并输入此 URL:http://localhost:8080/movie

16.4 扩展 Qute 数据类

问题

你想要扩展 Qute 数据类的功能。

解决方案

模板扩展方法必须遵循以下规则:

  • 必须是静态的。

  • 方法不能返回 void。

  • 必须包含至少一个参数。第一个参数用于匹配基本数据对象。

提示

你可以使用 模板扩展 来添加专门用于报告目的的方法,当你无法访问数据对象源代码时。

通过使用 @io.quarkus.qute.TemplateExtension 注解,你可以实现 模板扩展方法。在这种情况下,让我们实现一个方法来对 rating 数字进行四舍五入:

@TemplateExtension
static double roundStars(Movie movie, int decimals) { ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png) ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
    double scale = Math.pow(10, decimals);
    return Math.round(movie.ratings * scale) / scale;
}

1

第一个参数是 POJO 数据对象。

2

可以设置自定义参数

从模板引擎,movie 有一个 roundStars 方法,带有一个参数,该参数是要舍入的小数位数。

现在在模板中可以调用以下内容:

({movie.roundStars(2)}) ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

1

Movie 类没有定义 roundStars 方法,但因为它是模板扩展,所以可以访问它

再次运行应用程序(或者如果已经在运行,请让实时重载完成其工作),然后打开浏览器,输入以下 URL:http://localhost:8080/movie

输出应与图 16-1 中显示的输出类似。

qucb 1601

图 16-1. HTML 输出

16.5 描述带有 OpenAPI 的端点

问题

您希望使用 OpenAPI 描述您的 REST API。

解决方案

使用 SmallRye OpenAPI 扩展。

一旦您使用 Quarkus 创建了一个 RESTful API,您只需要添加 openapi 扩展:

./mvnw quarkus:add-extension -Dextensions="openapi"

然后重新启动应用程序,使所有内容生效:

./mvnw compile quarkus:dev

默认情况下,API 的规范位于 /openapi。要更改此设置,请使用 quarkus.smallrye-openapi.path 配置:

quarkus.smallrye-openapi.path=/rest-api

您可以通过http://localhost:8080/openapi访问规范:

openapi: 3.0.1
info:
  title: Generated API
  version: "1.0"
paths:
  /task:
    get:
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetTask'
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Task'
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetTask'
    delete:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Task'
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetTask'
components:
  schemas:
    Task:
      type: object
      properties:
        complete:
          type: boolean
        description:
          type: string
        reminder:
          format: date-time
          type: string
    SetTask:
      type: array
      items:
        type: object
        properties:
          complete:
            type: boolean
          description:
            type: string
          reminder:
            format: date-time
            type: string

基于之前的规范,有 GET、POST 和 DELETE 端点。您还可以看到 DELETE 和 POST 需要一个任务对象。任务需要一个布尔值、一个字符串和一个日期时间。这非常简单易懂。

讨论

使用 Quarkus 中的 SmallRye OpenAPI 扩展非常容易创建 OpenAPI 规范。这使您能够轻松查看和理解您的 RESTful API。

SmallRye OpenAPI 是 Eclipse MicroProfile OpenAPI 的实现。OpenAPI 规范是一种标准的、与语言无关的描述和发现 RESTful API 的方式。它既可由人类阅读,也可由机器处理。OpenAPI 文档定义为 JSON 或 YAML。

参见

欲了解更多信息,请访问 GitHub 上的以下页面:

在示例 16.6 中,您将学习如何使用 SmallRye OpenAPI 的注解来自定义生成的规范。

16.6 自定义 OpenAPI 规范

问题

您希望自定义生成的 API 规范。

解决方案

使用 SmallRye OpenAPI 扩展的 OpenAPI 注解。

重用上一个示例中创建的任务 API,示例 16.5,可以轻松使用 OpenAPI 注解来添加自定义和进一步的 API 文档:

package org.acme.openapi;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Set;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;

  @Path("/task")
  @Produces(MediaType.APPLICATION_JSON)
  @Consumes(MediaType.APPLICATION_JSON)
  public class TaskResource {

    Set<Task> tasks = Collections.newSetFromMap(
        Collections.synchronizedMap(new LinkedHashMap<>()));

    public TaskResource() {
      tasks.add(new Task("First task",
            LocalDateTime.now().plusDays(3), false));
      tasks.add(new Task("Second task",
            LocalDateTime.now().plusDays(6), false));
    }

    @GET
    @Operation(summary = "Get all tasks",
               description = "Get the full list of tasks.")
    public Set<Task> list() {
      return tasks;
    }

    @POST
    @Operation(summary = "Create a new task")
    public Set<Task> add(
        @Parameter(required = true, content =
          @Content(schema = @Schema(implementation = Task.class))) Task task) {
      tasks.add(task);
      return tasks;
    }

    @DELETE
    @Operation(summary = "Remove the specified task")
    public Set<Task> delete(
        @Parameter(required = true,
        content = @Content(schema = @Schema(implementation = Task.class)))
        Task task) {
      tasks.removeIf(existingTask -> existingTask.equals(task));
      return tasks;
    }
  }
package org.acme.openapi;

import java.time.LocalDateTime;
import java.util.Objects;

import javax.json.bind.annotation.JsonbDateFormat;

import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

public class Task {
    public String description;

    @Schema(description = "Flag indicating the task is complete")
    public Boolean complete;

    @JsonbDateFormat("yyyy-MM-dd'T'HH:mm")
    @Schema(example = "2019-12-25T06:30", type = SchemaType.STRING,
            implementation = LocalDateTime.class,
            pattern = "yyyy-MM-dd'T'HH:mm",
            description = "Date and time for the reminder.")
    public LocalDateTime reminder;

    public Task() {
    }

    public Task(String description,
                LocalDateTime reminder,
                Boolean complete) {
        this.description = description;
        this.reminder = reminder;
        this.complete = complete;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Task task = (Task) o;
        return Objects.equals(description, task.description) &&
                Objects.equals(reminder, task.reminder) &&
                Objects.equals(complete, task.complete);
    }

    @Override
    public int hashCode() {
        return Objects.hash(description, reminder, complete);
    }
}

上述代码将创建以下规范:

---
openapi: 3.0.1
info:
  title: Generated API
  version: "1.0"
paths:
  /task:
    get:
      summary: Get all tasks
      description: Get the full list of tasks.
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetTask'
    post:
      summary: Create a new task
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Task'
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetTask'
    delete:
      summary: Remove the specified task
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Task'
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetTask'
components:
  schemas:
    Task:
      type: object
      properties:
        complete:
          description: Flag indicating the task is complete
          type: boolean
        description:
          type: string
        reminder:
          format: date-time
          description: Date and time for the reminder.
          pattern: yyyy-MM-dd'T'HH:mm
          type: string
          example: 2019-12-25T06:30
    SetTask:
      type: array
      items:
        type: object
        properties:
          complete:
            description: Flag indicating the task is complete
            type: boolean
          description:
            type: string
          reminder:
            format: date-time
            description: Date and time for the reminder.
            pattern: yyyy-MM-dd'T'HH:mm
            type: string
            example: 2019-12-25T06:30

基于之前的规范,有 GET、POST 和 DELETE 端点。您还可以看到 DELETE 和 POST 需要一个任务对象。任务需要一个布尔值、一个字符串和一个日期时间。这非常简单易懂。

讨论

使用各种 OpenAPI 注解提供有关 API 的额外信息,包括描述、摘要和示例。关于这些注解的更多信息可以在规范中和“参见”部分的链接中找到。

使用 Quarkus 进行生成的 OpenAPI 规范进一步定制非常容易。对于最终定制,Quarkus 支持提供静态文件 OpenAPI 规范。为此,您需要将有效的 OpenAPI 规范文件放置在 META-INF/openapi.ymlMETA-INF/openapi.json。然后 Quarkus 将结合这两个文件并提供一个结合了静态和动态规范的服务。要禁用动态规范生成,只需在 applications.properties 文件中使用 mp.openapi.scan.disable=true 配置。

参见

获取更多信息,请访问 GitHub 上的以下页面:

16.7 发送同步邮件

问题

您希望同步发送邮件。

解决方案

利用 Quarkus 邮件扩展。

Quarkus 使得以纯文本和 HTML 发送电子邮件以及添加附件变得非常直观。还有一个简单易用的方法来测试是否正确发送了邮件,而无需设置自己的中继。将 Email Quarkus 扩展添加到现有项目中:

mvn quarkus:add-extensions -Dextensions="mailer"

Quarkus 使用 Vert.x Mail 客户端,虽然有两个包装器以便于使用:

@Inject
Mailer mailer;

@Inject
ReactiveMailer reactiveMailer;

Mailer 类使用标准的阻塞和同步 API 调用,而 ReactiveMailer 则使用非阻塞和异步 API 调用,如预期那样。ReactiveMailer 将在下一个示例中讨论;这两个类都提供相同的功能。要发送电子邮件,只需使用 withTextwithHtml 方法。您需要提供收件人、主题和正文。如果需要添加 CC、BCC 和附件等内容,可以在实际的 Mail 实例上进行操作。

您还需要配置 SMTP 提供者(在本例中,我们使用 Gmail TLS):

quarkus.mailer.from=quarkus-test@gmail.com
quarkus.mailer.host=smtp.gmail.com
quarkus.mailer.port=587
quarkus.mailer.start-tls=REQUIRED

![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.mailer.username=YOUREMAIL@gmail.com
quarkus.mailer.password=YOURGENERATEDAPPLICATIONPASSWORD

1

这些也可以通过系统属性和/或环境属性进行设置

通过使用 MockMailbox 组件可以轻松地测试邮件组件。它是一个简单的组件,包括三个方法:

  • getMessagesSentTo

  • clear

  • getTotalMessagesSent

以下测试演示了这三种方法的全部内容:

package org.acme.email;

import java.util.List;

import javax.inject.Inject;

import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;
import io.quarkus.mailer.MockMailbox;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

@QuarkusTest
public class MailerTest {
    @Inject
    Mailer mailer;

    @Inject
    MockMailbox mbox;

    @BeforeEach
    void clearMBox() {
        mbox.clear(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    }

    @Test
    public void assertBasicTextEmailSent() {
        final String mailTo = "test@example.org";
        final String testingSubject = "Testing email";
        final String testingBody = "Hello World!";

        mailer.send(Mail.withText(mailTo,
                testingSubject,
                testingBody));

        assertThat(mbox.getTotalMessagesSent()).isEqualTo(1); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)
        List<Mail> emails = mbox.getMessagesSentTo(mailTo); ![3](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/3.png)

        assertThat(emails).hasSize(1);
        Mail email = emails.get(0);

        assertThat(email.getSubject()).isEqualTo(testingSubject);
        assertThat(email.getText()).isEqualTo(testingBody);
    }
}

1

我们在每次测试开始之前清空邮箱

2

使用 getTotalMessagesSent 来验证 Quarkus 发送了多少封消息

3

验证发送到特定地址的消息

讨论

支持常规附件和内联附件。以下是内联附件的简单示例:

    @Test
    void attachmentTest() throws Exception {
        final String mailTo = "test@example.org";
        final String testingSubject = "email with Attachment";
        final String html = "<strong>E-mail by:</strong>" + "\n" +
                "<p><img src=\"cid:logo@quarkus.io\"/></p>";    ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)

        sendEmail(mailTo, testingSubject, html);

        Mail email = mbox.getMessagesSentTo(mailTo).get(0);
        List<Attachment> attachments = email.getAttachments();

        assertThat(email.getHtml()).isEqualTo(html);
        assertThat(attachments).hasSize(1);
        assertThat(attachments.get(0).getFile())
                .isEqualTo(new File(getAttachmentURI()));
    }

    private void sendEmail(String to, String subject, String body)
          throws URISyntaxException {
        final File logo = new File(getAttachmentURI());

        Mail email = Mail.withHtml(to, subject, body)
                .addInlineAttachment("quarkus-logo.svg",
                        logo,
                        "image/svg+xml",
                        "<logo@quarkus.io>");   ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

        mailer.send(email);
    }

1

请确保通过content-id引用内联附件。

2

附件的content-id

参见

欲了解更多信息,请参阅以下内容:

16.8 响应式发送电子邮件

问题

您希望以非阻塞、响应式的方式发送电子邮件。

解决方案

利用 Quarkus 邮件扩展。

前一节详细介绍了基础知识。要以响应式方式执行此操作,只需注入ReactiveMailer组件并使用它即可。方法是相同的;它们只是返回响应式对应项而不是同步对应项:

package org.acme.email;

import java.util.List;
import java.util.concurrent.CountDownLatch;

import javax.inject.Inject;

import io.quarkus.mailer.Mail;
import io.quarkus.mailer.MockMailbox;
import io.quarkus.mailer.reactive.ReactiveMailer;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

@QuarkusTest
public class ReactiveMailerTest {
    @Inject
    ReactiveMailer reactiveMailer;

    @Inject
    MockMailbox mbox;

    @BeforeEach
    void clearMbox() {
        mbox.clear();
    }

    @Test
    public void testReactiveEmail() throws Exception {
        final String mailTo = "test@example.org";
        final String testingSubject = "Testing email";
        final String testingBody = "Hello World!";
        final CountDownLatch latch = new CountDownLatch(1);

        reactiveMailer.send(Mail.withText(mailTo,
                testingSubject,
                testingBody)).subscribeAsCompletionStage().join();

        assertThat(mbox.getTotalMessagesSent()).isEqualTo(1);
        List<Mail> emails = mbox.getMessagesSentTo(mailTo);

        assertThat(emails).hasSize(1);
        Mail email = emails.get(0);

        assertThat(email.getSubject()).isEqualTo(testingSubject);
        assertThat(email.getText()).isEqualTo(testingBody);
    }
}

这个测试与上一节中的测试完全相同;唯一的区别是将CompletionStage转换为CompletableFuture,并调用join以返回测试的命令式样式。

讨论

Qute 与 Mailer 扩展集成,因此消息的主体内容是从模板中呈现的。

这次您只需要qute扩展,因为不需要 RESTEasy 集成:

mvn quarkus:add-extensions -Dextensions="quarkus-qute"

主类是io.quarkus.mailer.MailTemplate,使用方式与io.quarkus.qute.Template相同,但前者包含特定于邮件逻辑的方法:

@ResourcePath("mail/welcome.txt") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
MailTemplate mailTemplate;

CompletionStage<Void> c = hello.to("to@acme.org")
     .subject("Hello from Qute template")
     .data("name", "Alex")
     .send(); ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

模板位于src/main/resources/templates/mail/welcome.txt

2

使用提供的数据从模板文件渲染正文发送电子邮件

以响应式方式发送电子邮件遵循完全相同的方法名称和用法,只是使用了响应式类。这使得切换和理解非常容易。

参见

欲了解更多信息,请参阅以下内容:

  • 16.7 配方

  • 16.1 配方

16.9 创建定时任务

问题

您希望某些任务按计划运行。

解决方案

在 Quarkus 中安排任务快速简单,但提供了高度的控制和定制。Quarkus 有一个与 Quartz 集成的scheduler扩展。

创建定时任务非常简单:只需将@io.quarkus.scheduler.Scheduled注解添加到应用范围的 bean 中即可。有两个属性可用于指定任务的调度时间:cronevery

cron属性使用 Quartz cron 语法。如果您不熟悉 Quartz,请注意与标准 cron 语法存在一些差异。您可以在“参见”的链接中了解更多信息。

every属性可能是最容易使用的,尽管它有一些微妙之处。every使用Duration#parse解析字符串。如果表达式以数字开头,则会自动添加PT前缀。

everycron都会查找以{开始和以}结束的表达式进行配置。

有一个 delay 属性,它接受一个长整型,还有一个 delayUnit 属性,它接受一个 TimeUnit。这两者一起使用时,将指定一个延迟时间,在此时间后触发器启动。默认情况下,触发器在注册时开始。

这里演示了一个非常简单的用法:

package org.acme.scheduling;

import java.util.concurrent.atomic.AtomicInteger;

import javax.enterprise.context.ApplicationScoped;

import io.quarkus.scheduler.Scheduled;
import io.quarkus.scheduler.ScheduledExecution;

@ApplicationScoped
public class Scheduler {

    private AtomicInteger count = new AtomicInteger();

    int get() {
        return count.get();
    }

    @Scheduled(every = "5s")
    void fiveSeconds(ScheduledExecution execution) {
        count.incrementAndGet();
        System.out.println("Running counter: 'fiveSeconds'. Next fire: "
                + execution.getTrigger().getNextFireTime());
    }
}

讨论

Qute 可用于定期生成报告。

您只需要 qute 扩展,因为不需要 RESTEasy 集成:

mvn quarkus:add-extensions -Dextensions="quarkus-qute"

现在必须手动调用 render() 方法以获取结果:

@ResourcePath("reports/report_01.html")
Template report;

@Scheduled(cron="0 30 * * * ?")
void generate() {
    final String reportContent = report
        .data("sales", listOfSales)
        .data("now", java.time.LocalDateTime.now())
        .render();
    Files.write(reportOuput, reportContent.getBytes());
}

参见

欲了解更多信息,请参阅以下内容:

16.10 使用应用程序数据缓存

问题

您希望在方法响应时间较长时避免等待时间。

解决方案

使用应用程序数据缓存。

有些情况下,方法可能比预期花费更长的时间来响应,可能是因为正在向外部系统发出请求,或者因为执行的逻辑需要较长时间来执行。

改善此情况的一种方法是使用应用程序数据缓存。其思想是保存方法调用的结果,以便对于那些使用相同输入调用的方法,返回先前计算的结果。

Quarkus 与 Caffeine 集成作为缓存提供者。

要开始使用应用程序数据缓存,请添加 cache 扩展:

./mvnw quarkus:add-extension -Dextensions="cache"

这里是一个模拟长时间执行的方法:

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
    long initial = System.currentTimeMillis();
    String msg = greetingProducer.getMessage(); ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
    long end = System.currentTimeMillis();
    return msg + " " + (end - initial) + "ms";
}

1

此逻辑具有随机的休眠时间

如果运行项目,您将能够看到这种延迟:

./mvnw compile quarkus:dev

curl http://localhost:8080/hello
Hello World 4009ms

curl http://localhost:8080/hello
Hello World 3003ms

让我们通过使用 @io.quarkus.cache.CacheResult 注解来缓存 getMessage() 方法调用:

@CacheResult(cacheName = "greeting-cache") ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
public String getMessage() {
    try {
        TimeUnit.SECONDS.sleep(random.nextInt(4) + 1);
        return "Hello World";
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    }

}

1

为此方法调用创建一个新的缓存

再次运行应用程序(或者如果已经在运行,请让实时重载完成其工作),并重复调用 http://localhost:8080/hello

curl http://localhost:8080/hello
Hello World 2004ms

curl http://localhost:8080/hello
Hello World 0ms

第二次调用方法时,不会调用方法,而是从缓存中返回。Quarkus 为每个调用计算一个缓存键,并检查缓存系统中是否有命中。

要计算缓存键,Quarkus 默认使用所有参数值。如果没有参数方法,则从缓存名称派生键。

讨论

@io.quarkus.cache.CacheKey 注解可用于方法参数中,以指定确切用于缓存键计算的参数。例如 public String myMethod(@CacheKey String keyElement1, String notPartOfTheKey)

重要提示

@io.quarkus.cache.CacheKey 注解不能用于返回 void 的方法。

@io.quarkus.cache.CacheInvalidate 注解可用于使缓存中的条目失效。调用带有 @CacheInvalidate 注解的方法时,将计算缓存键并用于从缓存中移除现有条目。

@io.quarkus.cache.CacheInvalidateAll 注解用于使所有缓存条目无效。

每个数据缓存选项都可以在application.properties文件中单独配置:

quarkus.cache.caffeine."greeting-cache".initial-capacity=10 ![1](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/1.png)
quarkus.cache.caffeine."greeting-cache".expire-after-write=5S ![2](https://gitee.com/OpenDocCN/ibooker-java-zh/raw/master/docs/qks-cb/img/2.png)

1

greeting-cache缓存的内部数据结构的最小总大小

2

设置过期时间,在对greeting-cache缓存进行写操作后开始计时

再次运行应用程序(或者如果已经运行,请让实时重新加载完成其工作),并重复调用http://localhost:8080/hello

curl http://localhost:8080/hello
Hello World 2004ms

curl http://localhost:8080/hello
Hello World 0ms

// Wait 5 seconds

curl http://localhost:8080/hello
Hello World 1011ms
提示

quarkus.cache.caffeine."greeting-cache".expire-after-access属性可用于将缓存的过期时间设置为最近一次读取或写入缓存值后的一段时间。

附录 A. Minikube

本书中涉及 Kubernetes 集群的所有示例都在 minikube 中进行了测试;然而,它们也应该在任何其他 Kubernetes 集群中正常工作。

Minikube 是一个工具,使得在本地而非远程 Kubernetes 集群中运行 Kubernetes 变得非常简单。

在本书中,使用了 minikube 1.7.3 和 Kubernetes 1.17.3;但是,由于没有使用高级技术,所以其他版本也应该可以。Minikube 需要安装一个虚拟化程序。我们建议您使用 VirtualBox 虚拟化程序。根据我们的经验,这是运行 minikube 最便携和稳定的方式。

安装 minikube、VirtualBox 和 kubectl 的方式可能取决于您正在运行的系统,因此我们提供了安装每个组件的说明链接:

安装完所有软件后,您可以通过打开一个终端窗口并运行以下命令来启动 minikube:

minikube start --vm-driver=virtualbox --memory='8192mb' \
 --kubernetes-version='v1.17.3'

ߙ䠠[serverless] minikube v1.7.3 on Darwin 10.15.3
✨  Using the virtualbox driver based on user configuration
⌛  Reconfiguring existing host ...
ߔ䠠Starting existing virtualbox VM for "default" ...
ߐ㠠Preparing Kubernetes v1.17.3 on Docker 19.03.6 ...
ߚࠠLaunching Kubernetes ...
ߌEnabling addons: dashboard, default-storageclass, storage-provisioner
ߏ䠠Done! kubectl is now configured to use "default"

然后,配置 docker CLI 使用 minikube 的 docker 主机:

eval $(minikube docker-env)

然后,任何使用 docker 执行的操作,如 docker builddocker run,都将在 minikube 集群内进行。

附录 B. Keycloak

Keycloak 是一个开源的身份和访问管理系统。配置和部署 Keycloak 到生产环境超出了本书的范围。在下面的示例中,提供了一个包含所有用户、角色、配置等的领域文件,并需要导入到正在运行的 Keycloak 服务器中。

为了简化 Keycloak 的安装,使用了 Keycloak Docker 容器:

docker run --name keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin \
 -p 8180:8080 jboss/keycloak:8.0.1

然后打开浏览器并输入以下 URL:http://localhost:8180

单击管理控制台,如图 B-1 所示。

qucb ab01

图 B-1. Keycloak 首页

接下来,您将看到一个与图 B-2 类似的登录选项。使用登录/密码 admin 作为凭据。

qucb ab02

图 B-2. Keycloak 登录页面

在主页面中,切换主按钮以显示“添加领域”按钮,并单击它,使您的屏幕看起来像图 B-3 所示。

qucb ab03

图 B-3. Keycloak 添加领域按钮

最后,您应该看到一个类似于图 B-4 所示的屏幕。您需要导入一个 Keycloak 文件。我们使用的文件在本书的代码中,位于https://oreil.ly/quarkus-cookbook-code

qucb ab04

图 B-4. 导入领域的结果

附录 C. Knative

在 第十章 中,你需要访问一个 Kubernetes 集群 —— 它可以是 minikube 安装或其他任何类型。但是,你还需要安装 Knative Serving 来运行 Knative 示例。对于本书而言,Kourier 用作 Knative 的 Ingress。

本书中使用的版本为 minikube 1.7.3、Kubernetes 1.17.3、Knative 0.13.0 和 Kourier 0.3.12。

要安装 Knative Serving,需要运行以下命令:

kubectl apply -f \
 https://github.com/knative/serving/releases/download/v0.13.0/serving-core.yaml
kubectl apply -f \
 https://raw.githubusercontent.com/3scale/kourier/v0.3.12/deploy/\
 kourier-knative.yaml

配置 Knative Serving 使用正确的 ingress.class

kubectl patch configmap/config-network \
 -n knative-serving \
 --type merge \
 -p '{"data":{"clusteringress.class":"kourier.ingress.networking.knative.dev",
 "ingress.class":"kourier.ingress.networking.knative.dev"}}'

设置你所需的域名;在本例中,使用 127.0.0.1 是因为它在 minikube 中运行:

kubectl patch configmap/config-domain \
 -n knative-serving \
 --type merge \
 -p '{"data":{"127.0.0.1.nip.io":""}}'

现在,你已经准备好开始部署 Knative 服务。

posted @ 2024-06-15 12:23  绝不原创的飞龙  阅读(21)  评论(0编辑  收藏  举报