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,深入使用它能解决你绝大部分的问题的。