• @Valid的作用(级联校验)以及常用约束注解的解释说明



    分组校验

    @Getter
    @Setter
    @ToString
    public class Person {
        @NotNull(message = "名字不能为空", groups = Simple.class)
        public String name;
    
    
        /**
         * 内置的分组:default
         */
        @Max(value = 10, groups = Simple.class)
        @Positive(groups = Default.class)
        public Integer age;
    
        @NotNull(groups = Complex.class)
        @NotEmpty(groups = Complex.class)
        private List<@Email String> emails;
    
        @Future(groups = Complex.class)
        private Date start;
    
        // 定义两个组 Simple组和Complex组
    
        public interface Simple {
        }
    
        public interface Complex {
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    public class ValidationTest {
        @Test
        public void testValidation(){
            Person person = new Person();
            //person.setName("fsx");
            person.setAge(18);
            // email校验:虽然是List都可以校验哦
            person.setEmails(Arrays.asList("fsx@gmail.com", "baidu@baidu.com", "aaa.com"));
            //person.setStart(new Date()); //start 需要是一个将来的时间: Sun Jul 21 10:45:03 CST 2019
            //person.setStart(new Date(System.currentTimeMillis() + 10000)); //校验通过
    
            HibernateValidatorConfiguration configure = Validation.byProvider(HibernateValidator.class).configure();
            ValidatorFactory validatorFactory = configure.failFast(false).buildValidatorFactory();
            // 根据validatorFactory拿到一个Validator
            Validator validator = validatorFactory.getValidator();
    
    
            // 分组校验(可以区分对待Default组、Simple组、Complex组)
            Set<ConstraintViolation<Person>> result = validator.validate(person, Person.Simple.class);
            //Set> result = validator.validate(person, Person.Complex.class);
    
            // 对结果进行遍历输出
            result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue())
                    .forEach(System.out::println);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    运行打印:

    age 最大不能超过10: 18
    name {message} -> 名字不能为null -> 名字不能为null: null
    
    • 1
    • 2

    可以直观的看到效果,此处的校验只执行Person.Simple.class这个Group组上的约束~

    分组约束在Spring MVC中的使用场景还是相对比较多的,但是需要注意的是:javax.validation.Valid没有提供指定分组的,但是org.springframework.validation.annotation.Validated扩展提供了直接在注解层面指定分组的能力


    @Valid注解

    我们知道JSR提供了一个@Valid注解供以使用,在本文之前,绝大多数小伙伴都是在Controller中并且结合@RequestBody一起来使用它,但在本文之后,你定会对它有个全新的认识.

    该注解用于验证级联的属性、方法参数或方法返回类型。

    当验证属性、方法参数或方法返回类型时,将验证对象及其属性上定义的约束,另外:此行为是递归应用的。

    为了理解@Valid,那就得知道处理它的时机:


    MetaDataProvider

    元数据提供者:约束相关元数据(如约束、默认组序列等)的Provider。它的作用和特点如下:

    • 基于不同的元数据:如xml、注解。(还有个编程映射) 这三种类型。对应的枚举类为:
    public enum ConfigurationSource {
    	ANNOTATION( 0 ),
    	XML( 1 ),
    	API( 2 ); //programmatic API
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • MetaDataProvider只返回直接为一个类配置的元数据
    • 它不处理从超类、接口合并的元数据(简单的说你@Valid放在接口处是无效的)
    public interface MetaDataProvider {
    
    	// 将 注解处理选项 归还给此Provider配置。  它的唯一实现类为:AnnotationProcessingOptionsImpl
    	// 它可以配置比如:areMemberConstraintsIgnoredFor  areReturnValueConstraintsIgnoredFor
    	// 也就说可以配置:让免于被校验~~~~~~(开绿灯用的)
    	AnnotationProcessingOptions getAnnotationProcessingOptions();
    	// 返回作用在此Bean上面的`BeanConfiguration`   若没有就返回null了
    	// BeanConfiguration持有ConfigurationSource的引用~
    	<T> BeanConfiguration<? super T> getBeanConfiguration(Class<T> beanClass);
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    // 表示源于一个ConfigurationSource的一个Java类型的完整约束相关配置。  包含字段、方法、类级别上的元数据
    // 当然还包含有默认组序列上的元数据(使用较少)
    public class BeanConfiguration<T> {
    	// 三种来源的枚举
    	private final ConfigurationSource source;
    	private final Class<T> beanClass;
    	// ConstrainedElement表示待校验的元素,可以知道它会如下四个子类:
    	// ConstrainedField/ConstrainedType/ConstrainedParameter/ConstrainedExecutable
    	
    	// 注意:ConstrainedExecutable持有的是java.lang.reflect.Executable对象
    	//它的两个子类是java.lang.reflect.Method和Constructor
    	private final Set<ConstrainedElement> constrainedElements;
    
    	private final List<Class<?>> defaultGroupSequence;
    	private final DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider;
    	... // 它自己并不处理什么逻辑,参数都是通过构造器传进来的
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    它的继承树:
    在这里插入图片描述

    三个实现类对应着上面所述的三种元数据类型。本文很显然只需要关注和注解相关的:AnnotationMetaDataProvider


    AnnotationMetaDataProvider

    这个元数据均来自于注解的标注,然后它是Hibernate Validation的默认configuration source。它这里会处理标注有@Valid的元素~

    public class AnnotationMetaDataProvider implements MetaDataProvider {
    
    	private final ConstraintHelper constraintHelper;
    	private final TypeResolutionHelper typeResolutionHelper;
    	private final AnnotationProcessingOptions annotationProcessingOptions;
    	private final ValueExtractorManager valueExtractorManager;
    
    	// 这是一个非常重要的属性,它会记录着当前Bean  所有的待校验的Bean信息~~~
    	private final BeanConfiguration<Object> objectBeanConfiguration;
    
    	// 唯一构造函数
    	public AnnotationMetaDataProvider(ConstraintHelper constraintHelper,
    			TypeResolutionHelper typeResolutionHelper,
    			ValueExtractorManager valueExtractorManager,
    			AnnotationProcessingOptions annotationProcessingOptions) {
    		this.constraintHelper = constraintHelper;
    		this.typeResolutionHelper = typeResolutionHelper;
    		this.valueExtractorManager = valueExtractorManager;
    		this.annotationProcessingOptions = annotationProcessingOptions;
    
    		// 默认情况下,它去把Object相关的所有的方法都retrieve:检索出来放着  我比较费解这件事~~~  
    		// 后面才发现:一切为了效率
    		this.objectBeanConfiguration = retrieveBeanConfiguration( Object.class );
    	}
    
    	// 实现接口方法
    	@Override
    	public AnnotationProcessingOptions getAnnotationProcessingOptions() {
    		return new AnnotationProcessingOptionsImpl();
    	}
    
    
    	// 如果你的Bean是Object  就直接返回了~~~(大多数情况下  都是Object)
    	@Override
    	@SuppressWarnings("unchecked")
    	public <T> BeanConfiguration<T> getBeanConfiguration(Class<T> beanClass) {
    		if ( Object.class.equals( beanClass ) ) {
    			return (BeanConfiguration<T>) objectBeanConfiguration;
    		}
    		return retrieveBeanConfiguration( beanClass );
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    如上可知,核心解析逻辑在retrieveBeanConfiguration()这个私有方法上。总结一下调用此方法的两个原始入口(一个构造器,一个接口方法):

    • ValidatorFactory.getValidator()获取校验器的时候,初始化时会自己new一个BeanMetaDataManager:

    在这里插入图片描述

    • 调用Validator.validate()方法的时候,beanMetaDataManager.getBeanMetaData( rootBeanClass )它会遍历初始化时所有的metaDataProviders(默认情况下两个,没有xml方式的),拿出所有的BeanConfiguration交给BeanMetaDataBuilder,最终构建出一个属于此Bean的BeanMetaData。对此有一点注意事项描述如下:

    处理MetaDataProvider时会调用ClassHierarchyHelper.getHierarchy( beanClass )方法,不仅仅处理本类。拿到本类自己和所有父类后,统一交给provider.getBeanConfiguration( clazz )处理(也就是说任何一个类都会把Object类处理一遍)

    在这里插入图片描述


    retrieveBeanConfiguration()详情

    这个方法说白了,就是从Bean里面去检索属性、方法、构造器等需要校验的ConstrainedElement项。

    	private <T> BeanConfiguration<T> retrieveBeanConfiguration(Class<T> beanClass) {
    		// 它检索的范围是:clazz.getDeclaredFields()  什么意思:就是搜集到本类所有的字段  包括private等等  但是不包括父类的所有字段
    		Set<ConstrainedElement> constrainedElements = getFieldMetaData( beanClass );
    		constrainedElements.addAll( getMethodMetaData( beanClass ) );
    		constrainedElements.addAll( getConstructorMetaData( beanClass ) );
    
    		//TODO GM: currently class level constraints are represented by a PropertyMetaData. This
    		//works but seems somewhat unnatural
    		// 这个TODO很有意思:当前,类级约束由PropertyMetadata表示。这是可行的,但似乎有点不自然
    		// ReturnValueMetaData、ExecutableMetaData、ParameterMetaData、PropertyMetaData
    
    		// 总之吧:此处就是把类级别的校验器放进来了(这个set大部分时候都是空的)
    		Set<MetaConstraint<?>> classLevelConstraints = getClassLevelConstraints( beanClass );
    		if (!classLevelConstraints.isEmpty()) {
    			ConstrainedType classLevelMetaData = new ConstrainedType(ConfigurationSource.ANNOTATION, beanClass, classLevelConstraints);
    			constrainedElements.add(classLevelMetaData);
    		}
    		
    		// 组装成一个BeanConfiguration返回
    		return new BeanConfiguration<>(ConfigurationSource.ANNOTATION, beanClass,
    				constrainedElements, 
    				getDefaultGroupSequence( beanClass ),  //此类上标注的所有@GroupSequence注解
    				getDefaultGroupSequenceProvider( beanClass ) // 此类上标注的所有@GroupSequenceProvider注解
    		);
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这一步骤把该Bean上的字段、方法等等需要校验的项都提取出来。就拿上例中的Demo校验Person类来说,最终得出的BeanConfiguration如下:(两个)

    在这里插入图片描述
    在这里插入图片描述
    这是直观的结论,可以看到仅仅是一个简单的类其实所包含的项是挺多的。

    此处说一句:项是有这么多,但是并不是每一个都需要走验证逻辑的。因为毕竟大多数项上面并没有约束(注解),大多数ConstrainedElement.getConstraints()为空嘛

    总得来说,我个人建议不能光只记忆结论,因为那很容易忘记,所以还是得稍微深入一点,让记忆更深刻吧。那就从下面四个方面深入:

    检索Field:getFieldMetaData( beanClass )

    • 拿到本类所有字段Field:clazz.getDeclaredFields()
    • 把每个Field都包装成ConstrainedElement存放起来
    • 注意:此步骤完成了对每个Field上标注的注解进行了保存

    检索Method:getMethodMetaData( beanClass ):

    • 拿到本类所有的方法Method:clazz.getDeclaredMethods()
    • 排除掉静态方法和合成(isSynthetic)方法
    • 把每个Method都转换成一个ConstrainedExecutable装着~~(ConstrainedExecutable也是个ConstrainedElement)。在此期间它完成了如下事(方法和构造器都复杂点,因为包含入参和返回值):
      • 1. 找到方法上所有的注解保存起来
      • 2. 处理入参、返回值(包括自动判断是作用在入参还是返回值上)

    检索Constructor:getConstructorMetaData( beanClass ):

    完全同处理Method,略

    检索Type:getClassLevelConstraints( beanClass ):

    • 找打标注在此类上的所有的注解,转换成ConstraintDescriptor
    • 对已经找到每个ConstraintDescriptor进行处理,最终都转换Set>这个类型
    • 把Set>用一个ConstrainedType包装起来(ConstrainedType是个ConstrainedElement)

    关于级联校验元数据提取是由findCascadingMetaData方法完成(@Valid信息在这里被提取出来),我们这里更关心的是该方法在哪些场景下会被调用,也就说明了级联校验在哪些场景下会生效了:

    	// type解释:分如下N中情况
    	// Field为:.getGenericType() // 字段的类型
    	// Method为:.getGenericReturnType() // 返回值类型
    	// Constructor:.getDeclaringClass() // 构造器所在类
    
    	// annotatedElement:可不一定说一定要有注解才能进来(每个字段、方法、构造器等都能传进来)
    	private CascadingMetaDataBuilder getCascadingMetaData(Type type, AnnotatedElement annotatedElement, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData) {
    		return CascadingMetaDataBuilder.annotatedObject( type, annotatedElement.isAnnotationPresent( Valid.class ), containerElementTypesCascadingMetaData, getGroupConversions( annotatedElement ) );
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    findCascadingMetaData方法在提取对象属性元数据和方法,构造方法元数据提取中都会进行调用。


    validator.validate方法源码流程简析

    获取元数据信息,准备上下文环境

    • 解析当前对象拿到对象的元数据信息
    	@Override
    	public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
    		...
    		Class<T> rootBeanClass = (Class<T>) object.getClass();
    		BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
            //如果当前对象没有相关约束,按摩直接返回空
    		if ( !rootBeanMetaData.hasConstraints() ) {
    			return Collections.emptySet();
    		}
            //准备好validationContext和valueContext 
    		BaseBeanValidationContext<T> validationContext = getValidationContextBuilder().forValidate( rootBeanClass, rootBeanMetaData, object );
    
    		ValidationOrder validationOrder = determineGroupValidationOrder( groups );
    		BeanValueContext<?, Object> valueContext = ValueContexts.getLocalExecutionContextForBean(
    				validatorScopedContext.getParameterNameProvider(),
    				object,
    				validationContext.getRootBeanMetaData(),
    				PathImpl.createRootPath()
    		);
            //利用validationContext和valueContext完成对bean对象的校验
    		return validateInContext( validationContext, valueContext, validationOrder );
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    validationContext最重要的部分就是其内部管理的BeanMetaData,也就是对象的元数据信息。

    在这里插入图片描述
    valueContext更加侧重于对对象属性值获取和验证的相关操作
    在这里插入图片描述
    BeanMetaData是完成数据校验的核心,他的结构如下:
    在这里插入图片描述
    BeanMetaData内部记录了当前对象相关约束信息,并且内部的allMetaConstraints数组内记录了约束信息,该数组内每一个MetaConstraint内部提供的ConstraintTree负责完成具体的验证逻辑:
    在这里插入图片描述
    validationOrder保存的就是用户需要同时校验几个分组:

    Set<ConstraintViolation<Person>> result = validator.validate(person, Person.Simple.class,Person.Complex.class);
    
    • 1

    在这里插入图片描述


    按照分组挨个进行校验

    • validateInContext利用validationContext和valueContext 提供的上下文信息完成数据校验
    	private <T, U> Set<ConstraintViolation<T>> validateInContext(BaseBeanValidationContext<T> validationContext, BeanValueContext<U, Object> valueContext,
    			ValidationOrder validationOrder) {
    		...
    		
    		BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
    		
    		...
    
    		//将用户需要进行校验的分组挨个进行校验
    		Iterator<Group> groupIterator = validationOrder.getGroupIterator();
    		while ( groupIterator.hasNext() ) {
    			Group group = groupIterator.next();
    			//可以猜到valueContext负责完成对属于当前分组的约束的校验
    			valueContext.setCurrentGroup( group.getDefiningClass() );
    			//进行具体校验
    			validateConstraintsForCurrentGroup( validationContext, valueContext );
    			//如果设置了failFast标记为真,并且当前分组校验产生了错误,那么直接短路返回
    			//默认为false
    			if ( shouldFailFast( validationContext ) ) {
    				return validationContext.getFailingConstraints();
    			}
    		}
            
            //再对每个分组的级联属性进行校验
    		groupIterator = validationOrder.getGroupIterator();
    		while ( groupIterator.hasNext() ) {
    			Group group = groupIterator.next();
    			valueContext.setCurrentGroup( group.getDefiningClass() );
    			validateCascadedConstraints( validationContext, valueContext );
    			if ( shouldFailFast( validationContext ) ) {
    				return validationContext.getFailingConstraints();
    			}
    		}
            
            //这块暂时忽略---问题不大
    		// now we process sequences. For sequences I have to traverse the object graph since I have to stop processing when an error occurs.
    		Iterator<Sequence> sequenceIterator = validationOrder.getSequenceIterator();
    		while ( sequenceIterator.hasNext() ) {
    			...
    		}
    		//返回上面校验完后的错误结果
    		return validationContext.getFailingConstraints();
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    设置快速失败的作用上面也体现出来了:

            HibernateValidatorConfiguration configure = Validation.byProvider(HibernateValidator.class).configure();
            ValidatorFactory validatorFactory = configure.failFast(false).buildValidatorFactory();
    
    • 1
    • 2

    对当前分组的非级联属性完成校验

    • validateConstraintsForCurrentGroup: 对当前分组的非级联属性完成校验
    	private void validateConstraintsForCurrentGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<?, Object> valueContext) {
    		// we are not validating the default group there is nothing special to consider. If we are validating the default
    		// group sequence we have to consider that a class in the hierarchy could redefine the default group sequence.
    		//判断是对默认分组进行校验还是用户自定义分组
    		if ( !valueContext.validatingDefault() ) {
    			validateConstraintsForNonDefaultGroup( validationContext, valueContext );
    		}
    		else {
    			validateConstraintsForDefaultGroup( validationContext, valueContext );
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • validateConstraintsForNonDefaultGroup: 先看看对用户自定义分组的校验过程
    	private void validateConstraintsForNonDefaultGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<?, Object> valueContext) {
    	    //进行校验
    		validateMetaConstraints( validationContext, valueContext, valueContext.getCurrentBean(), valueContext.getCurrentBeanMetaData().getMetaConstraints() );
    		//标记当前对象已经被处理过了
    		validationContext.markCurrentBeanAsProcessed( valueContext );
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • validateMetaConstraints: 对MetaConstraints集合中,每个MetaConstraint进行校验

    在对bean对象进行元数据提取的时候,会将当前对象上每条约束都提取为一个MetaConstraint

    	private void validateMetaConstraints(BaseBeanValidationContext<?> validationContext, ValueContext<?, Object> valueContext, Object parent,
    			Iterable<MetaConstraint<?>> constraints) {
            //对每条metaConstraint都进行校验,然后判断是否要快速失败
    		for ( MetaConstraint<?> metaConstraint : constraints ) {
    			validateMetaConstraint( validationContext, valueContext, parent, metaConstraint );
    			//如果当前metaConstraint校验失败了,并且快速失败标记为真,那么就直接跳过后面的约束校验
    			if ( shouldFailFast( validationContext ) ) {
    				break;
    			}
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • validateMetaConstraint: 对单个MetaConstraint进行校验
    	private boolean validateMetaConstraint(BaseBeanValidationContext<?> validationContext, ValueContext<?, Object> valueContext, Object parent, MetaConstraint<?> metaConstraint) {
    		BeanValueContext.ValueState<Object> originalValueState = valueContext.getCurrentValueState();
    		valueContext.appendNode( metaConstraint.getLocation() );
    		boolean success = true;
             //如果当前约束对应的分组不是当前分组,那么就跳过不进行处理,当然还有别的过滤逻辑,但是都不重要
    		if ( isValidationRequired( validationContext, valueContext, metaConstraint ) ) {
                //如果是校验对象里面的属性的话,那么这里parent就是属性属于的那个对象
    			if ( parent != null ) {
    			//将当前属性在对象中的值提前出来,设置到对应的valueContext保存
    			//CurrentValidatedValue表示当前需要被进行校验的属性值
    				valueContext.setCurrentValidatedValue( valueContext.getValue( parent, metaConstraint.getLocation() ) );
    			}
                //metaConstraint的validateConstraint完成数据校验---valueContext存放了当前被校验属性对应的值
    			success = metaConstraint.validateConstraint( validationContext, valueContext );
                //当前metaConstraint被标记已经被处理过了
    			validationContext.markConstraintProcessed( valueContext.getCurrentBean(), valueContext.getPropertyPath(), metaConstraint );
    		}
    
    		// reset the value context to the state before this call
    		valueContext.resetValueState( originalValueState );
    
    		return success;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • metaConstraint.validateConstraint: 完成对当前metaConstraint约束校验的逻辑如下:
    	public boolean validateConstraint(ValidationContext<?> validationContext, ValueContext<?, Object> valueContext) {
    		   ...
    		   //核心
    			success = doValidateConstraint( validationContext, valueContext );
               ...
    		return success;
    	}
    
    	private boolean doValidateConstraint(ValidationContext<?> executionContext, ValueContext<?, ?> valueContext) {
    	    //当前校验是字段校验,方法校验,还是类级别校验
    		valueContext.setConstraintLocationKind( getConstraintLocationKind() );
    		//利用MetaConstraint内部的ConstraintTree的validateConstraints完成最终的校验逻辑,如果出现错误
    		//错误信息会被放到validationContext中,这里也就是executionContext中
    		boolean validationResult = constraintTree.validateConstraints( executionContext, valueContext );
    		return validationResult;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    所以完成数据校验的核心逻辑是在MetaConstraint内部的constraintTree的validateConstraints方法中


    constraintTree的validateConstraints方法完成最终校验

    在这里插入图片描述

    这里目前只涉及到单个约束进行校验,还没有涉及到复合校验,因此constraintTree的具体实现是: SimpleConstraintTree

    • 首先是父类ConstraintTree的validateConstraints方法:
    	public final boolean validateConstraints(ValidationContext<?> validationContext, ValueContext<?, ?> valueContext) {
    	    //存放校验错误信息的集合,如果没有校验成功通过,那么该集合为空
    		List<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts = new ArrayList<>( 5 );
    		//调用子类的实现,完成最终的校验逻辑
    		validateConstraints( validationContext, valueContext, violatedConstraintValidatorContexts );
    		//判断当前约束校验是成功还是失败
    		if ( !violatedConstraintValidatorContexts.isEmpty() ) {
    			for ( ConstraintValidatorContextImpl constraintValidatorContext : violatedConstraintValidatorContexts ) {
    				for ( ConstraintViolationCreationContext constraintViolationCreationContext : constraintValidatorContext.getConstraintViolationCreationContexts() ) {
    				//添加失败错误到validationContext
    					validationContext.addConstraintFailure(
    							valueContext, constraintViolationCreationContext, constraintValidatorContext.getConstraintDescriptor()
    					);
    				}
    			}
    			return false;
    		}
    		return true;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • SimpleConstraintTree的validateConstraints方法完成真正的约束校验逻辑
    	@Override
    	protected void validateConstraints(ValidationContext<?> validationContext,
    			ValueContext<?, ?> valueContext,
    			Collection<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts) {
    		...
    		// find the right constraint validator
    		//初始化当前约束注解对应的校验器
    		ConstraintValidator<B, ?> validator = getInitializedConstraintValidator( validationContext, valueContext );
    
    		// create a constraint validator context
    		//约束校验器上下文环境
    		ConstraintValidatorContextImpl constraintValidatorContext = validationContext.createConstraintValidatorContextFor(
    				descriptor, valueContext.getPropertyPath()
    		);
    
    		// validate
    		//validateSingleConstraint完成单个约束校验,返回的是optional对象,如果optional内部存在对象,说明是错误信息
    		//否则说明校验成功,没有出错
    		if ( validateSingleConstraint( valueContext, constraintValidatorContext, validator ).isPresent() ) {
    			violatedConstraintValidatorContexts.add( constraintValidatorContext );
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • validateSingleConstraint可见应该是谜题揭晓的地方了
    	protected final <V> Optional<ConstraintValidatorContextImpl> validateSingleConstraint(
    			ValueContext<?, ?> valueContext,
    			ConstraintValidatorContextImpl constraintValidatorContext,
    			ConstraintValidator<A, V> validator) {
    		boolean isValid;
    		try {
    			@SuppressWarnings("unchecked")
    			//从valueContext中取出需要被校验的值
    			V validatedValue = (V) valueContext.getCurrentValidatedValue();
    			//调用校验器的isValid方法,通过返回值决定是否校验成功,第一个参数是需要被校验的值,第二个参数是上下文环境
    			isValid = validator.isValid( validatedValue, constraintValidatorContext );
    		}
    		catch (RuntimeException e) {
    			if ( e instanceof ConstraintDeclarationException ) {
    				throw e;
    			}
    			throw LOG.getExceptionDuringIsValidCallException( e );
    		}
    		//如果校验失败了,会返回传入的constraintValidatorContext 
    		if ( !isValid ) {
    			//We do not add these violations yet, since we don't know how they are
    			//going to influence the final boolean evaluation
    			return Optional.of( constraintValidatorContext );
    		}
    		//校验成功了,返回的是空对象
    		return Optional.empty();
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    对当前分组的级联属性完成校验

    在对分组中的普通属性校验完毕后,下面就需要对级联属性进行校验:

            ....
    		groupIterator = validationOrder.getGroupIterator();
    		while ( groupIterator.hasNext() ) {
    			Group group = groupIterator.next();
    			valueContext.setCurrentGroup( group.getDefiningClass() );
    			//进行级联属性的校验
    			validateCascadedConstraints( validationContext, valueContext );
    			if ( shouldFailFast( validationContext ) ) {
    				return validationContext.getFailingConstraints();
    			}
    		}
    		....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    validateCascadedConstraints的核心逻辑就是递归校验;

    	private void validateCascadedConstraints(BaseBeanValidationContext<?> validationContext, ValueContext<?, Object> valueContext) {
    	    //这里Validatable就是上面的beanMetea元数据
    		Validatable validatable = valueContext.getCurrentValidatable();
    		BeanValueContext.ValueState<Object> originalValueState = valueContext.getCurrentValueState() ;
            //获取当前对象中所有标注了@Valid注解的级联属性,依次处理
    		for ( Cascadable cascadable : validatable.getCascadables() ) {
    			   ...
    			   //拿到当前级联属性对应的值
    				Object value = getCascadableValue( validationContext, valueContext.getCurrentBean(), cascadable );
    				//拿到级联属性对应的元数据 
    				CascadingMetaData cascadingMetaData = cascadable.getCascadingMetaData();
                      ...
    			//当前级联属性按照当前属性属于的分组进行校验
    			validateCascadedAnnotatedObjectForCurrentGroup( value, validationContext, valueContext, effectiveCascadingMetaData );
    					...
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    validateCascadedAnnotatedObjectForCurrentGroup这里就要进入递归校验了:

    	private void validateCascadedAnnotatedObjectForCurrentGroup(Object value, BaseBeanValidationContext<?> validationContext, ValueContext<?, Object> valueContext,
    			CascadingMetaData cascadingMetaData) {
    		
    		Class<?> originalGroup = valueContext.getCurrentGroup();
    		Class<?> currentGroup = cascadingMetaData.convertGroup( originalGroup );
           ...
           //ValidationOrder中保存的分组就是当前级联属性属于的分组
    		ValidationOrder validationOrder = validationOrderGenerator.getValidationOrder( currentGroup, currentGroup != originalGroup );
            //构建级联属性对应的ValueContext,而validationContext和父亲用同一个
    		BeanValueContext<?, Object> cascadedValueContext = buildNewLocalExecutionContext( valueContext, value ); 
           //开始递归
    		validateInContext( validationContext, cascadedValueContext, validationOrder );
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    小结

    到这里为止,我们已经基本把validator进行validate数据校验的核心源码大致过了一遍。

    如果大家还在思考为什么某个约束注解没生效,或者级联属性为什么没有被解析,这些问题需要去看一下元数据信息提取的过程,看看你写的注解是否被探查到了,这部分我上面并没有讲,大家可以在遇到问题的时候,自行去debug源码。


    常用约束注解解释

    所有的约束注解都是可以重复标记的,因为它身上都有如下重复标记的标注:

    @Repeatable(List.class)
    
    • 1

    java:@Repeatable注解使用

    JSR标准注解:

    在这里插入图片描述
    在这里插入图片描述
    说明:

    1. @DecimalMax和@Max的区别:

      1. @DecimalMax支持类型:Number、BidDecimal、Float、Double、BigInteger、Long
      2. @Max支持的类型:同上
      3. 它俩都还能标注在String上,比如“6”这种字符串。(若你不是数字字符串,永远校验不通过)
    2. 所有没有特殊说明的:null is valid

    3. 若在不支持的类型上使用约束注解,运行时抛出异常:javax.validation.UnexpectedTypeException:No validator could be found for constraint ‘javax.validation.constraints.Future’ validating type ‘java.lang.String’

    4. @FutureOrPresent和@PastOrPresent这块注意:对于Present的匹配,要注意程序是有执行时间的。so如果是匹配时间戳Instant,若是Instant.now()的话,@FutureOrPresent就是非法的,而@PastOrPresent就成合法的了。但是若是日期的话比如LocalDate.now()就不会有这问题,毕竟你的程序不可能执行一天嘛

    5. @NotNull:有的人问用在基本类型(非包装类型报错吗?),很显然不会报错。因为基本类型都有默认值,不可能为null的

    6. 所有的注解都能标注在:字段、方法、构造器、入参、以及注解上

    JSR的注解都申明都非常的简单,没有Hibernate提供的复杂,比如没有用到@ReportAsSingleViolation等注解内容~为了方面,下面列出各个注解的默认提示消息(中文):

    javax.validation.constraints.AssertFalse.message     = 只能为false
    javax.validation.constraints.AssertTrue.message      = 只能为true
    javax.validation.constraints.DecimalMax.message      = 必须小于或等于{value}
    javax.validation.constraints.DecimalMin.message      = 必须大于或等于{value}
    javax.validation.constraints.Digits.message          = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
    javax.validation.constraints.Email.message           = 不是一个合法的电子邮件地址
    javax.validation.constraints.Future.message          = 需要是一个将来的时间
    javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
    javax.validation.constraints.Max.message             = 最大不能超过{value}
    javax.validation.constraints.Min.message             = 最小不能小于{value}
    javax.validation.constraints.Negative.message        = 必须是负数
    javax.validation.constraints.NegativeOrZero.message  = 必须是负数或零
    javax.validation.constraints.NotBlank.message        = 不能为空
    javax.validation.constraints.NotEmpty.message        = 不能为空
    javax.validation.constraints.NotNull.message         = 不能为null
    javax.validation.constraints.Null.message            = 必须为null
    javax.validation.constraints.Past.message            = 需要是一个过去的时间
    javax.validation.constraints.PastOrPresent.message   = 需要是一个过去或现在的时间
    javax.validation.constraints.Pattern.message         = 需要匹配正则表达式"{regexp}"
    javax.validation.constraints.Positive.message        = 必须是正数
    javax.validation.constraints.PositiveOrZero.message  = 必须是正数或零
    javax.validation.constraints.Size.message            = 个数必须在{min}{max}之间
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    参考文件ValidationMessages_zh_CN.properties,若消息不适合你,可自行定制~


    Hibernate Validation扩展的注解

    在这里插入图片描述
    说明:

    1. @ReportAsSingleViolation:如果@NotEmpty、@Pattern都校验失败,不添加此注解,则会生成两个校验失败的结果。若添加了此注解,那错误消息以它标注的本注解的message为准
    2. 所有没有特殊说明的:null is valid。
    3. 所有约束注解都可重复标注

    各个注解的默认提示消息(中文):

    org.hibernate.validator.constraints.CreditCardNumber.message        = 不合法的信用卡号码
    org.hibernate.validator.constraints.Currency.message                = 不合法的货币 (必须是{value}其中之一)
    org.hibernate.validator.constraints.EAN.message                     = 不合法的{type}条形码
    org.hibernate.validator.constraints.Email.message                   = 不是一个合法的电子邮件地址
    org.hibernate.validator.constraints.Length.message                  = 长度需要在{min}{max}之间
    org.hibernate.validator.constraints.CodePointLength.message         = 长度需要在{min}{max}之间
    org.hibernate.validator.constraints.LuhnCheck.message               = ${validatedValue}的校验码不合法, Luhn10校验和不匹配
    org.hibernate.validator.constraints.Mod10Check.message              = ${validatedValue}的校验码不合法,10校验和不匹配
    org.hibernate.validator.constraints.Mod11Check.message              = ${validatedValue}的校验码不合法,11校验和不匹配
    org.hibernate.validator.constraints.ModCheck.message                = ${validatedValue}的校验码不合法, ${modType}校验和不匹配
    org.hibernate.validator.constraints.NotBlank.message                = 不能为空
    org.hibernate.validator.constraints.NotEmpty.message                = 不能为空
    org.hibernate.validator.constraints.ParametersScriptAssert.message  = 执行脚本表达式"{script}"没有返回期望结果
    org.hibernate.validator.constraints.Range.message                   = 需要在{min}{max}之间
    org.hibernate.validator.constraints.SafeHtml.message                = 可能有不安全的HTML内容
    org.hibernate.validator.constraints.ScriptAssert.message            = 执行脚本表达式"{script}"没有返回期望结果
    org.hibernate.validator.constraints.URL.message                     = 需要是一个合法的URL
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    此处用到了$ { validatedValue }、$ { modType }是EL表达式的语法。

    @DurationMax和@DurationMin的message消息此处未贴出,有大量的EL计算,太长了~~~


    参考

    深入了解数据校验(Bean Validation):从深处去掌握@Valid的作用(级联校验)以及常用约束注解的解释说明【享学Java】

  • 相关阅读:
    oracle常用命令
    SaaSBase:UiPath是什么?
    【算法系列篇】分治-归并
    python+django+vue酒店入住客房管理系统
    DDD—Repository仓储&工厂模式
    java毕设项目课程与成绩管理(附源码)
    手写Promise.all/race/any/settled方法
    No module named ‘PyQt5.QtWebEngineWidgets‘kn-----已解决
    ps命令介绍及常用操作和参数说明
    山西电力市场日前价格预测【2023-10-06】
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/126011357