1. Fichier de configuration et classpath
Avant d'écrire la moindre annotation, il faut comprendre où vit réellement le fichier de configuration. Dans une application Java, « fichier à côté du code » ne signifie pas toujours « fichier accessible au runtime ». Surtout quand Gradle assemble un jar. Maintenant, découvrons ce qu'est src/main/resources, ce qui entre dans le classpath et pourquoi Spring aime le préfixe classpath:.
Dans un projet Gradle, il y a une « constitution » tacite : tout ce qui se trouve dans src/main/java devient .class, et tout ce qui est dans src/main/resources devient « ressources de l'application ». Ces ressources, Gradle les copie dans le répertoire de build, puis les empaquette dans un jar (si vous construisez un jar). Autrement dit, contextflow.properties ne doit pas traîner « quelque part à côté », « à la racine du disque » ou « dans Téléchargements parce que c'est pratique ». Il doit être là où la build le récupérera à coup sûr.
Je vais le montrer sous forme d'arborescence (pour éviter que votre cerveau essaie de deviner où Spring cherche le fichier) :
spring-core-contextflow/
├─ src/
│ └─ main/
│ ├─ java/
│ │ └─ com/example/contextflow/...
│ └─ resources/
│ └─ contextflow.properties <-- voici où le mettre
└─ build/
└─ resources/
└─ main/
└─ contextflow.properties <-- Gradle le copie ici lors de la compilation/exécution
Lorsque vous lancez l'application depuis l'IDE ou via Gradle, c'est le contenu de build/resources/main et build/classes/java/main qui entre généralement dans le classpath. Et Spring, lorsqu'il voit classpath:contextflow.properties, effectue une recherche « logique » de ce type : « y a‑t‑il une ressource avec ce chemin dans le classpath ? ».
Sans cette compréhension, apparaissent les situations classiques du type : « j'ai bien créé le fichier, promis, promis… pourquoi Spring écrit qu'il n'existe pas ? ». Et dans 90% des cas, la réponse est banale : le fichier ne se trouve pas dans src/main/resources mais dans le « dossier du projet » ou ailleurs, et Gradle ne le copie pas, donc le classpath n'en a même pas entendu parler.
2. Fichier contextflow.properties
Un fichier de propriétés n'est pas un dépotoir de tout et n'importe quoi, mais un petit contrat entre « celui qui écrit le code » et « celui qui exécute l'application ». Si les clés sont chaotiques, au bout d'une semaine vous aurez oublié ce que signifient mode=1 et value2=yes. Créons contextflow.properties et mettons-nous tout de suite d'accord sur des noms compréhensibles.
Commençons par le fichier lui-même. Il est simple : des lignes de la forme clé=valeur, des commentaires via #, pas de JSON, pas de YAML — une pure « comptabilité » pour les paramètres.
Fichier : src/main/resources/contextflow.properties
# Paramètres de base de ContextFlow
contextflow.app-name=ContextFlow
# Où mettre les résultats (uniquement dans build/ pour que le projet soit reproductible)
contextflow.report.output-dir=build/reports
# Les autres valeurs sont encore des chaînes (nous arriverons au typage plus tard)
contextflow.notifications.default-channel=console
contextflow.audit.mode=console
contextflow.locale.default=ru
Pour les operational values comme console et file, il est pratique de les garder en minuscules dès le début : ainsi les paramètres se lisent comme des options de lancement, et non comme des constantes Java.
Remarquez la discipline des noms : toutes les clés commencent par contextflow.. Ce n'est pas un « amour des points », c'est un moyen de se protéger du chaos. Si les clés s'appellent simplement name, mode, format — vous aurez très vite un conflit « le nom de qui ? » et « quel mode ? ». Le préfixe rend l'espace de noms explicite.
Pour que ce soit encore plus clair, vous pouvez garder en tête un petit tableau (il n'est pas obligatoire pour le code, mais il aide beaucoup à la lecture du projet) :
| Clé | Exemple de valeur | À quoi sert-elle |
|---|---|---|
| contextflow.app-name | ContextFlow | Nom technique de l'application (pour la sortie, le diagnostic, un journal clair) |
| contextflow.report.output-dir | build/reports | Répertoire de base pour les fichiers de rapports |
| contextflow.notifications.default-channel | console | Canal de notifications par défaut (pour l'instant sous forme de chaîne) |
| contextflow.audit.mode | console | Mode d'audit (pour l'instant sous forme de chaîne) |
| contextflow.locale.default | ru | Locale par défaut (pour l'instant sous forme de chaîne) |
Notez un point important : dans cette leçon, nous ne rendons volontairement pas les valeurs typées (nous ne transformons pas console en enum). Pour l'instant, ce sont des chaînes. Notre objectif aujourd'hui est d'apprendre à Spring à « voir » le fichier et d'apprendre au conteneur à injecter les valeurs.
3. Connexion via @PropertySource
Maintenant que le fichier existe, il est logique de s'attendre à ce que Spring le trouve automatiquement, le lise et commence à l'utiliser. Mais dans Spring pur, il n'y a pas de « télépathie » : le conteneur doit recevoir une instruction explicite que cette ressource doit être ajoutée à la liste des sources de configuration. Cette instruction sera @PropertySource sur la classe de configuration.
Le moyen le plus direct est de placer @PropertySource sur votre classe de configuration (celle que vous passez à AnnotationConfigApplicationContext). Dans notre ContextFlow, elle joue généralement le rôle de « point d'assemblage de l'application ».
Exemple de configuration minimale :
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@ComponentScan("com.example.contextflow") // Dis à Spring où chercher @Component/@Service etc.
@PropertySource(
value = "classpath:contextflow.properties", // Prend le fichier du classpath (c'est-à-dire des resources dans le build)
encoding = "UTF-8" // Fixe explicitement l'encodage pour éviter les caractères illisibles
)
public class AppConfig {
// La configuration peut être vide : l'essentiel est que les annotations définissent déjà le comportement du conteneur
}
Il y a ici plusieurs « détails » qui se révèlent être loin d'être anodins en pratique.
classpath:contextflow.properties dit : « cherche le fichier dans le classpath ». C'est parfait pour un projet d'apprentissage et pour de nombreuses applications réelles où les paramètres par défaut se trouvent dans le jar.
encoding = "UTF-8" n'est pas toujours indispensable, mais très utile si un jour vous voulez mettre des lettres cyrilliques dans les valeurs (par exemple, le nom de l'application « KontekstFlow dlya demo »). Sans encodage, vous pouvez soudainement obtenir des caractères illisibles, et déboguer ces caractères est un sport à part entière.
Après cela, le fichier devient PropertySource à l'intérieur de l'Environment. Et on peut le vérifier sans aucun @Value : il suffit de demander à l'Environment s'il voit la clé.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApplication {
public static void main(String[] args) {
// try-with-resources garantit la fermeture correcte du contexte (et la libération des ressources)
try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Vérifions que l'Environment voit bien la clé de contextflow.properties
String appName = ctx.getEnvironment().getProperty("contextflow.app-name");
System.out.println("appName = " + appName); // appName = ContextFlow
}
}
}
Si vous voyez ContextFlow — cela signifie que @PropertySource a fonctionné : le fichier est connecté, l'Environment lit les valeurs.
À ce stade, AppConfig joue déjà le rôle de point d'assemblage principal : c'est lui que vous démarrez via AnnotationConfigApplicationContext, et les configurations plus spécifiques peuvent être conservées comme modules de cette même assemblée.
Remarque sur file:
Parfois, on veut dire : « Je veux un fichier externe à côté du jar, pour pouvoir le modifier sans reconstruire ». Pour cela, il existe effectivement le préfixe file: :
@PropertySource("file:./contextflow.properties") // ./ est le répertoire de travail courant du processus
Mais cette approche a une nuance : ./ est le « répertoire de travail courant ». Et il dépend d'où vous lancez l'application (IDE, Gradle, terminal). Pour un projet d'apprentissage et la stabilité, il est préférable de commencer avec classpath:. Lorsque vous maîtrisez bien ce que sont l'Environment et PropertySource, le passage à file: externe sera plus simple et sans mystère superflu.
4. ${...} et PropertySourcesPlaceholderConfigurer
Après @PropertySource, l'Environment sait déjà lire les clés du fichier. La question pratique suivante est de savoir comment cette valeur parvient jusqu'à ${...} dans @Value. Dans un simple AnnotationConfigApplicationContext, un PropertySourcesPlaceholderConfigurer séparé n'est pas toujours nécessaire comme première étape, mais il est quand même utile de le connaître : c'est un mécanisme d'infrastructure explicite qui rend visible la couche de résolution des placeholders et donne un point où la configurer séparément.
Il est utile de garder à l'esprit deux idées différentes : connecter le fichier au contexte et externaliser explicitement le mécanisme des ${...} ne sont pas la même chose. La plupart du temps, le premier suffit, mais le second montre bien à travers quelle couche le conteneur injecte les valeurs dans les chaînes.
Regardons d'abord un @Value classique :
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class AppNamePrinter {
public AppNamePrinter(@Value("${contextflow.app-name}") String appName) {
// @Value essaie de résoudre le placeholder ${...} via le mécanisme des placeholders
System.out.println("AppNamePrinter: " + appName);
}
}
Si le placeholder n'est pas résolu pour une raison quelconque, l'erreur ressemblera à ceci :
Could not resolve placeholder 'contextflow.app-name' ...
Donc le problème n'est pas dans la clé elle-même, mais dans le fait que la couche de substitution ${...} n'a pas fonctionné comme vous l'espériez.
Ci-dessous, une variante explicite d'infrastructure, pratique à avoir sous la main pour comprendre la mécanique ou lorsqu'on souhaite configurer cette couche séparément :
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
@Configuration
public class PlaceholderConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
// Explicite la couche de résolution des placeholders.
// static est généralement utilisé ici pour que le configurateur apparaisse le plus tôt possible —
// avant la création des beans ordinaires où se trouve @Value("${...}").
return new PropertySourcesPlaceholderConfigurer();
}
}
Ici, le mot clé est static. Pour cette variante explicite, on le rend généralement static pour que le configurateur apparaisse le plus tôt possible, avant même que Spring ne commence à créer les beans ordinaires avec @Value.
Si vous voulez une analogie, c'est comme la sécurité à l'entrée d'une salle de concert. Elle doit arriver avant le public. Si la sécurité arrive après le public, elle fonctionnera certes encore, mais il sera trop tard et ce sera bien trop amusant.
Je ne rentre pas volontairement dans les détails du type de bean d'infrastructure et de la phase où il opère. Deux conclusions pratiques nous suffisent : PropertySourcesPlaceholderConfigurer est un moyen explicite d'externaliser la résolution des placeholders dans une couche séparée, et si vous le déclarez dans une méthode @Bean séparée, on le rend généralement static.
Il est utile de fixer dans l'esprit un petit schéma de ce qui se connecte à quoi :
flowchart TD
A["System properties"] --> ENV["Environment"]
B["OS env vars"] --> ENV
C["@PropertySource: contextflow.properties"] --> ENV
ENV --> PSC["PropertySourcesPlaceholderConfigurer"]
PSC --> VAL["@Value: ${...} resolution"]
Ainsi, le fichier de configuration devient l'une des sources de l'Environment, et PropertySourcesPlaceholderConfigurer fait en sorte que @Value puisse extraire les valeurs de cet Environment.
Vérification dans un contexte vivant
La théorie, c'est bien, mais dans Spring il y a une excellente pratique : « vérifier que ça démarre vraiment ». Si l'on souhaite externaliser explicitement la couche de placeholders dans un fragment d'infrastructure, on peut démarrer le contexte avec deux configurations : AppConfig (où se trouve @PropertySource) et PlaceholderConfig (où cette couche est isolée).
Commençons par vérifier que AppNamePrinter est créé et imprime le nom de l'application.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApplication {
public static void main(String[] args) {
try (var ctx = new AnnotationConfigApplicationContext(
AppConfig.class,
PlaceholderConfig.class // Connecte la couche explicite de substitution ${...}
)) {
String appName = ctx.getEnvironment().getProperty("contextflow.app-name");
System.out.println("Environment says: " + appName); // Environment says: ContextFlow
}
}
}
Et maintenant le « printer » du nom (nous l'avons déjà) :
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class AppNamePrinter {
public AppNamePrinter(@Value("${contextflow.app-name}") String appName) {
// Ici, il est important que appName arrive déjà substitué, et non sous forme de chaîne "${contextflow.app-name}"
System.out.println("AppNamePrinter: " + appName); // AppNamePrinter: ContextFlow
}
}
Si tout est correctement connecté, vous verrez les deux sorties. L'ordre des lignes peut légèrement différer (Spring crée les beans dans son propre ordre), mais le sens est le même : l'Environment et @Value aboutissent à la même valeur.
Test de surcharge sans modifier le fichier
Pour ressentir la puissance réelle des « sources de configuration multiples », on peut ajouter une propriété système dans main(). Elle a généralement une priorité élevée.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApplication {
public static void main(String[] args) {
// Surcharge la valeur au niveau des propriétés système (généralement haute priorité)
System.setProperty("contextflow.app-name", "ContextFlow-Demo");
try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class, PlaceholderConfig.class)) {
System.out.println(ctx.getEnvironment().getProperty("contextflow.app-name"));
// ContextFlow-Demo
}
}
}
Et voilà, vous avez changé le comportement au lancement sans modifier contextflow.properties ni les classes Java. Oui, c'est un petit exemple, mais c'est exactement la porte par laquelle l'application cesse d'être « éternellement liée à un seul environnement ».
5. Assemblage de la configuration dans le projet
Quand le mécanisme fonctionne, une pensée dangereuse surgit : « Super ! Je vais maintenant disperser @PropertySource et ${...} dans tout le projet ! ». Et c'est là qu'il est important de s'arrêter et de faire un pas soigneux et mature : organiser la configuration de sorte que dans un mois vous puissiez comprendre d'où viennent les paramètres, sans transformer le projet en fouilles archéologiques. Faisons maintenant ce petit mais correct incrément.
Il y a deux règles pratiques qui maintiennent le projet en ordre.
Première : @PropertySource doit vivre dans la couche de configuration (dans config.*), et non dans les services métier. Un service métier doit « obtenir une valeur prête », et non « chercher un fichier ».
Deuxième : PropertySourcesPlaceholderConfigurer est une infrastructure, et il est préférable qu'il vive aussi près de la configuration, et non « par hasard quelque part dans support ».
Dans le projet pédagogique ContextFlow, on peut le laisser sous forme de deux fichiers (AppConfig et PlaceholderConfig), si l'on veut voir la couche d'infrastructure séparément. Mais il est plus pratique de garder un seul root — AppConfig, et de considérer les autres configurations comme ses modules. Si vous voulez encore plus compact, vous pouvez placer le static @Bean directement dans AppConfig. Ce n'est ni « mieux » ni « pire », c'est juste une autre organisation. L'essentiel est qu'il y ait un accord clair dans le projet.
Par exemple, comme ceci (une seule classe, mais toujours lisible) :
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
@Configuration
@ComponentScan("com.example.contextflow") // Scanne les composants de l'application
@PropertySource(value = "classpath:contextflow.properties", encoding = "UTF-8") // Connecte le fichier de propriétés depuis les ressources
public class AppConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
// Montre explicitement la couche de substitution ${...} pour @Value
return new PropertySourcesPlaceholderConfigurer();
}
}
C'est ce AppConfig qu'il est pratique de considérer comme le point d'assemblage principal de ContextFlow : vous le passez à AnnotationConfigApplicationContext, et les autres morceaux de configuration vivent comme ses modules si nécessaire.
Maintenant le point d'entrée redevient « un seul bouton » :
new AnnotationConfigApplicationContext(AppConfig.class);
Et c'est agréable. Mais encore une fois : pas « obligatoire », mais « pratique ».
Et encore une pensée très importante qui vous sera utile dès aujourd'hui : si une propriété est vraiment obligatoire et que vous l'injectez comme ${contextflow.app-name} sans valeur par défaut, alors en l'absence de la clé, Spring doit échouer au démarrage. Ce n'est pas de la cruauté, c'est une préoccupation pour l'avenir. Fail-fast, c'est quand l'application casse là où la cause est immédiatement visible, et non une demi-heure plus tard au milieu d'un scénario métier. Nous reparlerons des valeurs par défaut dans @Value, mais il est déjà utile d'accepter l'idée : l'absence d'une clé obligatoire est une erreur de construction de l'application, et non « bon, tant pis ».
6. Erreurs de configuration typiques
Erreur n°1 : avoir placé contextflow.properties ailleurs que dans src/main/resources.
C'est un classique. Le fichier se trouve à côté de build.gradle.kts, vous le voyez dans le projet, mais Spring non. Symptôme : @PropertySource se plaint que la ressource est introuvable, ou l'Environment retourne null. Le remède est simple : le fichier doit se trouver dans src/main/resources, ainsi Gradle le placera à coup sûr dans le classpath.
Erreur n°2 : confondre classpath: et le chemin du fichier.
Parfois on écrit @PropertySource("classpath:/src/main/resources/contextflow.properties"). Cela semble logique humainement, mais pas en Java. Dans classpath:, le chemin est donné par rapport à la racine du classpath, et non par rapport aux dossiers sources. Correct : @PropertySource("classpath:contextflow.properties"), si le fichier se trouve directement dans src/main/resources.
Erreur n°3 : ne pas distinguer la connexion PropertySource et la couche de résolution des placeholders autour de ${...}.
Symptôme : Environment.getProperty() retourne une valeur, mais ${...} dans @Value ne se comporte pas comme prévu, ou l'infrastructure est difficile à déboguer. L'un des moyens explicites de rendre cette couche compréhensible est d'extraire PropertySourcesPlaceholderConfigurer dans un static @Bean séparé. Dans Spring Boot, une partie de cette infrastructure arrive automatiquement, mais dans Spring pur, il est utile de la voir explicitement au moins une fois.
Erreur n°4 : si vous avez extrait PropertySourcesPlaceholderConfigurer dans un @Bean séparé, vous ne l'avez pas déclaré static.
Parfois l'application semble démarrer, mais parfois elle commence à se comporter bizarrement lorsque la configuration se complexifie, ou vous attrapez des problèmes d'initialisation précoces. Pour une variante d'infrastructure explicite, il vaut mieux apprendre tout de suite le pattern : public static PropertySourcesPlaceholderConfigurer .... C'est l'un de ces cas où « plus court » ne signifie pas « meilleur ».
Erreur n°5 : avoir mis une « valeur par défaut absurde » à une propriété obligatoire pour que l'application ne plante pas.
Par exemple : ${contextflow.app-name:asd}. L'application démarre, mais vous avez balayé le problème sous le tapis. Une semaine plus tard, vous lancez l'application, voyez « asd » dans la sortie et commencez à déboguer « pourquoi l'audit n'écrit pas là où il faut », alors que le vrai problème était que le fichier de propriétés n'était pas connecté. Si la clé est obligatoire, que l'échec soit honnête et précoce.
GO TO FULL VERSION