Setelah kode Kotlin ditulis dan diuji, langkah berikutnya adalah memaketkannya menjadi artefak yang dapat dijalankan di mesin target. JVM memiliki beberapa format JAR dengan karakteristik berbeda: thin JAR ringkas tetapi butuh classpath panjang, fat JAR bisa langsung dijalankan dengan java -jar, dan module JAR mendukung JPMS untuk isolasi modul. Pada chapter ini kita akan membedah tipe artefak JAR, mendemokan pembuatan executable JAR via manifest, serta memetakan perintah packaging populer — dari kotlinc -include-runtime hingga Gradle Shadow plugin dan Spring Boot bootJar.

Tipe Artefak JAR #

JAR (Java ARchive) adalah file ZIP dengan struktur khusus: file class, resource, dan META-INF/MANIFEST.MF. Tipe JAR menentukan cara aplikasi dijalankan dan bagaimana dependency diselesaikan.

TipeIsiCara jalan
Thin JARClass aplikasi sajaClasspath eksplisit
Fat JARClass + semua dependency digabungjava -jar app.jar
Executable JARFat JAR + manifest Main-ClassLangsung java -jar
Spring Boot JARFat JAR dengan loader khususjava -jar app.jar
Module JARModular dengan module-infoJava 9+ module path

Fat JAR adalah pilihan paling populer untuk deploy karena satu file sudah berisi semua kebutuhan. Trade-off: ukuran file besar (10-50 MB) dan update dependency memerlukan rebuild penuh. Untuk container Docker, thin JAR + dependency di layer terpisah sering lebih efisien karena caching layer.

Manifest dan Main-Class #

Manifest adalah file META-INF/MANIFEST.MF di dalam JAR yang menyimpan metadata. Atribut paling penting untuk executable JAR adalah Main-Class — kelas yang berisi main(). Tanpa atribut ini, java -jar app.jar menolak menjalankan.

kotlin
// packaging_jvm_applications.kt — Demo packaging JAR dan struktur manifest

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("=== Packaging JVM Applications ===")
    println()
    println("Strategi memaketkan aplikasi Kotlin menjadi artefak siap deploy.")
    println()

    // === 1. Tipe JAR ===
    println("=== 1. Tipe Artefak JAR ===")
    val jarTypes = listOf(
        Triple("Thin JAR", "Hanya class aplikasi; dependency terpisah", "Classpath panjang saat run"),
        Triple("Fat JAR / Uber JAR", "Class aplikasi + semua dependency digabung", "java -jar app.jar"),
        Triple("Executable JAR", "Fat JAR + manifest Main-Class", "Bisa langsung java -jar"),
        Triple("Spring Boot JAR", "Fat JAR dengan loader khusus", "java -jar app.jar"),
        Triple("Module JAR (JPMS)", "Modular JAR dengan module-info", "Java 9+ module path")
    )
    jarTypes.forEach { (tipe, isi, eksekusi) ->
        println("  " + tipe.padEnd(25) + " | " + isi.padEnd(45) + " | " + eksekusi)
    }
    println()

    // === 2. Membuat JAR di memori (demo, bukan file) ===
    println("=== 2. Demo Buat Executable JAR (in-memory) ===")
    val baos = ByteArrayOutputStream()
    val manifest = Manifest().apply {
        mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0"
        mainAttributes[Attributes.Name.MAIN_CLASS] = "MainKt"
        mainAttributes[Attributes.Name.IMPLEMENTATION_TITLE] = "demo-app"
        mainAttributes[Attributes.Name.IMPLEMENTATION_VERSION] = "1.0.0"
    }

    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("  JAR dibuat di memori (demo.jar)")
    println("  Ukuran: ${jarBytes.size} byte")
    println()

    // === 3. Inspect manifest ===
    println("=== 3. Inspect Manifest ===")
    val tmpFile = File.createTempFile("demo-", ".jar").apply {
        writeBytes(jarBytes)
        deleteOnExit()
    }
    JarFile(tmpFile).use { jar ->
        val mf = jar.manifest
        mf.mainAttributes.forEach { (k, v) ->
            println("  $k: $v")
        }
    }
    tmpFile.delete()
    println()

    // === 4. Perintah packaging populer ===
    println("=== 4. Perintah Packaging Populer ===")
    val commands = listOf(
        Pair("kotlinc langsung", Pair("kotlinc main.kt -include-runtime -d app.jar", "Fat JAR dengan runtime Kotlin")),
        Pair("Shadow plugin", Pair("./gradlew shadowJar", "Fat JAR dengan resolusi konflik")),
        Pair("Spring Boot", Pair("./gradlew bootJar", "Executable JAR dengan loader")),
        Pair("Maven Assembly", Pair("mvn assembly:single", "Fat JAR via plugin assembly")),
        Pair("jlink", Pair("jlink --module-path ... --output image", "Custom runtime tanpa JVM penuh"))
    )
    commands.forEach { (tool, pair) ->
        val (cmd, desc) = pair
        println("  " + tool.padEnd(20) + " | " + cmd)
        println("  " + "".padEnd(20) + " | -> " + desc)
    }
    println()

    // === 5. Aturan packaging ===
    println("=== 5. Aturan Packaging ===")
    val rules = listOf(
        "1. Fat JAR untuk deploy sederhana (java -jar app.jar)",
        "2. Thin JAR + classpath di Docker untuk image kecil",
        "3. Spring Boot -> bootJar; Ktor -> Shadow plugin",
        "4. Set Main-Class di manifest untuk executable JAR",
        "5. Pakai jlink untuk image minimal tanpa JVM penuh",
        "6. Reproducible build: set timestamp fixed, sort entries"
    )
    rules.forEach { println("  " + it) }
}

Output yang Dihasilkan:

text
=== Packaging JVM Applications ===

Strategi memaketkan aplikasi Kotlin menjadi artefak siap deploy.

=== 1. Tipe Artefak JAR ===
  Thin JAR                  | Hanya class aplikasi; dependency terpisah     | Classpath panjang saat run
  Fat JAR / Uber JAR        | Class aplikasi + semua dependency digabung    | java -jar app.jar
  Executable JAR            | Fat JAR + manifest Main-Class                 | Bisa langsung java -jar
  Spring Boot JAR           | Fat JAR dengan loader khusus                  | java -jar app.jar
  Module JAR (JPMS)         | Modular JAR dengan module-info                | Java 9+ module path

=== 2. Demo Buat Executable JAR (in-memory) ===
  JAR dibuat di memori (demo.jar)
  Ukuran: 364 byte

=== 3. Inspect Manifest ===
  Manifest-Version: 1.0
  Main-Class: MainKt
  Implementation-Title: demo-app
  Implementation-Version: 1.0.0

=== 4. Perintah Packaging Populer ===
  kotlinc langsung     | kotlinc main.kt -include-runtime -d app.jar
                       | -> Fat JAR dengan runtime Kotlin
  Shadow plugin        | ./gradlew shadowJar
                       | -> Fat JAR dengan resolusi konflik
  Spring Boot          | ./gradlew bootJar
                       | -> Executable JAR dengan loader
  Maven Assembly       | mvn assembly:single
                       | -> Fat JAR via plugin assembly
  jlink                | jlink --module-path ... --output image
                       | -> Custom runtime tanpa JVM penuh

=== 5. Aturan Packaging ===
  1. Fat JAR untuk deploy sederhana (java -jar app.jar)
  2. Thin JAR + classpath di Docker untuk image kecil
  3. Spring Boot -> bootJar; Ktor -> Shadow plugin
  4. Set Main-Class di manifest untuk executable JAR
  5. Pakai jlink untuk image minimal tanpa JVM penuh
  6. Reproducible build: set timestamp fixed, sort entries

Mari kita bedah demo di atas:

  1. Tipe Artefak: Thin JAR cocok untuk library; Fat JAR cocok untuk aplikasi standalone; Spring Boot JAR punya struktur khusus dengan BOOT-INF/classes dan BOOT-INF/lib.
  2. Demo Buat JAR: Kode membuat Manifest dengan Main-Class: MainKt, Implementation-Title: demo-app, dan Implementation-Version: 1.0.0. JarOutputStream menulis entry MainKt.class dengan magic number 0xCAFEBABE (header bytecode Java). Hasilnya: 364 byte JAR.
  3. Inspect Manifest: JarFile.manifest membaca metadata kembali. Atribut yang ditulis muncul persis seperti yang dimasukkan. Atribut Implementation-Title dan Implementation-Version berguna untuk pelacakan versi di production.
  4. Perintah Populer: kotlinc -include-runtime paling sederhana untuk satu file. Shadow plugin (Gradle) menangani fat JAR dengan resolusi konflik dependency. bootJar Spring Boot menghasilkan JAR dengan org.springframework.boot.loader.JarLauncher sebagai Main-Class utama.

kotlinc dan -include-runtime #

kotlinc main.kt -include-runtime -d app.jar menghasilkan fat JAR berisi class aplikasi plus runtime Kotlin standar (~1-2 MB). Cocok untuk CLI tool atau prototype cepat. Untuk aplikasi yang punya banyak dependency eksternal (mis. Ktor, Retrofit), gunakan Gradle Shadow plugin karena kotlinc tidak menyelesaikan dependency Maven.

bash kotlinc -include-runtime
kotlinc src/main/kotlin/App.kt -include-runtime -d app.jar
java -jar app.jar

Gradle Shadow Plugin #

Shadow plugin menggabungkan dependency Gradle menjadi satu fat JAR dengan resolusi konflik otomatis (mis. dua library yang sama-sama membawa guava versi berbeda). Konfigurasi:

kotlin build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.21"
    id("com.github.johnrengelman.shadow") version "8.1.1"
    application
}

application {
    mainClass.set("com.example.MainKt")
}

tasks.shadowJar {
    archiveBaseName.set("app")
    archiveClassifier.set("")
    mergeServiceFiles() // penting untuk ServiceLoader (mis. JDBC driver)
}

Build dengan ./gradlew shadowJar, hasil di build/libs/app.jar.

Spring Boot dan Reproducible Build #

Spring Boot bootJar menghasilkan JAR dengan struktur nested — class aplikasi di BOOT-INF/classes, dependency di BOOT-INF/lib, dan JarLauncher sebagai entry point. Spring Boot loader menangani classpath di sini. Untuk reproducible build (artefak identik antar mesin), set preserveFileTimestamps = false dan reproducibleFileOrder = true di konfigurasi.

Referensi Lanjutan #

  • Kotlin command-line — panduan resmi kompilasi dan packaging Kotlin dari command line
  • Gradle — penggunaan Gradle untuk packaging aplikasi Kotlin

Selanjutnya #

Setelah memahami tipe JAR, kita perlu memastikan JAR dapat dieksekusi dengan benar — topik yang dibahas pada chapter Executable JARs, mencakup manifest Main-Class, java -jar execution flow, dan Spring Boot loader.