- Spring實戰(第6版)
- (美)克雷格·沃斯
- 2498字
- 2022-12-20 19:14:51
2.3 校驗表單輸入
在設計新的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。
2.3.1 聲明校驗規則
對于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屬性,該屬性定義了當輸入的信息不滿足聲明的校驗規則時,要給用戶展現的消息。
2.3.2 在表單綁定的時候執行校驗
現在,我們已經聲明了如何校驗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:/";
}
在這兩個場景中,如果沒有校驗錯誤,方法都允許處理提交的數據;如果存在校驗錯誤,請求將會被轉發至表單視圖上,以便用戶糾正他們的錯誤。
但是,用戶該如何知道有哪些要糾正的錯誤呢?如果我們無法指出表單上的錯誤,那么用戶只能不斷猜測如何才能成功提交表單。
2.3.3 展現校驗錯誤
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 在訂單表單上展現校驗錯誤
現在,我們的Taco Cloud控制器不僅能夠展現和捕獲輸入,還能校驗用戶提交的信息是否滿足一定的基本驗證規則。接下來,我們后退一步,重新考慮第1章中的HomeController,并學習一種替代實現方案。