Középhaladó Olvasási idő: ~13 perc

IoC és Dependency Injection

IoC, DI, constructor injection, field injection, setter injection, Bean lifecycle, Bean scopes

IoC és Dependency Injection

Az IoC és a Dependency Injection a Spring keretrendszer alapja: a container kezeli az objektumok létrehozását és összekötését, így az alkalmazáskód az üzleti logikára koncentrálhat.


1. Definíció

  • Mi ez? — Az Inversion of Control (IoC) egy tervezési elv, ahol az objektumok létrehozásának és összekötésének felelőssége kikerül az alkalmazáskódból egy külső container-be. A Dependency Injection (DI) ennek a leggyakoribb megvalósítási formája.
  • Miért létezik? — A hagyományos megközelítésben az osztályok maguk hozzák létre a függőségeiket (new operátorral), ami szoros csatolást eredményez. A DI lazán csatolt, tesztelhető és cserélhető komponenseket tesz lehetővé.
  • Hol helyezkedik el? — A Spring IoC container az ApplicationContext interfészen keresztül érhető el. Ez a Spring ökoszisztéma központi eleme, amelyre az összes többi modul (Boot, Web, Data, Security) épül.

2. Alapfogalmak

IoC vs DI

Az IoC egy elv, a DI egy minta. Az IoC azt mondja: „ne te irányítsd az objektumgráfot". A DI pedig megmondja hogyan: „a függőségeket kívülről kapod meg". A Spring IoC container mindkettőt megvalósítja — bean definíciókat olvas, példányosít és injektál.

A Service Locator szintén IoC megvalósítás, de a Spring a DI-t részesíti előnyben, mert az átláthatóbb és a kód nem függ a container API-tól.

Injection típusok részletesen

Típus Mechanizmus Mikor használd?
Constructor injection Konstruktor paraméterek ✅ Kötelező függőségek (ajánlott default)
Setter injection Setter metódusok + @Autowired Opcionális vagy változtatható függőségek
Field injection @Autowired mező ⚠️ Kerülendő — rejtett dependency-k

Constructor injection — miért ez a default

@Service
public class OrderService {
    private final PaymentGateway paymentGateway;
    private final InventoryService inventoryService;

    // Spring automatikusan injektál, ha egyetlen konstruktor van
    public OrderService(PaymentGateway paymentGateway, InventoryService inventoryService) {
        this.paymentGateway = paymentGateway;
        this.inventoryService = inventoryService;
    }
}

Előnyök:

  • A dependency-k final-ek lehetnek → immutable design
  • Nem lehet „elfelejteni" egy dependency-t → compile-time safety
  • Tesztben egyszerűen mock-olható konstruktoron keresztül
  • Circular dependency-t azonnal jelzi (fail-fast)
  • Nincs szükség @Autowired-re, ha egyetlen konstruktor van (Spring 4.3+)

@Autowired viselkedés részletesen

Az @Autowired három helyen működik: konstruktoron, setter-en és mezőn.

// Több konstruktor esetén meg kell jelölni melyiket használja a Spring
@Service
public class NotificationService {
    private final EmailSender emailSender;
    private final SmsSender smsSender;

    @Autowired  // Kötelező, mert két konstruktor van
    public NotificationService(EmailSender emailSender, SmsSender smsSender) {
        this.emailSender = emailSender;
        this.smsSender = smsSender;
    }

    public NotificationService(EmailSender emailSender) {
        this(emailSender, null);
    }
}

Fontos viselkedések:

  • @Autowired(required = false) — nem dob exception-t, ha nincs matching bean
  • Generikus típusokat is feloldja: @Autowired List<EventListener> → minden EventListener bean-t begyűjti
  • @Autowired Map<String, PaymentGateway> → kulcs a bean neve, érték a bean

Több implementáció feloldása: @Primary és @Qualifier

Ha egy interfésznek több implementációja van, a Spring-nek el kell döntenie melyiket injektálja:

public interface PaymentGateway {
    String charge(double amount);
}

@Service
@Primary  // Ha nincs explicit @Qualifier, ez az alapértelmezett
public class StripeGateway implements PaymentGateway {
    public String charge(double amount) { return "Stripe: " + amount; }
}

@Service("paypal")
public class PaypalGateway implements PaymentGateway {
    public String charge(double amount) { return "PayPal: " + amount; }
}

@Service
public class OrderService {
    private final PaymentGateway defaultGateway;   // → StripeGateway (@Primary)
    private final PaymentGateway paypalGateway;

    public OrderService(
            PaymentGateway defaultGateway,
            @Qualifier("paypal") PaymentGateway paypalGateway) {
        this.defaultGateway = defaultGateway;
        this.paypalGateway = paypalGateway;
    }
}

Feloldási sorrend: @Qualifier név → @Primary → bean név egyezés → exception.

Bean lifecycle

Egy Spring bean életciklusa:

  1. Bean definition betöltése@Component scan vagy @Bean metódus
  2. Példányosítás — konstruktor meghívása
  3. Dependency injection — setter/field injection (constructor már megtörtént)
  4. BeanPostProcessor.postProcessBeforeInitialization()
  5. Inicializáció@PostConstruct vagy InitializingBean.afterPropertiesSet()
  6. BeanPostProcessor.postProcessAfterInitialization() — itt készül a proxy is (AOP, @Transactional)
  7. Használat — a bean elérhető az alkalmazásban
  8. Destrukció@PreDestroy vagy DisposableBean.destroy() (csak singleton)

Bean scope-ok

Scope Élettartam Tipikus használat
singleton Egy példány az egész ApplicationContext-ben ✅ Default, stateless service-ek
prototype Minden lekérésre új példány Stateful vagy rövid életű objektumok
request Egy HTTP kérés Web-specifikus, pl. request context
session Egy HTTP session Web-specifikus, pl. user session data
application Egy ServletContext Ritkán használt, globális web scope

3. Gyakorlati használat

Mikor használd

  • Constructor injection: minden kötelező dependency-re — ez a Spring csapat ajánlása
  • @Autowired setter: opcionális dependency, ami nélkül is működik az osztály
  • ObjectProvider<T>: lazy vagy opcionális feloldás, conditional logic
  • @PostConstruct: inicializációs logika, ami a dependency-k injektálása után kell
  • @Qualifier: ha több implementáció van és explicit választani kell
  • @Primary: ha van egy „default" implementáció, amit a legtöbb helyen használsz

Mikor NE használd

  • Field injection: rejtett dependency-ket hoz, tesztelést nehezíti, nem támogat immutable design-t
  • @Lazy circular dependency workaround: a design-t kell javítani, nem a tünetet kezelni
  • Prototype bean singletonba injektálva proxy nélkül: a singleton mindig ugyanazt a prototype példányt fogja látni
  • Sok dependency egy konstruktorban (>5): jelzi, hogy az osztály túl sokat csinál — refaktor kell

ObjectProvider és Provider

@Service
public class ReportService {
    private final ObjectProvider<ExpensiveClient> clientProvider;

    public ReportService(ObjectProvider<ExpensiveClient> clientProvider) {
        this.clientProvider = clientProvider;
    }

    public void generateReport() {
        // Lazy: csak akkor hozza létre, ha tényleg kell
        ExpensiveClient client = clientProvider.getIfAvailable();
        if (client != null) {
            client.fetchData();
        }
    }
}

Az ObjectProvider Spring-specifikus, a Provider<T> (JSR-330) szabványos. Mindkettő lazy feloldást ad, de az ObjectProvider gazdagabb API-t nyújt (getIfAvailable, getIfUnique, stream()).


4. Kód példák

Alapszintű — Constructor injection és lifecycle

package com.example.billing;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;

public interface PaymentGateway {
    String charge(double amount);
}

@Service
public class StripePaymentGateway implements PaymentGateway {
    @Override
    public String charge(double amount) {
        return "Stripe charged: " + amount;
    }
}

@Service
public class BillingService {
    private final PaymentGateway paymentGateway;

    // Egyetlen konstruktor → @Autowired nem szükséges
    public BillingService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    @PostConstruct
    void init() {
        System.out.println("BillingService ready");
    }

    @PreDestroy
    void shutdown() {
        System.out.println("BillingService shutting down");
    }

    public String processBilling(double amount) {
        return paymentGateway.charge(amount);
    }
}

Haladó — Scope-ok és ObjectProvider

package com.example.audit;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Configuration
public class AuditConfig {
    @Bean
    @Scope("prototype")
    public AuditContext auditContext() {
        return new AuditContext();
    }
}

public class AuditContext {
    private final long createdAt = System.nanoTime();
    public long getCreatedAt() { return createdAt; }
}

@Service
public class AuditService {
    private final ObjectProvider<AuditContext> auditContextProvider;

    public AuditService(ObjectProvider<AuditContext> auditContextProvider) {
        this.auditContextProvider = auditContextProvider;
    }

    public void audit(String action) {
        // Minden hívásra friss prototype példányt kap
        AuditContext ctx = auditContextProvider.getObject();
        System.out.println("Audit [" + ctx.getCreatedAt() + "]: " + action);
    }
}

Több implementáció kezelése — @Qualifier egyedi annotációval

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface Premium {}

@Service
@Premium
public class PremiumNotificationService implements NotificationService {
    // prémium csatorna: SMS + email + push
}

@Service
public class BasicNotificationService implements NotificationService {
    // alap csatorna: csak email
}

@Service
public class AccountService {
    public AccountService(@Premium NotificationService premiumNotifier,
                          NotificationService basicNotifier) {
        // A @Premium explicit jelöli melyik implementációt kérjük
    }
}

Gyakori hiba

// ❌ ROSSZ — field injection, rejtett dependency
@Service
public class BadService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private EmailService emailService;
    // Tesztben reflection kell a mock-oláshoz
}

// ✅ JÓ — constructor injection, explicit dependency
@Service
public class GoodService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    public GoodService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
}

5. Trade-offok

Szempont Részletek
⚡ Teljesítmény A singleton scope gyors, mert egyszer jön létre. A prototype overhead-et jelent minden lekérésnél. Webes scope-ok a leglassabbak a scope proxy miatt.
💾 Memória A singleton egyetlen példányt tart memóriában. A prototype példányokat a GC kezeli, de a container nem hívja meg a @PreDestroy-t.
🔧 Karbantarthatóság Constructor injection explicit és átlátható. Field injection rövid, de rejtett és nehezebben tesztelhető.
🔄 Rugalmasság Az ObjectProvider és a @Lazy késleltetett feloldást adnak. A scope proxy átlátszóan kezeli az eltérő életciklusokat.
🧪 Tesztelhetőség Constructor injection → new Service(mockA, mockB). Field injection → reflection vagy @InjectMocks.

6. Gyakori hibák

  1. Field injection használata — Rövid, de rejtett dependency-ket hoz. A Spring csapat is constructor injection-t ajánl. Tesztben reflection kell a mock-oláshoz.

  2. Circular dependency elfedése @Lazy-val — Ha A függ B-től és B függ A-tól, az @Lazy csak elodázza a problémát. A megoldás: egy köztes service bevezetése, vagy az events pattern használata.

  3. Prototype beanből singleton viselkedés elvárása — Ha egy singleton service-be prototype beant injektálsz, a singleton mindig ugyanazt a példányt fogja használni. Megoldás: ObjectProvider vagy @Lookup.

  4. @PreDestroy prototype bean-en — A container nem kezeli a prototype bean életciklusának végét. A @PreDestroy callback nem hívódik meg automatikusan.

  5. Túl sok dependency egy konstruktorban — Ha 6-7+ paraméter van, az osztály valószínűleg túl sok felelősséget vállal (Single Responsibility Principle megsértése).

  6. new operátorral létrehozni Spring-managed objektumot — Az így létrehozott példány nem Spring bean, nem kap injection-t, nem megy át a lifecycle-on.

  7. @Autowired kollekcióra félreértés — A @Autowired List<Validator> nem egy Validator listát keres bean-ként, hanem az ÖSSZES Validator típusú bean-t gyűjti össze. Ez meglepetés lehet, ha nem szándékos.

  8. @Qualifier elfelejtése — Ha két azonos típusú bean van és nincs @Primary vagy @Qualifier, NoUniqueBeanDefinitionException keletkezik induláskor.


7. Mélyebb összefüggések

BeanPostProcessor és a proxy mechanizmus

A Spring IoC container nem csak létrehozza a bean-eket, hanem egy pipeline-on futtatja át őket. A BeanPostProcessor interfész két hook-ot ad: postProcessBeforeInitialization és postProcessAfterInitialization. Az utóbbiban készülnek el az AOP proxy-k, a @Transactional wrapper-ek és a security interceptor-ok.

Ez azt jelenti, hogy a bean, amit más service-ek kapnak, nem feltétlenül az eredeti objektum — lehet CGLIB proxy vagy JDK dynamic proxy. Ennek következménye: final osztályokon nem működik a CGLIB proxy, private metódusokra nem hat a @Transactional.

Singleton vs Thread-safety

A singleton scope nem jelent automatikus thread-safety-t. Egy singleton bean-t egyszerre több szál is használhat. Ha mutable állapotot tartasz benne (pl. List field), az race condition-höz vezet. A megoldás: stateless design, ThreadLocal, vagy synchronized blokkok.

// ❌ Nem thread-safe singleton
@Service
public class CounterService {
    private int count = 0;  // mutable állapot!
    public void increment() { count++; }  // race condition
}

// ✅ Thread-safe megoldások
@Service
public class SafeCounterService {
    private final AtomicInteger count = new AtomicInteger(0);
    public void increment() { count.incrementAndGet(); }
}

Scope proxy működése

Amikor egy request-scoped bean-t singleton-ba injektálsz, a Spring egy CGLIB proxy-t hoz létre. Minden metódushíváskor a proxy megkeresi az aktuális HTTP request-hez tartozó bean példányt. Emiatt a getClass() a proxy osztályt adja vissza, és az equals()/hashCode() viselkedése megváltozhat.

@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public RequestContext requestContext() {
    return new RequestContext();
}

Circular dependency kezelés belülről

A Spring 5/6-ban a circular dependency kezelés alapértelmezetten tiltott constructor injection-nél. Setter injection esetén a Spring "early reference"-t használ — a bean részlegesen inicializálódik (konstruktor lefut, de setter-ek még nem). Ez az un. "three-level cache" mechanizmus:

  1. singletonObjects — teljesen inicializált bean-ek
  2. earlySingletonObjects — részleges bean-ek (factory-ból korán kihúzva)
  3. singletonFactories — ObjectFactory-k, amelyek a bean-t inicializálás közben adják vissza

Ez törékeny mechanizmus — ha az early reference proxyzva van (pl. @Transactional), problémák léphetnek fel. Ezért best practice: ne függj a circular dependency resolution-re.


8. Interjúkérdések

K: Mi a különbség az IoC és a DI között? V: Az IoC egy tervezési elv — az objektumok létrehozásának kontrollját megfordítja (a container irányít, nem az alkalmazáskód). A DI ennek az elvnek a konkrét megvalósítása — a függőségeket kívülről kapja az objektum, constructor, setter vagy field injection révén.

K: Miért jobb a constructor injection, mint a field injection? V: A constructor injection explicit (a compiler kikényszeríti), támogat immutable design-t (final field-ek), circular dependency-t azonnal jelzi, és tesztben egyszerűen mock-olható new Service(mockDep) formában, reflection nélkül.

K: Mi történik, ha singleton bean-be prototype bean-t injektálsz? V: A singleton csak egyszer inicializálódik, és mindig ugyanazt a prototype példányt fogja használni. Ha friss prototype kell minden híváshoz, ObjectProvider<T>, Provider<T> (JSR-330) vagy @Lookup metódus szükséges.

K: Hogyan kezeli a Spring a circular dependency-t? V: Constructor injection esetén a context nem tud elindulni (fail-fast, jó jelzés). Setter/field injection esetén a Spring early reference-t használ (singleton bean-ek részleges példányosítása), de ez fragilis és Spring 6-ban alapértelmezetten tiltott. A helyes megoldás: refaktorálás, events, vagy köztes dependency bevezetése.

K: Mi a különbség a @Bean és a @Component között? V: A @Component (és @Service, @Repository) class-level annotáció — a component scan automatikusan regisztrálja. A @Bean method-level annotáció @Configuration osztályban — kézi kontrollt ad a bean létrehozása felett, külső library osztályoknál is használható.

K: Mi a különbség a @Primary és a @Qualifier között? V: A @Primary az alapértelmezett jelöltet jelöli ki, ha több azonos típusú bean van. A @Qualifier az injection ponton nevesíti, hogy melyik bean-t kéri. A @Qualifier erősebb — felülírja a @Primary-t.

K: Mit csinál az @Autowired List<T>? V: Az ApplicationContext-ből az ÖSSZES T típusú bean-t összegyűjti egy listába. A Map<String, T> variáns a bean nevét is megadja kulcsként. Hasznos strategy pattern, plugin architektúra esetén.


9. Szószedet

Fogalom Jelentés
IoC Inversion of Control — a container irányítja az objektumok létrehozását
DI Dependency Injection — a függőségek átadása kívülről
Bean A Spring container által kezelt objektum
Scope A bean élettartamát meghatározó szabály
Lifecycle hook Callback a bean életciklusának adott pontjain (@PostConstruct, @PreDestroy)
Circular dependency Körkörös függés két vagy több bean között
ObjectProvider Spring interfész lazy/opcionális dependency feloldáshoz
Scope proxy CGLIB proxy, amely eltérő scope-ok közötti injektálást tesz lehetővé
@Primary Alapértelmezett bean jelölő, ha több azonos típusú létezik
@Qualifier Explicit bean kiválasztó név vagy egyedi annotáció alapján
BeanPostProcessor Pipeline hook bean-ek testreszabásához (proxy létrehozás, stb.)
Early reference Részlegesen inicializált bean circular dependency feloldáshoz

10. Gyorsreferencia

INJECTION TÍPUSOK:
  Constructor  → ✅ default, final field-ek, fail-fast circular dep
  Setter       → opcionális dependency-k, @Autowired kell
  Field        → ❌ anti-pattern, rejtett dependency-k

@AUTOWIRED VISELKEDÉS:
  1 konstruktor          → automatikus, @Autowired nem kell
  2+ konstruktor         → @Autowired jelöli a használandót
  @Autowired List<T>     → minden T típusú bean-t összegyűjti
  @Autowired Map<S,T>    → bean név → bean instance

TÖBB IMPLEMENTÁCIÓ FELOLDÁSA:
  @Primary               → alapértelmezett jelölt
  @Qualifier("name")     → explicit bean név
  Egyedi @Qualifier       → saját annotáció alapú feloldás
  Feloldási sorrend       → @Qualifier > @Primary > bean név

SCOPE-OK:
  singleton   egy példány (default)
  prototype   minden lekérésre új
  request     HTTP kérésenként
  session     HTTP session-önként

LIFECYCLE:
  Constructor → setter DI → @PostConstruct → használat → @PreDestroy
  BeanPostProcessor: proxy-k itt készülnek (AOP, @Transactional)

TIPP-EK:
  5+ dependency → SRP violation, refaktorálj
  Prototype singletonba → ObjectProvider<T> kell
  Circular dependency → design smell, ne @Lazy workaround
  new ClassName() → NEM Spring bean, nincs injection

🎮 Játékok

10 kérdés