Aplikasi web terdiri atas banyak komponen kecil yang saling membutuhkan: kontroler memakai layanan, layanan memakai repositori, repositori memakai sumber data. Prinsip Dependency Injection (DI) untuk pola ini sudah kita bahas pada Chapter Dependency Injection di Section software design — objek menerima kebutuhannya dari luar, bukan menciptakan sendiri. Bab ini membahas penyempurnaan yang hanya dimiliki aplikasi web: penyusunan graf dependensi tersebut diserahkan ke wadah (container) yang mengelola ribuan komponen dan masa hidupnya sepanjang hidup aplikasi. Kita akan melihat cara kerja CDI dan Spring IoC container, lalu membuktikan mekanismenya dengan wadah mini yang benar-benar menyusun dependensi lewat refleksi.

Masalah new Berantai #

Tanpa DI, satu endpoint sederhana memuat seluruh rantai pembangunan di dalam methodnya:

java Instansiasi manual berantai
@GetMapping("/produk")
public String tampilkan() {
    KontrolerProduk kontroler = new KontrolerProduk(
            new LayananProduk(
                    new RepositoriProduk(new KoneksiDb(url, pengguna, sandi))));
    return kontroler.tampilkan();
}

Tiga masalah muncul sekaligus. Kode teknis mengubur logika bisnis. Konfigurasi (URL basis data) terpencar di banyak tempat. Yang paling merugikan: mengganti RepositoriProduk dengan versi palsu untuk pengujian menuntut penulisan ulang method — persis masalah yang diatasi pola dependency inversion, hanya kini dalam skala ratusan komponen.

Wadah Mengambil Alih Penyusunan #

Pada aplikasi web, wadah DI bekerja sebagai pabrik terpusat. Kita mendaftarkan kontrak beserta implementasinya sekali di awal, lalu meminta objek jadi:

java Anotasi CDI pada komponen web
@ApplicationScoped                                    // satu instance sepanjang hidup aplikasi
public class LayananProduk {

    private final RepositoriProduk repositori;

    @Inject                                            // wadah mengisi lewat konstruktor
    public LayananProduk(RepositoriProduk repositori) {
        this.repositori = repositori;
    }
}

@RequestScoped                                        // satu instance per permintaan HTTP
public class KeranjangBelanja { }

Anotasi scope menentukan masa hidup: @ApplicationScoped untuk komponen berat yang dibagi semua pengguna, @RequestScoped untuk data milik satu permintaan, @SessionScoped untuk data lintas permintaan satu pengguna. Wadah menghitung dependensi tiap class, membuat objek dalam urutan yang benar, dan membuangnya sesuai scope-nya — pekerjaan yang mustahil dilakukan manual pada aplikasi dengan ratusan komponen.

Wadah Mini dengan Refleksi #

Mekanisme inti wadah DI ternyata ringkas: peta ikatan kontrak-implementasi, peta cache instance, dan rekursi konstruktor. WadahDependensi mewujudkannya dalam Java murni:

import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class WadahDependensi {

    interface RepositoriProduk {
        String namaSemua();
    }

    static class RepositoriMemori implements RepositoriProduk {
        @Override
        public String namaSemua() {
            return "buku, pena, tas";
        }
    }

    static class RepositoriCadangan implements RepositoriProduk {
        @Override
        public String namaSemua() {
            return "(cadangan) buku, pena, tas";
        }
    }

    static class LayananProduk {
        private final RepositoriProduk repositori;

        LayananProduk(RepositoriProduk repositori) {
            this.repositori = repositori;
        }

        String ringkas() {
            return "produk: " + repositori.namaSemua();
        }
    }

    static class KontrolerProduk {
        private final LayananProduk layanan;

        KontrolerProduk(LayananProduk layanan) {
            this.layanan = layanan;
        }

        String tampilkan() {
            return "200 OK: " + layanan.ringkas();
        }
    }

    static class Wadah {
        private final Map<Class<?>, Object> instance = new HashMap<>();
        private final Map<Class<?>, Class<?>> ikatan = new HashMap<>();

        <T> void ikat(Class<T> kontrak, Class<? extends T> implementasi) {
            ikatan.put(kontrak, implementasi);
        }

        @SuppressWarnings("unchecked")
        <T> T ambil(Class<T> tipe) throws Exception {
            if (instance.containsKey(tipe)) {
                return (T) instance.get(tipe);
            }
            Class<?> implementasi = ikatan.getOrDefault(tipe, tipe);
            Constructor<?> konstruktor = implementasi.getDeclaredConstructors()[0];
            Class<?>[] tipeParam = konstruktor.getParameterTypes();
            Object[] argumen = new Object[tipeParam.length];
            for (int i = 0; i < tipeParam.length; i++) {
                argumen[i] = ambil(tipeParam[i]);
            }
            Object objek = konstruktor.newInstance(argumen);
            instance.put(tipe, objek);
            return (T) objek;
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("== Instansiasi manual tanpa wadah ==");
        KontrolerProduk manual = new KontrolerProduk(
                new LayananProduk(new RepositoriMemori()));
        System.out.println(manual.tampilkan());

        System.out.println();
        System.out.println("== Wadah produksi: wadah menyusun graf dependensi ==");
        Wadah produksi = new Wadah();
        produksi.ikat(RepositoriProduk.class, RepositoriMemori.class);
        KontrolerProduk kontroler = produksi.ambil(KontrolerProduk.class);
        System.out.println(kontroler.tampilkan());

        System.out.println();
        System.out.println("== Wadah pengujian: cukup ganti satu ikatan ==");
        Wadah uji = new Wadah();
        uji.ikat(RepositoriProduk.class, RepositoriCadangan.class);
        KontrolerProduk kontrolerUji = uji.ambil(KontrolerProduk.class);
        System.out.println(kontrolerUji.tampilkan());

        System.out.println();
        System.out.println("== Wadah menyimpan instance tunggal (scope) ==");
        KontrolerProduk sama = produksi.ambil(KontrolerProduk.class);
        System.out.println("ambil() kedua mengembalikan objek sama: " + (kontroler == sama));
    }
}

Output yang Dihasilkan:

text
== Instansiasi manual tanpa wadah ==
200 OK: produk: buku, pena, tas

== Wadah produksi: wadah menyusun graf dependensi ==
200 OK: produk: buku, pena, tas

== Wadah pengujian: cukup ganti satu ikatan ==
200 OK: produk: (cadangan) buku, pena, tas

== Wadah menyimpan instance tunggal (scope) ==
ambil() kedua mengembalikan objek sama: true

Bedah mekanisme wadah mini ini:

  1. Baris 25-30 (LayananProduk): class hanya menyatakan kebutuhan lewat parameter konstruktor — tidak ada new Repositori... di dalamnya. Kontrak inilah yang dibaca wadah; CDI menyebutnya constructor injection dan menjadi bentuk yang dianjurkan karena dependensi menjadi final dan wajib.
  2. Baris 53-55 (ikat): pendaftaran kontrak-implementasi setara dengan anotasi @ApplicationScoped pada class concrete. Di Spring, pekerjaan ini otomatis: class beranotasi @Component dipindai dan didaftarkan sendiri.
  3. Baris 58-61 (instance.containsKey): cache instance adalah pengejawantahan scope @ApplicationScoped. Panggilan kedua untuk tipe yang sama mengembalikan objek identik — output membuktikannya lewat perbandingan ==.
  4. Baris 62 (getOrDefault): tipe tanpa ikatan dianggap mengikat dirinya sendiri — cukup untuk class concrete seperti KontrolerProduk.
  5. Baris 63-68 (getDeclaredConstructors + rekursi): inti wadah. Refleksi membaca parameter konstruktor, lalu ambil memanggil dirinya sendiri untuk setiap parameter — graf dependensi diselesaikan dari daun ke akar: repositori lebih dulu, layanan kedua, kontroler terakhir.
  6. Baris 90-93 (wadah uji): keuntungan yang paling terasa. Lingkungan pengujian membangun wadah sendiri dengan satu ikatan berbeda; seluruh rantai kontroler-layanan-repositori berubah perilaku tanpa satu baris pun diubah di class komponen. Pola ini adalah fondasi mocking yang kita pelajari pada Section Testing.

Dengan perangkai komponen tersedia, semua bahan aplikasi web akhirnya lengkap: protokol, pemroses permintaan, memori pengguna, rendering, platform standar, dan penyusun komponen. Saatnya merakit semuanya menjadi layanan yang paling banyak dibangun dewasa ini — REST API — topik Chapter Membangun REST API.

Referensi #

Berkas Sumber

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

WadahDependensi.java

Unduh berkas
java
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class WadahDependensi {

    interface RepositoriProduk {
        String namaSemua();
    }

    static class RepositoriMemori implements RepositoriProduk {
        @Override
        public String namaSemua() {
            return "buku, pena, tas";
        }
    }

    static class RepositoriCadangan implements RepositoriProduk {
        @Override
        public String namaSemua() {
            return "(cadangan) buku, pena, tas";
        }
    }

    static class LayananProduk {
        private final RepositoriProduk repositori;

        LayananProduk(RepositoriProduk repositori) {
            this.repositori = repositori;
        }

        String ringkas() {
            return "produk: " + repositori.namaSemua();
        }
    }

    static class KontrolerProduk {
        private final LayananProduk layanan;

        KontrolerProduk(LayananProduk layanan) {
            this.layanan = layanan;
        }

        String tampilkan() {
            return "200 OK: " + layanan.ringkas();
        }
    }

    static class Wadah {
        private final Map<Class<?>, Object> instance = new HashMap<>();
        private final Map<Class<?>, Class<?>> ikatan = new HashMap<>();

        <T> void ikat(Class<T> kontrak, Class<? extends T> implementasi) {
            ikatan.put(kontrak, implementasi);
        }

        @SuppressWarnings("unchecked")
        <T> T ambil(Class<T> tipe) throws Exception {
            if (instance.containsKey(tipe)) {
                return (T) instance.get(tipe);
            }
            Class<?> implementasi = ikatan.getOrDefault(tipe, tipe);
            Constructor<?> konstruktor = implementasi.getDeclaredConstructors()[0];
            Class<?>[] tipeParam = konstruktor.getParameterTypes();
            Object[] argumen = new Object[tipeParam.length];
            for (int i = 0; i < tipeParam.length; i++) {
                argumen[i] = ambil(tipeParam[i]);
            }
            Object objek = konstruktor.newInstance(argumen);
            instance.put(tipe, objek);
            return (T) objek;
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("== Instansiasi manual tanpa wadah ==");
        KontrolerProduk manual = new KontrolerProduk(
                new LayananProduk(new RepositoriMemori()));
        System.out.println(manual.tampilkan());

        System.out.println();
        System.out.println("== Wadah produksi: wadah menyusun graf dependensi ==");
        Wadah produksi = new Wadah();
        produksi.ikat(RepositoriProduk.class, RepositoriMemori.class);
        KontrolerProduk kontroler = produksi.ambil(KontrolerProduk.class);
        System.out.println(kontroler.tampilkan());

        System.out.println();
        System.out.println("== Wadah pengujian: cukup ganti satu ikatan ==");
        Wadah uji = new Wadah();
        uji.ikat(RepositoriProduk.class, RepositoriCadangan.class);
        KontrolerProduk kontrolerUji = uji.ambil(KontrolerProduk.class);
        System.out.println(kontrolerUji.tampilkan());

        System.out.println();
        System.out.println("== Wadah menyimpan instance tunggal (scope) ==");
        KontrolerProduk sama = produksi.ambil(KontrolerProduk.class);
        System.out.println("ambil() kedua mengembalikan objek sama: " + (kontroler == sama));
    }
}