Mendefinisikan annotation kustom ternyata belum cukup untuk membuat annotation tersebut berguna saat program berjalan. Ada satu kejutan yang hampir selalu menjumpai pemula: annotation kustom yang dideklarasikan polos — tanpa @Retention — tidak dapat dibaca oleh reflection, dan getAnnotation() diam-diam mengembalikan null. Penyebabnya bukan bug, melainkan kebijakan retensi bawaan Java yang memilih menyimpan annotation di berkas .class tanpa memuatnya ke JVM. Pada Chapter ini kita bedah tiga kebijakan RetentionPolicy — SOURCE, CLASS, dan RUNTIME — lalu membuktikan kontrasnya dengan program yang dapat dijalankan.

Tiga Kebijakan Retensi #

Retensi menentukan sampai tahap mana sebuah annotation bertahan hidup dalam alur kompilasi dan eksekusi. Meta-annotation @Retention dipasang pada deklarasi annotation type, dengan salah satu dari tiga nilai enum RetentionPolicy:

KebijakanDi kode sumberDi berkas .classDimuat JVMTerbaca reflection
SOURCEyatidak — dibuang javactidaktidak
CLASS (bawaan)yayatidaktidak
RUNTIMEyayayaya

Kebijakan SOURCE dipilih bila annotation hanya berarti bagi compiler — setelah javac selesai memeriksa, informasinya tidak dibutuhkan lagi. Annotation bawaan @Override dan @SuppressWarnings memakai kebijakan ini. Kebijakan CLASS adalah bawaan ketika @Retention tidak ditulis: compiler menyimpan annotation di berkas .class sebagaimana adanya, tetapi JVM memilih untuk tidak mengeksposnya, sehingga reflection tetap tidak melihatnya. Hanya kebijakan RUNTIME yang membuat annotation sampai ke JVM dalam bentuk yang bisa dibaca getAnnotation() — inilah kebijakan yang dipakai @Deprecated dan hampir semua annotation framework.

Alur lengkapnya dapat digambarkan begini:

Demo Kontras Dua Retensi #

Mari kita buktikan tabel di atas dengan dua annotation kustom yang identik kecuali retensinya. TersimpanRuntime diberi @Retention(RetentionPolicy.RUNTIME), sedangkan TanpaRetention dideklarasikan polos tanpa meta-annotation apa pun. Keduanya dipasang pada class Pengaturan, lalu program mencoba membaca keduanya lewat reflection:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@interface TersimpanRuntime {}

@interface TanpaRetention {} // tanpa @Retention: kebijakan bawaan CLASS

@TersimpanRuntime
@TanpaRetention
class Pengaturan {}

public class KontrasRetensi {
    public static void main(String[] args) {
        System.out.println("TersimpanRuntime terbaca? "
                + (Pengaturan.class.getAnnotation(TersimpanRuntime.class) != null));
        System.out.println("TanpaRetention terbaca?  "
                + (Pengaturan.class.getAnnotation(TanpaRetention.class) != null));

        Retention r = Override.class.getAnnotation(Retention.class);
        Retention d = Deprecated.class.getAnnotation(Retention.class);
        System.out.println("Retention @Override   : " + r.value());
        System.out.println("Retention @Deprecated : " + d.value());
    }
}

Output yang Dihasilkan:

text
TersimpanRuntime terbaca? true
TanpaRetention terbaca?  false
Retention @Override   : SOURCE
Retention @Deprecated : RUNTIME

Mari kita telaah baris-baris kode di atas:

  1. Baris 3-4 (@Retention(RetentionPolicy.RUNTIME) di atas TersimpanRuntime): memerintahkan compiler dan JVM agar annotation ini tersedia saat program berjalan. Retention sendiri sengaja ber-retensi RUNTIME sehingga kebijakannya dapat dibaca kembali lewat reflection.
  2. Baris 6 (@interface TanpaRetention {}): tanpa @Retention, Java memakai kebijakan bawaan RetentionPolicy.CLASS. Annotation tetap ditulis ke berkas .class oleh compiler, tetapi JVM tidak memuatnya ke struktur runtime.
  3. Baris 8-11 (class Pengaturan): class yang sama diberi kedua annotation. Pemakaian ganda ini memungkinkan perbandingan yang adil — faktor lain seperti @Target atau letak deklarasi tidak ikut membedakan hasil.
  4. Baris 16-19 (getAnnotation(...)): dua baris pertama mencetak true untuk TersimpanRuntime dan false untuk TanpaRetention. JVM hanya menghidupkan annotation ber-kebijakan RUNTIME pada objek Class.
  5. Baris 21-24 (Override.class.getAnnotation(Retention.class)): membaca meta-annotation dari annotation bawaan Java. Hasilnya SOURCE untuk @Override dan RUNTIME untuk @Deprecated — bukti bahwa pustaka standar sendiri memakai rentang kebijakan yang sama.

Hasil false pada TanpaRetention sering disalahartikan sebagai kegagalan reflection, padahal mekanismenya bekerja sesuai spesifikasi. Tidak ada error, tidak ada peringatan compiler — annotation hanya tidak pernah tiba di sisi runtime.

Kapan Memilih Tiap Kebijakan #

Pemilihan retensi sebaiknya mengikuti pembaca annotation. Annotation yang dibaca compiler saja — validasi khusus saat build, penandaan experimental — cukup dengan SOURCE karena menyimpannya lebih lanjut hanya memperbesar berkas .class tanpa pembaca. Annotation yang dibaca alat pemroses bytecode — misalnya alat analisis yang memeriksa berkas .class langsung tanpa menjalankan JVM — dapat memakai CLASS. Annotation untuk framework dan library yang memeriksa class saat program berjalan, seperti pemetaan ORM atau routing web, mutlak memakai RUNTIME.

Perlu dicatat bahwa retensi hanyalah satu dari beberapa meta-annotation pembentuk perilaku. @Documented membuat annotation ikut muncul di dokumentasi javadoc, @Inherited membuat annotation pada superclass ikut terbaca pada subclass, dan @Repeatable mengizinkan annotation yang sama dipasang berkali-kali pada satu deklarasi. Retensi tetap yang paling menentukan: tanpa RUNTIME, meta-annotation lain tidak berarti apa pun di sisi reflection.

Dengan retensi tuntas, satu pertanyaan berikutnya muncul: bolehkah annotation dipasang di sembarang tempat? Java menjawab dengan pembatasan @Target — annotation untuk method yang terpasang di class harus ditolak compiler sejak awal. Chapter berikutnya membahas ElementType dan cara membatasi lokasi pemasangan annotation.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

KontrasRetensi.java

Unduh berkas
java
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@interface TersimpanRuntime {}

@interface TanpaRetention {} // tanpa @Retention: kebijakan bawaan CLASS

@TersimpanRuntime
@TanpaRetention
class Pengaturan {}

public class KontrasRetensi {
    public static void main(String[] args) {
        System.out.println("TersimpanRuntime terbaca? "
                + (Pengaturan.class.getAnnotation(TersimpanRuntime.class) != null));
        System.out.println("TanpaRetention terbaca?  "
                + (Pengaturan.class.getAnnotation(TanpaRetention.class) != null));

        Retention r = Override.class.getAnnotation(Retention.class);
        Retention d = Deprecated.class.getAnnotation(Retention.class);
        System.out.println("Retention @Override   : " + r.value());
        System.out.println("Retention @Deprecated : " + d.value());
    }
}