Anotasi yang kita pasang pada kode perlu dibaca oleh sesuatu agar bermakna. Ada tiga jalur utama untuk memproses anotasi di Kotlin: KSP (Kotlin Symbol Processing) saat kompilasi, KAPT (Kotlin Annotation Processing Tool) sebagai jembatan ke annotation processor Java, dan reflection saat runtime. Pada chapter ini kita akan membandingkan ketiganya, melihat kapan masing-masing cocok dipakai, dan mendemonstrasikan pemrosesan reflection sederhana.

Perbandingan Tiga Jalur #

JalurSaat berjalanOutputKecepatanCocok untuk
KSPKompilasiFile Kotlin/Java baruCepat (2-3x KAPT)Library baru: Room, Moshi, Hilt.
KAPTKompilasiStub Java + file hasil processorLambatLibrary lama: Dagger, Realm versi lama.
ReflectionRuntimeTidak ada; langsung eksekusiPaling lambatRouter web, DI ringan, konfigurasi dinamis.

Karakteristik KSP #

KSP adalah API resmi Kotlin untuk pemrosesan simbol saat kompilasi. KSP membaca source code Kotlin sebagai simbol (class, function, property) dan menjalankan processor yang kita tulis. Output processor dapat berupa file Kotlin baru — misalnya implementasi serializer untuk @Serializable class Pengguna. KSP tidak bergantung pada Java reflection sehingga mendukung semua target Kotlin (JVM, JS, Native).

Keuntungan utama KSP:

  • Lebih cepat 2-3x dibanding KAPT karena tidak perlu men-generate stub Java.
  • Mendukung Kotlin Multiplatform.
  • API stabil dan resmi didukung JetBrains.

Karakteristik KAPT #

KAPT adalah jembatan agar annotation processor Java (yang memakai javax.annotation.processing) dapat berjalan di atas kode Kotlin. Caranya: KAPT men-generate stub Java dari setiap file Kotlin, lalu menjalankan processor Java, lalu menggabungkan hasilnya ke output build.

KAPT hanya cocok untuk library Java lama yang belum memiliki dukungan KSP. Saat library pilihan sudah menyediakan processor KSP (mis. Room sejak versi 2.4+), bermigrasilah dari KAPT ke KSP.

Pemrosesan Reflection #

Reflection adalah cara termudah untuk memproses anotasi — tidak perlu menulis processor terpisah. Kita langsung membaca annotations pada KClass atau KFunction. Cocok untuk pola dinamis seperti router web yang memetakan anotasi @Endpoint ke handler.

kotlin
// File: annotation_processing.kt
// Mendemonstrasikan perbandingan jalur pemrosesan anotasi:
// compile-time (KSP), runtime (reflection), dan KAPT (legacy).
package com.example.proses

// == Anotasi yang dibaca saat runtime via reflection ==
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class Endpoint(val path: String, val method: String = "GET")

@Endpoint("/api/pengguna")
class ApiPengguna {
    @Endpoint("/api/pengguna/{id}", method = "GET")
    fun ambil(id: Long): String = "Pengguna#$id"

    @Endpoint("/api/pengguna", method = "POST")
    fun buat(nama: String): String = "Pengguna baru $nama"
}

// == Pemroses runtime sederhana ==
object PemindaiEndpoint {
    fun pindai(kelas: kotlin.reflect.KClass<*>): List<Triple<String, String, String>> {
        val hasil = mutableListOf<Triple<String, String, String>>()
        val kelasPath = kelas.annotations.filterIsInstance<Endpoint>().firstOrNull()?.path ?: ""
        if (kelasPath.isNotEmpty()) {
            hasil.add(Triple(kelasPath, "CLASS", kelas.simpleName ?: "?"))
        }
        for (fn in kelas.java.declaredMethods) {
            val ep = fn.annotations.filterIsInstance<Endpoint>().firstOrNull()
            if (ep != null) {
                hasil.add(Triple(ep.path, ep.method, fn.name))
            }
        }
        return hasil
    }
}

fun main() {
    val pemindaian = PemindaiEndpoint.pindai(ApiPengguna::class)
    println("Hasil pemindaian endpoint:")
    for ((path, method, name) in pemindaian) {
        println("  $method $path -> $name")
    }

    println()
    println("Perbandingan jalur pemrosesan anotasi:")
    println("  KSP (Kotlin Symbol Processing):")
    println("    - Berjalan saat kompilasi (sama seperti annotation processor Java).")
    println("    - Menghasilkan kode Kotlin/Java baru; tidak memengaruhi runtime.")
    println("    - Lebih cepat 2-3x dibanding KAPT; direkomendasikan untuk library baru.")
    println("  KAPT (Kotlin Annotation Processing Tool):")
    println("    - Jembatan ke annotation processor Java (mis. Dagger, Room versi lama).")
    println("    - Lambat karena perlu men-generate stub Java dari Kotlin.")
    println("    - Tidak direkomendasikan untuk proyek baru; pilih KSP.")
    println("  Reflection (Runtime):")
    println("    - Membaca anotasi saat program berjalan.")
    println("    - Cocok untuk pola dinamis: router, DI ringan, konfigurasi.")
    println("    - Lebih lambat daripada KSP karena tidak ada kode yang di-generate.")
}

Output yang Dihasilkan:

text
Hasil pemindaian endpoint:
  CLASS /api/pengguna -> ApiPengguna
  GET /api/pengguna/{id} -> ambil
  POST /api/pengguna -> buat

Perbandingan jalur pemrosesan anotasi:
  KSP (Kotlin Symbol Processing):
    - Berjalan saat kompilasi (sama seperti annotation processor Java).
    - Menghasilkan kode Kotlin/Java baru; tidak memengaruhi runtime.
    - Lebih cepat 2-3x dibanding KAPT; direkomendasikan untuk library baru.
  KAPT (Kotlin Annotation Processing Tool):
    - Jembatan ke annotation processor Java (mis. Dagger, Room versi lama).
    - Lambat karena perlu men-generate stub Java dari Kotlin.
    - Tidak direkomendasikan untuk proyek baru; pilih KSP.
  Reflection (Runtime):
    - Membaca anotasi saat program berjalan.
    - Cocok untuk pola dinamis: router, DI ringan, konfigurasi.
    - Lebih lambat daripada KSP karena tidak ada kode yang di-generate.

Mari kita amati bagian-bagian penting di atas:

  1. Baris 7 (@Retention(AnnotationRetention.RUNTIME)): retention wajib RUNTIME agar anotasi dapat dibaca reflection. Tanpa retention ini, anotasi hilang dari bytecode.
  2. Baris 13 (@Endpoint("/api/pengguna") class ApiPengguna): anotasi class-level yang menandai base path. Sering muncul pada framework web seperti Ktor atau Spring.
  3. Baris 15-16 (@Endpoint("/api/pengguna/{id}", method = "GET") fun ambil): anotasi function-level. Setiap function menjadi endpoint tersendiri.
  4. Baris 25 (kelas.annotations.filterIsInstance<Endpoint>()): membaca anotasi class-level. Bila ada, ditambahkan ke hasil sebagai entri khusus dengan tanda “CLASS”.
  5. Baris 30 (kelas.java.declaredMethods): mengakses metode-metode pada class via Java reflection. Kotlin reflection memberFunctions juga bisa dipakai, tetapi java.declaredMethods lebih sederhana untuk membaca anotasi parameter.
  6. Baris 31 (fn.annotations.filterIsInstance<Endpoint>()): membaca anotasi pada setiap method. Hasilnya dipetakan ke triple (path, method, name).
  7. Baris 45-58 (output perbandingan): tiga paragraf ringkas yang membandingkan karakteristik KSP, KAPT, dan reflection.

Memilih Jalur #

Panduan praktis memilih jalur pemrosesan:

  • KSP: bila library menyediakan processor KSP (Room, Moshi, Hilt). Selalu pilih ini bila tersedia.
  • KAPT: bila library hanya punya annotation processor Java (mis. Dagger versi lama). Migrasi ke KSP saat tersedia.
  • Reflection: bila anotasi bersifat dinamis (router, konfigurasi runtime). Hindari di hot path.

Referensi Lanjutan #

  • Annotations — konteks annotation processing dan KSP pada panduan anotasi resmi

Selanjutnya #

Setelah memahami jalur pemrosesan, kita akan mendalami reflection itu sendiri. Chapter Kotlin Reflection akan membahas dasar-dasar kotlin.reflect: KClass, KFunction, dan KProperty.