MicroProfile-Java-云原生开发实践指南-全-
MicroProfile Java 云原生开发实践指南(全)
原文:
zh.annas-archive.org/md5/9e48bdda7bab2cd523fc774b93882e04译者:飞龙
前言
MicroProfile 提供了一套 API,以促进使用最佳实践构建云原生应用。一旦您学会了 MicroProfile,您就可以开发一个标准化的云原生应用,它可以部署到许多运行时,无需担心迁移问题。
本书提供了一种实用的方法来实现和相关的方法论,让您能够迅速上手并变得高效。它附带了一个端到端的应用程序,演示了如何使用 MicroProfile 4.1 构建云原生应用并将其部署到云中。
在本书结束时,您将能够理解 MicroProfile,并自信地使用 API 进行云原生应用开发。
本书面向的对象
这本书是为那些希望使用在云中表现良好的开放标准框架构建高效应用的 Java 应用开发者和架构师而写的。希望了解云原生应用工作原理的 DevOps 工程师也会发现这本书很有用。为了充分利用这本书,需要具备 Java、Docker、Kubernetes 和云的基本理解。
本书涵盖的内容
第一章, 云原生应用,定义了云原生应用的概念,并简要讨论了最佳实践。
第二章, MicroProfile 如何融入云原生应用开发?,概述了 MicroProfile,然后从满足云原生应用需求的角度描述了 MicroProfile 的规范。
第三章, 介绍 IBM 股票交易云原生应用,展示了具有高级架构的股票交易应用,并描述了其功能和设计。
第四章, 开发云原生应用,解释了 JAX-RS、JSON-B、JSON-P、CDI 和 MicroProfile Rest Client 的细节,然后通过一些代码示例演示如何使用它们来构建云原生应用。
第五章, 增强云原生应用,解释了如何使用 MicroProfile Config 配置您的云原生应用,确保应用在各种条件下都能正常运行,使用 MicroProfile 容错机制。最后,您将了解如何使用 MicroProfile JWT 来保护应用。
第六章, 观察和监控云原生应用,从如何观察云原生应用的健康状态和操作状态,以及如何使用 MicroProfile Open Tracing 识别故障的角度,讨论了第 2 天的操作。
第七章,使用 Open Liberty、Docker 和 Kubernetes 的 MicroProfile 生态系统,解释了如何将云原生应用程序部署到云中以及它与 Docker、Kubernetes 和 Istio 等云基础设施的交互。
第八章,构建和测试您的云原生应用程序,涵盖了如何从头开始构建现实世界的云原生应用程序 Stock Trader,并逐步利用 MicroProfile 规范来满足云原生应用程序的最佳实践。
第九章,部署和第二天操作,讨论了如何通过 Operator 部署 Stock Trader 应用程序,并讨论了第二天操作,例如部署后的维护。
第十章,响应式云原生应用程序,解释了命令式和响应式应用程序之间的区别,并演示了如何使用响应式消息构建响应式应用程序。
第十一章,MicroProfile GraphQL,扩展了为什么需要使用 MicroProfile GraphQL 进行查询,并随后演示了如何使用 GraphQL 构建查询。
第十二章,MicroProfile LRA 和 MicroProfile 的未来,解释了云原生应用程序事务是什么,并演示了如何使用 MicroProfile LRA 执行云原生事务,随后是 MicroProfile 的未来路线图。
为了充分利用本书
您需要在您的计算机上安装 Java 8 或 11。为了运行示例代码,您需要安装 Maven 的最新版本。

如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包,可在 github.com/PacktPublishing/ 获取。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781801078801_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
文本中的代码: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的 WebStorm-10*.dmg 磁盘镜像文件挂载为系统中的另一个磁盘。”
代码块设置如下:
@Provider
public class ColorParamConverterProvider implements ParamConverterProvider {
@Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
if (rawType.equals(Color.class)) {
return (ParamConverter<T>) new ColorParamConverter();
}
return null;
}
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
global:
auth: basic
healthCheck: true
ingress: false
istio: false
istioNamespace: mesh
route: true
traceSpec: "com.ibm.hybrid.cloud.sample.stocktrader.broker. BrokerService=fine:*=info"
jsonLogging: true
disableLogFiles: false
monitoring: true
specifyCerts: false
任何命令行输入或输出都应如下编写:
kubectl create configmap app-port --from-literal port=9081
粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“我们已经了解到有一些有用的工具,例如 GraphiQL,可以简化测试。”
小贴士或重要注意事项
看起来像这样。
联系我们
我们欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 发送电子邮件给我们,并在邮件主题中提及书名。
勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。
盗版: 如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 联系我们,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
分享您的想法
一旦您阅读了《使用 MicroProfile 的实用云原生 Java 开发》,我们非常乐意听到您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
第一部分:云原生应用程序
在本节中,你将了解一个应用程序为何是云原生的含义。你还将学习 MicroProfile 的基础知识以及它是如何提供构建你自己的云原生 Java 应用程序所需的工具。在本节中,你还将了解将在本书其余部分使用的真实世界示例应用程序。
本节包括以下章节:
-
第一章, 云原生应用程序
-
第二章, MicroProfile 如何融入云原生应用程序开发?
-
第三章, 介绍 IBM 股票交易云原生应用程序
第一章: 云原生应用
当谈论云原生应用时,重要的是对云原生意味着什么有一个共同的理解。通常有一个假设,即云原生和微服务是同一件事,但实际上,微服务只是构建云原生应用时可以使用的一种架构模式。这引出了以下问题:什么是云原生应用,构建它们的最佳实践是什么?这将是本章的重点。
尤其是我们将涵盖以下主要主题:
-
什么是云原生应用?
-
介绍分布式计算
-
探索云原生应用架构
-
云原生开发最佳实践
本章将为理解本书的其余部分提供一些基础,同时帮助你在构建云原生应用时取得成功。
什么是云原生应用?
回到 2010 年,Paul Freemantle 写了一篇关于云原生的早期博客文章(pzf.fremantle.org/2010/05/cloud-native.html),并使用了试图在 6 车道高速公路上驾驶马车类比。无论高速公路作为道路有多好,马车能运输的货物和运输速度都是有限的。你需要为高速公路设计的车辆。应用也是如此。
设计在传统数据中心运行的应用程序与专门设计以利用云的能力的应用程序相比,在云上运行效果不佳。换句话说,云原生应用是专门设计以利用云提供的功能的。来自第八章的股票交易员应用程序,构建和测试云原生应用,就是这样一个应用的例子。微服务的真实世界例子是 Netflix。
也许在核心上,云的承诺是能够按需获取计算资源,几分钟或几秒钟而不是几天或几周,并且根据增量使用量而不是潜在使用量进行收费——尽管,对于许多人来说,吸引力仅仅是不再需要管理和维护多个数据中心。云提供的计算资源商品化导致了对应用的设计、规划和设计有非常不同的思考方式,这些差异显著影响了应用。应用设计中的一个关键变化是应用的分布式程度。
介绍分布式计算
大多数云原生架构涉及将应用程序拆分为几个离散的服务,这些服务通过网络链接而不是进程内方法调用进行通信。这使得云原生应用隐式地成为分布式应用,尽管分布式计算并不是什么新鲜事物,但它确实增加了理解分布式计算优势和陷阱的需求。在构建分布式应用时,重要的是要考虑和理解分布式计算的八个谬误。这些如下:
-
网络是可靠的。
-
延迟为零。
-
带宽是无限的。
-
网络是安全的。
-
拓扑不会改变。
-
只有一个管理员。
-
传输成本为零。
-
网络是同质的。
从本质上讲,这些谬误意味着网络调用比调用 Java 方法或 C 过程要慢,安全性更低,可靠性更差,更难修复。在创建云原生应用时,需要仔细考虑以确保这些谬误得到正确考虑,否则,应用将会运行缓慢、不可靠、不安全,且难以调试。
由多个服务通过网络交互组成的程序可以产生许多好处,例如能够单独扩展和更新服务,但必须注意设计服务以最小化实现最终商业解决方案所需的网络交互次数。
因此,可以使用几种云原生架构来构建云原生应用,这些架构在分布式计算的优势和挑战之间提供了不同的权衡。
探索云原生应用架构
自 2019 年以来,关于微服务作为云原生应用架构的优缺点在业界引发了越来越多的讨论。这主要是由于许多与微服务相关的失败,因此,人们现在正在讨论是否有些应用使用不同的架构会更好。甚至有关于重建单体应用的复兴开始出现,在过去的几年里,这类应用被视为反模式。
虽然将云原生视为仅仅是技术选择很有吸引力,但了解开发流程、组织结构和文化如何影响云原生应用的演变、系统架构以及任何最终的成功是很重要的。康威定律指出以下内容:
任何设计系统的组织都会产生一个结构,其结构与组织的沟通结构相匹配。
将这种想法简单化的一种方式是,如果你的开发组织在构建单体应用方面很成功,那么在没有某种重组的情况下,它不太可能成功构建微服务。这并不意味着每个想要进行云原生开发的团队都应该出去重组;这意味着在决定采用什么架构时,你应该了解自己的优势和劣势。如果需要,你也应该开放地考虑重组。
本节讨论了目前一些更受欢迎的云原生应用架构及其优缺点。让我们从微服务开始。
微服务
虽然 Netflix 并没有发明微服务的概念,但他们对该架构的使用确实使其流行起来。单个微服务被设计用来做一件事情。尽管名字上听起来服务很小或轻量级,但实际上并不一定如此——一个单独的微服务可能有数百万行代码,但微服务中的代码具有高度的凝聚力。微服务永远不会处理 ATM 取款和出售电影票。确定将云原生应用程序设计成一系列精心设计的微服务的最佳方式并不是一个简单的任务;不同的人可能会有不同的观点,认为银行账户的存取是否需要单个微服务或两个。
微服务通常通过 REST 接口或消息系统相互集成,尽管 gRPC 和 GraphQL 越来越受欢迎。面向 Web 的微服务可能会使用 REST 或 GraphQL 接口,但内部微服务更有可能使用 Apache Kafka 等消息系统。消息系统通常对网络问题具有很强的容错能力,因为一旦消息系统接受了消息,它就会存储该消息,直到它可以成功处理。
基于微服务的架构的关键承诺是每个微服务都可以独立部署、更新和扩展,允许拥有不同微服务的团队并行工作,无需协调即可进行更新。这可能是在微服务架构中最大的挑战。有良好意愿的开发者开始构建微服务时,最终可能会构建一个分布式单体,这种情况相对常见。这通常是由于服务之间定义不明确和文档不充分,以及验收测试不足,导致在更新单个微服务时缺乏对其他服务的信任。这被称为分布式单体,因为最终你得到了单体和微服务的所有缺点,却失去了它们的优点。
在理想的世界里,一个构建微服务的开发组织会将微服务与单个开发团队对齐。如果微服务的数量超过开发团队的数量,这可能很困难。随着一个团队管理的微服务数量增加,将会有更多的时间用于管理服务而不是演进服务。
单体应用
单体应用与云前的应用架构紧密相关,被认为是云原生应用的反模式。因此,在讨论云原生架构时,这似乎有些奇怪。然而,有一些原因包括它们在内。
第一点实际上只是现实,即单体应用是构建最简单的一种应用。虽然单个服务不能独立扩展,但只要单体应用设计得可以扩展,这可能不是问题。
第二点是,现在有很多单体应用,许多企业正在将它们迁移到云端。MicroProfile提供了额外的 API,可以将许多云原生行为重构到现有应用程序中。
单体应用的技巧是确保尽管服务位于单个部署工件中,但单体应用可以快速启动,以便在应用程序失败时启用动态扩展和重启。
通常,小型开发组织会从单体应用中受益,因为只有一个应用程序需要构建、部署和管理。
宏服务
宏服务位于单体应用和微服务架构之间,也被称为模块化单体应用。在宏服务中,服务被组合成少量单体应用,它们以与一系列微服务相同的方式交互操作。
这提供了许多微服务的好处,但显著简化了操作环境,因为要管理的事物更少。如果一个宏服务编写得很好,那么其中的单个服务如果从独立的生命周期中受益,就可以将其拆分出来。一个著名的宏服务例子是Stack Overflow。Stack Overflow (www.infoq.com/news/2015/06/scaling-stack-overflow/) 众所周知是一个单体应用,除了标签功能,由于性能需求不同,该功能由另一个应用程序处理。这种拆分使其从纯单体应用转变为宏服务领域(尽管 Stack Overflow 使用术语monolith-plus)。
当开发组织被组织成比服务数量更少的团队时,这种架构可以特别有效。
函数即服务
函数即服务(FaaS),通常被称为无服务器,是一种服务创建为在事件发生时运行的函数的架构。该函数旨在快速启动和快速执行,可以通过诸如 HTTP 请求或接收到的消息等事件触发。FaaS 承诺你可以将函数部署到云中,并由事件触发器启动和执行,而不是必须让函数一直运行以备不时之需。通常,支持 FaaS 的公共云提供商只对函数运行时间收费。如果事件相对不常见,这非常吸引人,因为没有在罕见事件发生时让系统运行的经济成本。
这种架构的挑战在于,你的函数需要能够快速启动,并且通常也需要快速执行完成;因此,它不适合长时间运行的过程。它也没有移除服务器;服务器仍然存在。相反,它只是将成本从开发者转移到了云服务提供商。如果云服务提供商是公共云,那么这是他们的问题,因为他们会根据函数运行时间收费,但如果你部署到私有云,这就会成为你的问题,从而消除了部分好处。
事件溯源
通常,我们认为服务提供 REST 端点,并且服务会调用它们。实际上,十二要素应用(在下一节中讨论)的第 VII 要素明确指出这一点。这种方法的缺点是,REST 调用是隐式同步的,如果服务提供者运行缓慢或失败,则容易出现问题。
当为移动应用或网页浏览器提供外部 API 时,REST API 通常是最佳选择。然而,对于企业内部的服务,使用如 Kafka 的消息系统以及使用异步****事件有许多好处。能够保证消息将被传递的消息系统允许客户端和服务解耦,这样服务提供者的问题不会阻止请求的发生;只是意味着它会被稍后处理。一对一的事件系统使得单个服务通过简单的消息发送就能触发多个不同的动作。不同的服务可以通过接收消息副本采取不同的动作,如果需要新的行为,额外的服务可以接收相同的消息,而无需更改发送服务。一个简单的例子可能是,一个订购商品的订单事件可以被支付服务、调度服务、补货服务和基于过去购买提供推荐的服务处理。
云原生应用程序的一个趋势是数据从集中的数据存储移动到更靠近单个服务的地方。每个服务都操作它持有的数据,因此如果某个服务的数据存储变慢,它不会对其他服务产生连锁反应。这意味着需要新的机制来确保数据一致性。使用事件来处理数据更新有助于这一点,因为单个事件可以被分发到每个需要独立处理更新的服务。即使服务在更新触发时处于关闭状态,更新也可以生效。这种方法的另一个优点是,如果数据存储失败,可以通过重放所有事件来重建它。
在选择了构建你的云原生应用程序的架构(或架构)之后,下一步是开始构建它,为此,了解一些关于云原生应用程序开发的行业最佳实践是个好主意。
云原生开发最佳实践
有许多最佳实践,如果遵循这些实践,将提高你的云原生应用程序成功的可能性。遵循这些最佳实践并不能保证成功,正如忽视它们并不能保证失败一样,但它们确实编码了已被证明可以增加成功机会的关键实践。最著名的最佳实践集合是十二要素应用。
十二要素应用
十二要素应用(12factor.net)是一套 12 个最佳实践,如果遵循这些实践,可以显著提高构建云原生应用程序成功的可能性。其中一些因素对于许多软件开发者来说可能很显然,即使是在云原生之外,但综合起来,它们形成了一种构建云原生应用程序的流行方法。以下是 12 个要素:
-
代码库
-
依赖项
-
配置
-
后端服务
-
构建、发布、运行
-
进程
-
端口绑定
-
并发
-
可丢弃性
-
开发/生产一致性
-
日志
-
管理进程
I – 代码库
第一个要素指出,云原生应用程序由一个单一的代码库组成,该代码库在版本控制系统(如 Git)中跟踪,并且该代码库将被部署多次。部署可能是在测试、预发布或生产环境中。这并不意味着这些环境中的代码将是相同的;显然,测试环境将包含尚未被证明对生产环境安全但被提出的代码更改,但仍然是一个代码库。
II – 依赖项
对于 Java 应用程序来说,使用存储在 Maven 仓库(如 Maven Central)中的依赖项作为常见的开发实践已经有一段时间了。例如,Maven 和 Gradle 工具要求你表达你的依赖项,以便针对它们进行构建。虽然这种做法绝对需要这样做,但它不仅限于构建时的依赖项,还包括运行时的依赖项。12 因素应用程序将依赖项打包到应用程序中,以确保单个开发工件可以在任何合适的环境中可靠地部署。这意味着管理员在文件系统上的知名位置提供库是不可接受的,因为总是有可能管理员部署的库与应用程序所需的库不兼容。
在考虑这种实践时,明确决定云原生应用程序的定义非常重要,因为最终应用程序提供的内容和部署环境提供的内容之间将会有所分离。这个因素促使企业 Java 从WAR文件转向可执行的JAR文件,因为许多人认为应用程序服务器是一个隐含的依赖。然而,这仅仅是将隐含的依赖降低了一个级别;它并没有消除它。现在,隐含的依赖是 Java。在某种程度上,容器化解决了这个问题,同时消除了围绕可执行JAR文件重新架构的需求。
III – 配置
由于 12 因素应用程序可能有多个部署,并且每个部署可能连接到不同的系统,使用不同的凭证,因此将配置外部化到环境中至关重要。在媒体上关于开发者意外将凭证检查到版本控制系统中导致的安全问题也很常见,如果配置存储在代码库外部,这种情况就不会发生。
虽然这个因素表明配置存储在环境变量中,但许多人对于在环境变量中存储安全敏感的配置感到不安。这里的关键是要以简单的方式外部化配置,以便在生产环境中提供。
IV – 后端服务
后端服务被视为附加资源。应该能够通过简单的配置更改从一种数据库切换到另一种数据库。
V – 构建、发布、运行
所有应用都会经历某种构建、发布、运行的过程,但 12 因子应用在这三个阶段之间有严格的分离。构建阶段涉及将应用源代码转换为应用工件。发布阶段将应用工件与配置结合,以便可以部署。运行阶段是实际执行的时候。这种严格的分离意味着运行阶段永远不会进行配置更改,因为没有方法可以将其回滚到发布阶段。相反,如果需要配置更改,则创建一个新的发布并运行。如果需要代码更改,也是如此。没有通过构建和运行来更改正在运行中的代码。这确保了你始终知道正在运行什么,并且可以轻松地重现问题或回滚到先前的版本。
VI – 进程
一个 12 因子应用由一个或多个无状态进程组成。这并不意味着每个请求都映射到一个单独的进程;在 Java 中,一个 JVM 同时处理多个请求是完全合理的。这意味着应用不应该依赖于某个进程在请求之间保持可用。如果一个客户端发送了 20 个请求,必须假设每个请求都是由一个独立的进程处理的,且进程之间不保留任何状态。将服务器端与用户关联的状态存储为一个常见模式。这个状态应该始终持久化到外部数据存储中,这样如果后续请求发送到不同的进程,就不会对客户端产生影响。
VII – 端口绑定
应用通过端口绑定导出服务。这意味着 HTTP 应用不应该依赖于被安装到 Web 容器中,而应该声明对 HTTP 服务器的依赖,并在启动时打开一个端口。这导致许多人认为 12 因子 Java 应用必须构建为一个 uber-jar,但这只是构建单个部署工件并绑定端口的想法之一。一个替代方案并且更有用的一种解释是使用容器;容器在很大程度上是围绕端口绑定的概念构建的。应该注意的是,这种做法并不总是适用;例如,由 Kafka 消息驱动的微服务就不会绑定到端口。此外,许多 FaaS 平台不提供端口绑定的 API。
VIII – 并发
Java 中的并发通常是通过增加分配给进程的资源来实现的,以便可以创建更多的线程。在 12 因素中,你增加的是实例数量而不是计算能力。向单台机器添加计算能力是有限的,但添加一个等效大小的新的虚拟机相对容易。这种做法与第 VI 个因素相关,因此它们相互补充和加强。尽管这可能被解读为建议每个请求一个进程的模型,但基于 Java 的应用程序完全能够比 1:1 的进程与请求比例更有效地运行多个线程。
IX – 可丢弃性
每个应用程序都应被视为可丢弃的。这意味着确保进程快速启动、迅速关闭并能够处理终止。采用这种方法可以使应用程序很好地扩展并快速扩展,同时也能抵御意外的故障,因为可以从最后发布版本快速且轻松地重新启动进程。
X – 开发/生产一致性
许多应用问题都源于开发和预发布环境之间的差异。在过去,这是因为安装和启动所有下游软件都很困难,但随着容器技术的出现,这一体验得到了显著简化,使得许多系统可以在更早的环境中运行。这种做法的优势在于,你不再会因为你的开发数据库与预发布环境对 SQL 的解释不同而遇到问题。
XI – 日志
应用程序应编写遵循进程输出而不是日志文件的log函数。
XII:管理进程
管理进程应作为独立于应用程序的一次性进程运行,并且不应与应用程序启动同时进行。这些应用程序进程的代码应与主应用程序一起管理,以便可以使用用于正常流程的发布来执行管理任务。这确保了应用程序和管理代码不会出现分歧。
其他最佳实践
12 因素应用的概念已经存在了一段时间;在任何方法论中,重要的是要记住,对某些人有效的方法可能对其他人无效,有时方法论需要随着我们对如何成功理解的演变而发展。因此,通常还会添加一些其他最佳实践到之前讨论的 12 个因素中。最常见的一个是关于描述服务 API 及其测试方法的重要性,以确保对某个服务的更改不需要客户端服务的协调部署。
API 和合同测试
虽然 12 因素方法详细介绍了创建和执行云原生应用程序的大量有用实践,但它很少讨论应用程序服务如何交互以及如何确保更改一个服务不会导致另一个服务需要更改。设计良好且文档清晰的API对于确保服务更改不会影响客户端至关重要。
仅对 API 有文档是不够的;还必须确保服务提供商的更改不会对客户端产生负面影响。由于任何错误修复都可能引起更改,因此提供商可能认为更改是安全的,并意外地破坏客户端。这就是契约测试可以发挥作用的地方。契约测试的优势在于,每个系统(客户端和服务器)都可以进行测试,以确保对任一方的更改不会违反契约。
安全性
12 因素方法中最明显的差距之一是关于安全性的最佳实践的缺乏。从某个角度来看,这是因为已经存在一套用于保护应用程序的最佳实践,这些最佳实践同样适用于云原生应用程序和传统应用程序。例如,关于配置地址的第三项实践,至少部分地解决了如何通过外部化来保护凭证(或其他机密信息)的问题。然而,这个因素并没有讨论如何安全地将机密信息注入环境以及它们是如何被存储和保护的,这取决于部署环境。这在第七章,“使用 Open Liberty、Docker 和 Kubernetes 的 MicroProfile 生态系统”中进行了更详细的讨论。
将系统分解为微服务会增加额外的复杂性,这在单体架构中并不适用。在单体架构中,你可以信任应用程序的各个组件,因为它们通常在相同的过程空间中共同部署。然而,当单体被分解为微服务并使用网络连接时,需要使用其他机制来维持信任。使用JSON Web Tokens(JWTs)就是这样一种在微服务之间管理和建立信任的机制。这在第五章,“增强云原生应用程序”中进行了更详细的讨论。
GraphQL
在许多云原生思想中存在一个默认假设,即暴露的 API 是基于 REST 的。然而,这可能导致网络调用增加和通过网络发送的数据过多。GraphQL是一种相对较新的创新,它允许服务客户端通过 HTTP 连接从数据存储请求所需的确切信息。传统的 REST API 必须提供有关资源的所有数据,但通常只需要一部分。当使用 RESTful API 时,由于提供了客户端不使用的数据,网络带宽和客户端数据处理通常会被浪费。GraphQL 通过允许客户端向服务发送查询,请求他们确切需要的数据,而不需要更多数据,从而解决了这个问题。这减少了传输和从后端数据存储检索的数据量。MicroProfile 提供了一个基于 Java 的 API 来编写 GraphQL 后端,这使得编写为客户端提供基于查询的 API 的服务变得容易。
摘要
在本章中,我们学习了什么是云原生应用,并了解了一些构建它们的架构。我们还学习了构建云原生应用的一些最佳实践以及它们存在的原因,这样我们就可以确定何时以及如何应用它们。这为将本书中剩余部分所学内容应用于构建和部署云原生应用提供了良好的基础。
在下一章中,我们将探讨 MicroProfile 是什么以及如何用它来构建云原生应用。
第二章:MicroProfile 如何融入云原生应用开发?
本章为您提供了 MicroProfile 的概述,并从满足云原生应用需求的角度描述了 MicroProfile 的规范。在本章中,我们首先回顾 MicroProfile 的历史,探讨其创建的原因和目的,然后我们将探讨 MicroProfile 的内容。这将使您对每个 MicroProfile 规范有一个高层次的理解,以便您了解 MicroProfile 如何融入云原生应用开发,以及为什么您应该采用 MicroProfile 技术。最后,我们将关注 MicroProfile 代码生成器,即 MicroProfile Starter,重点介绍如何创建云原生应用。这很有用,因为它将帮助您从头开始使用 MicroProfile。
本章将涵盖以下主题:
-
MicroProfile 概述
-
MicroProfile 规范
-
MicroProfile Starter
MicroProfile 概述
让我们先回顾 MicroProfile 的历史,探讨其成立的原因和进展,以及如何建立其工作组。在本节中,我们将探讨两个不同的子主题:MicroProfile 的历史和MicroProfile 的特点。我们将从历史开始。了解发布周期和各个 MicroProfile 版本包含的内容非常重要,这样我们才能选择使用哪个版本,并了解 MicroProfile 发布新版本的频率。
MicroProfile 历史
看到 Java EE 发展的缓慢步伐,包括 IBM、Red Hat、Payara、Tomitribe 等在内的几家主要行业参与者于 2016 年聚在一起,讨论如何使服务器端 Java 框架发展更快,并解决与新兴微服务空间相关的新挑战。作为这次合作的结果,MicroProfile 于 2016 年秋季诞生。它旨在帮助 Java 开发者开发云原生应用,而无需学习新语言。
MicroProfile 1.0 于 2016 年 9 月在 JavaOne 上宣布发布。MicroProfile 1.0 的首个版本包括 CDI、JSON-P 和 JAX-RS。2016 年 12 月,MicroProfile 在 Apache License v2.0 (Alv2.0) 许可下加入了 Eclipse 基金会。MicroProfile 1.1 于 2017 年 8 月发布,并包括了第一个新的 MicroProfile 规范,即 MicroProfile Config 1.0。从 2017 年到 2019 年,MicroProfile 每年发布三次:2 月、6 月和 10 月。
2020 年,在 MicroProfile 3.3 版本发布之后,Eclipse 基金会要求 MicroProfile 在进行任何进一步的重大或小版本发布之前,先设立其工作组。社区为此花费了近一年的时间来决定是设立自己的工作组还是与 Jakarta EE 工作组合并。最终,IBM、Red Hat 和 Tomitribe 决定成立一个独立的工作组。由于工作组要求至少有 5 家企业成员,社区花了些时间才获得另外 2 家企业成员的支持。10 月份,亚特兰大 Java 用户组(AJUG)和 Jelastic 与 IBM、Red Hat 和 Tomitribe 联合,成立了 MicroProfile 工作组。最终,在 2020 年 10 月,工作组章程获得批准。不久之后,Payara、Oracle、富士通、Garden State JUG 和 iJUG 也加入了 MicroProfile 工作组。
2020 年 10 月 MicroProfile 工作组成立后,MicroProfile 社区立即开始准备使用新建立的发布流程发布 MicroProfile 4.0 版本。12 月 23 日,MicroProfile 4.0 版本发布。最新的 MicroProfile 版本 4.1 于 2021 年 7 月最近发布。以下是 MicroProfile 发布的时间线:

图 2.1 – MicroProfile 发布时间线
如您从 图 2.1 中所见,MicroProfile 具有快速的发布节奏。此外,MicroProfile 还有其他识别特征。在下一节中,我们将探讨它们。
MicroProfile 的特点
MicroProfile 由于其开放性和多样性等独特特性而迅速发展。具体如下:
-
开放性和透明度:MicroProfile 在会议、项目、贡献等方面对公众开放。没有等级制度,每个人都有权表达自己的观点。没有人有否决权。
-
多样性:社区是多元化的。其贡献者包括 IBM、Red Hat、Tomitribe、Payara、Java 用户组以及其他个人和团体。每个人都欢迎加入 MicroProfile 并表达自己的观点。加入对话的方式是通过 MicroProfile Google 群组,可通过 microprofile.io 上的 加入讨论 访问。
-
许多运行时实现:MicroProfile 发展 API、规范和技术兼容性工具包(TCK)。它只创建 API,但不包括实现。然而,并不缺少实现。大约有十几种实现,如下所示:
a) Open Liberty (
openliberty.io/)b) WebSphere Liberty (
www.ibm.com/uk-en/cloud/websphere-liberty)c) Quarkus (
quarkus.io/)d) Wildfly (
www.wildfly.org/)e) Payara (
www.payara.fish/)f) TomEE (
tomee.apache.org/)g) Helidon (
helidon.io)h) Launcher (
github.com/fujitsu/launcher)i) KumuluzEE (
ee.kumuluz.com/)j) Piranha Cloud (
piranha.cloud/)k) Apache Geronimo (
geronimo.apache.org/) -
轻量级、迭代的过程:MicroProfile 建立了一种快速移动并采用迭代过程的模式。它适应变化,允许在版本之间进行破坏性更改,并采用语义版本控制策略,这意味着包含破坏性更改的主要版本发布。然而,MicroProfile 试图最小化破坏性更改的数量。当有破坏性更改时,这些更改必须在相应规范的发布说明中进行明确记录。
由于 MicroProfile 的上述特性,其采用率迅速增长。因此,越来越多的公司开始投资 MicroProfile 技术。现在,MicroProfile 被视为开发云原生应用的标准。在下一节中,我们将更详细地查看每个单独的 MicroProfile 规范。
MicroProfile 规范
在这一点上,你可能想知道 MicroProfile 包含哪些内容。正如你可能知道的,MicroProfile 在开发云原生应用方面不断演进应用程序编程接口(APIs)。MicroProfile 有几个规范,为云原生应用提供了各种功能。作为一名云原生应用开发者,了解这些是非常重要的。
MicroProfile 4.0 版本包括来自 Jakarta EE 8 的四个规范和八个 MicroProfile 规范,如下所示:
Jakarta EE 规范:
-
Jakarta 上下文和依赖注入(CDI)2.0
-
Jakarta RESTful Web 服务(JAX-RS)2.1
-
Jakarta JSON 绑定 1.0
-
Jakarta JSON 处理 1.1
MicroProfile 规范:
-
MicroProfile 配置 2.0
-
MicroProfile 容错 3.0
-
MicroProfile 健康 3.0
-
MicroProfile JWT 传播 1.2
-
MicroProfile 度量 3.0
-
MicroProfile OpenAPI 2.0
-
MicroProfile OpenTracing 2.0
-
MicroProfile Rest 客户端 2.0
MicroProfile 规范根据不同的发布类别进行分组。MicroProfile 有平台发布和独立发布的概念。在本节中,我们将详细介绍这两个概念。
平台发布
平台发布包括 12 个规范,其中 4 个来自 Jakarta EE。根据它们的用途,这些规范可以分为三个子组(三个层次):
-
构建云原生应用:CDI、JAX-RS、Rest 客户端、JSON-B 和 JSON-P
-
增强云原生应用:Open API、容错、JWT 传播和配置
-
观察和监控云原生应用:Open Tracing、健康和度量
让我们依次探索这些组。
开发云原生应用的技术
云原生应用程序可以使用 CDI、JAX-RS、JSON-B 或 JSON-P 来开发。MicroProfile Rest Client 用于连接云原生应用程序。接下来是一个关于这些技术的快速概述。第四章,开发云原生应用程序,将更详细地解释它们。
CDI – Contexts and Dependency Injection
@ApplicationScoped、@RequestScoped 和 @Dependent。@ApplicationScoped 注解意味着每个云原生应用程序只有一个实例。依赖注入通过 @Inject 注解指定,该注解由 Jakarta 注入规范定义。
JAX-RS – Jakarta RESTful Web Services
@Path、@GET、@PUT、@POST、DELETE、@Produces 和 @Consumes。让我们逐个查看这些实例:
-
@Path指定资源或方法的相对路径。 -
@GET、@PUT、@POST和@DELETE指定 HTTP 请求类型。 -
@Produces指定响应媒体类型,例如MediaType.APPLICATION_JSON。 -
@Consumes指定接受的媒体类型。 -
以下是一个 JAX-RS 服务的示例。它声明了一个带有端点的
GET操作,例如http://localhost:9080/system/properties。当调用此 URL 时,将返回系统属性。由于@Produces指定了MediaType.APPLICATION_JSON格式,因此有效载荷格式将采用称为 JavaScript 对象表示法(JSON)的格式:
@ApplicationScoped // (1)
@Path("/properties") // (2)
public class PropertyController {
@GET // (3)
@Produces(MediaType.APPLICATION_JSON) // (4)
public Properties getProperties() {
return System.getProperties();
}
}
让我们来看看这段代码中的四条注释行:
-
@ApplicationScoped注解,由 CDI 定义,表示PropertyController资源的生命周期是单一的,每个应用程序只有一个实例。 -
@Path注解指定了到PropertyController资源的相对路径,即为/properties。 -
@GET注解表示 JAX-RS 操作类型。 -
@Produces(MediaType.APPLICATION_JSON)注解强制要求有效载荷以 JSON 格式。
到现在为止,你应该对 JAX-RS 有了一个基本的了解。在下一节中,我们将探讨如何使用 MicroProfile Rest Client 连接 RESTful 服务。
MicroProfile Rest Client
@RegisterRestClient 用于声明类型安全接口,如下面的代码片段所示。
以下示例定义了 JAX-RS 操作 PropertyController.getProperties() 的类型安全接口,如前节所示:
@RegisterRestClient(baseUri="http://localhost:9081/system") // (1)
public interface PropertiesClient {
@GET // (2)
@Produces(MediaType.APPLICATION_JSON) // (3)
@Path("/properties") // (4)
public Properties getProperities();
}
让我们来看看这段代码中的四条注释行:
-
@RegisterRestClient注解将PropertiesClient接口注册为 RESTful 客户端。 -
@GET注解表示getProperties()方法是一个GET操作。 -
@Produces(MediaType.APPLICATION_JSON)注解指定了有效载荷格式为 JSON 格式。 -
@Path注解声明了getProperties()操作的相对路径。
类型安全的客户端同时支持 CDI 和程序性查找。以下代码片段演示了如何使用 CDI 注入PropertiesClient RESTful 客户端,然后调用其getProperties()方法:
@Path("/client")
@ApplicationScoped
public class ClientController {
@Inject @RestClient
private PropertiesClient; // (1)
@GET
@Path("/props")
@Produces(MediaType.APPLICATION_JSON)
public Properties displayProps() throws IllegalStateException, RestClientDefinitionException,
URISyntaxException {
return propertiesClient.getProperities(); // (2)
}
}
让我们来看看这个代码片段中的两条注释行:
-
@Inject(由@RestClient(由PropertiesClient定义的 CDI 限定符)注入到propertiesClient变量中。 -
调用后端操作,
getProperties(),即PropertyController.getProperties()。
或者,您可以使用RestClientBuilder程序性 API 来获取客户端,这将在第四章 开发云原生应用程序中讨论。
在前面的示例中,JAX-RS 服务的响应是 JSON 格式,这是最受欢迎的响应格式。我们将在下一节中讨论 JSON,如何将其转换为对象,以及从对象转换为 JSON 对象。
JSON-B 和 JSON-P
JSON 是云原生应用程序中传输数据的主要格式。JSON 支持两种数据结构:对象和数组。对象是一系列键值对,由大括号括起来,而数组将这些对象收集到一个集合中。
两者JSON-B(github.com/eclipse-ee4j/jsonb-api)和JSON-P(github.com/eclipse-ee4j/jsonp)都是 Jakarta EE API 规范,用于将 POJOs 转换为 JSON 数据以及从 JSON 数据转换回 POJOs。JSON-P 的第一个版本比 JSON-B 早几年发布。JSON-P 为 JSON 处理提供了流式和数据模型。
JSON-B 提供了一种将 Java 对象转换为 JSON 消息或从 JSON 消息转换回 Java 对象的机制。它提供了多种方法来序列化/反序列化 Java 对象到/从 JSON。JSON-B 提供的 API 比 JSON-P 更高级。JSON-B 与 JAX-RS 和 JAX-RS 2.1 兼容,并强制使用 JSON-B 来自动将返回的对象转换为 HTTP 响应中的 JSON 数据。
增强云原生应用程序的技术
建立云原生应用程序后,不幸的是,工作还没有完成。您需要考虑如何改进应用程序。下一个任务是提高其可靠性和可维护性。例如,您是否希望自由更改配置值而无需重新编译应用程序,比如端口号?您是否希望应用程序具有弹性,无论发生什么情况都能持续运行?您是否希望应用程序具有安全性,这意味着不允许未经授权的请求?当您有数十或数百个应用程序时,您需要帮助确定应用程序正在做什么?
如果上述任何问题的答案是是,您将需要添加一些基本的服务质量(QoS),包括以下内容:
-
配置
-
弹性
-
安全性
-
文档
MicroProfile 配置 提供了一种无需重新部署即可配置应用程序的方法。MicroProfile 故障恢复 使应用程序更具弹性。MicroProfile JWT 认证 以便携和简单的方式保护应用程序,而 MicroProfile Open API 用于记录应用程序。接下来,我们将快速概述这些技术,并在 第五章,增强云原生应用程序 中深入探讨每个技术。
MicroProfile 配置
MicroProfile 配置 (github.com/eclipse/microprofile-config) 定义了一个简单灵活的系统来检索应用程序配置。配置定义在配置源中,这些配置源可以由应用程序提供。检索配置有两种方式:CDI 或 程序性查找。让我们依次查看这些方法:
-
"customer.name",以下代码片段可以用来检索其值:@Inject @ConfigProperty(name="customer.name") String customerName; -
customer.name也可以通过以下 API 进行程序性查找:Config = ConfigProvider.getConfig(); String customerName = config.getValue("customer.name", String.class);
在类路径上的 microprofile-config.properties 文件中定义的属性,环境变量和系统属性对云原生应用程序自动可用。这意味着 MicroProfile 配置还可以访问映射到 Pod 的 Kubernetes ConfigMaps 或 Secrets 的环境变量的值。以下代码片段演示了在 Java 属性文件格式中定义的 customer.name 属性:
customer.name=Bob
MicroProfile 配置允许外部化配置。存储在环境中的配置可以通过 Config API 被云原生应用程序访问。本规范实现了在 第一章,云原生应用程序 中提到的 Twelve-Factor App 的第三个因素,配置。您已经学习了如何配置您的应用程序。接下来,我们将简要讨论如何使用 MicroProfile 故障恢复使您的应用程序更具弹性。
MicroProfile 故障恢复
MicroProfile 故障恢复 (github.com/eclipse/microprofile-fault-tolerance/) 定义了一组用于使云原生应用程序具有弹性的注解。这些注解如下:
-
@Retry:从短暂的网络故障中恢复。这允许您定义重试的次数、可以触发重试的异常、重试的时间长度等。 -
@Timeout:定义了最大允许的响应时间。这用于时间敏感的操作。它定义了相应操作响应的最大时间长度。 -
@CircuitBreaker:快速失败并避免可重复的无限等待或超时。您可以指定要检查电路的滚动窗口、电路打开的失败率、电路断路器考虑或忽略的异常等。 -
@Bulkhead:隔离故障并避免整个系统崩溃。有两种类型的隔离舱。当此注解与@Asynchronous注解一起使用时,这意味着线程隔离,这意味着带有此注解的方法将在子线程上执行。否则,它意味着信号量隔离,这意味着方法将在父线程上执行。 -
@Fallback:为失败的执行提供替代解决方案。您应该始终使用此注解来确保健壮的云原生应用程序能够应对各种情况。此注解在原始方法返回异常时提供替代操作。 -
上述注解可以一起使用,这提高了您云原生应用程序的健壮性。一旦您的应用程序可配置且健壮,下一步就是考虑如何防止敏感信息被无关方获取。这正是 MicroProfile JWT 传播发挥作用的地方。
MicroProfile JWT 传播
@RolesAllowed,用于保护 JAX-RS 端点。
MicroProfile JWT 传播建立了一种将用户信息传递到后端的方法,以便后端可以确定调用是否允许。MicroProfile JWT 传播建立在 JWT 之上,并添加了一些额外的声明:JsonWebToken,它扩展了 java.security.Principal 接口。此 API 通过 getter 访问器提供一系列声明。
JAX-RS 应用程序可以通过 SecurityContext 注解访问 JsonWebToken:
@GET
@Path("/getGroups")
public Set<String> getGroups(@Context SecurityContext sec) {
Set<String> groups = null;
Principal user = sec.getUserPrincipal();
if (user instanceof JsonWebToken) {
JsonWebToken jwt = (JsonWebToken) user;
groups= = jwt.getGroups();
}
return groups;
}
或者,它也可以被注入:
@Inject private JsonWebToken jwt;
@Inject @Claim(standard= Claims.raw_token) private String rawToken;
@Inject @Claim("iat") private Long dupIssuedAt;
@Inject @Claim("sub") private ClaimValue<Optional<String>> optSubject;
MicroProfile JWT 认证还确保了单点登录,并且运行时将自动拒绝权限不足或缺少适当声明的请求。一旦您的应用程序可配置、健壮且安全,您就需要考虑如何记录您的应用程序。您可以使用 MicroProfile OpenAPI 来记录您的应用程序。
MicroProfile OpenAPI
http://myHost:myPort/openapi,作为一个 GET 操作。一些 MicroProfile Open API 实现,如 Open Liberty (openliberty.io/),也提供了 Swagger UI 集成并公开了端点,http://myHost:myPort/openapi/ui,允许测试端点。
注意
MicroProfile OpenAPI 生成一组 Java 接口和注解,允许 Java 开发者从他们的 JAX-RS 生成 OpenAPI v3 文档。MicroProfile OpenAPI 受 OpenAPI v3 的影响很大,但它们并不相同。
作为云原生开发者,你的工作几乎已经完成。你可以将你的应用程序部署到云端。如果一切顺利,你将有一个轻松的工作。然而,如果出了问题,你可能很难找出问题所在。为了帮助服务,你需要了解观察和监控应用程序的技术。继续阅读,了解你可以做些什么来帮助服务性。
观察和监控云原生应用程序的技术
在完成云原生应用程序的开发后,下一个阶段是第二天操作,在这一阶段,监控、维护和故障排除变得重要。MicroProfile Health、MicroProfile Metrics和MicroProfile Open Tracing在这些领域提供支持。接下来是对这些技术的快速概述,以帮助你了解这些技术如何协同工作以帮助第二天操作。第六章,观察和监控云原生应用程序,将更详细地介绍这一点。
MicroProfile Health
@Readiness和@Liveness注解,相应地。@Readiness注解应用于HealthCheck实现,以定义就绪检查程序,而@Liveness应用于存活检查程序。HealthCheck程序的响应可以是UP或DOWN。
就绪检查的响应决定了云原生应用程序是否准备好服务请求。如果响应是UP,云基础设施,如Kubernetes,将路由请求到它所在的 Pod。如果响应是DOWN,Kubernetes 将不会将请求路由到 Pod。存活检查的响应意味着云原生应用程序是否仍然存活。如果响应是DOWN,Kubernetes 将销毁云原生应用程序所在的 Pod,并启动一个新的 Pod。
MicroProfile Health 定义了其实现需要暴露的http://myHost:myPort/health/ready和http://myHost:myPort/health/live端点,以表示整个运行时的就绪和存活状态,这可以用于 Kubernetes 的就绪和存活检查。
MicroProfile 度量指标
http://myHost:myPort/metrics/base,http://myHost:myPort/metrics/vendor和http://myHost:myPort/metrics/application。http://myHost:myPort/metrics端点列出了度量指标所有三个作用域的聚合。通过 HTTP REST 暴露的数据可以是 JSON 格式或OpenMetrics文本格式,可以被监控工具如Prometheus消费,以便将度量指标以图表的形式显示在仪表板上。
MicroProfile OpenTracing
将SpanContext信息放入任何出去的 JAX-RS 请求中,然后为任何出去的 JAX-RS 请求启动一个跨度,并在请求完成时完成Span。MicroProfile OpenTracing 采用每个应用程序可用的io.opentracing.Tracer。
MicroProfile OpenTracing 公开了跟踪工具(如Jaeger或Zipkin)可以用来收集数据并在仪表板上图形化的跟踪跨度。
你已经学到了 MicroProfile 的基本规范。然而,还有一些额外的规范在独立版本之后发布。让我们看看这些规范,看看你是否可以在你的应用中使用它们。
独立版本
从 2018 年开始,又发布了几个 MicroProfile 规范:MicroProfile Reactive Streams Operators、MicroProfile Messaging、MicroProfile Context Propagation和MicroProfile GraphQL。
MicroProfile 社区希望在将这些规范合并到综合版本之前收集更多反馈。因此,它们仍然作为独立规范存在。第十章,反应式云原生应用,和第十一章,MicroProfile GraphQL将更详细地讨论这些规范。现在,让我们概述每个规范:
-
map、filter和flatMap。它还提供了 MicroProfile Messaging 可用的 API。 -
@Incoming用于消费消息,@Outgoing用于发布消息。 -
CompletionStage、CompletableFuture和Function,以便在具有一些相关上下文的云原生应用中更好地工作。MicroProfile Context Propagation 使异步编程能够感知上下文,因为新线程可以从父线程继承一些上下文,例如Security Context、CDI Context、Application Context、Transaction Context以及定义在ThreadContext中的其他应用。 -
@Query和@Mutuation,用于构建 GraphQL 查询和突变。
到目前为止,我们已经迅速了解了所有 MicroProfile 规范。如果你对其中的一些规范不理解,不要担心,我们将在接下来的章节中更深入地介绍它们。
你现在可能想知道如何使用 MicroProfile 创建云原生应用,以及是否有任何工具可以帮助创建云原生应用。我们将在下一节中介绍。
MicroProfile Starter
MicroProfile Starter (start.microprofile.io/)是使用 MicroProfile 开发云原生应用的代码生成器。这个工具可以通过网络、命令行或 IDE 插件访问。在本节中,我们将学习 MicroProfile Starter 工具,用于创建云原生应用。
通过网络访问 MicroProfile Starter
以下截图显示了 MicroProfile Starter 的用户界面,可以用来创建云原生应用,然后下载 ZIP 文件:

图 2.2 – MicroProfile Starter UI
在前面的 UI 中,我们可以指定以下内容:
-
groupId:生成的应用的 Maven 组 ID。
-
artifactId:生成的应用的 Maven 项目的 ID。
-
MicroProfile 版本:MicroProfile 发布版本的版本号。
-
MicroProfile 运行时:支持所选 MicroProfile 发布版本的所选运行时。在图 2.2中,选择了 MicroProfile 版本 3.3,然后显示了实现 MicroProfile 版本 3.3 的三个运行时:Open Liberty、Wildfly和Payara Micro。
-
Java SE 版本:一旦我们选择了我们喜欢的运行时,我们就可以选择 Java SE 版本。如果运行时支持多个 Java SE 版本,我们就可以选择我们想要的 Java SE 版本。例如,Open Liberty 支持 Java SE 8 和 Java SE 11。一旦我们选择了 Open Liberty,我们就可以选择 Java SE 8 或 Java SE 11。
-
规范示例:生成的应用将要使用的 MicroProfile 规范。点击复选框将选择相应的 MicroProfile 规范,代码示例将包括所选的 MicroProfile 规范。如果选择了 TypeSafe Rest Client 或 JWT Auth,将生成两个云原生应用来演示客户端-服务器架构。如果您想从头创建 MicroProfile 应用,则不需要选择任何复选框。在这种情况下,您将设置好结构,然后可以直接编写业务代码。
通过命令行访问 MicroProfile Starter
MicroProfile Starter 支持命令行,如果您想在命令行或作为自动化过程的一部分自动生成云原生应用,这将非常有用。
您可以通过以下命令找到所有信息,其中输出显示了所有支持的功能及其对应的命令:
curl 'https://start.microprofile.io/api'
在上述命令的输出中,您可以找到用于为所选运行时、MicroProfile 版本等创建特定云原生应用的进一步命令。
通过 IDE 插件访问 MicroProfile Starter
MicroProfile Starter 还可以通过 IDE 插件访问,即Visual Studio Code MicroProfile 扩展包或IntelliJ IDEA MicroProfile 插件,我们现在将探讨这些插件。
Visual Studio Code 插件
Visual Studio Code MicroProfile 扩展包(marketplace.visualstudio.com/items?itemName=MicroProfile-Community.vscode-microprofile-pack)可以下载并安装到 Visual Studio Code(code.visualstudio.com)。
此扩展包还包括MicroProfile 语言服务器支持、Open Liberty 工具、Quarkus和Payara 工具。
IntelliJ IDEA 插件
IntelliJ IDEA (www.jetbrains.com/idea/) 提供了一个 MicroProfile Starter 插件 (plugins.jetbrains.com/plugin/13386-microprofile-starter),让您可以直接从 IntelliJ IDE 访问 MicroProfile Starter。您只需简单地安装插件,就可以开始使用 MicroProfile Starter。在使用插件时,您可以输入与 图 2.2 中所示相同的字段,然后就会创建一个应用程序。
摘要
在本章中,我们学习了所有的 MicroProfile 规范,并讨论了它们如何帮助创建云原生应用程序。然后我们添加了各种 QoS,例如配置、弹性、安全和监控。有了这些,您将有一个基本的想法,了解如何使用最佳实践来设计您的云原生应用程序,使其安全、可配置、弹性、智能和可监控。在接下来的章节中,我们将更深入地学习这些技术。
此外,在了解了 MicroProfile 规范之后,我们介绍了 MicroProfile Starter,这是一个用于开发云原生应用程序的工具。该工具可以通过网页、命令行、Visual Studio 插件或 IntelliJ IDEA 插件访问。您将能够使用这些工具从头开始创建您的云原生应用程序。
在下一章中,我们将介绍一个利用 MicroProfile 技术解决一些常见问题的真实世界、云原生应用程序,并学习 MicroProfile 如何帮助解决真实世界用例带来的挑战。
第三章:介绍 IBM 股票交易员云原生应用
在整本书中,我们将使用一个名为IBM 股票交易员的示例应用来演示各种概念和技术。这个开源示例旨在向人们展示如何开发、部署和使用一个由各种微服务组成且利用各种外部服务(如数据库、消息系统和服务)的典型云原生应用。所有微服务都是容器化的,并通过操作员部署到 Kubernetes 集群,例如OpenShift 容器平台。
如其名所示,IBM 股票交易员示例存在于金融领域,模拟了一个跟踪客户在其投资组合中购买的股票的经纪应用。虽然它实际上不购买或出售任何东西,但它确实会查找指定股票的当前实际价格,并计算映射到客户忠诚度级别的整体投资组合价值。它还模拟了一个账户余额,从中扣除每次交易的佣金,并跟踪每个投资组合的投资回报率(ROI)。此外,它还包括一些可选部分,例如在新忠诚度级别达到时发送通知,并分析提交的反馈以查看是否应授予免费(无佣金)交易,展示了它如何与 Slack、Twitter 或国际商业机器公司(IBM)的 Watson 等现实世界系统交互。
在接下来的章节中,我们将讨论每个 MicroProfile 4.x 技术时,会回过头来看这个示例是如何演示每个技术的使用。我们将包括来自构成示例的各种微服务的代码片段,解释应用程序从使用每个 MicroProfile 技术中获得的益处。
本章将涵盖以下主要内容:
-
IBM 股票交易员应用的概述
-
强制性微服务和外部服务
-
可选微服务和外部服务
到本章结束时,你将熟悉该应用的应用方式、如何使用它、各个部分如何组合成一个复合应用,以及如果你时间紧迫可以忽略哪些部分。
IBM 股票交易员应用的概述
在过去 3-4 年中创建和改进的这种多语言示例演示了如何创建容器化微服务,针对各种应用程序运行时。大部分微服务被故意保持简单,以免读者陷入可能存在于真实经纪应用中的深奥技术复杂性。尽管如此,它非常旨在比云原生编程初学者文档中经常展示的各种Hello World级别示例具有更显著的教育意义。
该示例由大约一打微服务组成,这些微服务与大约一打外部依赖项(其中大部分是可选的)进行交互。此外,还有一个 Helm 图表和一个 OpenShift 操作符(它封装了 Helm 图表),用于部署示例,这将在第九章,“部署和第二天操作”中介绍。
在本节中,我们将提供一个关于应用程序、构成它的微服务以及它们所做的高层次概述。让我们首先看看用户界面(UIs)。
UIs
在深入探讨在云中运行的全部后端微服务之前,让我们看看作为客户端提供给你在网页浏览器中使用的有哪些。实际上,对于这个示例,有几种图形用户界面(GUI)客户端可供选择。有一个简单的基于 Java servlet / JSP 的 UI 称为Trader,它故意使用非常简单的超文本标记语言(HTML)来渲染结果,以便 servlet 代码易于理解。让我们在下面的屏幕截图中看看这个简单的客户端:

图 3.1 – 简单基于 Java servlet 的 UI:Trader
如您所见,这个客户端提供了一个投资组合列表,并允许您查看一个投资组合的详细信息、修改一个、创建一个新的或删除一个。您必须成功登录才能使用客户端,并且可以选择提交反馈,这可能导致免费(无佣金)交易。它将显示您的当前忠诚度级别、您的账户余额和您的投资回报率。
此外,还有一个更华丽的用户界面称为Tradr,它使用Vue.js UI 框架编写,并提供了一个更现代的体验;这需要在您的浏览器中启用 JavaScript。让我们也看看这个——您可以在下面的屏幕截图中看到它:

图 3.2 – 奢华的基于 Node.js 的 UI:Tradr
这两个客户端具有相同的功能。更华丽的那个在阅读其代码时理解起来稍微复杂一些,但它提供了一个看起来更加专业、响应式的体验。Trader 看起来像是 20 世纪末编写的,而 Tradr 看起来像是现代编写的。
此外,还有一个名为loopctl的命令行客户端,它运行指定次数的迭代(在并行线程上)的操作,可以对投资组合进行性能和吞吐量测试,如下所示:
sh-4.4$ ./loopctl.sh 1 1
1: GET /broker
[{"owner": "Raunak", "total": 1,160,209.07, "loyalty": "PLATINUM", "balance": -89.82, "commissions": 139.82, "free": 0, "nextCommission": 5.99, "sentiment": "Unknown", "stocks": {}}, {"owner": "Karri", "total": 10,413.06, "loyalty": "BRONZE", "balance": 31.02, "commissions": 18.98, "free": 0, "nextCommission": 8.99, "sentiment": "Unknown", "stocks": {}}, {"owner": "Alex", "total": 12,049.00, "loyalty": "BRONZE", "balance": 41.01, "commissions": 8.99, "free": 0, "nextCommission": 8.99, "sentiment": "Unknown", "stocks": {}}, {"owner": "John", "total": 79,544.03, "loyalty": "SILVER", "balance": 16.04, "commissions": 33.96, "free": 0, "nextCommission": 7.99, "sentiment": "Unknown", "stocks": {}}, {"owner": "Eric", "total": 120,835.00, "loyalty": "GOLD", "balance": 43.01, "commissions": 6.99, "free": 0, "nextCommission": 6.99, "sentiment": "Unknown", "stocks": {}}, {"owner": "Charlie", "total": 3,004,905.16, "loyalty": "PLATINUM", "balance": 34.02, "commissions": 15.98, "free": 0, "nextCommission": 5.99, "sentiment": "Unknown", "stocks": {}}]
2: POST /broker/Looper1
{"owner": "Looper1", "total": 0.00, "loyalty": "Basic", "balance": 50.00, "commissions": 0.00, "free": 0, "nextCommission": 9.99, "sentiment": "Unknown", "stocks": {}}
3: PUT /broker/Looper1?symbol=IBM&shares=1
{"owner": "Looper1", "total": 127.61, "loyalty": "Basic", "balance": 40.01, "commissions": 9.99, "free": 0, "nextCommission": 9.99, "sentiment": "Unknown", "stocks": {"IBM": {"symbol": "IBM", "shares": 1, "price": 127.61, "date": "2021-03-12", "total": 127.61, "commission": 9.99}}}
4: PUT /broker/Looper1?symbol=AAPL&shares=2
{"owner": "Looper1", "total": 369.67, "loyalty": "Basic", "balance": 30.02, "commissions": 19.98, "free": 0, "nextCommission": 9.99, "sentiment": "Unknown", "stocks": {"AAPL": {"symbol": "AAPL", "shares": 2, "price": 121.03, "date": "2021-03-12", "total": 242.06, "commission": 9.99}{"IBM": {"symbol": "IBM", "shares": 1, "price": 127.61, "date": "2021-03-12", "total": 127.61, "commission": 9.99}}}
为了简洁起见,前一个输出中只显示了每个迭代中的前 4 个步骤。简而言之,它创建一个新的投资组合,在其中买卖股票,然后删除它,并且根据您的请求,在尽可能多的并行线程上重复这些步骤,并报告时间。
无论您使用哪三个客户端,它们都会向同一个经纪人微服务发出表示性状态转移(REST)调用,而该微服务随后根据需要与其他微服务进行交互,正如我们将在下一节中看到的。
架构图
让我们看看一个展示所有组件如何组合在一起的图。一开始可能会觉得有点令人不知所措,但根据《银河系漫游指南》的建议,“不要慌张”!在下面的图中,您看到的多数微服务和依赖项都是可选的:

图 3.3 – 架构图
对于图 3.3中显示的每个实色框,都有一个位于github.com/IBMStockTrader的 GitHub 仓库。根据标准的 GitHub 命名约定,每个微服务名称被转换为全部小写,多词名称中的单词之间用连字符分隔;例如,股票报价微服务可在github.com/IBMStockTrader/stock-quote找到。
对于这些微服务,每个都有一个位于hub.docker.com/u/ibmstocktrader的 Docker Hub 仓库。当然,您可以从 GitHub(我们将在第八章,*构建和测试您的云原生应用程序)中的源代码构建每个微服务,并将镜像推送到您想要的任何镜像仓库,例如集成到您的 OpenShift 集群中的那个。但为了更容易部署示例,也提供了预构建的镜像。如果您使用操作员部署示例,它将默认从 Docker Hub 拉取镜像,但您可以替换每个微服务的默认镜像和标签字段,以从任何镜像仓库拉取。
在以下各节中,我们将查看应用程序中的每个微服务及其依赖项。
必需的微服务和外部服务
如前所述,本例的核心部分仅仅是创建投资组合和买卖股票的基本操作所需的部分。这些部分的示例都有坚固的边框,如图图 3.3所示。
以下小节将描述构成IBM 股票交易员应用程序主要功能(即创建投资组合和在其中买卖股票的能力)所需的每个微服务及其依赖项。
交易员
交易员是本例的标准 UI 客户端。如前文图 3.1所示,它展示了一个现有投资组合列表,允许您创建新的投资组合,更新现有的投资组合(通过买卖股票),以及删除投资组合。它通过 REST 服务调用与经纪人微服务进行通信,传递用于单点登录(SSO)的JavaScript 对象表示法(JSON)Web 令牌(JWT)。
它通过一组简单的 Java servlets 和 JSP 实现,就像大多数股票交易员微服务一样,在开源 Open Liberty 应用程序服务器上运行,该服务器在通用基础镜像(UBI)上运行,这是一个Red Hat Enterprise Linux(RHEL) 8.4 容器,带有 Open J9 Java 11 虚拟机(VM)。
对于此客户端如何执行认证,有一些选择。默认且最简单的方式是在交易微服务的server.xml文件中的basicRegistry部分定义的硬编码凭据列表中进行登录,例如stock/trader作为标识符(ID)/密码。
LDAP
您还可以选择使用交易微服务对公司的轻量级目录访问协议(LDAP)服务器进行登录。这样,您的员工可以使用他们的公司用户注册信息进行登录,例如使用序列号或电子邮件地址。请注意,如果您将示例部署到公共云中的 OpenShift 集群,并且您的用户注册服务器在防火墙后面的本地数据中心运行,那么您需要设置一个虚拟专用网络(VPN)连接回该 LDAP 服务器。
OIDC
使用交易微服务的最终认证选项是使用OpenID Connect(OIDC)服务器进行登录。如果您想通过互联网上的第三方提供者进行认证,例如通过 Facebook、Twitter 或 GitHub 凭据登录,通常会使用此选项。出于测试目的,您还可以在 OpenShift 集群中本地部署自己的 OIDC 服务器,例如使用 OperatorHub 中的Red Hat SSO(RH-SSO)操作符,该操作符基于开源的Keycloak项目。
经纪人
架构师通常建议使用模型-视图-控制器(MVC)架构的多层应用程序。在股票交易员示例中,JSON 是模型,交易员(或可选的 Tradr 或 Looper)是视图,而经纪人微服务充当控制器。
这是一个无状态的微服务,就像本例中的大多数微服务一样,通过Jakarta RESTful Web Services(JAX-RS)暴露 REST 接口。它协调对各种其他微服务的调用,例如投资组合以及可选的账户和交易历史。它不直接依赖于任何外部服务。
投资组合
此微服务负责处理特定投资组合的所有股票相关操作。它联系股票报价微服务以获取所需股票的当前价格。
从概念上讲,它是一个有状态的微服务;然而,它不在内存中维护任何状态。相反,它连接到关系数据库以持久化和访问其数据。该数据库可以运行在您的 OpenShift 集群本地,或在云中,或在本地数据中心(如果是这样,则需要 VPN 连接才能访问)。微服务使用 Java 数据库连接(JDBC)与数据库交互,并可选择使用 Kafka 向一个主题发布消息。
JDBC 数据库
Portfolio 和 Stock。Portfolio 表中有一行对应于客户端表中看到的每一行。对于每支购买的股票,Stock 表中也有一个行。Stock 表有一个外键回指 Portfolio 表,并且在该关系上有一个 级联删除 规则,如果删除该投资组合,将删除该投资组合的所有股票。
由于此示例是由 IBM 员工创建的,并且经常被用来演示如何将云原生应用程序连接到各种 IBM 产品,因此通常需要更新 IBM 的 server.xml 文件和 Dockerfile(将 JDBC Java 归档(JAR)文件复制到容器中),以便选择不同的关系数据库供应商。
下面是 Portfolio 在 IBM 云中托管的 Db2-as-a-Service 数据库所使用的资源的图形视图:

图 3.4 – Portfolio 所使用的 IBM Db2 云数据库的详细信息
在前面的屏幕截图中,您可以查看包含已购买股票详细信息的表;OWNER 列是一个外键,回指包含股票的投资组合。
Kafka
Portfolio 还有一个可选的依赖项 Kafka。如果配置了,每当股票交易时,Portfolio 将向 Kafka 主题发布一条消息。可选的 Trade History 微服务将订阅此主题,使用 MicroProfile Reactive Messaging(我们将在 第十章,反应式云原生应用程序)进行讨论),并对消息采取行动。
通常,选择作为 Kafka 提供商的是来自 IBM 云集成包 的 IBM Event Streams 产品。然而,如果需要,也可以使用其他提供商,例如来自 Red Hat 的 AMQ Streams。
股票报价
这是示例中最简单的微服务。它仅仅调用云中的 REST 应用程序编程接口(API),返回指定股票的当前价格(报价比这个免费服务晚 15 分钟;更实时的报价需要付费)。还有一个可选的缓存服务,可以用来快速返回相同股票代码(在可配置的默认为 1 小时的配置期间内)的调用,而无需再次调用互联网。
注意,这是唯一配置在 Red Hat 的Quarkus应用程序框架上运行的微服务。所有其他基于 Java 的微服务都在 Open Liberty 上运行。无论是哪种方式,Java 代码都是相同的;唯一的区别在于它的构建和配置方式以及它运行的起始点 Docker 容器。
API Connect
股票报价调用的 REST API 是在API Connect(IBM Cloud Pak for Integration 的一部分)中实现的。你可以简单地接受默认设置,它将使用预先配置的实例,其中所有设置都已就绪。此 API 面向IEX Cloud中的免费服务,该服务以 15 分钟的延迟返回股票价格(获取更实时的价格需要付费)。有关如何在 API Connect 的自己的实例中设置此 API 的信息,请参阅medium.com/cloud-engagement-hub/introducing-api-connect-a0218c906ce4。
注意
此 API 曾经使用来自 Quandl 的不同免费股票价格服务,但该服务已下线;好事是,股票交易示例中的任何内容都不需要更改——这只是 API Connect 实现的一个更新,它仍然提供了相同的操作签名。
Redis
股票报价使用Redis作为其可选缓存服务。如果没有提供,那么每次调用都会导致访问互联网以获取股票价格。通过在 Redis 中缓存每只股票的价格,这意味着你可以将股票报价微服务扩展到任意数量的 Pod,并确保无论每次路由到哪个 Pod,你都会得到一致的答案。这也意味着在真正的无服务器风格中(其中不经常使用的东西会被停止以节省金钱,并在新请求到达时即时(JIT)重新启动),当不需要时,你可以将 Pod 扩展到零,并确保当你扩展回以处理新请求时,你仍然可以受益于之前缓存的资料。
可选微服务和外部服务
示例中还有一些可选部分,你只有在想要某些额外功能(例如,当你从银色升级到金色时发送推文)时才会设置。这些部分在架构图中带有虚线边框。
大多数人在设置示例时都会跳过许多(有时甚至全部)以下部分,以追求简单性。但每个部分都展示了如何以云原生的方式执行一些额外操作,因此它们作为如何利用额外的Java 企业版(EE)/Jakarta EE和MicroProfile技术的良好示例。
在本节中,我们将查看这些可选微服务和它们的依赖关系。首先是我们在之前看到的备用 UI。
Tradr
更吸引人的用户界面称为 Tradr。它的源代码(示例中唯一的非 Java 微服务)读起来稍微复杂一些,但它提供了一个更加现代、响应式的界面,这是如今专业网站普遍期望的。它调用来自经纪人微服务的相同 REST 服务——只是以更吸引人的方式呈现结果。
注意,虽然 Trader 提供了认证方法的选项,默认方法是一个非常简单的无需额外设置的方法,但 Tradr 客户端需要使用 OIDC。这意味着您必须进行额外设置,要么在自己的 OpenShift 集群中建立自己的 OIDC 服务器,要么调整外部 OIDC 服务器的配置(这通常需要注册一个回调统一资源定位符(URL),指向 Tradr 的 OpenShift 路由)。
账户
这个可选的微服务负责处理与投资组合相关的事务,这些事务超出了它持有的股票列表。这包括忠诚度等级、账户余额、支付的佣金、所有者的情绪以及他们赚取的任何免费交易。如果这个微服务未配置,这些字段将仅显示未知(对于字符串)或-1(对于数字)。
尽管投资组合微服务选择使用老式的结构化查询语言(SQL)数据库,但这个示例展示了使用更现代的 NoSQL 数据库来存储每个 JSON 文档:IBM Cloudant(来自 IBM Cloud Pak for Data)。
Cloudant
注意,虽然投资组合微服务必须进行对象到关系映射(例如,将 Portfolio 和 Stock 之间的一对多包含关系转换为外键,并具有级联删除规则),但对于账户微服务来说这不是必要的。这个基于 JAX-RS 的微服务上每个 REST 操作返回的精确 JSON 就是存储在 Cloudant 数据库中的内容,正如我们在这里可以看到的:
![图 3.5 – 存储在 IBM Cloudant 中的一个示例账户文档
![img/Figure_1.5_B17377.jpg]
图 3.5 – 存储在 IBM Cloudant 中的一个示例账户文档
在前面的屏幕截图中,我们可以看到账户管理的数据,包括忠诚度等级和账户余额。请注意,_id 和 _rev 字段是由 Cloudant 本身添加的,用于管理如何找到特定文档(_id)以及文档的修订版本(_rev)。
ODM
与在 Java 中硬编码确定忠诚度等级的业务规则不同,这个微服务将这个规则外部化到一个业务规则引擎中。这使得我们可以通过仪表板实时调整阈值,例如,投资组合的总价值必须达到多高才能实现金级状态,而无需更改和重新部署微服务。
示例使用 IBM 的操作决策管理器(ODM),来自 IBM 自动化云包,作为其业务规则引擎。在账户 Git 仓库中有一个规则集 ZIP 文件(github.com/IBMStockTrader/account/blob/master/stock-trader-loyalty-decision-service.zip),你可以将其导入决策中心 UI 并部署到决策服务器。如果 ODM 没有配置,忠诚度级别将永远保持在起始值。
让我们看一下以下截图中的决策中心UI:
![Figure 3.6 – The IBM ODM Decision Center UI, showing our decision table
![img/Figure_1.6_B17377.jpg]
图 3.6 – IBM ODM 决策中心 UI,显示我们的决策表
在这里,我们可以看到决策表,显示了各种阈值。例如,一旦你的投资组合总价值超过美元(USD)50,000,其忠诚度级别将从青铜变为银。
雅加达消息
当忠诚度级别发生变化(这意味着你已经配置了 ODM 并且购买了足够的股票——例如,默认阈值,如你在图 3.6中看到的,是 10 万美元以达到金级别)时,账户微服务将向一个雅加达消息队列发送消息。有下游微服务会对此消息做出反应。
通常,IBM 的server.xml文件和Dockerfile中的一个行,用于将雅加达消息.rar文件复制到容器中。
Watson Tone Analyzer
通常,无论何时买卖股票,都会从你的账户余额中扣除佣金。然而,通过使用sentiment将确定并返回,你可以赚取免费(零佣金)交易。提交大多数类型的反馈,你将获得一次免费交易,但有一条规则,如果它确定你的情绪是愤怒,将给你三次免费交易以安慰你。
如果你没有配置 Watson Tone Analyzer,你将得到未知并且没有免费交易。
交易历史
这个微服务记录了你所进行的每一次交易,交易时间以及你交易时的股票价格。没有这个微服务,示例只能知道汇总信息。例如,如果你一个月前以 100 美元的价格购买了 10 股 IBM 股票,一周前以 110 美元的价格购买了 5 股,今天又以 120 美元的价格购买了 2 股,投资组合微服务只会知道你现在有 17 股,以及它们现在的价值(在这个例子中是 2040 美元)。交易历史微服务记得所有细节,所以它会知道你花费了 1790 美元,因此投资回报率为 14%。如果这个微服务没有配置,交易员和 Tradr 客户端将只会说未知作为投资回报率。
如同在投资组合微服务的讨论中提到的,这个微服务订阅并消费了投资组合发布到 Kafka 主题的消息,例如由IBM 事件流管理。它是通过MicroProfile 反应式消息来做到这一点的。
Mongo
这个微服务使用Ready状态,因此永远不会从主题中消费消息。
消息
这个微服务消费来自 Account 微服务的关于忠诚度级别变化的 JMS 队列中的 JSON 消息。这个微服务在本例中只有一个 Jakarta 企业 Bean,使用消息驱动 Bean(MDB)接收消息。然后通过Notification微服务通知您达到的新级别。与 Account 微服务一样,这个微服务也需要一个 Jakarta Messaging 提供者,如 IBM MQ,如下面的屏幕截图所示:

图 3.7 – IBM MQ UI,显示由 Account 微服务发送的消息
这里,我们看到一个简单的 JSON 消息在 MQ 队列上,该消息由 Messaging 微服务处理以提供通知——在这种情况下,关于Emily将忠诚度级别从银色提升到金色的通知。
通知
Notification 微服务有两种不同的版本——一个发送推文,另一个发布到 Slack 频道。两者都有相同的 REST 接口,因此您只需在部署示例时选择您想要使用的版本即可。
Notification-Twitter
Notification-Twitter版本使用来自twitter4j.org的开源库与 Twitter 的 REST API 交互以发送推文。
与本例中的大多数微服务在 Open Liberty 上运行不同,这个微服务在 Docker 容器中运行传统 WebSphere 应用服务器(tWAS)。
关于如何设置此信息的更多信息,请参阅medium.com/cloud-engagement-hub/experiences-using-the-twas-docker-container-557a9b044370。
要配置示例以通过您的账户发送推文,您需要获取您的 Twitter 账户的开放授权(OAuth)凭证。您需要消费者密钥和消费者密钥,以及访问令牌和访问令牌密钥。以下是从 Notification-Twitter 发送的示例推文:

图 3.8 – 由 Notification-Twitter 发送的示例推文
在图 3.8中,您可以看到当忠诚度级别从青铜提升到金色时,由@IBMStockTrader账户发送的推文。
Notification-Slack
这种 Notification 微服务会将消息发布到 Slack 频道。与 Twitter 版本一样,每当处理关于忠诚度级别变化的 JMS 消息时,它都会这样做。微服务调用一个无服务器函数,将实际帖子发送到 Slack,正如我们在这里看到的:

图 3.9 – 由 Notification-Slack 发送的示例 Slack 消息
如您所见,消息与发送到 Twitter 的消息非常相似,但在这个案例中,它是发送到 Slack 频道的。
无服务器函数是通过Apache OpenWhisk框架实现的。您可以将 OpenWhisk 部署到您的 OpenShift 集群或使用 IBM 的函数即服务(FaaS)称为IBM Cloud Functions。有关创建将消息发布到 Slack 频道的操作序列的详细信息,请参阅medium.com/cloud-engagement-hub/serverless-computing-and-apache-openwhisk-164676af8972。
注意
此外,还有一个亚马逊网络服务(AWS)Lambda 无服务器函数,Notification-Slack 可以调用它,将消息发布到 Slack 频道。它期望与 OpenWhisk 函数相同的 API 定义,因此无需对 Notification-Slack 微服务进行任何更改——您只需通过操作员配置不同的 URL 和凭据即可。无论您选择 IBM Cloud Functions 还是 AWS Lambda,这都表明 Kubernetes 和无服务器框架可以和谐地协同工作。
收集器
此微服务从其他微服务接收证据,将其持久化到 IBM Cloudant,并使其可供安全/合规工具定期抓取,例如Trader可以配置为发送有关登录尝试的证据,而Stock Quote可以配置为发送有关缓存命中的证据(在 Redis 中)。它通过 REST(一个POST请求)接收证据,并通过 REST(一个GET请求)公开证据。
与其他微服务不同,这个微服务不使用 /collector 端点。这类似于 /metrics 端点。
Looper
在此示例中,如果未指定,最后一个可选的微服务在其路由 URL 上称为1,您可以指示它运行一系列操作的指定迭代次数,这些操作展示了在 Broker 微服务上可用的所有创建、检索、更新和删除(CRUD)操作。
例如,此示例的操作员(将在第九章中详细描述,部署和第 2 天操作)有一个复选框来设置水平 Pod 自动缩放器(HPA),如果微服务达到某些中央处理器(CPU)阈值,则将其扩展到额外的 Pod(当 CPU 使用率下降时将缩减)。通过使用 Looper 对示例进行负载测试,您可以看到 HPA 的实际操作,并且您可以看到 OpenShift 控制台中的资源使用图显示了活动。
loopctl
请求Looper servlet 的大量迭代次数的一个问题是,您在它们全部完成之前看不到任何输出。实际上,大多数浏览器默认情况下,如果请求返回所需的时间不合理,将会超时。
为了解决这个问题,有一个名为loopctl的 Looper servlet 命令行客户端,它循环调用Looper servlet。
你可以告诉它在一个指定的并行线程数上运行指定次数的迭代。你之前已经看到了这个命令行客户端的输出。要自己运行它,请求在 4 个并行线程上运行 25 次迭代,最简单的方法是进入./loopctl.sh 25 4,这将运行总共 100 次迭代(临时创建名为Looper1、Looper2、Looper3和Looper4的投资组合),并将输出每次迭代的平均毫秒数。
摘要
现在,你应该对本书中将使用的云原生示例有了感觉。尽管一开始可能看起来有些令人畏惧,但强制性的部分设置起来相当简单(尤其是如果你使用 Docker Hub 中的预构建镜像),所以你可以在几分钟内启动示例的基本功能。然后,你可以根据自己的节奏添加任何你想要的可选功能。
在即将到来的章节中,将详细讨论各种 MicroProfile 技术。每个都将展示这个示例中特定微服务的片段。正如你所见,不同的微服务旨在展示 Jakarta EE 和 MicroProfile 的不同功能,并提供一个真实运行的教程,说明如何与各种外部服务集成。
在第八章,构建和测试你的云原生应用程序中,我们将检查这些微服务是如何开发的,以便你可以学习如何自己开发这样的微服务。在第第九章,逐步股票交易者开发中,我们将详细介绍示例的部署,以及你可以执行的第二天操作。
在下一章中,我们将开始查看每个 MicroProfile 技术本身,并了解它们如何帮助 Java 开发者创建可以在公共或私有云中运行并深度集成的云原生应用程序。
第二部分:MicroProfile 4.1 深入探讨
在本节中,你将了解有关 MicroProfile 的所有内容。这包括开发和使用 RESTful 微服务,然后通过添加配置选项、容错性等功能来增强它们。你还将学习如何使用 MicroProfile 技术来观察和调试你的应用。最后,你将了解应用的云部署选项。
本部分包括以下章节:
-
第四章, 开发云原生应用
-
第五章, 增强云原生应用
-
第六章, 观察和监控云原生应用
-
第七章, 使用 Open Liberty、Docker 和 Kubernetes 的 MicroProfile 生态系统
第四章:开发云原生应用程序
MicroProfile 4.1 基于某些 Jakarta EE(以前称为 Java Enterprise Edition)应用程序编程接口(API)——具体来说,是 Jakarta RESTful Web Services(JAX-RS)、Jakarta Contexts and Dependency Injection(CDI)、JavaScript Object Notation Processing(JSON-P)和 JSON Binding(JSON-B)。仅使用这些技术,就可以开发一个完全能够胜任的云原生应用程序。MicroProfile 社区增加了一个用于调用 RESTful 服务的类型安全机制,称为 MicroProfile Rest Client。这些技术对于构建可以在云中互操作的基于 Java 的微服务至关重要。学习使用这些技术将使您能够构建健壮且安全的 Java 微服务。
在本章中,我们将探讨以下所有主题:
-
使用 JAX-RS 开发 RESTful 服务
-
使用 JSON-P 和 JSON-B 管理有效载荷
-
使用 MicroProfile Rest Client 消费 RESTful 服务
-
使用 CDI 管理生命周期和 依赖注入(DI)
本章涵盖了广泛的技术以及每个技术中的功能。当您完成本章后,您应该对如何构建可靠、健壮的 RESTful 应用程序,并使用 JSON 互相通信有广泛而深入的理解。
技术要求
为了构建和运行本章中提到的示例,您需要一个装有以下软件的 Mac 或 PC(Windows 或 Linux):
-
Java 开发工具包(JDK)版本 8 或更高 (
ibm.biz/GetSemeru) -
Apache Maven (
maven.apache.org/) -
Git 客户端 (
git-scm.com/) -
本章中使用的所有源代码均可在 GitHub 上找到,网址为
github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/tree/main/Chapter04。
一旦您已经克隆了 GitHub 仓库,您可以通过进入 Chapter04 目录并在命令行中运行以下命令来启动 Open Liberty 服务器,这些代码示例将在其中执行:
mvn clean package liberty:run
您可以通过在相同的命令窗口中按 Ctrl + C 来停止服务器。
现在我们已经处理好了先决条件,让我们先构建一个基本的 RESTful 服务。
使用 JAX-RS 开发 RESTful 服务
在本节中,我们将使用 JAX-RS 开发几个 RESTful 服务。我们将从一个简单的例子开始,然后添加更复杂和强大的技术,如异常处理、将 超文本传输协议(HTTP)数据高级转换为 Java 对象(反之亦然)、横切关注点、异步方法和 DI。
JAX-RS 是围绕 请求-响应 管道这一理念构建的。在服务器端,一个 HTTP 请求进入管道,然后 JAX-RS 服务器在请求上调用任何预匹配的 过滤器。然后它尝试将请求与 JAX-RS 资源方法 匹配。
当 JAX-RS 容器收到一个传入请求时,它将执行以下过程:
-
调用任何已注册的预匹配过滤器。
-
尝试将请求与
resource方法匹配。如果无法匹配,容器将响应适当的not foundHTTP 响应。 -
调用任何已注册的匹配后过滤器。
-
如果需要,执行 HTTP 数据(如 HTTP 实体有效负载或参数、头等)到资源方法可消费的 Java 对象的转换。
-
调用
resource方法。 -
如果需要,执行异常处理。
-
调用任何已注册的响应过滤器。
-
如果需要,执行 Java 对象到 HTTP 响应数据的转换。
-
将 HTTP 响应返回给客户端。
以下图表展示了该管道流程:

图 4.1 – JAX-RS 服务器管道流程
在 JAX-RS 中,有三种类型的组件,如下概述:
-
资源:资源是使 RESTful 服务成为其自身的最终因素,因为它们包含业务逻辑。
-
MessageBodyReader、MessageBodyWriter、ParamConverter、ExceptionMapper、ReaderInterceptor和WriterInterceptor都是提供者。 -
使用
Application子类来为 JAX-RS 应用程序提供配置。
现在我们已经了解了基本流程,让我们创建一个简单的 JAX-RS 应用程序。
Hello World!
一个 JAX-RS 应用程序必须至少包含一个资源类。提供者可选。只有在没有指定应用程序路径的 web.xml 文件时,才需要 Application 子类。因此,一个非常简单的应用程序可能看起来像这样:
@ApplicationPath("/rest")
public class HelloWorldApp extends Application {}
@Path("/hello")
public class HelloWorldResource {
@GET
public String helloWorld() {
return "Hello World!";
}
}
如果我们将此代码构建成一个名为 myApp.war 的 Web 应用程序,并将其部署到 Open Liberty 等 JAX-RS 容器中,我们可以通过浏览到 http://localhost:9080/myApp/rest/hello 来快速使用 HTTP 客户端测试它,我们会看到文本 Hello World!。
这之所以有效,是因为 helloWorld() 方法上的 @GET 注解告诉 JAX-RS 容器,当客户端向 /rest 应用程序路径下的 .war 扩展名的 /hello 路径发出 GET 请求时,应该调用此方法。默认情况下,大多数 HTTP 客户端(浏览器、curl 等)使用 GET,除非指定其他方式。这提出了关于工具的一个很好的观点。在开发 RESTful 应用程序时,拥有一个可以发出不同类型 HTTP 请求的客户端工具非常有价值。命令行工具如 curl 非常有用,还有基于浏览器扩展的工具也可以使用。
一个更贴近现实世界的例子
现在我们已经构建了一个简单的 JAX-RS 应用程序,让我们构建一个更复杂的应用程序——一个同义词服务,客户端可以搜索和更新同义词。我们将从一个 异常映射器 开始,如下所示:
@Provider
public class NoSuchWordExceptionMapper implements
ExceptionMapper<NoSuchWordException> {
@Override
public Response toResponse(NoSuchWordException ex) {
return Response.status(404) .entity(ex.getMessage()).build();
}
}
大多数应用程序都会遇到 NoSuchWordException 异常,该异常可以用来指示搜索的单词不存在。在应用程序中,有人指定了一个不存在的单词是明确的,但对于 HTTP 客户端来说则不是。NoSuchWordExceptionMapper 提供器类使得这一点成为可能。它使得资源类方法能够抛出 NoSuchWordException 异常,JAX-RS 容器会将该异常映射到一个 HTTP 响应(在这种情况下,是一个 404 Not Found 错误)。
接下来是资源类(完整的源代码可在 github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/thesaurus/ThesaurusResource.java 找到)的示例,如下代码片段所示:
@Path("/thesaurus/{word}")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public class ThesaurusResource { // ...
资源类上有几个新的注解:@Produces 和 @Consumes。这些注解可以放置在资源类或方法上——与 JAX-RS 中此类注解的大多数注解一样,方法上的注解优先于类上的注解。这些注解有助于控制请求的匹配以及用于从请求反序列化 HTTP 实体或响应中序列化 HTTP 实体的实体提供者(MessageBodyReaders 和 MessageBodyWriters)。
HTTP 请求和响应可能包含一个表示 Content-Type 的头信息。HTTP 请求也可能包含一个头信息,指定它期望在响应中接收的媒体类型(或多个媒体类型)——Accept。如果没有这些头信息,则允许所有媒体类型(用 */* 表示)。
在前面的例子中,资源类指定了 MediaType.TEXT_PLAIN 或 text/plain。其他媒体类型包括 text/html、application/json、application/xml、image/jpeg 以及更多。指定 text/plain 将会阻止资源方法在请求包含如 Content-Type: application/pdf 或 Accept: image/png 等头信息时被调用——在这种情况下,JAX-RS 容器会返回一个 415 Unsupported Media Type 错误。
最佳实践
总是使用 @Produces 和 @Consumes 来限制媒体类型。这将限制你的服务将响应的类型。这将确保你的应用程序(如果经过适当测试)可以处理指定媒体类型的请求。
此示例还介绍了新的方法级 HTTP 动词注解:@POST、@PUT、@DELETE 和 @PATCH。与 @GET 一样,这些注解指定应根据 HTTP 请求的 @HEAD 和 @OPTIONS(较少使用)调用哪个方法。
特别注意
如果资源类包含一个带有 @GET 注解但没有 @HEAD 注解的方法,JAX-RS 容器将调用 @GET 方法来匹配 HTTP HEAD 请求,但会移除实体。同样,如果资源类包含除 @OPTIONS 之外的其他 HTTP 动词注解,JAX-RS 容器将返回一个响应,指示可以匹配该请求的所有有效动词。使用前面的示例,OPTIONS 请求将导致一个包含类似 Allow: DELETE, HEAD, GET, OPTIONS, PATCH, POST, PUT 的响应头。
此示例还介绍了 HTTP 参数的概念——特别是 @PathParam("word") String word;。
这个注解可以放置在字段或方法参数上。@PathParm 的值是 word,它对应资源类 @Path 值中的模板变量("/thesaurus/{word}")。这意味着对于像 http://localhost:9080/myApp/rest/thesaurus/funny 这样的 HTTP 请求,注入到 word 字段的值将是 funny。
在 JAX-RS 中可以使用其他 HTTP 参数类型,包括 @QueryParam、@FormParam、@CookieParam、@HeaderParam 和 @MatrixParam,它们都对应 HTTP 请求的不同部分。JAX-RS 还允许在单个 Java 类上聚合多个 HTTP 参数注解,然后在资源类或方法中作为 @BeanParam 参数类型引用。以下是一个示例:
public class ParamBean {
private int id;
@QueryParam("id")
public void setId(int id) {
this.id = id;
}
@HeaderParam("X-SomeHeader")
public String someHeaderValue;
@PathParam("path")
public String pathParamValue;
@Override
public String toString() {
return "ID: " + id + " X-SomeHeader: " + someHeaderValue + " path: " + pathParamValue;
}
}
ParamBean 实体只是一个 @*Param 注解。然后,这个 POJO 被注入到一个资源中,如下所示:
@GET
@Path("/beanparam/{path}")
public Response get(@BeanParam ParamBean params) {
return Response.ok(params.toString()).build();
}
@BeanParam 可以非常有助于聚合常见的 RESTful 参数集合,以避免编写重复的代码。让我们使用 curl 从命令行测试此示例,如下所示:
$ curl "http://localhost:9080/rest/beanparam/ myPath?id=1234" -H "X-SomeHeader: MyHeaderValue"
ID: 1234 X-SomeHeader: MyHeaderValue path: myPath
有一个需要注意的事情是,并非所有参数都不会为空,因此您需要检查空值,或者可以使用 @DefaultValue 注解。这也适用于方法参数。这里提供了一个示例:
@GET public String get(@QueryParam("startPage")
@DefaultValue("1") Integer startPage) { // ...
注意,@DefaultValue 注解中的值始终是字符串,但只要它可以从字符串转换为参数类型(在这种情况下是 Integer),它就会工作。在下一节中,我们将学习如何将客户端发送的数据转换为我们的应用程序代码中的 Java 对象。
实体提供者和参数转换器
到目前为止,我们的资源方法主要处理字符串或其他原始数据。JAX-RS 容器负责序列化和反序列化这些对象,但如果我们想发送和接收更复杂的对象怎么办?在这些情况下,我们可能需要实现一些 ParamConverter。
实体提供者
实体提供者包括MessageBodyReader和MessageBodyWriter,它们分别负责将 HTTP 实体数据反序列化为 Java 对象,以及将 Java 对象序列化为 HTTP 实体。
假设我们有一个Person对象,如下所示:
public class Person {
public enum Color {
RED, BLUE, YELLOW, GREEN, ORANGE, PURPLE
}
private String firstName;
private String lastName;
private int age;
private Color favoriteColor;
//public getters/setters
}
再假设我们有一个service对象,如下所示:
@Path("/person")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PersonService {
static List<Person> people = new ArrayList<>();
@GET
@Path("/{id}")
public Person getPerson(@PathParam("id") int id) {
try {
return people.get(id);
} catch (IndexOutOfBoundsException ex) {
throw new WebApplicationException (Response.status(404).entity("ID " + id + " not found.").build());
}
}
@POST
public int postPerson(Person person) {
people.add(person);
return people.lastIndexOf(person);
}
}
这里需要注意的一点是,getPerson(…)方法抛出了一个新的WebApplicationException异常,并将其作为404响应传递。这是将异常映射到响应而不需要ExceptionMapper实例的另一种方式。
最佳实践
当多个资源方法可能抛出相同的异常时,请使用ExceptionMappers。只有在没有合适的业务异常可以抛出,或者你只从单个资源方法中抛出异常的情况下,才使用带有传入的Response对象的WebApplicationException异常。
另一点需要注意是,这个资源使用APPLICATION_JSON作为它产生的和消费的媒体类型。application/json媒体类型是微服务中最常用的内容类型。以下是从上一个示例代码中获取的Person对象:
{
"firstName": "John",
"lastName": "Doe",
"age": 33,
"favoriteColor":"RED"
}
为了使客户端能够通过POST方法创建一个新的Person对象,我们需要一个MessageBodyReader实例。有一些内置的读取器可以很好地处理这个问题,但到目前为止,我们将编写并注册自己的,如下所示(完整的源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/entityandparamproviders/MyJsonReader.java):
@Provider
@Consumes(MediaType.APPLICATION_JSON)
public class MyJsonReader implements MessageBodyReader<Person> {
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type.equals(Person.class) && mediaType.isCompatible(MediaType .APPLICATION_JSON_TYPE);
}
@Override
public Person readFrom(Class<Person> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
String s = new BufferedReader(new InputStreamReader (entityStream)).lines().collect (Collectors.joining(" ")).trim();
if (!s.startsWith("{") || !s.endsWith("}")) {
throw new WebApplicationException(Response .status(400).build());
}
Person p = new Person();
// ... parse string into Peron object ...
return p;
}
}
此外,为了将Person对象作为 JSON 写入响应实体,我们必须注册一个 JSON 的MessageBodyWriter实例,如下所示(完整的源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/entityandparamproviders/MyJsonWriter.java):
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class MyJsonWriter implements
MessageBodyWriter<Person> {
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type.equals(Person.class) && mediaType .isCompatible(MediaType.APPLICATION_JSON_TYPE);
}
@Override
public void writeTo(Person p, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
PrintStream ps = new PrintStream(entityStream);
// print Person object to entity stream
}
}
这些提供者可以很容易地合并成一个实现两个接口的 MyJsonEntityProvider 类。这两个实现都使用 InputStream 从客户端请求中读取实体,并使用 OutputStream 来写入响应实体。这两个提供者都有一个布尔检查,以验证这是否是应该调用的正确实体提供者——除了指定的泛型类型(Person)和 @Consumes/@Produces 值之外,返回 true 或 false 对于 isReadable 或 isWriteable 方法将告诉 JAX-RS 容器是否应该使用此提供者来序列化/反序列化数据。
选择多个提供者的另一个标准将是 @Priority 注解——JAX-RS 容器将选择具有最高优先级的提供者(优先级值最低——因此,@Priority(1) 将在 @Priority(2) 之前被选中)。
最终,从读取者的 readFrom 方法返回的值将被注入到资源方法的 @Context(更多内容请参阅 上下文注入 部分)或 @*Param。资源方法可能包含零个或一个实体参数——任何更多都将导致部署失败。
在响应方面,写入到写入者的 writeTo 实体流中的内容将被写入发送回客户端的 HTTP 响应。
如果此时编写和读取 JSON 的代码复杂度看起来有点令人畏惧,不要担心!我们将在下一节中介绍一个更简单的方法。
因此,实体提供者负责将 HTTP 实体序列化和反序列化为对象,但参数如查询参数、路径参数等怎么办?这些参数使用 ParamConverter 进行反序列化。
ParamConverter
在我们的 PersonService 示例的基础上,让我们添加一个 PATCH 方法,允许客户端更改一个人的最喜欢的颜色,如下所示:
@PATCH
@Path("/{id}")
public Person updateFavoriteColor(@PathParam("id") int id, @QueryParam("color") Color color) { // ...
我们可以从命令行像这样调用此方法:
$ curl http://localhost:9080/rest/person/0?color=BLUE -X PATCH
{
"firstName": "John",
"lastName": "Doe",
"age": 33,
"favoriteColor":"BLUE"
}
我们能够更新约翰最喜欢的颜色,因为 JAX-RS 容器能够识别出 Color 是一个枚举类型,因此它会调用其 valueOf(String) 方法来获取在调用 updateFavoriteColor 方法时注入的 Color 对象。但是当我们指定小写的 color 时会发生什么?让我们看一下以下输出以了解情况:
$ curl http://localhost:9080/rest/person/0?color=blue -X PATCH -v
...
< HTTP/1.1 404 Not Found
...
<
哎呀!JAX-RS 容器无法将请求与资源方法匹配(导致返回 404 Not Found 响应),因为它无法将 blue 转换为 Color.BLUE。为了使我们的服务更具弹性或处理更复杂的参数,我们必须像这样使用 ParamConverterProvider 和 ParamConverter:
@Provider
public class ColorParamConverterProvider implements ParamConverterProvider {
@Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
if (rawType.equals(Color.class)) {
return (ParamConverter<T>) new ColorParamConverter();
}
return null;
}
}
ParamConverterProvider 负责返回一个实现 ParamConverter 接口的类的实例,例如如下所示:
public class ColorParamConverter implements ParamConverter<Color> {
@Override
public Color fromString(String value) {
return Color.valueOf(value.toUpperCase());
}
@Override
public String toString(Color value) {
return value.name();
}
}
第一类,ColorParamConverterProvider是已注册的提供者类。当一个资源方法有一个需要从String转换为对象的参数时,JAX-RS 容器将调用任何已注册的ParamConverterProvider类的getContainer方法,直到其中一个返回非空的ParamConverter实例。
ColorParamConverter类简单地将字符串值转换为大写,以确保枚举的valueOf方法将返回Color.BLUE颜色,无论客户端请求的查询参数是BLUE、Blue、blue、bLuE等。
如示例所示,ParamConverter适用于@QueryParam参数,但也适用于@CookieParam、@FormParam、@HeaderParam、@MatrixParam和@PathParam参数,并且可以将字符串转换为任何对象,反之亦然。在客户端,从对象到String的转换非常重要。我们将在使用 MicroProfile Rest Client 消费 RESTful 服务这一节中讨论这个问题。
请求和响应的拦截
在某些情况下,您可能需要在MessageBodyReader实体提供者处理请求之前或之后检查输入流。同样,也可能会出现您想在MessageBodyWriter实体提供者处理输出流之前或之后执行额外处理的情况。ReaderInterceptors和WriterInterceptors就是为了这类任务而设计的。
ReaderInterceptors 和 WriterInterceptors
在我们的MessageBodyReader实体提供者中,我们进行了大量的字符串修剪调用,这在性能方面可能是昂贵的。我们可能减少此类调用的一种方法是在ReaderInterceptor提供者中从实体流中删除空白字符,这样MessageBodyReader提供者就可以始终假设流中不包含空白字符。以下是一个示例:
@Provider
public class WhiteSpaceRemovingReaderInterceptor implements ReaderInterceptor {
@Override
public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException {
InputStream originalStream = context.getInputStream();
String entity = // convert stream to string
entity = entity.replaceAll("\\s","");
context.setInputStream(new ByteArrayInputStream (entity.getBytes()));
return context.proceed();
}
}
当客户端发送包含空格、换行符或其他空白字符的多行请求实体时,您可以看到这将与系统输出一起转换,如下所示:
PRE: {
"firstName": "John",
"lastName": "Doe",
"age": 33,
"favoriteColor":"RED"
}
POST: {"firstName":"John","lastName":"Doe","age":33, "favoriteColor":"RED"}
WriterInterceptors的一个常见用途是使用 GZIP 压缩来减少响应实体的大小——请参阅github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/interceptorsandfilters/GzipWriterInterceptor.java的完整示例。
关于实体流的特殊说明
虽然你可以从多个地方读取实体流(实体提供者、读取器或写入拦截器、过滤器——我们将在稍后介绍这些),但在这样做时可能会遇到问题。例如,你的初始请求实体流可能不支持重新读取,所以如果你尝试读取两次,你可能会遇到 IOException 异常。在这种情况下,你可能需要重置流(如果流支持重置——每个 JAX-RS 容器可能略有不同)或完全复制并替换流,就像我们在 ReaderInterceptor 示例中所做的那样。
过滤器
在 ReaderInterceptors 和 WriterInterceptors 截获实体流的读取和写入时,过滤器截获整体请求和响应。过滤器在 RESTful 应用程序中启用了一些强大的横切能力。那么,我们可以用过滤器做什么呢?我们可能可以用这本书的其余部分来填充有用的示例,从管理身份验证、授权请求、重定向请求、管理头信息、在浪费服务器资源之前终止无效请求、审计日志请求/响应、检测可疑活动、提供应用程序统计信息、跟踪请求/响应、限制特定客户端的请求,等等。
让我们从以下示例开始,该示例检查传入的 API 密钥请求,如果没有找到 API 密钥、API 密钥不可识别或该 API 密钥已超过当天最大请求数量(完整源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/interceptorsandfilters/ApiKeyCheckFilter.java找到)。请查看以下代码片段:
@PreMatching
@Provider
public class ApiKeyCheckFilter implements
ContainerRequestFilter {
private final Map<String, Integer> apiInvocations = new
ConcurrentHashMap<>();
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String apiKey = requestContext.getHeaderString (API_KEY_HEADER);
if (apiKey == null) {
requestContext.abortWith(Response.status( Status.UNAUTHORIZED).build());
return;
}
// get count of recent invocations for this API key
int currentInvocations = // ...
if (currentInvocations == -1) {
requestContext.abortWith( Response.status(Status.FORBIDDEN).build());
return;
}
if (currentInvocations > MAX_REQUESTS_PER_INTERVAL) {
requestContext.abortWith( Response.status(Status.TOO_MANY_REQUESTS) .header(HttpHeaders.RETRY_AFTER, 5) .build());
return;
}
}
}
此示例检查客户端是否通过 HTTP 头发送了 API 密钥,API 密钥是否有效(通过其在映射中的存在),以及用户的密钥是否未超过其请求配额。如果发生任何这些条件,过滤器将终止请求并返回对客户端有用的响应。如果过滤器方法正常退出,则请求将继续,JAX-RS 容器将尝试将请求与资源类和方法匹配。
一旦请求与资源方法匹配,JAX-RS 容器将调用匹配后请求过滤器。这些过滤器在作为预匹配过滤器执行 ContainerRequestFilter 时很有用,但没有 @PreMatching 注解。匹配后过滤器还启用了 RequestContext 对象的更多方法,以便它知道将调用哪个资源。这在你的过滤器可能根据它将调用的资源类/方法而表现不同的场合很有用。
响应过滤器与请求过滤器类似,但在资源方法完成后被调用。然后,响应过滤器可以进一步细化或转换响应。它们可以添加或修改响应头或 cookie。它们也可以完全替换响应实体,尽管MessageBodyWriter提供者和/或WriterInterceptor提供者可能更适合这种情况。
动态提供者
到目前为止,我们讨论的所有提供者都将应用于所有请求——或者至少是所有匹配的请求——唯一的例外是实体提供者,它将应用于请求指定的所有媒体类型(s)的请求。但如果我们希望某些提供者只在特定情况下执行,例如在调用特定资源方法或请求包含特定内容,或者客户端的用户是特殊群体的一部分时,怎么办呢?JAX-RS 提供了一些不同的机制来实现更动态的提供者。首先,我们将看看名称绑定。
名称绑定允许用户在一个或多个提供者类以及一个或多个资源类或方法上放置自定义注解。JAX-RS 容器将识别这个注解,并且只有在目标资源方法或类也被注解的情况下才会调用提供者。例如,如果我们想记录某些请求,我们可能会创建一个这样的注解:
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Logged {}
@NameBinding注解是告诉 JAX-RS 容器注意这个注解的。我们现在可以创建一个记录请求方法、统一资源标识符(URI)以及请求和响应实体的过滤器(完整的源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/dynamicbinding/LoggingFilter.java找到)。看看下面的代码片段:
@Logged
@Provider
public class LoggingFilter implements
ContainerRequestFilter, ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
int requestID = idCounter.incrementAndGet();
requestContext.setProperty("request.id", requestID);
System.out.println(">>> " + requestID + " " + requestContext.getRequest().getMethod() + " " + requestContext.getUriInfo().getRequestUri() + " " + getAndReplaceEntity(requestContext));
}
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
int requestID = (int) requestContext.getProperty ("request.id");
System.out.println("<<< " + requestID + " " + requestContext.getUriInfo().getRequestUri() + " " + responseContext.getEntity());
}
//...
}
这个filter类既是请求过滤器也是响应过滤器。虽然将请求和响应过滤器(或者甚至其他提供者类型)组合在一起非常方便,但重要的是要注意,生命周期行为可能从一个 JAX-RS 容器变化到另一个容器。一般来说,被认为是一种最佳实践,不要在实例变量中存储数据。如果您想从请求的过滤器方法存储一些数据以在响应的过滤器方法中使用,一个更便携的方法是将该数据存储在requestContext中作为一个属性,就像我们在前面的例子中为请求标识符(ID)所做的那样。
现在,我们只需将@Logged注解添加到需要记录的类(类中的所有方法)或方法上,所以在这个例子中,只有POST方法会被记录(完整的源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/dynamicbinding/DynamicResource.java找到):
@Path("/dynamic")
public class DynamicResource {
@GET
public String getMessage() { // ...
@POST
@Logged
public String postMessage(String message) { // ...
}
另一种动态应用提供者的方法是使用configure,它提供了一个ResourceInfo对象,用于确定匹配资源的具体信息,以及一个FeatureContext对象,用于配置提供者和属性或查看每个请求的应用程序配置。以下示例将LoggingFilter类添加到所有以get开头的方法资源中:
@Provider
public class MyDynamicFeature implements DynamicFeature {
@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
Method m = resourceInfo.getResourceMethod();
if (m.getName().startsWith("get")) {
context.register(LoggingFilter.class);
}
}
}
由于ResourceInfo对象将返回匹配的类和方法,因此也可以检查匹配资源的注解。这使得动态功能可以为特定 HTTP 动词的所有请求(if (resourceInfo.getResourceMethod().getAnnotation(GET.class) != null) { //…)或没有@NameBinding注解的方法注册提供者变得容易。
名称绑定注解和动态过滤器是控制请求和响应处理的有效方法。
异步
足够了,关于提供者!让我们回到 RESTful 服务的核心——资源。在许多情况下,JAX-RS 请求/响应流的同步特性效率低下。例如,假设你的资源倾向于将请求传递给一个数据存储,该数据存储在数据库中查找或修改数据。如果你的数据存储逻辑有一组固定的线程执行数据库操作,那么当服务负载较高时,传入的请求可能会排队。使用我们迄今为止一直在使用的同步流程,这意味着执行流程会在资源方法内部阻塞,等待数据存储逻辑完成,然后才能完成流程。这可能会效率低下,因为一个线程实际上在等待另一个线程完成。如果这个初始线程在数据存储操作进行时执行其他任务,可能会更有效率。使用 JAX-RS 中的异步API 可以获得更高的效率。
异步响应
在 JAX-RS 中创建异步方法是通过在资源方法中添加一个AsyncResponse参数并使用@Suspended注解来实现的。一旦从数据存储中获取了数据,AsyncResponse对象就可以用来恢复请求。让我们看一个例子。假设我们有一个跟踪人员的服务,就像我们在实体提供者部分使用的那样。我们将稍微修改资源类,以便数据存储访问使用一个单独的Executor类来检索数据(完整的源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/async/AsyncPersonService.java找到))。看看下面的代码片段:
@Path("/person")
public class AsyncPersonService {
static ExecutorService executor = Executors.newFixedThreadPool(5);
@GET
@Path("sync/{id}")
public Person getPersonSync(@PathParam("id") int id) throws InterruptedException, ExecutionException {
Future<Person> someData = executor.submit(() -> getPerson(id));
return someData.get();
}
private Person getPerson(int id) {//...
}
在这个版本的代码中,getPersonSync方法将向执行器服务提交一个请求以检索具有指定 ID 的Person对象,然后它将阻塞,直到执行器服务完成操作。在这种情况下(为了代码的简洁性),它只是从哈希表中拉取数据,但如果它要从远程数据库中拉取数据,那么在someData.get()调用中阻塞的时间可能会更长。
因此,让我们尝试提高效率,这样我们就不必阻塞。我们可以将getPersonSync()方法重写如下:
@GET
@Path("async/{id}")
public void getPersonAsync(@PathParam("id") int id,
@Suspended AsyncResponse ar) {
executor.submit(() -> {
ar.resume(getPerson(id));
});
}
现在,执行器服务正在调用getPerson(id)方法,然后将结果传递给ar.resume(…),这将从上次停止的地方恢复请求/响应流程并返回一个响应。调用getPersonAsync(…)方法的请求线程立即返回,可以用来处理另一个请求。
AsyncResponse对象也可以用来处理异常。假设我们希望在指定的 ID 与数据库中的任何Person实例不匹配时抛出NoSuchPersonException异常。我们可能将代码修改如下:
executor.submit(() -> {
Optional<Person> p = Optional.ofNullable(getPerson(id));
if (p.isPresent())
ar.resume(p.get());
else ar.resume(new NoSuchPersonException());
});
当我们用一个异常恢复响应时,JAX-RS 容器将尝试将异常映射到一个合适的响应,就像在同步情况下所做的那样。
服务器端发送事件
服务器端异步的另一种形式是服务器端发送事件(SSEs)。SSEs 是超文本标记语言 5(HTML 5)规范的一部分,为客户端提供了一种从服务器异步注册和接收事件的方式。
JAX-RS 有两种发送 SSEs 的方式——直接向每个客户端流式传输和 广播 到所有客户端。让我们看看如何实现第一种方式,如下所示(完整源代码可在 github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/async/SseService.java 找到):
@Path("/sse")
@Produces(MediaType.SERVER_SENT_EVENTS)
public class SseService {
@GET
public void stream3Events(@Context SseEventSink sink, @Context Sse sse) {
Executors.newSingleThreadExecutor().submit(() -> {
try (SseEventSink sinkToClose = sink) {
sink.send(sse.newEventBuilder()
.mediaType(TEXT_PLAIN_TYPE)
.data("foo")
.name("fooEvent")
.id("1")
.build());
Thread.sleep(500);
// repeat for 2/bar
Thread.sleep(500);
// repeat for 3/baz
} catch (InterruptedException ex) {}
});
}
}
这是一个虚构的例子,但它显示了方法在启动一个新线程后立即返回,该线程以半秒的延迟向客户端发送一些文本事件。
这个例子向我们展示了,为了使 JAX-RS 资源能够发送服务器端事件(SSEs),它必须生成 SSE 媒体类型(MediaType.SERVER_SENT_EVENTS 或 text/event-stream),并且该方法必须接收 Sse 和 SseEventSink 参数,这两个参数都带有 @Context 注解。Sse 类型是一个实用类,可以创建事件和广播器。SseEventSink 类型代表服务器和客户端之间的连接,因此调用 send(…) 方法会将新事件发送给特定的客户端,而调用 close() 方法(这是通过 try-with-resources 逻辑隐式执行的)将优雅地关闭与客户端的连接。
我们发送的事件具有 text/plain 媒体类型——媒体类型用于确定应该使用哪个 MessageBodyWriter 提供程序来序列化传递给数据方法的那个对象。name(…) 和 id(...) 方法可以为每个发送的事件提供额外的上下文。尽管 data(…) 方法是必需的,但始终指定媒体类型是一个最佳实践。
如果我们使用 curl 调用此服务,我们会看到如下内容:
$ curl http://localhost:9080/rest/sse
event: fooEvent
id: 1
data: foo
event: barEvent
id: 2
data: bar
event: bazEvent
id: 3
data: baz
发送事件的另一种方法是使用 SseEventSinks,它将事件发送给所有已注册的客户端。让我们看看我们可能添加到 SseService 类中的代码示例,如下所示:
static SseBroadcaster broadcaster;
static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private void startBroadcasting(Sse sse) {
if (broadcaster == null) {
broadcaster = sse.newBroadcaster(); //...
}
}
@GET
@Path("/broadcast")
public void broadcast(@Context SseEventSink sink, @Context Sse sse) {
startBroadcasting(sse);
broadcaster.register(sink);
broadcaster.broadcast(sse.newEventBuilder()
.mediaType(TEXT_PLAIN_TYPE)
.data("new registrant")
.build());
}
与直接流方法一样,这种方法也要求方法生成 SERVER_SENT_EVENTS 媒体类型,并且方法具有 SseEventSink 和 Sse 参数类型。
首先,我们需要将 SseBroadcaster 设置为静态字段。我们这样做是因为 JAX-RS 资源默认的生命周期是每个请求。我们将在 使用 CDI 管理生命周期和 DI 部分讨论替代生命周期——这将简化此代码并提高性能。
一旦我们设置了广播器,我们就会将其与事件接收器注册。一旦注册,与该事件接收器关联的客户端将接收到从该广播器发送的所有事件。在这个例子中,每当有新的客户端注册以及每 5 秒钟,我们都会广播一个事件。让我们看看当我们是第一个客户端时,使用 curl 在命令行上看起来是什么样子,以及当第二个客户端注册(从一个单独的命令窗口)时的情况,如下所示:
$ curl http://localhost:9080/rest/sse/broadcast
UnnamedEvent
data: new registrant
UnnamedEvent
data: ping
UnnamedEvent
data: ping
关于这个输出的一个需要注意的事项是 未命名的 Event 文本——这是因为 SSE 必须有一个名称,所以如果在构建时没有提供名称,JAX-RS 容器会为其创建一个名称。如果没有指定,其他 JAX-RS 容器可能会使用不同的名称。
上下文注入
我们已经讨论了在使用 @Context 注解 SSE 对象时的注入,但这个注解可以用在很多地方。上下文注入可以在资源和提供者中发生。你可以注入很多有用的事物,如下所述:
-
ResourceContext: 用于初始化子资源定位器 -
ResourceInfo: 用于确定匹配的资源类和方法 -
HttpHeaders: 用于读取客户端请求中的 HTTP 头 -
SecurityContext: 用于确定当前用户、他们的安全角色等 -
UriInfo: 用于读取客户端请求的 URI -
应用程序: 用于获取表示此 RESTful 服务的应用程序 -
提供者: 用于访问其他 JAX-RS 提供者 -
Sse和SseEventSink: 在上一节中讨论最佳实践
在大多数情况下,建议上下文注入发生在字段而不是参数中。原因是 Jakarta REST 项目打算废弃
@Context注解,转而使用 CDI 的@Inject注解,该注解不针对方法参数。
Javadoc (jakarta.ee/specifications/restful-ws/2.1/apidocs/overview-summary.html) 是理解这些可注入类型功能的最佳资源。以下是一些基本示例:
@Context
SecurityContext secCtx;
@GET
public Response getSomeData() {
if (secCtx.isUserInRole("special")) {
return getSpecialResponse();
}
return getNormalResponse();
}
此示例使用客户端用户主体的角色来确定要返回的实体响应。以下示例使用 ResourceInfo 类来确定是否应该使用 MessageBodyWriter 提供者:
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class MySpecialJsonWriter implements MessageBodyWriter<Person> {
@Context
ResourceInfo resInfo;
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
Class<?> resourceClass = resInfo.getResourceClass();
return resourceClass.equals(SpecialResource.class) && type.equals(Person.class) && mediaType .isCompatible (APPLICATION_JSON_TYPE);
}
将上下文对象注入资源和提供者使我们能够开发强大且灵活的应用程序。现在,让我们将注意力转向格式化应用程序需要发送和接收的数据。
使用 JSON-P 和 JSON-B 管理有效载荷
虽然一般来说 RESTful 服务和具体来说 JAX-RS 应用可以提供任何媒体类型的实体(纯文本、可扩展标记语言(XML)、可移植文档格式(PDF)、二进制等),但 JSON 是云原生应用的当红炸子鸡。JSON 流行是因为它既易于阅读又易于解析——几乎在所有现代语言中都有 JSON 解析和绑定的库。
在 实体提供者 部分,我们体验了将 Java 对象(Person)序列化和反序列化为 JSON 的过程。在那个部分,我们通过字符串操作手动执行了这一操作。虽然手动方法可以工作,但现在我们将讨论两个 API,它们可以简化并增强 Java 中对 JSON 的控制。
JSON-P 是一个用于操作 JSON 的程序性 API,而 JSON-B 是一个声明性(基于注解)的 API,用于快速轻松地将对象映射到 JSON 或相反。
JSON-P
JsonObject、JsonArray 等,这些都是对象模型 API 的一部分。
假设我们有一些这样的对象:
public class Starship {
private String name;
private boolean hasHyperdrive;
private List<Weapon> weapons;
private int speedRating;
//with public getters and setters
}
public class Weapon {
private String name;
private String type;
private int damageRating;
//with public getters and setters
}
假设我们想要将其转换为类似以下的 JSON 内容:
{
"name": "Coreillian Freighter",
"hasHyperdrive": true,
"speedRating": 22,
"weapons": [
{
"name":"Quad Blaster Turret",
"type":"Laser",
"damageRating":24
}
]
}
我们将从将 Starship 实例转换为 JSON 字符串开始。我们可以通过使用 Json 类来创建对象构建器和数组构建器来完成此操作。这些构建器可以通过添加属性或对象来创建对象。因此,为了创建飞船的 JSON,我们需要一个飞船的对象构建器,以及每个武器的对象构建器,然后是一个用于所有武器的数组构建器。一个这样的例子可以在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/jsonp/JsonpConverter.java找到。
每个对象都需要自己的 JsonObjectBuilder 实例,每个数组或集合都需要自己的 JsonArrayBuilder 实例。然后,你只需向它们添加项目即可。
将 JSON 字符串反序列化为对象的方式相反。首先,你必须从 JsonReader 中提取 JsonObject 实例,如下所示:
JsonReader reader = Json.createReader(new StringReader(json));
JsonObject shipObject = reader.readObject();
然后,你必须创建一个 Starship 实例,并从 JsonObject 的属性中填充它,如下所示:
Starship ship = new Starship();
ship.setName(shipObject.getString("name"));
ship.setHasHyperdrive(shipObject.getBoolean
("hasHyperdrive"));
//...
此方法使用对象模型,它需要在将其转换为对象之前将整个 JSON 流加载到内存中。对于小 JSON 文件,这不会成为问题,并且允许模型存储在内存中并重新访问。它还允许在将 JSON 写回流之前更改模型。
流式方法需要的内存远少于对象模型,并且能够读取非常大的 JSON 流而不会耗尽内存。它是通过在读取 JSON 时触发事件,然后丢弃该部分的 JSON 来实现这一点的。这种方法非常高效,性能优于对象模型,但需要更复杂的编码,并且由于对象模型不在内存中,你无法稍后回过头来询问:“现在,那个值是什么来着?”
使用与之前相同的 Java 对象和 JSON 流,以下是序列化代码的示例(完整源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/jsonp/JsonpStreamingConverter.java找到):
StringWriter sw = new StringWriter();
JsonGenerator generator = Json.createGenerator(sw);
generator.writeStartObject()
.write("name", ship.getName())
.write("hasHyperdrive", ship.isHasHyperdrive())
.write("speedRating", ship.getSpeedRating())
.writeStartArray("weapons");
for (Weapon w : ship.getWeapons()) {
generator.writeStartObject()
.write("name", w.getName())
.write("type", w.getType())
.write("damageRating", w.getDamageRating())
.writeEnd();
}
generator.writeEnd()
.writeEnd();
generator.close();
与 JsonObjectBuilder 和 JsonArrayBuilder 类似,JsonGenerator 可以传入类似映射的值来构建 JSON 对象。与对象模型构建器 API 不同,JsonGenerator 无法进行更改——一旦 JSON 被写入,就无法更改值。这两个构建器都有 remove 方法,而 JsonArrayBuilder API 有设置方法,允许您更改之前配置的值。存在这两种方法的原因是让您在灵活性和效率之间做出选择。
将 JSON 解析到对象中,基于流的方案甚至更复杂,如下所示:
Starship ship = new Starship();
JsonParser parser = Json.createParser(new StringReader(json));
while (parser.hasNext()) {
Event event = parser.next();
if (event == Event.KEY_NAME) {
String keyName = parser.getString();
parser.next();
switch(keyName) {
case "name": ship.setName(parser.getString()); break;
//...
case "weapons": ship.setWeapons(parseWeapons(parser));
}
}
解析器的工作方式类似于 Java 迭代器,返回事件以指示诸如对象开始({)、对象结束(})、数组开始([)、数组结束(])、键名(例如,name 和 speedRating)以及键值(例如,Coreillian Freighter 和 24)等信息。每个事件在其上下文中被解释是很重要的。例如,一个数组可能包含多个对象,因此需要跟踪当前正在解析的对象,以避免数据混淆。parseWeapons 方法通过分别解析数组中的每个项目来提供示例,如下面的代码片段所示:
private List<Weapon> parseWeapons (JsonParser parser) {
List<Weapon> weapons = new ArrayList<>();
Event event = null;
while ((event = parser.next()) != Event.END_ARRAY) {
Weapon w = new Weapon();
while (event != Event.END_OBJECT) {
if (event == Event.KEY_NAME) {
String keyName = parser.getString();
parser.next();
switch(keyName) {
case "name": w.setName(parser.getString()); //...
}
}
event = parser.next();
}
weapons.add(w);
}
return weapons;
}
JSON-P 提供了一套非常强大的 API,用于程序化地读取和写入 JSON。不过,代码可能会有些冗长。这正是 JSON-B 可以帮助解决的问题。
JSON-B
虽然 JSON-P 非常强大且灵活,但 JSON-B 在序列化和反序列化对象到 JSON 方面非常简单且高效。JSON-B 有一些程序化 API,但总体而言,它采用声明式方法来编写/读取 JSON。这意味着对象到 JSON 的转换将基于对象类型的 getter 方法——同样,JSON 到对象的转换将基于对象类型的 setter 方法。
如果我们使用来自 JSON-P 的Starships和Weapons的示例对象,对象到 JSON 以及相反的转换非常简单,如下所示:
StringWriter sw = new StringWriter();
Jsonb jsonb = JsonbBuilder.create();
jsonb.toJson(ship, sw);
String json = sw.getBuffer().toString();
直接转换为字符串是可能的,但使用OutputStream或Writer更可取,尤其是在处理大型 JSON 对象时。这里的主要对象是Jsonb和toJson(…)方法。你可以在这里看到输出:
{"hasHyperdrive":true,"name":"Coreillian Freighter","speedRating":22,"weapons":[{"damageRating":24,"name":"Quad Blaster Turret","type":"Laser"}]}
这看起来与我们使用 JSON-P 创建的非常相似,但它都在一行中,难以区分一个对象在哪里结束,下一个对象在哪里开始。对于大多数 JSON 消费者来说,这应该不是问题,但如果我们想让它更易于阅读,我们可以通过用以下代码替换JsonbBuilder.create()方法调用来添加一些配置:
Jsonb jsonb = JsonbBuilder.create(
new JsonbConfig().withFormatting(true));
这将产生以下输出:
{
"hasHyperdrive": true,
"name": "Coreillian Freighter",
"speedRating": 22,
"weapons": [
{
"damageRating": 24,
"name": "Quad Blaster Turret",
"type": "Laser"
}
]
}
你可以使用几种其他配置选项来处理 Java 对象的序列化。例如,你也可以添加自己的JsonParser从 JSON-P,以你自己的方式将对象转换为 JSON。
将 JSON 转换回对象同样简单,如下所示:
Starship shipFromJson = jsonb.fromJson(json,Starship.class);
与toJson(…)方法一样,你可以使用字符串或流。
那么,如果你有一个对象,但你想 JSON 字段的名字与 Java 属性名不同呢?或者,也许你根本不想将某些字段暴露为 JSON?这时,@JsonbProperty("someOtherName")和@JsonbTransient这样的注解就派上用场了,而且根据你放置注解的位置,它会产生不同的效果。如果注解在 getter 上,那么它只会应用于序列化(从 Java 到 JSON 的转换)。如果注解在 setter 上,那么它只会应用于反序列化。如果注解在字段本身上,那么它将应用于两者。让我们考虑以下代码片段:
public class Person {
private String firstName;
@JsonbTransient
private String middleName;
@JsonbProperty("familyName")
private String lastName;
private String favoriteColor;
private int age;
//...all other public unannotated getters/setters
@JsonbProperty("favouriteColour")
public String getFavoriteColor() {
return favoriteColor;
}
@JsonbProperty("yearsOld")
public void setAge(int age) {
this.age = age;
}
}
我们将创建一个Person实例并将其打印为 JSON,如下所示:
Person p = new Person();
p.setFirstName("John");
p.setMiddleName("Tiberius");
p.setLastName("Doe");
p.setFavoriteColor("Green");
p.setAge(25);
String jsonPerson = jsonb.toJson(p);
System.out.println(jsonPerson);
输出将看起来像这样:
{
"age": 25,
"familyName": "Doe",
"favouriteColour": "Green",
"firstName": "John"
}
lastName字段已转换为familyName,middleName字段根本未打印,而favoriteColor字段已被英国化为favouriteColour。但如果我们尝试从这个 JSON 创建一个新的Person实例,我们会得到一个不完整的Person实例。让我们看一下,如下所示:
Person p2 = jsonb.fromJson(jsonPerson, Person.class);
System.out.println(p2.getFirstName());
System.out.println(p2.getMiddleName());
System.out.println(p2.getLastName());
System.out.println(p2.getFavoriteColor());
System.out.println(p2.getAge());
这将产生以下输出:
John
null
Doe
null
0
middleName字段在 JSON 中缺失,所以它为 null 并不奇怪。由于@JsonbProperty("favouriteColour")注解只存在于 getter 上,JSON-B 不会将英国化的 JSON 字段转换为美国化的 Java 字段。而且,由于@JsonbProperty("yearsOld")注解应用于setAge(…)方法,它将不会被设置,因为 JSON 仍在使用age字段名。
使用 JSON-B 时,注意注解放置的位置导致的差异行为是很重要的。
现在,让我们将这一点与 JAX-RS 相关联。JAX-RS 规范说明,支持 JSON-P 的产品必须为 JsonStructure、JsonObject、JsonArray、JsonString 和 JsonNumber 实体类型提供 MessageBodyReaders 和 MessageBodyWriters。此外,支持 JSON-B 的产品必须在媒体类型为 application/json、text/json、*/json 或 */*+json 时为任何对象类型提供 MessageBodyReaders 和 Writers。任何实现整个 MicroProfile 规范集的产品都将包含 JAX-RS、JSON-P 和 JSON-B。这意味着在大多数情况下,你可以依赖你的 JAX-RS 容器来处理 JSON 到对象以及对象到 JSON 的转换。
现在我们已经学会了轻松处理 JSON 的方法,让我们学习如何使用客户端 API 发送请求并消费结果。
使用 MicroProfile Rest Client 消费 RESTful 服务
到目前为止,我们已经介绍了如何设计复杂的 RESTful 服务,以及如何轻松地将 JSON 转换为 Java 对象,反之亦然。接下来,我们需要使用客户端 API 来消费这些服务。在微服务架构中,RESTful 客户端对于调用远程服务至关重要。
JAX-RS 客户端 API
消费 RESTful 服务的一种方式是使用 JAX-RS 客户端 API。与 JSON-P(与 JSON-B 相反),这些 API 通常更程序化,对单个选项(如头部、路径构建等)有更多的控制。让我们看看一些使用本章前面提到的同义词例子的代码,如下所示:
String uri = "http://localhost:9080/rest/thesaurus";
Client client = ClientBuilder.newBuilder().build();
WebTarget target = client.target(uri).path(word);
Builder builder = target.request(MediaType.TEXT_PLAIN);
try (Response response = builder.get()) {
int status = response.getStatus();
assert status == 200;
} finally {
client.close();
}
客户端实例是通过 ClientBuilder 构建的。在这个例子中,它只是构建了一个 Client 实例,但你也可以使用 ClientBuilder 来设置配置属性或注册客户端提供者。Client 实例在使用完毕后应显式关闭——它们目前没有实现 AutoCloseable 接口,但 JAX-RS 规范的未来版本将添加该接口,允许在 try-with-resources 块中关闭 Client 实例。
WebTarget 表示客户端请求的目的地。它有用于附加路径元素、解析路径模板变量、添加查询或矩阵参数或指定预期响应媒体类型的方法。在先前的例子中,我们使用以下行代码将 word 变量附加到 uri 变量:
WebTarget target = client.target(uri).path(word);
或者,我们可以将 uri 变量更改为 http://localhost:9080/rest/thesaurus/{word},然后我们可以使用以下行代码:
WebTarget target = client.target(uri).resolveTemplate ("word", word);
根据具体情况,两者可能都更易使用。
通过在 WebTarget 上调用 request(…) 方法创建一个 Invocation.Builder 对象——一个可选的媒体类型参数用于确定期望的响应媒体类型;它将设置 Accept 头。Invocation.Builder 对象具有 get(…)、post(…)、put(…)、delete(…) 以及其他表示请求中使用的 HTTP 动词的方法。您可以使用 method(…) 方法指定 API 中未内置的 HTTP 动词。它还具有设置 cookie 或头的其他方法。
Invocation.Builder 对象还具有 async() 和 rx() 方法,分别返回异步调用器和响应式调用器。这些调用器使用户能够异步检索响应,这通常可以提高性能。
Response 对象代表来自远程服务器的 HTTP 响应。从 Response 对象中,您可以检查响应的状态码(如 200、204、400、404、500 等)和响应头,读取响应实体,等等。请注意,Response 对象是 AutoCloseable 的——始终关闭 Response 和 Client 对象是一个好习惯。
MicroProfile Rest Client
如果 JAX-RS 客户端类似于 JSON-P,那么 @Path、@GET、@PUT、@POST、@DELETE 等注解。MicroProfile Rest Client 实现提供了一个接口实例,然后您可以通过调用该接口来调用远程服务。
让我们来看一个例子,如下(完整的源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/client/ThesaurusClient.java找到):
@Path("/thesaurus/{word}")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public interface ThesaurusClient {
@GET
String getSynonymsFor(@PathParam("word") String word) throws NoSuchWordException;
@POST
String setSynonymsFor(@PathParam("word") String word, String synonyms) throws WordAlreadyExistsException;
// other methods matching ThesaurusResource ...
}
这里的方法与 ThesaurusResource 类中的五个资源方法相匹配,但 @PathParam 参数是一个方法参数。这些方法都返回一个 String 对象,但它们也可以返回 Response 对象,如果查看响应中的内容(如头或状态码等)很重要的话。通常,这些内容可以被抽象出来,以便返回实际的数据类型。这种接口方法允许我们通过简单地调用这些方法来调用服务。但首先,我们需要构建这个客户端的一个实例。如果我们处于使用 CDI(见下一节)和 MicroProfile Config(见第五章,增强云原生应用程序)的环境,那么框架可以自动构建和注入客户端实例。否则(或者如果您只想程序化地构建实例),您可以使用 RestClientBuilder API,如下所示:
ThesaurusClient thesaurus = RestClientBuilder.newBuilder() .baseUri(URI.create("http://localhost:9080/rest"))
.build(ThesaurusClient.class);
这设置了baseUri,即添加@Path注解之前的 URI 路径。类似于 JAX-RS ClientBuilder API,我们也可以使用RestClientBuilder API 来指定客户端实例的属性或注册提供者。一旦我们构建了客户端实例,我们就可以这样调用它:
thesaurus.getSynonymsFor(word);
客户端提供者
这是一种调用服务的好方法——代码更少,阅读起来也更简单!你可能正在想:那个方法会抛出异常——实现如何知道何时抛出它呢? 这是一个很好的问题!答案是ResponseExceptionMapper。它基本上是 JAX-RS ExceptionMapper的逆过程——不是将异常映射到响应,而是将响应映射到异常。默认情况下,MicroProfile Rest Client 实现将在任何状态码为400或更高的响应上抛出WebApplicationException——这些代码是客户端错误或服务器错误。要将更具体的响应映射到异常,你需要注册一个或多个ResponseExceptionMapper,如下所示:
public class NoSuchWordResponseMapper implements ResponseExceptionMapper<NoSuchWordException> {
@Override
public boolean handles(int status,MultivaluedMap<String, Object> headers) {
return status == 404;
}
@Override
public NoSuchWordException toThrowable(Response resp) {
return new NoSuchWordException();
}
}
此响应异常映射器实现了两种方法。第一种,handles(…), 用于通知客户端实现是否应该使用此映射器处理当前响应。如果它返回true,则客户端实现将调用toThrowable(…)方法来获取它应该抛出的异常。如果handles(…)方法返回false,则客户端实现将在假定响应成功并简单地返回一个有效值给客户端调用者而不是抛出异常之前检查任何其他已注册的响应异常映射器。
特别注意
toThrowable(…)方法应该返回异常,而不是抛出它。客户端实现实际上会抛出异常;它只需要知道应该抛出哪个异常。
与服务器端提供者一样,客户端提供者需要被注册。有两种方式可以注册客户端提供者。首先,你可以在构建客户端之前从RestClientBuilder API 注册它们,如下面的代码片段所示:
ThesaurusClient thesaurus = RestClientBuilder.newBuilder() .baseUri(URI.create("http://localhost:9080/rest"))
.register(NoSuchWordResponseMapper.class)
.build(ThesaurusClient.class);
注册客户端提供者的第二种方式是在客户端接口上使用一个或多个@RegisterProvider注解,如下所示:
@RegisterProvider(NoSuchWordResponseMapper.class)
public interface ThesaurusClient { //...
你可以在客户端注册与服务器端相同类型的提供者,包括MessageBodyReader和Writer、Reader和WriterInterceptors。你无法注册ExceptionMappers或服务器端过滤器(ContainerRequestFilter或ContainerResponseFilter)。然而,你可以注册客户端过滤器(ClientRequestFilter或ClientResponseFilter)——它们与服务器端过滤器的工作方式非常相似。
小贴士
ClientRequestFilter过滤器的ClientRequestContext有一个类似于ContainerRequestFilter过滤器的abortWith(Response)方法。这个方法在测试代码中模拟不同的服务器响应时非常有用。
所有这些客户端提供程序都将与 JAX-RS 客户端和 MicroProfile Rest 客户端一起工作,除了ResponseExceptionMapper——这些只与 MicroProfile Rest 客户端一起工作。
异步
JAX-RS 客户端和 MicroProfile Rest 客户端都能够异步调用服务。这可能对客户端来说甚至更强大,因为客户端通常在资源受限的环境中运行,并且可能需要执行多个请求以实现其目标。
JAX-RS 客户端通过创建一个将产生引用响应对象的java.util.concurrent.Future对象的AsyncInvoker实例来异步调用服务。这允许用户指定一个当响应可用(或在请求/响应过程中发生异常时)被通知的InvocationCallback。以下是一个Future方法的示例:
AsyncInvoker invoker = builder.async();
Future<Response> future = invoker.get();
// do something else while waiting for the response...
try (Response response = future.get()) {
// handle response...
} finally {
client.close();
}
这段代码看起来与本章早些时候的同步代码非常相似,提供了一种执行异步客户端请求的简单方法。接下来,让我们看看如何使用InvocationCallbacks,如下所示(此方法和Future方法的完整源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/client/JAXRSClient.java找到):
String uri = "http://localhost:9080/rest/thesaurus";
Client client = ClientBuilder.newBuilder().build();
for (String word : words) {
WebTarget target = client.target(uri).path(word);
Builder builder = target.request(MediaType.TEXT_PLAIN);
AsyncInvoker invoker = builder.async();
invoker.get(new InvocationCallback<String>() {
@Override
public void completed(String response) {
sb.append(response + "\n");
}
@Override
public void failed(Throwable th) {
th.printStackTrace();
}
});
}
这个例子展示了如何查找多个单词。它向服务器并行发送多个请求,当响应可用时,会调用InvocationCallback的completed方法。
MicroProfile Rest 客户端的异步请求略有不同。客户端接口方法必须返回包装了预期返回类型的CompletionStage。因此,我们将修改我们的客户端接口,如下所示(完整源代码可在github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/client/ThesaurusAsyncClient.java找到):
@Path("/thesaurus/{word}")
@RegisterProvider(NoSuchWordResponseMapper.class)
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public interface ThesaurusAsyncClient {
@GET
CompletionStage<String> getSynonymsFor(@PathParam ("word") String word);
@POST
CompletionStage<String>setSynonymsFor (@PathParam("word") String word, String synonyms);
//...similar methods for PUT, DELETE, and PATCH
}
注意,这些方法中没有任何一个声明会抛出任何异常。这是因为任何异常处理都是在返回的CompletionStage处理之后发生的。这就是我们可能调用此客户端的方式:
StringBuffer sb = new StringBuffer();
CountDownLatch latch = new CountDownLatch(wordsArr.length);
ThesaurusAsyncClient client = RestClientBuilder.newBuilder()
.baseUri(URI.create("http://localhost:9080/rest"))
.register(NoSuchWordResponseMapper.class)
.build(ThesaurusAsyncClient.class);
Arrays.stream(wordsArr).parallel()
.map(client::getSynonymsFor)
.forEach(cs -> {
cs.exceptionally(t -> {
t.printStackTrace();
return "unable to complete request";
}).thenAccept(s -> {
sb.append(s + "\n");
latch.countDown();
});
});
latch.await(5, TimeUnit.SECONDS);
与 JAX-RS 客户端回调示例类似,此示例同时查找多个单词的同义词。通过使用CompletionStage,我们可以轻松处理异常或执行额外的内联处理。
记住另一种异步操作的形式是 SSEs。JAX-RS 客户端 API 允许你以 InboundSseEvent 对象的形式接收事件。MicroProfile Rest Client 进一步允许你使用 Publisher 对象接收事件。事件可以是允许你在每个事件上读取额外元数据的 InboundSseEvent 对象,或者是一个特定的 Java 类型,只要你有 MessageBodyReader 将事件转换为该类型。
如果我们想消费在“使用 Jakarta REST 构建 RESTful 服务”部分末尾编写的服务的 SSE 事件,我们可能会编写一个看起来像这样的客户端接口:
@Path("/sse")
@Produces(MediaType.SERVER_SENT_EVENTS)
public interface SseClient {
@GET
Publisher<String> receiveSSEs();
客户端接口很简单,对吧?所以,这就是你可能使用它的方法(完整的源代码可在 github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/blob/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/async/MPSseConsumerResource.java 找到):
client.receiveSSEs().subscribe(new Subscriber<String>() {
@Override
public void onSubscribe(Subscription s) {
s.request(3);
}
@Override
public void onNext(String s) {
// handle event
}
@Override
public void onError(Throwable t) {
// exception while processing event
}
@Override
public void onComplete() {
// done receiving events
}
});
一旦从客户端接口返回 Publisher,调用者可以订阅它,并通过 Subscription 的 request(int) 方法控制 SSEs 的流动。对于每个新事件(只要它已被请求),都会调用 onNext 回调方法。当发生错误时(例如,找不到可以反序列化事件的 MessageBodyReader),会调用 onError 回调。当与服务器的连接关闭时,会调用 onComplete 回调。
从服务器到客户端传播 HTTP 头部
很常见的情况是,你可能需要构建一个需要消费其他 RESTful 服务的 RESTful 服务。这可能是网关或委托模式的一部分,或者可能是你的服务需要聚合其他服务,例如以下服务,其中休假服务可能需要为航空公司、酒店、娱乐场所等预订:

图 4.2 – 服务聚合
在这些情况下,从原始请求传播头部到委托请求通常很有用。例如,假设你想要在后续请求中重用原始请求上发送的认证凭据;MicroProfile Rest Client 有一些内置机制使得这变得容易实现。
首先,你必须使用 @RegisterClientHeaders 注解你的客户端接口,然后指定一个以逗号分隔的头部列表,该列表应该由容器自动传播到一个 MicroProfile Config 属性中,如下所示:
org.eclipse.microprofile.rest.client.propagateHeaders=Authorization,X-RequestID
接下来,你可以在客户端接口中声明性地指定头部,如下所示:
@RegisterRestClient
@ClientHeaderParam(name="AgentID", value="Bob's Travel Co.")
public interface AirlineReservationClient {
@GET
Reservation getReservation(String reservationID);
@POST
@ClientHeaderParam(name = "RequestID", value = "{newId}")
String makeReservation(Reservation r);
default String newId() {
return UUID.randomUUID().toString();
}
}
在此代码中,带有硬编码值 Bob's Travel Co. 的 AgentID 标头将随着每个请求从该客户端发送,因为接口上应用了 @ClientHeaderParam 注解。当调用 makeReservation 方法时,MicroProfile Rest Client 实现将调用 newId 方法以获取 RequestID 标头的值——标头的值是方法的返回值,因为注解值被大括号包围。
这两种方法都允许发送标头而无需修改客户端接口方法的签名。
使用 CDI 管理生命周期和 DI
默认情况下,JAX-RS 资源为每个请求创建。虽然这在某些情况下可能很有用,但如果它们是单例的,效率会更高。这样,我们就不会为每个请求创建新的对象实例(这是一个昂贵的操作),也不会在请求完成后产生多余的垃圾。
虽然我们可以创建一个返回资源的 Application 子类,通过 getSingletons() 方法,但这将阻止容器自动发现和注册资源及提供者。避免这种方法的另一个原因是 getSingletons() 方法在 JAX-RS 的未来版本中已被弃用,并最终将被移除。
相反,我们可以使用 上下文和依赖注入(CDI)。CDI 使用注解允许开发者声明式地管理对象生命周期并执行字段、构造函数和设置方法的注入。
范围
CDI 包含几个内置的注解 @ApplicationScoped、@RequestScoped 和 @Dependent。正如你可能猜到的,被 @ApplicationScoped 注解的对象将 存活 在应用程序的生命周期内,而被 @RequestScoped 注解的对象则只会在单个请求的生命周期内存活。
@Dependent 注解有点特殊。基本上,被这个注解注解的对象将继承它被注入的对象的作用域。对于实际上没有注入任何东西的 JAX-RS 资源类,它继承 JAX-RS 容器的作用域。对于资源对象,这是 每个请求,但对于提供者对象,这是 每个应用程序。
这意味着我们可以用 @ApplicationScoped 注解我们的 JAX-RS 资源类,然后我们可以摆脱那些讨厌的静态字段,只使用正常的实例字段。
注入
DI 是 CDI 的另一个强大功能。使用注解,你可以指定你的依赖关系,并让容器处理所有的连接。在最基本的案例中,如果你想注入一个类或接口的一个实例,其中只有一个管理 Bean 实现该接口,你可以使用 @Inject,CDI 会完成其余的工作。
让我们来看一个例子。假设我们有一个类,如下所示,我们想要注入(所有 CDI 示例的完整源代码可以在 github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/tree/main/Chapter04/src/main/java/com/packt/microprofile/book/ch4/cdi 找到):
public interface MyDependency {
int getInstanceId();
}
@RequestScoped
public class MyDependencyImpl implements MyDependency {
static AtomicInteger COUNTER = new AtomicInteger();
private final int instanceId = COUNTER.getAndIncrement();
@Override
public int getInstanceId() {
return instanceId;
}
}
我们有一个请求作用域的 Bean,当实例化时,将有一个唯一的实例 ID。现在,假设我们想要将其注入到一个由 CDI 管理的 JAX-RS 资源类中,但我们希望资源类是应用程序作用域的,以获得更好的性能。它可能看起来像这样:
@ApplicationScoped
@Path("/cdi")
public class MyCdiResource {
@Inject
MyDependency dependency;
//...
}
这个对象,MyCdiResource,在整个应用程序的生命周期中只会实例化一次,没有额外的对象创建或多余的垃圾。但是注入的 MyDependency 对象是一个返回依赖实例 ID 的 GET 方法,因为依赖的实例 ID 在每次请求时都会增加,如图所示:
$ curl http://localhost:9080/rest/cdi
1
$ curl http://localhost:9080/rest/cdi
2
$ curl http://localhost:9080/rest/cdi
3
有时候,你可能想要创建自己的注入 Bean。CDI 提供了一种机制,你可以使用 @Produces 注解(与 JAX-RS 的 @Produces 注解同名但不同包,用于指定媒体类型)。要使用这个注解,你需要在 CDI 管理的 Bean 上的一个方法上应用这个注解;该方法返回的对象将被适当地注入。让我们看看在代码示例中这会是什么样子,如下所示:
@ApplicationScoped
public class SomeOtherBean {
@Produces
public MyProducedDependency produceDependency() {
return new MyProducedDependency(Math.random() * 10);
}
}
@ApplicationScoped
@Path("/cdi")
public class MyCdiResource {
//...
@Inject
MyProducedDependency producedDependency;
//...
}
在这种情况下,由于 MyCdiResource 类被注解为 @ApplicationScoped,MyProducedDependency 对象在整个应用程序的生命周期中只构建和注入一次。如果我们把 MyCdiResource 改为 @RequestScoped,那么随机数会随着每个请求而改变。producer 方法在需要时由 消费 Bean 调用。
那么,如果你有多个可能的 Bean 可以注入,会发生什么?你的应用程序可能会因为 DeploymentException 异常而无法启动,这表明存在模糊的依赖关系。为了解决这个问题,你可以使用 @Named 注解或 限定符 注解。
@Named 注解可能看起来像这样:
@ApplicationScoped
@Named("max")
public class MyOtherDependencyImpl implements MyDependency {
//...
}
@ApplicationScoped
@Path("/cdi")
public class MyCdiResource {
@Inject
@Named("max")
MyDependency dependency;
}
值相同的 @Named 注解应用于实现类 和 注入点。
限定符稍微复杂一些,但提供了更多的灵活性。它首先涉及到创建一个新的注解,如下所示:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Minimal { }
然后,我们只需在类和注入目标上添加这个注解,如下所示:
@ApplicationScoped
@Minimal
public class MyThirdDependencyImpl implements MyDependency {
//...
}
@ApplicationScoped
@Path("/cdi")
public class MyCdiResource {
@Inject
@Minimal
MyDependency dependency;
}
在其他 CDI 艺术品(如拦截器和可移植扩展)中可以更广泛地使用限定符。其中一个扩展内置在 MicroProfile Rest Client 实现中,它允许构建客户端实例并将其注入到您的 CDI 容器中。它使用 @RestClient 限定符。为了使其工作,您需要将 @RegisterRestClient 注解添加到客户端接口上。使用此注解,您还可以指定 baseUri 路径。或者,您可以使用 MicroProfile Config 来指定客户端实例的 baseUri 路径和其他配置选项。下面是一个示例:
@Path("/thesaurus/{word}")
@RegisterProvider(NoSuchWordResponseMapper.class)
@RegisterRestClient(baseUri = "http://localhost:9080/rest")
public interface ThesaurusClient {
@GET
String getSynonymsFor(@PathParam("word") String word) throws NoSuchWordException;
//...
}
然后,我们可以注入客户端实例并像这样使用它:
@ApplicationScoped
@Path("/cdi")
public class MyCdiResource {
@Inject
@RestClient
ThesaurusClient thesaurusClient;
@GET
@Path("/thesaurus/{word}")
public String lookup(@PathParam("word") String word) {
try {
return thesaurusClient.getSynonymsFor(word);
} catch (NoSuchWordException ex) {
return "Sorry, that word is not found.";
}
}
虽然创建一个 RESTful 服务然后使用客户端访问同一应用程序中的另一个 RESTful 服务可能看起来很愚蠢,但这个原则对于微服务架构来说非常常见。这个服务可能是真实同义词服务的网关,或者完整的同义词可能分布在几个 虚拟机(VMs)上。提供或消费那些服务生命周期和依赖关系声明式管理的 RESTful 服务的能力确实非常强大。
摘要
在本章中,我们学习了如何使用行业标准 API(如 JAX-RS、CDI、JSON-P、JSON-B 和 MicroProfile Rest Client)创建和消费基本和复杂的 RESTful 服务。我们了解到,其中一些 API 提供了非常简洁和类型安全的途径,而其他 API 则以增加代码复杂性的代价提供了额外的灵活性。完成本章后,我们现在可以创建利用 REST 和 JSON 的完整功能的微服务。我们还可以通过使用异步客户端和适当的生命周期范围来提高这些服务的效率。
在下一章中,我们将探讨如何通过其他 MicroProfile API 来提高这些服务的可配置性和鲁棒性。
第五章:增强云原生应用
在上一章第四章,开发云原生应用中,我们学习了如何构建云原生应用。然而,构建云原生应用只是开始。下一步是增强应用,使其可配置、具有弹性、可文档化和安全。在本章中,你将学习如何使用 MicroProfile 配置配置你的云原生应用,使用 MicroProfile 故障容错使应用具有弹性,使用 MicroProfile OpenAPI 文档其 API,最后,使用 MicroProfile JWT 保护应用。在本章之后,你应该能够使用这些技术来提高你云原生应用的质量。为了完全理解本章,你需要具备一些 Java、Maven 和 Gradle 的基本知识。
我们将涵盖以下主题:
-
使用 MicroProfile 配置配置云原生应用
-
使用 MicroProfile 故障容错使云原生应用具有弹性
-
使用 MicroProfile OpenAPI 文档云原生应用
-
使用 MicroProfile JWT 保护云原生应用
使用 MicroProfile 配置配置云原生应用
MicroProfile 配置(源代码位于github.com/eclipse/microprofile-config)是 MicroProfile 社区创建的第一个规范。配置的概念已经存在十年了。你可能还记得在第一章,云原生应用中,我们简要讨论了十二要素应用,其中第三要素III. 配置 (12factor.net/config)建议将十二要素应用的配置存储在与应用程序代码分离的环境中。这是因为任何配置值的更新都不会导致应用程序代码的重建。但是,有时在那种环境中存储所有配置,如安全凭证等,在现实中并不现实。将一些配置存储在数据库中也很常见。由于配置可能存在于许多不同的地方,因此需要一个获取配置的机制。许多库都提供了这种机制,例如 Apache DeltaSpike 配置 (deltaspike.apache.org/documentation/configuration.html)、Apache Tamaya (tamaya.incubator.apache.org/)等。
MicroProfile 配置被创建为一个标准,这样你就不必担心将哪个库拉入你的应用程序。
在本节中,我们将学习 MicroProfile 配置如何定义云原生应用存储和检索配置的方式。
存储配置
在 MicroProfile Config 中,配置存储在ConfigSource,这是一个您可以放置配置值的地方。环境变量、系统属性、属性文件、数据库、ZooKeeper 等都可以用作配置源。每个ConfigSource都有一个关联的序号,用于指示ConfigSource的重要性。序号较高的ConfigSource意味着它指定的配置值将覆盖序号较低的指定相同配置的ConfigSource。
例如,customer_name属性在序号为 200 的ConfigSource(ordinal = 200)中指定为Bob,而在另一个序号为 120 的ConfigSource中指定为Alice。然后当云原生应用程序查找customer_name时,应该检索到Bob的值。ConfigSource的序号可以通过相应的ConfigSource中的config_ordinal属性来定义,这表示包含的配置源的排名顺序。如果没有指定,则默认配置序号为 100。
有两种类型的配置源:默认配置源和自定义配置源,我们将在下一小节中讨论。
默认配置源
默认配置源是由 MicroProfile Config 规定的,所有 MicroProfile Config 实现都必须提供这些配置源。MicroProfile Config 规定必须支持三个默认配置源:
-
默认序号为 400 的系统属性
-
默认序号为 300 的环境变量
-
在类路径上找到的属性文件
META-INF/microprofile-config.properties,默认序号为 100
默认配置源的序号可以通过在配置源内部定义属性config_ordinal来覆盖。例如,如果您想将环境变量的序号设置为 500,您可以简单地定义一个环境变量的config_ordinal值为500。
环境变量映射规则
一些属性名不满足有效环境变量的条件,因为在某些情况下,app.name在环境变量中。搜索将在以下列表中找到匹配项后终止:
-
精确找到的属性名,例如
app.name。 -
如果属性名包含一些不是字母或数字的字符,将这些字符转换为
_*,然后转换所有字母为大写,转换后的属性名(app_name)将被找到。 -
如果属性名包含一些不是字母或数字的字符,将这些字符转换为
_*,然后转换所有字母为大写,转换后的属性名(APP_NAME)将被找到。
除了开箱即用的配置源之外,您可以使用文件、数据库等创建自己的配置源。这些配置源被称为自定义配置源,我们将在下一节中讨论。
自定义配置源
自定义配置源 是除了默认配置源之外您在应用程序中定义的配置源。要定义自定义配置源,您可以按照以下步骤操作:
-
按如下方式实现
ConfigSource接口:public interface ConfigSource { String CONFIG_ORDINAL = "config_ordinal"; int DEFAULT_ORDINAL = 100; default Map<String, String> getProperties() { Map<String, String> props = new HashMap<>(); getPropertyNames().forEach((prop) -> props.put(prop, getValue(prop))); return props; } Set<String> getPropertyNames(); default int getOrdinal() { String configOrdinal = getValue(CONFIG_ORDINAL); if (configOrdinal != null) { try { return Integer.parseInt(configOrdinal); } catch (NumberFormatException ignored) { } } return DEFAULT_ORDINAL; } String getValue(String propertyName); String getName(); }getPropertyNames()、getValue(String propertyName)和getName()方法是需要实现的方法。 -
使用以下任一方法注册这些函数的实现:
a). 创建一个
META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource文件,其中包含自定义实现的完全限定类名。b). 通过以下方式程序化地向
ConfigBuilder添加:ConfigBuider.withSources(ConfigSource… configSource)
有时,您有敏感的配置,需要将其存储在安全的地方。您可能需要考虑使用 HashiCorp Vault (www.vaultproject.io/),它管理密钥并存储敏感数据。如果您在 Vault 中存储了一些密钥属性,您可以将 Vault 添加为自定义配置源。接下来,我们将探讨另一种使用 Kubernetes ConfigMaps 和 Secrets 存储配置的方法,它们存储配置。
Kubernetes ConfigMaps 和 Secrets
Kubernetes ConfigMaps 和 Secrets 通常用于存储云原生应用程序的属性。您可以使用以下命令声明 Kubernetes ConfigMaps 或 Secrets:
-
创建一个名为
app-port的 ConfigMap:app-port, in your cluster, and that ConfigMap contains a key called port with a value of 9081. The –-from-literal is used to store individual name-value pairs in this ConfigMap. -
创建一个名为
app-credentials的 Secret:kubectl create secret generic app-credentials --from-literal username=Bob --from-literal password=TheBuilder此命令类似于创建 ConfigMap。Secret 与 ConfigMap 的主要区别在于,Secret 只显示文本的 Base64 编码版本,而不是明文。
在指定 Kubernetes ConfigMap 或 Secret 后,您可以通过 deployment yaml 文件将 ConfigMap 或 Secret 映射到环境变量:
env:
- name: PORT
valueFrom:
configMapKeyRef:
name: app-port
key: port
optional: true
- name: APP_USERNAME
valueFrom:
secretKeyRef:
name: app-credentials
key: username
- name: APP_PASSWORD
valueFrom:
secretKeyRef:
name: app-credentials
key: password
您可以使用以下代码来查找属性:
@Inject @ConfigProperty(name="port", defaultValue="9080") int port;
@Inject @ConfigProperty(name="app.username") String user;
@Inject @ConfigProperty(name="app.password") String pwd;
根据在环境变量中查找属性时定义的映射规则,port 将首先被搜索,然后是 PORT。由于 ConfigMap 属性 app_port 是可选的,因此它不需要在 ConfigMap 中定义。如果未找到,则将默认值 9080 分配给变量 port。至于其他属性 app.username,将首先搜索 app.username,然后是 app_username,最后是 APP_USERNAME。
到目前为止,我们已经学习了属性值的存储位置,但有时您可能需要删除属性值。在下一节中,我们将学习如何删除属性。
删除属性
为了删除属性,您只需从配置源中删除属性条目即可。然而,您可能无法更新配置源。在这种情况下,您可以定义一个具有更高序号的配置源,并使用空值定义属性。这实际上会删除配置属性。因此,任何对此属性的查找都将解析为异常。
我们已经介绍了在哪里指定配置属性。当我们存储配置属性值时,它们都是通过字符串表达的。你可能希望将字符串转换为其他类型,如int、float等。为了实现这一点,我们需要一些转换器。
转换器
转换器将字符串转换为目标类型。如果向转换器传递了 null 值,将抛出NullPointerException(NPE)。
正如存在多种配置源一样,也存在几种类型的转换器:
-
内置转换器:它们可以将字符串转换为原始类型、包装类型以及类类型。
-
数组转换器:它们可以将逗号分隔的字符串转换为数组。
-
T是从一个类派生的,该类包含以下之一:public static T of(String value) public static T valueOf(String value) public static T parse(CharSequence value ) public T(String value)如果找到多个方法,将使用上述列表中顶部的方法进行转换。
-
org.eclipse.microprofile.config.spi.Converter。- 通过创建一个包含自定义实现完全限定类名的
META-INF/services/org.eclipse.microprofile.config.spi.Converter文件或通过以下方式之一在程序中添加到ConfigBuilder来注册转换器:
ConfigBuider.withConverters(converter) ConfigBuilder.withConverter (Class<T> type, int priority, Converter<T> converter - 通过创建一个包含自定义实现完全限定类名的
存在多种类型的转换器。对于给定类型可能有多个转换器。但是问题来了,哪个转换器将被用来将配置字符串值转换为给定类型?这由转换器优先级决定。
转换器优先级
@javax.annotation.Priority注解。如果不存在,自定义转换器的优先级将默认为 100。优先级较高的转换器会覆盖优先级较低的转换器。
我们已经介绍了如何使用字符串指定配置属性的值,然后如何将字符串转换为特定类型。在下一节中,我们将学习如何在云原生应用程序中查找配置属性。
配置查找
我们已经了解到配置源包含以字符串指定的属性,转换器可以将字符串转换为目标类型。但是问题来了,如何检索配置属性?这可以通过程序化方式或通过注入来完成:
-
使用
getValue方法检索customer.age并将它转换为整数,这是通过程序化查找完成的:Config = ConfigProvider.getConfig(); int age = config.getValue("customer.age", int.class); -
customer.age:@Inject @ConfigProperty(name="customer.age") int age;
在你的云原生应用程序中,有时在项目不同阶段会指定不同的值。例如,你可能在不同的项目阶段使用不同的数据库。当项目通过不同的阶段时,与这些阶段相关的值将被注入到相应的配置属性中。这被称为配置配置文件。我们将在下一节中更详细地介绍这一内容。
理解配置配置文件
配置配置文件是一个重要的概念,可以用来表示项目阶段,如开发、测试、生产等。我们首先将讨论如何定义使用配置配置文件的属性,然后学习如何激活特定的配置文件。
配置文件感知属性
定义配置文件感知属性有两种方式:
-
使用命名约定%
. ),例如,在一个配置源中定义一个名为%dev.customer.name的属性。 -
在
microprofile-config-<profile name>.properties文件中定义属性。
激活配置文件
使用属性mp.config.profile指定活动配置文件,该属性可以包含在任何配置源中。如果有多个配置源指定此属性,则使用具有最高序号的配置源的值。
让我们看看几个例子。假设我们有一个以下配置源,包含以下属性:
%dev.discount=0.1
%testing.discount=0.6
%live.discount=0.2
discount=0.15
如果mp.config.profile的值设置为dev,则属性discount的值将解析为%dev.discount的值,即0.1。同样,如果活动配置文件是live,则属性discount的值将是0.2。如果没有定义活动配置文件,则属性discount的值将是0.15。
配置配置文件也适用于micorprofile-config.properties配置源文件。考虑以下由您的应用程序提供的properties文件:
META-INF\microprofile-config.properties
META-INF\microrpofile-config-dev.properties
META-INF\microprofile-config-testing.properties
META-INF\microprofile-config-live.properties
如果mp.config.profile的值设置为dev,则配置文件META-INF\microrpofile-config-dev.properties将被激活。其内容将合并到META-INF\microrpofile-config.properties中,并覆盖两个文件中存在的相同属性的属性值。
有时候一个属性的值引用另一个属性。我们称这种引用为配置引用。我们将在下一小节中介绍这个主题。
配置引用
属性值可能引用另一个属性的值,这被称为配置引用,也称为属性表达式。属性表达式的语法是${another.property}。
考虑以下示例:
customer.name=${forename}-${surname}
forename = Bob
surname = Johnson
customer.name的值将是Bob-Johnson。属性表达式也可以嵌套,格式为\({a\){n}}。在嵌套表达式中,内部表达式将首先被解析。
如前所述,一个属性可以在多个配置源中指定。
有时候你需要确定哪个配置源提供了值,以便在需要时更新有效值。在下一小节中,我们将学习如何找出哪个配置源是获胜的配置源。
我的属性值从哪里来?
有时候你可能想知道特定属性的值是由哪个配置源提供的,因为相同的属性可能存在于多个配置源中。如果你需要更新属性值,你需要更新获胜配置源的值。MicroProfile Config 提供了一个 API,ConfigValue,它允许你找到获胜的配置源。有几种方法可以获取指定属性的ConfigValue对象。你可以使用以下程序性查找来查找属性host的配置值:
ConfigValue configValueHost=ConfigProvider.getConfig()
.getConfigValue("host");
或者,您可以使用 CDI 来获取host属性的config值,如下所示:
@Inject @ConfigProperty(name="host") ConfigValue configValueHost;
然后,我们可以使用以下方法来检索有关获胜配置源及其值的一些信息:
String configSourceForHost = configValueHost.getSourceName();
String valueOfHost = configValueHost.getValue();
通常,一些属性是相关的,并且它们经常一起查找。如果这些属性被聚合并映射到特定的 Java 类型,这将非常有用。
聚合配置属性
当查找多个相关属性时,重复相同的配置查找语句可能会很繁琐。它们可能出现在不同的类中,其中一些属性可能是动态的,而另一些可能是静态的,这可能会导致不一致的状态。在这种情况下,将相关的配置属性聚合到一个 CDI bean 中,并使用@ConfigProperties注解该 CDI bean,以便同时执行属性查找,是一种最佳实践。
让我们通过一个例子来看看@ConfigProperties是如何工作的。以下是一个自定义的ConfigSource:
config_ordinal=220
customer.forename=Bob
customer.surname=Builder
要查找与特定客户相关的属性,我们可以使用@ConfigProperites,如下所示:
@ApplicationScoped
@ConfigProperties(prefix = "customer")
public class ConfigProps {
private String forename;
private String surname;
public String getForename() {
return forename;
}
public String getSurname() {
return surname;
}
}
在查找属性时,您需要使用@ConfigProperites(prefix="customer")注解。如果前缀值与 CDI bean 前缀相同,则可以省略前缀。如果您指定了不同的前缀值,则指定的前缀值将覆盖 CDI bean 上定义的值,并使用指定的前缀来查找属性。
到目前为止,config对象是由一个配置规范实现提供的,它加载可用的配置源和转换器。对于一些需要控制使用哪些配置源和转换器的先进用例,您可能想自己构建config对象。MicroProfile Config 也为这种用例提供了灵活性。
自己构建 Config 实例
要构建一个 Config 实例,您需要提供配置源和转换器。MicroProfile Config 提供了一个构建器,用于构建配置对象,按照以下步骤进行:
-
首先,我们需要创建一个构建器,如下面的代码片段所示,通过创建
ConfigProviderResolver的一个实例,然后调用getBuilder()方法来创建一个构建器的实例:ConfigProviderResolver resolver = ConfigProviderResolver.instance(); ConfigBuilder builder = resolver.getBuilder(); -
然后,我们将添加配置源和转换器到构建器中,然后调用
build()方法来构建一个config对象,如下所示:Config = builder.addDefaultSources().withSources(aSource) .withConverters(aConverter).build(); -
接下来,我们需要注册
config对象,以便我们始终可以向相同的classloader对象提供相同的config对象,如下所示://register the config with the specified classloader resolver.registerConfig(config, classloader); -
最后,当我们完成对
config对象的使用后,我们需要将其释放,如下所示://release this config when no longer needed. resolver.releaseConfig(config);
如果一个配置对象与多个类加载器相关联,在释放配置对象时,必须删除它的所有实例。
我们已经涵盖了 MicroProfile Config 最有用的功能。要使用 MicroProfile Config 的 API,你需要指定 Maven 或 Gradle 依赖项。我们将在下一节中更详细地介绍这一点。
使 MicroProfile Config API 可用
MicroProfile Config API JARs 可以为 Maven 和 Gradle 项目提供。如果你创建了一个 Maven 项目,你可以直接将以下内容添加到你的pom.xml中:
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
<version>2.0</version>
</dependency>
或者,如果你创建了一个 Gradle 项目,你需要添加以下依赖项:
dependencies {
providedCompile org.eclipse.microprofile.config :microprofile-config-api:2.0
}
你已经学会了如何配置你的云原生应用。下一步是让你的应用具有弹性。在下一节中,我们将详细介绍如何使用 MicroProfile 容错性来让你的应用具有弹性。
使用 MicroProfile 容错性使云应用具有弹性
为什么你需要关注弹性?对于关键任务应用,非常短暂的中断可能会给你带来巨大的损失。此外,如果你的应用过于脆弱且不具有弹性,客户满意度将会下降。因此,你应该考虑构建具有弹性的应用,这意味着它们将在各种情况下无中断地运行。MicroProfile 容错性(源代码位于github.com/eclipse/microprofile-fault-tolerance)引入了几个弹性策略,可以帮助你构建具有弹性的应用。这些策略可以应用于 CDI beans。它还提供了一种通过@Asynchronous注解异步执行方法调用的方式。
@Asynchronous
@Asynchronous注解可以放置在 CDI bean 类或 CDI bean 类的方法上。如果放置在类上,这意味着这个类上声明的所有方法都将在一个单独的线程上执行。使用@Asynchronous注解的方法必须返回一个Future或CompletionStage值,否则将抛出FaultToleranceDefinitionException。以下是一个代码片段来展示其用法:
@Asynchronous
public CompletionStage<String> serviceA() {
return CompletableFuture.completedFuture("service a");
}
上述示例意味着serviceA()的调用将在不同的线程上执行,然后返回CompletionStage。如果方法返回Future,方法调用始终被视为成功,除非方法抛出异常。如果方法返回CompletionStage,方法调用只有在CompletionStage完成且没有任何异常的情况下才被视为成功。
注意
当使用@Asynchronous注解时,最佳实践是返回CompletionStage,这允许在异常返回时调用其他容错策略。
但是当你遇到暂时的网络故障时,你会怎么做?第一反应是再试一次,这正是下一个注解所帮助的。
@Retry
MicroProfile 容错性通过@Retry注解提供重试功能。当使用此注解时,你可以指定以下属性:
-
maxRetries:这表示最大重试次数。 -
delay:这表示每次重试之间的延迟。 -
delayUnit:这指定了延迟的时间单位。 -
maxDuration:这表示整体重试的最大持续时间。 -
durationUnit:这指定了持续时间单位。 -
jitter:这表示每个延迟的随机间隔。 -
jitterDelayUnit:这指定了抖动延迟单位。 -
retryOn:这表示导致重试的失败。 -
abortOn:这指定了跳过重试的失败。
您可以在任何类或业务方法上添加 @Retry 注解(docs.jboss.org/cdi/spec/2.0/cdi-spec.html#biz_method)。让我们看看以下示例:
@Retry(maxRestries=5, delay=400, maxDuaration=4000, jitter=200, retryOn=Exception.class, abortOn=
IllegalArgumentException.class)
public void callService(){
//do something doSomething();
}
在上述代码片段中,@Retry 注解表示以下内容:
-
除了
IllegalArgumentException之外的异常将触发Retry操作。 -
最大重试次数为
5,最大持续时间为4000毫秒(毫秒)。当满足任一条件时,Retry操作将终止。 -
重试之间的延迟是
200毫秒(抖动)和600毫秒(延迟+抖动)。
有时您可能不想不断重试;您可能希望快速失败并在时间限制内返回给调用者,例如,如果调用者只分配了有限的等待时间,无法承担等待请求返回的时间。@Timeout 注解被引入来强制操作在指定期间内返回。
@Timeout
@Timeout 注解指定了最大响应时间。如果没有指定超时时间,可能会发生不确定的等待。使用 @Timeout 注解后,受影响的操作需要在指定的时间段内返回,否则将抛出 TimeoutException。此注解可以用于类或任何业务方法。让我们看看以下代码示例:
@Timeout(700)
public String getServiceName() {
//retrieve the backend service name
}
在上述代码片段中,getServiceName() 操作将在 700 毫秒内返回,或者如果操作计算时间超过 700 毫秒,将抛出 TimeoutException。
如果服务不工作,您可能不想等待指定的超时时间,然后反复得到 TimeoutException。在一定的失败时间后快速失败会更好。快速失败为后端恢复提供时间,请求可以立即得到反馈。这正是 @CircuitBreaker 所做的。
@CircuitBreaker
断路器模式帮助调用快速失败,并通过防止请求到达后端为后端恢复提供时间。断路器模式是如何工作的?它非常简单。断路器模式中的电路就像一个电路。如果电路是断开的,就没有电。闭合电路意味着正常服务。断路器有三个状态:
-
在连续请求或滚动窗口(称为
requestVolumeThreshold)中的failureRatio数量,当断路器跳闸时,断路器将从关闭状态转换为打开状态。 -
CircuitBreakerOpenException。在指定延迟期后,断路器将转换为半开状态。 -
当
successThreshold被满足时,断路器将转换为 关闭 状态。
断路器有以下参数:
-
requestVolumeThreshold指定了断路器将被评估的滚动窗口的大小。如果requestVolumeThreshold的值为 10,这意味着断路器只有在 10 次请求之后才会进行检查。 -
failureRatio控制滚动窗口内的失败率。如果失败率等于或高于failureRatio参数,则断路器将跳闸打开。 -
successThreshold指定了当断路器从半开状态转换为关闭状态时的标准。如果successThreshold的值为2,这意味着在 2 次成功请求之后,断路器将从半开状态转换为关闭状态。 -
delay和delayUnit指示断路器保持打开状态的时间。断路器不会永远保持打开。 -
failOn指定了一些被认为是失败的异常。 -
skipOn指定了一些异常,这些异常不会被计入断路器的贡献。
可以通过 @CircuitBreaker 注解指定断路器模式。@CircuitBreaker 注解可以放置在类或业务方法上,以避免重复失败。您可以在注解上配置参数,如下所示:
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio=0.5, successThreshold = 3, delay = 2, delayUnit=ChronoUnit.SECONDS, failOn={ExceptionA.class, ExceptionB.class}, skipOn=ExceptionC.class)
在上述代码片段中,这意味着如果连续 10 次请求中有 5 次或更多(10*0.5)失败,则断路器将跳闸打开。在参数 delay 和 delayUnit 指定的 2 秒后,电路将转换为半开状态。在 3 次(successThreshold)连续成功之后,断路器将转换为关闭状态。
在决定一个调用是否应该被视为失败时,标准是调用中抛出的异常必须可以分配给 ExceptionA 或 ExceptionB,但不能分配给 ExceptionC。如果一个异常可以分配给 ExceptionC,那么在计算断路器时,这次执行将被视为成功而不是失败。
断路器的成功
由 @CircuitBreaker 注解标记的成功并不意味着操作返回正常。它仅意味着返回被断路器视为成功。例如,如果返回的异常可以分配给 skipOn 中的任何异常,则该异常不计为失败,而是计为成功。
@CircuitBreaker注解的作用域是每个类每个方法,这意味着特定类的所有实例对于相同的方法共享同一个断路器。当在 CDI bean 上应用@CircuitBreaker注解时,bean 的 CDI 作用域无关紧要。特定方法的调用共享同一个断路器。
在一个关键任务服务中,确保服务具有故障隔离能力,防止系统某一部分的故障级联到整个系统,这一点非常重要。这被称为舱壁模式。
舱壁模式
舱壁模式是为了防止一个故障级联,从而导致整个应用程序崩溃。舱壁模式是通过限制访问资源的并发请求数量来实现的。有两种不同的舱壁类型:信号量隔离和线程池隔离。
信号量隔离
信号量隔离限制了并发请求数量到指定的数量。你可以直接使用@Bulkhead(n)来限制最大请求,其中n是并发请求数量。额外的请求n+1将因BulkheadException而失败。
然而,有时你可能希望为额外的请求提供一个等待队列。对于这个需求,你需要使用线程池隔离。
线程池隔离
线程池隔离也限制了并发请求数量。它为方法执行分配一个新的线程。此外,它还有一个等待队列来存储额外的请求。只有当等待队列满时,才会抛出BulkheadException异常。
要定义线程池隔离,你需要使用@Bulkhead与@Asynchronous一起使用,如下所示:
@Asynchronous
@Bulkhead(value=5, waitingTaskQueue=6)
public CompletionStage<String> serviceA() {
}
如上述代码片段所示,舱壁的大小为5,允许 5 个并发请求访问serviceA()方法。额外的请求将被排队在等待队列中。当等待队列满时,额外的请求将导致抛出BulkheadException异常。
与@CircuitBreaker注解类似,@Bulkhead注解的作用域是每个类每个方法,这意味着特定类的所有实例对于相同的方法共享同一个舱壁。当在 CDI bean 上应用@Bulkhead注解时,bean 的 CDI 作用域无关紧要。特定方法的调用共享同一个舱壁。
除了重试之外,我们之前讨论的容错策略都是关于优雅地失败。然而,有时你可能想向调用者返回一个合理的响应。在这些情况下,你应该提供一个替代答案。这就是回退策略的作用。
回退
@Fallback注解。@Fallback注解具有以下参数:
-
value:这指定了一个实现FallbackHandler接口的类。 -
fallbackMethod:此方法指定回退方法名称。回退方法必须在应用@Fallback注解的方法所在的同一类中定义。 -
applyOn:这定义了回退策略应该应用的标准。 -
skipOn:这定义了回退策略应该跳过的标准。
value和fallbackMethod参数是互斥的。如果你在@Fallback注解上同时指定了这两个参数,将会抛出FaultToleranceDefinitionException异常。要指定备份操作,你需要定义fallbackMethod或value参数之一。
fallbackMethod签名和返回类型必须与定义回退策略的方法相匹配。如果你想定义一个处理多个回退策略的处理程序,应该使用value参数。
你可以使用skipOn或applyOn来指定应该触发回退操作的异常。正如其名,所有在skipOn下列出的异常及其子类都将绕过回退,而applyOn列出的异常及其子类应该触发回退操作。当skipOn和applyOn一起使用时,skipOn具有优先权。
让我们看看以下代码片段,以了解回退策略的示例:
@Fallback(fallbackMethod="myFallback",
applyOn={ExceptionA.class, ExceptionB.class}, skipOn={ExceptionAsub.class})
public String callService() {
...
}
public String myFallback() {
...
}
在上述代码片段中,如果callService()方法抛出异常,并且这个异常可以被赋值给ExceptionAsub,那么异常将被重新抛出,回退方法myFallback将不会执行。然而,如果抛出的异常不能被赋值给ExceptionAsub,但可以被赋值给ExceptionA或ExceptionB,那么回退方法myFallback将被触发。
到目前为止,我们已经涵盖了所有的回退策略。你可能已经意识到所有策略都是注解。实际上,这些注解是 CDI 拦截器绑定,这意味着它们仅在 CDI bean 的业务方法调用上工作,如jakarta.ee/specifications/cdi/2.0/cdi-spec-2.0.html#biz_method中所述。
CDI 拦截器有优先级。在容错中,一些实现可能为所有拦截器绑定提供一个拦截器,而其他实现可能提供多个拦截器。容错规范声明基本优先级为Priority.PLATFORM_AFTER (4000)+10,即4010。如果实现提供了多个拦截器,优先级范围应在基本优先级和基本优先级+40 之间,以便其他应用程序拦截器可以根据它们是否希望在容错拦截器之前或之后调用来相应地定义它们的优先级。你可以通过属性mp.fault.tolerance.interceptor.priority更新基本优先级。
带有容错注解的方法也可以指定任何其他的拦截器绑定。拦截器的调用顺序将由拦截器的优先级决定。
到目前为止,我们已经涵盖了所有的容错注解。你可能想知道是否可以一起使用这些注解。当然可以。我们将在下一节中详细讨论。
使用容错注解一起使用
使用注解一起实现多个容错能力,例如同时设置Retry和Timeout,是可行的。让我们看看以下示例:
@GET
@Path("/{parameter}")
@Asynchronous
@CircuitBreaker
@Retry
@Bulkhead
@Timeout(200)
@Fallback(fallbackMethod = "myFallback")
public CompletionStage<String> doSomething(@PathParam
("parameter") String parameter) {
//do something
}
上述代码片段表示doSomething()操作应用了所有的容错策略。如果此方法抛出异常,将按以下顺序应用以下容错策略:
-
检查
CircuitBreaker是否开启。 -
如果没有开启,计时器将开始记录时间长度。
-
尝试在
Bulkhead中获取一个槽位。 -
如果已经有 10 个正在运行的任务,它将在等待队列中排队,该队列有 10 个槽位。然而,如果没有空闲槽位在等待队列中,将抛出
BulkheadException异常。如果有槽位在等待队列中,它将等待被调度。在等待期间,如果超时时间超过200毫秒,将抛出TimeoutException。抛出的异常将被记录在CircuitBreaker记录中,然后触发Retry。回到步骤 1进行Retry。
在Retry尝试耗尽后,将触发回退策略。
你可能已经注意到每个注解的参数都有默认值。你还可以在注解上指定值。你在想这些值是否可配置吗?我们将在下一节中讨论这个问题!
容错配置
好消息是,注解上的所有参数都是可配置的。这些值可以是全局覆盖或单独覆盖。这是通过MicroProfile Config实现的。所有参数都是属性。它们的值可以在任何配置的配置源中指定。
覆盖参数值
要覆盖单个注解参数,你可以使用方法级别配置、类级别配置或全局配置,如这里所述:
-
方法级别配置:使用格式<完全限定的类名>/<方法名>/<注解名>/<参数名>进行指定。
-
类级别配置:使用格式<完全限定的类名>/<注解名>/<参数名>进行指定。
-
全局配置:使用格式<注解名>/<参数名>进行指定。
让我们通过一个示例来进一步解释:
package cloudnative.sample.fault.tolerance;
public class FaultToleranceDemo {
@Timeout(500)
@Retry(maxRetries = 6)
@GET
public String invokeService() {
//do something
}
}
此代码片段将Timeout设置为500毫秒,最多重试6次。要设置Timeout为300毫秒且最多重试10次,您可以在任何配置源中指定以下方法级别配置属性:
cloudnative.sample.fault.tolerance.FaultToleranceDemo/invokeService/Timeout/value=300
cloudnative.sample.fault.tolerance.FaultToleranceDemo/invokeService/Retry/maxRetries=10
或者,如果你想更新此类的所有超时和最大重试次数,你可以省略方法名称,这意味着配置适用于所有具有相应注解的方法。然后,在配置的配置源中指定以下类级别配置:
cloudnative.sample.fault.tolerance.FaultToleranceDemo/Timeout/value=300
cloudnative.sample.fault.tolerance.FaultToleranceDemo/Retry/maxRetries=10
有时,你可能想为单个应用程序下的所有类指定相同的超时值。这很简单。只需省略完全限定的类名。因此,你可以使用以下全局配置来实现这一点:
Timeout/value=300
Retry/maxRetries=10
有时,你的云基础设施,例如Istio,我们将在第七章“与 Open Liberty、Docker 和 Kubernetes 一起的 MicroProfile 生态系统”中讨论,提供一些容错能力。然而,云基础设施无法提供回退能力,因为基础设施需要业务知识。如果你更喜欢使用 Istio 提供的容错能力,你可能想关闭一些 MicroProfile 容错能力。如果不关闭,你将获得两种容错功能,它们将相互干扰。
禁用容错策略
如果你想使用 Istio 提供的容错策略,你可以指定以下属性来关闭除Fallback之外的所有容错策略:
MP_Fault_Tolerance_NonFallback_Enabled=false
要在方法上禁用特定策略,请指定以下方法级别属性:
<fully.qualified.class.name>/<method.name>/<annotation>/enabled=false
在前面的例子中,为了在invokeService()方法上关闭Timeout,请指定以下方法级别配置:
cloudnative.sample.fault.tolerance.FaultToleranceDemo/invokeService/Timeout/enabled=false
或者,为了在类中关闭Timeout,请指定以下类级别配置。
cloudnative.sample.fault.tolerance.FaultToleranceDemo/Timeout/enabled=false
要禁用云原生应用的Timeout功能,请指定以下属性:
Timeout/enabled=false
如果指定了多个配置,方法级别的配置具有更高的优先级,其次是类级别配置,然后是全球配置。
到目前为止,你已经学习了如何将容错策略应用于某些操作并配置策略。然而,这些策略是为了在某些标准下被触发的。很可能没有任何策略被利用。你可能非常想了解是否有任何容错策略被激活。为了满足这一需求,我们有了容错指标。让我们在下一节中看看它们。
容错指标
当与 MicroProfile Metrics 一起使用时,MicroProfile Fault Tolerance 能够为 Retry、Timeout、CircuitBreaker、Bulkhead 和 Fallback 策略提供一些有用的指标。在以下表格中,我列出了所有相关的指标。
以下表格显示了应用容错时的一般指标:

表 5.1 – 方法调用指标
如果使用了@Retry,将提供重试的指标:

表 5.2 – 重试指标
如果使用@Timeout,将提供以下指标:

表 5.3 – 超时指标
如果使用@CircuitBreaker,您将看到以下指标:

表 5.4 – 电路断路器指标
如果使用@Bulkhead,您将能够看到以下指标:

表 5.5 – 防火墙指标
您已经了解了所有容错功能。您可能想知道如何开始使用容错提供的 API。我们将在下一节中讨论这个问题。
使 MicroProfile 容错 API 可用
MicroProfile 容错 API JAR 可以为 Maven 和 Gradle 项目提供。如果您创建了一个 Maven 项目,您可以直接将以下内容添加到您的pom.xml中:
<dependency>
<groupId>
org.eclipse.microprofile.fault.tolerance
</groupId>
<artifactId>microprofile-fault-tolerance-api</artifactId>
<version>3.0</version>
</dependency>
如果您创建了一个 Gradle 项目,您需要添加以下依赖项:
dependencies {
providedCompile org.eclipse.microprofile.fault.tolerance: microprofile-fault-tolerance-api:3.0
}
您现在已经学会了如何使您的云原生应用程序具有弹性。下一步是记录您的应用程序。您将在下一节中学习如何使用 MicroProfile OpenAPI 记录您的应用程序。
使用 MicroProfile OpenAPI 记录云原生应用程序
如同在第二章中提到的,MicroProfile 如何适应云原生应用程序?,当您管理数十或数百个云原生应用程序时,您可能会难以记住特定云原生应用程序的功能。您将需要为它们提供文档。有了文档化的端点,一些客户端可以发现并调用它们。
MicroProfile OpenAPI(源代码位于github.com/eclipse/microprofile-open-api/)提供了一套注解和编程模型,使您能够记录云原生应用程序,然后生成符合OpenAPI v3 规范的文档(github.com/OAI/OpenAPI-Specification)。OpenAPI v3 规范定义了一组用于记录和公开 RESTful API 的接口。MicroProfile OpenAPI 采用了 OpenAPI 规范,并进一步简化了 OpenAPI 模型,使得 Java 开发者更容易记录他们的云原生应用程序。
MicroProfile OpenAPI 提供了三种方式来记录云原生应用程序:
-
通过在 JAX-RS 操作上应用MicroProfile OpenAPI注解
-
使用MicroProfile OpenAPI的编程模型来提供OpenAPI模型树
-
使用预先生成的OpenAPI文档
在下一节中,我们将更详细地介绍这三个机制。
在 JAX-RS 操作上应用 MicroProfile OpenAPI 注解
为您的 JAX-RS 操作生成文档的简单方法是添加 MicroProfile OpenAPI 注解。以下列出了一些有用且广泛使用的注解:
![Table 5.6 – MicroProfile OpenAPI annotations
![Table 5.6.jpg]
Table 5.6 – MicroProfile OpenAPI annotations
以下是在 IBM StockTrader 应用程序 Trade-History 中使用 OpenAPI 注解记录操作的示例:
@APIResponses(value = {
@APIResponse(
responseCode = "404",
description = "The Mongo database cannot be found. ", content = @Content( mediaType = "text/plain")),
@APIResponse(
responseCode = "200",
description = "The latest trade has been retrieved successfully.",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = Quote.class)))})
@Operation(
summary = "Shows the latest trade.",
description = "Retrieve the latest record from the mongo database."
)
public String latestBuy() {
. . .
}
上述代码片段使用 @APIResponse 注解记录了两个响应:404 和 200。@Operation 注解记录了操作的目的。使用前面的注解生成的 OpenAPI 文档的详细信息如下:
---
openapi: 3.0.3
info:
title: Generated API
version: "1.0"
servers:
- url: http://localhost:9080
- url: https://localhost:9443
paths:
/data/latestBuy:
get:
summary: Shows the latest trade.
description: Retrieve the latest record from the mongo database.
responses:
"404":
description: 'The Mongo database cannot be found'
content:
text/plain: {}
"200":
description: The latest trade has been retrieved successfully.
content:
application/json:
schema:
$ref: '#/components/schemas/Quote'
components:
schemas:
Quote:
type: object
properties:
date:
type: string
price:
format: double
type: number
symbol:
type: string
time:
format: int64
type: integer
在上述文档中,使用 @Operation 注解中的信息解释了端点路径和 get 操作描述。进一步地,从 @APIResponse 注解提供的信息中描述了 200 和 404 的响应。至于返回代码 200 的响应,通过 @Schema 注解引用了模式 Quote,因此 $ref: '#/components/schemas/Quote' 显示为模式引用。该引用指向 components/schemas/Quote 部分,显示了模式 Quote 的详细信息。
在不使用任何 MicroProfile OpenAPI 注解的情况下,MicroProfile OpenAPI 仍然为所有 JAX-RS 端点生成 OpenAPI 文档,并包含最小信息。以下是在未应用 OpenAPI 注解的情况下 latestBuy 端点的示例。它只列出了成功返回代码 200 的最小信息:
---
openapi: 3.0.3
info:
title: Generated API
version: "1.0"
servers:
- url: http://localhost:9080
- url: https://localhost:9443
paths:
/data/latestBuy:
get:
responses:
"200":
description: OK
content:
application/json:
schema:
type: string
除了使用 MicroProfile OpenAPI 注解外,您还可以使用编程模型生成文档。我们将在下一节中讨论这个问题。
使用编程模型生成文档
有时,可能无法在 JAX-RS 操作上放置 MicroProfile OpenAPI 注解。在这种情况下,您可以从 MicroProfile OpenAPI 提供自己的 OASModelReader 实现。OASModelReader API 提供了一种从头开始构建 OpenAPI 文档的方法。按照提到的步骤构建 OpenAPI 模型树:
-
实现
org.eclipse.microprofile.openapi.OASModelReader接口。您可以在以下示例中看到:github.com/eclipse/microprofile-open-api/wiki/OASModelReader-Samples。 -
使用
mp.openapi.model.reader配置键注册实现,并将配置存储在配置源中。 -
如果提供完整的 OpenAPI 模型树,设置配置
mp.openap.scan.disabled=true。
链接 github.com/eclipse/microprofile-open-api/wiki/OASModelReader-Samples 提供了实现 OASModelReader 的示例。在 步骤 2 和 步骤 3 中,您可以在 META-INF/microprofile-config.properties 或作为系统属性中指定属性。
使用预生成的 OpenAPI 文档
有时,您会首先使用像 Swagger Editor(editor.swagger.io/)这样的编辑器来编写 OpenAPI 文档。文档必须命名为 openapi,并带有 yml、yaml 或 json 文件扩展名,并放置在 MEAT-INF 目录下。如果文档完整,您可以设置属性 mp.openap.scan.disabled=true。否则,将执行扫描操作以发现任何 MicroProfile OpenAPI 注解。
到目前为止,我们简要介绍了创建 OpenAPI 文档的三种方法。这些文档可以通过 URL host_name/port_number/openapi 查看。Open Liberty,将在第七章中讨论,即 使用 Open Liberty、Docker 和 Kubernetes 的 MicroProfile 生态系统,集成了 Swagger UI,因此您可以访问 host_name/port_number/openapi/ui 以获得图形用户界面视图,在那里您可以提供参数然后直接调用单个端点。有关此 UI 的更多信息将在第九章中介绍,即 部署和第 2 天操作。
Cloud-native 应用程序开发人员可能希望删除或更新 OpenAPI 文档的某些元素。这是通过过滤器完成的,该过滤器在 OpenAPI 文档创建后调用。我们将在下一节中讨论过滤器。
应用过滤器到 OpenAPI 文档
如果您想向 OpenAPI 文档添加一些信息,您可以创建一个过滤器。该过滤器需要实现 OASFilter 接口,它提供了一种更新 OpenAPI 文档的方式。让我们看看以下过滤器:
public class MyOASFilter implements OASFilter {
@Override
public void filterOpenAPI(OpenAPI openAPI) {
openAPI.setInfo(OASFactory.createObject(Info.class) .title("Stock App").version("1.0").description ("App for displaying stocks.").license (OASFactory.createObject(License.class) .name("Apache License 2.0").url ("https://www.apache.org/licenses/ LICENSE-2.0"))); //1
openAPI.addServer(OASFactory.createServer() .url("http://localhost:{port}").description ("Open Liberty Server.").variables(Collections .singletonMap("port",OASFactory .createServerVariable().defaultValue("9080") .description("HTTP port.")))); //2
}
}
在上述代码片段中,filterOpenAPI() 方法过滤 OpenAPI 元素。该方法仅作为特定过滤器的最后一个方法被调用。它提供了 OpenAPI 文档的 info 和 servers 元素。
在您创建了过滤器类之后,下一步是在配置源中指定配置 mp.openapi.filter=com.acme.MyOASFilter,例如 microprofile-config.properties。
MicroProfile OpenAPI 实现按以下顺序生成文档:
-
实现通过
mp.openapi前缀检索配置值。 -
然后,它调用
OASModelReader。 -
它检索名为
openapi.yml、openapi.yaml或openapi.json的静态 OpenAPI 文件。 -
它处理 MicroProfile OpenAPI 注解。
-
最后,它通过
OASFilter过滤 OpenAPI 文档。
如[步骤 1]所述,MicroProfile OpenAPI 可以定义带有 mp.openapi 前缀的几个配置。我们将在下一节中介绍这些配置。
MicroProfile OpenAPI 配置
MicroProfile OpenAPI 为应用程序开发者提供了在生成 OpenAPI 文档时配置过程的灵活性。配置项列在以下表格中:

表 5.7 – MicroProfile OpenAPI 配置
现在我们已经了解了MicroProfile OpenAPI的工作原理。MicroProfile OpenAPI 的实现将基于 MicroProfile OpenAPI 策略生成一个 OpenAPI 文档。你可以在哪里查看完全处理后的 OpenAPI 文档?在下一节中,我们将讨论 OpenAPI 文档的访问位置。
查看 OpenAPI 文档
完全处理后的 OpenAPI 文档必须在根 URL /openapi 上可用,作为一个 HTTP Get操作,例如http://localhost:9080/openapi。默认的文档格式是 YAML。如果响应包含值为application/json的Content-Type头,则需要支持 JSON 格式。Open Liberty 支持查询参数格式,允许你指定你想要 YAML 还是 JSON 格式。例如,端点http://localhost:9080/openapi?format=JSON以 JSON 格式显示 OpenAPI 文档,而端点localhost:9080/openapi?format=YAML则以 YAML 格式显示 OpenAPI 文档。
Open Liberty 提供了一个用于 OpenAPI 文档的 UI,你可以调用其中的端点。该 UI 在根 URL /openapi/ui 上可用。
你已经学会了 MicroProfile OpenAPI 的工作原理以及在哪里可以找到 OpenAPI 文档。现在,你准备好使用 MicroProfile OpenAPI 了。在下一节中,我们将讨论如何使 API 对你的 Maven 和 Gradle 项目可用。
使 MicroProfile OpenAPI API 可用
要使用 MicroProfile OpenAPI API,你需要使这些 API 对你的应用程序可用。如果你创建了一个 Maven 项目,你可以直接将以下内容添加到你的pom.xml文件中:
<dependency>
<groupId>org.eclipse.microprofile.openapi</groupId>
<artifactId>microprofile-openapi-api</artifactId>
<version>2.0</version>
</dependency>
或者,如果你创建了一个 Gradle 项目,你需要添加以下依赖项:
dependencies {
providedCompile org.eclipse.microprofile.openapi:
microprofile-openapi-api:2.0
}
通过这种方式,你已经学会了如何记录你的云原生应用程序。下一步是保护你的应用程序。我们将学习如何借助 MicroProfile JWT(github.com/eclipse/microprofile-jwt-auth)来保护你的应用程序。
使用 MicroProfile JWT 保护云原生应用程序
MicroProfile JWT 利用JSON Web Token(JWT)和一些额外的声明,用于端点的基于角色的访问控制,以帮助保护云原生应用。保护云原生应用通常是必须具备的功能。通常情况下,云原生应用提供敏感信息,这些信息只能由特定用户组访问。如果不保护云原生应用,任何人都可以访问信息。Jakarta Security(源代码位于github.com/eclipse-ee4j/security-api),是 Jakarta EE 下的一个规范(jakarta.ee/specifications/security/),可用于保护云原生应用。
在以下示例中,checkAccount 方法通过 Jakarta Security API 的@RolesAllowed进行安全保护。此方法只能由具有StockViewer或StockTrader访问组的客户端调用。所有其他用户都会被拒绝,如下所示:
@RolesAllowed({ "StockViewer", "StockTrader" })
@GET
public String checkAccount() {
return "CheckAccount";
}
或者,您可以使用web.xml配置直接保护端点。在以下代码片段中,它定义了两个角色,StockViewer和StockTrader,并且它们可以访问所有GET操作,其中StockViewer只能访问只读操作,而StockTrader可以访问所有操作:
<security-role>
<description>Group with read-only access to stock
portfolios</description>
<role-name>StockViewer</role-name>
</security-role>
<security-role>
<description>Group with full access to stock
portfolios</description>
<role-name>StockTrader</role-name>
</security-role>
<security-constraint>
<display-name>Account read-only security</display-name>
<web-resource-collection>
<web-resource-name>Account read-only methods</web-resource-name>
<description>Applies to all paths under the context root (this service specifies the account as a path param)</description>
<url-pattern>/*</url-pattern>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<description>Roles allowed to access read-only operations on accounts</description>
<role-name>StockViewer</role-name>
<role-name>StockTrader</role-name>
</auth-constraint>
</security-constraint>
在端点被保护之后,我们接下来需要考虑如何让客户端调用受保护的后端。为了回答这个问题,我们首先将讨论云原生应用的独特应用安全方面。
云原生应用安全与传统应用不同,因为云原生应用通常是无状态的,服务器端可能无法在客户端持久化任何状态。此外,后端可能有不同的实例,每个后续客户端请求可能不会击中相同的后端实例。正如您所看到的,在后端存储客户端数据是有问题的。因此,建议的方法是通过每个请求传递安全相关信息。
后端服务将为每个请求创建一个安全上下文,并执行认证和授权检查。这意味着安全相关信息需要包括认证和授权的详细信息。在下一节中,我们将探讨实现此目标所使用的技术。
我们使用什么技术来保护云原生应用?
用于云原生应用安全的技术基于OAuth2、OpenID Connect和JSON Web Token(JWT)标准:
-
OAuth2:一个授权框架,用于控制对受保护资源(如云原生应用)的授权。OAuth2 是关于用户授权的。
-
OpenID Connect (OIDC):一个建立在 OAuth 2.0 协议之上的认证框架。它允许第三方应用程序(如 Google 或 Facebook)验证最终用户的身份,并获取一些用户信息。OIDC 是关于用户认证的。
-
JWT:一种紧凑、URL 安全的方式来在双方之间传输声明。它包含一组声明,表示为一个 JSON 对象,该对象是 base64url 编码的、数字签名的(JWS),并且可选地加密(JWE)。
JWT 格式
JWT 可以用于传播身份验证的 ID 和授权的用户权限。它被 OAuth2 和 OpenID Connect 所使用。JWT 最重要的特性是数据本身是自我描述和可验证的。它提供了一种防篡改的方式来传递信息。在 JWT 创建时,发行者使用其 私钥 对 JWT 进行签名。在接收到 JWT 后,接收者可以使用匹配的 公钥 来验证 JWT,以确保 JWT 没有被篡改。
JWT 是一个通用抽象术语。它可以是 签名 JWT 或 加密 JWT。签名 JWT 被称为 JWS(JSON Web 签名),而加密 JWT 被称为 JWE(JSON Web 加密)。JSON Web 密钥(JWK)是一个表示加密密钥的 JSON 对象,用于解密 JWE 或 JWS 以验证签名。
在生成安全令牌 - JWT - 之后,通常使用基于令牌的认证来允许系统根据安全令牌做出认证和授权决策。对于云原生应用程序,基于令牌的认证机制为安全控制和安全令牌在云原生应用程序之间传播用户和权限信息提供了一种轻量级的方法。由于 JWT 令牌用于认证和授权目的,MicroProfile JWT 引入了两个新的声明:
-
upn:此声明指定用户主体。
-
groups:此声明列出组名。
JWT 中可以包含许多声明。以下声明是 MicroProfile JWT 所必需的,而从 MicroProfile JWT Propagation 版本 1.2 开始,组声明是可选的:
-
iss:此声明指定令牌发行者。 -
iat:此声明指定了令牌签发的时间。 -
exp:此声明表示令牌的过期时间。 -
upn(或preferred_username或sub):此声明指定主体名称。如果upn不存在,则回退到preferred_username。如果preferred_username不存在,则回退到sub。
除了上述必需的声明外,还有一些可选的声明列在这里:
-
aud:JWT 可以评估的端点。 -
jti:此 JWT 的唯一标识符。 -
sub:此 JWT 的主体的唯一标识符。 -
preferred_user_name:此 JWT 的首选用户名。
如前所述,JWT 有两种不同类型:JWS 和 JWE。JWS 非常流行。我们将更详细地讨论它。
JWS 的详细说明
JWS 是 JWT 的一个突出格式。JWS 有三个部分,它们由点分隔。以下是一个 JWS 的示例,您可以在第一行找到第一个点,在第七行找到第二个点:
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsImF1ZCI6ImNsb3V kLW5hdGl2ZS1hcHAiLCJqdGkiOiIwZmQwODljYi0xYzQ1LTRjMzAtOWIyMy02YW E0ZmQ1ZTcwYjUiLCJleHAiOjE2MTk2OTk5MDYsImlhdCI6MTYxOTY5OTg3Niwic 3ViIjoiRW1pbHkiLCJ1cG4iOiJFbWlseSIsInByZWZlcnJlZF91c2VybmFtZSI6 bnVsbCwiYm9vayI6IlByYWN0aWNpYWwgTWljcm9Qcm9maWxlIiwiZ3JvdXBzIjp bInVzZXIiLCJwcm90ZWN0ZWQiXX0.d0BF1qTrjlQPnb5tppM4LP1T2QWvs9sh6Q lsXcKbFUsHvdzXGDSXkkZTqZl67EkJcUBPy9-I4i5913r9LTBbLIR_bataTVSL9 AcMsSY6tk_B4IU69IjV1GRohGf_LXHyFu_iWGfSWO7TV3-tX43E5Yszvik5sial OrqgVF9uYUy_UaOOY7TEQynpHv4oCTwNKg48-Nlw15Yfz__i7CaOmiRNROp6_cD Zhn1t_aFndplxv4Q-A-p_j2gPpsEldl5mbnBi73-cQvuImawBxA1srRYQSj6aAK JDCBcCj4wh338Nb93l61_PxET8_blXZywJszmLQllJfi2SeR3WucxJ3w
这三个点分隔的部分是头部、有效载荷和签名。您可以将前面的编码形式粘贴到 https://jwt.io/ 以检索三个部分的可读文本。我们将在下面更详细地审查这三个部分:
-
alg(RS256 或 ES256)、enc(用于加密声明的加密算法)、typ(JWT)和kid(用于保护 JWT 的密钥)。以下是一个头部示例。它表示JWT作为typ,RS256作为alg:{ "typ": "JWT", "alg": "RS256" } -
有效载荷:这包含多个声明。以下是一个有效载荷的示例:
{ "iss": "https://server.example.com", "aud": "cloud-native-app", "jti": "0fd089cb-1c45-4c30-9b23-6aa4fd5e70b5", "exp": 1619699906, "iat": 1619699876, "sub": "Emily", "upn": "Emily", "preferred_username": null, "book": "Practicial MicroProfile", "groups": [ "user", "protected" ] } -
签名:签名用于验证消息在传输过程中未被更改。它使用编码的头部、编码的有效载荷、密钥、头部中指定的算法进行创建,然后进行签名。以下是一个验证签名的示例,它基于头部和有效载荷计算签名,然后将其与传入的签名进行比较。如果它们匹配,则表示令牌在传输过程中未被篡改:
RSASHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), <paste-public-key> , <paste-private-key> )
在下一节中,我们将讨论 MicroProfile JWT 的工作原理。
MicroProfile JWT 如何工作?
我们一起学习了 MicroProfile JWT 的外观以及 MicroProfile JWT 的基本概念。要使用 JWT,我们首先需要创建一个令牌。
发布 MicroProfile JWT 令牌
MicroProfile JWT 令牌可以由支持创建 MicroProfile JWT 或受信任的 OpenID Connect 提供商的运行时发布。MicroProfile JWT 可以由受信任的服务器自行发布,其中提供了一组令牌发布 API。Open Liberty、Eclipse Vert.x 和其他运行时都提供了这样的 API。以下是一个使用 Open Liberty API 创建 JWT 令牌的示例:
import com.ibm.websphere.security.jwt.JwtBuilder;
import com.ibm.websphere.security.jwt.Claims;
private String buildJwt(String userName, Set<String> roles) throws Exception {
return JwtBuilder.create("jwtBuilder")
.claim(Claims.SUBJECT, userName)
.claim("upn", userName)
.claim("groups", roles.toArray(new
String[roles.size()]))
.buildJwt()
.compact();
}
此 JWT 令牌包含声明 sub (Claims.SUBJECT)、upn 和 groups。在生产环境中,MicroProfile JWT 通常由一些受信任的 OpenID Connect 提供商发布,如 Auth0、Keycloak、Azure、Okta 等。
支持的 JWT 算法只有 RS256 和 ES256。如果使用其他算法,令牌将被拒绝。
JWT 生成
由于 MicroProfile JWT 比 JWT 多两个声明,因此这些声明需要手动添加为自定义声明到 OpenID Connect 提供商,如 Keycloak、Okta 等。
从客户端传输 MicroProfile JWT 到服务器
MicroProfile JWT 通过 HTTP 请求进行传输。它们可以通过 HTTP 授权头部或 HTTP 饼干传递:
-
mp.jwt.token.header可以用于以下格式存储 JWT:mp.jwt.token.header=Bearer as14efgscd31qrewtadg -
mp.jwt.token.cookie可以用于以下格式存储 JWT:mp.jwt.token.cookie=Bearer= as14efgscd31qrewtadg
服务器通过验证 MicroProfile JWT
MicroProfile JWT 必须签名(JWSes)。为了解码 JWSes,必须指定相应的公钥。公钥的位置可以通过属性 mp.jwt.verify.publickey.location 提供。或者,可以通过属性 mp.jwt.verify.publickey 提供公钥本身。这两个属性不允许同时指定。否则,将抛出 DeploymentException。
签名的 JWT 可以进一步加密(JWE)。当服务器收到 MicroProfile JWT 时,它将验证 JWT。如果 JWT 是 JWE,它需要使用相应的私钥来解密 JWE。私钥的位置可以在属性 mp.jwt.decrypt.key.location 中指定。如果此属性存在,则接收到的 JWT 必须是 JWE。否则,JWT 将被拒绝。
在 JWT 的签名被验证或令牌被解密后,可以执行一些进一步的验证,例如 iss 和 aud 验证。mp.jwt.verify.issuer 属性指定预期的 iss 值,而 mp.jwt.verify.audiences 属性指定 aud 值。
在 JWT 被验证后,对象 JsonWebToken 将作为后端提供给云原生应用程序。
一些 MicroProfile JWT 实现可能还提供额外的验证机制。在 Open Liberty 中,您可以在 server.xml 中指定以下行以验证 issuer 和 audiences,如果相应的属性不存在:
<mpJwt id="stockTraderJWT" audiences="${JWT_AUDIENCE}" issuer="${JWT_ISSUER}" keyName="jwtSigner" ignoreApplicationAuthMethod="false" />
此机制将由 股票交易员 应用程序使用,该应用程序将在第 8 章,构建和测试您的云原生应用程序 中更详细地介绍。
在前面的示例中,当收到并解码 JWT 时,其 aud 和 iss 声明将与在 ${JWT_AUDIENCE} 和 ${JWT_ISSUER} 中定义的受众进行对比。如果它们匹配,JWT 将被接受,然后执行进一步的认证和授权检查。
访问 JsonWebToken
云原生应用程序可以通过以下方式访问 JsonWebToken:
-
通过
SecurityContext访问JsonWebToken。以下代码片段演示了如何从注入的SecurityContext中检索JsonWebToken对象:@GET @Path("/getAud") public Set<String> getAudience(@Context SecurityContext sec) Set<String> auds = null; Principal user = sec.getUserPrincipal(); if (user instanceof JsonWebToken) { JsonWebToken jwt = (JsonWebToken) user; auds = jwt.getAudience(); } return auds; }注意
JsonWebToken包含所有声明。JsonWebToken是Principal的子类。 -
您可以使用 CDI 注入来注入整个
JsonWebToken:@Inject private JsonWebToken jwt; -
您可以使用 CDI 注入来注入特定的声明。以下示例演示了将
raw_token字符串赋值给rawToken变量:@Inject @Claim(standard= Claims.raw_token) private String rawToken;此行用于将
iat声明(类型为Long)注入到dupIssuedAt变量中:@Inject @Claim("iat") private Long dupIssuedAt; -
以下行将
sub声明(类型为ClaimValue)注入到optSubject变量中。此查找是动态的:@Inject @Claim("sub") private ClaimValue<Optional<String>> optSubject;注意
ClaimValue是JsonWebToken中声明的表示,它是一个用于指定声明的包装类。
由于 JWT 附属于特定的请求,因此预期该令牌绑定到 RequestScoped 的生命周期。当将 JsonWebToken 或 Claims 注入生命周期大于 RequestScoped 的作用域,例如 ApplicationScoped 或 SessionScoped 时,你应该使用 Provider、Instance 或 ClaimValue 类型,因为这些是包装器,并且将始终为指定的请求动态检索 JWT。一般来说,你可以使用以下代码片段来检索指定的声明 a_claim:
@Inject @Claim("a_claim") <Claim_Type> private myClaim;
<Claim_Type> 可以是 String、Long、long、Boolean、Set<String>、JsonValue.TRUE、JsonValue.FALSE、JsonString、JsonNumber、JsonArray 或 JsonObject。它可以是这些类型的可选包装器或 ClaimValue 的类型,包括 Optional。
如果通过 MicroProfile Rest Client (MicroProfile Rest Client 在 第四章,开发云原生应用程序)进行调用,MicroProfile JWT 可以传播到下游服务。传播将通过设置以下配置自动处理:
org.eclipse.microprofile.rest.client.propagateHeaders= Authorization/Cookie
如果 JWT 被指定为授权头,其值将是 Authentication。如果 JWT 被指定为 cookie,其值将是 Cookie。
公钥和私钥的查找位置
当谈到安全问题时,你马上会想到密钥。这里也不例外。在 MicroProfile JWT 中,你需要一个公钥来解码 JWS,以及一个私钥来解密 JWE。如果你收到了 JWE,你需要首先使用你的私钥来解密它。解密后,你将得到一个 JWS。然后你需要使用你的公钥来解码 JWS 以验证签名。MicroProfile JWT 为你提供了以下配置来检索密钥:

表 5.8 – MicroProfile JWT 配置
如果设置了 mp.jwt.decrypt.key.location 属性,并且 mp.jwt.verify.publickey.location 或 mp.jwt.verify.publickey 之一被设置,则只能接受加密的 JWT。
上表中提到的属性是常规的 MicroProfile 配置。它们可以在任何配置源中指定,例如 META-INF/microprofile-config.properties、系统属性、环境变量 或其他自定义配置源。
我们已经介绍了 MicroProfile JWT 的工作原理。在下一节中,我们将讨论如何使 API 对你的 Maven 或 Gradle 项目可用。
如何使 MicroProfile JWT 对应用程序可用?
要使用 MicroProfile JWT API,你需要使这些 API 对你的应用程序可用。如果你创建了一个 Maven 项目,你可以在你的 pom.xml 中直接添加以下内容:
<dependency>
<groupId>org.eclipse.microprofile.jwt</groupId>
<artifactId>microprofile-jwt-auth-api</artifactId>
<version>1.2</version>
</dependency>
或者,如果你创建了一个 Gradle 项目,你需要添加以下依赖项:
dependencies {
providedCompile org.eclipse.microprofile.jwt: microprofile-jwt-auth-api:1.2
}
摘要
在本章中,我们学习了 MicroProfile Config,它使你能够将配置外部化,以实现灵活高效的云原生架构,这也被十二要素应用高度推荐。
然后,我们继续探索如何使用 MicroProfile Fault Tolerance 创建一个具有弹性的云原生应用,这样应用开发者就可以专注于他们的业务逻辑,而 MicroProfile Fault Tolerance 则处理可能出现的故障,然后通过简单的容错性注解执行重试或限制资源消耗等等。
在我们介绍了容错性之后,我们接着深入探讨了 MicroProfile OpenAPI,学习如何使用 MicroProfile OpenAPI 记录你的云原生应用。我们讨论了几种生成 OpenAPI 文档的方法。一些 MicroProfile OpenAPI 实现,如 Open Liberty,还提供了 UI 集成,让你可以通过 UI 直接访问端点。
最后,我们学习了使用 MicroProfile JWT 保护云原生应用的主题。我们一般讨论了保护云原生应用的独特要求,然后解释了如何使用 MicroProfile JWT 以便携和互操作的方式促进云原生应用的安全保护。
到目前为止,你已经了解了许多你需要开发云原生应用的技术。接下来的任务是考虑如何通过构建一个智能和智能的云原生应用来改善你的日常运营体验,这意味着当应用准备好接收请求时,它可以自动与云基础设施进行通信等等。如果应用能够提供一些指标,那么在为时已晚之前,日常运营可以采取一些预防措施。我们都知道错误无法完全消除。如果出现问题,日常运营将提供简单直观的诊断方法来立即识别故障。MicroProfile Health、MicroProfile Metrics 和 MicroProfile OpenTracing 是满足这些要求的技术。
在下一章中,我们将讨论为什么以及如何使用 MicroProfile Health、MicroProfile Metrics 和 MicroProfile OpenTracing 来帮助处理日常运营。
第六章:观察和监控云原生应用程序
在前两章中,我们讨论并解释了 MicroProfile 4.1 平台在构建和增强您的云原生应用程序方面的各种功能。此时,您的云原生应用程序建立在强大的基础核心之上,这得益于 Jakarta EE 平台的经过验证的组件。在此基础上,您添加了一些功能,使您的应用程序更具弹性、安全性、可配置性和可文档化。从所有目的和意义上讲,您已经拥有了一个完全有能力的云原生应用程序,准备部署。但作为您这样的精明开发者,您知道一旦部署了您的云原生应用程序,故事还没有结束。没有任何事情是真正完美的,并且根据您的应用程序生态系统的复杂性,让您的应用程序随意运行可能会造成灾难。
这带来了监控应用程序的重要任务。您、您的团队或您的运维团队需要能够监控应用程序的活动和性能,以识别任何潜在的问题。有效的监控可以用作即将到来的麻烦的早期预警,揭示可能需要优化的区域,或在事后分析中查看可能出错的地方。或者,从更乐观的角度来看,有效的监控可以简单地提供关于应用程序性能的美丽数据。
这就引出了本章的内容,我们将介绍 MicroProfile 平台发布范围内包含的最后三个规范。为了观察和监控您的云原生应用程序,MicroProfile 平台提供了 MicroProfile Health、MicroProfile Metrics 和 MicroProfile OpenTracing 技术。
尤其是以下内容:
-
使用 MicroProfile Health 确定您的云原生应用程序的健康状况
-
使用 MicroProfile Metrics 对您的云原生应用程序进行仪表化和使用指标
-
使用 MicroProfile OpenTracing 跟踪您的云原生应用程序
技术要求
要构建和运行本章中提到的示例,您需要一个装有以下软件的 Mac 或 PC(Windows 或 Linux):
-
Java 开发工具包,版本 8 或更高:
adoptium.net/ -
Apache Maven:
maven.apache.org/ -
Git 客户端:
git-scm.com/
本章中使用的所有源代码均可在 GitHub 上找到
一旦您已经克隆了 GitHub 仓库,您可以通过进入 ch6 目录并在命令行中输入以下命令来启动 Open Liberty 服务器,这些代码示例将在其中执行:
mvn clean package liberty:run
您可以通过在相同的命令窗口中按 Ctrl + C 来停止服务器。
部署到 Open Liberty 服务器的应用程序将被分配一个上下文根 ch6。例如,一个 JAX-RS 资源的完整 URL 将是 http://localhost:9080/ch6/path/to/resource。这一点将在本章中展示如何向端点发送请求的代码示例中得到体现。
使用 MicroProfile Health 确定您的云原生应用程序的健康状况
为了开始我们关于 MicroProfile 可观察性工具包的三部分之旅,我们将检查 MicroProfile Health 技术。我们选择首先检查这项技术,因为与本章中的其他两项技术相比,其优势和用例范围更广。MicroProfile Health 技术报告有关您的微服务健康或状态的信息。预期的健康状态是 UP 或 DOWN。
MicroProfile Health 在云原生应用程序中的重要性
我们现在知道了 MicroProfile Health 技术能做什么。但它有什么作用呢?为了找出答案,我们必须退一步。开发使用 MicroProfile 技术的应用程序的驱动力是它们将是云原生的。如果您还记得 第一章,云原生应用程序,云原生应用程序与非云原生应用程序之间的重要区别在于其利用云提供的功能的能力。MicroProfile Health 技术是这一点的完美例子。
在其核心,MicroProfile Health 技术致力于向某些外部观察者报告应用程序的健康状况。由于我们正在开发一个云原生应用程序,该应用程序将在您云平台上部署的容器中运行其生命周期。其任期是短暂还是长久,将由健康状况来决定。实际上,这些健康状况报告了容器健康状态给您的云平台。利用容器状态报告,云平台的监控服务可以使用这些数据来做出决定,终止并替换任何有问题的容器。您的容器何时终止和重启最终取决于您,即开发者。您的云平台可能对容器健康状况下的操作有规则,但这些健康状况报告的上下文取决于 MicroProfile Health 技术在您的应用程序中的使用方式。
在本章的后面部分,我们将探讨使用Kubernetes应用程序/容器的健康状态的示例场景。Kubernetes 是一个开源项目,它为部署、扩展和管理容器提供了一种容器编排解决方案。作为更知名的容器编排平台之一,它将为展示使用 MicroProfile Health 技术的优势提供一个极好的工具。Kubernetes 以及其他云基础设施主题将在第七章**,与 Open Liberty、Docker 和 Kubernetes 的 MicroProfile 生态系统*中更详细地介绍。
MicroProfile Health 技术概述
MicroProfile Health 提供了三种类型的健康或状态检查:存活、就绪和启动健康检查。我们将在稍后详细解释这些健康检查,但现在是,要知道它们的目的是报告应用程序是否存活、就绪,或者它是否甚至已经完成了启动。应用程序中健康检查的实现和存在被定义为流程。这个流程被调用以检查应用程序是否已启动,以及其存活状态或就绪状态对于应用程序中实现组件。从现在开始,我们将把健康检查称为健康检查、流程或两者的组合。
健康检查流程可以在您的微服务中实现,并将返回UP或DOWN状态,以指示应用程序各个组件的存活状态或就绪状态,以及应用程序是否已完成初始化。存活状态、就绪状态和启动健康检查将通过http://host:port/health/live、http://host:port/health/ready和http://host:port/health/started端点分别报告。每个端点都提供了一个总体状态,它是所有流程的逻辑合取。如果您的应用程序中有五个就绪流程被实现,但只有一个流程返回UP,那么您应用程序的总体就绪状态将是DOWN。还有一个http://host:port/health端点,它提供了整个应用程序的总体状态,这是通过存活状态、就绪状态和启动健康检查的健康检查流程的合取得到的。当使用http://host:port/health端点时,存活状态、就绪状态或启动健康检查之间没有区别。所有健康检查流程,无论其类型如何,都必须返回UP,http://host:port/health端点才能返回UP。需要注意的是,调用流程的顺序是任意的,因此可以按任何顺序调用。从现在开始,当提到健康端点时,我们将为了简洁省略http://host:port。
在我们继续之前,让我们更深入地了解这三种健康检查类型。
存活健康检查
活跃性健康检查程序的目的,正如其名称所暗示的,是报告应用程序是否处于活跃状态。在云环境中,这种状态可以被监控服务用来确定应用程序是否按预期运行。如果健康检查失败,可能会触发您的云平台监控服务终止应用程序的容器。根据您配置的策略,这可能会导致应用程序的容器被重新启动。
注意,失败的活跃性程序并不意味着应用程序不再运行。相反,它意味着用于检查的已采用策略认为应用程序已经或正在遭受服务质量的下降,并且不能再被视为操作上有效。例如,活跃性程序可以用来检测 JVM 中的内存泄漏以及内存损失的速度,因此现在终止此容器比以后更明智。因此,将返回DOWN状态。
启动健康检查
启动健康检查的目的在于提供一个中间检查,它是活跃性健康检查的前奏。在容器环境中,并非所有容器都是平等的。可以理解的是,某些容器可能由于容器内运行的应用程序的复杂性而启动和初始化较慢。在兼容的云环境中,启动检查可以在执行活跃性检查之前进行一个UP检查。
准备性健康检查
准备性健康检查程序的目的在于允许外部观察者(例如,云监控服务)确定应用程序是否已准备好接收并执行业务逻辑。尽管活跃性检查表明应用程序已经有效启动并且运行良好,没有问题,但应用程序可能还没有准备好接收流量。这可能是因为应用程序仍在尝试初始化资源或连接到它所依赖的其他应用程序。准备性检查将在其尝试建立连接的过程中报告DOWN状态。
关于默认健康检查的特殊说明
根据您底层的 MicroProfile 运行时,您的运行时可能提供mp.health.disable-default-procedures配置元素,并设置其值为true。
健康检查程序的仪表化
健康检查程序由 MicroProfile 运行时调用,以找出应用程序特定组件的健康状况,无论是存活、就绪还是启动程序。但健康检查报告也可能同时报告存活、就绪和启动程序。这是由于 MicroProfile Health 运行时在底层的工作方式。像其他 MicroProfile 技术一样,MicroProfile Health 与@Liveness、@Readiness和@Startup限定符注解内在集成。使用这些注解之一可以让 MicroProfile Health 运行时知道正在报告哪些健康状态。但在我们过于领先之前,应用程序代码中的健康检查究竟是什么?
每个健康检查程序的基础是名为HealthCheck的功能接口。它由一个函数call()组成,该函数返回一个HealthCheckResponse。在应用程序中,HealthCheck实现至少注解了@Liveness、@Readiness或@Startup之一。记住,MicroProfile Health 与 CDI 的集成意味着每个健康检查程序(即HealthCheck实现)都是一个 CDI bean,并在应用程序的生命周期上下文中有一个位置。为你的健康检查程序定义一个 CDI 作用域也是明智的。在我们的示例中,我们将使用@ApplicationScoped。
以下代码片段演示了如何在同一程序中配置存活、就绪和启动健康检查。你可以通过使用一个注解来配置单个健康检查:
@ApplicationScoped
@Liveness
@Readiness
@Startup
public class LivenessCheck implements HealthCheck {
public HealthCheckResponse call() {
[...]
}
}
因此,现在我们已经知道了如何创建和定义不同类型的健康检查,我们可以学习如何构建健康检查响应,其数据将通过/health/*端点之一被外部观察者消费。
正如我们之前提到的,我们将返回一个HealthCheckResponse对象。这个数据对象包含我们需要的所有信息,用于唯一标识健康检查,最重要的是,你的云原生应用程序的健康状况。
HealthCheckResponse由三个字段组成:
-
一个
String字段,用于区分这个健康检查程序与其他健康检查程序。 -
一个具有UP或DOWN值的
enum字段。 -
Map<String, Object>。当String键及其值可以是String、long或boolean时。
现在,让我们看看构建健康检查程序的不同方法。
使用 HealthCheckResponseBuilder
要创建一个HealthCheckResponse,你可以在HealthCheckResponse中调用两种静态方法之一,这将返回一个HealthCheckResponseBuilder。这两种方法是builder()和named(String name)。后者创建一个已指定名称的HealthCheckResponseBuilder,而前者提供一个干净的实例。
HealthCheckResponseBuilder 提供了一个构建模式,用于构建包含所需和可选字段的 HealthCheckResponse。如果您打算提供可选数据,这是首选方法。
以下代码示例展示了基于 JVM 堆内存使用情况执行存活健康检查的场景。
LivenessCheck 的完整源代码可以在 bit.ly/2WbiVyV 找到:
@ApplicationScoped
@Liveness
public class LivenessCheck implements HealthCheck {
public HealthCheckResponse call() {
//Percentage value from 0.0-1.0
Double memoryUsage = getMemUsage();
HealthCheckResponseBuilder builder = HealthCheckResponse.named("LivenessCheck");
if (memoryUsage < 0.9) {
builder.up();
} else {
builder.down();
}
builder = builder.withData("MemoryUsage", memoryUsage.toString());
return builder.build();
}
}
在这个示例中,我们使用了 named(String name) 静态方法为健康检查提供名称。然后,我们使用了 HealthCheckResponseBuilder 类的 up()、down() 和 withData(String key, String value) 方法来指定健康检查的状态并提供任何额外的上下文数据。withData(…) 方法是一个重载方法,可以接受 String、long 和 boolean 类型的值。在这个示例中,如果内存使用率低于 90%(即 getMemUsage() 方法返回的值小于 0.9),我们将返回 UP 状态。否则,我们将返回 DOWN 状态。
或者,如果您使用的是 HealthCheckResponse.builder(),您将需要使用 HealthCheckResponseBuilder 类的 name(String name) 为健康检查提供名称。
现在,我们不再需要一行多的 if-else 块,我们可以使用 HealthCheckResponseBuilder.status(boolean status) 在一行中完成:
return HealthCheckResponse.builder()
.name("LivenessCheck")
.status(memoryUsage < 0.9)
.withData("MemoryUsage", memoryUsage
.toString()).build()
如您所见,我们将八行代码缩减到了一行!
使用 HealthCheckResponse
我们可以不用 HealthCheckResponseBuilder,也可以使用 HealthCheckResponse 类的两个静态方法,它们方便地创建 UP 或 DOWN 的 HealthCheckResponse,如下例所示。
ReadinessCheck 的完整源代码可以在 bit.ly/3iV3WBP 找到:
@ApplicationScoped
@Readiness
public class ReadinessCheck implements HealthCheck {
public final String NAME = "evenNumberPhobic";
public HealthCheckResponse call() {
long time = System.currentTimeMillis();
if (time % 2 == 0)
return HealthCheckResponse.down(NAME);
else
return HealthCheckResponse.up(NAME);
}
}
这里使用的方法名称恰如其分,分别是 up(String name) 和 down(String name),它们接受一个 String 参数,用于定义健康检查的名称。这种方法假设没有额外的可选数据需要与这个健康检查过程结合。在以下示例中,我们将检索当前系统时间,如果它是偶数,我们将返回 DOWN 状态(否则,将返回 UP 状态)。
CDI 生产者
由于 MicroProfile Health 对 CDI 的隐式依赖,健康检查过程也可以使用 CDI 方法生产者进行监控。您可以使用 CDI 方法生产者在单个类中监控多个健康检查过程。以下示例展示了存活、就绪和启动健康检查过程作为 CDI 方法生产者进行监控。
CDIMethodProducerCheck 的完整源代码可以在 bit.ly/3k9GrUT 找到:
@ApplicationScoped
public class CDIMethodProducerChecks {
@Produces
@Liveness
HealthCheck livenessCDIMethodProducer() {
return () -> HealthCheckResponse.named("cdiMemUsage") .status(getMemUsage() < 0.9).build();
}
@Produces
@Readiness
HealthCheck readinessCDIMethodProducer() {
return () -> HealthCheckResponse.named("cdiCpuUsage") .status(getCpuUsage() < 0.9).build();
}
@Produces
@Startup
HealthCheck startupCDIMethodProducer() {
return () -> HealthCheckResponse.named ("cdiStartStatus").status(getStatus()).build();
}
}
由livenessCDIMethodProducer方法封装的存活性程序,如果内存使用率低于 90%(即getMemUsage()方法返回的值小于 0.9),则返回UP。由readinessCDIMethodProducer方法封装的准备性程序,如果 CPU 使用率低于 90%(即getCpuUsage()方法返回的值小于 0.9),则返回UP。由startupCDIMethodProducer方法封装的启动程序将执行getStatus()业务方法来评估应用程序启动状态的条件,并将返回true或false以调用UP或DOWN状态,分别。
获取健康检查数据
正如我们之前提到的,我们可以通过/health、/health/liveness、/health/readiness和/health/started端点查看数据。因此,这些健康检查可以通过 HTTP/REST 请求进行消费。通过 HTTP/REST 调用,健康检查程序以 JSON 格式呈现。根级别包含总体健康状态,有一个status字段,它是从checks JSON 列表中定义的所有健康检查程序的交集计算得出的。
总体状态决定了 HTTP 响应代码。UP状态返回 HTTP 200,而DOWN状态返回 HTTP 500。健康检查程序遇到的任何故障或错误将导致返回 HTTP 503 错误代码,这相当于DOWN状态。请记住,如果任何健康检查报告处于DOWN状态,则总体状态为DOWN。列表中的每个健康检查 JSON 对象都显示HealthCheckReponse的内容(即其名称、状态和可选的键值映射)。如果没有健康检查程序,则自动返回UP(即 HTTP 200)。之前列出的格式结构和行为适用于所有四个端点。使用响应代码很重要,因为这可能是外部观察者确定您的应用程序健康状态的方法(即云平台)。
以下示例输出可以应用于任何四个健康端点,因此我们不会定义它是从哪个端点来的:
{
"status": "DOWN",
"checks": [
{
"name": "goodCheck",
"status": "UP"
},
{
"name": "questionableCheck",
"status": "DOWN",
"data": {
"application": "backend",
"locale": "en"
}
}
]
}
输出报告称我们有一个名为"goodCheck"的健康检查程序报告UP。我们还有一个名为"questionableCheck"的程序报告DOWN。这导致总体状态报告DOWN,并将导致返回 HTTP 500 错误。如输出所示,"questionableCheck"程序已包含额外的上下文映射数据;即"application": "backend"和"locale": "en"。
关于默认就绪和启动程序的特别说明
MicroProfile Health 运行时提供了一个配置值(通过 MicroProfile Config),称为 mp.health.default.readiness.empty.response。其值可以是 UP 或 DOWN。默认值是 DOWN。当应用程序仍在启动且就绪程序尚未调用时,此值用于报告微服务的就绪状态。如果应用程序代码中没有定义就绪健康检查程序,则不适用。如果是这种情况,则没有健康检查程序的默认行为是在 /health/readiness 端点上返回带有 UP 状态的 HTTP 200 响应。
对于启动健康检查,也存在一个配置值,称为 mp.health.default.startup.empty.response。如果没有启动健康检查,则 /health/started 端点将返回默认的 UP 状态。
另一方面,存活检查没有可配置的值。它们遵循简单的规则:如果应用程序仍在启动且存活检查尚未准备好被调用,则返回带有 UP 状态的 HTTP 200 响应。
其他连接和有效载荷格式
根据您选择的运行时,健康检查程序的结果可能可以通过其他方式获得(例如,TCP 或 JMX)。我们在这里使用“额外”一词,因为至少,MicroProfile Health 运行时必须支持 HTTP/REST 请求。然而,作为一种云原生技术,MicroProfile Health 了解可能更倾向于其他获取数据的方法。MicroProfile Health 规范定义了一组协议和线格式规则,用于如何消费和展示数据。尽可能以 JSON 格式展示健康检查数据。如果不行,则必须提供相同的数据有效载荷。
本书将不会讨论在 MicroProfile Health 规范中定义的协议和线格式语义的复杂性。您可以在 bit.ly/3ecI6Gz 查阅 MicroProfile Health 规范以获取此类信息。
MicroProfile 健康检查与 Kubernetes 的存活、就绪和启动探测
现在我们将探讨如何在实际场景中消费健康检查程序报告的健康检查数据。为此,我们将使用 Kubernetes。由于这是更知名的云容器编排平台之一,这将成为一个出色的演示工具。我们将使用 Kubernetes 术语,并尽力在本节中描述这些术语。我们将在 第七章,使用 Docker、Kubernetes 和 Istio 的 MicroProfile 生态系统 中更深入地探讨 Kubernetes 和云基础设施。
在云环境中,您部署的容器存在于物理或虚拟机的互联网络中。Kubernetes 通过无缝管理和集成驻留在 Kubernetes 的 Pods 中的容器部署来提供服务。Pod 可以包含一个或多个容器。为了了解这个网络(即您的云)中 Pods 的活动情况,每个机器上都有一个 kubelet。它充当节点代理,管理机器上的 Pods 并与中央 Kubernetes 管理设施通信。作为其职责的一部分,它可以确定这些 Pods 内的容器何时过时或损坏,并在需要时有权停止和重启它们。Kubelets 还被赋予评估容器何时准备好接收流量或不准备接收流量的任务。最基本的是,它们可以检查容器是否已初始化完成。它们通过检查 Pods 内容器的存活、就绪和启动状态来完成这些任务,使用存活、就绪和启动探测。
这种行为是容器特定的,必须在每个容器的基础上启用。这通过在 Pod 的配置 YAML 文件中配置容器来实现。以下示例使用了来自 broker.yaml 文件的片段,该文件配置了我们在 第三章 中介绍的 StockTrader 应用程序的 Broker 微服务,即 介绍 IBM 股票交易云原生应用程序,我们将在 第八章 中再次探讨,逐步开发股票交易应用程序。YAML 文件包含 Kubernetes Deployment 定义,它提供了将容器(s)部署到 Pod 上的配置,包括要使用的容器镜像、环境变量,当然还有,当然,存活、就绪和启动探测,可以为每个定义的容器进行配置。我们省略了文件的其他部分,只显示存活、就绪和启动探测的配置。
broker.yaml 的完整源代码可以在 bit.ly/3sEvHAa 找到:
apiVersion: apps/v1
kind: Deployment
[...]
spec:
[...]
readinessProbe:
httpGet:
path: /health/ready
port: 9080
initialDelaySeconds: 60
periodSeconds: 15
failureThreshold: 2
livenessProbe:
httpGet:
path: /health/live
port: 9080
periodSeconds: 15
failureThreshold: 3
startupProbe:
httpGet:
path: /health/started
port: 9080
periodSeconds: 30
failureThreshold: 4
[...]
存活性、就绪性和启动端点分别在 livenessProbe、readinessProbe 和 startupProbe 部分定义。探测被配置为使用 HTTP/S 通过 httpGet。在我们的例子中,我们将使用一个未加密的 HTTP 端点。如果你想建立一个安全连接,你需要在 httpGet 下添加一个新的字段,命名为 scheme,并将其值设置为 HTTPS。我们使用 path 字段指定 /health/live、/health/ready 和 /health/started,并使用 port 指定到达它的端口。就绪性探测使用 initialDelaySeconds 字段配置了 60 秒的初始延迟,这可以防止就绪性探测在此时之前触发,以便容器及其应用程序启动。当探测正在触发时,就绪性和存活性探测每 15 秒发送一次请求,启动探测每 30 秒发送一次请求,这是通过 periodSeconds 配置的。然而,在这个例子中没有定义的 timeoutSeconds 字段。默认情况下,值为 1 秒,它定义了 kubelet 在超时之前应该等待的时间。failureThreshold 定义了探测在被视为失败之前将重试多少次。
你可能会注意到存活性探测没有指定 initialDelaySeconds 字段。你可以这样做,但这是不必要的,因为我们正在使用 startUpProbe。请记住,(如果已定义)启动探测将首先被查询,直到它提供 UP 状态,然后才会检查存活性探测。这是 Kubernetes 提供的行为。
如果任何一个探测完全失败,即所有尝试都失败了,那么容器将面临重启。
现在可能不会让人感到惊讶,MicroProfile Health 技术在设计时考虑到了 Kubernetes 平台,提供了存活性、就绪性和启动端点,所有这些都与 Kubernetes 的特定存活性、就绪性和启动探测相匹配。然而,简单的 /health 端点的存在使得它能够被只关心单个健康端点的其他平台使用。但请记住,当使用 /health 端点时,存活性、就绪性和启动的概念可能不再适用。除此之外,MicroProfile Health 的简单协议和线缆格式规则允许其健康检查数据被任何外部观察者(无论是有意识的还是无意识的)轻松消费。
我们现在已经到达了 MicroProfile Health 部分的结尾。正如我们之前提到的,在 MicroProfile Health 的介绍中,这项技术旨在满足广泛的监控范围。在下一节中,我们将开始详细介绍 MicroProfile Metrics 的监控范围。
使用 MicroProfile Metrics 在你的云原生应用程序上度量指标
这是我们的 MicroProfile 可观察性三部曲的第二部分,在这一部分中,我们发现自己正深入 MicroProfile Metrics 的细节。我们之前讨论的技术——MicroProfile Health——努力通过允许开发者有策略地在应用中放置健康检查来报告云原生应用的整体健康。另一方面,MicroProfile Metrics 努力通过你在应用中配置的指标以及 MicroProfile Metrics 运行时提供的指标来报告应用及其环境的性能和内部工作。这提供了可以记录和/或聚合以供专用监控工具分析的实时统计数据。为了实现这一点,MicroProfile Metrics 技术配备了七种不同类型和功能的指标。随着我们通过本章的这一部分继续前进,我们将非常熟悉它们。
微服务指标在云原生应用中的重要性
能够监控应用中特定组件的统计和性能数据,并不是一个云原生或开发特定的想法。这应该是一种健康的实践,无论你的努力是否在云上。然而,当我们谈论一个高度可扩展和多样化的应用拓扑时,能够监控你的微服务是至关重要的。即使你管理的不是一个应用集群,而是几个应用,收集指标的好处也是无可争议的宝贵。这是你的微服务与你交流并告诉你它感受的方式。这为你提供了机会,在应用的生命活力健康检查意外宣布它处于DOWN状态之前,识别任何令人担忧的模式。例如,在前一节中,我们演示了一个场景,其中生命活力健康检查过程依赖于正在使用的内存量。一旦超过某个阈值,它就会失败并报告DOWN。仅仅使用 MicroProfile Health,我们不会知道出了什么问题,直到为时已晚,那时,你的云平台可能已经重启了容器。也许你可能完全不知道发生了任何事情。
MicroProfile Metrics 报告此类统计数据允许你提前预见此类灾难,并了解应用的表现。作为另一个例子,我们可以让指标报告对微服务中 REST 端点发出的请求数量以及平均完成请求所需的时间。这些指标信息可以揭示你的微服务有多受欢迎,以及你的微服务表现得好还是不好。这可以促使采取必要的步骤来修订和修改部署环境,甚至可能是应用本身。
然而,MicroProfile Metrics 只能报告指标的瞬时值。为了正确利用这一信息流,我们需要在时间上聚合指标数据,实际上将其转换为时间序列指标。MicroProfile Metrics 本身,以及任何其他 MicroProfile 技术,都不用于完成这项任务。MicroProfile Metrics 的存在只是为了提供一个无缝且有效的将指标仪器化的方式到您的微服务中。已经存在一个专门用于聚合指标和可视化的工具和平台生态系统。一个流行的监控堆栈是利用Prometheus和Grafana的。
Prometheus 是一个开源的监控解决方案,用于收集、存储和查询时间序列指标。Prometheus 通常与另一个名为 Grafana 的工具结合使用。Grafana 是另一个开源监控解决方案,它通过使用针对时间序列数据库(例如 Prometheus)进行的定制查询,通过图表、表格和其他类型的可视化来显示和可视化时间序列指标。这可以为您提供或您的运维团队以人性化的方式通过有意义的可视化监控微服务的性能的能力。
在本节的结尾,我们将演示如何使用 Grafana 来可视化 MicroProfile Metrics 运行时收集的指标数据。能够战略性地仪器化指标以提供有意义的信息是战斗的一半;有效地使用这些信息才是赢得战斗的方式。
MicroProfile Metrics 技术概述
你可能已经注意到,在本节的介绍中,我们提到指标可以来自应用程序或运行时本身。就像 MicroProfile Health,其中可能提供默认的健康检查一样,MicroProfile Metrics 运行时也可以提供默认的即用型指标。运行时必须在很大程度上提供它希望提供的任何可选指标之上的某些指标集。这些指标被称为基本指标和供应商指标。然而,并非所有基本指标都是严格必需的,我们将在稍后解释这一点。开发者在应用程序中通过仪器化的指标被称为应用程序指标。所有这些不同的指标集都分别独立存在,不受彼此影响,在不同的指标注册表下。指标注册表是 MicroProfile Metrics 技术的控制中心和核心。指标注册表是指标注册、存储、检索和删除的地方。这种将不同类型的指标逻辑分组到它们自己的独特指标注册表中,简化了处理不同范围的指标,最重要的是,避免了如果它们在一个单一的指标注册表中存在时可能发生的任何指标名称冲突。
要检索指标数据,MicroProfile Metrics 运行时提供了四个 HTTP/REST 端点。第一个是一个通用的 http://host:port/metrics 端点,它显示所有作用域和注册表中的所有指标。指标以它们各自指标注册表的名字为前缀,以避免混淆。其他三个端点是 http://host:port/metrics 端点的子资源,它们报告每个特定注册表中的指标。它们是 http://host:port/metrics/base、http://host:port/metrics/vendor 和 http://host:port/metrics/application HTTP/REST 端点。指标可以以 JSON 或 Prometheus 展示格式报告。我们将在稍后详细介绍这两种格式。在接下来的讨论中,当提到指标端点时,我们将为了简洁省略 http://host:port。
总结来说,以下图表展示了指标生命周期的总体流程。首先,指标被配置到您的微服务中(或由运行时提供!)。这些指标在 /metrics 端点上进行报告。然后,使用某些监控工具或平台(例如 Prometheus)检索指标数据并将其存储,从而将其转换为时间序列指标。然后,使用另一个监控工具或平台(例如 Grafana)来可视化这些数据:

图 6.1 – 指标的生命周期
我们现在将更详细地描述三种不同的指标作用域;即,基础指标、供应商指标和应用指标。
基础指标
基础指标是一组所有 MicroProfile Metrics 运行时必须提供的指标。然而,也有一些例外情况,其中指标可以可选实现。这种轻微的变异性是由于基础指标旨在实现的目标。基础指标列表的创建是为了捕捉和报告每个运行时可能拥有的指标。由运行时定义和实现基础指标可以减轻开发者需要自己配置指标以捕获基本和/或常用统计数据的负担。通过提供这些基础指标,它们将始终可用,无论是否需要。
基础指标显然的目标是包括 Java 虚拟机(JVM)的统计数据。基础指标覆盖了针对内存统计、垃圾回收统计、线程使用、线程池统计、类加载统计以及操作系统统计的众多指标。然而,并非每个 JVM 都是相同的,其中一些指标是可选的,因为底层的 JVM 可能不保留此类统计数据。基础指标还包括可选的 REST 指标,这些指标跟踪请求计数、未映射异常计数以及每个 REST/JAX-RS 端点上的时间。我们鼓励您通过查看 MicroProfile Metrics 规范来审查基础指标及其定义,规范链接为 bit.ly/3mXpL42。
MicroProfile Metrics 规范仅明确定义了上述 JVM 和 REST 指标作为基础指标,但 MicroProfile 故障恢复生成的指标也被归类为基础指标。我们在上一章的“故障恢复指标”部分介绍了 MicroProfile 故障恢复指标。
供应商指标
供应商指标是供应商为其 MicroProfile Metrics 的实现提供的指标。不同的 MicroProfile Metrics 实现将包含不同的供应商指标集。供应商指标是完全可选的,并且可能存在您选择的 MicroProfile Metrics 运行时不提供任何供应商指标的情况。供应商指标的目的在于允许供应商的实现提供任何可以增强最终用户对特定 MicroProfile Metrics 运行时监控能力的指标。例如,如果您使用的运行时也符合 Jakarta EE 标准,那么它可能能够提供与该平台下组件相关的指标。供应商指标可以通过 /metrics/vendor 端点独家访问,或者与 /metrics 端点上的其他作用域的指标结合访问。
应用指标
应用指标是您,即开发者,在您的应用程序中实现的指标。这些指标报告了您和您的团队感兴趣并用于观察和监控应用程序性能的统计数据。这是您在实现指标时将主要与之交互的指标作用域。应用指标可以通过 /metrics/application 端点独家访问,或者与 /metrics 端点上的其他作用域的指标结合访问。
七种指标类型
现在我们了解了可用的指标的不同作用域,我们可以列出七种应用指标类型:
-
计数器
-
度量
-
并发度量
-
直方图
-
计数器
-
计时器
-
简单计时器
根据名称,很容易推断出不同类型的指标旨在实现什么。如果不清楚,请不要担心——在我们介绍如何在不同指标上实现时,我们将在“实现指标”部分详细讨论这些指标。
指标模型
现在我们已经知道了有哪些类型的指标以及它们可能存在的范围,现在是时候让我们了解其背后的指标模型了。这听起来可能像是一个枯燥的话题,你可能会有跳过它的冲动,但如果你希望了解如何有效地进行指标监控和处理,理解这一点是至关重要的。
一个指标,除了是七种指标类型之一外,还包括一个名称,一组可选的键值/metrics/*端点。
命名的目的是相当明显的:它是为了唯一地识别出与其他指标不同的指标。然而,在某些情况下,这可能还不够,因为不同的指标可能具有相同的名称。这是因为 MicroProfile Metrics 支持带有键值标签的多维指标。
指标的名称及其标签的组合封装在指标注册表中的MetricID对象中。MetricID是指标的标识符。它在指标注册表中与指标实例本身紧密耦合。对于指标的标签使用是可选的,并且可能存在你应用程序中的所有指标都使用没有标签的独立指标名称的情况。这导致MetricID只有一个名称而没有标签。然而,如果你需要利用多维指标的力量,这可能是有用的。这种需求可能出现在你试图从多个类似来源记录相同类型的数据(例如,一个计数器来计数某些东西)时。你可以使用相同的指标名称并提供一个标签,以唯一地识别它与其他来源的不同。一个例子是,如果你正在使用指标来计数特定类中的方法被调用的次数。你可以将指标命名为classMethodInvocations,并为每个方法提供一个标签,其中键是method,值是方法的名称。
当使用可用的可视化监控工具之一,如 Grafana 时,这种多维指标的使用最能发挥其优势。你可以通过一个简单的查询快速检索和显示所有具有相同名称的指标,无论它们的标签是什么。
用于识别指标的最后一项是它的元数据。元数据包括指标名称、其类型、指标的度量单位(如果适用)、可选的描述和可选的易读显示名称。对于每个唯一的指标名称,只有一个元数据。因此,可以有多个 MetricID 与一个元数据相关联。能够在元数据中重复使用指标的名称有助于将 MetricIDs 和元数据关联起来,因为它们在指标注册表中是松散耦合的。之前描述的关系在以下图中表示。*****表示 0 到多个:

图 6.2 – 指标注册表指标模型
获取指标数据
在我们继续讨论指标仪表化的主题之前,我们将介绍指标是如何提供的。在 仪表化指标 部分,我们将逐个介绍每个指标并提供其输出的示例,特别是其 Prometheus 输出。因此,首先,我们必须了解我们将要查看的内容。
如我们之前提到的,指标可以通过向 /metrics、/metrics/base、/metrics/vendor 或 /metrics/application 端点发送 HTTP/REST 请求以 JSON 或 Prometheus 展示格式获取。可以通过向 /metrics/<scope>/<metric_name> 发送请求来检索特定指标名称的指标输出。
JSON 格式
指标的 JSON 格式输出分为两部分。我们可以通过指定 Accept 报头为 application/json 来调用带有 GET 报头的请求,以获取指标及其数据。如果我们发出 OPTION 请求,我们将能够检索与指标关联的元数据。
让我们看看向 /metrics 发送 GET 请求会返回什么。注意,来自不同作用域的指标都位于它们自己的 JSON 数组列表中。我们只展示基本指标,并在示例输出中隐藏任何供应商或应用程序指标。我们还将通过使用基本作用域中列出的前两个指标来查看多维指标的一个示例。存在两个 gc.total 指标,其键值对为 "name=scavenge" 和 "name=global":
{
"base": {
"gc.total;name=scavenge": 361,
"gc.total;name=global": 9,
"classloader.loadedClasses.count": 9448,
"gc.time;name=global": 33,
"gc.time;name=scavenge": 368,
"cpu.systemLoadAverage": -1,
"thread.count": 73,
"classloader.unloadedClasses.total": 0,
"jvm.uptime": 52938,
"cpu.processCpuTime": 23359375000,
"memory.committedHeap": 53805056,
"thread.max.count": 89,
"cpu.availableProcessors": 12,
"classloader.loadedClasses.total": 9448,
"thread.daemon.count": 69,
"memory.maxHeap": 536870912,
"cpu.processCpuLoad": 0.0023284173808607016,
"memory.usedHeap": 41412992
},
"vendor": {
[..]
},
"application": {
[..]
},
}
要找出 gc.total 指标的目的,我们可以通过向 /metrics 发送 OPTIONS 请求来获取指标的元数据。由于此请求的输出将很长,我们只展示 gc.total 指标并模糊其余部分。像 GET 请求一样,每个作用域/注册表的指标都分开到它们自己的 JSON 数组中:
{
"base": {
"gc.total": {
"unit": "none",
"displayName": "Garbage Collection Count",
"name": "gc.total",
"description": "Displays the total number of collections that have occurred. This attribute lists -1 if the collection count is undefined for this
collector.",
"type": "counter",
"tags": [
[
"name=global"
],
[
"name=scavenge"
]
]
},
[...]
},
"vendor": {
[...]
},
"application": {
[...]
}
}
如我们从元数据中看到的,gc.total 指标是一个计数器,用于统计在这个 JVM 中发生的垃圾回收次数。标签用于识别系统上两个不同的垃圾回收器,这两个指标正在监控。
向 /metrics 发送请求是为了展示如何从不同作用域中划分指标。我们也可以调用 /metrics/base/gc.total 来特别检索 gc.total 指标的元数据。
Prometheus 展示格式
使用 Prometheus 展示格式,所有指标数据都通过向 /metrics/* 端点发送 GET 请求一起提供。如果没有指定 application/json,则默认返回 Prometheus 格式。正如其名称所暗示的,这种格式可以直接由 Prometheus 监控工具使用。
格式化指标必须遵循特定的模板。为了描述这一点,我们只看一下 gc.total 指标的输出。这里只使用片段,因为完整的输出会太长:
# TYPE base_gc_total counter
# HELP base_gc_total Displays the total number of collections that have occurred. This attribute lists -1 if the collection count is undefined for this collector.
base_gc_total{name="global"} 9
base_gc_total{name="scavenge"} 372
[...]
在 Prometheus 展示格式中,指标按其指标名称组织。第一组是针对 base_gc_total 指标。这对应于我们在前面提到的 JSON 格式示例中看到的 gc.total 指标。真正的指标名称是 gc.total,但必须转换为 gc_total,因为 Prometheus 格式的指标是包含下划线的字母数字字符。MicroProfile Metrics 运行时还会在指标名称前添加指标所属的注册表作用域的名称。这可能是 base_、vendor_ 或 application_。请注意,标签附加到指标名称的末尾,在波浪线括号内。
每个按指标名称唯一分组的指标前都有一个 # TYPE 行和一个 # HELP 行。这两行定义了指标的类型和描述(如果有的话)。请记住,描述是指标元数据中的一个可选字段。
对于某些指标,存在额外的格式化规则。我们将在下一节中介绍。
配置指标
MicroProfile Metrics 技术提供了一个丰富的 Java API,用于编程配置指标,并提供 CDI 注解,以便轻松为方法、字段甚至整个类配置指标。我们不会涵盖使用 Java API 和其注解的所有可能场景,特别是关于 MetricRegistry 类的使用。相反,本节将解释 API 和注解的主要用途,以便您能够自信地使用这项技术。我们鼓励您在希望完全掌握所有内容时,查阅 MicroProfile Metrics 的 Java 文档。
在本节中,我们将介绍如何通过编程和注释来配置每个指标。这将随后通过 Prometheus 展示格式中 /metrics 端点的输出示例。在此之前,我们将介绍指标注册表、元数据、标签和 MetricID 的技术方面。它们提供了有效配置指标所需的基本知识。
如您从 MicroProfile Metrics 技术概述 部分中回忆的那样,指标注册表是 MicroProfile Metrics 运行时的操作核心。除非您严格使用注解在您的微服务中配置指标,否则您将需要获取一个 MetricRegistry(CDI)bean。正是通过这个 MetricRegistry,我们可以编程地创建、注册和检索指标。即使您严格使用注解来配置指标,您也会在底层与 MetricRegistry 交互。
本节包含大量内容。以下是我们将要涵盖的摘要:
-
获取指标注册表
-
创建、注册和检索指标:
a) 元数据、标签和 MetricIDs
b) 计数器
c) 并发仪表
d) 计量器
e) 计时器和简单计时器
f) 仪表
-
@Metric注解
让我们开始吧!
获取指标注册表
要获取MetricRegistry,我们可以使用注入,如下面的代码示例所示:
@Inject
MetricRegistry metricRegistry;
请记住,存在三种类型的指标注册表作用域:基本指标注册表、供应商指标注册表和应用指标注册表。默认情况下,当你将MetricRegistry注入到你的应用程序中时,MicroProfile Metrics 运行时会提供一个应用注册表。如果你愿意,你可以注入其他类型的注册表。你需要使用@RegistryType注解来注解你的注入,并使用一个注解参数指定要注入的注册表类型。以下示例说明了@RegistryType的使用,其中我们指定类型为MetricRegistry.Type.Application:
@Inject
@RegistryType(type=MetricRegistry.Type.APPLICATION)
MetricRegistry metricRegistry;
如果你指定了@RegistryType(type=MetricRegistry.Type.BASE)或@RegistryType(type=MetricRegistry.Type.VENDOR)注解,则可以注入基本指标注册表和供应商指标注册表。然而,在你的应用程序中,你不应该注册指标或操作基本或供应商指标。这两个指标注册表应仅用于检索指标,以便你可以查看其数据。
关于MetricRegistry和注解使用的说明
当使用注解来配置指标时,你将只与应用程序的指标注册表进行交互。你将无法选择指标注解应用于哪个MetricRegistry。
创建、注册和检索指标
使用MetricRegistry,你可以使用针对每种指标类型特定的方法来创建和注册指标。除了度量表(Gauge)之外,每种指标类型都将具有以下方法签名。从MetricRegistry调用此类方法将在注册表中不存在具有给定名称、元数据和标签的指标时创建、注册并返回该指标的实例。如果已经存在,则返回现有指标。需要注意的是,使用指标注解(除了度量表注解之外)的工作方式类似。我们将使用Counter指标类型来演示方法签名模式:
Counter counter(String name);
Counter counter (String name, Tag... tags);
Counter counter (MetricID metricID);
Counter counter (Metadata metadata);
Counter counter (Metadata metadata, Tag... tags);
其他指标类型的名称为concurrentGauge、timer、simpleTimer、histogram和meter。我们将在特定于指标的章节中演示这些方法的多种用法。度量表(Gauge)也有其自己的方法集,由MetricRegistry提供,但我们将这些内容放在度量表(Gauge)部分进行介绍。
关于指标重用性的说明
不论你是使用MetricRegistry还是使用指标注解来配置你的指标,你可以通过指定匹配的元数据或MetricID值来重用现有的指标。
如果只想检索指标,你可以从MetricRegistry调用getMetrics()、getCounters()、getGauges()、getConcurrentGauges()、getHistograms()、getMeters()、getTimers()或getSimpleTimers()方法之一。这些调用将返回一个包含所需指标的映射,其中MetricID作为键。
有其他方法用于创建、注册、检索和从指标注册表中删除指标,其中一些使用MetricFilter,以及其他与检索元数据和指标 ID 有关的方法。你甚至可以创建自己的指标实现并将其注册到 MicroProfile Metrics 运行时提供的实例上。然而,这些方法将不会在本节中介绍,因为它们太多了!我们鼓励你查阅MetricRegistry类的 Java 文档。我们之前提供的关于使用MetricRegistry的信息是为了帮助你理解后续章节。
元数据、标签和MetricID
正如你在上一节中可能已经注意到的,元数据、标签和MetricID可以被指标注册表在你的应用程序代码中使用。然而,在我们学习如何配置和使用它们之前,我们必须了解如何创建和使用它们。
每个指标都必须包含元数据信息。正如你可能记得的,元数据信息包括其名称、指标类型、度量单位、描述和显示名称。这个集合中必需的字段是名称和指标类型。其他元数据字段是可选的。所有这些信息都包含在一个Metadata对象中。Metadata对象中的每个字段都是String类型。对于指标类型字段,你需要指定来自MetricType枚举的一个enum值。对于单位字段,你需要指定MetricUnits中的静态字段之一。
如果你正在配置多维指标,那么你还需要为你的指标提供标签。每个标签都是一个String值的键值对,并由一个Tag对象表示。标签的名称必须匹配正则表达式[a-zA-Z_][a-zA-Z0-9_]*。标签的值可以是任何内容。一个指标可以包含 0 个或多个标签。然后这个Tag被设置到一个包含指标String名称的MetricID中。
关于可配置标签的说明
使用 MicroProfile Config,我们可以为设置 MicroProfile Metrics 运行时中所有指标的标签值定义两个配置值。mp.metrics.appName接受一个用于标识应用程序名称的单个字符串值。这将作为键值标签_app=<application_name>.附加到所有指标上。mp.metrics.tags配置允许以tag1=value1,tag2=value2的形式定义逗号分隔的键值标签列表。这些标签将被应用到所有指标上。
使用元数据和标签进行编程
当程序化度量指标时,我们需要创建一个 Metadata 对象。为了完成这个任务,我们需要通过调用静态的 Metadata.builder() 方法来检索 MetadataBuilder。使用这个 MetadataBuilder,我们可以使用构建器模式构造一个 Metadata 对象。至少,我们希望指定其名称和度量类型。在以下示例中,我们不会注册任何度量,所以我们将使用 MetricType.INVALID 度量类型。在接下来的部分中,我们将演示如何为每个单独的度量使用适当的 MetricType:
@Inject
MetricRegistry metricRegistry;
public void metaDataExample() {
Metadata metadata = Metadata.builder()
.withName("testMetadata")
.withType(MetricType.INVALID)
.build();
}
要创建一个包含所有字段的 Metadata 对象,你可以这样做。再次强调,由于这个例子是为了演示,我们将使用 MetricUnits.NONE 值。由于即将到来的部分不会大量使用单位字段,我们鼓励你通过查看源文件 bit.ly/3ds4IDK 来探索可用的单位值。以下示例还包括了使用标签和 MetricID。创建一个 Tag 是一个简单的过程,你只需要使用 String 名称和值参数调用 Tag 构造函数。然后,你可以通过将度量名称和可变长度的 Tag 参数传递给 MetricID 构造函数来构造一个 MetricID。
MetricsResource 的完整源代码可以在 bit.ly/2UzoczI 找到:
@ApplicationScoped
@Path("/metricsResource")
public class MetricsResource {
@Inject
MetricRegistry metricRegistry;
public void metadataTagMetricIDExample() {
String metricName = "myMetric";
Metadata metadata = Metadata.builder()
.withName(metricName)
.withType(MetricType.INVALID)
.withDisplayName("Human readable display name")
.withDescription("This metadata example"
+ " demonstrates how to create a"
+ " Metadata object")
.withUnit(MetricUnits.NONE).build();
Tag tag = new Tag("tagKey", "tagValue");
Tag anotherTag = new Tag("anotherTag", "tagValue");
MetricID metricID = new MetricID(metricName, tag, anotherTag);
}
}
通过组合 MetricID、标签和元数据,你可以创建、注册并从 MetricRegistry 中检索度量。如你所回忆的,在前面的部分中,列出了不同的方法签名,MetricID 和 Metadata 从不作为参数一起使用。然而,我们知道度量注册表使用它们来分类和识别已注册的度量。这是因为度量注册表将在处理过程中推断出构建其他对象(无论是 MetricID 还是元数据)所需的最小必要数据。
使用注解与元数据和标签
当使用注解来度量指标时,元数据和标签通过注解参数提供。可能根本不需要指定任何参数。当使用 CDI 时,MicroProfile Metrics 运行时可以推断出必要的信息。这种类型的注解已经提供了一个度量类型,如果没有提供名称,则将使用包名、类名和方法名生成一个名称。或者,在注解用于构造函数的情况下,它将是包名、类名和构造函数名的组合(即类名再次!)。
即使提供了名称,完整的度量名称也是类名和度量名称的组合。然而,这可能会证明是不理想的。为了解决这个问题,每个度量注解参数都包含一个 absolute 参数,你可以将其设置为 true,这样度量就会使用提供的度量名称。
为了演示如何使用注解提供元数据信息,以下代码片段将使用 Counter 类的 @Counted 注解:
@ApplicationScoped
@Path("/metricsResource")
public class MetricsResource {
@Counted(name="sample.metric", displayName="sample metric", description="This sample counter metric illustrates how to instrument a metric annotation", unit=MetricUnits.NONE, absolute=true, tags= {"tag1=value1", "tag2=value2")
public void someMethod() {
//logic
}
}
如我们所见,存在接受 String 值的 name、displayName 和 description 参数。absolute 参数接受一个 Boolean 值。单位接受来自 MetricUnits 的静态字段,标签以 键值 格式接受为 String 值列表。
计数器
我们终于到达了我们的第一个指标:计数器。计数器指标,正如其名称所暗示的,是一个记录指标数量的指标。计数器只能单调递增。你可以使用它来跟踪一个方法或业务逻辑块被调用的次数,或者接收或发送请求的次数。
使用编程方式对计数器进行度量
以下代码示例演示了如何使用两个 GET 请求创建和检索名为 counterMetric 的计数器指标。在第一个 GET 资源,即 /counter1 URI 中,我们通过调用 MetricRegistry.counter(Metadata metadata, Tags… tags) 创建 counterMetric。这将返回一个新的计数器指标,我们可以通过调用 counter.inc() 来递增计数器,这将计数器递增 1。在第二个 GET 资源,即 /counter2 URI 中,我们做了一些不同的事情,并调用 MetricRegistry.counter(MetricID metricID)。在这里,MetricID 与我们在首次创建和注册 counterMetric 时由度量注册器生成的 MetricID 属性相匹配。由于它已经存在,我们通过度量注册器返回现有的 counterMetric。然后我们通过调用 inc(long value) 方法来递增计数器,以指定的数量递增。在我们的示例中,我们递增了 3。在两个 GET 资源中,我们通过调用 getCount() 返回一个包含计数器当前计数的字符串。
CounterResource 的完整源代码可以在 bit.ly/2XGDDXZ 找到:
@GET
@Path("/counter1")
public String getCounter1(){
Metadata counterMetadata = Metadata.builder()
.withName(COUNTER_METRIC_NAME)
.withType(MetricType.COUNTER).build();
Counter counter = metricRegistry
.counter(counterMetadata, COUNTER_TAG);
counter.inc(); //increments by one
return "A counter metric has been created and incremented" + "by 1, the total is now " + counter.getCount(); }
@GET
@Path("/counter2")
public String getCounter2(){
MetricID counterMetricID = new
MetricID(COUNTER_METRIC_NAME,
COUNTER_TAG);
Counter counter = metricRegistry.counter(counterMetricID);
counter.inc(3);
return "A counter metric was retrieve and incremented" + " by 3, the total is now " + counter.getCount();
}
现在,让我们看看当我们向两个 GET 资源发送请求,然后通过 /metrics/application/counterMetric 直接查看结果时会发生什么:
$ curl http://localhost:9080/ch6/counterResource/counter1
A counter metric has been created and incremented by 1, the total is now 1
$ curl http://localhost:9080/ch6/counterResource/counter2
A counter metric was retrieve and incremented by 3, the total is now 4
$ curl http://localhost:9080/metrics/application/counterMetric
# TYPE application_counterMetric_total counter
application_counterMetric_total{metricType="counter"} 4
在输出中,我们向 /ch6/counterResource/counter1 和 /ch6/counterResource/counter2 端点发出 GET 请求,计数器指标分别递增 1 和 3。然后我们向 /metrics/application/counterMetric 发出 GET 请求,直接查看计数器指标的 Prometheus 格式输出。返回 application_counterMetric_total{metricType="counter"},它代表具有 metricType="counter" 标签的计数器指标。其值为 4,正如预期的那样。
关于计数器的 Prometheus 格式说明
Prometheus 展示格式中的计数器指标将在指标名称后附加 _total 后缀。
使用注解对计数器进行度量
使用注解是一个更简单的事情。您可以在方法、构造函数甚至整个类上注解@Counted注解。当注解的元素被调用时,计数器增加 1。
在我们的示例中,我们将使用@Counted注解来注解MetricsResource类。当一个度量注解注解在类上时,它将应用于该注解的所有适用目标。对于@Counted,这意味着所有构造函数和方法都将被度量。此示例还将演示生成的度量名称。请注意,由于我们使用注解,我们不需要注入MetricRegistry。
CounterAnnotatedResource的完整源代码可以在bit.ly/3iZiL6D找到:
@ApplicationScoped
@Path("/counterResource")
@Counted
public class CounterAnnotatedResource {
@GET
@Path("/getResource")
public String getResource() {
return "Counting the class";
}
}
让我们驾驶一下应用程序。我们将省略显示到应用程序 REST 端点的curl命令,只显示查询/metrics/application的输出:
$ curl http://localhost:9080/metrics/application
# TYPE application_metrics_demo_CounterAnnotatedResource
_getResource_total counter
application_metrics_demo_CounterAnnotatedResource_getResource_total 1
# TYPE application_metrics_demo_CounterAnnotatedResource
_CounterAnnotatedResource_total counter
application_metrics_demo_CounterAnnotatedResource_CounterAnnotatedResource_total 1
在向/ch6/counterResource/getResource发出单个GET请求后,我们应该在查看/metrics/application端点的度量数据时看到上述值。application_metrics_demo_CounterAnnotatedResource_getResource_total是针对getResource()方法创建的计数器度量,而application_metrics_demo_CounterAnnotatedResource_CounterAnnotatedResource_total是针对类构造函数创建的计数器度量。两个值都是1,正如预期的那样。
并发度量
并发度量指标是一种用于计算被测量组件的并行调用的度量。其值可以增加或减少。此度量可以用来计算方法、业务逻辑、请求等的并行调用次数。除了计算并行调用次数外,并发度量指标还跟踪之前已完成的完整分钟内记录的最高和最低计数。一个完成的完整分钟表示时钟从 0:00:00.9999999 到 0:00:59.99999999 的时间段。一个完成的完整分钟并不意味着从当前瞬时时间的最后 60 秒。
以编程方式对并发度量指标进行度量
在本节中,我们将演示如何使用并发量规。它们通常使用名为 sleeper 的 Runnable 并行调用。这创建并随后检索一个名为 concurrentGaugeMetric 的并发量规。在这个例子中,我们将使用 MetricRegistry.concurrentGauge(String name) 与度量注册表进行交互。这是度量注册表提供的最简单的创建或检索方法,因为您只需要提供名称。这表示与此度量没有关联的标签。然后,sleeper Runnable 将增加并发量规(例如,使用 inc()),睡眠 10 秒,然后减少它(例如,使用 dec())。您只能增加或减少 1。我们将使用 for 循环和 ExecutorService 进行并行调用。然而,在此代码示例中没有显示三个值的获取方法;即 getCount()、getMin() 和 getMax()。
ConcurrentGaugeResource 的完整源代码可以在 bit.ly/3ghFyZz 找到:
@GET
@Path("/concurrentGauge")
public String getConcurrentGage(){
ExecutorService executorService =
Executors.newCachedThreadPool();
Runnable sleeper = () -> {
ConcurrentGauge concurrentGauge = metricRegistry.concurrentGauge
(CONCURRENTGAUGE_METRIC_NAME);
concurrentGauge.inc();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
concurrentGauge.dec();
};
for (int i = 0; i < 10; i++) {
executorService.submit(sleeper);
}
return "Concurrent Gauge created and invoked in parallel";
}
对于这个例子,我们将发送一个名为 /ch6/concurrentGaugeResource/concurrentGauge 的 GET 请求。一旦当前分钟完成,我们将通过 /metrics/application 查看输出:
$ curl http://localhost:9080/ch6/concurrentGaugeResource
/concurrentGauge
Concurrent Gauge created and invoked in parallel
$ curl http://localhost:9080/metrics/application
# TYPE application_concurrentGaugeMetric_current gauge
application_concurrentGaugeMetric_current 10
# TYPE application_concurrentGaugeMetric_min gauge
application_concurrentGaugeMetric_min 0
# TYPE application_concurrentGaugeMetric_max gauge
application_concurrentGaugeMetric_max 0
## after a complete full minute…
$ curl http://localhost:9080/m**etrics/application**
# TYPE application_concurrentGaugeMetric_current gauge
application_concurrentGaugeMetric_current 0
# TYPE application_concurrentGaugeMetric_min gauge
application_concurrentGaugeMetric_min 0
# TYPE application_concurrentGaugeMetric_max gauge
application_concurrentGaugeMetric_max 10
在前面的输出中,我们向 /ch6/concurentGaugeResource/concurrentGauge 发出了一个 GET 请求。然后,我们跟进了一个 GET 请求,/metrics/application,以查看输出。application_concurrentGaugeMetric_current 显示了预期的当前值 10。application_concurrentGaugeMetric_max 和 application_concurrentGaugeMetric_min,显示了上一分钟记录的最大和最小值,都是预期的 0。在当前整分钟完成之后,我们再次查看结果,我们看到当前、最大和最小值都是预期的 0、0 和 10。
关于具有多个值的度量注意事项
并发量规是我们第一个具有多个输出值的度量。为了使用相同的度量名称显示所有值,每个度量值都分配了自己的后缀。我们将在其他复杂度量中看到这种模式。
在我们向 /ch6/concurrentGaugeResource/concurrentGaugeParallel 发出 GET 请求后立即,我们将看到并发量规的当前计数为 10。当每个线程的 10 秒已过并且已经过去整整一分钟时,我们将看到当前值是 0,最大值是 10。
使用注解仪表化并发量规
要使用注解对并发量规进行仪表化,您必须使用 @ConcurrentGauge 注解。这适用于方法、构造函数和类。当目标被调用时,并发量规注解将增加,当它完成时将减少。
我们将以与程序示例类似的方式演示 @ConcurrentGauge 的用法。sleeper 可运行对象将调用带有 @ConcurrentGauge 注解的 sleeper() 方法。在这个例子中,我们将指定 absolute=true,这将导致 MicroProfile Metrics 运行时使用指标名称。/metrics/* 输出将与程序示例相同,因此在此处不会展示。
ConcurrentGaugeAnnotatedResource 的完整源代码可以在 bit.ly/3xZZhD0 找到:
@GET
@Path("/concurrentGuage")
public String getConcurrentGauge(){
ExecutorService executorService = Executors.newCachedThreadPool();
Runnable sleeper = () -> sleeper();
for (int i = 0; i < 10; i++) {
executorService.submit(sleeper);
}
return "Concurrent Gauge created and invoked in parallel";
}
@ConcurrentGauge(name = CONCURRENTGAUGE_METRIC_NAME, absolute = true)
public void sleeper() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
直方图
直方图指标,就像直方图图表一样,以统计分布的方式处理它所提供的数据。直方图指标输出 12 个值:计数、总和、最小值、最大值、平均值、标准差以及第 50、75、95、98、99 和 99.9 个百分位数。与其他指标不同,直方图指标只能通过编程方式进行仪表化。没有注解支持。您可以使用直方图指标来记录和计算应用程序在处理过程中接收到的数据大小的分布。
在我们的演示中,我们将生成 1,000 个介于 0-999 范围内的随机数,并将它们输入到我们的直方图中。这次,我们将使用 metricRegistry.histogram(Metadata metadata) 来创建我们的直方图。我们不会展示 getCount()、getSum() 和 getSnapshot() 获取器方法,这些方法返回包含剩余统计值获取器方法的 Snapshot 对象。由于这会太长而无法列出,您可以在以下位置查看 Snapshot 类及其方法:bit.ly/2QndNFf。
HistogramResource 的完整源代码可以在 bit.ly/3y4AoWK 找到:
@GET
@Path("/histogram")
public String getHistogram() {
Metadata histogramMetadata = Metadata.builder()
.withName(HISTOGRAM_METRIC_NAME)
.withUnit(MetricUnits.MILLISECONDS)
.withDescription("This histogram tracks random
millesconds")
.withType(MetricType.HISTOGRAM).build();
Histogram histogram = metricRegistry.histogram(histogramMetadata);
Random random = new Random();
for (int i = 0; i < 1000 ; i++) {
int randomInt = random.nextInt(1000);
histogram.update(randomInt);
}
int count = (int) histogram.getCount(); //returns long value of count
Snapshot snapshot = histogram.getSnapshot(); //rest of the stats
return "Histogram created/retrieved and is tracking random milliseconds";
}
}
让我们看看我们会得到什么结果:
$ curl http://localhost:9080/ch6/histogramResource
/histogram
Histogram created/retrieved and is tracking random milliseconds
$ curl http://localhost:9080/metrics/application
# TYPE application_histogramMetric_mean_seconds gauge
application_histogramMetric_mean_seconds 0.5048109999999999
# TYPE application_histogramMetric_max_seconds gauge
application_histogramMetric_max_seconds 0.998
# TYPE application_histogramMetric_min_seconds gauge
application_histogramMetric_min_seconds 0.0
# TYPE application_histogramMetric_stddev_seconds gauge
application_histogramMetric_stddev_seconds 0.2884925116515156
# TYPE application_histogramMetric_seconds summary
# HELP application_histogramMetric_seconds This histogram tracks random millesconds
application_histogramMetric_seconds_count 1000
application_histogramMetric_seconds_sum 504.81100000000004
application_histogramMetric_seconds{quantile="0.5"} 0.507
application_histogramMetric_seconds{quantile="0.75"} 0.755
application_histogramMetric_seconds{quantile="0.95"} 0.9510000000000001
application_histogramMetric_seconds{quantile="0.98"} 0.974
application_histogramMetric_seconds{quantile="0.99"} 0.981
application_histogramMetric_seconds{quantile="0.999"} 0.995
在前面的输出中,我们向 /ch6/histogramResource/histogram 发出了一个 GET 请求,并随后向 /metrics/application 发出了一个 GET 请求以查看结果。正如预期的那样,计数为 1,000,这是由 application_histogramMetric_seconds_count 值报告的。剩余的指标值是计算得出的值。由于值数量众多,我们不会明确覆盖所有这些值。提供的指标值名称是自解释的,以表明它们代表什么值。
关于使用直方图的 Prometheus 格式说明
如果已定义了单位,则将指标名称附加到单位上作为 _<unit>。Prometheus 只接受某些 基本单位,因此 MicroProfile Metrics 运行时将值缩放到适当的基单位。例如,如果指定了毫秒作为单位,则值将缩放到秒的基单位。
此外,请注意,分位数指标值具有相同的名称,但使用标签来标识它代表的是哪个百分位数。
计数器
仪表度量,就像直方图度量一样,聚合输入值并执行计算以产生结果。仪表计算的是每秒的速率,而不是统计分布。指定的度量单位将被忽略。这仅适用于 Prometheus 输出。仪表输出平均速率以及 1、5 和 15 分钟的指数加权移动平均速率。仪表对于监控微服务中特定方法或组件的流量非常有用。
以编程方式对仪表进行配置
在我们的示例中,我们将演示使用仪表度量来监控两个 GET 资源 /meter 和 /meter2 的请求速率。对于第一个 GET 资源,我们将使用我们尚未与 MetricRegistry.meter(String metricName, Tags… tags) 一起使用的注册/检索方法的最后一个变体。一旦创建或检索到度量,我们将调用 mark() 方法,该方法将仪表记录的点击次数增加 1。对于第二个 GET 资源,我们可以传递一个长参数值,以便调用 mark(long value),这将按指定值增加仪表的点击次数。注意,我们在 /meter2 GET 资源中使用 MetricID 来检索在 /meter 资源中创建和注册的度量。
MeterResource 的完整源代码可以在 bit.ly/3ASCV8j 找到:
private final Tag METER_TAG = new Tag("metricType", "meter");
@GET
@Path("/meter")
public String getMeter(){
Meter meter = metricRegistry.meter(METER_METRIC_NAME, METER_TAG);
meter.mark();
return "Meter created/retrieved and marked by 1";
}
@GET
@Path("/meter2")
public String getMeter2(@QueryParam("value")
@DefaultValue("1") int value){
MetricID meterMetricID = new MetricID(METER_METRIC_NAME, METER_TAG);
Meter meter = metricRegistry.meter(meterMetricID);
meter.mark(value);
return "Meter created/retrieved and marked by " + value;
}
}
未显示的是值的获取方法;即 getCount()、getMeanRate()、getOneMinuteRate()、getFiveMinuteRate() 和 getFifteenMinuteRate()。让我们运行一下对 GET 资源的访问,并在 /metrics/application 查看结果:
$ curl http://localhost:9080/ch6/meterResource/meter
Meter created/retrieved and marked by 1
$ curl http://localhost:9080/ch6/meterResource/meter2?value=3
Meter created/retrieved and marked by 3
$ curl http://localhost:9080/metrics/application
# TYPE application_histogramMetric_total counter
application_histogramMetric_total{metricType="meter"} 4
# TYPE application_histogramMetric_rate_per_second gauge
application_histogramMetric_rate_per_second{metricType="meter"} 0.4348951236275281
# TYPE application_histogramMetric_one_min_rate_per_second gauge
application_histogramMetric_one_min_rate_per_second{metricType="meter"} 0.8
# TYPE application_histogramMetric_five_min_rate_per_second gauge
application_histogramMetric_five_min_rate_per_second{metricType="meter"} 0.8
# TYPE application_histogramMetric_fifteen_min_rate_per
_second gauge
application_histogramMetric_fifteen_min_rate_per_second{metricType="meter"} 0.8
在前面的输出中,我们向 /ch6/meterResource/ 仪表发送了一个 GET 请求,将其增加 1,然后向 /ch6/meterResource/meter2 发送了一个 GET 请求,并提供了参数值以将仪表增加 3。然后我们在 /metrics/application 中查看了结果输出。application_histogramMetric_total 显示计数为 4,正如预期的那样,其余的值是计算值。再次强调,与剩余度量值相关的名称是自解释的,将不会进行明确解释。
使用注释对仪表进行配置
要使用注释对仪表度量进行配置,必须使用 @Metered 注解。此注解适用于方法、构造函数和类。与其他注解度量一样,使用注解只能增加单个值。我们将演示一个使用 @Metered 注解的示例,并省略显示结果。
MeterAnnotatedResource 的完整源代码可以在 bit.ly/3mhnHpk 找到:
@GET
@Path("/meter")
@Metered(name=METER_METRIC_NAME, tags={"metricType=meter"})
public String getMeterWithAnnotations() {
return "Meter created/retrieved and marked by 1 with annotations";
}
计时器和简单计时器
由于计时器和简单计时器度量非常相似,我们将演示如何一起使用这两个度量。
计时器指标,正如其名称所暗示的,记录通过仪器组件所花费的时间。在其核心,它跟踪总经过时间。此外,它还提供了记录的击中次数的吞吐量/速率,以及记录时间的统计分布。这些输出的值与直方图和计量指标相同。
另一方面,简单计时器指标是一个计时器,但去掉了额外的功能。它只报告计数、总经过时间,以及,类似于并发计量器,上一完整分钟的最高和最低记录时间。如果你不需要计时器提供的所有额外值,或者打算稍后自己计算它们,那么简单计时器应该是你的首选指标。
以编程方式仪器化计时器和简单计时器
在我们的示例中,我们将在各自的GET资源中仪器化计时器和简单计时器。在这两个资源中,我们将提供一个使用Context对象记录时间的示例。这允许我们通过从计时器或简单计时器调用time()方法来显式标记我们想要计时的开始和结束,以开始计时,然后调用Context对象的close()方法来停止计时。请注意,Context对象是Timer和SimpleTimer类的内部接口,并且你需要使用适当的Context对象。计时器和简单计时器指标都可以计时Runnable或Callable对象或 lambda 表达式的执行。以下两个代码片段来自同一个TimersResource类,完整的源代码可以在bit.ly/37YaWYy找到。
以下代码片段显示了名为/timer的GET资源,它使用计时器指标演示了使用Runnable对象进行计时:
@GET
@Path("/timer")
public String getTimer() {
Timer timer = metricRegistry.timer(TIMER_METRIC_NAME);
Timer.Context timerContext = timer.time();
timerContext.close();
Runnable runnableTimer = () -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// Time a Runnable
timer.time(runnableTimer);
return "Timer created/retrieved and recorded total elapsed time of " + timer.getElapsedTime();
}
以下代码片段显示了名为/simpleTimer的GET资源,它使用简单计时器指标演示了使用Callable对象进行计时:
@GET
@Path("/simpleTimer")
public String getSimpleTimer(){
SimpleTimer simpleTimer = metricRegistry.simpleTimer(SIMPLETIMER_METRIC_NAME);
SimpleTimer.Context simpleTimerContext = simpleTimer.time();
simpleTimerContext.close();
// Time a Callable
Callable<String> callable = () -> {
Thread.sleep(2000);
return "Finished Callable";
};
simpleTimer.time(callable);
return "SimpleTimer created/retrieved and recorded total elapsed time of " + simpleTimer .getElapsedTime();
}
未显示的是获取指标值的获取方法。对于计时器,你可以调用getCount()、getElapsedTime()、getSnapshot()、getMeanRate()、getOneMinuteRate()、getFiveMinuteRate()和getFifteenMinuteRate()。对于简单计时器,你可以调用getCount()、getElapsedTime()、getMinTimeDuration()和getMaxTimeDuration()。
让我们来看看GET资源,并查看结果:
$ curl http://localhost:9080/ch6/timersResource/timer
Timer created/retrieved and recorded total elapsed time of 2001 milliseconds
$ curl http://localhost:9080/ch6/timersResource/simpleTimer
SimpleTimer created/retrieved and recorded total elapsed time of 2000 milliseconds
$ curl http://localhost:9080/metrics/application
# TYPE application_simpleTimerMetric_total counter
application_simpleTimerMetric_total 1
# TYPE application_simpleTimerMetric_elapsedTime_seconds gauge
application_simpleTimerMetric_elapsedTime_seconds 2.0005379000000003
# TYPE application_simpleTimerMetric_maxTimeDuration
_seconds gauge
application_simpleTimerMetric_maxTimeDuration_seconds NaN
# TYPE application_simpleTimerMetric_minTimeDuration
_seconds gauge
application_simpleTimerMetric_minTimeDuration_seconds NaN
首先,我们向/ch6/timersResource/timer和/ch6/timersResource/simpleTimer发送GET请求以调用我们的两个计时器。然后,我们向/metrics/application发送请求以查看结果。由于我们已经展示了并发仪表的最大和最小行为的相似性,因此我们在此不会演示简单计时器的该行为。此外,由于计时器指标输出记录时间的统计分布(包括总记录持续时间)和请求吞吐量,类似于直方图和仪表指标,因此计时器指标的输出将被省略。剩下的就是简单计时器的输出。请注意,application_simpleTimerMetric_maxTimeDuration_seconds和application_simpleTimerMetric_minTimeDuration_seconds的值报告为NaN。这是因为之前完成的分钟内没有记录的值。如果您想查看完整的输出,我们鼓励您直接尝试样本。请查看本章开头的技术要求部分,了解如何运行样本的说明。
使用注解仪表化计时器和简单计时器
要对计时器和简单计时器指标进行仪表化,您需要分别使用@Timed和@SimplyTimed。这些注解适用于方法、构造函数和类。它们都会记录目标注解元素执行所需的时间。
我们将展示一个简单示例,演示如何在 JAX-RS 端点上注解@Timed和@SimplyTimed。
TimersAnnotatedResource的完整源代码可以在bit.ly/3xVroDb找到:
@GET
@Path("/timers")
@Timed(name=ANNOTATED_TIMER_METRIC_NAME)
@SimplyTimed(name= ANNOTATED_SIMPLETIMER_METRIC_NAME)
public String getTimerWithAnnotations() {
//some business logic to time
return "Timer with annotations";
}
仪表
仪表指标用于报告应用程序提供的某些值。这可以是任何值,但强烈建议该值是一个数字,因为 Prometheus 仅支持数字仪表。这并不是 JSON 输出的限制。此外,您只能使用指标注册表的创建仪表的方法来创建数字仪表。
程序化仪表化仪表
如我们之前提到的,仪表指标不遵循与其他指标相同的注册和检索方法签名模式。这是由于仪表指标的性质所致。在注册或检索仪表时,您需要指定一个Supplier或Function对象或 lambda 表达式。
以下是为注册或检索仪表指标的方法签名:
<T, R extends Number> Gauge<R> gauge(String name, T object, Function<T, R> func, Tag... tags);
<T, R extends Number> Gauge<R> gauge(MetricID metricID, T object, Function<T, R> func);
<T, R extends Number> Gauge<R> gauge(Metadata metadata, T object, Function<T, R> func, Tag... tags);
<T, R extends Number> Gauge<R> gauge(Metadata metadata, T object, Function<T, R> func, Tag... tags);
<T extends Number> Gauge<T> gauge(MetricID metricID, Supplier<T> supplier);
<T extends Number> Gauge<T> gauge(Metadata metadata, Supplier<T> supplier, Tag... tags);
您可以使用仪表指标调用的唯一显著方法是getValue()。由于您应该熟悉MetricID类、Metadata类、如何创建指标以及 Java 函数(我们假设您熟悉),因此我们不会提供任何用于程序化仪表化仪表指标的示例代码。
使用注解仪表化仪表
要对量规指标进行仪表化,您需要使用 @Gauge 注解。此注解只能应用于方法。使用量规注解时,您必须指定单位参数。我们将展示一个简单示例,其中方法(因此是量规)将返回自上次纪元以来的当前毫秒数。
GaugeResource 的完整源代码可以在 bit.ly/3mfj6Ux 找到:
@ApplicationScoped
@Path("/metricsResource")
public class MyMetricsResource {
@Gauge(name="time.since.epoch", unit = MetricUnits.MILLISECONDS)
public long getGaugeWithAnnotations() {
return System.currentTimeMillis();
}
}
我们假设一个 GET 请求调用此方法,所以我们只在这里展示结果 /metrics/application 输出:
$ curl http://localhost:9080/metrics/application
# TYPE application_metrics_demo_gaugeResource_time_since
_epoch_seconds gauge
application_metrics_demo_gaugeResource_time_since_epoch
_seconds 1.6181035765080001E9
关于 Prometheus 格式化量规的注意事项
为该量规定义的单位附加为 _<unit> 并将其缩放到适当的基单位。
@Metric 注解
@Metric 注解是一种独特的注解,允许您注入与被注解的字段或参数类型相对应的指标。@Metric 注解包含与其他指标注解相同的注解参数。如果存在匹配的元数据,它将返回一个指标;否则,将创建、注册并注入指定类型的新指标。让我们看看使用两种注入策略的示例。
MetricsResource 的完整源代码可以在 bit.ly/3iBAz7E 找到:
@Inject
@Metric(name="fieldInjectedCounter")
Counter fieldInjectedCounter;
Counter parameterInjectedCounter;
@Inject
public void setCounterMetric(@Metric(name = "parameterInjectedCounter") Counter parameterInjectedCounter) {
this.parameterInjectedCounter = parameterInjectedCounter;
}
在上述示例中,fieldInjectedCounter 使用字段注入进行注入,而 parameterInjectedCounter 使用参数注入进行注入。
使用 Prometheus 和 Grafana 可视化指标数据
MicroProfile 指标运行时只能报告瞬时指标值。为了有效地使用这些数据用于监控目的,我们需要使用 Prometheus 等工具聚合这些数据。然后,使用 Grafana 等工具,我们可以创建各种可视化,展示可配置时间段内的指标数据。Prometheus 可以从多个来源抓取数据,然后 Grafana 通过对 Prometheus 执行查询(使用 REST.request),从 StockTrader 应用程序的代理微服务中获取数据。
理解 REST.request 指标
REST.request 指标是一个简单的计时器,由 MicroProfile 指标运行时自动仪表化到所有 REST 端点。仪表化的 REST.request 指标通过与类名和方法签名相关的标签区分彼此。
代理微服务
代理服务包含多个 JAX-RS/REST 端点,用于创建、检索和删除代理对象,以及使用 GET、POST 和 DELETE 请求检索投资组合回报。所有这些都在 AccountService 类中发生。完整源代码可以在 bit.ly/3sBGvPE 找到。
首先,我们将查看REST.request指标的样本输出,以便在演示如何使用 Grafana 查询它之前,了解指标名称的格式及其标签。我们将展示一个GET端点的输出,该端点查询所有方法为getAccounts()的账户。其他基本指标(REST.request的最大时间和最小时间值以及指标描述)已从输出中省略:
$ curl http://localhost:9080/metrics/base
# TYPE base_REST_request_total counter
base_REST_request_total{class="com.ibm.hybrid.cloud.sample.stocktrader.account.AccountService",method="getAccounts"} 45
# TYPE application_simpleTimerMetric_elapsedTime_seconds gauge
base_REST_request_elapsedTime_seconds{class="com.ibm.hybrid.cloud.sample.stocktrader.account.AccountService",method="getAccounts"} 1.7304427800000002
使用 Grafana 进行可视化
在 Grafana 中,我们可以通过查询指标名称来创建每个指标的可视化。例如,我们可以简单地查询base_REST_request_total,Grafana 将显示该指标的所有实例,该指标计算对 REST 端点的请求调用次数。或者,如果我们只想查看单个微服务(如AccountService)的指标,我们可以发出以下查询:
base_REST_request_total{class=" com.ibm.hybrid.cloud.sample. stocktrader.account.AccountService"}
然而,仅仅计数器的总计数并不能告诉我们太多。我们更感兴趣的是知道在过去 10 分钟内指标增加了多少次。在这里,我们可以执行以下查询:
increase(base_REST_request_total[10m]).
或者,也许我们想知道在过去 10 分钟内请求增加的速率:
rate(base_REST_request_total[10m])
当使用简单的计时器时,我们最感兴趣的是计时数据。然而,仅凭经过的时间本身并没有什么意义,但我们可以计算一个新的值,这可能会更有用。使用经过的时间和计数,我们可以使用以下查询计算每个请求的平均持续时间:
rate(base_REST_request_elapsedTime_seconds[10m]) / rate(base_REST_request_total[10m]).
以下是对上述查询的图形可视化。快照的详细信息并不重要;快照的作用是说明使用 Grafana 时您期望看到的布局。查询输入在顶部,可视化显示在中间,底部是查询的指标表格或列表:

图 6.3 – Grafana 图形可视化
这些示例仅展示了使用 Prometheus 和 Grafana 的潜在能力的一小部分。在前面的图中,我们只使用了图形可视化。存在大量适合您可能需要的特定可视化需求的可视化。除此之外,还有大量可用于与 PromQL 一起使用的函数,以计算您和您的团队可能发现有用的任何特定值。还应注意的是,前面的图只显示了单个可视化的直接视图。请记住,您可以使用多个可视化同时显示的仪表板。
现在我们已经到达了 MicroProfile Metrics 部分的结尾。在您的微服务中配置了指标后,您可以详细监控应用程序的不同部分。在下一节中,我们将学习如何使用 MicroProfile OpenTracing 观察跨越多个微服务的请求。
使用 MicroProfile OpenTracing 跟踪您的云原生应用程序
我们将通过探讨 MicroProfile OpenTracing 技术来结束我们的 MicroProfile 可观察性之旅。与本章中我们检查的其他两种技术相比,MicroProfile OpenTracing 要轻量得多。我们将概述这项技术的重要性,并直接学习如何使用 MicroProfile OpenTracing。
微服务原生应用中 MicroProfile OpenTracing 的重要性及概述
MicroProfile OpenTracing 技术与分布式跟踪的概念相结合。在云环境中,应用程序或微服务相互通信和交互,反过来,它们还可以与其他微服务交互。这种交互链可能相当长,具体取决于应用程序部署的性质和上下文。当出现意外故障时,在如此复杂和分布式的拓扑结构中诊断问题可能是一个困难和麻烦的任务。
这就是分布式跟踪发挥作用的地方。分布式跟踪使我们能够跟踪和监控请求或过程,它在从应用程序到另一个应用程序的导航过程中。在其旅程中,被称为跟踪,性能数据(例如,花费的时间)、以标签形式存在的上下文数据以及任何重要的日志都会为跨度检索。跨度定义了构成跟踪的个体分层段。每个跨度都可以通过名称进行识别。
例如,调用一个方法会创建一个名为method1的跨度。然后,这个方法可以调用另一个方法,这将创建一个新的子跨度,名为method2,它位于第一个方法的父跨度范围内。当子跨度完成(即,方法完成调用)时,它返回到第一个方法,当第一个方法完成时,跟踪完成。子跨度的数量没有限制。生成的跟踪记录将被发送到外部工具或平台,这些工具或平台收集并存储这些记录,并提供一种方式,使我们能够查看所有跟踪及其包含的跨度。
通过这种方式,我们可以分析和理解请求的性能和延迟,以及从单个跨度中检索的任何附加上下文信息,当它通过多个微服务导航时。有了分布式跟踪,我们可以轻松地分析请求的性能和延迟,并诊断发生的任何错误或故障。
要使分布式跟踪在系统中有效,所有应用程序都必须使用相同的分布式跟踪库。现在,你可能认为这正是 MicroProfile OpenTracing 旨在满足的。但这并非事实。MicroProfile OpenTracing 技术建立在现有的 OpenTracing 技术之上。OpenTracing 技术是一个定义了用于在应用程序中实施分布式跟踪的供应商中立 API 的外观。这种 OpenTracing 技术被整合到 MicroProfile OpenTracing 运行时中。为了能够将跟踪仪器应用于您的应用程序,您需要使用兼容的 跟踪器 实现。您可以在 opentracing.io/docs/supported-tracers/ 查看兼容的跟踪器。然而,请注意,不同的 MicroProfile OpenTracing 运行时与不同的跟踪库集兼容。请查阅您选择的运行时的文档以获取更多详细信息。甚至可能的情况是,您选择的运行时可能支持 OpenTracing 官方支持跟踪器列表之外的跟踪器。
系统中的每个应用程序都需要配置以使用相同的跟踪库。不同的跟踪库在如何传达跟踪的上下文识别数据方面可能有所不同,这被称为 span 上下文。span 上下文包含伴随请求在网络微服务中导航的状态信息。这使得 OpenTracing 技术能够在跨越应用程序边界时将 span 链接在一起形成一个单一的跟踪。
MicroProfile OpenTracing 通过定义一个额外的 @Traced 注解来完善 OpenTracing 技术。然而,MicroProfile OpenTracing 的主要好处是您可以在入站和出站 JAX-RS 请求上自动检测跟踪。任何 JAX-RS 应用都将被跟踪,而无需开发者处理 MicroProfile OpenTracing API 或 OpenTracing API。本书中我们将不涵盖如何使用 OpenTracing API,只介绍 MicroProfile OpenTracing 提供的修改。我们将探索 OpenTracing API 及其文档的任务留给你。您可以在 bit.ly/3gEHLis 查看相关信息。
实现库的制作者也可能提供一个平台/服务器,用于聚合跟踪记录。我们将在本节末尾使用 Jaeger 跟踪平台来演示这一点。
关于 OpenTracing 的特别说明
在撰写本文时,OpenTracing 项目已与 OpenCensus 合并,形成 OpenTelemetry。OpenTelemetry 是一种全能技术,将满足您在跟踪、日志记录和指标方面的监控需求。MicroProfile 平台的未来迭代可能会看到 OpenTelemetry 及其子组件的整合。
自动检测 JAX-RS 请求
MicroProfile OpenTracing 允许您在客户端和服务器端自动插装 JAX-RS 请求的跟踪。当通过 JAX-RS 客户端或使用 MicroProfile Rest Client 发送请求时,将自动创建一个跨度。如果已存在活动跨度,则它将是活动跨度的子跨度。此跨度从客户端发送请求时开始。
类似地,当接收到传入的 JAX-RS 请求时,将创建一个跨度。如果请求是跟踪的一部分,MicroProfile OpenTracing 运行时将通过尝试从传入请求中提取跨度上下文信息来自动确定这一点。如果存在此类数据,则新创建的跨度将是此跟踪中先前跨度的子跨度。如果没有活动跨度或可提取的跨度上下文信息,则将创建一个新的跨度,随后创建一个新的跟踪。此跨度从接收到请求时开始,并与 JAX-RS 资源方法相关联。这种在 JAX-RS 资源方法上自动插装的默认行为可以通过使用 @Traced 注解来覆盖,这将在 插装 @Traced 注解和注入 Tracer 部分中介绍。
我们将在描述如何自动插装出站和传入 JAX-RS 请求之后,介绍有关名称和标签的一些附加规则。
出站的 JAX-RS 请求
在出站的 JAX-RS 请求中,创建的跨度将使用要调用的 HTTP 方法的名称。例如,一个 GET 请求将导致名为 GET 的跨度。
关于使用 JAX-RS 客户端的说明
如果您正在使用 JAX-RS 客户端创建出站请求,则需要将您创建的 ClientBuilder 传递给 ClientTracingRegistrar,以便 MicroProfile OpenTracing 运行时为其创建一个跨度。您可以调用 configure(ClientBuilder clientBuilder) 或 configure(ClientBuilder clientBuilder, ExecutorService executorService) 静态方法,这将返回一个 ClientBuilder 对象,您可以使用它。MicroProfile OpenTracing 运行时的实现可能已经被配置,以便任何使用的 ClientBuilder 都将创建一个跨度,因此不需要调用 configure(…) 方法。有关详细信息,请参阅您的 MicroProfile OpenTracing 运行时文档。
传入的 JAX-RS 请求
在传入的 JAX-RS 请求中,创建的跨度将使用以下格式的名称:
<HTTP method>:<package name>.<class name>.<method name>
这被称为 类-方法 命名格式,是默认格式。或者,您可以使用 http-path 命名格式,它使用以下格式:
<HTTP method>:<@Path value of endpoint's class>/<@Path value of endpoint's method>.
要启用 http-path 格式,请使用名为 mp.opentracing.server.operation-name-provider 的 MicroProfile Config 配置元素,并指定 http-path。
跨度标签
在传入和出站请求中创建的跨度使用以下标签:
-
"Tags.SPAN_KIND_CLIENT"。传入请求的值为"Tags.SPAN_KIND_SERVER"。 -
Tags.HTTP_METHOD:这个值是已调用的 HTTP 方法。
-
Tags.HTTP_URL:这是请求已发送到的 HTTP URL 的值。
-
Tags.HTTP_STATUS:这是 HTTP 请求的状态。它指定了客户端收到的响应或服务器返回的响应。
-
jaxrs. -
event=error和error.object=<error object instance>。
仪器化@Traced注解并注入跟踪器
MicroProfile OpenTracing 提供了@Traced CDI 注解。这个注解可以应用于方法和类。当应用于类时,类中的每个方法都被注解为@Traced。@Traced注解可以用来进一步微调构成跟踪的跨度。它也可以用来覆盖 JAX-RS 资源方法的默认自动仪器化,例如禁用或重命名跨度,或者进一步指定应用程序中其他方法的跨度。
@Traced注解包含两个参数:
-
value:这是一个布尔参数。默认值为 true,这意味着被注解的方法将被自动用于跟踪。false 值将禁用方法的自动跟踪。这可以用来在 JAX-RS 端点上禁用自动仪器化。
-
String并定义了当方法被调用时将创建的跨度的名称。
注意,当类及其内部的方法都使用了@Traced注解时,方法注解及其参数具有优先级。
MicroProfile OpenTracing 运行时还可以注入一个可选的io.opentracing.Tracer对象。使用这个 OpenTracing 对象,你可以使用 OpenTracing API 编程创建和操作跨度。你可以添加自己的标签、日志和行李。我们不会在本书中介绍如何使用 OpenTracing API。
以下示例展示了如何注入 OpenTracing 的Tracer对象,以及如何在 JAX-RS 端点和普通业务方法上使用@Traced。
TraceResource的完整源代码可以在bit.ly/3AXmiIr找到:
@Path("/traceResource")
public class TraceResource {
@Inject
io.opentracing.Tracer tracer;
@GET
@Path("automaticTracing")
@Traced(value=false)
public String doNotTraceMe(){
return "Do NOT trace me!";
}
@Traced(operationName="traceMe")
public void traceMe(){
System.out.println("Trace me!");
}
}
在上述示例中,doNotTraceMe()被注解为@Traced(value=false),这会通知 OpenTracing 运行时不要跟踪这个 JAX-RS 端点。"traceMe()"是一个普通业务方法,并注解为@Traced(operationName="traceMe"),以通知 OpenTracing 运行时如果代码路径到达此方法,则将其跟踪为一个跨度。这个跨度被称为"traceMe"。
使用 Jaeger 可视化跟踪
在此演示中,我们将使用一个由两个名为OutboundRequestResource和InboundRequestResource的 JAX-RS 资源组成的简单应用程序。我们将向OutboundRequestResource的localhost:9080/outbound/tracing发出 GET 请求,这将创建一个ClientBuilder来向InboundRequestResource发送GET请求。这将反过来调用一个带有@Traced(operationName="epoch")注解的TracedExample类中的epoch()方法。生成的跟踪可视化可以在此处看到:

图 6.4 – 在 Jaeger 中的跟踪检查
注意
您可以在bit.ly/3swFZEb找到OutBoundTraceResource的完整源代码。
您可以在bit.ly/3xZxrXz找到InBoundTraceResource的完整源代码。
您可以在bit.ly/3y6pHmM找到TracedExample的完整源代码。
这是在 Jaeger 网络客户端检查跟踪时您可能期望看到的快照。前面的图可能难以辨认,因此我们将对其进行描述。左上角显示了跟踪的名称。跟踪被命名为book: GET:com.packt.microprofile.book.ch6.opentracing.OutBoundTraceResource.tracing。跟踪被赋予了此跟踪中第一个 span 的名称,即我们向OutBoundTraceResource中的/tracing端点发出的GET请求。
接口的其他部分由构成跟踪的 span 的顺序列表组成。当最小化时,它将显示每个 span 的持续时间以及与其他 span 相比的活跃持续时间,以实心水平条的形式显示。当您点击一个 span 条目时,它将展开以显示更多详细信息,例如其上下文数据。在上文提到的图中,从InBoundTraceResource的入站 JAX-RS 请求创建的 span,以及从epoch()方法的@Traced注解中仪器化的 span 已被展开。
让我们描述第一个展开的 span,即由入站请求创建的 span。它被称为GET:com.packt.microprofile.book.ch6.opentracing.InBoundTraceResource.waiting。在详细信息中包括我们在此部分前面讨论过的标签;即,component、http.method、http.status_code、http.url和span.kind。Jaeger 附加的标签在internal.span.format中。仪器化的 span 不包含除 Jaeger 提供的标签之外的其他标签。
结合对追踪的摘要视图和查看构成追踪的各个跨度(span)的能力,使用分布式追踪来剖析请求所采取的路径对于分析性能和延迟非常有用。在前面提到的示例中,我们展示了使用 Jaeger 平台进行分布式追踪。另一个提供仪表化库以及查看和分析追踪功能的分布式追踪平台是Zipkin。Zipkin 并未包含在 OpenTracing 文档中官方追踪器的列表中,但您可能会发现您选择的 MicroProfile OpenTracing 运行时支持它。请查阅您运行时的文档,了解其支持的库列表以及配置它的必要步骤。
摘要
在本章中,我们探讨了 MicroProfile 平台提供的三种可观测性技术;即 MicroProfile Health、MicroProfile Metrics 和 MicroProfile OpenTracing。从通过健康检查报告应用程序的整体健康状况,到提供详细统计数据的指标,再到通过分布式追踪跟踪和剖析请求在微服务中的传输路径,每种技术都有其不可估量的用途,满足了监控和观察云原生应用程序的重要任务。现在,您的应用程序已经充分利用了 MicroProfile 发布平台所能提供的所有功能和能力。MicroProfile 的独立发布还包含其他技术。我们将在本书的最后一章中介绍这些技术。
在下一章中,我们将探讨将您的云原生应用程序部署到云上的主题。我们将看到它如何与 Docker、Kubernetes 和 Istio 等云基础设施交互。
第七章:使用 Open Liberty、Docker 和 Kubernetes 的 MicroProfile 生态系统
到目前为止,本书的前几章中,我们专注于使用 MicroProfile API 编写云原生应用程序。在本章中,我们将探讨如何运行云原生应用程序。MicroProfile 与其他一些云原生应用程序框架的不同之处在于,MicroProfile 提供了 API 的多个实现。这减少了最终陷入特定实现或发现您所利用的 API 背后的开源社区不如您想象的那么活跃,维护者消失的可能性。此外,不同的实现往往采取不同的设计决策,这可能更适合您的需求。在撰写本文时,MicroProfile API 的最新版本有四个实现:Open Liberty、Payara、WebSphere Liberty和WildFly。此外,Helidon、JBoss EAP、KumuluzEE和Quarkus实现了之前的版本。
一旦选择了实现方式,您需要将应用程序部署到生产环境中。越来越普遍的做法是使用Docker、Kubernetes和服务网格等技术。这将是本章的重点。
在本章中,我们将涵盖以下主题:
-
将云原生应用程序部署到 Open Liberty
-
使用 Docker 容器化云原生应用程序
-
将云原生应用程序部署到 Kubernetes
-
MicroProfile 和 Service Mesh
在本章中,您将学习如何配置 MicroProfile 应用程序以在 Open Liberty 上运行,将其打包为容器,并部署到像 Red Hat OpenShift 这样的 Kubernetes 运行时中。
技术要求
要构建和运行本章中提到的示例,您需要一个装有以下软件的 Mac 或 PC(Windows 或 Linux):
-
Java 开发工具包(JDK)- Java 8 或更高版本:
ibm.biz/GetSemerut -
Apache Maven:
maven.apache.org -
Git 客户端:
git-scm.com -
Docker 客户端:
www.docker.com/products -
OpenShift 客户端:
docs.openshift.com/container-platform/4.6/cli_reference/openshift_cli/getting-started-cli.html
本章节中使用的所有源代码均可在 GitHub 上找到,地址为github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/tree/main/Chapter07。
将云原生应用程序部署到 Open Liberty
在本节中,我们将探讨如何使用 Open Liberty 部署 MicroProfile 应用程序。我们选择 Open Liberty 是因为我们是在 Open Liberty 的提交者,但它的关注点是保持与最新 MicroProfile 版本的同步、性能和易用性,这使得它成为任何人的好选择。
如您从其名称中预期的那样,Open Liberty 是一个开源的 Java 运行时,用于构建和部署云原生应用程序。它围绕称为 功能 的组件设计,这些组件可以配置以提供满足您的应用程序需求的最小运行时。这意味着如果您的应用程序不使用或不需要 MicroProfile OpenTracing,那么您不需要配置 MicroProfile OpenTracing 功能,并且运行时将更小、更快、更精简——它将更适合您的应用程序需求。Open Liberty 具有编程 API 的功能,例如 MicroProfile API、Java EE、Jakarta EE 和 gRPC。它还具有运行时功能,例如用于与 OpenID Connect 集成进行身份验证的功能。
Open Liberty 主要使用一个简单的 XML 文件格式进行配置,称为 server.xml 文件。使用 XML 的原因有几个:
-
Java 内置了对 XML 解析的支持,这是其主要原因之一。
-
XML 模型非常适合分层配置(与属性格式不同)。
-
空格字符不影响文件格式的语义解释(与 YAML 不同)。
当解析配置文件时,Open Liberty 采取忽略任何它不理解配置的方法。这有几个优点。这意味着一个 server.xml 文件可以包含对正在使用的 Open Liberty 版本无效的配置,而不会导致启动失败。这也意味着配置中的简单错误不会阻止服务器启动应用程序。
server.xml 文件的核心职责之一是配置要加载的功能。通过使用 server.xml 文件来配置要使用哪些功能,并通过确保行为更改仅通过新功能引入,Open Liberty 保证配置的行为在各个版本之间保持不变。以下是一个简单的服务器配置示例,它启用了所有 MicroProfile API:
<server>
<featureManager>
<feature>microProfile-4.1</feature>
</featureManager>
</server>
Open Liberty 的配置可以集中在一个单独的 server.xml 文件中,也可以分散到多个配置文件中。这既促进了配置的共享,也根据服务器配置部署到的环境分离了配置。一个例子可能是开发环境中使用内存数据库,但在生产中可能使用数据库,如 DB2、Oracle 或 MariaDB。这通过两种机制实现。第一种是一个可以显式包含另一个 server.xml 文件的 server.xml 文件。第二种是使用所谓的 defaults 和 overrides,这些在主服务器配置文件之前和之后读取。这些目录中的文件按字母顺序读取,提供了配置读取的可预测性。配置文件还可以使用变量替换语法进行参数化。变量可以在 server.xml 文件中定义,作为 Java 系统属性,或使用环境变量。server.xml 中的变量可以定义多次,变量的最后定义将用于变量解析。变量的定义可能如下所示:
<server>
<variable name="microProfile.feature"
value="microProfile-4.1" />
</server>
然后,它可以在其他地方这样引用:
<server>
<featureManager>
<feature>${microProfile.feature}</feature>
</featureManager>
</server>
变量也可以有一个默认值,这允许编写始终有效的配置,同时在生产中允许覆盖:
<server>
<variable name="http.port" defaultValue="9043" />
</server>
变量的优先级根据其定义的位置而不同,这使得它们可以轻松覆盖。优先级顺序(后定义的优先级覆盖先定义的优先级)如下:
-
server.xml的默认值 -
环境变量
-
bootstrap.properties文件 -
Java 系统属性
-
在
server.xml`中定义的变量 -
服务器启动时定义的变量
这提供了多种简单的方法来根据 Open Liberty 部署到的环境更改其行为。
Open Liberty 允许您将 MicroProfile 应用程序打包为 WAR 文件以部署到服务器。MicroProfile 规范对应用程序的打包和部署方式没有意见,因此 Open Liberty 重新使用 Jakarta EE 的 WAR 打包模型作为打包应用程序的方式。这很有意义,因为 MicroProfile 利用了几种 Jakarta EE 编程模型,并且使 MicroProfile 应用程序更容易利用 Jakarta EE 中不在 MicroProfile 中的部分,例如 Jakarta EE 的并发工具。它还允许您重用现有的 Maven 和 Gradle 构建工具来打包 MicroProfile 应用程序。
将 WAR 文件部署到 Open Liberty 有两种方式。第一种是将 WAR 文件放入 dropins 文件夹,第二种是通过 server.xml 文件:
<server>
<webApplication location="myapp.war" />
</server>
使用服务器配置方法而不是dropins文件夹的主要原因是可以自定义应用程序的运行方式,例如,设置应用程序的contextRoot,配置类加载,或配置安全角色绑定。
Open Liberty 支持多种机制来打包应用程序以进行部署。最简单的方法是将应用程序打包为WAR文件。但在云原生环境中,这不太可能。Open Liberty 还支持将服务器打包为zip文件、可执行的JAR文件和 Docker 容器(下一节将描述)。Open Liberty 为 Maven 和 Gradle 提供了插件,使得构建将在 Open Liberty 上运行的应用程序变得简单。这些插件的一个特性是在 Maven 中提供 Open Liberty 的dev模式,只需将 Open Liberty Maven 插件添加到pom.xml文件中的plugin部分,如下所示:
<plugin>
<groupId>io.openliberty.tools</groupId>
<artifactId>liberty-maven-plugin</artifactId>
<version>[3.3.4,)</version>
</plugin>
此配置使插件使用 3.3.4 版本的插件或如果存在的话,使用更近期的版本。
当你运行liberty:dev Maven 目标时,插件将编译应用程序,下载运行应用程序所需的任何依赖项,将其部署到 Liberty 中,并使用 Java 调试器支持运行服务器。这允许你在任何代码编辑器中对应用程序进行更改,无论是简单的vi编辑器还是功能齐全的 IDE,如IntelliJ IDEA或Eclipse IDE。
Liberty 的设计使得构建将在容器环境(如 Docker)中运行的应用程序变得非常简单。甚至还有一个用于容器的dev模式,可以使用liberty:devc来运行。下一节将讨论如何创建容器作为部署工件。
使用 Docker 容器化云原生应用程序
在本节中,我们将探讨如何容器化 MicroProfile 应用程序。容器化的关键有两部分:第一是创建镜像,第二是运行该镜像。虽然 Docker 不是第一个进行容器化的产品,但它以开发者和管理员能够理解的方式普及了这一概念。Cloud Foundry 是一个常见的早期替代品,它有类似的概念,但将它们隐藏为内部实现细节,而不是作为一级概念。使用 Docker,这两个概念被分为两部分,通过创建镜像时使用的docker build命令和运行镜像时使用的docker run命令暴露出来。这些概念进一步扩展,成为标准化,这意味着现在有多个替代方案用于docker build和docker run。
容器镜像
容器镜像 是容器部署的工件。容器镜像包含运行应用程序所需的一切。这意味着容器镜像可以从一个环境移动到另一个环境,有信心它在同一方式下运行。座右铭是“一次思考,一次创建,到处运行”;然而,这也有一些限制。容器与 CPU 架构相关联,因此为 x86 CPU 设计的容器在没有 CPU 指令转换层(如 Rosetta 2,将 Mac x86 指令转换为 Mac ARM 指令以支持在具有 M 系列 ARM 处理器的 Mac 上运行的 x86 Mac 应用程序)的情况下,无法在 ARM 或 Power CPU 上运行。
Dockerfile 是创建容器镜像的一系列指令。一个 Dockerfile 首先声明一个基于的命名镜像,然后确定一系列步骤以将额外的内容添加到容器镜像中。常见的做法可能是基于包含 操作系统(OS)、Java 镜像或预包装应用程序运行时(如 Open Liberty)的镜像。
虽然将容器镜像想象成一个包含镜像中所有内容的单个大文件很方便,但这并不是容器镜像的工作方式。容器镜像由一系列通过 SHA 哈希标识的层组成。这提供了三个关键优势:
-
它减少了存储图像所需的存储空间。如果你有 30 个基于公共基础镜像的图像,你只需存储一次那个公共基础镜像,而不是 30 次。虽然存储空间相对便宜,但如果你有大量应用程序,容器之间的文件重复很快就会累积成大量。
-
它减少了带宽需求。当你将容器镜像传输到和从 容器注册库 时,你不需要上传或下载你已经拥有的层。很可能你会有很多容器镜像,但一个常见的操作系统镜像和一个常见的 JVM 镜像。
-
它减少了构建时间。如果层的输入没有变化,就没有必要重新构建它。层的输入是你在该行所做的任何更改,加上上一行的输出。
每个层都有一个指向其构建在其上的层的指针。当你创建一个容器镜像时,它由基础镜像的所有层和一个 Dockerfile 中每行的单个新层组成。一个用于打包简单 MicroProfile 应用程序的简单 Dockerfile 可能看起来像这样:
FROM open-liberty:full-java11-openj9
ADD src/main/config/liberty/server.xml /config
ADD target/myapp.war /config/dropins
这将创建一个基于 Ubuntu 20.04 和 Java SE 11 的容器镜像,使用 OpenJ9 JRE 实现并包含所有可用的 Open Liberty 功能。然后,它将从 Maven 项目的默认位置复制 server.xml 和应用程序到 dropins 文件夹。这将创建一个包含与 open-liberty:full-java11-openj9 镜像相关联的所有层的镜像,以及与该镜像相关联的两个层。
最佳实践:使用多个层
这种方法的优点是当你推送或拉取镜像时,只有尚未存在的层才会被传输。在之前提到的简单 MicroProfile 示例中,当你构建和推送镜像时,只有与应用程序和服务器配置相关的层会被传输。可以这样想:如果你有一个 500MB 大小的基础镜像,而你镜像的层总共是 5MB,那么你的镜像总共将是 505MB,但当你将其推送到容器注册库时,只需要发送 5MB,因为基础镜像已经存在了。
这在设计 Docker 镜像时引发了一些有趣的设计问题。Docker 镜像的目的是显然的,即让它运行起来,并且最好是尽可能快地运行起来。这使得部署新的镜像、扩展它或发生问题时用新的容器替换容器变得更快。构建 Docker 镜像的一个简单方法是将你的应用程序打包在一个单一的 JAR 文件中,将其添加到 Dockerfile 中,然后运行它:
FROM adoptopenjdk:11-openj9
ADD target/myapp.jar /myapp.jar
CMD ["java", "-jar", "/myapp.jar"]
这是一种创建 Docker 镜像的流行方式,如果应用程序很小,效果很好,但以这种方式构建的许多应用程序并不小。考虑这种情况,如果那个jar文件包含 Open Liberty、应用程序代码和一些开源依赖项。这意味着每次修改应用程序时,都必须重新创建和部署包含所有这些代码的新层。另一方面,如果应用程序被拆分,那么对应用程序的更改将需要更小的上传:
FROM open-liberty:full-java11-openj9
ADD target/OS-dependencies /config/library
ADD target/myapp.war /config/apps/myapp.war
在这个例子中,对应用程序代码的更改只会重建最后一层,这意味着上传的文件会更小,分发速度更快。当然,对开源依赖项的更改会导致该层被重新构建,但这些更改的频率通常低于应用程序。如果有许多应用程序共享一组公共库(无论是否为开源),那么创建一个所有应用程序都可以使用的命名基础镜像可能是有意义的。如果容器镜像通常在同一个主机上运行,这将特别有用。
关于层的一个重要理解是,一旦创建,它们就是不可变的。这意味着如果你删除了早期层中创建的文件,它不会从镜像中删除文件;它只是将它们标记为已删除,因此无法访问。这意味着在传输容器镜像时,你会复制文件的字节,但文件永远不会可访问。如果文件是由基础镜像贡献的,这是不可避免的,但如果你控制镜像,那么这是需要避免的。
Dockerfile 指令
如前所述,Dockerfile 是一系列指令,详细说明了如何创建 Docker 镜像。有几种创建镜像的指令。到目前为止所有示例中的第一个指令是FROM指令。
FROM
FROM 指令定义了您正在创建的镜像的基础容器镜像。所有 Dockerfile 都必须以 FROM 开头。Dockerfile 可以有多个 FROM 行:这些通常用于创建多阶段构建。一个例子可能是,如果您需要一些额外的工具来构建您的镜像,而这些工具在运行时不需要存在。一个例子可能是 wget 或 curl 用于下载文件,以及 unzip 用于解压缩 zip 文件。一个简单的多阶段构建可能看起来像这样:
FROM ubuntu:21.04 as BUILD
RUN apt-get update
RUN apt-get install -y unzip wget
RUN wget https://example.com/my.zip -O my.zip
RUN unzip my.zip /extract
FROM ubuntu:21.04
COPY /extract /extract --from=BUILD
在这个例子中,第一阶段安装了 wget 和 unzip,下载了一个文件,并将其解压缩。第二阶段从基础镜像开始,然后将提取的文件复制到新的镜像层。如果创建了一个单阶段 Dockerfile,这将导致包含 unzip 和 get 的二进制文件、zip 文件和 extract 的三个额外层的镜像。多阶段构建只包含 extract。要使用单阶段 Docker 构建实现这一点,可读性较差,看起来可能像这样:
FROM ubuntu:21.04
RUN apt-get update && \
apt-get install -y unzip wget && \
wget https://example.com/my.zip -O my.zip && \
unzip my.zip /extract && \
rm my.zip && \
apt-get uninstall -y unzip wget && \
rm -rf /var/lib/apt/lists/*
这个 Dockerfile 使用单个 RUN 命令来运行多个命令,以创建单个层,并且必须在结束时撤销每个步骤。最后一行是必需的,用于清理 apt 创建的文件。多阶段 Dockerfile 要简单得多。多阶段构建的另一个常见用途是使用第一阶段构建应用程序,然后使用第二阶段运行:
FROM maven as BUILD
COPY myBuild /build
WORKDIR build
RUN mvn package
FROM open-liberty:full-java11-openj9
COPY /target/myapp.war /config/apps/ --from=BUILD
COPY 和 ADD
COPY 和 ADD 指令执行类似的功能。ADD 指令的功能集合包含了 COPY 的功能,因此通常建议只有在需要扩展功能时才使用 ADD。这两个指令的第一个参数指定了源文件(或目录),默认情况下解释为从运行构建的机器复制。命令始终相对于运行构建的目录,并且不能使用 .. 来导航到父目录。正如前一个部分所示,使用 from 参数可以将复制重定向到另一个容器镜像。第二个参数是容器中文件应该被复制到的位置。
ADD 命令在 COPY 命令的基础上提供了一些额外的功能。第一个功能是它允许您将 URL 作为第一个参数指定,以从该 URL 下载文件。第二个功能是它可以将 tar.gz 文件解压缩到目录中。回到第一个多阶段构建示例,如果输出是一个 tar.gz 文件,这意味着它可以简化为以下内容:
FROM ubuntu:21.04 as BUILD
ADD https://example.com/my.tar.gz /extract
RUN
RUN 指令简单地使用操作系统层的 shell 执行一个或多个命令。这允许你做几乎你想做或需要做的任何事情,或者提供在基础操作系统镜像中可用的命令。例如,在基础 Linux 操作系统镜像中通常不包含 unzip 或 wget,因此除非采取行动安装它们,否则这些命令将失败。每个 RUN 指令创建一个新的层,所以如果你在一个 RUN 命令中创建一个文件,然后在另一个 RUN 命令中删除它,由于层的不可变性,文件将存在但不再可见。因此,通常很重要使用 && 操作符将多个命令连接到单个层中。这个例子之前已经展示过,但在此处重复:
FROM ubuntu:21.04
RUN apt-get update && \
apt-get install -y unzip wget && \
wget https://example.com/my.zip -O my.zip && \
unzip my.zip /extract && \
rm my.zip && \
apt-get uninstall -y unzip wget && \
rm -rf /var/lib/apt/lists/*
ARG 和 ENV
ARG 定义了一个在构建时可以指定的构建参数。ARG 的值在运行 docker build 时通过 build-arg 参数设置。如果构建时没有提供,ARG 可以有一个默认值。这些构建参数在构建完成后不会持久化,因此它们在运行时不可用,也不会在镜像中持久化。
ENV 定义了一个在构建和运行时都可用的环境变量。
这两个都是用相同的方式引用的,所以关键的区别是值的可见性。
ENTRIES 和 CMD
当运行一个容器时,你需要发生一些事情,比如启动 Open Liberty 服务器。发生的事情可以通过 Dockerfile 使用 ENTRYPOINT 和 CMD 指令来定义。这两个指令之间的区别在于它们如何与 docker run 命令交互。当运行 Docker 容器时,任何在 Docker 镜像名称之后的参数都会传递到容器中。CMD 在没有提供命令行参数的情况下提供了一个默认值。ENTRYPOINT 定义了一个将被运行的命令,并且提供给 docker run 的任何命令行参数都会在 ENTRYPOINT 之后传递。CMD 和 ENTRYPOINT 有相同的语法。Open Liberty 容器指定了这两个指令,因此基于它们的镜像通常不会指定它们。
WORKDIR
WORKDIR 指令用于更改未来 RUN、CMD、COPY 和 ENTRYPOINT 指令的当前目录。
USER
当构建镜像时,用于执行命令的默认用户账户是 root。对于某些操作,这是合理且公平的。如果你正在执行操作系统更新,通常需要以 root 身份执行。然而,当使用 root 账户运行容器时,这是一个明显的安全问题。Dockerfile 有一个 USER 指令,它设置了用于 RUN 指令以及容器运行时执行的进程的用户账户。这使得将账户设置为非 root 账户变得简单。前面示例中的 Open Liberty 镜像将 USER 设置为 1001,这意味着基于它的任何先前示例都不会使用 root 账户运行,但基于 Java 镜像的示例会。先前 Dockerfile 示例的一个问题是 ADD 和 COPY 指令将写入文件,因此它们属于 root 用户,这可能在运行时引起问题。这可以通过更新 ADD 或 COPY 指令来更改所有权来解决,如下所示:
FROM open-liberty:full-java11-openj9
ADD --chown=1001:0 src/main/config/liberty/server.xml/config
ADD --chown=1001:0 target/myapp.war /config/dropins
或者,可以使用 RUN 指令来执行 chown 命令行工具。这将创建一个新的层,但如果 ADD 或 COPY 指令移动多个文件,而只有一些文件需要更改所有权,则可能需要这样做。
虽然 Dockerfile 是创建容器镜像最常用的方式,但还有其他构建容器镜像的方法。以下是一些示例。
源到镜像
源到镜像(S2I)是一种技术,它将应用程序源代码转换为容器镜像。与创建 Dockerfile 不同,S2I 构建器会摄取源代码,运行构建,并将其编码到容器镜像中。这使得开发人员可以专注于应用程序代码,而不是容器创建。通过在 S2I 构建器中编码构建容器的最佳实践,它可以在应用程序之间重用,使得一组应用程序都拥有精心设计的容器镜像的可能性更大。有适用于许多语言和框架的 S2I 构建器,包括 Open Liberty。S2I 是 Red Hat 创建的开源技术,旨在帮助开发人员采用 OpenShift,尽管它可以,并且确实被用来创建可以在任何地方运行的容器。
云原生构建包
WAR 文件。
创建容器镜像后,下一步是运行它。虽然开发人员可能会在桌面运行时使用 Docker 来运行镜像,但在生产中运行容器镜像最常见的方式是使用 Kubernetes,我们将在下一节中讨论。
将云原生应用程序部署到 Kubernetes
Kubernetes 最初是谷歌的一个项目,旨在让他们能够大规模管理软件。随后,它转变为一个由 云原生计算基金会(CNCF)管理的开源项目,并得到了整个行业的贡献者。每个主要(以及大多数次要)的公共云提供商都使用 Kubernetes 来管理容器的部署。还有一些私有云产品,如 Red Hat OpenShift,提供 Kubernetes 的发行版,用于在本地或公共云上部署,但仅限于单一公司。
Kubernetes 部署被称为 集群。为了运行容器并提供高度可用、可扩展的环境,集群由控制平面和一组关键资源组成,这些资源使它能够运行或管理容器、扩展它们,并在发生任何故障时保持容器的运行。在 Kubernetes 中运行容器时,容器被放置在 Pod 中,然后根据控制平面的决策在节点上运行。
Pod 为运行一组容器提供了一个共享的上下文。Pod 中的所有容器都在同一个节点上运行。虽然你可以在 Pod 中运行多个容器,但通常一个 Pod 将包含一个单一的应用容器,而 Pod 中运行的任何其他容器都将作为辅助容器,为应用容器提供一些管理或支持功能。
在传统的自动化操作环境中,自动化将描述如何设置环境。当使用 Kubernetes 时,不是描述如何设置环境,而是描述期望的最终状态,而控制平面将决定如何实现这一点。此配置以一个(或更常见的是一组)YAML 文档的形式提供。这种效果是,在部署到 Kubernetes 时,你不是通过在节点上定义 Pod 并将容器放入其中来描述部署。相反,你定义一个 Deployment,它将表达你想要部署的容器以及应该创建多少个容器副本。可以使用以下 YAML 部署 Open Liberty 的单个实例:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: demo
name: demo
spec:
replicas: 1
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
containers:
- image: openliberty/open-liberty:full-java11-openj9-
ubi
name: open-liberty
然后,可以使用 kubectl apply 命令部署此 YAML。这会导致部署一个运行 Open Liberty 的单一 Pod。当容器运行并能够响应 HTTP 请求时,没有路由让网络流量到达容器。使网络流量能够到达部署的关键是 Kubernetes 服务。服务定义了容器中进程监听的端口以及通过 Kubernetes 网络堆栈访问的端口。可以使用以下 YAML 定义此类服务:
apiVersion: v1
kind: Service
metadata:
labels:
app: demo
name: demo
spec:
ports:
- name: 9080-9080
port: 9080
protocol: TCP
targetPort: 9080
selector:
app: demo
type: ClusterIP
一个服务允许运行 Kubernetes 的其他容器访问它,但不允许集群外的服务访问它。有几种方法可以将容器外部暴露,例如端口转发、入口控制器,或者 OpenShift 有 路由 的概念。路由本质上只是将服务外部暴露给集群。您可以指定主机和路径,或者您可以允许 Kubernetes 默认设置。为了将此 Open Liberty 服务器外部暴露,您可以使用以下 YAML 定义一个路由:
kind: Route
apiVersion: route.openshift.io/v1
metadata:
name: demo
labels:
app: demo
spec:
to:
kind: Service
name: demo
weight: 100
port:
targetPort: 9080-9080
这三个 YAML 文件已部署了一个容器并将其外部暴露,以便可以访问和使用。这三个 YAML 文件可以使用 --- 作为分隔符放置在单个文件中,但除了使用 YAML 配置一切之外,还有另一种管理部署的选项,那就是使用 Operator。
Operator 是一种打包、部署和管理与应用程序相关的一组资源的方式。它们最初旨在帮助管理有状态的应用程序,如果只是丢弃并启动一个新的 Pod,可能会导致数据丢失;然而,它们也可以用来简化应用程序的部署。Operator 监视它理解的 自定义资源 的定义,并配置相关的 Kubernetes 资源来运行该应用程序。Operator 可以执行诸如管理应用程序的部署和更新,当有新镜像可用时。Open Liberty 提供了一个 Operator,可以管理基于 Open Liberty 的应用程序的部署。例如,所有之前的 YAML 文件都可以简单地替换为以下 YAML:
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
name: my-liberty-app
spec:
applicationImage: openliberty/open-liberty:full-java11-
openj9-ubi
service:
type: ClusterIP
port: 9080
expose: true
Kubernetes 中的 MicroProfile Health
MicroProfile Health 规范允许您配置对存活性和就绪性检查的支持。这些探针允许 Kubernetes 控制平面了解容器的健康状况以及应采取什么行动。一个失败的存活性探针将触发 Pod 被回收,因为它表明发生了无法解决的问题。另一方面,就绪性探针将简单地导致 Kubernetes 停止将流量路由到 Pod。在这两种情况下,您需要多个实例以确保在任何容器故障期间,客户端仍然不会察觉。要配置这些存活性和就绪性探针,您需要确保 Open Liberty 服务器配置为运行 MicroProfile Health:
<server>
<featureManager>
<feature>mpHealth-3.0</feature>
</featureManager>
</server>
然后,在定义应用程序时,配置存活性和就绪性探针:
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
name: my-liberty-app
spec:
expose: true
applicationImage: openliberty/open-liberty:full-java11- openj9-ubi
readinessProbe:
httpGet:
path: /health/ready
port: 9080
initialDelaySeconds: 30
livenessProbe:
httpGet:
path: /health/live
port: 9080
initialDelaySeconds: 90
replicas: 1
此配置将存活性和就绪性探针配置为向 MicroProfile 健康端点发送 HTTP get 请求以检查存活性和就绪性。它还在容器启动后配置了一个等待期,在执行第一次检查之前。这给了容器一个机会在开始轮询以确定状态之前运行任何启动程序。
Kubernetes 中的 MicroProfile Config
MicroProfile Config 提供了一种在您的应用程序中接收配置的方法,该配置可以在环境中提供。在 Kubernetes 中,此类配置通常存储在 ConfigMap 或 Secret 中。如前所述,在 第五章,“增强云原生应用程序”中,ConfigMap 实质上是存储在 Kubernetes 中的键/值对集合,可以绑定到 Pod 中,使其对容器可用。要从 Kubernetes 接收应用程序的配置,请确保 Open Liberty 服务器已配置为运行 MicroProfile Config:
<server>
<featureManager>
<feature>mpConfig-2.0</feature>
</featureManager>
</server>
创建 ConfigMap 有许多方法,第五章,“增强云原生应用程序”演示了一种机制。定义 ConfigMap 的另一种方法是应用以下 YAML 的 ConfigMap:
kind: ConfigMap
apiVersion: v1
metadata:
name: demo
data:
example.property.1: hello
example.property.2: world
现在,当部署您的应用程序时,您可以选择绑定此 ConfigMap 中的一个单个环境变量,或者全部绑定。要将 ConfigMap 中的 example.property.1 的值绑定到名为 PROP_ONE 的变量,并将其绑定到 Open Liberty 应用程序,您将使用以下 YAML:
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
name: demo-app
spec:
expose: true
applicationImage: openliberty/open-liberty:full-java11- openj9-ubi
env:
- name: PROP_ONE
valueFrom:
configMapKeyRef:
key: example.property.1
name: demo
replicas: 1
ConfigMap 可以(如上述示例所示)包含许多容器可能需要访问的属性,而不是绑定单个条目或逐个绑定条目,您可以绑定所有条目。以下 YAML 将定义一个将 ConfigMap 的所有值绑定的应用程序:
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
name: demo-app
spec:
expose: true
applicationImage: openliberty/open-liberty:full-java11- openj9-ubi
envFrom:
- configMapRef:
name: demo
replicas: 1
MicroProfile Config 的最新功能之一是配置配置文件的概念。想法是您可以为开发、测试和生产环境中的应用程序提供配置,并且 MicroProfile Config 只加载所需配置文件的配置。为此,您还需要定义配置配置文件。MicroProfile Config 规范说明配置文件名称中的属性以 %<profile name> 开头;然而,% 在环境变量名称中无效,因此它被替换为 _。以下 YAML 是此示例的示例:
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
name: demo-app
spec:
expose: true
applicationImage: openliberty/open-liberty:full-java11- openj9-ubi
env:
- name: mp.config.profile
value: dev
envFrom:
- configMapRef:
name: dev
prefix: '_dev.'
- configMapRef:
name: test
prefix: '_test.'
- configMapRef:
name: prod
prefix: '_test.'
replicas: 1
Kubernetes 中的 ConfigMaps 适用于存储非敏感数据,但当涉及到存储 API 密钥、凭证等内容时,Kubernetes 提供了一种称为 Secrets 的替代概念。Secrets 可以代表多种不同类型的 Secrets,但在这里我们只考虑简单的键/值对。Kubernetes 平台为 Secrets 提供了比 ConfigMaps 更好的保护,尽管许多人更喜欢使用第三方产品进行 Secret 管理。了解 Secrets 的工作原理仍然很有用,因为第三方产品通常遵循相同的约定来从容器内部访问敏感数据。
秘密使用 base64 编码,这并不是非常好的保护。Open Liberty 允许加载的密码使用 AES 加密,并提供了解密受保护字符串的 API,因此你的 base64 编码的秘密可能是一个 base64 编码的 AES 加密字符串。然而,由于你仍然需要向 Open Liberty 提供解密密钥,而这不是一本关于安全加固的书,所以我们不会在这里进一步详细介绍。从部署中引用秘密中的单个密钥对几乎与从 ConfigMap 中引用相同,但使用 secretKeyRef 而不是 configMapKeyRef;例如,以下 YAML:
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
name: demo-app
spec:
expose: true
applicationImage: openliberty/open-liberty:full-java11- openj9-ubi
env:
- name: PROP_ONE
valueFrom:
secretKeyRef:
key: my_secret
name: secret.config
replicas: 1
如果你部署了秘密 YAML 并按照前面提到的示例进行绑定,你的容器将有一个名为 PROP_ONE 的环境变量,其值为 super secret。
就像 ConfigMap 一样,你可以将秘密中的所有键/值对绑定到容器中,并且就像前面的例子一样,它是以与 ConfigMaps 非常相似的方式进行操作的:
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
name: demo-app
spec:
expose: true
applicationImage: openliberty/open-liberty:full-java11- openj9-ubi
envFrom:
- secretRef:
name: secret.config
replicas: 1
秘密也可以绑定到容器文件系统中的文件,这对于需要高度安全的数据来说可能更可取。当你这样做时,秘密将在文件系统中定义,秘密的值将是文件的内容。MicroProfile Config 不能消费以这种方式绑定的秘密,但它提供了一种添加额外 ConfigSources 的方法,让你可以轻松地加载配置。将秘密绑定到文件系统的 YAML 实际上就是将其挂载为一个卷。以下示例 YAML 将导致秘密 secret.config 中的每个键/值在容器的 /my/secret 目录下作为文件挂载:
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
name: demo-app
spec:
expose: true
applicationImage: openliberty/open-liberty:full-java11- openj9-ubi
volumeMounts:
- mountPath: /my/secrets
name: secret
volumes:
- secret:
secretName: secret.config
name: secret
replicas: 1
要启用注入绑定的秘密,你需要在应用程序类路径上有一个 META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource,这将自动导致它被加载。以下是一个简短的示例 ConfigSource,它将执行此操作:
public class FileSystemConfigSource implements ConfigSource {
private File dir = new File("/my/secrets");
public Set<String> getPropertyNames() {
return Arrays.asList(dir.listFiles())
.stream()
.map(f -> f.getName())
.collect(Collectors.toSet());
}
public String getValue(String s) {
File f = new File(dir, s);
try {
if (f.exists())
Path p = f.toPath();
byte[] secret = Files.readAllBytes(f);
return new String(secret, StandardCharsets.UTF_8);
} catch (IOException ioe) {
}
return null;
}
public String getName() {
return "kube.secret";
}
public int getOrdinal() {
return 5;
}
}
此配置源将加载具有属性名称的文件内容,该属性名称是从一个定义良好的目录中读取的。如果文件无法读取,它将表现得好像该属性未定义。当秘密更新时,Kubernetes 将更新文件内容,这意味着更新可以自动对应用程序可见,因为每次读取属性时,此代码都会重新读取文件。
在下一节中,我们将讨论在使用 MicroProfile 与服务网格时的一些考虑因素。
MicroProfile 和服务网格
当部署到 Kubernetes 集群时,有些人选择使用服务网格。服务网格的目标是将微服务的某些考虑因素从应用程序代码中移出,并将其放置在应用程序周围。服务网格可以消除一些应用程序关注点,例如服务选择、可观察性、故障容忍,以及在某种程度上,安全性。一种常见的服务网格技术是 Istio。Istio 的工作方式是在容器的 Pod 中插入一个边车,所有传入和网络流量都通过该边车路由。这允许边车执行应用访问控制策略、将请求路由到下游服务以及应用故障容忍策略,例如重试请求或超时。其中一些功能与 MicroProfile 的一些功能重叠,例如,Istio 可以处理将 OpenTracing 数据附加到请求并传播。如果你使用 Istio,显然不需要使用 MicroProfile OpenTracing,尽管同时使用两者会相辅相成而不是产生冲突。
在使用服务网格和 MicroProfile 可能产生负面冲突的一个领域是故障容忍。例如,如果你在 MicroProfile 中配置了 5 次重试,在 Istio 中也配置了 5 次重试,并且它们都失败了,你最终会有 25 次重试。因此,当使用服务网格时,通常禁用 MicroProfile 的故障容忍功能。这可以通过将环境变量 MP_Fault_Tolerance_NonFallback_Enabled 设置为 false 来完成。这将禁用所有 MicroProfile 故障容忍支持,除了回退功能。这是因为执行失败时的逻辑本质上是一个应用程序考虑因素,而不是可以提取到服务网格中的东西。这可以通过以下 YAML 简单禁用:
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
name: demo-app
spec:
expose: true
applicationImage: openliberty/open-liberty:full-java11- openj9-ubi
env:
- name: MP_Fault_Tolerance_NonFallback_Enabled
value: 'false'
replicas: 1
这将配置应用程序具有一个硬编码的环境变量,该变量禁用了非回退的 MicroProfile 故障容忍行为。这也可以通过 ConfigMap 来完成。
摘要
在本章中,我们回顾了本书其余部分使用的 MicroProfile 实现,构建 MicroProfile 应用程序容器的最佳实践,以及如何将应用程序部署到 Kubernetes。虽然本章并不是对 Kubernetes 中可用功能的详尽审查,但它确实关注了将 MicroProfile 应用程序部署到 Kubernetes 的特定考虑因素以及它们如何与 Kubernetes 服务交互。本章应该为您提供了创建和部署使用 Open Liberty、容器和 Kubernetes 的 MicroProfile 应用程序的良好起点。
下一章将描述一个示例应用程序,该应用程序利用 MicroProfile 在一个容器中部署到 Kubernetes 集群中的一组微服务。
第三部分:使用 MicroProfile 的端到端项目
在本节中,理论与实践相结合——您将学习如何在预期的云部署环境中构建和测试应用程序。您还将学习如何成功部署和管理多服务应用程序。
本节包含以下章节:
-
第八章, 构建和测试您的云原生应用程序
-
第九章, 部署和第二天运营
第八章:构建和测试您的云原生应用
在前面的章节中,我们单独研究了各种 MicroProfile 技术。现在,让我们回到我们的示例应用,即IBM 股票交易者,它首次在第三章中介绍,介绍 IBM 股票交易者云原生应用,看看这些技术如何在各个微服务中使用。在本章中,我们将重点关注如何构建这些微服务,如何构建每个微服务的容器镜像并将它们推送到镜像库,如何对它们进行单元测试,以及各种 MicroProfile 特性的使用。
在本章中,我们将涵盖以下主要主题:
-
编译股票交易微服务
-
构建股票交易容器镜像
-
测试股票交易微服务
-
股票交易对 MicroProfile 的使用
到本章结束时,你将熟悉如何构建此类云原生应用的各个部分,如何尝试每个部分,以及如何展示它们的用法。
技术要求
要构建和测试本节中描述的微服务,你需要安装以下工具:
-
Java 开发工具包(JDK) – Java 8 或更高版本:
ibm.biz/GetSemeru -
Apache Maven:
maven.apache.org -
一个 Git 客户端:
git-scm.com -
一个 Docker 客户端:
www.docker.com/products
编译股票交易微服务
在本节中,我们将探讨如何为每个微服务创建源代码的本地副本,以及如何编译它并将它打包成一个可以部署到应用服务器的存档。
GitHub
一个名为IBMStockTrader的公共 GitHub 组织,位于github.com/IBMStockTrader,包含应用中由十几个微服务组成的每个微服务的仓库。
让我们专注于trader仓库,并克隆其内容:
jalcorn@Johns-MBP-8 StockTrader % git clone https://github.com/IBMStockTrader/trader
Cloning into 'trader'...
remote: Enumerating objects: 2840, done.
remote: Counting objects: 100% (247/247), done.
remote: Compressing objects: 100% (132/132), done.
remote: Total 2840 (delta 58), reused 0 (delta 0), pack- reused 2593
Receiving objects: 100% (2840/2840), 28.25 MiB | 322.00 KiB/s, done.
Resolving deltas: 100% (1049/1049), done.
jalcorn@Johns-MBP-8 StockTrader % cd trader
jalcorn@Johns-MBP-8 trader % ls
BUILD.md Jenkinsfile lab
CONTRIBUTING.md Jenkinsfiledemo manifest.yml
Dockerfile LICENSE manifests
Dockerfile-build README.md pipeline-template.yaml
Dockerfile-lang build_parameters.sh pom.xml
Dockerfile-tools chart src
Dockerfile.basicregistry cli-config.yml
jalcorn@Johns-MBP-8 trader %
这里最重要的文件是Dockerfile和pom.xml(我们将在稍后讨论这两个文件),以及src目录,它包含所有源代码。这包括 Java 代码(位于src/main/java – 每个股票交易微服务都在com.ibm.hybrid.cloud.sample.stocktrader下的子包中),Web 工件(位于src/main/webapp),以及 Open Liberty 配置(位于src/main/liberty/config – 我们将在构建股票交易容器镜像部分进一步讨论)。应用中的所有微服务都遵循相同的结构。让我们看看src目录的内容:

图 8.1 – IBMStockTrader Git 仓库中的源代码布局
现在我们已经看到了每个 Git 仓库中的源代码结构,让我们看看如何使用 Maven 来构建它并将结果打包以部署到 Open Liberty 应用程序服务器的 Docker 容器。
Maven
pom.xml 文件告诉 Maven 要构建什么以及如何构建。以下是步骤:
-
首先,您需要在
pom.xml文件中包含以下段落,以便org.eclipse.microprofile.*包的使用可以编译:<dependency> <groupId>org.eclipse.microprofile</groupId> <artifactId>microprofile</artifactId> <version>4.0.1</version> <type>pom</type> <scope>provided</scope> </dependency>这是将所有 MicroProfile 4 功能放在编译时类路径上的 总依赖项。如果您愿意,也可以只选择特定的 MicroProfile 功能;例如,要将仅 MicroProfile Health 放在编译时类路径上,您需要指定
org.eclipse.microprofile.health。注意
<scope>provided</scope>行 - 这告诉 Maven,尽管它应该将这些 JAR 文件添加到编译时类路径,但它不应该将这些 JAR 文件捆绑在构建的 WAR 文件中。托管 WAR 文件的应用服务器(在我们的例子中是 Open Liberty)提供了这些 JAR 文件,在应用程序内部有额外的副本可能会导致类加载器问题,所以我们告诉它provided以避免这种情况。 -
接下来,让我们使用 Maven 构建我们的
Trader微服务。大多数现代系统通过brew install maven来安装它。请注意,Maven 也依赖于 Java,您需要设置JAVA_HOME环境变量以指向您的 Java 安装。让我们使用mvn compile来编译我们的代码:jalcorn@Johns-MBP-8 trader % mvn compile [INFO] Scanning for projects... [INFO] [INFO] --------< com.stocktrader:trader >------------- [INFO] Building StockTrader - trader 1.0-SNAPSHOT [INFO] ----------------[ war ]----------------------- [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ trader --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 1 resource [INFO] [INFO] --- maven-compiler-plugin:3.1:compile (default- compile) @ trader --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 15 source files to /Users/jalcorn StockTrader/trader/target/classes [INFO] ----------------------------------------------- [INFO] BUILD SUCCESS [INFO] ----------------------------------------------- [INFO] Total time: 1.913 s [INFO] Finished at: 2021-05-09T12:26:19-05:00 [INFO] ----------------------------------------------- jalcorn@Johns-MBP-8 trader %如您所见,编译所有代码只需几秒钟的时间。当然,如果发生任何编译错误,它们将在运行时显示。
-
接下来,让我们通过
mvn package打包 WAR 文件。为了节省空间,一些输出页被省略了,但这里显示了重要的部分:mvn package, which takes about half a minute due to starting and stopping the Open Liberty server, actually does run the compile as well, so you would only use the command mvn compile directly when wanting a very fast way to check for compile errors. -
最后,我们可以通过命令
mvn verify运行一些基本的集成测试。这将执行
src/test/java下可能存在的任何测试类(强烈推荐,以尽早发现问题),例如我们位于src/test/java/com/ibm/hybrid/cloud/sample/stocktrader/trader/test下的HealthEndpointIT.java和HomePageIT.java:404 errors, as tests sometimes start trying to hit URLs before the server is fully started; that's why it retries up to *5* times before giving up.
现在我们已经看到了如何编译、打包和测试我们的代码,接下来让我们看看如何容器化它并测试容器。
构建股票交易者容器镜像
如前所述,我们使用 Open Liberty 作为托管大多数股票交易者微服务应用程序服务器的服务器。我们使用 Docker 来生成最终在 Kubernetes 集群(如 OpenShift Container Platform 集群)中运行的容器镜像。
以下小节将描述我们如何配置服务器,以及如何将其打包成容器镜像。
Open Liberty
市场上有许多符合 MicroProfile 规范的 Java 应用程序服务器。作为提醒,从第三章,“介绍 IBM 股票交易云原生应用程序”,大多数股票交易微服务(交易员、经纪人、经纪人-查询、投资组合、账户、交易历史、消息传递、通知-Slack、收集器、循环器)都是基于开源的 Open Liberty 应用程序服务器。为了多样性,还有三个基于不同服务器的其他微服务:
-
股票报价,运行在红帽的Quarkus上。
-
通知-Twitter,运行在传统 WebSphere 应用程序服务器(tWAS)上。
-
Tradr 是唯一的非 Java 微服务,它用 Node.js 编写。
但在这里,我们将关注在 Open Liberty 上运行的那些。
配置 Open Liberty 服务器的最重要的文件是server.xml文件。它定义了应在服务器中启用的功能以及每个功能的配置。您可以通过在server.xml中列出microProfile-4.1功能来启用所有 MicroProfile 4.1 功能,如下所示:
<featureManager>
<feature>microProfile-4.1</feature>
</featureManager>
否则,就像 Maven 依赖项一样,如果您更喜欢启用较少的功能,例如,如果您只想启用MicroProfile Health和MicroProfile Metrics,您可以像下面这样单独列出它们:
<featureManager>
<feature>mpHealth-3.1</feature>
<feature>mpMetrics-3.1</feature>
</featureManager>
除了您启用的功能外,其中一些支持配置段。许多 Java 企业版资源都在此配置,例如以下内容:
-
用于与关系数据库(如 IBM DB2)通信的 JDBC 数据源。
-
用于与消息系统(如 IBM MQ)通信的 JMS ActivationSpecs。
-
CloudantDatabase 用于与 NoSQL 数据存储(如 IBM Cloudant)通信。
对于特定的 MicroProfile 功能,以下是股票交易微服务如何配置MicroProfile JWT和MicroProfile Metrics功能:
<mpJwt id="stockTraderJWT" audiences="${JWT_AUDIENCE}" issuer="${JWT_ISSUER}" keyName="jwtSigner" ignoreApplicationAuthMethod="false" expiry="12h" sslRef="defaultSSLConfig"/>
<mpMetrics authentication="false"/>
这些功能已在之前的章节中讨论过,所以这里不再重复。但需要注意的是,我们引用的是环境变量,如${JWT_ISSUER},而不是硬编码这些值;这样,如果我们想修改这些值,我们就不必重新构建容器镜像——相反,我们只需更新 Kubernetes ConfigMap或Secret中的值,然后我们的操作员将为Deployment配置.yaml文件,以获取和传递适当的环境变量值。
除了 server.xml 文件外,src/main/liberty/config 目录中的其他 Open Liberty 特定文件包括 jvm.options(用于传递 JVM 系统属性等),以及你的密钥库和/或信任库文件(例如 src/main/liberty/config/resources/security 目录中的 key.p12 和 trust.p12)。这些文件按照与 Open Liberty 服务器所需的相同目录结构排列,这样我们就可以将整个目录复制到 Docker 容器中 Open Liberty 的适当位置,正如我们将在下一节中看到的。
Docker
一旦准备好所有需要成为 Docker 容器镜像一部分的输入文件,你就可以使用 Dockerfile 来指定你想要复制到哪里的文件。你还可以运行除了 COPY 以外的命令,例如设置文件权限或在容器中安装额外的工具:
-
首先,让我们看一下 Trader 微服务的
Dockerfile:docker pull of the image before running a build, to ensure you have the latest (as Docker would think you already have an image with such a tag, even though the one you have locally might be many months old). For example, do the following:kernel-slim 镜像,这意味着除了内核之外,镜像中不包含任何其他功能,以减小镜像大小。下一重要行表示我们想要将
src/main/liberty/config目录复制到容器镜像中的/config目录。实际上,这个/config目录在 Open Liberty 中是一个指向/opt/ol/wlp/usr/servers/defaultServer的软链接(或在商业 WebSphere Liberty 中指向/opt/ibm/wlp/usr/servers/defaultServer,你将通过FROM ibmcom/websphere-liberty:kernel-java11-openj9-ubi获取它)。我们复制整个目录(及其子目录),而不是为每个文件单独进行COPY,以减少最终 Docker 镜像中的层数。 -
接下来,我们运行一个名为
features.sh的脚本,该脚本下载我们在上一步中复制到 Docker 镜像中的server.xml中指定的所有功能。如果我们没有使用kernel-slim镜像,而是使用了full镜像,这一步就不需要了。注意我们希望在将 Maven 生成的 WAR 文件复制到容器之前运行此步骤,这样我们就不需要每次对应用程序进行小改动时都重新运行这个耗时的步骤。 -
接下来,我们将我们的 WAR 文件(或消息微服务通过
ear文件复制,因为它有一个 EJB - 一个消息驱动 Bean)复制到容器中。注意--chown 1001:0,这告诉它文件应由哪个用户和组拥有 - 没有这个,服务器不会以 root 身份运行,因此没有权限访问文件。 -
最后,我们运行
configure.sh脚本,它对文件权限进行一些进一步的调整,并构建共享类缓存以改善性能。 -
现在我们运行构建过程,这将执行
Dockerfile的每一行:jalcorn@Johns-MBP-8 trader % docker build -t trader .[+] Building 45.7s (10/10) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 37B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/openliberty /open-liberty:k 0.0s => [1/5] FROM docker.io/openliberty/open-liberty:kernel-slim-java11- 0.0s => [internal] load build context 0.3s => => transferring context: 11.41MB 0.2s => CACHED [2/5] COPY --chown=1001:0 src/main/liberty/config /config 0.0s => CACHED [3/5] RUN features.sh 0.0s => [4/5] COPY --chown=1001:0 target/TraderUI.war /config/apps/Trader 0.1s => [5/5] RUN configure.sh 44.8s => exporting to image 0.4s => => exporting layers 0.4s => => writing image sha256:d0a03e6e7fd2873a8361aa9c9c ad22dd614686778 0.0s => => naming to docker.io/library/trader 0.0s jalcorn@Johns-MBP-8 trader % -
现在我们已经生成了一个 Docker 镜像,我们可以将其推送到镜像仓库,以便你的操作员可以在 Kubernetes 环境中(如 OpenShift 容器平台(OCP))访问和使用。例如,这里我们将其推送到 Docker Hub:
jalcorn@Johns-MBP-8 trader % docker tag trader:latest ibmstocktrader/trader:latest jalcorn@Johns-MBP-8 trader % docker push ibmstocktrader/trader:latest The push refers to repository [docker.io/ibmstocktrader/trader] c139b5a83739: Pushing 13.9MB/62.74MB dae5b07894dc: Pushing 2.888MB/11.4MB 0b797df05047: Pushing 5.893MB/69.17MB 7daae910987c: Pushed 3efa9ea44ae4: Layer already exists 7d02e9817200: Layer already exists 267522994240: Layer already exists 0db07c8859ff: Layer already exists 2b4eefc8e725: Layer already exists 8a9f64ec0b16: Layer already exists 9b61e11e8907: Layer already exists 09b9a9d4c9f4: Layer already exists 83713a30b4bb: Layer already exists 1e8cd6732429: Layer already exists 476579af086a: Layer already exists jalcorn@Johns-MBP-8 trader %
既然我们已经构建了 Trader 微服务,那么就重复上述步骤来为 Stock Quote 微服务执行,我们稍后在本章中也会用到它。作为一个快速回顾,运行以下命令:
-
git clone https://github.com/IBMStockTrader/stock-quote -
cd stock-quote -
mvn package -
docker build -t stock-quote .
在下一节中,我们将探讨如何测试我们刚刚构建的容器镜像。
测试股票交易微服务
现在我们已经学会了如何构建我们的微服务,在将它们部署到 OpenShift 环境之前,下一步重要的步骤是首先对它们进行一些单元测试,以确保它们按预期工作。
测试前端微服务
我们可以通过在笔记本电脑上使用本地安装的 Docker 来执行此类单元测试。让我们运行我们刚刚为 Trader 构建的 Docker 容器,并尝试一下:
jalcorn@Johns-MBP-8 portfolio % docker run -p 9443:9443 -e JWT_AUDIENCE=test -e JWT_ISSUER=test -e TEST_MODE=true trader:latest
Launching defaultServer (Open Liberty 21.0.0.4/wlp-1.0.51.cl210420210407-0944) on Eclipse OpenJ9 VM, version 11.0.11+9 (en_US)
[AUDIT] CWWKE0001I: The server defaultServer has been launched.
<snip>
[INFO] SRVE0169I: Loading Web Module: Trader UI.
[INFO] SRVE0250I: Web Module Trader UI has been bound to default_host.
[AUDIT] CWWKT0016I: Web application available (default_host): http://5708495d563b:9080/trader/
[AUDIT] CWWKZ0001I: Application TraderUI started in 5.701 seconds.
[AUDIT] CWWKF0012I: The server installed the following features: [appSecurity-2.0, appSecurity-3.0, cdi-2.0, distributedMap-1.0, el-3.0, federatedRegistry-1.0, jaxrs-2.1, jaxrsClient-2.1, jndi-1.0, json-1.0, jsonb-1.0, jsonp-1.1, jsp-2.3, jwt-1.0, jwtSso-1.0, ldapRegistry-3.0, microProfile-4.0, monitor-1.0, mpConfig-2.0, mpFaultTolerance-3.0, mpHealth-3.0, mpJwt-1.2, mpMetrics-3.0, mpOpenAPI-2.0, mpOpenTracing-2.0, mpRestClient-2.0, oauth-2.0, openidConnectClient-1.0, opentracing-2.0, servlet-4.0, ssl-1.0, transportSecurity-1.0].
[INFO] CWWKF0008I: Feature update completed in 8.604 seconds.
[AUDIT] CWWKF0011I: The defaultServer server is ready to run a smarter planet. The defaultServer server started in 9.631 seconds.
这个 docker run 命令启动了容器,告诉它暴露其端口 9443(Open Liberty 的默认 HTTPS 端口),并传递了一些环境变量。
注意
环境变量 TEST_MODE 用于简化测试 Trader 微服务,它通常需要连接到代理微服务。它有一个选项可以绕过这一点,并使用硬编码的数据进行工作。
现在我们已经启动了容器,让我们在浏览器中访问 https://localhost:9443/trader。登录后(作为 stock/trader),我们可以看到硬编码的 TEST_MODE 数据:
![图 8.2 – 通过 docker run 测试 Trader UI 微服务]

图 8.2 – 通过 docker run 测试 Trader UI 微服务
恭喜你,你已经在容器中成功测试了 Trader 微服务!
测试后端微服务
在浏览器中测试前端微服务相当简单,因为网络浏览器会为你处理登录 Cookie,但当涉及到测试后端微服务之一时,由于使用了用于单点登录(SSO)的 JSON Web Token(JWT),这会变得稍微困难一些。股票报价 微服务就是这样一个后端微服务的例子,它使用 MicroProfile JWT 来确保在成功通过登录挑战并返回 JWT 之前,不允许任何调用者进入。
通过 docker run -p 9080:9080 -e JWT_AUDIENCE=test -e JWT_ISSUER=test stock-quote:latest 启动我们之前构建的股票报价微服务的 Docker 容器。和之前一样,这暴露了一个端口,以便我们可以与之通信;在这种情况下,它是端口 9080,用于标准的(未加密的)HTTP 访问。
然而,直接调用由股票报价微服务公开的 REST API,通过运行如 curl http://localhost:9080/stock-quote/TEST 这样的命令,将会因为缺少 SSO 凭据而返回 401 错误。
注意
TEST 是另一个特殊值,它返回硬编码的数据,绕过调用互联网以获取实际股票报价的过程。
我们可以通过使用我们的前端交易员微服务进行登录,然后在浏览器内集成的 Web 检查器中查找 JWT cookie 的值来解决此问题。这将在每个浏览器中略有不同;在我的情况下,我正在使用 Mac 上的 Safari,并从菜单栏中选择开发 | 显示 Web 检查器。然后我只需找到摘要请求(这是显示所有投资组合的 servlet)并将其JWT cookie 的值复制到剪贴板:

图 8.3 – 从 Web 检查器获取 JWT 值
然后将这个长字符串粘贴到curl中的Authorization头中,如下所示:
jalcorn@Johns-MBP-8 StockTrader % curl -H "Authorization: eyJraWQiOiJWeUltUHBWVG1RY0ZfeV9SdVdHZmh1YkRGd1cxYjQ1d3FO QU1mUWZmV3hBIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJ0b2tlbl 90eXBlIjoiQmVhcmVyIiwiYXVkIjoic3RvY2stdHJhZGVyIiwic3ViIjoic3R vY2siLCJ1cG4iOiJzdG9jayIsImdyb3VwcyI6WyJTdG9ja1RyYWRlciJdLCJy ZWFsbSI6IkJhc2ljUmVnaXN0cnkiLCJpc3MiOiJodHRwOi8vc3RvY2stdHJhZG VyLmlibS5jb20iLCJleHAiOjE2MjIxNTA0MDUsImlhdCI6MTYyMjE0MzIwNX0.k2z65b36MJU4fhpqq7S66pYV8rwZalT3aQK-hoOnINeVarg6k3AHIP6lN_ZHsT KX5W4b8q81o5gC0KSdEFN6VSi3qdC7a02aotICbuuZh459F7IuPOC5rWbwrUa kznNxh2I7s8Nurhcb2_UDq1WM0POyZYMpuDokys-CeH5w3QyLZ7tx_IS6czU9 yh17bX4pp3eNH0JLCZybB_i-rBHh8cwzKLk3q73CvPhHJ2jw_zw79viaSUs WOeIkF21S-iB2v4PYw7nTz54pp02pu_eHi8W-hRCebN0O7xsG_JNZUPEgloN b9O8b0d_7V8qyKD5m_YpSh45y_CZ9j82i_Ho_9A " http://localhost:90 80/stock-quote/TEST
{"date":"2021-05-10","price":123.45,"symbol":
"TEST","time":0}% jalcorn@Johns-MBP-8 StockTrader %
如您所见,我们从我们后端股票报价微服务的快速单元测试中得到了123.45的股票价格。请注意,JWT 会自动过期,所以过一段时间后,如果您的curl调用开始被拒绝,您将不得不再次这样做以获取一个新的 JWT。当然,也有 GUI REST 测试客户端可以简化如何将凭证传递给后端服务这一常见问题。
现在我们已经看到了如何构建和测试每个微服务的容器的方法,让我们更深入地探讨在股票交易员应用程序中使用 MicroProfile 4.1 特性的情况。
在股票交易员中使用 MicroProfile 特性
让我们从了解哪些股票交易员微服务使用了哪些 MicroProfile 特性开始。请注意,一个微服务可以从给定的 MicroProfile 特性中受益有两种方式——隐式或显式:
-
在隐式情况下,只需在您的
server.xml文件中列出该特性即可获得价值;例如,您可以通过启用mpHealth-3.1特性来获得就绪和存活探针的默认实现。 -
在显式情况下,您直接针对该特性提供的 API 进行编码,例如通过在您的 Java 类中针对
org.eclipse.microprofile.health包中的类来编写自己的自定义逻辑,以确定您的微服务是否健康。
在以下表中,我们可以看到哪些微服务使用了哪些特性,其中not表示完全不使用,dash表示隐式使用,checkmark表示显式使用:

表 8.1 – 每个股票交易员微服务中的 MicroProfile 使用情况
注意,基于 tWAS 的 Notification-Twitter 和基于 Node.js 的 Tradr 没有被列出(因为它们不在 MicroProfile 兼容的服务器上运行),以及消息微服务只是一个没有 HTTP 端点的 MDB,所以大多数 MicroProfile 特性不适用于它。此外,在股票交易员中,我们倾向于使用(基于 Istio 的)OpenShift 服务网格通过.yaml文件来定义我们的容错策略,而不是直接编码到 mpFaultTolerance 特性中。
MicroProfile 特性的隐式使用
MicroProfile 为那些正在现代化为 MicroProfile 兼容应用程序服务器的应用程序提供了显著的好处,即使这些应用程序没有更新到各种 MicroProfile 功能的 API。这在某些情况下可能很有用,例如,当你从传统的 WebSphere 应用程序服务器现代化到 Open Liberty 时,但不想进行大量的代码更新。在以下子节中,我们将检查哪些 MicroProfile 功能可以提供这样的隐式好处。
MicroProfile 健康检查
如前所述,你只需在server.xml文件中启用mpHealth-3.1功能,就可以获得 Kubernetes 启动、就绪和存活探测的自动实现。请注意,启动探测在 Kubernetes 中相对较新,是在版本 1.16 中添加的,对应于 OpenShift 版本 4.3。MicroProfile 对这些探测的支持是在版本 4.1 中添加的,这是包含 MicroProfile Health 版本 3.1 的第一个版本。
如果你的应用程序启动时间较长(例如,如果它从数据库加载并缓存大量数据),启动探测非常有用。
就绪探测非常重要,这样工作就不会被路由到尚未真正准备好处理此类请求的新启动 Pod。
默认的就绪实现将在服务器及其所有应用程序完全启动之前返回false,然后将继续返回true,直到收到服务器停止的信号(例如,当HorizontalPodAutoscaler减少部署的 Pod 数量时)。这也是默认的启动探测实现——所以,如果你提供了自己的显式实现,你从启动探测中获得的额外价值,将超出默认就绪探测实现所提供的内容。
默认的存活实现只要服务器能够处理传入的 HTTP 请求就会返回true。这通常情况下是成立的,除非 Web 容器中的所有线程都挂起/正在使用,或者发生了像OutOfMemoryError这样的严重问题。Kubernetes 会自动杀死任何连续多次失败存活探测的 Pod,并启动一个新的 Pod 来替换它。
这里是直接在我们的容器上调用启动、就绪和存活探测的结果:
jalcorn@Johns-MBP-8 StockTrader % curl http://localhost:9080/health/started
{"checks":[],"status":"UP"}
% jalcorn@Johns-MBP-8 StockTrader % curl http://localhost:9080/health/ready
{"checks":[],"status":"UP"}%
jalcorn@Johns-MBP-8 StockTrader % curl http://localhost:9080/health/live
{"checks":[],"status":"UP"}%
jalcorn@Johns-MBP-8 StockTrader %
MicroProfile 度量指标
有三种类型的度量指标:基础、供应商和应用。前两种类型将自动对定期抓取/metrics端点(如Prometheus)的任何人可用,只需在server.xml文件中启用mpMetrics-3.0功能即可。第三种类型仅在应用程序编码为注释或来自org.eclipse.microprofile.metrics包的显式 API 调用时才可用。
基础指标由 MicroProfile Metrics 规范定义,通常包括与堆大小、垃圾收集和线程计数相关的 JVM 级别指标,以及各种计数器和计时器。供应商指标因每个应用程序服务器而异,包括 JDBC 和 JMS 连接池使用以及其他应用程序服务器为你管理的事情。让我们看看从我们的运行 Stock Quote 容器中可用的几个基础和供应商指标(完整集合将占用许多页面):
jalcorn@Johns-MBP-8 StockTrader % curl http://localhost:9080/metrics
# TYPE base_classloader_loadedClasses_count gauge
# HELP base_classloader_loadedClasses_count Displays the number of classes that are currently loaded in the Java virtual machine.
base_classloader_loadedClasses_count 12491
# TYPE base_thread_count gauge
# HELP base_thread_count Displays the current number of live threads including both daemon and non-daemon threads.
base_thread_count 53
# TYPE base_memory_usedHeap_bytes gauge
# HELP base_memory_usedHeap_bytes Displays the amount of used heap memory in bytes.
base_memory_usedHeap_bytes 6.675884E7
# TYPE vendor_servlet_request_total counter
# HELP vendor_servlet_request_total The number of visits to this servlet since the start of the server.
vendor_servlet_request_total{servlet="StockQuote_com_ibm_hybrid_cloud_sample_stocktrader_stockquote_StockQuote"} 1
# TYPE vendor_threadpool_size gauge
# HELP vendor_threadpool_size The size of the thread pool.
vendor_threadpool_size{pool="Default_Executor"} 8
# TYPE vendor_servlet_responseTime_total_seconds gauge
# HELP vendor_servlet_responseTime_total_seconds The total response time of this servlet since the start of the server.
vendor_servlet_responseTime_total_seconds{servlet="StockQuote_com_ibm_hybrid_cloud_sample_stocktrader_stockquote_StockQuote"} 0.9500412
jalcorn@Johns-MBP-8 StockTrader %
MicroProfile OpenTracing
与可观察性相关的另一个功能是 MicroProfile OpenTracing。通过启用mpOpenTracing-2.0功能,它将自动为任何 JAX-RS 操作生成跟踪跨度。这些跟踪跨度被发送到任何已注册的跟踪器,例如 Jaeger。如果你已经注册了 Jaeger 跟踪器,你应该在你的容器输出中看到以下内容,表明跟踪跨度正在每个 JAX-RS 操作上发送:
[INFO] Initialized tracer=JaegerTracer(version=Java-1.5.0, serviceName=StockQuote, reporter=CompositeReporter(reporters=[RemoteReporter(sender=UdpSender(host=localhost, port=6831), closeEnqueueTimeout=1000), LoggingReporter(logger=org.slf4j.impl.JDK14LoggerAdapter(io.jaegertracing.internal.reporters.LoggingReporter))]), sampler=RemoteControlledSampler (maxOperations=2000, manager=HttpSamplingManager(hostPort=localhost:5778), sampler=ProbabilisticSampler(tags={sampler.type=probabilistic, sampler.param=0.001})), tags={hostname=5f0 6cf0b9a96, jaeger.version=Java-1.5.0, ip=172.17.0.2}, zipkinSharedRpcSpan=false, expandExceptionLogs=false, useTraceId128Bit=false)
[INFO] CWMOT1001I: A JaegerTracer instance was created for the StockQuote application. Tracing information is sent to localhost:6831.
MicroProfile Open Tracing 的另一个重要功能是,一个跨度可以链接到所需数量的调用,因此,而不是只看到一个跨度显示 A 调用了 B,另一个跨度显示 B 调用了 C,一个跨度可以包含从 A 到 B 再到 C 的调用路径,包括它们发生的时间、每个部分花费的时间以及更多。能够看到例如包含交易员调用经纪人调用投资组合调用股票报价的跨度,对于那些想要了解所有这些各种微服务在运行时如何组合在一起以及它们是否按预期执行的人来说是有价值的。
MicroProfile OpenAPI
MicroProfile OpenAPI 功能相当酷,因为它会通过在server.xml文件中启用mpOpenAPI-2.0功能来生成关于你的 JAX-RS 类的文档。如果有人想知道你的微服务提供了哪些操作,他们只需执行curl http://localhost:9080/openapi来获取一个.yaml文件,该文件解释了每个可用的操作、它们的输入参数以及它们返回的数据结构。
Open Liberty 提供的一个很好的额外功能是能够生成一个人类友好的网页渲染信息(有时这被称为 Swagger UI)。只需在浏览器中输入http://localhost:9080/openapi/ui即可查看 HTML 渲染。让我们看看我们的 Broker 微服务的 HTML 渲染:
![图 8.4 – MicroProfile OpenAPI UI]

图 8.4 – MicroProfile OpenAPI UI
在这里我们可以看到,这些操作返回了一个Broker JSON 对象。
我们还可以深入到其中一个操作中,查看其详细信息 – 让我们选择用于更新股票交易的PUT操作:

图 8.5 – MicroProfile OpenAPI UI 中的操作详情
现在,我们可以看到预期的路径和查询参数,甚至可以点击表单中每个字段输入的curl命令,以及调用操作的结果。
MicroProfile JWT
MicroProfile 提供的最后一个具有隐含价值的特性是 MicroProfile JWT。只需在server.xml文件中启用mpJWT-1.2功能(以及server.xml文件中的几个其他段落、WAR 文件的web.xml中的一个段落,以及keystore/truststore中的签名密钥),就可以使应用服务器拒绝任何没有在 Authorization HTTP 头或 cookie 中包含所需 JWT 的调用。
这是一个非常强大的功能,因为它提供了很好的单点登录执行,而无需编辑任何 Java 代码。如果有人试图调用你的微服务而没有附加适当的 JWT,它将因403错误而被拒绝:
jalcorn@Johns-MBP-8 StockTrader %
curl -I http://localhost:9080/broker
HTTP/1.1 403 Forbidden
X-Powered-By: Servlet/4.0
Content-Type: text/html;charset=ISO-8859-1
$WSEP:
Content-Language: en-US
Connection: Close
Date: Tue, 11 May 2021 05:27:45 GMT
jalcorn@Johns-MBP-8 StockTrader %
当这种情况发生时,你将看到以下消息被你的容器记录:
[ERROR] CWWKS5522E: The MicroProfile JWT feature cannot perform authentication because a MicroProfile JWT cannot be found in the request.
在不进行任何 Java 编码的情况下,获得如此强大的安全执行功能,是 MicroProfile 提供的一个非常好的特性!
摘要
你现在应该对如何构建和单元测试 Stock Trader 应用程序中的任何微服务有了一定的了解。你也应该现在对如何容器化这些微服务,然后运行这些容器并调用这些微服务感到舒适。
注意,通常情况下,你不会像本章所介绍的那样手动从命令提示符中运行这些构建步骤,而是会有一个 DevOps 管道为你运行这些步骤,例如,当你向 Git 仓库提交更改时,会自动通过 webhook 启动。例如,请参阅关于 Trader 微服务的此类 CI/CD 管道的这篇博客文章,它还执行了各种安全和合规性检查:medium.com/cloud-engagement-hub/are-your-ci-cd-processes-compliant-cee6db1cf82a。但是,了解如何手动操作是很有好处的,而不是让它看起来像某种神奇、神秘的事情发生,以便你的容器镜像在镜像仓库中构建并可用。
我们还介绍了 Stock Trader 如何从许多 MicroProfile 功能中受益,即使它没有明确编码到这些功能中。将现代化为 MicroProfile 兼容的应用服务器可以“免费”提供这些好处,而无需你的开发人员花费时间修改他们的代码,这是参与此类应用程序现代化努力的强大动力。
在下一章中,我们将探讨如何通过其操作员将此应用程序部署到 OpenShift 集群,并探讨我们如何使用它来执行一些Day 2操作。
第九章:部署和第二天操作
到目前为止,我们已经看到了许多示例应用程序的代码片段和截图,即IBM 股票交易员。现在,让我们学习如何将其部署到您自己的OpenShift Container Platform集群或您选择的任何Kubernetes平台。虽然让它运行很重要,但学习如何维护它并调整它以满足您的需求也同样重要。
与您在网上可能看到的许多 Hello World 示例不同,这些示例要么根本没有任何操作员(只是手动应用 .yaml 文件进行安装),要么每个微服务可能只有一个非常简单的操作员,而IBM 股票交易员示例有一个组合操作员,它不仅安装所有微服务,还配置了与所有先决服务的连接性,包括用于身份验证的所有凭证。
此组合操作员还提供了由操作员定义的自定义资源的先进形式 .yaml 文件。
在本章中,我们将涵盖以下主要主题:
-
理解操作员的作用
-
通过 OpenShift 控制台安装操作员
-
通过操作员表单 UI 部署应用程序
-
通过命令行部署应用程序
-
第二天操作
到本章结束时,您将熟悉使用操作员从 UI 或 CLI 部署应用程序,以及如何使用它来维护应用程序的后续操作。
技术要求
要使用本节中描述的操作员,您需要安装以下工具:
-
Git 客户端 –
git-scm.com -
Kubernetes 客户端 (
kubectl) – https://kubernetes.io/docs/tasks/tools/#kubectl
此外,您还需要有一个可用的 Kubernetes 集群。对于本章的 CLI 部分,您可以使用您喜欢的任何 Kubernetes 发行版。但为了尝试本章的 UI 部分,Kubernetes 发行版需要是 OpenShift Container Platform (OCP),因为所有截图都是从 OpenShift 控制台获取的。请向您的管理员询问如何为您的集群提供 kubectl 访问权限,以及如果使用 OCP,控制台的 URL。
理解操作员的作用
在深入探讨 IBM 股票交易员操作员的详细信息之前,让我们稍微退后一步,考虑一下操作员的作用以及为什么它们是好事。为此,重要的是要回忆起从 第七章,Open Liberty、Docker 和 Kubernetes 的 MicroProfile 生态系统,Kubernetes 定义了一个模型,其中包含几个内置对象类型,例如 Deployments、Services、Ingresses、ConfigMaps 和 Secrets。在真正的面向对象哲学中,这些对象不仅具有数据,还具有行为;操作员的职责是参与并指导它们所管理的对象的完整创建、检索、更新和删除(CRUD)生命周期。
一个关键点是,Kubernetes 不仅有其内置对象,还有一个可扩展模型,其中供应商可以向该词汇添加内容,定义额外的对象类型以及它们在 Kubernetes 环境中的行为。Kubernetes 称这为自定义资源定义(CRD)。CRD 本质上是一个模式,描述了特定实例的自定义资源(CR)配置的字段。我经常把它想象成CR对应于它的CRD,就像XML对应于它的XSD。
在操作员兴起之前,一种称为yaml文件的技术为应用程序需要的每个 Kubernetes 内置对象提供,并具有在yaml文件中参数化字段的一些能力。股票交易应用本身在 OpenShift 版本 4 到来之前有一个 Helm 图表(在github.com/IBMStockTrader/stocktrader-helm),OpenShift 版本 4 是一个相当大的架构重设计,操作员是其核心。
尽管 Helm 工作得很好,但它是一种有限的技术,因为它在安装应用后没有提供任何帮助。另一方面,操作员始终在监听,并准备好对它所操作的 CRD 类型的 CR 的任何变化做出反应。操作员还可以提供第二天操作,正如我们将在本章后面看到的那样。
注意
编写操作员有不同的方法,其中一种是将 Helm 图表包装起来。股票交易应用的操作员就是这样一种基于 Helm 的操作员。查看(或克隆)github.com/IBMStockTrader/stocktrader-operator存储库以浏览其源代码,打开问题,或提交拉取请求(PR)以改进操作员。
另一种思考方式是,操作员扩展了 Kubernetes 知道如何管理的对象类型;例如,一个管理PacktBook类型 CRD 的操作员将启用诸如kubectl get PacktBooks或kubectl describe PacktBook microprofile之类的命令,就像您会对内置的 Kubernetes 对象(如Deployments或Services)进行操作一样。
现在,让我们学习如何使用操作员来构建我们的股票交易应用。我们将从 OpenShift 控制台 UI 方法开始,然后我们将探讨如何从 CLI 使用它。
通过 OpenShift 控制台安装操作员
作为对第三章的快速回顾,介绍 IBM 股票交易云原生应用,IBM 股票交易应用由大约一打微服务(其中许多是可选的)以及大约相同数量的先决资源组成,例如数据库和消息系统。组合操作员引导您为每个微服务提供所有设置,并配置它们与各种后端资源的连接,正如我们在这里可以看到的:

图 9.1 – 架构图
如我们所见,需要配置很多部分才能使一切正常工作。操作符引导我们为每个部分提供此类配置信息(尽管它实际上并不安装后端资源——它只是请求连接到现有资源的端点和凭证详细信息,这些资源可能正在您的集群中运行,或可以从其他地方访问,例如云中的 DB2-as-a-Service (DB2aaS))。在接下来的两个部分中,我们将了解 OperatorHub 以及如何在我们的集群中安装操作符。
OperatorHub
OpenShift 4.x 控制台包含一个名为 OperatorHub 的操作符目录。这里有几个内置的目录源,管理员可以添加额外的源,以便在目录中显示更多他们从供应商(如各种 IBM Cloud Paks 的目录源)购买或他们自己的开发者创建的操作符。让我们通过点击左侧导航菜单中的 Operators | OperatorHub 来查看 OpenShift 控制台的 OperatorHub 部分:

图 9.2 – OperatorHub
在这里,我们可以看到我们有 460 个操作符可用,其中 5 个目前安装在我们的集群中。让我们学习如何安装我们自己的目录源,以便我们可以使我们的操作符(们)在这里显示:
-
我们将首先点击左侧导航菜单中的 管理 | 集群设置。
-
然后,在过滤器字段中,我们选择
Hub,列表将过滤出仅包含该字符串的条目):![图 9.3 – 集群设置]()
图 9.3 – 集群设置
-
然后,在结果页面上,点击 源 选项卡。
您会看到有四个预先配置的源(全部来自 Red Hat),以及每个源贡献的操作符数量:
![图 9.4 – 目录源]()
图 9.4 – 目录源
如果您的管理员已将您的集群设置为显示默认操作符之外的操作符,您可能会看到超过最初的四个。
-
然后,只需点击 创建目录源 按钮提供我们新源包含 IBM 股票交易员 应用程序的操作符的详细信息:
![图 9.5 – IBM 股票交易员目录源的详细信息]()
图 9.5 – IBM 股票交易员目录源的详细信息
注意
我们选择将其设置为 集群范围内的目录源,这样无论您在集群中使用哪个命名空间,它都是可用的。如果您更喜欢(或只在特定命名空间中拥有权限),您可以选择 命名空间目录源。请注意,如果您在一个使用有限安全权限的 ID 的真正受限环境中,您可能需要要求管理员为您执行此操作。
-
您可以在前三个字段中输入您想要的任何值(我选择了我的团队名称,
Cloud Engagement Hub)。 -
最后一个字段,指定在哪里找到目录源的 Docker 镜像,是最重要的,需要设置为
docker.io/ibmstocktrader/stocktrader-operator-catalog:v0.2.0。注意
当然,您可以从 GitHub 仓库
IBMStockTrader/stocktrader-operator克隆并构建自己的镜像(按照github.com/IBMStockTrader/stocktrader-operator/blob/master/bundle/README.md中的说明)并将其推送到您自己的镜像仓库,并在此处指定它,但为了简化操作,我们使用预先构建的版本,它托管在Docker Hub上,以便方便使用。 -
点击创建按钮后,它将带您回到目录源列表。
初始时,来自该新源的运算符数量将只显示一个短横线,直到 OpenShift 能够下载指定的 Docker 容器镜像并解析其内容。一旦完成,它将更新以显示正确的运算符数量,在我们的例子中只有一个,如图所示:
![图 9.6 – IBM 股票交易员目录源详细信息
![图 9.7 – 我们集群的 OperatorHub 中的 IBM 股票交易员操作符
图 9.6 – IBM 股票交易员目录源详细信息
-
现在如果您回到
股票,在过滤器字段中,它将只显示包含该字符串的条目:
![图 9.7 – 我们集群的 OperatorHub 中的 IBM 股票交易员操作符
![图 9.7 – IBM 股票交易员操作符信息页面
图 9.7 – 我们集群的 OperatorHub 中的 IBM 股票交易员操作符
恭喜!您现在已将 IBM 股票交易员应用程序的操作符添加到 OperatorHub 目录中。在下一节中,我们将探讨如何安装操作符。
安装操作符
现在我们已经将操作符添加到我们集群的目录中,让我们来使用它:
-
只需点击操作符的瓷砖。这样做将显示有关我们刚刚在上一节中提供的操作符的更多详细信息:![图 9.8 – IBM 股票交易员操作符信息页面
![图 9.8 – IBM 股票交易员操作符信息页面
图 9.8 – IBM 股票交易员操作符信息页面
如您所见,这显示了关于操作符的一些基本信息,包括其 readme 文件。在此对话框中无需执行任何操作,只需点击安装按钮。
![图 9.9 – IBM 股票交易员操作符订阅页面
![图 9.9 – IBM 股票交易员操作符订阅页面
图 9.9 – IBM 股票交易员操作符订阅页面
在这里,我们可以看到关于操作符在我们集群中如何表现的信息;例如,我们可以看到,像所有安装到您集群所有命名空间中工作的 OperatorHub 集成操作符一样,操作符实际运行的命名空间被称为openshift-operators。请注意,安装操作符在技术上是指对该操作符进行订阅(有关更多信息,请参阅下一节通过 CLI 部署应用程序)。
-
一旦您点击安装按钮,您将短暂地看到一个对话框,显示正在安装(直到其 pod 启动并通过就绪性检查),然后它会告诉您是否已成功安装。
![图 9.10 – IBM 股票交易员运营商已安装
![图片 B17377_09_10.jpg]
图 9.10 – IBM 股票交易员运营商已安装
恭喜!您现在已安装了IBM 股票交易员应用程序的运营商。在下一节中,我们将探讨如何使用该运营商部署应用程序。
通过运营商表单 UI 部署应用程序
部署应用程序时,我们将遵循以下步骤:
-
点击图 9.10中显示的查看运营商按钮。您将被带到显示运营商信息的页面,当然,它看起来与我们第一次在运营商中心点击运营商时看到的非常相似。![图 9.11 – IBM 股票交易员运营商详细信息页面
![图片 B17377_09_11.jpg]
图 9.11 – IBM 股票交易员运营商详细信息页面
-
我们可以使用图 9.11中显示的创建实例链接来启动安装我们的IBM 股票交易员应用程序实例的对话框:![图 9.12 – IBM 股票交易员运营商表单 UI
![图片 B17377_09_12.jpg]
图 9.12 – IBM 股票交易员运营商表单 UI
-
接下来,我们将提供一个
microprofile作为实例名称在此处使用,然后创建的配置文件部署将被命名为microprofile-portfolio。由于这是一个复合运营商——也就是说,它安装整个应用程序,而不是仅针对特定微服务的运营商——它使用可展开/可折叠的部分来分隔每个微服务的配置设置。它还为它所依赖的每个服务(如数据库部分和IBM MQ 设置部分)提供这样的部分。在最上面是一个全局部分,用于适用于所有选定微服务的设置。请注意,大多数设置都有很好的、合理的默认值,只有在特殊情况下才需要调整。一个例外是数据库部分,所以让我们展开它,看看我们需要填写什么,因为这是我们无法在没有的情况下运行的一个强制性先决服务:
![图 9.13 – IBM 股票交易员运营商表单 UI 的数据库部分
![图片 B17377_09_13.jpg]
图 9.13 – IBM 股票交易员运营商表单 UI 的数据库部分
-
如您所见,必须提供标准端点类型信息,例如数据库服务器的数据库主机名(或 IP 地址)和用于连接它的数据库端口号,以及用于认证所需凭证信息。对于每个后续部分,如Cloudant、ODM或MQ,都需要请求非常类似的信息。
让我们展开一个可选的微服务,例如账户微服务:
![图 9.14 – IBM 股票交易员运营商表单 UI 的账户微服务部分
![图片 B17377_09_14.jpg]
图 9.14 – IBM 股票交易操作员表单 UI 的账户微服务部分
注意顶部的真/假开关,你可以在这里指定是否要启用此可选微服务。该部分的其他设置只有在选择启用此微服务时才会生效。还要注意指定此微服务的 Docker 容器镜像位置的选项卡;默认情况下,它将预先填充在Docker Hub中的位置,我们在这里托管每个微服务的预构建版本以方便使用,尽管如果你自己构建了微服务(如前一章所述)并将其推送到自己的镜像仓库,你也可以在这里输入自己的值。
注意,第一次或第二次安装应用程序时,有表单 UI 引导你完成整个过程是非常有帮助的,但过了一段时间,这可能会变得有点无聊,因为需要为每个微服务和它们依赖的每个先决服务填写每个值。因此,你也可以选择将这些问题的答案简单地放在一个.yaml文件中,你可以直接将其拖放到此表单的第二选项卡(YAML 视图):

图 9.15 – IBM 股票交易操作员表单 UI 的 YAML 视图选项卡
无论你以何种方式提供输入,一旦你在页面底部点击创建按钮,就会得到相同的结果。你应该会看到你提供名称的新实例出现在IBM 股票交易应用程序实例列表中:

图 9.16 – IBM 股票交易操作员 UI 的StockTraders部分
如果我们点击那个名称,我们将看到关于我们新部署实例的信息。有几个选项卡;资源选项卡特别有用,可以查看在第七章,“使用 Open Liberty、Docker 和 Kubernetes 的 MicroProfile 生态系统”中讨论的哪些 Kubernetes 资源是由操作员创建的:

图 9.17 – IBM 股票交易操作员 UI 的“资源”选项卡
这里显示的内容取决于你选择启用哪些微服务。由于这可能是一个很长的列表,因此顶部左侧有一个过滤器按钮,可以用来过滤列表,只显示特定类型的 Kubernetes 对象:

图 9.18 – IBM 股票交易操作员表单 UI 的过滤器对话框
在这里,我们可以看到在这个实例中,我们有 8 个 Deployment(微服务),7 个 Service(其中一个微服务是一个没有 HTTP 端点的 MDB,因此没有 Service),1 个 ConfigMap,1 个 Secret,3 个 Route,和 2 个 HorizontalPodAutoscalers。如果我们启用了其他选项,例如在表单/yaml 的 全局 部分的 Istio 真假设置,那么我们会看到像 Gateway、VirtualService、DestinationRule 和 NetworkPolicy 这样的额外项,这些项的值不为零。我们还可以通过在 OpenShift 控制台中点击 工作负载 | Pods 来查看资源,以查看所有正在运行的 pod:
![图 9.19 – 组成 IBM 股票交易应用的每个微服务的 pod]
![img/B17377_09_19.jpg]
图 9.19 – 组成 IBM 股票交易应用的每个微服务的 pod
我们已经看到了如何通过 OpenShift 控制台安装 IBM 股票交易应用。在下一节中,我们将探讨如何通过 kubectl 命令行界面(CLI)来使用操作符。
通过 CLI 部署应用
有时候你需要通过除了使用图形用户界面以外的其他方法来完成某些事情。也许你只是更喜欢使用 CLI。或者,也许你想要在 持续集成/持续部署(CI/CD)管道的步骤中自动化此类工作。或者,也许你正在使用除了 Red Hat OpenShift 容器平台(OCP)以外的 Kubernetes 发行版,例如来自超大规模提供商之一,如 Amazon Web Services(AWS)或 Microsoft Azure。要在没有 OpenShift 控制台的好处下部署应用,请按照以下步骤操作:
-
你首先需要做的是确保通过遵循
olm.operatorframework.io/docs/getting-started/中的说明来安装brew install operator-sdk。注意
operator-sdk用于生成像 IBM 股票交易应用那样的操作符。生成此操作符起点确切的命令是operator-sdk init --plugins helm --group operators --kind StockTrader --domain ibm.com --version v1 --helm-chart ../stocktrader-helm/stocktrader-1.5.0.tgz。 -
下一步是创建包含以下内容的
.yaml文件的目录源:apiVersion: operators.coreos.com/v1alpha1 kind: CatalogSource metadata: name: cloud-engagement-hub spec: publisher: IBM displayName: Cloud Engagement Hub image: 'docker.io/ibmstocktrader/stocktrader- operator-catalog:v0.2.0'唯一真正重要的值是
image字段 – 你可以为其他字段指定任何你想要的值。将文件命名为你想要的任何名称,例如catalog-source.yaml。确保你从终端窗口登录到你的集群,然后运行命令kubectl apply -f catalog-source.yaml。大约一分钟后,目录源将可用,就像通过 OpenShift 控制台完成的那样。 -
接下来,你将通过目录源安装操作符。创建另一个包含以下内容的
.yaml文件:apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: stocktrader-operator namespace: openshift-operators spec: channel: alpha installPlanApproval: Automatic name: stocktrader-operator source: cloud-engagement-hub sourceNamespace: openshift-marketplace startingCSV: stocktrader-operator.v0.2.0你可以取任何你想要的名称,例如
subscription.yaml。然后,运行kubectl apply -f subscription.yaml。一旦完成,操作员将被安装并可用。 -
最后一步是应用你想要创建的 IBM 股票交易员应用程序实例的 yaml 文件。正如在 理解操作员角色 部分中讨论的那样,操作员定义了一个 CRD – 在这种情况下,为
StockTrader类型的对象 – 而在这里我们正在创建该类型的 CR。股票交易员 CR yaml 的结构与我们在 OpenShift 控制台中看到的是相同的;每个可展开的部分映射到 .yaml 文件中的缩进级别。例如,每个微服务都有一个部分,以及它们所依赖的每个先决服务。以下是一个示例
CR yaml的片段,其中大部分内容已被裁剪以避免出现许多页的 yaml 内容:apiVersion: operators.ibm.com/v1 kind: StockTrader metadata: name: microprofile spec: global: auth: basic healthCheck: true ingress: false istio: false istioNamespace: mesh route: true traceSpec: "*=info" jsonLogging: false disableLogFiles: false monitoring: true specifyCerts: false database: type: db2 db: BLUDB host: dashdb-txn-sbox-yp-dal09- 08.services.dal.bluemix.net id: my-id-goes-here password: my-password-goes-here port: 50000 account: enabled: true replicas: 1 autoscale: false maxReplicas: 10 cpuThreshold: 75 image: repository: ibmstocktrader/account tag: 1.0.0 url: http://{{ .Release.Name }}-account- service:9080/account在你传递的 yaml 中未指定的任何字段将使用其默认值,所以你实际上只需要填写你想要设置为非默认值的字段。要查看完整的
Stock Trader CR yaml 文件示例,你可以复制/粘贴操作员页面(如图 9.15 所示)切换到 YAML 视图 选项卡时出现的文本,或者去 GitHub 上查看github.com/IBMStockTrader/stocktrader-operator/blob/master/config/samples/operators_v1_stocktrader.yaml。 -
一旦你的 CR yaml 文件填写完毕,保存它,使用你喜欢的任何文件名,例如
stock-trader.yaml,然后部署 IBM 股票交易员应用程序的实例就像运行kubectl apply -f stock-trader.yaml那么简单。这需要几分钟才能完成。一旦完成,你可以通过运行简单的
kubectl get pods命令来查看为你所选微服务运行的哪些 pod:

图 9.20 – 控制台输出
恭喜!你已确认部署了 IBM 股票交易员应用程序,只需创建并应用了三个 yaml 文件!现在你已经学会了如何部署应用程序,让我们学习通过操作员可以做什么。
理解第二天操作
Kubernetes 社区的人常说部署是 第一天,而你之后为维护应用程序所做的事情是 第二天 操作。以下是一些 第二天 操作的例子:
-
扩展或缩减特定微服务的规模
-
升级到微服务的新版本
-
设置跟踪字符串以执行问题确定
让我们详细看看每一个。
扩展微服务
微服务架构的一个好处是你可以独立扩展每个微服务。你不必一次性扩展单体应用程序的所有部分,而只需扩展遇到吞吐量或响应时间问题的部分。
当使用由操作员生成的资源时,需要注意的一点是操作员本身 拥有 这些资源,并且不会让您直接更改它们。例如,如果您想编辑 Portfolio 部署以扩展 pod 的数量,尝试这样做可能会暂时看起来有效,但实际上,操作员会持续监控,并将任何在其权限之外被编辑的资源还原。有一个称为 reconciliation 的过程,高级操作员可以使用它来决定是否以及如何合并请求的更改,但像 Stock Trader 这样的简单基于 Helm 的操作员将拒绝直接编辑其生成的任何 Kubernetes 资源。
正确进行此类更改的方法是编辑 Stock Trader 部署实例的 CR yaml 文件。您可以通过 OpenShift 控制台或 CLI 来这样做。如果使用 CLI,您可以通过设置 KUBE_EDITOR 环境变量来选择您想要的任何基于文本的编辑器。例如,如果您在 Mac 上开发,并且更喜欢它的 nano 编辑器而不是老式的 vi 编辑器,只需运行命令 export KUBE_EDITOR=nano。
因此,如果您想将您的 Portfolio 部署从单个 pod 扩展到两个,您只需运行 kubectl edit StockTrader microprofile,这将将其当前的 yaml 文件加载到指定的编辑器中,在那里您会进入 portfolio 部分,将 replicas 字段的值更改为 2,保存文件并退出,这将导致生成的 Portfolio 部署更新为具有两个 pod:
portfolio:
replicas: 2
autoscale: false
maxReplicas: 10
cpuThreshold: 75
image:
repository: ibmstocktrader/portfolio
tag: 1.0.0
url: http://{{ .Release.Name }}-portfolio- service:9080/portfolio
注意
除了硬编码副本数量外,您还可以启用 autoscale: true 来启用 HPA,如果达到阈值,Kubernetes 将增加 pod 的数量,当活动减少时将缩减。
升级微服务
您可能还希望对已部署的 Stock Trader 实例进行的一项更改是升级到给定微服务的新版本。例如,如果开发人员构建并推送了 Account 微服务的新版本到您的 Docker 镜像仓库,您将使用操作员指向新版本。同样,您不会直接编辑 Account 部署,而是编辑 Stock Trader CR yaml 文件,然后操作员会代表您对 Account 部署进行更改。
如果你之前使用的是为账户微服务提供的1.0.0标签的镜像,并且你想升级到1.0.1版本,你将使用之前用于扩展的方法,但这次,当你将 CR yaml 文件导入到你的nano编辑器时,你需要将版本号改为1.0.1。当你保存CR yaml文件并退出编辑器后,操作员将更新账户部署以使用新的tag image。这将导致一个新的账户 pod 使用1.0.1标签启动,一旦它通过就绪检查,原始的1.0.0级别的 pod 将被终止(Kubernetes 称这为滚动升级,因为它避免了任何应用程序版本不可用的停机时间):
account:
enabled: true
replicas: 1
autoscale: false
maxReplicas: 10
cpuThreshold: 75
image:
repository: ibmstocktrader/account
tag: 1.0.1
url: http://{{ .Release.Name }}-account- service:9080/account
通过始终对整个StockTrader CR yaml文件采取行动,而不是担心每个微服务的生成 yaml 文件,操作员使你专注于业务应用层面,同时仍然给你提供独立版本化各个部分的灵活性。
执行问题确定
对于应用程序,你经常需要做的一件事是,当某些事情没有按预期工作时要尝试找出原因。这个过程被称为问题确定(PD),或者在事后分析以找出导致失败的原因时,有时被称为根本原因分析(RCA)。再次强调,操作员可以在这里帮助你,例如,通过允许你在每个微服务托管的 Open Liberty 容器中开启额外的跟踪。
如前所述,你编辑 CR yaml 文件以实现这种更改。CR yaml 文件的global部分有一个traceSpec字段,你可以编辑它以提供所需的跟踪规范。例如,如果你想为代理微服务开启细粒度跟踪,你将把traceSpec字段设置为代理微服务的完全限定类名,即com.ibm.hybrid.cloud.sample.stocktrader.broker.BrokerService,并将其设置为fine。请注意,你可能仍然希望保留其他所有内容的info级别跟踪,因此你将使用冒号来分隔跟踪规范的两个部分。
为了帮助进行 PD(问题确定),你可能还想开启 JSON 日志记录。这会导致日志以可以被工具如ElasticSearch消费的格式输出,这样你就可以轻松地在一个单一的联邦日志仪表板,如Kibana中过滤来自各个微服务 pod 的日志。
注意
ElasticSearch、LogStash 和 Kibana 的组合通常被称为 ELK Stack;有时,FluentD 会替代 LogStash,那么组合就被称为 EFK;有关使用 OperatorHub 操作员在您的集群中设置 ELK/EFK 的详细信息,请参阅 docs.openshift.com/container-platform/4.6/logging/cluster-logging-deploying.html。请注意,还有其他更多企业级日志分析工具,例如 LogDNA 或 Instana。
让我们看看一个 Kibana 仪表板:

图 9.21 – 为 IBM 股票交易微服务过滤的联邦可观察性仪表板
在 图 9.21 中,我们看到来自构成 IBM 股票交易应用程序的各个微服务的日志消息,这些消息根据每个 JSON 日志消息发送的时间戳进行交错。能够在单个仪表板中看到所有来自云原生应用程序中所有微服务的日志,而不是必须分别查看每个微服务的输出,这极大地增强了问题确定体验。
JSON 日志的另一个好处是它让您能够控制哪些源将它们的日志发送到 Kubernetes 集群的日志分析工具。除了 HTTPS 访问日志或审计记录等选择之外,还有一个选择是将跟踪记录发送到那里,就像我们刚才讨论的 traceSpec 设置一样(否则,您将不得不使用 kubectl cp 将 trace.log 文件从 pod 复制出来以调查跟踪日志,或者将您自己的 持久卷 (PV) 挂载到容器的 /logs 位置,跟踪日志就会放在那里):
global:
auth: basic
healthCheck: true
ingress: false
istio: false
istioNamespace: mesh
route: true
traceSpec: "com.ibm.hybrid.cloud.sample.stocktrader .broker.BrokerService=fine:*=info"
jsonLogging: true
disableLogFiles: false
monitoring: true
specifyCerts: false
您可以使用操作员执行许多其他日常 2 运维操作。但这一点应该足以说明操作员是控制您应用程序所有配置的,因此当需要时,它被用来实施任何更改。
摘要
我们现在已经探讨了拥有一个操作员帮助您在 Kubernetes 集群中部署复合应用程序以及日常 2 运维的一些好处。虽然在不使用操作员的情况下部署特定的微服务是可能的,但有一个操作员引导您就像有一个副驾驶在适当的时候建议好的默认值,以确保您的应用程序以最佳配置设置部署。在部署后阶段拥有一个操作员,帮助您进行如扩展、升级和问题确定等日常 2 运维操作,确保您在生产使用中维护应用程序时拥有最佳体验。
我们现在已经涵盖了所有核心的 MicroProfile 特性,并在运行在 Kubernetes 平台(如 OCP)的基于微服务的实际应用中展示了它们的使用。展望未来,接下来的章节将涵盖一些辅助的 MicroProfile 特性(例如反应式消息传递),并展望 MicroProfile 在当前 4.x 状态之外的未来。
第四部分:MicroProfile 独立规范和未来
在本节中,你将了解一些 MicroProfile 的 独立 技术,例如上下文传播、反应式消息 API、基于 Java 的 GraphQL 规范,以及长时间运行操作(LRAs)。最后,你将了解未来 MicroProfile 的期望以及如何成为社区的一部分。
本节包含以下章节:
-
第十章,反应式云原生应用
-
第十一章,MicroProfile GraphQL
-
第十二章,MicroProfile LRA 和 MicroProfile 的未来
第十章:响应式云原生应用程序
到目前为止,我们主要讨论了采用具有明确定义的输入和输出的命令式编程的传统云原生应用程序。命令式编程是最古老的编程范式。使用这种范式的应用程序是通过一个明确定义的指令序列构建的,这使得它更容易理解。其架构要求连接服务是预定义的。
然而,有时,云原生应用程序不知道它应该调用哪些服务。它的目的可能只是发送或接收消息或事件,保持响应性和反应性。因此,命令式编程不再适用于这些类型的应用程序。在这种情况下,你需要依赖响应式编程并使用事件驱动架构来实现响应式、响应性和消息驱动的应用程序。我们将在本章中讨论响应式云原生应用程序。
首先,你将学习命令式和响应式应用程序之间的区别。然后我们将讨论如何使用MicroProfile Reactive Messaging 2.0创建响应式云原生应用程序。
我们将涵盖以下主题:
-
区分命令式和响应式应用程序
-
使用MicroProfile Context Propagation来改进异步编程
-
使用MicroProfile Reactive Messaging开发响应式云原生应用程序
为了完全理解本章,你应该具备 Java 多线程、CompletableFuture类和 Java 8 的CompletionStage接口的知识。
到本章结束时,你应该能够理解什么是响应式云原生应用程序,如何创建避免阻塞 I/O 问题的响应式云原生应用程序,以及如何利用像 Apache Kafka 这样的消息库。
区分命令式和响应式应用程序
在开发命令式应用程序时,应用程序开发者定义如何执行任务。你可能一开始设计一个同步应用程序。然而,为了处理重负载和提高性能,你可能会考虑从同步编程切换到异步编程,通过并行执行多个任务来加速。在使用同步编程时,一旦遇到阻塞 I/O,线程必须等待,并且在该线程上无法执行其他任务。然而,在异步编程的情况下,如果有一个线程被阻塞,可以调度多个线程来执行其他任务。
异步编程可以调度多个线程,但它并不能解决阻塞 I/O 问题。如果有阻塞,最终应用程序将消耗所有线程。因此,应用程序将耗尽资源。命令式编程的一个特点是,一个应用程序需要知道要与之交互的服务。在某些情况下,它可能不知道也不关心下游服务。因此,命令式编程不适用于这种情况。在这里,反应式编程就派上用场了。
反应式编程是一种关注数据流和变化传播的范式。这种范式用于构建一个消息驱动、弹性且响应式的云原生应用程序。
反应式应用程序采用了反应式宣言的设计原则。反应式宣言(www.reactivemanifesto.org/)概述了以下四个特性:
-
响应性:应用程序在所有条件下都能及时响应。
-
弹性:系统在各种负载下保持响应性,可以根据需求进行扩展或缩减。
-
弹性:系统在所有情况下都具有弹性。
-
消息驱动:该系统依赖于异步消息作为组件之间的通信渠道。
反应式应用程序使用异步编程来实现时间解耦。正如我们之前提到的,异步编程涉及调度更多线程。每个线程通常需要一个java.util.concurrent.ForkJoinPool类来创建新线程,新调度线程不会关联任何上下文。因此,为了在不同的新线程上继续一个未完成的任务,你需要从先前的线程将一些上下文推送到新线程以继续任务执行。MicroProfile 上下文传播可以用来实现这一点,我们将在下一节中讨论。
使用 MicroProfile 上下文传播来管理上下文
MicroProfile 上下文传播(download.eclipse.org/microprofile/microprofile-context-propagation-1.2/)定义了一种从当前线程传播上下文到新线程的机制。上下文类型包括以下几种:
-
java:comp、java:module和java:app。 -
SessionScoped和ConversationScoped,在新工作单元中(如新的CompeletionStage)仍然有效。 -
安全性:这包括与当前线程关联的凭证。
-
事务:这是与当前线程关联的活跃事务作用域。通常不期望传播此上下文,而是清除它。
除了上述上下文之外,如果需要,应用程序可以引入自定义上下文。
为了传播上述上下文,本规范引入了两个接口:
-
ManagedExecutor:此接口提供了一种异步执行机制,用于定义线程上下文的传播。 -
ThreadContext:此接口允许更精细地控制线程上下文的捕获和传播。您可以将此接口与特定的函数关联。
在以下小节中,我们将简要讨论如何使用上述两个接口将当前线程关联的上下文传播到各种工作单元,例如CompletionStage、CompletableFuture、Function和Runnable。
使用ManagedExecutor传播上下文
ManagedExecutor与其他已知执行器(如ForkJoinPool)不同,后者没有传播上下文的设施。要使用ManagedExecutor传播上下文,您需要创建一个ManagedExecutor实例,这可以通过构建器模式实现:
ManagedExecutor executor = ManagedExecutor.builder()
.cleared(ThreadContext.TRANSACTION)
.propagated(ThreadContext.ALL_REMAINING)
.maxAsync(10)
.build();
上述代码片段用于创建一个ManagedExecutor的执行器对象,该对象清除Transaction上下文并传播所有其他剩余的上下文。此执行器支持最多10个并发执行。然后,您可以调用执行器的一些方法来创建一个CompletableFuture对象,用于异步编程,如下所示:
CompletableFuture<Long> stage1 = executor.newIncompleteFuture();
stage1.thenApply(function1).thenApply(function2);
stage1.completeAsync(supplier);
上述代码片段演示了使用executor对象创建一个不完整的CompletableFuture stage1,然后执行function1。function1完成后,将执行function2。最后,将使用给定的supplier函数完成未来的stage1。
除了ManagedExecutor之外,另一种传播上下文的方式是使用ThreadContext接口。让我们在下一节中更详细地讨论它。
使用ThreadContext传播上下文
ThreadContext提供了对捕获和恢复上下文的精细控制。要使用ThreadContext,您需要创建一个ThreadContext实例,该实例可以通过以下方式通过构建器模式创建:
ThreadContextthreadContext = ThreadContext.builder() .propagated(ThreadContext.APPLICATION, ThreadContext .SECURITY).unchanged().cleared(ThreadContext .ALL_REMAINING).build();
上述代码片段创建了一个ThreadContext实例,它从当前线程传播应用程序和安全性上下文,并清除其他上下文。之后,您可以通过调用threadContext.withContextCapture()方法创建一个CompletionStage实例:
CompletionStage stage = threadContext.withContextCapture(someMethod);
stage.thenApply(function1).thenAccept(aConsumer);
在上述代码片段中,function1和aConsumer函数都将从当前线程继承应用程序和安全性上下文。
或者,您可以从一个threadContext对象创建一个上下文函数,然后将此上下文函数提供给CompletableFuture,如下所示:
Function<String, String> aFunction = threadContext.contextualFunction(function1);
CompletableFuture.thenApply(aFunction) .thenAccept(aConsumer);
在前面的代码片段中,当aFunction执行时,运行此aFunction的线程将从其父线程继承应用程序和安全上下文,这意味着该线程将能够执行与创建aFunction对象的线程类似的功能,而aConsumer函数将不会从其父线程继承应用程序和安全上下文。为了使用上下文传播,你需要使 API 对你的应用程序可用。
使 MicroProfile 上下文传播 API 可用
MicroProfile 上下文传播 API JAR 可以为 Maven 和 Gradle 项目提供。如果你创建了一个 Maven 项目,你可以直接将以下内容添加到你的pom.xml文件中:
<dependency>
<groupId>org.eclipse.microprofile.context- propagation</groupId>
<artifactId>microprofile-context-propagation- api</artifactId>
<version>1.2</version>
</dependency>
另外,如果你创建了一个 Gradle 项目,你需要添加以下依赖项:
dependencies {
providedCompile org.eclipse.microprofile.context- propagation: microprofile-context-propagation-api:1.2
}
我们简要讨论了如何捕获和恢复上下文作为异步编程的一部分。如前所述,异步编程并不解决阻塞 I/O 问题,而是通过调度新线程来绕过这个问题。为了解决阻塞 I/O 问题,你需要考虑构建一个响应式应用程序。在下一节中,我们将讨论使用 MicroProfile 响应式消息传递来帮助你构建响应式应用程序。
使用 MicroProfile 响应式消息传递构建响应式应用程序
@Outgoing注解用于发布消息,@Incoming用于消费消息。以下图示说明了消息如何从发布者(方法 A)传输到消费者(方法 B)。消息可以被发送到消息存储,例如 Apache Kafka、MQ 等,然后将被传递到消费者,如方法 B:


图 10.1 – 消息流
在响应式消息传递中,CDI 豆用于产生、处理和消费消息。这些消息可以通过远程代理或 Apache Kafka、MQ 等各种消息传输层进行发送和接收。让我们讨论 MicroProfile 响应式消息传递的一些关键元素:消息、消息确认、通道、消息消费、消息生产、消息处理和Emitter。
消息
消息是包裹在信封中的信息片段。此外,此信息片段可以包括确认逻辑,可以是正面的或负面的。以下是一些产生消息的方法:
-
Message.of(P p): 此方法包装给定的有效负载p,不提供任何确认逻辑。 -
Message.of(P p, Supplier<CompletionStage<Void>> ack): 此方法包装给定的有效负载p并提供ack确认逻辑。 -
Message.of(P p, Supplier<CompletionStage<Void>> ack, Function<Throwable, CompletionSTage<Void>>> nack): 此方法包装给定的有效负载p,提供ack确认逻辑和nack否定确认逻辑。
或者,如果你有一个Message对象,你可以通过获取其有效载荷并可选地提供新的积极或消极确认来从它创建一个新的Message对象,如下所示:
Message<T> newMessage = aMessage.withPayload(payload).withAck(...).withNack(...)
上述代码片段从aMessage创建newMessage,并提供了新的有效载荷、积极确认和消极确认逻辑。
你可能想知道如何执行消息确认和消极确认。我们将在下一节中更详细地讨论它们。
消息确认
所有消息都必须进行积极或消极的确认,可以使用 MicroProfile Reactive Messaging 实现显式或隐式地进行确认。
有两种不同的确认类型:积极确认和消极确认。积极确认表示消息已成功处理,而消极确认表示消息处理失败。
可以通过@Acknowledgment注解显式指定确认。此注解与@Incoming注解一起使用。你可以指定以下三种确认策略之一:
-
Message#ack()用于确认接收到的消息。 -
@Acknowledgment(PRE_PROCESSING):Reactive Messaging 实现,在执行注解方法之前确认消息。
-
@Acknowledgment(POST_PROCESSING):Reactive Messaging 实现,在方法完成且方法不发出数据或发出的数据被确认后确认消息。
以下是一个手动确认的示例。consume()方法通过在msg对象上调用msg.ack()方法手动确认消息消费:
@Incoming("channel-c")
@Acknowledgment(Acknowledgment.Strategy.MANUAL)
public CompletionStage<Void> consume(Message<I> msg) {
System.out.println("Received the message: " + msg.getPayload());
return msg.ack();
}
如果缺少@Acknowledgment注解,你认为应该使用哪种确认策略?这个答案取决于应用@Incoming注解的方法签名。默认的确认策略如下:
-
如果方法参数或返回类型包含
message类型,则默认确认策略为MANUAL。 -
否则,如果方法仅注解了
@Incoming,则默认确认策略为POST_PROCESSING。 -
最后,如果方法同时注解了
@Incoming和@Outgoing,则默认确认策略为PRE_PROCESSING。
现在我们已经涵盖了消息及其确认策略的必要概念。你可能想知道消息将被发送到何处或从何处消费,即其目的地或源。这些被称为通道,我们将在下一节中讨论。
通道
通道是一个表示消息源或目的地的字符串。MicroProfile Reactive Messaging 有两种类型的通道:
-
内部通道:这些通道是应用本地的,允许在消息源和消息目的地之间进行多步处理。
-
外部通道:这些通道连接到远程代理或各种消息传输层,如 Apache Kafka、AMQP 代理或其他消息传递技术。
消息从上游通道流向下游通道,直到达到消费者进行消息消费。接下来,我们将讨论这些消息是如何被消费的。
使用@Incoming进行消息消费
一个带有@Incoming注解的 CDI bean 上的方法可以消费消息。以下示例从channel-a通道消费消息:
@Incoming("channel-a")
CompletionStage<Void> method(Message<I> msg) {
return message.ack();
}
当消息发送到channel-a通道时,将调用此方法。此方法确认接收到的消息。支持的数据消费方法签名如下:
-
Subscriber<Message<I>> consume(); Subscriber<I> consume(); -
SubscriberBuilder<Message<I>, Void> consume(); SubscriberBuilder<I, Void> consume(); -
void consum(I payload); -
CompletionStage<Void> consume(Message<I> msg); CompletionStage<?> consume(I payload);
在上述列表中,I是传入的有效负载类型。
接收消息的另一种方式是注入org.reactivestreams.Publisher或org.eclipse.microprofile.reactive.streams.operators.PublisherBuilder,并使用@Channel注解,如下所示:
@Inject
@Channel("channel-d")
private Publisher<String> publisher;
上述代码片段意味着一个publisher实例将被连接到channel-d通道。然后消费者可以直接从发布者接收消息。
我们已经讨论了消息流和消息消费。接下来,我们将看看消息是如何生成的。
使用@Outgoing进行消息生产
带有@Outgoing注解的 CDI bean 上的方法是一个消息生产者。以下代码片段演示了消息生产:
@Outgoing("channel-b")
public Message<Integer> publish() {
return Message.of(123);
}
在上述代码片段中,对于每个消费者请求都会调用publish()方法,并将123消息发布到channel-b通道。每个应用程序中只能有一个发布者使用指定通道名的@Outgoing,这意味着只能有一个发布者可以向特定通道发布消息。否则,在应用程序部署期间将发生错误。消息发布到通道后,消费者可以从中消费消息。支持的数据生产方法签名如下:
-
Publisher<Message<O>> produce(); Publisher <O> produce(); -
PublisherBuilder<Message<O>> produce (); Publisher Builder<O> produce(); -
Message<O> produce(); O produce(); -
CompletionStage<Message<O>> produce(); CompletionStage<O> produce();
在上述列表中,O是传出有效负载类类型。
一个方法可以作为消息消费者和消息生产者。这种类型的方法被称为消息处理器。
使用@Incoming和@Outgoing进行消息处理
消息处理器既是消息生产者也是消费者,这意味着它具有@Incoming和@Outgoing注解。让我们看看这个方法:
@Incoming("channel-a")
@Outgoing("channel-b")
public Message<Integer> process(Message<Integer> message) {
return message.withPayload(message.getPayload() + 100);
}
process()方法从channel-a通道接收整数消息,然后加上100。之后,它将新的整数发布到channel-b通道。处理数据支持的方法签名如下:
-
Processor<Message<I>, Message<O>> process(); Processor<I, O> process(); -
ProcessorBuilder<Message<I>, Message<O>> process(); ProcessorBuilder<I, O> process(); -
PublisherBuilder<Message<O>> process(Message<I> msg); PublisherBuilder<O> process(I payload); -
PublisherBuilder<Message<O>> process(PublisherBuilder<Message<I>> publisherBuilder); PublisherBuilder<O> process(PublisherBuilder<I> publisherBuilder); -
Publisher<Message<O>> method(Publisher<Message<I>> publisher); Publisher<O> method(Publisher<I> publisher); -
Message<O> process(Message<I> msg); O process(I payload); -
CompletionStage<Message<O>> process(Message<I> msg); CompletionStage<O> process(I payload);
在前面的列表中,I是传入负载类类型,O是传出负载类类型。
到目前为止,消息消费和生产是 CDI 豆上的方法。这些 CDI 豆可以是Dependent或ApplicationScoped。我们已经在第四章**,开发云原生应用中介绍了 CDI。
我们已经介绍了通过 CDI 豆上的方法进行消息发布和消息消费,当你的应用程序启动并运行时,这些方法将由 Reactive Messaging 实现自动触发。你可能想知道,如果你想在端点被触发时发布一些消息,应该怎么做。我们将在下一节讨论如何做到这一点。
使用 Emitter 发布消息
为了从 JAX-RS 资源发布消息,你可以注入一个Emitter对象,然后调用send()方法。让我们看看以下示例:
@Inject @Channel("channel-c")
private Emitter<String> emitter;
public void publishMessage() {
emitter.send("m");
emitter.send("n");
emitter.complete();
}
在上述代码片段中,首先你可以注入一个带有目标通道的Emitter。然后,你可以通过调用emitter.send()方法发送消息。本例直接发送一个消息负载。你可以通过包装负载来发送消息,具体细节如下:
@Inject @Channel("channel-e")
private Emitter<String> emitter;
public void publishMessage() {
emitter.send(Message.of("m");
emitter.send(Message.of("n");
emitter.send(Message.of("q")
}
通常,消息发布的速度可能不会与消息消费的速度相同。使用Emitter发布消息时,@OnOverflow注解用于处理反压。以下是一个示例,演示如何使用缓冲策略来处理反压:
@Inject @Channel("channel-d")
@OnOverflow(value=OnOverflow.Strategy.BUFFER, bufferSize=100)
private Emitter<String> emitter;
public void publishMessage() {
emitter.send("message1");
emitter.send("message2");
emitter.complete();
}
在前面的代码片段中,一个Emitter对象emitter使用容量为100个元素的缓冲区反压策略连接到channel-d。emitter对象发送了两个消息并完成了它们。@OnOverflow注解支持以下配置,如下表所示:
表 10.1 – 反压策略
我们已经学习了如何使用@Outgoing和@Incoming注解进行消息的生产和消费。MicroProfile Reactive Messaging 通过反应式消息连接器将出站和入站通道连接到外部技术,如 Apache Kafka、WebSocket、AMQP、JMS 和 MQTT。连接是通过反应式消息连接器实现的。我们将在下一节详细讨论连接器。
使用连接器桥接到外部消息技术
连接器可以作为发布者、消费者或处理器。它是一个 CDI bean,实现了 MicroProfile Reactive Messaging 的两个接口IncomingConnectorFactory和OutgoingConnectorFactory,分别用于接收消息和分发消息。Reactive Messaging 实现为支持的消息技术(如 Kafka、MQTT、MQ 等)提供开箱即用的连接器。但是,如果您需要的连接器未由实现者提供,您可以自己创建一个连接器。以下是一个连接到 Apache Kafka 的连接器示例:
@ApplicationScoped
@Connector("my.kafka")
public class KafkaConnector implements IncomingConnectorFactory, OutgoingConnectorFactory {
// ...
}
一旦定义了连接器,接下来显示的进一步配置是必需的,以便将您的云原生应用程序中的通道与连接器桥接的外部消息技术相匹配。在以下配置中,channel-name必须与@Incoming或@Outgoing注解中的值相匹配,而attribute可以是任何类型的字符串:
-
mp.messaging.incoming.[channel-name].[attribute]: 该属性用于将带有@Incoming注解的通道映射到由相应消息技术提供的外部目标。 -
mp.messaging.outgoing.[channel-name].[attribute]: 这个属性用于将带有@Outgoing注解的通道映射到由相应消息技术提供的外部目标。 -
mp.messaging.connector.[connector-name].[attribute]: 这个属性用于指定连接器的详细信息。
如果您的云原生应用程序连接到 Apache Kafka,您可能需要为以下消费者方法提供以下配置:
@Incoming("order")
CompletionStage<Void> method(Message<I> msg) {
return message.ack();
}
在以下配置中,mp.messaging.incoming.order.connector属性指定了连接器名称为liberty-kafka,然后使用mp.messaging.connector.liberty-kafkabootstrap.servers属性进一步指定该连接器的配置。然后通过mp.messaging.incoming.order.topic属性将topic-order Kafka 主题映射到通道order:
mp.messaging.incoming.order.connector=liberty-kafka
mp.messaging.connector.liberty-kafka.bootstrap. servers=localhost:9092
mp.messaging.incoming.order.topic=topic-order
我们已经介绍了 MicroProfile Reactive Messaging。现在让我们将其全部整合起来。如果您需要创建一个从事件流系统(如 Apache Kafka)消费消息的消费者,您只需创建一个 CDI 实例,并编写一个带有 @Incoming 注解的方法来连接特定的通道。同样,如果您需要发送消息,您将需要创建一个 CDI 实例,并编写一个带有 @Outging 注解的方法来连接到生产者通道。最后,您配置通道,如前所述,以声明通道连接到 Apache Kafka 连接器。Open Liberty 提供了 liberty-kafka Kafka 连接器。本 Open Liberty 指南 (openliberty.io/guides/microprofile-reactive-messaging.html) 展示了如何创建 Java 微服务。
要使用 MicroProfile Reactive Messaging 的 API,您需要指定如后所述的 Maven 或 Gradle 依赖项。
使 MicroProfile Reactive Messaging API 可用
MicroProfile Reactive Messaging API JAR 可以用于 Maven 和 Gradle 项目。如果您创建了一个 Maven 项目,您可以直接将以下内容添加到您的 pom.xml 文件中:
<dependency>
<groupId>
org.eclipse.microprofile.reactive.messaging
</groupId>
<artifactId>
microprofile-reactive-messaging-api
</artifactId>
<version>2.0</version>
</dependency>
另外,如果您创建了一个 Gradle 项目,您需要添加以下依赖项:
dependencies {
providedCompile org.eclipse.microprofile.reactive .messaging: microprofile-reactive-messaging-api:2.0
}
您现在已经学会了如何在需要与消息传递技术交互并需要消息驱动架构时创建响应式云原生应用程序。
摘要
在本章中,我们学习了命令式和响应式应用程序之间的区别。我们简要讨论了如何使用 MicroProfile Context Propagation 来传播异步编程的上下文,然后介绍了 MicroProfile Reactive Messaging 概念,讨论了如何使用 Reactive Messaging 来创建一个响应式云原生应用程序。通过本章,你现在将能够将构建的应用程序与您选择的如 Apache Kafka 之类的消息传递技术连接起来。你现在也理解了,每当您需要创建一个消息驱动应用程序时,您应该考虑使用 MicroProfile Reactive Messaging。
在下一章中,我们将介绍 MicroProfile GraphQL,学习如何在您的云原生应用程序中使用 GraphQL 来提高性能,如果您需要频繁执行查询。
![表 10.1 – 反压策略
![img/Table_01.png]
第十一章:MicroProfile GraphQL
GraphQL 是一种分布式查询语言,它解决了 REpresentational State Transfer (REST) 的一些缺点。特别是,GraphQL 解决了 过度获取(接收比客户端预期更多的数据)和 不足获取(要求客户端发出多个请求以获取所需的数据)的概念。GraphQL 应用程序利用一个模式文件,向客户端展示其可用的查询和突变,以及它可以访问和操作的对象。
GraphQL 的易用性和健壮性解释了为什么它的受欢迎程度在增长,尤其是在云原生应用中。MicroProfile GraphQL (MP GraphQL) 使得创建基于 GraphQL 的应用变得简单。
在本章中,我们将涵盖以下主要主题:
-
理解 GraphQL 基础知识及其适用场景
-
使用 MP GraphQL 构建服务
-
使用客户端 应用程序编程接口 (APIs) 消费 GraphQL 服务
到本章结束时,您将了解 GraphQL 是什么以及何时适合使用它,并且您将能够构建自己的 GraphQL 应用程序,准备在开源、云就绪服务器(如 Open Liberty)上部署。
技术要求
要构建和运行本章中提到的示例,您需要一个装有以下软件的 Mac 或 PC(Windows 或 Linux):
-
Java 开发工具包 (JDK) 版本 8 或更高 (
ibm.biz/GetSemeru) -
Apache Maven (
maven.apache.org/) -
Git 客户端 (
git-scm.com/)
本章中使用的所有源代码都可以在 GitHub 上找到,网址为 github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/tree/main/Chapter11。
一旦您已经克隆了 GitHub 仓库,您可以通过进入 Chapter11 目录并在命令行中执行以下命令来启动 Open Liberty 服务器,这些代码示例将在其中执行:
mvn clean package liberty:run
然后,您可以通过按 Ctrl + C 在同一个命令窗口中停止服务器。
现在我们已经处理好了先决条件,让我们从学习 GraphQL 的基础知识开始。
理解 GraphQL 基础知识及其适用场景
与 REST 一样,GraphQL 是通过基于 Web 的传输访问和修改远程数据的一种方式。它使用一个公开可见的模式,允许客户端确切地知道它可以查询哪些实体,哪些字段可以修改,等等。这与 OpenAPI 描述 RESTful API 的方式相似。该模式充当客户端和服务的合同。GraphQL 严格强制执行模式,防止客户端访问或修改未在该模式中定义的实体或字段。这种严格性为客户端和服务端开发者提供了很多自由,我们将在本节稍后介绍。
GraphQL 支持以下操作:
-
REST 中的
GET请求。 -
变更:变更用于修改数据——即创建、更新和/或删除它。
-
订阅:订阅用于使客户端能够接收特定事件的通知,例如当某个特定实体被创建或某个字段低于某个特定阈值,或者甚至是无关的事件。
与 REST 不同,在 REST 中,API 的不同部分分散在多个超文本传输协议(HTTP)端点,GraphQL 应用程序通常使用单个 HTTP 端点,并且操作嵌入在 HTTP 请求的主体中。
GraphQL 操作和模式使用它们自己的语法,但响应以JavaScript 对象表示法(JSON)格式。这允许 GraphQL 服务和客户端用任何语言编写。虽然我们计划介绍如何在 Java 中创建服务,但目前也可以用 JavaScript、Python、Go、Haskell、Perl、Ruby、Scala 以及许多其他语言编写服务和客户端应用程序。
模式定义了服务可访问的实体类型以及可以执行的操作。内置或原始的 GraphQL 类型被称为标量。任何服务都可以自由定义自己的标量类型,但 GraphQL 规范指出,所有服务都必须至少使用以下五个标准标量:
-
int—32 位有符号整数 -
float—有符号双精度浮点数 -
string—使用Unicode 转换格式-8(UTF-8)编码的字符序列 -
boolean—true或false -
ID—一个旨在作为实体的唯一标识符(ID)的字符串;它不打算供人类阅读
GraphQL 对象可以由标量或其他对象组成。每个操作必须明确指定它希望在响应中查看的所有字段。对于复杂类型(包含其他类型或标量的类型),这可能意味着指定多层深度的字段。
要求客户端在查询中指定所有字段,确保在向现有对象添加新字段时保持向后兼容性。如果对象上出现新字段,而客户端现有的查询没有指定它,那么客户端就不会感到意外!
要求客户端指定他们感兴趣的所有字段的好处之一是避免了过度获取。过度获取发生在通过网络发送的数据多于所需的情况。REST 中过度获取的一个常见例子是天气数据。如果你向多个天气网站发出 RESTful 请求,以检查特定位置的当前条件,你会看到大量的信息,但当你只想知道室外温度以及是否下雨时,大部分数据都是未使用的。
通过将查询作为 HTTP 请求的有效负载发送,GraphQL 还避免了不足获取。正如你可能猜到的,不足获取发生在返回的数据不足的情况下。使用天气示例,假设你还想了解其他城市朋友家的温度。你必须为每个位置向天气网站发出类似的 RESTful 请求。但在 GraphQL 中,你可以在单个 HTTP 请求中发出多个查询,使你能够通过单次往返服务器获取所需的确切数据,从而使其快速高效!
查询和突变有自己的语法,尽管它与 JSON 和其他查询语言相似。通常,这些操作以query或mutation开头,然后是操作的标签,然后在大括号内指定要调用的查询或突变,以及括号内的任何参数。然后,你会在大括号内添加你感兴趣的字段。我们将在本章后面看到一些查询和突变的示例。
GraphQL 还允许503(服务不可用)错误。有些数据总比没有好,对吧?
由于模式对客户端是公开的,各种工具可以内省模式,使用户能够构建查询和突变并在实时中测试它们。我们将稍后讨论的一个这样的工具被称为GraphiQL(github.com/graphql/graphiql)。
虽然 REST 仍然是云中更广泛使用的通信架构,但 GraphQL 正迅速获得人气,因为它解决了 REST 中的许多差距。那么,哪种方法适合你?答案,就像大多数事情一样,是取决于。GraphQL 主要只与 JSON 作为响应类型一起工作;如果你想使用其他数据类型,你可能需要考虑 REST 或另一种方法。如果你的数据本质上是分层的,它可能更适合 REST。
另一个考虑因素是针对/public/*的安全限制,同时限制对其他实体的访问(例如,/private/*)。在 GraphQL 中,如果不将服务拆分为单独的公共和私有服务,这是不可能实现的,这可能不是最佳方案。
类似地,与 GraphQL 一起,HTTP 缓存也更加复杂。由于 REST 使用 URI 路径,客户端和服务器都可以根据使用的路径缓存实体结果。在 GraphQL 中,根据路径进行缓存是可能的,但这将要求客户端将它们的查询作为 HTTP GET 查询参数传递。这可能对客户端来说很麻烦,同时也可能是一个潜在的安全风险,因为代理服务器将能够看到查询参数,并且您可能仍然会因查询的间距和格式问题而遇到缓存问题。幸运的是,大多数 GraphQL 的实现都在服务器端使用查询缓存来减少不必要的重复工作。
那么,您何时会使用 GraphQL 呢?GraphQL 在服务器端(为了过滤结果以获取客户端所需的确切内容)可能会稍微昂贵一些,但这种权衡意味着客户端处理显著减少。因此,如果您有很多客户端或希望优化客户端性能,GraphQL 是一个好的选择。
GraphQL 通常可以减少网络流量,因为它避免了数据获取不足和过度获取。在网络带宽昂贵的环境中,GraphQL 是理想的解决方案。
还应注意的是,没有任何东西阻止您为同一服务同时编写 GraphQL 和 RESTful API。这可能会增加更多的维护工作,但它允许您的客户端进行选择。
现在我们已经了解了什么是 GraphQL 以及何时应该使用它,接下来让我们探讨如何使用 MicroProfile 来构建 GraphQL 应用程序。
使用 MP GraphQL 构建服务
在本节中,我们将学习如何使用 MP GraphQL API 和运行时框架开发 GraphQL 应用程序。我们将涵盖构建查询和突变,以及如何使用一个名为 GraphiQL 的交互式网络工具来调用它们。我们还将涵盖实体和枚举类型。最后,我们将介绍一种减少不必要的服务器端计算并传递部分结果的技术。
大多数针对 Java 的 GraphQL API 都需要您首先编写一个模式,然后围绕它构建 Java 代码。这种方法往往会导致一定程度的双重维护,并且随着您应用的发展,它可能会减慢开发速度。MP GraphQL 使用一个 Hello World 查询服务。
开发查询
与 JAX-RS 一样,MP GraphQL 也是基于注解的。首先需要考虑的注解是 @GraphQLApi。这个注解是一个 Contexts and Dependency Injection (CDI) 的 bean 定义注解,这意味着当您将此注解应用于一个类时,它就变成了一个 CDI bean。这使 CDI 框架能够管理其生命周期并注入依赖项。这个注解对于包含查询或突变方法的类是必需的。
我们接下来要考虑的下一个注解是 @Query。当这个注解应用于方法时,它告诉 MP GraphQL 运行时在模式中创建一个顶级查询。让我们看看一个简单的例子,如下所示:
@GraphQLApi
public class SimpleApi {
@Query
public String hello() {
return "Hello World";
}
}
@GraphQLApi注解告诉运行时管理此 bean 的生命周期,而@Query注解告诉运行时在模式中生成一个无参数的查询,该查询返回一个String标量。如果我们在一个 MP GraphQL 服务器(如 Open Liberty)上运行此示例,然后我们可以通过浏览到http://localhost:9080/ch11/graphql/schema.graphql来查看模式文件。然后,我们会看到类似这样的:
"Query root"
type Query {
hello: String
}
使用http://localhost:9080/ch11/graphql-ui然后输入以下查询字符串:
query hello {
hello
}
然后,点击三角形的播放按钮查看结果。你应该看到类似这样的:

图 11.1 – GraphiQL 中的简单查询
注意,结果是带有标签data的 JSON 对象。查询的结果始终位于data字段下。如果发生错误,将会有一个单独的errors字段,而不是data字段,或者与data字段一起。该字段将包括错误详情。
这是一个很好的开始,你可能会猜到在这个类中可以有多个查询方法,它们可以返回不同的数据,但带有参数的查询要强大得多。在之前的章节中,我们一直在处理股票交易员应用程序。让我们在我们的后续示例中将该应用程序 GraphQL 化。
如果我们想让客户端能够指定查询的参数,我们只需将 Java 方法参数添加到@Query注解的方法中。让我们看看Portfolio服务可能的做法,如下所示:
@GraphQLApi
public class PortfolioGraphQLApi {
@Inject
private PortfolioDatabase portfolioDB;
@Query
@Description("Returns the portfolio of the given owner.")
public Portfolio portfolio(@Name("owner") String owner)
throws UnknownPortfolioException {
return Optional.ofNullable(portfolioDB.getPortfolio (owner)).orElseThrow(() -> new UnknownPortfolioException(owner));
}
//...
}
这里有一些新事物需要考虑。首先,我们注入PortfolioDatabase实例。这不过是一个HashMap的包装器,但它也可以访问真实的 SQL 或 NoSQL 数据库来检索股票投资组合数据。CDI 为我们注入了它。非常感谢!
接下来,portfolio查询方法也应用了@Description注解。这允许我们指定一个人类可读的描述,该描述将出现在生成的模式中,这对于描述查询及其参数的意图很有用。
说到参数,该方法接受一个名为owner的String参数。@Name注解告诉运行时在生成模式时使用哪个名称。
最佳实践
使用@Name注解在参数上以实现可移植性。某些 MP GraphQL 实现可能无法从代码中确定参数名称,并最终使用arg0、arg1等参数名称编写模式。@Name注解确保运行时将在模式中生成指定的参数名称。
在上述代码中还有一点值得注意,那就是我们不是返回一个 string 或其他原始数据类型,而是返回一个 Portfolio 对象。这是我们应用程序中的一个自定义对象。通过这样做,运行时会反射 Portfolio Java 对象,并将其作为模式中的实体生成。它还会生成它引用的任何其他对象。让我们看看从这个代码生成的模式,如下所示:
type Portfolio {
accountID: ID
loyalty: Loyalty
owner: String
stocks: [Stock]
total: Float!
}
"Query root"
type Query {
"Returns the portfolio of the given owner."
portfolio(owner: String): Portfolio
}
type Stock {
type Stock {
commission: Float!
dateOfLastUpdate: String
pricePerShare: Float!
shares: Int!
symbol: String!
total: Float!
}
}
enum Loyalty {
BRONZE
GOLD
SILVER
}
首先,我们看到 Portfolio 类型(实体)及其各种字段及其类型。因此,accountID 字段是一个 string;total 字段是一个 float,感叹号表示该字段的值必须非空;stocks 字段是一个 Stock 对象的数组,方括号表示这是一个 数组。
我们还看到了我们查询的文本描述。查询部分表明,portfolio 查询接受一个名为 owner 的单个 String 参数,并返回一个 Portfolio 对象。
Stock 类型被引入是因为它被 Portfolio 类型所引用。同样,Loyalty Portfolio 类型。GraphQL 中的枚举是从 Java 枚举 生成的,并且行为类似。
让我们再次查看生成此模式的代码,我们会看到 portfolio 方法抛出 UnknownPortfolioException 异常。这个异常由框架处理。当异常被抛出时,框架将向客户端返回一个错误响应。让我们看看当我们查询两个投资组合——一个存在的和一个不存在的——会发生什么,如下所示:

图 11.2 – 多个查询:一个成功,一个失败并抛出预期异常
图 11.2 显示我们可以在同一个请求中发送多个查询。它还显示我们可以接收部分结果。在这种情况下,查询 Emily J 的投资组合详情是成功的,但查询 Andy M 的投资组合详情失败了,因为他的投资组合尚未在数据库中。
现在我们已经基本了解了如何创建查询方法,让我们看看我们如何创建突变。
开发突变
当我们想到 创建、读取、更新和删除 (CRUD) 操作时,查询是 读取 部分,而突变是其他所有操作。尽管如此,查询和突变只是标签——一个 GraphQL 查询当然可以创建、更新或删除实体,而突变可以简单地返回实体的视图,但这不是预期的实践。
最佳实践
查询方法永远不应该操作实体数据。使用查询来返回实体的当前状态,使用突变来更改这些数据。
要创建一个变异方法,你只需将@Mutation注解应用到你的 Java 方法上。在大多数情况下,变异方法将接受参数来指示要进行的更改类型,以及/或者指定要更新或删除的实体。让我们看看我们如何使用变异方法来创建一个Portfolio对象,如下所示:
@GraphQLApi
public class PortfolioGraphQLApi {
//...
@Mutation
public Portfolio createNewPortfolio(@Name("portfolio")
Portfolio portfolio)
throws DuplicatePortfolioOwnerException, UnknownPortfolioException {
portfolioDB.addPortfolio(portfolio);
return portfolio(portfolio.getOwner());
}
这里有几个需要注意的地方。首先,createNewPortfolio方法返回它刚刚创建的Portfolio对象——它实际上调用了我们在上一节中编写的portfolio方法,以确保新的Portfolio对象在数据库中成功创建。变异,就像查询一样,必须始终返回某些内容。不允许有空的变异或查询方法。
建议
如果你真的不想返回任何内容,可以考虑返回一个boolean类型的值来表示变异是否成功完成,或者考虑返回一个int类型的值,表示创建了/更新了/删除了多少实体。
关于此代码的第二个需要注意的地方是它接受一个复杂对象作为参数。这将在模式中生成一些新的条目。让我们看看如下:
input PortfolioInput {
accountID: ID
loyalty: Loyalty
owner: String
stocks: [StockInput]
total: Float!
}
input StockInput {
commission: Float!
shares: Int!
symbol: String
}
这些输入类型与我们生成查询方法模式时看到的类型非常相似。不同之处在于这些类型后面附加了input。GraphQL 区分用于输入的类型和用于输出的类型。这有一个优点,即这意味着客户端可能可以查看他们无法修改或反之亦然的内容。那么,变异在GraphiQL中看起来会是什么样子呢?让我们看看如下:
![图 11.3 – 创建新投资组合的变异]

图 11.3 – 创建新投资组合的变异
图 11.3 展示了如何指定一个复杂的参数,portfolio。语法与 JSON 非常相似,但并不完全一样——注意字段名没有引号。还要注意变异指定了一个返回值,owner——一个有效的查询或变异必须包含至少一个返回值。
关于参数和分页的说明
查询或变异中的参数不需要与底层业务实体相关。你也可以使用参数来指定pageNumber和entriesPerPage,这样客户端就可以根据自己的节奏处理投资组合。
现在我们已经涵盖了查询和变异,让我们更深入地看看实体以及我们如何在 GraphQL 世界中塑造它们!
编写实体
实体都是用于输入或输出的复杂类型(不是标量)。MP GraphQL 运行时会计算所有由根级查询和突变引用的实体,并将它们自动添加到模式中。它将区分用作参数(输入)的实体和用作返回值(输出)的实体。而且,正如我们在上一节中发现的那样,框架还会添加由其他实体引用的实体,这些实体可能不是由根级查询和突变直接引用的。这包括类、枚举和接口。
使用 GraphQL 接口
我们已经介绍了作为实体的基本类和枚举,现在让我们看看接口。与 Java 中的接口一样,GraphQL 接口可以被具体的 GraphQL 类型实现。一个区别是输入类型不能实现接口,这可能会使事情变得复杂。让我们通过一个例子来更好地理解。假设我们想要一个包含账户所有者联系信息的投资组合所有者配置文件。由于一些投资组合账户可能由除所有者之外的人管理,我们可能想要两种不同的配置文件类型——一种用于单人所有者,另一种用于指定管理员的账户。为了满足这个要求,我们可能编写如下代码:
@Interface
public interface OwnerProfile {
String getOwnerId();
String getEmailAddress();
void setEmailAddress(String emailAddress);
}
public class OwnerProfileImpl implements OwnerProfile {
private String ownerId;
private String emailAddress;
// ... public getters / setters
}
public class ManagedOwnerProfileImpl extends
OwnerProfileImpl implements OwnerProfile {
private String managerName;
private String managerEmailAddress;
// ... public getters / setters
}
在前面的代码片段中,我们看到 @Interface 注解应用于 OwnerProfile 接口。这告诉 MP GraphQL 框架将此接口作为模式中的 GraphQL 接口处理。然后,框架将搜索此接口的实现,并将它们也添加到模式中。
接下来,让我们看看 API 类可能的样子,如下所示:
@GraphQLApi
public class ProfileGraphQLApi {
@Inject
OwnerProfileDatabase db;
@Query
public OwnerProfile profile(String ownerId) {
return db.getProfile(ownerId);
}
@Mutation
public boolean addProfile(OwnerProfileImpl profile) throws DuplicatePortfolioOwnerException {
db.addProfile(profile);
return true;
}
@Mutation
public boolean addManagedProfile
(ManagedOwnerProfileImpl profile) throws DuplicatePortfolioOwnerException {
db.addProfile(profile);
return true;
}
}
注意,API 类为创建每种类型的配置文件提供了单独的突变方法。这是 GraphQL 不允许输入类型实现接口的一个不幸副作用——尽管 Java 代码实现了接口,但 GraphQL 代码并没有。这意味着参数不能是接口。另一方面,输出类型没有这种限制,因此我们可以使用一个查询方法来处理两种配置文件类型。这个 API 类与实体接口和类的组合将生成一个如下所示的模式(简化版):
interface OwnerProfile {
emailAddress: String
ownerId: String
}
type ManagedOwnerProfileImpl implements OwnerProfile {
emailAddress: String
managerEmailAddress: String
managerName: String
ownerId: String
}
type OwnerProfileImpl implements OwnerProfile {
emailAddress: String
ownerId: String
}
input ManagedOwnerProfileImplInput {
emailAddress: String
managerEmailAddress: String
managerName: String
ownerId: String
}
input OwnerProfileImplInput {
emailAddress: String
ownerId: String
}
如我们所预期,ManagedOwnerProfileImpl 类型实现了 OwnerProfile 接口。它具有与接口相同的字段,并且还有一些额外的字段。那么,我们如何在查询中访问这些额外的字段呢?魔法发生在查询的第 6 行和第 14 行,如下面的截图所示:

图 11.4 – 使用接口的查询
如 图 11.4 所示,… on ManagedOwnerProfileImpl 代码类似于将接口转换为实现类,然后调用仅在实现类上存在的 getter 方法。注意在输出中,为 Emily J 返回的配置文件类型不是 ManagedOwnerProfileImpl 类型,因此它不包含额外的字段。
就像 Java 一样,接口对于组织和重用实体非常有用。现在,让我们看看我们如何进一步细化实体。
使用实体注解
在 GraphQL 模式中将实体类公开是很常见的,但可能需要重命名一个字段(或排除一个)或使一个字段为只读,或者进行其他修改。这可以通过在实体字段和/或 getter/setter 方法上使用注解来实现。由于 MP GraphQL 与 JSON 绑定(JSON-B)集成,许多 MP GraphQL 特定的注解可以被 JSON-B 注解所替代,以避免注解过载。
我们已经看到了在查询/突变方法中的参数上使用 @Name 注解,但我们也可以在实体字段和 getters/setters 上使用此注解来 重命名 在生成的 GraphQL 模式中字段。与本章中描述的所有注解一样,如果您将注解放在 getter 方法上,它将仅应用于输出类型。如果您将注解放在 setter 方法上,它将仅应用于输入类型。如果您将其放在字段上,它将应用于两者。
下表列出了在向您的 GraphQL 应用程序添加实体时可能非常有用的注解:

表 11.1 – MP GraphQL 实体注解及其 JSON-B 等价物
将这些注解应用于您的实体类型可以使您更好地控制模型类的外部视图,并更好地重用现有类。
外包
假设您有一个具有计算成本高昂的字段的实体——可能需要复杂的数学计算,或者可能需要查询远程数据库等。当客户端对它不感兴趣时计算该字段似乎是浪费的。幸运的是,可以通过 @Source 注解避免昂贵的计算。
例如,假设配置文件服务需要能够检查特定投资组合所有者的忠诚度级别,但该信息位于投资组合数据库中,而不是配置文件数据库中。因此,在这个例子中,想要查看配置文件数据的客户端最终会要求服务器连接到两个不同的数据库以获取结果。我们可以通过仅在客户端请求忠诚度字段时检查投资组合数据库来优化这种情况。我们通过在 ProfileGraphQLApi 类中添加一个 getLoyalty(@Source OwnerProfileImpl profile) 方法来实现这一点,如下所示:
@GraphQLApi
public class ProfileGraphQLApi {
@Inject
PortfolioDatabase portfolioDB;
public Loyalty getLoyalty(@Source OwnerProfileImpl profile) throws UnknownPortfolioException {
String ownerId = profile.getOwnerId();
Portfolio p = portfolioDB.getPortfolio(ownerId);
return p.getLoyalty();
}
// ...
}
这所做的操作是在模式中的OwnerProfileImpl实体中添加了一个新的字段,loyalty。从客户端的角度来看,这个新字段就像任何其他字段一样,但只有当客户端明确请求该字段时,getLoyalty方法才会被调用。这是一种避免在客户端不需要结果数据时支付昂贵操作费用的有用方式。
最佳实践
使用@Source注解进行昂贵的数据获取,以优化服务器端性能。这也使得你能够减少大型查询在服务器上的内存消耗。
如果@Source方法抛出异常,MP GraphQL 框架将为该字段返回一个 null 结果,并将发送错误数据,但将继续发送来自其他字段的数据作为部分结果。
使用 GraphQLException 发送部分结果
我们已经看到了两种方法可以将@Source注解发送出去以外包字段的数据获取器。
通过使用GraphQLException异常,还有一种发送部分结果的方法。这个异常允许你在抛出异常回 MP GraphQL 框架之前包含部分结果。然后框架将尝试将部分结果与错误数据一起发送回客户端。以下是一个示例:
@Mutation
public Collection<Portfolio> createNewPortfolios(
@Name("portfolios") List<Portfolio> newPortfolios)
throws GraphQLException, UnknownPortfolioException {
Tuple<Collection<Portfolio>, Collection<String>> tuple = portfolioDB.addPortfolios(newPortfolios);
if (!tuple.second.isEmpty()) {
// some of the portfolios to be added already exist;
// throw an exception with partial results
throw new GraphQLException(
"The following portfolios already exist and " + "cannot be re-added: " + tuple.second, tuple.first); // here are the partial results
}
return tuple.first;
}
这个变更允许客户端在一次请求中创建多个新的投资组合。如果客户端尝试为已存在的所有者创建投资组合,这将引发异常,但所有其他投资组合仍然会被创建,并将它们的结果以及无法创建的投资组合列表发送回客户端。
在本节中,我们学习了如何使用 MP GraphQL 在 Java 中构建服务器端 GraphQL 应用程序。虽然本节没有具体涉及,但应注意的是,MP GraphQL 与 MicroProfile 的其他功能,如容错和度量,很好地集成。MP GraphQL 1.0 规范已正式发布,并得到开源 Java 服务器(如 Open Liberty、Quarkus 和 WildFly)的支持。规范的未来版本将添加新的功能,例如支持订阅、定义自定义标量、联合类型、内置分页支持和客户端 API。
在本节中,我们学习了如何使用 MicroProfile API 编写简单和高级的 GraphQL 服务。到目前为止,我们只使用GraphiQL工具调用了这些服务。在下一节中,我们将学习如何使用 Java API 调用这些服务。
使用客户端 API 消费 GraphQL 服务
客户端 API 目前还不是 MP GraphQL 规范的官方部分。在撰写本文时,这些 API 仍在SmallRye GraphQL项目中开发,目的是将它们正式纳入规范。
免责声明
由于这些 API 尚未官方发布,它们可能会发生变化。本节中的信息适用于 SmallRye GraphQL 版本 1.2.3 客户端 API。当这些 API 添加到官方 MP GraphQL 规范中时,它们可能会发生变化,因此请查阅github.com/eclipse/microprofile-graphql的官方文档以了解任何更改。
MP GraphQL 项目旨在支持两种客户端 API 的版本。类似于 JAX-RS 客户端和 MicroProfile REST 客户端(见第四章,开发云原生应用),存在一个动态客户端API 和一个类型安全的客户端API。与 JAX-RS 客户端一样,动态客户端允许用户指定请求的细节,而类型安全的客户端允许用户构建一个接口来模拟远程服务,并在需要发送新请求时简单地调用它。
这两个客户端 API 在 GitHub 仓库的集成测试中得到了演示,仓库地址为github.com/PacktPublishing/Practical-Cloud-Native-Java-Development-with-MicroProfile/tree/main/Chapter11/src/test/java/com/packt/microprofile/ch11/client。它们测试了一个allProfiles查询,该查询返回服务器所知的所有配置文件。在我们的例子中,我们创建了两个用于测试的配置文件。让我们首先看看动态客户端。
动态客户端
动态客户端通过构建一个DynamicGraphQLClient实例,然后将其作为Request或Document对象传递来工作。Request对象通常包含一个包含您要执行的查询或变异的纯文本字符串,而Document对象必须通过编程方式构建。让我们首先看看Request方法,如下所示:
@Test
public void testAllProfilesWithStringDocument()
throws Exception {
verify(() -> executeSync(
new RequestImpl("query allProfiles {"
+" allProfiles {"
+" ownerId, emailAddress"
+" }"
+"}")));
}
private Response executeSync(Request req) {
try (DynamicGraphQLClient client = newClient()) {
return client.executeSync(req);
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private DynamicGraphQLClient newClient() {
return DynamicGraphQLClientBuilder.newBuilder()
.url(URL)
.build();
}
private void verify(Supplier<Response> responseSupplier) throws Exception {
Response resp = responseSupplier.get();
JsonObject data = resp.getData();
assertNotNull(data);
JsonArray allProfiles = data.getJsonArray("allProfiles");
assertNotNull(allProfiles);
JsonObject emily = allProfiles.getJsonObject(0);
assertNotNull(emily);
assertEquals("Emily J", emily.getString("ownerId"));
assertEquals("emilyj@notmyrealaddress.com", emily.getString("emailAddress"));
JsonObject andy = allProfiles.getJsonObject(1);
assertNotNull(andy);
assertEquals("Andy M", andy.getString("ownerId"));
assertEquals("andym@notmyrealaddress.com", andy.getString("emailAddress"));
}
在这个代码片段中,我们使用建造者模式创建一个新的DynamicGraphQLClient实例,并指定http://localhost:9080/ch11/graphql。然后,我们在该客户端实例上调用executeSync方法,将带有我们的查询的纯文本字符串作为RequestImpl传递。这返回一个Response对象,我们可以从中提取包含 GraphQL 结果的 JSON-P JsonObject实例。
我们还可以用更类似于建造者模式的方式来编写这个,其中我们使用 Java 代码将查询的每一部分构建成一个Document对象。以下是一个例子:
@Test
public void testAllProfilesWithConstructedDocument() throws Exception {
Field query = field("allProfiles");
query.setFields(Arrays.asList(field("ownerId"), field("emailAddress")));
verify(() -> executeSync(document(operation
(OperationType.QUERY, "allProfiles",query))));
}
在此代码中,我们为查询本身创建了一个 allProfiles 字段,然后创建了我们所感兴趣的子字段:ownerId 和 emailAddress。然后,我们从查询字段构造一个 Operation,并从 Operation 构造一个 Document 对象。然后,我们将 Document 对象传递给 executeSync 方法以调用查询,我们的 Response 对象与之前的代码片段相同。这可能会比简单地用纯文本编写查询看起来更复杂,但优点是你可以根据情况使用这种方法构建更复杂的查询——例如,你可以根据某些情况在查询中程序化地请求额外的字段。
动态客户端是一种编写可能需要根据调用时间进行更改的 GraphQL 查询和变异的好方法。对于预期查询相对静态的情况,类型安全的客户端更为合适。接下来,让我们看看它是什么样子。
类型安全客户端
类型安全的客户端借鉴了 MicroProfile REST 客户端的设计。它使用注解和接口来表示远程服务,然后使用构建器模式或 CDI 注入来创建客户端实例。让我们看看我们如何在集成测试用例中实现这一点。首先,我们需要表示实际的响应对象,即 OwnerProfile 对象,如下所示:
class OwnerProfile {
String ownerId;
String emailAddress;
// public getters/setters
}
这与服务器端的相同类非常相似。现在,让我们看看客户端接口的样子,如下所示:
@GraphQLClientApi
interface ProfileApi {
List<OwnerProfile> allProfiles();
}
此接口使用 @GraphQLClientApi 注解来表示它代表远程服务。因为我们只对 allProfiles 查询感兴趣,所以我们只有一个方法:allProfiles。我们可以添加其他方法来匹配其他查询或变异。由于这是一个单查询方法,我们不需要用 @Query 注解它,但如果我们想包括变异,那么我们就需要使用 @Query 和 @Mutation 注解来指定哪些方法是哪些。
现在,让我们通过构建代码和执行来整合所有这些内容,如下所示:
@Test
public void testAllProfiles() throws Exception {
ProfileApi api = TypesafeGraphQLClientBuilder .newBuilder()
.endpoint(URL)
.build(ProfileApi.class);
List<OwnerProfile> allProfiles = api.allProfiles();
assertNotNull(allProfiles);
assertEquals(2, allProfiles.size());
assertEquals("Emily J", allProfiles.get(0).getOwnerId());
assertEquals("emilyj@notmyrealaddress.com", allProfiles.get(0).getEmailAddress());
assertEquals("Andy M",allProfiles.get(1).getOwnerId());
assertEquals("andym@notmyrealaddress.com", allProfiles.get(1).getEmailAddress());
}
我们使用 TypesafeGraphQLClientBuilder 构建一个 ProfileApi 客户端接口的实例。然后,一旦我们在该接口上调用一个方法,查询就会被发送到服务器,并返回与上一节中动态客户端返回的相同的数据列表。
这两种客户端选项都为调用远程 GraphQL 服务提供了大量的功能和灵活性,即使这些服务不是用 MicroProfile 或 Java 构建的。
摘要
在本章中,我们学习了 GraphQL 以及它是如何填补 REST 中的一些空白的。我们还学习了如何使用 MP GraphQL 创建和消费 GraphQL 服务,而无需额外维护一个与 Java 代码并存的模式。我们了解到,通过在 API 类上应用注解,我们可以构建查询和突变,并通过添加描述、参数、格式化等功能来丰富它们。通过外包,我们学会了在不需要时避免执行昂贵的操作。我们还学习了在发生异常时如何发送部分结果。我们还了解到,有一些有用的工具,如 GraphiQL,可以简化测试。尽管客户端 API 在规范中并未得到完全支持,但我们已经能够查看两个不同的客户端,并看到我们如何可以使用它们进行集成测试或消费 GraphQL 服务。
结合本章所学,再加上我们可用的工具,我们现在能够开发和测试云原生 GraphQL 应用,或者将 GraphQL 前端应用到我们的现有应用中。我们的微服务现在可以避免过度获取和不足获取,减少网络流量,并为客户提供他们真正需要的内容。
在下一章中,我们将探讨 MicroProfile 的未来,并看看我们未来几年可以期待看到哪些变化。
第十二章: MicroProfile LRA 及其 MicroProfile 的未来
您已经到达了这本书的最后一章。恭喜您走到了这一步!在本章的最后,我们将简要讨论新发布的 MicroProfile 长运行操作(LRA),然后看看 MicroProfile 的未来。
在编写这本书的过程中,MicroProfile LRA 1.0 版本发布,以解决微服务事务的需求。正如我们大家所知道的,传统的事务是指资金的移动,例如在线支付或从银行取款。在传统应用中,你通常使用两阶段提交或扩展架构(XA)协议来管理事务。然而,这些技术并不适合云原生应用的事务。在本章中,我们将探讨 MicroProfile 如何满足管理云原生事务的需求。我们还将查看云原生应用的交易架构。之后,我们将向您介绍最新的 MicroProfile 平台版本。最后,我们将了解 MicroProfile 的未来路线图以及它与 Jakarta EE 社区的协同。
我们将涵盖以下主题:
-
云原生应用事务
-
使用最新的 MicroProfile 平台版本
-
MicroProfile 的技术路线图
-
MicroProfile 与 Jakarta EE 的协同
到本章结束时,您应该能够使用 MicroProfile LRA 进行云原生应用事务,并描述 MicroProfile 的路线图,这将帮助您为未来设计应用。
云原生应用事务
云原生应用程序事务试图确保数据一致性和完整性,类似于传统事务。传统事务通常使用两阶段提交或 XA 协议。两阶段提交协议确保事务更新在所有数据库中提交,或者在失败的情况下完全回滚。它被许多数据库广泛支持。正如其名称所暗示的,此协议由两个阶段组成:投票 阶段和提交 阶段。在投票阶段,事务管理器从参与 XA 资源获得批准或拒绝。在提交阶段,事务管理器通知参与者结果。如果结果是积极的,整个事务将被提交。否则,它将被回滚。此协议非常可靠,并保证数据一致性。缺点是它会锁定资源,可能导致无限期阻塞。因此,它不适合云原生应用程序,因为它们扩展性不好,持有的锁的延迟问题。因此,为云原生应用程序事务建立了saga 模式以实现最终数据一致性。MicroProfile LRA 是 saga 模式的实现。在接下来的小节中,我们将更详细地讨论 LRA。
使用 MicroProfile LRA 进行云原生应用程序事务
MicroProfile LRA (download.eclipse.org/microprofile/microprofile-lra-1.0) 为云原生应用程序事务提供了一种解决方案。它引入了两个主要概念:
-
LRA 参与者:LRA 参与者是事务参与者,即云原生应用程序。
-
LRA 协调器:LRA 协调器是一个事务协调器,负责管理 LRA 处理和 LRA 参与者。LRA 协调器管理所有 LRAs 并根据 LRA 状态调用 LRA 方法。
以下说明了 LRA 参与者和 LRA 协调器之间的关系:

图 12.1 – LRA 协调器和参与者
如图 12.1所示,LRA 参与者向 LRA 协调器注册,然后根据事务状态调用相关的 JAX-RS 方法。我们现在将更详细地讨论 LRA 参与者。
LRA 参与者
LRA 参与者是涉及事务的 JAX-RS 方法,并在org.eclipse.microprofile.lra.annotation包中注解了以下 LRA 注解:
-
@LRA:带有此注解的方法将与 LRA 相关联。@LRA注解将方法注册到 LRA 协调器。当使用此注解时,以下LRA.Type类似于TransactionAttributeType枚举,其变体如下:a)
REQUIRED: 使用此类型时,如果方法在 LRA 上下文外部被调用,方法调用将运行在新 LRA 上下文中。否则,它将以相同的上下文运行。b)
REQUIRES_NEW: 使用此类型时,方法调用将始终在新 LRA 上下文中运行。c)
MANDATORY: 使用此类型时,方法调用将在 LRA 上下文内部运行。如果它在 LRA 上下文外部被调用,将返回错误。d)
SUPPORTS: 使用此类型时,如果方法在 LRA 上下文外部被调用,它将在 LRA 上下文外部执行。如果它在 LRA 上下文内部被调用,它将在 LRA 上下文内部执行。e)
NOT_SUPPORTED: 使用此类型时,方法始终在 LRA 上下文外部执行。f)
NEVER: 使用此类型时,如果方法在 LRA 上下文外部被调用,它将在 LRA 上下文外部执行。如果在 LRA 上下文内部调用,方法执行将失败,并返回返回代码412。g)
NESTED: 使用此类型时,当方法被调用时,将创建一个新的 LRA,这可以是顶层或嵌套的,具体取决于它是否在 LRA 上下文内部被调用。如果在外部上下文中调用,新的 LRA 将是顶层的。否则,新的 LRA 将是嵌套的。
你可能想知道如何确定方法是否在 LRA 上下文内部被调用。如果存在LRA_HTTP_CONTEXT_HEADER头,则表示方法是在 LRA 上下文内部被调用的。
-
@Complete: 带有此注解的方法将在 LRA 关闭时被调用。 -
@Compensate: 如果 LRA 被取消,将调用带有此注解的方法。 -
@Forget: 如果@Complete或@Compensate方法调用失败,将调用带有此注解的方法。 -
@Leave: 带有此注解的方法会导致 LRA 参与者从 LRA 参与中移除。 -
@Status: 带有此注解的方法报告相关 LRA 的状态。 -
@AfterLRA: 带有此注解的方法将在 LRA 结束时被调用。
@Compensate、@Complete和@AfterLRA注解用于PUT操作,而@Status注解用于GET操作,@Forget用于DELETE操作。让我们通过这个代码片段进一步解释这些注解:
@LRA(value = LRA.Type.REQUIRED, end=false)
@POST
@Path("/book")
public Response bookHotel(@HeaderParam
(LRA_HTTP_CONTEXT_HEADER) String lraId) {
// code
}
@Complete
@Path("/complete")
@PUT
public Response completeBooking(@HeaderParam (LRA_HTTP_CONTEXT_HEADER) String lraId, String userData) {
//code
}
@Compensate
@Path("/compensate")
@PUT
public Response cancelBooking(@HeaderParam (LRA_HTTP_CONTEXT_HEADER) String lraId, String userData) {
//code
}
在代码片段中,当bookHotel()方法在 LRA 内部被调用时,此方法将以相同的 LRA 上下文运行。如果它在 LRA 上下文外部被调用,该方法将以新的上下文运行。此方法可能会调用另一个服务。如果此方法成功,将调用completeBooking()方法。否则,将调用cancelBooking()方法。你可能想知道哪个服务调用completeBooking()和cancelBooking()方法。这是 LRA 协调器的职责,它将确保调用相应的方法。在下一节中,我们将讨论如何将 LRA 的 API 提供给你的 Maven 和 Gradle 项目。
使 MicroProfile LRA 可用
要使用 MicroProfile LRA API,您需要使这些 API 可用于您的应用程序。如果您创建 Maven 项目,您可以直接将以下内容添加到您的 pom.xml 中:
<dependency>
<groupId>org.eclipse.microprofile.lra</groupId>
<artifactId>microprofile-lra-api</artifactId>
<version>1.0</version>
</dependency>
或者,如果您创建 Gradle 项目,您需要添加以下依赖项:
dependencies {
providedCompile org.eclipse.microprofile.lra :microprofile-lra-api:1.0
}
通过这种方式,您已经学会了如何在您的云原生应用程序中执行事务。恭喜!您现在已经了解了所有 MicroProfile 规范。在下一节中,让我们讨论如何最好地使用最新的 MicroProfile 平台版本。
使用最新的 MicroProfile 平台版本
在 第二章,“MicroProfile 如何适应云原生应用程序开发?”,我们提到了 MicroProfile 平台版本及其内容。到目前为止,最新的 MicroProfile 平台版本是 MicroProfile 4.1,可以在 download.eclipse.org/microprofile/microprofile-4.1/ 找到。
MicroProfile 4.1 是建立在 MicroProfile 4.0 之上的,MicroProfile Health 从 3.0 更新到 3.1。MicroProfile 4.1 与以下 Jakarta EE 8 规范保持一致:
-
Jakarta 上下文和依赖注入 2.0
-
Jakarta 注解 1.3、Jakarta RESTful Web 服务 2.1
-
Jakarta JSON-B 1.0
-
Jakarta JSON-P 1.1
-
Jakarta 注解 1.3
它还包括以下 MicroProfile 规范:
-
配置 2.0
-
容错 3.0
-
健康性 3.1
-
JWT 传播 1.2
-
指标 3.0
-
OpenAPI 2.0
-
OpenTracing 2.0
-
Rest 客户端 2.0
如果您想为您的云原生应用程序使用 MicroProfile 4.1 的一些规范,您需要遵循以下步骤:
-
使 MicroProfile 4.1 的 API 可用于编译您的云原生应用程序。
-
如果您构建 Maven 项目,请在您的
pom.xml中添加以下依赖项,以便将 API 供您的云原生应用程序使用:<dependency> <groupId>org.eclipse.microprofile</groupId> <artifactId>microprofile</artifactId> <version>4.1</version> <type>pom</type> <scope>provided</scope> </dependency>或者,为您的 Gradle 项目指定以下依赖项:
dependencies { providedCompile org.eclipse.microprofile :microprofile:4.1 } -
选择一个 MicroProfile 4.1 实现来运行您的云原生应用程序。
Open Liberty 被用作兼容实现来发布 MicroProfile 4.1。如 第七章,“使用 Open Liberty、Docker 和 Kubernetes 的 MicroProfile 生态系统”中提到的,Open Liberty 是一个非常轻量级且性能优异的运行时,用于支持 MicroProfile 规范。它也是一个可组合的运行时。在您的
server.xml中指定以下 MicroProfile 4.1 功能会导致 MicroProfile 4.1 的实现被加载:<feature>microProfile-4.1</feature> -
要使用独立规范,例如 MicroProfile GraphQL、MicroProfile 上下文传播、MicroProfile 反应式消息传递和 MicroProfile LRA,您需要指定前几章中提到的相关 API Maven 依赖项,然后在您的
server.xml中包含相应的功能元素,如下所示。以下行引入了 MicroProfile GraphQL 1.0 的实现:<feature>mpGraphQL-1.0</feature>这行代码启用了 MicroProfile Context Propagation 1.2 的支持:
<feature>mpContextPropagation-1.2</feature>这行代码引入了 MicroProfile Reactive Messaging 1.0 的实现:
<feature>mpReactiveMessaging-1.0</feature>这行代码启用了 MicroProfile LRA 1.0 的 MicroProfile LRA 参与者:
<feature>mpLRA-1.0</feature>这行代码启用了 MicroProfile LRA 1.0 的 MicroProfile LRA 协调器:
<feature>mpLRACoordinator-1.0</feature>从 20.0.0.12-beta 版本开始,Open Liberty beta 驱动程序提供了对 MicroProfile LRA 1.0 的支持。
通过这些,你已经获得了关于 MicroProfile 的最新信息。在下一节中,我们将讨论 MicroProfile 的未来路线图。
MicroProfile 的技术路线图
MicroProfile 用于定义开发云原生应用的编程模型。它有助于使用不同的云基础设施技术建立良好的生态系统。这些云基础设施技术包括一些云原生框架,如 Kubernetes、Jaeger、Prometheus、Grafana 和 OpenTelemetry。Kubernetes、Jaeger、Prometheus 和 Grafana 是成熟的技术,你可能已经了解它们。你可能对 OpenTelemetry 了解不多。OpenTelemetry 是来自 云原生计算基金会 (CNCF) 的新沙盒项目,我们将花一些时间对其进行简要解释。
采用 OpenTelemetry 的 MicroProfile
OpenTelemetry (opentelemetry.io/) 是由 OpenTracing (opentracing.io/) 和 OpenCensus (opencensus.io/) 合并而成的一个新的 CNCF 可观测性框架。由于 MicroProfile OpenTracing,如 第六章 中所述,观察和监控云原生应用 基于 OpenTracing,OpenTelemetry 最终必然会被 MicroProfile 采用。
MicroProfile 社区投入了大量精力来研究如何在 MicroProfile 中利用 OpenTelemetry。一个建议是继续支持 OpenTracing API,但其实现采用 OpenTelemetry,通过以下代码片段将 OpenTelemetry 追踪器转换为 OpenTracing 追踪器:
io.opentracing.Tracer tracer = TracerShim.createTracerShim(openTelemetryTracer);
通过追踪器转换,当前的 MicroProfile OpenTracing 应该能够继续工作。然而,OpenTracing 追踪器 API 将不再维护,因为社区已经转向开发 OpenTelemetry API。最终目标是暴露 OpenTelemetry 的追踪器 API (io.opentelemetry.api.trace.Tracer)。MicroProfile 社区正在努力寻找采用 OpenTelemetry 的最佳方式。
你可能知道 OpenTelemetry 也提供了指标支持。我们需要将 OpenTelemetry 指标拉入 MicroProfile 吗?这是一个悬而未决的问题。让我们在下一节中讨论 MicroProfile Metrics 的未来路线图。
MicroProfile Metrics 的未来是什么?
MicroProfile 度量指标,在第6 章中解释,观察和监控云原生应用,基于Dropwizard (www.dropwizard.io/en/latest/),这是一个用于开发操作友好、高性能、RESTful Web 服务的 Java 框架。Dropwizard 在过去几年中非常受欢迎。然而,最近,Micrometer (micrometer.io/)获得了更多的动力,并成为了一个突出的度量指标框架。MicroProfile 度量指标规范团队正在研究如何在保持当前 API 与 Micrometer 或 Dropwizard 兼容的同时采用 Micrometer。
如前所述,OpenTelemetry 也包含度量指标支持。另一个建议是让 MicroProfile 度量指标与 OpenTelemetry 度量指标对齐。如果 OpenTelemetry 度量指标是未来的新度量标准,MicroProfile 应该采用 OpenTelemetry 度量指标。因此,MicroProfile 提供了两个度量指标候选方案供选择。现在,问题出现了:你应该选择哪一个?这取决于哪一个将成为主流。理想的情况是 Micrometer 与 OpenTelemetry 集成。如果 Micrometer 与 OpenTelemetry 度量指标集成良好,采用 Micrometer 将自然与 OpenTelemetry 度量指标对齐。也许 MicroProfile 度量指标应该等待 OpenTelemetry 度量指标稳定下来,然后确定采用哪个框架。
除了现有的 MicroProfile 规范之外,MicroProfile 社区也对新的倡议感兴趣,例如 gRPC。
采用 gRPC
除了当前 MicroProfile 规范的演变之外,社区还感兴趣于采用新技术以提供更好的云原生应用支持。一个潜在的新规范是 gRPC。
gRPC (grpc.io/)是一个现代高性能远程过程调用(RPC)框架,可以在任何环境中运行。为了使 gRPC 在云原生应用中更容易使用,如果它能与 CDI、JAX-RS 等集成,那就太好了。如果 MicroProfile 采用 gRPC 创建一个新的规范 MicroProfile gRPC,这个规范将能够与其他 MicroProfile 规范紧密且无缝地工作。
既然我们已经讨论了规范更新,我们将讨论 MicroProfile 与 Jakarta EE 版本的对齐。
MicroProfile 与 Jakarta EE 对齐
MicroProfile 采用了几个 Jakarta EE 技术,例如 CDI、JAX-RS、JSON-B、JSON-P 和 Common Annotations。MicroProfile 4.0 和 4.1 与 Jakarta EE 8 版本对齐。本书基于 MicroProfile 4.1 版本。MicroProfile 与 Jakarta EE 一直保持着非常紧密的合作。MicroProfile 中的大多数主要参与者也参与了 Jakarta EE。MicroProfile 和 Jakarta EE 形成了一个非常适合开发云原生应用的生态系统。它们始终保持同步并且相互兼容非常重要。Jakarta EE 9.1([jakarta.ee/release/9.1/](https://jakarta.ee/release/9.1/))于 2021 年发布,这为 MicroProfile 与此版本一起工作添加了一个要求,以便最终用户可以使用这两个框架的 API。由于这个要求,我们将在下一节讨论 MicroProfile 5.0,该版本计划与 Jakarta EE 9.1 对齐。
将 MicroProfile 5.0 与 Jakarta EE 9.1 对齐
MicroProfile 5.0 的重点是与 Jakarta EE 9.1 对齐。包括 Config、容错、Rest 客户端、健康、度量、OpenTracing、OpenAPI 和 JWT 传播在内的八个组件规范需要更新,以便与 Jakarta EE 9.1 对齐。其中一些规范在它们的 API 中并不直接依赖于 Jakarta 规范,但它们的 技术兼容性工具包(TCKs)会引入 Jakarta 规范。对于这些规范,只需要进行小版本更新。为了使所有 MicroProfile 规范都能与 Jakarta 9.1 一起工作,独立发布下的规范,如 Reactive Streams Operators、Reactive Messaging、LRA、GraphQL 和 Context Propagation,都需要更新以与 Jakarta EE 9.1 对齐。
除了与 Jakarta EE 对齐之外,一些 MicroProfile 规范扩展了当前的 Jakarta 规范。由于这些 MicroProfile 规范是在 Java EE 停滞不前时创建的,因此这些 MicroProfile 规范成为 Jakarta 规范可能是正确的时间,这样其他 Jakarta 规范就可以从中受益。让我们看看那些规范。
将一些 MicroProfile 规范迁移到 Jakarta EE
一些 Jakarta EE 规范,例如Jakarta NoSQL(GitHub 仓库:https://github.com/eclipse-ee4j/nosql),将受益于 MicroProfile Config 进行配置。如果 Jakarta EE 依赖于 MicroProfile,这将创建一个循环依赖,因为 MicroProfile 与 Jakarta EE 规范保持一致。另一个问题是,Jakarta EE 规范传统上更仔细地维护向后兼容性,而 MicroProfile 规范有时会引入向后不兼容的更改。因此,Jakarta EE 规范直接依赖于 MicroProfile 规范可能存在一些问题。为了解决这个问题,提出了一个新的提案,Jakarta Config,以与 MicroProfile Config 合作。Jakarta Config(GitHub 仓库:github.com/eclipse-ee4j/config)可能成为 Jakarta EE 的核心。Jakarta Config 的目标是包含在 Jakarta Core Profile 中,以便其他配置文件和 MicroProfile 可以依赖于这个规范。
除了与 Jakarta EE 保持一致之外,MicroProfile 还在尝试采用长期支持(LTS)Java 版本,如 Java 11 和 Java 17。
你可能还记得 MicroProfile 中的两个版本:平台和独立。MicroProfile 社区需要查看独立版本中包含的规范,以确定是否是时候将一些规范移回平台版本桶中。MicroProfile 社区需要改进的另一个领域是最终用户体验。MicroProfile 社区将继续改进其入口页面(microprofile.io/)。
我们简直不敢相信这本书已经接近尾声。还有这么多主题需要涵盖。有关 MicroProfile 的更多信息,请访问microprofile.io/。如果您想了解与 MicroProfile 相关的任何内容,请访问 Open Liberty 指南(openliberty.io/guides/)。
摘要
就这样,我们来到了这本书的结尾。让我们回顾一下你所学到的内容。在这本书中,我们学习了如何使用 Jakarta REST、JSON-P、JSON-B、CDI 和 MicroProfile Rest Client 创建云原生应用;然后使用 MicroProfile Config、容错、Open API 和 JWT Propagation 增强云原生应用;最后使用 MicroProfile Health、Metrics 和 Open Tracing 监控云原生应用。然后我们了解了 MicroProfile 生态系统,包括 Open Liberty、Docker、Kubernetes 和 Istio。在覆盖了所有技术之后,我们查看了一个利用不同 MicroProfile 技术的端到端项目。之后,我们讨论了部署和第二天操作。然后我们研究了独立的规范:MicroProfile GraphQL、MicroProfile Context Propagation 和 MicroProfile Reactive Messaging。
在本章的最后,我们讨论了 MicroProfile LRA 1.0 的最新版本。然后,我们讨论了 MicroProfile 的未来路线图,接着是与其 Jakarta EE 对齐的计划。本章的要点是 MicroProfile 和 Jakarta EE 是相互补充的,它们共同构成了一个支持云原生应用程序的伟大生态系统。
我们希望您喜欢阅读这本书,并学会了如何使用 MicroProfile 的惊人特性来帮助您进行云原生应用程序的开发、部署和管理。如果您想为 MicroProfile 做出贡献,请点击 microprofile.io 网站上的加入讨论链接(microprofile.io/)以表达您加入邮件列表的兴趣。

订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助您规划个人发展并推进职业生涯。更多信息,请访问我们的网站。
第十三章:为什么订阅?
-
通过来自 4,000 多名行业专业人士的实用电子书和视频,节省学习时间,更多时间编码
-
通过为您量身定制的技能计划提高您的学习效果
-
每月免费获得一本电子书或视频
-
完全可搜索,便于轻松访问关键信息
-
复制粘贴、打印和收藏内容
您知道 Packt 为每本书都提供电子书版本,并提供 PDF 和 ePub 文件吗?您可以在packt.com升级到电子书版本,并且作为印刷书客户,您有权获得电子书副本的折扣。如需了解更多详情,请联系我们 customercare@packtpub.com。
在www.packt.com,您还可以阅读一系列免费技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
您可能还会喜欢的其他书籍
如果您喜欢这本书,您可能对 Packt 的其他书籍也感兴趣:
Jakarta EE Cookbook 第二版
Elder Moraes
ISBN: 978-1-83864-288-4
-
使用 Jakarta EE 最常用的 API 和功能进行服务器端开发
-
在 Web 应用程序中借助 HTTP2 实现快速安全的通信
-
使用可重用组件构建企业应用程序
-
使用 Jakarta EE 和 Eclipse MicroProfile 将单体应用程序分解为微服务
-
通过多线程和并发提高您的企业应用程序
-
在云中借助容器运行应用程序
-
掌握持续交付和部署,以有效地发布您的应用程序
Supercharge Your Applications with GraalVM
A B Vijay Kumar
ISBN: 978-1-80056-490-9
-
深入了解 GraalVM 及其内部工作原理
-
使用 GraalVM 的高性能优化编译器,了解它如何在 JIT(即时)和 AOT(提前)模式下使用
-
了解 GraalVM 在运行时执行的各项优化
-
使用高级工具分析和诊断代码中的性能问题
-
使用 Truffle 在 GraalVM 上编译、嵌入、运行和语言间互操作
-
使用 Micronaut 和 Quarkus 等流行框架构建最佳微服务,以创建原生云应用程序
Packt 正在寻找像您这样的作者
如果您有兴趣成为 Packt 的作者,请访问authors.packtpub.com并今天申请。我们已与成千上万的开发者和技术专业人士合作,就像您一样,帮助他们将见解分享给全球技术社区。您可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交您自己的想法。
分享您的想法
您已经完成了《Practical Cloud-Native Java Development with MicroProfile》,我们非常想听听您的想法!如果您在亚马逊购买了这本书,请点击此处直接跳转到该书的亚马逊评论页面并分享您的反馈或在该购买网站上留下评论。
您的评论对我们和整个技术社区都很重要,并将帮助我们确保我们提供高质量的内容。





浙公网安备 33010602011771号