Kotej

How to compare two JAR files and see what changed between builds

Published · Alexandre Vazquez

A hotfix should change two classes. Did the new artifact change only those two? Or: CI built the same commit twice and the checksums differ, so something in the build is not deterministic. Either way you need to look inside the two archives and compare them entry by entry.

The quick way: list and compare

A JAR (like a WAR, an EAR or an Android AAR) is a ZIP file. A first look from the command line:

# which entries exist in one and not the other
diff <(unzip -Z1 old.jar | sort) <(unzip -Z1 new.jar | sort)

# entries with their CRC32, to spot changed content
diff <(unzip -v old.jar | awk '{print $7, $8}' | sort -k2) \
     <(unzip -v new.jar | awk '{print $7, $8}' | sort -k2)

That tells you which entries changed, not how. And it stops at the first level: a WAR with forty JARs inside it, or an EAR with WARs inside, needs unpacking at every level before the comparison means anything.

Comparing archives in Kotej

Kotej opens both archives in the browser and shows them as two trees, with every entry marked as the same, different, or present on one side only. Nested archives open all the way down, so a changed library three levels deep inside an EAR still shows up.

It is fast on large archives because it reads the CRC32 checksums from each ZIP's central directory: an entry with the same size and checksum on both sides is the same, without decompressing it. Only the entries that differ are read.

For the files where what changed matters, it goes further than the bytes: web.xml, persistence.xml and the pom.xml under META-INF/maven are compared as XML, JSON configuration by structure, and properties and text files line by line. Classes are compared by their bytes; Kotej tells you which ones changed but doesn't decompile them.

Why two builds of the same code differ

When nothing in the source changed and the archives still differ, the usual suspects are:

Kotej compares entries by content, so timestamp-only changes don't count as differences, and the generated files show up for what they are: one line with a date.

Making the build reproducible

To get byte-identical archives from the same commit, fix the timestamps and the order:

<!-- Maven: pom.xml -->
<properties>
  <project.build.outputTimestamp>2026-01-01T00:00:00Z</project.build.outputTimestamp>
</properties>
// Gradle: build.gradle.kts
tasks.withType<AbstractArchiveTask>().configureEach {
    isPreserveFileTimestamps = false
    isReproducibleFileOrder = true
}

Then build twice and compare the two artifacts again. Whatever still differs is either a plugin that writes the date into a file or a real non-determinism worth understanding.

Archives and folders

An archive on one side and an unpacked folder on the other compare the same way, which is handy to check a deployed, exploded WAR on a server against the artifact you built.

Compare two JAR, WAR or EAR files. Free, no account, and nothing is uploaded: everything is compared in your browser.

More guides