分组序列@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()的逻辑~

相关文章
|
4天前
|
XML JSON 数据库
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
这篇文章详细介绍了RESTful的概念、实现方式,以及如何在SpringMVC中使用HiddenHttpMethodFilter来处理PUT和DELETE请求,并通过具体代码案例分析了RESTful的使用。
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
|
1天前
|
前端开发 应用服务中间件 数据库
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
这篇文章通过一个具体的项目案例,详细讲解了如何使用SpringMVC、Thymeleaf、Bootstrap以及RESTful风格接口来实现员工信息的增删改查功能。文章提供了项目结构、配置文件、控制器、数据访问对象、实体类和前端页面的完整源码,并展示了实现效果的截图。项目的目的是锻炼使用RESTful风格的接口开发,虽然数据是假数据并未连接数据库,但提供了一个很好的实践机会。文章最后强调了这一章节主要是为了练习RESTful,其他方面暂不考虑。
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
|
17天前
|
JSON 前端开发 Java
Spring MVC返回JSON数据
综上所述,Spring MVC提供了灵活、强大的方式来支持返回JSON数据,从直接使用 `@ResponseBody`及 `@RestController`注解,到通过配置消息转换器和异常处理器,开发人员可以根据具体需求选择合适的实现方式。
41 4
|
17天前
|
XML 前端开发 Java
Spring MVC接收param参数(直接接收、注解接收、集合接收、实体接收)
Spring MVC提供了灵活多样的参数接收方式,可以满足各种不同场景下的需求。了解并熟练运用这些基本的参数接收技巧,可以使得Web应用的开发更加方便、高效。同时,也是提高代码的可读性和维护性的关键所在。在实际开发过程中,根据具体需求选择最合适的参数接收方式,能够有效提升开发效率和应用性能。
43 3
|
18天前
|
XML 前端开发 Java
Spring MVC接收param参数(直接接收、注解接收、集合接收、实体接收)
Spring MVC提供了灵活多样的参数接收方式,可以满足各种不同场景下的需求。了解并熟练运用这些基本的参数接收技巧,可以使得Web应用的开发更加方便、高效。同时,也是提高代码的可读性和维护性的关键所在。在实际开发过程中,根据具体需求选择最合适的参数接收方式,能够有效提升开发效率和应用性能。
41 2
|
1月前
|
前端开发 Java 应用服务中间件
我以为我对Spring MVC很了解,直到我遇到了...
所有人都知道Spring MVC是是开发的,却鲜有人知道Spring MVC的理论基础来自于1978 年提出MVC模式的一个老头子,他就是Trygve Mikkjel Heyerdahl Reenskaug,挪威计算机科学家,名誉教授。Trygve Reenskaug的MVC架构思想早期用于图形用户界面(GUI) 的软件设计,他对MVC是这样解释的。MVC 被认为是解决用户控制大型复杂数据集问题的通用解决方案。最困难的部分是为不同的架构组件想出好的名字。模型-视图-编辑器是第一个。
我以为我对Spring MVC很了解,直到我遇到了...
|
22天前
|
前端开发 Java API
Spring Boot 中的 MVC 支持
### Spring Boot 注解摘要 - **@RestController** - **@RequestMapping** - **@PathVariable** - **@RequestParam** - **@RequestBody**
21 2
|
7天前
|
前端开发 Java Spring
Java 新手入门:Spring Boot 轻松整合 Spring 和 Spring MVC!
Java 新手入门:Spring Boot 轻松整合 Spring 和 Spring MVC!
18 0
|
22天前
|
JSON 前端开发 Java
Spring Boot中的MVC支持
本节课主要讲解了 Spring Boot 中对 MVC 的支持,分析了 @RestController、 @RequestMapping、@PathVariable、 @RequestParam 和 @RequestBody 四个注解的使用方式,由于 @RestController 中集成了 @ResponseBody 所以对返回 json 的注解不再赘述。以上四个注解是使用频率很高的注解,在所有的实际项目中基本都会遇到,要熟练掌握。
|
1月前
|
前端开发 Java Spring
Spring MVC中使用ModelAndView传递数据
Spring MVC中使用ModelAndView传递数据