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 (
newoperá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
ApplicationContextinterfé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>→ mindenEventListenerbean-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:
- Bean definition betöltése —
@Componentscan vagy@Beanmetódus - Példányosítás — konstruktor meghívása
- Dependency injection — setter/field injection (constructor már megtörtént)
BeanPostProcessor.postProcessBeforeInitialization()- Inicializáció —
@PostConstructvagyInitializingBean.afterPropertiesSet() BeanPostProcessor.postProcessAfterInitialization()— itt készül a proxy is (AOP,@Transactional)- Használat — a bean elérhető az alkalmazásban
- Destrukció —
@PreDestroyvagyDisposableBean.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
@Autowiredsetter: opcionális dependency, ami nélkül is működik az osztályObjectProvider<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
@Lazycircular 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
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.
Circular dependency elfedése
@Lazy-val — Ha A függ B-től és B függ A-tól, az@Lazycsak elodázza a problémát. A megoldás: egy köztes service bevezetése, vagy az events pattern használata.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:
ObjectProvidervagy@Lookup.@PreDestroyprototype bean-en — A container nem kezeli a prototype bean életciklusának végét. A@PreDestroycallback nem hívódik meg automatikusan.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).
newoperá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.@Autowiredkollekcióra félreértés — A@Autowired List<Validator>nem egy Validator listát keres bean-ként, hanem az ÖSSZESValidatortípusú bean-t gyűjti össze. Ez meglepetés lehet, ha nem szándékos.@Qualifierelfelejtése — Ha két azonos típusú bean van és nincs@Primaryvagy@Qualifier,NoUniqueBeanDefinitionExceptionkeletkezik 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:
- singletonObjects — teljesen inicializált bean-ek
- earlySingletonObjects — részleges bean-ek (factory-ból korán kihúzva)
- 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