目 录CONTENT

文章目录

SpringBoot-Validate优雅的实现参数校验

zhouzz
2024-10-06 / 0 评论 / 0 点赞 / 6 阅读 / 29824 字
温馨提示:
本文最后更新于 2024-10-06,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

1.SpringBoot-Validate是什么

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

2.引入依赖

springboot 版本是 2.7.18

<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>

3.参数验证

3.1 实体上加@Validated注解

@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;
}

3.2 开启校验规则

import com.zhouzz.validate.entity.Person;
import com.zhouzz.validate.model.*;
import org.springframework.validation.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import java.util.*;

@RestController
public class TestController {

    @PostMapping("/test1")
    public Result 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 -> {
                // 如果没有在相应的注解中加message,则会获取默认信息
                // 例如:@NotBlank(message = "品牌名必须提交"),获取的message则为品牌名必须提交
                String message = item.getDefaultMessage();
                // 获取哪个字段出现的问题
                String field = item.getField();
                map.put(field, message);
            });
            return Result.fail(BusinessErrorEnum.PARAM_VALIDATE_FAILED).putData(map);
        }
        // 执行正常逻辑
        System.out.println(person);
        return Result.success();
    }

    /**
     * 单个接口处理
     * @param person
     * @return
     */
    @PostMapping("/test2")
    public Result test2(@Validated @RequestBody Person person, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return Result.fail(BusinessErrorEnum.PARAM_VALIDATE_FAILED);
        }
        return Result.success(person);
    }

    /**
     * 全局处理(需要结合全局异常捕获处理)
     * @param person
     * @return
     */
    @PostMapping("/test3")
    public Result test3(@Validated @RequestBody Person person) {
        return Result.success(person);
    }
}

3.3 测试一下

1.方式一: 手写参数校验捕获

POST http://localhost:8080/test1
Content-Type: application/json
Accept: application/json
{
}

返回结果:

{
    "code": 600,
    "msg": "参数检验失败",
    "data": {
        "name": "name 姓名不能为空",
        "gender": "gender 性别不能为空",
        "age": "age 年龄不能为空"
    }
}

2.方式二:直接校验

POST http://localhost:8080/test2
Content-Type: application/json
Accept: application/json
{
}

返回结果:

{
    "code": 600,
    "msg": "参数检验失败",
    "data": null
}

很明显第二种提示信息太少了, 但是每个接口向方式一那样手动一个个处理,就显得很麻烦了。

方式三:定义一个全局处理处理参数校验

import com.zhouzz.validate.model.BusinessErrorEnum;
import com.zhouzz.validate.model.Result;
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 javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 全局异常捕获类
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

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

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

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

测试下

POST http://localhost:8080/test3
Content-Type: application/json
Accept: application/json
{
}

返回结果:

{
    "code": 600,
    "msg": "[name 姓名不能为空, age 年龄不能为空, gender 性别不能为空]",
    "data": null
}

4.相关注解信息

注解数据类型说明
@NotBlankCharSequence验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@NotEmptyCharSequence,Collection,Map,Arrays验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Length(min=下限, max=上限)CharSequence验证注解的元素值长度在min和max区间内
@NotNull所有类型验证注解的元素值不是null
@Null所有类型验证注解的元素值是null
@Max(value=n)Number的任何子类型验证注解的元素值小于等于@Max指定的value值
@Min(value=n)Number的任何子类型验证注解的元素值大于等于@Min指定的value值
@Size(min=最小值, max=最大值)字符串,集合,映射和数组验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@EmailCharSequence验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Pattern(regex=正则表达式, flag=)CharSequence验证注解的元素值与指定的正则表达式匹配
@Range(min=最小值, max=最大值CharSequence, Collection, Map and Arrays, BigDecimal, BigInteger, CharSequece, byte, short, int, long以及原始类型各自的包装验证注解的元素值在最小值和最大值之间
@AssertFalseBoolean, boolean验证注解的元素值是false
@AssertTrueBoolean, boolean验证注解的元素值是true
@DecimalMax(value=n)Number和CharSequence的任何子类型验证注解的元素值小于等于@ DecimalMax指定的value值
@DecimalMin(value=n)Number和CharSequence的任何子类型验证注解的元素值小于等于@ DecimalMin指定的value值
@Digits(integer=整数位数, fraction=小数位数)Number和CharSequence的任何子类型验证注解的元素值的整数位数和小数位数上限
@Future日期类型验证注解的元素值(日期类型)比当前时间晚
@Past日期类型验证注解的元素值(日期类型)比当前时间早
@ValidAny non-primitive type(引用类型)验证关联的对象,如账户对象里有一个订单对象,指定验证订单对象

5.分组校验

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

5.1 定义分组信息(唯一即可)

public interface AddGroup {
}
public interface UpdateGroup extends Default {
}

5.2 实体类上指定groups

在分组参数后指定它的groups,需要指定在什么情况下需要进行校验,类型是一个接口数组

@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;
}

5.3 控制器指定分组

然后再我们的controller层参数前加上@Validated(value = {UpdateGroup.class})注解,指定它是哪一组

import com.zhouzz.validate.entity.Person;
import com.zhouzz.validate.group.AddGroup;
import com.zhouzz.validate.group.UpdateGroup;
import com.zhouzz.validate.model.Result;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.groups.Default;

@RestController
public class TestGroupController {

    /**
     * 测试添加分组
     * @param person
     * @return
     */
    @PostMapping("/testAddGroup")
    public Result testAddGroup(@Validated(AddGroup.class) Person person) {
        return Result.success(person);
    }

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

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

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

1.测试AddGroup

POST http://localhost:8080/testAddGroup
Content-Type: application/json
Accept: application/json
{
}

返回结果:

{
    "code": 600,
    "msg": "[name 姓名不能为空, gender 性别不能为空]",
    "data": null
}

2.测试testUpdateGroup

POST http://localhost:8080/testUpdateGroup
Content-Type: application/json
Accept: application/json
{
}

返回结果:

{
    "code": 600,
    "msg": "[gender 性别不能为空, phone 手机号不能为空, age 年龄不能为空, email 邮箱不能为空]",
    "data": null
}

6.自定义校验规则

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

6.1 自定义校验注解

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;

@Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {

    // 默认错误消息
    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();
    }
}
public interface MessageConstant {
    String ENUM_VALUE_MESSAGE = "性别字段值只能为0、1、2";
}

6.2 自定义校验器

/**
 * 枚举值校验注解处理类
 */
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;
    }
}

6.3 关联自定义的校验器和自定义的校验注解

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

7.小结

0

评论区