1. 我们希望我的的对比工具能进行一些自定义配置
package local.my.demo_jdk.diff_json;
import java.util.*;
public class DiffCfg {
public boolean allowDiffStrAndNum = false;
public boolean allowDiffBoolAndStr = false;
public boolean allowEmptyAsNull = true;
public boolean ignoreCase = true;
public boolean snakeAsCamelCase = true;
public boolean smartListCompare = true;
public int scale = 16;
public final Map<String, List<DiffRule>> customRules = new HashMap<>(0);
public final Set<String> ignoredPaths = new HashSet<>(0);
public static DiffCfg ofStrict(){
var d = new DiffCfg();
d.allowDiffStrAndNum = false;
d.allowDiffBoolAndStr = false;
d.allowEmptyAsNull = false;
d.ignoreCase = false;
d.snakeAsCamelCase = false;
d.smartListCompare = false;
d.scale = 32;
return d;
}
}
2. 我们还有自定义对比规则
package local.my.demo_jdk.diff_json;
import cn.hutool.core.util.BooleanUtil;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
public interface DiffRule {
boolean math(Object expect, Object actual, String jsonPath, Object expectRoot, Object actualRoot);
static DiffRule ofScript(String script) {
return new DiffRuleByGroovy(script);
}
class DiffRuleByGroovy implements DiffRule {
static final GroovyShell shell = new GroovyShell();
private final String scriptText;
private Script script;
public DiffRuleByGroovy(String scriptText) {
this.scriptText = scriptText;
}
@Override
public boolean math(Object expect, Object actual, String jsonPath, Object expectRoot, Object actualRoot) {
if (script==null){
script = shell.parse(scriptText);
shell.resetLoadedClasses();
}
var b = new Binding();
b.setVariable("expect", expect);
b.setVariable("actual", actual);
b.setVariable("jsonPath", jsonPath);
b.setVariable("expectRoot", expectRoot);
b.setVariable("actualRoot", actualRoot);
script.setBinding(b);
Object s = script.run();
return switch (s) {
case null -> false;
case Boolean b1 -> b1;
case String s1 -> BooleanUtil.toBoolean(s1);
default -> throw new RuntimeException("script must return boolean or string");
};
}
}
}
3. 我们的对比结果结构要完善
package local.my.demo_jdk.diff_json;
import com.alibaba.fastjson2.annotation.JSONField;
import java.util.ArrayList;
import java.util.List;
public class DiffNode {
public String path;
public Object expect;
public Object actual;
public List<DiffNode> children;
public DiffRes res;
public String note;
@JSONField(serialize = false)
public List<DiffRule> diffRule;
public String actualKey;
public Integer actualIndex;
public Boolean parsedJson;
public Integer keyNameEqCount;
public DiffType diffType;
public enum DiffType {
OBJECT,
ARRAY,
STRING,
NUMBER,
BOOLEAN,
NULL;
}
@JSONField(serialize = false)
public boolean isSame(){
return res == DiffRes.SAME || res == DiffRes.IGNORED;
}
public List<String> getDiffRules(){
if(diffRule == null){
return null;
}
return diffRule.stream().map(s->s.getClass().getSimpleName()).toList();
}
public synchronized void addChild(DiffNode child){
if(children == null){
children = new ArrayList<>(8);
}
children.add(child);
}
public synchronized void addChild(int i,DiffNode child){
if(children == null){
children = new ArrayList<>();
}
if (children.size()<i+1){
for (int j = children.size();j<=i;++j){
children.add(null);
}
}
children.set(i,child);
}
public int sameChildCount(){
if(children == null){
return 0;
}
return (int) children.stream().filter(DiffNode::isSame).count();
}
}
package local.my.demo_jdk.diff_json;
public enum DiffRes {
SAME,
IGNORED,
DIFFERENT,
UNSUPPORTED,
MISSING,
EXTRA,
CONFLICT
}
package local.my.demo_jdk.diff_json;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
public class DiffResult {
public DiffCfg diffCfg;
public DiffNode diffNode;
public void print(){
System.out.println("--------");
var cnt = new int[]{0,0};
loopPrint(diffNode,cnt);
System.out.println("--------");
System.out.println("对比元素不相同的有"+cnt[0]+"个,忽略的元素有"+cnt[1]+"个;");
System.out.println("--------");
}
private void loopPrint(DiffNode diffNode,int[] cnt){
if (diffNode.isSame()) {
if (diffNode.res==DiffRes.IGNORED){
cnt[1]+=1;
}
return;
}
if (diffNode.children==null||diffNode.children.isEmpty()){
StringBuilder sb = new StringBuilder();
sb.append("不同节点: [").append(diffNode.res).append(' ')
.append(diffNode.diffType==DiffNode.DiffType.NULL?Util.typeOf(diffNode.actual):diffNode.diffType)
.append(']');
if (diffNode.parsedJson!=null&&diffNode.parsedJson){
sb.append("[parsedJson]");
}
sb.append(" path: ").append(diffNode.path).append(' ');
sb.append("\n expect: ").append(JSON.toJSONString(diffNode.expect, JSONWriter.Feature.WriteNulls));
sb.append("\n actual: ").append(JSON.toJSONString(diffNode.actual, JSONWriter.Feature.WriteNulls));
if (diffNode.note!=null&&!diffNode.note.isBlank())
sb.append("\n note: ").append(diffNode.note);
if (diffNode.actualIndex!=null){
sb.append("\n actualIndex: ").append(diffNode.actualIndex);
}
if (diffNode.actualKey!=null){
sb.append("\n actualKey=").append(diffNode.actualKey);
}
cnt[0]+=1;
System.out.println(sb);
}else{
for (DiffNode child : diffNode.children) {
loopPrint(child,cnt);
}
}
}
}
4. 需要提供一些工具以完成对比
package local.my.demo_jdk.diff_json;
import java.util.Arrays;
public class LargeBitmaskMark {
private final int size;
private final long[] maskArray;
public LargeBitmaskMark(int size) {
this.size = size;
this.maskArray = new long[(size + 63) / 64];
}
private int getArrayIndex(int bitIndex) {
return bitIndex >> 6;
}
private int getBitPosition(int bitIndex) {
return bitIndex & 63;
}
public boolean mark(int index) {
if (index < 0 || index >= size) return false;
int arrayIndex = getArrayIndex(index);
int bitPosition = getBitPosition(index);
maskArray[arrayIndex] = maskArray[arrayIndex] | (1L << bitPosition);
return true;
}
public boolean marked(int index) {
if (index < 0 || index >= size) return false;
int arrayIndex = getArrayIndex(index);
int bitPosition = getBitPosition(index);
return (maskArray[arrayIndex] & (1L << bitPosition)) != 0L;
}
public void clear(int index) {
if (index < 0 || index >= size) return;
int arrayIndex = getArrayIndex(index);
int bitPosition = getBitPosition(index);
maskArray[arrayIndex] = maskArray[arrayIndex] & ~(1L << bitPosition);
}
public void reset() {
Arrays.fill(maskArray, 0L);
}
public int markedCount() {
int count = 0;
for (long longValue : maskArray) {
count += Long.bitCount(longValue);
}
return count;
}
}
package local.my.demo_jdk.diff_json;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import java.util.List;
import java.util.Map;
public class Util {
public static boolean isBaseJsonItemType(Object o) {
return o== null || o instanceof String || o instanceof Number || o instanceof Boolean;
}
public static boolean isJsonString(Object o) {
return o instanceof String s && (
(s=s.trim()).startsWith("{") && s.endsWith("}") ||
s.startsWith("[") && s.endsWith("]"));
}
public static DiffNode.DiffType typeOf(Object o){
return switch (o) {
case null -> DiffNode.DiffType.NULL;
case String s -> {
var ss = s.trim();
if (s.startsWith("{")&&s.endsWith("}"))
yield DiffNode.DiffType.OBJECT;
else if (s.startsWith("[")&&s.endsWith("]"))
if (JSON.isValid(ss, JSONReader.Feature.AllowUnQuotedFieldNames))
yield DiffNode.DiffType.ARRAY;
yield DiffNode.DiffType.STRING;
}
case Number _ -> DiffNode.DiffType.NUMBER;
case Boolean _ -> DiffNode.DiffType.BOOLEAN;
case Map<?,?> _ -> DiffNode.DiffType.OBJECT;
case List<?> _ -> DiffNode.DiffType.ARRAY;
default -> throw new IllegalArgumentException("unsupported type: " + o.getClass().getSimpleName());
};
}
}
5. 实现json对比功能
package local.my.demo_jdk.diff_json;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.function.Function;
public class DiffJson {
public final DiffCfg cfg;
protected final HashMap<String,String[]> ignoredPathsCache;
protected final HashMap<String, Pair<String[],List<DiffRule>>> customRulesCache;
public final Object expect,actual;
public DiffJson(Object expect,Object actual,DiffCfg cfg){
if (cfg==null){
cfg=new DiffCfg();
}
ignoredPathsCache = new HashMap<>(cfg.ignoredPaths.size());
customRulesCache = new HashMap<>(cfg.customRules.size());
this.cfg=cfg;
Function<Object,Object> toDiffObj = obj ->
Util.isJsonString(obj) ? JSON.parse(obj.toString(),JSONReader.Feature.AllowUnQuotedFieldNames):
Util.isBaseJsonItemType(obj) ? obj : JSON.parse(JSON.toJSONString(obj, JSONWriter.Feature.WriteNulls),JSONReader.Feature.AllowUnQuotedFieldNames);
this.expect=toDiffObj.apply(expect);
this.actual=toDiffObj.apply(actual);
}
public DiffResult diff(){
var d = new DiffNode();
d.path="$";
d.expect=expect;
d.actual=actual;
var r_=new DiffResult();
r_.diffCfg=cfg;
r_.diffNode=diffObjs(d);
return r_;
}
protected DiffNode diffObjs(DiffNode d){
if (isPathIgnored(d.path)){
d.res=DiffRes.IGNORED;
return d;
}
var actual = d.actual;
return switch (d.expect){
case null -> {
if (actual==null){
d.res=DiffRes.SAME;
}else if (cfg.allowEmptyAsNull &&(
actual instanceof Map<?,?> m && m.isEmpty()
|| actual instanceof List<?> l && l.isEmpty()
|| actual instanceof String s && s.isBlank()
|| actual instanceof Number n && n.doubleValue()==0
|| actual instanceof Boolean b && !b
)){
d.res=DiffRes.SAME;
d.note="empty as null";
}
matchRulesWhenNotSame(d);
d.diffType= DiffNode.DiffType.NULL;
yield d;
}
case Boolean bool -> {
if (actual instanceof Boolean b && bool==b){
d.res=DiffRes.SAME;
}else if (cfg.allowEmptyAsNull &&actual==null&&!bool){
d.res=DiffRes.SAME;
}
else if (cfg.allowDiffBoolAndStr&&
actual instanceof String s && bool==BooleanUtil.toBoolean(s)){
d.res=DiffRes.SAME;
}
matchRulesWhenNotSame(d);
d.diffType= DiffNode.DiffType.BOOLEAN;
yield d;
}
case Number num_ -> {
var num = NumberUtil.toBigDecimal(num_).setScale(cfg.scale, RoundingMode.HALF_UP);
switch (actual) {
case null -> {
if (cfg.allowEmptyAsNull && num.equals(BigDecimal.ZERO)){
d.note = "empty as null";
d.res=DiffRes.SAME;
}else{
d.res=DiffRes.DIFFERENT;
}
}
case Number num_a -> {
var num_a_ = NumberUtil.toBigDecimal(num_a).setScale(cfg.scale, RoundingMode.HALF_UP);
if (num.equals(num_a_)) {
d.res = DiffRes.SAME;
}
}
case String s when cfg.allowDiffStrAndNum -> {
var num_a_ = NumberUtil.toBigDecimal(s).setScale(cfg.scale, RoundingMode.HALF_UP);
if (num.equals(num_a_)) {
d.res = DiffRes.SAME;
}
}
default -> {
d.res = DiffRes.UNSUPPORTED;
d.note = "unsupported actual type: " + actual.getClass().getSimpleName();
}
}
matchRulesWhenNotSame(d);
d.diffType= DiffNode.DiffType.NUMBER;
yield d;
}
case String str -> {
d.diffType = DiffNode.DiffType.STRING;
if (actual==null && cfg.allowEmptyAsNull && str.isEmpty()){
d.res=DiffRes.SAME;
d.note="empty as null";
d.diffType= DiffNode.DiffType.NULL;
}
else if (cfg.allowDiffBoolAndStr && actual instanceof Boolean actual_b && actual_b==BooleanUtil.toBoolean(str)){
d.res=DiffRes.SAME;
d.note="auto cast";
d.diffType = DiffNode.DiffType.BOOLEAN;
}
else if (cfg.allowDiffStrAndNum && actual instanceof Number actual_n &&
NumberUtil.toBigDecimal(actual_n).setScale(cfg.scale, RoundingMode.HALF_UP).equals(NumberUtil.toBigDecimal(str).setScale(cfg.scale, RoundingMode.HALF_UP))){
d.res=DiffRes.SAME;
d.note="auto cast";
d.diffType = DiffNode.DiffType.NUMBER;
}
else if (actual instanceof String str_a){
if (str.equals(str_a))
d.res=DiffRes.SAME;
else if (str.startsWith("{")&&str.endsWith("}")&&str_a.startsWith("{")&&str_a.endsWith("}")){
d.diffType = DiffNode.DiffType.OBJECT;
diffMap(d,JSON.parseObject(str,JSONReader.Feature.AllowUnQuotedFieldNames),JSON.parseObject(str_a,JSONReader.Feature.AllowUnQuotedFieldNames));
}
else if(str.startsWith("[")&&str.endsWith("]")&&str_a.startsWith("[")&&str_a.endsWith("]")){
d.diffType = DiffNode.DiffType.ARRAY;
diffList(d,JSON.parseArray(str,JSONReader.Feature.AllowUnQuotedFieldNames),JSON.parseArray(str_a,JSONReader.Feature.AllowUnQuotedFieldNames));
}
}else if (actual instanceof Map<?,?> m){
var str_ = str.trim();
d.diffType = DiffNode.DiffType.OBJECT;
if (m.isEmpty() && cfg.allowEmptyAsNull && str_.isEmpty()){
d.res=DiffRes.SAME;
d.diffType = DiffNode.DiffType.NULL;
}
else if (str_.startsWith("{")&&str.endsWith("}")){
var map = JSON.parseObject(str,JSONReader.Feature.AllowUnQuotedFieldNames);
d.parsedJson=true;
diffMap(d,map,m);
}
}else if (actual instanceof List<?> actual_l){
d.diffType = DiffNode.DiffType.ARRAY;
var str_ = str.trim();
if (actual_l.isEmpty() && cfg.allowEmptyAsNull && str_.isEmpty()){
d.res=DiffRes.SAME;
}
else if (str_.startsWith("[")&&str.endsWith("]")){
var list = JSON.parseArray(str,JSONReader.Feature.AllowUnQuotedFieldNames);
d.parsedJson=true;
diffList(d,list,actual_l);
}
}
matchRulesWhenNotSame(d);
yield d;
}
case Map<?,?> map -> {
d.diffType = DiffNode.DiffType.OBJECT;
if (map.isEmpty() && cfg.allowEmptyAsNull &&(actual==null || actual instanceof String s && StrUtil.isBlank(s) || actual instanceof Map<?,?> m && m.isEmpty())){
d.res=DiffRes.SAME;
d.note="empty as null";
d.diffType = DiffNode.DiffType.NULL;
}
else if (actual instanceof Map<?,?> actualMap){
diffMap(d,map,actualMap);
}else if (actual instanceof String s && s.trim().startsWith("{") && s.endsWith("}")){
d.parsedJson=true;
diffMap(d,map,JSON.parseObject(s, JSONReader.Feature.AllowUnQuotedFieldNames));
}
matchRulesWhenNotSame(d);
yield d;
}
case List<?> list -> {
d.diffType = DiffNode.DiffType.ARRAY;
if (list.isEmpty() && cfg.allowEmptyAsNull &&(actual==null || actual instanceof String s && StrUtil.isBlank(s) || actual instanceof List<?> l && l.isEmpty())){
d.res=DiffRes.SAME;
d.note="empty as null";
d.diffType = DiffNode.DiffType.NULL;
}
else if (actual instanceof List<?> actualList){
diffList(d,list,actualList);
}else if (actual instanceof String s && s.trim().startsWith("[") && s.endsWith("]")){
d.parsedJson=true;
diffList(d,list,JSON.parseArray(s, JSONReader.Feature.AllowUnQuotedFieldNames));
}
matchRulesWhenNotSame(d);
yield d;
}
default -> {
if (Objects.equals(actual,d.expect)){
d.res=DiffRes.SAME;
}else{
d.res=DiffRes.UNSUPPORTED;
d.note="unsupported type: "+d.expect.getClass().getSimpleName();
}
matchRulesWhenNotSame(d);
yield d;
}
};
}
private void matchRulesWhenNotSame(final DiffNode d) {
if (d.isSame()){
return;
}
if (d.res==null){
d.res=DiffRes.DIFFERENT;
}
List<DiffRule> rules = customRules(d.path);
if (rules!=null&&!rules.isEmpty()){
if(d.res==null) d.res=DiffRes.DIFFERENT;
if (d.note==null||d.note.isEmpty()) d.note="diff by custom rules";
d.diffRule=rules;
for (int i = 0; i < rules.size(); i++) {
var rule = rules.get(i);
if (rule.math(d.expect, d.actual, d.path, this.expect, this.actual)){
d.res=DiffRes.SAME;
d.note="rule match "+i;
break;
}
}
}
}
protected void diffMap(DiffNode d,final Map<?,?> expected,final Map<?,?> actual){
boolean ok = true;
if (d.keyNameEqCount ==null)d.keyNameEqCount =0;
var notDoneActualKeys = new HashSet<Object>(actual.keySet());
var conflictKeys = new HashSet<String>();
for (Map.Entry<?, ?> expectE : expected.entrySet()) {
var expectK = expectE.getKey().toString();
var expectV = expectE.getValue();
var dRes = new DiffNode();
dRes.path=d.path+"."+expectK;
dRes.expect=expectV;
String hasActual = doSetActualInMap(dRes,expectK,actual,notDoneActualKeys);
if (hasActual!=null){
d.keyNameEqCount += 1;
}else{
dRes.res=DiffRes.MISSING;
}
if (hasActual!=null && !conflictKeys.add(hasActual)){
dRes.res = DiffRes.CONFLICT;
dRes.note = "conflict expect key: "+expectK;
}
else if (isPathIgnored(dRes.path)){
dRes.res=DiffRes.IGNORED;
}
else {
diffObjs(dRes);
if (hasActual==null && !dRes.isSame()){
if (dRes.res != DiffRes.MISSING){
dRes.note=StrUtil.nullToEmpty(dRes.note)+"; originRes:"+dRes.res.name();
}
dRes.res=DiffRes.MISSING;
}
}
d.addChild(dRes);
if (!dRes.isSame()){
ok = false;
}
}
for (Object key : notDoneActualKeys) {
var dRes = new DiffNode();
dRes.path=d.path+"."+key;
dRes.actual=actual.get(key);
dRes.res=DiffRes.EXTRA;
diffObjs(dRes);
if (!dRes.isSame()){
if (dRes.res!=DiffRes.EXTRA){
dRes.note=StrUtil.nullToEmpty(dRes.note)+"; originRes:"+dRes.res.name();
}
dRes.res=DiffRes.EXTRA;
ok = false;
}
d.addChild(dRes);
}
d.res = ok?DiffRes.SAME:d.isSame()?d.res:DiffRes.DIFFERENT;
}
private String doSetActualInMap(DiffNode dRes,final String expectK,final Map<?,?> actual,Set<Object> notDoneActualKeys){
if (!cfg.ignoreCase && !cfg.snakeAsCamelCase){
dRes.actual=actual.get(expectK);
boolean has = notDoneActualKeys.remove(expectK);
return has?expectK:null;
}
String conflictKey = null;
for (Object o : actual.keySet()) {
String aK = o.toString();
if (!notDoneActualKeys.contains(aK)){
continue;
}
boolean isSameKey=false;
if (cfg.ignoreCase &&!cfg.snakeAsCamelCase){
isSameKey=expectK.equalsIgnoreCase(aK);
if (isSameKey)
conflictKey=expectK.toLowerCase();
}
else if(!cfg.ignoreCase &&cfg.snakeAsCamelCase){
String camelCase = StrUtil.toCamelCase(expectK);
isSameKey=camelCase.equals(StrUtil.toCamelCase(aK));
if (isSameKey)
conflictKey=camelCase;
}
else if (cfg.ignoreCase){
String camelCase = StrUtil.toCamelCase(expectK);
isSameKey=camelCase.equalsIgnoreCase(StrUtil.toCamelCase(aK));
if (isSameKey)
conflictKey=camelCase.toLowerCase();
}
if (isSameKey){
dRes.actual=actual.get(aK);
notDoneActualKeys.remove(aK);
dRes.actualKey=aK;
}
}
return conflictKey;
}
protected void diffList(DiffNode d,final List<?> expect,final List<?> actual){
int eMaxI = expect.size()-1;
int aMaxI = actual.size()-1;
if (eMaxI==aMaxI&& eMaxI<0){
d.res=DiffRes.SAME;
return;
}
boolean ok = true;
if (!cfg.smartListCompare){
for (int i = 0; i <= Math.max(eMaxI,aMaxI); i++) {
Object e = eMaxI<i?null:expect.get(i);
Object a = aMaxI<i?null:actual.get(i);
var dRes = new DiffNode();
dRes.path=d.path+"["+i+"]";
dRes.expect=e;
dRes.actual=a;
diffObjs(dRes);
d.addChild(i,dRes);
if (!dRes.isSame()){
ok=false;
if (eMaxI<i){
if (dRes.res!=null){
dRes.note=StrUtil.nullToEmpty(dRes.note)+"; originRes:"+dRes.res.name();
}
dRes.res=DiffRes.EXTRA;
}else if (aMaxI<i){
if (dRes.res!=null){
dRes.note=StrUtil.nullToEmpty(dRes.note)+"; originRes:"+dRes.res.name();
}
dRes.res=DiffRes.MISSING;
}
}
}
}else{
diffListSmart(d, expect, actual);
}
if (d.children==null||d.children.isEmpty()){
d.res=DiffRes.SAME;
}else{
d.res = d.children.stream().filter(Objects::nonNull).allMatch(DiffNode::isSame)?DiffRes.SAME:DiffRes.DIFFERENT;
}
}
@SuppressWarnings("unchecked")
protected void diffListSmart(DiffNode d, List<?> expect, List<?> actual) {
if (d.children==null) d.children=new ArrayList<>();
List<Pair<DiffNode,Integer>>[] notCompResList = new List[expect.size()];
var okMarkE = new LargeBitmaskMark(expect.size());
var okMarkA = new LargeBitmaskMark(actual.size());
diffListSmart_p1(d, expect, actual,notCompResList, okMarkA, okMarkE);
diffListSmart_p2(d, expect,notCompResList, okMarkA, okMarkE);
diffListSmart_p3(d, expect,actual, notCompResList, okMarkE, okMarkA);
diffListSmart_p4(d, actual, okMarkA);
}
private void diffListSmart_p4(DiffNode d, List<?> actual, LargeBitmaskMark okMarkA) {
for (int i = 0; i < actual.size(); i++) {
if (!okMarkA.marked(i)){
var dRes = new DiffNode();
dRes.path= d.path+"["+i+"]";
dRes.actual= actual.get(i);
dRes.expect=null;
diffObjs(dRes);
if (!dRes.isSame() && dRes.res!=DiffRes.EXTRA){
dRes.res=DiffRes.EXTRA;
dRes.note=StrUtil.nullToEmpty(dRes.note)+"; originRes:"+dRes.res.name();
}
dRes.actualIndex=i;
d.addChild(dRes);
}
}
}
private void diffListSmart_p3(DiffNode d, List<?> expect,List<?> actual, List<Pair<DiffNode, Integer>>[] notCompResList, LargeBitmaskMark okMarkE, LargeBitmaskMark okMarkA) {
for (int i = 0; i < notCompResList.length; i++) {
if (d.children.get(i)!=null|| okMarkE.marked(i)) continue;
var diffNodes = notCompResList[i];
if (diffNodes == null||diffNodes.isEmpty()){
diffListSmart_p3_miss(d, expect, actual, okMarkE, okMarkA, i);
}else{
boolean ok = false;
for (Pair<DiffNode, Integer> diffPair : diffNodes) {
DiffNode diffNode = diffPair.getKey();
if (okMarkA.marked(diffNode.actualIndex))
continue;
var aT = Util.typeOf(diffNode.actual);
var eT = Util.typeOf(diffNode.expect);
if (aT==eT||aT==null||eT==null||
cfg.allowDiffStrAndNum&&diffNode.diffType==DiffNode.DiffType.NUMBER||
cfg.allowDiffBoolAndStr&&diffNode.diffType==DiffNode.DiffType.BOOLEAN){
d.addChild(i,diffNode);
okMarkE.mark(i);
okMarkA.marked(diffNode.actualIndex);
if (diffNode.actualIndex==i){
diffNode.actualIndex=null;
}
ok=true;
break;
}
}
if (!ok){
diffListSmart_p3_miss(d, expect, actual, okMarkE, okMarkA, i);
}
}
}
}
private void diffListSmart_p3_miss(DiffNode d, List<?> expect, List<?> actual, LargeBitmaskMark okMarkE, LargeBitmaskMark okMarkA, int i) {
var dRes = new DiffNode();
dRes.path= d.path+"["+ i +"]";
dRes.expect= expect.get(i);
if (i >= actual.size()|| okMarkA.marked(i))dRes.res=DiffRes.MISSING;
else{
dRes.actual= okMarkA.marked(i)?null:actual.get(i);
okMarkA.mark(i);
diffObjs(dRes);
}
d.addChild(i,dRes);
okMarkE.mark(i);
}
private void diffListSmart_p2(DiffNode d, List<?> expect,List<Pair<DiffNode, Integer>>[] notCompResList, LargeBitmaskMark okMarkA, LargeBitmaskMark okMarkE) {
loop_0: for (int i = 0; i < notCompResList.length; i++) {
if (d.children.get(i)!=null||okMarkE.marked(i)) continue;
var diffNodes = notCompResList[i];
if (diffNodes == null||diffNodes.isEmpty()) continue;
Object expect_i = expect.get(i);
DiffNode.DiffType diffType_e = Util.typeOf(expect_i);
loop_1:for (Pair<DiffNode, Integer> diffNode_ : diffNodes) {
Integer v_cnt=diffNode_.getValue();
DiffNode diffNode = diffNode_.getKey();
var idx_a = diffNode.actualIndex;
if (okMarkA.marked(idx_a)) continue;
if (diffNode.diffType!= DiffNode.DiffType.NULL&&diffType_e!= DiffNode.DiffType.NULL){
if (cfg.allowDiffBoolAndStr){
if (diffNode.diffType== DiffNode.DiffType.BOOLEAN&&(diffType_e== DiffNode.DiffType.STRING||diffType_e==DiffNode.DiffType.BOOLEAN))
continue ;
}
else if (cfg.allowDiffStrAndNum){
if(diffNode.diffType== DiffNode.DiffType.NUMBER&&(diffType_e== DiffNode.DiffType.STRING||diffType_e==DiffNode.DiffType.NUMBER))
continue ;
}
else if (diffNode.diffType!=diffType_e){
continue ;
}
}
loop_2: for (int j = 0; j < notCompResList.length; j++) {
if (j==i){
continue;
}
var diffNodes_in = notCompResList[j];
if (diffNodes_in != null) {
for (Pair<DiffNode, Integer> pair_in : diffNodes_in) {
DiffNode diff_in = pair_in.getKey();
if (diff_in.actualIndex==null||okMarkA.marked(diff_in.actualIndex)){
continue;
}
if (pair_in.getValue()>v_cnt){
continue loop_1;
}
}
}
}
okMarkE.mark(i);
okMarkA.mark(idx_a);
d.addChild(i,diffNode);
if (diffNode.actualIndex==i){
diffNode.actualIndex=null;
}
continue loop_0;
}
}
}
private void diffListSmart_p1(DiffNode d, List<?> expect, List<?> actual, List<Pair<DiffNode, Integer>>[] notCompResList, LargeBitmaskMark okMarkA, LargeBitmaskMark okMarkE) {
loop_1: for (int i = 0; i < expect.size(); i++) {
if (okMarkE.marked(i)) continue;
d.addChild(i,null);
Object e = expect.get(i);
String path = d.path+"["+i+"]";
DiffNode sameIdxDiffNode = null;
if (i<actual.size()){
Object a = actual.get(i);
var dRes = new DiffNode();
dRes.path=path;
dRes.expect=e;
dRes.actual=a;
diffObjs(dRes);
sameIdxDiffNode = dRes;
if (dRes.isSame()){
okMarkA.mark(i);
okMarkE.mark(i);
d.addChild(i,dRes);
continue ;
}else{
dRes.actualIndex=i;
}
}
for (int j_ = -1; j_ < Math.min(actual.size(),expect.size()); j_++) {
var j = j_;
if (j==-1){
if (okMarkA.marked(i)){
continue ;
}
j = i;
if (j>=(actual.size())){
continue ;
}
}else if (j==i){
continue ;
}
else if (okMarkA.marked(j)){
continue;
}
DiffNode dRes;
if (j==i&&sameIdxDiffNode!=null){
dRes = sameIdxDiffNode;
}else{
Object a = actual.get(j);
dRes = new DiffNode();
dRes.path=path;
dRes.expect=e;
dRes.actual=a;
dRes.actualIndex=j;
diffObjs(dRes);
if (dRes.isSame()){
okMarkA.mark(j);
okMarkE.mark(i);
d.addChild(i,dRes);
if (dRes.actualIndex==i){
dRes.actualIndex=null;
}
continue loop_1;
}
}
if (notCompResList[i]==null)
notCompResList[i]=new ArrayList<>();
int sameChildCnt = 0;
if (dRes.children!=null&&!dRes.children.isEmpty()){
for (DiffNode child : dRes.children) {
if (child.isSame()){
sameChildCnt++;
}
}
}
notCompResList[i].add(Pair.of(dRes,sameChildCnt));
}
}
for (int i = 0; i < notCompResList.length; i++) {
List<Pair<DiffNode, Integer>> diffNodes = notCompResList[i];
if (diffNodes == null) continue;
diffNodes.sort((a,b)->{
var res = a.getValue()-b.getValue();
if (res==0){
if (a.getKey().keyNameEqCount!=null&&b.getKey().keyNameEqCount==null){
res = -1 ;
}
else if (a.getKey().keyNameEqCount==null&&b.getKey().keyNameEqCount!=null){
res = 1 ;
}
else if (a.getKey().keyNameEqCount!=null&&b.getKey().keyNameEqCount!=null){
var r = a.getKey().keyNameEqCount-b.getKey().keyNameEqCount;
if (r>0) res=1;
else if (r<0) res = -1;
}
}
return res;
});
}
}
protected boolean isPathIgnored(final String expectedPath) {
if (cfg.ignoredPaths.contains(expectedPath)){
return true;
}
String[] expectedPathArr = expectedPath.split("\.");
if (expectedPathArr.length==0){
return false;
}
return cfg.ignoredPaths.stream().anyMatch(p->{
if (expectedPath.equals(p)){
return true;
}
var ignoredPaths=ignoredPathsCache.computeIfAbsent(p, _ ->p.split("\."));
if (expectedPathArr.length!=ignoredPaths.length){
return false;
}
for (int i = 0; i < ignoredPaths.length; i++) {
if (!"*".equals(ignoredPaths[i]) && !isSameKey(ignoredPaths[i],expectedPathArr[i])){
return false;
}
}
return true;
});
}
protected boolean isSameKey(final String expectedKey,final String actualKey){
if (!cfg.ignoreCase &&!cfg.snakeAsCamelCase){
return expectedKey.equals(actualKey);
}
else if (cfg.ignoreCase &&!cfg.snakeAsCamelCase){
return expectedKey.equalsIgnoreCase(actualKey);
}
else if (!cfg.ignoreCase){
return StrUtil.toCamelCase(expectedKey).equals(StrUtil.toCamelCase(actualKey));
}
else {
return StrUtil.toCamelCase(expectedKey).equalsIgnoreCase(StrUtil.toCamelCase(actualKey));
}
}
protected List<DiffRule> customRules(final String path){
if (cfg.customRules.containsKey(path)){
return cfg.customRules.get(path);
}
String[] expectedPathArr = path.split("\.");
for (Map.Entry<String, List<DiffRule>> e : cfg.customRules.entrySet()) {
var pathAndRule = customRulesCache.computeIfAbsent(e.getKey(),
_ -> Pair.of(e.getKey().split("\."),e.getValue()));
var paths = pathAndRule.getKey();
if (paths.length!=expectedPathArr.length){
return null;
}
var rules = pathAndRule.getValue();
boolean match = true;
for (int i = 0; i < expectedPathArr.length; i++) {
if (!"*".equals(paths[i]) && (
!expectedPathArr[i].equals(paths[i]) ||
(cfg.ignoreCase && !expectedPathArr[i].equalsIgnoreCase(paths[i]))
)){
match = false;
break;
}
}
if (match){
return rules;
}
}
return null;
}
}
6. 测试它!
package local.my.demo_jdk.t;
import local.my.demo_jdk.diff_json.DiffCfg;
import local.my.demo_jdk.diff_json.DiffJson;
public class JTest implements CaseFactory{
static synchronized void main() {
System.out.println("\n====== 1: 严格配置,预期2个不同");
new DiffJson("""
{x:1}
""","""
{X:1}
""", DiffCfg.ofStrict()).diff().print();
System.out.println("\n====== 2: 默认配置,预期0个不同");
new DiffJson("""
{x:1,a_b:2,c:[{A:3},4]}
""","""
{X:1,aB:2,c:[4,{a:3}]}
""", null).diff().print();
System.out.println("\n====== 3: list智能对比null,预期0个不同");
new DiffJson("""
[null,1]
""","""
[1,null]
""", null).diff().print();
System.out.println("\n====== 4: list智能对比非null,预期0个不同");
new DiffJson("""
[2,1,{a:3},[4]]
""","""
[1,2,[4],{A:3}]
""", null).diff().print();
}
}