json-diff简单使用

一、摘要

今天推荐的是一款java中,对比两个json-diff对象是否一致的工具包 json-diff` 。他可以对比任何结构的两个json数据,并且将其中的不一致信息反馈给用户。工具还内置了很多配置可以来控制对比过程中的行为。目前已经补充大量单测,稳定性还是比较好的。

二、背景

公司最近在重构一个核心系统,至于为什么重构原因很多,就不说明了。但是这个核心系统承载较多的线上业务。为了不影响依赖依赖该服务的应用,所以我们重构的最核心就是完全兼容老系统接口。

为了保证平滑上线,并且测试新系统与老系统是否一致,我们决定系统并行一段时间,并且在这段时间之中验证新接口对老接口的兼容性。我们新起一个代理服务,他会将我们的用户流量分别转发到新老接口,然后拿到两个结果,将老接口结果直接返回;异步去比较新老结果是否符合预期,进行记录或者报警。

这样系统在经过一段时间的测试,稳定性更高,出错的概率更小。

图片

因为系统都是采用http接口对外提供服务,且返回数据格式统一的是json格式。所以我们急需一款强大的Java语言的Json对比工具来帮助我们发现新老系统的不兼容之处。

三、工具介绍

1. 介绍

json-diff 是一款功能强大的json差异发现工具,支持任何结构的json对比,并且可以将对比结果返给用户。目前该工具更新到了 3.0.0-RC1-RELEASE 版本。最新版可以查看 版本列表 。建议使用最新版,旧版可能存在缺陷。

优点:

  • 轻量级:工具只依赖 fastjson2
  • 精准定位:可以返回最精准且详细的信息
  • 功能全面:几乎覆盖任何json结构
  • 高性能

2. 使用教程

2.1 快速开始

  • 引入依赖
<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 则是对比信息。

2.2 更多配置

配置 类型 备注
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 进行开启唯一配置

3. 进阶

3.1. 全局使用固定配置

由于在设计中考虑到各线程比较配置相互独立。所以默认将配置防止在 ThreadLocal 中进行存储。但在大多数情况下,我们在全局比较时,配置并不会发生变化。

工具提供了全局配置方式。采用的方式是静态类属性。这样也会获得更好的性能。

// 开启并设置全局配置
JsonDiffOption.openUniqueOption();
JsonDiffOption.setGloballyUniqueOption(new JsonComparedOption());
// 不想使用时可以调用调整回线程独有模式
 JsonDiffOption.closeUniqueOption();

3.2. 数组元素为对象关联

当我们在遇到数组元素是一个对象时。如下:

[
    {
        "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 正常用法。配置如上。

3.3. 字段映射

在比较两个对象时。也许由于字段名变更。导致校验不通过。这时可以使用 mapping 配置。将 真实字段名称映射至期望字段名称。在比较过程中会将

actual.mappingKey 与 expect.mappingValue 认为是应该比较的对象。具体配置如下

// mapping key 是 actual 键名
// mapping value 是 expect 键名
HashMap<String, String> mapping = new HashMap<>();
mapping.put("date", "sunrise");

3.4. 字段忽略

如果有一些字段是想在整个json都进行忽略的,可以使用 ignoreKey 进行全局忽略。当然如果不想全局忽略,但是配置了该项,还是会被忽略掉。

HashSet<String> ignoreKey = new HashSet<>();
ignoreKey.add("sunrise");
ignoreKey.add("high");

3.5 自定义比较器

在我们一个大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 之后,根据不再接管比较操作。一切行为由用户自行定义。但工具依然预留默认的比较器给用户处理后续字段。这需要用户自行进行组合调用。

4.其他说明

前面提到工具几乎可以支持所有json结果的对比校验,并且发现差异。那它到底可以支持哪些呢,不知道是否符合你的需求呢?

  • 对象 ✅

    这是最简单的数据结构了,其中元素都以key-value构成,key是字符串,value可以是任何数据结构。

  • 数组 ✅

    支持严格顺序对比和忽略顺序对比,可以细化数组元素的类型

    • 基本类型 ✅

    • 对象类型 ✅

      该类型在对比时,可以通过ignorePath参数进行元素是否进行比较,将不关心的元素忽略掉。当然ignoreKey也可以,但其是全局生效

    • 数组类型 ✅

      元素也是数组,这样就形成了多维数组,工具理论上来说支持n维数组的对比

    • 元素类型不统一 ✅

      数组中,类型可能包含前面三种类型,这时工具会按照类型分类进行匹配,最后找不到的元素再反馈给用户。

由于json结构在单个看来,就只有对象和数组两种类型,该工具完美支持了所有类型。

四、其他

1. 交流群

交流群: 710435809

2. 测试用例

目前工具测试覆盖率已经达到80%。剩余测试用例正在补全中,完全可用于生产环境。

本文由博客一文多发平台 OpenWrite 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容