1. 程式人生 > 其它 >Spring MVC資料校驗詳解

Spring MVC資料校驗詳解

技術標籤:Spring MVCJavaEE

參考:【SpringMVC學習06】SpringMVC中的資料校驗

排版有問題,注重閱讀體驗的可以看這個資料校驗

什麼是資料校驗?

資料校驗就是驗證客戶端的輸入資料是否合法,比如使用者登入時,使用者名稱不能為空,或者不能超出指定長度等要求,這就叫做資料校驗。

在實際中,通常使用較多是前端校驗,比如頁面中的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拓展註解說明
@Email被註解的物件必須是電子郵箱地址
@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 入參來接收錯誤資訊。值得注意的是:@ValidatedBindingResult 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 在呼叫處理方法時發生了錯誤。