This article is also available in English
Letztes Jahr im November ist mit Version 3.0 nach über fünf Jahren das nächste Major Release von Spring Boot erschienen. Damit läuft auch dieses Jahr im November der freie Support für den letzten noch unterstützten Strang von Spring Boot 2, nämliche 2.7, aus. Gleichzeitig endet auch der freie Support für 3.0 und damit wird das gerade erst erschienene 3.1 die einzige Version mit freiem Support sein.
Die Kernpunkte von Spring Boot 3.0 waren die Unterstützung von neueren Java-Versionen, es muss nun mindestens Java 17 verwendet werden, und das Update von Java EE auf Jakarta EE und die damit einhergehenden Änderungen der Package-Namen. Daneben wurde auch eine ganze Menge weiterer Abhängigkeiten auf den aktuellen Stand gebracht. Vor allem das Upgrade auf Spring Security 6 erfordert dabei in der Regel eine Reihe von Änderungen, bei denen der Migration Guide hilfreich ist. Natürlich gab es auch noch eine Reihe von kleinen Änderungen und Verbesserungen an bestehenden Funktionalitäten.
Spring Boot 3.1 enthält, passend zu meiner letzten Kolumne, vor allem eine bessere und direktere Integration von Testcontainers. Außerdem sticht der neue Support für Docker Compose während der lokalen Entwicklung heraus.
Diese beiden neuen Features wollen wir uns deswegen in dieser Kolumne einmal im Detail anschauen.
Testen mit Testcontainers
Meine letzte Kolumne zeigt zum Schluss einen eigenen Testslice, um auf PostgreSQL basierende Repositories gegen eine mit Testcontainers gestartete Datenbank zu testen. In der Realität wurde jedoch meistens eine simplere Möglichkeit genutzt. Hierzu werden die beiden in Listing 1 zu sehenden Abhängigkeiten inklusive der BOM für Testcontainers benötigt.
Anschließend können wir die Testcontainers JUnit 5 Extension in
Kombination mit der von Spring bereitgestellten @DynamicPropertySource
verwenden, um einen Testcontainer zu starten und im Spring ApplicationContext
bekannt zu machen (s. Listing 2).
Dadurch, dass mit Spring Boot 3.1 nun auch Testcontainers Teil des automatischen
Abhängigkeitsmanagements ist, brauchen wir die BOM nicht mehr selbst zu
importieren, da dies bereits innerhalb von spring-boot-dependencies
geschieht
und wir diese BOM entweder indirekt über spring-boot-starter-parent
importieren oder es explizit selbst machen.
Weiterhin können wir nun zusätzlich zu den beiden Abhängigkeiten aus Listing 1
noch die in Listing 3 zu sehende Abhängigkeit hinzufügen. Mit dieser ist es nun
möglich, die neue Annotation @ServiceConnection
zu verwenden und dafür auf
eine manuelle Registrierung über @DynamicPropertySource
zu verzichten (s.
Listing 4).
Diese Annotation sorgt dafür, dass eine Spring-Bean vom Typ ConnectionDetails
erzeugt wird. Mit Spring Boot 3.1 werden Beans von diesem Typ, beziehungsweise
genau genommen von den zur Verfügung gestellten Subtypen, für die Konfiguration
der Verbindung zu externen Diensten verwendet. Für per JDBC angebundene
Datenbanken wird somit nun eine Bean vom Typen JdbcConnectionDetails
verwendet. Sollten wir selbst keine Bean von diesem Typen registrieren, direkt
oder über @ServiceConnection
an einem Testcontainer, dann wird diese mit den
Properties unter spring.datasource
erzeugt.
Um bei der Nutzung von @ServiceConnection
an einem Testcontainer den genauen
Typ zu erkennen, wird in der Regel der Typ des Testcontainers genutzt. Die oben
erwähnten JdbcConnectionDetails
werden für alle Container registriert, die vom
Typen JdbcDatabaseContainer
sind. Für andere Arten von Containern,
beispielsweise für Redis, wird der Name des Dienstes ausgewertet, um
festzustellen, welche ConnectionDetails
zur Verfügung gestellt werden müssen.
Wird der Name über das Attribut value
oder name
an der Annotation nicht
explizit angegeben, wird der Name des Docker-Images analysiert.
Für Testslices, die @AutoConfigureTestDatabase
nutzen, bei denen wir eine mit
@ServiceConnection
registrierte Verbindung verwenden wollen, müssen wir
manuell das Attribute replace
auf NONE
setzen. Machen wir das nicht,
verwendet der Testslice eine In-Memory-Datenbank. Hier kann es in Zukunft noch
Verbesserungen geben, beispielsweise über das
Ticket 19038, wodurch wir auf dieses explizite
Überschreiben der Standards verzichten können.
Lokal entwickeln mit Testcontainers
Neben der Verwendung in Tests gibt es nun auch die Möglichkeit, Testcontainers
während der Entwicklung zu nutzen. Hierzu benötigen wir innerhalb unseres
Testklassenpfades eine über die main
-Methode startbare Klasse (s. Listing 5).
Diese sollte, per Konvention, im selben Package wie die Anwendungsklasse liegen
und denselben Namen mit dem Präfix Test
besitzen. Innerhalb der main
-Methode
nutzen wir die Möglichkeit, über die from
-Methode die gesamte Konfiguration
der Anwendung zu laden und diese über with
um eine Testkonfiguration zu
erweitern. In dieser Testkonfiguration (s. Listing 6) können wir nun
Testcontainers als Beans registrieren und über die
@ServiceConnection
-Annotation dafür sorgen, dass diese als Verbindung
verwendet werden.
Die Anwendung kann nun lokal gestartet werden, indem wir die
Testanwendungsklasse in unserer IDE starten oder durch Ausführung des neuen
Maven-Goals spring-boot:test-run
beziehungsweise des Gradle-Tasks
bootTestRun
. Reichen uns die Defaults einer @ServiceConnection
nicht oder
müssen wir noch weitere Properties konfigurieren, ist es auch, ähnlich wie bei
den Tests, möglich, innerhalb der Bean-Registrierung einer Testkonfiguration die
DynamicPropertyRegistry
zu nutzen (s. Listing 7).
Nutzen wir während der Entwicklung die spring-boot-devtools
, sehen wir, dass
beim erneuten Laden der Anwendung nach einer Änderung auch ein neuer Container
gestartet wird. Dies kann erwünscht sein, führt aber auch dazu, dass nach jedem
neuen Laden alle vorher erstellten Daten wieder verschwunden sind. Wollen wir
das vermeiden, so können wir die Bean-Registrierung des Testcontainers um die
Annotation @RestartScope
erweitern (s. Listing 8).
Alternativ kann auch das, aktuell noch experimentelle, Feature für
Reusable Containers genutzt werden. Da dies jedoch vom in
Spring Boot dokumentierten Vorgehen abweicht und noch experimentell ist, würde
ich zur Verwendung von @RestartScope
raten.
Lokal entwickeln mit Docker Compose
In meinen letzten Projekten war es üblich, um die zur Entwicklung benötigten
externen Services zu starten, Docker Compose zu nutzen. Hierzu existierte eine
compose.yml-Datei, siehe Listing 9, und bevor die Anwendung gestartet wurde
musste docker compose up
ausgeführt werden. Nachdem die Anwendung gestoppt
wurde, konnte dann mit docker compose down
dafür gesorgt werden, dass auch
die externen Services gestoppt werden.
Genau dieser Workflow wird nun mit Spring Boot 3.1 direkt unterstützt. Dazu
müssen wir, wie in Listing 10 zu sehen, eine Abhängigkeit auf das neue
spring-boot-docker-compose
hinzufügen. Starten wir nun unsere Anwendung,
können wir im Log (s. Listing 11) sehen, dass unsere compose.yml erkannt und
der darin definierte Service postgres
gestartet wurde.
Standardmäßig wird dabei jedoch nicht down
, sondern stop
zum Stoppen
verwendet. Somit bleibt der Container, nachdem wir die Anwendung anhalten,
erhalten. Wollen wir dies ändern, lässt sich das über den Konfigurationswert
spring.docker.compose.stop.command
erledigen. Ähnliches gilt für den Ort und
den Namen der Docker Compose-Datei. Dieser kann über
spring.docker.compose.file
geändert werden.
Neben dem Starten und Stoppen der innerhalb von Docker Compose definierten Services werden auch hier, wie beim Testcontainer Support, automatisch Service Connections erzeugt. Um zu erkennen, welche Art von Service Connection von einem Service bereitgestellt wird, wird der Name des Container Images analysiert. Sollte das nicht funktionieren, weil ein eigenes Image verwendet wird, gibt es, wie in Listing 12 zu sehen, die Möglichkeit, dies selbst zu spezifizieren. Listing 12 zeigt dabei gleichzeitig auch noch, wie es möglich ist, einen Service zu definieren, der gleichzeitig mit der Anwendung gestartet und gestoppt wird, aber für den keine Service Connection erzeugt werden soll.
Um zu erkennen, wann ein Service erfolgreich gestartet ist, wird der definierte
healthcheck
aus der Docker Compose-Datei verwendet. Sollte hier keiner
definiert sein, wartet Spring Boot Docker Compose so lange, bis der definierte
Port per TCP erreichbar ist. Dies kann auch ausgeschaltet werden, die
standardmäßigen Timeouts lassen sich ebenfalls verändern.