數據校驗是在平時的編碼過程中常做的工作,在系統的各個層可能都要去實現一些校驗邏輯,再去做業務處理。這些繁瑣的校驗與我們的業務代碼在一塊就會顯得臃腫。而且這些校驗通常是業務無關的。也是在工作中使用到Hibernate Validator,但卻發現有人沒有使用好它(竟然還能看到一些if else的校驗代碼...),所以在這里決定整理下關於Hibernate Validator的使用
Bean Validation 2.0(JSR 380)定義了用於實體和方法驗證的元數據模型和API,Hibernate Validator是目前最好的實現,這篇主要是說Hibernate Validator的使用
Hibernate Validator的使用
依賴
如果是Spring Boot項目,那么spring-boot-starter-web
中就已經依賴hibernate-validator
了
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
如果是Spring Mvc,那可以直接添加hibernate-validator
依賴
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.17.Final</version> </dependency>
先給我們的Java對象添加約束注解
@Data @AllArgsConstructor public class User { private String id; @NotBlank @Size(max = 20) private String name; @NotNull @Pattern(regexp = "[A-Z][a-z][0-9]") private String password; @NotNull private Integer age; @Max(10) @Min(1) private Integer level; }
驗證實體實例需要先獲取Validator
實例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
Validator
接口有三個方法,可用於驗證整個實體或僅驗證實體的單個屬性
Validator#validate()
驗證所有bean的所有約束Validator#validateProperty()
驗證單個屬性Validator#validateValue()
檢查給定類的單個屬性是否可以成功驗證
public class UserTest { private static Validator validator; @BeforeAll public static void setUpValidator() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); } @Test public void validatorTest() { User user = new User(null, "", "!@#$", null, 11); // 驗證所有bean的所有約束 Set<ConstraintViolation<User>> constraintViolations = validator.validate(user); // 驗證單個屬性 Set<ConstraintViolation<User>> constraintViolations2 = validator.validateProperty(user, "name"); // 檢查給定類的單個屬性是否可以成功驗證 Set<ConstraintViolation<User>> constraintViolations3 = validator.validateValue(User.class, "password", "sa!"); constraintViolations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations2.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations3.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); } }
測試結果
不能為空 最大不能超過10 需要匹配正則表達式"[A-Z][a-z][0-9]" 不能為null 不能為空 需要匹配正則表達式"[A-Z][a-z][0-9]"
方法約束聲明和驗證,ExecutableValidator
從Bean Validation 1.1開始,約束不僅可以應用於JavaBean及其屬性,而且可以應用於任何Java類型的方法和構造函數的參數和返回值,這里簡單看一個例子
public class RentalStation { public RentalStation(@NotNull String name) { //... } public void rentCar(@NotNull @Future LocalDate startDate, @Min(1) int durationInDays) { //... } @NotNull @Size(min = 1) public List<@NotNull String> getCustomers() { //... return null; } }
ExecutableValidator
接口可以完成方法約束的驗證
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
executableValidator = factory.getValidator().forExecutables();
該ExecutableValidator
界面共有四種方法:
validateParameters()和validateReturnValue()用於方法驗證
validateConstructorParameters()和validateConstructorReturnValue()用於構造函數驗證
public class RentalStationTest { private static ExecutableValidator executableValidator; @BeforeAll public static void setUpValidator() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); executableValidator = factory.getValidator().forExecutables(); } @Test public void validatorTest() throws NoSuchMethodException { RentalStation rentalStation = new RentalStation("z"); Method method = RentalStation.class.getMethod("rentCar", LocalDate.class, int.class); Object[] parameterValues = {LocalDate.now().minusDays(1), 0}; Set<ConstraintViolation<RentalStation>> violations = executableValidator.validateParameters( rentalStation, method, parameterValues); violations.forEach(violation -> System.out.println(violation.getMessage())); } }
測試結果
需要是一個將來的時間
最小不能小於1
約束注解
validator-api-2.0的約束注解有22個,具體我們看下面表格
空與非空檢查
注解 | 支持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地址驗證 |
自定義約束注解
除了以上提供的約束注解(大部分情況都是能夠滿足的),我們還可以根據自己的需求自定義自己的約束注解
定義自定義約束,有三個步驟
- 創建約束注解
- 實現一個驗證器
- 定義默認的錯誤信息
那么下面就直接來定義一個簡單的驗證手機號碼的注解
@Documented @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Constraint(validatedBy = {MobileValidator.class}) @Retention(RUNTIME) @Repeatable(Mobile.List.class) public @interface Mobile { /** * 錯誤提示信息,可以寫死,也可以填寫國際化的key */ String message() default "手機號碼不正確"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String regexp() default "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$"; @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Documented @interface List { Mobile[] value(); } }
關於注解的配置這里不說了,自定義約束需要下面3個屬性
message
錯誤提示信息,可以寫死,也可以填寫國際化的keygroups
分組信息,允許指定此約束所屬的驗證組(下面會說到分組約束)payload
有效負載,可以通過payload來標記一些需要特殊處理的操作
@Repeatable
注解和List
定義可以讓該注解在同一個位置重復多次,通常是不同的配置(比如不同的分組和消息)
@Constraint(validatedBy = {MobileValidator.class})
該注解是指明我們的自定義約束的驗證器,那下面就看一下驗證器的寫法,需要實現javax.validation.ConstraintValidator
接口
public class MobileValidator implements ConstraintValidator<Mobile, String> { /** * 手機驗證規則 */ private Pattern pattern; @Override public void initialize(Mobile mobile) { pattern = Pattern.compile(mobile.regexp()); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) { return true; } return pattern.matcher(value).matches(); } }
ConstraintValidator
接口定義了在實現中設置的兩個類型參數。第一個指定要驗證的注解類(如Mobile
),第二個指定驗證器可以處理的元素類型(如String
);initialize()
方法可以訪問約束注解的屬性值;isValid()
方法用於驗證,返回true表示驗證通過
Bean驗證規范建議將空值視為有效。如果
null
不是元素的有效值,則應使用@NotNull
顯式注釋
到這里我們自定義的約束就寫好了,可以用個例子來測試一下
public class MobileTest { public void setMobile(@Mobile String mobile){ // to do } private static ExecutableValidator executableValidator; @BeforeAll public static void setUpValidator() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); executableValidator = factory.getValidator().forExecutables(); } @Test public void manufacturerIsNull() throws NoSuchMethodException { MobileTest mobileTest = new MobileTest(); Method method = MobileTest.class.getMethod("setMobile", String.class); Object[] parameterValues = {"1111111"}; Set<ConstraintViolation<MobileTest>> violations = executableValidator.validateParameters( mobileTest, method, parameterValues); violations.forEach(violation -> System.out.println(violation.getMessage())); } }
手機號碼不正確
分組約束
在上面的自定義約束中,有個groups
屬性是用來指定驗證約束的分組,我們在為屬性加上注解的時候,如果沒有配置分組信息,那么默認會采用默認分組 javax.validation.groups.Default
分組是用接口定義的,用做標識,這里創建兩個標識AddGroup
和UpdateGroup
,分別標識新增和修改
public interface AddGroup { } public interface UpdateGroup { }
然后對我們的User
對象的id屬性做分組標識
@Data @NoArgsConstructor @AllArgsConstructor public class User { @Null(groups = AddGroup.class) @NotBlank(groups = UpdateGroup.class) private String id; // ... 省略了其他屬性 }
我們看下如何使用
@Test public void validatorGroupTest() { User user = new User(); // 檢查給定類的單個屬性是否可以成功驗證 Set<ConstraintViolation<User>> constraintViolations = validator.validateValue(User.class, "id", "", UpdateGroup.class); Set<ConstraintViolation<User>> constraintViolations2 = validator.validateValue(User.class, "id", ""); Set<ConstraintViolation<User>> constraintViolations3 = validator.validateValue(User.class, "id", "", AddGroup.class); constraintViolations.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations2.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); constraintViolations3.forEach(constraintViolation -> System.out.println(constraintViolation.getMessage())); }
上面的測試只有加了UpdateGroug
分組才會驗證,返回錯誤信息,而下面的constraintViolations2並不會去驗證,因為默認會采用Default
分組。如果想要不標記分組的時候,也會去驗證Default
分組,可以去繼承默認分組
public interface AddGroup extends Default { }
在Spring中使用Hibernate Validator
上面介紹了Validator的一些使用,還有注解的介紹,那么在Spring中我們怎么去使用Hibernate Validator做驗證呢?或者說再Web項目中怎么使用Hibernate Validator?
spring-boot-starter-web
中是添加了hibernate-validator
依賴的,說明Spring Boot本身也是使用到了Hibernate Validator驗證框架的
配置Validator
@Configuration public class ValidatorConfig { /** * 配置驗證器 * * @return validator */ @Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() // 快速失敗模式 .failFast(true) // .addProperty( "hibernate.validator.fail_fast", "true" ) .buildValidatorFactory(); return validatorFactory.getValidator(); } }
可以通過方法 failFast(true)
或 addProperty("hibernate.validator.fail_fast", "true")
設置為快速失敗模式,快速失敗模式在校驗過程中,當遇到第一個不滿足條件的參數時就立即返回,不再繼續后面參數的校驗。否則會一次性校驗所有參數,並返回所有不符合要求的錯誤信息
如果是Spring MVC的話,需要xml配置可參考下面的配置
<mvc:annotation-driven validator="validator"/> <!-- validator基本配置 --> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/> <!-- 映射資源文件 --> <property name="validationMessageSource" ref="messageSource"/> </bean> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource" name="messageSource"> <!--<property name="basenames"> <list> <value>classpath:messages/messages</value> <value>classpath:messages/ValidationMessages</value> </list> </property>--> <property name="useCodeAsDefaultMessage" value="false" /> <property name="defaultEncoding" value="UTF-8" /> <property name="cacheSeconds" value="60" /> </bean>
請求參數bean驗證
接口上的Bean驗證,需要在參數前加上@Valid
或Spring的 @Validated
注解,這兩種注釋都會導致應用標准Bean驗證。如果驗證不通過會拋出BindException
異常,並變成400(BAD_REQUEST)響應;或者可以通過Errors
或BindingResult
參數在控制器內本地處理驗證錯誤。另外,如果參數前有@RequestBody
注解,驗證錯誤會拋出MethodArgumentNotValidException
異常。
@RestController public class UserController { @PostMapping("/user") public R handle(@Valid @RequestBody User user, BindingResult result) { // 在控制器內本地處理驗證錯誤 if (result.hasErrors()) { result.getAllErrors().forEach(s -> System.out.println(s.getDefaultMessage())); return R.fail(result.getAllErrors().get(0).getDefaultMessage()); } // ... return R.success(); } @PostMapping("/user2") public R handle2(@Valid User user, BindingResult result) { // 在控制器內本地處理驗證錯誤 if (result.hasErrors()) { result.getAllErrors().forEach(s -> System.out.println(s.getDefaultMessage())); return R.fail(result.getAllErrors().get(0).getDefaultMessage()); } // ... return R.success(); } /** * 驗證不通過拋出 `MethodArgumentNotValidException` */ @PostMapping("/user3") public R handle3(@Valid @RequestBody User user) { // ... return R.success(); } /** * 驗證不通過拋出 `BindException` */ @PostMapping("/user4") public R handle4(@Valid User user) { // ... return R.success(); } }
配合Spring的BindingResult
參數,我們是可以在控制器中去處理驗證錯誤,不過通常也是把驗證錯誤的消息轉成我們自己的返回格式,那么在每個方法中都去做這樣的驗證錯誤處理,顯然是沒有必要的。我們可以利用驗證不通過的異常來做統一的錯誤處理
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * hibernate validator 數據綁定驗證異常攔截 * * @param e 綁定驗證異常 * @return 錯誤返回消息 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException.class) public R validateErrorHandler(BindException e) { ObjectError error = e.getAllErrors().get(0); log.info("數據驗證異常:{}", error.getDefaultMessage()); return R.fail(error.getDefaultMessage()); } /** * hibernate validator 數據綁定驗證異常攔截 * * @param e 綁定驗證異常 * @return 錯誤返回消息 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) public R validateErrorHandler(MethodArgumentNotValidException e) { ObjectError error = e.getBindingResult().getAllErrors().get(0); log.info("數據驗證異常:{}", error.getDefaultMessage()); return R.fail(error.getDefaultMessage()); } }
方法參數驗證
配置
Hibernate Validator是可以在方法級驗證參數的,Spring中當然也是有實現的。
我們在Validator的配置中,添加MethodValidationPostProcessor
Bean,在上面的ValidatorConfig.java中添加一下配置
/** * 設置方法參數驗證器 */ @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); // 設置validator模式為快速失敗返回 postProcessor.setValidator(validator()); return postProcessor; }
如果是Spring Mvc,那么要在spring-mvc.xml中聲明bean信息,不然在Controller里面是無效的
<!-- 設置方法參數驗證器 --> <bean id="methodValidationPostProcessor" class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"> <property name="validator" ref="validator"/> </bean>
使用
配置了上面的MethodValidationPostProcessor
,我們就可以在方法參數或返回值使用約束注解了,要注意的是,在要使用參數驗證的類上一定要加上@Validated
注解,否則無效
/** * 一定要加上 `@Validated` 注解 */ @Validated @RestController public class UserController { @GetMapping("/user") public R handle(@Mobile String mobile) { // ... return R.success(); } }
如果驗證不通過,會拋出ConstraintViolationException
異常,同樣的,我們可以在全局的異常處理器里面處理驗證錯誤,在GlobalExceptionHandler中添加一下代碼
/** * spring validator 方法參數驗證異常攔截 * * @param e 綁定驗證異常 * @return 錯誤返回消息 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(ConstraintViolationException.class) public R defaultErrorHandler(ConstraintViolationException e) { Set<ConstraintViolation<?>> violations = e.getConstraintViolations(); ConstraintViolation<?> violation = violations.iterator().next(); log.info("數據驗證異常:{}", violation.getMessage()); return R.fail(violation.getMessage()); }
分組
Spring的@Validate
注解是可以支持分組驗證的
@PostMapping("/user") public R handle(@Validated(AddGroup.class) @RequestBody User user) { // ... return R.success(); }
作者:TurboSnail
鏈接:https://www.jianshu.com/p/3267689ebf1b
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。