Spring Validator接口校驗
上一篇日誌使用Bean Validation校驗機制,對基本數據類型進行校驗,方法是在實體類屬性上使用註解標識校驗方式,最後在Controller類中具體方法的形參裏添加@Vlidated註解。Bean Validation校驗有一個缺點是,我們的數據校驗是在Java實體類裏進行約束的,如果我們有多個處理器方法需要用到同一個實體類,那麼定義在實體類屬性上的校驗規則就不好劃分了,有的處理器只需要校驗一個屬性,而有的處理器需要校驗多個屬性,我們不可能爲每一個處理器都創建一個實體類。解決的方法在上一篇日誌裏也說到,使用分組校驗方式,除此之外,還可以使用Spring的Validator接口校驗,它允許我們在外部指定某一對象的校驗規則。
校驗器實現類
Spring的Validator是一個接口,我們自己的校驗實現類必須實現這個接口,纔可以通過重寫方法完成自定義的校驗規則,需要我們實現的方法有兩個:supports()和validate()
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
// 反射機制通過類的class靜態變量獲得該類的實例
return User.class.equals(clazz);
}
@Override
public void validate(Object obj, Errors errors) {
// 錯誤信息放入errors對象
ValidationUtils.rejectIfEmpty(errors, "username", "Username.is.empty",
"用戶名不允許爲空。");
User user = (User) obj;
if (user.getPassword() == null || user.getPassword().equals("")) {
// rejectValue()參數:錯誤字段名,全局錯誤碼,默認錯誤提示信息
errors.rejectValue("password", "Password.is.empty", "密碼不允許爲空。");
} else if (user.getPassword().length() < 8) {
errors.rejectValue("password", "Length.too.short", "密碼長度不能小於八位。");
}
}
}
Support()方法的功能是判斷該校驗類,是否支持被校驗的實體類。例如我們這個校驗類負責對User類進行校驗,supports()方法傳入被校驗的實體類,通過反射機制獲得User類實例,然後判斷是否與傳入的被校驗實體類匹配。Validate()方法則是進行校驗的具體實現方法,方法參數列表中有一個Errors對象,負責往裏面存放校驗的錯誤信息。下面就是具體的校驗規則了,我們可以使用ValidationUtils校驗工具類的方法進行校驗,提供的參數依次爲存放錯誤信息對象error,校驗的字段名(對於校驗實體類中的屬性),全局錯誤碼(類似於Bean Validation校驗中根據錯誤碼,使用外部properties的錯誤提示信息),最後一個參數是默認錯誤提示信息,當全局錯誤碼沒有找到對應的提示信息時,使用默認的錯誤提示信息。
除了使用ValidationUtils校驗工具類外,第23行還也可以使用erroe對象的方法,設置獲取校驗錯誤信息,參數和ValidationUtils類的方法幾乎一致。
Controller實現類
校驗器類配置完後,在具體的業務邏輯處理部分,Controller類中使用。
@Controller
@RequestMapping("user")
public class InterfaceValidationController {
@InitBinder
public void initBinder(DataBinder binder) {
// 爲DataBinder對象設置Validator校驗接口
binder.setValidator(new UserValidator());
}
@RequestMapping("login")
public String login(Model model, @Valid User user, BindingResult result) {
List<ObjectError> allErrors = null;
if (result.hasErrors()) {
allErrors = result.getAllErrors();
// 輸出所有錯誤信息
for(ObjectError objectError : allErrors) {
System.out.println("code = " + objectError.getCode() +
"DefaultMessage = " + objectError.getDefaultMessage());
// 將錯誤信息發送到前端頁面
model.addAttribute("allErrors", allErrors);
}
// 最後返回視圖
return "users/login";
} else {
// 如果校驗沒有錯誤,跳轉到成功登陸的頁面
return "users/successLogin";
}
}
首先需要通過initBinder()方法,在Controller類方法中進行校驗器的綁定,方法需要DataBinder對象參數,DataBinder對象的功能是進行數據綁定,可以將數據進行類型轉換,設置校驗器等。DataBinder有一個成員變量BindingResult,進行了數據綁定了校驗器綁定,當校驗數據有錯誤信息時,就會將其放入到BindingResult對象中的Errors屬性中,Errors對象集合前面說到,就是用來存放錯誤信息的。在Controller具體方法的參數列表中對要校驗的數據對象User類添加@Valid註解,標識對該對象進行數據校驗,接着添加BindingResult對象(這裏有一點要注意,BindingResult參數位置必須緊跟在被校驗的數據對象後面),當校驗出現錯誤信息時,第15行我們就可以通過該對象的hasErrors()方法判斷校驗是否出錯,然後使用getAllErrors()方法獲取錯誤信息進行輸出。最後第22行我們將錯誤信息傳到前端頁面上顯示,給用戶提示。
前端頁面測試
最後,在前端頁面進行簡單的登陸測試:
<%@ 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>
<from action="login.action" method="post">
用戶名:<input type="text" name="username" /></br>
密碼: <input type="password" name="password" /></br>
<input type="submit" value="登錄"/>
<!-- 顯示校驗錯誤信息 -->
<c:if test="${allErrors != null }">
<c:forEach items="${allErrors}" var="error">
</br><font color="red">${error.defaultMessage}</font>
</c:forEach>
</c:if>
</from>
</body>
</html>
第15行遍歷後臺發來的allErrors錯誤信息集合,如果出現校驗出錯,則顯示錯誤信息。根據我們前面校驗器的配置,對於User類對象的數據校驗,用戶名和密碼都不允許爲空:
當輸入信息正確,用戶名和密碼都不爲空,且密碼長度不低於8位,便可成功跳轉:
全局異常處理器Exception Resolver
對於程序運行時的錯誤信息,我們可以通過查看日誌來排查錯誤,當我們把錯誤信息傳到前端頁面時,爲了讓用戶能看懂錯誤原因,就需要對錯誤信息進行處理,在信息傳送到前端頁面前,將其捕獲。完成錯誤信息捕獲和加工處理,就需要配置我們的異常處理器。異常處理器用來自定義程序運行時如何解析異常,它需要自定義異常類,裏面存儲了對應異常的異常信息。還需要配置異常處理器,對於捕捉到的異常,如果是在自定義異常類中配置好的預期異常,則拋出相應的錯誤信息,否則,就進行其他顯示。
自定義異常類
首先是自定義異常類,示例我們定義一個處理User類的異常類和異常處理器,在異常類中,設置對於User類出現異常時的錯誤信息存儲。
package com.mvc.exception;
public class UserException extends Exception {
private static final long serialVersionUID = 1L;
private String exceptionMessage;
public UserException(String exceptionMessage) {
super(exceptionMessage);
this.exceptionMessage = exceptionMessage;
}
public String getExceptionMessage() {
return exceptionMessage;
}
public void setExceptionMessage(String exceptionMessage) {
this.exceptionMessage = exceptionMessage;
}
}
自定義異常類UserException專門負責處理User類異常,它怎麼指定處理User類呢?這個是在異常處理器中完成,UserException繼承了Exception類,這樣我們就可以在具體Controller方法中將其throws拋出該異常。該類中定義了一個異常信息變量,用來存放異常信息,當異常處理器捕獲到User類的異常時,通過UserException的構造方法設置異常信息,最後拋出UserException。
異常處理器
來到異常處理器的配置,異常處理器是捕獲和處理異常的核心,在Spring MVC中,底層異常會一級一級往上拋,最後到達全局異常處理器,全局異常處理器的工作主要有四步:
- 捕獲異常,解析出異常類型。
- 如果異常是預期異常(有定義好的異常類),則拋出相應的異常信息。
- 如果異常不是預期異常,則創建一個自定義異常類,拋出相應的異常信息(如:“未知異常信息”)。
- 將異常信息綁定到前端頁面,跳轉到相應的異常信息頁面中去。
結合上面的自定義異常類,來看看針對User類的異常處理器的配置:
package com.mvc.exception;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
public class UserExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
// 首先解析出異常的類型
UserException userException = null;
if (ex instanceof UserException) {
// 如果異常類型是UserException,則直接創建該類型的異常信息
userException = (UserException) ex;
} else {
// 否則創建一個自定義的異常類型
userException = new UserException("發生未知錯誤。");
}
// 取出錯誤信息
String errorMessage = userException.getExceptionMessage();
ModelAndView modelAndView = new ModelAndView();
// 錯誤信息傳送到前端頁面
modelAndView.addObject("errorMessage", errorMessage);
// 定向到錯誤提示頁面
modelAndView.setViewName("errorPage/userError");
return modelAndView;
}
}
Spring MVC中,異常信息最終通過DispatcherServlet交由全局異常處理器處理,需要全局異常處理器實現HandlerExceptionResolver接口接,重寫裏面的resolverException()方法完成異常處理。該方法中有兩個參數要注意,object handler指定異常處理器要處理的對象,Exception ex顯然就是接收底層拋出的異常。
在我們的異常處理器UserExceptionResolver中,第14行首先判斷異常類型是否我們的定義的預期異常UserException,如果是,則拋出,否則,創建一個自定義異常類型,並給出錯誤提示“發生未知錯誤”。最後第26行,對異常信息處理完後,發送到前端頁面進行展示,並跳轉到錯誤提示界面。
測試用例
最後要使用我們的異常處理器,先要在Spring配置文件中添加這個異常處理器:
<!-- 配置全局異常處理器 -->
<bean class="com.mvc.exception.UserExceptionResolver"></bean>
然後在Controller類方法中做相應的判斷,如果出現預期異常,則拋出:
@Controller
@RequestMapping("user")
public class InterfaceValidationController {
@InitBinder
public void initBinder(DataBinder binder) {
// 爲DataBinder對象設置Validator校驗接口
binder.setValidator(new UserValidator());
}
@RequestMapping("login")
public String login(Model model, @Valid User user, BindingResult result)
throws UserException {
boolean allowVisit = checkUser(user);
if (!allowVisit) {
// 該用戶沒有訪問權限,拋出異常
throw new UserException("您沒有權限訪問!");
}
List<ObjectError> allErrors = null;
if (result.hasErrors()) {
allErrors = result.getAllErrors();
// 輸出所有錯誤信息
for(ObjectError objectError : allErrors) {
System.out.println("code = " + objectError.getCode() +
"DefaultMessage = " + objectError.getDefaultMessage());
// 將錯誤信息發送到前端頁面
model.addAttribute("allErrors", allErrors);
}
// 最後返回視圖
return "users/login";
} else {
// 如果校驗沒有錯誤,跳轉到成功登陸的頁面
return "users/successLogin";
}
}
可以看到第57行最後我們還要跳轉到錯誤頁面,將錯誤信息顯示出來:
<%@ page language="java" import="java.util.*"
contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta charset="utf-8">
<title>錯誤提示</title>
</head>
<body>
發生異常,錯誤信息如下:</br>
<h3>
<font color="red">${errorMessage}</font>
</h3></br>
</body>
</html>
完整代碼已上傳GitHub:
https://github.com/justinzengtm/SSM-Framework/tree/master/SpringMVC_Project