Dieser Artikel ist auch auf Deutsch verfügbar
Last year in November, the most recent major release of Spring Boot, version 3.0, appeared after five years. This means that the free support for the last still supported branch of Spring Boot 2, namely 2.7, runs out in November of this year. At the same time, the free support for version 3.0 is also coming to an end, making the most recently released version 3.1 the only version with free support.
The key aspects of Spring Boot 3.0 were the support for newer Java versions (at least Java 17 must now be used) and the updating of Java EE to Jakarta EE and the associated changes to the package names. A great many other dependencies were also updated. In particular, the upgrade to Spring Security 6 generally requires a number of changes, for which the Migration Guide can be quite helpful. Of course, there were also many other small changes and improvements to existing functionality.
In particular, Spring Boot 3.1 contains better and more direct integration of Testcontainers (see also my last column). The new support for Docker Compose during local development is also worth noting.
We would therefore like to take a detailed look at both of these new features in this column.
Testing with Testcontainers
At the end of my last column, I demonstrated a custom test slice for testing repositories based on PostgreSQL against a database started with Testcontainers. In reality, however, a simpler method would be used in such a case. This requires the two dependencies seen in Listing 1, including the BOM for Testcontainers.
We can then use the Testcontainers JUnit 5 extension in combination with the @DynamicPropertySource
provided by Spring to start a Testcontainer and declare it in the Spring ApplicationContext
(see Listing 2).
Since Testcontainers is now also part of the automatic dependency management in Spring Boot 3.1, we no longer have to import the BOM ourselves because this already takes place within spring-boot-dependencies
and we either import this BOM indirectly via spring-boot-starter-parent
or do it explicitly ourselves.
In addition to the two dependencies in Listing 1, we can now also add the dependency seen in Listing 3. This makes it possible to use the new annotation @ServiceConnection
and thereby dispense with a manual registration via @DynamicPropertySource
(see Listing 4).
This annotation ensures that a Spring bean of type ConnectionDetails
is created. With Spring Boot 3.1, beans of this type – or, more precisely, of the provided subtypes – are used for configuring the connection to external services. For databases connected via JDBC, this means that a bean of the type JdbcConnectionDetails
is used. If we don’t register a bean of this type ourselves either directly or via @ServiceConnection
on a Testcontainer, then it will be created with the properties specified under spring.datasource
.
To identify the exact type when using @ServiceConnection
on a Testcontainer, the type of the Testcontainer is generally used. The JdbcConnectionDetails
mentioned above are registered for all containers that are of type JdbcDatabaseContainer
. For other types of containers, such as for Redis, the name of the service is evaluated to determine which ConnectionDetails
must be provided. If the name is not explicitly specified via the attribute value
or name
in the annotation, the name of the Docker image is analyzed.
For test slices utilizing the @AutoConfigureTestDatabase
for which we want to use a connection registered with @ServiceConnection
, we must manually set the attribute replace
to NONE
. If we don’t do this, the test slice will use an in-memory database. There may be improvements here in the future, such as via Ticket 19038, which would allow us to dispense with explicitly overwriting the defaults in this way.
Local development with Testcontainers
In addition to their use in tests, it is now also possible to utilize Testcontainers during development. To do this, we need to have within our test class path a class that can be started via the main
method (see Listing 5).
By convention, this should be located in the same package as the application class and have the same name plus the prefix Test
. Inside the main
method, we take advantage of the option to load the entire configuration of the application via the from
method and expand this with a test configuration using with
. In this test configuration (see Listing 6), we can now register Testcontainers as beans and use the @ServiceConnection
annotation to ensure that these are used as the connection.
The application can now be launched locally by starting the TestApplication class in our IDE or by running the new Maven goal spring-boot:test-run
or the Gradle task bootTestRun
. If the defaults of a @ServiceConnection
are insufficient for our needs or if we have to configure additional properties, it is also possible – similar to the tests – to use the DynamicPropertyRegistry
within the bean registration of a test configuration (see Listing 7).
If we use the spring-boot-devtools
during development, we see that a new container is also started after reloading the application following a change. This may be desirable but also means that all previously created data will be lost after every reload. If we wish to avoid this, we can extend the bean registration of the Testcontainer with the annotation @RestartScope
(see Listing 8).
Alternatively, we could use the (still experimental) feature for reusable containers. However, because this deviates from the procedure documented in Spring Boot and is still experimental, I would advise using @RestartScope
.
Local development with Docker Compose
In my most recent projects, it was typical to use Docker Compose to start the external services required for development. A compose.yml file existed for this purpose (see Listing 9), and docker compose up
had to be run before starting the application. After the application had been stopped, docker compose down
could be used to ensure that the external services were also stopped.
Precisely this workflow is now directly supported in Spring Boot 3.1. For this, we must add the new spring-boot-docker-compose
as a dependency, as shown in Listing 10. If we now start our application, we can see in the log (see Listing 11) that our compose.yml was detected and the postgres
service defined there was started.
By default, however, stop
is used for stopping rather than down
. The container is then retained even after we stop the application. If we want to change this, we can do so via the configuration value spring.docker.compose.stop.command
. A similar situation applies to the location and name of the Docker Compose file. These can be changed via spring.docker.compose.file
.
In addition to starting and stopping the services defined within Docker Compose, Service Connections are also automatically generated here, as with the Testcontainer support. To identify which type of Service Connection is provided by a service, the name of the container image is analyzed. If this doesn’t work because a custom image is being used, there is a way to specify this yourself, as seen in Listing 12. Listing 12 also shows how to define a service that is started and stopped simultaneously with the application but for which no Service Connection should be created.
To identify when a service has been started successfully, the defined healthcheck
from the Docker Compose file is used. If nothing is defined here, Spring Boot Docker Compose waits until the defined port is reachable via TCP. This can also be switched off, and the default timeouts can be changed as well.