运用Spring Boot

你正在阅读本书,说明你是一位读书人。也许你是一个书虫,博览群书;也许你只读自己需要的东西,拿起本书只是为了知道怎么用 Spring 开发应用程序。

无论何种情况,你都是一位读书人,是读书人便有心维护一个阅读列表,里面是自己想读或者需要读的书。就算没有白纸黑字的列表,至少在你心里会有这么一个列表。

在本书中,我们会构建一个简单的阅读列表应用程序。在这个程序里,用户可以输入想读的图书信息,查看列表,删除已经读过的书。我们将使用 Spring Boot 来辅助快速开发,各种繁文缛节越少越好。

开始前,我们需要先初始化一个项目。在第 1 章里,我们看到了好几种从 Spring Initializr 开始 Spring Boot 开发的方法。因为选择哪种方法都行,所以要选个最合适的,着手用 Spring Boot 开发就好了。

从技术角度来看,我们要用 Spring MVC 来处理 Web 请求,用 Thymeleaf 来定义 Web 视图,用 Spring Data JPA 来把阅读列表持久化到数据库里,姑且先用嵌入式的 H2 数据库。虽然也可以用 Groovy,但是我们还是先用 Java 来开发这个应用程序吧。此外,我们使用 Gradle 作为构建工具。

无论是用 Web 界面、Spring Tool Suite 还是 IntelliJ IDEA,只要用了 Initializr,你就要确保勾选了 Web、Thymeleaf 和 JPA 这几个复选框。还要记得勾上 H2 复选框,这样才能在开发应用程序时使用这个内嵌式数据库。至于项目元信息,就随便你写了。以阅读列表为例,我创建项目时使用了图2-1中的信息。

image 2024 03 14 23 28 00 881
Figure 1. 图2-1 通过Initializr的Web界面初始化阅读列表应用程序

如果你创建项目时用的是 Spring Tool Suite 或者 IntelliJ IDEA,那么把图2-1的内容适配成 IDE 需要的东西就好了。

另一方面,如果用 Spring Boot CLI 来初始化应用程序,可以在命令行里键入以下内容:

$ spring init -dweb, data-jpa, h2, thymeleaf --build gradle readinglist

请记住,CLI 的 init 命令是不能指定项目根包名和项目名的。包名默认是 demo,项目名默认是 Demo。在项目创建完毕之后,你可以打开项目,把包名 demo 改为 readinglist,把 DemoApplication.java 改名为 ReadingListApplication.java。

项目创建完毕后,你应该能看到一个类似图2-2的项目结构。

image 2024 03 14 23 29 46 171
Figure 2. 图2-2 初始化后的readinglist项目结构

这个项目结构基本上和第 1 章里 Initializr 生成的结构是一样的,只不过你现在真的要去开发应用程序了,所以让我们先放慢脚步,仔细看看初始化的项目里都有什么东西。

查看初始化的Spring Boot新项目

图2-2中值得注意的第一件事是,整个项目结构遵循传统 Maven 或 Gradle 项目的布局,即主要应用程序代码位于 src/main/java 目录里,资源都在 src/main/resources 目录里,测试代码则在 src/test/java 目录里。此刻还没有测试资源,但如果有的话,要放在 src/test/resources 里。

再进一步,你会看到项目里还有不少文件。

  • build.gradle:Gradle 构建说明文件。

  • ReadingListApplication.java:应用程序的启动引导类(bootstrap class),也是主要的 Spring 配置类。

  • application.properties:用于配置应用程序和 Spring Boot 的属性。

  • ReadingListApplicationTests.java:一个基本的集成测试类。

因为构建说明文件里有很多 Spring Boot 的优点尚未揭秘,所以我打算把最好的留到最后,先让我们来看看 ReadingListApplication.java。

启动引导Spring

ReadingListApplication 在 Spring Boot 应用程序里有两个作用:配置和启动引导。首先,这是主要的 Spring 配置类。虽然 Spring Boot 的自动配置免除了很多 Spring 配置,但你还需要进行少量配置来启用自动配置。正如代码清单2-1所示,这里只有一行配置代码。

package readinglist;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// 开启组件扫描和自动配置
@SpringBootApplication
public class ReadingListApplication {
    public static void main(String[] args) {
        // 负责启动引导应用程序
        SpringApplication.run(ReadingListApplication.class, args);
    }
}

@SpringBootApplication开启了Spring的组件扫描和Spring Boot的自动配置功能。实际上,@SpringBootApplication将三个有用的注解组合在了一起。

  • Spring 的 @Configuration:标明该类使用 Spring 基于 Java 的配置。虽然本书不会写太多配置,但我们会更倾向于使用基于 Java 而不是 XML 的配置。

  • Spring 的 @ComponentScan:启用组件扫描,这样你写的 Web 控制器类和其他组件才能被自动发现并注册为 Spring 应用程序上下文里的 Bean。本章稍后会写一个简单的 Spring MVC 控制器,使用 @Controller 进行注解,这样组件扫描才能找到它。

  • Spring Boot 的 @EnableAutoConfiguration :这个不起眼的小注解也可以称为 @Abracadabra,就是这一行配置开启了 Spring Boot 自动配置的魔力,让你不用再写成篇的配置了。

在 Spring Boot 的早期版本中,你需要在 ReadingListApplication 类上同时标上这三个注解,但从 Spring Boot 1.2.0 开始,有 @SpringBootApplication 就行了。

如我所说,ReadingListApplication 还是一个启动引导类。要运行 Spring Boot 应用程序有几种方式,其中包含传统的 WAR 文件部署。但这里的 main() 方法让你可以在命令行里把该应用程序当作一个可执行 JAR 文件来运行。这里向 SpringApplication.run() 传递了一个 ReadingListApplication 类的引用,还有命令行参数,通过这些东西启动应用程序。

实际上,就算一行代码也没写,此时你仍然可以构建应用程序尝尝鲜。要构建并运行应用程序,最简单的方法就是用 Gradle 的 bootRun 任务:

$ gradle bootRun

bootRun 任务来自 Spring Boot 的 Gradle 插件,我们会在2.1.2节里详细讨论。此外,你也可以用 Gradle 构建项目,然后在命令行里用 java 来运行它:

$ gradle build
...
$ java -jar build/libs/readinglist-0.0.1-SNAPSHOT.jar

应用程序应该能正常运行,启动一个监听 8080 端口的 Tomcat 服务器。要是愿意,你可以用浏览器访问 http://localhost:8080 ,但由于还没写控制器类,你只会收到一个 HTTP 404(NOT FOUND)错误,看到错误页面。在本章结束前,这个 URL 将会提供一个阅读列表应用程序。

你几乎不需要修改 ReadingListApplication.java。如果你的应用程序需要 Spring Boot 自动配置以外的其他 Spring 配置,一般来说,最好把它写到一个单独的 @Configuration 标注的类里。(组件扫描会发现并使用这些类的。)极度简单的情况下,可以把自定义配置加入 ReadingListApplication.java。

测试Spring Boot应用程序

Initializr 还提供了一个测试类的骨架,可以基于它为你的应用程序编写测试。但 ReadingListApplicationTests(代码清单2-2)不止是个用于测试的占位符,它还是一个例子,告诉你如何为 Spring Boot 应用程序编写测试。

代码清单2-2 @SpringApplicationConfiguration加载Spring应用程序上下文
package readinglist;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import readinglist.ReadingListApplication;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes=ReadingListApplication.class) // 通过Spring Boot 加载上下文
@WebAppConfiguration
public class ReadingListApplicationTests {

    @Test
    public void contextLoads() { // 测试加载的上下文
    }
}

一个典型的 Spring 集成测试会用 @ContextConfiguration 注解标识如何加载 Spring 的应用程序上下文。但是,为了充分发挥 Spring Boot 的魔力,这里应该用 @SpringApplication-Configuration注解。正如你在代码清单2-2里看到的那样,ReadingListApplicationTests 使用 @SpringApplicationConfiguration 注解从 ReadingListApplication 配置类里加载 Spring 应用程序上下文。

ReadingListApplicationTests 里还有一个简单的测试方法,即 contextLoads()。实际上它就是个空方法。但这个空方法足以证明应用程序上下文的加载没有问题。如果 ReadingListApplication 里定义的配置是好的,就能通过测试。如果有问题,测试就会失败。

当然,现在这只是一个新的应用程序,你还会添加自己的测试。但 contextLoads() 方法是个良好的开端,此刻可以验证应用程序提供的各种功能。第 4 章会更详细地讨论如何测试 Spring Boot 应用程序。

配置应用程序属性

Initializr 为你生成的 application.properties 文件是一个空文件。实际上,这个文件完全是可选的,你大可以删掉它,这不会对应用程序有任何影响,但留着也没什么问题。

稍后,我们肯定有机会向 application.properties 里添加几个条目。但现在,如果你想小试牛刀,可以加一行看看:

server.port=8000

加上这一行,嵌入式 Tomcat 的监听端口就变成了 8000,而不是默认的 8080。你可以重新运行应用程序,看看是不是这样。

这说明 application.properties 文件可以很方便地帮你细粒度地调整 Spring Boot 的自动配置。你还可以用它来指定应用程序代码所需的配置项。在第 3 章里我们会看到好几个例子,演示 application.properties 的这两种用法。

要注意的是,你完全不用告诉 Spring Boot 为你加载 application.properties,只要它存在就会被加载,Spring 和应用程序代码都能获取其中的属性。

我们差不多已经把初始化的项目介绍完了,还剩最后一样东西,让我们来看看 Spring Boot 应用程序是如何构建的。

Spring Boot项目构建过程解析

Spring Boot 应用程序的大部分内容都与其他 Spring 应用程序没有什么区别,与其他 Java 应用程序也没什么两样,因此构建一个 Spring Boot 应用程序和构建其他 Java 应用程序的过程类似。你可以选择 Gradle 或 Maven 作为构建工具,描述构建说明文件的方法和描述非 Spring Boot 应用程序的方法相似。但是,Spring Boot 在构建过程中耍了些小把戏,在此需要做个小小的说明。

Spring Boot 为 Gradle 和 Maven 提供了构建插件,以便辅助构建 Spring Boot 项目。代码清单2-3是 Initializr 创建的 build.gradle 文件,其中应用了 Spring Boot 的 Gradle 插件。

代码清单2-3 使用Spring Boot的Gradle插件
plugins {
    id 'org.springframework.boot' version '3.2.2'
    id 'java'
}

jar {
    baseName = 'readinglist'
    version = '0.0.1-SNAPSHOT'
}

// 指定JDK版本
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

repositories {
    maven { url 'https://maven.aliyun.com/repository/google' }
    maven { url 'https://maven.aliyun.com/repository/public/'}
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
    implementation("com.h2database:h2")
}

另一方面,要是选择用 Maven 来构建应用程序,Initializr 会替你生成一个 pom.xml 文件,其中使用了 Spring Boot 的 Maven 插件,如代码清单 2-4 所示。

<?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>3.2.3</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.manning</groupId>
	<artifactId>readinglist</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>ReadingList</name>
	<description>Reading List Demo</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</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>

无论你选择 Gradle 还是 Maven, Spring Boot 的构建插件都对构建过程有所帮助。你已经看到过如何用 Gradle 的 bootRun 任务来运行应用程序了。Spring Boot 的 Maven 插件与之类似,提供了一个 spring-boot:run 目标,如果你使用 Maven,它能实现相同的功能。

构建插件的主要功能是把项目打包成一个可执行的超级 JAR(uber-JAR),包括把应用程序的所有依赖打入 JAR 文件内,并为 JAR 添加一个描述文件,其中的内容能让你用 java -jar 来运行应用程序。

除了构建插件,代码清单2-4里的 Maven 构建说明中还将 spring-boot-starter-parent 作为上一级,这样一来就能利用 Maven 的依赖管理功能,继承很多常用库的依赖版本,在你声明依赖时就不用再去指定版本号了。请注意,这个 pom.xml 里的 <dependency> 都没有指定版本。

遗憾的是,Gradle 并没有 Maven 这样的依赖管理功能,为此 Spring Boot Gradle 插件提供了第三个特性,它为很多常用的 Spring 及其相关依赖模拟了依赖管理功能。其结果就是,代码清单2-3的 build.gradle 里也没有为各项依赖指定版本。

说起依赖,无论哪个构建说明文件,都只有五个依赖,除了你手工添加的 H2 之外,其他的 Artifact ID 都有 spring-boot-starter-前缀。这些都是 Spring Boot 起步依赖,它们都有助于 Spring Boot 应用程序的构建。让我们来看看它们究竟都有哪些好处。