使用JSR提供的@GroupSequence注解控制校验顺序
上面的实现方式是最佳实践,使用起来不难,灵活度也非常高。但是我们必须要明白它是Hibernate Validation提供的能力,而不费JSR标准提供的。
@GroupSequence它是JSR标准提供的注解(只是没有provider强大而已,但也有很适合它的使用场景)
// Defines group sequence. 定义组序列(序列:顺序执行的) @Target({ TYPE }) @Retention(RUNTIME) @Documented public @interface GroupSequence { Class<?>[] value(); }
顾名思义,它表示Group组序列。默认情况下,不同组别的约束验证是无序的
在某些情况下,约束验证的顺序是非常的重要的,比如如下两个场景:
- 第二个组的约束验证依赖于第一个约束执行完成的结果(必须第一个约束正确了,第二个约束执行才有意义)
- 某个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来完成,相对来说还是比较困难的。但是也并不是不能做,此处我提供参考思路:
- 多字段之间的逻辑、“通信”通过类级别的自定义校验注解来实现(至于为何必须是类级别的,不用解释吧~)
- @GroupSequence用来控制组执行顺序(让类级别的自定义注解先执行)
- 增加Bean级别的第三属性来辅助校验~
当然喽,在实际应用中不可能使用它来解决如题的问题,所以我此处就不费篇幅了。我个人建议有兴趣者可以自己动手试试,有助于加深你对数据校验这块的理解。
这篇文章里有说过:数据校验注解是可以标注在Field属性、方法、构造器以及Class类级别上的。那么关于它们的校验顺序,我们是可控的,并不是网上有些文章所说的无法抉择~
说明:顺序只能控制在分组级别,无法控制在约束注解级别。因为一个类内的约束(同一分组内),它的顺序是Set<MetaConstraint<?>> metaConstraints来保证的,所以可以认为同一分组内的校验器是木有执行的先后顺序的(不管是类、属性、方法、构造器…)
所以网上有说:校验顺序是先校验字段属性,在进行类级别校验不实,请注意辨别。
原理解析
本文中,我借助@GroupSequenceProvider来解决了平时开发中多字段组合逻辑校验的痛点问题,总的来说还是使用简单,并且代码也够模块化,易于维护的。
但对于上例的结果输出,你可能和我一样至少有如下疑问:
- 为何必须有这一句:defaultGroupSequence.add(Person.class)
- 为何if (bean != null)必须判空
- 为何年龄为: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()的逻辑~