Aplikasi nyata jarang berdiri di atas kode sendirian: kita memakai Jackson untuk JSON, JUnit untuk pengujian, SLF4J untuk logging. Pada era sebelum build tool, setiap library diunduh manual dari situs masing-masing, diletakkan di folder lib/, dan didaftarkan satu per satu ke classpath — pekerjaan yang membosankan dan cepat usang. Maven menggantikan seluruh ritual itu dengan beberapa baris deklarasi di pom.xml, lalu mengunduh, menyimpan, dan menyusun classpath secara otomatis. Pada chapter ini kita akan menulis deklarasi dependency, memahami dependency transitif, dan mengenal scope yang menentukan kapan sebuah library ikut hadir.

Mendeklarasikan Dependency #

Dependency dideklarasikan dengan koordinat GAV yang sudah kita kenal di chapter Maven Fundamentals. Berikut deklarasi dua library untuk aplikasi-gudang:

xml Blok dependencies di pom.xml
<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.17.1</version>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Dua blok <dependency> di atas berperan sangat berbeda. Jackson dideklarasikan tanpa <scope>, sehingga mendapat scope bawaan compile — library hadir saat kompilasi, saat pengujian, dan di JAR final. JUnit dideklarasikan dengan <scope>test</scope> sehingga hanya hadir saat kompilasi dan eksekusi kode uji; JUnit tidak pernah ikut terbawa ke artefak produksi, dan pengguna library Anda tidak akan pernah mengunduhnya sebagai dependency transitif.

Dependency Transitif #

Nilai terbesar deklarasi Maven bukan pada unduhan satu library, melainkan pada penelusuran dependency transitif: dependency dari dependency. Kita tidak perlu tahu bahwa jackson-databind membutuhkan jackson-core dan jackson-annotations — Maven membaca POM milik Jackson, lalu POM milik dependency-nya lagi, hingga seluruh pohon terkumpul di classpath. Program berikut meniru penelusuran pohon tersebut:

import java.util.ArrayList;
import java.util.List;

public class ResolusiTransitif {

    // Setiap artifact dan daftar dependency-nya (versi disederhanakan).
    private static final String[] NAMA = {
        "aplikasi-gudang", "jackson-databind", "logback-classic",
        "jackson-core", "jackson-annotations", "logback-core"
    };

    private static final String[][] DEPENDENSI = {
        {"jackson-databind", "logback-classic"},   // aplikasi-gudang
        {"jackson-core", "jackson-annotations"},   // jackson-databind
        {"logback-core"},                          // logback-classic
        {},                                        // jackson-core
        {},                                        // jackson-annotations
        {}                                         // logback-core
    };

    private static final List<String> dikunjungi = new ArrayList<>();
    private static final List<Integer> kedalaman = new ArrayList<>();

    public static void main(String[] args) {
        telusuri("aplikasi-gudang", 0);

        int langsung = 0;
        int transitif = 0;
        for (int i = 0; i < dikunjungi.size(); i++) {
            System.out.printf("kedalaman %d: %s%n", kedalaman.get(i), dikunjungi.get(i));
            if (kedalaman.get(i) == 1) {
                langsung++;
            } else if (kedalaman.get(i) >= 2) {
                transitif++;
            }
        }
        System.out.println();
        System.out.println("dependency langsung : " + langsung);
        System.out.println("dependency transitif: " + transitif);
    }

    /** DFS: catat artifact lalu turuti dependency-nya secara berurutan. */
    private static void telusuri(String nama, int kedalamanSekarang) {
        if (dikunjungi.contains(nama)) {
            return;
        }
        dikunjungi.add(nama);
        kedalaman.add(kedalamanSekarang);

        for (String dependen : DEPENDENSI[indeks(nama)]) {
            telusuri(dependen, kedalamanSekarang + 1);
        }
    }

    private static int indeks(String nama) {
        for (int i = 0; i < NAMA.length; i++) {
            if (NAMA[i].equals(nama)) {
                return i;
            }
        }
        throw new IllegalArgumentException("artifact tidak dikenal: " + nama);
    }
}

Output yang Dihasilkan:

text
kedalaman 0: aplikasi-gudang
kedalaman 1: jackson-databind
kedalaman 2: jackson-core
kedalaman 2: jackson-annotations
kedalaman 1: logback-classic
kedalaman 2: logback-core

dependency langsung : 2
dependency transitif: 3

Mari kita bedah cara kerja penelusuran ini:

  1. Baris telusuri("aplikasi-gudang", 0): penelusuran mulai dari proyek kita di kedalaman 0 — Maven menyebutnya root dari pohon dependency.
  2. Method telusuri: mencatat nama artifact bila belum pernah dikunjungi, lalu menuruni setiap dependency dengan kedalaman +1; inilah inti pembacaan POM berantai yang dilakukan Maven.
  3. Pemanggilan indeks(nama): memetakan nama artifact ke daftar dependency-nya — padanan sederhana dari Maven membaca POM milik artifact lain.
  4. Baris penghitungan langsung dan transitif: kedalaman 1 berarti dideklarasikan langsung oleh proyek; kedalaman 2 ke bawah adalah dependency transitif.
  5. Output kedalaman 2: jackson-core dan jackson-annotations muncul di classpath kita tanpa pernah kita deklarasikan — hanya karena jackson-databind membutuhkan keduanya.

Scope Dependency #

Scope menentukan di tahap mana sebuah dependency tersedia. Maven mengenal empat scope utama:

ScopeSaat kompilasiSaat pengujianIkut di JAR finalContoh klasik
compile (bawaan)yayayaJackson, SLF4J
testtidakyatidakJUnit, Mockito
providedyayatidakServlet API di container
runtimetidakyayaDriver JDBC

Scope provided dipakai ketika lingkungan eksekusi menyediakan library yang sama — misalnya servlet container sudah membawa Servlet API, sehingga menyertakannya lagi di JAR berisiko bertabrakan versi. Scope runtime untuk library yang hanya dibutuhkan lewat refleksi saat program berjalan, seperti driver JDBC yang dimuat lewat DriverManager.

Optional dan Exclusions #

Dua mekanisme memotong pohon transitif. Pertama, <optional>true</optional> pada dependency yang kita terbitkan: konsumen library kita tidak menerima dependency tersebut secara transitif dan wajib memilih sendiri bila membutuhkan fitur terkait — pola yang lazim untuk adapter opsional.

xml Exclusion memangkas dependency transitif
<dependency>
    <groupId>com.toko.gudang</groupId>
    <artifactId>layanan-pembayaran</artifactId>
    <version>1.4.2</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Kedua, <exclusions> seperti di atas: saat layanan-pembayaran menyeret slf4j-log4j12 yang sudah tidak kita inginkan, exclusion membuang artifact itu dari cabang pohon ini tanpa mengubah POM milik pihak ketiga. Exclusion adalah pisau bedah untuk kasus spesifik; bila benturan versi terjadi di mana-mana, dependencyManagement lebih tepat.

dependencyManagement dan Bill of Materials #

Blok <dependencyManagement> tidak menambahkan dependency — blok ini menyetel versi tunggal untuk sekian banyak artifact. Bila proyek punya banyak modul, versi cukup dikunci sekali di POM induk. Pola lanjutannya adalah mengimpor Bill of Materials: pihak seperti Jackson atau Spring menerbitkan daftar artifact yang saling kompatibel dalam satu POM khusus, lalu kita mengimpornya dan mendeklarasikan dependency tanpa tag versi sama sekali.

Deklarasi dependency menunjukkan apa yang masuk classpath; perintah Maven menunjukkan kapan semuanya diproses. Chapter berikutnya membedah lifecycle — urutan fase dari validate hingga deploy yang mengatur seluruh perjalanan build.

Referensi #

Berkas Sumber

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

ResolusiTransitif.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class ResolusiTransitif {

    // Setiap artifact dan daftar dependency-nya (versi disederhanakan).
    private static final String[] NAMA = {
        "aplikasi-gudang", "jackson-databind", "logback-classic",
        "jackson-core", "jackson-annotations", "logback-core"
    };

    private static final String[][] DEPENDENSI = {
        {"jackson-databind", "logback-classic"},   // aplikasi-gudang
        {"jackson-core", "jackson-annotations"},   // jackson-databind
        {"logback-core"},                          // logback-classic
        {},                                        // jackson-core
        {},                                        // jackson-annotations
        {}                                         // logback-core
    };

    private static final List<String> dikunjungi = new ArrayList<>();
    private static final List<Integer> kedalaman = new ArrayList<>();

    public static void main(String[] args) {
        telusuri("aplikasi-gudang", 0);

        int langsung = 0;
        int transitif = 0;
        for (int i = 0; i < dikunjungi.size(); i++) {
            System.out.printf("kedalaman %d: %s%n", kedalaman.get(i), dikunjungi.get(i));
            if (kedalaman.get(i) == 1) {
                langsung++;
            } else if (kedalaman.get(i) >= 2) {
                transitif++;
            }
        }
        System.out.println();
        System.out.println("dependency langsung : " + langsung);
        System.out.println("dependency transitif: " + transitif);
    }

    /** DFS: catat artifact lalu turuti dependency-nya secara berurutan. */
    private static void telusuri(String nama, int kedalamanSekarang) {
        if (dikunjungi.contains(nama)) {
            return;
        }
        dikunjungi.add(nama);
        kedalaman.add(kedalamanSekarang);

        for (String dependen : DEPENDENSI[indeks(nama)]) {
            telusuri(dependen, kedalamanSekarang + 1);
        }
    }

    private static int indeks(String nama) {
        for (int i = 0; i < NAMA.length; i++) {
            if (NAMA[i].equals(nama)) {
                return i;
            }
        }
        throw new IllegalArgumentException("artifact tidak dikenal: " + nama);
    }
}