itemis Blog

How to: Reactive Stream mit Spring Boot und RxJava in Kotlin

Geschrieben von Auryn Engel | 04.06.2019

Jeder kennt diese Situation: Du öffnest eine Website und sie lädt und lädt. 😣 Doch woran liegt das? Die Antwort auf diese Frage und einen konkreten Lösungsansatz mit Spring Boot und RxJava in Kotlin findest du in diesem Artikel.

Beim Aufruf einer Website lädt der Browser Daten vom Server und stellt sie dar. Sind die Daten aufgrund hoher Serverauslastung oder schlechter Verbindung nicht sofort verfügbar, kommt es zu langen Ladezeiten.

Der Grund: Der Server muss die Daten erst aufbereiten, bevor er sie an den Browser sendet.

Es drängt sich die Frage auf, ob der Server wirklich immer erst alle Daten sammeln muss, bevor er sie ausliefert. Es wäre doch viel besser, wenn er bereits einen Teil der Daten ausliefern würde, sobald diese verfügbar sind. 🤔

Reactive Stream als Lösung für lange Ladezeiten von Webapplikationen

Die gewünschte reaktive Datenauslieferung kann beispielsweise durch Streams realisiert werden. Dabei handelt es sich um ein asynchrones Senden der Daten vom Server zum Client. Sobald ein Teil der Daten verfügbar ist, werden sie zum Client gesendet.

Im Folgenden wirst du sehen, wie du dies mit RxJava, Kotlin und Spring Boot implementieren kannst. Weiterhin wird noch ein synchroner Endpunkt erstellt und eine Vue.js-Seite, um den Unterschied zu verdeutlichen.

Reactive Streams implementieren – los geht’s!

Als Erstes benötigen wir Daten, die wir darstellen wollen:

data class Car(val id: String, val model: String, val company: String)

Nun haben wir in Kotlin ein Modell erstellt, welches wir mit RxJava asynchron zur Verfügung stellen können. Im Folgenden werden wir ein Observable erstellen, welches jedes ankommende Objekt an den Client weiterleiten kann.

Wie Observables funktionieren, wird auf vielen anderen Seiten beschrieben und führt hier ein wenig zu weit. Allerdings kann man sehr gut auf der Seite von RxMarbles sehen, wie verschiedene Funktionen auf einem Observable funktionieren.

fun getDataStream(timeout: Long) : Observable {
    return Observable.create {
        for (i in 0..10) {
            Thread.sleep(timeout)
            it.onNext(createRandomCar())
        }
        it.onComplete()
    }
}

private fun createRandomCar(): Car {
    val carNames = listOf("e-tron", "TT Coupé", "Nova", "Uno", "Kuga", "Pinto", "Probe", "Vaneo", "iMIEV", "Opa", "Phaeton")
    val companyNames = listOf("Audi", "Toyota", "VW", "Ford", "Kia", "Fiat", "Chevrolet", "Mercedes")
    return Car(Random.nextInt(0, 100000), companyNames[Random.nextInt(0, companyNames.size)], carNames[Random.nextInt(0, carNames.size)])
}

Die Funktion createRandomCar() erstellt ein neues, zufälliges Auto. Dies kannst du dir auf dem GitHub-Repo genauer ansehen.

Es wird zwischen jedem Auto ein Timeout von 150ms erzeugt, um beispielsweise eine langsame Verbindung zur Datenbank oder das Laden von anderen REST-Services zu simulieren.

Die so erzeugten Daten können als REST-Service zur Verfügung gestellt werden:

@Controller
class RestEndpoint {

    @Autowired
    lateinit var dataProvider: DataProvider

    @GetMapping(path = ["cars"], produces = [MediaType.APPLICATION_STREAM_JSON_VALUE])
    @ResponseBody
    @ApiResponses(value = [ApiResponse(code = 200, message = "Cars")])
    fun getCarsAsStream(): Observable {
        return dataProvider.getDataStream()
    }

    @GetMapping(path = ["cars"], produces = [MediaType.APPLICATION_JSON_VALUE])
    @ResponseBody
    @CrossOrigin(origins = ["http://localhost:8081"])
    fun getCarsAsJson(): List {
        return dataProvider.getDataStream().toList().blockingGet()
    }

}

Den CrossOrigin-Header setzen wir, da wir später die Daten von einer lokalen Vue.js-Anwendung anfragen wollen und sie sonst nicht bekommen.

Es wurden nun zwei Endpunkte erstellt, die je nach Header einen Stream oder eine Liste von Autos zurücksenden.

Laden wir nun diese Daten asynchron, bekommen wir folgende Antwort:

{“id”:44095,”model”:”Audi”,”company”:”e-tron”} {“id”:8272,”model”:”Ford”,”company”:”Kuga”} {“id”:63213,”model”:”Kia”,”company”:”Opa”} {“id”:41440,”model”:”Fiat”,”company”:”Kuga”} {“id”:33670,”model”:”Toyota”,”company”:”Kuga”} {“id”:66710,”model”:”Ford”,”company”:”Nova”} {“id”:64250,”model”:”VW”,”company”:”Opa”} {“id”:83594,”model”:”Chevrolet”,”company”:”iMIEV”} {“id”:70848,”model”:”Audi”,”company”:”TT Coupé”} {“id”:55812,”model”:”Chevrolet”,”company”:”iMIEV”} {“id”:81105,”model”:”Audi”,”company”:”Kuga”}

Diese Daten können auch synchron geladen werden, dann sehen sie wie folgt aus:

[{“id”:36599,”model”:”Audi”,”company”:”Probe”},{“id”:30709,”model”:”Kia”,”company”:”Probe”},{“id”:62511,”model”:”Kia”,”company”:”Phaeton”},{“id”:95672,”model”:”Fiat”,”company”:”Pinto”},{“id”:19564,”model”:”Mercedes”,”company”:”Pinto”},{“id”:88003,”model”:”VW”,”company”:”Uno”},{“id”:72413,”model”:”Mercedes”,”company”:”Phaeton”},{“id”:18516,”model”:”Fiat”,”company”:”e-tron”},{“id”:21171,”model”:”Ford”,”company”:”Opa”},{“id”:27514,”model”:”VW”,”company”:”Uno”},{“id”:21767,”model”:”Mercedes”,”company”:”Nova”}]

Den Unterschied beim Laden kann man sehr gut sehen, wenn man das in einer Website verwendet.

Zum asynchronen Laden verwenden wir in der Vue.js-Anwendung oboe.js, für das synchrone Laden axios. Beide sind bekannte Bibliotheken, um Anfragen an Server zu senden.

Am besten erkennbar ist das Ergebnis im GIF auf dem Github-Repo:

Probier es selbst aus: Lade dir das Repo und teste den Unterschied in der UX!

Fazit

Das Erstellen eines asynchronen Rest-Endpunktes ist nicht schwer, doch der Unterschied in der UX ist erheblich.

Reaktives Programmieren sollte also so oft wie möglich verwendet werden, um dem Nutzer ein besonders gutes Verhalten der Anwendung zu geben.

Denn was macht ein Nutzer, wenn nichts passiert, nachdem er auf einen Button geklickt hat? Genau, nochmal klicken. Somit wird der Server weiter belastet, der Nutzer bekommt keine Antwort und alle sind frustriert.

Also: Nutze reaktives Programmieren!👨‍💻

Du hast Fragen oder möchtest etwas zum Thema „reaktives Programmieren mit reactive stream“ beitragen? Hinterlasse mir doch einen Kommentar unterhalb dieses Beitrags!