Pada Chapter sebelumnya kita melihat betapa rapuhnya kelas yang merakit dependensinya sendiri. Sekarang kita masuk ke mesin penggeraknya: di dalam aplikasi Spring, ada satu pihak yang bertugas menyambungkan seluruh objek — ApplicationContext, implementasi utama IoC container Spring. Container ini membaca definisi bean, memutuskan urutan pembuatan, menuntaskan injeksi, lalu menyerahkan objek siap pakai kepada kode kita. Pada Chapter ini kita bedah tiga gaya injeksi, alasan constructor injection menjadi standar industri, dan cara container menyelesaikan graf dependensi secara rekursif.
ApplicationContext sebagai Container IoC #
ApplicationContext adalah evolusi dari BeanFactory yang lebih tua; selain menyimpan bean, container ini juga menangani event, internacionalisasi, dan integrasi AOP. Siklus kerjanya sederhana pada intinya: aplikasi memulai container, container membaca bean definition (hasil pemindaian anotasi atau method @Bean), lalu container mengonstruksi setiap bean beserta seluruh dependensinya. Ketika sebuah bean membutuhkan bean lain yang belum ada, container menyelesaikan dependensi itu lebih dulu — perilaku recursive resolution inilah yang membuat kita cukup mendaftarkan bean paling atas dan biarkan sisanya tersambung otomatis.
Suntikan ini biasanya tidak terlihat karena terjadi lewat konstruktor. Perhatikan potongan Spring berikut yang ditampilkan apa adanya untuk Spring Boot 3.4/3.5:
@Service
public class NotifikasiService {
private final PengirimPesan pengirim;
private final KatalogProduk katalog;
public NotifikasiService(PengirimPesan pengirim, KatalogProduk katalog) {
this.pengirim = pengirim;
this.katalog = katalog;
}
}Kode NotifikasiService tidak berisi satu pun new untuk dependensinya. Saat container Spring menemukan bean ini, container membaca tipe parameter konstruktor, mencari bean PengirimPesan dan KatalogProduk di registry, lalu memanggil konstruktor dengan bean tersebut.
Tiga Gaya Injeksi dan Pemenangnya #
Spring mengenal tiga tempat suntikan dependensi. Tabel berikut membandingkannya:
| Gaya | Bentuk | Catatan |
|---|---|---|
| Constructor injection | parameter konstruktor | dependensi wajib, field bisa final, mudah diuji |
| Setter injection | method setX(...) bertanda @Autowired | dependensi opsional atau bisa berubah |
| Field injection | field bertanda @Autowired | ringkas ditulis, menyulitkan pengujian tanpa container |
Constructor injection menjadi anjuran resmi dokumentasi Spring karena tiga alasan teknis. Pertama, field final menjamin objek tidak pernah berada di kondisi setengah terisi. Kedua, kelas dengan konstruktor penuh bisa diuji tanpa Spring sama sekali — cukup new NotifikasiService(pengirimPalsu, katalogPalsu) dengan test double. Ketiga, banyaknya parameter konstruktor menjadi sinyal alami bahwa sebuah kelas mulai memikul tanggung jawab berlebih dan pantas dipecah. Field injection menyembunyikan sinyal itu; setter injection hanya berguna untuk dependensi yang sungguh opsional.
Mensimulasikan Resolusi Container #
Untuk melihat mekanisme resolusi secara transparan, kita bangun container mini dengan Java murni: registry berbentuk Map<Class<?>, Object> dan resolusi konstruktor lewat reflection — persis inti dari apa yang dilakukan ApplicationContext:
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class WadahIoC {
public static void main(String[] args) {
WadahIoC wadah = new WadahIoC();
// Cukup minta bean paling atas; dependensinya diselesaikan otomatis.
NotifikasiService layanan = wadah.ambil(NotifikasiService.class);
layanan.pesananBaru("Budi", "Kopi Gayo 250g");
System.out.println();
System.out.println("Ambil lagi: bean yang sama dikembalikan (singleton)");
NotifikasiService layananLagi = wadah.ambil(NotifikasiService.class);
System.out.println("Instans identik: " + (layanan == layananLagi));
}
private final Map<Class<?>, Object> bean = new HashMap<>();
@SuppressWarnings("unchecked")
<T> T ambil(Class<T> tipe) {
if (bean.containsKey(tipe)) {
return (T) bean.get(tipe);
}
T instans = (T) cipta(tipe);
bean.put(tipe, instans);
System.out.println("[Wadah] bean siap: " + tipe.getSimpleName());
return instans;
}
private Object cipta(Class<?> tipe) {
Constructor<?> konstruktor = tipe.getDeclaredConstructors()[0];
Class<?>[] tipeParam = konstruktor.getParameterTypes();
Object[] argumen = new Object[tipeParam.length];
for (int i = 0; i < tipeParam.length; i++) {
System.out.println("[Wadah] " + tipe.getSimpleName() + " butuh "
+ tipeParam[i].getSimpleName() + " -> selesaikan lebih dulu");
argumen[i] = ambil(tipeParam[i]);
}
try {
return konstruktor.newInstance(argumen);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Gagal menciptakan " + tipe.getSimpleName());
}
}
}
class KatalogProduk {
private final Map<String, Long> harga = new HashMap<>();
KatalogProduk() {
harga.put("Kopi Gayo 250g", 85_000L);
System.out.println("[KatalogProduk] konstruktor berjalan");
}
long harga(String nama) {
return harga.getOrDefault(nama, 0L);
}
}
class PengirimLog {
PengirimLog() {
System.out.println("[PengirimLog] konstruktor berjalan");
}
void tulis(String pesan) {
System.out.println("[LOG] " + pesan);
}
}
class NotifikasiService {
private final KatalogProduk katalog;
private final PengirimLog log;
NotifikasiService(KatalogProduk katalog, PengirimLog log) {
this.katalog = katalog;
this.log = log;
}
void pesananBaru(String pembeli, String produk) {
log.tulis("Pesanan " + pembeli + ": " + produk
+ " Rp" + katalog.harga(produk));
}
}Output yang Dihasilkan:
[Wadah] NotifikasiService butuh KatalogProduk -> selesaikan lebih dulu
[KatalogProduk] konstruktor berjalan
[Wadah] bean siap: KatalogProduk
[Wadah] NotifikasiService butuh PengirimLog -> selesaikan lebih dulu
[PengirimLog] konstruktor berjalan
[Wadah] bean siap: PengirimLog
[Wadah] bean siap: NotifikasiService
[LOG] Pesanan Budi: Kopi Gayo 250g Rp85000
Ambil lagi: bean yang sama dikembalikan (singleton)
Instans identik: trueProgram di atas hanya mendaftarkan satu permintaan, NotifikasiService, tetapi jejaknya menunjukkan tiga bean tercipta. Mari kita bedah bagian kuncinya:
<T> T ambil(Class<T> tipe): method ini adalah pintu masuk alagetBean(). Jika bean sudah ada di map, instans lama dikembalikan langsung — itulah mengapa pengambilan kedua menghasilkanInstans identik: true, perilaku scope singleton.cipta(Class<?> tipe): method membaca konstruktor pertama kelas, lalu untuk setiap parameter mencetak kebutuhan dan memanggilambilsecara rekursif. BarisNotifikasiService butuh KatalogProduk -> selesaikan lebih dulumembuktikan urutan penyelesaian: dependensi selalu selesai sebelum dependen.konstruktor.newInstance(argumen): reflection membangun objek dengan argumen yang sudah terkumpul — teknologi yang sama dipakai Spring untuk melakukan injeksi constructor tanpa mengubah bytecode kelas kita.KatalogProduk()danPengirimLog(): dua kelas ini tidak saling mengetahui, danNotifikasiServicetidak tahu cara membangun keduanya. Seluruh pengetahuan perakitan dipindahkan ke satu tempat: container.
@Autowired dan Batasnya #
Anotasi @Autowired memberi tahu Spring bahwa sebuah anggota kelas perlu diisi container. Pada constructor injection anotasi ini bahkan opsional sejak Spring 4.3: konstruktor tunggal otomatis dipakai asalkan kelas adalah bean. Kebutuhan @Autowired muncul kembali pada field injection, setter injection, atau saat ada lebih dari satu konstruktor. Ketika dua bean mengimplementasikan satu antarmuka, container menolak menebak dan melempar NoUniqueBeanDefinitionException; penyelesaiannya adalah @Primary pada kandidat utama, @Qualifier("namaBean") pada titik injeksi, atau penamaan parameter sesuai nama bean.
Mekanisme injeksi hanyalah separuh cerita container. Bean tidak berhenti pada konstruktor — ia punya siklus hidup lengkap dari instansiasi hingga pemusnahan, yang kita telusuri pada Chapter Spring Container dan Beans.
Referensi #
- Spring Framework Reference: Dependency Injection
- Spring Framework Reference: The IoC Container
- Spring Guides
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
WadahIoC.java
Unduh berkasimport java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class WadahIoC {
public static void main(String[] args) {
WadahIoC wadah = new WadahIoC();
// Cukup minta bean paling atas; dependensinya diselesaikan otomatis.
NotifikasiService layanan = wadah.ambil(NotifikasiService.class);
layanan.pesananBaru("Budi", "Kopi Gayo 250g");
System.out.println();
System.out.println("Ambil lagi: bean yang sama dikembalikan (singleton)");
NotifikasiService layananLagi = wadah.ambil(NotifikasiService.class);
System.out.println("Instans identik: " + (layanan == layananLagi));
}
private final Map<Class<?>, Object> bean = new HashMap<>();
@SuppressWarnings("unchecked")
<T> T ambil(Class<T> tipe) {
if (bean.containsKey(tipe)) {
return (T) bean.get(tipe);
}
T instans = (T) cipta(tipe);
bean.put(tipe, instans);
System.out.println("[Wadah] bean siap: " + tipe.getSimpleName());
return instans;
}
private Object cipta(Class<?> tipe) {
Constructor<?> konstruktor = tipe.getDeclaredConstructors()[0];
Class<?>[] tipeParam = konstruktor.getParameterTypes();
Object[] argumen = new Object[tipeParam.length];
for (int i = 0; i < tipeParam.length; i++) {
System.out.println("[Wadah] " + tipe.getSimpleName() + " butuh "
+ tipeParam[i].getSimpleName() + " -> selesaikan lebih dulu");
argumen[i] = ambil(tipeParam[i]);
}
try {
return konstruktor.newInstance(argumen);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Gagal menciptakan " + tipe.getSimpleName());
}
}
}
class KatalogProduk {
private final Map<String, Long> harga = new HashMap<>();
KatalogProduk() {
harga.put("Kopi Gayo 250g", 85_000L);
System.out.println("[KatalogProduk] konstruktor berjalan");
}
long harga(String nama) {
return harga.getOrDefault(nama, 0L);
}
}
class PengirimLog {
PengirimLog() {
System.out.println("[PengirimLog] konstruktor berjalan");
}
void tulis(String pesan) {
System.out.println("[LOG] " + pesan);
}
}
class NotifikasiService {
private final KatalogProduk katalog;
private final PengirimLog log;
NotifikasiService(KatalogProduk katalog, PengirimLog log) {
this.katalog = katalog;
this.log = log;
}
void pesananBaru(String pembeli, String produk) {
log.tulis("Pesanan " + pembeli + ": " + produk
+ " Rp" + katalog.harga(produk));
}
}