數據校驗是在平時的編碼過程當中常作的工做,在系統的各個層可能都要去實現一些校驗邏輯,再去作業務處理。這些繁瑣的校驗與咱們的業務代碼在一塊就會顯得臃腫。並且這些校驗一般是業務無關的。也是在工做中使用到Hibernate Validator,但卻發現有人沒有使用好它(居然還能看到一些if else的校驗代碼...),因此在這裏決定整理下關於Hibernate Validator的使用java
Bean Validation 2.0(JSR 380)定義了用於實體和方法驗證的元數據模型和API,Hibernate Validator是目前最好的實現,這篇主要是說Hibernate Validator的使用git
若是是Spring Boot項目,那麼spring-boot-starter-web
中就已經依賴hibernate-validator
了web
<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對象添加約束註解spring
@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
實例api
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
複製代碼
Validator
接口有三個方法,可用於驗證整個實體或僅驗證明體的單個屬性spring-mvc
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()));
}
}
複製代碼
測試結果bash
不能爲空
最大不能超過10
須要匹配正則表達式"[A-Z][a-z][0-9]"
不能爲null
不能爲空
須要匹配正則表達式"[A-Z][a-z][0-9]"
複製代碼
從Bean Validation 1.1開始,約束不只能夠應用於JavaBean及其屬性,並且能夠應用於任何Java類型的方法和構造函數的參數和返回值,這裏簡單看一個例子mvc
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
接口能夠完成方法約束的驗證app
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) |
註解 | 支持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有效 |
註解 | 支持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 {
}
複製代碼
上面介紹了Validator的一些使用,還有註解的介紹,那麼在Spring中咱們怎麼去使用Hibernate Validator作驗證呢?或者說再Web項目中怎麼使用Hibernate Validator?
spring-boot-starter-web
中是添加了hibernate-validator
依賴的,說明Spring Boot自己也是使用到了Hibernate 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驗證,須要在參數前加上@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();
}
複製代碼