6. 自定义容器类型元素验证,类级别验证(多字段联合验证)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 6. 自定义容器类型元素验证,类级别验证(多字段联合验证)

✍前言


你好,我是YourBatman。


本文是上篇文章的续篇,个人建议可先花3分钟移步上篇文章浏览一下:5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类


很多人说Bean Validation只能验证单属性(单字段),但我却说它能完成99.99%的Bean验证,不信你可继续阅读本文,能否解你疑惑。


版本约定


  • Bean Validation版本:2.0.2
  • Hibernate Validator版本:6.1.5.Final


✍正文


本文接上文叙述,继续介绍Bean Validation声明式验证四大级别中的:容器元素验证(自定义容器类型)以及类级别验证(也叫多字段联合验证)。


据我了解,很多小伙伴对这部分内容并不熟悉,遇到类似场景往往被迫只能是一半BV验证 + 一半事务脚本验证的方式,显得洋不洋俗不俗。 本文将给出具体案例场景,然后统一使用BV来解决数据验证问题,希望可以帮助到你,给予参考之作用。


自定义容器类型元素验证


通过上文我们已经知道了Bean Validation是可以对形如List、Set、Map这样的容器类型里面的元素进行验证的,内置支持的容器虽然能cover大部分的使用场景,但不免有的场景依旧不能覆盖,而且这个可能还非常常用。


譬如我们都不陌生的方法返回值容器Result<T>,结构形如这样(最简形式,仅供参考):

@Data
public final class Result<T> implements Serializable {
    private boolean success = true;
    private T data = null;
    private String errCode;
    private String errMsg;
}


Controller层用它包装(装载)数据data,形如这样:


@GetMapping("/room")
Result<Room> room() { ... }
public class Room {
    @NotNull
    public String name;
    @AssertTrue
    public boolean finished;
}

这个时候希望对Result<Room>里面的Room进行合法性验证:借助BV进行声明式验证而非硬编码。希望这么写就可以了:Result<@Notnull @Valid LoggedAccountResp>。显然,缺省情况下即使这样声明了约束注解也是无效的,毕竟Bean Validation根本就“不认识”Result这个“容器”,更别提验证其元素了。


好在Bean Validation对此提供了扩展点。下面我将一步一步的来对此提供实现,让验证优雅再次起来。


  • 自定义一个可以从Result<T>里提取出T值的ValueExtractor值提取器


Bean Validation允许我们对自定义容器元素类型进行支持。通过前面这篇文章:4. Validator校验器的五大核心组件,一个都不能少 知道要想支持自定义的容器类型,需要注册一个自定义的ValueExtractor用于值的提取。


/**
 * 在此处添加备注信息
 *
 * @author yourbatman
 * @site https://www.yourbatman.cn
 * @date 2020/10/25 10:01
 * @see Result
 */
public class ResultValueExtractor implements ValueExtractor<Result<@ExtractedValue ?>> {
    @Override
    public void extractValues(Result<?> originalValue, ValueReceiver receiver) {
        receiver.value(null, originalValue.getData());
    }
}


  • 将此自定义的值提取器注册进验证器Validator里,并提供测试代码:


把Result作为一个Filed字段装进Java Bean里:

public class ResultDemo {
    public Result<@Valid Room> roomResult;
}


测试代码:


public static void main(String[] args) {
    Room room = new Room();
    room.name = "YourBatman";
    Result<Room> result = new Result<>();
    result.setData(room);
    // 把Result作为属性放进去
    ResultDemo resultDemo = new ResultDemo();
    resultDemo.roomResult = result;
    // 注册自定义的值提取器
    Validator validator = ValidatorUtil.obtainValidatorFactory()
            .usingContext()
            .addValueExtractor(new ResultValueExtractor())
            .getValidator();
    ValidatorUtil.printViolations(validator.validate(resultDemo));
}


运行测试程序,输出:

roomResult.finished只能为true,但你的值是: false


完美的实现了对Result“容器”里的元素进行了验证。


小贴士:本例是把Result作为Java Bean的属性进行试验的。实际上大多数情况下是把它作为方法返回值进行校验。方式类似,有兴趣的同学可自行举一反三哈


在此弱弱补一句,若在Spring Boot场景下你想像这样对Result<T>提供支持,那么你需要自行提供一个验证器来覆盖掉自动装配进去的,可参考ValidationAutoConfiguration。


类级别验证(多字段联合验证)


约束也可以放在类级别上(也就说注解标注在类上)。在这种情况下,验证的主体不是单个属性,而是整个对象。如果验证依赖于对象的几个属性之间的相关性,那么类级别约束就能搞定这一切。


这个需求场景在平时开发中也非常常见,比如此处我举个场景案例:Room表示一个教室,maxStuNum表示该教室允许的最大学生数,studentNames表示教室里面的学生们。很明显这里存在这么样一个规则:学生总数不能大于教室允许的最大值,即studentNames.size() <= maxStuNum。如果用事务脚本来实现这个验证规则,那么你的代码里肯定穿插着类似这样的代码:


if (room.getStudentNames().size() > room.getMaxStuNum()) {
    throw new RuntimeException("...");
}


虽然这么做也能达到校验的效果,但很明显这不够优雅。期望这种case依旧能借助Bean Validation来优雅实现,下面我来走一把。


相较于前面但字段/属性验证的使用case,这个需要验证的是整个对象(多个字段)。下面呀,我给出两种实现方式,供以参考。


方式一:基于内置的@ScriptAssert实现


虽说Bean Validation没有内置任何类级别的注解,但Hibernate-Validator却对此提供了增强,弥补了其不足。@ScriptAssert就是HV内置的一个非常强大的、可以用于类级别验证注解,它可以很容易的处理这种case:


@ScriptAssert(lang = "javascript", alias = "_", script = "_.maxStuNum >= _.studentNames.length")
@Data
public class Room {
    @Positive
    private int maxStuNum;
    @NotNull
    private List<String> studentNames;
}


@ScriptAssert支持写脚本来完成验证逻辑,这里使用的是javascript(缺省情况下的唯一选择,也是默认选择)


测试用例:


public static void main(String[] args) {
    Room room = new Room();
    ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(room));
}


运行程序,抛错:

Caused by: <eval>:1 TypeError: Cannot get property "length" of null
  at jdk.nashorn.internal.runtime.ECMAErrors.error(ECMAErrors.java:57)
  at jdk.nashorn.internal.runtime.ECMAErrors.typeError(ECMAErrors.java:213)
  ...


这个报错意思是_.studentNames值为null,也就是room.studentNames字段的值为null。


what?它头上不明明标了@NotNull注解吗,怎么可能为null呢?这其实涉及到前面所讲到的一个小知识点,这里提一嘴:所有的约束注解都会执行,不存在短路效果(除非校验程序抛异常),只要你敢标,我就敢执行,所以这里为嘛报错你懂了吧。


小贴士:@ScriptAssert对null值并不免疫,不管咋样它都会执行的,因此书写脚本时注意判空哦


当然喽,多个约束之间的执行也是可以排序(有序的),这就涉及到多个约束的执行顺序(序列)问题,本文暂且绕过。例子种先给填上一个值,后续再专文详解多个约束注解执行序列问题和案例剖析。


修改测试脚本(增加一个学生,让其不为null):


public static void main(String[] args) {
    Room room = new Room();
    room.setStudentNames(Collections.singletonList("YourBatman"));
    ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(room));
}


再次运行,输出:

执行脚本表达式"_.maxStuNum >= _.studentNames.length"没有返回期望结果,但你的值是: Room(maxStuNum=0, studentNames=[YourBatman])
maxStuNum必须是正数,但你的值是: 0


验证结果符合预期:0(maxStuNum) < 1(studentNames.length)。


小贴士:若测试脚本中增加一句room.setMaxStuNum(1);,那么请问结果又如何呢?


方式二:自定义注解方式实现


虽说BV自定义注解前文还暂没提到,但这并不难,因此这里先混个脸熟,也可在阅读到后面文章后再杀个回马枪回来。


  • 自定义一个约束注解,并且提供约束逻辑的实现

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = {ValidStudentCountConstraintValidator.class})
public @interface ValidStudentCount {
    String message() default "学生人数超过最大限额";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}


public class ValidStudentCountConstraintValidator implements ConstraintValidator<ValidStudentCount, Room> {
    @Override
    public void initialize(ValidStudentCount constraintAnnotation) {
    }
    @Override
    public boolean isValid(Room room, ConstraintValidatorContext context) {
        if (room == null) {
            return true;
        }
        boolean isValid = false;
        if (room.getStudentNames().size() <= room.getMaxStuNum()) {
            isValid = true;
        }
        // 自定义提示语(当然你也可以不自定义,那就使用注解里的message字段的值)
        if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("校验失败xxx")
                    .addPropertyNode("studentNames")
                    .addConstraintViolation();
        }
        return isValid;
    }
}


  • 书写测试脚本


public static void main(String[] args) {
    Room room = new Room();
    room.setStudentNames(Collections.singletonList("YourBatman"));
    ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(room));
}


运行程序,输出:

maxStuNum必须是正数,但你的值是: 0
studentNames校验失败xxx,但你的值是: Room(maxStuNum=0, studentNames=[YourBatman])


完美,完全符合预期。


这两种方式都可以实现类级别的验证,它俩可以说各有优劣,主要体现在如下方面:


  • @ScriptAssert是内置就提供的,因此使用起来非常的方便和通用。但缺点也是因为过于通用,因此语义上不够明显,需要阅读脚本才知。推荐少量(非重复使用)、逻辑较为简单时使用
  • 自定义注解方式。缺点当然是“开箱使用”起来稍显麻烦,但它的优点就是语义明确,灵活且不易出错,即使是复杂的验证逻辑也能轻松搞定


总之,若你的验证逻辑只用一次(只一个地方使用)且简单(比如只是简单判断而已),推荐使用@ScriptAssert更为轻巧。否则,你懂的~


✍总结



如果说能熟练使用Bean Validation进行字段、属性、容器元素级别的验证是及格60分的话,那么能够使用BV解决本文中几个场景问题的话就应该达到优秀级80分了。


本文举例的两个场景:Result<T>和多字段联合验证均属于平时开发中比较常见的场景,如果能让Bean Validation介入帮解决此类问题,相信对提效是很有帮助的,说不定你还能成为团队中最靓的仔呢。

相关文章
|
5月前
|
监控 Serverless 文件存储
函数计算产品使用问题之如何确保新建的实例拉取的是最新的自定义容器镜像
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。
|
3月前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
|
3月前
|
容器
【Qt 学习笔记】Qt常用控件 | 容器类控件 | Group Box的使用及说明
【Qt 学习笔记】Qt常用控件 | 容器类控件 | Group Box的使用及说明
279 3
|
3月前
|
容器
【Qt 学习笔记】Qt常用控件 | 容器类控件 | Tab Widget的使用及说明
【Qt 学习笔记】Qt常用控件 | 容器类控件 | Tab Widget的使用及说明
131 2
|
3月前
|
存储 容器
容器镜像解析问题之desc.Image() 方法确定返回的 Image 接口类型如何解决
容器镜像解析问题之desc.Image() 方法确定返回的 Image 接口类型如何解决
20 0
|
3月前
|
数据采集 监控 Kubernetes
Job类日志采集问题之iLogtail以减小容器发现和开始采集的延时如何优化
Job类日志采集问题之iLogtail以减小容器发现和开始采集的延时如何优化
|
3月前
|
数据采集 Kubernetes Java
Job类日志采集问题之在日志中添加容器的元信息标签,如何操作
Job类日志采集问题之在日志中添加容器的元信息标签,如何操作
|
3月前
|
存储 Kubernetes 数据处理
Job类日志采集问题之为什么Job容器的日志采集要考虑容器发现速度和开始采集延时,如何理解
Job类日志采集问题之为什么Job容器的日志采集要考虑容器发现速度和开始采集延时,如何理解
|
3月前
|
C++ 容器
C++中自定义结构体或类作为关联容器的键
C++中自定义结构体或类作为关联容器的键
42 0
|
13天前
|
Kubernetes Cloud Native Docker
云原生时代的容器化实践:Docker和Kubernetes入门
【10月更文挑战第37天】在数字化转型的浪潮中,云原生技术成为企业提升敏捷性和效率的关键。本篇文章将引导读者了解如何利用Docker进行容器化打包及部署,以及Kubernetes集群管理的基础操作,帮助初学者快速入门云原生的世界。通过实际案例分析,我们将深入探讨这些技术在现代IT架构中的应用与影响。
55 2
下一篇
无影云桌面