分组序列@GroupSequenceProvider、@GroupSequence控制数据校验顺序,解决多字段联合逻辑校验问题【享学Spring MVC】(上)

简介: 分组序列@GroupSequenceProvider、@GroupSequence控制数据校验顺序,解决多字段联合逻辑校验问题【享学Spring MVC】(上)

前言


本以为洋洋洒洒的把Java/Spring数据(绑定)校验这块说了这么多,基本已经算完结了。但今天中午一位热心小伙伴在使用Bean Validation做数据校验时上遇到了一个稍显特殊的case,由于此校验场景也比较常见,因此便有了本文对数据校验补充。


关于Java/Spring中的数据校验,我有理由坚信你肯定遇到过这样的场景需求:在对JavaBean进行校验时,b属性的校验逻辑是依赖于a属性的值的;换个具象的例子说:当且仅当属性a的值=xxx时,属性b的校验逻辑才生效。这也就是我们常说的多字段联合校验逻辑~

因为这个校验的case比较常见,因此促使了我记录本文的动力,因为它会变得有意义和有价值。当然对此问题有的小伙伴说可以自己用if else来处理呀,也不是很麻烦。本文的目的还是希望对数据校验一以贯之的做到更清爽、更优雅、更好扩展而努力。


需要有一点坚持:既然用了Bean Validation去简化校验,那就(最好)不要用得四不像,遇到问题就解决问题~

热心网友问题描述


为了更真实的还原问题场景,我贴上聊天截图如下:


image.png


待校验的请求JavaBean如下:


image.png


校需求描述简述如下:


image.png


这位网友描述的真实生产场景问题,这也是本文讲解的内容所在。

虽然这是在Spring MVC条件的下使用的数据校验,但按照我的习惯为了更方便的说明问题,我会把此部分功能单摘出来,说清楚了方案和原理,再去实施解决问题本身(文末)~


方案和原理


对于单字段的校验、级联属性校验等,通过阅读我的系列文章,我有理由相信小伙伴们都能驾轻就熟了的。本文给出一个最简单的例子简单"复习"一下:


@Getter
@Setter
@ToString
public class Person {
    @NotNull
    private String name;
    @NotNull
    @Range(min = 10, max = 40)
    private Integer age;
  @NotNull
    @Size(min = 3, max = 5)
    private List<String> hobbies;
    // 级联校验
    @Valid
    @NotNull
    private Child child;
}


测试:


public static void main(String[] args)  {
    Person person = new Person();
    person.setName("fsx");
    person.setAge(5);
  person.setHobbies(Arrays.asList("足球","篮球"));
    person.setChild(new Child());
    Set<ConstraintViolation<Person>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(person);
    // 对结果进行遍历输出
    result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}

运行,打印输出:

child.name 不能为null: null
age 需要在10和40之间: 5
hobbies 个数必须在3和5之间: [足球,篮球]


结果符合预期,(级联)校验生效。


通过使用@Valid可以实现递归验证,因此可以标注在List上,对它里面的每个对象都执行校验


问题来了,针对上例,现在我有如下需求:


  1. 若20 <= age < 30,那么hobbies的size需介于1和2之间
  2. 若30 <= age < 40,那么hobbies的size需介于3和5之间
  3. age其余值,hobbies无校验逻辑


实现方案


Hibernate Validator提供了非标准的@GroupSequenceProvider注解。本功能提供根据当前对象实例的状态,动态来决定加载那些校验组进入默认校验组。


为了实现上面的需求达到目的,我们需要借助Hibernate Validation提供给我们的DefaultGroupSequenceProvider接口来处理。


// 该接口定义了:动态Group序列的协定
// 要想它生效,需要在T上标注@GroupSequenceProvider注解并且指定此类为处理类
// 如果`Default`组对T进行验证,则实际验证的实例将传递给此类以确定默认组序列(这句话特别重要  下面用例子解释)
public interface DefaultGroupSequenceProvider<T> {
  // 合格方法是给T返回默认的组(多个)。因为默认的组是Default嘛~~~通过它可以自定指定
  // 入参T object允许在验证值状态的函数中动态组合默认组序列。(非常强大)
  // object是待校验的Bean。它可以为null哦~(Validator#validateValue的时候可以为null)
  // 返回值表示默认组序列的List。它的效果同@GroupSequence定义组序列,尤其是列表List必须包含类型T
  List<Class<?>> getValidationGroups(T object);
}


注意:

  1. 此接口Hibernate并没有提供实现
  2. 若你实现请必须提供一个空的构造函数以及保证是线程安全的


按步骤解决多字段组合验证的逻辑:


1、自己实现DefaultGroupSequenceProvider接口(处理Person这个Bean)

public class PersonGroupSequenceProvider implements DefaultGroupSequenceProvider<Person> {
    @Override
    public List<Class<?>> getValidationGroups(Person bean) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();
        defaultGroupSequence.add(Person.class); // 这一步不能省,否则Default分组都不会执行了,会抛错的
        if (bean != null) { // 这块判空请务必要做
            Integer age = bean.getAge();
            System.err.println("年龄为:" + age + ",执行对应校验逻辑");
            if (age >= 20 && age < 30) {
                defaultGroupSequence.add(Person.WhenAge20And30Group.class);
            } else if (age >= 30 && age < 40) {
                defaultGroupSequence.add(Person.WhenAge30And40Group.class);
            }
        }
        return defaultGroupSequence;
    }
}


2、在待校验的javaBean里使用@GroupSequenceProvider注解指定处理器。并且定义好对应的校验逻辑(包括分组)

@GroupSequenceProvider(PersonGroupSequenceProvider.class)
@Getter
@Setter
@ToString
public class Person {
    @NotNull
    private String name;
    @NotNull
    @Range(min = 10, max = 40)
    private Integer age;
    @NotNull(groups = {WhenAge20And30Group.class, WhenAge30And40Group.class})
    @Size(min = 1, max = 2, groups = WhenAge20And30Group.class)
    @Size(min = 3, max = 5, groups = WhenAge30And40Group.class)
    private List<String> hobbies;
    /**
     * 定义专属的业务逻辑分组
     */
    public interface WhenAge20And30Group {
    }
    public interface WhenAge30And40Group {
    }
}



测试用例同上,做出简单修改:person.setAge(25),运行打印输出:

年龄为:25,执行对应校验逻辑
年龄为:25,执行对应校验逻辑


没有校验失败的消息(就是好消息),符合预期。

再修改为person.setAge(35),再次运行打印如下:

年龄为:35,执行对应校验逻辑
年龄为:35,执行对应校验逻辑
hobbies 个数必须在3和5之间: [足球, 篮球]


校验成功,结果符合预期。

从此案例可以看到,通过@GroupSequenceProvider我完全实现了多字段组合校验的逻辑,并且代码也非常的优雅、可扩展,希望此示例对你有所帮助。


本利中的provider处理器是Person专用的,当然你可以使用Object+反射让它变得更为通用,但本着职责单一原则,我并不建议这么去做。

相关文章
|
21天前
|
Java 测试技术 程序员
为什么Spring不推荐@Autowired用于字段注入?
作为Java程序员,Spring框架在日常开发中使用频繁,其依赖注入机制带来了极大的便利。然而,尽管@Autowired注解简化了依赖注入,Spring官方却不推荐在字段上使用它。本文将探讨字段注入的现状及其存在的问题,如难以进行单元测试、违反单一职责原则及易引发NPE等,并介绍为何Spring推荐构造器注入,包括增强代码可读性和维护性、方便单元测试以及避免NPE等问题。通过示例代码展示如何将字段注入重构为构造器注入,提高代码质量。
|
12天前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
25 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
4月前
|
前端开发 JavaScript Java
Spring Boot中的数据校验
Spring Boot中的数据校验
|
3月前
|
Java Spring 容器
建模底层逻辑问题之以Spring IOC容器为例,使用因果法建模,如何操作
建模底层逻辑问题之以Spring IOC容器为例,使用因果法建模,如何操作
|
4月前
|
存储 Java Spring
Spring初始化加速的思路和方案问题之替换默认的Spring Bean初始化逻辑中的问题如何解决
Spring初始化加速的思路和方案问题之替换默认的Spring Bean初始化逻辑中的问题如何解决
|
5月前
|
运维 Java 关系型数据库
Spring运维之boot项目bean属性的绑定读取与校验
Spring运维之boot项目bean属性的绑定读取与校验
51 2
|
4月前
|
Java 数据库连接 测试技术
在Spring Boot中实现数据校验与验证
在Spring Boot中实现数据校验与验证
|
6月前
|
存储 安全 Java
Spring Security的密码加密和校验
本文介绍了Spring Security中密码的加密和校验。首先,在`SecurityConfig`配置类中添加了两个Bean,一个是`PasswordEncoder`的无操作实例,用于明文密码校验,另一个是`UserDetailsService`,用于创建内存中的用户信息。接着,文章对比了对称加密、非对称加密和摘要加密三种加密方式,并重点讲解了BCrypt摘要加密的特性,强调其安全性高于MD5。最后,通过代码示例展示了如何使用BCryptPasswordEncoder改造权限密码加密,确保密码的安全存储和校验。
275 6
|
6月前
|
JSON Java 数据格式
Spring Boot实现各种参数校验
这些是Spring Boot中实现参数校验的一些常见方法,你可以根据项目需求选择适合的方式来进行参数校验。
56 0
|
6月前
|
机器学习/深度学习 运维 Java
江帅帅:Spring Boot 底层级探索系列 02 - 自动配置的底层逻辑
江帅帅:Spring Boot 底层级探索系列 02 - 自动配置的底层逻辑
45 0