BindException、ConstraintViolationException、MethodArgumentNotValidException入参验证异常分析和全局异常处理解决方法

时间:2024-11-15 16:21:48

Spring validation验证框架注解

Spring validation验证框架提供了大量接口入参检验注解,注意三个非空注解:

  • @NotNull:验证对象是否不为null, 无法查检长度为0的字符串
  • @NotBlank:检查约束 (字符串) 是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格
  • @NotEmpty:检查(集合)约束元素是否为NULL或者是EMPTY
  • @Size(min=,max=):验证对象(Array,Collection,Map,String)长度是否在给定的范围之内。不要错用了异常类型,比如在int上不可用@size,而@Length(min=, max=) 只适用于String 类型
  • @AssertTrue: 验证 Boolean 对象是否为 true,@AssertFalse: 验证 Boolean 对象是否为 false
  • @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指定小数精度。

相关检验注解参考:

/qq_45151158/article/details/103126907

代码测试

非空检验实体类:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {

    private static final long serialVersionUID = -5236977214039498801L;

    // 基本类型
    @NotNull(message = "用户ID不能为空!")
    private Long userId;

    // 集合类型
    @NotEmpty(message = "地址集合不能为空!")
    private List<String> addressId;

    // 字符串
    @NotBlank(message = "备注不能为空!")
    private String comment;

}

BindException异常

测试接口:

@RestController
@RequestMapping("/student")
@Slf4j
@Validated // 可对接口方法的集合参数校验
public class StudentController {

    /**
     * 测试空参接口
     * @param student
     * @return
     */
    @PostMapping("/test")
    public ResponseResult<Void> test(@Valid Student student) {
        return new ResponseResult<>(ResponseResult.CodeStatus.OK, "正常访问!");
    }

}

这里接收参数没有加@RequestBody注解,Postman测试如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KpJHg0aL-1619077558263)(./images/)]

无法正常访问,BindException异常,日志报错如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yu1nHpM6-1619077558265)(./images/)]

解决方法

可以使用BindingResult(可能出现其它异常,代码冗余,不建议)在接口方法中获取message返回或者全局捕获BindException异常进行返回处理。全局捕获BindException异常代码如下:

/**
 * Title:统一异常,返回json
 * Description:
 * @author WZQ
 * @version 1.0.0
 * @date 2021/4/22
 */
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
@Slf4j
public class BaseExceptionHandler {

    /**
     * 空参异常处理
     * @param ex
     * @param request
     * @return
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseResult<Void> bindException(BindException ex, HttpServletRequest request) {
        log.warn("BindException:", ex);
        try {
            // 拿到@NotNull,@NotBlank和 @NotEmpty等注解上的message值
            String msg = Objects.requireNonNull(ex.getBindingResult().getFieldError()).getDefaultMessage();
            if (StrUtil.isNotEmpty(msg)) {
                // 自定义状态返回
                return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, msg);
            }
        } catch (Exception ignored) {
        }
        // 参数类型不匹配检验
        StringBuilder msg = new StringBuilder();
        List<FieldError> fieldErrors = ex.getFieldErrors();
        fieldErrors.forEach((oe) ->
                msg.append("参数:[").append(oe.getObjectName())
                        .append(".").append(oe.getField())
                        .append("]的传入值:[").append(oe.getRejectedValue()).append("]与预期的字段类型不匹配.")
        );
        return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, msg.toString());
    }

}

再次测试,捕获到异常并返回对应的错误信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-80dj2zt1-1619077558267)(./images/)]

ConstraintViolationException异常

利用BindingResult返回错误信息,测试接口:

@RestController
@RequestMapping("/student")
@Slf4j
@Validated // 可对接口方法的集合参数校验
public class StudentController {

    /**
     * 测试空参接口
     * @param student
     * @return
     */
    @PostMapping("/test")
    public ResponseResult<Void> test(@Valid Student student, BindingResult results) {
        if (results.hasErrors()) {
            // 没有统一异常的返回,获取注解上的默认message
            return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, Objects.requireNonNull(results.getFieldError()).getDefaultMessage());
        }
        return new ResponseResult<>(ResponseResult.CodeStatus.OK, "正常访问!");
    }

}

postman测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TugqWDZY-1619077558270)(./images/)]

不是BindingResult获得的错误信息,无法正常访问,ConstraintViolationException异常,日志报错如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ec0NyWA6-1619077558273)(./images/)]

解决方法1

正确使用@Validated和@Valid注解,由于实体类Student属性有List集合存在,需要结合@Validated和@Valid使用嵌套检验的方式,代码修改如下:

    // 基本类型
    @NotNull(message = "用户ID不能为空!")
    private Long userId;

    // 集合类型
    @NotEmpty(message = "地址集合不能为空!")
    @Valid // @Valid可用于集合嵌套验证
    private List<String> addressId;

    // 字符串
    @NotBlank(message = "备注不能为空!")
    private String comment;
@RestController
@RequestMapping("/student")
@Slf4j
//@Validated // 可对接口方法的集合参数校验
public class StudentController {

    /**
     * 测试空参接口
     * @param student
     * @return
     */
    @PostMapping("/test")
    public ResponseResult<Void> test(@Validated Student student, BindingResult results) {
        if (results.hasErrors()) {
            // 没有统一异常的返回,获取注解上的默认message
            return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, Objects.requireNonNull(results.getFieldError()).getDefaultMessage());
        }
        return new ResponseResult<>(ResponseResult.CodeStatus.OK, "正常访问!");
    }

}

正常访问:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0rQpW5qh-1619077558274)(./images/)]

@Validated和@Valid的区别和使用,包括嵌套检验可以参考:

/qq_27680317/article/details/79970590

/qq_45151158/article/details/112349233?spm=1001.2014.3001.5501

解决方法2

同样也可以全局捕获ConstraintViolationException异常并返回错误信息,错误信息是校验注解上的默认message。全局捕获ConstraintViolationException异常代码如下:

    /**
     * jsr 规范中的验证异常,嵌套检验问题
     * @param ex
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseResult<Void> constraintViolationException(ConstraintViolationException ex, HttpServletRequest request) {
        log.warn("ConstraintViolationException:", ex);
        Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();
        String message = violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));

//        ConstraintViolation<?> violation = ().next();
//        String path = ((PathImpl) ()).getLeafNode().getName();
//        String message2 = ("%s:%s", path, ());

        return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, message);
    }

postman测试如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qAQXIJ0w-1619077558276)(./images/)]

MethodArgumentNotValidException异常

BindingResult实际开发不常用,不可能每次都要在接口检测并返回错误信息,代码冗余。但是接口不加BindingResult参数又会出现异常。

@RestController
@RequestMapping("/student")
@Slf4j
//@Validated // 可对接口方法的集合参数校验
public class StudentController {

    /**
     * 测试空参接口
     * @param student
     * @return
     */
    @PostMapping("/test")
    public ResponseResult<Void> test(@RequestBody @Validated Student student) {
        return new ResponseResult<>(ResponseResult.CodeStatus.OK, "正常访问!");
    }

}

注意,这里接收参数加上@RequestBody注解才会有这种异常,Postman测试如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dhRz7vPB-1619077558277)(./images/)]

直接400返回,跟MethodArgumentNotValidException异常有关,日志信息如下:

2021-04-22 15:25:27.051  WARN 18068 --- [nio-7001-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument ...

解决方法

同样,全局捕获MethodArgumentNotValidException异常并返回错误信息,错误信息是校验注解上的默认message。全局捕获MethodArgumentNotValidException异常代码如下:

    /**
     * spring 封装的参数验证异常, 在controller中没有写BindingResult参数时,会进入
     * @param ex
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseResult<Void> methodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) {
        log.warn("MethodArgumentNotValidException:", ex);
        return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, Objects.requireNonNull(ex.getBindingResult().getFieldError()).getDefaultMessage());
    }

测试如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jr1lrj1T-1619077558278)(./images/)]

统一异常处理完整代码

Spring validation入参验证框架,一般在Controller类加上@Validated注解(可检验集合参数),接口方法对应的dto加上@Valid注解,然后直接对以上三个异常进行全局捕获处理即可。完整代码如下:

import cn.hutool.core.util.StrUtil;
import com.course.commons.dto.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Title:统一异常,返回json
 * Description:
 * @author WZQ
 * @version 1.0.0
 * @date 2021/4/22
 */
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
@Slf4j
public class BaseExceptionHandler {

    /**
     * 空参异常处理
     * @param ex
     * @param request
     * @return
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseResult<Void> bindException(BindException ex, HttpServletRequest request) {
        log.warn("BindException:", ex);
        try {
            // 拿到@NotNull,@NotBlank和 @NotEmpty等注解上的message值
            String msg = Objects.requireNonNull(ex.getBindingResult().getFieldError()).getDefaultMessage();
            if (StrUtil.isNotEmpty(msg)) {
                // 自定义状态返回
                return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, msg);
            }
        } catch (Exception ignored) {
        }
        // 参数类型不匹配检验
        StringBuilder msg = new StringBuilder();
        List<FieldError> fieldErrors = ex.getFieldErrors();
        fieldErrors.forEach((oe) ->
                msg.append("参数:[").append(oe.getObjectName())
                        .append(".").append(oe.getField())
                        .append("]的传入值:[").append(oe.getRejectedValue()).append("]与预期的字段类型不匹配.")
        );
        return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, msg.toString());
    }

    /**
     * jsr 规范中的验证异常,嵌套检验问题
     * @param ex
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseResult<Void> constraintViolationException(ConstraintViolationException ex, HttpServletRequest request) {
        log.warn("ConstraintViolationException:", ex);
        Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();
        String message = violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));

//        ConstraintViolation<?> violation = ().next();
//        String path = ((PathImpl) ()).getLeafNode().getName();
//        String message2 = ("%s:%s", path, ());

        return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, message);
    }

    /**
     * spring 封装的参数验证异常, 在controller中没有写result参数时,会进入
     * @param ex
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseResult<Void> methodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) {
        log.warn("MethodArgumentNotValidException:", ex);
        return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, Objects.requireNonNull(ex.getBindingResult().getFieldError()).getDefaultMessage());
    }
}