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.
...
<dependencies>
...
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
...
</dependencies>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.18.3</version>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
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).
@JdbcTest
@Import(GreetingTextRepository.class)
@Testcontainers
@AutoConfigureTestDatabase(replace = NONE)
class GreetingTextRepositoryTest {
@Container
static PostgreSQLContainer<?> DATABASE =
new PostgreSQLContainer<>("postgres:15");
@DynamicPropertySource
static void databaseProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url",DATABASE::getJdbcUrl);
registry.add("spring.datasource.username", DATABASE::getUsername);
registry.add("spring.datasource.password", DATABASE::getPassword);
}
@Autowired
GreetingTextRepository greetingTextRepository;
@Test
void getDefaultGreeting_shouldReturnGreetingTextFromDatabase() {
var greetingText = greetingTextRepository.getDefaultGreetingText();
assertThat(greetingText)
.isEqualTo("Hallo %s.");
}
}
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).
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
...
@JdbcTest
@Import(GreetingTextRepository.class)
@Testcontainers
@AutoConfigureTestDatabase(replace = NONE)
class GreetingTextRepositoryConnectionDetailsTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> DATABASE =
new PostgreSQLContainer<>("postgres:15");
@Autowired
GreetingTextRepository greetingTextRepository;
@Test
void getDefaultGreeting_shouldReturnGreetingTextFromDatabase() {
var greetingText = greetingTextRepository.getDefaultGreetingText();
assertThat(greetingText)
.isEqualTo("Hallo %s.");
}
}
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).
public class TestApplication {
public static void main(String[] args) {
SpringApplication
.from(Application::main)
.with(ContainerConfiguration.class)
.run(args);
}
}
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.
@TestConfiguration(proxyBeanMethods = false)
public class ContainerConfiguration {
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgreSQLContainer() {
return new PostgreSQLContainer<>("postgres:15");
}
}
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).
@TestConfiguration(proxyBeanMethods = false)
public class ContainerConfiguration {
@Bean
public PostgreSQLContainer<?> postgreSQLContainer(
DynamicPropertyRegistry registry) {
var container = new PostgreSQLContainer<>("postgres:15");
registry.add("spring.datasource.url", container::getJdbcUrl);
registry.add("spring.datasource.username", container::getUsername);
registry.add("spring.datasource.password", container::getPassword);
return container;
}
}
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).
@TestConfiguration(proxyBeanMethods = false)
public class ContainerConfiguration {
@Bean
@RestartScope
@ServiceConnection
public PostgreSQLContainer<?> postgreSQLContainer() {
return new PostgreSQLContainer<>("postgres:15");
}
}
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.
services:
postgres:
image: 'postgres:15'
environment:
POSTGRES_PASSWORD: password
ports:
- 5432
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.
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
...
... : Starting Application using Java ...
...
... : Using Docker Compose file '... /compose.yml'
... : Network javaspektrum-spring-boot31_default Creating
... : Network javaspektrum-spring-boot31_default Created
... : Container javaspektrum-spring-boot31-postgres-1 Creating
... : Container javaspektrum-spring-boot31-postgres-1 Created
... : Container javaspektrum-spring-boot31-postgres-1 Starting
... : Container javaspektrum-spring-boot31-postgres-1 Started
... : Container javaspektrum-spring-boot31-postgres-1 Waiting
... : Container javaspektrum-spring-boot31-postgres-1 Healthy
...
... : Started Application in 5.546 seconds (process running for 5.946)
...
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.
services:
postgres:
image: 'postgres:15'
environment:
POSTGRES_PASSWORD: password
ports:
- 5432
labels:
org.springframework.boot.service-connection: jdbc
redis:
image: 'redis:7'
ports:
- 6379
labels:
org.springframework.boot.ignore: true
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.