RU | EN | DE

Spring Core

Inversion of Control

Inversion of Control — das ist ein Prinzip, bei dem die Erstellung von Objekten und die Übergabe von Abhängigkeiten an einen speziellen Container (z.B. Spring) übertragen wird, damit die Klassen selbst diesen Prozess nicht steuern und nicht wissen, woher ihre Abhängigkeiten kommen.

Arten der Abhängigkeitsinjektion

  • Injektion über ein Feld mit der Annotation — Autowired
@Autowired
private UserService userService;
  • Injektion über den Konstruktor (die beliebteste Variante)
@Service
public class TestService {        
 
	private final ProcessService processService;    
 
	public TestService(ProcessService processService) {        
		this.processService = processService;    
	}
}
  • Injektion über Setter
@Service
public class TestService {    
	private ProcessService processService;    
	
	@Autowired    
	public void setProcessService(ProcessService processService) {        
		this.processService = processService;    
	}
}

Unterschiede zwischen @Component, @Service, @Repository, @Controller

  • @Component — Basis-Annotation; markiert eine beliebige Klasse als Spring-Bean.
  • @Service — dasselbe wie @Component, aber semantisch für die Geschäftslogik; verbessert die Lesbarkeit und Architekturstruktur.
  • @Repository — @Component für die DAO-Schicht; fängt zusätzlich Datenbankausnahmen ab und konvertiert sie in Spring DataAccessException.
  • @Controller — wird für die Web-Schicht in MVC-Anwendungen verwendet; gibt standardmäßig HTML/Templates zurück, kein JSON.
  • @RestController — das ist @Controller + @ResponseBody und gibt standardmäßig JSON zurück.

@ComponentScan

ComponentScan — das ist eine Annotation, die Spring angibt, wo nach Klassen mit den Annotationen @Component, @Service, @Repository, @Controller, @RestController sowie @Configuration gesucht werden soll, um automatisch Beans zu erstellen — einschließlich derjenigen, die über @Bean-Methoden innerhalb dieser Konfigurationsklassen definiert sind, und sie der Kontrolle des Containers zu übergeben.

Lebenszyklus von Beans

  1. Erstellung — der Container erstellt das Bean-Objekt.
  2. Befüllung mit Abhängigkeiten — alle Abhängigkeiten werden injiziert (DI).
  3. Initialisierung — Initialisierungsmethoden werden aufgerufen:
    • Annotation @PostConstruct
    • wenn der Bean über @Bean definiert ist, kann initMethod angegeben werden.
  4. Zerstörung — Destruktionsmethoden werden aufgerufen:
    • Annotation @PreDestroy
    • oder destroyMethod bei @Bean.

Bean Scopes

  • Singleton (Standard) — eine Instanz pro Container.
  • Prototype — eine neue Instanz bei jeder Bean-Anforderung.
  • Request / Session / Application — für Web-Anwendungen, werden pro HTTP-Anfrage, Session oder Anwendung erstellt.

Fallstrick: wenn ein Prototype in einen Singleton injiziert wird, erstellt Spring nur eine Instanz bei der Erstellung des Singleton, nicht jedes Mal eine neue.

BeanFactoryPostProcessor und BeanPostProcessor

  • BeanFactoryPostProcessor — ermöglicht die Änderung von Bean-Metadaten vor deren Erstellung durch den Container. Beispiel: PropertySourcesPlaceholderConfigurer.
  • BeanPostProcessor — fängt den bereits erstellten Bean vor der Verwendung ab. Darauf basiert Proxying, AOP und @Transactional. Methoden werden in dieser Reihenfolge aufgerufen: postProcessBeforeInitialization → Initialisierung → postProcessAfterInitialization.

Spring AOP

Spring AOP — das ist ein Mechanismus der aspektorientierten Programmierung, der es ermöglicht, sich wiederholende Funktionalität (Logging, Transaktionen, Sicherheit) in separate Aspekte auszulagern, ohne sie mit der Geschäftslogik zu vermischen.

Aspekt — das ist ein Modul, das Advice (Code, der vor, nach oder um eine Methode herum ausgeführt wird) und Pointcut (Regeln, wo dieser Code angewendet werden soll) enthält.

Einschränkungen von Spring AOP:

  • Aufrufe innerhalb einer Klasse — Methoden rufen sich gegenseitig direkt auf, der Proxy wird nicht aktiviert.
  • Finale Klassen und Methoden — CGLIB-Proxies können final nicht überschreiben, JDK-Proxies funktionieren nicht mit Klassen.
  • Private Methoden — Proxy funktioniert nur mit public/protected/package-private Methoden, private können nicht mit AOP versehen werden.

CGLIB — das ist eine Bibliothek, die Spring zur Erstellung von Proxy-Klassen durch Vererbung verwendet. Sie ermöglicht das Umhüllen eines Beans, wenn dieser kein Interface implementiert (im Gegensatz zu JDK Dynamic Proxy, der nur mit Interfaces funktioniert).

Das heißt, AOP funktioniert durch Proxies, und diese Einschränkungen sind direkte Folgen dieses Mechanismus.

Mit der AOP-Einschränkung werden bei Vorstellungsgesprächen oft Aufgaben gegeben, Beispiel:

@Service
public class OrderService {    
	
	@PostConstruct    
	public void init() {        
		// Aufruf einer transaktionalen Methode innerhalb von @PostConstruct        
		processPayment(); // @Transactional funktioniert hier nicht    
	}    
	
	@Transactional    
	public void processPayment() {        
		System.out.println("Zahlung durchgeführt");    
	}
}

Warum es nicht funktioniert:

  1. Proxy-Mechanismus: Spring erstellt einen Proxy um den Bean für die Verarbeitung von @Transactional
  2. Interner Aufruf: Wenn processPayment() aus init() derselben Klasse aufgerufen wird, umgeht man den Proxy und ruft die Methode direkt auf
  3. AOP wird nicht angewendet: Der Transaktionsinterceptor wird nicht ausgelöst, da der Aufruf nicht durch den Proxy geht

Wie man das umgeht:

Aus der Spring-Dokumentation:

BeanPostProcessors are applied before any initialization methods (such as @PostConstruct)

In der Spring Framework Reference ist explizit angegeben, dass BeanPostProcessors (die AOP-Proxies erstellen) vor dem Aufruf von @PostConstruct angewendet werden.

@Component
public class OrderService {        
	
	@Autowired    
	private ApplicationContext context;        
	
	@PostConstruct    
	public void init() {        
		context.getBean(MyService.class).processPayment();    
	}    
	
	@Transactional    
	public void processPayment() {        
		System.out.println("Zahlung durchgeführt");    
	}
}

Diese Lösung mit ApplicationContext sollte funktionieren. Aber gleich möchte man sagen, erklären Sie, dass man in diesem Fall die Methode besser in einen separaten Bean auslagern sollte, und wenn gesagt wird, dass das nicht geht und wir das wirklich in diesem wollen, dann kann man ApplicationContext erwähnen.

@SpringBootApplication

Um zu verstehen, was @SpringBootApplication aufruft und wie es funktioniert, reicht es, in die Dokumentation zu schauen oder direkt in die Annotation in IDEA zu gehen:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),	
	@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
 
public @interface SpringBootApplication

Wie Spring Boot den Kontext aufbaut:

  1. Environment — sammelt Properties und Profile.
  2. ApplicationContext — erstellt und konfiguriert den Bean-Container.
  3. BeanFactory — registriert alle Beans und Abhängigkeiten.
  4. Refresh — Initialisierung von Beans und Aufruf von Lifecycle-Methoden.
  5. Listeners — Anwendungsereignisse werden ausgelöst (ApplicationReadyEvent usw.).
  6. Embedded Server — wenn es eine Web-Anwendung ist, wird ein eingebetteter Server gestartet (Tomcat/Jetty).

Das heißt, Boot durchläuft die Kette: Konfiguration → Bean-Erstellung → Initialisierung → Ereignisse → Start des Web-Servers.

@Primary

  • Markiert einen Bean als Hauptkandidat, wenn es mehrere Kandidaten desselben Typs gibt.
  • Spring wählt ihn automatisch bei der Injektion, wenn kein @Qualifier angegeben ist.
@Primary
@Component
public class PaypalPaymentService implements PaymentService { }
 
@Component
public class StripePaymentService implements PaymentService { }
 
@Service
public class OrderService {    
	@Autowired    
	private PaymentService paymentService; // PaypalPaymentService wird gewählt
}

@Qualifier

Ermöglicht die explizite Angabe, welcher Bean verwendet werden soll, auch wenn es mehrere Kandidaten gibt.

@Service
public class OrderService {    
	@Autowired    
	@Qualifier("stripePaymentService")    
	private PaymentService paymentService; // StripePaymentService wird gewählt
}

@Transactional

Ich weiß, dass es schon eine Million Artikel zum Thema @Transactional gibt, wie es unter der Haube funktioniert, welche Probleme es gibt usw., ich erkläre es sehr kurz:

  • Proxy um die Methode — Spring erstellt einen Proxy (JDK oder CGLIB), der den Methodenaufruf abfängt und die Transaktion verwaltet.
  • TransactionInterceptor — eine Komponente, die die Methode umhüllt, vor der Ausführung eine Transaktion öffnet und danach committet/rollbackt.
  • Propagation — Regeln, wie eine Methode an einer bestehenden Transaktion teilnimmt:
    • REQUIRED — bestehende Transaktion verwenden oder eine neue erstellen (Standard).
    • REQUIRES_NEW — immer eine neue erstellen und die aktuelle suspendieren.
    • NESTED — verschachtelte Transaktion, kann teilweise zurückgerollt werden.
    • SUPPORTS — Transaktion verwenden, wenn vorhanden, sonst ohne laufen.
    • NOT_SUPPORTED — außerhalb der Transaktion laufen und die bestehende suspendieren.
    • NEVER — Fehler werfen, wenn eine Transaktion bereits vorhanden ist.
    • MANDATORY — bestehende Transaktion zwingend verwenden, sonst Ausnahme.
  • Isolation levels — Isolationsstufe: READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE und READ_UNCOMMITTED.
  • Warum die Transaktion möglicherweise nicht funktioniert:
    • private Methode (Proxy sieht den Aufruf nicht)
    • interner Aufruf einer Methode derselben Klasse (this.method())
    • finale Klasse oder finale Methode (CGLIB kann keinen Proxy erstellen)
    • Aufruf von setRollbackOnly() ohne korrekten Rollback

@Profile

@Profile - Annotation zur bedingten Einbindung eines Beans oder einer Konfiguration abhängig vom aktiven Profil der Anwendung:

@Configuration
@Profile("dev")
public class DevConfig {    
	@Bean    
	public DataSource dataSource() {        
		return new H2DataSource();    
	}
}

Wichtig: wenn ein Bean mit @Profile markiert ist und Sie versuchen, ihn bei inaktivem Profil zu injizieren, findet Spring diesen Bean nicht und die Anwendung bricht mit dem Fehler NoSuchBeanDefinitionException ab.

@ConditionalOnProperty

@ConditionalOnProperty — eine Spring Boot-Annotation, die die Erstellung eines Beans nur erlaubt, wenn eine bestimmte Property in application.properties oder application.yml gesetzt ist.

@Service
@ConditionalOnProperty(name = "app.payment.enabled", havingValue = "true")
public class PaymentService {    
	// Bean wird nur erstellt, wenn feature.payment.enabled=true
}

Wie man mehrere Beans, die dasselbe Interface implementieren, injiziert

Eine Frage, die mir mehr als einmal gestellt wurde)
Stellen wir uns vor, dass wir ein Interface PaymentService haben, das 2 Implementierungen besitzt, und wir im Service beide Zahlungsmethoden ausführen möchten:

public interface PaymentService {    
	void pay();
}
 
@Service("creditCardPayment")
public class CreditCardPaymentService implements PaymentService {    
	@Override    
	public void pay() {        
		System.out.println("Zahlung per Kreditkarte");
	}
}
 
@Service("paypalPayment")
public class PaypalPaymentService implements PaymentService {    
	@Override    
	public void pay() {        
		System.out.println("Zahlung per PayPal");    
	}
}

Injektion über List<PaymentService>

Oft wird die Injektion über List\<PaymentService\> verwendet, um alle Implementierungsservices zu erhalten:

@Service
public class OrderService {    
	private final List<PaymentService> paymentServices;    
	
	@Autowired    
	public OrderService(List<PaymentService> paymentServices) {
		this.paymentServices = paymentServices;    
	}    
	
	public void processAllPayments() {
		paymentServices.forEach(PaymentService::pay);    
	}
}

Injektion über Map<String, PaymentService>

Wir können solche Beans über Map<String, PaymentService> injizieren. In diesem Fall sind die Schlüssel die Bean-Namen ("creditCardPayment" und "paypalPayment"), und die Werte sind die entsprechenden Implementierungen.

@Service
public class OrderService {    
	private final Map<String, PaymentService> paymentServices;    
	
	@Autowired    
	public OrderService(Map<String, PaymentService> paymentServices) {
		this.paymentServices = paymentServices;    
	} 
	
	public void processSpecificPayment(String type) {
		paymentServices.get(type).pay();
	}
}

Self Injection

Self Injection - das ist der Fall, wenn eine Klasse sich selbst über den Spring-Container injiziert, normalerweise über einen Proxy.

Wofür es benötigt wird:

  • Um eine eigene Methode aufzurufen, die mit @Transactional oder @Async annotiert ist, damit der Spring-Proxy den Aspekt korrekt verarbeitet.
  • Ein direkter Methodenaufruf über this geht nicht durch den Proxy, daher werden Annotationen nicht funktionieren.
@Service
public class OrderService {    
	@Autowired    
	@Lazy    
	private OrderService self;    
	
	@Transactional    
	public void processOrder() {        
		// Code    
	}    
	
	public void startProcess() {        
		self.processOrder(); // Proxy wird aktiviert    
	}
}

@Cacheable

Es gibt eine wunderbare Annotation @Cacheable, die Caching für eine Methode aktiviert, damit beim Aufruf der Methode mit gleichen Parametern diese nicht erneut ausgeführt, sondern der Wert aus dem Cache zurückgegeben wird:

@Cacheable(cacheNames = "test", key = "#test")  
public String test(int test) {      
	return LocalDateTime.now().toString();  
}

Manchmal wird eine ähnliche Frage gestellt wie bei Transaktionen, aber mit Cache. Nehmen wir als Beispiel:

@Service
@EnableCaching
public class CacheClass {        
	
	@SneakyThrows    
	@PostConstruct    
	public void init() {        
		System.out.println(test(1));        
		Thread.sleep(1000);        
		System.out.println(test(2));        
		Thread.sleep(1000);        
		System.out.println(test(1));    
	}    
 
	@Cacheable(cacheNames = "test", key = "#integer")    
	public String test(int integer) {        
		return LocalDateTime.now().toString();    
	}
}

Hier sehen wir, dass weder Self Injection noch ApplicationContext vorhanden ist, was bedeutet, dass dies nicht funktionieren wird. Mit Self Injection oder ApplicationContext würde alles funktionieren, aber für @Cacheable ist die Situation noch schlimmer: der Cache-Aspekt wird später initialisiert, daher funktioniert der Aufruf von @Cacheable aus @PostConstruct in der Regel nichtdazu gibt es ein explizites Issue in Spring.

Hier können Sie ApplicationRunner oder CommandLineRunner verwenden, die helfen können, indem sie einfach implementiert werden:

@Service
@EnableCaching
public class CacheClass implements ApplicationRunner {    
	@Autowired    
	@Lazy    
	private CacheClass self;        
 
	@Override    
	public void run(ApplicationArguments args) throws Exception {       
		System.out.println(self.test(1));        
		Thread.sleep(1000);        
		System.out.println(self.test(2));        
		Thread.sleep(1000);        
		System.out.println(self.test(1));    
	}    
	
	@Cacheable(cacheNames = "test", key = "#integer")    
	public String test(int integer) {        
		return LocalDateTime.now().toString();    
	}
}

Code zum Experimentieren mit @Transactional

Um verschiedene Arten der Arbeit mit @Transactional zu testen, habe ich diesen Code verwendet:

@Service
public class TestClass {    
	@Autowired    
	@Lazy    
	private TestClass self;    
 
	@Autowired    
	private ApplicationContext applicationContext;    
 
	@PostConstruct    
	public void init() {        
		System.out.println("Teste @Transactional in @PostConstruct");        
 
		// Test 1: Direkter Aufruf (sollte nicht funktionieren)        
		System.out.println("1. Direkter Aufruf:");        
 
		try {            
			directTransactionalMethod();        
		} catch (Exception e) {            
			System.out.println("Fehler: " + e.getMessage());        
		}        
 
		// Test 2: Über self + @Lazy        
		System.out.println("2. self + @Lazy:");        
		self.selfTransactionalMethod();        
		
		// Test 3: Über ApplicationContext        
		System.out.println("3. ApplicationContext:");        
		TestClass proxy = applicationContext.getBean(TestClass.class);        
		proxy.contextTransactionalMethod();        
		System.out.println("@PostConstruct ausgeführt");    
	}    
 
	@Transactional(propagation = Propagation.REQUIRES_NEW)    
	public void directTransactionalMethod() {
		checkTransaction("directTransactionalMethod");    
	}    
 
	@Transactional(propagation = Propagation.REQUIRES_NEW)    
	public void selfTransactionalMethod() {
		checkTransaction("selfTransactionalMethod");    
	}    
 
	@Transactional(propagation = Propagation.REQUIRES_NEW)    
	public void contextTransactionalMethod() {
		checkTransaction("contextTransactionalMethod");    
	}    
 
	private void checkTransaction(String methodName) {        
		boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();   
		String transactionName = TransactionSynchronizationManager.getCurrentTransactionName();        
		System.out.println("   " + methodName + ":");        
		System.out.println("   - Transaktion aktiv: " + isActive);        
		System.out.println("   - Transaktionsname: " + transactionName);    
	}
}

Ergebnis:

Teste @Transactional in @PostConstruct
1. Direkter Aufruf:
   directTransactionalMethod:
   - Transaktion aktiv: false
   - Transaktionsname: null
2. self + @Lazy:
   selfTransactionalMethod:
   - Transaktion aktiv: true
   - Transaktionsname: TestClass.selfTransactionalMethod
3. ApplicationContext:
   contextTransactionalMethod:
   - Transaktion aktiv: true
   - Transaktionsname: TestClass.contextTransactionalMethod

Er ist ziemlich einfach; mit seiner Hilfe können Sie verschiedene Methoden ausprobieren oder Ihre eigenen testen)

Fazit

Heute haben wir die wichtigsten Aspekte von Spring betrachtet: die Arbeit mit Beans, die wichtigsten Annotationen und Fallstricke, die bei Java/Kotlin-Vorstellungsgesprächen oft auftauchen. Die Themenliste wurde auf Basis meiner Erfahrung und der Erfahrung von Kollegen erstellt, die Vorstellungsgespräche für Positionen von Junior bis Senior durchlaufen haben.