Spring Validation参数校验
Valid 和 Validated 的区别
区别 | Valid | Validated |
---|---|---|
提供者 | JSR-303规范 | Spring |
是否支持分组 | 不支持 | 支持 |
标注位置 | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE, METHOD, PARAMETER |
嵌套校验 | 支持 | 不支持 |
引入依赖
如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。
如果spring-boot版本大于2.3.x,则需要手动引入依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
预定义对象的说明
Result结果
接口统一返回Result格式的结果:
package com.qiankai.valid.common;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 返回结果
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 10:38
*/
@Data
@Accessors(chain = true)
public class Result<T> {
private int code;
private String message;
private T data;
public boolean ok() {
return this.code == 0;
}
public static <T> Result<T> success() {
return new Result<T>().setCode(0).setMessage("成功");
}
public static <T> Result<T> success(T data) {
return new Result<T>().setCode(0).setMessage("成功").setData(data);
}
public static <T> Result<T> failure() {
return new Result<T>().setCode(-1).setMessage("失败");
}
public static <T> Result<T> failure(int code, String msg) {
return new Result<T>().setCode(code).setMessage(msg);
}
public static <T> Result<T> failure(int code, String msg, T data) {
return new Result<T>().setCode(-1).setMessage("失败").setData(data);
}
}
ErrorCode
全局异常错误码,后面统一处理异常会用到:
一般为了更好的处理全局异常,使用的错误码都是定义成枚举类型(包含错误码和错误描述),我这边方便演示就随便定义了一个类
package com.qiankai.valid.common;
/**
* 错误码
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 11:08
*/
public final class ErrorCode {
/**
* 参数校验失败错误码
*/
public static final int ARGUMENT_VALID_FAILURE = -2;
}
常用参数校验
在DTO上添加注解,实现参数校验,假设存在 UserDTO 如下:
@Data
public class UserDTO {
private Long userId;
@NotNull
@Length(min = 2, max = 10)
private String userName;
@NotNull
@Length(min = 6, max = 20)
private String account;
@NotNull
@Length(min = 6, max = 20)
private String password;
}
RequestBody校验
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* RequestBody 参数校验
* 校验失败会抛出 MethodArgumentNotValidException 异常
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 10:37
*/
@RequestMapping("/api/user01")
@RestController
public class User01Controller {
/**
* RequestBody 参数校验
* 使用 @Valid 和 @Validated 都可以
*/
@PostMapping("/save/1")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
return Result.success();
}
@PostMapping("/save/2")
public Result save2User(@RequestBody @Valid UserDTO userDTO) {
return Result.success();
}
}
RequestParam / PathVariable 校验
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserDTO;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* RequestMapping / PathVariable 参数校验
* 校验失败会抛出 ConstraintViolationException 异常
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 10:57
*/
@RequestMapping("/api/user02")
@RestController
@Validated
public class User02Controller {
/**
* 此时必须在Controller上标注 @Validated 注解,并在入参上声明约束注解
*/
/**
* 路径变量
* 添加约束注解 @Min
*/
@GetMapping("{userId}")
public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
// 校验通过,才会执行业务逻辑处理
UserDTO userDTO = new UserDTO();
userDTO.setUserId(userId);
userDTO.setAccount("11111111111111111");
userDTO.setUserName("xixi");
userDTO.setAccount("11111111111111111");
return Result.success(userDTO);
}
/**
* 查询参数
* 添加约束注解 @Length @NotNull
*/
@GetMapping("getByAccount")
public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) {
// 校验通过,才会执行业务逻辑处理
UserDTO userDTO = new UserDTO();
userDTO.setUserId(10000000000000003L);
userDTO.setAccount(account);
userDTO.setUserName("xixi");
userDTO.setAccount("11111111111111111");
return Result.success(userDTO);
}
}
全局异常处理
上面如果校验失败,会抛出 MethodArgumentNotValidException
或者 ConstraintViolationException
异常。
在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。
比如我们系统要求无论发送什么异常,http的状态码必须返回200,由业务码去区分系统的异常情况。
package com.qiankai.valid.exception;
import com.qiankai.valid.common.ErrorCode;
import com.qiankai.valid.common.Result;
import org.springframework.http.HttpStatus;
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;
import javax.validation.ConstraintViolationException;
/**
* 统一异常处理
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 11:05
*/
@RestControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
}
String msg = sb.toString();
return Result.failure(ErrorCode.ARGUMENT_VALID_FAILURE, msg);
}
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleConstraintViolationException(ConstraintViolationException ex) {
return Result.failure(ErrorCode.ARGUMENT_VALID_FAILURE, ex.getMessage());
}
}
使用全局异常前
使用全局异常后
分组校验
有时候,为了区分业务场景,对于不同场景下的数据验证规则可能不一样(例如新增时可以不用传递 ID,而修改时必须传递ID),可以使用分组校验。
代码示例
DTO 如下:
在约束注解上声明适用的分组
package com.qiankai.valid.dto;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* 分组校验
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 11:12
*/
@Data
public class UserGroupValidDTO {
@NotNull(groups = Update.class)
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
/**
* 保存的时候校验分组
*/
public interface Save {
}
/**
* 更新的时候校验分组
*/
public interface Update {
}
}
Controller 如下:
Validated注解上指定校验的分组
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 分组校验
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 11:19
*/
@RestController
@RequestMapping("/api/user_group_valid")
public class UserGroupValidController {
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserGroupValidDTO.Save.class) UserGroupValidDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.success();
}
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserGroupValidDTO.Update.class) UserGroupValidDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.success();
}
}
嵌套校验
上面的校验主要是针对基本类型进行了校验,如果DTO中包含了自定义的实体类,就需要用到嵌套校验。
代码示例
DTO 如下:
package com.qiankai.valid.dto;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* 嵌套校验
* DTO中的某个字段也是一个对象,这种情况下,可以使用嵌套校验
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 12:21
*/
@Data
public class UserNestedValidDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
/**
* 此时DTO类的对应字段必须标记@Valid注解
*/
@Valid
@NotNull(groups = {Save.class, Update.class})
private Job job;
@Data
public static class Job {
@NotNull(groups = {Update.class})
@Min(value = 1, groups = Update.class)
private Long jobId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String jobName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String position;
}
/**
* 保存的时候校验分组
*/
public interface Save {
}
/**
* 更新的时候校验分组
*/
public interface Update {
}
}
Controller 如下:
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserNestedValidDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 嵌套校验
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 13:31
*/
@RestController
@RequestMapping("/api/user_nested_valid")
public class UserNestedValidController {
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserNestedValidDTO.Save.class) UserNestedValidDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.success();
}
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserNestedValidDTO.Update.class) UserNestedValidDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.success();
}
}
接口调用示例
根据DTO以及Controller中的校验规则,在update时,如果不传 jobId
嵌套校验就会报错,如下:
集合校验
如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。
此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:
代码示例
先自定义一个用于包装List的集合
package com.qiankai.valid.entity;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Delegate;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
/**
* 包装 List类型,并声明 @Valid 注解
* @param <E>
*/
@Getter
@Setter
public class ValidationList<E> implements List<E> {
@Delegate // @Delegate是lombok注解
@Valid // 一定要加@Valid注解
public List<E> list = new ArrayList<>();
// 一定要记得重写toString方法
@Override
public String toString() {
return list.toString();
}
}
@Delegate注解受lombok版本限制,1.18.6以上版本可支持。如果校验不通过,会抛出
NotReadablePropertyException
,同样可以使用统一异常进行处理。
Controller 如下:
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import com.qiankai.valid.entity.ValidationList;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 集合校验
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 14:14
*/
@RestController
@RequestMapping("/api/valid_list")
public class ValidListController {
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserGroupValidDTO.Save.class) ValidationList<UserGroupValidDTO> userList) {
// 校验通过,才会执行业务逻辑处理
return Result.success();
}
}
调用结果
编程式校验
上面都是通过注解来进行校验,也可以使用编程的方式进行校验:
Controller:
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.ConstraintViolation;
import java.util.Set;
/**
* 编程式校验参数
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 15:34
*/
@RequestMapping("/api/valid_with_code")
@RestController
public class ValidWithCodeController {
@Autowired
private javax.validation.Validator globalValidator;
/**
* 编程式校验
*/
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserGroupValidDTO userGroupValidDTO) {
Set<ConstraintViolation<UserGroupValidDTO>> validate = globalValidator.validate(userGroupValidDTO, UserGroupValidDTO.Save.class);
// 如果校验通过,validate为空;否则,validate包含未校验通过项
if (validate.isEmpty()) {
// 校验通过,才会执行业务逻辑处理
} else {
for (ConstraintViolation<UserGroupValidDTO> userGroupValidDTOConstraintViolation : validate) {
// 校验失败,做其它逻辑
System.out.println(userGroupValidDTOConstraintViolation);
// throw new RuntimeException();
}
}
return Result.success();
}
}
结果如下:
配置快速失败
Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。
/**
* 配置快速失败
*/
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}