1. 概述

在这个教程中,我们将探讨如何以不同的方式将JSON文档读取为Map并进行比较。我们还将研究在两个Map之间查找差异的方法。

2. 转换为Map

2.1. 使用Jackson

首先,我们会介绍几种将JSON文档转换为Map的方法。让我们先看看我们将用于测试的JSON对象。

创建一个名为first.json的文件,内容如下:

{
  "name": "John",
  "age": 30,
  "cars": [
    "Ford",
    "BMW"
  ],
  "address": {
    "street": "Second Street",
    "city": "New York"
  },
  "children": [
    {
      "name": "Sara",
      "age": 5
    },
    {
      "name": "Alex",
      "age": 3
    }
  ]
}

同样地,创建一个名为second.json的文件,内容如下:

{
  "name": "John",
  "age": 30,
  "cars": [
    "Ford",
    "Audi"
  ],
  "address": {
    "street": "Main Street",
    "city": "New York"
  },
  "children": [
    {
      "name": "Peter",
      "age": 5
    },
    {
      "name": "Cathy",
      "age": 10
    }
  ]
}

如上所述,上述JSON文档之间的差异有以下几点:

  • cars数组的值不同
  • address对象中street键的值不同
  • children数组存在多个差异

2.1.1. 使用Jackson

Jackson是一个流行的用于JSON操作的库。我们可以使用Jackson将JSON转换为Map

首先,添加Jackson依赖项:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>

现在,我们可以使用Jackson(参见:Jackson - 将JSON转换为Map)将JSON文档转换为Map

class JsonUtils {
    public static Map<String, Object> jsonFileToMap(String path) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        return mapper.readValue(new File(path), new TypeReference<Map<String, Object>>() {});
    }
}

这里,我们使用ObjectMapper类的readValue()方法将JSON文档转换为Map。它接受一个File对象和一个TypeReference对象作为参数。

2.1.2. 使用Gson

类似地,我们也可以使用Gson将JSON文档转换为Map。为此,我们需要添加Gson的依赖项:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>

现在来看看转换JSON的代码:

public static Map<String, Object> jsonFileToMapGson(String path) throws IOException {
    Gson gson = new Gson();
    return gson.fromJson(new FileReader(path), new TypeToken<Map<String, Object>>() {}.getType());
}

这里,我们使用Gson类的fromJson()方法将JSON文档转换为Map。它接受一个FileReader对象和一个TypeToken对象作为参数。

3. 比较Maps

现在我们已经将JSON文档转换为Map,接下来我们将探讨比较它们的不同方法。

3.1. 使用Guava的Map.difference()

Guava提供了Maps.difference()方法,可以用来比较两个Map。为了使用它,我们需要在项目中添加Guava依赖项:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.0.0-jre</version>
</dependency>

现在,让我们看看如何比较Maps的代码:

@Test
void givenTwoJsonFiles_whenCompared_thenTheyAreDifferent() throws IOException {
    Map<String, Object> firstMap = JsonUtils.jsonFileToMap("src/test/resources/first.json");
    Map<String, Object> secondMap = JsonUtils.jsonFileToMap("src/test/resources/second.json");

    MapDifference<String, Object> difference = Maps.difference(firstFlatMap, secondFlatMap);
    difference.entriesDiffering().forEach((key, value) -> {
        System.out.println(key + ": " + value.leftValue() + " - " + value.rightValue());
    });
    assertThat(difference.areEqual()).isFalse();
}

Guava只能比较一层Map,这对于嵌套的Map并不适用。

让我们看看如何比较上面的嵌套Map。我们使用entriesDiffering()方法获取Map之间的差异。这返回一个包含差异的Map,其中键是值的路径,值是MapDifference.ValueDifference对象。这个对象包含两个Map中的值。运行测试后,我们会看到Maps之间的不同键及其值:

cars: [Ford, BMW] - [Ford, Audi]
address: {street=Second Street, city=New York} - {street=Main Street, city=New York}
children: [{name=Sara, age=5}, {name=Alex, age=3}] - [{name=Peter, age=5}, {name=Cathy, age=10}]

可以看到,这显示了carsaddresschildren字段不同,并列出了差异。然而,它并未指出哪些嵌套字段导致这些差异。例如,它没有指出address对象中的street字段不同。

3.2. 展平Maps

为了精确指出嵌套Map之间的差异,我们需要展平Map,使得每个键都是值的路径。例如,address对象中的street键将展平为address.street,以此类推。

以下是实现这一功能的代码:

class FlattenUtils {
    public static Map<String, Object> flatten(Map<String, Object> map) {
        return flatten(map, null);
    }

    private static Map<String, Object> flatten(Map<String, Object> map, String prefix) {
        Map<String, Object> flatMap = new HashMap<>();
        map.forEach((key, value) -> {
            String newKey = prefix != null ? prefix + "." + key : key;
            if (value instanceof Map) {
                flatMap.putAll(flatten((Map<String, Object>) value, newKey));
            } else if (value instanceof List) {
                // check for list of primitives
                Object element = ((List<?>) value).get(0);
                if (element instanceof String || element instanceof Number || element instanceof Boolean) {
                    flatMap.put(newKey, value);
                } else {
                    // check for list of objects
                    List<Map<String, Object>> list = (List<Map<String, Object>>) value;
                    for (int i = 0; i < list.size(); i++) {
                        flatMap.putAll(flatten(list.get(i), newKey + "[" + i + "]"));
                    }
                }
            } else {
                flatMap.put(newKey, value);
            }
        });
        return flatMap;
    }
}

这里,我们使用递归来展平Map。对于任何字段,以下条件之一成立:

  • 值可能是一个Map(嵌套JSON对象)。在这种情况下,我们将递归调用flatten()方法,参数为值。例如,address对象将展平为address.streetaddress.city
  • 接下来,我们可以检查值是否是List(JSON数组)。如果列表包含基本类型值,我们将键和值添加到展平的Map中。
  • 如果列表包含对象,我们将递归调用flatten()方法,参数为每个对象。例如,children数组将展平为children\[0\].namechildren\[0\].agechildren\[1\].namechildren\[1\].age
  • 如果值既不是Map也不是List,我们将键和值添加到展平的Map中。

这将递归执行,直到达到Map的最底层。此时,我们将有一个展平后的Map,其中每个键都是值的路径。

3.3. 测试

现在我们展平了Maps,让我们看看如何使用Maps.difference()来比较它们:

@Test
void givenTwoJsonFiles_whenCompared_thenTheyAreDifferent() throws IOException {
    Map<String, Object> firstFlatMap = FlattenUtils.flatten(JsonUtils.jsonFileToMap("src/test/resources/first.json"));
    Map<String, Object> secondFlatMap = FlattenUtils.flatten(JsonUtils.jsonFileToMap("src/test/resources/second.json"));

    MapDifference<String, Object> difference = Maps.difference(firstFlatMap, secondFlatMap);
    difference.entriesDiffering().forEach((key, value) -> {
        System.out.println(key + ": " + value.leftValue() + " - " + value.rightValue());
    });
    assertThat(difference.areEqual()).isFalse();
}

再次打印出不同的键和值。这将得到以下输出:

cars: [Ford, BMW] - [Ford, Audi]
children[1].age: 3 - 10
children[1].name: Alex - Cathy
address.street: Second Street - Main Street
children[0].name: Sara - Peter

4. 总结

在这篇文章中,我们探讨了在Java中比较两个JSON文档的方法。我们研究了将JSON文档转换为Map的不同方式,然后使用Guava的Maps.difference()方法进行比较。我们也了解了如何展平Maps以便于比较嵌套的Maps。

如往常一样,本文的所有代码可在GitHub上找到。