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.
| Tipe | Isi | Cara jalan |
|---|---|---|
| Thin JAR | Class aplikasi saja | Classpath eksplisit |
| Fat JAR | Class + semua dependency digabung | java -jar app.jar |
| Executable JAR | Fat JAR + manifest Main-Class | Langsung java -jar |
| Spring Boot JAR | Fat JAR dengan loader khusus | java -jar app.jar |
| Module JAR | Modular dengan module-info | Java 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.
// 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:
=== 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 entriesMari kita bedah demo di atas:
- Tipe Artefak: Thin JAR cocok untuk library; Fat JAR cocok untuk aplikasi standalone; Spring Boot JAR punya struktur khusus dengan
BOOT-INF/classesdanBOOT-INF/lib. - Demo Buat JAR: Kode membuat
ManifestdenganMain-Class: MainKt,Implementation-Title: demo-app, danImplementation-Version: 1.0.0.JarOutputStreammenulis entryMainKt.classdengan magic number 0xCAFEBABE (header bytecode Java). Hasilnya: 364 byte JAR. - Inspect Manifest:
JarFile.manifestmembaca metadata kembali. Atribut yang ditulis muncul persis seperti yang dimasukkan. AtributImplementation-TitledanImplementation-Versionberguna untuk pelacakan versi di production. - Perintah Populer:
kotlinc -include-runtimepaling sederhana untuk satu file. Shadow plugin (Gradle) menangani fat JAR dengan resolusi konflik dependency.bootJarSpring Boot menghasilkan JAR denganorg.springframework.boot.loader.JarLaunchersebagaiMain-Classutama.
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.
kotlinc src/main/kotlin/App.kt -include-runtime -d app.jar
java -jar app.jarGradle 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:
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.