en / de
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

Eine Woche mit Micronaut – ein Erfahrungsbericht

Vor einiger Zeit konnte ich mich eine Woche lang mit einer mir neuen Technologie auseinandersetzen. Ich habe Micronaut und Kotlin gewählt, weil mir beides schon bekannt war und ich mehr wissen wollte. Ich gehe auf die gewonnenen Erkenntnisse ein und ziehe einen Vergleich zu Spring.

Die Bedingung war, dass ich ein «Projekt-Ziel» definiere, auch wenn dieses nicht in einer Woche umsetzbar ist. Daher habe ich mir eine kleine Anwendung vorgenommen, welche Abstimmungsresultate indexiert und den Index persistiert. Ein modernes Angular-Front-End sollte die Resultate darstellen.

Architektur mit Angular und Micronaut

Zustande gekommen ist bis jetzt ein Grundgerüst des Back-End. Und vor allem habe ich Erfahrungen mit Micronaut gesammelt.

Was ist Micronaut?

Micronaut ist ein JVM-basiertes Full-Stack-Framework für Microservices und Serverless-Anwendungen. Ähnliche Frameworks sind Spring Boot und Quarkus.

Micronaut fokussiert sich auf folgende Features:

Unterschiede zu Spring

Der grösste Unterschied ist wohl der Verzicht auf Reflection. Denn alles wird kompiliert. So auch die Instanzierung und Verknüpfung der Komponenten. Und dies hat folgende Vorteile:

Ein kleiner Nachteil sind die längeren Build-Zeiten. Ich finde diese aber vernachlässigbar, weil die Integrationstests schneller sind.

Werden jetzt alle Probleme schon beim Kompilieren erkannt?

Leider nein. Denn zur Kompilierzeit ist über die endgültige Implementierung nichts bekannt. Und das ist ja gerade die Idee von IOC. Fehlt also zum Beispiel eine Instanz, wird dies erst beim Starten oder später bemerkt. Später, weil Micronaut die Komponenten immer erst bei Verwendung instanziert. Sie werden also immer lazy-loaded. Dies lässt sich applikationsweit ändern, sollte aber nur für Tests gemacht werden.

Unterschiede bei den Scopes – Micronaut vs Spring

In Spring wird das Verhalten einer Komponente mit @Scope definiert. Micronaut verwendet dafür verschiedene Annotationen.

@Micronaut: Annotation Spring: @Scope value
@Singleton (javax) singleton
@Prototype prototype default von Mirconaut’s @Bean
@RequestScope request
session Bei Micronaut muss die Session als Parameter in die Methode injected werden
@Context Wie @Singleton, wird aber zur selben Zeit wie der BeanContext erzeugt (also nicht lazy-loaded)
@ThreadLocal Eine Instanz pro Thread
@Refreshable Wird neu erzeugt beim Event «RefreshEvent»

 

Weitere Bean-Definitionen – Micronaut vs. Spring

@Infrastructure Kann nicht durch andere Beans ersetzt werden
@ConfigurationProperties @ConfigurationProperties
@Factory + @Singleton/@Prototype @Configuration + @Bean
@Controller @Controller

Weitere Annotationen – Micronaut vs. Spring

Micronaut kommt mit einer Fülle an weiteren Annotationen. Diese steuern die Erstellung und Vernetzung der Komponenten. Eine Auswahl:

@Requires: Komponente wird nur instanziert, wenn eine Bedingung erfüllt ist
@Retryable: Erneuter Methoden-Aufruf im Fall einer Exception
@Recoverable und @Fallback: Fallback-Implementation, wenn @Retryable erschöpft ist
@CircuitBreaker: Aufrufe werden eine bestimmte Zeit lang zurückgewiesen, wenn ein Fehler aufgetreten ist
@Replaces + @Singleton/@Prototype Ersetzt eine andere Instanz, wenn im Build vorhanden (z.B. in Test-Builds)

 

Beispiele:

@Factory
class ApiClientFactory {

    @Bean
    fun locationApiClient(clientConfiguration: clientConfiguration, oauthApiClient: OauthApiClient): LocationApi {
        ApiClient.accessToken = oauthApiClient.accessToken
        return LocationApi(clientConfiguration.baseUrl)
    }
}

@Singleton
open class Client (private val locationApi: LocationApi){

    fun getLocation(locationId: String): String {
        return locationApi.getLocation(locationId, "de").location.toString()
    }
}

@ConfigurationProperties("clientext.clientname")
class ClientConfiguration {
    var baseUrl: String = "https://localhost:8080/reflect"
}

 

Annotation Processor von Micronaut

Micronaut generiert anhand der Annotationen neue Kotlin Klassen. Aus einer Klasse LocationService werden also:

Das Verarbeiten der Annotationen läuft in IntelliJ nicht so geschmiert (Details weiter unten). Der Maven-Build läuft aber gut, sobald alles richtig konfiguriert ist.

Maven

In Maven muss jedes Modul mit den <annotationProcessorPaths> versehen werden. So z.B. in meinem Core-Modul:

<annotationProcessorPaths combine.self="override">
    <annotationProcessorPath>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-inject-java</artifactId>
        <version>${micronaut.version}</version>
    </annotationProcessorPath>
    <annotationProcessorPath>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-validation</artifactId>
        <version>${micronaut.version}</version>
    </annotationProcessorPath>
</annotationProcessorPaths>
<annotationProcessorArgs>
    <annotationProcessorArg>micronaut.processing.group=com.noser.heuteabstimmung</annotationProcessorArg>
    <annotationProcessorArg>micronaut.processing.module=core</annotationProcessorArg>
</annotationProcessorArgs>

Ich habe dafür ein pom-Template erstellt. Dieses enthält die wichtigsten Abhängigkeiten und Konfigurationen.

IntelliJ

Das Kotlin-Plugin muss unbedingt mit der Kotlin-Version übereinstimmen. Denn ansonsten hatte ich seltsame Fehlermeldungen wie „unresolved references“.
Weiter muss natürlich die Verarbeitung der Annotationen aktiviert werden:
Preferences: Build, Execution, Deployment -> Compiler -> Annotation Processors -> „Enable annotation processing“

Doch leider gibt es bei der Verarbeitung Probleme. Denn ändert man etwas an den Annotationen und startet die App aus IntelliJ neu, werden diese Änderungen nicht wirksam! Nur wenn zuerst ein Maven-Build gestartet wird, klappt es. Mir sind bis heute leider keine Workarounds bekannt. Der Bug liegt bei Jetbrains seit vier Jahren herum.

Mix mit Code-Generierungs-Tools

Die APIs von Abstimmungsresultaten habe ich aus einer OpenApi 3 Spec generiert. Dabei wollte ich im selben Modul die Bean-Factories haben. Und diese sollten den Client als Bean zur Verfügung stellen.
Dies hat zu Konflikten geführt, welche erst zur Laufzeit auftreten. Denn wenn Informationen fehlen, verwendet Kotlins Annotation Processor die Klasse error.NonExistentClass. Weil die Zeit fehlte, konnte ich keine Lösung finden. Also habe ich das Modul aufgeteilt:

Dies hat den Vorteil, dass ich die API auch auslagern und anderen Java-Lösungen zur Verfügung stellen kann.

Tests mit Kotest

Micronaut und Kotlin kommen mit guten Test-Werkzeugen daher. Diese funktionieren mit Junit 5, Spock oder Kotest von Kotlin. Die Verwendung von Mocks ist allerdings nicht so bequem wie das Zusammenspiel von Spring und Mockito.

Wegen der knappen Zeit habe ich mich mit Unit-Tests noch zu wenig beschäftigt. Jedoch sieht es so aus, dass gemockte Abhängigkeiten immer über eine Builder-Methode erzeugt werden müssen:

@MicronautTest
class LocationServiceTest(
    private val locationService: locationService,
    private val client: LocationClient // Mock wird verwendet
) : StringSpec({
    „test retreive location“ {
    …
}

}) {

    @MockBean(LocationClientImpl::class)
    fun locationClient(): LocationClient {
        return mockk()
    }
}

 

Dafür sind Integrationstests gerade wegen der schnellen Startzeit ein Klacks:

@MicronautTest
class DbLookUpTest(
    private val lookupDataUseCase: LookupDataUseCase,
    private val locationRepository: locationRepository
) : StringSpec() {
    override fun beforeTest(testCase: TestCase) {
        super.beforeTest(testCase)
        locationRepository.save(xy)
    }

    init {
        "test retreive location" {
            lookupDataUseCase.get(xy)
        }
    }
}

 

Starten von Micronaut

Bootstrap der App

Micronaut wird über die main()-Methode gestartet. Diese ist bei Java der Einstiegspunkt:

package com.noser.heuteabstimmung

import io.micronaut.runtime.Micronaut

object HeuteAbstimmungApplication {

    @JvmStatic
    fun main(args: Array<String>) {
        Micronaut.build()
            .args(*args)
            .mainClass(HeuteAbstimmungApplication.javaClass)
            .start()
    }
}

Das Kotlin-Objekt muss im Maven-POM als property referenziert sein:

<properties>
    <exec.mainClass>com.noser.heuteabstimmung.HeuteAbstimmungApplication</exec.mainClass>
    ...
</properties>

Starten über die Commandline

Mit dem Maven-Build lässt sich eine ausführbare Jar-Datei erstellen. Also wie bei Spring. Und diese kann dann mit dem Java-Befehl gestartet werden. Die Umgebung wiederum wird über das System Property ‹micronaut.environments› gesetzt. Oder aber über die Umgebungsvariable ‹MICRONAUT_ENVIRONMENTS›. Dadurch werden die entsprechenden Einstellungen geladen.

$> java -Dmicronaut.environments=local -jar path/to/project/app-module/target/app-0.1.jar -DDB_PASSWORD=xyz

Über das ‹micronaut-maven-plugin› lässt sich die App folgendermassen starten:

$> cd path/to/project/app-module
$> mvn mn:run -Dmicronaut.environments=local -DDB_PASSWORD=xsz

Das ‹micronaut-maven-plugin› muss übrigens nur im Haupt-Maven-Modul verwendet werden. Also dort wo die main()-Methode verwendet wird.
Micronaut wird dann in einem change-detection-mode gestartet. Bei Änderungen innerhalb der Sourcen wird das Modul neu kompiliert und die App neu gestartet. Bei meinem Multi-Modul-Aufbau war das leider wenig nützlich, denn in meinem App-Modul liegt lediglich die Bootstrap-Klasse ApplicationKt.kt.

Starten über IntelliJ

In IntelliJ kann mit dem Micronaut-Plugin eine Run-Configuration erstellt werden. Es können aber (zum Zeitpunkt meiner Arbeit) keine Micronaut-Umgebungen gesetzt werden. Darum setzt man in der Run-Configuration die Umgebungs-Variable ‹MICRONAUT_ENVIRONMENTS›.

Fazit

Eine Woche mit Micronaut und Kotlin ist natürlich schnell vorüber! Wenn die Anfangsschwierigkeiten überwunden sind und man schon Erfahrung mit Spring (Boot) hat kommt man gut voran und es macht Spass. Einzig der nötige Rebuild bei Änderungen an den Annotationen nervt gewaltig, weil er so viel Zeit frisst.
Ich hatte auch Herausforderungen mit ‹Micronaut Data›. Und auch beim Generieren einer API mittels Open-API 3 Spec, weil Code generiert wird. Falls das Interesse vorhanden ist, würde ich erst in einem nächsten Blog-Eintrag darauf eingehen. Aber in den verlinkten Sourcen sind schon ein paar Knacknüsse in ‹Journal.md› erwähnt.

Links

Projekt (Github, in Arbeit): https://github.com/christianspiller/heuteabstimmung
Micronaut: https://micronaut.io/
Kotlin: https://kotlinlang.org

Versionen

 

Kommentare

Schreiben Sie einen Kommentar

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

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden
NACH OBEN
Privacy Policy Cookie Policy
Zur Webcast Übersicht