前言
参数校验在日常开发时是很常用的操作。为了避免null指针、非法数据存入数据库,我们通常要进行参数校验。
导包只需要spring-boot-starter-validation和web即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
测试DTO
public class TestDTO {
@NotNull(message = "姓名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Max(value = 120, message = "最大值不能大于120")
@Min(value = 0, message = "最小值不能低于0")
private Integer age;
测试
@RestController
@RequestMapping("/test")
public class TestController {
/**
* 测试
* `@Valid` 表示对这个对象校验
* `BindingResult` 获取的是校验的结果,这个对象有许多方法获取校验信息,可以自定义返回信息
*
*
* @param dto
* @param bindingResult
* @return
*/
@PostMapping("/post")
public Map<Object, Object> test(@Validated @RequestBody TestDTO dto, BindingResult bindingResult) {
Map<Object, Object> res = new HashMap<>();
if (bindingResult.hasErrors()) {
res.put("status", 400);
res.put("msg", bindingResult.getFieldError().getDefaultMessage());
return res;
} else {
res.put("status", 200);
res.put("msg", "ok");
res.put("data", dto);
return res;
}
}
}
自定义
1.创建自定义注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Constraint(validatedBy = FlagValidatorClass.class) // 绑定对应校验器
public @interface FlagValidator {
String[] value() default {};
String message() default "flag is not found";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2.编写对应校验器
/**
* 标志位校验器
*/
public class FlagValidatorClass implements ConstraintValidator<FlagValidator, Integer> {
private String[] values;
/**
* 初始化
*
* @param flagValidator 注解上设置的值
*/
@Override
public void initialize(FlagValidator flagValidator) {
this.values = flagValidator.value();
}
/**
* 校验
*
* @param value 被校验的值,即输入
* @param constraintValidatorContext 校验上下文
* @return 返回true证明校验通过,false校验失败
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
boolean isValid = false;
// 当value为null,校验失败
if (value == null) {
return false;
}
//遍历校验
for (int i = 0; i < values.length; i++) {
if (values[i].equals(String.valueOf(value))) {
isValid = true;
break;
}
}
return isValid;
}
}
这样子就完成了自定义参数校验。当然了,我们可以在isValid方法中添加更加复杂的校验逻辑,如正则匹配等方法,这里就不展开了。
测试一下,先在DTO中添加flag属性,标上注解。
@NotNull(message = "标识位不能为空")
@FlagValidator(value = {"0", "1"}, message = "标志位有误")
private Integer flag;
效果:
System.out.println(bindingResult);
// 成功时,这没什么好说的
org.springframework.validation.BeanPropertyBindingResult: 0 errors
// 失败时
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'testDTO' on field 'flag': rejected value [12];
codes [FlagValidator.testDTO.flag,FlagValidator.flag,FlagValidator.java.lang.Integer,FlagValidator];
arguments
[org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDTO.flag,flag];
arguments [];
default message [flag],[Ljava.lang.String;@1017351];
default message [标志位有误]
我们可以看到一些属性,Field、Default message、Rejected value。我们可以通过他的方法获取对应的值。获取Error,getFieldError默认返回第一个错误。getAllErrors返回错误列表。
获取对应的属性,构造我们自己的校验返回内容。通过Java8的stream方法我们可以简单的获取到报错信息并返回前端;
Object[] objects = bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).toArray();
优化
每次都要写bindingResult以及判断方法其实也挺烦人的,所以再写一个工具类,constraintViolations是一个Set集合,里面包含了错误的信息,我们可以自定义一个异常抛出,并定义全局异常处理器处理请求返回。
public class ValidatorUtils {
private static Validator validator;
static {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
/**
* 校验对象
*
* @param object 待校验对象
* @param groups 待校验的组
* @throws ApiException 校验不通过,则报ApiException异常
*/
public static void validateEntity(Object object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
constraintViolations.forEach(o -> {
throw new ApiException(400, o.getMessage());
});
}
}
}
修改TestController,可以看到简洁了许多。效果是一样的,就不演示了。
@RestController
@RequestMapping("/test")
public class TestController {
/**
* 测试
* @param dto
* @param bindingResult
* @return
*/
@PostMapping("/post")
public Map<Object, Object> test(@RequestBody TestDTO dto) {
ValidatorUtils.validateEntity(dto);
Map<Object, Object> res = new HashMap<>();
res.put("status", 200);
res.put("msg", "ok");
res.put("data", dto);
return res;
}
}
数据传递到spring中的执行过程大致为:前端通过http协议将数据传递到spring,spring通过HttpMessageConverter类将流数据转换成Map类型,然后通过ModelAttributeMethodProcessor类对参数进行绑定到方法对象中,并对带有@Valid或@Validated注解的参数进行参数校验,对参数进行处理和校验的方法为ModelAttributeMethodProcessor.resolveArgument(…),通过查看源码,当BindingResult中存在错误信息时,会抛出BindException异常,BindException实现了BindingResult接口(BindResult是绑定结果的通用接口, BindResult继承于Errors接口),所以该异常类拥有BindingResult所有的相关信息,因此我们可以通过捕获该异常类,对其错误结果进行分析和处理。这样,我们对是content-type类型为form(表单)类型的请求的参数校验的异常处理就解决了。
对于不同的传输数据的格式spring采用不同的HttpMessageConverter(http参数转换器)来进行处理.以下是对HttpMessageConverter进行简介:
HTTP 请求和响应的传输是字节流,意味着浏览器和服务器通过字节流进行通信。但是,使用 Spring,controller 类中的方法返回纯 String 类型或其他 Java 内建对象。如何将对象转换成字节流进行传输?
在报文到达SpringMVC和从SpringMVC出去,都存在一个字节流到java对象的转换问题。在SpringMVC中,它是由HttpMessageConverter来处理的。
当请求报文来到java中,它会被封装成为一个ServletInputStream的输入流,供我们读取报文。响应报文则是通过一个ServletOutputStream的输出流,来输出响应报文。
针对不同的数据格式,springmvc会采用不同的消息转换器进行处理,当使用json作为传输格式时,springmvc会采用MappingJacksonHttpMessageConverter消息转换器, 而且底层在对参数进行校验错误时,抛出的是MethodArgumentNotValidException异常,因此我们需要对BindException和MethodArgumentNotValidException进行统一异常管理。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 参数验证异常(前端json格式提交)
*
* @param e 异常
* @return 响应实体
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public String handleParameterException(MethodArgumentNotValidException e) {
log.error(e.getMessage(), e);
String errorMessage = CommonUtil.getErrorMessage(e.getBindingResult());
return errorMessage;
}
/**
* 参数验证异常(前端form表单格式提交)
*
* @param exception 异常
* @return http响应对象
*/
@ExceptionHandler(BindException.class)
public String handlerBindException(BindException exception) {
String errorMessage = CommonUtil.getErrorMessage(exception.getBindingResult());
return errorMessage;
}
}
常用注解
@Null 只能为null Object
@NotNull 不能为null Object
@NotEmpty [字符串、数组、集合、Map]不能为null&长度不为0 字符串、Object[]、基础类型数组、[Collection、Map]接口实现类
@NotBlank 不能为null&去除首尾空格后,不能为空字符串 字符串
@Max [字符串长度、数字]不能大于value 数值、数值格式的字符串
@Min [字符串长度、数字]不能小于value 数值、数值格式的字符串
@Size [字符串、数组、集合、Map]长度为min至max之间 字符串、Object[]、基础类型数组、[Collection、Map]接口实现类
@AssertFalse 只能为false boolean
@AssertTrue 只能为true boolean
@DecimalMax 不能大于等于value; inclusive为false时, 不能大于value 数值、数值格式的字符串
@DecimalMin 不能小于等于value; inclusive为false时, 不能小于value 数值、数值格式的字符串
@Digits 整数位数不能大于integer, 小数位数不能大于fraction 数值、数值格式的字符串
@Pattern 根据正则表达式(regexp)进行效验 字符串
@Negative 只能为负数 数值
@NegativeOrZero 只能为负数或0 数值
@Positive 只能为正数 数值
@PositiveOrZero 只能为正数或0 数值
@Future 只能是未来时间 时间
@FutureOrPresent 只能是未来时间或当前时间 时间
@Past 只能是过去时间 时间
@PastOrPresent 只能是过去时间或当前时间 时间
@Email 验证邮箱格式, 可以使用regex指定格式 字符串
@Length 字符串长度为min至max之间 字符串
@Range [字符串、数字]大小在min和max之间,字符串必须是数字格式 数值、数值格式的字符串