Do you also get lost in the configuration annotations of Spring Boot and how to use them? In this blog, you will take a look at the configuration annotations, what they mean and how you can apply them in your code. Everything explained by means of examples. Enjoy!

1. Introduction

The annotations containing the word configuration in Spring Boot can be overwhelming. You have @Configuration, @EnableConfigurationProperties, @ConfigurationPropertiesScan, etc. But what do they actually do and how do you need to apply them in your code?

A good starting point for applying best practices can be found in the official Spring Boot documentation (search for configuration). However, it lacks clear examples, in my opinion. That is the goal of this blog: explain the different options, explain the differences and show how they can be used by means of examples.

The source code used in this blog can be found at GitHub.

2. Sample Application

The sample application is created with start.spring.io and makes use of the Spring Web and Lombok dependencies.

In this section an overview is given how the repository and the basic application is set up. Details will be made clear in the sections hereafter.

The properties are located in the MyProperties class and consist out of:

  • a boolean enabled;
  • a string stringConfig;
  • a nested additional configuration item consisting out of:
    • a boolean addEnabled;
    • a string addString.

The boolean values contain their default value and the string values will be given a value in the application.properties file. The value of the addString property, will also contain a reference to the stringConfig property. This way, you can combine property values.

my.properties.string-config=First piece
my.properties.additional.add-string=${my.properties.string-config} Second piece

One ConfigurationController is added which reads the configuration properties and returns them.

@RequestMapping("/configuration")
public String configuration() {
    StringBuilder result = new StringBuilder();
    result.append("Value of enabled = ").append(myProperties.isEnabled()).append("\n");
    result.append("Value of stringConfig = ").append(myProperties.getStringConfig()).append("\n");
    if (myProperties.getAdditional() != null) {
        result.append("Value of additional.addEnabled = ").append(myProperties.getAdditional().isAddEnabled()).append("\n");
        result.append("Value of additional.addString = ").append(myProperties.getAdditional().getAddString()).append("\n");
    }
    return result.toString();
}

The application consists out of several modules. Each module corresponds to a different topic covered in this blog.

Build the application:

$ mvn clean verify

Navigate to the directory of a module and run the application:

$ mvn spring-boot:run

The endpoint can be invoked as follows:

$ curl http://localhost:8080/configuration

Each module contains three tests:

  • HttpRequestIT: this test will start a complete server using the @SpringBootTest annotation;
  • WebLayerIT: this test starts an application context with a limited number of beans using the @WebMvcTest annotation;
  • WebLayerTestPropertiesIT: identical to WebLayerIT, but this time test specific application properties are used.

3. @ConfigurationProperties + @Component

In module config1, you will map the properties to config class MyProperties by means of the @ConfigurationProperties annotation. In order to use it in the controller, you also add the @Component annotation. Also note, that setters and getters need to be available in the MyProperties class, these are generated by means of the @Getter and @Setter Lombok annotations.

The MyProperties class is the following:

@Getter
@Setter
@Component
@ConfigurationProperties("my.properties")
public class MyProperties {

    private boolean enabled;

    private String stringConfig;

    private final Additional additional = new Additional();

    @Getter
    @Setter
    public static class Additional {

        private boolean addEnabled;

        private String addString;

    }

}

In the controller, you just need to inject the MyProperties component.

@RestController
public class ConfigurationController {

    private final MyProperties myProperties;

    @Autowired
    public ConfigurationController(MyProperties myProperties) {
        this.myProperties = myProperties;
    }
   ...
} 

The tests do not need any specific annotations in this case.

3.1 Full Spring Server Test

This test (HttpRequestIT) is annotated with @SpringBootTest and it is ensured that a random port is being used in order to prevent port conflicts when running the test. The endpoint is called and the response is validated. No additional annotations are needed in order to use the properties.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HttpRequestIT {

    @Value(value="${local.server.port}")
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void configurationShouldReturnProperties() throws Exception {
        assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/configuration",
                String.class)).contains("""
                Value of enabled = false
                Value of stringConfig = First piece
                Value of additional.addEnabled = false
                Value of additional.addString = First piece Second piece""");
    }
}

3.2 Spring Application Context Without Server Test

This test (WebLayerIT) is annotated with @WebMvcTest meaning that this test starts an application context with a limited number of beans. Therefore, you need to add the @EnableConfigurationProperties annotation in order to use the properties. @EnableConfigurationProperties creates a binding between a configuration class and the test.

@WebMvcTest
@EnableConfigurationProperties(MyProperties.class)
public class WebLayerIT {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void configurationShouldReturnProperties() throws Exception {
        this.mockMvc.perform(get("/configuration")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(containsString("""
                        Value of enabled = false
                        Value of stringConfig = First piece
                        Value of additional.addEnabled = false
                        Value of additional.addString = First piece Second piece""")));
    }

}

3.3 Test Application Properties

The above tests use the application properties as defined in your application. For test purposes, it is often needed to use test specific application properties. These can be put in the tests directory. In order to use them, you have to use the @TestPropertyResource annotation. Test WebLayerTestPropertiesIT is identical to the above WebLayerIT besides that it uses the test application properties.

@WebMvcTest
@EnableConfigurationProperties(MyProperties.class)
@TestPropertySource(locations = "/application-test.properties")
public class WebLayerTestPropertiesIT {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void configurationShouldReturnProperties() throws Exception {
        this.mockMvc.perform(get("/configuration")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(containsString("""
                        Value of enabled = false
                        Value of stringConfig = First test piece
                        Value of additional.addEnabled = false
                        Value of additional.addString = First test piece Second piece""")));
    }

}

4. @ConfigurationProperties + @EnableConfigurationProperties

In module config2, you will use again the @ConfigurationProperties, just like in module config1, but this time without the @Component annotation. In order to use the properties in the controller, you need to add the @EnableConfigurationProperties annotation.

The MyProperties class without the @Component annotation:

@Getter
@Setter
@ConfigurationProperties("my.properties")
public class MyProperties {
...
}

The controller with the @EnableConfigurationProperties annotation:

@RestController
@EnableConfigurationProperties(MyProperties.class)
public class ConfigurationController {

    private final MyProperties myProperties;

    @Autowired
    public ConfigurationController(MyProperties myProperties) {
        this.myProperties = myProperties;
    }
   ...
}

It is not necessary to add the @EnableConfigurationProperties annotation in the tests because this annotation is already added to the controller.

5. @ConfigurationProperties + @Component

In module config3, you will use the same setup as in module config1, but this time with the @Configuration annotation. The @Configuration annotation creates a Spring bean of configuration stereotype. The @EnableConfigurationProperties annotation should not be used directly together with the @Configuration annotation, see also this Stack Overflow answer of Andy Wilkinson.

The MyProperties class is identical to the one of module config2:

@Getter
@Setter
@ConfigurationProperties("my.properties")
public class MyProperties {
...
}

You introduce a new class ApplicationConfig which will act as a configuration bean. You annotate it therefore with @Configuration. You also need to annotate it with @EnableConfigurationProperties so that the properties are available to the bean.

@Configuration
@EnableConfigurationProperties(MyProperties.class)
public class ApplicatonConfig {

}

The controller does not need any extra annotations:

@RestController
public class ConfigurationController {

    private final MyProperties myProperties;

    @Autowired
    public ConfigurationController(MyProperties myProperties) {
        this.myProperties = myProperties;
    }
    ...
}

The tests are identical to the tests of config1, it is necessary to add the @EnableConfigurationProperties annotation to the @WebMvcTest tests.

6. @ConfigurationProperties + @ConfigurationPropertiesScan

In module config4, you use @ConfigurationProperties to map the properties to class MyProperties. This time, you add annotation @ConfigurationPropertiesScan to the class MySpringBootConfigurationPlanetApplication, the one annotated with @SpringBootApplication. With the @ConfigurationPropertiesScan annotation, you can also specify which packages contain the configuration classes.

The main class is defined as follows:

@SpringBootApplication
@ConfigurationPropertiesScan("com.mydeveloperplanet.myspringbootconfigurationplanet.config4.config")
public class MySpringBootConfigurationPlanetApplication {

	public static void main(String[] args) {
		SpringApplication.run(MySpringBootConfigurationPlanetApplication.class, args);
	}

}

The MyProperties class only needs @ConfigurationProperties.

@Getter
@Setter
@ConfigurationProperties("my.properties")
public class MyProperties {
...
}

The controller does not need any configuration annotations:

@RestController
public class ConfigurationController {

    private final MyProperties myProperties;

    @Autowired
    public ConfigurationController(MyProperties myProperties) {
        this.myProperties = myProperties;
    }
    ...
}

The tests are again identical to the tests of config1.

7. Constructor Binding

In module config5, you use the same setup as the previous config4 module. This time you will use constructor binding in the MyProperties class. The differences are:

  • No need to specify setters and the properties can be made final;
  • You need to add a constructor to map the configuration;
  • Default values need to be specified by means of the @DefaultValue annotation in the constructor argument list;
  • You may need to annotate nested properties with @DefaultValue, otherwise they are considered to be absent when no property of the nested property is assigned a value.

The MyProperties class is the following:

@Getter
@ConfigurationProperties("my.properties")
public class MyProperties {

    private final boolean enabled;

    private final String stringConfig;

    private final Additional additional;

    public MyProperties(boolean enabled, String stringConfig, @DefaultValue Additional additional) {
        this.enabled = enabled;
        this.stringConfig = stringConfig;
        this.additional = additional;
    }

    @Getter
    public static class Additional {

        private final boolean addEnabled;

        private final String addString;

        public Additional(boolean addEnabled, String addString) {
            this.addEnabled = addEnabled;
            this.addString = addString;
        }
    }

}

If you leave out the @DefaultValue in the argument list for argument additional and you put property my.properties.additional.add-string in application.properties in comment, you will notice that the output is:

Value of enabled = false
Value of stringConfig = First test piece

instead of :

Value of enabled = false
Value of stringConfig = First test piece
Value of additional.addEnabled = false
Value of additional.addString = null

when you run the WebLayerIT test.

The tests are again identical to the tests of config1.

8. Conclusion

There are several options in order to configure properties in a Spring Boot application. This post tries to cover some of the options and explains the differences by example. The Constructor Binding configuration is different from the other options because it does not let you to modify the properties during runtime.

Which one to choose? In my opinion, the @ConfigurationProperties + @ConfigurationPropertiesScan or the Constructor Binding are the ones to choose from as they require the least annotations. Constructor Binding is even more safe because the properties cannot be modified during runtime. However, you should always consider the options based on your use case.