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. 比较Map
s
现在我们已经将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>
现在,让我们看看如何比较Map
s的代码:
@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
中的值。运行测试后,我们会看到Map
s之间的不同键及其值:
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}]
可以看到,这显示了cars
、address
和children
字段不同,并列出了差异。然而,它并未指出哪些嵌套字段导致这些差异。例如,它没有指出address
对象中的street
字段不同。
3.2. 展平Map
s
为了精确指出嵌套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.street
和address.city
。 - 接下来,我们可以检查值是否是
List
(JSON数组)。如果列表包含基本类型值,我们将键和值添加到展平的Map
中。 - 如果列表包含对象,我们将递归调用
flatten()
方法,参数为每个对象。例如,children
数组将展平为children\[0\].name
、children\[0\].age
、children\[1\].name
和children\[1\].age
。 - 如果值既不是
Map
也不是List
,我们将键和值添加到展平的Map
中。
这将递归执行,直到达到Map
的最底层。此时,我们将有一个展平后的Map
,其中每个键都是值的路径。
3.3. 测试
现在我们展平了Map
s,让我们看看如何使用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()
方法进行比较。我们也了解了如何展平Map
s以便于比较嵌套的Map
s。
如往常一样,本文的所有代码可在GitHub上找到。