Aplikasi yang selesai dikembangkan berupa ratusan berkas .class di direktori build — bentuk yang tidak bisa dikirim ke produksi begitu saja. Chapter Packaging JAR di section build tools sudah memperkenalkan format arsipnya; sekarang kita fokus pada tujuan deployment: satu berkas yang dijalankan dengan java -jar di mesin mana pun yang memuat JDK. Dua hambatan utamanya: penentu class utama lewat manifest dan cara membawa dependency. Kita menuntaskan keduanya dengan atribut Main-Class+Class-Path, lalu fat jar yang menelan seluruh dependency ke dalam satu arsip.
JAR Eksekusi dan Manifest #
Atribut Main-Class di META-INF/MANIFEST.MF adalah kontrak yang dibaca launcher java -jar: nama class yang memuat method main. Atribut ditulis lewat berkas manifest kustom lalu diserahkan ke opsi --manifest. Skenario kita memakai aplikasi kasir (package toko) dengan library util:
javac -d out-util src/util/Kwitansi.java
javac -d out-toko -cp out-util src/toko/AplikasiKasir.java
jar --create --file dist/toko-app.jar --manifest manifest.txt -C out-toko .Isi manifest.txt sebelum dikemas:
Class-Path: lib/util.jar
Main-Class: toko.AplikasiKasirOutput yang Dihasilkan (isi META-INF/MANIFEST.MF di dalam JAR):
Manifest-Version: 1.0
Class-Path: lib/util.jar
Main-Class: toko.AplikasiKasir
Created-By: 25.0.4.1 (Eclipse Adoptium)Alat jar menambahkan Manifest-Version dan Created-By secara otomatis, sedangkan kedua baris kitalah yang membuat JAR ini layak dieksekusi.
Menjalankan Dependency dengan Class-Path #
Perintah java -jar mengabaikan flag -cp — classpath hanya boleh datang dari isi JAR itu sendiri atau atribut Class-Path di manifest. Entry Class-Path adalah URL relatif terhadap lokasi JAR, bukan path absolut mesin. Karena util.jar kita letakkan di dist/lib/:
jar --create --file dist/lib/util.jar -C out-util .
java -jar dist/toko-app.jarOutput yang Dihasilkan:
aplikasi-kasir 2.1.0
Rp250,000.00Struktur dist/ harus ikut didistribusikan utuh: toko-app.jar di samping folder lib/. Pendekatan ini menjaga dependency sebagai JAR terpisah sehingga tiap library bisa diganti tanpa membangun ulang aplikasi — pola server aplikasi klasik.
Fat Jar dan Shading #
Cara kedua membuang ketergantungan pada struktur folder: fat jar (uber jar) — seluruh class dependency ditarik ke dalam satu arsip. Opsi -C boleh diulang, satu per direktori kompilasi:
jar --create --file dist/toko-fat.jar --main-class toko.AplikasiKasir -C out-toko . -C out-util .
java -jar dist/toko-fat.jar
jar --describe-module --file dist/toko-fat.jarOutput yang Dihasilkan:
META-INF/
META-INF/MANIFEST.MF
toko/
toko/AplikasiKasir.class
util/
util/Kwitansi.class
aplikasi-kasir 2.1.0
Rp250,000.00
No module descriptor found. Derived automatic module.
toko.fat automatic
requires java.base mandated
contains toko
contains util
main-class toko.AplikasiKasirSatu berkas, satu perintah, dependency ikut di dalamnya. Kedua pendekatan pembawa dependency — folder lib/ dengan Class-Path dan fat jar — berkompromi berbeda:
| Aspek | Class-Path + folder lib/ | Fat jar |
|---|---|---|
| Bentuk distribusi | JAR aplikasi + folder lib/ | satu berkas mandiri |
| Mengganti satu dependency | timpa berkas di lib/ | bangun ulang seluruh arsip |
| Konflik class senama | terpisah per JAR | berisiko saling menimpa |
| Ukuran unduhan per rilis | hanya JAR yang berubah | seluruh bundle setiap kali |
Pada proyek nyata dengan puluhan dependency, penggabungan ini diotomatiskan build tool — Maven memakai maven-shade-plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>toko.AplikasiKasir</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>Membaca Manifest dari Program Java #
Kadang aplikasi perlu membaca manifestnya sendiri — misalnya menampilkan versi dari atribut Implementation-Version. Class java.util.jar.Manifest menyediakan baca-tulis atribut yang sama dengan yang dipakai launcher. Program berikut membangun manifest di memori, menserikannya menjadi bytes (persis isi MANIFEST.MF), lalu membacanya kembali dan menginterpretasi Main-Class serta Class-Path:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
/** Membaca manifest seperti launcher java -jar: Main-Class dan Class-Path relatif. */
public class BacaManifest {
/** Bangun manifest dari pasangan atribut, lalu serikan ke bytes (format MANIFEST.MF). */
static byte[] buatManifest(String[][] atribut) throws Exception {
Manifest mf = new Manifest();
Attributes utama = mf.getMainAttributes();
utama.put(Attributes.Name.MANIFEST_VERSION, "1.0");
for (String[] pasangan : atribut) {
utama.putValue(pasangan[0], pasangan[1]);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
mf.write(out);
return out.toByteArray();
}
/** Baca kembali bytes manifest — tahap yang dilakukan launcher sebelum menjalankan JAR. */
static Manifest bacaManifest(byte[] bytes) throws Exception {
return new Manifest(new ByteArrayInputStream(bytes));
}
/** Selesaikan entry Class-Path relatif terhadap lokasi berkas JAR. */
static String jalurAbsolut(String direktoriJar, String entryRelatif) {
return direktoriJar + "/" + entryRelatif;
}
public static void main(String[] args) throws Exception {
byte[] bytes = buatManifest(new String[][]{
{"Main-Class", "toko.AplikasiKasir"},
{"Class-Path", "lib/util.jar lib/promo.jar"}
});
System.out.println("== Isi MANIFEST.MF hasil serialisasi ==");
// Spesifikasi JAR mewajibkan CRLF sebagai penanda baris pada manifest;
// dinormalisasi ke \n agar tampilan output rapi di terminal.
System.out.print(new String(bytes, StandardCharsets.UTF_8).replace("\r\n", "\n"));
Manifest terbaca = bacaManifest(bytes);
Attributes utama = terbaca.getMainAttributes();
System.out.println("== Interpretasi launcher java -jar ==");
System.out.println("Main-Class : " + utama.getValue(Attributes.Name.MAIN_CLASS));
String classPath = utama.getValue(Attributes.Name.CLASS_PATH);
if (classPath == null) {
System.out.println("Class-Path : (tidak ada) -> hanya isi JAR sendiri");
} else {
System.out.println("Class-Path : " + classPath);
for (String entry : classPath.split(" ")) {
System.out.println(" " + entry + " -> " + jalurAbsolut("dist", entry));
}
}
System.out.println("== Manifest tanpa Main-Class ==");
byte[] tanpaMain = buatManifest(new String[][]{
{"Class-Path", "lib/util.jar"}
});
Manifest m2 = bacaManifest(tanpaMain);
String main = m2.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
System.out.println("Main-Class : " + main);
System.out.println("hasil : launcher menolak, JAR bukan aplikasi eksekusi");
}
}Output yang Dihasilkan:
== Isi MANIFEST.MF hasil serialisasi ==
Manifest-Version: 1.0
Main-Class: toko.AplikasiKasir
Class-Path: lib/util.jar lib/promo.jar
== Interpretasi launcher java -jar ==
Main-Class : toko.AplikasiKasir
Class-Path : lib/util.jar lib/promo.jar
lib/util.jar -> dist/lib/util.jar
lib/promo.jar -> dist/lib/promo.jar
== Manifest tanpa Main-Class ==
Main-Class : null
hasil : launcher menolak, JAR bukan aplikasi eksekusiMari kita bedah bagian-bagiannya:
buatManifest(...): method ini mengisiAttributesutama melaluiputValue, lalumf.write(out)menuliskan format manifest standar: setiap atribut satu barisNama: nilaidiakhiri baris kosong.bacaManifest(...): konstruktorManifest(InputStream)mem-parsing bytes kembali menjadi struktur data — tahap yang dilakukan launcher sebelum mengeksekusi JAR.Attributes.Name.MAIN_CLASSdanCLASS_PATH: konstanta nama atribut baku; memakainya mencegah salah ketik yang membuat manifest terbaca kosong.jalurAbsolut("dist", entry): resolusi entryClass-Pathrelatif terhadap direktori JAR — mengapalib/util.jarmenunjuk kedist/lib/util.jar.- Skenario ketiga: manifest tanpa
Main-Classmenghasilkannull, dan launcher memang menolak menjalankan JAR seperti ini — sumber errorno main manifest attributepada JAR yang dikemas terburu-buru.
Fat jar menuntaskan masalah cara membawa kode, tetapi masih menyisakan satu kebutuhan: mesin target harus terpasang JDK versi yang tepat. Chapter berikutnya membalik pendekatannya — jangan pasang runtime di mesin, bawa runtime bersama aplikasi lewat jlink.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BacaManifest.java
Unduh berkasimport java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
/** Membaca manifest seperti launcher java -jar: Main-Class dan Class-Path relatif. */
public class BacaManifest {
/** Bangun manifest dari pasangan atribut, lalu serikan ke bytes (format MANIFEST.MF). */
static byte[] buatManifest(String[][] atribut) throws Exception {
Manifest mf = new Manifest();
Attributes utama = mf.getMainAttributes();
utama.put(Attributes.Name.MANIFEST_VERSION, "1.0");
for (String[] pasangan : atribut) {
utama.putValue(pasangan[0], pasangan[1]);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
mf.write(out);
return out.toByteArray();
}
/** Baca kembali bytes manifest — tahap yang dilakukan launcher sebelum menjalankan JAR. */
static Manifest bacaManifest(byte[] bytes) throws Exception {
return new Manifest(new ByteArrayInputStream(bytes));
}
/** Selesaikan entry Class-Path relatif terhadap lokasi berkas JAR. */
static String jalurAbsolut(String direktoriJar, String entryRelatif) {
return direktoriJar + "/" + entryRelatif;
}
public static void main(String[] args) throws Exception {
byte[] bytes = buatManifest(new String[][]{
{"Main-Class", "toko.AplikasiKasir"},
{"Class-Path", "lib/util.jar lib/promo.jar"}
});
System.out.println("== Isi MANIFEST.MF hasil serialisasi ==");
// Spesifikasi JAR mewajibkan CRLF sebagai penanda baris pada manifest;
// dinormalisasi ke \n agar tampilan output rapi di terminal.
System.out.print(new String(bytes, StandardCharsets.UTF_8).replace("\r\n", "\n"));
Manifest terbaca = bacaManifest(bytes);
Attributes utama = terbaca.getMainAttributes();
System.out.println("== Interpretasi launcher java -jar ==");
System.out.println("Main-Class : " + utama.getValue(Attributes.Name.MAIN_CLASS));
String classPath = utama.getValue(Attributes.Name.CLASS_PATH);
if (classPath == null) {
System.out.println("Class-Path : (tidak ada) -> hanya isi JAR sendiri");
} else {
System.out.println("Class-Path : " + classPath);
for (String entry : classPath.split(" ")) {
System.out.println(" " + entry + " -> " + jalurAbsolut("dist", entry));
}
}
System.out.println("== Manifest tanpa Main-Class ==");
byte[] tanpaMain = buatManifest(new String[][]{
{"Class-Path", "lib/util.jar"}
});
Manifest m2 = bacaManifest(tanpaMain);
String main = m2.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
System.out.println("Main-Class : " + main);
System.out.println("hasil : launcher menolak, JAR bukan aplikasi eksekusi");
}
}