2.1 展现信息

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]: 样式表的内容与我们的讨论无关,它只是包含了让配料两列显示的样式,避免出现一个很长的配料列表。