Anotasi yang kita buat perlu disimpan di suatu tempat — di kode sumber saja, di bytecode saja, atau tersedia saat runtime untuk dibaca reflection. Pilihan itu diatur oleh meta-annotation @Retention. Pada chapter ini kita akan membedah tiga nilai AnnotationRetention (SOURCE, BINARY, RUNTIME) dan melihat kapan masing-masing cocok dipakai. Pemahaman retention penting agar anotasi yang dibutuhkan reflection tidak hilang, sekaligus agar anotasi yang hanya dipakai kompilator tidak menggembungkan ukuran bytecode.

Tiga Nilai Retention #

RetentionDisimpan diDapat dibaca oleh
SOURCEHanya file sumber.Kompilator dan annotation processor (KSP). Tidak ada di bytecode.
BINARYBytecode (file .class / .klib).Kompilator saat modul lain mengimpor. Tidak terbaca reflection default.
RUNTIMEBytecode + tersedia via reflection.Kompilator dan reflection runtime.

Di Kotlin, retention default adalah RUNTIME — berbeda dari Java yang defaultnya CLASS (setara dengan BINARY di Kotlin). Pemilihan default ini disengaja karena banyak library Kotlin memakai reflection untuk membaca anotasi (router web, DI ringan, serializer).

Contoh Kode #

Kode berikut mendeklarasikan tiga anotasi dengan retention berbeda lalu mencoba membacanya via reflection:

kotlin
// File: annotation_retention.kt
// Mendemonstrasikan retention SOURCE, BINARY, dan RUNTIME.
package com.example.retensi

import kotlin.reflect.full.hasAnnotation

@Retention(AnnotationRetention.SOURCE)
annotation class CatatanSumber(val info: String)

@Retention(AnnotationRetention.BINARY)
annotation class CatatanBiner(val info: String)

@Retention(AnnotationRetention.RUNTIME)
annotation class CatatanRuntime(val info: String)

@CatatanSumber("hanya terlihat di source")
@CatatanBiner("terlihat di bytecode")
@CatatanRuntime("terlihat saat runtime")
class ServicePajak {
    fun hitung(nilai: Long): Long = nilai + (nilai / 10L)
}

fun main() {
    val kelas = ServicePajak::class

    // SOURCE tidak ada di bytecode; tidak bisa dibaca reflection.
    val sumber = kelas.annotations.filterIsInstance<CatatanSumber>().firstOrNull()
    println("@CatatanSumber: ${sumber?.info ?: "(tidak terbaca — hilang dari bytecode)"}")

    // BINARY ada di bytecode tetapi tidak selalu terbaca reflection (tergantung platform).
    // Pada Kotlin/JVM, anotasi BINARY juga bisa terbaca jika target Java mendukung.
    val biner = kelas.annotations.filterIsInstance<CatatanBiner>().firstOrNull()
    println("@CatatanBiner: ${biner?.info ?: "(tidak terbaca via kotlin.reflect)"}")

    // RUNTIME pasti terbaca reflection.
    val runtime = kelas.annotations.filterIsInstance<CatatanRuntime>().firstOrNull()
    println("@CatatanRuntime: ${runtime?.info ?: "(tidak ada)"}")

    // hasAnnotation helper.
    println("ServicePajak hasAnnotation<CatatanRuntime>: ${kelas.hasAnnotation<CatatanRuntime>()}")
}

Output yang Dihasilkan:

text
@CatatanSumber: (tidak terbaca — hilang dari bytecode)
@CatatanBiner: (tidak terbaca via kotlin.reflect)
@CatatanRuntime: terlihat saat runtime
ServicePajak hasAnnotation<CatatanRuntime>: true

Mari kita bedah kode di atas:

  1. Baris 6 (@Retention(AnnotationRetention.SOURCE)): anotasi CatatanSumber hanya ada di file .kt. Setelah kompilasi, anotasi ini hilang dari bytecode. Cocok untuk anotasi yang dipakai annotation processor KSP saat kompilasi.
  2. Baris 9 (@Retention(AnnotationRetention.BINARY)): anotasi CatatanBiner disimpan di bytecode. Kompilator modul lain dapat melihatnya saat mengimpor, tetapi kotlin.reflect default tidak membacanya pada beberapa platform.
  3. Baris 12 (@Retention(AnnotationRetention.RUNTIME)): anotasi CatatanRuntime pasti terbaca via reflection. Inilah default Kotlin bila @Retention tidak ditulis.
  4. Baris 19-22 (@CatatanSumber @CatatanBiner @CatatanRuntime): tiga anotasi ditempel pada class yang sama. Reflection pada main() akan menemukan hanya CatatanRuntime (dan mungkin CatatanBiner pada JVM), tetapi tidak CatatanSumber.
  5. Baris 30 (kelas.annotations.filterIsInstance<CatatanSumber>()): hasilnya null karena retention SOURCE membuat anotasi tidak ada di runtime. Program mencetak pesan default yang menjelaskan keadaan ini.
  6. Baris 41 (kelas.hasAnnotation<CatatanRuntime>()): helper dari kotlin.reflect.full yang lebih ringkas daripada filterIsInstance<...>().isNotEmpty().

Pemilihan Retention #

Pemilihan retention bergantung pada siapa pembaca anotasi:

  • SOURCE: untuk anotasi yang hanya dipakai saat kompilasi. Contoh: @Suppress, @OptIn, anotasi yang diolah KSP. Bytecode tetap ramping.
  • BINARY: untuk anotasi yang dibaca kompilator modul lain, tetapi tidak perlu diakses reflection. Jarang dipakai di praktik Kotlin.
  • RUNTIME: untuk anotasi yang dibaca reflection saat runtime. Contoh: @Endpoint pada router web, @Inject pada DI ringan.

BINARY pada Multiplatform #

Pada Kotlin Multiplatform, retention BINARY berguna untuk anotasi yang dipakai lintas target. Misalnya, anotasi @Serializable dari kotlinx.serialization memakai retention BINARY agar kompilator plugin serializer dapat menemukan anotasi saat memproses kode commonMain dan target-target turunannya.

Referensi Lanjutan #

  • Annotations — bagian @Retention dan AnnotationRetention (SOURCE/BINARY/RUNTIME) pada panduan resmi

Selanjutnya #

Setelah memahami target dan retention, kita akan membahas tipe parameter apa saja yang diperbolehkan dalam anotasi — topik chapter Annotation Parameters.