增强版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: 指定小数位数
算法说明
数组对比算法
- 第一阶段:匹配完全相等的元素
- 第二阶段:基于相似度匹配剩余元素
- 第三阶段:报告未匹配的元素(缺失/多余)
相似度计算
- 字符串相似度:使用Levenshtein距离算法
- 对象相似度:基于共同属性的比例
- 数组相似度:基于相同元素的比例
JSONPath匹配
支持简单的通配符匹配:
*: 匹配任意字符序列user.name: 精确匹配路径*.price: 匹配所有price字段
性能考虑
- 数组对比:时间复杂度O(n²),对于大型数组建议预处理
- JSON5解析:fastjson2提供高性能解析,支持可读性更强的json5格式
- 内存使用:深度对比可能消耗较多内存,建议分块处理大型JSON
扩展建议
- 缓存优化:对频繁对比的路径添加缓存
- 并行处理:大型数组可以并行计算相似度
- 增量对比:支持只对比变化的部分
- 自定义比较器:支持用户自定义类型比较器
代码
对比工具代码
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()
}
}