SpringBoot 2.4.x Configuration

In their new release, Spring decided to change the logic behind loading configuration files.

To represent elements that may contribute to the environment, a ConfigDataEnvironmentContributor is introduced, each contributor would be replaced during the process.

Spring decided to use a tree as a data structure in order to process/apply the configuration data.

I will refer to ConfigDataEnvironmentContributor as CDEContributor.

Contributors tree

Each contributor can imports some properties (more on that later), and it will be appended as children, thus creating a whole tree.

Each contributor will contain some metadata describing the node:

1. location: a configDataLocation that will be resolved using a configDataLocationResolver to one or more ressources.

2. ressource: each propertySource will be wrapped inside a ConfigData object, ConfigResource is indicating from which ConfigData can be loaded.

3. propertySource: the propertySource linked to this node.

4. children: children contributors, as said before, when a contributor is importing some properties a new contributors will be added as children of this contributor. children are created for each importPhase.

5.kind: there are various kind of contributors:

ROOT: the root of our tree

INITIAL_IMPORT, this contributor is added at the beginning, it indicates that. the contributor is active and should be processed

EXISTING Contributors obtained by wraping an existing propertySource these type of propertySources will not be processed

UNBOUND_IMPORT a contributor imported but not yet bound

BOUND_IMPORT a contributor imported from another contributor

EMPTY_LOCATION a valid location that contained nothing to load.

In addition to that, children contributors will be separated into two groups of children BEFORE_PROFILE_ACTIVATION and AFTER_PROFILE_ACTIVATION (think of it like left and right), this information would be used to set priority while traversing the tree i.e look first in the AFTER_PROFILE_ACTIVATION realm before going through the other one (or go first right then left in this example).

Before starting to explain how spring load configuration I will recall some concepts.

ApplicationListener

During SpringBoot process, some events will be sent to registered listeners in order to inform them about the progress of the application. These listeners will act regarding the type of received events.

Application events are sent in the following order, as your application runs:

  1. An ApplicationStartingEvent is sent at the start of a run but before any processing, except for the registration of listeners and initializers.
  2. An ApplicationEnvironmentPreparedEvent is sent when the Environment to be used in the context is known but before the context is created.
  3. An ApplicationContextInitializedEvent is sent when the ApplicationContext is prepared and ApplicationContextInitializers have been called but before any bean definitions are loaded.
  4. An ApplicationPreparedEvent is sent just before the refresh is started but after bean definitions have been loaded.
  5. An ApplicationStartedEvent is sent after the context has been refreshed but before any application and command-line runners have been called.
  6. An AvailabilityChangeEvent is sent right after with LivenessState.CORRECT to indicate that the application is considered as live.
  7. An ApplicationReadyEvent is sent after any application and command-line runners have been called.
  8. An AvailabilityChangeEvent is sent right after with ReadinessState.ACCEPTING_TRAFFIC to indicate that the application is ready to service requests.
  9. An ApplicationFailedEvent is sent if there is an exception on startup.

Profiles

Profiles represent conditional configuration, which can be used to configure different beans in different environments.

MutablePropertySources

All external resource configurations are added to a MutablePropertySources This object encapsulates the collection of attribute resources .

Every time we need to obtain a variable configuration, we would hit the MutablePropertySources managed by spring, using for example env.getPropertySources, we can then add (depending on the order), modify or delete a configuration.

Let’s introduce some new relevant classes added by spring

ConfigData

Configuration data that has been loaded from a ConfigDataResource and may contribute property sources to Spring's Environment

ConfigDataProperties

When loading configData it may contain some crucial informations, for example you want to activate a profile spring.profiles.active or to restrict the loading on a specific profile spring.config.activate.on-profile

ConfigDataLoaders

A collection of ConfigDataLoaders instances loaded via
spring.factories

ConfigDataActivationContext

Context information used when determining when ConfigData is activated.

ConfigDataLoaderContext

Context provided to ConfigDataLoader methods

ConfigDataLocationResolvers

A collection of ConfigDataLocationResolver instances loaded via
spring.factories

ConfigDataLocationResolverContext

Context provided to ConfigDataLocationResolver methods

ConfigDataEnvironment

Wrapper around a ConfigurableEnvironment that can be used to import and apply ConfigData

ConfigDataEnvironmentPostProcessor

ConfigFileApplicationListener is no longer used during the loading of the configurations, spring repalced it with EnvironmentPostProcessorApplication Listener

replaced by

You can still use it but it’s deprecated and very soon will removed.

If you want to still use older version, you can set spring.config.use-leggacy-processing to true to your application.yml/properties file.

When the environment gets created, all the registered listeners (from spring.factories) gets notified by sending a ApplicationEnvironmentPrepared Event, in this section we will concentrate on EnvironmentPostProcessor ApplicationListener.

EnvironmentPostProcessorApplicationListener#onApplicationEnvironmentPreparedEvent

EnvironmentPostProcessor is used to customize the application’s Environment before application context is refreshed.

Registered postProcessors gets informed by invoking postProcessEnvironment signal, in this part we will concentrate on ConfigDataEnvironment PostProcessor.

from spring.factories

ConfigDataEnvironmentPostProcessor#postProcessEnvironment

ConfigDataEnvironment get instantiated and processAndApply is invoked (process contributors then apply to environment imported propertySources).

Initial set of ConfigDataEnvironmentContributors gets configured by wrapping property sources from Spring and initial set of locations are added (see createContributors section below ).

The initial locations can be influenced via the LOCATION_PROPERTY, ADDITIONAL_LOCATION_PROPERTY, IMPORT_PROPERTY properties. If no explicit properties are set, the DEFAULT_SEARCH_LOCATIONS will be used.

As you may have observed

If legacy property has been set, an exception will get thrown and we will switch to older version.

ConfigDataEnvironment#createContributors

Configures the initial set of ConfigDataEnvironmentContributors by wrapping property sources from Spring, we first iterate over the propertySources from the current environment and create a correponding contributors with EXISTING kind.

ConfigDataEnvironment#getInitialImportContributors

Using the provided binder, we will check the environment if the location property has been set (same for the importProperty and AdditionalLocationProperty), if not use the default location and create an appropriate contributor.

Next relevants methods

For each Location, the factory method ofInitialImport will be used to instantiate new ConfigDataEnvironmentContributor with INITIAL_IMPORT as kind.

Now that ConfigurationDataEnvironment has been instantiated, we are ready to process the intial contributors and apply necessary changes to current environment.

ConfigurationDataEnvironment#processAndApply

As discussed we first process the active contributors and then apply to environment (process then apply).

ConfigurationDataEnvironment#processInitial

During this phase, we will process initial import contributors without activation context, for example, in this part bootstrap.yml or application.yml get’s imported.

ConfigDataEnvironmentContributors#withProcessedImports

This method will process active contributors and return a new ConfigDataEnvironmentContributors instance (a new tree if you want).

Given a contributor, using getImports

we obtain the list of ConfigDataLocation and feed that to the provided ConfigDataImporter and invoke resolveAndLoad method (more details on that later) in order to resolve propertySources from the provided locations and get them wrapped inside a ConfigData object.

An UNBOUND_IMPORT contributor gets created and appended as children to the current contributor.

The importPhase is very important, it will help us to decide which children should contribute to the environment first.

Next when the UNBOUND_IMPORT contributor is observed, we will replace it with a new instance but with bound properties (more on that later) and continue to next contributor if any.

ConfigDataImporter#resolveAndLoad

First we check if there is any active profile using the provided activationContext, resolve gets invoked.

ConfigDataImporter#resolve(WithLocations)

For each of the provided locations, we try to load the configData.

ConfigDataImporter#resolve(WithLocation)

For each of the provided resolvers we try to resolve the location.

registred resolvers

ConfigDataResolver#resolve(WithResolver)

we wrap the resolve signal inside a resolvable action, and if there is an activated profile, we try also to load it and merge the results.

Something to point out is that when the resolver gets created

We check if explicit property for spring.config.name is set, otherwise we use the default application name.

ConfigDataResolver#resolve(WithResolveAction)

For each ressource resolved we wrap it inside a ConfigDataResolutionResult, it will encapsulate the location, ressource, classloader ect.. and something very important to note is that it will contain the PropertySourceLoader (by looking at the extension of the resolved file) in order to know directly which loader to use later.

Back to resolveAndLoad method:

Now that we have in hand a ConfigDataResolutionResult we can start loading.

By checking the type of the ressource, an appropriate ConfigDataLoader will be used in order to load the configuration file (please have a look at this article: https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config-files-configtree), in our case, a StandardConfigDataLoader will be used.

Let’s have a look at PropertiesPropertySourceLoader as an example:

Next relevant method

Start reading the configuration file, think of each line putting an entry in a Map. A reader is used for tracking purposes

Load key and corresponding value

Now that ConfigData have been loaded, let’s come back to withProcessedImports

When configuration data are loaded, a new contributor gets instantiated and appended as children with UNBOUND_IMPORT to it’s parent (the one that imported it), then we have to bound it, Why ?

First because the document resolved may import other configuration files (https://spring.io/blog/2020/08/14/config-file-processing-in-spring-boot-2-4), the configuration file will be resolved and loaded on the next iteration after being bound.

Another case is when using multi-document YAML file, some properties are restricted to be activated only on the activation of some profile, we have to bind these values and decide whether to include it in the environment later.

When binding spring.config property name, we will bind it to ConfigDataProperties class, a list of imports is parsed, an annotations is used in order to accept the key as spring.config.import

In Spring Boot 2.3, you’d use the spring.profiles key to do include some configuration. With Spring Boot 2.4, they changed the property to spring.config.activate.on-profile/on-cloud-plateform.

Another thing to point out is that you can not use spring.config.activate.on-profile and spring.profiles.active in one document, this would throw an exception

ConfigDataEnvironmentContributors#processWithoutProfiles

Next we process contributors with initial ActiveContext, profile will not be resolved at that moment, cloudPlateform will be detected during this phase, and only appropriate contributors will then be activated.

We will see later how to traverse our tree of contributors

For example, you can have this type of configuration, and the second document will contribute only if Kubernetes platform is detected in the current environment.

ConfigDataEnvironmentContributors#withProfiles

In this part we update the activationContext by checking the contributors’s properties and include specified profiles.

ConfigDataEnvironmentContributors#processWithProfiles

Now we load the configuration payloads using the provided profile, this is when application-profile.yml/properties are loaded and added as a children contributors to the one that imported them.

The same process will happen, the only difference is other files might be included because this time we will try to reslove profileSpecificRessources.

ConfigDataEnvironmentContributors#applyToEnvironment

Now we walk through the contributors tree and add the propertySources to the environment by respecting the priorities (we will se later how we do that).

Now the next question that comes to our mind is, how priority is respected and how contributor’s tree is traversed ?

This leads us to:

ConfigDataEnvironmentContributor#ContributorIterator

Each contributor will set a ContributorIterator in order to recursively explores the set of contributors (the tree).

As said before, spring use the import phase information in order to process first the propertySources loaded during the activation profile phase

Once current contributor set, we recursively fetch it until no more contributor is available in this case, using the children iterator, we will ask for another element, if no element is available, we switch the phase (from ACTIVE_PROFILE_ACTIVATION to BEFORE_PROFILE_ACTIVATION), if still not, we return this contributor as next.

Algorithm

In order to understand how we traverse the tree, let’s take an example

The first step is to create an iterator for the root contributor

The initial phase will be the AFTER_PROFILE_ACTIVATION, in this case we move right and obtain the children contributors iterator (a list containing the contributors corresponding to this phase), in this case the list is empty on that level, so we move to BEFORE_PROFILE_ACTIVATION phase.

1. First check if next element has been selectioned if yes return it

Is this.next!=null ?: the answer is no so go to second test

2. Has current contributor iterator been set ? if yes fetch it and return it's next element

Is this.current.hasNext(): no, so move to third test

3. if no current contributor has been set or all have been exhausted ask children iterator if there is any/more contributor to process if yes set current contributor iterator and fetch

Is this.children.hasNext() ? true, in this case contributor1 will be selected as current one, we invoke again fetchIfNecessary to see if we can go deeper (if this contributor has any children or we can directly return it)

First test will still fail but not the second one, because the current iterator now is not empty (we set it before), we ask for the next element to process using contributor1 iterator (it would be either contributor1 or one of its children), AFTER_PROFILE_ACTIVATION children will first be fetched and in this case it will return contributor1_2. This contributor has no AFTER_PROFILE_ACTIVATION children so we try also switching the phase to BEFORE_PROFILE_ACTIVATION and still nothing, in this case we will return it as next element. Following the same process contributor1_1 will be fetched and returned .

Here is a recall on leaf contributors returned in order:

contributor1_2

contributor1_1

In this example contributor 1_2 may import application-dev.yml and contributor1_1 application.yml.

You can find below a diagram that resumes the complete flow

Conclusion

In this post, we explored some interesting changes in config file processing, introduced by Spring since their new SpringBoot release (2.4.0).

In the next post, I will discuss Spring Cloud Config server.

References

https://github.com/spring-projects/spring-boot

https://docs.spring.io

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store