我们在对外提供接口的时候,为了提高安全性,我们需要在后端做数据的校验。实际上,Java 早在 2009 年就提出了 Bean Validation 规范,该规范定义的是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。并且已经历经 JSR303、JSR349、JSR380 三次标准的置顶,发展到了 2.0 。下面即将要介绍的是该数据验证的规范,以及相应的技术框架日常使用。
1、JSR规范提案
JSR:Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务,JSR已成为Java界的一个重要标准。
本文介绍的Bean Validation 就是出自JSR303,JSR349,以及JSR380 规范提案。该规范从JSR 303 发展到 JSR 380,目前最新规范是Bean Validation 2.0。
相信有小伙伴想去看下到底是个啥。规范提案地址:https://jcp.org/en/jsr/summary?id=bean+validation
需要注意的是,规范提案只是提供了规范,并没有提供具体的实现。具体实现框架有默认的javax.validation.api等。
2、JSR303定义的校验类型

3、@Valid和@Validated的区别
- @Valid注解是javax提供的,遵循标准 JSR-303 规范,所属包为: javax.validation.Valid 配合BindingResult可以直接提供参数验证结果。
- @Validated是@Valid的一次封装,是Spring提供的校验机制使用,遵循 Spring’s JSR-303 规范(是标准 JSR-303 的一个变种),所属包为: org.springframework.validation.annotation.Validated
@Validation对@Valid进行了二次封装,在基本使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明。
3.1、注解位置
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。
3.2、分组
先定义分组接口(接口什么都不需要,空的就可以):
public interface Insert { } public interface Update { }
在需要校验的bean上加上分组注解:
@NotBlank(groups = {Update.class}, message = "ID不能为空") private String id; @NotBlank(groups = {Insert.class, Update.class}, message = "名称不能为空") @Size(groups = {Insert.class, Update.class}, max = 32, message = "名称最大长度为32") private String name;
根据需要,在Controller处理请求中加入 @Validated 并引入需要校验的分组(未引入分组则都校验)
@PostMapping("/insert") public int insert(@RequestBody @Validated({Insert.class}) HospitalRequest request) { return hospitalService.insert(request); } @PostMapping("/update") public int update(@RequestBody @Validated({Update.class}) HospitalRequest request) { return hospitalService.update(request); }
在进行insert的时候不会对id进行校验
3.3、 嵌套验证
在比较两者嵌套验证时,先说明下什么叫做嵌套验证。比如我们现在有个实体叫做Item:
public class Item { @NotNull(message = "id不能为空") @Min(value = 1, message = "id必须为正整数") private Long id; @NotNull(message = "props不能为空") @Size(min = 1, message = "至少要有一个属性") private List<Prop> props; }
Item带有很多属性,属性里面有属性id,属性值id,属性名和属性值,如下所示:
public class Prop { @NotNull(message = "pid不能为空") @Min(value = 1, message = "pid必须为正整数") private Long pid; @NotNull(message = "vid不能为空") @Min(value = 1, message = "vid必须为正整数") private Long vid; @NotBlank(message = "pidName不能为空") private String pidName; @NotBlank(message = "vidName不能为空") private String vidName; }
属性这个实体也有自己的验证机制,比如属性和属性值id不能为空,属性名和属性值不能为空等。
现在我们有个ItemController接受一个Item的入参,想要对Item进行验证,如下所示:
@RestController public class ItemController { @RequestMapping("/item/add") public void addItem(@Validated Item item, BindingResult bindingResult) { doSomething(); } }
在上图中,如果Item实体的props属性不额外加注释,只有@NotNull和@Size,无论入参采用@Validated还是@Valid验证,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证,也就是@Validated和@Valid加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List中有Prop的pid为空或者是负数,入参验证不会检测出来。
为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
我们修改Item类如下所示:
public class Item { @NotNull(message = "id不能为空") @Min(value = 1, message = "id必须为正整数") private Long id; @Valid // 嵌套验证必须用@Valid @NotNull(message = "props不能为空") @Size(min = 1, message = "props至少要有一个自定义属性") private List<Prop> props; }
然后我们在ItemController的addItem函数上再使用@Validated或者@Valid,就能对Item的入参进行嵌套验证。此时Item里面的props如果含有Prop的相应字段为空的情况,Spring Validation框架就会检测出来,bindingResult就会记录相应的错误。
总结一下@Validated和@Valid在嵌套验证功能上的区别:
- @Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
- @Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
@Valid
保证了整个对象的验证, 但是它是对整个对象进行验证,当仅需要部分验证的时候就会出现问题。 这时候,可以使用@Validated
进行分组验证。
4、使用BindingResult接收校验结果信息
使用注解进行校验的时候,我们可以通过BindingResult来收集校验结果信息,具体操作如下:
Controller中,在@Valid或@Validated修饰的参数后跟上BindingResult参数(@Valid或@Validated 和 BindingResult 是一 一对应的,如果有多个@Valid或@Validated,那么每个@Valid或@Validated后面都需要添加BindingResult用于接收bean中的校验信息)
@PostMapping("/insert") public int insert(@RequestBody @Validated HospitalRequest request, BindingResult bindingResult) { if (bindingResult.hasErrors()) { List<String> collect = bindingResult.getFieldErrors().stream().map( DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toList()); StringBuilder errorMsg = new StringBuilder(); for (String s : collect) { errorMsg.append(s); errorMsg.append(","); } errorMsg = new StringBuilder(errorMsg.substring(0, errorMsg.length() - 1)); log.error("校验未通过:{}", errorMsg.toString()); Assert.state(Boolean.FALSE, errorMsg.toString()); } return hospitalService.insert(request); }
这样就可以接收到校验的结果信息,可以根据校验的结果信息进行一系列操作,如打印错误信息、抛出指定异常等。
5、统一异常处理
在日常开发中,我们可能需要让校验返回指定的信息或对象,这时我们就可以进行统一异常处理:
package com.app.config; import com.framework.common.domain.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; 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.RestControllerAdvice; import java.util.List; /** * 参数校验异常处理 */ @Slf4j @RestControllerAdvice public class BadRequestExceptionHandler { /** * 校验错误拦截处理 * * @param exception 错误信息集合 * @return ErrorResponse 错误响应,当HTTP响应状态码不为200时,使用该响应返回 */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) private ErrorResponse validateRequestException(MethodArgumentNotValidException exception) { BindingResult bindingResult = exception.getBindingResult(); StringBuilder errorMsg = new StringBuilder(); if (bindingResult.hasErrors()) { List<ObjectError> errors = bindingResult.getAllErrors(); for (ObjectError objectError : errors) { FieldError fieldError = (FieldError) objectError; if (log.isDebugEnabled()) { log.error("Data check failure : object: {},field: {},errorMessage: {}", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage()); } errorMsg.append(objectError.getDefaultMessage()); errorMsg.append(","); } errorMsg = new StringBuilder(errorMsg.substring(0, errorMsg.length() - 1)); } return new ErrorResponse("ILLEGAL_ARGUMENT_ERROR", errorMsg.toString()); } }
返回的自定义响应体如下:
package com.framework.common.domain; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 错误响应,当HTTP响应状态码不为200时,使用该响应返回 */ @JsonIgnoreProperties(ignoreUnknown = true) @AllArgsConstructor @NoArgsConstructor @Data public class ErrorResponse { /** * 错误码 */ private String code; /** * 错误信息 */ private String message; }
6、总结
最后,我们总结一下。
- 1)@validated 支持分组校验,@valid 不支持
- 2)@validated 和 @valid 使用的地方不一样
- 3)@Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上。能配合嵌套验证注解 @Valid 进行嵌套验证。
- 4)@Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上。能配合嵌套验证注解 @Valid 进行嵌套验证。
7、附
最近在做业务的时候发现注解@valid在接口参数是list的情况下不起作用。
然后去查了一下资料得以解决,总结如下两点:
解决方式:如下图
