如何比较这个json字符串(不限定层级,属性,集合数组等顺序下)的一致性

582 阅读3分钟

作者:独钓寒江雪
一剑一酒一江湖, 便是此生
心中有图,何必点灯。装载请注明出处


image.png

Flowable传送门

最近休假在家。小无聊。一直在实践Quarkus如何实际运用到我们的生产上。真是收益匪浅。 后续更新个Quarkus的专栏。写的Flowable也继续更新下。前段时间一直在忙着几个项目和自身Framework的升级,空下来好好搞一搞。

闲话不说,进入主题。写这个文章呢,主要是前几天一个朋友,问我如何比较一个复杂的json字符串的一致性,看样子应该是内部做升级比对用的。刚听到的时候以为很简单啊,上代码。 如下2段json比较。

{
	"group": "testGroup",
	"school": "",
	"students": [
                {
			"id": 1,
			"name": "abc",
			"age": 23,
			"activeFlag": false
                },
		{
			"id": 2,
			"name": "efg",
			"age": 26,
			"activeFlag": true
		},
		{
			"id": 3,
			"name": "rst",
			"age": 26,
			"activeFlag": false
		}
	]
}
{
	"group": "testGroup",
	"school": "",
	"students": [
                {
			"id": 2,
			"name": "efg",
			"age": 26,
			"activeFlag": true
		},
                {
			"id": 1,
			"name": "abc",
			"age": 23,
			"activeFlag": false
                },
		{
			"id": 3,
			"name": "rst",
			"age": 26,
			"activeFlag": false
		}
	]
}

初版天真代码

脑子里第一思路就是Jackson转成JsonNode直接equals一下不就好了。但是事实却是完全不一样。

public class Test {

    public static void main(String[] args) {
        try {
            String jsonPath1 = Test.class.getResource("/json1").getPath();
            String jsonPath2 = Test.class.getResource("/json2").getPath();
            File file1 = new File(jsonPath1);
            File file2 = new File(jsonPath2);

            List<String> json1 = Files.readLines(file1, Charset.defaultCharset());
            List<String> json2 = Files.readLines(file2, Charset.defaultCharset());

            String jsonStr1 = StringUtils.join(json1, "");
            String jsonStr2 = StringUtils.join(json2, "");

            ObjectMapper objectMapper = getObjectMapper();

            JsonNode doc1 = objectMapper.readTree(jsonStr1);
            JsonNode doc2 = objectMapper.readTree(jsonStr2);

            System.out.println(doc1.equals(doc2));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }



    public static ObjectMapper getObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        objectMapper.enable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME);
        objectMapper.setTimeZone(TimeZone.getDefault());
        return objectMapper;
    }

}

结果呢。直接输出了false。从我一开始的理解上来说呢,不应该啊。想着跟顺序有关。就去看了下源码,果然发现是和顺序有关系的。 image.png

看到equals方法,可以重写个Comparator进去。进行比较,结果呢,还是太天真了。代码被我删除了。大家看上面的源码也就知道了,Comparator只对ValueNode的子类生效如BooleanNode,TextNode,NumberNode等基础节点的比较进行接管。 此处真想喷一下坑爹的百度和一些作者,太基础了,每次都是一些误导人的博文。 很多写了一堆就很单纯的一个array的json,然后去处理他的顺序,但是很多时候上,我们都是对象包含数组的,而非一个简单的数组。下面这篇stackoverflow上的讨论和我自己本身测试也证实了说法。当Arraynode inside of ObjectNode的时候,Comprator是失效的。

最终代码

一开始我还是不放弃,去github上找相应的issue看有没人处理,没人反馈的话,准备提个Issue的(现在为止提了不少开源的PR。此处装个B),后来发现了这么一个对话,作者认为顺序对于数据来说是至关重要的,所以不打算进行此处的处理。

如下代码,主要就是对ObjectNode做递归处理,接管掉ArrayNode的比较。进行不关心顺序的比较。其他还是交给本身的equals进行比较。

public class Test {

    public static void main(String[] args) {
        try {
            String jsonPath1 =  Test.class.getResource("/json1").getPath();
            String jsonPath2 =  Test.class.getResource("/json2").getPath();
            File file1 = new File(jsonPath1);
            File file2 = new File(jsonPath2);

            List<String> json1 = Files.readLines(file1, Charset.defaultCharset());
            List<String> json2 = Files.readLines(file2, Charset.defaultCharset());

            String jsonStr1 = StringUtils.join(json1, "");
            String jsonStr2 = StringUtils.join(json2, "");

            ObjectMapper objectMapper = getObjectMapper();
            JsonNode doc1 = objectMapper.readTree(jsonStr1);
            JsonNode doc2 = objectMapper.readTree(jsonStr2);

            System.out.println(equalsJsonNode(doc1, doc2));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 比较2个arrayNode是否相等
     * 和Jackson本身携带的ArrayNode.equals()方法比,此处不考虑arrayNode里元素的顺序,只要互相包含即可。
     *
     * @param arrayNode
     * @param compareArrayNode
     * @return
     */
    private static boolean compareArrayNode(ArrayNode arrayNode, ArrayNode compareArrayNode) {
        boolean compareFlag = true;
        for(int i = 0; i < arrayNode.size(); i++) {
            boolean compareNodeFlag = false;
            JsonNode o1Node = arrayNode.get(i);
            for(int j = 0; j < compareArrayNode.size(); j++) {
                JsonNode o2Node = compareArrayNode.get(j);
                if (equalsJsonNode(o1Node, o2Node)) {
                    compareNodeFlag = true;
                    break;
                }
            }
            if (!compareNodeFlag) {
                compareFlag = false;
                break;
            }

        }
        return compareFlag;
    }

    private static boolean equalsJsonNode(JsonNode node, JsonNode compareNode) {
        return equalsJsonNode(null, node, compareNode);
    }

    private static boolean equalsJsonNode(Comparator comparator, JsonNode node, JsonNode compareNode) {
        boolean compareFlag = true;
        if (!node.getClass().equals(compareNode.getClass())) {
            return false;
        }
        if (node.size() != compareNode.size()) {
            return false;
        }
        if (node instanceof ArrayNode) {
            compareFlag = compareArrayNode((ArrayNode) node, (ArrayNode) compareNode);
        } else if (node instanceof ValueNode) {
            if (comparator != null) {
                compareFlag = node.equals(comparator, compareNode);
            } else {
                compareFlag = node.equals(compareNode);
            }
        } else if (node instanceof ObjectNode) {
            Iterator<Map.Entry<String, JsonNode>> nodeIterator = node.fields();
            while (nodeIterator.hasNext() && compareFlag) {
                Map.Entry<String, JsonNode> entry = nodeIterator.next();

                JsonNode elementNode = entry.getValue();
                JsonNode compareElementNode = compareNode.get(entry.getKey());
                compareFlag = equalsJsonNode(elementNode, compareElementNode);
            }
        }
        return compareFlag;
    }

    public static ObjectMapper getObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        objectMapper.enable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME);
        objectMapper.setTimeZone(TimeZone.getDefault());
        return objectMapper;
    }

}

这种比较就根据各自业务来比较了,看各位看官的比较上是否需要考虑其顺序性相关进行比较了。

image.png