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

小架构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.xdnf.cn/news/16801.html

相关文章:

  • 《Spring Security源码深度剖析:Filter链与权限控制模型》
  • 文件权限值的表示方法
  • 怎样在 Vue 中定义全局方法?
  • 【C语言】深度剖析指针(二):指针与数组,字符,函数的深度关联
  • AWS VPC NAT 网关可观测最佳实践
  • 15、点云<—>深度图转换原理
  • 数据集:机器学习的基石
  • RPA软件推荐:提升企业自动化效率
  • 北京理工大学医工交叉教学实践分享(1)|如何以实践破解数据挖掘教学痛点
  • 在 Elasticsearch 8.19 和 9.1 中引入更强大、更具弹性和可观测性的 ES|QL
  • 《Vuejs设计与实现》第 12 章(组件实现原理 下)
  • 44、鸿蒙HarmonyOS Next开发:视频播放 (Video)组件和进度条 (Progress)组件的使用
  • OSS-服务端签名Web端直传+STS获取临时凭证+POST签名v4版本开发过程中的细节
  • webpack-性能优化
  • STM32CubeMX 生成时钟获取函数的分析
  • 【网络运维】 Linux:使用 Cockpit 管理服务器
  • 矩阵指数函数 e^A
  • 移动管家手机控车系统硬件安装与软件绑定设置
  • LeetCode 4:寻找两个正序数组的中位数
  • DISTILLM:迈向大型语言模型的简化蒸馏方法
  • 基于React+Express的前后端分离的个人相册管理系统
  • OpenBayes 一周速览丨Self Forcing 实现亚秒级延迟实时流视频生成;边缘AI新秀,LFM2-1.2B采用创新性架构超越传统模型
  • 爱车生活汽车GPS定位器:智能监控与安全驾驶的守护者
  • 云原生环境里的显示变革:Docker虚拟浏览器与cpolar穿透技术实战
  • 新零售“实—虚—合”逻辑下的技术赋能与模式革新:基于开源AI大模型、AI智能名片与S2B2C商城小程序源码的研究
  • RAG:检索增强生成的范式演进、技术突破与前沿挑战
  • pytorch入门2:利用pytorch进行概率预测
  • 智慧城市SaaS平台|市政公用管理系统
  • LeetCode Hot 100 搜索旋转排序数组
  • Java项目:基于SSM框架实现的济南旅游网站管理系统【ssm+B/S架构+源码+数据库+毕业论文+远程部署】