The challenge of auditing changes or finding differences between Java objects is often encountered in complex Java applications. As projects evolve and grow in complexity, the ability to identify and understand the differences between objects becomes significant in ensuring data integrity and making informed decisions.

In this blog post, the challenges with Java object comparison and change auditing will be emphasized while exploring how object comparison libraries such as Javers contribute to simplifying the process and improving the clarity of comparison results.  

The Importance of Object Comparison

In today’s data-driven world, Java applications often deal with vast amounts of critical information. As this data undergoes frequent updates, it becomes crucial to track changes accurately. Object comparison involves analyzing two versions or a collection of objects to identify modifications, additions, and deletions. 

These insights allow developers, quality assurance teams, and other stakeholders to understand precisely what has changed, when, and why. They are valuable for troubleshooting issues, maintaining data consistency, and adhering to regulatory requirements. 

Challenges in Object Comparison:

  • Precision Matters: Detecting subtle differences

Accurate object comparison demands precision, especially when dealing with nested objects and deep data structures. Libraries designed for this purpose, like Javers, perform thorough object graph traversal and attribute analysis, enabling developers to pinpoint even the smallest changes ensuring reliable comparisons.

  • Handling Nested Structures: Managing hierarchical structures and tracking changes within nested attributes

Dealing with nested structures can be overwhelming without a specialized object comparison library like Javers. Developers must manually traverse the data structures, compare each element, and identify changes, which becomes increasingly complex as the hierarchy deepens.

Javers simplifies this process by providing deep object graph traversal capabilities. It efficiently navigates through the complex nested structures and identifies changes at all hierarchy levels. For instance, Javers can easily compare nested lists, sets, or maps, and objects contained within other objects.

To demonstrate this in Javers, consider the following example:

public class Address {
    private String street;
    private String city;

    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }
}

public class Person {
    private String name;
    private Address address;

    public Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
}
// Original Person object with an address
Address originalAddress = new Address("Main St", "New York");
Person originalPerson = new Person("John Doe", originalAddress);

// Updated Person object with a modified address
Address updatedAddress = new Address("Elm St", "New York");
Person updatedPerson = new Person("John Doe", updatedAddress);

// Perform the comparison using Javers
Javers javers = JaversBuilder.javers().build();
Diff diff = javers.compare(originalPerson, updatedPerson);

// Print the differences
System.out.println(diff);

The output would be the following:

Diff:
* changes on Person:
  - 'address.street' changed: 'Main St' -> 'Elm St'

In this example, the change is precisely identified in the street attribute within the address object of the Person class. 

  • Addressing Serialization Differences: Ensuring data consistency despite serialization variations

When comparing objects, it is essential to account for serialization variations to avoid false positives or missed changes. When serialized using different techniques, the same object may produce slightly different representations due to formatting, whitespace, or encoding differences. These seemingly insignificant variations can lead to incorrect comparison results if not appropriately handled.

Javers addresses this challenge by providing built-in serialization-aware comparison capabilities. It can recognize the differences between objects, even when serialized using different formats, and accurately identify actual data modifications.

  • Tracking Collections: Accurately identifying additions, removals, and modifications within dynamic collections

The main difficulty when comparing objects containing collections lies in efficiently and precisely detecting changes at both the collection level and within the collection elements. Several factors contribute to this challenge:

  • Handling Nested Collections: Collections may also contain other collections, creating multi-level nested structures. Accurately tracking changes within nested collections adds an additional layer of complexity to the comparison process.
  • Element Identity: Determining the identity of elements within collections is critical. The comparison should be able to distinguish between different instances of the same object to accurately identify additions, updates, and removals.
  • Performance Considerations: The comparison process for applications with large collections must be efficient to avoid performance bottlenecks.

To demonstrate this in Javers, consider the modification of the previous example:

class Address {
    private String street;
    private String city;

    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }
}
class Person {
    private String name;
    private Address primaryAddress;
    private Map<String, Address> addresses = new HashMap<>();
    private List<String> hobbies = new ArrayList<>();

    public Person(String name, Address primaryAddress) {
        this.name = name;
        this.primaryAddress = primaryAddress;
    }

    public void addAddress(String type, Address address) {
        addresses.put(type, address);
    }

    public void addHobbies(List<String> hobbiesToAdd) {
        hobbies.addAll(hobbiesToAdd);
    }
}
Address homeAddress = new Address("123 Main St", "City A");
Address workAddress = new Address("456 Elm St", "City B");
Address vacationAddress = new Address("789 Beach St", "City C");

Person originalPerson = new Person("John Doe", homeAddress);
originalPerson.addAddress("Home", homeAddress);
originalPerson.addAddress("Work", workAddress);
originalPerson.addHobbies(Arrays.asList("Reading", "Cooking"));

Person updatedPerson = new Person("John Doe", homeAddress);
updatedPerson.addAddress("Home", homeAddress);
updatedPerson.addAddress("Vacation", vacationAddress);
updatedPerson.addHobbies(Arrays.asList("Reading", "Painting"));

Javers javers = JaversBuilder.javers().build();
Diff diff = javers.compare(originalPerson, updatedPerson);

System.out.println(diff);

The output of the Javers comparison in this example, which involves the Person class with a Map field for addresses and List field for hobbies, would look like this:

Diff:
* changes on Person :
  - 'addresses' map changes :
    · entry ['Vacation' : Person/#addresses/Vacation'] added
    · entry ['Work' : Person/#addresses/Work'] removed
  - 'addresses/Vacation.city' = 'City C'
  - 'addresses/Vacation.street' = '789 Beach St'
  - 'addresses/Work.city' value 'City B' unset
  - 'addresses/Work.street' value '456 Elm St' unset
  - 'hobbies' collection changes :
    1. 'Cooking' changed to 'Painting'
  • Choosing the appropriate difference output format

Choosing the appropriate difference output format can be challenging due to varying object complexities, different audience needs, readability concerns, and integration requirements. Striking the right balance between detail and readability while considering specific use cases is crucial for making effective decisions.

Human-readable textual formats, like side-by-side or unified diff output, are easily understood by humans and are helpful for quick comparisons or debugging. On the other hand, structured formats like JSON or XML provide a more formalized representation, making them suitable for seamless integration with other systems and automated processing.

Example of the human-readable format output from the previous example:

Diff:
* changes on Person:
  - 'address.street' changed: 'Main St' -> 'Elm St'

Example of the same output in JSON format in Javers:

{
  "changes": [
    {
      "changeType": "ValueChange",
      "globalId": {
        "valueObject": "Address",
        "ownerId": {
          "valueObject": "Person"
        },
        "fragment": "address"
      },
      "property": "street",
      "propertyChangeType": "PROPERTY_VALUE_CHANGED",
      "left": "Main St",
      "right": "Elm St"
    }
  ]
}

Utilizing Diff Results

Beyond simply identifying differences between Java objects, Javers provides a powerful tool for making informed, programmatic decisions based on these differences.

A crucial artifact, known as a Diff object, is generated when two Java objects are compared using Javers. A comprehensive report of all changes detected between the two objects is contained within this object.This Diff report serves as the key to informed decision-making within the application. It comprises a list of all changes detected during the comparison that can be analyzed, enabling dynamic reactions to specific events or conditions in the code.

Whether it is dispatching notifications, triggering a specific action or validation, updating related records, or initiating particular workflows, Javers provides the means to make the Java application dynamic and responsive.

In the next example, handling various types of detected changes via simulating sending notifications when certain changes occur is shown.

Diff diff = javers.compare(originalPerson, updatedPerson);

// Iterate through all changes detected by Javers
for (Change change : diff.getChanges()) {
    if (change.isPropertyChange()) {
        String propertyName = change.getPropertyName();
        Object oldValue = change.getLeft();
        Object newValue = change.getRight();

        // Simulate sending a notification when certain property changes
        String notificationMessage = String.format("Property '%s' 
        changed from '%s' to '%s'", propertyName, oldValue, newValue);
        sendNotification(notificationMessage);
        }
    } else if (change.isObjectAdded()) {
        String notificationMessage = "A new object was added: " +
        change.getAffectedGlobalId();
        sendNotification(notificationMessage);

    } else if (change.isObjectRemoved()) {
        String notificationMessage = "An object was removed: " + 
        change.getAffectedGlobalId();
        sendNotification(notificationMessage);
    }
}

private void sendNotification(String message) {
    System.out.println("Notification sent: " + message);
}

Conclusion

Object comparison and change detection are essential for data integrity and informed choices in complex Java applications.  This blog post detailed challenges like precision, nested structures, and serialization. Javers simplifies with effective nested traversal, accurate serialization handling, and dynamic collection tracking, as it enables clarity, precision, and efficiency in identifying and understanding differences. Choosing the output format is nuanced and requires balance, favoring human-readable debugging and structured JSON integration.

Javers doesn’t stop at merely highlighting differences; it provides users with the means to utilize the results of comparisons programmatically, and it empowers informed decision-making within Java applications.


“Challenges With Auditing Changes in Java objects” Tech Bite was brought to you by Dinija Seferović, Junior Software Engineer at Atlantbh.

Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.

Leave a Reply