Spring参数校验及通用异常信息返回

发布于 2020-10-21  365 次阅读


前言

用注解@Validated、@Valid进行参数验证,相对于以前常用的if等条件,会显得简练很多,而且显得更加优雅。

然后是他俩的区别

@Validated:用在方法入参上无法单独提供嵌套验证功能,不能用在成员属性(字段)上,也无法提示框架进行嵌套验证,能配合嵌套验证注解@Valid进行嵌套验证。

@Valid:用在方法入参上无法单独提供嵌套验证功能,能够用在成员属性(字段)上,提示验证框架进行嵌套验证,能配合嵌套验证注解@Valid进行嵌套验证。

安装Maven依赖

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

常用的验证注解

@Null  被注释的元素必须为null
@NotNull  CharSequence, Collection, Map 和 Array 对象不能是 null, 但可以是空集(size = 0)。  
@NotEmpty  CharSequence, Collection, Map 和 Array 对象不能是 null 并且相关对象的 size 大于 0。  
@NotBlank  String 不是 null 且去除两端空白字符后的长度(trimmed length)大于 0。 
@AssertTrue  被注释的元素必须为true
@AssertFalse  被注释的元素必须为false
@Min(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max,min)  被注释的元素的大小必须在指定的范围内。
@Digits(integer,fraction)  被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past  被注释的元素必须是一个过去的日期
@Future  被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式。
@Email 被注释的元素必须是电子邮件地址
@Length 被注释的字符串的大小必须在指定的范围内
@Range  被注释的元素必须在合适的范围内

全局异常捕获

ControllerAdvice用于捕获全局的控制器抛出的异常,这里只列出了验证相关的异常,其他Exception或者自定义的异常,都可以在这里捕获。

@RestControllerAdvice
@Slf4j
@Component
public class GlobalExceptionHandler {
   /**
     * 处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
     *
     * @param e e
     * @return Result
     */
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public String bindExceptionHandler(BindException e) {
        log.error("{}, 发生异常: {}", httpServletRequest.getRequestURI(), e);
        String message = e.getBindingResult().getAllErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return Result.build().errorJson(message);
    }

    /**
     * 处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。
     *
     * @param httpServletRequest httpServletRequest
     * @param e                  e
     * @return Result
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public String exceptionHandler(HttpServletRequest httpServletRequest, MethodArgumentNotValidException e) {
        log.error("{}, 发生异常: {}", httpServletRequest.getRequestURI(), e);
        String errField = Objects.nonNull(e.getBindingResult().getFieldError()) ? ":" + e.getBindingResult().getFieldError().getField() : "";
        Object rejectValue = e.getBindingResult().getFieldError().getRejectedValue();
        return Result.build().errorJson(StrUtil.format("字段{}不允许值:{}", errField, rejectValue));
    }

    /**
     * 处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException
     *
     * @param httpServletRequest httpServletRequest
     * @param e                  e
     * @return Result
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public String exceptionHandler(HttpServletRequest httpServletRequest, ConstraintViolationException e) {
        log.error("{}, 发生异常: {}", httpServletRequest.getRequestURI(), e);
        StringBuilder sb = new StringBuilder();
        e.getConstraintViolations().forEach(constraintViolation -> {
            sb.append(StrUtil.format("字段:{},不允许值:{},{} ", constraintViolation.getPropertyPath(),
                    constraintViolation.getInvalidValue(), constraintViolation.getMessage()));
        });
        return Result.build().errorJson(sb.toString().trim());
    }

}

如何使用

比如新建这样一个类,给对应的字段添加注解以及设置message参数,如果不设置message,会使用默认的错误消息

public class User {
    @NotEmpty(message = "请输入用户名")
    private String username;
    @NotEmpty
    @Length(min=6,max=18,message = "密码在6位到18位之间")
    private String password;
}

如果验证不符合会自动抛出异常,由上面的exceptionHandler进行捕获处理。

针对方法的普通变量参数进行验证时,@Validated需要加在控制器上,然后变量前加验证注解

这个如果自动抛出异常的话时ConstraintViolationException

@RestController
@RequestMapping("/file")
@Validated
public class FileController{    
   @DeleteMapping("/deleteFile")
    public Result<?> deleteFile( @NotBlank String jobId,@NotBlank String filePath) throws IOException {
        return jobService.deleteFile(jobId, filePath);
    }
}
{
    "code": "1",
    "data": {},
    "msg": "操作失败:字段:deleteFile.jobId,不允许值:null,不能为空 字段:deleteFile.filePath,不允许值:null,不能为空"
}

用Validated或Valid注解,验证一个实体类,也就是接收json数据,这个需要将注解标注于变量前面

这个如果自动抛出异常的话时MethodArgumentNotValidException

    @PostMapping
    public Result<?> add(@RequestBody @Validated JobDO jobDO) {
        return Result.build(jobService.insertOrUpdate(jobDO)).success();
    }

    @PostMapping
    public Result<?> add(@RequestBody @Valid JobDO jobDO) {
        return Result.build(jobService.insertOrUpdate(jobDO)).success();
    }
{
    "code": "1",
    "data": {},
    "msg": "操作失败:字段:fileId,不允许值:null,不能为空"
}

嵌套校验

对象的嵌套校验,需要在嵌套的字段上添加@Valid注解,否则验证时会略过字段props所属类Item里字段的验证规则

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

        @NotNull(message = "props不能为空")
        @Size(min = 1, message = "至少要有一个属性")
        @Valid // 嵌套验证必须用@Valid
        private List<Item> props;
    }
    public class Item {
        @NotBlank(message = "name不能为空")
        private String name;

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

分组验证

Validation的分组验证,比如添加/编辑数据时,添加是不需要验证id的,而编辑的话id是必须的,还有可能这个类被不同控制器,方法使用时需要的参数不同,这时可以指定其groups参数指定要验证的规则。

    //用于声明验证规则所属的分组 
    public interface EditSelectListGroup
    {
    }
    
    public class SelectList {
        //指定验证规则属于哪个分组
        @NotNull(message = "id不能为空",groups = {EditSelectListGroup.class})
        @Min(value = 1, message = "id必须为正整数" ,groups= {EditSelectListGroup.class})
        private Long id;

        @NotNull(message = "props不能为空")
        @Size(min = 1, message = "至少要有一个属性")
        private List<Item> props;
    }
    //指定使用EditSelectListGroup分组下的验证规则,如果不指定分组,那么SelectList的字段id上注解@NotNull和@Min会被忽略
    //此处必须使用@Validated而不是@Valid,因为@Valid不支持分组条件
    @PutMapping
    public Result<?> edit(@RequestBody @Validated(value = {EditSelectListGroup.class}) JobDO jobDO) {
        //xxxxxxxxxxxxxxxxxxxxxxx
    }

数据传递到spring中的执行过程

前端通过HTTP协议将数据传递到Spring,Spring通过HttpMessageConverter类将流数据转换成Map类型,然后通过ModelAttributeMethodProcessor类对参数进行绑定到方法对象中,并对带有@Valid或@Validated注解的参数进行参数校验,对参数进行处理和校验的方法为ModelAttributeMethodProcessor.resolveArgument(…),通过查看源码,当BindingResult中存在错误信息时,会抛出BindException异常,BindException实现了BindingResult接口(BindResult是绑定结果的通用接口, BindResult继承于Errors接口),所以该异常类拥有BindingResult所有的相关信息,因此我们可以通过捕获该异常类,对其错误结果进行分析和处理。这样,我们对是Content-Typeapplication/x-www-form-urlencoded的请求(也就是表单),的参数校验的异常处理就解决了。

对于不同的传输数据的格式spring采用不同的HttpMessageConverter(http参数转换器)来进行处理,比如JasksonHttpMessageConverter,或者使用fastjson的话,可以自定义FastJsonHttpMessageConverter

HttpMessageConverter简介

HTTP 请求和响应的传输是字节流,意味着浏览器和服务器通过字节流进行通信。但是使用Spring MVC的Controller 类中的方法都是返回String类型或其他Java对象,如何将对象转换成字节流进行传输?这时就需要一个消息转化器。

在报文到达Spring MVC和从Spring MVC出去,都存在一个字节流到Java对象的转换问题。在Spring MVC中,它是由HttpMessageConverter来处理的。

当请求报文来到java中,它会被封装成为一个ServletInputStream的输入流,供我们读取报文。响应报文则是通过一个ServletOutputStream的输出流,来输出响应报文。

针对不同的数据格式,Spring MVC会采用不同的消息转换器进行处理,当使用json作为传输格式时,Spring MVC会采用MappingJacksonHttpMessageConverter消息转换器, 而且底层在对参数进行校验错误时,抛出的是MethodArgumentNotValidException异常,因此我们需要对BindException和MethodArgumentNotValidException进行统一异常管理,最终代码演示如上所示。


LoneKing