Eğitim Portalı/Spring/Dependency Injection (Bağımlılık Enjeksiyonu)
Spring02-spring/01-dependency-injection

Dependency Injection (Bağımlılık Enjeksiyonu)

Önceki bölümde IoC felsefesini ve container'ın nesneleri bizim yerimize oluşturup bağladığını gördük. Şimdi bu bağlamanın nasıl yapıldığına yakından bakıyoruz. Dependency Injection (DI), bir nesnenin ihtiyaç duyduğu b…

Dependency Injection (Bağımlılık Enjeksiyonu)

Önceki bölümde IoC felsefesini ve container'ın nesneleri bizim yerimize oluşturup bağladığını gördük. Şimdi bu bağlamanın nasıl yapıldığına yakından bakıyoruz. Dependency Injection (DI), bir nesnenin ihtiyaç duyduğu bağımlılıkların ona dışarıdan verilmesidir. Spring üç farklı enjeksiyon türü sunar — ve hangisini seçtiğin, kodunun kalitesini doğrudan etkiler.

@Autowired ve enjeksiyon noktaları

Spring, bir bean'in bağımlılığını çözmek için @Autowired anotasyonunu kullanır (constructor, setter veya alan üzerinde). Bu, container'a "buraya uygun tipte bir bean enjekte et" der. Modern Spring'de tek constructor varsa @Autowired yazmaya bile gerek yoktur; Spring onu otomatik kullanır.

Not: Örneklerde, tek dosyalık çalışma senaryosu nedeniyle bean'leri ctx.register(...) ile doğrudan kaydediyoruz. Gerçek uygulamada bu sınıflar @ComponentScan ile otomatik bulunur (bir sonraki bölümde göreceğiz).

1) Constructor injection (önerilen)

Bağımlılığı constructor parametresi olarak almaktır ve Spring'in önerdiği yoldur:

@Service
class UrunServisi {
    private final UrunDeposu depo;        // final olabilir!
    UrunServisi(UrunDeposu depo) { this.depo = depo; }
}

Avantajları (Örnek 1, ./Ornek1.java):

  • Değişmezlik: Alanlar final olabilir → güvenli, durumu değişmeyen nesne.
  • Açık ve zorunlu bağımlılıklar: Nesne, bağımlılıkları olmadan var olamaz; eksik kurulum derleme/başlatma anında belli olur, çalışma anında NullPointerException ile patlamaz.
  • Test edilebilirlik: Testte bağımlılığı doğrudan constructor'a verirsin — Spring'e bile ihtiyaç kalmaz.

2) Setter injection

Bağımlılık, bir setter metodu aracılığıyla, nesne kurulduktan sonra enjekte edilir:

@Autowired
void setEposta(EpostaServisi eposta) { this.eposta = eposta; }

Opsiyonel bağımlılıklar veya çalışma sırasında yeniden yapılandırma gereken durumlar için uygundur. Dezavantajı: alan final olamaz ve nesne, setter çağrılana kadar yarım kurulu kalır.

3) Field injection

Bağımlılık doğrudan alana (reflection ile) yazılır:

@Autowired
private EpostaServisi eposta;

En kısa görünen ama en sorunlu yöntemdir: alan final olamaz, bağımlılık "gizlidir" (constructor'a bakarak göremezsin), ve Spring olmadan birim testi yapmak zorlaşır (reflection gerekir). Örnek 2 (./Ornek2.java) setter ve field injection'ı yan yana gösterir.

Pratik kural: Zorunlu bağımlılıklar için constructor injection kullan. Field injection'dan kaçın; setter injection'ı yalnızca gerçekten opsiyonel bağımlılıklar için sakla.

Belirsizliği çözmek: @Primary ve @Qualifier

Bir arayüzün (Bildirimci) birden çok uygulaması (e-posta, SMS) bean olarak tanımlıysa, Spring hangisini enjekte edeceğini bilemez ve hata verir. İki çözüm vardır:

  • @Primary: "Eşit adaylar arasında varsayılan olarak BENİ seç."
  • @Qualifier("ad"): "Tam olarak şu isimli bean'i ver" — açık ve nokta atışı seçim.
@Component @Primary class EpostaBildirimci implements Bildirimci { ... }
@Component("sms")    class SmsBildirimci    implements Bildirimci { ... }

AcilServis(@Qualifier("sms") Bildirimci b) { ... } // açıkça SMS

Örnek 3 (./Ornek3.java) bir bildirim senaryosunda her ikisini de gösterir: varsayılan servis @Primary olan e-postayı, acil servis ise @Qualifier ile SMS'i alır.

Özet

Dependency Injection'ın üç türünü gördük: constructor (önerilen — final, zorunlu, test edilebilir), setter (opsiyonel bağımlılıklar) ve field (kaçınılması gereken). Aynı tipte birden çok bean olduğunda belirsizliği @Primary ve @Qualifier ile çözdük. Bağımlılıkların nasıl verildiğini öğrendiğimize göre, sırada bu bean'lerin container içindeki yaşam döngüsü ve kapsamları (scope) var.

Kod Örnekleri(3)

Ornek1

ortam gerekir
Ornek1.java
1// Ornek1: Constructor injection — Spring'in ÖNERDİĞİ enjeksiyon türü.
2// @Service/@Repository sınıflarını container'a kaydedip otomatik bağlamasını izliyoruz.
3// Çalıştırma: java Ornek1.java   (Spring classpath portal tarafından sağlanır)
4import org.springframework.context.annotation.AnnotationConfigApplicationContext;
5import org.springframework.stereotype.Repository;
6import org.springframework.stereotype.Service;
7
8public class Ornek1 {
9
10    public static void main(String[] args) {
11        // @ComponentScan yerine sınıfları doğrudan kaydediyoruz (tek dosya senaryosu).
12        // Gerçek uygulamada @ComponentScan bu sınıfları otomatik bulur.
13        var ctx = new AnnotationConfigApplicationContext();
14        ctx.register(UrunServisi.class, UrunDeposu.class);
15        ctx.refresh();
16
17        UrunServisi servis = ctx.getBean(UrunServisi.class);
18        servis.urunEkle("Klavye");
19        servis.listele();
20
21        ctx.close();
22
23        System.out.println("""
24
25                --- Neden constructor injection? ---
26                1) Alanlar 'final' olabilir -> değişmez (immutable), güvenli nesne.
27                2) Zorunlu bağımlılıklar AÇIKTIR: nesne, eksiksiz kurulmadan var olamaz.
28                3) Test kolaydır: testte bağımlılığı doğrudan constructor'a verirsin (Spring bile gerekmez).
29                Spring 4.3+ ile tek constructor varsa @Autowired yazmaya bile gerek yoktur.""");
30    }
31}
32
33@Repository
34class UrunDeposu {
35    private final java.util.List<String> kayitlar = new java.util.ArrayList<>();
36    void kaydet(String urun) { kayitlar.add(urun); }
37    java.util.List<String> hepsi() { return kayitlar; }
38}
39
40@Service
41class UrunServisi {
42    // CONSTRUCTOR INJECTION: bağımlılık final, zorunlu ve açık.
43    private final UrunDeposu depo;
44
45    // Tek constructor olduğu için @Autowired ZORUNLU değil; Spring otomatik enjekte eder.
46    UrunServisi(UrunDeposu depo) {
47        this.depo = depo;
48        System.out.println("UrunServisi kuruldu; depo enjekte edildi: " + (depo != null));
49    }
50
51    void urunEkle(String urun) { depo.kaydet(urun); System.out.println("Eklendi: " + urun); }
52    void listele() { System.out.println("Tüm ürünler: " + depo.hepsi()); }
53}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek1.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek2

ortam gerekir
Ornek2.java
1// Ornek2: Setter ve field injection — alternatifler ve neden constructor kadar iyi olmadıkları.
2// Çalıştırma: java Ornek2.java
3import org.springframework.beans.factory.annotation.Autowired;
4import org.springframework.context.annotation.AnnotationConfigApplicationContext;
5import org.springframework.stereotype.Component;
6import org.springframework.stereotype.Service;
7
8public class Ornek2 {
9
10    public static void main(String[] args) {
11        var ctx = new AnnotationConfigApplicationContext();
12        ctx.register(EpostaServisi.class, SetterServisi.class, FieldServisi.class);
13        ctx.refresh();
14
15        System.out.println("Setter injection:");
16        ctx.getBean(SetterServisi.class).calistir();
17
18        System.out.println("\nField injection:");
19        ctx.getBean(FieldServisi.class).calistir();
20
21        ctx.close();
22
23        System.out.println("""
24
25                --- Karşılaştırma ---
26                Setter injection: bağımlılık SONRADAN değiştirilebilir (opsiyonel bağımlılıklar için uygun)
27                                  ama alan 'final' olamaz ve nesne yarım kurulu bir an yaşayabilir.
28                Field injection : en kısa görünür AMA en sorunlusudur:
29                                  - alan 'final' olamaz, gizli bağımlılık (constructor'a bakınca görünmez),
30                                  - Spring olmadan test etmek zordur (reflection gerekir).
31                ÖNERİ: Zorunlu bağımlılıklar için CONSTRUCTOR injection kullan (Örnek 1).""");
32    }
33}
34
35@Component
36class EpostaServisi {
37    void gonder(String m) { System.out.println("  [e-posta] " + m); }
38}
39
40@Service
41class SetterServisi {
42    private EpostaServisi eposta; // final OLAMAZ
43
44    // SETTER INJECTION: Spring, kurulumdan sonra bu setter'ı çağırıp enjekte eder.
45    @Autowired
46    void setEposta(EpostaServisi eposta) { this.eposta = eposta; }
47
48    void calistir() { eposta.gonder("setter ile enjekte edildi"); }
49}
50
51@Service
52class FieldServisi {
53    // FIELD INJECTION: Spring değeri doğrudan alana (reflection ile) yazar.
54    @Autowired
55    private EpostaServisi eposta; // final OLAMAZ, gizli bağımlılık
56
57    void calistir() { eposta.gonder("field ile enjekte edildi"); }
58}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek2.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek3

ortam gerekir
Ornek3.java
1// Ornek3: Aynı tipte birden çok bean — belirsizliği @Primary ve @Qualifier ile çözmek.
2// Gerçek senaryo: e-posta ve SMS aynı arayüzü uygular; Spring hangisini enjekte etsin?
3// Çalıştırma: java Ornek3.java
4import org.springframework.beans.factory.annotation.Qualifier;
5import org.springframework.context.annotation.AnnotationConfigApplicationContext;
6import org.springframework.context.annotation.Primary;
7import org.springframework.stereotype.Component;
8import org.springframework.stereotype.Service;
9
10public class Ornek3 {
11
12    public static void main(String[] args) {
13        var ctx = new AnnotationConfigApplicationContext();
14        ctx.register(EpostaBildirimci.class, SmsBildirimci.class,
15                VarsayilanServis.class, AcilServis.class);
16        ctx.refresh();
17
18        // VarsayilanServis, @Primary olan bildirimciyi (e-posta) alır.
19        System.out.print("Varsayılan servis -> ");
20        ctx.getBean(VarsayilanServis.class).bildir("Hoş geldiniz");
21
22        // AcilServis, @Qualifier ile AÇIKÇA SMS'i ister.
23        System.out.print("Acil servis       -> ");
24        ctx.getBean(AcilServis.class).bildir("Sunucu çöktü!");
25
26        ctx.close();
27
28        System.out.println("""
29
30                --- Belirsizlik (ambiguity) ---
31                Bildirimci tipinde İKİ bean var (e-posta, SMS). Spring hangisini seçeceğini bilemez.
32                Çözüm:
33                  @Primary  -> "eşit adaylar varsa BENİ seç" (varsayılan tercih).
34                  @Qualifier-> "tam olarak ŞU isimli bean'i ver" (açık seçim).""");
35    }
36}
37
38interface Bildirimci { void gonder(String mesaj); }
39
40@Component
41@Primary // birden çok aday olduğunda varsayılan olarak bu seçilir
42class EpostaBildirimci implements Bildirimci {
43    public void gonder(String m) { System.out.println("[e-posta] " + m); }
44}
45
46@Component("sms") // bean adı: "sms"
47class SmsBildirimci implements Bildirimci {
48    public void gonder(String m) { System.out.println("[SMS] " + m); }
49}
50
51@Service
52class VarsayilanServis {
53    private final Bildirimci bildirimci;
54    VarsayilanServis(Bildirimci bildirimci) { this.bildirimci = bildirimci; } // @Primary geldi
55    void bildir(String m) { bildirimci.gonder(m); }
56}
57
58@Service
59class AcilServis {
60    private final Bildirimci bildirimci;
61    // @Qualifier ile tam olarak "sms" bean'ini istiyoruz.
62    AcilServis(@Qualifier("sms") Bildirimci bildirimci) { this.bildirimci = bildirimci; }
63    void bildir(String m) { bildirimci.gonder(m); }
64}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek3.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.