@Configuration and @ConfigurationBuilder

Learn how to utilize @Configuration and @ConfigurationBuilder annotations to effectively configure declared properties.

Authors: Nirav Assar

Micronaut Version: 1.2.7

1 Getting Started

In this guide you are going to learn how to effectively use the annotations @ConfigurationProperties, @ConfigurationBuilder, and @EachProperty to use configured properties in a Micronaut application. These annotations allow declared values to be injected into a bean for easy usage in the application.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 How to complete the guide

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the Application

The app was created with Micronaut CLI using the mn create-app command. The /initial folder was created as such so you as a developer can follow along with the coding steps.

3 Team Configuration with @ConfigurationProperties

Imagine a feature where you can configure a sports team in a declarative manner. The team has a few attributes like team name, color, and players.

src/main/resources/application.yml
team:
  name: 'Steelers'
  color: 'Black'
  player-names:
    - 'Mason Rudolph'
    - 'James Connor'

With Micronaut we can use the @ConfigurationProperties annotation to slurp the configuration into a bean. Each property that matches the configuration in the application.yml will call the setter in the bean. The bean will be subsequently available for injection in the application!

src/main/java/example/micronaut/TeamConfiguration.java
@ConfigurationProperties("team")
public class TeamConfiguration {
    private String name;
    private String color;
    private List<String> playerNames;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public List<String> getPlayerNames() {
        return playerNames;
    }

    public void setPlayerNames(List<String> playerNames) {
        this.playerNames = playerNames;
    }
}

3.1 Test @ConfigurationProperties

Let’s validate that the bean is available in the application context and is created with the values declared in the application.xml.

src/test/java/example/micronaut/TeamConfigurationTests.java
    @Test
    void testTeamConfiguration() {

        List<String> names = Arrays.asList("Nirav Assar", "Lionel Messi");
        Map<String, Object> items = new HashMap<>();
        items.put("team.name", "evolution");
        items.put("team.color", "green");
        items.put("team.player-names", names);

        ApplicationContext ctx = ApplicationContext.run(ApplicationContext.class, items); (1)
        TeamConfiguration teamConfiguration = ctx.getBean(TeamConfiguration.class);

        assertEquals("evolution", teamConfiguration.getName());
        assertEquals("green", teamConfiguration.getColor());
        assertEquals(names.size(), teamConfiguration.getPlayerNames().size());
        names.forEach(name -> assertTrue(teamConfiguration.getPlayerNames().contains(name)));

        ctx.close();
    }
}
1 Setup configuration properties for the test to use

4 Team Admin Builder with @ConfigurationBuilder

The Builder pattern is a great way to build configuration objects incrementally. Read about the Builder pattern in this DZone article to learn more. Micronaut supports the Builder pattern with @ConfigurationBuilder.

Let’s suppose we want to add team administrators to a team. The team administration is composed by using a builder pattern object. We can add a coach, manager and president to the team.

src/main/resources/application.yml
team:
  name: 'Steelers'
  color: 'Black'
  player-names:
    - 'Mason Rudolph'
    - 'James Connor'
  team-admin:
    manager: 'Nirav Assar' (1)
    coach: 'Mike Tomlin'
    president: 'Dan Rooney'
1 manager property is an example of an element that will be built

The TeamAdmin object abides by the Builder pattern.

src/main/java/example/micronaut/TeamAdmin.java
package example.micronaut;

public class TeamAdmin { (1)

    private String manager;
    private String coach;
    private String president;

    // should use the builder pattern to create the object
    private TeamAdmin() {
    }

    public String getManager() {
        return manager;
    }

    public void setManager(String manager) {
        this.manager = manager;
    }

    public String getCoach() {
        return coach;
    }

    public void setCoach(String coach) {
        this.coach = coach;
    }

    public String getPresident() {
        return president;
    }

    public void setPresident(String president) {
        this.president = president;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder { (2)
        private String manager;
        private String coach;
        private String president;

        (3)
        public Builder withManager(String manager) {
            this.manager = manager;
            return this;
        }

        public Builder withCoach(String coach) {
            this.coach = coach;
            return this;
        }

        public Builder withPresident(String president) {
            this.president = president;
            return this;
        }

        public TeamAdmin build() { (4)
            TeamAdmin teamAdmin = new TeamAdmin();
            teamAdmin.manager = this.manager;
            teamAdmin.coach = this.coach;
            teamAdmin.president = this.president;
            return teamAdmin;
        }

        public String getManager() {
            return manager;
        }

        public String getCoach() {
            return coach;
        }

        public String getPresident() {
            return president;
        }
    }
}
1 TeamAdmin is the configuration object which consumes the declared properties.
2 The builder object is used to incrementally construct the object.
3 An example of a builder method, where a attribute is set and then the builder itself is returned.
4 The final build() method creates the TeamAdmin object.

At the bottom of TeamConfiguration, we add the inner class TeamAdmin.Builder and annotate it with @ConfigurationBuilder. This tells Micronaut that configuration can be read in and an object can be constructed using the Builder pattern.

We are using the builder only here, so we will have to call builder.build() to actually get the TeamAdmin object, at a later time. In our case, we will call builder.build() in the JUnit test.
src/main/java/example/micronaut/TeamConfiguration.java
@ConfigurationProperties("team")
public class TeamConfiguration {
    private String name;
    private String color;
    private List<String> playerNames;

    public TeamConfiguration() {
    }

    @ConfigurationBuilder(prefixes = "with", configurationPrefix = "team-admin") (1)
    protected TeamAdmin.Builder builder = TeamAdmin.builder(); (2)

    public TeamAdmin.Builder getBuilder() {
        return builder;
    }

    public void setBuilder(TeamAdmin.Builder builder) {
        this.builder = builder;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public List<String> getPlayerNames() {
        return playerNames;
    }

    public void setPlayerNames(List<String> playerNames) {
        this.playerNames = playerNames;
    }
}
1 prefixes tells Micronaut to find methods that are prefixed by with; configurationPrefix allows the developer to customize the application.yml element
2 Instantiate the builder object so it can be populated with configuration values.

4.1 Test @ConfigurationBuilder

We can validate @ConfigurationBuilder is applied properly with the following JUnit test. The test format is similar to previous tests.

src/test/java/example/micronaut/TeamConfigurationSpec.java
    @Test
    void testTeamConfigurationBuilder() {

        List<String> names = Arrays.asList("Nirav Assar", "Lionel Messi");
        Map<String, Object> items = new HashMap<>();
        items.put("team.name", "evolution");
        items.put("team.color", "green");
        items.put("team.team-admin.manager", "Jerry Jones"); (1)
        items.put("team.team-admin.coach", "Tommy O'Neill");
        items.put("team.team-admin.president", "Mark Scanell");
        items.put("team.player-names", names);

        ApplicationContext ctx = ApplicationContext.run(ApplicationContext.class, items);
        TeamConfiguration teamConfiguration = ctx.getBean(TeamConfiguration.class);
        TeamAdmin teamAdmin = teamConfiguration.builder.build(); (2)

        assertEquals("evolution", teamConfiguration.getName());
        assertEquals("green", teamConfiguration.getColor());
        assertEquals("Nirav Assar", teamConfiguration.getPlayerNames().get(0));
        assertEquals("Lionel Messi", teamConfiguration.getPlayerNames().get(1));

        // check the builder has values set
        assertEquals("Jerry Jones", teamConfiguration.builder.getManager());
        assertEquals("Tommy O'Neill", teamConfiguration.builder.getCoach());
        assertEquals("Mark Scanell", teamConfiguration.builder.getPresident());

        // check the object can be built
        assertEquals("Jerry Jones", teamAdmin.getManager()); (3)
        assertEquals("Tommy O'Neill", teamAdmin.getCoach());
        assertEquals("Mark Scanell", teamAdmin.getPresident());

        ctx.close();
    }
1 Properties which will invoke the builder methods on TeamAdmin.Builder
2 The builder object is now configured, so we must run build() on it to create the TeamAdmin object
3 Verify the object is created with the applicaton.yml properties

5 Stadiums with @EachProperty

Micronaut is also able to read a "list" of configurations that are related. Imagine we would like to declare stadiums and their attributes.

src/main/resources/application.yml
stadium:
  coors: (1)
    city: 'Denver'
    size: 50000
  pnc:
    city: 'Pittsburgh'
    size: 35000
1 This element will be the name of the bean.

We can use @EachProperty which will cycle through the configuration and read each nested clause as a bean. The higher level property will be parameterized as the name.

src/main/java/example/micronaut/StadiumConfiguration.java
package example.micronaut;


import io.micronaut.context.annotation.EachProperty;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.core.annotation.Introspected;

@Introspected
@EachProperty("stadium") (1)
public class StadiumConfiguration {
    private String name; (2)
    private String city;
    private Integer size;

    public StadiumConfiguration(@Parameter String name) { (2)
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public Integer getSize() {
        return size;
    }

    public void setSize(Integer size) {
        this.size = size;
    }
}
1 Establish the top layer of configuration
2 name is read in from the property key and send as a parameter to the bean.

5.1 Test @EachProperty

Validate the configuration with a test. Notice multiple beans are created from the configuration. In a controller we can inject a particular StadiumConfiguration instance bean by using the @Named parameter with qualifier name.

src/test/java/example/micronaut/StadiumConfigurationTests.java
package example.micronaut;

import io.micronaut.context.ApplicationContext;
import io.micronaut.inject.qualifiers.Qualifiers;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class StadiumConfigurationTests {

    @Test
    void testStadiumConfiguration() {
        Map<String, Object> items = new HashMap<>();
        items.put("stadium.fenway.city", "Boston"); (1)
        items.put("stadium.fenway.size", 60000);
        items.put("stadium.wrigley.city", "Chicago"); (1)
        items.put("stadium.wrigley.size", 45000);

        ApplicationContext ctx = ApplicationContext.run(ApplicationContext.class, items);
        (2)
        StadiumConfiguration fenwayConfiguration = ctx.getBean(StadiumConfiguration.class, Qualifiers.byName("fenway"));
        StadiumConfiguration wrigleyConfiguration = ctx.getBean(StadiumConfiguration.class, Qualifiers.byName("wrigley"));

        assertEquals("fenway", fenwayConfiguration.getName());
        assertEquals(60000, fenwayConfiguration.getSize());
        assertEquals("wrigley", wrigleyConfiguration.getName());
        assertEquals(45000, wrigleyConfiguration.getSize());

        ctx.close();
    }
}
1 Multiple configurations can be declared for the same class.
2 Since there are multiple beans to retrieve a bean a Qualifier must be sent.

6 Running the Application

To run the application use the ./gradlew run command which will start the application on port 8080.

7 Controller

Configuration beans can be injected into the application with just like any other beans. As a demonstration, create a controller where the beans are constructor injected. The StadiumConfiguration class has two instances, so for injection we need to use the @Named annotation with a qualifier name to specify the bean.

src/main/java/example/micronaut/MyController.java
package example.micronaut;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

import javax.annotation.Nullable;
import javax.inject.Named;

@Controller("/my")
public class MyController {

    private final TeamConfiguration teamConfiguration;
    private final StadiumConfiguration stadiumConfiguration;

    public MyController(@Nullable TeamConfiguration teamConfiguration,
                        @Nullable @Named("pnc") StadiumConfiguration stadiumConfiguration) { (1)
        this.teamConfiguration = teamConfiguration;
        this.stadiumConfiguration = stadiumConfiguration;
    }

    @Get("/team")
    public TeamConfiguration team() {
        return this.teamConfiguration;
    }

    @Get("/stadium")
    public  StadiumConfiguration stadium() {
        return this.stadiumConfiguration;
    }
}
1 Injection of configuration beans; @Named annotation is needed to choose which StadiumConfiguration instance is retrieved.
In the browser go to http://localhost:8080/my/team and http://localhost:8080/my/stadium. To test the app, run ./gradlew test

Add test:

src/test/java/example/micronaut/MyControllerTest.java
package example.micronaut;

import example.micronaut.StadiumConfiguration;
import example.micronaut.TeamConfiguration;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.runtime.server.EmbeddedServer;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import javax.inject.Inject;

import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@MicronautTest
public class MyControllerTest {
    @Inject
    EmbeddedServer server;

    @Inject
    @Client("/")
    HttpClient client;

    @Test
    void testMyTeam() {
        TeamConfiguration teamConfiguration = client.toBlocking()
                .retrieve(HttpRequest.GET("/my/team"), TeamConfiguration.class);
        assertEquals("Steelers", teamConfiguration.getName());
        assertEquals("Black", teamConfiguration.getColor());
        List<String> expectedPlayers = Arrays.asList("Mason Rudolph", "James Connor");
        assertEquals(expectedPlayers.size(), teamConfiguration.getPlayerNames().size());
        expectedPlayers.forEach(name -> assertTrue(teamConfiguration.getPlayerNames().contains(name)));
    }

    @Test
    void testMyStadium() {
        StadiumConfiguration conf = client.toBlocking()
                .retrieve(HttpRequest.GET("/my/stadium"), StadiumConfiguration.class);
        assertEquals("Pittsburgh", conf.getCity());
        assertEquals(35000, conf.getSize());
    }
}

8 Generating a Micronaut Application's Native Image with GraalVM

Micronaut itself does not rely on reflection or dynamic classloading so works automatically with GraalVM native.

First, add Micronaut Graal’s annotation processor that helps to handle generating the reflection-config.json metadata that is automatically picked up by the native-image tool:

build.gradle
dependencies {
...
..
.
    annotationProcessor "io.micronaut:micronaut-graal"
}

GraalVM Native Image allows you to ahead-of-time compile Java code to a standalone executable, called a native image. This executable includes the application, the libraries, the JDK and does not run on the Java VM, but includes necessary components like memory management and thread scheduling from a different virtual machine, called “Substrate VM”. Substrate VM is the name for the runtime components (like the deoptimizer, garbage collector, thread scheduling etc.). The resulting program has faster startup time and lower runtime memory overhead compared to a Java VM.

We need to add a dependency to svm:

build.gradle
dependencies {
...
..
.
    compileOnly "org.graalvm.nativeimage:svm:19.3.0"
}

To simplify building the image you need to create a native-image.properties file. The convention is to use the folder src/main/resources/META-INF/native-image and then a folder following the maven coordinates of the application. For our example example.micronaut/micronautguide.

Add a native-image.properties inside src/main/resources/META-INF/native-image/example.micronaut/micronautguide folder.

src/main/resources/META-INF/native-image/example.micronaut/micronautguide/native-image.properties
Args = -H:IncludeResources=logback.xml|application.yml \
       -H:Name=micronautguide \
       -H:Class=example.micronaut.Application
  • The -H:IncludeResources argument allows you to tweak which static resources to include. You can use regular expressions.

  • The -H:Name argument specifies the name of the native image that will be generated.

  • The -H:Class argument specifies the entry point of your application (the class that defines a static void main method.

To generate the native image you need to generate the FAT JAR first.

$ ./gradlew assemble

Invoke native-image. It may take a minute to complete.

$ which java
/Users/sdelamo/.sdkman/candidates/java/19.3.0.r8-grl/bin/java
$ native-image --no-server -cp build/libs/complete-0.1-all.jar
[micronautguide:39362]    classlist:   3,884.97 ms
[micronautguide:39362]        (cap):   5,919.80 ms
[micronautguide:39362]        setup:   7,222.88 ms
[micronautguide:39362]   (typeflow):  14,037.78 ms
[micronautguide:39362]    (objects):  17,689.40 ms
[micronautguide:39362]   (features):   1,751.37 ms
[micronautguide:39362]     analysis:  35,421.99 ms
[micronautguide:39362]     (clinit):     978.01 ms
[micronautguide:39362]     universe:   2,114.65 ms
[micronautguide:39362]      (parse):   2,327.75 ms
[micronautguide:39362]     (inline):   2,153.46 ms
[micronautguide:39362]    (compile):  13,066.94 ms
[micronautguide:39362]      compile:  19,714.97 ms
[micronautguide:39362]        image:   3,509.84 ms
[micronautguide:39362]        write:   1,114.96 ms
[micronautguide:39362]      [total]:  73,294.82 ms

--no-server options tells to not use server-based image building.

You can invoke the generated native image micronautguide. Startup will be really fast.

 $ ./micronautguide -Xmx68m
07:42:20.792 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 21ms. Server Running: http://localhost:8080

You can invoke the controller exposed by the native image:

curl "http://localhost:8080/my/stadium"

or

curl "http://localhost:8080/my/team"

Alternatively, you can use Dockerfile to construct the native image and a script docker-build.sh to run it:

Dockerfile
FROM oracle/graalvm-ce:19.3.0-java8 as graalvm
COPY . /home/app/micronautguide
WORKDIR /home/app/micronautguide
RUN gu install native-image
RUN native-image --no-server -cp build/libs/complete-*-all.jar

FROM frolvlad/alpine-glibc
EXPOSE 8080
COPY --from=graalvm /home/app/micronautguide /app/micronautguide
ENTRYPOINT ["/app/micronautguide","-Xmx68m"]
docker-build.sh
#!/bin/sh
docker build . -t micronautguide
echo
echo
echo "To run the docker container execute:"
echo "    $ docker run -p 8080:8080 micronautguide"
$ ./gradlew assemble

$ ./docker-build.sh
Sending build context to Docker daemon  64.67MB
Step 1/9 : FROM oracle/graalvm-ce:19.3.0-java8 as graalvm
 ---> d625000893c9
Step 2/9 : COPY . /home/app/micronautguide
 ---> 4c0daf50c6f3
Step 3/9 : WORKDIR /home/app/micronautguide
 ---> Running in e07cf22b668a
Removing intermediate container e07cf22b668a
 ---> d3699ea8e284
Step 4/9 : RUN gu install native-image
 ---> Running in 71edf17b9a11
Downloading: Component catalog from www.graalvm.org
Processing Component: Native Image
Downloading: Component native-image: Native Image  from github.com
Installing new component: Native Image (org.graalvm.native-image, version 19.3.0)
Refreshed alternative links in /usr/bin/
Removing intermediate container 71edf17b9a11
 ---> ee26c71bbd1e
Step 5/9 : RUN native-image --no-server --static -cp build/libs/complete-*-all.jar
 ---> Running in d2cd451f33be
[micronautguide:28]    classlist:   8,773.36 ms
[micronautguide:28]        (cap):   1,044.04 ms
[micronautguide:28]        setup:   2,748.03 ms
[micronautguide:28]   (typeflow):  20,611.56 ms
[micronautguide:28]    (objects):  20,377.15 ms
[micronautguide:28]   (features):   2,168.30 ms
[micronautguide:28]     analysis:  45,559.16 ms
[micronautguide:28]     (clinit):   1,141.31 ms
[micronautguide:28]     universe:   2,719.10 ms
[micronautguide:28]      (parse):   2,808.15 ms
[micronautguide:28]     (inline):   3,094.66 ms
[micronautguide:28]    (compile):  25,648.31 ms
[micronautguide:28]      compile:  34,018.50 ms
[micronautguide:28]        image:   3,811.46 ms
[micronautguide:28]        write:     643.59 ms
[micronautguide:28]      [total]:  99,095.08 ms
Removing intermediate container d2cd451f33be
 ---> 429a467e2fdd
Step 6/9 : FROM frolvlad/alpine-glibc
 ---> 38dd85a430e8
Step 7/9 : EXPOSE 8080
 ---> Using cache
 ---> 643f9d29e3f8
Step 8/9 : COPY --from=graalvm /home/app/micronautguide/micronautguide /app/micronautguide
 ---> 0d41749e3ceb
Step 9/9 : ENTRYPOINT ["/app/micronautguide"]
 ---> Running in 31b4c6c21e68
Removing intermediate container 31b4c6c21e68
 ---> 3ff1de337ef4
Successfully built 3ff1de337ef4
Successfully tagged micronautguide:latest


To run the docker container execute:
    $ docker run -p 8080:8080 micronautguide

You can use docker to run the image with the app’s native-image:

 docker run -p 8080:8080 micronautguide
06:58:26.977 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 27ms. Server Running: http://f8143044b1ee:8080

9 Where To Go From Here?

10 Help with Micronaut

OCI sponsored the creation of this Guide. OCI offers several Micronaut services:

Free consultation

The OCI Micronaut Team includes Micronaut co-founders, Jeff Scott Brown and Graeme Rocher. Check our Micronaut courses and learn from the engineers who developed, matured and maintain Micronaut.

Micronaut OCI Team