Kontakt
stefan.bente[at]th-koeln.de
+49 2261 8196 6367
Discord Server
Prof. Bente Personal Zoom
Adresse
Steinmüllerallee 1
51643 Gummersbach
Raum 3219
Sprechstunde nach Vereinbarung
Terminanfrage: calendly.com Wenn Sie dieses Tool nicht nutzen wollen, schicken Sie eine Mail und ich weise Ihnen einen Termin zu.

Eine Variante des Service ohne Broker: Statt eines Handlers, der als Orchestrator die Aggregates nacheinander aufruft, veröffentlicht der Receiver das eingehende Event als Spring Event, und jedes Aggregate reagiert selbst darauf (Choreographie). Die Seite zeigt, was sich dafür ändert, was man gewinnt und was man dafür bezahlt.

Sprache
English version
Beispielcode-Repository
https://gitlab.com/archi-lab/public/learning/learn-event-driven-architecture/-/tree/main/service-development-2a-with-spring-events
Keywords
Event-getriebene Architektur, Microservices, Spring Events, ApplicationEventPublisher, EventListener, Orchestrator, Choreographie, Handler, Receiver, MEA

Dieselbe Reaktion mit Spring Events

Dieses Modul ist eine Variante von service-development-2-with-interfaces-and-tests. Es löst dieselbe Aufgabe: Der Billing-Service reagiert auf das Event RentalEnded, berechnet die Fahrt und zählt sie. Der Unterschied liegt darin, wer die Aggregates aufruft.

In Modul 2 macht das der RentalEndedHandler. Er ist ein Orchestrator, das heißt, er kennt alle Reaktionen auf das Event und ruft sie nacheinander auf. In dieser Variante gibt es keinen Handler. Der Receiver veröffentlicht das Event innerhalb des Service als Spring Event, und jedes Aggregate reagiert selbst darauf. Das nennt man Choreographie. Alles andere ist wie in Modul 2: der Core, der Mapper, das Interface ChargeEvents und die Tests.

Der Weg einer Nachricht

flowchart LR
    Receiver["RentalEndedReceiver<br/>receive(payloadJson)"]:::new
    subgraph Charge["charge"]
        ChargeListener["RentalEndedChargeListener<br/>@EventListener"]:::new
        Charging["ChargingService"]:::existing
        Events["ChargeEvents<br/>(Interface)"]:::existing
        ChargeListener --> Charging
        Charging --> Events
    end
    subgraph Ride["ride"]
        RideListener["RentalEndedRideListener<br/>@EventListener"]:::new
        Rides["RideCountService"]:::existing
        RideListener --> Rides
    end

    Receiver -- "RentalEnded<br/>als Spring Event" --> ChargeListener
    Receiver -- "RentalEnded<br/>als Spring Event" --> RideListener

    classDef existing fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px
    classDef new fill:#fff3cd,stroke:#ff9800,stroke-width:3px
    style Charge fill:#fafafa,stroke:#9e9e9e
    style Ride fill:#fafafa,stroke:#9e9e9e

Orange mit dickem Rand ist, was gegenüber Modul 2 neu ist oder sich geändert hat, grün ist, was unverändert aus Modul 2 kommt.

Was sich gegenüber Modul 2 ändert

thkoeln.archilab.servicedevelopment
├── eventhandlers
│   ├── RentalEnded                     unverändert, wird jetzt als Spring Event veröffentlicht
│   └── infrastructure
│       └── RentalEndedReceiver         geändert: veröffentlicht das Event
├── charge
│   └── application
│       └── RentalEndedChargeListener   neu
└── ride
    └── application
        └── RentalEndedRideListener     neu

Der RentalEndedHandler und sein Test entfallen.

Der Receiver veröffentlicht das Event

public void receive( String payloadJson ) {
    events.publishEvent( mapper.toRentalEnded( payloadJson ) );
}

events ist der ApplicationEventPublisher von Spring. Der Receiver weiß jetzt nicht mehr, wer auf das Event reagiert. Er gibt es nur an Spring weiter.

Jedes Aggregate reagiert selbst

@EventListener
public void on( RentalEnded rental ) {
    chargingService.chargeForRide( rental.rentalId(), rental.customerId(), rental.rideDuration() );
}

Spring ruft jede Methode mit @EventListener auf, deren Parameter zum Typ des Events passt. Diese Klassen heißen tatsächlich Listener, weil sie eine Annotation tragen, über die Spring sie aufruft. Das ist der Unterschied zum Receiver, der keine trägt. Jeder Listener liegt im Package application seines Aggregates. Er ruft dort den Service auf, der die Geschäftsregel kennt.

Die Listener laufen synchron, im selben Thread wie der Receiver. Jeder Service öffnet wie in Modul 2 seine eigene Transaktion.

Orchestrator oder Choreographie?

Der Vorteil der Choreographie: Eine neue Reaktion auf das Event ist eine neue Klasse. Dafür muss niemand eine bestehende Klasse ändern, auch nicht den Receiver.

Der Preis: Es gibt keine Stelle mehr, an der man alle Reaktionen auf das Event und ihre Reihenfolge sieht. In welcher Reihenfolge die Listener laufen, legt Spring fest, solange man es nicht mit @Order vorgibt. Und im Debugger springt man von publishEvent in die Listener, was schwerer nachzuvollziehen ist als die zwei Zeilen im Handler.

In unseren Konventionen bleibt der Handler der Standard, weil er für den Einstieg leichter zu verstehen und zu debuggen ist. Diese Variante zeigt, wie es auch geht.

Testen

Die Tests sind dieselben wie in Modul 2, mit einer Ausnahme. Statt des Handlers testet RentalEndedEventIT, was die Aggregates mit dem Event tun. Dazu veröffentlicht der Test das Event direkt, in Domain-Typen:

@Test
void aFinishedRentalIsChargedCountedAndAnnounced() {
    events.publishEvent( new RentalEnded( RENTAL_ID, CUSTOMER_ID, Duration.ofMinutes( 20 ) ) );

    List<Charge> charges = chargingService.chargesOf( CUSTOMER_ID );
    assertThat( charges ).hasSize( 1 );
    assertThat( rideCountService.ridesOf( CUSTOMER_ID ) ).isEqualTo( 1 );
}

RentalEndedReceiverIT ist unverändert. Er prüft weiterhin den ganzen Weg vom JSON bis zum veröffentlichten Event ChargeCreated, jetzt eben über die Spring Events.

Das Modul ausführen

Voraussetzung: Java 21 und Maven. Aus dem Hauptverzeichnis des Repositories:

mvn verify -pl service-development-2a-with-spring-events

Die Tests laufen ohne Kafka und ohne Docker; die Datenbank ist eine H2 im Speicher.