【SpringBoot】@Validated @Valid 参数校验概述以及使用方式

1  前言

最近在思考 SpringBoot 中的参数校验,比如我们写一段业务代码,首要的就是校验参数,单据编码空不空,数量空不空,客户空不空等,最简单的就是单独抽个方法逐个进行 if else 校验,高级点的整个校验工厂,当需要校验某种业务的时候,拿到校验器来校验,可以是简单工厂或者工厂方法都可以实现。SpringBoot 也提供了一个参数校验的注解供我们使用,可以对 Bean 中的方法参数进行校验 @Validated 以及搭配的还有 @Valid,平时可能大家都用过,我们本节就看看它的使用方式以及背后的原理。

2  准备工作

我们先准备两个基本类,一个是客户类,一个是客户地址类,用于后续的测试:

客户类:

@Data
public class Customer {

    /**
     * 客户编码
     */
    @NotBlank(groups = Add.class)
    private String code;
    /**
     * 客户名称 varchar
     */
    @NotBlank(groups = Add.class)
    private String name;
    /**
     * 客户地址
     */
    @NotEmpty(groups = Add.class)
    private List<Address> addressList;

}

客户地址类:

@Data
public class Address {

    /**
     * 省
     */
    @NotBlank(groups = Add.class)
    private String province;
    /**
     * 市
     */
    private String city;
    /**
     * 区
     */
    private String district;
    /**
     * 详细地址
     */
    private String detail;

}

一个 Test 类:

public class TestValidator {

    @SneakyThrows
    public static void main(String[] args) {
        // 获取校验器工厂
        ValidatorFactory factory = Validation
                .byProvider(HibernateValidator.class)
                .configure()
                // 快速失败 用于发现一个不符合的直接退出
//                .failFast(true)
                .buildValidatorFactory();
        // 获取校验器
        Validator validator = factory.getValidator();
        // 准备参数信息
        Customer customer = new Customer();
        customer.setAddressList(Lists.newArrayList(new Address()));
        // 预热 后续验证预热不预热影响多大
        validator.getConstraintsForClass(Customer.class);
        // 参数校验
        Set<ConstraintViolation<Customer>> validate = validator.validate(customer, Add.class);
        // 打印校验结果
        printConstraintViolation(validate);
    }

    private static void printConstraintViolation(Set<ConstraintViolation<Customer>> validate) {
        if (CollectionUtils.isEmpty(validate)) return;
        for (ConstraintViolation<Customer> constraintViolation : validate) {
            System.out.println(String.format("属性:%s,原因:%s", constraintViolation.getPropertyPath(), constraintViolation.getMessage()));
        }
    }
}

3  基础认识

3.1  概述

首先我们先看下 @Validated 以及 @Valid:

@Validated 是 Spring 框架中的一个注解,它是JSR-303规范的扩展,可以用于在方法级别上校验方法参数,它的主要特点是分组验证。

/**
 * Variant of JSR-303's {@link javax.validation.Valid}, supporting the
 * specification of validation groups. Designed for convenient use with
 * Spring's JSR-303 support but not JSR-303 specific.
 * @author Juergen Hoeller
 * @since 3.1
 * @see javax.validation.Validator#validate(Object, Class[])
 * @see org.springframework.validation.SmartValidator#validate(Object, org.springframework.validation.Errors, Object...)
 * @see org.springframework.validation.beanvalidation.SpringValidatorAdapter
 * @see org.springframework.validation.beanvalidation.MethodValidationPostProcessor
 */
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {

    Class<?>[] value() default {};

}

@Valid 是 Java 扩展包中的一个注解,也是Java Bean Validation(JSR 303/JSR 380)框架中的一个注解,用于触发对象的验证过程。这个注解通常用于方法的参数上,表示在方法执行之前应该先验证该参数是否符合定义的约束条件。

@Valid注解的作用是:

  1. 触发验证:当一个对象被标记为@Valid时,框架会自动对该对象进行验证,确保它符合所有定义的约束条件。
  2. 嵌套验证:对于复杂的数据结构(如包含其他对象的对象),@Valid可以递归地进行验证,确保整个数据结构都是有效的。
/**
 * Marks a property, method parameter or method return type for validation cascading.
 * <p>
 * Constraints defined on the object and its properties are be validated when the
 * property, method parameter or method return type is validated.
 * <p>
 * This behavior is applied recursively.
 *
 * @author Emmanuel Bernard
 * @author Hardy Ferentschik
 */
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
}

小疑惑:

JSR-303 是什么?

JSR 303(Java Specification Request 303)是Java Bean Validation(Java Bean验证)的规范,也称为Bean Validation。它定义了一套用于验证Java对象(特别是Java Bean)的有效性的标准约束注解和API。JSR 303允许开发者在Java Bean的属性上标注特定的注解来指定验证规则,并通过标准的验证API来执行这些规则。

主要特点

  1. 标准注解:

    • JSR 303定义了一组标准的注解,如@NotNull@Size@Min@Max等,用于在Java Bean的属性上声明验证规则。
    • 这些注解可以应用于字段、getter方法和setter方法。
  2. 可扩展性:

    • JSR 303允许开发者定义自定义的注解和验证逻辑,从而支持更加复杂的验证需求。
  3. 集成支持:

    • 许多流行的Java框架(如Spring、Hibernate等)都支持Bean Validation,使得验证逻辑可以很容易地集成到现有的应用中。

为什么 SpringBoot 包中在看校验器的时候总有个 hibernate 相关的?

在Spring Boot项目中,通常会看到hibernate-validator这个依赖包,这是因为hibernate-validator是Java Bean Validation(JSR 303/JSR 380)的一个实现。它提供了@Valid@Validated等注解的功能,用于验证Java对象的有效性。

为什么Spring Boot会包含hibernate-validator?

  1. 默认依赖:

    • Spring Boot在创建项目时,默认包含了hibernate-validator作为Bean Validation的实现。这是因为Spring Boot的目标是简化项目的搭建过程,提供一套开箱即用的解决方案。
    • 在Spring Boot的starters中,例如spring-boot-starter-web,已经包含了hibernate-validator,以确保项目能够方便地使用验证功能。
  2. 集成Spring MVC:

    • hibernate-validator与Spring MVC框架紧密集成,可以方便地用于验证HTTP请求中的参数。
    • 当你在Spring MVC控制器中使用@Valid@Validated注解时,hibernate-validator会被用来验证请求参数的有效性。
  3. 统一的验证机制:

    • 使用hibernate-validator可以为项目提供一致的验证机制,使得验证逻辑更容易维护和扩展。
    • hibernate-validator支持自定义注解,可以方便地扩展标准的验证规则,满足项目中的特殊需求。
  4. 社区支持和成熟度:

    • hibernate-validator是广泛使用的Bean Validation实现之一,拥有成熟的社区支持和丰富的文档资源。
    • 使用hibernate-validator可以降低因使用不成熟或较少支持的验证库带来的风险。

hibernate-validator是Spring Boot默认包含的Bean Validation实现,它提供了便捷的验证功能,并且与Spring MVC紧密集成。如果你需要使用不同的验证库,可以通过排除依赖并添加新的验证库来实现。根据项目的具体需求选择合适的验证库可以提高代码的可维护性和可扩展性。

Validator 接口中的 forExecutables 是干什么的?

Validator接口中的ExecutableValidator forExecutables()方法用于获取一个专门用于验证方法和构造函数的ExecutableValidator实例。在Java Bean Validation中,除了验证对象的属性外,还可以验证方法和构造函数的参数。ExecutableValidator提供了这样的功能。

ExecutableValidator的作用

  1. 方法参数验证:

    • ExecutableValidator可以用来验证方法的参数。
    • 当一个方法被@Valid@Validated注解标记时,ExecutableValidator会验证该方法的参数是否符合定义的约束。
  2. 构造函数参数验证:

    • ExecutableValidator同样可以用来验证构造函数的参数。
    • 这对于确保对象在创建时就处于有效状态非常有用。

3.2  @Valid和@Validated比较

@Valid@Validated 都是用于触发验证的注解,但它们在使用场景和作用上有一定的区别。

@Valid 注解主要用于单个对象的验证。它通常用在方法参数、方法返回值或字段上,表示在方法执行前需要验证该对象是否符合定义的约束条件。

特点:

  • 单个对象验证:适用于单一对象的验证。
  • 嵌套验证:如果对象中有其他对象,@Valid 会递归地验证这些对象。
  • 独立使用:可以独立使用,不需要额外的框架支持。

@Validated 注解则是Spring框架提供的,用于在Spring MVC或Spring WebFlux中对控制器方法参数进行验证。它通常用于整个方法级别的验证,可以结合Spring的AOP功能来实现。

特点:

  • 方法级别的验证:用于整个方法的参数验证。
  • Spring框架支持:需要Spring框架的支持,通常用于Spring MVC或Spring WebFlux中。
  • 组合使用:可以与@Valid一起使用,增强验证逻辑。
  • 分组验证

区别:

  1. 使用场景:

    • @Valid:适用于单一对象的验证,可以独立使用。
    • @Validated:适用于方法级别的验证,通常用于Spring MVC或Spring WebFlux中。
  2. 验证范围:

    • @Valid:只对单个对象进行验证。
    • @Validated:对整个方法的参数进行验证。
  3. 框架支持:

    • @Valid:不需要特定框架支持,可以直接在Java Bean上使用。
    • @Validated:需要Spring框架的支持,通常与Spring MVC或Spring WebFlux一起使用。
  4. 组合使用:

    • @Valid 可以单独使用,也可以与 @Validated 结合使用,增强验证逻辑。
    • @Validated 则通常与 @Valid 一起使用,确保方法的所有参数都经过验证。

实际使用建议

  • 如果你只需要对单个对象进行验证,可以使用 @Valid
  • 如果你需要对整个方法的参数进行验证,并且你已经在使用Spring框架,可以考虑使用 @Validated

其实只需要记住,@Validated 是 Spring 的注解所以搭配 Spring 框架以及 AOP 做参数校验,其最大的特点是分组验证。@Valid 是java扩展包注解依赖小,最大的特点是支持嵌套校验。

下边使用的时候,会演示什么是分组校验,什么是嵌套校验。

3.3  常用的校验注解

3.4  @Valid 相关类体系

@Valid涉及到的类体系主要与Java Bean Validation API(JSR 303/JSR 380)以及其实现库(如Hibernate Validator)相关。我们这里说一下相关的几个核心类和接口。

@Constraint 注解,是一个标记注解,用于标识一个注解可以作为验证约束,比如 @NotNull @Max @Min等注解都是依赖或者继承的它。

@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {

    /**
     * 该标记需要用哪个类来解析验证 借助于 ConstraintValidator
     * 
     */
    Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}

ConstraintValidator 是一个接口,用于定义如何验证一个约束注解,这个其实就是验证上边的某个注解。比如 AbstractMaxValidator 就是验证 @Max的底座,AbstractMinValidator 就是验证 @Min的底座等。

public interface ConstraintValidator<A extends Annotation, T> {

    /**
     * 初始化
     */
    default void initialize(A constraintAnnotation) {
    }

    /**
     * 该方法用于验证约束是否有效
     */
    boolean isValid(T value, ConstraintValidatorContext context);
}
public abstract class AbstractMaxValidator<T> implements ConstraintValidator<Max, T> {
public abstract class AbstractMinValidator<T> implements ConstraintValidator<Min, T> {

Validator 接口定义了验证对象的方法,如validate等,它是验证逻辑的核心,负责执行具体的验证操作。也就是我上边定义了要验证什么@Constraint 以及如何验证 ConstraintValidator,那么该如何使用呢?Validator 就是入口,用它来开启校验。

ValidatorFactory Validator是个接口我们不能直接使用,那么如何拿到它的实现呢?就借助 ValidatorFactory,这个接口负责创建Validator实例,通常通过Validation.buildDefaultValidatorFactory()方法获取ValidatorFactory实例。

ValidatorImpl 是Validator的一个具体实现类,通常由ValidatorFactory创建,它实现了Validator接口定义的方法,负责具体的验证逻辑。

ConstraintContext 是一个接口,提供了验证时的上下文信息,用于传递验证过程中的一些必要信息,比如都可以解析哪些约束,约束对应的校验器是谁等。

我们这里画个图捋一下:

3.5  @Validated 相关类体系

ValidationAutoConfiguration 校验的自动装配,它引进了 PrimaryDefaultValidatorPostProcessor、LocalValidatorFactoryBean、MethodValidationPostProcessor。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    @ConditionalOnMissingBean(Validator.class)
    public static LocalValidatorFactoryBean defaultValidator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        return factoryBean;
    }

    @Bean
    @ConditionalOnMissingBean
    public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
            @Lazy Validator validator) {
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
        processor.setProxyTargetClass(proxyTargetClass);
        processor.setValidator(validator);
        return processor;
    }

}

至于 AOP 如何增强,具体执行过程我们在看原理的时候再细看。

4  使用用法

4.1  分组校验

新增时校验 name,更新时校验 code 和 name:

@Data
public class Customer {

    /**
     * 客户编码
     */
    @NotBlank(groups = Modify.class)
    private String code;
    /**
     * 客户名称 varchar
     */
    @NotBlank(groups = Add.class)
    private String name;
}

测试校验新增 Add 场景:

public class TestValidator {

    @SneakyThrows
    public static void main(String[] args) {
        // 获取校验器工厂
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        // 获取校验器
        Validator validator = factory.getValidator();
        // 准备参数信息
        Customer customer = new Customer();
        // 预热
        validator.getConstraintsForClass(Customer.class);
        // 参数校验 校验 Add 新增场景
        Set<ConstraintViolation<Customer>> validate = validator.validate(customer, Add.class);
        // 打印校验结果
        printConstraintViolation(validate);
    }

    private static void printConstraintViolation(Set<ConstraintViolation<Customer>> validate) {
        if (CollectionUtils.isEmpty(validate)) return;
        for (ConstraintViolation<Customer> constraintViolation : validate) {
            System.out.println(String.format("属性:%s,原因:%s", constraintViolation.getPropertyPath(), constraintViolation.getMessage()));
        }
    }
}

测试更新:

// 参数校验 校验 Modify 更新场景
Set<ConstraintViolation<Customer>> validate = validator.validate(customer, Modify.class);

4.2  嵌套校验

比如这里的客户地址的校验:

@Data
public class Customer {

    /**
     * 客户编码
     */
    @NotBlank(groups = Modify.class)
    private String code;

    /**
     * 客户地址 嵌套校验 客户地址的校验
     */
    @Valid
    @NotEmpty(groups = Add.class)
    private List<Address> addressList;

}
@Data
public class Address {

    /**
     * 省
     */
    @NotBlank(groups = Add.class, message = "省信息不能为空")
    private String province;
    /**
     * 市
     */
    private String city;
    /**
     * 详细地址
     */
    private String detail;
}
public class TestValidator {

    @SneakyThrows
    public static void main(String[] args) {
        // 获取校验器工厂
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        // 获取校验器
        Validator validator = factory.getValidator();
        // 准备参数信息
        Customer customer = new Customer();
        customer.setAddressList(Lists.newArrayList(new Address()));
        // 预热 后续验证预热不预热影响多大
        validator.getConstraintsForClass(Customer.class);
        // 参数校验
        Set<ConstraintViolation<Customer>> validate = validator.validate(customer, Add.class);
        // 打印校验结果
        printConstraintViolation(validate);
    }

    private static void printConstraintViolation(Set<ConstraintViolation<Customer>> validate) {
        if (CollectionUtils.isEmpty(validate)) return;
        for (ConstraintViolation<Customer> constraintViolation : validate) {
            System.out.println(String.format("属性:%s,原因:%s", constraintViolation.getPropertyPath(), constraintViolation.getMessage()));
        }
    }
}

4.3  自定义校验

首先定义一个自定义的注解标识:

@Target({ElementType.METHOD, ElementType.FIELD,
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
        ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 这里我们指定自己的校验器
@Constraint(validatedBy = { MyConstraintAddressConstraintValidator.class })
public @interface MyConstraintAddress {

    String message() default "自定义校验地址不合法";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

}

自定义的校验器:

public class MyConstraintAddressConstraintValidator implements ConstraintValidator<MyConstraintAddress, List<Address>> {

    @Override
    public boolean isValid(List<Address> address, ConstraintValidatorContext context) {
        // 如果地址为空 直接 false
        if (CollectionUtils.isEmpty(address)) return false;

        return true;
    }
}

4.4  快速失败设置

快速失败即当发现某个属性校验失败的时候,就结束校验,比如这里的两个属性,当开启快速失败的时候,有一个属性为空,就结束了:

@Data
public class Customer {

    /**
     * 客户编码
     */
    @NotBlank(groups = Modify.class)
    private String code;

    /**
     * 客户名称
     */
    @NotBlank(groups = Modify.class)
    private String name;

}

开启快速失败并测试:

public class TestValidator {

    @SneakyThrows
    public static void main(String[] args) {
        // 获取校验器工厂
        ValidatorFactory factory = Validation
                .byProvider(HibernateValidator.class)
                .configure()
                // 开启快速失败
                .failFast(true)
                .buildValidatorFactory();
        // 获取校验器
        Validator validator = factory.getValidator();
        // 准备参数信息
        Customer customer = new Customer();
        // 预热
        validator.getConstraintsForClass(Customer.class);
        // 参数校验
        Set<ConstraintViolation<Customer>> validate = validator.validate(customer, Modify.class);
        // 打印校验结果
        printConstraintViolation(validate);
    }

    private static void printConstraintViolation(Set<ConstraintViolation<Customer>> validate) {
        if (CollectionUtils.isEmpty(validate)) return;
        for (ConstraintViolation<Customer> constraintViolation : validate) {
            System.out.println(String.format("属性:%s,原因:%s", constraintViolation.getPropertyPath(), constraintViolation.getMessage()));
        }
    }
}

开启方式有两种:

// 开启方式一
ValidatorFactory factory = Validation
        .byDefaultProvider()
        .configure()
        .addProperty("hibernate.validator.fail_fast", "true")
        .buildValidatorFactory();
// 开启方式二
ValidatorFactory factory = Validation
        .byProvider(HibernateValidator.class)
        .configure()
        .failFast(true)
        .buildValidatorFactory();

本节篇幅有点长了,再看原理的话,内容更多了,我这里放到下一篇,多的话容易疲劳= =。

5  小结

好啦,本节我们就看到这里,主要是看了一下 @Valid 和 @Validated 相关的一些主要作用类,以及一些使用方法,有理解不对的地方欢迎指正哈。

posted @ 2024-09-24 16:00  酷酷-  阅读(1255)  评论(0编辑  收藏  举报