Existe uma biblioteca Java que pode “diferenciar” dois objetos?

86

Existe uma biblioteca de utilitários Java que é análoga ao programa diff do Unix, mas para objetos? Estou procurando por algo que pode comparar dois objetos do mesmo tipo e gerar uma estrutura de dados que representa as diferenças entre eles (e pode comparar recursivamente diferenças em variáveis ​​de instância). Estou não à procura de uma implementação Java de um diff texto. Também não estou procurando ajuda sobre como usar a reflexão para fazer isso.

O aplicativo que estou mantendo tem uma implementação frágil dessa funcionalidade que teve algumas escolhas de design ruins e que precisa ser reescrita, mas seria ainda melhor se pudéssemos usar algo disponível.

Aqui está um exemplo do tipo de coisa que estou procurando:

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

Após a comparação, o utilitário me diria que "prop1" é diferente entre os dois objetos e "prop2" é o mesmo. Eu acho que é mais natural para DiffDataStructure ser uma árvore, mas não vou ser exigente se o código for confiável.

Kaypro II
fonte
8
verifique isso, code.google.com/p/jettison
denolk
13
Esta não é uma duplicata de "Como testar a igualdade de gráficos de objetos complexos?" Estou procurando uma biblioteca, não um conselho sobre como fazê-lo. Além disso, estou interessado no delta, não se eles são iguais ou não.
Kaypro II
1
Veja github.com/jdereg/java-util que possui utilitários GraphComparator. Esta classe irá gerar uma lista de delta entre os gráficos de objetos. Além disso, ele pode mesclar (aplicar) um delta a um gráfico. Efetivamente, é List <Delta> = GraphComparator (rootA, rootB). Bem como GraphComparator.applyDelta (rootB, List <Delta>).
John DeRegnaucourt
Eu sei que você não quer usar reflexão, mas essa é definitivamente a maneira mais fácil / simples de implementar algo assim
RobOhRob

Respostas:

42

Pode ser um pouco tarde, mas eu estava na mesma situação que você e acabei criando minha própria biblioteca exatamente para o seu caso de uso. Como fui forçado a encontrar uma solução sozinho, decidi lançá-lo no Github, para poupar os outros do trabalho duro. Você pode encontrá-lo aqui: https://github.com/SQiShER/java-object-diff

--- Editar ---

Aqui está um pequeno exemplo de uso com base no código OPs:

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;
SQiShER
fonte
1
Posso apontar esta postagem que mostra um caso de uso de java-object-diff? stackoverflow.com/questions/12104969/… Muito obrigado por esta biblioteca útil!
Matthias Wuttke
Eu dei uma olhada em sua postagem e escrevi uma resposta que acabou sendo muito longa para ser postada como um comentário, então aqui está a "essência" dela: gist.github.com/3555555
SQiShER
2
Arrumado. Acabei escrevendo minha própria implementação também. Não tenho certeza se terei a chance de realmente explorar sua implementação, mas estou curioso para saber como você lida com as listas. Acabei tratando todas as diferenças de coleção como diferenças de Maps (definindo as chaves de maneiras diferentes, dependendo se é uma lista, conjunto ou mapa; em seguida, usando operações de conjunto no conjunto de chaves). No entanto, esse método é excessivamente sensível a alterações de índice de lista. Não resolvi esse problema porque não precisava para meu aplicativo, mas estou curioso para saber como você lidou com isso (mais como uma diferença de texto, eu suponho).
Kaypro II
2
Você trouxe um bom ponto. As coleções são realmente difíceis de lidar. Especialmente se você apoiar a fusão, como eu. Resolvi o problema usando identidades de objeto para detectar se um item foi adicionado, alterado ou removido (via hashCode, igual a e contém). O bom disso é que posso tratar todas as coleções da mesma forma. O ruim é que eu não consigo controlar adequadamente (por exemplo) ArrayLists que contêm o mesmo objeto várias vezes. Eu adoraria adicionar suporte para isso, mas parece ser bem complicado e felizmente ninguém pediu, ainda. :-)
SQiShER
Daniel, obrigado pelo seu comentário! Você está certo, reconstruir o objeto original a partir das diferenças é difícil, mas no meu caso não é muito importante. Originalmente, armazenei versões anteriores do meu objeto no banco de dados - como você recomenda. O problema era que a maioria das partes dos documentos grandes não mudava, havia muitos dados duplicados. É por isso que comecei a pesquisar alternativas e encontrei java-object-diff. Eu também tive dificuldades com coleções / mapas e alterei meus métodos "iguais" para verificar a igualdade do banco de dados primário (e não do objeto inteiro), isso ajudou.
Matthias Wuttke,
39

http://javers.org é uma biblioteca que faz exatamente o que você precisa: tem métodos como compare (Object leftGraph, Object rightGraph) retornando o objeto Diff. Diff contém uma lista de mudanças (ReferenceChange, ValueChange, PropertyChange), por exemplo

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

Ele pode lidar com ciclos em gráficos.

Além disso, você pode obter um instantâneo de qualquer objeto gráfico. Javers tem serializadores e desserializadores JSON para instantâneos e alterações para que você possa salvá-los facilmente no banco de dados. Com esta biblioteca, você pode implementar facilmente um módulo de auditoria.

Paweł Szymczyk
fonte
1
isso não funciona. como você cria uma instância Javers?
Ryan Vettese de
2
Javers tem uma documentação excelente que explica tudo: javers.org
Paweł Szymczyk
2
Tentei usá-lo, mas é apenas para Java> = 7. Pessoal, ainda tem gente trabalhando em projetos em Java 6 !!!
jesantana
2
E se os dois objetos que comparo tiverem uma lista de objetos internamente e eu também vou ver essas diferenças? Qual é o nível de hierarquia que os javers podem comparar?
Deepak de
1
Sim, você verá diferenças em objetos internos. No momento, não há limite para o nível de hierarquia. Javers irá o mais fundo possível, a limitação é o tamanho da pilha.
Paweł Szymczyk
15

Sim, a biblioteca java-util tem uma classe GraphComparator que irá comparar dois Java Object Graphs. Ele retorna a diferença como uma Lista de deltas. O GraphComparator também permite mesclar (aplicar) os deltas. Este código só tem dependências no JDK, nenhuma outra biblioteca.

John DeRegnaucourt
fonte
12
Parece uma boa biblioteca, você deve mencionar que é um colaborador
Christos
3

Toda a biblioteca Javers tem suporte apenas para Java 7, eu estava em uma situação em que quero que isso seja usado para um projeto Java 6, então peguei o código-fonte e mudei de forma que funciona para Java 6 abaixo é o código do github .

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

Link do jar: https://github.com/sand3sh/javers-forJava6/blob/master/build/javers-forjava6.jar

Eu só mudei as conversões de cast inerentes '<>' suportadas por Java 7 para suporte a Java 6. Não garanto que todas as funcionalidades funcionarão, pois comentei alguns códigos desnecessários para mim que funcionam para todas as comparações de objetos personalizados.

Sandesh
fonte
1

Talvez isso ajude, dependendo de onde você usa esse código, pode ser útil ou problemático. Testei este código.

    /**
 * @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();
        }
}
r0ast3d
fonte
2
Obrigado, mas já temos o código que percorre o gráfico do objeto usando listas de reflexão das mudanças. Essas questões não são sobre como fazer, mas sobre como evitar a reinvenção da roda.
Kaypro II
Reinventar a roda não é ruim se a reinvenção já aconteceu. (IE: a roda reinventada já foi paga.)
Thomas Eding
1
Concordo com você, mas neste caso o código reinventado é uma bagunça insustentável.
Kaypro II
3
Eu Fieldsnão verificaria métodos. Os métodos podem ser qualquer coisa getRandomNumber().
Boêmio
-3

Uma abordagem mais simples para dizer rapidamente se dois objetos são diferentes é usar a biblioteca apache commons

    BeanComparator lastNameComparator = new BeanComparator("lname");
    logger.info(" Match "+bc.compare(firstInstance, secondInstance));
r0ast3d
fonte
Isso não funciona para mim porque preciso saber o que mudou, não apenas se houve uma mudança em algum lugar.
Kaypro II