Java写一个JSON对比工具

9 阅读6分钟

1. 我们希望我的的对比工具能进行一些自定义配置

package local.my.demo_jdk.diff_json;

import java.util.*;


public class DiffCfg {
    /**是否str对比num时,转num对比*/
    public boolean allowDiffStrAndNum = false;
    /**是否str对比bool时,转bool对比*/
    public boolean allowDiffBoolAndStr = false;
    /**
     * null和空字符串/map/list/false/0视为相同(默认关闭)
     */
    public boolean allowEmptyAsNull = true;
    /**
     * 全局是否忽略Map key的大小写
     */
    public boolean ignoreCase = true;
    /**
     * 是否将驼峰规则字段和下划线规则字段视为相同,为true时会将下划线规则字段转换为驼峰规则字段
     */
    public boolean snakeAsCamelCase = true;
    /**
     * 是否开启智能List对比(默认开启)
     */
    public boolean smartListCompare = true;
    /**
     * 数字最大精度(小数位数),默认16位
     */
    public int scale = 16;
    /**
     * 当直接对比不相同时,自定义对比规则映射,自定义diff时传入的是原始值
     * <br>key: JSONPath表达式,支持*通配符,如:$[1],*,$[1].a,$[1].*.a,$.*.a,$.a.*
     * <br>value: 对比规则列表,一个字段可以使用多个对比规则
     */
    public final Map<String, List<DiffRule>> customRules = new HashMap<>(0);
    /**
     * 忽略的字段路径(jsonPath)
     */
    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 {
    /**json path*/
    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;
    /**在忽略大小写或下划线时,如果对比预期值的key不能直接==实际值的key,需要指定实际值的key*/
    public String actualKey;
    /**对比数组时,如果不是按照下标对比,需要指定实际值的下标*/
    public Integer actualIndex;
    /**如果时字符串解析的json,则为true*/
    public Boolean parsedJson;
    /**相同的key数量*/
    public Integer keyNameEqCount;
    /**对比类型(以expect为准)*/
    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,
    /**在忽略大小写或驼峰下划线的情况下有key冲突*/
    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;
    // 使用 Long 数组,每个 Long 可以存储 64 个标志位
    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;  // 除以64,同 bitIndex / 64
    }

    private int getBitPosition(int bitIndex) {
        return bitIndex & 63;  // 取模64,同 bitIndex % 64
    }

    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;
                }
            }
        }
    }

    /**对比map结构*/
    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;
    }
    /**设置map结构实际值,返回非null表示找到了相同的key*/
    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();
            //已经处理过的key跳过
            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;
    }
    /**对比list*/
    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;
        }
    }

    /**智能对比list*/
    @SuppressWarnings("unchecked")
    protected void diffListSmart(DiffNode d, List<?> expect, List<?> actual) {
        if (d.children==null) d.children=new ArrayList<>();
        // 先用expect,expect没有的再查找,value是相同数量
        List<Pair<DiffNode,Integer>>[] notCompResList = new List[expect.size()];
        var okMarkE = new LargeBitmaskMark(expect.size());
        var okMarkA = new LargeBitmaskMark(actual.size());
        // 第1步 智能对比找相同
        diffListSmart_p1(d, expect, actual,notCompResList, okMarkA, okMarkE);
        // 第2步 找最相似
        diffListSmart_p2(d, expect,notCompResList, okMarkA, okMarkE);
        //第3步 expect剩下的按顺序设置为MISSING
        diffListSmart_p3(d, expect,actual, notCompResList, okMarkE, okMarkA);
        // 第4步 actual剩下的按顺序设置为EXTRA
        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) {
                            //下面会设null当对比的下标相同时
                            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){
                    // -1时对比过了,就跳过
                    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;
            //把count大的放首位,其中下标相同的放在前面
            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;
        });
    }

    /**判断key是否相同,判断是否忽略大小写和蛇形转驼峰*/
    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();
    }

}