Как элегантно сделать валидацию данных — подробная инструкция Hibernate Validator

Spring

Проверка данных часто выполняется в обычном процессе кодирования. Может потребоваться реализовать некоторую логику проверки на каждом уровне системы перед выполнением бизнес-обработки. Эти утомительные проверки и наш бизнес-код будут казаться раздутыми вместе. И эти проверки обычно не зависят от бизнеса. Я также использовал 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 ограничения можно применять не только к JavaBeans и их свойствам, но и к параметрам и возвращаемым значениям методов и конструкторов любого типа 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

Аннотация ограничения

Существует 22 аннотации ограничений для validator-api-2.0, подробности см. в следующей таблице.

Пустая и ненулевая проверка

аннотация Поддержка типов Java инструкция
@Null Object нулевой
@NotNull Object не ноль
@NotBlank CharSequence не нулевой и должен иметь непробельный символ
@NotEmpty CharSequence, Коллекция, Карта, Массив Не нулевой и не пустой (длина/размер>0)

проверка логического значения

аннотация Поддержка типов Java инструкция Примечание
@AssertTrue логическое значение, логическое значение истинный действителен для нуля
@AssertFalse логическое значение, логическое значение ложный действителен для нуля

проверка даты

аннотация Поддержка типов Java инструкция Примечание
@Future Дата, Календарь, Мгновенное, LocalDate, LocalDateTime, LocalTime, MonthDay, OffsetDateTime, OffsetTime, Year, YearMonth, ZonedDateTime, HijrahDate, JapaneseDate, MinguoDate, ThaiBuddhistDate Убедитесь, что дата более поздняя, ​​чем текущее время. действителен для нуля
@FutureOrPresent Дата, Календарь, Мгновенное, LocalDate, LocalDateTime, LocalTime, MonthDay, OffsetDateTime, OffsetTime, Year, YearMonth, ZonedDateTime, HijrahDate, JapaneseDate, MinguoDate, ThaiBuddhistDate Подтвердите, что дата является текущим временем или более поздней действителен для нуля
@Past Дата, Календарь, Мгновенное, LocalDate, LocalDateTime, LocalTime, MonthDay, OffsetDateTime, OffsetTime, Year, YearMonth, ZonedDateTime, HijrahDate, JapaneseDate, MinguoDate, ThaiBuddhistDate Убедитесь, что дата предшествует текущему времени действителен для нуля
@PastOrPresent Дата, Календарь, Мгновенное, LocalDate, LocalDateTime, LocalTime, MonthDay, OffsetDateTime, OffsetTime, Year, YearMonth, ZonedDateTime, HijrahDate, JapaneseDate, MinguoDate, ThaiBuddhistDate Подтвердите, что дата является текущим временем или раньше действителен для нуля

Численная проверка

аннотация Поддержка типов Java инструкция Примечание
@Max BigDecimal, BigInteger, byte, short, int, long и классы-оболочки меньше или равно действителен для нуля
@Min BigDecimal, BigInteger, byte, short, int, long и классы-оболочки больше или равно действителен для нуля
@DecimalMax BigDecimal, BigInteger, CharSequence, byte, short, int, long и классы-оболочки меньше или равно действителен для нуля
@DecimalMin BigDecimal, BigInteger, CharSequence, byte, short, int, long и классы-оболочки больше или равно действителен для нуля
@Negative BigDecimal, BigInteger, byte, short, int, long, float, double и классы-оболочки отрицательное число ноль действителен, 0 недействителен
@NegativeOrZero BigDecimal, BigInteger, byte, short, int, long, float, double и классы-оболочки отрицательный или нулевой действителен для нуля
@Positive BigDecimal, BigInteger, byte, short, int, long, float, double и классы-оболочки Положительное число ноль действителен, 0 недействителен
@PositiveOrZero BigDecimal, BigInteger, byte, short, int, long, float, double и классы-оболочки положительный или нулевой действителен для нуля
@Digits(integer = 3, fraction = 2) BigDecimal, BigInteger, CharSequence, byte, short, int, long и классы-оболочки Целые и десятичные разряды действителен для нуля

разное

аннотация Поддержка типов Java инструкция Примечание
@Pattern CharSequence соответствует указанному регулярному выражению действителен для нуля
@Email CharSequence адрес электронной почты Действителен для нуля, по умолчанию обычный'.*'
@Size CharSequence, Коллекция, Карта, Массив Размерный ряд (длина/размер>0) действителен для нуля

Ограничения расширения Hibernate-Validator (частичные)

аннотация Поддержка типов Java инструкция
@Length String Диапазон длины строки
@Range Числовые типы и строка указанный диапазон
@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Сообщение об ошибке, вы можете написать его до смерти, или вы можете заполнить интернационализированный ключ
  • groupsИнформация о группировке, позволяющая указать группу валидации, к которой принадлежит это ограничение (ограничения группировки будут рассмотрены ниже)
  • 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 Validation рекомендует рассматривать нулевые значения как допустимые. если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Группа будет проверена, и будет возвращено сообщение об ошибке, а следующее ограничениеViolations2 не будет проверено, поскольку будет использоваться значение по умолчанию.Defaultгруппировка. Если вы хотите снять отметку с группы, она также проверитDefaultГруппировка, вы можете унаследовать группировку по умолчанию

public interface AddGroup extends Default {
}

Использование Hibernate Validator с Spring

Вышеприведенное представляет некоторые варианты использования Validator, а также введение аннотаций, так как же мы можем использовать Hibernate Validator для проверки в Spring? Или как использовать 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>

проверка компонента запроса параметра

Проверка бина на интерфейсе должна быть добавлена ​​перед параметром@Validили весна@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();
    }
}

Сотрудничайте с ВеснойBindingResultParameters, мы можем обрабатывать ошибки валидации в контроллере, но обычно мы также конвертируем сообщения об ошибках валидации в свой собственный формат возврата, так что явно нет необходимости делать такую ​​обработку ошибок валидации в каждом методе. Мы можем использовать исключение, когда проверка не может выполнить унифицированную обработку ошибок.

@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.

Мы в конфигурации Валидатора добавляемMethodValidationPostProcessorBean, добавьте конфигурацию в указанный выше ValidatorConfig.java.

/**
 * 设置方法参数验证器
 */
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
    MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
    // 设置validator模式为快速失败返回
    postProcessor.setValidator(validator());
    return postProcessor;
}

Если это Spring Mvc, то информация о bean-компоненте должна быть объявлена ​​в spring-mvc.xml, иначе она будет недействительна в контроллере.

<!-- 设置方法参数验证器 -->
<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());
}

группировка

Весна@ValidateАннотации могут поддерживать групповую проверку

@PostMapping("/user")
public R handle(@Validated(AddGroup.class) @RequestBody User user) {
    // ...
    return R.success();
}