
Spring Boot possède une fonctionnalité d’externalisation de la configuration extrêmement riche permettant de configurer les applications à partir des sources très variées (fichiers dans le classpaths, fichiers externes, variables d’environnement, propriétés systèmes, arguments de la ligne de commande, Servlet context …). La multiplicité des sources peut parfois entraîner de la confusion et aboutir à une mauvaise configuration de l’application. D’autant plus que la propriété mal configurée peut être difficile à identifier si elle empêche l’application de démarrer et d’afficher suffisament de logs pour diagnostiquer le problème.
Ce billet présente une solution à ce problème : l’affichage précoce des propriétés utilisées dans l’application avec la valeur réellement prise en compte par Spring Boot et l’origine de cette valeur.
Comment connaître les propriétés disponibles dans une application Spring avec les PropertySource ?
Link to heading
Spring utilise des objets PropertySource
pour représenter les propriétés issues de différents fichiers ou origines.
C’est grâce au sous-type EnumerablePropertySource
de PropertySource qu’il est possible de connaître les propriétés rendues disponibles par une source de propriétés :
avec la méthode EnumerablePropertySource#getPropertyNames. La plupart des sources de propriétés (fichiers, propriétés systèmes,
arguments de la ligne de commande …) sont du type EnumerablePropertySource et permettent donc de connaître les propriétés
qu’elles apportent au contexte de l’application.
Si l’application utilise un type de PropertySource qui n’est pas énumérable, il ne sera pas possible de connaître à l’avance
les propriétés apportées par la source en question : c’est le cas de JndiPropertySource. Ce type de PropertySource devrait
néanmoins rester minoritaire.
Pour connaître les objets PropertySource disponibles dans un contexte applicatif Spring, il faut se référer à l’objet ConfigurableEnvironment.
L’interface ConfigurableEnvironment est un sous-type particulier de Environment dont une instance peut être récupérée via
ApplicationContext#getEnvironment dès lors que l’application est initialisée
ou bien de manière bien plus précoce via ApplicationEnvironmentPreparedEvent dans le cas d’une application Spring Boot
En pratique, la quasi-totalité des environnements manipulés dans les applications Spring sont du type ConfigurableEnvironment
donc l’instance d’Environment ainsi récupérée peut être castée en ConfigurableEnvironment et les sources de propriétés disponibles
accédées via la méthode ConfigurableEnvironment#getPropertySources. Cette méthode retourne un objet de type MutablePropertySources
qui est entre autres un Iterable<PropertySource<?>>, ce qui permet de lister les différentes sources de propriétés puis les propriétés.
On peut résumer les éléments présentés dans cette section avec le bloc de code suivant :
public Set<String> findAllPropertyNames(ApplicationContext applicationContext) {
return ((ConfigurableEnvironment) applicationContext.getEnvironment()).getPropertySources().stream()
.filter(EnumerablePropertySource.class::isInstance)
.map(EnumerablePropertySource.class::cast)
.map(EnumerablePropertySource::getPropertyNames)
.flatMap(Arrays::stream)
.collect(Collectors.toSet());
}
Comment connaître l’origine de la valeur d’une propriété avec API Origin ?
Link to heading
Si une même clé est présente dans plusieurs sources de propriété à la fois, la propriété en question (c’est à dire la valeur
qui sera associée par Spring à la clé en question dans l’application) sera définie en suivant des règles de surcharge
bien précises documentées dans la section “Externalized Configuration” de la documentation Spring Boot
Bien que ces règles soient déterministes et pertinentes, dans certains environnements, la multiplicité des sources peut
rendre délicate la détermination de la source qui est à l’origine de la valeur associée par Spring à la clé. Pour pallier cela,
il est possible de déterminer l’origine de la valeur d’une propriété grâce à l’API Origin
de Spring Boot.
L’origine d’une propriété se représente dans Spring Boot à travers une instance de l’interface org.springframework.boot.origin.Origin
et est susceptible de fournir une information sur l’origine de la valeur retenue par Spring Boot pour la propriété à l’issue
de l’application des règles de surcharge. Cette information s’obtient par exemple par un appel à Origin#toString et peut
par exemple donner une sortie de la forme :
class path resource [application.properties] - 2:29
Une telle instance d’Origin n’est pas accessible directement mais peut se retrouver depuis une instance de ConfigurationProperty
La classe ConfigurationProperty consiste en une représentation par Spring Boot d’une propriété issue d’une source de configuration
externe. Les instances de ConfigurationProperty peuvent être obtenues à l’aide de la méthode ConfigurationPropertySource#getConfigurationProperty
L’interface ConfigurationPropertySource est quant à elle le pendant côté Spring Boot de la classe PropertySource de Spring (c’est une source
pour un ensemble de ConfigurationProperty). On peut en obtenir une instance :
- soit à partir d’une instance de
PropertySourcepar le biais de la méthodeConfigurationPropertySource#from - soit à partir de la classe utilitaire
ConfigurationPropertySources(plusieurs méthodes statiques possibles)
La méthode ConfigurationPropertySource#getConfigurationProperty vue plus haut prend un argument un objet ConfigurationPropertyName
et non une simple String pour désigner la clé de la propriété : afin d’obtenir une instance d’un tel objet, on utilisera
la factory statique ConfigurationPropertyName#of avec le nom (la clé) de la propriété en argument.
On peut résumer les éléments présentés dans cette section avec le bloc de code suivant :
public Optional<String> findPropertyOrigin(String key, ApplicationContext applicationContext) {
ConfigurationPropertyName configurationPropertyName = ConfigurationPropertyName.of(key);
return StreamSupport.stream(ConfigurationPropertySources.get(applicationContext.getEnvironment()).spliterator(), false)
.map(configurationPropertySource -> configurationPropertySource.getConfigurationProperty(configurationPropertyName))
.filter(Objects::nonNull)
.map(ConfigurationProperty::getOrigin)
.filter(Objects::nonNull)
.map(Origin::toString)
.findFirst();
}
Pour éclairer la gestion des propriétés côté Spring Boot :
Voici une correspondance entre Spring core et Spring Boot pour la représentation des propriétés et de leurs sources :
| Spring core | Spring Boot | |
|---|---|---|
| Nom (clé) de la propriété | String | ConfigurationPropertyName |
| Paire clé valeur | Variable suivant les cas (par exemple Map.Entry) | ConfigurationProperty |
| Source de propriétés | PropertySource | ConfigurationPropertySource |
| Ensemble de sources de propriétés | PropertySources (en réalité MutablePropertySources) | SpringConfigurationPropertySources |
NB : la classe
SpringConfigurationPropertySourcesn’est pas publique et l’accès aux différentesConfigurationPropertySourced’un environnement se fait grâce à la classe utilitaireConfigurationPropertySources
Comment afficher les propriétés au plus tôt dans le cycle de vie de l’application ? Link to heading
Lorsqu’une application Spring Boot démarre, la plupart des propriétés sont résolues très tôt dans le cycle de vie de l’application ;
la documentation sur les listeners spring boot en apporte l’illustration en décrivant le cycle de vie de l’application :
notamment à l’issue de la création de l’Environment, lorsque l’évènement ApplicationEnvironmentPreparedEvent
est émis. À partir de ce moment, la plupart des sources de propriétés ont été inspectées par Spring Boot et les valeurs des
propriétés résolues : il est donc possible dès ce moment de retrouver les valeurs réellement utilisées pour les propriétés
ainsi que leurs origines.
Les exemples de code présentés jusqu’à présent n’utilisent pas cette possibilité d’accéder aux propriétés avant la création du contexte
Spring de l’application puisqu’ils s’appuient sur l’objet ApplicationContext. Cette observation ne se limite pas à ces seuls exemples :
des applications sont parfois concernées par un affichage tardif des propriétés, par exemple au cours de l’instanciation ou de
l’appel d’une méthode @PostConstruct d’un bean donné. Si une propriété mal valorisée fait échouer l’instanciation des
beans dans le contexte, il se peut que le bean chargé d’afficher les propriétés ne soit pas encore instancié. Ainsi l’application
s’arrête sans avoir d’indication dans la log sur les valeurs retenues pour ses propriétés.
L’évènement ApplicationEnvironmentPreparedEvent émis par Spring Boot permet de récupérer l’objet Environment juste après sa
création et avant que Spring n’entame d’autres phases du démarrage de l’application. La récupération de l’instance d’Environment
se fait à travers la définition d’un objet ApplicationListener<ApplicationEnvironmentPreparedEvent> et son enregistrement en tant
que listener auprès du contexte applicatif.
Associé aux autres exemples présentés plus hauts, voici ce que donnerait par exemple la classe principale d’une application qui affiche tôt lors de son démarrage ses propriétés et ses origines :
package poc;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.origin.Origin;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.StreamSupport;
@SpringBootApplication
public class Main {
void main(){
new SpringApplicationBuilder(Main.class)
.listeners(new EarlyPropertiesDisplayer())
.build().run();
}
static class EarlyPropertiesDisplayer implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
Environment environment;
List<ConfigurationPropertySource> configurationPropertySources;
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
this.environment = event.getEnvironment();
this.configurationPropertySources = StreamSupport.stream(ConfigurationPropertySources.get(environment).spliterator(), false)
.toList();
((ConfigurableEnvironment) environment).getPropertySources().stream()
.filter(EnumerablePropertySource.class::isInstance)
.map(EnumerablePropertySource.class::cast)
.map(EnumerablePropertySource::getPropertyNames)
.flatMap(Arrays::stream)
.distinct()
.map(this::toDislpayedLine)
.forEach(IO::println);
}
private String toDislpayedLine(String key) {
return key + " = " + this.environment.getProperty(key) + findPropertyOrigin(key).map(" ### FROM "::concat).orElse("");
}
private Optional<String> findPropertyOrigin(String key) {
ConfigurationPropertyName configurationPropertyName = ConfigurationPropertyName.of(key);
return this.configurationPropertySources.stream()
.map(configurationPropertySource -> configurationPropertySource.getConfigurationProperty(configurationPropertyName))
.filter(Objects::nonNull)
.map(ConfigurationProperty::getOrigin)
.filter(Objects::nonNull)
.map(Origin::toString)
.findFirst();
}
}
}
Utiliser une librairie tierce pour afficher systématiquement le contenu et l’origine des propriétés dans une application Spring Boot Link to heading
En intégrant le code ci-dessus dans une application Spring Boot, on bénéficiera de l’affichage précoce des propriétés avec leurs valeurs et leurs origines. Cependant, il restera à gérer les cas d’erreurs, contrôler l’affichage qui peut se révéler trop verbeux en limitant les propriétés qui seront affichées ou les sources utilisées pour récupérer les clés des propriétés ou encore masquer les secrets. En outre, l’intégration du code au moyen d’un copier-coller est une mauvaise pratique et il est préférable d’adjoindre le processus d’affichage des propriétés à l’application existante sans en modifier le code, par exemple en ajoutant une dépendance dans le classpath. C’est ce que propose la librairie Spring-Boot-Properties-Logger qui rend tous les services ci-dessus simplement en ajoutant son artifact maven dans les dépendances de l’application et en configurant quelques propriétés pour la paramétrer :
<dependency>
<groupId>io.github.fbibonne</groupId>
<artifactId>boot-properties-logger-starter</artifactId>
<version>2.2.0</version>
</dependency>
# Affichage des propriétés Spring ainsi que les propriétés du domaine com.example sur la base des préfixes des clés
properties.logger.prefix-for-properties=debug, trace, info, logging, spring, server, management, springdoc, properties, com.example
La librairie Spring-Boot-Properties-Logger gère l’affichage des propriétés, de leurs valeurs et de leur origine mais aussi des sources de propriétés détectées. Les source de propriétés détectées ainsi que les propriétés dont les secrets sont à masquer sont aussi paramétrables.
Afin de pouvoir s’enregistrer comme listener auprès du contexte applicatif sans modifier le code applicatif, la librairie
Spring-Boot-Properties-Logger utilise le mécanisme des fichiers spring.factories
dans le dossier META-INF de son jar : c’est par ce truchement que le listener fourni par la librairie pour l’évènement
ApplicationEnvironmentPreparedEvent est passé au contexte applicatif.