Fat jar menyelesaikan masalah dependency aplikasi, tetapi masih ada prasyarat tersembunyi: mesin target harus terpasang JDK/JRE dengan versi yang sesuai. Ketidakcocokan versi JDK antara mesin build dan mesin produksi adalah sumber bug yang sulit dicari, sementara menginstal JDK penuh di setiap server juga memboroskan ruang. Alat jlink pada JDK membalik logikanya: aplikasi membawa runtime-nya sendiri — satu direktori berisi bin/java plus hanya modul-modul yang benar-benar dibutuhkan. Chapter ini membongkar cara jlink menentukan modul mana yang boleh ikut, lalu merakit runtime kustom untuk aplikasi kasir kita.

Runtime Lengkap sebagai Beban #

JDK 25 yang terpasang di mesin kita memuat modul platform yang jauh lebih banyak daripada kebutuhan satu aplikasi:

bash
java --list-modules | wc -l
java --list-modules | head -5

Output yang Dihasilkan:

Angka 69 modul itu mencakup desktop Swing, compiler javac, API manajemen dan diagnostik, sampai alat untuk scripting — padahal aplikasi kasir kita hanya memakai sebagian kecil java.base. Semua modul itu ikut termuat di setiap instalasi JDK. Dua cara menyediakan runtime memperlihatkan posisi masalahnya:

AspekJDK terpasang di mesinRuntime hasil jlink
Modul yang ikutseluruh 69 modul platformhanya closure kebutuhan aplikasi
Prasyarat mesin targetinstalasi JDK versi tepattidak ada — direktori disalin
Risiko beda versi runtimeada, tiap mesin sendiritidak — runtime dibekukan bersama aplikasi
Cocok untukmesin pengembangandeployment server dan container

Graf Modul dan Dependensi Transitif #

Sebelum merakit, jlink harus tahu modul apa saja yang dibutuhkan aplikasi — dihitung dari graf requires Java Platform Module System (JPMS). Deskriptor satu modul bisa dilihat langsung:

bash
java --describe-module java.sql

Output yang Dihasilkan:

text
[email protected]
exports java.sql
exports javax.sql
requires java.logging transitive
requires java.transaction.xa transitive
requires java.base mandated
requires java.xml transitive
uses java.sql.Driver

Modul java.sql ternyata menarik tiga modul lain secara transitif; baris uses mencatat service provider yang dimuat lewat ServiceLoader. Untuk aplikasi nyata, alat jdeps menghitung titik awalnya dari bytecode hasil kompilasi:

bash
jdeps --print-module-deps halo.jar

Output yang Dihasilkan:

text
java.base

Aplikasi halo.jar kita hanya memakai API dari java.base, jadi jdeps merekomendasikan itu sebagai daftar --add-modules.

Perintah berikut merakit runtime kustom ke direktori img, lalu membuktikan aplikasi tetap berjalan:

bash
jlink --add-modules java.base --output img --strip-debug --no-header-files --no-man-pages
img/bin/java --list-modules
img/bin/java -jar halo.jar

Output yang Dihasilkan:

text
[email protected]
halo dari image kustom

Direktori img adalah runtime mandiri: cukup disalin ke mesin target (atau dijadikan basis image Docker di chapter berikutnya) tanpa instalasi JDK apa pun. Opsi --strip-debug membuang tabel simbol debug, --no-header-files dan --no-man-pages membuang berkas non-runtime, dan --compress memampatkan modul hasil rakitan.

Bagaimana persisnya jlink meluaskan --add-modules java.sql menjadi lima modul? Program berikut meniru mesin resolusinya dengan graf requires yang disalin dari output java --describe-module JDK 25 yang sesungguhnya:

java GrafModul.java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/** Simulasi resolusi modul jlink: closure graf requires dari sekumpulan modul akar. */
public class GrafModul {

    /** Graf requires modul JDK 25 (disalin dari java --describe-module). */
    static final Map<String, List<String>> REQUIRES = Map.of(
            "java.sql", List.of("java.logging", "java.transaction.xa", "java.xml"),
            "java.logging", List.of(),
            "java.transaction.xa", List.of(),
            "java.xml", List.of(),
            "java.net.http", List.of(),
            "java.management", List.of());

    /** Closure ala jlink: berangkat dari modul akar, ikuti semua edge requires.
     *  java.base selalu disertakan karena sifatnya mandated. */
    static Set<String> resolusi(String... akar) {
        Set<String> hasil = new TreeSet<>(); // TreeSet -> urutan cetak deterministik
        hasil.add("java.base");
        Deque<String> antrean = new ArrayDeque<>(List.of(akar));
        while (!antrean.isEmpty()) {
            String m = antrean.poll();
            if (hasil.add(m)) {
                antrean.addAll(REQUIRES.getOrDefault(m, List.of()));
            }
        }
        return hasil;
    }

    static void laporan(String judul, Set<String> modul) {
        System.out.println("== " + judul + " ==");
        System.out.println("jumlah modul runtime: " + modul.size());
        for (String m : modul) {
            System.out.println("  " + m);
        }
        System.out.println();
    }

    public static void main(String[] args) {
        System.out.println("JDK lengkap terpasang: 69 modul (java --list-modules)");
        System.out.println();

        laporan("jlink --add-modules java.sql", resolusi("java.sql"));
        laporan("jlink --add-modules java.net.http", resolusi("java.net.http"));
        laporan("jlink --add-modules java.sql,java.management",
                resolusi("java.sql", "java.management"));
    }
}

Output yang Dihasilkan:

text
JDK lengkap terpasang: 69 modul (java --list-modules)

== jlink --add-modules java.sql ==
jumlah modul runtime: 5
  java.base
  java.logging
  java.sql
  java.transaction.xa
  java.xml

== jlink --add-modules java.net.http ==
jumlah modul runtime: 2
  java.base
  java.net.http

== jlink --add-modules java.sql,java.management ==
jumlah modul runtime: 6
  java.base
  java.logging
  java.management
  java.sql
  java.transaction.xa
  java.xml

Mari kita bedah mekanismenya:

  1. REQUIRES: peta graf modul — kunci adalah nama modul, nilai adalah daftar modul yang di-requires. Data ini bukan karangan: baris-barisnya persis output java --describe-module java.sql yang kita lihat di atas.
  2. resolusi(...): penelusuran breadth-first memakai ArrayDeque sebagai antrean. Setiap modul yang baru masuk hasil menaruh seluruh tetangganya ke antrean — cara baku menghitung transitive closure dari sekumpulan simpul akar.
  3. hasil.add("java.base") di awal: modul java.base bersifat mandated — setiap modul Java secara implisit membutuhkannya, sehingga jlink selalu menyertakannya tanpa perlu ditulis eksplisit.
  4. TreeSet sebagai wadah hasil: iterasi terurut menaik sehingga daftar modul tercetak rapi dan deterministik — urutan yang sama dengan listing img/bin/java --list-modules pada runtime hasil jlink --add-modules java.sql.
  5. Laporan ketiga: dua modul akar digabung menjadi satu rakitan — closure gabungan berisi enam modul karena java.management hanya menambah dirinya di atas closure java.sql.

Runtime kustom plus JAR aplikasi masih berbentuk dua artefak terpisah. Bagi pengguna akhir desktop, kita bisa melangkah lebih jauh: membungkus semuanya menjadi installer platform — .msi, .dmg, atau .deb — dengan jpackage.

Referensi #

Berkas Sumber

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

GrafModul.java

Unduh berkas
java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/** Simulasi resolusi modul jlink: closure graf requires dari sekumpulan modul akar. */
public class GrafModul {

    /** Graf requires modul JDK 25 (disalin dari java --describe-module). */
    static final Map<String, List<String>> REQUIRES = Map.of(
            "java.sql", List.of("java.logging", "java.transaction.xa", "java.xml"),
            "java.logging", List.of(),
            "java.transaction.xa", List.of(),
            "java.xml", List.of(),
            "java.net.http", List.of(),
            "java.management", List.of());

    /** Closure ala jlink: berangkat dari modul akar, ikuti semua edge requires.
     *  java.base selalu disertakan karena sifatnya mandated. */
    static Set<String> resolusi(String... akar) {
        Set<String> hasil = new TreeSet<>(); // TreeSet -> urutan cetak deterministik
        hasil.add("java.base");
        Deque<String> antrean = new ArrayDeque<>(List.of(akar));
        while (!antrean.isEmpty()) {
            String m = antrean.poll();
            if (hasil.add(m)) {
                antrean.addAll(REQUIRES.getOrDefault(m, List.of()));
            }
        }
        return hasil;
    }

    static void laporan(String judul, Set<String> modul) {
        System.out.println("== " + judul + " ==");
        System.out.println("jumlah modul runtime: " + modul.size());
        for (String m : modul) {
            System.out.println("  " + m);
        }
        System.out.println();
    }

    public static void main(String[] args) {
        System.out.println("JDK lengkap terpasang: 69 modul (java --list-modules)");
        System.out.println();

        laporan("jlink --add-modules java.sql", resolusi("java.sql"));
        laporan("jlink --add-modules java.net.http", resolusi("java.net.http"));
        laporan("jlink --add-modules java.sql,java.management",
                resolusi("java.sql", "java.management"));
    }
}