Sent from Hauptstadt!

ein Blog für den geneigten Leser

Kotlin im Backend – Ein Erfahrungsbericht

Tags: ,

Kategorie Software Engineering | Keine Kommentare »

Seit 10 Monaten arbeite ich in einem Projekt, in dem ich diverse Server Backends (aka Microservices) sowohl mit Java als auch mit Kotlin entwickel und betreue. Bei allen Projekten kommen Spring Boot, Kafka, PostgreSQL und Maven zum Einsatz. Durch die Gleichartigkeit der Software-Architekturen kann ich gut die Vor- und Nachteile von Kotlin beurteilen. Soviel vorneweg: Es gibt keinen eindeutigen Sieger…

Was ist Kotlin?

Kotlin ist eine von der Firma JetBrains entwickelte Programmiersprache, die 2011 vorgestellt wurde. JetBrains ist der Hersteller der führenden Java Entwicklungsumgebung IntelliJ. Vermutlich wäre Kotlin heute eine unter vielen Programmiersprachen, wenn Google nicht Kotlin als bevorzugte Sprache für die Entwicklung von Android Apps auserkoren hätte. Kotlin und die zugehörigen Werkzeuge wie Compiler sind OpenSource und können ohne Einschränkungen auch in kommerziellen Projekten verwendet werden.

Die Kotlin Syntax fühlt sich stark nach Java an. Es gibt Klassen mit Methoden und Attributen, etc. Kotlin ist keine reine funktionale Programmiersprache, sondern genau wie Java eine Mischung aus Objektorientierung und funktionalen Elementen. Der Kotlin Compiler erzeugt JVM Bytecode, der dann auf jeder handelsüblichen JVM ausgeführt werden kann.

Ich kann in Kotlin natürlich alle Klassen des Java JDKs verwenden. Kotlin selbst ergänzt und erweitert das JDK durch eine eigene Klassenbibliothek. Auch das Typsystem von Kotlin ist moderner. So gibt es zum Beispiel nicht die Unterscheidung zwischen int und Integer, sondern alles sind Objekte.

Wo habe ich Kotlin eingesetzt?

Aktuell gibt es 4 große Einsatzgebiete, in denen Kotlin verwendet werden kann:

  • Android App Entwicklung
  • plattformunabhängige Entwicklung von Code für mobile Apps wie iOS und Android (etwa eine Game Engine)
  • Entwicklung von Server Backends (aka Microservices)
  • Web Frontend Entwicklung (Cross-Compiler von Kotlin nach JavaScript)

Die meisten Tipps findet man im Web für das erste Einsatzgebiet von Kotlin: die Entwicklung von Android Apps. Ich hingegen habe Kotlin ausschließlich für die Entwicklung von Microservices verwendet. Neben Kotlin (1.5 und 1.6) bzw. Java 11 umfasste der Technologiestack jeweils:

  • JDK 11
  • Maven 3
  • Spring Boot 2.4 bis 2.6
  • Apache Kafka
  • PostgreSQL
  • Deployment als FatJAR in Docker Containern
  • Jenkins mit Groovy Pipelines und SonarQube
  • IntelliJ Ultimate (das ist die umfangreichere Kaufversion)

Wie habe ich Kotlin gelernt?

Da ich in meinem Software Ingenieur Dasein schon sehr viele Programmiersprachen gelernt und wieder vergessen habe, hat mich Kotlin an keiner Stelle überrascht. Alle in Kotlin gegenüber Java eingeführten Konzepte habe ich schon in zumindest ähnlicher Form in anderen Programmiersprachen gesehen.

Glücklicherweise muss ich mir im Jahr 2022 nicht die genaue Syntax einer Programmiersprache merken, denn das ist der Job der Entwicklungsumgebung und in Spezialfällen von StackOverflow :-)

Zur Einarbeitung habe ich primär genutzt:

Der Rest hat sich während der täglichen Arbeit ergeben. Auch meine Team Kollegen, alles erfahrene Software Ingenieure aus dem Java Umfeld, hatten keine größeren Probleme bei der Einarbeitung in Kotlin.

Vorteile von Kotlin gegenüber Java

Moderne kompakte Syntax

In Kotlin kann ich sehr präzise meinen Code formulieren. Gegenüber Java ist der Quellcode kompakter. Ich muss weniger überflüssige Sachen schreiben und lesen. Statt

public String generateRandomMessage(String salutation) {
    return salutation + " " + UUID.randomUUID();
}

schreibe ich

fun generateRandomMessage(salutation: String) = "$salutation ${UUID.randomUUID()}"

Der Kotlin Compiler erkennt selbst, dass meine Funktion einen String zurückgibt. Da meine Funktion aus nur einer Anweisung besteht, verzichte ich auf die Klammern und verwende stattdessen einen so genannten Expression Body.

Eine Sache, die ich in Java schmerzlich vermisse, sind Default-Werte für Funktionsparameter.

fun generateRandomMessage(salutation: String = "Hello") = "$salutation ${UUID.randomUUID()}"

Kotlin unterstützt natürlich auch die Angabe von Parameternamen beim Methodenaufruf, was Code in manchen Situationen besser lesbar macht:

val message = generateRandomMessage(salutation = "Hi")

Schön ist in Kotlin auch das Pattern Matching über when-Ausdrücke gelöst. In vielen Fällen ersetze ich if-then-else Anweisungen durch when-Ausdrücke, da die Bedingungen gut lesbar untereinander stehen.

fun formalSalutation(gender: Gender, name: String) = when (gender) {
    FEMALE -> "Ms. $name"
    MALE -> "Mr. $name"
    else -> "Dear $name"
}

Fairerweise muss ich erwähnen, dass Java ab Version 17 so genannte Switch-Expressions eingeführt hat. Das obige Kotlin Beispiel sieht in Java ab Version 17 so aus:

public String formalSalutation(Gender gender, String name) {
    return switch (gender) {
        case FEMALE -> "Ms. " + name;
        case MALE -> "Mr. " + name;
        default -> "Dear " + name;
    };
}

Schön gelöst ist in Kotlin das Kopieren von Objekten. Gerade beim Zusammenstellen von Testdaten für einen speziellen Testfall muss ich mein Standardtestobjekt um wenige Attribute abändern. In Kotlin kann ich dies über den Copy-Konstruktor tun:

data class Person(val lastName: String, val firstName: String, val gender: Gender)
...
val joe = Person(lastName = "Smith", firstName = "Joe", gender = MALE)
val jack = joe.copy(firstName = "Jack")

Kotlin null-Sicherheit

Als Vorteil von Kotlin wird häufig die null-Sicherheit (null-safety) genannt. Folgendes Beispiel produziert einen Compiler Fehler:

data class Person(val lastName: String, val firstName: String, val gender: Gender)
...
val joe = Person(lastName = null, firstName = "Joe", gender = MALE)

Einer Variable oder einem Parameter darf der Wert null nur zugewiesen werden, wenn man den Typ entsprechend über ein nachgestelltes „?“ gekennzeichnet hat. Folgender Code kompiliert, da nun der Nachname einer Person als „String?“ definiert ist.

data class Person(val lastName: String?, val firstName: String, val gender: Gender)
...
val joe = Person(lastName = null, firstName = "Joe", gender = MALE)

Tatsächlich habe ich in meinen Projekten bis jetzt nicht sehr stark von dieser null-Sicherheit profitiert, da ich aufgrund fachlicher Anforderungen gezwungen bin, fast jedes Attribut als „nullbar“ zu definieren. Trotzdem ist die null-Sicherheit von Kotlin ein gutes Feature.

Lombok und MapStruct adé!

Auch den Java Entwicklern macht es keinen Spaß, immer wieder die gleichen Code-Konstrukte zu schreiben. Stattdessen haben sich verschiedene Hilfswerkzeuge wie Code-Generatoren etabliert, um die größte Schreibwut in Java zu umgehen. Zwei prominente Beispiele sind

  • Lombok – generiert zum Beispiel Getter, Setter und Default Konstruktoren in Klassen, wenn diese mit wenigen Annotationen versehen sind
  • MapStruct – generiert Code, um Objekte etwa von einer REST DTO Klasse in das zugehörige interne Domänenmodell umzuwandeln

In Kotlin benötigt man beide Werkzeuge nicht mehr! Statt Lombok nehme ich lieber eine Data-Klasse. Der Kotlin Compiler erzeugt für folgende Klasse für jedes Attribut Getter und Setter sowie für die Klasse insgesamt die in der JVM wichtigen Equals und HashCode Funktionen.

data class Person(val lastName: String, val firstName: String, val gender: Gender)

Auch in Java wird Lombok ab Java 17 nicht mehr benötigt, denn Java führt mit den Records ein ähnliches Sprachkonstrukt ein.

Ich habe MapStruct auch in Kotlin verwendet und es dann nach wenigen Wochen wieder rausgeworfen. Stattdessen verwende ich handgeschriebene Mapper Komponenten.

// a DTO, maybe part of REST API
data class PersonDto(val familyName: String, val name: String, val gender: GenderDto)

// internal domain model to represent a person
data class Person(val lastName: String, val firstName: String, val gender: Gender)

// a hand-written mapper class
@Component
class PersonMapper(private val genderMapper: GenderMapper) {
    fun fromDto(dto: PersonDto) = Person(
        lastName = dto.familyName,
        firstName = dto.name,
        gender = genderMapper.fromDto(dto.gender)
    )

    fun toDto(domain: Person?) = domain?.let {
        PersonDto(
            familyName = domain.lastName,
            name = domain.firstName,
            gender = genderMapper.toDto(domain.gender)
        )
    }
}

Ja, das ist manueller Aufwand, aber der hält sich in Grenzen. Er überwiegt die Nachteile:

  • MapStruct verlängert die Compile-Zeit
  • MapStruct erzeugt altmodischen Java 8 Code
  • MapStruct Annotationen können komplex werden
  • MapStruct kennt Kotlin-Konstrukte wie null-Sicherheit nicht

Spring Integration

Kotlin wird von Spring einwandfrei unterstützt. Viele Spring Handbücher bieten inzwischen Beispiele in Java und Kotlin. Es gibt auch eine Unmenge an Spring – Kotlin Tutorials. Die Arbeit mit Kotlin und Spring macht Spaß! Aber Vorsicht: Es gibt ein paar fiese Stolpersteine, etwa bei der Validierung von Kotlin Data-Klassen mit Jackson.

Kotlin Nachteile gegenüber Maven

Kotlin Tooling

Kotlin wird primär von JetBrains unterstützt und entwickelt. Auch wenn JetBrains viel in Kotlin investiert, ist es doch eine verhältnismäßig kleine Firma. Auch Google trägt immer wieder Sachen zu Kotlin bei, aber das ist keine so umfassende Unterstützung, wie ich sie zum Beispiel seitens Google bei Angular beobachte. Dementsprechend gibt es einige Lücken in der Werkzeuglandschaft, die ich kurz einzeln diskutieren will.

Gradle statt Maven

Google empfiehlt Gradle als Bauwerkzeug für Android Apps. In meinem aktuellen Projekt wird hingegen ausschließlich Maven genutzt und deshalb war der Einsatz von Gradle keine Option. In 95% der Fälle funktioniert die Zusammenarbeit von Kotlin und Maven problemlos.

Allerdings gibt es zum Beispiel ein neues Plugin namens Kover von Google zur Messung der Code Coverage ausschließlich für Gradle. Und es sieht nicht so aus, dass eine Maven Version hohe Priorität hätte.

Falls der geneigte Leser also ein neues Kotlin Projekt startet, sollte er unbedingt Gradle als Build Werkzeug wählen.

IntelliJ oder nichts

Wenig überraschend: Die Kotlin Unterstützung in der Entwicklungsumgebung IntelliJ von JetBrains ist ausgereift und stabil. Für die reine Kotlin Entwicklung ist die kostenlose Community Edition von IntelliJ ausreichend. Allerdings unterstützt auch IntelliJ nicht alle Kotlin Features, etwa fehlt eine automatische Erkennung von so genannten Annotation Processors.

Außerhalb von IntelliJ sieht es hingegen sehr düster aus. Es gibt zwar ein Kotlin Plugin für Visual Studio Code, aber an die ausgereifte Unterstützung von IntelliJ kommt dies nicht ran. Ich glaube, bei Netbeans oder Eclipse sieht es hinsichtlich Kotlin Unterstützung noch trauriger aus.

Möchte der geneigte Leser also mit Kotlin entwickeln, kommt er nicht an IntelliJ vorbei. Außer der geneigte Leser nutzt sowieso ausschließlich vim :-)

SonarQube und ktlint

Ein wichtiges Werkzeug im Kotlin Umfeld ist der Kotlin Linter ktlint. Diesen habe ich in allen meinen Kotlin Projekten eingebunden. Per Maven Konfiguration stelle ich sicher, dass ktlint vor der eigentlich Kompilierung ausgeführt wird, damit etwaige Fehler so früh als möglich aufpoppen. Den Code Formatierer von ktlint setze ich hingegen nicht ein, da IntelliJ ausreichend schönen Code erzeugt.

Natürlich verwende ich SonarQube in der Build Pipeline, um die Code Qualität zu messen. Prinzipiell funktioniert SonarQube gut genug. Kurz nach dem Release einer neuen Kotlin Version passiert es, dass SonarQube über angeblich ungetestete Code Zweige schimpft. Meist dauert es dann ein paar Wochen, bis dies mit einem Update des Kotlin Plugins in Sonar behoben wird.

Tatsächlich lassen sich diese Probleme vermutlich erst durch ein neues Kotlin Werkzeug zur Messung der Code Coverage vollständig beheben, doch das von Google veröffentlichte Kover Werkzeug gibt es derzeit nur für Gradle (siehe weiter oben).

Inzwischen haben sich in meinen Projekten doch ein paar Nonsense Tests angesammelt, um SonarQube zu besänftigen. Das frustriert immer wieder. Hinzu kommt, dass neue Versionen des Kotlin Plugins nur noch für SonarQube 9 veröffentlicht werden. Ein Major Upgrade von SonarQube ist aber in einer größeren Organisation kein kleines Unterfangen, da andere Teams auf SonarQube 8 wegen dessen Java 8 Unterstützung angewiesen sind.

Kotlin oder nicht?!

Ein Fazit ist schwierig. Kotlin bietet natürlich eine modernere Syntax und schleppt nicht Javas Jugendsünden mit sich rum. Andererseits entwickelt sich Java weiter und viele Vorteile von Kotlin wie Data-Klassen und Pattern Matching mit when-Ausdrücken gibt es inzwischen auch in Java. Gefühlt entwickelt sich Java aktuell schneller weiter als Kotlin.

Möchte der geneigte Leser Android Apps entwickeln, führt kein Weg an Kotlin vorbei. Bei der reinen Microservice Entwicklung hat Kotlin hingegen weniger klare Vorteile im Vergleich zu Java 17 und neuer.

Ist eine Organisation hingegen noch auf Java 11 oder 8 (grusel) gefangen, lohnt der Wechsel auf Kotlin. Die Entwickler können sich mit modernen syntaktischen Elementen vertraut machen und vielleicht gelingt dadurch dann auch der Schritt auf eine moderne Java Version.

Persönlich empfinde ich das Lernen und Entdecken einer neuen Programmiersprache bereichernd. Ich bin aber kein Verfechter der These, man sollte jedes Jahr eine neue Sprache lernen, aber ab und zu tut das gut.

Steht der geneigte Leser also am Anfang eines neuen Projekts, dann kann er ruhig Kotlin wählen. Viel falsch kann er damit nicht machen!

Schreiben sie ein Kommentar