Spring Boot: Local Development Enhancements, Let’s Compose!
August 25, 2023Quite often when we are developing an application we need external services such as rabbitMQ, Kafka, etc.
When you are developing locally, you are quite likely using a docker-compose file to start these up, and I am certainly (hopefully) not the only one that has forgotten at least once to start these instances up.
And maybe you are even already using Testcontainers for your testing.
Luckily, Spring Boot 3.1 introduced some nice improvements to make our lives a bit easier.
- Docker compose support which allows us to make use of our
compose.yml
file to start these up and create the service connections for supported containers - Testcontainers at development time
Both of these functionalities are built atop the ConnectionDetails abstraction, so if you are unfamiliar with this. I recommend checking out this article.
Note: the docker compose
CLI application needs to be on your path for these to work properly.
Feel free to clone the demo repository, to run the samples!
Docker compose support
This method allows us to leverage our existing docker-compose.yml
files, with some extra quality of live functionality.
We just need to add a dependency on spring-boot-docker-compose
Maven:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-docker-compose</artifactId> <optional>true</optional> </dependency> </dependencies>
Gradle:
dependencies { developmentOnly("org.springframework.boot:spring-boot-docker-compose") }
note: the docker-compose support is limited at the moment (such as no Kafka) when we’re using spring-boot-testcontainers
we can use any container with the programmatic API.
How it works
When we then start our application Spring boot will:
- look for common filenames (
compose.yml
|compose.yaml
|docker-compose.yml
|docker-compose.yaml
) - start the defined containers/services using
docker compose up
- create the service connection beans for supported containers
And when the application stops, the defined containers/services are shut down using docker compose down
Configuration
There are a slew of configuration options, but some useful ones to know:
- specifying a specific compose file:
spring.docker.compose.file
- managing the docker-compose lifecycle can be done using
spring.docker.compose.lifecycle-management
to configure it as:- none: do not start nor stop
- start-only
- start-and-stop
- making use of spring profile-specific docker compose files (
docker–compose-{profile}.yaml
) can be done using:spring.docker.compose.profiles.active
Testcontainers at development time
The setup
We just need to add a dependency on spring-boot-testcontainers
Maven:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-testcontainers</artifactId> <optional>true</optional> </dependency> </dependencies>
Gradle:
dependencies { testImplementation("org.springframework.boot:spring-boot-testcontainers") }
The application itself is a very simple one that allows us to get a die result which is then stored in our Redis
instance, and to retrieve all these rolls
Now rather than having to install a Redis instance locally, or using a docker.yaml
file, we’re making use of the new Testcontainers functionality.
As you can see in DemoConfiguration
we are making use of the new ServiceConnection
to define our Redis instance making use of a Testcontainer.
@Bean @ServiceConnection(name = "redis") GenericContainer<?> redisContainer() { return new GenericContainer<>(DockerImageName.parse("redis:latest")).withExposedPorts(6379); }
Now in TestTestcontainersDemoApplication
you’ll see that we are making use of the new SpringApplication.from
method to delegate to our actual application, and we are passing in our Test configuration.
public static void main(String[] args) { SpringApplication.from(TestcontainersDemoApplication::main) .with(DemoConfiguration.class) .run(args); }
This way we can run our application for development purposes.
Alternatively, we can make use of: ./gradlew bootTestRun
or ./mvnw spring-boot:test-run
.
After this, we can see that our application has started up including our Testcontainers.
What if my desired container does not have a ServiceConnection yet?
Using @ServiceConnection
is recommended, but not all technologies support this method yet.
If this is the case, then you can inject your @Bean
definition of the container with DynamicPropetyRegistry
to contribute the dynamic properties at development time.
This works akin to the @DnamicPropertySource
annotation from tests and allows us to add properties that become available once the container has started.
For example, let’s say we want to send out e-mails from our application and we want to use make use of MailHog
which does not have a service connection factory provided yet in spring-boot-testcontainers
we can do:
@Bean public GenericContainer mailhogContainer(DynamicPropertyRegistry registry) { GenericContainer container = new GenericContainer("mailhog/mailhog") .withExposedPorts(1025); registry.add("spring.mail.host", container::getHost); registry.add("spring.mail.port", container::getFirstMappedPort); return container; }
To provide the required information at development time.
Keeping our data
You will notice that when your application stops, the containers are also stopped.
This does mean that you’ll also lose your data.
There are two options to work around this in case you want to keep your data.
Reusable testcontainers (experimental)
The first option, Reusable Testcontainers is an experimental feature that can be used by adding .withReuse(true)
.
These containers are not stopped when your application stops!
@Bean @ServiceConnection(name = "redis") GenericContainer<?> redisContainer() { return new GenericContainer<>(DockerImageName.parse("redis:latest")) .withExposedPorts(6379) .withReuse(true); }
Given the experimental state there are still some limitations which you will have to keep in mind which are document in the
.
Spring Boot devtools with @RestartScope
The second option requires you to annotate the desired containers with @RestartScope
, and to have devtools set up.
After which they’re no longer restarted when devtools restarts your application.
For devtools we’ll need to add this to our pom.xml file:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency>
or our Gradle build file:
dependencies { developmentOnly("org.springframework.boot:spring-boot-devtools") }
and then we just need to annotate our container(s)
@Bean @ServiceConnection(name = "redis") @RestartScope GenericContainer<?> redisContainer() { return new GenericContainer<>(DockerImageName.parse("redis:latest")) .withExposedPorts(6379); }
Testcontainers desktop app
This software is not needed, but it’s still a nice extra utility to get even more mileage out of your testcontainer usage.
It was recently (donated to the community) as a free testcontainers desktop application, and can be downloaded from https://testcontainers.com/desktop/ and is available for Windows, Mac & Linux.
There are some quite useful features in there such as:
- proxying a service to a fixed port to facilitate debugging
- tracking of used images & test parallelization
- functionality to switch local runtime for (cloud based) testcontainers
- tweak Testcontainer behaviour such as freezing containers on shutdown/enable reusable testcontainers
- …
Wrap up
I hope this brief showcase was helpful and offered some new insights as to how to ease local development.
In case of any questions, feel free to reach out.
The people at the testcontainers slack are also very kind, and always willing to help out.
References
- Testcontainers: the official Testcontainers website
- Testcontainers in the cloud
- spring-boot-devtools
- Provided service connections