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:

text
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.0

Mari kita baca dua putusannya:

  1. Putusan Maven: kedua jalur menuju json-lib sama-sama berkedalaman dua langkah dari root, jadi aturan kedua berlaku — layanan-pembayaran dideklarasikan lebih dulu, maka json-lib:1.0.0 menang. Versi yang lebih tua bisa menang semata karena urutan penulisan di POM.
  2. Putusan Gradle: 2.0.0 menang karena merupakan versi tertinggi — urutan deklarasi tidak berpengaruh. Asumsinya: versi lebih baru lebih berkemungkinan kompatibel dengan kedua peminta.
  3. 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:

bash
mvn dependency:tree -Dincludes=com.toko.lib:json-lib
gradle dependencies --configuration runtimeClasspath
gradle dependencyInsight --dependency json-lib

Laporan 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:

xml Mengunci versi json-lib di pom.xml
<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:

kotlin Memaksa versi di build.gradle.kts
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 #

Berkas Sumber

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

MediasiTerdekat.java

Unduh berkas
java
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;
    }
}