添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
贪玩的野马  ·  kkv图数据库·  8 月前    · 
腼腆的火柴  ·  张超·  10 月前    · 
眼睛小的牛腩  ·  炸裂!VSCode ...·  1 年前    · 
【Spring Boot系列】 如何优雅的进行参数校验?

【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之类参数,支持校验如下类型的参数:

  1. java.util.Date
  2. java.util.Calendar
  3. java.time.Instant
  4. java.time.LocalDate
  5. java.time.LocalDateTime
  6. java.time.LocalTime
  7. java.time.MonthDay
  8. java.time.OffsetDateTime
  9. java.time.OffsetTime
  10. java.time.Year
  11. java.time.YearMonth
  12. java.time.ZonedDateTime
  13. java.time.chrono.HijrahDate
  14. java.time.chrono.JapaneseDate
  15. java.time.chrono.MinguoDate
  16. java.time.chrono.ThaiBuddhistDate

如果一个参数的值是null,则表示有效。

@FutureOrPresent注解

用于校验表示现在或者未来的instant、date 或者time之类参数,支持校验如下类型的参数:

  1. java.util.Date
  2. java.util.Calendar
  3. java.time.Instant
  4. java.time.LocalDate
  5. java.time.LocalDateTime
  6. java.time.LocalTime
  7. java.time.MonthDay
  8. java.time.OffsetDateTime
  9. java.time.OffsetTime
  10. java.time.Year
  11. java.time.YearMonth
  12. java.time.ZonedDateTime
  13. java.time.chrono.HijrahDate
  14. java.time.chrono.JapaneseDate
  15. java.time.chrono.MinguoDate
  16. 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或者空集合)。

支持一下校验几种类型的参数:

  1. CharSequence:校验字符串长度。
  2. Collection:校验集合元素的数量。
  3. Map:校验key-value对的数量。
  4. Array:校验数组元素的数量。

@NotNull注解

用于校验一个参数的值不能是null,否则就会抛出错误信息。

支持任何类型。

@Null注解

用于校验一个参数的值必须是null,否则就会抛出错误信息。

支持任何类型。

@Past注解

用于校验表示过去的instant、date 或者time之类参数,支持校验如下类型的参数:

  1. java.util.Date
  2. java.util.Calendar
  3. java.time.Instant
  4. java.time.LocalDate
  5. java.time.LocalDateTime
  6. java.time.LocalTime
  7. java.time.MonthDay
  8. java.time.OffsetDateTime
  9. java.time.OffsetTime
  10. java.time.Year
  11. java.time.YearMonth
  12. java.time.ZonedDateTime
  13. java.time.chrono.HijrahDate
  14. java.time.chrono.JapaneseDate
  15. java.time.chrono.MinguoDate
  16. java.time.chrono.ThaiBuddhistDate

如果一个参数的值是null,则表示有效。

@PastOrPresent注解

用于校验表示现在或者过去的instant、date 或者time之类参数,支持校验如下类型的参数:

  1. java.util.Date
  2. java.util.Calendar
  3. java.time.Instant
  4. java.time.LocalDate
  5. java.time.LocalDateTime
  6. java.time.LocalTime
  7. java.time.MonthDay
  8. java.time.OffsetDateTime
  9. java.time.OffsetTime
  10. java.time.Year
  11. java.time.YearMonth
  12. java.time.ZonedDateTime
  13. java.time.chrono.HijrahDate
  14. java.time.chrono.JapaneseDate
  15. java.time.chrono.MinguoDate
  16. java.time.chrono.ThaiBuddhistDate

如果一个参数的值是null,则表示有效。

@Pattern注解

正则表达式校验。

被该注解标注的参数的值,必须符合该注解指定的正则表达式,否则就会抛出错误信息。

如果一个参数的值是null,则表示有效。

@Positive注解

正数校验。

用于校验BigDecimal、BigInteger、CharSequence、byte(Byte)、short(Short)、int(Integer)、long(Long)类型的参数。

被该注解标注的元素的值必须是正数(大于0),否则就会抛出错误信息。

如果一个参数的值是null,则表示有效。

@Size注解

被该注解标注的参数的元素大小必须在指定的边界(包括)之内。

支持一下校验几种类型的参数:

  1. CharSequence:校验字符串长度。
  2. Collection:校验集合元素的数量。
  3. Map:校验key-value对的数量。
  4. 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": ""
}

结果

{