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

发布时间 2023-11-07 15:10:55作者: 追风fc

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指定小数精度。
相关检验注解参考:

https://blog.csdn.net/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;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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, "正常访问!");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这里接收参数没有加@RequestBody注解,Postman测试如下:

 

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

 

解决方法
可以使用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());
}

}
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
再次测试,捕获到异常并返回对应的错误信息:

 

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, "正常访问!");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
postman测试:

 

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

 

解决方法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;
1
2
3
4
5
6
7
8
9
10
11
12
@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, "正常访问!");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
正常访问:

 

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

https://blog.csdn.net/qq_27680317/article/details/79970590

https://blog.csdn.net/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 = violations.iterator().next();
// String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
// String message2 = String.format("%s:%s", path, violation.getMessage());

return new ResponseResult<>(ResponseResult.CodeStatus.FAIL, message);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
postman测试如下:

 

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, "正常访问!");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意,这里接收参数加上@RequestBody注解才会有这种异常,Postman测试如下:

 

直接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 ...
1
解决方法
同样,全局捕获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());
}
1
2
3
4
5
6
7
8
9
10
11
测试如下:

 

统一异常处理完整代码
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 = violations.iterator().next();
// String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
// String message2 = String.format("%s:%s", path, violation.getMessage());

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());
}
}
————————————————
版权声明:本文为CSDN博主「wzq_55552」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43409401/article/details/116017177