spring-boot-starter-validation

542 阅读8分钟

官网

  • docs.jboss.org/hibernate/v…
    • 超清晰
  • 测试:github.com/hibernate/h…
  • JSR-303
    • 早在JavaEE6规范中就定义了参数校验的规范,它就是JSR-303,它定义了Bean Validation,即对bean属性进行校验。
    • SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底层使用Hibernate Validator,Hibernate Validator是Bean Validation 的参考实现。

为什么使用?

  1. 优点
    • 可方便修改校验(对于值的特定校验,比如email,对于业务的校验,比如存在userId,唯一email)
  2. 对于性能来说,我不知道有没有损失,但是应该差不多

依赖

  • spring-boot-starter-validation就是直接使用hibernate-validator的
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

@Valid和@Validated

  • 相同使用:校验bean,校验方法参数
  • 在Spring开启方法AOP校验:@Validated注解在类上,@Validated或者@Valid让方法启用校验
  • 区别
    • @Validated有分组参数
    • 当在Spring把@Validated注解在类上,即使用方法参数校验时,最好用@Validated来级联bean属性校验
      • 原因看MethodValidationInterceptor的bean校验部分

spring的validation实现

  • 方法参数和方法返回值
    • MethodValidationInterceptor
      • AOP:因为是AOP,因此会对类代理,而想要让类被代理,就必须注解@Validated
        • 因此要支持方法参数和方法返回值就必须在类上注解@Validated
      • bean校验
        • @Valid + 有body的比如JSON
          • 导致进行了2次bean校验
          • 因为HandlerMethodArgumentResolver(如RequestResponseBodyMethodProcessor)也会自动进行校验
          • 解决方法:使用@Validated而不是@Valid
            • 因为hibernate只认得自己的@Valid注解,使得方法校验不会进行bean校验
            • RequestResponseBodyMethodProcessor认得@Validated,直接让其进行bean校验
  • MethodArgumentResolver会自动进行校验
    • 目前有:ModelAttributeMethodProcessor``RequestPartMethodArgumentResolver``RequestResponseBodyMethodProcessor
    • 都使用:org.springframework.validation.DataBinder#validate
      • 都有一个差不多的validateIfApplicable方法进行遍历参数注解进行调用
    • 分组:根据方法参数上的注解
      • javax.validation.Valid无分组
      • @Validated或者其他Valid注解的value属性
  • 异常
    • BindExceptionModelAttributeMethodProcessor抛出,这个是最后一个ArgumentResolver,有@ModelAttribute或者无@RequestBody的bean对象
    • ConstraintViolationExceptionMethodValidationInterceptor抛出,即普通参数,基本类/集合
    • MethodArgumentNotValidExceptionRequestResponseBodyMethodProcessor/RequestPartMethodArgumentResolver抛出
    • 为什么path/其他的不用检查
      • 比如path, 如果参数类型是Long,传字符串,convertService就会抛出MethodArgumentTypeMismatchException
  • 都是使用javax.validation.Validator
    • 用的是org.hibernate.validator.internal.engine.ValidatorImpl

contraint注解

位置

  • field constraints:就是字段
  • property constraints:就是属性(getter、setter方法)
    • 通常用作方法参数校验,而不是属性校验
  • container element constraints:容器元素,但更像是泛型参数
    • List<@NotEmtpy String>
    • A<@NotEmtpy String>需要一个类实现ValueExtractor<A<@ExtractedValue ?>>取出元素值
  • class constraints
  • object graph validation(Cascaded validation级联校验):对象图校验,hibernate确保了不会循环校验,不会校验NULL值
    • object-graph-validation(cascaded validation)
    • 如校验User,User有字段Pet,给Pet加@Valid,这样就会对Pet进行级联校验
      • 原本只是校验User的字段,现在还会校验User的Pet的字段
    • 容器也可以,如List<@NotNull @Valid Person>
      • 原本只是校验元素,现在还会校验元素字段
      • 6.0支持@Valid List<Person>等同于List<@Valid Person>
  • method or constructor的parameters:
    • cross-parameter(跨参数校验):cross-parameter-constraints
    • cascaded validation:method cascaded validation
      • ConstrainValidator需要注解@SupportedValidationTarget(ValidationTarget.PARAMETERS)
      • void m(@Valid @NotNull ABean bean);校验参数bean@NotNull,进行级联校验@Valid
  • 顺序:
    • method cross-parameter > method parameter(如果有cascaded就执行)
    • bean类 > bean字段(如果有cascaded就执行)
    • 至于在一个地方的顺序==注解的顺序:通常是从上到下(但不一定,不能依赖这个)

继承问题

  • bean constraint inheritance
  • method constraints inheritance
  • 总的来说,遵循Liskov替换原则,确保子类型能够替换其基类型而不改变程序的行为(也是继承遵循的原则)
    • bean:父类的仍然会生效,而且子类的也会生效
    • 方法:父类必须生效,因此无法加一样的constraint,但是可以加子类自己的constraint,因为这样父类仍然生效

自定义

  • customconstraints
  • 实现ConstraintValidator,会在IOC中创建(不需要手动注入),因此可以注入属性
    • initialize:只在创建时调用
      • 只有相同的注解值,才会使用同一个ConstraintValidator对象
  • constraint-composition
    • 使用一个注解,该注解注解了constraint注解的集合
    • @ReportAsSingleViolation就只报告一个ConstraintViolation,而是各自都有ConstraintViolation

构建消息

//禁止默认的ConstraintViolation
context.disableDefaultConstraintViolation();
//创建一个ConstraintViolation,默认当前环境,如当前是一个bean的属性就自动带有该属性的信息
//   更详细例子看ConstraintValidatorContext#buildConstraintViolationWithTemplate
context
    .buildConstraintViolationWithTemplate("TestLog-dynamic-message")
    .addConstraintViolation();

消息模板

  • chapter-message-interpolation
  • ValidationMessages.properties
  • 默认ResourceBundleMessageInterpolator
    1. resolveMessage(从properties资源中获取{KEY}的value)
    2. 遍历先解析{},再解析${}(EL表达式),如果没有则直接跳过。
      1. EL表达式需要配置constraintExpressionLanguageFeatureLevel
  • 值:
    • {}: 注解属性、context.messageParameters
    • ${}: org.hibernate.validator.internal.engine.messageinterpolation.ElTermResolver.bindContextValues
      1. ElTermResolver.VALIDATED_VALUE_NAME=validatedValue(当前验证的值)
      2. RootResolver.FORMATTER: formatter.format('%s %d', xxx,xxx), 如String.format一样(java.util.Formatter)
      3. 注解属性、context.expressionVariables
    • 添加messageParameter、expressionVariable:((HibernateConstraintValidatorContext)context).addXXXX

Validator实现

  • org.hibernate.validator.internal.engine.ValidatorImplimplementsjavax.validation.Validator**, **javax.validation.executable.ExecutableValidator
  • 创建
    • Validation.byDefaultProvider().configure().buildValidatorFactory().getValidator()
      • 获取Java SPI机制的第一个Service
    • Validation.byProvider(HibernateValidator.class).configure().buildValidatorFactory().getValidator()
  • 主要方法:都返回Set<ConstraintViolation>即约束违规集合
    • 检验beanvalidate(T object, Class<?>... groups)
    • 检验bean或者说一个类的属性,共用一个ConstraintValidator实例
      • validateProperty(T object,String propertyName,Class<?>... groups)
      • validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups)
    • 校验可执行的(比如方法,构造函数)
      • ExecutableValidator forExecutables()hibernate就是返回这个ValidatorImpl即this
    • ExecutableValidator:主要校验方法参数、返回值
      • validateParameters(T object,Method method,Object[] parameterValues,Class<?>... groups)
      • validateReturnValue(T object,Method method,Object returnValue,Class<?>... groups)
      • 对构造函数:validateConstructorParameters``validateConstructorReturnValue
  • ConstraintViolation
  • BeanMetaData:bean类/方法所属类,约束的缓存
    • 在第一次时,从类解析出constraintMetaDatas
    • ConstraintMetaData:某个位置的约束数据(多个)
      • 位置:类、属性、方法、参数、返回值
    • 校验顺序
      • 同位置有多个时,从靠近位置的开始
        • 比如方法参数是注解在右边的,那就从最左边一个约束开始
      • 方法参数校验:注解在方法上 >  注解在参数上(按顺序)
        • 注解在方法上的ConstraintValidtor需要额外注解@SupportedValidationTarget(ValidationTarget.PARAMETERS),value是所有参数
      • bean校验:bean类 > bean属性
  • Validtor#validateExecutableValidator#validateParameters(其他的校验也差不多)
    • 一开使用ValidationOrder计算Groups/Sequences(后面校验就是从这获取的Groups/Sequences进行校验)
      • Groups和Sequences只会执行一种方式,默认Groups,有Sequences就不用Groups
      • 对于Sequence, 默认failFast, 可以在 Validation.byProvider(HibernateValidator.class).configure().failFast(false)配置
    • Group/Sequence校验
      1. 第一步:校验本身
        1. 对于bean,就是先校验bean字段/属性上的(值是字段值),再校验bean类上的(值是该bean)
        2. 对方法,就是先校验cross-parameter(值是所有参数即Object[]),再校验单个参数上的(值是参数值)
        3. 统一校验目标上有多个,不确定顺序
      2. 第二步:再校验validateCascadedConstraints
        1. 即校验有Valid的方法参数的属性/有Valid的bean的属性的属性

正常需求例子

package com.liruo.learn.spring.mvc.controller.valid;

import com.liruo.learn.spring.mvc.validate.TestIO;
import com.liruo.learn.spring.mvc.validate.TestLog;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.groups.Default;

/**
 * @Author:liruo
 * @Date:2023-06-10-23:06:49
 * @Desc
 */
@Validated
@RequestMapping("/normal")
@RestController
public class NormalController {
    @Data
    public static class Bean {
        @TestLog("校验name")
        private String name;

        @TestLog(value = "校验name2-Create", groups = Groups.Create.class)
        private String name2;

        @TestLog(value = "校验name3-Update", groups = Groups.Update.class)
        private String name3;
    }


    /**
     * 重复校验反例
     */
    @PostMapping("/Valid/{id}")
    public String post(@Valid @NotNull @TestLog("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
        //RequestResponseBodyMethodProcessor
        //  annotationValue=校验name, value=123
        //MethodValidationInterceptor
        //  annotationValue=io1, value=NormalController.Bean(name=123)
        //  annotationValue=校验name, value=123
        return "id=" + id + " " + bean.toString();
    }

    @PostMapping("/Validated/{id}")
    public String get(@Validated @NotNull @TestLog("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
        //RequestResponseBodyMethodProcessor
        //  annotationValue=校验name, value=awd
        //MethodValidationInterceptor
        //  annotationValue=io1, value=NormalController.Bean(name=awd)
        return "id=" + id + " " + bean.toString();
    }

    //Validated分组校验
    interface Groups{
        interface Create {}
        interface Update {}
    }
    @PostMapping("/Validated/group/Update/{id}")
    public String postUpdateGroup(@Validated({Default.class, Groups.Update.class}) @NotNull @TestIO("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
        //annotationValue=校验name, value=1
        //annotationValue=校验name3-Update, value=1
        //annotationValue=io1, value=NormalController.Bean(name=1, name2=1, name3=1)
        return "id=" + id + " " + bean.toString();
    }
    @PostMapping("/Validated/group/Create/{id}")
    public String postCreateGroup(@Validated({Default.class, Groups.Create.class}) @NotNull @TestIO("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
        //annotationValue=校验name, value=w
        //annotationValue=校验name2-Create, value=w
        //annotationValue=io1, value=NormalController.Bean(name=w, name2=w, name3=w)
        return "id=" + id + " " + bean.toString();
    }

}

spring的配置

  • 在autocinfigure项目中有org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration
    • 注册的LocalValidatorFactoryBean是Validator的关键
    • 还注册了MethodValidationPostProcessor
  • org.springframework.validation.beanvalidation.LocalValidatorFactoryBean#afterPropertiesSet
    • 使用javax.validation.Configuration进行配置javax.validation.ValidatorFactory创建Validator
    • ConstraintValidatorFactory:SpringConstraintValidatorFactory
      • 因此创建Validator对象时会使用spring进行创建,可以注入依赖
      • 先注入依赖,再调用initialize

  • docs.jboss.org/hibernate/v…
  • 默认都在javax.validation.groups.Default,即默认校验Default组
  • 可以认为组就是一个类(一般是一个接口)
  • 功能
    • 给约束分配组:如@NotEmpty(groups = Default.class)
    • 让多组约束按指定组顺序执行校验:
      • 方法就是合并为一个组,在这个组指定这多组的顺序
      • 使用javax.validation.GroupSequence指定组顺序
        • 注解在类上,即组上,使该组合并多组
      • 防止循环:即合并组是多组中某个组的父级
    • @GroupSequence注解在bean类上:定义默认组,默认组顺序
      • 注意:必须附带上默认组,但不是Default类而是bean类
    • @ConvertGroup:定义级联校验的默认组
      • 最外层校验需要手动传入组(但是像Spring要使用Spring的@Validated才能获取组),但是可以指定级联校验的默认组
      • 比如Bean1的属性Bean2,给Bean2注解@ConvertGroup就可以指定Bean2要校验什么组
        • 因为是默认组,因此如果validator有传入参数组,还是会使用传入的
      • 方法参数取巧指定默认组:在参数bean上注解@Valid+@ConvertGroup也可以传入指定的组(解决了像Spring的@Validated才能传入组的不便)