Asynchroner Aufruf eines REST Services mit Spring aus einer JavaFX Applikation

Heutzutage sind ja REST Services sehr beliebt. Normalerweise läuft dieser Aufruf synchron ab und der Thread wartet schön, bis die Antwort zurückkommt. Dies ist aber auch einfach asynchron mittels asyncRestTemplate möglich (Welches nur bis Spring 4 funktioniert und in Spring 5 von WebClient abgelöst wird, oje…) Somit bleibt der aktuelle Thread nicht blockiert, sondern kann weiterarbeiten und irgendwann trudelt dann die Antwort des REST Services ein.

Statt nach dem REST Call mit .get() auf die Antwort zu warten, gibt man ihm zwei Callbacks mit: Einen SuccessCallback und ein FailureCallback. Der SuccessCallback wird bei erfolgreichem Aufruf aktiviert und enthält das Ergebnis des Services. Der FailureCallback wird aufgerufen, wenn beim Aufruf des REST Services oder im Service selber eine Exception auftritt. Das Objekt, dass wir dabei für die Callbacks verwenden, ist ein ListenableFuture vom Package „org.springframework.util.concurrent“.

Macht man das ganze in einer JavaFX Applikation, muss man noch etwas bestimmtes berücksichtigen. Ich will jetzt hier aber nichts spoilern.

Aufruf des Rest Services

Der Aufruf des Services ist simpel. Wir sagen, welchen Mediatype wir verwenden, bauen uns ein AsyncRestTemplate und rufen die exchange Methode auf. In diesem Beispiel wird die Antwort ein Objekt des Typs User sein. Obwohl der REST Service JSON zurückliefert, kann uns das egal sein, da Spring die Antwort automatisch nach JSON serialisiert und in das Objekt deserialisiert. Natürlich ist es dabei von enormen Vorteil, wenn die Serverseite das gleiche Objekt/die gleiche Klasse verwendet (In diesem Beispiel die Klasse User).

String url = "http://localhost:8089/meineresturl/user/11");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
HttpEntity request = new HttpEntity<>("params", headers);
AsyncRestTemplate asyncRestTemplate = new AsyncRestTemplate();

ListenableFuture> response = asyncRestTemplate.exchange(url, HttpMethod.GET, request,
User.class);

response.addCallback(successCallback, ex -> onFailure(ex));

Beim addCallback fällt auf, dass SuccessCallback und FailureCallback verschieden hinzugefügt werden. Das kann beliebig gemacht werden. In diesem Beispiel wurde der SuccessCallback in einer anderen Klasse erstellt (nämlich im JavaFX ViewModel, das Zugriff auf alle GUI Objekte hat) und hier hinzugefügt. So landet das Ergebnis am Ende praktischerweise in der anderen Klasse.

FailureCallback direkt in einer Methode

Der einfachere Weg, mit dem Ergebnis bzw. in diesem Fall einem Fehler umzugehen, ist es, mittels eines Lambdas direkt auf eine Methode zuweisen. In dieser Methode kann das Ergebnis wie gewünscht behandelt werden.

private void onFailure(Throwable ex) {
LOGGER.error("Fehler beim Aufruf des Services: ", ex);
}

Direktes Hinzufügen des SuccessCallbacks plus Wechseln in JavaFX Thread

Damit der SuccessCallback hinzugefügt werden kann, muss er irgendwo definiert werden. Wie gesagt, das kann in irgendeiner Klasse geschehen und dann der Klasse, die für den oben stehenden REST Call zuständig ist, übergeben werden.

Der Body der Response entspricht dabei dem angegebenen Objekt User und die Werte werden automatisch gesetzt. User muss dabei ein POJO mit Gettern und Settern sein, damit die Werte von Spring gesetzt werden können.

Mit .getStatusCode() kann der HTTP Statuscode abgefragt werden. Die gesamte Liste findet man auch in dieser Klasse org.springframework.http.HttpStatus.

public SuccessCallback> createDurchfahrtenSuccessCallback() {
return response -> {
HttpStatus httpStatusCode = response.getStatusCode();
if (HttpStatus.NOT_FOUND.equals(httpStatusCode)) {
LOGGER.info("HTTP Status Code 404 erhalten - Ressource nicht verfuegbar");
} else if (HttpStatus.OK.equals(httpStatusCode)) {
// Wieder in den JavaFX Hauptthread gehen
Platform.runLater(() -> {
User user = response.getBody();
speichereUser(user);
});
} else {
LOGGER.info("HTTP Status Code " + httpStatusCode + " erhalten"); }
};
}

In der Mitte des folgenden Codestücks sieht man auch den Teil, den man bei einem Aufruf aus einer JavaFX Applikation berücksichtigen muss. Die Antwort des REST Services kommt asynchron und läuft dort in irgendeinem Thread, aber nicht im JavaFX Hauptthread (der Code kann übrigens gedebuggt werden, einfach Breakpoint im Callback setzen).

Damit nun die Verarbeitung und somit der Zugriff auf die GUI Elemente klappt, muss man mittels Platform.runlater() in den JavaFX Thread wechseln, bevor man das Ergebnis verwendet.

Simulieren des REST Services in einem jUnit Test mittels WireMock

Programmiert man gleichzeitig die Client- wie auch Serverseite eines REST Services kann es durchaus vorkommen, dass der Code für den Aufruf des Services vor dem eigentlichen Service-Code fertig ist.

In diesem Fall kann man mittels des Frameworks WireMock einfach einen REST Service für Testzwecke simulieren.

Dazu erstellt man wie gewohnt einen jUnit Test und verwendet WireMock mit den gewünschten Parametern. Natürlich muss das WireMock JAR vorher noch dazugeladen werden, etwa im Maven POM File:

groupId: com.github.tomakehurst
artifactId: wiremock
version: 2.19.0
scope: test

Als erstes erstellt man eine sogeannnte WireMockRule und gibt den gewünschten Port an (dieser muss dann natürlich in der URL beim REST Service Aufruf auch drin sein).

import com.github.tomakehurst.wiremock.junit.WireMockRule;

@Rule
public WireMockRule wireMockRule = new WireMockRule(8089);
// No-args constructor defaults to port 8080

Das eigentliche Simulieren des REST Services geschieht dann tatsächlich in nur einer einzigen Zeile Code, in der man sagt, welchen HTTP Statustyp man zurückgeben will (200 = Alles OK), ob es in JSON sein soll und welchen Body man senden will.

String url = "/meineresturl/user/11");  // URL ohne Host

stubFor(get(urlEqualTo(url)).willReturn(aResponse().withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(userJson)));

Der Body kann ein einfacher String „Hallo Welt“ sein. Ich habe in meinem Fall ein Test User erstellt, ein paar Beispielwerte reingepflanzt und dann das ganze Objekt mittels Jackson ObjectMapper von Hand serialisiert.

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

private String userToJson(User user) {
String userJson = "";
ObjectMapper mapper = new ObjectMapper();
try {
userJson = mapper.writeValueAsString(user);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return user;
}

Also zuerst definiert man im jUnit Test den REST Service mit stubFor und kann dann eine Instanz des REST Service Aufrufers verwenden, um den REST Service aufzurufen. Falls es zu Beginn nicht klappt muss man sicherstellen, dass die URL, die der Service Aufrufer verwendet, wirklich 1-zu-1 mit der URL übereinstimmt, die man dem stubFor mitgibt.

Als einfacher Test kann man also zum Beispiel einen User mit Namen „Peter“ erstellen, diesen in JSON Serialisieren und dort als Body angeben. Dann rufe ich den REST Service auf, erhalte automatisch das „User“ Objekt und prüfe mit jUnit

assertEquals("Peter", user.getName());

WireMock hat noch viele weitere Möglichkeiten, um das Resultat zu verifizieren. Siehe dazu http://wiremock.org/

PS: Muss man beim REST Service Aufruf wie ganz oben beschrieben den SuccessCallback mitgeben muss man im jUnit Test natürlich auch so einen definieren, damit man das Ergebnis bearbeiten kann. Oder man verwendet beim SuccessCallback einfach auch die direkte Methode wie beim FailureCallback.

Ich musste übrigens noch einen kleinen Sleep zwischen dem Aufruf des REST Services und dem anschliessenden assertEquals einbauen, damit der jUnit Test erfolgreich war.

private void warteAufCallback() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

So das wäre alles für heute. Ich hoffe, der Beitrag erleichtert jemandem das Leben 🙂

Monolith oder Microservice Architektur?

In meiner Einführung in Microservices bin ich auf eben dieses Thema eingangen. Das Gegenteil von Microservices, quasi die „altertümliche“ Architektur, wird hingegen Monolith genannt.

Wenn man nun ein neues System designen möchte, nach welchen Kriterien entscheided man sich, ob man jetzt Microservices verwendet oder einen Monolith baut?

Die Eigenschaften von Microservices

Entscheidet man sich für eine Microservices Architektur teilt man eine Applikation in mehrere Komponenten, wobei jede Komponente individuell entwickelt und verpackt wird. Jede Komponente hat einen eigenen Prozess. Die einzelnen Services kommunizieren dabei etwa über eine REST Schnittstelle.

Die Eigenschaften einer Monolith Architektur

Bei einem traditionellen Monolith System gehören die Bausteine zu einer einzigen Applikation, die den Software Lifecycle durchlauft. Alle benötigten Komponenten sind darin enthalten und werden zusammen deployed. Die meisten Applikationen bestehen dabei mehr oder weniger aus einer Datenbank und einer serverseitigen Applikation. Die serverseitige Applikation hat dabei die meiste Logik, etwa das behandeln von HTTP Requests oder das Holen und Schreiben von Daten auf die Datenbank.

Die gesamte Businesslogik ist also an einem Ort. Natürlich besteht die Applikation aus verschiedenen Klassen und Interfaces etc., diese werden am Ende aber zu einem Paket verschnürt. In Java wird aus einer Webapplikation etwa ein WAR File erstellt, welches dann auf einem Tomcat Server deployed werden kann.

Die Nachteile eines Monoliths

Ist die gesamte Applikation eine einziges Stück, kann dieses natürlich über die Jahre ordentlich wachsen und unübersichtlich werden – oder sogar in einem degenerierten Design ausarten, welches nur noch schlecht zu korrigieren ist. Die Applikation mit ihren Anforderungen und Bugs muss immer von jemandem verwaltet werden, was sehr mühsam werden kann.

Was dazu kommt, ist das Ausliefern der Applikation. Da es eine einzige Applikation ist, muss bei jeder kleinen Änderung das gesamte Paket ausgeliefert werden. Dasselbe gilt für die Skalierung.

Gerade im Hinblick auf Microservices wird auch der Wechsel auf eine andere Technologie sehr mühsam bei einem Monolith. Hat der Monolith erst mal eine gewisse Grösse erreicht, wird kaum eine Firma mal fröhlich Geld in die Hand nehmen, um die ganze Applikation in einer anderen Technologie neu zu schreiben.

Monolith ist also schlecht, Microservices gut?

Genau! Microservices werden unabhängig voneinander entwicklet und ausgeliefert. Jeder Microservice wird individuell skaliert. Bei einer neuen Technologie kann ein Microservice einfach durch einen neuen ersetzt werden. So einfach kann das sein!

Wichtig: Jeder Microservice hat dabei seine eigene Datenbank! Das bedeutet gleichzeitig, dass jeder Microservice seinen eigenen Datenbanktyp einsetzen kann, sei das etwa eine relationale Datenbank oder NoSQL. Und jeder Microservice kann hinter einen eigenen Load Balancer gesetzt werden, um eine optimale Last zu gewährleisten.

Super! Ab jetzt nur noch Microservices!

Moment! Microservices haben ebenfalls ein paar Nachteile, welche ich hier auflisten möchte.

Ein Monolith hat den grossen Vorteil, dass die verschiedenen Komponenten ganz einfach miteinander kommunizieren können. Bei Java etwa ruft man einfach die Methoden der entsprechenden Klassen auf. Bei Microservices muss der Entwickler immer eine Schnittstelle gegen aussen für andere Microservices anbieten.

Ebenfalls ein wichtiger Punkt bei Microservices: Mehrere Microservices müssen sich gegenseitig finden beziehungsweise kennen – nicht ganz einfach, wenn die IP eines Microservice gegebenfalls ändern kann. Zu diesem Zweck braucht es einen sogenannten Service Discovery Mechanismus, damit die Microservices die Netzwerklokationen finden. Dies kann etwa eine speziell dafür eingesetzte Service Registry Datenbank sein, welche die Adressen aller Microservices kennt. Ein Microservice registriert sich dort, wenn er gestartet wird. Diese Datenbank muss natürlich immer verfügbar sein und darf seine IP nicht dynamisch wechseln 😉

Fazit

Als Fazit kann man sagen: Hat man das Glück und kann eine neue Applikation erstellen, muss man sich fragen, ob diese sehr gross und komplex oder eher klein und simpel ausfallen wird. Gross und komplex wäre dann eher was für verschiedene Microservices, während klein und simpel für einen Monolith sprechen würde. Oftmals ist die Realität aber so, dass man schon mit einem über die Jahre gewachsenen Monolithen zu tun hat. Ich probiere, herauszufinden, ob man einen Monolithen stückweise in Microservices migrieren kann oder ob dafür ein Big Bang notwendig ist.

Microservices Einführung

Jeder moderne Software Architekt sollte wissen, um was es sich beim Thema Microservices handelt. Man findet im Internet alle möglichen technischen Erklärungen dafür, dabei ist es überhaupt nichts Übersinnliches.

Die simpelste Erklärung ist: Ein Microservice ist eine Einheit für sich, hat genau eine spezifische Aufgabe, und kann in nicht mehr als zwei Wochen gebaut werden.

Dies scheint wirklich zu einfach zu sein, nicht wahr?

Schauen wir uns drei wichtige Punkte dazu genauer an:

  1. Ein Microservice fokussiert sich auf genau eine Aufgabe, ähnlich der Kohäsion einer Methode oder einer Klasse. Wenn etwa Amazon als Microservice gebaut werden würde, dann gäbe es genau ein Modul für das Kaufen und Verkaufen eines Artikels, ein Modul für die Anzeige und das Hinzufügen der Benutzerkommentare etc. Im Gegenzug dazu sind die meisten Applikationen heutzutage grosse, schwere Kreuzer, die über Jahrzehnte gewachsen sind und innerlich sehr vernetzt sind.

  2. Microservices laufen für sich alleine, unabhängig von anderen Modulen. Wenn ein Microservice also abstürzt, wenn man andere Teile der Applikation ändert, ist es dieser nicht gut designt.

  3. Einer der wichtigsten Punkte ist allerdings: Ein Microservice ist in kurzer Zeit zu erstellen und in genauso kurzer Zeit nochmals zu erstellen. Wie oben geschrieben, spricht man von einer Zeitspanne von etwa zwei Wochen. Warum man einen Service nochmals schreiben sollte, fragen Sie? Nun, stellt man fest, dass der Microservice entweder das falsche macht oder ganz einfach nicht mehr aktuell ist, kann dieser in kurzer Zeit durch ein modernes Pendant ersetzt werden. Dies gilt natürlich auch für Performance: Läuft ein Microservice nicht schnell genug, kann dieser in kürzester Zeit durch eine neue Variante ersetzt werden.

Dazu kommt, dass jeder Microservice für sich ein API zur Verfügung stellt, welches unabhängig von der verwendeten Sprache oder Technologie ist. Dies bedeutet beispielsweise, dass man bei Amazon die ganze Applikation mit Java & Spring bauen könnte bis auf den wichtigsten Teil, das Anzeigen der Werbung. Weil dieser Teil für das Business unglaublich wichtig ist, wird dieser kurzerhand in C++ geschrieben, um sicherzustellen, dass dieser gut performt.

Dasselbe gilt natürlich auch für die dahinterliegende Datenbank. Für die Artikel könnte man eine MongoDB verwenden, während ein anderer Teil mit Oracle arbeitet.

Fokussierte Teams

Was nun bei Microservices dazukommt ist die kleiner granulierte Organisation. Statt ein grosses Team zu haben, welche die Herrschaft über alle Services hat, arbeiten viele kleine Teams an einer überschaubaren Anzahl von Microservices.

Fazit

Diese Einführung sollte reichen, um eine Idee zum Thema Microservices zu erhalten.