1. Analyse : frontière → carte → pont
Maintenant, nous avons déjà deux outils : nous savons lire le XML comme une carte de beans et nous savons l’intégrer dans un ApplicationContext commun. Cela signifie que nous pouvons cesser de parler de manière abstraite et ouvrir le legacy-notification-context.xml concret de ContextFlow.
L’ordre de lecture reste le même : d’abord la frontière du fragment, puis la liste des beans, ensuite les dépendances et le cycle de vie, et enfin le pont vers le contexte commun. Mais désormais, tout cela doit être fait non pas sur un teaser pédagogique à un bean, mais sur un morceau vivant du projet.
Où se trouve le XML legacy dans ContextFlow
Dans les petits projets d’apprentissage, on peut placer le XML à la racine des ressources et faire comme on peut. Mais en réalité, vous gagnez si le legacy a un emplacement explicite dans le dépôt. Cela aide à la fois la lecture du code et sa migration progressive : vous savez toujours que « voilà l’îlot du passé, et voilà la configuration moderne ».
Dans ContextFlow, nous avons convenu de stocker les fragments legacy dans src/main/resources/legacy/. La structure la plus simple ressemble à :
src/main/resources/
└── legacy/
└── legacy-notification-context.xml
Pourquoi justement resources et précisément une liaison classpath: ? Parce que ce fichier fait partie de la construction de l’application. Quand vous compilez le jar, la ressource sera incluse dans l’artefact et pourra être chargée comme classpath:/legacy/legacy-notification-context.xml. C’est important : si nous nous étions appuyés sur des chemins absolus comme C:\Users\..., le projet n’aurait fonctionné que chez un seul chanceux (généralement l’auteur).
Retenez une petite règle : si le XML est un fragment legacy « interne » de l’application, il doit être une ressource classpath. En revanche, s’il vient de l’extérieur (par exemple, un fichier de configuration pour l’environnement), c’est une autre histoire, mais dans notre cours nous ne compliquons pas volontairement.
Et oui, quand vous voyez dans le code :
@ImportResource("classpath:/legacy/legacy-notification-context.xml")
votre cerveau doit automatiquement comprendre deux choses. Premièrement, c’est un « module de configuration importé », pas un « fichier quelconque ». Deuxièmement, c’est une promesse : les beans du XML seront placés dans le même ApplicationContext que les beans de la configuration Java.
3. Lire legacy-notification-context.xml
Ouvrons le fichier. Commençons par le premier bean – c’est l’îlot legacy minimal qui sera ensuite intégré au contexte commun :
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Déclaration explicite du bean : en legacy, c'est souvent plus lisible que component-scan -->
<bean id="legacyNotificationSender"
class="com.example.contextflow.infrastructure.notification.legacy.LegacyConsoleNotificationSender"
init-method="init" destroy-method="destroy">
<!-- value : valeur simple, sera passée au setter -->
<property name="appName" value="ContextFlow (legacy)"/>
<!-- ref : référence vers un autre bean dans le même ApplicationContext -->
<property name="messageSource" ref="messageSource"/>
</bean>
</beans>
Si on lit cela « humainement », vous devez littéralement dire à voix haute :
« Il y a un bean avec le nom legacyNotificationSender. La classe est LegacyConsoleNotificationSender. Il a des méthodes de cycle de vie init et destroy. On lui définit via des setters appName comme chaîne et messageSource comme référence vers un autre bean nommé messageSource. Donc le contexte doit obligatoirement contenir un messageSource. »
Remarquez que le XML n’utilise pas context:component-scan. C’est fait exprès : pour un petit îlot legacy, le scan nuit souvent à la lisibilité. Quand le fragment est isolé, il est agréable de voir la liste des beans explicitement.
C’est toujours le même legacy-notification-context.xml ; nous ne passons pas à un autre instantané, nous l’enrichissons simplement d’un bean supplémentaire dans le même fichier :
<!-- Dans le même legacy-notification-context.xml, on ajoute un autre bean -->
<bean id="legacyNotificationDispatchService"
class="com.example.contextflow.application.service.NotificationDispatchService">
<!-- ref : on passe la dépendance dans le constructeur -->
<constructor-arg ref="legacyNotificationSender"/>
</bean>
Et encore une fois, nous verbalisons : « il y a un service legacyNotificationDispatchService, il est créé par la classe NotificationDispatchService, et dans son constructeur on lui passe une référence vers legacyNotificationSender ».
L’essentiel : vous ne « devinez » pas, vous reconstituez le graphe d’objets. C’est exactement ainsi que raisonne Spring.
Pour que ce XML ne reste pas uniquement des mots, montrons le code minimal du legacy sender lui-même. On utilise ici délibérément l’injection par setter et init/destroy — c’est une manière typique du legacy. Nous ne faisons pas cela « partout », mais nous devons être capables de le lire.
import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.context.MessageSource;
public class LegacyConsoleNotificationSender implements NotificationSender {
// Dépendance issue du contexte : dans le XML, elle arrive via <property ref="...">
private MessageSource messageSource;
// Valeur primitive : dans le XML, elle arrive via <property value="...">
private String appName = "ContextFlow";
// Injection par setter — style legacy typique
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
public void setAppName(String appName) {
this.appName = appName;
}
// init-method depuis le XML : init-method="init"
public void init() {
System.out.println(appName + ": legacy sender init"); // ContextFlow (legacy) : legacy sender init
}
// destroy-method depuis le XML : destroy-method="destroy"
public void destroy() {
System.out.println(appName + ": legacy sender destroy");
}
@Override
public void send(String messageKey) {
// MessageSource provient de l'ApplicationContext commun, pas créé manuellement
var msg = messageSource.getMessage(messageKey, null, messageKey, java.util.Locale.ENGLISH);
System.out.println("LEGACY NOTIFY: " + msg);
}
}
À cet endroit, il est important de ne pas discuter avec l’histoire. Oui, nous avons appris l’injection par constructeur comme style principal. Mais le legacy n’est pas obligé de suivre nos méthodes. Notre tâche est de comprendre et de contrôler.
4. Carte de wiring : beans et dépendances
Si vous lisez le XML à l’œil nu, au bout de 10 minutes votre tête bourdonne. C’est pourquoi l’astuce la plus pratique est de dresser rapidement une « carte » : un tableau id → class → dépendances → style d’injection → notes. Cela semble ennuyeux, mais sur un vrai projet, une telle carte peut parfois vous faire gagner une journée.
Supposons que nous ayons deux beans issus des exemples ci-dessus. La carte ressemblera à :
| Bean id | Class | Dépendances | Style d’injection | Notes |
|---|---|---|---|---|
| legacyNotificationSender | LegacyConsole NotificationSender | messageSource (ref), appName (value) | property / setter | a init-method, destroy-method |
| legacyNotification DispatchService | Notification DispatchService | legacyNotificationSender (ref) | constructor | le service dépend du legacy sender |
Avec ce tableau, la magie opère. Vous commencez à voir que le XML est simplement une forme alternative de ce que vous écririez en Java :
- constructor-arg ref="..." – cela signifie « passe le bean au constructeur » ;
- property name="..." ref="..." – cela signifie « appelle le setter et passe le bean » ;
- property name="..." value="..." – cela signifie « appelle le setter et passe une chaîne/un nombre ».
Et regardons séparément le cycle de vie. Dans notre exemple :
- init-method="init" – c’est en fait « à la fin de l’initialisation, appelle init() » ;
- destroy-method="destroy" – « à la fermeture du contexte, appelle destroy() ».
C’est donc le même cycle de vie que celui que nous avons déjà vu avec @PostConstruct/@PreDestroy, simplement habillé en XML.
Si vous voulez renforcer un peu l’idée « XML = même modèle », voici l’équivalent Java de notre legacy sender (pas nécessaire de le réécrire maintenant, juste pour que le cerveau voie la traduction) :
import com.example.contextflow.infrastructure.notification.legacy.LegacyConsoleNotificationSender;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class NotificationLegacyAsJavaConfig {
// Équivalent de init-method/destroy-method du XML, mais en configuration Java
@Bean(initMethod = "init", destroyMethod = "destroy")
LegacyConsoleNotificationSender legacyNotificationSender(MessageSource messageSource) {
var sender = new LegacyConsoleNotificationSender();
// Équivalent de <property name="appName" value="..."/>
sender.setAppName("ContextFlow (legacy)");
// Équivalent de <property name="messageSource" ref="messageSource"/>
sender.setMessageSource(messageSource);
return sender;
}
}
Vous voyez ? Mêmes dépendances, mêmes valeurs, même cycle de vie. Simplement, au lieu du XML, c’est du Java.
5. Intégrer le XML via @ImportResource
La mécanique d’intégration est déjà connue : le XML se trouve dans resources/legacy/ et est intégré au conteneur commun via un petit bridge-config. Un fait est important : legacyNotificationSender et legacyNotificationDispatchService vivent dans le même ApplicationContext que messageSource, les profils, les listeners et le reste de la configuration Java.
Voici le pont minimal dans notre projet :
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
// Intègre le XML legacy dans le ApplicationContext courant (ne crée pas un contexte séparé)
@ImportResource("classpath:/legacy/legacy-notification-context.xml")
public class LegacyBridgeConfig {
}
Ensuite, ce pont est intégré à la configuration principale avec les modules Java-config habituels :
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({
com.example.contextflow.config.core.CoreConfig.class,
com.example.contextflow.config.legacybridge.LegacyBridgeConfig.class
})
public class ContextFlowAppConfig {
}
Maintenant, messageSource provient de la configuration Java, et le legacy sender du XML. Pour le conteneur, c’est simplement un seul graphe d’objets, pas deux mondes différents.
6. Vérifier le wiring par l’exécution
Lire le XML est une chose. S’assurer que vous avez bien compris en est une autre. Pour un débutant, il est très utile de faire un petit lancement qui montrera : « Oui, le bean du XML existe vraiment dans le contexte, oui, il est accessible par son nom, oui, il participe au wiring ». Ce n’est pas du test en tant que discipline, c’est simplement une vérification de bon sens pour l’esprit.
Créons un petit main qui démarre le contexte et récupère les beans par leur nom. Notez que nous utilisons try-with-resources pour que le contexte soit correctement fermé et appelle destroy-method.
import com.example.contextflow.application.service.NotificationDispatchService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class LegacyXmlWiringPreview {
public static void main(String[] args) {
// try-with-resources est important : on garantit la fermeture du contexte et l’appel de destroy-method du XML
try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {
// Diagnostic : on récupère le bean par son nom (exactement comme il est déclaré dans le XML via id="...")
var senderClass = context.getBean("legacyNotificationSender").getClass().getName();
System.out.println(senderClass); // ...LegacyConsoleNotificationSender
// Diagnostic : on récupère le service et on vérifie que le wiring fonctionne réellement
var svc = context.getBean("legacyNotificationDispatchService", NotificationDispatchService.class);
svc.sendOrderCreated("ORDER-1"); // LEGACY NOTIFY: notification.order.created
}
}
}
Ce qui est important ici. Nous ne faisons pas de « getBean() » dans le code métier, nous le faisons dans un petit lancement de diagnostic. C’est normal, car le but est de comprendre le wiring.
Et encore une micro-astuce utile : parfois on veut simplement voir rapidement quels noms de beans sont apparus. On peut filtrer par le préfixe legacy et afficher la liste. Pour la compréhension, c’est parfait.
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;
public class LegacyBeansListPreview {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {
// BeanDefinitionNames — un moyen pratique de vérifier rapidement ce qui a été chargé dans le conteneur
Arrays.stream(context.getBeanDefinitionNames())
// Dans les fragments legacy, il est utile de garder un préfixe dans l’id pour éviter les collisions de noms
.filter(n -> n.startsWith("legacy"))
.forEach(System.out::println); // legacyNotificationSender, legacyNotificationDispatchService
}
}
}
Après une telle exécution, l’idée principale se fixe dans votre tête : le XML participe bien au même conteneur. Cela dissipe encore un peu de « magie ».
7. Plusieurs implémentations d’interface et XML legacy
Dans ContextFlow, NotificationSender est rarement seul, et c’est pourquoi le nom legacyNotificationSender n’est pas seulement utile pour l’esthétique. Il rend le choix explicite : quand vous avez besoin du legacy sender, vous le liez par son nom, sans espérer que le conteneur devine.
import com.example.contextflow.application.service.NotificationDispatchService;
import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class LegacyNotificationWiring {
@Bean
NotificationDispatchService legacyOnlyDispatchService(
// On sélectionne explicitement le bean legacy du XML par son id
@Qualifier("legacyNotificationSender") NotificationSender sender
) {
return new NotificationDispatchService(sender);
}
}
Ici, @Qualifier("legacyNotificationSender") prolonge littéralement l’id du XML. C’est pratique pour deux raisons. Premièrement, le wiring se lit à l’œil : on voit que ce consommateur spécifique est bien sur l’implémentation legacy. Deuxièmement, ce pont survit à une migration progressive : quand vous déplacerez le bean du XML vers la configuration Java, vous pourrez conserver le même nom sans casser le reste du code.
Si dans le projet il y a à la fois un sender moderne et un sender legacy, c’est cette explicitation qui est généralement plus utile qu’un @Primary global : ce qui importe n’est pas un « défaut pour toute l’application », mais un chemin de migration spécifique.
Et encore un détail qui embrouille souvent les débutants. Si vous aviez auparavant un bean nommé notificationSender et que dans le XML vous déclarez par inadvertance <bean id="notificationSender" ...>, vous pourriez obtenir soit un conflit, soit une « redéfinition silencieuse » (selon les réglages). C’est pourquoi il est extrêmement utile pour le legacy de donner des noms explicites avec un préfixe : legacyNotificationSender, legacyAuditWriter, etc. Ce n’est pas de l’esthétique gratuite, c’est une protection contre les collisions accidentelles.
8. Erreurs typiques lors de la lecture et de l’intégration du XML legacy
Les erreurs dans le XML legacy sont rarement « de syntaxe ». La syntaxe se corrige en une minute : l’IDE la souligne, le XSD la suggère. Les vrais problèmes commencent quand le XML est intégré de telle sorte que le wiring cesse d’être lisible et que le conteneur commence à s’assembler « au hasard ». Voici les pièges les plus fréquents que j’ai vus chez les débutants (et quelques fois chez moi, mais c’était dans une autre vie).
Erreur n°1 : lire le XML comme du texte, sans construire de carte des beans.
Quand vous essayez de « comprendre le fichier dans son ensemble », vous vous noyez dans les attributs et les espaces, et votre cerveau cesse de voir l’essentiel. Il est bien plus rapide de d’abord lister tous les <bean id="..."> et seulement ensuite d’entrer dans chacun pour regarder les dépendances. Cela transforme le chaos en une liste d’objets.
Erreur n°2 : ne pas remarquer que ref est le nom du bean, pas le nom de la classe.
ref="messageSource" signifie « trouve le bean nommé messageSource ». Ce n’est pas de la « magie », et ce n’est pas de l’« autowiring par type ». Si plusieurs candidats existent dans le projet, le nom devient critique. Si ce nom n’existe pas, le conteneur échouera honnêtement et bruyamment – et c’est bien, car fail-fast préserve les nerfs.
Erreur n°3 : intégrer le XML et oublier qu’il vit désormais dans le conteneur commun.
Après @ImportResource, les beans du XML ne sont pas « à côté ». Ils participent à la résolution commune des candidats, peuvent provoquer des ambiguïtés, peuvent entrer en collision par leur nom, peuvent soudainement devenir des dépendances d’autres beans. Ce n’est pas une raison pour craindre @ImportResource, c’est une raison pour garder le fragment legacy petit et explicitement nommé.
Erreur n°4 : mélanger le fragment legacy et le scanning de sorte que la frontière disparaisse.
La variante la plus sournoise est quand le XML semble « petit », mais qu’il contient context:component-scan base-package="com.example.contextflow". Formellement, cela fonctionne, mais en pratique cela transforme le fragment en « point d’assemblage de tout le projet », et personne ne comprend d’où vient tel ou tel bean. Si le scanning est vraiment nécessaire, il doit être étroit et pertinent, et idéalement, il ne devrait pas du tout faire partie d’un petit îlot legacy.
Erreur n°5 : migrer vers la configuration Java sans préserver le comportement.
Il est très tentant de réécrire le XML en @Bean et de se réjouir. Mais la migration réelle consiste à « déplacer un bean, conserver son nom, conserver ses dépendances, conserver son cycle de vie, vérifier le lancement ». Si lors du déplacement vous changez accidentellement le nom du bean ou la méthode d’injection, vous pouvez casser la moitié du projet, et le plus désagréable est de ne pas comprendre immédiatement pourquoi.
GO TO FULL VERSION