SpringBoot MVC(5)请求参数的验证

1,067 阅读6分钟

SpringBoot集合了验证框架包,常见用法是在字段上添加注解,使用验证注解可以减少在Controller写参数的验证,从而简化开发流程。验证框架十分庞大,本文所展示的不过是它的冰山一角。

在获得请求参数之后,会有一段参数验证的过程,当然,只有在开启参数验证的时候,才会进行。

验证类demo

public class Tree {
    @Min(3)
    private Integer id;

    private Integer pid;	
}

是否需要参数验证

ModelAttributeMethodProcessor->resolveArgument():
    // 绑定参数
    bindRequestParameters(binder, webRequest);
    validateIfApplicable(binder, parameter);
ModelAttributeMethodProcessor->validateIfApplicable():
    // 获取参数的注解
    for (Annotation ann : parameter.getParameterAnnotations()) {
        Object[] validationHints = determineValidationHints(ann);
        // 以下是参数验证的过程
        if (validationHints != null) {
            binder.validate(validationHints);
            break;
        }
    }
ModelAttributeMethodProcessor->determineValidationHints():
    Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
    if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
        ......

从代码中可以看出,是否需要参数验证的关键是参数中有@Validated或以@Valid开头的注解,这就是为什么只有加上这样的注解参数验证才能生效的原因了。

参数验证的过程

// 接前面的binder.validate(validationHints)
DataBinder->validate():
    for (Validator validator : getValidators()) {
        // 第一次传过来的时候validationHints必然为空,所以走else if 方法
        if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
            ((SmartValidator) validator).validate(target, bindingResult, validationHints);
        }
        else if (validator != null) {
            validator.validate(target, bindingResult);
        }
    }
ValidatorAdapter->validate():
    // 注意这里的this.target和和target并非同一对象
    this.target.validate(target, errors);
SpringValidatorAdapter->validate():
    if (this.targetValidator != null) {
        processConstraintViolations(this.targetValidator.validate(target), errors);
    }

验证的查找与限制

this.targetValidator.validate(target),这里的target是实例化的tree对象实例

ValidatorImpl->validate():
    ValidationContext<T> validationContext = getValidationContextBuilder().forValidate( object );
ValidationContext->ValidationContextBuilder()->forValidate():
    // 这是构造验证类的过程,关注这句代码beanMetaDataManager.getBeanMetaData( rootBeanClass )
    
BeanMetaDataManager->getBeanMetaData():
    BeanMetaData<T> beanMetaData = (BeanMetaData<T>) beanMetaDataCache.computeIfAbsent( beanClass,
				bc -> createBeanMetaData( bc ) );
BeanMetaDataManager->createBeanMetaData():	// --1
    // 第一个是ProgrammaticMetaDataProvider类,它没有设置
    // 第二个是AnnotationMetaDataProvider类,意为通过注解提供验证
    for ( MetaDataProvider provider : metaDataProviders ) {
        for ( BeanConfiguration<? super T> beanConfiguration : getBeanConfigurationForHierarchy( provider, clazz ) ) {
            builder.add( beanConfiguration );	
        }
    }
BeanMetaDataManager->getBeanConfigurationForHierarchy():
    BeanConfiguration<? super T> configuration = provider.getBeanConfiguration( clazz );
AnnotationMetaDataProvider->getBeanConfiguration():
    if ( Object.class.equals( beanClass ) ) {
        return (BeanConfiguration<T>) objectBeanConfiguration;
    }
    // 获取类中的限制类型
    return retrieveBeanConfiguration( beanClass );
AnnotationMetaDataProvider->retrieveBeanConfiguration():
    // 获取字段中的限制类型
    Set<ConstrainedElement> constrainedElements = getFieldMetaData( beanClass );
    // 遍历所有的方法,去除非静态方法
    constrainedElements.addAll( getMethodMetaData( beanClass ) );
    // 遍历所有的构造方法
    constrainedElements.addAll( getConstructorMetaData( beanClass ) );
AnnotationMetaDataProvider->getFieldMetaData():
    // 遍历所有字段,去除非静态字段
    propertyMetaData.add( findPropertyMetaData( field ) );
AnnotationMetaDataProvider->findPropertyMetaData():
    Set<MetaConstraint<?>> constraints = convertToMetaConstraints(
            findConstraints( field, ElementType.FIELD ),
            field
    );
AnnotationMetaDataProvider->findConstraints():
    for ( Annotation annotation : ( (AccessibleObject) member ).getDeclaredAnnotations() ) {
        // 接下来是得到字段的注解,如果非jdk注解并且是验证注解的话,则会封装成ConstraintDescriptorImpl类,并添加到metaData中
        metaData.addAll( findConstraintAnnotations( member, annotation, type ) );
    }
    return metaData;

查找是否有限制注解的代码从AnnotationMetaDataProvider->findConstraintAnnotations()开始,如@NotNull、@NotEmpty、@Min这些被提前封装成了map集合的key,只有注解在集合的key中,才返回true,具体有哪些注解,这些注解对应处理的字段类型ConstraintHelper->ConstraintHelper()构造方法以及ConstraintHelper->builtinConstraints变量

getBeanConfigurationForHierarchy(provider, clazz)得到的是封装的BeanConfiguration对象数组,从tree开始解析,然后是其父类,直到Object类,它的属性如下

BeanConfiguration重要属性与作用:

  • source 来源,AnnotationMetaDataProvider固定为ANNOTATION
  • beanClass 解析的bean
  • constrainedElements 解析的元素,包括字段,方法和构造方法,位于AnnotationMetaDataProvider->getFieldMetaData(),不管有没有限制,只要解析了,就会添加进集合中
    • 如果是方法或者构造方法,则属于ConstrainedExecutable类
    • 如果是字段,则属于ConstrainedField类
      • constraints 限制的set集合
        • constraintTree 封装了限制信息
          • descriptor 限制描述
            • annotationDescriptor 限制的注解描述
              • type 注解类
              • attributes 注解的属性
            • constraintValidatorClasses 限制类枚举
            • matchingConstraintValidatorDescriptors 限制类枚举描述
              • validatorClass 限制类,对应constraintValidatorClasses中的一个
              • validatedType 限制的字段类
              • validationTargets 固定为ANNOTATED_ELEMENT
          • validatedValueType 字段的值类型
        • location 记录的是字段的相关信息

@Min(3) private Integer id被封装成了下图中复杂的信息

BeanMetaDataManager->createBeanMetaData():	// --1
    for ( MetaDataProvider provider : metaDataProviders ) {
        for ( BeanConfiguration<? super T> beanConfiguration : getBeanConfigurationForHierarchy( provider, clazz ) ) {
            // 添加进build的builds变量中,builds是一个set集合
            builder.add( beanConfiguration );   
        }
    }
    return builder.build();

builder.build()方法,将tree及其父类的所有字段和方法存入Set集合aggregatedElements中,区分出方法还是字段,然后返回封装成BeanMetaDataImpl对象。

BeanMetaDataImpl重要属性与作用:

  • hasConstraints 是否有限制
  • allMetaConstraints 所有限制
  • directMetaConstraints 属于自己类的限制
  • executableMetaDataMap 限制方法,map集合
  • unconstrainedExecutables 没有限制的方法,set集合
  • propertyMetaDataMap 字段描述,map集合
  • beanDescriptor 限制描述
    • constraintedProerties 限制的字段
      • propertyName 字段名称
      • type 字段类型
      • constraintDescriptors 限制描述,这里的结构与前面的descriptor 限制描述相同
    • constraintedMethods 限制的方法
    • constraintedConstructors 限制的构造方法
  • classHierarchyWithoutInterfaces 解析过的类

验证是否生效

ValidatorImpl->validate():
    // 前面是一段查找限制的过程,BeanMetaDataImpl被封装成ValidationContext和ValueContext的一个属性,validationOrder使用默认的排序
    // 该方法返回Set的限制集合
    return validateInContext( validationContext, valueContext, validationOrder );
ValidatorImpl->validateInContext():
    validateConstraintsForCurrentGroup( validationContext, valueContext );
ValidatorImpl->validateConstraintsForCurrentGroup():
    validateConstraintsForDefaultGroup( validationContext, valueContext );
ValidatorImpl->validateConstraintsForDefaultGroup():
    // 获取属于本类限制
    Set<MetaConstraint<?>> metaConstraints = hostingBeanMetaData.getDirectMetaConstraints();
    validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz, metaConstraints,
            Group.DEFAULT_GROUP );
ValidatorImpl->validateConstraintsForSingleDefaultGroupElement():
    // 对metaConstraints遍历,验证是否符合
    boolean tmp = validateMetaConstraint( validationContext, valueContext, valueContext.getCurrentBean(), metaConstraint );
ValidatorImpl->validateMetaConstraint():
    success = metaConstraint.validateConstraint( validationContext, valueContext );
MetaConstraint->validateConstraint():
    success = doValidateConstraint( validationContext, valueContext );
MetaConstraint->doValidateConstraint():
    // 获取验证的类型,这里是FIELD
    valueContext.setElementType( getElementType() );
    boolean validationResult = constraintTree.validateConstraints( executionContext, valueContext );
ConstraintTree->validateConstraints():
    // constraintViolations
    validateConstraints( executionContext, valueContext, constraintViolations );
SimpleConstraintTree->validateConstraints():
    // 注解是否作用于该字段类型,如果匹配,返回约束验证器
    ConstraintValidator<B, ?> validator = getInitializedConstraintValidator( validationContext, valueContext );
    ......
    // 得到validator后,进行验证
    constraintViolations.addAll(
        validateSingleConstraint(
            validationContext,
            valueContext,
            constraintValidatorContext,
            validator
        )
    );
ConstraintTree->validateSingleConstraint():
    // 根据不同的validator得到不同的结果,如果不匹配的话就会返回限制集合
    isValid = validator.isValid( validatedValue, constraintValidatorContext );
注解与字段类型
ConstraintTree->getInitializedConstraintValidator():
    validator = getInitializedConstraintValidator( validationContext );
    // 字段不匹配的话会抛出异常
    if ( validator == DUMMY_CONSTRAINT_VALIDATOR ) {
        throw getExceptionForNullValidator( validatedValueType, valueContext.getPropertyPath().asString() );
    }
    return validator;
ConstraintTree->getInitializedConstraintValidator():
    ConstraintValidator<A, ?> validator = validationContext.getConstraintValidatorManager().getInitializedValidator(
        validatedValueType,
        descriptor,
        validationContext.getConstraintValidatorFactory(),
        validationContext.getConstraintValidatorInitializationContext()
    );
ConstraintValidatorManager->getInitializedValidator():
    constraintValidator = createAndInitializeValidator( validatedValueType, descriptor, constraintValidatorFactory, initializationContext );
ConstraintValidatorManager->createAndInitializeValidator():
    ConstraintValidatorDescriptor<A> validatorDescriptor = findMatchingValidatorDescriptor( descriptor, validatedValueType );
ConstraintValidatorManager->findMatchingValidatorDescriptor():
    // 遍历matchingConstraintValidatorDescriptors,当前字段是否是validatedType类或其子类
    // 没有返回null,有多个时返回第一个
    List<Type> discoveredSuitableTypes = findSuitableValidatorTypes( validatedValueType, availableValidatorDescriptors.keySet() );

从代码中可以看出,主要是遍历matchingConstraintValidatorDescriptors中的validatedType,如果字段类型相等或是其子类,返回validatorClass,如@Min对于Integer类型返回的是MinValidatorForNumber,如果没有符合的类型返回一个默认描述符,当是这个默认描述符的时候,就抛出一个类似于类型不匹配的异常。之后调用描述符的isValid方法返回true/false,如果为false,会创建错误限制集合的描述(如提示信息,对象引用,值,字段,限制信息),如果提示信息没有,则用模板,主要是根据当前语言环境读取对应的文件,获得提示信息(中文读取的是ValidationMessages_zh_CN.properties这个文件)。

最终处理

ConstraintTree->validateConstraints()方法中,如果有错误限制,则会添加到failingConstraintViolations变量中,该变量是一个Set集合,用来存储违反的限制。

回到ValidatorImpl->validateConstraintsForSingleDefaultGroupElement(),进入下一个限制的验证。

回到ValidatorImpl->validateConstraintsForDefaultGroup(),进入父类的验证。

ModelAttributeMethodProcessor->resolveArgument():
    // 如果有错误限制,会将此限制添加到bidder.biddingResult.errors变量中
    validateIfApplicable(binder, parameter);
    // 从这里看出,所有的参数绑定错误都属于BindException异常
    if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
        throw new BindException(binder.getBindingResult());
    }

总结

流程大致如下:

  1. 参数绑定完成后,查询是否开启了参数验证
  2. 如果是,遍历参数类及其父类,找到什么类使用了验证注解,该类的验证字段是什么,该验证字段使用了什么注解
  3. 遍历类,遍历验证字段,如果注解不符合字段类型,则抛出异常;否则查看是否符合,不符合添加进集合
  4. 查询集合是否为空,如果不为空则继续下一个参数的绑定、验证,否则抛出异常