Selama ini aplikasi kita berjalan lewat java HaloDuniaSwing.java — nyaman bagi pembeli buku, tidak bagi pengguna akhir. Pengguna desktop menginginkan satu berkas pemasang (installer) yang dijalankan sekali, menaruh ikon di menu Start atau Launchpad, dan membuka aplikasi dengan klik ganda tanpa memasang apa pun yang bernama JDK. Tiga alat bawaan JDK menuntaskan perjalanan itu: jar membungkus kode menjadi satu arsip, jlink merakit citra runtime yang memuat hanya modul yang dibutuhkan aplikasi, dan jpackage membungkus keduanya menjadi pemasang asli platform — msi untuk Windows, dmg untuk macOS, deb/rpm untuk Linux. Chapter ini menelusuri ketiga lapis tersebut berurutan.

Tiga Lapis Distribusi #

LapisAlatHasilPrasyarat di komputer pengguna
1jaraplikasi.jar yang bisa dijalankan java -jarJDK/JRE terpasang
2jlinkdirektori citra runtime dengan bin/java sendiritidak ada
3jpackagepemasang asli platformtidak ada

Lapis pertama memang mudah tetapi memindahkan beban pemasangan Java ke pengguna. Lapis kedua membebaskan beban itu dengan runtime mini — sering di bawah 50 MB untuk aplikasi Swing karena jlink hanya menyalin modul yang memang dirujuk. Lapis ketiga membungkus citra runtime plus ikon plus pintasan menjadi pengalaman pemasangan yang dikenal tiap sistem operasi.

Jar yang Bisa Dijalankan #

Langkah pertama adalah arsip jar dengan titik masuk. Berkas MANIFEST.MF di dalam jar menyatakan kelas utama sehingga java -jar tahu harus mulai dari mana:

bash Membuat jar yang dapat dieksekusi
javac -d target/classes HaloDuniaSwing.java
jar --create --file target/aplikasi-siswa.jar \
    --main-class HaloDuniaSwing \
    -C target/classes .
java -jar target/aplikasi-siswa.jar

Opsi --main-class pada jar menuliskan Main-Class: ke manifest secara otomatis; -C target/classes . memasukkan isi direktori kompilasi ke akar arsip. Hasilnya satu berkas yang memuat seluruh class aplikasi — tetapi masih membutuhkan Java di mesin target.

Menemukan Modul dengan jdeps #

Citra runtime jlink dirakit dari daftar modul, dan jdeps adalah alat yang menyusun daftar itu dari bytecode kita. Untuk aplikasi Swing sederhana:

bash jdeps mencari modul yang dibutuhkan
jdeps --print-module-deps target/aplikasi-siswa.jar

Keluaran untuk aplikasi Swing murni adalah java.desktop — modul yang membawa javax.swing sekaligus dependensinya java.base. Aplikasi yang memakai HttpClient akan menambah java.net.http; aplikasi dengan JavaFX menambahkan modul OpenJFX dari lokasi SDK. Nilai keluaran jdeps inilah yang kita berikan ke --add-modules milik jlink, sehingga citra runtime tidak membawa modul yang tidak pernah disentuh — tidak ada java.sql, tidak ada java.compiler.

Aplikasi modular — yang memiliki module-info.java — menikmati jlink paling penuh karena dependensinya sudah dinyatakan eksplisit:

java module-info.java untuk aplikasi Swing modular
module aplikasi.siswa {
    requires java.desktop;
    exports aplikasi.siswa;
}
bash Merakit citra runtime dengan jlink
jlink --add-modules aplikasi.siswa \
      --strip-debug --no-header-files --no-man-pages \
      --compress zip-6 \
      --output runtime-ku

Hasilnya direktori runtime-ku/ berisi bin/java (atau bin\java.exe), lib/modules yang sudah dikompresi, dan tidak ada file pengembang. Aplikasi kini bisa berjalan dengan runtime-ku/bin/java -jar target/aplikasi-siswa.jar di komputer tanpa Java — seluruh mesin virtual ikut berpindah. Opsi --compress zip-6 memakai kompresi tertinggi yang tetap cepat saat start; --strip-debug membuang tabel informasi baris yang hanya berguna saat menelusuri galat.

Pemasang dengan jpackage #

jpackage — alat resmi sejak JDK 14 — mengambil jar plus citra runtime lalu menghasilkan pemasang asli:

bash jpackage menghasilkan pemasang asli
jpackage --type msi \
         --name aplikasi-siswa --version 1.0.0 \
         --input target \
         --main-jar aplikasi-siswa.jar \
         --main-class HaloDuniaSwing \
         --runtime-image runtime-ku \
         --win-menu --win-shortcut
Platform--typeHasil
Windowsmsi, exepemasang database Windows
macOSdmg, pkgcitra cakram / paket pemasang
Linuxdeb, rpmpaket Debian / Red Hat

Opsi --input menunjuk direktori berisi seluruh jar aplikasi (semua berkas di direktori itu ikut dimasukkan), --main-jar memilih titik masuk, dan --runtime-image memakai citra jlink yang sudah kita rakit. Tanpa --runtime-image, jpackage merakit runtime sendiri dari --add-modules yang kita sebutkan — hasilnya sama, kendali ukurannya saja berkurang. macOS menambahkan syarat khusus: aplikasi yang bertanda tangan dan notarized Apple saja lolos Gatekeeper default — proses administratif di luar kode Java.

Merencanakan Kemasan Secara Program #

Parameter kemasan yang tepat berbeda di tiap platform, dan merangkumnya dalam satu rencana yang bisa dibaca — serta nanti diterjemahkan ke skrip build — adalah praktik yang baik. Program berikut menyusun manifest, perintah jlink, dan tiga perintah jpackage dari satu tabel konfigurasi:

import java.util.LinkedHashMap;
import java.util.Map;

/** Menyusun rencana kemasan: manifest, jlink, dan jpackage per OS. */
public class RencanaKemasan {
    public static void main(String[] args) {
        Map<String, String> aplikasi = new LinkedHashMap<>();
        aplikasi.put("nama", "aplikasi-siswa");
        aplikasi.put("versi", "1.0.0");
        aplikasi.put("kelasUtama", "HaloDuniaSwing");
        aplikasi.put("modul", "aplikasi.siswa");

        System.out.println("== berkas MANIFEST.MF (jar biasa) ==");
        System.out.println("Main-Class: " + aplikasi.get("kelasUtama"));
        System.out.println("Implementation-Title: " + aplikasi.get("nama"));
        System.out.println("Implementation-Version: " + aplikasi.get("versi"));

        System.out.println();
        System.out.println("== citra runtime jlink (modul java.desktop disertakan) ==");
        System.out.println("jlink --add-modules " + aplikasi.get("modul")
                + " --strip-debug --no-header-files --no-man-pages"
                + " --compress zip-6 --output runtime-ku");

        System.out.println();
        System.out.println("== perintah jpackage per platform ==");
        Map<String, String> tipePemasang = new LinkedHashMap<>();
        tipePemasang.put("linux", "deb");
        tipePemasang.put("windows", "msi");
        tipePemasang.put("macos", "dmg");

        for (Map.Entry<String, String> e : tipePemasang.entrySet()) {
            System.out.printf("jpackage --type %s --name %s --version %s%n",
                    e.getValue(), aplikasi.get("nama"), aplikasi.get("versi"));
            System.out.printf("          --input target/ --main-jar %s.jar%n",
                    aplikasi.get("nama"));
            System.out.printf("          --main-class %s --runtime-image runtime-ku   [%s]%n",
                    aplikasi.get("kelasUtama"), e.getKey());
        }
    }
}

Output yang Dihasilkan:

text
== berkas MANIFEST.MF (jar biasa) ==
Main-Class: HaloDuniaSwing
Implementation-Title: aplikasi-siswa
Implementation-Version: 1.0.0

== citra runtime jlink (modul java.desktop disertakan) ==
jlink --add-modules aplikasi.siswa --strip-debug --no-header-files --no-man-pages --compress zip-6 --output runtime-ku

== perintah jpackage per platform ==
jpackage --type deb --name aplikasi-siswa --version 1.0.0
          --input target/ --main-jar aplikasi-siswa.jar
          --main-class HaloDuniaSwing --runtime-image runtime-ku   [linux]
jpackage --type msi --name aplikasi-siswa --version 1.0.0
          --input target/ --main-jar aplikasi-siswa.jar
          --main-class HaloDuniaSwing --runtime-image runtime-ku   [windows]
jpackage --type dmg --name aplikasi-siswa --version 1.0.0
          --input target/ --main-jar aplikasi-siswa.jar
          --main-class HaloDuniaSwing --runtime-image runtime-ku   [macos]

Mari kita bedah struktur kecilnya:

  1. Baris 7-12 (LinkedHashMap): peta konfigurasi memakai LinkedHashMap agar urutan cetakan sama dengan urutan penyisipan — satu keputusan kecil yang membuat keluaran deterministik dan mudah dibandingkan.
  2. Baris 14-17 (manifest): tiga baris teratas manifest jar — Main-Class wajib agar java -jar berfungsi; dua sisanya metadata yang tampil saat pengguna memeriksa properti berkas.
  3. Baris 20-23 (jlink): nama modul aplikasi dari peta langsung menempel pada --add-modules. Karena module-info.java kita requires java.desktop, jlink menarik modul itu beserta seluruh rangkaian dependensinya.
  4. Baris 26-30 + loop 31-40: tabel tipePemasang memetakan platform ke jenis pemasang. Loop mencetak tiga blok perintah — satu per platform — dari sumber data yang sama, sehingga menambah platform baru cukup menambah satu baris peta.

Dengan pemasang di tangan, siklus hidup aplikasi desktop kita rampung: dari jendela pertama hingga produk yang terpasang di komputer pengguna. Sebelum menutup Section ini, satu topik penutup yang menentukan umur kode: arsitektur — bagaimana menyusun aplikasi agar tampilan tetap tipis, logika teruji, dan tim bisa bekerja tanpa saling menginjak.

Referensi #

Berkas Sumber

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

RencanaKemasan.java

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

/** Menyusun rencana kemasan: manifest, jlink, dan jpackage per OS. */
public class RencanaKemasan {
    public static void main(String[] args) {
        Map<String, String> aplikasi = new LinkedHashMap<>();
        aplikasi.put("nama", "aplikasi-siswa");
        aplikasi.put("versi", "1.0.0");
        aplikasi.put("kelasUtama", "HaloDuniaSwing");
        aplikasi.put("modul", "aplikasi.siswa");

        System.out.println("== berkas MANIFEST.MF (jar biasa) ==");
        System.out.println("Main-Class: " + aplikasi.get("kelasUtama"));
        System.out.println("Implementation-Title: " + aplikasi.get("nama"));
        System.out.println("Implementation-Version: " + aplikasi.get("versi"));

        System.out.println();
        System.out.println("== citra runtime jlink (modul java.desktop disertakan) ==");
        System.out.println("jlink --add-modules " + aplikasi.get("modul")
                + " --strip-debug --no-header-files --no-man-pages"
                + " --compress zip-6 --output runtime-ku");

        System.out.println();
        System.out.println("== perintah jpackage per platform ==");
        Map<String, String> tipePemasang = new LinkedHashMap<>();
        tipePemasang.put("linux", "deb");
        tipePemasang.put("windows", "msi");
        tipePemasang.put("macos", "dmg");

        for (Map.Entry<String, String> e : tipePemasang.entrySet()) {
            System.out.printf("jpackage --type %s --name %s --version %s%n",
                    e.getValue(), aplikasi.get("nama"), aplikasi.get("versi"));
            System.out.printf("          --input target/ --main-jar %s.jar%n",
                    aplikasi.get("nama"));
            System.out.printf("          --main-class %s --runtime-image runtime-ku   [%s]%n",
                    aplikasi.get("kelasUtama"), e.getKey());
        }
    }
}