Spring-Boot-启动指南-一-

139 阅读1小时+

Spring Boot 启动指南(一)

原文:zh.annas-archive.org/md5/8803f34bb871785b4bbbecddf52d5733

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎

“风筝不是随风上升的,而是逆风上升的。”

约翰·尼尔,来自《企业与毅力》(《每周镜报》)

欢迎来到Spring Boot: Up and Running。很高兴你在这里。

今天有其他的 Spring Boot 书籍可供选择。这些都是由优秀的作者写成的好书。但每位作者在他们的材料中都做出了决定,包括什么要包含在内,什么要排除,如何呈现所选内容,以及许多大大小小的决策,使他们的书籍独特。对于一个作者而言,似乎是可选的材料,对于另一个作者而言可能是绝对必要的。我们都是开发人员,像所有开发人员一样,我们有自己的看法。

我认为有一些遗漏的部分,我觉得这些部分要么是必需的,要么对于对 Spring Boot 新手有帮助。随着我与全球各地越来越多的开发人员互动,这些遗漏的部分列表也在增加,他们在 Spring Boot 的旅程中处于不同的阶段,用不同的方式学习不同的东西。因此出现了这本书。

如果您是 Spring Boot 的新手,或者您觉得强化基础知识将会很有用 — 面对现实,什么时候不强化基础知识都不合适呢? — 这本书就是为您而写的。这是一个温和的介绍,涵盖了 Spring Boot 的关键能力,并进入到这些能力在现实世界中的有用应用。

感谢您加入我的旅程。让我们开始吧!

本书使用的约定

本书使用以下排版约定:

斜体

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

常量宽度

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

常量宽度粗体

显示用户应逐字输入的命令或其他文本。

常量宽度斜体

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

提示

此元素表示提示或建议。

此元素表示一般注释。

警告

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

使用代码示例

附加材料(代码示例,练习等)可在https://resources.oreilly.com/examples/0636920338727下载。

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

这本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在程序和文档中使用它。除非您复制了大部分代码,否则无需征得我们的许可。例如,编写一个使用本书中几个代码片段的程序不需要许可。销售或分发 O’Reilly 书籍中的示例需要许可。

引用本书并引用示例代码回答问题无需许可。将本书中大量示例代码整合到产品文档中需要许可。

我们欣赏但通常不需要署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Spring Boot: Up and Running by Mark Heckler (O’Reilly). Copyright 2021 Mark Heckler, 978-1-098-10339-1.”

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

致谢

我无法再多次感谢那些鼓励我写这本书以及在我写作期间给予我鼓励的每个人。如果你阅读了早期版本并提供反馈,甚至在 Twitter 上说了一句好话,你都不知道这对我意味着多少。我由衷地感谢你们。

能够实现这一切,并不仅仅是一个有希望的计划,有一些人使之成为可能:

致我的老板、导师和朋友 Tasha Isenberg。Tasha,你与我合作以适应时间表,当形势紧急时,你为我铺平道路,让我能够迎头冲刺并达到关键的截止日期。能在 VMware 内部有一个理解的倡导者和坚定的支持者,我感激不尽。

致 Spring Boot、Spring Cloud、Spring Batch 的创始人,以及无数 Spring 项目的贡献者,David Syer 博士。您的洞察力和反馈真的非常出色和深思熟虑,我对您所做的一切感激不尽。

致 Spring Data 团队成员 Greg Turnquist。感谢你的严谨目光和直率反馈;你提供了宝贵的额外视角,通过这样做使这本书变得更加出色。

致我的编辑们,Corbin Collins 和 Suzanne(Zan)McQuade。从概念到完成,你们无微不至的支持鼓励我创作出最好的作品,并在外部环境似乎要打破的期限下,鼓励我达成目标。我无法要求更好的了。

致 Rob Romano、Caitlin Ghegan、Kim Sandoval 以及整个 O'Reilly 制作团队。你们帮助我走完了最后一英里,不论从字面意义上还是从实际意义上,把这本书真正投入生产

最后但也是最重要的,致我聪明、充满爱心并且极度耐心的妻子 Kathy。说你激励并使我能够做我所做的一切,简直是低估了。从我内心深处,谢谢你为一切

第一章:简介 Spring Boot

本章探讨了 Spring Boot 的三个核心特性以及它们如何作为开发者的增强因子。

Spring Boot 的三大核心特性

Spring Boot 的三个核心特性是简化的依赖管理、简化的部署和自动配置,这些是构建一切的基础。

简化依赖管理的起步

Spring Boot 的一个天才之处在于它使依赖管理变得…可管理。

如果您已经开发了任何重要的软件一段时间,您几乎肯定不得不应对围绕依赖管理的几个头痛。您在应用程序中提供的任何功能通常都需要一些“前线”依赖项。例如,如果您想提供一个 RESTful web API,您必须提供一种方法来通过 HTTP 公开端点,监听请求,将这些端点与处理这些请求的方法/函数绑定,然后构建并返回适当的响应。

几乎每个主要依赖项都包含许多其他次要依赖项,以实现其承诺的功能。继续我们提供 RESTful API 的示例,我们可能期望看到一组依赖项(在某种合理但有争议的结构中),其中包括提供特定格式(例如 JSON、XML、HTML)响应的代码;编组/解编组对象以请求的格式;监听和处理请求并返回相同响应的代码;解码用于创建多功能 API 的复杂 URI 的代码;支持各种传输协议的代码;以及更多。

即使对于这个相当简单的示例,我们在我们的构建文件中已经可能需要大量依赖项。而且在此时,我们甚至还没有考虑我们可能希望在我们的应用程序中包含的功能。

现在,让我们谈谈版本。谈谈每一个依赖项的版本。

将库一起使用需要一定的严谨性,因为特定依赖项的一个版本可能仅已与另一个特定依赖项的特定版本一起测试(或者甚至能够正确工作)。当这些问题不可避免地出现时,它导致了我所说的“依赖性打地鼠”。

就像其同名的嘉年华游戏一样,“依赖性打地鼠”可能是一种令人沮丧的经历。与其同名不同的是,在追逐和解决由于依赖关系之间的不匹配而引起的错误时,没有奖品,只有难以捉摸的结论性诊断和浪费的小时

Spring Boot 及其起步项目登场。Spring Boot 起步项目是围绕一个被证明的前提建立的材料清单(BOM),即您几乎每次提供特定功能时都以几乎相同的方式提供它。

在上一个例子中,每次我们构建一个 API 时,我们都会暴露端点,监听请求,处理请求,对象之间的转换,以及使用一个特定协议通过电线发送和接收数据等等。这种设计/开发/使用模式变化不大;它是整个行业广泛采用的方法,几乎没有变化。像其他类似的模式一样,它方便地被 Spring Boot starter 捕获。

添加一个单一的 starter,例如spring-boot-starter-web,提供所有相关功能在单一应用程序依赖项中。由该单一 starter 包含的所有依赖项也是版本同步的,这意味着它们已经成功地一起测试过,包含的库 A 版本与包含的库 B、C、D 等版本一起正常运行。这极大地简化了您的依赖列表和您的生活,几乎消除了您需要提供应用程序关键功能时可能遇到的难以识别的版本冲突的可能性。

在那些罕见的情况下,当您必须整合由包含的依赖项的不同版本提供的功能时,您可以简单地覆盖已测试的版本。

注意

如果必须覆盖依赖项的默认版本,请这样做...但您应该增加测试的级别,以减少因此引入的风险。

如果对于您的应用程序来说,某些依赖项是不必要的,您也可以将它们排除在外,但同样要注意谨慎。

总的来说,Spring Boot 的 starter 概念大大简化了您的依赖关系,并减少了向应用程序添加整套功能所需的工作。它还显著减少了您测试、维护和升级它们所需的开销。

可执行的 JAR 文件,简化部署过程。

很久以前,在应用服务器漫游地球的日子里,Java 应用程序的部署是一件复杂的事情。

为了提供一个工作的应用程序,比如具有数据库访问权限的微服务今天和几乎所有的单体应用过去和现在,你需要做以下几点:

  1. 安装并配置应用服务器。

  2. 安装数据库驱动程序。

  3. 创建数据库连接。

  4. 创建连接池。

  5. 构建并测试你的应用程序。

  6. 将你的应用程序及其(通常很多的)依赖项部署到应用服务器。

注意,此列表假定您已由管理员配置了机器/虚拟机,并且在某些时候您独立于此过程创建了数据库。

Spring Boot 彻底改变了这种繁琐的部署过程,并将以前的步骤合并为一个,或者也许是两个,如果将一个单一文件复制或cf push到目标视为一个实际的步骤的话。

Spring Boot 并不是所谓的超级 JAR 的起源,但它革新了它。与从应用程序 JAR 文件和所有依赖的 JAR 文件中分离出每个文件,然后将它们组合成单个目标 JAR 文件(有时称为阴影)不同,Spring Boot 的设计者们从一个真正新颖的角度来看待问题:如果我们可以嵌套 JAR 文件,保留它们的预期和交付格式呢?

将 JAR 文件进行嵌套而不是对它们进行阴影处理可以减轻许多潜在问题,因为在依赖 JAR A 和依赖 JAR B 使用不同版本的 C 时,不会遇到潜在的版本冲突问题;它还消除了由于重新打包软件并与其他使用不同许可证的软件组合而导致的潜在法律问题。保持所有依赖的 JAR 文件以其原始格式干净地避免了这些及其他问题。

如果有需要,提取 Spring Boot 可执行 JAR 文件的内容也是非常简单的。在某些情况下这样做有一些很好的理由,我在本书中也会讨论这些理由。现在,只需知道 Spring Boot 可执行 JAR 文件已经为你准备好了。

那个单一的 Spring Boot JAR 文件及其所有依赖使得部署变得轻而易举。与收集和验证所有依赖项是否已部署相比,Spring Boot 插件确保它们全部被压缩到输出的 JAR 文件中。一旦你有了这个,应用程序可以在任何有 Java 虚拟机(JVM)的地方运行,只需执行类似java -jar <SpringBootAppName.jar>的命令。

还有更多。

通过在构建文件中设置一个属性,Spring Boot 构建插件还可以使单个 JAR 完全(自行)可执行。假设 JVM 存在,而不是必须键入或脚本化整个麻烦的java -jar <SpringBootAppName.jar>命令行,你只需简单地键入<SpringBootAppName.jar>(当然,替换为你的文件名),一切都搞定。没有比这更简单的了。

自动配置

对于那些对 Spring Boot 还不熟悉的人来说,自动配置有时被称为“魔术”,可能是 Spring Boot 为开发者带来的最大“乘数效应”。我经常把它称为开发者的超能力:Spring Boot 通过为广泛使用和重复使用的案例带来观点,极大地提升了生产力

软件中的观点?这有什么帮助?!?

如果你做开发时间很长,无疑会注意到某些模式经常重复出现。当然,不是完全一样,但高达 80-90%的时间,事情都在某种设计、开发或活动范围内。

我前面提到过软件中的这种重复,这就是使得 Spring Boot 的启动器惊人一致和有用的原因。这种重复也意味着,当涉及到必须编写的代码以完成特定任务时,这些活动非常适合优化。

借用 Spring Data 的例子,这是一个与 Spring Boot 相关且启用的项目,我们知道每次访问数据库时,我们需要打开某种类型的连接到该数据库。我们也知道当我们的应用程序完成其任务时,必须关闭该连接以避免潜在问题。在此期间,我们可能会使用查询(简单和复杂、只读和写入使能)向数据库发出许多请求,并且这些查询将需要一些努力来正确创建。

现在想象一下我们能够简化所有这些。在我们指定数据库时自动打开连接。在应用程序终止时自动关闭连接。遵循简单而预期的约定,以最小的努力从开发者那里自动创建查询。甚至通过简单约定再次轻松定制那些最小代码,创建可靠一致且高效的复杂定制查询。

这种编码方式有时被称为约定优于配置,如果您对某种约定不熟悉,乍一看可能会有些不适(无论是打算的还是其他)。但如果您以前实现过类似功能,写过数百行重复、令人昏昏欲睡的设置/拆卸/配置代码以完成甚至最简单的任务,那么这就像一股清新的空气。Spring Boot(以及大多数 Spring 项目)遵循约定优于配置的口号,确保如果您遵循简单、成熟和详细记录的约定来做某事,您需要编写的配置代码将是最小甚至完全没有。

自动配置给你超能力的另一种方式是 Spring 团队对“开发者优先”环境配置的激烈关注。作为开发者,当我们能够专注于手头的任务而不是无数的设置琐事时,我们的生产力最高。Spring Boot 是如何实现这一点的呢?

让我们借用另一个与 Spring Boot 相关的项目 Spring Cloud Stream 的例子:当连接到消息平台如 RabbitMQ 或 Apache Kafka 时,开发者通常必须指定某些设置以连接和使用该平台——主机名、端口、凭据等。专注于开发体验意味着在未指定任何设置时提供默认值,有利于开发者在本地工作:localhost、默认端口等。这在观点上是有意义的,因为在开发环境中几乎 100%一致,但在生产环境中则不然。在生产环境中,由于平台和托管环境差异很大,您需要提供特定的值。

使用这些默认设置的共享开发项目还可以消除开发环境设置所需的大量时间。这对你有利,也对你的团队有利。

在一些情况下,您的具体用例可能并不完全符合典型用例的 80–90%,而是属于另外的 10–20%有效用例。在这些情况下,可以有选择地覆盖自动配置,甚至完全禁用它,但当然,您会失去所有的超能力。覆盖某些默认设置通常是通过设置一个或多个属性为您希望的值,或者提供一个或多个 Bean 来完成 Spring Boot 通常会为您自动配置的某些任务。换句话说,当您必须这样做时,通常这是一个非常简单的事情。总之,自动配置是一个强大的工具,默默无闻地为您工作,使您的生活更轻松,您的生产力更高。

摘要

Spring Boot 的三个核心特性是简化的依赖管理、简化的部署以及自动配置。这三者都是可定制的,但您很少需要这样做。这三者共同努力,让您成为一个更好、更高效的开发者。Spring Boot 给您带来飞跃的感觉!

在下一章中,我们将探讨一些在创建 Spring Boot 应用程序时可供选择的优秀选项。选择多多益善!

第二章:选择您的工具并入门

要开始创建 Spring Boot 应用程序很容易,您很快就会看到。 最困难的部分可能是决定您想要选择哪个可用选项。

在本章中,我们将探讨您可以用来创建 Spring Boot 应用程序的一些出色选择:构建系统、语言、工具链、代码编辑器等等。

Maven 还是 Gradle?

从历史上看,Java 应用程序开发人员在项目构建工具方面有几个选择。 随着时间的推移,一些选择因有充分理由而不再受欢迎,现在我们作为一个社区聚集在两个选择周围:Maven 和 Gradle。 Spring Boot 同样支持两者。

Apache Maven

Maven 是一个流行且可靠的构建自动化系统选择。 它已经存在了相当长的时间,最早在 2002 年开始,并于 2003 年成为 Apache Software Foundation 的一个顶级项目。 其声明性方法在当时和现在(仍然)在概念上比替代方案更简单:只需创建一个名为 pom.xml 的 XML 格式文件,其中包含所需的依赖项和插件。 当您执行 mvn 命令时,可以指定完成的“阶段”,以完成像编译、删除先前的输出、打包、运行应用程序等所需的任务:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>11</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

Maven 还按约定创建并期望特定的项目结构。 除非您准备好与您的构建工具作斗争,否则通常不应该偏离这种结构,如果有的话,这是一个适得其反的任务。 对于绝大多数项目来说,传统的 Maven 结构完全有效,因此您不太可能需要更改它。 图 2-1 显示了具有典型 Maven 项目结构的 Spring Boot 应用程序。

sbur 0201

图 2-1。在 Spring Boot 应用程序中的 Maven 项目结构
注意

有关 Maven 预期项目结构的更多详细信息,请参阅 The Maven Project’s Introduction to the Standard Directory Layout

如果有一天,当 Maven 的项目约定和/或对构建的严格结构化方法变得过于限制性时,还有另一个绝佳选择。

Gradle

Gradle 是构建 Java 虚拟机(JVM)项目的另一个流行选项。 首次发布于 2008 年,Gradle 利用特定领域语言(DSL)生成了一个既简洁又灵活的 build.gradle 构建文件。 以下是一个 Spring Boot 应用程序的 Gradle 构建文件示例。

plugins {
	id 'org.springframework.boot' version '2.4.0'
	id 'io.spring.dependency-management' version '1.0.10.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

Gradle 允许开发人员选择使用 Groovy 或 Kotlin 编程语言进行 DSL。 它还提供了几个功能,旨在减少您等待项目构建的时间,例如以下内容:

  • Java 类的增量编译

  • Java 的编译避免(在没有更改发生的情况下)

  • 项目编译的专用守护程序

在 Maven 和 Gradle 之间做出选择

在这一点上,你选择的构建工具可能听起来并不像是一个选择。为什么不简单地选择 Gradle 呢?

Maven 更为严格的声明性(有人可能会说是有主见的)方法使得从一个项目到另一个项目、从一个环境到另一个环境都保持了一致性。如果你遵循 Maven 的方式,通常不会出现太多问题,让你可以专注于你的代码,而不必过多地纠缠于构建过程中。

作为围绕编程/脚本构建的构建系统,Gradle 有时也会在消化新语言版本的初始发布时遇到问题。Gradle 团队反应迅速,通常能够迅速解决这些问题,但如果你喜欢(或必须)立即深入了解早期版本的语言发布,这值得考虑。

对于构建,Gradle 可能会更快——有时甚至 显著 更快,特别是在较大的项目中。也就是说,对于你典型的基于微服务的项目,类似的 Maven 和 Gradle 项目之间的构建时间可能不会有太大差异。

对于简单项目和具有非常复杂构建需求的项目来说,Gradle 的灵活性可能是一种清新的空气。但特别是在那些复杂的项目中,Gradle 的额外灵活性可能会导致在事情不按预期方式工作时花费更多时间进行调整和故障排除。TANSTAAFL(没有免费午餐)。

Spring Boot 支持 Maven 和 Gradle 两种构建工具,如果你使用 Initializr(将在接下来的章节中介绍),项目和所需的构建文件将被创建,以便让你快速启动。简而言之,尝试两者,然后选择最适合你的方式。Spring Boot 将乐意支持你。

Java 还是 Kotlin?

JVM 上有许多可供使用的语言,但其中两种使用最广泛。一种是最初的 JVM 语言 Java;另一种是相对较新的 Kotlin。在 Spring Boot 中,两者都是完整的一流公民。

Java

取决于你将公开发布的 1.0 版本还是项目起源视为其官方诞生日期,Java 已经存在了 25 或 30 年。然而,它绝不是停滞不前的。自 2017 年 9 月以来,Java 已经采用了六个月的发布周期,导致比以前更频繁地改进特性。维护者已经清理了代码库,并修剪了被新特性所取代的特性,同时引入了由 Java 社区驱动的重要特性。Java 比以往任何时候都更加充满活力。

那种充满活力的创新步伐,再加上 Java 的长寿和持续专注于向后兼容性,意味着全球每天都有无数的 Java 店在维护和创建关键的 Java 应用程序。其中许多应用程序使用 Spring。

Java 构建了几乎整个 Spring 代码库的坚实基础,因此,它是构建 Spring Boot 应用程序的绝佳选择。检查 Spring、Spring Boot 和所有相关项目的代码只需访问其托管的 GitHub 页面,然后在线查看或克隆项目以离线审阅。并且,Java 编写的大量示例代码、示例项目和“入门指南”使得使用 Java 编写 Spring Boot 应用程序可能比市场上任何其他工具链组合都更受支持。

Kotlin

相对而言,Kotlin 是一个新生力量。由 JetBrains 在 2010 年创建,并在 2011 年公开,Kotlin 的创建旨在填补 Java 可用性中的感知空白。Kotlin 从一开始就被设计成:

简洁

Kotlin 需要最少的代码来清晰地向编译器(以及自己和其他开发人员)传达意图。

安全

Kotlin 默认消除了空指针相关的错误,除非开发人员明确覆盖行为以允许它们。

可互操作

Kotlin 旨在与所有现有的 JVM、Android 和浏览器库无摩擦地互操作。

工具友好

在多种集成开发环境(IDE)或命令行中构建 Kotlin 应用程序,就像构建 Java 应用程序一样。

Kotlin 的维护者们以极大的关怀和速度扩展语言的能力。虽然没有将 25 年以上的语言兼容性作为核心设计重点,但他们迅速添加了非常有用的功能,这些功能可能会在 Java 的某些版本中出现。

除了简洁外,Kotlin 也是一种非常流畅的语言。在不深入细节的情况下,几个语言特性有助于这种语言优雅,其中包括扩展函数中缀符号。稍后我会更深入地讨论这个概念,但 Kotlin 使得这样的语法选项成为可能:

infix fun Int.multiplyBy(x: Int): Int { ... }

// calling the function using the infix notation
1 multiplyBy 2

// is the same as
1.multiplyBy(2)

想象一下,定义自己更流畅的“语言内语言”的能力可以成为 API 设计的利器。结合 Kotlin 的简洁性,这使得用 Kotlin 编写的 Spring Boot 应用程序甚至比其 Java 对应版本更短更易读,而不会丢失意图的传达。

Kotlin 自 2017 年秋季发布版本 5.0 以来,就成为 Spring Framework 的全面一等公民,随后通过 Spring Boot(2018 年春季)和其他组件项目获得全面支持。此外,所有 Spring 文档都在扩展以包含 Java 和 Kotlin 的示例。这意味着实际上,你可以像使用 Java 一样轻松地用 Kotlin 编写整个 Spring Boot 应用程序。

在 Java 和 Kotlin 之间做出选择

令人惊讶的是,你实际上并不需要做选择。Kotlin 编译成与 Java 相同的字节码输出;由于 Spring 项目可以同时包含 Java 源文件和 Kotlin,并且可以轻松调用两者的编译器,因此你甚至可以在同一个项目中使用更合理的语言。这种方法算不错吧?

当然,如果你更偏好其中一个,或者有其他个人或专业限制,你显然可以完全使用其中之一开发整个应用程序。有选择总是好的,不是吗?

选择 Spring Boot 的版本

对于生产应用程序,你应该始终使用当前版本的 Spring Boot,但以下是一些临时和狭隘的例外:

  • 你当前正在运行一个较旧版本,但正在按某种顺序进行升级、重新测试和部署应用程序,以至于你还没有到达这个特定的应用程序。

  • 你当前正在运行一个较旧版本,但存在已知冲突或错误,你已向 Spring 团队报告,并被告知等待 Boot 或相关依赖项的更新。

  • 你需要在 GA(正式发布)之前的快照、里程碑或发布候选版本中利用功能,并且愿意接受尚未声明 GA、即“可供生产使用”的代码所固有的风险。

注意

快照、里程碑和发布候选(RC)版本在发布前经过了广泛测试,因此已经付出了大量工作来确保它们的稳定性。然而,在完整的 GA 版本获得批准和发布之前,总是存在 API 更改、修复等的可能性。对于你的应用程序来说风险很低,但你需要自己决定(并测试和确认)在考虑使用任何早期版本软件时这些风险是否可管理。

Spring Initializr

创建 Spring Boot 应用程序有很多种方式,但大多数都会指向一个起点:Spring Initializr,如 图 2-2 所示。

sbur 0202

图 2-2. Spring Initializr

有时简称为其网址,start.spring.io,Spring Initializr 可以从突出的 IDE 项目创建向导、命令行或者最常见的是通过网页浏览器访问。通过网页浏览器使用还提供了一些其他渠道无法(目前)获取的额外实用功能。

要开始以“最佳方式”创建 Spring Boot 项目,请将浏览器指向https://start.spring.io。从那里,我们将选择一些选项然后开始。

要开始使用 Initializr,我们首先选择要与项目一起使用的构建系统。如前所述,我们有两个很好的选择:Maven 和 Gradle。我们选择 Maven 作为示例。

接下来,我们将选择 Java 作为该项目的(语言)基础。

正如您可能已经注意到的,Spring Initializr 为所呈现的选项选择了足够的默认值,以便无需您的任何输入即可创建项目。当您访问此网页时,Maven 和 Java 已经预先选定。当前版本的 Spring Boot 也是如此,对于这个——以及大多数——项目来说,这是您希望选择的版本。

我们可以在项目元数据下留下选项而没有问题,尽管我们将来会修改它们以适应未来的项目。

现在,我们还不包括任何依赖项。这样,我们可以专注于项目创建的机制,而不是任何特定的结果。

在生成项目之前,还有几个 Spring Initializr 的非常好的功能,我想指出,并附带一条侧记。

如果您想在基于当前选择生成项目之前检查项目的元数据和依赖项详细信息,您可以单击“探索”按钮或使用键盘快捷键 Ctrl+Space 打开 Spring Initializr 的项目浏览器(如图 2-3 所示)。然后,Initializr 将向您展示将包含在即将下载的压缩(.zip)项目中的项目结构和构建文件。您可以查看目录/包结构,应用属性文件(稍后详述),以及在构建文件中指定的项目属性和依赖项:因为我们在这个项目中使用 Maven,所以我们的是pom.xml

sbur 0203

图 2-3. Spring Initializr 项目浏览器

这是在下载、解压和加载到您的 IDE 中全新空项目之前,验证项目配置和依赖项的快速便捷方法。

Spring Initializr 的另一个较小的功能,但却受到许多开发人员的欢迎,是暗色模式。通过点击页面顶部显示的Dark UI切换,如图 2-4 所示,您可以切换到 Initializr 的暗色模式,并使其成为每次访问页面时的默认模式。这是一个小功能,但如果您在其他所有地方都保持暗色模式,那么加载 Initializr 时肯定会减少不适感,使体验更加愉快。您会希望继续使用它!

sbur 0204

图 2-4. Spring Initializr,暗色模式下!
注意

除了主应用程序类及其主方法以及空测试之外,Spring Initializr 不会为您生成代码;它根据您的指导为您生成项目。这是一个小区别,但却是一个非常重要的区别:代码生成结果千差万别,通常会在您开始进行更改时束缚您。通过生成项目结构,包括具有指定依赖项的构建文件,Initializr 为您提供了一个运行的起点,以编写您需要利用 Spring Boot 自动配置的代码。自动配置为您提供超能力,而无需约束。

接下来,单击“生成”按钮生成、打包并下载您的项目,将其保存到本地机器上选择的位置。然后导航到下载的 .zip 文件,并解压以准备开发您的应用程序。

直接来自命令行

如果您乐意在命令行上尽可能多地花时间,或者希望最终脚本化项目创建过程,那么 Spring Boot 命令行界面 (CLI) 是为您量身定制的。Spring Boot CLI 具有许多强大的功能,但目前我们将专注于创建新的 Boot 项目。

sbur 0205

图 2-5. 在 SDKMAN 上的 Spring Boot CLI

安装完 Spring Boot CLI 后,您可以使用以下命令创建与刚刚创建的相同项目:

spring init

要将压缩的项目提取到名为 demo 的目录中,您可以执行以下命令:

unzip demo.zip -d demo

等等,怎么会这么简单?用一个词来说,那就是默认设置。Spring CLI 使用与 Spring Initializr 相同的默认设置(Maven、Java 等),允许您仅为希望更改的值提供参数。让我们特别为其中一些默认值提供值(并为项目提取添加一个有用的变化),以更好地了解所涉及的内容:

spring init -a demo -l java --build maven demo

我们仍在使用 Spring CLI 初始化项目,但现在我们提供了以下参数:

  • -a demo (或 --artifactId demo) 允许我们为项目提供一个 artifact ID;在本例中,我们称其为“demo”。

  • -l java (或 --language java) 允许我们指定 Java、Kotlin 或 Groovy¹ 作为此项目的主要语言。

  • --build 是用于构建系统参数的标志;有效的值是 mavengradle

  • -x demo 请求 CLI 提取 Initializr 返回的项目 .zip 文件;请注意,-x 是可选的,并且在没有扩展名的情况下指定文本标签(如我们在这里所做的)会被推断为提取目录。

注意

执行 spring help init 命令可以进一步查看所有这些选项。

当指定依赖关系时,事情会变得更加复杂。正如您可能想象的那样,从 Spring Initializr 提供的“菜单”中选择依赖项非常方便。但是,Spring CLI 的灵活性对于快速启动、脚本化和构建管道非常有用。

还有一件事:默认情况下,CLI 利用 Initializr 提供其项目构建能力,这意味着通过 CLI 或通过 Initializr 网页创建的项目是相同的。在直接使用 Spring Initializr 能力的场景中,这种一致性是非常重要的。

不过,有时组织会严格控制开发者能够使用的依赖项,甚至是创建项目的工具。坦率地说,这种做法让我感到沮丧,因为它会限制组织的灵活性和用户/市场响应能力。如果你在这样的组织中工作,那么在完成任何工作时可能会变得更加复杂。

在这种情况下,您可以创建自己的项目生成器(甚至克隆 Spring Initializr 的存储库)并直接使用生成的网页…或者只暴露 REST API 部分并从 Spring CLI 中使用。为此,只需将此参数添加到之前显示的命令中(当然,要用您的有效 URL 替换 ):

--target https://insert.your.url.here.org

在集成开发环境(IDE)中停留

无论如何创建 Spring Boot 项目,您都需要打开它并编写一些代码以创建有用的应用程序。

有三种主要的集成开发环境(IDE)和许多文本编辑器可以很好地支持开发者。IDE 包括但不限于Apache NetBeansEclipseIntelliJ IDEA。这三种都是开源软件(OSS),在许多情况下都是免费的。²

在本书中,以及在我的日常生活中,我主要使用 IntelliJ Ultimate Edition。在选择 IDE 时,并没有绝对的正确选择,更多取决于个人喜好(或组织的要求或偏好),因此请根据自己的情况选择最适合你和你喜好的工具。大多数主要工具之间的概念转移都非常顺畅。

还有几款编辑器在开发者中拥有大量的追随者。例如,像Sublime Text这样的付费应用程序由于其质量和长期性而拥有激烈的追随者。其他更近期进入这一领域的编辑器,如由 GitHub 创建的Atom(现在由 Microsoft 拥有)和由 Microsoft 创建的Visual Studio Code(简称 VSCode),正在迅速增强其功能和获得忠实的追随者。

在本书中,我偶尔会使用 VSCode 或其从相同代码库构建但已禁用遥测/追踪的对应版本,VSCodium。为了支持大多数开发者期望和/或需要的某些功能,我向 VSCode/VSCodium 添加了以下扩展:

Spring Boot Extension Pack(Pivotal)

这还包括几个其他扩展,如 Spring Initializr Java SupportSpring Boot ToolsSpring Boot Dashboard,它们分别在 VSCode 中便于创建、编辑和管理 Spring Boot 应用程序。

Debugger for Java(Microsoft)

Spring Boot 仪表板的依赖项。

IntelliJ IDEA 快捷键(加藤圭佑)

因为我主要使用 IntelliJ,这使得我更容易在这两者之间切换。

Java™语言支持(Red Hat)

Spring Boot 工具的依赖。

Java 的 Maven(Microsoft)

便于使用基于 Maven 的项目。

还有其他扩展可能对处理 XML、Docker 或其他辅助技术很有用,但对于我们当前的目的来说,这些是必需的。

继续进行我们的 Spring Boot 项目,接下来您将希望在您选择的 IDE 或文本编辑器中打开它。在本书的大多数示例中,我们将使用 IntelliJ IDEA,这是一款由 JetBrains 开发的非常强大的 IDE(使用 Java 和 Kotlin 编写)。如果您已将您的 IDE 与项目构建文件关联起来,您可以在项目目录中双击pom.xml文件(在 Mac 上使用 Finder,在 Windows 上使用 File Explorer,或在 Linux 上使用各种文件管理器),自动将项目加载到 IDE 中。如果没有,请按照其开发者推荐的方式在您的 IDE 或编辑器中打开项目。

注意

许多 IDE 和编辑器提供了一种创建命令行快捷方式的方法,可以通过简短的命令启动和加载项目。例如,IntelliJ 的idea,VSCode/VSCodium 的code,以及 Atom 的atom快捷方式。

Cruising Down main()

现在我们已经在我们的 IDE(或编辑器)中加载了项目,请看一下什么使得一个 Spring Boot 项目(图 2-6)与标准的 Java 应用有些不同。

sbur 0206

图 2-6. 我们的 Spring Boot 演示应用程序的主应用程序类

标准的 Java 应用程序(默认情况下)包含一个空的public static void main方法。当我们执行 Java 应用程序时,JVM 会搜索此方法作为应用程序的起点,如果没有此方法,应用程序启动将失败,并显示类似以下的错误:

Error:
Main method not found in class PlainJavaApp, please define the main method as:
	public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

当然,你可以将要在应用程序启动时执行的代码放置在 Java 类的主方法中,Spring Boot 应用程序正是这样做的。在启动时,Spring Boot 应用程序会检查环境、配置应用程序、创建初始上下文,并启动 Spring Boot 应用程序。它通过一个顶级注解和一行代码完成,如图 2-7 所示。

sbur 0207

图 2-7. Spring Boot 应用程序的本质

在本书逐步展开时,我们将深入探讨这些机制的内部工作。现在,可以说,通过设计和默认设置,Boot 在应用程序启动时会为您减少大量繁琐的应用程序设置工作,这样您就可以迅速专注于编写有意义的代码。

总结

本章已经探讨了创建 Spring Boot 应用程序时的一些一流选择。无论您喜欢使用 Maven 还是 Gradle 构建项目,在 Java 或 Kotlin 中编写代码,还是通过 Spring Initializr 提供的 Web 界面或其命令行伙伴 Spring Boot CLI 创建项目,您都可以毫不妥协地利用 Spring Boot 的全部功能和便利。您还可以使用各种支持 Spring Boot 的顶级 IDE 和文本编辑器来处理 Boot 项目。

正如这里和第一章中所述,Spring Initializr 努力为您快速轻松地创建项目。Spring Boot 在开发生命周期中通过以下功能有意义地做出贡献:

  • 简化的依赖管理,从项目创建到开发和维护都起到作用。

  • 自动配置大大减少/消除了在处理问题域之前可能编写的样板代码。

  • 简化的部署使打包和部署变得十分轻松。

不论您在这个过程中做出了哪些构建系统、语言或工具链的选择,所有这些能力都得到了充分支持。这是一个令人惊讶的灵活和强大的组合。

在下一章中,我们将创建我们的第一个真正有意义的 Spring Boot 应用程序:一个提供 REST API 的应用程序。

¹ Spring Boot 仍然支持 Groovy,但远不及 Java 或 Kotlin 广泛使用。

² 有两个选项可供选择:社区版(CE)和旗舰版(UE)。社区版支持 Java 和 Kotlin 应用程序开发,但要获得所有可用的 Spring 支持,您必须使用旗舰版。某些用例符合 UE 的免费许可证条件,或者您当然也可以购买一个。此外,这三个版本都为 Spring Boot 应用程序提供了出色的支持。

第三章:创建您的第一个 Spring Boot REST API

在本章中,我将解释并演示如何使用 Spring Boot 开发一个基本的工作应用程序。由于大多数应用程序涉及将后端云资源暴露给用户,通常通过前端 UI,因此应用程序编程接口(API)是理解和实践的绝佳起点。让我们开始吧。

API 的作用与原因

能够做所有事情的单体应用程序的时代已经结束了。

这并不意味着单体应用程序不再存在,或者它们不会继续存在很长时间。在各种情况下,一个将众多功能打包到一个包中的单体应用程序仍然是有意义的,尤其是在以下环境中:

  • 领域及其边界在很大程度上是未知的。

  • 提供的功能是紧密耦合的,模块交互的绝对性能优先于灵活性。

  • 所有相关能力的扩展要求是已知的和一致的。

  • 功能不是易变的;变化是缓慢的,范围有限,或者两者兼有。

对于其他一切,都有微服务。

当然,这是一个极度简化的说法,但我认为这是一个有用的总结。通过将功能分成更小、更有凝聚力的“块”,我们可以解耦它们,从而有可能实现更灵活、更强大的系统,这些系统可以更快地部署和更容易地维护。

在任何分布式系统中——毫无疑问,一个由微服务组成的系统就是这样的——通信至关重要。没有服务是孤立的。虽然有许多连接应用程序/微服务的机制,但我们通常通过模拟我们日常生活的基本结构——互联网——来开始我们的旅程。

互联网是为通信而建立的。事实上,其前身的设计者,高级研究计划局网络(ARPANET),预见到了即使在“重大中断”事件发生时也需要保持系统间通信的需求。可以合理地推断,一种类似于我们日常生活中使用的一种 HTTP 方法,同样能够让我们通过“过程”创建、检索、更新和删除各种资源。

尽管我很喜欢历史,但我不会深入研究 REST API 的历史,除了说罗伊·菲尔丁在他 2000 年的博士论文中阐述了它们的原则,该原则是建立在 1994 年的HTTP 对象模型之上的。

REST 是什么,为什么它很重要?

正如前面提到的,API 是我们开发人员用来编写以便我们的代码可以使用其他代码的规范/接口:库、其他应用程序或服务。但RESTREST API中代表什么?

REST 是表述性状态转移的首字母缩写,这是一种有点神秘的方式,它表明当一个应用程序与另一个应用程序通信时,应用程序 A 将其当前状态带入,而不是期望应用程序 B 在通信调用之间维护状态——当前和累积的、基于进程的信息。应用程序 A 在每个对应用程序 B 的请求中提供其相关状态的表示。您可以很容易地看出,这为应用程序的生存能力和弹性增加了,因为如果存在通信问题或者应用程序 B 崩溃并重新启动,它不会丢失与应用程序 A 的交互的当前状态;应用程序 A 可以简单地重新发出请求并继续两个应用程序之间停下的地方。

注意

这个通用概念通常被称为无状态应用/服务,因为每个服务在一系列交互中都保持自己的当前状态,而不期望其他服务代表其执行这种操作。

您的 API,HTTP 动词风格

现在,关于那个 REST API——有时称为 RESTful API,这是一种很好、令人放松的方式,不是吗?

在一些互联网工程任务组(IETF)的请求评论(RFCs)中定义了许多标准化的 HTTP 动词。其中,少数几个通常被一致地用于构建 API,还有几个偶尔会被使用。REST API 主要建立在以下 HTTP 动词之上:

  • POST

  • GET

  • PUT

  • PATCH

  • DELETE

这些动词对应我们在资源上执行的典型操作:创建(POST)、读取(GET)、更新(PUTPATCH)和删除(DELETE)。

注意

我承认通过将PUT大致等同于更新资源,稍微模糊了界限,并且通过将POST等同于创建资源的方式稍微减少了一些。我请求读者在我实施并提供澄清的过程中给予理解。

偶尔会使用以下两个动词:

  • OPTIONS

  • HEAD

这些可以用来获取请求/响应对可用的通信选项(OPTIONS)以及获取响应头部分的响应,不包括其主体(HEAD)。

对于本书以及大多数实际生产中的使用,我将专注于第一组,即大量使用的组。为了开始(没有双关意味),让我们创建一个实现非常基本的 REST API 的简单微服务。

回到 Initializr

我们像往常一样从 Spring Initializr 开始,如图 3-1 所示。我已经更改了 Group 和 Artifact 字段以反映我使用的详细信息(请随意使用您喜欢的命名方式),在选项(可选的,任何列出的版本都可以很好地完成)下选择了 Java 11,并且仅选择了 Spring Web 依赖项。正如显示的描述中所示,此依赖项带有多种功能,包括“[构建]使用 Spring MVC 构建 Web,包括 RESTful应用程序”(强调添加)。这正是我们当前任务所需的。

sbur 0301

图 3-1. 创建一个 Spring Boot 项目以构建 REST API

一旦我们在 Initializr 中生成了项目并将结果*.zip文件保存在本地,我们将提取压缩的项目文件——通常通过双击在文件浏览器中下载的sbur-rest-demo.zip文件或通过从 shell/终端窗口使用unzip*——然后在您选择的 IDE 或文本编辑器中打开现在已提取的项目,以查看类似于图 3-2 的视图。

sbur 0302

图 3-2. 我们的新 Spring Boot 项目,正等待我们开始

创建一个简单的领域

为了处理资源,我们需要编写一些代码来适应一些资源。让我们首先创建一个非常简单的领域类,表示我们想要管理的资源。

我有点咖啡爱好者,正如我的好朋友们——现在包括你——所知道的。考虑到这一点,我将使用一个咖啡领域,其中一个类代表一种特定类型的咖啡,作为本例的领域。

让我们开始创建Coffee类。这对例子至关重要,因为我们需要一个某种资源来演示如何通过 REST API 管理资源。但领域的简单或复杂对于本例来说不重要,所以我会保持简单,专注于目标:最终的 REST API。

如图 3-3 所示,Coffee类有两个成员变量:

  • 一个id字段用于唯一标识特定类型的咖啡

  • 一个name字段,描述咖啡的名称

sbur 0303

图 3-3. 咖啡类:我们的领域类

我将id字段声明为final,这样它只能被分配一次且不能修改;因此,在创建Coffee类的实例时必须分配它,这也意味着它没有修改器方法。

我创建了两个构造函数:一个接受两个参数,另一个在创建Coffee时如果没有提供唯一标识符则提供一个。

接下来,我创建了访问器和修改器方法——或者您更愿意称之为获取器和设置器方法——用于name字段,该字段未声明为final,因此可变。这是一个有争议的设计决策,但对于本例子的即将到来的需求非常合适。

有了这个,我们现在有了一个基本的领域。接下来是 REST 的时候了。

进行 GET 请求

或许最常用的最常用的动词是GET。所以让我们开始吧(双关语)。

@RestController 简介

不要陷得太深,Spring MVC(模型-视图-控制器)的创建是为了在数据、其传递和呈现之间分离关注点,假设视图将作为服务器呈现的网页提供。@Controller注解帮助将各个部分联系在一起。

@Controller@Component注释的一种别名,这意味着在应用启动时,Spring Bean——由 Spring 控制反转(IoC)容器在应用程序中创建和管理的对象——从该类中创建。带有@Controller注释的类可以容纳一个Model对象,以向表示层提供基于模型的数据,并使用ViewResolver来指示应用程序显示特定视图,由视图技术渲染。

注意

Spring 支持多种视图技术和模板引擎,这些将在后续章节中介绍。

还可以指示Controller类通过将@ResponseBody注释添加到类或方法(默认为 JSON)来返回格式化的响应。这将导致方法的对象/可迭代返回值成为 web 请求响应的整个主体,而不是作为Model的一部分返回。

@RestController注释是一个方便的标注,将@Controller@ResponseBody结合成一个描述性注释,简化您的代码并使意图更加明显。一旦我们将类标记为@RestController,我们就可以开始创建我们的 REST API。

让我们GET忙碌起来

REST API 处理对象,对象可以单独出现,也可以作为一组相关对象出现。为了利用我们的咖啡场景,您可能希望检索特定的咖啡;或者您可能希望检索所有咖啡,或者所有被视为深烘焙的咖啡,或者在描述中包含“哥伦比亚”等。为了满足检索一个实例或多个实例的需求,在我们的代码中创建多个方法是一个良好的做法。

我将首先创建一个Coffee对象列表,以支持方法返回多个Coffee对象,如以下基本类定义所示。我将定义变量,用于保存这组咖啡,作为Coffee对象列表。我选择List作为成员变量类型的高级接口,但实际上将在RestApiDemoController类中分配一个空的ArrayList以供使用:

@RestController
class RestApiDemoController {
	private List<Coffee> coffees = new ArrayList<>();
}
注意

接受以最高级别类型(类、接口)作为可以清洁地满足内部和外部 API 的实践是一种推荐做法。这些可能在所有情况下都不匹配,正如这里不匹配。在内部,List提供了使我能够基于我的标准创建最清晰实现的 API 级别;在外部,我们可以定义一个更高级别的抽象,我很快会演示。

始终有一些数据可以检索,以确认一切是否按预期工作。在以下代码中,我为RestApiDemoController类创建一个构造函数,并添加代码以在对象创建时填充咖啡列表:

@RestController
class RestApiDemoController {
	private List<Coffee> coffees = new ArrayList<>();

	public RestApiDemoController() {
		coffees.addAll(List.of(
				new Coffee("Café Cereza"),
				new Coffee("Café Ganador"),
				new Coffee("Café Lareño"),
				new Coffee("Café Três Pontas")
		));
	}
}

如下代码所示,我在RestApiDemoController类中创建了一个方法,该方法返回一个由我们的成员变量coffees表示的可迭代咖啡组。我选择使用Iterable<Coffee>,因为任何可迭代类型都能满足此 API 所需的功能:

使用 @RequestMapping 获取咖啡列表的GET

@RestController
class RestApiDemoController {
	private List<Coffee> coffees = new ArrayList<>();

	public RestApiDemoController() {
		coffees.addAll(List.of(
				new Coffee("Café Cereza"),
				new Coffee("Café Ganador"),
				new Coffee("Café Lareño"),
				new Coffee("Café Três Pontas")
		));
	}

	@RequestMapping(value = "/coffees", method = RequestMethod.GET)
	Iterable<Coffee> getCoffees() {
		return coffees;
	}
}

对于 @RequestMapping 注解,我添加了路径规范为 /coffees,方法类型为 RequestMethod.GET,表示该方法将响应路径为 /coffees 的请求,并且限制请求仅为 HTTP GET 请求。数据的检索由该方法处理,但不处理任何更新。Spring Boot 通过包含在 Spring Web 中的 Jackson 依赖自动执行对象到 JSON 或其他格式的编组和解组操作。

我们可以进一步简化使用另一个便利的注解。使用 @GetMapping 整合指令以仅允许 GET 请求,减少样板代码,只需指定路径,甚至省略 path =,因为不需要参数解决冲突。下面的代码清楚地展示了此注解替换带来的可读性好处:

@GetMapping("/coffees")
Iterable<Coffee> getCoffees() {
    return coffees;
}

进行POST

创建资源时,首选的方法是使用 HTTP POST 方法。

注意

POST 提供资源的详细信息,通常以 JSON 格式,请求目标服务在指定的 URI 下创建该资源。

如下代码片段所示,POST 是一个相对简单的操作:我们的服务接收指定的咖啡详情作为 Coffee 对象(得益于 Spring Boot 的自动编组),并将其添加到我们的咖啡列表中。然后返回请求的应用程序或服务的 Coffee 对象(默认情况下由 Spring Boot 自动解组为 JSON):

@PostMapping("/coffees")
Coffee postCoffee(@RequestBody Coffee coffee) {
    coffees.add(coffee);
    return coffee;
}

PUT 操作

一般来说,PUT 请求用于更新已知 URI 的现有资源。

注意

根据 IETF 的文档《超文本传输协议(HTTP/1.1):语义和内容》,PUT 请求应更新指定的资源(如果存在);如果资源不存在,则应创建它。

下面的代码符合规范:搜索具有指定标识符的咖啡,如果找到,则更新它。如果列表中没有这样的咖啡,则创建它:

@PutMapping("/coffees/{id}")
Coffee putCoffee(@PathVariable String id, @RequestBody Coffee coffee) {
    int coffeeIndex = -1;

    for (Coffee c: coffees) {
        if (c.getId().equals(id)) {
            coffeeIndex = coffees.indexOf(c);
            coffees.set(coffeeIndex, coffee);
        }
    }

    return (coffeeIndex == -1) ? postCoffee(coffee) : coffee;
}

DELETE 操作

要删除资源,我们使用 HTTP DELETE 请求。如下代码片段所示,我们创建了一个方法,接受咖啡标识符作为 @PathVariable 并使用 removeIf Collection 方法从我们的列表中删除适用的咖啡。removeIf 接受一个 Predicate,意味着我们可以提供一个 lambda 表达式来评估是否返回要删除的目标咖啡的布尔值。整洁而简便:

@DeleteMapping("/coffees/{id}")
void deleteCoffee(@PathVariable String id) {
    coffees.removeIf(c -> c.getId().equals(id));
}

等等

虽然这个场景有许多改进的地方,但我将重点放在两个特定的方面上:减少重复和根据规范返回必要的 HTTP 状态码。

为了减少代码中的重复,我将在 RestApiDemoController 类中将通用于该类内所有方法的 URI 映射部分提升到类级别的 @RequestMapping 注解中,即 "/coffees"。然后,我们可以从每个方法的映射 URI 规范中删除相同的 URI 部分,减少文本噪音,如下面的代码所示:

@RestController
@RequestMapping("/coffees")
class RestApiDemoController {
	private List<Coffee> coffees = new ArrayList<>();

	public RestApiDemoController() {
		coffees.addAll(List.of(
				new Coffee("Café Cereza"),
				new Coffee("Café Ganador"),
				new Coffee("Café Lareño"),
				new Coffee("Café Três Pontas")
		));
	}

	@GetMapping
	Iterable<Coffee> getCoffees() {
		return coffees;
	}

	@GetMapping("/{id}")
	Optional<Coffee> getCoffeeById(@PathVariable String id) {
		for (Coffee c: coffees) {
			if (c.getId().equals(id)) {
				return Optional.of(c);
			}
		}

		return Optional.empty();
	}

	@PostMapping
	Coffee postCoffee(@RequestBody Coffee coffee) {
		coffees.add(coffee);
		return coffee;
	}

	@PutMapping("/{id}")
	Coffee putCoffee(@PathVariable String id, @RequestBody Coffee coffee) {
		int coffeeIndex = -1;

		for (Coffee c: coffees) {
			if (c.getId().equals(id)) {
				coffeeIndex = coffees.indexOf(c);
				coffees.set(coffeeIndex, coffee);
			}
		}

		return (coffeeIndex == -1) ? postCoffee(coffee) : coffee;
	}

	@DeleteMapping("/{id}")
	void deleteCoffee(@PathVariable String id) {
		coffees.removeIf(c -> c.getId().equals(id));
	}
}

接下来,我查阅了早前提到的 IETF 文档,并注意到虽然 GET 方法未指定 HTTP 状态码,但建议 POSTDELETE 方法,但要求 PUT 方法响应状态码。为了实现这一点,我修改了 putCoffee 方法,如下面的代码段所示。现在,putCoffee 方法将不仅返回更新或创建的 Coffee 对象,还将返回一个包含该 Coffee 和适当 HTTP 状态码的 ResponseEntity:如果 PUT 的咖啡尚不存在,则返回 201(已创建),如果存在并已更新,则返回 2000(成功)。当然,我们还可以做更多,但当前应用程序代码满足要求,并表示简单且清晰的内部和外部 API:

@PutMapping("/{id}")
ResponseEntity<Coffee> putCoffee(@PathVariable String id,
        @RequestBody Coffee coffee) {
    int coffeeIndex = -1;

    for (Coffee c: coffees) {
        if (c.getId().equals(id)) {
            coffeeIndex = coffees.indexOf(c);
            coffees.set(coffeeIndex, coffee);
        }
    }

    return (coffeeIndex == -1) ?
            new ResponseEntity<>(postCoffee(coffee), HttpStatus.CREATED) :
            new ResponseEntity<>(coffee, HttpStatus.OK);
}

信任,但要验证

代码已经就绪,让我们来测试这个 API。

注意

我使用 HTTPie 命令行 HTTP 客户端处理几乎所有基于 HTTP 的任务。偶尔也会使用 curlPostman,但我发现 HTTPie 是一个功能强大且具有简洁命令行界面的多用途客户端。

如 图 3-4 所示,我查询 coffees 端点以获取当前列表中的所有咖啡。如果没有提供主机名,HTTPie 默认为 GET 请求并假定 localhost,减少了不必要的输入。正如预期的那样,我们看到了我们预填充列表中的所有四种咖啡。

sbur 0304

图 图 3-4. 获取所有咖啡

接下来,我复制了列表中一种咖啡的 id 字段,并将其粘贴到另一个 GET 请求中。图 3-5 显示了正确的响应。

sbur 0305

图 3-5. 获取一种咖啡

使用 HTTPie 执行 POST 请求非常简单:只需将包含 idname 字段的 JSON 表示形式的纯文本文件传输,HTTPie 将会执行 POST 操作。图 3-6 显示了命令及其成功的结果。

sbur 0306

图 3-6. 向列表中添加新咖啡的 POST 操作

正如前面提到的,PUT 命令应允许更新现有资源或在请求的资源不存在时添加新资源。在 图 3-7 中,我指定了我刚添加的咖啡的 id 并向命令传递了另一个具有不同名称的 JSON 对象。结果是,具有 id “99999”的咖啡现在的 name 是 “Caribou Coffee”,而不是之前的 “Kaldi’s Coffee”。返回码也符合预期,为 200(OK)。

sbur 0307

图 3-7. PUT 更新现有咖啡

在 图 3-8 中,我以相同方式发起了 PUT 请求,但引用了 URI 中不存在的 id。应用程序遵循 IETF 指定的行为添加了它,并正确返回了 HTTP 状态码 201(Created)。

sbur 0308

图 3-8. PUT 添加新咖啡

使用 HTTPie 创建 DELETE 请求与创建 PUT 请求非常相似:必须指定 HTTP 动词,并且资源的 URI 必须完整。 图 3-9 显示了结果:HTTP 状态码为 200(OK),表明资源已成功删除,并且没有显示值,因为资源已不存在。

sbur 0309

图 3-9. 删除咖啡

最后,我们重新查询我们的咖啡全列表以确认预期的最终状态。如 图 3-10 所示,我们现在有了一个之前列表中没有的额外咖啡:Mötor Oil Coffee。API 验证成功。

sbur 0310

图 3-10. 获取当前列表中的所有咖啡

摘要

本章演示了如何使用 Spring Boot 开发基本的工作应用程序。由于大多数应用程序涉及将后端云资源暴露给用户,通常通过前端用户界面,我展示了如何创建和发展一个有用的 REST API,可以以多种一致的方式消费,以提供创建、读取、更新和删除几乎每个关键系统中心资源所需的功能。

我检查并解释了 @RequestMapping 注解及其各种便捷注解特化,这些特化与定义的 HTTP 动词相一致:

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @PatchMapping

  • @DeleteMapping

创建了处理许多这些注释及其关联操作的方法后,我稍微重构了代码以简化它,并在需要时提供了 HTTP 响应代码。验证 API 确认其正确操作。

在下一章中,我将讨论并演示如何将数据库访问添加到我们的 Spring Boot 应用程序中,使其变得更加实用并且为生产准备好。

第四章:将数据库访问添加到您的 Spring Boot 应用程序

正如前一章所讨论的,出于许多非常好的原因,应用程序通常会暴露无状态的 API。然而,在幕后,很少有有用的应用程序是完全短暂的;某种状态通常是为了某事而存储的。例如,每次对在线商店购物车的请求可能都会包含其状态,但一旦下订单,订单的数据就会被保留。有许多方法可以做到这一点,以及共享或路由这些数据的方法,但几乎所有足够大的系统中都会涉及一种或多种数据库。

在本章中,我将演示如何将数据库访问添加到前一章中创建的 Spring Boot 应用程序中。本章旨在简要介绍 Spring Boot 的数据功能,并且后续章节将深入探讨。但在许多情况下,这里介绍的基础仍然适用并提供完全足够的解决方案。让我们深入了解吧。

代码检查

请从代码仓库的分支chapter4begin检出以开始。

为数据库访问设置自动配置

如前所示,Spring Boot 旨在尽可能简化所谓的 80–90% 使用案例:开发人员一遍又一遍地执行的代码和过程模式。一旦识别出模式,Boot 会自动初始化所需的 Bean,使用合理的默认配置。定制一个能力就像提供一个或多个属性值或创建一个定制版本的一个或多个 Bean 一样简单;一旦自动配置检测到变化,它就会退出并遵循开发人员的指导。数据库访问就是一个完美的例子。

我们希望获得什么?

在我们之前的示例应用程序中,我使用了一个ArrayList来存储和维护我们的咖啡列表。这种方法对于单个应用程序来说足够简单,但它确实有其缺点。

首先,它根本不具备弹性。如果您的应用程序或运行该应用程序的平台失败,所有在应用程序运行期间对列表所做的更改——不论持续了几秒钟还是几个月——都会消失。

其次,它不具备可伸缩性。启动应用程序的另一个实例会导致第二个(或后续)应用实例具有其自己独特的咖啡列表。数据不会在多个实例之间共享,因此一个实例对咖啡所做的更改——添加新的咖啡、删除、更新——对于访问不同应用实例的任何人都是不可见的。

显然这不是运行铁路的方式。

我将在接下来的章节中探讨几种不同的方法来完全解决这些非常现实的问题。但现在,让我们奠定一些基础,这些步骤将在未来的道路上非常有用。

添加数据库依赖

要从您的 Spring Boot 应用程序访问数据库,您需要一些东西:

  • 运行中的数据库,无论是由您的应用程序启动/嵌入,还是仅对您的应用程序可访问

  • 数据库驱动程序启用程序化访问,通常由数据库供应商提供。

  • 一个用于访问目标数据库的 Spring Data 模块

某些 Spring Data 模块将适当的数据库驱动程序作为 Spring Initializr 内的单个可选择依赖项包括在内。在其他情况下,例如当 Spring 使用 Java 持久化 API(JPA)访问符合 JPA 的数据存储时,需要选择 Spring Data JPA 依赖项目标数据库的特定驱动程序依赖项,例如 PostgreSQL。

为了从内存构造迈出第一步到持久性数据库,我将从向我们项目的构建文件中添加依赖项和因此的功能开始。

H2 是一个完全用 Java 编写的快速数据库,具有一些有趣且有用的特性。首先,它符合 JPA 标准,因此我们可以像连接任何其他 JPA 数据库(如 Microsoft SQL、MySQL、Oracle 或 PostgreSQL)一样连接我们的应用程序到它上面。它还具有内存和基于磁盘的模式。这使得在我们从内存中的ArrayList转换到内存数据库之后,我们可以选择一些有用的选项:要么将 H2 更改为基于磁盘的持久化,要么(因为我们现在使用的是 JPA 数据库)切换到另一个 JPA 数据库。在那一点上,任何选项都变得简单得多。

为了使我们的应用程序能够与 H2 数据库交互,我将在我们项目的pom.xml<dependencies>部分添加以下两个依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
注意

H2 数据库驱动程序依赖项的runtime范围表示它将出现在运行时和测试类路径中,但不会出现在编译类路径中。这是对于不需要编译的库采用的良好做法。

一旦保存了更新的pom.xml文件并且(如果必要的话)重新导入/刷新了 Maven 依赖项,您就可以访问所添加依赖项中包含的功能。接下来,是时候写一些代码来使用它了。

添加代码

由于我们已经有了一些管理咖啡的代码,我们需要在添加新的数据库功能时进行一些重构。我发现最好的开始地方是领域类(们),在这种情况下是Coffee

@Entity

如前所述,H2 是一个符合 JPA 标准的数据库,因此我将添加 JPA 注解来连接这些点。对于Coffee类本身,我将添加来自javax.persistence@Entity注解,表示Coffee是一个可持久化的实体,并且对现有的id成员变量,我将添加@Id注解(也来自javax.persistence)来标记它作为数据库表的 ID 字段。

注意

如果类名——在本例中是Coffee——不与期望的数据库表名匹配,@Entity注解接受一个name参数来指定匹配带注解实体的数据表名。

如果你的集成开发环境足够智能,它可能会提示你在Coffee类中仍然缺少某些内容。例如,IntelliJ 会用红色下划线标出类名,并在鼠标悬停时显示有用的弹出窗口,如图 4-1 所示。

sbur 0401

Figure 4-1. JPA Coffee类中缺少的构造函数

Java 持久化 API 要求在从数据库表行创建对象时使用无参数构造函数,因此接下来我将添加这个构造函数。这导致我们的 IDE 显示下一个警告,如图 4-2 所示:为了有一个无参数构造函数,我们必须使所有成员变量可变,即非 final。

sbur 0402

Figure 4-2. 有了无参数构造函数,id不能是 final

从声明id成员变量中删除final关键字解决了这个问题。为了使id可变,我们的Coffee类还需要为id添加一个 mutator 方法,以便 JPA 能够为该成员变量分配一个值,因此我也添加了setId()方法,如图 4-3 所示。

sbur 0403

Figure 4-3. 新的setId()方法

仓库(Repository)

现在将Coffee定义为有效的 JPA 实体,可以进行存储和检索,是时候与数据库建立连接了。

对于一个如此简单的概念,在 Java 生态系统中配置和建立数据库连接长期以来一直是一件相当繁琐的事情。正如在第一章中提到的,使用应用服务器托管 Java 应用程序需要开发人员执行多个繁琐的步骤才能做好准备工作。一旦开始与数据库交互,或者如果直接从 Java 实用程序或客户端应用程序访问数据存储,则需要执行涉及PersistenceUnitEntityManagerFactoryEntityManager API(以及可能的DataSource对象)、打开和关闭数据库等额外步骤。对于开发人员如此频繁地进行的操作来说,这些仪式感很多。

Spring Data 引入了仓库(repositories)的概念。Repository是 Spring Data 中定义的一个接口,作为对各种数据库的有用抽象。Spring Data 中还有其他访问数据库的机制,将在后续章节中详细解释,但各种类型的Repository可以说是最常用的。

Repository本身只是以下类型的一个占位符:

  • 存储在数据库中的对象

  • 对象的唯一 ID/主键字段

当然,关于仓库还有很多内容,这些内容我会在第六章中详细介绍。现在,让我们专注于当前示例中直接相关的两个:CrudRepositoryJpaRepository

还记得我之前提到的使用最高级别接口来编写代码的首选实践吗?虽然JpaRepository扩展了几个接口,并因此包含了更广泛的功能,但CrudRepository涵盖了所有关键的 CRUD 功能,对于我们(到目前为止)简单的应用程序已经足够了。

为了启用我们应用程序的仓库支持,首先需要通过扩展 Spring Data 的Repository接口来定义一个特定于我们应用程序的接口:.interfaceCoffeeRepo

interface CoffeeRepository extends CrudRepository<Coffee, String> {}
注意

定义的两种类型是存储对象类型及其唯一 ID 的类型。

这代表了在 Spring Boot 应用程序中创建仓库的最简表达方式。在某些情况下,定义仓库的查询是可能的,也是非常有用的;在未来的章节中我将深入讨论。但这里有一个“神奇”的部分:Spring Boot 的自动配置考虑了类路径上的数据库驱动(在本例中是 H2)、我们应用程序中定义的仓库接口,以及 JPA 实体Coffee类定义,并为我们创建了一个数据库代理 bean on our behalf。当模式如此明确和一致时,无需为每个应用程序编写几乎相同的样板代码,这使开发人员能够专注于新的、被请求的功能。

实用程序,即“Springing”进入行动

现在是时候让那个仓库投入运行了。我会像前几章一样,分步骤地介绍功能,先引入功能,然后再进行完善。

首先,我将仓库 bean 自动装配/注入到RestApiDemoController中,以便控制器可以在通过外部 API 接收请求时访问它,如图 4-4 所示。

首先,我声明了成员变量:

private final CoffeeRepository coffeeRepository;

接下来,我通过以下方式将其作为构造函数的参数添加:

public RestApiDemoController(CoffeeRepository coffeeRepository){}
注意

在 Spring Framework 4.3 之前,必须在所有情况下在方法上方添加@Autowired注解,以指示参数表示 Spring bean 应自动装配/注入。从 4.3 开始,具有单个构造函数的类不需要为自动装配的参数添加注解,这是一个有用的时间节省功能。

sbur 0404

图 4-4. 将仓库自动装配到RestApiDemoController

当仓库设置完成后,我删除了List<Coffee>成员变量,并在构造函数中将该列表的初始填充更改为将相同的咖啡保存到仓库中,如图 4-4 中所示。

根据图 4-5,立即删除coffees变量会立即标记所有对它的引用为不可解析的符号,因此下一个任务是用适当的仓库交互替换这些引用。

sbur 0405

图 4-5. 替换已移除的coffees成员变量

作为没有参数的简单检索所有咖啡的方法,getCoffees()方法是一个很好的起点。使用内置在CrudRepository中的findAll()方法,甚至不需要更改getCoffees()的返回类型,因为它还返回一个Iterable类型;只需调用coffeeRepository.findAll()并返回其结果即可完成任务,如下所示:

@GetMapping
Iterable<Coffee> getCoffees() {
    return coffeeRepository.findAll();
}

重构getCoffeeById()方法为我们的代码带来了一些洞见,感谢存储库为混合带来的功能。我们不再需要手动搜索匹配的id咖啡列表了;CrudRepositoryfindById()方法为我们处理了,如下代码片段所示。由于findById()返回一个Optional类型,因此我们的方法签名不需要任何更改:

@GetMapping("/{id}")
Optional<Coffee> getCoffeeById(@PathVariable String id) {
    return coffeeRepository.findById(id);
}

postCoffee()方法转换为使用存储库也是一个相当简单的尝试,如下所示:

@PostMapping
Coffee postCoffee(@RequestBody Coffee coffee) {
    return coffeeRepository.save(coffee);
}

使用putCoffee()方法,我们再次看到了CrudRepository所展示的大量节省时间和代码的功能。我使用内置的existsById()存储库方法来确定这是新的还是现有的Coffee,并返回适当的 HTTP 状态代码以及保存的Coffee,如此清单所示:

@PutMapping("/{id}")
ResponseEntity<Coffee> putCoffee(@PathVariable String id,
                                 @RequestBody Coffee coffee) {

    return (!coffeeRepository.existsById(id))
            ? new ResponseEntity<>(coffeeRepository.save(coffee),
                  HttpStatus.CREATED)
            : new ResponseEntity<>(coffeeRepository.save(coffee), HttpStatus.OK);
}

最后,我更新了deleteCoffee()方法以使用CrudRepository内置的deleteById()方法,如下所示:

@DeleteMapping("/{id}")
void deleteCoffee(@PathVariable String id) {
    coffeeRepository.deleteById(id);
}

利用使用CrudRepository的流畅 API 创建的存储库 bean 简化了RestApiDemoController的代码,并使其更加清晰,无论是从可读性还是可理解性方面,都能清晰表达,如完整代码清单所示:

@RestController
@RequestMapping("/coffees")
class RestApiDemoController {
    private final CoffeeRepository coffeeRepository;

    public RestApiDemoController(CoffeeRepository coffeeRepository) {
        this.coffeeRepository = coffeeRepository;

        this.coffeeRepository.saveAll(List.of(
                new Coffee("Café Cereza"),
                new Coffee("Café Ganador"),
                new Coffee("Café Lareño"),
                new Coffee("Café Três Pontas")
        ));
    }

    @GetMapping
    Iterable<Coffee> getCoffees() {
        return coffeeRepository.findAll();
    }

    @GetMapping("/{id}")
    Optional<Coffee> getCoffeeById(@PathVariable String id) {
        return coffeeRepository.findById(id);
    }

    @PostMapping
    Coffee postCoffee(@RequestBody Coffee coffee) {
        return coffeeRepository.save(coffee);
    }

    @PutMapping("/{id}")
    ResponseEntity<Coffee> putCoffee(@PathVariable String id,
                                     @RequestBody Coffee coffee) {

        return (!coffeeRepository.existsById(id))
                ? new ResponseEntity<>(coffeeRepository.save(coffee),
                HttpStatus.CREATED)
                : new ResponseEntity<>(coffeeRepository.save(coffee), HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    void deleteCoffee(@PathVariable String id) {
        coffeeRepository.deleteById(id);
    }
}

现在,唯一剩下的就是验证我们的应用程序按预期工作并且外部功能保持不变。

注意

测试功能的另一种方法——也是推荐的实践方法——是首先创建单元测试,类似于测试驱动开发(TDD)。我强烈推荐这种方法在真实的软件开发环境中,但我发现当目标是演示和解释离散的软件开发概念时,越少越好;尽可能少地显示以清晰传达关键概念会增加信号,减少噪音,即使噪音后来也很有用。因此,我在本书的后面的一个专门章节中介绍了测试。

保存和检索数据

再次进入领域,亲爱的朋友们,再次:使用 HTTPie 从命令行访问 API。查询咖啡端点会从我们的 H2 数据库中返回与之前相同的四种咖啡,如图 4-6 所示。

复制刚列出的其中一种咖啡的id字段,并将其粘贴到特定于咖啡的GET请求中,将产生如图 4-7 所示的输出。

sbur 0406

图 4-6. 获得所有咖啡

sbur 0407

图 4-7. 获得一杯咖啡

在图 4-8 中,我向应用程序和其数据库POST了一个新的咖啡。

sbur 0408

图 4-8。向列表中POST一个新的咖啡

如前一章所讨论的,PUT命令应允许更新现有资源或如果请求的资源尚不存在则添加一个新的资源。在图 4-9 中,我指定了刚刚添加的咖啡的id,并传递给命令一个修改该咖啡名称的 JSON 对象。更新后,id为“99999”的咖啡现在的name是“Caribou Coffee”,而不是“Kaldi's Coffee”,返回码是 200(OK),如预期的那样。

sbur 0409

图 4-9。对现有咖啡进行PUT更新

接下来我发起了类似的PUT请求,但在 URI 中指定了一个不存在的id。应用程序根据 IETF 指定的行为向数据库添加了一个新的咖啡,并正确返回了 HTTP 状态码 201(已创建),如图 4-10 所示。

sbur 0410

图 4-10。PUT一个新的咖啡

最后,我通过发出DELETE请求来测试删除指定的咖啡,该请求仅返回 HTTP 状态码 200(OK),表示资源已成功删除,因为资源不再存在,根据图 4-11。为了检查我们的最终状态,我们再次查询所有咖啡的完整列表(参见图 4-12)。

sbur 0411

图 4-11。DELETE一个咖啡

sbur 0412

图 4-12。现在在列表中GET所有的咖啡

与以往一样,我们现在有一个额外的咖啡,最初不在我们的存储库中:Mötor Oil Coffee。

一点打磨

像往常一样,有许多地方可以从额外的关注中受益,但我将专注于两个方面:将示例数据的初始填充提取到单独的组件中,并进行一些条件重排序以提高清晰度。

上一章我在RestApiDemoController类中填充了咖啡列表的一些初始值,因此在将其转换为具有存储库访问权限的数据库后,在本章中我保持了同样的结构。更好的做法是将该功能提取到一个可以快速轻松启用或禁用的单独组件中。

有许多方法可以在应用程序启动时自动执行代码,包括使用CommandLineRunnerApplicationRunner并指定 lambda 来实现所需的目标:在这种情况下,创建和保存示例数据。但我更喜欢使用@Component类和@PostConstruct方法来实现相同的功能,原因如下:

  • CommandLineRunnerApplicationRunner生成 bean 方法自动装配一个存储库 bean 时,测试单元会打破在测试中(通常情况下是这样)模拟存储库 bean。

  • 如果您在测试中模拟存储库 bean 或希望在不创建示例数据的情况下运行应用程序,只需注释掉其 @Component 注解即可禁用实际数据填充 bean,这样做快捷而简单。

我建议创建一个类似于下面代码块中展示的 DataLoader 类。将创建示例数据的逻辑提取到 DataLoader 类的 loadData() 方法中,并用 @PostContruct 注解进行标注,将 RestApiDemoController 恢复到其既定的单一目的,即提供外部 API,并使 DataLoader 负责其既定(和显而易见的)目的:

@Component
class DataLoader {
    private final CoffeeRepository coffeeRepository;

    public DataLoader(CoffeeRepository coffeeRepository) {
        this.coffeeRepository = coffeeRepository;
    }

    @PostConstruct
    private void loadData() {
        coffeeRepository.saveAll(List.of(
                new Coffee("Café Cereza"),
                new Coffee("Café Ganador"),
                new Coffee("Café Lareño"),
                new Coffee("Café Três Pontas")
        ));
    }
}

另一个润色的一点是在 putCoffee() 方法中三元运算符的布尔条件微调。在重构方法以使用存储库后,不再需要评估否定条件。从条件中去除否定(!)操作符略微提升了清晰度;当然,交换三元运算符的真和假值是必需的,以保持原始结果,如以下代码所示:

@PutMapping("/{id}")
ResponseEntity<Coffee> putCoffee(@PathVariable String id,
                                 @RequestBody Coffee coffee) {

    return (coffeeRepository.existsById(id))
            ? new ResponseEntity<>(coffeeRepository.save(coffee),
                HttpStatus.OK)
            : new ResponseEntity<>(coffeeRepository.save(coffee),
                HttpStatus.CREATED);
}

Code Checkout Checkup

要获取完整的章节代码,请从代码仓库的 chapter4end 分支检出。

摘要

本章展示了如何将数据库访问添加到上一章创建的 Spring Boot 应用程序中。虽然它旨在简明介绍 Spring Boot 的数据功能,但我提供了以下概述:

  • Java 数据库访问

  • Java 持久化 API(JPA)

  • H2 数据库

  • Spring Data JPA

  • Spring Data 存储库

  • 通过存储库创建示例数据的机制

后续章节将深入探讨 Spring Boot 数据库访问的更多细节,但本章涵盖的基础已经为构建提供了坚实的基础,在许多情况下,这些基础已经足够。

在下一章中,我将讨论和演示 Spring Boot 提供的有用工具,以便在应用程序不按预期方式运行或需要验证其运行情况时,获取对应用程序的洞察。

第五章:配置和检查您的 Spring Boot 应用程序

任何应用程序都可能出现许多问题,其中一些问题甚至可能有简单的解决方案。然而,除了偶尔的猜测,必须在真正解决问题之前确定问题的根本原因。

调试 Java 或 Kotlin 应用程序——或者任何其他应用程序,这是每个开发人员在职业生涯早期都应该学会并在其后不断完善和扩展的基本技能。我并不认为这在所有情况下都是适用的,所以如果你尚未熟悉所选语言和工具的调试能力,请尽快探索你手头的选择。这确实在你开发的每个项目中都非常重要,并可以节省大量时间。

也就是说,调试代码只是确定、识别和隔离应用程序中显现行为的一种级别。随着应用程序变得更加动态和分布式,开发人员通常需要执行以下操作:

  • 动态配置和重新配置应用程序

  • 确定/确认当前设置及其来源

  • 检查和监控应用程序环境和健康指标

  • 临时调整活动应用程序的日志级别以识别根本原因

本章演示如何利用 Spring Boot 的内置配置能力、其自动配置报告和 Spring Boot 执行器来灵活、动态地创建、识别和修改应用程序环境设置。

代码检出检查

请查看代码仓库中的 chapter5begin 分支以开始。

应用程序配置

没有应用程序是孤立的。

当我说这句话时,大多数时候是为了指出这个真理:几乎在每种情况下,一个应用程序在没有与其他应用程序/服务的交互时不能提供其全部效用。但还有另一层意思同样正确:没有应用程序能够在没有以某种形式访问其环境的情况下如此有用。一个静态、不可配置的应用程序是僵化的、不灵活的和受限的。

Spring Boot 应用程序为开发人员提供了多种强大的机制,可以在应用程序运行时动态配置和重新配置其应用程序。这些机制利用了 Spring Environment 来管理来自所有来源的配置属性,包括以下内容:

  • 当开发工具(devtools)处于活动状态时,Spring Boot 开发者工具(devtools)全局设置属性位于 $HOME/.config/spring-boot 目录中。

  • 测试中的 @TestPropertySource 注解。

  • 测试中的 properties 属性,可在 @SpringBootTest 和各种测试注解中用于测试应用程序片段。

  • 命令行参数。

  • 来自 SPRING_APPLICATION_JSON(嵌入在环境变量或系统属性中的内联 JSON)的属性。

  • ServletConfig 初始化参数。

  • ServletContext 初始化参数。

  • 来自 java:comp/env 的 JNDI 属性。

  • Java 系统属性(System.getProperties())。

  • 操作系统环境变量。

  • 仅包含 random.* 属性的 RandomValuePropertySource

  • 打包在 jar 外的特定配置文件的应用程序属性(application-{profile}.properties 和 YAML 变体)。

  • 打包在 jar 中的特定配置文件的应用程序属性(application-{profile}.properties 和 YAML 变体)。

  • 打包在 jar 外的应用程序属性(application.properties 和 YAML 变体)。

  • 打包在 jar 中的应用程序属性(application.properties 和 YAML 变体)。

  • @PropertySource 注解用于 @Configuration 类;请注意,这些属性源在应用程序上下文刷新之前不会添加到 Environment 中,这对于在刷新开始之前读取的某些属性(例如 logging.*spring.main.*)进行配置来说为时已晚。

  • 通过设置 SpringApplication.setDefaultProperties 指定的默认属性。注意:前述属性源按优先级降序列出:来自列表更高位置的属性将替换来自列表较低位置的相同属性。¹

所有这些都可能非常有用,但在本章的代码场景中,我特别选择了其中几个:

  • 命令行参数

  • 操作系统环境变量

  • 打包在 jar 内的应用程序属性(application.properties 和 YAML 变体)。

让我们从应用程序的 application.properties 文件中定义的属性开始,并逐步深入了解。

@Value

@Value 注解可能是将配置设置引入代码中最直接的方法。围绕模式匹配和 Spring 表达语言(SpEL)构建,它简单而强大。

我将从我们应用程序的 application.properties 文件中定义一个属性开始,如 图 5-1 所示。

sbur 0501

图 5-1. 在 application.properties 中定义 greeting-name

为了展示如何使用这个属性,我在应用程序中创建了一个额外的 @RestController 来处理与问候应用程序用户相关的任务,如 图 5-2 所示。

sbur 0502

图 5-2. 问候 @RestController

注意,@Value 注解适用于 name 成员变量,并接受类型为 String 的单个名为 value 的参数。我使用 SpEL 定义 value,将变量名(作为要评估的表达式)放置在 ${} 之间的定界符中。还有一件事需要注意:在这个示例中,SpEL 允许在冒号后设置默认值——“Mirage”,用于在应用程序 Environment 中未定义变量的情况。

在执行应用程序并查询 /greeting 端点时,应用程序如预期地响应“Dakota”,如 图 5-3 所示。

sbur 0503

图 5-3. 具有定义属性值的问候响应

为了验证默认值正在被评估,我在 application.properties 中以 # 注释掉以下行,并重新启动应用程序:

#greeting-name=Dakota

查询 greeting 端点现在返回 Figure 5-4 中所示的响应。由于应用程序的 Environment 中不再定义 greeting-name,因此期望的默认值“Mirage”生效了。

sbur 0504

Figure 5-4. 默认值问候响应

使用自定义属性和 @Value 提供了另一个有用的功能:一个属性的值可以使用另一个属性的值进行推导/构建。

为了演示属性嵌套的工作原理,我们至少需要两个属性。我在 application.properties 中创建了第二个属性 greeting-coffee,如 Figure 5-5 所示。

sbur 0505

Figure 5-5. 属性值传递给另一个属性

接下来,我向我们的 GreetingController 添加了一些代码,以表示一个带有咖啡的问候和我们可以访问以查看结果的端点。请注意,我还为 coffee 的值提供了一个默认值,如 Figure 5-6 所示。

sbur 0506

Figure 5-6. 向 GreetingController 添加咖啡问候

为了验证正确的结果,我重新启动应用程序并查询新的 /greeting/coffee 端点,结果显示在 Figure 5-7 中。请注意,由于问题中的两个属性都在 application.properties 中定义,因此显示的值与这些值的定义一致。

sbur 0507

Figure 5-7. 查询咖啡问候端点

就像生活和软件开发中的所有事物一样,@Value 确实有一些限制。由于我们为 greeting-coffee 属性提供了一个默认值,我们可以将其在 application.properties 中的定义注释掉,@Value 注解仍然会使用 GreetingController 中的 coffee 成员变量正确处理其(默认)值。但是,如果在属性文件中注释掉 greeting-namegreeting-coffee 两者,那么实际上没有任何 Environment 源定义它们,进而在应用程序尝试使用 GreetingController 中的 (现在未定义的) greeting-name 引用 greeting-coffee 时会导致以下错误:

org.springframework.beans.factory.BeanCreationException:
    Error creating bean with name 'greetingController':
        Injection of autowired dependencies failed; nested exception is
        java.lang.IllegalArgumentException:
            Could not resolve placeholder 'greeting-name' in value
            "greeting-coffee: ${greeting-name} is drinking Cafe Ganador"
注意

完整的堆栈跟踪已删除以提高简洁性和清晰性。

另一个 application.properties 中定义的属性并且仅通过 @Value 使用的限制是:它们不被 IDE 认为是应用程序使用的,因为它们只在引号限定的 String 变量中的代码中被引用;因此,与代码没有直接的关联。当然,开发人员可以通过目视检查属性名称和用法的正确拼写,但这完全是手动操作,因此更容易出错。

正如你所想象的那样,一种类型安全且可验证的属性使用和定义机制将是更好的全面选择。

@ConfigurationProperties

欣赏@Value的灵活性,但也意识到其局限性,Spring 团队创建了@ConfigurationProperties。使用@ConfigurationProperties,开发者可以定义属性,将相关属性分组,并以可验证和类型安全的方式引用/使用它们。

例如,如果在应用的application.properties文件中定义了一个未在代码中使用的属性,开发者将看到其名称被突出显示,以标识其为确认的未使用属性。同样地,如果属性定义为String,但与不同类型的成员变量相关联,IDE 将指出类型不匹配。这些都是捕捉简单但频繁错误的宝贵帮助。

为了演示如何使用@ConfigurationProperties,我将从定义一个 POJO 开始,用于封装所需的相关属性:在这种情况下,我们先前引用的greeting-namegreeting-coffee属性。如下所示的代码中,我创建了一个Greeting类来保存这两个属性:

class Greeting {
    private String name;
    private String coffee;

    public String getName() {
        return name;
    }

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

    public String getCoffee() {
        return coffee;
    }

    public void setCoffee(String coffee) {
        this.coffee = coffee;
    }
}

为了注册Greeting以管理配置属性,我添加了如下所示的@ConfigurationProperties注解,并指定了用于所有Greeting属性的前缀。此注解仅为使用配置属性准备该类;还必须告知应用程序处理这种方式注释的类,以便包含在应用程序Environment中的属性。请注意产生的有用错误消息:

sbur 0508

图 5-8. 注解和错误

在大多数情况下,指示应用处理@ConfigurationProperties类并将其属性添加到应用的Environment中最好的方法是将@ConfigurationPropertiesScan注解添加到主应用类中,如下所示:

@SpringBootApplication
@ConfigurationPropertiesScan
public class SburRestDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SburRestDemoApplication.class, args);
    }

}
注意

除了要求 Boot 扫描@ConfigurationProperties类之外的例外情况是,如果需要有条件地启用某些@ConfigurationProperties类或者正在创建自己的自动配置。然而,在所有其他情况下,应使用@ConfigurationPropertiesScan来扫描和启用类似于 Boot 组件扫描机制中的@ConfigurationProperties类。

为了使用注解处理器生成元数据,使 IDE 能够连接@ConfigurationProperties类和application.properties文件中定义的相关属性之间的关联,我将以下依赖项添加到项目的pom.xml构建文件中:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
注意

这个依赖项也可以在项目创建时从 Spring Initializr 自动选择并添加。

一旦将配置处理器依赖项添加到构建文件中,就需要刷新/重新导入依赖项并重新构建项目以充分利用它们。要重新导入依赖项,我在 IntelliJ 中打开 Maven 菜单,并点击左上角的重新导入按钮,如 图 5-9 所示。

sbur 0509

图 5-9. 重新导入项目依赖
注意

除非选项被禁用,IntelliJ 也会在更改的 pom.xml 上方显示一个小按钮,允许快速重新导入,而无需打开 Maven 菜单。重导入按钮位于其底部左侧带有圆形箭头的小 m,悬停在第一个依赖项的 <groupid> 条目上时可见;当重新导入完成时,它会消失。

一旦更新了依赖项,我从 IDE 中重新构建项目以整合配置处理器。

现在,要为这些属性定义一些值。返回到 application.properties,当我开始输入 greeting 时,IDE 会显示匹配的属性名,如 图 5-10 所示。

sbur 0510

图 5-10. @ConfigurationProperties 的完整 IDE 属性支持

要使用这些属性替代之前使用的属性,需要进行一些重构。

我可以完全放弃 GreetingController 自己的成员变量 namecoffee 以及它们的 @Value 注解;相反,我创建了一个成员变量用于管理 Greeting bean 现在管理的 greeting.namegreeting.coffee 属性,并通过构造函数注入到 GreetingController 中,如下面的代码所示:

@RestController
@RequestMapping("/greeting")
class GreetingController {
    private final Greeting greeting;

    public GreetingController(Greeting greeting) {
        this.greeting = greeting;
    }

    @GetMapping
    String getGreeting() {
        return greeting.getName();
    }

    @GetMapping("/coffee")
    String getNameAndCoffee() {
        return greeting.getCoffee();
    }
}

运行应用程序并查询 greetinggreeting/coffee 端点会得到 图 5-11 中捕获的结果。

sbur 0511

图 5-11. 检索 Greeting 属性

@ConfigurationProperties bean 管理的属性仍然从 Environment 及其所有潜在来源中获取其值;与基于 @Value 的属性相比,唯一显著缺失的是在注解成员变量中指定默认值的能力。乍看起来可能不太理想,但这并不是问题,因为应用程序的 application.properties 文件通常用于定义应用程序的合理默认值。如果需要不同的属性值以适应不同的部署环境,这些环境特定的值通过其他来源,例如环境变量或命令行参数,被摄入到应用程序的 Environment 中。简而言之,@ConfigurationProperties 简单地强制执行了更好的默认属性值的实践。

潜在的第三方选项

对于@ConfigurationProperties已经令人印象深刻的实用性的进一步扩展,是能够包装第三方组件并将它们的属性合并到应用的Environment中。为了演示这一点,我创建了一个 POJO 来模拟一个可能被整合到应用中的组件。请注意,在典型的使用案例中,当这个特性最有用时,我们会向项目添加一个外部依赖,并参考组件的文档来确定创建 Spring bean 的类,而不像我在这里手动创建。

在接下来的代码清单中,我创建了一个模拟的第三方组件称为Droid,具有两个属性——iddescription——及其关联的访问器和修改器方法:

class Droid {
    private String id, description;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

下一步的操作与真实的第三方组件完全相同:将组件实例化为 Spring bean。可以通过几种方式从定义的 POJO 创建 Spring bean,但对于这个特定的用例来说,最合适的方法是在带有@Configuration注解的类中创建一个@Bean注解的方法,无论是直接还是通过元注解。

一个将@Configuration包含在其定义中的元注解是@SpringBootApplication,它位于主应用程序类上。这就是为什么开发人员经常将 bean 创建方法放在这里的原因。

注意

在 IntelliJ 和大多数其他具有良好 Spring 支持的 IDE 和高级文本编辑器中,可以深入探索 Spring 元注解来探索嵌套的注解。在 IntelliJ 中,Cmd+LeftMouseClick(在 MacOS 上)将展开注解。@SpringBootApplication包含@SpringBootConfiguration,它包含@Configuration,使得与凯文·贝肯(Kevin Bacon)仅有两度分离。

在接下来的代码清单中,我演示了 bean 创建方法以及必需的@ConfigurationProperties注解和prefix参数,指示应将Droid属性合并到Environment中的顶层属性组droid中:

@SpringBootApplication
@ConfigurationPropertiesScan
public class SburRestDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SburRestDemoApplication.class, args);
    }

    @Bean
    @ConfigurationProperties(prefix = "droid")
    Droid createDroid() {
        return new Droid();
    }
}

正如以往一样,需要重新构建项目以便配置处理器检测到此新配置属性源暴露的属性。执行构建后,我们可以返回到application.properties,看到droid属性现在完整地展现出来,包括类型信息,如图 5-12 所示。

sbur 0512

图 5-12. droid属性和类型信息现在在application.properties中可见

我为droid.iddroid.description分配了一些默认值,用作默认值,如图 5-13 所示。对于所有的Environment属性来说,这是一个养成的好习惯,即使是从第三方获取的属性也不例外。

sbur 0513

图 5-13。在 application.properties 中分配了默认值的 droid 属性

为了验证一切是否按预期运行与 Droid 属性,我创建了一个非常简单的 @RestController,其中包含一个单独的 @GetMapping 方法,如下所示的代码:

@RestController
@RequestMapping("/droid")
class DroidController {
    private final Droid droid;

    public DroidController(Droid droid) {
        this.droid = droid;
    }

    @GetMapping
    Droid getDroid() {
        return droid;
    }
}

构建并运行项目后,我查询新的 /droid 端点,并确认适当的响应,如 图 5-14 中所示。

sbur 0514

图 5-14。查询 /droid 端点以检索来自 Droid 的属性

自动配置报告

如前所述,Boot 通过自动配置为开发人员执行了大量操作:使用所选功能、依赖项和代码来设置应用程序所需的 bean,从而实现所选功能和代码。还提到了根据用例更具体(符合您的用例)地实现功能所需的任何自动配置的能力。但是,如何查看创建了哪些 bean、未创建哪些 bean,以及是什么条件促使了其中任一结果呢?

使用 JVM 的灵活性,通过在几种方式之一中使用 debug 标志,可以很容易地生成自动配置报告:

  • 使用 --debug 选项执行应用程序的 jar 文件:java -jar bootapplication.jar --debug

  • 使用 JVM 参数执行应用程序的 jar 文件:java -Ddebug=true -jar bootapplication.jar

  • debug=true 添加到应用程序的 application.properties 文件

  • 在您的 shell(Linux 或 Mac)中执行 export DEBUG=true 或将其添加到 Windows 环境中,然后运行 java -jar bootapplication.jar

任何方式将肯定值添加到应用程序的 Environment 中,如前所述,都将提供相同的结果。这些只是更常用的选项。

自动配置报告的部分列出了正匹配的条件——这些条件评估为真,并导致执行操作的条件——列在以“Positive matches”为标题的部分中。我在这里复制了该部分标题,以及一个正匹配及其相应的自动配置操作的示例:

============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource',
      'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'
      (OnClassCondition)

这种匹配符合我们的预期,尽管确认以下内容总是好的:

  • JPA 和 H2 是应用程序依赖项。

  • JPA 与 SQL 数据源一起使用。

  • H2 是一个嵌入式数据库。

  • 找到支持嵌入式 SQL 数据源的类。

结果,调用了 DataSourceAutoConfiguration

同样,"Negative matches" 部分显示了 Spring Boot 自动配置未执行的操作以及原因,如下所示:

Negative matches:
-----------------
   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class
          'javax.jms.ConnectionFactory' (OnClassCondition)

在这种情况下,未执行 ActiveMQAutoConfiguration,因为应用程序在启动时未找到 JMS ConnectionFactory 类。

另一个有用的信息片段是列出“无条件类”的部分,这些类无需满足任何条件即可创建。鉴于前一节,我列出了一个特别感兴趣的类:

Unconditional classes:
----------------------
    org.springframework.boot.autoconfigure.context
     .ConfigurationPropertiesAutoConfiguration

正如您所见,ConfigurationPropertiesAutoConfiguration始终被实例化,以管理 Spring Boot 应用程序中创建和引用的任何ConfigurationProperties;它是每个 Spring Boot 应用程序的一部分。

执行器

执行器

n. 特指:用于移动或控制某物的机械装置

Spring Boot Actuator 的原始版本于 2014 年达到了一般可用性(GA),因为它为生产中的 Boot 应用程序提供了宝贵的洞察。通过 HTTP 端点或 Java 管理扩展(JMX)提供正在运行的应用程序的监控和管理功能,Actuator 涵盖并公开了 Spring Boot 的所有生产准备功能。

随着 Spring Boot 2.0 版本的彻底改造,Actuator 现在利用 Micrometer 仪表化库提供度量标准,通过与多种主流监控系统的一致外观类似于 SLF4J 处理各种日志机制,大大扩展了可以在任何给定的 Spring Boot 应用程序中通过执行器集成、监控和公开的范围。

要开始使用执行器,我将在当前项目的pom.xml依赖项部分中添加另一个依赖项。如下片段所示,spring-boot-starter-actuator依赖项提供了必要的功能;为此,它将 Actuator 本身和 Micrometer 一起带到 Spring Boot 应用程序中,并具备几乎零配置的自动配置能力:

<dependencies>
    ... (other dependencies omitted for brevity)
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

刷新/重新导入依赖后,我再次运行应用程序。应用程序运行时,通过访问其主要端点,我们可以查看执行器默认公开的信息。再次使用 HTTPie 完成此操作,如图 5-15 所示。

sbur 0515

图 5-15. 访问执行器端点,默认配置

执行器的所有信息默认情况下都集中在应用程序的*/actuator*端点下,但这也是可配置的。

这似乎并不像执行器所创造的那么热闹(和粉丝基地)。但这种简洁是有意的。

执行器可以访问并公开有关正在运行的应用程序的大量信息。此信息对开发人员、运营人员以及可能希望威胁您应用程序安全性的不良个人尤为有用。遵循 Spring Security 的默认安全目标,执行器的自动配置仅公开非常有限的healthinfo响应——事实上,info默认为空集,提供应用程序心跳和其他少量信息(OOTB)。

与大多数 Spring 的事物一样,您可以创建一些非常复杂的机制来控制对各种 Actuator 数据源的访问,但也有一些快速、一致和低摩擦的选项可用。 现在让我们来看看这些。

可以通过属性轻松配置 Actuator,要么使用一组包含的端点,要么使用一组排除的端点。 为了简单起见,我选择了包含路线下内容添加到 application.properties 中:

management.endpoints.web.exposure.include=env, info, health

在此示例中,我指示应用程序(和 Actuator)仅暴露*/actuator/env*、/actuator/info和*/actuator/health*端点(和任何下级端点)。

图 5-16 在重新运行应用程序并查询其*/actuator*端点后确认了预期结果。

为了充分展示 Actuator 的 OOTB 能力,我可以进一步禁用安全性,仅用于演示目的,通过使用前述的application.properties 设置的通配符:

management.endpoints.web.exposure.include=*
警告

这一点不容忽视:对于敏感数据的安全机制应该仅在演示或验证目的下禁用。永远不要为生产应用程序禁用安全性

sbur 0516

图 5-16. 在指定要包括的端点后访问 Actuator

启动应用程序时进行验证,Actuator 忠实地报告了它当前正在暴露的端点数量和到达它们的根路径——在这种情况下,默认为*/actuator*——如下所示的启动报告片段。 这是一个有用的提醒/警告,可以快速进行视觉检查,以确保在将应用程序推进到目标部署之前不会暴露更多端点:

INFO 22115 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      :
    Exposing 13 endpoint(s) beneath base path '/actuator'

要检查通过 Actuator 当前可访问的所有映射,只需查询提供的 Actuator 根路径以检索完整列表:

mheckler-a01 :: ~/dev » http :8080/actuator
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.spring-boot.actuator.v3+json
Date: Fri, 27 Nov 2020 17:43:27 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
    "_links": {
        "beans": {
            "href": "http://localhost:8080/actuator/beans",
            "templated": false
        },
        "caches": {
            "href": "http://localhost:8080/actuator/caches",
            "templated": false
        },
        "caches-cache": {
            "href": "http://localhost:8080/actuator/caches/{cache}",
            "templated": true
        },
        "conditions": {
            "href": "http://localhost:8080/actuator/conditions",
            "templated": false
        },
        "configprops": {
            "href": "http://localhost:8080/actuator/configprops",
            "templated": false
        },
        "env": {
            "href": "http://localhost:8080/actuator/env",
            "templated": false
        },
        "env-toMatch": {
            "href": "http://localhost:8080/actuator/env/{toMatch}",
            "templated": true
        },
        "health": {
            "href": "http://localhost:8080/actuator/health",
            "templated": false
        },
        "health-path": {
            "href": "http://localhost:8080/actuator/health/{*path}",
            "templated": true
        },
        "heapdump": {
            "href": "http://localhost:8080/actuator/heapdump",
            "templated": false
        },
        "info": {
            "href": "http://localhost:8080/actuator/info",
            "templated": false
        },
        "loggers": {
            "href": "http://localhost:8080/actuator/loggers",
            "templated": false
        },
        "loggers-name": {
            "href": "http://localhost:8080/actuator/loggers/{name}",
            "templated": true
        },
        "mappings": {
            "href": "http://localhost:8080/actuator/mappings",
            "templated": false
        },
        "metrics": {
            "href": "http://localhost:8080/actuator/metrics",
            "templated": false
        },
        "metrics-requiredMetricName": {
            "href": "http://localhost:8080/actuator/metrics/{requiredMetricName}",
            "templated": true
        },
        "scheduledtasks": {
            "href": "http://localhost:8080/actuator/scheduledtasks",
            "templated": false
        },
        "self": {
            "href": "http://localhost:8080/actuator",
            "templated": false
        },
        "threaddump": {
            "href": "http://localhost:8080/actuator/threaddump",
            "templated": false
        }
    }
}

Actuator 端点列表提供了捕获和暴露供检查的信息范围的良好概念,但对于好的和坏的行为者特别有用的是以下内容:

/actuator/beans

应用程序创建的所有 Spring bean

*/ /actuator/conditions

创建 Spring bean 所需满足的条件(或不满足的条件); 类似于之前讨论过的条件评估报告

/actuator/configprops

应用程序可访问的所有Environment 属性

/actuator/env

应用程序正在运行的环境的种种方面; 特别有用的是看到每个个体configprop 值的来源

/actuator/health

健康信息(基本或扩展,取决于设置)

/actuator/heapdump

启动堆转储以进行故障排除和/或分析

/actuator/loggers

每个组件的日志级别

/actuator/mappings

所有端点映射和支持详细信息

/actuator/metrics

应用程序当前正在捕获的指标

/actuator/threaddump

启动线程转储以进行故障排除和/或分析

这些,以及其余的预配置执行器端点,在需要时都非常方便,并且易于访问进行检查。继续专注于应用程序的环境,即使在这些端点之中也有同行中的先例。

让执行器打开

正如提到的,执行器的默认安全姿态有意仅公开非常有限的healthinfo响应。实际上,/actuator/health端点提供了一个相当实用的应用程序状态“UP”或“DOWN”。

大多数应用程序都有依赖关系,执行器会跟踪健康信息;除非获得授权,否则它不会公开其他信息。为了显示预配置依赖项的扩展健康信息,我将以下属性添加到application.properties中:

management.endpoint.health.show-details=always
注意

健康指标的show-details属性有三个可能的取值:never(默认值)、when_authorizedalways。在这个例子中,我选择always仅仅是为了演示可能性,但对于每个投入生产的应用程序,正确的选择应该是要么never,要么when_authorized,以限制应用程序扩展的健康信息的可见性。

重新启动应用程序会导致将应用程序的主要组件的健康信息添加到访问*/actuator/health*端点时的整体应用程序健康摘要中,参见图 5-17。

sbur 0517

图 5-17. 扩展健康信息

使用执行器更环保意识

开发者经常会受到一种毛病的困扰——包括在内的现在这个公司——即当行为与预期不符时,完全了解当前应用程序环境/状态的假设。这并不完全出乎意料,尤其是如果是自己写的异常代码。一个相对快速且非常宝贵的第一步是检查所有的假设。你知道那个值是多少吗?还是你只是确信你知道?

你有检查过吗?

特别是在输入驱动结果的代码中,这应该是一个必需的起点。执行器帮助使这一过程变得轻松。查询应用程序的*/actuator/env*端点返回所有环境信息;以下是该结果的部分,仅显示到目前为止在应用程序中设置的属性:

{
    "name": "Config resource 'classpath:/application.properties' via location
     'optional:classpath:/'",
    "properties": {
        "droid.description": {
            "origin": "class path resource [application.properties] - 5:19",
            "value": "Small, rolling android. Probably doesn't drink coffee."
        },
        "droid.id": {
            "origin": "class path resource [application.properties] - 4:10",
            "value": "BB-8"
        },
        "greeting.coffee": {
            "origin": "class path resource [application.properties] - 2:17",
            "value": "Dakota is drinking Cafe Cereza"
        },
        "greeting.name": {
            "origin": "class path resource [application.properties] - 1:15",
            "value": "Dakota"
        },
        "management.endpoint.health.show-details": {
            "origin": "class path resource [application.properties] - 8:41",
            "value": "always"
        },
        "management.endpoints.web.exposure.include": {
            "origin": "class path resource [application.properties] - 7:43",
            "value": "*"
        }
    }
}

执行器不仅显示每个定义属性的当前值,还显示其来源,甚至显示定义每个值的行号和列号。但是,如果其中一个或多个值被另一个来源覆盖,例如在执行应用程序时由外部环境变量或命令行参数?

为了演示一个典型的生产绑定应用程序场景,我从应用程序的目录中使用命令行运行mvn clean package,然后使用以下命令执行应用程序:

java -jar target/sbur-rest-demo-0.0.1-SNAPSHOT.jar --greeting.name=Sertanejo

再次查询 /actuator/env,您可以看到有一个新的命令行参数部分,其中只有一个条目greeting.name

{
    "name": "commandLineArgs",
    "properties": {
        "greeting.name": {
            "value": "Sertanejo"
        }
    }
}

遵循之前提到的Environment输入的优先顺序,命令行参数应该覆盖application.properties内部设置的值。查询 /greeting 端点返回预期的“Sertanejo”;同样,通过 SpEL 表达式查询 /greeting/coffee 也将覆盖的值合并到响应中:Sertanejo is drinking Cafe Cereza

通过 Spring Boot Actuator,试图弄清楚错误的、数据驱动行为变得更加简单。

使用 Actuator 增加日志记录的音量

与开发和部署软件中的许多其他选择一样,为生产应用程序选择日志记录级别涉及权衡。选择更多日志记录会导致更多的系统级工作和存储消耗,以及捕获更多相关和不相关的数据。这反过来可能会使寻找难以捉摸的问题变得更加困难。

作为提供 Boot 生产就绪功能的使命的一部分,Actuator 也解决了这个问题,允许开发人员为大多数或所有组件设置典型的日志级别,当关键问题出现时可以临时更改这个级别……所有这些都是在现场、生产中的 Spring Boot 应用中进行的。Actuator 通过向适用的端点发送简单的POST请求来便捷地设置和重置日志级别。例如,图 5-18 显示了org.springframework.data.web的默认日志级别。

sbur 0518

图 5-18. org.springframework.data.web的默认日志级别

特别值得注意的是,由于未为此组件配置日志级别,因此使用了有效级别“INFO”。再次强调,当未提供具体配置时,Spring Boot 会提供合理的默认设置。

如果我收到有关正在运行的应用程序的问题并希望增加日志记录以帮助诊断和解决问题,那么为了特定组件执行此操作所需的全部步骤就是向其 /actuator/loggers 端点发布一个新的 JSON 格式的configuredLevel值,如下所示:

echo '{"configuredLevel": "TRACE"}'
  | http :8080/actuator/loggers/org.springframework.data.web

现在重新查询日志级别,确认 org.springframework.data.web 的记录器现在设置为“TRACE”,将为应用程序提供详尽的诊断日志记录,如图 5-19 所示。

警告

“TRACE”在确定难以捉摸的问题时可能至关重要,但它是一个相当重量级的日志级别,甚至比“DEBUG”还要详细——在生产应用程序中使用可以提供重要的信息,但要注意其影响。

sbur 0519

图 5-19. org.springframework.data.web的新“TRACE”日志级别

代码检查

获取完整的章节代码,请从代码库的chapter5end分支查看。

总结

开发人员必须拥有有用的工具来建立、识别和隔离生产应用程序中表现出的行为是至关重要的。随着应用程序变得越来越动态和分布式,通常需要做以下工作:

  • 配置和动态重新配置应用程序

  • 确定/确认当前设置及其来源

  • 检查和监控应用程序环境和健康指标

  • 临时调整实时应用程序的日志级别以识别根本原因

本章展示了如何利用 Spring Boot 的内置配置能力、其自动配置报告和 Spring Boot 执行器来灵活动态地创建、识别和修改应用程序环境设置。

在下一章中,我将深入探讨数据:如何使用各种行业标准和领先的数据库引擎定义其存储和检索,以及 Spring Data 项目和工具如何以最简单和强大的方式支持它们的使用。

¹ Spring Boot 属性源的优先级顺序