前言
“镜” 寓意是凡事都有两面性,Json对比也不例外!
因公司业务功能当中有一个履历的功能,它有多个版本的JSON数据需要对比出每个版本的不同差异节点并且将差异放置在一个新的JSON当中原有结构不能变动,差异节点使用数组对象的形式存储,前端点击标红即可显示多个版本的节点差异数据如下图
示例
代码语言:json复制// JSON One
{
"employee":
{
"id": "1212",
"fullName":"John Miles",
"age": 34,
"contact":
{
"email": "john@xyz.com",
"phone": "9999999999"
}
}
}
// Json Two
{
"employee":
{
"id": "1212",
"ae86": "12162",
"age": 34,
"fullName": "John Miles111",
"contact":
{
"email": "john@xyz.com",
"phone": "我是改了的",
"668": "999999991199"
}
}
}
可以看到
employee.ae86
是新增的。contact.668
也是新增的phone
字段是修改了的
对比后的Json
代码语言:json复制// 获取差异的节点 使用数组对象表示
{
"employee/fullName/": [{
"old": "John Miles"
}, {
"new": "John Miles111"
}],
"employee/contact/phone/": [{
"old": "9999999999"
}, {
"new": "我是改了的"
}],
"employee/contact/668": [{
"new": "999999991199"
}],
"employee/ae86": [{
"new": "12162"
}]
}
// 将差异节点的数据覆盖上去
{
"employee" : {
"id" : "1212",
"fullName" : [ {
"old" : "John Miles"
}, {
"new" : "John Miles111"
} ],
"age" : 34,
"contact" : {
"email" : "john@xyz.com",
"phone" : [ {
"old" : "9999999999"
}, {
"new" : "我是改了的"
} ],
"668" : [ {
"new" : "999999991199"
} ]
},
"ae86" : [ {
"new" : "12162"
} ]
}
}
实现
一、得到差异点Map
代码语言:java复制package com.yby6;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.ObjectUtils;
import java.io.IOException;
import java.util.*;
/**
* @author Yang Shuai
* Create By 2023/8/26
*/
public class JsonComparerUtils2 {
static ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) {
String s1 = "{ n"
" "employee":n"
" {n"
" "id": "1212",n"
" "fullName":"John Miles",n"
" "age": 34,n"
" "contact":n"
" {n"
" "email": "john@xyz.com",n"
" "phone": "9999999999"n"
" }n"
" }n"
"}";
String s2 = "{n"
" "employee":n"
" {n"
" "id": "1212",n"
" "ae86": "12162",n"
" "age": 34,n"
" "fullName": "John Miles111",n"
" "contact":n"
" {n"
" "email": "john@xyz.com",n"
" "phone": "我是改了的",n"
" "668": "999999991199"n"
" }n"
" }n"
"}";
try {
// 将json转Json节点树
JsonNode node1 = mapper.readTree(s1);
JsonNode node2 = mapper.readTree(s2);
List<String> ignoreKey = new ArrayList<>();
// 获取两个JSON之间的差异
Map<String, Object> nodesDiff = getNodesDiff(node1, node2,
"", ignoreKey);
System.out.println(mapper.writeValueAsString(nodesDiff));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 得到节点差异
*
* @param node1 node1
* @param node2 node2
* @param path 路径
* @param ignoreKey 忽略关键
* @return {@link Map}<{@link String}, {@link Object}>
*/
private static Map<String, Object> getNodesDiff(JsonNode node1, JsonNode node2, String path, List<String> ignoreKey) {
Map<String, Object> diff = new LinkedHashMap<>();
String[] split = path.split("/");
String filed = split[split.length - 1];
if (!node1.getNodeType().equals(node2.getNodeType())) {
addToMap(path, node1, node2, diff, "update");
} else {
switch (node1.getNodeType()) {
case OBJECT:
if (node1.isObject() && !node1.isEmpty()) {
for (Iterator<String> it = node1.fieldNames(); it.hasNext(); ) {
String fieldName = it.next();
JsonNode childNode1 = node1.get(fieldName);
JsonNode childNode2 = node2.get(fieldName);
// 忽略指定字段不对比
if (ignoreKey.contains(fieldName)) {
continue;
}
if (childNode2 != null) {
Map<String, Object> nestedDiff = getNodesDiff(childNode1, childNode2, path fieldName "/", ignoreKey);
if (!nestedDiff.isEmpty()) {
diff.putAll(nestedDiff);
}
} else {
// 旧的存在新的则不存在表示删除
addToMap(path fieldName, childNode1, childNode1, diff, "delete");
}
}
for (Iterator<String> it = node2.fieldNames(); it.hasNext(); ) {
String fieldName = it.next();
if (ignoreKey.contains(fieldName)) {
continue;
}
// 如果旧的没有这个数据那么表示新增
if (node1.get(fieldName) == null) {
addToMap(path fieldName, null, node2.get(fieldName), diff, "add");
}
}
}
break;
case ARRAY:
// 判断两个数组的长度不一样则需要将两个数组的长度补齐
if (node1.size() > 0 && node2.size() > 0 && node1.size() != node2.size()) {
try {
String m1 = mapper.writeValueAsString(node1);
String m2 = mapper.writeValueAsString(node2);
List list1 = mapper.readValue(m1, List.class);
List list2 = mapper.readValue(m2, List.class);
if (list1.size() > list2.size()) {
for (int i = list2.size(); i < list1.size(); i ) {
String o = mapper.writeValueAsString(list1.get(i));
JsonNode jsonNode = mapper.readTree(o);
// 清空的
clearNodeValues(jsonNode, ignoreKey);
// 将jsonNode2添加到jsonNode1中
((ArrayNode) node2).add(jsonNode);
}
} else {
for (int i = list1.size(); i < list2.size(); i ) {
String o = mapper.writeValueAsString(list2.get(i));
JsonNode jsonNode = mapper.readTree(o);
// 清空的
clearNodeValues(jsonNode, ignoreKey);
((ArrayNode) node1).add(jsonNode);
}
}
// 排序数组
List<JsonNode> firstList = mapper.readValue(node1.traverse(), new TypeReference<List<JsonNode>>() {
});
List<JsonNode> secondList = mapper.readValue(node2.traverse(), new TypeReference<List<JsonNode>>() {
});
// 补齐后递归对比
for (int i = 0; i < firstList.size(); i ) {
Map<String, Object> nestedDiff = getNodesDiff(firstList.get(i), secondList.get(i), path "[" i "]/", ignoreKey);
if (!nestedDiff.isEmpty()) {
diff.putAll(nestedDiff);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
// 判断数组里面是不是对象
if (node1.size() > 0 && node1.get(0).getNodeType().equals(JsonNodeType.OBJECT)) {
for (int i = 0; i < node1.size(); i ) {
if (ignoreKey.contains(filed)) {
break;
}
Map<String, Object> nestedDiff = getNodesDiff(node1.get(i), node2.get(i), path "[" i "]/", ignoreKey);
if (!nestedDiff.isEmpty()) {
diff.putAll(nestedDiff);
}
}
} else {
if (!node1.equals(node2)) {
if (ignoreKey.contains(filed)) {
break;
}
addToMap(path, node1, node2, diff, "update");
}
}
}
break;
case STRING:
case BOOLEAN:
case NUMBER:
if (ignoreKey.contains(filed)) {
break;
}
// 如果新的为空则为删除
if (ObjectUtils.isEmpty(node2)) {
addToMap(path, node1, node2, diff, "delete");
}
if (!node1.equals(node2)) {
addToMap(path, node1, node2, diff, "update");
}
break;
default:
throw new IllegalArgumentException("Unsupported JSON type:" node1.getNodeType().name());
}
}
return diff;
}
/**
* 清空节点参数
*
* @param node 节点
*/
private static void clearNodeValues(JsonNode node, List<String> ignoreKey) {
// 忽略部分清空
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
objectNode.fields().forEachRemaining(entry -> {
if (!ignoreKey.contains(entry.getKey())) {
objectNode.replace(entry.getKey(), null);
}
});
} else if (node.isArray()) {
for (JsonNode childNode : node) {
clearNodeValues(childNode, ignoreKey);
}
}
}
/**
* 将两个json的差异添加到Map
*
* @param path 路径
* @param oldValue 旧值
* @param newValue 新值
* @param diff diff
*/
private static void addToMap(String path, JsonNode oldValue, JsonNode newValue, Map<String, Object> diff, String diffType) {
List<Object> values = new ArrayList<>();
HashMap<String, Object> map = new HashMap<>();
map.put("old", oldValue != null ? getContent(oldValue) : "");
map.put("new", newValue != null ? getContent(newValue) : "");
map.put("diffType", diffType);
values.add(map);
diff.put(path, values.toArray(new Object[0]));
}
/**
* 获取内容
*
* @param node 节点
* @return {@link Object}
*/
private static Object getContent(JsonNode node) {
if (node.isBoolean()) {
return node.asBoolean();
} else if (node.isNumber()) {
return node.asInt();
} else if (node.isTextual()) {
return node.asText();
} else if (node.isObject()) {
Map<String, Object> obj = new LinkedHashMap<>();
for (Iterator<String> it = node.fieldNames(); it.hasNext(); ) {
String propName = it.next();
obj.put(propName, getContent(node.get(propName)));
}
return obj;
} else if (node.isArray()) {
ArrayNode array = (ArrayNode) node;
Object[] contents = new Object[array.size()];
for (int i = 0; i < array.size(); i ) {
contents[i] = getContent(array.get(i));
}
return contents;
} else if (node.isNull()) {
return null;
} else {
throw new UnsupportedOperationException("不支持的 JSON 类型:" node.getNodeType().name());
}
}
测试输出
代码语言:json复制{
"employee/fullName/": [{
"new": "John Miles111",
"old": "John Miles",
"diffType": "update"
}],
"employee/contact/phone/": [{
"new": "我是改了的",
"old": "9999999999",
"diffType": "update"
}],
"employee/contact/668": [{
"new": "999999991199",
"old": "",
"diffType": "add"
}],
"employee/ae86": [{
"new": "12162",
"old": "",
"diffType": "add"
}]
}
获取差异代码讲解
这段代码是一个处理两个 JSON 节点之间差异的方法,以及一些辅助方法。下面我将解释每个方法的作用和代码逻辑:
getNodesDiff
方法
描述
该方法用于比较两个 JSON 节点(node1
和 node2
)之间的差异,包括子节点差异,并返回一个表示差异的 Map
。
方法签名
代码语言:java复制private static Map<String, Object> getNodesDiff(JsonNode node1, JsonNode node2, String path, List<String> ignoreKey)
代码解释
diff
是一个用于存储差异的LinkedHashMap
。- 首先,它根据路径
path
中的最后一个部分(field
)来确定节点的类型。 - 然后,它检查
node1
和node2
的节点类型是否相同,如果不同,将差异添加到diff
中。 - 如果节点类型相同,则根据节点类型进行处理,包括对象、数组、字符串、布尔值和数字类型。
- 对于对象类型,它递归地比较对象的字段,同时考虑了一些特殊情况,例如忽略指定的字段和
isValid
字段为 0 的情况。 - 对于数组类型,它首先检查数组长度是否不一致,如果不一致,则尝试将两个数组的长度补齐,然后递归比较数组元素。如果数组元素是对象类型,也会递归比较对象。
- 对于其他基本数据类型,它会直接比较节点的值,如果不同,将差异添加到
diff
中。
clearNodeValues
方法
描述
这是一个辅助方法,用于清空节点的值,但保留节点结构。
方法签名
代码语言:java复制private static void clearNodeValues(JsonNode node, List<String> ignoreKey)
代码解释
- 如果节点是对象类型,则清空对象中指定的字段,但忽略
ignoreKey
中的字段。 - 如果节点是数组类型,则递归地清空数组元素的值,但保留数组结构。
addToMap
方法
描述
这是一个辅助方法,用于将差异信息添加到差异 Map
中。
方法签名
代码语言:java复制private static void addToMap(String path, JsonNode oldValue, JsonNode newValue, Map<String, Object> diff, String diffType)
代码解释
- 该方法将差异信息以指定的格式添加到
diff
中,包括路径path
、旧值oldValue
、新值newValue
和差异类型diffType
。
getContent
方法
描述
这是一个辅助方法,用于从 JsonNode
中提取内容。
方法签名
代码语言:java复制private static Object getContent(JsonNode node)
代码解释
- 该方法根据
JsonNode
的类型提取内容,可能是布尔值、整数、字符串、对象、数组或 null 值。 - 对于对象和数组类型,它递归提取内容并返回。
二、合并
代码语言:json复制 /**
* 将差异应用到指定的 JSON 字符串,并返回处理后的字符串。
*
* @param json 要应用差异的原始 JSON 字符串
* @param diff 差异内容,即 {@link #getNodesDiff} 返回的 Map 对象
* @return 经过差异处理后的 JSON 字符串
*/
public static String applyDiff(String json, Map<String, Object> diff) throws IOException {
JsonNode node = mapper.readTree(json);
for (Map.Entry<String, Object> entry : diff.entrySet()) {
String[] path = entry.getKey().split("/");
JsonNode parentNode = node;
for (int i = 0; i < path.length - 1; i ) {
// 如果是null则跳过
if (parentNode == null) {
continue;
}
// 如果该节点是数组那么解析一下
if (parentNode.isArray()) {
int index = getIndexFromPath(path[i]);
parentNode = parentNode.get(index);
} else {
parentNode = parentNode.get(path[i]);
}
}
// 如果拿到的父节点是null则跳过
if (parentNode == null) {
continue;
}
String propertyName = path[path.length - 1];
JsonNode childNode = parentNode.get(propertyName);
if (entry.getValue() == null) {
if (parentNode.isArray()) {
((ArrayNode) parentNode).remove(Integer.parseInt(propertyName.substring(1, propertyName.length() - 1)));
} else {
((ObjectNode) parentNode).remove(propertyName);
}
} else {
Object value = entry.getValue();
// 是否是数组
if (ArrayUtil.isArray(value)) {
// ArrayNode arrayNode = mapper.createArrayNode(); // 新建一个空的数组节点
// arrayNode.addPOJO(value);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode arrayNode = objectMapper.valueToTree(value);
// Object[] arr = (Object[]) value;
// for (Object item : arr) {
// if (item != null) {
// // 将数组元素依次加入新建的数组节点中,不需要处理逗号问题
// arrayNode.addPOJO(item);
// }
// }
if (childNode != null && !childNode.isMissingNode()) { // 已经存在该属性,需要替换
((ObjectNode) parentNode).replace(propertyName, arrayNode);
} else { // 不存在该属性,直接应用差异
// 如果父节点是数组,在数组末尾添加新元素
// 如果父节点是对象,在该对象中添加新属性,值为空
if (parentNode.isArray()) {
int position = 0;
if (StringUtils.isNotBlank(propertyName)) {
position = Integer.parseInt(propertyName.substring(1, propertyName.length() - 1));
}
while (position > parentNode.size()) {
((ArrayNode) parentNode).add(mapper.createObjectNode().put("", ""));
}
((ArrayNode) parentNode).add(arrayNode);
} else {
((ObjectNode) parentNode).set(propertyName, arrayNode);
}
}
} else {
String newValue = entry.getValue().toString();
if (childNode == null || childNode.isNull() || childNode.isMissingNode()) {
if (parentNode.isArray()) { // 如果父节点是数组,在数组末尾添加新元素
((ArrayNode) parentNode).add(mapper.createObjectNode().put(propertyName, ""));
} else { // 如果父节点是对象,在该对象中添加新属性,值为空
((ObjectNode) parentNode).put(propertyName, "");
}
childNode = parentNode.get(propertyName);
}
if (childNode.isValueNode()) {
if (childNode.isBoolean()) {
((ObjectNode) parentNode).put(propertyName, Boolean.parseBoolean(newValue));
} else if (childNode.isIntegralNumber()) {
((ObjectNode) parentNode).put(propertyName, newValue);
} else if (childNode.isFloatingPointNumber()) {
((ObjectNode) parentNode).put(propertyName, Double.parseDouble(newValue));
} else if (childNode.isTextual()) {
try {
((ObjectNode) parentNode).put(propertyName, newValue.substring(1, newValue.length() - 1)); // 去掉 JSON 字符串外层的双引号
} catch (Exception e) {
((ObjectNode) parentNode).put(propertyName, newValue);
}
}
} else {
((ObjectNode) parentNode).set(propertyName, mapper.readTree(newValue));
}
}
}
}
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(node);
}
/**
* 移除方括号并将剩余字符串解析为整数索引
*
* @param path 路径
* @return int
*/
private static int getIndexFromPath(String path) {
return Integer.parseInt(path.substring(1, path.length() - 1));
}
测试差异应用
代码语言:java复制{
"employee": {
"id": "1212",
"ae86": [{
"new": "12162",
"old": "",
"diffType": "add"
}],
"age": 34,
"fullName": [{
"new": "John Miles111",
"old": "John Miles",
"diffType": "update"
}],
"contact": {
"email": "john@xyz.com",
"phone": [{
"new": "我是改了的",
"old": "9999999999",
"diffType": "update"
}],
"668": [{
"new": "999999991199",
"old": "",
"diffType": "add"
}]
}
}
}
差异应用代码讲解
applyDiff
方法
描述
该方法将差异应用到指定的 JSON 字符串,并返回处理后的字符串。它接受一个原始的 JSON 字符串和一个差异的 Map,通常是从 getNodesDiff
方法获取的。
方法签名
代码语言:java复制public static String applyDiff(String json, Map<String, Object> diff) throws IOException
代码解释
- 该方法首先使用 Jackson ObjectMapper
mapper
将输入的 JSON 字符串json
解析为一个JsonNode
对象。 - 遍历差异的 Map 中的每个条目,每个条目表示要应用到 JSON 的变更。
- 对于每个条目,它通过 '/' 来分割条目的键(表示 JSON 内的路径),然后按照路径迭代 JSON 结构,更新当前节点指针。
- 如果父节点为 null 或缺失,会跳过当前迭代。
- 根据条目的值是否为 null,它要么移除一个节点,要么更新它:
- 如果值为 null,它会从 JSON 结构中移除节点。如果父节点是数组,则移除指定索引处的元素;否则,从对象中移除指定属性。
代码语言:txt复制- 如果值不为 null,它会检查值是否为数组。如果是数组,它会创建一个新的 JSON 数组节点,并根据属性是否已存在,要么替换要么添加到父节点中。如果值不是数组,则根据其类型(布尔值、数字、字符串或 JSON 对象)更新 JSON 结构中的属性。
- 最后,它使用
mapper
将修改后的JsonNode
转换回 JSON 字符串,并返回结果的 JSON 字符串。
getIndexFromPath
方法
描述
这是一个私有的实用方法,用于移除字符串中的方括号,并将剩余的字符串解析为整数索引。
方法签名
代码语言:java复制private static int getIndexFromPath(String path)
代码解释
- 该方法以一个
path
字符串作为输入。 - 它移除
path
字符串的首尾字符(假设它们是方括号),然后将剩余的子串解析为整数索引。 - 解析后的整数索引被返回。
最后
本期结束咱们下次再见