SpringBoot 优雅式数据校验

Spring Boot 优雅式数据校验

----javax.validation

0X01 需求描述

在实际项目开发中,总会需要对于由客户端用户传递过来的数据进行校验工作,如果需要校验的字段比较多,此时可以选择进行统一的异常处理并返回异常信息,如果需要进行精准返回错误信息则需要进行大量的条件判断,那么这样则编码工作是非常痛苦的,而且代码还相当冗长,所以这时我们可以使用Spring的javax.validation注解式参数校验来简化这部分的代码开发,从而写出更优雅式、清晰阅读、便于维护的代码。

0X02 快速实现

下面就简单写个Demo来体验一把使用javax.validation来简化业务开发的快感!

由于SpringBoot2.3.0之后就不在集成Validation组件了,那么此时就需要手动导入Spring Boot Starter Validation。或者可以降低SpringBoot的版本。我这里使用SpringBoot2.6.6。

1.导入相关依赖

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

2.编写实体类

package cn.imyjs.pojo;

import lombok.Data;

// 注意以下注解均是由javax.validation提供!
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * @Classname user
 * @Description TODO
 * @Date 2022/4/22 19:03
 * @Created by YJS
 * @WebSite www.imyjs.cn
 */
@Data  // 此处是lombok提供相关注解,用于简化实体类开发
public class User {

    @NotNull(message = "用户ID不得为空!")
    private Integer id;

    @Size(min = 3, max = 8, message = "用户名长度在3~8个字符!")
    private String username;

    @Size(min = 6, max = 16, message = "用户名长度在6~16个字符!")
    private String password;

    @Min(value = 18, message = "年龄最低限制18!")
    @Max(value = 60, message = "年龄最高限制18!")
    private Integer age;
}

3.编写控制器类

@RestController
@RequestMapping("user")
public class UserController {

    /**
     * 演示GET请求方式的使用
     * @param user
     * @return
     */
    @GetMapping("register")
    public String login( @Valid User user){  // 此处使用@Valid 注解开启校验
        System.out.println(user);
        return "注册成功!";
    }

    /**
     * 演示POST请求方式的使用
     * @param user
     * @param bindingResult
     * @return
     */
    @PostMapping("register")
    public String register(@Valid @RequestBody User user, BindingResult bindingResult){ // 此处使用@Valid 注解开启校验
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        if ( allErrors.size() > 0){
            return allErrors.get(0).getDefaultMessage();
        }
        System.out.println(user);
        return "注册成功!";
    }
}

此时,对于校验非法的数据相关提示信息并不会返回到前端。需要进行如下的处理!

4.编写异常处理类

@RestControllerAdvice
public class GlobalExceptionHandler {
    //处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public String BindExceptionHandler(BindException e) {
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return message;
    }
}

经过测试,此时只能处理GET请求的异常信息,那么对于其他方式请求可以在对应的控制层方法中,进行如下校验:

@PostMapping("register")
    public String register(@Valid @RequestBody User user, BindingResult bindingResult){ // 此处使用@Valid 注解开启校验
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        if ( allErrors.size() > 0){
            return allErrors.get(0).getDefaultMessage();
        }
        System.out.println(user);
        return "注册成功!";
    }
  • 在方法参数列表中添加 BindingResult bindingResult
  • 然后通过getAllErrors()方法获取可能产生的校验失败信息
  • 最后将失败信息返回即可!

0X03 常用注解

验证注解 验证的数据类型 说明
@AssertFalse Boolean,boolean 验证注解的元素值是false
@AssertTrue Boolean,boolean 验证注解的元素值是true
@NotNull 任意类型 验证注解的元素值不是null
@Null 任意类型 验证注解的元素值是null
@Min(value=值) BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 验证注解的元素值大于等于@Min指定的value值
@Max(value=值) 和@Min要求一样 验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值) 和@Min要求一样 验证注解的元素值大于等于@ DecimalMin指定的value值
@DecimalMax(value=值) 和@Min要求一样 验证注解的元素值小于等于@ DecimalMax指定的value值
@Digits(integer=整数位数, fraction=小数位数) 和@Min要求一样 验证注解的元素值的整数位数和小数位数上限
@Size(min=下限, max=上限) 字符串、Collection、Map、数组等 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Past java.util.Date,java.util.Calendar;Joda Time类库的日期类型 验证注解的元素值(日期类型)比当前时间早
@Future 与@Past要求一样 验证注解的元素值(日期类型)比当前时间晚
@NotBlank CharSequence子类型 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限) CharSequence子类型 验证注解的元素值长度在min和max区间内
@NotEmpty CharSequence子类型、Collection、Map、数组 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Range(min=最小值, max=最大值) BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式) CharSequence子类型(如String) 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式) String,任何CharSequence的子类型 验证注解的元素值与指定的正则表达式匹配
@Valid 任何非原子类型 指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证

0X04 方法级别验证

在能够对传入的实体进行验证之后,希望能够对通过 @RequestParam 或者是 @PathVariable 传入的参数通过注解校验,那就很方便了。Spring对此也进行了扩展,使用方式如下:

  1. 增加配置 MethodValidationPostProcessor 参数校验处理器
    package cn.imyjs.config;
    
    import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
    
    /**
     * @Classname MethodValidationPostProcessor
     * @Description TODO
     * @Date 2022/4/22 21:21
     * @Created by YJS
     * @WebSite www.imyjs.cn
     */
    @Configuration
    @EnableAutoConfiguration
    public class MethodValidationConfig {
        @Bean
        public MethodValidationPostProcessor methodValidationPostProcessor() {
            return new MethodValidationPostProcessor();
        }
    }
    
  2. 在对应的 controller 或者 service 或者其他想要校验方法参数的类增加 @Validated
    @RestController("hello")
    @Validated
    public class HelloController {
        @RequestMapping("/{username}")
        public String hello(@PathVariable @Size(min = 6, max = 16, message = "用户名长度在6~16位!") String username){
            return "Hello" + username;
        }
    }
    
  3. 此时,需要进行重新编写参数异常处理类
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        // 处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
        // 用于处理类似http://localhost/user/register?age=10&username=aa请求中age和username的校验引发的异常
        // BindException --- 用于处理请求参数为实体类时校验引发的异常  --- Content-Type为application/x-www-form-urlencoded
        @ExceptionHandler(BindException.class)
        @ResponseBody
        public String BindExceptionHandler(BindException e) {
            String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
            return message;
        }
    
        //处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException
        @ExceptionHandler(ConstraintViolationException.class)
        @ResponseBody
        public String ConstraintViolationExceptionHandler(ConstraintViolationException e) {
            String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
            return message;
        }
    
        //处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。
        // MethodArgumentNotValidException --- 用于处理请求参数为实体类时校验引发的异常 --- Content-Type为application/json
        @ExceptionHandler(MethodArgumentNotValidException.class)
        @ResponseBody
        public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
            String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
            return message;
        }
    
    
        // 可能出现的未知异常
        @ExceptionHandler(value = Exception.class)
        @ResponseBody
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public String handle(Exception e) {
            return "系统未知异常";
        }
    
    }
    

0X05 @Valid与@Validated

以下内容参考自网络:@Validated和@Valid区别

在检验 Controller 的入参是否符合规范时,使用 @Validated 或者 @Valid 在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:

1. 分组

@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。将不同的校验规则分给不同的组,在使用时,指定不同的校验规则:

  • 创建接口类
    /**
     * 校验分组1
     */
    public interface Group1 {
    }
    
    // ------------------------------ //
    
    /**
     * 校验分组2
     */
    public interface Group2 {
    }
    
  • 实体类
    @Data
    public class User {
        /**
         * 用户名
         */
        @NotBlank(message = "用户名不能为空!", groups = {Group1.class})
        private String username;
        /**
         * 性别
         */
        @NotBlank(message = "性别不能为空!")
        private String gender;
        /**
         * 年龄
         */
        @Min(value = 1, message = "年龄有误!", groups = {Group1.class})
        @Max(value = 120, message = "年龄有误!", groups = {Group2.class})
        private int age;
        /**
         * 地址
         */
        @NotBlank(message = "地址不能为空!")
        private String address;
        /**
         * 邮箱
         */
        @Email(message = "邮箱有误!", groups = {Group2.class})
        private String email;
        /**
         * 手机号码
         */
        @Pattern(regexp = "^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$",message = "手机号码有误!", groups = {Group2.class})
        private String mobile;
    }
    
  • 控制类
    @RestController
    @RequestMapping("/api")
    public class Demo1Controller {
     
        @PostMapping("/insert1")
        public Result validatedDemo3(@Validated @RequestBody User user){
            return ResultUtil.success(user);
        }
     
        @PostMapping("/insert2")
        public Result validatedDemo4(@Validated(Group1.class) @RequestBody User user){
            return ResultUtil.success(user);
        }
     
        @PostMapping("/insert3")
        public Result validatedDemo5(@Validated(Group2.class) @RequestBody User user){
            return ResultUtil.success(user);
        }
    }
    

@Valid:作为标准JSR-303规范,还没有分组的功能。

2. 注解地方

@Validated:可以用在类、方法和方法参数上。但是不能用在成员属性(字段)上。

@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上。

两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。

3. 嵌套验证

在比较两者嵌套验证时,先说明下什么叫做嵌套验证。比如我们现在有个实体叫做Item:

public class Item {

    @NotNull(message = "id不能为空")
    @Min(value = 1, message = "id必须为正整数")
    private Long id;

    @NotNull(message = "props不能为空")
    @Size(min = 1, message = "至少要有一个属性")
    private List<Prop> props;
}

props带有很多属性,属性里面有属性id,属性值id,属性名和属性值,如下所示:

public class Prop {

    @NotNull(message = "pid不能为空")
    @Min(value = 1, message = "pid必须为正整数")
    private Long pid;

    @NotNull(message = "vid不能为空")
    @Min(value = 1, message = "vid必须为正整数")
    private Long vid;

    @NotBlank(message = "pidName不能为空")
    private String pidName;

    @NotBlank(message = "vidName不能为空")
    private String vidName;
}

属性这个实体也有自己的验证机制,比如属性和属性值id不能为空,属性名和属性值不能为空等。

现在我们有个 ItemController 接受一个Item的入参,想要对Item进行验证,如下所示:

@RestController
public class ItemController {

    @RequestMapping("/item/add")
    public void addItem(@Validated Item item, BindingResult bindingResult) {
        doSomething();
    }
}

在以上代码中,如果Item实体的props属性不额外加注释,只有@NotNull和@Size,无论入参采用@Validated还是@Valid验证,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证,也就是@Validated和@Valid加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List中有Prop的pid为空或者是负数,入参验证不会检测出来。

为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。

我们修改Item类如下所示:

public class Item {

    @NotNull(message = "id不能为空")
    @Min(value = 1, message = "id必须为正整数")
    private Long id;

    @Valid // 嵌套验证必须用@Valid
    @NotNull(message = "props不能为空")
    @Size(min = 1, message = "props至少要有一个自定义属性")
    private List<Prop> props;
}

4.小结

@Validated @Valid
分组 提供分组功能,可在入参验证时,根据不同的分组采用不同的验证机制。 无分组功能
可注解位置 可以用在类型、方法和方法参数上。但是不能用在成员属性上 可以用在方法、构造函数、方法参数和成员属性上(两者是否能用于成员属性上直接影响能否提供嵌套验证的功能)
嵌套验证 用在方法入参上无法单独提供嵌套验证功能。

不能用在成员属性上。

也无法提供框架进行嵌套验证。

能配合嵌套验证注解@Valid进行嵌套验证。

用在方法入参上无法单独提供嵌套验证功能。

能够用在成员属性上,提示验证框架进行嵌套验证。

能配合嵌套验证注解@Valid进行嵌套验证。

总体来说@validated 相当于 @Valid 验证的升级版,功能更加强大。

0X06 微信关注

编程那点事儿

 

阅读剩余
THE END