一、故事背景
關於引數合法性驗證的重要性就不多說了,即使前端對引數做了基本驗證,後端依然也需要進行驗證,以防不合規的資料直接進入伺服器,如果不對其進行攔截,嚴重的甚至會造成系統直接崩潰!
本文結合自己在專案中的實際使用經驗,主要以實用為主,對資料合法性驗證做一次總結,不瞭解的朋友可以學習一下,同時可以立馬實踐到專案上去。
下面我們透過幾個示例來演示如何判斷引數是否合法,廢話不多說,直接擼起來!
二、斷言驗證
對於引數的合法性驗證,最初的做法比較簡單,自定義一個異常類。
public class CommonException extends RuntimeException {
private Integer code;
public Integer getCode() {
return code;
}
public CommonException(String message) {
super(message);
this.code = 500;
}
public CommonException(Integer code, String message) {
super(message);
this.code = code;
}
}
當檢查到某個引數不合法的時候,直接拋異常!
@RestController
public class HelloController {
@RequestMapping("/upload")
public void upload(MultipartFile file) {
if (file == null) {
throw new CommonException("請選擇上傳檔案!");
}
//.....
}
}
最後寫一個統一異常攔截器,對拋異常的邏輯進行兜底處理。
這種做法比較簡單直觀,如果當前引數既要判斷是否為空,又要判斷長度是否超過最大限制的時候,程式碼就會顯得很臃腫,而且複用性很差!
於是,程式界的大佬想到了一個更加優雅又能節省程式碼的方式,建立一個斷言類工具類,專門用來判斷引數的是否合法,如果不合法就拋異常,示例如下:
/**
* 斷言工具類
*/
public abstract class LocalAssert {
public static void isTrue(boolean expression, String message) throws CommonException {
if (!expression) {
throw new CommonException(message);
}
}
public static void isStringEmpty(String param, String message) throws CommonException{
if(StringUtils.isEmpty(param)) {
throw new CommonException(message);
}
}
public static void isObjectEmpty(Object object, String message) throws CommonException {
if (object == null) {
throw new CommonException(message);
}
}
public static void isCollectionEmpty(Collection coll, String message) throws CommonException {
if (coll == null || (coll.size() == 0)) {
throw new CommonException(message);
}
}
}
當我們需要對引數進行驗證的時候,直接透過這個類就可以完成,示例如下:
@RestController
public class HelloController {
@RequestMapping("/save")
public void save(String name, String email) {
LocalAssert.isStringEmpty(name, "使用者名稱不能為空!");
LocalAssert.isStringEmpty(email, "郵箱不能為空!");
//.....
}
}
相比上面的實現方式,這種處理邏輯,程式碼明顯要簡潔的多!
類似這樣的工具類還很多,比如spring
也提供了一個名為Assert
的斷言工具類,在開發的時候,可以直接使用!
三、註解驗證
下面我們要介紹的是另一種更簡潔的引數驗證邏輯,使用註解來對資料進行合法性驗證,不僅程式碼會變得很簡潔,閱讀起來也十分令人賞心悅目!
以 Spring Boot 工程為例,下面我們一起來看看具體的實踐方式。
3.1、新增依賴包
首先在pom.xml
中引入spring-boot-starter-web
依賴包即可,它會自動將註解驗證相關的依賴包打入工程!
<!-- spring boot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3.2、編寫註解校驗請求物件
接著建立一個實體User
,用於封裝使用者註冊時的請求引數,並在引數屬性上新增對應的註解驗證規則!
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class User {
@NotBlank(message = "使用者名稱不能為空!")
private String userName;
@Email(message = "郵箱格式不正確")
@NotBlank(message = "郵箱不能為空!")
private String email;
@NotBlank(message = "密碼不能為空!")
@Size(min = 8, max = 16,message = "請輸入長度在8~16位的密碼")
private String userPwd;
@NotBlank(message = "確認密碼不能為空!")
private String confirmPwd;
// set、get方法等...
}
3.3、編寫請求介面
在web
層建立一個register()
註冊介面方法,同時在請求引數上新增@Valid
註解,示例如下:
import javax.validation.Valid;
@RestController
public class UserController {
@RequestMapping("/register")
public ResultMsg register(@RequestBody @Valid User user){
if(!user.getUserPwd().equals(user.getConfirmPwd())){
throw new CommonException(4001, "確認密碼與密碼不相同,請確認!");
}
//業務處理...
return ResultMsg.success();
}
}
3.4、編寫全域性異常處理器
最後自定義一個異常全域性處理器,用於處理異常邏輯,如下:
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 攔截Controller層的異常
* @param e
* @return
*/
@ExceptionHandler(value = {Exception.class})
@ResponseBody
public Object exceptionHandler(HttpServletRequest request, Exception e){
LOGGER.error("【統一異常攔截】請求地址:{}, 錯誤資訊:{}", request.getRequestURI(), e.getMessage());
// 註解驗證丟擲的異常
if(e instanceof MethodArgumentNotValidException){
// 獲取錯誤資訊
String error = ((MethodArgumentNotValidException) e).getBindingResult().getFieldError().getDefaultMessage();
return ResultMsg.fail(500, error);
}
// 自定義丟擲的異常
if(e instanceof CommonException){
return ResultMsg.fail(((CommonException) e).getCode(), e.getMessage());
}
return ResultMsg.fail(999, e.getMessage());
}
}
統一響應物件ResultMsg
,如下:
public class ResultMsg<T> {
/**狀態碼**/
private int code;
/**結果描述**/
private String message;
/**結果集**/
private T data;
/**時間戳**/
private long timestamp;
// set、get方法等...
}
3.5、服務測試
啟動專案,使用postman
來驗證一下程式碼的正確性,看看效果如何?
- 測試欄位是否為空
- 測試郵箱是否合法
- 測試密碼長度是否符合要求
- 測試密碼與確認密碼是否相同
可以看到,驗證結果與預期一致!
四、自定義註解驗證
事實上,熟悉 SpringMVC 原始碼的同學可能知道,Spring Boot 內建了一個hibernate-validator
校驗元件,上文就是利用它來完成對請求時入參上的註解驗證。
預設的情況下,依賴包已經給我們提供了非常多的校驗註解,如下!
- JSR 提供的校驗註解!
- Hibernate Validator 提供的校驗註解
但是某些情況,例如性別這個引數,可能需要我們自己去手動驗證。
針對這種情況,我們也可以自定義一個註解來完成引數的校驗,也便於進一步瞭解註解驗證的原理。
自定義註解驗證,實現方式如下!
首先,建立一個Sex
註解。
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = SexValidator.class)
@Documented
public @interface Sex {
String message() default "性別值不在可選範圍內";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
然後,建立一個SexValidator
類,實現自ConstraintValidator
介面
public class SexValidator implements ConstraintValidator<Sex, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Set<String> sexSet = new HashSet<String>();
sexSet.add("男");
sexSet.add("女");
return sexSet.contains(value);
}
}
最後,在User
實體類上加入一個性別引數,使用自定義註解進行校驗!
public class User {
@NotBlank(message = "使用者名稱不能為空!")
private String userName;
@Email(message = "郵箱格式不正確")
@NotBlank(message = "郵箱不能為空!")
private String email;
@NotBlank(message = "密碼不能為空!")
@Size(min = 8, max = 16,message = "請輸入長度在8~16位的密碼")
private String userPwd;
/**
* 自定義註解校驗
*/
@Sex(message = "性別輸入有誤!")
private String sex;
// set、get方法等...
}
啟動服務,重新請求,執行結果如下:
結果與預期一致!
五、總結
引數驗證,在開發中使用非常頻繁,如何優雅的進行驗證,讓程式碼變得更加可讀,是業界大佬一直在追求的目標!
本文主要圍繞在 Spring Boot 中實現引數統一驗證進行相關的知識總結和介紹,如果有描述不對的地方,歡迎留言支援。
示例程式碼:spring-boot-example-valid