2020-08-15

Config file processing in Spring Boot 2.4


Spring Boot 2.4.0.M2 has been released, and it brings with it some interesting changes to the way that application.properties and application.yml files are loaded.

If your applications use typical setup of only using application.properties or application.yml file, you might want to read on to learn about what changed and why.

Why These Changes

One thing that we wanted to add in Spring Boot 2.3, but couldn’t, was support for volume mounted configuration.

Volume mounted configuration is a popular feature of Kubernetes where a ConfigMap directive is used to surface configuration directly on the file system. You can either mount a complete YAML file that contains an amalgamation of multiple keys and values, or you can use a simpler directory tree format where the filename is the key and the contents of the file is the value.

Spring offer support for both, and in a way that felt natural to use alongside our existing application.properties and application.yml support. To do that we needed to touch the dreaded ConfigFileApplicationListener class.

The Problem with ConfigFileApplicationListener
A few years ago some amusing clips from the video game Trap Adventure 2 started doing the rounds. They’re a pretty good analogy for what can happen in software. Sometimes you just find yourself with areas of code that are very hard to change. In Spring Boot, ConfigFileApplicationListener ended up becoming one of these "trap adventures".

The two main problems that we have with the code are to do with profile specific documents (mainly in YAML). Namely:

You can enable additional profiles from a profile specific document.

It’s hard to know the order that documents will be added.

Take the following example:

security.user.password: usera
---
spring.profiles: local
security.user.password: userb
runlocal: true
---
spring.profiles: !dev
spring.profiles.include: local
security.user.password: userc
Here we have a multi-document YAML file (a single file that’s comprised of three logical documents separated by ---).

If you run with --spring.profiles.active=prod what’s the value of security.user.password? Is the runlocal property set? Are you sure? Is the middle document even included since the profile isn’t activated at the time it’s processed?

Spring regularly had issues raised about this file processing logic, but whenever Spring tried to fix them something would break for someone else. Spring ultimately decided the only way forward was to rethink the entire thing.

So in Spring Boot 2.4 make two significant changes to the way the properties and YAML files are loaded:

Documents will be loaded in the order that they’re defined.

Profiles can no longer be activated from profile specific documents.

Document Order
Starting with Spring Boot 2.4, there will be a simple rule that can be applied when loading properties and YAML files. Properties declared lower in the file will override those higher up.

This follows the same ordering rules that plain old .properties files already use. Think of each line putting an entry in a Map. Any existing entry is replaced when a new value with the same key is put into the Map.

Following these rules, given a multi-document YAML file, the lower document will override values in the higher one:

test: "value"
---
test: "overridden-value"
Multi-document Properties Files
With Spring Boot 2.4, It's bring YAML-like multi-document support to Java properties files. Multi-document properties files use a comment (#) followed by the familiar three dash notation to split documents.

For example, the properties equivalent of the YAML snippet above would be:

test=value
#---
test=overridden-value
Profile Specific Documents
The above example is a bit artificial since it wouldn’t really make sense to always override a value. A more common setup would be to declare that the second document is only active with a specific profile.

In Spring Boot 2.3, you’d use the spring.profiles key to do this. Spring Boot 2.4 change the property to spring.config.activate.on-profile.

For example, if we only want to override test when the dev profile is active, we can use the following:

test=value
#---
spring.config.activate.on-profile=dev
test=overridden-value
Profile Activation
You can still use spring.profiles.active properties to activate or include profiles from an application.properties or application.yaml file.

For example, the following is perfectly valid:

test=value
spring.profiles.active=local
#---
spring.config.activate.on-profile=dev
test=overridden value
One thing that you’re no longer allowed to do is use that property in combination with spring.config.activate.on-profile. For example, the following file would now throw an exception:

test=value
#---
spring.config.activate.on-profile=dev
spring.profiles.active=local # will fail
test=overridden value
We hope that this new restriction will ultimately make your application.properties and application.yml files easier to reason about and understand. We also hope that it’ll make Spring Boot itself easier to manage and maintain. We are, however, aware of a least one valid use-case where people want to expand a profile into multiple sub-profiles. In order to support that, we’re adding a feature called "profile groups".

Profile Groups
Profile groups are a new feature in Spring Boot 2.4 that allow you to expand a single profile into multiple sub-profiles. For example, suppose you have a complex set of @Configuration classes that you conditionally enable using @Profile annotations. You might have a database configuration with @Profile("proddb"), a message configuration with @Profile("prodmq") and so on.

Using multiple discrete profiles might make your code easier to reason about, but it’s not ideal for deployment. You don’t want to force users to remember that they must always activate proddb, prodmq, prodmetrics etc at the same time. Instead, you just want them to be able to activate a single prod profile. Groups allow you to do just that.

To define a group, you can use the spring.profiles.group property in your application.properties or application.yml file. For example:

spring.profiles.group.prod=proddb,prodmq,prodmetrics
Importing Additional Configuration
Now that we’ve fixed that basic issues with config file processing, we’re finally able to think about new features that we want to offer. The main one that we’re delivering with Spring Boot 2.4 is support for importing additional configuration.

With earlier versions of Spring Boot, it was pretty hard to import additional properties or yaml files beyond application.properties and application.yml. You could use the spring.config.additional-location property, but you needed to set it pretty early and it was quite limited with the types of files that it could deal with.

With the latest milestone, you can use the new spring.config.import property directly in your application.properties or application.yml files. For example, you might want to import a "git ignored" developer.properties file so that any developer in your team can quickly change properties just for them:

application.name=myapp
spring.config.import=developer.properties
You can even combine spring.config.import declarations with spring.config.activate.on-profile properties. For example, here we load prod.properties only if the prod profile is active:

spring.config.activate.on-profile=prod
spring.config.import=prod.properties
Imports can be considered as additional documents inserted just below the document that declares them. They follow the same top-down ordering as regular multi-document files: An import will only be imported once, no matter how many times it is declared.

Volume Mounted Configuration Trees
Import definitions use URL-like syntax as their value. If your location doesn’t have a prefix, it’s considered to be a regular file or folder. If, however, you use the configtree: prefix, you’re telling Spring Boot that you expect a Kubernetes style volume mounted configuration tree at the location.

For example, you can declare the following in your application.properties:

spring.config.import=configtree:/etc/config
If you have the following mounted content:

etc/
 +- config/
     +- my/
     |  +- application
     +- test
You end up with my.application and test properties in your Spring Environment. The value for my.application will be the contents of /etc/config/my/application, and the value of test will be the contents of /etc/config/test.

Cloud Platform Activation

If you only want volume mounted config trees (or any properties for that matter) to be active on a specific cloud platform, you can use the spring.config.activate.on-cloud-platform property. This works in a similar way to the spring.config.activate.on-profile property, but uses CloudPlatform values, rather than profile names.

If we want to only enable our config tree example above when we’re deployed to Kubernetes, we can do the following:

spring.config.activate.on-cloud-platform=kubernetes
spring.config.import=configtree:/etc/config
Supporting Additional Locations
The location strings specified in spring.config.import properties are completely pluggable and may be extended by writing a few custom classes. We expect that third-party libraries may start to offer support for custom locations in the future. For example, you could imagine third-party jars to support locations such as archaius://…​, vault://…​ or zookeeper://…​.

If you’re interested in adding additional location support, look at the javadoc for ConfigDataLocationResolver and ConfigDataLoader in the org.springframework.boot.context.config package.

Using Legacy Processing
If you’re upgrading an existing Spring Boot application, and you don’t feel comfortable using all these new features, you can switch back to the older processor. To do that, you can set spring.config.use-legacy-processing to true to your application.properties or application.yml file. This should give you application config processing that’s identical to a Spring Boot 2.3 application.

No comments:

Post a Comment