Spring Boot Configuration

In this article, you will learn more about Spring Boot configuration. I’ll show you how to use it effectively in different environments. Especially, we talk a little bit more about configuration for Kubernetes. There are a lot of available options including properties, YAML files, environment variables, and command-line arguments. The thing we want to achieve is a strict separation of config from code for our app. We should comply with the third rule of the Twelve-Factor App.

If you like topics related to the Spring Boot configuration you may be interested in two other articles on my blog. In order to read about Spring Boot auto-configuration please refer to the following article. To find out more about configuration in general read that post.

Source Code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. After that, you should just follow my instructions. Let’s begin.

Levels of loading configuration

Configuration management is a very interesting topic in Spring Boot. In total, there are 14 levels of ordering property values and 4 levels of ordering config data files. You will find the full list of all levels here. Let’s begin with the first example. Let’s assume there is a single property property.default inside the Spring Boot application.yml.

1
property.default: app

We have another configuration file in the classpath additional.yml. It contains the same property, but with a different value.

1
property.default: additional

Our Spring Boot app loads the additional.yml file on startup using the @PropertySource annotation. That’s not all. In the same code, it also set a default value of that property using the SpringApplication.setDefaultProperties method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootApplication
@PropertySource(value = "classpath:/additional.yml", 
                ignoreResourceNotFound = true)
public class ConfigApp {

   private static final Logger LOG = 
      LoggerFactory.getLogger(ConfigApp.class);

   public static void main(String[] args) {
      SpringApplication app = new SpringApplication(ConfigApp.class);
      app.setDefaultProperties(Map.of("property.default", "default"));
      app.setAllowBeanDefinitionOverriding(true);
      app.run(args);
   }

   @Value("${property.default}")
   String propertyDefault;

   @PostConstruct
   public void printInfo() {
      LOG.info("========= Property (default): {}", propertyDefault);
   }

}

What’s the final value of the property load by the app? Before we answer that question, let’s complicate our example a little bit more. We set the environment variable PROPERTY_DEFAULT as shown below.

1
$ export PROPERTY_DEFAULT=env

Finally, we can run the app. The following fragment of code <em>LOG</em>.info("========= Property (default): {}", propertyDefault) will print the value env. The environment variable overrides all other levels used in our example. In fact, that’s just a fifth config level of 14.

spring-boot-configuration-env|696x352

Spring configuration on Kubernetes

Usually, in cloud platforms, we use environment variables to configure our apps. For example, in Kubernetes, we have two types of objects for managing configuration: Secret and ConfigMap. We can define the value of our property inside Kubernetes ConfigMap as shown below.

1
2
3
4
5
6
apiVersion: v1
kind: ConfigMap
metadata:
  name: springboot-configuration-playground
data:
  PROPERTY_DEFAULT: env

We can pass this value to the app just by attaching our ConfigMap to the Kubernetes Deployment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot-configuration-playground
spec:
  selector:
    matchLabels:
      app: springboot-configuration-playground
  template:
    metadata:
      labels:
        app: springboot-configuration-playground
    spec:
      containers:
      - name: springboot-configuration-playground
        image: piomin/springboot-configuration-playground
        ports:
        - containerPort: 8080
        envFrom:
          - configMapRef:
              name: springboot-configuration-playground

In fact, we get the same result as before for the environment variable set on the local OS. Our app is using the value env as the propertyDefault variable. Although we have 14 levels of configuration in Spring Boot, we can use just five of them to effectively manage the app. I will say more - be careful with using other config levels since you may disable overriding values with environment variables. Of course, you may have reasons for that. But it is important to understand that levels before starting with a more complex configuration. You can verify the logs after deployment using the kubectl logs command:

|696x332

In Kubernetes, we can also pass the whole configuration files to the Deployment via ConfigMap, not just separated properties. Now, let’s assume that instead of setting the environment variable we just create the application.yml file as shown below.

1
2
3
4
5
6
7
apiVersion: v1
kind: ConfigMap
metadata:
  name: springboot-configuration-playground-ext
data:
  application.yml: |
        property.default: external

We need to reconfigure the app Deployment YAML. We will attach our ConfigMap as a mounted volume under the /config path. Since we use Jib Maven plugin as a tool for building image the main app running directory is /. By default, Spring Boot reads properties from the files located in the current directory or in the /config subdirectory in the current directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot-configuration-playground
spec:
  selector:
    matchLabels:
      app: springboot-configuration-playground
  template:
    metadata:
      labels:
        app: springboot-configuration-playground
    spec:
      containers:
      - name: springboot-configuration-playground
        image: piomin/springboot-configuration-playground
        ports:
        - containerPort: 8080
        volumeMounts:
          - mountPath: /config
            name: app-vol
      volumes:
        - name: app-vol
          configMap:
            name: springboot-configuration-playground-ext

Here’s the result of our test after reloading the app with the latest ConfigMap. The application.yml file located outside the packaged jar overrides the same file from the classpath.

spring-boot-configuration-external

Another interesting way of passing config to the Spring Boot app is through the inline JSON property. When your application starts, any spring.application.json or SPRING_APPLICATION_JSON properties will be parsed and added to the Environment. This method will override values set by all the previously described load levels. To test it on Kubernetes let’s modify our ConfigMap with environment variables and then reload the app. Now the output in logs should be json.

1
2
3
4
5
6
7
apiVersion: v1
kind: ConfigMap
metadata:
  name: springboot-configuration-playground
data:
  PROPERTY_DEFAULT: env
  SPRING_APPLICATION_JSON: '{"property":{"default":"json"}}'

Setting cloud platform-specific configuration

Let’s consider another interesting feature related to cloud platforms. Do you think it is possible to load only a part of the configuration depending on the cloud environment? Yes! Spring Boot is able to detect if we run our app e.g. on Kubernetes. The only thing we need to do is to mark that part of the configuration properly. There is a dedicated property spring.config.activate.on-cloud-platform. We need to set there the name of a target platform. In our case it is Kubernetes, but Spring Boot supports for example Heroku or Azure also. Let’s add another property property.activation in our application.yml. That property is enabled only if we run our app on Kubernetes:

1
2
3
4
5
6
7
property.default: app
---
spring:
  config:
    activate:
      on-cloud-platform: "kubernetes"
property.activation: "I'm on Kubernetes!"

Then we need to update a part of the app’s main class responsible for printing properties in the logs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Value("${property.default}")
String propertyDefault;
@Value("${property.activation:none}")
String propertyActivation;

@PostConstruct
public void printInfo() {
   LOG.info("========= Property (default): {}", propertyDefault);
   LOG.info("========= Property (activation): {}", propertyActivation);
}

If you use Skaffold you can easily run the sample app on Kubernetes with the skaffold dev command. Otherwise, just deploy using the image in my Docker Hub repository: piomin/springboot-configuration-playground. Here’s the result:

spring-boot-configuration-cloud|696x118

If we run the same app locally with the mvn spring-boot:run command it will print the default value none.

|635x144

Testing configuration with Spring Boot Test

Configuration properties may have a huge impact on the app. For example, we may load different beans depending on configuration properties with @Conditional annotations. Therefore, we should test it carefully. Let’s say we have the following @Configuration class containing the MyBean2 and MyBean3 beans:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class MyConfiguration {

   @Bean
   @ConditionalOnProperty("myBean2.enabled")
   public MyBean2 myBean2() {
      return new MyBean2();
   }

   @Bean
   @ConditionalOnJava(range = ConditionalOnJava.Range.EQUAL_OR_NEWER, 
                      value = JavaVersion.EIGHTEEN)
      public MyBean3 myBean3() {
      return new MyBean3();
   }

}

This bean is registered in context only if we define the myBean2.enabled property. On the other hand, there is the @Configuration class that overrides the bean visible above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class MyConfigurationOverride {

   @Bean
   public MyBean2 myBean2() {
      MyBean2 b = new MyBean2();
      b.setMe("I'm MyBean2 overridden");
      return b;
   }

}

How to test it with JUnit tests? Fortunately, there is a ApplicationContextRunner class that allows us to easily test bean loading with various configuration options. First of all, we need to pass both these classes as @Configuration classes in the test. By default, since version 2.1 Spring Boot doesn’t allow override beans. We need to activate it directly with the property spring.main.allow-bean-definition-overriding=true. Finally, we have to set values of test properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Test
public void testOrder() {
   final ApplicationContextRunner contextRunner = 
      new ApplicationContextRunner();
   contextRunner
         .withAllowBeanDefinitionOverriding(true)
         .withUserConfiguration(MyConfiguration.class, 
                                MyConfigurationOverride.class)
         .withPropertyValues("myBean2.enabled")
         .run(context -> {
            MyBean2 myBean2 = context.getBean(MyBean2.class);
            Assertions.assertEquals("I'm MyBean2 overridden", myBean2.me());
         });
}

Then let’s verify we use the right version of Java for the MyBean3 bean. It should be at least 18. What happens if I’m using an earlier version of Java?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Test
public void testMyBean3() {
   final ApplicationContextRunner contextRunner = 
      new ApplicationContextRunner();
   Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> {
      contextRunner
            .withUserConfiguration(MyConfiguration.class)
            .run(context -> {
               MyBean3 myBean3 = context.getBean(MyBean3.class);
               Assertions.assertEquals("I'm MyBean3", myBean3.me());
            });
   });
}

Final Thoughts

Spring Boot is a very powerful framework. It provides a lot of configuration options you can use on cloud platforms efficiently. In this article, you may learn how to use them properly e.g. on Kubernetes. You can also see how to use different levels of loading properties and how to switch between them.

Reference https://piotrminkowski.com/2022/08/02/a-deep-dive-into-spring-boot-configuration/