# obj-compare
**Repository Path**: smartboot/obj-compare
## Basic Information
- **Project Name**: obj-compare
- **Description**: 一款基于Java反射的对象比较工具,适用于单测参数匹配、数据结果对比,快照落存等场景
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 37
- **Forks**: 7
- **Created**: 2023-01-17
- **Last Updated**: 2025-05-29
## Categories & Tags
**Categories**: Uncategorized
**Tags**: 反射, 对象比较
## README
# 工具介绍
obj-compare是一款基于Java反射的轻量级对象比较工具。不仅支持常见的POJO对象、集合对象、数组类型比较功能,也支持高度自定义比较定制等功能。
## 适用场景
* 单测结果对比以及单测mock框架中参数匹配
* 业务领域中快照对比记录
* 测试领域数据对比
* 文本/json内容对比
* 一些需要自定义比较的场景
## 功能列表
- [X] 普通POJO、Boolean、Date、数组类型比较
- [X] 容器类List、Set、Map及其子类比较
- [X] 支持自定义类型比较
- [X] 支持自定义属性过滤
- [X] 循环依赖检查、宽松模式等特性
# 5分钟快速开始
## 引入Maven依赖
```xml
io.github.smartboot.compare
obj-compare
1.1.3
```
## 示例代码
```java
@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());
}
```
## 结果输出
```text
================ 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 ===============
```
# Collections/Map/Array Builtin Rules
## List/Array Rules
先比较size,size相等再按照index逐一进行对比
## Map/Set rules
* Set 先比较size,size相等遍历期望Set,进行key的有无比较
* Map 先比较size,size相等遍历期望Map,同时比较key对应的value是否相等
# Features
## 自定义属性过滤
比较过程中,如果需要跳过部分属性的比较,例如时间、随机字符串等字段比较。有以下2种方式供您选择:
* 使用自定义字段过滤器
* 使用自定义字段忽略器
字段忽略器最终会被转化为字段过滤器执行,所以您不需要担心2者有太大差异。
### 自定义字段过滤器
* 注册全局自定义过滤器
```java
// 过滤对象Field上未标注@Snapshoted的字段
FieldFilters.registerGlobal((f, context) -> f.getType() == Kind.FIELD && f.getField().getAnnotation(Snapshoted.class) == null);
```
* 当次比较自定义过滤器
```java
CompareHelper.compare(expect, actual, 0, null, fieldFilter1,fieldFilter2);
```
### 自定义字段忽略器
* 单个字段过滤器
```java
List ignoreFields = new ArrayList<>();
// 忽略类型为String.class 字段名为id的 属性比较
ignoreFields.add(new IgnoreField("id", String.class));
CompareHelper.compare(expect, actual, ignoreFields);
```
* Pattern字段过滤器
```java
List ignoreFields = new ArrayList<>();
// 忽略任何以word结尾的属性,类型为任意类型
IgnorePatternField ignorePatternField = new IgnorePatternField("[\\s\\d]*word");
ignoreFields.add(ignorePatternField);
CompareHelper.compare(expect, actual, ignoreFields);
```
### 结果输出
```text
================ 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种方式可以实现。
* 注册全局类型比较器, 如果类型重复,会覆盖默认已有的比较器
```java
ComparatorRegister.register(String.class, new CustomizedStringComparator());
```
* 注册Name-Type类型比较器(推荐)
```java
// 匹配名为attribute,值类型为String的字段项进行比较
ComparatorRegister.register(NameType.of("attribute", String.class), AttributesComparator.getInstance());
// 匹配名为c或者d,值类型为Integer的字段项进行比较
ComparatorRegister.register(NamesType.of(Integer.class, "c", "d"), new AbstractComparator() {
@Override
public Difference compare(Integer expect, Integer actual, ComparatorContext context) {
return Difference.SAME;
}
});
```
**自定义类型/属性比较**不仅适用于POJO对象,也适用于Map中某个key的比较。
## 多线程自定义比较器隔离
如果多线程下比较的情况,针对以下case
* 线程A,针对User的某个String属性需要进行自定义比较1
* 线程B,针对User的某个String属性需要进行自定义比较2
* 线程C,针对String类型需要做特殊比较处理3
使用常规的`ComparatorRegister`无法满足要求,它们之前注册的会相互进行影响,根据此情况,提供了`ThreadLocalComparatorRegister`来支持。
* 使用`ComparatorRegister`注册的比较器仍然会全局生效,覆盖了自带的比较器在比较完后也不会自动重置,需要手动处理
* `ThreadLocalComparatorRegister`与`ComparatorRegister`同理,但作用范围在当前线程内
* 使用`ThreadLocalComparatorRegister`注册的比较器不会在子线程中派生出的线程生效(因为非`InheritableThreadLocal`实现)
* 使用`ThreadLocalComparatorRegister`如果需要移除,通过方法`removeAll`移除所有或者使用`removeAll`单个移除注册的比较器
## 循环依赖检测
对于两个对象之间的循环依赖字段(非基本类型及其包装类型),对比过程中将会一一记录
### 示例代码
对象的User字段相互引用
```java
@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);
}
```
### 结果输出
```text
================ 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序列化包并完成序列器初始化
* fastjson2、fastjson、gson 引入依赖即可,默认会自动初始化
* 其他json框架,需要手动初始化
### 使用示例
```java
// 初始化gson的json序列化
JsonSerializer.setDefaultInstance(new GsonSerializer());
JsonSerializer.setDefaultInstance(new GsonSerializer());
Map expect = new HashMap<>();
expect.put("a", 1);
expect.put("b", "123456888");
expect.put("c", "hhaks");
Map 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));
```
### 输出示例
```json
{
"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
}
```
## Configuration接口
1.1.1版本新增Configuration接口,用户可实现接口后,将自定义配置信息放入里面
```java
import org.smartboot.compare.Configuration;
class CustomConfiguration implements Configuration {
// 自定义配置
}
```
## FeatureFunction
1.1.1版本为了增加比较过程中的自定义处理,新增FeatureFunction功能。目前1.1.1版本中支持以下几个特性
* 判断在某个路径下的option是否生效(默认生效)
```java
default Boolean isEffectOption(ComparatorContext> ctx, Option option) {
return true;
}
```
* string比较中将string转换为其他对象进行比较,例如json
```java
default Object convertString2Object(ComparatorContext> ctx, String value) {
return value;
}
```
* 数组比较前的排序
```java
default void sort(ComparatorContext> ctx, Object expectArray, Object actualArray, int type) {
}
```
具体使用可以参照以下示例
```java
ComparatorContext