• springboot hibernate-validator 校验


    前言

    这段时间在调整老系统相关的一些业务代码;发现一些模块,在无形中就被弄的有点乱了,由于每个开发人员技术水平不同、编码习惯差异;从而导致在请求、响应、异常这一块儿,出现了一些比较别扭的代码;但是归根究底,主要问题还是出在规范上面;不管是大到项目还是小到功能模块,对于请求、响应、异常这一块儿,应该是一块儿公共的模板化的代码,一旦定义清楚之后,是不需要做任何改动,而且业务开发过程中,也几乎是不需要动到他丝毫;所以,一个好的规范下,是不应该在这部分代码上出现混乱或者别扭的情况的;忍不住又得来整理一下这一块儿的东西;

    作为一个后台的工程师,接受请求、处理业务、解决异常、响应数据,几乎覆盖了日常开发的全部;但是这个中间,除了业务代码是不可避免且无可替代之外;其他的三项操作,不管是啥功能,也都是大同小异的,那我们要如何来把这一块儿的东西抽离出来,让我们只需要去管业务,不用去管那些杂七杂八的的破事儿,从而腾出更多的时间学(mo)习(yu)呢?当然就是得去定义一个好的规则,运用优秀的轮子;让这部分重复的、可复用的工作给模板化、标准化

    这样,开发一遍,后面就不需要再去弄这些通用的东西了。

    思考一下,关于请求、响应、异常,我们到底要注意些啥问题呢?

    问题点

    请求

    1. 如何优雅的接受数据?
    2. 如何优雅的校验数据?

    响应

    1. 响应数据格式如何统一?
    2. 错误码如何规范
    3.如何将业务功能和响应给剥离开来?

    异常

    1. 异常如何捕获?
    2. 业务异常、校验异常如何合理的转换为友好的标准响应?
    3. 如何规避未捕获到的异常并优雅返回标准响应?

    这一些列的问题,就衍生出,我们该如何去规范的问题?任何利用已有的优秀框架去解决这些问题?
    接下来,就通过一个完整的示例,基于这三个大点下面的小问题,去把这个规范给讲清楚;
    讲每个大的问题点之前,我会给大家一个或几个疑问;然后可以带着这些疑问,边思考边看。

    下面的介绍,我们就以一个简单的用户信息(UserInfo)的CURD展开

    hibernate-validator优雅的处理请求
    疑问

    1. 我们要如何去校验请求的数据?
    2. 相同的对象去接受不同请求数据,如何能区别校验?

    主要的目的是为了减少一些非必要的DTO对象

    @RestController
    @RequestMapping("user")
    public class UserController{
    
        @PostMapping("add")
        public String add(@RequestBody UserAddRequestDto addInfo){
            // ......
            return "ok";
        }
    
        @PutMapping("update")
        public void update(@RequestBody UserUpdateRequestDto updateInfo)
        {
            // ......
            return "ok";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这样?嗯!这样确实可以接受到请求参数,但是我们回归到上面的疑问;

    参数如何校验?难道这样?

    if(null==addInfo.getUserName()){
     throw new Exceprion();
    }
    if(null==addInfo.getPassWord()){
     throw new Exceprion();
    }
    // 。。。。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    固然可以,这样真的好吗?很明显不好。。。。劳力伤神的事儿,咱可不干。
    addInfo和updateInfo大部分属性都是一样的,添加的字段,大部分都是可以进行修改的,但是也有部分是不可以修改的;比如密码,一般都是单独写接口进行修改;

    既然大部分都一样;有必要定义这么多个请求的DTO对象吗?有必要!!没办法啊!大部分一样,他也有不一样的地方!

    那有没有能优雅的去解决参数校验问题,又可以将请求对象合多为一呢?

    hibernate-validator就是一个可以完美的解决这些问题的优秀框架;
    接下来,我们就详细的来看一下,如何使用这个工具。

    hibernate-validator

    优点

    1. 解耦,数据的校验与业务逻辑进行分离,降低耦合度
      到controller的对象就已经是校验过的对象了,接受到之后就只需要安心处理业务就好,不用再进行数据校验相关逻辑

    2. 规范的校验方式,减少参数校验所带来的繁琐体力活
      以注解的方式配置校验规则;大大减少校验的工作量,而且复用性强

    3. 简洁代码,提高代码的可读性
      以注解方式即可完成属性校验,去掉了各种冗长的校验代码;且所有的校验规则都定义在对象内部;使得代码结构更加清晰,可读性非常强。

    注解说明
    下面包含了validator的所有内置的注解

    注解作用
    @AssertFalse被注释的元素必须为 false
    @AssertTrue被注释的元素必须为 true
    @DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
    @Email被注释的元素必须是电子邮箱地址
    @Future被注释的元素必须是一个将来的日期
    @Length(min=,max=)被注释的字符串的大小必须在指定的范围内
    @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @Negative该值必须小于0
    @NegativeOrZero该值必须小于等于0
    @Null被注释的元素必须为 null
    @NotNull被注释的元素必须不为 null
    @NotBlank(message =)验证字符串非null,且长度必须大于0
    @NotEmpty被注释的字符串的必须非空
    @Past被注释的元素必须是一个过去的日期
    @Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
    @Positive该值必须大于0
    @PositiveOrZero该值必须大于等于0
    @Range(min=,max=,message=)被注释的元素必须在合适的范围内
    @Size(max=, min=)数组大小必须在[min,max]这个区间
    @URL(protocol=,host,port)检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件
    @Valid该注解主要用于字段为一个包含其他对象的集合或map或数组的字段,或该字段直接为一个其他对象的引用,这样在检查当前对象的同时也会检查该字段所引用的对象

    如何简单使用?

    第一步;引入依赖

    <dependency>
       <groupId>org.hibernate.validatorgroupId>
       <artifactId>hibernate-validatorartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    第二步;属性添加对应的注解

    按照上面表格的说明,根据自己定义属性的特点,添加相应的注解。

    如下示例,用户名,密码,年龄不能为空;那我们就用@NotBlank @NotNull去修饰,如果违背规则,就会按message的文本提示

    年龄不能小于0岁、大于120岁;那么就用@min @max进行约束

    message描述了违背校验规则之后的描述。

    @Data
    public class UserRequestDto {
        /**
         * 用户名
         */
        @NotBlank(message = "姓名不能为空")
        public String userName;
    
        /**
         * 密码
         */
        @NotBlank(message = "密码不能为空")
        public String passWord;
    
        /**
         * 年龄
       */
        @NotNull(message = "年龄不能为空")
        @Min(value = 0,message = "年龄不能小于0岁")
        @Max(value = 120,message = "年龄不能大于120岁")
        private Integer age;
        
        /**
         * 手机号码;使用正则进行匹配
         */
        @NotBlank(message = "手机号码不能为空")
        @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "号码格式不正确!")
        private String phoneNum;
     
        // 。。。。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    第三步,Controller的参数加上@Validated

    @PostMapping("add")
    public String add(@Validated @RequestBody UserRequestDto userRequestDto) {
     // 。。。。
    }
    
    • 1
    • 2
    • 3
    • 4

    第四步,测试
    在这里插入图片描述
    第五步,异常处理

    上面的操作可以看出,当请求参数如果不符合条件的话,就已经抛出异常并响应客户端了;

    但是异常并没有针对性的处理,也没有进行友好的提示;前端收到错误之后,没办法根据错误信息准确的判断出是什么问题;因此对于的异常还需要进行特殊处理;
    全局异常:

    @RestControllerAdvice
    @Slf4j
    public class GlobalExceptionHandler {
        /**
         * 处理自定义异常
         *
         */
        @ExceptionHandler(value = BusinessException.class)
        public AjaxResult bizExceptionHandler(BusinessException e) {
            log.error(e.getMessage(), e);
            return AjaxResult.defineError(e);
        }
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public AjaxResult exceptionHandler( MethodArgumentNotValidException e) {
            log.error(e.getMessage(), e);
            return AjaxResult.otherError(e.getFieldError().getDefaultMessage(),500);
    
        }
        /**
         *处理其他异常
         *
         */
        @ExceptionHandler(value = Exception.class)
        public AjaxResult exceptionHandler( Exception e) {
            log.error(e.getMessage(), e);
            return AjaxResult.otherError(ErrorEnum.INTERNAL_SERVER_ERROR);
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    异常枚举类

    public enum ErrorEnum {
        // 数据操作错误定义
        SUCCESS(200, "成功"),
        NO_PERMISSION(403,"你没得权限"),
        NO_AUTH(401,"未登录"),
        NOT_FOUND(404, "未找到该资源!"),
        INTERNAL_SERVER_ERROR(500, "服务器异常请联系管理员"),
        ;
    
        /** 错误码 */
        private Integer errorCode;
    
        /** 错误信息 */
        private String errorMsg;
    
        ErrorEnum(Integer errorCode, String errorMsg) {
            this.errorCode = errorCode;
            this.errorMsg = errorMsg;
        }
    
        public Integer getErrorCode() {
            return errorCode;
        }
    
        public String getErrorMsg() {
            return errorMsg;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    自定义异常

    public class BusinessException extends RuntimeException{
        private static final long serialVersionUID = 1L;
        /**
         * 错误状态码
         */
        protected Integer errorCode;
        /**
         * 错误提示
         */
        protected String errorMsg;
    
        public BusinessException(){
        }
    
        public BusinessException(Integer errorCode, String errorMsg) {
            this.errorCode = errorCode;
            this.errorMsg = errorMsg;
        }
    
        public Integer getErrorCode() {
            return errorCode;
        }
    
        public void setErrorCode(Integer errorCode) {
            this.errorCode = errorCode;
        }
    
        public String getErrorMsg() {
            return errorMsg;
        }
    
        public void setErrorMsg(String errorMsg) {
            this.errorMsg = errorMsg;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    通用返回类

    public class AjaxResult {
        //是否成功
        private Boolean success;
        //状态码
        private Integer code;
        //提示信息
        private String msg;
        //数据
        private Object data;
        public AjaxResult() {
    
        }
        //自定义返回结果的构造方法
        public AjaxResult(Boolean success,Integer code, String msg,Object data) {
            this.success = success;
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
        //自定义异常返回的结果
        public static AjaxResult defineError(BusinessException de){
            AjaxResult result = new AjaxResult();
            result.setSuccess(false);
            result.setCode(de.getErrorCode());
            result.setMsg(de.getErrorMsg());
            result.setData(null);
            return result;
        }
        //其他异常处理方法返回的结果
        public static AjaxResult otherError(ErrorEnum errorEnum){
            AjaxResult result = new AjaxResult();
            result.setMsg(errorEnum.getErrorMsg());
            result.setCode(errorEnum.getErrorCode());
            result.setSuccess(false);
            result.setData(null);
            return result;
        }
        public static AjaxResult otherError(String msg, Integer code){
            AjaxResult result = new AjaxResult();
            result.setMsg(msg);
            result.setCode(code);
            result.setSuccess(false);
            result.setData(null);
            return result;
        }
        public Boolean getSuccess() {
            return success;
        }
        public void setSuccess(Boolean success) {
            this.success = success;
        }
        public Integer getCode() {
            return code;
        }
        public void setCode(Integer code) {
            this.code = code;
        }
        public String getMsg() {
            return msg;
        }
        public void setMsg(String msg) {
            this.msg = msg;
        }
        public Object getData() {
            return data;
        }
        public void setData(Object data) {
            this.data = data;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    上面我们已经将请求的参数以一种比较优雅的方式给验证了;但是并没有将请求对象合并,依然还是使用的addInfo和updateInfo对参数进行接受的;下面就一起来看一下,如何将这边同质化的对象进行优雅的合并。

    请求对象的合并

    group分组校验说明

    上面的业务场景中添加和修改用户信息,添加的时候,密码字段是必传的;修改的时候,密码是不需要传的;那我们能否把添加和修改所有用到的属性定义到一个对象中,然后根据不同的请求,去校验参数,比如,调用添加接口,密码是必传的;调用修改接口,就不需要传密码;为了能做到接口区分校验,就可以用到group这个关键参数;

    group的理解

    可以简单的理解就是把各个属性进行分组;校验的时候,会根据当前Controller指定的组进行校验,这些组里面包含了那些属性,就只校验那些属性,其他不在范围内的,就直接给忽略调掉。

    group定义
    group的定义是以接口为基本单元;也就是一个接口代表一个组;

    使用示例
    定义基础的、修改、添加的接口(group)

    // 基础的校验接口,标识着所有操作都需要校验的字段
    public interface UserRequestDtoSimpleValidate {};
    
    // 修改的校验;继承自UserRequestDtoSimpleValidate 
    // 也就是说指定为这个组的时候在满足当前校验规则的同时还得校验simple接口的属性
    public interface UserRequestDtoUpdateValidate extends UserRequestDtoSimpleValidate {}
    
    // 原理同上
    public interface UserRequestDtoAddValidate extends UserRequestDtoUpdateValidate {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    属性校验添加上分组配置

    @Data
    public class UserRequestDtoGroups {
        /**
         * 用户名
         */
        @NotBlank(message = "姓名不能为空",groups = UserRequestDtoSimpleValidate.class)
        public String userName;
    
        /**
         * 密码
         */
        @NotBlank(message = "密码不能为空",groups = UserRequestDtoAddValidate.class)
        public String passWord;
    
        /**
         * 年龄
         */
        @NotNull(message = "年龄不能为空",groups = UserRequestDtoSimpleValidate.class)
        @Min(value = 0,message = "年龄不能小于0岁",groups = UserRequestDtoSimpleValidate.class)
        @Max(value = 120,message = "年龄不能大于120岁",groups = UserRequestDtoSimpleValidate.class)
        private Integer age;
    
        /**
         * 手机号码;使用正则进行匹配
         */
        @NotBlank(message = "手机号码不能为空",groups = UserRequestDtoAddValidate.class)
        @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "号码格式不正确!",groups = UserRequestDtoAddValidate.class)
        private String phoneNum;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    Controller指定分组进行校验

    如下@Validated中,指定分组接口类;可以一个,也可以多个,这样就会按照指定的分组进行参数校验

     @PostMapping("add")
        public String add(@Validated(UserRequestDtoAddValidate.class) @RequestBody UserRequestDtoGroups dtoGroups) {
            // 后续业务
            return "ok";
        }
    
        @PutMapping("update")
        public void update(@Validated(UserRequestDtoUpdateValidate.class) @RequestBody UserRequestDtoGroups dtoGroups)  {
            // 后续业务
           
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    测试:
    在这里插入图片描述
    结果没有报错,因为熟悉走的是新增的校验group,而controller写的是修改的group,所以不会报错。
    ;修改属性的校验分组修改为修改的校验。

    在这里插入图片描述
    因为分组一致则进行了校验。

    自定义校验

    上面的所有校验,全部使用的是内置的注解,实际的使用过程中,不可避免的有一些特殊的业务场景,参数规则太过于个性化,内置的注解无法满足我们的需求时,要怎么办?比如说,文本必须全部是大写或者小写(该需求其实也可以通过正则表达式的方式进行);为了剧情需要,那我们可以基于这个需求,来自定义一个校验器;

    定义大小写的枚举

    用于注解使用的时候,来指定是校验规则是大写的还是小写的

    public enum CaseMode {
        //大写
        UPPER,
        //小写
        LOWER;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    定义校验大小写的注解

    @Documented
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    //指定校验器
    @Constraint(validatedBy = CaseCheckValidator.class)
    public @interface CaseCheck {
        String message() default "";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        CaseMode value() default CaseMode.UPPER;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这里面,我们将CaseMode枚举作为了注解中的value参数,可以根据需要动态设置大小写的参数,这里默认就是大写的;

    @Constraint(validatedBy = CaseCheckValidator.class) 指明的使用CaseCheckValidator这个校验器进行数据校验;具体的校验规则,判断逻辑,就是写在这个校验器里面。

    自定义校验器

    public class CaseCheckValidator implements ConstraintValidator<CaseCheck, String> {
        //大小写的枚举
        private CaseMode caseMode;
    
        @Override
        public void initialize(CaseCheck caseCheck) {
            this.caseMode = caseCheck.value();
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            //如果文本是空,则不进行校验,因为有其他的注解是可以校验空或者空字符串的
            if (null == value) {
                return true;
            }
    
            //文本只能是字母的正则
            String pattern = "^[a-zA-Z]*$";
            //校验传进来的是否是只包含了字母的文本
            boolean isMatch = Pattern.matches(pattern, value);
            //如果存在其他字符则返回校验失败
            if (!isMatch) {
                return false;
            }
    
            //如果没有指定方式,则直接返回false
            if (null == caseMode) {
                return false;
            }
    
            //判断是否符合大小写条件
            if (caseMode == CaseMode.UPPER) {
                return value.equals(value.toUpperCase());
            } else {
                return value.equals(value.toLowerCase());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    泛型说明
    该校验器继承自ConstraintValidator这个接口;并传递了两个泛型参数;第一个是指明你自定义的注解;第二个是该注解作用的属性类型;

    校验初始化
    如果属性添加了该校验器对应的注解,就会初始化(initialize)该校验器时,将你加在属性上面的注解传递进来;

    验证
    初始化完会调用isValid方法·,并传递属性值;拿到属性值之后,就可以根据初始化传入的注解指定的规则,对属性值进行校验。验证通过返回true,并进行下一个属性的校验;验证失败返回false,并抛出异常;
    测试

    /**
     * 用户名
     */
    @NotBlank(message = "姓名不能为空",groups = UserRequestDtoSimpleValidate.class)
    @CaseCheck(value = CaseMode.UPPER,message = "用户名必须大写字母",groups = UserRequestDtoSimpleValidate.class)
    public String userName;
    
    // 。。。。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

  • 相关阅读:
    揭秘 Task.Wait
    sql2o 将 sql 语句的结果转换为对象支持命名参数
    spring 单元测试为 logback 添加环境变量
    5G与物联网应用:新一代网络技术融合开创新时代
    自学的程序员一点竞争力都没有吗?投了5天简历,一个面试通知都没有怎么办?
    一次logstash的实践解锁了如此多的玩法....
    webstorm 去除注释空格
    智能反射面辅助的物理层安全技术综述
    excel数据透视表
    Thymeleaf
  • 原文地址:https://blog.csdn.net/qq_44732146/article/details/127725758