Di Maven kita mendeklarasikan dependency dengan scope; di Gradle kita mendeklarasikannya ke dalam configuration — wadah bernama yang menentukan ke classpath mana sebuah dependency dibawa. Filosofinya berbeda secara halus namun berdampak besar: configuration bukan sekadar “kapan library dipakai”, melainkan “apa yang boleh terlihat oleh modul lain”. Ditambah version catalog untuk merapikan ratusan nomor versi, Gradle menawarkan pengelolaan dependency yang presisi untuk build sekecil aplikasi CLI maupun sebesar monorepo ratusan modul. Pada chapter ini kita akan menulis deklarasi dependency, membedakan implementation dari api, dan menyusun version catalog libs.versions.toml.

Deklarasi Dependency dan Konfigurasi #

Dependency dideklarasikan pada blok dependencies di build.gradle.kts:

kotlin Blok dependencies di build.gradle.kts
dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
    implementation(project(":lib-internal"))
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
    runtimeOnly("org.postgresql:postgresql:42.7.3")
}

Setiap baris memuat koordinat group:artifact:version — format yang sama dengan Maven karena Gradle membaca repositori yang kompatibel, termasuk Maven Central. Nama configuration di depan (implementation, testImplementation, runtimeOnly) yang menentukan nasibnya. Bentuk project(":lib-internal") menyatakan dependency antar modul dalam multi-project build, diresolusikan lewat output task masing-masing modul, bukan lewat JAR terbitan.

implementation versus api #

Perbedaan paling penting dalam daftar Gradle adalah antara implementation dan api. Keduanya sama-sama membawa dependency ke classpath runtime; bedanya pada classpath kompilasi modul konsumen. Program berikut memodelkan dua classpath proyek app yang mengonsumsi modul lib-internal:

import java.util.Map;
import java.util.TreeMap;

public class KonfigurasiDistribusi {

    // Dependency milik modul 'lib-internal' beserta konfigurasinya.
    private static final String[][] DEPS_LIB_INTERNAL = {
        {"jackson-databind", "api"},
        {"logback-classic",  "implementation"}
    };

    public static void main(String[] args) {
        // Bagian 1: version catalog sebagaimana tersimpan di libs.versions.toml
        Map<String, String> katalog = new TreeMap<>();
        katalog.put("jackson-databind", "com.fasterxml.jackson.core:jackson-databind:2.17.1");
        katalog.put("logback-classic",  "ch.qos.logback:logback-classic:1.5.6");

        System.out.println("== version catalog (gradle/libs.versions.toml) ==");
        for (Map.Entry<String, String> entri : katalog.entrySet()) {
            System.out.printf("%-16s -> %s%n", entri.getKey(), entri.getValue());
        }
        System.out.println();

        // Bagian 2: resolusi classpath proyek 'app' yang bergantung pada lib-internal
        System.out.println("proyek 'app' memakai implementation(project(\":lib-internal\"))");
        System.out.println();
        System.out.println("compileClasspath 'app' (saat menulis kode):");
        System.out.println("  lib-internal");
        for (String[] dep : DEPS_LIB_INTERNAL) {
            if (dep[1].equals("api")) {
                System.out.println("  " + dep[0] + "  (via api lib-internal)");
            }
        }
        System.out.println();
        System.out.println("runtimeClasspath 'app' (saat program berjalan):");
        System.out.println("  lib-internal");
        for (String[] dep : DEPS_LIB_INTERNAL) {
            System.out.println("  " + dep[0]);
        }
    }
}

Output yang Dihasilkan:

text
== version catalog (gradle/libs.versions.toml) ==
jackson-databind -> com.fasterxml.jackson.core:jackson-databind:2.17.1
logback-classic  -> ch.qos.logback:logback-classic:1.5.6

proyek 'app' memakai implementation(project(":lib-internal"))

compileClasspath 'app' (saat menulis kode):
  lib-internal
  jackson-databind  (via api lib-internal)

runtimeClasspath 'app' (saat program berjalan):
  lib-internal
  jackson-databind
  logback-classic

Mari kita bedah hasil resolusinya:

  1. compileClasspath 'app': hanya memuat lib-internal dan jackson-databind — dependency yang dideklarasikan api oleh lib-internal ikut terlihat oleh konsumen; logback-classic sama sekali tidak muncul karena dideklarasikan implementation.
  2. runtimeClasspath 'app': memuat ketiganya — saat berjalan, seluruh dependency transitif hadir tanpa memandang configuration asalnya.
  3. Implikasi kompilasi: karena logback-classic tak terlihat, kode app tidak bisa memanggil class logging milik Logback — modul terlindung dari detail internal pustaka yang dipakainya.
  4. Implikasi kecepatan: bila lib-internal kelak mengganti implementasi logging-nya, app tidak perlu dikompilasi ulang karena classpath kompilasinya tidak berubah — inilah alasan Gradle merekomendasikan implementation sebagai pilihan bawaan.
KonfigurasiTerlihat di kompilasi konsumenHadir saat runtimeKegunaan tipikal
apiyayatipe library muncul di method publik modul
implementationtidakyadetail internal modul
testImplementationtidakhanya saat ujiJUnit, Mockito
compileOnlyyatidakannotation processor
runtimeOnlytidakyadriver JDBC, implementasi logging

Version Catalog #

Proyek multi-modul dengan puluhan dependency membuat nomor versi tersebar. Version catalog memusatkan seluruhnya dalam satu berkas TOML — gradle/libs.versions.toml:

toml gradle/libs.versions.toml
[versions]
jackson = "2.17.1"
logback = "1.5.6"

[libraries]
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
logback-classic  = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }

Berkas ini dibaca Gradle pada tahap initialization dan diekspos ke build script sebagai objek libs, dengan alias yang dipisah tanda hubung menjadi akses bertitik:

kotlin Pemakaian alias di build.gradle.kts
dependencies {
    implementation(libs.jackson.databind)
    implementation(libs.logback.classic)
}

Manfaatnya ganda. Pertama, satu versi untuk semua modul — jackson = "2.17.1" diabadikan sekali dan dipakai lintas app, lib-internal, dan modul lain. Kedua, versi dapat dinaikkan dalam satu commit yang mudah direviu, jauh lebih aman daripada mengganti angka di dua belas build script berbeda.

Menyelidiki dan Mengarahkan Resolusi #

Sama seperti Maven, Gradle menyediakan perintah diagnostik untuk membaca hasil resolusi:

bash
gradle dependencies
gradle dependencyInsight --dependency jackson-databind --configuration runtimeClasspath

Perintah dependencies menampilkan seluruh pohon per configuration, sedangkan dependencyInsight menelusuri satu artifact — jawaban cepat untuk pertanyaan “versi berapa yang terpilih dan dari cabang mana asalnya”. Ketika hasil resolusi perlu diarahkan, Gradle membuka resolution strategy: aturan pada level configuration untuk memaksa versi, menolak versi tertentu, atau mensubstitusi modul — alat utama saat menghadapi konflik versi yang kita bahas tuntas pada chapter Konflik Dependency.

Deklarasi dan versi sudah tertata; satu potong lagi membuat build Gradle terasa ringkas dalam praktik: plugin yang otomatis mendaftarkan task compileJava, test, dan jar begitu dua baris ditulis. Chapter berikutnya membongkar cara kerja plugin Gradle.

Referensi #

Berkas Sumber

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

KonfigurasiDistribusi.java

Unduh berkas
java
import java.util.Map;
import java.util.TreeMap;

public class KonfigurasiDistribusi {

    // Dependency milik modul 'lib-internal' beserta konfigurasinya.
    private static final String[][] DEPS_LIB_INTERNAL = {
        {"jackson-databind", "api"},
        {"logback-classic",  "implementation"}
    };

    public static void main(String[] args) {
        // Bagian 1: version catalog sebagaimana tersimpan di libs.versions.toml
        Map<String, String> katalog = new TreeMap<>();
        katalog.put("jackson-databind", "com.fasterxml.jackson.core:jackson-databind:2.17.1");
        katalog.put("logback-classic",  "ch.qos.logback:logback-classic:1.5.6");

        System.out.println("== version catalog (gradle/libs.versions.toml) ==");
        for (Map.Entry<String, String> entri : katalog.entrySet()) {
            System.out.printf("%-16s -> %s%n", entri.getKey(), entri.getValue());
        }
        System.out.println();

        // Bagian 2: resolusi classpath proyek 'app' yang bergantung pada lib-internal
        System.out.println("proyek 'app' memakai implementation(project(\":lib-internal\"))");
        System.out.println();
        System.out.println("compileClasspath 'app' (saat menulis kode):");
        System.out.println("  lib-internal");
        for (String[] dep : DEPS_LIB_INTERNAL) {
            if (dep[1].equals("api")) {
                System.out.println("  " + dep[0] + "  (via api lib-internal)");
            }
        }
        System.out.println();
        System.out.println("runtimeClasspath 'app' (saat program berjalan):");
        System.out.println("  lib-internal");
        for (String[] dep : DEPS_LIB_INTERNAL) {
            System.out.println("  " + dep[0]);
        }
    }
}