实验介绍
尽管希腊哲学家赫拉克利特(Heraclitus)并不作为一名软件开发人员而闻名,但他似乎深谙此道。他的一句话经常被引用:“唯一不变的就是变化”,这句话抓住了软件开发的真谛。
我们现在开发应用的方式和 1 年前、5 年前、10 年前都是不同的,更别提 15 年前了,当时 Rod Johnson 的图书 Expert One-on-One J2EE Design and Development 介绍了 Spring 框架的初始形态。
当时,最常见的应用形式是基于浏览器的 Web 应用,后端由关系型数据库作为支撑。尽管这种形式的开发依然有它的价值,Spring 也为这种应用提供了良好的支持,但是我们现在感兴趣的还包括如何开发面向云的由微服务组成的应用,这些应用会将数据保存到各种类型的数据库中。另外一个崭新的关注点是反应式编程,它致力于通过非阻塞操作提供更好的扩展性并提升性能。
随着软件开发的发展,Spring 框架也在不断变化,以解决现代应用开发中的问题,其中就包括微服务和反应式编程。Spring 还通过引入 Spring Boot 简化自己的开发模型。
不管你是开发以数据库作为支撑的简单 Web 应用,还是围绕微服务构建一个现代应用,Spring 框架都能帮助你达成目标。本章是使用 Spring 进行现代应用开发的第一步。
知识点
- Spring 和 Spring Boot 的必备知识
- 初始化 Spring 项目
- Spring 生态系统
实验环境
- SpringBoot 2.0.4.RELEASE
什么是 Spring
我知道你现在可能迫不及待地想要开始编写 Spring 应用了,我可以向你保证,在本章结束之前,你肯定能够开发一个简单的 Spring 应用。首先,我将使用 Spring 的一些基础概念为你搭建一个舞台,帮助你理解 Spring 是如何运行起来的。
任何实际的应用程序都是由很多组件组成的,每个组件负责整个应用功能的一部分,这些组件需要与其他的应用元素进行协调以完成自己的任务。当应用程序运行时,需要以某种方式创建并引入这些组件。
Spring 的核心是提供了一个容器(container),通常称为 Spring 应用上下文(Spring application context),它们会创建和管理应用组件。这些组件也可以称为 bean,会在 Spring 应用上下文中装配在一起,从而形成一个完整的应用程序。这就像砖块、砂浆、木材、管道和电线组合在一起,形成一栋房子似的。
将 bean 装配在一起的行为是通过一种基于依赖注入(dependency injection,DI)的模式实现的。此时,组件不会再去创建它所依赖的组件并管理它们的生命周期,使用依赖注入的应用依赖于单独的实体(容器)来创建和维护所有的组件,并将其注入到需要它们的 bean 中。通常,这是通过构造器参数和属性访问方法来实现的。
举例来说,假设在应用的众多组件中,有两个是我们需要处理的:库存服务(用来获取库存水平)和商品服务(用来提供基本的商品信息)。商品服务需要依赖于库存服务,这样它才能提供商品的完整信息。下图阐述这些 bean 和 Spring 应用上下文之间的关系。
在核心容器之上,Spring 及其一系列的相关库提供了 Web 框架、各种持久化可选方案、安全框架、与其他系统集成、运行时监控、微服务支持、反应式编程以及众多现代应用开发所需的特性。
在历史上,指导 Spring 应用上下文将 bean 装配在一起的方式是使用一个或多个 XML 文件(描述各个组件以及它们与其他组件的关联关系)。例如,如下的 XML 描述了两个 bean,也就是 InventoryService bean 和 ProductService bean,并且通过构造器参数将 InventoryService 装配到了 ProductService 中:
<bean id="inventoryService"
class="com.example.InventoryService" />
<bean id="productService"
class="com.example.ProductService" />
<constructor-arg ref="inventoryService" />
</bean>
但是,在最近的 Spring 版本中,基于 Java 的配置更为常见。如下基于 Java 的配置类是与 XML 配置等价的:
@Configuration
public class ServiceConfiguration {
@Bean
public InventoryService inventoryService() {
return new InventoryService();
}
@Bean
public ProductService productService() {
return new ProductService(inventoryService());
}
}
@Configuration 注解会告知 Spring 这是一个配置类,会为 Spring 应用上下文提供 bean。这个配置类的方法使用 @Bean 注解进行了标注,表明这些方法所返回的对象会以 bean 的形式添加到 Spring 的应用上下文中(默认情况下,这些 bean 所对应的 bean ID 与定义它们的方法名称是相同的)。
相对于基于 XML 的配置方式,基于 Java 的配置会带来多项额外的收益,包括更强的类型安全性以及更好的重构能力。即便如此,不管是使用 Java 还是使用 XML 的显式配置,只有当 Spring 不能进行自动配置的时候才是必要的。
在 Spring 技术中,自动配置起源于所谓的自动装配(autowiring)和组件扫描(component scanning)。借助组件扫描技术,Spring 能够自动发现应用类路径下的组件,并将它们创建成 Spring 应用上下文中的 bean。借助自动装配技术,Spring 能够自动为组件注入它们所依赖的其他 bean。
最近,随着 Spring Boot 的引入,自动配置的能力已经远远超出了组件扫描和自动装配。Spring Boot 是 Spring 框架的扩展,提供了很多增强生产效率的方法。最为大家所熟知的增强方法就是自动配置(autoconfiguration),Spring Boot 能够基于类路径中的条目、环境变量和其他因素合理猜测需要配置的组件并将它们装配在一起。
我非常愿意为你展现一些关于自动配置的示例代码,但是我做不到。自动配置就像风一样,你可以看到它的效果,但是我找不到代码指给你说,「看!这就是自动配置的样例!」事情发生了,组件启用了,功能也提供了,但是不用编写任何代码。没有代码就是自动装配的本质,也是它如此美妙的原因所在。
Spring Boot 大幅度减少了构建应用所需的显式配置的数量(不管是 XML 配置还是 Java 配置)。实际上,当完成本章的样例时,我们会有一个可运行的 Spring 应用,该应用只有一行 Spring 配置代码。
Spring Boot 极大地改善了 Spring 的开发,因此很难想象在没有它的情况下如何开发 Spring 应用。因此,本书会将 Spring 和 Spring Boot 当成一回事。我们会尽可能多地使用 Spring Boot,只有在必要的时候才使用显式配置。因为 Spring XML 配置是一种过时的方式,所以我们主要关注 Spring 基于 Java 的配置。
闲言少叙,既然本书的名称中包含「实战」这个词,那么就开始动手吧!下面我们将会编写使用 Spring 的第一个应用。
初始化 Spring 应用
在本书中,我们将会创建一个名为 Taco Cloud 的在线应用,它能够订购人类所发明的一种美味,也就是墨西哥煎玉米卷(taco)。当然,在这个过程中,为了达成我们的目标,我们将会用到 Spring、Spring Boot 以及各种相关的库和框架。
我们有多种初始化 Spring 应用的可选方案。尽管我可以教你手动创建项目目录结构和定义构建规范的各个步骤,但这无疑是浪费时间,我们最好将时间花在编写应用代码上。因此,我们将会学习如何使用 Spring Initializr 初始化应用。
Spring Initializr 是一个基于浏览器的 Web 应用,同时也是一个 REST API,能够生成一个 Spring 项目结构的骨架,我们还可以使用各种想要的功能来填充它。使用 Spring Initializr 的几种方式如下:
- 通过地址为 https://start.spring.io/ 的 Web 应用;
- 在命令行中使用 curl 命令;
- 在命令行中使用 Spring Boot 命令行接口;
- 在 Spring Tool Suite 中创建新项目;
- 在 IntelliJ IDEA 中创建新项目;
- 在 NetBeans 中创建新项目。
初始化项目
实验中我们将通过命令行 curl 命令的方式来创建项目,在实验楼 WebIDE 中执行以下命令。
curl https://start.spring.io/starter.tgz
-d groupId=sia
-d artifactId=taco-cloud
-d packaging=jar
-d name=taco-cloud
-d packageName=tacos
-d dependencies=web,devtools,thymeleaf
-d type=maven-project
-d baseDir=taco-cloud
-d bootVersion=2.0.4.RELEASE
-d javaVersion=1.8
| tar -xzvf -
-d 指定额外的参数。
- groupId:指定 groupId。
- artifactId:指定 artifactId。
- packaging:指定打包方式。
- name:指定项目名,如果没有指定 applicationName,则将其作为 Application Name。
- packageName:指定项目的包名。
- dependencies:指定项目的依赖。
- type:指定项目的类型(使用哪种构建工具)。
- baseDir:指定项目的根目录名称。
- bootVersion:指定 SpringBoot 的版本。
这些参数都有其默认值,可以参考手册。使用 curl https://start.spring.io 命令,该命令将会输出使用手册内容。为了便于阅读,你也可以加上 -o manual.txt 将输出结果保存在 manual.txt 文件中。
检查 Spring 项目结构
项目创建之后,我们来看一下项目的目录结构,看一下其中都包含什么内容。
你可能已经看出来了,这就是一个典型的 Maven 或 Gradle 项目结构,其中应用的源码放到了 src/main/java 中,测试代码放到了 src/test/java 中,而非 Java 的资源放到了 src/main/resources。在这个项目结构中,我们需要注意以下几点。
- mvnw 和 mvnw.cmd:这是 Maven 包装器(wrapper)脚本。借助这些脚本,即便你的机器上没有安装 Maven,也可以构建项目。
- pom.xml:这是 Maven 构建规范,随后我们将会深入介绍该文件。
- TacoCloudApplication.java:这是 Spring Boot 主类,它会启动该项目。随后,我们会详细介绍这个类。
- application.properties:这个文件起初是空的,但是它为我们提供了指定配置属性的地方。在本章中,我们会稍微修改一下这个文件,但是我会将配置属性的详细阐述放到第 5 章。
- static:在这个文件夹下,你可以存放任意为浏览器提供服务的静态内容(图片、样式表、JavaScript 等),该文件夹初始为空。
- templates:这个文件夹中存放用来渲染内容到浏览器的模板文件。这个文件夹初始是空的,不过我们很快就会往里面添加 Thymeleaf 模板。
- TacoCloudApplicationTests.java:这是一个简单的测试类,它能确保 Spring 应用上下文可以成功加载。在开发应用的过程中,我们会将更多的测试添加进来。
随着 Taco Cloud 应用功能的增长,我们会不断使用 Java 代码、图片、样式表、测试以及其他附属内容来充实这个项目结构。不过,在此之前,我们先看一下 Spring Initializr 提供的几个条目。
探索构建规范
使用 Initializr 创建项目时,其默认使用 Maven 来进行构建。因此,Spring Initializr 所生成的 pom.xml 文件已经包含了我们所选择的依赖。下面代码展示了 Initializr 为我们提供的完整 pom.xml。
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>sia</groupId>
<artifactId>taco-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>taco-cloud</name>
<description>Taco Cloud Example</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>
UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>
UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>htmlunit-driver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在 pom.xml 文件中,我们第一个需要注意的地方就是
打包为 JAR 文件是基于云思维做出的选择。尽管 WAR 文件非常适合部署到传统的 Java 应用服务器上,但对于大多数云平台来说它们并不是理想的选择。有些云平台(比如 Cloud Foundry)也能够部署和运行 WAR 文件,但是所有的 Java 云平台都能够运行可执行的 JAR 文件。因此,Spring Initializr 默认会使用基于 JAR 的打包方式,除非我们明确告诉它采用其他的方式。
如果你想要将应用部署到传统的 Java 应用服务器上,那么需要选择使用基于 WAR 的打包方式并要包含一个 Web 初始化类。在第 2 章中,我们将会更详细地了解如何构建 WAR 文件。
接下来,请留意
既然我们谈到了依赖的话题,那么需要注意在
你可能也会注意到这 4 个依赖的 Artifact ID 中有 3 个都有 starter 这个单词。Spring Boot starter 依赖的特别之处在于它们本身并不包含库代码,而是传递性地拉取其他的库。这种 starter 依赖主要有 3 个好处。
- 构建文件会显著减小并且更易于管理,因为这样不必为每个所需的依赖库都声明依赖。
- 我们能够根据它们所提供的功能来思考依赖,而不是根据库的名称。如果是开发 Web 应用,那么你只需要添加 web starter 就可以了,而不必添加一堆单独的库再编写 Web 应用。
- 我们不必再担心库版本的问题。你可以直接相信给定版本的 Spring Boot,传递性引入的库的版本是兼容的。现在,你只需要关心使用的是哪个版本的 Spring Boot 就可以了。
最后,构建规范还包含一个 Spring Boot 插件。这个插件提供了一些重要的功能。
- 它提供了一个 Maven goal,允许我们使用 Maven 来运行应用。在后面小节,我们将会尝试这个 goal。
- 它会确保依赖的所有库都会包含在可执行 JAR 文件中,并且能够保证它们在运行时类路径下是可用的。
- 它会在 JAR 中生成一个 manifest 文件,将引导类(在我们的场景中,也就是 TacoCloudApplication)声明为可执行 JAR 的主类。
谈到了主类,我们打开它看一下。
因为我们将会通过可执行 JAR 文件的形式来运行应用,所以很重要的一点就是要有一个主类,它将会在 JAR 运行的时候被执行。我们同时还需要一个最小化的 Spring 配置,以引导该应用。这就是 TacoCloudApplication 类所做的事情,如下面代码所示。
package tacos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // <--- Spring Boot应用
public class TacoCloudApplication {
public static void main(String[] args) {
SpringApplication.run(TacoCloudApplication.class, args); // <--- 运行应用
}
}
尽管在 TacoCloudApplication 中只有很少的代码,但是它包含了很多的内容。其中,最强大的一行代码也是最短的。@SpringBootApplication 注解明确表明这是一个 Spring Boot 应用。但是,@SpringBootApplication 远比看上去更强大。
@SpringBootApplication 是一个组合注解,它组合了 3 个其他的注解。
- @SpringBootConfiguration:将该类声明为配置类。尽管这个类目前还没有太多的配置,但是后续我们可以按需添加基于 Java 的 Spring 框架配置。这个注解实际上是 @Configuration 注解的特殊形式。
- @EnableAutoConfiguration:启用 Spring Boot 的自动配置。我们随后会介绍自动配置的更多功能。就现在来说,我们只需要知道这个注解会告诉 Spring Boot 自动配置它认为我们会用到的组件。
- @ComponentScan:启用组件扫描。这样我们能够通过像 @Component、@Controller、@Service 这样的注解声明其他类,Spring 会自动发现它们并将它们注册为 Spring 应用上下文中的组件。
TacoCloudApplication 另外一个很重要的地方是它的 main() 方法。这是 JAR 文件执行的时候要运行的方法。在大多数情况下,这个方法都是样板代码,我们编写的每个 Spring Boot 应用都会有一个类似或完全相同的方法(类名不同则另当别论)。
这个 main() 方法会调用 SpringApplication 中静态的 run() 方法,后者会真正执行应用的引导过程,也就是创建 Spring 的应用上下文。在传递给 run() 的两个参数中,一个是配置类,另一个是命令行参数。尽管传递给 run() 的配置类不一定要和引导类相同,但这是最便利和最典型的做法。
你可能并不需要修改引导类中的任何内容。对于简单的应用程序来说,你可能会发现在引导类中配置一两个组件是非常方便的,但是对于大多数应用来说,最好还是要为没有实现自动配置的功能创建一个单独的配置类。在本课程的整个过程中,我们将会创建多个配置类,所以请继续关注后续的细节
测试应用
测试是软件开发的重要组成部分。鉴于此,Spring Initializr 为我们提供了一个测试类作为起步。下面代码展现了这个测试类的概况。
package tacos;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class) // <--- 使用Spring的运行器
@SpringBootTest // <--- Spring Boot测试
public class TacoCloudApplicationTests {
@Test // <--- 测试方法
public void contextLoads() {
}
}
TacoCloudApplicationTests 类中的内容并不多:这个类中只有一个空的测试方法。即便如此,这个测试类还是会执行必要的检查,确保 Spring 应用上下文能够成功加载。如果你所做的变更导致 Spring 应用上下文无法创建,那么这个测试将会失败,你就可以做出反应来解决相关的问题了。
另外,注意这个类带有 @RunWith(SpringRunner.class) 注解。@RunWith 是 JUnit 的注解,它会提供一个测试运行器(runner)来指导 JUnit 如何运行测试。可以将其想象为给 JUnit 应用一个插件,以提供自定义的测试行为。在本例中,为 JUnit 提供的是 SpringRunner,这是一个 Spring 提供的测试运行器,它会创建测试运行所需的 Spring 应用上下文。
测试运行器的其他名称
如果你已经熟悉如何编写 Spring 测试或者见过其他一些基于 Spring 的测试类,那么你可能见过名为 SpringJUnit4ClassRunner 的测试运行器。SpringRunner 是 SpringJUnit4ClassRunner 的别名,是在 Spring 4.3 中引入的,以便于移除对特定 JUnit 版本的关联(比如,JUnit 4)。毫无疑问,这个别名更易于阅读和输入。
@SpringBootTest 会告诉 JUnit 在启动测试的时候要添加上 Spring Boot 的功能。从现在开始,我们可以将这个测试类视同为在 main() 方法中调用SpringApplication.run()。在这本书中,我们将会多次看到 @SpringBootTest,而且会不断见识它的威力。
最后,就是测试方法本身了。尽管 @RunWith(SpringRunner.class) 和 @SpringBootTest 会为测试加载 Spring 应用上下文,但是如果没有任何测试方法,那么它们其实什么事情都没有做。即便没有任何断言或代码,这个空的测试方法也会提示这两个注解完成了它们的工作并成功加载 Spring 应用上下文。如果这个过程中有任何问题,那么测试都会失败。
此时,我们已经看完了 Spring Initializr 为我们提供的代码。我们看到了一些用来开发 Spring 应用程序的基础样板,但是还没有编写任何代码。现在是时候启动 IDE、准备好键盘并向 Taco Cloud 应用程序添加一些自定义的代码了。