2.11.2022 |

Spring ApplicationEvents und Spring Modulith Framework

Im Rahmen des Java Forum Nord haben wir einen Talk von Oliver Drotbohm hören dürfen. Das grundlegende Thema war das Spring Modulith Framework. Die Idee des Frameworks ist die Bereitstellung einer Möglichkeit einen Monolithen in Module aufzuteilen und die Kommunikation der Module mit Spring Application Events zu realisieren und dadurch die interne Kopplung des Monolithen zu minimieren.

Neu waren dabei für mich die diversen Optionen, wie Datenbank-Transaktionen mit Spring Application Events interagieren. Sehr spannend war auch die Architektur die durch das Framework entsteht. Durch die Aufteilung der Applikation in Module, wird das separate Testen dieser Module innerhalb des Monolithen möglich.

In einem kleinen Testprojekt auf Github habe ich das Framework und die Events einmal ausprobiert.

Die Architektur mit Spring Modulith:

Module werden auf ebene der Spring Application in Form von packages definiert. Ressourcen die sich die Applikationen teilen werden auf oberster Ebene des Moduls bereitgestellt und modulinterne Resourcen in einem weiteren Subpackages (hier "internal") verpackt.

Screenshot_2022-11-02_at_13.18.43

Die Kommunikation unterhalb der Module:

Um die Kopplung unterhalb der Module zu verringern können die Module mit Spring Applikation Events kommunizieren. Spring Events können auf verschiedenste Art und Weise implementiert werden:

  • Manuelles publishen mithilfe des ApplikationEventsPublisher:
private final ApplicationEventPublisher applicationEventPublisher;

public void fieldCreated(Field field) {
    applicationEventPublisher.publishEvent(new FieldCreatedEvent(field));
}
  • Sammeln und Publishen von DomainEvents. Die an einer Entität definierten DomainEvents werden bei einem save gepublished.
public class Field extends AbstractAggregateRoot<Field> {

   //...

    @Transactional
    public Field createField() {
        registerEvent(new FieldCreatedEvent(this));
        return this;
    }
}

Die Listener für diese Events können die Verarbeitung innerhalb einer Transaktion oder losgelöst nach dem Commit einer Transaktion verarbeiten. "EventListener" werden innerhalb einer Transaktion verarbeitet und bei TransactionalEventListenern kann die Transactions-Phase der Verarbeitung definiert werden.

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void on(FieldCreatedEvent fieldCreatedEvent) {
    mailSender.handle(fieldCreatedEvent.field());
}

Wenn ein Fehler innerhalb einer Transaktion auftritt, findet ein Rollback statt. Findet ein Fehler in Events außerhalb der Transaktion statt, bietet das Modulith eine Möglichkeit, die gefailten Events zu persistieren.

Screenshot_2022-11-02_at_20.33.02

Das Testen der Module:

Die Module können mithilfe des Frameworks separat getestet werden. Durch die Annotation @ApplicationModuleTest werden lediglich die für das Testen des Moduls nötigen Beans erstellt.

@ApplicationModuleTest
@Testcontainers
class CreateFieldTest {

    //...

    private CreateField cut;

    @Test
    void publishesOrderCompletion(PublishedEvents events) {
        Field field = cut.handle("field");
        var createdEvents = events.ofType(FieldCreatedEvent.class).matching(ev -> ev.field().equals(field));
        assert createdEvents.iterator().hasNext();
    }
}

Das Framework bietet auch weiterhin die Möglichkeit die Modulstruktur mit ArchUnit-Tests zu überprüfen oder eine aufbereitete Dokumentation mit Aufbau und Abhängigkeiten unter den Modulen kann automatisiert erstellt werden.

Michael
Zur Übersicht

Mehr vom DevSquad...

Dennis Hundertmark

Angular Router Standalone APIs

Lucas Meurer

NextJS 13 and React Server Components