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.
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.
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.
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.
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.
@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.
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.
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.
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.