Woche 8, Tag 2 — Spring Events
Ziel
Heute verstehst du Spring Application Events.
Die Kernfragen:
- Was ist ein Spring-Event?
- Warum Events nutzen?
- Was ist
ApplicationEventPublisher? - Was ist
@EventListener? - Sind Spring-Events synchron oder asynchron?
- Was ist ein transaktionales Event?
- Was ist
@TransactionalEventListener? - Was bedeutet
AFTER_COMMIT? - Wann solltest du Events nutzen?
- Wann solltest du keine Events nutzen?
- Was sind häufige Prüfungsfallen?
1. Kurz-Wiederholung aus Woche 8, Tag 1
In Tag 1 hast du gelernt:
- AOP bedeutet Aspect-Oriented Programming.
- AOP trennt Querschnittsbelange von der Geschäftslogik.
- Spring AOP ist proxy-basiert.
- AOP funktioniert, wenn Aufrufe über den Spring-Proxy laufen.
- Selbstaufruf umgeht den Proxy.
@Transactional, Methodensicherheit, Caching und Async setzen oft auf Proxies.- Nutze AOP für technische Querschnittsbelange.
Merksatz:
Spring AOP funktioniert, indem Methoden über einen Proxy aufgerufen werden.
Heute lernst du einen weiteren Spring-Kommunikationsmechanismus:
Spring Application Events.
2. Was ist ein Spring-Event?
Ein Spring-Event ist eine Nachricht, die innerhalb des Spring Application Context veröffentlicht wird.
Einfache Idee:
Etwas ist passiert.
Andere Teile der Anwendung können darauf reagieren.
Beispiele:
UserRegisteredEvent
TaskCreatedEvent
InvoicePaidEvent
DocumentUploadedEvent
PasswordResetRequestedEvent
ClientOnboardedEvent
Statt dass ein Service viele andere Services direkt aufruft, kann er ein Event veröffentlichen.
Merksatz:
Ein Spring-Event sagt: Etwas ist in der Anwendung passiert.
3. Warum Events nutzen?
Ohne Events:
public void registerUser(RegisterUserRequest request) {
User user = userRepository.save(...);
emailService.sendWelcomeEmail(user);
auditService.auditUserRegistered(user);
crmService.syncUser(user);
notificationService.notifyAdmin(user);
}
Problem:
UserService kennt zu viele andere Services.
Die Methode wird lang.
Eine neue Reaktion erfordert Änderungen am UserService.
Geschäftsaktion und Nebeneffekte sind vermischt.
Mit Events:
public void registerUser(RegisterUserRequest request) {
User user = userRepository.save(...);
eventPublisher.publishEvent(new UserRegisteredEvent(user.getId(), user.getEmail()));
}
Dann reagieren Listener:
WelcomeEmailListener
AuditListener
CrmSyncListener
AdminNotificationListener
Merksatz:
Events reduzieren die direkte Kopplung zwischen der Hauptaktion und Nebeneffekten.
4. Direkter Aufruf vs. Event
Direkter Aufruf
UserService -> EmailService
UserService -> AuditService
UserService -> NotificationService
Event-Stil
UserService -> veröffentlicht UserRegisteredEvent
WelcomeEmailListener hört zu
AuditListener hört zu
NotificationListener hört zu
Wichtig:
Der Publisher muss nicht alle Listener kennen.
Merksatz:
Der Publisher kennt das Event, nicht jede Reaktion.
5. Event-Publisher
Spring stellt bereit:
ApplicationEventPublisher
Damit veröffentlichst du Events.
Beispiel:
@Service
public class UserRegistrationService {
private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;
public UserRegistrationService(
UserRepository userRepository,
ApplicationEventPublisher eventPublisher
) {
this.userRepository = userRepository;
this.eventPublisher = eventPublisher;
}
@Transactional
public UserDto register(RegisterUserRequest request) {
UserEntity user = new UserEntity(request.email());
UserEntity saved = userRepository.save(user);
eventPublisher.publishEvent(
new UserRegisteredEvent(
saved.getId(),
saved.getEmail()
)
);
return new UserDto(saved.getId(), saved.getEmail());
}
}
Merksatz:
ApplicationEventPublisherveröffentlicht Events in den Spring Application Context.
6. Event-Klasse
Moderne Spring-Events können einfache POJOs oder Records sein.
Beispiel:
public record UserRegisteredEvent(
Long userId,
String email
) {
}
Der ältere Stil kann erweitern:
ApplicationEvent
Beispiel:
public class UserRegisteredEvent extends ApplicationEvent {
private final Long userId;
private final String email;
public UserRegisteredEvent(Object source, Long userId, String email) {
super(source);
this.userId = userId;
this.email = email;
}
public Long getUserId() {
return userId;
}
public String getEmail() {
return email;
}
}
Für den meisten modernen App-Code sind einfache Records sauber.
Merksatz:
Event-Klassen sollten beschreiben, was passiert ist.
7. Event-Listener
Ein Listener reagiert auf Events.
Beispiel:
@Component
public class WelcomeEmailListener {
private final EmailService emailService;
public WelcomeEmailListener(EmailService emailService) {
this.emailService = emailService;
}
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
}
Wenn UserRegisteredEvent veröffentlicht wird, ruft Spring diese Methode auf.
Merksatz:
@EventListenermarkiert eine Methode, die auf ein Event reagiert.
8. Mehrere Listener
Ein Event kann viele Listener haben.
@Component
public class AuditListener {
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
System.out.println("Audit user registered: " + event.userId());
}
}
@Component
public class AdminNotificationListener {
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
System.out.println("Notify admin about user: " + event.email());
}
}
Eine Veröffentlichung:
eventPublisher.publishEvent(new UserRegisteredEvent(1L, "user@example.com"));
Kann auslösen:
WelcomeEmailListener
AuditListener
AdminNotificationListener
Merksatz:
Ein Event kann viele Listener haben.
9. Event-Ablauf
Service-Methode
↓
eventPublisher.publishEvent(event)
↓
Spring ApplicationEventMulticaster
↓
passende @EventListener-Methoden
↓
Listener-Logik läuft
Einfaches Bild:
UserRegistrationService
veröffentlicht UserRegisteredEvent
↓
WelcomeEmailListener
AuditListener
AdminNotificationListener
Merksatz:
Spring verteilt veröffentlichte Events an passende Listener.
10. Sind Spring-Events synchron?
Standardmäßig sind Spring-Event-Listener synchron.
Das bedeutet:
publishEvent() wartet, bis die Listener fertig sind.
Beispiel:
eventPublisher.publishEvent(new UserRegisteredEvent(...));
Wenn der Listener eine E-Mail sendet und 3 Sekunden braucht:
publishEvent() kann ebenfalls 3 Sekunden dauern.
Wichtig:
Standard-Spring-Events sind nicht automatisch asynchron.
Merksatz:
Standardmäßig sind Spring-Events synchron.
11. Folge synchroner Events
Wenn der Listener eine Exception wirft:
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
throw new RuntimeException("Email failed");
}
Dann kann auch der Publisher fehlschlagen, weil der Listener im selben Thread läuft.
Wenn die Publisher-Methode transaktional ist, kann das die Transaktion beeinflussen.
Merksatz:
Bei synchronen Events kann ein Listener-Fehler den Publisher beeinflussen.
12. Soll ein Listener-Fehler den Haupt-Use-Case abbrechen?
Stelle dir diese Design-Frage:
Wenn die Willkommens-E-Mail fehlschlägt, soll die Benutzerregistrierung fehlschlagen?
Manchmal ja:
Zahlungsvalidierung fehlgeschlagen
Betrugsprüfung fehlgeschlagen
Verpflichtende Domänenregel verletzt
Oft nein:
Willkommens-E-Mail fehlgeschlagen
Audit-Logging vorübergehend fehlgeschlagen
Benachrichtigung fehlgeschlagen
Analytics-Event fehlgeschlagen
Wenn ein Fehler den Haupt-Use-Case nicht abbrechen soll, behandle ihn sorgfältig.
Merksatz:
Entscheide, ob ein Listener-Fehler die Hauptaktion abbrechen soll.
13. Transaktionsproblem
Stell dir diesen Service vor:
@Transactional
public UserDto register(RegisterUserRequest request) {
UserEntity user = userRepository.save(new UserEntity(request.email()));
eventPublisher.publishEvent(
new UserRegisteredEvent(user.getId(), user.getEmail())
);
return new UserDto(user.getId(), user.getEmail());
}
Standard-Event-Listener:
@EventListener
public void sendWelcomeEmail(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
Problem:
Der Event-Listener kann laufen, bevor die Transaktion committed wird.
Wenn die Transaktion später zurückgerollt wird:
Die E-Mail wurde möglicherweise schon gesendet, obwohl der Benutzer nicht committed wurde.
Merksatz:
Ein normaler
@EventListenerkann laufen, bevor die Transaktion committed wird.
14. Transaktionaler Event-Listener
Nutze:
@TransactionalEventListener
wenn der Listener abhängig vom Transaktionsergebnis laufen soll.
Beispiel:
@Component
public class WelcomeEmailListener {
private final EmailService emailService;
public WelcomeEmailListener(EmailService emailService) {
this.emailService = emailService;
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
}
Das bedeutet:
Führe diesen Listener aus, nachdem die Transaktion erfolgreich committed wurde.
Merksatz:
@TransactionalEventListener(AFTER_COMMIT)läuft nur nach erfolgreichem Commit.
15. Transaktionsphasen
Gängige Transaktionsphasen:
BEFORE_COMMIT
AFTER_COMMIT
AFTER_ROLLBACK
AFTER_COMPLETION
Bedeutung:
| Phase | Bedeutung |
|---|---|
BEFORE_COMMIT | vor dem Commit der Transaktion |
AFTER_COMMIT | nach erfolgreichem Commit |
AFTER_ROLLBACK | nach Rollback |
AFTER_COMPLETION | nach Commit oder Rollback |
Am häufigsten für Nebeneffekte:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
Merksatz:
Nutze
AFTER_COMMITfür Nebeneffekte, die erst nach erfolgreichem Datenbank-Commit passieren sollen.
16. @EventListener vs. @TransactionalEventListener
| Thema | @EventListener | @TransactionalEventListener |
|---|---|---|
| Transaktionsbewusst | nein, normaler Event-Listener | ja |
| Standard-Timing | sofort bei Veröffentlichung | an Transaktionsphase gebunden |
| Typischer Einsatz | einfache In-Memory-Reaktion | After-Commit-Nebeneffekte |
| Läuft ohne Transaktion | ja | standardmäßig nein |
| Beispiel | einfaches Event loggen | E-Mail senden nach Benutzer-Save |
Merksatz:
Nutze normale Events für einfache Reaktionen; transaktionale Events für transaktionssensitive Nebeneffekte.
17. Fallback-Ausführung
@TransactionalEventListener erwartet normalerweise eine Transaktion.
Wenn keine Transaktion existiert, läuft er möglicherweise nicht.
Es gibt eine Option:
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT,
fallbackExecution = true
)
Bedeutung:
Wenn keine Transaktion existiert, trotzdem ausführen.
Nutze das vorsichtig.
Merksatz:
fallbackExecution = truelässt den transaktionalen Listener auch ohne Transaktion laufen.
18. Asynchrone Events
Manchmal soll Listener-Arbeit asynchron passieren.
Beispiel:
E-Mail senden
CRM synchronisieren
Analytics-Event senden
PDF generieren
externes System benachrichtigen
Nutze @Async am Listener:
@Component
public class WelcomeEmailListener {
private final EmailService emailService;
public WelcomeEmailListener(EmailService emailService) {
this.emailService = emailService;
}
@Async
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
}
Async aktivieren:
@Configuration
@EnableAsync
public class AsyncConfig {
}
Merksatz:
@Asynckann Event-Listener-Arbeit in einem anderen Thread ausführen lassen.
19. Async-Event-Falle
@Async nutzt ebenfalls Spring-Proxy/Interception.
Wichtig:
Der Listener-Bean muss ein Spring Bean sein.
@EnableAsync muss aktiviert sein.
Async-Ausführung nutzt einen anderen Thread.
ThreadLocal-Kontext ist nicht automatisch verfügbar.
Exceptions werden anders behandelt.
Beispielproblem:
SecurityContext ist möglicherweise nicht automatisch verfügbar.
Transaktionskontext ist nicht derselbe Thread.
MDC/Logging-Kontext wird möglicherweise nicht automatisch übernommen.
Merksatz:
Asynchrone Listener laufen nicht im selben Thread-Kontext.
20. Async + transaktionales Event
Gängiges Muster:
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
Bedeutung:
Nach Transaktions-Commit das Event asynchron verarbeiten.
Gut für:
E-Mails
Benachrichtigungen
externe Synchronisation
Analytics
Aber denk daran:
Das ist immer noch In-Process-Async.
Wenn die App abstürzt, kann das Event verloren gehen.
Merksatz:
Asynchrone transaktionale Events sind nützlich, aber kein durable Messaging.
21. Spring-Events sind In-Process
Spring Application Events sind In-Process.
Das bedeutet:
innerhalb einer laufenden Anwendungsinstanz
innerhalb einer JVM
nicht automatisch gespeichert
nicht automatisch wiederholt
nicht automatisch mit anderen Services geteilt
Wenn die App nach dem Commit, aber vor dem Ende des Async-Listeners abstürzt:
Event kann verloren gehen
Für durable Messaging nutze:
Kafka
RabbitMQ
SQS
Database-Outbox-Pattern
Message Broker
Merksatz:
Spring-Events sind kein Ersatz für durable Messaging.
22. Gutes Event-Design
Gute Event-Namen:
UserRegisteredEvent
TaskCompletedEvent
InvoicePaidEvent
DocumentUploadedEvent
ClientCreatedEvent
PasswordResetRequestedEvent
Schlechte Event-Namen:
SendEmailEvent
CallAuditServiceEvent
DoNotificationEvent
Warum?
Gutes Event sagt, was passiert ist.
Schlechtes Event sagt, was der Listener tun soll.
Merksatz:
Benenne Events nach Fakten, nicht nach Befehlen.
23. Was soll ein Event enthalten?
Gute Event-Payload:
public record UserRegisteredEvent(
Long userId,
String email
) {
}
Üblicherweise enthalten:
IDs
kleine unveränderliche Daten
Geschäftsfakten
Timestamp, falls nützlich
Tenant-ID, falls nötig
Vermeide:
große Entity-Graphen
verwaltete JPA-Entities
sensible Daten
Klartext-Passwörter
zu viel veränderlicher Zustand
Merksatz:
Events sollten kleine, unveränderliche Fakten sein.
24. JPA-Entities in Events vermeiden
Schlecht:
public record UserRegisteredEvent(UserEntity user) {
}
Probleme:
Entity kann später lazy geladen werden
Entity kann detached sein
Listener kann Entity versehentlich ändern
Payload wird groß
Transaktionsgrenze wird verwirrend
Besser:
public record UserRegisteredEvent(
Long userId,
String email
) {
}
Merksatz:
Bevorzuge IDs und einfache Werte statt JPA-Entities in Events.
25. Event-Beispiel: Task erstellt
Event:
public record TaskCreatedEvent(
Long taskId,
Long tenantId,
String title
) {
}
Publisher:
@Service
public class TaskService {
private final TaskRepository taskRepository;
private final ApplicationEventPublisher eventPublisher;
public TaskService(
TaskRepository taskRepository,
ApplicationEventPublisher eventPublisher
) {
this.taskRepository = taskRepository;
this.eventPublisher = eventPublisher;
}
@Transactional
public TaskDto create(CreateTaskRequest request, CurrentUser currentUser) {
TaskEntity task = new TaskEntity(
request.title(),
currentUser.tenantId()
);
TaskEntity saved = taskRepository.save(task);
eventPublisher.publishEvent(
new TaskCreatedEvent(
saved.getId(),
saved.getTenantId(),
saved.getTitle()
)
);
return new TaskDto(saved.getId(), saved.getTitle());
}
}
Listener:
@Component
public class TaskCreatedAuditListener {
private final AuditService auditService;
public TaskCreatedAuditListener(AuditService auditService) {
this.auditService = auditService;
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void audit(TaskCreatedEvent event) {
auditService.recordTaskCreated(
event.taskId(),
event.tenantId(),
event.title()
);
}
}
Merksatz:
Veröffentliche Domänenfakten aus Services und reagiere in Listenern.
26. Event-Beispiel: Passwort-Reset
Event:
public record PasswordResetRequestedEvent(
Long userId,
String email,
String resetToken
) {
}
Publisher:
@Transactional
public void requestPasswordReset(String email) {
UserEntity user = userRepository.findByEmail(email)
.orElseThrow();
String token = resetTokenService.createToken(user);
eventPublisher.publishEvent(
new PasswordResetRequestedEvent(
user.getId(),
user.getEmail(),
token
)
);
}
Listener:
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendResetEmail(PasswordResetRequestedEvent event) {
emailService.sendPasswordResetEmail(
event.email(),
event.resetToken()
);
}
Wichtig:
Sei vorsichtig mit sensiblen Tokens.
Logge sie nicht.
Merksatz:
Events können E-Mails nach dem Commit auslösen, aber sensible Daten müssen geschützt werden.
27. Listener-Reihenfolge
Wenn mehrere Listener existieren, kann die Reihenfolge wichtig sein.
Nutze:
@Order(1)
Beispiel:
@Component
public class FirstListener {
@Order(1)
@EventListener
public void handle(UserRegisteredEvent event) {
}
}
@Component
public class SecondListener {
@Order(2)
@EventListener
public void handle(UserRegisteredEvent event) {
}
}
Aber verlasse dich nicht zu stark auf die Listener-Reihenfolge.
Wenn die Reihenfolge geschäftskritisch ist, kann direkte Orchestrierung klarer sein.
Merksatz:
Listener-Reihenfolge existiert, aber geschäftskritische Reihenfolge sollte explizit sein.
28. Events aus Listenern zurückgeben
Eine @EventListener-Methode kann einen Rückgabewert haben, der ein weiteres Event wird.
Beispiel:
@EventListener
public WelcomeEmailEvent onUserRegistered(UserRegisteredEvent event) {
return new WelcomeEmailEvent(event.userId(), event.email());
}
Das kann nützlich sein, macht den Ablauf aber auch schwerer verständlich.
Für die Prüfung merk dir:
Event-Listener können weitere Events auslösen.
Merksatz:
Rückgabewerte von Listenern können Folge-Events veröffentlichen — nutze das vorsichtig.
29. Bedingter Event-Listener
@EventListener kann Bedingungen nutzen.
Beispiel:
@EventListener(condition = "#event.email.endsWith('@example.com')")
public void handleExampleDomainUser(UserRegisteredEvent event) {
// nur example.com-Benutzer
}
Das nutzt SpEL.
Manchmal nützlich, aber nicht übermäßig nutzen.
Merksatz:
Event-Listener können bedingt sein.
30. Events vs. Methodenaufrufe
Nutze direkte Methodenaufrufe, wenn:
die Aktion sofort erforderlich ist
der Aufrufer das Ergebnis braucht
die Reihenfolge wichtig ist
die Logik Teil desselben Use-Cases ist
ein Fehler die Hauptoperation abbrechen soll
Nutze Events, wenn:
etwas passiert ist
andere Komponenten reagieren können
der Publisher nicht alle Reaktionen kennen soll
Nebeneffekte getrennt werden können
später neue Listener hinzukommen können
Merksatz:
Direkter Aufruf für erforderliche Arbeit; Event für Reaktionen auf etwas, das passiert ist.
31. Events vs. Message Broker
Nutze Spring-Events für:
In-Process-Entkopplung
dieselbe Anwendung
einfache Nebeneffekte
lokale Modul-Kommunikation
After-Commit-Listener
Nutze einen Message Broker für:
Kommunikation zwischen Services
durable Zustellung
Retry
Backpressure
Event-Replay
verteilte Systeme
Event-Driven Architecture über Apps hinweg
Merksatz:
Spring-Events sind lokal; Broker sind verteilt und durable.
32. Events und Hexagonal/Clean Architecture
Events können Clean Architecture unterstützen.
Application Service:
führt den Haupt-Use-Case aus
veröffentlicht ein Domänen-Event
Listener/Adapter:
E-Mail senden
Audit-Log schreiben
externes System benachrichtigen
Suchindex synchronisieren
Das hält den Kern-Use-Case sauberer.
Aber nutze Events nicht, um unklaren Kontrollfluss zu verstecken.
Merksatz:
Events können Module entkoppeln, aber zu viele Events können den Ablauf verstecken.
33. Event-Veröffentlichung testen
Service-Test mit gemocktem Publisher:
class UserRegistrationServiceTest {
private final UserRepository userRepository = mock(UserRepository.class);
private final ApplicationEventPublisher eventPublisher =
mock(ApplicationEventPublisher.class);
private final UserRegistrationService service =
new UserRegistrationService(userRepository, eventPublisher);
@Test
void publishesUserRegisteredEvent() {
when(userRepository.save(any(UserEntity.class)))
.thenReturn(new UserEntity(1L, "user@example.com"));
service.register(new RegisterUserRequest("user@example.com"));
ArgumentCaptor<UserRegisteredEvent> captor =
ArgumentCaptor.forClass(UserRegisteredEvent.class);
verify(eventPublisher).publishEvent(captor.capture());
UserRegisteredEvent event = captor.getValue();
assertThat(event.userId()).isEqualTo(1L);
assertThat(event.email()).isEqualTo("user@example.com");
}
}
Merksatz:
Unit-Tests können prüfen, dass ein Service das richtige Event veröffentlicht.
34. Event-Listener testen
Listener-Unit-Test:
class WelcomeEmailListenerTest {
private final EmailService emailService = mock(EmailService.class);
private final WelcomeEmailListener listener =
new WelcomeEmailListener(emailService);
@Test
void sendsWelcomeEmail() {
listener.onUserRegistered(
new UserRegisteredEvent(1L, "user@example.com")
);
verify(emailService).sendWelcomeEmail("user@example.com");
}
}
Das testet nicht die Spring-Event-Verteilung.
Es testet die Listener-Logik.
Merksatz:
Listener-Logik kann als normaler Unit-Test getestet werden.
35. Spring-Event-Integration testen
Integrationstest:
@SpringBootTest
class EventIntegrationTest {
@Autowired
private ApplicationEventPublisher eventPublisher;
@MockitoBean
private EmailService emailService;
@Test
void listenerReceivesPublishedEvent() {
eventPublisher.publishEvent(
new UserRegisteredEvent(1L, "user@example.com")
);
verify(emailService).sendWelcomeEmail("user@example.com");
}
}
Das beweist:
Spring verteilt das Event an den Listener
Listener ruft den E-Mail-Service auf
Merksatz:
Nutze den Spring-Test-Context, um echte Event-Verteilung zu testen.
36. Transaktionalen Event-Listener testen
Transaktionale Event-Listener sind schwerer zu testen, weil sie von Commit/Rollback abhängen.
Beispielansatz:
@SpringBootTest
class TransactionalEventIntegrationTest {
@Autowired
private UserRegistrationService userRegistrationService;
@MockitoBean
private EmailService emailService;
@Test
void sendsEmailAfterCommit() {
userRegistrationService.register(
new RegisterUserRequest("user@example.com")
);
verify(emailService).sendWelcomeEmail("user@example.com");
}
}
Aber sei vorsichtig:
Wenn die Testmethode selbst @Transactional ist und zurückrollt,
läuft der AFTER_COMMIT-Listener möglicherweise nicht.
Merksatz:
AFTER_COMMIT-Listener brauchen einen echten Commit, um zu laufen.
37. Häufige Test-Falle
Schlechter Test:
@SpringBootTest
@Transactional
class TransactionalEventTest {
@Test
void sendsEmailAfterCommit() {
userRegistrationService.register(...);
verify(emailService).sendWelcomeEmail(...);
}
}
Problem:
Die Test-Transaktion rollt nach dem Test zurück.
Der AFTER_COMMIT-Listener läuft möglicherweise nicht.
Lösungsoptionen:
@Transactional nicht auf den Test setzen
TestTransaction nutzen, um die Transaktion manuell zu beenden/committen
Listener-Logik separat testen
Integrationstest-Design sorgfältig planen
Merksatz:
Erwarte nicht, dass ein
AFTER_COMMIT-Event innerhalb einer zurückgerollten Test-Transaktion läuft.
38. Häufige Prüfungsfallen
Falle 1
Spring-Events sind In-Process-Events.
Falle 2
Standardmäßig sind Spring-Event-Listener synchron.
Falle 3
publishEvent() kann warten, bis die Listener fertig sind.
Falle 4
Listener-Exceptions können den Publisher im synchronen Modus beeinflussen.
Falle 5
Ein normaler @EventListener ist nicht transaktionsphasenbewusst.
Falle 6
@TransactionalEventListener(AFTER_COMMIT) läuft nach erfolgreichem Commit.
Falle 7
AFTER_COMMIT-Listener laufen möglicherweise nicht, wenn die Transaktion zurückrollt.
Falle 8
@TransactionalEventListener läuft möglicherweise ohne Transaktion nicht, außer mit fallbackExecution = true.
Falle 9
@Async braucht @EnableAsync.
Falle 10
Asynchrone Listener laufen in einem anderen Thread.
Falle 11
Spring-Events sind kein durable Messaging.
Falle 12
Übergib keine großen JPA-Entities in Events.
Falle 13
Benenne Events nach dem, was passiert ist, nicht nach dem, was getan werden soll.
Falle 14
Nutze direkte Methodenaufrufe, wenn der Aufrufer das Ergebnis braucht oder ein Fehler den Use-Case stoppen soll.
Falle 15
Nutze Message Broker, wenn durable Zustellung zwischen Services nötig ist.
39. Echte Prüfungsfrage: Spring-Event
Frage:
Was ist ein Spring Application Event?
Antwort:
Ein Spring Application Event ist ein Objekt, das innerhalb des Application Context veröffentlicht wird, um zu signalisieren, dass etwas passiert ist. Spring verteilt das Event an passende Listener.
40. Echte Prüfungsfrage: ApplicationEventPublisher
Frage:
Wofür wird ApplicationEventPublisher genutzt?
Antwort:
Damit veröffentlichst du Events in den Spring Application Context, indem du publishEvent(...) aufrufst.
41. Echte Prüfungsfrage: @EventListener
Frage:
Was macht @EventListener?
Antwort:
Es markiert eine Spring-Bean-Methode als Event-Listener. Spring ruft die Methode auf, wenn ein passendes Event veröffentlicht wird.
42. Echte Prüfungsfrage: synchron oder asynchron
Frage:
Sind Spring-Events standardmäßig asynchron?
Antwort:
Nein. Standardmäßig sind Spring-Event-Listener synchron.
43. Echte Prüfungsfrage: transaktionales Event
Frage:
Wofür wird @TransactionalEventListener genutzt?
Antwort:
Damit bindest du die Listener-Ausführung an eine Transaktionsphase, zum Beispiel nach Commit oder nach Rollback.
44. Echte Prüfungsfrage: AFTER_COMMIT
Frage:
Was bedeutet TransactionPhase.AFTER_COMMIT?
Antwort:
Der Listener läuft nur, nachdem die umgebende Transaktion erfolgreich committed wurde.
45. Echte Prüfungsfrage: asynchroner Listener
Frage:
Was brauchst du, um @Async auf Event-Listenern zu nutzen?
Antwort:
Die Listener-Methode muss auf einem Spring Bean liegen, und Async-Support muss mit @EnableAsync aktiviert sein.
46. Interview-Antwort
Frage:
Erkläre Spring-Events in einfachen Worten.
Gute Antwort:
Spring-Events erlauben einem Teil der Anwendung zu veröffentlichen, dass etwas passiert ist, und andere Spring Beans können zuhören und reagieren. Zum Beispiel kann der Service nach einer Benutzerregistrierung UserRegisteredEvent veröffentlichen, und separate Listener können eine Willkommens-E-Mail senden, ein Audit-Log schreiben oder einen Admin benachrichtigen. Das reduziert die direkte Kopplung zwischen dem Haupt-Service und Nebeneffekten.
47. Interview-Antwort
Frage:
Sind Spring-Events synchron oder asynchron?
Gute Antwort:
Standardmäßig sind Spring-Events synchron. Der publishEvent()-Aufruf wartet, bis die Listener fertig sind. Wenn ich asynchrone Verarbeitung will, kann ich @Async mit @EnableAsync nutzen oder den Event-Multicaster konfigurieren — aber ich muss bedenken, dass asynchrone Listener in einem anderen Thread laufen und kein durable Messaging sind.
48. Interview-Antwort
Frage:
Warum
@TransactionalEventListener(AFTER_COMMIT)nutzen?
Gute Antwort:
Ich nutze AFTER_COMMIT, wenn ein Nebeneffekt erst nach erfolgreichem Commit der Datenbank-Transaktion passieren soll. Zum Beispiel sollte das Senden einer Willkommens-E-Mail nach der Benutzerregistrierung üblicherweise nur passieren, wenn der Benutzer wirklich gespeichert wurde. Ein normaler @EventListener kann laufen, bevor die Transaktion committed wird — das kann Nebeneffekte für zurückgerollte Daten verursachen.
49. Interview-Antwort
Frage:
Was ist der Unterschied zwischen Spring-Events und Kafka?
Gute Antwort:
Spring-Events sind lokale, In-Process-Events innerhalb eines Application Context. Sie sind nützlich, um Module innerhalb derselben Anwendung zu entkoppeln. Kafka ist ein verteilter, persistenter Message Broker für Kommunikation zwischen Services, Retries, Backpressure und Event-Driven Architecture über Anwendungen hinweg. Spring-Events sind nicht durable und können verloren gehen, wenn die Anwendung abstürzt.
50. Kleine Code-Übung
Erstelle ein Event für Task-Abschluss.
Event:
public record TaskCompletedEvent(
Long taskId,
Long tenantId,
String title
) {
}
Publisher:
@Service
public class TaskService {
private final TaskRepository taskRepository;
private final ApplicationEventPublisher eventPublisher;
public TaskService(
TaskRepository taskRepository,
ApplicationEventPublisher eventPublisher
) {
this.taskRepository = taskRepository;
this.eventPublisher = eventPublisher;
}
@Transactional
public TaskDto complete(Long taskId) {
TaskEntity task = taskRepository.findById(taskId)
.orElseThrow();
task.complete();
eventPublisher.publishEvent(
new TaskCompletedEvent(
task.getId(),
task.getTenantId(),
task.getTitle()
)
);
return new TaskDto(task.getId(), task.getTitle(), task.getStatus());
}
}
Listener:
@Component
public class TaskCompletedAuditListener {
private final AuditService auditService;
public TaskCompletedAuditListener(AuditService auditService) {
this.auditService = auditService;
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void audit(TaskCompletedEvent event) {
auditService.recordTaskCompleted(
event.taskId(),
event.tenantId(),
event.title()
);
}
}
Frage:
Warum AFTER_COMMIT nutzen?
Antwort:
Weil der Audit-Nebeneffekt erst passieren soll, nachdem der Task-Abschluss erfolgreich committed wurde.
51. Kleine Bug-Übung 1
Problem:
@Transactional
public void registerUser(RegisterUserRequest request) {
UserEntity user = userRepository.save(...);
eventPublisher.publishEvent(new UserRegisteredEvent(user.getId(), user.getEmail()));
throw new RuntimeException("Something failed");
}
Listener:
@EventListener
public void sendEmail(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
Frage:
Was ist gefährlich?
Antwort:
Der normale @EventListener kann laufen, bevor die Transaktion committed wird. Die E-Mail kann gesendet werden, obwohl die Transaktion später zurückrollt.
Besser:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
52. Kleine Bug-Übung 2
Problem:
@Async
@EventListener
public void handle(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
Aber es läuft trotzdem synchron.
Frage:
Was fehlt möglicherweise?
Antwort:
@EnableAsync fehlt möglicherweise.
53. Kleine Bug-Übung 3
Problem:
public record UserRegisteredEvent(UserEntity user) {
}
Frage:
Warum ist das nicht ideal?
Antwort:
Eine JPA-Entity in einem Event kann Lazy-Loading-Probleme, Detached-Entity-Probleme, versehentliche Änderungen, große Payloads und verwirrende Transaktionsgrenzen verursachen. Bevorzuge IDs und einfache Werte.
54. Kleine Bug-Übung 4
Problem:
public record SendWelcomeEmailEvent(String email) {
}
Frage:
Warum ist dieser Event-Name nicht ideal?
Antwort:
Er benennt die Aktion, die ein Listener ausführen soll, nicht den passierten Fakt. Besser:
public record UserRegisteredEvent(Long userId, String email) {
}
Übungsfragen
Frage 1
Was ist ein Spring-Event?
Antwort:
Ein Spring-Event ist ein Objekt, das innerhalb des Application Context veröffentlicht wird, um zu signalisieren, dass etwas passiert ist.
Frage 2
Warum Events nutzen?
Antwort:
Events reduzieren die direkte Kopplung zwischen der Haupt-Geschäftsaktion und Nebeneffekten oder Reaktionen.
Frage 3
Was macht ApplicationEventPublisher?
Antwort:
ApplicationEventPublisher veröffentlicht Events, indem du publishEvent(...) aufrufst.
Frage 4
Was macht @EventListener?
Antwort:
@EventListener markiert eine Spring-Bean-Methode als Listener für einen passenden Event-Typ.
Frage 5
Sind Spring-Events standardmäßig synchron?
Antwort:
Ja. Standardmäßig sind Spring-Event-Listener synchron.
Frage 6
Was passiert, wenn ein synchroner Listener eine Exception wirft?
Antwort:
Die Exception kann zum Publisher zurückpropagieren und die Publisher-Operation zum Fehlschlagen bringen.
Frage 7
Warum kann ein normaler @EventListener innerhalb einer Transaktion gefährlich sein?
Antwort:
Weil der Listener laufen kann, bevor die Transaktion committed wird. Wenn die Transaktion später zurückrollt, sind Nebeneffekte möglicherweise schon passiert.
Frage 8
Was macht @TransactionalEventListener?
Antwort:
Es bindet die Listener-Ausführung an eine Transaktionsphase.
Frage 9
Was bedeutet AFTER_COMMIT?
Antwort:
Der Listener läuft, nachdem die Transaktion erfolgreich committed wurde.
Frage 10
Was bedeutet AFTER_ROLLBACK?
Antwort:
Der Listener läuft, nachdem die Transaktion zurückgerollt wurde.
Frage 11
Was bedeutet fallbackExecution = true?
Antwort:
Es erlaubt dem transaktionalen Event-Listener zu laufen, auch wenn keine Transaktion existiert.
Frage 12
Wie mache ich einen Event-Listener asynchron?
Antwort:
Annotiere den Listener mit @Async.
Frage 13
Was braucht @Async?
Antwort:
Async-Support muss mit @EnableAsync aktiviert sein, und der Listener muss ein Spring Bean sein.
Frage 14
Sind Spring-Events durable Messaging?
Antwort:
Nein. Spring-Events sind lokale In-Process-Events und kein durable Messaging.
Frage 15
Wann solltest du Kafka/RabbitMQ statt Spring-Events nutzen?
Antwort:
Nutze einen Broker, wenn die Kommunikation zwischen Services läuft, durable sein muss, wiederholt werden muss oder Anwendungsabstürze überleben soll.
Frage 16
Was sollen Event-Namen beschreiben?
Antwort:
Event-Namen sollten beschreiben, was passiert ist — zum Beispiel UserRegisteredEvent, nicht was getan werden soll.
Frage 17
Was soll eine Event-Payload enthalten?
Antwort:
Kleine unveränderliche Daten wie IDs, Tenant-IDs, E-Mail, Status, Timestamp oder andere Geschäftsfakten.
Frage 18
Warum JPA-Entities in Events vermeiden?
Antwort:
Weil JPA-Entities Lazy Loading, Detached-Entity-Probleme, versehentliche Änderungen, große Payloads und Transaktionsgrenzen-Probleme verursachen können.
Frage 19
Wie teste ich Event-Veröffentlichung?
Antwort:
Mocke ApplicationEventPublisher, rufe den Service auf, fange das veröffentlichte Event mit ArgumentCaptor ab und prüfe seine Felder.
Frage 20
Wie teste ich Listener-Logik?
Antwort:
Instanziiere den Listener mit gemockten Abhängigkeiten, rufe die Listener-Methode direkt auf und prüfe dann den erwarteten Nebeneffekt.
Merksätze zum Mitnehmen
- Spring-Events signalisieren, dass etwas passiert ist.
ApplicationEventPublisherveröffentlicht Events.@EventListenerempfängt Events.- Ein Event kann viele Listener haben.
- Standardmäßig sind Spring-Events synchron.
publishEvent()wartet auf synchrone Listener.- Listener-Exceptions können den Publisher beeinflussen.
- Ein normaler
@EventListenerkann laufen, bevor die Transaktion committed wird. @TransactionalEventListenerbindet die Listener-Ausführung an eine Transaktionsphase.AFTER_COMMITläuft nach erfolgreichem Commit.AFTER_ROLLBACKläuft nach Rollback.fallbackExecution = trueläuft auch ohne Transaktion.@Asynckann Listener asynchron machen.@EnableAsyncwird für Async-Support benötigt.- Asynchrone Listener laufen in einem anderen Thread-Kontext.
- Spring-Events sind In-Process, kein durable Messaging.
- Nutze Kafka/RabbitMQ/Outbox für durable verteilte Events.
- Benenne Events nach Fakten, nicht nach Befehlen.
- Halte Event-Payloads klein und unveränderlich.
- Bevorzuge IDs und einfache Werte statt JPA-Entities.
- Direkter Aufruf für erforderliche Arbeit; Event für Reaktionen.