Spring实战(第6版)
上QQ阅读APP看书,第一时间看更新

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

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

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

比较幸运的是,Spring支持JavaBean校验API(JavaBean Validation API,也称为JSR-303),使我们能够更容易地声明检验规则,而不必在应用程序代码中显式编写声明逻辑。

要在Spring MVC中应用校验,我们需要:

在构建文件中添加Spring Validation starter;

在要被校验的类上声明校验规则,具体到我们的场景中,要被校验的类就是Taco类;

在需要进行校验的控制器方法中声明要进行校验,具体来讲,此处的控制器方法也就是DesignTacoController的processTaco()方法和OrderController的processOrder()方法;

修改表单视图以展现校验错误。

Validation API提供了一些注解,可以添加到领域对象的属性上,以便声明校验规则。Hibernate的Validation AP实现又添加了一些校验注解。通过将Spring Validation starter添加到构建文件中,我们就能将这两者引入项目中。在Spring Boot Starter向导的I/O区域下面选中Validation复选框就可以实现这一点,但是如果想手动编写构建文件,在Maven pom.xml中添加如下的条目同样可以做到这一点:

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

如果你使用Gradle,需要如下的依赖:

implementation 'org.springframework.boot:spring-boot-starter-validation'

我们是否还需要validation starter?

在早期版本的Spring Boot中,Spring Validation starter会自动包含到web starter中。从Spring Boot 2.3.0版本开始,如果想要使用校验,需要显式地将其添加到构建文件中。

validation starter已经准备就绪,我们看一下如何使用其中的一些注解来校验用户提交的Taco和TacoOrder。

对于Taco类来说,我们想要确保name属性不能为空或null,同时希望有至少一项配料被选中。程序清单2.11展示了更新后的Taco类,它使用@NotNull和@Size注解来声明这些校验规则。

程序清单2.11 为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;
  @NotNull
  @Size(min = 1, message = "You must choose at least 1 ingredient")
  private List<Ingredient> ingredients;

}

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

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

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

程序清单2.12 校验订单的字段

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

@Data
public class TacoOrder {

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

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

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

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

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

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

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

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

  private List<Taco> tacos = new ArrayList<>();

  public void addTaco(Taco taco) {
    this.tacos.add(taco);
  }
}

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

令人遗憾的是,目前还没有现成的注解来校验ccExpiration属性的MM/YY格式。在这里,我使用了@Pattern注解并为其提供了一个正则表达式,确保属性值符合预期的格式。如果你想知道如何解释这个正则表达式,我建议你参考一些在线的正则表达式指南,比如Regular Expressions Info网站。正则表达式仿佛一种魔法,已经超出了本书的范围。最后,ccCVV属性上添加了@Digits注解,确保它的值包含3位数字。

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

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

要校验提交的Taco,我们需要为DesignTacoController中processTaco()方法的Taco参数添加一个JavaBean Validation API的@Valid注解,如程序清单2.13所示。

程序清单2.13 校验POST提交的Taco

import javax.validation.Valid;
import org.springframework.validation.Errors;

...

  @PostMapping
  public String processTaco(
          @Valid Taco taco, Errors errors,
          @ModelAttribute TacoOrder tacoOrder) {

    if (errors.hasErrors()) {
      return "design";
    }

    tacoOrder.addTaco(taco);
    log.info("Processing taco: {}", taco);

    return "redirect:/orders/current";
  }

@Valid注解会告诉Spring MVC要对提交的Taco对象进行校验,而校验时机是在它绑定完表单数据之后、调用processDesign()之前。如果存在校验错误,这些错误的细节将会捕获到一个Errors对象中并传递给processTaco()。processTaco ()方法的前几行会查阅Errors对象,调用其hasErrors()方法判断是否有校验错误。如果存在校验错误,这个方法将不会处理Taco对象并返回“design”视图名,以使表单重新展现。

为了对提交的TacoOrder对象进行校验,OrderController的processOrder()方法也需要进行类似的变更,如程序清单2.14所示。

程序清单2.14 校验POST提交的TacoOrder

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

  log.info("Order submitted: {}", order);
  sessionStatus.setComplete();

  return "redirect:/";
}

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

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

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

程序清单2.15 展现校验错误

<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>

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

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

在为订单表单的其他输入域都添加类似的<span>标签之后,如果提交错误信息,表单会如图2.5所示。其中,错误信息提示姓名、城市和邮政编码字段为空,而且所有支付相关的输入域均未满足校验条件。

2-5

图2.5 在订单表单上展现校验错误

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