Reaktive REST-Webservices mit Spring WebFlux

Mit Spring WebFlux entwickeln wir deutlich performanterer Web-Anwendungen und REST-Services. WebFlux ist Teil vom Spring Reactor Projekt. Es ist die moderne, reaktive Alternative zu Spring MVC. In diesem Artikel baue ich mit Mono und Flux eine reaktive API.

Unterschied zwischen WebFlux und Spring MVC

Das Reactor Projekt bildet die Grundlage des reaktiven Stacks in Spring. Es bietet eine Event-basierte, nicht blockierende Architektur, so dass darauf aufbauende Anwendungen mehr Leistung aus ihren CPU-Ressourcen herausholen.

Spring WebFlux ist Teil des reaktiven Stacks. Es ist das reaktive Gegenstück zu Spring MVC im klassischen Servlet Stack.
Weitere Infos zu Spring MVC findet ihr in meinem Blog: spring-mvc-thymeleaf.html
Weitere Details zum Spring Reactor Projekt findet ihr hier: https://spring.io/reactive
Von dort stammt die folgende Gegenüberstellung zur besseren Einordnung der einzelnen Komponenten aus dem Servlet und reaktivem Stack.

Gegenüberstellung Reactive und Servlet Stack

Im ersten Blog-Artikel zur reaktiven Programmierung stellte ich den Spring WebClient vor und zeigte, dass dieser deutlich performanter ist als der klassische HTTP Client: reactive-webclient.html

Hier zeige ich die Vorteile der reaktiven Programmierung anhand einer mit Spring WebFlux geschriebene Serverseite. Konkret entwickle ich eine REST-API und schicke Mono und Flux als Antwort an einen aufrufenden Spring WebClient.

Video zur Umstellung von Spring MVC auf WebFlux

Design Pattern: Publisher-Subscriber

Die Kommunikation zwischen Client und Server funktioniert in Spring WebFlux nach dem Publisher und Subscriber Entwurfsmuster. Serverseitig agiert Spring WebFlux als Publisher und publiziert Daten als Mono oder Flux, sobald diese verfügbar sind. Clientseitig meldet sich der Spring WebClient beim Publisher an (subscribe) und wartet dann in einem separaten Thread auf die Daten der Server-Antwort. Der Haupt-Thread auf Client-Seite kann ist nicht blockiert und läuft weiter, weil das Warten in einem anderen Clientseitigen Thread geschieht. Dadurch haben wir eine Nachrichten-basierte, nicht blockierende Kommunikation, so wie in der reaktiven Programmierung benötigt. Weitere Details zum Design Pattern findet ihr hier: Wikipedia_Publish_Subscribe

Was ist ein Mono?

Wenn die Serverseite mit Spring WebFlux einen oder keinen Datensatz als Ergebnis liefert, dann verwenden wir als Antworttyp Mono. Dazu ein Beispiel aus der analogen Welt:

Im Restaurant bestellt der Gast (Clientseite) ein Hauptgericht (Mono). Die Küche (Serverseite) fängt nun an das Gericht zu kochen. Sie bringt es dem Gast auf einem Teller (Mono), wenn es fertig ist. In der Zwischenzeit kann der Gast noch andere Dinge tun: Trinken, Reden, Handy spielen usw. - der Gast ist also nicht blockiert (reaktiv), während er auf die Nachricht Hauptgang ist fertig wartet.

Was ist ein Flux?

Liefert die Serverseite eine beliebig große Menge an Datensätzen als Ergebnis, so ist der Antworttyp ein Flux. Ein Flux ist ein Datenstrom der vom Server zum Client fließt. Die Flussgeschwindigkeit kann sich dabei beliebig ändern. Dazu wieder ein Beispiel aus der analogen Gastronomie:

Im Sterne-Restaurant kocht die Küche (Serverseite) ein 9 Gänge Menu (Flux) für die Gäste (Clientseite). Dabei kommen die einzelnen Gänge getrennt von einander beim Gast an, manche Gänge kommen schneller hintereinander andere brauchen etwas länger (Datenstrom). Wie schon beim Mono-Beispiel ist der Gast im Restaurant nicht blockiert bzw. reaktiv, da er sich in der Zwischenzeit beliebig beschäftigen kann.

Mono als Antwort einer REST-API

Eine reaktive REST-API schickt den Datensatz nach dem Publisher-Subscriber Prinzip an den Client. Wenn es höchstens ein Datensatz ist, verwenden wir serverseitig einen Mono. Im folgenden wird ein Spring RestController gezeigt, der ein Mapping für eine GET Methode hat: 

@RestController
public class ReactiveEmployeeController {
    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/employee/mono")
    public Mono<Employee> receiveMono() {
        Mono<Employee> result = Mono.fromSupplier(
                () -> generateEmployee())
    .doOnSuccess(
                employee -> log.info("Mono published: " + employee));
log.info("Returning Mono.");
return result;
    }
    ...
}

  • @RestController und @GetMapping erkläre ich hier: rest-apis-in-java.html
  • Die Methode generateEmployee ist ausgeblendet, da sie lediglich eine Employee Instanz erstellt. In meiner Demo wartet die Methode einige Sekunden bevor sie als Ergebnis eine Employee Instanz liefert. Das Warten simuliert eine lang dauernde Suche nach einem Employee in einer großen Datenbank. Je länger die Such- oder Rechenoperationen zum Beschaffen des Ergebnisses dauern und je mehr Anfragen ankommen, desto mehr profitieren wir hinsichtlich Performance von der reaktiven Programmierung.
  • Mono<Employee> ist der Rückgabetyp unserer GET Methode. 
  • Den HTTP Response-Code definiere ich mit der Annotation @ResponseStatus(HttpStatus.OK).
  • return Mono.fromSupplier antwortet dem Client direkt, schickt aber noch nicht den eigentlichen Datensatz. Der Parameter der Methode fromSupplier ist ein Lambda Ausdruck bzw. ein Supplier. Der Supplier berechnet den eigentlichen Datensatz (Employee) und publiziert ihn dann durch den Mono an den Client.
  • doOnSuccess hat als Parameter einen Consumer, den ich hier nur zum Loggen verwende. doOnSuccess wird ausgeführt, wenn der Mono erfolgreich seinen Datensatz publiziert hat.
  • Supplier und Consumer sind funktionale Interfaces, die in Java 8 eingeführt wurden.
Wie man clientseitig mit dem reaktiven Spring WebClient einen Mono abfragt und einen Lambda Ausdruck als Consumer zur Verarbeitung des Datensatzes registriert (subscribe), zeige ich in reactive-webclient.htmlDaher hier ohne weitere Erklärungen der Code der Clientseite:

WebClient reactiveClient = WebClient.builder()
        .baseUrl("http://localhost:8080").build();
reactiveClient.get().uri("/employee/mono")
        .retrieve().bodyToMono(Employee.class)
        .subscribe(employee -> {
    log.info("Response received: " + employee);
});
log.info("Request send. " 
        "Data published in Mono will be handled in another thread.");

Folgende 4 Log-Einträge werden vom Client und Server durch den Logger (log.info) in dieser Reihenfolge geschrieben:

2023-02-21 21:46:12.255  INFO [           main] Client   : Request send. Data published in Mono will be handled in another thread.
2023-02-21 21:46:12.492  INFO [ctor-http-nio-4] ReactiveEmployeeController     : Returning Mono.
2023-02-21 21:46:14.515  INFO [ctor-http-nio-4] ReactiveEmployeeController     : Mono published: Employee(id=-772...)
2023-02-21 21:46:14.654  INFO [ctor-http-nio-3] Client   : Response received: Employee(id=-772...)

Beachtet die verschiedenen clientseitig eingesetzten Threads: main und ctor-http-nio-3. Aufgrund der geringen Last wurde serverseitig nur ein Thread ctor-http-nio-4 verwendet, das können bei höherer Last mehr sein. Die Reihenfolge der Logeinträge zeigt, dass die Threads weder client- noch serverseitig blockiert wurde. Bei einer synchronen, blockierenden Kommunikation wären, die ersten beiden Log-Einträge am Ende: 
  • Clientseitig, weil der Log-Eintrag "Request send..." erst nach erhaltener Antwort vom Server geschrieben wird - vorher ist der main Thread blockiert.
  • Serverseitig, weil zuerst das Ergebnis (Employee Instanz) berechnet wird und erst danach "Returning..." geloggt werden würde. (Inhaltlich machen die geloggten Texte bei einer synchronen Kommunikation natürlich keinen Sinn.)

Flux als Antwort einer REST-API

Wie zuvor erwähnt ist der Unterschied zwischen Mono und Flux die Anzahl der Datensätze, welche der reaktive Server publiziert. Beim Flux können es beliebig viele sein, es ist also eine Art Stream. Im folgenden Beispiel werde ich serverseitig die Datensätze im Flux aus einem Stream beziehen. Das Stream Interface wurde in Java 8 eingeführt und wird zum Beispiel hier vorgestellt: https://ertan-toker.de/java-streams-tutorial-and-examples/

@ResponseStatus(HttpStatus.OK)
@GetMapping(path = "/employee/flux"
    produces = MediaType.APPLICATION_NDJSON_VALUE)
public Flux<Employee> receiveFlux() {
    Supplier<Employee> supplier = () -> generateEmployee();
    return Flux.fromStream(Stream.generate(supplier))
        .doOnNext(employee -> log.info("Flux emits: " + employee));
}

  • Flux<Employee> ist der Rückgabetyp unserer Methode. Damit der Client erkennt, dass die Antwort ein reaktiver Datenstrom (Flux) ist, muss der Content Type für unsere HTTP Response entsprechend als "application/x-ndjson" definiert werden. Das mache ich in der Annotation @GetMapping mit: produces = MediaType.APPLICATION_NDJSON_VALUE 
  • Ansonsten habe ich @GetMapping und @ResponseStatus bereits im Mono Beispiel weiter oben erklärt.
  • Die Supplier<Employee> Instanz ist ein Lambda Ausdruck, der die Methode generateEmployee aufruft. Diese Methode wird auch im Mono Beispiel verwendet und erklärt.
  • Stream.generate erstellt mit Hilfe der Supplier<Employee> Instanz einen Datenstrom vom Typ Stream<Employee>. Die Supplier Instanz befüllt also kontinuierlich den Stream mit neu generierten Employee Objekten bzw. Daten. Dieses Beispiel ist ein endloser Datenstrom. In der Praxis könnte ein Stream aus einer endlichen Liste erstellt werden oder eine reaktive Datenbank-Abfrage, die mit längerer Rechenzeit immer mehr Ergebnisse liefert. Die reaktive Datenbank-Abfrage stelle ich hier vor: spring-data-reactive.html
  • Flux.fromStream ist eine von vielen Möglichkeit eine Flux Instanz zu erstellen. Schaut euch einfach die abstrakte Klasse Flux im Quellcode an, um weitere Möglichkeiten zu sehen.
  • Analog zu Mono (.doOnSuccess) gibt es auch beim Flux diverse Methode, um in den reaktiven Datenstrom einzugreifen. Mit .doOnNext greift Ihr jedes im Flux publizierte Objekt zu - ich logge hier einfach nur das publizierte Objekt.
Die Clientseite sieht beim Flux vergleichbar zum Mono aus:

Flux<Employee> employees = reactiveClient.get().uri("/employee/flux")
        .retrieve().bodyToFlux(Employee.class);
employees.subscribe(employee -> {
    log.info("Part of response received: " + employee);
});
  • reactiveClient ist dieselbe Instanz von WebClient, die zuvor im Mono Beispiel verwendet wurde.
  • bodyToFlux wird anstatt bodyToMono verwendet und legt fest, dass die Antwort des Servers ein Datenstrom ist.
  • Jedes vom Server durch den Flux publizierte Employee Objekt wird in einem Consumer verarbeitet. Der Consumer bindet sich mit der subscribe Methode als Lambda Funktion an den Flux<Employee>.
Hier ein kleiner Ausschnitt aus den client- und serverseitigen Logs:

2023-02-23 21:08:31.258 INFO [ctor-http-nio-4]                            ReactiveEmployeeController: Flux emits: Employee(id=-835...)
2023-02-23 21:08:31.447 INFO [ctor-http-nio-3] Client:
    Parts of response received: Employee(id=-835...)
2023-02-23 21:08:33.353 INFO [ctor-http-nio-4]                            ReactiveEmployeeController: Flux emits: Employee(id=-526...)
2023-02-23 21:08:33.358 INFO [ctor-http-nio-3] Client: 
    Parts of response received: Employee(id=-526...)
2023-02-23 21:08:35.363 INFO [ctor-http-nio-4]                           ReactiveEmployeeController: Flux emits: Employee(id=494...)
2023-02-23 21:08:35.367 INFO [ctor-http-nio-3] Client:
    Parts of response received: Employee(id=494...)

Der Client bzw. ein Thread auf der Clientseite verarbeitet immer dann einen Datensatz, wenn der Datensatz vom Server publiziert wurde. Damit haben wir mit einem HTTP Request einen Datenstrom zwischen Client und Server aufgebaut, der effizient und Nachrichten-basiert in Threads verarbeitet wird. Diese Konstellation ist für mich ein Highlight der reaktiven Programmierung! 🎆

Die langsameren Alternativen in der klassischen Programmierung bzw. bei der Verwendung von synchronen Requests sind: 
  • langes Warten auf eine riesige Antwort mit allen Datensätzen
  • oder viele kleine Abfragen (Pagination), um die Datensätze in kleineren Teilmengen abzufragen.
     

Fazit

Ich habe gezeigt, wie man mit reaktiver Programmierung und Spring WebFlux eine Client-Server-Kommunikation nach dem Publisher und Subscriber Entwurfsmuster aufbauen kann. Sowohl die Client- als auch die Serverseite sind nicht blockierend und damit besonders performant auf aktuellen Rechnern mit mehreren Kernen.

Die Unterschiede zwischen Mono (höchstens ein Datensatz als Antwort) und Flux (beliebig viele Datensätze als Antwort) habe ich anhand von Beispielen erklärt. Wie immer findet ihr den kompletten Code mit JUnit-Tests, die als Clientseite fungieren, in GitHub:

Weiterführendes zum Einsatz von Datenbank in Spring Reactive findet ihr hier: spring-data-reactive.html

Kommentare

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

Authentifizierung in Web-Anwendungen mit Spring Security 6