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:
| Komponen | Naik ketika | Dampak bagi pemakai library |
|---|---|---|
| MAJOR | API berubah tidak kompatibel | kode lama berpotensi gagal kompilasi |
| MINOR | fitur baru yang kompatibel mundur | aman; fitur baru tersedia |
| PATCH | perbaikan bug kompatibel | aman; 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:
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:
== 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 kompatibilitasMari kita bedah hasilnya:
- Baris
2.9.1 vs 2.10.0: perbandingan numerik per komponen memutuskan2.10.0lebih baru karena komponen kedua10 > 9— komponen pertama yang sama langsung dilewati. - Baris perbandingan string mentah:
String.compareTomembandingkan 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 methoduraidi atas. - Blok klasifikasi: dari
1.4.2, naik ke1.4.3,1.5.0, dan2.0.0mewakili 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:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>[2.16,)</version>
</dependency>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.
<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 berkaspublic 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);
}
}
}