Spring官网阅读(十七)Spring中的数据校验(1)

简介: Spring官网阅读(十七)Spring中的数据校验(1)

Java中的数据校验


在学习Java中的数据校验前,我们需要先了解一个概念,即什么是JSR?

JSR:全称Java Specification Requests,意思是Java 规范提案。我们可以将其理解为Java为一些功能指定的一系列统一的规范。跟数据校验相关的最新的JSR为JSR 380。


Bean Validation 2.0 是JSR第380号标准。该标准连接如下:https://www.jcp.org/en/egc/view?id=380

Bean Validation的主页:http://beanvalidation.org

Bean Validation的参考实现:https://github.com/hibernate/hibernate-validator


Bean Validation(JSR 380)


微信图片_20221113131226.png

从官网中的截图我们可以看到,Bean Validation 2.0的唯一实现就是Hibernate Validator,对应版本为6.0.1.Final,同时在2.0版本之前还有1.1(JSR 349)及1.0(JSR 303)两个版本,不过版本间的差异并不是我们关注的重点,而且Bean Validation 2.0本身也向下做了兼容。


在上面的图中,可以看到Bean Validation2.0的全称为Jakarta Bean Validation2.0,关于Jakarta,感兴趣的可以参考这个链接:https://www.oschina.net/news/94055/jakarta-ee-new-logo,就是Java换了个名字。


使用示例


导入依赖:

<!--除了导入hibernate-validator外,还需要导入一个tomcat-embed-el包,用于提供EL表达式的功能
因为错误message是支持EL表达式计算的,所以需要导入此包
-->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>9.0.16</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.14.Final</version>
    <scope>compile</scope>
</dependency>
<!--
如果你用的是一个SpringBoot项目只需要导入下面这个依赖即可
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
-->

测试Demo:

@Data
public class Person {
    @NotEmpty
    private String name;
    @Positive
    @Max(value = 100)
    private int age;
}
public class SpringValidation {
    public static void main(String[] args) {
        Person person = new Person();
        person.setAge(-1);
        Set<ConstraintViolation<Person>> result =
               Validation.buildDefaultValidatorFactory().getValidator().validate(person);
        // 对结果进行遍历输出
        result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue())
                .forEach(System.out::println);
    }
    // 运行结果:
    // name 不能为空: null
    // age 必须是正数: -1
}

对于其中涉及的细节目前来说我不打算过多的探讨,我们现在只需要知道Java提供了数据校验的规范,同时Hibernate对其有一套实现就可以了,并且我们也验证了使用其进行校验是可行的。那么接下来我们的问题就变成了Spring对Java的这套数据校验的规范做了什么支持呢?或者它又做了什么扩展呢?


Spring对Bean Validation的支持


我们先从官网入手,看看Spring中如何使用数据校验,我这里就直接取官网中的Demo了

@Data
public class Person {
    private String name;
    private int age;
}
public class PersonValidator implements Validator {
    @Override
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }
    @Override
    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        person.setAge(-1);
        DirectFieldBindingResult errorResult = new DirectFieldBindingResult(person, "dmz");
        PersonValidator personValidator = new PersonValidator();
        personValidator.validate(person, errorResult);
        System.out.println(errorResult);
        // 程序打印:
//Field error in object 'dmz' on field 'name': rejected value [null]; codes //[name.empty.dmz.name,name.empty.name,name.empty.java.lang.String,name.empty]; arguments //[]; default message [null]
//Field error in object 'dmz' on field 'age': rejected value [-1]; codes //[negativevalue.dmz.age,negativevalue.age,negativevalue.int,negativevalue]; arguments //[]; default message [null]
    }
}

在上面的例子中,PersonValidator实现了一个Validator接口,这个接口是Spring自己提供的,全称:org.springframework.validation.Validator,我们看看这个接口的定义


Spring中的Validator


org.springframework.validation.Validator是专门用于应用相关的对象的校验器。


这个接口完全从基础设施或者上下文中脱离的,这意味着它没有跟web层或者数据访问层或者其余任何的某一个层次发生耦合。所以它能用于应用中的任意一个层次,能对应用中的任意一个对象进行校验。,


接口定义

public interface Validator {
    // 此clazz是否可以被validate
  boolean supports(Class<?> clazz);
    // 执行校验,错误消息放在Errors中
    // 如果能执行校验,通常也意味着supports方法返回true
  // 可以参考ValidationUtils这个工具类
  void validate(Object target, Errors errors);
}

UML类图


微信图片_20221113131849.png


SmartValidator


对Validator接口进行了增强,能进行分组校验

public interface SmartValidator extends Validator {
  // validationHints:就是启动的校验组
    // target:需要校验的结果
    // errors:封装校验
    void validate(Object target, Errors errors, Object... validationHints);
    // 假设value将被绑定到指定对象中的指定字段上,并进行校验
    // @since 5.1  这个方法子类需要复写 否则不能使用
    default void validateValue(Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
        throw new IllegalArgumentException("Cannot validate individual value for " + targetType);
    }
}

SpringValidatorAdapter


在之前的接口我们会发现,到目前为止Spring中的校验跟Bean Validation还没有产生任何交集,而SpringValidatorAdapter就完成了到Bean Validation的对接

// 可以看到,这个接口同时实现了Spring中的SmartValidator接口跟JSR中的Validator接口
public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator {
    //@NotEmpty,@NotNull等注解都会有这三个属性
  private static final Set<String> internalAnnotationAttributes = new HashSet<>(4);
  static {
    internalAnnotationAttributes.add("message");
    internalAnnotationAttributes.add("groups");
    internalAnnotationAttributes.add("payload");
  }
    // targetValidator就是实际完成校验的对象
  @Nullable
  private javax.validation.Validator targetValidator;
  public SpringValidatorAdapter(javax.validation.Validator targetValidator) {
    Assert.notNull(targetValidator, "Target Validator must not be null");
    this.targetValidator = targetValidator;
  }
  SpringValidatorAdapter() {
  }
  void setTargetValidator(javax.validation.Validator targetValidator) {
    this.targetValidator = targetValidator;
  }
    // 支持对所有类型的Bean的校验
  @Override
  public boolean supports(Class<?> clazz) {
    return (this.targetValidator != null);
  }
    // 调用targetValidator完成校验,并通过processConstraintViolations方法封装校验后的结果到Errors中
  @Override
  public void validate(Object target, Errors errors) {
    if (this.targetValidator != null) {
      processConstraintViolations(this.targetValidator.validate(target), errors);
    }
  }
    // 完成分组校验
  @Override
  public void validate(Object target, Errors errors, Object... validationHints) {
    if (this.targetValidator != null) {
      processConstraintViolations(
          this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
    }
  }
    // 完成对对象上某一个字段及给定值的校验
  @SuppressWarnings("unchecked")
  @Override
  public void validateValue(
      Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
    if (this.targetValidator != null) {
      processConstraintViolations(this.targetValidator.validateValue(
          (Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors);
    }
  }
  // @since 5.1
  // 将validationHints转换成JSR中的分组
  private Class<?>[] asValidationGroups(Object... validationHints) {
    Set<Class<?>> groups = new LinkedHashSet<>(4);
    for (Object hint : validationHints) {
      if (hint instanceof Class) {
        groups.add((Class<?>) hint);
      }
    }
    return ClassUtils.toClassArray(groups);
  }
  // 省略对校验错误的封装
    // .....
    // 省略对JSR中validator接口的实现,都是委托给targetValidator完成的
    // ......
}

ValidatorAdapter


跟SpringValidatorAdapter同一级别的类,但是不同的是他没有实现JSR中的Validator接口。一般不会使用这个类


CustomValidatorBean

public class CustomValidatorBean extends SpringValidatorAdapter implements Validator, InitializingBean {
    // JSR中的接口,校验器工厂
  @Nullable
  private ValidatorFactory validatorFactory;
    // JSR中的接口,用于封装校验信息
  @Nullable
  private MessageInterpolator messageInterpolator;
     // JSR中的接口,用于判断属性能否被ValidatorProvider访问
  @Nullable
  private TraversableResolver traversableResolver;
  // 忽略setter方法
    // 在SpringValidatorAdapter的基础上实现了InitializingBean,在Bean初始化时调用,用于给上面三个属性进行配置
  @Override
  public void afterPropertiesSet() {
    if (this.validatorFactory == null) {
      this.validatorFactory = Validation.buildDefaultValidatorFactory();
    }
    ValidatorContext validatorContext = this.validatorFactory.usingContext();
    MessageInterpolator targetInterpolator = this.messageInterpolator;
    if (targetInterpolator == null) {
      targetInterpolator = this.validatorFactory.getMessageInterpolator();
    }
    validatorContext.messageInterpolator(new LocaleContextMessageInterpolator(targetInterpolator));
    if (this.traversableResolver != null) {
      validatorContext.traversableResolver(this.traversableResolver);
    }
    setTargetValidator(validatorContext.getValidator());
  }
}

LocalValidatorFactoryBean

public class LocalValidatorFactoryBean extends SpringValidatorAdapter
    implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean {
    //......
}

可以看到,这个类额外实现了ValidatorFactory接口,所以通过它不仅能完成校验,还能获取一个校验器validator。


OptionalValidatorFactoryBean

public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean {
  @Override
  public void afterPropertiesSet() {
    try {
      super.afterPropertiesSet();
    }
    catch (ValidationException ex) {
      LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
    }
  }
}

继承了LocalValidatorFactoryBean,区别在于让校验器的初始化成为可选的,即使校验器没有初始化成功也不会报错。


使用示例


在对整个体系有一定了解之后,我们通过一个例子来体会下Spring中数据校验

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        // 将CustomValidatorBean注册到容器中,主要是为了让它经过初始化阶段完成对校验器的配置
        ac.register(CustomValidatorBean.class);
        // 刷新启动容器
        ac.refresh();
        // 获取到容器中的校验器
        CustomValidatorBean cb = ac.getBean(CustomValidatorBean.class);
        // 校验simple组的校验
        Person person = new Person();
        DirectFieldBindingResult simpleDbr = new DirectFieldBindingResult(person, "person");
        cb.validate(person, simpleDbr, Person.Simple.class);
        // 校验Complex组的校验
        DirectFieldBindingResult complexDbr = new DirectFieldBindingResult(person, "person");
        person.setStart(new Date());
        cb.validate(person, complexDbr, Person.Complex.class);
        System.out.println(complexDbr);
    }
}

运行结果我这里就不贴出来了,大家可以自行测试

到目前为止,我们所接触到的校验的内容跟实际使用还是有很大区别,我相信在绝大多数情况下大家都不会采用前文所采用的这种方式去完成校验,而是通过@Validated或者@Valid来完成校验。

相关文章
|
6月前
|
Java 数据库连接 Spring
Spring之数据校验:Validation
【1月更文挑战第17天】 一、Spring Validation概述 二、实验一:通过Validator接口实现 三、实验二:Bean Validation注解实现 四、实验三:基于方法实现校验 五、实验四:实现自定义校验
156 2
Spring之数据校验:Validation
|
6月前
|
Java 应用服务中间件 Spring
Spring5源码(50)-SpringMVC源码阅读环境搭建
Spring5源码(50)-SpringMVC源码阅读环境搭建
76 0
|
Java Nacos Spring
Nacos spring-cloud 版本没找到共享配置文件的说明,Nacos服务中共享,并且可以被多个应用获取和使用。这个在官网哪里有说明啊
Nacos spring-cloud 版本没找到共享配置文件的说明,Nacos服务中共享,并且可以被多个应用获取和使用。这个在官网哪里有说明啊
74 1
|
4月前
|
前端开发 JavaScript Java
Spring Boot中的数据校验
Spring Boot中的数据校验
|
5月前
|
存储 Java 程序员
Spring 注册BeanPostProcessor 源码阅读
Spring 注册BeanPostProcessor 源码阅读
|
4月前
|
Java 数据库连接 测试技术
在Spring Boot中实现数据校验与验证
在Spring Boot中实现数据校验与验证
|
6月前
|
Java 数据库连接 Maven
【Spring】掌握 Spring Validation 数据校验
【Spring】掌握 Spring Validation 数据校验
273 0
|
前端开发 Java 数据库
Spring Entity数据校验,分组校验,返回校验结果给前端
Spring Entity数据校验,分组校验,返回校验结果给前端
98 0
|
Java 中间件 Maven
Spring 6 源码编译和高效阅读源码技巧分享
Spring 6 源码编译和高效阅读源码技巧分享
|
6月前
|
人工智能 运维 Java
spring数据校验
spring数据校验
27 0