• HibernateValidator扩展之自定义注解


    一、Hibernate-Validator介绍

    ​ Hibernate-Validator框架提供了一系列的注解去校验字段是否符合预期,如@NotNull注解可以校验字段是否为null,如果为null则抛出对应的异常提示信息,通过注解大大减少了我们日常的开发工作量。包括流行的spring-boot-starter-validation,底层也是靠Hibernate-Validator实现的。

    ​ 但是在实际的开发中,现有的注解可能不能满足我们的校验需求,Hibernate-Validator框架就贴心的提供了扩展,通过自定义校验注解来封装我们自己的校验逻辑。

    二、自定义校验注解

    下面以一个例子去说明如何根据自己的业务需求,去自定义校验注解。

    需求背景:

    在日常开发中,我们经常需要在Controller接口对入参的字段做校验,而且有些字段的值只允许在某个枚举定义范围内,如果不在枚举范围内,则抛出异常和错误信息。针对这种情况,我们可以自定义一个注解@StatusCodeCheck去实现。

    根据官方文档的描述,自定义校验注解需要如下三个步骤:

    1. 创建一个约束注解
    2. 实现一个校验器
    3. 定义默认的错误信息

    2.1创建一个约束注解

    创建之前可以先看看@NotNull的源码

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(NotNull.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({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        public @interface List {
            NotNull[] value();
        }
    }
    
    

    @Target: 该元注解用于指定注解的作用范围,包括类,方法,字段等。

    @Retention: 指定该元注解的保留策略,包括source,class和runtime。

    @Repeatable: 这是一个很有意思的元注解,在没有@Repeatable注解的的注解中,在同一个地方使用相同的注解会报错,有了此元注解注解的注解,就可以在同一个地方使用相同的注解。

    @Documented: 表示会生成Java doc

    @Constraint: 用于指定校验器,通过校验器返回的结果(true/false)来判断是否抛出异常信息。

    除了这些元注解,还有一些属性,其作用写在了注释中

    package cn.sp.validation;
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.*;
    
    /**
     * @author Ship
     * @version 1.0.0
     * @description:
     * @date 2021/11/10 16:52
     */
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = StatusCodeCheckValidator.class)
    @Documented
    public @interface StatusCodeCheck {
    		// 指定校验失败时的异常信息,后面会详细说明
        String message() default "{cn.sp.validation.StatusCode.message}";
    		// 分组,如同一个实体类的字段有些情况需要该校验,有些情况不需要,则可通过指定分组实现
        Class<?>[] groups() default {};
    		// 指定错误的级别,一般不会用
        Class<? extends Payload>[] payload() default {};
    		// 自定义的属性
        Class<? extends StatusCode> value();
    }
    
    

    StatusCode是一个泛型的接口,所以需要使用@StatusCodeCheck注解的枚举都需要实现StatusCode接口,主要起一个标记作用。

    public interface StatusCode<T> {
    
        /**
         * 获取code
         *
         * @return
         */
        T getCode();
    }
    

    2.2 实现一个校验器

    实现一个校验器很简单,创建一个类实现ConstraintValidator接口即可,ConstraintValidator的源码如下

    public interface ConstraintValidator<A extends Annotation, T> {
        default void initialize(A constraintAnnotation) {
        }
    
        boolean isValid(T var1, ConstraintValidatorContext var2);
    }
    

    该接口是一个泛型接口,A表示作用的注解,T表示被校验对象的类型,里面有两个方法需要实现。

    initialize(A constraintAnnotation)

    校验器的初始化逻辑,一般用于获取自定义注解的属性,该方法是可选的。

    isValid(T var1, ConstraintValidatorContext var2)

    该方法有两个参数,var1为被校验的对象,var2是一个上下文提供了很多API去操作默认约束信息等,返回值表示校验是否通过,即真正的校验逻辑处理都在该方法中完成。

    知道这些后,就可以开始写自己的校验器StatusCodeCheckValidator了

    /**
     * @author Ship
     * @version 1.0.0
     * @description:
     * @date 2021/11/10 17:06
     */
    public class StatusCodeCheckValidator implements ConstraintValidator<StatusCodeCheck, Object> {
    
        private Class<? extends StatusCode> enumClass;
        /**
         * 枚举缓存
         */
        private static final Map<Class<? extends StatusCode>, List<StatusCode>> CACHE_MAP = new ConcurrentHashMap<>(64);
    
        @Override
        public void initialize(StatusCodeCheck constraintAnnotation) {
            this.enumClass = constraintAnnotation.value();
        }
    
        @Override
        public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
            if (object == null) {
                return false;
            }
            if (!enumClass.isEnum()) {
                throw new RuntimeException("StatusCode 的实现类必须是枚举类型");
            }
            List<StatusCode> statusCodeList = CACHE_MAP.computeIfAbsent(enumClass, (key) -> {
                try {
                    Method method = key.getDeclaredMethod("values");
                    StatusCode[] statusCodes = (StatusCode[]) method.invoke(null);
                    return Stream.of(statusCodes).collect(Collectors.toList());
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return Lists.newArrayList();
            });
            for (StatusCode statusCode : statusCodeList) {
                if (statusCode.getCode().equals(object)) {
                    return true;
                }
            }
            return false;
        }
    }
    
    
    

    这里通过反射来获取所有的枚举实例,后面发现用 EnumSet.of() 方法也是可以的,出于好奇就看了下它的源码,发现底层也是通过反射调用values方法+缓存来实现的,这就叫万变不离其宗吧。

    2.3定义默认的错误信息

    @StatusCodeCheck注解的message属性可以指定默认错误信息,既用可以写死字符串的方式如

    String message() default "error message";
    

    也可以通过${}符号去读取ValidationMessages.properties文件配置的信息

    String message() default "{javax.validation.constraints.NotNull.message}";
    

    ValidationMessages.properties

    cn.sp.validation.StatusCode.message=can not find code in {value}.
    

    这里的{value}会读取@StatusCodeCheck注解的value,功能还是挺强大的。

    三、测试

    首先,编写测试代码,创建用于测试的枚举类ThirdPartyPlatformEnum

    /**
     * @author Ship
     * @version 1.0.0
     * @description
     * @date 2021/11/02 11:25
     */
    public enum ThirdPartyPlatformEnum implements StatusCode<String> {
    
        /**
         * 拼多多
         */
        PDD("PDD", "拼多多"),
        /**
         * 天猫
         */
        TIAN_MALL("TIAN_MALL", "天猫"),
        /**
         * 有赞
         */
        YOU_ZAN("YOU_ZAN", "有赞"),
        /**
         * 美团
         */
        MEI_TUAN("MEI_TUAN", "美团");
    
    
        private String code;
    
        private String desc;
    
        ThirdPartyPlatformEnum(String code, String desc) {
            this.code = code;
            this.desc = desc;
        }
    
        @Override
        public String getCode() {
            return code;
        }
    
        public String getDesc() {
            return desc;
        }
    
    }
    
    

    测试实体类ValidationTest

    public class ValidationTest {
    
        @StatusCodeCheck(message = "无效的第三方平台类型", value = ThirdPartyPlatformEnum.class)
        private String thirdPartyPlatform;
    
    
        public String getThirdPartyPlatform() {
            return thirdPartyPlatform;
        }
    
        public void setThirdPartyPlatform(String thirdPartyPlatform) {
            this.thirdPartyPlatform = thirdPartyPlatform;
        }
    }
    
    

    测试接口

    @RequestMapping("/validation")
    @RestController
    public class ValidationTestTestController {
    
    
        @PostMapping("/test")
        public void test(@RequestBody @Validated ValidationTest validationTest) {
            System.out.println("validation test");
        }
    }
    

    然后启动项目,请求接口http://localhost:9001/validation/test,请求参数如下

    {
    	"thirdPartyPlatform":"ali"
    }
    

    控制台日志显示校验未通过,因为"ali"不在ThirdPartyPlatformEnum的code范围内。

    Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public void cn.sp.validation.test.ValidationTestTestController.test(cn.sp.validation.ValidationTest): [Field error in object 'validationTest' on field 'thirdPartyPlatform': rejected value [ali]; codes [StatusCodeCheck.validationTest.thirdPartyPlatform,StatusCodeCheck.thirdPartyPlatform,StatusCodeCheck.java.lang.String,StatusCodeCheck]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTest.thirdPartyPlatform,thirdPartyPlatform]; arguments []; default message [thirdPartyPlatform],class cn.sp.validation.ThirdPartyPlatformEnum]; default message [无效的第三方平台类型]] ]
    
    

    改为PDD再次请求

    {
    	"thirdPartyPlatform":"PDD"
    }
    

    发现控制台打印出了validation test,说明校验通过。

    四、总结

    Hibernate-Validator框架如何实现可以自定义注解的原理还需要深入研究下,同时在阅读英文官方文档时,感觉自己的英语水平还是不够啊。本文代码已经上传到github,如果有兴趣可以自行下载。

    参考:

    https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#preface

  • 相关阅读:
    IE的if条件判断
    嵌套div的margin-top不生效
    DocumentFragment对象
    javascript严格模式
    某视频网站下载分析
    c# winform 视频转字符动画
    asp.net mvc 5 蛋疼的问题
    asp.net mvc 防止重复提交
    easyHOOK socket send recv
    C# 之泛型详解
  • 原文地址:https://www.cnblogs.com/2YSP/p/15546945.html
Copyright © 2020-2023  润新知