前言
用注解@Validated、@Valid进行参数验证,相对于以前常用的if等条件,会显得简练很多,而且显得更加优雅。
然后是他俩的区别
@Validated:用在方法入参上无法单独提供嵌套验证功能,不能用在成员属性(字段)上,也无法提示框架进行嵌套验证,能配合嵌套验证注解@Valid进行嵌套验证。
@Valid:用在方法入参上无法单独提供嵌套验证功能,能够用在成员属性(字段)上,提示验证框架进行嵌套验证,能配合嵌套验证注解@Valid进行嵌套验证。
安装Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
常用的验证注解
@Null 被注释的元素必须为null
@NotNull CharSequence, Collection, Map 和 Array 对象不能是 null, 但可以是空集(size = 0)。
@NotEmpty CharSequence, Collection, Map 和 Array 对象不能是 null 并且相关对象的 size 大于 0。
@NotBlank String 不是 null 且去除两端空白字符后的长度(trimmed length)大于 0。
@AssertTrue 被注释的元素必须为true
@AssertFalse 被注释的元素必须为false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max,min) 被注释的元素的大小必须在指定的范围内。
@Digits(integer,fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式。
@Email 被注释的元素必须是电子邮件地址
@Length 被注释的字符串的大小必须在指定的范围内
@Range 被注释的元素必须在合适的范围内
全局异常捕获
ControllerAdvice用于捕获全局的控制器抛出的异常,这里只列出了验证相关的异常,其他Exception或者自定义的异常,都可以在这里捕获。
@RestControllerAdvice
@Slf4j
@Component
public class GlobalExceptionHandler {
/**
* 处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
*
* @param e e
* @return Result
*/
@ExceptionHandler(BindException.class)
@ResponseBody
public String bindExceptionHandler(BindException e) {
log.error("{}, 发生异常: {}", httpServletRequest.getRequestURI(), e);
String message = e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
return Result.build().errorJson(message);
}
/**
* 处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。
*
* @param httpServletRequest httpServletRequest
* @param e e
* @return Result
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public String exceptionHandler(HttpServletRequest httpServletRequest, MethodArgumentNotValidException e) {
log.error("{}, 发生异常: {}", httpServletRequest.getRequestURI(), e);
String errField = Objects.nonNull(e.getBindingResult().getFieldError()) ? ":" + e.getBindingResult().getFieldError().getField() : "";
Object rejectValue = e.getBindingResult().getFieldError().getRejectedValue();
return Result.build().errorJson(StrUtil.format("字段{}不允许值:{}", errField, rejectValue));
}
/**
* 处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException
*
* @param httpServletRequest httpServletRequest
* @param e e
* @return Result
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public String exceptionHandler(HttpServletRequest httpServletRequest, ConstraintViolationException e) {
log.error("{}, 发生异常: {}", httpServletRequest.getRequestURI(), e);
StringBuilder sb = new StringBuilder();
e.getConstraintViolations().forEach(constraintViolation -> {
sb.append(StrUtil.format("字段:{},不允许值:{},{} ", constraintViolation.getPropertyPath(),
constraintViolation.getInvalidValue(), constraintViolation.getMessage()));
});
return Result.build().errorJson(sb.toString().trim());
}
}
如何使用
比如新建这样一个类,给对应的字段添加注解以及设置message参数,如果不设置message,会使用默认的错误消息
public class User {
@NotEmpty(message = "请输入用户名")
private String username;
@NotEmpty
@Length(min=6,max=18,message = "密码在6位到18位之间")
private String password;
}
如果验证不符合会自动抛出异常,由上面的exceptionHandler进行捕获处理。
针对方法的普通变量参数进行验证时,@Validated需要加在控制器上,然后变量前加验证注解
这个如果自动抛出异常的话时ConstraintViolationException
@RestController
@RequestMapping("/file")
@Validated
public class FileController{
@DeleteMapping("/deleteFile")
public Result<?> deleteFile( @NotBlank String jobId,@NotBlank String filePath) throws IOException {
return jobService.deleteFile(jobId, filePath);
}
}
{
"code": "1",
"data": {},
"msg": "操作失败:字段:deleteFile.jobId,不允许值:null,不能为空 字段:deleteFile.filePath,不允许值:null,不能为空"
}
用Validated或Valid注解,验证一个实体类,也就是接收json数据,这个需要将注解标注于变量前面
这个如果自动抛出异常的话时MethodArgumentNotValidException
@PostMapping
public Result<?> add(@RequestBody @Validated JobDO jobDO) {
return Result.build(jobService.insertOrUpdate(jobDO)).success();
}
@PostMapping
public Result<?> add(@RequestBody @Valid JobDO jobDO) {
return Result.build(jobService.insertOrUpdate(jobDO)).success();
}
{
"code": "1",
"data": {},
"msg": "操作失败:字段:fileId,不允许值:null,不能为空"
}
嵌套校验
对象的嵌套校验,需要在嵌套的字段上添加@Valid注解,否则验证时会略过字段props所属类Item里字段的验证规则
public class SelectList {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@NotNull(message = "props不能为空")
@Size(min = 1, message = "至少要有一个属性")
@Valid // 嵌套验证必须用@Valid
private List<Item> props;
}
public class Item {
@NotBlank(message = "name不能为空")
private String name;
@NotBlank(message = "value不能为空")
private String value;
}
分组验证
Validation的分组验证,比如添加/编辑数据时,添加是不需要验证id的,而编辑的话id是必须的,还有可能这个类被不同控制器,方法使用时需要的参数不同,这时可以指定其groups参数指定要验证的规则。
//用于声明验证规则所属的分组
public interface EditSelectListGroup
{
}
public class SelectList {
//指定验证规则属于哪个分组
@NotNull(message = "id不能为空",groups = {EditSelectListGroup.class})
@Min(value = 1, message = "id必须为正整数" ,groups= {EditSelectListGroup.class})
private Long id;
@NotNull(message = "props不能为空")
@Size(min = 1, message = "至少要有一个属性")
private List<Item> props;
}
//指定使用EditSelectListGroup分组下的验证规则,如果不指定分组,那么SelectList的字段id上注解@NotNull和@Min会被忽略
//此处必须使用@Validated而不是@Valid,因为@Valid不支持分组条件
@PutMapping
public Result<?> edit(@RequestBody @Validated(value = {EditSelectListGroup.class}) JobDO jobDO) {
//xxxxxxxxxxxxxxxxxxxxxxx
}
数据传递到spring中的执行过程
前端通过HTTP协议将数据传递到Spring,Spring通过HttpMessageConverter类将流数据转换成Map类型,然后通过ModelAttributeMethodProcessor类对参数进行绑定到方法对象中,并对带有@Valid或@Validated注解的参数进行参数校验,对参数进行处理和校验的方法为ModelAttributeMethodProcessor.resolveArgument(…),通过查看源码,当BindingResult中存在错误信息时,会抛出BindException异常,BindException实现了BindingResult接口(BindResult是绑定结果的通用接口, BindResult继承于Errors接口),所以该异常类拥有BindingResult所有的相关信息,因此我们可以通过捕获该异常类,对其错误结果进行分析和处理。这样,我们对是Content-Type为application/x-www-form-urlencoded的请求(也就是表单),的参数校验的异常处理就解决了。
对于不同的传输数据的格式spring采用不同的HttpMessageConverter(http参数转换器)来进行处理,比如JasksonHttpMessageConverter,或者使用fastjson的话,可以自定义FastJsonHttpMessageConverter
HttpMessageConverter简介
HTTP 请求和响应的传输是字节流,意味着浏览器和服务器通过字节流进行通信。但是使用Spring MVC的Controller 类中的方法都是返回String类型或其他Java对象,如何将对象转换成字节流进行传输?这时就需要一个消息转化器。
在报文到达Spring MVC和从Spring MVC出去,都存在一个字节流到Java对象的转换问题。在Spring MVC中,它是由HttpMessageConverter来处理的。
当请求报文来到java中,它会被封装成为一个ServletInputStream的输入流,供我们读取报文。响应报文则是通过一个ServletOutputStream的输出流,来输出响应报文。
针对不同的数据格式,Spring MVC会采用不同的消息转换器进行处理,当使用json作为传输格式时,Spring MVC会采用MappingJacksonHttpMessageConverter消息转换器, 而且底层在对参数进行校验错误时,抛出的是MethodArgumentNotValidException异常,因此我们需要对BindException和MethodArgumentNotValidException进行统一异常管理,最终代码演示如上所示。