JSR303
- 1)、導入 javax.validation、hibernate-validator依賴,尤其是第二個,在springboot應用中使用校驗,必須導入
- 2)、給Bean的字段添加校驗注解:javax.validation.constraints,並定義自己的message提示
- @NotNull: CharSequence, Collection, Map 和 Array 對象不能是 null, 但可以是空集(size = 0)。
- @NotEmpty: CharSequence, Collection, Map 和 Array 對象不能是 null 並且相關對象的 size 大於 0。
- @NotBlank: String 不是 null 且 至少包含一個字符
- 3)、開啟校驗功能 使用@Valid
- 效果:校驗錯誤以后會有默認的響應;
- 4)、給校驗的bean后緊跟一個BindingResult,就可以獲取到校驗的結果
- 5)、分組校驗(多場景的復雜校驗)
- @NotBlank(message = "品牌名必須提交",groups ={AddGroup.class,UpdateGroup.class})
- @Validated({AddGroup.class}),給校驗注解標注什么情況需要進行校驗
- 默認沒有指定分組的字段校驗使用注解@Valid,在分組校驗情況下,只會在@Validated({AddGroup.class})生效; - 6)、自定義校驗
- 1、編寫一個自定義的校驗注解
- 2、編寫一個自定義的校驗器 ConstraintValidator
- 3、關聯自定義的校驗器和自定義的校驗注解
- @Constraint(validatedBy = { ListValueConstraintValidator.class【可以指定多個不同的校驗器,適配不同類型的校驗】 })
- 統一的異常處理
- @ControllerAdvice
- 編寫異常處理類,使用@ControllerAdvice。
- 使用@ExceptionHandler標注方法可以處理的異常。
舉例
要校驗的實體類
- 注意 username, password, code 字段都有多個校驗注解
@Data
public class RegisterVO {
@NotBlank(message = "用戶名不能為空")
@Length(min = 4, max = 20, message = "用戶名長度為4-20字符")
private String username;
@NotBlank(message = "密碼不能為空")
@Length(min = 8, max = 16, message = "密碼長度為8-16字符")
private String password;
@Pattern(regexp = "^1[3-9][0-9]{9}$", message = "手機號格式不合法")
private String phone;
@NotNull(message = "驗證碼不能為空")
@Pattern(regexp = "^[0-9]{6}$", message = "驗證碼為6位數字")
private String code;
}
控制器
- 注意我這里沒標@RequestBody注解,這個等會再說
@PostMapping("/register")
public String register(@Validated RegisterVO registerVO) {
// 校驗出錯會被異常處理器處理
return "success";
}
異常處理器
- 使用@ControllerAdvice和@ExceptionHandler組合
- @ExceptionHandler標注在方法上,指定這個方法處理的是哪個異常
- @ControllerAdvice指名這個類既是一個控制器,也是一個異常處理類,也就是說,你下面的方法,
- 如果返回值是String,那么它也會被視圖解析器處理,返回視圖頁面;
- 如果你想讓它返回json數據,那么加上@ResponseBody注解即可;
- 如果你這個類所有方法最終都不返回視圖,只返回json,那么很簡單,直接把@
ControllerAdvice換成@RestControllerAdvice即可。
現在我們使用下面這個異常處理來處理對前端傳來的數據RegisterVO 進行校驗的結果。當數據校驗失敗時,會拋出異常,會拋出哪個異常呢,我們先直接使用Exception.class來接收,使用它總是沒錯的。
- 我們把所有校驗結果封裝成一個map,key是字段名字,value是校驗出錯的信息。
@ControllerAdvice
public class AuthExceptionHandler {
/**
* 注冊,表單提交數據格式校驗失敗;返回json數據
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler({Exception.class})
public Map<String, String> exceptionHandler(Exception e) {
BindingResult bindingResult = e.getBindingResult();
// 使用stream api
Map<String, String> map = bindingResult.getFieldErrors()
.stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
// 上面那種是種簡寫,如果你不熟悉,這樣也可以
// Map<String, String> map = new HashMap<>();
// bindingResult.getFieldErrors().forEach(fieldError -> {
// String field = fieldError.getField();
// String message = fieldError.getDefaultMessage();
// map.put(field, message);
// });
return map;
}
}
驗證
然后我們讓前端提交一個全空的數據
我們期待的返回給我們校驗結果,以json數據返回。但是它報錯了!!!
java.lang.IllegalStateException: Duplicate key 用戶名長度為4-20字符
DuplicateKey一般是兩個相同鍵出現,比如你在數據庫插入兩條id字段相同的記錄,假設id是唯一索引,此時就會拋出DuplicateKeyXXXException,仔細查看報錯內容,發現出錯的代碼位置
at com.vivi.auth.exception.AuthExceptionHandler.bindingExceptionHandler(AuthExceptionHandler.java:38)
也就是我們寫的異常處理的這一行
Map<String, String> map = bindingResult.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
所以可以得出結果,肯定是這個校驗結果是,某個key出現了兩次,導致無法封裝成功,因此他也不知道同一個鍵,第二次的值是要丟掉還是替換第一個呢?
所以這兩個相同鍵是哪里來的?還記得我開始寫的 RegisterVO 類么,有些字段上面有兩個校驗注解,那么是這個原因么,我們可以在異常處理方法上debug,在它封裝成map之前,看一下它這個校驗結果里面有什么
我們發現有6個校驗錯誤,其中 username 和 password 都出現了兩次,正如我們的校驗注解縮寫,每個字段都有兩個校驗
@NotBlank(message = "用戶名不能為空")
@Length(min = 4, max = 20, message = "用戶名長度為4-20字符")
private String username;
@NotBlank(message = "密碼不能為空")
@Length(min = 8, max = 16, message = "密碼長度為8-16字符")
private String password;
我們在點開看一下,比如 username 的兩個結果,是不是我們的校驗注解所寫的message,的確是的!
總結:
- 某個字段上有兩個或多個校驗注解時,如果兩個的規則都被觸發,那么就會有兩個鍵相同(都是這個字段名),值不同(兩個校驗各自的message)的校驗結果。
- 這時我們想把它封裝成一個map,直接使用使用之前那種寫法肯定是不行的,我們可以簡單修改一下,既然是同一個字段的校驗結果,將這兩個信息聯合起來就好了呀,比如入下面這樣:
bindingResult.getFieldErrors().forEach(fieldError -> {
String field = fieldError.getField();
String message = fieldError.getDefaultMessage(); // 當次結果
String msg = map.getOrDefault(field, ""); // 上次校驗結果
map.put(field, msg + "," + message); // 連接起來再賦值
});
最后,還有一個問題,就是之前說的 數據校驗失敗拋出的異常到底是什么類型?
我這里簡單說一下,有興趣的朋友可以自己翻翻源碼。
- 如果前端是form表單提交數據,數據格式就為 'application/x-www-form-urlencoded;charset=UTF-8' ,Spring 使用 FormHttpMessageConverter 轉化請求體(表單數據),到封裝成對象 ,校驗失敗拋出異常 BindException;這種情況下,我們在controller接收時也不能使用@RequestBody,否則會報錯 Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported, org.springframework.web.HttpMediaTypeNotSupportedException
- 如果前端是ajax或別的方式,以json格式傳輸數據,那我們接收時就需要添加 @RequestBody ,Spring按照json格式進行解析以及封裝,校驗失敗拋出 MethodArgumentNotValidException
- 最后,如果你還是不清楚,你就使用Exception來處理,肯定能成功,你再打印一下異常的類型就能看到它具體是哪個類了!