精通-SpringBoot3-全-

精通 SpringBoot3(全)

原文:zh.annas-archive.org/md5/40d9612ff55dffe4043960172afdfd1e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《精通 Spring Boot 3.0》深入探讨了 Spring Boot 3.0,重点关注其高级特性。这项技术被视为渴望构建复杂和可扩展后端系统的 Java 开发者的必备技术。引言为全面指南奠定了基础,强调了 Spring Boot 3.0 在现代软件开发中的实用性。

这本书面向的对象

如果你是一名渴望提升技能的 Java 开发者,那么《精通 Spring Boot 3.0》这本书适合你。对于想要通过高级 Spring Boot 特性构建强大后端系统的微服务架构师、DevOps 工程师和技术负责人,这本书也将非常有用。对微服务架构的基础理解以及一些 RESTful API 的经验将帮助你最大限度地利用这本书。

这本书涵盖的内容

第一章, 高级 Spring Boot 概念简介, 介绍了 Spring Boot 3.0 的高级特性,为后续章节提供了基础。

第二章, 微服务中的关键架构模式 – DDD、CQRS 和事件溯源, 探索了 DDD、CQRS 和事件溯源等基本架构模式,提供了理论知识和实践示例。

第三章, 反应式 REST 开发和异步系统, 涵盖了 Spring Boot 中的反应式编程以及异步系统的实现细节。

第四章, Spring Data:SQL、NoSQL、缓存抽象和批处理, 讨论了使用 Spring Data 管理数据,包括 SQL 和 NoSQL 数据库,并介绍了缓存抽象和批处理。

第五章, 保护您的 Spring Boot 应用程序, 综合探讨了使用 OAuth2、JWT 和 Spring Security 过滤器保护 Spring Boot 应用程序的方法。

第六章, 高级测试策略, 深入探讨了 Spring Boot 应用程序中的测试策略,重点关注单元测试、集成测试和安全测试技术。

第七章, Spring Boot 3.0 的容器化和编排特性, 专注于 Spring Boot 3.0 的容器化和编排特性,包括 Docker 和 Kubernetes 的集成。

第八章, 使用 Kafka 探索事件驱动系统, 探讨了 Kafka 与 Spring Boot 的集成,用于构建事件驱动系统,并包括监控和故障排除技巧。

第九章, 提高生产力和开发简化, 通过诸如面向方面编程和自定义 Spring Boot 启动器等工具和技术简化开发过程。

为了最大限度地利用这本书

在阅读本书之前,您需要具备 Java 17 的基本知识。应在您的计算机上安装Java 开发工具包JDK 17)。所有代码示例都已在 macOS 上使用 JDK 17 进行了测试。然而,它们应该在其他操作系统上也能工作。

本书涵盖的软件/硬件 操作系统要求
JDK 17 Windows, macOS, 或 Linux
Gradle 8.7
Docker Desktop
IDE IntelliJ Community Edition, Eclipse

如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件 github.com/PacktPublishing/Mastering-Spring-Boot-3.0。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他丰富的图书和视频资源中的代码包可供在 github.com/PacktPublishing/ 获取。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“然而,在响应式世界中,我们使用ReactiveCrudRepositoryR2dbcRepository。”

代码块设置如下:

# Enable H2 Console
spring.h2.console.enabled=true
# Database Configuration for H2
spring.r2dbc.url=r2dbc:h2:mem:///testdb
spring.r2dbc.username=sa
spring.r2dbc.password=
# Schema Generation
spring.sql.init.mode=always
spring.sql.init.platform=h2

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

./gradlew bootRun

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“一旦您完成所有选择,请点击生成按钮以获取可构建的项目。”

小贴士或重要注意事项

看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及本书标题。

勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告这一点。请访问 www.packtpub.com/support/errata 并填写表格。

盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packt.com 并附上材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《Mastering Spring Boot 3.0》,我们很乐意听到您的想法!请点击此处直接转到该书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本

感谢您购买此书!

您喜欢在路上阅读,但无法携带您的印刷书籍到处走?

您的电子书购买是否与您选择的设备不兼容?

别担心!现在,每本 Packt 书籍都免费提供该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不止于此,您还可以获得独家折扣、时事通讯和每天收件箱中的优质免费内容

按照以下简单步骤获取好处:

  1. 扫描二维码或访问以下链接:

packt.link/free-ebook/9781803230788

  1. 提交您的购买证明。

  2. 就这些!我们将直接将您的免费 PDF 和其他好处发送到您的电子邮件。

第一部分:建筑基础

在本部分中,我们将深入探讨高级 Spring Boot 概念的复杂世界。本部分为更深入地理解如何利用 Spring Boot 构建强大和可扩展的应用程序奠定了基础。

本部分包含以下章节:

  • 第一章高级 Spring Boot 概念简介

第一章:高级 Spring Boot 概念介绍

欢迎来到掌握 Spring Boot 3.0 项目指南之旅。这本书不是一本手册;相反,它充当你探索现代 Java 开发复杂世界的路线图。Spring Boot 不是一个新来者,而是一个成熟的框架,多年来一直在简化 Java 开发。但在 3.0 版本中,Spring Boot 使开发过程变得更加无缝和方便使用。Java 17 是与 Spring Boot 3.0 一起所需的最低 Java 版本,Java 19 也支持这些版本之一,这确保了开发者能够利用 Java 的最新特性和改进。Spring Boot 3.0 提出了 AppStartup – 一个在应用程序启动的不同阶段注册回调的功能,有助于资源初始化和配置错误检查等任务。除此之外,Spring Boot 3.0 还引入了一种新的依赖解析算法,有助于提高启动速度并降低内存占用,因此可以更有效地处理更复杂的项目。

当您完成这本书的阅读时,您将不仅熟悉,而且熟练、高效,最重要的是,能够在实际场景中有效地实施 Spring Boot。

那么,您在本章中可以期待什么?我们将深入探讨为什么 Spring Boot 是项目首选框架的原因。我们将探讨其优势和 Spring Boot 3.0 的新特性。本章为更有效地使用 Spring Boot 3.0 打下基础,确保您能够自信且熟练地应对复杂项目。让我们开始吧!

在本章中,我们将涵盖以下主要主题:

  • 为什么要在高级项目中使用 Spring Boot?

  • 对即将介绍内容的简要概述

技术要求

本章没有技术要求。本章包含的代码块用于解释某些概念,并不打算执行。

为什么要在高级项目中使用 Spring Boot?

欢迎来到您探索 Spring Boot 3.0 世界之旅的开始!在本节中,我们将讨论 Spring Boot 在创建最复杂的软件项目方面的潜力。我们将详细阐述为什么 Spring Boot 不仅仅是一个框架,而且更加复杂。它将是您在处理软件开发复杂挑战时的最佳伙伴。

现代软件开发复杂性

首先,让我们明确现代软件开发的复杂性。正如您所知,在软件项目中会出现许多不同的挑战。当我们有一个任务或项目时,我们需要考虑可扩展性、数据安全、在云环境中编排服务等等。在以前,开发者负责代码质量和性能。但现在,我们需要考虑和覆盖整个栈。

看看现代应用程序。它们必须适应用户需求的演变动态,必须利用云原生能力和尖端技术,并且必须始终保持安全。在确保为用户提供响应和可靠体验的同时完成所有这些,并不容易。

我能感觉到你眼中的担忧。不必害怕;我们有一个完美的工具来克服所有这些困难。这是一个帮助我们穿越这个复杂景观的工具。这是一个简化开发并使开发者能够克服所提到的挑战的框架。这个工具就是 Spring Boot – 它的好处使其成为未来项目的有力候选者。

让我们深入探讨为什么 Spring Boot 成为了处理高级软件项目的首选框架。

Spring Boot 的优势

本节包含 Spring Boot 的各种优势。我们将逐一介绍这些优势,并讨论它们如何使我们的生活变得更轻松,以及我们如何使用它们。

优势 1 – 快速开发

在软件开发的世界里,时间是最宝贵的资源。我们应该尽快将我们的产品推向市场,因为市场竞争非常激烈。Spring Boot 提供了流线化的开发体验,使其成为许多开发者的优秀选择。它消除了样板配置的需求,使您能够专注于编写业务逻辑。借助 Spring Boot 的自动配置和启动依赖项,您可以在几分钟内而不是几小时内设置好项目。仅此一项功能就节省了大量时间和精力,使开发者能够专注于他们最擅长的事情——编写代码。正如您在图 1**.1中看到的那样,在 Spring Initializr 中只需单击一下即可开始开发。

图 1.1:Spring Initializr 页面

图 1.1:Spring Initializr 页面

想象一下快速开发的好处。这意味着您可以更快地交付,更快地获得利益相关者的反馈,并迅速实施新的变更请求。Spring Boot 使您能够在竞争激烈的市场中保持敏捷和响应。

优势 2 – 微服务就绪

如您所知,微服务架构是新时代的产物。即使我们为一个小型初创想法设计最小可行产品MVP),我们也是在考虑微服务结构,包括异步通信的可扩展性、使其独立部署,并确保灵活性。那么,哪个框架能帮助我们实现这一点呢?是的,Spring Boot!

关于微服务的可扩展性优势,我们可以根据需要扩展我们应用程序的各个组件,优化资源使用。Spring Boot 对构建微服务的支持简化了这一过程,使您能够专注于开发每个服务的核心功能。

优势 3 – 流线化配置

每个在更大或更复杂的项目上工作过的开发者都会面临配置管理的噩梦。传统方法通常会使 XML 文件、属性文件和环境特定设置变得混乱。

Spring Boot 遵循“约定优于配置”的哲学,提供合理的默认值,并提供自动设置,从而简化了设置的管理。

你是否想过一个世界,在那里你花在配置文件调整上的时间更少,而花在真正编写代码上的时间更多?使用 Spring Boot,配置的简单性将导致更干净、更易于维护的代码。你可以通过遵循最佳实践和避免不必要的样板代码来专注于应用程序的实际功能,从而实现这一点。

请参阅以下示例 XML 配置:

<beans 

       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- Bean Definition -->
    <bean id="myBean" class="com.example.MyBean">
        <property name="propertyName" value="value"/>
    </bean>
    <!-- Data Source Configuration -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
        <property name="username" value="user"/>
        <property name="password" value="password"/>
    </bean>
</beans>

在 XML 配置中引入服务或 Bean 是复杂且难以管理的,正如你在之前的 XML 文件中看到的。在你编写服务之后,你还需要在 XML 文件中对其进行配置。

现在,我们将看到在 Spring Boot 中这有多简单。你可以用简单的 @Service 注解来编写你的类,它就变成了一个 Bean:

@Service
public class MyBean {
    private String propertyName = "value";
    // ...
}

以下是一个应用程序属性文件。在之前的 XML 配置中,你看到很难看到和管理数据源属性。但在 Spring Boot 中,我们可以通过以下方式在 YAML 或属性文件中定义数据源:

spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

你可以看到,让我们的代码更易于阅读和管理是多么简单。

它还通过简化的配置促进了开发团队之间的协作。当每个人都使用相同的约定并依赖于 Spring Boot 提供的自动配置时,它减少了理解和工作在彼此代码上所需的时间。这意味着在做事上的一致性,除了最小化由配置引起的问题风险外,还促进了效率。

优势 4 – 广泛的生态系统

如果我们都能只编写代码而不需要任何集成,那将是非常棒的。但正如我们在本章引言中所说的,我们有时要处理复杂的项目,而所有复杂的项目都需要数据库、组件间的消息传递以及与外部服务的交互。因此,多亏了 Spring 生态系统,我们可以通过使用 Spring 的库和项目来实现这些功能。

正如你在 图 1.2 中所见,Spring 不仅仅是一个框架,而是一个生态系统,每个组件都准备好与其他组件顺畅地通信。

图 1.2:Spring 生态系统

图 1.2:Spring 生态系统

我希望花更多的时间来探讨 Spring Boot 的生态系统,它提供了许多工具和集成,可以全面解决这些挑战。以下是 Spring Boot 生态系统成为宝贵资产的原因:

  • 支持多种数据库:Spring Boot 最重要的特性之一是它使 SQL 以及 NoSQL 数据库(如 MySQL 和 MongoDB)的数据访问和管理变得更加容易。其配置能力简化了两种数据库之间的切换,只需更改对象的注解和属性文件中的Java 持久化 APIJPA)数据源即可。

  • 消息解决方案:通过您的应用程序支持异步通信或事件驱动架构,Spring Boot 与 Apache Kafka 和 RabbitMQ 等工具的兼容性,在高效消息队列以及有效事件流方面提供了很大帮助。

  • Spring Cloud 微服务支持:Spring Boot 提供了一个 Spring Cloud 扩展,它提供了一套工具,使开发者能够快速构建和操作微服务,以便作为应用程序运行。它通过声明性编程模型帮助进行服务发现、负载均衡和分布式配置。

  • 云服务集成:在当前的云计算领域,Spring Boot 提供了与该领域主要玩家的集成能力,包括亚马逊网络服务AWS)、Azure 和谷歌云。这使您能够利用这些云提供商提供的资源和服务,包括存储、计算和机器学习,以增强您应用程序的功能和能力。

  • 安全和身份验证:Spring Boot 生态系统提供了强大的安全库,这些库支持易于配置的安全身份验证和授权。无论您是想实现 OAuth 2.0 或 JWT 身份验证,还是希望根据角色应用访问控制,Spring Boot 都能满足这些需求。

  • 应用程序监控和管理:正确地监控和管理应用程序对于保持软件应用程序处于健康状态至关重要。Spring Boot Actuator 作为 Spring Boot 的关联子项目,提供了内置的指标收集、健康检查功能和管理端点支持,将其功能添加到您的服务中并不困难。

  • 第三方集成:除了核心功能外,Spring Boot 还提供了与一系列第三方库和框架的平滑集成。无论您是想集成特定的技术栈或专用库,通常您都会找到适合情况的 Spring Boot 扩展或集成。

通过使用 Spring Boot 的广泛生态系统,可以使软件开发过程更快,在各个集成级别遇到的障碍更少,并且可以访问广泛的工具和资源。Spring Boot 提供的生态系统在增强软件开发过程中的灵活性和多功能性方面非常出色,尤其是在软件开发的动态环境中。

优势 5 – 云原生能力

现在,让我们看看 Spring Boot 如何最好地融入云原生开发。当我们谈论云原生时,实际上我们指的是为云环境(如 AWS、Azure 和 Google Cloud)设计的应用程序。Spring Boot 为这些环境中的应用程序提供了诸如可扩展性和弹性等优秀特性,这意味着我们的应用程序将根据需求水平扩展或收缩,同时我们还能访问多个托管服务。

想要用 Spring Boot 构建你的应用程序并在云上部署它吗?好消息是 Spring Boot 封装了所有配置细节,因此使得在云上的部署过程变得非常简单。它被设计成与云环境无缝工作。这意味着你可以轻松地将你的应用程序绑定到提供商提供的各种云服务。这些服务可能包括数据库和存储解决方案,甚至到身份管理系统。

使用 Spring Boot 进行云原生应用程序开发的一个优势是适应性。无论你选择公共云、私有云,还是两者的混合——我们称之为混合环境——Spring Boot 都提供了一个简化的体验。你永远不会担心与手动配置相关的复杂性。Spring Boot 中的云原生能力使你能够充分利用今天云计算提供的各种能力。

这意味着根据特定时间点的某种持续情况进行应用程序的扩展或缩减。例如,你想要创建一个应用程序,当其用户突然增加时,它会自动扩展其资源——这将涉及 Spring Boot 的云原生开发和在 Cloud Foundry 上的部署。在这种情况下,Spring Boot 是桥梁,因为它负责你的应用程序,并确保它在云环境中充分利用所提供的一切,保持功能完整。它将使你的开发过程高效且有效,并确保你开发的应用程序在部署时更具弹性。

优势 6 - 测试变得简单

现在,我们将讨论测试在软件开发中的重要性以及 Spring Boot 如何帮助这一庞大的过程。正如你所知,为了确保我们的软件可靠且按预期行为,进行充分的测试非常重要。我相信你将非常熟悉为什么测试如此重要——我们必须在软件上线之前捕捉到错误和问题。

Spring Boot 真正促进了测试,并拥有许多工具和约定来实现这一点。这不仅确保了长期节省时间,还为我们的用户提供了更好的产品。Spring Boot 完美地适合这种“先测试”的方法,这种方法推动我们在开发的每一步都考虑测试,而不是事后才考虑。

那么,Spring Boot 是如何帮助我们进行测试的呢?它的一大优点是它的灵活性,因此它不会引入各种测试框架,这可能会产生兼容性问题。无论你更喜欢 JUnit、TestNG 还是其他任何流行的测试工具,使用 Spring Boot,这些工具都可以轻松集成到工作流程中。这样,你可以选择你感到舒适的工具,Spring Boot 不会限制你的选择。

此外,Spring Boot 不会限制你只进行一种类型的测试。它允许你编写不同类型的测试——从验证一小段代码正确性的单元测试,到验证应用程序不同部分之间良好通信的集成测试,甚至模拟用户如何通过应用程序的端到端测试。这里的想法是为你提供所有这些工具和灵活性,以便在深度上测试你的应用程序的任何级别。

简而言之,Spring Boot 为你提供了使测试高效和有效的所有工具。它就像一个工具箱,每个工具都是为了解决特定的测试需求而设计的,使你的软件更加健壮和可靠。记住,良好的测试是高质量软件开发的关键要素之一,Spring Boot 将引导你通过这一过程。

优势 7 – 活跃的开发

让我们讨论一下 Spring Boot 及其在技术快速发展的世界中的兼容性。

在软件开发中,由于技术的快速发展,跟上时代步伐至关重要。这就是 Spring Boot 发挥作用的地方,作为一个随着时间不断发展的动态框架。此外,它正由一个致力于添加新功能以及最大化安全应用的社区积极开发。借助 Spring Boot,你可以与最新的技术趋势互动,例如新的 Java 版本或容器化技术,而无需每次都从头开始。这个框架随着行业的发展而不断变化,以保持你的开发之旅与时俱进,甚至更接近你项目构建的现代进步性基础。在技术世界中,一切都在不断变化,Spring Boot 作为一个实用的最新指南,帮助你保持领先。

优势 8 – 社区驱动的插件

让我们了解 Spring Boot 的社区。它就像一个大家庭,每个人都有一个共同的目标。来自世界各地的人们为 Spring Boot 创建了大量的附加组件和扩展,使其变得更好。这就像拥有一个巨大的工具箱,其中每个工作都有理想的工具。

在这个工具箱中,有插件来满足每个目的。需要连接数据库或建立消息系统?有相应的插件。想要让你的应用程序更安全或更容易部署?也有相应的插件。最好的部分是?这些插件已经经过很多人的试用和测试,因此它们已经得到了完善。

使用这些详尽、由社区制作的插件意味着你不必每次都从头开始,可以避免浪费时间制作已经存在的东西。有了这些插件,你能够更快地构建,并加入全球开发者团队,分享他们的知识和工具。这样,所有开发者都能更快地构建更酷的东西。

在讨论了 Spring Boot 的基础优势之后,我们现在将开始学习它的最新版本。

迎接新时代——Spring Boot 3.0 的创新

Spring Boot 3.0 标志着高级 Java 应用程序开发故事的一个重要部分。让我们探索这个新主题包含的内容。

Java 17 基线

将 Spring Boot 3.0 与 Java 17 对齐,为你带来 Java 宇宙的最新发展。通常,像密封类和 Java 17 中的新 API 等特性,以及其他一些特性,可以提高代码的可读性和可维护性。使用 Java 17 与 Spring Boot 一起意味着使用一个不仅是最新的版本,而且还有 Java 的扩展支持的版本。这为你提供了更干净的代码以及更好的性能,同时走在技术的前沿。使用 Java 17,引入了许多新特性——以下是一个使用密封类的简单示例:

public sealed class Animal permits Dog, Cat, Rabbit {
    // class body
}
final class Dog extends Animal {
    // class body
}
final class Cat extends Animal {
    // class body
}
final class Rabbit extends Animal {
    // class body
}

这个功能允许你控制哪些类或接口可以扩展或实现特定的类或接口。这个功能特别有用,可以维护代码完整性并防止意外的子类。

GraalVM 支持

在 Spring Boot 3.0 中,GraalVM 的支持是一个重要的特性,尤其是对于云原生解决方案。当我们有一个开发无服务器项目的任务时,Java 通常不是首选选项。这是因为 Java 项目在启动时需要更多的时间,并且比其他开发语言消耗更多的内存。但是,GraalVM 支持帮助 Spring Boot 减少内存使用并缩短启动时间。对于微服务和无服务器架构,这意味着达到一种效率水平,允许更快地扩展和优化资源利用。

使用 Micrometer 进行可观察性

让我们谈谈 Spring Boot 3.0 中的一个令人兴奋的功能——Micrometer 的集成。想象一下,Micrometer 是一个工具,它通过查看日志、指标和跟踪,让我们了解应用程序内部正在发生的事情。有了 Micrometer 跟踪,Micrometer 工具在 Spring Boot 中变得更加有用。现在我们能够更有效地记录应用程序指标,并执行更有效的操作跟踪。这就像有了更高级的方式来检查我们的应用程序在当前技术下的执行情况,比我们过去依赖的老方法好得多,尤其是在我们处理由编译的本地代码构建的应用程序时。

Jakarta EE 10 兼容性

我将尝试解释 Spring Boot 3.0 中向 Jakarta EE 10 的过渡。所以,这就像在出发前更新你的 GPS 到最新的地图和功能一样。以类似的方式,转向 Jakarta EE 10 使我们能够利用企业 Java 中可用的最新工具和标准。这样,我们就能确保所有构建的应用程序都使用现代标准,并且具有前瞻性。这次更新不仅使我们的应用程序保持最新,还使我们能够与其他符合新标准的更先进的技术一起工作。所以,这无外乎是我们开发旅程中的飞跃。

简化的 MVC 框架

Spring Boot 3.0 中的 MVC 框架更新改进了我们的通信管理方式,尤其是 API 错误处理。对 RFC7807 (datatracker.ietf.org/doc/html/rfc7807) 的支持意味着我们的应用程序可以在一个地方处理异常。以下代码示例说明了如何在同一处处理异常:

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
  @ExceptionHandler(Exception.class)
  public ResponseEntity<ProblemDetail> handleException(Exception ex, WebRequest request) {
    ProblemDetail problemDetail = new ProblemDetail();
    problemDetail.setTitle("Internal Server Error");
    problemDetail.setDetail(ex.getMessage());
    problemDetail.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    return new ResponseEntity<>(problemDetail, HttpStatus.INTERNAL_SERVER_ERROR);
  }
  @ExceptionHandler(ResourceNotFoundException.class)
  public ResponseEntity<ProblemDetail> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
    ProblemDetail problemDetail = new ProblemDetail();
    problemDetail.setTitle("Resource Not Found");
    problemDetail.setDetail(ex.getMessage());
    problemDetail.setStatus(HttpStatus.NOT_FOUND.value());
    return new ResponseEntity<>(problemDetail, HttpStatus.NOT_FOUND);
  }
  // other exception handlers
}

在这个例子中,GlobalExceptionHandler 是一个 @ControllerAdvice 类,它处理应用程序抛出的所有异常。它为应用程序可能抛出的每种异常类型都有一个 @ExceptionHandler 方法。每个 @ExceptionHandler 方法返回一个包含 ProblemDetail 对象作为正文和适当的 HTTP 状态码的 ResponseEntityProblemDetail 对象包含错误的详细信息,包括标题、详细信息和状态码。

增强的 Kotlin 支持

Kotlin 在开发者中越来越受欢迎。如果你对 Kotlin 更有信心,Spring Boot 3.0 现在提供了增强的 Kotlin 支持。这种支持扩大了 Spring 社区。

总结——为什么 Spring Boot 3.0 是你的高级项目盟友

在前面的章节中,我们看到了 Spring Boot 如何通过其快速开发成为开发大型和高级软件项目的强大工具。有了 Spring Boot,我们谈论的是通过其“约定优于配置”的设置大幅减少开发时间和努力。这意味着什么?有更多的时间用于开发,更少的时间用于设置和配置。

好吧,现在让我们谈谈它如何适应微服务。Spring Boot 不仅是一种促进开发的方式,还可以使你的应用程序更可扩展和高效。随着新的微服务架构的兴起,这变得至关重要。它允许你将应用程序分解为更小、更易于管理和完全独立的实体,这些实体作为一个整体完美协作。

我们讨论的另一个方面是处理简化的配置。Spring Boot 的自动配置功能取代了手动配置的处理,这可能非常无聊。这对于处理配置可能增长的大型项目来说非常关键,因为这是一个非常复杂且耗时的任务。

我们还简要介绍了 Spring Boot 提供的生态系统。这个生态系统提供了一系列插件和工具。这个环境将你需要的所有构建、测试和部署高标准应用程序的工具都放在了你的指尖。

云原生能力使 Spring Boot 成为无服务器应用程序开发的框架选择。鉴于越来越多的迁移趋势是向云环境,这种能力变得更加关键。

最后,一切都关乎于社区的不断发展和支持。一个活跃的社区和持续的发展使 Spring Boot 与最新的技术和趋势保持一致。这使得这款软件成为处理复杂项目的持久和未来证明的选择。

现在是时候用 Spring Boot 3.0 来提升你的开发叙事了。

随着我们在这本书中的进展,我们将更深入地探索 Spring Boot 的世界。我们将研究各种架构模式、响应式编程、数据管理、测试、安全、容器化和事件驱动系统。在每一章中,你将获得实践经验,并更接近你在现实世界项目中的成功。

对即将发生的事情的简要概述

本节将概述本书剩余部分我们将讨论的内容。这将照亮你的道路,并给你一个关于接下来章节内容的预览。

第二章,微服务的关键架构模式——领域驱动设计(DDD)、CQRS 和事件溯源

本章深入探讨了微服务的关键模式。在一个微服务系统中,你可能会有很多微服务,这取决于应用程序的大小;例如,Netflix 有超过 1,000 个微服务。因此,我们需要一个优秀的模式来管理这些微服务,并正确地维护它们。没有它,我们就失去了对它们的控制,整个系统变成了一堆垃圾。

第一部分是 领域驱动设计DDD)。DDD 是关于根据业务需求构建软件。每个微服务只对业务的一个小部分负责。在 DDD 中,我们有两个主要部分,即战略部分和战术部分。在战略部分,我们审视业务的宏观图景。我们关注的细节是战术部分。在这里,我们将详细探讨关于业务每个部分的全部知识。

接下来是 CQRS。它是 Command Query Responsibility Segregation 的缩写。我喜欢这个名字。对于一个简单想法来说,这个名字太华丽了。我们分离了读取数据和写入数据。把它想象成两个工具——一个用来提问,另一个用来下达命令。这种分离使得我们的软件运行得更顺畅、更快。这对于需要管理大量数据的复杂系统来说非常棒。

接下来,我们有事件溯源。这是将我们对软件所做的所有更改记录为事件。每当交易方发生变化时,我们都会在日记中记录下来。因此,日记记录了过去发生的事情。我们还可以深入了解我们的对象的历史。事件溯源的相关性在于,在任何时候都可能需要保留过去的数据。

最后,我们快速浏览了微服务中的其他模式。这部分仅仅提出了一些构建软件的其他想法。我们不会在这里深入细节,但了解这些其他模式是很好的。它们就像工具箱中的不同工具。了解更多的工具使我们更好地构建软件。

在本章中,我们将通过示例介绍这些模式。我们将看到它们如何在真实软件中得到应用。这有助于我们更好地理解为什么这些模式很重要,以及如何稳妥地使用它们。

每个这样的模式都是朝着制作更好的软件迈出的一步。我们将学习如何使用 DDD、CQRS 和事件溯源。这些将帮助我们编写强大、智能和有用的软件,并解决真实业务问题。本章全部关于学习这些基本技能。

第三章,反应式 REST 开发和异步系统

第三章 打开了 Spring Boot 3.0 中反应式编程的动态世界。在这里,我们学习如何构建快速响应的软件。这是关于制作能够处理多个并发或同时请求的应用程序。

我们从反应式编程的介绍开始。这是一种全新的编写软件的方式。在以前,我们的应用程序一次只能做一件事。有了反应式编程,它们可以同时处理许多任务,流畅且无需等待。就像一个杂技演员轻松地同时抛接多个球一样。

构建反应式 REST API 是我们的下一个目标。将 REST API 想象成服务员接受订单并将食物端上桌:一个服务员,一个订单。一个反应式 REST API 就像一个超级服务员,即使在餐厅非常繁忙的时候也能同时处理多个订单。这对于你有很多用户,他们同时需要快速服务的情况非常棒。

接下来,我们探索异步系统和背压。异步意味着在不同的时间做事情,而不是严格的顺序。这就像有一个你可以按任何顺序完成任务的任务清单。背压是一种管理工作的方式,这样我们就不会感到不知所措。就像有一个聪明的系统,知道何时说“请稍等”,以确保一切都能正确完成,而不会崩溃或减慢速度。

第三章结束时,我们不仅会讨论这些想法,还会通过真实示例看到它们的应用。我们将了解为什么在当今快节奏的世界中,响应式编程是必不可少的。我们将学习如何使用这些新工具使我们的软件强大、智能和有帮助。我们还将看到它们如何解决当今商业中的实际问题。这一章充满了现代软件开发者必备的技能。

第四章,Spring 数据:SQL、NoSQL、缓存抽象和批量处理

第四章 将介绍如何在 Spring Boot 3.0 应用程序中管理数据。这是一章将理论与处理各种类型数据的实际步骤相结合的章节。

我们从 Spring Data 的介绍开始。这是 Spring Boot 最重要的组件之一。我们可以用它来编排数据。Spring Data 就像一座桥梁,连接着你的应用程序和数据库的世界。我们将看到 Spring Data 如何轻松地与数据库通信。

然后,我们将探讨 Spring Data 如何与 SQL 数据库连接。SQL 数据库将数据存储在表中,当你有清晰的数据结构时,它们非常出色。它们可靠且强大。使用 Spring Boot,使用这些数据库变得更容易。你可以设置关系并有效地存储你的数据。

接下来,我们将关注点转向 NoSQL 数据库。这些数据库与 SQL 数据库不同。它们更像是一个灵活的储藏室,你可以将数据放入其中,而不需要严格的布局。Spring Boot 支持各种 NoSQL 数据库,例如 MongoDB、Neo4j 和 Cassandra。当你的数据不适合整齐地放入表格中,你需要更多灵活性时,这些数据库非常出色。

我们还将讨论 Spring Boot 的缓存抽象。缓存是将数据的副本存储在临时存储区域中,以便你可以更快地访问它。这就像将你常用的工具放在工作台上以便快速访问一样。Spring 的缓存抽象让你可以智能地管理这种缓存,通过记住频繁使用的数据来提高应用程序的性能。

然后,我们将介绍 Spring Batch 的批量处理。这是当你需要一次性处理大量数据时使用的。想象一下,就像工厂的装配线,高效地处理大量任务。Spring Batch 是一个用于开发健壮批量应用的框架。它用于大规模数据迁移和处理,非常适合处理发送数千封电子邮件或处理大型数据集等大型工作。

最后,我们将讨论数据迁移和一致性。当你将数据从一个地方移动到另一个地方时,你想要确保在这个过程中没有任何东西丢失或改变。我们将学习在迁移过程中保持数据安全和一致性的策略。这就像搬家时没有丢失任何你的物品。

在本章中,我们将这些概念与实际示例联系起来,展示 Spring Boot 3.0 如何使这些任务变得更容易。到第四章结束时,你将了解如何在 Spring Boot 应用中管理和处理数据,确保它们快速、可靠和安全。

第五章,保护你的 Spring Boot 应用

在第五章中,我们将处理一些极其重要的事情——保护我们的 Spring Boot 应用。到目前为止,我们已经学到了很多好的实践。有了这些信息,我们已经构建了一个可维护、健壮的应用。所有部分都运行得非常顺畅。但现在,我们应该保持这个领域的安全。

首先,我们将深入探讨在 Spring Boot 3.0 的世界中什么是安全的。安全不仅仅是一个锦上添花的东西;它是一个必须品。我们将探讨 Spring Boot 如何帮助我们建立强大的防御来对抗黑客。

然后,是时候进入 Oauth 2.0 和 JWT 了。安全不仅仅重要,因为它可以防止攻击;它还确保每个用户的数据都是隔离的。它确保只有拥有正确通行证的正确的人才能进入。

接下来是角色基于访问控制。这关乎为谁可以在你的应用中走到哪里设定规则。就像决定谁可以得到前门钥匙,谁只能进入车库。

我们不会忘记关于响应式应用的内容。它们需要能够跟上它们快速节奏的安全措施。这有点像一位超级擅长多任务处理的保安。

Spring 安全过滤器就像是你的应用的保安。他们在让人进入之前会检查每个人。我们将学习如何设置这些过滤器来检查门口的身份证。

到了本章结束时,你将感觉自己像一位安全专家。你会知道如何使用所有这些工具来确保你的 Spring Boot 应用像堡垒一样安全。我们将通过示例和测试我们的安全措施,确保它是顶级的。所以,让我们做好准备,把我们的 Spring Boot 应用锁得严严实实!

第六章,高级测试策略

让我们深入到第六章,在那里我们将真正在 Spring Boot 中进行测试。测试不仅仅是一个需要勾选的复选框;它是确保我们的应用在现实面前不会崩溃的关键。在 Spring Boot 中,测试可以是一次相当刺激的旅程!

我们首先介绍测试领域的两大巨头:单元测试和集成测试。将单元测试想象成检查拼图的单个碎片,确保每个碎片都切割得恰到好处。集成测试呢?它关乎验证所有碎片是否能够组合在一起形成完整的画面。两者都极其重要,原因各不相同,我们将会看到原因所在。

接下来,我们将解决测试响应式组件的问题。如果你在 Spring Boot 中玩过响应式编程,你会知道它就像玩杂技一样——同时发生很多事情,你必须确保所有的事情都保持在空中。本节将确保你的响应式组件在压力之下不会掉链子。

然后,是安全测试的广阔而危险的世界。我们不仅要确保应用程序能正常工作,还要确保它像诺克斯堡一样坚不可摧。我们将深入探讨如何测试你的 Spring Boot 应用程序以抵御黑客,涵盖从谁被允许进入到谁被拒之门外的所有内容。

最后,我们将讨论 Spring Boot 世界的测试驱动开发TDD)。TDD 就像在烤蛋糕之前先写好食谱。这听起来可能有些反直觉,但它是一个变革者。我们首先编写测试,然后编写代码,最终得到的东西不仅美味,而且可靠。

到本章结束时,你不仅将了解在 Spring Boot 中测试的“如何”,还将了解“为什么”。这是确保你的应用程序不仅今天能工作,而且明天、下周和明年都能继续工作。准备好提升你的测试技能吧!

第七章,Spring Boot 3.0 的容器化和编排功能

第七章 中,我们将学习如何让我们的 Spring Boot 3.0 应用程序准备好旅行和工作在任何地方。这是关于使用像容器和编排器这样的酷工具。

首先,我们将讨论容器化的含义。这就像把你的应用程序打包在箱子里,让它能在任何电脑或服务器上运行,就像那样!

Spring Boot 有特殊的功能来帮助这一点。它拥有你确保应用程序在这些容器中打包得很好的所有东西。

然后,我们将深入了解 Spring Boot 与 Docker 的协同工作方式。Docker 就像为我们的应用程序提供的特殊公交车。它确保无论它们去哪里,都能平稳运行。

我们还将了解 Kubernetes。把它想象成公交车的老大。它组织我们所有的应用程序容器,并确保它们都能正确地协同工作。

最后,我们将探索 Spring Boot Actuator。这是我们应用程序的健康检查工具。它显示我们的应用程序在运行后表现如何。

到本章结束时,我们将能够打包我们的应用程序,让它们在任何我们喜欢的地方运行。我们将感觉自己像是我们应用程序的旅行代理人!

第八章,探索使用 Kafka 的事件驱动系统

第八章 将教会我们如何使用 Kafka 和 Spring Boot 应用程序构建事件驱动系统。这就像在我们的应用程序内设置一个强大的邮件服务,邮件永远不会消失。

首先,我们将了解事件驱动架构。这是一种构建应用程序的方式,其中不同的部分通过事件相互通信。就像应用程序的一部分向另一部分发送“嘿,发生了什么事!”的笔记一样。

接下来,我们将看到 Kafka 如何帮助我们的 Spring Boot 应用程序发送和接收这些笔记。Kafka 就像我们应用程序消息的邮局。它确保我们的应用程序的所有部分都能在正确的时间收到正确的消息。

然后,我们将实际使用 Spring Boot 构建一个事件驱动的应用程序。我们将使用 Spring Boot 的消息工具来确保我们的应用程序部分可以通过事件进行通信。

最后,我们将学习如何关注所有这些消息。我们将介绍如何监视我们的应用程序,并在出现问题时进行修复。这就像是一个侦探,寻找线索来解决任何消息谜团。

到了第八章结束时,我们将成为事件驱动的专家,准备好创建超级响应和最新的应用程序。

第九章,提高生产力和开发简化

第九章是我们真正深入探索 Spring Boot 提供的最酷工具的地方,所有这些工具都是为了使我们的开发者生活变得更加容易。

首先,我们有面向切面的编程,或AOP。它就像拥有一个魔杖,让我们可以整洁地收起所有重复的部分。因此,我们可以保持代码的整洁,专注于独特的东西。

然后,我们将轻松地通过 Feign 客户端浏览 HTTP API。它就像有一个翻译器,让我们的应用程序能够与其他应用程序聊天,而不需要任何麻烦。

我们还将掌握自动配置的艺术。这是 Spring Boot 给我们一个先发优势的方式,就像一辆车,当我们上车时,座椅和后视镜会自动调整到我们喜欢的位置。

我们以一些关于最佳实践和要避免的陷阱的实用建议结束。这是关于用我们的代码明智行事,向他人学习,并避免那些狡猾的陷阱。

当我们结束这一章时,我们将更聪明、更快地编码,并且信心满满。我们将成为像生产力忍者一样,轻松地穿越开发丛林。

摘要

这一章全是关于跳入 Spring Boot 3.0。将 Spring Boot 视为一个使 Java 工作变得容易得多的工具,尤其是在处理大型、复杂项目时。我们看到了它是如何帮助加快项目设置的速度,并简化处理大任务的流程。

下面是我们学到的:

  • 快速设置: Spring Boot 使启动新项目变得容易,允许开发者以最小的麻烦专注于开发有趣的部分

  • 微服务: 简单来说,这是一个将大项目(们)拆分成小部分的高级术语,这样事情就更容易管理

  • 用户友好: Spring Boot 的自动配置功能帮助开发者绕过手动设置过程

  • 丰富的工具: 它就像编程的瑞士军刀,提供了管理数据库和安全的工具

  • 云就绪: 与在云中运行的项目一起工作真是太棒了

  • 测试变得简单: 测试你的工作非常重要,Spring Boot 使它变得更简单

  • 社区和更新:有如此多的用户在致力于 Spring Boot 的开发,使其不断变得更好

从现在开始,在下一章中,我们将学习微服务架构、DDD、CQRS 和事件溯源。我们将了解微服务设计模式为何重要,以及如何为我们的项目选择正确的模式。

第二部分:架构模式和响应式编程

在这部分,我们将深入研究塑造现代软件开发创新的框架,重点关注架构模式和响应式编程。在第二章中,你将探索诸如领域驱动设计、命令查询责任分离和事件溯源等关键概念。然后,在第三章中,你将掌握响应式 REST 开发和异步系统的复杂性。这些章节旨在为你提供构建响应性和高效应用程序的技能。

本部分包含以下章节:

  • 第二章微服务中的关键架构模式 – DDD、CQRS 和事件溯源

  • 第三章响应式 REST 开发和异步系统

第二章:微服务中的关键架构模式——领域驱动设计(DDD)、命令查询责任分离(CQRS)和事件溯源

这章全部关于欣赏微服务的骨架——那些使我们的软件设计强大、可扩展和有效的中心模式。

首先,我们将深入探讨领域驱动设计的世界。这是一种将业务关注点适应任何给定软件项目的方法。它类似于确保我们的软件与它试图解决的商业挑战使用相同的语言。

接下来是命令查询责任分离。这是一种将我们如何操作数据分为两部分的不错方式——一个用于更新,另一个用于检索。它以更干净、更高效的方式划分了我们的软件职责。

接下来是事件溯源。我们在这里记录每个更改作为一个事件序列。它就像一个详细记录了所发生一切的清单——这对于回顾我们的数据和选择的历史可以非常强大。

随着我们进入本章,我们将了解架构模式为何重要,以及如何正确地应用它们来构建我们的微服务。我们将不仅学习什么是架构模式,还将学习它们的实际应用。在本章中,我们将涵盖以下主要主题:

  • 微服务架构模式简介

  • 领域驱动 设计DDD

  • 命令查询责任 分离CQRS

  • 事件溯源

  • 其他架构模式的简要概述

技术要求

为了更好地理解本章,以下领域的知识将有所帮助:

  • 对微服务架构原则的深入了解:掌握支撑微服务的基础概念

  • 熟悉软件设计模式:了解解决软件设计问题的常见模式

  • 基本编程概念:掌握编程的基本原则

  • 对分布式系统的理解:了解分布式系统的工作原理及其挑战

  • 微服务目的和实现的知识:了解微服务为何被使用,如何实现,以及它们的优点

  • 掌握微服务通信和操作:了解微服务如何相互通信并在更大的系统中操作

微服务架构模式简介

好的,所以在这个部分,我们将探讨如何将设计模式应用到微服务中。为了真正理解这个主题,让我们首先回顾一些子标题,以帮助构建整体图景。

我们为什么一开始就需要一个建筑设计呢?

简而言之,架构模式是经验证明的平衡解决方案,用于解决软件架构中的一些常见问题。这些模式从硬件限制到高可用性和最小化商业风险等问题都有所涉及。

一个显著的优势是,它们提供了解决软件问题的方法,因为大多数基本架构设计问题已经过测试。它们通过简化创建紧密连接和通信的模块的过程,这些模块以最小的耦合协同工作。这也通过根据实际需要调整结构,有助于使整个系统更容易理解和维护。

另一个巨大的优势是,设计模式有助于提高开发者和设计师之间沟通的有效性。当进行系统设计时,如果开发人员或设计师通过模式名称进行引用,那么每个人都能立即知道他们所讨论的一般高级设计。

设计模式是什么?

设计模式基本上是开发者用来解决软件设计中常见问题的模板。每个模式都展示了一种典型的解决方案,你可以根据自己项目的需求进行定制。例如,如果你经常需要以某种方式结构化程序,设计模式可以为你提供一个经过验证的方法,根据需要对其进行修改。

微服务是什么?

微服务是一种架构风格,你基本上是将应用程序拆分成许多小型、独立的微服务。每个服务专注于做一件特定的事情,并通过简单的协议与其他服务进行通信。最大的好处是团队可以在不影响应用程序其他部分的情况下,独立地工作在自己的服务上。

这种方法允许团队专注于他们的特定任务,而不必担心更改可能会对应用程序的其他部分产生什么影响。他们可以快速迭代代码和功能,而无需在整个代码库中进行广泛的协调和测试。如果做得正确,微服务可以使开发过程更高效,因为团队对自己的服务拥有自主权。

这种分离也使得应用程序更具有可扩展性和弹性。由于每个服务都是独立的,团队可以更新他们的代码并部署新版本,而不会影响其他服务。如果一个服务出现故障或需要离线进行维护,它不会使整个应用程序崩溃。公司能够在某些部分暂时不可用的情况下,保持软件的平稳运行。

此外,模块化设计意味着应用程序可以变得非常大,而不会变得过于混乱和复杂。新功能不需要对整个代码库进行更改。团队可以简单地构建额外的服务来处理新的功能。有了微服务,公司可以非常快速地构建软件,因为团队不会因为等待代码审查和部署而相互拖慢进度。

代价是保持服务分离需要更多前期工作。管理独立部分之间的通信有额外的复杂性。然而,对于大型应用程序,微服务通过实现快速、可靠的规模化开发,提供了超过初始成本的益处。

微服务的原则是什么?

微服务背后的六个主要原则是自主性、松耦合、重用、容错性、可组合性和可发现性。让我更详细地解释一下每一个:

  • 自主性意味着每个微服务都是独立的,并控制自己的运行时和数据库。这使得它更快、更可靠,因为它不依赖于其他服务。只要它保持无状态,它也可以轻松扩展。

  • 松耦合意味着服务之间依赖性不大。通过使用标准化的 API,一个服务可以改变而不影响其他服务。这允许有更多的灵活性和随时间的演变。它也使得开发和修复更快。

  • 重用仍然很重要,但在业务中更具体的应用领域。团队可以决定如何根据每个新的用例来调整服务。这种有指导的重用方法比僵化的预定模型更好。

  • 容错性意味着即使另一个服务失败,每个服务也可以继续工作。像断路器这样的东西可以阻止单个故障扩散。这保持了整个系统的可靠性。

  • 可组合性意味着服务可以以不同的组合方式提供价值。多个服务协同工作成为构建应用程序的新方式。

  • 可发现性意味着每个服务都清楚地传达它解决的业务问题和如何使用其技术接口。这使得开发者能够理解微服务的功能以及如何消费它发布的事件。

总结来说,这六个原则:自主性、松耦合、重用、容错性、可组合性和可发现性,构成了微服务架构的基础。

微服务设计模式

到目前为止,我们已经讨论了为什么我们需要架构设计,什么是设计模式以及微服务的原则。在本节中,我们的重点是微服务设计模式。也就是说,如前所述,设计模式以这种方式帮助解决微服务架构的特定挑战,并有助于降低微服务的失败风险——但前提是我们清楚地理解它们。

接下来的问题是那些设计模式究竟是什么。好吧,我想分享一个图表,它展示了微服务架构中设计模式的全貌。它并不涵盖所有设计模式,但涵盖了最常见的模式。

图 2.1:常见的微服务设计模式

图 2.1:常见的微服务设计模式

在前面的章节中,我们讨论了微服务架构以及在设计微服务时拥有良好定义模式的重要性。基于存在许多移动部件的事实,微服务可能会很快变得复杂;设计模式有助于处理一些特定问题,同时降低失败的风险。在本节中,我们想稍微深入探讨一些最常见且值得了解的微服务设计模式。

我们将讨论一些最常见的微服务设计模式,并对每个模式进行一些解释。

聚合设计模式

当你需要在一个页面或界面上显示来自多个微服务的多个数据时,聚合模式非常有用。例如,如果你有一个仪表板,它从不同的服务中拉取各种指标和状态,聚合模式允许你高效地将这些数据收集在一个地方。

API 网关设计模式

API 网关充当微服务的单一入口点或“前门”。所有请求都必须通过 API 网关,它处理身份验证、授权、监控并将请求路由到适当的服务。与直接暴露服务相比,这提供了一层额外的安全保护。

Saga 设计模式

当你的业务流程涉及多个服务,并且步骤必须以事务方式执行时, Saga 模式非常有用。例如,当在社交资料上发布照片时,Saga 模式可以协调以可靠的方式保存照片、更新资料和通知关注者,即使某些服务失败。

总结来说,如今有如此多的不同微服务设计模式。决定哪些可能最适合你的特定项目可能会很困难。但我发现,很多时候,你可以根据你试图达成的目标同时使用几个不同的模式。

例如,假设你有一个包含多个独立服务的项目,所有这些服务都需要访问同一个数据库。在这种情况下,使用网关模式来访问数据库可能是有意义的,这样可以避免每个服务都直接连接。这样,你可以通过单个服务来整合数据库连接。

然而,同时,你的某些服务可能在与它们之间的客户端/服务器模式中工作得非常好。也许一个服务作为服务器向其他作为客户端的服务提供数据。因此,在这个架构部分,客户端/服务器可能是一个很好的选择。

重要的是要考虑每个单独的服务或服务组的目标和需求。哪些模式能帮助你实现松散耦合、可扩展性、容错性等目标?只要你能清楚地解释你为什么选择了这些模式,以及它们如何帮助解决特定的目标,那么在一个项目中使用多个模式是完全合理的。模式是为了服务于你的设计——而不是反过来。

在讨论了各种微服务设计模式和它们在特定架构挑战中的应用之后,我们将进入下一部分。在这里,我们将探索 DDD,这将帮助我们了解每个微服务如何负责特定领域的操作。

探索 DDD

我们将以一种易于理解的方式为你分解 DDD。现在,如果你之前从未听说过 DDD,请不要担心——它主要用于需要六个月或更长时间才能完成的大型项目。但即使你只是做些小项目,学习基础知识仍然是有帮助的。

DDD 的核心是围绕你的软件试图解决的特定问题或“领域”来结构化你的代码。用更简单的话说,就是组织你的代码以匹配你的应用程序实际上关注的内容。

首先,我们将讨论一些基本术语,如领域和 DDD,然后稍后我们将探讨如何实现 DDD。最后,我们将讨论一个现实世界的例子。

什么是领域?

领域指的是你的应用程序关注的主题或领域。例如,如果你正在构建一个订单应用程序,领域可能很可能是在线购物或订单处理。真正理解领域也很重要,因为一家公司可能同时在工作多个领域,如购物、配送、运输、维修——你懂的。

有时候一个领域可能看起来太宽泛,例如,“食品”就是一个例子。在这些情况下,你应该指定你正在处理的该行业的确切部分。现在对于非常大的领域模型,你可以将它们分解成更小的边界上下文,以便更容易管理。例如,在一家食品公司内部,可能有针对销售团队和配送团队的单独上下文,每个上下文都有自己的专家。

这些领域专家与开发者紧密合作,确保功能的实现。将领域划分为边界上下文简化了工作并保持了组织性。

总结来说,DDD 是一种通过关注领域和上下文来开发复杂软件的方法,确保你的代码与你要解决的问题的具体性相匹配。

什么是 DDD?

现在我们将讨论 DDD。DDD 的核心是将你的代码与业务领域的核心概念或上下文深度链接。目标是帮助通过促进领域专家和开发者之间的有效协作来处理复杂场景,这样误解的空间就会更小。

DDD 在具有许多动态部分的大项目中特别出色,在这些项目中,你需要专家的意见,并且每个人都必须共同努力。但对于你可以独立管理的小型个人项目来说,可能就有些过度了。成功协作的关键是沟通。使用 DDD,开发人员和专家(如架构和领域专家)在讨论事情、构建领域模型和编写代码时共享一种共同的语言。这有助于加快反馈循环。

但你必须小心。如果你不持续丰富和定义这种共享语言,团队内部可能会开始形成不同的语言。然后,有效的沟通就会告吹——这会导致不准确和混淆。例如,“客户端”一词在一个上下文中可能指用户,而在另一个上下文中可能指系统服务。因此,清楚地定义每一件事的含义非常重要。

此外,每个领域都应该有自己的定制语言以避免冲突。我们还将想要在领域之间建立边界,以防止交叉污染。保护领域的一种方法是通过 反腐败层。这一层充当不同领域模型之间的翻译者,使用适配器、外观或翻译者等模式来帮助领域之间进行通信而不相互污染。这有助于解释 DDD 是什么。

如何定义 DDD 结构?

在本节中,我们将分解一个在线购物应用程序的 DDD 结构示例,我们将在本节稍后讨论。我们需要关注几个关键层以确保我们的应用程序顺利运行:

  • 首先是 UI 层。这是当客户在手机或电脑上浏览时看到的。它显示产品,并允许他们添加商品到购物车并结账。它接收用户输入并将其发送到下一层。

  • 下一个层是 应用层。现在这一层没有实际的业务逻辑,它只是引导用户通过 UI 流程并与其他系统进行通信。它组织所有对象并确保任务以正确的顺序完成。

  • 现在我们来到 领域层,这是整个操作的灵魂和核心。这一层拥有使业务运转的核心概念。它包括用户、产品、订单等与应用程序主要功能相关的一切。每个实体都有自己的唯一标识符,这样无论其他什么发生变化,都可以追踪。

    这里的服务也有预定义的行为,每个人都能够理解。领域层独立存在,不依赖于其他层,但它们都可以依赖于领域层,因为领域层已经锁定了所有重要的业务规则。

  • 最后,我们还有基础设施层。这一层促进了所有其他层之间的通信。它还提供诸如库之类的工具,以帮助 UI 顺利工作。但事实上,它没有任何业务逻辑——它只是支持幕后技术功能。

因此,总的来说——UI 层与用户交流,应用层管理任务,领域层处理核心业务功能,基础设施层帮助它们无缝协作。确保每一层都专注于你的应用架构。

图 2.2帮助你从视觉上理解不同层之间的关系:

图 2.2:DDD 层之间的关系

图 2.2:DDD 层之间的关系

现在,我们将通过遵循 DDD 结构来通过一个示例来了解它,使其在我们心中清晰易懂——我们将通过一个在线商店的真实世界示例来分解微服务。

我们都知道现在网上购物非常流行,所以让我来介绍一下如何使用领域驱动设计来构建一个电子商务网站:

  • 首先,你需要有一个用户服务。这个服务处理所有账户详情——登录、个人资料、地址、支付信息以及所有相关细节。

  • 接下来是产品服务。这个服务负责产品目录——跟踪库存水平、产品详情、描述——你可以购买的所有物品的信息。

  • 然后我们有订单服务。正如你可能猜到的,这个服务在人们结账时创建订单。它还使用不同的方式处理支付,例如信用卡或 PayPal。当然,它还会将订单发送给客户。

  • 接下来是支付服务。现在这个服务专门关注处理来自不同来源的支付,例如 Visa、Mastercard 或数字钱包。它与支付网关进行交互。

  • 评论服务管理所有客户留下的评论、评分和反馈——人们在购买之前应该能看到其他人对这个产品的看法,对吧?

  • 最后,我们有通知服务。这个服务会发送电子邮件或推送通知,让客户了解他们的订单、销售、新产品——你叫什么名字——让每个人都能跟上进度!

图 2.3展示了按照 DDD 方法这些微服务:

图 2.3:项目在 DDD 结构中的服务

图 2.3:项目在 DDD 结构中的服务

因此,总的来说,这些是按照 DDD 方法可能使用的在线商店的主要微服务。

我们现在对 DDD 方法有了很好的理解。我们按业务领域划分微服务。接下来,我们将探索 CQRS——正如你可以从其名称中理解的那样,我们再次将微服务划分,但方式不同。

了解 CQRS

在本节中,我们将讨论 CQRS,这是一个出色的软件架构模式!CQRS 基于在系统中分离命令和查询的责任。这意味着我们垂直切割我们的应用程序逻辑。

通过分离命令和查询,我们的系统变得非常高效。命令专注于数据变更,而不必担心查询。查询只关注读取数据,而不影响命令。系统的每个部分都针对其单一目的进行优化。这就像分割并征服,使一切更快!

在本节中,我们将讨论 CQRS 的背景,包括其优点和缺点。稍后,我们将看到一个真实场景以及 CQRS 如何帮助解决实际问题。

CQRS 的背景是什么?

你可能会想,“我为什么要关心 CQRS?”简单的答案是效率和简单性。当我们把应用程序分成命令和查询部分时,我们可以轻松地分别优化每一部分。

让我们谈谈这两个主要组件:

  • 命令端全部关于动作——创建、更新和删除数据。正如我们所见,这些都是我们应用程序的“执行者”操作。

  • 另一方面,查询端是“查看者”或“读者”。它获取数据,但不改变数据的状态。

因此,有了这样的清晰分离,你可以看到根据它们的需求优化和扩展命令和查询是多么容易。

然而,我们必须记住,CQRS 并不是万能的解决方案。如果我们的系统中的其他所有架构设计都有不平衡的命令和查询操作,那么我们应该选择这种设计。否则,它将增加我们应用程序的复杂性。

总结来说,CQRS 全部关于将你的应用程序分成两部分——一部分用于命令(执行)和另一部分用于查询(查看)。这种分离可以导致更高效、可维护和可扩展的应用程序。但请记住,评估 CQRS 是否符合你项目的需求是至关重要的。现在,在下一节中,我们将看到在解决方案中实施 CQRS 时的最佳实践和常见错误。

最佳实践和常见陷阱是什么?

与所有其他架构设计模式一样,在实施此架构设计时,我们需要遵循一些最佳实践,并考虑一些常见的错误。

让我们从最佳实践开始:

  • 从简单开始:从简单的方法开始。你不需要一开始就把你应用程序的每一部分都分成命令和查询。你仍然处于微服务领域,并且可以单独确定每个服务的需求。

  • 保持沟通清晰:确保命令和查询端之间的沟通定义良好。这与下一项有关,因为如果你设计好数据库,你可以在命令和查询端之间建立清晰的沟通。

  • 优化数据库设计:设计数据库以适应 CQRS 的分割特性。请仔细考虑这个设计,这不仅仅是创建一个表——在这个数据库设计中,代码的一侧插入数据,另一侧代码将查看它。你需要比其他设计模式更加关注这个数据库设计。

  • 定期测试和改进:持续测试和改进你的实现。这是所有实现不可避免的一步。只有当我们测试了它们,我们才能对我们的设计感到舒适。

在实施 CQRS 时,有一些错误是我们想要避免的:

  • 过度复杂化:你需要确保你的系统是可管理的,并且正在做它需要做的事情,不多也不少。

  • 误判规模:在一个实际上并不需要 CQRS 的系统中实施 CQRS 就像用 18 轮卡车去杂货店一样。在评估你的应用程序是否真正从 CQRS 中受益时,你必须非常小心。

  • 忽视业务逻辑分离:你必须严格区分我们的命令和查询责任。你需要检查我们创建的每个 pull request 和每次代码审查,因为如果它们被混淆,应用程序可能会很快变成垃圾。

  • 低估学习曲线:你必须认识到 CQRS 对你的团队来说需要一段学习曲线。你需要学习工具,让新加入的人能够了解我们的系统。

通过遵循这些最佳实践并避免常见陷阱,你可以使你的 CQRS 实现成功。这关乎找到那个甜蜜点,使你的系统既高效又不过度复杂化。记住,目标是创建一个像经过精心调校的汽车一样平滑高效的系统,随时准备带你去你需要去的地方。

CQRS 设计模式的优点是什么?

使用 CQRS,你将获得一些非常棒的功能,包括以下内容:

  • 独立扩展 – CQRS 允许读取和写入工作负载分别扩展,这意味着更少的减速。

  • 优化模式 – 读取端可以有一个针对查询完美优化的模式,而写入端则专注于更新。使用 CQRS,你可以通过添加更多实例或资源来扩展命令端(写入操作),而不必增加查询端的负载,同时写入端专注于更新。

  • 安全性 – 确保只有正确的人对数据进行写入要容易得多。

  • 关注点分离 – 分割读取和写入意味着模型更容易维护和适应。大多数复杂业务逻辑都放在写入模型中,而读取则是简单而甜蜜的。

将这些模式结合起来,在增加复杂实现成本的同时最大化性能。但它确保你的领域模型和数据能够适应任何未来的变化!

到目前为止,我们已经学习了 CQRS,它的好处、最佳实践和常见陷阱。但 CQRS 有一个孪生兄弟,它们通常一起使用——即事件溯源。在下一节中,我们将学习事件溯源的核心概念以及它是如何与 CQRS 协同工作的。

理解事件溯源

在本节中,我们将讨论事件溯源。我们还将检查事件驱动架构EDA),并分析 EDA 与事件溯源之间的区别。此外,我们之前提到了 CQRS,但在这个章节中,我们将学习 CQRS 在整个架构中的位置,我将用简单易懂的方式解释这一切。

事件驱动架构

在本节中,我们将分解 EDA 中的几个关键概念。图 2.4展示了该架构中事件和命令的一些基本示例。

图 2.4:事件和命令的示例

图 2.4:事件和命令的示例

首先,我们有事件。事件基本上是发生的事情——例如用户登录或订单被下。然后我们有命令。命令就像订单或请求,告诉其他事物去做某事。

事件可以以事件通知的形式进行通信,命令以消息的形式。它们非常相似——它们都包含信息。有时事件通知也被称为消息。在实践中,人们通常只称它们为事件。但从技术上讲,事件是发生的事情,而不是关于它的通知。事件与命令不同,命令更多地关于意图。但在这个章节中,我们将简单地称它们为事件以保持简单。这些事件可以包含关于发生的事情的数据,或者只是通知。它们是不可变的,这意味着一旦创建就不能更改。EDA 围绕这些事件展开。关于它是否只是事件或包括其他消息也存在一些争议。但到目前为止,只需关注通过系统流动的事件即可。

好的,所以在 EDA 中,通常有三个主要组件,如上图所示:

图 2.5:EDA 组件

图 2.5:EDA 组件

首先,生产者创建事件。然后代理将事件重定向到正确的消费者,消费者根据事件采取相应的行动。

什么是事件溯源?

在本节中,我们将讨论事件溯源以及它如何有助于跟踪应用程序中的更改。基本上,除了保存最终状态外,您还可以将每个发生的变化记录为事件。这些事件存储在称为事件日志的东西中,它们按顺序排列,以便您可以查看完整的历史记录。通过从头到尾阅读事件日志,您可以实际上重建应用程序的整个状态!这就是人们所说的事件溯源

让我们现在通过一个示例事件日志架构,然后简要谈谈事件溯源的主要功能之一——并行处理。

事件日志模式示例

再次以电子商务商店为例。假设您想跟踪所有产品的库存水平。嗯,您可以有ProductAdded事件,该事件存储产品 ID 和每次补货时添加了多少产品。当有人购买东西时,您会记录一个带有 ID 和数量的ProductPurchased事件。通过按顺序重新播放所有这些事件,您可以始终知道当前的库存水平,无论发生什么。事件溯源对于需要随时间变化的全部变更审计跟踪的任何事物都非常有用。如果您希望您的应用具有那种类型的历史数据,这绝对值得一试。

图 2.6展示了事件日志如何排序库存。

图 2.6:事件日志——详细示例

图 2.6:事件日志——详细示例

现在让我快速为您分解一下。首先,我们在我们的库存中添加一些产品——比如说我们添加了 10 个产品。后来,一些客户开始购买这些产品。我们得到了一个关于一个产品的购买事件,然后又得到了另一个关于另一个产品的购买事件。现在这里有个有趣的部分——我们可以在任何时间查看我们的事件日志,并找出我们当前的库存是什么!我们知道我们开始时有 10 个产品。然后我们有两个购买事件,这意味着我们现在必须有 8 个产品了。

事件溯源的关键之处在于我们可以在任何时候将库存重置为零。我们可以删除我们有多少产品。但只要我们有那个事件日志,我们就可以随时回去重新计算我们应该有多少产品,基于所有的事件!

总结来说,事件溯源使用事件日志来跟踪所有发生的事情,这样您就可以随时回到过去,查看数据在任何时刻的样子,并检查数据的先前状态。例如,假设在第一天您向库存中添加了 10 个产品。第二天,有人购买了一个产品。第三天,另一个人又购买了一个产品。使用事件溯源,您可以回顾并确切地看到第二天或甚至第一天库存的样子。这对于调试或在其他地方复制数据非常有用。您所要做的就是重新播放事件日志——无需手动从头开始设置一切。

并行处理

并行处理是事件溯源的一个非常有用的功能,当您有多个应用或服务从相同的数据源读取时。而不是每个应用都必须等待它们的轮次来读取东西,它们可以并行地同时读取。如果您有远多于写入者的读取者,这非常完美。

假设您有一个跟踪您系统中所有发生的事件的日志。而不是一个应用读取日志然后下一个应用读取,它们可以同时读取,如下所示:

图 2.7:并行处理功能

图 2.7:并行处理功能

只要日志中的数据不发生变化,就可以进行大量的并行读取,没有问题。这非常关键,因为它意味着每个应用程序都可以独立地从日志中获取它所需的内容,并执行自己的处理,而不会干扰其他应用程序。它们可以并行处理。只要日志只添加新事件而不修改旧事件,一切都会保持一致。

因此,并行读取是最大化吞吐量和充分利用所有资源的一种很好的方法。

事件驱动架构和 Event Sourcing 之间的区别

EDA 是关于组件通过事件进行通信的。当系统中发生重要事件时,它会发出一个事件。其他组件可以订阅这些事件并相应地做出反应。这种松散耦合使得 EDA 对于可扩展性和实时响应性非常出色。你在微服务、消息系统和物联网应用程序中经常看到它。

在 Event Sourcing 中,除了存储当前数据状态外,它还存储了随时间变化而改变数据的所有事件的日志。因此,当前状态是通过重新播放所有这些过去的事件来重建的。这对于审计、拥有不同的版本和分析历史数据非常有用。Event Sourcing 还与 CQRS(将读取和写入分离)很好地协同工作。写入端存储事件,而读取端针对查询进行了优化。

虽然 EDA 和 Event Sourcing 都涉及事件,但它们关注的重点不同。EDA 更多的是关于组件如何通过事件进行通信。Event Sourcing 是关于将事件日志持久化以表示随时间变化的状态变化,从而受益于包括审计和版本在内的东西。你可以将 Event Sourcing 作为事件驱动系统的一部分,但它们各自解决自己的问题。

Event Sourcing 模式的真实世界示例

在 Event Sourcing 中,每一个变化都是按顺序记录下来的,这样我们就可以始终回顾并看到事物是如何随着时间线而变化的。我们可以通过一个名叫莎拉的人的故事来更好地理解这一点,以及她如何与一个在线平台互动。她的活动,比如发布状态或添加联系信息,都被记录为一系列事件。每个事件都是交易的一部分,并有一个唯一的序列 ID,确保每个变化都能追踪到时间顺序。在我们的下一个示例中,每个事件都保持为 JavaScript 对象表示法JSON)格式。

这里是莎拉操作创建的事件列表:

  1. 事件 1 – 账户创建 – 旅程始于莎拉创建她的账户:

      {
        "event": "Account Created",
        "transactionId": "tx200",
        "sequenceId": 1,
        "date": "2023-03-01",
        "userId": "user123",
        "details": {
          "name": "Sarah",
          "email": "sarah@example.com"
        }
      }
    
  2. 事件 2 – 电子邮件更新 – 稍后,莎拉更新了她的电子邮件地址:

      {
        "event": "Email Updated",
        "transactionId": "tx200",
        "sequenceId": 2,
        "date": "2023-03-05",
        "userId": "user123",
        "details": {
          "newEmail": "s.new@example.com"
        }
      }
    
  3. 事件 3 – 添加邮寄地址 – 然后,莎拉在她的个人资料中添加了一个邮寄地址:

      {
        "event": "Address Added",
        "transactionId": "tx200",
        "sequenceId": 3,
        "date": "2023-03-10",
        "userId": "user123",
        "details": {
          "address": "123 Main St, Anytown, USA"
        }
      }
    
  4. 事件 4 – 更新姓名 – 之后,她决定更新个人资料中的姓名:

      {
        "event": "Name Updated",
        "transactionId": "tx200",
        "sequenceId": 4,
        "date": "2023-03-15",
        "userId": "user123",
        "details": {
          "name": "Sarah N."
        }
      }
    
  5. 事件 5 – 添加电话号码 – 最后,莎拉添加了她的电话号码:

      {
        "event": "Phone Number Added",
        "transactionId": "tx200",
        "sequenceId": 5,
        "date": "2023-03-20",
        "userId": "user123",
        "details": {
          "phoneNumber": "555-1234"
        }
      }
    

当我们按照事件记录的顺序处理这些事件时,我们能够重建 Sarah 的个人资料在任何时间点的当前状态。每个事件都是不可变的,所以一旦事件被记录,就不能更改。这个账户提供了关于 Sarah 个人资料随时间变化的非常清晰和全面的历史记录。

总之,事件溯源提供了一个非常强大的框架,用于记录和管理系统状态的变化。它在需要详细审计跟踪和历史数据分析的场景中表现出色。事件溯源将每个更改记录为一个独立的事件,提供了数据演化的强大概览,使系统不仅能够展示当前状态,还能够回顾和分析过去的状态。因此,这是一种对复杂系统非常有价值的方法,其中理解数据的旅程与系统本身一样重要。

事件溯源与 CQRS 的关系

我们在 事件溯源 部分的开头提到,我们将发现 CQRS 如何融入更大的图景,所以现在让我们结合在前几节中学到的所有概念。好的!基本上,在 CQRS 中,你写入数据的方式与读取数据的方式不同。在过去,你可能只是使用一个数据库来处理所有事情。你可以执行诸如插入数据然后立即从同一位置获取数据等操作,如果你只是进行基本的创建、读取、更新和删除操作,这工作得很好。但有时你可能想分别扩展写入和读取的量。或者你可能需要为读取和写入提供不同的数据视图。这就是 CQRS 发挥作用的地方。

图 2**.8 展示了命令和查询如何与数据库交互:

图 2.8:CQRS 模式的表示

图 2.8:CQRS 模式的表示

基本的想法是,你将数据写入的方式与查询或获取数据的方式分开。因此,你可以写入一个针对插入速度优化的数据库,然后设置一个单独的数据库,专门用于读取,在那里你可以结合来自其他服务的多个数据源,以提供定制的信息视图。将写入和读取分开,使你能够根据具体需求独立扩展。它还允许你以不同的方式对数据进行转换,以适应不同的用途。

因此,假设我们有一些服务——我们有一个支付服务、运输服务和订单服务。每个服务都在使用事件日志跟踪其流程部分的情况。

支付服务知道哪些支付成功,哪些失败。运输服务知道所有交付的位置。订单服务拥有每个用户的完整订单历史。

现在我们想要创建一个页面,让用户在一个地方查看他们所有的历史订单。嗯,为了做到这一点,我们的订单回顾功能将使用所有三个服务来在一个地方显示所有历史订单。它将向支付服务请求支付信息,向运输服务请求运输状态,以及向订单服务请求基本订单详情。

图 2.9:放大服务端

图 2.9:放大服务端

然后,它将所有这些数据合并成一个整洁的页面,让用户能够在一个地方看到他们订单历史的全貌。即使每个服务独立使用事件处理自己的部分,我们仍然可以将它们全部结合起来,为用户提供一个统一的视图!

使用 CQRS 的主要好处是它允许你在不同的系统中分离读取和写入。这允许你独立扩展它们。如果你有更多的读取而不是写入,或者相反,你可以独立扩展这些部分。你也可以有不同的逻辑——你可以在写入时进行额外的处理,或者在你读取时添加额外的层或功能。你也可以从不同的系统中获取信息。

最后,让我们看看事件溯源和 CQRS 模式在更大图景中是如何协同工作的:

图 2.10:带有 CQRS 的事件溯源模式

图 2.10:带有 CQRS 的事件溯源模式

让我们一起通过这个图表来了解一下:

  • UI: 用户界面

    首先,用户界面就像我们办公室的前台。这是您,用户,进行交互的地方。您可以下达指令(命令)或请求信息(查询)。

  • 命令: 完成任务

    通过用户界面下达命令就像发送请求去完成某事——比如添加一个新文件或更新一个现有文件。这个命令被系统接收,执行其魔法,并更新我们的“写入”数据库,我们在那里跟踪所有这些变化。

  • 查询: 请求信息

    现在如果你感兴趣的是信息,那就是一个查询。这就像在办公室里请求某人帮你拿出一个文件。这个信息是从专门为读取目的设计的另一个数据库中提取的。它更快、更干净,有点像每次都准备好一套文件供查看。

  • 事件溯源: 详细记录

    现在,到了有趣的部分。在事件溯源中,系统会保留每个单独变化的详细日志,而不是只保留最新的更新——更像是每个单独动作的日记条目。每个这样的动作,添加或移除某些东西,都被记录为一个事件。

  • 事件存储或日志: 系统的记忆

    日志或事件存储实际上是系统的记忆。它保存了所有这些事件记录。如果需要,可以滚动查看这个日志,查看其完整的历史变化,甚至可以回放它们来了解系统是如何到达当前状态的。

  • 发布事件:传播新闻

    在成功处理命令后,系统不仅更新数据库,还广播一个事件。它就像是对全世界喊出刚刚发生了什么变化的公告。

  • 事件处理器或总线:信使

    事件总线或处理器与办公室信使没有区别。它捕获事件并将消息传递到所有相关的地方,以便读取数据库更新为最新信息。

  • 数据库(读取):优化快速访问

    读取数据库的存在是为了使信息访问快速且容易。它与写入数据库不同,并且被设置成能够让您迅速且毫不犹豫地获取信息。

  • 物化视图:准备就绪的信息

    最后,我们带来了物化视图。它是数据的快照,本质上已经准备并优化了您的查询。把它想象成一个总结报告,随时可用。

图 2.10 展示了一个智能且高效的系统,其中任务被巧妙地分配给所有其他便于管理和执行对数据仓库查询以及数据仓库本身的资源。在一侧,您有改变事物的命令,在另一侧,您有获取信息的查询。在事件源中,每个变化的完整历史都有记录。它就像是一个运行顺畅、高效、有组织且透明的公司办公室工作流程,在这个过程中没有任何东西被放置不当或丢失。

事件源模式下的 CQRS 的真实世界示例

在本节中,我将提供一个使用 CQRS 和事件源的真实世界银行系统示例图。我们将关注银行账户方面。以一个涉及开设账户、存钱、处理交易和关闭账户的银行为例。为了充分执行这些任务,银行实施了一个基于 CQRS 和事件源的系统。

以下图显示了系统的工作方式:

图 2.11:使用事件源模式的 CQRS 模式的银行示例

图 2.11:使用事件源模式的 CQRS 模式的银行示例

让我们用简单的话来分解这个图:

  • OpenAccountCommand就像走进银行并说,“我想开设一个新账户。”这些命令是银行账户命令 API的一部分,这只是说这个系统理解并处理您要求银行做的事情的一种花哨的说法。

  • 命令处理器:这可以被认为是银行员工,他们接受您的请求并启动流程。处理器确保执行您命令所需的所有步骤。

  • 事件存储:用户所做的每一件事(例如开设账户)都会以事件的形式存储在一个称为事件存储的特殊数据库中。它就像一本详尽的日记,记录了所有发生的事情。

  • 事件发布者:这是银行中的一个扩音器,它会告诉每个人刚刚发生了什么。例如,当你开设账户时,事件发布者就会告诉整个系统,“嘿,一个新的账户被开设了!”

  • Apache Kafka 消息队列代理:Apache Kafka 就像银行的邮件系统,确保消息(事件)被发送到正确的部门。它非常高效,即使银行非常繁忙,每一条消息都能被传递。

  • 事件消费者:这个系统组件是一个监听器。它监听任何与之相关的公告(事件)。

  • 事件处理器:如果事件消费者就像是在听公告的员工,那么事件处理器就是那个实际接收信息并相应更新银行记录的员工。

  • FindAllAccountsQueries

  • 查询处理器:这与接受你的查询并为你查找信息的客户服务代表非常相似。

  • 读取数据库:这是一个独立的数据库,银行在这里保存可以读取的信息——例如,你的账户余额,甚至所有账户的列表。它组织得很好,任何形式的信息都可以轻松快速地获取。

上述图表基本上只是详细说明了银行系统中两个不同的过程——一个是执行事务(例如,开设账户的过程)另一个是查询事务(例如,查询你账户中的金额)。它们协同工作但又是独立的,使得整个银行系统运行得既顺畅又高效。执行部分将发生的一切记录为事件并记住所有信息,而查询部分则使用简化的数据库来快速回答你的问题。

到目前为止,我们已经了解了一些在微服务架构中常用的架构设计。在下一节中,我们将简要讨论一些其他架构设计。

其他架构模式的简要概述

在本节中,我们将提到一些额外的架构模式。使用模式有助于提高开发效率和生产率。它还有助于优化成本和提高规划——基本上,它使一切变得更简单。

有许多不同的企业模式可供选择。为了帮助您为项目选择正确的模式,我已经汇总了一些模式的摘要。

面向服务的架构(SOA)设计模式

面向服务的架构模式有点像用乐高积木构建软件。你将整个程序分解成更小的可重用组件,称为服务

每个服务都有自己的特定任务。它可以独立工作,不需要整个程序。但这些服务会互相交流,以完成所有工作。

这有点像如果你有一个大项目,然后把它分成不同的部分供不同的人处理。每个人只关注他们自己的部分,而不担心其他人正在做什么。最后,所有部分都会汇集在一起。

这使得软件更加灵活,并且以后更容易更改。就像乐高积木一样,你可以使用相同的部件来构建很多不同的东西,而不必总是从头开始。交换东西更容易。

图 2.12中,我们可以看到 SOA 的一般设计:

图 2.12:SOA 设计模式的示例

图 2.12:SOA 设计模式的示例

如我们所见,在服务和用户之间有一个事件服务总线(ESB)。它将任务在服务之间进行委派。在服务和数据库系统之间有一个共享层。通过这一层,所有服务都可以访问它们所需的所有数据,即使这些数据不是由该服务创建的。

因此,总的来说,这个模式将程序分解成更小的通信服务,就像将项目分解成部分或使用可重复使用的积木来构建一样。随着时间的推移,这使得软件更容易使用。

电路断路器模式

电路断路器模式完全是关于构建能够处理问题而不会崩溃的软件。代码中的断路器与电路中的断路器工作方式相似。如今,许多程序都依赖于许多不同的部分在网络中协同工作,对吧?但有时其中一个部分可能会崩溃。使用电路断路器模式,你的程序会持续检查不同的部分是否正在正常通信。如果它注意到同一个部分反复出现错误,它将暂时阻止对该部分的任何更多请求。这可以防止整个系统因为一个小故障而超负荷运行。而不是让一切停滞不前,程序可以在那个问题部分整理好之前继续运行。对于创建既能弯曲又不会断裂的软件来说,这非常方便!

为了更好地理解这个模式,请参阅图 2.13

图 2.13:电路断路器模式的示例

图 2.13:电路断路器模式的示例

在电路断路器模式中,如我们的图所示,服务 A是负责处理用户请求的主要服务,并有一些关键内部组件,如健康检查器请求处理器,负责监控系统的健康状态以及它与用户的交互。这个服务 A由关键玩家电路断路器所中介,它监视失败的请求,一旦达到一定数量,就会像真正的电路断路器一样跳闸,停止任何进一步的请求,以防止系统过载,从而允许恢复时间。

这种机制不仅旨在保护服务 A,而且也是一种保护外部依赖(如服务 B和数据库)免受压垮的方法。这是一个安全措施,有助于使系统稳定,从而防止构成微服务架构的其他服务像多米诺骨牌一样倒塌。

分层设计模式

分层设计模式是程序员以有组织的方式结构化代码的一种方法。基本上,它所做的就是将软件分解成不同的层级,每个层级都有自己的特定任务。

层级是层层叠放的,较低层级为较高层级提供服务。因此,最底层的重点将放在数据访问或硬件接口等方面。然后,下一层级可以使用这些较低层级的服务来完成诸如业务逻辑等任务,而顶层则更多地关注接口,例如用户界面。

通过这种方式将一切分开,使得代码更容易管理和维护。程序员可以在单个层级上工作,而不必过多担心其他部分。由于每个层级都有一个明确的目的,因此代码的重用也更容易。如果你需要更新数据的存储方式,你只需更改底层而不是在整个程序中挖掘。

总体而言,它促进了一种逻辑结构,其中每个新构建的层级都依赖于下面的工作。这种分层设置有助于将大型和复杂的软件系统组织成可理解和可管理的块。

在下面的图 2.14中,我们可以看到分层设计模式的整体样子:

图 2.14:分层设计模式的示例

图 2.14:分层设计模式的示例

如图中所示,每一层都是相互隔离的,它们不能跳过一层直接与另一层通信。因此,通过这种设计,你可以将用户交互保持在控制器层,执行业务逻辑在应用层,并将数据库操作保持在数据层。

MVC 设计模式

MVC代表模型-视图-控制器,基本上将一切分为三个部分。这是构建 Web 和移动应用的一种非常常见的设计模式!

模型是存储所有重要内容的地方,例如用户账户、帖子、产品——你叫什么名字!这是应用程序的核心数据和逻辑。

视图是用户在屏幕上看到的内容——例如 HTML、CSS,如果是单页应用,可能还有模板。它渲染模型数据,以便用户可以查看。

然后我们有了控制器!它处理用户所做的所有操作,包括点击、表单和 API 调用。当发生某些事情时,控制器会找出原因,如果需要,更新模型,并告诉视图进行更改。

通过这样分离代码,随着时间的推移,代码保持超级干净和有序。你可以调整一部分而不太影响其他部分。此外,它还提供了灵活性,可以重新使用或替换其中的部分。

图 2.15 展示了一个 MVC 设计的示例,接下来我们将讨论请求如何返回视图:

图 2.15:MVC 设计模式的示例

图 2.15:MVC 设计模式的示例

在前面的图中,我们可以看到一个与之前分层架构设计模式非常相似的图。然而,在这里,我们没有应用层。业务逻辑在控制器和模型层之间划分。当用户发起请求,并在应用业务逻辑之后,控制器返回一个视图,这可能是一个网页或 JSON 对象。

总结来说,MVC 是构建用户界面的最佳模式,它使应用程序开发变得非常容易,并且可以长期维护和有序。

Saga 设计模式

Saga 设计模式对于构建分布式系统来说非常酷!它有助于管理长事务,并保持不同服务之间的数据一致性。当你在多个服务中分散多个相关步骤时,维护原子性和一致性是困难的。但 Saga 为你解决了这个问题!

当你有一个由许多微服务组成的大型分布式系统时,Saga 模式是完美的。有时你需要一个涉及多个步骤的业务事务,这些步骤发生在不同的服务中。没问题!使用 Saga,你可以将事务分解为一系列较小的交易,每个交易都完全包含在单个服务中。它协调所有数据流,确保每个请求最终正确对齐,而不需要复杂的分布式事务。分布式事务可能很复杂且难以扩展,但 Saga 避免了所有这些,以确保顺利航行。

图 2.16 所示,Saga 设计模式是一种处理复杂、多服务交易的技术,例如电子商务环境中的订单、支付和运输服务:

图 2.16:Saga 设计模式的示例

图 2.16:Saga 设计模式的示例

让我们分解前面的图。订单服务启动一个多步骤事务,每个步骤都由不同的服务处理。它们通过事件流进行通信,确保事务的每个步骤按顺序发生。如果任何步骤出现问题,应采取特殊措施(需要一些抽象来说明发生了什么,但图上未显示)来撤销确保一切恢复的先前步骤。每个服务都有自己的数据库,因此它们可以独立操作,而 Saga 模式确保整个事务要么完全成功,要么在出现任何问题时得到适当的补偿。这对于管理系统中不同服务必须无缝协作的复杂事务至关重要。

摘要

让我们回顾一下本章学到的内容,并展望接下来我们将要讨论的内容。以下是我们的学习成果:

  • DDD:确保我们的软件开发工作与特定业务需求正确对齐的策略。这种方法指导我们创建真正服务于其预期目的的软件。

  • CQRS:我们学会了有效地管理数据,将更改数据的操作(命令)与检索它们的操作(查询)分开。这种分离旨在提高我们系统的性能和可靠性,使其在实际用例场景中更具可行性。

  • 事件溯源:这种模式涉及将系统中的每次更改都记录为事件。当你跟踪随时间的变化时,它特别出色,并且是理解整个生命周期中决策和行动历史的基本构建块,这对于系统的操作至关重要。

  • 设计架构的好处:我们学会了如何构建健壮、高效且与业务对齐的系统,如何构建软件以更好地管理数据,以及如何共享现代商业应用程序预期的功能和非功能需求。

现在我们期待着阅读第三章,我们将学习如何使用 Spring Boot 构建响应式的 REST API,并深入探讨异步系统和背压原理。本章重点介绍设计网络应用程序的高级原理和概念,考虑到我们从第一章第二章中获得的软件架构理解,以便开发既响应又高效的应用程序。

第三章:反应式 REST 开发和异步系统

到目前为止,我们已经利用微服务和 Spring Boot 3.0 的功能构建了我们的理论基础。现在,是时候动手实践了。我们将学习并使用 Spring Boot 3.0 进行反应式编程。我们将回顾说明并构建我们的第一个反应式 REST 应用程序。我们还将讨论 背压以及它如何帮助我们的应用程序。

在本章中,我们将涵盖以下主要主题:

  • 反应式编程基础知识介绍

  • 构建反应式 REST API

  • 异步系统和背压

技术要求

对于本章,我们需要以下内容:

反应式编程简介

谈到响应式编程的基础,首先,我想说的是,没有任何解决方案/方法可以满足每一个需求。因此,项目有不同的问题,根据问题,有不同的解决方案。其中之一是响应式编程方法。在这里,我们将学习如何使我们的应用程序在面对处理大量请求和提供快速响应等特定要求时更加响应迅速、高效和健壮。随着我们深入概念和实践,您将看到这种方法如何从根本上改变您能构建的应用程序的本质——使它们完全准备好应对实时用户期望和高数据吞吐量带来的挑战。在下一节中,我们将探讨响应式编程的一些基础知识。

介绍响应式编程基础知识

让我们从响应式编程是什么开始。为了理解它,我们首先需要定义其核心术语。

接受新的范式——异步性和响应性

响应式编程引入了一种创新的软件开发方法;它是异步响应。传统的编程事件一个接一个地执行,而在响应式编程中,许多事件是并行发生的。异步性使得您的应用程序不会浪费时间等待,并继续处理其他任务。这使得它显著更快、更响应。

响应式的本质——数据流和传播

在其核心,响应式编程实际上关乎数据流和对这些流中变化的响应。想象一下,数据流就像一条始终向前移动和变化的信息传送带。任何用户输入、消息或实时数据都可能属于这个流。在响应式编程中,您使用称为可观察者的概念来处理这些流。

可观察者就像信使,当新数据到达时,它会通知应用程序的各个部分。随着数据通过这些流流动,它们可以被转换、组合或响应。我们将在以下章节中看到这些概念的应用。当新数据到达或数据状态发生变化时,应用程序的相关部分会立即更新。这被称为变化的传播,它始终确保您的应用程序反映当前的事态,并根据需要调整和响应。

在响应式编程中,不仅关乎如何有效地处理数据,还关乎让您的应用程序感觉生动且交互式。我们正在处理的应用程序可能是一个新闻实时流、社交媒体更新或实时控制系统——响应式编程可以帮助您构建能够跟上当今信息节奏的软件。

在本文的后面部分,我将解释术语,深入探讨一些好处,然后展示如何使用 Spring Boot 3.0 等工具使构建这些反应式系统更加容易。所以,无论你是这个概念的新手,还是希望通过更好地理解反应式编程来提高自己的技能,它都是你开发者工具箱中的一个强大多用途工具。那么,让我们继续前进,我将通过这种新颖的方法向你展示我们如何革命性地思考并构建软件。

对比范式 - 反应式编程与传统编程

为了使其更清晰,我们可以将反应式编程与传统开发方式进行比较。在学习软件开发时,我们首先理解流程算法。我们都很清楚传统方式,因此,这种比较将帮助我们理解新方式。让我们分析从传统到反应式编程的转变,了解它是如何改变我们的代码行为以及我们的应用程序性能的。

在传统编程中,操作通常逐个处理。这被称为阻塞。假设你在一个购物中心,在你挑选好商品后,你加入了一个收银台前的付款队列。收银员在当前顾客完成付款后才会接受下一位顾客。这种流程就是传统编程的工作方式。当顾客很多时,解决方案是开设新的收银台。这种方法也是可扩展的,但你需要增加服务器的数量来处理大量的顾客。

反应式编程引入了一个非阻塞模型。在这种方法中,任务在必要的资源可用时立即处理,同时应用程序可以在其间继续做其他事情。在我们的前一个例子中,收银员不必排队等待,可以同时处理多个顾客。当一个顾客试图找到信用卡时,收银员可以扫描下一位顾客的商品。当下一位顾客试图把商品放入袋子时,收银员可以从另一位顾客那里收取付款。在反应式编程中,这些过程不会混淆。在用户期望快速响应的应用程序的世界中,这种处理多个任务的变化至关重要。

总结来说,从传统到反应式编程的转变完全是关于让你的应用程序更加响应、高效和适应。在下一节中,我们将探讨有助于我们理解反应式编程的术语。

探索反应式编程的词汇表

为了更好地理解反应式编程,我们需要学习其基本词汇。本节将用简单的英语定义术语,这将使你能够舒适地使用反应性的语言。我们将解码以下列表中你可能会遇到的一些最常见的术语:

  • 可观察对象:在最简单的意义上,你可以将可观察对象想象成出版者。你也可以将其想象成发送最新新闻/信息的新闻广播者。在编程术语中,它是你想要处理的数据或事件的流。它可以是从网页上的点击到通过网络传入的数据的任何数量的事物。

  • 观察者:如果可观察对象是发送事件的广播者,那么观察者就是订阅并当事件发生时被通知的听众或观众。它是你的代码中“观察”或“监视”可观察对象并对其发出的数据或事件做出反应的部分。每当有新数据到达时,观察者会通过更新用户界面、处理数据或执行任何其他必要的任务来做出响应。

  • 订阅:这是可观察对象和观察者之间的联系。当观察者订阅可观察对象时,它开始接收来自它的更新。可以将其想象成订阅通讯或关注社交媒体上的某人——你实际上是在说:“我对你说的话感兴趣。”

  • 主题:主题是一种特殊的可观察对象,它可以充当可观察对象或观察者。它允许值和事件被发出并传递给观察者,同时也可以通过成为观察者来监听可观察对象。可以将其想象成一个团队,你是经理,你提供更新并倾听团队中的人。

  • 背压:想象一下在饮水机边喝水,水流得太快。背压就是能够控制这种流动,这样你就可以舒适地喝水而不会被淹没。在编程环境中,背压代表的是控制两个交互组件之间数据流动速率的能力,以便通信通道的接收端不会因为无法处理的数据而超负荷。这确保了应用程序在尽可能处理数据的同时不会变得无响应,并保持稳定。

  • 异步操作:在同步世界中,通常认为一个任务必须完成,另一个任务才能开始。然而,在异步操作中,多个任务可以同时运行。这类似于厨房的性质,厨师可以在等待汤煮沸的同时切菜和准备沙拉。在编程术语中,这意味着你的应用程序可以在处理用户输入的同时执行计算和加载数据,而不是等待每个过程完成后再进行下一个过程。

在您开始使用 Android 进行响应式编程之前,这些基本术语与学习驾驶规则一样重要。它们是掌握响应式编程的核心,并将在您开始开发应用程序时导航响应式高速公路时,不仅帮助您了解概念,还能帮助您获得其原则的情境知识。随着您对这些概念深入了解,您将意识到它们不仅仅是理论概念,而且是实用的工具,将赋予您作为开发者的能力,以创建对用户来说既吸引人又强大的软件。在了解所有响应式编程的基本术语后,我们将关注如何决定使用这种方法而不是传统方式。

识别响应式编程的机会

当涉及到将响应式编程集成到您的项目中时,我们应该知道何时何地应用它以提升我们的应用程序性能和用户体验。让我们探讨选择响应式的理想场景以及如何评估它是否符合您的特定需求。

以下是一些理想的响应式编程场景,表明何时从传统编程转向响应式编程:

  • 实时用户界面:如果您的应用程序显示实时更新(例如,实时体育比分、股票行情、社交媒体动态等),那么响应式编程对这个应用程序来说是一个相当好的提升。它负责保持用户界面的快速速度,并立即正确地响应变化。

  • 复杂的网络应用程序:响应式编程在管理需要重复调用网络的程序方面证明是有益的,例如聊天应用或在线游戏。使用响应式编程,通信得到妥善管理以及优化,即使在重负载期间,也能保持良好的数据流。

  • 微服务架构:响应式编程为围绕微服务架构构建的系统提供了两个关键的性能和弹性优势。广泛地说,每个服务都可以独立以及异步地处理请求,这提供了更高的响应性和容错性。

  • 物联网和数据流:考虑数据在响应式系统中从设备或传感器连续流出的场景。异步响应式编程将实现良好的流管理,这允许实时流处理以及对传入数据的响应,这是物联网应用或甚至在数据分析环境中的基本用例。

在更清楚地理解上述场景后,您将更有能力判断响应式编程是否是您应用程序的最佳方法。这一切都围绕着将应用程序的需求和挑战与响应式编程带来的优势相匹配。

要评估响应式需求,请参考以下列表:

  • 性能要求:考虑你的应用程序的响应性和速度要求。如果你的用户期望应用程序快速响应并且比静态页面更具交互性,那么反应式编程是必要的。

  • 数据量与速度:估计你的应用程序需要处理的数据量和速度程度。在涉及大量数据或高速数据流的应用中,反应性可能引入所需的鲁棒性和速度。

  • 操作复杂性:审查你的应用程序正在执行的操作的复杂性。无论是复杂的异步任务处理还是复杂的用户交互动态,你的反应式应用程序将提供更有效的解决方案。

  • 资源限制:考虑可用的硬件和资源。与资源使用相关的反应式编程,与传统编程相比,可能更合理地进行优化,因此它是适用于资源受限环境的适当范例。

我们可以根据这些实例决定是否使用反应式编程。记住,整个目的不是使用最新和最伟大的技术,而是为你的用户提供更好的体验,并为你的团队创建一个更易于管理的代码库。考虑在考虑这些因素和场景的情况下,反应式编程是否是你下一个项目的合适选择。在下一节中,我们将看到哪些行业最有可能选择使用反应式方法,我们还将看到反应式编程在现实世界中的应用。

从实践中学习——反应式编程在行动

我们现在知道在项目中何时可能需要使用反应式编程。虽然我们可能不知道每个公司的确切技术栈,但我们可以通过识别反应式编程可能发挥作用的场景,并从其应用已得到证实的案例中汲取灵感,来稍微强迫我们的大脑做出一些预测。以下是一些很可能使用反应式编程的平台:

  • 如 Netflix 等流媒体平台:如果你正在以高质量流式传输媒体,这意味着你拥有庞大的数据量。数百万客户端期望同时获得平滑、不间断的服务。为了处理这个数据流并提供不间断的服务,大多数媒体服务在其项目中使用反应式编程方法。

  • 如领英和推特等社交媒体平台:当我们谈论社交媒体时,你可以想象他们拥有的流有多大。再次,数百万用户同时发布、阅读和收听。不可计数的用户交互和即时内容交付是这些项目的需求。因此,再次,最好的解决方案是反应式编程,因为这些需求与反应式编程的原则非常吻合。

我们通过考虑他们的需求,假设一些公司正在使用响应式编程。然而,一些组织已经公开分享了他们成功采用响应式编程的经验:

  • Booking.com:以其全球住宿预订平台而闻名,Booking.com 实施了 Spring WebFlux,增强了其提供响应和高效服务的能力。

  • The Guardian:这家国际新闻机构利用 Akka 和 Play 框架来管理其实时新闻更新和高流量,确保用户能够立即访问最新故事。

  • Patreon:这个会员平台使用 RxJava 来处理复杂的金融交易和用户交互,展示了响应式编程在管理复杂、数据密集型任务方面的能力。

这些实际应用反映了响应式编程在各个行业中的实现方式。虽然它们使用不同的框架来处理需求,但在下一节中,我们将关注 Spring Boot 3.0 如何帮助我们实现响应式编程。

利用 Spring Boot 3.0 进行响应式解决方案

当我们提到 Spring Boot 时,突然在我们脑海中浮现出的词是“简单”。对于响应式编程来说,也是如此。在本节中,首先,我们将回顾 Spring Boot 3.0 的关键概念,这些概念可以帮助我们在参与响应式编程时使用。稍后,我们将看到 Spring Boot 为此方法使用了哪些库。

简化响应式开发 – Spring Boot 3.0 中的工具和功能

正如我们所见,Spring Boot 3.0 通过使其更快、更易于创建健壮的应用程序来简化整个开发过程。以下是它是如何增强响应式编程的:

  • Auto-configuration:Spring Boot 3.0 根据你添加的依赖项自动配置你的应用程序,减少了手动设置的需求,让你可以专注于编写业务逻辑。

  • Standalone:它允许你创建“只需运行”的独立应用程序,简化了部署和测试。你不需要外部服务器或容器;你的应用程序可以作为简单的可执行文件运行。

  • Opinionated defaults:Spring Boot 为项目配置提供了合理的默认值。这意味着你花费在配置上的时间更少,有更多时间按照最佳实践构建你的应用程序。

  • 社区和扩展:我们可以从庞大的 Spring 社区中获得帮助,并找到现成的扩展,这样我们就可以轻松地将额外的功能集成到我们的应用程序中,从安全到数据访问,无需重新发明轮子。

这些是常见的 Spring Boot 功能,它们不是针对响应式编程的特定工具。但它们帮助我们实现响应式方法,因为它们通过降低入门门槛和减少样板代码来帮助我们使用其他框架。

响应式编程的构建块——深入了解 WebFlux 和 Project Reactor

Spring Boot 3.0 中用于响应式能力的库是 WebFlux 和 Project Reactor。我们将讨论这些组件,这将为我们在这里构建响应式应用程序提供一个坚实的基础:

  • WebFlux:这是 Spring 的响应式 Web 框架。它被设计用于创建非阻塞、异步的 Web 应用程序,能够有效地服务于大量并发用户。它被设计用于与所有响应式库一起工作,并提供构建完全响应式 REST 应用程序所需的一切。

  • 0N个元素的数据流称为 Flux 和 Mono。通过 Project Reactor,你获得了一个强大的工具集来构建、转换和消费响应式数据流。

WebFlux 和 Project Reactor 共同构成了 Spring Boot 中完整响应式编程的骨架。它们提供了一种高度简化的数据处理和事件处理方式,为你提供了一个统一模型,你可以在其上构建响应性和健壮的应用程序。实现了实时数据或高负载应用程序可以利用这些工具提供的容量。

总结与展望

当我们总结这一节关于响应式编程的内容时,让我总结一下我们学到了什么。首先,我们讨论了响应式编程的“是什么”、“为什么”和“如何”,了解了术语,并对一些实际应用进行了调查。我们了解到,响应式编程不仅仅是一套技术,实际上是一种关于软件开发的思想方式,你的首要任务是保持响应性,然后是健壮性,其重点是提供出色的用户体验。最后,我们提到了如何在 Spring Boot 3.0 中使用它。现在,是我们动手实践的时候了。我们现在拥有了响应式编程的知识,下一步是通过实践本身来应用这些知识。

记住,然而,随着你的前进,掌握响应式编程的过程是一个持久的旅程。这是终身持续学习、实验和抓住每一个机会实现成长的过程之一。所以,享受使用它提供的机遇来使你的应用程序更加动态、更加健壮和更加用户友好吧。祝你在响应式世界中的所有成功!

构建响应式 REST API

是的,现在我们开始用 Spring Boot 3.0 编写响应式世界的第一段代码了。我们将开发一个 REST API。众所周知,REST 是应用中最常用的传输协议。通过这个协议,我们可以开发平台和语言无关的应用程序。因为我们可以在 Java、Python、C#等多种语言中使用 REST,多亏了这个协议,每个应用程序都可以轻松地相互交互。这就是为什么我选择了这个示例中的 REST API 开发。

首先,我们将启动我们的 Spring Boot 3.0 项目。我们将逐步介绍每个组件,并了解我们为什么要使用它们。所以,让我们卷起袖子,开始这段进入反应式世界的旅程。

设置开发环境

这是第一步——我们需要确保我们已经在本地机器上准备好了一切。我们已经在 技术要求 部分中提到了基本步骤。在这里,我们将进行快速检查并启动我们的项目。

工具和依赖项

为了开始开发,我们需要一个 IDE。您可以选择您喜欢的 IDE。Java 开发者中流行的选择包括 IntelliJ IDEA、Eclipse 和 Visual Studio Code。如果 IDE 支持 Spring Boot,它将使您的生活更加轻松,通过有用的插件和内置功能简化编码体验。

接下来,Java 是我们开发环境中最重要的一部分。Spring Boot 3.0 至少需要 Java 11,但我们将使用 Java 17。所以,请确保您的开发机器上已安装 Java 17。

Spring Initializr 是我们的下一个目的地;我们之前在 第一章 中提到过它。这是一个启动 Spring Boot 项目的非常方便的工具。它可以通过网页界面或直接通过您的 IDE 访问,并允许您生成具有所需依赖项、打包方式和 Java 版本的工程,所有这些都已准备好导入到您的 IDE 中。

创建一个新的 Spring Boot 项目

让我们从访问 Spring Initializr (start.spring.io) 网站或通过您的 IDE 访问它开始。我们将选择 Spring Boot 版本 3.2.5,因为它是 Spring Boot 3.0 的最新稳定版本,Java 版本将是 17。当您访问链接时,您将看到一个类似于 图 3.1 的屏幕:

图 3.1:Spring Initializr 窗口的截图

图 3.1:Spring Initializr 窗口的截图

如您所见,图中的是一个 Spring Initializr 示例项目。至于项目元数据,请填写相关的详细信息,例如 GroupArtifactNameDescription 以个性化您的项目。对于依赖项,请选择以下键:

  • Spring Reactive Web:这是 WebFlux,也是本项目最重要的依赖项,因为我们的示例将是一个反应式应用程序。

  • Spring Data R2DBC:我们需要这个来在反应式应用程序中连接关系型数据库。

  • H2 数据库:在我们的示例项目中,我们将使用嵌入式数据库以减少对我们另一个数据库服务器的依赖。

一旦您做出了所有选择,请点击 生成 按钮以获取准备构建的项目。这将下载一个包含 Gradle 项目的 ZIP 文件,具体取决于您的偏好。解压缩并将其导入到您选择的 IDE 中。现在您已经准备好深入实际的编码了!

现在,我们已经完成了第一个重要步骤。我们在本地机器上运行了一个 Spring Boot 3.0 应用。在下一节中,我们将逐步构建它。

定义响应式数据模型

数据模型是项目的关键点。因为数据模型是应用架构设计的最小部分,我们将决定数据如何被结构化、存储和访问。在我们的示例项目中,我们将有一个User实体。我们将看到如何创建它,并讨论它,强调 Java 中记录的简洁性和强大,以及@Table@Id等注解在将这些结构链接到数据库中的重要性。

创建用户记录

这是一个具有 ID、姓名和电子邮件的简单User实体。在 Java 中,我们可能会将其定义为一个类,但利用 Java 记录的优势,我们将以记录的形式引入它:

@Table("users")
public record User(@Id Long id, String name, String email) {}

这一行代码封装了我们User实体的一切:其字段是不可变的,它配备了必要的如equals()hashCode()等方法,并且已经准备好与我们的数据库进行交互。让我们来分解一下这些细节。

理解记录与类之间的区别

初看之下,你可以看到记录与类之间一个主要的不同点;那就是简洁性。但在未看到的方面还有一些其他的不同。这就是为什么在现代 Java 应用中,记录通常比传统类更受欢迎的原因:

  • 不可变性:记录的字段是最终的。这种不可变特性在需要线程安全和可预测性的响应式应用中是一大优势。

  • equals()hashCode()toString()方法减少了重复代码,使你的模型更加精简和专注。

  • 清晰性:记录的结构使其所代表的内容一目了然,从而促进了一个更容易理解和维护的代码库。

现在,我们已经清楚为什么选择record而不是class。接下来,让我们学习下一节代码片段中的注解。

理解@Table 和@Id 注解

当我们将记录连接到数据库时,诸如@Table@Id这样的注解就派上用场,尤其是在使用 Spring Data JPA 或 R2DBC 进行关系型数据库操作时:

  • @Table:这个注解指定了与该实体关联的数据库表名。在我们的例子中,@Table("users")表示User记录对应于数据库中名为users的表。这是 ORM 框架用来将记录映射到正确数据库表的关键信息。

  • @Id:每个表都需要一个主键,@Id注解可以帮助你标记实体中的哪个字段是主键。在User记录中,将id字段注解为@Id告诉 ORM 框架该字段唯一标识每个用户,应在数据库表中作为主键处理。

总之,定义我们的数据模型是一个重要的步骤。通过使用@Table@Id等注解,我们使我们的模型准备好与数据库进行高效交互。这种方法使我们的代码更干净、更易于维护。在下一节中,我们将处理数据库操作部分。

实现仓库层

在我们的反应式用户管理服务中,建立一个结构良好的仓库层至关重要。这一层将在应用程序的业务逻辑和数据库之间进行协调,处理所有数据交互。在本节中,我们将深入探讨为什么 H2 数据库是开发目的的首选,如何配置它,以及使用R2dbcRepository实现反应式仓库的重要性。

选择 H2 数据库

在开发应用程序时,H2 数据库是一个不错的选择,因为它简单易用。它是一个轻量级和内存数据库,无需安装或设置,非常适合测试和开发。数据库与你的应用程序一起启动和停止,允许快速测试而不影响任何实时数据库。此外,H2 数据库与 SQL 兼容,如果需要,可以轻松切换到更持久的数据库。

配置应用程序属性和模式

要将 H2 数据库集成到你的 Spring Boot 应用程序中,你需要配置application.properties文件。以下是一个基本设置:

# Enable H2 Console
spring.h2.console.enabled=true
# Database Configuration for H2
spring.r2dbc.url=r2dbc:h2:mem:///testdb
spring.r2dbc.username=sa
spring.r2dbc.password=
# Schema Generation
spring.sql.init.mode=always
spring.sql.init.platform=h2

此外,我们还需要在资源目录中的schema.sql文件中定义我们的数据库模式和初始数据。此 SQL 脚本在应用程序启动时自动执行,设置数据库模式。

反应式仓库简介

在传统的 Spring 应用程序中,你可能熟悉 JPA 和CrudRepository接口来管理数据操作。然而,在反应式世界中,我们使用ReactiveCrudRepositoryR2dbcRepository。这些接口旨在与反应式类型,如MonoFlux一起工作。通过使用这些接口,我们将确保所有数据操作都是非阻塞的,并支持背压。这种对反应式类型的转变使得我们的应用程序能够异步和高效地处理操作。

理解 R2dbcRepository

ReactiveCrudRepository提供了所有标准的Mono用于单个结果或Flux用于多个结果。这有助于我们与反应式基础设施无缝集成。

创建用户仓库

现在,让我们为用户实体定义一个仓库。利用R2dbcRepository接口,你可以创建一个扩展其功能的自定义仓库。以下是一个简单的UserRepository示例:

public interface UserRepository extends R2dbcRepository<User, String> {
    Mono<User> findByEmail(String email);
}

在这个片段中,UserRepository 扩展了 R2dbcRepositoryfindByEmail 方法是一个自定义查询方法,它返回 Mono<User>。此方法在响应式包装器中返回单个用户结果。此方法可能用于检查唯一电子邮件约束或检索用户信息。

通过这些简单的代码行,我们可以实现数据访问层。通过选择 H2 数据库进行开发,我们简化了我们的设置过程,并使我们的开发周期更快、更灵活。在我们继续构建用户管理服务时,请记住,从数据模型到数据访问层,每一层都是我们响应式应用程序的一部分。现在,我们将看看我们如何将用户和数据访问层与控制器层连接起来。

构建响应式 REST 控制器

控制器层是我们与外部世界交互的地方。我们的应用程序将处理传入的 HTTP 请求并以响应式的方式做出响应。让我们深入了解响应式控制器的工作方式,并逐步实现基本的 CRUD 操作。

控制器结构概述

在 Spring Boot 应用程序中,控制器是我们 API 的守门人。它将传入的请求导航到适当的服务或操作。在响应式环境中,这些控制器被设计为与非阻塞操作一起工作,并有效地处理数据流。@RestController 注解是 RESTful 控制器的默认注解。在创建控制器时,对于响应式并没有特定的更改。

下面是我们响应式 UserController 的基本结构:

@RestController
@RequestMapping("/users")
public class UserController {
    private final UserRepository userRepository;
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    // ... CRUD operations
}

在这个类中,@RestController 将其标记为控制器,其中每个方法都返回一个域对象,客户端以 JSON 对象的形式接收它。@RequestMapping("/users") 为控制器中的所有路由设置基本路径。我们还注入 UserRepository 以响应式地与数据库交互。

实现 CRUD 操作

CRUD 操作构成了大多数 API 的核心功能,允许客户端创建、读取、更新和删除资源。以下是这些操作在 UserController 中如何以响应式方式实现的:

  • 创建 (POST):我们始终使用 HTTP POST 调用来创建新记录:

        @PostMapping
        public Mono<User> createUser(@RequestBody User user) {
            return userRepository.save(user);
        }
    

    createUser 方法处理对 /users 的 POST 请求,将新用户保存到数据库中。注意返回类型是 Mono<User>,表示这是一个单个异步操作。

  • 列出所有用户 (GET):此方法用于列出或从应用程序中读取数据:

        @GetMapping
        public Flux<User> getAllUsers() {
            return userRepository.findAll();
        }
    `getUserById` fetches a single user based on the provided ID. It’s a typical example of a read operation in a REST API, returning `Mono<User>` as it expects at most one result.
    
  • 删除 (DELETE):当我们想要在我们的应用程序中删除记录时,我们始终使用此方法:

        @DeleteMapping("/{id}")
        public Mono<Void> deleteUser(@PathVariable String id) {
            return userRepository.deleteById(id);
        }
    

    deleteUser 方法处理根据其 ID 删除用户。Mono<Void> 返回类型表示一个将完成而不发出任何数据(void)的操作。

使用这个响应式 REST 控制器,我们确保每个操作都是非阻塞和可扩展的。这将允许我们的 API 处理大量并发用户和操作。正如你所注意到的,每个 CRUD 操作都返回 Mono 或 Flux(单个项或流)。这些方法是基本操作,现在我们将在下一节看到一个稍微复杂一些的示例。

添加高级 Mono 操作

创建一个响应式 REST API 不仅涉及实现基本的 CRUD 操作。它还关乎增强功能,以高效有效地处理现实世界场景。现在,假设我们有一个业务需求,在创建新用户时检查用户的电子邮件唯一性。让我们通过使用高级 Mono 操作来丰富 createUser 方法,以确保电子邮件唯一性并提供更稳健的错误处理策略。

为了检查电子邮件唯一性,我们需要使用几个响应式操作符和错误处理。

下面是高级的 createUser 方法:

@PostMapping
public Mono<ResponseEntity<User>> createUser(@RequestBody User user) {
    return userRepository.findByEmail(user.email())
            .flatMap(existingUser -> Mono.error(new EmailUniquenessException("Email already exists!")))
            .then(userRepository.save(user)) // Save the new user if the email doesn't exist
            .map(ResponseEntity::ok) // Map the saved user to a ResponseEntity
            .doOnNext(savedUser -> System.out.println("New user created: " + savedUser)) // Logging or further action
            .onErrorResume(e -> { // Handling errors, such as email uniqueness violation
                System.out.println("An exception has occurred: " + e.getMessage());
                if (e instanceof EmailUniquenessException) {
                    return Mono.just(ResponseEntity
                            .status(HttpStatus.CONFLICT).build());
                } else {
                    return Mono.just(ResponseEntity
                            .status(HttpStatus.INTERNAL_SERVER_ERROR)
                            .build());
                }
            });
}

让我们了解这个代码片段中的响应式方法:

  • flatMap(): 对于映射和扁平化非常有用,flatMap() 允许你链式调用异步操作,使其非常适合检查条件或转换数据。在这个示例中,这个操作符用于检查电子邮件是否已存在。如果找到电子邮件,它将抛出一个自定义的 EmailUniquenessException。这确保了系统中每个电子邮件都是唯一的,并为错误处理提供了清晰的路径。

  • then(): 这用于在之前的操作完成后链式调用另一个 Mono。在我们的例子中,在确保电子邮件不存在后,then() 用于保存新用户。它有效地链式调用用户保存操作,确保只有在电子邮件唯一性检查通过的情况下才会发生。

  • map(): 这将转换流中的数据,通常用于将一种类型转换为另一种类型或将其包装在另一个对象中。在我们的例子中,一旦用户被保存,map() 将保存的用户转换为带有 OK 状态的 ResponseEntity,准备返回给客户端。

  • doOnNext(): 这执行副作用,如日志记录或度量收集,而不改变流。在我们的例子中,这个操作符用于日志记录或任何其他你希望在创建新用户后执行的侧操作。这是一种进入流并执行操作而不改变数据流的方式。

  • onErrorResume(): 这提供了一种从错误中恢复的方法,允许你使用一个替代的 Mono 继续数据流。这对于错误处理至关重要;这个操作符捕获异常,并允许你提供一个替代的 Mono。在 EmailUniquenessException 的情况下,它返回带有冲突状态的 ResponseEntity,表示电子邮件已被使用。

将这些高级Mono操作添加到createUser方法中,将其从简单的保存功能转变为更复杂、适用于现实世界的操作。这种方法确保了通过电子邮件唯一性检查的数据完整性。它还向客户端提供了关于其请求结果(无论成功还是出错)的更清晰的通信。现在,是时候运行应用程序并看看它在用户眼中的样子了。

使用 Gradle 和 Java 17 运行 Spring Boot 应用程序

当您的反应式 REST API 已经就绪并准备好展示其能力时,是时候使用 Gradle 和 Java 17 运行应用程序了。以下是您可以在短时间内使应用程序启动运行的方法。

要使用 Gradle 运行 Spring Boot 应用程序,请按照以下步骤操作:

  1. 打开终端或命令提示符,并将目录更改为包含build.gradle文件的项目的根目录。

  2. 执行运行命令 – 使用以下命令启动您的应用程序:

       ./gradlew bootRun
    

此命令启动 Gradle 构建过程,编译您的 Java 代码,并启动 Spring Boot 附带的嵌入式服务器(通常是 Tomcat)。

执行运行命令后,请关注控制台中的日志消息。Spring Boot 提供了详细的日志,可以帮助您了解幕后发生的事情。以下是需要关注的关键点:

  • 构建成功:Gradle 成功构建应用程序的指示

  • 应用程序启动:与应用程序上下文、bean 和嵌入式服务器启动相关的消息

  • 运行应用程序:一个类似于“应用程序在 X 秒内启动”的日志条目表明您的应用程序正在运行

默认情况下,您的应用程序将在http://localhost:8080处可用。如果需要不同的端口或上下文路径,可以在application.propertiesapplication.yml文件中进行更改。

现在我们的应用程序已经启动并运行,测试其功能很重要。使用 cURL 命令或其他您用于进行 API 调用的工具与 API 中的某些端点进行交互。这种交互将验证您的应用程序是否正在运行(即,处理来自用户的请求并按要求响应)。

现在,我们的应用程序正在本地机器上运行。我们可以在嵌入式数据库中创建、列出和删除用户。此外,我们还可以测试其反应式能力。

在下一节中,我们将测试其功能,看看反应式方法如何帮助我们处理并发请求。

测试是应用程序开发不可或缺的一部分。我们需要确保每个组件按预期工作,整个系统运行顺畅。我们将在第六章中介绍单元测试部分。然而,单元测试并不是测试我们应用程序的唯一方式。我们可以手动测试反应式 REST API 的端点以验证其功能。我们将在本节中看到如何进行此操作。

cURL 是一个多功能的命令行工具,用于通过 URL 传输数据。它是手动测试 HTTP 端点的绝佳选择。通过执行 cURL 命令,你可以模拟客户端对应用的请求并观察响应,确保应用中的每个部分都按预期反应。

这里是用于测试用户管理服务各种端点的 cURL 脚本及其预期输出,包括电子邮件唯一性违规的场景:

  • POST /users): 创建新用户的步骤如下:

    • 创建新用户:我们将使用以下 cURL 命令创建单个用户:

      409 (conflict).
      
    • GET /users): 此命令将列出系统中的所有用户:

      GET /users/{id}): This command will gather just one user:
      
      

      DELETE /users/{id}1:

      204 (no content), indicating successful deletion.
      
      
      

    通过这些手动测试,我们确保应用在各种场景下都能正常工作并按预期行事。此外,我们还可以测试电子邮件唯一性的边缘情况。

    结论

    随着我们进入本节的结尾,让我们回顾一下我们的旅程。我们学习了如何使用 Spring Boot 3.0 构建反应式 REST API——这不仅仅是关于编码;这是选择一种现代的构建应用的方式。因此,在学习反应式编程的过程中,你学会了如何创建更快、更坚韧的服务,以及处理更大负载的能力。你经历了设置、创建数据模型、创建一个用于反应式存储数据的地方,以及创建一个可以执行许多操作的 REST 控制器。最后,我们进行了一个反应式 API 示例的工作。

    反应式编程是一个巨大的转变。在其中,重点是永不停止且异步的数据。如果你学会了它,你就准备好应对现代软件的需求了。这些是快速反应、良好性能和优秀用户体验。反应式编程使系统在压力下工作得更好,合理利用资源,并且更自然地管理同时处理许多任务。

    持续应用你所学习的关于反应式原则和技术。一开始可能会有些挑战,但绝对值得。拥抱反应式方式,你将创建出既强大又高效、健壮的系统。

    但这还不是全部。在下一节中,你将了解所有关于异步系统和背压的内容。我们将探讨如何保持大量数据的管理,以确保你的应用保持稳定和快速。在我们的高数据世界中,理解背压是关键,因为我们对速度和可靠性有很高的期望。

    不要忘记使用最终确定的项目来获得第一手经验。查看 GitHub 仓库以获取完整代码,了解整个系统的运作方式,并从这里开始你的工作。

    异步系统和背压

    在竞争激烈的世界中,异步通信已成为现代 Web 应用的关键元素,因为它承诺了效率和可扩展性。当我们使用“异步”这个词时,就会想到反应式系统。正如我们已经知道的,异步系统的需求来自于处理大量数据流的必要性。因此,系统应该处理这些流并保护自己免于耗尽。在这个时候,背压的作用就显现出来了。

    在本节中,我们将发现什么是背压以及我们如何在当前的示例项目中实现它。

    深入了解背压

    我们被告知背压是一种救命机制,但我们需要澄清这一点。在本节中,我们将讨论什么是背压,为什么我们需要它,以及它是如何保护我们的系统的。

    什么是背压?

    想象一下这样的情况:有一个高速传送带将产品运送到包装工那里。如果包装工跟不上速度,产品就会堆积起来,产品可能会损坏或丢失。背压是一种基于包装工能力控制产品流动的方法。这就像给包装工一个“停止”按钮,以防止过度负载的情况发生。

    在反应式流中,背压是一个关键概念。它允许数据消费者(订阅者)与数据生产者(发布者)就一次可以处理的数据量进行通信。这防止了过度负载的情况发生,并确保数据流平稳且可管理。

    为什么背压是必要的?

    我们将其定义为“停止”按钮,所以你可以想象如果没有它可能会造成的灾难。因此,没有背压,系统可能会遇到以下一些关键问题:

    • 内存溢出:当传入的数据流比应用能够处理的速度快时,服务器可能会耗尽内存

    • 性能低下:当系统努力管理不规则的数据流时,整体性能可能会下降,导致响应时间变慢,用户体验变差

    • 系统崩溃:在极端情况下,例如我们的应用出现意外的高需求时,系统可能会崩溃

    背压不仅仅是一个锦上添花的特性。如果我们需要管理不可预测的数据流,它对于系统来说是不可避免的必需品。在下一节中,我们将讨论如何在 Spring 世界中使用它。

    Project Reactor 中的背压

    Project Reactor 是 Spring 生态系统中进行反应式编程的基础库。它使用反应式流规范实现背压。这意味着确保程序中发送数据的部分以受控的方式发送,基于接收数据的部分可以处理的内容。

    在 Reactor 中,作为一个 Flux 或 Mono 通常发送数据(数据序列),它不会一次性发送所有数据。在这个模式中,接收数据的部分(订阅者)只会请求它能一次性处理的数据量。这种请求是通过一个名为 request(n)的方法来完成的。以下是它是如何工作的:

    • Request(n) 订阅者可以请求特定数量的项目。通过这种方式,订阅者通知生产者其当前的处理能力。

    • 动态调整:随着订阅者处理数据,它可以根据当前的负载、处理速度和其他因素动态调整其请求。

    • 传播背压:背压不仅仅是存在于一个生产者和一个消费者之间,而是在整个系统中用来保持平衡。

    使用 Project Reactor,开发者可以构建可靠且稳定的应用程序,其中数据将以最佳方式处理,不会过载系统。每个组件只处理它能处理的数据集,因此有助于构建平衡的系统。

    在我们进行项目工作时,我们将看到这些想法如何转化为代码,以及在实际应用中它们会简化成什么样子。

    我们将探讨不同的策略,查看日志和数据,并观察背压,以保持系统的平衡。记住,理解背压是关于构建能够很好地处理数据并保持稳定和快速的系统。

    在项目中实现背压

    背压就像数据发送者和接收者之间的舞蹈。它确保数据不会过快或过慢。我们的目标是保持数据流的平滑,防止过多的数据造成问题。让我们看看我们如何判断接收者接收了过多的数据,我们能做些什么,以及它是如何工作的。

    检测过载的消费者

    在理想情况下,数据会从发送者平滑地移动到接收者。但有时,接收者接收到的数据超过了它们能处理的能力。我们应该检测这种情况并相应地实施解决方案。在监控应用程序性能时,我们可以观察到一些信号,如下所示:

    • 观察延迟和吞吐量:如果数据处理时间变长或随着时间的推移处理的数据量减少,我们可以判断出有问题

    • 错误率和模式:大量的错误或某些类型的错误也可能表明数据过多

    • 资源利用率:如果消费者使用了过多的计算机功率或内存,它可能接收了过多的数据

    现在我们已经知道了如何检测是否需要背压,在下一节中,我们将探讨处理背压的策略。

    处理背压的策略

    一旦我们知道我们的消费者正在努力,我们该如何处理?我们可以引入这里提到的不同方法中的一些:

    • 缓冲:暂时持有数据,直到消费者准备好。如果过载是短暂的或间歇性的,这种策略效果很好。

    • 丢弃数据:在某些情况下,尤其是实时数据,可能可以接受丢弃一些数据以跟上流程。

    • 批处理:将数据累积到更大、更不频繁的批次中可以减少开销,并允许消费者赶上。

    • 速率限制:限制生产者发送数据的速率以匹配消费者的容量。

    在学习了处理背压的策略之后,我们将在下一节中在我们的应用程序中实现背压。

    在项目中实现背压

    现在,让我们增强我们的 Spring Boot 项目以包括背压处理。我们将关注一个典型场景:消费者是请求大量数据的客户端应用程序的 RESTful 服务。让我们看看如何一步一步实现它:

    1. 定义一个日志记录器:设置一个日志实例来观察背压如何影响数据流:

          private static final Logger log = LoggerFactory.getLogger(UserController.class);
      
    2. 记录更多详细信息的 getAllUsers 端点:

      @GetMapping
          public Flux<User> getAllUsers() {
              long start = System.currentTimeMillis();
              return userRepository.findAll()
                      .doOnSubscribe(subscription -> log.debug("Subscribed to User stream!"))
                      .doOnNext(user -> log.debug("Processed User: {} in {} ms", user.name(), System.currentTimeMillis() - start))
                      .doOnComplete(() -> log.info("Finished streaming users for getAllUsers in {} ms", System.currentTimeMillis() - start));
          }
      
    3. /stream 端点: 实现一个新端点,通过 Project Reactor 内置机制引入背压:

         @GetMapping("/stream")
          public Flux<User> streamUsers() {
              long start = System.currentTimeMillis();
              return userRepository.findAll()
                      .onBackpressureBuffer()  // Buffer strategy for back-pressure
                      .doOnNext(user -> log.debug("Processed User: {} in {} ms", user.name(), System.currentTimeMillis() - start))
                      .doOnError(error -> log.error("Error streaming users", error))
                      .doOnComplete(() -> log.info("Finished streaming users for streamUsers in {} ms", System.currentTimeMillis() - start));
          }
      

      在我们的项目中引入这些更改后,我们可以进行一个小型负载测试。

    4. 首先,我使用以下 bash 脚本在系统上创建负载:

      #!/bin/bash
      # A simple script to create load by sending multiple concurrent requests to the server.
      # Define the number of requests
      REQUESTS=300
      # The endpoint to test
      URL="http://localhost:8080/users/stream"
      for i in $(seq 1 $REQUESTS)
      do
         curl "$URL" &  # The ampersand at the end sends the request in the background, allowing for concurrency
      done
      wait # Wait for all background jobs to finish
      echo "All requests sent."
      

      此 bash 脚本将创建 300 个并发请求到脚本中提到的 URL。我们也可以使用相同的脚本为 URL="http://localhost:8080/users",并比较响应时间和系统负载。

    使用 cURL 命令在 /getAllUsers/streamUsers 端点上创建负载的日志显示了系统在压力下的表现,并提供了关于背压和异步处理如何工作的见解。让我们分析差异以及它们可能表明的内容。

    getAllUsers 端点日志

    /getAllUsers 端点非常快速地发送所有用户数据,并且不控制数据流。最初,它很快(每个请求大约 226 毫秒)。但是当更多人使用它时,每个请求所需的时间会更长——甚至可能翻倍。这种情况发生在系统非常繁忙且不控制传入请求时。它们继续接受更多的工作,但随着系统变得更加繁忙,每个任务所需的时间会更长。

    streamUsers 端点日志

    /streamUsers 端点有一种管理数据的方式称为背压,使用 .onBackpressureBuffer()。一开始,它稳定地处理请求。随着它变得更加繁忙,每个请求的时间会增加——比 /getAllUsers 端点慢,但它仍在增加。这种较慢的增加是系统正在控制数据流的迹象。它正在调整以处理它能够处理的内容,而不会过于超负荷。

    观察和结论

    我们的本地区域测试即使在没有性能监控工具的情况下也能告诉我们信息。您可以在本地服务器上查看两个端点的日志。以下是日志的观察结果:

    • 初始性能:两个端点在处理流式用户时都以相似的处理时间开始。这表明在正常条件下,两个端点几乎以相同的方式工作。

    • /getAllUsers端点响应更快。这体现在它更快地达到饱和状态。/streamUsers端点显示了时间逐渐增加,这表明背压允许系统更优雅地处理负载。

    • 系统压力:这是在负载下我们看到响应时间增加的情况。在现实场景中,这是一个绝对信号,表明我们需要应用性能优化,例如改进数据库访问、增加服务器资源或进一步细化背压策略。

    日志让我们对系统在大量压力下的工作情况有了很好的了解,并表明背压在/streamUsers中有所帮助。然而,要真正理解和改进系统,你需要更多详细的信息。当系统繁忙时,响应时间的缓慢增加是系统正在挣扎的常见迹象。这时,你可能需要做出重大改变或微调系统。

    通过谨慎使用和监控背压,我们使我们的项目更加稳定和高效。我们学到了很多关于数据如何移动和控制的知识。记住,背压不是一个简单的解决方案。它需要仔细思考和调整,以满足应用程序不断变化的需求。但凭借我们所学的知识,我们准备好确保你的应用程序能够很好地处理数据。

    摘要

    第三章中,我们深入探讨了 Spring Boot 3.0 带来的向响应式编程范式转变。从对响应式编程核心和基础的理解出发,到证明这些范式的重要性,以使应用程序更加响应迅速、高效和健壮。

    下面是我们所涵盖的内容:

    • 过渡到响应式编程:我们通过转向非阻塞模型,以异步方式解决问题,从而对比了响应式编程和传统编程,以实现更快和更响应的应用程序。

    • 构建响应式 REST API:我们涵盖了构建响应式 REST API 的基本要素,理解异步系统以及背压的概念,以有效地执行数据流。

    • 成功设置:本章详细介绍了如何设置您的开发环境,从安装 Java 17 和 IntelliJ IDEA,一直到如何创建响应式数据模型,如何实现仓库层,以及如何使用 Spring Boot 3.0 构建响应式 REST 控制器。

    第四章的后续内容中,我们将使用 Spring Data 来管理数据,我将探讨 SQL 数据库以及 NoSQL 数据库。我们还将讨论数据迁移以及数据一致性,确保您的应用程序依然健壮且运行顺畅。下一章将包括使用 SQL 数据库的 Spring DataSpring Boot 中的 NoSQL 数据库以及数据迁移和一致性部分。下一章将深入探讨 Spring 生态系统内部的数据管理问题,并为您准备应对各种数据库的挑战。

    随着我们从仅仅理解软件开发中的任何反应式范式过渡到真正掌握 Spring Boot 3.0 的强大功能来简化并增强这些流程,请务必记住本章所学的内容。敬请期待下一章中更多有见地的讨论和实用指南。

第三部分:数据管理、测试和安全

本部分讨论软件开发的关键组件:数据管理、测试和安全。从第四章开始,您将深入了解 Spring Data,探索 SQL、NoSQL 和缓存技术。第五章专注于保护您的 Spring Boot 应用程序,确保它们能够抵御未经授权的访问。最后,第六章介绍了高级测试策略,有助于验证和提升软件的可靠性。本部分对于创建全面、安全且高效管理的应用程序至关重要。

本部分包含以下章节:

  • 第四章, Spring 数据:SQL、NoSQL、缓存抽象和批处理

  • 第五章, 保护您的 Spring Boot 应用程序

  • 第六章, 高级测试策略

第四章:Spring Data:SQL、NoSQL、缓存抽象和批处理

欢迎来到第四章。在这里,我们将更深入地了解 Spring Data 方法。在本章中,我们想要了解 Spring Data 方法如何为我们工作。Spring Data 是 Spring Boot 生态系统中的关键部分。这将帮助您清楚地了解如何在 Spring Boot 3.0 中处理不同类型的数据库。

为什么本章很重要?在软件开发中,我们如何管理数据非常重要,而不仅仅是存储数据。本书的这一部分不仅关于研究 Spring Data 的各个部分,而且还关于将它们应用于实际场景。在本章中,我们将看到如何配置和使用 Spring Data,这对于数据管理活动非常有帮助。您将学习如何处理存储在 SQL 数据库中的结构化数据以及存储在 NoSQL 数据库中的非结构化数据,这对于各种类型的数据都是理想的。此外,我们将介绍缓存抽象是什么以及为什么它有助于使您的应用程序运行更快。另一个非常重要的主题是批处理,以及如何一次性处理大量数据。更进一步,您将学习如何安全地更改和更新数据的重要技术。

了解如何处理数据,无论是简单还是复杂,都是关键,并将有助于提高软件编程技能。在本章结束时,您不仅将了解理论,还将能够在实际项目中亲自动手,应用这些技能。我们将使用一个真实的项目,一个在线书店管理系统,向您展示事物是如何运作的。

在本章结束时,您将对 Spring Data 的理论和实践应用有很好的掌握。这对于任何开发者来说都是必不可少的,无论被视为经验丰富还是初出茅庐。

让我们开始吧。我们将看到 Spring Data 如何帮助您改变在项目中管理数据的方式。我们将把理论转化为实际技能,并帮助您作为开发者成长。

在本章中,我们将重点关注以下主要主题:

  • Spring Data 简介

  • 使用 Spring Data 与 SQL 数据库

  • Spring Boot 中的 NoSQL 数据库

  • Spring Boot 缓存抽象

  • Spring Boot 批处理

  • 数据迁移和一致性

技术要求

对于本章,我们将在本地机器上需要一些安装:

下面是安装 Docker Desktop 的步骤:

  1. 访问 Docker Desktop 网站:docs.docker.com/desktop/

  2. 按照安装 Docker Desktop菜单下的说明操作。它适用于各种操作系统,并提供了一个简单的安装过程。

Spring Data 简介

在本节中,我们将探讨 Spring Data 的一般基本概念及其用途。我们还将检查它们在我们案例研究项目——在线书店管理系统中的应用。

Spring Data是 Spring 框架的一部分,它可以尽可能简化我们应用程序与数据的交互。其主要优点和最大优势是简化数据库操作的能力。这意味着您可以用更少的代码和复杂性执行查询数据库或更新记录等任务。

理解 Spring Data 将有助于您在有效处理数据方面提升技能——这是软件开发的关键因素之一。无论是小型应用程序还是复杂的企业系统,在数据层上实施有效的基于角色的访问对整个系统的性能和维护都起着重要作用。

在本章中,我们将展示 Spring Data 的主要特性以及它如何帮助您简化工作。了解这些概念将帮助您在软件项目中管理数据,从而使开发过程更加容易,并加快其流程。

因此,让我们带着 Spring Data 踏上这段有趣的旅程。这将是一次实用且富有信息性的旅程,到结束时,您将肯定能够熟练地使用 Spring Data 来管理自己的应用程序中的数据。

理解 Spring Data 的基本知识和好处

在本节中,我们将关注 Spring Data 的基础知识。我们首先将了解其核心概念和每个开发者都至关重要的好处。从那里,我们将进入设置 Spring Boot 项目并定义一些关键的 JPA 实体。通过采取这些小步骤,我们将逐步建立起 Spring Data 的坚实基础,这将使后续的应用程序更加高级。

我们将从 Spring Data 在 Spring Boot 生态系统中的强大工具以及它为何对您的项目有益开始。然后,我们将继续探讨设置项目并深入了解 Java 持久化 API(JPA)实体的实际部分。

在深入技术设置之前,让我们首先了解在项目中使用 Spring Data 的核心原则和优势。

探索 Spring Data 的核心概念

Spring Data 旨在简化 Java 应用程序与数据库的交互。以下是其一些基本概念:

  • 数据访问简化:Spring Data 简化了数据访问操作。您不再需要为常见的数据库交互编写样板代码。我们将看到如何在不编写任何代码的情况下执行创建、读取、更新和删除CRUD)操作。这将使我们的代码更易于阅读和管理。

  • 仓库抽象:Spring Data 的一个关键特性是其仓库抽象。这使我们能够将数据库操作像函数一样在我们的框架中使用。如果我们不知道如何在特定数据库中编写查询,我们不需要担心。这种抽象使得它对所有支持的数据库都一样工作。它抽象了数据层,这意味着你可以更多地关注业务逻辑,而不是数据库的复杂性。

  • 支持多种数据库类型:Spring Data 支持广泛的数据库类型,包括 SQL 和 NoSQL 选项。这种多功能性使其成为可能需要不同数据库技术的项目的宝贵工具。

使用 Spring Data 的好处

现在,让我们看看为什么 Spring Data 对你的开发过程有益:

  • 提高效率和生产力:Spring Data 不仅减少了数据库操作中的样板代码,而且以最佳实践和高效的方式执行这些操作。在旧结构中,我们经常处理未关闭的连接问题。Spring Data 有效地管理所有连接池问题。

  • 易于学习和使用:Spring Data 被设计成用户友好。只需翻几页,你就会明白我的意思。开发者可以快速学习如何使用它,并将其应用到他们的项目中。它与 Spring 生态系统的集成也意味着它可以与其他 Spring 技术无缝工作。

  • 提高代码质量和可维护性:通过减少代码冗余和更干净的数据处理方法,Spring Data 提升了代码的整体质量。这使得你的应用程序在长期内更容易维护和更新。

随着我们逐步完成 Spring Boot 应用程序的设置和使用,你会更好地理解这些优势和概念。现在,我们已经对 Spring Data 是什么以及为什么它有优势有了基本了解,让我们继续设置我们的项目并定义我们的 JPA 实体。

设置你的 Spring Boot 项目

首先,让我们设置一个 Spring Boot 项目。这是任何 Spring 应用程序的起点。你可以遵循与 第三章 中相同的步骤。或者,你可以直接克隆 技术要求 部分提供的 Git 仓库。

在创建应用程序时,你需要以下基本依赖项:

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

这个名为 Lombok 的库通常用于消除像 getIdsetId 方法这样的样板代码。

通过这些步骤,你的 Spring Boot 项目就准备好了。现在,让我们定义一些 JPA 实体。

定义 JPA 实体:书籍、作者和出版社

在一个书店应用程序中,我们处理书籍、作者和出版社。让我们将它们定义为 JPA 实体。我们将创建以下三个类。你可以在 model 包下的仓库中看到它们:

@Entity
@Table(name = "books")
@Data
public class Book {
    @Id @GeneratedValue
    private Long id;
    private String title;
    private String isbn;
}
@Entity
@Table(name = "publishers")
@Data
public class Publisher {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String address;
}
@Entity
@Table(name = "authors")
@Data
public class Author {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String biography;
}

每个 BookAuthorPublisher 实体都代表了我们书店系统的一个关键部分。通过使用 @Data 注解,我们简化了我们的代码。

理解 Spring Data 中仓库的作用

在定义我们的实体之后,我们需要创建仓库。Spring Data 中的仓库帮助我们轻松地与数据库交互。

为了为每个实体(BookAuthorPublisher)创建一个仓库,创建一个扩展 JpaRepository 的接口。此接口提供了常见数据库操作的方法。

对于 Book,它可能看起来像这样:

public interface BookRepository extends JpaRepository<Book, Long> {}

这些仓库提供了一些通用的方法来保存、查找、删除和更新实体。例如,要查找所有书籍,你可以使用 bookRepository.findAll()

这个设置将成为我们即将构建的 Spring Boot 应用程序的基础。到目前为止,你应该有一个包含实体和仓库的基本项目准备就绪。

现在,我们已经设置了一个带有 Spring Data 依赖项的 Spring Boot 项目,并定义了基本的 JPA 实体和仓库。这是我们使用 Spring Data 的工作基础。我们尚未连接到数据库。

在下一节中,我们将深入探讨如何使用 Spring Data 与 SQL 数据库。我们将查看在项目中使用 Spring Data 的简单有效的方法。

使用 Spring Data 与 SQL 数据库

对于处理 Spring Data 的开发者来说,了解其与 SQL 数据库的关系非常重要。SQL 数据库以其对数据管理的结构化方法而闻名。它们在各种应用程序中得到广泛应用。我们将在我们的在线书店管理系统中使用它们。

在本节中,我们将探讨 Spring Data JPA 如何与 SQL 数据库接口,重点关注 PostgreSQL 配置和复杂实体关系的创建。

使用 Docker 将 PostgreSQL 集成到 Spring Boot 中

首先,我们需要在我们的本地机器上有一个运行中的 PostgreSQL 服务器。最简单的方法是使用 Docker 容器。Docker 是一个简化应用程序及其依赖项设置和部署的工具。让我们看看如何使用 Docker 设置 PostgreSQL 并配置你的 Spring Boot 应用程序以连接到它。

使用 Docker 设置 PostgreSQL

使用 Docker,你可以轻松地安装和运行 PostgreSQL 数据库。这种方法为你的数据库提供了一个一致且隔离的环境,无论你的本地设置如何。

你已经按照 技术要求 部分的说明安装了 Docker Desktop。我们将使用一个 Docker Compose 文件,该文件也可以在 GitHub 仓库的根目录中找到。以下是一个使用 Docker Compose 文件的示例:

    version: '3.1'
    services:
      db:
        image: postgres
        restart: always
        environment:
          POSTGRES_PASSWORD: yourpassword
          POSTGRES_DB: bookstore
        ports:
          - "5432:5432"

这将设置一个名为 bookstore 的数据库的 PostgreSQL 服务器。将 yourpassword 替换为你选择的密码。将此文件保存为 docker-compose.yml 在根源文件夹中。

在你的 docker-compose.yml 文件所在的目录中运行 docker-compose up 命令。此命令将下载 PostgreSQL 镜像并启动数据库服务器。

配置 Spring Boot 以连接到 PostgreSQL

现在 PostgreSQL 在 Docker 容器中运行,让我们配置我们的 Spring Boot 应用程序以连接到它:

  1. 更新application.properties:打开您的 Spring Boot 项目中的application.properties文件。添加以下属性以配置到 PostgreSQL 服务器的连接:

        spring.datasource.url=jdbc:postgresql://localhost:5432/bookstore
        spring.datasource.username=postgres
        spring.datasource.password=yourpassword
        spring.jpa.hibernate.ddl-auto=update
    

    确保将yourpassword替换为您在Docker Compose文件中设置的密码。spring.jpa.hibernate.ddl-auto=update属性有助于根据您的实体类管理数据库模式。

  2. 验证连接:运行您的 Spring Boot 应用程序。在这个阶段,我们只能看到应用程序是否能够正确启动。在在线书店实现实际 CRUD 操作部分,我们将介绍 REST 控制器以验证它是否成功连接到 PostgreSQL 数据库。

通过这些步骤,您已成功将 PostgreSQL 数据库集成到您的 Spring Boot 应用程序中,使用 Docker。这种设置不仅简化了初始配置,还确保了开发和测试环境中数据库的一致性。现在,我们将更进一步,在下一节介绍对象之间的高级实体关系。

开发实体之间的复杂关系

在本节中,我们将专注于开发我们实体之间的复杂关系:BookAuthorPublisher。我们将使用 Spring Data 的注解驱动方法来链接这些实体,反映我们数据库设计中的现实世界联系。

在我们的书店应用程序中,我们创建了基本对象。现在,我们将把它们连接起来。让我们从书籍作者的连接开始。

每本书可以有一个或多个作者,形成一个多对多关系。

这是我们在Book记录中表示它的方法:

    @ManyToMany
    private List<Author> authors;

@ManyToMany注解表示每本书可以与多个作者相关联。这种关系是双向的,这意味着作者也可以与多本书相关联。在多对多关系中,你需要一个新的交叉表来链接这些表。这是数据库设计的一部分,因此我们将提及此功能,以免您在查看数据库中的表时感到惊讶。

现在,我们将把作者出版社关联起来。一个作者可能关联到一个出版社。这是一个多对一的关系,因为多个作者可以被同一个出版社出版:

    @ManyToOne
    private Publisher publisher;

这里的@ManyToOne注解表示每个作者都与一个出版社相关联,而一个出版社可以有多个作者。

出版社实体保持简单,因为它在此上下文中不需要建立新的关系。

图 4**.1中,我们可以看到表及其之间的关系:

图 4.1 – 表的数据库图

图 4.1 – 表的数据库图

图表中您可以看到的表是在我们启动应用程序时由 Spring Data 库生成的。您可以看到有一个额外的表叫做 books_authors。这个表用于 booksauthors 表之间的多对多关系。

通过这种实现,我们在应用程序中应用了书籍、作者和出版社之间的现实世界联系。

在我们总结本节关于实体关系的内容时,我们在在线书店管理系统中定义了复杂的数据结构。接下来,我们将看到这些关系在实际场景中的工作方式。

在在线书店中实现实用的 CRUD 操作

在我们建立了复杂的实体关系后,让我们看看这些关系如何在在线书店管理系统中实际实现。我们将通过 REST 端点介绍 创建、读取、更新、删除CRUD)操作,展示控制器如何与 PostgreSQL 数据库交互。

首先,我们将创建控制器类。让我们回顾一下 book 对象。您可以对 AuthorPublisher 对象进行类似的修改。或者,您可以查看 GitHub 仓库 (github.com/PacktPublishing/Mastering-Spring-Boot-3.0/tree/main/Chapter-4-1-intorduction-spring-data/src/main/java/com/packt/ahmeric/bookstore/data) 以获取所有三个对象的最新实现。

开发 CRUD 端点

现在,我们将动手操作,使这些对象可以从外部访问。首先,我们需要一个 Repository 类来管理实体。这是一个用于 book 对象的基本仓库类:

@Repository
public interface BookRepository extends JpaRepository<Book, Long> { }

这个单行类将帮助我们使用许多常用方法,例如 findAll()save(book)findById(id)。这就是 Spring Data 的力量。

为了创建一个处理与书籍相关的请求的控制器类,我们引入了 BookController 类来处理书籍相关的操作。这个控制器将管理添加新书、检索书籍详情、更新书籍信息和删除书籍等操作。

因此,让我们引入一个新的类名为 BookController

@RestController
@RequestMapping("/books")
@RequiredArgsConstructor
public class BookController {
    private final BookRepository bookRepository;
    @PostMapping
    @CacheEvict(value = "books", allEntries = true) // Invalidate the entire books cache
    public ResponseEntity<Book> addBook(@RequestBody Book book) {
        Book savedBook = bookRepository.save(book);
        return ResponseEntity.ok(savedBook);
    }
    @GetMapping("/{id}")
    public ResponseEntity<Book> getBook(@PathVariable Long id) {
        Optional<Book> book = bookRepository.findById(id);
        return book.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
    }
}

在这个简单的类中,您可以看到类名上方的新注解 – @RestController@RequestMapping。如您所记,我们在 第三章 中使用过它们。但这里有一个新的注解:@RequiredArgsConstructor。这个注解也属于 Lombok。这个注解将在编译时创建一个构造函数,因此我们有一个没有样板代码行的清晰类。

在这个示例代码中,我们有两个端点用于创建书籍和通过 ID 获取书籍。这些是接受和返回 JSON 数据的 REST 端点。您可以看到我们正在使用来自bookRepository的默认方法,例如findById()save()。我们并没有在我们的Repository类中编写它们。它们来自JpaRepository扩展。Spring Data JPA 仓库与 Hibernate 结合,有效地管理底层的 SQL 查询和事务,抽象复杂性并确保数据处理的顺畅。因此,我们甚至不需要写一行代码来在数据库中保存实体。我们只使用save()方法。

您可以在 GitHub 仓库中看到其他用于删除和查找所有更新的端点。如果您愿意,可以类似地创建AuthorControllerPublisherController来管理作者和出版商。

正如我们介绍的表之间的关系,当我们添加带有其作者的书籍时,books_authors表将相应更新。

让我们进行一些 curl 请求来查看它是如何工作的。

使用 curl 请求进行实际操作演练

使用以下命令运行 Spring Boot 应用程序:

./gradlew bootRun

现在,我们将按顺序运行以下请求:

  1. 创建出版商:

    curl -X POST --location "http://localhost:8080/publishers" -H "Content-Type: application/json" -d "{\"name\": \"Publisher Name\", \"address\": \"Address of the publisher\"}"
    

    这是响应:

    {
      "id": 1,
      "name": "Publisher Name",
      "address": "Address of the publisher"
    }
    
  2. 创建作者:

    curl -X POST --location "http://localhost:8080/authors"
        -H "Content-Type: application/json"
        -d "{\"name\": \"Author Name\",
            \"biography\": \"A long story\",
            \"publisher\": {\"id\": 1}}"
    
  3. 创建书籍:

    curl -X POST --location "http://localhost:8080/books"-H "Content-Type: application/json" -d "{\"title\": \"Book title\",\"isbn\": \"12345\",\"authors\": [{\"id\": 1}]}"
    

如您所见,我们使用了链接对象的id。例如,当我们创建一个作者时,我们使用 ID 为 1 的出版商将作者链接起来。

在这个实际实施阶段,我们已经建立了如何在我们的控制器中创建功能端点来管理在线书店中的书籍、作者和出版商。这些端点与 PostgreSQL 数据库无缝交互,展示了在真实应用程序中使用 Spring Data 的强大和高效。

在掌握这一部分内容后,我们准备好学习我们旅程的下一步——将 NoSQL 数据库集成到 Spring Boot 中,并进一步扩展其数据管理功能。

Spring Boot 中的 NoSQL 数据库

在研究了结构化 SQL 数据库之后,我们现在将深入研究 NoSQL 数据库。我们将看到它们比 SQL 数据库更加灵活。在本节中,我们将看到在 Spring Boot 3.0 中实现 NoSQL 数据库是多么容易。我们将在我们的书店管理系统应用程序中实现这个数据库连接。

探索 Spring Boot 中 NoSQL 数据库的集成

在我们的 Spring Boot 之旅的这个阶段,我们将转换方向,研究 NoSQL 数据库的集成,这是现代应用程序堆栈的一个基本组成部分。与传统的 SQL 数据库不同,NoSQL 数据库如 MongoDB 提供了一种不同的数据管理风格,因此它们适用于这种情况。在这里,我们将不仅了解这些好处,而且还将学习如何在现实世界的应用程序中有效地实现它们。

NoSQL 数据库因其能够灵活处理不同数据类型而备受赞誉,这些数据类型大多是未结构化或半结构化的。这种灵活性为面临多样化数据需求或甚至不断变化的数据结构的开发者提供了巨大的优势。在 NoSQL 的世界中,MongoDB 被视为面向文档的,这使得它成为需要可扩展和敏捷数据存储平台的应用程序的最佳数据存储选项之一。

关于将 NoSQL 数据库与 Spring Boot 集成的过程,整个过程得到了简化,并且非常容易完成。Spring Boot 与 MongoDB 等 NoSQL 数据库的良好集成方式实际上允许开发者将其直接插入到他们的应用程序中,而无需进行烦人的配置。这种集成开辟了应用程序开发的新天地,其前景围绕着构建更动态、可扩展和高效应用程序的潜力。

那种协同效应的最好之处在于在 Spring Boot 中使用 NoSQL 数据库。Spring Boot 的哲学是简化应用程序的开发,这与 NoSQL 的本质相辅相成,带来了可扩展性和灵活性。这种组合对于开发不仅涉及处理复杂数据结构,而且必须适应数据需求变化的应用程序尤其有效。

在我们的在线书店管理系统背景下,集成如 MongoDB 这样的 NoSQL 数据库不仅将为应用程序的功能增加大量价值,还将展示如何将这些最先进的技术家族结合起来的实际例子。我们可以使用 MongoDB 来集成用户评论或个性化推荐等功能,这些功能利用了由 NoSQL 数据库提供的灵活的数据建模。

当我们深入研究如何将 NoSQL 数据库集成到 Spring Boot 中时,我们不仅又获得了一个可用的工具,而且对各种数据库的工作原理有了更多的了解,这样我们就可以开发出更强大、更灵活且性能更快的应用程序。在这个适应和随着现代技术进步而发展的能力仍然是成功关键因素之一的领域中,这种知识是无价的。在接下来的章节中,我们将逐步在我们的项目中实现 MongoDB。

设置和配置 MongoDB

我们需要在本地机器上运行 MongoDB,就像在上一节中为 PostgreSQL 所做的那样。与 PostgreSQL 类似,MongoDB 可以在 Docker 容器中设置,确保数据库环境独立且一致。

您可以在此处看到我们 docker-compose.yml 文件的增强版本:

version: '3.1'
services:
  db:
    image: postgres
    restart: always
    environment:
      POSTGRES_PASSWORD: yourpassword
      POSTGRES_DB: bookstore
    ports:
      - "5432:5432"
  mongodb:
    image: mongo
    restart: always
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_DATABASE: bookstore

您也可以在 GitHub 仓库中找到相同的文件。您可以通过运行 docker-compose up 命令同时运行 MongoDB 和 PostgreSQL。

运行 MongoDB 实例后,我们需要更新我们资源文件夹中的 application.properties 文件。

这条单独的配置行将在 Spring Boot 应用程序和 MongoDB 之间创建一个连接:

spring.data.mongodb.uri=mongodb://localhost:27017/bookstore

通过这个简单的配置更新,我们就可以将 MongoDB 连接到我们的本地机器。在下一节中,我们将介绍一个新的对象,并查看我们的应用程序如何与 MongoDB 一起工作。

构建 Review 对象及其仓库

正如我们对 BookAuthorPublisher 对象所做的那样,我们需要引入一个新的对象,称为 Review.class。您也可以在 GitHub 仓库的 data 包下查看它:

@Document(collection = "reviews")
@Data
public class Review {
    @Id
    private String id;
    private Long bookId;
    private String reviewerName;
    private String comment;
    private int rating;
}

您可以看到与其他数据对象的不同之处。这里有一个新的注解叫做 @Document。这个注解指的是这个对象的集合。所以,我们在这个对象中放入的任何内容都将写入 reviews 集合。我们刚刚介绍了一些评论可能需要的基字段。

现在,我们还需要一个仓库来管理这个文档在 MongoDB 中的数据。让我们在 repositories 包下引入 ReviewRepository 类:

public interface ReviewRepository extends MongoRepository<Review, String> { }

就这样!现在,我们可以在我们想要的地方管理数据。我们正在扩展 MongoRepository 而不是 JPA 仓库接口。这是 BookRepositoryAuthorRepository 之间的唯一区别。因此,现在我们有了所有的 CRUD 功能,如 findById()save()。此外,这可以针对更复杂的企业需求进行定制。我们可以在下一节开始实现 Review 对象的控制器。

在在线书店中实现混合数据模型

我们的项目现在已经演变成一个混合模型,集成了 SQL(PostgreSQL)和 NoSQL(MongoDB)数据库。因此,让我们将 review 对象暴露给 REST 世界,这样我们就可以在 MongoDB 中创建和读取评论。

我们需要在控制器包中创建一个新的控制器类:

@RestController
@RequestMapping("/reviews")
@RequiredArgsConstructor
public class ReviewController {
    private final ReviewRepository reviewRepository;
    @PostMapping
    public ResponseEntity<Review> addReview(@RequestBody Review review) {
        Review savedReview = reviewRepository.save(review);
        return ResponseEntity.ok(savedReview);
    }
    @GetMapping("/{id}")
    public ResponseEntity<Review> getReview(@PathVariable String id) {
        Optional<Review> review = reviewRepository.findById(id);
        return review.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
    }
}

如您所见,BookController 类和 ReviewController 类之间没有区别,因为我们已经将数据库层从仓库级别隔离出来。这两个端点暴露了 GET reviewPOST review 端点。您可以引入其余的 CRUD 端点,或者查看 GitHub 仓库。

让我们进行一些 curl 请求来查看它是如何工作的:

curl -X POST --location "http://localhost:8080/reviews"
    -H "Content-Type: application/json"
    -d "{\"bookId\": 1, \"reviewerName\": \"Reader\", \"comment\": \"A great book to read\", \"rating\": 5}"

响应将如下:

{
  "id": "65adb0d7c8d33f5ab035b517",
  "bookId": 1,
  "reviewerName": "Reader",
  "comment": "A great book to read",
  "rating": 5
}

记录的 idReview 类中的 @Id 注解生成。

这是在 MongoDB 中的样子:

图 4.2 – MongoDB 数据视图

图 4.2 – MongoDB 数据视图

在这个图中,我们可以看到 MongoDB 如何在 _class 属性中标记我们的对象。在 Spring Boot 上下文中,对 NoSQL 数据库(以 MongoDB 为重点)的这种探索,扩大了我们对于在现代应用程序中管理多种数据类型的理解。通过在在线书店管理系统中实现 MongoDB,我们不仅丰富了应用程序的新功能,还拥抱了混合数据库方法的优点。

在我们总结本节内容时,我们通过 Spring Boot 的数据景观之旅仍在继续。接下来,我们将深入研究 Spring Boot 中的缓存抽象,我们将探讨优化应用程序性能的策略。从 NoSQL 数据库到缓存技术这一进展,体现了 Spring Boot 应用程序中数据管理的全面性。

Spring Boot 缓存抽象

在本节中,我们将深入研究 Spring Boot 中的缓存抽象。这是最大化应用程序性能的重要辅助组件之一。我们将了解缓存抽象是什么,如何进行其设置,以及最后如何在我们的应用程序中使用它。我们将使用我们的在线书店管理系统来展示这一点。

由于缓存抽象位于您的缓存系统之上,它能够记住重复使用的信息,从而提高应用程序的执行速度。这就像把您常用的工具放在桌子上一样,这样您就不必每次都去寻找它们。这带来了时间上的节省,因为您的应用程序不必反复从数据库等慢速来源中获取这些信息。

现在我们来看看如何将缓存抽象添加到您的 Spring Boot 应用程序中,这将使您的应用程序运行更加顺畅。在上面的上下文中,缓存是可以用来快速显示不经常更改的书籍详情或用户评论的。

到这部分结束时,您将了解如何通过缓存使您的 Spring Boot 应用程序更快。这是您武器库中一个非常好的技能,用于开发更好、更快的应用程序。

理解缓存抽象

因此,让我们深入了解 Spring Boot 中的缓存抽象以及为什么它对应用程序的性能来说就像是一种超级能力。缓存抽象只是在某个特殊的内存空间中存储应用程序在任意应用程序中大量使用的一些信息。这样,应用程序就不必反复请求相同的信息——这可能会非常令人沮丧。

在 Spring Boot 中使用缓存抽象非常简单,并且能带来巨大的收益。例如,在我们的在线书店应用程序中,我们可以使用缓存来记住书籍的详情。通常情况下,每当有人想要查看一本书时,应用程序都必须从数据库中请求信息。有了缓存,当应用程序请求一本书的详情后,它会记住这些信息。因此,下次有人想查看那本书时,应用程序可以非常快速地显示详情,而无需再次回到数据库。这有助于使您的应用程序运行得更快,减少数据库的负载,并为您的用户提供更好的体验。

在本节的下一部分,我们将探讨如何在 Spring Boot 3.0 中轻松设置缓存以及它可以在您的应用程序中带来哪些不同。我们将通过一些实际步骤来展示如何在书店应用程序中集成缓存,以展示它如何加快那些不经常变化的功能的速度。如果您想构建高效且用户友好的应用程序,这是关键技术之一。

在应用程序中配置和使用缓存抽象

在本节中,我们将看到如何在 Spring Boot 3.0 中轻松实现缓存抽象,特别是在我们的书店应用程序中。缓存抽象不仅关乎性能提升,还关乎简化我们处理应用程序中频繁访问的数据。在 Spring Boot 3.0 中,这达到了前所未有的简单程度。

根据我们的书店应用程序条款,有效地使用缓存抽象意味着经常引用的数据,如书籍详情,可以在不反复击中数据库的情况下获得。从两个角度来看,这很重要:减少等待时间和减少服务器负载。

让我们看看在 Spring Boot 3.0 中实现缓存有多简单。启用项目中的缓存只需要两个简单的步骤:

  1. 首先,我们需要将库添加到 build.gradle 文件中:

    implementation 'org.springframework.boot:spring-boot-starter-cache'
    
  2. 接下来,我们将在主类上添加 @EnableCaching

    @SpringBootApplication
    @EnableCaching
    public class BookstoreApplication {
        public static void main(String[] args) {
            SpringApplication.run(BookstoreApplication.class, args);
        }
    }
    

就这样!我们现在可以在需要的地方使用缓存了。

让我们看看如何在书店应用程序的 BookController 类中实现缓存。控制器已经具有几个端点——用于添加、获取、更新和删除书籍。我们将专注于集成 Spring Boot 的缓存能力以优化这些操作。

使用 @CacheEvict 添加、更新和删除书籍

当添加新书或更新现有书籍时,确保我们的缓存反映了这些更改至关重要。在这里使用 @CacheEvict 注解来使缓存失效。这意味着缓存数据被删除或更新,确保后续请求获取最新的数据。

它们看起来是这样的:

@PutMapping("/{id}")
@CacheEvict(value = "books", allEntries = true)
public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book book) {...}
@PostMapping
@CacheEvict(value = "books", allEntries = true) // Invalidate the entire books cache
public ResponseEntity<Book> addBook(@RequestBody Book book) {...}

addBookupdateBook 方法中,@CacheEvict(value = "books", allEntries = true) 有效地清除了与书籍相关的所有条目的缓存。这种方法保证了缓存不会提供过时的信息。

类似地,当一本书被删除时,我们使用 @CacheEvict(value = "books", key = "#id") 来仅删除该特定书籍的缓存条目。这种有针对性的方法有助于我们保持缓存准确性,而不会影响其他缓存数据。

使用缓存进行高效检索

尽管提供的代码中没有明确显示,但获取操作(如 getAllBooksgetBook)可以使用 @Cacheable 进行优化。这个注解确保方法调用的结果被存储在缓存中,使得后续对相同数据的请求更快、更高效。

此外,我们还可以在存储库级别实现此功能。例如,我们可以引入一个查询并使其可缓存:

    @Override
    @Cacheable( "books")
    Optional<Book> findById(Long id);

我们不需要某个分析器来看到差异;只需运行应用程序,看看你将如何获得对第二次调用的更快响应。

总结来说,Spring Boot 3.0 中的缓存抽象是优化数据检索过程的一种强大且简单直接的方法。我们看到了在书店应用程序中实现它的简便性。通过利用缓存控制注解,如@CacheEvict@Cacheable,我们确保我们的应用保持响应性和高效性,并始终保持数据的准确性。

总之

在总结我们对 Spring Boot 中缓存抽象的探索时,我们了解到它在提升应用性能时为我们提供了显著的优点。我们注意到,缓存可以极大地提高数据的快速访问,尤其是像我们在线书店管理系统中的书籍详情这样的简单重复信息检索。我们从缓存抽象的实现中学习到,它不仅极大地减轻了数据库的负载,还提供了流畅、快捷的用户体验。

这次对缓存抽象的游览为我们提供了在当今快速发展的技术环境中绝对必要的实用工具。很明显,正确理解和使用缓存是开发 Spring Boot 中高效和响应性应用的重要关键。

接下来,我们将进入 Spring Boot 批处理的世界。我们将深入了解如何高效地处理大量数据,这对于旨在处理大量记录的应用程序来说是常见的。批处理是我们工具箱中的另一个关键工具,帮助我们从各个方面管理大规模数据,同时确保我们的应用能够很好地处理复杂任务,而不会被压垮。

Spring Boot 批处理

现在,让我们看看 Spring Boot 最重要的功能之一:批处理。我们将探讨如何使用 Spring Boot 有效地管理和处理大量数据。当你的应用程序需要处理像导入大型数据集或一次性对大量记录执行操作这样的任务时,批处理尤为重要。

在本指南的这一部分,我们将涵盖三个主要领域。首先,让我们讨论 Spring Boot 中的批处理,为什么它在我们的讨论一开始就如此关键,以及它如何成为任何企业或与大量数据操作相关的应用的变革者。接下来,我们将详细介绍批作业的设置和执行——这是高效处理大规模数据任务的关键方面。

最后,但也是最有意思的,我们将探讨如何在我们的在线书店项目中实际实现批处理。想象一下,如果必须将数千本书或出版商详情上传到系统中将有多么不切实际——批处理将使这项任务变得极其可行。如果你将这些概念应用到书店中,你将真正感受到批处理在现实应用程序(如批量导入书籍)中的工作方式。

到本节结束时,你将牢固掌握 Spring Boot 中的批处理,并能够有效地在实际用例中运用它。这是需要了解的关键内容,尤其是在开发需要为多种目的进行高吞吐量数据管理的 Web 应用程序时。让我们开始吧,看看批处理如何增强我们的 Spring Boot 应用程序的功能。

理解 Spring Boot 中批处理的作用

批处理是一种轻量级且非常有效的处理大量数据的方法。它有点像在你的应用程序中有一个超级高效的装配线,其中大数据任务被分解并分批处理,特别是如果你的应用程序需要一次性处理数千条记录的重型工作。

Spring Boot 中的批处理将使管理此类大规模数据操作成为可能。它有助于组织、执行和自动化许多现代应用程序所需的批量数据处理。大多数批处理作业是连续过程,而不是一次性过程。记住,我们必须每周或每天向我们的平台引入新书、出版商和作者。批处理过程将自动以简单的方式为我们处理这些任务。

你将了解 Spring Boot 中批处理的重要性;你将能够处理涉及处理大量数据集并执行它们的场景,而系统不会受到影响。这对于从事数据密集型应用程序开发的开发者来说是一项必备技能,以确保你的应用程序能够处理大型任务而不会显得力不从心。随着我们的深入,你将开始看到批处理是如何实现的,以及它可能对你的应用程序的性能和效率产生的影响。

实现 Spring Batch

在所有这些理论信息之后,我们必须亲自动手实践,以更好地学习。

在本节中,我们将学习如何在 Spring Boot 3.0 中设置批处理过程。我们将介绍一个独立的批处理仓库应用程序,逐步向您展示如何处理诸如为我们的在线书店进行批量书籍导入等任务。

创建批处理应用程序

我们将首先设置一个新的 Spring Boot 项目,专门用于批处理。我们可以在项目中使用以下依赖项:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'org.postgresql:postgresql'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

我们还添加了 PostgreSQL 依赖项,因为我们希望将大量数据导入 PostgreSQL 数据库。

介绍 Publisher 类

我们需要再次添加Publisher类,因为它位于data包下:

@Entity
@Table(name = "publishers")
@Data
public class Publisher {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String address;
}

我们将在批量处理中处理Publisher对象。因此,我们需要与我们在项目中使用的完全相同的对象。

配置批量作业

让我们在我们的应用程序中定义一个批量作业。这包括指定作业将采取的步骤,例如读取数据、处理数据,然后将结果写入。创建一个名为config的包,并创建一个BatchConfig.java文件。我们需要做的所有事情都将在这个文件中完成。

首先,我们需要了解这里的流程。我们的示例代码将有一个作业,但根据需求,我们可以定义多个作业。

这就是作业的样子:

  @Bean
    public Job importPublisherJob(JobRepository jobRepository, Step step1) {
        return new JobBuilder("importPublisherJob", jobRepository)
                .incrementer(new RunIdIncrementer())
                .start(step1)
                .build();
    }

如您所见,我们只有一个作业仓库和步骤。在我们的例子中,我们有一个步骤,但根据需求,我们可能有多个步骤。

让我们看看Step函数,因为它将向我们解释如何构建一个步骤:

@Bean
public Step step1(JobRepository jobRepository,
                  PlatformTransactionManager transactionManager,
                  ItemReader<Publisher> reader,
                  ItemProcessor<Publisher, Publisher> processor,
                  ItemWriter<Publisher> writer) {
    StepBuilder stepBuilder = new StepBuilder("step1",jobRepository);
    return stepBuilder.<Publisher, Publisher >chunk(10, transactionManager)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();
}

如您所见,一个步骤有一个readerprocessorwriter方法。这些方法的函数在它们的名称中是隐含的。这些函数基本上读取数据,如果需要,处理数据,对它进行一些处理,如设置地址名称值,然后将其写入仓库。让我们在下一节逐一查看它们。

读取、处理和写入数据

对于批量作业中的每个步骤,定义应用程序将如何读取、处理和写入数据。

在前面的代码示例中,您可以了解如何从逗号分隔值CSV)文件中读取发布者数据,将其处理以映射到您的发布者实体,然后将它写入数据库。

@Bean
public FlatFileItemReader<Publisher> reader() {
    return new FlatFileItemReaderBuilder<Publisher>()
            .name("bookItemReader")
            .resource(new ClassPathResource("publishers.csv"))
            .delimited()
            .names(new String[]{"name", "address"})
            .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{
                setTargetType(Publisher.class);
            }}).linesToSkip(1)
            .build();
}

reader中,我们定义了读取数据的位置以及如何解析数据。我们还将数据映射到我们的实体对象,在processor函数中,我们可以将其转换为所需的对象或对象。

@Bean
public ItemProcessor<Publisher, Publisher> processor() {
    return publisher -> {
        publisher.setAddress(publisher.getAddress().toUpperCase());
        publisher.setName(publisher.getName().toUpperCase());
        return publisher;
    };
}

这里是processor函数。我们可以在该函数中完成所有的处理步骤。例如,我已经将文本转换为大写。

@Bean
public JpaItemWriter<Publisher> writer() {
    JpaItemWriter<Publisher> writer = new JpaItemWriter<>();
    writer.setEntityManagerFactory(entityManagerFactory);
    return writer;
}

最后,这是writer对象;它从处理器获取处理后的数据并将其写入数据库。在下一步中,我们将执行我们的应用程序并讨论输出。

执行批量作业

一旦设置批量作业,它就会在事件或计划触发时运行。这有助于触发作业以启动对大数据集的处理和操作,这是一个非常高效的作业任务管理过程。

您可以在Resources文件夹中创建一个简单的 CSV 文件。我们可以将其命名为publishers.csv。这个名字应该与reader函数中的文件名匹配。示例数据如下:

名称,地址

发布者名称 1,地址 1

发布者名称 2,地址 2

您可以写尽可能多的行。我们可以运行我们的书店批量应用程序。我们将看到这些值已经被导入我们的 PostgreSQL 数据库作为处理后的数据(见图 4**.3)。

图 4.3 – 批量操作后的发布者表

图 4.3 – 批量操作后的发布者表

如我们在图 4**.3中看到的那样,在导入时值被转换为大写。

通过这样的批量处理,这可以正确地管理将通过我们的在线书店应用程序处理的大量数据任务。这也使得我们的数据处理高效,并且随着应用程序的运行,可以扩展以管理数据库中的大规模数据操作。

总结我们对 Spring Boot 3.0 中批量处理的探索,我们获得了关于高效处理大量数据集的宝贵见解。我们看到了如何将庞大的数据任务分解成可管理的块,这不仅简化了过程,而且有助于提高应用程序的性能。在我们的在线书店的背景下,批量处理已经证明了在管理大量数据,如批量出版商导入时,这一功能的重要性。

在这次旅程中,我们了解到批量处理不仅是一个技术需求,而且也是处理 Spring Boot 中智能密集型操作的重要战略方式。当与需要将处理大量数据作为其业务工作负载一部分的应用程序一起工作时,这种洞察力变得尤为重要,这些数据在后台定期和规律性地处理。

现在,随着我们进入下一部分,我们准备深入探讨数据迁移和一致性。我们已经看到了一些强大的策略,这些策略可以在不留下任何缝隙的情况下保持和演进我们应用程序的数据结构。确保我们应用程序处理数据不仅保持高效,而且随着时间的推移更加可靠和坚固,这是一个重要的方面。因此,让我们继续前进,准备好迎接新的挑战,从而加强我们对 Spring Boot 这些高级功能的掌握。

数据迁移和一致性

在这个关键部分,我们将探讨使用 Spring Boot 进行数据迁移和一致性。我们将讨论如何在不影响精度或造成问题的前提下,实际上迁移甚至修改我们应用程序中的关键数据。我们将详细介绍一些数据迁移的策略,并特别关注像 Liquibase 这样的工具,它能够实现此类过程的管理甚至自动化。

在此之前,我们将从数据迁移策略的介绍开始,并探讨为什么这些策略对于保持应用程序健康至关重要。然后,我们将继续使用 Liquibase 作为核心工具的实践步骤进行数据迁移实施。具体来说,我们将了解如何将 Liquibase 集成到项目中,并使用它来管理数据库变更。

这些策略将在我们的在线书店中得到实际应用。我们将看到如何通过应用数据迁移和一致性技术来添加新功能,这些技术可以保持现有数据的一致性和可靠性。让我们开始,解锁管理数据变更平稳高效的能力。

探索数据迁移策略和工具,如 Liquibase

在本节中,我们将深入探讨 数据迁移策略。我们将关注理解其重要性以及工具如 Liquibase 的重要性。数据迁移涉及将数据从一个系统移动到另一个系统,或从一个数据库版本移动到另一个版本,以安全、高效和可靠的方式进行。这是一个至关重要的过程,尤其是在更新或改进您的应用程序时。

Liquibase 是一个关键工具,就像一位熟练的建筑师,为您的数据迁移提供支持。它帮助管理数据库版本,跟踪变更,并在不同的环境中一致地应用这些变更。此工具使用简单的格式来定义数据库变更,使得跟踪和实施变更变得更加容易。

如果我们理解和应用了像 Liquibase 这样的数据迁移工具的策略,那么我们将能够非常有效地处理我们应用程序需求的变化。

当我们结束本节时,我们正准备深入到确保数据一致性的世界,这基于我们对迁移及其发生方式的理解。接下来的主题将关注在数据变更过程中维护数据完整性的技术,如前一个主题所述。请继续关注,我们将继续在 Spring Boot 应用程序数据管理错综复杂的领域中导航。

使用 Liquibase 实施数据迁移的实用步骤

当涉及到更新或更改您应用程序的数据库时,数据迁移是一个关键步骤。在本节中,我们将介绍实施数据迁移的实际步骤。我们将使用 Liquibase。我们将看到它是一个强大的工具,有助于管理数据库变更。

将 Liquibase 集成到您的项目中

第一步是将 Liquibase 添加到您的项目中。您可以打开您的书店应用程序,或者您可以在您的 Spring Boot 应用程序中遵循以下步骤来实施 Liquibase。由于我们从一开始就使用 Gradle,我们需要在 build.gradle 文件中添加一个依赖项:

implementation 'org.liquibase:liquibase-core'

这将导入您项目所需的所有库。

设置 Liquibase 配置

接下来,您需要在您的应用程序中配置 Liquibase。这包括指定数据库连接属性和变更日志文件的路径,该文件将包含您想要应用的所有数据库变更。在我们的应用程序中,我们将按照以下方式更新 application.properties 文件:

spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.yaml

由于我们已经在此处添加了数据库连接设置,我只是在提及与 Liquibase 相关的行。

创建 Liquibase 变更日志

Liquibase 的变更日志文件是您定义数据库变更的地方,例如创建新表、添加列或修改现有结构。变更以 XML、JSON、YAML 或 SQL 格式编写。以下是我们以 YAML 格式提供的示例:

databaseChangeLog:
  - includeAll:
      path: db/changelog/changes/

这里,我们使用了includeAll方法。这意味着检查路径,按字母顺序排序文件,并逐个执行它们。还有另一种方法,我们可以用include定义每个文件,Liquibase 将遵循此文件中的顺序,而不是文件夹中的文件。

执行迁移

一旦创建变更日志文件,Liquibase 就有能力在您的数据库上执行这些变更。这可以在应用程序启动时自动完成,或者通过手动运行 Liquibase 命令来完成。当执行发生时,变更日志被 Liquibase 读取,然后,按照databaseChangeLog中定义的顺序或数据库中的字母顺序,逐个执行变更。

按照这些步骤进行,将使你能够有效地在你的数据库项目中处理此类变更,并保持控制,从而降低在迁移过程中犯错的概率。当我们有随着时间的推移而演化的应用程序时——比如我们的在线书店——这种方法的这种做法变得至关重要,因为数据完整性和一致性至关重要。

接下来,我们将探讨如何在我们的书店应用程序中使用 Liquibase。

在在线书店中实施迁移策略

现在,让我们将我们关于数据迁移策略的知识应用到我们的在线书店项目中。这种实际实施将侧重于在整个过程中集成新功能和维护数据一致性。假设我们有一个新的需求,需要在books表中添加一个published列。我们需要在不破坏数据且不手动接触数据库服务器的情况下处理这个需求。当我们需要在另一个平台上运行我们的应用程序时,我们需要确保我们不需要手动处理数据结构;它将由应用程序处理。

设置 Liquibase 进行迁移

我们已经在我们的书店应用程序中介绍了 Liquibase 的依赖和配置。现在,我们将介绍一个变更日志文件。

让我们在resources/db/changelog/目录下创建一个名为changes的文件夹。这是 Liquibase 监听的文件夹。然后创建一个名为001-add-published-column.yaml的文件。命名很重要,有两个原因:正如我们之前提到的,Liquibase 将按字母顺序排序文件并相应地执行它们。我们需要保持这种排序的一致性,并且最新的变更始终需要放在列表的末尾。第二个原因是,当我们读取文件名时,我们需要理解它包含的内容。否则,当我们需要跟踪某些变更时,找到特定文件需要花费很长时间。

这里是一个示例 YAML 文件,用于向books表添加一个published列:

databaseChangeLog:
  - changeSet:
      id: 1
      author: ahmeric
      changes:
        - addColumn:
            tableName: books
            columns:
              - column:
                  name: published
                  type: boolean
                  defaultValue: false

在阅读内容时,你可以理解所有这些字段的意义。在这里,author是实现此变更的开发者的名字。它基本上添加了一个名为published的新列,默认值是false

这足以更改数据库中的表,但我们还需要通过更新我们的Book实体来确保它在我们的应用程序中保持对齐:

@Entity
@Table(name = "books")
@Data
public class Book {
    @Id @GeneratedValue
    private Long id;
    private String title;
    private String isbn;
    @ManyToMany
    private List<Author> authors;
    private Boolean published;
}

因此,当我们检索或保存数据时,我们将相应地管理数据库表。

执行迁移

准备好更改日志后,我们运行 Liquibase 来应用这些更改。此过程将在我们的数据库中的书籍表中创建新列,而不会干扰现有数据。这是经过仔细考虑的,以确保书店的用户不会出现停机或服务中断。

当你检查你的数据库时,你会看到新列已经被创建,正如你在图 4.4中可以看到的那样:

图 4.4 – 更新的书籍表

图 4.4 – 更新的书籍表

在我们进行这些更改的同时,我们持续确保数据一致性得到维护。这涉及到检查新数据是否与现有数据结构一致,并遵循所有完整性规则。

我们现在已经了解到,我们应该谨慎处理我们在线书店的新功能添加。仔细的数据迁移有助于新列顺利添加到books表中,从而保持数据的一致性和可靠性。这对于保持书店的时效性和对用户的有效性至关重要。

在本节中,我们获得了在应用开发中所需管理数据的知识和技能。这使我们为未来的挑战做好准备,因此有助于我们的应用程序在数字世界中保持相关性。

如 Liquibase 之类的工具使我们能够安全有效地更改我们的数据库。这对于在不损害现有功能的情况下更新我们的应用程序非常重要。

这些想法已经应用于在线书店,展示了理论如何在现实生活中得到实践。它使我们的应用在增长的同时保持准确性和可靠性。

这一部分信息量很大,并为我们提供了必要的技能和知识。这些对于任何 Spring Boot 应用程序开发者来说都是至关重要的。继续前进,这些关于数据管理的课程将证明是一个坚实的基础。它们将指导你开发不仅功能性强,而且数据结构坚固的应用程序。

摘要

随着我们进入本章的结尾,让我们回顾一下我们收集到的关键学习和见解。本章深入探讨了使用 Spring Boot 3.0 的数据管理世界,涵盖了任何接触此强大框架任何方面的开发者都至关重要的广泛主题。你现在应该已经掌握了 Spring Boot 中的数据管理功能,因为它们是构建健壮、高效和可扩展应用程序的基础。在本章完成后获得的技能不仅对后端开发至关重要,而且在应对现代应用开发带来的复杂性和不确定性时也非常有用。

下面是我们所涵盖的内容:

  • Spring Data 简介: 我们从 Spring Data 的基础开始,了解这种数据访问技术如何简化 Spring 应用程序中的数据访问。

  • Spring Data 中的 SQL 数据库: 我们还涉及了数据库集成,包括 PostgreSQL,以及设置简单数据源和包含多个数据源的数据源,以及如何处理复杂的关系实体。

  • Spring Boot 中的 NoSQL 数据库: 本章引导我们了解 NoSQL 数据库的集成,特别是 MongoDB,指出它们提供的灵活性和扩展选项。

  • 数据迁移和一致性: 我们深入探讨了数据迁移的策略,提到了 Liquibase 等工具,这些工具在确保数据完整性在转换过程中不被丢失时非常有用。

  • Spring Boot 中的缓存抽象: 这个主题真正让我们了解了缓存抽象,并提出了在寻求提高应用程序性能时其重要性。

  • Spring Boot 中的批处理: 我们探讨了批处理的概念,这在有效处理大量数据集时非常重要。

  • 实际应用: 我们在关于在线书店管理系统的真实项目中实际应用了这些概念,展示了在 Spring Boot 中描述的数据管理策略的具体实现。

随着本章的结束,请记住,学习路径和 Spring Boot 的掌握是一个持续的过程。技术变化相当频繁,跟上这些变化将大大有助于在开发应用程序方面变得有效。继续探索,继续编码,让本章的知识成为构建更复杂、更高效的 Spring Boot 应用程序的垫脚石。

第五章中,我们将学习高级测试策略。这些知识将帮助我们有效地进行应用测试时获得信心。我们将了解单元测试和集成测试之间的差异,测试应用反应式组件,以及确保应用功能的安全性。除此之外,实现将展示对 Spring Boot 生态系统中的测试驱动开发TDD)的广泛理解。

第五章:保护您的 Spring Boot 应用程序

欢迎来到您 Spring Boot 学习旅程的重要阶段。在本章中,我们将重点介绍安全性:这是一个至关重要的方面,将帮助您保护您的应用程序免受不断发展的数字威胁。在这里,您将学习如何使用 Spring Boot 3.0 实现强大的安全性,包括使用 Open Authorization 2.0OAuth2)、JSON Web TokenJWT)和 基于角色的访问控制RBAC)的技术。我们还将深入了解如何确保反应式应用程序的安全性。

您将学习如何使用 OAuth2 验证用户,并使用 JWT 管理安全令牌。您还将精通 RBAC,其任务是提供正确的访问权限给正确的用户。我们甚至为希望确保他们的反应式应用程序至少与标准 Web 应用程序一样安全的反应式开发者提供了一个专门的章节。

这为什么重要?在我们的数字世界中,安全性不是一个特性;它是一种生活方式。您将掌握的这些概念将帮助您构建安全可靠的应用程序,保护您的数据和用户身份。

最后,您将拥有一个安全、运行中的示例应用程序,该应用程序实现了所提到的所有安全原则。准备好使您的应用程序安全可靠了吗?让我们深入挖掘!

在本章中,我们将涵盖以下主题:

  • 在 Spring Boot 3.0 中引入安全性

  • 实现 OAuth2 和 JWT

  • 在 Spring Boot 中实现 RBAC

  • 保护反应式应用程序

让我们开始这段旅程,使您的 Spring Boot 应用程序既安全又稳健!

技术要求

对于本章,我们的本地机器需要以下软件:

在 Spring Boot 3.0 中引入安全性

在本章中,我们将深入研究 Spring Boot 3.0 的安全性方面。安全性不是一个复选框;它是构建任何应用程序的重要成分。在这里,我们将介绍 Spring Boot 预先集成的安全功能,以从零开始保护我们的应用程序。现在,让我们看看如何自定义和扩展所有这些功能,以满足我们的需求,确保我们不仅实现了一个功能性的应用程序,而且还是一个安全的应用程序。

首先,让我们探索 Spring Boot 的安全架构,它被构建得既强大又灵活。您将看到 Spring Boot 如何使保护应用程序变得简单,它提供了一些合理的默认设置,同时也允许您根据更高级的使用场景进行自定义。到本章结束时,您将意识到为什么安全性如此重要,以及 Spring Boot 3.0 提供了哪些工具来实现有效的安全性。

探索 Spring Boot 3.0 的安全特性

当我们开始构建任何类型的 Web 应用程序时,首先想到的便是确保它们的安全性。在这里,Spring Boot 3.0 提供了所有强大的工具来确保我们的应用程序安全。在本节中,我们将深入探讨如何在 Spring Boot 3.0 中构建一个安全架构,以帮助我们实现这一目标,确保我们的 Web 应用程序安全可靠。

Spring Boot 最棒的地方在于设置安全性的过程非常简单。它基于 Spring Security,这是一个用于保护一切的系统框架。将 Spring Security 想象成一个警惕的安全守卫,在门口检查每一个身份证件;这样,它确保只有拥有正确权限的人才能访问应用程序的特定部分。

以下是一些 Spring Boot 3.0 最重要的安全特性:

  • 在我们的应用程序中配置安全性:使用 Spring Boot 3.0 为您的应用程序设置安全性就像在您的移动设备上配置设置一样。您可以选择您想要开启什么,关闭什么。Spring Boot 允许我们非常容易地定义谁可以访问我们的应用程序中的什么内容。我们通过代码中的简单而强大的配置来实现这一点。

  • 用户认证:在最基本的意义上,认证是我们验证一个人身份的方式。Spring Boot 3.0 可以帮助我们完成这项工作。这可能通过用户名和密码、通过令牌或以其他方式实现,而 Spring Security 就在那里为您提供帮助。它就像在您的应用程序门口有一个保安,确保只有授权用户才能进入。

  • 授权:在确定一个人的身份之后,必须定义这个人可以做什么。Spring Security 允许我们为认证用户设置访问规则。这就像根据谁需要去哪里,给你的应用程序的不同门分发钥匙。如您所见,虽然认证是验证用户的身份,但授权决定了用户被允许访问哪些资源和执行哪些操作。

  • 防御常见威胁:互联网就像一个充满各种威胁的丛林。Spring Boot 3.0 的安全架构旨在保护这些威胁。从跨站脚本攻击(XSS)到 SQL 注入,Spring Security 帮助保护您的应用程序免受已知漏洞的侵害。

  • 利用高级安全特性:深入了解,Spring Boot 3.0 提供了另一套高级安全特性。它添加了 OAuth2 来保护 API 访问,以及JSON Web Tokens(JWTs)来进行无状态身份验证。这就像为你的应用程序添加了一个高级安全系统,比如摄像头和探测器。

到目前为止,我们已经理解和应用了 Spring Boot 3.0 的安全架构,我们现在可以开发健壮且安全的应用程序。我们已经看到了基础——从身份验证和授权到保护免受威胁,包括高级特性。这一切都是为了确保我们的应用程序是用户访问和互动的安全场所。

本节展示了 Spring Boot 3.0 在其安全领域下有一套强大的特性,旨在确保我们的应用程序安全。请记住,安全是一个持续的过程。持续监控和更新是必由之路。本章描述了持续监控和更新我们安全措施步骤。这种方法确保我们的应用程序随着时间的推移保持安全,适应新出现的挑战。

在本节中,我们学习了 Spring Boot 3.0 的基本安全特性。在下一节中,我们将开始查看实现 Spring Boot 安全性的代码。

设置基本安全配置

让我们逐步设置基本安全配置,确保你的应用程序从一开始就受到保护。以下指导将向你展示如何将安全层引入我们的示例书店应用程序,使其成为用户和数据的安全空间:

  1. build.gradle文件:

    implementation 'org.springframework.boot:spring-boot-starter-security'
    

    现在我们已经将所有必要的库添加到我们的项目中。

  2. 配置 Web 安全:接下来,我们将创建一个基本安全配置类:

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                    .csrf(AbstractHttpConfigurer::disable)
                    .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .authorizeHttpRequests(authz -> authz
                            .requestMatchers("/login").permitAll()
                            .anyRequest().authenticated()
                    )
                    .httpBasic(Customizer.withDefaults());
            return http.build();
        }
    }
    DENY response from this request because we have hidden the endpoints behind Spring Security.
    

    让我们了解我们通过这个SecurityConfig.java类引入了什么:

    • @Configuration: 这个注解将类标记为应用程序上下文 Bean 定义的来源。它告诉 Spring,这个类包含配置信息。

    • @EnableWebSecurity: 这个注解启用了 Spring Security 的 Web 安全支持,并为 Spring MVC 提供了集成。它向 Spring 框架发出信号,开始添加安全配置。

    • @Bean: 这个注解告诉 Spring,该方法将返回一个对象,该对象应注册为 Spring 应用程序上下文中的一个 bean。在这种情况下,它是SecurityFilterChain bean。

    • public SecurityFilterChain securityFilterChain(HttpSecurity http): 这个方法定义了安全过滤器链。它接受一个HttpSecurity实例作为参数,允许你为特定的 HTTP 请求配置基于 Web 的安全性。

    • .csrf(AbstractHttpConfigurer::disable): .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)): 这配置会话创建策略为无状态。在无状态 API 中,请求之间不存储任何会话信息在服务器上。这对于 REST API 来说是典型的,其中每个请求都是独立的,认证是通过令牌而不是 cookies 完成的。

    • .authorizeHttpRequests(authz -> authz: 这部分开始授权配置。

    • .requestMatchers("/login").permitAll(): 这行代码指定匹配/login模式的请求无需认证即可允许。这是在应用程序中定义公开端点的一种方式。

    • .anyRequest().authenticated(): 这确保了任何不匹配先前匹配器的请求都必须进行认证。这是一个通用的安全措施,默认情况下保护了应用程序的其余部分。

    • .httpBasic(Customizer.withDefaults()): 启用 HTTP 基本认证。这是一个简单、无状态的认证机制,允许客户端在每个请求中发送用户名和密码。Customizer.withDefaults()部分为 HTTP 基本认证应用默认配置,使设置变得简单直接。

此代码基本上设置了一个安全过滤器链,实际上禁用了 CSRF 保护,非常适合无状态应用程序。它为 REST API 提供了所需的无状态会话管理,并允许对少数 URL(如通过/login)进行公开访问。对于所有其他 URL,它强制通过 HTTP 基本认证进行认证。

在本节中,你学习了如何为你的 Spring Boot 应用程序实施基本的安全设置。但我们对安全的调查还没有结束。

通过了解和实施这些基本的安全配置,你正朝着创建安全可信的应用程序迈出重要步伐。始终牢记,一个安全的应用程序不仅意味着保护数据,还意味着在应用方面信任你的用户。

实施 OAuth2 和 JWT

接下来,我们将继续讨论应用程序安全的话题,现在我们将讨论一些针对不断变化的环境的更高级机制。这使我们来到了两种重要的技术:OAuth2 和 JWT。它们都是提高现代应用程序安全配置的关键参与者;然而,它们有不同的角色,相互补充以实现安全认证和授权的整体更大图景。

在后续章节中,我们提供了如何为 Keycloak 设置 OAuth2 的详细信息。我们详细介绍了 Keycloak 的 OAuth2 配置,随后是所需的代码片段。我们将使用 Keycloak,这是一个开源平台,完全支持现成的 OAuth2 协议,并具有广泛的定制能力,以提供身份和访问管理IAM)。

配置 Keycloak 的 OAuth2

进一步深入到高级安全领域,提高应用程序安全性的关键步骤之一是配置 OAuth2。我们将使用 Keycloak 进行 IAM。我们选择 Keycloak 是因为它开源,并且其设置过程非常简单。它是简化我们应用程序安全过程中复杂性的工具。它包括对 OAuth2 的内置支持,因此使得与用户身份管理以及保护用户对应用程序访问相关的所有事情都变得更容易。将 Keycloak 想象成一个已经非常了解您的用户的守门人,并确保只有那些拥有正确权限的人才能访问您应用程序的某些部分。

让我们开始逐步实施。我们将使用 Docker Compose 来运行 Keycloak,同时与我们的 PostgreSQL 和 MongoDB 设置一起。我们将更新当前的 docker-compose.yml 文件如下。您也可以在 GitHub 仓库中找到它:github.com/PacktPublishing/Mastering-Spring-Boot-3.0/blob/main/Chapter-5-implementing-oauth2-jwt/docker-compose.yml

version: '3.1'
services:
  db:
    image: postgres
    restart: always
    environment:
      POSTGRES_PASSWORD: yourpassword
      POSTGRES_DB: bookstore
    ports:
      - "5432:5432"
  mongodb:
    image: mongo
    restart: always
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_DATABASE: bookstore
  keycloak_db:
    image: postgres
    restart: always
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloakpassword
    ports:
      - "5433:5432"
  keycloak:
    image: bitnami/keycloak:latest
    restart: always
    environment:
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: admin
      DB_VENDOR: POSTGRES
      DB_ADDR: keycloak_db
      DB_PORT: 5432
      DB_DATABASE: keycloak
      DB_USER: keycloak
      DB_PASSWORD: keycloakpassword
    ports:
      - "8180:8080"
    depends_on:
      - keycloak_db

我们保留了我们的 MongoDB 和 PostgreSQL 设置不变,并引入了两个新的镜像:keycloak_dbkeycloak。让我们在这里分解参数:

  • image: postgres: 指定用于容器的 Docker 镜像。在这种情况下,它使用官方的 PostgreSQL 镜像。

  • restart: always: 此设置确保容器在停止时始终重新启动。如果 Docker 重新启动或容器由于任何原因退出,此设置将导致它重新启动。

  • 环境变量: 为容器定义环境变量。对于 keycloak_db,它将 PostgreSQL 数据库 (POSTGRES_DB) 设置为 keycloak,数据库用户 (POSTGRES_USER) 设置为 keycloak,以及用户的密码 (POSTGRES_PASSWORD) 设置为 keycloakpassword

  • ports: 将容器中的端口映射到主机机器。"5433:5432" 将容器内的默认 PostgreSQL 端口 (5432) 映射到主机的端口 5433。这允许您使用端口 5433 从主机机器连接到数据库。我们使用 5433 是因为我们已经在我们的应用程序的 PostgreSQL 数据库中使用了 5432

  • image: bitnami/keycloak:latest: 指定 Keycloak 服务器的 Docker 镜像,使用 bitnami/keycloak 镜像的最新版本。* restart: always: 与 keycloak_db 类似,确保 Keycloak 容器在停止时由于任何原因始终重新启动。* 环境变量: 为 Keycloak 服务器设置特定环境变量:

    • KEYCLOAK_USER: Keycloak 的管理员用户名 (admin)。

    • KEYCLOAK_PASSWORD: Keycloak 的管理员密码 (admin)。

    • DB_VENDOR: 指定正在使用的数据库类型(在本例中为 POSTGRES)。

    • DB_ADDR:数据库容器的地址。使用 keycloak_db 服务名称允许 Keycloak 在 Docker 网络中找到数据库。

    • DB_PORT:数据库监听的端口(5432)。

    • DB_DATABASE:Keycloak 应使用的数据库名称(keycloak)。

    • DB_USERDB_PASSWORD:Keycloak 将用于连接到数据库的凭据。* ports:将容器中的 Keycloak 服务器端口映射到主机机器。"8180:8080" 将内部端口 8080(Keycloak 的默认端口)映射到主机上的 8180。这允许您使用端口 8180 从主机访问 Keycloak 服务器。我们更改了原始端口,因为我们的 Spring Boot 应用程序正在使用端口 8080。* depends_on

    • keycloak_db:指定 keycloak 服务依赖于 keycloak_db 服务。Docker Compose 将确保在 keycloak 之前启动 keycloak_db

这种设置提供了一个强大且简单的方法来使用 Docker Compose 部署带有 PostgreSQL 数据库的 Keycloak。通过理解这些参数,您可以自定义您的设置以适应您的特定需求,例如更改端口、数据库名称或凭据。

当我们在包含此 docker-compose.yml 文件的目录中的终端中运行 docker-compose up 命令时,我们的四个服务(PostgreSQL、MongoDB、Keycloak 的 PostgreSQL 和 Keycloak 服务)将在我们的本地机器上启动并运行。在下一步中,我们将根据我们的需求配置 Keycloak 服务器。

配置 Keycloak 服务

如我们在 Docker Compose 文件中定义的那样,我们现在有了 Keycloak 服务器的 URL 和端口。打开一个网页浏览器,导航到 http://localhost:8180/。使用我们在 Docker Compose 文件中之前设置的管理员凭据登录以访问 Keycloak 管理控制台。在我们的示例中,用户名是 admin,密码是 admin。按照以下步骤,让我们配置一个 Keycloak 服务:

  1. 创建领域:

    1. 点击 BookStoreRealm

图 5.1:添加领域屏幕

图 5.1:添加领域屏幕

  1. 点击 创建

  2. 创建客户端:

    1. 在您的领域内,导航到 bookstore-client根 URL 到您的 Spring Boot 应用程序的 URL(对于我们的应用程序,它是 http://localhost:8080)。

图 5.2:客户端创建屏幕

图 5.2:客户端创建屏幕

  1. 在下一屏幕上,将客户端身份验证设置为 true,并在客户端创建流程的末尾点击保存按钮。

  2. 凭据 下,注意密钥,您将需要在 配置书店应用程序以 OAuth2 *部分的应用程序属性中使用它。

图 5.3:客户端凭据屏幕

图 5.3:客户端凭据屏幕

  1. 创建用户:

    导航到 用户,添加一个用户,并在 凭据 选项卡下设置密码。

图 5.4:用户凭据屏幕

图 5.4:用户凭据屏幕

现在,我们将有一个具有用户名、密码和我们的 Keycloak 客户端密钥的用户,这些将在下一节中使用。

重要提示

Keycloak 定期进行更新和改进,这可能会导致用户界面的变化。因此,UI 可能与本章中提供的描述和截图不同。

配置图书商店应用程序以使用 OAuth2

我们有一个 Keycloak 服务器,并且已经进行了配置;现在,我们需要配置我们的图书商店应用程序,以便它可以与 Keycloak 服务器通信:

  1. build.gradle 文件:

    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    
  2. application.properties:将以下属性添加到您的 application.properties 文件中,用您的实际 Keycloak 和客户端详细信息替换占位符:

    spring.security.oauth2.client.registration.keycloak.client-id=bookstore-client
    spring.security.oauth2.client.registration.keycloak.client-secret=<Your-Client-Secret>
    spring.security.oauth2.client.registration.keycloak.client-name=Keycloak
    spring.security.oauth2.client.registration.keycloak.provider=keycloak
    spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email
    spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
    spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/keycloak
    spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/auth/realms/BookStoreRealm
    spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/auth/realms/BookStoreRealm
    

    这些设置用于配置 Spring Boot 应用程序的 OAuth2 客户端注册和资源服务器属性。它们具体配置应用程序使用 Keycloak 作为身份验证提供者。让我们分解每个设置的含义:

    • spring.security.oauth2.client.registration.keycloak.client-id:这是在 Keycloak 中注册的 OAuth2 客户端的唯一标识符。在我们的案例中,bookstore-client 是代表我们在 Keycloak 服务器中的应用程序的 ID。

    • spring.security.oauth2.client.registration.keycloak.client-secret:此密钥用于通过 Keycloak 服务器验证客户端。它是一个只有应用程序和 Keycloak 服务器知道的机密字符串,充当密码。

    • spring.security.oauth2.client.registration.keycloak.client-name:客户端的人类可读名称,在我们的配置中是 Keycloak。

    • spring.security.oauth2.client.registration.keycloak.provider:指定此客户端注册的提供者名称。它设置为 keycloak,将此客户端注册链接到在属性文件中配置的 Keycloak 提供者。

    • spring.security.oauth2.client.registration.keycloak.scope:定义访问请求的范围。openidprofileemail 范围表示应用程序正在请求 ID 令牌以及访问用户的个人资料和电子邮件信息。

    • spring.security.oauth2.client.registration.keycloak.authorization-grant-type:指定要使用的 OAuth2 流。在这里,它设置为 authorization_code,这是一种安全且常用的获取访问和刷新令牌的方法。

    • spring.security.oauth2.client.registration.keycloak.redirect-uri:这是用户登录或登出后重定向到的 URI。{baseUrl} 是 Spring Security 替换为应用程序基本 URL 的占位符,确保重定向 URI 与应用程序的域名匹配。

    • spring.security.oauth2.client.provider.keycloak.issuer-uri:此 URL 指向您使用的域(BookStoreRealm)的 Keycloak 发行者 URI,通常是 Keycloak 的基本 URL 加上/auth/realms/{realm-name}。它告诉 Spring Boot 应用程序在哪里可以找到此域的 Keycloak 服务器。

    • spring.security.oauth2.resourceserver.jwt.issuer-uri:类似于提供者发行者 URI,此设置配置 JWT 发行者的发行者 URI。它被资源服务器(您的应用程序)用于验证 JWT。发行者 URI 必须与 JWT 中声明的发行者匹配,才能使令牌被视为有效。

    这些设置将我们的 Spring Boot 应用程序连接到使用 Keycloak 进行身份验证,指定了我们的应用程序应该如何在 Keycloak 中注册,它请求哪些作用域以及如何验证 Keycloak 签发的令牌。

  3. SecurityConfig 文件:我们需要更新SecurityConfig文件以使用 Keycloak 的 OAuth2 登录:

     @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                    .csrf(AbstractHttpConfigurer::disable)
                    .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .authorizeHttpRequests(authz -> authz
                            .requestMatchers("/login").permitAll()
                            .anyRequest().authenticated()
                    )
                    .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
            return http.build();
        }
    

    基本上,在这个代码中,我们只更改了build命令之前的最后一个语句。

    .oauth2ResourceServer(...)配置 OAuth2 资源服务器支持,.jwt(Customizer.withDefaults())表示资源服务器期望 JWT 进行身份验证。

    后者使用默认的 JWT 解码器配置,这对于大多数场景都是合适的。这一行对于与 OAuth2 集成至关重要,其中应用程序充当资源服务器,验证 JWT。

    此外,我们已将/login端点排除在受保护之外,因为此端点应该是公开的,以便用户可以获取凭证。这使我们转向引入一个具有/login端点的LoginController类。

  4. login类,它将用作此POST端点的请求体对象,以及一个LoginController类。让我们按照下面的方式编写它们。

    这是客户端到服务器的数据传输对象。此对象包含在 Keycloak 服务器中创建的用户登录凭证:

    public record LoginRequestDto(String username, String password) {}
    

    此外,我们还需要一个额外的配置文件来引入RestTemplate bean。我们将在LoginController类中使用它:

    @Configuration
    public class AppConfig {
        @Bean
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }
    

    这是引入到我们应用程序中的/login端点的LoginController类:

    @RestController
    public class LoginController {
        @Value("${spring.security.oauth2.client.registration.keycloak.client-id}")
        private String clientId;
    @Value("${spring.security.oauth2.client.registration.keycloak.client-secret}")
        private String clientSecret;
    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
        private String baseUrl;
        @PostMapping("/login")
        public ResponseEntity<?> login(@RequestBody LoginRequestDto loginRequestDto) {
            String tokenUrl = baseUrl + "/protocol/openid-connect/token";
            // Prepare the request body
            MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();
            requestBody.add("client_id", clientId);
            requestBody.add("username", loginRequestDto.username());
            requestBody.add("password", loginRequestDto.password());
            requestBody.add("grant_type", "password");
            requestBody.add("client_secret", clientSecret);
            // Use RestTemplate to send the request
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> response = restTemplate.postForEntity(tokenUrl, requestBody, String.class);
            // Return the response from Keycloak
            return ResponseEntity.ok(response.getBody());
        }
    }
    

    让我们了解当用户向/login端点发送POST请求时,这个控制器做了什么。

    这是我们第一次使用 Spring Boot 的@Value注解。这个注解将属性值注入到字段中。在这里,它被用来将 Keycloak 客户端 ID、客户端密钥和 Keycloak 服务器的 URL 从应用程序的属性文件中注入到clientId变量中。

    我们的应用程序基本上从请求体中获取用户的用户名和密码,并使用应用程序属性文件中的参数准备对 Keycloak 服务器的 REST 调用。它从 Keycloak 服务器获取响应并将响应返回给用户。为了演示它的工作原理,请参阅以下图示,图 5。5*。

图 5.5:使用 Keycloak 服务器的用户登录过程

图 5.5:使用 Keycloak 服务器的用户登录过程

如图中所示,用户发起一个调用。我们的应用程序准备请求,向 Keycloak API 发起一个 POST 调用,并将响应返回给用户。我们将在我们的应用程序调用中使用此访问令牌。我们将在下一节中测试这一点。

使用访问令牌测试我们的端点

让我们运行我们的应用程序。如您所记得,在我们之前的测试中,我们从应用程序收到了 HTTP 403 禁止 消息。现在,在从登录过程获取访问令牌后,我们将对其进行测试:

curl --location "http://localhost:8080/login"
--header "Content-Type: application/json"
--data "{
    \"username\":<username>,
    \"password\":<password>
}"

请将用户名和密码值更改为您在 Keycloak 服务器中创建的用户。您将收到以下类似的响应:

{
    "access_token": <JWT Token>
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": <JWT Token>,
    "token_type": "Bearer",
    "not-before-policy": 0,
    "session_state": "043d9823-7ef4-4778-b746-10dd8e75baa4",
    "scope": "email profile"
}

我没有在这里放置确切的 JWT,因为它是一个巨大的字母数字值。让我们解释这些值是什么:

  • access_token:这是一个客户端应用程序可以使用它通过在 HTTP 请求的授权头中传递它来访问受保护资源的 JWT。它是编码的,并包含有关用户身份验证和授权的声明(或断言)。令牌本身对客户端是透明的,但可以被资源服务器(或任何拥有适当密钥的实体)解码和验证。

  • expires_in:指定访问令牌的生存期(以秒为单位)。在此时间之后,访问令牌将不再有效,无法访问受保护资源。在此示例中,访问令牌在 300 秒(5 分钟)后过期。

  • refresh_expires_in:表示刷新令牌的生存期(以秒为单位)。刷新令牌可以在当前访问令牌过期时用于获取新的访问令牌。在这里,它设置为 1800 秒(30 分钟)。

  • refresh_token:这是另一个 JWT,与访问令牌类似,但仅用于获取新的访问令牌,而无需用户再次登录。它比访问令牌有更长的有效期,并且应该安全存储。

  • token_type:指定发行的令牌类型。在 OAuth2 中,这通常是 Bearer,这意味着持有此令牌的人有权访问资源。客户端应用程序在构造 HTTP 请求的授权头时应使用此令牌类型。

  • not-before-policy:此字段是 Keycloak 和类似授权服务器特有的。它表示一个策略或时间戳,在此时间戳之前,令牌不应被视为有效。0 的值通常表示令牌在发行时立即有效。

  • session_state:与令牌关联的用户会话的唯一标识符。应用程序可以使用此标识符进行会话管理或跟踪目的。

  • scope:指定请求的访问范围。范围由空格分隔,表示应用程序已被授予的访问权限。在这种情况下,email profile 表示应用程序可以访问用户的电子邮件地址和简介信息。

现在,我们将使用这个访问令牌,并在我们的请求头中使用它:

curl --location "http://localhost:8080/books"
--header "Authorization: Bearer <access_token>"

我们将获得一个书籍列表作为响应。您也可以使用相同的令牌对其他端点进行请求。在5分钟(300秒)后,访问令牌将过期,我们需要再次调用/login端点以获取新的访问令牌。

我们终于来到了本节的结尾。我们在本地运行了 Keycloak 服务器,并定义了一个新的领域、客户端和用户。随后,我们配置了我们的 Spring Boot 应用程序以与 Keycloak 服务器通信。经过所有这些配置和实现,我们能够使用我们的用户名和密码获取一个访问令牌,并从我们的受保护端点获得有效的响应。

在下一节中,我们将学习如何定义一个角色,并通过角色过滤请求的角色;这是使用基于角色的安全保护我们的端点的一个重要步骤。

在 Spring Boot 中实现 RBAC

在现代 Web 开发领域,确保用户只能访问他们有权访问的资源至关重要。这就是 RBAC 发挥作用的地方。想象一下,在您的应用程序中设置一系列门,每个门都需要只有某些用户才拥有的特定钥匙。这就是 RBAC 的本质——确保基于分配给用户的角色授予访问权限,同时增强安全和可用性。

为什么在您的 Spring Boot 应用程序中使用 Keycloak 优先考虑基于角色的访问控制(RBAC)?首先,它简化了复杂的访问管理任务,使开发人员更容易定义和执行安全策略。这使得您的应用程序能够利用 Keycloak 对 OAuth2 的出色支持,并提供了一种非常结构化、可扩展的方式来保护端点。这将使您的应用程序在控制用户访问时更加安全,其功能也更加清晰。当我们更深入地设置 Keycloak 中的 RBAC 时,请记住,这并不是关于限制本身;它更多的是关于为用户提供无缝和安全的体验,这意味着他们可以获得正确的工具和权限,有效地在您的应用程序中导航。让我们开始这段旅程,挖掘我们 Spring Boot 应用程序中基于角色安全性的全部潜力。

在 Keycloak 中定义角色和权限

在 Keycloak 中定义角色和权限对于建立提供非常流畅的用户访问管理功能的 secure 应用程序非常重要。通过这个过程,您将能够具体说明某个用户可以执行的操作,从而增强系统中的安全和生产力。以下是一个关于在 Keycloak 中设置角色和权限的简单指南,以及关于这种配置如何影响您的安全和管理的见解:

  1. 首先,使用您的管理员凭据登录 Keycloak 管理控制台(http://localhost:8180)。这是我们管理领域、用户、角色和权限的控制面板。

  2. 导航到 角色 部分——从下拉菜单中选择您希望配置的区域,然后在左侧菜单中点击 角色。在这里,您将看到现有角色的列表。

图 5.6:添加角色界面

图 5.6:添加角色界面

  1. 添加角色:点击 添加角色。输入用户角色的名称和描述,以帮助您识别角色在应用程序中的用途。

  2. 保存:点击 保存。您现在已创建了一个可以分配给用户的角色。

  3. 添加一个名为 admin 的角色。

  4. 前往 用户,选择上一步创建的用户,然后点击 角色映射

图 5.7:用户角色映射界面

图 5.7:用户角色映射界面

  1. 在这里,您可以给用户分配 用户 角色。选择角色并点击 分配

  2. 创建一个新用户,并将 admin 角色分配给此用户。

角色定义和分配后,让我们了解将创建的角色包含到应用程序中的过程。

为基于角色的访问定制书店应用程序

在您的 Spring Boot 书店应用程序中使用 Keycloak 实现 RBAC 显著增强了其安全性,确保用户只能根据其角色访问允许的内容。这不仅通过设计使您的应用程序更加安全,而且为用户权限的管理设置了一个非常稳固的框架。让我们按照以下步骤将基于角色的设置包含到您的应用程序中,其中引入了一个新类——KeycloakRoleConverter——以及特定的安全配置。新类将成为 Keycloak 角色和 Spring Security 角色之间的适配器。让我们一步一步地学习如何将这个结构应用到我们的应用程序中:

  1. /books/authorsPOST 请求向具有 ROLE_ADMIN 权限的用户发送,并配置 OAuth2 资源服务器使用自定义 JWT 认证转换器。打开您的安全配置类,并更新 securityFilterChain bean,如下所示:

    @EnableMethodSecurity
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authz -> authz
                    .requestMatchers("/login").permitAll()
                    .requestMatchers(HttpMethod.POST, "/books", "/authors").hasAuthority("ROLE_ADMIN")
    .requestMatchers(HttpMethod.GET, "/books/**","/reviews/**", "/authors/**", "/publishers/**").hasAnyAuthority("ROLE_USER","ROLE_ADMIN")
                    .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakRoleConverter())));
            return http.build();
        }
    }
    
  2. KeycloakRoleConverter :这个类至关重要,因为它将 Keycloak JWT 转换为 Spring Security 的认证结构。这个自定义转换器从 JWT 中提取角色,并将它们作为 Spring Security 上下文中的权限分配:

    public class KeycloakRoleConverter implements Converter<Jwt, AbstractAuthenticationToken> {
        @Override
        public AbstractAuthenticationToken convert(Jwt jwt) {
            // Default converter for scopes/authorities
            JwtGrantedAuthoritiesConverter defaultAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
            Collection<GrantedAuthority> defaultAuthorities = defaultAuthoritiesConverter.convert(jwt);
            // Extract realm_access roles and map them to GrantedAuthority objects
            Collection<GrantedAuthority> realmAccessRoles = extractRealmAccessRoles(jwt);
            // Combine authorities
            Set<GrantedAuthority> combinedAuthorities = new HashSet<>();
            combinedAuthorities.addAll(defaultAuthorities);
            combinedAuthorities.addAll(realmAccessRoles);
            return new AbstractAuthenticationToken(combinedAuthorities) {
                @Override
                public Object getCredentials() {
                    return null;
                }
                @Override
                public Object getPrincipal() {
                    return jwt.getSubject();
                }
            };
        }
        public static List<GrantedAuthority> extractRealmAccessRoles(Jwt jwt) {
            Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
            if (realmAccess == null) {
                return Collections.emptyList();
            }
            List<String> roles = (List<String>) realmAccess.get("roles");
            if (roles == null) {
                return Collections.emptyList();
            }
            return roles.stream()
                    .map(roleName -> new SimpleGrantedAuthority("ROLE_" + roleName.toUpperCase()))
                    .collect(Collectors.toList());
        }
    }
    

我们引入了一个实现 Converter 接口的类,将 JWT 转换为 AbstractAuthenticationToken 类,这是 Spring Security 用于认证信息的一个概念。通过这些更改,我们的书店应用程序现在拥有一个非常安全和强大的基于角色的访问控制系统。它只允许具有正确角色的认证用户执行特定操作——这显著提高了应用程序的安全性和完整性。此外,该设置非常细致,它提供了对用户权限的最佳控制,并简化了应用程序中访问权限的管理。

我们现在可以测试我们的应用程序了。当我们使用user角色登录并发出对/booksGET请求时,我们会得到一个成功的响应,但当我们尝试对/books发出POST请求并创建一本新书时,响应将是forbidden。如果我们使用admin角色登录,无论请求是GET还是POST,我们都会一直成功。在我们的 Spring Boot 书店应用程序的 RBAC(基于角色的访问控制)领域确保安全立足点将为我们扩展安全架构开辟一条有利之路。

我们旅程的下一部分是确保反应式应用程序的安全。向前推进,本章将讨论所有安全、身份验证和授权原则如何应用于编程的反应式上下文中。这不仅将帮助我们扩大对实际中可利用的安全方法的了解范围,还将为我们提供帮助,以提供足够的保护来保护我们的反应式应用程序。我们将深入了解反应式安全,以使我们的应用程序具有应对不断发展的网络威胁所需的弹性。

确保反应式应用程序的安全

在软件开发的动态环境中,确保反应式应用程序的安全带来了一组新的挑战和机遇。在更深入地了解了反应式编程的世界之后,我们现在需要调整我们的安全策略,以适应这些应用程序的非阻塞、事件驱动的特性。那些被识别出来处理大量并发数据流的自反应系统强烈要求采取一种强大而灵活的安全方法。本节将尝试揭示确保反应式应用程序的复杂性,并将指导您通过必要的步骤和考虑,以有效地保护您的反应式生态系统。

我们将探讨如何使用 Spring Security 提供的反应式支持来启用这些安全功能,同时不破坏反应式原则,确保高响应性和弹性。

转向反应式编程领域可能意味着在安全方面的一个挑战,尤其是考虑到 Spring Security 提供的强大功能。使用 Spring Security 实现反应式安全实际上是将经典的安全范式适应于反应式应用程序的异步、非阻塞模型。这种重新定位可以描述为一种转变,不是任何技术变化的转变,而是在安全过程与数据流和用户请求交互的方式上的转变。在这个环境中,安全模型需要是功能性的,而不寻求与反应式原则有任何妥协,也不对其施加瓶颈。

在确保响应式应用的安全性方面,一个关键的区别在于认证和授权的方式。与通常将安全上下文绑定到线程本地的基于 servlet 的传统应用不同,响应式安全性必须能够处理这种解耦、无状态的反应式编程特性。Spring Security 提供了一个响应式安全上下文,它被限制在响应流中,确保安全决策在上下文感知的方式上与应用的流程保持一致。

在本系列中,当我们探讨 Spring Security 的响应式部分时,我们将了解如何有效地在我们的应用中包含它们以实现响应式系统的安全性。这包括处理 Spring Security 提供的响应式 API、理解谁作为发布者来做出安全决策(MonoFlux),并确保您的应用保持安全、响应式和可扩展。本指南通过实际示例和逐步方法,将帮助您导航响应式安全性的复杂性,确保应用不仅安全,而且符合响应式编程模型所设定的性能和扩展期望。

让我们开始将安全性实现到响应式应用中。我们将使用我们在 第三章 中开发的项目:

  1. build.gradle 文件:

    implementation 'org.springframework.security:spring-security-config'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    
  2. application.properties:将以下属性添加到您的 application.properties 文件中,用您的实际 Keycloak 和客户端详细信息替换占位符:

    spring.security.oauth2.client.registration.keycloak.client-id=bookstore-client
    spring.security.oauth2.client.registration.keycloak.client-secret=<Your-Client-Secret>
    spring.security.oauth2.client.registration.keycloak.client-name=Keycloak
    spring.security.oauth2.client.registration.keycloak.provider=keycloak
    spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email
    spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
    spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/keycloak
    spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/auth/realms/BookStoreRealm
    spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/auth/realms/BookStoreRealm
    
  3. 在您的 SecurityConfig 类中指定授权规则并设置 JWT 转换器以提取角色的 SecurityWebFilterChain 类:

    @Configuration
    public class SecurityConfig {
        @Bean
        public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
            http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .authorizeExchange(exchanges -> exchanges
                    .pathMatchers("/login").permitAll()
                    .pathMatchers(HttpMethod.POST, "/users").hasAuthority("ROLE_ADMIN")
                    .pathMatchers(HttpMethod.GET, "/users/**").hasAnyAuthority("ROLE_ADMIN", "ROLE_USER")
                    .anyExchange().authenticated()
                )
                .oauth2ResourceServer(oauth2ResourceServer ->
                    oauth2ResourceServer.jwt(jwt ->
                        jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
                );
            return http.build();
        }
        private Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
            ReactiveJwtAuthenticationConverter jwtConverter = new ReactiveJwtAuthenticationConverter();
            jwtConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
            return jwtConverter;
        }
    }
    
  4. KeycloakRoleConverterKeycloakRoleConverter 类对于将 Keycloak 角色映射到 Spring Security 权限至关重要:

    public class KeycloakRoleConverter implements Converter<Jwt, Flux<GrantedAuthority>> {
        @Override
        public Flux<GrantedAuthority> convert(final Jwt jwt) {
            // Extracting roles from realm_access
            return Flux.fromIterable(getRolesFromToken(jwt))
                    .map(roleName -> "ROLE_" + roleName.toUpperCase()) // Prefixing role with ROLE_
                    .map(SimpleGrantedAuthority::new);
        }
        private List<String> getRolesFromToken(Jwt jwt) {
            Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
            if (realmAccess == null) {
                return Collections.emptyList();
            }
            List<String> roles = (List<String>) realmAccess.get("roles");
            if (roles == null) {
                return Collections.emptyList();
            }
            return roles;
        }
    }
    
  5. token 处理身份验证请求的 LoginController 类,利用 Keycloak 的 token 端点:

    @RestController
    public class LoginController {
        @Value("${spring.security.oauth2.client.registration.keycloak.client-id}")
        private String clientId;
        @Value("${spring.security.oauth2.client.registration.keycloak.client-secret}")
        private String clientSecret;
        @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
        private String baseUrl;
        @PostMapping("/login")
        public Mono<ResponseEntity<?>> login(@RequestBody LoginRequestDto loginRequestDto) {
            // URL for Keycloak token endpoint
            String tokenUrl = baseUrl + "/protocol/openid-connect/token";
            // Prepare the request body
            MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();
            requestBody.add("client_id", clientId);
            requestBody.add("username", loginRequestDto.username());
            requestBody.add("password", loginRequestDto.password());
            requestBody.add("grant_type", "password");
            requestBody.add("client_secret", clientSecret);
            // Use RestTemplate to send the request
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> response = restTemplate.postForEntity(tokenUrl, requestBody, String.class);
            // Return the response from Keycloak
            return Mono.just(ResponseEntity.ok(response.getBody()));
        }
    }
    

现在我们已经将基于角色的安全过滤器引入到我们的响应式应用中。几乎所有内容都与传统应用非常相似,只是响应是在响应式领域,例如 MonoFlux。我们可以使用我们之前的 curl 脚本进行登录,为 useradmin 角色获取访问令牌,并且我们可以测试我们的 POSTGET 端点。

这意味着将响应式安全性集成到 Spring Boot 应用中需要故意配置一系列自定义实现,特别是使用 OAuth2 和 Keycloak。从设置依赖项和配置属性到安全过滤器链,最后到自定义角色转换器——每一步都进行了描述,以便可以遵循以获得我们书店应用的安全响应式环境。这种实现不仅利用了响应式编程的非阻塞特性,而且还确保我们的应用在响应式环境中既安全又可扩展,能够高效地处理需求。

摘要

通过这一点,我们将结束本章,本章主要讨论了如何确保 Spring Boot 应用程序的安全性。我们现在已经通过了解上下文和所需的安全工具集,有效地保护我们的应用程序,完成了对 Spring Boot 安全性的探索。让我们总结本章的关键学习内容:

  • 理解 Spring Boot 安全性:我们理解了保护 Spring Boot 应用程序的需求和 Spring Security 的基本原则。

  • 使用 OAuth2 实现:我们学习了如何使用 OAuth2 验证用户并使用 JWT 管理安全令牌。

  • 使用 Keycloak 进行 RBAC:我们详细展示了如何配置 Keycloak 来管理我们系统中的角色和权限,从而增强我们应用程序的安全结构。

  • 针对反应式的修改安全配置:我们详细阐述了如何针对反应式编程模型定制安全配置,以便我们的应用程序既能保证安全又能具备能力。

  • 使用 Spring Security 实现反应式安全:为了在反应式环境中实现安全性,需要必要的差异和修改。本章强调了非阻塞和事件驱动的安全机制。

本章简要概述了 Spring Security 的内容。下一章,高级测试策略,将进一步深入 Spring Boot 生态系统,这次将深入探讨测试。本章将更深入地探讨单元测试和集成测试之间的差异,指出测试反应式组件的挑战,进而讨论安全功能测试,并简要介绍 Spring Boot 3.0 的测试驱动开发(TDD)方面。从确保安全性到测试我们的应用程序的这一进展,真正强调了在开发能够满足现代应用开发需求的健壮、高质量软件时需要采取的全面方法。

第六章:高级测试策略

本章将进一步引导我们进入复杂测试方法的领域,并提供清晰的指南,以确保我们的软件可靠且健壮。它将涵盖一系列主题:从考虑测试驱动开发TDD)的基本原理到具体内容,如考虑安全因素的单元测试网络控制器、应用程序不同部分的集成以及反应式环境测试的独特挑战。这将使你成为一个更好的开发者,能够编写覆盖所有代码的测试。这些技术为确保质量改进提供了坚实的基础,并且这些改进适用于各种软件架构 – 从经典网络应用程序到现代软件架构的反应式系统。

换句话说,通过学习这些测试策略,你不仅是为了捕捉错误或防止错误,而是为了应对现代软件开发的需求。本章概述了创建高性能、可扩展和可维护应用程序所需的一切。随着我们继续阅读本章,你将了解何时以及如何自信地应用这些测试技术,无论你的应用程序或其架构的复杂性如何。本章将为你提供必要的信息和工具,以成功地穿越不断变化的软件管理世界。

在本章中,我们将涵盖以下内容:

  • Spring Boot 中的 TDD

  • 带有安全层的控制器单元测试

  • 集成测试 – 将组件连接起来

  • 测试反应式组件

让我们开始这段学习之旅,了解如何使你的 Spring Boot 应用程序安全且健壮!

技术要求

对于本章,我们需要在我们的本地机器上设置一些配置:

Spring Boot 中的 TDD

当我第一次接触到 TDD 的概念时,我承认我相当怀疑。我觉得在编写代码之前先写单元测试的想法似乎很荒谬,或者说,疯狂。我和其他人一样,觉得这只是一个额外的过程,会减缓已经非常繁忙的开发周期。但现在,在探索了使用 Spring Boot 3.0 进行应用程序开发中的 TDD 之后,我知道情况并非如此。

Spring Boot 3.0 是一个非常出色的平台,可以用于基于 TDD 的工作。我刚刚接手了一个新项目,并开始基于 TDD 的概念前进。这个过程本身至少是尴尬的。它就像通过为不存在的代码编写测试来预先判断未来。然而,我继续这样做。单元测试实际上以我以前从未见过的方式推动了代码的编写。每个测试都有一个明确和定义的目的,以及编写满足它的代码,这使得每个相关的发展方法都变得专注和经过深思熟虑。

其目的是捕捉早期错误,并使代码库有组织且易于维护。以这种方式编写测试形成了一个通过(编写失败的测试)、绿(使测试通过)和重构(清理代码)的循环。它推动着项目前进。在编写测试时投入的时间通过减少后续的调试和修正错误代码而得到回报。

现在,让我们实际操作,将理论应用到我们的书店应用中。在本节中,我们将练习 TDD,构建应用中的一个功能。你将学习如何首先编写测试,然后根据功能通过测试。所有这些实践经验旨在为你提供一个坚实的基础,而不仅仅是理论上的知识,在 Spring Boot 3.0 的应用开发中。

这当然是为了让你在使用 TDD 时感到舒适和自信。让我们开始吧!

实施 TDD

在本节中,我们将开始一个新的 TDD 之旅。作为这项任务的一部分,我们对书店应用有一个新的需求:在控制器和Author流程的存储库之间引入一个额外的服务层。控制器有两个 GET 方法,一个 PUT 方法,一个 POST 方法和一个 DELETE 方法。要求是创建一个名为AuthorService.java的类,提供控制器类需要的方法,并在DELETE过程中在数据库中找不到Author时抛出EntityNotFound异常。让我们通过使用 TDD 方法来完成这个任务:

  1. src/test/java文件夹下的AuthorServiceTest.java。首先,我们将为潜在的getAuthor方法编写第一个测试。然而,当我们开始编写测试方法时,我们会看到我们还没有服务类,我们将创建一个名为AuthorService.java的空服务类。但当我们尝试自动完成getAuthor方法时,我们会看到没有这样的方法。因此,我们将在服务类中创建一个新的方法,如下所示:

    public Optional<Author> getAuthor(Long id) {
    return Optional.empty();
    }
    

    如你所见,这种方法几乎是空的。然而,我们知道我们将有一个名为getAuthor的方法,接受Id作为参数,并返回Optional<Author>。在所有测试中,我们需要为这个测试准备环境,例如创建所需的数据。因此,我们将authorRepository类注入到服务和测试类中。现在,我们可以编写第一个测试用例:

    @Mock
    private AuthorRepository authorRepository;
    @Test
    void givenExistingAuthorId_whenGetAuthor_thenReturnAuthor() {
    Publisher defaultPublisher = Publisher.builder().name("Packt Publishing").build();
    Author savedAuthor = Author.builder()
                    .id(1L)
                    .name("Author Name")
                    .publisher(defaultPublisher)
                    .biography("Biography of Author")
                    .build();
        when(authorRepository.findById(1L)).thenReturn(Optional.of(savedAuthor));
        Optional<Author> author = authorService.getAuthor(1L);
        assertTrue(author.isPresent(), "Author should be found");
        assertEquals(1L, author.get().getId(), "Author ID should match");
    }
    @Mock: This mocks the class and allows us to manipulate all its methods.
    
  2. when:这有助于我们操作模拟方法的返回对象。在我们的示例中,它模拟了当调用 authorRepository.findById() 时,它将始终返回 savedAuthor

  3. assertTrue:这个断言断言方法返回 true。

  4. assertEquals:这个断言断言提供的两个值是相等的。

当我们运行 givenExistingAuthorId_whenGetAuthor_thenReturnAuthor 方法时,测试将失败,因为我们的方法始终返回 Optional.empty()。所以,我们手头有一个失败的测试;让我们去修复它。

  1. 绿色(使测试通过):为了通过这个测试,我们需要在我们的方法中使用仓库类:

    public Optional<Author> getAuthor(long id) {
           return authorRepository.findById(id);
    }
    

    现在,它已经完成了。当我们再次运行测试时,我们会看到它会通过。

  2. 重构(清理代码):在这个步骤中,我们需要检查测试和源类,看看它们是否需要重构。在我们的例子中,在源类中我们不需要任何重构,但在测试方面,我们可能需要稍微整理一下。我们可以从测试类中移除对象创建,使测试用例更易于阅读。此外,我们可以在未来的其他测试类中重用该对象:

    private Author savedAuthor;
    @BeforeEach
    void setup() {
     Publisher defaultPublisher = Publisher.builder().name("Packt Publishing").build();
     savedAuthor = Author.builder()
             .id(1L)
             .name("Author Name")
             .publisher(defaultPublisher)
             .biography("Biography of Author")
             .build();
    }
    

    我们引入了一个 setup 方法来设置公共变量,以减少测试类中的代码重复。

    在引入 setup 方法之后,我们的测试方法变得更加清晰,代码行数更少:

    @Test
    void givenExistingAuthorId_whenGetAuthor_thenReturnAuthor() {
    when(authorRepository.findById(1L)).thenReturn(Optional.of(savedAuthor));
        Optional<Author> author = authorService.getAuthor(1L);
        assertTrue(author.isPresent(), "Author should be found");
        assertEquals(1L, author.get().getId(), "Author ID should match");
    }
    

    我们已经完成了对作者服务的 TDD 迭代。我们需要对所有方法进行相同的迭代,直到我们有一个成熟的 AuthorService,可以在控制器类中使用。

    在这些过程结束时,我们将拥有 GitHub 仓库中的 AuthorServiceTest。然而,我们还将有一些新的单元测试术语,我们将在 讨论单元测试术语 部分中进行讨论。

  3. 我们已经留下了一步最后的工作:将 AuthorController 类更新为使用这个新的 (AuthorService) 服务,而不是使用仓库。

    我们需要将 AuthorService 类注入到控制器类中,并使用 AuthorService 中的方法,而不是使用仓库中的方法。

    你可以在 GitHub 仓库中看到更新的 AuthorControllergithub.com/PacktPublishing/Mastering-Spring-Boot-3.0/blob/main/Chapter-6-unit-integration-test/src/main/java/com/packt/ahmeric/bookstore/controller/AuthorController.java)。我想在这里提到 delete 方法:

        @DeleteMapping("/{id}")
        public ResponseEntity<Object> deleteAuthor(@PathVariable Long id) {
            try {
                authorService.deleteAuthor(id);
                return ResponseEntity.ok().build();
            } catch (EntityNotFoundException e) {
                return ResponseEntity.notFound().build();
            }
        }
    }
    

    如你所见,我们已经替换了方法,在 delete 函数中,我们添加了一个异常处理程序来覆盖 EntityNotFoundException

在本节中,我们学习了 TDD(测试驱动开发)以及如何在现实世界的示例中实现它。这需要一些时间,并且需要一些耐心,使其成为开发周期中的习惯。然而,一旦你学会了如何进行 TDD,你将会有更少的错误和更易于维护的代码。

讨论单元测试术语

让我们讨论一些单元测试中的基本术语:

  • assertThrows(): 这是一个在 JUnit 测试中使用的用于断言在执行代码片段期间抛出特定类型的异常的方法。当您想测试您的代码是否正确处理错误条件时,它特别有用。在我们的测试类中,您可以看到如下:

    assertThrows(EntityNotFoundException.class, () -> authorService.deleteAuthor(1L));
    

    此方法接受两个主要参数:预期的异常类型和一个功能接口(通常是 lambda 表达式),其中包含预期抛出异常的代码。如果指定的异常被抛出,则测试通过;否则,测试失败。

  • verify(): Mockito 是我们始终在单元测试中使用的库。它有一些非常有用的方法,使我们的测试更加可靠和可读。verify()是其中之一;它用于检查与模拟对象发生的某些交互。它可以验证方法是否以特定的参数、特定次数调用,甚至从未被调用。这对于测试您的代码是否按预期与依赖项交互至关重要。在我们的测试类中,您可以看到如下:

    verify(authorRepository).delete(savedAuthor);
    verify(authorRepository, times(1)).delete(savedAuthor);
    
  • @InjectMocks: Mockito 中的@InjectMocks注解用于创建类的实例并将模拟字段注入其中。这在您有一个依赖于其他组件或服务的类时特别有用,您想通过使用其依赖项的模拟版本来单独测试该类。以下代码片段展示了示例用法:

    @InjectMocks
    private AuthorService authorService;
    
  • @ExtendWith(MockitoExtension.class): 此注解与 JUnit 5 一起使用,以在测试中启用 Mockito 支持。通过在类级别声明@ExtendWith(MockitoExtension.class),您允许 Mockito 在测试运行之前初始化模拟并注入它们。这使得编写更干净的测试代码并减少样板代码变得更容易。您可以在我们的测试类中看到它:

    @ExtendWith(MockitoExtension.class)
    public class AuthorServiceTest
    {}
    
  • @BeforeEach: 在 JUnit 5 中,@BeforeEach注解用于方法上,指定它应该在当前测试类中的每个测试方法之前执行。它通常用于所有测试都通用的设置任务,确保每个测试从一个新鲜的状态开始。它与方法一起使用,正如您在以下代码中可以看到的:

    @BeforeEach
    void setUp() {
       // common setup code
    }
    

既然我们已经了解了单元测试中的新术语,在下一节中,我们将利用我们的单元测试知识,通过学习如何测试控制器类,特别是带有安全层的控制器类来提高它。

带有安全层的控制器单元测试

在这个新章节中,我们将处理测试控制器类。为什么我们为控制器类采用不同的测试方法?请求和响应可以表示为 JSON 对象,类似于真实的请求和响应,而不是我们项目中的对象。这将帮助我们检查是否一切正常以接受请求,并在它们符合要求时断言 JSON 响应。我们将讨论一些新的注解,接下来,我们将逐步关注如何为AuthorControllerTest实现这些新注解。

Spring MVC 控制器测试的关键注解

Spring 的@WebMvcTest@Import@WithMockUser@MockBean是 Spring MVC 控制器测试中的关键角色。这些注解帮助建立测试框架,确保我们的控制器无论在独立测试还是与 Spring 的 Web 上下文和安全组件集成时都能按预期执行。让我们来看看它们:

  • @WebMvcTest(AuthorController.class)@WebMvcTest注解用于以更集中的方式对 Spring MVC 应用程序进行单元测试。它应用于需要测试 Spring MVC 控制器的测试类。使用@WebMvcTest与特定的控制器类,如AuthorController.class,告诉 Spring Boot 仅实例化给定的控制器及其所需依赖项,而不是整个上下文。这使得测试运行更快,并且严格关注 MVC 组件。此注解会自动为测试配置 Spring MVC 基础设施。

  • @Import(SecurityConfig.class)@Import注解允许您将额外的配置类导入 Spring 测试上下文。在控制器测试中,尤其是在与@WebMvcTest一起使用时,通常需要包含特定的配置类,这些配置类不是由@WebMvcTest自动获取的。通过指定@Import(SecurityConfig.class),您明确告诉 Spring 加载您的SecurityConfig类。此类包含安全配置(如身份验证和授权设置),这对于测试在接近您应用程序安全设置的环境中运行是必要的。

  • @MockBean:Spring 应用程序上下文使用诸如 Service 和 Repository 之类的 bean,在我们的测试上下文中,我们需要模拟这些 bean。@MockBean将模拟对象添加到 Spring 应用程序上下文中,这些模拟对象将用于替代真实的 Service 和 Repository 对象。这对于注入模拟实现服务、存储库或控制器所依赖的任何其他组件非常有用,而无需从真实的应用程序上下文中实际加载这些 bean。

  • @WithMockUser:这个注解用于 Spring Security 测试,用于模拟使用模拟认证用户运行测试。此注解允许您指定模拟用户的具体细节,例如用户名、角色和权限,而无需与实际的安全环境或认证机制交互。它特别适用于控制器测试,您希望测试端点在不同认证或授权场景下的行为。通过使用 @WithMockUser,您可以轻松模拟不同的用户上下文,测试应用程序如何响应不同的访问级别,并确保安全约束得到正确执行。这使得它成为全面测试 Spring Boot 应用程序中受保护端点的必备工具。

对于控制器测试,尤其是在有安全层的情况下,这些注解在确保您的测试专注且快速,尽可能反映应用程序的实际运行条件方面发挥着关键作用。在下一节中,我们将将这些注解实现到测试类中。

使用 Spring 注解构建控制器测试

当我们开始为控制器创建测试时,使用 Spring 注解非常重要。这些注解,例如 @WebMvcTest@Import@WithMockUser@MockBean,对于建立与我们的应用程序的 Web 层和安全设置相匹配的测试环境至关重要。本节重点介绍如何利用这些注解来为我们的控制器开发有针对性的测试。通过整合这些工具,我们的目标是平衡测试的速度和准确性,以确保我们的控制器在 Web 环境中有效运行。让我们探讨如何实际应用这些注解来模拟现实场景,并验证我们的 Spring MVC 控制器在某些情况下的功能。

在 Spring Boot 应用程序中为 AuthorController 创建一个全面的测试套件涉及多个步骤,从使用特定注解设置初始测试环境到为不同的用户角色和操作编写详细的测试用例。以下是实现 AuthorControllerTest.java 最终状态的逐步指南。

第 1 步 - 设置您的测试环境

要设置您的环境,请按照以下步骤操作:

  1. 创建一个名为 AuthorControllerTest.java 的测试类文件。使用 @WebMvcTest(AuthorController.class) 注解该类,以专注于测试 AuthorController。这告诉 Spring Boot 仅配置测试所需的 MVC 组件,而不使用完整的应用程序上下文。

  2. 使用 @Import(SecurityConfig.class) 将您的自定义安全配置包含在测试上下文中。这在测试期间准确模拟安全行为至关重要。

  3. 声明所需的字段:

    • ApplicationContext 用于设置 MockMvc 对象

    • 一个用于执行和断言 HTTP 请求的 MockMvc 对象

    • 使用 @MockBean 注解模拟控制器所依赖的任何服务或组件,例如 AuthorServiceJwtDecoder

    • 用于测试中 JSON 序列化和反序列化的 ObjectMapper

这是这一步的代码更改:

@WebMvcTest(AuthorController.class)
@Import(SecurityConfig.class)
class AuthorControllerTest {
    @Autowired
    private WebApplicationContext context;
    private MockMvc mockMvc;
    @MockBean
    private AuthorService authorService;
    @MockBean
    private JwtDecoder jwtDecoder;
    private final ObjectMapper objectMapper = new ObjectMapper();
}

在这里,我们已经通过模拟 AuthorServiceJwtDecoder 来设置测试类。我们将在下一节中需要时能够操纵它们。

第 2 步 – 初始化测试框架

实现一个带有 @BeforeEach 注解的设置方法,以便在每个测试之前初始化 MockMvc 对象:

    @BeforeEach
    public void setup() {
        mockMvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
    }

此方法使用 MockMvcBuilders 工具构建带有 Web 应用程序上下文和 Spring Security 集成的 MockMvc 对象。

第 3 步 – 编写测试用例

在设置我们的测试类之后,我们可以开始逐步编写我们的测试用例:

  1. 编写针对添加和获取不同角色作者的参数化测试用例。使用 @ParameterizedTest@MethodSource 来提供角色和预期的 HTTP 状态。在这些测试中,您将模拟具有不同用户角色的请求并断言预期的结果。

    • 对于添加作者,模拟 AuthorService 的响应并执行 POST 请求,根据角色断言状态。

    • 对于通过 ID 获取作者,模拟 AuthorService 的响应并执行 GET 请求,根据角色断言状态和内容。

  2. 编写获取所有作者、更新作者和删除作者的测试用例。利用 @Test@WithMockUser 注解指定用户详细信息。这些测试将执行以下操作:

    • 模拟服务层响应

    • 执行相关的 HTTP 请求(GET 用于获取所有作者,PUT 用于更新,DELETE 用于删除)

    • 断言预期的结果,包括状态码和,当适用时,响应体内容

您可以在控制器测试类中看到这些步骤是如何实现的。您可以在 GitHub 仓库中看到五个测试方法,这些方法验证了本节中提到的测试用例,链接为 github.com/PacktPublishing/Mastering-Spring-Boot-3.0/blob/main/Chapter-6-unit-integration-test/src/test/java/com/packt/ahmeric/bookstore/controller/AuthorControllerTest.java。我们将在下一节讨论如何测试异常处理。

第 4 步 – 处理异常情况

编写一个处理要删除的作者未找到场景的测试用例。模拟服务在尝试删除不存在的作者时抛出 EntityNotFoundException,并断言控制器正确地响应了 404 状态:

    @Test
    @WithMockUser(username="testUser", authorities={"ROLE_ADMIN"})
    void testDeleteAuthorNotFoundWithAdminRole() throws Exception {
        Long id = 1L;
        doThrow(new EntityNotFoundException("Author not found with id: " + id))
                .when(authorService).deleteAuthor(id);
        mockMvc.perform(delete("/authors/" + id))
                .andExpect(status().isNotFound());
    }

在这个测试方法中,我们通过使用 doThrow() 方法操纵 authorService.deleteAuthor 方法来抛出异常,并期望得到一个“未找到”的状态响应。

第 5 步 – 运行测试

运行你的测试以验证所有测试都通过,并且你的控制器在各种场景和用户角色下表现如预期。

这种全面的测试方法不仅验证了AuthorController的功能方面,还确保了安全约束得到尊重,从而在应用程序的行为和安全性方面提供了信心。

在本节中,我们学习了如何使用具有安全配置的 Spring MVC 控制器编写控制器测试。我们已经为断言、功能和安全编写了测试。通过这些测试,我们确信我们的控制器和安全过滤器按预期工作。

在完成单元测试之旅后,我们将在下一节中关注集成测试。我们将探讨如何测试我们项目中的不同组件之间的集成/交互,包括数据库、网络服务和外部 API。

集成测试 – 将组件连接起来

一旦你理解了单元测试的概念,尤其是在处理棘手的网络安全层时,你可以将你的视野扩展到集成测试。在编写单元测试时,你可以将质量检查想象成对砖块进行的检查,以确保用这些砖块建造的墙能让你和家人免受雨、雪以及其他一切天气的影响。集成测试评估我们应用程序的不同部分如何协同工作。这就是完整的应用程序测试在全面运行的地方:模块交互、数据库、网络服务和与其他所有外部系统的交互都会被交叉检验,以确保一切顺利。

为什么需要集成测试?单元测试不足以证明我们的应用程序是健壮的吗?简短的答案是:不。在单元测试中,我们只是在证明方法按预期工作,但在现实生活中,这些方法并不是独立工作的。它们与其他组件交互。因此,集成测试对于查看任何组件是否受到你的更改的影响是至关重要的。

因此,系好安全带,因为我们现在要开始一段旅程。为了确保万无一失,一个大型集成系统将满足所有要求和可能的场景——这实际上让你的应用程序为现实世界和满足用户需求的最终测试做好了更好的准备。我们将通过清晰、实用的示例引导你,最终提供简洁的集成测试,确保你的高质量软件产品最终能够交付。

设置测试环境

集成测试的主要目的是识别和解决与应用程序不同部分之间交互相关的问题。这可能包括几个服务层、数据库或外部 API 之间的设计交互,所有这些共同的目标是让它们有目的地以相同的方式协同工作,就像函数应该执行一样。与单元测试不同,集成测试确保隔离的功能是正确的,因为它研究系统的行为。总的来说,应该从接口级别部分测试应用程序本身,以确保质量和功能完整,避免在缺陷、性能瓶颈和其他集成问题中留下接口缺失,这些问题可能来自单元测试。

在我们对作者控制器端点的集成测试策略中,我们利用这两个主要类:

  • AbstractIntegrationTest:这个类作为我们集成测试的基础,提供跨多个测试类共享的常见配置和设置例程。它是一个抽象类,不直接运行测试;相反,它设置测试环境。这包括为数据库配置测试容器,初始化 WireMock 以模拟外部服务,以及设置 Spring 的应用程序上下文,包括集成测试所需的必要配置文件和配置。WireMock 是我们用于模拟 REST 和简单对象访问协议SOAP)服务等的库。通过这种模拟能力,我们可以隔离我们的组件,避免与外部连接以及这些服务的潜在故障。由于所有集成测试也需要相同的设置,我们可以使用这个抽象类。使用抽象基类有助于我们维护一个干净且不要重复自己DRY)的测试代码库。

  • AuthorControllerIntegrationTest:从AbstractIntegrationTest扩展而来,这个类专门用于测试作者控制器端点。它从AbstractIntegrationTest继承了常见的测试环境设置,并添加了覆盖作者控制器功能的测试,例如创建、读取、更新和删除作者。AuthorControllerIntegrationTest类利用 Spring 的 MockMvc 来模拟 HTTP 请求并断言响应,确保当与其他应用程序组件(如安全层和数据库)集成时,作者控制器表现如预期。

通过以这种方式构建我们的集成测试,我们实现了一种分层测试方法,使我们能够在仍然利用测试之间共享的公共设置的同时,隔离特定组件(如作者控制器)的测试。这种组织结构使我们的测试更加高效,更容易维护,并确保我们全面测试对应用程序整体性能和可靠性至关重要的交互和集成。

配置集成测试的应用程序属性

在集成测试中,我们的应用程序将需要以它在真实环境中的方式运行。因此,设置应用程序属性文件至关重要。然而,我们还需要将集成测试环境与其他测试环境隔离开来。这就是我们启动一个新的application-integration-test.properties文件的原因。通过这种隔离,我们确保集成测试环境中的配置仅针对该环境,并且不会影响其他测试和开发环境。

我们正在添加与当前源代码中使用的相同的属性。这是因为以下参数将在我们的应用程序以集成测试配置运行时被需要:

spring.security.oauth2.client.registration.keycloak.client-id=bookstore-client
spring.security.oauth2.client.registration.keycloak.client-secret=secret-client
spring.security.oauth2.client.registration.keycloak.client-name=Keycloak
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/keycloak
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8180/auth/realms/BookStoreRealm
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8180/auth/realms/BookStoreRealm

通过配置这些属性,我们创建了一个受控、可预测和隔离的环境,使我们能够彻底和准确地测试应用程序的集成点。这种设置对于评估应用程序在模拟生产环境中的行为至关重要,确保所有组件协同工作。

接下来,我们将深入了解这些配置的实际应用,为稳健、真实环境测试做好准备。

使用 Testcontainers 初始化数据库

Testcontainers是一个为 JUnit 和系统测试设计的 Java 库。它通常在运行时提供轻量级、可丢弃的实例方式,用于共享数据库和 Selenium 网络浏览器或任何可以在 Docker 容器内运行的东西。在底层,Testcontainers使用 Docker 来帮助完成实际的数据库实例的设置和,特别是,拆卸,这些实例是隔离的、短暂的,并且完全受控。借助像Testcontainers这样的工具,可以在不增加太多开销的情况下,准确测试满足业务需求的数据库交互和持久性,无需数据库安装的复杂性或某些共享测试实例的设置。

我们现在将通过在AbstractIntegrationTest类中初始化 PostgreSQL 和 MongoDB 数据库的Testcontainers来配置 PostgreSQL 和 MongoDB 容器。以下是操作方法:

  • 使用Testcontainers库。此方法指定要使用的 Docker 镜像(postgres:latest),以及数据库特定的配置,如数据库名称、用户名和密码。一旦初始化,容器就会被启动,并且Java 数据库连接JDBC)URL、用户名和密码将被动态注入到 Spring 应用程序上下文中。这允许集成测试与一个真实的 PostgreSQL 数据库实例交互,该实例与生产中使用的实例相同。

  • 通过指定 Docker 镜像(mongo:4.4.6)使用Testcontainers库。在启动 MongoDB 容器时,连接 URI 被注入到 Spring 应用程序上下文中,使测试能够与真实的 MongoDB 实例通信。

要初始化数据库,请按照以下步骤操作:

  1. 首先,我们将定义所需的参数,例如数据库镜像版本和数据库名称:

       private static final String POSTGRES_IMAGE = "postgres:latest";
        private static final String MONGO_IMAGE = "mongo:4.4.6";
        private static final String DATABASE_NAME = "bookstore";
        private static final String DATABASE_USER = "postgres";
        private static final String DATABASE_PASSWORD = "yourpassword";
        private static final int WIREMOCK_PORT = 8180;
        private static WireMockServer wireMockServer;
    
  2. 当我们初始化我们的测试数据库容器时,我们将使用这些参数:

        @Container
        static final PostgreSQLContainer<?> postgresqlContainer = initPostgresqlContainer();
        @Container
        static final MongoDBContainer mongoDBContainer = initMongoDBContainer();
        private static PostgreSQLContainer<?> initPostgresqlContainer() {
            PostgreSQLContainer<?> container = new PostgreSQLContainer<>(POSTGRES_IMAGE)
                    .withDatabaseName(DATABASE_NAME)
                    .withUsername(DATABASE_USER)
                    .withPassword(DATABASE_PASSWORD);
            container.start();
            return container;
        }
        private static MongoDBContainer initMongoDBContainer() {
            MongoDBContainer container = new MongoDBContainer(DockerImageName.parse(MONGO_IMAGE));
            container.start();
            return container;
        }
    

    在此代码块中,首先,我们定义容器并在其中调用容器初始化函数。容器初始化函数消耗在前面代码块中定义的步骤 1中的参数。

  3. 我们需要一些动态属性,这些属性将在容器被触发后定义。通过以下代码,我们可以让应用程序知道将使用哪个数据源 URL 来连接到数据库:

        @DynamicPropertySource
        static void properties(DynamicPropertyRegistry registry) {
            registry.add("spring.datasource.url", postgresqlContainer::getJdbcUrl);
            registry.add("spring.datasource.username", postgresqlContainer::getUsername);
            registry.add("spring.datasource.password", postgresqlContainer::getPassword);
            registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
        }
    

    每个容器在测试前后都会自动启动、准备和拆除,确保每个测试套件都在一个干净、隔离的数据库环境中运行。这个自动化过程提高了集成测试的可靠性和可重复性,并简化了数据库交互的设置和故障排除。

在数据库已经准备好并在容器化的沙盒中设置好之后,下一节将展示如何做到这一点:模拟外部 API 的响应,以便我们可以深入到我们的代码库中,提供一个全面、无保留的测试策略。

使用 WireMock 模拟外部服务

在集成测试的上下文中,模拟外部服务的能力很重要,因为你不能在集成测试环境中运行所有外部设备。即使你可以运行它们,集成测试的目的也是测试组件中的代码。外部系统的问题与应用程序代码的质量无关。WireMock 为这个挑战提供了一个强大的解决方案。通过创建模拟这些外部服务行为的可编程 HTTP 服务器,WireMock 允许开发者生成可靠、一致和快速的测试。模拟外部服务确保测试不仅从应用程序控制之外的因素中隔离出来,而且可以在任何环境中运行,无需实际服务连接。

为了有效地模拟与 OpenID Connect 提供者的交互,WireMock 可以被配置为以预定义的响应来响应身份验证和令牌请求。我们需要这个设置来测试受保护的端点,而无需与真实的身份验证服务交互。以下是实现此目的的方法:

  1. AbstractIntegrationTest类中,设置一个 WireMock 服务器在特定端口上运行。这个服务器充当你的模拟 OpenID Connect 提供者。

  2. 模拟 OpenID 配置:配置 WireMock 为 OpenID Connect 发现文档和其他相关端点提供服务。我们需要模拟端点以返回提供者元数据,包括授权、令牌、用户信息和JSON Web Key SetJWKS)URI 的 URL。这确保了当你的应用程序尝试发现 OpenID Connect 提供者的配置时,它会从 WireMock 接收到一致和受控的响应。

  3. 模拟令牌和授权响应:进一步配置 WireMock 以模拟响应令牌和授权请求。这些响应应模仿来自 OpenID Connect 提供者的真实响应结构,包括必要的访问令牌、ID 令牌和刷新令牌。

请参阅 GitHub 仓库中的相关抽象类,了解我们如何在github.com/PacktPublishing/Mastering-Spring-Boot-3.0/blob/main/Chapter-6-unit-integration-test/src/test/java/integrationtests/AbstractIntegrationTest.java中模拟 key-cloak 服务器。每当我们的应用程序需要与 key-cloak 通信时,我们的模拟服务器将按预期响应我们的应用程序。

通过这种方式模拟 OpenID Connect 提供者,您可以准确且一致地测试您的应用程序的认证和授权流程,确保您的安全机制按预期工作,而不依赖于外部系统。

在为数据库交互和外部服务依赖项建立了受控环境之后,我们现在已准备好在下一节开始编写集成测试。

为作者控制器编写集成测试

在深入测试本身之前,确保每个测试运行都有一个干净的页面至关重要。@BeforeEach方法在这个过程中起着至关重要的作用,它允许我们在每次测试之前将我们的数据库重置到已知状态:

@BeforeEach
void clearData() { authorRepository.deleteAll(); }

通过调用如authorRepository.deleteAll()这样的方法,我们可以清除所有数据,防止跨测试污染并确保每个测试独立运行。

使用@WithMockUser 保护测试

由于我们的应用程序有一个安全层,我们需要考虑这个层来编写我们的测试,即使我们有模拟的第三方安全依赖项。我们的应用程序仍然检查安全过滤器中的请求角色。我们有一个非常有用的注解:@WithMockUser注解允许我们模拟具有特定角色的认证用户请求,确保我们的测试准确反映了应用程序的安全约束。这样,我们可以确认我们的安全配置正在有效工作。

测试端点

现在,我们已准备好为每个端点编写测试。我们拥有运行中的测试数据库和模拟的第三方依赖项。现在,我们只需向/authors端点发送请求并断言响应。这部分与控制器单元测试非常相似,但不同之处在于我们不会模拟服务——我们将使用服务本身。所有测试都将从头到尾运行。因此,我们将确保我们的应用程序及其所有组件按预期运行。

在下面的代码块中,我们将为Get /authors/{authorId}端点编写一个测试用例:

@Test
@WithMockUser(username="testUser", authorities={"ROLE_ADMIN"})
void testGetAuthor() throws Exception {
    Author author = Author.builder().name("Author Name").build();
    authorRepository.save(author);
    mockMvc.perform(get("/authors/" + author.getId()))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name", is(author.getName())));
}

在这里,我们向应用程序发送一个带有管理员角色的模拟用户 GET 请求,并通过插入一个样本Author对象来准备数据库。我们还期望从应用程序获得有效的响应。正如你所看到的,我们没有模拟仓库类或服务类,因此当应用程序开始工作时,我们发起一个GET请求,所有相关的类和方法都真的像真实应用程序一样工作。

对于其余的测试用例,你可以在我们的 GitHub 仓库github.com/PacktPublishing/Mastering-Spring-Boot-3.0/blob/main/Chapter-6-unit-integration-test/src/test/java/integrationtests/AuthorControllerIntegrationTest.java中查看。

当我们运行我们的测试类时,它将测试所有端点的端到端。集成测试在单元测试和端到端测试之间架起了一座桥梁,专注于应用程序不同部分之间的交互。它验证了应用程序组件按预期协同工作,并识别出在单独测试组件时可能看不到的问题。通过使用Testcontainers和 WireMock 等工具,我们已经看到了如何模拟真实世界的环境和依赖关系,从而实现全面和可靠的测试。

总结来说,我们可以看到在软件开发周期中集成测试是多么重要。它为我们应用程序的整体功能提供了一个全面的测试。我总是想象这些集成测试就像本地开发测试一样。当你更改代码库时,你可以依赖集成测试来确保你的更改不会破坏其他流程。在下一节中,我们将处理异步环境:响应式组件。

测试响应式组件

在本节中,我们将深入研究测试响应式组件,重点关注我们示例响应式 Spring Boot 应用程序中的UserController端点。测试响应式组件与传统应用程序略有不同,因为响应式编程提供了一种非阻塞、事件驱动的处理数据流和变化传播的方法。我们将使用 Spring WebFlux 和WebTestClient来测试响应式 HTTP 请求。

设置测试环境

正如我们在第三章中学到的,Spring 中的响应式编程,由 Spring WebFlux 提供支持,引入了一种非阻塞、事件驱动的模型,该模型能够有效地处理异步数据流。这就是为什么我们需要一个稍微不同的策略来测试这些响应式组件,以确保异步和非阻塞行为被准确考虑。响应式测试环境必须能够处理随时间变化的数据流和序列,因此理解如何有效地设置和使用正确的工具至关重要。

我们将使用WebTestClient来测试反应式组件,而不是像在非反应式应用测试中使用的那样使用MockMVC。在配置了@WebFluxTest(controllers = UserController.class)UserControllerTest类中,WebTestClient被自动注入,以便能够直接与UserController端点进行交互。这个注解帮助我们隔离控制器,确保测试轻量级且具有针对性,从而显著加快测试过程:

@WebFluxTest(controllers = UserController.class,
        excludeAutoConfiguration = {ReactiveSecurityAutoConfiguration.class, ReactiveOAuth2ClientAutoConfiguration.class})
class UserControllerTest {
    @Autowired
    private WebTestClient webTestClient;
}

@WebFluxTest还为我们的测试环境设置了WebTestClient,使其准备好发送模拟的 HTTP 请求并断言响应。

WebTestClient有助于模拟请求和响应的行为,就像它们在一个实时、反应式网络环境中发生一样。它还再次展示了 Spring 如何无缝支持反应式端点的测试。在接下来的理论信息之后,我们将深入到下一节中模拟组件的部分。

准备模拟组件

模拟在测试期间防止实际数据库操作中起着关键作用,这有几个重要原因。首先,它确保了测试隔离,允许每个测试独立运行,而不会受到共享数据的影响。您已经在上一章中知道了这一点。因此,我们直接进入代码片段:

@MockBean
private UserRepository userRepository;
private User testUser;
@MockBean
private SecurityWebFilterChain securityWebFilterChain;
@BeforeEach
void setUp() {
    testUser = new User(1L, "Test User", "test@example.com");
}

在每个测试之前,我们已经模拟了依赖项并初始化了测试数据,这使得我们能够深入到测试策略的核心,检查UserController中的每个端点是如何被测试的。接下来,我们将开始编写测试用例,我们将分解每个操作的测试过程,确保UserController在各种条件下都能按预期行为。

编写测试用例

现在,我们准备为每个方法编写测试。我们已经模拟了依赖项;我们只需要编写单元测试并查看它们是否返回预期的结果。与非反应式组件测试的唯一区别是,我们将使用webTestClient而不是mockMVC。让我们开始吧:

@Test
void getAllUsersTest() {
    when(userRepository.findAll()).thenReturn(Flux.just(testUser));
    webTestClient.get().uri("/users")
            .exchange()
            .expectStatus().isOk()
            .expectBodyList(User.class).hasSize(1);
}

在这个代码块中,我们编写了一个单元测试来获取所有用户端点。首先,我们操作了userRepositry.findAll()方法,使其返回一个Flux testUser对象,并期望得到一个成功的响应。

对于其他端点的测试方法,请参阅 GitHub 仓库github.com/PacktPublishing/Mastering-Spring-Boot-3.0/blob/main/Chapter-6-reactive-test/src/test/java/com/packt/ahmeric/reactivesample/controller/UserControllerTest.java

在我们结束对 Spring Boot 应用程序中测试反应性组件的动手实践之前,回顾一下是有必要的。转向反应性编程迫使开发者改变他们构建应用程序和沟通的方式,本质上主要关注在压力下的非阻塞、异步和可扩展交互。然而,巨大的权力伴随着巨大的责任,代码的测试正是这一原则的顶峰。测试这些非常反应性组件带来的主要测试挑战是确保处理数据流、保证非阻塞性质和处理背压。

在本节中讨论了多种测试目标的方法,从通过@WebFluxTest设置测试环境到指定依赖关系,以及使用WebTestClient测试异步结果,一个人就可以配备上实现反应性 Spring Boot 应用程序质量、可扩展性和可维护性的所需工具。这些策略确保无论运行时实现什么条件,应用程序都能表现良好,并交付预期的功能和性能。

测试的第三个良好实践是“反应性”。当应用程序变得更加复杂和规模更大时,有效地测试这些反应性组件的能力成为成功开发生命周期的基石。从另一个角度来看,实践这些测试方法的开发者可以在问题甚至上升到令人烦恼的程度之前捕捉到它们,并找到一种方法来培养质量和弹性的文化。

换句话说,测试反应性部分是动态变化的 Web 开发景观的一个特色,它总是在最佳开发和最佳测试实践中遇到创新。通过这次探索的见解和技术,我们向未来构建更稳健、负责任和用户友好的应用程序迈进。

摘要

随着我们结束对非反应性和反应性 Spring Boot 应用程序的高级测试策略的全面探索,很明显,这次旅程既具有启发性又具有赋权性。我们学习了测试对于开发生命周期的重要性,以及它如何利用 Springboot 的能力变得简单。通过实际示例和动手指导,本章为您提供了在当今快速发展的软件开发领域中至关重要的基本技能和见解。以下是我们所涵盖内容的总结:

  • TDD 的基础原则:我们学习了 TDD 的基础原则及其对软件质量和可靠性的影响。

  • 单元测试控制器:我们探索了使用安全层进行单元测试控制器的技术,确保我们的应用程序不仅功能齐全,而且安全。

  • 集成测试的重要性:我们学习了集成测试的重要性,确保我们应用程序的不同部分能够无缝协作。

  • 测试响应式组件:我们探讨了测试响应式组件的策略,解决了响应式编程范式带来的独特挑战。

这些技能将使你的应用程序经过测试,更加可靠、可扩展和易于维护。掌握 Spring Boot 中的这些测试技术,将使你作为一个开发者脱颖而出。

展望未来,软件开发之旅持续演变,带来新的挑战和机遇。在下一章中,我们将深入探讨容器化和编排的世界。这一即将到来的章节将揭示如何使 Spring Boot 应用程序准备好容器化,以及如何使用 Kubernetes 进行编排以增强可扩展性和可管理性。

第四部分:部署、可扩展性和生产力

在这部分,我们将把重点转向有效地部署和扩展应用程序,同时提高生产力。第七章 探讨了最新的 Spring Boot 3.0 功能,特别是那些增强容器化和编排以简化部署流程的功能。第八章 深入探讨了使用 Kafka 构建事件驱动系统,这对于考虑可扩展性管理高吞吐量数据至关重要。最后,第九章 介绍了提高生产力和简化开发策略,确保随着项目的增长,你可以保持快速高效的流程。这一部分对于掌握软件开发的操作方面至关重要,为你轻松处理大规模部署做好准备。

本部分包含以下章节:

  • 第七章Spring Boot 3.0 的容器化和编排功能

  • 第八章使用 Kafka 探索事件驱动系统

  • 第九章提高生产力和开发简化

第七章:Spring Boot 3.0 的容器化和编排功能

在本章中,我们将深入探讨使用 Spring Boot 3.0容器化编排 领域,这对于当代开发者来说是一个关键技能集。随着您翻阅这些页面,您不仅将获得知识,还将获得可以立即应用于您项目的实践经验。对于任何寻求最大化 Spring Boot 能力的开发者来说,这段旅程具有重要意义,他们开发的应用程序不仅高效和可扩展,而且在当今不断发展的数字世界中具有弹性和健壮性。

完成本章后,您将掌握无缝容器化 Spring Boot 应用程序的知识,理解作为容器平台的 Docker 的复杂性,并掌握 Kubernetes 编排您容器化应用程序的概念。这些技能在当今的软件开发领域中至关重要,因为应用程序开发、部署和管理速度和可靠性可以极大地影响项目成功。

在现实世界的场景中,根据需求对环境的适应性、资源效率和可扩展性是软件开发的一些方面。本章通过强调容器化和编排的优势来满足这些要求,以增强您应用程序的可移植性、效率和可管理性。

在本章中,我们将涵盖以下内容:

  • Spring Boot 中的容器化和编排

  • Spring Boot 和 Docker

  • 优化 Spring Boot 应用程序以适应 Kubernetes

  • Spring Boot Actuator 与 Prometheus 和 Grafana

让我们开始这段旅程,将您的 Spring Boot 应用程序容器化,并使它们更容易管理!

技术要求

对于本章,我们需要在我们的本地机器上做一些设置:

Spring Boot 中的容器化和编排

欢迎来到容器化的领域,我们将为 Spring Boot 应用程序在任意平台上的部署做好准备。如果您对容器技术如何革命性地改变应用程序的开发和部署流程感兴趣,您就来到了正确的位置。本节将为您提供将 Spring Boot 应用程序打包成容器的见解,确保在环境中的灵活性、一致性和适应性。您将深入了解背后的原因,并学习将重塑您交付应用程序方法的技巧。我们将一起踏上这段旅程,简化您的应用程序部署过程。

理解容器化——您的应用程序在一个盒子里

将容器化视为打包应用程序的一种方式。想象一下准备旅行,确保所有必需品都装进一个行李箱。在你的应用程序的上下文中,将“行李箱”想象成一个容器,它容纳的不是你的应用程序,而是必要的代码、库和配置设置。这个容器是通用的——无论它是在你的电脑上、朋友的设备上还是在云端,它都能无缝运行。

这对你有什么好处?想象一下创建一个你想让每个人都能使用的应用程序。没有容器,它可能在你的系统上运行得很好,但可能在其他地方遇到问题——这可能会令人沮丧。有了容器化,如果它对你工作得很好,那么对其他人也是如此。它提供了可靠性,消除了那些令人烦恼的时刻,当有人说,“它对我不起作用。”

容器就像盒子,让你的应用程序能够轻松旅行,没有任何麻烦。这就像一个节省你时间和头痛的技巧。通过接受这个概念,你确保了无论应用程序需要去哪里或需要扩展多少,它都能茁壮成长。

正因如此,了解容器化至关重要;它简化了开发者的生活,并增强了应用程序的灵活性。现在让我们探索如何为 Spring Boot 应用程序准备这次容器之旅。

享受收益——负载更轻,启动更快

容器不仅帮助应用程序轻松移动。它们就像是科技世界的背包。不是每个应用程序都携带一个装满运行所需一切物品的行李箱,容器共享资源。这加快了应用程序的启动时间,并在你的电脑上节省空间和内存。这个概念类似于拼车上班。当每个人都一起开车时,你们所有人都能更快地到达目的地,并且更加环保。

这就是使用容器对你有益的原因——当你的应用程序在容器中时,它可以瞬间启动,只需一挥手。你不必等待它开始。此外,由于容器轻量级,你可以在一台机器上运行应用程序而不会发生资源冲突。此外,如果你的应用程序变得流行,创建容器来处理流量很简单——当活动减慢时,停止一些容器是轻而易举的。

使用容器提供了在需求高时将汽车连接到火车上,在需求减少时将其拆卸下来的灵活性。选择容器代表了一种高效管理应用程序的方法。它彻底改变了你创建、测试和发布应用程序的方式,提高了可靠性和响应速度。在下一节中,我们将探讨你的 Spring Boot 应用程序如何利用这些好处。

将 Spring Boot 带入比赛——从一开始就支持容器

让我们为您的 Spring Boot 应用程序准备容器环境。Spring Boot 作为您的应用程序的向导,确保在容器内运行。从一开始,Spring Boot 就旨在与容器一起工作。为什么这很重要?这就像拥有一辆随时准备好的汽车,以便您需要时进行长途旅行。

Spring Boot 为您承担了许多重任。它根据其部署位置自动适应您的应用程序需求,使其非常适合高度可移植的环境。使用 Spring Boot,您不需要对每个方面进行微观管理——它本能地理解容器设置并相应地适应。这就像有一个伴侣,每次旅行都能轻松地知道需要携带哪些必需品。

Spring Boot 还确保您的应用程序随时可用——无论您是在自己的电脑上、朋友的设备上还是在云中运行您的应用程序。这使您能够更多地关注增强您的应用程序,而不是处理设置过程。

通过确保您的 Spring Boot 应用程序具有容器友好性,您不仅是在追随潮流;您是在选择一条减少压力并提高成功率的道路。这一切都是为了简化您作为开发者的生活并增强您应用程序的弹性。现在,让我们继续将您的 Spring Boot 应用程序转换为容器。

激发 Spring Boot 的超级力量——可移植性、效率和控制

让我们探索 Spring Boot 在容器内部为您的应用程序提供的功能。这些功能包括可移植性、效率和控制。它们旨在简化您作为开发者的生活。这些功能如下:

  • 可移植性:这就像为您的应用程序配备了一个适配器。无论您将其插入何处,它都能无缝地运行。无论您将应用程序从您的电脑转移到测试环境或云中,它每次都会一致地运行。这消除了当应用程序在一台设备上运行但在另一台设备上不运行时出现的问题。

  • 效率:这意味着用更少的资源实现。容器通过尽可能共享资源并最小化浪费来利用资源。您的应用程序启动快速且运行顺畅,就像一台调校过的机器。因此,您的应用程序可以同时为许多用户提供服务,而无需大量电力或机器。

  • 控制:这使您能够轻松地监督您应用程序的所有方面。您可以启动它、停止它、在使用期间扩展它,或在较安静的时间缩小它。这就像拥有您应用程序的遥控器,其中每个按钮都对应您可能需要的每个操作。Spring Boot 通过用户直观的设计方式,使访问所有这些控制变得容易。

当你将 Spring Boot 应用程序打包到容器中时,你不仅仅是把它放入一个盒子;你还在其中配备了增强其灵活性、强度和智能的工具。这使我们的应用程序能够满足当前和未来的用户需求。最好的部分?你正在以允许你专注于改进应用程序本身而不是担心它如何以及在哪里运行的方式设置它。这就是使用容器,特别是 Spring Boot 的美妙之处——它赋予你增强应用程序功能的同时最小化复杂性的能力。现在,让我们继续前进,在我们容器化 Spring Boot 应用程序的过程中实现这些功能。

Spring Boot 和 Docker

在通过理解容器化和编排及其整体优势打下基础之后,现在是时候深入实践了。本节将指导你将 Docker 集成到示例 Spring Boot 应用程序中,并利用 Spring Boot 3.0 的功能。我们将演示如何将你的 Spring Boot 应用程序转换为一系列容器,这些容器可以高效地编排以实现可伸缩性。

让我们开始这段旅程,我们将在这里将容器化和编排的概念与 Spring Boot 结合起来付诸实践。我们将一起学习如何创建不仅功能强大而且定制以增强你的工作流程的 Docker 镜像,为与容器编排平台的无缝集成铺平道路。

使用分层 jar 构建高效的 Docker 镜像

Docker 通过强调创建 Docker 镜像的重要性来简化开发者的生活。Spring Boot 的 jars 概念引起了开发者的注意。想象一下烘焙蛋糕——不是作为一个整体烘焙,而是分别烘焙单独的层次。这种方法允许在不重建整个蛋糕的情况下修改层次。同样,Docker 中的分层 jars 允许你将应用程序分割成可以由 Docker 独立管理和更新的层次。

这种方法通过减少构建时间和生成 Docker 镜像来革新开发过程。通过缓存这些层次,Docker 只在应用程序发生更改时重建修改过的层次。例如,对应用程序代码的修改不需要重建那些保持基本不变的组件,如 JVM 层。

准备开始了吗?以下是一个逐步指南,介绍如何设置你的 Spring Boot 项目以利用分层 jar:

  1. 创建一个新项目:使用 Spring Initializr (start.spring.io/) 创建一个新的 Spring Boot 项目。选择 Spring Boot 版本 3.2.1。对于依赖项,添加 Spring Web 以创建一个简单的 Web 应用程序。请选择 Gradle 作为构建工具。

  2. 生成和下载:配置完成后,点击 生成 下载你的项目骨架。

  3. 在适当的包中,在src/main/java/目录下创建一个名为HelloController的新 Java 类。

  4. 添加 REST 端点:实现一个简单的 GET 端点,返回一个问候语:

    @RestController
    public class HelloController {
        @GetMapping("/")
        public String hello() {
            return "Hello, Spring Boot 3!";
        }
    }
    
  5. 启用分层:首先,配置您的 Spring Boot 构建插件以识别分层功能。这只是一个简单的问题,即在您的构建文件中包含正确的依赖项和配置设置。

  6. ./gradlew build ```java ```` build/libs` 目录。

现在,我们手中有一个分层 jar。让我们看看我们如何检查其中的层:

> jar xf build/libs/demo-0.0.1-SNAPSHOT.jar BOOT-INF/layers.idx
> cat BOOT-INF/layers.idx
- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

layers.idx文件将应用程序组织成逻辑层。典型的层包括以下内容:

  • dependencies:应用程序需要的外部库

  • spring-boot-loader:负责启动您的应用程序的 Spring Boot 部分

  • snapshot-dependencies:任何快照版本的依赖项,它们比常规依赖项更可能更改

  • application:您的应用程序的编译类和资源

每个层都旨在优化 Docker 的构建过程。不太可能改变的层(如dependencies)与更易变的层(如application)分离,允许 Docker 独立缓存这些层。这减少了在只有小改动时重建和重新部署应用程序所需的时间和带宽。

在探讨了分层 jar 的效率之后,接下来我们将看看 Spring Boot 如何使用云原生构建包简化 Docker 镜像的创建。准备好看到即使没有深入的 Docker 专业知识,您也可以创建和管理既健壮又适合云的 Docker 镜像。

使用云原生构建包简化 Docker 化

云原生构建包标志着我们在为 Docker 准备应用程序方面的进步——把它们视为您的 Docker 化助手。在创建 Dockerfile 时,您需要列出所有构建 Docker 镜像的命令,构建包会自动化这个过程。它们分析您的代码,确定其需求,并将其打包成容器镜像,而无需在 Dockerfile 中编写任何一行代码。

这种自动化对于缺乏 Docker 专业知识或没有时间维护 Dockerfile 的团队特别有益。它还促进了一致性和对实践的遵守,确保构建包生成的镜像符合安全、效率和兼容性的标准。

这就是您如何利用云原生构建包的力量与 Spring Boot 结合使用:

  1. 在终端中,导航到我们的 Spring Boot 应用程序的根目录。

  2. 使用内置对构建包支持的 Spring Boot Gradle 插件。通过一个简单的命令,./gradlew bootBuildImage --imageName=demoapp,您就可以触发构建包开始工作。我们还为我们的镜像起了一个名字——demoapp

  3. 构建包检查你的应用程序,将其识别为 Spring Boot 应用程序。然后它自动选择一个基础镜像,并将你的应用程序代码及其依赖项层叠在其上。

  4. 接下来,构建包将优化你的镜像以适应云环境。这意味着移除任何不必要的组件,以确保你的镜像尽可能轻量级和安全。

    我们的 Spring Boot 应用程序现在已容器化,并准备好部署到任何 Docker 环境,无论是云环境还是其他环境。你得到了一个强大、标准化的 Docker 镜像,没有任何 Dockerfile 的戏剧性。

我们可以使用 Docker 测试它是否按预期工作。请确保 Docker Desktop 在你的本地机器上已启动并运行。稍后,我们只需运行以下命令:

8080. So, we can easily test the response, which should be curl http://localhost:8080.
With Docker images sorted, let’s turn our attention to ensuring our applications exit gracefully in a Docker environment. In the following section, we’ll dive into why a graceful shutdown is important and how Spring Boot’s enhanced support for this can safeguard your data and user experience during the inevitable shuffling of Docker containers in production environments.
Enhancing graceful shutdown capabilities
When it’s time for your program to finish running, you’ll want it to exit smoothly, like how it started. This is what we call a shutdown – making sure that your containerized apps can properly handle termination signals, complete tasks, and not abruptly stop active processes. In Docker setups, where apps are frequently stopped and moved around due to scaling or updates, graceful shutdown isn’t a nicety; it’s crucial for preserving data integrity and providing a user experience.
Spring Boot 3.0 improves this process by ensuring that your apps can effectively respond to **Signal Terminate** (**SIGTERM**) signals. The method is for instructing a process to stop. Let’s walk through how you can set up and verify that your Spring Boot app gracefully handles shutdowns:

1.  Configure graceful shutdown by adding the following in `'application.properties'`:

    ```

    server.shutdown=graceful

    spring.lifecycle.timeout-per-shutdown-phase=20s

    ```java

    `20s` represents the duration that the application waits before it shuts down.

     2.  Let’s rebuild the image and run it in Docker:

    ```

    ./gradlew bootBuildImage --imageName=demoapp

    docker run –-name demoapp-container -p 8080:8080 demoapp:latest

    ```java

     3.  After starting your app, send a SIGTERM signal to your Docker container and observe the graceful shutdown.

    ```

    docker stop demoapp-container

    ```java

     4.  When you check out the logs of your Docker container, you will see these logs:

    ```

    开始优雅关闭。等待活跃请求完成

    优雅关闭完成

    ```java

As we conclude our exploration of Spring Boot’s containerization capabilities, let’s recap the points and explore how they can be implemented in your projects. You’ll find that whether you want to enhance build efficiency using jars streamline image creation with Buildpacks or ensure smooth shutdowns, Spring Boot 3.0 provides the tools to strengthen your containerized applications for cloud deployment.
Now that we’ve discussed the way to end services gracefully, let’s delve into how Spring Boot 3.0 helps in managing application configurations within Docker and why it is important for containerized applications. We will also discover how Spring Boot applications thrive within the Kubernetes ecosystem.
Optimizing Spring Boot apps for Kubernetes
Picture a harbor where ships come and go non-stop. This harbor relies on a system to manage the traffic smoothly to ensure each ship is in the place at the right time. In the realm of containerized applications, Kubernetes plays the role of this master harbor system. While Docker handles packaging applications into containers, Kubernetes orchestrates which containers should run, scales them as needed, manages traffic flow, and ensures their well-being.
Kubernetes isn’t meant to replace Docker; rather, it complements Docker effectively. Docker excels at containerization and transforming applications into efficient units. On the other hand, Kubernetes takes these units and seamlessly integrates them within the intricate landscape of modern cloud architecture.
By leveraging Kubernetes functionalities, developers can now oversee Spring Boot applications with an unprecedented level of efficiency and reliability. From deployments with no downtime to automated scaling capabilities, Kubernetes empowers your containerized applications to perform optimally under workloads and scenarios.
Let’s dive in by exploring how Spring Boot’s integrated Kubernetes probes collaborate with Kubernetes health check mechanisms to enhance your application’s resilience and uptime.
Integrating Kubernetes probes for application health
In the dynamic realm of Kubernetes, it’s crucial to make sure your application is in shape and prepared to handle requests. This is where readiness and liveness checks come in, serving as the protectors of your application’s health. Liveness checks inform Kubernetes about the status of your application – whether it is functioning or unresponsive, while readiness checks indicate when your app is set to receive traffic. These checks ensure that operational and ready-to-go instances of your application receive traffic and play a vital role in enhancing the robustness of your deployments.
Understanding probes
Probes are diagnostic tools used in Kubernetes. Kubernetes uses them to check the status of the component periodically.
Let’s see what are these probes:

*   **Liveness probe**: This probe checks whether your application is alive. If it fails, Kubernetes restarts the container automatically, offering a self-healing mechanism.
*   **Readiness probe**: This determines whether your application is ready to receive requests. A failing readiness probe means Kubernetes stops sending traffic to that pod until it’s ready again.

Now, we will be activating probes in Spring Boot 3.0, which simplifies the integration of these probes, thanks to its Actuator endpoints:

1.  **Include Spring Boot Actuator**: Ensure the Spring Boot Actuator dependency is included in your project. It provides the necessary endpoints for Kubernetes probes:

    ```

    implementation 'org.springframework.boot:spring-boot-starter-actuator'

    ```java

     2.  `application.properties`, you can specify the criteria for these probes:

    ```

    management.endpoint.health.group.liveness.include=livenessState

    management.endpoint.health.group.readiness.include=readinessState

    ```java

That’s all our application needs to be ready for Kubernetes. Let’s create our first Kubernetes YAML file.
Creating Kubernetes YAML file
Our YAML file includes two main sections. Each section defines a Kubernetes object. The following section is Deployment resource:

apiVersion: apps/v1

kind: Deployment

metadata:

name: spring-boot-demo-app

spec:

replicas: 1

selector:

matchLabels:

app: spring-boot-demo-app

template:

metadata:

labels:

app: spring-boot-demo-app

spec:

containers:

  • name: spring-boot-demo-app

image: demoapp:latest

imagePullPolicy: IfNotPresent

ports:

  • containerPort: 8080

livenessProbe:

httpGet:

path: /actuator/health/liveness

port: 8080

initialDelaySeconds: 10

periodSeconds: 5

readinessProbe:

httpGet:

path: /actuator/health/readiness

port: 8080

initialDelaySeconds: 5

periodSeconds: 5


 Let’s break down what we have introduced in this Deployment resource:

*   `metadata.name: spring-boot-demo-app`: This is the unique name of the deployment within the Kubernetes cluster. It’s specific to the application being deployed, in this case, `spring-boot-demo-app`.
*   `spec:template:metadata:labels:app: spring-boot-demo-app`: This label is crucial for defining which pods belong to this Deployment resource. It must match the selector defined in the Deployment resource and is used by `Service` to route traffic to the pods.
*   `spec:containers:name: spring-boot-demo-app`: The name of the container running in the pod. It’s more for identification and logging purposes.
*   `spec:containers:image: demoapp:latest`: This specifies the Docker image to use for the container, which is pivotal as it determines the version of the application to run. The `latest` tag here can be replaced with a specific version tag to ensure consistent environments through deployments.
*   `spec:containers:ports:containerPort: 8080`: This port number is essential because it must match the application’s configured port. For Spring Boot applications, the default is `8080`, but if your application uses a different port, it needs to be reflected here.
*   `livenessProbe:` and `readinessProbe:` are configured to check the application’s health and readiness at the `/actuator/health/liveness` and `/actuator/health/readiness` endpoints, respectively. These paths are Spring Boot Actuator endpoints, which are specific to Spring Boot applications. Adjusting the probe configurations (such as `initialDelaySeconds` and `periodSeconds`) may be necessary based on the startup time and behavior of your application.

Now, we will add the load balancer part to our YAML file:


apiVersion: v1

kind: Service

metadata:

name: spring-boot-demo-app-service

spec:

type: LoadBalancer

ports:

  • port: 8080

targetPort: 8080

selector:

app: spring-boot-demo-app


 In this part, we have defined the following parameters:

*   `metadata:name: spring-boot-demo-app-service`: This is the name of the `Service` object, which is how you would refer to this service within the Kubernetes cluster. It should be descriptive of the service it provides.
*   `spec:type: LoadBalancer`: This type makes `Service` accessible through an external IP provided by the cloud hosting the Kubernetes cluster. This detail is crucial for applications that need to be accessible from outside the Kubernetes cluster.
*   `spec:ports:port: 8080`: This is the port on which `Service` will listen, which must match `containerPort` if you want external traffic to reach your application. It’s specifically tailored to the application’s configuration.
*   `spec:selector:app: spring-boot-demo-app`: This selector must match the labels of the pods you want `Service` to route traffic to. It’s crucial for connecting `Service` to the appropriate pods.

This file sets up a basic deployment of a Spring Boot application on Kubernetes, with a single replica, and exposes it externally via a `LoadBalancer` service. It includes health checks to ensure traffic is only sent to healthy instances.
Let’s now run our first Kubernetes cluster in our local.
Running Kubernetes cluster
In this book, for everything related to Docker, we have used Docker Desktop. So, we need to enable Kubernetes in our Docker Desktop app first. Please open **Preferences** in Docker Desktop, navigate to **Kubernetes**, check the enable box, and then finally click on the **Save and Restart** button. That’s it! We have Kubernetes in our local machines.
In order to run our YAML file, we need to open a terminal and navigate to the folder where we saved our YAML file. Then, we run the following command:

使用 kubectl 命令观察探针的实际操作。这确保它们配置正确并按预期响应。此外,你可以通过以下 curl 命令向 HelloController 发起 GET 请求:

Curl http://localhost:8080/

这是我们将获得的响应:

Hello, Spring Boot 3!

这意味着我们的应用程序正在运行,并且已成功与 Kubernetes 的 readiness 和 liveness 探针进行通信。

在你的应用程序的健康检查已经稳固设置,确保 Kubernetes 确切知道你的服务何时准备好并能够执行之后,是时候转移我们的焦点了。接下来,我们将深入 Kubernetes 的ConfigMapsSecrets领域。这一步将向你展示如何熟练地处理应用程序配置和管理敏感数据,利用 Kubernetes 原生机制进一步提高 Spring Boot 应用程序的操作效率和安全性。

使用 Kubernetes 管理配置和 Secrets

在 Kubernetes 的世界中,有效管理应用程序配置和敏感信息不仅是一种最佳实践;对于安全且可扩展的部署来说,它是一种必要性。Kubernetes 提供了两个强大的工具来完成这个目的:

  • ConfigMaps 允许将配置工件与镜像分离,以实现可移植性

  • Secrets 安全地存储敏感信息,如密码、OAuth 令牌和 SSH 密钥

ConfigMaps 和 Secrets 可以彻底改变你管理应用程序环境特定配置和敏感数据的方式。以下是如何在 Spring Boot 应用程序中利用这些 Kubernetes 原生工具,使用一个新的控制器作为说明性示例。

想象一个简单的 Spring Boot 控制器,当访问特定端点时会返回一条消息和一个 API 密钥:

@RestController
public class MessageController {
    @Value("${app.message:Hello from Spring Boot!}")
    private String message;
    @Value("${api.key:not very secure}")
    private String apiKey;
    @GetMapping("/message")
    public String getMessage() {
        return message;
    }
    @GetMapping("/apikey")
    public String getApiKey() {
        return apiKey;
    }
}

@Value 注解从应用程序的环境中提取配置值,为消息和 API 密钥都提供了默认值。

接下来,我们将使用 ConfigMap 和 Secret 外部化配置。ConfigMap 存储自定义消息,Secret 安全地存储 API 密钥。

我们将创建一个名为 app-configmap.yaml 的新 YAML 文件,内容如下:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  app.message: 'Hello from ConfigMap!'

如您所容易理解的,此配置将为我们的 app.message 参数设置一条消息。

现在,让我们创建一个具有 Kubernetes 功能的安全密钥:

kubectl create secret generic app-secret --from-literal=api.key=mysecretapikey

现在,我们需要修改应用程序的部署 YAML 文件,将 ConfigMap 和 Secret 中的值注入到 Spring Boot 应用程序的环境:

containers:
  - name: spring-boot-demo-app
    image: demoapp:latest
    imagePullPolicy: IfNotPresent
    ports:
      - containerPort: 8080
    env:
      - name: APP_MESSAGE
        valueFrom:
          configMapKeyRef:
            name: app-config
            key: app.message
      - name: API_KEY
        valueFrom:
          secretKeyRef:
            name: app-secret
            key: api.key

此配置将 ConfigMap 中的 app.messageSecret 中的 api.key 分别注入到 APP_MESSAGEAPI_KEY 环境变量中,Spring Boot 将使用这些变量。

现在,我们需要重新生成我们的镜像并重启 Kubernetes 集群:

./gradlew bootBuildImage --imageName=demoapp
kubectl rollout restart deployment/spring-boot-demo-app

应用更新后的部署后,您的应用程序现在将返回 /message 端点,当访问 /apikey 端点时会返回安全的 API 密钥,这证明了配置和敏感数据外部化的成功。

现在,您已经配置了应用程序以保持其秘密安全且配置动态,让我们探索 Spring Boot 为各种 Kubernetes 环境提供的针对特定配置文件的简化方法。此下一步将增强您根据部署环境动态管理应用程序行为的能力,进一步定制应用程序的功能以满足不同的运营需求。

在 Kubernetes 中利用特定配置文件配置

在应用程序部署的领域,根据设置(如开发、测试和生产)定制应用程序以表现出不同的行为不仅是有帮助的;这是至关重要的。Spring Boot 通过提供基于配置文件的配置来简化此过程,允许您根据配置文件设置配置。当与 Kubernetes 一起使用时,此功能为您的部署提供了适应性和多功能性。

在 Spring Boot 中,通过配置文件,你可以将你的应用程序属性组织成针对每个环境的特定文件。例如,你可以有 application-prod.properties 用于生产设置和 application-test.properties 用于测试环境设置。这种分离允许你分别管理环境配置,如数据库 URL、外部服务端点和功能开关,从而降低环境配置混淆的风险。

让我们考虑一个例子,其中你的 Spring Boot 应用程序需要根据它是在测试或生产环境中运行来从 /message 端点返回不同的消息:

  1. 首先,让我们定义测试和生产环境的配置:

    • application-test.properties:这是为测试环境设计的:

      app.message=Hello from the Test Environment!
      
    • application-prod.properties:这是为生产环境设计的:

      app.message=Hello from the Production Environment!
      
      1. 要在 Kubernetes 中利用这些配置文件,你可以在部署配置中设置一个环境变量,Spring Boot 会自动识别以激活特定的配置文件:
       apiVersion: apps/v1
       kind: Deployment
       metadata:
         name: your-application
       spec:
         containers:
         - name: your-application
           image: your-application-image
           env:
             - name: SPRING_PROFILES_ACTIVE
               value: "prod" # Change this to "test" for test environment
    

    通过设置 SPRING_PROFILES_ACTIVE 环境变量为 prod 或 test,你指示 Spring Boot 激活相应的配置文件并加载其关联的属性。

    1. 现在,我们需要重新生成我们的镜像并重启 Kubernetes 集群:
    ./gradlew bootBuildImage --imageName=demoapp
    kubectl rollout restart deployment/spring-boot-demo-app
    

    使用激活生产配置文件的方式部署你的应用程序到 Kubernetes。访问 /message 端点应返回 SPRING_PROFILES_ACTIVE 值以进行测试和重新部署,而相同的端点现在应返回 来自测试环境的问候!,展示了配置特定行为的实际应用。

在探索基于配置文件配置之后,让我们花点时间反思我们所走过的旅程以及这些功能如何与 Kubernetes 结合,从而为你和你的 Spring Boot 应用程序带来好处。这种方法不仅简化了环境设置的处理,还提高了应用程序在不同部署场景下的适应性和可靠性。

在本节中,我们讨论了 Spring Boot 3.0 的创新功能如何无缝地与 Kubernetes 结合,以改进应用程序的部署、配置和管理。我们探讨了利用 Kubernetes 探针进行应用程序健康监控、管理配置和机密以保护数据,以及通过配置特定设置轻松适应各种环境。这些功能不仅简化了部署,还增强了应用程序在 Kubernetes 环境中的弹性和灵活性。凭借其对 Kubernetes 的原生支持,Spring Boot 3.0 使开发者能够利用容器编排来确保应用程序可以大规模部署,同时保持可维护性和安全性。

现在,你的 Spring Boot 应用程序已经为在 Kubernetes 中的性能做好了准备,我们接下来的部分将专注于监控这些应用程序。该集成旨在易于使用,提供在 Kubernetes 设置中跟踪和分析数据的帮助。这确保了你拥有提高性能和可靠性的信息。

Spring Boot Actuator 与 Prometheus 和 Grafana

在 Kubernetes 的领域,应用程序在容器组中动态处理,监控和指标的重要性不容忽视。这些洞察是应用程序的心跳,标志着其健康、性能和效率。没有它们,你就像在复杂的迷宫中盲目导航,无法检测或解决可能影响应用程序可靠性和用户体验的问题。监控和指标赋予开发者和运维团队可见性,确保应用程序不仅在 Kubernetes 环境中生存,而且蓬勃发展。

介绍 Spring Boot Actuator,这是每个开发者工具箱中用于揭示应用程序丰富细节的工具。Actuator 端点通过提供实时指标、健康检查等功能,让你窥视应用程序的工作原理。这些洞察对于维护应用程序状态、在问题升级之前识别问题以及优化性能以满足要求是无价的。有了 Spring Boot Actuator,你就能了解应用程序的行为及其当前状态,这对于在 Kubernetes 设置中进行有效监控至关重要。

让我们探索 Spring Boot Actuator 如何为 Prometheus 提供所需端点以收集数据,为监控能力铺平道路。这些基础工作将帮助我们解锁 Prometheus 和 Grafana 的功能,并开发一个能够清晰管理大规模应用程序的监控系统。

集成 Prometheus 进行指标收集

Prometheus 在 Kubernetes 生态系统中的监控中扮演着角色,作为监控应用程序和基础设施健康和效率的工具。它收集和整合指标的能力非常宝贵,尤其是与 Spring Boot 的 Actuator 端点结合使用时。这些端点揭示了 Prometheus 可以收集的信息,以提供一个关于应用程序操作状况的全面概述。

要将 Prometheus 集成到 Spring Boot 应用程序中,你需要配置 Prometheus 以识别和抓取 Actuator 指标端点。以下是一个使用 Kubernetes ConfigMaps 和部署设置此环境的实用指南:

  1. 首先,我们需要更新我们的 Spring Boot 应用程序。我们将在 gradle.build 文件中添加一个新的库:

    implementation 'io.micrometer:micrometer-registry-prometheus'
    
    1. 然后,我们需要在 application.properties 文件中将 prometheus 添加到 web.exposure 列表中,以启用 Prometheus Actuator 端点:
    management.endpoints.web.exposure.include=health,info,prometheus
    
    1. 首先定义一个包含你的 Prometheus 配置的 ConfigMap 资源。这包括指定抓取间隔和 Prometheus 应从哪些目标收集指标。以下是 prometheus-config.yaml 的样子:
       apiVersion: v1
       kind: ConfigMap
       metadata:
         name: prometheus-config
       data:
         prometheus.yml: |
           global:
             scrape_interval: 15s
           scrape_configs:
             - job_name: 'spring-boot'
               metrics_path: '/actuator/prometheus'
               static_configs:
                 - targets: ['spring-boot-demo-app-service:8080']
    

    此配置指示 Prometheus 每 15 秒从你的 Spring Boot 应用程序的 Actuator Prometheus 端点抓取指标。

    1. ConfigMap 就位后,使用 prometheus-deployment.yaml 部署 Prometheus 服务器。此部署指定了 Prometheus 服务器镜像、端口和卷挂载,以使用之前创建的 ConfigMap 进行配置。

    首先,我们需要定义这个 Kubernetes 容器部署的部分如下:

       apiVersion: apps/v1
       kind: Deployment
       metadata:
         name: prometheus-deployment
       spec:
         replicas: 1
         selector:
           matchLabels:
             app: prometheus-server
         template:
           metadata:
             labels:
               app: prometheus-server
           spec:
             containers:
               - name: prometheus-server
                 image: prom/prometheus:v2.20.1
                 ports:
                   - containerPort: 9090
                 volumeMounts:
                   - name: prometheus-config-volume
                     mountPath: /etc/prometheus/prometheus.yml
                     subPath: prometheus.yml
             volumes:
               - name: prometheus-config-volume
                 configMap:
                   name: prometheus-config
    

    现在,我们可以继续定义这个容器的负载均衡部分如下:

       apiVersion: v1
       kind: Service
       metadata:
         name: prometheus-service
       spec:
         type: LoadBalancer
         ports:
           - port: 9090
             targetPort: 9090
             protocol: TCP
         selector:
           app: prometheus-server
    

    通过此 YAML 文件,我们定义了一个 Kubernetes 容器,可以在其中运行 Prometheus 镜像,并通过端口 9090 提供服务。

    1. 使用以下命令将配置应用到你的 Kubernetes 集群中:
    kubectl apply -f prometheus-config.yaml
    kubectl apply -f prometheus-deployment.yaml
    

    这些命令创建了必要的 ConfigMap 并在集群内部署 Prometheus,设置它自动从你的 Spring Boot 应用程序中抓取指标。

Prometheus 收集指标只是获取关于应用程序性能的可操作见解的第一步。真正的魔法发生在我们将这些数据可视化时,使其易于访问和理解。接下来,我们将探讨如何使用 Grafana 创建 Prometheus 收集的指标的引人入胜的可视化,将原始数据转化为推动决策和优化的有价值的见解。

使用 Grafana 可视化指标

Grafana 像一座灯塔,引导我们穿越今天应用程序产生的指标海洋。它不仅仅是一个工具。它是一个平台,通过其多功能的仪表板将指标数据转化为有价值的见解。Grafana 支持包括 Prometheus 在内的数据源,并在创建查询、设置警报以及以多种格式展示数据方面表现出色。无论你是跟踪系统健康、用户行为还是应用程序性能,Grafana 都提供了做出决策所需的清晰度和即时信息。

要利用 Grafana 监控你的 Spring Boot 应用程序指标,你将首先在 Kubernetes 集群中部署 Grafana:

  1. 创建一个 grafana-deployment.yaml 文件,该文件定义了 Kubernetes 中的 Grafana 部署和服务。此部署将运行 Grafana,并通过 LoadBalancer 暴露它,使 Grafana UI 可访问:

       apiVersion: apps/v1
       kind: Deployment
       metadata:
         name: grafana-deployment
       spec:
         replicas: 1
         selector:
           matchLabels:
             app: grafana
         template:
           metadata:
             labels:
               app: grafana
           spec:
             containers:
               - name: grafana
                 image: grafana/grafana:7.2.0
                 ports:
                   - containerPort: 3000
       ---
       apiVersion: v1
       kind: Service
       metadata:
         name: grafana-service
       spec:
         type: LoadBalancer
         ports:
           - port: 3000
             targetPort: 3000
             protocol: TCP
         selector:
           app: grafana
    

    此配置将帮助我们创建一个 grafana 实例,并通过端口 3000 使其可访问。

    1. 使用以下命令应用此配置:
    admin/admin).
    
  2. 导航到 http://prometheus-service:9090 Prometheus 服务,因为 Prometheus 部署在同一 Kubernetes 集群中。保存并测试连接,以确保 Grafana 可以与 Prometheus 通信。

  3. 配置 Prometheus 作为数据源后,您现在可以在 Grafana 中创建仪表板来可视化您的 Spring Boot 应用程序指标。首先点击12900 ID 的SpringBoot APM 仪表板,然后选择您为 Prometheus 创建的数据源。就这样!您拥有一个广泛的仪表板来监控您的应用程序。

图 7.1:展示仪表板外观的示例可视化

图 7.1:展示仪表板外观的示例可视化

图 7.1中,Grafana 仪表板以可视化的方式展示了 Spring Boot 应用程序的性能指标。SpringBoot APM 仪表板设计有易于理解的面板,可以一目了然地显示信息。在顶部,您可以查看统计信息,如正常运行时间和内存使用仪表,它们提供了系统健康状况的快照。下面是展示 CPU 使用和 JVM 内存统计信息的图表和图形,提供了对应用程序性能的洞察。仪表板利用仪表、柱状图和折线图以用户友好的方式呈现数据,使用户能够监控和分析应用程序随时间的行为,而不会感到复杂过载。

在讨论了收集和可视化技术之后,让我们探索这些洞察如何增强您应用程序的性能和可靠性。通过利用 Grafana 仪表板,我们可以从反应式方法转变为主动式方法来管理我们的 Spring Boot 应用程序,以确保在 Kubernetes 环境中达到最佳性能。

在我们探索应用程序监控的过程中,我们强调了密切监控我们应用程序的必要性,尤其是在它们在 Kubernetes 环境中运行时。Spring Boot Actuator 已成为一个提供我们检查应用程序迹象的工具。当与 Prometheus 结合使用时,这对组合充当观察者,收集提供应用程序运行视图的指标。

通过集成 Grafana,我们将 Prometheus 收集的数据转化为展示应用程序性能和健康状况的故事,从而完成了我们的监控三重奏。通过用户仪表板,我们不仅能够观察,而且能够深入交互我们的指标,深入挖掘指导我们主动行动的模式。

当我们考虑可用的工具时,我们意识到我们不仅配备了监控工具;我们还获得了预测、调整并确保我们的 Kubernetes 部署以最佳状态运行的能力。这种集成监控策略的具体优势——如提高可见性、更快的响应时间以及对应用程序行为的更深入洞察——是我们努力提供弹性可靠应用程序的资产。

摘要

在我们结束本章关于 Spring Boot 3.0 容器化和编排特性的讨论时,可以说这是一段学习与技能提升的旅程。本章不仅强调了容器化和编排在软件开发中的作用,而且还为您提供了有效利用这些技术的必要工具和知识。

让我们回顾一下您所获得的有价值见解和技能:

  • 理解容器化的基本知识: 我们从深入了解容器化概念开始,学习了如何将我们的 Spring Boot 应用程序打包到容器中,以实现可移植性和效率

  • 掌握 Docker 与 Spring Boot: 我们已经讨论了如何为我们的 Spring Boot 应用程序创建和管理 Docker 镜像,使它们在任何环境中都准备就绪,同时强调部署的简便性和容器轻量级的特性

  • 使用 Kubernetes 编排容器: 我们已经学习了如何使用 Kubernetes 部署和管理我们的 Docker 化 Spring Boot 应用程序,突出了该平台的可扩展性和维护应用程序健康的能力

  • 使用 Prometheus 和 Grafana 进行监控: 最后,我们探讨了如何设置 Prometheus 进行指标收集和 Grafana 进行可视化,确保您能够监控应用程序的性能并迅速响应任何问题

这些能力和专业知识在当今的技术领域中极为宝贵,使您能够创建不仅强大且适应性强的应用程序,而且易于维护和在不同平台上高效运行。掌握容器化和编排原则为开发前沿的云原生应用程序奠定了基础。

随着我们期待下一章,我们将深入研究 Kafka 与 Spring Boot 的集成,以构建响应式、可扩展的事件驱动系统。

从容器化和编排领域过渡到事件驱动设计,为您提供了提升技能集、进一步应对软件开发挑战和优势的机会。下一章预计将是您在掌握 Spring Boot 及其生态系统旅程中的又一重要步骤。


第八章:使用 Kafka 探索事件驱动系统

在本章中,我们将深入探讨使用 Kafka 和 Spring Boot 创建事件驱动系统的机制。在这里,我们将发现如何使用 Docker 在您的计算机上配置 Kafka 和 ZooKeeper,为开发能够通过事件无缝通信的微服务奠定基础。您将获得实际操作经验,构建两个 Spring Boot 应用程序:一个用于生成事件,另一个用于消费事件,模拟消息框架中的发送者和接收者的功能。

本章的最终目标是让您掌握设计、部署和监控利用 Kafka 功能并结合 Spring Boot 简单性的 事件驱动架构EDA)的技能。这些知识对于您在本书旅程中的进步并非至关重要,但在现实世界中,可扩展和响应性系统不仅受到青睐,而且被视为期望。

掌握这些原则和工具对于创建能够适应、可扩展并能满足当代软件环境不断变化需求的程序至关重要。在本章结束时,您将在本地机器上拥有一个事件驱动设置,这将增强您处理更复杂系统的信心。

以下是该章节的主要主题,您将进行探索:

  • 事件驱动架构简介

  • 在本地开发中设置 Kafka 和 ZooKeeper

  • 使用 Spring Boot 消息构建事件驱动应用程序

  • 监控事件驱动系统

技术要求

对于本章,我们需要在我们的本地机器上配置一些设置:

事件驱动架构简介

事件驱动架构,也称为 EDA,是一种在软件开发中广泛使用的架构方法。它更侧重于根据事件触发动作,而不是遵循严格的步骤流程。在 EDA 中,当发生特定事件时,系统会迅速做出反应,执行相应的动作或一系列动作。这种方法与依赖于请求-响应模式的其他模型不同,它提供了一个更动态和实时的系统行为。

在我们这个数据不断生成和更新的时代,EDA 具有重要意义。能够迅速响应变化的能力在这样的快节奏环境中是无价的。与传统的系统相比,EDA 使企业能够迅速抓住机会和应对挑战。这种敏捷性在金融、实时分析、物联网IoT)和其他快速变化频繁、信息时效性重要的领域尤为重要。

转向 EDA 可以显著改变公司的运作方式,带来以下好处:

  • 响应性:通过实时处理事件,事件驱动系统提供即时的反馈或行动,这对于时间敏感的任务至关重要。

  • 可扩展性:事件驱动的设置可以在不造成处理延迟的情况下管理大量事件。这种可扩展性对于处理数据量不断增加和复杂性的企业来说非常重要。

  • 灵活性:由于 EDA 中的组件松散连接,它们可以独立更新或替换,而不会影响系统。这种灵活性使得升级和功能的集成更加简单。

  • 效率:通过减少通过轮询或查询检查新数据的需求,可以降低资源消耗,从而提高整体系统效率。

  • 增强的用户体验:在需要实时信息的应用中,如游戏和实时更新,EDA 有助于提供动态的用户体验。

这些优势突显了为什么许多组织正在转向 EDA 以满足现代技术挑战的需求。

在 EDA 中,我们需要一个消息代理。消息代理帮助我们分发组件之间的消息。在本章中,我们将使用 Apache Kafka 作为消息代理。Kafka 是一个开源的流处理平台。它最初由 LinkedIn 开发,后来捐赠给了 Apache 软件基金会。Kafka 主要作为一个高效的、擅长处理大量数据的消息代理。

其设计特性促进了持久消息存储和高效的事件处理,以实现有效的 EDA 实现。这个平台允许分布式数据流及时被消费,使其成为需要广泛数据处理和传输能力的应用的理想解决方案。

使用 Kafka,开发者可以在事件驱动系统的组件之间无缝传输数据,确保即使在复杂的交易场景中也能保持事件完整性和顺序。这一特性使 Kafka 成为许多现代高性能应用架构中的组件,这些应用依赖于实时数据处理。

既然我们已经了解了 EDA 的含义及其带来的好处,以及理解了 Kafka 在此类系统中的作用,我们将通过在 Docker 上设置 Kafka 的过程。这种设置创建了一个受控且可重复的环境,用于探索 Kafka 在 EDA 中的功能。我们的目标是为你提供部署 Kafka 的工具和知识,使你能够利用实时数据处理在项目中的潜力。

通过掌握使用 Docker 部署 Kafka 的技能,你将获得理解和管理事件驱动系统复杂性的必要经验。这种动手实践方法不仅加强了理论概念,而且使你能够有效地处理现实世界中的应用。

为本地开发设置 Kafka 和 ZooKeeper

Kafka 在事件驱动系统中扮演着角色,促进不同组件之间的顺畅通信。它使服务能够通过消息交换进行通信,就像人们使用即时通讯应用保持联系一样。这种架构通过允许系统的各个部分独立运行并迅速响应事件,促进了可扩展应用程序的开发。我们还将更详细地提及 Kafka 以及其在 理解 Kafka 代理及其在事件驱动系统中的作用 部分中的作用。

然而,Kafka 并不单独工作;它与 ZooKeeper 协作,ZooKeeper 作为其监管者。ZooKeeper 监控 Kafka 的代理以确保它们正常运行。把它想象成一个协调员,分配任务并确保操作。ZooKeeper 对于管理在高峰负载期间维持 Kafka 的稳定性和可靠性的后台进程至关重要。

在讨论了我们需要组件之后,我还会提到安装。我们将使用 Docker,就像我们在前面的章节中所做的那样。Docker 简化了 Kafka 和 ZooKeeper 在你机器上的设置。它提供了一个易于启动的便携式配置版本,你可以在需要时轻松启动,无需麻烦。

这种设置 Kafka 和 ZooKeeper 的方法不仅是为了方便;它还关乎确保你可以在不担心复杂的安装程序或设置之间的差异的情况下探索、创建和测试你的事件驱动系统。当我们深入研究使用 Docker 设置 Kafka 和 ZooKeeper 的步骤时,请记住,这构成了基础。你正在为应用程序建立一个适应性强的基础设施——这将促进有效的通信和无缝的可扩展性。让我们继续前进,为你的本地开发环境准备 EDA。

理解 Kafka 代理及其在事件驱动系统中的作用

在不断变化的 EDA 世界中,Kafka 代理作为高效的枢纽,精心管理消息的接收、路由和交付到指定的目的地。在 Kafka 生态系统中,Kafka 代理作为一组协同工作以监控消息流量的代理的一部分发挥作用。简单来说,想象这些代理就像勤奋的邮递员,处理生产者的消息并将它们组织成类似特定邮箱或地址的主题。这些主题可以分成多个分区,以促进可扩展的消息处理。

让我们看看在图 8.1中 Kafka 集群是如何工作的。

图 8.1:Kafka 集群架构

图 8.1:Kafka 集群架构

在这个图中,你可以看到 Kafka 如何组织其工作流程。生产者是向 Kafka 系统发送数据的来源。它们将消息推送到 Kafka 集群,该集群由多个代理(代理 1代理 2代理 3)组成。这些代理存储消息并使其可供消费。ZooKeeper 作为该集群的管理者,跟踪代理的状态并执行其他协调任务。消费者组,标记为Group-AGroup-B,根据需要从代理中拉取消息。

Kafka 代理的真正魔法在于它们在管理这些主题分区方面的熟练程度。当一条消息到达时,代理根据其重要性等级等标准确定将其放置在分区内的位置。这种方法确保了消息的分布,并在一个分区中将相似的消息(具有共同属性的)分组。这种分区过程对于分配工作负载至关重要,并使消费者应用程序能够并发处理消息,从而实现更高效的数据处理。

此外,Kafka 代理的另一个关键功能是确保 Kafka 系统中的消息重复,以防代理故障导致数据丢失。这种重复过程通过在不同代理之间创建分区副本作为安全措施。如果一个代理离线,另一个可以介入,平滑地保持系统的强大和灵活。

代理擅长存储和为消费者提供消息。他们使用偏移量来跟踪消费者已读取的消息,使消费者能够从消息流中上次离开的地方继续读取。这确保了每条消息都被处理,并给消费者提供了按自己的节奏管理消息的灵活性。

Kafka 集群中由代理监督的消息编排是一个结合了效率和可靠性的过程。由代理执行的这种复杂协调使得事件驱动系统能够高效运行,精确地管理大量数据。通过利用 Kafka 代理的功能,开发者可以创建不仅可扩展和弹性好,而且能够快速、准确地处理消息以满足当今快节奏数字景观需求的系统。

随着我们进一步探索设置和使用 Kafka 的方面,代理作为可靠和高效消息分发基础的作用变得越来越明显。它们处理和引导消息的能力是任何事件驱动架构(EDA)的核心,确保信息能够准时准确地送达目的地。

使用 Docker 运行 Kafka 和 ZooKeeper

通过 Docker 在您的计算机上运行 Kafka 和 ZooKeeper,对于开发者来说可能是一个颠覆性的改变。它将曾经繁琐的设置过程简化为简单易操作的过程。Docker 容器作为可携带的空间,可以迅速启动、停止和删除,非常适合开发和测试目的。这种安排使您能够在机器上重新创建生产级环境,而无需设置或专用硬件。

由于我们在几乎所有前面的章节中都使用了 Docker Compose,您将熟悉它。我们将使用 Docker Compose 通过单个命令运行这两个服务。以下是一个简单的docker-compose.yml文件示例,用于设置 Kafka 和 ZooKeeper:

version: '2'
services:
  zookeeper:
    image: zookeeper
    ports:
      - "2181:2181"
    networks:
      - kafka-network
  kafka:
    image: confluentinc/cp-kafka
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    networks:
      - kafka-network
networks:
  kafka-network:
    driver: bridge

docker-compose.yml文件就像一个食谱,告诉 Docker 如何运行您的 Kafka 和 ZooKeeper 容器。它告诉 Docker 使用哪些镜像,容器如何在网络上相互通信,哪些端口需要打开,以及需要设置哪些环境变量。在这个文件中,我们告诉 Docker 在端口2181上运行 ZooKeeper,在端口9092上运行 Kafka。使用这个文件,我们简化了整个过程,使其像按按钮一样简单,以启动您的设置。这是一个出色的开发者工具,减少了手动步骤,让您专注于有趣的部分——构建和实验您的事件驱动应用程序。

将此文件保存为docker-compose.yml,并使用以下命令运行:

docker-compose up -d

此命令拉取必要的 Docker 镜像,创建容器,并以分离模式启动 Kafka 和 ZooKeeper,使它们在后台运行。

通过遵循这些步骤,您已经为您的应用程序搭建了一个强大、可扩展的消息骨干,以便在此基础上构建。这个基础不仅支持事件驱动系统的开发,还为在受控的本地环境中实验 Kafka 的强大功能铺平了道路。

完成我们对使用 Docker 配置 Kafka 的探索后,很明显,这种组合消除了在您的计算机上运行 Kafka 的障碍。Docker 的容器魔法将可能是一项繁重的工作转变为一个简单的过程,让您能够更多地关注应用程序开发的创造性方面,而不是陷入设置的复杂性中。这种简化的设置不仅关乎便利,还关乎技术民主化和管理的简化,赋予开发者无需处理过于复杂的配置即可进行实验和创新 EDA 的能力。

随着我们从设置 Kafka 和 ZooKeeper 的方面转向探索使用 Spring Boot 消息构建事件驱动应用程序的激动人心领域,我们正从基础设施建设的基石转向应用程序设计的艺术。在本节中,您将亲身体验到您的 Kafka 设置如何赋予您力量,因为我们将引导您创建使用 Spring Boot 生成和消费消息的应用程序。这正是抽象概念转化为创造的地方,让您能够充分利用事件驱动系统的功能。

使用 Spring Boot 消息构建事件驱动应用程序

使用 Spring Boot 构建事件驱动应用程序涉及构建一个响应迅速、可扩展并能处理现代软件需求复杂性的系统。本质上,事件驱动应用程序响应的事件范围从用户交互到外部系统的消息。这种方法使您的应用程序组件能够独立交互和操作,从而提高灵活性和效率。由于 Spring Boot 的约定优于配置的哲学以及它从一开始就提供的工具集,使用 Spring Boot 设置此类应用程序变得更加容易。

在整个旅程中,我们将通过介绍两个 Spring Boot 项目来采取动手实践的方法——一个将专注于生成事件,另一个将专注于消费它们。这种分离反映了现实生活中的场景,其中生产者和消费者通常位于系统或微服务中,突出了当代应用程序的去中心化特性。通过参与这些项目,您将获得配置用于发送消息的生产者和用于响应这些消息的消费者在 Spring Boot 和 Kafka 上下文中的经验。这种方法不仅加强了您对事件驱动系统的理解,还为您创建和改进可扩展的应用程序提供了所需的资源。

随着我们继续前进,我们将深入了解创建 Kafka 集成的 Spring Boot 项目的细节。这将为我们的事件驱动应用程序奠定基础,引导您通过配置 Spring Boot 项目使用 Kafka 发送和接收消息的过程。您将了解启动实现所需的设置、库和初始代码结构。在这里,我们的理论想法将转化为可执行代码。所以,让我们开始这段旅程,用 Spring Boot 和 Kafka 开发强大的交互式应用程序。

创建用于 Kafka 集成的 Spring Boot 项目

在 Spring Boot 中启动一个专门针对 Kafka 集成的项目是解锁事件驱动应用程序功能的具体步骤。这一步骤结合了 Spring Boot 的便捷性和适应性以及 Kafka 的消息功能,使开发者能够创建可扩展和敏捷的应用程序。通过这次集成,我们正在建立一个基础,它有助于在分布式环境中进行通信和管理大量数据和操作。目标是建立一个框架,该框架不仅满足消息生产和消费需求,而且随着应用程序的发展能够无缝扩展。

我们需要两个不同的项目来演示消费者和生成者。因此,您需要重复执行相同的步骤两次来创建这两个项目。但在 步骤 2 中输入项目元数据时,最好选择不同的名称。

图 8.2 中,我们可以看到我们的应用程序将如何相互通信。

图 8.2:我们的应用程序如何相互通信

图 8.2:我们的应用程序如何相互通信

如您在 图 8.2 中所见,生产者应用程序和消费者应用程序之间没有直接的调用。生产者应用程序向 Kafka 发送消息,Kafka 将此消息发布给消费者应用程序。

这里是创建 Spring Boot 项目的分步指南:

  1. 导航到 Spring Initializr (start.spring.io/) 以启动您的项目。这是一个在线工具,允许您快速生成带有所选依赖项的 Spring Boot 项目。

  2. 输入您项目的元数据,例如 工件描述。为消费者和生成者项目提供不同的名称。根据您的偏好选择 MavenGradle 作为构建工具。在我们的示例中,我们将使用 Gradle

  3. 选择您的依赖项。对于 Kafka 项目,您需要为生产者项目添加 Spring Web。这个依赖项包括将 Kafka 与 Spring Boot 集成的必要库。

  4. 生成项目。一旦您填写了所有详细信息并选择了依赖项,请点击 生成 下载您的项目模板。

    图 8.3中,我们可以看到我们需要哪些依赖项以及如何配置 Spring Initializr。

图 8.3:Spring Initializr 的截图

图 8.3:Spring Initializr 的截图

  1. 提取下载的 ZIP 文件,并在您最喜欢的 IDE 中打开项目,例如 IntelliJ IDEA、Eclipse 或 VS Code。

  2. 使用以下行更新application.properties文件。为消费者和发布者项目使用不同的端口:

    server.port:8181
    

当将 Kafka 与 Spring Boot 项目集成时,一个关键组件是Spring Kafka,这是由 Spring Initializr 作为 Apache Kafka 的 Spring 版本添加的。这个库通过提供用户抽象简化了基于 Kafka 的消息解决方案的处理。它简化了在 Spring Boot 应用和 Kafka 代理之间发送和接收消息的过程。通过抽象生产者和消费者配置的复杂性,它使您能够专注于实现业务逻辑,而不是处理消息处理的重性代码。

在配置好 Spring Boot 项目并放置了必要的 Kafka 集成依赖项后,您现在可以深入了解生产和消费消息的细节。这个设置是探索通信和 EDAs 的起点,为在应用中管理数据流提供了一种有效的方法。

进入下一小节构建生产者应用标志着从设置到实现的转变。在这里,我们将指导您在 Spring Boot 项目中设置 Kafka 生产者。这是您所有基础工作开始成形的地方,让您能够向 Kafka 主题发送消息,并启动任何事件驱动系统的通信过程。准备好将理论转化为实践,并见证您的应用如何与 Kafka 交互。

构建生产者应用

创建生产者应用就像在基于事件框架内建立一个广播中心,您的 Spring Boot 设置已经准备好向世界——或者更确切地说,向 Kafka 主题——发送消息。这一阶段非常重要,因为它标志着系统内信息流的开始,确保数据在正确的时间到达预定的目的地。

在 Spring Boot 中创建 Kafka 生产者涉及几个简单的步骤。首先,您需要配置您的应用以连接到 Kafka。这是在您的生产者 Spring Boot 项目的application.properties文件中完成的。您将指定有关 Kafka 服务器地址和您想要发送消息的默认主题的详细信息。

这是我们如何在 Spring Boot 应用中实现 Kafka 生产者的方式:

@RestController
public class EventProducerController {
    private final KafkaTemplate<String, String> kafkaTemplate;
    @Autowired
    public EventProducerController(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }
    @GetMapping("/message/{message}")
    public String trigger(@PathVariable String message) {
        kafkaTemplate.send("messageTopic", message);
        return "Hello, Your message has been published: " + message;
    }
}

在此代码中,KafkaTemplate是一个 Spring 提供的类,它简化了向 Kafka 主题发送消息的过程。我们将此模板注入到MessageProducer服务中,并使用其send方法发布消息。send方法接受两个参数——主题的名称和消息本身。

为了确保您的生产者应用程序能够成功地向 Kafka 发送消息,您需要在application.properties文件中添加一些配置:

spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

这些配置帮助 Spring Boot 识别您的 Kafka 服务器位置(引导服务器)以及如何将消息转换为通过网络传输的格式(键序列化和值序列化)。序列化涉及将您的消息(在这种情况下,是一个字符串)转换为可以在网络上传输的格式。

通过设置和配置您的 Kafka 生产者,您已经朝着开发事件驱动应用程序迈出一步。此配置允许您的应用程序通过发送其他系统部分可以响应和处理的消息,在分布式系统中发起对话。

接下来,让我们将注意力转向这种交互的对应物:构建消费者应用程序。这包括创建监听器,以预测和响应由我们的生产者发送的消息。它在我们的 EDA 内部关闭通信循环中发挥作用,将我们的系统转变为一个能够对实时数据进行响应的动态服务网络。让我们继续我们的探索,揭示我们如何释放事件驱动应用程序的潜力。

构建消费者应用程序

一旦我们使用生产者应用程序设置了广播站,就是时候通过开发消费者应用程序调整到正确的频率。这一步骤确保生产者发送的消息不会在太空中丢失,而是真正被接收、理解并付诸行动。在我们的事件驱动结构中,消费者应用程序就像人群中捕捉到针对它的信号并相应处理的听众。通过将 Kafka 消费者集成到 Spring Boot 应用程序中,我们建立了一个渴望等待消息并准备好在消息到来时立即处理它们的元素。这种能力在创建真正交互式并能实时迅速响应变化和事件的系统中发挥作用。

在 Spring Boot 中设置 Kafka 消费者之前,您首先需要配置应用程序以监听感兴趣的 Kafka 主题。这涉及到在application.properties文件中指定您的 Kafka 服务器位置以及应用程序应订阅哪些主题。

下面是如何在我们的 Spring Boot 应用程序中实现 Kafka 消费者的方法:

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class MessageConsumer {
    @KafkaListener(topics = "messageTopic", groupId = "consumer_1_id")
    public void listen(String message) {
        System.out.println("Received message: " + message);
    }
}

在这个片段中,@KafkaListener注解将listen方法标记为messageTopic上的消息监听器。groupId由 Kafka 用于将消费者分组,这些消费者应被视为一个单一单元。这种设置允许您的应用程序自动从指定的主题中获取并处理消息。

为了确保您的消费者应用程序高效地消费消息,请将以下配置添加到您的application.properties文件中:

spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id= consumer_1_id
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

这些配置确保您的用户连接到 Kafka 服务器(引导服务器)并正确解码它接收到的消息(键反序列化和值反序列化)。auto-offset-reset 选项指导 Kafka 在您的用户组没有偏移量时从何处开始读取消息;将其设置为 earliest,我们的应用程序将开始从事件主题的开始消费。

一旦您的消费者应用程序处于活动状态,您的事件驱动系统现在完全运行,能够通过 Kafka 消息管道发送和接收消息。这种双向通信框架为可扩展的应用程序奠定了基础,这些应用程序可以处理实时数据流并迅速对事件做出响应。

展望未来,下一个关键步骤涉及测试生产者和消费者应用程序以确保它们的集成。这一阶段将理论与实践相结合,让您见证您努力的成果。测试不仅用于验证单个组件的功能,还用于验证系统的整体响应性和效率。让我们通过在我们的事件驱动应用程序上启动测试来前进,确保它们准备好应对可能出现的任何挑战。

测试整个堆栈 – 使您的事件驱动架构变为现实

在使用 Kafka、Spring Boot 和 Docker 配置我们的基于事件的系统后,我们达到了一个关键的时刻,即测试整个设置以见证我们的系统运行。这一关键阶段确认了我们的各个元素,即生产者和消费者应用程序,已正确设置并按预期进行通信,同时确保由 Docker 管理的 Kafka 有效地在它们之间传输消息。这一测试阶段标志着我们工作的完成,使我们能够直接观察作为任何事件驱动系统核心的消息动态交换。

这里是运行整个堆栈的说明:

  1. 定义 Kafka 和 ZooKeeper 服务并运行以下 docker-compose.yml 文件:

    8282. This can be configured in the application.properties file with the following line:
    
    

    server.port=8282

    
    Start the application through your IDE or by running `./gradlew bootRun` in the terminal within the project directory.
    
  2. 通过在 application.properties 文件中设置 8181

    server.port=8181
    

    使用您的 IDE 或与生产者相同的 Gradle 命令来启动消费者应用程序。

  3. 向生产者的消息触发端点发送 GET 请求:

    http://localhost:8282/message/hello-world
    

    hello-world 替换为您希望发送的消息的任何字符串。触发几个不同的消息以测试各种场景。

  4. 观察消费者的日志:切换到您的消费者应用程序的控制台或日志输出。您应该看到随着消费而记录的消息,表明从生产者通过 Kafka 到消费者的通信成功。输出将如下所示:

    Received message: hello-world
    Received message: hello-world-2
    Received message: hello-world-3
    

成功运行测试栈并观察通过 Kafka 从生产者到消费者的消息流是一种宝贵的经验,因为它展示了事件驱动架构(EDA)的强大和灵活性。这种动手测试不仅增加了你对将 Kafka 与 Spring Boot 应用程序集成的理解,还强调了在分布式系统中无缝通信的重要性。正如你所看到的,Docker 在简化开发和测试环境的设置中扮演着关键角色。经过这次实践经验,你将准备好深入研究复杂和可扩展的事件驱动应用程序,这在现代软件开发中是必需的。

现在,我们手头有一个功能齐全的事件驱动应用程序,是时候向前看了。下一步是确保我们的应用程序不仅在各种条件下运行,而且能够成功。这意味着深入监控——这是任何应用程序生命周期中的关键组成部分。在接下来的部分,我们将探讨如何密切关注应用程序的性能,以及如何迅速解决出现的任何问题。这些知识不仅有助于维护应用程序的健康,还有助于优化其效率和可靠性。因此,让我们继续前进,自信地应对这些新的挑战。

监控事件驱动系统

在事件驱动系统的动态世界中,应用程序通过持续的流量进行通信,监控在确保一切顺利运行中扮演着关键角色。正如繁忙的机场需要空中交通管制来确保飞机安全高效地移动一样,事件驱动架构(EDA)依赖于监控来维护其组件的健康和性能。这种监督对于在问题发生时及时发现和理解系统在各种负载和条件下的整体行为至关重要。它使开发者和运维团队能够做出明智的决策,优化性能,并在问题影响用户之前预防问题。

对于使用 Kafka 和 Spring Boot 构建的应用,一套强大的监控工具和技术对于监控系统的脉搏至关重要。在核心上,Kafka 被设计来处理大量数据,因此监控如消息吞吐量、代理健康和消费者延迟等特性变得至关重要。Apache Kafka 的 JMX 指标和像 Prometheus 和 Grafana 这样的外部工具提供了对 Kafka 性能的深入洞察。这些工具可以追踪从正在处理的消息数量到消息穿越系统所需时间的所有方面。

如同在第七章“Spring Boot Actuator with Prometheus and Grafana”部分已经涵盖了 Spring Boot 应用的监控,这里将不再重复。本节我们将专注于 Kafka 的监控。

监控你的 Kafka 基础设施

监控 Kafka 设置就像使用工具来仔细检查你的事件驱动系统的核心功能。这全部关乎了解你的 Kafka 环境运行状况,这对于识别问题、优化资源使用以及确保消息及时可靠地传递至关重要。鉴于 Kafka 在管理数据流和事件处理中的作用,任何问题或低效都可能影响整个系统。因此,建立监控系统不仅是有帮助的,而且是维护强大高效架构的必要条件。

这里是 Kafka 中需要监控的关键指标:

  • 代理指标:这些包括集群中活跃代理的数量及其健康状况。监控每个代理的 CPU、内存使用和磁盘 I/O 帮助识别资源瓶颈。

  • 主题指标:这里的重要指标包括消息输入率、消息输出率和主题大小。关注这些指标有助于理解数据流并发现任何异常模式。

  • 消费者指标:消费者延迟,表示消费者组在处理消息方面落后多少,对于确保数据及时处理至关重要。此外,监控活跃消费者的数量有助于检测消费者可扩展性和性能问题。

  • 生产者指标:监控生产消息的速率以及错误率,可以突出显示数据生成或提交到 Kafka 主题中的问题。

我们将使用 Kafka Manager(现称为 CMAK,或 Apache Kafka 集群管理器)来监控我们的 Kafka 服务器。在包含 Kafka 和 ZooKeeper 设置的同一 Docker Compose 文件中运行 CMAK,便于本地管理和监控 Kafka 集群。

使用 CMAK 监控 Kafka 服务器

以下是您如何在 Docker Compose 设置中包含 CMAK 并在本地机器上运行它的方法:

  1. 要在现有的 Docker Compose 设置中包含 CMAK,您需要为其添加一个新的服务定义。打开您的 docker-compose.yml 文件,并附加以下服务定义:

      kafka-manager:
        image: hlebalbau/kafka-manager:latest
        depends_on:
          - zookeeper
          - kafka
        ports:
          - "9000:9000"
        environment:
          ZK_HOSTS: zookeeper:2181
        networks:
          - kafka-network
    

    我们已经在 docker-compose.yml 文件中简单地引入了 kafka-manager 镜像——CMAK 依赖于 ZooKeeper 和 Kafka,因为它需要监控它们的性能,并且它将在端口 9000 上提供服务。

  2. 更新了 docker-compose.yml 文件后,通过在包含 Docker Compose 文件的目录中的终端运行以下命令来启动服务:

    -d flag runs them in detached mode, so they’ll run in the background.
    
  3. 一旦所有服务都启动并运行,打开网页浏览器并访问 http://localhost:9000。你应该会看到 Kafka Manager(CMAK)界面。

    要使用 Kafka Manager 开始监控 Kafka 集群,您需要将您的集群添加到 Kafka Manager UI 中。

  4. 点击 添加 集群 按钮。

  5. 填写集群信息。如果您在本地运行一切,请使用zookeeper:2181,并使用 Docker Compose 文件中的默认 ZooKeeper 设置。请注意,由于 Kafka Manager 运行在由 Docker Compose 创建的同一 Docker 网络中,它可以直接解析 ZooKeeper 主机名。

    图 8.4中,我们可以看到如何通过使用 Kafka Manager 的添加集群界面来填写表格。

图 8.4:Kafka Manager 应用程序中添加集群屏幕的截图

图 8.4:Kafka Manager 应用程序中添加集群屏幕的截图

  1. 保存您的集群配置。

现在您的 Kafka 集群已添加到 Kafka Manager 中,您可以探索各种指标和配置,例如主题创建、主题列表和消费者组。

图 8.5:我们的主题的 Kafka Manager 屏幕

图 8.5:我们的主题的 Kafka Manager 屏幕

图 8.5中,您可以看到 CMAK 仪表板的截图,该仪表板提供了关于名为messageTopic的特定 Kafka 主题的信息。仪表板提供了一个概述,包括主题的复制因子、分区数量以及表示主题中总消息数的分区偏移总和的详细信息。此外,它还提供了管理主题的控件,例如删除主题、添加分区或修改主题配置的选项。仪表板还展示了分区如何在代理之间分布的见解,包括首选副本百分比等指标,并标记任何倾斜或低副本分区,这对于诊断和维护 Kafka 集群中的最佳健康和平衡至关重要。

这种设置使您能够轻松地本地管理和监控您的 Kafka 集群,提供了一个强大的界面来处理 Kafka 配置和观察集群性能。

实施涵盖这些关键指标的监控策略,并利用 Kafka Manager 等工具,可以帮助您更好地理解您的 Kafka 基础设施。这不仅有助于主动维护和优化,还能让您准备好迅速有效地应对任何出现的问题。

简而言之,有效地监控 Kafka 对于事件驱动系统至关重要。关注关键指标,如代理健康、分区平衡、消息流和消费者延迟非常重要。CMAK、Prometheus 和 Grafana 等工具不仅简化了这些任务,还提供了深入可见性和分析,将原始数据转化为可操作的见解。通过监控,可以在问题成为大问题之前发现并解决,确保 Kafka 消息管道的平稳运行。

一个被监控的事件驱动系统能够处理现代数据流和负载需求的复杂性。它确保系统的每个部分都能可靠地运行,保持今天应用程序所需的性能。最终,系统的强大之处在于关注运营细节——在这里,监控不仅仅是一项常规工作,而是系统健康和长寿的一个关键方面。

摘要

随着本章的结束,让我们花一点时间回顾一下我们共同走过的旅程。我们深入到 Kafka 和 Spring Boot 的世界,组装起我们事件驱动系统的每一块。以下是我们的成就:

  • 设置 Kafka 和 ZooKeeper:我们使用 Docker 在我们的本地机器上设置了 Kafka 和 ZooKeeper,为我们的消息系统创建了一个强大的骨干。

  • 构建 Spring Boot 应用程序:我们从零开始构建了两个 Spring Boot 应用程序,一个作为事件生产者,另一个作为消费者,学习了它们如何协同工作形成一个响应式的 EDA。

  • 监控 Kafka 基础设施:我们学习了监控 Kafka 基础设施的重要性,使用 CMAK 等工具密切关注系统的健康和性能。

本章探讨的见解不仅仅是理论上的;它们可以迅速转化为你在现实场景中可以立即利用的能力。这些能力对于确保你的系统运行并保持弹性至关重要,使它们能够敏捷地适应不断变化的数据景观。在当今快速发展的技术领域,设置、集成和管理系统的能力是不可或缺的。

通过继续与我们一同学习,你不仅获得了技能集的工具;你还改进了你的开发工作流程,使其更加流畅和高效。你还在增强应用程序的耐用性和可管理性,为竞争激烈的技术领域提供优势。

随着我们迈向下一章,我们将深入了解增强开发过程的 Spring Boot 高级功能。你将发现组织代码的面向方面编程的艺术,利用 Feign 客户端实现无缝的 HTTP API 集成,并利用 Spring Boot 复杂的自动配置功能。下一章的重点是简化作为开发者的任务,使它们更加高效和富有成效。让我们携手前进,进一步扩展我们的知识。

第九章:提高生产力和开发简化

在本章中,我们的重点是提高 Spring Boot 中的生产力和简化开发。使用 Spring Boot 提高生产力包括简化配置、减少样板代码,并利用促进更快开发周期、更好的代码质量和更顺畅部署流程的集成和工具。我们将通过深入了解 Spring Boot 中的面向切面编程AOP)来开始,了解它如何通过将横切关注点从我们的主要应用程序逻辑中分离出来,帮助我们创建一个更整洁的代码库。这种方法使我们的代码更容易维护和理解。

接下来,我们将介绍 Feign 客户端。它作为一个简化与服务通信并简化 HTTP API 交互的 web 服务客户端,最终减少了重复的样板代码。

之后,我们将深入研究 Spring Boot 中的自动配置技术。这些方法允许我们根据我们的需求调整 Spring Boot 的约定优于配置哲学,从而进一步简化我们应用程序的设置过程。

记住这一点至关重要:强大的力量伴随着巨大的责任。本章还将引导我们了解在利用 Spring Boot 中的 AOP、Feign 客户端和高级自动配置功能时的陷阱和最佳实践。我们将学习如何避免常见错误,并有效地利用这些工具来构建健壮、可维护和高效的应用程序。

到本章结束时,你将掌握如何利用 Spring Boot 的强大功能来显著提高你的开发效率。你将具备实施方法和避免典型错误的专业知识,确保你的应用程序可靠、有序且易于管理。

下面是一个我们将涵盖的快速概述:

  • 在 Spring Boot 中引入 AOP

  • 使用 Feign 客户端简化 HTTP API

  • 高级 Spring Boot 自动配置

  • 常见陷阱和最佳实践

让我们开始这段旅程,解锁 Spring Boot 在你的项目中的全部潜力。

技术要求

对于本章,我们需要在我们的本地机器上做一些设置:

在 Spring Boot 中引入 AOP

让我们深入探讨 AOP。你可能想知道:“AOP 究竟是什么?”好吧,它是一种编程方法,有助于分离应用程序中的关注点,特别是那些跨越应用程序多个部分的关注点,如日志记录、事务管理或安全性。以日志记录为例;你可以在每个方法中添加日志记录行。AOP 帮助你将它们分开,这样你的主要代码就能保持干净和专注于它应该做的事情。它还会作为单独类的一部分记录所需的数据。

Spring Boot 内置了对 AOP 的支持,这使得你更容易实现这些横切关注点,而不会让你的代码变得一团糟。使用 AOP,你可以定义建议(这意味着 AOP 代表应在特定点运行的代码),切入点(你希望在代码的哪个位置运行这些建议),以及方面(建议和切入点的组合)。这意味着你可以以一致的方式自动将常见功能应用于你的应用程序,而无需干扰你服务的核心逻辑。在下一节中,我们将更详细地了解这些内容。

因此,你可能正在想:“很好,但我实际上该如何做?”这正是我们接下来要涵盖的内容。我们将带你了解如何在 Spring Boot 应用程序中设置 AOP,从基础知识开始,然后过渡到更高级的概念。到那时,你将看到 AOP 不仅能够简化你的应用程序开发,还能让你的代码更干净、更高效。

探索 AOP 的基础——连接点、切入点、建议声明和织入(weaving)

在我们深入创建我们的方面之前,让我们先简化 Spring Boot 中的 AOP 术语。理解这些概念就像为你的编程任务解锁一套工具:

  • 连接点(Join points):这些是你可以在代码中融入 AOP 方面的位置。你可以把它们看作是在你的应用程序中可以发生额外动作的机会或区域。例如,方法执行或抛出异常可以作为连接点。

  • 切入点(Pointcuts):这些决定了你的面向方面编程(AOP)功能应该应用的位置。它们作为过滤器,通知你的应用程序在哪个点执行代码。这种方法确保你的方面(aspect)只在必要时实现,而不是全局性地实现。

  • 建议声明(Advice declarations):在 AOP 中,这些扮演着重要的角色。它们定义了你在由切入点标识的选定连接点(join point)上想要采取的动作。建议声明可以在你的代码之前、之后或周围执行。例如,每次调用特定方法时自动记录日志,这在实践中就是一个示例建议(exemplifying advice)

  • 方面(Aspects):这些将所有组件整合在一起。一个方面将切入点和建议声明组合成一个包,指定“在这些位置、代码(pointcuts)中执行这个动作(advice)。”

  • 编织:这涉及到将元素集成到你的代码中。这可以在代码编译或执行阶段发生。将其视为触发 AOP 魔法的阶段,使元素能够与你的应用程序交互。

现在我们已经涵盖了你可能感兴趣的术语,让我们将这些概念应用到 Spring Boot 中。我们将指导你定义你的切面,使用切入点选择连接点,并指定你的建议应该采取的操作。随着 Spring Boot 简化 AOP 实现,你将见证这些想法如何无缝地集成到你的项目中。

构建日志切面 – 一步步示例

想象一下你正在开发一个应用程序,并希望监控其工作而不在代码中混入日志消息。这正是 AOP 突出的地方。

让我们深入探讨如何在 Spring Boot 中构建一个日志切面,以启用对应用程序中方法调用的日志记录。这种方法允许你跟踪每个方法的开始和结束时间,简化调试和监督任务:

  1. 首先,让我们从 Spring Initializr 网站创建一个新的项目(start.spring.io),并添加 Spring Web 依赖。在这个项目中我们也将使用 Gradle。点击 Generate 按钮,就像我们在前面的章节中所做的那样,然后用你最喜欢的 IDE 打开项目。

  2. 接下来,我们需要在 build.gradle 中添加 AOP starter 依赖项:

    implementation 'org.springframework.boot:spring-boot-starter-aop'
    

    这一步为你的 Spring Boot 项目配备了必要的 AOP 功能。

  3. 然后,在你的项目中创建一个新的类,并用 @Aspect 注解它,以告诉 Spring Boot 它是一个切面。让我们称它为 LoggingAspect。在这个类内部,我们将定义我们想要记录的内容和时机:

    @Aspect
    @Component
    public class LoggingAspect {
        private final Logger log = LoggerFactory.getLogger(this.getClass());
        @Around("execution(* com.packt.ahmeric..*.*(..))")
        public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("Starting method: {}", joinPoint.getSignature().toShortString());
            long startTime = System.currentTimeMillis();
            Object result = joinPoint.proceed();
            long endTime = System.currentTimeMillis();
            log.info("Completed method: {} in {} ms", joinPoint.getSignature().toShortString(), endTime - startTime);
            return result;
        }
        @Before("execution(* com.packt.ahmeric..*.*(..))")
        public void logMethodEntry(JoinPoint joinPoint) {
            log.info("Entering method: {} with args {}", joinPoint.getSignature().toShortString(), Arrays.toString(joinPoint.getArgs()));
        }
        @After("execution(* com.packt.ahmeric..*.*(..))")
        public void logMethodExit(JoinPoint joinPoint) {
            log.info("Exiting method: {}", joinPoint.getSignature().toShortString());
        }
    }
    

    在这个例子中,@Before@After@Around 注解是建议声明,它们指定了何时进行日志记录。execution(* com.packt.ahmeric..*.*(..)) 这部分是一个切入点表达式,它告诉 Spring AOP 将这些建议声明应用于应用程序中的所有方法(请注意,你可能需要将 com.packt.ahmeric 调整为匹配你的实际包结构)。

    定义了你的切面后,Spring Boot 现在将自动记录你的应用程序中每个方法的进入和退出,正如你的切入点所指定的。这种设置意味着你不需要手动为每个方法添加日志记录,从而保持你的业务逻辑整洁和清晰。

  4. 现在,让我们创建一个简单的 REST 控制器来测试这个功能。我们将简单地使用前面章节中使用的相同的 HelloController

    @RestController
    public class HelloController {
        @GetMapping("/")
        public String hello() {
            return "Hello, Spring Boot 3!";
        }
    }
    
  5. 让我们运行我们的应用程序,并对 localhost:8080/; 进行 GET 调用,我们将在控制台中观察到以下日志:

    Starting method: String HelloController.hello()
    Entering method: HelloController.hello() with args []
    Exiting method: HelloController.hello()
    Completed method: String HelloController.hello() in 0 ms
    

    你可以跟踪日志的写入位置。第一行和最后一行是在logMethodExecution中编写的,第二行,正如你所料,是在logMethodEntry中编写的,第三行是在logMethodExit中编写的。由于hello()方法是一个非常简单的方法,所以我们只有这些日志。想象一下,如果你有很多微服务,并且你想记录每个请求和响应。使用这种方法,你不需要在每个方法中编写日志语句。

在遵循这些步骤之后,我们已经成功地为我们的 Spring Boot 应用程序添加了日志功能。这个实例展示了 AOP 在管理如日志等切面问题上的有效性。AOP 组织你的代码库,并确保在不与核心业务逻辑混合的情况下进行日志记录。

在我们结束本节时,很明显 AOP 是 Spring Boot 工具箱中的一个有用工具。它简化了整个应用程序中问题的处理。像任何工具一样,当有知识和谨慎地使用时,它才能发挥最佳性能。

现在,让我们将注意力转向 Spring Boot 中另一个可以大大提高你效率的功能;Feign 客户端。在下一节中,我们将深入探讨 Feign 客户端如何简化消费 HTTP API,使其轻松连接和与服务进行通信。这在当今微服务时代尤其有用,你的应用程序可能需要与服务的交互。请保持关注。我们将看到如何通过在代码中调用一个方法来轻松建立这些连接。

使用 Feign 客户端简化 HTTP API

你是否曾经因为 Spring Boot 应用程序中制作 HTTP 调用的复杂性而感到有些不知所措?这就是 Feign 客户端发挥作用的地方,它提供了一个更简化的方法。

什么是 Feign 客户端?

Feign 客户端是一个声明式 Web 服务客户端。它使编写 Web 服务客户端变得更容易、更高效。将其视为简化应用程序通过 HTTP 与其他服务通信的方式。

Feign 客户端的魔力在于其简单性。你不需要处理 HTTP 请求和响应的低级复杂性,你只需定义一个简单的 Java 接口,Feign 就会处理其余部分。通过使用 Feign 注解来注解这个接口,你可以告诉 Feign 将请求发送到何处,发送什么,以及如何处理响应。这让你可以专注于应用程序的需求,减少对制作 HTTP 调用繁琐细节的担忧。

它提供了一个比 RestTemplate 和 WebClient 更简单的替代方案。Feign 客户端是 Spring 应用程序中客户端 HTTP 访问的一个很好的选择。虽然 RestTemplate 一直是 Spring 应用程序中同步客户端 HTTP 访问的传统选择,但它需要为每个调用编写更多的代码。另一方面,WebClient 是较新的、反应式 Spring WebFlux 框架的一部分,专为异步操作设计。它是一个强大的工具,但可能需要更多的努力去学习,尤其是如果你不熟悉反应式编程。

Feign 客户端是一个提供 RestTemplate 简单性和易用性的工具,但采用了更现代、接口驱动的方案。它抽象掉了制作 HTTP 调用所需的许多手动编码,使你的代码更干净、更易于维护。

在下一节中,我们将逐步解释如何将 Feign 客户端集成到你的 Spring Boot 应用程序中,以便无缝地与其他服务进行通信。这不仅会使你的代码更有组织性,而且在开发过程中也能节省你大量的时间。

在 Spring Boot 中实现 Feign 客户端

将 Feign 客户端集成到 Spring Boot 应用程序中可以提高你的项目效率。为了帮助你轻松地执行 HTTP API 调用,我将带你走过设置和配置过程:

  1. 首先,你需要将 Feign 依赖项包含到你的 Spring Boot 应用程序中。这一步启用了你的项目中的 Feign,并且只需在构建配置中添加几行即可。

    build.gradle中插入以下依赖项:

    dependencies {
        implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
        ...
    }
    ext {
        set('springCloudVersion', '2023.0.1')
    }
    dependencyManagement {
        imports {
            mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        }
    }
    

    通过这个变更,我们已经将所需的库导入到我们的项目中以使用 Feign 客户端。

  2. 在放置好依赖项之后,下一步是启用你的应用程序中的 Feign 客户端。这通过在你的 Spring Boot 应用程序的配置类或主应用程序类中添加一个简单的注解来完成:

    @SpringBootApplication
    @EnableFeignClients
    public class MyApplication {
        public static void main(String[] args) {
            SpringApplication.run(MyApplication.class, args);
        }
    }
    

    @EnableFeignClients注解会扫描声明为 Feign 客户端的接口(使用@FeignClient),为它们创建一个动态代理。本质上,@EnableFeignClients告诉 Spring Boot,“嘿,我们在这里使用 Feign 客户端,所以请相应地对待它们。”

  3. 配置你的 Feign 客户端涉及定义一个接口,该接口指定了你希望调用的外部 HTTP API。在这里,你使用@FeignClient来声明你的接口为 Feign 客户端,并指定如客户端名称和 API URL 等详细信息。

    下面是一个定义简单 JSON 占位符 API Feign 客户端的基本示例:

    @FeignClient(name = "jsonplaceholder", url = "https://jsonplaceholder.typicode.com")
    public interface JsonPlaceholderClient {
        @GetMapping("/posts")
        List<Post> getPosts();
        @GetMapping("/posts/{id}")
        Post getPostById(@PathVariable("id") Long id);
    }
    

    在这个例子中,JsonPlaceholderClient是一个表示对 JSON 占位符 API 客户端的接口。@FeignClient注解将JsonPlaceholderClient接口标记为 Feign 客户端,其中name指定了客户端的唯一名称,url指示外部 API 的基本 URI。接口内部的方法对应于你希望消费的端点,Spring MVC(模型-视图-控制器)注解(@GetMapping@PathVariable)定义了请求类型和参数。

  4. 我们还需要引入一个简单的Post对象,以便 JSON 响应可以映射到它:

    public record Post(int userId, int id, String title, String body) { }
    
  5. 让我们在一个示例控制器中使用这个客户端服务:

    @RestController
    public class FeignController {
        private final JsonPlaceholderClient jsonPlaceholderClient;
        public FeignController(JsonPlaceholderClient jsonPlaceholderClient) {
            this.jsonPlaceholderClient = jsonPlaceholderClient;
        }
        @GetMapping("/feign/posts/{id}")
        public Post getPostById(@PathVariable Long id) {
            return jsonPlaceholderClient.getPostById(id);
        }
        @GetMapping("/feign/posts")
        public List<Post> getAllPosts() {
            return jsonPlaceholderClient.getPosts();
        }
    }
    

    在这个控制器中,我们已经将jsonPlaceholderClient注入到我们的控制器中,并公开了jsonPlaceholderClient为我们提供的相同端点。通过这种方式,我们可以测试我们的实现是否正常工作。

  6. 现在,我们可以启动我们的应用程序,并对localhost:8080/feign/posts和[http://localhost:8080/feign/posts/65]进行一些 GET 调用,我们将确保我们的应用程序可以正确地向服务器发出 REST 调用并获取响应。

这就是 Spring Boot 应用程序中 Feign 客户端的基本设置和配置的全部内容。我们已经添加了必要的依赖项,在我们的应用程序中启用了 Feign 客户端,并定义了一个接口来与外部 HTTP API 交互。完成这些步骤后,你就可以无缝地进行 API 调用了。

我们刚刚穿越了 Feign 客户端的世界,发现了它是如何简化 Spring Boot 应用程序中服务之间通信的。Feign 客户端的美丽之处在于它的简单和高效,去除了 HTTP 调用的复杂性,让我们专注于应用程序中真正重要的事情。有了 Feign 客户端,我们可以定义接口并轻松连接我们的服务,使外部 API 调用感觉像本地方法调用。

随着我们结束 Feign 客户端的讨论,是时候深入了解 Spring Boot 的能力,特别是它的高级自动配置功能了。想象一下,Spring Boot 不仅处理基本的设置,还能根据上下文和你包含的库智能地配置你的应用程序。这就是高级自动配置的力量。

高级 Spring Boot 自动配置

Spring Boot 的强大之处在于它能够快速为你设置,所需设置最少。这个特殊功能在很大程度上归功于它的自动配置能力。让我们探索自动配置包含什么,以及 Spring Boot 是如何适应处理更复杂的情况的。

什么是高级自动配置?

当启动一个新的 Spring Boot 项目时,你并不是从零开始。Spring Boot 会检查你的类路径中的库、你定义的 bean 以及你配置的属性,以自动设置你的应用程序。这可能包括建立 Web 服务器、配置数据库连接,甚至为安全措施准备你的应用程序。这就像有一个智能助手,根据它感知到你可能需要什么来安排一切。

然而,随着应用程序的扩展和变得更加复杂,基本的自动配置可能无法涵盖所有场景。这就是高级自动配置介入的地方。Spring Boot 已经发展到允许你个性化并增强这个自动配置过程。它为你提供了与 Spring Boot 通信的手段,说“嘿,我认可你的努力,但让我们在这里和那里做一些调整。”

例如,你可能会遇到一个不符合标准自动配置模型的具体数据源,或者你可能需要以适应应用程序需求的方式独特地配置第三方服务。高级自动配置允许进行更深入的定制,让你能够影响 Spring Boot 如何设置你的应用程序以完美满足你的要求。

高级自动配置的价值在于其能够在保持 Spring Boot 的简洁性和效率的同时,提供处理更复杂配置的灵活性。它结合了使用 Spring Boot 快速启动的便利性以及为复杂场景微调配置的选项。

展望未来,我们将深入探讨利用这些高级自动配置功能。我们将涵盖创建自定义自动配置、理解条件配置以及开发自己的启动器等主题。这些知识将使你能够精确地调整 Spring Boot 的自动配置以满足应用程序的需求,从而简化并增强你的开发过程。

理解条件配置

Spring Boot 能够根据类路径中找到的类自动配置你的应用程序,这不是很酷吗?更酷的是,它的灵活性,归功于 @Conditional 注解。这些注解允许 Spring Boot 在运行时确定是否应该应用特定的配置。这意味着你可以通过调整应用程序运行的环境来定制应用程序的行为,而无需更改代码——只需调整其操作环境即可。

@Conditional 注解使 Spring Boot 能够根据特定条件做出决策。例如,你可能希望只有在设置了某个属性或存在特定类时才加载一个 Bean。Spring Boot 提供了各种 @Conditional 注解来满足不同的场景,包括 @ConditionalOnProperty@ConditionalOnClass@ConditionalOnExpression

假设我们决定在特定环境中不使用 LoggingAspect,而更愿意通过我们的属性文件来管理它。

首先,我们需要引入以下属性来不使用 LoggingAspect

logging.aspect.enabled=false

然后,我们可以使用这个属性在我们的 LoggingAspect 类中使用 @ConditionalOnExpression

@Aspect
@Component
@ConditionalOnExpression("${logging.aspect.enabled:false}")
public class LoggingAspect {
    // No change in the rest of the code
}

以这种方式,@ConditionalOnExpression 注解可以直接读取 logging.aspect.enabled 属性值。这个条件根据属性值创建 LoggingAspect 实例。如果我们的值是 true,那么我们的 loggingAspect 类将工作并记录方法。如果值是 false,那么这个类将不会被初始化,并且控制台输出将不会有日志。

使用条件设置是一种在软件中创建适应性强、特定上下文的功能的有价值技术。无论你是在处理一个根据特定条件需要不同行为的代码库,还是在开发一个根据配置设置调整其特性的应用程序,使用@Conditional注解提供了一种有组织且可持续的方法来实现这一目标。

使用条件设置的真正优势在复杂的软件系统和库中变得明显,在这些系统中,高度的适应性是必要的。条件设置使你能够构建仅在特定条件下激活的组件,从而增强你应用程序的模块化和灵活性,以适应各种情况。

在学习了如何有效地使用条件属性来启用或禁用诸如LoggingAspect等特性之后,我们现在准备探索本章所学的特性的常见陷阱和最佳实践。

常见陷阱和最佳实践

开始掌握 Spring Boot 的旅程涉及导航其多样化的生态系统,其中包括 AOP、Feign 客户端和高级自动配置。了解最佳实践并关注常见陷阱对于开发者有效地利用这些强大的工具至关重要。

本节旨在为开发者提供利用这些工具所需的知识,强调做出与特定项目要求相一致的良好决策的重要性。通过概述最佳使用的关键策略,以及解决常见错误和实际解决方案,辅以现实世界的示例以增强清晰度,我们为创建整洁、高效和可持续的 Spring Boot 应用程序铺平了道路。这次探索不仅关注利用 Spring Boot 的特性,而且关注以最大化项目潜力的方式来利用这些特性。

接受 Spring Boot 的最佳实践——AOP、Feign 客户端和高级自动配置

Spring Boot 是一个强大的开发者平台,提供了诸如 AOP、Feign 客户端和复杂的自动配置等特性,以简化应用程序的开发过程。然而,充分利用这些工具需要彻底掌握它们的特性以及它们如何与你的项目相匹配。让我们探讨一些推荐的方法,以最佳方式利用这些功能。

AOP 的最佳实践

面向切面编程(AOP)是一种通过将日志、安全性和事务管理等不同方面从核心业务逻辑中分离出来来组织应用程序的绝佳方式。为了充分利用它,请执行以下操作:

  • 谨慎使用 AOP:仅对跨越你代码多个部分的功能使用它。过度使用它会使应用程序的流程更难以理解。

  • 定义精确的切入点:确保您的切入点表达式是具体的,以避免意外应用建议,这可能会导致性能问题或错误。

  • 保持建议简单:建议应该是直接和专注的。将复杂逻辑添加到建议中可能会影响应用程序的性能。

Feign Client 的最佳实践

Feign Client 通过将接口声明转换为可用的 HTTP 客户端,使您的应用程序更容易通过 HTTP 与其他服务交互。为了有效地使用 Feign Client,请执行以下操作:

  • 保持配置集中化:为所有 Feign Clients 创建一个集中的配置类,以保持设置的组织性和易于管理。

  • 有效处理错误:开发自定义错误解码器来管理应用程序交互的服务提供的各种响应,确保健壮的错误处理。

  • 使用模拟进行测试:利用 Feign Client 的模拟和存根功能,在单元和集成测试中避免进行真实的 HTTP 调用。

高级自动配置的最佳实践

Spring Boot 的高级自动配置功能提供了根据您的需求自定义框架的灵活性。以下是一些如何有效利用它的建议:

  • 使用@Conditional注解确保只有在满足特定条件时才加载您的 bean,有助于保持应用程序的简洁。

  • 防止冲突:在开发自定义自动配置时,务必检查任何现有配置,以防止可能导致的意外 bean 加载问题的冲突。

  • @AutoConfigureOrder:在具有多个自动配置的项目中,使用@AutoConfigureOrder来管理它们的顺序和控制 bean 创建的顺序。

有效利用 AOP、Feign Client 和高级自动配置

为了有效地利用 AOP、Feign Client 和高级自动配置,掌握这些工具的细节并根据项目需求做出明智的决策至关重要。以下是一些需要考虑的关键点:

  • 评估您的需求:在深入之前,评估您的应用程序真正需要什么。并非每个项目都会从 AOP 的复杂性或 Feign Client 在每次服务交互中使用中受益。

  • 理解影响:考虑这些工具如何影响性能、可维护性和可测试性。AOP 可能会使调试复杂化;Feign Client 在 HTTP 调用上添加了一层,而高级自动配置需要深入了解 Spring Boot 的内部工作原理。

  • 保持最新状态:Spring Boot 随着每个版本的发布,迅速地引入新的特性和增强。保持对最新版本和推荐实践的更新,以充分利用 Spring Boot 提供的全部潜力。

Spring Boot 提供了一套全面的工具集,用于开发稳健且高效的应用程序。通过遵循 AOP、Feign 客户端和高级自动配置的最佳实践,你可以创建出不仅强大且可扩展,而且易于管理和演化的应用程序。请记住,要深思熟虑地使用这些工具,以确保它们在不增加不必要复杂性的情况下增强你的项目。

在 Spring Boot 中导航常见陷阱 – AOP、Feign 客户端和高级自动配置

Spring Boot 通过处理许多复杂任务简化了 Java 开发并加快了开发过程。但请记住,随着其益处而来的是需要谨慎。让我们讨论一下开发者在使用 Spring Boot 时遇到的典型错误,特别是与 AOP、Feign 客户端和高级自动配置相关的问题,以及如何有效地避免这些问题。

过度使用 AOP

  • 常见陷阱:与 AOP 相关的一个常见错误是过度使用它来处理本可以更好地在其他地方管理的横切关注点。这种误用可能导致性能问题,并使调试更加复杂,因为执行流程可能变得不清晰。

  • 预防策略:谨慎地使用 AOP。将其保留用于真正的横切关注点,如日志记录、事务管理或安全。始终评估是否有更简单、更直接的方法来实现相同的目标,而无需引入方面。

配置 Feign 客户端错误

  • 常见陷阱:配置 Feign 客户端很容易出错。一个常见的错误是忽视根据目标服务的需求定制客户端,这可能导致超时或错误处理不当等问题。

  • 预防策略:为与你链接的服务个性化你的 Feign 客户端。根据需要调整超时、错误处理和日志记录。利用 Feign 客户端的特性,如自定义编码器和解码器,以针对特定服务定制客户端。

忽视自动配置条件

  • 常见陷阱:虽然 Spring Boot 的自动配置功能强大,但如果管理不当,可能会导致不希望的结果。开发者常常依赖 Spring Boot 自动配置一切,而不考虑潜在的后果,导致不必要的 Bean 被创建或关键的 Bean 被假设为已自动配置。

  • 使用 @Conditional 注解来调整你的配置,确保只有在必要时才创建 Bean。此外,利用 @ConditionalOnMissingBean 来设置默认值,只有在该类型的其他 Bean 未设置时才会生效。

真实世界示例 – AOP 中错误配置的代理范围

在一个应用程序中使用 AOP 进行事务管理的情况下,开发者错误地将方面添加到单例作用域服务的类级别方法中。这个错误导致整个服务在方法执行期间被锁定,从而形成瓶颈。

为了防止这个问题,确保您的代理被正确地范围化。在实现事务管理时,确保在考虑应用程序的并发需求的同时,将方面应用于改变状态的方法周围。熟悉 Spring 的代理机制,根据您的具体情况决定使用基于接口的(JDK 代理)或基于类的(代码生成库[CGLIB]代理)代理。

通过理解这些工具并针对您项目的独特需求做出明智的选择,您可以避免常见的陷阱并有效地利用 Spring Boot 的功能,从而实现维护良好、高效的程序。关键在于简化您的开发过程并增强您应用程序的可靠性。

总是记住,目标不仅仅是利用 Spring Boot 的功能,而是要深思熟虑地使用它们。

摘要

在本章中,我们深入探讨了 Spring Boot 的一些最具影响力的功能,扩展了我们创建强大和高效应用程序的工具集。让我们回顾一下我们讨论的内容:

  • 探索面向切面编程(AOP): 我们探讨了如何通过分离诸如日志记录和安全等任务来使用 AOP 更有效地构建代码。这简化了代码管理和理解。

  • 使用 Feign 客户端简化 HTTP: 我们介绍了 Feign 客户端,这是一个简化通过 HTTP 连接到其他服务的工具。它专注于保持您的代码整洁并提高您使用 Web 服务的体验。

  • 利用 Spring Boot 自动配置进行进步: 我们揭示了高级自动配置方法,展示了 Spring Boot 如何根据您的特定需求进行定制,进一步简化了您的开发工作流程。

  • 避免常见问题并采用最佳实践: 通过讨论常见问题和最佳实践,您已经获得了有效利用这些工具的见解,以确保您的应用程序不仅强大,而且易于维护和更新。

为什么这些课程至关重要?它们不仅超越了使用 Spring Boot 的功能,而且强调了深思熟虑地使用它们。通过掌握和应用我们涵盖的概念,您将朝着成功创建不仅强大高效,而且有组织且易于管理的应用程序迈进。关键是简化您的开发过程并增强您应用程序的可靠性。

当我们结束这本书时,回顾一下您获得的关键技能:掌握高级 Spring Boot 功能、实现架构模式以及保护应用程序。您还学习了关于反应式系统、数据管理和使用 Kafka 构建事件驱动系统。装备了这些工具,您现在可以有效地应对现实世界项目。祝贺您完成这段旅程,并祝您在开发工作中应用这些强大技术取得成功!

posted @ 2025-09-12 13:57  绝不原创的飞龙  阅读(46)  评论(0)    收藏  举报