Kotlin-Updates: ktor 1.5

Teil 5 der Kotlin-Updates-Serie behandelt die Backend-Bibliothek ktor. Als ich mein Kotlin-Buch im Herbst 2020 fertiggestellt habe, war noch Version 1.3 aktuell, mittlerweile ist es 1.5.

ktor entwickelt sich rasch weiter, aber nicht alle Entwicklungen sind erfreulich: Ärgerlicherweise gilt das ktor-Plugin nun als obsolet. Zwar gibt es ein neues Plugin, dieses ist aber nur in IntelliJ Ultimate enthalten. So wird Jetbrains ktor nicht zum Erfolg verhelfen …

Davon abgesehen ist ktor zwar im Funktionsumfang gewachsen, aber — zumindest was die Beispielprogramme im Buch betrifft — nahezu kompatibel geblieben. (Die einzige Ausnahme betrifft den Umgang des Zeichens / am Ende von URLs, siehe unten.)

Dieser Text bezieht sich auf die folgenden Versionsnummern:

IntelliJ: 2021.1
Android Studio: 4.2
Kotlin: 1.5.20
ktor: 1.5.4
kotlinx.coroutines: 1.5
kotlinx-serialization: 1.2.1
Exposed: 0.32.1
Gradle: 6.7 und 6.8

Vom ktor-Plugin zum ktor-Projektgenerator

Jetbrains will das in der Vergangenheit sehr populäre ktor-Plugin nicht mehr weiterführen. Das Plugin steht zwar weiterhin zur Verfügung, wird aber keine ktor-Versionen > 1.5 unterstützen. Jetbrains empfiehlt ktor-Entwicklern den Wechsel auf die IntelliJ Ultimate-Edition, für die ein neues ktor-Plugin zur Verfügung steht. Persönlich halte ich das für eine sehr unglückliche Entwicklung: Ja, professionelle Entwickler haben ohnedies oft die Ultimate Edition. Aber das trifft sicherlich nicht für jeden zu, der ktor ausprobieren oder kennenlernen will. Wenn Jetbrains Interesse daran hat, dass sich ktor auf breiter Ebene etabliert, dann ist das eine ausnehmend stupide Entscheidung.

Auf das neue Plugin gehe ich hier gar nicht ein — sie finden hier eine Beschreibung. Stattdessen erkläre ich Ihnen hier, wie Sie neue Projekte mit dem Ktor Project Generator einrichten. Diese Website erzeugt ähnlich wie das bisherige Plugin ein Projekt, das Sie in der Folge als ZIP-Datei herunterladen, auf Ihrem Rechner auspacken und dann in IntelliJ öffnen können. Das ist nicht viel schwieriger als bisher, aber wesentlich umständlicher.

Im ktor-Generator wählen Sie die Features aus, die Sie anfänglich benötigen. Für erste Experimente reichen server-seitig die Module HTML-DSL, CSS-DSL und Routing aus. (Das spätere Hinzufügen von Features ist natürlich möglich. Allerdings müssen Sie dann build-gradle manuell erweitern, was natürlich immer ein wenig mühsam ist.)

Der ktor-Projektgenerator

Nachdem Sie die ZIP-Datei heruntergeladen und ausgepackt haben, öffnen Sie das fertige Projekt in IntelliJ. Sobald IntelliJ mit dem ersten Build-Prozess fertig ist und das neue Projekt läuft, können Sie den enthaltenen Testcode im Webbrowser unter der Adresse localhost:8080 ausprobieren.

Bei meinen Tests hat der Projektgenerator zwar eine aktuelle ktor-Version verwendet, aber eine alte Kotlin-Version. Abhilfe: Öffnen Sie gradle.properties und ändern Sie kotlin_version=1.4.32 zu kotlin_version=1.5.20.

Der Projektgenerator verwendet die Gradle-Version 6.6. Grundsätzlich gibt es keinen zwingenden Grund, eine neuere Version zu verwenden. Wenn Sie das doch möchten, öffnen Sie gradle/gradle-wrapper.properties und ersetzen dort gradle-6.6.1-all.zip durch gradle-6.8.zip. Danach schließen Sie das Projekt und öffnen es neuerlich.

Vorhandene Projekte aktualisieren (build.gradle)

Um vorhandene ktor-Projekte auf die aktuelle Version der Bibliothek zu aktualisieren, müssen Sie gradle.properties aktualisieren. Aktuell sollte die Datei so aussehen:

ktor_version=1.5.4
kotlin.code.style=official
kotlin_version=1.5.20
logback_version=1.2.1

Außerdem müssen Sie sicherstellen, dass in build.gradle die Repositories stimmen. Sie haben sich teilweise geändert. Das folgende Listing dient als Orientierungshilfe.

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'
apply plugin: 'application'

group 'info.kofler'
version '0.0.1-SNAPSHOT'
mainClassName = "io.ktor.server.netty.EngineMain"

sourceSets {
    main.kotlin.srcDirs = main.java.srcDirs = ['src']
    test.kotlin.srcDirs = test.java.srcDirs = ['test']
    main.resources.srcDirs = ['resources']
    test.resources.srcDirs = ['testresources']
}

repositories {
    mavenLocal()
    mavenCentral()
    maven { url 'https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers' }
}

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"
    implementation "io.ktor:ktor-server-core:$ktor_version"
    implementation "io.ktor:ktor-html-builder:$ktor_version"

    implementation "org.jetbrains:kotlin-css-jvm:1.0.0-pre.31-kotlin-1.2.41"
    // alternativ die aktuellste Version, wird aber nicht vom Generator verwendet:
    // implementation "org.jetbrains:kotlin-css-jvm:1.0.0-pre.148-kotlin-1.4.30"
    // siehe auch: https://mvnrepository.com/artifact/org.jetbrains/kotlin-css-jvm?repo=kotlin-js-wrappers

    // testImplementation "io.ktor:ktor-server-tests:$ktor_version"
}

Routing-Inkompatibilität: »hostname/path« versus »hostname/path/«

Mein größtes Problem nach dem Update auf ktor 1.5 bestand darin, dass das Routing anders verarbeitet wird. Während bisher ein GET-Request für hostname/todos funktionierte, ist neuerdings hostname/todos/ mit einem /-Zeichen am Ende erforderlich. Die Inkompatibilität hat mit der Behebung des folgenden Bugs zu tun:

https://github.com/ktorio/ktor/issues/1876

ktor kann jetzt zwischen URLs mit/ohne einem Slash am Ende differenzieren. An sich ist das schön, außer man weiß es nicht :-) Der folgende Code-Ausschnitt aus dem Projekt ktor-todo macht den Unterschied klar:

route("/todos") {
    // URL ohne / am Ende verarbeiten, also localhost:8080/todos
    get {
        println("get todos: $todos")
        call.respond(todos.sortedBy { it.priority })
    }

    // URL mit / am Ende verarbeiten, also localhost:8080/todos/
    get("/") {
        println("get todos/: $todos")
        call.respond(todos.sortedBy { it.priority })
    }  
    ...

Beim Test des Evaluation-Clients (Kapitel 30) habe ich daher relativ lange gebraucht, bis ich den Grund gefunden habe, warum der Verbindungsaufbau nicht klappte. Der Test

try {
   // 1: testet, ob Kontakt zum Server möglich ist
   val msg1 = httpClient.get<String>("$evalhost/api")
   ...

scheiterte immer wegen des fehlenden Slash. So funktioniert es (ohne Änderungen an eval-backend):

try {
   // 1: testet, ob Kontakt zum Server möglich ist
   val msg1 = httpClient.get<String>("$evalhost/api/")
   ...

Updates/Links

Plugin / Project Generator

Routing (Handling /)

Download

Die aktualisierten Beispielprojekte zu den Kapiteln 27 bis 30 können Sie hier herunterladen:

https://kofler.info/uploads/kotlin/kap27-30.zip

Beachten Sie, dass der Evaluaierungs-Client auf View Binding umgestellt ist. Was das bedeutet, erfahren Sie im Android-Studio-Update-Artikel.

Die Kotlin-Updates-Serie

Weitere Kotlin-Update-Artikel finden Sie hier auf meiner Website:

https://kofler.info/tag/kotlin-updates