2.3 校验表单输入

2.3 校验表单输入

在设计新的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。

2.3.1 声明校验规则

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

程序清单2.10 为Taco领域类添加校验

1
2
3
4
5
6
7
8
9
10
11
12
13
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类添加注解。对于地址相关的属性,我们只想确保用户没有提交空白字段。为此,我们可以使用HibernateValidator的@NotBlank注解。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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属性,该属性定义了当输入的信息不满足声明的校验规则时要给用户展现的消息。

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

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

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

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

1
2
3
4
5
6
7
8
9
10
@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()之前。如果存在校验错误,那么这些错误的细节将会捕获到一个Errors对象中并传递给processDesign()。processDesign()方法的前几行会查阅Errors对象,调用其hasErrors()方法判断是否有校验错误。如果存在校验错误,那么这个方法将不会处理Taco对象并返回“design”视图名,表单会重新展现。

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

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

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

程序清单2.14 展现校验错误

1
2
3
4
5
<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.4所示。错误信息提示姓名、城市和邮政编码字段为空,而且所有支付相关的输入域均未满足校验条件。

epub_29101559_23

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

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