本文原文鏈接地址:http://nullpointer.pw/hibernate-validator-best-practice.html
幾年前剛學習 SpringBoot 的時候,就接觸並開始使用 HibernateValidator 校驗框架,注解校驗結合統一異常處理,對代碼的整潔性提高特別有幫助。但是目前發現公司里比較新的項目中對參數進行校驗還是使用以前傳統的方式,需要逐一對請求對象中的屬性使用 if 來判斷合法性,當需要校驗的屬性很多時,一大坨的 if 判斷校驗代碼就不可避免。本文介紹 HibernateValidator 校驗框架的日常使用,不涉及自定義約束注解。
沒有對比就沒有傷害
首先來看一下使用校驗框架前后的代碼對比
使用校驗框架后,代碼簡潔很多有木有~
環境配置
首先需要引入依賴,需要注意是否依賴有沖突,如果有一定要解決掉沖突
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.0.Final</version>
</dependency>
依賴引入后,Spring 環境需要配置一下校驗器。
@Configuration
public class ValidatorConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失敗
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
這里設置 failFast 為 true,代表着如果有多個參數的情況下,只要校驗出一個參數有誤就返回錯誤而不再繼續校驗。
為對象添加校驗注解
@Data
public class StudentVo {
@NotNull(message = "學號不能為空")
private Integer id;
@NotNull(message = "姓名不能為空")
private String name;
@NotNull(message = "郵箱地址不能為空")
@Email(message = "郵箱地址不正確")
private String email;
private Integer age;
}
Hibernate Validator 是對 JSR 349 驗證規范的具體實現,相關的常用注解 我貼在文末以供參考。
請求接口處理
@PostMapping("/create")
public Result<StudentVo> create(@Validated StudentVo student) {
System.out.println(student.toString());
return Result.success(student);
}
@PostMapping("/update")
public Result<StudentVo> update(@Validated StudentVo student) {
System.out.println(student.toString());
return Result.success(student);
}
注意 @Validated 是 org.springframework.validation.annotation.Validated
,不要引入錯了。加了這個注解之后,就會自動會參數進行校驗。如果校驗不通過,會拋出 BindException
或者 MethodArgumentNotValidException
這兩個異常中的一個異常,一般項目中為了規范接口返回值,都會進行統一異常處理。
校驗異常統一異常處理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BindException.class)
@ResponseBody
public Result validateErrorHandler(BindException e) {
ObjectError error = e.getAllErrors().get(0);
return Result.fail(error.getDefaultMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Result<?> validateErrorHandler(MethodArgumentNotValidException e) {
ObjectError error = e.getBindingResult().getAllErrors().get(0);
return Result.fail(error.getDefaultMessage());
}
@ExceptionHandler(Throwable.class)
@ResponseBody
public Result<?> handleException(HttpServletRequest request, Throwable ex) {
return Result.fail(ex.getMessage());
}
}
因為配置校驗為 failFast,因此錯誤信息中只會有一條記錄。
第一次測試
curl -i -X POST -d "name=張三" 'http://localhost:8082/learning-validator/create'
只填寫了 name 參數,而 id 與 email 未填寫的情況下進行請求,返回結果
{"success":false,"msg":"學號不能為空"}
按照一般開發邏輯而言,create 接口是不需要傳遞 id 參數的,但是 update 接口一般必須要傳 id 參數,難不成用這個這個校驗框架后需要寫兩個對象么?其實不是的。這就引出了 校驗分組 的概念,即可以選擇校驗某些組的屬性進行校驗。
校驗分組
首先需要創建兩個分組,CreateGroup 與 UpdateGroup,分別代表着新增時需要校驗與更新時需要校驗。兩個組都是空的接口,只是作為一個標記類使用。
public interface CreateGroup {
}
public interface UpdateGroup {
}
接着修改請求對象,分別指定哪些對象是哪些組需要校驗的,如果不指定組,默認都會進行校驗。
@Data
public class StudentVo {
@NotNull(groups = UpdateGroup.class, message = "學號不能為空")
private Integer id;
@NotNull(message = "姓名不能為空")
private String name;
@NotNull(groups = CreateGroup.class, message = "郵箱地址不能為空")
@Email(groups = CreateGroup.class, message = "郵箱地址不正確")
private String email;
private Integer age;
}
本文示例中請求對象,id 屬性只在更新時校驗;name 屬性無論是新增還是更新都要校驗;email 屬性只在新增時進行校驗,而 age 屬性因為未指定校驗注解,因此不進行校驗,這里的 groups 可以是多個分組。
指定屬性的分組之后,控制器接口也需要指定使用哪一個組來進行校驗。
@PostMapping("/create")
public Result<StudentVo> create(@Validated(CreateGroup.class) StudentVo student) {
System.out.println(student.toString());
return Result.success(student);
}
@PostMapping("/update")
public Result<StudentVo> update(@Validated(UpdateGroup.class) StudentVo student) {
System.out.println(student.toString());
return Result.success(student);
}
第二次測試
curl -i -X POST -d "name=張三" 'http://localhost:8082/learning-validator/create'
只填寫了 name 參數,而 id 與 email 未填寫的情況下進行請求,返回結果:
{"success":false,"msg":"郵箱地址不能為空"}
填寫 name 參數以及 email 參數進行請求
curl -i -X POST \
-d "name=張三" \
-d "email=vcmq@foxmail.com" \
'http://localhost:8082/learning-validator/create'
返回結果:
{"data":{"name":"張三","email":"vcmq@foxmail.com"},"success":true,"msg":"success"}
可以看到 id 這個字段在 create 的時候,並沒有進行校驗。修改為 update 接口進行測試。
curl -i -X POST \
-d "name=張三" \
-d "email=vcmq@foxmail.com" \
'http://localhost:8082/learning-validator/update'
返回結果:
{"success":false,"msg":"學號不能為空"}
手動校驗
除了使用@Validated 注解方式校驗,也可以進行手動校驗,手動校驗同樣也支持分組校驗。
@PostMapping("/create2")
public Result<StudentVo> create2(StudentVo student) {
ValidatorUtils.validateEntity(student, CreateGroup.class);
System.out.println(student.toString());
return Result.success(student);
}
校驗工具類:
public class ValidatorUtils {
private static Validator validator;
static {
validator = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失敗
.failFast(true)
.buildValidatorFactory().getValidator();
}
/**
* 校驗對象
*
* @param object 待校驗對象
* @param groups 待校驗的組
* @throws ApiException 校驗不通過,則報 ApiException 異常
*/
public static void validateEntity(Object object, Class<?>... groups)
throws ApiException {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
constraintViolations.stream().findFirst()
.map(ConstraintViolation::getMessage)
.ifPresent(v1 -> {
throw new ApiException(v1);
});
}
}
常用注解
空與非空檢查
注解 | 支持 Java 類型 | 說明 |
---|---|---|
@Null | Object | 為 null |
@NotNull | Object | 不為 null |
@NotBlank | CharSequence | 不為 null,且必須有一個非空格字符 |
@NotEmpty | CharSequence、Collection、Map、Array | 不為 null,且不為空(length/size>0) |
Boolean 值檢查
注解 | 支持 Java 類型 | 說明 | 備注 |
---|---|---|---|
@AssertTrue | boolean、Boolean | 為 true | 為 null 有效 |
@AssertFalse | boolean、Boolean | 為 false | 為 null 有效 |
日期檢查
注解 | 支持 Java 類型 | 說明 | 備注 |
---|---|---|---|
@Future | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 驗證日期為當前時間之后 | 為 null 有效 |
@FutureOrPresent | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 驗證日期為當前時間或之后 | 為 null 有效 |
@Past | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 驗證日期為當前時間之前 | 為 null 有效 |
@PastOrPresent | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 驗證日期為當前時間或之前 | 為 null 有效 |
數值檢查
注解 | 支持 Java 類型 | 說明 | 備注 |
---|---|---|---|
@Max | BigDecimal、BigInteger,byte、short、int、long 以及包裝類 | 小於或等於 | 為 null 有效 |
@Min | BigDecimal、BigInteger,byte、short、int、long 以及包裝類 | 大於或等於 | 為 null 有效 |
@DecimalMax | BigDecimal、BigInteger、CharSequence,byte、short、int、long 以及包裝類 | 小於或等於 | 為 null 有效 |
@DecimalMin | BigDecimal、BigInteger、CharSequence,byte、short、int、long 以及包裝類 | 大於或等於 | 為 null 有效 |
@Negative | BigDecimal、BigInteger,byte、short、int、long、float、double 以及包裝類 | 負數 | 為 null 有效,0 無效 |
@NegativeOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double 以及包裝類 | 負數或零 | 為 null 有效 |
@Positive | BigDecimal、BigInteger,byte、short、int、long、float、double 以及包裝類 | 正數 | 為 null 有效,0 無效 |
@PositiveOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double 以及包裝類 | 正數或零 | 為 null 有效 |
@Digits(integer = 3, fraction = 2) | BigDecimal、BigInteger、CharSequence,byte、short、int、long 以及包裝類 | 整數位數和小數位數上限 | 為 null 有效 |
其他
注解 | 支持 Java 類型 | 說明 | 備注 |
---|---|---|---|
@Pattern | CharSequence | 匹配指定的正則表達式 | 為 null 有效 |
CharSequence | 郵箱地址 | 為 null 有效,默認正則 '.*' |
|
@Size | CharSequence、Collection、Map、Array | 大小范圍(length/size>0) | 為 null 有效 |
hibernate-validator 擴展約束(部分)
注解 | 支持 Java 類型 | 說明 |
---|---|---|
@Length | String | 字符串長度范圍 |
@Range | 數值類型和 String | 指定范圍 |
@URL | URL 地址驗證 |