Dasar Anotasi dan Reflection

Memakai anotasi bawaan, menulis anotasi sendiri, dan membongkar misteri sihir framework lewat reflection.

Daftar isi

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.

AnotasiTujuannyaRetensi
@Overridememeriksa bahwa metode benar-benar mengganti metode indukSOURCE
@Deprecatedmenandai anggota yang usang, dengan since dan forRemovalRUNTIME
@SuppressWarningsmenekan peringatan kompiler tertentu pada lingkupnyaSOURCE
@FunctionalInterfacememeriksa bahwa interface hanya memiliki satu metode abstrakRUNTIME

Program berikut membaca anotasi lewat reflection dan sekaligus membuktikan perbedaan retensinya.

java BacaAnotasi.java
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() {
    }
}
text Keluaran
aktif : sapa
usang : sapaLama
Retensi @Deprecated : RUNTIME
Retensi @Override   : SOURCE

Metode 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.

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.

java Contoh: deklarasi anotasi penanda metode uji
// 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.

java MiniRunner.java
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;
    }
}
text Keluaran
LULUS : faktorialLima
GAGAL : kasusGagal
LULUS : pangkatSepuluh
Total : 2 lulus, 1 gagal

Metode 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, tetapi isAnnotationPresent selalu bernilai false tanpa 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 pesan annotation interface not applicable to this kind of declaration.
  • Menangkap InvocationTargetException lalu melaporkan pesan bungkusnya ke log. Pesan asli ada di penyebabnya, sehingga baca getCause() sebelum mencatat kegagalan.
  • Menganggap anotasi menjalankan sesuatu. Deklarasi anotasi tidak berisi perilaku, dan tanpa kode pembaca, anotasi hanya dekorasi yang menempel pada deklarasi.
  • Memanggil invoke pada metode privat tanpa setAccessible(true). Hasilnya IllegalAccessException saat program berjalan, meskipun kode tampak sah saat kompilasi.

Ringkasan#

  • Anotasi bawaan seperti @Override, @Deprecated, @SuppressWarnings, dan @FunctionalInterface berbeda retensinya, dan hanya retensi RUNTIME yang terbaca lewat reflection.
  • Anotasi sendiri dideklarasikan dengan @interface, dibentasi @Retention dan @Target, dan elemennya ditulis seperti metode dengan nilai bawaan.
  • Reflection membaca anotasi lewat isAnnotationPresent dan getAnnotation, lalu menjalankan anggotanya lewat invoke, dengan exception terbungkus InvocationTargetException.
  • 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#