前端校驗後,爲什麼需要後端校驗
在前面完成數據參數綁定到Controller時,我們可以在@RequestParam註解中做簡單的空校驗,就是設置required屬性爲true,以此來指定Controller方法中形參是否必須傳入。數據校驗是很常見的操作,有前端校驗,即用戶在前端頁面上填寫表單時,檢查數據的合法性,再傳入到後端。到了後端,還可以有後端的數據校驗,後端校驗通常是在業務邏輯方法,也就是我們的Controller方法裏完成,例如在參數列表裏使用註解完成數據校驗。後端校驗的原因是爲了安全性考慮,防止別人通過接口亂調用後臺的業務邏輯方法,假設在業務方法中不進行數據校驗,有可能會被別讓繞過前端校驗,直接通過後端的接口方法調用,傳入大量的髒數據到數據庫。
在這篇日誌中,總結自己使用集成Hibernate的校驗框架, 和Spring MVC的Bean Validation數據校驗。Bean Validation數據校驗可以檢查JavaBean中的數據,接下來先看看校驗器的搭建。
配置validation校驗器
validator聲明和屬性配置
上面說到,後端校驗通常放在業務邏輯方法中,在Spring MVC裏也就是我們的處理器適配器中具體實現方法,通過在形參列表中使用註解,完成數據校驗。我的處理器映射器和適配器都是用註解的方式配置,即在Spring MVC的配置文件裏使用的annotation-driven標籤對,既然後端數據校驗發生在處理器適配器中,當然校驗器也要在annotation-driven標籤裏聲明瞭:
<!-- 簡寫方式 配置註解的處理器映射器和適配器 -->
<!-- 添加名爲"validator的校驗器" -->
<mvc:annotation-driven validator="validator"></mvc:annotation-driven>
在annotation-driven標籤中我們聲明一個validator屬性,指定了一個id爲validator校驗器。然後我們就繼續在下面配置這個id爲“validator”的校驗器屬性:
<!-- 配置校驗器 -->
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
<property name="validationMessageSource" ref="messageSource" />
</bean>
LocalValidatorFactoryBean是默認的校驗器實現,下面配置的providerClass爲繼承的Hibernate校驗框架,最後validationMessageSource表示的是校驗使用的資源文件,裏面可以自定義編寫當校驗出數據不合法時,給出的錯誤提示。如果不配置這個校驗資源文件,會默認使用ValidationMessages.properties。
校驗資源文件配置
當我們聲明瞭校驗資源文件後,就可以使用自己編寫的文件實現自定義錯誤提示信息。首先在Spring MVC配置文件中配置這個資源文件:
<!-- 配置校驗資源文件 -->
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:UserValidationMessages</value>
</list>
</property>
<property name="fileEncodings" value="utf-8" /> <!-- 資源文件編碼格式 -->
<property name="cacheSeconds" value="120" /> <!-- 資源文件內容的緩存事件1 -->
</bean>
ReloadableResourceBundleMessageSource類用提供讀取資源屬性文件,也就是我們的校驗資源文件,它需要指定一些屬性,資源文件名basenames和資源文件的編碼格式fileEncodings和資源文件內容的緩存時間cacheSeconds,在這裏我們配置校驗資源文件爲UserValidationMessages,這是我們自己編寫的properties文件:
# 設置校驗錯誤提示信息
user.name.length.error=Please enter a name with a length of 1-20 characters.
user.gender.isEmpty=Please enter gender
在下面具體校驗註解中我們纔會看到它的使用。
前端數據類型轉換
還有一個配置,使用conversionService接口完成將前端客戶端的一些數據進行轉換後,再傳入後端服務器端。爲什麼需要數據類型轉換?你看再前端頁面中用戶輸入的數據通常是基本數據類型,整數或者字符串,但傳到後端Controller進行業務邏輯處理時,通常我們是用Java類來保存數據。前面的數據參數綁定我們就可以看到,我們的查詢用戶頁面,用戶名和性別在前端頁面都是字符串類型,傳到後端進行查詢時,是要用Java類型保存,再傳入Controller方法中。所以有時候需要對前端數據進行轉換後,再傳入後端。我們可以在annotation-driven標籤中,配置conversionService屬性:
<mvc:annotation-driven conversion-service="conversionService"
validator="validator">
</mvc:annotation-driven>
然後配置conversionService使用FromattingConversionServiceFactoryBean類,該類可以完成字符串和Date類型的轉換。
<!-- 字符串轉爲Date類型的Java類 -->
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionService-FactoryBean">
</bean>
其實,如果我們不在annotation-driven標籤中配置conversion-service屬性,還是會默認加載一個ConversionService,而且使用的就是FromattingConversionServiceFactoryBean類,在這裏我是想展示出有這麼一個配置,可以完成前端的數據類型轉換。
測試用例
添加校驗註解
來到校驗註解配置,在這裏就要指定我們要對哪些數據進行校驗了,例如我們的Controller方法完成對用戶數據的查詢,Java實體類是User,要求前端填入的查詢數據,姓名長度範圍在1-20個字符之間,性別不能爲空。那麼在User類中我們就可以這麼配置:
public class User {
private Integer id;
@Size(min=1, max=20, message="{user.name.length.error}")
private String username;
private String password;
@NotEmpty(message="{user.gender.isEmpty}")
private String gender;
private String email;
private String province;
private String city;
private Date birthday;
private Integer age;
// 省略get()和set()方法
}
在username屬性上面,我麼使用@Size註解,指定min=1和max=20,校驗的錯誤信息爲user.name.length.error。在gender屬性上,使用@NotEmpty註解完成非空校驗,錯誤信息爲user.gender.isEmpty。由於我們在前面配置了校驗資源文件,所以輸出的會是我們的自定義錯誤提示。
最後的配置,就是在我們的Controller方法形參列表中,使用@Validated註解,指定哪些參數數據需要進行校驗:
@Controller
@RequestMapping("user")
public class validationControllerTest {
private UserServiceImpl userService = new UserServiceImpl();
@RequestMapping("validationByCondition")
public String queryUserByCondition(Model model,
@Validated User user,
BindingResult bindingResult) {
// 保存校驗錯誤信息
List<ObjectError> ErrorList = null;
if (bindingResult.hasErrors()) {
ErrorList = bindingResult.getAllErrors();
for (ObjectError objectError : ErrorList) {
System.out.println(objectError.getDefaultMessage());
}
}
List<User> userList = null;
if(user == null || (user.getUsername() == null && user.getGender() == null)) {
// 如果查詢框中兩個查詢條件都爲空,則默認查詢所有顧客數據
userList = userService.queryUserList();
} else {
// 否則進行條件查詢
userList = userService.queryUserByCondition(user);
}
// model數據傳到頁面
model.addAttribute("userList", userList);
model.addAttribute("ErrorList", ErrorList);
return "users/validateUser"; // 返回視圖
}
}
可以看到,我們要對傳入方法的User類實例進行數據校驗,並且傳入BindingResult類對象,它存放了校驗錯誤信息。第9行首先定義一個ErrorList來存放校驗錯誤信息。第11行獲取所有的錯誤信息放到ErrorList中,然後做輸出,如果只是這樣輸出,錯誤信息是顯示在了控制檯上,所以第29行我們通過model對象,將這個ErrorList傳到前端頁面去顯示。
前端視圖
爲了在前端頁面中顯示錯誤信息提示,我們通過model對象把錯誤信息傳到了前端視圖中顯示:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用戶查詢列表</title>
</head>
<body>
<form action="validationByCondition.action" method="post">
用戶名:<input type="text" name="username" />
性別:<input type="text" name="gender" />
<input type="submit" value="查找" />
</form>
<!-- 顯示錯誤信息 -->
<c:if test="${ErrorList != null}">
<c:forEach items="${ErrorList}" var="error">
<font color="red">${error.defaultMessage}</font><br/>
</c:forEach>
</c:if>
<hr/>
<h2>搜索結果</h2>
<table width="300px;" border=1>
<tr>
<td>顧客名</td>
<td>性別</td>
<td>電子郵箱</td>
<td>省會</td>
<td>城市</td>
</tr>
<c:forEach items="${userList }" var="user">
<tr>
<td>${user.username }</td>
<td>${user.gender }</td>
<td>${user.email }</td>
<td>${user.province }</td>
<td>${user.city }</td>
</tr>
</c:forEach>
</table>
</body>
</html>
第15行我們接收傳來的ErrorList,並遍歷錯誤信息,將其顯示出來:
Bean Validation分組校驗
使用場景
分組校驗的使用場景是這樣的,假設我們的多個Controller方法都用到了同一個Java類作爲數據存儲並傳入,我們需要對類中哪些屬性進行校驗,是在實體類裏面使用註解標識的,但是如果我們一個Controller方法只需對類中的某一個屬性做校驗,而另一個Controller方法需要對類中的兩個屬性做校驗,怎麼辦?方法是使用分組校驗,在Java實體類中的屬性裏標註分組信息,然後在不同的Controller方法中形參列表裏的@Validated註解添加分組屬性,根據不同的分組,實現不同的校驗。
配置分組信息
首先創建兩個空接口,用來標識不同的分組:
然後在Java實體類中,爲需要校驗的屬性username和gender,在其註解裏添加分組:
public class User {
private Integer id;
@Size(min=1, max=20, message="{user.name.length.error}",
groups= {UserGroup1.class})
private String username;
private String password;
@NotEmpty(message="{user.gender.isEmpty}", groups= {UserGroup2.class})
private String gender;
private String email;
private String province;
private String city;
private Date birthday;
private Integer age;
//省略get()和set()方法
}
我們將username屬性的校驗放在了UserGroup1,gender屬性的校驗放在了UserGroup2。
最後,我們在Controller方法裏,根據不同的業務邏輯,配置校驗分組,實現對共用數據類型的不同校驗方式:
@Controller
@RequestMapping("user")
public class validationControllerTest {
private UserServiceImpl userService = new UserServiceImpl();
@RequestMapping("validationByCondition")
public String queryUserByCondition(Model model,
@Validated(value=UserGroup1.class) User user,
BindingResult bindingResult) {
// 保存校驗錯誤信息
List<ObjectError> ErrorList = null;
if (bindingResult.hasErrors()) {
ErrorList = bindingResult.getAllErrors();
for (ObjectError objectError : ErrorList) {
System.out.println(objectError.getDefaultMessage());
}
}
List<User> userList = null;
if(user == null || (user.getUsername() == null && user.getGender() == null)) {
// 如果查詢框中兩個查詢條件都爲空,則默認查詢所有顧客數據
userList = userService.queryUserList();
} else {
// 否則進行條件查詢
userList = userService.queryUserByCondition(user);
}
// model數據傳到頁面
model.addAttribute("userList", userList);
model.addAttribute("ErrorList", ErrorList);
return "users/validateUser"; // 返回視圖
}
}
在@Validated註解中,添加UserGroup1分組,這樣,在調用該方法進行業務邏輯處理時,只會對該分組標識的username屬性進行校驗:
完整實現GitHub已上傳:
https://github.com/justinzengtm/SSM-Framework/tree/master/SpringMVC_Project