增强版JSON对比工具类

增强版JSON对比工具使用文档

概述

这是一个功能强大的JSON对比工具(依赖fastjson2),支持JSON5解析和智能对比功能。特别适用于测试、数据验证和API对比场景。

主要特性

1. JSON5解析支持

  • 支持JSON5格式(单引号字符串、尾随逗号、注释等)
  • 自动检测和解析JSON5
  • 向后兼容标准JSON

2. 智能数组对比

  • 长度不同也能对比:即使数组长度不同,也能进行对比并展示差异
  • 详细下标信息:明确提示哪个下标是多的、哪个是少的
  • 相似性匹配:不严格按照顺序,基于内容相似性进行匹配
  • 差异分类:区分"缺失元素"、"多余元素"、"差异元素"

3. 字符串智能处理

  • 自动JSON检测 :字符串如果是JSON格式(以{开头}结尾或[开头]结尾),自动解析后对比
  • 多种对比模式:精确匹配、忽略大小写、去除空格、相似度对比等
  • 模式匹配:支持字符串包含模式和正则表达式

4. 灵活的配置系统

  • 全局配置:设置默认对比行为
  • JSONPath级别配置:为特定路径设置特殊规则
  • 忽略功能:可以忽略特定路径的对比

5. 数字对比增强

  • 容差对比:支持在容差范围内对比数字
  • 小数位数限制:可以指定对比的小数位数
  • 类型转换:自动处理字符串和数字之间的转换

核心类说明

EnhancedJsonComparator

主对比类,提供所有对比功能。

主要方法:
  • compare(String expect, String real, GlobalConfig config):使用配置对比两个JSON字符串,config为null时使用默认配置
  • ComparisonResult:对比结果类,包含详细差异信息

JsonComparatorConfig

配置管理类,支持灵活的对比规则配置。

配置类型:
  • GlobalConfig:全局配置
  • PathConfig:JSONPath级别配置
  • CompareMode:对比模式(严格、宽松、相似度)
  • StringCompareMode:字符串对比模式
  • NumberCompareMode:数字对比模式

使用示例

基本使用

groovy 复制代码
import local.my.EnhancedJsonComparator

def json1 = '{"name": "张三", "age": 25}'
def json2 = '{"name": "张三", "age": 25}'

def result = EnhancedJsonComparator.simpleCompare(json1, json2)
println(result.summary)

JSON5解析

groovy 复制代码
def json5 = '''
{
    // JSON5注释
    name: '张三',  // 单引号
    age: 25,
    tags: ['tag1', 'tag2',],  // 尾随逗号
}
'''

def standardJson = '''
{
    "name": "张三",
    "age": 25,
    "tags": ["tag1", "tag2"]
}
'''

def result = EnhancedJsonComparator.compare(json5, standardJson)

数组长度不同对比

groovy 复制代码
def array1 = '{"items": [1, 2, 3, 4]}'
def array2 = '{"items": [1, 3, 4]}'

def result = EnhancedJsonComparator.simpleCompare(array1, array2)
println(result.summary)
// 输出会显示:
// - 缺失元素[下标1]: 2
// - 多余元素[下标2]: 4 (实际位置)

字符串JSON自动检测

groovy 复制代码
def json1 = '{"data": "{\\"name\\": \\"test\\", \\"value\\": 123}"}'
def json2 = '{"data": "{\\"name\\": \\"test\\", \\"value\\": 456}"}'

def result = EnhancedJsonComparator.simpleCompare(json1, json2)
// 会自动检测data字段的字符串是JSON,并进行深度对比

自定义配置

groovy 复制代码
import local.my.JsonComparatorConfig

// 创建宽松配置
def config = JsonComparatorConfig.createLenientConfig()
config.decimalPlaces = 2  // 限制2位小数
config.stringSimilarityThreshold = 70  // 字符串相似度阈值

// 添加JSONPath级别配置
def pathConfig = new JsonComparatorConfig.PathConfig(
    stringMode: JsonComparatorConfig.StringCompareMode.CASE_INSENSITIVE,
    ignore: false
)
JsonComparatorConfig.addPathConfig(config, "user.name", pathConfig)

// 使用配置进行对比
def result = EnhancedJsonComparator.compare(json1, json2, config)

字符串相似度对比

groovy 复制代码
def config = JsonComparatorConfig.createSimilarityConfig()
config.stringSimilarityThreshold = 60  // 设置相似度阈值

def str1 = '{"text": "这是一个测试字符串"}'
def str2 = '{"text": "这是一个测试字串"}'

def result = EnhancedJsonComparator.compare(str1, str2, config)
// 如果相似度>=60%,则认为匹配

CONTAINS模式匹配

groovy 复制代码
def config = JsonComparatorConfig.DEFAULT_CONFIG

def likeConfig = new JsonComparatorConfig.PathConfig(
    stringMode: JsonComparatorConfig.StringCompareMode.CONTAINS,
        containsPattern: "user"
)
JsonComparatorConfig.addPathConfig(config, "username", likeConfig)

def json1 = '{"username": "user_123", "age": 25}'
def json2 = '{"username": "user_456", "age": 25}'

def result = EnhancedJsonComparator.compare(json1, json2, config)
// 两个username都匹配"user_%"模式,所以通过

忽略特定路径

groovy 复制代码
def config = JsonComparatorConfig.DEFAULT_CONFIG

def ignoreConfig = new JsonComparatorConfig.PathConfig(ignore: true)
JsonComparatorConfig.addPathConfig(config, "metadata.timestamp", ignoreConfig)

def json1 = '{"name": "test", "metadata": {"timestamp": "2025-12-26T10:00:00", "version": "1.0"}}'
def json2 = '{"name": "test", "metadata": {"timestamp": "2025-12-26T11:00:00", "version": "1.0"}}'

def result = EnhancedJsonComparator.compare(json1, json2, config)
// 忽略timestamp字段的差异

配置选项详解

全局配置 (GlobalConfig)

配置项 类型 默认值 说明
compareMode CompareMode STRICT 对比模式:严格、宽松、相似度
enableJson5 boolean true 是否启用JSON5解析
autoDetectJsonInStrings boolean true 是否自动检测字符串中的JSON
numberTolerance double 0.000001 数字容差范围
decimalPlaces int -1 小数位数限制(-1表示不限制)
stringSimilarityThreshold int 80 字符串相似度阈值(0-100)
defaultStringMode StringCompareMode EXACT 默认字符串对比模式
defaultNumberMode NumberCompareMode EXACT 默认数字对比模式

JSONPath配置 (PathConfig)

配置项 类型 说明
stringMode StringCompareMode 字符串对比模式
numberMode NumberCompareMode 数字对比模式
tolerance Double 容差范围
decimalPlaces Integer 小数位数
similarityThreshold Integer 相似度阈值
containsPattern String CONTAINS模式
regexPattern String 正则表达式
ignore Boolean 是否忽略该路径

对比模式 (CompareMode)

  • STRICT: 严格对比,要求完全一致
  • LENIENT: 宽松对比,允许一定差异
  • SIMILARITY: 相似度对比,基于阈值判断

字符串对比模式 (StringCompareMode)

  • EXACT: 精确匹配
  • CASE_INSENSITIVE: 忽略大小写
  • TRIM: 去除空格后对比
  • CONTAINS: 字符串包含模式
  • SIMILARITY: 相似度对比
  • REGEX: 正则表达式匹配

数字对比模式 (NumberCompareMode)

  • EXACT: 精确匹配
  • WITHIN_TOLERANCE: 在容差范围内
  • DECIMAL_PLACES: 指定小数位数

算法说明

数组对比算法

  1. 第一阶段:匹配完全相等的元素
  2. 第二阶段:基于相似度匹配剩余元素
  3. 第三阶段:报告未匹配的元素(缺失/多余)

相似度计算

  • 字符串相似度:使用Levenshtein距离算法
  • 对象相似度:基于共同属性的比例
  • 数组相似度:基于相同元素的比例

JSONPath匹配

支持简单的通配符匹配:

  • *: 匹配任意字符序列
  • user.name: 精确匹配路径
  • *.price: 匹配所有price字段

性能考虑

  1. 数组对比:时间复杂度O(n²),对于大型数组建议预处理
  2. JSON5解析:fastjson2提供高性能解析,支持可读性更强的json5格式
  3. 内存使用:深度对比可能消耗较多内存,建议分块处理大型JSON

扩展建议

  1. 缓存优化:对频繁对比的路径添加缓存
  2. 并行处理:大型数组可以并行计算相似度
  3. 增量对比:支持只对比变化的部分
  4. 自定义比较器:支持用户自定义类型比较器

代码

对比工具代码

groovy 复制代码
package local.my

import com.alibaba.fastjson2.JSON
import com.alibaba.fastjson2.JSONReader
import groovy.transform.CompileStatic

/**
 * 增强版JSON对比工具类
 * 支持JSON5解析和智能对比功能
 * 
 * 主要特性:
 * 1. 支持JSON5解析
 * 2. 数组长度不同时也能对比,展示差异下标
 * 3. 字符串自动检测JSON并解析对比
 * 4. 支持全局和JSONPath级别的解析规则配置
 * 5. 支持字符串包含、相似度、小数位数限制等
 */
@CompileStatic
class EnhancedJsonComparator {
    
    /**
     * 对比结果类
     */
    static class ComparisonResult {
        public boolean isEqual = true
        List<String> differences = []
        List<ArrayDiffInfo> arrayDiffs = []  // 数组差异信息

        /**
         * 数组差异信息
         */
        static class ArrayDiffInfo {
            String path
            int expectedIndex = -1
            int actualIndex = -1
            Object expectedValue
            Object actualValue
            String diffType  // "missing", "extra", "different"

            String toString() {
                switch (diffType) {
                    case "missing":
                        def expectedPath = JsonComparatorConfig.buildJsonPath(path, null, expectedIndex)
                        return "期望值: ${expectedPath}: ${formatValue(expectedValue)}\n实际值: <不存在>"
                    case "extra":
                        def actualPath = JsonComparatorConfig.buildJsonPath(path, null, actualIndex)
                        return "期望值: <不存在>\n实际值: ${actualPath}: ${formatValue(actualValue)}"
                    case "different":
                        def expectedPath = JsonComparatorConfig.buildJsonPath(path, null, expectedIndex)
                        def actualPath = JsonComparatorConfig.buildJsonPath(path, null, actualIndex)
                        return "期望值: ${expectedPath}: ${formatValue(expectedValue)}\n实际值: ${actualPath}: ${formatValue(actualValue)}"
                    default:
                        return "路径: $path 未知差异类型"
                }
            }
        }

        void addDifference(String path, Object expected, Object actual) {
            isEqual = false
            differences.add("期望值: $path: ${formatValue(expected)}\n实际值: $path: ${formatValue(actual)}".toString())
        }

        void addArrayDiff(ArrayDiffInfo diffInfo) {
            isEqual = false
            arrayDiffs.add(diffInfo)
        }

        private static String formatValue(Object value) {
            if (value == null) return "null"
            if (value instanceof String) return "\"$value\""
            if (value instanceof Number) return value.toString()
            if (value instanceof List) return "数组(${value.size()}个元素)"
            if (value instanceof Map) return "对象(${value.size()}个属性)"
            return value.toString()
        }

        String getSummary() {
            if (isEqual) {
                return "JSON对比结果: 完全匹配"
            } else {
                def summary = new StringBuilder()
                //数组差异和普通差异是有重合的
                summary.append("JSON对比结果: 发现 ${differences.size()} 处普通差异, ${arrayDiffs.size()} 处数组差异\n")

                if (!differences.isEmpty()) {
                    summary.append("\n=== 普通差异 ===\n")
                    differences.each { diff ->
                        summary.append(diff).append("\n").append("-" * 50).append("\n")
                    }
                }

                if (!arrayDiffs.isEmpty()) {
                    summary.append("\n=== 数组差异 ===\n")
                    arrayDiffs.each { diff ->
                        summary.append(diff.toString()).append("\n").append("-" * 50).append("\n")
                    }
                }

                return summary.toString()
            }
        }
    }
    
    /**
     * JSON5解析工具
     */
    static class Json5Parser {
        /**
         * 解析JSON5字符串
         */
        static Object parseJson5(String json5Str) {
            try {
                // 使用fastjson2的JSON5特性
                // 注意:fastjson2的JSON5支持有限
                return JSON.parse(json5Str, JSONReader.Feature.AllowUnQuotedFieldNames)
            } catch (Exception e) {
                def ex = new IllegalArgumentException("JSON5解析失败: $json5Str", e)
                ex.setStackTrace(new StackTraceElement[0])
                throw ex
            }
        }
        
        /**
         * 判断字符串是否是JSON5格式
         */
        static boolean isJson5String(String str) {
            if (str == null || str.trim().isEmpty()) return false
            
            def trimmed = str.trim()
            // 检查是否是JSON对象或数组
            return (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
                   (trimmed.startsWith('[') && trimmed.endsWith(']'))
        }
        
        /**
         * 尝试解析字符串为JSON对象
         */
        static Object tryParseAsJson(String str) {
            if (!isJson5String(str)) return str
            
            try {
                return parseJson5(str)
            } catch (Exception e) {
                return str  // 解析失败,返回原始字符串
            }
        }
    }
    
    /**
     * 字符串对比工具
     */
    static class StringComparator {
        /**
         * 计算字符串相似度(Levenshtein距离)
         */
        static int calculateSimilarity(String str1, String str2) {
            if (str1 == null || str2 == null) return 0
            if (str1 == str2) return 100
            
            def len1 = str1.length()
            def len2 = str2.length()
            
            if (len1 == 0 || len2 == 0) return 0
            
            // 计算Levenshtein距离
            def matrix = new int[len1 + 1][len2 + 1]
            
            for (int i = 0; i <= len1; i++) matrix[i][0] = i
            for (int j = 0; j <= len2; j++) matrix[0][j] = j
            
            for (int i = 1; i <= len1; i++) {
                for (int j = 1; j <= len2; j++) {
                    def cost = str1.charAt(i - 1) == str2.charAt(j - 1) ? 0 : 1
                    matrix[i][j] = Math.min(
                        matrix[i - 1][j] + 1,      // 删除
                        Math.min(
                            matrix[i][j - 1] + 1,  // 插入
                            matrix[i - 1][j - 1] + cost  // 替换
                        )
                    )
                }
            }
            
            def distance = matrix[len1][len2]
            def maxLen = Math.max(len1, len2)
            def similarity = (1 - distance / (double) maxLen) * 100
            
            return similarity as int
        }
        
        /**
         * 字符串包含匹配
         */
        static boolean containsMatch(String str, String pattern) {
            if (str == null || pattern == null) return false
            // 检查字符串是否包含指定的模式
            return str.contains(pattern)
        }
        
        /**
         * 对比两个字符串
         */
        static boolean compareStrings(String str1, String str2, JsonComparatorConfig.StringCompareMode mode, 
                                      int similarityThreshold = 80, String containsPattern = null, String regexPattern = null) {
            if (str1 == null && str2 == null) return true
            if (str1 == null || str2 == null) return false

            switch (mode) {
                case JsonComparatorConfig.StringCompareMode.EXACT:
                    return str1 == str2

                case JsonComparatorConfig.StringCompareMode.CASE_INSENSITIVE:
                    return str1.equalsIgnoreCase(str2)

                case JsonComparatorConfig.StringCompareMode.TRIM:
                    return str1.trim() == str2.trim()

                case JsonComparatorConfig.StringCompareMode.CONTAINS:
                    return containsPattern ? containsMatch(str1, containsPattern) && containsMatch(str2, containsPattern) : false
                    
                case JsonComparatorConfig.StringCompareMode.SIMILARITY:
                    return calculateSimilarity(str1, str2) >= similarityThreshold
                    
                case JsonComparatorConfig.StringCompareMode.REGEX:
                    return regexPattern ? str1.matches(regexPattern) && str2.matches(regexPattern) : false
                    
                default:
                    return str1 == str2
            }
        }
    }
    
    /**
     * 数字对比工具
     */
    static class NumberComparator {
        /**
         * 对比两个数字
         */
        static boolean compareNumbers(Number num1, Number num2, JsonComparatorConfig.NumberCompareMode mode,
                                      double tolerance = 0.000001, int decimalPlaces = -1) {
            if (num1 == null && num2 == null) return true
            if (num1 == null || num2 == null) return false
            
            def d1 = num1.doubleValue()
            def d2 = num2.doubleValue()
            
            switch (mode) {
                case JsonComparatorConfig.NumberCompareMode.EXACT:
                    return d1 == d2
                    
                case JsonComparatorConfig.NumberCompareMode.WITHIN_TOLERANCE:
                    return Math.abs(d1 - d2) <= tolerance
                    
                case JsonComparatorConfig.NumberCompareMode.DECIMAL_PLACES:
                    if (decimalPlaces < 0) return d1 == d2
                    
                    def factor = Math.pow(10, decimalPlaces)
                    def rounded1 = Math.round(d1 * factor) / factor
                    def rounded2 = Math.round(d2 * factor) / factor
                    return rounded1 == rounded2
                    
                default:
                    return d1 == d2
            }
        }
    }
    
    /**
     * 主对比方法
     */
    static ComparisonResult compare(String expect, String real, JsonComparatorConfig.GlobalConfig config = null) {
        def actualConfig = config ?: JsonComparatorConfig.DEFAULT_CONFIG
        
        // 解析JSON(支持JSON5)
        def obj1 = parseJson(expect, actualConfig.enableJson5)
        def obj2 = parseJson(real, actualConfig.enableJson5)
        
        return compareObjects(obj1, obj2, "", actualConfig)
    }
    
    /**
     * 解析JSON(支持JSON5)
     */
    private static Object parseJson(String jsonStr, boolean enableJson5) {
        if (enableJson5) {
            return Json5Parser.parseJson5(jsonStr)
        } else {
            return JSON.parse(jsonStr)
        }
    }
    
    /**
     * 对比两个对象
     */
    private static ComparisonResult compareObjects(Object obj1, Object obj2, String basePath, 
                                                   JsonComparatorConfig.GlobalConfig config) {
        def result = new ComparisonResult()

        // 检查是否需要忽略该路径
        def pathConfig = JsonComparatorConfig.getPathConfig(config, basePath)
        if (pathConfig?.ignore) {
            return result  // 忽略该路径的对比
        }
        
        // 处理null值
        if (obj1 == null && obj2 == null) {
            return result
        }
        if (obj1 == null || obj2 == null) {
            result.addDifference(basePath ?: "root", obj1, obj2)
            return result
        }
        
        // 类型检查
        def type1 = getObjectType(obj1)
        def type2 = getObjectType(obj2)
        
        if (type1 != type2) {
            // 尝试类型转换和对比
            if (!tryTypeConversionCompare(obj1, obj2, basePath, config, result)) {
                result.addDifference(basePath ?: "root", obj1, obj2)
            }
            return result
        }
        
        // 根据类型进行对比
        switch (type1) {
            case "Map":
                compareMaps(obj1 as Map, obj2 as Map, basePath, config, result)
                break
                
            case "List":
                compareArrays(obj1 as List, obj2 as List, basePath, config, result)
                break
                
            case "String":
                compareStrings(obj1 as String, obj2 as String, basePath, config, pathConfig, result)
                break
                
            case "Number":
                compareNumbers(obj1 as Number, obj2 as Number, basePath, config, pathConfig, result)
                break
                
            case "Boolean":
                if (obj1 != obj2) {
                    result.addDifference(basePath ?: "root", obj1, obj2)
                }
                break
                
            default:
                if (!obj1.equals(obj2)) {
                    result.addDifference(basePath ?: "root", obj1, obj2)
                }
        }
        
        return result
    }
    
    /**
     * 获取对象类型
     */
    private static String getObjectType(Object obj) {
        if (obj instanceof Map) return "Map"
        if (obj instanceof List) return "List"
        if (obj instanceof String) return "String"
        if (obj instanceof Number) return "Number"
        if (obj instanceof Boolean) return "Boolean"
        return obj.getClass().simpleName
    }
    
    /**
     * 判断是否是简单类型(字符串、数字、布尔、null)
     */
    private static boolean isSimpleType(Object obj) {
        return obj == null || 
               obj instanceof String || 
               obj instanceof Number || 
               obj instanceof Boolean
    }
    
    /**
     * 尝试类型转换和对比
     */
    private static boolean tryTypeConversionCompare(Object obj1, Object obj2, String basePath,
                                                    JsonComparatorConfig.GlobalConfig config,
                                                    ComparisonResult result) {
        // 字符串和数字之间的转换
        if (obj1 instanceof String && obj2 instanceof Number) {
            try {
                def num1 = Double.parseDouble(obj1 as String)
                return compareNumbers(num1, obj2 as Number, basePath, config, 
                                     JsonComparatorConfig.getPathConfig(config, basePath), result)
            } catch (Exception e) {
                return false
            }
        }
        
        if (obj1 instanceof Number && obj2 instanceof String) {
            try {
                def num2 = Double.parseDouble(obj2 as String)
                return compareNumbers(obj1 as Number, num2, basePath, config,
                                     JsonComparatorConfig.getPathConfig(config, basePath), result)
            } catch (Exception e) {
                return false
            }
        }
        
        return false
    }
    
    /**
     * 对比两个Map
     */
    private static void compareMaps(Map map1, Map map2, String basePath, 
                                    JsonComparatorConfig.GlobalConfig config,
                                    ComparisonResult result) {
        // 将所有的key转换为String,避免GString问题
        def keys1 = map1.keySet().collect { it.toString() }
        def keys2 = map2.keySet().collect { it.toString() }
        def allKeys = (keys1 + keys2).unique()
        
        allKeys.each { key ->
            def path = JsonComparatorConfig.buildJsonPath(basePath, key)
            
            // 使用String类型的key进行查找
            def hasKey1 = map1.any { k, v -> k.toString() == key }
            def hasKey2 = map2.any { k, v -> k.toString() == key }
            
            if (hasKey1 && hasKey2) {
                // 获取实际的值
                def value1 = map1.find { k, v -> k.toString() == key }?.value
                def value2 = map2.find { k, v -> k.toString() == key }?.value
                
                def subResult = compareObjects(value1, value2, path, config)
                if (!subResult.isEqual) {
                    result.isEqual = false
                    result.differences.addAll(subResult.differences)
                    result.arrayDiffs.addAll(subResult.arrayDiffs)
                }
            } else if (hasKey1) {
                def value1 = map1.find { k, v -> k.toString() == key }?.value
                result.addDifference(path, value1, "缺失")
            } else {
                def value2 = map2.find { k, v -> k.toString() == key }?.value
                result.addDifference(path, "缺失", value2)
            }
        }
    }
    
    /**
     * 对比两个数组(支持长度不同)
     */
    private static void compareArrays(List list1, List list2, String basePath,
                                      JsonComparatorConfig.GlobalConfig config,
                                      ComparisonResult result) {
        // 标记已匹配的元素
        def matched1 = new boolean[list1.size()]
        def matched2 = new boolean[list2.size()]
        
        // 第一阶段:匹配完全相等的元素
        for (int i = 0; i < list1.size(); i++) {
            if (matched1[i]) continue
            
            for (int j = 0; j < list2.size(); j++) {
                if (matched2[j]) continue
                
                def subResult = compareObjects(list1[i], list2[j], 
                                              JsonComparatorConfig.buildJsonPath(basePath, null, i), config)
                if (subResult.isEqual) {
                    matched1[i] = true
                    matched2[j] = true
                    break
                }
            }
        }
        
        // 第二阶段:处理未匹配的元素
        for (int i = 0; i < list1.size(); i++) {
            if (matched1[i]) continue
            
            // 寻找最相似的元素
            def bestMatchIndex = -1
            def bestMatchScore = -1
            
            for (int j = 0; j < list2.size(); j++) {
                if (matched2[j]) continue
                
                def score = calculateElementSimilarity(list1[i], list2[j], config)
                if (score > bestMatchScore) {
                    bestMatchScore = score
                    bestMatchIndex = j
                }
            }
            
            if (bestMatchIndex != -1) {
                // 对比相似元素
                def subResult = compareObjects(list1[i], list2[bestMatchIndex],
                                              JsonComparatorConfig.buildJsonPath(basePath, null, i), config)
                if (!subResult.isEqual) {
                    result.isEqual = false
                    result.differences.addAll(subResult.differences)
                    result.arrayDiffs.addAll(subResult.arrayDiffs)
                    
                    // 添加数组差异信息
                    def diffInfo = new ComparisonResult.ArrayDiffInfo(
                        path: basePath,
                        expectedIndex: i,
                        actualIndex: bestMatchIndex,
                        expectedValue: list1[i],
                        actualValue: list2[bestMatchIndex],
                        diffType: "different"
                    )
                    result.addArrayDiff(diffInfo)
                }
                matched1[i] = true
                matched2[bestMatchIndex] = true
            }
        }
        
        // 第三阶段:处理剩余未匹配的元素
        // 尝试为每个未匹配的元素找到最相似的对应元素
        for (int i = 0; i < list1.size(); i++) {
            if (matched1[i]) continue
            
            // 寻找最相似的元素
            def bestMatchIndex = -1
            def bestMatchScore = -1
            
            for (int j = 0; j < list2.size(); j++) {
                if (matched2[j]) continue
                
                def score = calculateElementSimilarity(list1[i], list2[j], config)
                if (score > bestMatchScore) {
                    bestMatchScore = score
                    bestMatchIndex = j
                }
            }
            
            if (bestMatchIndex != -1) {
                // 找到相似元素,记录差异
                def diffInfo = new ComparisonResult.ArrayDiffInfo(
                    path: basePath,
                    expectedIndex: i,
                    actualIndex: bestMatchIndex,
                    expectedValue: list1[i],
                    actualValue: list2[bestMatchIndex],
                    diffType: "different"
                )
                result.addArrayDiff(diffInfo)
                result.isEqual = false
                matched1[i] = true
                matched2[bestMatchIndex] = true
            } else {
                // 没有找到相似元素,标记为缺失
                def diffInfo = new ComparisonResult.ArrayDiffInfo(
                    path: basePath,
                    expectedIndex: i,
                    expectedValue: list1[i],
                    diffType: "missing"
                )
                result.addArrayDiff(diffInfo)
                result.isEqual = false
            }
        }
        
        // 处理list2中剩余的未匹配元素(多余的元素)
        for (int j = 0; j < list2.size(); j++) {
            if (!matched2[j]) {
                def diffInfo = new ComparisonResult.ArrayDiffInfo(
                    path: basePath,
                    actualIndex: j,
                    actualValue: list2[j],
                    diffType: "extra"
                )
                result.addArrayDiff(diffInfo)
                result.isEqual = false
            }
        }
    }
    
    /**
     * 计算元素相似度
     */
    private static int calculateElementSimilarity(Object obj1, Object obj2, JsonComparatorConfig.GlobalConfig config) {
        if (obj1 == null && obj2 == null) return 100
        if (obj1 == null || obj2 == null) return 0
        
        // 字符串相似度计算
        if (obj1 instanceof String && obj2 instanceof String) {
            return StringComparator.calculateSimilarity(obj1 as String, obj2 as String)
        }
        
        // 数字相似度计算 - 考虑容差
        if (obj1 instanceof Number && obj2 instanceof Number) {
            def num1 = obj1 as Number
            def num2 = obj2 as Number
            def diff = Math.abs(num1.doubleValue() - num2.doubleValue())
            
            // 如果数字完全相同,相似度为100
            if (diff == 0) return 100
            
            // 如果数字在容差范围内,相似度较高
            if (diff <= config.numberTolerance) return 90
            
            // 数字相差较大,计算相对差异
            def maxVal = Math.max(Math.abs(num1.doubleValue()), Math.abs(num2.doubleValue()))
            if (maxVal == 0) return 0
            
            def relativeDiff = diff / maxVal
            return Math.max(0, 100 - (relativeDiff * 100).intValue())
        }
        
        // 布尔值相似度
        if (obj1 instanceof Boolean && obj2 instanceof Boolean) {
            return obj1 == obj2 ? 100 : 0
        }
        
        // Map相似度计算
        if (obj1 instanceof Map && obj2 instanceof Map) {
            def map1 = obj1 as Map
            def map2 = obj2 as Map
            
            // 将所有的key转换为String,避免GString问题
            def keys1 = map1.keySet().collect { it.toString() }
            def keys2 = map2.keySet().collect { it.toString() }
            def commonKeys = keys1.intersect(keys2)
            
            return (commonKeys.size() * 100 / Math.max(map1.size(), map2.size())) as int
        }
        
        // List相似度计算
        if (obj1 instanceof List && obj2 instanceof List) {
            def list1 = obj1 as List
            def list2 = obj2 as List
            
            // 计算两个列表的相似度
            def minSize = Math.min(list1.size(), list2.size())
            def maxSize = Math.max(list1.size(), list2.size())
            
            if (maxSize == 0) return 100
            
            // 简单计算:基于长度的相似度
            def sizeSimilarity = (minSize * 100 / maxSize) as int
            
            return sizeSimilarity
        }
        
        // 其他类型:如果equals返回true,相似度为100
        if (obj1.equals(obj2)) return 100
        
        // 类型不同但可以转换的情况
        if ((obj1 instanceof String && obj2 instanceof Number) || 
            (obj1 instanceof Number && obj2 instanceof String)) {
            try {
                def num1 = obj1 instanceof Number ? obj1.doubleValue() : Double.parseDouble(obj1 as String)
                def num2 = obj2 instanceof Number ? obj2.doubleValue() : Double.parseDouble(obj2 as String)
                if (num1 == num2) return 100
            } catch (Exception e) {
                // 转换失败,返回0
            }
        }
        
        return 0
    }
    
    /**
     * 对比字符串
     */
    private static void compareStrings(String str1, String str2, String basePath,
                                       JsonComparatorConfig.GlobalConfig config,
                                       JsonComparatorConfig.PathConfig pathConfig,
                                       ComparisonResult result) {
        // 检查是否需要自动检测JSON
        if (config.autoDetectJsonInStrings && !hasSpecificStringConfig(pathConfig)) {
            def parsed1 = Json5Parser.tryParseAsJson(str1)
            def parsed2 = Json5Parser.tryParseAsJson(str2)
            
            // 如果两个字符串都能解析为JSON,则按JSON对比
            if (parsed1 != str1 && parsed2 != str2) {
                def subResult = compareObjects(parsed1, parsed2, basePath, config)
                if (!subResult.isEqual) {
                    result.isEqual = false
                    result.differences.addAll(subResult.differences)
                    result.arrayDiffs.addAll(subResult.arrayDiffs)
                }
                return
            }
        }
        
        // 使用配置的字符串对比模式
        def stringMode = pathConfig?.stringMode ?: config.defaultStringMode
        def similarityThreshold = pathConfig?.similarityThreshold ?: config.stringSimilarityThreshold
        def containsPattern = pathConfig?.containsPattern
        def regexPattern = pathConfig?.regexPattern

        if (!StringComparator.compareStrings(str1, str2, stringMode, similarityThreshold, containsPattern, regexPattern)) {
            result.addDifference(basePath ?: "root", str1, str2)
        }
    }
    
    /**
     * 对比数字
     */
    private static boolean compareNumbers(Number num1, Number num2, String basePath,
                                         JsonComparatorConfig.GlobalConfig config,
                                         JsonComparatorConfig.PathConfig pathConfig,
                                         ComparisonResult result) {
        def numberMode = pathConfig?.numberMode ?: config.defaultNumberMode
        def tolerance = pathConfig?.tolerance ?: config.numberTolerance
        def decimalPlaces = pathConfig?.decimalPlaces ?: config.decimalPlaces
        
        if (!NumberComparator.compareNumbers(num1, num2, numberMode, tolerance, decimalPlaces)) {
            result.addDifference(basePath ?: "root", num1, num2)
            return false
        }
        return true
    }
    
    /**
     * 检查是否有特定的字符串配置
     */
    private static boolean hasSpecificStringConfig(JsonComparatorConfig.PathConfig pathConfig) {
        return pathConfig?.stringMode != null || 
               pathConfig?.containsPattern != null ||
               pathConfig?.regexPattern != null ||
               pathConfig?.similarityThreshold != null
    }
    
}

/**
 * JSON对比工具配置类
 * 支持全局和JSONPath级别的解析规则配置
 */
@CompileStatic
class JsonComparatorConfig {

    /**
     * 对比模式
     */
    enum CompareMode {
        STRICT,          // 严格对比
        LENIENT,         // 宽松对比
        SIMILARITY       // 相似度对比
    }

    /**
     * 字符串对比模式
     */
    enum StringCompareMode {
        EXACT,           // 精确匹配
        CASE_INSENSITIVE, // 忽略大小写
        TRIM,            // 去除空格后对比
        CONTAINS,        // 包含模式
        SIMILARITY,      // 相似度对比(0-100)
        REGEX            // 正则表达式匹配
    }

    /**
     * 数字对比模式
     */
    enum NumberCompareMode {
        EXACT,           // 精确匹配
        WITHIN_TOLERANCE, // 在容差范围内
        DECIMAL_PLACES   // 指定小数位数
    }

    /**
     * 全局配置
     */
    static class GlobalConfig {
        public CompareMode compareMode = CompareMode.STRICT
        public boolean enableJson5 = true
        public boolean autoDetectJsonInStrings = true
        public double numberTolerance = 0.000001
        public int decimalPlaces = -1  // -1表示不限制小数位数
        public int stringSimilarityThreshold = 80  // 字符串相似度阈值(0-100)
        public StringCompareMode defaultStringMode = StringCompareMode.EXACT
        public NumberCompareMode defaultNumberMode = NumberCompareMode.EXACT

        // JSONPath级别的特定配置
        public Map<String, PathConfig> pathConfigs = [:]
    }

    /**
     * JSONPath级别的配置
     */
    static class PathConfig {
        public StringCompareMode stringMode
        public NumberCompareMode numberMode
        public Double tolerance
        public Integer decimalPlaces
        public Integer similarityThreshold
        public String containsPattern
        public String regexPattern
        public Boolean ignore = false  // 是否忽略该路径
    }

    /**
     * 默认全局配置
     */
    static final GlobalConfig DEFAULT_CONFIG = new GlobalConfig()

    /**
     * 创建宽松对比配置
     */
    static GlobalConfig createLenientConfig() {
        def config = new GlobalConfig()
        config.compareMode = CompareMode.LENIENT
        config.numberTolerance = 0.01
        config.stringSimilarityThreshold = 70
        config.defaultStringMode = StringCompareMode.TRIM
        config.defaultNumberMode = NumberCompareMode.WITHIN_TOLERANCE
        return config
    }

    /**
     * 创建相似度对比配置
     */
    static GlobalConfig createSimilarityConfig() {
        def config = new GlobalConfig()
        config.compareMode = CompareMode.SIMILARITY
        config.stringSimilarityThreshold = 60
        config.defaultStringMode = StringCompareMode.SIMILARITY
        return config
    }

    /**
     * 添加JSONPath配置
     */
    static void addPathConfig(GlobalConfig config, String jsonPath, PathConfig pathConfig) {
        config.pathConfigs[jsonPath] = pathConfig
    }

    /**
     * 获取指定路径的配置
     */
    static PathConfig getPathConfig(GlobalConfig config, String jsonPath) {
        // 查找最匹配的路径配置
        def matchedConfig = config.pathConfigs.find { pathPattern, pathConfig ->
            matchJsonPath(jsonPath, pathPattern)
        }?.value

        return matchedConfig
    }

    /**
     * 简单的JSONPath匹配(支持通配符*)
     */
    private static boolean matchJsonPath(String actualPath, String pattern) {
        if (actualPath == pattern) return true

        // 将通配符*转换为正则表达式.*
        def regexPattern = pattern.replaceAll(/\./, /\\./)
                .replaceAll(/\*/, /.*/)
                .replaceAll(/\[(\d+)\]/, /\\[$1\\]/)

        return actualPath.matches(regexPattern)
    }

    /**
     * 构建JSONPath
     */
    static String buildJsonPath(String basePath, String key, int index = -1) {
        def path = basePath ?: ""
        if (key) {
            path = path ? "$path.$key" : key
        }
        if (index >= 0) {
            path = path ? "$path[$index]" : "[$index]"
        }
        return path
    }
}

使用案例

groovy 复制代码
package local.my

import groovy.transform.CompileStatic

/**
 * 增强版JSON对比工具使用示例
 */
@CompileStatic
class EnhancedJsonComparatorExample {

    static void main(String[] args) {
        println("=== 增强版JSON对比工具示例 ===\n")
        ex1()
        ex2()
        ex2_2()
        ex2_3()
        ex2_4()
        ex3()
        ex4()
        ex5()
        ex6()
        ex7()
        ex8()
        ex9()
        ex10()
        ex11()
        ex12()
        ex13()
        println("\n=== 所有示例执行完成 ===")
    }

    private static void ex1(){
        // 示例1: JSON5解析支持
        println("示例1: JSON5解析支持")
        def json5_1 = '''
            {
                // 这是JSON5注释
                name: '张三',  // 单引号字符串
                age: 25,
                active: true,
                tags: ['tag1', 'tag2',],  // 尾随逗号
            }
        '''

        def json5_2 = '''
            {
                "name": "张三",
                "age": 25,
                "active": true,
                "tags": ["tag2", "tag1"]
            }
        '''

        def result1 = EnhancedJsonComparator.compare(json5_1, json5_2)
        println(result1.summary)
        println()
    }

    private static void ex2(){
        // 示例2: 数组数据不同对比
        println("示例2.1: 数组数据不同对比")
        def array1 = '''
            {
                "items": [
                    {"id": 1, "name": "苹果"},
                    {"id": 2, "name": "香蕉"},
                    {"id": 3, "name": "橙子"},
                    "黄瓜","白菜",123
                ]
            }
        '''

        def array2 = '''
            {
                "items": [
                    {"id": 2, "name": "香蕉"},
                    {"id": 1, "name": "苹果"},
                    {"id": 4, "name": "葡萄"},
                    "白菜","黄瓜",1234
                ]
            }
        '''

        def result2 = EnhancedJsonComparator.compare(array1, array2)
        println(result2.summary)
        println()
    }

    private static void ex2_2(){
        // 示例2.2: 数组长度不同对比
        println("示例2.2: 数组长度不同对比(期望多了)")
        def array1 = '''
            {
                "items": [
                    {"id": 1, "name": "苹果"},
                    {"id": 2, "name": "香蕉"},
                    {}
                ]
            }
        '''

        def array2 = '''
            {
                "items": [
                    {"id": 2, "name": "香蕉"},
                    {"id": 1, "name": "苹果"},
                ]
            }
        '''

        def result2 = EnhancedJsonComparator.compare(array1, array2)
        println(result2.summary)
        println()
    }

    private static void ex2_3(){
        // 示例2.3: 数组长度不同对比
        println("示例2.3: 数组长度不同对比(实际多了)")
        def array1 = '''
            {
                "items": [
                    {"id": 1, "name": "苹果"},
                    {"id": 2, "name": "香蕉"},
                ]
            }
        '''

        def array2 = '''
            {
                "items": [
                    {"id": 2, "name": "香蕉"},
                    {"id": 1, "name": "苹果"},
                    {id: 3}
                ]
            }
        '''

        def result2 = EnhancedJsonComparator.compare(array1, array2)
        println(result2.summary)
        println()
    }

    private static void ex2_4(){
        // 示例2.4: 数组嵌套不同对比
        println("示例2.4: 数组嵌套不同对比")
        def array1 = '''
            {
                "items": [
                    [1,2],
                    [1]
                ]
            }
        '''

        def array2 = '''
            {
                "items": [
                    [1],
                    [1,3],
                ]
            }
        '''

        def result2 = EnhancedJsonComparator.compare(array1, array2)
        println(result2.summary)
        println()
    }

    private static void ex3(){
        // 示例3: 字符串自动JSON检测
        println("示例3: 字符串自动JSON检测")
        def strJson1 = '''
            {
                "user": {
                    "name": "张三",
                    "profile": "{\\"email\\": \\"zhangsan@example.com\\", \\"age\\": 25}"
                }
            }
        '''

        def strJson2 = '''
            {
                "user": {
                    "name": "张三",
                    "profile": "{\\"email\\": \\"zhangsan@test.com\\", \\"age\\": 25}"
                }
            }
        '''

        def result3 = EnhancedJsonComparator.compare(strJson1, strJson2)
        println(result3.summary)
        println()
    }

    private static void ex4(){
        // 示例4: 自定义配置 - 宽松对比
        println("示例4: 自定义配置 - 宽松对比")
        def config1 = JsonComparatorConfig.createLenientConfig()

        def num1 = '{"price": 10.005, "quantity": 100}'
        def num2 = '{"price": 10.01, "quantity": 100}'

        def result4 = EnhancedJsonComparator.compare(num1, num2, config1)
        println("宽松对比结果: ${result4.isEqual ? '通过' : '失败'}")
        println()
    }

    private static void ex5(){
        // 示例5: JSONPath级别配置
        println("示例5: JSONPath级别配置")
        def config2 = JsonComparatorConfig.DEFAULT_CONFIG

        // 配置特定路径的对比规则
        def pathConfig1 = new JsonComparatorConfig.PathConfig(
                stringMode: JsonComparatorConfig.StringCompareMode.CASE_INSENSITIVE
        )
        JsonComparatorConfig.addPathConfig(config2, "user.name", pathConfig1)

        def pathConfig2 = new JsonComparatorConfig.PathConfig(
                numberMode: JsonComparatorConfig.NumberCompareMode.DECIMAL_PLACES,
                decimalPlaces: 2
        )
        JsonComparatorConfig.addPathConfig(config2, "*.price", pathConfig2)

        def custom1 = '''
            {
                "user": {"name": "ZHANGSAN", "age": 25},
                "order": {"price": 10.555, "quantity": 100}
            }
        '''

        def custom2 = '''
            {
                "user": {"name": "zhangsan", "age": 25},
                "order": {"price": 10.56, "quantity": 100}
            }
        '''

        def result5 = EnhancedJsonComparator.compare(custom1, custom2, config2)
        println("JSONPath配置对比结果: ${result5.isEqual ? '通过' : '失败'}")
        if (!result5.isEqual) {
            println(result5.summary)
        }
        println()
    }

    private static void ex6(){
        // 示例6: 字符串相似度对比
        println("示例6: 字符串相似度对比")
        def config3 = JsonComparatorConfig.createSimilarityConfig()
        config3.stringSimilarityThreshold = 70

        def similar1 = '{"description": "这是一个测试字符串用于演示相似度对比"}'
        def similar2 = '{"description": "这是一个测试字串用于演示相似度比较"}'

        def result6 = EnhancedJsonComparator.compare(similar1, similar2, config3)
        println("相似度对比结果: ${result6.isEqual ? '通过' : '失败'}")
        println()
    }

    private static void ex7(){
        // 示例7: CONTAINS模式匹配
        println("示例7: CONTAINS模式匹配")
        def config4 = JsonComparatorConfig.DEFAULT_CONFIG

        def likeConfig = new JsonComparatorConfig.PathConfig(
                stringMode: JsonComparatorConfig.StringCompareMode.CONTAINS,
                containsPattern: "test"
        )
        JsonComparatorConfig.addPathConfig(config4, "data.value", likeConfig)

        def like1 = '{"data": {"value": "test123", "type": "string"}}'
        def like2 = '{"data": {"value": "test456", "type": "string"}}'

        def result7 = EnhancedJsonComparator.compare(like1, like2, config4)
        println("CONTAINS模式对比结果: ${result7.isEqual ? '通过' : '失败'}")
        println()
    }

    private static void ex8(){
        // 示例8: 忽略特定路径
        println("示例8: 忽略特定路径")
        def config5 = JsonComparatorConfig.DEFAULT_CONFIG

        def ignoreConfig = new JsonComparatorConfig.PathConfig(ignore: true)
        JsonComparatorConfig.addPathConfig(config5, "metadata.timestamp", ignoreConfig)

        def ignore1 = '''
            {
                "name": "张三",
                "age": 25,
                "metadata": {
                    "timestamp": "2025-12-26T10:00:00",
                    "version": "1.0"
                }
            }
        '''

        def ignore2 = '''
            {
                "name": "张三",
                "age": 25,
                "metadata": {
                    "timestamp": "2025-12-26T11:00:00",
                    "version": "1.0"
                }
            }
        '''

        def result8 = EnhancedJsonComparator.compare(ignore1, ignore2, config5)
        println("忽略路径对比结果: ${result8.isEqual ? '通过' : '失败'}")
        println()
    }
    
    private static void ex9(){
        // 示例9: 简单类型数组对比 - 数字不同
        println("示例9: 简单类型数组对比 - 数字不同")
        def simple1 = '[123]'
        def simple2 = '[1234]'

        def result9 = EnhancedJsonComparator.compare(simple1, simple2)
        println("数字数组对比结果: ${result9.isEqual ? '通过' : '失败'}")
        if (!result9.isEqual) {
            println(result9.summary)
        }
        println()
    }

    private static void ex10(){
        // 示例10: 简单类型数组对比 - 混合类型顺序不同
        println("示例10: 简单类型数组对比 - 混合类型顺序不同")
        def mixed1 = '{a:[1, "a"]}'
        def mixed2 = '{a:["b", 1]}'

        def result10 = EnhancedJsonComparator.compare(mixed1, mixed2)
        println("混合类型数组对比结果: ${result10.isEqual ? '通过' : '失败'}")
        if (!result10.isEqual) {
            println(result10.summary)
        }
        println()
    }

    private static void ex11(){
        // 示例11: 简单类型数组对比 - 布尔和null值
        println("示例11: 简单类型数组对比 - 布尔和null值")
        def bool1 = '[true, false, null]'
        def bool2 = '[false, true, null]'

        def result11 = EnhancedJsonComparator.compare(bool1, bool2)
        println("布尔和null数组对比结果: ${result11.isEqual ? '通过' : '失败'}")
        if (!result11.isEqual) {
            println(result11.summary)
        }
        println()
    }

    private static void ex12(){
        // 示例12: 复杂类型数组对比 - 对象数组
        println("示例12: 复杂类型数组对比 - 对象数组")
        def obj1 = '''
            [
                {"id": 1, "name": "张三"},
                {"id": 2, "name": "李四"},
                [2],[1]
            ]
        '''

        def obj2 = '''
            [
                {"id": 2, "name": "李四"},
                {"id": 1, "name": "张三"},
                [1],[2]
            ]
        '''

        def result12 = EnhancedJsonComparator.compare(obj1, obj2)
        println("对象数组对比结果: ${result12.isEqual ? '通过' : '失败'}")
        if (!result12.isEqual) {
            println(result12.summary)
        }
        println()
    }

    private static void ex13(){
        // 示例13: 混合类型数组对比 - 简单和复杂类型混合
        println("示例13: 混合类型数组对比 - 简单和复杂类型混合")
        def mixed1 = '''
            [
                123,
                "test",
                {"id": 1, "name": "张三"},
                true
            ]
        '''

        def mixed2 = '''
            [
                {"id": 1, "name": "张三"},
                123,
                true,
                "test",
            ]
        '''

        def result13 = EnhancedJsonComparator.compare(mixed1, mixed2)
        println("混合类型数组对比结果: ${result13.isEqual ? '通过' : '失败'}")
        if (!result13.isEqual) {
            println(result13.summary)
        }
        println()
    }
}
相关推荐
BD_Marathon2 小时前
Spring——核心概念
java·后端·spring
幽络源小助理2 小时前
SpringBoot+Vue数字科技风险报告管理系统源码 | Java项目免费下载 – 幽络源
java·vue.js·spring boot
ss2732 小时前
线程池配置-七大关键参数
java·开发语言
__万波__2 小时前
二十三种设计模式(十五)--访问者模式
java·设计模式·访问者模式
fanruitian2 小时前
SpringBoot 集成retrofit httpclient
java·spring boot·retrofit
talenteddriver2 小时前
web: jwt令牌构成、创建的基本流程及原理
java·开发语言·python·网络协议·web
码农水水2 小时前
宇树科技Java被问:数据库连接池的工作原理
java·数据库·后端·oracle
Seven972 小时前
回溯算法总结
java
小鸡脚来咯2 小时前
软链接的作用和用途
java·ide·eclipse