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.
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: |
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); |
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() | |
} |
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?, | |
) |
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.
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
.
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.