有没有可以“区分”两个对象的 Java 库?

是否有一个类似于 Unix 程序 diff 的 Java 实用程序库,但是用于对象?我正在寻找能够比较两个相同类型的对象并生成一个表示它们之间差异的数据结构(并且能够递归地比较实例变量中的差异)。我的 没有寻找一个文本差异的 Java 实现。我也 没有寻求帮助,如何使用反射来做到这一点。

我正在维护的应用程序有一个脆弱的功能实现,有一些糟糕的设计选择,需要重写,但如果我们可以使用现成的东西会更好。

这里有一个我正在寻找的例子:

SomeClass a = new SomeClass();
SomeClass b = new SomeClass();


a.setProp1("A");
a.setProp2("X");


b.setProp1("B");
b.setProp2("X");


DiffDataStructure diff = OffTheShelfUtility.diff(a, b);  // magical recursive comparison happens here

经过比较,该实用程序会告诉我,“ prop1”在两个对象之间是不同的,而“ prop2”是相同的。我认为对于迪夫数据结构来说,成为一棵树是最自然的,但是如果代码是可靠的,我就不挑剔了。

93852 次浏览

Maybe this will help, depending on where you use this code, it could be useful or problematic. Tested this code.

    /**
* @param firstInstance
* @param secondInstance
*/
protected static void findMatchingValues(SomeClass firstInstance,
SomeClass secondInstance) {
try {
Class firstClass = firstInstance.getClass();
Method[] firstClassMethodsArr = firstClass.getMethods();


Class secondClass = firstInstance.getClass();
Method[] secondClassMethodsArr = secondClass.getMethods();




for (int i = 0; i < firstClassMethodsArr.length; i++) {
Method firstClassMethod = firstClassMethodsArr[i];
// target getter methods.
if(firstClassMethod.getName().startsWith("get")
&& ((firstClassMethod.getParameterTypes()).length == 0)
&& (!(firstClassMethod.getName().equals("getClass")))
){


Object firstValue;
firstValue = firstClassMethod.invoke(firstInstance, null);


logger.info(" Value "+firstValue+" Method "+firstClassMethod.getName());


for (int j = 0; j < secondClassMethodsArr.length; j++) {
Method secondClassMethod = secondClassMethodsArr[j];
if(secondClassMethod.getName().equals(firstClassMethod.getName())){
Object secondValue = secondClassMethod.invoke(secondInstance, null);
if(firstValue.equals(secondValue)){
logger.info(" Values do match! ");
}
}
}
}
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

A simpler approach to quickly tell you if two objects are different would be is to use the apache commons library

    BeanComparator lastNameComparator = new BeanComparator("lname");
logger.info(" Match "+bc.compare(firstInstance, secondInstance));

Might be a little late, but I was in the same situation like you and ended up creating my own library for exactly your use-case. Since I was forced to come up with a solution myself, I decided to release it on Github, to spare others the hard work. You can find it here: https://github.com/SQiShER/java-object-diff

--- Edit ---

Here is a little usage example based on the OPs code:

SomeClass a = new SomeClass();
SomeClass b = new SomeClass();


a.setProp1("A");
a.setProp2("X");


b.setProp1("B");
b.setProp2("X");


DiffNode diff = ObjectDifferBuilder.buildDefault().compare(a, b);


assert diff.hasChanges();
assert diff.childCount() == 1;
assert diff.getChild('prop1').getState() == DiffNode.State.CHANGED;

http://javers.org is library that does exacly what you need: has methods like compare(Object leftGraph, Object rightGraph) returning the Diff object. Diff contains a list of changes (ReferenceChange, ValueChange, PropertyChange) e.g.

given:
DummyUser user =  dummyUser("id").withSex(FEMALE).build();
DummyUser user2 = dummyUser("id").withSex(MALE).build();
Javers javers = JaversTestBuilder.newInstance()


when:
Diff diff = javers.compare(user, user2)


then:
diff.changes.size() == 1
ValueChange change = diff.changes[0]
change.leftValue == FEMALE
change.rightValue == MALE

It can handle cycles in graphs.

In addition you can get Snapshot of any graph object. Javers has JSON serializers and deserializers to snapshot, and changes so you can easily save them in database. With this library you can easily implement a module for auditing.

All the Javers library has support to only Java 7, I was in a situation since I want this to be used for a Java 6 project so I happened to take the source and change in a way it works for Java 6 below is the github code.

https://github.com/sand3sh/javers-forJava6

Jar link: https://github.com/sand3sh/javers-forJava6/blob/master/build/javers-forjava6.jar

I have only changed the Java 7 supported '<>' inherent cast conversions to Java 6 support I dont gurantee all the functionalities will work since I have commented few unnecessary code for me it works for all custom objects comparision.

You could also take a look at the solution from Apache. Most projects already have it on their classpath since its part of commons-lang.

Check difference for specific field(s):
http://commons.apache.org/proper/commons-lang/javadocs/api-3.9/org/apache/commons/lang3/builder/DiffBuilder.html

Check difference by using reflection:
http://commons.apache.org/proper/commons-lang/javadocs/api-3.9/org/apache/commons/lang3/builder/ReflectionDiffBuilder.html

Firstly we have to convert our objects to map:

    public Map<String, Object> objectToMap(Object object) throws JsonProcessingException {
var mapper = new ObjectMapper();
final var type = new TypeReference<HashMap<String, Object>>() {


};
ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
return mapper.readValue(ow.writeValueAsString(object), type);
}

after that convert the map to flatten map:

    public Map<String, Object> flatten(Map<String, Object> map) {
return map.entrySet().stream()
.flatMap(this::flatten)
.collect(LinkedHashMap::new, (m, e) -> m.put(camelToUnderScore("/" + e.getKey()), e.getValue()),
LinkedHashMap::putAll);
}


public Stream<Map.Entry<String, Object>> flatten(Map.Entry<String, Object> entry) {


if (entry == null) {
return Stream.empty();
}


if (entry.getValue() instanceof Map<?, ?>) {
return ((Map<?, ?>) entry.getValue()).entrySet().stream()
.flatMap(e -> flatten(
new AbstractMap.SimpleEntry<>(camelToUnderScore(entry.getKey() + "/" + e.getKey()),
e.getValue())));
}


if (entry.getValue() instanceof List<?>) {
List<?> list = (List<?>) entry.getValue();
return IntStream.range(0, list.size())
.mapToObj(i -> new AbstractMap.SimpleEntry<String, Object>(
camelToUnderScore(entry.getKey() + "/" + i), list.get(i)))
.flatMap(this::flatten);
}


return Stream.of(entry);
}

and finaly call getDifferenceBetween2Maps to get the differences:

    public Map<String, Object> getDifferenceBetween2Maps(final Map<String, Object> leftFlatMap,
final Map<String, Object> rightFlatMap) {
  

final MapDifference<String, Object> difference = Maps.difference(leftFlatMap, rightFlatMap);


var differencesList = new HashMap<String, Object>();
  

differencesList.putAll(difference.entriesOnlyOnLeft());


differencesList.putAll(difference.entriesOnlyOnRight());


return differencesList;
}

using example :

Map<String, Object> oldObjectFlatMap = flatten(objectToMap(oldObject));
Map<String, Object> newObjectFlatMap = flatten(objectToMap(newObject));
var differencesList = getDifferenceBetween2Maps(oldObjectFlatMap , newObjectFlatMap);