CodeGym /Kurse /Spring Core /Von ApplicationListener zu @EventListener

Von ApplicationListener zu @EventListener

Spring Core
Level 18, Lektion 0
Verfügbar

1. Kurzform für Listener

Wenn Sie gerade anfangen, Listener zu schreiben, scheint ApplicationListener<T> ideal: alles streng, typsicher und ohne Überraschungen. Aber sobald die Anwendung etwas lebendiger wird, stellt sich plötzlich heraus, dass es mehrere Reaktionen auf ein Ereignis geben kann und auch Ereignisse mehr als eines sind. Und Ihr Projekt beginnt, mit Dutzenden kleiner Klassen zu überwuchern, die sich nur in drei Zeilen unterscheiden.

Das Problem ist nicht, dass „viel Code schlecht“ ist, sondern dass dieser Code zu Rauschen wird. Sie lesen das Projekt und sehen: „aha, eine Klasse, die nur existiert, um ein Interface zu implementieren und eine Methode zu schreiben“. Das ist keine Katastrophe, aber es verwandelt sich schnell in visuellen Müll, in dem es schwierig wird, wirklich wichtige Details zu erkennen: welche Reaktion genau wir machen, welche Abhängigkeiten sie hat und wie sehr sie überhaupt zum aktuellen Geschäftsszenario gehört.

Und hier bietet Spring eine bequemere Schreibweise für dieselbe Bedeutung: Statt „die Klasse ist ein Listener“ beginnen wir zu denken „diese Methode ist ein Ereignishandler“. Das klingt fast wie „oh, endlich eine normale Welt“, denn oft ist die Reaktion auf ein Ereignis tatsächlich eine kleine Methode und keine vollwertige Entität, die ein eigenes Interface verdient.

2. Wie publishEvent(...) funktioniert

Bevor wir zur Annotation übergehen, ist es wichtig, das Gesamtbild im Auge zu behalten. Es ist sehr leicht, @EventListener als „neue Technologie“ zu betrachten, die irgendwie anders funktioniert. Tatsächlich ändert sich nur die Form der Handlerdeklaration, während das Ereignismodell selbst dasselbe bleibt: es gibt eine Veröffentlichung, eine Zustellung und einen Aufruf der Handler.

Es ist praktisch, dies als einfaches Schema im Kopf zu behalten. Wir studieren die inneren Abläufe von Spring nicht im Detail, aber das mentale Modell sollte stabil sein:

flowchart TD
    A["Use-Case-Service"] -->|publishEvent| B["ApplicationEventPublisher"]
    B --> C["Ereigniszustellung innerhalb des Kontexts"]
    C --> D1["Listener #1"]
    C --> D2["Listener #2"]
    C --> D3["Listener #3"]

@EventListener fügt keinen neuen Block „magischer Ereignisbus“ hinzu. Es sagt Spring lediglich: „Diese Methode soll als Handler für ein solches Ereignis registriert werden“. Danach macht der Container alles genauso wie gestern: Wenn das Ereignis veröffentlicht wird, wird es an die Abonnenten zugestellt.

Im Projekt ContextFlow bedeutet dies, dass OrderPlacementService und OrderCancellationService dieselben „Ereignisveröffentlicher“ bleiben und Audit/Benachrichtigungen/Statistiken dieselben „Reaktionen“ bleiben. Wir schreiben sie lediglich in einer bequemeren Form um, damit der Code wie eine Sammlung von Reaktionen gelesen wird und nicht wie eine Ansammlung von „Klassen um des Interfaces willen“.

3. ApplicationListener<T>: Vorteile und Rauschen

Damit der Übergang zu @EventListener sinnvoll ist, werfen wir einen ehrlichen Blick auf den Ausgangspunkt. ApplicationListener<T> hat Stärken: der Ereignistyp ist am generischen Parameter sichtbar, der Handlervertrag ist formalisiert, die IDE schlägt die Signatur hervorragend vor, und Sie können kaum „versehentlich“ das falsche Ereignis abhören.

So könnte gestern unser Audit zur Auftragserstellung ausgesehen haben (vereinfacht, um nicht in Details zu verlieren):

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component // Spring erstellt einen Bean, und erst dann wird er Abonnent des Ereignisses
public class AuditOrderCreatedListener implements ApplicationListener<OrderCreatedEvent> {

    @Override
    public void onApplicationEvent(OrderCreatedEvent event) {
        // Wichtig: Der Ereignistyp wird durch den generischen Parameter der Schnittstelle festgelegt
        System.out.println("AUDIT created: " + event.orderId()); // AUDIT created: ...
    }
}

Bei einer kleinen Anzahl von Listenern ist das in Ordnung. Aber stellen Sie sich vor, Sie müssen auch OrderCancelledEvent abhören, plus beide Ereignisse an einer Stelle protokollieren, plus Sie haben Statistiken, Berichte, „Benachrichtigungen über verschiedene Kanäle“ und andere Freuden des Lebens. Dann erstellen Sie entweder viele gleichartige Klassen oder beginnen, komplexe Prüfungen innerhalb eines Listeners für verschiedene Typen einzubauen (und verlieren die Eleganz des typsicheren Modells).

Und hier taucht eine einfache Frage auf: Müssen wir wirklich jedes Mal die Reaktion als „separate Listener-Klasse“ umsetzen, wenn es semantisch gesehen nur eine Methode ist: onOrderCreated(...)? Normalerweise nicht. Und genau diese Frage beantwortet @EventListener.

4. @EventListener: Handler als Methode eines Beans

Den Übergang zu @EventListener sollte man nicht als „neues Ereignissystem“ verstehen, sondern als bequemere Syntax über dem Alten. Wir erstellen immer noch einen Spring-Bean, leben immer noch im ApplicationContext und nutzen immer noch Constructor Injection. Nur müssen wir jetzt kein Interface implementieren, um zu sagen „diese Methode reagiert auf ein Ereignis“.

Hier ist dasselbe Audit, aber im method-based-Stil:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Wichtig: Der Methodenhandler wird nur bei einem Bean registriert
public class AuditOrderEventsListener {

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Der Ereignistyp wird aus dem Methodenparameter bezogen (nicht aus dem generischen Interface)
        System.out.println("AUDIT created: " + event.orderId()); // AUDIT created: ...
    }
}

Vom Ergebnis her ist alles gleich: Bei der Veröffentlichung eines OrderCreatedEvent ruft Spring den Handler auf. Aber solchen Code liest man lieber: Sie sehen den Methodennamen (also die Absicht), Sie sehen den Ereignisparameter (also den Typ), und Sie sind nicht gezwungen, jeden Handler in eine separate „Interface-Entität“ zu verwandeln.

Ein weiteres angenehmes Detail: Jetzt kann eine Klasse mehrere Handler enthalten, und das ist oft genau das, was wir wollen. Zum Beispiel kann ein Bean alle Order-Ereignisse protokollieren, wenn seine Verantwortung wirklich „Ereignisse von Aufträgen protokollieren“ ist, nicht „alles Mögliche tun“.

5. Wie der Ereignistyp ausgewählt wird

Wenn Sie ApplicationListener<T> verwenden, wird der Ereignistyp durch den Parameter T festgelegt. Wenn Sie @EventListener verwenden, wird der Ereignistyp normalerweise durch den Parameter der Methode bestimmt. Wenn die Methode also OrderCreatedEvent akzeptiert, wird sie genau für OrderCreatedEvent aufgerufen (und möglicherweise für seine Untertypen, falls Sie solche einführen).

Das fühlt sich fast zu einfach an, und darin liegt gerade die Stärke: Ihre Methode sieht aus wie eine normale Java-Methode, und Spring „abonniert“ sie einfach auf ein Ereignis des entsprechenden Typs.

Halten wir einen Moment fest, wie Ereignisse in ContextFlow aussehen könnten. Wir haben im Kurs bereits Payload-Events besprochen: wenn das Ereignis einfach ein normales Objekt ist (nicht unbedingt ein Erbe von ApplicationEvent). In modernen Spring-Anwendungen ist das oft der angenehmste Stil.

Zum Beispiel ein Ereignis zur Auftragserstellung:

import com.example.contextflow.domain.model.NotificationChannel;

/**
 * Payload-Ereignis: ein normales Objekt, das Spring an die Listener ausliefert.
 * orderId — die Bestell-ID.
 * channel — der Kanal, der von den Handlern (Benachrichtigungen usw.) benötigt werden kann.
 */
public record OrderCreatedEvent(String orderId, NotificationChannel channel) { }

Und ein Handler, der darauf reagiert, bekommt diesen Payload als Parameter:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Wenn @Component weggelassen wird, erstellt Spring keinen Bean – und der Handler wird nicht aufgerufen
public class OrderEventsLogger {

    @EventListener
    public void onCreated(OrderCreatedEvent event) {
        // Hier ist event der genaue Payload, der über publishEvent(...) veröffentlicht wurde
        System.out.println("created " + event.orderId()); // created ...
    }
}

Beachten Sie den wichtigen psychologischen Effekt: @EventListener lässt das Ereignis wie ein normales Methodenargument erscheinen. Das reduziert die „Ritualität“ des Codes erheblich. Sie haben nicht das Gefühl, „in eine spezielle Spring-Welt einzutreten“. Sie schreiben eine normale Java-Methode mit einem verständlichen Parameter – und das war's.

Es gibt jedoch eine Grenze. Wenn Sie anfangen, Handler „zu schlau“ zu machen, die halbe Geschäftslogik hineinzustecken, wird das Ganze nicht mehr einfach sein. Aber die Annotation selbst hat damit nichts zu tun. Sie macht lediglich das, was wir bereits können, bequemer.

6. Mehrere Handler in einem Bean

Nach der Bekanntschaft mit @EventListener kommen viele in Versuchung: „Oh! Dann kann ich alle Handler in eine Klasse packen und glücklich leben.“ Das sieht nach Dateisparen aus, verwandelt sich in der Praxis aber schnell in einen „God Listener“, der alles weiß und alles macht. Dabei haben wir den ganzen Kurs damit verbracht, die Kopplung zu verringern, nicht sie an einer neuen Stelle zu vergrößern.

Eine Regel, die auf Junior-Niveau gut funktioniert: Ein Listener-Bean kann mehrere @EventListener-Methoden haben, wenn sie zu einer klaren Verantwortung gehören. Zum Beispiel „Protokollierung von Order-Ereignissen“ ist eine Verantwortung. „Audit aller Aktionen zu Aufträgen“ ist ebenfalls eine. „Sowohl Audit als auch Benachrichtigungen als auch Statistiken als auch Berichterstellung als auch noch ein bisschen Magie“ – das ist ein Kombinat.

Hier ist ein gutes Beispiel: ein Bean, zwei Reaktionen auf zwei Ereignisse, aber beide betreffen die Protokollierung:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Ein Bean – mehrere Handler, wenn die Verantwortung einheitlich ist
public class OrderEventsLogger {

    @EventListener
    public void onCreated(OrderCreatedEvent event) {
        // Reaktion auf die Auftragserstellung
        System.out.println("created " + event.orderId()); // created ...
    }

    @EventListener
    public void onCancelled(OrderCancelledEvent event) {
        // Reaktion auf die Auftragsstornierung
        System.out.println("cancelled " + event.orderId()); // cancelled ...
    }
}

Und hier ein Beispiel, das besser nicht gemacht werden sollte (ich führe es absichtlich nicht als Code an, um es nicht zu verfestigen): „In einer Klasse drei Methoden – eine schreibt Audit, die zweite sendet Benachrichtigungen, die dritte zählt Statistiken“. Das wirkt kompakt, aber aus Sicht des Projektlesens ist es eine Verschlechterung: Sie verlieren die Modularität und beginnen, verschiedene Änderungsgründe zu vermischen.

In ContextFlow ist es gerade nützlich, dass Audit getrennt von Benachrichtigungen lebt, und Benachrichtigungen getrennt von Statistiken. Dann kann man die Reaktion nicht nur lesen, sondern auch per Profil deaktivieren, die Implementierung ändern, testen und unabhängig weiterentwickeln. Darauf werden wir in den nächsten Lektionen des Tages noch zurückkommen, aber den Kern der Disziplin pflanzt man besser jetzt.

7. Migration von ContextFlow auf @EventListener

Jetzt machen wir einen sehr praktischen Schritt: Wir schreiben einen Listener so um, dass er method-based wird. Wichtig zu betonen: Wir ändern weder die Domäne noch das Szenario noch die Reihenfolge der Aktionen, und wir komplizieren das Projekt nicht. Wir ändern nur die Form der Handlerdeklaration, damit es uns später leichter fällt, Seiteneffekte aus Services in Listener zu verlagern.

Stellen wir uns vor, dass wir bereits einen Audit-Service haben, der Einträge über AuditWriter schreibt (im dev-Profil auf die Konsole). Er könnte etwa so aussehen:

import org.springframework.stereotype.Service;

@Service // Dies ist die Abhängigkeit des Listeners: Hierhin wird der „Nebeneffekt“ der Auditierung delegiert
public class AuditService {

    public void recordOrderCreated(String orderId) {
        // In einem echten Projekt steht hier normalerweise eine DB-/Log-/externe Audit-Service-Schreibweise
        System.out.println("AUDIT: order created " + orderId); // AUDIT: order created ...
    }
}

Nun der Listener. Früher (gestern): Interface-Stil.

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component // Spring erstellt einen Bean und registriert ihn als ApplicationListener
public class AuditOrderCreatedListener implements ApplicationListener<OrderCreatedEvent> {

    private final AuditService auditService;

    public AuditOrderCreatedListener(AuditService auditService) {
        // Normale Constructor Injection: keine Magie, nur eine Abhängigkeit
        this.auditService = auditService;
    }

    @Override
    public void onApplicationEvent(OrderCreatedEvent event) {
        // Reaktion auf das Ereignis: Delegiere den „Nebeneffekt“ an den Audit-Service
        auditService.recordOrderCreated(event.orderId());
    }
}

Heute (jetzt): Annotationsstil. Dieselbe Logik, dieselben Abhängigkeiten, sogar der Methodenname ähnelt, nur versteht Spring jetzt das Abonnement über die Annotation.

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component // Immer noch ein normaler Spring Bean
public class AuditOrderEventsListener {

    private final AuditService auditService;

    public AuditOrderEventsListener(AuditService auditService) {
        // Abhängigkeit wie zuvor
        this.auditService = auditService;
    }

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Die Reaktion auf das Ereignis wird jetzt durch eine Annotation an der Methode ausgedrückt
        auditService.recordOrderCreated(event.orderId());
    }
}

Was haben wir gewonnen? Jetzt können wir in derselben Klasse (falls sinnvoll) einen zweiten Handler hinzufügen, z. B. für die Auftragsstornierung, ohne einen separaten Interface-Listener erzeugen zu müssen.

Und ein sehr wichtiger Gedanke, den man sich buchstäblich laut sagen sollte: AuditOrderEventsListener ist immer noch ein normaler Spring Bean. Wenn Sie ihn von Hand mit new (AuditOrderEventsListener(...)) irgendwo außerhalb des Containers erzeugen, wird @EventListener nicht „aktiviert“. Die Annotation ist kein magischer Zauberspruch, der im luftleeren Raum wirkt. Sie funktioniert nur, weil der Container Beans scannt und Methoden als Handler registriert.

Wenn Sie nach dieser Migration Ihr Szenario zur Auftragserstellung ausführen, sollten Sie denselben Audit-Effekt sehen wie zuvor, denn wir haben die Bedeutung nicht geändert, sondern nur anders notiert.

8. Typische Fehler bei @EventListener

Fehler Nr. 1: @EventListener als „neues Ereignissystem“ betrachten, nicht als bequeme Form des Alten.
Manchmal entsteht nach der ersten Annotation bei einem Neuling das Gefühl: „Wow, jetzt habe ich einen Event Bus!“. Nein, Sie haben immer noch denselben Spring Application Event-Mechanismus. Wenn Sie früher verstanden haben, was bei publishEvent(...) passiert, deklarieren Sie jetzt nur den Handler kürzer. Das ist wichtig, um nicht in die Fantasie von „Asynchronität standardmäßig“ und „Ereignisse regeln alles selbst“ abzudriften.

Fehler Nr. 2: Den Handler nicht als Bean anlegen und erwarten, dass er aufgerufen wird.
@EventListener funktioniert nur an Methoden von Objekten, die tatsächlich im ApplicationContext leben. Wenn Sie @Component vergessen, das Paket nicht in den Scanning einbezogen oder die Klasse falsch registriert haben, erstellt der Container sie nicht, und das Ereignis verpufft ins Leere. Ereignisse sind keine Reflektion über den gesamten Classpath, sondern ein Abonnement konkreter Beans.

Fehler Nr. 3: Alle Handler der Welt in einen „universellen“ Listener packen.
Mit Annotationen entsteht die Versuchung, eine Klasse für alles zu machen: Audit, Benachrichtigungen, Statistiken, Berichte. Das verschlechtert die Architektur: Sie schaffen wieder eine Kopplungszentrale, nur heißt sie jetzt nicht OrderPlacementService, sondern EverythingListener. Gruppieren Sie Methoden nach Verantwortung, nicht nach dem Prinzip „Hauptsache weniger Dateien“.

Fehler Nr. 4: Den Sinn des Handlers hinter einem schlechten Methodennamen verstecken.
Wenn ein Listener ein Interface ist, haben Sie immer onApplicationEvent, und der Sinn muss im Methodenrumpf gesucht werden. Im @EventListener-Modell ist der Methodenname ein Teil der Lesbarkeit. Wenn Sie die Methode handle() oder doStuff() nennen, schießen Sie sich selbst ins Knie. onOrderCreated, onOrderCancelled, auditCreated, sendNotificationOnCreated – lassen Sie es länger, aber verständlicher.

Fehler Nr. 5: Den Listener „aus Gewohnheit“ lazy machen und sich dann über die erste Verzögerung und späte Fehler wundern.
Selbst wenn der Listener korrekt registriert ist, bedeutet eine lazy-Erstellung, dass das echte Objekt erst beim ersten Ereignis erscheinen kann. Dann kann die erste Bestellung plötzlich langsamer werden, und ein Fehler im Konstruktor des Listeners zeigt sich nicht beim Start der Anwendung, sondern beim ersten Ereignis. Für den grundlegenden Teaching-Stil ist es besser, Listener als normale Singleton-Beans ohne @Lazy zu halten, damit die Anwendung fail-fast und vorhersagbar bleibt.

1
Aufgabe
Spring Core, Level 18, Lektion 0
Gesperrt
Zahlungslistener als Bean-Methode
Zahlungslistener als Bean-Methode
1
Aufgabe
Spring Core, Level 18, Lektion 0
Gesperrt
Zwei Ereignisse in einem Listener-Bean
Zwei Ereignisse in einem Listener-Bean
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION