前言
-
【校验参数在项目中是很常见的,在java中,几乎每个有入参的方法,在执行下一步操作之前,都要验证参数的合法性,比如是入参否为空,数据格式是否正确等等,往常的写法就是一大推的if-else,既不美观也不优雅,这个时候JCP组织站出来了,并且制定了一个标准来规范校验的操作,这个标准就是Java Validation API(JSR 303)。】 —摘自博客园 -
【Bean Validation是Java定义的一套基于注解的数据校验规范,目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成于2017.08)】— 摘自知乎
使用步骤
pom中添加依赖
以继承 spring-boot-starter-parent 为例
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>
另需加入依赖:
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
或者
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
因为hibernate-validator中也依赖了validation-api ( 高版本validation-api中包含了完善的核心注解,提供了与hibernate-validator相同功能的注解)
在低版本的spring-boot里可能需要两个都要引入,主要是低版本中的各有各的特色注解(比如在validation-api中在2.0版本才加入NotBlank这个注解),但是高版本中validation-api中的注解已经很完善;
hibernate-validator中的一些注解已经不建议使用了;
接收请求的参数添加注解
加入依赖后,我们就可以在我们的实体上加上对应的校验注解,例如:
package com.xx.log.common.pojo.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class TestDTO {
@NotBlank(message = "info 不能为空")
@NotNull(message = "info 不能为null")
private String info;
@NotBlank(message = "name 不能为空")
@NotNull(message = "name 不能为null")
private String name;
}
开启校验
开启校验是在我们controller 方法的参数前用 @Validated 或者 @Valid 注解
@GetMapping("/test")
public TestVO test(@Validated TestDTO testDTO) {
log.info("testDTO:{}", testDTO);
return TestVO.builder().code(0).build();
}
或者
@PostMapping("/test")
public TestVO test(@RequestBody @Validated TestDTO testDTO) {
log.info("testDTO:{}", testDTO);
return TestVO.builder().code(0).build();
}
源码分析
源码入口
参数校验是在调用controller方法前参数准备阶段里面,具体代码入口如下
org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest
↓
org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues
↓
org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#resolveArgument
↓
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument
↓
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#validateIfApplicable
我们看看 validateIfApplicable 这个方法
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
binder.validate(validationHints);
break;
}
}
}
从这个地方可以看出 @Validated和 @Valid 、以@Valid 开头的自定义注解都可以开启校验
binder代码还很深,包括它的创建和使用 binder创建是由 ServletRequestDataBinderFactory 类创建的ExtendedServletRequestDataBinder实例
public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory {
public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
@Nullable WebBindingInitializer initializer) {
super(binderMethods, initializer);
}
@Override
protected ServletRequestDataBinder createBinderInstance(
@Nullable Object target, String objectName, NativeWebRequest request) throws Exception {
return new ExtendedServletRequestDataBinder(target, objectName);
}
}
ExtendedServletRequestDataBinder的继承关系 接上面源码的调用栈的validateIfApplicable方法继续往下走
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#validateIfApplicable
↓
org.springframework.validation.DataBinder#validate(java.lang.Object...)
↓
org.springframework.boot.autoconfigure.validation.ValidatorAdapter#validate(java.lang.Object, org.springframework.validation.Errors)
↓
org.springframework.validation.beanvalidation.SpringValidatorAdapter#validate(java.lang.Object, org.springframework.validation.Errors)
最后一个方法是
@Override
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validate(target), errors);
}
}
- this.targetValidator 这个属性实际的对象是hibernate提供的校验类 org.hibernate.validator.internal.engine.ValidatorImpl
- processConstraintViolations 作用是将hibernate校验返回的结果统计成spring能够识别的结果,也就是将校验结果再封装一遍
注意点
- @NotBlank 不能用来校验数值类型,可以用来校验字符串
- 数值类型可以用以下注解
- @NotNull
- @Max
- @Min
- @DecimalMin
- @DecimalMax
转换校验失败异常提示
如果参数校验不通过,框架会抛出 MethodArgumentNotValidException,在调用方看来不是很友好,我们可以定义通用的该异常的处理类进行统一处理返回:
package com.xx.log.config;
import com.xx.log.common.pojo.vo.TestVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class ParameterCalibration {
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public TestVO handleMethodArgumentNotValidException(Exception exception) {
StringBuilder errorInfo = new StringBuilder();
BindingResult bindingResult = null;
if (exception instanceof MethodArgumentNotValidException) {
bindingResult = ((MethodArgumentNotValidException) exception).getBindingResult();
}
if (exception instanceof BindException) {
bindingResult = ((BindException) exception).getBindingResult();
}
for (int i = 0; i < bindingResult.getFieldErrors().size(); i++) {
if (i > 0) {
errorInfo.append(",");
}
FieldError fieldError = bindingResult.getFieldErrors().get(i);
errorInfo.append(fieldError.getField()).append(" :").append(fieldError.getDefaultMessage());
}
log.error(errorInfo.toString());
return TestVO.builder().msg(errorInfo.toString()).code(-1).build();
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public TestVO handleDefaultException(Exception exception) {
log.error(exception.toString());
return TestVO.builder().msg("服务器错误").code(-1).build();
}
}
异常统一处理原理可参考我 另一篇文章
自定义校验注解
自定义注解 【cnblogs 文章推荐】
over~~~
|