Microservices mit Kotlin, Vert.x und OpenAPI, Teil 2

Nachdem ein früherer Artikel den Anwendungsfall, die Technikauswahl sowie die zu implementierende Schnittstelle skizziert hatte, beschreit dieser Beitrag die eigentliche Implementierung des Microservice.

Architektur/Methoden  –  0 Kommentare
Microservices mit Kotlin, Vert.x und OpenAPI, Teil 2

Mit der OpenAPI-Spezifikation der Schnittstelle in Listing 1 aus dem ersten Artikel können Entwickler den Webserver aus dem Vert.x-Framework konfigurieren. Dabei nutzen sie die vergebenen Werte der operationID, um den Endpunkten Kotlin-Funktionen zuzuordnen.

In Listing 2 sehen Entwickler nun, wie in Vert.x ein Webserver mit der OpenAPI-Spezifikation aufgesetzt wird. Für das Routing der HTTP-Requests erzeugen sie zunächst eine Router-Factory, die die Dateinamen der Schnittstellenspezifikation als Parameter übergeben bekommt (Zeile 1).

Listing 2: Implementierung des HTTP-Servers mit Request-Routing

1 val httpServer = OpenAPI3RouterFactory.rxCreate(vertx, "swagger.yaml").map {
2 vertx.createHttpServer(http2ServerOptions())
3 .requestHandler(it.apply {
4 coroutineHandler(operationId = "getApods") { handleGetApods(it) }
5 coroutineHandler(operationId = "postApod") { handlePostApod(it) }
6 coroutineHandler(operationId = "getApodForDate") { prepareHandleGetApodForDate (it) }
7 coroutineHandler(operationId = "getApodForDate ") { handleGetApodForDate (it) }
8 //…
9 })
10 .listen(8080)
11
12 /** Options necessary for creating an http2 server */
13 fun http2ServerOptions(): HttpServerOptions = HttpServerOptions()
14 .setKeyCertOptions(
15 PemKeyCertOptions()
16 .setCertPath("tls/server-cert.pem")
17 .setKeyPath("tls/server-key.pem")
18 )
19 .setSsl(true)
20 .setUseAlpn(true)

In Zeile 4 wird die operationID "getApods", für das Abfragen aller Bilddaten, die vorher in der Spezifikation vergeben wurden, der Kotlin-Funktion handleGetApods() zugeordnet. Immer wenn ein Client den Endpunkt der operationID aufruft, wird der Router diese Methode anfragen.

In den Zeilen 6 und 7 werden zwei unterschiedliche Kotlin-Funktionen derselben operationID zugeordnet. Die Operation getApodForDate dient dazu, ein einzelnes bestimmtes Bild abzurufen. Das erfolgt in der Funktion handleGetApodForDate() (Zeile 7). Doch vor dem Abrufen des Bildes prüft man in der Funktion prepareHandleGetApodForDate() (Zeile 6), ob es das gewünschte überhaupt gibt.

Auf diese Weise ermöglicht es Vert.x, die komplexe Verarbeitung von Requests auf mehrere Funktionen zu verteilen. prepareHandleGetApodForDate() und handleGetApodForDate() teilen sich einen gemeinsamen Request-Kontext, über den sie bei der Abarbeitung eines Requests Informationen aneinander weitergeben können.

Von Interesse sind noch die Server-Optionen der Funktion http2ServerOptions(), die ab Zeile 13 definiert und dem Server in Zeile 2 übergeben werden. Da die Entwickler einen HTTP/2-Server implementieren sollen, benötigen sie ein SSL/TLS-Zertifikat. Weiterhin müssen sie zwei Optionen setzen, mit denen sie SSL für den Server aktivieren. Weiterhin aktivieren Entwickler die Application Layer Protocol Negotiation (ALPN). Sie erlaubt dem Server, die HTTP-Version mit dem Client auszuhandeln. Trägt die unterliegende JVM die Versionsnummer 9 oder höher, wird Vert.x immer versuchen, die neuere Version 2 des HTTP-Protokolls zu verwenden.

Im Listing 3 sieht man die Implementierung der Funktion prepareHandleGetApodForDate(), die den ersten Teil der Verarbeitung der Operation getApodForDate übernehmen soll. Sie prüft, ob das angefragte Bild dem Microservice tatsächlich bekannt ist. Dazu wird die ID des Bilds aus dem Request-Context geholt (Zeile 2). Zugriff auf die ID erhalten Entwickler über den Namen des Parameters (apodId), den sie in der OpenAPI-Spezifikation vergeben haben.

Listing 3: Request-Abarbeitung, Teil 1

private suspend fun prepareHandleGetApodForDate(ctx: RoutingContext) {
val apodId = ctx.pathParam("apodId")
val result = client.queryWithParamsAwait("SELECT ID, DATE_STRING FROM APOD WHERE ID=?",
json { array(apodId) })
when (result.rows.size) {
1 -> ctx.put("apodFromDB", result.rows[0]).next()
0 -> ctx.response().setStatusCode(HttpStatus.SC_NOT_FOUND).end()
else -> ctx.response().setStatusCode(HttpStatus.SC_BAD_REQUEST).end()
}
}

Mit dieser ID führen Entwickler eine Datenbankabfrage durch (Zeile 3). Sofern sie ein Ergebnis liefert, speichert man die Bilddaten im Request-Kontext und überlässt die weitere Verarbeitung dem nächsten definierten Request-Handler, indem man die Funktion next() aufruft (Zeile 6). Findet das Programm keinen Datenbankeintrag oder tritt ein anderer Fehler auf, beendet es schon hier die Request-Verarbeitung, indem ein Fehler zurückgegeben wird (Zeilen 7 und 8).

Für den Fall, dass die gewünschte Bildinformation gefunden wurde (Listing 2, Zeile 6), findet die weitere Verarbeitung im nächsten Request-Handler statt. Über den Aufruf von ctx.put("apodFromDB", ...) übergibt man die gefundenen Bildinformationen an den Request-Kontext. Der Request-Router wird dafür sorgen, dass nach dem Aufruf von next() die Funktion aus dem folgenden Beispiel aufgerufen wird:

Listing 4: Request-Abarbeitung, Teil 2 – Ereignis an den Eventbus senden

1 private fun handleGetApodForDate(ctx: RoutingContext) {
2 val jsonObject = ctx.get<JsonObject?>("apodFromDB")
3 jsonObject?.apply {
4 rxVertx.eventBus().rxSend<JsonObject>(
5 "apodQuery",
6 apodQueryParameters(
7 this.getInteger("ID").toString(),
8 this.getString("DATE_STRING"),
9 ctx.request().getHeader("X-API-KEY")
10 )
11 ).map { asApod(it.body()) }
12 .subscribe({
13 when {
14 it == null || it.isEmpty() ->
15 ctx.response().setStatusCode(HttpStatus.SC_SERVICE_UNAVAILABLE).end()
16 else ->
17 ctx.response().setStatusCode(HttpStatus.SC_OK).end(it.toJsonString())
18 }
19 }) {
20 // Error handling
21 ctx.response().setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR).end()
22 }
23 }
24 }

Hier beginnen Entwickler damit, dass sie sich das Objekt, das vorher im Request-Kontext gespeichert worden ist, mit dem Aufruf ctx.get<JsonObject?>("apodFromDB") wieder herausholen (Zeile 2). Mit diesen Informationen senden sie nun ein Ereignis mit dem Namen apodQuery an den Vert.x-Eventbus (Zeile 4). Es sorgt dafür, dass eine andere Komponente des Microservice den tatsächlichen Aufruf gegen die NASA-API ausführt. Das Ergebnis des API-Aufrufs konvertiert man in ein Apod-Objekt (Zeile 11).

Sofern das Objekt leer ist oder in sonstiger Art und Weise ungültig, wird man mit dem HTTP-Statuscode 503 (SERVICE UNAVAILABLE) antworten (Zeile 15). Bei positivem Ergebnis gibt die Anwendung jedoch genau das gewünschte Objekt zurück und versieht die Antwort mit dem Statuscode 200 (OK), siehe dazu Zeile 17. Andere technische Fehler, die zum Beispiel im Zusammenhang mit dem Eventbus auftreten können, behandelt der Code in Zeile 21, was zum Statuscode 500 (INTERNAL SERVER ERROR) führt.

In der OpenAPI-Spezifikation definieren Entwickler eine Reihe von Datentypen. Es handelt sich dabei um die Struktur der JSON-Daten, die zur Kommunikation über die Schnittstelle verwendet wird. Code-Generatoren können aus der Spezifikation sogar Kotlin-Klassen generieren.

Für den Datentyp Apod, der die Bilddaten zu einem "Astronomy Picture of the Day" repräsentiert, wird die Kotlin-Datenklasse aus dem folgenden Listing generiert.

Listing 5: Generierte Kotlin-Datenklasse "Apod"

data class Apod (
/* The unique id of this apod */
val id: kotlin.String,
/* The title of this apod */
val title: kotlin.String,
/* The uri of this image in hd resolution */
val imageUriHd: kotlin.String,
/* The date string of this apod */
val dateString: kotlin.String? = null
)

Sie enthält lediglich die vier definierten Felder id, title, imageUriHd und dateString. In der Kotlin-Welt erhalten Entwickler Getter-, Setter- sowie eine Reihe weiterer Standardfunktionen automatisch dazu, ohne sie selbst implementieren zu müssen. Da sie sich in einer Vert.x-Applikation bewegen, wären aber noch mindestens zwei weitere Funktionen nötig. Sie benötigen Methoden, die Instanzen der Klasse Apod so konvertieren, dass sie sie über den Eventbus verschicken können. Java-Programmierer würden Konvertierungswerkzeuge wie MapStruct verwenden oder sich eine Utility-Klasse schreiben.

In Kotlin können Entwickler sogenannte Extension-Functions schreiben (s. Listing 6). Die generierte Klasse Apod wird um zwei weitere Funktionen erweitert. In Zeile 4 sieht man eine Funktion zur Konvertierung in ein Objekt der Klasse JsonObject. In Zeile 6 wird ein Apod in einen JSON-String konvertiert. Beide Funktionen lassen sich nachher in der Klasse Apod verwenden.

Listing 6: Extension Functions für die Datenklasse "Apod"

/**
* Convert this asApod into a JsonObject.
*/
fun Apod.toJsonObject(): JsonObject = JsonObject.mapFrom(this)


/**
* Convert this asApod into a String encoded JsonObject.
*/
fun Apod.toJsonString(): String = this.toJsonObject().encode()