spring-boot 使用hibernate validation對引數進行優雅的校驗
阿新 • • 發佈:2020-12-29
springboot天生支援使用hibernate validation對引數的優雅校驗,如果不使用它,只能對引數挨個進行如下方式的手工校驗,不僅難看,使用起來還很不方便:
``` java
if(StringUtils.isEmpty(userName)){
throw new RuntimeException("使用者名稱不能為空");
}
```
下面將介紹hibernate validation的基本使用方法。
## 一、引入依賴
這裡在springboot 2.4.1中進行實驗,引入以下依賴:
``` xml
org.springframework.boot
spring-boot-starter-parent
2.4.1
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.projectlombok
lombok
1.18.16
org.hibernate.validator
hibernate-validator
6.1.6.Final
```
## 二、基本請求引數校驗
如下的一個spring mvc的請求呼叫中有一個id引數(Integer型別),如果不允許它為空,該怎麼做
1. 在Controller上加上`@Validated`註解
2. 在需要校驗的欄位前面加上`@NotNull(message = "使用者id不能為空")`註解
3. 定義全域性異常處理類,定製化返回結果
``` java
@RestControllerAdvice
@Slf4j
public class ValidationAdvice {
@ExceptionHandler(Exception.class)
@ResponseBody
public WrapperResult handler(Exception e) {
//獲取異常資訊,獲取異常堆疊的完整異常資訊
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
//日誌輸出異常詳情
log.error(sw.toString());
return WrapperResult.faild("服務異常,請稍後再試");
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public WrapperResult handler(ConstraintViolationException e) {
StringBuffer errorMsg = new StringBuffer();
Set> violations = e.getConstraintViolations();
violations.forEach(x -> errorMsg.append(x.getMessage()).append(";"));
return WrapperResult.faild(errorMsg.toString());
}
}
```
Controller層程式碼如下所示:
``` java
@RestController
@Slf4j
@RequestMapping("/user")
@Validated
public class UserController {
/**
* 根據id查詢使用者資訊
*
* @param id
* @return
*/
@GetMapping
public WrapperResult findUser(@NotNull(message = "使用者id不能為空")
@RequestParam(value = "id")
String id) {
return WrapperResult.success(new UserModel());
}
}
```
如果發起請求`127.0.0.1:8080/user?id=` 則會返回結果
``` json
{
"status": 1,
"data": "使用者id不能為空;",
"msg": "FAIL",
"success": false
}
```
## 三、物件內參數校驗
上面是GET請求,下面介紹POST請求,請求物件內的引數校驗。
### 1.Controller類上加上@Validated註解
``` java
@RestController
@Slf4j
@RequestMapping("/user")
**@Validated**
public class UserController {
}
```
### 2.在POST請求方法引數前面加上`@Validated `註解
``` java
@PostMapping("/mobile-regist")
public WrapperResult mobileRegit(@Validated @RequestBody UserModel userModel) {
return WrapperResult.success(true);
}
```
### 3.在上面介紹的`ValidationAdvice`類中加上物件引數校驗異常捕獲
``` java
//處理校驗異常,對於物件型別的資料的校驗異常
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public WrapperResult handler(MethodArgumentNotValidException e) {
StringBuffer sb = new StringBuffer();
List allErrors = e.getBindingResult().getAllErrors();
allErrors.forEach(msg -> sb.append(msg.getDefaultMessage()).append(";"));
return WrapperResult.faild(sb.toString());
}
```
UserModel類的定義如下:
``` java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class UserModel {
@NotEmpty(message = "姓名不能為空")
private String name;
@NotEmpty(message = "手機號不能為空")
// @Mobile(message = "手機號格式不正確")
private String mobile;
@NotEmpty(message = "電子郵箱不能為空")
@Email(message = "電子郵箱格式不正確")
private String email;
private String password;
private String address;
@NotNull(message = "年齡不能為空")
@Min(value = 12, message = "允許註冊年齡最小為12歲")
@Max(value = 24, message = "允許年齡最大為24歲")
private Integer age;
@NotEmpty(message = "聯絡人不允許為空")
@Size(min = 1, max = 3, message = "聯絡人長度只允許1到3之間")
private List contacts;
}
```
如果POST請求如下所示
``` json
{
"name":"",
"mobile":"12666666666",
"email":"",
"password":"",
"address":"",
"age": null,
"contacts":[
]
}
```
則會返回如下定製化返回結果:
``` json
{
"status": 1,
"data": "電子郵箱不能為空;聯絡人長度只允許1到3之間;年齡不能為空;聯絡人不允許為空;姓名不能為空;手機號格式不正確;",
"msg": "FAIL",
"success": false
}
```
## 四、自定義校驗器
像是@NotNull、@Email等註解都是hibernate validation 內建的註解,我們想開發像是@Email註解一樣功能的註解,如何做呢,比如@Mobile,它的使用方法將和@Email一模一樣。
首先,先定義一個工具類存放`ValidationUtil`兩個常量值
``` java
public class ValidationUtil {
//手機號校驗正則
public static final String MOBILE_REGX = "^[1][3-9][0-9]{9}$";
public static final String MOBILE_MSG = "手機號格式錯誤";
}
```
### 1.定義註解`Mobile`
具體程式碼可以參考@Email的實現,直接將Email名字改成Mobile即可,如下所示:
``` java
@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
public @interface Mobile {
String message() default ValidationUtil.MOBILE_MSG;
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
String regexp() default ValidationUtil.MOBILE_REGX;
Pattern.Flag[] flags() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
public @interface List {
Mobile[] value();
}
}
```
### 2.定義`MobileValidator`實現對引數的校驗邏輯
``` java
public class MobileValidator implements ConstraintValidator {
private String regexp;
@Override
public void initialize(Mobile constraintAnnotation) {
//獲取校驗的手機號的格式
this.regexp = constraintAnnotation.regexp();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (!StringUtils.hasText(value)) {
return true;
}
return value.matches(regexp);
}
}
```
### 3.使用方法和`@Email`一模一樣
不贅述
## 五、分組校驗
假設一個使用者註冊的場景,使用者註冊有三種方式
1. 使用者名稱+圖形驗證碼註冊
2. 郵箱+郵箱驗證碼註冊
3. 手機號+簡訊驗證碼註冊
使用者註冊的時候除了方式不一樣,其他使用者資訊基本相同,後端開了三個介面對應著著三種註冊方式,請求體中我們使用一個Model封裝了以上所有資訊,包含著使用者名稱、郵箱、手機號等資訊,這時候不同的介面被呼叫,model中需要校驗的引數就不一樣了:
使用者名稱註冊的時候郵箱地址和手機號可以為空,但是使用者名稱不能為空;通過郵箱註冊的時候,郵箱地址不能為空,但是使用者名稱和手機號可以為空;......
分組校驗專門應對這種情況。
### 1.首先定義三個介面,表示三種組類別
``` java
public interface ValidEmail {
}
public interface ValidMobile {
}
public interface ValidUserName {
}
```
### 2.在UserModel實體類上指名組類別
``` java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class UserModel {
@NotEmpty(message = "姓名不能為空", groups = {ValidUserName.class})
@UserName(groups = {ValidUserName.class})
private String name;
@NotEmpty(message = "手機號不能為空", groups = {ValidMobile.class})
@Mobile(groups = {ValidMobile.class})
private String mobile;
@NotEmpty(message = "電子郵箱不能為空", groups = {ValidEmail.class})
@Email(message = "電子郵箱格式不正確", groups = {ValidEmail.class})
private String email;
private String password;
private String address;
@NotNull(message = "年齡不能為空")
@Min(value = 12, message = "允許註冊年齡最小為12歲", groups = {ValidEmail.class,ValidMobile.class,ValidUserName.class})
@Max(value = 24, message = "允許年齡最大為24歲",groups = {ValidEmail.class,ValidMobile.class,ValidUserName.class})
private Integer age;
@NotEmpty(message = "聯絡人不允許為空",groups = {ValidEmail.class,ValidMobile.class,ValidUserName.class})
@Size(min = 1, max = 3, message = "聯絡人長度只允許1到3之間",groups = {ValidEmail.class,ValidMobile.class,ValidUserName.class})
private List contacts;
}
```
### 3.Controller方法上指名驗證組別
``` java
/**
* 手機號註冊
*
* @param userModel
* @return
*/
@PostMapping("/mobile-regist")
public WrapperResult mobileRegit(@Validated(ValidMobile.class) @RequestBody UserModel userModel) {
return WrapperResult.success(true);
}
```
這時候進行如下請求:
POST http://127.0.0.1:8080/user/mobile-regist
``` json
{
"mobile":"12666666666",
"password":"",
"address":"",
"age": null,
"contacts":[
]
}
```
則會返回結果:
``` json
{
"status": 1,
"data": "聯絡人長度只允許1到3之間;手機號格式錯誤;聯絡人不允許為空;",
"msg": "FAIL",
"success": false
}
```
該請求中並沒有傳遞email和username欄位,而且結果中也未校驗出這兩個欄位,符合預期結果。
## 六、手動校驗
此處的手動校驗並非是使用if/else進行簡單的手動校驗,而是使用Validation自帶的校驗工具對使用了@NotNull等註解的實體物件進行屬性校驗。
首先先獲取Valiation物件:
``` java
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
```
### 1. 全屬性校驗
``` java
/**
* 驗證某個物件所有欄位
*
* @param obj
* @param
* @return
*/
public static ValidationResult validateEntity(T obj) {
ValidationResult result = new ValidationResult();
Set> set = validator.validate(obj, Default.class);
if (!CollectionUtils.isEmpty(set)) {
result.setHasErrors(true);
Map errorMsg = new HashMap<>();
for (ConstraintViolation cv : set) {
errorMsg.put(cv.getPropertyPath().toString(), cv.getMessage());
}
result.setErrorMsg(errorMsg);
}
return result;
}
```
### 2.某個欄位的單獨校驗
``` java
/**
* 驗證某個物件某個欄位
*
* @param obj
* @param propertyName
* @param
* @return
*/
public static ValidationResult validateProperty(T obj, String propertyName) {
ValidationResult result = new ValidationResult();
Set> set = validator.validateProperty(obj, propertyName, Default.class);
if (!CollectionUtils.isEmpty(set)) {
result.setHasErrors(true);
Map errorMsg = new HashMap<>();
for (ConstraintViolation cv : set) {
errorMsg.put(propertyName, cv.getMessage());
}
result.setErrorMsg(errorMsg);
}
return result;
}
```
ValidationResult的定義如下:
``` java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class ValidationResult {
private Boolean hasErrors;
private Map errorMsg;
}
```
## 七、檔案上傳校驗
### 1.tomcat容器下檔案上傳校驗
在springboot+tomcat架構下的檔案上傳校驗,假如已經有了如下的配置:
``` yaml
spring:
servlet:
multipart:
max-file-size: 1MB
max-request-size: 1MB
```
這表示只允許上傳小於1MB大小的檔案,如果不指定異常處理器,預設會報前端400,在`ValidationAdvice`類中新增如下程式碼可以自定義返回結果:
``` java
//檔案上傳檔案大小超出限制
@ExceptionHandler(MaxUploadSizeExceededException.class)
@ResponseBody
public WrapperResult