【Spring Boot系列】 如何优雅的进行参数校验?
大家好,我是 @明人只说暗话 。
白嫖可耻。
欢迎点赞、评论、关注。
本文和大家聊聊后端开发中一个几乎不可避免的问题——参数校验。
不夸张的说,我们遇到的99.99%以上的接口都有参数,为了程序的健壮性,绝大多数情况下,我们都要对这些参数进行校验。
如下代码中存在大量参数校验的代码,和controller中的try-catch一样,也是非常丑陋的。因此,我们要考虑如何优化这些丑陋的参数校验代码,让程序整体看起来简洁明了。
package com.panda.paramter.validation.controller;
import com.panda.paramter.validation.vo.UserVo;
import common.core.Result;
import common.enums.GenderType;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
import java.util.regex.Pattern;
@RestController
@RequestMapping("user")
public class TestController {
private static final Pattern ID_CARD_PATTERN = Pattern.compile("(^\\d{15}$)|(^\\d{18}$)|(^\\d{17}(\\d|X|x)$)");
private static final Pattern MOBILE_PHONE_PATTERN = Pattern.compile("^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$");
@RequestMapping("saveUser")
public Result<Boolean> saveUser(UserVo user) {
if (StringUtils.isBlank(user.getUserName())) {
throw new IllegalArgumentException("用户姓名不能为空");
if (Objects.isNull(user.getGender())) {
throw new IllegalArgumentException("性别不能为空");
if (Objects.isNull(GenderType.getGenderType(user.getGender()))) {
throw new IllegalArgumentException("性别错误");
if (Objects.isNull(user.getAge())) {
throw new IllegalArgumentException("年龄不能为空");
if (user.getAge() < 0 || user.getAge() > 150) {
throw new IllegalArgumentException("年龄必须在0-150之间");
if (StringUtils.isBlank(user.getIdCard())) {
throw new IllegalArgumentException("身份证号不能为空");
if (!ID_CARD_PATTERN.matcher(user.getIdCard()).find()) {
throw new IllegalArgumentException("身份证号格式错误");
if (StringUtils.isBlank(user.getMobilePhone())) {
throw new IllegalArgumentException("手机号不能为空");
if (!MOBILE_PHONE_PATTERN.matcher(user.getIdCard()).find()) {
throw new IllegalArgumentException("手机号格式错误");
// 省略其他业务代码
return Result.success(true);
铺垫了那么多,现在进入正题。
本文为大家介绍Spring Boot 项目如何优雅的整合JSR-303进行参数校验,消除上面丑陋的if判断代码。
第一个问题,JSR-303是个什么玩意?
JSR-303
JSR 是 Java Specification Requests的简称,即Java规范提案,是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。
而JSR-303 则是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。
Bean Validation 为 JavaBean 验证定义了相应的元数据模型和API,它是一个运行时的数据验证框架,在验证之后,验证的错误信息马上就会被返回。
在我们的应用程序中,通过使用Bean Validation 或是自己定义的约束(constraint),例如 @NotNull、@Max等 , 就可以确保数据模型(JavaBean)的正确性,非常的方便。
需要注意的是,JSR-303虽然定义了Bean校验的标准,但并没有提供实现。而hibernate validation是对这个规范的实现(JSR-303声明了@Valid接口,而hibernate-validator对其进行了实现),并增加了校验注解,如@Email、@Length等等。
而Spring Validation则是对hibernate validation的二次封装,用来支持Spring MVC的参数自动校验。
所需依赖
当 Spring Boot 的版本小于2.3.x,spring-boot-starter-web会自动引入hibernate-validator依赖。当 Spring Boot 的版本大于2.3.x时,则需要手动引入hibernate-validator依赖。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
@ Valid和 @ Validated区别
在参数校验过程中,我们可能会使用到两个注解:Valid注解和Validated注解。
在这里介绍一下两者的区别和联系。
@Validated来自Spring Validation,是@Valid(javax.validation.Valid)的变种,支持分组验证等。
两者的区别如下:
@Valid注解是在使用Hibernate validation校验机制的时候使用的;而@Validated注解是在使用Spring Validator校验机制的时候使用的。
@Valid注解支持在方法、构造函数、方法参数和成员属性(字段)上使用,支持嵌套校验;而@Validated注解支持在类型、方法和方法参数上使用。但是不能用在成员属性上,也不支持嵌套校验;
@Validated注解提供了分组校验功能,可以在入参校验时,根据不同的分组采用不同的校验机制。而@Valid注解不支持分组校验功能。
@Valid注解支持嵌套校验;@Validated注解不支持嵌套校验。
快速失败(Fail Fast)
还有一点要注意,Spring Validation校验机制默认会校验完所有字段,然后才抛出异常。
但是,在某些情况下,我们希望出现一个校验错误就立马返回。
如果想要达成这种效果,需要通过配置开启 Fali Fast 机制,一旦校验失败就立即返回。
package com.panda.paramter.validation.config;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
@Component
public class ParameterValidationConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 开启 fail fast 机制
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
Bean Validation 内嵌的注解
下面,我们简单介绍一下Bean Validation有哪些注解,各自的作用又是怎么样的。
@AssertFalse注解
用于校验boolean或者Boolean类型的参数。
被该注解标注的元素必须为false,否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@AssertTrue注解
用于校验boolean或者Boolean类型的参数。
被该注解标注的元素必须为true,否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@DecimalMax注解
用于校验BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)类型的参数。
最大值校验。
被该注解标注的元素的值必须小于等于该注解指定的值,否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@DecimalMin注解
用于校验BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)类型的参数。
最小值校验。
被该注解标注的元素的值必须大于等于该注解指定的值,否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@Digits注解
用于校验BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)类型的参数。
被该注解标注的元素的类型必须是BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)之一,否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@Email注解
用于校验CharSequence类型的参数。
被该注解标注的元素必须是CharSequence类型,且必须是格式正确的电子邮件地址。
@Future注解
用于校验表示未来的instant、date 或者time之类参数,支持校验如下类型的参数:
- java.util.Date
- java.util.Calendar
- java.time.Instant
- java.time.LocalDate
- java.time.LocalDateTime
- java.time.LocalTime
- java.time.MonthDay
- java.time.OffsetDateTime
- java.time.OffsetTime
- java.time.Year
- java.time.YearMonth
- java.time.ZonedDateTime
- java.time.chrono.HijrahDate
- java.time.chrono.JapaneseDate
- java.time.chrono.MinguoDate
- java.time.chrono.ThaiBuddhistDate
如果一个参数的值是null,则表示有效。
@FutureOrPresent注解
用于校验表示现在或者未来的instant、date 或者time之类参数,支持校验如下类型的参数:
- java.util.Date
- java.util.Calendar
- java.time.Instant
- java.time.LocalDate
- java.time.LocalDateTime
- java.time.LocalTime
- java.time.MonthDay
- java.time.OffsetDateTime
- java.time.OffsetTime
- java.time.Year
- java.time.YearMonth
- java.time.ZonedDateTime
- java.time.chrono.HijrahDate
- java.time.chrono.JapaneseDate
- java.time.chrono.MinguoDate
- java.time.chrono.ThaiBuddhistDate
如果一个参数的值是null,则表示有效。
@Max注解
最大值校验。
用于校验BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)类型的参数。
被该注解标注的元素的值必须小于等于该注解指定的值,否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@Min注解
最小值校验。
用于校验BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)类型的参数。
被该注解标注的元素的值必须大于等于该注解指定的值,否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@Negative注解
负数校验。
用于校验BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)类型的参数。
被该注解标注的元素的值必须是负数(小于0),否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@NegativeOrZero注解
负数或0校验。
用于校验BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)类型的参数。
被该注解标注的元素的值必须是负数或者0,否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@NotBlank注解
用于校验非空字符串。
被该注解标注的元素的值必须包含至少一个非空白的字符,否则就会抛出错误信息。
@NotEmpty注解
用于校验字符串或者集合的元素数量不能为空(不能是null或者空集合)。
支持一下校验几种类型的参数:
- CharSequence:校验字符串长度。
- Collection:校验集合元素的数量。
- Map:校验key-value对的数量。
- Array:校验数组元素的数量。
@NotNull注解
用于校验一个参数的值不能是null,否则就会抛出错误信息。
支持任何类型。
@Null注解
用于校验一个参数的值必须是null,否则就会抛出错误信息。
支持任何类型。
@Past注解
用于校验表示过去的instant、date 或者time之类参数,支持校验如下类型的参数:
- java.util.Date
- java.util.Calendar
- java.time.Instant
- java.time.LocalDate
- java.time.LocalDateTime
- java.time.LocalTime
- java.time.MonthDay
- java.time.OffsetDateTime
- java.time.OffsetTime
- java.time.Year
- java.time.YearMonth
- java.time.ZonedDateTime
- java.time.chrono.HijrahDate
- java.time.chrono.JapaneseDate
- java.time.chrono.MinguoDate
- java.time.chrono.ThaiBuddhistDate
如果一个参数的值是null,则表示有效。
@PastOrPresent注解
用于校验表示现在或者过去的instant、date 或者time之类参数,支持校验如下类型的参数:
- java.util.Date
- java.util.Calendar
- java.time.Instant
- java.time.LocalDate
- java.time.LocalDateTime
- java.time.LocalTime
- java.time.MonthDay
- java.time.OffsetDateTime
- java.time.OffsetTime
- java.time.Year
- java.time.YearMonth
- java.time.ZonedDateTime
- java.time.chrono.HijrahDate
- java.time.chrono.JapaneseDate
- java.time.chrono.MinguoDate
- java.time.chrono.ThaiBuddhistDate
如果一个参数的值是null,则表示有效。
@Pattern注解
正则表达式校验。
被该注解标注的参数的值,必须符合该注解指定的正则表达式,否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@Positive注解
正数校验。
用于校验BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)类型的参数。
被该注解标注的元素的值必须是正数(大于0),否则就会抛出错误信息。
如果一个参数的值是null,则表示有效。
@Size注解
被该注解标注的参数的元素大小必须在指定的边界(包括)之内。
支持一下校验几种类型的参数:
- CharSequence:校验字符串长度。
- Collection:校验集合元素的数量。
- Map:校验key-value对的数量。
- Array:校验数组元素的数量。
Hibernate Validator内嵌的注解
@Length注解
验证字符串的长度是否在指定的区间之内。
@Range注解
用于校验数字或者字符串的长度是否在指定的区间之内。
如何接收校验结果
说了这么多,都是说怎么校验参数。
但是,还有一个问题——如果参数校验失败了,我们怎么接收校验的结果,并返回给前端呢?
下面我们就介绍两种常见的接收校验结果的方式,分别是通过BindingResult接收校验结果,和通过统一异常处理校验结果。
BindingResult接收校验结果
此种方式需要我们在Controller层的每个接口方法参数中指定BindingResult参数,Validator校验器会将校验的信息自动封装到其中。
类似如下代码,在代码示例中,我会详细介绍使用方法。
@PostMapping("updateUser")
public Result<Boolean> updateUser(@Validated @RequestBody UserVo user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return Result.fail(bindingResult.getAllErrors().stream().map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";")));
// 其他业务代码
return Result.success(true);
}
上述代码中的@Validated注解,换成@Valid注解也可以正常运行。
在使用BindingResult接收校验结果时,有一点需要注意:
@Validated注解标注的参数和BindingResult参数必须相邻,且BindingResult参数必须在@Validated注解后面,否则BindingResult的变量result不能接受错误信息 。
统一异常处理 校验结果
参数在校验失败的时候会抛出的MethodArgumentNotValidException、ConstraintViolationException或者BindException异常。
因此,我们可以在全局的异常处理器中捕捉到这三种异常,将参数校验失败的信息或者自定义信息返回给前端。
从这里可以看出,通过统一异常处理校验结果要比通过BindingResult接收校验结果要优雅的多。
我们只需要在项目中写一个统一异常处理逻辑就可以了,而不用繁琐的在每个每个接口方法参数中指定BindingResult参数。
以下代码示一个统一异常处理类的例子。
package com.panda.paramter.validation.handler;
import common.core.Result;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
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.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;
@RestControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
* BindException异常处理
* 当BindingResult中存在错误信息时,会抛出BindException异常。
@ExceptionHandler({BindException.class})
public Result<Object> handleBindExceptionException(BindException ex) {
BindingResult bindingResult = ex.getBindingResult();
return Result.fail(bindingResult.getAllErrors().stream().map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";")));
* MethodArgumentNotValidException异常处理
* MethodArgumentNotValidException异常校验 @RequestBody标注的JSON对象参数
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
return Result.fail(bindingResult.getAllErrors().stream().map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";")));
* ConstraintViolationException异常处理
* 单个参数异常处理
@ExceptionHandler({ConstraintViolationException.class})
public Result<Object> handleConstraintViolationException(ConstraintViolationException ex) {
return Result.fail(ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";")));
* 兜底异常处理
@ExceptionHandler({Throwable.class})
public Result<Object> handleConstraintViolationException(Throwable throwable) {
return Result.fail(throwable.getMessage());
理论知识讲完了,下面我们通过几个例子加深理解。
创建Spring Boot项目的过程就不在介绍了。
项目需要的依赖如下:
<dependencies>
<dependency>
<groupId>com.panda</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.18</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
快速失败需要的配置如下:
package com.panda.paramter.validation.config;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
@Component
public class ParameterValidationConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 开启 fail fast 机制
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
公共响应结果类如下:
package common.core;
import common.enums.ExceptionEnum;
public class Result<T> {
private String code;
private String msg;
private Boolean successFlag;
private T data;
public static <T> Result<T> success() {
Result<T> result = new Result<>();
result.successFlag(true);
return result;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.successFlag(true).data(data);
return result;
public static <T> Result<T> fail(String errorMsg) {
Result<T> result = new Result<>();
result.successFlag(false).msg(errorMsg);
return result;
public static <T> Result<T> fail(String code, String errorMsg) {
Result<T> result = new Result<>();
result.successFlag(false).code(code).msg(errorMsg);
return result;
public static <T> Result<T> fail(ExceptionEnum exceptionEnum) {
Result<T> result = new Result<>();
result.successFlag(false).code(exceptionEnum.getCode()).msg(exceptionEnum.getErrorMsg());
return result;
public Result<T> code(String code) {
this.code = code;
return this;
public Result<T> msg(String msg) {
this.msg = msg;
return this;
public Result<T> successFlag(Boolean successFlag) {
this.successFlag = successFlag;
return this;
public Result<T> data(T data) {
this.data = data;
return this;
public String getCode() {
return code;
public String getMsg() {
return msg;
public T getData() {
return data;
public Boolean getSuccessFlag() {
return successFlag;
异常枚举类如下:
package common.enums;
public enum ExceptionEnum {
* 请求错误!
BAD_REQUEST("400", "请求错误!"),
* 未经授权的请求!
UNAUTHORIZED("401", "未经授权的请求!"),
* 没有访问权限!
FORBIDDEN("403", "没有访问权限!"),
* 请求的资源未不到!
NOT_FOUND("404", "请求的资源未不到!"),
* 服务器内部错误!
INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
* 服务器正忙,请稍后再试!
BAD_GATEWAY("502", "服务器正忙,请稍后再试!"),
* 服务器正忙,请稍后再试!
SERVICE_UNAVAILABLE("503", "服务器正忙,请稍后再试!"),
* 网关超时!
GATEWAY_TIMEOUT("504", "网关超时!"),
* 非法参数异常!
ILLEGAL_ARGUMENT_ERROR("10000", "非法参数异常!"),
* 用户ID不能为空!
USER_ID_NOT_BLANK("10001", "用户ID不能为空!"),
UNKNOWN("9999", "未知异常!");
* 错误码
private final String code;
* 错误描述
private final String errorMsg;
ExceptionEnum(String code, String errorMsg) {
this.code = code;
this.errorMsg = errorMsg;
public String getCode() {
return code;
public String getErrorMsg() {
return errorMsg;
统一异常处理类如下:
package com.panda.paramter.validation.handler;
import common.core.Result;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
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.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;
@RestControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
* BindException异常处理
* 当BindingResult中存在错误信息时,会抛出BindException异常。
@ExceptionHandler({BindException.class})
public Result<Object> handleBindExceptionException(BindException ex) {
BindingResult bindingResult = ex.getBindingResult();
return Result.fail(bindingResult.getAllErrors().stream().map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";")));
* MethodArgumentNotValidException异常处理
* MethodArgumentNotValidException异常校验 @RequestBody标注的JSON对象参数
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
return Result.fail(bindingResult.getAllErrors().stream().map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(";")));
* ConstraintViolationException异常处理
* 单个参数异常处理
@ExceptionHandler({ConstraintViolationException.class})
public Result<Object> handleConstraintViolationException(ConstraintViolationException ex) {
return Result.fail(ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";")));
* 兜底异常处理
@ExceptionHandler({Throwable.class})
public Result<Object> handleConstraintViolationException(Throwable throwable) {
return Result.fail(throwable.getMessage());
用户实体类信息如下:
package com.panda.paramter.validation.vo;
import com.panda.paramter.validation.InsertUser;
import com.panda.paramter.validation.PrefixByAbc;
import com.panda.paramter.validation.UpdateUser;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.List;
@Data
public class UserVo {
@NotEmpty(message = "用户ID不能为空", groups = {UpdateUser.class})
private String userId;
@NotEmpty(message = "用户姓名不能为空", groups = {UpdateUser.class, InsertUser.class})
private String userName;
@NotNull(message = "性别不能为空", groups = {UpdateUser.class, InsertUser.class})
private Integer gender;
@NotEmpty(message = "身份证号不能为空", groups = {UpdateUser.class, InsertUser.class})
@Pattern(regexp = "(^\\d{15}$)|(^\\d{18}$)|(^\\d{17}(\\d|X|x)$)", message = "身份证号格式错误")
private String idCard;
@NotEmpty(message = "电话号码不能为空", groups = {UpdateUser.class, InsertUser.class})
@Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$", message = "电话号码格式错误")
private String mobilePhone;
@AssertTrue(message = "当前用户是无效用户", groups = {UpdateUser.class})
private Boolean valid;
@AssertFalse(message = "当前用户已被删除", groups = {UpdateUser.class})
private Boolean deleted;
@Min(message = "年龄必须大于0", value = 0, groups = {UpdateUser.class, InsertUser.class})
@Max(message = "年龄不能超过150", value = 150, groups = {UpdateUser.class, InsertUser.class})
private Integer age;
@NotNull(message = "用户详情不能为空", groups = {UpdateUser.class, InsertUser.class})
@Valid
private UserDetailVO userDetail;
@NotNull(message = "账号信息不能为空", groups = {UpdateUser.class, InsertUser.class})
@Valid
private List<AccountVO> accounts;
@PrefixByAbc
private String prefix;
用户明细实体类信息如下:
package com.panda.paramter.validation.vo;
import com.panda.paramter.validation.InsertUser;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
@Data
public class UserDetailVO {
@NotEmpty(message = "爱好不能为空", groups = {InsertUser.class})
private String hobby;
@NotEmpty(message = "头像不能为空", groups = {InsertUser.class})
private String headPortraitUrl;
账号实体类信息如下。
package com.panda.paramter.validation.vo;
import com.panda.paramter.validation.InsertUser;
import com.panda.paramter.validation.UpdateUser;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
@Data
public class AccountVO {
@NotEmpty(message = "身账号不能为空", groups = {UpdateUser.class, InsertUser.class})
private String account;
@NotEmpty(message = "密码不能为空", groups = {UpdateUser.class, InsertUser.class})
private String password;
@NotEmpty(message = "用户ID不能为空", groups = {UpdateUser.class, InsertUser.class})
private String userId;
注意,测试时,把fail fast配置注释掉!
代码示例:单个参数校验
TestController
package com.panda.paramter.validation.controller;
import common.core.Result;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
@RestController
@RequestMapping("user")
@Validated
public class TestController {
@GetMapping("getUserById")
public Result<Boolean> getUserById(@NotBlank(message = "用户ID不能为空") String userId,
@NotBlank(message = "用户名称不能为空") String userName) {
// 省略业务其他业务逻辑代码
return Result.success(true);
}
测试
调用/user/getUserById方法,但是不传入参数,模拟参数值为空的情况。
结果
{
"code": null,
"msg": "用户ID不能为空;用户名称不能为空",
"successFlag": false,
"data": null
}
结果如上所示,可以捕获校验失败的参数抛出的错误信息。
其实是通过统一异常处理类的handleConstraintViolationException方法。
注意点
在进行单个参数校验时,一定要在Controler类上加@Validated注解,否则校验不会生效。即使添加@Valid注解也无效 。
代码示例:对象参数校验
TestController
package com.panda.paramter.validation.controller;
import com.panda.paramter.validation.vo.UserVo;
import common.core.Result;
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;
@RestController
@RequestMapping("user")
public class TestController {
@PostMapping(value = "saveUser")
public Result<Boolean> saveUser(@Validated @RequestBody UserVo user) {
return Result.success(true);
测试
调用/user/saveUser方法,但是不传入参数,模拟参数值为空的情况。
结果
{
"code": null,
"msg": "电话号码不能为空;用户姓名不能为空;身份证号不能为空;用户ID不能为空",
"successFlag": false,
"data": null
}
结果如上所示,可以捕获校验失败的参数抛出的错误信息。
其实是通过统一异常处理类的handleMethodArgumentNotValidException方法。
代码示例:分组参数校验
当参数的校验规则存在多种情况时。
例如,保存User的时候,userId是可以为空的(一般是后端生成,或者数据库表自增主键)。但是,在更新User的时候,userId的值则不能为空。
遇到这种场景时,我们一般选择分组参数校验。
TestController
package com.panda.paramter.validation.controller;
import com.panda.paramter.validation.InsertUser;
import com.panda.paramter.validation.vo.UserVo;
import common.core.Result;
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;
@RestController
@RequestMapping("user")
public class TestController {
@PostMapping(value = "saveUser")
public Result<Boolean> saveUser(@Validated(value = InsertUser.class) @RequestBody UserVo user) {
return Result.success(true);
注意!
我们在@Validated主街上指定校验参数时,使用的分组InsertUser。
测试
调用/user/saveUser方法,参数如下。
{
"age": 11111
}
结果
{
"code": null,
"msg": "年龄不能超过150;电话号码不能为空;用户姓名不能为空;性别不能为空;身份证号不能为空",
"successFlag": false,
"data": null
}
结果如上所示,同样可以捕获校验失败的参数抛出的错误信息。
也是通过统一异常处理类的handleMethodArgumentNotValidException方法。
代码示例:嵌套( 级联 )参数校验
嵌套参数校验用在一个实体是另一个实体的属性的场景下。
TestController
package com.panda.paramter.validation.controller;
import com.panda.paramter.validation.InsertUser;
import com.panda.paramter.validation.vo.UserVo;
import common.core.Result;
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;
@RestController
@RequestMapping("user")
public class TestController {
@PostMapping(value = "saveUser")
public Result<Boolean> saveUser(@Validated(value = InsertUser.class) @RequestBody UserVo user) {
return Result.success(true);
注意!
我们在@Validated主街上指定校验参数时,使用的分组InsertUser。
测试
调用/user/saveUser方法,参数如下。
{
"userId": "",
"userName": "",
"gender": 1,
"idCard": "",
"mobilePhone": "",
"valid": true,
"deleted": true,
"age": 1,
"userDetail": {
"hobby": "",
"headPortraitUrl": ""
"accounts": [
}
结果
{
"code": null,
"msg": "电话号码不能为空;头像不能为空;身份证号不能为空;用户姓名不能为空;爱好不能为空",
"successFlag": false,
"data": null
}
结果如上所示,同样可以捕获校验失败的参数抛出的错误信息。
也是通过统一异常处理类的handleMethodArgumentNotValidException方法。
注意点
想要嵌套校验有效果,必须在被校验的嵌套属性上加上@Valid注解(javax.validation.Valid),如下代码所示。
@NotNull(message = "用户详情不能为空", groups = {UpdateUser.class, InsertUser.class})
@Valid
private UserDetailVO userDetail;
另外还需要传指定的嵌套参数(本例中是hobby和headPortraitUrl)。
也就是说,如果我在请求的时候,不传userDetail参数,是不会校验嵌套参数(本例中是hobby和headPortraitUrl)的。
哪怕传个空,都可以生效。
代码示例: 集合 参数校验
在批量保存、批量更新等场景中,我们期望对集合中的每一项都进行参数校验。
此时,如果我们直接使用List或者Set等来接收数据,参数校验并不会生效!要使参数生效,我们需要重新实现List接口,并且在实现类里声明一个List类型变量,并且用@Valid注解标注,以此来接收参数。
TestController
package com.panda.paramter.validation.controller;
import com.panda.paramter.validation.InsertUser;
import com.panda.paramter.validation.ValidatedList;
import com.panda.paramter.validation.vo.UserVo;
import common.core.Result;
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;
@RestController
@RequestMapping("user")
public class TestController {
@PostMapping(value = "saveUser")
public Result<Boolean> saveUser(@Validated(InsertUser.class) @RequestBody ValidatedList<UserVo> users) {
return Result.success(true);
}
测试
调用/user/saveUser方法,参数如下。
[
"userId": "",
"userName": "",
"gender": 1,
"idCard": "",
"mobilePhone": "",
"valid": true,
"deleted": true,
"age": 1,
"userDetail": {
"hobby": "",
"headPortraitUrl": ""
"accounts": [
"account": "",
"password": "",
"userId": ""
"prefix": ""
]
结果
{
"code": null,
"msg": "身份证号不能为空;用户姓名不能为空;身账号不能为空;电话号码不能为空;用户ID不能为空;密码不能为空;
爱好不能为空;头像不能为空",
"successFlag": false,
"data": null
}
如上所示,集合参数校验生效了。
代码示例: 自定义参数校验
如上以上参数校验方法无法满足业务需求,我们还有大招——自定义参数校验。
自定义参数校验非常简单,只需要两步。
假设,我们需要一个校验某个字段必须以Abc为前缀。
第一步、自定义约束注解
package com.panda.paramter.validation;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = {PrefixByAbcValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrefixByAbc {
String message() default "必须以Abc开头";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
第二步、实现ConstraintValidator接口,编写约束校验器
package com.panda.paramter.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class PrefixByAbcValidator implements ConstraintValidator<PrefixByAbc, String> {
private static final String PREFIX = "Abc";
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value != null) {
return value.startsWith(PREFIX);
return true;
}
TestController
package com.panda.paramter.validation.controller;
import com.panda.paramter.validation.vo.UserVo;
import common.core.Result;
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;
@RestController
@RequestMapping("user")
public class TestController {
@PostMapping(value = "saveUser")
public Result<Boolean> saveUser(@Validated @RequestBody UserVo user) {
return Result.success(true);
测试
调用/user/saveUser方法,参数如下。
{
"userId": "userId_nzn5o",
"userName": "userName_vydjd",
"gender": 1,
"idCard": "idCard_wkpbc",
"mobilePhone": "mobilePhone_qyrer",
"valid": true,
"deleted": true,
"age": 1,
"userDetail": {
"hobby": "hobby_s8nh6",
"headPortraitUrl": "headPortraitUrl_cbemq"
"accounts": [
"account": "account_2goty",
"password": "password_3sxyw",
"userId": "userId_h4zaf"
"prefix": "prefix"
}
结果
{
"code": null,
"msg": "身份证号格式错误;必须以Abc开头;电话号码格式错误",
"successFlag": false,
"data": null
}
从上面的结果可以看出,我们自定义的约束校验器生效了。
编程式参数校验
上面都是讲述的参数校验都是通过注解的方式,如果在某种情况下,我们没法通过注解的方式实现参数校验,怎么办?
那就只能受累通过编程的方式进行参数校验了。
TestController
package com.panda.paramter.validation.controller;
import com.panda.paramter.validation.InsertUser;
import com.panda.paramter.validation.vo.UserVo;
import common.core.Result;
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 javax.validation.Validator;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
@RequestMapping("user")
public class TestController {
@Autowired
private Validator globalValidator;
@PostMapping(value = "saveUser")
public Result<Boolean> saveUser(@RequestBody UserVo user) {
Set<ConstraintViolation<UserVo>> validate = globalValidator.validate(user, InsertUser.class);
// 如果validate不为空,说明包含未校验通过的参数
if (!validate.isEmpty()) {
throw new IllegalArgumentException(validate.stream().map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";")));
return Result.success(true);
注意,我们去掉了@Validated注解!
测试
调用/user/saveUser方法,参数如下。
{
"userId": "",
"userName": "",
"gender": 1,
"idCard": "",
"mobilePhone": "",
"valid": true,
"deleted": true,
"age": 1,
"userDetail": {
"hobby": "",
"headPortraitUrl": ""
"accounts": [
"account": "",
"password": "",
"userId": ""
"prefix": ""
}
结果
{