1. Deux tâches dans la couche support
Si jusqu’à présent votre ContextFlow était perçu comme un « service pédagogique honnête », nous ajoutons aujourd’hui deux choses qui ressemblent beaucoup à ce que l’on rencontre dans des projets réels. La première : choisir l’implémentation souhaitée via la configuration sans éparpiller de if/else dans les services. La seconde : un diagnostic technique du contexte, mais sans que le code métier n’apprenne « à vivre du conteneur ».
Imaginons deux tâches en langage humain.
Première tâche : nous avons une interface ReportFormatter et deux implémentations — disons « textuelle » et « de type CSV ». Dans différents profils et environnements, nous voulons fournir un format de rapport différent, mais de manière à ce que ReportingService reste le plus simple possible : il doit dépendre du contrat, et non de la logique de sélection de l’implémentation.
Deuxième tâche : parfois on veut voir rapidement dans quel mode le contexte a démarré. Quels sont les profils actifs ? Quel est le nom de l’application ? Voire même quel format de rapport a été choisi ? Et on veut le faire sans avoir à écrire dans chaque service « donne-moi Environment… et maintenant getProperty(...)… ». Pour cela, nous allons créer un petit bean de diagnostic auquel le conteneur fournira lui-même les informations nécessaires.
Voici une petite « carte » des solutions pour voir qui fait quoi :
| Ce que nous voulons faire | Où cela vit | Pourquoi là |
|---|---|---|
| Choisir l’implémentation de ReportFormatter selon la propriété contextflow.report.format | support.factory + config.reporting | C’est la logique de construction/création d’objets, pas un scénario métier |
| Afficher un bref résumé sur le contexte (nom du bean, profils, quelques properties) | support.diagnostics + config.core | C’est un diagnostic technique, et il est autorisé à connaître l’environnement Spring |
2. ReportFormatter et choix du format
Avant d’ajouter FactoryBean, il est important de rappeler une idée simple : le service doit faire son travail, pas jouer à « l’assembleur d’application ». Quand ReportingService commence à décider quelle implémentation de formateur choisir, il prend une responsabilité supplémentaire. D’abord ça semble « ce n’est qu’un seul if », mais soudain cela devient « cinq if, trois propriétés, deux profils et un bug qui n’apparaît que le vendredi ».
Supposons que notre contrat de formateur ressemble à ceci (si vous l’avez déjà, vérifiez le sens) :
package com.example.contextflow.domain.ports;
import com.example.contextflow.domain.model.DailyReport;
public interface ReportFormatter {
// Formate le rapport domaine en une représentation textuelle (le format concret est décidé par l'implémentation)
String format(DailyReport report);
}
Les implémentations se trouvent généralement dans l’infrastructure ou à proximité de la partie reporting. Pour l’exemple, disons ceci :
package com.example.contextflow.infrastructure.reporting;
import com.example.contextflow.domain.model.DailyReport;
import com.example.contextflow.domain.ports.ReportFormatter;
public class TextReportFormatter implements ReportFormatter {
@Override
public String format(DailyReport report) {
// Format simple « humain » : adapté aux logs et à la console
return "DAILY REPORT: " + report;
}
}
Et la seconde :
package com.example.contextflow.infrastructure.reporting;
import com.example.contextflow.domain.model.DailyReport;
import com.example.contextflow.domain.ports.ReportFormatter;
public class CsvReportFormatter implements ReportFormatter {
@Override
public String format(DailyReport report) {
// Style CSV : d'abord l'en-tête, puis les données du rapport
return """
date,orderCount,cancelledCount,totalAmount
""" + report;
}
}
Maintenant, le point clé. ReportingService devrait ressembler à peu près à ceci : il reçoit un seul ReportFormatter et l’utilise simplement, sans décider « quel formateur exactement ». Exemple (simplifié à l’essentiel) :
package com.example.contextflow.application.reporting;
import com.example.contextflow.domain.model.DailyReport;
import com.example.contextflow.domain.ports.ReportFormatter;
public class ReportingService {
private final ReportFormatter formatter;
public ReportingService(ReportFormatter formatter) {
// Important : la dépendance est sur le contrat, pas sur une implémentation concrète
this.formatter = formatter;
}
public String format(DailyReport report) {
// Code métier « ennuyeux » : il appelle simplement le contrat
return formatter.format(report);
}
}
Si vous ressentez une légère déception du style « c’est tout ? » — félicitations, vous venez de voir le but d’un bon wiring : le code métier devient ennuyeux. Un code métier ennuyeux est souvent un compliment, pas une insulte.
3. ReportFormatterFactoryBean
Nous allons maintenant aborder FactoryBean non pas comme une « API astucieuse », mais comme un moyen de cacher la logique de création/choix là où elle a sa place : dans l’infrastructure. Imaginez que ReportFormatterFactoryBean est un barista. Le barista n’est pas lui-même le café, mais c’est lui qui décide si vous aurez un latte ou un expresso, et il fournit le résultat, pas lui-même.
Créons la factory dans support.factory. Elle dépendra uniquement des contrats et des implémentations des formateurs, pas des services métier.
package com.example.contextflow.support.factory;
import org.springframework.beans.factory.FactoryBean;
import com.example.contextflow.domain.ports.ReportFormatter;
import com.example.contextflow.infrastructure.reporting.CsvReportFormatter;
import com.example.contextflow.infrastructure.reporting.TextReportFormatter;
public class ReportFormatterFactoryBean implements FactoryBean<ReportFormatter> {
// Valeur par défaut : si la propriété n'est pas définie, on prend « text »
private String format = "text";
public void setFormat(String format) {
// Spring définira cette valeur à partir des properties via la configuration
this.format = format;
}
@Override
public ReportFormatter getObject() {
// C'est ici que vit la logique de choix de l'implémentation concrète (et pas dans le service)
return "csv".equalsIgnoreCase(format)
? new CsvReportFormatter()
: new TextReportFormatter();
}
@Override
public Class<?> getObjectType() {
// Informe le conteneur du type du « produit » que la factory fabrique
return ReportFormatter.class;
}
@Override
public boolean isSingleton() {
// Un seul formateur pour toute l'application — logique pour ce cas
return true;
}
}
Deux choses sont importantes ici.
Premièrement, le consommateur ne voit toujours que ReportFormatter : FactoryBean reste dans support, et vers l’extérieur le conteneur fournit le résultat de getObject().
Deuxièmement, le conteneur a besoin d’informations sur l’objet produit — c’est à cela que servent getObjectType() et isSingleton(). Grâce à cela, ReportingService ne connaît ni TextReportFormatter, ni CsvReportFormatter, ni ReportFormatterFactoryBean. Il reçoit un seul contrat et continue son travail.
C’est tout le sens de cette construction : la logique de choix est allée dans l’infrastructure, et le code métier est resté ennuyeux. Et un code métier ennuyeux, comme nous le savons déjà, vit généralement plus longtemps et casse moins souvent. Et oui, il n’est pas nécessaire d’écrire un FactoryBean pour tout ; ici, il est pertinent précisément parce que la logique de choix est devenue une responsabilité infrastructurelle distincte.
4. Configuration et différence entre factory et produit
Il faut maintenant intégrer la factory dans la configuration de sorte que le format soit choisi à partir des properties. Nous utilisons ici une technique déjà connue : @Configuration, @Bean et @Value avec une valeur par défaut. L’important est de choisir soigneusement le nom du bean, car dans le monde de FactoryBean, le nom devient particulièrement visible.
Créons le module config.reporting.ReportingConfig :
package com.example.contextflow.config.reporting;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.contextflow.support.factory.ReportFormatterFactoryBean;
@Configuration
public class ReportingConfig {
@Bean
public ReportFormatterFactoryBean reportFormatter(
@Value("${contextflow.report.format:text}") String format) {
// Crée la factory et lui transmet le paramètre de format depuis les properties
ReportFormatterFactoryBean factory = new ReportFormatterFactoryBean();
factory.setFormat(format);
return factory;
}
}
Remarquez le nom de la méthode reportFormatter(...). Pour Spring, ce nom devient le bean name. Et c’est là que ça devient intéressant : du point de vue du conteneur, le bean nommé reportFormatter est un FactoryBean, mais du point de vue des consommateurs, reportFormatter se comporte comme un ReportFormatter.
Ainsi, le code suivant fonctionnera (et renverra l’objet produit) :
// Obtient le produit de la factory (le vrai ReportFormatter), pas la factory elle-même
ReportFormatter formatter = context.getBean("reportFormatter", ReportFormatter.class);
Par contre, si vous voulez obtenir l’objet factory lui-même (par exemple, juste pour du diagnostic), on utilise le préfixe spécial &. Oui, c’est ce fameux « esperluette magique » qui n’est utile qu’à un seul endroit : quand on veut l’objet factory, et non l’objet produit.
// Obtient l'objet factory (FactoryBean) lui-même, pas le formateur produit
Object factory = context.getBean("&reportFormatter");
Pour le sentir concrètement, on peut faire une petite vérification directement dans le main() au démarrage (et c’est justement un cas où getBean() est acceptable : on est dans le code bootstrap/diagnostic, pas dans un service métier) :
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.example.contextflow.config.core.ContextFlowAppConfig;
import com.example.contextflow.domain.ports.ReportFormatter;
try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {
// Par le nom "reportFormatter", on obtient le produit de la factory (TextReportFormatter ou CsvReportFormatter)
ReportFormatter formatter = context.getBean("reportFormatter", ReportFormatter.class);
System.out.println(formatter.getClass().getSimpleName()); // TextReportFormatter
// Avec le préfixe "&", on obtient l'objet factory lui-même
Object factory = context.getBean("&reportFormatter");
System.out.println(factory.getClass().getSimpleName()); // ReportFormatterFactoryBean
}
Parlons maintenant des properties. Dans le fichier contextflow.properties de base (ou dans un fichier spécifique à un profil), vous aurez ce paramètre :
# Par défaut, on utilise le format textuel
contextflow.report.format=text
Et, par exemple, dans contextflow-demo.properties, on peut faire :
# Dans le profil demo, on veut du CSV
contextflow.report.format=csv
Et vous obtenez une chose simple et agréable : le même ReportingService reste inchangé, mais la composition et le comportement de l’application en matière de formatage du rapport sont modifiés par la configuration.
5. ContextDiagnosticsBean pour le diagnostic
Pour le diagnostic, nous tenons la même discipline. Pour le projet, nous fixons un bean d’infrastructure explicite, enregistré via la configuration : ainsi, Aware reste dans la couche support et ne ressemble pas à un @Component aléatoire découvert par le scanning.
C’est utile lorsque l’application devient profile-aware et property-heavy : au démarrage, vous voyez immédiatement dans quel mode le contexte a démarré et quelle configuration a réellement été chargée. Un tel bean ne doit pas traiter les commandes ni rechercher des dépendances. Son rôle est d’observer et d’informer.
Créons la classe dans support.diagnostics :
package com.example.contextflow.support.diagnostics;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
public class ContextDiagnosticsBean implements BeanNameAware, EnvironmentAware {
private String beanName;
private Environment environment;
@Override
public void setBeanName(String name) {
// Le conteneur communique au bean son nom
this.beanName = name;
}
@Override
public void setEnvironment(Environment environment) {
// Le conteneur donne accès à Environment (profils + propriétés)
this.environment = environment;
}
public String summary() {
// On prend quelques propriétés avec des valeurs par défaut — juste pour un diagnostic rapide
String appName = environment.getProperty("contextflow.app-name", "ContextFlow");
String format = environment.getProperty("contextflow.report.format", "text");
String profiles = String.join(",", environment.getActiveProfiles());
if (profiles.isBlank()) {
// S'il n'y a pas de profils actifs, on considère qu'on est en mode par défaut
profiles = "default";
}
// Renvoie une chaîne pratique pour le log au démarrage
return "[" + beanName + "] appName=" + appName
+ ", profiles=" + profiles
+ ", report.format=" + format;
}
}
Ici, nous avons utilisé deux interfaces Aware qui sont généralement les plus utiles pour le diagnostic.
BeanNameAware permet au conteneur de dire au bean : « ton nom est celui-ci ». Cela évite de se perdre dans les grandes configurations (surtout si vous utilisez des alias ou des noms explicites). EnvironmentAware nous donne Environment, que nous connaissons bien depuis les jours des properties, des profils et de la configuration externe. Autrement dit, nous n’avons pas « forcé l’entrée du conteneur », nous avons simplement reçu officiellement l’objet environnement.
Enregistrons le bean de diagnostic via la configuration (de manière explicite — pour qu’on voie bien qu’il s’agit d’infrastructure, pas « juste d’un composant aléatoire trouvé par scanning ») :
package com.example.contextflow.config.core;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.contextflow.support.diagnostics.ContextDiagnosticsBean;
@Configuration
public class DiagnosticsConfig {
@Bean
public ContextDiagnosticsBean contextDiagnosticsBean() {
// Crée explicitement le bean de diagnostic (infrastructure/support)
return new ContextDiagnosticsBean();
}
}
Dans le projet, nous fixons cette version : un bean de support dédié + un enregistrement explicite via DiagnosticsConfig.
Et maintenant, dans le code bootstrap, on peut faire ceci :
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.example.contextflow.config.core.ContextFlowAppConfig;
import com.example.contextflow.support.diagnostics.ContextDiagnosticsBean;
try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {
// Ceci est du code bootstrap/diagnostic : getBean() est ici approprié
ContextDiagnosticsBean diag = context.getBean(ContextDiagnosticsBean.class);
System.out.println(diag.summary());
// [contextDiagnosticsBean] appName=ContextFlow, profiles=dev, report.format=text
}
Cette ligne au début du démarrage est comme une étiquette sur une boîte : « à l’intérieur, c’est exactement ce que vous vouliez ». Et oui, parfois cela fait gagner plus de temps que le débogueur le plus intelligent : le débogueur est efficace, mais il ne peut pas lire dans vos pensées pour savoir quel profil vous avez « certainement activé ».
6. Vue d’ensemble et structure des packages
Rassemblons maintenant tout cela en une image claire : où se trouvent ces classes, qui connaît qui, et pourquoi cela ne transforme pas ContextFlow en « application qui ne pense qu’à Spring ». À cet endroit, il est utile de voir le projet comme une carte de la ville : les rues métier sont dans domain et application, les communications techniques dans support et config.
En nous appuyant sur la structure cible du cours, les parties qui nous intéressent ressemblent à ceci :
com.example.contextflow
├── application
│ └── reporting
│ └── ReportingService
├── domain
│ └── ports
│ └── ReportFormatter
├── infrastructure
│ └── reporting
│ ├── TextReportFormatter
│ └── CsvReportFormatter
├── support
│ ├── factory
│ │ └── ReportFormatterFactoryBean
│ └── diagnostics
│ └── ContextDiagnosticsBean
└── config
├── core
│ ├── DiagnosticsConfig
│ └── ContextFlowAppConfig
└── reporting
└── ReportingConfig
Pour que ce soit encore plus clair, voici un petit schéma des dépendances (sans « flèches superflues ») :
flowchart TD RS["ReportingService
(application)"] --> RF["ReportFormatter
(port)"] subgraph Support["couche support"] FBF["ReportFormatterFactoryBean
(FactoryBean)"] -->|"getObject()"| RF DIAG["ContextDiagnosticsBean
(Aware)"] end subgraph Config["couche config"] RC["ReportingConfig"] --> FBF DC["DiagnosticsConfig"] --> DIAG end ENV["Environment
(properties + profiles)"] --> RC ENV --> DIAG
Remarquez le « silence » utile dans ce schéma : ReportingService ne connaît ni la factory, ni Environment, ni ApplicationContext. Il ne connaît que ReportFormatter, et c’est exactement l’état auquel nous aspirons.
Quant à ContextDiagnosticsBean, il connaît Environment, mais c’est normal, car son travail est le diagnostic de l’environnement. Il ne choisit pas le format du rapport, il le montre. Le choix est fait par la configuration et la factory.
En pratique, vous verrez un effet similaire : au démarrage de l’application dans le profil dev, le diagnostic indiquera report.format=text, et au démarrage dans le profil demo, il indiquera report.format=csv. Et ReportingService restera la même classe, sans une seule ligne « si demo – alors … ».
C’est le snapshot de travail de ContextFlow : le choix du formateur et le diagnostic vivent dans support/config, tandis que ReportingService reste un simple consommateur de ReportFormatter.
7. Erreurs typiques avec FactoryBean et Aware
Erreur n°1 : rendre ReportingService dépendant de ReportFormatterFactoryBean.
C’est très tentant : « puisque la factory sait fabriquer le formateur, je vais l’injecter directement ». Mais alors votre service commence à connaître le mécanisme du conteneur. Il acquiert la connaissance « j’ai une factory », donc vous perdez le principal bénéfice de FactoryBean : le consommateur ne doit voir que l’objet produit (ReportFormatter) et mener une vie tranquille.
Erreur n°2 : ne pas comprendre que par le nom du bean vous obtenez l’objet produit, pas la factory.
Situation classique : vous écrivez context.getBean("reportFormatter"), vous vous attendez à la factory, et vous recevez TextReportFormatter. Ce n’est pas « Spring est cassé », c’est sa logique normale pour FactoryBean. Si vous avez réellement besoin de l’objet factory, utilisez &reportFormatter. Et efforcez-vous de garder cette astuce dans le diagnostic/l’infrastructure, pas dans la couche métier.
Erreur n°3 : oublier d’implémenter correctement getObjectType() et obtenir d’étranges erreurs de wiring.
Si getObjectType() retourne un type trop général, null ou quelque chose de faux, le conteneur a du mal à faire l’auto‑liaison et le diagnostic. Dans un projet pédagogique, cela se manifestera par « pourquoi le conteneur ne voit pas mon ReportFormatter », et dans un projet réel par « pourquoi cela ne fonctionne que dans un seul profil ». Gardez cette méthode honnête : si la factory produit un ReportFormatter, qu’elle le dise.
Erreur n°4 : utiliser Aware comme substitut à l’injection par constructeur.
Aware n’est pas une « injection plus puissante », c’est un canal spécial pour les informations du conteneur. Dès que vous commencez à faire du ApplicationContextAware dans un service ordinaire « pour ne pas écrire de constructeur », vous coupez la transparence des dépendances et revenez à un graphe caché. Au final, la classe a l’air simple, mais à l’intérieur elle cache un accès au monde.
Erreur n°5 : transformer ContextDiagnosticsBean en « service métier avec droits d’administrateur ».
Parfois, le bean de diagnostic commence à se doter de méthodes du style « tiens, je vais aussi créer une commande », « tiens, je vais changer le format du rapport à la volée ». Ce n’est plus du diagnostic, c’est une deuxième couche de l’application avec une responsabilité floue. Le bean de diagnostic doit être ennuyeux : lire quelques propriétés, produire une chaîne, au maximum — vérifier la présence de certaines conditions.
GO TO FULL VERSION