In diesem Workshop schaffen wir ein flexibles und leichtgewichtiges Setup, das die Arbeitsweise von Microservices in einem Live-System nachstellt. Dafür Erstellen wir mit Ktor zwei Microservices, die über einen zentralen Event Bus kommunizieren, für den wir das Event Messaging System NATS einsetzen.
Ktor
Ktor kommt aus dem Hause JetBrains und ist in der hauseigenen Programmiersprache Kotlin verfasst. Es ermöglicht eine schnelle Realisierung von Web-Client- und Server-Applikationen und bietet sich daher gut zur Erstellung von Microservices in vernetzten Systemen an (meine Eindrücke zu Ktor findet ihr hier im Blog).
Unser Workshop liefert uns nun einen praktischen Einblick in die Grundlagen und wichtigsten Features dieses Microframeworks.
Systemvoraussetzungen
Die folgenden beiden Programme müssen auf unserem System installiert sein, um den Workshop durchzuführen:- JDK 8
- Gradle 6.8.3
Ktor-Microservices erstellen
Im ersten Schritt wollen wir zwei neue, unabhängige Applikationen erstellen, die auf Ktor basieren. Eine der Anwendungen fungiert dabei als unser Frontend-Microservice und die zweite Anwendung repräsentiert unseren Backend-Microservice.
Neue Projekte im Online-Projektkonfigurator
Um ein leeres Grundgerüst für eine neue Ktor-Anwendung zu erstellen, bietet JetBrains einen Online-Ktor-Project-Generator an. Hier können wir die grundlegenden Einstellungen und benötigten APIs für unser neues Projekt auswählen und nach Abschluss des Assistenten als ein neues Gradle-Projekt herunterladen. Nicht aktivierte Erweiterungen im Client- oder Server-Bereich können jederzeit zu einem späteren Zeitpunkt nachinstalliert werden.
Wir verwenden die folgende initiale Projektkonfiguration:
Konfiguration | Wert |
Projekt SDK | Java 1.8 |
Project Type | Gradle Project |
Gradle Wrapper | Ja |
Server Engine | Netty |
Ktor Version | 1.5.1 |
Group | Beliebig, z.B. de.mayflower |
Name | microservice-frontend bzw. microservice-backend |
Wir wollen unsere beiden Microservices puristisch halten. Daher verzichten wir auf die Hinzunahme jeglicher APIs.
Die beiden Projekte können wir in unserem lokalen Dateisystem ablegen und beide in unserer Entwicklungsumgebung öffnen. Unserer Workshop verwendet als Projektverzeichnisse beispielhaft die folgenden beiden Verzeichnisse:
Für den Frontend-Microservice:
~/ktor/microservice-frontend
Für den Backend-Microservice:
~/ktor/microservice-backend
Setzen unterschiedlicher Ports
Da wir beide Microservices lokal auf unserem Localhost betreiben, benötigen die beiden Applikationen zwei unterschiedliche Ports. Diese können wir explizit in der Ktor-Konfigurationsdatei der jeweiligen Anwendung setzen. Standardmäßig ist hier bei beiden Anwendungen der Port 8080 eingetragen.
Wir wollen die Ports für unsere beide Anwendungen nun explizit setzen. In diesem Zug wollen wir auch den developer-mode aktiv setzen, sodass der laufende Server bei Codeänderungen einen selbstständigen Auto-Reload durchführt. Dies erreichen wir durch die folgenden Anpassungen in den Config-Files:
ktor { development = true deployment { port = 1234 } application { modules = [ de.mayflower.ApplicationKt.module ] } }
ktor { development = true deployment { port = 5678 } application { modules = [ de.mayflower.ApplicationKt.module ] } }
Unser Workshop verwendet für den Frontend-Microservice nun den Port 1234 und für den Backend-Microservice den Port 5678.
Bauen der Microservices
Um Ktor zu bauen und zu verwenden wird das Build-Tool Gradle verwendet. Mit dem Ausführen des folgenden Befehls im Projektverzeichnis eines Microservices kann das jeweilige Projekt gebaut werden.
Die verwendete Option -t
sorgt dafür, dass Gradle automatisch neu kompiliert, sobald am Code eines Projekts Änderungen durchgeführt werden:
% gradle build -t
Starten der Microservices
Mit dem folgenden Befehl innerhalb eines neuen Terminals kann – parallel zum Bauen – der aktuell gebaute Stand des jeweiligen Projekts betrieben werden:
% gradle run
Auf diese Art bauen und starten wir nun unsere beiden Microservices. Somit haben wir vier Gradle-Prozesse parallel laufen und unsere Entwicklungsumgebung fertig eingerichtet.
Die Basisroute
Unsere beiden Microservices sind jetzt unter 0.0.0.0:1234
und 0.0.0.0:5678
erreichbar, beim Request darauf liefern die beiden aber noch keine Antwort. Hierfür müssen wir für beide Anwendungen noch Routen hinzufügen.
Wir fügen beiden Microservices nun Basisrouten hinzu. Die sollen jeweils lediglich eine Begrüßung zurückgeben, welche den Microservice identifiziert:
Hier die Erweiterungen der neuen Basisroute für unser Frontend bzw. Backend:
package de.mayflower import io.ktor.application.* import io.ktor.http.* import io.ktor.response.* import io.ktor.routing.* fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { routing { get("/") { call.respondText("Hello Frontend!", contentType = ContentType.Text.Plain) } } }
package de.mayflower import io.ktor.application.* import io.ktor.http.* import io.ktor.response.* import io.ktor.routing.* fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { routing { get("/") { call.respondText("Hello Backend!", contentType = ContentType.Text.Plain) } } }
Testen und Auto-Reload der Anwendungen
Nun können wir beide Microservices im Frontend testen. Mit cURL können wir einen GET-Request auf die beiden Anwendungen machen und bekommen das Ergebnis auf der Konsole ausgegeben:
% curl -X GET 0.0.0.0:1234 Hello Frontend!% % curl -X GET 0.0.0.0:5678 Hello Backend!%
Das Auto-Reload-Feature der Ktor-Server-Anwendung ermöglicht uns beim Entwickeln eine schnelle Turnaround-Time. Wir können dieses Feature testen, indem wir die zurückgegebene Begrüßung unserer Basisroute ändern und den Service auf dieser Route neu requesten.
Ktor-Features
Jetzt, da wir das Ramp-up für unsere beide Microservices abgeschlossen haben, wollen wir uns zwei Features von Ktor ansehen und in unseren Microservices nutzen.
Wir erweitern unser Frontend um die Rückgabe eines HTML-Bodies und statten unser Backend mit der Rückgabe eines JSON-Bodies aus.
Rückgabe eines HTML-Bodies im Frontend
Wir wollen nun zu unserem Frontend eine neue Route hinzufügen, die einen HTML Response Body ausliefert. Hierfür wird die Kotlin-Erweiterung HTML DSL benötigt, die wir durch Hinzufügen der entsprechenden Dependency zur build.gradle
unseres Frontend-Microservices hinzufügen können:
[…] dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "ch.qos.logback:logback-classic:$logback_version" testImplementation "io.ktor:ktor-server-tests:$ktor_version" implementation "io.ktor:ktor-html-builder:$ktor_version" }
Danach können wir eine neue Route definieren, die uns strukturiertes HTML zurückgibt:
package de.mayflower import io.ktor.application.* import io.ktor.html.* import kotlinx.html.* import io.ktor.http.* import io.ktor.response.* import io.ktor.routing.* fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { routing { get("/") { call.respondText("Hello Frontend!", contentType = ContentType.Text.Plain) } get("/html") { val title = "Frontend HTML Test Route" val acclaim = "Welcome to our HTML Route" val content = "This route returns a rendered HTML body that contains templating." call.respondHtml { head { title { +title } } body { h1 { +acclaim } p { +content } } } } } }
Mit einem GET-Requests auf die neue Route können wir die neue Implementierung im Browser oder via cURL testen:
% curl -X GET 0.0.0.0:1234/html <!DOCTYPE html> <html> <head> <title>Frontend HTML Test Route</title> </head> <body> <h1>Welcome to our HTML Route</h1> <p>This route returns a rendered HTML body that contains templating.</p> </body> </html>
Rückgabe eines JSON-Bodies im Backend
Unser Backend wird ebenfalls mit einer neuen Route ausgestattet. Sie soll ein strukturiertes JSON-Objekt als Response zurückliefern. Hierfür benötigen wir die Kotlin-Erweiterung Jackson, die wir über das Hinzufügen der folgenden Dependency zur build.gradle
unseres Backends nutzbar machen können:
[…] dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "ch.qos.logback:logback-classic:$logback_version" testImplementation "io.ktor:ktor-server-tests:$ktor_version" implementation "io.ktor:ktor-jackson:$ktor_version" }
Danach fügen wir eine neue Route hinzu, die uns ein strukturiertes JSON liefern soll. Hierfür geben wir als Response ein Objekt vom Typ Map<K, V>
zurück, welches wir mit Hilfe Kotlins mapOf
-Funktion definieren können:
package de.mayflower import io.ktor.application.* import io.ktor.http.* import io.ktor.response.* import io.ktor.routing.* fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { routing { get("/") { call.respondText("Hello Backend!", contentType = ContentType.Text.Plain) } get("/json") { val status = "success" val id = 1 val text = "Example JSON data string" call.respond( mapOf( "status" to status, "value" to mapOf( "id" to id, "text" to text ) ) ) } } }
Führen wir nun einen Request auf die neue Route /json
durch, so erhalten wir als Response einen HTTP 500 Server Error und im Run-Prozess unseres Backends eine RuntimeException:
java.lang.IllegalArgumentException: Response pipeline couldn't transform 'class java.util.LinkedHashMap' to the OutgoingContent
Zum automatischen Wrappen der Map in ein JSON-Objekt ist die explizite Initialisierung des JSON-Wrappers Jackson für das Ktor-Feature ContentNegotiation erforderlich.
Gewünschte Installations-Routinen können zu Beginn der Funktion Application.module
hinzugefügt werden, indem die Funktion install
verwendet wird.
Beim Installieren können auch diverse Optionen konfiguriert werden, in unserem Beispiel wird für den Jackson-Prozessor die Option für die Rückgabe eines eingerückten JSON-Bodies aktiviert:
package de.mayflower import com.fasterxml.jackson.databind.SerializationFeature import io.ktor.application.* import io.ktor.features.* import io.ktor.http.* import io.ktor.jackson.* import io.ktor.response.* import io.ktor.routing.* fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } } routing { get("/") { call.respondText("Hello Backend!", contentType = ContentType.Text.Plain) } get("/json") { val status = "success" val id = 1 val text = "Example JSON data string" call.respond( mapOf( "status" to status, "value" to mapOf( "id" to id, "text" to text ) ) ) } } }
Ein GET-Request auf die Route /json
liefert uns nun die erwartete Response:
% curl -X GET 0.0.0.0:5678/json { "status" : "success", "value" : { "id" : 1, "text" : "Example JSON data string" } }
Einbau eines externen API
Mit Hilfe des Package Managers Gradle können wir natürlich nicht nur Libs aus dem Ktor-API nutzen, sondern auf sämtliche verfügbaren Java- und Kotlin-Repositories zugreifen.
Zur Demonstration wollen wir die Bibliothek JavaFaker einbinden. Dieses API ist ein Port der bekannten Faker-Bibliotheken für Ruby und Perl und generiert alle Arten an beliebigen, strukturierten Testdaten. Dies können Personendaten, postalische Adressen oder Chuck-Norris-Witze sein.
Die für JavaFaker erforderliche Dependency fügen wir zur build.gradle
unseres Backend-Microservices hinzu:
[…] dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "ch.qos.logback:logback-classic:$logback_version" testImplementation "io.ktor:ktor-server-tests:$ktor_version" implementation "io.ktor:ktor-jackson:$ktor_version" implementation 'com.github.javafaker:javafaker:1.0.2' }
Nun können wir im Backend unsere bestehende Route json
in joke
umbenennen und innerhalb des JSON-Body einen Chuck-Norris-Witz zurückgeben:
package de.mayflower import com.fasterxml.jackson.databind.SerializationFeature import io.ktor.application.* import io.ktor.features.* import io.ktor.http.* import io.ktor.jackson.* import io.ktor.response.* import io.ktor.routing.* import com.github.javafaker.Faker fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } } routing { get("/") { call.respondText("Hello Backend!", contentType = ContentType.Text.Plain) } get("/joke") { val chuck = Faker.instance().chuckNorris() val status = "success" val id = chuck.hashCode() val joke = chuck.fact() call.respond( mapOf( "status" to status, "value" to mapOf( "id" to id, "joke" to joke ) ) ) } } }
Ein GET-Request auf die Route /joke
unseres Backend-Services liefert uns nun den JSON-Body mit einem darin eingebetteten Chuck-Norris-Witz:
% curl -X GET 0.0.0.0:5678/joke { "status" : "success", "value" : { "id" : 1247513041, "joke" : "Chuck Norris doesn't use web standards as the web will conform to him." }
Ktor-Features sind modular erweiterbar
Ktors high-level API bietet Unterstützung für die wichtigsten Anforderungen an Web-Services im Client- und Server-Bereich. Unter anderem sind die folgenden Features und Use Cases im API implementiert und nach dem Einbau der entsprechenden Dependency und der Installation des gewünschten Features sofort nutzbar:
- Unterstützung für verschiedene User-Authentifizierungs-Standards, wie beispielsweise JWT, JWK, LDAP oder OAuth.
- Static Content Serving für viele Default-Dateitypen, beispielsweise HTML, JavaScript, PDF, JPG und PNG.
- Diverse Templating Engines für HTML, CSS und viele weitere.
- Umfangreiches Logging-Framework, implementiert in allen internen Ktor-Komponenten.
Zentraler Event Bus
Damit unsere beiden Microservice-Applikationen miteinander kommunizieren können, führen wir nun einen zentralen Event Bus ein. Unsere beiden Microservices kommunizieren fortan ausschließlich über diesen Bus miteinander – niemals kommunizieren die beiden synchron, also direkt miteinander!
In dieser Architektur findet eine asynchrone Kommunikation statt. Sie ermöglicht dem Event Bus die Verwendung einer Messaging Queue. Mit Hilfe einer Queue können alle zwischen den Microservices vermittelten Nachrichtenpakete gespeichert, protokolliert und prozedural abgearbeitet werden.
Eine Architektur in der eine asynchrone Kommunikation zwischen lose gekoppelten Microservices mit einem zentralen Event Bus und einer Messaging Queue stattfindet bezeichnet man als Microservicearchitektur. Vor allem bei nicht-zeitkritischen Anwendungen hat diese Architektur erhebliche Vorteile gegenüber einer Architektur in der eine synchrone Kommunikation stattfindet:
- Die gesamte Applikation wird ausfallsicherer, da beim Ausfall einzelner Microservices zu übermittelnde Nachrichten gespeichert und zu einem späteren Zeitpunkt zugestellt werden können, wenn der ausgefallene Microservice wieder erreichbar ist.
- Die Logik der Anwendung ist in kleine unabhängige Microservices aufgeteilt. Das macht die einzelnen Microservices besser testbar, da sie als voneinander isolierte Programmkomponenten getestet werden können.
- Alle Microservices sind lose gekoppelt und haben voreinander unabhängige Anforderungen. Das erhöht die Wartbarkeit, Skalierbarkeit und Flexibilität der gesamten Anwendung. Neue Services können mit einer minimalen Downtime eingehängt und bestehende Services jederzeit ausgehängt werden.
Bekannte Frameworks im Event-Messaging-Bereich sind Apache ActiveMQ, RabbitMQ und NATS.
NATS Server installieren
Unter macOS lässt sich der NATS Server schnell über den Package Manager Homebrew installieren. Für die Installation auf anderen Plattformen gibt es die offiziellen Installationsanweisungen.
% brew install nats-server
Anschließend starten wir den NATS Server durch einen Aufruf des Befehls nats-server
:
% nats-server [6399] 2021/03/08 12:32:56.628784 [INF] Starting nats-server version 2.1.9 [6399] 2021/03/08 12:32:56.628886 [INF] Git commit [not set] [6399] 2021/03/08 12:32:56.629210 [INF] Listening for client connections on 0.0.0.0:4222 [6399] 2021/03/08 12:32:56.629216 [INF] Server id is .. [6399] 2021/03/08 12:32:56.629218 [INF] Server is read
Der NATS Server ist nun standardmäßig unter 0.0.0.0:4222
erreichbar.
NATS Tools installieren
Der NATS Server ist in Go geschrieben. Zum Installieren weiterer Tools für den NATS Server benötigen wir daher die Programmiersprache und Laufzeitumgebung Go, die wir über Brew schnell installieren können:
% brew install go
Nun können wir über den in Go integrierten Package Manager die beiden Tools nats-sub
und nats-pub
installieren, die uns zum Testen unseres laufenden NATS Servers unterstützen:
% go get github.com/nats-io/go-nats-examples/tools/nats-sub % go get github.com/nats-io/go-nats-examples/tools/nats-pub
Nach dieser Installation stehen uns die beiden neuen Programme nats-sub
und nats-pub
in unserer Konsole zur Verfügung.
Message Subscription & Publishing
Wir starten jetzt den NATS Server. Mit der Option nats-server -V
können wir ihn im verbose-Modus starten, sodass er uns im laufenden Betrieb mehr Log-Ausgaben mit Debug-Meldungen liefert.
Wir können nun in einem zweiten Konsolenfenster mit dem Befehl nats-sub
den aktuellen Prozess auf ein Event im Bus subscriben. Für den Namen des Events wird ein beliebiger String erwartet – hier verwenden wir joke.channel
. Ein Subscriber wartet auf das Eintreffen dieses Events und wird bei dessen Auftreten benachrichtigt und mit dem mitgesendeten Daten beliefert.
% nats-sub "joke.channel" Listening on [joke.channel]
In einem dritten Konsolenfenster können wir nun mit Hilfe des Tools nats-pub
auf den registrierten Kanal einen Witz publizieren. Durch das Publishen wird ein Event mit dem angegebenen Eventnamen und Datenpaket auf den Bus gelegt:
% nats-pub "joke.channel" "Chuck Norris sleeps with a pillow under his gun."
Der zweite Prozess, der sich auf den Kanal joke.channel
subscribed hat, reagiert nun und liefert die folgende Ausgabe:
% nats-sub "joke.channel" Listening on [joke.channel] [#1] Received on [joke.channel]: 'Chuck Norris sleeps with a pillow under his gun.'
Damit haben wir erfolgreich die asynchrone Kommunikation in unserer Microservice-Architektur getestet.
NATS Client API für Java
Im letzten Kapitel unseres Workshops wollen wir nun unsere beiden Microservices über unseren NATS Event Bus miteinander kommunizieren lassen. Die beiden Funktionen Subskribieren
und Publizieren
können mittels der NATS Client API in unsere beiden Microservices verbaut werden. Die Kommunikation zwischen den Microservices erfolgt dabei analog zur Funktionsweise der beiden Tools nats-sub
und nats-pub
.
Um die NATS Client API für Java nutzen zu können, muss in beiden Microservices die folgende Dependency zur build.gradle
hinzugefügt werden:
[…] dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "ch.qos.logback:logback-classic:$logback_version" testImplementation "io.ktor:ktor-server-tests:$ktor_version" implementation "io.ktor:ktor-html-builder:$ktor_version" implementation 'io.nats:jnats:2.8.0' }
[…] dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "ch.qos.logback:logback-classic:$logback_version" testImplementation "io.ktor:ktor-server-tests:$ktor_version" implementation "io.ktor:ktor-jackson:$ktor_version" implementation 'com.github.javafaker:javafaker:1.0.2' implementation 'io.nats:jnats:2.8.0' }
Initialisierung des API
Die Initialisierung der NATS Client API besteht lediglich aus der Herstellung einer Verbindung zum NATS Server und sieht in beiden Microservices gleich aus. Mit dem erstellten Verbindungs-Objekt können wir auf den Event Bus subskribieren oder publizieren.
Um die korrekte Funktionsweise sicherzustellen, führen wir in beiden Microservices unmittelbar nach dem Herstellen einer Verbindung zum NATS Server das Publizieren einer Begrüßungsnachricht auf den Event Bus aus:
package de.mayflower import io.ktor.application.* import io.ktor.html.* import kotlinx.html.* import io.ktor.http.* import io.ktor.response.* import io.ktor.routing.* import io.nats.client.Connection import io.nats.client.Nats fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { val natsConnection: Connection = Nats.connect() natsConnection.publish("frontend.hello", "Hello NATS Event Bus from Frontend!".toByteArray()) routing { […] } }
package de.mayflower import com.fasterxml.jackson.databind.SerializationFeature import io.ktor.application.* import io.ktor.features.* import io.ktor.http.* import io.ktor.jackson.* import io.ktor.response.* import io.ktor.routing.* import com.github.javafaker.Faker import io.nats.client.Connection import io.nats.client.Nats fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { val natsConnection: Connection = Nats.connect() natsConnection.publish("backend.hello", "Hello NATS Event Bus from Backend!".toByteArray()) install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } } routing { […] } }
Beim Starten der Microservices wird nun von beiden Services einmalig eine Begrüßungsnachricht auf den Event Bus publiziert:
% nats-server -V […] [18517] 2021/03/11 12:31:24.837506 [TRC] 127.0.0.1:58779 - cid:2 - <<- [PUB frontend.hello 35] [18517] 2021/03/11 12:31:24.837520 [TRC] 127.0.0.1:58779 - cid:2 - <<- MSG_PAYLOAD: ["Hello NATS Event Bus from Frontend!"] […] [18517] 2021/03/11 12:33:02.472771 [TRC] 127.0.0.1:59171 - cid:3 - <<- [PUB backend.hello 34] [18517] 2021/03/11 12:33:02.472782 [TRC] 127.0.0.1:59171 - cid:3 - <<- MSG_PAYLOAD: ["Hello NATS Event Bus from Backend!"] […]
Einbau einer Message Subscription im Backend
In unseren Backend bauen wir nun eine Message Subscription auf das Event backend.joke
ein.
Wird dieses Event getriggert, so soll das Backend reagieren, einen Witz generieren und anschließend selbstständig das Event backend.joked
mit dem Witz als Datenpaket auslösen. In unserem Backend-Code sieht diese Implementierung folgendermaßen aus:
package de.mayflower import com.fasterxml.jackson.databind.SerializationFeature import io.ktor.application.* import io.ktor.features.* import io.ktor.http.* import io.ktor.jackson.* import io.ktor.response.* import io.ktor.routing.* import com.github.javafaker.Faker import io.nats.client.* fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { val natsConnection: Connection = Nats.connect() natsConnection.publish("backend.hello", "Hello NATS Event Bus from Backend!".toByteArray()) natsConnection.createDispatcher { message -> val joke = Faker.instance().chuckNorris().fact() natsConnection.publish("backend.joked", joke.toByteArray()) }.subscribe("backend.joke") install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } } routing { […] } }
Einbau eines Message Publishings im Frontend
Zuguterletzt bauen wir nun in unseren Frontend Microservice eine neue Route /joke
ein, die sich auf das Event backend.joked
subscribed.
Anschließend wird das Event backend.joke
ausgelöst und auf das reaktive Eintreffen des Events backend.joked
gewartet. Der hierbei als Datenpaket mitgelieferte Witz wird entgegengenommen und im Frontend als gerendertes HTML ausgegeben.
package de.mayflower import io.ktor.application.* import io.ktor.html.* import kotlinx.html.* import io.ktor.http.* import io.ktor.response.* import io.ktor.routing.* import io.nats.client.* import java.time.Duration fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf @kotlin.jvm.JvmOverloads fun Application.module(testing: Boolean = false) { val natsConnection: Connection = Nats.connect() natsConnection.publish("frontend.hello", "Hello NATS Event Bus from Frontend!".toByteArray()) routing { […] get("/joke") { val sub: Subscription = natsConnection.subscribe("backend.joked") natsConnection.publish("backend.joke", "".toByteArray()) val message: Message = sub.nextMessage(Duration.ofSeconds(2)) val title = "Joke Frontend" val headline = "Joke of the Day" val joke = String(message.data) call.respondHtml { head { title { +title } } body { h1 { +headline } p { +joke } } } } } }
Jetzt können wir die neue Route /joke
im Frontend testen. Das Frontend requested für diese Route nun über das NATS Event Messaging einen Witz vom Backend und gibt ihn im Frontend aus.
% curl -X GET 0.0.0.0:1234/joke <!DOCTYPE html> <html> <head> <title>Joke Frontend</title> </head> <body> <h1>Joke of the Day</h1> <p>Chuck Norris can instantiate an abstract class.</p> </body> </html>
Ktor, NATS & Microservices
Um das Thema Microservices kommt man als Entwickler heutzutage nicht mehr herum. Generell ergibt es Sinn, einzelne und in sich abgeschlossene Programmfunktionen in einem entschlackten, gut lesbaren und klar strukturierten Programm zu verwalten. Auch als Modernisierungsstrategie für Legacy-Projekte oder monolithische Architekturen wird oftmals eine Überführung unabhängiger Funktionalitäten in autark arbeitende (Micro-)Services angestrebt.
Das Microframework Ktor ermöglicht eine schnelle Realisierung von Microservices und bietet dabei eine hohe Developer Experience. Ich freue mich wenn ich Ihnen einen schnellen Einstieg in die praktische Arbeit mit diesem Microframework und dem Event-Messaging-System NATS geben konnte. Für Feedback oder Rückfragen können Sie mich gerne via christopher.stock@mayflower.de erreichen.
Die Codebeispiele für die beiden Microservices finden Sie auf GitHub unter:
Schreibe einen Kommentar