Spring中的新华字典,你说经典不经典,全球10万+开发者使用他

异步社区官方博客

《Spring实战》第5版本书是一本经典而实用的畅销Spring学习指南。这本书就相当于是关于Spring的新华字典,全球有超过100 000名开发者使用本书来学习Spring,畅销经典Spring技术图书,针对Spring 5全面升级。

第5版涵盖了Spring 5.0和Spring Boot 2.0里程碑式的更新。全书分为5个部分,共19章。第1部分(第1~5章)涵盖了构建Spring应用的基础话题。第2部分(第6~9章)讨论如何将Spring应用与其他应用进行集成。第3部分(第10~12章)探讨Spring对反应式编程提供的全新支持。第4部分(第13~15章)拆分单体应用模型,介绍Spring Cloud和微服务开发。第5部分(第16~19章)讨论如何为应用投入生产环境做准备以及如何进行部署。

Spring 5更新了哪些内容?

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

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

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

针对学习者的建议

如果你是经验丰富的老手,《Spring实战(第5版)》可以作为指南,指导你去学习Spring提供的新功能;如果你是Spring新手,那么现在是行动起来的最佳时机,本书的前几章会让你快速上手!

《Spring实战》(第5版)学习路线图

本书分成了5个部分,共计19章。

第1部分涵盖构建Spring应用的基础话题。

第2部分讨论如何将Spring应用与其他应用进行集成。

第3部分探讨Spring对反应式编程提供的全新支持。

第4部分将会拆分单体应用模型,介绍Spring Cloud和微服务开发。

在第5部分中,我们将会讨论如何做好将应用投入生产环境的准备,并看一下如何进行部署。

通常来讲,刚刚接触Spring的开发人员应该从第1章开始,并按顺序阅读每一章;经验丰富的Spring开发人员可能更愿意在任何感兴趣的时候参与进来。即便如此,每一章都是建立在前一章的基础上的,所以如果从中间开始阅读,那么可能会漏掉一些上下文信息。

样章试读:第2章 开发Web应用

本章内容:

  • 在浏览器中展现模型数据

  • 处理和校验表单输入

  • 选择视图模板库

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

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

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

2.1 展现信息


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

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

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

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

..\19-0887 改图\2-1.tif{90%}

图2.1 典型的Spring MVC请求流

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

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

2.1.1 构建领域类


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

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

程序清单2.1 定义taco配料

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()等方法。

在程序清单中没有这些方法,部分原因是节省空间,另外还因为我们使用了名为Lombok的库(这是一个非常棒的库,它能够在运行时动态生成这些方法)。实际上,类级别的@Data注解就是由Lombok提供的,它会告诉Lombok生成所有缺失的方法,同时还会生成所有以final属性作为参数的构造器。通过使用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),并且会在运行时进行自动化的方法生成。但是,我们还需要将Lombok作为扩展添加到IDE上,否则IDE将会报错,提示缺少方法和final属性没有赋值。参见Lombok项目页面,以查阅如何在你所选择的IDE上安装Lombok。

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

2.1.2 创建控制器类


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

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

程序清单2.2中的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(Simple Logging Facade for Java)Logger。这个简单的注解和在类中通过如下代码显式声明的效果是一样的:

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注解作为替代:

@RequestMapping(method=RequestMethod.GET)

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

表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)注解很容易让开发人员采取懒惰的方式,也就是忽略掉method属性。幸亏有了Spring 4.3的新注解,正确的事情变得更容易了,我们的输入变得更少了。

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

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

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

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

我们的DesignTacoController已经具备雏形了。如果你现在运行应用并在浏览器上访问“/design”路径,DesignTacoController的showDesignForm()将会被调用,它会从repository中获取数据并放到模型中,然后将请求传递给视图。但是,我们现在还没有定义视图,请求将会遇到很糟糕的问题,也就是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的模型抽象,因此无法与控制器放到Model中的数据协同工作。但是,它们可以与Servlet的request属性协作。所以,在Spring将请求转移到视图之前,它会把模型数据复制到request属性中,Thymeleaf和其他的视图模板方案就能访问到它们了。

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

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

当模板渲染成HTML的时候,<p>元素体将会被替换为Servlet request中key为“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 request属性所对应集合中的每个元素重复渲染<div>了。在每次迭代的时候,配料元素都会绑定到一个名为ingredient的Thymeleaf变量上。

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

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

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

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

程序清单2.3 设计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="<i class="math-start"></i>{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的一个页面。

..\19-0887 图\2-2.tif{61%}

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

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

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

2.2 处理表单提交


仔细看一下视图中的<form>标签,你将会发现它的method属性被设置成了POST。除此之外,<form>并没有声明action属性。这意味着当表单提交的时候,浏览器会收集表单中的所有数据,并以HTTP POST请求的形式将其发送至服务器端,发送路径与渲染表单的GET请求路径相同,也就是“/design”。

因此,在该POST请求的接收端,我们需要有一个控制器处理方法。在DesignTacoController中,我们会编写一个新的处理器方法来处理针对“/design”的POST请求。

在程序清单2.2中,我们曾经使用@GetMapping注解声明showDesignForm()方法要处理针对“/design”的HTTP GET请求。与@GetMapping处理GET请求类似,我们可以使用@PostMapping来处理POST请求。为了处理taco设计的表单提交,在DesignTacoController中添加程序清单2.4所述的processDesign()方法。

程序清单2.4 使用@PostMapping来处理POST请求

@PostMapping
public String processDesign(Design design) {
  // Save the taco design...
  // We'll do this in chapter 3
  log.info("Processing design: " + design);

  return "redirect:/orders/current";
}

如processDesign()方法所示,@PostMapping与类级别的@RequestMapping协作,指定processDesign()方法要处理针对“/design”的POST请求。我们所需要的正是以这种方式处理taco艺术家的表单提交。

当表单提交的时候,表单中的输入域会绑定到Taco对象(这个类会在下面的程序清单中进行介绍)的属性中,该对象会以参数的形式传递给processDesign()。从这里开始,processDesign()就可以针对Taco对象采取任意操作了。

程序清单2.5 定义taco设计的领域对象

package tacos;
import java.util.List;
import lombok.Data;
@Data
public class Taco {

  private String name;
  private List<String> ingredients;

}

我们可以看到,Taco是一个非常简单的Java领域对象,其中包含了几项属性。与Ingredient类似,Taco类也添加了@Data注解,会在运行期自动生成必要的JavaBean方法。

回过头来再看一下程序清单 2.3 中的表单,你会发现其中包含多个checkbox元素,它们的名字都是ingredients,另外还有一个名为name的文本输入元素。表单中的这些输入域直接对应Taco类的ingredients和name属性。

表单中的name输入域只需要捕获一个简单的文本值。因此,Taco的name属性是String类型的。配料的复选框也有文本值,但是用户可能会选择一个或多个,所以它们所绑定的ingredients属性是一个List<String>,能够捕获选中的每种配料。

processDesign()方法对Taco对象没有执行任何操作。实际上,这个方法什么都没做。现在,这样是可以的。到第3章,我们将会添加一些持久化的逻辑,将提交的Taco保存到一个数据库中。

与showDesignForm()方法类似,processDesign()最后也返回了一个String类型的值。同样与showDesignForm()相似,返回的这个值代表了一个要展现给用户的视图。但是,区别在于processDesign()返回的值带有“redirect:”前缀,表明这是一个重定向视图。更具体地讲,它表明在processDesign()完成之后,用户的浏览器将会重定向到相对路径“/order/current”。

这里的想法是在创建完taco后,用户将会被重定向到一个订单表单页面,在这里用户可以创建一个订单,将他们所创建的taco快递过去。但是,我们现在还没有处理“/orders/current”请求的控制器。

根据已经学到的关于@Controller@RequestMapping@GetMapping的知识,我们可以很容易地创建这样的控制器。它应该如程序清单2.6所示。

程序清单2.6 展现taco订单表单的控制器

package tacos.web;
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.RequestMapping;
import lombok.extern.slf4j.Slf4j;
import tacos.Order;
@Slf4j
@Controller
@RequestMapping("/orders")
public class OrderController {

  @GetMapping("/current")
  public String orderForm(Model model) {
    model.addAttribute("order", new Order());
    return "orderForm";
  }

}

在这里,我们再次使用Lombok @Slf4j注解在运行期创建一个SLF4J Logger对象。稍后,我们将会使用这个Logger记录所提交订单的详细信息。

类级别的@RequestMapping指明这个控制器的请求处理方法都会处理路径以“/orders”开头的请求。当与方法级别的@GetMapping注解结合之后,它就能够指定orderForm()方法,会处理针对“/orders/current”的HTTP GET请求。

orderForm()方法本身非常简单,只是返回了一个名为orderForm的逻辑视图名。在第3章学习完如何将所创建的taco保存到数据库之后,我们将会重新回到这个方法并对其进行修改,用一个Taco对象的列表来填充模型并将其放到订单中。

orderForm视图是由名为orderForm.html的Thymeleaf模板来提供的,如程序清单2.7所示。

程序清单2.7 一个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>

    <form method="POST" th:action="@{/orders}" th:object="${order}">
      <h1>Order your taco creations!</h1>

      <img th:src="@{/images/TacoCloud.png}"/>
      <a th:href="@{/design}" id="another">Design another taco</a><br/>

      <div th:if="${#fields.hasErrors()}">
        <span class="validationError">
        Please correct the problems below and resubmit.
        </span>
      </div>
      <h3>Deliver my taco masterpieces to...</h3>
      <label for="name">Name: </label>
      <input type="text" th:field="*{name}"/>
      <br/>

      <label for="street">Street address: </label>
      <input type="text" th:field="*{street}"/>
      <br/>

      <label for="city">City: </label>
      <input type="text" th:field="*{city}"/>
      <br/>

      <label for="state">State: </label>
      <input type="text" th:field="*{state}"/>
      <br/>

      <label for="zip">Zip code: </label>
      <input type="text" th:field="*{zip}"/>
      <br/>

      <h3>Here's how I'll pay...</h3>
      <label for="ccNumber">Credit Card #: </label>
      <input type="text" th:field="*{ccNumber}"/>
      <br/>

      <label for="ccExpiration">Expiration: </label>
      <input type="text" th:field="*{ccExpiration}"/>
      <br/>

      <label for="ccCVV">CVV: </label>
      <input type="text" th:field="*{ccCVV}"/>
      <br/>

      <input type="submit" value="Submit order"/>
    </form>

  </body>
</html>

从很大程度上来讲,orderForm.html就是典型的HTML/Thymeleaf内容,不需要过多关注。但是,需要注意一点,这里的<form>标签和程序清单2.3中的<form>标签有所不同,它指定了一个表单的action。如果不指定action,那么表单将会以HTTP POST的形式提交到与展现该表单相同的URL上。在这里,我们明确指明表单要POST提交到“/orders”上(使用Thymeleaf的@{…}操作符指定相对上下文的路径)。

因此,我们需要在OrderController中添加另外一个方法,以便于处理针对“/orders”的POST请求。我们在第3章才会对订单进行持久化,在此之前,我们让它尽可能简单,如程序清单2.8所示。

程序清单2.8 处理taco订单的提交

@PostMapping
public String processOrder(Order order) {
  log.info("Order submitted: " + order);
  return "redirect:/";
}

当调用processOrder()方法处理所提交的订单时,我们会得到一个Order对象,它的属性绑定了所提交的表单域。Order与Taco非常相似,是一个非常简单的类,其中包含了订单的信息,如程序清单2.9所示。

程序清单2.9 taco订单的领域对象

package tacos;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.hibernate.validator.constraints.NotBlank;
import lombok.Data;

@Data
public class Order {

  private String name;
  private String street;
  private String city;
  private String state;
  private String zip;
  private String ccNumber;
  private String ccExpiration;
  private String ccCVV;
}

现在,我们已经开发了OrderController和订单表单的视图,接下来我们可以尝试运行一下。打开浏览器并访问http://localhost:8080/design,为taco选择一些配料,并点击“Submit Your Taco”按钮,将会看到如图2.3所示的一个表单。

..\19-0887 图\2-3.tif{78%}

图2.3 taco订单表单

填充表单的一些输入域并点击“Submit order”按钮。请关注应用的日志来查看你的订单信息。在我尝试运行的时候,日志条目如下所示(为了适应页面的宽度,重新进行了格式化):

Order submitted: Order(name=Craig Walls,street1=1234 7th Street,
     city=Somewhere, state=Who knows?, zip=zipzap, ccNumber=Who can guess?,
ccExpiration=Some day, ccCVV=See-vee-vee)

如果仔细查看上述测试订单的日志,就会发现尽管processOrder()方法完成了它的工作并处理了表单提交,但是它让一些坏信息混入了进来。表单中的大多数输入域包含的可能都是不正确的信息。我们接下来添加一些校验,确保所提交的数据至少与所需的信息比较相似。

2.3 校验表单输入


在设计新的taco作品的时候,如果用户没有选择配料或者没有为他们的作品指定名称,那么将会怎样呢?当提交表单的时候,没有填写所需的地址输入域又将发生什么呢?或者,在信用卡域中输入了一个根本不合法的数字,又该怎么办呢?

就目前的情况来看,没有什么能够阻止用户在创建taco的时候不选择任何配料,或者输入空的快递地址,甚至将他们喜欢的歌词作为信用卡号进行提交。这是因为我们还没有指明这些输入域该如何进行校验。

有种校验方法就是在processDesign()和processOrder()方法中添加大量乱七八糟的if/then代码块,逐个检查每个输入域都满足对应的校验规则。但是,这样会非常烦琐,并且难以阅读和调试。