zoukankan      html  css  js  c++  java
  • SpringValid参数校验实践

    一、简介

      后台业务入口类Controller,对于入参的合法性校验,可以简单粗暴的写出一堆的 if 判断,如下:

    @RestController
    @RequestMapping("user")
    public class UserController {
    
        @PostMapping("saveUser")
        public String saveUser(UserInfoVo userInfoVo){
            if(StrUtil.isBlank(userInfoVo.getUserName())){
                return "userName is not null";
            }
            if(StrUtil.isBlank(userInfoVo.getPwd())){
                return "pwd is not null";
            }
            return "save success";
        }
    }

     二、重要说明

       2.1、springboot在2.3之后,spring-boot-starter-web的依赖项已经去除了validate依赖,推荐导入依赖:

         <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>

       2.2、关于 @Valid 和 @Validated

        @Validated 是Spring Validation验证框架对JSR-303规范的一个扩展, javax提供@Valid 是标准的JSR-303规范。使用基本无区别,但是在Group分组使用上还是使用 @Validated方便。

        嵌套验证上,必须在待验证的vo中的嵌套实体属性上增加@Valid。

    三、实验出真知

      3.1、牛刀小试

        定义VO,在需要校验的字段上加上相应注解

    @Data
    public class UserInfoVo {
        @NotBlank(message = "userName is not null")
        private String userName;
        @NotNull(message = "age is not null")
        private Integer age;
        @NotBlank(message = "pwd is not null")
        private String pwd;
    }

        Controller入参加上@Valid、校验结果BindingResult:

    @RestController
    @RequestMapping("user")
    public class UserController {
    
        @PostMapping("saveUser")
        public String saveUser(@Valid UserInfoVo userInfoVo, BindingResult bindingResult){
            if(bindingResult.hasErrors()){
                return bindingResult.getAllErrors().get(0).getDefaultMessage();
            }
            return "save success";
        }
    
    }

      通过工具访问,可看到如果入参不符合,会有相应message返回

      

      以上就完成了一个最简单的优雅校验过程,其中内置常用校验类型:

    空检查
    @Null       验证对象是否为null
    @NotNull    验证对象是否不为null, 无法查检长度为0的字符串
    @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
    @NotEmpty 检查约束元素是否为NULL或者是EMPTY.
     
    Booelan检查
    @AssertTrue     验证 Boolean 对象是否为 true 
    @AssertFalse    验证 Boolean 对象是否为 false 
     
    长度检查
    @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 
    @Length(min=, max=) Validates that the annotated string is between min and max included.
      
    日期检查
    @Past       验证 Date 和 Calendar 对象是否在当前时间之前 
    @Future     验证 Date 和 Calendar 对象是否在当前时间之后 
    @Pattern    验证 String 对象是否符合正则表达式的规则
     
    数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
    @Min            验证 Number 和 String 对象是否大等于指定的值 
    @Max            验证 Number 和 String 对象是否小等于指定的值 
    @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
    @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
    @Digits     验证 Number 和 String 的构成是否合法 
    @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
    @Range(min=, max=) Checks whether the annotated value lies between (inclusive) the specified minimum and maximum.
    @Range(min=10000,max=50000,message="range.bean.wage")
    private BigDecimal wage;
     
    @CreditCardNumber信用卡验证
    @Email  验证是否是邮件地址,如果为null,不进行验证,算通过验证。
    @ScriptAssert(lang= ,script=, alias=)
    @URL(protocol=,host=, port=,regexp=, flags=) 

       3.2、第一次改版

      入参很多的情况下,可能会同时产生多个不同的错误校验,那么如果每次只是返回一个错误提示,每次客户端改一个,那么体验是极差的。基于此,封装返回类,提供统一返回。

    @Data
    @NoArgsConstructor
    @RequiredArgsConstructor
    public class ResponseData<T> {
    
        @NonNull
        private Integer code;
        @NonNull
        private String message;
        private T data;
    
        public static ResponseData success() {
            return new ResponseData(HttpStatus.OK.value(), "SUCCESS");
        }
    
        public static ResponseData success(Object data) {
            ResponseData entity = success();
            entity.setData(data);
            return entity;
        }
    
        public static ResponseData fail(Integer code, String msg) {
            return new ResponseData(code, msg);
        }
    
        public static ResponseData fail(Integer code, String msg, Object data) {
            ResponseData entity = fail(code, msg);
            entity.setData(data);
            return entity;
        }
    }
    @RestController
    @RequestMapping("user")
    public class UserController {
    
        @PostMapping("saveUser")
        public ResponseData saveUser(@Valid UserInfoVo userInfoVo, BindingResult bindingResult){
            if(bindingResult.hasErrors()){
                List<String> collect = bindingResult.getFieldErrors().stream().map(item -> item.getDefaultMessage()).collect(Collectors.toList());
                return ResponseData.fail(900, "req param invalid", collect);
            }
            return ResponseData.success();
        }
    }

       3.3、第二次改版

        上述改版,已经能够一次性的返回所有未校验通过异常,但是,每个方法中都这么来一遍,还是挺麻烦的。下面利用统一异常处理参数校验,改造完成后Controller中专注于业务处理即可

    @RestController
    @RequestMapping("user")
    public class UserController {
    
        @PostMapping("saveUser")
        public ResponseData saveUser(@Valid UserInfoVo userInfoVo){
            return ResponseData.success();
        }
    }
    @RestControllerAdvice
    public class GlobalExceptionHandler {
        private static final Integer BAD_REQUEST_CODE = 900;
        private static final String BAD_REQUEST_MSG = "req param invalid";
    
        @ExceptionHandler(BindException.class)
        public ResponseData bindExceptionHandler(BindException exception){
            List<String> collect = exception.getAllErrors().stream().map(item -> item.getDefaultMessage())
                    .collect(Collectors.toList());
            return ResponseData.fail(BAD_REQUEST_CODE, BAD_REQUEST_MSG, collect);
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseData exceptionHandler(Exception exception){
            return ResponseData.fail(500, exception.getMessage());
        }
    }

       3.4、分组校验

      通常,存在场景:name参数在注册接口必须非空,但是修改接口无所谓。那么此时,分组group很好解决问题

    /**
     * @author cfang 2020/9/23 10:47
     *
     * 关于 extends Default
     * 继承 Default 的话,所有定义校验规则的都会校验
     * 不继承的话,则只校验加了group信息的校验字段
     */
    public interface UserGroup extends Default{
    }
    
    @Data
    public class UserInfoVo {
    
        @NotBlank(message = "userName is not null", groups = UserGroup.class)
        private String userName;
        @NotNull(message = "age is not null")
        private Integer age;
        @NotBlank(message = "pwd is not null")
        private String pwd;
    }
    
    @RestController
    @RequestMapping("user")
    public class UserController {
    
        @PostMapping("saveUser")
        public ResponseData saveUser(@Validated({UserGroup.class}) UserInfoVo userInfoVo){
            return ResponseData.success();
        }
    
        @PostMapping("updateUser")
        public ResponseData updateUser(@Valid UserInfoVo userInfoVo){
            return ResponseData.success();
        }
    
        @InitBinder
        public void init(HttpServletRequest request, DataBinder dataBinder){
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            dateFormat.setLenient(false); //是否校验转化日期格式,true-转化日期,false-参数错误则直接异常。eg. 2020-55-10, true->2024-10-10 , false-异常报错。默认值true
            dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
        }
    
    }

       3.5、递归校验

    @Data
    public class AddressInfoVo {
    
        @NotBlank(message = "street is not null")
        private String street;
    }
    
    @Data
    public class UserInfoVo {
    
        @NotBlank(message = "userName is not null", groups = UserGroup.class)
        private String userName;
        @NotNull(message = "age is not null")
        private Integer age;
        @NotBlank(message = "pwd is not null")
        private String pwd;
        @Past(message = "predate is invalid")
        @NotNull(message = "predate is not null")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone="GMT+8")
        private Date preDate;
        @Valid
        private AddressInfoVo infoVo;
    }
    
    @RestController
    @RequestMapping("user")
    public class UserController {
    
        @PostMapping("saveUser")
        public ResponseData saveUser(@Validated({UserGroup.class}) @RequestBody UserInfoVo userInfoVo){
            return ResponseData.success();
        }
    
        @PostMapping("updateUser")
        public ResponseData updateUser(@Valid @RequestBody UserInfoVo userInfoVo){
            return ResponseData.success();
        }
    
        @InitBinder
        public void init(HttpServletRequest request, DataBinder dataBinder){
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            dateFormat.setLenient(false);
            dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
        }
    }
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
        private static final Integer BAD_REQUEST_CODE = 900;
        private static final String BAD_REQUEST_MSG = "req param invalid";
    
        @ExceptionHandler(BindException.class)
        public ResponseData bindExceptionHandler(BindException exception){
            List<String> collect = exception.getAllErrors().stream().map(item -> item.getDefaultMessage())
                    .collect(Collectors.toList());
            return ResponseData.fail(BAD_REQUEST_CODE, BAD_REQUEST_MSG, collect);
        }
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResponseData argInvalidExceptionHandler(MethodArgumentNotValidException exception){
            List<String> collect = exception.getBindingResult().getFieldErrors().stream().map(item -> item.getDefaultMessage())
                    .collect(Collectors.toList());
            return ResponseData.fail(BAD_REQUEST_CODE, BAD_REQUEST_MSG, collect);
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseData exceptionHandler(Exception exception){
            return ResponseData.fail(500, exception.getMessage());
        }
    }

      

  • 相关阅读:
    夺命雷公狗—玩转SEO---50---让页面更好的被搜索引擎发现,自动推送篇
    夺命雷公狗—玩转SEO---49---让页面更好的被搜索引擎发现,主动推送篇
    夺命雷公狗—玩转SEO---48---让页面更好的被搜索引擎发现,外链篇
    夺命雷公狗—玩转SEO---47---让页面更好的被搜索引擎发现,网站地图篇
    夺命雷公狗—玩转SEO---46---浅谈搜索引擎
    夺命雷公狗—玩转SEO---45---浅入批量获得高频词
    夺命雷公狗—玩转SEO---44---外链群发原理
    夺命雷公狗—玩转SEO---43---外链提升网站权重
    夺命雷公狗—玩转SEO---42---快速交换友情链接
    夺命雷公狗—玩转SEO---41---H1标签的玩法
  • 原文地址:https://www.cnblogs.com/eric-fang/p/13713591.html
Copyright © 2011-2022 走看看