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.

kotlin
// 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:

text
=== 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 -cp

Mari kita bedah demo di atas:

  1. 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), dan BOOT-INF/lib/** (dependency sebagai JAR dalam JAR).
  2. Buat JAR Minimal: Kode membuat manifest dengan dua atribut: Main-Class: MainKt (titik masuk) dan Class-Path: lib/kotlin-stdlib.jar (dependency eksternal). Entry MainKt.class diisi magic number 0xCAFEBABE — header valid Java class file.
  3. Verifikasi: JarFile.manifest.mainAttributes.getValue(MAIN_CLASS) mengembalikan "MainKt". List Entries menunjukkan JAR hanya berisi META-INF/MANIFEST.MF dan MainKt.class — tidak ada nested JAR karena ini bukan Spring Boot.
  4. Execution Flow: Saat java -jar app.jar, JVM membaca manifest, mencari Main-Class, load class tersebut, lalu memanggil method main(String[]). Bila ada Class-Path, dependency ditambahkan ke application classloader.
  5. Perintah: -D untuk system property, -X untuk JVM option (heap, GC). Perhatikan: java -jar mengabaikan -cp — semua classpath harus dari manifest atau Class-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.

kotlin Struktur Spring Boot JAR
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.jar

Class-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:

text META-INF/MANIFEST.MF
Manifest-Version: 1.0
Main-Class: MainKt
Class-Path: lib/kotlin-stdlib-1.9.0.jar lib/kotlinx-coroutines-core-1.8.0.jar

Dengan 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 #

  1. no main manifest attribute: Main-Class belum di-set di manifest. Solusi: tambahkan atribut saat packaging.
  2. Could not find or load main class: nama class salah (mis. Main vs MainKt — Kotlin menambah suffix Kt).
  3. -cp diabaikan: java -jar selalu mengabaikan -cp. Gunakan -D untuk property atau pakai java -cp alih-alih -jar.
  4. Missing mergeServiceFiles: bila menggunakan Shadow plugin tanpa mergeServiceFiles(), JDBC driver dan SLF4J binding tidak terdeteksi.

Referensi Lanjutan #

  • Kotlin command-line — panduan resmi kotlinc -include-runtime untuk menghasilkan executable JAR
  • Gradle — konfigurasi Gradle jar dan shadowJar untuk 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.