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:
Tabel : pelanggan
Skema : publik
id -> kolom=id, wajib=true, indeks=[unik]
namaLengkap -> kolom=nama_lengkap, wajib=false, indeks=[]
Diaudit? trueMari kita telaah bagian-bagian penting kode di atas:
- Baris 6-9 (
@interface Entitas ...): mendeklarasikan annotation dengan dua elemen. Elementabel()wajib diisi karena tidak punyadefault; elemenskema()boleh ditinggalkan dan otomatis bernilai"publik". - Baris 17-18 (
@Retention(RetentionPolicy.RUNTIME)dan@Target(ElementType.TYPE)): dua meta-annotation yang menempel pada annotation kita — annotation tentang annotation.@Retentionmenjaga annotation tetap terbaca saat program berjalan, dan@Targetmembatasi@Entitashanya untuk deklarasi tipe. Pembahasan rinci keduanya ada pada dua Chapter berikutnya. - Baris 27 (
@interface Diaudit {}): marker annotation — annotation tanpa elemen yang kegunaannya sekadar menandai kehadiran suatu sifat, seperti label “perlu audit”. - Baris 31-40 (
@Entitas(tabel = "pelanggan")dan@Kolom(...)): pemakaian annotation. Bentukindeks = {"unik"}mengisi elemen array dengan satu nilai; bila lebih dari satu, nilainya dipisah koma di dalam kurung kurawal. - Baris 44-53 (
getAnnotation(...)): membaca kembali metadata lewat reflection — methodtabel(),skema(),nama(),wajib(), danindeks()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:
beranda melayani /beranda
tentang melayani /tentangPenulisan @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:
KolomTanpaNama.java:11: error: annotation @Kolom is missing a default value for the element 'nama'
@Kolom(wajib = true) // elemen wajib 'nama' tidak diisi
^
1 errorPesan 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 #
- Package java.lang.annotation (Oracle)
- JLS 9.6: Annotation Interfaces
- JLS 9.7: Annotations
- Dev.java: Annotations
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ContohAnotasiKustom.java
Unduh berkasimport 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 berkasimport 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 berkasimport 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) {}
}