Micronaut-微服务构建指南-全-
Micronaut 微服务构建指南(全)
原文:
zh.annas-archive.org/md5/c7105fc3ca9badbb94b1aad238472531译者:飞龙
前言
Micronaut 是一个基于 JVM 的框架,用于构建轻量级、模块化的应用程序。它是一个快速增长的框架,旨在使创建微服务变得快速且简单。这本书将帮助全栈/Java 开发者使用 Micronaut 构建模块化、高性能和反应式的基于微服务的应用程序。
这本书面向谁
这本书是为那些在传统框架(如 Spring Boot)上构建微服务的开发者而写的,他们正在寻找更快的替代方案。需要具备中级 Java 编程知识,以及使用 Java 实现 Web 服务开发的相应知识。
这本书涵盖的内容
第一章, 使用 Micronaut 框架开始微服务之旅,从微服务及其设计模式的一些基本概念开始。然后,您将了解 Micronaut 框架以及为什么它是开发微服务的理想框架。稍后,您将通过使用 Maven 和 Gradle 进行 hello-world 项目来亲身体验 Micronaut 框架。
第二章, 数据访问工作,涵盖了与各种数据库和持久化框架一起工作的方面。您将从对象关系映射框架开始,在动手实践 Hibernate 框架的同时,然后深入使用持久化框架(MyBatis)。最后,您还将与非关系型数据库(MongoDB)集成。
第三章, RESTful Web 服务工作,从数据传输对象和映射器的讨论开始。然后,您将深入了解在 Micronaut 框架中与 RESTful 接口一起工作。稍后,您将了解 Micronaut 的 HTTP 服务器和客户端 API。
第四章, 保护 Web 服务,涵盖了在 Micronaut 框架中保护 Web 端点的一些方法,例如会话认证、JWT 和 OAuth。
第五章, 使用事件驱动架构集成微服务,从事件驱动架构和两种不同的事件发布模型开始:拉模型和推模型。然后,您将深入了解事件流以及如何在宠物诊所应用程序(示例项目)中使用 Apache Kafka 来集成两个微服务。
第六章, 测试微服务,对各种自动化测试进行了探讨——单元测试、服务测试和集成测试,以及如何采用谨慎的自动化测试策略来降低成本并提高微服务的健壮性。
第七章,处理微服务关注点,涵盖了在微服务上工作时的一些核心关注点,例如分布式配置管理、记录服务 API、服务发现和 API 网关。稍后,您还将探索 Micronaut 框架中的容错机制。
第八章,部署微服务,涵盖了微服务的构建和部署方面。您将首先使用自动化工具构建容器工件,然后利用 Docker Compose 部署微服务。
第九章,分布式日志、跟踪和监控,介绍了在微服务中使用分布式日志、分布式跟踪和分布式监控来实现可观察性模式。
第十章,使用 Micronaut 的物联网,从对物联网和 Alexa 的介绍开始,涵盖 Alexa 基础知识和一个 hello-world 示例。稍后,您将能够在开发宠物诊所应用程序的同时将 Micronaut 与 Alexa 集成。
第十一章,构建企业级微服务,涵盖了在微服务上工作的最佳实践以及如何构建和扩展企业级微服务。
为了充分利用本书

如果您正在使用本书的数字版,我们建议您亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/Building-Microservices-with-Micronaut。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781800564237_ColorImages.pdf
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“遵循这些说明,我们添加了一个foo-stream主题并向此主题添加了一条消息。”
代码块设置如下:
@KafkaClient
public interface VetReviewClient {
@Topic("vet-reviews")
void send(@Body VetReviewDTO vetReview);
}
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“在 Kafdrop 上查看,我们可以验证来自 pet-clinic-reviews 微服务的事件已流出并添加到vet-reviews主题。”
小贴士或重要注意事项
看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过 mailto:customercare@packtpub.com 发送邮件给我们。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
版权侵权:如果您在互联网上发现任何形式的我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 mailto:copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
分享您的想法
一旦您阅读了《使用 Micronaut 构建微服务》,我们很乐意听到您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
第一部分:核心概念和基础知识
本节启动了在 Micronaut 框架中微服务的旅程,同时涵盖了微服务的一些基本概念、微服务设计模式和为什么 Micronaut 是微服务开发的理想框架。
本节包含以下章节:
- 第一章, 使用 Micronaut 框架入门微服务
第一章:使用 Micronaut 框架开始微服务之旅
近年来,关于微服务及其如何使微服务架构在开发快速、敏捷和面向企业的网络服务中发挥变革性作用的话题引起了广泛关注。微服务架构推动了这些网络服务开发标准的规范化。在本章中,我们将探讨网络服务向微服务的演变。我们将快速深入了解一些有用的微服务设计模式。我们将关注大多数传统 Java 开发框架中的关键陷阱,以及它们对微服务架构的表面级采用如何提高了性能和优化问题。然后,我们将探讨 Micronaut 框架如何通过彻底的、从头开始的方法解决微服务中的性能和优化问题。最后,为了开始使用 Micronaut 框架,我们将设置 Micronaut CLI 并完成一个小型的 hello world 项目。
本章中,我们将特别关注以下主题:
-
介绍微服务及其演变
-
理解微服务设计模式
-
为什么 Micronaut 是开发微服务的最佳选择
-
开始使用 Micronaut 框架
-
在 Micronaut 框架中完成 hello world 项目
在本章结束时,您将了解网络服务如何演变为微服务,以及为什么与 Micronaut 框架相比,传统的 Java 框架在开发微服务方面效率低下。此外,我们还将通过在 Micronaut 框架中完成一个小项目来获得使用 Micronaut 框架的实际知识。
技术要求
本章中所有命令和技术说明均在 Windows 10 和 mac OS X 上运行。本章涵盖的代码示例可在本书的 GitHub 存储库中找到,网址为github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter01。
在开发环境中需要安装和设置以下工具:
-
Java SDK:版本 13 或更高(我们使用了 Java 14)。
-
Maven:这是可选的,仅当您想使用 Maven 作为构建系统时才需要。然而,我们建议在任何开发机器上设置 Maven。有关下载和安装 Maven 的说明,请参阅
maven.apache.org/download.cgi。 -
开发 IDE:根据您的偏好,可以使用任何基于 Java 的 IDE,但为了编写本章,使用了 IntelliJ。
-
Git:有关下载和安装 Git 的说明,请参阅
git-scm.com/downloads。
介绍微服务及其演变
在我们深入介绍和定义微服务之前,了解微服务的演变过程将非常有帮助。在 20 世纪 60 年代末,艾伦·凯提出了面向对象编程这一术语。虽然这是一个明确的概念,但后来它孕育了使用面向对象编程构建软件解决方案的四个支柱:
-
封装
-
继承
-
多态
-
抽象
用简短的助记符,它被称为 EIPA。自从这四个支柱诞生以来,软件行业见证了众多编程语言、框架、设计模式等的兴衰。随着每一次这样的适应和理念,思想家和发明家们试图通过保持模块化设计和松散耦合但紧密封装的应用程序组件,来更接近 EIPA。在过去的几十年里,软件团队通过系统地采用这些关键支柱,从面向对象编程的艺术转向面向对象编程的科学。这一迭代之旅是微服务的演变。
在 20 世纪 80 年代末和 90 年代初,几乎每个企业应用程序都以命令行或本地桌面软件的形式暴露出来。应用程序与数据库紧密相连,几乎就像最终用户直接与数据库交互,而应用程序作为一个薄薄的门面介于两者之间。这是单体应用程序或客户端/服务器架构的时代。
在接下来的图中,我们可以看到用户如何与单体应用程序交互:

图 1.1 – 单体客户端/服务器架构
如图 1.1所示,在单体客户端/服务器架构中,应用程序与数据库紧密耦合,用户通过终端门面或桌面应用程序进行交互。在这个架构中,维护良好的服务级别协议(SLA)非常痛苦。几乎所有关键的非功能性因素,如可伸缩性、高可用性、容错性和灵活性,都表现不佳或失败。
为了解决这些问题,面向服务的架构(SOA)应运而生。在 21 世纪初,SOA 在行业中得到了正式化,定义了一些标准协议,如简单对象访问协议(SOAP)。Web 服务描述语言(WSDL)也在此期间被创建。Web 2.0 应用程序因异步 JavaScript 和 XML(AJAX)而流行。企业服务总线和信息传递系统在企业应用程序中得到广泛使用。SOA 的进步催化了向最终用户提供软件解决方案的新范式:软件即服务(SaaS)。软件解决方案不再通过桌面应用程序和终端客户端提供,而是作为托管在线服务通过 HTTP 提供给最终用户。在接下来的图中,我们可以看到用户如何与基于 SOA 的应用程序交互:

图 1.2 – SOA
如上图所示,SOA 通过在 Web 应用程序、Web 服务器和应用程序服务器之间分离关注点引入了一些解耦。应用程序服务器或企业服务总线(ESB)通常与数据库交互,用户通过在 Web 浏览器上访问应用程序与它交互(SaaS 解决方案)。尽管 SOA 带来了一些缓解,但 SaaS 的采用留下了可扩展性和灵活性作为关键未解之谜。
2010 年之后,技术世界开始比前二十年移动得更快。随着容器、云、大数据和机器学习的引入,架构设计开始迅速发展。这是优步、爱彼迎、Netflix 和免费/付费应用的时代。应用程序是为分布式计算和可扩展性而设计的。在微服务架构中,应用程序被分解为松散耦合的微服务,每个微服务拥有自己的数据库。在下面的图中,我们可以看到用户如何与基于微服务的应用程序交互:

图 1.3 – 微服务架构
在前面的图中,我们可以看到一个完全实现的微服务应用程序,其中每个微服务都与自己的数据库交互。用户通过现代浏览器与单页应用程序交互。来自网络服务器的任何传入请求都会被路由到相应的微服务。微服务架构的完全实现是为了解决可扩展性、容错性、高可用性和灵活性等关键因素。
简单来说,微服务或微服务架构将应用程序分解为一系列相互交互的服务。每个服务都可以独立地进行开发、测试、部署和维护。因此,每个较小的(微)服务都有自己的独特生命周期。此外,由于每个服务都是松散耦合的(使用 HTTP/HTTPS 与其他服务交互),我们可以做以下事情:
-
扩大或缩小(根据服务流量)。
-
解决任何运行时故障(启动服务备份)。
-
进行新的更改(更改影响仅限于服务)。
因此,通过在微服务中完全实现解耦架构,我们解决了可扩展性、容错性、高可用性和灵活性等关键问题。
到目前为止,我们已经了解了微服务及其演变,以及它们是如何在解决当今世界独特、快速和敏捷需求方面产生变革性的影响的。这种理解是实现微服务潜力的良好前言。在下一节中,我们将深入探讨微服务设计模式。
理解微服务设计模式
为了充分实现任何架构(包括微服务架构)的好处,通常需要一个架构方法来支持设计模式。理解这些设计模式对于理想地采用架构至关重要。在接下来的章节中,我们将介绍一些实用且常用的微服务设计模式。每个模式都针对应用程序开发生命周期的不同方面,我们的重点是查看这些设计模式的实际使用角度。我们将从分解设计模式开始。
分解设计模式
分解设计模式规定了我们可以如何将大型/单体应用程序组件化或分解成更小的(微)服务。这些模式在为任何遗留单体应用程序设计转型架构时非常有用。以下是在分解中常用的设计模式。
按业务能力分解
任何业务能力都是一种盈利工具。如果我们能将应用程序列出来并分类成一系列业务能力,如库存管理、客户订单或运营,那么应用程序就可以分解成基于这些业务能力的微服务。这个过程对于中小型应用程序来说是有效且推荐的。
按领域/子领域分解
如果应用程序是一个企业级和重量级的应用程序,那么先前的做法可能会导致应用程序分解成更小的单体。这些单体虽然更小,但仍然是单体。在这种情况下,业务建模可以帮助将应用程序功能分类并映射到领域和子领域。领域/子领域内的功能相似,但与其他领域/子领域的功能非常不同。然后,可以围绕领域或子领域(如果有许多功能映射到领域)设计和构建微服务。
集成设计模式
一旦应用程序被分解成更小的(微)服务,我们就需要在这些服务之间建立内聚。集成设计模式解决了这样的协作需求。以下是在集成中常用的设计模式。
API 网关模式
通常上游前端消费者需要通过一个外观访问微服务。这个外观被称为 API 网关。API 网关设计模式为前端客户端保持简单提供了一个重要的目的:
-
前端客户端不会向微服务发送过多的请求。
-
前端客户端不会处理/聚合过多的响应(来自微服务)。
-
在服务器端,网关将请求路由到多个微服务,这些微服务可以并行运行。
-
在发送最终响应之前,我们可以聚合来自不同微服务的单个响应。
聚合器模式
这种模式与上述 API 网关模式非常相似。然而,复合微服务是关键区别。复合微服务的使命是将传入的请求卸载到多个微服务,然后协作创建统一的响应。当用户请求从业务逻辑的角度来看是原子的,但它由多个微服务处理时,使用此模式。
链式微服务模式
在某些场景中,一个传入的请求会通过一系列步骤执行,每个步骤都可能产生对微服务的调用。例如,在在线市场中订购商品可能需要以下步骤:
-
搜索商品(库存管理服务)
-
将商品添加到购物车(购物车服务)
-
检查添加的商品(支付服务、邮件服务、库存管理服务)
所有这些服务调用将是同步的。满足用户请求将是所有这些链式微服务调用的组合。
数据管理模式
将持久层集成是任何基于微服务应用程序的重要方面。绿色田野(全新)和棕色田野(遗留转型)应用程序可能规定了它们在如何选择数据管理模式方面的要求。以下是在微服务中数据管理最常用的设计模式。
每个服务一个数据库
在绿色田野(全新)应用程序中,每个服务拥有一个数据库是最理想的。每个服务是独立数据库(关系型或非关系型)的所有者,任何数据操作都必须通过微服务执行。此外,即使任何其他微服务需要执行数据库操作,也应该通过所有者微服务路由。
共享数据库
在棕色田野(转型)应用程序中,将数据库分解为每个服务一个数据库可能不切实际。在这种情况下,可以通过共享一个公共单体数据库来启动微服务架构的实现。
命令查询责任分离(CQRS)
在绿色田野或完全转型的应用程序中,每个微服务都是独立的数据库所有者,可能需要从多个数据库查询数据。CQRS 模式规定将应用程序分解为命令和查询:
-
命令:这部分将管理任何创建、更新和删除请求。
-
查询:这部分将使用数据库视图来管理查询请求,其中数据库视图可以统一来自多个模式或数据源的数据。
跨切面模式
一些关注点跨越了微服务的所有不同方面/层。在以下子节中,我们将讨论一些这些关注点和模式。
服务发现模式
在基于微服务的应用程序中,每个微服务在运行时可能拥有多个实例。此外,这些服务实例可以根据流量在运行时添加或删除。这种运行时的灵活性可能会成为上游消费者如何与服务连接的问题。
服务发现模式通过实现服务注册数据库来解决此问题。服务注册是一个元数据存储库,包含诸如服务名称、服务运行位置以及服务当前状态等信息。任何对服务运行时信息的更改都将更新到服务注册中,例如,当服务添加新实例或服务出现故障时。这简化了上游消费者连接到应用程序中不同微服务的痛苦。
断路器模式
在基于微服务的应用程序中,通常服务通过调用端点相互交互。可能会出现一种情况,即一个服务正在调用下游服务,但下游服务出现故障。如果没有断路器,上游服务将在下游服务处于故障状态时持续调用它,这将持续影响用户与应用程序的交互。
在断路器模式中,下游服务调用将通过代理进行路由。如果下游服务出现故障,该代理将在固定的时间间隔后超时。在超时到期后,代理将再次尝试连接。如果连接成功,它将与下游服务连接;否则,它将更新超时周期。因此,断路器不会对下游服务进行不必要的调用,也不会影响用户与应用程序的交互。
日志聚合模式
在微服务领域中,通常一个入站请求会被多个服务处理。每个服务都可能创建并记录其条目。为了追踪任何问题,访问这些零散的日志可能会显得反直觉。通过实现日志聚合模式,日志可以在一个中心位置进行索引,从而使得访问所有应用程序日志变得容易。可以使用Elasticsearch、Logstash、Kibana(ELK)来实现日志聚合。
在本节中,我们介绍了应用生命周期不同阶段中常用的一些设计模式。理解这些设计模式是充分利用微服务架构优势的必要条件。在下一节中,我们将深入探讨用于开发微服务的 Micronaut 框架。
为什么 Micronaut 是开发微服务的最佳选择
在前面的章节中,我们学习了微服务架构在架构方面的成熟度。不幸的是,在实施方面,向构建/开发微服务的全面转变并不像微服务架构那样成熟。为了解决这些实施挑战,许多传统的 Java 框架已经添加了小的、迭代的变更,但人们迫切需要的颠覆性和全面性的变更却缺失。从根本上讲,这些传统的 Java 框架自单体服务时代以来几乎保持不变。反射、运行时代理和庞大的配置管理一直困扰着所有传统框架,导致它们启动时间较慢和内存占用更大,这使得它们不适合微服务开发。
Micronaut 是从底层开发的,考虑到这些重要的挑战,以有机地支持微服务开发:
-
@Inject用于依赖注入。它将Java 注入模块添加到编译器中,并且所有注解都在编译时进行处理。编译器根据源代码中使用的注解生成所有类的字节码。所有这些都是在编译时完成的。在运行时,Micronaut 可以实例化 bean 并从生成的字节码中读取它们的元数据,而不需要使用缓慢的反射 API。 -
编译时编译:正如之前讨论的那样,一个关键的区别是 Micronaut 在编译时执行依赖注入、配置管理和面向方面的编程代理。Micronaut 依赖于一个或多个注解处理器来将注解元数据处理成由ASM生成的(汇编)字节码。此外,这种提前生成的字节码还通过 Java 的即时(JIT)编译器进一步优化。其他框架使用反射,并在应用程序启动时生成注解元数据。这些元数据被加载到运行时内存中,因此增加了内存占用。Micronaut 不是使用 Java 反射 API,而是使用 Java 注解处理器 API、Kotlin 编译器插件和 Groovy AST 转换来进行元编程。
-
更快的启动时间和更低的内存消耗:其他框架使用 Java 反射,并在应用程序启动时扫描所有类路径以生成每个字段、方法和构造函数的反射元数据。然后,这些元数据被用来确定并注入所需的对象到应用程序运行时。这显著增加了启动时间和运行时内存。正如之前讨论的那样,Micronaut 使用编译时编译和 Java 注解处理器 API 来将这项工作从运行时卸载,并通过不将不必要的反射元数据推送到运行时内存来降低内存需求。
-
无服务器应用程序支持:无服务器应用程序中的一个关键问题是启动时间。由于内存占用大且启动时间慢,传统框架不是开发无服务器应用程序的明智选择。Micronaut 通过保持最小的运行时内存占用和亚秒级启动时间,自然支持无服务器应用程序的开发。此外,Micronaut 原生支持常用的云平台用于无服务器函数开发。
-
语言无关的框架:Micronaut 框架支持 Java、Kotlin 和 Groovy 编程语言。由于对主要编程语言的广泛支持,开发者在考虑云需求时可以选择他们偏好的语言选项。例如,对于物联网需求,Groovy 可能是一个不错的选择。这种语言无关的启用使其灵活且适用于移动/网络/云解决方案的多种需求。
-
对 GraalVM 的支持:由于 Micronaut 不使用反射,任何基于 Micronaut 的应用都可以编译成 GraalVM 原生镜像。GraalVM 是 Oracle 提供的一个通用虚拟机,可以运行 Java 应用程序到机器码。这显著提高了应用程序的性能。任何编译成 GraalVM 原生镜像的 Micronaut 应用程序都可以在毫秒内启动。
基于前面的关键点,Micronaut 作为一个首选框架,在开发云原生、超轻量级和快速微服务方面脱颖而出。此外,我们进行了一个快速基准实验,比较了 Micronaut 与另一个流行的传统框架的应用程序启动时间。在下面的图表中,显示了 Micronaut 和传统框架的启动时间:

Figure 1.5 – 使用 SDKMAN! 在 macOS 上安装 Micronaut CLI
-
如果所有前面的步骤都执行成功,你可以通过在终端中运行以下命令来验证 Micronaut CLI 的安装:
mn -version
使用 Homebrew 安装 Micronaut
请按照以下步骤使用 MacPorts 安装 Micronaut CLI:
-
打开终端。
-
如果你还没有安装 Homebrew,请执行以下步骤:
a. 输入或粘贴以下命令:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"b. 接下来,输入或粘贴以下命令:
brew update -
输入或粘贴以下命令:
brew install micronaut在安装 Micronaut CLI 时,你将在终端观察到以下交互:
![Figure 1.6 – 使用 HomeBrew 在 macOS 上安装 Micronaut CLI
![img/Figure_1.6_B16585.jpg]()
Figure 1.6 – 使用 HomeBrew 在 macOS 上安装 Micronaut CLI
-
如果所有前面的步骤都执行成功,你可以通过在终端中执行以下命令来验证 Micronaut CLI 的安装:
mn -version
使用 MacPorts 安装 Micronaut
请按照以下步骤使用 Homebrew 安装 Micronaut CLI:
-
如果你还没有安装 MacPorts,请按照
www.macports.org/install.php中的说明进行操作。 -
打开终端。
-
输入或粘贴以下命令:
sudo port sync -
输入或粘贴以下命令:
sudo port install micronaut在安装 Micronaut CLI 时,你将在终端观察到以下交互:
![Figure 1.7 – 使用 MacPorts 在 macOS 上安装 Micronaut CLI
![img/Figure_1.7_B16585.jpg]()
Figure 1.7 – 使用 MacPorts 在 macOS 上安装 Micronaut CLI
-
如果所有前面的步骤都执行成功,你可以通过在终端中执行以下命令来验证 Micronaut CLI 的安装:
mn -version
在 Windows 上安装 Micronaut CLI
请按照以下步骤在 Windows 上安装 Micronaut CLI:
-
从 Micronaut 下载页面下载 Micronaut CLI 二进制文件:
micronaut.io/download.html。 -
将下载的二进制文件解压到系统上的一个文件夹中。最好将其保存在根目录下的单独文件夹中,例如
C:\Program Files\Micronaut。 -
创建一个名为
MICRONAUT_HOME的新系统变量,并使用前面的目录路径。请注意,要将此变量添加到系统变量(而不是用户变量)中。 -
然后,更新你的 Windows
PATH环境变量。你可以添加一个路径,例如%MICRONAUT_HOME%\bin。 -
打开命令提示符或任何终端,并输入以下命令:
mn这将启动 CLI 并解决任何依赖项。
-
要测试 CLI 是否正确安装,请输入以下命令:
mn – h这是命令输出的内容:
![Figure 1.8 – 在 Windows OS 上安装 Micronaut CLI
![img/Figure_1.8_B16585_Fixed.jpg]()
图 1.8 – 在 Windows OS 上安装 Micronaut CLI
-
在执行前面的命令后,您应该会看到所有 CLI 选项。
在本节中,我们探讨了在 Windows 和 macOS 上安装 Micronaut CLI 的不同方法。为了亲身体验 Micronaut 框架,我们将在下一节中开始在一个 hello world 项目上工作。
在 Micronaut 框架中工作于 hello world 项目
为了理解使用 Micronaut 框架开发微服务的实际方面,我们将通过一个 hello world 项目进行操作。这将帮助您快速开始使用 Micronaut 框架,并让您亲身体验微服务开发是多么简单。
Micronaut 与 Maven 和 Gradle 打包管理器无缝协作。我们将使用 Micronaut CLI 以及 Micronaut Launch(网页界面)各举一个例子来生成基础项目。
使用 Micronaut CLI 创建 hello world 项目
请按照以下步骤使用 Micronaut CLI 创建 hello world 应用程序:
-
打开终端(或命令提示符)。
-
将目录更改为您想要创建 hello world 项目的目标目录。
-
输入以下命令:
mn create-app hello-world-maven --build maven -
等待 Micronaut CLI 完成,它将创建一个
hello-world-maven项目。create-app命令将为您创建一个带有 Maven 构建和系统安装的 Java 版本的样板项目。它将创建Application.java以及一个名为ApplicationTest.java的示例测试类。 -
要探索您刚刚创建的
hello-world-maven项目,请使用您首选的 IDE 打开此项目。 -
要运行您的项目,请在 Bash 终端中运行以下命令:
mvnw by typing the following command:mvn -N io.takari:maven:wrapper
-
Maven 包装器默认会在
http://localhost:8080上构建和运行您的项目。
添加 HelloWorldController
要创建一个简单的端点,让我们向 hello-world-maven 项目添加一个简单的控制器:
-
将一个网络包添加到我们的
hello-world-maven项目中。 -
添加一个
HelloWorldControllerJava 类。它将包含一个简单的hello端点:@Controller("/hello") public class HelloController { @Get("/") @Produces(MediaType.TEXT_PLAIN) public String helloMicronaut() { return "Hello, Micronaut!"; } }HelloController可在…/hello路径上访问。helloMicronaut()将生成一个纯文本"Hello, Micronaut!"消息。 -
重新运行您的应用程序,并在浏览器窗口中点击
localhost:8080/hello/。服务器将返回以下响应:

图 1.9 – 你好,Micronaut!
默认情况下,应用程序将在端口 8080 上可用,并且可以在应用程序属性中更改此端口。
到目前为止,我们已经使用 Micronaut CLI 创建了一个 hello world 项目。接下来,我们将探索 Micronaut Launch,这是一个网页界面,用于生成样板项目。
使用 Micronaut Launch 创建 hello world 项目
Micronaut Launch (micronaut.io/launch/) 是一个直观的 Web 界面,随着 Micronaut 2.0.1 的发布而出现。我们可以使用此界面快速为不同类型的 Micronaut 应用程序(如服务器应用程序、CLI、无服务器函数、消息应用程序等)生成模板。让我们快速使用它为我们生成一个 hello world 应用程序。
请按照以下说明使用 Micronaut Launch Web 界面生成 hello world 项目:
-
在浏览器窗口中打开 Micronaut Launch:
micronaut.io/launch/。 -
在 应用程序类型 下,选择 应用程序。
-
在 Micronaut 版本 下,选择 2.0.1。
-
对于 Java 版本,选择 Java 14。
-
对于 语言,选择 Java。
-
给一个基本包名,例如
com.packtpub.micronaut。 -
选择 Gradle 作为构建选项。
-
给应用程序命名,例如
hello-world-gradle。 -
选择 JUnit 作为测试框架
-
在完成所有选项的选择后,点击 生成项目。
在选择前面的选项并输入各种输入后,Micronaut Launch 界面应如下所示:

图 1.10 – 使用 Micronaut Launch 生成模板项目
您的项目模板源代码将被生成到一个压缩文件中。您可以将此压缩文件解压到您想要的目录中,并在您首选的 IDE 中打开它。就像之前的例子 (hello-world-maven) 一样,我们可以添加一个基本的 HelloWorldController 实例。
要运行您的项目,请在 Bash 终端中运行以下命令:
gradlew.bat run
当项目运行时,请访问 http://localhost:8080/hello,你应该在浏览器标签页中看到 Hello, Micronaut! 消息。
在本节中,我们探讨了如何通过使用 Micronaut CLI 以及 Micronaut Launch 用户界面开发小型的 hello world 项目来开始使用 Micronaut 框架。这个小练习将为我们下一章将要介绍的内容做一个良好的铺垫。
摘要
在本章中,我们通过探索其演变和一些有用的设计模式,开始了我们的微服务之旅。我们对比了 Micronaut 框架与传统基于反射的 Java 框架。本质上,Micronaut 利用编译时编译(而不是反射)的方法,使其成为开发微服务的理想框架。为了亲身体验,我们介绍了在 mac OS 和 Windows OS 上设置 Micronaut CLI 的方法。最后,我们在 hello-world-maven 和 hello-world-gradle 项目上进行了工作。在这两个项目中,我们添加了 hello 端点。
在涵盖了微服务的基本原理以及实际的“Hello World”项目之后,本章增强了你对微服务演变、设计模式以及为什么应该选择 Micronaut 来开发微服务的了解。这种基础理解是开始在 Micronaut 框架中开发微服务冒险的基石。
在本章末尾,我们启动了一段使用 Micronaut CLI 和 Micronaut Launch 开发微服务的激动人心的旅程。在下一章中,我们将探讨如何在 Micronaut 框架中集成不同类型的持久存储和数据库。
问题
-
网络服务是如何演变成微服务的?
-
什么是微服务?
-
什么是微服务架构?
-
微服务设计模式有哪些?
-
什么是 Micronaut?
-
为什么应该优先选择 Micronaut 来开发微服务?
-
开发微服务应该使用哪个框架?
-
你如何在 macOS 上安装 Micronaut CLI?
-
你如何在 Windows OS 上安装 Micronaut CLI?
-
你如何使用 Micronaut CLI 创建一个项目?
-
你如何使用 Micronaut Launch 创建一个项目?
第二部分:微服务开发
本节将涵盖微服务开发,包括与持久层集成、处理 Web 端点、确保 Web 端点安全以及使用事件流集成微服务。你将在 Micronaut 框架中开发微服务应用程序的同时完成所有这些工作。
本节包含以下章节:
-
第二章, 数据访问工作
-
第三章, RESTful Web 服务工作
-
第四章, 微服务安全
-
第五章, 使用事件驱动架构集成微服务
第二章:数据访问工作
任何微服务的采用如果没有与持久化或数据存储集成都是不完整的。在本章中,我们将探讨 Micronaut 框架中持久化和数据访问的各个方面。我们将首先使用一个 对象关系映射(ORM)框架来与关系型数据库集成。然后,我们将深入探讨使用持久化框架集成数据库。此外,最后,我们将看到一个集成 NoSQL 数据库的示例。为了涵盖这些主题,我们将开发一个宠物诊所应用程序。该应用程序将由以下微服务组成:
-
pet-owner:一个使用 Micronaut 中的对象关系映射(ORM)框架与关系型数据库集成的微服务 -
pet-clinic:一个使用 Micronaut 中的持久化框架与关系型数据库集成的微服务 -
pet-clinic-review:一个与 Micronaut 中的 NoSQL 数据库集成的微服务
到本章结束时,您将具备良好的实践知识,了解如何使用各种类型的持久化框架,以及如何在 Micronaut 框架中将持久化框架与不同类型的数据库(关系型数据库以及 NoSQL 数据库)集成。
技术要求
本章中所有的命令和技术说明都是在 Windows 10 和 macOS 上运行的。本章涵盖的代码示例可在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter02。
需要在开发环境中安装和设置以下工具:
-
Java SDK:版本 13 或更高(我们使用了 Java 14)。
-
Maven:这是可选的,仅当您想使用 Maven 作为构建系统时才需要。然而,我们建议在任何开发机器上设置 Maven。有关下载和安装 Maven 的说明,请参阅
maven.apache.org/download.cgi。 -
开发 IDE:根据您的偏好,可以使用任何基于 Java 的 IDE,但为了编写本章,使用了 IntelliJ。
-
Git:有关下载和安装 Git 的说明,请参阅
git-scm.com/downloads。 -
PostgreSQL:有关下载和安装 PostgreSQL 的说明,请参阅
www.postgresql.org/download/。 -
MongoDB:MongoDB Atlas 提供了一个免费的在线数据库即服务,存储空间高达 512 MB。但是,如果您更喜欢本地数据库,则可以找到下载和安装的说明,网址为
docs.mongodb.com/manual/administration/install-community/。我们为本章的编写使用了本地安装。 -
MongoDB Studio 3T: 在 MongoDB 本地安装中,我们使用了 Studio 3T 的图形用户界面。有关下载和安装 Studio 3T 的说明,请参阅
studio3t.com/download/。
在 Micronaut 框架中集成持久化
要在 Micronaut 框架中展示持久化(数据库)的集成,我们将在pet-clinic应用程序中的三个不同的微服务上进行工作:

图 2.1 – pet-clinic 应用程序中的微服务
Hibernate 和 MyBatis 是关系型数据库的持久化框架,而要集成 NoSQL(MongoDB),我们将使用其原生的同步驱动程序。
在以下章节中,我们将通过分别与各自的微服务进行实际操作来介绍每种集成技术。每个微服务(在本章的范围内)将被组件化为以下类型的组件:
-
实体(Entity): 用于封装 ORM
-
存储库(Repository): 用于封装与底层 Hibernate 框架的交互
-
服务(Service): 用于包含任何业务逻辑以及下游存储库的礼宾服务调用
-
CLI 客户端(CLI client): 将创建-读取-更新-删除(CRUD)请求连接到服务
以下图表描述了这些组件及其相互之间的交互:

图 2.2 – 微服务组件
我们将遵循服务-存储库模式来分离关注点并在微服务内部解耦组件。我们将自下而上地介绍这些组件,首先从实体开始,然后是存储库,最后是服务。在下一节中,我们将探讨使用 ORM 框架集成关系型数据库。
使用 ORM(Hibernate)框架集成关系型数据库
一个 ORM 框架使您能够使用面向对象范式存储、查询或操作数据。它提供了一种面向对象的方法来访问数据库中的数据,换句话说,您可以使用 Java 对象与数据库交互,而不是使用 SQL。
在 Java 中,作为一个标准规范,Java 持久化 API(JPA)规定了以下内容:
-
应该持久化哪些 Java 对象
-
这些对象应该如何持久化
JPA 不是一个框架或工具,但它规定了标准协议并涵盖了持久化的核心概念以及如何持久化。Hibernate 和 EclipseLink 等各种实现框架已采用这些 JPA 标准。我们将使用Hibernate作为我们的 ORM 框架。
要在 Micronaut 框架中亲身体验 Hibernate,我们将工作于小的pet-clinic应用程序,并且对于 Hibernate,我们将专注于pet-owner微服务。以下图表捕捉了pet-owner微服务的模式设计:

图 2.3 – pet-owner 模式
在本质上,在pet-owner模式中,一个所有者可以拥有零个或多个宠物(某种类型的)并且一个宠物可以有零个或多个兽医访问。在下一节中,我们将开始设置pet-owner模式。
在 PostgreSQL 中生成 pet-owner 模式
要生成pet-owner模式,请按照以下说明操作:
-
打开 PostgreSQL 的 PgAdmin 并打开查询工具。
-
以
pet-owner用户、模式和表运行前面的 SQL。 -
最后,运行 SQL 数据将这些表中的某些示例数据导入。
在设置完模式后,我们将专注于在 Micronaut 项目中工作。
为 pet-owner 微服务创建 Micronaut 应用程序
为了生成pet-owner微服务的样板源代码,我们将使用 Micronaut Launch。Micronaut Launch 是一个直观的界面,用于生成样板,并且可以在micronaut.io/launch/访问。一旦打开,此界面将看起来如下截图所示:

图 2.4 – 使用 Micronaut Launch 生成 pet-owner 项目
在 Micronaut Launch 中,我们将选择以下功能(通过点击功能按钮):
-
data-jpa
-
hibernate-jpa
-
jdbc-hikari
-
logback
-
postgres
在指定上述选项后,点击生成项目按钮。系统将下载一个 ZIP 文件。将下载的源代码解压到您的工作区,并在您首选的 IDE 中打开项目。
在pet-owner微服务应用程序中,我们将遵循服务-存储库模式来分离关注点并解耦微服务内的组件。如前所述,我们将采取自下而上的方法来涵盖这些组件,从实体开始,然后探索存储库,最后是服务。
创建实体类
实体是一个@Entity注解。实体类定义通常包含对表中每个列的映射集合。因此,实体对象实例将代表映射表中的一行。
我们将创建一个域包来包含所有实体类。我们将在根包下创建com.packtpub.micronaut.domain包。
为了映射所有者表,我们可以定义一个Owner实体。让我们从映射基本列开始(跳过外键或任何关系):
@Entity
@Table(name = "owners", schema = "petowner")
public class Owner implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "address")
private String address;
@Column(name = "city")
private String city;
@Column(name = "telephone")
private String telephone;
}
在前面的代码片段中,我们为所有者表声明了一个 Owner 实体类。为了映射主键,我们使用 @Id 注解与 @GeneratedValue 一起使用。你可以为映射的列生成 getter 和 setter。同样,我们可以定义其他实体类:Pet 用于宠物表,Visit 用于访问表,PetType 用于类型表。我们将在下一节中查看定义关系。
定义实体之间的关系
使用 Hibernate 框架,我们可以定义以下关系类型:
-
一对一
-
一对多/多对一
-
多对多
让我们看看每种关系类型。
映射一对一关系
在 pet-owner 示例中,我们没有在任何实体之间定义一对一关系。然而,让我们考虑所有来自所有者表的地址信息已经被提取到一个地址表中;结果架构将如下所示:

图 2.5 – 所有者和地址之间的一对一关系
实质上,在先前的架构中,一个所有者将有一个地址。
在 Owner 实体中,为了定义这个关系,我们可以使用 @OneToOne:
@Entity
@Table(name = "owners", schema = "petowner")
public class Owner implements Serializable {
.// ...
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
// ... getters and setters
}
@JoinColumn 将引用连接所有者表中的 address_id 列。
而在 Address 实体中的这个关系将定义如下:
@Entity
@Table(name = "address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
//...
@OneToOne(mappedBy = "address")
private User user;
//... getters and setters
}
你可能会注意到在 Owner 实体中,我们使用 @JoinColumn 定义了一个一对一关系,因为所有者表包含 address_id。然而,在 Address 实体中,我们可以简单地使用 mappedBy 并指向在 Owner 实体中定义的 address 变量。JPA 将在幕后处理这个双向关系。
映射一对多/多对一关系
幸运的是,在 pet-owner 架构中,我们有许多一对一或多对一关系(多对一只是一对多关系的反转)。为了保持专注,让我们考虑所有者和宠物表之间的以下关系:

图 2.6 – 所有者和宠物之间的一对多关系
在 Owner 实体中,前面的多对一关系将定义如下:
@Entity
@Table(name = "owners", schema = "petowner")
public class Owner implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//...
@OneToMany(mappedBy = "owner", cascade =
CascadeType.ALL)
private Set<Pet> pets = new HashSet<>();
//...
在 Pet 实体中,这个关系将映射如下:
@Entity
@Table(name = "pets", schema = "petowner")
public class Pet implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//...
@ManyToOne
@JoinColumn(name = "owner_id")
private Owner owner;
//...
}
再次,如果你注意到在 Pet 实体中,我们使用 @JoinColumn 与 Owner 定义了一个清晰的关系(因为宠物表包含 owner_id),而在 Owner 实体中,我们简单地使用了 mappedBy = "owner"。JPA 将在幕后处理这个双向关系的定义和管理。
映射多对多关系
映射和管理多对多关系稍微复杂一些。在pet-owner模式中,我们没有多对多关系的实例,所以让我们假设两个虚构实体Foo和Bar之间的一种假设关系:

图 2.7 – foo和bar之间的多对多关系
在Foo实体中,上述的多对多关系将被定义为以下内容:
@Entity
@Table(name = "foos")
public class Foo {
// ...
@ManyToMany(cascade = { CascadeType.ALL })
@JoinTable(
name = "foo_bars",
joinColumns = { @JoinColumn(name = "foo_id") },
inverseJoinColumns = { @JoinColumn(name = "bar_id") }
)
Set<Bar> bars = new HashSet<>();
// getters and setters
}
我们使用@JoinTable将关系映射到一个多对多表。joinColumns指的是实体拥有的列,而inverseJoinColumns指的是联合实体中的列。
Bar实体将如下定义这种关系:
@Entity
@Table(name = "bars")
public class Bar {
// ...
@ManyToMany(mappedBy = "bars")
private Set<Foo> foos = new HashSet<>();
// getters and setters
}
就像之前的例子一样,我们使用@JoinTable在Foo实体中清晰地定义了多对多关系,而在Bar实体中我们只是简单地使用了mappedBy。
到目前为止,我们已经介绍了如何在实体类中定义不同类型的关系。接下来,我们将转向如何创建数据访问仓库。
创建数据访问仓库
Hibernate 框架使我们能够非常直观地定义对数据库的 CRUD 访问。对于每个实体,我们将定义一个仓库抽象类,其中每个仓库抽象类将实现JpaRepository。JpaRepository是io.micronaut.data.jpa.repository中定义的一个现成接口,它进一步扩展了CrudRepository和PageableRepository,以声明和定义支持常见 CRUD 操作的标准方法。这减少了语法糖,并使我们免于自己定义这些方法。
首先,我们将创建一个名为com.packtpub.micronaut.repository的新包来包含所有仓库。所有的仓库抽象类看起来都一样,以下是OwnerRepository的示例:
@Repository
public abstract class OwnerRepository implements JpaRepository<Owner, Long> {
@PersistenceContext
private final EntityManager entityManager;
public OwnerRepository(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Transactional
public Owner mergeAndSave(Owner owner) {
owner = entityManager.merge(owner);
return save(owner);
}
}
OwnerRepository使用 Micronaut 的标准@Repository注解,并利用JpaRepository来声明和定义Owner实体的标准 CRUD 方法。
同样,我们也可以为其他实体定义这些抽象类,例如在io.micronaut.data.jpa.repository中的Pet、Visit和PetType。
创建实体的服务
服务将包含任何业务逻辑以及到仓库的下层访问。我们可以为每个实体服务定义标准接口,这将概述服务中支持的基本操作。
为了包含所有服务,我们将创建一个名为com.packtpub.micronaut.service的包。OwnerService接口的声明如下:
public interface OwnerService {
Owner save(Owner owner);
Page<Owner> findAll(Pageable pageable);
Optional<Owner> findOne(Long id);
void delete(Long id);
}
OwnerService接口提供了一个所有服务方法的抽象声明。我们可以在一个具体类中实现所有声明的方法:
@Singleton
@Transactional
public class OwnerServiceImpl implements OwnerService {
private final Logger log =
LoggerFactory.getLogger(OwnerServiceImpl.class);
private final OwnerRepository ownerRepository;
public OwnerServiceImpl(OwnerRepository
ownerRepository) {
this.ownerRepository = ownerRepository;
}
@Override
public Owner save(Owner owner) {
log.debug("Request to save Owner : {}", owner);
return ownerRepository.mergeAndSave(owner);
}
@Override
@ReadOnly
@Transactional
public Page<Owner> findAll(Pageable pageable) {
log.debug("Request to get all Owners");
return ownerRepository.findAll(pageable);
}
@Override
@ReadOnly
@Transactional
public Optional<Owner> findOne(Long id) {
log.debug("Request to get Owner : {}", id);
return ownerRepository.findById(id);
}
@Override
public void delete(Long id) {
log.debug("Request to delete Owner : {}", id);
ownerRepository.deleteById(id);
}
}
服务方法定义基本上是将执行委托给下游仓库。我们将重复声明服务接口和定义其余实体(Pet、Visit 和 PetType)的具体服务类。
在下一节中,我们将专注于创建一个小型命令行工具,以在 pet-owner 数据库中执行常见的 CRUD 操作。
执行基本的 CRUD 操作
为了展示实体的基本 CRUD 操作,我们将创建一个简单的实用工具。我们可以在 com.packtpub.micronaut.utils 新包中定义 PetOwnerCliClient:
@Singleton
public class PetOwnerCliClient {
private final OwnerService ownerService;
private final PetService petService;
private final VisitService visitService;
private final PetTypeService petTypeService;
public PetOwnerCliClient(OwnerService ownerService,
PetService petService,
VisitService visitService,
PetTypeService petTypeService) {
this.ownerService = ownerService;
this.petService = petService;
this.visitService = visitService;
this.petTypeService = petTypeService;
}
// methods for performing CRUD operations...
}
此实用工具将通过构造函数方法注入所有服务。在此实用工具中进行的任何 CRUD 调用都将使用注入的服务执行。
执行读取/检索操作
我们可以在 PetOwnerCliClient 中定义一个简单的实用方法,该方法可以调用 OwnerService 来检索所有所有者。此外,由于 Owner 有多个宠物,每个宠物可以有多个访问记录,因此检索一个所有者将检索 pet-owner 模式中的几乎所有内容:
protected void performFindAll() {
Page<Owner> pOwners =
ownerService.findAll(Pageable.unpaged());
… iterate through paged content
}
performFindAll() 将检索所有所有者和他们的宠物(包括宠物访问记录)。
执行保存操作
要保存一个拥有宠物和访问记录的所有者,我们可以在 PetOwnerCliClient 中定义一个方法:
protected Owner performSave() {
Owner owner = initOwner();
return ownerService.save(owner);
}
private Owner initOwner() {Owner owner = new Owner();
owner.setFirstName("Foo");
owner.setLastName("Bar");
owner.setCity("Toronto");
owner.setAddress("404 Adelaide St W");
owner.setTelephone("647000999");
Pet pet = new Pet();
pet.setType(petTypeService.findAll(Pageable.unpaged()).getContent().get(1));
pet.setName("Baz");
pet.setBirthDate(LocalDate.of(2010, 12, 12));
pet.setOwner(owner);
Visit visit = new Visit();
visit.setVisitDate(LocalDate.now());
visit.setDescription("Breathing issues");
visit.setPet(pet);
return owner;
}
initOwner() 初始化一个拥有宠物和宠物访问记录的所有者,它将被 performSave() 用于调用下游服务类方法以保存此所有者。
执行删除操作
在 PetOwnerCliClient 中,我们将定义一个删除方法来删除一个所有者(包括他们的宠物和访问记录):
protected void performDelete(Owner owner) {
/** delete owner pets and their visits */
Set<Pet> pets = owner.getPets();
if (CollectionUtils.isNotEmpty(pets)) {
for (Pet pet : pets) {
Set<Visit> visits = pet.getVisits();
if (CollectionUtils.isNotEmpty(visits)) {
for (Visit visit : visits) {
visitService.delete(visit.getId());
}
}
petService.delete(pet.getId());
}
}
ownerService.delete(owner.getId());
}
performDelete() 首先遍历宠物和宠物访问记录;删除宠物访问记录和宠物后,它最终将调用删除所有者的操作。
总结
要执行 PetOwnerCliClient CRUD 操作,我们将在 Application.java 中添加以下代码逻辑:
@Singleton
public class Application {
private final PetOwnerCliClient petOwnerCliClient;
public Application(PetOwnerCliClient petOwnerCliClient) {
this.petOwnerCliClient = petOwnerCliClient;
}
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
@EventListener
void init(StartupEvent event) {
petOwnerCliClient.performDatabaseOperations();
}
}
然后,当我们运行应用程序时,上述 @EventListener 将调用 PetOwnerCliClient 来执行数据库操作。
在本节中,通过 pet-owner 微服务,我们介绍了如何将基于 Micronaut 的微服务与关系型数据库集成。我们还讨论了如何定义实体、仓库和服务,最后展示了 CRUD 操作。在下一节中,我们将探讨如何使用另一种持久化框架(MyBatis)与关系型数据库集成。
使用持久化(MyBatis)框架与关系型数据库集成
MyBatis 是一个 Java 持久化框架。与 Hibernate(一个 ORM 框架)不同,MyBatis 不支持直接将 Java 对象映射到数据库,而是将 Java 方法映射到 SQL 语句。
MyBatis 通常用于迁移或转换项目中,其中已经存在遗留数据库(s)。由于许多表、视图和其他数据对象已经在数据库中定义并使用,因此将这些表/视图定义重构和规范化以直接映射到 Java 对象(使用 ORM 框架)可能不是一个理想的情况。MyBatis 提供了一种将 Java 方法映射到 SQL 语句的理想方式。这些管理任何 CRUD 访问的 SQL 语句,使用 MyBatis 注解定义在 XML 映射器或 POJO 映射器中。
此外,由于 ORM 框架(如 Hibernate)会自行管理子实体并完全隐藏 SQL 部分,一些开发者更喜欢控制与 SQL 的交互。因此,MyBatis 可以作为首选的持久化框架介入。
Micronaut 支持通过 MyBatis 与关系数据库集成。为了展示这种集成,我们将工作在另一个微服务上,该微服务将管理宠物诊所应用程序的兽医方面。此微服务将集成以下架构:

图 2.8 – 宠物诊所架构
实际上,一位兽医可以有许多专长,而一种专长可以属于多位兽医。在下一节中,我们将开始设置pet-clinic架构。
在 PostgreSQL 中生成 pet-clinic 架构
要生成pet-clinic架构,请遵循以下说明:
-
打开 PostgreSQL 的 PgAdmin 并打开查询工具。
-
以
pet-owner用户、架构和表运行前面的 SQL。 -
最后,运行数据 SQL 将这些示例数据导入这些表中。
在设置架构完成后,我们将把注意力转向 Micronaut 项目。
为 pet-clinic 微服务生成 Micronaut 应用程序
为了生成pet-clinic微服务的样板源代码,我们将使用 Micronaut Launch:

图 2.9 – 使用 Micronaut Launch 生成 pet-clinic 项目
在 Micronaut Launch 中,我们将选择以下功能(通过点击功能按钮):
-
jdbc-hikari
-
logback
-
postgres
在指定上述选项后,点击生成项目按钮。一个 ZIP 文件将被下载到您的系统上。
将下载的源代码解压到您的工作区,并在您首选的 IDE 中打开项目。一旦项目在 IDE 中打开,请在pom.xml(或gradle build)中添加以下依赖项以用于 MyBatis:
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
这个依赖对于我们在 Micronaut 中与 pet-clinic 模式集成的核心。
与服务-仓库模式一致,我们将从下往上探索 MyBatis 集成。首先,我们将定义实体,然后是仓库,最后我们将处理服务。
定义 MyBatis 工厂
为了执行各种 SQL 语句,MyBatis 在运行时需要一个 SqlSessionFactory 对象。我们将首先添加一个包 - com.packtpub.micronaut.config。向这个新创建的包中添加以下类:
@Factory
public class MybatisFactory {
private final DataSource dataSource;
public MybatisFactory(DataSource dataSource) {
this.dataSource = dataSource;
}
@Context
public SqlSessionFactory sqlSessionFactory() {
TransactionFactory transactionFactory = new
JdbcTransactionFactory();
Environment environment = new Environment(
"pet-clinic", transactionFactory, dataSource);
Configuration configuration = new
Configuration(environment);
configuration.addMappers("com.packtpub.micronaut.repository");
return new
SqlSessionFactoryBuilder().build(configuration);
}
}
使用 application.yml 中的标准属性,Micronaut 将定义一个基于 Hikari 的数据源,该数据源将被注入以定义 SqlSessionFactory。在定义环境时,你可以选择任何名称(正如我们给出的 pet-clinic)。
创建实体类
与 Hibernate 实体类似,MyBatis 实体本质上定义了一个 Java 类,以将上游 Java 类与下游 SQL 交互(在 XML 或 Java 映射器中定义)集成。然而,一个细微的区别是,任何 MyBatis 实体都不会包含任何映射逻辑。
我们将添加一个 com.packtpub.micronaut.domain 包来包含域实体。添加一个表示 Specialty 的实体:
@Introspected
public class Specialty implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull
private Long id;
private String name;
// ... getters and setters
}
同样,我们可以为兽医表定义一个实体:
@Introspected
public class Vet implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull
private Long id;
private String firstName;
private String lastName;
private Set<Specialty> specialties = new HashSet<>();
// ... getters and setters
}
你可以注意到这两个实体都没有任何映射逻辑来映射到特殊或兽医表。在下一节中,我们将关注如何在 MyBatis 中创建数据访问仓库。
定义实体的映射器(仓库)
对于 Vet 和 Specialty 实体,我们需要定义 MyBatis 映射器。MyBatis 通过这些映射器与下游数据库交互。与典型的 Java 应用程序相比,MyBatis 映射器是数据访问仓库。我们将添加一个 com.packtpub.micronaut.repository 包来包含所有这些仓库。
在此包下,我们将添加 SpecialtyRepository 接口:
/**
* Mybatis mapper for {@link Specialty}.
*/
public interface SpecialtyRepository {
@Select("SELECT * FROM petclinic.specialties")
Collection<Specialty> findAll() throws Exception;
@Select("SELECT * FROM petclinic.specialties WHERE id =
#{id}")
Specialty findById(@Param("id") Long id) throws
Exception;
@Select("SELECT * FROM petclinic.specialties WHERE
UPPER(name) = #{name}")
Specialty findByName(@Param("name") String name) throws
Exception;
@Select(
{
"INSERT INTO petclinic.specialties(id, name)\n" +
"VALUES (COALESCE(#{id}, (select
nextval('petclinic.specialties_id_seq'))),
#{name})\n" +
"ON CONFLICT (id)\n" +
"DO UPDATE SET name = #{name} \n" +
"WHERE petclinic.specialties.id = #{id}\n" +
"RETURNING id"
}
)
@Options(flushCache = Options.FlushCachePolicy.TRUE)
Long save(Specialty specialty) throws Exception;
@Delete("DELETE FROM petclinic.specialties WHERE id =
#{id}")
void deleteById(@Param("id") Long id) throws Exception;
@Select({
"SELECT DISTINCT id, name FROM
petclinic.specialties WHERE id IN(",
"SELECT specialty_id FROM petclinic.vet_specialties
WHERE vet_id = #{vetId}",
")"
})
Set<Specialty> findByVetId(@Param("vetId") Long vetId)
throws Exception;
}
在前面的代码片段中,我们可以看到所有的实际 SQL 语句,然后绑定到 Java 方法。因此,每当任何上游调用者调用上述任何方法时,MyBatis 将执行相应的映射 SQL。同样,我们将定义 VetRepository 来管理对兽医表的访问。
与 Hibernate(为抽象仓库类提供具体实现)不同,在 MyBatis 中,我们必须为仓库提供具体实现。我们将将这些实现添加到 com.packtpub.micronaut.repository.impl。
SpecialtyRepository 的具体实现可以定义如下:
@Singleton
public class SpecialtyRepositoryImpl implements SpecialtyRepository {
... SqlSessionFactory injection …
private SpecialtyRepository
getSpecialtyRepository(SqlSession sqlSession) {
return
sqlSession.getMapper(SpecialtyRepository.class);
}
@Override
public Collection<Specialty> findAll() throws Exception {
try (SqlSession sqlSession =
sqlSessionFactory.openSession()) {
return
getSpecialtyRepository(sqlSession).findAll();
}
}
@Override
public Specialty findById(Long id) throws Exception {
try (SqlSession sqlSession =
sqlSessionFactory.openSession()) {
return getSpecialtyRepository
(sqlSession).findById(id);
}
}
@Override
public Specialty findByName(String name) throws
Exception {
try (SqlSession sqlSession =
sqlSessionFactory.openSession()) {
return getSpecialtyRepository
(sqlSession).findByName(name);
}
}
@Override
public Long save(Specialty specialty) throws Exception {
Long specialtyId;
try (SqlSession sqlSession =
sqlSessionFactory.openSession()) {
specialtyId = getSpecialtyRepository
(sqlSession).save(specialty);
sqlSession.commit();
}
return specialtyId;
}
@Override
public void deleteById(Long id) throws Exception {
try (SqlSession sqlSession =
sqlSessionFactory.openSession()) {
getSpecialtyRepository(sqlSession).deleteById
(id);
sqlSession.commit();
}
}
@Override
public Set<Specialty> findByVetId(Long vetId) throws
Exception {
try (SqlSession sqlSession =
sqlSessionFactory.openSession()) {
return getSpecialtyRepository
(sqlSession).findByVetId(vetId);
}
}
}
所有具体方法定义都使用 SqlSessionFactory 获取 SqlSession 实例。然后 getSpecialtyRepository() 方法将返回使用此 SqlSession 实例的 MyBatis 映射器。同样,可以定义 VetRepositoryImpl 来为 VetRepository 提供具体实现。
在下一节中,我们将为这些我们刚刚定义的仓库创建上游服务类。
创建实体服务
服务将包含任何业务逻辑以及到先前存储库的下游访问。我们可以为每个实体服务定义标准接口,这将概述服务中支持的基本操作。
为了包含所有服务,首先,我们将创建一个名为com.packtpub.micronaut.service的包。我们可以为SpecialtyService声明一个接口以抽象我们的基本结构:
public interface SpecialtyService {
Specialty save(Specialty specialty) throws Exception;
Collection<Specialty> findAll() throws Exception;
Optional<Specialty> findOne(Long id) throws Exception;
void delete(Long id) throws Exception;
}
对于这些基本方法,我们需要提供具体实现。
我们可以在服务包下添加一个名为com.packtpub.micronaut.service.impl的包。SpecialtyService的具体实现将定义如下:
@Singleton
public class SpecialtyServiceImpl implements SpecialtyService {
private final Logger log =
LoggerFactory.getLogger(SpecialtyServiceImpl.class);
private final SpecialtyRepository specialtyRepository;
public SpecialtyServiceImpl(SpecialtyRepository
specialtyRepository) {
this.specialtyRepository = specialtyRepository;
}
@Override
public Specialty save(Specialty specialty) throws
Exception {
log.debug("Request to save Specialty : {}",
specialty);
Long specialtyId =
specialtyRepository.save(specialty);
return specialtyRepository.findById(specialtyId);
}
@Override
public Collection<Specialty> findAll() throws Exception {
log.debug("Request to get all Specialties");
return specialtyRepository.findAll();
}
@Override
public Optional<Specialty> findOne(Long id) throws
Exception {
log.debug("Request to get Specialty : {}", id);
return Optional.ofNullable
(specialtyRepository.findById(id));
}
@Override
public void delete(Long id) throws Exception {
log.debug("Request to delete Specialty : {}", id);
specialtyRepository.deleteById(id);
}
}
具体服务方法本质上是简单的,并代理对下游存储库方法的调用。我们将为Vet添加一个类似的服务接口和具体实现,如VetService和VetServiceImpl。
在下一节中,我们将专注于创建一个小型命令行实用程序,以在pet-clinic数据库中执行常见的 CRUD 操作。
执行基本的 CRUD 操作
要使用上一节中定义的服务和存储库,并执行实体的基本 CRUD 操作,我们可以创建一个简单的实用程序。我们可以在com.packtpub.micronaut.utils包下创建一个新的包来定义PetClinicCliClient:
@Singleton
public class PetClinicCliClient {
private final VetService vetService;
private final SpecialtyService specialtyService;
public PetClinicCliClient(VetService vetService,
SpecialtyService
specialtyService) {
this.vetService = vetService;
this.specialtyService = specialtyService;
}
// …
}
PetClinicCliClient使用构造方法注入VetService和SpecialtyService。这些服务将用于对vets、specialties和vet_specialties表执行各种数据库操作。
执行读取/检索操作
我们可以在PetClinicCliClient中定义一个简单的实用方法,该方法可以调用VetService来检索所有兽医。此外,由于一个兽医可以有多个专业,检索一个兽医将同时从专业表中检索:
protected void performFindAll() {
List<Vet> vets;
try {
vets = (List<Vet>) vetService.findAll();
… iterate on vets
} catch (Exception e) {
log.error("Exception: {}", e.toString());
}
}
performFindAll()方法从数据库中检索所有兽医及其专业。
执行保存操作
我们将在PetClinicCliClient中保存一个具有专业的兽医:
protected Vet performSave() {
Vet vet = initVet();
Vet savedVet = null;
try {
savedVet = vetService.save(vet);
} catch (Exception e) {
log.error("Exception: {}", e.toString());
}
return savedVet;
}
private Vet initVet() {
Vet vet = new Vet();
vet.setFirstName("Foo");
vet.setLastName("Bar");
Specialty specialty = new Specialty();
specialty.setName("Baz");
vet.getSpecialties().add(specialty);
return vet;
}
initVet()方法初始化一个新的具有专业的兽医,然后由performSave()方法用来将此对象持久化到数据库表中。
执行删除操作
在PetClinicCliClient中,我们将定义一个删除方法来删除一个兽医(及其专业):
protected void performDelete(Vet vet) {
try {
vetService.delete(vet.getId());
} catch (Exception e) {
log.error("Exception: {}", e.toString());
}
}
performDelete()方法将调用委托给VetService,然后VetService调用存储库,最终从数据库中删除兽医。
总结
为了执行PetClinicCliClient的 CRUD 操作,我们将向Application.java添加以下代码逻辑:
@Singleton
public class Application {
private final PetClinicCliClient petClinicCliClient;
public Application(PetClinicCliClient
petClinicCliClient) {
this.petClinicCliClient = petClinicCliClient;
}
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
@EventListener
void init(StartupEvent event) {
petClinicCliClient.performDatabaseOperations();
}
}
当我们运行我们的应用程序时,上述的@EventListener将调用PetClinicCliClient来执行各种数据库操作。
在本节中,我们使用 MyBatis 与关系型数据库进行了集成。我们介绍了如何定义实体、存储库和服务,最后通过一个实用程序展示了基本的 CRUD 操作。在下一节中,我们将探索在 Micronaut 框架中与 NoSQL 数据库的集成。
与 NoSQL 数据库(MongoDB)集成
MongoDB 是一个基于文档的数据库,它以 JSON 或 BSON 格式存储数据。数据以键值对的形式存储,类似于 JSON 对象。MongoDB 采用横向扩展的设计,当数据量和结构敏捷且快速增长时,建议使用它。与关系型数据库相比,有几个关键术语:
-
数据库: MongoDB 中的数据库与关系型数据库中的数据库非常相似。
-
表: 一个(文档的)集合与关系型数据库中的表非常相似。
-
行: 一个 BSON 或 JSON 文档将类似于关系型数据库中的一行。
为了进行实际操作,我们将继续使用 pet-clinic 应用程序并添加一个新的微服务,即 pet-clinic-reviews。这个微服务将负责管理兽医评论。由于评论可能会快速增长,存储评论的模式可能会改变,因此我们更倾向于将此数据存储在 MongoDB 中:
{
"_id" : ObjectId("5f485523d0cfa84e00963fe4"),
"reviewId" : "0ee19b1c-ec8e-11ea-adc1-0242ac120002",
"rating" : 4.0,
"comment" : "Good vet in the area",
"vetId" : 1.0,
"dateAdded" : ISODate("2020-08-28T00:51:47.922+0000")
}
前面的 JSON 文档描述了一个存储在 MongoDB 中的评论。
在下一节中,我们将开始设置 MongoDB 中的 pet-clinic-reviews 模式。
在 MongoDB 中创建 vet-reviews 集合
对于 pet-clinic-reviews 微服务,我们将在 MongoDB 实例中创建一个数据库和集合:
-
打开 Studio 3T 或任何其他首选的 MongoDB GUI。
-
连接到您的本地实例或任何其他首选实例(例如 MongoDB Atlas)。
-
在
pet-clinic-reviews下。 -
在上述数据库中,我们将创建一个新的集合:
vet-reviews。 -
要将一些示例数据导入此集合,请使用导入选项从
github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter02/micronaut-petclinic/pet-clinic-reviews/src/main/resources/db导入数据(CSV 或 JSON)。
在设置完模式后,我们将把注意力转向处理 Micronaut 项目。
为 pet-clinic-reviews 微服务生成 Micronaut 应用程序
为了生成 pet-clinic-reviews 微服务的样板代码,我们将使用 Micronaut Launch。它是一个直观的界面,用于生成样板代码:

图 2.10 – 使用 Micronaut Launch 生成 pet-clinic-reviews 项目
在 Micronaut Launch 中,我们将选择以下功能(通过点击 功能 按钮):
-
mongodb-sync(对 MongoDB 的同步访问)
-
logback
在指定上述选项后,点击 生成项目 按钮。系统将下载一个 ZIP 文件。将下载的源代码解压到您的工作区,并在您首选的 IDE 中打开项目。
在接下来的章节中,我们将深入了解如何将 pet-clinic-reviews 微服务与 MongoDB 集成。
在 Micronaut 中配置 MongoDB
在新创建的 pet-clinic-reviews 项目中,我们需要更新 application.yml 中的 URI 以指向 MongoDB 的正确实例。此外,我们还将定义两个新的自定义属性 - databaseName 和 collectionName:
micronaut:
application:
name: Pet Clinic Reviews
mongodb:
uri: mongodb://mongodb:mongodb@localhost:27017/pet-clinic-reviews
databaseName: pet-clinic-reviews
collectionName: vet-reviews
要在整个项目中使用上述数据库和集合,我们可以在一个新包中定义一个配置类 - com.packtpub.micronaut.config:
public class MongoConfiguration {
@Value("${mongodb.databaseName}")
private String databaseName;
@Value("${mongodb.collectionName}")
private String collectionName;
public String getDatabaseName() {
return databaseName;
}
public String getCollectionName() {
return collectionName;
}
}
这将使得在整个项目中轻松访问 databaseName 和 collectionName 属性,我们就不需要使用 @Value。此外,由于这些属性是只读的,我们只为这些属性定义了获取器。
接下来,我们将创建一个实体类来定义兽医评论对象。
创建实体类
在根级别,添加一个名为 com.packtpub.micronaut.domain 的新包。这个包将包含兽医评论的领域/实体。我们可以定义以下 POJO 来表示兽医评论:
public class VetReview {
private String reviewId;
private Long vetId;
private Double rating;
private String comment;
private LocalDate dateAdded;
@BsonCreator
@JsonCreator
public VetReview(
@JsonProperty("reviewId")
@BsonProperty("reviewId") String reviewId,
@JsonProperty("vetId")
@BsonProperty("vetId") Long vetId,
@JsonProperty("rating")
@BsonProperty("rating") Double rating,
@JsonProperty("comment")
@BsonProperty("comment") String comment,
@JsonProperty("dateAdded")
@BsonProperty("dateAdded") LocalDate dateAdded) {
this.reviewId = reviewId;
this.vetId = vetId;
this.rating = rating;
this.comment = comment;
this.dateAdded = dateAdded;
}
// ... getters and setters
}
@BsonCreator 和 @JsonCreator 通过类构造函数映射 VetReview 对象到相应的 BSON 或 JSON 文档。这在从 MongoDB 检索或存储文档并将其映射到 Java 对象时非常有用。
接下来,我们将关注如何为 VetReview 创建数据访问仓库。
创建数据访问仓库
为了管理对 vet-reviews 集合的访问,我们将创建一个仓库。我们可以在 com.packtpub.micronaut.repository 中添加这个仓库类。
为了抽象出仓库将公开的所有方法,我们可以声明一个接口:
public interface VetReviewRepository {
List<VetReview> findAll();
VetReview findByReviewId(String reviewId);
VetReview save(VetReview vetReview);
void delete(String reviewId);
}
VetReviewRepository 概述了在此仓库中支持的基本操作。
我们需要在 com.packtpub.micronaut.repository.impl 包中为 VetReviewRepository 接口提供一个具体实现:
@Singleton
public class VetReviewRepositoryImpl implements VetReviewRepository {
private final MongoClient mongoClient;
private final MongoConfiguration mongoConfiguration;
public VetReviewRepositoryImpl(MongoClient mongoClient,
MongoConfiguration mongoConfiguration) {
this.mongoClient = mongoClient;
this.mongoConfiguration = mongoConfiguration;
}
@Override
public List<VetReview> findAll() {
List<VetReview> vetReviews = new ArrayList<>();
getCollection().find().forEach(vetReview -> {
vetReviews.add(vetReview);
});
return vetReviews;
}
@Override
public VetReview findByReviewId(String reviewId) {
return getCollection().find(eq("reviewId",
reviewId)).first();
}
@Override
public VetReview save(VetReview vetReview) {
getCollection().insertOne(vetReview).
getInsertedId();
return findByReviewId(vetReview.getReviewId());
}
@Override
public void delete(String reviewId) {
getCollection().deleteOne(eq("reviewId",
reviewId));
}
private MongoCollection<VetReview> getCollection() {
return mongoClient
.getDatabase(mongoConfiguration.getDatabaseName())
.getCollection(mongoConfiguration.getCollectionName(), VetReview.class);
}
}
在这个具体类中的 getCollection() 方法将从 MongoDB 获取数据库和集合。将数据保存到这个集合或从其中删除是直接了当的,如前所述。要基于某个条件查找,我们可以使用 find() 方法配合 eq()。eq() 是在 MongoDB 驱动程序中定义的一个 BSON 过滤器。
创建实体服务
一个服务将封装任何业务逻辑以及到 MongoDB 中 vet-reviews 集合(在 MongoDB 中)的下游访问。我们可以在服务中声明一个接口来抽象核心方法。为了包含一个服务,我们首先应该在根包下创建一个名为 com.packtpub.micronaut.service 的包。
然后,我们可以声明 VetReviewService 接口如下:
public interface VetReviewService {
List<VetReview> findAll();
VetReview findByReviewId(String reviewId);
VetReview save(VetReview vetReview);
void delete(String reviewId);
}
VetReviewService 接口将声明由该服务支持的基本方法。为了为这些方法提供具体实现,我们将在 com.packtpub.micronaut.service.impl 包下创建一个 VetServiceImpl 类:
@Singleton
public class VetReviewServiceImpl implements VetReviewService {
private final VetReviewRepository vetReviewRepository;
public VetReviewServiceImpl(VetReviewRepository
vetReviewRepository) {
this.vetReviewRepository = vetReviewRepository;
}
@Override
public List<VetReview> findAll() {
return vetReviewRepository.findAll();
}
@Override
public VetReview findByReviewId(String reviewId) {
return
vetReviewRepository.findByReviewId(reviewId);
}
@Override
public VetReview save(VetReview vetReview) {
return vetReviewRepository.save(vetReview);
}
@Override
public void delete(String reviewId) {
vetReviewRepository.delete(reviewId);
}
}
这些方法的具体实现基本上是将调用委托给 VetReviewRepository。然而,为了满足未来的需求,这些方法可以被扩展以包含任何业务逻辑,例如数据验证或数据转换。
在下一节中,我们将专注于创建一个小的命令行工具,以在 pet-clinic-reviews 集合上执行常见的 CRUD 操作。
执行基本的 CRUD 操作
为了在 VetReview 实体上执行基本的 CRUD 操作,我们可以创建一个简单的实用工具。我们可以在 com.packtpub.micronaut.utils 这个新包中定义 PetClinicReviewCliClient:
@Singleton
@Requires(beans = MongoClient.class)
public class PetClinicReviewCliClient {
private final Logger log = LoggerFactory.getLogger
(PetClinicReviewCliClient.class);
private final VetReviewService vetReviewService;
public PetClinicReviewCliClient(VetReviewService
vetReviewService) {
this.vetReviewService = vetReviewService;
}
}
此实用工具将通过构造函数注入 VetReviewService。我们将通过此服务进行 CRUD 调用。
执行读取/获取操作
我们可以在 PetClinicReviewCliClient 中定义一个简单的实用方法,该方法可以调用 VetReviewService 来获取所有兽医评论或特定的兽医评论:
protected void performFindAll() {
List<VetReview> vetReviews =
this.vetReviewService.findAll();
… iterate over vetReviews
}
protected void performFindByReviewId(String reviewId) {
VetReview vetReview =
vetReviewService.findByReviewId(reviewId);
log.info("Review: {}", vetReview);
}
performFindAll() 将获取集合中的所有兽医评论,而 performFindByReviewId() 将通过 reviewId 获取特定的评论。
执行保存操作
我们将通过 PetClinicReviewCliClient 中的方法保存一个兽医评论:
protected VetReview performSave() {
VetReview vetReview = new VetReview(
UUID.randomUUID().toString(),
1L,
3.5,
"Good experience",
LocalDate.now());
return vetReviewService.save(vetReview);
}
虽然我们在 Java 中定义了一个兽医评论对象,但 VetReview 实体中的构造函数将负责将此对象映射到 BsonDocument,并且 VetReviewRepository 中的下游代码将保存此文档到 vet-reviews 集合。
执行删除操作
在 PetClinicReviewCliClient 中,我们将定义一个删除方法来 delete 一个兽医评论:
protected void performDelete(VetReview vetReview) {
vetReviewService.delete(vetReview.getReviewId());
}
这是一个小方法,它将调用委托给 VetReviewService 以执行删除。VetReviewService 进一步调用存储库以从集合中删除指定的兽医评论。
总结
在 Application.java 中,我们可以将 PetClinicReviewCliClient 作为启动事件调用,然后在该 vet-reviews 集合上展示基本的 CRUD 操作:
@Singleton
public class Application {
private final PetClinicReviewCliClient
petClinicReviewCliClient;
public Application(PetClinicReviewCliClient
petClinicReviewCliClient) {
this.petClinicReviewCliClient =
petClinicReviewCliClient;
}
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
@EventListener
void init(StartupEvent event) {
petClinicReviewCliClient.
performDatabaseOperations();
}
}
当我们启动应用程序时,上述 @EventListener 将调用 PetClinicReviewCliClient 来执行数据库操作。
在本节中,我们介绍了如何将 Micronaut 应用程序与 MongoDB 集成。我们定义了实体、存储库和服务以访问 MongoDB 数据库。最后,我们通过一个轻量级实用工具展示了 CRUD 操作。
摘要
在本章中,我们探讨了将 Micronaut 应用程序与关系型数据库以及 NoSQL 数据库集成的各个方面。我们探索了不同的持久化集成方式,即使用 ORM(Hibernate)、持久化框架(MyBatis)或基于驱动程序的框架(MongoDB Sync)。在每种技术中,我们都深入探讨了如何定义实体、关系、存储库和服务。每个微服务定义了一个简单的命令行工具来展示常见的 CRUD 操作。
本章为我们提供了覆盖 Micronaut 中几乎所有数据访问方面的技能。在本书的其余部分,我们将通过进行更多动手练习和涵盖 Micronaut 框架的其他方面来进一步探索和使用这些技能和知识。
在下一章中,我们将处理pet-clinic应用程序的 Web 层,在所有微服务中定义各种 REST 端点。
问题
-
什么是对象关系映射(ORM)框架?
-
什么是 Hibernate?
-
你如何在 Micronaut 中使用 Hibernate 定义一个实体?
-
你如何在 Micronaut 中使用 Hibernate 映射一对一关系?
-
你如何在 Micronaut 中使用 Hibernate 映射一对一或一对多关系?
-
你如何在 Micronaut 中使用 Hibernate 映射多对多关系?
-
你如何在 Micronaut 中使用 Hibernate 执行 CRUD 操作?
-
你如何在 Micronaut 中使用 MyBatis 与关系型数据库集成?
-
你如何在 Micronaut 中定义 MyBatis 映射器?
-
你如何在 Micronaut 中使用 MyBatis 执行 CRUD 操作?
-
你如何在 Micronaut 中与 NoSQL(MongoDB)数据库集成?
-
你如何在 Micronaut 中在 MongoDB 中执行 CRUD 操作?
第三章: 在 RESTful Web 服务上工作
在任何微服务开发中,核心方面之一是微服务如何与外部世界接口。宠物诊所应用程序。对于这些端点的出站和入站有效载荷,我们将使用 数据传输对象(DTOs)与 MapStruct 一起映射 DTO 到/从实体。对于实际操作,我们将努力为以下微服务添加 RESTful 端点:
-
宠物主人: 通过实际操作为宠物主人模式对象添加 HTTP GET、POST、PUT 和 DELETE 端点 -
宠物诊所: 通过实际操作为宠物诊所模式对象添加 HTTP GET、POST、PUT 和 DELETE 端点 -
宠物诊所评论: 通过实际操作为宠物诊所评论集合添加 HTTP GET、POST、PUT 和 DELETE 端点
在本章中,我们将深入研究以下主题,以处理上述微服务:
-
在 Micronaut 框架中处理 RESTful 微服务
-
使用 DTOs 作为端点有效载荷
-
为微服务创建 RESTful 端点
-
使用 Micronaut 的 HTTP 服务器 API
-
使用 Micronaut 的 HTTP 客户端 API
到本章结束时,你将具备在 Micronaut 框架中处理 RESTful Web 服务的实际知识。这些知识对于在微服务的网络层工作非常重要。此外,我们还将探讨在 Micronaut 框架中利用 HTTP 服务器对象和客户端对象。
技术要求
本章中所有命令和技术说明均在 Windows 10 和 macOS 上运行。本章涵盖的代码示例可在本书的 GitHub 仓库 github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter03 中找到。
在开发环境中需要安装和设置以下工具:
-
Java SDK: 版本 8 或更高(我们使用了 Java 14)。
-
Maven: 这不是必需的,只有当你想使用 Maven 作为构建系统时才需要。然而,我们建议在任何开发机器上设置 Maven。下载和安装 Maven 的说明可以在
maven.apache.org/download.cgi找到。 -
开发 IDE: 根据您的偏好,可以使用任何基于 Java 的 IDE,但为了编写本章,使用了 IntelliJ。
-
Git: 下载和安装 Git 的说明可以在
git-scm.com/downloads找到。 -
PostgreSQL: 下载和安装 PostgreSQL 的说明可以在
www.postgresql.org/download/找到。 -
MongoDB:MongoDB Atlas 提供了一个免费的在线数据库即服务,存储空间高达 512 MB。然而,如果您更喜欢本地数据库,则可以在
docs.mongodb.com/manual/administration/install-community/找到下载和安装的说明。我们为编写本章使用了本地安装。 -
REST 客户端:可以使用任何 HTTP REST 客户端。我们使用了 Advanced REST Client Chrome 插件。
在 Micronaut 框架中开发 RESTful 微服务
为了了解 Micronaut 框架中的 RESTful 微服务,我们将继续在 pet-clinic 应用程序上工作。以下表格总结了我们将对 pet-clinic 应用程序中的每个微服务进行的更改:

表 3.1 – pet-clinic 应用程序中的微服务
在每个微服务中,我们将亲自动手添加 HTTP 端点以对它们拥有的数据对象执行 CRUD 操作。
在我们的动手实践中,我们将关注以下内容:
-
DTO:如何使用 DTO 封装 RESTful 端点的出入有效载荷
-
服务:服务如何与数据库存储库协调以处理任何控制器请求
-
控制器:如何在 Micronaut 框架中为外部世界提供标准的 RESTful 接口
在第二章的数据访问工作中添加到 图 2.2,本章中每个微服务内的组件如下:

图 3.1 – 微服务组件
我们将继续通过遵循控制器-服务-存储库模式来分离关注点。对于服务和控制器之间的通信,我们将探讨 DTO。在涵盖 DTO、服务和最终控制器时,我们将采用自下而上的方式。DTO、映射器、服务和控制器遵循相同的方法,因此,为了使我们的讨论保持专注,我们将以 pet-owner 微服务为目标。
在下一节中,我们的焦点将是 DTO。
使用 DTO 进行端点有效载荷
DTO 模式起源于企业应用架构,本质上,数据对象聚合和封装要传输的数据。由于在微服务架构中,终端客户端可能需要从不同的持久化资源(如发票数据和用户数据)中获取不同的数据,因此 DTO 模式在限制对微服务的调用以获取所需的数据投影方面非常有效。DTO 也被称为组装对象,因为它们从多个实体类中组装数据。
在本节中,我们将探讨如何实现和映射(到实体)DTO。在后面的章节中,我们将进一步深入探讨使用 DTO 作为在微服务之间有效传输数据的一种机制。我们还将研究 DTO 如何通过组装数据来帮助减少对微服务的调用次数。
实现 DTO
要实现一个 DTO,我们将开始定义一个宠物主人的 DTO 类。
打开您首选 IDE 中的 pet-owner 微服务项目(在 第二章,处理数据访问 中创建),将 com.packtpub.micronaut.service.dto 包添加到包含所有 DTO 的包中。我们可以定义 OwnerDTO 如下:
@Introspected
public class OwnerDTO implements Serializable {
private Long id;
private String firstName;
private String lastName;
private String address;
private String city;
private String telephone;
private Set<PetDTO> pets = new HashSet<>();
… getters and setters
}
OwnerDTO 实现了 Serializable 标记接口,以标记 DTO 是可序列化的。此外,在继续我们之前关于组装模式的讨论中,OwnerDTO 还将包含一组 PetDTO 实例。
按照类似的 POJO 模型,我们可以在 pet-owner 微服务中为其他实体定义 DTO,例如在 com.packtpub.micronaut.service.dto 包中的 PetDTO、VisitDTO 和 PetTypeDTO。
在下一节中,我们将致力于将这些 DTO 映射到数据库实体。
使用 MapStruct 定义映射器
MapStruct 是一个代码生成器,它使用注解处理来实现源和目标 Java 类之间的映射。实现的 MapStruct 代码由简单的函数调用组成,因此它是类型安全的且易于阅读的代码。由于我们不需要为这些映射编写代码,MapStruct 在减少源代码体积方面非常有效。
要将 DTO 映射到实体以及反之亦然,我们将在 pet-owner 微服务中使用 MapStruct。由于我们使用 Maven,我们不得不在 pom.xml 项目中添加以下内容:
...
<properties>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
通过将 MapStruct 导入项目,POM 将允许我们利用 MapStruct 工具包。此外,对于 Maven 编译器,我们需要将 MapStruct 添加到 annotationProcessorPaths:
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
…
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.defaultComponentModel=jsr330</arg>
….
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
POM 中的注解处理设置将指导 Java 注解处理器为使用 MapStruct 注解标记的任何映射生成源代码。此外,jsr330 被指定为 Micronaut 框架(在 Spring 中,通常使用 Spring 模型)中的默认组件模型。
我们将创建一个新的包名为 com.packtpub.micronaut.service.mapper,以包含所有映射器接口。为了抽象出一个通用的实体映射器,我们可以声明以下接口:
public interface EntityMapper <D, E> {
E toEntity(D dto);
D toDto(E entity);
List <E> toEntity(List<D> dtoList);
List <D> toDto(List<E> entityList);
}
EntityMapper 接口抽象出了对象到对象和列表到列表的转换方法。通过扩展此接口,我们可以轻松定义一个接口来将 OwnerDTO 映射到 Owner 实体:
@Mapper(componentModel = "jsr330", uses = {PetMapper.class})
public interface OwnerMapper extends EntityMapper<OwnerDTO, Owner> {
default Owner fromId(Long id) {
if (id == null) {
return null;
}
Owner owner = new Owner();
owner.setId(id);
return owner;
}
}
OwnerMapper 接口扩展了 EntityMapper 并使用 PetMapper。PetMapper 用于将一组 PetDTO 实例映射到 Pet 实体。PetMapper 可以使用非常类似的方法定义。
遵循相同的方法,因此我们可以为Pet、Visit和PetType实体分别定义PetMapper、VisitMapper和PetTypeMapper。
到目前为止,我们已经深入探讨了 DTO 及其与相应实体类的映射。在下一节中,我们将专注于与 DTO 相关的服务更改。
修改服务以使用 DTO
在第二章《数据访问工作》中,为了简化讨论,我们定义了直接使用实体类的方法。由于多种原因,包括我们最终将业务逻辑与数据库实体耦合,或者如果服务方法需要使用多个实体,这可能会变得复杂,因此这种方法不建议使用。简单来说,我们必须通过解耦数据库实体与业务所需的数据对象来分离关注点。
要在业务服务中使用 DTO,我们需要修改抽象接口。OwnerService接口可以修改为使用 DTO 如下:
public interface OwnerService {
OwnerDTO save(OwnerDTO ownerDTO);
Page<OwnerDTO> findAll(Pageable pageable);
Optional<OwnerDTO> findOne(Long id);
void delete(Long id);
}
save()方法被修改为保存OwnerDTO而不是Owner实体。同样,findAll()和findOne()将返回OwnerDTO而不是Owner。
采用相同的方法,我们可以修改pet-owner微服务中其他实体的服务接口,即PetService、PetTypeService和VisitService。
由于我们修改了抽象服务接口,因此我们还需要修改实现类。OwnerServiceImpl可以修改为使用 DTO 如下:
@Singleton
@Transactional
public class OwnerServiceImpl implements OwnerService {
… injections for OwnerRepository and OwnerMapper
@Override
public OwnerDTO save(OwnerDTO ownerDTO) {
Owner owner = ownerMapper.toEntity(ownerDTO);
owner = ownerRepository.mergeAndSave(owner);
return ownerMapper.toDto(owner);
}
@Override
@ReadOnly
@Transactional
public Page<OwnerDTO> findAll(Pageable pageable) {
return ownerRepository.findAll(pageable)
.map(ownerMapper::toDto);
}
@Override
@ReadOnly
@Transactional
public Optional<OwnerDTO> findOne(Long id) {
return ownerRepository.findById(id)
.map(ownerMapper::toDto);
}
@Override
public void delete(Long id) {
ownerRepository.deleteById(id);
}
}
save()方法将使用OwnerMapper在调用存储库方法之前将OwnerDTO转换为实体。同样,获取方法将在返回响应之前将Owner实体转换回OwnerDTO。
采用相同的方法,我们还可以修改PetServiceImpl、PetTypeServiceImpl和VisitServiceImpl。
到目前为止,我们一直关注 DTO 以及如何通过使用 MapStruct 框架将它们映射到实体对象上来轻松使用 DTO。在下一节中,我们将关注与 DTO 相关的服务更改。
为微服务创建 RESTful 端点
使用 Micronaut 框架,我们可以创建所有常用的 HTTP 方法,即 GET、PUT、POST 和 DELETE。在核心上,所有 HTTP 关注点都被封装在io.micronaut.http包中。此包包含HttpRequest和HttpResponse接口。这些接口是定义任何 HTTP 端点的基石。使用这些标准实现,我们将在以下章节中涵盖所有 HTTP 方法。
创建用于检索资源列表的端点
要创建用于获取资源列表的端点,我们可以首先添加com.packtpub.micronaut.web.rest包以包含所有控制器资源。我们将在pet-owner微服务的OwnerResource控制器中添加一个获取所有所有者的端点:
@Controller("/api")
public class OwnerResource {
…
@Get("/owners")
@ExecuteOn(TaskExecutors.IO)
public HttpResponse<List<OwnerDTO>> getAllOwners(HttpRequest request, Pageable pageable) {
log.debug("REST request to get a page of Owners");
Page<OwnerDTO> page = ownerService.findAll(pageable);
return HttpResponse.ok(page.getContent()).headers(headers ->
PaginationUtil.generatePaginationHttpHeaders(headers, UriBuilder.of(request.getPath()), page));
}
…
}
在这里,我们需要思考以下几点:
-
@Controller:这是一个标记OwnerResource为 RESTful 控制器的.stereotype,并在/api基本 URL 上公开。 -
@Get:这个注解用于在 HTTP get 上公开getAllOwners()方法,相对于 URL/owners。 -
@ExecuteOn:这个注解指定在 I/O 线程池上执行此 GET 请求。@ExecuteOn注解的值可以是micronaut.executors中定义的任何执行器。 -
HttpRequest:getAllOwners()接受HttpRequest作为输入参数。通常,HttpRequest被传递以跟踪和记录详细信息,例如,哪个 IP 地址提交了请求。 -
pageable:这是io.micronaut.data.model包中的一个标准接口,通常用于在请求中传递分页信息,例如,…/api/owners?page=1&size=50&sort=(firstName)。这种分页信息通常是可选的,并作为查询参数传递。 -
HttpResponse:getAllOwners()返回HttpResponse,这是一个通用接口,在示例中体现为特定类型的响应。在这个例子中,它返回List<OwnerDTO>。 -
PaginationUtil:这是com.packtpub.micronaut.util包中的一个自定义类,它生成分页响应。
我们可以在本地运行pet-owner服务,默认情况下,它将在端口8080上运行。我们可以使用任何 REST 客户端,并访问http://localhost:8080/api/owners,我们将得到以下响应:


图 3.2 – 检索所有所有者
如前述截图所示,当我们调用检索所有所有者的端点时,我们没有传递任何分页信息以获取所有所有者。因此,它将从数据库中检索所有所有者和他们的宠物信息。
创建用于检索特定资源的端点
要创建用于检索特定资源的端点,我们将在OwnerResource中添加一个方法来检索特定所有者,如下所示:
@Get("/owners/{id}")
@ExecuteOn(TaskExecutors.IO)
public HttpResponse<Optional<OwnerDTO>> getOwner(@PathVariable Long id) {
log.debug("REST request to get Owner : {}", id);
return ownerService.findOne(id);
}
在这里有几个问题需要我们思考:
-
@PathVariable:这个注解用于指定和匹配 HTTP 调用中的路径变量。在上面的例子中,我们传递所有者 ID 作为路径变量。 -
Optional:在这个例子中,我们返回一个可选对象。如果服务可以找到指定 ID 的所有者,则发送HTTP 200响应;否则,将回滚为HTTP 404。
我们可以在本地运行pet-owner服务,默认情况下,它将在端口8080上运行。我们可以使用任何 REST 客户端,通过访问http://localhost:8080/api/owners/1,我们将得到以下响应:


图 3.3 – 检索特定所有者
OwnerDTO 也封装了宠物数据,因此响应负载将检索 ID 为1的所有者的完整详细信息。此外,如果向后续的 HTTP 请求传递任何不存在的 ID,则会导致HTTP 404:

图 3.4 – 获取不存在的所有者
在前面的 API 调用中,我们尝试获取具有 123789 ID 的所有者,但由于我们没有这个 ID 的所有者,结果是一个 HTTP 404 未找到响应。
创建插入资源的端点
要创建一个插入资源的端点,我们将向 OwnerResource 添加一个 HTTP POST 方法,如下所示:
…
@Post("/owners")
@ExecuteOn(TaskExecutors.IO)
public HttpResponse<OwnerDTO> createOwner(@Body OwnerDTO ownerDTO) throws URISyntaxException {
if (ownerDTO.getId() != null) {
throw new BadRequestAlertException("A new owner cannot already have an ID", ENTITY_NAME, "idexists");
}
OwnerDTO result = ownerService.save(ownerDTO);
URI location = new URI("/api/owners/" + result.getId());
return HttpResponse.created(result).headers(headers -> {
headers.location(location);
HeaderUtil.createEntityCreationAlert(headers, applicationName, true, ENTITY_NAME, result.getId().toString());
});
}
…
在这里有几个需要注意的事项:
-
@Post: 使用@Post注解将createOwner()公开为 HTTP POST API。 -
@ExecuteOn: 这个注解指定了在 I/O 线程池上执行这个 POST 请求。 -
@Body:@Body注解指定了在createOwner()方法中的ownerDTO参数与传入的 HTTP POST 请求体绑定。 -
Not null id check: 使用if结构,我们快速检查 HTTP 请求体中不包含已定义的 ID 的有效负载。这是一个业务验证的断言,如果它失败,则 API 会抛出一个错误的请求异常。 -
HttpResponse.created: 在愉快的路径场景中,API 将返回HTTP 201创建。这个响应表示请求已成功执行并且已创建资源。
我们可以在本地启动 pet-owner 微服务,并在 REST 客户端中发送以下 HTTP POST 请求:

图 3.5 – 插入所有者
在前面的 HTTP POST 调用中,我们在 HTTP 请求体中传递了一个要插入的所有者对象。正如预期的那样,API 调用成功并返回了一个 HTTP 201 创建响应。
此外,如果我们尝试调用这个 HTTP POST 方法来创建一个具有 ID 值的所有者,那么它将失败:

图 3.6 – 再次插入现有所有者
当我们尝试在请求体有效负载中插入具有 ID 值的所有者时,则会抛出一个带有消息 – 内部服务器错误:新的所有者不能已经有 ID 的 HTTP 500 内部服务器错误。
创建更新资源的端点
要创建一个更新资源的端点,我们将向 OwnerResource 添加一个 HTTP PUT 方法,如下所示:
…
@Put("/owners")
@ExecuteOn(TaskExecutors.IO)
public HttpResponse<OwnerDTO> updateOwner(@Body OwnerDTO ownerDTO) throws URISyntaxException {
log.debug("REST request to update Owner : {}", ownerDTO);
if (ownerDTO.getId() == null) {
throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull");
}
OwnerDTO result = ownerService.save(ownerDTO);
return HttpResponse.ok(result).headers(headers ->
HeaderUtil.createEntityUpdateAlert(headers, applicationName, true, ENTITY_NAME, ownerDTO.getId().toString()));
}
…
在这里有一些值得思考的问题:
-
@Put:@Put注解将updateOwner()方法公开为 HTTP PUT API。 -
@ExecuteOn: 这个注解指定了在 I/O 线程池上执行这个 PUT 请求。I/O 线程池是一种通过维护等待分配 I/O 请求的线程多数来达到 I/O 并发的机制。 -
@Body:@Body注解指定了在createOwner()方法中的ownerDTO参数与传入的 HTTP PUT 请求体绑定。 -
Null id check: 使用if结构,我们快速检查 HTTP 请求体中不包含具有 null ID 的有效负载。如果 ID 为 null,则 API 会抛出一个错误的请求异常。 -
HttpResponse.ok: 在愉快的路径场景中,API 将返回HTTP 200。这个响应表示请求已成功执行,并且资源已被更新。
我们可以在本地启动 pet-owner 微服务,并在 REST 客户端中发送一个 HTTP PUT 请求,如下所示:

图 3.7 – 更新所有者
在 HTTP PUT 请求中,我们请求更新我们刚刚插入的资源。我们正在更新这个资源的地址和城市。正如预期的那样,请求成功执行,并返回了一个 HTTP 200 响应。
如果我们尝试使用 null ID 更新资源,那么它将失败:

图 3.8 – 更新不存在的所有者
在之前的 HTTP PUT 请求中,我们尝试使用 null ID 更新一个所有者。抛出了一个带有消息的 HTTP 500 内部服务器错误 – 内部服务器错误:无效的 ID。
为删除资源创建一个端点
为了展示删除资源,我们可以在 OwnerResource 中添加一个 HTTP DELETE 方法,如下所示:
…
@Delete("/owners/{id}")
@ExecuteOn(TaskExecutors.IO)
public HttpResponse deleteOwner(@PathVariable Long id) {
log.debug("REST request to delete Owner : {}", id);
ownerService.delete(id);
return HttpResponse.noContent().headers(headers -> HeaderUtil.createEntityDeletionAlert(headers, applicationName, true, ENTITY_NAME, id.toString()));
}
…
这里有一些值得思考的问题:
-
@Delete:@Delete注解将deleteOwner()方法公开为 HTTP DELETE API。 -
@ExecuteOn: 这个注解指定在这个 I/O 线程池上执行这个 PUT 请求。 -
@PathVariable:@PathVariable将deleteOwner()方法中的 ID 参数绑定到 HTTP 请求 URL 中指定的变量。 -
HttpResponse.noContent: 在愉快的路径场景中,API 将返回HTTP 204。这个响应表示请求已成功执行,并且响应体中没有额外的内容。
我们可以在本地启动 pet-owner 微服务,并在 REST 客户端中发送一个 HTTP DELETE 请求,如下所示:

图 3.9 – 删除所有者
我们请求删除我们在之前的示例中插入/更新的所有者。正如预期的那样,请求成功,并返回了一个 HTTP 204 响应。
到目前为止,我们已经探讨了如何创建所有不同类型的 RESTful 端点。这次讨论为我们使用 Micronaut 的 HTTP 服务器 API 打开了道路。在下一节中,我们将介绍一些利用这些服务器 API 的实际方面。
使用 Micronaut 的 HTTP 服务器 API
Micronaut 框架提供了一个基于 Netty 的非阻塞 HTTP 服务器。这些服务器 API 可以用来解决一些有用的微服务需求场景。为了进行实际操作,我们将继续使用 pet-owner 微服务。
首先,我们必须在项目的 POM 文件中添加以下 Maven 依赖项:
…
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
<scope>compile</scope>
</dependency>
…
这个依赖项应该已经在 pet-owner 项目中。在接下来的几节中,我们将探索 HTTP 服务器世界中的几个有用配置。
在 Micronaut 框架中绑定 HTTP 请求
在 Micronaut 框架中,我们可以通过几种方式将参数绑定到 HTTP 请求上:
-
@Body:如前所述,@Body将 HTTP 请求体中的参数绑定到一个参数上。 -
@CookieValue:这会将 HTTP 请求中的 cookie 值绑定到一个参数上。 -
@Header:这会将 HTTP 请求中的一个头部的参数绑定到一个参数上。 -
@QueryValue:这会将 HTTP 请求中的查询参数绑定到一个参数上。 -
@PathVariable:这会将 HTTP 请求中的路径绑定到一个参数上。 -
@Part:在多部分 HTTP 请求中,这会将参数绑定到一个部分上。 -
@RequestBean:这会将任何对象(在 HTTP 请求中)绑定到一个 Java Bean 上。
除了前面的 HTTP 请求绑定之外,Micronaut 框架支持多种 URI 模板。这些模板在创建各种类型的 RESTful 微服务时非常有用。考虑到所有者资源,我们可以在 Micronaut 中支持以下 URI 模板匹配:
![Table 3.2 – URI templates in the Micronaut framework]
![Table 3.2.jpg]
表 3.2 – Micronaut 框架中的 URI 模板
在前面的表格中,我们可以看到各种匹配/限制传入 HTTP 请求的方法以及每种方法可以为特定资源实现什么。在下一节中,我们将讨论如何验证 HTTP 请求中的数据。
验证数据
Micronaut 框架支持 JSR 380 Bean 验证以及 Hibernate Bean 验证。默认情况下,Micronaut 项目通常包含一个micronaut-validation依赖项,该依赖项基于 JSR 380 标准实现。
使用如@NotNull和@NotEmpty这样的验证注解,我们可以在处理请求之前验证一个参数/参数是否满足所有验证标准。以下是一些有用的注解:
-
@NotNull:这断言参数/参数值不是 null,例如,@NotNull @PathVariable String ownerId。 -
@NotEmpty:这断言参数不是 null 且不为空。它可以应用于任何String、Collection或Map类型的参数等,例如,@NotEmpty @PathVariable String ownerId。 -
@Min:这验证注解的属性值大于或等于value属性,例如,@Min(value = 0) Integer offset。 -
@Max:这验证注解的属性值等于或小于value属性,例如,@Max(value =100) Integer offset。 -
@Valid:@Valid验证对象图中所有的参数/参数。它递归地扫描对象图中的所有内部@Valid使用,并在检查完所有内容后确定最终的验证。
如果任何这些验证未满足,则抛出javax.validation.ConstraintViolationException异常。
为了处理约束违反异常以及其他已检查/未检查的异常,我们将在下一节中探讨一些选项。
处理错误
Micronaut 框架在整个 HTTP 请求的生命周期中提供了对抛出不同类型异常的良好覆盖,以及处理这些异常。标准异常处理程序可以在io.micronaut.http.server.exceptions包中找到。默认情况下,此包内提供了以下处理程序:
-
ContentLengthExceededHandler: 这个处理程序通过返回一个HttpStatus.REQUEST_ENTITY_TOO_LARGE响应来处理ContentLengthExceededException。 -
ConversionErrorHandler: 这个处理程序通过返回一个HttpStatus.BAD_REQUEST响应来处理ConversionErrorException。 -
HttpStatusHandler: 这个处理程序通过返回HttpStatusException中指定的HttpStatus来处理HttpStatusException。 -
JsonExceptionHandler: 这个处理程序通过返回一个HttpStatus.BAD_REQUEST响应来处理JsonProcessingException。 -
UnsatisfiedArgumentHandler: 这个处理程序通过返回一个HttpStatus.BAD_REQUEST响应来处理UnsatisfiedArgumentException。 -
URISyntaxHandler: 这个处理程序通过返回一个HttpStatus.BAD_REQUEST响应来处理URISyntaxException。
除了前面提到的标准异常和异常处理程序之外,我们还可以创建我们自己的自定义异常和异常处理程序。
我们可以假设一个假设的FooException,它可以由微服务抛出:
public class FooException extends RuntimeException {
…
}
FooException扩展了RuntimeException,这使得它成为一个未检查的异常,我们可以创建FooExceptionHandler来处理这个异常:
@Produces
@Singleton
@Requires(classes = {FooException.class, ExceptionHandler.class})
public class FooExceptionHandler implements ExceptionHandler<FooException, HttpResponse> {
@Override
public HttpResponse handle(HttpRequest request, FooException exception) {
JsonError error = new JsonError(exception.getMessage());
error.path('/' + exception.getArgument().getName());
error.link(Link.SELF, Link.of(request.getUri()));
return HttpResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
FooExceptionHandler bean 注入强制要求注入FooException和ExceptionHandler。在代码库的任何地方,如果抛出FooException,它将被FooExceptionHandler捕获,并返回一个HttpStatus.INTERNAL_SERVER_ERROR响应。
API 版本控制
Micronaut 通过使用@Version注解支持 API 版本控制。这个注解可以在控制器或方法级别使用。
默认情况下不支持版本控制,要启用版本控制,我们必须在application.yml中进行以下更改:
micronaut:
application:
name: Pet-Owner
router:
versioning:
enabled: true
default-version: 1
…..
在配置中,我们已经启用了版本控制,并且默认情况下,如果没有指定版本,则入站请求将由版本1的 API 提供服务。
为了展示版本控制,我们将在pet-owner微服务的OwnerResource的getOwner()方法上添加@Version:
@Version("1")
@Get("/owners/{id}")
@ExecuteOn(TaskExecutors.IO)
public Optional<OwnerDTO> getOwner(@PathVariable Long id) {
log.debug("REST request to get Owner : {}", id);
return ownerService.findOne(id);
}
…
@Version("2")
@Get("/owners/{id}")
@ExecuteOn(TaskExecutors.IO)
public Optional<OwnerDTO> getOwnerV2(@PathVariable Long id) {
log.debug("REST request to get Owner : {}", id);
return ownerService.findOne(id);
}
@Version允许对getOwner()方法的多个投影。这个注解在支持事件驱动微服务时非常有用。
要测试这些更改,我们可以在本地运行pet-owner并使用高级 REST 客户端调用getOwner():
![Figure 3.10 – 调用一个版本化的 API
![Figure 3.10_B16585.jpg]
图 3.10 – 调用一个版本化的 API
在前面的 HTTP 调用中,我们使用X-API-VERSION头指定了版本。根据这个头中指定的值,这个调用将由版本2的 API 提供服务。
到目前为止,我们已经探讨了如何在 Micronaut 框架中利用 HTTP 服务器 API。在下一节中,我们将关注 HTTP 客户端方面。
使用 Micronaut 的 HTTP 客户端 API
Micronaut 的 HTTP 客户端是一个基于 Netty 的非阻塞客户端,内置了服务发现和负载均衡等云功能。这种自定义实现增强了标准 HTTP 客户端,以适应微服务架构。
为了展示基本的 HTTP 调用,我们将在 pet-owner 微服务中为 OwnerResource 创建一个 HTTP 客户端。一开始,我们必须在 pom.xml 项目中添加 micronaut-http-client 依赖项:
…
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>compile</scope>
</dependency>
…
micronaut-http-client 封装了所有与 HTTP 客户端相关的 API,例如创建 RxHttpClient,执行所有 HTTP 操作,以及处理和响应负载。
接下来,我们将探讨如何利用 micronaut-http-client 在我们之前创建的端点上执行各种 HTTP 操作。
执行 HTTP PUT 操作
要执行 HTTP PUT 操作,我们可以创建 OwnerResourceClient。这个客户端可以打包在 com.packtpub.micronaut.web.rest.client 中。
我们可以添加以下方法来执行 HTTP 调用:
public List<OwnerDTO> getAllOwnersClient() throws MalformedURLException {
HttpClient client = HttpClient.create(new URL("https://" + server.getHost() + ":" + server.getPort()));
OwnerDTO[] owners = client.toBlocking().retrieve(HttpRequest.GET("/api/owners"), OwnerDTO[].class);
return List.of(owners);
}
HttpClient.create() 将创建一个 HTTP 客户端,然后我们使用它来发送 HTTP GET 请求。
执行 HTTP POST 操作
要执行 HTTP POST 操作,我们可以向 OwnerResourceClient 添加以下方法:
public OwnerDTO createOwnerClient() throws MalformedURLException {
HttpClient client = HttpClient.create(new URL("https://" + server.getHost() + ":" + server.getPort()));
OwnerDTO newOwner = new OwnerDTO();
newOwner.setFirstName("Lisa");
newOwner.setLastName("Ray");
newOwner.setAddress("100 Queen St.");
newOwner.setCity("Toronto");
newOwner.setTelephone("1234567890");
return client.toBlocking().retrieve(HttpRequest.POST("/api/owners", newOwner), OwnerDTO.class);
}
HttpClient.create() 将创建一个 HTTP 客户端,然后我们使用它来发送 HTTP POST 请求。一个所有者对象将被作为请求负载传递。
执行 HTTP PUT 操作
要执行 HTTP PUT 操作,我们可以向 OwnerResourceClient 添加以下方法:
public OwnerDTO updateOwnerClient() throws MalformedURLException {
HttpClient client = HttpClient.create(new URL("https://" + server.getHost() + ":" + server.getPort()));
OwnerDTO owner = new OwnerDTO();
owner.setId(1L);
owner.setAddress("120 Queen St.");
return client.toBlocking().retrieve(HttpRequest.PUT("/api/owners", owner), OwnerDTO.class);
}
HttpClient.create() 将创建一个 HTTP 客户端,然后我们使用它来发送 HTTP PUT 请求。一个 owner 对象将被作为请求负载传递。
执行 HTTP DELETE 操作
要执行 HTTP DELETE 操作,我们可以向 OwnerResourceClient 添加以下方法:
public Boolean deleteOwnerClient() throws MalformedURLException {
HttpClient client = HttpClient.create(new URL("https://" + server.getHost() + ":" + server.getPort()));
long ownerId = 1L;
HttpResponse httpResponse = client.toBlocking().retrieve(HttpRequest.DELETE("/api/owners" + ownerId), HttpResponse.class);
return httpResponse.getStatus().equals(HttpStatus.NO_CONTENT);
}
HttpClient.create() 将创建一个 HTTP 客户端,然后我们使用它来发送 HTTP DELETE 请求。如果请求执行成功,将返回一个无内容消息。
摘要
在本章中,我们涵盖了在 Micronaut 应用程序中处理 Web 端点的各个方面。我们从组装器或 DTO 的概念开始,然后深入探讨了如何创建支持基本 CRUD 操作的 restful 端点。我们还实验了 Micronaut 框架中的一些 HTTP 服务器 API。最后,我们专注于 HTTP 客户端方面,并使用 micronaut-http-client 创建了一个客户端实用工具。
本章为我们提供了与 Micronaut 框架中 restful 微服务各种实际方面相关的技能。此外,通过探索 HTTP 客户端,我们从头到尾覆盖了这些方面。在开发任何微服务时,这种在 Web 层面上工作的实践经验至关重要。
在下一章中,我们将通过尝试不同的方法和方法来保护 restful 端点,来确保 pet-clinic 微服务的 Web 层安全。
问题
-
什么是 DTO?
-
你如何在 Micronaut 框架中使用 MapStruct?
-
你如何在 Micronaut 框架中创建一个 RESTful HTTP GET 端点?
-
你如何在 Micronaut 框架中创建一个 RESTful HTTP POST 端点?
-
你如何在 Micronaut 框架中创建一个 RESTful HTTP PUT 端点?
-
你如何在 Micronaut 框架中创建一个 RESTful HTTP DELETE 端点?
-
Micronaut 框架是如何支持 HTTP 请求绑定的?
-
我们如何在 Micronaut 框架中验证数据?
-
我们如何在 Micronaut 框架中对 API 进行版本控制?
-
Micronaut 框架是如何支持 HTTP 客户端方面的?
-
我们如何在 Micronaut 框架中创建一个 HTTP 客户端?
第四章: 保护微服务
保护微服务接口以及它们所包含的世界是任何应用程序开发的关键方面。近年来,出现了各种拓扑、工具和框架来解决 Web 服务/微服务的安全方面。在本章中,我们将深入研究微服务中的一些核心和常用安全范式。我们将继续使用上一章的 pet-clinic 应用程序。为了实际操作,我们将致力于在 Micronaut 框架中涵盖以下认证策略的同时保护微服务:
-
pet-owner: 通过实际操作来保护pet-owner微服务端点,使用 会话认证 -
pet-clinic: 通过实际操作来保护pet-clinic微服务端点,使用 JWT 认证 -
pet-clinic-review: 通过实际操作来保护pet-clinic-review微服务端点,使用 OAuth 认证
通过上述实际操作练习,我们将在本章中涵盖以下主题:
-
使用 会话认证来保护服务端点
-
使用 JWT 认证来保护服务端点
-
使用 OAuth 来保护服务端点
到本章结束时,您将掌握在 Micronaut 框架中与各种认证策略和本地或云身份提供者一起工作的实用知识。
技术要求
本章中所有命令和技术说明均在 Windows 10 和 Mac OS X 上运行。本章涵盖的代码示例可在本书的 GitHub 仓库中找到,地址为 github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter04。
在开发环境中需要安装和设置以下工具:
-
Java SDK: 版本 13 或更高(我们使用了 Java 14)。
-
Maven: 这不是必需的,只有当你想使用 Maven 作为构建系统时才需要。然而,我们建议在任何开发机器上设置 Maven。下载和安装 Maven 的说明可以在
maven.apache.org/download.cgi找到。 -
开发 IDE: 根据您的偏好,可以使用任何基于 Java 的 IDE,但为了编写本章,使用了 IntelliJ。
-
Git: 下载和安装 Git 的说明可以在
git-scm.com/downloads找到。 -
PostgreSQL: 下载和安装 PostgreSQL 的说明可以在
www.postgresql.org/download/找到。 -
MongoDB:MongoDB Atlas 提供了一个免费的在线数据库即服务,存储空间高达 512 MB。然而,如果您更喜欢本地数据库,则可以在
docs.mongodb.com/manual/administration/install-community/找到下载和安装的说明。我们为本章使用了本地安装。 -
Rest 客户端:可以使用任何 HTTP REST 客户端。我们使用了 Advanced REST Client Chrome 插件。
-
Docker:有关下载和安装 Docker 的说明可以在
docs.docker.com/get-docker/找到。 -
OpenSSL:有关下载和安装 OpenSSL 的说明可以在
www.openssl.org/source/找到。
在 Micronaut 框架中处理 RESTful 微服务
为了深入了解 Micronaut 框架的安全方面,我们将继续在 pet-clinic 应用程序上工作。以下表格总结了我们将对应用程序中的每个微服务进行的安全更改:

表 4.1 – 在 pet-clinic 应用程序中保护微服务
为了确保微服务中的预期端点,我们将重点关注以下两个关键方面:
-
身份提供者:本质上,身份提供者拥有存储和维护数字身份的担忧。此外,它通过使用其存储的数字身份的多数派来验证提交的数字身份,解决任何安全声明。
-
认证策略:认证策略将决定微服务如何与身份提供者通信以认证和授权用户请求。
在添加到来自第三章,“在 Restful Web 服务上工作”的组件图的基础上,本章中每个微服务的以下内容将发生变化:

图 4.1 – 微服务组件
我们将坚持我们通常的模式,将关注点分开。我们将在每个微服务中使用一个身份提供者,并与认证策略协同工作。
在下一节中,我们的重点将是介绍 Micronaut 框架为安全考虑提供的现成工具。
Micronaut 安全基础
为了处理任何安全方面,Micronaut 框架内置了一个 SecurityFilter 对象。SecurityFilter 对象拦截任何传入的 HTTP 请求,并启动应用程序中配置的认证/授权过程。在以下图中,您可以看到 SecurityFilter 对象中授权用户请求的工作流程:

图 4.2 – Micronaut 安全过滤器
Micronaut 的 SecurityFilter 有三个基本部分:
-
AuthenticationFetcher将获取用于验证用户请求所需的下游认证器。 -
Authenticator注入配置的认证提供程序和安全配置以验证用户请求。根据认证操作的成功或失败,创建一个AuthenticationResponse对象。 -
通过
SecuredAnnotationRule或IpPatternsRule或通过扩展AbstractSecurityRule创建自己的安全规则。如果请求满足所有安全规则,则安全过滤器将返回成功的AuthenticationResponse响应;否则,它将返回失败的AuthenticationResponse响应。
通过利用 SecurityFilter,在下一节中,我们将关注如何在 Micronaut 框架中使用会话认证来保护微服务。
使用会话认证保护服务端点
在基于会话的认证中,用户状态存储在服务器端。当用户登录到服务器时,服务器启动会话并发出一个会话 ID 作为 cookie。服务器使用会话 ID 从会话共识中唯一标识一个会话。任何后续的用户请求都必须将此会话 ID 作为 cookie 传递以恢复会话:

图 4.3 – 基于会话的认证
如前图所示,在基于会话的认证策略中,服务器负责跟踪会话。客户端必须提供一个有效的会话 ID 以恢复会话。
要了解如何使用基于会话的认证来保护微服务,我们将对 pet-owner 微服务进行实验。首先,我们需要通过向 pom.xml 项目添加以下依赖项来启用安全功能:
<!-- Micronaut security -->
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-session</artifactId>
</dependency>
…
通过导入 micronaut-security 和 micronaut-security-session 依赖项,我们可以在 pet-owner 微服务中利用会话认证工具包。一旦导入这些依赖项,我们接下来需要按照下一个代码块所示配置 application.properties:
security:
enabled: true
authentication: session
session:
enabled: true
# Auth endpoint
endpoints:
login:
enabled: true
logout:
enabled: true
如前所述的 application.properties 实例,我们将通过将 enabled 设置为 true 并指定 session 作为所需的认证策略来启用安全功能。此外,Micronaut 安全工具包提供了开箱即用的 LoginController 和 LogoutController。在应用程序属性中,我们已经启用了它们,并且由于我们没有指定这些控制器的自定义路径,它们将分别可在默认指定的路径 …/login 和 …/logout 上访问。
我们将使用一个基本的本地身份提供程序,该程序将利用应用程序属性来存储用户数据。这非常原始,但有助于简化学习和探索。让我们向 application.properties 添加一些用户数据:
identity-store:
users:
alice: alice@1
bob: bob@2
charlie: charlie@3
roles:
alice: ADMIN
bob: VIEW
charlie: VIEW
我们已添加了三个用户:alice、bob 和 charlie。每个用户也被分配了 pet-owner 微服务的角色。
在下一节中,我们将探讨如何实现一个使用配置的应用程序属性进行用户数据操作的认证提供者。
实现基本认证提供者
要实现一个基本的身份验证提供者,我们首先将创建一个 com.packtpub.micronaut.security 安全包。这个包将包含所有与安全相关的工件。
我们首先将 IdentityStore 添加到这个包中:
@ConfigurationProperties("identity-store")
public class IdentityStore {
@MapFormat
Map<String, String> users;
@MapFormat
Map<String, String> roles;
public String getUserPassword(String username) {
return users.get(username);
}
public String getUserRole(String username) {
return roles.get(username);
}
}
IdentityStore 类映射到应用程序属性,用于访问用户数据。我们可以利用这个身份存储库来实现认证提供者,如下面的代码片段所示:
@Singleton
public class LocalAuthProvider implements AuthenticationProvider {
@Inject
IdentityStore store;
@Override
public Publisher<AuthenticationResponse>
authenticate(HttpRequest httpRequest,
AuthenticationRequest authenticationRequest) {
String username =
authenticationRequest.getIdentity().toString();
String password =
authenticationRequest.getSecret().toString();
if (password.equals(store.getUserPassword
(username))) {
UserDetails details = new UserDetails
(username, Collections.singletonList
(store.getUserRole(username)));
return Flowable.just(details);
} else {
return Flowable.just(new
AuthenticationFailed());
}
}
}
LocalAuthProvider 通过具体定义 authenticate() 方法来实现标准 AuthenticationProvider 接口。在 authenticate() 方法中,我们简单地检查用户请求中指定的身份和密码是否与身份存储库中的任何用户名和密码匹配。如果我们找到匹配项,则返回 UserDetails 对象,否则返回 AuthenticatonFailed。
在下一节中,我们将集中讨论如何为 pet-owner 端点配置授权。
配置服务端点的授权
在微服务的用户需求中,通常会有需要匿名访问以及受保护访问的场景。首先,我们将为 PetResource 和 VisitResource 提供匿名访问。
在 Micronaut 安全中提供匿名访问有两种方式:
-
使用
@Secured(SecurityRule.IS_ANONYMOUS) -
在应用程序属性中配置
intercept-url-map
在接下来的章节中,我们将深入探讨这两种方法。
使用 SecurityRule.IS_ANONYMOUS 授予匿名访问
Micronaut 安全内置了一个匿名访问安全规则。为了给整个控制器提供访问权限或将其限制在特定的端点上,我们可以简单地使用 @Secured 注解。在 PetResource 中,我们通过在控制器级别使用此注解来为所有端点提供匿名访问:
@Controller("/api")
@Secured(SecurityRule.IS_ANONYMOUS)
public class PetResource {
….
}
使用 @Secured(SecurityRule.IS_ANONYMOUS) 允许对所有 PetResource 端点进行匿名访问。我们可以简单地启动服务并尝试访问任何 PetResource 端点。您可以使用任何 REST 客户端来调用端点。在下面的屏幕截图中,您将注意到我们如何使用 REST 客户端进行 HTTP GET 调用:

图 4.4 – 对宠物的匿名访问
如前一个屏幕截图所示,我们可以匿名访问 PetResource,因为它使用 @Secured(SecurityRule.IS_ANONYMOUS) 进行了匿名访问配置。
在下一节中,我们将看到如何使用应用程序属性授予匿名访问。
使用应用程序属性授予匿名访问
我们还可以使用应用程序属性配置控制器或控制器中的特定端点的匿名访问。在以下代码片段中,我们正在配置对 …/api/visits 端点的匿名访问:
# Intercept rules
intercept-url-map:
- pattern: /api/visits
access: isAnonymous()
在应用程序属性中,我们已配置任何用户对…/api/visits的请求都应授予匿名访问权限。这将允许所有用户(包括已认证和未认证的用户)访问VisitResource。
要快速测试我们能否匿名访问…/api/vistis,我们可以尝试对任何VisitResource端点进行访问:



图 4.7 – 获取安全访问的 cookie
如前一个屏幕截图所示,我们将使用正确的用户名和密码向…/login发送 POST 请求,服务将返回 cookie 作为响应。
我们可以将此 cookie 传递给对OwnerResource的任何请求。在下面的屏幕截图中,我们传递了获取到的 cookie 来对/api/owners端点进行 HTTP GET 调用:

图 4.8 – 使用获取到的 cookie 进行安全访问
由于我们在请求头中传递了获取到的 cookie,服务将提取此 cookie,验证并成功返回HTTP 200响应。
到目前为止,我们介绍了如何使用会话认证处理匿名和认证访问场景。在下一节中,我们将深入探讨在 Micronaut 框架中使用JSON Web Tokens(JWTs)来保护对微服务的访问。
使用 JWT 认证保护服务端点
在基于令牌的身份验证中,用户状态存储在客户端。当客户端登录到服务器时,服务器将用户数据加密成一个带有秘密的令牌,并将其发送回客户端。任何后续的用户请求都必须在请求头中设置此令牌。服务器检索令牌,验证其真实性,并恢复用户会话:

图 4.9 – 基于令牌的身份验证
如前图所示,在基于令牌的身份验证策略中,客户端负责在 JSON Web 令牌中跟踪会话。客户端必须提供一个有效的令牌以恢复会话。
要学习如何使用基于令牌的身份验证来保护微服务,我们将通过一个实际的pet-clinic微服务进行操作。首先,我们将使用 Keycloak 设置一个第三方身份提供者。在下一节中,我们将本地设置 Keycloak。
设置 Keycloak 作为身份提供者
我们将在本地 Docker 容器中运行 Keycloak 服务器。如果您尚未安装 Docker,您可以参考技术要求部分了解如何在您的开发工作区中安装 Docker。要启动本地 Keycloak 服务器,请打开 Bash 终端并运行以下命令:
$ docker run -d --name keycloak -p 8888:8080 -e KEYCLOAK_USER=micronaut -e KEYCLOAK_PASSWORD=micronaut123 jboss/keycloak
之后,Docker 将在容器中实例化一个 Keycloak 服务器,并将容器端口8080映射到主机操作系统的端口8888。此外,它将创建一个密码为micronaut123的micronaut管理员用户。安装成功后,您可以通过http://localhost:8888/访问 Keycloak。在下一节中,我们将开始为微服务设置客户端。
在 Keycloak 服务器上创建客户端
要将 Keycloak 用作身份提供者,我们将从设置客户端开始。按照以下说明设置 Keycloak 身份提供者客户端:
-
访问
localhost:8888/auth/admin/的Keycloak 管理员模块。 -
提供有效的管理员用户名和密码(在我们的例子中,是
micronaut和micronaut123)。 -
从左侧导航菜单中选择客户端。
-
提供客户端 ID(您可以跳过其余的输入)。
-
创建具有提供的客户端 ID 的客户端后,Keycloak 将为客户端打开设置标签页。您必须选择以下截图所示的突出显示的值:

图 4.10 – 在 Keycloak 服务器中创建客户端
Keycloak 服务器将在默认的主域中创建pet-clinic客户端。接下来,我们将为这个客户端空间设置一些用户。
在客户端空间中设置用户
用户设置将使我们能够将这些身份用作测试用户(当然,稍后,实际用户也可以进行配置)。我们将首先创建角色。对于pet-clinic微服务,我们将定义两个角色:pet-clinic-admin和pet-clinic-user。要创建一个角色,请遵循以下说明:
-
在主菜单中选择角色。
-
点击添加角色按钮。
-
提供角色名称并点击保存按钮。
我们将添加三个用户 – Alice(管理员)、Bob(用户)和Charlie(用户)。要添加用户,请遵循以下说明:
-
在主菜单中选择用户并点击添加用户。
-
提供用户名并保持默认设置。点击保存按钮。
-
用户创建后,前往凭证选项卡,指定密码并将临时标志更改为关闭。点击重置****密码按钮。
-
要配置
user-role,请前往角色映射选项卡并选择所需用户角色。更改将自动保存。
重复前面的说明,将Alice设置为pet-clinic-admin,Bob设置为pet-clinic-user,Charlie设置为pet-clinic-user。
为了从 Keycloak 中公开这些角色数据,我们需要进行以下更改:
-
从主菜单中选择客户端作用域选项。
-
在列出的选项中选择角色。
-
前往角色的映射器选项卡,并选择领域角色。
-
提供以下截图中所突出显示的输入:

图 4.11 – 配置领域角色映射器
一旦创建了用户并分配了角色,我们就可以继续对pet-clinic微服务进行更改。在下一节中,我们将深入了解如何使用 Keycloak 身份提供者的基于令牌的认证来使pet-clinic微服务安全。
使用基于令牌的认证保护 pet-clinic 微服务
为了保护pet-clinic微服务,我们首先需要在pom.xml项目中添加以下依赖项来启用安全功能:
<!-- Micronaut security -->
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security</artifactId>
<version>${micronaut.version}</version>
</dependency>
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-jwt</artifactId>
<version>${micronaut.version}</version>
</dependency>
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-oauth2</artifactId>
<version>${micronaut.version}</version>
</dependency>
…
通过导入micronaut-security和micronaut-security-jwt依赖项,我们可以在pet-clinic微服务中利用令牌认证工具包。我们将使用 OAuth 2 与 Keycloak 服务器集成。一旦导入这些依赖项,我们还需要按照以下方式配置application.properties:
security:
authentication: idtoken
endpoints:
login:
enabled: true
redirect:
login-success: /secure/anonymous
token:
jwt:
enabled: true
signatures.jwks.keycloak:
url: http://localhost:8888/auth/realms/master/protocol/openid-connect/certs
oauth2.clients.keycloak:
grant-type: password
client-id: pet-clinic
client-secret: XXXXXXXXX
authorization:
url: http://localhost:8888/auth/realms/master/protocol/openid-connect/auth
token:
url: http://localhost:8888/auth/realms/master/protocol/openid-connect/token
auth-method: client_secret_post
在应用程序属性中,client-id和client-secret必须从KeyCloak复制。客户端密钥可以通过访问http://localhost:8888/auth/realms/master/.well-known/openid-configuration来复制。
在下一节中,我们将重点介绍如何使用配置的基于令牌的认证策略和 Keycloak 身份服务器来授予控制器端点的安全访问权限。
使用 KeyCloak 身份提供者授予安全访问权限
为了授予安全访问权限,我们可以使用 @Secured 注解以及 intercept-url-map。在动手实验中,我们将使用 @Secured 注解授予 VetResource 的安全访问权限,如下代码片段所示:
@Controller("/api")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class VetResource {
...
}
VetResource 中的所有端点都只授予安全访问权限。如果我们尝试访问任何 …/vets 端点,微服务将返回一个禁止响应:

图 4.12 – 未认证访问兽医
如前图所示,如果我们尝试访问任何兽医端点而不指定有效的令牌,微服务将抛出 HTTP 401 未授权 响应。
为了成功访问兽医端点,我们需要获取一个有效的 JWT。我们可以使用内置的登录控制器进行登录。要登录,只需向 …/login 路径发送带有正确用户名和密码的 POST 请求。如果请求成功,响应中将发送包含 JWT 的 cookie,如下一屏幕截图所示:

图 4.13 – 获取用于安全访问的 cookie
如屏幕截图所示,我们将使用正确的用户名和密码向 …/login 端点发送 POST 请求。服务将使用 KeyCloak 身份提供者授予安全访问权限,并返回包含 JWT 的 cookie。
复制前一个响应中的 JWT 部分。我们可以将此令牌传递给对 VetResource 的任何请求。在下面的屏幕截图中,我们正在调用刚刚获取的 JWT 的 …/vets 端点:

图 4.14 – 使用获取的令牌进行安全访问
由于我们在请求头中传递了有效的令牌,服务将验证此令牌并成功返回 HTTP 200 响应。
到目前为止,我们已经探讨了如何使用外部身份提供者的 JWT 来保护微服务。在下一节中,我们将关注如何使用云身份提供者的 OAuth 来实现微服务安全。
使用 OAuth 保护服务端点
OAuth 是另一种基于令牌的认证策略。由于其广泛的接受度、对网络安全深度和广度的良好覆盖,以及客户端和服务器端管理用户会话的灵活性,使其成为企业级认证机制。OAuth 规定使用令牌来建立身份,而不是传递用户名和密码。令牌可以从外部身份提供者获取,然后可以将此令牌传递给任何后续请求以恢复会话:

图 4.15 – 使用 OAuth 分离关注点
如前图所示,在基于 OAuth 令牌的认证策略中,客户端从身份提供者获取令牌,并在向服务器发送任何 API 请求时使用此令牌。服务器通过身份提供者验证此令牌以返回适当的响应。
为了了解如何使用 OAuth 和基于云的身份提供者来保护微服务,我们将通过pet-clinic-review微服务进行实际操作练习。首先,我们将使用 Okta 设置一个云身份提供者。
设置 Okta 作为身份提供者
Okta 是领先的 SaaS 身份管理门户。我们将使用 Okta 作为身份提供者。为了开始,您必须在 Okta 上注册。在developer.okta.com上注册。注册后,Okta 将要求用户确认电子邮件。在电子邮件中,您还将收到一个 Okta 域名,如图中所示:
![Figure 4.16 – Okta 注册确认中的域名]
![Figure_4.16_B16585.jpg]
![Figure 4.16 – Okta 注册确认中的域名]
如图中所示,将为您的开发者账户创建一个 Okta 域名。您必须保存此信息,因为这将用于以后配置 Okta 作为身份提供者。
在下一节中,我们将了解如何在 Okta 上创建应用程序。
在 Okta 上创建应用
为了使用 Okta 与您的微服务,您需要在 Okta 上创建一个应用。按照以下说明创建 Okta 上的应用:
-
在主页面上,选择创建 Web 应用程序。
-
选择原生作为您的平台并点击下一步按钮。
-
提供以下截图中的应用程序设置,并在所有输入都提供后,点击完成按钮:![Figure 4.17 – 在 Okta 上创建微服务应用]
![Figure 4.17_B16585.jpg]
![Figure 4.17 – 在 Okta 上创建微服务应用]
-
我们将保留大部分输入的默认值。在允许的授权类型下,勾选所有复选框。
-
一旦应用创建成功,编辑客户端凭证并选择使用客户端身份验证选项以进行客户端身份验证。
按照之前的说明创建应用程序后,记下客户端 ID 和客户端密钥。这将在以后使用。接下来,我们将为该应用程序设置一些用户。
在客户端空间中设置用户
用户设置将使我们能够将这些身份用作测试用户。我们将添加三个用户 - Alice(管理员)、Bob(用户)和Charlie(用户)。要添加用户,请遵循以下说明:
-
在主导航栏上,将鼠标悬停在用户上,然后选择人员。
-
点击添加人员按钮。
-
提供以下截图中的输入:![Figure 4.18 – 在 Okta 上添加人员(用户)]
![Figure 4.18_B16585.jpg]
![Figure 4.18 – 在 Okta 上添加人员(用户)]
-
在密码输入中,选择由管理员设置,并且必须保持用户必须在首次登录时更改密码未选中。这将允许我们快速使用身份而不重置密码。
重复前面的说明,为Bob和Charlie设置应用程序用户。一旦用户创建,我们就可以继续对pet-clinic-reviews微服务进行更改。
在下一节中,我们将深入探讨如何使pet-clinic-reviews安全,但首先让我们先启用 SSL,以便在 HTTPS 上加密通信。
在 Micronaut 框架中启用 SSL
如果微服务没有暴露给 HTTPS,任何安全防护措施都将是不完整的。在前面的小节中,我们故意只关注了身份验证和授权,而跳过了 SSL。由于我们将使用通过云提供的第三方身份,建议并在pet-clinic-reviews微服务中启用 SSL。
为了启用 SSL,我们需要一个用于 localhost 的 SSL 证书。我们将使用 OpenSSL 创建自签名证书。按照以下说明使用 OpenSSL 创建自签名证书:
-
打开 Git Bash 终端。
-
将目录更改为
pet-clinic-reviews项目的根目录。 -
在 Git Bash 中运行
winpty openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365。提供正确信息以创建自签名证书。这将创建一个key.pem文件和一个cert.pem文件在打开的目录中。 -
要合并密钥和证书文件,请在 Git Bash 中运行
winpty openssl pkcs12 -inkey key.pem -in cert.pem -export -out cert.p12。 -
要验证您已创建 P12 文件,请在 Git Bash 中运行
winpty openssl pkcs12 -in cert.p12 -noout -info。您必须提供用于创建 P12 文件的相同密码。
根据前面的说明,我们可以成功创建一个平台无关的证书。P12 格式因其可以在不同平台和操作系统之间使用而变得流行。接下来,我们将添加此证书到主机操作系统的信任库,以便系统上所有运行的应用程序都可以信任它。按照以下说明将证书添加到信任库:
-
确定
$JAVA_HOME。它可以在系统变量中找到。 -
将刚刚创建的
cert.pem文件复制到$JAVA_HOME/jre/lib/security/cacerts。 -
以管理员权限打开 Git Bash 终端,并将目录更改为
$JAVA_HOME/jre/lib/security。 -
在 Git Bash 终端中运行
winpty keytool -importcert -file cert.pem -alias localhost -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit。
通过遵循前面的说明,我们将把自签名证书添加到信任库中。这将使系统在通过 SSL 使用此证书时信任它。
我们的定制开发者 Okta 域也可能不被系统信任。我们将遵循类似的说明将 Okta 证书添加到cacerts信任库中:
-
在 Chrome 浏览器中打开一个新的标签页。打开开发者工具。
-
点击
https://${yourOktaDomain}/oauth2/default/.well-known/oauth-authorization-server?client_id=${yourClientId}。 -
在开发者工具中,转到安全选项卡并点击查看证书。
-
这将在提示中打开证书。转到此提示的详细信息选项卡。
-
点击复制文件并按照说明将证书导出到本地目录。
-
将刚刚导出的证书复制到
$JAVA_HOME/jre/lib/security/cacerts。 -
使用管理员权限打开 Git Bash 终端,并将目录更改为
$JAVA_HOME/jre/lib/security。 -
在 Git Bash 终端中运行
winpty keytool -importcert -file okta.cert -alias localhost -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit。在文件选项中,你必须提供导出的证书名称。
将开发域 Okta 证书添加到系统信任存储库将使我们能够与 Okta 身份提供者通信。在下一节中,我们将深入了解如何使用自签名证书在pet-clinic-reviews微服务中启用 SSL。
配置 SSL 的应用程序属性
一旦你有了可读的证书,Micronaut 框架提供了一种快速的方法通过配置一些应用程序属性来启用 SSL。对应用程序属性进行以下更改以启用 SSL:
micronaut:
ssl:
enabled: true
key-store:
type: PKCS12
path: file:cert.p12
password: Pass@w0rd
为了启用 SSL,我们使用了在上一节中创建的自签名证书。用于安全通信的8443。
在下一节中,我们将关注如何使用 Okta 身份提供者配置pet-clinic-reviews微服务以使用 OAuth 安全。
使用 OAuth 保护pet-clinic-reviews微服务
为了保护pet-clinic微服务,我们首先需要通过在pom项目中添加以下依赖项来启用安全:
<!-- Micronaut security -->
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-oauth2</artifactId>
</dependency>
…
通过导入micronaut-security和micronaut-security-jwt依赖项,我们可以在pet-clinic-reviews微服务中利用令牌认证和 OAuth 工具包。一旦导入这些依赖项,我们还需要按照以下方式配置application.properties:
security:
authentication: idtoken
oauth2:
clients:
okta:
client-secret: HbheS-
q4P6oewQgT7uK58bgMbtHbCwcarzWuHB32
client-id: 0oa37vkb7Sq23P1kh5d6
openid:
issuer: https://dev-
4962048.okta.com/oauth2/default
endpoints:
logout:
get-allowed: true
在应用程序属性中,client-id和client-secret必须从 Okta 复制。对于发行者,你必须提供你的 Okta 域名在第一部分。你可能只需要更改你的开发域名,但你可以通过访问https://${yourOktaDomain}/oauth2/default/.well-known/oauth-authorization-server?client_id=${yourClientId}中的 OAuth 配置来获取有关授权和令牌 URL 的更多信息。
在下一节中,我们将关注如何使用 OAuth 和 Okta 身份服务器授予控制器端点的安全访问权限。
使用 Okta 身份提供者授予安全访问权限
为了授予安全访问权限,我们可以使用@Secured注解以及intercept-url-map。在我们的实际操作示例中,我们将使用@Secured注解来授予VetReviewResource的安全访问权限:
@Controller("/api")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class VetReviewResource {
...
}
VetReviewResource 中的所有端点都仅授予安全访问权限。如果我们尝试访问任何 …/vet-reviews 端点,微服务将返回一个禁止响应。在以下图中,我们尝试未加密地访问 …/vet-reviews 端点,服务响应为 HTTP 401:

图 4.19 – 未认证访问兽医
如前一个屏幕截图所示,如果我们尝试访问任何未指定有效令牌的 vet-reviews 端点,微服务将抛出 HTTP 401 未授权 响应。
为了成功访问兽医端点,我们需要获取一个有效的 JWT。我们可以通过访问 Okta 令牌 API 来获取有效令牌。以下是通过调用 Okta 令牌 API 的 curl 命令:
curl -k -u client_id:client_secret \
--location --request POST 'https://dev-4962048.okta.com//oauth2/default/v1/token' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'username=Alice' \
--data-urlencode 'password=Pass@w0rd' \
--data-urlencode 'scope=openid'
在之前的 curl 命令中,你必须提供正确的 client_id 和 client_secret、POST URL 和用户凭据。如果一切验证成功,令牌 API 将返回一个载体令牌和 ID 令牌。复制返回的访问令牌。我们可以将此令牌传递给对 VetReviewResource 的任何请求,以实现安全通信:

图 4.20 – 使用获取的令牌进行安全访问
由于我们在请求头中传递了一个有效令牌,该服务将验证此令牌并成功返回 HTTP 200 响应。
在本节中,我们学习了 OAuth 安全性,并进行了实验。为了使 pet-clinic-reviews 服务端点安全,我们使用了 OAuth 和 Okta 作为第三方身份提供者。
摘要
在本章中,我们探讨了在 Micronaut 框架中保护微服务的各种方法。我们的旅程从深入研究会话身份验证策略开始,然后探讨了使用外部 Keycloak 身份服务器进行的基于令牌的身份验证。最后,我们使用基于云的身份提供者 OAuth 来保护微服务。此外,我们还启用了 SSL,以确保通过 HTTPS 进行服务通信的安全。
本章为你提供了一套实用的技能集,用于在 Micronaut 框架中使用各种身份验证策略来保护微服务,以及如何与本地或外部(云)身份提供者协同工作。
在下一章中,我们将探讨如何使用事件驱动架构集成不同的微服务。
问题
-
Micronaut 框架中有哪些不同的身份验证策略?
-
Micronaut 中的安全过滤器是什么?
-
你如何在 Micronaut 框架中设置基于会话的身份验证?
-
Micronaut 框架中的
@Secured注解是什么? -
Micronaut 框架中的
intercept-url-maps是什么? -
你如何在 Micronaut 框架中设置基于令牌的身份验证?
-
你如何在 Micronaut 框架中设置 JWT 身份验证?
-
你如何在 Micronaut 框架中与 Keycloak 集成?
-
你如何在 Micronaut 框架中设置 OAuth 认证?
-
你如何在 Micronaut 框架中与 Okta 集成?
-
你如何在 Micronaut 框架中启用 SSL?
第五章: 使用事件驱动架构集成微服务
微服务架构的本质是将单体分解为解耦或松耦合的微服务。这种分解成微服务的结果是,我们将每个微服务拥有的用户和/或业务关注点分开。然而,对于整个应用程序来说,所有微服务需要通过相互交互来执行和响应用户请求,共同工作。事件驱动架构在解决这些微服务间的交互方面越来越受欢迎。
在本章中,我们将探讨如何在 Micronaut 框架中实现事件驱动架构。我们将深入研究以下主题:
-
理解事件驱动架构
-
使用 Apache Kafka 生态系统进行事件流
-
使用事件流集成微服务
到本章结束时,读者将掌握关于事件驱动架构以及如何在 Micronaut 框架中实现事件流代理以集成应用程序微服务的高级知识。
技术要求
本章中的所有命令和技术说明都是在 Windows 10 和 macOS 上运行的。本章涵盖的代码示例可在书籍的 GitHub 仓库 https://github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter05 中找到。
以下工具需要在开发环境中安装和设置:
-
Java SDK: Java SDK 版本 13 或更高(我们使用了 Java 14)。
-
Maven: 这不是必需的,只有当您想使用 Maven 作为构建系统时才需要。然而,我们建议在所有开发机器上设置 Maven。有关下载和安装 Maven 的说明可以在
maven.apache.org/download.cgi找到。 -
开发 IDE: 根据您的偏好,可以使用任何基于 Java 的 IDE,但为了本章节的目的,使用了 IntelliJ。
-
Git: 有关下载和安装 Git 的说明可以在
git-scm.com/downloads找到。 -
PostgreSQL: 有关下载和安装的说明可以在
www.postgresql.org/download/找到。 -
MongoDB: MongoDB Atlas 提供了高达 512 MB 存储空间的免费在线数据库即服务。然而,如果您更喜欢本地数据库,则可以在
docs.mongodb.com/manual/administration/install-community/找到有关下载和安装的说明。在编写本章节时,我们使用了本地安装。 -
REST 客户端: 可以使用任何 HTTP REST 客户端。我们使用了 高级 REST 客户端(ARC)Chrome 插件。
-
Docker: 有关下载和安装 Docker 的说明可以在 https://docs.docker.com/get-docker/ 找到。
理解事件驱动架构
事件驱动架构在连接不同的微服务中起着至关重要的作用。在我们深入了解如何实现事件驱动交互系统之前,让我们先了解其基本原理。
以下是在任何事件驱动架构实现中的核心关键组件:
-
事件:事件仅仅是系统状态的变化,需要被追踪。在微服务架构中,一个微服务可能会创建或检测数据状态的变化,这可能值得其他服务注意。这种状态变化以事件的形式进行沟通。
-
事件生产者:事件生产者是任何正在创建或检测状态变化并为系统中的其他组件/服务生成事件的微服务或组件。
-
事件消费者:事件消费者是任何消费事件的微服务或组件。有趣的是,这种事件消费可能会触发该组件生成另一个事件。
-
事件代理:事件代理在所有生产者和消费者之间充当中间人。它维护一个元数据共识以跟踪事件。
这些关键组件共同实现了一个事件驱动架构。广义上讲,有两种实现策略——发布/订阅(也称为事件消息)和事件流。要了解更多关于这些策略的信息,让我们深入了解以下章节。
事件驱动架构中的事件消息或发布/订阅模型
发布/订阅模型是一个基于推送的模型。在基于推送的模型中,事件发布由事件生产者拥有,事件从生产者推送并发送到消费者。以下是在发布/订阅实现中的关键组件:
-
事件生产者:任何正在创建或检测状态变化的组件都会生成一个事件并将其发布到事件代理。
-
事件代理:事件代理将接收生成的事件并将其推送到所有必需的事件队列。这些事件队列由事件消费者订阅。因此,事件通过代理被推送到事件消费者。
-
事件消费者:事件消费者将接收事件并执行所需操作。它也可能生成一个新的事件(或多个事件)。
这在以下图中表示:

图 5.1 – 发布/订阅模型
如前图所示,当事件生产者 1生成事件 Foo时,它被推送到事件代理。事件代理进一步将此事件推送到事件消费者 1和事件消费者 2。
因此,总的来说,对于由事件生产者 n生成的事件 Bar,事件代理将其推送到事件消费者 k。
在发布/订阅模型中,一旦事件被产生并通过事件代理传达给消费者,事件消费者必须立即执行必要的操作,因为一旦事件被消费,它就会消失。事件消费者永远无法回到历史事件。这种模型有时也被称为事件消息模型。
事件驱动架构中的事件流
一个事件流模型是一个基于拉取的模型。在拉取模型中,事件消费者负责获取事件。在事件流实现中,关键组件将如下操作:
-
事件生产者:任何创建或检测状态变化的组件都会生成事件并将其发送到事件代理。
-
事件代理:事件代理将接收生成的事件,并通过将事件放入事件流来广播事件。
-
事件消费者:事件消费者持续监控事件代理上的一个或多个事件流。当新事件被推送到事件流时,消费者获取事件并执行所需的操作。
参考以下图表:

图 5.2 – 事件流模型
如前图所示,当事件生产者 1生成事件 Foo时,它将事件推送到事件代理,并将它放入Foo 流。事件消费者 1和事件消费者 2从Foo 流中获取事件。事件 Bar由事件消费者 k从Bar 流中获取。
在事件流模型中,当事件消费者从事件流中获取数据时,他们可以获取事件流的任何偏移量。这甚至使事件消费者能够访问历史事件。如果你向系统中添加了一个新的消费者,它可能还没有接触过系统的最新状态,并且可能首先开始处理历史事件,这尤其有用。出于这些原因,事件流通常比事件消息更受欢迎。
在下一节中,我们将开始使用流行的事件流堆栈进行实际的事件流操作。
Apache Kafka 生态系统中的事件流
Apache Kafka 是业界领先的事件流系统。在 Apache Kafka 生态系统中,以下是一些关键组件:
- 事件主题:事件主题由属于特定类别的不可变、有序消息流组成。每个事件主题可能有一个或多个分区。分区是支持 Apache 中多并发索引存储。Apache Kafka 每个主题至少保留一个分区,并根据指定(在主题创建时)或需求添加更多分区。当新消息发布到主题时,Apache Kafka 决定将使用哪个主题分区来附加消息。每个主题将最新消息附加到末尾。这在下图中显示:

图 5.3 – Apache Kafka 主题结构
如前图所示,当新消息发布到流中时,它被附加到末尾。事件消费者可以自由选择读取哪个主题偏移量。当消费者 1从第一个偏移量读取时,消费者 2从第六个偏移量读取。
-
事件代理:事件代理是一个门面,提供了写入或读取事件主题(s)的接口。Apache Kafka 通常有主代理和从代理。对于主题,主代理将服务所有写入请求。如果主代理失败,那么从代理将作为主代理介入。
-
Kafka 集群:如果 Apache Kafka 的法定多数由多个事件代理组成,那么它被称为集群。在集群中,每个代理通常会领导一个不同的主题,并且可能作为其他主题的从属代理。
-
事件生产者:生产者将事件消息发布到主题流。生产者将与 Apache Kafka 生态系统交互,以了解应使用哪个代理将事件消息写入主题流。如果代理失败或添加了新的代理,Apache Kafka 将通知所需的生产者。
-
事件消费者:消费者从主题流中读取事件消息。事件消费者将与 Apache Kafka 生态系统交互,以了解应使用哪个代理从主题流中读取。此外,Apache Kafka 会跟踪每个事件消费者的主题偏移量,以便正确地恢复事件消费。
-
Zookeeper:Zookeeper 维护 Apache Kafka 生态系统的元数据法定多数。它本质上维护了所有代理的信息,包括事件生产者和消费者。它还跟踪每个事件消费者的主题偏移量。
在以下图中,我们可以看到 Apache Kafka 生态系统中的各种组件以及它们在事件流中的交互方式:

图 5.4 – Apache Kafka 生态系统
在前面的图中,Apache Kafka 生态系统一览无余。对于每个事件主题,至少将有一个主事件代理和一个或多个从事件代理。领导者(主)和从属代理的信息由 Zookeeper 维护。当事件生产者推送消息时,代理会将消息写入所需的主题流。同样,当事件消费者拉取消息时,事件代理将从所需的主题流中检索消息。Zookeeper 为所有事件消费者维护每个主题的偏移量信息。
在下一节中,我们将深入了解如何在宠物诊所应用程序中使用 Apache Kafka 生态系统进行事件流。
使用事件流集成微服务
为了学习和进行动手练习,我们将在宠物诊所应用程序中实现一个简单的事件流场景。考虑以下图:

图 5.5 – 宠物诊所应用程序中的事件流
在上述图中,每当有新的兽医审查时,pet-clinic-reviews 微服务将审查发送到 Apache Kafka Streaming。Apache Kafka 将审查追加到 vet-reviews 主题流。而且,由于 pet-clinic 微服务正在持续监控 vet-reviews 主题流,它将获取追加到主题的任何新审查并相应地更新平均评分。这是一个简单的图,但有助于集中学习关键目标。
在下一节中,我们将开始设置本地 Docker 中的 Apache Kafka 生态系统,以了解更多关于 Apache Kafka 流的信息。
本地设置 Apache Kafka 生态系统
要本地设置 Apache Kafka 生态系统,我们将使用 Docker。所有必需组件和配置的 docker-compose 文件可以在章节的 GitHub 工作空间下的 resources 中找到:github.com/PacktPublishing/Building-Microservices-with-Micronaut/blob/master/Chapter05/micronaut-petclinic/pet-clinic-reviews/src/main/resources/kafka-zookeeper-kafdrop-docker/docker-compose.yml。
执行以下步骤以安装和设置 Apache Kafka:
-
从上述网址下载
docker-compose文件。 -
打开 GitBash 终端。
-
将目录更改到您放置
docker-compose文件的位置。 -
在 GitBash 终端中运行
docker-compose up命令。
按照以下说明操作后,Docker 将安装 Zookeeper、Apache Kafka 和 Kafdrop。Kafdrop 是一个直观的 Apache Kafka 管理 GUI。在下一节中,我们将验证它们的安装。
测试 Apache Kafka 生态系统设置
要测试 Apache Kafka 生态系统是否成功安装,请执行以下步骤:
-
打开 GitBash 终端并运行以下命令:
winpty docker exec -it kafka bash -
将目录更改到 opt/bitnami/kafka/bin/。
-
在 GitBash 终端中运行以下命令以添加一个主题流:
command ./kafka-topics.sh --bootstrap-server kafka:9092 --create --partitions 3 --replication-factor 1 --topic foo-stream -
要向主题添加消息,请在 GitBash 终端中运行以下命令:
command ./kafka-console-producer.sh --broker-list kafka:9092 --topic foo-stream -
将出现一个终端提示符,输入
hello-world!,然后按 Enter。 -
按 Ctrl + D,这将成功将事件添加到主题。
通过遵循这些说明,我们添加了一个 foo-stream 主题并向此主题添加了一条消息。要查看此主题流,我们可以在浏览器窗口中打开 http://localhost:9100/。请参阅以下屏幕截图:

图 5.6 – Kafdrop 显示 foo-stream 主题消息
Kafdrop 提供了一个直观的 GUI,用于查看和管理所有 Apache Kafka 流。在上一张屏幕截图中,我们可以看到刚刚创建的 foo-stream 中的消息。
到目前为止,我们已经在 Docker 化的环境中本地设置了 Apache Kafka 生态系统,在下一节中,我们将使用此设置在 pet-clinic-reviews 和 pet-clinic 微服务中进行实际的事件流处理。我们将首先在 pet-clinic-reviews 微服务中进行必要的更改。
在 pet-clinic-reviews 微服务中实现事件生产者客户端
我们将开始对 pet-clinic-reviews 微服务进行必要的更改,以便它可以将兽医评论流式传输到 Apache Kafka。对于这个实际练习,我们将保持简单。因此,我们将跳过安全设置,并从 第三章 在 RESTful Web 服务上工作 的代码库继续。
执行以下步骤以查看结果:
-
首先,我们需要在
pom.xml项目中添加 Kafka 依赖项:<!-- Kafka --> <dependency> <groupId>io.micronaut.kafka</groupId> <artifactId>micronaut-kafka</artifactId> </dependency> …通过导入
micronaut-kafka依赖项,我们可以在pet-clinic-reviews微服务中利用 Kafka 工具包。 -
一旦导入依赖项,我们接下来需要按照以下方式配置
application.properties:micronaut: application: name: PetClinicReviews server: port: 8083 kafka: bootstrap: servers: localhost:9094如前所述的
application.properties中提到的,我们将为pet-clinic-reviews微服务修复端口8083并通过提供 Bootstrap 服务器详细信息来配置 Kafka 连接。 -
接下来,我们将在
pet-clinic-reviews微服务中创建一个 Kafka 客户端,该客户端可以向vet-reviews主题发送消息。首先创建一个包,com.packtpub.micronaut.integration.client。此包将包含所需的客户端,并且在未来可能包含更多与服务集成相关的工件。我们现在将VetReviewClient添加到这个包中:@KafkaClient public interface VetReviewClient { @Topic("vet-reviews") void send(@Body VetReviewDTO vetReview); }VetReviewClient被注解为@KafkaClient。使用@KafkaClient注解,我们可以将VetReviewClient注入为 Kafka 客户端。此外,只需简单地使用@Topic("vet-reviews"),我们就可以将消息(甚至不需要创建主题)发送到vet-reviews主题流。
到目前为止,我们已经配置了应用程序属性并创建了一个简单的 Kafka 客户端。在下面的代码中,我们将对 VetReviewResource 中的 createVetReview() 进行修改,以便在发布新的兽医评论时向主题流发送消息:
@Post("/vet-reviews")
@ExecuteOn(TaskExecutors.IO)
public HttpResponse<VetReviewDTO> createVetReview(@Body VetReviewDTO vetReviewDTO) throws URISyntaxException {
log.debug("REST request to save VetReview : {}", vetReviewDTO);
if (vetReviewDTO.getReviewId() != null) {
throw new BadRequestAlertException("A new vetReview cannot already have an ID", ENTITY_NAME, "idexists");
}
VetReviewDTO result = vetReviewService.save(vetReviewDTO);
/** Stream to other services */
vetReviewClient.send(result);
URI location = new URI("/api/vet-reviews/" + result.getReviewId());
return HttpResponse.created(result).headers(headers -> {
headers.location(location);
HeaderUtil.createEntityCreationAlert(headers, applicationName, true, ENTITY_NAME, result.getReviewId());
});
}
从前面的代码中,我们可以看到我们可以简单地注入 VetReviewClient 到 VetReviewResource。在 createVetReview() 中,当兽医评论成功插入时,我们可以使用 VetReviewClient 将消息发送到 vet-reviews 流。
在本节中,我们介绍了 pet-clinic-reviews 微服务中的事件生产者。在下一节中,我们将通过调用 HTTP POST 端点来创建新的兽医评论来验证此事件生产者。
在 pet-clinic-reviews 微服务中测试事件生产者
要测试刚刚创建的事件生产者,请在本地上启动 pet-clinic-reviews 微服务并访问 HTTP POST 端点。在下面的屏幕截图中,我们使用 REST 客户端调用 vet-reviews HTTP POST 端点以创建审查:
![图 5.7 – 为测试事件生产者创建审查]
![img/Figure_5.7_B16585_Fixed.jpg]
图 5.7 – 为测试事件生产者创建审查
如前一个屏幕截图所示,当我们提交一个创建新审查的请求时,它将持久化审查并将审查流出到 Apache Kafka。可以通过访问 http://localhost:9100/ 上的 Kafdrop 来验证此事件消息。这是屏幕输出的内容:
![图 5.8 – 新增的 vet-reviews 流审查]
![img/Figure_5.8_B16585_Fixed.jpg]
图 5.8 – 新增的 vet-reviews 流审查
如在 Kafdrop 中所见,我们可以验证来自 pet-clinic-reviews 微服务的事件被流出到并添加到 vet-reviews 主题中。
在本节中,我们验证了 pet-clinic-reviews 微服务中的事件生产者。在下一节中,我们将探讨如何在 Micronaut 框架中实现事件消费者。
在 pet-clinic 微服务中实现事件消费者客户端
在本节中,我们将在 pet-clinic 微服务中实现一个事件消费者,以便它可以消费 vet-reviews 主题中的流消息。
首先,我们需要在 pom.xml 项目中添加一个 Kafka 依赖。这可以通过以下代码展示:
<!-- Kafka -->
<dependency>
<groupId>io.micronaut.kafka</groupId>
<artifactId>micronaut-kafka</artifactId>
</dependency>
…
导入 micronaut-kafka 将使我们能够利用 Kafka 消费者工具包。一旦导入依赖项,我们还需要按照以下方式配置 application.properties:
micronaut:
application:
name: Pet-Clinic
server:
port: 8082
kafka:
bootstrap:
servers: localhost:9094
如前述代码所述,我们将为 pet-clinic 微服务固定端口 8082 并通过提供 Bootstrap 服务器详细信息来配置 Kafka 连接。
接下来,为了包含所有 Kafka 集成工件,我们将创建一个 com.packtpub.micronaut.integration 包。由于我们将从 vet-reviews 主题流中消费,我们将 VetReviewDTO 添加到 com.packtpub.micronaut.integration.domain 包中。
一些开发者提倡将 DTOs 保留在共享仓库中,以便在所有微服务中重用。然而,将所有 DTOs 放在拥有微服务下有利于更好的封装。此外,可能存在某些情况下,如 VetReviewDTO,可以在一个微服务中假定所需的对象定义,而在另一个微服务中则不同。
我们将在 com.packtpub.micronaut.integration.client 包中创建一个 Kafka 监听器,以利用 micronaut-kafka 工具包。请参考以下代码块:
@KafkaListener(groupId = "pet-clinic")
public class VetReviewListener {
private static final Logger log = LoggerFactory.getLogger(VetReviewListener.class);
private final VetService vetService;
public VetReviewListener(VetService vetService) {
this.vetService = vetService;
}
@Topic("vet-reviews")
public void receive(@Body VetReviewDTO vetReview) {
log.info("Received: vetReview -> {}", vetReview);
try {
vetService.updateVetAverageRating(vetReview.getVetId(), vetReview.getRating());
} catch (Exception e) {
log.error("Exception occurred: {}", e.toString());
}
}
}
从前面的代码中,我们可以看到我们使用 @KafkaListener 注解创建了 VetReviewListener。在 @KafkaListener 注解中,我们传递了 groupId。为 Kafka 监听器分配一个组 ID 将其添加到消费者组中。当存在多个针对主题流的消费者服务时,这可能是必需的,以便 Kafka 生态系统可以为每个消费者维护一个隔离的偏移量。使用 @Topic("vet-reviews") 允许 VetReviewListener 接收来自 vet-reviews 流的任何流式输出消息。当 VetReviewListener 接收到任何消息时,它会在 VetService 中调用 updateVetAverageRating()。在以下代码片段中,我们在 VetService 中添加了此方法,以便在向 pet-clinic-reviews 微服务添加新的评论时更新兽医的平均评分:
public void updateVetAverageRating(Long id, Double rating) throws Exception {
log.debug("Request to update vet rating, id: {}, rating: {}", id, rating);
Optional<VetDTO> oVetDTO = findOne(id);
if (oVetDTO.isPresent()) {
VetDTO vetDTO = oVetDTO.get();
Double averageRating = vetDTO.getAverageRating() != null ? vetDTO.getAverageRating() : 0D;
Long ratingCount = vetDTO.getRatingCount() != null ? vetDTO.getRatingCount() : 0L;
Double newAvgRating = ((averageRating * ratingCount) + rating) / (ratingCount + 1);
Long newRatingCount = ratingCount + 1;
vetRepository.updateVetAverageRating(id, newAvgRating, newRatingCount);
}
}
从前面的代码中,我们可以看到 updateVetAverageRating() 方法检索最后存储的评分。如果最后存储的评分是 null,它假定其为 0。在任何情况下,它都会添加新的评分并确定新的平均评分。一旦确定了平均评分,就会通过调用存储库来将评分信息持久化到数据库中。
在本节中,我们探讨了如何在 pet-clinic 微服务中实现事件消费者。在下一节中,我们将通过创建一个新的兽医评论来验证此事件消费者。
在 pet-clinic 微服务中测试事件消费者
为了测试刚刚创建的事件消费者,我们可以启动 pet-clinic(事件消费者)和 pet-clinic-reviews(事件生产者)微服务。一旦 pet-clinic-reviews 微服务启动,添加一个新的兽医评论。在以下屏幕截图中,您可以看到我们正在使用 HTTP REST 客户端发布兽医评论:

图 5.9 – 添加新的兽医评论以测试事件消费者
在对 vet-reviews 资源的 POST 请求中,我们添加了一个极低的评分。pet-clinic-reviews 微服务成功执行了请求,并返回了一个 HTTP 201 响应,为提交的评论分配了一个评论 ID。
如以下屏幕截图所示,在 pet-clinic 微服务中,如果我们向 VetReviewListener 中设置一个调试点,我们可以验证 Kafka 主题流正在发送新的兽医评论的消息:

图 5.10 – 事件消费者接收的事件消息
如前一张截图所示,当 pet-clinic-reviews 微服务产生一个事件消息时,它被 pet-clinic 微服务接收。这是使用事件驱动架构将这些两个微服务集成的魔力。并且这种模式可以扩展到各种不同的场景中集成微服务,例如一个服务向多个微服务发送消息或链式事件消息,或者编排复杂的微服务集成。
在本节中,我们验证了 pet-clinic 微服务中的事件消费者,以确保当新的兽医评论添加到 pet-clinic-reviews 时,pet-clinic 从 vet-reviews 主题流中接收评论信息。
摘要
在本章中,我们首先介绍了事件驱动架构的一些基础知识,讨论了两种不同的事件发布模型,即 pub/sub 和事件流。我们讨论了每个模型的核心组件,以及使用每个模型的优缺点。
由于事件流更适合宠物诊所应用程序,我们深入探讨了使用 Apache Kafka 生态系统的事件流。为了实际操作练习,我们使用 Apache Kafka 主题流集成了 pet-clinic-reviews 和 pet-clinic 微服务。我们通过创建一个新的兽医评论并接收 pet-clinic 微服务中的评分来验证集成,以更新兽医的平均评分。
本章为您提供了对事件驱动架构的坚实基础理解,以及如何在 Micronaut 框架中实现事件流系统的实用技能集。
在下一章中,我们将探讨如何使用 Micronaut 框架中的内置以及第三方工具来自动化质量测试。
问题
-
什么是事件驱动架构?
-
事件驱动架构中的 pub/sub 模型是什么?
-
什么是事件流?
-
描述构成 Apache Kafka 生态系统的各种组件。
-
如何在 Docker 中设置 Apache 生态系统?
-
如何使用事件流在 Micronaut 框架中集成微服务?
-
在 Micronaut 框架中如何实现事件消费者?
-
在 Micronaut 框架中如何实现事件生产者?
第三部分:微服务测试
本节将专注于 Micronaut 框架中的微服务测试,并包含以下章节:
- 第六章, 测试微服务
第六章:测试微服务
在一个相当简单的定义中,软件测试是验证产生的软件应用程序是否按预期工作。自从编程语言和软件开发初期以来,已经建立了良好的先例来确保它们按预期工作。几乎所有编程语言(除了某些脚本语言)都有强大的编译器来在编译时捕获异常。尽管编译时检查是一个好的开始,但它们不能验证软件应用程序在运行时是否会按预期运行。为了安心,软件开发团队会执行各种类型的测试来验证软件应用程序将按预期工作。随着分布式组件数量的增加,任何测试练习都会成倍增加,简单来说,测试单体应用程序比分布式应用程序要容易得多。为了节省时间和减少交付特性的周转时间,自动化不同级别的测试是高效的。
在本章中,我们将探讨如何在不同级别的微服务中自动化测试。我们将深入以下主题:
-
理解测试金字塔
-
Micronaut 框架中的单元测试
-
Micronaut 框架中的服务测试
-
使用测试容器进行集成测试
到本章结束时,您将掌握在微服务不同级别自动化测试的实用知识。
技术要求
本章中所有命令和技术说明均在 Windows 10 和 macOS 上运行。本章涵盖的代码示例可在本书的 GitHub 上找到,地址为github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter06。
以下工具需要在开发环境中安装和设置:
-
Java SDK版本 13 或更高(我们使用了 Java 14)
-
Maven:这是可选的,只有当你想使用 Maven 作为构建系统时才需要。然而,我们建议在任何开发机器上设置 Maven。下载和安装 Maven 的说明可以在
maven.apache.org/download.cgi找到。 -
开发 IDE:根据您的偏好,可以使用任何基于 Java 的 IDE,但为了编写本章,使用了 IntelliJ。
-
Git:下载和安装的说明可以在
git-scm.com/downloads找到。 -
PostgreSQL:下载和安装的说明可以在
www.postgresql.org/download/找到。 -
MongoDB: MongoDB Atlas 提供了高达 512 MB 存储空间的免费在线数据库服务。然而,如果您更喜欢本地数据库,可以在
docs.mongodb.com/manual/administration/install-community/找到下载和安装的说明。我们使用本地安装来编写这一章节。 -
REST 客户端:可以使用任何 HTTP REST 客户端。我们使用了 Advanced REST Client Chrome 插件。
-
Docker:有关下载和安装 Docker 的说明,请参阅
docs.docker.com/get-docker/。
理解测试金字塔
测试金字塔是一个易于理解不同测试类型相对性能、成本和健壮性的概念。以下图表显示了测试金字塔中的各种测试类型以及所需的努力:

图 6.1 – 测试金字塔
如前图所示,单元测试快速、健壮且成本低,而当我们接近金字塔的顶部时,测试变得缓慢、脆弱且昂贵。尽管所有类型的测试都是验证应用程序是否按预期工作所必需的,但良好的平衡对于降低成本、提高健壮性和速度至关重要。简单来说,要有大量的单元测试、一定数量的服务测试和非常少的端到端测试。这将确保以更快的速度和更低的成本保证质量。
在下一节中,我们将从单元测试开始自动化测试之旅。
Micronaut 框架中的单元测试
在面向对象的范式下,一个对象可以具有多种行为。这些行为由它们的方法定义。有效的单元测试一次探测一个对象的行为。这并不意味着测试一个方法,因为方法可以通过不同的执行路径(如果方法有分叉的控制流)改变其行为。因此,本质上,单元测试一次将探测一个方法的单个执行路径。迭代地,我们可以添加更多的单元测试来探测相同或不同方法中的其他执行路径。这种自下而上的方法依赖于在较小、隔离的级别验证行为,以确保整个应用程序按预期工作。
要执行单元测试,需要隔离。本质上,我们需要隔离对象的行为(我们想要测试的行为),同时忽略对象与系统内其他对象/组件的交互。为了实现这种隔离,我们在单元测试中有各种机制:
-
模拟:模拟是一个创建测试替身的操作,其中测试框架将根据对象的类定义创建一个模拟/虚拟对象(编译时)。为了隔离主题对象与其他交互对象的交互,我们可以简单地模拟交互对象。当对主题对象执行单元测试时,它将跳过与其他对象的交互。
-
监视:通过监视,我们通过探测对象的实际实例(运行时)创建一个测试替身。监视对象将只是与真实对象相同,除了任何存根。存根用于定义一个虚拟调用,以便监视对象可以正常执行,但当调用与存根定义匹配时,它将执行由存根定义的虚拟行为。
虽然模拟和监视可以帮助隔离行为,但有时主题对象可能没有与其他对象交互,因此不需要测试替身。在下一节中,我们将从如何在 Micronaut 框架中使用 JUnit 5 实现单元测试开始。
使用 JUnit 5 进行单元测试
为了学习如何在 Micronaut 框架中实现单元测试,我们将从第五章,使用事件驱动架构集成微服务继续代码库。我们将继续使用宠物-所有者微服务,并确保你已经将以下依赖项添加到项目的pom.xml文件中:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-junit5</artifactId>
<scope>test</scope>
</dependency>
通过导入前面的 JUnit 依赖项,我们可以在宠物-所有者微服务中利用 JUnit 和 Micronaut 测试工具包。
接下来,我们将在com.packtpub.micronaut.util中创建一个TestUtil类,它可以封装一些基本的测试方法:
public final class TestUtil {
public static <T> void equalsVerifier(Class<T> clazz)
throws Exception {
T domainObject1 =
clazz.getConstructor().newInstance();
assertThat(domainObject1.toString()).isNotNull();
assertThat(domainObject1).isEqualTo(domainObject1);
assertThat(domainObject1.hashCode()).isEqualTo(domainObject1.hashCode());
// Test with an instance of another class
Object testOtherObject = new Object();
assertThat(domainObject1).isNotEqualTo(testOtherObject);
assertThat(domainObject1).isNotEqualTo(null);
// Test with an instance of the same class
T domainObject2 =
clazz.getConstructor().newInstance();
assertThat(domainObject1).isNotEqualTo(domainObject2);
/* HashCodes are equals because the objects are not persisted yet */
assertThat(domainObject1.hashCode()).isEqualTo(domainObject2.hashCode());
}
}
在TestUtil中,我们添加了equalsVerifier()方法,该方法可以验证两个对象是否相等。此方法接受一个类类型作为输入参数,以对测试对象的不同条件进行断言。
在下一节中,我们将探讨如何对领域对象进行单元测试。
单元测试领域对象
领域对象在宠物-所有者微服务中简单来说就是一个Owner类。在下面的代码片段中,我们创建了一个OwnerTest类来断言两个所有者实例的相等性:
public class OwnerTest {
@Test
public void equalsVerifier() throws Exception {
TestUtil.equalsVerifier(Owner.class);
Owner owner1 = new Owner();
owner1.setId(1L);
Owner owner2 = new Owner();
owner2.setId(owner1.getId());
assertThat(owner1).isEqualTo(owner2);
owner2.setId(2L);
assertThat(owner1).isNotEqualTo(owner2);
owner1.setId(null);
assertThat(owner1).isNotEqualTo(owner2);
}
}
OnwerTest类包含一个测试方法equalsVerifier()。使用org.junit.jupiter.api.Test注解将其标记为测试方法。为了验证预期的行为,我们使用断言语句。同样,我们可以在宠物-所有者微服务中为其他领域对象定义测试类。
在下一节中,我们将对映射器对象进行单元测试。
单元测试映射器对象
我们在宠物-所有者微服务中的映射器对象很简单,我们可以使用@Test注解为OwnerMapper类创建一个基本的测试。在下面的代码片段中,OwnerMapperTest正在对OwnerMapper中的fromId()方法进行单元测试:
public class OwnerMapperTest {
private OwnerMapper;
@BeforeEach
public void setUp() {
ownerMapper = new OwnerMapperImpl();
}
@Test
public void testEntityFromId() {
Long id = 1L;
assertThat(ownerMapper.fromId(id).getId()).
isEqualTo(id);
assertThat(ownerMapper.fromId(null)).isNull();
}
}
OwnerMapperTest 类包含一个测试方法 testEntityFromId()。为了验证预期的行为,我们使用了 assert 语句。同样,我们还可以为宠物-所有者微服务中的其他映射对象定义测试类。
到目前为止,我们为不需要任何测试替身的领域和映射对象编写了简单的单元测试。在下一节中,我们将探讨如何使用模拟来创建所需的测试替身。
在单元测试中使用模拟
如前所述,模拟测试框架将基于类定义创建一个测试替身。这些测试替身在单元测试中调用其他对象的方法的对象时非常有用。
为了了解单元测试中的模拟,我们将对宠物诊所微服务中的 VetService 类进行工作。让我们看看宠物诊所微服务中的 VetServiceImpl:
@Singleton
public class VetServiceImpl implements VetService {
private final VetRepository;
private final SpecialtyRepository;
private final VetMapper;
private final SpecialtyMapper;
public VetServiceImpl(VetRepository,
SpecialtyRepository, VetMapper, SpecialtyMapper
specialtyMapper) {
this.vetRepository = vetRepository;
this.specialtyRepository = specialtyRepository;
this.vetMapper = vetMapper;
this.specialtyMapper = specialtyMapper;
}
…
}
VetService 在构造函数中实例化了 VetRepository、SpecialtyRepository、VetMapper 和 SpecialtyMapper。这些实例化的对象随后在 VetService 方法中使用。为了对 VetService 对象进行单元测试,我们需要为这些交互对象定义模拟对象。
让我们创建 VetServiceTest 来封装 VetService 的单元测试。在这个测试类中,我们将使用 @MockBean 注解模拟一些交互对象:
@MicronautTest
class VetServiceTest {
@Inject
private VetRepository;
@Inject
private SpecialtyRepository;
@Inject
private VetMapper;
@Inject
private SpecialtyMapper;
@Inject
private VetService;
/** Mock beans */
@MockBean(VetRepositoryImpl.class)
VetRepository vetRepository() {
return mock(VetRepository.class);
}
@MockBean(SpecialtyRepositoryImpl.class)
SpecialtyRepository specialtyRepository() {
return mock(SpecialtyRepository.class);
}
…
}
VetServiceTest 类被 @MicronautTest 注解标记。它将测试类作为一个实际的 Micronaut 应用程序运行,具有完整的应用程序上下文,从而避免了生产代码和测试代码之间的人工分离。
为了注入交互对象,我们使用了 @Inject 注解。@Inject 将应用程序上下文中的一个 bean 注入到类中。此外,使用 @MockBean 注解,我们正在覆盖 VetRepository 和 SpecialtyRepository 的运行时 bean。@MockBean 将在应用程序上下文中用模拟对象替换实际对象。
我们可以轻松地使用这些测试替身模拟在编写 VetService 方法的单元测试:
@Test
public void saveVet() throws Exception {
// Setup Specialty
Long specialtyId = 100L;
SpecialtyDTO = createSpecialtyDTO(specialtyId);
Specialty = specialtyMapper.toEntity(specialtyDTO);
// Setup VetDTO
Long vetId = 200L;
VetDTO = createVetDTO(vetId);
vetDTO.setSpecialties(Set.of(specialtyDTO));
Vet = vetMapper.toEntity(vetDTO);
// Stubbing
when(vetRepository.save(any(Vet.class))).thenReturn
(vetId);
when(specialtyRepository.findByName(anyString())).
thenReturn(specialty);
doNothing().when(vetRepository).saveVetSpecialty
(anyLong(), anyLong());
when(vetRepository.findById(anyLong())).thenReturn
(vet);
// Execution
VetDTO savedVetDTO = vetService.save(vetDTO);
verify(vetRepository, times(1)).save(any(Vet.class));
verify(specialtyRepository,
times(1)).findByName(anyString());
verify(vetRepository, times(1)).saveVetSpecialty
(anyLong(), anyLong());
verify(vetRepository, times(1)).findById(anyLong());
assertThat(savedVetDTO).isNotNull();
assertThat(savedVetDTO.getId()).isEqualTo(vetId);
assertThat(savedVetDTO.getSpecialties()).isNotEmpty();
assertThat(savedVetDTO.getSpecialties().size()).
isEqualTo(1);
assertThat(savedVetDTO.getSpecialties().stream().
findFirst().orElse(null).getId()).isEqualTo
(specialtyId);
}
在前面的代码片段中,你可以看到我们是如何为模拟的 VetRepository 和 SpecialtyRepository 类定义存根的。通常,模拟存根的形式为 when(object.methodCall()).thenReturn(result),除非是 void 方法调用,此时为 doNothing().when(object).methodCall()。
理想情况下,在模拟存根之后跟随 verify() 语句是谨慎的。verify() 将确认在执行单元测试期间确实调用了所需的方法调用。
在下一节中,我们将探讨另一种使用间谍创建测试替身的方法。
在单元测试中使用间谍
正如我们之前讨论的,在测试框架上间谍将基于类的实际运行时对象创建一个测试双胞胎。虽然模拟创建了一个完整的真实对象测试双胞胎,但在间谍中,我们可以控制测试双胞胎是部分还是完整的。在间谍对象中,我们可以存根一些方法调用,同时保持其他方法调用为真实。在这种情况下,单元测试将进行模拟和真实调用。因此,间谍为我们提供了更多控制权,以确定我们想要伪造的内容。
为了了解单元测试中的间谍技术,我们将在 pet-clinic 微服务中处理 SpecialtyService 类。让我们看看 pet-clinic 微服务中的 SpecialtyServiceImpl:
public class SpecialtyServiceImpl implements SpecialtyService {
private final SpecialtyRepository;
private final SpecialtyMapper;
public SpecialtyServiceImpl(SpecialtyRepository
specialtyRepository, SpecialtyMapper specialtyMapper) {
this.specialtyRepository = specialtyRepository;
this.specialtyMapper = specialtyMapper;
}
…
}
SpecialtyService 在构造函数中实例化了 SpecialtyRepository 和 SpecialtyMapper。这些实例化的对象随后在 SpecialtyService 方法中使用。为了对 SpecialtyService 对象进行单元测试,我们需要为这些交互对象定义一些间谍。
让我们创建 SpecialtyServiceTest 来封装 SpecialtyService 的单元测试。在这个测试类中,我们将使用 @MockBean 注解和 JUnit 中的 spy() 方法来间谍一些交互对象:
@MicronautTest
class SpecialtyServiceTest {
@Inject
private SpecialtyRepository;
@Inject
private SpecialtyMapper;
@Inject
private SpecialtyService;
@MockBean(SpecialtyRepositoryImpl.class)
SpecialtyRepository specialtyRepository() {
return spy(SpecialtyRepository.class);
}
…
}
SpecialtyServiceTest 类被 @MicronautTest 注解,它将测试类作为实际的 Micronaut 应用程序运行,具有完整的应用程序上下文。
使用 @MockBean 注解,我们正在覆盖 SpecialtyRepository 的运行时 Bean。@MockBean 将在应用程序上下文中用间谍对象替换实际对象。在间谍 SpecialtyRepository 对象上,我们可以轻松定义一些在测试方法中执行而不是实际调用的存根:
@Test
public void saveSpecialty() throws Exception {
// Setup Specialty
Long specialtyId = 100L;
SpecialtyDTO = createSpecialtyDTO(specialtyId);
Specialty = specialtyMapper.toEntity(specialtyDTO);
// Stubbing
doReturn(100L).when(specialtyRepository).save(any
(Specialty.class));
doReturn(specialty).when(specialtyRepository).findById
(anyLong());
// Execution
SpecialtyDTO savedSpecialtyDTO =
specialtyService.save(specialtyDTO);
verify(specialtyRepository,
times(1)).save(any(Specialty.class));
verify(specialtyRepository,
times(1)).findById(anyLong());
assertThat(savedSpecialtyDTO).isNotNull();
assertThat(savedSpecialtyDTO.getId()).isEqualTo
(specialtyId);
}
在前面的代码片段中,你可以看到我们是如何为间谍 SpecialtyRepository 实例定义存根的。通常,间谍存根的形式为 doReturn(result).when(object).methodCall(),除非是 void 方法调用,此时为 doNothing().when(object).methodCall()。
再次提醒,在间谍存根之后跟随 verify() 语句是明智的。这些语句将确认在执行单元测试时是否执行了期望的方法调用。
到目前为止,我们已经学习了使用模拟和间谍进行单元测试的各种方法。在下一节中,我们将探讨如何在 Micronaut 框架中执行服务测试。
Micronaut 框架中的服务测试
服务测试是单元测试的下一级。通过测试微服务中的所有端点,并对所有其他微服务重复此过程,我们可以确保所有服务都按预期从边缘到边缘工作。它将质量检查提升到下一个层次。话虽如此,正如我们之前讨论的,当我们向上移动到测试金字塔时,测试用例变得更加脆弱、昂贵和缓慢,因此,我们需要在高级别上不过度测试之间建立良好的平衡。
要了解我们如何在 Micronaut 框架中执行服务测试,我们将继续使用 pet-clinic 微服务。在接下来的几节中,我们将进入测试服务的所有 REST 端点。我们将使用 @Order 注解来建立套件中测试的执行顺序。一个有序的测试套件可以帮助从头开始并最终清理。在以下示例中,我们将创建、获取、更新,最后删除资源。
创建测试套件
为了测试 VetResource 端点,让我们创建一个 VetResourceIntegrationTest 类。这个套件将封装所有快乐和不快乐的集成测试:
@MicronautTest(transactional = false)
@Property(name = "micronaut.security.enabled", value = "false")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class VetResourceIntegrationTest {
@Inject
private VetMapper;
@Inject
private VetRepository;
@Inject
private SpecialtyRepository;
@Inject @Client("/")
RxHttpClient client;
…
}
在前面的代码片段中,有几个值得思考的点:
-
transactional = false确保套件在没有事务的情况下运行。 -
@Property注解覆盖了应用程序配置。在我们的情况下,我们禁用了安全。 -
TestInstance.Lifecycle.PER_CLASS启动实例并保持整个套件的应用程序上下文。您可以在测试方法级别使用@TestInstance(TestInstance.Lifecycle.PER_METHOD)实例化测试对象和应用程序上下文。 -
JUnit 中的
@TestMethodOrder注解用于定义测试套件中每个测试方法的执行顺序。 -
@Inject @Client:此注解注入了一个反应式 HTTP 客户端(内置在 Micronaut 中),用于对资源端点执行 RESTful 调用。
在设置好测试套件后,我们可以进行服务测试。在接下来的几节中,我们将介绍测试方法中的所有 REST 调用。
测试创建端点
VetResource 有一个用于创建新 Vet 的 POST 端点。它接受请求体中的 VetDTO。让我们使用 HTTP 客户端创建一个兽医:
@Test
@Order(1)
public void createVet() throws Exception {
int databaseSizeBeforeCreate =
vetRepository.findAll().size();
VetDTO = vetMapper.toDto(vet);
// Create the Vet
HttpResponse<VetDTO> response =
client.exchange(HttpRequest.POST("/api/vets", vetDTO),
VetDTO.class).blockingFirst();
assertThat(response.status().getCode()).isEqualTo
(HttpStatus.CREATED.getCode());
// Validate the Vet in the database
List<Vet> vetList = (List<Vet>)
vetRepository.findAll();
assertThat(vetList).hasSize(databaseSizeBeforeCreate +
1);
Vet testVet = vetList.get(vetList.size() - 1);
// Set id for further tests
vet.setId(testVet.getId());
assertThat(testVet.getFirstName()).isEqualTo
(DEFAULT_FIRST_NAME);
assertThat(testVet.getLastName()).isEqualTo
(DEFAULT_LAST_NAME);
}
在前面的测试中,我们正在创建一个 VetDTO 对象,并使用 HTTP 客户端调用 POST 端点。为了使反应式客户端返回可观察对象并执行伪同步调用,我们使用了 blockingFirst()。它阻塞线程直到可观察对象发出一个项,然后返回可观察对象发出的第一个项。最后,我们断言以确认预期的与实际的行为。
测试 GET 端点
在接下来的测试中,我们在 POST 端点服务测试中创建了一个新的兽医。我们可以仅利用持久化的兽医来测试 GET 端点:
@Test
@Order(3)
public void getAllVets() throws Exception {
// Get the vetList w/ all the vets
List<VetDTO> vets = client.retrieve(HttpRequest.GET
("/api/vets?eagerload=true"),
Argument.listOf(VetDTO.class)).blockingFirst();
VetDTO testVet = vets.get(vets.size() - 1);
assertThat(testVet.getFirstName()).isEqualTo
(DEFAULT_FIRST_NAME);
assertThat(testVet.getLastName()).isEqualTo
(DEFAULT_LAST_NAME);
}
@Test
@Order(4)
public void getVet() throws Exception {
// Get the vet
VetDTO testVet =
client.retrieve(HttpRequest.GET("/api/vets/" +
vet.getId()), VetDTO.class).blockingFirst();
assertThat(testVet.getFirstName()).isEqualTo
(DEFAULT_FIRST_NAME);
assertThat(testVet.getLastName()).isEqualTo
(DEFAULT_LAST_NAME);
}
在前面的测试中,我们正在测试两个端点,getVet() 和 getAllVets()。为了使反应式客户端返回结果,我们使用了 blockingFirst() 操作符。虽然 getAllVets() 将返回兽医列表,但 getVet() 只会返回所需的兽医对象。
测试更新端点
为了测试 update 端点,我们将利用创建的端点服务测试中持久化的兽医资源,因此,在 create 和 GET 调用之后使用一个顺序:
@Test
@Order(6)
public void updateVet() throws Exception {
int databaseSizeBeforeUpdate =
vetRepository.findAll().size();
// Update the vet
Vet updatedVet = vetRepository.findById(vet.getId());
updatedVet
.firstName(UPDATED_FIRST_NAME)
.lastName(UPDATED_LAST_NAME);
VetDTO updatedVetDTO = vetMapper.toDto(updatedVet);
@SuppressWarnings("unchecked")
HttpResponse<VetDTO> response =
client.exchange(HttpRequest.PUT("/api/vets",
updatedVetDTO), VetDTO.class)
.onErrorReturn(t -> (HttpResponse<VetDTO>)
((HttpClientResponseException)
t).getResponse()).blockingFirst();
assertThat(response.status().getCode()).isEqualTo
(HttpStatus.OK.getCode());
// Validate the Vet in the database
List<Vet> vetList = (List<Vet>)
vetRepository.findAll();
assertThat(vetList).hasSize(databaseSizeBeforeUpdate);
Vet testVet = vetList.get(vetList.size() - 1);
assertThat(testVet.getFirstName()).isEqualTo
(UPDATED_FIRST_NAME);
assertThat(testVet.getLastName()).isEqualTo
(UPDATED_LAST_NAME);
}
在前面的测试中,我们测试了updateVet()端点。我们首先获取了持久化的 vet,然后更新了名字和姓氏,在调用更新端点之前。最后,我们断言以确认实际行为符合预期行为。
测试删除端点
要测试delete端点,我们将利用在早期端点调用中持久化的 vet 资源。因此,我们将在create、GET和update调用之后使用一个订单:
@Test
@Order(8)
public void deleteVet() throws Exception {
int databaseSizeBeforeDelete =
vetRepository.findAll().size();
// Delete the vet
@SuppressWarnings("unchecked")
HttpResponse<VetDTO> response =
client.exchange(HttpRequest.DELETE("/api/vets/"+
vet.getId()), VetDTO.class)
.onErrorReturn(t -> (HttpResponse<VetDTO>)
((HttpClientResponseException)
t).getResponse()).blockingFirst();
assertThat(response.status().getCode()).isEqualTo
(HttpStatus.NO_CONTENT.getCode());
// Validate the database is now empty
List<Vet> vetList = (List<Vet>)
vetRepository.findAll();
assertThat(vetList).hasSize
(databaseSizeBeforeDelete - 1);
}
在前面的测试中,我们测试了deleteVet()端点。我们传递了之前持久化的vetId。在成功调用服务后,我们通过比较服务调用前后的数据库大小来断言以确认实际行为符合预期行为。
套件中的测试订单确保我们始终从头开始,并在完成套件中的所有测试后将其清理干净。与在测试方法级别设置和清理相比,这种模式对于服务测试既有优点也有缺点。您可以根据分析应用程序需求和是否使用套件设置和清理或测试方法级别来选择和选择一种模式。
在下一节中,我们将探索Testcontainers在集成测试中的精彩世界。
使用 Testcontainers 进行集成测试
Testcontainers是一个 Java 库,它优雅地将测试世界与 Docker 虚拟化相结合。使用Testcontainers库,我们可以设置、实例化和注入任何 Docker 容器到测试代码中。这种方法为执行集成测试开辟了许多途径。在测试套件或测试方法设置中,我们可以启动一个 Docker 化的数据库、Kafka 或邮件服务器或任何集成应用程序,执行集成测试,并在清理中销毁 Docker 化的应用程序。使用这种模式,我们接近生产环境,同时不会对环境产生任何测试后的副作用。
要了解我们如何使用Testcontainers库,我们将在与 MongoDB 集成的 pet-clinic-reviews 微服务上进行实验。在下一节中,我们将开始设置 Micronaut 应用程序中的Testcontainers。
在 Micronaut 应用程序中设置 Testcontainers
要在 pet-clinic-reviews 微服务中使用Testcontainers,请在项目的pom.xml文件中添加以下依赖项:
<!-- Test containers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.15.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>1.15.2</version>
<scope>test</scope>
</dependency>
通过导入一个 MongoDB 风格的测试容器,我们将能够利用 MongoDB Docker 工具包。在导入所需的Testcontainers依赖项后,让我们设置一个抽象类,它可以提供集成测试所需的任何应用程序容器:
public class AbstractContainerBaseTest {
public static final MongoDBContainer
MONGO_DB_CONTAINER;
static {
MONGO_DB_CONTAINER = new MongoDBContainer
(DockerImageName.parse("mongo:4.0.10"));
MONGO_DB_CONTAINER.start();
}
}
在AbstractContainerBaseTest中,我们配置并启动 Docker 中的 MongoDB 实例。这个容器的静态性质将简化访问并避免在测试套件或测试方法级别启动太多实例。Testcontainers优雅地,用最少的代码拉取 MongoDB Docker 镜像,启动它,并使其运行。
在下一节中,我们将使用 Testcontainers 为 VetReviewRepository 编写集成测试。
使用 Testcontainers 编写集成测试
在前一节中,我们介绍了如何使用 Testcontainers 创建 Docker 化的 MongoDB。我们将继续使用 Docker MongoDB 实例来测试 VetReviewRepository。让我们从测试套件和测试方法设置开始:
@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class VetReviewRepositoryIntegrationTest extends AbstractContainerBaseTest {
private VetReviewRepository;
@BeforeAll
void init() {
ApplicationContext context =
ApplicationContext.run(
PropertySource.of("test", Map.of
("mongodb.uri",
MONGO_DB_CONTAINER.getReplicaSetUrl()))
);
vetReviewRepository =
context.getBean(VetReviewRepository.class);
}
@BeforeEach
public void initTest() {
if (!MONGO_DB_CONTAINER.isRunning()) {
MONGO_DB_CONTAINER.start();
}
}
…
}
在测试套件设置中,我们正在覆盖 MongoDB 的应用程序属性。此外,我们从应用程序上下文中获取 VetReviewRepository 实例。这将确保我们注入的是与 Docker 化的 MongoDB 通信的仓库实例。在测试方法设置中,我们确保在执行测试方法之前 MongoDB 容器正在运行。由于我们在测试套件和测试方法级别进行了设置,让我们继续编写集成测试:
@Test
@Order(1)
public void saveVetReview() {
VetReview = new VetReview();
String reviewId = UUID.randomUUID().toString();
vetReview.setReviewId(reviewId);
vetReview.setVetId(1L);
vetReview.setRating(3D);
vetReview.setDateAdded(LocalDate.now());
vetReview.setComment("Good vet");
vetReviewRepository.save(vetReview);
VetReview savedVetReview =
vetReviewRepository.findByReviewId(reviewId);
assertThat(savedVetReview).isNotNull();
assertThat(savedVetReview.getReviewId()).isEqualTo
(reviewId);
}
在 saveVetReview() 测试中,我们创建一个新的兽医评审并调用 VetReviewRepository 来持久化这个评审。最后,我们通过获取并比较值来断言兽医评审已成功持久化。我们在测试套件中使用 @Order 模式,以便后续测试可以确保清理。
在本节中,我们探讨了如何通过启动数据库或其他服务组件的 Docker 实例来简化集成测试。我们通过创建 MongoDB 测试容器实现了 VetReviewRepository 的集成测试。
摘要
我们从测试金字塔开始,在单元测试、服务测试和集成测试的自动化测试中找到一个良好的平衡。我们首先介绍了单元测试的一些基础知识,例如利用模拟和间谍来编写单元测试。然后我们深入探讨了如何在 Micronaut 框架中使用响应式 HTTP 客户端编写服务测试以测试各种 RESTful 端点。最后,我们探索了集成测试的激动人心的世界,我们使用 Testcontainer 在测试环境中实例化 MongoDB。
本章为您提供了对 Micronaut 框架中不同级别测试(如单元、服务或集成)的深入理解。在细微而灵活的理论讨论之后,我们通过一些实用的示例来增强您在 Micronaut 框架中自动化测试的实践技能。
在下一章中,我们将探讨如何在 Micronaut 框架中处理微服务架构问题。
问题
-
单元测试是什么?
-
单元测试中的模拟是什么?
-
我们如何在 Micronaut 框架中使用 JUnit 进行模拟?
-
单元测试中的间谍活动是什么?
-
我们如何在 Micronaut 框架中使用 JUnit 进行间谍活动?
-
我们如何在 Micronaut 框架中编写服务测试?
-
测试容器是什么?
-
我们如何在 Micronaut 框架中使用测试容器?
-
你如何在 Micronaut 框架中编写集成测试?
第四部分:微服务部署
本节将探讨微服务工作的下一阶段,即部署。首先,你将专注于处理微服务的一些核心问题,例如服务发现、API 网关、应用程序配置和容错。随后,你将使用 Docker 容器部署示例pet-clinic应用程序。
本节包含以下章节:
-
第七章,处理微服务问题
-
第八章,部署微服务
第七章:处理微服务问题
任何微服务架构的实施如果没有处理一些基本的微服务问题都是不完整的,例如配置管理、API 文档、服务发现、API 网关和容错。迄今为止,我们关注的是微服务的分解之旅,例如如何在模块化微服务中分离关注点。为了实现无缝和统一的应用访问,我们需要微服务集成并暴露一个聚合接口。聚合接口使上游消费者能够像与单个微服务交互一样与后端微服务交互。
实施微服务的一个关键好处是容错性。如按需扩展、回退和断路器等容错机制使微服务无处不在且稳健。
在本章中,我们将探讨处理和实施以下微服务问题的方法:
-
外部化应用程序配置
-
记录服务 API 文档
-
实施服务发现
-
实施 API 网关
-
实施容错机制
到本章结束时,你将具备在 Micronaut 框架中处理和实施这些关键微服务问题的实际知识。
技术要求
本章中所有命令和技术说明均在 Windows 10 和 macOS 上运行。本章涵盖的代码示例可在本书的 GitHub 仓库中找到,网址为github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter07。
需要在开发环境中安装和设置以下工具:
-
Java SDK版本 13 或更高(我们使用了 Java 14)。
-
Maven – 这不是必需的,只有当你想使用 Maven 作为构建系统时才需要。然而,我们建议在任何开发机器上设置 Maven。有关下载和安装 Maven 的说明,请参阅
maven.apache.org/download.cgi。 -
开发 IDE – 根据您的偏好,可以使用任何基于 Java 的 IDE,但为了编写本章,我们使用了 IntelliJ。
-
Git – 有关下载和安装的说明,请参阅
git-scm.com/downloads。 -
PostgreSQL – 有关下载和安装的说明,请参阅
www.postgresql.org/download/。 -
MongoDB – MongoDB Atlas 提供了一个免费在线数据库即服务,存储空间高达 512 MB。然而,如果您更喜欢本地数据库,则有关下载和安装的说明,请参阅
docs.mongodb.com/manual/administration/install-community/。我们为本章的编写使用了本地安装。 -
REST 客户端 – 可以使用任何 HTTP REST 客户端。我们使用了 Advanced REST Client Chrome 插件。
-
Docker – 有关下载和安装 Docker 的说明,请参阅
docs.docker.com/get-docker/。
外部化应用程序配置
无论微服务应用的大小和复杂性如何,维护每个服务的配置设置的任务似乎是与微服务一起工作的最关键方面之一。解耦服务配置回到了我们之前关于分离关注点的讨论。在前面的章节中,我们看到了如何使用application.properties文件来处理服务配置。尽管这比在生产代码中硬编码这些配置要好,但这仍然不够。
任何微服务的关键要求之一是敏捷性。一个理想的微服务应该能够灵活且快速地应对用户需求的变化,以及处理代码缺陷或网络问题。话虽如此,每个企业应用都需要满足特定的合规性和审计要求,这意味着开发者通常不能直接将工作区中的代码工件部署到生产环境中。如果配置与服务代码逻辑解耦,那么我们可以轻松地构建一次(不包含配置)的工件,并将其部署到多个环境中(每个环境都可以启动自己的配置)。
在下一节中,我们将深入了解如何在 Micronaut 框架中管理分布式服务配置。
使用分布式配置管理来外部化配置
Micronaut 为pet-owner微服务提供了开箱即用的功能。
在 Consul 中实现配置存储
我们将使用 Docker 化的 Consul 实例。按照以下说明在 Docker 中安装和运行 Consul:
-
确保 Docker 应用正在您的 workspace/environment 中运行。然后打开一个 bash 终端(我使用的是 Git Bash)并运行以下命令:
docker run -d --name consul -p 8500:8500 consul -
等待 Docker 下载并安装Consul。
上述命令将启动一个单节点 Consul 实例,并在端口8500上公开。我们可以通过访问http://localhost:8500/的 Consul 网页界面来验证安装。
现在,要创建 Consul 中的配置存储,请按照以下说明操作:
-
打开
http://localhost:8500/并从顶部标题中选择键/值。 -
点击创建按钮。
-
在键或文件夹输入框中输入
config/pet-owner/application.yml。 -
在文本区域中添加
pet-owner的application.properties:micronaut: application: name: pet-owner router: static-resources: swagger: paths: classpath:META-INF/swagger mapping: /swagger/** datasources: default: url: "jdbc:postgresql://localhost:5432/postgres" username: postgres password: postgres driverClassName: org.postgresql.Driver jpa: default: entity-scan: packages: - com.packtpub.micronaut.domain properties: hibernate: show_sql: false dialect: org.hibernate.dialect.PostgreSQL95Dialect enable_lazy_load_no_trans: true
通过遵循前面的步骤,我们已经在 Consul 中为pet-owner微服务设置了一个键值存储。
将这些属性备份到pet-owner 资源文件夹是一个好习惯,因为 Docker 化的 Consul 实例在重启时可能会丢失配置。
我们可以通过导航到pet-owner application.yml文件来查看配置:

图 7.3 – 生成的 Swagger 工件
如图 7.3所示,Swagger 将在目标文件夹中创建pet-owner-service-1.0.yml工件。生成的纯文本工件可以在 Swagger 编辑器中打开,网址为editor.swagger.io/:

图 7.4 – 在编辑器中审查 Swagger YAML
使用 Swagger 编辑器审查生成的 YAML 非常直观。它提供了一个简单的用户界面,包括尝试 API 调用的选项。尽管 Micronaut 提供了一个生成 Swagger UI 视图的机制,但它非常新,需要很多更改。因此,使用标准的 Swagger 编辑器是一个更简单、更快捷的选择。
当我们有多支产品团队在独立微服务上工作时,API 文档就非常有用。此外,如果一个微服务暴露给最终用户,它就是了解服务端点的首选资源。继续我们的合并之旅,在下一节中,我们将为pet-clinic应用中的所有微服务实现服务发现。
实现服务发现
在传统的单体架构中,如果一个应用程序有多个服务,那么这些服务通常运行在固定且众所周知的位置(例如 URL 或端口)。这种对“众所周知”的理解被耦合到代码逻辑中,以实现服务间调用。消费者服务将调用另一个服务,要么在代码级别,要么使用硬编码的远程网络调用。
相比之下,微服务通常在虚拟化或容器化环境中运行,IP 端口是动态分配的。为了便于服务间调用,我们实现了服务发现。在服务发现模式中,所有微服务都将将自己的运行实例注册到服务发现中,然后客户端(即上游客户端或另一个服务)将与服务发现同步以获取所需服务的网络位置。此外,服务发现将对所有已注册服务进行持续的健康检查。在下一节中,我们将在 Micronaut 框架中使用 Consul 实现服务发现。
使用 Consul 实现服务发现
要启用服务发现,您需要在pom.xml项目文件中导入以下依赖项。我们已将其添加到pet-owner微服务中;现在也将其添加到pet-clinic和pet-clinic-reviews微服务中:
<!-- Service discovery -->
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-discovery-client</artifactId>
</dependency>
一旦将micronaut-discovery-client导入到服务中,我们就可以利用其服务发现功能。
为了与 Consul 同步服务,请在pet-clinic和pet-clinic-reviews微服务中进行以下更改:
consul:
client:
registration:
enabled: true
通过这些更改,我们使pet-clinic和pet-clinic-reviews微服务能够注册到 Consul 服务发现中。Micronaut 的micronaut-discovery-client实现已经包含了所需的工具,因此我们不需要进行任何代码更改。为了验证所有服务都已注册到 Consul,只需运行服务,它们将自动以它们的应用程序名称注册,如下面的屏幕截图所示:
注意
如果你正在使用pet-clinic和pet-clinic-reviews微服务,那么在服务启动之前启动 Apache Kafka 的 Docker 容器。

图 7.5 – 宠物诊所应用服务发现
在成功启动后,所有微服务都将将自己的实例注册到 Consul 服务发现中。我们可以在 Consul 的服务屏幕上查看正在运行的服务。
虽然服务发现通过集中运行时元数据(主要是网络位置)将所有服务统一在一起,但它仍然为上游消费者留下了一个缺口,因为我们还没有一个统一的接口。在下一节中,我们将为pet-clinic应用程序实现一个 API 网关,它将为所有客户端提供一个统一的接口。
实现 API 网关
为了进一步讨论 微服务架构 中的动态网络位置,我们现在将关注 API 网关。API 网关是一个编排服务,旨在为所有服务提供统一、无处不在的访问。尽管我们可以在后端运行多个微服务,但 API 网关可以为上游消费者提供一个统一的接口来访问它们。对于上游消费者来说,API 网关看起来是后端运行的唯一服务。在收到客户端请求后,API 网关将使用服务发现来确定调用哪个服务实例。
为了了解如何实现 API 网关,我们将向 pet-clinic 应用程序添加一个 API 网关服务。由于这个微服务是一个编排服务,我们可以将其称为 pet-clinic-concierge。网关服务之后的系统组件将如 图 7.6 所示:

图 7.6 – 带有服务发现和 API 网关的宠物诊所应用程序
在前面的图中,我们可以看到 pet-clinic 应用程序。pet-clinic-concierge 服务将实现 API 网关,任何服务消费者都将调用网关(而不是服务或服务发现),网关将通过与服务发现同步来确定服务实例。图中的双线连接器显示了实际服务请求如何在 pet-clinic 应用程序内部执行。
在下一节中,我们将深入了解如何在 pet-clinic-concierge 服务中实现 API 网关。
实现 API 网关服务
为了了解我们如何在 Micronaut 中实现 API 网关,我们将创建一个新的服务项目,名为 pet-clinic-concierge。要生成样板代码,请按照以下说明操作:
-
通过访问
micronaut.io/launch/打开 Micronaut Launch。 -
从 应用程序类型 下拉菜单中选择 Micronaut 应用程序。
-
从 Java 版本 下拉菜单中选择 Java 13。
-
在 基础包 输入框中输入
com.packtpub.micronaut。 -
在 名称 输入框中输入
pet-clinic-concierge。 -
从 功能 多选选项中选择以下功能:
config-consul discovery-consul
http-client
netflix-hystrix
netflix-ribbon
openapi
-
点击 生成项目 按钮,并选择 下载 Zip 选项。
Micronaut Launch 现在将为 pet-clinic-concierge 服务生成样板代码。在 Micronaut Launch 中,我们选择了发现和 OpenAPI,因此样板代码已经启用了这些功能并进行了配置。在下一节中,我们将探讨在 pet-clinic-concierge 服务中实现统一服务外观。
实现 API 网关的统一服务外观
首先,我们需要复制所有的pet-clinic-concierge服务。这些 DTO 将在实现所有pet-clinic服务的客户端中使用。复制pet-clinic-concierge项目中的所有 DTO。然后我们可以定义所有 RESTful 服务的客户端。
在下一节中,我们将专注于定义pet-owner微服务的客户端。
访问宠物主资源
要访问pet-owner资源,我们将在com.packtpub.micronaut.web.rest.client.petowner包下创建客户端。对于pet-owner微服务中的每个资源控制器,我们将声明一个 HTTP 客户端接口。以下是为OwnerResource声明的客户端接口:
@Client(id = "pet-owner")
public interface OwnerResourceClient {
@Post("/api/owners")
HttpResponse<OwnerDTO> createOwner(@Body OwnerDTO
ownerDTO);
@Put("/api/owners")
HttpResponse<OwnerDTO> updateOwner(@Body OwnerDTO
ownerDTO);
@Get("/api/owners")
HttpResponse<List<OwnerDTO>> getAllOwners(HttpRequest
request, Pageable pageable);
@Get("/api/owners/{id}")
Optional<OwnerDTO> getOwner(@PathVariable Long id);
@Delete("/api/owners/{id}")
HttpResponse deleteOwner(@PathVariable Long id);
}
@Client注解将实现一个具体的客户端。这个客户端将集成 Consul 服务实例的pet-owner服务。我们需要声明OwnerResource中公开的所有 RESTful 方法及其相对路径。
尽管在构建后我们将有一个具体的OwnerResourceClient,我们仍然需要将OwnerResourceClient中的各种 RESTful 方法映射到本地控制器。然后这个控制器将被暴露为上游消费者的服务外观。对于OwnerResourceClient,我们可以创建OwnerResourceClientController如下:
@Controller("/api")
public class OwnerResourceClientController {
@Inject
OwnerResourceClient ownerResourceClient;
@Post("/owners")
public HttpResponse<OwnerDTO> createOwner(OwnerDTO
ownerDTO) {
return ownerResourceClient.createOwner(ownerDTO);
}
@Put("/owners")
HttpResponse<OwnerDTO> updateOwner(@Body OwnerDTO
ownerDTO) {
return ownerResourceClient.updateOwner(ownerDTO);
}
@Get("/owners")
public HttpResponse<List<OwnerDTO>>
getAllOwners(HttpRequest request, Pageable pageable) {
return ownerResourceClient.getAllOwners(request,
pageable);
}
@Get("/owners/{id}")
public Optional<OwnerDTO> getOwner(@PathVariable Long
id) {
return ownerResourceClient.getOwner(id);
}
@Delete("/owners/{id}")
HttpResponse deleteOwner(@PathVariable Long id) {
return ownerResourceClient.deleteOwner(id);
}
}
在OwnerResourceClientController中,我们正在注入OwnerResourceClient。任何发送到OwnerResourceClientController的请求都将传递给客户端,然后客户端将调用一个pet-owner服务实例(在同步 Consul 服务发现之后)进行进一步处理。同样,你可以为pet-owner微服务中的其他资源实现Clients和Controllers。
接下来,我们将实现pet-clinic资源的服务外观。
访问宠物诊所资源
要访问pet-clinic资源,我们将在com.packtpub.micronaut.web.rest.client.petclinic包下创建客户端。对于pet-clinic微服务中的每个资源控制器,我们将声明一个 HTTP 客户端接口。以下是为VetResource声明的客户端接口:
@Client(id = "pet-clinic")
public interface VetResourceClient {
@Post("/api/vets")
HttpResponse<VetDTO> createVet(@Body VetDTO vetDTO);
@Put("/api/vets")
HttpResponse<VetDTO> updateVet(@Body VetDTO vetDTO);
…
}
@Client注解将使用服务发现中的pet-clinic服务实例实现一个具体的客户端。为了在服务外观上公开这些方法,我们将实现VetResourceClientController:
@Controller("/api")
public class VetResourceClientController {
@Inject
VetResourceClient vetResourceClient;
@Post("/vets")
public HttpResponse<VetDTO> createVet(VetDTO vetDTO) {
return vetResourceClient.createVet(vetDTO);
}
@Put("/vets")
public HttpResponse<VetDTO> updateVet(VetDTO vetDTO) {
return vetResourceClient.updateVet(vetDTO);
}
…
}
我们正在将VetResourceClient注入到VetResourceClientController中,因此任何发送到控制器的请求都将传递给客户端,然后客户端将调用一个pet-clinic服务实例进行进一步处理。
在下一节中,我们的重点将是实现pet-clinic-reviews的服务外观。
访问宠物诊所评论资源
对于访问pet-clinic-reviews资源,你将在com.packtpub.micronaut.web.rest.client.petclinicreviews包下创建客户端。我们将首先声明一个VetReviewResource的客户端接口:
@Client(id = "pet-clinic-reviews")
public interface VetReviewResourceClient {
@Post("/api/vet-reviews")
HttpResponse<VetReviewDTO> createVetReview(@Body
VetReviewDTO vetReviewDTO);
@Put("/api/vet-reviews")
HttpResponse<VetReviewDTO> updateVetReview(@Body
VetReviewDTO vetReviewDTO);
…
}
@Client 注解将实现一个具体的客户端,使用服务发现中的 pet-clinic-reviews 服务实例。为了在服务外观上公开这些方法,我们将实现 VetReviewResourceClientController:
@Controller("/api")
public class VetReviewResourceClientController {
@Inject
VetReviewResourceClient vetReviewResourceClient;
@Post("/vet-reviews")
public HttpResponse<VetReviewDTO>
createVetReview(VetReviewDTO vetReviewDTO) {
return vetReviewResourceClient.createVetReview
(vetReviewDTO);
}
@Put("/vet-reviews")
public HttpResponse<VetReviewDTO>
updateVetReview(VetReviewDTO vetReviewDTO) {
return vetReviewResourceClient.updateVetReview
(vetReviewDTO);
}
…
}
在这里,我们将 VetReviewResourceClient 注入到 VetReviewResourceClientController 中,并将进入控制器的请求传递给客户端,我们将在 pet-clinic-reviews 服务实例上调用该客户端以进行进一步处理。
在下一节中,我们将重点关注处理与微服务相关的容错问题。
实现容错机制
在微服务环境中,故障和失败是不可避免的。随着分布式组件数量的增加,每个组件内部的故障以及由它们交互产生的故障数量也在增加。任何微服务应用程序都必须为这些不幸的情况内置弹性。在本节中,我们将探讨和实现 Micronaut 框架中处理故障和失败的不同方法。
利用内置机制
Micronaut 是一个云原生框架,并具有内置的错误和故障处理能力。本质上,其容错性是由 @Retryable 和 @CircuitBreaker 注解驱动的,这些注解可以在任何 HTTP 客户端中使用。
在 HTTP 客户端使用 @Retryable
@Retryable 是一种简单但有效的容错机制——简单来说,它用于在出现故障时再次尝试。这些尝试可以在固定延迟后再次进行,并且可以继续进行,直到服务响应或没有更多尝试为止。
要使用 @Retryable,我们只需在客户端声明上添加注解。我们可以在 OwnerResource 上使用 @Retryable 如下所示:
@Retryable(attempts = "5", delay = "2s", multiplier = "1.5", maxDelay = "20s")
@Client(id = "pet-owner")
public interface OwnerResourceClient {
…
}
通过在 OwnerResourceClient 上使用 @Retryable,您可以在所有方法上启用容错性。如果 pet-owner 微服务关闭,则 OwnerResourceClient 将尝试最多五次建立通信。我们可以使用以下设置配置 @Retryable:
-
尝试次数:这表示客户端可以尝试的最大重试次数。默认为 3 次尝试。
-
延迟:这表示重试之间的延迟,默认为 1 秒。
-
乘数:这指定了用于计算延迟的乘数,默认为 1.0。
-
最大延迟:这指定了最大整体延迟,默认为空。如果指定,任何重试尝试将在达到最大延迟限制时停止。
@Retryable 非常适合处理暂时性的故障,但对于持续时间较长的故障,我们需要使用断路器模式。在下一节中,我们将看到如何在 Micronaut 框架中使用断路器。
在 HTTP 客户端使用 @CircuitBreaker
正如我们讨论的,在高度分布的系统,如微服务中,失败是不可避免的。在微服务架构中,如果一个服务崩溃,有一个防御机制可以帮助避免在服务恢复健康之前,通过更多的请求来拥挤服务流量。这个机制被称为 断路器。在正常情况下,电路是打开的,接受请求。在失败实例中,计数器会增加,直到达到指定的阈值。达到阈值后,电路进入关闭状态,服务将立即响应错误,避免任何超时。断路器有一个内部轮询机制来确定服务的健康状态,如果服务再次健康,则电路将回到打开状态。
我们可以简单地使用 Micronaut 内置的注解来指定 HTTP 客户端上的断路器。让我们在 PetResourceClient 中实现一个断路器:
@Client(id = "pet-owner")
@CircuitBreaker(delay = "5s", attempts = "3", multiplier = "2", reset = "300s")
public interface PetResourceClient {
@Post("/api/pets")
HttpResponse<PetDTO> createPet(@Body PetDTO petDTO);
@Put("/api/pets")
HttpResponse<PetDTO> updatePet(@Body PetDTO petDTO);
…
}
在前面的断路器实现中,如果 PetResource 端点失败,那么 PetResourceClient 将尝试五次,第一次尝试等待 3 秒,后续尝试的等待时间将是前一次的两倍。五次尝试后,如果服务仍然没有响应,那么电路将进入关闭状态。它将在 5 分钟的重置间隔后再次尝试访问,以检查服务是否已经健康。
使用 @Fallback 为 HTTP 客户端
在断路器实现的常见情况下,通常会有 Feign 客户端 或 回退。当电路关闭时,而不是引发服务器错误,回退实现可以处理请求并正常响应。这在实际服务调用可能返回回退也可以返回的结果时尤其有效。
在以下 PetResourceClient 中断路器的示例中,我们可以创建一个简单的回退机制,当电路关闭时,它会处理传入的请求:
@Fallback
public class PetResourceFallback implements PetResourceClient {
@Override
public HttpResponse<PetDTO> createPet(PetDTO petDTO) {
return HttpResponse.ok();
}
@Override
public HttpResponse<PetDTO> updatePet(PetDTO petDTO) {
return HttpResponse.ok();
}
@Override
public HttpResponse<List<PetDTO>>
getAllPets(HttpRequest request, Pageable pageable) {
return HttpResponse.ok();
}
@Override
public Optional<PetDTO> getPet(Long id) {
return Optional.empty();
}
@Override
public HttpResponse deletePet(Long id) {
return HttpResponse.noContent();
}
}
PetResourceFallback 为所有 PetResource 端点提供了默认实现,当 PetResource 不可访问时,它将提供一个优雅的响应。在这个例子中,我们从所有端点返回一个空响应。你可以调整实现,创建一个所需的默认响应。
摘要
在本章中,你学习了如何在 Micronaut 框架中处理各种微服务问题。我们通过使用 Consul 外部化应用程序配置来启动这次旅程,并了解了为什么在微服务中需要分布式配置管理。然后,我们深入探讨了如何使用 OpenAPI 和 Swagger 自动化 API 文档。稍后,我们讨论了服务发现和 API 网关,并在宠物诊所应用程序中实现了这些功能。
最后,我们探讨了容错的需求以及如何在 Micronaut 框架中简单地使用内置机制来构建微服务应用程序的弹性。
本章为你提供了处理与服务发现、API 网关和容错相关的各种微服务问题的所有第一手知识。本章通过在 pet-clinic 应用程序中添加这些方面,采用了实用的方法。
在下一章中,你将探索部署 pet-clinic 微服务应用程序的各种方法。
问题
-
分布式配置管理是什么?
-
你如何在 Consul 中实现配置存储?
-
你如何在 Micronaut 框架中使用 Swagger 自动化 API 文档的生成过程?
-
服务发现是什么?
-
你如何在 Micronaut 框架中实现服务发现?
-
在微服务架构中,API 网关是什么?
-
你如何在 Micronaut 框架中实现 API 网关?
-
Micronaut 中的
@Retryable是什么? -
Micronaut 中的
@CircuitBreaker是什么? -
你如何在 Micronaut 框架中实现断路器?
-
你如何在 Micronaut 中实现回退机制?
第八章:部署微服务
部署的直译意思是将资源投入有效行动。因此,在微服务环境中,这意味着将微服务投入有效行动。任何服务部署都是一个多步骤的过程,通常涉及构建工件然后将工件推送到运行环境。在微服务世界中,一个有效的微服务部署策略至关重要。本质上,在规划部署过程时,我们需要注意以下几点:
-
继续采用关注点分离的模式,并为每个微服务的工件构建过程进行自我隔离。
-
解耦微服务内部的任何连接需求,并让服务发现或接近服务发现的实现来处理微服务绑定。
-
实施一个无缝的部署过程,能够以统一和自动化的方式实例化所有微服务应用程序组件。
在本章中,我们将深入探讨上述关注点,同时涵盖以下主题:
-
构建容器工件
-
部署容器工件
到本章结束时,你将对微服务部署的这些方面有深入的了解。
技术要求
本章中所有命令和技术说明均在 Windows 10 和 macOS 上运行。本章涵盖的代码示例可在本书的 GitHub 仓库中找到:
github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter08
以下工具需要在开发环境中安装和设置:
-
Java SDK:版本 13 或以上(我们使用了 Java 14)。
-
Maven:这是可选的,仅在你希望使用 Maven 作为构建系统时需要。然而,我们建议在任何开发机器上设置 Maven。下载和安装 Maven 的说明可以在
maven.apache.org/download.cgi找到。 -
开发 IDE:根据你的偏好,可以使用任何基于 Java 的 IDE,但为了编写本章,使用了 IntelliJ。
-
Git:下载和安装的说明可以在
git-scm.com/downloads找到。 -
PostgreSQL:下载和安装的说明可以在
www.postgresql.org/download/找到。 -
MongoDB:MongoDB Atlas 提供了一个免费在线数据库即服务,存储空间高达 512 MB。然而,如果你更喜欢本地数据库,下载和安装的说明可以在
docs.mongodb.com/manual/administration/install-community/找到。我们为本章的编写使用了本地安装。 -
REST 客户端:任何 HTTP REST 客户端都可以使用。我们使用了 Advanced REST Client Chrome 插件。
-
Docker:有关下载和安装 Docker 的说明,请参阅
docs.docker.com/get-docker/。
构建容器工件
部署任何应用程序的第一步是构建所有必需的工件。构建一个工件通常涉及检出源代码、编译并创建可部署的工件。在微服务方面,通常,这种可部署的形式是一个 Docker 容器镜像。Docker 镜像优雅地解耦了运行时拓扑需求。Docker 镜像是平台无关的,可以部署到任何运行 Docker 的主机机器。在下一节中,我们将深入了解如何在我们的 pet-clinic 应用程序中为微服务构建 Docker 镜像。
使用 Jib 容器化 Micronaut 微服务
Jib 是来自 Google 的一个容器化框架,它可以无缝地与 Java 构建框架(如 Maven 或 Gradle)结合使用来构建容器镜像。Jib 极大地简化了创建容器(Docker)镜像的过程。让我们快速看看没有 Jib 创建 Docker 镜像的工作流程:

图 8.3 – 验证本地 Docker 注册表中的宠物主人镜像
在前面的屏幕截图中,我们可以看到docker images的输出。一个名为pet-owner-0.1-image的镜像存储在本地 Docker 注册表中。
容器化宠物诊所微服务
为了使宠物诊所微服务容器就绪,对application.properties文件进行以下更改:
micronaut:
application:
name: pet-clinic
server:
port: 32582
kafka:
bootstrap:
servers: kafka:9092
datasources:
default:
url:
«jdbc:postgresql://host.docker.internal:5432/postgres»
username: postgres
password: postgres
driverClassName: org.postgresql.Driver
consul:
client:
default-zone: "consul:8500"
registration:
enabled: true
…
在应用程序属性更改中需要思考的以下几点:
-
使用
host.docker.internal而不是 localhost 来指向主机操作系统(Docker 外部)中安装的 Postgres。 -
consul服务。要使用来自宠物诊所 Docker 容器的 Docker 化consul,我们需要指定服务名称而不是 localhost。 -
Kafka 服务器: 在我们的 Docker 服务中,我们将配置一个Kafka服务。
-
端口: 我们指定了一个固定的端口来运行宠物诊所微服务,因为我们将在部署过程中暴露这个端口。
在进行前面的应用程序配置更改后,我们可以继续容器化。为了容器化宠物诊所微服务,我们将使用 Jib。在项目的pom文件构建设置中进行以下更改:
<build>
<plugins>
...
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.8.0</version>
<configuration>
<from>
<image>openjdk:13-jdk-slim</image>
</from>
<to>
<image>pet-clinic-0.1-image</image>
</to>
<container>
<creationTime>${maven.build.timestamp}</creationTime>
</container>
</configuration>
</plugin>
</plugins>
</build>
在前面的pom更改中,我们使用jib-maven-plugin来构建容器镜像。<configuration>部分指定了 Docker 配置,例如<from>镜像(指向使用 JDK 13)。为了给创建的镜像命名,我们使用<to>结合<creationTime>来正确地在镜像上标记时间坐标。
构建镜像的步骤如下:
-
打开终端并切换到
pet-clinic根目录。 -
在终端中输入并运行
mvn compile jib:dockerBuild命令。 -
等待命令完成。
上述指令将在终端中创建一个本地 Docker 镜像,可以使用 docker images | grep pet-clinic 命令进行验证。
容器化 pet-clinic-reviews 微服务
要使 pet-clinic-reviews 微服务容器准备就绪,请在 application.properties 文件中进行以下更改:
micronaut:
application:
name: pet-clinic-reviews
server:
port: 32583
kafka:
bootstrap:
servers: kafka:9092
mongodb:
uri: mongodb://mongodb:mongodb@host.docker.internal:27017/pet-clinic-reviews
databaseName: pet-clinic-reviews
collectionName: vet-reviews
consul:
client:
default-zone: «consul:8500»
registration:
enabled: true
…
在应用属性更改中,以下是一些需要思考的事项:
-
使用
host.docker.internal而不是 localhost 来指向安装在宿主操作系统(Docker 外部)上的 MongoDB 实例。 -
consul服务。要从 pet-clinic-concierge Docker 容器中使用 Docker 化的consul,我们需要指定服务名称而不是 localhost。 -
kafka服务。要从pet-clinic-reviewsDocker 容器中使用 Docker 化的consul,我们需要指定服务名称而不是 localhost。 -
端口:我们指定了一个固定端口来运行 pet-clinic-reviews 微服务,因为我们将在部署中稍后公开此端口。
在进行上述应用程序配置更改后,我们可以继续容器化。要容器化 pet-clinic-reviews 微服务,我们将使用 Jib。在项目的 pom 文件构建设置中进行以下更改:
<build>
<plugins>
...
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.8.0</version>
<configuration>
<from>
<image>openjdk:13-jdk-slim</image>
</from>
<to>
<image>pet-clinic-reviews-0.1-image</image>
</to>
<container>
<creationTime>${maven.build.timestamp}</creationTime>
</container>
</configuration>
</plugin>
</plugins>
</build>
在前面的 pom 更改中,我们使用 jib-maven-plugin 来构建容器镜像。<configuration> 部分指定了 Docker 配置,如 <from> 镜像(指向使用 JDK 13)。为了给创建的镜像命名,我们使用 <to> 结合 <creationTime> 来正确地在镜像上标记时间坐标。
要构建镜像,请执行以下步骤:
-
打开终端并切换到
pet-clinic-reviews根目录。 -
在终端中输入并运行
mvn compile jib:dockerBuild命令。 -
等待命令完成。
上述指令将在终端中创建一个本地 Docker 镜像,可以使用 docker images | grep pet-clinic-reviews 命令进行验证。
容器化 pet-clinic-concierge 微服务
要使 pet-clinic-concierge 微服务容器准备就绪,请在 application.properties 文件中进行以下更改:
micronaut:
application:
name: pet-clinic-concierge
server:
port: 32584
config-client:
enabled: true
consul:
client:
default-zone: "consul:8500"
registration:
enabled: true
…
在应用属性更改中,以下是一些需要思考的事项:
-
consul服务。要从 pet-clinic-concierge Docker 容器中使用 Docker 化的consul,我们需要指定服务名称而不是 localhost。 -
pet-clinic-concierge微服务,因为我们将在部署中稍后公开此端口。
在进行上述应用程序配置更改后,我们可以继续容器化。要容器化 pet-clinic-concierge 微服务(API 网关),我们将使用 jib。在项目的 pom 文件构建设置中进行以下更改:
<build>
<plugins>
...
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.8.0</version>
<configuration>
<from>
<image>openjdk:13-jdk-slim</image>
</from>
<to>
<image>pet-clinic-concierge-0.1-image</image>
</to>
<container>
<creationTime>${maven.build.timestamp}</creationTime>
</container>
</configuration>
</plugin>
</plugins>
</build>
在前面的pom更改中,我们使用jib-maven-plugin来构建容器镜像。<configuration>部分指定了 Docker 配置,如<from>镜像(指向使用 JDK 13)。为了正确标记镜像上的时间坐标,我们使用<to>结合<creationTime>来命名创建的镜像。
要构建镜像,请执行以下步骤:
-
打开终端并切换到
pet-clinic-concierge根目录。 -
在终端中输入并运行
mvn compile jib:dockerBuild命令。 -
等待命令完成。
前面的说明将创建一个本地 Docker 镜像,您可以在终端中使用docker images | grep pet-clinic-concierge命令进行验证。
现在,我们已经容器化了所有微服务。我们也可以在 Docker 仪表板中验证这些镜像:

图 8.4 – 在 Docker UI 中验证 Docker 镜像
在 Docker UI 中,我们可以简单地转到Images,然后在LOCAL选项卡下,我们可以过滤所有 pet-clinic 应用程序的镜像。在下一节中,我们将在 pet-clinic 应用程序的部署中使用这些 Docker 镜像。
部署容器工件
在前面的章节中,我们探讨了如何使用Jib简化微服务容器化。在本节中,我们将深入了解如何使用docker-compose与 Docker 编排相结合,使端到端部署无缝且统一。
使用 docker-compose 部署 pet-clinic 服务
docker-compose是 Docker 生态系统中的一个工具,它在定义和部署多容器应用程序时非常直观。使用简单的 YAML 风格语法,我们可以设置所有服务及其依赖关系,并使用单个命令部署整个应用程序。我们将为 pet-clinic 应用程序创建一个docker-compose文件,涵盖所有必要的服务/组件,包括微服务、服务发现和 Apache Kafka 生态系统。
首先,让我们在docker-compose中定义以下辅助服务:
version: '3'
services:
consul:
image: bitnami/consul:latest
ports:
- '8500:8500'
zookeeper:
image: bitnami/zookeeper:3-debian-10
ports:
- 2181:2181
volumes:
- zookeeper_data:/pet-clinic-reviews
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka:
image: bitnami/kafka:2-debian-10
ports:
- 9094:9094
volumes:
- kafka_data:/pet-clinic-reviews
environment:
- KAFKA_BROKER_ID=1
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- ALLOW_PLAINTEXT_LISTENER=yes
- KAFKA_LISTENERS=INTERNAL://kafka:9092,OUTSIDE://
kafka:9094
- KAFKA_ADVERTISED_LISTENERS=INTERNAL://kafka:9092,OUTSIDE://localhost:9094
- KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,OUTSIDE:PLAINTEXT
- KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
depends_on:
- zookeeper
kafdrop:
image: obsidiandynamics/kafdrop
ports:
- 9100:9000
environment:
- KAFKA_BROKERCONNECT=kafka:9092
- JVM_OPTS=-Xms32M -Xmx64M
depends_on:
- kafka
…
从前面的代码中,我们看到docker-compose文件。我们首先定义一个用于consul服务发现的consul服务。我们将consul暴露在端口8500上。此外,我们将定义 Apache Kafka 生态系统的服务;即 Zookeeper、Kafka 和 Kafdrop UI。一旦这些服务在docker-compose文件中定义,我们就可以继续处理pet-clinic微服务。请参考以下代码:
…
pet-owner:
image: "pet-owner-0.1-image"
ports:
- "32581:32581"
depends_on:
- consul
pet-clinic:
image: "pet-clinic-0.1-image"
ports:
- "32582:32582"
depends_on:
- kafka
- consul
pet-clinic-reviews:
image: "pet-clinic-reviews-0.1-image"
ports:
- "32583:32583"
depends_on:
- kafka
- consul
pet-clinic-concierge:
image: "pet-clinic-concierge-0.1-image"
ports:
- "32584:32584"
depends_on:
- consul
…
在定义pet-clinic微服务的配置时,我们可以使用depends_on来指定依赖关系。这将确保 Docker 按照依赖顺序实例化服务。此外,对于部署每个服务,我们将使用pet-clinic微服务的 Docker 镜像。
一旦为pet-clinic应用程序定义了docker-compose文件,请参考以下说明来部署pet-clinic应用程序:
-
打开
bash终端。 -
将目录更改为存储
docker-compose文件的位置。 -
输入并运行
docker compose up命令。 -
等待 Docker 根据
docker-compose文件指定的容器实例化。
在成功运行docker-compose命令后,我们可以在 Docker 仪表板上验证pet-clinic应用程序,如下所示:

图 8.5 – 在 Docker 仪表板上验证宠物诊所的部署
在前面的截图上,你可以看到宠物诊所应用程序中所有服务的状态。你可以点击一个服务来监控日志和访问 Web 界面(如果有)。此外,我们可以检查consul服务发现以了解宠物诊所微服务的健康状况。参看以下截图:

图 8.6 – 在服务发现中验证微服务健康
在consul服务发现中,我们可以观察到每个微服务实例的健康状况。在前面的截图上,我们可以看到所有pet-clinic微服务都在正常运行。
微服务容器编排是任何部署策略的基础。在本章的范围内,我们涵盖了宠物诊所应用程序的本地 Docker 部署,但构建的容器镜像可以在任何地方部署,无论是本地还是云环境,使用容器编排工具如docker-compose。
摘要
在本章中,我们通过讨论使 Micronaut 微服务容器就绪来启动我们的理解。随后,我们深入探讨了使用 Jib 为每个微服务创建容器镜像。我们看到了如何使用docker-compose定义所有服务容器配置,并使用单个命令无缝部署所有必需的服务组件。
本章通过为您提供容器化和自动化部署的第一手知识,增强了您的 Micronaut 微服务之旅的部署方面。这一技能集在微服务应用程序的开发和维护中非常受欢迎。
在下一章中,我们将探讨在 Micronaut 中监控宠物诊所应用程序不同方面的各种方法。
问题
-
什么是 Jib?
-
我们如何使用 Jib 在 Micronaut 中创建 Docker 容器?
-
我们如何从 Micronaut 的 Docker 容器连接到 localhost?
-
我们如何使用
docker-compose在 Micronaut 中部署多服务应用程序? -
我们如何执行 Micronaut 微服务应用程序的 Docker 容器化?
第五部分:微服务维护
本节将涵盖 Micronaut 框架中微服务的维护方面,并包含以下章节:
- 第九章, 分布式日志、追踪和监控
第九章: 分布式日志记录、跟踪和监控
微服务应用程序通常在多个主机上运行多个微服务。对于上游消费者,API 网关提供了一个一站式商店接口,用于访问所有应用程序端点。对 API 网关的任何请求都会分散到一个或多个微服务。这种分布式扩散的请求处理增加了维护基于微服务的应用程序的挑战。如果发生任何异常或错误,很难确定哪个微服务或分布式组件有故障。此外,任何有效的微服务实现都必须积极处理维护挑战。
在本章中,我们将探讨以下主题:
-
分布式日志记录: 我们如何实现分布式微服务的日志聚合,以便可以在一个地方访问和索引应用程序日志?
-
分布式跟踪: 我们如何跟踪可能分散到多个主机环境上运行的多个微服务的用户请求的执行?
-
分布式监控: 我们如何持续监控所有服务组件的关键性能指标,以获得系统健康状况的整体视图?
通过收集这三种不同类型的数据——日志记录、跟踪和监控——我们增强了系统的可观察性。在任何时间点访问这些遥测数据,我们可以直观且精确地获得请求在系统中执行的整体上下文。要了解更多关于可观察性的信息,我们将通过实际的 pet-clinic 应用程序探索分布式日志记录、跟踪和监控的微服务模式。
到本章结束时,你将具备在 Micronaut 框架中实现这些可观察性模式的良好知识。
技术要求
本章中的所有命令和技术说明都是在 Windows 10 和 macOS 上运行的。本章涵盖的代码示例可在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter09。
以下工具需要在开发环境中安装和设置:
-
Java SDK: 版本 13 或以上(我们使用了 Java 14)。
-
Maven: 这不是必需的,只有当您想使用 Maven 作为构建系统时才需要。然而,我们建议在任何开发机器上设置 Maven。有关下载和安装 Maven 的说明,请参阅
maven.apache.org/download.cgi。 -
开发 IDE: 根据您的偏好,可以使用任何基于 Java 的 IDE,但为了编写本章,我们使用了 IntelliJ。
-
Git: 有关下载和安装 Git 的说明,请参阅
git-scm.com/downloads。 -
PostgreSQL:有关下载和安装 PostgreSQL 的说明,请参阅
www.postgresql.org/download/。 -
MongoDB:MongoDB Atlas 提供了一个免费的在线数据库即服务,存储空间高达 512 MB。然而,如果更喜欢本地数据库,则有关下载和安装的说明,请参阅
docs.mongodb.com/manual/administration/install-community/。我们为本章使用了本地安装。 -
REST 客户端:可以使用任何 HTTP REST 客户端。我们使用了 Advanced REST Client Chrome 插件。
-
Docker:有关下载和安装 Docker 的说明,请参阅 https://docs.docker.com/get-docker/。
分布式日志记录在 Micronaut 微服务中
正如我们在章节引言中讨论的,在基于微服务的应用程序中,用户请求在运行在不同主机环境上的多个微服务上执行。因此,日志消息会分散在多个主机机器上。这对维护应用程序的开发者或管理员来说是一个独特的挑战。如果出现故障,那么确定问题将变得很困难,因为你必须登录到多个主机机器/环境,grep 日志,并将它们组合起来以便理解。
在本节中,我们将深入了解微服务中分布式日志记录的日志聚合。
如其名所示,日志聚合是将各种微服务和应用程序组件产生的日志组合在一起。日志聚合通常涉及以下组件:
-
日志生产者:这是在执行控制流时产生日志的任何微服务或分布式组件。
-
日志分发器:日志分发器负责收集日志生产者产生的日志并将它们分发到集中存储。
-
日志存储:日志存储持久化和索引所有应用程序组件和微服务产生的日志。
-
日志可视化器:日志可视化器提供了一个用户界面,用于访问、搜索和过滤存储在日志存储中的日志。
在 pet-clinic 应用程序上下文中,我们将实现 ELK Stack(即 Elasticsearch、Logstash、Kibana)以实现分布式日志记录。请参考以下图示:

Figure 9.2 – 在 Docker 仪表板中验证 ELK
在这里,您可以验证 ELK 实例化。默认情况下,Elasticsearch 在端口 9200 上运行,Logstash 在 5000,Kibana 在端口 5601。
在下一节中,我们将修改我们的 pet-clinic 微服务,以便将日志发送到 Logstash 实例。
将 Logstash 与 Micronaut 微服务集成
要将 Logstash 集成到 pet-clinic 微服务中,我们将利用 Logback。我们将向 Logback 引入一个新的追加器,可以将日志发送到之前创建的 Logstash 实例。
在本地检出的 docker-elk 目录中,您可以验证 Logstash 已配置以下设置:
input {
tcp {
port => 5000
type => syslog
codec => json_lines
}
}
filter {
grok {
match => [ "message", "%{GREEDYDATA}" ]
}
mutate {
add_field => { "instance_name" => "%{app_name}-
%{host}:%{app_port}" }
}
}
output {
stdout { # This will log all messages so that we can
confirm that Logstash is receiving them
codec => rubydebug
}
elasticsearch {
hosts => [
"${XPACK_MONITORING_ELASTICSEARCH_HOSTS}" ]
user =>
"${XPACK_MONITORING_ELASTICSEARCH_USERNAME}"
password =>
"${XPACK_MONITORING_ELASTICSEARCH_PASSWORD}"
index => "logstash-%{+YYYY.MM.dd}"
}
}
在 logstash.config 中,我们有以下三个部分:
-
input:Logstash 有能力聚合超过 50 种不同的日志源。input配置 Logstash 以配置一个或多个输入源。在我们的配置中,我们正在启用端口5000上的tcp输入。 -
filter:Logstash 的filter提供了一种简单的方法将传入的日志转换为过滤器定义的日志事件。然后,这些事件被推送到日志存储。在上述配置中,我们使用grok过滤器与mutate一起添加额外的信息(app_name和app_port)到日志事件中。 -
output:output部分配置接收源,以便 Logstash 可以将日志事件推送到配置的输出源。在上述配置中,我们正在配置标准输出和 Elasticsearch 以接收生成的日志事件。
到目前为止,我们已经启动了一个带有 Logstash 配置为接收、转换并将日志事件发送到 Elasticsearch 的 ELK Docker 实例。接下来,我们将对 pet-clinic 微服务进行必要的修改,以便日志可以发送到 Logstash。
配置微服务以进行分布式日志记录
为了使 pet-clinic 微服务能够聚合并将日志发送到 Logstash,我们需要将以下 logstash-logback-encoder 依赖项添加到所有微服务的 pom.xml 文件中:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.3</version>
</dependency>
通过导入 logstash-logback-encoder,我们可以在 logback.xml 中利用 net.logstash.logback.appender.LogstashTcpSocketAppender 类。此类提供了 logstash 追加器,可以将微服务的日志发送到 Logstash 服务器。
通过以下方式修改所有微服务的logback.xml,添加 Logstash appender:
<appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<param name="Encoding" value="UTF-8"/>
<remoteHost>host.docker.internal</remoteHost>
<port>5000</port>
<encoder
class=»net.logstash.logback.encoder.LogstashEncoder»/>
</appender>
…
<root level="debug">
<appender-ref ref="logstash"/>
<appender-ref ref="stdout"/>
</root>
Logstash appender 将帮助将日志发送到localhost:5000,因为我们正在 Docker 容器中运行 Logstash,所以我们提供地址为host.docker.internal。
此外,我们还需要通过使用appender-ref将 appender 添加到根级别。
此外,我们还需要为app_name和app_port定义两个属性。这些是 Logstash 将用于创建带有应用程序信息的所需日志事件的过滤器配置。这是我们的做法:
<property scope="context" name="app_name" value="pet-owner "/>
<property scope="context" name="app_port" value="32581"/>
在前面的代码片段中,我们为pet-owner微服务添加了所需的属性。我们需要在所有服务中添加类似的属性,以便 Logstash 可以生成特定于服务的日志事件。
验证pet-clinic应用程序中的分布式日志
为了验证 Logstash 是否正在从pet-clinic应用程序中的所有微服务接收日志,我们需要重新构建 Docker 镜像并重新部署pet-clinic应用程序。执行以下步骤:
-
在
pet-owner微服务根目录中打开终端:a. 运行
jib命令构建 Docker 镜像mvn compile jib:dockerBuild。b. 等待
jib构建并将 Docker 镜像上传到本地 Docker 镜像仓库。 -
在
pet-clinic微服务根目录中打开终端:a. 运行
jib命令构建 Docker 镜像mvn compile jib:dockerBuild。b. 等待
jib构建并将 Docker 镜像上传到本地 Docker 镜像仓库。 -
在
pet-clinic-reviews微服务根目录中打开终端:a. 运行
jib命令构建 Docker 镜像mvn compile jib:dockerBuild。b. 等待
jib构建并将 Docker 镜像上传到本地 Docker 镜像仓库。 -
在
pet-clinic-concierge微服务根目录中打开终端:a. 运行
jib命令构建 Docker 镜像mvn compile jib:dockerBuild。b. 等待
jib构建并将 Docker 镜像上传到本地 Docker 镜像仓库。 -
打开任何 Bash 终端并将目录更改为您已检出
pet-clinicdocker-compose.yml文件的位置:a. 运行
docker compose up –d。b. 等待 Docker 完成启动
pet-clinic堆栈。
一旦pet-clinic应用程序在 Docker 中实例化并运行,我们需要配置 Kibana 以索引和显示日志。要在 Kibana 中索引日志,执行以下步骤:
-
导航到
http://localhost:5601上的 Kibana 并使用docker-elk目录中.env文件中提到的 Elasticsearch 凭据登录。 -
打开主页并点击连接到您的 Elasticsearch 索引超链接。点击连接到您的 Elasticsearch 索引后,Kibana 将提供一个设置页面来连接您的索引(见以下截图):![图 9.3 – 在 Kibana 中连接 Elasticsearch 索引
![图片]()
图 9.3 – 在 Kibana 中连接 Elasticsearch 索引
Kibana 提供了一个直观的用户界面来连接到你的 Elasticsearch 索引。点击截图中的高亮部分,并按照 Kibana 展示的步骤进行操作。
-
当设置页面加载时,在索引模式文本框中输入
logstash。 -
点击下一步按钮,并在配置设置中选择@timestamp。
-
然后,点击创建索引模式。
在成功建立索引连接后,你可以转到Discover页面,并按以下方式查看应用程序日志:

图 9.4 – 在 Discover 中查看应用程序日志
在app_name和app_port上,我们可以深入查看这两个参数,以查看特定微服务的日志。
因此,现在我们已经实现了提供直观访问微服务日志的 ELK Stack 分布式日志记录。如果任何微服务出现故障,你可以直接访问 Kibana 并查看/搜索日志。随着你向运行时拓扑中添加更多的微服务实例和组件,ELK 将简化日志管理。
在下一节中,我们将深入了解分布式追踪以及如何在pet-clinic应用程序中实现分布式追踪。
Micronaut 微服务中的分布式追踪
分布式追踪是系统追踪和观察分布式系统中请求执行流程的能力,通过收集数据来追踪请求从一个服务组件到另一个服务组件的过程。这些追踪数据会编译出每个服务所花费的时间以及端到端的执行流程。时间指标可以帮助定位性能问题,例如哪个服务组件是执行流程的瓶颈以及原因。
追踪是一种类似于甘特图的数据结构,它将追踪信息存储在跨度中。每个跨度将保持特定服务组件中的执行流程的追踪。此外,跨度可以引用父跨度以及子跨度。参考以下图示:

图 9.5 – 分布式追踪
在前面的图中,我们可以看到用户界面应用程序加载foo页面时的追踪/跨度。它首先调用foo对象,然后分别调用Bars和Bazs来处理foo。整个执行所花费的时间将是各个服务组件执行时间的累积总和。
在下一节中,我们将实现pet-clinic应用程序中的分布式追踪解决方案。
在 Micronaut 中实现分布式追踪
为了在 Micronaut 中实际操作分布式追踪,我们将在pet-clinic应用程序中实现基于 Zipkin 的追踪。
我们将在 Docker 中运行 Zipkin 实例。要在 Docker 中运行 Zipkin,请执行以下步骤:
-
打开任何 Bash 终端。
-
运行
docker run -d -p 9411:9411 openzipkin/zipkin命令。等待 Docker 在端口
9411下载并实例化 Zipkin。实例化成功后,您可以通过访问http://localhost:9411/zipkin来验证 Zipkin。 -
接下来,我们将从
pet-clinic-concierge服务开始,这是 API 网关。向pet-clinic-conciergePOM 添加以下依赖项:<!-- Distributed tracing --> <dependency> <groupId>io.micronaut</groupId> <artifactId>micronaut-tracing</artifactId> <version>${micronaut.version}</version> <scope>compile</scope> </dependency> <dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-instrumentation-http</artifactId> <version>5.12.3</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter</artifactId> <version>2.15.0</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.opentracing.brave</groupId> <artifactId>brave-opentracing</artifactId> <version>0.37.2</version> <scope>compile</scope> </dependency>通过导入前面的依赖项,我们可以利用 Micronaut 以及第三方代码工件进行分布式追踪。
-
要启用分布式追踪,我们还需要修改
application.properties文件。添加以下与 Zipkin 相关的属性:tracing: zipkin: http: url: http://host.docker.internal:9411 enabled: true sampler: probability: 1上述 Zipkin 应用程序属性添加在根级别。在
url中,我们指定了一个运行在本地的 Docker 实例的 Zipkin。此外,在sampler.probability中,我们指定值为1,这将启用对所有用户请求的追踪。此概率可以降低到 0 到 1 之间的任何值,其中 0 表示从不采样,1 表示对每个请求进行采样。 -
接下来,我们需要为控制器方法标记标签。对于管理跨度,Micronaut 中有以下两个标签:
a.
@NewSpan:这将从它标记的方法开始创建一个新的跨度。b.
@ContinueSpan:这将继续前一个跨度。
由于pet-clinic-concierge中的所有客户端控制器都是任何上游消费者的接口点,因此我们将在这类方法上使用@NewSpan,以便开始新的追踪。以下是在OwnerResourceClientController中的跨度相关更改:
@Controller("/api")
public class OwnerResourceClientController {
@Inject
OwnerResourceClient;
@NewSpan
@Post("/owners")
public HttpResponse<OwnerDTO>
createOwner(@SpanTag("owner.dto") OwnerDTO ownerDTO) {
return ownerResourceClient.createOwner(ownerDTO);
}
@NewSpan
@Put("/owners")
HttpResponse<OwnerDTO> updateOwner
(@SpanTag("owner.dto") @Body OwnerDTO ownerDTO) {
return ownerResourceClient.updateOwner(ownerDTO);
}
...
}
应该在所有其他客户端的pet-owner、pet-clinic和pet-clinic-reviews微服务中对注释客户端控制器方法进行类似更改。
接下来,我们需要修改pet-clinic微服务以支持分布式追踪。
修改pet-clinic微服务以支持分布式追踪
继续分布式追踪的更改,我们需要在pet-owner、pet-clinic和pet-clinic-reviews微服务项目 POM 和应用程序属性中进行必要的修改,如前所述。
此外,为了继续追踪,我们需要使用@ContinueSpan标签注释控制器方法。参考以下代码块:
@Post("/owners")
@ExecuteOn(TaskExecutors.IO)
@ContinueSpan
public HttpResponse<OwnerDTO> createOwner(@Body OwnerDTO ownerDTO) throws URISyntaxException {
...
}
@ContinueSpan必须注释在所有微服务中的所有控制器方法上(不包括作为 API 网关的pet-clinic-concierge)。@ContinueSpan将从前一个跨度/追踪中继续跨度/追踪。在pet-clinic-concierge中,我们使用@NewSpan注释createOwner()方法,在pet-owner微服务中,我们使用@ContinueSpan。使用这些标签可以一起追踪端到端的执行流程。
在下一节中,我们将验证pet-clinic应用程序中 HTTP 请求的端到端追踪。
验证pet-clinic应用程序中的分布式追踪
要验证 pet-clinic 应用程序中的分布式跟踪,您必须确保 pet-clinic 微服务正在运行。我们将通过 API 网关获取所有者列表。为此,请执行以下步骤:
-
在任何浏览器标签或 REST 客户端中转到
http://localhost:32584/api/owners。 -
导航到 Zipkin,以验证在
http://localhost:9411/zipkin的先前的 HTTPGET调用的跟踪。 -
点击 运行查询 按钮。
-
在返回的结果中转到
get /api/owners请求并点击 显示。
在成功执行这些步骤后,您将看到以下屏幕:

图 9.6 – Zipkin 中的 GET owners 分布式跟踪
Zipkin 提供了一个直观的用户界面来访问请求执行跟踪。您可以看到请求首先到达 pet-clinic-concierge,然后进一步传递给 pet-owner 微服务。总共花费了大约 948 毫秒来完成请求,其中大部分时间花在 pet-owner 微服务上。
在下一节中,我们将重点关注分布式监控以及如何在 Micronaut 框架中实现分布式监控。
Micronaut 微服务的分布式监控
监控简单来说就是记录关键性能指标,以增强对应用程序状态的可见性。通过记录和展示所有分布式组件的系统性能指标,如 CPU 使用率、线程池、内存使用率和数据库连接,它可以提供一个全面的画面,说明在特定时间点微服务系统是如何运行的。微服务的分布式特性要求在监控系统方面进行转变。我们不再依赖于主机环境监控工具,我们需要一个统一的监控解决方案,能够结合来自各种服务的性能指标,并呈现一个一站式界面。在本节中,我们将探讨如何为 pet-clinic 应用程序实现这样的分布式监控解决方案。
要实现分布式监控,我们将使用非常流行的 Prometheus 和 Grafana 堆栈。让我们看看我们用于分布式监控的系统组件:

图 9.7 – 使用 Prometheus 和 Grafana 进行分布式监控
如前图所示,pet-clinic 微服务将向 Prometheus 服务器发送指标,而 Grafana 将获取指标以展示用户界面。Prometheus 配置将存储在 YAML 文件中。
在下一节中,我们将从在 Docker 中设置 Prometheus 和 Grafana 开始。
在 Docker 中设置 Prometheus 和 Grafana
在我们实例化 Docker 中的 Prometheus 和 Grafana 之前,我们需要为 Prometheus 定义配置,以便它可以从pet-clinic微服务中拉取所需的指标。您可以查看docker-prometheus docker-compose和prometheus.yml,链接为github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter09/micronaut-petclinic/docker-prometheus。
一旦在本地检出,您可以查看prometheus.yml文件,如下所示:
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'pet-clinic-monitor'
scrape_configs:
- job_name: 'prometheus'
scrape_interval: 10s
static_configs:
- targets: ['host.docker.internal:9090','node-
exporter:9110']
- job_name: 'micronaut'
metrics_path: '/metrics'
scrape_interval: 10s
static_configs:
- targets: ['host.docker.internal:32581', 'host.docker.internal:32582', 'host.docker.internal:32583', 'host.docker.internal:32584']
在prometheus.yml中,我们主要需要配置scrape_configs。这将负责调用微服务端点以获取指标。我们可以在目标中指定pet-clinic微服务。此外,请注意抓取间隔为10秒。这将配置 Prometheus 每 10 秒获取一次指标。
接下来,让我们在 Docker 中设置我们的分布式监控堆栈。
要在 Docker 中设置 Prometheus 和 Grafana,请按照以下说明操作:
-
从
github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter09/micronaut-petclinic/docker-prometheus检出docker-prometheus。 -
打开任何 Bash 终端(我们使用了 Git Bash)。
-
将目录切换到您检出
docker-prometheus的位置。 -
运行
docker compose up –d。 -
等待 Docker 下载镜像并实例化 Prometheus 应用程序容器。
这些说明将在 Docker 中启动监控应用程序。您可以通过访问 Docker 仪表板并进入容器/应用程序来验证安装。
在下一节中,我们将探讨如何将pet-clinic微服务集成到 Prometheus 中。
配置微服务进行分布式监控
为了配置pet-clinic微服务进行分布式监控,我们需要使用Micrometer依赖项更新project POM。
将以下依赖项添加到pet-owner项目的 POM 中:
<!-- Micrometer -->
<dependency>
<groupId>io.micronaut.micrometer</groupId>
<artifactId>micronaut-micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut.micrometer</groupId>
<artifactId>micronaut-micrometer-registry-
prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-management</artifactId>
</dependency>
通过导入micronaut-micrometer依赖项,我们可以在pet-owner微服务中利用分布式监控工具包。
为了向 Prometheus 公开服务指标,我们需要在所有pet-clinic微服务中公开metrics端点。我们将在com.packtpub.micronaut.web.rest.commons包中添加一个新的控制器,称为PrometheusController,如下所示:
@RequiresMetrics
@Controller("/metrics")
public class PrometheusController {
private final PrometheusMeterRegistry;
@Inject
public PrometheusController(PrometheusMeterRegistry
prometheusMeterRegistry) {
this.prometheusMeterRegistry =
prometheusMeterRegistry;
}
@Get
@Produces("text/plain")
public String metrics() {
return prometheusMeterRegistry.scrape();
}
}
PrometheusController将在/metrics端点上公开prometheusMeterRegistry.scrape()。
prometheusMeterRegistry.scrape()将提供服务性能指标,这些指标已在application.properties文件中配置。
我们需要按照以下方式配置application.properties文件:
micronaut:
...
metrics:
enabled: true
export:
prometheus:
enabled: true
step: PT1M
descriptions: true
endpoints:
metrics:
enabled: false
prometheus:
enabled: false
在application.properties中,我们正在启用指标并将指标以 Prometheus 格式导出。此外,由于我们提供了自定义的/metrics端点,我们在应用程序属性中禁用了metrics和prometheus端点。
同样,我们需要修改项目 POM,添加PrometheusController,并更新pet-clinic、pet-clinic-reviews和pet-clinic-concierge微服务的应用程序属性。之后,我们需要在终端中运行mvn compile jib:dockerBuild命令来重新构建所有运行的服务项目的 Docker 镜像。一旦 Docker 镜像构建并上传到本地 Docker 仓库,我们需要退役旧的pet-clinic应用程序在 Docker 中,并重新运行docker compose up –d来重新实例化修改后的pet-clinic应用程序。
在下一节中,我们将验证pet-clinic应用程序中的分布式监控实现。
验证 pet-clinic 应用程序的分布式监控
要验证pet-clinic应用程序的分布式监控,你必须让pet-clinic和 Prometheus 应用程序在 Docker 中运行。你需要遵循以下说明来验证 Prometheus 和pet-clinic应用程序之间的集成:
-
访问所有微服务的
/metrics端点以验证服务是否向 Prometheus 暴露指标。 -
通过访问
http://localhost:32581/metrics来验证pet-owner指标。 -
通过访问
http://localhost:32582/metrics来验证pet-clinic指标。 -
通过访问
http://localhost:32583/metrics来验证pet-clinic-reviews指标。 -
通过访问
http://localhost:32584/metrics来验证pet-clinic-concierge指标。 -
导航到
http://localhost:9090/graph并检查是否可以看到system_cpu_usage指标。在完成前面的步骤后,你将看到以下屏幕:
![Figure 9.8 – 在 Prometheus 中访问 pet-clinic 应用程序的系统 CPU 使用率图表]
![img/Figure_9.8_B16585_Fixed.jpg]
图 9.8 – 在 Prometheus 中访问 pet-clinic 应用程序的系统 CPU 使用率图表
在前面的屏幕截图中,我们可以验证pet-clinic微服务能够在其端点上暴露性能指标,Prometheus 可以调用/metrics端点。我们可以在 Prometheus 图表中看到系统 CPU 使用率图表,但作为系统管理员或开发者,你可能需要一个包含所有指标图表的系统仪表板。
在以下说明中,我们将集成 Grafana 与 Prometheus 服务器:
-
导航到
http://localhost:3000/并使用用户名admin和密码pass登录。 -
登录后,导航到
http://localhost:3000/datasources。 -
点击添加数据源按钮。
-
在时间序列数据库列表下,选择Prometheus。
-
在
http://prometheus:9090。保持其余值保持默认设置。
-
点击 保存并测试 按钮。你应该会收到一条成功消息。
-
转到相邻的 仪表板 选项卡,并点击 Prometheus 2.0 统计。
在成功完成这些步骤后,你应该会看到以下仪表板:

图 9.9 – Grafana 中的 Prometheus 仪表板
如前一个屏幕截图所示,Grafana 为访问 pet-clinic 应用程序中所有服务组件的关键系统指标提供了一个非常直观、统一的仪表板。在解决生产环境中任何性能问题和系统故障时,一次性访问这些遥测数据非常方便。
在本节中,我们探讨了分布式监控是什么以及我们如何在 Micronaut 框架中使用 Prometheus 和 Grafana 实现分布式监控。
摘要
在本章中,我们首先介绍了分布式日志以及为什么它在任何微服务实现中都很重要。我们在 pet-clinic 应用程序中实现了 ELK Stack 用于分布式日志记录。此外,我们还深入探讨了如何使用 Kibana 用户界面连接到 Elasticsearch 应用程序日志索引。
之后,我们探讨了分布式追踪是什么以及如何在 Micronaut 框架中使用 Zipkin 实现分布式追踪。我们还验证了在 Zipkin 用户界面中的 HTTP 调用追踪。
最后,我们深入分布式监控的世界,并使用 Prometheus 和 Grafana 栈为 pet-clinic 应用程序实现了一个分布式监控解决方案。
本章通过使你具备如何在 Micronaut 框架中实现这些模式的手动知识,增强了你的 Micronaut 微服务之旅,这些模式包括分布式日志记录、分布式追踪和分布式监控。
在下一章中,我们将探讨如何在 Micronaut 框架中实现 IoT 解决方案。
问题
-
微服务中的分布式日志是什么?
-
你如何在 Docker 中运行 ELK 栈?
-
你如何在 Micronaut 框架中实现分布式日志记录?
-
你如何从 Micronaut 微服务连接到 Docker Logstash?
-
微服务中的分布式追踪是什么?
-
你如何在 Micronaut 框架中实现分布式追踪?
-
微服务中的分布式监控是什么?
-
你如何在 Docker 中运行 Prometheus 和 Grafana 栈?
-
你如何在 Micronaut 框架中实现分布式监控?
第六部分:使用 Micronaut 和 Closure 的物联网
本节将通过一个关于物联网(IoT)的强化章节来总结在 Micronaut 框架中的微服务之旅,之后你将学习如何构建企业级微服务。
本节包含以下章节:
-
第十章,使用 Micronaut 的物联网
-
第十一章,构建企业级微服务
第十章:使用 Micronaut 的物联网
物联网(IoT)是发展最快的科技之一。它是一个由设备或事物组成的网络。这些设备具有与传感器或软件相同的性能,并且可以通过互联网与其他设备通信。设备或事物可以来自各个领域,包括灯泡、门锁、心跳监测器、位置传感器以及许多可以启用传感器的设备。这是一个具有互联网功能的智能设备或事物生态系统。物联网在各个领域都有趋势。以下是一些热门领域的几个例子:
-
家庭自动化
-
制造和工业应用
-
医疗和医学科学
-
军事和防御
-
汽车、交通和物流
除了这些领域,在本章中,我们还将学习以下主题:
-
物联网基础
-
与 Micronaut Alexa 技能一起工作
到本章结束时,您将熟练掌握有关 Micronaut 集成的物联网的先前方面。
技术要求
本章中所有的命令和技术说明都可以在 Windows 10 和 macOS 上运行。本章中的代码示例可以在本书的 GitHub 仓库中找到,地址为github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter10/.
以下工具需要在开发环境中安装和设置:
-
Java SDK:版本 13 或更高(我们使用了 Java 14)。
-
Maven:这是可选的,仅当您希望使用 Maven 作为构建系统时才需要。然而,我们建议在任何开发机器上设置 Maven。有关下载和安装 Maven 的说明可以在
maven.apache.org/download.cgi找到。 -
开发 IDE:根据您的偏好,可以使用任何基于 Java 的 IDE,但为了编写本章,我们使用了 IntelliJ。
-
Git:有关下载和安装 Git 的说明可以在
git-scm.com/downloads找到。 -
PostgreSQL:有关下载和安装 PostgreSQL 的说明可以在
www.postgresql.org/download/找到。 -
MongoDB:MongoDB Atlas 提供高达 512 MB 存储空间的免费在线数据库即服务(DBaaS)。然而,如果您希望使用本地数据库,则可以在
docs.mongodb.com/manual/administration/install-community/找到下载和安装 MongoDB 的说明。我们使用本地安装来编写本章。 -
REST 客户端:可以使用任何 HTTP REST 客户端。在本章中,我们使用了 Advanced REST Client Chrome 插件。
-
Docker: 有关下载和安装 Docker 的说明可在
docs.docker.com/get-docker/找到。 -
亚马逊: 您需要一个亚马逊账户来使用 Alexa,您可以在
developer.amazon.com/alexa上设置账户。
物联网基础
物联网是一个设备或事物的网络。这些事物可以是任何东西——它可以是佩戴健康监测器的人类,佩戴地理定位传感器的宠物,带有轮胎压力传感器的汽车,具有语音/视觉功能的电视,或智能扬声器。物联网还可以在云中使用高级机器学习(ML)和人工智能(AI)功能来提供更高级的服务。物联网可以通过数据收集和自动化使事物变得智能。以下图表说明了物联网:
![Figure 10.1 – 物联网 (IoT)
![Figure 10.1 – 物联网 (IoT)
图 10.1 – 物联网 (IoT)
这些设备或事物具有互联网功能并且相互连接,因此它们充当一个生态系统。这个生态系统可以收集、发送并根据从其他事物获取的数据采取行动。例如,当您到达家时,您可以打开家中的灯光。
物联网为个人、企业和组织提供了显著的好处。物联网可以通过在两个系统或设备之间无缝传输数据来减少手动工作和干预。物联网设备在消费市场中的重要性日益增加,无论是作为锁、门铃、灯泡、扬声器、电视、医疗产品还是健身系统。物联网现在主要通过语音启用的生态系统访问,例如 Google Home、Apple Siri、Amazon Alexa、Microsoft Cortana、Samsung Bixby 等。物联网有许多积极方面;然而,在安全和隐私问题上也有一些缺点。
现在我们已经了解了物联网及其应用的基础知识,让我们了解 Alexa 技能的基础。
在 Alexa 技能的基础知识上工作
Alexa 是一种基于云的语音识别服务,可在亚马逊和第三方设备制造商的数百万台设备上使用,例如电视、蓝牙扬声器、耳机、汽车等。您可以使用 Alexa 构建基于交互式语音的请求-响应应用程序。
Alexa 可以集成到各种应用程序中。Alexa 还具有屏幕功能,可以直观地显示响应,Echo Show 是一款带有显示屏的 Alexa 扬声器。以下图表说明了亚马逊 Alexa 架构:
![Figure 10.2 – 亚马逊 Alexa 架构
![Figure 10.2 – 亚马逊 Alexa 架构
图 10.2 – 亚马逊 Alexa 架构
用户可以说出设备的唤醒词Alexa,并执行操作。例如,要查找您当前位置的天气,您可以说Alexa,当前天气是什么?,您将收到如下响应:您当前位置的天气是 28 度。Alexa 技能就像应用一样,您可以使用特定设备的 Alexa 应用来启用或禁用技能。技能是基于语音的 Alexa 功能。
Alexa 可以做以下事情:
-
设置闹钟。
-
播放来自 Spotify、Apple Music 或 Google Music 的音乐。
-
创建待办事项列表并将项目添加到购物清单中。
-
查看天气。
-
检查您的日历。
-
阅读新闻简报。
-
查看银行账户。
-
在餐厅点餐。
-
在互联网上查事实。
这些只是 Alexa 能做的许多事情中的一部分。现在,让我们继续了解更多的 Alexa。
Alexa 技能基础
拥有任何基于语音的助手或工具的用户可以使用唤醒词来打开技能或应用。例如,使用 Google Home,我们使用Hey Google或OK Google,对于 Apple Siri,我们使用Hey Siri或Siri,而对于 Amazon Alexa,我们使用Alexa。这个唤醒词可以被替换为Amazon,Echo或computer。所有 Alexa 技能都是基于语音交互模型设计的;也就是说,您可以说的短语来让技能做您想要的事情,例如Alexa,打开灯光或Alexa,当前温度是多少?
Alexa 支持以下两种类型的语音交互模型:
-
预构建的语音交互模型:Alexa 为您定义了每个技能的短语。
-
自定义语音交互模型:您定义用户可以说的短语来与您的技能交互。
对于我们的工作示例代码,我们将使用自定义语音交互模型。以下图表说明了使用自定义语音交互模型打开技能的过程:
![Figure 10.3 – 打开技能
![img/Figure_10.3_B16585.jpg]
![img/Figure_10.3_B16585.jpg]
现在我们已经了解了唤醒词,紧随其后的短语是启动词,然后是调用名称。对于我们的示例应用宠物诊所,启动词将是打开,后面跟着调用宠物诊所。
以下图表说明了话语和意图之间的关系:
![Figure 10.4 – 打开技能
![img/Figure_10.4_B16585.jpg]
图 10.4 – 打开技能
话语是用户对 Alexa 说的话,用以传达他们想要做什么,例如打开灯光,当前温度是多少?等等。用户可以用不同的方式说出同样的话,例如查找温度,当前温度,室外温度,[位置]的温度,而 Alexa 将提供预构建的话语和相关请求作为自定义语音交互模型的一部分。这个话语列表可以映射到一个请求或意图。
以下图表展示了带有唤醒词、启动、调用名称、语音和意图的自定义语音交互模型:

图 10.5 – 打开技能 – 宠物诊所
对于我们的代码示例,我们将使用序列Alexa,打开宠物诊所和Alexa,查找附近的宠物诊所。在这里,唤醒词是Alexa,启动词是打开,调用名称是宠物诊所。语音可以是找到最近的宠物诊所。我们还可以有其他语音变体,例如找到宠物诊所。所有这些语音都可以映射到GetFactByPetClinicIntent。我们将在本章的下一节学习关于意图的内容。
意图的概述
Alexa 语音设计的基本原理之一是意图。意图捕捉了最终用户想要通过语音执行的事件。意图代表了由用户的语音请求触发的动作。Alexa 中的意图在称为意图模式的 JSON 结构中指定。内置的意图包括取消、帮助、停止、导航回家和回退。有些意图是基本的,例如帮助,技能应该有一个帮助意图。
以下图表展示了 Alexa 开发者控制台中的内置意图:

图 10.6 – 内置意图
如果我们有一个登录网站,该网站有用户名和密码字段以及一个提交按钮,在 Alexa 技能世界中将会有一个提交意图。然而,一个很大的不同之处在于用户可以用不同的方式说出“提交”;例如,提交、提交它、确认、好、获取、继续等等。这些表达相同意思的不同方式被称为语音。每个意图都应该包括一个语音列表;也就是说,用户可能说出以调用这些意图的所有内容。意图可以有称为槽位的参数,这些内容在本章中不会讨论。
现在我们已经通过涵盖语音、意图和内置意图学习了 Alexa 技能的基础,让我们创建我们的第一个功能性的 Alexa 技能。
您的第一个 HelloWorld Alexa 技能
要开始创建我们的 Alexa 技能,我们必须导航到developer.amazon.com/,选择Amazon Alexa,然后点击创建 Alexa 技能。这将打开 Alexa 开发者控制台。如果您没有 Amazon 开发者账户,您可以免费创建一个。以下屏幕截图展示了创建技能屏幕:

图 10.7 – 创建技能屏幕
在前面的屏幕截图中,你可以看到如何创建一个名为宠物诊所的新技能名称,选择一个要添加到你的技能选项中的模型,并选择一个托管你的技能后端资源的方法,称为自行提供。选择一个要添加到你的技能中的模板,称为从头开始创建。
通过使用自定义语音交互模型,我们了解到我们需要创建和配置我们的唤醒词、启动词、调用名称、语句和意图。唤醒词为设备配置,对所有技能都是相同的,因此我们不需要更改它。在我们的配置中,我们将配置代码启动、调用、语句和意图。以下图表说明了开发 Alexa 技能的基本原理:


图 10.8 – Alexa 技能
Alexa 技能基于 语音交互模型 和 编程逻辑。编程逻辑可以使用 Node.js、Java、Python、C# 或 Go 创建。这种编程逻辑允许我们连接到 Web 服务、微服务、API 和接口。有了这个,您可以调用 Alexa 技能的互联网可访问端点。以下图表说明了技能开发者控制台:


图 10.9 – Alexa 技能
我们可以将 pet clinic 设置并保存。您还可以选择 HelloWorldIntent 意图并将其重命名为 PetClinicWelcomeIntent。在意图中将有示例语句列出,您可以手动修改或使用 JSON 编辑器并从本书的 GitHub 仓库复制 alexa_petclinic_intent_schema.json 代码。以下代码说明了意图的 JSON 架构:
{
"interactionModel": {
"languageModel": {
"invocationName": "pet clinic",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": [
"cancel"
]
………………………
{
"name": "PetClinicWelcomeIntent",
"slots": [],
"samples": [
"find near by pet clinics",
"find pet clinics"
]
}
],
"types": []
}
}
}
您可以使用 JSON 配置文件配置意图的调用名称和示例语句。
一旦您已将 JSON 文件复制到 Alexa 开发者控制台的 JSON 编辑器中,请点击 保存模型 然后点击 构建模型和评估模型。
注意
上述配置是 GitHub 上本章文件夹中的示例。实际架构可以从 GitHub 复制。
一旦构建了模型,请在 Alexa 开发者控制台中点击 测试 并启用技能测试过程。现在,我们需要开发我们的后端代码以进行响应。
使用您最喜欢的 IDE 创建一个 Maven Java 项目。以下依赖项对于此项目是必需的:
<dependencies>
<dependency>
<groupId>com.amazon.alexa</groupId>
<artifactId>ask-sdk</artifactId>
<version>2.20.2</version>
</dependency>
</dependencies>
我们将使用亚马逊的 ask-sdk 进行我们的后端 Java 开发。您也可以使用 Gradle 配置依赖项。以下代码展示了示例 Gradle 配置:
dependencies {
compile 'com.amazon.alexa:alexa-skills-kit:1.1.2'
}
我们需要为所有意图创建一个 Java 类。在我们的 JSON 架构中,我们已定义了 CancelIntent、HelpIntent、StopIntent、NavigateHomeIntent、FallbackIntent 和 PetClinicWelcomeIntent 意图。对于每个意图,我们需要创建一个处理器;例如,PetClinicWelcomeIntent 应该有 PetClicWelcomeIntentHandler。处理器名称将被添加到每个意图名称的末尾。我们还必须创建一个额外的处理器,该处理器在 JSON 架构中尚未配置,称为 LaunchRequestHandler。这是每次启动技能时都会触发的第一个意图。以下代码说明了 LaunchRequestHandler:
public class LaunchRequestHandler implements RequestHandler {
@Override
public boolean canHandle(HandlerInput handlerInput) {
return handlerInput.matches
(requestType(LaunchRequest.class));
}
@Override
public Optional<Response> handle(HandlerInput
handlerInput) {
String speechText = "Welcome to Pet Clinic, You can
say find near by Pet Clinics";
return handlerInput.getResponseBuilder()
.withSpeech(speechText)
.withSimpleCard("PetClinic", speechText)
.withReprompt(speechText)
.build();
}
}
LaunchRequestHandler 将覆盖当技能启动时的处理方法以及响应语音消息。这已在代码块中定义。在代码中,我们有一个语音文本响应“欢迎来到宠物诊所,您可以说出查找附近的宠物诊所”,以及 PetClinic 的标题。
现在我们已经创建了处理程序(CancelandStopIntentHandler、HelpIntentHandler、LaunchRequestHandler、PetClinicWelcomeIntentHandler 和 SessionEndedRequestHandler),我们需要创建 StreamHandler。StreamHandler 是 AWS Lambda 函数的入口点。所有由最终用户发送到 Alexa、调用您的技能的请求都将通过此类传递。您需要在端点中配置从 Amazon Alexa 开发者控制台复制的技能 ID。请参考以下代码:
public class PetClinicStreamHandler extends SkillStreamHandler {
private static Skill getSkill() {
return Skills.standard()
.addRequestHandlers(
new CancelandStopIntentHandler(),
new
PetClinicWelcomeIntentHandler(),
new HelpIntentHandler(),
new LaunchRequestHandler(),
new SessionEndedRequestHandler())
.withSkillId("amzn1.ask.skill.de392a0b-
0a95-451a-a615-dcba1f9a42c6")
.build();
}
public PetClinicStreamHandler() {
super(getSkill());
}
}
通过这样,我们已经了解了如何使用流处理器以及如何调用意图处理器。让我们学习技能 ID 的使用,这是您可以获取有关技能 ID 信息的地方。以下截图说明了技能 ID 在开发者控制台中的位置:

Figure 10.10 – 端点技能 ID 和默认区域
技能 ID 可在 Alexa 开发者控制台的 .jar 文件中找到,该文件包含代码的依赖项。您可以通过执行 mvn assembly:assembly -DdescriptorId=jar-with-dependencies package 命令来创建 .jar 文件。此 .jar 文件将位于目标目录中,如图所示:

Figure 10.11 – Maven JAR 文件位置
下一步是创建 Amazon Lambda 函数,这是我们后端服务代码。
导航到 console.aws.amazon.com/lambda/ 创建一个函数。将函数命名为 lambda_for_petclinic,设置选项为 从头开始创建作者,并将运行时设置为 Java 11。用户界面如图所示:

Figure 10.12 – Alexa 创建函数屏幕
下一步是创建触发器,将 触发器配置 设置为 Alexa Skills Kit,如下面的截图所示。您需要从 Alexa 开发者控制台的 端点 屏幕中复制 Alexa 技能 ID。同时,您还需要从 Lambda 开发者控制台复制 函数 ARN(Amazon Resource Name)属性到 Alexa 技能开发者控制台的 端点 屏幕中。以下截图说明了 AWS Lambda 函数的 ARN 位置:

Figure 10.13 – AWS 函数 ARN
函数 ARN值必须从 Alexa 开发者控制台的端点屏幕复制到默认区域部分或到特定位置的区域,如图图 10.10所示。图图 10.10中显示的技能 ID 应复制到 AWS Lambda 触发器屏幕,如图所示:

图 10.14 – AWS Lambda – 添加触发器
一旦添加了必要的技能 ID 和.jar文件,以及任何依赖项(petclinic-Alexa-maven-1.0-SNAPSHOT-jar-with-dependencies.jar),到 Lambda 函数中。
以下截图说明了将.jar文件上传到 Amazon Lambda 函数的过程:

图 10.15 – 将代码上传到 Amazon Lambda
现在我们已经创建了第一个 Alexa 技能并上传了必要的代码,让我们来测试一下。
测试您的代码
最后和最终的过程是使用 Amazon Alexa 模拟器测试代码,该模拟器位于开发者控制台中。以下截图说明了如何请求 Alexa 模拟器:

图 10.16 – Alexa 模拟器请求/响应
请求/响应测试界面接受文本或语音。您可以在其中键入或说出“打开宠物诊所”和“查找附近的宠物诊所”。您应该能够在JSON 输出 1部分看到 Java 代码的响应。一旦您看到了响应,这意味着我们已经成功创建了第一个带有请求和响应的基本物联网宠物诊所示例。
我们将在下一节中将 Micronaut 和 Alexa 集成。您可以在本书的 GitHub 存储库中找到petclinic-alexa-maven项目的完整工作示例。
将 Micronaut 与 Alexa 集成
正如我们在前一节中讨论的,在本节中,我们将开始了解如何将 Micronaut 与 Alexa 集成。Micronaut 提供了各种扩展,支持micronaut-function-aws-alexa模块,包括支持使用 Micronaut 构建 Alexa 技能。Micronaut Alexa 支持可以通过AlexaFunction连接您的 Alexa 应用程序,并支持以下类型的依赖注入:
-
com.amazon.ask.dispatcher.request.handler.RequestHandler -
com.amazon.ask.dispatcher.request.interceptor.RequestInterceptor -
com.amazon.ask.dispatcher.exception.ExceptionHandler -
com.amazon.ask.builder.SkillBuilder
Micronaut 的aws-alexa模块简化了我们使用 Java、Kotlin 或 Groovy 开发 Alexa 技能的方式。以下代码是aws-alexa模块的 Java Maven 依赖项:
<dependency>
<groupId>io.micronaut.aws</groupId>
<artifactId>micronaut-aws-alexa</artifactId>
</dependency>
正如我们在前面的章节中学到的,Micronaut 使用 Java 注解。为了改变任何 Alexa Java 处理器,使其能够与 Micronaut 一起工作,我们只需要添加必要的@Singleton注解;即javax.inject.Singleton。以下是一个带有Singleton注解的示例LaunchRequestHandler:
@Singleton
public class LaunchRequestHandler implements RequestHandler {
@Override
public boolean canHandle(HandlerInput handlerInput) {
return handlerInput.matches
(requestType(LaunchRequest.class));
}
@Override
public Optional<Response> handle(HandlerInput
handlerInput) {
String speechText = "Welcome to Pet Clinic, You can
say find near by Pet Clinics";
return handlerInput.getResponseBuilder()
.withSpeech(speechText)
.withSimpleCard("PetClinic", speechText)
.withReprompt(speechText)
.build();
}
}
在 Micronaut 的帮助下,你可以轻松地对你的意图进行单元测试。这是因为 @MicronautTest 注解提供了无缝的单元测试功能。在这里,我们可以将处理程序注入到单元测试用例中。Micronaut 框架利用 Amazon 的 LaunchRequest 类来完成以下操作:
@MicronautTest
public class LaunchRequestIntentHandlerTest {
@Inject
LaunchRequestHandler handler;
@Test
void testLaunchRequestIntentHandler() {
LaunchRequest request =
LaunchRequest.builder().build();
HandlerInput input = HandlerInput.builder()
.withRequestEnvelope
(RequestEnvelope.builder()
.withRequest(request)
.build()
).build();
assertTrue(handler.canHandle(input));
Optional<Response> responseOptional =
handler.handle(input);
assertTrue(responseOptional.isPresent());
Response = responseOptional.get();
assertTrue(response.getOutputSpeech() instanceof
SsmlOutputSpeech);
String speechText = "Welcome to Pet Clinic, You can
say find near by Pet Clinics";
String expectedSsml = "<speak>" + speechText +
"</speak>";
assertEquals(expectedSsml, ((SsmlOutputSpeech)
response.getOutputSpeech()).getSsml());
assertNotNull(response.getReprompt());
assertNotNull(response.getReprompt()
.getOutputSpeech());
assertTrue(response.getReprompt().getOutputSpeech()
instanceof SsmlOutputSpeech);
assertEquals(expectedSsml,((SsmlOutputSpeech)
response.getReprompt().getOutputSpeech())
.getSsml());
assertTrue(response.getCard() instanceof
SimpleCard);
assertEquals("PetClinic", ((SimpleCard)
response.getCard()).getTitle());
assertEquals(speechText, ((SimpleCard)
response.getCard()).getContent());
assertFalse(response.getShouldEndSession());
}
你可以在本书的 GitHub 仓库中找到 petclinic-alexa-micronaut-maven 项目的完整工作示例。你可以在处理程序中连接到网络服务或后端数据库,发送请求并接收响应。以下图表展示了 Alexa 技能与后端集成的设计:

图 10.17 – 带自定义后端的 Alexa 技能
如以下代码片段所示,你的 speechText:
@Override
public Optional<Response> handle(HandlerInput
handlerInput) {
speechText = service.getPetClinicLocation();
return handlerInput.getResponseBuilder()
.withSpeech(speechText)
.withSimpleCard("PetClinic", speechText)
.withReprompt(speechText)
.build();
}
可以从微服务调用中添加 speechText 处理方法,并可以从数据库或服务中检索信息。
现在我们已经将 Micronaut 与 Alexa 集成,我们可以使用语音和 Micronaut 微服务来控制物联网设备。
摘要
在本章中,我们探讨了如何使用物联网和 Amazon Alexa 的基础知识。然后,我们深入研究了创建 Micronaut 微服务并将其与 Amazon Alexa 集成的过程。通过这种集成,我们可以使用语音和 Micronaut 微服务来控制物联网设备。
本章增强了你在物联网中的 Micronaut 微服务之旅。它使你获得了物联网和 Amazon Alexa 的第一手知识。Micronaut 还支持 语音合成标记语言 (SSML) 和闪报。
在下一章中,我们将把我们所学的所有主题结合起来,通过构建企业级微服务、查看 OpenAPI、扩展 Micronaut 以及深入构建企业级微服务,将事物提升到新的水平。
问题
-
物联网是什么?
-
列举一些物联网设备。
-
什么是 Alexa 技能?
-
什么是 Alexa 意图?
-
Alexa 支持哪些编程语言?
-
默认启动处理程序类的名称是什么?
-
你需要对你的注解处理程序进行哪一项更改,以便它们与 Micronaut 兼容?
第十一章:构建企业级微服务
我们已经到达了使用 Micronaut 学习动手微服务的最后阶段。我们的旅程现在已进入最终阶段;在前几章中,我们在 Micronaut 的动手实践方面获得了大量知识。现在,我们必须将所有这些知识串联起来,使用 Micronaut 构建我们企业级的微服务。正如我们从前面的章节中已经了解到的,以下是一些使用 Micronaut 的好处:
-
基于 JVM 的现代全栈框架
-
易于测试的微服务
-
为无服务器应用程序构建
-
反应式堆栈
-
最小内存占用和启动时间
-
云原生框架
-
多语言支持(Java、Groovy、Kotlin)
在本章中,我们将涵盖以下主题:
-
将所有内容整合在一起
-
架构企业级微服务
-
理解 Micronaut 的 OpenAPI
-
实施 Micronaut 的微服务
到本章结束时,您将熟练掌握构建、设计和扩展企业级微服务。
技术要求
本章中的所有命令和技术说明都可以在 Windows 10 和 Mac OS X 上运行。本章中的代码示例可以在本书的 GitHub 仓库中找到,地址为 github.com/PacktPublishing/Building-Microservices-with-Micronaut/tree/master/Chapter11/micronaut-petclinic。
以下工具需要在您的开发环境中安装和设置:
-
Java SDK:版本 8 或更高(我们使用了 Java 13)。
-
Maven:这是可选的,仅当您希望使用 Maven 作为构建系统时才需要。然而,我们建议在所有开发机器上设置 Maven。下载和安装 Maven 的说明可以在
maven.apache.org/download.cgi找到。 -
开发 IDE:根据您的偏好,可以使用任何基于 Java 的 IDE,但在这章中,我们使用了 IntelliJ。
-
Git:下载和安装 Git 的说明可以在
git-scm.com/downloads找到。 -
PostgreSQL:下载和安装 PostgreSQL 的说明可以在
www.postgresql.org/download/找到。 -
MongoDB:MongoDB Atlas 提供免费的在线数据库即服务,最多 512 MB 的存储空间。但是,如果您希望使用本地数据库,则可以在
docs.mongodb.com/manual/administration/install-community/找到下载和安装 MongoDB 的说明。我们在编写本章时使用了本地安装。 -
REST 客户端:可以使用任何 HTTP REST 客户端。在本章中,我们使用了 Advanced REST Client Chrome 插件。
-
Docker:下载和安装 Docker 的说明可以在
docs.docker.com/get-docker/找到。 -
Amazon:为 Alexa 创建一个 Amazon 账户:
developer.amazon.com/alexa。
将所有内容整合在一起
让我们回顾一下到目前为止所有章节中学到的内容。在第一章《使用 Micronaut 框架开始微服务之旅》中,我们开始使用 Micronaut 框架来探讨微服务。在那里,我们学习了微服务及其演变:微服务设计模式。我们学习了为什么 Micronaut 是开发微服务的最佳选择,并创建了我们的第一个 Micronaut 应用程序。在第二章《数据访问工作》中,我们学习了如何进行数据访问。
我们开始了我们的第一个宠物诊所、宠物主人以及宠物诊所评论的 Micronaut 项目。我们学习了如何使用 Micronaut 框架集成持久化层,以及如何使用对象关系映射 Hibernate 框架与关系型数据库集成。我们在 PostgreSQL 中创建了我们的 Micronaut 后端数据库,定义了实体之间的关系,映射了实体之间的关系,并创建了数据访问存储库。我们还通过在数据库中使用 Micronaut 进行插入/创建、读取/检索、更新和删除操作来创建了基本的 CRUD 操作。之后,我们使用 MyBatis 框架集成了关系型数据库。我们还探索了 MongoDB 和 Micronaut 的 NoSQL 数据库功能。
在第三章《使用 Micronaut 进行 RESTful Web 服务开发》中,我们学习了如何使用 Micronaut 进行 RESTful Web 服务的开发。我们为宠物诊所、宠物主人以及宠物诊所评论的 Micronaut 项目添加了 RESTful Web 服务功能。我们学习了数据传输对象、端点有效负载、映射结构、RESTful 端点、HTTP 服务器 API、验证数据、处理错误、API 版本控制和 HTTP 客户端 API。我们使用 GET、POST、PUT 和 DELETE 在服务上执行了 RESTful 操作。在第四章《保护微服务》中,我们学习了如何保护微服务。通过启用安全方面,我们创建了一个具有安全功能的 RESTful 微服务的工作示例。我们通过使用会话认证来保护服务端点、实现基本认证提供者、配置授权、授予未经授权和安全的访问、使用 JWT 认证、在 Docker 中设置 Keycloak、使用 OAuth、设置 Okta 身份提供者以及启用 Micronaut 框架中的 SSL,学习了 Micronaut 安全的基础知识。
在 第五章《使用事件驱动架构集成微服务》中,我们学习了如何使用事件驱动架构集成微服务。我们学习了事件驱动架构的基础知识、Apache Kafka 的事件流以及如何在宠物诊所评论微服务中实现事件生产者和事件消费者客户端。在第 第六章《测试微服务》中,我们掌握了使用 Micronaut 测试微服务。我们学习了在 Micronaut 框架中使用 JUnit 5 进行单元测试的基础知识、模拟测试、服务测试、可用的测试套件以及使用测试 Docker 容器进行的集成测试。
在第七章《处理微服务问题》中,我们学习了如何处理微服务问题。我们学习了外部化应用程序配置、分布式配置管理、使用 Swagger 记录服务 API、实现服务发现、使用 Consul 创建服务发现、实现 API 网关服务以及使用断路器和回退实现容错机制。在第 第八章《部署微服务》中,我们学习了如何部署微服务。我们学习了构建容器工件、使用 Jib 容器化、部署容器工件、使用 docker-compose 以及部署多服务应用程序。
在 第九章《分布式日志、跟踪和监控》中,我们学习了分布式日志、跟踪和监控。我们还学习了日志生产者、调度器、存储和可视化器。我们实现了 Elasticsearch、Logstash 和 Kibana。之后,我们在 Docker 中设置了 ELK,与一些 Micronaut 微服务集成,并在 Micronaut 中实现了分布式跟踪。最后,我们在 Docker 中设置了 Prometheus 和 Grafana。在第 第十章《使用 Micronaut 的物联网》中,我们学习了使用 Micronaut 的物联网。我们学习了物联网、Alexa 技能、太空事实、话语、意图、您的第一个 Alexa 技能、语音交互模型、将 Micronaut 与 Alexa、AWS、AlexaFunction 集成以及使用 Micronaut 测试 AlexaFunction。
总结来说,我们学习了创建企业应用程序所需的所有构建块,包括数据库、Web 服务、容器、部署、测试、配置、监控、事件驱动架构、安全和物联网。本书中涵盖的所有工作示例都可在本书的 GitHub 仓库中找到。
以下图表提供了本书所有章节的总结:

图 11.1 – 整合所有内容 – 章节路线图
通过这些,我们已经了解了创建 Micronaut 微服务的基础。现在,我们已经熟悉了 Micronaut、必要的开发工具、测试、数据库、事件驱动架构、分布式日志、跟踪、监控和物联网,我们拥有了创建企业级微服务所需的所有知识和技能。我们将在下一节学习如何架构企业级微服务。
架构企业级微服务
实施企业级微服务需要组织内多个利益相关者的理解和动力。你需要计划和分析微服务是否适合当前的问题。如果是的话,那么你必须设计、开发、部署、管理和维护该服务。
在开始使用微服务之前,让我们了解一下何时不应使用它们。问问自己以下问题:
-
你的团队是否了解微服务?
-
你的业务是否足够成熟以采用微服务?
-
你是否有敏捷的 DevOps 实践和基础设施?
-
你是否有可扩展的本地或云基础设施?
-
你是否有使用现代工具和技术的支持?
-
你的数据库是否准备好去中心化?
-
你是否得到了所有利益相关者的支持?
如果你对这些问题中的每一个都回答“是”,那么你可以适应并部署微服务。部署微服务最困难的部分是你的数据和基础设施。传统上,应用程序被设计成大型的单体应用,与去中心化、松散耦合的微服务相比。当你架构微服务时,你需要在各个阶段应用多种技术。以下图表展示了部署企业级微服务的阶段:

图 11.2 – 架构和部署企业级微服务的阶段
我们将在以下各节中查看这些各个阶段。
规划和分析
在你可以为企业的微服务创建架构之前,你需要分析它是否符合你的要求。你也可以只为应用程序的一部分实现微服务。
从传统的单体架构过渡到微服务可能是一个耗时且复杂的过程。然而,如果规划得当,它可以无缝部署。让所有利益相关者支持这一点至关重要,这可以在规划和分析阶段实现。团队成员对微服务的了解也是至关重要的,并且是成功的关键因素。
设计
在设计阶段,你可以使用我们在第一章“使用 Micronaut 框架开始微服务之旅”中学到的设计模式。设计模式是针对重复出现的业务或技术问题的可重用和经过验证的解决方案。以下是我们在这本书中迄今为止探索的设计模式:
-
按业务能力分解
-
按领域/子领域分解
-
API 网关模式
-
链式微服务模式
-
每个服务一个数据库
-
命令查询责任分离模式
-
服务发现模式
-
断路器模式
-
日志聚合模式
设计模式是持续演进的。始终检查行业中的新模式,并评估它们是否适合您的解决方案。在设计微服务时,下一个重要的考虑领域是安全性。我们在第四章“保护微服务”中学习了关于保护微服务的内容。在这里,我们学习了评估身份验证策略、安全规则、基于权限的访问、OAuth 和 SSL。
在设计时需要考虑的其他因素包括检查应用程序中的隐私特定标准,例如HIPAA(即健康保险可携带性和责任法案)、通用数据保护条例(GDPR)、个人信息保护与电子文件法案(PIPEDA)、银行法等。检查加密标准,例如静态加密和传输加密,您的数据在传输前是否加密,或者您的数据是否已存储并加密。检查当前使用的安全协议版本,并应用最新稳定、受支持的版本补丁。数据保留策略是设计微服务时需要考虑的另一个领域。您需要存储数据多长时间,数据存档策略是什么?此外,一些国家有关于数据存储的法规,请检查要求,并在设计时考虑它们。
开发
开发是实现微服务的一个重要阶段。始终使用最新稳定、受支持的版本进行开发。使用 Micronaut 框架进行自动化测试。在各个级别进行测试,例如单元测试、服务测试和集成测试。在第六章“测试微服务”中,我们学习了关于测试微服务的内容。在这里,我们了解到您应该始终使用容器或云基础设施来模拟真实世界环境,以及在测试期间使用模拟和监视概念。
如果为每个服务创建单独的版本控制策略,则可以将服务存储在具有所需配置和日志的单独存储库中。确保您同步开发、QA、UAT 和 PROD 环境,并在开发的不同阶段拥有相同的基础设施。在开发过程中,如果您想支持旧版本的微服务,请考虑向后兼容性。为每个微服务保留单独的数据库以发挥其最大潜力。
部署
当涉及到部署时,你应该使用自动化工具和技术,并利用快速应用部署策略。我们在第八章“部署微服务”中学习了如何部署微服务。使用容器、虚拟机、云、Jib 和 Jenkins 等工具,并高效地使用基础设施——不要过度分配。最后,确保你有一个专门的微服务 DevOps 策略,以促进持续集成和持续交付(CI/CD)。
管理和维护
维护多个微服务至关重要且复杂。你应该使用分布式日志、跟踪和监控来监控你的应用程序,正如我们在第九章“分布式日志、跟踪和监控”中学到的。使用 Elasticsearch、Logstash、Kibana、Prometheus 和 Grafana 等工具来做到这一点。使用 Sonar DevSecOps 监控代码库的健康状况,并定期检查代码中的安全漏洞。你应该经常更新技术的版本、操作系统和工具。此外,实时监控你的 CPU 使用率、内存占用和存储空间。
最后,在运行时扩展你的基础设施以避免硬件过度使用。我们将在接下来的章节中讨论如何扩展 Micronaut。
现在我们已经知道了如何架构微服务,让我们更深入地了解 Micronaut 的 OpenAPI。
理解 Micronaut 的 OpenAPI
API 是机器之间相互交互的通用语言。拥有 API 定义确保了存在一个正式的规范。所有 API 都应该有一个规范,这可以提高开发效率并减少交互问题。规范充当文档,帮助第三方开发人员或系统轻松理解服务。Micronaut 在编译时支持 OpenAPI(Swagger)YAML。我们曾在第七章中了解到这一点,处理微服务关注点。OpenAPI 倡议(OAI)之前被称为Swagger 规范。它用于创建描述、生成、消费和可视化 RESTful Web 服务的机器可读接口文件。OAI现在是一个促进供应商中立描述格式的联盟。OpenAPI 也被称为公共 API,它公开发布给软件开发人员和公司。Open API 可以用 REST API 或 SOAP API 实现。RESTful API 是行业中最流行的 API 格式。OpenAPI 必须具备强大的加密和安全措施。这些 API 可以是公开的或私有的(封闭的)。公开的 OpenAPI 可以通过互联网访问;然而,私有的 OpenAPI 只能在防火墙或 VPN 服务内的内网中访问。Open API 生成准确的文档,例如所有必需的元信息、可重用组件和端点详细信息。OpenAPI 规范(OAS)有几个版本;当前版本是 3.1。
您可以在www.openapis.org/了解更多关于 OpenAPI 的信息。
现在我们已经了解了 Micronaut 的 OpenAPI,让我们来了解如何在企业中扩展 Micronaut。
Micronaut 的扩展
在设计和实施企业应用程序时,需要规划扩展能力。使用微服务的最大优点之一是可扩展性。Micronaut 服务的扩展是设计中的一个关键因素。这不仅仅是处理量的问题——它还涉及到以最小的努力进行扩展。Micronaut 使得识别扩展问题并解决每个微服务级别的挑战变得更加容易。Micronaut 微服务是单一用途的应用程序,可以组合起来构建大型企业级软件系统。运行时扩展是现代化企业的一个关键因素。
有三种扩展类型:x轴、y轴和z轴扩展。以下图表说明了 x 轴(水平扩展)和 y 轴(垂直扩展):

图 11.3 – 垂直扩展与水平扩展
水平扩展也称为 x 轴扩展。在水平扩展中,我们通过创建新的服务器或虚拟机来扩展;服务的整个基础设施都会进行扩展。例如,如果有 10 个服务在一个虚拟机中运行,如果其中一个服务需要额外的容量,我们需要添加一个包含 10 个服务的虚拟机。如果你分析这个场景,你会看到会有未使用的服务器容量,因为只有一个服务需要额外的 CPU,而不是 10 个。然而,这种扩展类型提供了基础设施的无限制扩展。
垂直扩展也称为 y 轴扩展。在垂直扩展中,我们通过向现有服务器添加容量来扩展。容量通过添加额外的 CPU、存储和 RAM 来扩展。例如,如果有 10 个服务在一个虚拟机中运行,如果其中一个服务需要额外的容量,如 RAM 和 CPU,我们需要向同一个虚拟机添加额外的 RAM 和 CPU。这是水平扩展和垂直扩展之间的基本区别。然而,垂直扩展有一个限制:它不能超过特定的限制。以下图表展示了 x 轴(水平扩展)和 z 轴(微服务水平扩展):

图 11.4 – 传统水平扩展与微服务水平扩展对比
微服务水平扩展也称为 z 轴扩展。这几乎与传统水平扩展相同。例如,如果有 10 个服务在一个微服务容器环境中运行,如果其中一个服务需要额外的容量,我们需要添加一个微服务容器环境,而不是 10 个。如果你分析这个场景,你会看到这是对可用容量的最优化使用。这种扩展类型允许你按需多次扩展基础设施,并且是最具成本效益的方法。它的性能比非扩展环境要好得多。你可以使用容器和云基础设施进行扩展。自动扩展的功能非常强大,对于微服务来说非常实用。
现在我们已经学习了微服务的扩展,接下来让我们实现具有我们所学所有功能的的企业级微服务。
实现 Micronaut 的微服务
现在,让我们实现本章所学的内容。你可以使用本章 GitHub 仓库中的代码。我们将使用本书中涵盖的四个项目——宠物诊所、宠物主人、宠物评论和礼宾服务。我们还将使用 Zipkin 容器镜像进行分布式跟踪、Prometheus 进行指标和监控,以及elk容器镜像用于 Elasticsearch、Logstash 和 Kibana。
以下截图展示了本书 GitHub 仓库中我们将要使用的项目列表:

图 11.5 – 我们实现项目的 GitHub 项目
按照以下步骤操作:
-
第一步是设置 Keycloak。请参阅 第四章,保护微服务,设置 Keycloak 作为身份提供者 和 在 Keycloak 服务器上创建客户端 部分。
可以运行以下命令来创建 Keycloak Docker 镜像:
8888:Figure 11.6 – Docker Keycloak running -
一旦 Keycloak 启动并运行,需要从
pet clinic用户复制客户端密钥,并且客户端应该已经设置好。有关更多信息,请参阅 第四章,保护微服务。 -
在 Keycloak 中更改超时设置。转到 Keycloak 控制台 | 客户端 | 设置 | 高级设置。建议测试示例代码时使用 15 分钟。
以下屏幕截图说明了 Keycloak 访问 令牌有效期的位置:
![图 11.8 – Keycloak 访问令牌有效期]()
图 11.8 – Keycloak 访问令牌有效期
-
需要将密钥复制,以便在
pet-clinic、pet-owner、pet-clinic-reviews和pet-clinic-concierge项目中替换。YAML 应用程序配置文件client-secret需要从 Keycloak 密钥更新,如前一张截图所示。以下是需要更新的文件:Chapter11/micronaut-petclinic/pet-clinic-reviews/src/main/resources/application.yml Chapter11/micronaut-petclinic/pet-owner/src/main/resources/application.yml Chapter11/micronaut-petclinic/pet-clinic/src/main/resources/application.yml Chapter11/micronaut-petclinic/pet-clinic-concierge/src/main/resources/application.yml以下屏幕截图说明了需要替换客户端密钥 ID 的示例
.yaml文件配置:![图 11.9 – Keycloak 门户 – 应用 YAML 文件中的客户端密钥]()
图 11.9 – Keycloak 门户 – 应用 YAML 文件中的客户端密钥
-
一旦所有客户端密钥都已更新,请在所有四个项目(
pet-clinic-concierge、pet-clinic-reviews、pet-clinic和pet-owner)中执行 Maven Docker 构建。以下 Maven 命令为每个项目创建 Docker 镜像:pet-clinic and pet-reviews. Refer to *Chapter 02, Working on Data Access* for more details. -
检查 Docker 设置资源。您需要 4 个 CPU 和至少 6 GB 的内存。转到 Docker 设置 | 资源 | 高级:
![图 11.10 – Docker CPU 和内存设置]()
图 11.10 – Docker CPU 和内存设置
-
创建 Docker 镜像后的下一步是创建 Kafka、Zipkin、Prometheus 和 ELK 的
docker-compose。在终端或控制台中执行以下命令以创建 Docker 镜像:Chapter11/micronaut-petclinic/docker-zipkin/docker-compose.yml这就是文件的工作方式:
![图 11.12 – Docker Compose – Zipkin]()
Chapter11/micronaut-petclinic/docker-prometheus/docker-compose.yml这就是文件的工作方式:
![图 11.13 – Docker Compose – Prometheus]()
Chapter11/micronaut-petclinic/docker-prometheus/docker-elk这就是文件的工作方式:
![图 11.14 – Docker Compose – ELK]()
Chapter11/micronaut-petclinic/docker-compose.yml现在,这是文件的工作方式:
![图 11.15 – Docker Compose Micronaut pet clinic]()
图 11.15 – Docker Compose Micronaut pet clinic
注意,安全配置保护了使用 Keycloak 的所有项目,除了
pet-clinic-reviews:-
pet-clinic-concierge: 使用 Keycloak 保护 -
pet-clinic: 使用 Keycloak 保护 -
pet-owner: 使用 Keycloak 保护 -
pet-clinic-reviews: 未受保护
-
-
在测试应用之前,检查所有应用是否都在 Docker 容器中运行。以下截图展示了成功在 Docker 容器中运行的应用:![图 11.16 – 运行所有必需应用的 Docker 容器
![图片]()
图 11.16 – 运行所有必需应用的 Docker 容器
-
现在所有项目都在 Docker 中运行,让我们测试 URL 的集成。你可以从 API 网关调用所有服务,如下所示:
-
pet-owner:http://localhost:32584/api/owners -
pet-clinic:http://localhost:32584/api/vets -
pet-clinic-reviews:http://localhost:32584/api/vet-reviews一旦你在 Docker 中运行了应用,你需要通过在 Keycloak 上调用以下 API 来获取安全令牌:
curl -L -X POST 'http://localhost:8888/auth/realms/master/protocol/openid-connect/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'client_id=pet-clinic' \ --data-urlencode 'grant_type=password' \ --data-urlencode 'client_secret=PUT_CLIENT_SECRET_HERE' \ --data-urlencode 'scope=openid' \ --data-urlencode 'username=alice' \ --data-urlencode 'password=alice'备注
PUT_CLIENT_SECRET_HERE必须替换为 Keycloak 凭据的秘密。现在,这是控制台中的请求看起来像这样:
-

图 11.17 – 获取 JWT 的 curl 请求
然后,我们可以检查 curl 请求返回访问令牌:

图 11.18 – JWT 的 curl 响应
-
使用 JSON 格式化工具复制和格式化 JSON 响应。格式化响应中的
access_token属性值必须复制,并将此值作为 JWT 传递,如下所示:![图 11.19 – JWT 的 curl 响应![图片]()
图 11.19 – JWT 的 curl 响应
-
一旦你收到
curl的响应,你可以复制access_token值。access_token可以作为请求头中的值传递,如下面的截图所示:

图 11.20 – vets API 的 REST 响应
现在,我们必须使用以下链接测试所有应用 URL 及其集成:
-
http://localhost:32584/. -
http://localhost:8500/ui/dc1/services。这将启动 Consul,如截图所示:

图 11.21 – vets API 的 REST 响应
http://localhost:5601/app/kibana. 这将启动如以下截图所示的 Kibana 日志门户:

图 11.22 – Kibana 日志门户
备注
Kibana 的默认用户名是 elastic,密码是 changeme。
http://localhost:3000/?orgId=1. 这将启动 Grafana 监控工具。配置仪表板的详细步骤可以在第九章,分布式日志、跟踪和监控中找到:

图 11.23 – 使用 Grafana 进行分布式监控
备注
Prometheus,默认用户名为 admin,密码为 pass。
http://localhost:9411/zipkin/。以下截图展示了 Zipkin 分布式跟踪的用户界面:
![Figure 11.24 – 使用 Zipkin 进行分布式跟踪
![img/Figure_11.24_B16585.jpg]
图 11.24 – 使用 Zipkin 进行分布式跟踪
-
http://localhost:8888/auth/是将启动 Keycloak 的身份提供者,而 Keycloak 是身份提供者。 -
http://localhost:9100/。在这里,你可以使用 Kafdrop 查看集群:
![Figure 11.25 – Kafdrop 集群视图
![img/Figure_11.25_B16585.jpg]
图 11.25 – Kafdrop 集群视图
总体而言,我们已经测试了从网关到分布式监控、跟踪、日志记录和搜索的集成所有功能。有了这些,我们就完成了这一章。在本节中,我们学习了如何在生产中实现微服务。
摘要
在本章中,我们将前几章所学的内容综合起来。然后,我们深入探讨了构建企业级微服务的架构。我们还学习了微服务的扩展、不同类型的扩展及其优势。
本章增强了你的 Micronaut 微服务知识,以便你可以创建生产就绪的应用程序。它为你提供了所有必要的技能和专业知识。我们从微服务的基础开始,现在我们有了使用 Micronaut 创建企业级微服务的知识。我们希望你喜欢与我们一起学习的旅程。
问题
-
在设计微服务时,你应该考虑哪些因素?
-
列举一些设计模式。
-
在微服务的设计阶段,你应该考虑哪些因素?
-
在微服务的开发阶段,你应该考虑哪些因素?
-
列举一些在部署阶段使用的工具。
-
列举一些在管理和维护阶段使用的工具。
-
有哪些不同的扩展类型?
-
在微服务架构中使用了哪种类型的扩展?
第十二章:评估
第一章到第九章的答案大多是理论或实践性的。所以,阅读它们就足够回答这些章节的问题了。
第十章
-
物联网(IoT)是一个设备或事物的网络。这些事物可以是任何东西,从佩戴健康监测器的人类,佩戴地理定位传感器的宠物,带有轮胎压力传感器的汽车,具有语音/视觉功能的电视,或智能扬声器。
我们在 物联网基础 部分讨论了这一点。
-
门铃、锁、灯泡、扬声器、电视、医疗系统、健身系统、Google Home、Amazon Alexa、Apple Siri、Microsoft Cortana 和 Samsung Bixby。
我们在 物联网基础 部分讨论了这一点。
-
Alexa 技能就像应用一样,你可以使用特定设备的 Alexa 应用程序启用或禁用技能。技能是基于语音的 Alexa 功能。
我们在 物联网基础 部分讨论了这一点。
-
意图捕获最终用户希望用语音实现的事件。意图代表由用户的语音请求触发的动作。Alexa 中的意图在名为意图方案的 JSON 结构中指定。内置意图包括 取消、帮助、停止、导航回家 和 回退。一些意图是基本的,例如帮助,技能应该有一个 帮助 意图。
我们在 意图基础 部分讨论了这一点。
-
Java、Node.js、C# 或 Go。
-
我们在 你的第一个 HelloWorld Alexa 技能 部分讨论了这一点。
-
LaunchRequestHandler.LaunchRequestHandler是每次技能启动时都会触发的第一个意图代码。我们在 你的第一个 HelloWorld Alexa 技能 部分讨论了这一点。
-
@Singleton注解。要将任何 Alexa Java 处理器改为与 Micronaut 一起工作,我们只需要添加@Singleton注解,即javax.inject.Singleton注解。 -
我们在 将 Micronaut 与 Alexa 集成 部分讨论了这一点。
第十一章
-
架构微服务时需要考虑的以下是一些因素:
-
你的团队是否了解微服务?
-
你的业务是否足够成熟以采用微服务?
-
你是否有敏捷 DevOps 实践和基础设施?
-
你是否有可扩展的本地或云基础设施?
-
你是否有使用现代工具和技术支持?
-
你的数据库是否准备好进行去中心化?
-
你是否得到了所有利益相关者的支持?
我们在 企业微服务架构 部分讨论了这一点。
-
-
这些是一些设计模式:
-
按业务能力分解
-
按领域/子领域分解
-
API 网关模式
-
链式微服务模式
-
每个服务一个数据库
-
命令查询责任分离模式
-
服务发现模式
-
断路器模式
-
日志聚合模式
我们在 设计 部分讨论了这一点。
-
-
以下是一些在微服务设计阶段应考虑的因素:
-
设计模式
-
安全
-
认证策略
-
安全规则
-
基于权限的访问
-
OAuth 和 SSL
-
针对隐私的特定标准,如 HIPAA、GDPR 和 PIPEDA
-
静态加密和传输加密
-
数据保留策略和日志归档策略
-
本地国家法规
-
技术货币/版本
我们在设计部分讨论了这一点。
-
-
以下是一些在微服务开发阶段应考虑的因素:
-
开发时始终使用最新稳定且受支持的版本
-
利用自动化测试
-
在各种级别进行测试,如单元测试、服务测试和集成测试
-
在测试期间使用模拟和监视概念
-
使用容器或云基础设施模拟真实世界环境
-
同步开发、QA、UAT 和 PROD 环境
-
在开发过程中,考虑微服务的向后兼容性
-
为每个微服务拥有单独的数据库以实现最大潜力
我们在开发部分讨论了这一点。
-
-
以下是一些在部署阶段使用的工具:
-
容器
-
虚拟机
-
云
-
Jib
-
Jenkins
-
DevOps 策略和支持持续集成和持续交付的基础设施
我们在部署部分讨论了这一点。
-
-
以下是一些在管理和维护阶段使用的工具:
-
Elasticsearch
-
Logstash
-
Kibana
-
Prometheus
-
Grafana
-
Sonar
我们在管理和维护部分讨论了这一点。
-
-
以下是一些缩放类型:
-
垂直
-
横向
-
微服务横向
我们在缩放 Micronaut部分讨论了这一点。
-
-
使用的工具是微服务横向(基于容器/服务的缩放)。
我们在缩放 Micronaut部分讨论了这一点。















浙公网安备 33010602011771号