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:

java Tabel.java
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Tabel {
    String nama();
}
java Kolom.java
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:

java ContohOrm.java
@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:

text
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: Sementara

Mari kita telaah bagian-bagian penting mesin dan program utamanya:

  1. Baris 8-11 (entitas.getAnnotation(Tabel.class) dan guard null): membaca nama tabel dari class. Bila annotation tidak terpasang, generator melempar IllegalArgumentException dengan nama class — kegagalan harus nyaring, bukan diam-diam menghasilkan SQL kosong.
  2. Baris 12-13 (getDeclaredFields() lalu Arrays.sort): mengambil seluruh field milik class. Urutan hasil getDeclaredFields() tidak dijamin JLS, sehingga pengurutan berdasarkan nama membuat DDL deterministik — kolom selalu tercetak dalam urutan abjad yang sama di setiap eksekusi.
  3. Baris 16-19 (f.getAnnotation(Kolom.class) dan continue): hanya field ber-@Kolom menjadi kolom tabel. Field catatanInternal di Pelanggan dibuktikan tidak muncul pada output — mekanisme opt-in yang sama dipakai ORM nyata.
  4. 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. Variabel pemisah menyisipkan koma hanya di antara kolom, bukan setelah kolom terakhir.
  5. 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 #

Berkas Sumber

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

ContohOrm.java

Unduh berkas
java
@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 berkas
java
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;
}

PembuatTabel.java

Unduh berkas
java
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();
    }
}

Tabel.java

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Tabel {
    String nama();
}