A Deep Dive Into 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
.
property.default: app
We have another configuration file in the classpath additional.yml
. It contains the same property, but with a different value.
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.
@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.
$ export PROPERTY_DEFAULT=env
Finally, we can run the app. The following fragment of code LOG.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 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.
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
.
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:
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.
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.
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.
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
.
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:
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:
@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:
If we run the same app locally with the mvn spring-boot:run
command it will print the default value none
.
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:
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:
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.
@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?
@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.
2 COMMENTS