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.
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.
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:
ApplicationStartingEventis sent at the start of a run but before any processing, except for the registration of listeners and initializers.
ApplicationEnvironmentPreparedEventis sent when the
Environmentto be used in the context is known but before the context is created.
ApplicationContextInitializedEventis sent when the
ApplicationContextis prepared and ApplicationContextInitializers have been called but before any bean definitions are loaded.
ApplicationPreparedEventis sent just before the refresh is started but after bean definitions have been loaded.
ApplicationStartedEventis sent after the context has been refreshed but before any application and command-line runners have been called.
AvailabilityChangeEventis sent right after with
LivenessState.CORRECTto indicate that the application is considered as live.
ApplicationReadyEventis sent after any application and command-line runners have been called.
AvailabilityChangeEventis sent right after with
ReadinessState.ACCEPTING_TRAFFICto indicate that the application is ready to service requests.
ApplicationFailedEventis sent if there is an exception on startup.
Profiles represent conditional configuration, which can be used to configure different beans in different environments.
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
Configuration data that has been loaded from a ConfigDataResource and may contribute property sources to Spring's Environment
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
A collection of ConfigDataLoaders instances loaded via
Context information used when determining when ConfigData is activated.
Context provided to ConfigDataLoader methods
A collection of ConfigDataLocationResolver instances loaded via
Context provided to ConfigDataLocationResolver methods
Wrapper around a ConfigurableEnvironment that can be used to import and apply ConfigData
ConfigFileApplicationListener is no longer used during the loading of the configurations, spring repalced it with EnvironmentPostProcessorApplication Listener
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.
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.
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.
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.
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.
As discussed we first process the active contributors and then apply to environment (process then apply).
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.
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.
First we check if there is any active profile using the provided activationContext, resolve gets invoked.
For each of the provided locations, we try to load the configData.
For each of the provided resolvers we try to resolve the location.
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.
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
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.
In this part we update the activationContext by checking the contributors’s properties and include specified profiles.
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.
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:
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.
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:
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