Executable JAR adalah JAR yang dapat dijalankan langsung dengan perintah java -jar app.jar tanpa perlu menentukan classpath atau class utama secara eksplisit. Kunci dari executable JAR adalah atribut Main-Class di META-INF/MANIFEST.MF. Tanpa atribut ini, java -jar menolak dengan error no main manifest attribute. Pada chapter ini kita akan membedah anatomi executable JAR, mendemokan pembuatan manifest dengan Main-Class dan Class-Path, serta menjelaskan execution flow JVM saat menjalankan java -jar — termasuk kasus khusus Spring Boot yang menggunakan JarLauncher untuk memuat nested JAR.
Anatomi Executable JAR #
Sebuah executable JAR minimal berisi tiga komponen: file META-INF/MANIFEST.MF dengan atribut Main-Class, file class utama, dan (opsional) runtime library. Struktur ini bisa diperluas untuk kasus khusus seperti Spring Boot yang menyimpan dependency sebagai JAR di dalam JAR.
// executable_jars.kt — Demo executable JAR manifest dan execution flow
import java.util.jar.Attributes
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import java.io.ByteArrayOutputStream
import java.io.File
fun main() {
println("=== Executable JARs ===")
println()
println("Manifest Main-Class dan execution flow java -jar.")
println()
// === 1. Anatomi executable JAR ===
println("=== 1. Anatomi Executable JAR ===")
val parts = listOf(
Triple("META-INF/MANIFEST.MF", "Metadata", "Main-Class, Class-Path, Implementation-Version"),
Triple("MainKt.class", "Class aplikasi", "Berisi bytecode main()"),
Triple("kotlin/stdlib/**", "Runtime Kotlin", "Disertakan bila -include-runtime"),
Triple("org/springframework/boot/**", "Loader Spring Boot", "Hanya untuk bootJar"),
Triple("BOOT-INF/classes/**", "Class aplikasi Spring", "Struktur nested"),
Triple("BOOT-INF/lib/**", "Dependency Spring", "JAR terpisah di dalam JAR")
)
parts.forEach { (path, peran, catatan) ->
println(" " + path.padEnd(35) + " | " + peran.padEnd(22) + " | " + catatan)
}
println()
// === 2. Buat executable JAR minimal di memori ===
println("=== 2. Buat Executable JAR Minimal ===")
val baos = ByteArrayOutputStream()
val manifest = Manifest().apply {
mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0"
mainAttributes[Attributes.Name.MAIN_CLASS] = "MainKt"
mainAttributes[Attributes.Name.CLASS_PATH] = "lib/kotlin-stdlib.jar"
}
JarOutputStream(baos, manifest).use { jos ->
val entry = JarEntry("MainKt.class")
jos.putNextEntry(entry)
jos.write(byteArrayOf(0xCA.toByte(), 0xFE.toByte(), 0xBA.toByte(), 0xBE.toByte()))
jos.closeEntry()
}
val jarBytes = baos.toByteArray()
println(" Ukuran JAR: ${jarBytes.size} byte")
println()
// === 3. Verifikasi Main-Class ===
println("=== 3. Verifikasi Main-Class ===")
val tmpFile = File.createTempFile("exec-", ".jar").apply {
writeBytes(jarBytes); deleteOnExit()
}
JarFile(tmpFile).use { jar ->
val main = jar.manifest.mainAttributes.getValue(Attributes.Name.MAIN_CLASS)
val classPath = jar.manifest.mainAttributes.getValue(Attributes.Name.CLASS_PATH)
println(" Main-Class: $main")
println(" Class-Path: $classPath")
println(" Entries: ${jar.entries().asSequence().toList().map { it.name }}")
}
tmpFile.delete()
println()
// === 4. Execution flow java -jar ===
println("=== 4. Execution Flow java -jar app.jar ===")
val flow = listOf(
"1. JVM membaca META-INF/MANIFEST.MF",
"2. Mencari atribut Main-Class (wajib untuk executable)",
"3. JVM load class Main-Class via bootstrap classloader",
"4. Memanggil method main(String[] args) class tersebut",
"5. Class-Path (opsional) menambah dependency ke classpath",
"6. Bila Spring Boot: JarLauncher memuat BOOT-INF/lib/*.jar"
)
flow.forEach { println(" " + it) }
println()
// === 5. Perintah java -jar ===
println("=== 5. Perintah Eksekusi ===")
val cmds = listOf(
Pair("java -jar app.jar", "Run executable JAR default"),
Pair("java -jar app.jar arg1 arg2", "Run dengan argumen command-line"),
Pair("java -Dserver.port=8080 -jar app.jar", "Set system property sebelum -jar"),
Pair("java -Xmx512m -jar app.jar", "Set max heap 512 MB"),
Pair("java -cp app.jar:lib/* MainKt", "Run via classpath (non-executable)")
)
cmds.forEach { (cmd, desc) ->
println(" " + cmd.padEnd(45) + " -> " + desc)
}
println()
// === 6. Jebakan umum ===
println("=== 6. Jebakan Umum ===")
val gotchas = listOf(
"no main manifest attribute -> Main-Class belum di-set",
"Could not find or load main class -> salah nama class di manifest",
"java -jar mengabaikan -cp; pakai -D untuk properti",
"Spring Boot nested JAR butuh JarLauncher, bukan java -cp"
)
gotchas.forEach { println(" " + it) }
}Output yang Dihasilkan:
=== Executable JARs ===
Manifest Main-Class dan execution flow java -jar.
=== 1. Anatomi Executable JAR ===
META-INF/MANIFEST.MF | Metadata | Main-Class, Class-Path, Implementation-Version
MainKt.class | Class aplikasi | Berisi bytecode main()
kotlin/stdlib/** | Runtime Kotlin | Disertakan bila -include-runtime
org/springframework/boot/** | Loader Spring Boot | Hanya untuk bootJar
BOOT-INF/classes/** | Class aplikasi Spring | Struktur nested
BOOT-INF/lib/** | Dependency Spring | JAR terpisah di dalam JAR
=== 2. Buat Executable JAR Minimal ===
Ukuran JAR: 358 byte
=== 3. Verifikasi Main-Class ===
Main-Class: MainKt
Class-Path: lib/kotlin-stdlib.jar
Entries: [META-INF/MANIFEST.MF, MainKt.class]
=== 4. Execution Flow java -jar app.jar ===
1. JVM membaca META-INF/MANIFEST.MF
2. Mencari atribut Main-Class (wajib untuk executable)
3. JVM load class Main-Class via bootstrap classloader
4. Memanggil method main(String[] args) class tersebut
5. Class-Path (opsional) menambah dependency ke classpath
6. Bila Spring Boot: JarLauncher memuat BOOT-INF/lib/*.jar
=== 5. Perintah Eksekusi ===
java -jar app.jar -> Run executable JAR default
java -jar app.jar arg1 arg2 -> Run dengan argumen command-line
java -Dserver.port=8080 -jar app.jar -> Set system property sebelum -jar
java -Xmx512m -jar app.jar -> Set max heap 512 MB
java -cp app.jar:lib/* MainKt -> Run via classpath (non-executable)
=== 6. Jebakan Umum ===
no main manifest attribute -> Main-Class belum di-set
Could not find or load main class -> salah nama class di manifest
java -jar mengabaikan -cp; pakai -D untuk properti
Spring Boot nested JAR butuh JarLauncher, bukan java -cpMari kita bedah demo di atas:
- Anatomi: Baris pertama menampilkan enam komponen JAR. Standar JAR hanya punya tiga baris pertama. Spring Boot menambahkan
org/springframework/boot/**(loader),BOOT-INF/classes/**(aplikasi), danBOOT-INF/lib/**(dependency sebagai JAR dalam JAR). - Buat JAR Minimal: Kode membuat manifest dengan dua atribut:
Main-Class: MainKt(titik masuk) danClass-Path: lib/kotlin-stdlib.jar(dependency eksternal). EntryMainKt.classdiisi magic number 0xCAFEBABE — header valid Java class file. - Verifikasi:
JarFile.manifest.mainAttributes.getValue(MAIN_CLASS)mengembalikan"MainKt". ListEntriesmenunjukkan JAR hanya berisiMETA-INF/MANIFEST.MFdanMainKt.class— tidak ada nested JAR karena ini bukan Spring Boot. - Execution Flow: Saat
java -jar app.jar, JVM membaca manifest, mencariMain-Class, load class tersebut, lalu memanggil methodmain(String[]). Bila adaClass-Path, dependency ditambahkan ke application classloader. - Perintah:
-Duntuk system property,-Xuntuk JVM option (heap, GC). Perhatikan:java -jarmengabaikan-cp— semua classpath harus dari manifest atauClass-Path.
Spring Boot Loader #
Spring Boot mengemas aplikasi sebagai nested JAR — class aplikasi di BOOT-INF/classes/, dependency di BOOT-INF/lib/*.jar. JVM standar tidak bisa memuat JAR dari dalam JAR, sehingga Spring Boot menyertakan org.springframework.boot.loader.JarLauncher sebagai Main-Class utama. Launcher ini memuat class aplikasi dan dependency via custom classloader yang membaca nested JAR secara langsung dari file JAR luar.
app.jar
├── META-INF/
│ └── MANIFEST.MF # Main-Class: org.springframework.boot.loader.JarLauncher
├── org/springframework/boot/loader/
│ ├── JarLauncher.class # Entry point yang memuat nested JAR
│ └── LaunchedURLClassLoader.class
├── BOOT-INF/
│ ├── classes/ # Class aplikasi Anda
│ │ └── com/example/MainKt.class
│ └── lib/ # Dependency sebagai JAR terpisah
│ ├── spring-boot-3.2.0.jar
│ └── kotlinx-coroutines-core-1.8.0.jarClass-Path dan Classpath Manifest #
Atribut Class-Path di manifest menambahkan JAR eksternal ke classpath saat java -jar. Berguna untuk thin JAR deployment di mana dependency disimpan terpisah:
Manifest-Version: 1.0
Main-Class: MainKt
Class-Path: lib/kotlin-stdlib-1.9.0.jar lib/kotlinx-coroutines-core-1.8.0.jarDengan manifest di atas, java -jar app.jar otomatis memuat lib/*.jar relatif terhadap lokasi app.jar. Trade-off: dependency harus dikelola terpisah, tetapi update dependency tidak memerlukan rebuild JAR aplikasi.
Jebakan Umum #
no main manifest attribute:Main-Classbelum di-set di manifest. Solusi: tambahkan atribut saat packaging.Could not find or load main class: nama class salah (mis.MainvsMainKt— Kotlin menambah suffixKt).-cpdiabaikan:java -jarselalu mengabaikan-cp. Gunakan-Duntuk property atau pakaijava -cpalih-alih-jar.- Missing
mergeServiceFiles: bila menggunakan Shadow plugin tanpamergeServiceFiles(), JDBC driver dan SLF4J binding tidak terdeteksi.
Referensi Lanjutan #
- Kotlin command-line
— panduan resmi
kotlinc -include-runtimeuntuk menghasilkan executable JAR - Gradle
— konfigurasi Gradle
jardanshadowJaruntuk aplikasi Kotlin
Selanjutnya #
Executable JAR menghasilkan artefak yang siap dijalankan di JVM. Namun, untuk distribusi tanpa JVM terinstal di mesin target, kita perlu native binary — topik yang dibahas pada chapter Native Binaries dengan Kotlin/Native.