今天推荐的是一款java中,对比两个json-diff对象是否一致的工具包 json-diff` 。他可以对比任何结构的两个json数据,并且将其中的不一致信息反馈给用户。工具还内置了很多配置可以来控制对比过程中的行为。目前已经补充大量单测,稳定性还是比较好的。
公司最近在重构一个核心系统,至于为什么重构原因很多,就不说明了。但是这个核心系统承载较多的线上业务。为了不影响依赖依赖该服务的应用,所以我们重构的最核心就是完全兼容老系统接口。
为了保证平滑上线,并且测试新系统与老系统是否一致,我们决定系统并行一段时间,并且在这段时间之中验证新接口对老接口的兼容性。我们新起一个代理服务,他会将我们的用户流量分别转发到新老接口,然后拿到两个结果,将老接口结果直接返回;异步去比较新老结果是否符合预期,进行记录或者报警。
这样系统在经过一段时间的测试,稳定性更高,出错的概率更小。
因为系统都是采用http接口对外提供服务,且返回数据格式统一的是json格式。所以我们急需一款强大的Java语言的Json对比工具来帮助我们发现新老系统的不兼容之处。
json-diff 是一款功能强大的json差异发现工具,支持任何结构的json对比,并且可以将对比结果返给用户。目前该工具更新到了 3.0.0-RC1-RELEASE
版本。最新版可以查看 版本列表 。建议使用最新版,旧版可能存在缺陷。
优点:
<dependency> <groupId>cn.xiaoandcai</groupId> <artifactId>json-diff</artifactId> <!-- 旧版本可能存在某些缺陷。版本请以maven仓库最版为准。 --> <version>${version}</version> </dependency>
版本查看 2022-03-04 最新版本:3.0.0-RC1-RELEASE
/** * @author: codeleep * @createTime: 2022/11/22 16:57 * @description: 使用示例 */ public class UseExample { public static void main(String[] args) { String array1 = "[1, 2, 3, 4, 5]"; String array2 = "[1, 3, 9, 4, 5]"; JsonComparedOption jsonComparedOption = new JsonComparedOption().setIgnoreOrder(true); JsonCompareResult jsonCompareResult = new DefaultJsonDifference() .option(jsonComparedOption) .detectDiff(JSON.parseArray(array1), JSON.parseArray(array2)); System.out.println(JSON.toJSONString(jsonCompareResult)); } }
结果展示:
{ "defectsList": [ { "actual": 9, "expect": 2, "illustrate": "The expect('2') data is inconsistent with the actual('9') data", "travelPath": { "abstractTravelPath": "root[]", "actualTravelPath": "root[2]", "expectTravelPath": "root[1]" } } ], "match": false }
工具会返回 match 表示是否通过比对。defectsList 则是对比信息。
配置 | 类型 | 备注 |
---|---|---|
ignoreOrder | boolean | 是否比较过程中忽略数组顺序 |
mapping | Map<String, String> | 将真实字段映射到期望字段,key是真实字段name,value是期望的字段name |
ignorePath | Set<String> | 当对比的路径完全匹配时会被跳过。遇到数组使用 [] 即可。无需填入下标 |
ignoreKey | Set<String> | 对比object时。或忽略该key。对整个json生效 |
customComparator | Map<String, Class<JsonNeat>> | 用户自定义比较器。具体说明见下文 |
在
2.0.1-RC1-RELEASE
之后版本中移除了keyFunction
配置参数。可以使用ignorePath
来代替达到同样的效果。
工具提供了四个配置,来之对比过程中一些其他的要求。工具还在积极开发中,如果有新的需求,可以给作者提一个issuse。
在开发中。很多时候对比配置一致。可以使用 JsonDiffOption
进行开启唯一配置
由于在设计中考虑到各线程比较配置相互独立。所以默认将配置防止在 ThreadLocal
中进行存储。但在大多数情况下,我们在全局比较时,配置并不会发生变化。
工具提供了全局配置方式。采用的方式是静态类属性。这样也会获得更好的性能。
// 开启并设置全局配置 JsonDiffOption.openUniqueOption(); JsonDiffOption.setGloballyUniqueOption(new JsonComparedOption()); // 不想使用时可以调用调整回线程独有模式 JsonDiffOption.closeUniqueOption();
当我们在遇到数组元素是一个对象时。如下:
[ { "date": "23日星期五", "sunrise": "06:16", "high": "高温 18.0℃" }, { "date": "24日星期六", "sunrise": "06:14", "high": "高温 21.0℃" } ]
在比较时, 如果希望 date
字段一致,则认为两个对象一致。那么可以将 sunrise
, high
字段都配置到 ignorePath
中。如:
HashSet<String> ignorePath = new HashSet<>(); ignorePath.add("root[].sunrise"); ignorePath.add("root[].high");
如果只是不想关注某个字段。即是 ignorePath
正常用法。配置如上。
在比较两个对象时。也许由于字段名变更。导致校验不通过。这时可以使用 mapping
配置。将 真实字段名称映射至期望字段名称。在比较过程中会将
actual.mappingKey 与 expect.mappingValue 认为是应该比较的对象。具体配置如下
// mapping key 是 actual 键名 // mapping value 是 expect 键名 HashMap<String, String> mapping = new HashMap<>(); mapping.put("date", "sunrise");
如果有一些字段是想在整个json都进行忽略的,可以使用 ignoreKey
进行全局忽略。当然如果不想全局忽略,但是配置了该项,还是会被忽略掉。
HashSet<String> ignoreKey = new HashSet<>(); ignoreKey.add("sunrise"); ignoreKey.add("high");
在我们一个大json文件下。可能遇到某些节点希望实现自定义比较。可以通过 customComparator
来进行实现。
它配置的key是一个 travelPath 。具体格式参照 ignorePath 。value 则是一个自定义比较器。对于自定义比较器需要继承对应的抽象类。并且实现具体的抽象接口。具体如下:
对象比较:
需要继承 me.codeleep.jsondiff.core.handle.array.AbstractArrayJsonNeat
并且重写以下方法。
/** * 比较对象 * @param expect 期望的json对象 * @param actual 实际的json对象 * @return 返回比较结果 * @throws IllegalAccessException 发生异常直接抛出 */ JsonCompareResult detectDiff(JSONObject expect, JSONObject actual);
数组比较:
需要继承 me.codeleep.jsondiff.core.handle.object.AbstractObjectJsonNeat
并且重写以下方法。
/** * 比较数组.调用入口。需要自己去分别调用 ignoreOrder 和 keepOrder。 * @param expect 期望的json对象 * @param actual 实际的json对象 * @return 返回比较结果 */ JsonCompareResult detectDiff(JSONArray expect, JSONArray actual); // 忽略顺序的比较 JsonCompareResult ignoreOrder(JSONArray expect, JSONArray actual); // 保持顺序比较 JsonCompareResult keepOrder(JSONArray expect, JSONArray actual);
基本类型比较:
基本类型指的是java基础类型的包装类型以及Number的实现类型。
需要继承 me.codeleep.jsondiff.core.handle.primitive.AbstractPrimitiveJsonNeat
并且重写以下方法。
/** * 比较数组 * @param expect 基础类型对象 * @param actual 基础类型对象 * @return 返回比较结果 */ JsonCompareResult detectDiff(Object expect, Object actual);
用户可以自己根据 travelPath 来决定使用何种自定义比较。三种比较器都返回 JsonCompareResult 对象作为当前节点的比较结果。对于JsonCompareResult对象。需要填入以下信息:
// 示例 JsonCompareResult result = new JsonCompareResult(); Defects defects = new Defects() .setActual(actualDiffJson) .setExpect(expectDiffJson) .setTravelPath(nextTravelPath) .setIllustrateTemplate(DATA_TYPE_INCONSISTENT, expectDiffJson.getClass().getName(), actualDiffJson.getClass().getName()); result.addDefects(defects);
如果遇到在自定义节点中,还需要使用系统自带的比较器时。
// 该值可以在上述三个抽象类中获得。但需要经自行处理 String abstractTravelPath = "root"; // 下一级是对象 TravelPath nextTravelPath = new TravelPath(abstractTravelPath, mappingKey); // 下一级是数组 TravelPath nextTravelPath = new TravelPath(abstractTravelPath, expectIndex, actualIndex); // 获得比较器 JsonDiffUtil.getJsonNeat(expectDiffJson, actualDiffJson, nextTravelPath); // 执行比较获得结果 JsonCompareResult diff = jsonNeat.diff(expectDiffJson, actualDiffJson, nextTravelPath); // 本级创建的 JsonCompareResult result 将下一级结果合并 this.result.mergeDefects(diff.getDefectsList());
可以使用上述代码获取系统自带的比较器。
自定义比较器值得注意的是: 从匹配到 travelPath 之后,根据不再接管比较操作。一切行为由用户自行定义。但工具依然预留默认的比较器给用户处理后续字段。这需要用户自行进行组合调用。
前面提到工具几乎可以支持所有json结果的对比校验,并且发现差异。那它到底可以支持哪些呢,不知道是否符合你的需求呢?
对象 ✅
这是最简单的数据结构了,其中元素都以key-value构成,key是字符串,value可以是任何数据结构。
数组 ✅
支持严格顺序对比和忽略顺序对比,可以细化数组元素的类型
基本类型 ✅
对象类型 ✅
该类型在对比时,可以通过ignorePath参数进行元素是否进行比较,将不关心的元素忽略掉。当然ignoreKey也可以,但其是全局生效
数组类型 ✅
元素也是数组,这样就形成了多维数组,工具理论上来说支持n维数组的对比
元素类型不统一 ✅
数组中,类型可能包含前面三种类型,这时工具会按照类型分类进行匹配,最后找不到的元素再反馈给用户。
由于json结构在单个看来,就只有对象和数组两种类型,该工具完美支持了所有类型。