• SpringBoot如何优雅的实现参数验证


    唠嗑部分

    在我们设计接口时,参数验证是必不可少的一个环节,严格的参数验证能够保证数据的严谨,那么在SpringBoot项目中,你是如何验证参数的呢?

    首先我们来描述一下需求

    用户类,有用户名、用户头像、邮件地址、年龄、手机号、出生日期,要求:

    1、用户名不能为空且由字母数字下划线组成,不超过16个字符。

    2、用户头像不能为空且为一个网络图片。

    3、邮箱不能为空且必须为一个合法的邮件地址。

    4、年龄不能为空且大于0。

    5、手机号不能为空且必须合法。

    6、出生日期不能为空且小于当前。

    对于以上需求,你的验证方式是否如下呢

    @PostMapping("/user/save")
    public CommonResult save(@RequestBody UserReq req){
        CommonResult r = CommonResult.success(null);
        String username = req.getUsername();
        String avatar = req.getAvatar();
        Integer age = req.getAge();
        LocalDate bothDate = req.getBothDate();
        String email = req.getEmail();
    
        String pattern = "[A-Za-z0-9_]+";
        Pattern p = Pattern.compile(pattern);
        Matcher m = p.matcher(username);
        if (!StringUtils.hasLength(username)) {
            return CommonResult.error(400, "用户名岂能为空??");
        } else if (m.matches()) {
            return CommonResult.error(400, "用户名格式有误!");
        } else if (username.length() > 16) {
            return CommonResult.error(400, "用户名长度不大于16位");
        }
    
        if (!StringUtils.hasLength(avatar)) {
            return CommonResult.error(400, "用户头像岂能为空??");
        } else if (!avatar.startsWith("http://") && !avatar.startsWith("https://")) {
            return CommonResult.error(400, "用户头像格式有误!");
        }
        ...
        // 剩余字段验证类似
        r.setData(req);
        return r;
    }
    
    • 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

    上述代码可以实现各种场景的参数验证,但是仅仅两个字段已经写了20行代码,更不要说后台管理系统中复杂的参数验证了,那有些小伙伴可能会问,既然参数验证这么麻烦,那么能不能接口不验证,将这个麻烦的活交给前端去做呢?

    有这个想法的小伙伴趁早打消念头哈,肯定是不行的,前端只能验证从我们系统前端发出的请求,如果有黑客绕过浏览器,使用http工具访问后台服务,并且有一些不好的企图,那么服务就危险了

    那么今天我们就来说一说如何优雅的实现参数验证,跟if…else说拜拜吧

    言归正传

    Spring已经整合了Hibernate Validator,对于SpringBoot来说使用也十分方便,使用 @Validated 注解,实现声明式校验

    我们先认识一下相关注解

    空和非空检查

    • @NotBlank :只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 。
    • @NotEmpty :集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null 。
    • @NotNull :不能为 null 。
    • @Null :必须为 null 。

    数值检查

    • @DecimalMax(value) :被注释的元素必须是一个数字,其值必须小于等于指定的最大值。
    • @DecimalMin(value) :被注释的元素必须是一个数字,其值必须大于等于指定的最小值。
    • @Digits(integer, fraction) :被注释的元素必须是一个数字,其值必须在可接受的范围内。
    • @Positive :判断正数。
    • @PositiveOrZero :判断正数或 0 。
    • @Max(value) :该字段的值只能小于或等于该值。
    • @Min(value) :该字段的值只能大于或等于该值。
    • @Negative :判断负数。
    • @NegativeOrZero :判断负数或 0 。

    Boolean 值检查

    • @AssertFalse :被注释的元素必须为 true 。
    • @AssertTrue :被注释的元素必须为 false 。

    长度检查

    • @Size(max, min) :检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。

    日期检查

    • @Future :被注释的元素必须是一个将来的日期。
    • @FutureOrPresent :判断日期是否是将来或现在日期。
    • @Past :检查该字段的日期是在过去。
    • @PastOrPresent :判断日期是否是过去或现在日期。

    其它检查

    • @Email :被注释的元素必须是电子邮箱地址。
    • @Pattern(value) :被注释的元素必须符合指定的正则表达式。

    Hibernate Validator 附加的约束注解

    org.hibernate.validator.constraints 包下,定义了一系列的约束( constraint )注解。如下:

    • @Range(min=, max=) :被注释的元素必须在合适的范围内。
    • @Length(min=, max=) :被注释的字符串的大小必须在指定的范围内。
    • @URL(protocol=,host=,port=,regexp=,flags=) :被注释的字符串必须是一个有效的 URL 。
    • @SafeHtml :判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。

    下面就让我们来体验一下

    1、首先导入依赖

    小白记得在2.1.6版本的时候,这个依赖是不用导入的,但是具体在哪个版本,官方将这个依赖分离了出来,2.4.2需要手动导入

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-validationartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    2、DTO添加参数验证注解

    @Data
    public class UserReq {
    
        @NotBlank(message = "用户名岂能为空")
        @Pattern(regexp = "[A-Za-z0-9_]+", message = "用户名格式有误")
        @Size(max = 16, message = "用户名不能大于16位")
        private String username;
    
        @NotBlank(message = "用户头像岂能为空")
        @Pattern(regexp = "[a-zA-z]+:\\/\\/[^\\s]*", message = "用户头像格式有误")
        private String avatar;
    
        @NotBlank(message = "邮箱岂能为空")
        @Email(message = "邮箱格式有误")
        private String email;
    
        @NotNull(message = "年龄岂能为空")
        @Min(value = 0, message = "年龄必须大于0")
        private Integer age;
    
        @NotBlank(message = "手机号岂能为空")
        @Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "手机号格式有误")
        private String phone;
    
        @NotNull(message = "出生日期岂能为空")
        @Past(message = "出生日期不可大于当前")
        @JsonFormat(pattern = "yyyy-MM-dd")
        private LocalDate bothDate;
    }
    
    • 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

    3、接口参数添加@Validated注解

    @PostMapping("/user/save")
    public CommonResult save(@RequestBody @Validated UserReq req){
        CommonResult r = CommonResult.success(null);
        r.setData(req);
        return r;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4、添加全局异常处理器,处理参数验证异常

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(value = ConstraintViolationException.class)
        public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
            // 拼接错误
            StringBuilder detailMessage = new StringBuilder();
            for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
                if (detailMessage.length() > 0) {
                    detailMessage.append(";");
                }
                detailMessage.append(constraintViolation.getMessage());
            }
            return CommonResult.error(400, detailMessage.toString());
        }
    
        @ExceptionHandler(value = BindException.class)
        public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {
            // 拼接错误
            StringBuilder detailMessage = new StringBuilder();
            for (ObjectError objectError : ex.getAllErrors()) {
                if (detailMessage.length() > 0) {
                    detailMessage.append(";");
                }
                detailMessage.append(objectError.getDefaultMessage());
            }
            return CommonResult.error(400, detailMessage.toString());
        }
    
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public CommonResult MethodArgumentNotValidExceptionHandler(HttpServletRequest req, MethodArgumentNotValidException ex) {
            // 拼接错误
            StringBuilder detailMessage = new StringBuilder();
            for (ObjectError objectError : ex.getBindingResult().getAllErrors()) {
                if (detailMessage.length() > 0) {
                    detailMessage.append(";");
                }
                detailMessage.append(objectError.getDefaultMessage());
            }
            return CommonResult.error(400, detailMessage.toString());
        }
    
    
        /**
         * 处理其它 Exception 异常
         *
         * @param req
         * @param e
         * @return
         */
        @ExceptionHandler(value = Exception.class)
        public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {
            return CommonResult.error(500, e.getMessage());
        }
    }
    
    • 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

    5、验证错误参数

    image-20230522112409593

    6、验证正确参数

    image-20230522112445841

    结语

    1、大多数场景下,我们使用 @Validated 注解即可。如果有嵌套校验的场景,使用 @Valid 注解添加到成员属性上。

    2、保证接口的健壮性,参数验证是必不可少的环节,不可省略,而且应该极为严格,绝不能让恶意用户有可乘之机。

    3、制作不易,一键三连再走吧,您的支持永远是我最大的动力!

  • 相关阅读:
    C# | 使用Json序列化对象时忽略只读的属性
    用js理解常用设计模式
    A1050 String Subtraction
    【linux环境下安装opencv3.4.5】
    【luogu P1912】诗人小G(二分栈)(决策单调性优化DP)
    LLM - 旋转位置编码 RoPE 代码详解
    【前端】响应式布局笔记——rem
    训练跳跃(青蛙跳台阶),剑指offer,力扣
    LLM大模型训练和预测如何计算算力需求?
    零代码编程:用ChatGPT批量自动下载archive.org上的音频书
  • 原文地址:https://blog.csdn.net/admin_2022/article/details/130840361