@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

Create a Kotlin Micronaut app using the Micronaut Command Line Interface.

mn create-app example.micronaut.complete --lang=kotlin

The previous command creates a micronaut app with the default package example.micronaut in a folder named complete.

Due to the --lang kotlin flag, it generates a Kotlin Micronaut app that uses the Gradle build system. However, you could use other build tools such as Maven or other programming languages such as Java or Groovy.

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

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/kotlin/example/micronaut/TeamConfiguration.kt
package example.micronaut

import io.micronaut.context.annotation.ConfigurationBuilder
import io.micronaut.context.annotation.ConfigurationProperties

@ConfigurationProperties("team")
class TeamConfiguration  {
    var name: String? = null
    var color: String? = null
    var playerNames: List<String>? = null
}

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/kotlin/example/micronaut/TeamConfigurationTest.kt
package example.micronaut

import io.kotlintest.shouldBe
import io.kotlintest.specs.BehaviorSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.test.annotation.MicronautTest
import java.util.function.Consumer

@MicronautTest
class TeamConfigurationTest : BehaviorSpec({
    given("an application context started with configuration") {
        val names = listOf("Nirav Assar", "Lionel Messi")
        val items = mapOf("team.name" to "evolution",
                "team.color" to "green",
                "team.player-names" to names)
        val ctx = ApplicationContext.run(ApplicationContext::class.java, items) (1)
        `when`("TeamConfiguration is retrieved from the context") {
            val teamConfiguration = ctx.getBean(TeamConfiguration::class.java)
            then("configuration properties are populated") {
                teamConfiguration.name shouldBe "evolution"
                teamConfiguration.color shouldBe "green"
                teamConfiguration.playerNames!!.size shouldBe names.size
                names.forEach(Consumer { name: String? -> teamConfiguration.playerNames!!.contains(name) shouldBe true })
            }
        }
        ctx.close()
    }
})
1 Start application context with configuration

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/kotlin/example/micronaut/TeamAdmin.kt
package example.micronaut

class TeamAdmin private constructor(
        val manager: String?,
        val coach: String?,
        val president: String?) { (1)
    data class Builder( (2)
            var manager: String? = null,
            var coach: String? = null,
            var president: String? = null) {
        fun withManager(manager: String) = apply { this.manager = manager } (3)
        fun withCoach(coach: String) = apply { this.coach = coach }
        fun withPresident(president: String) = apply { this.president = president }
        fun build() = TeamAdmin(manager, coach, president) (4)
    }
}
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.

Add a test for the builder:

src/test/kotlin/example/micronaut/TeamAdminTest.kt
package example.micronaut;

import io.kotlintest.shouldBe
import io.kotlintest.specs.BehaviorSpec

class TeamAdminTest : BehaviorSpec({
    given("A Team Admin class") {
        `when`("A team admin is constructed with the Builder") {
            val teamAdmin = TeamAdmin.Builder().withManager("Nirav")
                    .withCoach("Tommy")
                    .withPresident("Mark").build()
            then("the team admin manager is populated") {
                teamAdmin.manager shouldBe "Nirav"
                teamAdmin.coach shouldBe "Tommy"
                teamAdmin.president shouldBe "Mark"
            }
        }
    }
})

At the bottom of TeamConfiguration, we add a 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/kotlin/example/micronaut/TeamConfiguration.kt
    @ConfigurationBuilder(prefixes = ["with"], configurationPrefix = "team-admin") (1)
    var builder = TeamAdmin.Builder() (2)
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/kotlin/example/micronaut/TeamConfigurationBuilderTest.kt
package example.micronaut

import io.kotlintest.shouldBe
import io.kotlintest.specs.BehaviorSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.test.annotation.MicronautTest
import java.util.function.Consumer

@MicronautTest
class TeamConfigurationBuilderTest : BehaviorSpec({
    given("an application context started with configuration") {
        val names = listOf("Nirav Assar", "Lionel Messi")
        val items = mapOf("team.name" to "evolution",
                "team.color" to "green",
                "team.team-admin.manager" to "Jerry Jones", (1)
                "team.team-admin.coach" to "Tommy O'Neill",
                "team.team-admin.president" to "Mark Scanell",
                "team.player-names" to names)
        val ctx = ApplicationContext.run(ApplicationContext::class.java, items) (1)
        `when`("TeamConfiguration is retrieved from the context") {
            val teamConfiguration = ctx.getBean(TeamConfiguration::class.java)
            then("configuration properties are populated") {
                teamConfiguration.name shouldBe "evolution"
                teamConfiguration.color shouldBe "green"
                teamConfiguration.playerNames!!.size shouldBe names.size
                names.forEach(Consumer { name: String? -> teamConfiguration.playerNames!!.contains(name) shouldBe true })
                teamConfiguration.builder.manager shouldBe "Jerry Jones"
                teamConfiguration.builder.coach shouldBe "Tommy O'Neill"
                teamConfiguration.builder.president shouldBe "Mark Scanell"

                val teamAdmin = teamConfiguration.builder.build() (2)
                teamAdmin.manager shouldBe "Jerry Jones" (3)
                teamAdmin.coach shouldBe "Tommy O'Neill"
                teamAdmin.president shouldBe "Mark Scanell"
            }
        }
        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/kotlin/example/micronaut/StadiumConfiguration.kt
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)
class StadiumConfiguration
constructor(@param:Parameter val name: String) {  (2)
    var city: String? = null
    var size: Int? = null
}
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/kotlin/example/micronaut/StadiumConfigurationTest.kt
package example.micronaut

import io.kotlintest.shouldBe
import io.kotlintest.specs.BehaviorSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.inject.qualifiers.Qualifiers
import io.micronaut.test.annotation.MicronautTest

@MicronautTest
class StadiumConfigurationTest : BehaviorSpec({

    given("an application context started with configuration") {
        val items = mapOf("stadium.fenway.city" to "Boston", (1)
                "stadium.fenway.size" to  60000,
                "stadium.wrigley.city" to  "Chicago", (1)
                "stadium.wrigley.size" to 45000
        )
        val ctx = ApplicationContext.run(ApplicationContext::class.java, items)

        `when`("StadiumConfiguration are retrieved with bean qualifier") {
            val fenwayConfiguration = ctx.getBean(StadiumConfiguration::class.java, Qualifiers.byName("fenway"))  (2)
            val wrigleyConfiguration = ctx.getBean(StadiumConfiguration::class.java, Qualifiers.byName("wrigley")) (2)

            then("configuration properties are populated") {
                fenwayConfiguration.name shouldBe "fenway"
                fenwayConfiguration.size shouldBe 60000
                wrigleyConfiguration.name shouldBe "wrigley"
                wrigleyConfiguration.size shouldBe 45000
            }
        }

        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/kotlin/example/micronaut/MyController.kt
package example.micronaut

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import javax.inject.Named

@Controller("/my")
class MyController(val teamConfiguration: TeamConfiguration,
                   @Named("pnc") val stadiumConfiguration: StadiumConfiguration) {

    @Get("/team")
    fun team(): TeamConfiguration {
        return teamConfiguration
    }

    @Get("/stadium")
    fun stadium(): StadiumConfiguration {
        return 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/kotlin/example/micronaut/MyControllerTest.kt
package example.micronaut

import io.kotlintest.shouldBe
import io.kotlintest.specs.BehaviorSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.context.env.Environment
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.test.annotation.MicronautTest
import java.util.function.Consumer

@MicronautTest
class MyControllerTest : BehaviorSpec() {
    init {
        given("the math service") {
            val embeddedServer = autoClose(ApplicationContext.run(EmbeddedServer::class.java, mapOf(), Environment.TEST))

            val client = autoClose(embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url))

            `when`("fetching the teams config") {
                val expectedPlayers = listOf("Mason Rudolph", "James Connor")
                val teamConfiguration = client.toBlocking().retrieve(HttpRequest.GET<Any>("/my/team"), TeamConfiguration::class.java)
                then("the config values from application.yml are used") {
                    teamConfiguration.name shouldBe "Steelers"
                    teamConfiguration.color shouldBe "Black"
                    expectedPlayers.size shouldBe teamConfiguration.playerNames!!.size
                    expectedPlayers.forEach(Consumer { name: String? -> teamConfiguration.playerNames!!.contains(name) shouldBe true })
                }
            }
            `when`("fetching the stadium config") {
                val conf = client.toBlocking().retrieve(HttpRequest.GET<Any>("/my/stadium"), StadiumConfiguration::class.java)
                then("the config values from application.yml are used") {
                    conf.city shouldBe "Pittsburgh"
                    conf.size shouldBe 35000
                }
            }
        }
    }
}

8 Generating a Micronaut Application's Native Image with GraalVM

We are going to use GraalVM, the polyglot embeddable virtual machine, to generate a Native image of our Micronaut application.

Native images compiled with GraalVM ahead-of-time improve the startup time and reduce the memory footprint of JVM-based applications.

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 {
...
..
.
    kapt "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.

The easiest way to install GraalVM is to use SDKMan.io.

$ sdk list java
================================================================================
Available Java Versions
================================================================================
 Vendor        | Use | Version      | Dist    | Status     | Identifier
--------------------------------------------------------------------------------
....
 GraalVM       |     | 19.3.0.r11   | grl     | installed  | 19.3.0.r11-grl
               |     | 19.3.0.r8    | grl     | installed  | 19.3.0.r8-grl
               |     | 19.2.1       | grl     |            | 19.2.1-grl
               |     | 19.1.1       | grl     |            | 19.1.1-grl
               |     | 19.0.2       | grl     |            | 19.0.2-grl


# For Java 8
$ sdk install java 19.3.0.r8-grl

# For Java 11
$ sdk install java 19.3.0.r11-grl

You need to install the native-image component which is not installed by default.

$ gu install native-image

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?