Managing Spring Boot Properties After Version 2.4
Managing Spring Boot properties in multi-module application could be quite intimidating. In this post, we take a look how Spring Boot will resolve properties conflicts and how to make our build more consistent. Changes introduced in version 2.4 are going to help us. Let's review how to take advantage of new features and incorporate them into the project.
To better understand what version 2.4 brings we need to take a step back and remind ourselves how things used to work before 2.4 release.
How Spring Boot deals with property overrides?
Let's take this basic example. We could try to define the same value twice in a single file. Like this:
main:
value: option-A
value: option-B
But this will fail, and the application will crash at the boot. You can't override properties in the same file. But it's more than fine to just create another property file (with a different format):
# application.yaml
main:
value: option-A
# application.properties
main.value=option-B
In this case, the system will pick up option-B
since values from .properties
files have higher priority over values from
.yaml
files. The full hierarchy of values looks like this: (starting from lowest to highest priorities)
- Default value defined in code
@ConfigurationProperties(prefix = "main")
class MyConfigurationProperties {
var value: String = "CODE_DEFAULT"
}
- Property defined at default
.yaml
file - Property defined at default
.properties
file - Property defined at profile-specific
.yaml
file - Property defined at profile-specific
.properties
file - Environment variable
- JVM option
Properties in multi-module system
With that out of the way let's step up a notch and see how we could manage properties in a multi-module application.
To work on something real, let's assume our app contains 3 modules: main one, moduleA
and moduleB
.
The tree structure of that project would look like this:
├── src
│ ├── main
│ │ ├── resources
│ │ │ ├── application.yaml
├── moduleA
│ ├── src
│ │ ├── main
│ │ │ ├── resources
│ │ │ │ ├── application.yaml
├── moduleB
│ ├── src
│ │ ├── main
│ │ │ ├── resources
│ │ │ │ ├── application.yaml
Now if we define the same property in all three files which version is going to be picked up? Well... we could predict that is going to be the one
from the main module and that would be correct. What if we remove the main application.yaml
file? Then Spring Boot will pick up the value from one of the two
remaining files. Which one? It's non-deterministic. Let's avoid situations like this altogether. Any ambiguity is
a potential risk hazard. And this is one is especially harmful since we might find out about our mistake when the app goes into production.
One easy way out would be to define all properties in a single application.yaml
. Not even creating dedicated yaml files per module. But we have those modules
for a reason, most likely our intention was to keep all the logic encapsulated. So why we should spread module-specific properties outside?
It makes no sense.
Before Spring Boot 2.4, we could have handled this problem by using spring.profiles.include
. However, after 2.4 release this feature is no longer present.
Reasons for the deprecation are described in detail in the blog post.
The short version is - this feature yield non-deterministic results. With that option out of the way, what's the alternative?
When one door closes, another opens.
What could be done after Spring Boot 2.4
Spring Boot 2.4 introduced spring.config.import
which is much more predictable. The idea of that property is simple: we just point to the other sources that
we would like to use in the final config file. Let's see how we could use this new feature in the multi-module application.
# src/main/resources/application.yaml
spring:
config:
import:
- moduleA.yaml
- moduleB.yaml
In the main application.yaml
file we are loading additional configs files specific to the modules present in the app. Let's create those files. With those changes our source
tree should look like this:
├── src
│ ├── main
│ │ ├── resources
│ │ │ ├── application.yaml
├── moduleA
│ ├── src
│ │ ├── main
│ │ │ ├── resources
│ │ │ │ ├── moduleA.yaml
├── moduleB
│ ├── src
│ │ ├── main
│ │ │ ├── resources
│ │ │ │ ├── moduleB.yaml
In the module dedicated files: moduleA.yaml
and moduleB.yaml
we can now place configuration specific to the module and that module alone.
# moduleA/src/main/resources/moduleA.yaml
module-a:
value: some value
Even if we are using moduleA.yaml
file only for the module-specific/ custom properties it's still possible to override global properties inside that file. For instance, if we
set app port server.port=3000
inside moduleA.yaml
that value might override the property from the main application.yaml
. That could be confusing and breaks the original
intention of profile-specific configuration. To make things neat and tidy let's prefix all properties with a custom namespace unique only to that module. For
instance, all properties inside moduleA
have to start with module-a
.
To sum it up:
Keep all the generic configurations inside the main application.yaml
and only module-specific properties inside the module-specific files, only under module unique namespace.
How does it work with custom profiles?
They still work as expected! Meaning, if we create moduleA-prod.yaml
file alongside moduleA.yaml
values from that file are going to be used to override properties from
the base file. Neat! No additional changes are required to make it work!
How does it work with integration tests?
If the test configuration is simple enough it's fine to just place it inside module-specific application-test.yaml
, for instance:
# moduleA/src/test/resources/application-test.yaml
module-a:
value: some test value
module-b:
value: test value for the other module
If the configuration is more extensive and it doesn't feel right to have it copy-pasted all over every test configuration file, plus the
configuration doesn't have to change from module to module, we can apply the same trick as before. By importing shared moduleB
configuration file inside application-test.yaml
:
# moduleA/src/test/resources/application-test.yaml
spring:
config:
import:
- moduleB.yaml
module-a:
value: some test value
# moduleB/src/main/resources/moduleB-test.yaml
module-b:
value: shared testing property
Please notice that moduleB-test.yaml
is inside main
not test
directory!