在项目里面,我们需要对前端传入的参数做一个简单的简单的校验,避免出现脏数据和业务逻辑错误。如果每个接口单独写校验逻辑的话,我们需要在controller层做逻辑判断。参数较少时,还勉强能够接受,如果参数和接口较多,无形中加重了工作量,也多了很多重复代码。所以引入注解式参数校验很有必要。
本文是基于springboot来实现参数校验,引入方式很简单,在pom中引入spring-boot-starter-validation即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
一般情况下,我们使用参数校验都需要返回异常信息,搭配全局异常捕获食用效果最佳。这里我就不介绍异常捕获的工作原理了,简单贴一下代码以供参考。先不需要考虑这的捕获器1、2、3、4是干什么的,下面会介绍作用。
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//捕获器1
@ExceptionHandler(value = {MissingServletRequestParameterException.class})
public ResponseVO<String> handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) {
if (log.isErrorEnabled()) {
log.error(ex.getMessage(), ex);
}
return ResponseVO.error(ResponseConstant.ERROR_CODE, String.format("缺少必要参数[%s]", ex.getParameterName()), "");
}
//捕获器2
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
public ResponseVO<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
if (log.isErrorEnabled()) {
log.error(ex.getMessage(), ex);
}
BindingResult result = ex.getBindingResult();
FieldError error = result.getFieldError();
return ResponseVO.error(ResponseConstant.ERROR_CODE, null == error ? ResponseConstant.ERROR_MESSAGE : error.getDefaultMessage(), "");
}
//捕获器3
@ExceptionHandler(value = {BindException.class})
public ResponseVO<String> handleBindException(BindException ex) {
if (log.isErrorEnabled()) {
log.error(ex.getMessage(), ex);
}
BindingResult result = ex.getBindingResult();
FieldError error = result.getFieldError();
return ResponseVO.error(ResponseConstant.ERROR_CODE, null == error ? ResponseConstant.ERROR_MESSAGE : error.getDefaultMessage(), "");
}
//捕获器4
@ExceptionHandler(value = {ConstraintViolationException.class})
public ResponseVO<String> handleConstraintViolationException(ConstraintViolationException ex) {
if (log.isErrorEnabled()) {
log.error(ex.getMessage(), ex);
}
Optional<ConstraintViolation<?>> first = ex.getConstraintViolations().stream().findFirst();
return ResponseVO.error(ResponseConstant.ERROR_CODE, first.isPresent() ? first.get().getMessage() : ResponseConstant.ERROR_MESSAGE, "");
}
//其他所有异常捕获器
@ExceptionHandler(Exception.class)
public ResponseVO<String> otherErrorDispose(Exception e) {
// 打印错误日志
log.error("错误代码({}),错误信息({})", ResponseConstant.ERROR_CODE, e.getMessage());
e.printStackTrace();
return ResponseVO.error(ResponseConstant.ERROR_CODE, ResponseConstant.ERROR_MESSAGE, e.getMessage());
}
}
自定义异常返回和自定义常量
//自定义接口响应类
@Data
public class ResponseVO<T> implements Serializable {
// 状态码: 0-成功,其他-失败
private final Integer code;
// 返回信息
private final String message;
//返回值
private final T data;
//是否成功
private final Boolean success;
// 成功返回
public static <T> ResponseVO<T> success(T data) {
return new ResponseVO<>(data);
}
// 失败返回
public static <T> ResponseVO<T> error(Integer code, String message, T data) {
return new ResponseVO<>(code, message, data);
}
public ResponseVO(T data) {
this.code = ResponseConstant.SUCCESS_CODE;
this.message = ResponseConstant.OK;
this.data = data;
this.success = true;
}
public ResponseVO(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.success = code == ResponseConstant.SUCCESS_CODE;
}
}
//自定义常量类
public class ResponseConstant {
public static final String OK = "OK";
public static final String ERROR = "error";
public static final int SUCCESS_CODE = 200;
public static final int ERROR_CODE = 500;
public static final String ERROR_MESSAGE = "操作失败!!";
}
这里我就不介绍使用方式了,网上有很多详细的案例,包括每个注解的作用介绍,分组校验和自定义校验的使用方法(话说我自己都没有用过,只用过简单的注解)。
注解不生效的情况有很多,主要参考的解决思路:jar包冲突、加错注解、少了关键注解。
(1)jar包冲突可能是引入的时候引入了多个版本的jar包,注意检查pom,在springboot中只需要安装上述方式引入即可,无需再引入其他validator相关jar包。
(2)加错注解,主要是看你引入的注解是不是在下述这个路径下的。
(3)少了关键注解
//@Validated
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("test1")
public ResponseVO<Integer> test2(@NotNull(message = "最小值不能为空") Integer minNum,
@NotNull(message = "最大值不能为空") @Min(value = 10,message = "参数必须大于10") Integer maxNum) {
return ResponseVO.success(11);
}
}
如上明明写了@NotNull,也确认了引入的注解是对的,但是就是不返回错误信息。像这种参数没有放在一个对象中,而是直接写在接口上的情况,需要在类上加@Validated注解,否则不会生效。
@Validated
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("test3")
public ResponseVO<Integer> test3(BlacklistPageParamVO vo) {
return ResponseVO.success(11);
}
@PostMapping("test4")
public ResponseVO<Integer> test4(@Valid @RequestBody BlacklistPageParamVO vo) {
return ResponseVO.success(11);
}
}
@Data
public class BlacklistPageParamVO{
@NotBlank(message = "日期不能为空")
private String date;
@NotBlank(message = "日期2不能为空")
private String date2;
@Valid
@NotNull(message = "内部对象不能为空")
private InnerVO innerVO;
}
@Data
public class InnerVO {
@NotNull(message = "num1不能为空")
private Integer num1;
@NotNull(message = "num2不能为空")
private Integer num2;
}
第二种情况就是,我的校验字段在一个对象里面,这个时候需要在对象前面加上@Valid注解,所以test3的检验不会生效。这里扩展一下如果一个对象中还有另一个对象,且内部的对象也有需要检验的字段,需要给这个内部对象也加上@Valid注解才会生效。
@Validated
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("test1")
public ResponseVO<Integer> test1(@NotNull(message = "最小值不能为空") @RequestParam Integer minNum,
@NotNull(message = "最大值不能为空") @RequestParam Integer maxNum) {
return ResponseVO.success(11);
}
}
像上面这种情况如果在参数前加了@RequestParam表示参数必传,可以理解为作用和@NotNull是一样的。此时调用接口时,忘记传参数minNum了,我期待返回的是“最小值不能为空”,但是实际上返回的是“Required Integer parameter ‘minNum’ is not present”,且控制台打印的错误日志如下:
从错误名称,我们就可以看出,这是由于加了@RequestParam注解导致,让你传,你不传,所以报了这个错。这个错误对应的就是我们上文中的捕获器1。而且由此可以看出MissingServletRequestParameterException异常是比validation的异常优先级高的。
MissingServletRequestParameterException
加了@RequestParam注解,但是接口调用时没有传指定的参数(注意:是没有传,而不是传了,但是值是null)。
MethodArgumentNotValidException
经过测试,当校验的参数放在对象中,接口的请求方式是post请求,用@Valid @RequestBody方式接受参数时,如果报错,会被该捕获器捕获。
BindException
经过测试,当校验参数写在类中,接口请求方式是get请求时,报错会被该捕获器捕获。
ConstraintViolationException
传了值,但是不符合要求。@NotNull(message = “最大值不能为空”) @Min(value = 10,message = “参数必须大于10”),要求传非null值,且值必须大于10,否则会返回错误信息。经过测试,当校验参数直接写在接口上,而不是写在类中,报错会被该捕获器捕获。
4个捕获器对应4种不同的场景。