当前位置: 首页 > news >正文

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

1 概述

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

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

2 原理

2.1 Java注解

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

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

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

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

@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()得到的是Pattern$List、而不是两个注解的数组,Pattern$List里面有两个@Pattern注解。

2.2 多个相同注解的实现

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

// 源码位置: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的条件为falseif ( constraintCreationContext.getConstraintHelper().isJdkAnnotation( annotation.annotationType() ) ) {return Collections.emptyList();}List<Annotation> constraints = newArrayList();Class<? extends Annotation> annotationType = annotation.annotationType();// 3. @List上没有标注@Contraint注解,if的条件为falseif ( 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的条件为falseif ( constraintCreationContext.getConstraintHelper().isJdkAnnotation( annotation.annotationType() ) ) {return Collections.emptyList();}List<Annotation> constraints = newArrayList();Class<? extends Annotation> annotationType = annotation.annotationType();// 3. @List上没有标注@Contraint注解,if的条件为falseif ( 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 例子

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

@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)@Documentedpublic @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)@Documentedpublic @interface List {Email[] value();}
}

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

3 架构一小步

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

http://www.lryc.cn/news/604336.html

相关文章:

  • Mysql事务基础
  • LeetCode Hot 100:15. 三数之和
  • 大模型赋能:台风“竹节草”精细化路径预测实践
  • 硬件电路设计(基本元器件)
  • 深入理解C++编译器优化:从O0到O3及构建模式详解
  • 【从零实践Onvif】01、Onvif详细介绍(从Onvif客户端开发的角度认识Onvif、Web Servies、WSDL、SOAP)
  • 压测合格标准
  • 智能体产品化的关键突破:企业智能化转型的“最后一公里”如何迈过?
  • 【刷题】东方博宜oj 1307 - 数的计数
  • 亮数据MCP智能服务助力数据服务
  • AD域设计与管理-批量创建域用户
  • Java 14 新特性解析与代码示例
  • 力扣刷题日常(7-8)
  • day 40 打卡-装饰器
  • 让科技之光,温暖银龄岁月——智绅科技“智慧养老进社区”星城国际站温情纪实
  • 品牌侵权查询怎么查?跨境电商怎样查品牌是否侵权?
  • 笔记本电脑开机慢系统启动慢怎么办?【图文详解】win7/10/11开机慢
  • Apache Ignite 中如何配置和启用各类监控指标
  • T113-i Linux系统完整构建指南:从SDK开箱到内核镜像量产烧录全流程
  • 计算机网络1-3:三种交换方式
  • 【38】WinForm入门到精通 ——WinForm平台为AnyCPU 无法切换为x64,也无法添加 x64及其他平台
  • 15.10 单机8卡到千卡集群!DeepSpeed实战调参手册:A100训练效率翻倍,百万成本优化实录
  • 文心大模型4.5开源:国产AI的破茧时刻与技术普惠实践
  • 工作笔记-----FreeRTOS中的lwIP网络任务为什么会让出CPU
  • 24串高边BMS全套设计方案!
  • 51单片机入门:数码管原理介绍及C代码实现
  • YOLO融合MogaNet中的ChannelAggregationFFN模块
  • 基于 Python 开发的信阳市天气数据可视化系统源代码+数据库+课程报告
  • 基于 Hadoop 生态圈的数据仓库实践 —— OLAP 与数据可视化(三)
  • C++ Qt网络编程实战:跨平台TCP调试工具开发