有没有一种Java库可以“diff”两个对象?

119
有没有类Unix程序diff的Java通用库可以比较对象之间的差异,生成表示它们之间差异的数据结构(并可以递归地比较实例变量的差异)?我不需要文本diff的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”相同。我认为DiffDataStructure最自然的形式应该是一棵树,但如果代码可靠,我不会太挑剔。


9
请查看这个链接:http://code.google.com/p/jettison/。 - denolk
1
可能是如何测试复杂对象图的相等性?的重复问题。 - skaffman
18
这并不是“如何测试复杂对象图的相等性”的重复。我正在寻找一个库,而不是如何做到这一点的建议。此外,我对差异感兴趣,而不是它们是否相等。 - Kaypro II
1
请查看 https://github.com/jdereg/java-util,其中包含 GraphComparator 工具。该类将生成两个对象图之间的 delta 列表。此外,它还可以将 delta 应用于图形以进行合并。实际上,它是 List<Delta> = GraphComparator(rootA, rootB)。同时也有 GraphComparator.applyDelta(rootB, List<Delta>)。 - John DeRegnaucourt
我知道你不想使用反射,但那绝对是实现这样东西最简单/简洁的方法。 - RobOhRob
@denolk,Jettison 有新的代码库吗? - koppor
8个回答

55
可能有点晚了,但我曾经和你处于相同的情况,并最终为你的用例创建了自己的库。由于我被迫自己想出解决方案,所以我决定将其发布在Github上,以节省其他人的辛苦工作。您可以在此处找到它:https://github.com/SQiShER/java-object-diff ---编辑---
这里是一个基于原始代码的小使用示例:
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;

1
我可以指向这篇文章展示了java-object-diff的一个使用案例吗?https://dev59.com/BGct5IYBdhLWcg3wa83p 非常感谢这个有用的库! - Matthias Wuttke
2
不错。最后我也写了自己的实现。不确定是否会有机会深入了解你的实现,但我很想知道你是如何处理列表的。我最终将所有集合差异处理为 Map 的差异(通过根据是否是列表、集合或映射定义不同的键,并使用 keySet 上的集合操作)。然而,该方法对列表索引更改过于敏感。我没有解决这个问题,因为我的应用程序不需要,但我很想知道你是如何处理它的(更像是文本差异,我猜测)。 - Kaypro II
2
你提出了一个很好的观点。集合确实很难处理,特别是如果你像我一样支持合并。我通过使用对象标识来检测项目是否已添加、更改或删除(通过hashCode、equals和contains)来解决了这个问题。好处是,我可以将所有集合视为相同的。坏处是,我无法正确处理(例如)包含多个相同对象的ArrayLists。我很想为此添加支持,但似乎相当复杂,幸运的是还没有人要求它。 :-) - SQiShER
Daniel,我基本上最后添加了一个覆盖设置来将列表视为集合(并执行与你所做的相同操作),因为在我的应用程序中,我们实际上没有真正的列表,只是有人决定将集合放入列表中。这个链接看起来很有趣,适合一个正确的列表实现:http://en.wikipedia.org/w/index.php?title=Diff&oldid=563831324#Algorithm。 - Kaypro II
@Michael 我想找,但是我在另一个问题中找不到Github链接。 :-) - SQiShER
显示剩余6条评论

50

http://javers.org 是一个能够满足你需求的库:它拥有一些方法,例如compare(Object leftGraph, Object rightGraph),可以返回Diff对象。Diff对象包含了一系列变化(ReferenceChange、ValueChange、PropertyChange),例如:

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

它可以处理图形中的循环。

另外,您可以获取任何图形对象的快照。Javers具有JSON序列化器和反序列化器以进行快照和更改,因此您可以轻松将它们保存在数据库中。使用此库,您可以轻松实现审计模块。


1
这个不起作用。你如何创建一个Javers实例? - Ryan Vettese
2
Javers有出色的文档,解释了一切:javers.org - Paweł Szymczyk
2
我尝试使用它,但它只适用于Java >=7。伙计们,仍有人在使用Java 6开发项目!!! - jesantana
2
如果我比较的两个对象内部有一个对象列表,那么我也能看到这些差异吗?Javers可以比较到哪个层次结构? - Deepak
1
是的,您将看到内部对象的差异。目前没有层次结构级别的阈值。Javers 将尽可能深入,限制是堆栈大小。 - Paweł Szymczyk
显示剩余7条评论

20

当我使用这些时出现错误... java.lang.NullPointerException: Cannot invoke "org.apache.commons.lang3.JavaVersion.atLeast(org.apache.commons.lang3.JavaVersion)" because "org.apache.commons.lang3.SystemUtils.JAVA_SPECIFICATION_VERSION_AS_ENUM" is null - Jose Martinez

3
所有Javers库只支持Java 7,但我需要在Java 6项目中使用它,所以我改变了源代码使其适用于Java 6。以下是Github代码: https://github.com/sand3sh/javers-forJava6
Jar下载链接: https://github.com/sand3sh/javers-forJava6/blob/master/build/javers-forjava6.jar 我只改变了Java 7支持的'<>'强制转换为Java 6支持。我不能保证所有功能都可以正常工作,因为我已经注释掉了一些不必要的代码,但对于所有自定义对象比较,它对我来说可以正常工作。

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

2
也许这可以帮助你,根据你使用这段代码的地方,它可能会有用或者存在问题。已经测试过了这段代码。
    /**
 * @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();
        }
}

2
谢谢,但我们已经有代码使用反射遍历对象图并列出更改。这个问题不是关于如何做到这一点,而是试图避免重复造轮子。 - Kaypro II
如果已经发生了重新发明,那么重新发明轮子并不是坏事。(例如:重新发明的轮子已经付款。) - Thomas Eding
1
我同意你的观点,但在这种情况下,重新设计的代码是一团难以维护的混乱。 - Kaypro II
3
我会检查“字段”而不是方法。方法可以是任何东西,比如“getRandomNumber()”。 - Bohemian

2

比较对象所有属性的好方法是将它们转换为java.util.Map。这样做,java.util.Map#equals将深度比较对象,工作就完成了!

唯一的问题是将对象转换为Map。一种方法是使用org.codehaus.jackson.map.ObjectMapper进行反射。

因此,存在一个来自com.google.common.collect.MapDifference的工具,描述两个映射之间的差异。

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

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

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

// Convert object to Map
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> aMap =  objectMapper.convertValue(a, Map.class);
Map<String, Object> bMap =  objectMapper.convertValue(b, Map.class);

aMap.equals(bMap); // --> false

// Show deeply all differences
MapDifference<String, Object> diff = Maps.difference(aMap, bMap);

-1

首先,我们必须将对象转换为映射表:

    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);
}

然后将地图转换为扁平地图:

    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);
}

最后调用getDifferenceBetween2Maps来获取差异:
    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;
}

使用示例:

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

-3
一个更简单的方法来快速判断两个对象是否不同,是使用Apache Commons库。
    BeanComparator lastNameComparator = new BeanComparator("lname");
    logger.info(" Match "+bc.compare(firstInstance, secondInstance));

1
这对我不起作用,因为我需要知道发生了什么变化,而不仅仅是有一个地方发生了变化。 - Kaypro II

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接