2.3 验证表单输入

2.3 验证表单输入

当设计一个新的 taco 产品时,如果用户没有选择任何食材或者没有为他们的产品指定名称,该怎么办?当提交订单时,如果他们没有填写所需的地址字段,该怎么办?或者,如果他们在信用卡字段中输入的值甚至不是有效的信用卡号,该怎么办?

按照目前的情况,没有什么能阻止用户创建一个没有任何配料或空空如也的送货地址的玉米饼,甚至提交他们最喜欢的歌曲的歌词作为信用卡号码。这是因为还没有指定应该如何验证这些字段。

执行表单验证的一种方法是在 processDesign() 和 processOrder() 方法中加入一堆 if/then 块,检查每个字段以确保它满足适当的验证规则。但是这样做会很麻烦,并且难于阅读和调试。

幸运的是,Spring 支持 Java’s Bean Validation API(也称为 JSR-303;https://jcp.org/en/jsr/detail?id=303)。这使得声明验证规则比在应用程序代码中显式地编写声明逻辑更容易。使用 Spring Boot,不需要做任何特殊的事情来将验证库添加到项目中,因为 Validation API 和 Validation API 的 Hibernate 实现作为 Spring Boot web 启动程序的临时依赖项自动添加到了项目中。

要在 Spring MVC 中应用验证,需要这样做:

  • 对要验证的类声明验证规则:特别是 Taco 类。
  • 指定验证应该在需要验证的控制器方法中执行,具体来说就是:DesignTacoController 的 processDesign() 方法和 OrderController 的 processOrder() 方法。
  • 修改表单视图以显示验证错误。

Validation API 提供了几个可以放在域对象属性上声明验证规则的注释。Hibernate 的 Validation API 实现甚至添加了更多的验证注释。让我们看看如何应用这些注释来验证提交的 Taco 或 Order。

2.3.1 声明验证规则

对于 Taco 类,希望确保 name 属性不是空的或 null 的,并且所选配料列表中至少有一项。下面的程序清单显示了一个更新后的 Taco 类,它使用 @NotNull 和 @Size 来声明这些验证规则。程序清单 2.10 为 Taco 域类添加验证。

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 个字符的长度。

当涉及到对提交玉米饼订单进行验证声明时,必须对 Order 类应用注解。对于地址的属性,只需要确保用户没有留下任何空白字段。对于这一点,将使用 Hibernate Validator 的 @NotBlank 注解。

支付领域的验证是一个比较奇特的存在。你不仅需要确保 ccNumber 属性不为空,还要确保它包含的是一个有效的信用卡号码的值。该 ccExpiration 属性必须符合 MM/YY(两位数的年/月)格式。而 ccCVV 属性必须是一个三位的数字。为了实现这种验证,需要使用一些其他的 Java Bean Validation API 注释,同时需要从 Hibernate Validator 集合中借用一些验证注解。下面程序清单列出了验证 Order 类所需要的改变。程序清单 2.11 验证 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 算法(https://en.wikipedia.org/wiki/Luhn_algorithm)检查过的有效信用卡号。这可以防止用户出错的数据和故意错误的数据,但不能保证信用卡号码实际上被分配到一个帐户,或该帐户可以用于交易。

不幸的是,没有现成的注释来验证 ccExpiration 属性的 MM/YY 格式。我已经应用了 @Pattern 注释,为它提供了一个正则表达式,以确保属性值符合所需的格式。如果想知道如何破译正则表达式,我建议查看许多在线正则表达式指南,包括 http://www.regularexpressions.info/。正则表达式语法是一门黑暗的艺术,当然也超出了本书的范围。

最后,用 @Digits 注释 ccCVV 属性,以确保值恰好包含三个数字。

所有的验证注释都包含一个消息属性,该属性定义了如果用户输入的信息不符合声明的验证规则的要求时将显示给用户的消息。

2.3.2 在表单绑定时执行验证

既然已经声明了应该如何验证 Taco 和 Order,那么我们需要重新访问每个控制器,并指定应该在将表单提交到各自的处理程序方法时执行验证。

要验证提交的 Taco,需要将 Java Bean Validation API 的 @Valid 注释添加到 DesignTacoController 的 processDesign() 方法的 Taco 参数中。程序清单 2.12 验证 POST 来的 Taco。

@PostMapping
public String processDesign(@Valid Taco design, Errors errors) {
    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() 方法之前,对提交的 Taco 对象执行验证。如果存在任何验证错误,这些错误的详细信息将在传递到 processDesign() 的错误对象中捕获。processDesign() 的前几行查询 Errors 对象,询问它的 hasErrors() 方法是否存在任何验证错误。如果有,该方法结束时不处理 Taco,并返回 “design” 视图名,以便重新显示表单。

要对提交的 Order 对象执行验证,还需要对 OrderController 的 processOrder() 方法进行类似的更改。程序清单 2.13 验证 POST 来的 Order。

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

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

在这两种情况下,如果没有验证错误,则允许该方法处理提交的数据。如果存在验证错误,则请求将被转发到表单视图,以便用户有机会纠正其错误。

但是用户如何知道哪些错误需要改正呢?除非调出表单上的错误,否则用户将只能猜测如何成功提交表单。

2.3.3 显示验证错误

Thymeleaf 通过 fields 属性及其 th:errors 属性提供了对 Errors 对象的便捷访问。例如,要在信用卡号字段上显示验证错误,可以添加一个 元素,该元素将这些错误引用用于订单模板,如下所示。程序清单 2.14 显示验证错误。

<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 字段,并且假设该字段存在错误,它将用验证消息替换 元素的占位符内容。

如果在其他字段的订单表单周围使用类似的 标记,则在提交无效信息时可能会看到类似图 2.4 的表单。这些错误表明姓名、城市和邮政编码字段被留空,所有的支付字段都不符合验证标准。

![图 2.4 在订单表单上显示验证错误](E:\Document\spring-in-action-v5-translate\第一部分 Spring 基础\第二章 开发 Web 应用程序\图 2.4 在订单表单上显示验证错误.jpg)

图 2.4 在订单表单上显示验证错误

现在 Taco Cloud 控制器不仅可以显示和捕获输入,还可以验证信息是否符合一些基本的验证规则。让我们后退一步,重新考虑第 1 章中的 HomeController,看看另一种实现。

上一页
下一页