前言
“鏡” 寓意是凡事都有兩面性,Json對比也不例外!
因公司業(yè)務(wù)功能當(dāng)中有一個履歷的功能,它有多個版本的JSON數(shù)據(jù)需要對比出每個版本的不同差異節(jié)點并且將差異放置在一個新的JSON當(dāng)中原有結(jié)構(gòu)不能變動,差異節(jié)點使用數(shù)組對象的形式存儲,前端點擊標(biāo)紅即可顯示多個版本的節(jié)點差異數(shù)據(jù)如下圖
示例
// 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
// 獲取差異的節(jié)點 使用數(shù)組對象表示
{
"employee/fullName/": [{
"old": "John Miles"
}, {
"new": "John Miles111"
}],
"employee/contact/phone/": [{
"old": "9999999999"
}, {
"new": "我是改了的"
}],
"employee/contact/668": [{
"new": "999999991199"
}],
"employee/ae86": [{
"new": "12162"
}]
}
// 將差異節(jié)點的數(shù)據(jù)覆蓋上去
{
"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"
} ]
}
}
實現(xiàn)
一、得到差異點Map
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轉(zhuǎn)Json節(jié)點樹
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);
}
}
/**
* 得到節(jié)點差異
*
* @param node1 node1
* @param node2 node2
* @param path 路徑
* @param ignoreKey 忽略關(guān)鍵
* @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;
}
// 如果舊的沒有這個數(shù)據(jù)那么表示新增
if (node1.get(fieldName) == null) {
addToMap(path + fieldName, null, node2.get(fieldName), diff, "add");
}
}
}
break;
case ARRAY:
// 判斷兩個數(shù)組的長度不一樣則需要將兩個數(shù)組的長度補(bǔ)齊
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添加到j(luò)sonNode1中
((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);
}
}
// 排序數(shù)組
List<JsonNode> firstList = mapper.readValue(node1.traverse(), new TypeReference<List<JsonNode>>() {
});
List<JsonNode> secondList = mapper.readValue(node2.traverse(), new TypeReference<List<JsonNode>>() {
});
// 補(bǔ)齊后遞歸對比
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 {
// 判斷數(shù)組里面是不是對象
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;
}
/**
* 清空節(jié)點參數(shù)
*
* @param node 節(jié)點
*/
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]));
}
/**
* 獲取內(nèi)容
*
* @param node 節(jié)點
* @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());
}
}
測試輸出
{
"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 節(jié)點之間差異的方法诽偷,以及一些輔助方法坤学。下面我將解釋每個方法的作用和代碼邏輯:
getNodesDiff
方法
描述
該方法用于比較兩個 JSON 節(jié)點(node1
和 node2
)之間的差異,包括子節(jié)點差異报慕,并返回一個表示差異的 Map
深浮。
方法簽名
private static Map<String, Object> getNodesDiff(JsonNode node1, JsonNode node2, String path, List<String> ignoreKey)
代碼解釋
-
diff
是一個用于存儲差異的LinkedHashMap
。 - 首先眠冈,它根據(jù)路徑
path
中的最后一個部分(field
)來確定節(jié)點的類型飞苇。 - 然后,它檢查
node1
和node2
的節(jié)點類型是否相同蜗顽,如果不同布卡,將差異添加到diff
中。 - 如果節(jié)點類型相同雇盖,則根據(jù)節(jié)點類型進(jìn)行處理忿等,包括對象、數(shù)組崔挖、字符串贸街、布爾值和數(shù)字類型庵寞。
- 對于對象類型,它遞歸地比較對象的字段薛匪,同時考慮了一些特殊情況皇帮,例如忽略指定的字段和
isValid
字段為 0 的情況。 - 對于數(shù)組類型蛋辈,它首先檢查數(shù)組長度是否不一致,如果不一致将谊,則嘗試將兩個數(shù)組的長度補(bǔ)齊冷溶,然后遞歸比較數(shù)組元素。如果數(shù)組元素是對象類型尊浓,也會遞歸比較對象逞频。
- 對于其他基本數(shù)據(jù)類型,它會直接比較節(jié)點的值栋齿,如果不同苗胀,將差異添加到
diff
中。
clearNodeValues
方法
描述
這是一個輔助方法瓦堵,用于清空節(jié)點的值基协,但保留節(jié)點結(jié)構(gòu)。
方法簽名
private static void clearNodeValues(JsonNode node, List<String> ignoreKey)
代碼解釋
- 如果節(jié)點是對象類型菇用,則清空對象中指定的字段澜驮,但忽略
ignoreKey
中的字段。 - 如果節(jié)點是數(shù)組類型惋鸥,則遞歸地清空數(shù)組元素的值杂穷,但保留數(shù)組結(jié)構(gòu)。
addToMap
方法
描述
這是一個輔助方法卦绣,用于將差異信息添加到差異 Map
中耐量。
方法簽名
private static void addToMap(String path, JsonNode oldValue, JsonNode newValue, Map<String, Object> diff, String diffType)
代碼解釋
- 該方法將差異信息以指定的格式添加到
diff
中,包括路徑path
滤港、舊值oldValue
廊蜒、新值newValue
和差異類型diffType
。
getContent
方法
描述
這是一個輔助方法溅漾,用于從 JsonNode
中提取內(nèi)容劲藐。
方法簽名
private static Object getContent(JsonNode node)
代碼解釋
- 該方法根據(jù)
JsonNode
的類型提取內(nèi)容,可能是布爾值樟凄、整數(shù)聘芜、字符串、對象缝龄、數(shù)組或 null 值汰现。 - 對于對象和數(shù)組類型挂谍,它遞歸提取內(nèi)容并返回。
二瞎饲、合并
/**
* 將差異應(yīng)用到指定的 JSON 字符串口叙,并返回處理后的字符串。
*
* @param json 要應(yīng)用差異的原始 JSON 字符串
* @param diff 差異內(nèi)容嗅战,即 {@link #getNodesDiff} 返回的 Map 對象
* @return 經(jīng)過差異處理后的 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;
}
// 如果該節(jié)點是數(shù)組那么解析一下
if (parentNode.isArray()) {
int index = getIndexFromPath(path[i]);
parentNode = parentNode.get(index);
} else {
parentNode = parentNode.get(path[i]);
}
}
// 如果拿到的父節(jié)點是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();
// 是否是數(shù)組
if (ArrayUtil.isArray(value)) {
// ArrayNode arrayNode = mapper.createArrayNode(); // 新建一個空的數(shù)組節(jié)點
// arrayNode.addPOJO(value);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode arrayNode = objectMapper.valueToTree(value);
// Object[] arr = (Object[]) value;
// for (Object item : arr) {
// if (item != null) {
// // 將數(shù)組元素依次加入新建的數(shù)組節(jié)點中妄田,不需要處理逗號問題
// arrayNode.addPOJO(item);
// }
// }
if (childNode != null && !childNode.isMissingNode()) { // 已經(jīng)存在該屬性,需要替換
((ObjectNode) parentNode).replace(propertyName, arrayNode);
} else { // 不存在該屬性驮捍,直接應(yīng)用差異
// 如果父節(jié)點是數(shù)組疟呐,在數(shù)組末尾添加新元素
// 如果父節(jié)點是對象,在該對象中添加新屬性东且,值為空
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()) { // 如果父節(jié)點是數(shù)組启具,在數(shù)組末尾添加新元素
((ArrayNode) parentNode).add(mapper.createObjectNode().put(propertyName, ""));
} else { // 如果父節(jié)點是對象,在該對象中添加新屬性珊泳,值為空
((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);
}
/**
* 移除方括號并將剩余字符串解析為整數(shù)索引
*
* @param path 路徑
* @return int
*/
private static int getIndexFromPath(String path) {
return Integer.parseInt(path.substring(1, path.length() - 1));
}
測試差異應(yīng)用
{
"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"
}]
}
}
}
差異應(yīng)用代碼講解
applyDiff
方法
描述
該方法將差異應(yīng)用到指定的 JSON 字符串鲁冯,并返回處理后的字符串。它接受一個原始的 JSON 字符串和一個差異的 Map色查,通常是從 getNodesDiff
方法獲取的薯演。
方法簽名
public static String applyDiff(String json, Map<String, Object> diff) throws IOException
代碼解釋
- 該方法首先使用 Jackson ObjectMapper
mapper
將輸入的 JSON 字符串json
解析為一個JsonNode
對象。 - 遍歷差異的 Map 中的每個條目秧了,每個條目表示要應(yīng)用到 JSON 的變更涣仿。
- 對于每個條目,它通過 '/' 來分割條目的鍵(表示 JSON 內(nèi)的路徑)示惊,然后按照路徑迭代 JSON 結(jié)構(gòu)好港,更新當(dāng)前節(jié)點指針。
- 如果父節(jié)點為 null 或缺失米罚,會跳過當(dāng)前迭代钧汹。
- 根據(jù)條目的值是否為 null,它要么移除一個節(jié)點录择,要么更新它:
- 如果值為 null拔莱,它會從 JSON 結(jié)構(gòu)中移除節(jié)點。如果父節(jié)點是數(shù)組隘竭,則移除指定索引處的元素塘秦;否則,從對象中移除指定屬性动看。
- 如果值不為 null尊剔,它會檢查值是否為數(shù)組。如果是數(shù)組菱皆,它會創(chuàng)建一個新的 JSON 數(shù)組節(jié)點须误,并根據(jù)屬性是否已存在挨稿,要么替換要么添加到父節(jié)點中。如果值不是數(shù)組京痢,則根據(jù)其類型(布爾值奶甘、數(shù)字、字符串或 JSON 對象)更新 JSON 結(jié)構(gòu)中的屬性祭椰。
- 最后臭家,它使用
mapper
將修改后的JsonNode
轉(zhuǎn)換回 JSON 字符串,并返回結(jié)果的 JSON 字符串方淤。
getIndexFromPath
方法
描述
這是一個私有的實用方法钉赁,用于移除字符串中的方括號,并將剩余的字符串解析為整數(shù)索引臣淤。
方法簽名
private static int getIndexFromPath(String path)
代碼解釋
- 該方法以一個
path
字符串作為輸入。 - 它移除
path
字符串的首尾字符(假設(shè)它們是方括號)窃爷,然后將剩余的子串解析為整數(shù)索引邑蒋。 - 解析后的整數(shù)索引被返回。
最后
本期結(jié)束咱們下次再見??~
按厘,關(guān)注我不迷路医吊,如果本篇文章對你有所幫助,或者你有什么疑問逮京,歡迎在評論區(qū)留言卿堂,我一般看到都會回復(fù)的。大家點贊支持一下喲~ ??
【選題思路】
基于兩串不同的JSON數(shù)據(jù)進(jìn)行對比出來差異再將差異應(yīng)用到最新的Json字符串當(dāng)中.
【寫作提綱】
一懒棉、前言
因公司業(yè)務(wù)功能當(dāng)中有一個履歷的功能,它有多個版本的JSON數(shù)據(jù)需要對比出每個版本的不同差異節(jié)點并且將差異放置在一個新的JSON當(dāng)中原有結(jié)構(gòu)不能變動,差異節(jié)點使用數(shù)組對象的形式存儲,前端點擊標(biāo)紅即可顯示多個版本的節(jié)點差異數(shù)據(jù)
二草描、示例
介紹兩個Json的差異對比效果
三、實現(xiàn)
先得到兩個Json的差異節(jié)點集合策严、接著在最新的Json中轉(zhuǎn)換json節(jié)點對象進(jìn)行判斷每個節(jié)點的字段是否符合則插入到對應(yīng)的字段當(dāng)中!
本文由mdnice多平臺發(fā)布