SpringBoot-Validation优雅实现参数校验

1、是什么?

它简化了 Java Bean Validation 的集成。Java Bean Validation 通过 JSR 380,也称为 Bean Validation 2.0,是一种标准化的方式,用于在 Java 应用程序中对对象的约束进行声明式验证。它允许开发人员使用注解来定义验证规则,并自动将规则应用于相应的字段或方法参数

为了我们方便地使用参数校验功能了

2、怎么玩?

(1) 首先导入相关依赖
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ly</groupId>
    <artifactId>springboot-validate</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.4</version>
    </parent>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>
(2) 在对应实体上加@Validated注解,开启校验规则
java 复制代码
package com.ly.valid.controller;

import com.ly.valid.common.R;
import com.ly.valid.common.ResultCodeEnum;
import com.ly.valid.entity.Person;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
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.RestController;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author ly (个人博客:https://www.cnblogs.com/ybbit)
 * @date 2023-12-11  21:01
 * @tags 喜欢就去努力的争取
 */
@RestController
public class TestController {

    /**
     * 保存
     */
    @PostMapping("/test1")
    public R save(@Validated @RequestBody Person person, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
            Map<String, String> map = new HashMap<>(fieldErrorList.size());
            fieldErrorList.forEach(item -> {
                String message = item.getDefaultMessage(); // 如果没有在相应的注解中加message,则会获取默认信息  例如:@NotBlank(message = "品牌名必须提交"),获取的message则为品牌名必须提交
                String field = item.getField(); // 获取哪个字段出现的问题
                map.put(field, message);
            });
            return R.fail(ResultCodeEnum.PARAM_ERROR, map);
        } else {
            // 伪代码
            // personService.save(person);
        }
        return R.success();
    }

    /**
     * 全局处理
     *
     * @param person
     * @return
     */
    @PostMapping("/test2")
    public R test(@Validated Person person) {
        return R.success(person);
    }

}
(3) 给对应的实体Bean添加校验注解,并自定义message提示
java 复制代码
package com.ly.valid.entity;

import com.ly.valid.constant.RegularConstant;
import jakarta.validation.constraints.*;
import lombok.Data;

import java.time.LocalDate;

/**
 * @author ly (个人博客:https://www.cnblogs.com/ybbit)
 * @date 2023-12-11  21:03
 * @tags 喜欢就去努力的争取
 */
@Data
public class Person {

    @NotBlank(message = "name 姓名不能为空")
    private String name;

    @NotNull(message = "age 年龄不能为空")
    @Min(value = 0, message = "年龄不能小于0")
    private Integer age;

    @NotNull(message = "gender 性别不能为空")
    private Integer gender;

    @Email(regexp = RegularConstant.EMAIL, message = "email 邮箱格式不正确")
    private String email;

    @Pattern(regexp = RegularConstant.PHONE, message = "phone 手机号格式不正确")
    private String phone;

    @Past(message = "birthday 生日日期有误")
    private LocalDate birthday;

}
(4) 不加@Validate注解,测试一下
(5) 开启校验功能@Validated:不使用自定义message会有默认提示信息
(6) 方式一: 我想自定义提示信息怎么整?简单,只需要给响应的校验bean后面紧跟着添加一个BindingResult,就可以获取到校验的结果了
java 复制代码
/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
            Map<String, String> map = new HashMap<>();
            fieldErrorList.forEach(item -> {
                String message = item.getDefaultMessage(); // 如果没有在相应的注解中加message,则会获取默认信息  例如:@NotBlank(message = "品牌名必须提交"),获取的message则为品牌名必须提交
                String field = item.getField(); // 获取哪个字段出现的问题
                map.put(field, message);
            });
            return R.error(400, "提交的数据不合法").put("data", map);
        } else {
            brandService.save(brand);
        }
        return R.ok();
    }
(7) 测试一下
(8) 方式二:如果有很多需要这样手动一个个处理,就显得很麻烦了;所以我们需要一个全局处理
java 复制代码
package com.ly.valid.common;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @author ly (个人博客:https://www.cnblogs.com/ybbit)
 * @date 2023-12-11  22:13
 * @tags 喜欢就去努力的争取
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理 form data方式调用接口校验失败抛出的异常 (对象参数)
     */
    @ExceptionHandler(BindException.class)
    public R error(BindException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> errorMessages = fieldErrors.stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .toList();
        return R.fail(ResultCodeEnum.PARAM_ERROR.getCode(), errorMessages.toString());
    }

    /**
     * 处理 json 请求体调用接口校验失败抛出的异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R error(MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> errorMessages = fieldErrors.stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .toList();
        return R.fail(ResultCodeEnum.PARAM_ERROR.getCode(), errorMessages.toString());
    }

    /**
     * 单个参数校验失败抛出的异常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public R error(ConstraintViolationException e) {
        String errorMsg = e.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessageTemplate)
                .collect(Collectors.joining());
        return R.fail(errorMsg);
    }
}
(9) 测试一下

3、相关注解信息

注解 数据类型 说明
@NotBlank CharSequence 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@NotEmpty CharSequence,Collection,Map,Arrays 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Length(min=下限, max=上限) CharSequence 验证注解的元素值长度在min和max区间内
@NotNull 所有类型 验证注解的元素值不是null
@Null 所有类型 验证注解的元素值是null
@Max(value=n) BigDecimal,BigInteger,byte,short,int,long和原始类型的相应包装。HV额外支持:CharSequence的任何子类型(评估字符序列表示的数字值),Number的任何子类型。 验证注解的元素值小于等于@Max指定的value值
@Min(value=n) BigDecimal,BigInteger,byte,short,int,long和原始类型的相应包装。HV额外支持:CharSequence的任何子类型(评估char序列表示的数值),Number的任何子类型。 验证注解的元素值大于等于@Min指定的value值
@Size(min=最小值, max=最大值) 字符串,集合,映射和数组。HV额外支持:CharSequence的任何子类型。 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Email CharSequence 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Pattern(regex=正则表达式, flag=) CharSequence 验证注解的元素值与指定的正则表达式匹配
@Range(min=最小值, max=最大值 CharSequence, Collection, Map and Arrays, BigDecimal, BigInteger, CharSequece, byte, short, int, long以及原始类型各自的包装 验证注解的元素值在最小值和最大值之间
@AssertFalse Boolean, boolean 验证注解的元素值是false
@AssertTrue Boolean, boolean 验证注解的元素值是true
@DecimalMax(value=n) BigDecimal,BigInteger,String,byte,short,int,long和原始类型的相应包装。HV额外支持:Number和CharSequence的任何子类型。 验证注解的元素值小于等于@ DecimalMax指定的value值
@DecimalMin(value=n) BigDecimal,BigInteger,String,byte,short,int,long和原始类型的相应包装。HV额外支持:Number和CharSequence的任何子类型。 验证注解的元素值小于等于@ DecimalMin指定的value值
@Digits(integer=整数位数, fraction=小数位数) BigDecimal,BigInteger,String,byte,short,int,long和原始类型的相应包装。HV额外支持:Number和CharSequence的任何子类型。 验证注解的元素值的整数位数和小数位数上限
@Future 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; Additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant 验证注解的元素值(日期类型)比当前时间晚
@Past 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; ,则由HV附加支持:ReadablePartial和ReadableInstant的任何实现。 验证注解的元素值(日期类型)比当前时间早
@Valid Any non-primitive type(引用类型) 验证关联的对象,如账户对象里有一个订单对象,指定验证订单对象

注:HV ---> Hibernate-Validator

4、分组校验(当我们新增和修改字段时,有可能校验规则是不一样的,那么该如何处理呢?)

(1) 定义分组信息(唯一即可)
java 复制代码
package com.ly.valid.group;

/**
 * @author ly (个人博客:https://www.cnblogs.com/ybbit)
 * @date 2023-12-11  23:10
 * @tags 喜欢就去努力的争取
 */
public interface AddGroup {
}

package com.ly.valid.group;

import jakarta.validation.groups.Default;

/**
 * @author ly (个人博客:https://www.cnblogs.com/ybbit)
 * @date 2023-12-11  23:10
 * @tags 喜欢就去努力的争取
 */
public interface UpdateGroup extends Default {
}
(2) 在分组参数后指定它的groups,需要指定在什么情况下需要进行校验,类型是一个接口数组
java 复制代码
package com.ly.valid.entity;

import com.ly.valid.constant.RegularConstant;
import com.ly.valid.group.AddGroup;
import com.ly.valid.group.UpdateGroup;
import jakarta.validation.constraints.*;
import jakarta.validation.groups.Default;
import lombok.Data;

import java.time.LocalDate;

/**
 * @author ly (个人博客:https://www.cnblogs.com/ybbit)
 * @date 2023-12-11  21:03
 * @tags 喜欢就去努力的争取
 */
@Data
public class Person {

    @NotBlank(message = "name 姓名不能为空", groups = AddGroup.class)
    private String name;

    @NotNull(message = "age 年龄不能为空", groups = UpdateGroup.class)
    @Min(value = 0, message = "年龄不能小于0")
    private Integer age;

    @NotNull(message = "gender 性别不能为空", groups = {AddGroup.class, UpdateGroup.class})
    private Integer gender;

    @NotBlank(message = "email 邮箱不能为空", groups = Default.class)
    @Email(regexp = RegularConstant.EMAIL, message = "email 邮箱格式不正确", groups = Default.class)
    private String email;

    @NotBlank(message = "phone 手机号不能为空", groups = UpdateGroup.class)
    @Pattern(regexp = RegularConstant.PHONE, message = "phone 手机号格式不正确")
    private String phone;

    @Past(message = "birthday 生日日期有误")
    private LocalDate birthday;
}
(3) 然后再我们的controller层参数前加上@Validated(value = {UpdateGroup.class})注解,指定它是哪一组
java 复制代码
/**
     * 测试添加分组
     *
     * @param person
     * @return
     */
    @PostMapping("/testAddGroup")
    public R testAddGroup(@Validated(AddGroup.class) Person person) {
        return R.success(person);
    }

    /**
     * 测试修改分组
     *
     * @param person
     * @return
     */
    @PostMapping("/testUpdateGroup")
    public R testUpdateGroup(@Validated(UpdateGroup.class) Person person) {
        return R.success(person);
    }

    /**
     * 测试添加和修改分组
     *
     * @param person
     * @return
     */
    @PostMapping("/testAddAndUpdateGroup")
    public R testAddAndUpdateGroup(@Validated({UpdateGroup.class, AddGroup.class}) Person person) {
        return R.success(person);
    }

    /**
     * 测试默认分组
     *
     * @param person
     * @return
     */
    @PostMapping("/testDefaultGroup")
    public R testDefaultGroup(@Validated(Default.class) Person person) {
        return R.success(person);
    }
(4) 测试AddGroup
(5) 测试UpdateGroup

注意:细心的同学发现了,为什么email邮箱是Default.class,修改的时候也触发了呢?原因如下图:

(6) 测试AddAndUpdateGroup
(7) 测试DefaultGroup

注意:默认没有指定分组的情况下@NotBlank,在分组校验的情况下@Validated(value = {AddGroup.class})不生效

5、自定义校验规则

例如:现在我有一个字段gender它的取值就三种0:保密 1:男 2:女 ,像这种有限个数的枚举值我们该如何去限制呢?这就要使用到的自定义校验注解

(1) 自定义校验注解
java 复制代码
package com.ly.valid.anno;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * @author ly (个人博客:https://www.cnblogs.com/ybbit)
 * @date 2023-12-12  0:27
 * @tags 喜欢就去努力的争取
 */
@Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {

    // 默认错误消息
    String message() default "{my.enum-value.err.msg}";
    // String message() default ENUM_VALUE_MESSAGE;

    String[] strValues() default {};

    int[] intValues() default {};

    // 分组
    Class<?>[] groups() default {};

    // 负载,可以增加自定义校验逻辑
    Class<? extends Payload>[] payload() default {};

    // 指定多个时使用
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        EnumValue[] value();
    }
}
(2) 自定义校验器
java 复制代码
package com.ly.valid.anno;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * 枚举值校验注解处理类
 *
 * @author ly (个人博客:https://www.cnblogs.com/ybbit)
 * @date 2023-12-12  0:34
 * @tags 喜欢就去努力的争取
 */
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {

    /**
     * 字符串
     */
    private final Set<String> strValueSet = new HashSet<>();

    /**
     * 数值
     */
    private final Set<Integer> intValueSet = new HashSet<>();

    @Override
    public void initialize(EnumValue constraintAnnotation) {
        strValueSet.addAll(Arrays.asList(constraintAnnotation.strValues()));
        for (int i : constraintAnnotation.intValues()) {
            intValueSet.add(i);
        }
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value instanceof Integer) {
            // 整数值类型
            return intValueSet.contains(value);
        } else if (value instanceof String) {
            // 字符串类型
            return strValueSet.contains(value);
        }
        return false;
    }
}
(3) 关联自定义的校验器和自定义的校验注解

注意:这里的ValidationMessages.properties是数据校验国际化配置文件;名字必须是这个

properties 复制代码
my.enum-value.err.msg=性别字段值只能为0、1、2
java 复制代码
@EnumValue(intValues = {0, 1, 2}, groups = AddGroup.class)
    @NotNull(message = "gender 性别不能为空", groups = {AddGroup.class, UpdateGroup.class})
    private Integer gender;
(4) 测试一下

6、paload() 负载的用法

java 复制代码
// TODO 未完待续