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

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

使用JSR提供的@GroupSequence注解控制校验顺序


上面的实现方式是最佳实践,使用起来不难,灵活度也非常高。但是我们必须要明白它是Hibernate Validation提供的能力,而不费JSR标准提供的。

@GroupSequence它是JSR标准提供的注解(只是没有provider强大而已,但也有很适合它的使用场景)


// Defines group sequence.  定义组序列(序列:顺序执行的)
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
public @interface GroupSequence {
  Class<?>[] value();
}


顾名思义,它表示Group组序列。默认情况下,不同组别的约束验证是无序的

在某些情况下,约束验证的顺序是非常的重要的,比如如下两个场景:


  1. 第二个组的约束验证依赖于第一个约束执行完成的结果(必须第一个约束正确了,第二个约束执行才有意义)
  2. 某个Group组的校验非常耗时,并且会消耗比较大的CPU/内存。那么我们的做法应该是把这种校验放到最后,所以对顺序提出了要求


一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。


给个栗子:

public class User {
    @NotEmpty(message = "firstname may be empty")
    private String firstname;
    @NotEmpty(message = "middlename may be empty", groups = Default.class)
    private String middlename;
    @NotEmpty(message = "lastname may be empty", groups = GroupA.class)
    private String lastname;
    @NotEmpty(message = "country may be empty", groups = GroupB.class)
    private String country;
    public interface GroupA {
  }
  public interface GroupB {
  }
  // 组序列
  @GroupSequence({Default.class, GroupA.class, GroupB.class})
  public interface Group {
  }
}


测试:


public static void main(String[] args)  {
    User user = new User();
    // 此处指定了校验组是:User.Group.class
    Set<ConstraintViolation<User>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class);
    // 对结果进行遍历输出
    result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}


运行,控制台打印:

middlename middlename may be empty: null
firstname firstname may be empty: null


现象:只有Default这个Group的校验了,序列上其它组并没有执行校验。更改如下:

        User user = new User();
        user.setFirstname("f");
        user.setMiddlename("s");


运行,控制台打印:

lastname lastname may be empty: null


现象:Default组都校验通过后,执行了GroupA组的校验。但GroupA组校验木有通过,GroupB组的校验也就不执行了~

@GroupSequence提供的组序列顺序执行以及短路能力,在很多场景下是非常非常好用的。


针对本例的多字段组合逻辑校验,若想借助@GroupSequence来完成,相对来说还是比较困难的。但是也并不是不能做,此处我提供参考思路:


  1. 多字段之间的逻辑、“通信”通过类级别的自定义校验注解来实现(至于为何必须是类级别的,不用解释吧~)
  2. @GroupSequence用来控制组执行顺序(让类级别的自定义注解先执行)
  3. 增加Bean级别的第三属性来辅助校验~


当然喽,在实际应用中不可能使用它来解决如题的问题,所以我此处就不费篇幅了。我个人建议有兴趣者可以自己动手试试,有助于加深你对数据校验这块的理解。


这篇文章里有说过:数据校验注解是可以标注在Field属性、方法、构造器以及Class类级别上的。那么关于它们的校验顺序,我们是可控的,并不是网上有些文章所说的无法抉择~


说明:顺序只能控制在分组级别,无法控制在约束注解级别。因为一个类内的约束(同一分组内),它的顺序是Set<MetaConstraint<?>> metaConstraints来保证的,所以可以认为同一分组内的校验器是木有执行的先后顺序的(不管是类、属性、方法、构造器…)


所以网上有说:校验顺序是先校验字段属性,在进行类级别校验不实,请注意辨别。

原理解析


本文中,我借助@GroupSequenceProvider来解决了平时开发中多字段组合逻辑校验的痛点问题,总的来说还是使用简单,并且代码也够模块化,易于维护的。

但对于上例的结果输出,你可能和我一样至少有如下疑问:


  1. 为何必须有这一句:defaultGroupSequence.add(Person.class)
  2. 为何if (bean != null)必须判空
  3. 为何年龄为:35,执行对应校验逻辑被输出了两次(在判空里面还出现了两次哦~),但校验的失败信息却只有符合预期的一次


带着问题,我从validate校验的执行流程上开始分析:


1、入口:ValidatorImpl.validate(T object, Class<?>... groups)

ValidatorImpl:
  @Override
  public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
    Class<T> rootBeanClass = (Class<T>) object.getClass();
    // 获取BeanMetaData,类上的各种信息:包括类上的Group序列、针对此类的默认分组List们等等
    BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
    ...
  }


2、beanMetaDataManager.getBeanMetaData(rootBeanClass)得到待校验Bean的元信息

请注意,此处只传入了Class,并没有传入Object。这是为啥要加!= null判空的核心原因(后面你可以看到传入的是null)。


BeanMetaDataManager:
  public <T> BeanMetaData<T> getBeanMetaData(Class<T> beanClass) {
    ...
    // 会调用AnnotationMetaDataProvider来解析约束注解元数据信息(当然还有基于xml/Programmatic的,本文略) 
    // 注意:它会递归处理父类、父接口等拿到所有类的元数据
    // BeanMetaDataImpl.build()方法,会new BeanMetaDataImpl(...) 这个构造函数里面做了N多事
    // 其中就有和我本例有关的defaultGroupSequenceProvider
    beanMetaData = createBeanMetaData( beanClass );
  }


3、new BeanMetaDataImpl( ... )构建出此Class的元数据信息(本例为Person.class


BeanMetaDataImpl:
  public BeanMetaDataImpl(Class<T> beanClass,
              List<Class<?>> defaultGroupSequence, // 如果没有配置,此时候defaultGroupSequence一般都为null
              DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider, // 我们自定义的处理此Bean的provider
              Set<ConstraintMetaData> constraintMetaDataSet, // 包含父类的所有属性、构造器、方法等等。在此处会分类:按照属性、方法等分类处理
              ValidationOrderGenerator validationOrderGenerator) {
    ... //对constraintMetaDataSet进行分类
    // 这个方法就是筛选出了:所有的约束注解(比如6个约束注解,此处长度就是6  当然包括了字段、方法等上的各种。。。)
    this.directMetaConstraints = getDirectConstraints();
    // 因为我们Person类有defaultGroupSequenceProvider,所以此处返回true
    // 除了定义在类上外,还可以定义全局的:给本类List<Class<?>> defaultGroupSequence此字段赋值
    boolean defaultGroupSequenceIsRedefined = defaultGroupSequenceIsRedefined();
    // 这是为何我们要判空的核心:看看它传的啥:null。所以不判空的就NPE了。这是第一次调用defaultGroupSequenceProvider.getValidationGroups()方法
    List<Class<?>> resolvedDefaultGroupSequence = getDefaultGroupSequence( null );
    ... // 上面拿到resolvedDefaultGroupSequence 分组信息后,会放到所有的校验器里去(包括属性、方法、构造器、类等等)
    // so,默认组序列还是灰常重要的(注意:默认组可以有多个哦~~~)
  }
  @Override
  public List<Class<?>> getDefaultGroupSequence(T beanState) {
    if (hasDefaultGroupSequenceProvider()) {
      // so,getValidationGroups方法里请记得判空~
      List<Class<?>> providerDefaultGroupSequence = defaultGroupSequenceProvider.getValidationGroups( beanState );
      // 最重要的是这个方法:getValidDefaultGroupSequence对默认值进行分析~~~
      return getValidDefaultGroupSequence( beanClass, providerDefaultGroupSequence );
    }
    return defaultGroupSequence;
  }
  private static List<Class<?>> getValidDefaultGroupSequence(Class<?> beanClass, List<Class<?>> groupSequence) {
    List<Class<?>> validDefaultGroupSequence = new ArrayList<>();
    boolean groupSequenceContainsDefault = false; // 标志位:如果解析不到Default这个组  就抛出异常
    // 重要
    if (groupSequence != null) {
      for ( Class<?> group : groupSequence ) {
        // 这就是为何我们要`defaultGroupSequence.add(Person.class)`这一句的原因所在~~~ 因为需要Default生效~~~
        if ( group.getName().equals( beanClass.getName() ) ) {
          validDefaultGroupSequence.add( Default.class );
          groupSequenceContainsDefault = true;
        } 
        // 意思是:你要添加Default组,用本类的Class即可,而不能显示的添加Default.class哦~
        else if ( group.getName().equals( Default.class.getName() ) ) { 
          throw LOG.getNoDefaultGroupInGroupSequenceException();
        } else { // 正常添加进默认组
          validDefaultGroupSequence.add( group );
        }
      }
    }
    // 若找不到Default组,就抛出异常了~
    if ( !groupSequenceContainsDefault ) {
      throw LOG.getBeanClassMustBePartOfRedefinedDefaultGroupSequenceException( beanClass );
    }
    return validDefaultGroupSequence;
  }


到这一步,还仅仅在初始化BeanMetaData阶段,就执行了一次(首次)defaultGroupSequenceProvider.getValidationGroups(null),所以判空是很有必要的。并且把本class add进默认组也是必须的(否则报错)~

到这里BeanMetaData<T> rootBeanMetaData创建完成,继续validate()的逻辑~

相关文章
|
17天前
|
SQL Java 数据库连接
对Spring、SpringMVC、MyBatis框架的介绍与解释
Spring 框架提供了全面的基础设施支持,Spring MVC 专注于 Web 层的开发,而 MyBatis 则是一个高效的持久层框架。这三个框架结合使用,可以显著提升 Java 企业级应用的开发效率和质量。通过理解它们的核心特性和使用方法,开发者可以更好地构建和维护复杂的应用程序。
106 29
|
4月前
|
Java 测试技术 程序员
为什么Spring不推荐@Autowired用于字段注入?
作为Java程序员,Spring框架在日常开发中使用频繁,其依赖注入机制带来了极大的便利。然而,尽管@Autowired注解简化了依赖注入,Spring官方却不推荐在字段上使用它。本文将探讨字段注入的现状及其存在的问题,如难以进行单元测试、违反单一职责原则及易引发NPE等,并介绍为何Spring推荐构造器注入,包括增强代码可读性和维护性、方便单元测试以及避免NPE等问题。通过示例代码展示如何将字段注入重构为构造器注入,提高代码质量。
137 1
|
2月前
|
设计模式 前端开发 Java
步步深入SpringMvc DispatcherServlet源码掌握springmvc全流程原理
通过对 `DispatcherServlet`源码的深入剖析,我们了解了SpringMVC请求处理的全流程。`DispatcherServlet`作为前端控制器,负责请求的接收和分发,处理器映射和适配负责将请求分派到具体的处理器方法,视图解析器负责生成和渲染视图。理解这些核心组件及其交互原理,有助于开发者更好地使用和扩展SpringMVC框架。
66 4
|
3月前
|
前端开发 Java 开发者
Spring MVC中的请求映射:@RequestMapping注解深度解析
在Spring MVC框架中,`@RequestMapping`注解是实现请求映射的关键,它将HTTP请求映射到相应的处理器方法上。本文将深入探讨`@RequestMapping`注解的工作原理、使用方法以及最佳实践,为开发者提供一份详尽的技术干货。
230 2
|
3月前
|
Java 开发者 Spring
Spring AOP深度解析:探秘动态代理与增强逻辑
Spring框架中的AOP(Aspect-Oriented Programming,面向切面编程)功能为开发者提供了一种强大的工具,用以将横切关注点(如日志、事务管理等)与业务逻辑分离。本文将深入探讨Spring AOP的底层原理,包括动态代理机制和增强逻辑的实现。
72 4
|
4月前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
86 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
4月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
5月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
|
4月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
77 2
|
4月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
319 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习