Spring Boot(4)之 Validator 验证数据

2023-02-02 21:36:35

1、Hibernatr-Validator

1.1、简介

在 RESTful 的接口服务中,会有各种各样的入参,我们不可能完全不做任何校验就直接进入到业务处理的环节,通常我们会有一个基础的数据验证的机制,待这些验证过程完毕,结果无误后,参数才会进入到正式的业务处理中。而数据验证又分为两种,一种是无业务关联的规则性验证,一种是根据现有数据进行的联动性数据验证(简单来说,参数的合理性,需要查数据库)。而Hibernate-Validator则适合做无业务关联的规则性验证,而这类验证的代码大多是可复用的。

  • JSR 303和JSR 349

简单来说,就是Java规定了**一套关于验证器的接口**。开始的版本是Bean Validation 1.0(JSR-303),然后是Bean Validation 1.1(JSR-349),目前最新版本是Bean Validation 2.0(JSR-380),大概是这样。

从上可以看出Bean Validation并不是一项技术而是一种规范,需要对其实现。hibernate团队提供了参考实现,Hibernate validator 5是Bean Validation 1.1的实现,Hibernate Validator 6.0是Bean Validation 2.0规范的参考实现。新特性可以到官网查看,笔者最喜欢的两个特性是:跨参数验证(比如密码和确认密码的验证)和在消息中使用EL表达式,其他的还有如方法参数/返回值验证、CDI和依赖注入、分组转换等。对于方法参数/返回值验证,大家可以参阅《hibernate官方文档)》

如果项目的框架是spring boot的话,在依赖**spring-boot-starter-web中已经包含了Hibernate-validator**的依赖。Hibernate-Validator的主要使用的方式就是注解的形式,并且是“零配置”的,无需配置也可以使用。下面展示一个最简单的案例。

补充:《Validated数据校验,看这一篇就够了》

1.2、添加到Spring Boot

<!-- Spring Boot 新版本取消了 Hibernatr-Validator 的默认配置,所以需要手动添加 -->
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>

1.3、校验注解与异常捕获

  • 在需要校验的POJO前面加@Validated注解代表校验该参数。
  • @Validated:Spring提供的数据校验
  • @Valid:JSR303数据校验
  • 在需要校验的POJO后面加BindingResult参数,用来接收校验出错误时的提示信息。

@Validated注解和BindingResult参数必须配对使用,并且位置顺序固定。 如果要校验的参数有多个,入参写法:(@Validated Foo foo, BindingResult fooBindingResult, @Validated Bar bar, BindingResult barBindingResult);

1.4、常用注解

注意:

  1. 除了@Empty要求字符串不能全是空格,其他的字符串校验都是允许空格的。
  2. message是可以引用常量的,但是如@Size里max不允许引用对象常量,基本类型常量是可以的。
  3. 注意大部分规则校验都是允许参数为null,即当不存在这个值时,就不进行校验了。
注解 作用类型 释义
@CreditCardNumber 字符串 验证信用卡号码
@DateFromatCheckPatterm 字符串 验证日期格式是否满足正则表达式
@DateValidator 字符串 验证日期格式是否满足正则表达式
@Email 字符串 必须是邮箱格式
@Length(min=, max=) 字符串 长度范围
@NotBlank(message=) 字符串 不能为null,长度大于0
@NotEmpty 字符串 不能为null,长度大于0
@NotEmptyPatterm 字符串 不能为null,长度大于0,是否匹配正则表达式
@Pattern(regex=, flag=) 正则表达式 必须符合正则表达式
@Max(value) 数字,不支持小数 必须为数字,其值小于或等于指定的最大值
@Min(value) 数字,不支持小数 必须为数字,其值大于或等于指定的最小值
@DecimalMax(value) 数字 必须为数字,其值小于或等于指定的最大值
@DecimalMin(value) 数字 必须为数字,其值大于或等于指定的最小值
@Digits(integer, fraction) 数字 必须为数字,其值必须再可接受的范围内
@Range(min=, max=, message=) 数值类型、字符串、字节等 元素的大小范围
@Future 日期 必须是将来的日期
@Past 日期 必须是过去的日期
@Size(max=, min=) 任意 集合的长度
@NotNull 任意 不能为null
@Null 任意 必须为null
@AssertFalse 布尔值 必须为false
@AssertTrue 布尔值 必须为true
@ListStringPattern List 验证集合中的字符串是否满足正则表达式

2、案例

2.1、Controller中直接校验

package com.example.test1.controller;

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.Min;

@Validated
@RestController
@RequestMapping("Validator")
public class ValidatorController {

    /**
     * 1、id数不能小于10,@RequestParam类型的参数需要在Controller上增加@Validated
     * 2、BindingResult 用来捕获验证异常信息
     */
    @RequestMapping(value = "/Min")
    public String test(@Min(value = 10, message = "id最小只能是10") @RequestParam("id") Integer id, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return getValidateError(bindingResult);
        }
        return "输入参数符合要求:" + id.toString();
    }
}

2.2、分组校验、顺序校验、级联校验

同一个校验规则,不可能适用于所有的业务场景,对每一个业务场景去编写一个校验规则,又显得特别冗余。实际上我们可以用到Hibernate-Validator的分组功能。
使用分组能极大的复用需要验证的类信息。而不是按业务重复编写冗余的类。其中@GroupSequence提供组序列的形式进行顺序式校验,即先校验@Save分组的,如果校验不通过就不进行后续的校验多了。我认为顺序化的校验,场景更多的是在业务处理类,例如联动的属性验证,值的有效性很大程度上不能从代码的枚举或常量类中来校验。


2.2.1、案例

com/example/test1/bean/Update.java

package com.example.test1.bean;

public interface Update {
}

com/example/test1/bean/Save.java

package com.example.test1.bean;

public interface Save {
}

com/example/test1/bean/UserDto.java

package com.example.test1.bean;

import lombok.Data;

import javax.validation.GroupSequence;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.time.LocalDate;
import java.util.List;


/**
 * 注解@GroupSequence指定分组校验的顺序,即先校验Save分组的,如果不通过就不会去做后面分组的校验了
 */
@Data
@GroupSequence({Save.class, Update.class, UserDto.class})
public class UserDto {

    @NotEmpty(message = "缺少参数", groups = Update.class)
    private String id;

    @NotEmpty(message = "缺少参数", groups = Save.class)
    @Size(min = 1, max = 20, message = "用户名输入过长")
    @Pattern(regexp = "^[\\u4E00-\\u9FA5\\uf900-\\ufa2d·s]{2,50}$", message = "用户名输入错误")
    private String name;

    @NotNull(message = "缺少参数")
    @Min(message = "缺少参数", value = 0, groups = Save.class)
    private Integer age;

    @Pattern(message = "手机号输入格式错误", regexp = "^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$")
    private String phone;

    @Past(message = "出生不能早于当前时间")
    private LocalDate birthday;

    // 自定义验证,validation没有验证列表内容的注解,需要自己添加
    @EnumCheck(message = "只能选:1(男)或2(女)", enumClass = SexEnum.class)
    private Integer sex;

    /**
     * 级联校验只需要添加@Valid
     * 注解@ConvertGroup用于分组的转换,只能和@Valid一起使用。(一般用不到)
     */
    @Size(message = "列表数量超出范围", max = 50)
    @Valid
    //@ConvertGroup(from = Search.class, to = Update.class)
    private List<UserModel> list;
}

2.2.2、校验的接口

package com.example.test1.controller;

import com.example.test1.bean.Save;
import com.example.test1.bean.Update;
import com.example.test1.bean.UserDto;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.constraints.Min;
import javax.validation.groups.Default;
import java.util.HashMap;
import java.util.Map;

@Validated
@RestController
@RequestMapping("Validator")
public class ValidatorController {
    /**
     * 这里的@Validated({Save.class, Default.class}) 其中Default.class是校验注解默认的分组,
     * (也就说明自定义校验注解时要加上)
     */
    @RequestMapping(value = "/addUser")
    public String addUser(@Validated({Save.class, Default.class}) @RequestBody UserDto addDto, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return getValidateError(bindingResult);
        }
        return "添加成功:" + addDto.toString();
    }

    @PostMapping(value = "/updatedUser")
    public String updatedUser(@Validated({Update.class, Default.class}) @RequestBody UserDto updateDto, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return getValidateError(bindingResult);
        }
        return "修改成功:" + updateDto.toString();
    }

    /**
     * 校验错误信息方法可封装成工具类
     */
    static public String getValidateError(BindingResult bindingResult) {

        if (!bindingResult.hasErrors()) {
            return null;
        }

        Map<String, String> fieldErrors = new HashMap<>();

        for (FieldError error : bindingResult.getFieldErrors()) {
            fieldErrors.put(error.getField(), error.getCode() + " | " + error.getDefaultMessage());
        }

        HashMap<String, Object> result = new HashMap<>();
        result.put("fieldErrors", fieldErrors);

        return HttpStatus.UNPROCESSABLE_ENTITY.value() + "参数错误" + result;
    }
}

2.3、自定义校验注解(枚举)、组合校验注解

上面这些注解能适应我们绝大多数的验证场景,但是为了应对更多的可能性,我们可以自定义注解来满足验证的需求。

我们一定会用到这么一个业务场景,vo中的属性必须符合枚举类中的枚举。Hibernate-Validator中还没有关于枚举的验证规则,那么,我们则需要自定义一个枚举的验证注解。


2.3.1、编写注解

com/example/test1/bean/EnumCheck.java

package com.example.test1.bean;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 限定使用范围--方法、字段
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
// 表明注解的生命周期,它在运行代码时可以通过反射获取到注解
@Retention(RetentionPolicy.RUNTIME)
// validatedBy:以指定该注解的校验逻辑
@Constraint(validatedBy = EnumCheckValidator.class)
public @interface EnumCheck {
    // 是否必填 默认是必填的
    boolean required() default true;

    // 验证失败的消息
    String message() default "枚举的验证失败";

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

    // 错误验证的级别
    Class<? extends Payload>[] payload() default {};

    // 枚举的Class
    Class<? extends Enum<?>> enumClass();

    // 枚举中的验证方法
    String enumMethod() default "validation";
}

2.3.2、编写注解的校验类

com/example/test1/bean/EnumCheckValidator.java

package com.example.test1.bean;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Method;

public class EnumCheckValidator implements ConstraintValidator<EnumCheck, Object> {

    private EnumCheck enumCheck;

    /**
     * 初始化验证消息,可以得到配置的注解内容
     *
     * @param enumCheck 需要验证的数据
     */
    @Override
    public void initialize(EnumCheck enumCheck) {
        this.enumCheck = enumCheck;
    }

    /**
     * 执行验证方法,用来验证业务逻辑,需要继承 ConstraintValidator 接口
     *
     * @param value                      注解表明
     * @param constraintValidatorContext 继承 ConstraintValidator 接口
     * @return 验证结果
     */
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        // 注解表明为必选项 则不允许为空,否则可以为空
        if (value == null) {
            return !this.enumCheck.required();
        }

        Boolean result = Boolean.FALSE;
        Class<?> valueClass = value.getClass();
        try {
            // 通过反射执行枚举类中validation方法
            Method method = this.enumCheck.enumClass().getMethod(this.enumCheck.enumMethod(), valueClass);
            result = (Boolean) method.invoke(null, value);
            if (result == null) {
                return false;
            }
        } catch (Exception e) {
            System.out.println("custom EnumCheckValidator error\n" + e);
        }
        return result;
    }
}

2.3.3、编写枚举类

com/example/test1/bean/SexEnum.java

package com.example.test1.bean;

import java.util.Objects;

public enum SexEnum {
    MAN("男", 1), WOMAN("女", 2);

    private String label;
    private Integer value;

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public Integer getValue() {
        return value;
    }

    public void setValue(Integer value) {
        this.value = value;
    }

    SexEnum(String label, int value) {
        this.label = label;
        this.value = value;
    }

    /**
     * 判断值是否满足枚举中的value
     *
     * @param value 映射值
     * @return 判断结果:布尔值
     */
    public static boolean validation(Integer value) {
        for (SexEnum s : SexEnum.values()) {
            if (Objects.equals(s.getValue(), value)) {
                return true;
            }
        }
        return false;
    }
}

2.3.4、使用方法

// 自定义验证,validation没有验证列表内容的注解,需要自己添加
@EnumCheck(enumClass = SexEnum.class, message = "用户性別输入错误")
private Integer sex;

2.4、验证

2.4.1、正常情况

在这里插入图片描述

2.4.2、 缺少必填参数

在这里插入图片描述

2.4.3、缺少不同组的必填参数

核查优先级高的组:优先级高的组(Save),校验报错,优先级低的组(Update)将不再校验;

在这里插入图片描述

核查优先级低的组:优先级低的组(Update),校验报错,优先级高的组(Save)也校验;

在这里插入图片描述

2.4.4、字符串正则表达式校验失败

在这里插入图片描述

2.4.5、超出字数限制

在这里插入图片描述

2.4.6、日期校验

在这里插入图片描述

2.4.7、自定义校验

在这里插入图片描述

2.4.8、未分组多参数校验失败

在这里插入图片描述

  • 作者:憶
  • 原文链接:https://blog.csdn.net/weixin_43094965/article/details/122580705
    更新时间:2023-02-02 21:36:35