/sbldi

Spring boot local development improvements demo

Primary LanguageJavaMIT LicenseMIT

Spring Boot local development improvements

Quite often when we are developing an application we will need some external services such as rabbitMQ, Kafka, …​

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.

  1. 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

  2. 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 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 announcement post.

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 testcontainers slack are also very kind, and always willing to help out.

References