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:
<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:
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: 3Mari kita bedah cara kerja penelusuran ini:
- Baris
telusuri("aplikasi-gudang", 0): penelusuran mulai dari proyek kita di kedalaman 0 — Maven menyebutnya root dari pohon dependency. - Method
telusuri: mencatat nama artifact bila belum pernah dikunjungi, lalu menuruni setiap dependency dengan kedalaman +1; inilah inti pembacaan POM berantai yang dilakukan Maven. - Pemanggilan
indeks(nama): memetakan nama artifact ke daftar dependency-nya — padanan sederhana dari Maven membaca POM milik artifact lain. - Baris penghitungan
langsungdantransitif: kedalaman 1 berarti dideklarasikan langsung oleh proyek; kedalaman 2 ke bawah adalah dependency transitif. - Output kedalaman 2:
jackson-coredanjackson-annotationsmuncul di classpath kita tanpa pernah kita deklarasikan — hanya karenajackson-databindmembutuhkan keduanya.
Scope Dependency #
Scope menentukan di tahap mana sebuah dependency tersedia. Maven mengenal empat scope utama:
| Scope | Saat kompilasi | Saat pengujian | Ikut di JAR final | Contoh klasik |
|---|---|---|---|---|
compile (bawaan) | ya | ya | ya | Jackson, SLF4J |
test | tidak | ya | tidak | JUnit, Mockito |
provided | ya | ya | tidak | Servlet API di container |
runtime | tidak | ya | ya | Driver 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.
<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 berkasimport 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);
}
}