Ktor – Es muss nicht immer Spring sein

Keine Kommentare

Ktor ist ein Kotlin Framework zum Erstellen von Webservern (und -clients).
Auch wenn Spring mittlerweile gut mit Kotlin harmoniert und Kotlin bereits für viele (teilweise experimentelle) Spring-Features verwendet wird, lohnt es sich einen Blick auf Ktor zu werfen. Anders als Spring ist Ktor nämlich komplett in Kotlin geschrieben und kann deshalb Sprachfeatures wie Coroutines, reified Generics und Extension Functions viel effektiver nutzen als Spring.

Weniger Magie

Nachdem ich Ktor zu meinem Projekt hinzugefügt habe kann es schon mit der main-Funktion losgehen.

Hier ist auch schon der erste Unterschied zu Spring zu sehen: ich programmiere mit Ktor wie in einem „ganz normalen“ Programm. Ich kann direkt loslegen, benötige kein Gradle-Plugin, keine Annotationen und starte meinen Server selbst in der main-Funktion.

Damit ich den Netty embedded Server verwenden kann, muss ich die entsprechende Dependency hinzufügen. Hier folgt Ktor einem ähnlichen Ansatz wie Spring Boot mit den Startern. Allerdings muss ich mit factory = Netty trotzdem explizit angeben, dass ich Netty verwenden will. Auch hier gilt: weniger Magie, Ktor macht viele Dinge expliziter.

Das hat zum einen den Vorteil einer besseren Lesbarkeit, da ich nicht anhand der Konfiguration herausfinden muss welche Komponenten tatsächlich verwendet werden. Zum Anderen machen es mir explizite Angaben leichter die richtige Stelle für meinen Code zu finden. Welche Funktion im Server ausgeführt werden soll, wird (auch wieder explizit) über module = Application::module festgelegt.

Diese Funktion ist nichts weiter als eine von mir definierte Extension Function:

Ein simpler Web Service

Routing über eine DSL

Ein Server braucht Routen und die soll er jetzt auch bekommen. Ich muss dafür keinen Controller schreiben, sondern definiere diese über eine DSL:

Nachdem ich den Server gestartet habe, kann ein POST Request nach http://localhost:8080/fruits geschickt werden. Wenn dieser einen String, z.B. "Banane" im Body stehen hat, wird diese Banane zum fruitStash hinzugefügt. Ein GET auf dieselbe URL gibt dann diesen String (bzw. alle angelegten Früchte) wieder zurück.

Stringly typed programming ist allerdings nicht so ganz mein Ding. Daher muss schnell ein Typ Fruit her.

Ein POST mit dem Body {"name":"banane"} wird jetzt sicher das Stück Obst in meinem Service anlegen.

$ curl -X POST http://localhost:8080/fruits -H 'Content-Type: application/json' -d '{ "name": "banane" }' -i

HTTP/1.1 500 Internal Server Error
Content-Length: 0

Ein Internal Server Error? Was habe ich falsch gemacht?
Ich habe vergessen Ktor zu sagen, wie eine Fruit serialisiert bzw. deserialisert wird. Auch hier gilt nämlich: weniger Magie. Wenn ich JSON-Serialisierung möchte, muss ich zunächst ein entsprechendes Feature installieren.

Funktionalität hinzufügen mit Features

Ktor setzt stark auf Modularität und Erweiterbarkeit. Eigentlich muss jede zusätzliche Funktionalität über Features aktiviert („installiert“) werden. Das Feature, das ich für die Serialisierung installieren muss ist ContentNegotiation.

ContentNegotiation

Zum Serialisieren nehme ich Jackson, aber Moshi, GSON oder eigene Lösungen können genau so leicht verwendet werden.

Ein erneuter POST klappt jetzt:

$ curl -X POST http://localhost:8080/fruits -H 'Content-Type: application/json' -d '{ "name": "banane" }' -i

HTTP/1.1 201 Created
Content-Length: 14
Content-Type: text/plain; charset=UTF-8

Und auch das GET gibt mir jetzt JSON zurück:

$ curl -X GET http://localhost:8080/fruits

[{"name":"banane"}]

Bisher habe ich nur eine GET Route definiert, die alle Früchte zurück gibt. Aber auch einzelne können mit Hilfe von Pfadvariablen abgerufen werden:

Um es einfach zu halten habe ich den Index in der Liste als ID verwendet. Wenn ich jetzt also eine gültige Frucht abfrage erhalte ich diese zurück. Aber was passiert, wenn es für den angegebenen Index keine Frucht gibt? Ein GET nach http://localhost:8080/fruits/22 liefert kein Ergebnis. Was ist passiert? Ein Internal Server Error ist passiert.

StatusPages

Der Zugriff fruitStash[index] schmeißt eine IndexOutOfBoundException. Wenig überraschend, wäre aber schöner gewesen, wenn ich das direkt gemerkt hätte. Zum Glück gibt es auch dafür ein Feature: StatusPages.

Nun erhalte ich die erwartete Message als Antwort vom Server: Index: 8, Size: 0. Ich kann jetzt auch direkt die IndexOutOfBoundsException auf einen 404 mappen:

Genauso wäre es möglich vernünftige Fehlerseiten oder -DTOs zu definieren und zurückzugeben. An diesem Beispiel sieht man sehr schön wie das Kotlin Feature reified Generics den Code sprechender macht.

Authentication

Natürlich möchte ich nur registrierten Nutzern erlauben Ressourcen anzulegen. Dafür benutze ich – richtig geraten – ein Feature.


In diesem Beispiel habe ich mich für BasicAuth entschieden, aber auch OAuth, LDAP und andere werden von Haus aus unterstützt. Nach dem Set-up im install-Block dekoriere ich einfach die Routen mit einem authenticate, bei denen ich Authentifizierung nutzen möchte.

Lesbarkeit

Wie eingangs erwähnt halte ich Ktor gerade in kleinen Projekten für lesbarer, einfacher zu verstehen und zu handhaben als Annotation, Beans und Configurations. Es gibt aber noch ein, zwei kleine Tricks die den so eben produzierten Code noch ein bisschen besser machen.

Nachdem ich erstmal gemerkt hatte, dass für die Ktor DSL quasi nur Funktionen verwendet werden, wurde mir klar, dass die üblichen Refactoring-Tools schon hervorragend dazu geeignet sind Ktor-Code zu verbessern. So kann ich z.B eine eigene Funktion für das Installieren der Features herausziehen:

Darüber hinaus bietet Ktor von sich aus auch einige Hilfsfunktionen, die das Programmieren übersichtlicher machen. Mit Hilfe der route-Funktion kann ich z.B. mehrere Routen zusammenfassen und spare mir Schreibarbeit (und reduziere Fehlerquellen):

Der Spring-Killer?

Sicher bietet Spring Boot viel mehr Funktionalität und ist vermutlich auch flexibler. Aber gerade für kleine Projekte, Microservices oder Prototypen sehe ich durchaus großes Potenzial für Ktor.

Das Zusammenbauen von Schnittstellen geht sehr schnell und man muss nicht mehrere Sekunden auf den Spring Context warten. Auch Reflection wird nur sparsam verwendet (wenn überhaupt) und die meisten Fehler erkennt man bereits zur Compile-Zeit. Die Online-Doku ist hervorragend. Die Developer Experience ist einfach flüssiger. Nicht dadurch, dass moderne Sprachfeatures von Kotlin genutzt werden. Klar – DSLs muss man mögen, Kotlin hat hier aber schlicht den Vorteil, dass es sich um interne DSLs (also normalen Code und keine Meta-Sprache) handelt.

Der vollständige Code befindet sich auf Github, die Dokumentation gibt es hier.

Lovis Möller

Lovis Möller ist ein echter „Hamburger Jung“ und immer auf der Suche nach neuen Möglichkeiten, Denkweisen und Perspektiven in der Softwareentwicklung.

Weitere Inhalte zu Kotlin

Kommentieren

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