分组序列@GroupSequenceProvider、@GroupSequence控制数据校验顺序,解决多字段联合逻辑校验问题【享学Spring MVC】(下)

简介: 分组序列@GroupSequenceProvider、@GroupSequence控制数据校验顺序,解决多字段联合逻辑校验问题【享学Spring MVC】(下)

4、determineGroupValidationOrder(groups)从调用者指定的分组里确定组序列(组的执行顺序)


ValidatorImpl:
  @Override
  public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
    ...
    BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
    ...
    ... // 准备好ValidationContext(持有rootBeanMetaData和object实例)
    // groups是调用者传进来的分组数组(对应Spring MVC中指定的Group信息~)
    ValidationOrder validationOrder = determineGroupValidationOrder(groups);
    ... // 准备好ValueContext(持有rootBeanMetaData和object实例)
    // 此时还是Bean级别的,开始对此bean执行校验
    return validateInContext( validationContext, valueContext, validationOrder );
  }
  private ValidationOrder determineGroupValidationOrder(Class<?>[] groups) {
    Collection<Class<?>> resultGroups;
    // if no groups is specified use the default
    if ( groups.length == 0 ) {
      resultGroups = DEFAULT_GROUPS;
    } else {
      resultGroups = Arrays.asList( groups );
    }
    // getValidationOrder()主要逻辑描述。此时候resultGroups 至少也是个[Default.class]
    // 1、如果仅仅只是一个Default.class,那就直接return
    // 2、遍历所有的groups。(指定的Group必须必须是接口)
    // 3、若遍历出来的group标注有`@GroupSequence`注解,特殊处理此序列(把序列里的分组们添加进来)
    // 4、普通的Group,那就new Group( clazz )添加进`validationOrder`里。并且递归插入(因为可能存在父接口的情况)
    return validationOrderGenerator.getValidationOrder( resultGroups );
  }


到这ValidationOrder(实际为DefaultValidationOrder)保存着调用者调用validate()方法时传入的Groups们。分组序列@GroupSequence在此时会被解析。

到了validateInContext( ... )就开始拿着这些Groups分组、元信息开始对此Bean进行校验了~


5、validateInContext( ... )在上下文(校验上下文、值上下文、指定的分组里)对此Bean进行校验

ValidatorImpl:
  private <T, U> Set<ConstraintViolation<T>> validateInContext(ValidationContext<T> validationContext, ValueContext<U, Object> valueContext, ValidationOrder validationOrder) {
    if ( valueContext.getCurrentBean() == null ) { // 兼容整个Bean为null值
      return Collections.emptySet();
    }
    // 如果该Bean头上标注了(需要defaultGroupSequence处理),那就特殊处理一下
    // 本例中我们的Person肯定为true,可以进来的
    BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
    if ( beanMetaData.defaultGroupSequenceIsRedefined() ) {
      // 注意此处又调用了beanMetaData.getDefaultGroupSequence()这个方法,这算是二次调用了
      // 此处传入的Object哟~这就解释了为何在判空里面的 `年龄为:xxx`被打印了两次的原因
      // assertDefaultGroupSequenceIsExpandable方法是个空方法(默认情况下),可忽略
      validationOrder.assertDefaultGroupSequenceIsExpandable( beanMetaData.getDefaultGroupSequence( valueContext.getCurrentBean() ) );
    }
    // ==============下面对于执行顺序,就很重要了===============
    // validationOrder装着的是调用者指定的分组(解析分组序列来保证顺序~~~)
    // 需要特别注意:光靠指定分组,是无序的(不能保证校验顺序的) 所以若指定多个分组需要小心求证
    Iterator<Group> groupIterator = validationOrder.getGroupIterator();
    // 按照调用者指定的分组(顺序),一个一个的执行分组校验。
    while ( groupIterator.hasNext() ) {
      Group group = groupIterator.next();
      valueContext.setCurrentGroup(group.getDefiningClass()); // 设置当前正在执行的分组
      // 这个步骤就稍显复杂了,也是核心的逻辑之一。大致过程如下:
      // 1、拿到该Bean的BeanMetaData
      // 2、若defaultGroupSequenceIsRedefined()=true  本例Person标注了provder注解,所以有指定的分组序列的
      // 3、根据分组序列的顺序,挨个执行分组们(对所有的约束MetaConstraint都顺序执行分组们)
      // 4、最终完成所有的MetaConstraint的校验,进而完成此部分所有的字段、方法等的校验
      validateConstraintsForCurrentGroup( validationContext, valueContext );
      if ( shouldFailFast( validationContext ) ) {
        return validationContext.getFailingConstraints();
      }
    }
    ... // 和上面一样的代码,校验validateCascadedConstraints
    // 继续遍历序列:和@GroupSequence相关了
    Iterator<Sequence> sequenceIterator = validationOrder.getSequenceIterator();
    ...
    // 校验上下文的错误消息:它会把本校验下,所有的验证器上下文ConstraintValidatorContext都放一起的
    // 注意:所有的校验注解之间的上下文ConstraintValidatorContext是完全独立的,无法互相访问通信
    return validationContext.getFailingConstraints();
  }


that is all. 到这一步整个校验就完成了,若不快速失败,默认会拿到所有校验失败的消息。


真正执行isValid的方法在这里:


public abstract class ConstraintTree<A extends Annotation> {
  ...
  protected final <T, V> Set<ConstraintViolation<T>> validateSingleConstraint(
      ValidationContext<T> executionContext, // 它能知道所属类
      ValueContext<?, ?> valueContext,
      ConstraintValidatorContextImpl constraintValidatorContext,
      ConstraintValidator<A, V> validator) {
    boolean isValid;
    // 解析出value值
    V validatedValue = (V) valueContext.getCurrentValidatedValue(); 
    // 把value值交给校验器的isValid方法去校验~~~
    isValid = validator.isValid(validatedValue,constraintValidatorContext);
    ...
    if (!isValid) {
      // 校验没通过就使用constraintValidatorContext校验上下文来生成错误消息
      // 使用上下文是因为:毕竟错误消息可不止一个啊~~~
      // 当然此处借助了executionContext的方法~~~内部其实调用的是constraintValidatorContext.getConstraintViolationCreationContexts()这个方法而已
      return executionContext.createConstraintViolations(valueContext, constraintValidatorContext);
    }
  }
}


至于上下文ConstraintValidatorContext怎么来的,是new出来的:new ConstraintValidatorContextImpl( ... ),每个字段的一个校验注解对应一个上下文(一个属性上可以标注多个约束注解哦~),所以此上下文是有很强的隔离性的。


ValidationContext<T> validationContext和ValueContext<?, Object> valueContext它哥俩是类级别的,直到ValidatorImpl.validateMetaConstraints方法开始一个一个约束器的校验~


自定义注解中只把ConstraintValidatorContext上下文给调用者使用,而并没有给validationContext和valueContext,我个人觉得这个设计是不够灵活的,无法方便的实现dependOn的效果~


ConstraintValidatorContext一般它能用于在代码里个性化错误消息ConstraintViolation。可以这么来做:


        context.disableDefaultConstraintViolation(); // 禁用注解上默认的模版消息
        // context.getDefaultConstraintMessageTemplate(); // 未插值的消息模版
        //context.buildConstraintViolationWithTemplate("这是我的错误消息模版")
        //        .addParameterNode()
        //        .addPropertyNode()
        //        .addContainerElementNode()
        //        .addConstraintViolation();


若你想要更多的功能,可使用子接口HibernateConstraintValidatorContext提供的方法。参见这里:【小家Java】深入了解数据校验(Bean Validation):基础类打点(ValidationProvider、ConstraintDescriptor、ConstraintValidator)


解决网友的问题


我把这部分看似是本文最重要的引线放到最后,是因为我觉得我的描述已经解决这一类问题,而不是只解决了这一个问题。


回到文首截图中热心网友反应的问题,只要你阅读了本文,我十分坚信你已经有办法去使用Bean Validation优雅的解决了。如果各位没有意见,此处我就略了~


总结


本文讲述了使用@GroupSequenceProvider来解决多字段联合逻辑校验的这一类问题,这也许是曾经很多人的开发痛点,希望本文能帮你一扫之前的障碍,全面拥抱Bean Validation吧~

本文我也传达了一个观点:相信流行的开源东西的优秀,不是非常极端的case,深入使用它能解决你绝大部分的问题的。


相关文章
|
1天前
|
前端开发 Java 应用服务中间件
Spring MVC框架概述
Spring MVC 是一个基于Java的轻量级Web框架,采用MVC设计模型实现请求驱动的松耦合应用开发。框架包括DispatcherServlet、HandlerMapping、Handler、HandlerAdapter、ViewResolver核心组件。DispatcherServlet协调这些组件处理HTTP请求和响应,Controller处理业务逻辑,Model封装数据,View负责渲染。通过注解@Controller、@RequestMapping等简化开发,支持RESTful请求。Spring MVC具有清晰的角色分配、Spring框架集成、多种视图技术支持以及异常处理等优点。
16 1
|
1天前
|
JSON Java 数据格式
Spring Boot实现各种参数校验
这些是Spring Boot中实现参数校验的一些常见方法,你可以根据项目需求选择适合的方式来进行参数校验。
16 0
|
1天前
|
数据采集 前端开发 Java
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
23 3
|
1天前
|
存储 前端开发 Java
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
14 1
|
1天前
|
前端开发 Java Spring
数据之桥:深入Spring MVC中传递数据给视图的实用指南
数据之桥:深入Spring MVC中传递数据给视图的实用指南
34 3
|
1天前
|
前端开发 Java 容器
家族传承:Spring MVC中父子容器的搭建与管理指南
家族传承:Spring MVC中父子容器的搭建与管理指南
26 3
|
1天前
|
前端开发 Java API
头头是道:揭示Spring MVC如何获取和处理请求头数据
头头是道:揭示Spring MVC如何获取和处理请求头数据
27 1
|
1天前
|
前端开发 Java API
饼干探秘:深入Spring MVC中获取Cookie数据的技术解析
饼干探秘:深入Spring MVC中获取Cookie数据的技术解析
18 3
|
1天前
|
前端开发 Java Spring
转换之术:解析Spring MVC中类型转换器的实际运用
转换之术:解析Spring MVC中类型转换器的实际运用
22 0
|
1天前
|
前端开发 Java Spring
参数解密:揭示Spring MVC请求参数处理的实际应用指南
参数解密:揭示Spring MVC请求参数处理的实际应用指南
26 1