Kita menaikkan versi satu library kecil, dan sepekan kemudian aplikasi gagal di produksi dengan NoSuchMethodError — padahal build hijau dan seluruh pengujian lulus. Pelakunya sering kali bukan kode kita, melainkan konflik dependency: dua pustaka yang kita pakai menuntut versi berbeda dari library yang sama, dan hanya satu versi yang boleh hadir di classpath. Pada chapter ini kita akan membedah pola konflik paling umum — diamond dependency — membandingkan cara Maven dan Gradle menyelesaikannya, lalu belajar mendiagnosis dan memaksa keputusan versi secara sadar.
Masalah Diamond Dependency #
Pola konflik klasik berbentuk berlian: aplikasi bergantung pada dua modul, dan kedua modul bergantung pada library json-lib dengan versi berbeda. Karena classpath hanya mengizinkan satu com.toko.lib.json.Kurs per classloader, salah satu versi harus menang:
Bahayanya nyata pada runtime. Bila versi 1.0.0 yang menang sedangkan laporan-penjualan dikompilasi melawan method milik 2.0.0, pemanggilan method tersebut melempar NoSuchMethodError — error linkage yang tidak terdeteksi kompilasi karena compiler kita tidak melihat kode kedua pihak. Pengujian integration yang mengcover jalur laporan-penjualan menjadi satu-satunya jaring pengaman sebelum produksi.
Mediasi Maven dan Gradle #
Maven dan Gradle menyelesaikan benturan dengan aturan berbeda. Maven memakai nearest-wins: versi yang berada paling dekat dengan root pohon menang; bila kedalaman sama, deklarasi yang ditulis lebih dulu menang. Gradle memilih versi tertinggi di antara semua kandidat. Program berikut menghitung kedua keputusan untuk kasus berlian di atas:
public class MediasiTerdekat {
// Deklarasi langsung proyek dan versi json-lib yang diminta transitif.
private static final String[][] DEKLARASI = {
{"layanan-pembayaran", "1.4.2", "1.0.0"}, // dideklarasikan lebih dulu
{"laporan-penjualan", "2.1.0", "2.0.0"}
};
public static void main(String[] args) {
System.out.println("pohon dependency:");
System.out.println(" aplikasi-gudang");
for (String[] d : DEKLARASI) {
System.out.println(" +-- " + d[0] + ":" + d[1]);
System.out.println(" +-- json-lib:" + d[2]);
}
System.out.println();
System.out.println("konflik: json-lib diminta dalam dua versi");
// Maven: nearest-wins; kedalaman sama -> deklarasi pertama menang.
System.out.println();
System.out.println("Maven (nearest-wins):");
System.out.println(" kedalaman json-lib sama (2) pada kedua jalur");
System.out.println(" pemenang: json-lib:" + DEKLARASI[0][2]
+ " (dideklarasikan lebih dulu)");
// Gradle: versi tertinggi menang.
String gradle = bandingkan(DEKLARASI[0][2], DEKLARASI[1][2]) >= 0
? DEKLARASI[0][2] : DEKLARASI[1][2];
System.out.println();
System.out.println("Gradle (versi tertinggi):");
System.out.println(" pemenang: json-lib:" + gradle);
}
/** Bandingkan dua versi numerik per komponen, kiri ke kanan. */
private static int bandingkan(String a, String b) {
String[] pa = a.split("\\.");
String[] pb = b.split("\\.");
for (int i = 0; i < Math.min(pa.length, pb.length); i++) {
int hasil = Integer.compare(Integer.parseInt(pa[i]), Integer.parseInt(pb[i]));
if (hasil != 0) {
return hasil;
}
}
return 0;
}
}Output yang Dihasilkan:
pohon dependency:
aplikasi-gudang
+-- layanan-pembayaran:1.4.2
+-- json-lib:1.0.0
+-- laporan-penjualan:2.1.0
+-- json-lib:2.0.0
konflik: json-lib diminta dalam dua versi
Maven (nearest-wins):
kedalaman json-lib sama (2) pada kedua jalur
pemenang: json-lib:1.0.0 (dideklarasikan lebih dulu)
Gradle (versi tertinggi):
pemenang: json-lib:2.0.0Mari kita baca dua putusannya:
- Putusan Maven: kedua jalur menuju
json-libsama-sama berkedalaman dua langkah dari root, jadi aturan kedua berlaku —layanan-pembayarandideklarasikan lebih dulu, makajson-lib:1.0.0menang. Versi yang lebih tua bisa menang semata karena urutan penulisan di POM. - Putusan Gradle:
2.0.0menang karena merupakan versi tertinggi — urutan deklarasi tidak berpengaruh. Asumsinya: versi lebih baru lebih berkemungkinan kompatibel dengan kedua peminta. - Implikasi migrasi: proyek yang berpindah dari Maven ke Gradle dapat menerima versi pemenang yang berbeda untuk kasus yang sama — salah satu alasan hasil build wajib diverifikasi setelah migrasi, bukan dianggap identik.
Mendiagnosis dengan Laporan Dependency #
Kedua tool menyediakan laporan untuk melihat pohon resolusi dan versi mana yang dibuang. Maven memakai goal dependency:tree, Gradle memakai task dependencies dan dependencyInsight:
mvn dependency:tree -Dincludes=com.toko.lib:json-lib
gradle dependencies --configuration runtimeClasspath
gradle dependencyInsight --dependency json-libLaporan dependency:tree menandai versi yang kalah mediasi dengan keterangan omitted for conflict beserta versi yang menang, sedangkan dependencyInsight Gradle meringkas satu artifact: versi terpilih, kandidat yang bersaing, dan alasan pemilihan. Filter -Dincludes membatasi laporan Maven pada satu artifact agar pohon besar tetap mudah dibaca. Jadikan kedua laporan ini langkah pertama setiap kali mencurigai konflik — menebak tanpa laporan hanya memperpanjang penelusuran.
Memaksa Versi Saat Kehendak Kita #
Ketika keputusan otomatis salah, kita mengambil alih. Di Maven, kunci versi ditulis pada dependencyManagement — seluruh cabang pohon tunduk pada versi ini:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.toko.lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
</dependencyManagement>Di Gradle, resolution strategy pada configuration memaksa versi sekalipun ada kandidat lain:
configurations.all {
resolutionStrategy {
force("com.toko.lib:json-lib:2.0.0")
}
}Alternatif pembedah yang lebih halus tetap tersedia dari chapter Maven Dependencies: <exclusions> membuang satu cabang transitif bermasalah, dan API dependency constraints milik Gradle menyetel batas versi tanpa menambah dependency baru. Kombinasi lazimnya: kunci versi lewat dependencyManagement atau version catalog agar keputusan hidup di satu tempat, dan pakai force hanya sebagai jalan terakhir untuk kasus darurat.
Satu benang merah terlihat dari seluruh chapter dependency: kepastian versi adalah segalanya. Konflik, snapshot, dan rentang versi sama-sama mengaburkan pertanyaan sederhana — “apakah build hari ini identik dengan build kemarin?” Chapter berikutnya menjawabnya dengan disiplin reproducible builds.
Referensi #
- Maven: Dependency Mechanism
- Maven Dependency Plugin
- Gradle: Viewing and Debugging Dependencies
- Gradle: Dependency Constraints
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
MediasiTerdekat.java
Unduh berkaspublic class MediasiTerdekat {
// Deklarasi langsung proyek dan versi json-lib yang diminta transitif.
private static final String[][] DEKLARASI = {
{"layanan-pembayaran", "1.4.2", "1.0.0"}, // dideklarasikan lebih dulu
{"laporan-penjualan", "2.1.0", "2.0.0"}
};
public static void main(String[] args) {
System.out.println("pohon dependency:");
System.out.println(" aplikasi-gudang");
for (String[] d : DEKLARASI) {
System.out.println(" +-- " + d[0] + ":" + d[1]);
System.out.println(" +-- json-lib:" + d[2]);
}
System.out.println();
System.out.println("konflik: json-lib diminta dalam dua versi");
// Maven: nearest-wins; kedalaman sama -> deklarasi pertama menang.
System.out.println();
System.out.println("Maven (nearest-wins):");
System.out.println(" kedalaman json-lib sama (2) pada kedua jalur");
System.out.println(" pemenang: json-lib:" + DEKLARASI[0][2]
+ " (dideklarasikan lebih dulu)");
// Gradle: versi tertinggi menang.
String gradle = bandingkan(DEKLARASI[0][2], DEKLARASI[1][2]) >= 0
? DEKLARASI[0][2] : DEKLARASI[1][2];
System.out.println();
System.out.println("Gradle (versi tertinggi):");
System.out.println(" pemenang: json-lib:" + gradle);
}
/** Bandingkan dua versi numerik per komponen, kiri ke kanan. */
private static int bandingkan(String a, String b) {
String[] pa = a.split("\\.");
String[] pb = b.split("\\.");
for (int i = 0; i < Math.min(pa.length, pb.length); i++) {
int hasil = Integer.compare(Integer.parseInt(pa[i]), Integer.parseInt(pb[i]));
if (hasil != 0) {
return hasil;
}
}
return 0;
}
}