- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
本文接上文叙述,继续介绍Bean Validation声明式验证四大级别中的:容器元素验证(自定义容器类型)以及类级别验证(也叫多字段联合验证)。
通过上文我们已经知道了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对此提供了扩展点。下面我将一步一步的来对此提供实现,让验证优雅再次起来。
Bean Validation允许我们对自定义容器元素类型进行支持。
要想支持自定义的容器类型,需要注册一个自定义的ValueExtractor用于值的提取。
public class ResultValueExtractor implements ValueExtractor<Result<@ExtractedValue ?>> {
@Override
public void extractValues(Result<?> originalValue, ValueReceiver receiver) {
receiver.value(null, originalValue.getData());
}
}
把Result作为一个Filed字段装进Java Bean里:
public class ResultDemo {
public Result<@Valid Room> roomResult;
}
测试代码:
public static void main(String[] args) {
Room room = new Room();
room.name = "DHY";
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,这个需要验证的是整个对象(多个字段)。下面呀,我给出两种实现方式,供以参考。
虽说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("DHY"));
ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(room));
}
再次运行:
执行脚本表达式"_.maxStuNum >= _.studentNames.length"没有返回期望结果,但你的值是: Room(maxStuNum=0, studentNames=[DHY])
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更为轻巧。否则,你懂的~
好的,所以我编辑了以下... 只需将以下内容放入我的 custom.css #rt-utility .rt-block {CODE HERE} 但是当我尝试改变... 与 #rt-sideslid
在表格 View 中,我有一个自定义单元格(在界面生成器中高度为 500)。在该单元格中,我有一个 Collection View ,我按 (10,10,10,10) 固定到边缘。但是在 tablev
对于我的无能,我很抱歉,但总的来说,我对 Cocoa、Swift 和面向对象编程还很陌生。我的主要来源是《Cocoa Programming for OS X》(第 5 版),以及 Apple 的充满
我正在使用 meta-tegra 为我的 NVIDIA Jetson Nano 构建自定义图像。我需要 PyTorch,但没有它的配方。我在设备上构建了 PyTorch,并将其打包到设备上的轮子中。现
在 jquery 中使用 $.POST 和 $.GET 时,有没有办法将自定义变量添加到 URL 并发送它们?我尝试了以下方法: $.ajax({type:"POST", url:"file.php?
Traefik 已经默认实现了很多中间件,可以满足大部分我们日常的需求,但是在实际工作中,用户仍然还是有自定义中间件的需求,为解决这个问题,官方推出了一个 Traefik Pilot[1] 的功
我想让我的 CustomTextInputLayout 将 Widget.MaterialComponents.TextInputLayout.OutlinedBox 作为默认样式,无需在 XML 中
我在 ~/.emacs 中有以下自定义函数: (defun xi-rgrep (term) (grep-compute-defaults) (interactive "sSearch Te
我有下表: 考虑到每个月的权重,我的目标是在 5 个月内分散 10,000 个单位。与 10,000 相邻的行是我最好的尝试(我在这上面花了几个小时)。黄色是我所追求的。 我试图用来计算的逻辑如下:计
我的表单中有一个字段,它是文件类型。当用户点击保存图标时,我想自然地将文件上传到服务器并将文件名保存在数据库中。我尝试通过回显文件名来测试它,但它似乎不起作用。另外,如何将文件名添加到数据库中?是在模
我有一个 python 脚本来发送电子邮件,它工作得很好,但问题是当我检查我的电子邮件收件箱时。 我希望该用户名是自定义用户名,而不是整个电子邮件地址。 最佳答案 发件人地址应该使用的格式是: You
我想减小 ggcorrplot 中标记的大小,并减少文本和绘图之间的空间。 library(ggcorrplot) data(mtcars) corr <- round(cor(mtcars), 1)
GTK+ noob 问题在这里: 是否可以自定义 GtkFileChooserButton 或 GtkFileChooserDialog 以删除“位置”部分(左侧)和顶部的“位置”输入框? 我实际上要
我正在尝试在主页上使用 ajax 在 magento 中使用 ajax 显示流行的产品列表,我可以为 5 或“N”个产品执行此操作,但我想要的是将分页工具栏与结果集一起添加. 这是我添加的以显示流行产
我正在尝试使用 PasswordResetForm 内置函数。 由于我想要自定义表单字段,因此我编写了自己的表单: class FpasswordForm(PasswordResetForm):
据我了解,新的 Angular 7 提供了拖放功能。我搜索了有关 DnD 的 Tree 组件,但没有找到与树相关的内容。 我在 Stackblitz 上找到的一个工作示例.对比drag'ndrop功能
我必须开发一个自定义选项卡控件并决定使用 WPF/XAML 创建它,因为我无论如何都打算学习它。完成后应该是这样的: 到目前为止,我取得了很好的进展,但还有两个问题: 只有第一个/最后一个标签项应该有
我要定制xtable用于导出到 LaTeX。我知道有些问题是关于 xtable在这里,但我找不到我要找的具体东西。 以下是我的表的外观示例: my.table <- data.frame(Specif
用ejs在这里显示日期 它给我结果 Tue Feb 02 2016 16:02:24 GMT+0530 (IST) 但是我需要表现为 19th January, 2016 如何在ejs中执行此操作?
我想问在 JavaFX 中使用自定义对象制作 ListView 的最佳方法,我想要一个每个项目如下所示的列表: 我搜了一下,发现大部分人都是用细胞工厂的方法来做的。有没有其他办法?例如使用客户 fxm
我是一名优秀的程序员,十分优秀!