其他分享
首页 > 其他分享> > 开发Web应用(二)

开发Web应用(二)

作者:互联网

处理表单提交

仔细看一下视图中的

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

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

前面,我们曾经使用 @GetMapping 注解声明 showDesignForm() 方法要处理针对 /design 的 HTTP GET 请求。与 @GetMapping 处理 GET 请求类似,我们可以使用 @PostMapping 来处理 POST 请求。为了处理 taco 设计的表单提交,在 DesignTacoController 中添加下面代码的 processDesign() 方法。

@PostMapping
public String processDesign(Taco 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 对象采取任意操作了。

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

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

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

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

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

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

根据已经学到的关于 @Controller、@RequestMapping 和 @GetMapping 的知识,我们可以很容易地创建这样的控制器。它应该如下面代码所示。

在 src/main/java/tacos/web 包下新建 OrderController.java 文件,编写代码如下。

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 模板来提供的。

在 src/main/resources/templates 目录下创建 orderForm.html 文件,编写代码如下。

<!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 内容,不需要过多关注。但是,需要注意一点,这里的 标签和 design.html 中的 标签有所不同,它指定了一个表单的 action 。如果不指定 action,那么表单将会以 HTTP POST 的形式提交到与展现该表单相同的 URL 上。在这里,我们明确指明表单要 POST 提交到 /orders 上(使用 Thymeleaf 的 @{...} 操作符指定相对上下文的路径)。

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

在 OrderController 中添加以下方法(不要忘了 import)。

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

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

现在,我们已经开发了 OrderController 和订单表单的视图,接下来我们可以尝试运行一下。

在实验楼 WebIDE 中执行以下命令运行程序。

# 进入项目根目录
cd /home/project/taco-cloud
# 运行程序
mvn clean spring-boot:run

打开 Web 服务,并访问 /design,为 taco 选择一些配料,并点击 Submit Your Taco 按钮,将会看到如下图所示的一个表单。

填充表单的一些输入域并点击 Submit order 按钮。请关注应用的日志来查看你的订单信息。在我尝试运行的时候,日志条目如下所示。

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

校验表单输入

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

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

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

比较幸运的是,Spring 支持 Java 的 Bean 校验 API(Bean Validation API,也被称为 JSR-303)。这样的话,我们能够更容易地声明检验规则,而不必在应用程序代码中显式编写声明逻辑。借助 Spring Boot,要在项目中添加校验库,我们甚至不需要做任何特殊的操作,这是因为 Validation API 以及 Validation API 的 Hibernate 实现将会作为 Spring Boot web starter 的传递性依赖自动添加到项目中。

要在 Spring MVC 中应用校验,需要的步骤如下。

  • 在要被校验的类上声明校验规则:具体到我们的场景中,也就是 Taco 类。
  • 在控制器方法中声明要进行校验:具体来讲,也就是 DesignTacoController 的 processDesign() 方法和 OrderController 的 processOrder() 方法。
  • 修改表单视图以展现校验错误。

Validation API 提供了一些可以添加到领域对象上的注解,以便于声明校验规则。Hibernate 的 Validation AP 实现又添加了一些校验注解。接下来,我们看一下如何使用其中的一些注解来校验用户提交的 Taco 和 Order。

声明校验规则

对于 Taco 类来说,我们想要确保 name 属性不能为空或 null,同时希望选中的配料至少要包含一项。下面代码将展示更新后的 Taco 类,它使用 @NotNull 和 @Size 注解来声明这些校验规则。

package tacos;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;

@Data
public class Taco {

  @NotNull
  @Size(min=5, message="Name must be at least 5 characters long")
  private String name;

  @Size(min=1, message="You must choose at least 1 ingredient")
  private List<String> ingredients;

}

我们可以发现,除了要求 name 属性不为 null 之外,我们还声明了它的值在长度上至少要有 5 个字符。

在对提交的 taco 订单进行校验时,我们必须要给 Order 类添加注解。对于地址相关的属性,我们只想确保用户没有提交空白字段。为此,我们可以使用 Hibernate Validator 的 @NotBlank 注解。

但是,支付相关的字段就比较复杂了。我们不仅要确保 ccNumber 属性不为空,还要保证它所包含的值是一个合法的信用卡号码。ccExpiration 属性必须符合 MM/YY 格式(两位的月份和年份)。ccCVV 属性需要是一个 3 位的数字。为了实现这种校验,我们需要其他的一些 Java Bean Validation API 注解,并结合来自 Hibernate Validator 的注解。下面代码展现了校验 Order 类所需的变更。

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

@Data
public class Order {

  @NotBlank(message="Name is required")
  private String name;

  @NotBlank(message="Street is required")
  private String street;

  @NotBlank(message="City is required")
  private String city;

  @NotBlank(message="State is required")
  private String state;

  @NotBlank(message="Zip code is required")
  private String zip;

  @CreditCardNumber(message="Not a valid credit card number")
  private String ccNumber;

  @Pattern(regexp="^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$",
           message="Must be formatted MM/YY")
  private String ccExpiration;

  @Digits(integer=3, fraction=0, message="Invalid CVV")
  private String ccCVV;

}

我们可以看到,ccNumber 属性添加了 @CreditCardNumber 注解。这个注解声明该属性的值必须是合法的信用卡号,它要能通过 Luhn 算法的检查。这能防止用户有意或无意地输入错误的数据,但是该检查并不能确保这个信用卡号真的分配给了某个账户,也不能保证这个账号能够用来进行支付。

令人遗憾的是,目前还没有现成的注解来校验 ccExpiration 属性的 MM/YY 格式。在这里,我使用了 @Pattern 注解并为其提供了一个正则表达式,确保属性值符合预期的格式。如果你想知道如何解释这个正则表达式,那么我建议你参考一些在线的正则表达式指南。正则表达式是一种魔法,已经超出了本书的范围。

最后,在 ccCVV 属性上添加了 @Digits 注解,能够确保它的值包含 3 位数字。

所有的校验注解都包含了一个 message 属性,该属性定义了当输入的信息不满足声明的校验规则时要给用户展现的消息。

在表单绑定的时候执行校验

现在,我们已经声明了如何校验 Taco 和 Order,接下来我们要重新修改每个控制器,让表单在 POST 提交至对应的控制器方法时执行对应的校验。

要校验提交的 Taco,我们需要为 DesignTacoController 中 processDesign() 方法的 Taco 参数添加一个 Java Bean Validation API 的 @Valid 注解。修改 DesignTacoController 中的代码,如下所示。

@PostMapping
public String processDesign(@Valid @ModelAttribute("design") Taco design, Errors errors, Model model) {
  if (errors.hasErrors()) {
    return "design";
  }

  // Save the taco design...
  // We'll do this in chapter 3
  log.info("Processing design: " + design);

  return "redirect:/orders/current";
}

@Valid 注解会告诉 Spring MVC 要对提交的 Taco 对象进行校验,而校验时机是在它绑定完表单数据之后、调用 processDesign() 之前。如果存在校验错误,那么这些错误的细节将会捕获到一个 Errors 对象中并传递给 processDesign()。

processDesign() 方法的前几行会查阅 Errors 对象,调用其 hasErrors() 方法判断是否有校验错误。如果存在校验错误,那么这个方法将不会处理 Taco 对象并返回“design”视图名,表单会重新展现。

为了对提交的 Order 对象进行校验,OrderController 的 processOrder() 方法也需要进行类似的变更。修改 OrderController 中的代码,如下所示。

@PostMapping
public String processOrder(@Valid Order order, Errors errors) {
  if (errors.hasErrors()) {
    return "orderForm";
  }

  log.info("Order submitted: " + order);
  return "redirect:/";
}

在这两个场景中,如果没有校验错误,那么方法都会允许处理提交的数据。如果存在校验错误,那么请求将会被转发至表单视图上,以便让用户有机会纠正他们的错误。

但是,用户该如何知道有哪些要纠正的错误呢?如果我们无法指出表单上的错误,那么用户只能不断猜测如何才能成功提交表单。

展现校验错误

Thymeleaf 提供了便捷访问 Errors 对象的方法,这就是借助 fields 及其 th:errors 属性。举例来说,为了展现信用卡字段的校验错误,我们可以添加一个 元素,该元素会将对错误的引用用到订单的表单模板上,如下所示。

<label for="ccNumber">Credit Card #: </label>
<input type="text" th:field="*{ccNumber}" />
<span
  class="validationError"
  th:if="${#fields.hasErrors('ccNumber')}"
  th:errors="*{ccNumber}"
  >CC Num Error</span
>

在这里, 元素使用 class 属性来为错误添加样式,以引起用户的注意。除此之外,它还使用 th:if 属性来决定是否要显示该元素。fields 属性的 hasErrors() 方法会检查 ccNumber 域是否存在错误,如果存在,就将会渲染 。

th:errors 属性引用了 ccNumber 输入域,如果该输入域存在错误,那么它会将 元素的占位符内容替换为校验信息。

为订单表单的其他输入域也都添加类似的 标签,orderForm.html 的代码如下所示。

<!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}" />
      <span
        class="validationError"
        th:if="${#fields.hasErrors('name')}"
        th:errors="*{name}"
        >Name Error</span
      >

      <br />

      <label for="street">Street address: </label>
      <input type="text" th:field="*{street}" />
      <span
        class="validationError"
        th:if="${#fields.hasErrors('street')}"
        th:errors="*{street}"
        >Street Error</span
      >

      <br />

      <label for="city">City: </label>
      <input type="text" th:field="*{city}" />
      <span
        class="validationError"
        th:if="${#fields.hasErrors('city')}"
        th:errors="*{city}"
        >City Error</span
      >

      <br />

      <label for="state">State: </label>
      <input type="text" th:field="*{state}" />
      <span
        class="validationError"
        th:if="${#fields.hasErrors('state')}"
        th:errors="*{state}"
        >State Error</span
      >

      <br />

      <label for="zip">Zip code: </label>
      <input type="text" th:field="*{zip}" />
      <span
        class="validationError"
        th:if="${#fields.hasErrors('zip')}"
        th:errors="*{zip}"
        >Zip Error</span
      >

      <br />

      <h3>Here's how I'll pay...</h3>
      <label for="ccNumber">Credit Card #: </label>
      <input type="text" th:field="*{ccNumber}" />
      <span
        class="validationError"
        th:if="${#fields.hasErrors('ccNumber')}"
        th:errors="*{ccNumber}"
        >CC Num Error</span
      >
      <br />

      <label for="ccExpiration">Expiration: </label>
      <input type="text" th:field="*{ccExpiration}" />
      <span
        class="validationError"
        th:if="${#fields.hasErrors('ccExpiration')}"
        th:errors="*{ccExpiration}"
        >CC Num Error</span
      >

      <br />

      <label for="ccCVV">CVV: </label>
      <input type="text" th:field="*{ccCVV}" />
      <span
        class="validationError"
        th:if="${#fields.hasErrors('ccCVV')}"
        th:errors="*{ccCVV}"
        >CC Num Error</span
      >
      <br />

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

design.html 修改后的代码如下所示。

<!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}"
      th:action="@{/design}"
      id="tacoForm"
    >
      <span
        class="validationError"
        th:if="${#fields.hasErrors('ingredients')}"
        th:errors="*{ingredients}"
        >Ingredient Error</span
      >

      <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}" />
        <span th:text="${#fields.hasErrors('name')}">XXX</span>
        <span
          class="validationError"
          th:if="${#fields.hasErrors('name')}"
          th:errors="*{name}"
          >Name Error</span
        >
        <br />

        <button>Submit your taco</button>
      </div>
    </form>
  </body>
</html>

重新启动程序,如果提交错误信息,那么表单将会如下图所示。错误信息提示姓名、城市和邮政编码字段为空,而且所有支付相关的输入域均未满足校验条件。

现在,我们的 Taco Cloud 控制器不仅能够展现和捕获输入,还能校验用户提交的信息是否满足一定的基本验证规则。接下来,我们后退一步,重新考虑一下第 1 章中的 HomeController,介绍一种替代实现方案。

标签:Web,taco,校验,表单,开发,design,应用,Taco,import
来源: https://www.cnblogs.com/sakura579/p/14075573.html