Annotation baru benar-benar berguna ketika ada kode yang membacanya dan mengubah perilaku berdasarkan isinya. @Tabel(nama = "pelanggan") yang kita definisikan di Chapter sebelumnya tidak melakukan apa-apa sendirian — tetapi sebuah pembaca dapat memindai class domain, mengumpulkan seluruh metadata, lalu menghasilkan SQL, memvalidasi input, atau menyambungkan method ke URL. Inilah cara kerja inti framework modern: Hibernate membaca annotation entitas untuk membuat tabel, JUnit menemukan method @Test, dan Spring menyambungkan komponen lewat @Autowired. Pada Chapter ini kita membangun mini-ORM sendiri: sebuah generator yang mengubah class domain ber-annotation menjadi pernyataan CREATE TABLE.
Membaca Annotation dari AnnotatedElement #
Seluruh pembacaan annotation berpusat pada interface java.lang.reflect.AnnotatedElement. Class reflection seperti Class, Field, Method, dan Constructor semuanya mengimplementasikan interface tersebut, sehingga pola pemanggilannya seragam di semua jenis deklarasi. Method utamanya: getAnnotation(Jenis.class) mengembalikan objek annotation bila terpasang atau null bila tidak; isAnnotationPresent(Jenis.class) cukup untuk pengecekan keberadaan; dan getAnnotations() mengembalikan semua annotation ber-retensi RUNTIME yang menempel.
Objek annotation yang dikembalikan JVM berperilaku seperti record implementasi otomatis — setiap elemen dibaca dengan memanggil method-nya: tabel.nama() atau k.tidakNull(). Nilai yang tidak diisi pengguna saat menempelkan annotation otomatis bernilai default dari deklarasi. Pola bacanya selalu tiga langkah: dapatkan objek reflection, panggil getAnnotation, lalu akses elemen.
Merancang Mini ORM Tiga Lapis #
Generator kita terbagi tiga lapis seperti framework sungguhan. Lapis pertama adalah definisi metadata — annotation @Tabel dan @Kolom yang sama dengan Chapter Custom Annotations:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Tabel {
String nama();
}import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Kolom {
String nama() default "";
String tipe() default "TEXT";
boolean kunciUtama() default false;
boolean tidakNull() default false;
}Kedua annotation ber-retensi RUNTIME — syarat mutlak agar terbaca — dan masing-masing dibatasi @Target pada tipe dan field. Lapis kedua adalah mesin pembaca, PembuatTabel, yang menerima objek Class<?> dan merangkai DDL:
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Comparator;
public class PembuatTabel {
/** Menghasilkan DDL CREATE TABLE dari annotation pada class entitas. */
public static String buatDdl(Class<?> entitas) {
Tabel tabel = entitas.getAnnotation(Tabel.class);
if (tabel == null) {
throw new IllegalArgumentException("class tanpa @Tabel: " + entitas.getSimpleName());
}
Field[] field = entitas.getDeclaredFields();
Arrays.sort(field, Comparator.comparing(Field::getName)); // urutan stabil
StringBuilder sql = new StringBuilder("CREATE TABLE ");
sql.append(tabel.nama()).append(" (\n");
String pemisah = "";
for (Field f : field) {
Kolom k = f.getAnnotation(Kolom.class);
if (k == null) {
continue; // field tanpa @Kolom bukan kolom tabel
}
sql.append(pemisah)
.append(" ").append(k.nama())
.append(" ").append(k.tipe());
if (k.tidakNull()) {
sql.append(" NOT NULL");
}
if (k.kunciUtama()) {
sql.append(" PRIMARY KEY");
}
pemisah = ",\n";
}
sql.append("\n);");
return sql.toString();
}
}Lapis ketiga adalah domain dan program utama. Dua entitas diberi annotation, dan main memanggil generator untuk keduanya:
@Tabel(nama = "pelanggan")
class Pelanggan {
@Kolom(nama = "id", tipe = "INTEGER", kunciUtama = true, tidakNull = true)
private int id;
@Kolom(nama = "email", tidakNull = true)
private String email;
private String catatanInternal; // tanpa @Kolom: diabaikan generator
}
@Tabel(nama = "pesanan")
class Pesanan {
@Kolom(nama = "id", tipe = "INTEGER", kunciUtama = true)
private int id;
@Kolom(nama = "pelanggan_id", tipe = "INTEGER", tidakNull = true)
private int pelangganId;
@Kolom(nama = "total", tipe = "REAL", tidakNull = true)
private double total;
}
class Sementara {} // tanpa @Tabel
public class ContohOrm {
public static void main(String[] args) {
System.out.println(PembuatTabel.buatDdl(Pelanggan.class));
System.out.println();
System.out.println(PembuatTabel.buatDdl(Pesanan.class));
System.out.println();
System.out.println("Pelanggan punya @Tabel? "
+ Pelanggan.class.isAnnotationPresent(Tabel.class));
try {
PembuatTabel.buatDdl(Sementara.class);
} catch (IllegalArgumentException e) {
System.out.println("Ditolak : " + e.getMessage());
}
}
}Output yang Dihasilkan:
CREATE TABLE pelanggan (
email TEXT NOT NULL,
id INTEGER NOT NULL PRIMARY KEY
);
CREATE TABLE pesanan (
id INTEGER PRIMARY KEY,
pelanggan_id INTEGER NOT NULL,
total REAL NOT NULL
);
Pelanggan punya @Tabel? true
Ditolak : class tanpa @Tabel: SementaraMari kita telaah bagian-bagian penting mesin dan program utamanya:
- Baris 8-11 (
entitas.getAnnotation(Tabel.class)dan guardnull): membaca nama tabel dari class. Bila annotation tidak terpasang, generator melemparIllegalArgumentExceptiondengan nama class — kegagalan harus nyaring, bukan diam-diam menghasilkan SQL kosong. - Baris 12-13 (
getDeclaredFields()laluArrays.sort): mengambil seluruh field milik class. Urutan hasilgetDeclaredFields()tidak dijamin JLS, sehingga pengurutan berdasarkan nama membuat DDL deterministik — kolom selalu tercetak dalam urutan abjad yang sama di setiap eksekusi. - Baris 16-19 (
f.getAnnotation(Kolom.class)dancontinue): hanya field ber-@Kolommenjadi kolom tabel. FieldcatatanInternaldiPelanggandibuktikan tidak muncul pada output — mekanisme opt-in yang sama dipakai ORM nyata. - Baris 20-30 (rangkaian
StringBuilder): setiap elemen annotation dibaca lewat method-nya —k.nama(),k.tipe(),k.tidakNull(),k.kunciUtama()— lalu diterjemahkan menjadi potongan SQL. Variabelpemisahmenyisipkan koma hanya di antara kolom, bukan setelah kolom terakhir. - Baris 55-56 (
isAnnotationPresent): pengecekan cepat tanpa mengambil objek annotation — cukup untuk pertanyaan ya/tidak seperti “class ini entitas atau bukan”.
Perhatikan bahwa email pada Pelanggan tidak mengisi elemen tipe, dan hasilnya tetap TEXT — nilai default dari deklarasi @Kolom bekerja tanpa kita menuliskannya. Class Sementara yang polos ditolak dengan pesan Ditolak : class tanpa @Tabel: Sementara, membuktikan guard di baris 9-11 benar-benar aktif.
Pola yang Sama dengan Framework Besar #
Mini ORM ini, meski kecil, memuat pola dasar framework nyata. Hibernate memindai class ber-@Entity, membaca @Column dan @Id lewat reflection, lalu membuat DDL dan query; JUnit mencari method ber-@Test lalu memanggilnya satu per satu; Spring membaca @Component untuk mendaftarkan objek ke container. Perbedaannya skala: framework besar menambah validasi konflik, dukungan pewarisan dengan @Inherited, dan cache hasil pemindaian — tetapi langkah intinya tetap getAnnotation pada AnnotatedElement.
Kita juga baru menyentuh satu jenis objek reflection: Field. Padahal getAnnotation hanyalah satu dari ratusan kemampuan java.lang.reflect — package yang sama mampu memanggil method privat, membaca field tanpa getter, bahkan membuat objek tanpa new. Chapter berikutnya memetakan Reflection API secara menyeluruh sebelum kita berani memakai kekuatannya.
Referensi #
- Interface AnnotatedElement (Oracle)
- Package java.lang.annotation (Oracle)
- Package java.lang.reflect (Oracle)
- Dev.java: Annotations
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ContohOrm.java
Unduh berkas@Tabel(nama = "pelanggan")
class Pelanggan {
@Kolom(nama = "id", tipe = "INTEGER", kunciUtama = true, tidakNull = true)
private int id;
@Kolom(nama = "email", tidakNull = true)
private String email;
private String catatanInternal; // tanpa @Kolom: diabaikan generator
}
@Tabel(nama = "pesanan")
class Pesanan {
@Kolom(nama = "id", tipe = "INTEGER", kunciUtama = true)
private int id;
@Kolom(nama = "pelanggan_id", tipe = "INTEGER", tidakNull = true)
private int pelangganId;
@Kolom(nama = "total", tipe = "REAL", tidakNull = true)
private double total;
}
class Sementara {} // tanpa @Tabel
public class ContohOrm {
public static void main(String[] args) {
System.out.println(PembuatTabel.buatDdl(Pelanggan.class));
System.out.println();
System.out.println(PembuatTabel.buatDdl(Pesanan.class));
System.out.println();
System.out.println("Pelanggan punya @Tabel? "
+ Pelanggan.class.isAnnotationPresent(Tabel.class));
try {
PembuatTabel.buatDdl(Sementara.class);
} catch (IllegalArgumentException e) {
System.out.println("Ditolak : " + e.getMessage());
}
}
}
Kolom.java
Unduh berkasimport java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Kolom {
String nama() default "";
String tipe() default "TEXT";
boolean kunciUtama() default false;
boolean tidakNull() default false;
}
PembuatTabel.java
Unduh berkasimport java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Comparator;
public class PembuatTabel {
/** Menghasilkan DDL CREATE TABLE dari annotation pada class entitas. */
public static String buatDdl(Class<?> entitas) {
Tabel tabel = entitas.getAnnotation(Tabel.class);
if (tabel == null) {
throw new IllegalArgumentException("class tanpa @Tabel: " + entitas.getSimpleName());
}
Field[] field = entitas.getDeclaredFields();
Arrays.sort(field, Comparator.comparing(Field::getName)); // urutan stabil
StringBuilder sql = new StringBuilder("CREATE TABLE ");
sql.append(tabel.nama()).append(" (\n");
String pemisah = "";
for (Field f : field) {
Kolom k = f.getAnnotation(Kolom.class);
if (k == null) {
continue; // field tanpa @Kolom bukan kolom tabel
}
sql.append(pemisah)
.append(" ").append(k.nama())
.append(" ").append(k.tipe());
if (k.tidakNull()) {
sql.append(" NOT NULL");
}
if (k.kunciUtama()) {
sql.append(" PRIMARY KEY");
}
pemisah = ",\n";
}
sql.append("\n);");
return sql.toString();
}
}
Tabel.java
Unduh berkasimport java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Tabel {
String nama();
}