Annotation bawaan seperti @Override sudah menyelesaikan kebutuhan compiler, tetapi banyak kebutuhan nyata berada di luar daftar itu: memetakan class ke tabel database, menandai method yang melayani URL tertentu, atau memberi label kasus uji dengan prioritas. Untuk kebutuhan semacam itu Java membolehkan kita mendefinisikan annotation sendiri — custom annotation — lengkap dengan elemen bertipe, nilai bawaan, bahkan annotation tanpa elemen (marker). Pada Chapter ini kita akan mendefinisikan annotation @Entitas, @Kolom, dan @Diaudit bergaya pemetaan database, lalu membacanya kembali sebagai bukti bahwa metadata tersebut benar-benar tersimpan.

Mendeklarasikan Annotation dengan @interface #

Custom annotation dideklarasikan dengan kata kunci @interface, bukan interface biasa. Compiler javac memperlakukan deklarasi tersebut sebagai annotation type: setiap method tanpa parameter di dalamnya menjadi elemen yang nilainya kita isikan saat annotation dipakai. Aturan tipenya ketat — elemen hanya boleh bertipe String, tipe primitif, Class, enum, annotation lain, atau array dari tipe-tipe tersebut; tipe seperti Integer atau List ditolak dengan error invalid type for annotation interface element.

Deklarasi berikut membentuk tiga annotation untuk memetakan class domain ke skema database:

import java.lang.annotation.*;
import java.lang.reflect.Field;
import java.util.Arrays;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Entitas {
    String tabel();                    // elemen wajib: tanpa nilai bawaan
    String skema() default "publik";   // elemen opsional: punya nilai bawaan
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Kolom {
    String nama();
    boolean wajib() default false;
    String[] indeks() default {};      // elemen array dengan bawaan kosong
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Diaudit {}                  // marker: tanpa elemen sama sekali

@Entitas(tabel = "pelanggan")
@Diaudit
class Pelanggan {
    @Kolom(nama = "id", wajib = true, indeks = {"unik"})
    public int id;

    @Kolom(nama = "nama_lengkap")
    public String namaLengkap;
}

public class ContohAnotasiKustom {
    public static void main(String[] args) {
        Entitas e = Pelanggan.class.getAnnotation(Entitas.class);
        System.out.println("Tabel : " + e.tabel());
        System.out.println("Skema : " + e.skema());

        Field[] field = Pelanggan.class.getDeclaredFields();
        for (Field f : field) {
            Kolom k = f.getAnnotation(Kolom.class);
            System.out.println(f.getName() + " -> kolom=" + k.nama()
                    + ", wajib=" + k.wajib()
                    + ", indeks=" + Arrays.toString(k.indeks()));
        }
        System.out.println("Diaudit? " + (Pelanggan.class.getAnnotation(Diaudit.class) != null));
    }
}

Output yang Dihasilkan:

text
Tabel : pelanggan
Skema : publik
id -> kolom=id, wajib=true, indeks=[unik]
namaLengkap -> kolom=nama_lengkap, wajib=false, indeks=[]
Diaudit? true

Mari kita telaah bagian-bagian penting kode di atas:

  1. Baris 6-9 (@interface Entitas ...): mendeklarasikan annotation dengan dua elemen. Elemen tabel() wajib diisi karena tidak punya default; elemen skema() boleh ditinggalkan dan otomatis bernilai "publik".
  2. Baris 17-18 (@Retention(RetentionPolicy.RUNTIME) dan @Target(ElementType.TYPE)): dua meta-annotation yang menempel pada annotation kita — annotation tentang annotation. @Retention menjaga annotation tetap terbaca saat program berjalan, dan @Target membatasi @Entitas hanya untuk deklarasi tipe. Pembahasan rinci keduanya ada pada dua Chapter berikutnya.
  3. Baris 27 (@interface Diaudit {}): marker annotation — annotation tanpa elemen yang kegunaannya sekadar menandai kehadiran suatu sifat, seperti label “perlu audit”.
  4. Baris 31-40 (@Entitas(tabel = "pelanggan") dan @Kolom(...)): pemakaian annotation. Bentuk indeks = {"unik"} mengisi elemen array dengan satu nilai; bila lebih dari satu, nilainya dipisah koma di dalam kurung kurawal.
  5. Baris 44-53 (getAnnotation(...)): membaca kembali metadata lewat reflection — method tabel(), skema(), nama(), wajib(), dan indeks() dipanggil seperti method biasa pada objek annotation yang dikembalikan JVM.

Perhatikan pola pemakaian pada id dan namaLengkap. Field id mengisi ketiga elemen @Kolom, sedangkan namaLengkap hanya mengisi nama — elemen wajib bernilai false dan indeks menjadi array kosong karena keduanya memakai nilai bawaan. Output membuktikan nilai bawaan benar-benar terpakai tanpa kita tuliskan.

Marker dan Keluarga Annotation #

Marker seperti @Diaudit berguna ketika keputusan cukup diambil dari keberadaan penanda — “class ini perlu dicatat jejaknya” — tanpa perlu nilai tambahan. Serializable di Java bekerja dengan pola serupa pada level interface. Kehadiran marker mudah diperiksa: pemanggilan getAnnotation(Diaudit.class) mengembalikan objek bila penanda terpasang dan null bila tidak, sehingga baris terakhir program mencetak true.

Nama annotation kustom lazim dipilih menyerupai kata sifat atau label domain: @Entitas, @Diaudit, @Rute. Berbeda dengan annotation bawaan Java yang menempel pada compiler, annotation kustom sama sekali tidak dienkreasi oleh javac — kecuali kita pasangkan meta-annotation yang relevan. Efeknya murni datang dari kode yang membacanya: framework Anda sendiri, generator kode, atau program utama seperti contoh di atas.

Konvensi Elemen value Tunggal #

Annotation dengan tepat satu elemen lazim dinamai elemennya value, karena compiler memberi keringanan khusus: nilai dapat ditulis langsung tanpa menyebut nama elemen. Konvensi inilah yang dipakai @SuppressWarnings("unchecked") dari Chapter sebelumnya — "unchecked" sebenarnya mengisi elemen value. Berikut annotation @Rute untuk web routing sederhana:

import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Rute {
    String value(); // konvensi elemen tunggal bernama value
}

class ServerWeb {
    @Rute("/tentang")
    public void tentang() {}

    @Rute(value = "/beranda")
    public void beranda() {}
}

public class ContohValueTunggal {
    public static void main(String[] args) {
        Method[] semua = ServerWeb.class.getDeclaredMethods();
        Arrays.sort(semua, Comparator.comparing(Method::getName));
        for (Method m : semua) {
            Rute r = m.getAnnotation(Rute.class);
            if (r != null) {
                System.out.println(m.getName() + " melayani " + r.value());
            }
        }
    }
}

Output yang Dihasilkan:

text
beranda melayani /beranda
tentang melayani /tentang

Penulisan @Rute("/tentang") dan @Rute(value = "/beranda") menghasilkan struktur metadata yang sama persis — bentuk pertama hanyalah singkatan yang diperbolehkan karena elemen tunggal bernama value. Method main memindai seluruh method di ServerWeb, mencetak hanya yang berlabel @Rute. Urutan hasil dari getDeclaredMethods() tidak dijamin JLS, sehingga program mengurutkan berdasarkan nama method lebih dulu demi output yang stabil.

Elemen Wajib yang Ditinggalkan #

Compiler menjaga kelengkapan elemen pada saat kompilasi. Bila elemen tanpa default tidak diisi, program gagal dikompilasi — bukan gagal diam-diam saat runtime. Berkas berikut sengaja menghilangkan elemen nama:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Kolom {
    String nama();
    boolean wajib() default false;
}

public class KolomTanpaNama {
    @Kolom(wajib = true) // elemen wajib 'nama' tidak diisi
    int id;

    public static void main(String[] args) {}
}

Pesan error yang kita verifikasi dari compiler javac:

text
KolomTanpaNama.java:11: error: annotation @Kolom is missing a default value for the element 'nama'
    @Kolom(wajib = true) // elemen wajib 'nama' tidak diisi
    ^
1 error

Pesan tersebut menyebut nama elemen yang hilang sehingga perbaikannya jelas: isi nama = "..." atau beri default pada deklarasi elemen. Perlindungan compile-time seperti inilah yang membuat annotation layak dipakai sebagai konfigurasi — kesalahan ketik nama elemen maupun nilai yang hilang tertangkap sebelum program berjalan.

Dua keputusan penting pada contoh di atas sengaja ditunda: @Retention(RetentionPolicy.RUNTIME) dan @Target(...) kita pakai tanpa dibedah. Tanpa @Retention, annotation kustom tidak akan terbaca oleh reflection — perilaku yang mengejutkan banyak pemula. Chapter berikutnya membahas tiga tingkat retensi dan konsekuensinya bagi compiler, berkas .class, dan JVM.

Referensi #

Berkas Sumber

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

ContohAnotasiKustom.java

Unduh berkas
java
import java.lang.annotation.*;
import java.lang.reflect.Field;
import java.util.Arrays;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Entitas {
    String tabel();                    // elemen wajib: tanpa nilai bawaan
    String skema() default "publik";   // elemen opsional: punya nilai bawaan
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Kolom {
    String nama();
    boolean wajib() default false;
    String[] indeks() default {};      // elemen array dengan bawaan kosong
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Diaudit {}                  // marker: tanpa elemen sama sekali

@Entitas(tabel = "pelanggan")
@Diaudit
class Pelanggan {
    @Kolom(nama = "id", wajib = true, indeks = {"unik"})
    public int id;

    @Kolom(nama = "nama_lengkap")
    public String namaLengkap;
}

public class ContohAnotasiKustom {
    public static void main(String[] args) {
        Entitas e = Pelanggan.class.getAnnotation(Entitas.class);
        System.out.println("Tabel : " + e.tabel());
        System.out.println("Skema : " + e.skema());

        Field[] field = Pelanggan.class.getDeclaredFields();
        for (Field f : field) {
            Kolom k = f.getAnnotation(Kolom.class);
            System.out.println(f.getName() + " -> kolom=" + k.nama()
                    + ", wajib=" + k.wajib()
                    + ", indeks=" + Arrays.toString(k.indeks()));
        }
        System.out.println("Diaudit? " + (Pelanggan.class.getAnnotation(Diaudit.class) != null));
    }
}

ContohValueTunggal.java

Unduh berkas
java
import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Rute {
    String value(); // konvensi elemen tunggal bernama value
}

class ServerWeb {
    @Rute("/tentang")
    public void tentang() {}

    @Rute(value = "/beranda")
    public void beranda() {}
}

public class ContohValueTunggal {
    public static void main(String[] args) {
        Method[] semua = ServerWeb.class.getDeclaredMethods();
        Arrays.sort(semua, Comparator.comparing(Method::getName));
        for (Method m : semua) {
            Rute r = m.getAnnotation(Rute.class);
            if (r != null) {
                System.out.println(m.getName() + " melayani " + r.value());
            }
        }
    }
}

KolomTanpaNama.java

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Kolom {
    String nama();
    boolean wajib() default false;
}

public class KolomTanpaNama {
    @Kolom(wajib = true) // elemen wajib 'nama' tidak diisi
    int id;

    public static void main(String[] args) {}
}