小架构step系列29:校验注解的组合

1 概述

如果遇到某些属性需要多种校验,比如需要非空、符合某正则表达式、长度不能超过某值等,如果这种属性只有有限几个,那么手工把对应的校验注解都加上即可。但如果这种属性比较多,那么重复加这些校验注解,也是一种代码重复。

hibernate-validator包提供了一种组合注解的方式,来解决上面场景的问题。

2 原理

2.1 例子

先来看一个组合校验注解的例子:

1) 定义一个新注解@FormatedString

import org.hibernate.validator.constraints.CompositionType;
import org.hibernate.validator.constraints.ConstraintComposition;

import javax.validation.Constraint;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@ConstraintComposition(CompositionType.AND)
@NotEmpty
@Pattern(regexp = "")
@Size
@Constraint(validatedBy = {})
public @interface FormatedString {
    String message() default "{string.formated.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    @OverridesAttribute(constraint = Size.class, name = "min")
    int min() default 0;

    @OverridesAttribute(constraint = Size.class, name = "max")
    int max() default Integer.MAX_VALUE;

    @OverridesAttribute(constraint = Pattern.class, name = "regexp")
    String regexp() default "";
}

从上面看,@FormatedString注解的上方指定了非空(@NotEmpty)、正则表达式(@Pattern)、长度限制(@Size)这三个常用的的校验注解。@Constraint里面的validatedBy值为空,代表着自身没有提供ContraintValidator。@ConstraintComposition指定了多个注解校验结果的关系是AND,也就是都需要校验通过则总结果才算通过。

注意:使用@OverridesAttribute把参数传给原注解,这不属于Spring环境,不能使用Spring的@AliasFor来传参数。@Pattern的regexp属性没有给默认值,需要给一个默认值,否则无法通过编译。

2) 使用注解

// 以O开头且后面都是数字,长度在16-20之间
@FormatedString(regexp = "O[0-9]+", min = 16, max = 20)
private String orderNumber;

2.2 处理流程

详细的流程需要参考前面的文章,这里只给出涉及处理组合注解的部分。

// 源码位置:org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl
public ConstraintDescriptorImpl(ConstraintHelper constraintHelper,
        Constrainable constrainable,
        ConstraintAnnotationDescriptor<T> annotationDescriptor,
        ConstraintLocationKind constraintLocationKind,
        Class<?> implicitGroup,
        ConstraintOrigin definedOn,
        ConstraintType externalConstraintType) {
    this.annotationDescriptor = annotationDescriptor;
    this.constraintLocationKind = constraintLocationKind;
    this.definedOn = definedOn;
    this.isReportAsSingleInvalidConstraint = annotationDescriptor.getType().isAnnotationPresent(
            ReportAsSingleViolation.class
    );

    // 省略部分代码
    
    // 1. 当把校验注解的信息和属性封装为ConstraintDescriptorImpl时,还会解析一下校验注解里是否还带其它校验注解。
    //    annotationDescriptor为校验注解(如@NotNull),constrainable为属性字段信息(如标了@NotNull注解的mobilePhone属性字段),constraintType为GENERIC。
    //    返回结果赋值给了composingConstraints。
    this.composingConstraints = parseComposingConstraints( constraintHelper, constrainable, constraintType );
    this.compositionType = parseCompositionType( constraintHelper );
    validateComposingConstraintTypes();

    if ( constraintType == ConstraintType.GENERIC ) {
        this.matchingConstraintValidatorDescriptors = CollectionHelper.toImmutableList( genericValidatorDescriptors );
    }
    else {
        this.matchingConstraintValidatorDescriptors = CollectionHelper.toImmutableList( crossParameterValidatorDescriptors );
    }

    this.hashCode = annotationDescriptor.hashCode();
}

// 源码位置:org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl
private Set<ConstraintDescriptorImpl<?>> parseComposingConstraints(ConstraintHelper constraintHelper, Constrainable constrainable, ConstraintType constraintType) {
    Set<ConstraintDescriptorImpl<?>> composingConstraintsSet = newLinkedHashSet();
    Map<ClassIndexWrapper, Map<String, Object>> overrideParameters = parseOverrideParameters();
    Map<Class<? extends Annotation>, ComposingConstraintAnnotationLocation> composingConstraintLocations = new HashMap<>();

    // 2. annotationDescriptor为校验注解(如@NotNull),从注解上获取标注的其它注解
    for ( Annotation declaredAnnotation : this.annotationDescriptor.getType().getDeclaredAnnotations() ) {
        Class<? extends Annotation> declaredAnnotationType = declaredAnnotation.annotationType();
        // 3. NON_COMPOSING_CONSTRAINT_ANNOTATIONS为@Target、@Retention等常用注解,它们与校验无关,过滤掉
        if ( NON_COMPOSING_CONSTRAINT_ANNOTATIONS.contains( declaredAnnotationType.getName() ) ) {
            continue;
        }

        // 4. 判断注解是否为校验注解,内置的注解和加了@Constraint的注解才是校验注解
        if ( constraintHelper.isConstraintAnnotation( declaredAnnotationType ) ) {
            if ( composingConstraintLocations.containsKey( declaredAnnotationType )
                    && !ComposingConstraintAnnotationLocation.DIRECT.equals( composingConstraintLocations.get( declaredAnnotationType ) ) ) {
                throw LOG.getCannotMixDirectAnnotationAndListContainerOnComposedConstraintException( annotationDescriptor.getType(), declaredAnnotationType );
            }

            // 5. 为每个校验注解也生成一个ConstraintDescriptorImpl
            ConstraintDescriptorImpl<?> descriptor = createComposingConstraintDescriptor(
                    constraintHelper,
                    constrainable,
                    overrideParameters,
                    OVERRIDES_PARAMETER_DEFAULT_INDEX,
                    declaredAnnotation,
                    constraintType
            );
            
            // 6. 生成的ConstraintDescriptorImpl,放到composingConstraintsSet里面,该Set座位返回值赋值给了this.composingConstraints
            composingConstraintsSet.add( descriptor );
            composingConstraintLocations.put( declaredAnnotationType, ComposingConstraintAnnotationLocation.DIRECT );
            LOG.debugf( "Adding composing constraint: %s.", descriptor );
        }
        else if ( constraintHelper.isMultiValueConstraint( declaredAnnotationType ) ) {
            List<Annotation> multiValueConstraints = constraintHelper.getConstraintsFromMultiValueConstraint( declaredAnnotation );
            int index = 0;
            for ( Annotation constraintAnnotation : multiValueConstraints ) {
                if ( composingConstraintLocations.containsKey( constraintAnnotation.annotationType() )
                        && !ComposingConstraintAnnotationLocation.IN_CONTAINER.equals( composingConstraintLocations.get( constraintAnnotation.annotationType() ) ) ) {
                    throw LOG.getCannotMixDirectAnnotationAndListContainerOnComposedConstraintException( annotationDescriptor.getType(),
                            constraintAnnotation.annotationType() );
                }

                ConstraintDescriptorImpl<?> descriptor = createComposingConstraintDescriptor(
                        constraintHelper,
                        constrainable,
                        overrideParameters,
                        index,
                        constraintAnnotation,
                        constraintType
                );
                composingConstraintsSet.add( descriptor );
                composingConstraintLocations.put( constraintAnnotation.annotationType(), ComposingConstraintAnnotationLocation.IN_CONTAINER );
                LOG.debugf( "Adding composing constraint: %s.", descriptor );
                index++;
            }
        }
    }
    return CollectionHelper.toImmutableSet( composingConstraintsSet );
}
private static final List<String> NON_COMPOSING_CONSTRAINT_ANNOTATIONS = Arrays.asList(
        Documented.class.getName(),
        Retention.class.getName(),
        Target.class.getName(),
        Constraint.class.getName(),
        ReportAsSingleViolation.class.getName(),
        Repeatable.class.getName(),
        Deprecated.class.getName()
);

// 源码位置:org.hibernate.validator.internal.metadata.core.MetaConstraint
MetaConstraint(ConstraintValidatorManager constraintValidatorManager, ConstraintDescriptorImpl<A> constraintDescriptor,
        ConstraintLocation location, List<ContainerClassTypeParameterAndExtractor> valueExtractionPath,
        Type validatedValueType) {
    // 7. 在把找到的校验注解(如@NotNull)和属性字段封装为MetaConstraint时,要确定ConstraintTree的实际实现类,
    //    ConstraintTree的实现有两种,一种是对应单个校验注解的SimpleConstraintTree,另外一种是对应多个校验注解的ComposingConstraintTree
    this.constraintTree = ConstraintTree.of( constraintValidatorManager, constraintDescriptor, validatedValueType );
    this.location = location;
    this.valueExtractionPath = getValueExtractionPath( valueExtractionPath );
    this.hashCode = buildHashCode( constraintDescriptor, location );
    this.isDefinedForOneGroupOnly = constraintDescriptor.getGroups().size() <= 1;
}

// 源码位置:org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree
public static <U extends Annotation> ConstraintTree<U> of(ConstraintValidatorManager constraintValidatorManager,
        ConstraintDescriptorImpl<U> composingDescriptor, Type validatedValueType) {
    // 8. composingDescriptor.getComposingConstraintImpls()获取到的就是上面的composingConstraints
    //    当校验注解上还加了其它校验注解,则composingConstraints不为空,
    //    composingConstraints为空则创建的是SimpleConstraintTree,否则创建的是ComposingConstraintTree
    if ( composingDescriptor.getComposingConstraintImpls().isEmpty() ) {
        return new SimpleConstraintTree<>( constraintValidatorManager, composingDescriptor, validatedValueType );
    }
    else {
        return new ComposingConstraintTree<>( constraintValidatorManager, composingDescriptor, validatedValueType );
    }
}
// 源码位置:org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl
public Set<ConstraintDescriptorImpl<?>> getComposingConstraintImpls() {
    return composingConstraints;
}


// 实际校验的时候会调ConstraintTree的validateConstraints()方法,ComposingConstraintTree重载了父类ConstraintTree的validateConstraints()方法
// 源码位置:org.hibernate.validator.internal.engine.constraintvalidation.ComposingConstraintTree
protected void validateConstraints(ValidationContext<?> validationContext,
        ValueContext<?, ?> valueContext,
        Collection<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts) {
    // 9. 校验
    CompositionResult compositionResult = validateComposingConstraints(validationContext, valueContext, violatedConstraintValidatorContexts);

    Optional<ConstraintValidatorContextImpl> violatedLocalConstraintValidatorContext;

    if ( mainConstraintNeedsEvaluation( validationContext, violatedConstraintValidatorContexts ) ) {

        if ( LOG.isTraceEnabled() ) {
            LOG.tracef(
                    "Validating value %s against constraint defined by %s.",
                    valueContext.getCurrentValidatedValue(),
                    descriptor
            );
        }

        ConstraintValidator<B, ?> validator = getInitializedConstraintValidator( validationContext, valueContext );

        ConstraintValidatorContextImpl constraintValidatorContext = validationContext.createConstraintValidatorContextFor(
                descriptor, valueContext.getPropertyPath()
        );

        violatedLocalConstraintValidatorContext = validateSingleConstraint(
                valueContext,
                constraintValidatorContext,
                validator
        );

        if ( !violatedLocalConstraintValidatorContext.isPresent() ) {
            compositionResult.setAtLeastOneTrue( true );
        }
        else {
            compositionResult.setAllTrue( false );
        }
    }
    else {
        violatedLocalConstraintValidatorContext = Optional.empty();
    }

    if ( !passesCompositionTypeRequirement( violatedConstraintValidatorContexts, compositionResult ) ) {
        prepareFinalConstraintViolations(
                validationContext, valueContext, violatedConstraintValidatorContexts, violatedLocalConstraintValidatorContext
        );
    }
}

// 源码位置:org.hibernate.validator.internal.engine.constraintvalidation.ComposingConstraintTree
private CompositionResult validateComposingConstraints(ValidationContext<?> validationContext,
        ValueContext<?, ?> valueContext,
        Collection<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts) {
    CompositionResult compositionResult = new CompositionResult( true, false );
    
    // 10. 当校验注解上还加了其它校验注解,其它校验注解每个也对应生成了一个处理单个校验注解的SimpleConstraintTree
    //     children就是SimpleConstraintTree列表
    for ( ConstraintTree<?> tree : children ) {
        List<ConstraintValidatorContextImpl> tmpConstraintValidatorContexts = new ArrayList<>( 5 );
        
        // 11. 把注解当一个普通的校验注解完成校验逻辑
        tree.validateConstraints( validationContext, valueContext, tmpConstraintValidatorContexts );
        violatedConstraintValidatorContexts.addAll( tmpConstraintValidatorContexts );

        if ( tmpConstraintValidatorContexts.isEmpty() ) {
            compositionResult.setAtLeastOneTrue( true );
            if ( descriptor.getCompositionType() == OR ) {
                break;
            }
        }
        else {
            compositionResult.setAllTrue( false );
            if ( descriptor.getCompositionType() == AND
                    && ( validationContext.isFailFastModeEnabled() || descriptor.isReportAsSingleViolation() ) ) {
                break;
            }
        }
    }
    return compositionResult;
}

// 回到ComposingConstraintTree的validateConstraints继续处理
// 源码位置:org.hibernate.validator.internal.engine.constraintvalidation.ComposingConstraintTree
protected void validateConstraints(ValidationContext<?> validationContext,
        ValueContext<?, ?> valueContext,
        Collection<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts) {
    // 9. 校验
    CompositionResult compositionResult = validateComposingConstraints(validationContext, valueContext, violatedConstraintValidatorContexts);

    Optional<ConstraintValidatorContextImpl> violatedLocalConstraintValidatorContext;

    // 12. 如果校验注解本身也指定了校验器,那么它本身也需要当一个普通校验注解进行校验
    if ( mainConstraintNeedsEvaluation( validationContext, violatedConstraintValidatorContexts ) ) {

        if ( LOG.isTraceEnabled() ) {
            LOG.tracef(
                    "Validating value %s against constraint defined by %s.",
                    valueContext.getCurrentValidatedValue(),
                    descriptor
            );
        }

        ConstraintValidator<B, ?> validator = getInitializedConstraintValidator( validationContext, valueContext );

        ConstraintValidatorContextImpl constraintValidatorContext = validationContext.createConstraintValidatorContextFor(
                descriptor, valueContext.getPropertyPath()
        );

        violatedLocalConstraintValidatorContext = validateSingleConstraint(
                valueContext,
                constraintValidatorContext,
                validator
        );

        if ( !violatedLocalConstraintValidatorContext.isPresent() ) {
            compositionResult.setAtLeastOneTrue( true );
        }
        else {
            compositionResult.setAllTrue( false );
        }
    }
    else {
        violatedLocalConstraintValidatorContext = Optional.empty();
    }

    // 13. 有多个校验结果的时候,需要确定一下总结果是什么
    if ( !passesCompositionTypeRequirement( violatedConstraintValidatorContexts, compositionResult ) ) {
        prepareFinalConstraintViolations(
                validationContext, valueContext, violatedConstraintValidatorContexts, violatedLocalConstraintValidatorContext
        );
    }
}

// 源码位置:org.hibernate.validator.internal.engine.constraintvalidation.ComposingConstraintTree
private boolean passesCompositionTypeRequirement(Collection<?> constraintViolations, CompositionResult compositionResult) {
    // 14. 有三种关系:一是OR,只要有一个校验通过即可;二是AND,需要所有都校验通过;三是ALL_FALSE,所有校验都不通过。
    //     如果不指定CompositionType,那么默认是AND
    CompositionType compositionType = getDescriptor().getCompositionType();
    boolean passedValidation = false;
    switch ( compositionType ) {
        case OR:
            passedValidation = compositionResult.isAtLeastOneTrue();
            break;
        case AND:
            passedValidation = compositionResult.isAllTrue();
            break;
        case ALL_FALSE:
            passedValidation = !compositionResult.isAtLeastOneTrue();
            break;
    }
    assert ( !passedValidation || !( compositionType == AND ) || constraintViolations.isEmpty() );
    if ( passedValidation ) {
        constraintViolations.clear();
    }
    return passedValidation;
}

在找校验注解的时候,把关联的注解也当普通的校验注解处理(封装到ConstraintDescriptorImpl对象里),处理结果放到一个Set中。在校验的时候,先处理这个Set的内容,处理流程和普通的校验注解一样;处理完之后再处理当前校验注解本身,处理流程和普通的校验注解一样。

由此可以看出,在设计的时候,不管是内置的校验注解,还是自定义的校验注解,原理都是一致的,只有初始化的地方不一样;同样,组合校验注解也会分解成当前注解和关联的校验注解,分解之后也是普通的注解,也是在初始化的时候和校验开始时分解的地方不一样,其它的都是一样的。这个组合的设计思路值得学习。

3 架构一小步

针对需要一起组合使用的校验注解,利用hibernate-validator包提供的组合机制自定义一个新校验注解把它们组合起来,方便使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值