参数校验@Valid

617 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

学习一下SpringBoot中,关于请求参数校验以及及异常处理方式。

简介

参数校验时必要的,如何合理的处理也是必要的。学习一下@Valid 和 @Validated。


为何需要Valid

无论是前端,或是关联系统互调接口,我们的业务逻辑对于参数是有要求或是限制,如果不进行参数校验不可避免的会出现异常。

如果我们不借助Valid来处理异常,往往会使得代码臃肿,如:

需要嵌套判断,如果参数过多,很不好。

@RequestMapping(value = "/withOutValid", method = RequestMethod.POST)
public ResultVo<Object> validTest01(@RequestBody ValidTestVo validTestVo) {
    if (!ObjectUtils.isEmpty(validTestVo)){
        if (ObjectUtils.isEmpty(validTestVo.getParam01())){
            return ResultVo.error("param01参数不能为空");
        }
        if (ObjectUtils.isEmpty(validTestVo.getParam02())){
            return ResultVo.error("param02参数不能为空");
        }
        //、、、、
    }
    log.info("参数:{}", validTestVo);
    return ResultVo.success();
}

而使用@Valid注解可以很简洁的实现参数校验

注:参数校验的结果会封装到BindingResult中,可以很方便查看。

@RequestMapping(value = "/valid", method = RequestMethod.POST)
public ResultVo<Object> validTest02(@RequestBody @Valid ValidTestVo validTestVo, BindingResult bindingResult) {

    log.info("参数:{}", validTestVo);
    log.info("参数校验结果:{}", bindingResult);

    return ResultVo.success();
}

依赖

@Validated隶属于org.springframework下,只要是SpringBoot项目都包含。

@Valid隶属于javax下,版本不同,可能有区别,如果缺少依赖就添加如下依赖,版本选择本文2.6.6。

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

注解

空检查

  • @Null 只允许传null
  • @NotNull 不允许传null,不会检查空字符串
  • @NotBlank 不允许传null,且trim后不允许为空字符串
  • @NotEmpty 不允许传null,且empty返回false

Booelan检查

  • @AssertTrue Boolean类型,只允许传true
  • @AssertFalse Boolean类型,只允许传 false

长度检查

  • @Size(min=, max=) 验证存在Size()api的集合长度
  • @Length(min=, max=) 验证存在length()api的类 String)

日期检查

  • @Past 过去的时间
  • @Future 未来的时间
  • @Pattern 格式检查

数值检查

  • @Min 最小
  • @Max 最大

其他

  • @CreitCardNumber信用卡验证
  • @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。

嵌套校验

很多情况下,往往一个校验的参数中,嵌套着其他的对象。

对于继承关系来说

子类继承父类,父类的校验也会被扫描到,都可以参与校验。

对于聚合关系来说

测试

Param01

public class Param01 {

    @ApiModelProperty("参数1,不为null校验")
    @NotNull(message = "参数不能为null")
    String param01;

    Param02 param02;
}

Param02

public class Param02 {

    @ApiModelProperty("参数1,不为null校验")
    @NotNull(message = "参数不能为null")
    String param01;

    @ApiModelProperty("参数2,不为null校验")
    @NotNull(message = "参数不能为null")
    String param02;

}

controller

@RequestMapping(value = "/validX", method = RequestMethod.POST)
public ResultVo<Object> validTest04(@RequestBody @Valid Param01 param01) {
    log.info("参数:{}", param01);
    //log.info("参数校验结果:{}", bindingResult);
    return ResultVo.success();
}

postman测试

发现参数并没有参与校验!

{
    "param01": "demoData",
    "param02": {
        "param01": "demoData"
    }
}
解决

添加@Valid

public class Param01 {
    @ApiModelProperty("参数1,不为null校验")
    @NotNull(message = "参数不能为null")
    String param01;
    
	@Valid
    Param02 param02;
}
最佳实践

对于没有嵌套的校验,@Valid和@Validated没有区别,其校验结果都会放入BindingResult,但一般来说会通过异常捕获的方式处理校验失败的情况。

@Validated是Spring提供的,可以认为是对@Valid的封装,提供了分组等较灵活的使用方式。

使用@Validated在控制器参数校验,@Valid进行嵌套校验。


异常处理

第一步:在springBoot启动器同级目录下创建一个common.exception包,并在此目录下创建一个自定义全局异常处理类。

 public class GlobalCustomException extends RuntimeException {
     private static final long serialVersionUID = -7278881947512853935L;
     @ApiModelProperty("异常代码")
     private String code;
     @ApiModelProperty("异常描述")
     private String msg;
 }

第二步:在该目录下,创建一个异常处理器。

一般来说会有一个最后的屏障来处理未知异常RuntimeException,以便友好显示。

parse()方法是对BindResult的解析,显示具体哪个参数不符合校验规则,其实大可不必,上线了一般不会出现参数问题,主要方便联调。

@RestControllerAdvice
@Slf4j
public class GlobalCustomExceptionHandler {
    /**
     * 未知异常
     *
     * @param e 异常
     * @return ResultVo<String>
     */
    @ExceptionHandler(value = RuntimeException.class)
    public ResultVo<String> unKnownException(RuntimeException e) {
        log.error("系统出现异常:{}", e.getMessage());
        return ResultVo.error(e.getMessage());
    }
    /**
     * 请求参数不合法
     *
     * @param notValid 请求参数不合法异常
     * @return ResultVo<String>
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public ResultVo<Map<String, Object>> methodArgumentNotValidException(MethodArgumentNotValidException notValid) {
        Map<String, Object> errorDesc = parse(notValid);
        log.error("请求参数不合法:{}", JSON.toJSONString(errorDesc));
        return ResultVo.error(errorDesc);
    }
    
    /**
     * 解析异常
     *
     * @param notValid 异常
     * @return Map<String, String>
     */
    private Map<String, Object> parse(MethodArgumentNotValidException notValid) {
        //方法名
        String methodName = Objects.requireNonNull(notValid.getParameter().getMethod()).getName();
        //异常信息保存于BindingResult
        BindingResult exceptions = notValid.getBindingResult();
        List<ObjectError> allErrors = exceptions.getAllErrors();
        //返回结果
        Map<String, Object> result = new HashMap<>();
        //参数错误
        Properties fieldErrorProp = new Properties();
        allErrors.forEach((error) -> {
            //转化为fieldError
            FieldError fieldError = (FieldError) error;
            //放入prop
            fieldErrorProp.setProperty(fieldError.getField(), fieldError.getDefaultMessage());
        });

        result.put("methodName", methodName);
        result.put("fieldErrorProp", fieldErrorProp);
        return result;
    }
}