1. Ordre des property sources
Si vous avez déjà modifié server.port dans application.yml, lancé le service et vu qu'il démarre obstinément sur un autre port — félicitations : vous avez déjà rencontré le fait que la configuration Spring Boot ne vit pas dans un seul fichier, mais dans plusieurs « couches ». Dans un environnement Docker, cela remonte encore plus vite, car une partie des valeurs vient de « l'extérieur » du conteneur, et le développeur commence à traiter la mauvaise cause : il modifie le fichier, reconstruit l'image, se plaint sur Docker, puis sur Spring, puis sur lui-même (c'est un chemin normal, ne vous en faites pas).
Ce qui nous intéresse ici n'est pas la syntaxe spécifique de -e, -D ou --..., mais le fondement : Spring Boot assemble toujours les valeurs finales à partir de plusieurs sources, et ces sources ont un ordre de priorité. Dès que vous comprenez cet ordre, la configuration cesse d'être un mystère et devient un problème d'ingénierie : « où exactement la clé est-elle définie et quelle source l'a redéfinie ».
Property sources et Environment
Dans Spring Boot, le terme property est simplement une paire clé=valeur. Des exemples familiers : server.port=8080, spring.profiles.active=standalone, app.mode=postgres. Et property source est un emplacement spécifique d'où ces paires peuvent provenir : fichier de configuration, variables d'environnement, arguments de lancement, propriétés système JVM, etc. Ce qui importe ici n'est pas le nombre de sources (il y en a beaucoup), mais la mécanique : Boot les empile dans une même « tirelire », mais pas selon le principe « ce qui a été lu en premier est vrai », mais selon un principe de priorité.
À l'intérieur de l'application, le rôle de « comptable en chef de la configuration » est joué par l'objet Environment. Presque tout Spring (et votre code aussi) finit par lui demander : « quelle est la valeur de la clé X ? ». Et le plus intéressant : Environment répond déjà avec la valeur finale, sans expliquer « qui a gagné exactement ». Il se moque de savoir si la valeur vient d'un fichier, d'une variable d'environnement ou d'un argument de lancement — il renvoie le gagnant.
Voici un extrait minimal de logique qu'il est bon de garder dans n'importe quelle sonde de diagnostic temporaire. Ce qui nous importe ici n'est pas un nouveau endpoint en tant que tel, mais le fait même : la valeur finale est demandée à Environment.
String mode(Environment env) {
return env.getProperty("app.mode", "standalone");
}
Ici, standalone est simplement la valeur par défaut pour notre app.mode pédagogique, si la clé n'est définie nulle part ailleurs. En modifiant le fichier, les variables d'environnement ou les arguments de lancement, vous obtiendrez toujours la réponse d'ici : depuis l'Environment final.
2. Le modèle du « gâteau à étages »
Pour ne pas se noyer dans les détails, il est pratique de se représenter la configuration Spring Boot comme un gâteau à étages (ou un « hareng sous la fourrure », si la métaphore locale vous parle). En bas se trouve la configuration de base des fichiers (par exemple, application.yml à l'intérieur du jar). Par-dessus peuvent se superposer les variables d'environnement du conteneur. Encore au-dessus — les propriétés système JVM. Et tout en haut — les arguments de l'application (--key=value). Lorsqu'une même clé apparaît dans plusieurs couches, c'est la couche supérieure qui gagne. Et ce n'est pas « tout le fichier dans son ensemble » qui gagne, mais la clé spécifique.
Cette idée peut être fixée par un schéma :
flowchart TB
%% En bas — valeurs de base et « valeurs par défaut », en haut — les redéfinitions les plus prioritaires.
A["Code (default dans @Value / getProperty)"] --> B["Fichiers config data (application.yml)"]
B --> C["Variables d'environnement"]
C --> D["Propriétés système JVM (-Dkey=value)"]
D --> E["Arguments de l'application (--key=value)"]
%% Important : le conflit se résout au niveau de la CLÉ, pas au niveau du « fichier entier ».
note["Une même clé peut se trouver dans plusieurs couches. La source la plus haute où elle est définie gagne."]
E --> note
Remarquez la couche la plus basse. Lorsque vous écrivez ${app.mode:standalone} ou getProperty("app.mode", "standalone"), vous définissez une valeur par défaut dans le code, qui n'est utilisée que si la clé ne vient de nulle part. Ce n'est pas une property source au sens habituel, mais pour la réflexion d'un débutant, c'est utile : « même si je n'ai rien passé, l'application n'est pas obligée de planter — elle peut avoir des valeurs par défaut raisonnables ».
4. Échelle simplifiée des priorités
En réalité, Spring Boot a plus de sources que nécessaire pour un workflow quotidien en conteneur, et c'est normal. Mais pour le scénario conteneurisé, il nous suffit de retenir une échelle de travail qui couvre 95% des cas dans la vie quotidienne d'un développeur Java :
| Niveau (faible → fort) | Ce que c'est | Exemple de clé | Ce qu'il contient généralement |
|---|---|---|---|
| Le plus faible | Default dans le code | ${app.mode:standalone} | « Que ça fonctionne tant bien que mal sans configuration » |
| Base | application.yml (config data) | server.port: 8080 | Valeurs « par défaut pour le projet » |
| Plus fort | Variables d'environnement | SERVER_PORT=8081 | Redéfinitions liées à l'environnement (conteneur, machine de dev) |
| Encore plus fort | Propriétés système JVM | -Dserver.port=8082 | Paramètres de lancement JVM/processus |
| Le plus fort (dans notre modèle simplifié) | Arguments de l'application | --server.port=8083 | Arguments explicites de lancement de l'application |
Le but de ce tableau n'est pas de vous forcer à apprendre toutes les formes et nuances. L'objectif est plus simple : si la valeur n'est « pas comme dans application.yml », cela signifie que quelque part plus haut dans l'échelle elle a été redéfinie. Et plus vous êtes proche de Docker, plus les niveaux supérieurs participent à la vie.
Pour ne pas mélanger la priorité avec la mécanique des profils et les modes d'infrastructure, nous conservons une clé pédagogique simple : app.mode. Imaginons une situation très typique. Dans application.yml à l'intérieur du jar, nous avons défini un port par défaut et un mode par défaut :
# Valeurs de base du projet : fonctionnent « prêtes à l'emploi » si rien n'est redéfini au lancement.
server:
# Port par défaut pour le lancement local et pour le conteneur sans configuration supplémentaire.
port: 8080
app:
# Mode par défaut de l'application ; nous le redéfinirons ensuite par-dessus.
mode: standalone
C'est une excellente base de référence : le service démarre localement, dans un conteneur, n'importe où. Mais dès que vous lancez le conteneur avec une redéfinition de port (ou de profil), application.yml cesse d'être la « dernière instance ». Il reste la base, mais pas le gagnant.
5. Redéfinition par clé
L'une des choses les plus trompeuses dans la configuration est la sensation que si « la source X a gagné », alors elle a gagné partout. En réalité, Spring Boot choisit le gagnant pour chaque clé séparément. C'est très pratique : vous pouvez redéfinir exactement une seule valeur sans réécrire tout le fichier. Mais c'est aussi une source de confusion : vous modifiez application.yml en espérant que « tout deviendra comme dans ce fichier », mais seul ce qui n'est pas écrasé par le dessus change.
Étendons une sonde d'exécution temporaire pour afficher deux valeurs à la fois. Ce qui nous importe ici n'est pas un nouveau contrôleur, mais le fait lui-même : on peut voir deux clés simultanément et comprendre immédiatement ce qui a gagné pour chacune.
// Si vous avez déjà un point de diagnostic GET /api/runtime, il est pratique de l'étendre ainsi :
@Value("${server.port}")
private String port;
@Value("${app.mode:standalone}")
private String mode;
@GetMapping("/api/runtime")
String runtime() {
return mode + " @ " + port;
}
Cette méthode peut être placée sur votre point de diagnostic GET /api/runtime. L'idée importante est que nous regardons deux clés et voyons le gagnant pour chacune.
Imaginons maintenant trois lancements (localement ou dans un conteneur — le principe est identique). Je montre volontairement les commandes via java -jar, car dans un conteneur nous démarrons exactement ainsi, même si nous avons construit l'image en multi-stage.
Lancement n°1 : rien n'est redéfini, on prend la base de application.yml.
# Lancement de base : on prend les valeurs de application.yml (et les valeurs par défaut du code si la clé est absente).
java -jar app.jar
Alors l'appel :
GET /api/runtime
renverra approximativement :
standalone @ 8080
Lancement n°2 : on redéfinit uniquement app.mode via un argument de lancement (oui, c'est déjà la « couche supérieure »).
# On redéfinit exactement une clé via les arguments de l'application : elle écrasera le fichier.
java -jar app.jar --app.mode=postgres
Alors :
standalone @ 8080 (ne sera pas)
postgres @ 8080 (sera)
Remarquez : le port n'a pas changé, car nous n'avons pas touché à server.port. Et c'est le modèle normal : la configuration n'est pas « commutée dans son ensemble », elle est assemblée par clés.
Lancement n°3 : on crée un petit conflit : app.mode défini à la fois comme propriété système et comme argument de l'application.
# Conflit intentionnel : la même clé est définie via -D et via --args.
# Dans l'échelle simplifiée des priorités, --app.mode sera « plus haut » et gagnera.
java -Dapp.mode=fromSystem -jar app.jar --app.mode=fromArg
Le résultat sera :
fromArg @ 8080
Et c'est le moment qu'il faut accepter comme règle du jeu : si vous avez défini la même clé à plusieurs endroits, la source la plus prioritaire écrasera la moins prioritaire. Pas de « les deux valeurs seront prises en compte », « fusionnées », « choisies aléatoirement ». Il y aura simplement un gagnant. Et le perdant restera dans l'histoire comme la cause de vos futures questions « pourquoi est-ce ignoré ».
6. Pratique : conflits et débogage
Reconstruction d'image et conflits de configuration
Quand vous êtes dans le monde des conteneurs, la première réaction face à un comportement étrange est souvent : « L'image est probablement obsolète. Il faut la reconstruire ». C'est parfois vrai (si vous avez changé le code), mais pour les conflits de configuration, la reconstruction — c'est comme essayer de soigner la fièvre en changeant de thermomètre : l'action existe, mais a peu de sens. L'ordre des property sources ne change pas avec la reconstruction, et si une valeur est écrasée par le dessus, elle le reste.
Le scénario le plus fréquent ressemble à ceci. À l'intérieur du jar se trouve application.yml, vous y changez server.port: 8080, reconstruisez l'image, lancez le conteneur — mais le port est toujours « pas le bon ». Et commence alors le devinage, alors qu'il suffit de poser une question : « Est-ce que je passe server.port ailleurs ? ». Dans la réalité Docker, c'est facile : ENV dans Dockerfile, -e dans docker run, variables d'environnement de votre IDE ou terminal, arguments de lancement dans ENTRYPOINT/CMD. Tout cela peut écraser le fichier, et la reconstruction n'y est pour rien.
Une discipline utile est donc : si seule la configuration de lancement a changé — on ne reconstruit pas l'image, on cherche la source gagnante ; si le code/l'artéfact a changé — on reconstruit l'image. C'est exactement la frontière que nous voulons fixer dans le module 3.
Débogage : « chercher où la clé est définie »
Quand la configuration « ne correspond pas aux attentes », on a très envie de sauter sur la couche la plus visible — généralement application.yml — et de corriger là. Mais il est plus correct de commencer autrement : choisir une clé spécifique (par exemple, server.port) et mener une petite enquête : où peut-elle être définie ? À cette étape, il est important de ne pas étendre le problème à « toute la configuration est cassée », car elle ne l'est presque jamais en totalité — ce sont généralement 1 ou 2 clés qui se disputent.
La démarche pratique est la suivante. D'abord, vous vous assurez de voir la valeur finale. C'est pourquoi nous avons fait /api/runtime : une réponse rapide qui montre « ce qui s'est réellement passé ». Ensuite, vous vérifiez la couche de base : ce qui est écrit dans application.yml. Après cela, vous vous rappelez l'échelle des priorités et vous vous demandez honnêtement : « est-ce que je ne redéfinis pas cela via les variables d'environnement, via -D ou via --... ? ». Si l'application tourne dans un conteneur, vous ajoutez une couche importante : « qu'est-ce que j'ai exactement passé au conteneur au démarrage ? ».
Quand il s'agit d'un conteneur, la commande docker inspect, que nous avons étudiée dans les premiers jours du cours sur l'interface Docker CLI, est très utile. Elle permet de voir avec quelles variables d'environnement le conteneur a été lancé. Et ce n'est pas une « commande DevOps rare », mais une simple lampe de poche dans l'obscurité. Si vous voyez dans Env quelque chose comme SERVER_PORT=8081, alors la question « pourquoi pas 8080 du fichier » se résout d'elle-même.
Pour renforcer l'idée — un petit schéma sur lequel vous pouvez vous appuyer mentalement quand la panique configurationnelle commence :
flowchart TD
A["Symptôme : la valeur n'est pas bonne"] --> B["Fixer la clé (ex. server.port)"]
B --> C["Regarder le résultat (Environment / /api/runtime / logs)"]
C --> D["Chercher la clé dans application.yml"]
D --> E["Chercher si la clé est redéfinie par-dessus : variables d'environnement → -D → --args"]
E --> F["Si conteneur : vérifier docker inspect / commande de lancement"]
F --> G["Supprimer les doublons / garder une source principale"]
L'idée clé à la fin : votre objectif n'est pas de « deviner la bonne source », mais de faire en sorte que la clé ait un propriétaire clair et unique. Sinon, la configuration se transforme en sport « qui crie le plus fort » — et le plus fort est généralement celui qui a été défini en dernier et par-dessus.
7. Erreurs typiques
Erreur n°1 : considérer application.yml comme une vérité absolue.
Le fichier application.yml est la couche de base, excellente pour les valeurs par défaut du projet, mais il ne peut pas être la « vérité absolue », car Spring Boot est spécialement conçu pour pouvoir être redéfini au lancement. Dans les scénarios Docker, cela est particulièrement visible : l'environnement du conteneur écrase souvent les valeurs du jar, et c'est un comportement attendu, pas un bug.
Erreur n°2 : traiter un problème de configuration par la reconstruction de l'image.
Si vous n'avez changé que les valeurs de lancement, la reconstruction de l'image ne changera généralement rien, à part votre humeur et le temps d'attente. Un conflit de configuration se résout non pas par docker build, mais en cherchant la source gagnante et en supprimant les doublons. La reconstruction a du sens quand le code ou l'artéfact change, pas quand vous disputez avec SERVER_PORT.
Erreur n°3 : définir la même clé à trois endroits « au cas où ».
Il semble parfois logique : « que ce soit dans application.yml, dans une variable d'environnement, et aussi dans les arguments de lancement — au cas où ». En pratique, cela conduit à ce qu'une semaine plus tard, personne (y compris vous) ne se souvienne de ce qui influence réellement le résultat. La bonne habitude est de garder la valeur par défaut dans application.yml et de redéfinir ponctuellement et consciemment d'une seule manière choisie.
Erreur n°4 : penser la configuration comme un « interrupteur de mode global ».
Les débutants s'attendent souvent à ce que « si j'ai activé un certain mode », alors toute l'application commence à vivre selon un seul fichier, et les autres couches disparaissent. Mais Spring Boot redéfinit les valeurs par clés, vous pouvez donc obtenir un « hybride » : une clé vient du fichier, une autre d'une variable d'environnement, une troisième des arguments de lancement. Ce n'est pas une anomalie, c'est une mécanique normale qu'il faut garder à l'esprit.
Erreur n°5 : essayer d'analyser la configuration « par source » plutôt que « par clé ».
Un tableau très fréquent : le développeur regarde longtemps seulement le Dockerfile ou seulement application.yml et ne comprend pas pourquoi la valeur ne change pas. La configuration dans Boot se résout par clé, donc l'enquête doit aussi suivre la clé : « où server.port est-il défini ? », « où spring.profiles.active est-il défini ? ». Dès que vous passez à cette optique, le chaos diminue considérablement.
GO TO FULL VERSION