2.1 展现信息

从根本上来讲,Taco Cloud是一个可以在线订购taco的地方。但是,除此之外,Taco Cloud允许客户展现其创意,能够让他们通过丰富的配料(ingredient)设计自己的taco。

因此,Taco Cloud需要有一个页面为taco艺术家展现都可以选择哪些配料。可选的配料可能随时会发生变化,所以不能将它们硬编码到HTML页面中。我们应该从数据库中获取可用的配料并将其传递给页面,进而展现给客户。

在Spring Web应用中,获取和处理数据是控制器的任务,而将数据渲染到HTML中并在浏览器中展现则是视图的任务。为了支撑taco的创建页面,我们需要构建如下组件。

  • 用来定义taco配料属性的领域类。
  • 用来获取配料信息并将其传递至视图的Spring MVC控制器类。
  • 用来在用户的浏览器中渲染配料列表的视图模板。

这些组件之间的关系如图2.1所示。

epub_29101559_17

图2.1 典型的Spring MVC请求流

因为本章主要关注Spring的Web框架,所以我们会将数据库相关的内容放到第3章中进行讲解。现在的控制器只负责向视图提供配料。在第3章中,我们会重新改造这个控制器,让它能够与repository协作,从数据库中获取配料数据。

在编写控制器和视图之前,我们首先确定一下用来表示配料的领域类型,它会为我们开发Web组件奠定基础。

2.1.1 构建领域类

应用的领域指的是它所要解决的主题范围:也就是会影响到对应用理解的理念和概念[^1]。在Tao Cloud应用中,领域对象包括taco设计、组成这些设计的配料、顾客以及顾客所下的订单。作为开始,我们首先关注taco的配料。

在我们的领域中,taco配料是非常简单的对象。每种配料都有一个名称和类型,以便于对其进行可视化的分类(蛋白质、奶酪、酱汁等)。每种配料还有一个ID,这样的话对它的引用就能非常容易和明确。如下的Ingredient类定义了我们所需的领域对象。

程序清单2.1 定义taco配料

1
2
3
4
5
6
7
8
9
10
11
12
13
package tacos;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
public class Ingredient {
private final String id;
private final String name;
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}

我们可以看到,这是一个非常普通的Java领域类,它定义了描述配料所需的3个属性。在程序清单2.1中,Ingredient类最不寻常的一点是它似乎缺少了常见的getter和setter方法,以及equals()、hashCode()、toString()等方法。

在程序清单2.1中没有这些方法,部分原因是节省空间,此外还因为我们使用了名为Lombok的库(这是一个非常棒的库,它能够在运行时动态生成这些方法)。实际上,类级别的@Data注解就是由Lombok提供的,它会告诉Lombok生成所有缺失的方法,同时还会生成所有以final属性作为参数的构造器。通过使用Lombok,我们能够让Ingredient的代码简洁明了。

Lombok并不是Spring库,但是它非常有用,我发现如果没有它,开发工作将很难开展。当我需要在书中将代码示例编写得短小简洁时,它简直成了我的救星。

要使用Lombok,首先要将其作为依赖添加到项目中。如果你使用Spring ToolSuite,那么只需要用右键点击pom.xml,并从Spring上下文菜单选项中选择“Edit Starters”。在第1章中看到的选择依赖的对话框将会再次出现(见图1.4),这样的话我们就有机会添加依赖或修改已选择的依赖了。找到Lombok选项,并确保它处于已选中的状态,然后点击“OK”,Spring Tool Suite会自动将其添加到构建规范中。

另外,你也可以在pom.xml中通过如下条目进行手动添加:

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

这个依赖将会在开发阶段为你提供Lombok注解(例如@Data),并且会在运行时进行自动化的方法生成。但是,我们还需要将Lombok作为扩展添加到IDE上,否则IDE将会报错,提示缺少方法和final属性没有赋值。参见Lombok项目页面,以查阅如何在你所选择的IDE上安装Lombok。

我相信你会发现Lombok非常有用,但你也要知道,它是可选的。在开发Spring应用的时候,它并不是必备的,所以如果你不想使用它的话,完全可以手动编写这些缺失的方法。当你完成之后,我们将会在应用中添加一些控制器,让它们来处理Web请求。

2.1.2 创建控制器类

在Spring MVC框架中,控制器是重要的参与者。它们的主要职责是处理HTTP请求,要么将请求传递给视图以便于渲染HTML(浏览器展现),要么直接将数据写入响应体(RESTful)。在本章中,我们将会关注使用视图来为Web浏览器生成内容的控制器。在第6章中,我们将会看到如何以REST API的形式编写控制器来处理请求。

对于Taco Cloud应用来说,我们需要一个简单的控制器,它要完成如下功能。

  • 处理路径为“/design”的HTTP GET请求。
  • 构建配料的列表。
  • 处理请求,并将配料数据传递给要渲染为HTML的视图模板,发送给发起请求的Web浏览器。

程序清单2.2中的DesignTacoController类解决了这些需求。

程序清单2.2 初始的Spring控制器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package tacos.web;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
import tacos.Taco;
import tacos.Ingredient;
import tacos.Ingredient.Type;
@Slf4j
@Controller
@RequestMapping("/design")
public class DesignTacoController {
@GetMapping
public String showDesignForm(Model model) {
List<Ingredient> ingredients = Arrays.asList(
new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
new Ingredient("COTO", "Corn Tortilla", Type.WRAP),
new Ingredient("GRBF", "Ground Beef", Type.PROTEIN),
new Ingredient("CARN", "Carnitas", Type.PROTEIN),
new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES),
new Ingredient("LETC", "Lettuce", Type.VEGGIES),
new Ingredient("CHED", "Cheddar", Type.CHEESE),
new Ingredient("JACK", "Monterrey Jack", Type.CHEESE),
new Ingredient("SLSA", "Salsa", Type.SAUCE),
new Ingredient("SRCR", "Sour Cream", Type.SAUCE)
);
Type[] types = Ingredient.Type.values();
for (Type type : types) {
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients, type));
}
model.addAttribute("design", new Taco());
return "design";
}
private List<Ingredient> filterByType(
<Ingredient> ingredients, Type type) {
return ingredients
.stream()
.filter(x -> x.getType().equals(type))
.collect(Collectors.toList());
}
}

对于DesignTacoController,我们先要注意在类级别所应用的注解。首先是@Slf4j,这是Lombok所提供的注解,在运行时,它会在这个类中自动生成一个SLF4J(Simple Logging Facade for Java)Logger。这个简单的注解和在类中通过如下代码显式声明的效果是一样的:

1
2
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(DesignTacoController.class);

随后,我们将会用到这个Logger。

DesignTacoController用到的下一个注解是@Controller。这个注解会将这个类识别为控制器,并且将其作为组件扫描的候选者,所以Spring会发现它并自动创建一个DesignTacoController实例,并将该实例作为Spring应用上下文中的bean。

DesignTacoController还带有@RequestMapping注解。当@RequestMapping注解用到类级别的时候,它能够指定该控制器所处理的请求类型。在本例中,它规定DesignTacoController将会处理路径以“/design”开头的请求。

处理GET请求

修饰showDesignForm()方法的@GetMapping注解对类级别的@RequestMapping进行了细化。@GetMapping结合类级别的@RequestMapping,指明当接收到对“/design”的HTTP GET请求时,将会调用showDesignForm()来处理请求。

@GetMapping是一个相对较新的注解,是在Spring 4.3引入的。在Spring 4.3之前,你可能需要使用方法级别的@RequestMapping注解作为替代:

1
@RequestMapping(method=RequestMethod.GET)

显然,@GetMapping更加简洁,并且指明了它的目标HTTP方法。@GetMapping只是诸多请求映射注解中的一个。表2.1列出了Spring MVC中所有可用的请求映射注解。

表2.1 Spring MVC的请求映射注解

epub_29101559_19


让正确的事情变得更容易

在为控制器方法声明请求映射时,越具体越好。这意味着至少要声明路径(或者从类级别的@RequestMapping继承一个路径)以及它所处理的HTTP方法。

但是更长的@RequestMapping(method=RequestMethod.GET)注解很容易让开发人员采取懒惰的方式,也就是忽略掉method属性。幸亏有了Spring 4.3的新注解,正确的事情变得更容易了,我们的输入变得更少了。

新的请求映射注解具有和@RequestMapping完全相同的属性,所以我们可以在使用@RequestMapping的任何地方使用它们。

通常,我喜欢只在类级别上使用@RequestMapping,以便于指定基本路径。在每个处理器方法上,我会使用更具体的@GetMapping@PostMapping等注解。


现在,我们已经知道showDesignForm()方法会处理请求,接下来我们看一下方法体,看它都做了些什么工作。这个方法构建了一个Ingredient对象的列表。现在,这个列表是硬编码的。当我们学习第3章的时候,会从数据库中获取可用taco配料并将其放到列表中。

配料列表准备就绪之后,showDesignForm()方法接下来的几行代码会根据配料类型过滤列表。配料类型的列表会作为属性添加到Model对象上,这个对象是以参数的形式传递给showDesignForm()方法的。Model对象负责在控制器和展现数据的视图之间传递数据。实际上,放到Model属性中的数据将会复制到ServletResponse的属性中,这样视图就能在这里找到它们了。showDesignForm()方法最后返回“design”,这是视图的逻辑名称,会用来将模型渲染到视图上。

我们的DesignTacoController已经具备雏形了。如果你现在运行应用并在浏览器上访问“/design”路径,DesignTacoController的showDesignForm()将会被调用,它会从repository中获取数据并放到模型中,然后将请求传递给视图。但是,我们现在还没有定义视图,请求将会遇到很糟糕的问题,也就是HTTP 404(NotFound)。为了解决这个问题,我们将注意力切换到视图上,在这里数据将会使用HTML进行装饰,以便于在用户的Web浏览器中进行展现。

2.1.3 设计视图

在控制器完成它的工作之后,现在就该视图登场了。Spring提供了多种定义视图的方式,包括JavaServer Pages(JSP)、Thymeleaf、FreeMarker、Mustache和基于Groovy的模板。就现在来讲,我们会使用Thymeleaf,这也是我们在第1章开启这个项目时的选择。我们会在第2.5节考虑其他的可选方案。

为了使用Thymeleaf,我们需要添加另外一个依赖到项目构建中。如下的<dependency>条目使用了Spring Boot的Thymeleaf starter,从而能够让Thymeleaf渲染我们将要创建的视图:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在运行时,Spring Boot的自动配置功能会发现Thymeleaf在类路径中,因此会为Spring MVC创建支撑Thymeleaf视图的bean。

像Thymeleaf这样的视图库在设计时是与特定的Web框架解耦的。这样的话,它们无法感知Spring的模型抽象,因此无法与控制器放到Model中的数据协同工作。但是,它们可以与Servlet的request属性协作。所以,在Spring将请求转移到视图之前,它会把模型数据复制到request属性中,Thymeleaf和其他的视图模板方案就能访问到它们了。

Thymeleaf模板就是增加一些额外元素属性的HTML,这些属性能够指导模板如何渲染request数据。举例来说,如果有一个请求属性的key为“message”,我们想要使用Thymeleaf将其渲染到一个HTML <p>标签中,那么在Thymeleaf模板中我们可以这样写:

1
<p th:text="${message}">placeholder message</p>

当模板渲染成HTML的时候,<p>元素体将会被替换为Servlet Request中key为“message”的属性值。“th:text”是Thymeleaf命名空间中的属性,它会执行这个替换过程。${}会告诉它要使用某个请求属性(在本例中,也就是“message”)中的值。

Thymeleaf还提供了一个属性“th:each”,它会迭代一个元素集合,为集合中的每个条目渲染HTML。在我们设计视图展现模型中的配料列表时,这就非常便利了。举例来说,如果只想渲染“wrap”配料的列表,我们可以使用如下的HTML片段:

1
2
3
4
5
<h3>Designate your wrap:</h3>
<div th:each="ingredient : ${wrap}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>

在这里,我们在<div>标签中使用th:each属性,这样的话就能针对wrap request属性所对应集合中的每个元素重复渲染<div>了。在每次迭代的时候,配料元素都会绑定到一个名为ingredient的Thymeleaf变量上。

<div>元素中,有一个<input>复选框元素,还有一个为复选框提供标签的<span>元素。复选框使用Thymeleafth:value来为渲染出的<input>元素设置value属性,这里会将其设置为所找到的ingredientid属性。<span>元素使用th:text将“INGREDIENT”占位符文本替换为ingredientname属性。

当用实际的模型数据进行渲染的时候,其中一个<div>迭代的渲染结果可能会如下所示:

1
2
3
4
<div>
<input name="ingredients" type="checkbox" value="FLTO" />
<span>Flour Tortilla</span><br/>
</div>

最终,上述的Thymeleaf片段会成为一大段HTML表单的一部分,我们taco艺术家用户会通过这个表单来提交其美味的作品。完整的Thymeleaf模板会包括所有的配料类型,表单如程序清单2.3所示:

程序清单2.3 设计taco的完整页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Taco Cloud</title>
<link rel="stylesheet" th:href="@{/styles.css}" />
</head>
<body>
<h1>Design your taco!</h1>
<img th:src="@{/images/TacoCloud.png}"/>
<form method="POST" th:object="${design}">
<div class="grid">
<div class="ingredient-group" id="wraps">
<h3>Designate your wrap:</h3>
<div th:each="ingredient : ${wrap}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}"
/>
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
<div class="ingredient-group" id="proteins">
<h3>Pick your protein:</h3>
<div th:each="ingredient : ${protein}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}"
/>
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
<div class="ingredient-group" id="cheeses">
<h3>Choose your cheese:</h3>
<div th:each="ingredient : ${cheese}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}"
/>
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
<div class="ingredient-group" id="veggies">
<h3>Determine your veggies:</h3>
<div th:each="ingredient : ${veggies}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}"
/>
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
<div class="ingredient-group" id="sauces">
<h3>Select your sauce:</h3>
<div th:each="ingredient : ${sauce}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}"
/>
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
</div>
<div>
<h3>Name your taco creation:</h3>
<input type="text" th:field="*{name}"/>
<br/>
<button>Submit your taco</button>
</div>
</form>
</body>
</html>

可以看到,我们会为各种类型的配料重复定义<div>片段。另外,我们还包含了一个Submit按钮,以及用户用来定义其作品名称的输入域。

值得注意的是,完整的模板包含了一个Taco Cloud的logo图片以及对样式表的<link>引用[^2]。在这两个场景中,都使用了Thymeleaf的@{}操作符,用来生成一个相对上下文的路径,以便于引用我们需要的静态制件(artifact)。正如我们在第1章中所学到的,在Spring Boot应用中,静态内容要放到根路径的“/static”目录下。

我们的控制器和视图已经完成了,现在我们可以将应用启动起来,看一下我们的劳动成果。运行Spring Boot应用有很多种方式。在第1章中,我为你首先展示了如何将应用构建成一个可执行的JAR文件,然后通过java –jar命令来运行这个JAR。我还展示了如何直接通过mvn spring-boot:run构建命令来运行应用。

不管你采用哪种方式来启动Taco Cloud应用,在启动之后都可以通过 http://localhost:8080/design 来进行访问。你将会看到如图2.2所示的一个页面。

epub_29101559_21

图2.2 渲染之后的taco设计页面

这看上去非常不错!访问你站点的taco艺术家可以看到一个表单,这个表单中包含了各种taco配料,他们可以使用这些配料创建自己的杰作。但是当他们点击Submit Your Taco按钮的时候会发生什么呢?

我们的DesignTacoController还没有为接收创建taco的请求做好准备。如果提交设计表单,用户就会遇到一个错误(具体来讲,将会是一个HTTP 405错误:Request Method “POST” Not Supported)。接下来,我们通过编写一些处理表单提交的控制器代码来修正这个错误。

[^1]: 如果你想更深入地了解应用领域,我推荐你阅读Eric Evans的《领域驱动设计》(ISBN978-7-115-37675-6,人民邮电出版社出版)。
[^2]: 样式表的内容与我们的讨论无关,它只是包含了让配料两列显示的样式,避免出现一个很长的配料列表。

第2章 开发Web应用


本章内容:

  • 在浏览器中展现模型数据
  • 处理和校验表单输入
  • 选择视图模板库

第一印象是非常重要的:Curb Appeal能够在购房者真正进门之前就将房子卖掉;如果一辆车喷成了樱桃色,那么它的油漆会比它的引擎更引人注目;文学作品中充满了一见钟情的故事。内在固然非常重要,但是外在的,也就是第一眼看到的东西同样非常重要。

我们使用Spring所构建的应用能完成各种各样的事情,包括处理数据、从数据库中读取信息以及与其他应用进行交互。但是,用户对应用程序的第一印象来源于用户界面。在很多应用中,UI是以浏览器中的Web应用的形式来展现的。

在第1章中,我们创建了第一个Spring MVC控制器来展现应用的主页。但是,Spring MVC能做很多的事情,并不局限于展现静态内容。在本章中,我们将会开发Taco Cloud的第一个主要功能:设计定制taco的能力。在这个过程中,我们将会深入研究Spring MVC并会看到如何展现模型数据和处理表单输入。

1.5 小结

  • Spring旨在简化开发人员所面临的挑战,比如创建Web应用程序、处理数据库、保护应用程序以及实现微服务。
  • Spring Boot构建在Spring之上,通过简化依赖管理、自动配置和运行时洞察,使Spring更加易用。
  • Spring应用程序可以使用Spring Initializr进行初始化。Spring Initializr是基于Web的应用,并且为大多数Java开发环境提供了原生支持。
  • 在Spring应用上下文中,组件(通常称为bean)既可以使用Java或XML显式声明,也可以通过组件扫描发现,还可以使用Spring Boot自动配置功能实现自动化配置。

1.4 俯瞰Spring风景线

要想了解Spring的整体状况,只需查看完整版本的SpringInitializr Web表单上的那一堆复选框列表即可。它列出了100多个可选的依赖项,所以我不会在这里列出所有选项,也不会提供截图,但我鼓励你去看一看。同时,在这里我会简单介绍一些重点的项目。

1.4.1 Spring核心框架

如你所料,Spring核心框架是Spring领域中一切的基础。它提供了核心容器和依赖注入框架,另外还提供了一些其他重要的特性。

其中有一项是Spring MVC,也就是Spring的Web框架。你已经看到了如何使用Spring MVC来编写控制器类以处理Web请求。但是,你还没看到的是,Spring MVC还能用来创建RESTAPI,以生成非HTML的输出。在第2章中,我们将会更深入地介绍Spring MVC,并在第6章重新学习如何使用它来创建RESTAPI。

Spring核心框架还提供了一些对数据持久化的基础支持,尤其是基于模板的JDBC支持。在第3章中,你将会看到如何使用JdbcTemplate。

在最新版本的Spring中,还添加了对反应式(reactive)风格编程的支持,其中包括名为Spring WebFlux的新反应式Web框架,这个框架大量借鉴了Spring MVC。在第3部分中,我们将会学习Spring反应式编程模型,并在第11章专门学习SpringWebFlux。

1.4.2 Spring Boot

我们已经看到了Spring Boot带来的很多收益,包括starter依赖和自动配置。在本书中,我们会尽可能多地使用Spring Boot,并避免任何形式的显式配置,除非显式配置是绝对必要的。除了starter依赖和自动配置,Spring Boot还提供了大量其他有用的特性:

  • Actuator能够洞察应用运行时的内部工作状况,包括指标、线程dump信息、应用的健康状况以及应用可用的环境属性;
  • 灵活的环境属性规范;
  • 在核心框架的测试辅助功能之上提供了对测试的额外支持。

除此之外,Spring Boot还提供了一个基于Groovy脚本的编程模型,称为Spring Boot命令行接口(Command-LineInterface,CLI)。使用Spring Boot CLI,我们可以将整个应用程序编写为Groovy脚本的集合,并通过命令行运行它们。我们不会花太多时间介绍Spring Boot CLI,但是当它匹配我们的需求时,我们会偶尔提及它。

Spring Boot已经成为Spring开发中不可或缺的一部分,很难想象如果没有它我该如何开发Spring应用程序。因此,本书采用以Spring Boot为核心的视角。当我介绍Spring Boot所做的事情时,你可能会发现我却使用了Spring这个词。

1.4.3 Spring Data

尽管Spring核心框架提供了基本的数据持久化支持,但是Spring Data提供了非常令人惊叹的功能:将应用程序的数据repository定义为简单的Java接口,在定义驱动存储和检索数据的方法时使用一种命名约定即可。

此外,Spring Data能够处理多种不同类型的数据库,包括关系型数据库(JPA)、文档数据库(Mongo)、图数据库(Neo4j)等。在第3章中,我们将使用Spring Data为TacoCloud应用程序创建repository。

1.4.4 Spring Security

应用程序的安全性一直是一个重要的话题,而且正在变得越来越重要。幸运的是,Spring有一个健壮的安全框架,名为Spring Security。

Spring Security解决了应用程序通用的安全性需求,包括身份验证、授权和API安全性。Spring Security的范围太大,在本书中无法得到充分的介绍,但是我们将在第4章和第11章中讨论一些常见的使用场景。

1.4.5 Spring Integration和Spring Batch

从一定程度上来讲,大多数应用程序都需要与其他应用甚至本应用中的其他组件进行集成。在这方面,有一些应用程序集成模式可以解决这些需求。Spring Integration和Spring Batch为基于Spring的应用程序提供了这些模式的实现。

Spring Integration解决了实时集成问题。在实时集成中,数据在可用时马上就会得到处理。相反,Spring Batch解决的则是批处理集成的问题,在此过程中,数据可以收集一段时间,直到某个触发器(可能是一个时间触发器)发出信号,表示该处理批量数据了才会对数据进行批处理。我们将会在第9章中研究Spring Batch和Spring Integration。

1.4.6 Spring Cloud

在撰写本书的时候,应用程序开发领域正在进入一个新的时代,我们不再将应用程序作为单个部署单元来开发,而是使用由微服务组成的多个独立部署单元来组合形成应用程序。

微服务是一个热门话题,解决了开发期和运行期的一些实际问题。然而,在这样做的过程中,它们也面临着自己所带来的挑战。这些挑战将由Spring Cloud直面解决,Spring Cloud是使用Spring开发云原生应用程序的一组项目。

Spring Cloud覆盖了很多领域,本书不可能面面俱到,我们将在第13~15章中研究Spring Cloud的一些常见组件。要更全面地研究Spring Cloud,我建议阅读John Carnell的SpringMicroservices in Action一书^1(Manning,2017)。

1.3 编写Spring应用

因为是刚刚开始,所以我们首先为Taco Cloud做一些小的变更,但是这些变更会展现Spring的很多优点。在刚开始的时候,比较合适的做法是为Taco Cloud应用添加一个主页。在添加主页时,我们将会创建两个代码构件:

  • 一个控制器类,用来处理主页相关的请求;
  • 一个视图模板,用来定义主页看起来是什么样子。

测试是非常重要的,所以我们还会编写一个简单的测试类来测试主页。但是,要事优先,我们需要先编写控制器。

1.3.1 处理Web请求

Spring自带了一个强大的Web框架,名为Spring MVC。Spring MVC的核心是控制器(controller)的理念。控制器是处理请求并以某种方式进行信息响应的类。在面向浏览器的应用中,控制器会填充可选的数据模型并将请求传递给一个视图,以便于生成返回给浏览器的HTML。

在第2章中,我们将会学习更多关于Spring MVC的知识。现在,我们会编写一个简单的控制器类以处理对根路径(比如,“/”)的请求,并将这些请求转发至主页视图,在这个过程中不会填充任何的模型数据。程序清单1.4展示了这个简单的控制器类。

程序清单1.4 主页控制器

1
2
3
4
5
6
7
8
9
10
package tacos;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller //⇽--- 控制器
public class HomeController {
@GetMapping("/") //⇽--- 处理对根路径“/”的请求
public String home() {
return "home"; //⇽--- 返回视图名
}
}

可以看到,这个类带有@Controller。就其本身而言,@Controller并没有做太多的事情。它的主要目的是让组件扫描将这个类识别为一个组件。因为HomeController带有@Controller,所以Spring的组件扫描功能会自动发现它,并创建一个HomeController实例作为Spring应用上下文中的bean。

实际上,有一些其他的注解与@Controller有着类似的目的(包括@Component、@Service和@Repository)。你可以为HomeController添加上述的任意其他注解,其作用是完全相同的。但是,在这里选择使用@Controller更能描述这个组件在应用中的角色。

home()是一个简单的控制器方法。它带有@GetMapping注解,表明如果针对“/”发送HTTP GET请求,那么这个方法将会处理请求。该方法所做的只是返回String类型的home值。

这个值将会被解析为视图的逻辑名。视图如何实现取决于多个因素,但是因为Thymeleaf位于类路径中,所以我们可以使用Thymeleaf来定义模板。

为何使用Thymeleaf

你可能会想为什么要选择Thymeleaf作为模板引擎呢?为何不使用JSP?为何不使用FreeMarker?为何不选择其他的几个可选方案?

简单来说,我必须要做出选择,我喜欢Thymeleaf,相对于其他的方案,我会优先使用它。即便JSP是更加显而易见的选择,但是组合使用JSP和Spring Boot需要克服一些挑战。我不想脱离第1章的内容定位,所以在这里就此打住。在第2章中,我们将会看一下其他的模板方案,其中也包括JSP。

模板名称是由逻辑视图名派生而来的,再加上“/templates/”前缀和“.html”后缀。最终形成的模板路径将是“/templates/home.html”。所以,我们需要将模板放到项目的“/src/main/resources/templates/home.html”目录中。现在,就让我们来创建这个模板。

1.3.2 定义视图

为了让主页尽可能简单,除了欢迎用户访问站点之外,它不会做其他的任何事情。程序清单1.5展现了基本的Thymeleaf模板,它定义了Taco Cloud的主页。

程序清单1.5 Taco Cloud主页模板

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Taco Cloud</title>
</head>
<body>
<h1>Welcome to...</h1>
<img th:src="@{/images/TacoCloud.png}"/>
</body>
</html>

这个模板并没有太多需要讨论的。唯一需要注意的一行代码是用于展现Taco Cloud Logo的<img>标签。它使用了Thymeleaf的th:src属性和@{...}表达式,以便于引用相对于上下文路径的图片。除此之外,它就是一个Hello World页面。

但是,我们再讨论一下这个图片。我将定义Taco Cloud Logo的工作留给你,你需要将它放到应用的正确位置中。

图片是使用相对于上下文的“/images/TacoCloud.png”路径来进行引用的。回忆一下我们的项目结构,像图片这样的静态资源是放到“/src/main/resources/static”文件夹中的。这意味着,在项目中,Taco Cloud Logo图片必须要位于“/src/main/resources/static/ images/TacoCloud.png”。

我们已经有了一个处理主页请求的控制器并且有了渲染主页的模板,现在基本就可以启动应用来看一下它的效果了。在此之前,我们先看一下如何为控制器编写测试。

1.3.3 测试控制器

在测试Web应用时,对HTML页面的内容进行断言是比较困难的。幸好Spring对测试提供了强大的支持,这使得测试Web应用变得非常简单。

对于主页来说,我们所编写的测试在复杂性上与主页本身差不多。测试需要针对根路径“/”发送一个HTTP GET请求并期望得到成功结果,其中视图名称为home并且结果内容包含“Welcome to…”。程序清单1.6能够完成该任务。

程序清单1.6 针对主页控制器的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package tacos;
import static org.hamcrest.Matchers.containsString;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class) //⇽--- 针对HomeController的Web测试
public class HomeControllerTest {
@Autowired
private MockMvc mockMvc; //⇽--- 注入MockMvc
@Test
public void testHomePage() throws Exception {
mockMvc.perform(get("/")) //⇽--- 发起对“/”的GET
.andExpect(status().isOk()) //⇽--- 期望得到HTTP 200
.andExpect(view().name("home")) //⇽--- 期望得到home视图
.andExpect(content().string( //⇽--- 期望包含“Welcome to...”
containsString("Welcome to...")));
}

对于这个测试,我们首先注意到的可能就是它使用了与TacoCloudApplicationTests类不同的注解。HomeControllerTest没有使用@SpringBootTest标记,而是添加了@WebMvcTest注解。这是Spring Boot所提供的一个特殊测试注解,它会让这个测试在Spring MVC应用的上下文中执行。更具体来讲,在本例中,它会将HomeController注册到Spring MVC中,这样的话,我们就可以向它发送请求了。

@WebMvcTest同样会为测试Spring MVC应用提供Spring环境的支持。尽管我们可以启动一个服务器来进行测试,但是对于我们的场景来说,仿造一下Spring MVC的运行机制就可以。测试类被注入了一个MockMvc,能够让测试实现mockup。

通过testHomePage()方法,我们定义了针对主页想要执行的测试。它首先使用MockMvc对象对“/”(根路径)发起HTTPGET请求。对于这个请求,我们设置了如下的预期:

  • 响应应该具备HTTP 200 (OK)状态;
  • 视图的逻辑名称应该是home;
  • 渲染后的视图应该包含文本“Welcome to…”。

如果在MockMvc对象发送请求之后,这些期望有不满足的话,那么这个测试会失败。但是,我们的控制器和模板引擎在编写时都满足了这些预期,所以测试应该能够通过,并且带有成功的图标——至少能够看到一些绿色的背景,表明测试通过了。

控制器已经编写好了,视图模板也已经创建完毕,而且我们还通过了测试,看上去我们已经成功实现了主页。尽管测试已经通过了,但是如果能够在浏览器中看到结果那会更有成就感,毕竟这才是Taco Cloud的客户所能看到的效果。接下来,我们构建应用并运行它。

1.3.4 构建和运行应用

就像初始化Spring应用有多种方式一样,运行Spring应用也有多种方式。如果你愿意的话,可以翻到附录部分,以了解运行Spring Boot应用的一些通用方式。

因为我们选择了使用Spring Tool Suite来初始化和处理项目,所以可以借助名为Spring Boot Dashboard的便捷功能来帮助我们在IDE中运行应用。Spring Boot Dashboard的表现形式是一个Tab标签,通常会位于IDE窗口的左下角附近。图1.7展现了一个带有标注的Spring Boot Dashboard截屏。

epub_29101559_14

图1.7 Spring Boot Dashboard的重点功能

图1.7包含了一些最有用的细节,但是我不想花太多时间介绍Spring Boot Dashboard支持的所有功能。对我们来说,现在最重要的事情是需要知道如何使用它来运行TacoCloud应用。确保taco-cloud应用程序在项目列表中能够显示出来(这是图1.7中显示的唯一应用),然后点击启动按钮(上方工具栏最左边的按钮,也就是带有绿色三角形和红色正方形的按钮),应用程序应该就能立即启动。

在应用启动的过程中,你会在控制台看到一些Spring ASCII码,随后会是描述应用启动各个步骤的日志条目。在控制台输出的最后,你将会看到一条Tomcat已经在port(s): 8080 (http)启动的日志,这意味着此时你可以打开Web浏览器并导航至主页,这样就能看到我们的劳动成果了。

稍等一下!刚才说启动Tomcat?但是我们是什么时候将应用部署到Tomcat的呢?

Spring Boot应用的习惯做法是将所有它需要的东西都放到一起,没有必要将其部署到某种应用服务器中。在这个过程中,我们根本没有将应用部署到Tomcat中……Tomcat是我们应用的一部分!(在1.3.6小节,我会介绍Tomcat是如何成为我们应用的一部分的。)

现在,应用已经启动起来了,打开Web浏览器并访问http://localhost:8080(或者在Spring Boot Dashboard中点击上方的地球样式的按钮,如图1.7所示),你将会看到如图1.8所示的界面。如果你设计了自己的Logo图片,那么显示效果可能会有所不同。但是,与图1.8相比,应该不会有太大的差异。

epub_29101559_15

图1.8 Taco Cloud主页

看上去似乎并不太美观,但这不是一本关于平面设计的书。目前,略显简陋的主页外观已经足够了,它为我们学习Spring打下了一个良好的开端。

到现在为止,我一直没有提及DevTools。在初始化项目的时候,我们将其作为一个依赖添加了进来。在最终生成的pom.xml文件中,它表现为一个依赖项。甚至Spring BootDashboard都显示项目启用了DevTools。那么,DevTools是什么,它又能为我们做些什么呢?接下来,让我们快速浏览一下DevTools最有用的一些特性。

1.3.5 了解Spring Boot DevTools

顾名思义,DevTools为Spring开发人员提供了一些便利的开发期工具,其中包括:

  • 代码变更后应用会自动重启;
  • 当面向浏览器的资源(如模板、JavaScript、样式表)等发生变化时,会自动刷新浏览器;
  • 自动禁用模板缓存;
  • 如果使用H2数据库的话,内置了H2控制台。

需要注意,DevTools并不是IDE插件,它也不需要你使用特定的IDE。在Spring Tool Suite、IntelliJ IDEA和NetBeans中,它都能很好地运行。另外,因为它的目的是仅仅用于开发,所以能够很智能地在生产环境中把自己禁用掉。(我们将会在第19章学习应用部署的时候再讨论它是如何做到这一点的。)现在,我们主要关注Spring Boot DevTools最有用的特性,先从应用的自动重启开始。

应用自动重启

如果将DevTools作为项目的一部分,那么你可以看到,当对项目中的Java代码和属性文件做出修改后,这些变更稍后就能发挥作用。DevTools会监控变更,当它看到有变化的时候,将会自动重启应用。

更准确地说,当DevTools运行的时候,应用程序会被加载到Java虚拟机(Java virtual Machine,JVM)两个独立的类加载器中。其中一个类加载器会加载你的Java代码、属性文件以及项目中“src/main/”路径下几乎所有的内容。这些条目很可能会经常发生变化。另外一个类加载器会加载依赖的库,这些库不太可能经常发生变化。

当探测到变更的时候,DevTools只会重新加载包含项目代码的类加载器,并重启Spring的应用上下文,在这个过程中另外一个类加载器和JVM会原封不动。这个策略非常精细,但是它能减少应用启动的时间。

这种策略的一个不足之处就是自动重启无法反映依赖项的变化。这是因为包含依赖库的类加载器不会自动重新加载。这意味着每当我们在构建规范中添加、变更或移除依赖的时候,为了让变更生效,我们需要重新启动应用。

浏览器自动刷新和禁用模板缓存

默认情况下,像Thymeleaf和FreeMarker这样的模板方案在配置时会缓存模板解析的结果。这样的话,在为每个请求提供服务的时候,模板就不用重新解析了。在生产环境中,这是一种很好的方式,因为它会带来一定的性能收益。

但是,在开发期,缓存模板就不太好了。在应用运行的时候,如果缓存模板,那么我们刷新浏览器就无法看到模板变更的效果了。即便我们对模板做了修改,在应用重启之前,缓存的模板依然会有效。

DevTools通过禁用所有模板缓存解决了这个问题。你可以对模板进行任意数量的修改,只需要刷新一下浏览器就能看到结果。

如果你像我这样,连浏览器的刷新按钮都懒得点,那么对代码做出变更之后,马上在浏览器中看到结果就好了。幸运的是,DevTools有一些特殊的功能可以供我们使用。

DevTools在运行的时候,它会和你的应用程序一起,同时自动启动一个LiveReload服务器。LiveReload服务器本身并没有太大的用处。但是,当它与LiveReload浏览器插件结合起来的时候,就能够在模板、图片、样式表、JavaScript等(实际上,几乎涵盖为浏览器提供服务的所有内容)发生变化的时候自动刷新浏览器。

LiveReload有针对Google Chrome、Safari和Firefox的浏览器插件(要对Internet Explorer和Edge粉丝说声抱歉)。请访问LiveReload官网,以了解如何为你的浏览器安装LiveReload。

内置的H2控制台

虽然我们的项目还没有使用数据库,但是这种情况在第3章中就会发生变化。如果你使用H2数据库进行开发,DevTools将会自动启用H2。这样的话,我们可以通过Web浏览器进行访问。你只需要让浏览器访问http://localhost:8080/h2-console,就能看到应用所使用的数据。

此时,我们已经编写了一个尽管非常简单却很完整的Spring应用。在本书中,我们将会不断扩展它。现在,我们要回过头来看一下都完成了哪些工作以及Spring发挥了什么作用。

1.3.6 回顾一下

回想一下我们是怎样完成这一切的。简短来说,在构建基于Spring的Taco Cloud应用的过程中,我们执行了如下步骤:

  • 使用Spring Initializr创建初始的项目结构;
  • 编写控制器类处理针对主页的请求;
  • 定义了一个视图模板来渲染主页;
  • 编写了一个简单的测试类来验证工作符合预期。

这些步骤都非常简单直接,对吧?除了初始化应用的第一个步骤之外,我们所做的每一个操作都专注于生成主页的目标。

实际上,我们所编写的每行代码都致力于实现这个目标。除了Java import语句之外,我只能在控制器中找到两行Spring相关的代码,而在视图模板中一行Spring相关的代码都没有。尽管测试类的大部分内容都使用了Spring对测试的支持,但是它在测试的上下文中似乎没有那么具有侵入性。

这是使用Spring进行开发的一个重要收益。你可以只关注满足应用需求的代码,无须考虑如何满足框架的需求。尽管我们偶尔还是需要编写一些框架特定的代码,但是它们通常只占整个代码库很小的一部分。正如我在前文所述,Spring(以及Spring Boot)可以视为感受不到框架的框架(frameworklessframework)。

但是这又是如何运行起来的呢?Spring在幕后做了些什么来保证应用的需求能够得到满足呢?要理解Spring到底做了些什么,我们首先来看一下构建规范。

在pom.xml文件中,我们声明了对Web和Thymeleaf starter的依赖。这两项依赖会传递引入大量其他的依赖,包括:

  • Spring的MVC框架;
  • 嵌入式的Tomcat;
  • Thymeleaf和Thymeleaf布局方言;

它还引入了Spring Boot的自动配置库。当应用启动的时候,Spring Boot的自动配置将会探测到这些库,并自动完成如下功能:

  • 在Spring应用上下文中配置bean以启用Spring MVC;
  • 在Spring应用上下文中配置嵌入式的Tomcat服务器;
  • 配置Thymeleaf视图解析器,以便于使用Thymeleaf模板渲染Spring MVC视图。

简而言之,自动配置功能完成了所有的脏活累活,让我们能够集中精力编写实现应用功能的代码。如果你问我对此的观点,那么我认为这是一个很好的安排!

我们的Spring之旅才刚刚开始。Taco Cloud应用程序只涉及Spring所提供功能的一小部分。在开始下一步之前,我们先整体了解一下Spring,看看在我们的路途中都会有哪些地标。

1.2 初始化Spring应用

在本书中,我们将会创建一个名为Taco Cloud的在线应用,它能够订购人类所发明的一种美味,也就是墨西哥煎玉米卷(taco)^1。当然,在这个过程中,为了达成我们的目标,我们将会用到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中创建新项目。

我将这些细节放到了附录中,这样就不用在这里花费很多页的篇幅介绍每种方案了。在本章和本书中,我都会向你展示如何使用我最钟爱的方式创建新项目:在Spring Tool Suite中使用Spring Initializr。

顾名思义,Spring Tool Suite是一个非常棒的Spring开发环境。它同时还提供了便利的Spring Boot Dashboard特性,这个特性是其他IDE都不具备的(至少在我编写本书的时候如此)。

如果你不是Spring Tool Suite用户,那也没有关系,我们依然可以做朋友。你可以跳转到附录中,查看最适合你的Initializr方案,以此来替换后面小节中的内容。但是,在本书中,我偶尔会提到Spring Tool Suite特有的特性,比如Spring BootDashboard。如果你不使用Spring Tool Suite,那么需要调整这些指令以适配你的IDE。

1.2.1 使用Spring Tool Suite初始化Spring项目

要在Spring Tool Suite中初始化一个新的Spring项目,我们首先要点击File菜单,选择New,接下来选择Spring StarterProject。图1.2展现了要查找的菜单结构。

epub_29101559_9

图1.2 在Spring Tool Suite中使用Initializr初始化一个新项目

在选择Spring Starter Project之后,将会出现一个新的向导对话框(见图1.3)。向导的第一页会询问一些项目的通用信息,比如项目名称、描述和其他必要的信息。如果你熟悉Mavenpom.xml文件的内容,就可以识别出大多数的输入域条目最终都会成为Maven的构建规范。对于Taco Cloud应用来说,我们可以按照图1.3的样子来填充对话框。

epub_29101559_10

图1.3 为Taco Cloud应用指定通用的项目信息

向导的下一页会让我们选择要添加到项目中的依赖(见图1.4)。注意,在对话框的顶部,我们可以选择项目要基于哪个Spring Boot版本。它的默认值是最新的可用版本。一般情况下,最好使用这个默认的值,除非你需要使用不同的版本。

至于依赖项本身,你可以打开各个区域并查找所需的依赖项,也可以在Available顶部的搜索框中对依赖进行搜索。对于TacoCloud应用来说,我们最初的依赖项如图1.4所示。

epub_29101559_11

图1.4 选择Starter依赖

现在,你可以点击Finish来生成项目并将其添加到工作空间中。但是,如果你还想多体验一些,那么可以再次点击Next,看一下新Starter项目向导的最后一页,如图1.5所示。

epub_29101559_12

图1.5 指定备用的Initializr地址

默认情况下,新项目的向导会调用Spring Initializr来生成项目。通常情况下,没有必要覆盖默认值,这也是我们可以在向导的第二页直接点击Finish的原因。但是,如果你基于某种原因托管了自己的Initializr克隆版本(可能是本地机器上的副本或者公司防火墙内部运行的自定义克隆版本),那么你可能需要在点击Finish之前修改Base Url输入域,使其指向自己的Initializr实例。

在点击Finish之后,项目会从Initializr下载并加载到工作空间中。此时,要等待它加载和构建,然后你就可以开始开发应用功能了。下面我们看一下Initializr都为我们提供了什么。

1.2.2 检查Spring项目的结构

项目加载到IDE中之后,我们将其展开,看一下其中都包含什么内容。图1.6展现了Spring Tool Suite中已展开的Taco Cloud项目。

epub_29101559_13

图1.6 Spring Tool Suite中所展现的初始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文件已经包含了我们所选择的依赖。程序清单1.1展示了Initializr为我们提供的完整pom.xml。

程序清单1.1 初始的Maven构建规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?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> ⇽--- 打包为JAR
<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> ⇽--- Spring Boot的版本
<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> ⇽--- Starter依赖
<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> ⇽--- Spring Boot插件
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

在pom.xml文件中,我们第一个需要注意的地方就是<packaging>。我们选择了将应用构建成一个可执行的JAR文件,而不是WAR文件。这可能是你所做出的最奇怪的选择之一,对Web应用来说尤为如此。毕竟,传统的Java Web应用都是打包成WAR文件,JAR只是用来打包库和较为少见的桌面UI应用的。

打包为JAR文件是基于云思维做出的选择。尽管WAR文件非常适合部署到传统的Java应用服务器上,但对于大多数云平台来说它们并不是理想的选择。有些云平台(比如CloudFoundry)也能够部署和运行WAR文件,但是所有的Java云平台都能够运行可执行的JAR文件。因此,Spring Initializr默认会使用基于JAR的打包方式,除非我们明确告诉它采用其他的方式。

如果你想要将应用部署到传统的Java应用服务器上,那么需要选择使用基于WAR的打包方式并要包含一个Web初始化类。在第2章中,我们将会更详细地了解如何构建WAR文件。

接下来,请留意<parent>元素,更具体来说是它的<version>子元素。这表明我们的项目要以spring-boot-starter-parent作为其父POM。除了其他的一些功能之外,这个父POM为Spring项目常用的一些库提供了依赖管理,现在你不需要指定它们的版本,因为这是通过父POM来管理的。这里的2.0.4.RELEASE表明要使用Spring Boot 2.0.4,所以会根据这个版本的Spring Boot定义来继承依赖管理。

既然我们谈到了依赖的话题,那么需要注意在<dependencies>元素下声明了3个依赖。在某种程度上,你可能会对前两个更熟悉一些。它们直接对应我们在Spring ToolSuite新项目向导中点击Finish之前所选择的Web和Thymeleaf依赖。第三个依赖提供了很多有用的测试功能。我们没有必要在专门的复选框中选择它,因为Spring Initializr假定你将会编写测试(希望你会正确地开展这项工作)。

你可能也会注意到这3个依赖的artifact ID上都有starter这个单词。Spring Boot starter依赖的特别之处在于它们本身并不包含库代码,而是传递性地拉取其他的库。这种starter依赖主要有3个好处。

  • 构建文件会显著减小并且更易于管理,因为这样不必为每个所需的依赖库都声明依赖。
  • 我们能够根据它们所提供的功能来思考依赖,而不是根据库的名称。如果是开发Web应用,那么你只需要添加webstarter就可以了,而不必添加一堆单独的库再编写Web应用。
  • 我们不必再担心库版本的问题。你可以直接相信给定版本的Spring Boot,传递性引入的库的版本是兼容的。现在,你只需要关心使用的是哪个版本的Spring Boot就可以了。

最后,构建规范还包含一个Spring Boot插件。这个插件提供了一些重要的功能。

  • 它提供了一个Maven goal,允许我们使用Maven来运行应用。在1.3.4小节,我们将会尝试这个goal。
  • 它会确保依赖的所有库都会包含在可执行JAR文件中,并且能够保证它们在运行时类路径下是可用的。
  • 它会在JAR中生成一个manifest文件,将引导类(在我们的场景中,也就是TacoCloudApplication)声明为可执行JAR的主类。

谈到了主类,我们打开它看一下。

引导应用

因为我们将会通过可执行JAR文件的形式来运行应用,所以很重要的一点就是要有一个主类,它将会在JAR运行的时候被执行。我们同时还需要一个最小化的Spring配置,以引导该应用。这就是TacoCloudApplication类所做的事情,如程序清单1.2所示。

程序清单1.2 Taco Cloud的引导类

1
2
3
4
5
6
7
8
9
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为我们提供了一个测试类作为起步。程序清单1.3展现了这个测试类的概况。

程序清单1.3 应用测试类的概况

1
2
3
4
5
6
7
8
9
10
11
12
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应用上下文无法创建,那么这个测试将会失败,你就可以做出反应来解决相关的问题了。

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、准备好键盘并向TacoCloud应用程序添加一些自定义的代码了。

第1章 Spring起步


本章内容:

  • Spring和Spring Boot的必备知识
  • 初始化Spring项目
  • Spring生态系统概览

尽管希腊哲学家赫拉克利特(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进行现代应用开发的第一步。

资源与支持

本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

配套资源
本书提供如下资源:

  • 本书源代码;
  • 书中彩图文件。

要获得以上配套资源,请在异步社区本书页面中点击image-20211003192418824,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

提交勘误

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

image-20211003192607753

与我们联系

我们的联系邮箱是contact@epubit.com.cn

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

关于异步社区和异步图书

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

image-20211003192809100

异步社区

image-20211003192853222

微信服务号

前言

在使用了Spring 15年并编写了这本书的5个版本(暂时不算《Spring Boot实战》了)之后,你可能会认为,在为这本书撰写前言时,我很难想出一些关于Spring令人兴奋的新内容,但事实远非如此!

在Spring生态系统中,Spring、Spring Boot和所有其他项目的每个版本都发布了令人兴奋的新功能,重新点燃了开发应用程序的乐趣。Spring 5.0和Spring Boot 2.0的发布达到了一个重要的里程碑。Spring有了更多的乐趣,所以编写新版《Spring实战》是很容易的。

Spring 5的主要功能是对反应式编程的支持,包括SpringWebFlux。这是一个全新的反应式Web框架,借鉴了SpringMVC的编程模型,允许开发人员创建伸缩性更好且耗用更少线程的Web应用程序。至于Spring应用的后端,最新版本的Spring Data支持创建反应式、非阻塞的数据repository。所有这些都构建在Reactor项目之上,Reactor是一个用于处理反应式类型的Java库。

除了Spring 5新的反应式编程特性之外,Spring Boot 2提供了比以前更多的自动配置支持,以及一个完全重新设计的Actuator,用于探查和操作正在运行的应用。

更重要的是,当开发人员希望将单体应用拆分为分散的微服务时,Spring Cloud提供了一些工具,使配置和发现微服务变得容易,并增强了微服务的功能,使它们更能抵御失败。

我很高兴地说,《Spring实战(第5版)》涵盖了所有的这些功能,甚至更多!如果你是经验丰富的老手,《Spring实战(第5版)》可以作为指南,指导你去学习Spring提供的新功能;如果你是Spring新手,那么现在是行动起来的最佳时机,本书的前几章会让你快速上手!

与Spring合作的15年是令人兴奋的。现在我已经写了5个版本的《Spring实战》,我很想和你们分享这份兴奋!

致谢

Spring和Spring Boot所做的最令人惊奇的事情之一就是自动为应用程序提供所有的基础功能,让开发人员专注于应用程序特有的逻辑。不幸的是,对于写书这件事来说,并没有这样的魔法。是这样的吗?

在Manning,有很多人在施展“魔法”,确保这本书是最好的。特别要感谢我的项目编辑Jenny Stout以及制作团队,其中包括项目主管Janet Vail、文字编辑Andy Carroll和FrancesBuran,以及校对Katie Tennant和Melody Dolab。同时也要感谢技术校对Joshua White,他的工作很全面,很有帮助。

在此过程中,我们得到了几位同行评论的反馈,他们确保了这本书没有偏离目标,涵盖了正确的内容。为此,我要感谢Andrea Barisone、Arnaldo Ayala、Bill Fly、Colin Joyce、Daniel Vaughan、David Witherspoon、Eddu Melendez、Iain Campbell、Jettro Coenradie、John Gunvaldson、Markus Matzker、Nick Rakochy、Nusry Firdousi、PiotrKafel、Raphael Villela、Riccardo Noviello、SergioFernandez Gonzalez、Sergiy Pylypets、Thiago Presa、Thorsten Weber、Waldemar Modzelewski、Yagiz Erkan和Zeljko Trogrlić。

和往常一样,如果不是Spring工程团队成员所做的出色工作,写这本书是没有任何意义的。我惊叹于你们所创造的成果,并期待未来继续改变软件开发的方式。

非常感谢我的同行们在No Fluff/Just Stuff巡回演讲上的发言。我从你们每个人身上学到很多。我特别要感谢Brian Sletten、Nate Schutta和Ken Kousen关于Spring的对话和邮件,这些内容塑造了这本书。

我要再次感谢Phoenicians,你们太棒了[^1]。

最后,我要感谢我美丽的妻子Raymie。她是我生命中的挚爱,是我最甜蜜的梦想,也是我的灵感来源:谢谢你的鼓励,也谢谢你为这本新书做的努力。致我可爱的女儿Maisy和Madi:我为你们感到骄傲,为你们即将成为了不起的年轻女士感到骄傲。我对你们的爱超出了你们的想象,也超出了我语言所能表达的程度。

[^1]:Phoenicians指的是远古时代的腓尼基人,他们被认为是字母系统的创建者,基于字母的所有现代语言都是由此衍生而来的。在迪士尼世界的Epcot,有名为Spaceship Earth的时光穿梭体验,我们可以了解到人类交流的历史,甚至能够回到腓尼基人的时代,在这段旅程的旁白中这样说道:如果你觉得学习字母语言很容易,就感谢腓尼基人吧,是他们发明了它。这是作者的一种幽默说法。——译者注