Aufbau eines reaktiven RESTful Webdienstes mit Spring WebFlux

  • 24 Okt 2023
  • Rupert Burchardi
  • 10 min

In diesem Tutorial zeigen wir, wie leicht man Spring WebFlux mit Kotlin kombinieren kann, um einen reaktiven Web-Server zu entwickeln. Es handelt sich dabei um ein sehr einfaches Beispiel, dass nur den Einstieg aufzeigen soll und eigentlich nicht der Komplexität einer reaktiven Anwendung gerecht wird.

Wer noch nicht genau weiß, was ein reaktiver Web-Server ist, der sollte sich unser Tutorial: Reactive Programming mit Java ansehen.

Datenbank aufsetzen mit Docker Compose

In diesem Beispiel werden wir eine Postgres Datenbank nutzen. Diese können wir zum Beispiel mit Hilfe von Docker compose starten.

Die folgende Konfiguration legt eine Postgres Datenbank und startet ein Initialisierungsskript:

version: '3.5'
services:
postgres:
container_name: pg_webflux_tutorial
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
PGDATA: /data/postgres
volumes:
- ./db.sql:/docker-entrypoint-initdb.d/db.sql
- ./postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- postgres
restart: unless-stopped
networks:
postgres:
driver: bridge
volumes:
postgres-data:
view raw compose.yaml hosted with ❤ by GitHub

Das Datenbank-Skript db.sql legt uns eine Tabelle message an und füllt sie mit 1000 Testdatensätzen.

CREATE TABLE message
(
id SERIAL PRIMARY KEY,
message TEXT,
user_name VARCHAR(50),
date TIMESTAMP
);
-- generate some data with 1000 rows
INSERT INTO message (message, user_name, date)
SELECT
md5(random()::text),
md5(random()::text),
now() - (random() * interval '1 year')
FROM generate_series(1, 1000);
view raw db.sql hosted with ❤ by GitHub

Benötigte Dependencies

Für unser kleinen reaktiven Webserver nutzen wir zwei Spring Boot Starter Bibliotheken: Spring Boot Webflux Starter für den non-blocking REST-API-WebServer und Spring Boot Starter Data R2DBC für den non-blocking Datenbank Zugriff. Um die Vorteile von Kotlin nutzen zu können, brauchen wir zwei Bibliotheken: Kotlin Reflect und Kotlin Coroutines Reactor. Damit können wir anstatt mit Mono und Flux mit Flow und suspendable Methods arbeiten.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.1.4"
id("io.spring.dependency-management") version "1.1.3"
kotlin("jvm") version "1.8.22"
kotlin("plugin.spring") version "1.8.22"
}
group = "com.youniverse"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
runtimeOnly("org.postgresql:postgresql")
runtimeOnly("org.postgresql:r2dbc-postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}

Message model und Repository

Jetzt müssen wir noch das Message-Model anlegen. Wir realisieren das mit einer Kotlin Data Klasse, die einiges an Funktionalität mitbringt, wie z.B. equals() / hashCode() und eine toString()-Methode mit den Klassen-Attributen.

data class Message(
@Id
var id: Long = 0,
val message: String?,
val userName: String?,
val date: Instant?,
)
view raw message.kt hosted with ❤ by GitHub

Durch @Id wird das id Feld als das Feld markiert, welches den Primary Key repräsentiert. Ist der Wert 0, weiß Spring Boot Data, dass es sich um ein neues Objekt handelt.

Zum Schluss brauchen wir noch eine Repository-Klasse um die Elemente aus der Datenbank zu holen.

@Repository
interface MessageRepository : CoroutineCrudRepository<Message, Long>

Dafür nutzen wir ein CoroutineCrudRepository aus dem Spring Data Framework. Hier sind alle Methoden bereits suspendable, so dass wir die Vorteile von Kotlin nutzen können.

Rest Controller

Zum Schluss brauchen wir noch den Rest-Controller, der uns die CRUD-Operationen bereitstellt.

@RestController
@RequestMapping("/api/messages")
class MessageController(private val messageRepository: MessageRepository) {
@GetMapping
suspend fun getAll() = messageRepository.findAll()
@GetMapping("/{id}")
suspend fun getOne(@PathVariable id: Long) = messageRepository.findById(id)
@PostMapping
suspend fun post(@RequestBody messagePostRequestDto: MessagePostRequestDto) =
messageRepository.save(
Message(
message = messagePostRequestDto.message,
userName = messagePostRequestDto.userName,
date = Instant.now(),
)
)
@DeleteMapping("/{id}")
suspend fun deleteOne(@PathVariable id: Long) = messageRepository.deleteById(id)
}
// DTO to separate business/database model from API request model
data class MessagePostRequestDto(
val message: String?,
val userName: String?,
)

Die Methoden sind alle suspendable, aber mehr müssen wir nicht machen. Die gesamte Umwandlung von suspendable Method zu Mono/Flux übernimmt für uns Kotlin Coroutines Reactor und das reaktive Anbinden an den Netty Webserver übernimmt Spring Boot Webflux.

Fazit

Nie war es einfacher einen reaktiven Webserver mit Java bzw. Kotlin aufzusetzen. Allerdings handelt es sich hier aber nur um ein sehr kleines Beispiel. Bringt man mehr Komplexität in das Projekt, dann werden die Schwierigkeiten, die ein reaktives Framework mit sich bringen, erst wirklich deutlich. Wer sich dafür interessiert, sollte einfach mal dieses Projekt von Github clonen und selbst erweitern.

Falls es Fragen oder Anregungen gibt, dann schreibt uns gerne eine Mail an: info@youniverse.com.