前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >@Valid的作用(级联校验)以及常用约束注解的解释说明

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

作者头像
大忽悠爱学习
发布2022-08-23 11:08:00
3.6K0
发布2022-08-23 11:08:00
举报
文章被收录于专栏:c++与qt学习c++与qt学习

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


分组校验

代码语言:javascript
复制
@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 {

    }
}
代码语言:javascript
复制
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<ConstraintViolation<Person>> result = validator.validate(person, Person.Complex.class);

        // 对结果进行遍历输出
        result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue())
                .forEach(System.out::println);
    }
}

运行打印:

代码语言:javascript
复制
age 最大不能超过10: 18
name {message} -> 名字不能为null -> 名字不能为null: null

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

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


@Valid注解

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

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

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

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


MetaDataProvider

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

  • 基于不同的元数据:如xml、注解。(还有个编程映射) 这三种类型。对应的枚举类为:
代码语言:javascript
复制
public enum ConfigurationSource {
	ANNOTATION( 0 ),
	XML( 1 ),
	API( 2 ); //programmatic API
}
  • MetaDataProvider只返回直接为一个类配置的元数据
  • 它不处理从超类、接口合并的元数据(简单的说你@Valid放在接口处是无效的)
代码语言:javascript
复制
public interface MetaDataProvider {

	// 将 注解处理选项 归还给此Provider配置。  它的唯一实现类为:AnnotationProcessingOptionsImpl
	// 它可以配置比如:areMemberConstraintsIgnoredFor  areReturnValueConstraintsIgnoredFor
	// 也就说可以配置:让免于被校验~~~~~~(开绿灯用的)
	AnnotationProcessingOptions getAnnotationProcessingOptions();
	// 返回作用在此Bean上面的`BeanConfiguration`   若没有就返回null了
	// BeanConfiguration持有ConfigurationSource的引用~
	<T> BeanConfiguration<? super T> getBeanConfiguration(Class<T> beanClass);
	
}
代码语言:javascript
复制
// 表示源于一个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;
	... // 它自己并不处理什么逻辑,参数都是通过构造器传进来的
}

它的继承树:

在这里插入图片描述
在这里插入图片描述

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


AnnotationMetaDataProvider

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

代码语言:javascript
复制
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 );
	}
}

如上可知,核心解析逻辑在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项。

代码语言:javascript
复制
	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注解
		);
	}

这一步骤把该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<MetaConstraint<?>>这个类型
  • 把Set<MetaConstraint<?>>用一个ConstrainedType包装起来(ConstrainedType是个ConstrainedElement)

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

代码语言:javascript
复制
	// 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 ) );
	}

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


validator.validate方法源码流程简析

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

  • 解析当前对象拿到对象的元数据信息
代码语言:javascript
复制
	@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 );
	}

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

在这里插入图片描述
在这里插入图片描述

valueContext更加侧重于对对象属性值获取和验证的相关操作

在这里插入图片描述
在这里插入图片描述

BeanMetaData是完成数据校验的核心,他的结构如下:

在这里插入图片描述
在这里插入图片描述

BeanMetaData内部记录了当前对象相关约束信息,并且内部的allMetaConstraints数组内记录了约束信息,该数组内每一个MetaConstraint内部提供的ConstraintTree负责完成具体的验证逻辑:

在这里插入图片描述
在这里插入图片描述

validationOrder保存的就是用户需要同时校验几个分组:

代码语言:javascript
复制
Set<ConstraintViolation<Person>> result = validator.validate(person, Person.Simple.class,Person.Complex.class);
在这里插入图片描述
在这里插入图片描述

按照分组挨个进行校验

  • validateInContext利用validationContext和valueContext 提供的上下文信息完成数据校验
代码语言:javascript
复制
	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();
	}

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

代码语言:javascript
复制
        HibernateValidatorConfiguration configure = Validation.byProvider(HibernateValidator.class).configure();
        ValidatorFactory validatorFactory = configure.failFast(false).buildValidatorFactory();

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

  • validateConstraintsForCurrentGroup: 对当前分组的非级联属性完成校验
代码语言:javascript
复制
	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 );
		}
	}
  • validateConstraintsForNonDefaultGroup: 先看看对用户自定义分组的校验过程
代码语言:javascript
复制
	private void validateConstraintsForNonDefaultGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<?, Object> valueContext) {
	    //进行校验
		validateMetaConstraints( validationContext, valueContext, valueContext.getCurrentBean(), valueContext.getCurrentBeanMetaData().getMetaConstraints() );
		//标记当前对象已经被处理过了
		validationContext.markCurrentBeanAsProcessed( valueContext );
	}
  • validateMetaConstraints: 对MetaConstraints集合中,每个MetaConstraint进行校验

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

代码语言:javascript
复制
	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;
			}
		}
	}
  • validateMetaConstraint: 对单个MetaConstraint进行校验
代码语言:javascript
复制
	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;
	}
  • metaConstraint.validateConstraint: 完成对当前metaConstraint约束校验的逻辑如下:
代码语言:javascript
复制
	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;
	}

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


constraintTree的validateConstraints方法完成最终校验
在这里插入图片描述
在这里插入图片描述

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

  • 首先是父类ConstraintTree的validateConstraints方法:
代码语言:javascript
复制
	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;
	}
  • SimpleConstraintTree的validateConstraints方法完成真正的约束校验逻辑
代码语言:javascript
复制
	@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 );
		}
	}
  • validateSingleConstraint可见应该是谜题揭晓的地方了
代码语言:javascript
复制
	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();
	}

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

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

代码语言:javascript
复制
        ....
		groupIterator = validationOrder.getGroupIterator();
		while ( groupIterator.hasNext() ) {
			Group group = groupIterator.next();
			valueContext.setCurrentGroup( group.getDefiningClass() );
			//进行级联属性的校验
			validateCascadedConstraints( validationContext, valueContext );
			if ( shouldFailFast( validationContext ) ) {
				return validationContext.getFailingConstraints();
			}
		}
		....

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

代码语言:javascript
复制
	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 );
					...
		}
	}

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

代码语言:javascript
复制
	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 );
	}

小结

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

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


常用约束注解解释

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

代码语言:javascript
复制
@Repeatable(List.class)

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等注解内容~为了方面,下面列出各个注解的默认提示消息(中文):

代码语言:javascript
复制
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}之间

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


Hibernate Validation扩展的注解

在这里插入图片描述
在这里插入图片描述

说明:

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

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

代码语言:javascript
复制
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}的校验码不合法, Luhn模10校验和不匹配
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

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

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


参考

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-07-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • @Valid的作用(级联校验)以及常用约束注解的解释说明
  • 分组校验
  • @Valid注解
    • MetaDataProvider
      • AnnotationMetaDataProvider
  • validator.validate方法源码流程简析
    • 获取元数据信息,准备上下文环境
      • 按照分组挨个进行校验
        • 对当前分组的非级联属性完成校验
        • 对当前分组的级联属性完成校验
    • 小结
    • 常用约束注解解释
      • Hibernate Validation扩展的注解
      • 参考
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档