Validation工具库是个为改进团队协作,规范代码而生的工具包,主要应用场景是业务的前置条件检测以及后置条件检测。如果检测不通过则立即抛出异常并终止处理。它具有以下主要特点:
- 简单但不失灵活,开箱即用;
- 友好并可配置;
- 基本依赖只有slf4j-api,其他依赖根据使用方式可选:fast-classpath-scanner,snakeyaml,config。
基本用法
为了方便,假设有某系统中有以下对象:
1 public class Address {
2 private String province;
3 private String city;
4 private county;
5 private detailed;
6
7 ......
8 }
9
10 publi class Personnel {
11 private String name;
12 private Address home;
13 private Address office;
14
15 ......
16 } 在业务处理中要求Personnel的名称、家庭地址和办公地址都不能为空。使用Validation工具库就是:
1 String nameNotNull = "PERSONNEL_NAME_NOT_NULL";
2 String adddressNotNull = "PERSONNEL_ADDRESS_NOT_NULL";
3
4 // 这样校验
5 Constraint.of(nameNotNull).validator().invalidIf(personnel.getName() != null);
6 Constraint.of(adddressNotNull).validator().invalidIf(personnel.getHome() != null);
7 Constraint.of(adddressNotNull).validator().invalidIf(personnel.getOffice() != null);
8
9 // 或者
10 Validators.invalidIf(personnel.getName() != null, nameNotNull);
11 Validators.invalidIf(personnel.getHome() != null, adddressNotNull);
12 Validators.invalidIf(personnel.getOffice() != null, adddressNotNull);
13
14 // 或者
15 Validators.of(nameNotNull).invalidIf(personnel.getName() != null);
16 Validators.of(adddressNotNull).invalidIf(personnel.getHome() != null);
17 Validators.of(adddressNotNull).invalidIf(personnel.getOffice() != null);
18
19
20 // 重用校验器还可以写成
21 Validator nameValidator = Validators.of(nameNotNull);
22 Validator adddressValidator = Validators.of(adddressNotNull);
23 nameValidator.invalidIf(personnel.getName() != null);
24 adddressValidator.invalidIf(personnel.getHome() != null);
25 adddressValidator.invalidIf(personnel.getOffice() != null); 当条件成立时,Validator将抛出异常。异常的信息来自Constraint,包含其代码和描述。在上面展示的代码中,系统首先尝试将传入的Constraint字符串参数当成约束代码去匹配受管理的约束(包括工具包内置的,或程序定义和配置的),如果匹配失败,则以Constraint.UNCODED.code()为代码,Constraint字符串参数为描述构建临时的Constraint。
与Validator同理,当Constraint反复使用时,更合理的方式是事先构造好Constraint取代Constraint字符串参数,譬如:
1 Constraint adddressConstraint = Constraint.of(adddressNotNull);
2 adddressConstraint.validator().invalidIf(personnel.getHome() != null);
3 adddressConstraint.validator().invalidIf(personnel.getOffice() != null); 通常情况面,重用Validator实例是最佳的方式,应为其内部已经重用了Constraint。
真的很简单,不是吗?是的,使用很简单。但是不为空的逻辑实际情况可能会复杂得多。譬上例中,仅仅检测adddressNotNull其实并不能准确的表达业务约束。如果地址不为空,但是其内容为空,更进一步,其地址必须要能正确的表达业务(试想有个地址为“地球省亚洲市喜马拉雅县唐古拉公园”会怎么样)。针对这类情形,Validation工具库提供了可选的支持,那就是用一个Conditional实例去替换上述中的布尔值:
1 Predicate<Address> isBadAddress = address -> {
2 ......
3 }
4
5 Conditional homeIsBad = Conditional.newPredicative(isBadAddress, personnel.getHome());
6 Conditional officeIsBad = Conditional.newPredicative(isBadAddress, personnel.getOffice());
7
8 adddressValidator.invalidIf(homeIsBad);
9 adddressValidator.invalidIf(officeIsBad); 将“校验逻辑”放到Address中,直接Address::invalid()不是更好吗?是的,某些情况这样会更好,这里强调的是这样做不好的场景:
- 在初期的业务场景中,不需要校验逻辑;
- 在不同的业务场景中,“校验逻辑”是不同的;
- 在不同的业务场景中,校验“违规信息”有不同的。
Validation工具库专门设计了“名称空间”对约束的代码进行包装来处理此类问题。包装的方法也很简单:用名称空间的代码 + “.” + 约束代码替换原始的约束代码。而检索约束时,则是从当前名称空间开始,逐层往上层名称空间搜索,一旦搜索到则立即返回,从而保证提供的违规描述信息最贴近当前场景的。
使用“名称空间”能为校验违规提供更符合场景的信息。“名称空间”的用法如下:
1 NameSpace homeScene = NameSpace.of("scene.home")
2
3 homeScene.validator(adddressNotNull).invalidIf(homeIsBad);
4 // 或者
5 homeScene.invalidIf(homeIsBad, adddressNotNull);
6 // 或者
7 adddressValidator.invalidIf(homeIsBad, homeScene);Constraint配置
Validation工具库是开箱即用的,即使不做任何配置,也能工作起来。但灵活配置时其一个核心特征。合理配置可以得到意想不到的惊喜:
- 编码过程中获得IDE友好支持;
- 实现违规信息的提供与使用的团队成员角色分工;
- 部署时实现违规信息的替换;
- 运行时实现违规信息的替换;
- 替换违规行为的默认处理方式;
- 替换名称空间逻辑。
Validation工具库支持不同颗粒度的配置:约束系统,约束容器,维护处理,约束,名称空间。下面介绍使用过程中用的比较多的“约束级别”的配置。
1 String packageName = "me.szlx.constraint";
2 ImplementBundle.from(packageName).bindTo(ConstraintSystem.get().getConstraintContainer());Configurer工具类进行配置。工具包内置的Bundle有:
- ImplementBundle: 通过扫描类路径下实现了接口
Constraint的实例或类(非枚举类需无参构造函数)查找约束;- JdbcBundle: 从数据表中查找约束;
- MapBundle: 从Map的键值对中提取约束;
- PropertiesBundle: 从属性文件中提取约束;
- TypeSafeBundle: 从config支持的格式文件中提取约束;
- YamlBundle: 从yaml格式文件中提取约束。
最佳实践
使用枚举定义约束
使用枚举类定义的约束可以获得IDE环境的语法支持。不要试图将所有的约束放在同一个枚举类中。
1 public enum PersonnelConstraint implements Constraint { 2 NAME_NOT_NULL("nameNotNull", "名称不能为空"), ADDDRESS_NOT_NULL("adddressNotNull", "地址不能为空"); 3 4 private String code; 5 private String brief; 6 7 PersonnelConstraint(String code, String brief) { 8 this.code = code; 9 this.brief = brief; 10 } 11 12 @Override 13 public String code() { 14 return code; 15 } 16 17 @Override 18 public String brief() { 19 return brief; 20 } 21 }使用名称空间组织场景化的信息
名称空间可以有效组织约束违规描述,这样提供贴近场景的描述提供了可能。名称空间支持多层次的父子级联。
以上使用枚举类实现了枚举名称空间。枚举类实现的枚举空间的字面代码前缀格式为:短类名 + “.” + 枚举变量名。但1 public enum SCENE implements NameSpace { 2 CONSTRAINTS, CONSTRAINT, PERSONNEL; 3 }CONSTRAINTS和CONSTRAINT这两个名称做了特殊处理,前缀格式直接为:短类名。因此上面的枚举类实际定义了2个名称空间前缀:SCENE和SCENE.PERSONNEL。还可以定义多层次名称空间:
以上接口定义了3个名称空间:1 public interface SCENE { 2 NameSpace PERSONNEL = NameSpace.of("PERSONNEL"); 3 NameSpace PERSONNEL_NAME = NameSpace.of("NAME", PERSONNEL); 4 NameSpace PERSONNEL_ADDRESS = NameSpace.of("ADDRESS", PERSONNEL); 5 }PERSONNEL,PERSONNEL_NAME和PERSONNEL_ADDRESS。其中后两者是前者的子名称空间,PERSONNEL名称空间的前缀为“PERSONNEL”,PERSONNEL_NAME名称空间的前缀为“PERSONNEL.NAME”,PERSONNEL_ADDRESS名称空间的前缀为“PERSONNEL.ADDRESS”。子名称空间的全前缀为:子名称空间的全前缀 + “.” + 子名称空间的前缀。以下几种方式的效果相同的:
1 boolean predicate = personnel.getName() != null; 2 Validators.invalidIf(predicate, "PERSONNEL.NAME.NOT_NULL"); 3 Validators.invalidIf(predicate, "NAME.NOT_NULL", PERSONNEL); 4 Validators.invalidIf(predicate, "NOT_NULL", PERSONNEL_NAME);使用名称空间组织场景时,尽管可以,但是建议约束代码不要使用点(.)分的字符串,以免与名称空间混淆。使用点分约束代码时,应当右边第一个“.”右边的字符串看成是约束代码,而左边的则当成是名称空间代码。