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
de.mayflower
Name
microservice-frontend
microservice-frontend bzw.
microservice-backend
microservice-backend

Wir wollen unsere beiden Microservices puristisch halten. Daher verzichten wir auf die Hinzunahme jeglicher APIs.

Goodies von Mayflower

 

Das klingt nach einem Thema, dass Dich in Deinem Alltag bei euch beschäftigt? Das Dich mit vielen Fragen zurück lässt?

Keine Sorge – Hilfe ist nah! Melde Dich unverbindlich bei uns und wir schauen uns gemeinsam an, ob und wie wir Dich unterstützen können.

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
~/ktor/microservice-frontend

Für den Backend-Microservice:

~/ktor/microservice-backend
~/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:

Frontend-Config
Backend-Config
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ktor {
development = true
deployment {
port = 1234
}
application {
modules = [ de.mayflower.ApplicationKt.module ]
}
}
ktor { development = true deployment { port = 1234 } application { modules = [ de.mayflower.ApplicationKt.module ] } }
ktor {
development = true
deployment {
port = 5678
}
application {
modules = [ de.mayflower.ApplicationKt.module ]
}
}
ktor { development = true deployment { port = 5678 } application { modules = [ de.mayflower.ApplicationKt.module ] } }
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
-t sorgt dafür, dass Gradle automatisch neu kompiliert, sobald am Code eines Projekts Änderungen durchgeführt werden:

% gradle build -t
% 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
% 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
0.0.0.0:1234 und
0.0.0.0:5678
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:

Frontend
Backend
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 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)
}
}
}
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) } } }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% curl -X GET 0.0.0.0:1234
Hello Frontend!%
% curl -X GET 0.0.0.0:5678
Hello Backend!%
% curl -X GET 0.0.0.0:1234 Hello Frontend!% % curl -X GET 0.0.0.0:5678 Hello Backend!%
% 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
build.gradle unseres Frontend-Microservices hinzufügen können:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[]
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"
}
[…] 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" }
[…]

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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 }
}
}
}
}
}
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 } } } } } }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% 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>
% 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>
% 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
build.gradle unseres Backends nutzbar machen können:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[]
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"
}
[…] 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" }
[…]

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>
Map<K, V> zurück, welches wir mit Hilfe Kotlins
mapOf
mapOf-Funktion definieren können:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
)
)
)
}
}
}
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 ) ) ) } } }
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
/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
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
Application.module hinzugefügt werden, indem die Funktion
install
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
)
)
)
}
}
}
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 ) ) ) } } }
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
/json liefert uns nun die erwartete Response:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% curl -X GET 0.0.0.0:5678/json
{
"status" : "success",
"value" : {
"id" : 1,
"text" : "Example JSON data string"
}
}
% curl -X GET 0.0.0.0:5678/json { "status" : "success", "value" : { "id" : 1, "text" : "Example JSON data string" } }
% 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
build.gradle unseres Backend-Microservices hinzu:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[]
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'
}
[…] 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' }
[…]

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
json in
joke
joke umbenennen und innerhalb des JSON-Body einen Chuck-Norris-Witz zurückgeben:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
)
)
)
}
}
}
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 ) ) ) } } }
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
/joke unseres Backend-Services liefert uns nun den JSON-Body mit einem darin eingebetteten Chuck-Norris-Witz:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% 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."
}
% 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." }
% 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
% brew install nats-server

Anschließend starten wir den NATS Server durch einen Aufruf des Befehls

nats-server
nats-server:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% 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
% 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
% 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
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
% brew install go

Nun können wir über den in Go integrierten Package Manager die beiden Tools

nats-sub
nats-sub und
nats-pub
nats-pub installieren, die uns zum Testen unseres laufenden NATS Servers unterstützen:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% go get github.com/nats-io/go-nats-examples/tools/nats-sub
% go get github.com/nats-io/go-nats-examples/tools/nats-pub
% go get github.com/nats-io/go-nats-examples/tools/nats-sub % go get github.com/nats-io/go-nats-examples/tools/nats-pub
% 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
nats-sub und
nats-pub
nats-pub in unserer Konsole zur Verfügung.

Message Subscription & Publishing

Wir starten jetzt den NATS Server. Mit der Option

nats-server -V
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
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
joke.channel. Ein Subscriber wartet auf das Eintreffen dieses Events und wird bei dessen Auftreten benachrichtigt und mit dem mitgesendeten Daten beliefert.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% nats-sub "joke.channel"
Listening on [joke.channel]
% nats-sub "joke.channel" Listening on [joke.channel]
% nats-sub "joke.channel"
Listening on [joke.channel]

In einem dritten Konsolenfenster können wir nun mit Hilfe des Tools

nats-pub
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."
% nats-pub "joke.channel" "Chuck Norris sleeps with a pillow under his gun."

Der zweite Prozess, der sich auf den Kanal

joke.channel
joke.channel subscribed hat, reagiert nun und liefert die folgende Ausgabe:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% nats-sub "joke.channel"
Listening on [joke.channel]
[#1] Received on [joke.channel]: 'Chuck Norris sleeps with a pillow under his gun.'
% nats-sub "joke.channel" Listening on [joke.channel] [#1] Received on [joke.channel]: 'Chuck Norris sleeps with a pillow under his gun.'
% 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
Subskribieren und
Publizieren
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
nats-sub und
nats-pub
nats-pub.

Um die NATS Client API für Java nutzen zu können, muss in beiden Microservices die folgende Dependency zur

build.gradle
build.gradle hinzugefügt werden:

Frontend
Backend
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[]
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-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'
}
[…] 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' }
[…]

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:

Frontend
Backend
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 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 {
[]
}
}
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 { […] } }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% 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!"]
[]
% 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!"] […]
% 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
backend.joke ein.

Wird dieses Event getriggert, so soll das Backend reagieren, einen Witz generieren und anschließend selbstständig das Event

backend.joked
backend.joked mit dem Witz als Datenpaket auslösen. In unserem Backend-Code sieht diese Implementierung folgendermaßen aus:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 {
[]
}
}
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 { […] } }
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
/joke ein, die sich auf das Event
backend.joked
backend.joked subscribed.

Anschließend wird das Event

backend.joke
backend.joke ausgelöst und auf das reaktive Eintreffen des Events
backend.joked
backend.joked gewartet. Der hierbei als Datenpaket mitgelieferte Witz wird entgegengenommen und im Frontend als gerendertes HTML ausgegeben.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 }
}
}
}
}
}
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 } } } } } }
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
/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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
% 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>
% 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>
% 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:

Goodies von Mayflower

Keine Sorge – Hilfe ist nah! Melde Dich unverbindlich bei uns und wir schauen uns gemeinsam an, ob und wie wir Dich unterstützen können.

Unsere Data-Webinar-Reihe

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.