美文网首页程序员Java 杂谈
基于注解的参数校验器Hibernate Validator

基于注解的参数校验器Hibernate Validator

作者: HelloLittleRain | 来源:发表于2019-01-15 10:29 被阅读6次

    前言

    你还在为校验入参时写的那一串 if...else... 而苦恼嘛?
    你还在为了编写一个功能全面的参数校验器而夜夜不寐嘛?
    No~ NoNo~ NoNoNo~ No!
    人生苦短,大可不必讲宝贵的编程时间耗费在这些事情上~
    是时候换个活法啦!有现成的工具,拿去用吧!
    卖个萌。。。(✺ω✺)

    public class User {
        @NotBlank(message = "用户名不能为空")
        private String name;
     
        @NotBlank(message = "ERP不能为空")
        @Size(min = 3, message = "ERP长度不能小于3")
        private String erp;
     
        @Min(value = 22, message = "年龄不能低于22岁")
        private int age;
     
        // ... getter and setter
    }
    

    看看上面这种参数校验,多么的 优雅~
    直接通过在属性上写注解就可以达到给参数增加校验规则的目的啦!


    基于注解的参数校验器

    JSR303

    JSR303是一套JavaBean参数校验的 标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。

    image.png

    ps. 没有列举全,感兴趣的可以看JSR303官方文档

    Hibernate Validator

    Hibernate Validator在JSR303的基础上对校验注解进行了扩展

    image.png

    ps. 同样没列举全,大家感兴趣可以看Hibernate Validator官方文档

    Hibernate Validator就决定是你了

    基本上Hibernate validator就包括了所有常用的校验规则,而且它又是在JSR303基础上的扩展,所以直接 推荐使用 Hibernate Validator了。

    Hibernate Validator版本

    image.png

    可以看到Hibernate validator目前有两个稳定的版本,各取所需。
    这篇文章里面的例子用到的版本是 5.4.2.Final

    Maven引用

    <dependency>
       <groupId>org.hibernate</groupId>
       <artifactId>hibernate-validator</artifactId>
       <version>5.4.2.Final</version>
    </dependency>
    
    image.png

    代码实现

    对Object的属性进行校验

    // Bean属性上添加相应的注解,设置校验规则
    public class User {
        @NotBlank(message = "用户名不能为空")
        private String name;
        @NotBlank(message = "ERP不能为空")
        @Size(min = 3, message = "ERP长度不能小于3")
        private String erp;
        @Min(value = 22, message = "年龄不能低于22岁")
        private int age;
     
        // ... getter and setter
    }
    

    如上,给User这个Bean的三个属性增加了相应的规则,message后面规定的是校验不通过时报的错误信息文案:

    • 用户名name字段不能为null,也不能为空字符串(会过滤空格)。
    • 用户erp字段不能为null,也不能为空字符串(会过滤空格),且字符串长度不能小于3。
    • 用户年龄age字段不能小于22。
    // Controller的接口参数前面添加@Valid注解
    import javax.validation.Valid;
     
    @Controller
    @RequestMapping(value = "/validate")
    public class ValidateDemoController {
     
        @RequestMapping(value = "/user", method = RequestMethod.POST)
        @ResponseBody
        public String printValidatedUserInfo(@RequestBody @Valid User user) {
            return user.toString();
        }
     
    }
    

    如上,在Controller的接口参数(刚定义的Bean)之前添加 @Valid 注解。
    这样就可以用啦,校验不通过的时候会抛出异常,具体怎么处理异常就根据自己的项目需要来吧。

    分组校验

    有的时候,在不同场景下我们需要对同一个Bean里面的不同参数进行校验。
    比如说,在新增用户的时候我需要校验姓名、erp和年龄,而在修改用户的时候我只需要校验erp。
    又或者,部门A的员工的年龄不能低于22岁,无上限;而部门B的员工年龄不能高于35岁,无下限。
    难道我们需要根据每一个场景都增加一个Bean嘛?会不会有点太浪费?
    不用的!这里提供了一个 分组 的概念,不同的规则可以划分到不同的组里面,校验的时候选择相应的组,就会只校验该组下面的所有规则。

    // 首先定义两个接口,作为两个分组
    public interface UserValidGroupOne {
    }
     
    public interface UserValidGroupTwo {
    }
    

    定义两个接口UserValidGroupOne和UserValidGroupTwo,作为两个分组。

    // Bean参数校验规则划分为两个组
    public class UserByGroup {
        @NotBlank(groups = {UserValidGroupOne.class, UserValidGroupTwo.class}, message = "用户名不能为空")
        private String name;
     
        @NotBlank(groups = {UserValidGroupOne.class, UserValidGroupTwo.class}, message = "ERP不能为空")
        @Size(min = 3, groups = {UserValidGroupOne.class, UserValidGroupTwo.class}, message = "ERP长度不能小于3")
        private String erp;
     
        @Min(value = 22, groups = {UserValidGroupOne.class}, message = "年龄不能低于22岁")
        @Max(value = 35, groups = {UserValidGroupTwo.class}, message = "年龄不能高于35岁")
        private int age;
     
        // ... getter and setter
    }
    

    属性前写相应注解增加校验规则,注解里面的groups属性表示这条规则属于哪个分组,不加groups则表示在使用Deafault规则时起作用。
    比如上面代码中描述的是:

    • name和erp字段的校验规则同属于两个分组。
    • age字段的校验规则,在分组1中不能小于22,而在分组2中不能大于35。
    //接口参数前面注明要用哪个分组的规则来进行校验
    import org.springframework.validation.annotation.Validated;
     
    @Controller
    @RequestMapping(value = "/validate")
    public class ValidateDemoController {
         
        @RequestMapping(value = "/userByGroup1", method = RequestMethod.POST)
        @ResponseBody
        public String printValidatedUserInfoByGroupOne(@RequestBody @Validated(value = {UserValidGroupOne.class}) UserByGroup userByGroup) {
            return userByGroup.toString();
        }
     
        @RequestMapping(value = "/userByGroup2", method = RequestMethod.POST)
        @ResponseBody
        public String printValidatedUserInfoByGroupTwo(@RequestBody @Validated(value = {UserValidGroupTwo.class}) UserByGroup userByGroup) {
            return userByGroup.toString();
        }
     
    }
    

    如上,在Controller的接口参数(刚定义的Bean)之前添加 @Validated 注解,注意不是 在【对Object的属性进行校验】时讲的 @Valid 注解。
    在@Validated的value属性上注明要使用的规则分组。
    可写多个分组,但是只有第一个分组才生效。
    若使用@Valid则表示使用默认校验规则。

    自定义注解

    虽然Hibernate Validator提供了基本上所有常用的校验规则,可还是有些场景不能满足。
    比如说,现在需要一个校验规则,一个List中不能包含null。Hibernate Validator并没有提供相应注解。
    这时候就需要我们自定义注解,来扩展工具,满足我们自己的需求。

    //自定义参数校验注解,校验List集合中是否有null元素
    import com.jd.ershou.service.impl.ListNotHasNullValidatorImpl;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.*;
     
    import static java.lang.annotation.ElementType.*;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
     
    /**
     * 自定义参数校验注解
     * 校验 List 集合中是否有null 元素
     * Created by weixiaoyu on 2018/5/2.
     */
    @Target({ANNOTATION_TYPE, METHOD, FIELD})
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = ListNotHasNullValidatorImpl.class)
    public @interface ListNotHasNull {
        /**
         * 添加value属性,可以作为校验时的条件,若不需要,可去掉此处定义
         */
        int value() default 0;
     
        String message() default "List集合中不能含有null元素";
     
        Class<?>[] groups() default {};
     
        Class<? extends Payload>[] payload() default {};
     
        /**
         * 定义List,为了让Bean的一个属性上可以添加多套规则
         */
        @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
        @Retention(RUNTIME)
        @Documented
        @interface List {
            ListNotHasNull[] value();
        }
     
    }
    

    首先定义自定义注解@ListNotHasNull,校验List中不能含有null元素,注明其注解实现类为ListNotHasNullValidatorImpl.class。
    接下来编写实现类的逻辑。

    //注解@ListNotHasNull的实现类
    import org.springframework.stereotype.Service;
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    import java.util.List;
     
    /**
     * 自定义注解ListNotHasNull的实现类
     * 用于判断List集合中是否含有null元素
     * Created by weixiaoyu on 2018/5/2.
     */
    @Service
    public class ListNotHasNullValidatorImpl implements ConstraintValidator<ListNotHasNull, List> {
         
        private int value;
     
        @Override
        public void initialize(ListNotHasNull constraintAnnotation) {
            // 传入value值,可以在校验中使用
            this.value = constraintAnnotation.value();
        }
     
        @Override
        public boolean isValid(List list, ConstraintValidatorContext constraintValidatorContext) {
            if (list != null) {
                for (Object object : list) {
                    if (object == null) {
                        //如果List集合中含有Null元素,校验失败
                        return false;
                    }
                }
            }
            return true;
        }
     
    }
    

    自定义注解的实现类实现了 ConstraintValidator 接口。
    重要的是要重写 isValid 方法,其中的逻辑就是需要的校验规则。
    接下来是测试用例。

    //Bean的属性中含有List
    public class Person {
        @NotBlank(message = "姓名不能为空")
        private String name;
     
        @NotBlank(message = "性别不能为空")
        private String sex;
     
        @NotEmpty(message = "家庭成员不能为空")
        @ListNotHasNull(message = "所有家庭成员信息中不能有为null的")
        @Valid // 此处加@Valid注解的原因是注明要递归校验,加上这个注解就会递归校验List中每个元素的属性是否符合规则
        private List<FamilyMember> familyMembers;
         
        // ... getter and setter
    }
    

    家庭成员familyMember是一个List,其中每一个元素都是FamilyMember类。
    在familyMember上增加刚才自定义的@ListNotHasNull注解。
    ps. 此处加@Valid注解的原因是注明要递归校验,加上这个注解就会递归校验List中每个元素FamilyMember的属性是否符合其内部定义规则。

    //Controller的接口参数前面添加@Valid注解
    import javax.validation.Valid;
     
    @Controller
    @RequestMapping(value = "/validate")
    public class ValidateDemoController {
     
        @RequestMapping(value = "/person", method = RequestMethod.POST)
        @ResponseBody
        public String printValidatedPersonInfo(@RequestBody @Valid Person person) {
            return person.toString();
        }
     
    }
    

    同样,在Controller的接口参数(刚定义的Bean)之前添加 @Valid 注解,校验不通过则抛出异常。

    Spring validator方法级别的校验

    JSR和Hibernate Validator的校验只能对Object的属性进行校验,不能对单个的参数进行校验。
    Spring在此基础上进行了扩展,添加了 MethodValidationPostProcessor 拦截器,可以实现对方法参数的校验。
    首先需要将MethodValidationPostProcessor注入到Spring容器中。

    //注入到Spring容器中
    @Configuration
    public class PpValidatorBeanConfigurer {
        @Bean
        public MethodValidationPostProcessor methodValidationPostProcessor() {
            return new MethodValidationPostProcessor();
        }
    }
    

    这里是通过Java Bean的方式,用 @Bean 注解,将MethodValidationPostProcessor注入到Spring容器中的。
    也可以通过 配置xml文件 的方式,根据项目的需要选择使用。

    //在Controller类上添加@Validated注解,在接口方法的每一个参数前面添加相应校验规则注解
    @Controller
    @RequestMapping(value = "/validate")
    @Validated
    public class ValidateMethodController {
     
        @RequestMapping(value = "/param", method = RequestMethod.GET)
        @ResponseBody
        public String printValidatedParam(
                @NotBlank(message = "用户名不能为空") String name,
                @Size(min = 3, message = "ERP长度不能小于3") String erp,
                @Min(value = 22, message = "年龄不能低于22岁") int age) {
     
            String msg = "name=" + name + ", erp=" + erp + ", age=" + age;
            return msg;
        }
     
    }
    

    需要在Controller类的上方添加 @Validated 注解,然后在接口方法的每一个参数前面添加相应的校验规则注解。
    这样方法级别的校验就比较 灵活 了不是~

    异常处理

    之前一直在说,校验不通过的时候会 抛出异常,具体异常如何处理请根据自己的项目需要来。
    但具体都抛出哪些异常,如何处理,我这边在写测试用例的时候捕获到了如下 三类 异常:

    org.springframework.validation.BindException org.springframework.web.bind.MethodArgumentNotValidException
    javax.validation.ConstraintViolationException

    推荐 使用 @ControllerAdvice 搭配 @ExceptionHandler 来捕获Controller层抛出来的异常。
    写了简单的处理逻辑,仅供参考

    //异常处理
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.validation.BindException;
    import org.springframework.validation.ObjectError;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import javax.validation.ConstraintViolation;
    import javax.validation.ConstraintViolationException;
    import java.util.List;
    import java.util.Set;
     
    /**
     * Created by weixiaoyu on 2018/5/2.
     */
    @ControllerAdvice
    public class PpValidatorExceptionHandler {
        private static final Logger LOGGER = LoggerFactory.getLogger(PpValidatorExceptionHandler.class);
     
        @ExceptionHandler(Throwable.class)
        @ResponseBody
        public String handleThrowableException(Throwable ex) {
            LOGGER.error("PpValidatorExceptionHandler.handleThrowableException", ex);
            String msg = "Throwable error: " + ex.toString();
            return msg;
        }
     
        @ExceptionHandler(BindException.class)
        @ResponseBody
        public String handleBindException(BindException e1) {
            LOGGER.error("PpValidatorExceptionHandler.handleBindException", e1);
            List<ObjectError> errors = e1.getAllErrors();
            StringBuilder stringBuilder = new StringBuilder();
            for (ObjectError error : errors) {
                if (stringBuilder.length() != 0) {
                    stringBuilder.append(",");
                }
                stringBuilder.append(error.getDefaultMessage());
            }
            String msg ="BindException error: " + stringBuilder.toString();
            return msg;
        }
     
        @ExceptionHandler(MethodArgumentNotValidException.class)
        @ResponseBody
        public String handleMethodArgumentNotValidException(MethodArgumentNotValidException e2) {
            List<ObjectError> errors = e2.getBindingResult().getAllErrors();
            StringBuilder stringBuilder = new StringBuilder();
            for (ObjectError error : errors) {
                if (stringBuilder.length() != 0) {
                    stringBuilder.append(",");
                }
                stringBuilder.append(error.getDefaultMessage());
            }
            String msg ="MethodArgumentNotValidException error: " + stringBuilder.toString();
            return msg;
        }
     
        @ExceptionHandler(ConstraintViolationException.class)
        @ResponseBody
        public String handleConstraintViolationException(ConstraintViolationException e3) {
            Set<ConstraintViolation<?>> violations = e3.getConstraintViolations();
            StringBuilder stringBuilder = new StringBuilder();
            for (ConstraintViolation<?> violation : violations) {
                if (stringBuilder.length() != 0) {
                    stringBuilder.append(",");
                }
                stringBuilder.append(violation.getMessage());
            }
            String msg ="ConstraintViolationException error: " + stringBuilder.toString();
            return msg;
        }
     
    }
    

    结语

    安利了这么多,觉得好使不?
    那必须好使啊~~~
    部门在我的安利下所有新项目都在用Hibernate Validator参数校验器,经过了多种线上环境验证。大家就放心去用吧~
    最后,有问题的话咱们共同探讨~ 共同学习~ 共同进步哈~

    相关文章

      网友评论

        本文标题:基于注解的参数校验器Hibernate Validator

        本文链接:https://www.haomeiwen.com/subject/nhnjdqtx.html