乐趣区

关于spring:6-自定义容器类型元素验证类级别验证多字段联合验证

明天搬砖不狠,今天位置不稳。本文已被 https://www.yourbatman.cn 收录,外面一并有 Spring 技术栈、MyBatis、JVM、中间件等小而美的 专栏 供以收费学习。关注公众号【BAT 的乌托邦】一一击破,深刻把握,回绝浅尝辄止。

✍前言

你好,我是 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 染指帮解决此类问题,置信对提效是很有帮忙的,说不定你还能成为团队中最靓的仔呢。

✔举荐浏览:
  • 1. 不吹不擂,第一篇就能晋升你对 Bean Validation 数据校验的认知
  • 2. Bean Validation 申明式校验办法的参数、返回值
  • 3. 站在应用层面,Bean Validation 这些标准接口你须要烂熟于胸
  • 4. Validator 校验器的五大外围组件,一个都不能少
  • 5. Bean Validation 申明式验证四大级别:字段、属性、容器元素、类

♥关注 YourBatman♥

Author YourBatman
集体站点 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
沉闷平台
公众号 BAT 的乌托邦(ID:BAT-utopia)
常识星球 BAT 的乌托邦
每日文章举荐 每日文章举荐

退出移动版