是否有一个 Java 库可以“比较”两个对象?

发布于 2024-12-13 17:11:24 字数 630 浏览 5 评论 0 原文

是否有一个类似于 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”相同。我认为 DiffDataStructure 成为一棵树是最自然的,但如果代码可靠的话我不会挑剔。

Is there a Java utility library that is analogous to the Unix program diff, but for Objects? I'm looking for something that can compare two objects of the same type and generate a data structure that represents the differences between them (and can recursively compare differences in instance variables). I'm not looking for a Java implementation of a text diff. I'm also not looking for help with how to use reflection to do this.

The application I'm maintaining has a fragile implementation of this functionality that had some poor design choices and that needs to be rewritten, but it would be even better if we could use something off the shelf.

Here's an example of the kind of thing I'm looking for:

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

After comparison, the utility would tell me that "prop1" is different between the two objects and "prop2" is the same. I think it's most natural for DiffDataStructure to be a tree, but I'm not going to be picky if the code is reliable.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

阪姬 2024-12-20 17:11:24

可能有点晚了,但我和你的情况一样,最终为你的用例创建了我自己的库。由于我被迫自己想出一个解决方案,所以我决定将其发布在 Github 上,以免其他人辛苦工作。您可以在这里找到它:https://github.com/SQiShER/java-object-diff

--- 编辑 ---

这是一个基于OP代码的使用示例:

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;

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;
深爱成瘾 2024-12-20 17:11:24

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 序列化器和反序列化器,以便您可以轻松地将它们保存在数据库中。通过这个库,您可以轻松实现审计模块。

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.

耶耶耶 2024-12-20 17:11:24

您还可以看看 Apache 的解决方案。
大多数项目已经将它放在类路径中,因为它是 commons-lang 的一部分。

检查特定字段的差异:
http:// /commons.apache.org/proper/commons-lang/javadocs/api-3.9/org/apache/commons/lang3/builder/DiffBuilder.html

使用检查差异反思:
http:// /commons.apache.org/proper/commons-lang/javadocs/api-3.9/org/apache/commons/lang3/builder/ReflectionDiffBuilder.html

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

明媚如初 2024-12-20 17:11:24

所有 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 支持的固有强制转换
我不保证所有功能都能工作,因为我已经评论了一些不必要的代码,它适用于所有自定义对象比较。

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.

滥情哥ㄟ 2024-12-20 17:11:24

深入比较对象所有属性的一个好方法是将它们转换为 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);

A good way to compare deeply all properties of an objects is to convert them to java.util.Map. Done that, the java.util.Map#equals will deeply compare the objects and the job is done !

The only problem is to convert the object to a Map. One way to do it is to use reflexion with org.codehaus.jackson.map.ObjectMapper.

Therefore, it exist a tool from com.google.common.collect.MapDifference which describe the defferences between the two maps.

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);
过期情话 2024-12-20 17:11:24

也许这会有所帮助,具体取决于您使用此代码的位置,它可能有用或有问题。测试了这段代码。

    /**
 * @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();
        }
}

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();
        }
}
楠木可依 2024-12-20 17:11:24

首先,我们必须将对象转换为地图:

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

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);
听你说爱我 2024-12-20 17:11:24

快速告诉您两个对象是否不同的更简单方法是使用 apache commons 库

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

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));
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文