SpringBoot – Bean validation 參數校驗
- 2021 年 8 月 11 日
- 筆記
- Spring Boot
前言
後台開發中對參數的校驗是不可缺少的一個環節,為了解決如何優雅的對參數進行校驗?
- JSR303(Java Specification Requests)應運而生,JSR303 是JavaBean參數校驗的標準。
- Bean Validation 為 JavaBean 驗證定義了相應的元數據模型和 API。
- Hibernate validator 5 是 Bean Validation 1.1的實現。
常見註解
- Bean Validation中定義的註解:
註解 | 詳細資訊 |
---|---|
@Null | 被注釋的元素必須為 null |
@NotNull | 被注釋的元素必須不為 null |
@AssertTrue | 被注釋的元素必須為 true |
@AssertFalse | 被注釋的元素必須為 false |
@Min(value) | 被注釋的元素必須是一個數字,其值必須大於等於指定的最小值 |
@Max(value) | 被注釋的元素必須是一個數字,其值必須小於等於指定的最大值 |
@DecimalMin(value) | 被注釋的元素必須是一個數字,其值必須大於等於指定的最小值 |
@DecimalMax(value) | 被注釋的元素必須是一個數字,其值必須小於等於指定的最大值 |
@Size(max, min) | 被注釋的元素的大小必須在指定的範圍內 |
@Digits (integer, fraction) | 被注釋的元素必須是一個數字,其值必須在可接受的範圍內 |
@Past | 被注釋的元素必須是一個過去的日期 |
@Future | 被注釋的元素必須是一個將來的日期 |
@Pattern(value) | 被注釋的元素必須符合指定的正則表達式 |
- Hibernate validator 在JSR303的基礎上對校驗註解進行了擴展,擴展註解如下:
註解 | 詳細資訊 |
---|---|
被注釋的元素必須是電子郵箱地址 | |
@Length | 被注釋的字元串的大小必須在指定的範圍內 |
@NotEmpty | 被注釋的字元串的必須非空 |
@Range | 被注釋的元素必須在合適的範圍內 |
參數校驗的應用
依賴
<!-- hibernate validator-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.4.Final</version>
</dependency>
簡單的參數校驗示例
- 要想開啟參數校驗,需要在類上標註@Validated註解
- 控制器
@PostMapping(value = "/test")
public ValidOneEvt param(@RequestBody @Validated ValidOneEvt evt){
return evt;
}
- 實體驗證
@Getter
@Builder
public class ValidOneEvt {
@NotEmpty(message = "名稱不能為空")
private String name;
private String sex;
}
- 測試結果
級聯校驗
- 級聯校驗需要在校驗的實體上添加@Valid。
- 控制器
@PostMapping(value = "/test")
public ValidOneEvt param(@RequestBody @Validated ValidOneEvt evt){
return evt;
}
- 實體驗證
@Getter
@Builder
public class ValidOneEvt {
@NotEmpty(message = "名稱不能為空")
private String name;
private String sex;
@Valid
private ValidTwoEvt validTwoEvt;
}
- 級聯實體
@Getter
@Setter
public class ValidTwoEvt {
@Length(min = 2)
private String name;
}
- 請求示例
- 校驗結果
@Validated 與 @Valid
兩者具有相似性
註解地方
- @Valid:可以用在方法、構造函數、方法參數和成員屬性(欄位)上。
- @Validated:可以用在類型、方法和方法參數上,不能用在成員屬性(欄位)上。
@Validated和@Valid在級聯驗證功能上的區別
-
@Valid:用在方法入參上無法單獨提供級聯驗證功能。能夠用在成員屬性(欄位)上,提示驗證框架進行級聯驗證。
-
@Validated:用在方法入參上無法單獨提供級聯驗證功能。不能用在成員屬性(欄位)上,也無法提示框架進行級聯驗證。能配合級聯驗證註解@Valid進行級聯驗證。
-
總結: 通常使用@Validated, 級聯驗證使用@Valid。
自定義校驗註解
自定義校驗註解用於基礎校驗註解不能滿足業務需求。
- 自定義效驗註解驗證密碼是否相等
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* @Description 自定義參數校驗註解
* @Documented 註解中的注釋加入文檔
* @Retention 註解保留階段 RetentionPolicy.RUNTIME 運行階段
* @Target 作用目標,註解的使用範圍 TYPE:用於描述類、介面(包括註解類型) 或enum聲明
* @Constraint 將註解和註解關聯類關聯到一起
* @author coisini
* @date Aug 10, 2021
* @Version 1.0
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Constraint(validatedBy = PasswordValidator.class)
public @interface PasswordEquals {
int min() default 4;
int max() default 6;
String message() default "passwords are not equal";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- 自定義校驗註解關聯類
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* @Description 自定義校驗註解關聯類
* ConstraintValidator的第一個參數:註解的類型
* ConstraintValidator的第二個參數:自定義註解修飾的目標的類型
* @author coisini
* @date Aug 10, 2021
* @Version 1.0
*/
public class PasswordValidator implements ConstraintValidator<PasswordEquals, ValidEvt> {
private int min;
private int max;
/**
* 初始化獲取註解參數
* @param constraintAnnotation
*/
@Override
public void initialize(PasswordEquals constraintAnnotation) {
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
}
/**
* 校驗參數
* @param value
* @param context
* @return
*/
@Override
public boolean isValid(ValidEvt value, ConstraintValidatorContext context) {
String password1 = value.getPassword1();
String password2 = value.getPassword2();
return password1.equals(password2) && this.validLength(password1, password2);
}
/**
* 校驗密碼長度
* @param password1
* @param password2
* @return
*/
private boolean validLength(String password1, String password2) {
return password1.length() > min && password1.length() < max
&& password2.length() > min && password2.length() < max;
}
}
- 自定義註解校驗類
/**
* @Description 自定義註解校驗類
* @author coisini
* @date Aug 10, 2021
* @Version 1.0
*/
@Getter
@Builder
@PasswordEquals(min = 1, message = "Incorrect password length or passwords are not equal")
public class ValidEvt {
private String password1;
private String password2;
}
- 參數驗證異常統一處理
@ControllerAdvice
public class GlobalExceptionAdvice {
/**
* 參數校驗異常處理器
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public UnifyMessage handleBeanValidation(HttpServletRequest request, MethodArgumentNotValidException e) {
String method = request.getMethod();
String requestUrl = request.getRequestURI();
System.out.println(e);
List<ObjectError> errors = e.getBindingResult().getAllErrors();
String message = formatAllErrorMessages(errors);
return new UnifyMessage(10001, message,method + " " + requestUrl);
}
/**
* 自定義註解校驗異常處理器
* @param req
* @param e
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public UnifyMessage handleConstrainException(HttpServletRequest req, ConstraintViolationException e){
String method = req.getMethod();
String requestUrl = req.getRequestURI();
String message = e.getMessage();
return new UnifyMessage(10001, message,method + " " + requestUrl);
}
/**
* 異常消息拼接
* @param errors
* @return
*/
private String formatAllErrorMessages(List<ObjectError> errors){
StringBuffer errorMsg = new StringBuffer();
errors.forEach(error ->
errorMsg.append(error.getDefaultMessage()).append(";")
);
return errorMsg.toString();
}
}
- 統一消息返回
/**
* @Description 統一消息返回
* @author coisini
* @date Aug 9, 2021
* @Version 1.0
*/
public class UnifyMessage {
private int code;
private String message;
private String requestUrl;
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public String getRequestUrl() {
return requestUrl;
}
public UnifyMessage(int code, String message, String requestUrl) {
this.code = code;
this.message = message;
this.requestUrl = requestUrl;
}
}
- 測試類
@PostMapping(value = "/test1")
public ValidEvt test1(@RequestBody @Validated ValidEvt evt){
return evt;
}
- 測試結果
