2.1 展示信息

2.1 展示信息

从根本上说,Taco Cloud 是一个可以在线订购玉米饼的地方。但除此之外,Taco Cloud 还希望让顾客能够表达自己的创意,从丰富的配料中设计定制的玉米饼。

因此,Taco Cloud web 应用程序需要一个页面来显示玉米饼制作艺术家可以从中选择的配料。选择的原料可能随时改变,所以不应该硬编码到 HTML 页面中。相反,应该从数据库中获取可用配料的列表,并将其提交给页面以显示给客户。

在 Spring web 应用程序中,获取和处理数据是控制器的工作。视图的工作是将数据渲染成 HTML 并显示在浏览器中。将创建以下组件来支持 Taco 创建页面:

  • 一个定义玉米卷成分特性的领域类
  • 一个 Spring MVC 控制器类,它获取成分信息并将其传递给视图
  • 一个视图模板,在用户的浏览器中呈现一个成分列表

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

![图 2.1 典型 Spring MVC 请求流程](F:\workspace\spring-in-action-v5-translate\第一部分 Spring 基础\第二章 开发 Web 应用程序\图 2.1 典型 Spring MVC 请求流程.jpg)

图 2.1 典型 Spring MVC 请求流程

由于本章主要讨论 Spring 的 web 框架,所以我们将把数据库的内容推迟到第 3 章。现在,控制器将单独负责向视图提供组件。在第 3 章中,将重写控制器,使其与从数据库中获取配料数据的存储库进行协作。

在编写控制器和视图之前,让我们先确定表示配料的域类型。这将为开发 web 组件奠定基础。

2.1.1 建立域

应用程序的域是它所处理的主题领域 —— 影响应用程序理解的思想和概念。在 Taco Cloud 应用程序中,领域包括 Taco 设计、组成这些设计的成分、客户和客户下的 Taco 订单等对象。首先,我们将关注玉米饼配料。

在领域中,玉米饼配料是相当简单的对象。每一种都有一个名称和一个类型,这样就可以在视觉上对其进行分类(蛋白质、奶酪、酱汁等)。每一个都有一个 ID,通过这个 ID 可以轻松、明确地引用它。下面的成分类定义了需要的域对象。程序清单 2.1 定义玉米饼的配料。

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 域类,定义了描述一个成分所需的三个属性。对于程序清单 2.1 中定义的 Ingredient 类,最不寻常的事情可能是它似乎缺少一组常用的 getter 和 setter 方法,更不用说像 equals()hashCode()toString() 等有用的方法。

在清单中看不到它们,部分原因是为了节省空间,但也因为使用了一个名为 Lombok 的出色库,它会在运行时自动生成这些方法。实际上,类级别的 @Data 注释是由 Lombok 提供的,它告诉 Lombok 生成所有缺少的方法,以及接受所有最终属性作为参数的构造函数。通过使用 Lombok,可以让 Ingredient 的代码保持整洁。

Lombok 不是一个 Spring 库,但是它非常有用,没有它我很难开发。当我需要在一本书中保持代码示例简短明了时,它就成了我的救星。

要使用 Lombok,需要将其作为依赖项添加到项目中。如果正在使用 Spring Tool Suite,只需右键单击 pom.xml 文件并从 Spring 上下文菜单选项中选择 Edit Starters 即可。在第 1 章(图 1.4)中给出的依赖项的相同选择将出现,这样就有机会添加或更改所选的依赖项。找到 Lombok 选项,确保选中,然后单击 OK;Spring Tool Suite 将自动将其添加到构建规范中。

或者,可以使用 pom.xml 中的以下条目手动添加它:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

此依赖项将在开发时提供 Lombok 注释(如 @Data),并在运行时提供自动方法生成。但是还需要在 IDE 中添加 Lombok 作为扩展,否则 IDE 将会报错缺少方法和没有设置的最终属性。请访问 https://projectlombok.org/,以了解如何在 IDE 中安装 Lombok。

你会发现 Lombok 非常有用,但它是可选的。如果不希望使用它,或是不需要它来开发 Spring 应用程序,那么请随意手动编写那些缺少的方法。继续……我将等待。完成后,将添加一些控制器来处理应用程序中的 web 请求。

2.1.2 创建控制器类

控制器是 Spring MVC 框架的主要参与者。它们的主要工作是处理 HTTP 请求,或者将请求传递给视图以呈现 HTML(浏览器显示),或者直接将数据写入响应体(RESTful)。在本章中,我们将重点讨论使用视图为 web 浏览器生成内容的控制器的类型。在第 6 章中,我们将讨论如何在 REST API 中编写处理请求的控制器。

对于 Taco Cloud 应用程序,需要一个简单的控制器来执行以下操作:

  • 处理请求路径为 /design 的 HTTP GET 请求
  • 构建成分列表
  • 将请求和成分数据提交给视图模板,以 HTML 的形式呈现并发送给请求的 web 浏览器

下面的 DesignTacoController 类处理这些需求。程序清单 2.2 Spring 控制器类的开始。

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";
    }
}

关于 DesignTacoController,首先要注意的是在类级应用的一组注释。第一个是 @Slf4j,它是 Lombok 提供的注释,在运行时将自动生成类中的 SLF4J(Java 的简单日志门面,https://www.slf4j.org/)记录器。这个适当的注释具有与显式地在类中添加以下行相同的效果:

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

稍后您将使用这个 Logger

下一个应用到 DesignTacoController 的注释是 @Controller。此注释用于将该类标识为控制器并将其标记为组件扫描的候选对象,以便 Spring 将发现该类并在 Spring 应用程序上下文中自动创建 DesignTacoController 实例作为 bean。

DesignTacoController 也用 @RequestMapping 注释。@RequestMapping 注释在类级应用时,指定该控制器处理的请求的类型。在本例中,它指定 DesignTacoController 将处理路径以 /design 开头的请求。

处理 GET 请求

类级别的 @RequestMapping 注释用于 showDesignForm() 方法时,可以用 @GetMapping 注释进行改进。@GetMapping 与类级别的 @RequestMapping 配对使用,指定何时接收 /design 的 HTTP GET 请求,showDesignForm() 将用来处理请求。

@GetMapping 是一个相对较新的注释,是在 Spring 4.3 中引入的。在 Spring 4.3 之前,可能使用了一个方法级别的 @RequestMapping 注释:

表 2.1 Spring MVC 请求映射注释

注释 描述
@RequestMapping 通用请求处理
@GetMapping 处理 HTTP GET 请求
@PostMapping 处理 HTTP POST 请求
@PutMapping 处理 HTTP PUT 请求
@DeleteMapping 处理 HTTP DELETE 请求
@PatchMapping 处理 HTTP PATCH 请求

让正确的事情变得简单

在控制器方法上声明请求映射时,尽可能具体总是一个好主意。至少,这意味着声明一个路径(或者从类级 @RequestMapping 继承一个路径)和它将处理哪个 HTTP 方法。

长度更长的 @RequestMapping(method=RequestMethod.GET) 使我们很容易采取惰性的方式,同时去掉方法属性。由于 Spring 4.3 的新映射注释,正确的做法也很容易做到 —— 只需较少的输入。

新的请求映射注释具有与 @RequestMapping 相同的所有属性,因此可以在使用 @RequestMapping 的任何地方使用它们。

通常,我倾向于只在类级别上使用 @RequestMapping 来指定基本路径。我在每个处理程序方法上使用更具体的 @GetMapping@PostMapping 等。

现在已经知道 showDesignForm() 方法将处理请求,让我们来看看方法体,看看它是如何工作的。该方法的大部分构造了一个成份对象列表。这个列表现在是硬编码的。当我们讲到第 3 章的时候,你会从数据库中找到玉米饼的原料列表。

一旦准备好了原料列表,接下来的几行 showDesignForm() 将根据原料类型过滤该列表。然后将成分类型列表作为属性添加到传递到 showDesignForm() 的模型对象。模型是一个对象,它在控制器和负责呈现数据的视图之间传输数据。最后,放置在模型属性中的数据被复制到 servlet 响应属性中,视图可以在其中找到它们。showDesignForm() 方法最后返回 “design”,这是将用于向浏览器呈现模型的视图的逻辑名称。

DesignTacoController 真的开始成形了。如果您现在运行应用程序并将您的浏览器指向 /design 路径,DesignTacoControllershowDesignForm() 将被占用,它从存储库中获取数据并将其放在模型中,然后将请求传递给视图。但是因为还没有定义视图,所以请求会发生可怕的转变,导致 HTTP 404(Not Found)错误。为了解决这个问题,让我们将注意力转移到视图上,其中的数据将用 HTML 进行修饰,并在用户的 web 浏览器中显示。

2.1.3 设计视图

控制器创建完成后,就该开始设计视图了。Spring 为定义视图提供了几个很好的选项,包括 JavaServer Pages(JSP)、Thymeleaf、FreeMarker、Mustache 和基于 Groovy 的模板。现在,我们将使用 Thymeleaf,这是我们在第 1 章开始项目时所做的选择。我们将在 2.5 节中考虑其他一些选项。

为了使用 Thymeleaf,需要在构建项目时添加另一个依赖项。下面的 <dependency> 条目使用了 Spring Boot 的 Thymeleaf starter,使 Thymeleaf 渲染要创建的视图:

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

在运行时,Spring Boot 自动配置将看到 Thymeleaf 位于类路径中,并将自动创建支持 Spring MVC 的 Thymeleaf 视图的 bean。

像 Thymeleaf 这样的视图库被设计成与任何特定的 web 框架解耦。因此,他们不知道 Spring 的模型抽象,并且无法处理控制器放置在模型中的数据。但是它们可以处理 servlet 请求属性。因此,在 Spring 将请求提交给视图之前,它将模型数据复制到请求属性中,而 Thymeleaf 和其他视图模板选项可以随时访问这些属性。

Thymeleaf 模板只是 HTML 与一些额外的元素属性,指导模板在渲染请求数据。例如,如果有一个请求属性,它的键是 “message”,你希望它被 Thymeleaf 渲染成一个 HTML <p> 标签,你可以在你的 Thymeleaf 模板中写以下内容:

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

当模板被呈现为 HTML 时,<p> 元素的主体将被 servlet 请求属性的值替换,其键值为 “message”。th:text 是一个 Thymeleaf 的命名空间属性,用于需要执行替换的地方。${} 操作符告诉它使用请求属性的值(在本例中为 “message”)。

Thymeleaf 还提供了另一个属性 th:each,它遍历元素集合,为集合中的每个项目呈现一次 HTML。当设计视图列出模型中的玉米饼配料时,这将非常方便。例如,要呈现 “wrap” 配料列表,可以使用以下 HTML 片段:

<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 请求属性中的集合中的每一个项目进行重复呈现。在每次迭代中,成分项都绑定到一个名为 ingredient 的 Thymeleaf 变量中。

<div> 元素内部,有一个复选框 <input> 元素和一个 <span> 元素,用于为复选框提供标签。复选框使用 Thymeleaf 的 th:value 元素,它将把 <iuput> 元素的 value 属性呈现为在成分 id 属性中找到的值。<span> 元素使用 th:text 属性把 “INGREDIENT” 占位符替换为成分 name 属性的值。

当使用实际的模型数据呈现时,这个 <div> 循环迭代一次可能是这样的:

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

最后,前面的 Thymeleaf 片段只是一个更大的 HTML 表单的一部分,通过它,玉米饼艺术家用户将提交他们美味的作品。完整的 Thymeleaf 模板(包括所有成分类型和表单)如下所示。程序清单 2.3 完整的 design-a-taco 页面。

<!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> 片段。还包括一个提交按钮和一个字段,用户可以在其中命名他们的创建。

值得注意的是,完整的模板包括 Taco Cloud 图标图片和一个指向样式表的 <link> 引用。在这两种情况下,Thymeleaf 的 @{} 操作符被用来产生一个上下文相关路径的静态工件,它们正在引用。正如在第 1 章中了解到的,Spring 启动应用程序中的静态内容是从类路径根目录的 /static 目录提供的。

现在控制器和视图已经完成,可以启动应用程序了。运行 Spring Boot 应用程序有许多方法。在第 1 章中,展示了如何运行这个应用程序,首先将它构建到一个可执行的 JAR 文件中,然后使用 java -jar 运行这个 JAR。展示了如何使用 mvn spring-boot:run 从构建中直接运行应用程序。

无论如何启动 Taco Cloud 应用程序,一旦启动,使用浏览器访问 http://localhost:8080/design。应该看到类似图 2.2 的页面。

![图 2.2 呈现的玉米饼设计页面](E:\Document\spring-in-action-v5-translate\第一部分 Spring 基础\第二章 开发 Web 应用程序\图 2.2 呈现的玉米饼设计页面.jpg)

图 2.2 呈现的玉米卷设计页面

它看起来真不错!访问这个玉米饼艺术家呈现形式的网站,包含一个调色板的玉米饼成分,从中他们可以创建自己的杰作。但是当他们点击 Submit Your Taco 按钮时会发生什么呢?

DesignTacoController 还没有准备好接受玉米饼创作的请求。如果提交了设计表单,用户将看到一个错误。(具体来说,它将是一个 HTTP 405 错误:请求方法 “POST” 不受支持。)让我们通过编写更多处理表单提交的控制器代码来解决这个问题。

下一页