如何優雅的做數據校驗-Hibernate Validator詳細使用說明


數據校驗是在平時的編碼過程中常做的工作,在系統的各個層可能都要去實現一些校驗邏輯,再去做業務處理。這些繁瑣的校驗與我們的業務代碼在一塊就會顯得臃腫。而且這些校驗通常是業務無關的。也是在工作中使用到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>

 

bean約束聲明和驗證,Validator

先給我們的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有效
@Email 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 錯誤提示信息,可以寫死,也可以填寫國際化的key
  • groups 分組信息,允許指定此約束所屬的驗證組(下面會說到分組約束)
  • 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

分組是用接口定義的,用做標識,這里創建兩個標識AddGroupUpdateGroup,分別標識新增和修改

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)響應;或者可以通過ErrorsBindingResult參數在控制器內本地處理驗證錯誤。另外,如果參數前有@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的配置中,添加MethodValidationPostProcessorBean,在上面的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
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM