İçeriğe geç

IoC ve Dependency Injection

Başlangıç 9 dk Çok sık karşılaşılır

30 saniyede özet

Bir sınıf ihtiyaç duyduğu nesneleri kendisi yaratmaz, dışarıdan ister; Spring de onları getirip verir. Bunları constructor ile istemek en güvenlisi: bir döngü varsa uygulama açılırken hemen fark edersin.

Bir usta işe başlamadan önce kendi çekicini dövmez; çekiç ona verilir. Kodda da böyle: sınıf ihtiyaç duyduğu nesneyi kendisi yaratmaz, dışarıdan alır. Bunun adı Inversion of Control.

Aracını kendisi yapan usta ile aracı eline verilen usta.
Adım adım oku
  1. OrderService ödeme istemcisini kendisi yaratıyor: new StripeClient(). Araç sınıfa kaynaklı.
  2. Başka bir istemci kullanmak, örneğin testte sahte bir tane, sınıfın içini değiştirmeyi gerektirir.
  3. Dependency injection'da sınıf aracını constructor'da ister; Spring container onu bulup verir.
  4. Testte container'a sahte istemciyi vermek yeter. OrderService'in tek satırı bile değişmez.
  1. Bayt: OrderService'im ödeme istemcisini kendisi yaratıyor: new StripePaymentClient().

  2. Sen: Testte gerçek para çekmek istemiyorum. Ne yapacağım?

  3. Bayt: Sınıfın içini değiştirmeden değiştiremezsin; araç sınıfa kaynaklanmış!

  4. Bayt: Usta çekicini dövmez, çekiç ona verilir. Spring de tam olarak bunu yapıyor.

Kontrol sınıfta
public class OrderService {
// Bu sınıf hangi implementasyonu kullanacağına kendi karar veriyor
private final PaymentClient client = new StripePaymentClient();
}
Kontrol dışarıda — IoC
public class OrderService {
private final PaymentClient client;
public OrderService(PaymentClient client) { // kim olduğunu bilmiyor, verilmesini bekliyor
this.client = client;
}
}

İkinci sürüm testte sahte bir istemciyle, üretimde gerçeğiyle çalışır. Dependency Injection, IoC’yi uygulama yöntemidir; Spring container ise bunu yapan şey.

Spring perde arkasında ne yapıyor?

OrderService, constructor'ında bir PaymentClient istiyor. Spring bu ikisinden hangisini önce kurar? Cevabı göster

PaymentClient. OrderService’e verilecek nesne önce hazır olmalı.

Üç adım:

  1. Component scan — @Component, @Service, @Repository, @Controller işaretli sınıfları bul.
  2. Çözümleme — her beanYaşam döngüsünü Spring konteynerinin yönettiği nesne. Sen `new` demezsin; konteyner kurar, bağımlılıklarını verir ve gerekirse yok eder.Sözlükte gör →’in neye ihtiyacı olduğunu çıkar, bağımlılık grafiğini oluştur.
  3. Oluşturma ve enjeksiyon — bağımlılığı olmayanlardan başlayarak yukarı doğru nesneleri üret.

Kurulum sırası, sınıfları yazdığın sıra değil: önce kimseye ihtiyacı olmayanlar, en son en çok şeye ihtiyacı olanlar kurulur.

Kafam karıştı, daha basit anlat

Spring önce bütün parçaların listesini çıkarır, sonra kimin kime ihtiyacı olduğuna bakar. Kimseye ihtiyacı olmayanı önce kurar, en çok şeye ihtiyacı olanı en son.

Hızlı kontrolBaşlangıç

Dependency injection'ın temel fikri nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin izle

Varsayılan ayarda grafikte kasıtlı bir döngü var: OrderService → PaymentClient → OrderService.

Spring container — component scan, çözümleme ve enjeksiyon

Tohum 2
component scanconstructordöngü açık
  • OrderControllerbulunmadı

    ihtiyacı: OrderService

  • OrderServicebulunmadı

    ihtiyacı: PaymentClient, OrderRepository

  • PaymentClientbulunmadı

    ihtiyacı: RestClientConfig, OrderService

  • OrderRepositorybulunmadı

    ihtiyacı: DataSource

  • RestClientConfigbulunmadı
  • DataSourcebulunmadı
Hız
Adım 0

Şu an ne oldu?

Component scan — 0/6

Spring, ana sınıfın paketinden başlayarak @Component, @Service, @Repository ve @Controller işaretli sınıfları arıyor. Henüz hiçbir nesne oluşturulmadı.

Aklında kalsın: @SpringBootApplication kendi paketini ve altındakileri tarar. Bean’in bulunamamasının en sık sebebi, sınıfın bu paket ağacının dışında kalmasıdır.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

İki ayarı karşılaştır — bu dersin tamamı bu karşılaştırmada:

  • constructor + döngü açık — container hiç başlayamıyor. BeanCurrentlyInCreationException ve döngü zinciri ekrana basılıyor.
  • field + döngü açık — container başlıyor. Bean’ler önce boş kurucuyla oluşturuluyor, sonra alanlar dolduruluyor. Döngü hâlâ orada, sadece görünmüyor.

Sonra:

  • Döngüyü kapat, constructor’da kal. Oluşturma sırasına bak: RestClientConfig ve DataSource önce, OrderController en son. Yapraklardan köke.
  • @Lazy kutusunu işaretle. Döngü açık ama container başlıyor: Spring araya bir proxySpring'in bean'in yerine koyduğu sarmalayıcı nesne. `@Transactional` gibi anotasyonlar burada çalışır — çağrı proxy'den geçmezse hiçbir şey olmaz.Sözlükte gör → koyuyor ve gerçek bean’i ilk kullanımda çözüyor.
Hızlı kontrolOrta

Bu Spring uygulaması açılışta ne yazdırır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
SingletonKanit.java
1@Component
2class OrderService {
3 OrderService() { System.out.println("kuruldu"); }
4}
5
6@Component
7class A { A(OrderService s) { System.out.println("A: " + s.hashCode()); } }
8
9@Component
10class B { B(OrderService s) { System.out.println("B: " + s.hashCode()); } }
Java 21UTF-8LF

Constructor injection neden öneriliyor

ConstructorField (@Autowired alan)
Alan final olabilir miEvetHayır
Nesne oluştuğu anda geçerli miEvetHayır — alanlar bir süre null
Döngüsel bağımlılıkAçılışta patlarSessizce çözülür
Spring’siz testnew OrderService(mock)Reflection gerekir
Çok fazla bağımlılıkKurucu şişer — görünür uyarıAlan eklemek bedava — uyarı yok

Son satır gözden kaçar ama önemlidir: sekiz parametreli bir kurucu çirkindir ve sınıfın çok iş yaptığını görünür kılar. Sekiz @Autowired alan aynı şeyi söyler ama kimse fark etmez.

Aşağıdaki örnek bir bankanın para transferi servisinden: aynı arayüzün iki uygulaması (FAST ve EFT) sırayla denenir, dolandırıcılık kontrolü profile göre değişir ve servis Spring’siz test edilir.

Derinleş · Bankada para transferi: bağımlılıklar nereden geliyor? 6 dosya · ~171 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/main/java/com/bank/transfer/ TransferService.java Servis yalnızca arayüzlere, ayarlara ve Clock'a bağımlı; hepsi constructor'dan, hepsi final.

src/main/java/com/bank/transfer/TransferService.java
@Service
public class TransferService {
private final List<TransferRail> rails; // every TransferRail bean, in @Order order
private final FraudCheck fraud;
private final Clock clock;
// One constructor: no @Autowired needed. Fields are final, the object is complete after `new`.
public TransferService(List<TransferRail> rails, FraudCheck fraud, Clock clock) {
this.rails = rails;
this.fraud = fraud;
this.clock = clock;
}
public TransferResult send(TransferOrder order) {
if (fraud.score(order) == Risk.BLOCK) {
return TransferResult.rejected(order.id(), "FRAUD_SUSPECTED");
}
ZonedDateTime now = ZonedDateTime.now(clock);
// First rail that can carry this amount at this moment wins: FAST, then EFT.
return rails.stream()
.filter(rail -> rail.accepts(order, now))
.findFirst()
.map(rail -> rail.submit(order))
.orElseGet(() -> TransferResult.scheduled(order.id(), "NEXT_BUSINESS_DAY"));
}
}

src/main/java/com/bank/transfer/ TransferRail.java Sözleşme: servis FAST mı EFT mi kullanıldığını bilmez, yalnızca 'bu transferi taşıyabilir misin?' diye sorar.

src/main/java/com/bank/transfer/TransferRail.java
public interface TransferRail {
boolean accepts(TransferOrder order, ZonedDateTime now);
TransferResult submit(TransferOrder order);
}
// Amounts are BigDecimal, never double: 0.1 + 0.2 != 0.3 is not a rounding error a bank can afford.
public record TransferOrder(UUID id, String fromIban, String toIban, BigDecimal amount, String description) { }

src/main/java/com/bank/transfer/ Rails.java Aynı arayüzün bütün uygulamaları bir liste olarak gelir; önce FAST denensin diye sıra @Order ile verilir.

src/main/java/com/bank/transfer/Rails.java
@Component
@Order(1) // tried first: instant, 7/24, but only up to a per-transaction limit
class FastRail implements TransferRail {
private final FastClient client;
private final BigDecimal maxAmount;
// The limit is set by the scheme operator and changes over time, so it lives in config.
FastRail(FastClient client, @Value("${transfer.fast.max-amount}") BigDecimal maxAmount) {
this.client = client;
this.maxAmount = maxAmount;
}
@Override
public boolean accepts(TransferOrder order, ZonedDateTime now) {
return order.amount().compareTo(maxAmount) <= 0;
}
@Override
public TransferResult submit(TransferOrder order) {
return client.send(order);
}
}
@Component
@Order(2) // fallback: no amount limit, but only inside the business-day window
class EftRail implements TransferRail {
private final EftClient client;
private final BusinessCalendar calendar;
EftRail(EftClient client, BusinessCalendar calendar) {
this.client = client;
this.calendar = calendar;
}
@Override
public boolean accepts(TransferOrder order, ZonedDateTime now) {
return calendar.isEftOpen(now); // weekends and public holidays included
}
@Override
public TransferResult submit(TransferOrder order) {
return client.send(order);
}
}

src/main/java/com/bank/transfer/ FraudChecks.java İki uygulama, profil ile seçilir: gerçek skor servisi dev dışında, her şeye izin veren sahtesi yalnızca dev'de.

src/main/java/com/bank/transfer/FraudChecks.java
public interface FraudCheck {
Risk score(TransferOrder order);
}
@Component
@Profile("!dev") // everywhere except a developer laptop
class ScoringFraudCheck implements FraudCheck {
private final FraudScoringClient scoring;
ScoringFraudCheck(FraudScoringClient scoring) {
this.scoring = scoring;
}
@Override
public Risk score(TransferOrder order) {
return scoring.evaluate(order.fromIban(), order.toIban(), order.amount());
}
}
@Component
@Profile("dev") // exactly one FraudCheck exists per profile, so no @Primary is needed
class AllowAllFraudCheck implements FraudCheck {
@Override
public Risk score(TransferOrder order) {
return Risk.ALLOW;
}
}

src/main/java/com/bank/config/ TimeConfig.java Kendi yazmadığın sınıflar (Clock gibi) @Bean ile konteynere girer; tutar sınırı gibi değerler koda değil ayara yazılır.

src/main/java/com/bank/config/TimeConfig.java
@Configuration
class TimeConfig {
// Clock is a JDK class: no @Component to put on it, so a @Bean method registers it.
// Cut-off times depend on "now"; an injected Clock makes them testable.
@Bean
Clock clock() {
return Clock.system(ZoneId.of("Europe/Istanbul"));
}
}

src/test/java/com/bank/transfer/ TransferServiceTest.java Spring yok: new ile kurulur, sahte bağımlılıklar elle verilir. Constructor injection'ın asıl kazancı.

src/test/java/com/bank/transfer/TransferServiceTest.java
class TransferServiceTest {
// A Saturday: the EFT window is closed.
private final Clock saturday = Clock.fixed(Instant.parse("2026-09-26T10:00:00Z"), ZoneOffset.UTC);
@Test
void bigTransferOnSaturdayWaitsForTheNextBusinessDay() {
var fast = new FakeRail(false); // over the FAST limit
var eft = new FakeRail(false); // window closed
var service = new TransferService(List.of(fast, eft), order -> Risk.ALLOW, saturday);
TransferResult result = service.send(order("250000.00"));
assertThat(result.status()).isEqualTo(TransferStatus.SCHEDULED);
assertThat(fast.submitted).isEmpty();
assertThat(eft.submitted).isEmpty();
}
@Test
void fraudBlockNeverReachesARail() {
var fast = new FakeRail(true);
var service = new TransferService(List.of(fast), order -> Risk.BLOCK, saturday);
assertThat(service.send(order("100.00")).status()).isEqualTo(TransferStatus.REJECTED);
assertThat(fast.submitted).isEmpty();
}
// Hand-written fakes and a lambda for FraudCheck: no Mockito, no Spring context, milliseconds.
static final class FakeRail implements TransferRail {
final boolean open;
final List<TransferOrder> submitted = new ArrayList<>();
FakeRail(boolean open) { this.open = open; }
@Override
public boolean accepts(TransferOrder order, ZonedDateTime now) { return open; }
@Override
public TransferResult submit(TransferOrder order) {
submitted.add(order);
return TransferResult.sent(order.id());
}
}
private static TransferOrder order(String amount) {
return new TransferOrder(UUID.randomUUID(), "TR330006100519786457841326",
"TR320010009999901234567890", new BigDecimal(amount), "kira");
}
}
Kafam karıştı, daha basit anlat

İhtiyaçlarını constructor’da iste: sınıf eksik parçayla hiç kurulamaz. Testte de Spring’e gerek kalmaz, parçaları kendin verirsin.

Hızlı kontrolBaşlangıç

Constructor injection'da `@Autowired` yazmak zorunlu mu?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Constructor injection ile field injection arasındaki en önemli pratik fark nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Döngüsel bağımlılığın doğru çözümü. @Lazy ve field injection döngüyü çözmez, erteler. Döngü neredeyse her zaman bir tasarım sorununun belirtisidir:

  • Sorumluluğu ayır. İki servisin birbirine ihtiyaç duyması, genelde ikisine de ait olan üçüncü bir kavram olduğu anlamına gelir. Onu ayrı bir sınıfa çıkar.
  • Olayla gevşet. A, B’yi doğrudan çağırmak yerine “şu oldu” diye bir olay yayınlar, B de onu dinler.
  • Arayüzü daralt. A’nın B’den ihtiyaç duyduğu tek metotsa, o metodu ayrı ve küçük bir arayüze taşı.
Döngüye izin veren ayar· istersen atla

Spring Boot 2.6’dan beri döngüsel bağımlılıklar varsayılan olarak yasaktır. spring.main.allow-circular-references=true ile açılabilir, ama bu bir çözüm değil, bir erteleme düğmesidir.

Bean scope’ları

Varsayılan singleton: container başına bir örnek.

ScopeKaç örnek
singleton (varsayılan)Container başına bir tane
prototypeHer enjeksiyon/istekte yeni
requestHer HTTP isteğinde bir tane
sessionHer oturumda bir tane
Singleton içine prototype enjekte etmek· istersen atla

Bir singleton scopeVarsayılan bean kapsamı: konteyner başına tek örnek, tüm enjeksiyon noktaları aynı nesneyi paylaşır.Uygulama başına değil, konteyner başına tektir. Paylaşıldığı için singleton bir bean'e istek verisi yazmak, o veriyi tüm kullanıcılara dağıtmak demektir.Sözlükte gör →’a prototype bean enjekte edersen, prototype yalnızca bir kez oluşturulur — singleton bir kez kurulduğu için. Her çağrıda yeni örnek istiyorsan ObjectProvider<T> kullan ve ihtiyaç anında iste.

Her bean’den tek bir tane olduğu için bütün istekler aynı nesneyi kullanır. Bu yüzden bean’ler durumsuz (stateless) olmalı: bir alana istek verisi yazarsan, iki kullanıcının verisi birbirine karışır.

Kafam karıştı, daha basit anlat

Singleton bir kez kurulur ve hep aynı kalır. İçine koyduğun her şey de o tek kurulumda bir kez seçilir. Her seferinde yenisini istiyorsan, onu her seferinde iste.

Hızlı kontrolBaşlangıç

Her ifadeyi doğru enjeksiyon stiline yerleştir.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Constructor injection

Bağımlılıklar kurucudan verilir

    Field injection

    Alanlar reflection ile doldurulur

      Tuzaklar

      En sık üç sebep:

      1. Sınıf tarama ağacının dışında. @SpringBootApplication kendi paketini ve altındakileri tarar; kardeş bir paketteki bean bulunmaz.
      2. Anotasyon yok. Sınıf @Component ailesinden biriyle işaretlenmemiş, veya @Bean metodu bir @Configuration sınıfında değil.
      3. Aynı tipten birden fazla aday. NoUniqueBeanDefinitionException — @Primary veya @Qualifier gerekir.

      Kendini sına

      Şimşek turu1/5

      Dependency injection'da sınıf ihtiyaç duyduğu nesneleri kendisi yaratır.

      Soru 1/3Başlangıç

      İki bean birbirine bağımlı (A → B → A). Constructor injection ile ne olur?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

      Aklında kalacak üç şey

      1. 1 Bağımlılığı sınıfın kendisi oluşturmaz, dışarıdan verilir. Buna Inversion of Control denir; Spring bunu senin yerine yapar.
      2. 2 Constructor injection'ın üç artısı var: nesne doğduğu anda hazırdır, alanlar final olabilir ve döngüsel bağımlılık açılışta patlar.
      3. 3 Field injection döngüyü çözmez, saklar. Üstelik sınıfı Spring olmadan test etmeyi zorlaştırır.
      Sonraki kapı @PostConstruct içinden @Transactional bir metot çağırdın, ama transaction açılmadı. Neden? Bean Yaşam Döngüsü ve Scope'lar · 10 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı

      Bu dersin üstüne kurulanlar

      Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.