Microservices mit Ktor und NATS

Microservices mit Ktor und NATS

Avatar von Christopher Stock

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:

KonfigurationWert
Projekt SDKJava 1.8
Project TypeGradle Project
Gradle WrapperJa
Server EngineNetty
Ktor Version1.5.1
GroupBeliebig, z.B. de.mayflower
Namemicroservice-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:

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!

Microservice Architektur
Microservice Architektur

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:

Avatar von Christopher Stock

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.