当前位置:网站首页>Bean Validation自定义容器验证篇----06
Bean Validation自定义容器验证篇----06
2022-07-24 00:33:00 【大忽悠爱忽悠】
Bean Validation自定义容器验证篇----06
前言
本文接上文叙述,继续介绍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对此提供了扩展点。下面我将一步一步的来对此提供实现,让验证优雅再次起来。
- 自定义一个可以从Result< T >里提取出T值的ValueExtractor值提取器
Bean Validation允许我们对自定义容器元素类型进行支持。
要想支持自定义的容器类型,需要注册一个自定义的ValueExtractor用于值的提取。
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 = "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,这个需要验证的是整个对象(多个字段)。下面呀,我给出两种实现方式,供以参考。
方式一:基于内置的@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("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是内置就提供的,因此使用起来非常的方便和通用。但缺点也是因为过于通用,因此语义上不够明显,需要阅读脚本才知。推荐少量(非重复使用)、逻辑较为简单时使用
- 自定义注解方式。缺点当然是“开箱使用”起来稍显麻烦,但它的优点就是语义明确,灵活且不易出错,即使是复杂的验证逻辑也能轻松搞定
总之,若你的验证逻辑只用一次(只一个地方使用)且简单(比如只是简单判断而已),推荐使用@ScriptAssert更为轻巧。否则,你懂的~
边栏推荐
- Gbase 8C system table information function (I)
- Unity metaverse (I). Ready player me & blender customize your Avatar
- Pytest interface automated testing framework | common running parameters of pytest
- MySQL data query (select)
- Comparison of the shortcomings of redis master-slave, sentinel and cluster architectures
- Redis persistence mechanism RDB, AOF
- GBase 8c系统表信息函数(一)
- Gbase 8C access authority access function (IV)
- Development of main applet for business card traffic near the map
- Blockbuster | certik: Web3.0 industry safety report release in the second quarter of 2022 (PDF download link attached)
猜你喜欢

Implementation of singleton mode in C #

Classic example of C language - convert the input two digits into English

Robot dog back submachine gun shooting video fire, netizens shivering: stoooooooopppp!

Multi data source configuration of mongodb
![[video game training] non contact object size and shape measurement 2020 video game G](/img/b7/d70a702e52062e38176b589eb15415.jpg)
[video game training] non contact object size and shape measurement 2020 video game G

Redis 集群hash分片算法(槽位定位算法)

How to realize 485 wireless communication between multiple sensors and Siemens PLC?

Network system experiment: solve the problem of Ping failure

书写SQL必养成的好习惯

数仓数据标准详解-2022
随机推荐
GBase 8c 会话信息函数(一)
sed 深入理解与使用
Difference between data index and label system of data warehouse
GBase 8c 字符串操作符
Customize an object
Educational Codeforces Round 132 (Rated for Div. 2)(A-D)
Expérience du système réseau: résoudre les problèmes de ping
Classic examples of C language - adding two scores
Method of C language annotation
GBase 8c系统表信息函数(二)
Understanding polymorphism and letting different "people" do the same thing will produce different results
Gbase 8C access authority query function (II)
Simple implementation and analysis of binary search tree
GBase 8c 会话信息函数(三)
Printf function - conversion description
Detailed explanation of data warehouse standard -2022
Redis主从同步机制
Gbase 8C binary string operator
Redis data structure
Redis cluster hash sharding algorithm (slot location algorithm)