Spring MVC資料校驗詳解
技術標籤:Spring MVCJavaEE
排版有問題,注重閱讀體驗的可以看這個資料校驗。
什麼是資料校驗?
資料校驗就是驗證客戶端的輸入資料是否合法,比如使用者登入時,使用者名稱不能為空,或者不能超出指定長度等要求,這就叫做資料校驗。
在實際中,通常使用較多是前端校驗,比如頁面中的JS校驗。對於安全要求較高的建議在服務端也要進行校驗。服務端校驗可以在控制層conroller,也可以在業務層service,controller層校驗頁面的請求引數的合法性,在conroller層的校驗,不區分客戶端型別(瀏覽器、手機客戶端、遠端呼叫);service層主要校驗關鍵業務引數,僅限於service介面中使用的引數。
本文主要總結一下如何在spring MVC中進行controller層校驗。
JSR 303
-
JSR 303 是JAVA EE 6.0 中的一項子規範,叫做Bean Validation。
-
JSR 303 通過在 Bean 屬性上標註類似於
@NotNull
、@Max
等標準的註解來指定校驗規則,並通過標準的驗證介面對 Bean 進行驗證。 -
Hibernate Validator是 JSR 303 的參考實現。Hibernate Validator 提供了 JSR 303 規範中所有內建註解的實現,除此之外還有一些附加的校驗註解。
JSR 303標準註解 | 說明 |
---|---|
@Valid | 遞迴地對關聯物件進行校驗, 如果關聯物件是個集合或者陣列, 那麼對其中的元素進行遞迴校驗, 如果是一個map, 則對其中的值部分進行校驗。 |
@Validated | 好像是Spring的註解,相比@Valid它多了一個分組校驗的功能 |
@NotNull | 被註解的元素必須不為null |
@AssertFalse | 被註解的元素必須為false |
@AssertTrue | 被註解的元素必須為true |
@Min(value) | 被註解的元素必須為一個數字,其值必須大於等於指定的最小值 |
@Max(value) | 被註解的元素必須為一個數字,其值必須小於等於指定的最小值 |
@DecimalMin(Value) | 被註解的元素必須為一個數字,其值必須大於等於指定的最小值 |
@DecimalMax(value) | 被註解的元素必須為一個數字,其值必須小於等於指定的最小值 |
@Size(min=, max=) | 被註解的元素必須在制定的範圍 |
(資料型別: String, Collection, Map and arrays) | |
@Digits(integer=, fraction=) | 被註解的元素必須為一個數字,其值必須在可接受的範圍內 |
@Past(java.util.Date/Calendar) | 被註解的元素必須是過去的日期 |
@Future | 被註解的元素必須是未來的日期 |
@Pattern(regex=, flag=) | 被註解的元素必須符合正則表示式 |
Hibernate Validator拓展註解 | 說明 |
---|---|
被註解的物件必須是電子郵箱地址 | |
@Length (min=, max=) | 被註解的物件必須是字串的大小必須在制定的範圍內 |
@NotEmpty | 被註解的物件必須非空 |
(資料型別: String, Collection, Map, arrays) | |
@NotBlank | 被註解的物件必須為字串,不能為空,檢查時會將空格忽略 |
@Range (min=, max=) | 被註解的物件必須在合適的範圍內 |
(資料型別: BigDecimal, BigInteger, String, byte, short, int, long) | |
@CreditCardNumber | 對信用卡號進行一個大致的驗證 |
@URL (protocol=, host=, port=, regexp=, flags=) | 被註解的物件必須是字串,檢查是否是一個有效的URL, |
如果提供了protocol,host等,則該URL還需滿足提供的條件 |
資料校驗
-
Spring 4.0 擁有自己獨立的資料校驗框架,同時支援 JSR 303 標準的校驗框架,但Spring 本身並沒有提供 JSR303 的實現,所以必須將 JSR303 的實現hibernate-validator 包放到類路徑下。
-
Spring 在進行資料繫結時,可同時呼叫校驗框架完成資料校驗工作。在 Spring MVC 中,可直接通過註解驅動的方式進行資料校驗。
-
Spring 的 LocalValidatorFactroyBean 既實現了 Spring 的 Validator 介面,也實現了 JSR 303 的 Validator 介面。只要在 Spring 容器中配置一個 LocalValidatorFactoryBean,即可使校驗註解生效。
-
mvc:annotation-driven/ 預設會裝配好一個 LocalValidatorFactoryBean,通過在處理方法的入參上標註 @Valid 註解即可讓 Spring MVC 在完成資料繫結後執行資料校驗的工作。
1. 匯入jar包
匯入hibernate-validator校驗包的Maven依賴:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
不要匯入7.0以上的版本,否則會報錯我也不知道為什麼。
2. 配置校驗器
在 springmvc.xml 檔案中配置校驗器HibernateValidator
:
<!-- 指定成自己配置的校驗器 -->
<mvc:annotation-driven
validator="validator"/>
<!-- 配置校驗器 -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<!-- 校驗器,使用hibernate校驗器 -->
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<!-- 指定表示校驗資訊的資原始檔,在檔案中配置校驗的錯誤資訊,如果不指定則預設使用classpath下面的ValidationMessages.properties檔案 -->
<property name="validationMessageSource" ref="messageSource"/>
</bean>
<!-- 配置國際化資原始檔 -->
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<!-- 資原始檔名 -->
<property name="basenames">
<list>
<value>classpath:errors</value>
</list>
</property>
<!-- 資原始檔編碼格式 -->
<property name="defaultEncoding" value="utf-8"/>
<!-- 對資原始檔的快取時間, 單位秒 -->
<property name="cacheSeconds" value="120"/>
</bean>
這裡有個messageSource是為了配置國際化資原始檔,這個檔案中我們會儲存一些錯誤資訊。
3. 在POJO中新增校驗註解
可以在所有需要校驗的POJO屬性上標註相應的校驗註解:
public class Employee {
private Integer id;
@NotEmpty(message = "姓名不能為空")
@Length(min = 6, max = 18, message = "姓名長度必須在6到18之間")
private String lastName;
@Email(message = "郵箱格式不正確")
private String email;
private Integer gender;
private Department department;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Past(message = "必須是過去的時間")
private Date birth;
註解的message屬性用於設定錯誤訊息,如果不設定就採用預設的。
message屬性也可以用佔位符的方式,寫成錯誤訊息資原始檔中對應的key:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-fQLk5Gnm-1613133254673)(https://secure-static.wolai.com/static/vWjTt5ZpBYZcQjyvHvAs1A/image.png)]
4. 在處理方法中獲取校驗結果
在處理方法的入參上標註 @Valid
或@Validated
註解,Spring MVC 才會在資料繫結的時候進行校驗,並將校驗結果儲存在入參物件之後的 BindingResult 或 Errors 入參中。
BindingResult 常用方法:
-
FieldError getFieldError(String field)
-
List<FieldError> getFieldErrors()
-
Object getFieldValue(String field)
-
Int getErrorCount()
@Valid
不支援分組校驗,@Validated
支援分組校驗。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-tSKCM7jH-1613133254678)(https://secure-static.wolai.com/static/p17tY5FhP1VmsdMZem7ZnL/image.png)]
@PostMapping("/emps")
public String saveEmp(@Valid Employee employee, BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
List<FieldError> fieldErrors = bindingResult.getFieldErrors(); //獲得所有欄位錯誤
Map<String, String> errorMap = new HashMap<>();
//將所有欄位的錯誤訊息新增到errorMap中
for (FieldError fieldError : fieldErrors) {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
}
model.addAttribute("errorInfo", errorMap);
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts", departments);
//轉發回add.jsp, 回顯錯誤訊息
return "add";
} else {
employeeDao.save(employee);
return "redirect:/index.jsp";
}
}
在程式碼中我們通過 bindingResult.getFieldErrors()
獲得錯誤欄位和錯誤訊息,並將其新增到ModelMap中,以實現頁面的錯誤回顯。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-xWtoTMzu-1613133254681)(https://secure-static.wolai.com/static/uLMrrPDxc9ZYTkzaNARf6C/image.png)]
可以看出,在需要校驗的POJO前邊新增@Validated
註解,在需要校驗的POJO後邊新增BindingResult bindingResult
入參來接收錯誤資訊。值得注意的是:@Validated
和BindingResult bindingResult
是成對出現的,並且形參順序是固定的(一前一後),這樣才可以順利接收到錯誤資訊。
5. 在頁面上回顯錯誤訊息
Spring MVC 除了會將校驗結果儲存到對應的 BindingResult 或 Errors 入參中外,還會將所有校驗結果儲存到隱含模型ModelMap中,所以錯誤訊息是會被儲存到請求域中的。
在 JSP 頁面上可通過 <form:errors path="userName">
標籤顯示錯誤訊息。
下面是一個例子,回顯【員工新增】的錯誤訊息:
<form:form action="emps" modelAttribute="employee">
LastName:<form:input path="lastName"/>
<form:errors path="lastName"/><br/>
Email:<form:input path="email"/>
<form:errors path="email" /><br/>
Gender:<form:radiobutton path="gender" value="1" checked="checked"/>male
<form:radiobutton path="gender" value="0"/>female
<form:errors path="gender"/><br/>
Birth:<form:input path="birth"/>
<form:errors path="birth"/><br/>
Department:<form:select path="department.id" items="${depts}" itemLabel="departmentName" itemValue="id"/>
<form:errors path="department"/><br/>
<input type="submit" value="submit">
</form:form>
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-F6egroC9-1613133254686)(https://secure-static.wolai.com/static/hdmHT6Ld84G9HynWoY5CoF/image.png)]
實際開發中,我們幾乎不太可能使用 form:errors 這個標籤,因為它只適用於JSP,不夠靈活。
如果要使用原生HTML標籤,我們可以在Controller層取出BindingResult中的錯誤訊息存於ModelMap中,然後轉發回源頁面進行錯誤回顯,就像上面的saveEmp() 一樣**@PostMapping("/emps")
public String saveEmp(@Valid Employee employee, BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
List fieldErrors = bindingResult.getFieldErrors(); //獲得所有欄位錯誤
Map<String, String> errorMap = new HashMap<>();
//將所有欄位的錯誤訊息新增到errorMap中
for (FieldError fieldError : fieldErrors) {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
}
model.addAttribute(“errorInfo”, errorMap);
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts", departments);
//轉發回add.jsp, 回顯錯誤訊息
return "add";
} else {
employeeDao.save(employee);
return "redirect:/index.jsp";
}
}****。**
然後在目標頁面中就可以${}
的方式取出錯誤訊息:
<form:form action="emps" modelAttribute="employee">
LastName:<form:input path="lastName"/>
<span>${errorInfo.lastName}</span><br/>
Email:<form:input path="email"/>
<span>${errorInfo.email}</span><br/>
Gender:<form:radiobutton path="gender" value="1" checked="checked"/>male
<form:radiobutton path="gender" value="0"/>female
<span>${errorInfo.gender}</span><br/>
Birth:<form:input path="birth"/>
<span>${errorInfo.birth}</span><br/>
Department:<form:select path="department.id" items="${depts}" itemLabel="departmentName" itemValue="id"/>
<span>${errorInfo.department}</span><br/>
<input type="submit" value="submit">
</form:form>
效果和上面是一樣的。
分組資料校驗
上面已經基本完成了springmvc的校驗功能,但是有個問題:我們是將校驗註解標註在了POJO的屬性上,但是一個POJO通常會被多個controller所使用,如果兩個不同的controller需要不一樣的校驗規則,這麼做就行不通了。比如,一個controller不需要校驗生產日期,只要校驗一下商品名稱即可;另一個controller兩個都要校驗,那這樣就沒法做了,因為兩個controller使用的是同一個POJO。
為了解決這個問題,我們可以定義多個校驗分組(其實就是多個Java介面),在分組中定義校驗規則,讓每個controller方法使用不同的校驗分組即可。
首先定義一個校驗分組:
public interface ValidGroup1 {
//介面中不需要定義任何方法,僅僅是為了對不同的校驗規則進行分組
//此分組只校驗商品名稱的長度
}
然後我們在剛剛的POJO中,新增這個分組,如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-JAPAvI0V-1613133254688)(https://secure-static.wolai.com/static/3B8syHuEfjgKgMFJWRpMLa/image.png)]
然後在controller方法入參上指明分組:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-9sSfnV0t-1613133254690)(https://secure-static.wolai.com/static/fbnebmSz4Pm2tAMJWeN9gz/image.png)]
這樣該controller就不會去校驗createtime欄位了,通過這種方式就可以實現不同的controller校驗不同欄位了。
自定義國際化錯誤訊息
目前我們的錯誤訊息是寫死在註解的message
屬性中的public class Employee {
private Integer id;
@NotEmpty(message = "姓名不能為空")
@Length(min = 6, max = 18, message = "姓名長度必須在6到18之間")
private String lastName;
@Email(message = "郵箱格式不正確")
private String email;
private Integer gender;
private Department department;
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Past(message = "必須是過去的時間")
private Date birth;,如果有國際化需求的話,就不能這麼寫了,我們需要把錯誤訊息寫在國際化資原始檔中,就像下面這樣:
Email.email=email incorrect!
NotEmpty=must not empty!
# 支援用佔位符實現動態的錯誤訊息
Length.java.lang.String={0} length incorrect,must between {2} and {1}
Past=must a past time!
Email.email=郵箱不正確!
NotEmpty=不能為空!
Length.java.lang.String={0}長度不正確! 必須在{2}和{1}之間!
Past=必須是過去的時間!
key是錯誤程式碼,value是對應的錯誤訊息。
Length.java.lang.String= {0} length incorrect,must between {2} and {1}
* 這裡面使用了佔位符,用於實現動態的錯誤訊息,{0} 表示當前屬性名,之後{1}~{n}都表示按字典順序下**@Length
*註解的屬性值。
訊息程式碼
每個屬性在進行資料繫結和資料校驗的時候,如果發生了錯誤,都會生成一個對應的 FieldError
物件。
當一個屬性校驗失敗後,校驗框架會為該屬性生成 4 個訊息程式碼,**訊息程式碼是用於在資原始檔中作為key去獲取對應的錯誤訊息。**訊息程式碼是結合 modleAttribute、欄位名、欄位型別、校驗註解名生成的4個訊息程式碼,例如 Employee 類中的 email屬性標註了一個 @Email 註解,當該屬性值不滿足@Email 所定義的規則時, 就會產生以下 4 個訊息程式碼:
-
Email.employee.email:如果modleAttribute 中的employee的email屬性發生了@Email校驗錯誤,就會生成這個訊息程式碼。
-
Email.email:只要是任意的email屬性發生了@Email校驗錯誤,就會生成這個訊息程式碼;
-
Email.java.lang.String:只要是String型別發生了@Email校驗錯誤,就會生成這個訊息程式碼;
-
Email:只要發生了@Email校驗錯誤,就會生成這個訊息程式碼。
codes [Email.employee.email, //校驗規則.ModelMap中的key.ModelMap中的value的成員屬性
Email.email, //校驗規則.成員屬性
Email.java.lang.String, //校驗規則.變數型別
Email]; //校驗規則
我們可以使用 form:errors 標籤來顯示錯誤訊息。 Spring MVC 會檢視 WEB 上下文是否載入了對應的國際化資原始檔,如果沒有,則顯示預設的錯誤訊息,否則顯示國際化訊息。
不能使用原生HTML標籤,原生HTML標籤只能通過請求域獲得預設的錯誤訊息,Spring MVC不會提供國際化支援,但或許有其他辦法可以辦到吧?比如手動讀取資原始檔然後將錯誤訊息存到請求域裡?
另外,除了校驗出錯時會生成錯誤訊息外,在進行資料型別轉換、資料格式化轉換、或者引數不存在、處理方法調用出錯等情況下,也會在ModelMap中建立錯誤訊息,其中訊息程式碼的字首說明如下:
-
required
:必要的引數不存在。如 @RequiredParam(“param1”) 標註在一個入參上,但是該請求引數不存在。 -
typeMismatch
:在資料繫結時,發生資料型別不匹配的問題。 -
methodInvocation
:Spring MVC 在呼叫處理方法時發生了錯誤。