Di dalam aplikasi Spring, hampir semua objek yang kita pakai adalah bean: objek yang hidupnya dikelola penuh oleh container, mulai dari instansiasi, pengisian properti, inisialisasi, hingga pemusnahan. Pemahaman atas siklus hidup bean membedakan pengembang yang sekadar memakai Spring dari pengembang yang bisa menjelaskan mengapa resource database dibuka pada method bertanda @PostConstruct dan ditutup pada @PreDestroy. Pada Chapter ini kita bedah tahapan siklus hidup bean, dua scope terpenting, serta dua cara mendefinisikan bean: anotasi stereotip dan method @Bean di kelas @Configuration.

Tahapan Siklus Hidup Bean #

Container Spring tidak membangun bean secara instan sekaligus. Urutan pekerjaannya untuk satu bean berjalan seperti ini:

text
instansiasi (konstruktor) -> populate properties -> @PostConstruct -> siap dipakai -> @PreDestroy -> bean dibebaskan

Tahap instansiasi memanggil konstruktor; pada titik ini field @Autowired masih kosong, sehingga logika yang membutuhkan dependensi belum boleh berjalan di konstruktor. Tahap populate properties mengisi dependensi dan nilai @Value. Setelah semua terisi, container memanggil method bertanda @PostConstruct — tempat yang tepat untuk validasi akhir, membuka koneksi, atau memuat data referensi. Ketika container dimatikan, method @PreDestroy dipanggil untuk memberi kesempatan melepas resource sebelum objek benar-benar dikumpulkan garbage collector.

Berikut wujud nyatanya dalam kode Spring yang ditampilkan apa adanya untuk Spring Boot 3.4/3.5:

java
@Component
public class PenyimpanSesi {

    @Value("${sesi.kapasitas:64}")
    private int kapasitas;

    @PostConstruct
    public void mulai() {
        System.out.println("PenyimpanSesi siap, kapasitas=" + kapasitas);
    }

    @PreDestroy
    public void berhenti() {
        System.out.println("Menutup resource penyimpanan sesi");
    }
}

Mensimulasikan Siklus Hidup #

Agar setiap tahap terlihat, kita bangun simulasi container mini dengan Java murni. Program berikut mendaftarkan bean singleton dan prototype, lalu menutup container untuk memicu tahap pemusnahan:

java SiklusBean.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class SiklusBean {

    public static void main(String[] args) throws Exception {
        ContainerMini wadah = new ContainerMini();

        System.out.println("-- pendaftaran bean --");
        wadah.daftarSingleton("penyimpanSesi", PenyimpanSesi.class);
        wadah.daftarPrototype("pembuatId", PembuatId.class);

        System.out.println();
        System.out.println("-- pemakaian bean --");
        PenyimpanSesi sesi = wadah.ambil("penyimpanSesi");
        sesi.simpan("Budi", "token-abc");
        System.out.println("Sesi Budi: " + sesi.ambil("Budi"));

        System.out.println();
        System.out.println("-- lingkup singleton vs prototype --");
        PenyimpanSesi sesiLagi = wadah.ambil("penyimpanSesi");
        PembuatId idA = wadah.ambil("pembuatId");
        PembuatId idB = wadah.ambil("pembuatId");
        System.out.println("singleton identik : " + (sesi == sesiLagi));
        System.out.println("prototype identik : " + (idA == idB));

        System.out.println();
        System.out.println("-- container ditutup: hanya singleton yang dihancurkan --");
        wadah.tutup();
    }
}

// Marker penanda tahap siklus, mirror dari anotasi Spring.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface PascaBuat { }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface PraHapus { }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Nilai {
    String value();
}

class ContainerMini {

    private final Map<String, Object> singleton = new LinkedHashMap<>();
    private final Map<String, Class<?>> prototype = new LinkedHashMap<>();

    void daftarSingleton(String nama, Class<?> tipe) throws Exception {
        System.out.println("[Siklus] " + nama + ": instansiasi");
        Object instans = cipta(tipe);
        isiNilai(instans);
        panggilAnotasi(instans, PascaBuat.class);
        singleton.put(nama, instans);
    }

    void daftarPrototype(String nama, Class<?> tipe) {
        prototype.put(nama, tipe);
    }

    @SuppressWarnings("unchecked")
    <T> T ambil(String nama) throws Exception {
        if (singleton.containsKey(nama)) {
            return (T) singleton.get(nama);
        }
        Class<?> tipe = prototype.get(nama);
        if (tipe == null) {
            throw new IllegalStateException("bean tak dikenal: " + nama);
        }
        Object instans = cipta(tipe);
        isiNilai(instans);
        panggilAnotasi(instans, PascaBuat.class);
        return (T) instans;
    }

    private Object cipta(Class<?> tipe) throws Exception {
        return tipe.getDeclaredConstructor().newInstance();
    }

    // Tahap populate: container mengisi field bertanda @Nilai.
    private void isiNilai(Object instans) throws Exception {
        for (Field f : instans.getClass().getDeclaredFields()) {
            Nilai tanda = f.getAnnotation(Nilai.class);
            if (tanda == null) {
                continue;
            }
            f.setAccessible(true);
            if (f.getType() == int.class) {
                f.setInt(instans, Integer.parseInt(tanda.value()));
            } else {
                f.set(instans, tanda.value());
            }
            System.out.println("[Siklus] populate " + f.getName() + " = " + tanda.value());
        }
    }

    private void panggilAnotasi(Object instans, Class<? extends java.lang.annotation.Annotation> anotasi)
            throws Exception {
        for (Method m : instans.getClass().getDeclaredMethods()) {
            if (m.isAnnotationPresent(anotasi)) {
                m.setAccessible(true);
                m.invoke(instans);
            }
        }
    }

    void tutup() {
        List<String> urutan = new ArrayList<>(singleton.keySet());
        for (int i = urutan.size() - 1; i >= 0; i--) {
            String nama = urutan.get(i);
            Object instans = singleton.get(nama);
            System.out.println("[Siklus] " + nama + ": tahap hancur");
            try {
                panggilAnotasi(instans, PraHapus.class);
            } catch (Exception e) {
                throw new IllegalStateException("Gagal menghancurkan " + nama);
            }
        }
        System.out.println("[Siklus] container ditutup");
    }
}

class PenyimpanSesi {

    @Nilai("64")
    private int kapasitas;

    private final Map<String, String> sesi = new LinkedHashMap<>();

    @PascaBuat
    void mulai() {
        System.out.println("[Siklus] @PostConstruct PenyimpanSesi siap (kapasitas=" + kapasitas + ")");
    }

    @PraHapus
    void berhenti() {
        System.out.println("[Siklus] @PreDestroy membersihkan " + sesi.size() + " sesi");
        sesi.clear();
    }

    void simpan(String kunci, String nilai) {
        sesi.put(kunci, nilai);
    }

    String ambil(String kunci) {
        return sesi.get(kunci);
    }
}

class PembuatId {

    private static int pembuatke = 0;

    @PascaBuat
    void mulai() {
        pembuatke++;
        System.out.println("[Siklus] @PostConstruct instance PembuatId ke-" + pembuatke);
    }

    long baru() {
        return System.nanoTime(); // tidak dicetak: nilai tidak deterministik
    }
}

Output yang Dihasilkan:

text
-- pendaftaran bean --
[Siklus] penyimpanSesi: instansiasi
[Siklus] populate kapasitas = 64
[Siklus] @PostConstruct PenyimpanSesi siap (kapasitas=64)

-- pemakaian bean --
Sesi Budi: token-abc

-- lingkup singleton vs prototype --
[Siklus] @PostConstruct instance PembuatId ke-1
[Siklus] @PostConstruct instance PembuatId ke-2
singleton identik : true
prototype identik : false

-- container ditutup: hanya singleton yang dihancurkan --
[Siklus] penyimpanSesi: tahap hancur
[Siklus] @PreDestroy membersihkan 1 sesi
[Siklus] container ditutup

Mari kita bedah mekanisme di balik simulasi ini:

  1. daftarSingleton(...): urutan panggilan menirai tahap Spring — cipta() untuk instansiasi, isiNilai() untuk populate, panggilAnotasi(PascaBuat.class) untuk inisialisasi, baru bean dimasukkan ke registry. Urutan inilah yang membuat output populate kapasitas = 64 selalu muncul sebelum @PostConstruct.
  2. isiNilai(Object instans): method memindai field yang bertanda @Nilai("64") — mirror dari @Value — lalu mengisi nilainya lewat reflection. Inilah tahap populate properties yang membedahkan mengapa konstruktor tidak boleh membaca kapasitas: nilainya baru ada setelah tahap ini.
  3. tutup(): container memproses singleton dalam urutan pendaftaran terbalik, memanggil method @PraHapus satu per satu. Lihat bahwa PembuatId tidak pernah masuk tahap hancur — sesuai perilaku Spring nyata yang hanya memanggil callback pemusnahan untuk singleton.
  4. singleton identik : true dan prototype identik : false: pengambilan kedua bean penyimpanSesi mengembalikan objek sama, sedangkan dua kali ambil("pembuatId") menciptakan dua objek berbeda dan memanggil @PostConstruct dua kali, persis semantik scope prototype.

Scope Singleton dan Prototype #

Scope menentukan berapa instans yang dihasilkan satu definisi bean. Scope singleton (bawaan) membuat satu definisi menghasilkan satu instans per container; seluruh titik injeksi menerima objek yang sama. Scope prototype memerintahkan container membuat instans baru setiap kali bean diminta. Spring mengenal scope lain yang spesifik web — request, session, application — namun dua scope pertama yang paling menentukan desain sehari-hari.

Konsekuensi praktisnya besar. Bean singleton harus stateless atau setidaknya thread-safe, karena objek yang sama dipakai bersama seluruh request secara paralel. Menyimpan data pengguna di field bean singleton adalah kesalahan klasik yang menghasilkan kebocoran data antar pengguna. Sebaliknya, bean prototype yang memegang state mahal dibangun akan menumpuk biaya pembuatannya di mana-mana.

Mendefinisikan Bean dengan @Configuration #

Pemindaian komponen (@Component beserta turunannya @Service, @Repository, @Controller) menutup kebutuhan kelas milik kita sendiri. Namun kelas dari pustaka pihak ketiga tidak bisa ditandai anotasi milik kita — di sinilah kelas @Configuration dengan method @Bean berperan. Berikut contoh yang ditampilkan apa adanya:

java
@Configuration
public class KonfigurasiKafka {

    @Bean
    public PengirimPesan pengirimKafka(KafkaTemplate<String, String> template) {
        return new PengirimKafka(template);
    }
}

Setiap method @Bean menjadi satu definisi bean; nama method menjadi nama bean, tipe kembalian menjadi tipe yang dicari container, dan parameter method otomatis diinjeksikan. Pola ini juga menjadi pintu masuk kustomisasi bean pihak ketiga — RestClient, ObjectMapper, DataSource — yang kita rangkai sendiri kondisinya. Ketika konfigurasi mulai bercabang per lingkungan, profile mengambil alih, dan itu bahasan Chapter Configuration dan Profiles.

Referensi #

Berkas Sumber

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

SiklusBean.java

Unduh berkas
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class SiklusBean {

    public static void main(String[] args) throws Exception {
        ContainerMini wadah = new ContainerMini();

        System.out.println("-- pendaftaran bean --");
        wadah.daftarSingleton("penyimpanSesi", PenyimpanSesi.class);
        wadah.daftarPrototype("pembuatId", PembuatId.class);

        System.out.println();
        System.out.println("-- pemakaian bean --");
        PenyimpanSesi sesi = wadah.ambil("penyimpanSesi");
        sesi.simpan("Budi", "token-abc");
        System.out.println("Sesi Budi: " + sesi.ambil("Budi"));

        System.out.println();
        System.out.println("-- lingkup singleton vs prototype --");
        PenyimpanSesi sesiLagi = wadah.ambil("penyimpanSesi");
        PembuatId idA = wadah.ambil("pembuatId");
        PembuatId idB = wadah.ambil("pembuatId");
        System.out.println("singleton identik : " + (sesi == sesiLagi));
        System.out.println("prototype identik : " + (idA == idB));

        System.out.println();
        System.out.println("-- container ditutup: hanya singleton yang dihancurkan --");
        wadah.tutup();
    }
}

// Marker penanda tahap siklus, mirror dari anotasi Spring.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface PascaBuat { }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface PraHapus { }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Nilai {
    String value();
}

class ContainerMini {

    private final Map<String, Object> singleton = new LinkedHashMap<>();
    private final Map<String, Class<?>> prototype = new LinkedHashMap<>();

    void daftarSingleton(String nama, Class<?> tipe) throws Exception {
        System.out.println("[Siklus] " + nama + ": instansiasi");
        Object instans = cipta(tipe);
        isiNilai(instans);
        panggilAnotasi(instans, PascaBuat.class);
        singleton.put(nama, instans);
    }

    void daftarPrototype(String nama, Class<?> tipe) {
        prototype.put(nama, tipe);
    }

    @SuppressWarnings("unchecked")
    <T> T ambil(String nama) throws Exception {
        if (singleton.containsKey(nama)) {
            return (T) singleton.get(nama);
        }
        Class<?> tipe = prototype.get(nama);
        if (tipe == null) {
            throw new IllegalStateException("bean tak dikenal: " + nama);
        }
        Object instans = cipta(tipe);
        isiNilai(instans);
        panggilAnotasi(instans, PascaBuat.class);
        return (T) instans;
    }

    private Object cipta(Class<?> tipe) throws Exception {
        return tipe.getDeclaredConstructor().newInstance();
    }

    // Tahap populate: container mengisi field bertanda @Nilai.
    private void isiNilai(Object instans) throws Exception {
        for (Field f : instans.getClass().getDeclaredFields()) {
            Nilai tanda = f.getAnnotation(Nilai.class);
            if (tanda == null) {
                continue;
            }
            f.setAccessible(true);
            if (f.getType() == int.class) {
                f.setInt(instans, Integer.parseInt(tanda.value()));
            } else {
                f.set(instans, tanda.value());
            }
            System.out.println("[Siklus] populate " + f.getName() + " = " + tanda.value());
        }
    }

    private void panggilAnotasi(Object instans, Class<? extends java.lang.annotation.Annotation> anotasi)
            throws Exception {
        for (Method m : instans.getClass().getDeclaredMethods()) {
            if (m.isAnnotationPresent(anotasi)) {
                m.setAccessible(true);
                m.invoke(instans);
            }
        }
    }

    void tutup() {
        List<String> urutan = new ArrayList<>(singleton.keySet());
        for (int i = urutan.size() - 1; i >= 0; i--) {
            String nama = urutan.get(i);
            Object instans = singleton.get(nama);
            System.out.println("[Siklus] " + nama + ": tahap hancur");
            try {
                panggilAnotasi(instans, PraHapus.class);
            } catch (Exception e) {
                throw new IllegalStateException("Gagal menghancurkan " + nama);
            }
        }
        System.out.println("[Siklus] container ditutup");
    }
}

class PenyimpanSesi {

    @Nilai("64")
    private int kapasitas;

    private final Map<String, String> sesi = new LinkedHashMap<>();

    @PascaBuat
    void mulai() {
        System.out.println("[Siklus] @PostConstruct PenyimpanSesi siap (kapasitas=" + kapasitas + ")");
    }

    @PraHapus
    void berhenti() {
        System.out.println("[Siklus] @PreDestroy membersihkan " + sesi.size() + " sesi");
        sesi.clear();
    }

    void simpan(String kunci, String nilai) {
        sesi.put(kunci, nilai);
    }

    String ambil(String kunci) {
        return sesi.get(kunci);
    }
}

class PembuatId {

    private static int pembuatke = 0;

    @PascaBuat
    void mulai() {
        pembuatke++;
        System.out.println("[Siklus] @PostConstruct instance PembuatId ke-" + pembuatke);
    }

    long baru() {
        return System.nanoTime(); // tidak dicetak: nilai tidak deterministik
    }
}