小架构step系列30:多个校验注解

1 概述

有些时候,需要在一个属性上加多个相同的校验注解,目的是提供有限种场景的条件,比如邮箱希望有多个后缀的,URL希望有多个不同域名的,手机号希望有多个号段的等。有点类似枚举,把多个场景枚举出来做限制,如果是纯字符串字段,虽然可以用正则表达式来实现,但如果每个场景的情况本身也要用正则表达式表示,那就会使得正则表达式非常复杂。

hibernate-validator包提供的校验注解机制也考虑了这种情况,允许在一个属性字段上标注多个相同的校验注解,这样每个注解的表达性就会比较清晰。而按Java语法规定同一个注解在同一个目标上默认只能使用一次,这多个相同注解的使用是如何支持的呢?

2 原理

2.1 Java注解

Java注解本身就提供了解除"同一个注解在同一个目标上默认只能使用一次"这个限制的方法,那就是在注解里加上List的定义。

如果是不带List的定义,会报编译错误:

java 复制代码
// 如果
@Pattern(regexp = "^[A-Za-z]+$")
@Pattern(regexp = "^\\d+$")  // 编译报错:Duplicate annotation
private String password;

如果带List定义,则可以加多个:

java 复制代码
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface Pattern {
    String regexp();
    Flag[] flags() default { };
    String message() default "{javax.validation.constraints.Pattern.message}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
    // 省略部分代码
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List { // 代List定义提供数组
        Pattern[] value();
    }
}

// 使用
@Pattern(regexp = "^[A-Za-z]+$")
@Pattern(regexp = "^\\d+$")  // 不会报错
private String password; 

注意:如果使用反射对password这个Field进行获取注解,Field.getDeclaredAnnotations()得到的是PatternList、而不是两个注解的数组,PatternList里面有两个@Pattern注解。

2.2 多个相同注解的实现

当属性字段标注了多个相同注解时,hibernate-validator包也对这种情况做了特殊处理,多作为一个else分支进行处理。

java 复制代码
// 源码位置:org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider
private List<ConstraintDescriptorImpl<?>> findConstraints(Constrainable constrainable, JavaBeanAnnotatedElement annotatedElement, ConstraintLocationKind kind) {
    List<ConstraintDescriptorImpl<?>> metaData = newArrayList();
    // 1. 通过getDeclaredAnnotations()获取属性上标注的注解时,如果是有多个相同的注解getDeclaredAnnotations()得到的是一个@List对象,List有多个相同的注解
    for ( Annotation annotation : annotatedElement.getDeclaredAnnotations() ) {
        metaData.addAll( findConstraintAnnotations( constrainable, annotation, kind ) );
    }

    return metaData;
}

// 源码位置:org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider
protected <A extends Annotation> List<ConstraintDescriptorImpl<?>> findConstraintAnnotations(
        Constrainable constrainable,
        A annotation,
        ConstraintLocationKind type) {
    // 2. @List并不是内置的注解,if的条件为false
    if ( constraintCreationContext.getConstraintHelper().isJdkAnnotation( annotation.annotationType() ) ) {
        return Collections.emptyList();
    }

    List<Annotation> constraints = newArrayList();
    Class<? extends Annotation> annotationType = annotation.annotationType();
    // 3. @List上没有标注@Contraint注解,if的条件为false
    if ( constraintCreationContext.getConstraintHelper().isConstraintAnnotation( annotationType ) ) {
        constraints.add( annotation );
    }
    // 4. 判断是否是多值的场景
    else if ( constraintCreationContext.getConstraintHelper().isMultiValueConstraint( annotationType ) ) {
        constraints.addAll( constraintCreationContext.getConstraintHelper().getConstraintsFromMultiValueConstraint( annotation ) );
    }

    return constraints.stream()
            .map( c -> buildConstraintDescriptor( constrainable, c, type ) )
            .collect( Collectors.toList() );
}

// 源码位置:org.hibernate.validator.internal.metadata.core.ConstraintHelper
public boolean isMultiValueConstraint(Class<? extends Annotation> annotationType) {
    if ( isJdkAnnotation( annotationType ) ) {
        return false;
    }

    return multiValueConstraints.computeIfAbsent( annotationType, a -> {
        boolean isMultiValueConstraint = false;
        
        // 5. 取出注解里的value方法,即要求@List里必须有个value()方法才能支持多个相同注解
        final Method method = run( GetMethod.action( a, "value" ) );
        if ( method != null ) {
            Class<?> returnType = method.getReturnType();
            
            // 6. value()方法的返回值必须为数组(Array),且数组里元素的类型要为注解类型(这些注解需要为内置的校验注解或者带@Contraint的注解)
            if ( returnType.isArray() && returnType.getComponentType().isAnnotation() ) {
                @SuppressWarnings("unchecked")
                Class<? extends Annotation> componentType = (Class<? extends Annotation>) returnType.getComponentType();
                if ( isConstraintAnnotation( componentType ) ) {
                    isMultiValueConstraint = Boolean.TRUE;
                }
                else {
                    isMultiValueConstraint = Boolean.FALSE;
                }
            }
        }
        return isMultiValueConstraint;
    } );
}

// 回到AnnotationMetaDataProvider的findConstraintAnnotations()继续处理
// 源码位置:org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider
protected <A extends Annotation> List<ConstraintDescriptorImpl<?>> findConstraintAnnotations(
        Constrainable constrainable,
        A annotation,
        ConstraintLocationKind type) {
    // 2. @List并不是内置的注解,if的条件为false
    if ( constraintCreationContext.getConstraintHelper().isJdkAnnotation( annotation.annotationType() ) ) {
        return Collections.emptyList();
    }

    List<Annotation> constraints = newArrayList();
    Class<? extends Annotation> annotationType = annotation.annotationType();
    // 3. @List上没有标注@Contraint注解,if的条件为false
    if ( constraintCreationContext.getConstraintHelper().isConstraintAnnotation( annotationType ) ) {
        constraints.add( annotation );
    }
    // 4. 判断是否是多值的场景
    else if ( constraintCreationContext.getConstraintHelper().isMultiValueConstraint( annotationType ) ) {
        // 7. 处理多个相同的注解,注意返回的List是把元素加到constraints里的,也就是多个注解体现到结果里也是跟普通的多个不同校验注解的方式是一样的
        constraints.addAll( constraintCreationContext.getConstraintHelper().getConstraintsFromMultiValueConstraint( annotation ) );
    }

    return constraints.stream()
            .map( c -> buildConstraintDescriptor( constrainable, c, type ) )
            .collect( Collectors.toList() );
}

// 源码位置:org.hibernate.validator.internal.metadata.core.ConstraintHelper
public <A extends Annotation> List<Annotation> getConstraintsFromMultiValueConstraint(A multiValueConstraint) {
    // 8. 把@List注解分解成单个的注解,放到List返回
    Annotation[] annotations = run(
            GetAnnotationAttribute.action(
                    multiValueConstraint,
                    "value",
                    Annotation[].class
            )
    );
    return Arrays.asList( annotations );
}

// 源码位置: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<>();

    // 9. 在多个相同注解的情况下,这里annotationDescriptor对应的是一个由@List分开的注解
    for ( Annotation declaredAnnotation : annotationDescriptor.getType().getDeclaredAnnotations() ) {
        Class<? extends Annotation> declaredAnnotationType = declaredAnnotation.annotationType();
        if ( NON_COMPOSING_CONSTRAINT_ANNOTATIONS.contains( declaredAnnotationType.getName() ) ) {
            continue;
        }

        if ( constraintHelper.isConstraintAnnotation( declaredAnnotationType ) ) {
            if ( composingConstraintLocations.containsKey( declaredAnnotationType )
                    && !ComposingConstraintAnnotationLocation.DIRECT.equals( composingConstraintLocations.get( declaredAnnotationType ) ) ) {
                throw LOG.getCannotMixDirectAnnotationAndListContainerOnComposedConstraintException( annotationDescriptor.getType(), declaredAnnotationType );
            }

            ConstraintDescriptorImpl<?> descriptor = createComposingConstraintDescriptor(
                    constraintHelper,
                    constrainable,
                    overrideParameters,
                    OVERRIDES_PARAMETER_DEFAULT_INDEX,
                    declaredAnnotation,
                    constraintType
            );
            composingConstraintsSet.add( descriptor );
            composingConstraintLocations.put( declaredAnnotationType, ComposingConstraintAnnotationLocation.DIRECT );
            LOG.debugf( "Adding composing constraint: %s.", descriptor );
        }
        // 10. 分解之后的注解,如果它上面标注的注解一般不是多值(是多值的也比较难使用),所以composingConstraintsSet最终没有值
        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 );
}


// 源码位置:org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree
public static <U extends Annotation> ConstraintTree<U> of(ConstraintValidatorManager constraintValidatorManager,
        ConstraintDescriptorImpl<U> composingDescriptor, Type validatedValueType) {
    // 11. 由于composingConstraintsSet为空的,所以创建的是SimpleConstraintTree,按普通的一个校验注解处理。
    if ( composingDescriptor.getComposingConstraintImpls().isEmpty() ) {
        return new SimpleConstraintTree<>( constraintValidatorManager, composingDescriptor, validatedValueType );
    }
    else {
        return new ComposingConstraintTree<>( constraintValidatorManager, composingDescriptor, validatedValueType );
    }
}

从上面看,当在一个属性字段标注多个相同的校验注解时,会把这些校验注解当普通的校验注解看待。由于Java注解机制的限制,取出字段注解时一个@List的对象,需要对这种情况进行分解出来,分解之后就和普通的校验注解一样了。

2.3 例子

看几个内置的校验注解,它们都是支持标注多个相同的:

java 复制代码
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
    String message() default "{javax.validation.constraints.NotNull.message}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {

        NotNull[] value();
    }
}


@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface NotEmpty {
    String message() default "{javax.validation.constraints.NotEmpty.message}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        NotEmpty[] value();
    }
}


@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface Max {
    String message() default "{javax.validation.constraints.Max.message}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
    long value();

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        Max[] value();
    }
}

@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface Email {
    String message() default "{javax.validation.constraints.Email.message}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
    String regexp() default ".*";
    Pattern.Flag[] flags() default { };

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        Email[] value();
    }
}

上面这些校验注解,里面都有@interface List的定义,也就是都支持标注成多个相同的。

3 架构一小步

当有多种场景校验时,用多个相同的校验注解标注,使得校验规则更加明确,避免用过于复杂的嵌套正则表达式,难以维护。