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:
java --list-modules | wc -l
java --list-modules | head -5Output 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:
| Aspek | JDK terpasang di mesin | Runtime hasil jlink |
|---|---|---|
| Modul yang ikut | seluruh 69 modul platform | hanya closure kebutuhan aplikasi |
| Prasyarat mesin target | instalasi JDK versi tepat | tidak ada — direktori disalin |
| Risiko beda versi runtime | ada, tiap mesin sendiri | tidak — runtime dibekukan bersama aplikasi |
| Cocok untuk | mesin pengembangan | deployment 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:
java --describe-module java.sqlOutput yang Dihasilkan:
[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.DriverModul 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:
jdeps --print-module-deps halo.jarOutput yang Dihasilkan:
java.baseAplikasi halo.jar kita hanya memakai API dari java.base, jadi jdeps merekomendasikan itu sebagai daftar --add-modules.
Merakit Runtime dengan jlink #
Perintah berikut merakit runtime kustom ke direktori img, lalu membuktikan aplikasi tetap berjalan:
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.jarOutput yang Dihasilkan:
[email protected]
halo dari image kustomDirektori 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.
Mesin Resolusi di Balik jlink #
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:
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:
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.xmlMari kita bedah mekanismenya:
REQUIRES: peta graf modul — kunci adalah nama modul, nilai adalah daftar modul yang di-requires. Data ini bukan karangan: baris-barisnya persis outputjava --describe-module java.sqlyang kita lihat di atas.resolusi(...): penelusuran breadth-first memakaiArrayDequesebagai antrean. Setiap modul yang baru masukhasilmenaruh seluruh tetangganya ke antrean — cara baku menghitung transitive closure dari sekumpulan simpul akar.hasil.add("java.base")di awal: moduljava.basebersifat mandated — setiap modul Java secara implisit membutuhkannya, sehinggajlinkselalu menyertakannya tanpa perlu ditulis eksplisit.TreeSetsebagai wadah hasil: iterasi terurut menaik sehingga daftar modul tercetak rapi dan deterministik — urutan yang sama dengan listingimg/bin/java --list-modulespada runtime hasiljlink --add-modules java.sql.- Laporan ketiga: dua modul akar digabung menjadi satu rakitan — closure gabungan berisi enam modul karena
java.managementhanya menambah dirinya di atas closurejava.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 #
- Oracle: jlink Reference untuk JDK 25
- Oracle: jdeps Reference untuk JDK 25
- JEP 282: jlink
- JEP 220: Modular Run-Time Images
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
GrafModul.java
Unduh berkasimport 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"));
}
}