Setiap dependency di pom.xml atau build.gradle.kts diakhiri satu angka kecil yang menentukan banyak hal: 2.17.1. Angka tersebut bukan sekadar label — pada ekosistem Java yang mengikuti Semantic Versioning, susunan angkanya adalah kontrak kompatibilitas antara pembuat library dan pemakainya. Salah membaca kontrak itu berarti build yang sehat hari ini mendadak gagal kompilasi besok pagi tanpa satu baris kode kita berubah. Pada chapter ini kita akan membedah anatomi versi semantik, membandingkan versi dengan cara yang benar, lalu menimbang strategi rentang versi versus kunci versi eksplisit — termasuk posisi versi -SNAPSHOT dalam alur kerja tim.

Anatomi Semantic Versioning #

Semantic Versioning (SemVer) menuliskan versi sebagai MAJOR.MINOR.PATCH. Ketiga komponen dibaca dari kiri ke kanan dan masing-masing punya makna tetap:

KomponenNaik ketikaDampak bagi pemakai library
MAJORAPI berubah tidak kompatibelkode lama berpotensi gagal kompilasi
MINORfitur baru yang kompatibel munduraman; fitur baru tersedia
PATCHperbaikan bug kompatibelaman; perilaku keliru diperbaiki

Contohnya pada Jackson: naik dari 2.16.1 ke 2.16.2 seharusnya hanya perbaikan bug; naik ke 2.17.0 menambah fitur baru tanpa mematahkan kode lama; sedangkan 3.0.0 menandakan perubahan besar yang menuntut penyesuaian kode pemanggil. Istilah breaking change mengacu pada kenaikan MAJOR — momen ketika method dipindah, dihapus, atau perilakunya diubah.

Membandingkan Versi dengan Benar #

Jebakan klasik membandingkan versi adalah memperlakukannya sebagai teks biasa. Program berikut membandingkan versi secara numerik per komponen, lalu memperlihatkan betapa salahnya perbandingan string mentah:

java SemVer.java
public class SemVer {

    public static void main(String[] args) {
        System.out.println("== perbandingan numerik per komponen ==");
        System.out.println(verdict("2.9.1", "2.10.0"));
        System.out.println(verdict("1.4.2", "1.4.3"));
        System.out.println(verdict("3.0.0", "3.0.0"));
        System.out.println();
        System.out.println("perbandingan string mentah: \"2.10.0\" vs \"2.9.1\" -> "
                + ("2.10.0".compareTo("2.9.1") < 0
                        ? "\"2.10.0\" dianggap lebih tua (kesimpulan salah)"
                        : "\"2.10.0\" lebih baru"));
        System.out.println();

        System.out.println("== klasifikasi kenaikan dari 1.4.2 ==");
        klasifikasi("1.4.2", "1.4.3");
        klasifikasi("1.4.2", "1.5.0");
        klasifikasi("1.4.2", "2.0.0");
    }

    private static String verdict(String a, String b) {
        int hasil = bandingkan(urai(a), urai(b));
        if (hasil < 0) {
            return a + " vs " + b + " -> " + b + " lebih baru";
        }
        if (hasil > 0) {
            return a + " vs " + b + " -> " + a + " lebih baru";
        }
        return a + " vs " + b + " -> keduanya sama";
    }

    /** "2.10.0" menjadi [2, 10, 0]. */
    private static int[] urai(String versi) {
        String[] bagian = versi.split("\\.");
        int[] angka = new int[bagian.length];
        for (int i = 0; i < bagian.length; i++) {
            angka[i] = Integer.parseInt(bagian[i]);
        }
        return angka;
    }

    /** Bandingkan komponen dari kiri: major, lalu minor, lalu patch. */
    private static int bandingkan(int[] a, int[] b) {
        for (int i = 0; i < Math.min(a.length, b.length); i++) {
            if (a[i] != b[i]) {
                return Integer.compare(a[i], b[i]);
            }
        }
        return Integer.compare(a.length, b.length);
    }

    private static void klasifikasi(String dari, String ke) {
        int[] a = urai(dari);
        int[] b = urai(ke);
        if (b[0] > a[0]) {
            System.out.printf("ke %-6s -> MAJOR : perubahan yang merusak kompatibilitas%n", ke);
        } else if (b[1] > a[1]) {
            System.out.printf("ke %-6s -> MINOR : fitur baru, kompatibel mundur%n", ke);
        } else {
            System.out.printf("ke %-6s -> PATCH : perbaikan bug, kompatibel%n", ke);
        }
    }
}

Output yang Dihasilkan:

text
== perbandingan numerik per komponen ==
2.9.1 vs 2.10.0 -> 2.10.0 lebih baru
1.4.2 vs 1.4.3 -> 1.4.3 lebih baru
3.0.0 vs 3.0.0 -> keduanya sama

perbandingan string mentah: "2.10.0" vs "2.9.1" -> "2.10.0" dianggap lebih tua (kesimpulan salah)

== klasifikasi kenaikan dari 1.4.2 ==
ke 1.4.3  -> PATCH : perbaikan bug, kompatibel
ke 1.5.0  -> MINOR : fitur baru, kompatibel mundur
ke 2.0.0  -> MAJOR : perubahan yang merusak kompatibilitas

Mari kita bedah hasilnya:

  1. Baris 2.9.1 vs 2.10.0: perbandingan numerik per komponen memutuskan 2.10.0 lebih baru karena komponen kedua 10 > 9 — komponen pertama yang sama langsung dilewati.
  2. Baris perbandingan string mentah: String.compareTo membandingkan karakter demi karakter, dan karakter '1' lebih kecil dari '9', sehingga "2.10.0" dianggap lebih tua — kesimpulan yang keliru. Build tool dan package manager mengurai versi menjadi angka sebelum membandingkan, persis seperti method urai di atas.
  3. Blok klasifikasi: dari 1.4.2, naik ke 1.4.3, 1.5.0, dan 2.0.0 mewakili tiga jenis kenaikan dengan tingkat risiko yang naik — dari aman hingga berpotensi mematahkan kode.

Rentang Versi dan Risikonya #

Kedua build tool memungkinkan deklarasi rentang, bukan satu angka pasti:

xml Rentang versi di pom.xml
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>[2.16,)</version>
</dependency>
kotlin Rentang versi di build.gradle.kts
dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.+")
}

Notasi [2.16,) berarti “versi 2.16 atau lebih baru”, sedangkan 2.+ berarti “versi 2 apa pun”. Kemudahannya nyata: perbaikan dan fitur baru datang otomatis. Namun harga yang dibayar serius — hasil resolusi bergantung pada versi apa saja yang tersedia di repositori saat build berjalan, sehingga dua build pada waktu berbeda dapat menghasilkan classpath berbeda. Kenaikan MAJOR yang tidak kita sadari hadir diam-diam, dan penelusuran “kemarin masih jalan” menjadi sulit karena tidak ada satu titik perubahan pun yang bisa dicurigai.

SNAPSHOT versus Release #

Maven mengenal kelas versi khusus berakhiran -SNAPSHOT — misalnya 1.5.0-SNAPSHOT — versi kerja yang masih bergerak. Tim modul internal memakainya untuk saling menguji perubahan terbaru: artifact snapshot boleh ditimpa di repositori, dan Maven secara bawaan memeriksa versi lebih baru setiap interval waktu tertentu. Sebaliknya, versi release seperti 1.5.0 bersifat beku — sekali diterbitkan, berkasnya tidak pernah berubah, dan kesalahan pada rilis diperbaiki lewat rilis baru, bukan menimpa yang lama.

xml Versi kerja modul internal
<version>1.5.0-SNAPSHOT</version>

Disiplinnya jelas: -SNAPSHOT untuk pengembangan antar cabang tim, versi rilis untuk semua artifact yang dipakai luar siklus itu. Build produksi yang menyandarkan snapshot akan berubah-ubah tanpa jejak — halangan utama bagi reproducible build yang kita bahas beberapa chapter lagi.

Strategi Kunci Versi dalam Tim #

Praktik yang sehat menyatukan tiga kebiasaan. Pertama, kunci eksplisit: setiap dependency menulis versi penuh major.minor.patch, tanpa rentang. Kedua, satu sumber versi: version catalog Gradle atau dependencyManagement Maven menampung seluruh angka versi di satu tempat sehingga modul-modul tidak saling beda versi. Ketiga, kenaikan versi sebagai keputusan sadar: pembaruan dependency dilakukan lewat perubahan tercatat yang bisa direviu — apakah manual atau lewat alat otomasi pembaruan dependensi yang membuka pull request per kenaikan — sehingga setiap kenaikan MAJOR tertangani manusia, bukan kebetulan mesin.

Versi yang terkunci rapi belum menutup satu celah: dua dependency bisa menuntut versi berbeda dari library yang sama secara bersamaan. Bagaimana Maven dan Gradle memutuskan satu versi yang menang — dan bagaimana kita mendiagnosisnya — adalah pokok chapter Konflik Dependency.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

SemVer.java

Unduh berkas
java
public class SemVer {

    public static void main(String[] args) {
        System.out.println("== perbandingan numerik per komponen ==");
        System.out.println(verdict("2.9.1", "2.10.0"));
        System.out.println(verdict("1.4.2", "1.4.3"));
        System.out.println(verdict("3.0.0", "3.0.0"));
        System.out.println();
        System.out.println("perbandingan string mentah: \"2.10.0\" vs \"2.9.1\" -> "
                + ("2.10.0".compareTo("2.9.1") < 0
                        ? "\"2.10.0\" dianggap lebih tua (kesimpulan salah)"
                        : "\"2.10.0\" lebih baru"));
        System.out.println();

        System.out.println("== klasifikasi kenaikan dari 1.4.2 ==");
        klasifikasi("1.4.2", "1.4.3");
        klasifikasi("1.4.2", "1.5.0");
        klasifikasi("1.4.2", "2.0.0");
    }

    private static String verdict(String a, String b) {
        int hasil = bandingkan(urai(a), urai(b));
        if (hasil < 0) {
            return a + " vs " + b + " -> " + b + " lebih baru";
        }
        if (hasil > 0) {
            return a + " vs " + b + " -> " + a + " lebih baru";
        }
        return a + " vs " + b + " -> keduanya sama";
    }

    /** "2.10.0" menjadi [2, 10, 0]. */
    private static int[] urai(String versi) {
        String[] bagian = versi.split("\\.");
        int[] angka = new int[bagian.length];
        for (int i = 0; i < bagian.length; i++) {
            angka[i] = Integer.parseInt(bagian[i]);
        }
        return angka;
    }

    /** Bandingkan komponen dari kiri: major, lalu minor, lalu patch. */
    private static int bandingkan(int[] a, int[] b) {
        for (int i = 0; i < Math.min(a.length, b.length); i++) {
            if (a[i] != b[i]) {
                return Integer.compare(a[i], b[i]);
            }
        }
        return Integer.compare(a.length, b.length);
    }

    private static void klasifikasi(String dari, String ke) {
        int[] a = urai(dari);
        int[] b = urai(ke);
        if (b[0] > a[0]) {
            System.out.printf("ke %-6s -> MAJOR : perubahan yang merusak kompatibilitas%n", ke);
        } else if (b[1] > a[1]) {
            System.out.printf("ke %-6s -> MINOR : fitur baru, kompatibel mundur%n", ke);
        } else {
            System.out.printf("ke %-6s -> PATCH : perbaikan bug, kompatibel%n", ke);
        }
    }
}