Kotlin Tutorial

Kotlin: Ein Tutorial für Einsteiger, Teil 4

Alexander Hanschke

Die statisch typisierte Sprache Kotlin erfreut sich zunehmender Beliebtheit. Kotlin läuft sowohl auf der JVM als auch in JavaScript-Umgebungen. Durch die Kompatibilität zu Java 6 findet Kotlin zudem im Android-Umfeld Anklang, da die Sprache prägnanteren Code ermöglicht und zusätzlich die funktionale Programmierung unterstützt. In diesem mehrteiligen Tutorial betrachten wir die wichtigsten Sprachfeatures anhand einer Beispielanwendung.

JVM-Sprache Kotlin

In den vorherigen drei Teilen dieses Tutorials haben wir anhand der Entwicklung des API-Moduls bereits viele der Features kennengelernt, die Kotlin ausmachen. In diesem Teil integrieren wir das Spring Framework, um unsere eigentliche Anwendung zu entwickeln. Dabei liegt der Fokus darauf, zu zeigen, wie die Interoperabilität zwischen Kotlin und Java bzw. Spring funktioniert.

Am Ende soll Janitor in der Lage sein, uns alle existierenden Artikel sortiert darzustellen sowie einmal täglich alle Artikel, die ein bestimmtes Alter erreicht haben, zu archivieren. Hilfreich sind Kenntnisse von Spring (Boot) und Thymeleaf – die in den Tutorials bereits tiefer diskutiert wurden.

Springify

Wir verwenden Spring Boot, um uns den Großteil an Konfiguration abnehmen zu lassen. Daher fügen wir unserem Buildscript wie gewohnt zuerst die benötigten Abhängigkeiten hinzu.

build.gradle

buildscript {
    ext.spring_boot_version = '1.4.1.RELEASE'
    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_version"
    }
}

apply plugin: 'spring-boot'

dependencies {
    compile "org.springframework.boot:spring-boot-starter-thymeleaf"
}

Innerhalb des buildscript-Blocks definieren wir eine Variable für die zu verwendende Version, 1.4.1.RELEASE. Wir verwenden zusätzlich das Spring Boot Gradle Plug-in, welches sich unter anderem darum kümmert, die Versionen aller benötigten Abhängigkeiten zu verwalten, die transitiv durch die Nutzung von Spring entstehen.

Um die Views zu bauen, verwenden wir die Template Engine Thymeleaf, für die es einen Starter von Spring Boot gibt. Daher ist mit der Deklaration der entsprechenden Abhängigkeit bereits alles erledigt.

Um Janitor in eine Spring Boot Anwendung zu verwandeln, reicht die folgende Definition aus.

src/main/kotlin/de/techdev/janitor/Janitor.kt

@SpringBootApplication
open class Janitor

fun main(args: Array<String>): Unit {
    SpringApplication.run(Janitor::class.java, *args)
}

Wir erstellen eine Klasse Janitor in der gleichnamigen Datei und annotieren diese mit @SpringBootApplication. Die Annotation vereinheitlicht gleich mehrere Annotationen, wie auch @Configuration.

Für Konfigurationen erzeugt Spring-Proxy-Klassen, sodass wir die Klasse zusätzlich mit open markieren müssen, da eine Erweiterung andernfalls nicht möglich wäre.

Die main-Funktion haben wir so angepasst, dass sie die statische Methode run aufruft, um die Anwendung zu starten. Wie im letzten Teil beschrieben, handelt es sich bei dem Ausdruck Janitor::class.java um eine Referenz auf die Java-Klasse.

Hier kommt noch ein weiteres Element neu hinzu, der Spread Operator *.

Die run-Methode erwartet als zweiten Parameter ein Varargs, also beliebig viele String-Referenzen. Wir übergeben hier das Array, welches beim Aufruf der main-Funktion erzeugt wird, müssen die einzelnen Elemente aber „auspacken“. Genau darum kümmert sich der Spread Operator.

Führen wir jetzt die main-Funktion aus, wird die Anwendung wie erwartet gestartet.

Konfiguration

Janitor benötigt für seinen Einsatz im Grunde nur drei Parameter: den Consumer Key, das Access Token sowie das Alter, welches Artikel maximal erreichen dürfen, bevor sie archiviert werden.

Da wir das Spring Framework verwenden, können wir die Konfiguration einfach in eine entsprechende Datei auslagern.

application.yml

pocket:
    consumer: "40519-58cacc95cba03ad3d23b2ba6"
    access: "56de666b-76eb-0dd4-94cb-5d839f"
    item:
        maxage: 90

Wir legen das Alter der Artikel initial auf 90 Tage fest. Anschließend können wir eine Pocket-Instanz als Spring Bean wie folgt definieren.

src/main/kotlin/de/techdev/janitor/Janitor.kt

fun main(args: Array<String>): Unit {
    SpringApplication.run(Janitor::class.java, *args)
}

@SpringBootApplication
open class Janitor(
        @Value("\${pocket.consumer}") private val consumer: String,
        @Value("\${pocket.access}") private val access: String) {

    @Bean
    open fun pocket(): Pocket {
        return Pocket.connect(consumer, access)
    }

}

Die Annotation @Value erwartet einen String, welcher gegen die Konfigurationsdatei aufgelöst wird. Hier gibt es eine Besonderheit im Zusammenspiel mit Kotlin: die Syntax für das Auswerten der Properties ist analog zu der von Kotlin verwendeten.

Der Compiler nimmt an, dass wir eine Variable auswerten wollen und streikt, da die Variable zum einen nicht existiert und zum anderen in Annotationen nur Konstanten verwendet werden dürfen, die zur Kompilierungszeit bereits feststehen. Um das Problem zu umgehen escapen wir den Ausdruck einfach und der Compiler ist glücklich. Wem dies zu umständlich ist, dem bietet Spring auch die Möglichkeit, ein anderes Zeichen zu konfigurieren, bspw. &.

In der Funktion pocket erzeugen wir die Implementierung des API-Clients und übergeben dabei die beiden im Konstruktor injizierten Werte.

Controller

Als nächstes erstellen wir einen Controller, der sich um das Laden der Artikel kümmert.

src/main/kotlin/de/techdev/janitor/ItemController.kt

@Controller
class ItemController(private val pocket: Pocket) {

    @GetMapping("/items")
    fun overview(model: Model): String {
        val items = pocket.retrieveOperations().items(sort = Sort.NEWEST)

        with(model) {
            addAttribute("items", items)
            addAttribute("count", items.size)
        }

        return "items/overview"
    }

}

Die Klasse ItemController ist mit @Controller annotiert, sodass Spring sie beim Starten mit berücksichtigt.

Im Konstruktor übergeben wir eine Referenz auf den API-Client. Seit Spring 4.3 muss keine Annotation (@Autowired, @Inject) mehr für die Constructor Injection angegeben werden, solange nur ein Konstruktor existiert.

Wer eine ältere Spring-Version verwendet, kann auf die folgende Deklaration zurückgreifen.

class ItemController @Autowired constructor(private val pocket: Pocket)

Die Artikel sollen später unter dem Pfad /items sichtbar sein, daher definieren wir ein GET Mapping und annotieren die Funktion overview.

Als Erstes laden wir alle Artikel aus Pocket und überschreiben den Default-Wert für die Sortierung so, dass die neuesten Artikel zuerst gelistet werden.

Anschließend übergeben wir die Artikel an ein Model. Dieses wird von Spring bereitgestellt und dient als Container zwischen den Controllern und den Views, die wir im nächsten Schritt erstellen. Prinzipiell funktioniert das Model wie eine Map.

Unter dem Namen items legen wir die Artikel ab und übergeben zusätzlich die Anzahl der gefundenen Artikel, sodass wir diese ebenfalls darstellen können.

Hier kommt eine weitere Funktion aus der Kotlin-Bibliothek zum Einsatz: with.

Die Funktion erwartet eine Referenz und macht diese implizit als Empfänger aller Aufrufe im folgenden Block verfügbar. Somit muss die Referenz nicht bei jedem Aufruf explizit angegeben werden.

Alternativ ließe sich der Code auch wie folgt darstellen.

model.addAttribute("items", items)
model.addAttribute("count", items.size)

Nachdem der Controller erstellt ist, kümmern wir uns als nächstes um die passende View.

View

Da wir Thymeleaf verwenden, definieren wir unsere Views als einfache HTML-Dateien, die unter dem Ordner src/main/resources/templates abeglegt werden.

Zur besseren Strukturierung der Anwendung erzeugen wir einen zusätzlichen Unterordner items und erstellen darin die Datei overview.html, in welcher wir die Artikel darstellen wollen.

Die vollständige Datei ist recht umfangreich und kann auf GitHub eingesehen werden. Daher folgt hier nur der relevante Teil.

src/main/resources/templates/items/overview.html

<h2 class="subtitle">
    There are currently <span th:text="${count}"></span> items
</h2>

<table class="table is-striped" th:unless="${items.empty}">
    <thead>
        <tr>
            <th>Title</th>
        </tr>
    </thead>
    <tbody>
        <tr th:each="item : ${items}">
            <td>
                <a href="#" th:href="${item.url}" th:text="${item.title}"></a>
            </td>
        </tr>
    </tbody>
</table>

Innerhalb der Datei können wir auf die im Controller gesetzten Variablen items und count zugreifen, mit derselben Syntax wie auch Spring und Kotlin sie verwenden.

So zeigen wir die Zahl der Artikel in der Überschrift an. Falls Artikel vorhanden sind (th:unless="${items.empty}"), erstellen wir eine Tabelle, iterieren über alle Artikel und stellen jeweils den Titel dar.

Das allein ist etwas langweilig und zusätzlich gibt es Artikel, die keinen Titel haben, bspw. PDF Dokumente oder ähnliches. Es wäre auch hilfreich, beim Klicken auf einen Artikel direkt zu diesem zu gelangen. Und zuletzt wollen wir Artikel archivieren, wenn sie ein bestimmtes Alter erreicht haben. Daher wäre es hilfreich, das Alter in Tagen direkt abfragen zu können.

Um diese neuen Anforderungen umsetzen zu können, müssen wir noch einmal zu unserem item zurückkehren.

pocket-api/src/main/kotlin/de/techdev/pocket/api/Item.kt

data class Item(
    @JsonProperty("item_id") val id: Long,
    @JsonProperty("time_added") val added: Long,
    @JsonProperty("given_url") val url: String,
    @JsonProperty("resolved_title") val title: String?) {

    fun label(): String {
        if (title.isNullOrEmpty()) {
            return url
        }

        return title!!
    }

    fun age(): Long {
        val instant = Instant.ofEpochSecond(added)
        return Duration.between(instant, Instant.now()).toDays()
    }

}

Um die URL zu bekommen, mappen wir neben den bestehenden Feldern nun zusätzlich noch given_url. Da ein Titel ggf. nicht existiert, ändern wir den Typen des Title auf String?.

Beim Anzeigen der Artikel wollen wir den Titel verwenden, sofern dieser existiert. Anderenfalls geben wir die URL zurück. Dafür erstellen wir die Funktion label, in der wir einen entsprechenden Check durchführen. Hier sehen wir auch einen Anwendungsfall für die Verwendung von Assertions !!.

Da wir nicht direkt auf null testen, sondern die Überprüfung in der Funktion isNullOrEmpty geschieht, kann der Compiler keinen Smart Cast durchführen. Wir können aber hinreichend sicher sein, dass der Titel am Ende nicht null ist und casten ihn daher selbst.

Für die Ermittlung des Alters erstellen wir eine zweite Funktion age. Diese verwendet das Java Date/Time API und berechnet die Anzahl an Tagen, die vergangen sind, seit dem der Artikel hinzugefügt wurde.

Ändern wir die View so ab, dass sie das Label verwendet und uns ebenfalls das Alter ausgibt, sollte das Ergebnis beim Aufruf von http://localhost:8080/items ähnlich zu dem Folgenden sein.

items

Hausarbeit

Zuletzt wollen wir einen Job erstellen, welcher sich darum kümmert, Artikel zu archivieren, die das Alter von 90 Tagen überschritten haben. Dazu erstellen wir eine neue Klasse SweepTask, was folgendermaßen funktioniert:

src/main/kotlin/de/techdev/janitor/SweepTask.kt

@Component
class SweepTask(
        private val pocket: Pocket,
        @Value("\${pocket.item.maxage}") private val maxage: Long) {

    @Scheduled(cron = "30 2 * * * *")
    fun sweep() {
        val items = pocket.retrieveOperations().items().filter { it.age() > maxage }

        if (items.isEmpty().not()) {
            pocket.modifyOperations().archive(items)
        }
    }

}

Hier kommt nichts wirklich Neues mehr hinzu. Wir annotieren die Klasse mit @Component, sodass Spring sie berücksichtigt. Im Konstruktor lassen wir uns eine Referenz auf den API-Client und das Maximalalter übergeben. In der Funktion sweep findet nun endlich die Reinigung statt. Diese ist mit der Annotation @Scheduled markiert und der Cron-Ausdruck 30 2 * * * * besagt, dass die Funktion jeden Tag um 2.30 Uhr ausgeführt wird. Damit das Scheduling funktioniert müssen wir dem Janitor noch eine weitere Annotation spendieren, nämlich @EnableScheduling.

@EnableScheduling
@SpringBootApplication
open class Janitor

Wir rufen dann alle Artikel ab und filtern diese direkt basierend auf ihrem Alter. Wie schon im dritten Teil verwenden wir einen Lambda-Ausdruck, welcher den jeweils zu filternden Artikel unter dem Namen it verfügbar macht. Sofern wir Artikel gefunden haben, die dem Filter entsprechen, archivieren wir diese.

Fazit

Damit endet diese Einführung in die Programmiersprache Kotlin. Janitor ist voll funktionsfähig und kann weiter ausgebaut werden.

Neben den zuvor diskutierten Features lag der Fokus in diesem Teil auf der Zusammenarbeit mit dem Spring Framework. Hier gibt es erstaunlich wenig Reibung und die Lernkurve ist sehr flach. Nur die Tatsache, dass in Kotlin Klassen standardmäßig final sind, erfordert hier und da etwas Anpassungsaufwand.

Es bleibt abzuwarten, wie sich Kotlin als Alternative oder auch Ergänzug zu komplexeren Java-Anwendungen schlägt.

Verwandte Themen:

Geschrieben von
Alexander Hanschke
Alexander Hanschke
Alexander Hanschke arbeitet seit 2013 bei der techdev Solutions GmbH in Karlsruhe, wo er Kunden bei der erfolgreichen Umsetzung von IT-Projekten unterstützt.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: