obj-compare是一款基于Java反射的轻量级对象比较工具。不仅支持常见的POJO对象、集合对象、数组类型比较功能,也支持高度自定义比较定制等功能。
<dependency>
<groupId>io.github.smartboot.compare</groupId>
<artifactId>obj-compare</artifactId>
<version>1.1.3</version>
</dependency>
@Test
public void testCompareSimple() {
User expect = new User();
expect.setId("1");
expect.setPassword("111212");
expect.setUsername("Tom");
expect.setSex("male");
User target = new User();
target.setId("2");
target.setPassword("1112112");
target.setUsername("Jerry");
target.setSex("male");
CompareResult result = CompareHelper.compare(expect, target);
System.out.println(result);
Assert.assertFalse(result.isSame());
}
================ ProcessId : 71ea6da9-977a-4777-a0cc-b362258d4284 ===============
● Result : false
● Options :
● Differences : 3
● Recycle : 0
● Escaped : 128
● MaxDepth : 1
- difference details :
1. Based, ROOT.id, 期望值 [1], 实际值 [2]
2. Based, ROOT.username, 期望值 [Tom], 实际值 [Jerry]
3. Based, ROOT.password, 期望值 [111212], 实际值 [1112112]
================ ProcessId : 71ea6da9-977a-4777-a0cc-b362258d4284 ===============
先比较size,size相等再按照index逐一进行对比
比较过程中,如果需要跳过部分属性的比较,例如时间、随机字符串等字段比较。有以下2种方式供您选择:
字段忽略器最终会被转化为字段过滤器执行,所以您不需要担心2者有太大差异。
// 过滤对象Field上未标注@Snapshoted的字段
FieldFilters.registerGlobal((f, context) -> f.getType() == Kind.FIELD && f.getField().getAnnotation(Snapshoted.class) == null);
CompareHelper.compare(expect, actual, 0, null, fieldFilter1,fieldFilter2);
List<IgnoreField> ignoreFields = new ArrayList<>();
// 忽略类型为String.class 字段名为id的 属性比较
ignoreFields.add(new IgnoreField("id", String.class));
CompareHelper.compare(expect, actual, ignoreFields);
List<IgnoreField> ignoreFields = new ArrayList<>();
// 忽略任何以word结尾的属性,类型为任意类型
IgnorePatternField ignorePatternField = new IgnorePatternField("[\\s\\d]*word");
ignoreFields.add(ignorePatternField);
CompareHelper.compare(expect, actual, ignoreFields);
================ ProcessId : a96cbaa7-d4e5-42dc-be52-5765df1487d6 ===============
● Result : true
● Options :
● Differences : 0
● Recycle : 0
● Escaped : 98
● MaxDepth : 0
skipped fields :
1:ROOT.id
2:ROOT.password
================ ProcessId : a96cbaa7-d4e5-42dc-be52-5765df1487d6 ===============
如果需要对对象的某些字段进行自定义比较,例如某字段具有特殊的格式,需要展开进行对比,这种场景有2种方式可以实现。
ComparatorRegister.register(String.class, new CustomizedStringComparator());
// 匹配名为attribute,值类型为String的字段项进行比较
ComparatorRegister.register(NameType.of("attribute", String.class), AttributesComparator.getInstance());
// 匹配名为c或者d,值类型为Integer的字段项进行比较
ComparatorRegister.register(NamesType.of(Integer.class, "c", "d"), new AbstractComparator<Integer>() {
@Override
public Difference compare(Integer expect, Integer actual, ComparatorContext<Integer> context) {
return Difference.SAME;
}
});
自定义类型/属性比较不仅适用于POJO对象,也适用于Map中某个key的比较。
如果多线程下比较的情况,针对以下case
使用常规的ComparatorRegister
无法满足要求,它们之前注册的会相互进行影响,根据此情况,提供了ThreadLocalComparatorRegister
来支持。
ComparatorRegister
注册的比较器仍然会全局生效,覆盖了自带的比较器在比较完后也不会自动重置,需要手动处理ThreadLocalComparatorRegister
与ComparatorRegister
同理,但作用范围在当前线程内ThreadLocalComparatorRegister
注册的比较器不会在子线程中派生出的线程生效(因为非InheritableThreadLocal
实现)ThreadLocalComparatorRegister
如果需要移除,通过方法removeAll
移除所有或者使用removeAll
单个移除注册的比较器对于两个对象之间的循环依赖字段(非基本类型及其包装类型),对比过程中将会一一记录
对象的User字段相互引用
@Test
public void testRecycle() {
RecycleUser user = new RecycleUser();
RecycleUser _user = new RecycleUser();
user.setName("qinluo");
user.setPassword("haolo2");
user.setUser(_user);
_user.setName("qinluo");
_user.setPassword("haolo1");
_user.setUser(user);
CompareResult result = CompareHelper.compare(user, _user);
System.out.println(result);
}
================ ProcessId : a4b6b15b-a229-4224-9f0d-2ffa145ec673 ===============
● Result : false
● Options :
● Differences : 1
● Recycle : 1
● Escaped : 85
● MaxDepth : 1
messages :
1:detect recycle reference in path ROOT.user
difference details :
1:CommonDifference@ROOT.password, 期望值为 [haolo2], 实际值为 [haolo1]
================ ProcessId : a4b6b15b-a229-4224-9f0d-2ffa145ec673 ===============
json输出格式能够更直观清晰地观察结果,但需要自行引入相关json序列化包并完成序列器初始化
// 初始化gson的json序列化
JsonSerializer.setDefaultInstance(new GsonSerializer());
JsonSerializer.setDefaultInstance(new GsonSerializer());
Map<String, Object> expect = new HashMap<>();
expect.put("a", 1);
expect.put("b", "123456888");
expect.put("c", "hhaks");
Map<String, Object> actual = new HashMap<>();
actual.put("a", 1);
actual.put("b", "1234567890");
actual.put("c", "hhaklsa");
actual.put("d", 5);
CompareResult result = CompareHelper.compare(expect, actual);
// 使用json序列化输出结果
System.out.println(new JsonResultViewer(result));
{
"skippedFields": [],
"differences": 3,
"differenceDetails": {
"ROOT.(key)b": {
"expect": "123456888",
"actual": "1234567890",
"level": 1,
"type": "Based"
},
"ROOT.(key)c": {
"expect": "hhaks",
"actual": "hhaklsa",
"level": 1,
"type": "Based"
},
"ROOT.(key)d": {
"expect": "null",
"actual": "5",
"level": 1,
"type": "Based"
}
},
"maxDepth": 1,
"escaped": 64,
"result": false,
"options": "",
"messages": [],
"id": "ed48edb8-e41b-443a-922f-283838b35679",
"recycleCnt": 0
}
1.1.1版本新增Configuration接口,用户可实现接口后,将自定义配置信息放入里面
import org.smartboot.compare.Configuration;
class CustomConfiguration implements Configuration {
// 自定义配置
}
1.1.1版本为了增加比较过程中的自定义处理,新增FeatureFunction功能。目前1.1.1版本中支持以下几个特性
default Boolean isEffectOption(ComparatorContext<?> ctx, Option option) {
return true;
}
default Object convertString2Object(ComparatorContext<?> ctx, String value) {
return value;
}
default void sort(ComparatorContext<?> ctx, Object expectArray, Object actualArray, int type) {
}
具体使用可以参照以下示例
ComparatorContext<Object> ctx = new ComparatorContext<>(options);
ctx.setExpect(expect);
ctx.setActual(actual);
ctx.setFeatureFunction(new FeatureFunction() {
private boolean supportConvert2Json(Path path) {
return true;
}
public Object convertString2Object(ComparatorContext<?> ctx, String value) {
if (!supportConvert2Json(ctx.getPath())) {
return null;
}
return JSON.toJSONObject(value);
}
});
属性 | 类型 | 释义 |
---|---|---|
differences | List<Difference> | 差异项列表,为空说明比较结果一致 |
skippedFields | List<String> | 比较过程中跳过比较的属性列表 |
messages | List<String> | 比较过程中添加的一些信息 |
recycleCnt | int | 循环依赖次数 |
options | long | 比较选项bits |
escaped | long | 比较耗时,单位ms |
id | String | 比较id,默认为UUID |
maxDepth | int | 比较层级的最大深度 |
默认所有自带的Difference都继承AbstractDifference, 自带路径path
Type | 类型 | 释义 |
---|---|---|
Based | BaseDifference | 基本差异对象,包括expect和actual |
Size | SizeDifference | Array/List/Set/Map 期望大小不一致,expect和actual为对应size |
NullOfOne | NullOfOneObject | expect和actual其中一个对象为空 |
ComplementSet | ComplementSetDifference | 用于集合/数组之间的差异,会记录期望集合与实际集合各自的补集 |
TypeUnmatched | TypeDifference | expect和actual的类型不一致 |
Error | DifferenceError | 调用POJO自带的equals方法出现异常 |
CustomizedAttribute | AttributeCompareDifference | 默认提供的attribute比较不一致结果 |
在正常比较之于,obj-compare提供了一些可选项,每个可选项都会对对比过程产生影响。您可以使用以下方式进行设置一个或多个Option
CompareHelper.compare(expect, actual, Option.mix(LOOSE_MODE, DISABLE_EQUALS));
正常模式下,会严格对比对象的class以及值是否一致。而在宽松模式下,将会放宽一些比对处理:
@Test
public void testLooseMode() {
List<String> names = new ArrayList<>(100);
names.add("tom");
names.add("Jerry");
names.add(null);
List<String> linkedNames = new LinkedList<>();
linkedNames.add("tom");
linkedNames.add("Jerry");
linkedNames.add("");
// default not strict mode
CompareResult result = CompareHelper.compare(names, linkedNames, Option.mix(Option.LOOSE_MODE));
System.out.println(result);
System.out.println("===================================\n");
result = CompareHelper.compare(names, linkedNames);
System.out.println(result);
}
================ ProcessId : e31d9b79-d4d5-4e97-b520-e21e662671c7 ===============
● Result : true
● Options : LOOSE_MODE
● Differences : 0
● Recycle : 0
● Escaped : 15
● MaxDepth : 1
================ ProcessId : e31d9b79-d4d5-4e97-b520-e21e662671c7 ===============
================ ProcessId : 5856c594-97a9-4fc8-9b55-63373af0abdf ===============
● Result : false
● Options :
● Differences : 1
● Recycle : 0
● Escaped : 5
● MaxDepth : 0
difference details :
1:typeDifference ,比较路径 ROOT, 期望类型为 [java.util.ArrayList, [tom, Jerry, null]], 实际类型为 [java.util.LinkedList, [tom, Jerry, ]]
================ ProcessId : 5856c594-97a9-4fc8-9b55-63373af0abdf ===============
默认情况下,如果比较的POJO对象定义了equals
方法,默认会调用该方法进行比较。 但一些情况下,例如equals
方法由lombok生成,即使2个对象各个属性一致,但也可能会不相等。
此时可以使用以下示例禁用equals
比较。
只要idCard属性相等就认为2个对象相等。
private class EqualsUser {
private String name;
private String sex;
private String idCard;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EqualsUser that = (EqualsUser) o;
// idCard相等就相等
return Objects.equals(idCard, that.idCard);
}
}
@Test
public void testCustomizedEquals() {
EqualsUser expect = new EqualsUser();
expect.name = "Tom";
expect.sex = "man";
expect.idCard = "123456";
EqualsUser actual = new EqualsUser();
actual.name = "Jerry";
actual.sex = "woman";
actual.idCard = "123456";
// Use equals
CompareResult result = CompareHelper.compare(expect, actual);
System.out.println(result);
Assert.assertTrue(result.isSame());
// Disable equals method.
result = CompareHelper.compare(expect, actual, Option.mix(Option.DISABLE_EQUALS));
System.out.println(result);
Assert.assertFalse(result.isSame());
Assert.assertEquals(result.getDifferences().size(), 2);
}
================ ProcessId : 0a1a6fb4-135e-4c8b-a612-5d5060242240 ===============
● Result : true
● Options :
● Differences : 0
● Recycle : 0
● Escaped : 14
● MaxDepth : 0
================ ProcessId : 0a1a6fb4-135e-4c8b-a612-5d5060242240 ===============
================ ProcessId : b0e2506b-63e0-476d-a607-8a421938c456 ===============
● Result : false
● Options : DISABLE_EQUALS
● Differences : 2
● Recycle : 0
● Escaped : 140
● MaxDepth : 1
difference details :
1:CommonDifference@ROOT.name, 期望值为 [Tom], 实际值为 [Jerry]
2:CommonDifference@ROOT.sex, 期望值为 [man], 实际值为 [woman]
================ ProcessId : b0e2506b-63e0-476d-a607-8a421938c456 ===============
默认情况下,会逐一比对两个对象之间的所有差异,例如一个POJO对象/Map有10项属性,每项属性都会进行对比,如果不关心对象之间的所有差异细节, 只想知道是否有差异,那么推荐您使用IMMEDIATELY_INTERRUPT
@Test
public void testCompareMapWithImmediatelyInterrupt() {
Map<String, Integer> expect = new HashMap<>();
expect.put("a", 1);
expect.put("b", 2);
expect.put("c", 3);
expect.put("d", 3);
Map<String, Integer> actual = new HashMap<>();
actual.put("a", 1);
actual.put("b", 2);
actual.put("c", 4);
actual.put("d", 5);
CompareResult result = CompareHelper.compare(expect, actual);
System.out.println(result);
Assert.assertFalse(result.isSame());
Assert.assertEquals(result.getDifferences().size(), 2);
result = CompareHelper.compare(expect, actual, Option.mix(Option.IMMEDIATELY_INTERRUPT));
System.out.println(result);
Assert.assertFalse(result.isSame());
Assert.assertEquals(result.getDifferences().size(), 1);
}
================ ProcessId : b8b68db9-ad42-4868-91fa-3e2a2d80b33a ===============
● Result : false
● Options :
● Differences : 2
● Recycle : 0
● Escaped : 16
● MaxDepth : 1
difference details :
1:CommonDifference@ROOT.(key)c, 期望值为 [3], 实际值为 [4]
2:CommonDifference@ROOT.(key)d, 期望值为 [3], 实际值为 [5]
================ ProcessId : b8b68db9-ad42-4868-91fa-3e2a2d80b33a ===============
================ ProcessId : 35329eb0-2acd-4fc4-8938-8f719f6b6fec ===============
● Result : false
● Options : IMMEDIATELY_INTERRUPT
● Differences : 1
● Recycle : 0
● Escaped : 1
● MaxDepth : 1
difference details :
1:CommonDifference@ROOT.(key)c, 期望值为 [3], 实际值为 [4]
================ ProcessId : 35329eb0-2acd-4fc4-8938-8f719f6b6fec ===============
默认情况下,数组的比较输出如下: 按照index逐一进行比对并输出差异项
================ ProcessId : 76deec02-8e8b-4b3b-9311-31a02b723f34 ===============
● Result : false
● Options :
● Differences : 5
● Recycle : 0
● Escaped : 48
● MaxDepth : 0
difference details :
1:CommonDifference@ROOT.(index)0, 期望值为 [0], 实际值为 [9]
2:CommonDifference@ROOT.(index)1, 期望值为 [1], 实际值为 [8]
3:CommonDifference@ROOT.(index)2, 期望值为 [2], 实际值为 [7]
4:CommonDifference@ROOT.(index)3, 期望值为 [3], 实际值为 [6]
5:CommonDifference@ROOT.(index)4, 期望值为 [4], 实际值为 [5]
================ ProcessId : 76deec02-8e8b-4b3b-9311-31a02b723f34 ===============
如果想要简化下结果输出,可以在比较时,指定Option: BEAUTIFUL_ARRAY_RESULT
================ ProcessId : a3c6467c-e6eb-40fc-b8af-1e6450a84f7c ===============
● Result : false
● Options : BEAUTIFUL_ARRAY_RESULT
● Differences : 1
● Recycle : 0
● Escaped : 24
● MaxDepth : 0
difference details :
1:CommonDifference@ROOT, 期望值为 [[0, 1, 2, 3, 4]], 实际值为 [[9, 8, 7, 6, 5]]
================ ProcessId : a3c6467c-e6eb-40fc-b8af-1e6450a84f7c ===============
如果指定了Option: BEAUTIFUL_ARRAY_RESULT, 数组的比较也相当于指定了Option: IMMEDIATELY_INTERRUPT
List/Array比较策略为先判断size,然后再按照index逐一进行对比,如果期望转换为Set进行比较:即两个数组中包含的元素一样,但对应的位置不同,也认为相同
@Test
public void testCompareInt4() {
int[] expect = new int[12];
int[] actual = new int[10];
for (int i = 0; i < 10; i++) {
expect[i] = i;
actual[i] = 10 - i - 1;
}
CompareResult result = CompareHelper.compare(expect, actual, Option.mix(Option.TRANS_AS_SET));
System.out.println(result);
}
================ ProcessId : ce4206a7-6f1e-450d-97df-440d79ea457f ===============
● Result : true
● Options : TRANS_AS_SET
● Differences : 0
● Recycle : 0
● Escaped : 45
● MaxDepth : 0
================ ProcessId : ce4206a7-6f1e-450d-97df-440d79ea457f ===============
为了解决Map中,相同key对应的值属性不同的类型表现形式的时比较,例如如下case
{
"age": 1
}
不同形式比较
{
"age": "1"
}
正常情况下,比较将会返回不一致。但其实通过人工比较的方式可以知道它们其实是一致的,所以可以这样进行比较
Map<String, Object> v1 = new HashMap<>();
Map<String, Object> v2 = new HashMap<>();
ComparatorRegister.register(NameType.of(Object.class, "age"), new AbstractComparator<Object> {
@Override
public Difference compare(Object v1, Object v2, ComparatorContext<Object> ctx) {
String v1s = String.valueOf(v1);
String v2s = String.valueOf(v2);
if (Objects.equals(v1s, v2s)) {
return null;
}
return new BaseDifference(ctx.getPath(), v1, v2);
}
});
CompareResult result = CompareHelper.compare(v1, v2, Option.mix(Option.USE_EXPECT_TYPE_IN_MAP));
欢迎提Issue
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。