Anotasi adalah metadata yang ditempelkan pada deklarasi Java, sedangkan reflection adalah kemampuan program memeriksa dan memanipulasi strukturnya sendiri saat berjalan. Keduanya berpasangan erat, karena anotasi menandai anggota tertentu dan reflection membacanya tanpa mengenal anggota itu sebelumnya. Kombinasi inilah yang membuat framework terasa seperti sihir, yaitu metode yang tidak pernah dipanggil secara eksplisit ternyata dijalankan oleh kerangkanya. Topik ini mempraktikkan anotasi bawaan, penulisan anotasi sendiri, serta pembacaan dan pemanggilannya lewat reflection, sampai mini framework pengujian berhasil dirakit dalam satu berkas.
Anotasi bawaan dan retensinya#
Empat anotasi bawaan paling sering ditemui adalah @Override, @Deprecated, @SuppressWarnings, dan @FunctionalInterface. Perbedaan pentingnya ada pada retensi (retention), yaitu seberapa lama anotasi itu hidup. Retensi SOURCE menghilangkan anotasi setelah kompilasi sehingga hanya berguna bagi kompiler, retensi CLASS menyimpannya di berkas .class tetapi tidak memperlihatkannya saat berjalan, dan retensi RUNTIME membuatnya dapat dibaca lewat reflection.
| Anotasi | Tujuannya | Retensi |
|---|---|---|
@Override | memeriksa bahwa metode benar-benar mengganti metode induk | SOURCE |
@Deprecated | menandai anggota yang usang, dengan since dan forRemoval | RUNTIME |
@SuppressWarnings | menekan peringatan kompiler tertentu pada lingkupnya | SOURCE |
@FunctionalInterface | memeriksa bahwa interface hanya memiliki satu metode abstrak | RUNTIME |
Program berikut membaca anotasi lewat reflection dan sekaligus membuktikan perbedaan retensinya.
import java.lang.annotation.Retention;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
public class BacaAnotasi {
public static void main(String[] args) {
Method[] metode = Layanan.class.getDeclaredMethods();
Arrays.sort(metode, Comparator.comparing(Method::getName));
for (Method m : metode) {
if (m.isAnnotationPresent(Deprecated.class)) {
System.out.println("usang : " + m.getName());
} else {
System.out.println("aktif : " + m.getName());
}
}
Retention retensiDeprecated = Deprecated.class.getAnnotation(Retention.class);
System.out.println("Retensi @Deprecated : " + retensiDeprecated.value());
Retention retensiOverride = Override.class.getAnnotation(Retention.class);
System.out.println("Retensi @Override : " + (retensiOverride == null
? "SOURCE, tidak tersimpan di berkas .class" : retensiOverride.value()));
}
}
class Layanan {
void sapa() {
}
@Deprecated
void sapaLama() {
}
}aktif : sapa
usang : sapaLama
Retensi @Deprecated : RUNTIME
Retensi @Override : SOURCEMetode sapaLama terbaca sebagai usang karena @Deprecated berretensi RUNTIME, sedangkan @Override bahkan tidak tersimpan di berkas .class, sehingga reflection tidak pernah bisa membacanya. Urutan metode pada program sengaja diurutkan dengan sort karena getDeclaredMethods tidak menjamin urutan apa pun, dan pengurutan itu membuat keluaran program tetap sama di setiap kali dijalankan.
Menulis anotasi sendiri#
Anotasi baru dideklarasikan dengan @interface, dan isinya berupa elemen yang ditulis seperti metode dengan nilai bawaan yang opsional. Dua meta-anotasi menentukan perilakunya, yaitu @Retention yang menetapkan masa hidupnya dan @Target yang membatasi tempat melekatnya, misalnya ElementType.METHOD untuk metode saja atau ElementType.TYPE untuk kelas dan interface. Anotasi penanda sederhana tidak butuh elemen sama sekali.
// tidak-dijalankan
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Uji {
}Deklarasi ini tidak melakukan apa pun dengan sendirinya, dan di situlah letak kesalahpahaman yang sering muncul. Anotasi hanyalah label yang ditempelkan, sementara seluruh perilakunya lahir dari kode yang membaca label itu. Bila sebuah anotasi perlu membawa nilai, misalnya deskripsi atau prioritas, elemennya ditulis seperti metode, misalnya String deskripsi() default "", dan nilainya dibaca pemakai lewat getAnnotation pada bagian berikutnya.
Reflection membaca anotasi dan memanggil metode#
Reflection membuka isi kelas lewat objek Class, lalu turun ke anggotanya lewat getDeclaredMethods. Objek Method yang dihasilkan dapat diperiksa anotasinya dengan isAnnotationPresent, dan dapat dipanggil dengan invoke meskipun pemanggilnya tidak pernah menyebut nama metode itu di kode. Program berikut merakit mini framework pengujian dengan pola tersebut, meniru cara JUnit menjalankan metode bertanda @Test.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
public class MiniRunner {
public static void main(String[] args) throws Exception {
int lulus = 0;
int gagal = 0;
Method[] metode = UjiKalkulator.class.getDeclaredMethods();
Arrays.sort(metode, Comparator.comparing(Method::getName));
for (Method m : metode) {
if (m.isAnnotationPresent(Uji.class)) {
try {
m.invoke(null);
System.out.println("LULUS : " + m.getName());
lulus++;
} catch (Exception e) {
System.out.println("GAGAL : " + m.getName());
gagal++;
}
}
}
System.out.println("Total : " + lulus + " lulus, " + gagal + " gagal");
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Uji {
}
class UjiKalkulator {
@Uji
static void faktorialLima() {
cek(Kalkulator.faktorial(5) == 120);
}
@Uji
static void pangkatSepuluh() {
cek(Kalkulator.pangkat(2, 10) == 1024);
}
@Uji
static void kasusGagal() {
cek(Kalkulator.faktorial(4) == 25); // sengaja salah
}
static void cek(boolean benar) {
if (!benar) {
throw new AssertionError("pernyataan tidak terpenuhi");
}
}
}
class Kalkulator {
static int faktorial(int n) {
int hasil = 1;
for (int i = 2; i <= n; i++) {
hasil *= i;
}
return hasil;
}
static int pangkat(int basis, int eksponen) {
int hasil = 1;
for (int i = 0; i < eksponen; i++) {
hasil *= basis;
}
return hasil;
}
}LULUS : faktorialLima
GAGAL : kasusGagal
LULUS : pangkatSepuluh
Total : 2 lulus, 1 gagalMetode main sama sekali tidak menyebut faktorialLima atau pangkatSepuluh, tetapi keduanya dijalankan karena pembacaan anotasi berjalan atas nama metode, bukan atas pemanggilan yang ditulis tangan. Pemanggilan m.invoke(null) cocok untuk metode static karena tidak ada objek penerimanya. Exception yang dilempar metode sampai ke pemanggil terbungkus InvocationTargetException, sehingga catch (Exception e) di atas menampung kegagalan pernyataan maupun kegagalan akses dalam satu gerbang. Detail penyebab aslinya terbaca lewat getCause pada objek exceptionnya.
Bagaimana framework memakai pasangan ini#
Pola pada MiniRunner adalah versi kecil dari yang dilakukan framework setiap hari. Framework pengujian memindai kelas uji, mencari metode bertanda anotasi, lalu menjalankannya dan mencatat hasilnya. Framework aplikasi web memetakan anotasi pada metode menjadi rute permintaan, sedangkan framework penyimpanan data membaca anotasi pada field untuk menentukan pemetaan kolom. Semuanya memakai langkah yang sama, yaitu anotasi RUNTIME sebagai penanda, reflection sebagai pembacanya, dan invoke sebagai pelaksananya.
Pilihan retensi menentukan jalannya. Anotasi SOURCE tidak pernah sampai ke saat berjalan dan hanya dibaca alat yang bekerja saat kompilasi, misalnya pemroses anotasi (annotation processor) yang menghasilkan kode tambahan. Anotasi RUNTIME menanggung biaya reflection, yang lebih lambat daripada pemanggilan biasa, sehingga framework yang matang menyimpan objek Method yang sudah ditemukan ke dalam cache. Untuk anggota privat, reflection masih bisa masuk dengan setAccessible, dengan catatan modul bernama dapat menolak pembukaan tersebut, sebagaimana dibahas pada topik Modul.
Kesalahan umum#
- Melupakan
@Retention(RetentionPolicy.RUNTIME)pada anotasi yang akan dibaca framework. Anotasi itu tersimpan di berkas.class, tetapiisAnnotationPresentselalu bernilaifalsetanpa pesan error apa pun. Periksa retensinya lebih dahulu saat anotasi “tidak terdeteksi”. - Melekatkan anotasi di luar batas
@Target-nya, misalnya anotasi ber-target metode yang ditempelkan pada field. Kompilator menolaknya dengan pesanannotation interface not applicable to this kind of declaration. - Menangkap
InvocationTargetExceptionlalu melaporkan pesan bungkusnya ke log. Pesan asli ada di penyebabnya, sehingga bacagetCause()sebelum mencatat kegagalan. - Menganggap anotasi menjalankan sesuatu. Deklarasi anotasi tidak berisi perilaku, dan tanpa kode pembaca, anotasi hanya dekorasi yang menempel pada deklarasi.
- Memanggil
invokepada metode privat tanpasetAccessible(true). HasilnyaIllegalAccessExceptionsaat program berjalan, meskipun kode tampak sah saat kompilasi.
Ringkasan#
- Anotasi bawaan seperti
@Override,@Deprecated,@SuppressWarnings, dan@FunctionalInterfaceberbeda retensinya, dan hanya retensiRUNTIMEyang terbaca lewat reflection. - Anotasi sendiri dideklarasikan dengan
@interface, dibentasi@Retentiondan@Target, dan elemennya ditulis seperti metode dengan nilai bawaan. - Reflection membaca anotasi lewat
isAnnotationPresentdangetAnnotation, lalu menjalankan anggotanya lewatinvoke, dengan exception terbungkusInvocationTargetException. - Framework mengandalkan pasangan anotasi dan reflection untuk memindai, membaca, dan menjalankan anggota yang ditandai, dan biayanya ditekan dengan cache serta pemroses anotasi untuk kasus
SOURCE.
Referensi#
- Lesson: Annotations (Java Tutorials) — Oracle — panduan resmi deklarasi anotasi, retensi, dan target.
- Trail: The Reflection API — Oracle — panduan resmi pemeriksaan kelas, metode, dan anotasi saat berjalan.
- java.lang.reflect (Java SE 21 API) — Oracle
— dokumentasi paket reflection, termasuk
MethoddanInvocationTargetException.