24
.
7
.
2018

Reaktive Programmierung mit Java und Spring

Bei der Entwicklung von Software gibt es unterschiedliche Vorgehensweisen und Schwerpunkte. So kann man synchron und blockierend entwickeln (siehe Abbildung 1).

Abbildung 1: main-Thread wird durch I/O blockiert
       ‍Abbildung 1: main-Thread wird durch I/O blockiert

Bei diesem Beispiel führt der main-Thread eine lang andauernde I/O-Operation aus, was zur Folge hat, dass die Anwendung während der Ausführung der I/O-Operation blockiert ist und auf deren Ergebnis wartet. Der Code dazu dürfte für einen Entwickler einfach umzusetzen sein. Eine Verbesserung ist die Auslagerung der Operationen in neue Threads (siehe Abbildung 2).

Abbildung 2: Operationen in neue Threads auslagern: asynchron, aber blockierend
       ‍Abbildung 2: Operationen in neue Threads auslagern: asynchron, aber blockierend

In diesem Fall werden neue Threads für die Operationen erzeugt und die Ergebnisse am Ende zusammengeführt. Das ist effektiver, wenn mehrere Operationen notwendig sind, um das Ergebnis zu erzeugen. Schlussendlich wird der main-Thread blockiert, um die Ergebnisse zu erhalten. Das Erzeugen dieser Threads ist kostenintensiv, das Warten sowie Zusammenführen kompliziert. Beide gezeigten Ansätze blockieren die Anwendung. Was man hingegen erreichen möchte, ist asynchrone, nicht blockierende Verarbeitung. Daher folgt der dritte Ansatz mit einer Ereignisschleife (Event-Loop, siehe Abbildung 3).

Abbildung 3: Verwendung einer Ereignisschleife zur Abarbeitung der Operationen (Operationen in nicht blockierende Teile verlagern)
       ‍Abbildung 3: Verwendung einer Ereignisschleife zur Abarbeitung der Operationen (Operationen in nicht blockierende Teile verlagern)

Dabei arbeitet ein Thread mehrere Anfragen ab und kann die Verarbeitung dieser Anfragen unterbrechen. Dies führt zu einer verbesserten Ressourcen-Nutzung, da kein Thread im Hintergrund auf Ergebnisse wartet. Es werden somit nur so viele Threads verwendet, wie auch benötigt werden. Implementieren kann man diesen Ansatz in Java unterschiedlich, etwa mit Callbacks, was schnell zu einer Callback-Hölle führt, sodass man den Überblick über die aufgerufenen Funktionen verliert. Darüber hinaus kann man in Java Future (mit Java 8 CompletableFuture) verwenden, damit die Anwendung asynchron agiert, ohne Callbacks zu verwenden. Ein großer Nachteil ist, dass dies Ergebnisbasiert funktioniert. Das bedeutet, dass beim Erzeugen eines CompletableFuture<List> die gesamte Liste aufgebaut wird (kein Streaming möglich) und man dadurch den Vorteil der asynchronen Verarbeitung verliert. Hier kommt nun reaktive Programmierung ins Spiel, um die Anwendung asynchron und nicht blockierend zu programmieren; und das alles mit Unterstützung des Streaming-API.

Reaktive Programmierung

Reaktive Programmierung (Reactive Programming) ist ein Programmier-Paradigma. Es fokussiert sich auf Datenströme und das Propagieren von Änderungen. Ziel ist es, asynchrone, eventbasierte und nicht blockierende Operationen/Komponenten zu entwickeln. Dieses Vorgehen verhindert Ressourcen-Verschwendung durch auf I/O-Operationen wartende Threads.

Ein reaktives Beispiel-Programm ist Excel: Ändert man den Wert in einer Zelle, dann ändert sich auch der Wert in der Zelle mit der Berechnung. Microsoft hat mit der Reactive-Extentions-Bibliothek (Rx) im .NET-Ökosystem den Ansatz der reaktiven Programmierung gestartet. Die Bibliothek RxJava hat diesen Ansatz auf die JVM gebracht, wodurch reaktive Programmierung Einzug in Java hielt. Zwischenzeitlich hat sich eine Initiative gegründet, die eine Spezifikation bereitstellt, um einen Standard für das Verarbeiten von asynchronen Datenströmen mit nicht blockierendem Code anzubieten.

Spezifikation und Manifest

Entwickler von Netflix, Pivotal, Twitter und weiteren Unternehmen arbeiten zusammen an einer Spezifikation für reaktive Programmierung, den sogenannten Reactive Streams. Das Projekt stellt neben der Spezifikation ein Java-API, ein Technology-Compatibility-Kit (TCK, zur Validierung der eigenen Implementierung gegen die Spezifikation) sowie Beispiel-Implementierungen bereit. Das Projekt ist unter der Creative-Commons-Zero-Lizenz veröffentlicht. Dabei ist im Einzelnen zu prüfen, ob diese Lizenz im Projekt eingesetzt werden darf.

Interessant ist zudem das reaktive Manifest von Jonas Bonér, Dave Farley, Roland Kuhn und Martin Thompson, das kurz und prägnant die Eigenschaften von reaktiven Systemen darstellt. Die Autoren definieren dazu vier reaktive Qualitäten, die in der Architektur der Softwarekomponente beachtet werden sollen, um reaktive Systeme/Komponenten kombinierbar zu machen:

  • Das System ist responsive (antwortbereit) und antwortet zeitgerecht. Hier sollen konsistente Antwortzeiten eingehalten werden, damit man Fehler durch Ausbleiben von Antworten erkennen und behandeln kann.
  • Bei Ausfällen bleibt das System resilient (widerstandsfähig) und ist responsive. Diese Widerstandsfähigkeit wird durch das Replizieren von Funktionalität, Abschirmung der Komponenten,Eindämmung von Fehlern und das Delegieren von Verantwortung erreicht.
  • Durch die Widerstandsfähigkeit (siehe oben) bleibt die Komponente elastic (elastisch) und kann durch das Replizieren auf eine sich ändernde Last reagieren, weitere Ressourcen beanspruchen oder diese freigeben.
  • Zur Sicherstellung der Entkopplung sowie Isolation werden zwischen den Komponenten asynchron Nachrichten übermittelt. Dies wird mit der vierten Qualität message-driven (nachrichtenorientiert) beschrieben. Durch die Reactive-Streams-Spezifikation und das reaktive Manifest können reaktive Systeme einheitlich designt werden, ohne das Rad immer wieder neu erfinden zu müssen. Dies fördert Kompatibilität und ermöglicht, Komponenten in Projekten wiederzuverwenden.

Interfaces

Die Spezifikation definiert vier Interfaces, siehe dazu Listing 1. Das Publisher-Interface ist die Quelle für die zu verarbeiten Elemente. Damit man diese Daten verarbeiten kann, muss man sich mit einem Subscriber registrieren. Abbildung 4 zeigt den Zusammenhang zwischen den beiden Interfaces.

Abbildung 4: Zusammenhang zwischen Publisher und Subscriber
       ‍Abbildung 4: Zusammenhang zwischen Publisher und Subscriber

Damit der Publisher Daten bereitstellt, muss sich mindestens ein Subscriber registrieren. Passiert dies nicht, werden keine Daten vom Publisher verarbeitet. Sobald ein Subscriber sich registriert hat, benachrichtigt der Publisher ihn über vorhandene Daten und sendet diese an den Subscriber.

github:43a09d9416df1fffe5f5ec70d9849e2a

Zwischen Publisher und Subscriber sitzt ein Subscription-Objekt (siehe Listing 1). Es steuert den Datenfluss. Erhält der Subscriber zu viele Daten vom Publisher, kann er durch die Subscription die Menge reduzieren. Genauso kann der Subscriber die Menge der Daten erhöhen. Dies wird in diesem Zusammenhang Backpressure genannt und soll den Subscriber vor Überlastung schützen (siehe Abbildung 4). Zusätzlich gibt es das Processor-Interface, bei dem sich die Implementierung zugleich als Publisher und Subscriber verhält.

Implementierungen für Java-Entwickler

RxJava wurde in der Einführung schon erwähnt als sogenannte Reactive Extensions für die JVM. Seit Version 2 verwendet RxJava das API der Reactive Streams und hält sich an die Spezifikation. Eine weitere Implementierung der Reactive-Streams-Interfaces bietet das Project Reactor von Pivotal.

Beide Projekte bieten vorgefertigte Subscriber- und Publisher-Implementierungen, um die Entwicklung von reaktiven Applikationen zu erleichtern. Mit Java 9 sind die Interfaces in das Flow-API übernommen worden. Hier muss man als Entwickler die Interfaces selbst implementieren und sich um die Einhaltung der Spezifikation kümmern, da es keine vorgefertigten Implementierungen gibt.

Reaktiv mit Spring

Nachdem das Grundgerüst für reaktive Programmierung geschaffen ist, nun das Beispiel-Projekt mit Spring Boot. Spring bietet über die Webseite einen einfachen Projekt-Generator für Spring-Boot-Applikationen (siehe Abbildung 5). Die Abhängigkeit Reactive Web beinhaltet das Project Reactor, das als Implementierung für die reaktive Programmierung dient.

Abbildung 5: Spring-Boot-Projekt-Generator

Das generierte Projekt ist der Einstieg für die nächsten Beispiele. Wichtig ist, dass man Version 2 oder höher bei Spring Boot verwendet sowie die Abhängigkeit zu Reactive Web und Reactive MongoDB hinzufügt, um in die Vorzüge der reaktiven Programmierung zu kommen.

Als Beispiel-Anwendung dient ein Todo-Webservice, der über REST angesprochen wird und die Daten in einer MongoDB speichert. Das Projekt zeigt auf, was Spring an Implementierungen für eine reaktive Anwendung bereitstellt und welche für das Beispiel genutzt werden. Das Projekt ist auf GitLab veröffentlicht und im Artikel nur in Ausschnitten gezeigt. Listing 2 zeigt das Modell Todo, ein einfaches POJO. Die Klasse verwendet keine speziellen Typen für die reaktive Programmierung und dient nur zur Datenhaltung. Die beiden Annotationen @Document und @Id dienen dem Datenbank-Zugriff.

github:9d55ca0cfcc333149b33a14411791c24

Das Spring Framework bietet mit Spring Data eine einfache Möglichkeit auf Datenbanken zuzugreifen, welche zusätzlich den Boilerplate-Code reduziert. Man benötigt im Projekt ein Interface, welches von einem Repository-Interface erbt. In diesem Fall vom Interface ReactiveMongoRepository<T, ID>, siehe dazu Listing 3. Für das Beispiel werden keine weiteren Methoden benötigt, wodurch das Interface leer bleibt.

github:3c0d6af442f814f746f49b9551ef7577

Methoden wie findAll(), save(S entity) oder findById(ID id) werden vom Interface geerbt; in diesem Beispiel stehen das Generic S für Todo sowie ID für String. Der Namenszusatz Reactive lässt erahnen, dass sich das Repository anders verhalten wird. Man kann als Alternative vom Interface MongoRepository erben, das dann keinen reaktiven Support bietet. Im Beispielprojekt wird das Repository durch einen Service gekapselt. Da keine Logik im Service vorhanden ist, wird der Service hier nicht vorgestellt.

Listing 4 zeigt einen Ausschnitt des REST-Controllers mit den GET-Methoden. Die Annotationen @RestController, @RequestMapping und @GetMapping dienen dazu, die Klasse zu markieren, damit Spring die Klasse als Controller erkennt und zusätzlich das Mapping für die verschiedenen REST-Methoden auflösen kann.

github:de2361a3e5089ead1d9adb9dabb065bc

Die Annotationen sind Entwicklern bekannt, die mit Spring arbeiten. Neu hingegen sind die Rückgabewerte Flux und Mono. Hierbei handelt es sich um Publisher-Implementierungen aus dem Project Reactor. Vom REST-Controller bis zum Repository werden die Typen Flux und Mono durchgängig verwendet. Flux ist ein Publisher, der 0 bis n Elemente zurückliefert, Mono hingegen ist ein Publisher, der nur 0 bis 1 Element liefert. Beide Klassen implementieren das Publisher-Interface von Reactive Streams. Durch Nutzung des ReactiveMongoRepository<T, ID>-Interface sowie die durchgängige Nutzung von Flux und Mono erhält der Entwickler eine reaktive Anwendung ohne großen Mehraufwand.

Testen des Controllers

Das Spring-Framework sowie Project Reactor bieten Test-Bibliotheken an, um reaktive Programmierung zu testen; Listing 5 zeigt die zwei Abhängigkeiten, Listing 6 einen Test des REST-Controllers mit dem WebTestClient. Die Klasse ist speziell zum Testen von reaktiven Programmen, da es ein nicht blockierender reaktiver Client ist. Der Test überprüft MediaType, HTTP-Statuscode und RequestBody.

github:8e9121009c1510e405ddd69083f498a9
github:d78ba67a1ca25f4ab95cf0f1375bc085

Fallstricke bei reaktiver Programmierung

Eine Anwendung besitzt meist eine Datenbank-Schnittstelle und hier bestehen Defizite zu den vorhandenen reaktiven Datenbank-Treibern. Zurzeit gibt es offiziell nur Implementierungen für MongoDB, Redis und Casandra. Möchte man dagegen JDBC oder JPA einsetzen, hat man blockierenden Zugriff auf die Datenbank. Ob es für JDBC und JPA eine reaktive Implementierung geben wird, wird sich zeigen.

Fazit

Der Artikel gibt eine Einführung in die reaktive Programmierung und zeigt, dass es mit wenig Aufwand möglich ist, die Vorteile zu nutzen. Die reaktive Programmierung ist allerdings nicht das Allheilmittel; es ist im Projekt zu überprüfen, ob die Rahmenbedingungen erfüllt sind − dies fängt bei der Auswahl des Datenbank-Systems an und endet beim Entwickler-Wissen.

Vorteile bietet die reaktive Programmierung durch die Zusammensetzbarkeit der Komponenten (siehe dazu das reaktive Manifest) und den effektiven Ressourcen-Einsatz. Die nächsten Monate werden zeigen, wie die Java-Community reaktive Programmierung aufnimmt und vor allem wie das Flow-API aus Java 9 eingesetzt wird.

Torsten
Software Engineer - Java, JVM

Kennst du schon unsere Lightweight Java User Group Munich bei meetup.com? Werde Teil unserer Community und bleibe informiert über Themen und Neuigkeiten im Lightweight Java.

Registriere dich jetzt!