1. Rôle de BeanPostProcessor
Si vous venez tout juste de vous familiariser avec l’injection de dépendances, vous pourriez avoir l’impression : « Bon, le conteneur sait créer des objets et lier les dépendances — que faut‑il de plus ? » En pratique, on souhaite bien plus : que le conteneur puisse intervenir au moment de la naissance de chaque bean, comme sur une chaîne de montage où existent des postes de contrôle qualité, d’emballage et d’étiquetage.
BeanPostProcessor est justement un tel poste sur la chaîne. Le conteneur a déjà créé l’objet, injecté les dépendances, mais ne le considère pas encore comme complètement prêt. À ce moment, Spring vous offre deux « fenêtres » autour de la phase init : l’une avant l’initialisation, l’autre après. Dans ces fenêtres, vous pouvez réaliser des opérations techniques qu’il serait très lourd de recopier manuellement dans toutes les classes.
Il est important de fixer d’emblée la philosophie : BeanPostProcessor est un mécanisme infrastructurel du conteneur, et non « un endroit de plus pour la logique métier ». Si vous commencez à y placer des règles métier sur les commandes (« si le montant dépasse 1000, une remise de 5 % »), vous obtiendrez une application impossible à lire sans connaître les rouages internes du conteneur. En revanche, utilisez BPP pour du diagnostic, des enveloppes techniques, un réglage précis et de l’intégration : il devient alors un outil puissant qui explique la moitié de la « magie » de Spring.
2. BeanPostProcessor dans le cycle de vie
Pour que BeanPostProcessor cesse d’être une abstraction, il faut le placer sur la ligne de temps du cycle de vie du bean. C’est un bon moment pour se souvenir du Niveau 10 : nous y avons déjà dit qu’un bean connaît « naissance », « initialisation », « prêt », « destruction ». BPP s’intègre exactement dans la partie où le conteneur amène l’objet à son état final.
Regardons un schéma simplifié (sans les détails rares, mais avec le bon sens) :
flowchart TD BD["BeanDefinition
description"] --> C["constructor
création de l'objet"] C --> DI["dependency injection
injection des dépendances"] DI --> B1["BPP beforeInit
postProcessBeforeInitialization"] B1 --> INIT["init callbacks
@PostConstruct / initMethod / InitializingBean"] INIT --> B2["BPP afterInit
postProcessAfterInitialization"] B2 --> READY["bean prêt à l'emploi"]
Le sens est le suivant : postProcessBeforeInitialization est appelé après l’injection des dépendances mais avant @PostConstruct et les autres init‑callbacks. Et postProcessAfterInitialization intervient après que le bean a franchi la phase init et « est officiellement vivant ».
Et voici un détail capital qui fait souvent perdre la tête aux débutants. Les méthodes de BPP renvoient un Object. Ce n’est pas un hasard ni « juste pour faire joli ». Le conteneur vous dit : « Tiens, voici l’instance courante. Tu veux ? Renvoie‑la telle quelle. Tu veux ? Renvoie un autre objet (une enveloppe). Mais ensuite je travaillerai avec ce que tu auras renvoyé. »
Pour bien marquer la différence avec BFPP, un tableau comparatif est utile. Le tableau vaut mieux qu’une liste : moins d’emphase, plus de sens.
| Mécanisme | Quand s’exécute‑t‑il | Avec quoi travaille‑t‑il | Ce qu’il modifie réellement | Sens typique |
|---|---|---|---|---|
| BeanFactoryPostProcessor | avant la création des singletons ordinaires | BeanDefinition et métadonnées | les « plans » : scope, lazy‑init, valeurs de propriétés au niveau de la description | vérifications fail‑fast et corrections de la structure du contexte |
| BeanPostProcessor | autour de la phase init de chaque instance de bean | l’objet réel + beanName | l’objet qui sera placé plus loin dans le conteneur (y compris les enveloppes) | traitement technique des instances, diagnostic, décoration |
Si vous conservez ce tableau en tête, la confusion « pourquoi ne pas simplement prendre un bean et appeler une méthode dans BFPP ? » disparaît d’elle‑même : BFPP vit dans le monde des plans, BPP dans celui des pièces assemblées.
3. Un BeanPostProcessor minimal : ordre des phases
Je voudrais maintenant un exemple aussi « honnête » que possible, pour que vous voyiez : BPP n’est pas appelé « un jour », mais à un moment très précis. Un simple diagnostic convient parfaitement : afficher dans la console les messages « before init » et « after init » pour certains beans choisis.
Dans ContextFlow, nous ne voulons pas journaliser tout sans discrimination (ce serait trop bruité) ; nous allons donc filtrer. Le filtre le plus simple pour un projet d’apprentissage est le nom du bean : par exemple, suivre tout ce qui se termine par Service.
package com.example.contextflow.support.postprocessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class TrackedComponentBeanPostProcessor implements BeanPostProcessor {
@Override public Object postProcessBeforeInitialization(Object bean, String name) {
// Filtrons les beans pour ne pas se noyer dans les logs
// (le contexte contient beaucoup de beans d'infrastructure Spring)
if (name.endsWith("Service")) System.out.println("Before init: " + name); // Before init: orderPlacementService
// Important : on renvoie l'objet au conteneur (ou l'enveloppe si nous l'avions faite)
return bean;
}
@Override public Object postProcessAfterInitialization(Object bean, String name) {
// Même logique : on intervient après la phase init, mais on ne touche pas aux autres beans
if (name.endsWith("Service")) System.out.println("After init: " + name); // After init: orderPlacementService
// Il faut aussi obligatoirement renvoyer l'objet final
return bean;
}
}
Remarquez deux choses. Premièrement, nous renvoyons toujours bean. C’est important : le conteneur a besoin de l’objet final. Deuxièmement, le filtre est le plus étroit possible, sinon nous serions vite noyés dans les logs et ne comprendrions plus rien.
Ce BPP doit maintenant être enregistré. Nous avons déjà convenu que ces mécaniques vivent dans support.postprocessor et sont branchées via config.*. Pour une démonstration locale, un petit BppDemoConfig suffit : il montre que BPP est un bean ordinaire que le conteneur récupère à la phase appropriée.
package com.example.contextflow.config.core;
import com.example.contextflow.support.postprocessor.TrackedComponentBeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BppDemoConfig {
@Bean
public TrackedComponentBeanPostProcessor trackedComponentBeanPostProcessor() {
// BeanPostProcessor est aussi un bean : il suffit de l'enregistrer dans le contexte
return new TrackedComponentBeanPostProcessor();
}
}
Si vous lancez ensuite l’application (le contexte se monte, les singletons sont créés), vous verrez que les messages « Before init » apparaissent avant la fin de la phase init, et « After init » après. Dans la sortie réelle, il y aura beaucoup de lignes, mais l’ordre pour un bean donné correspondra à la logique de la chaîne.
Et là vient généralement la question : « Où est @PostConstruct ? » Excellente question, que nous pouvons illustrer avec un mini‑fragment du déjà connu NotificationTemplateCatalog (c’est un bean lifecycle‑managed du Jour 10). Supposons qu’il contienne cette méthode :
import jakarta.annotation.PostConstruct;
@PostConstruct
void loadTemplates() {
// Ce callback doit se situer entre beforeInit et afterInit du BeanPostProcessor
System.out.println("Templates loaded"); // Templates loaded
}
Si votre BPP est configuré pour suivre aussi ce bean (par exemple, ajoutez temporairement la condition || name.equals("notificationTemplateCatalog")), alors l’ordre sera : d’abord « Before init … », puis « Templates loaded », puis « After init … ». C’est la preuve directe que BPP encadre l’étape d’init.
4. Substitution d’un bean par un décorateur
L’idée même que « BPP peut renvoyer un autre objet » semble à la fois puissante et un peu effrayante. Faisons donc un exemple très concret : pas d’AOP, pas de proxy, pas de magie noire, mais un simple décorateur Java.
Dans ContextFlow, nous avons l’interface NotificationSender. Représentons‑la simplement :
package com.example.contextflow.domain.ports;
public interface NotificationSender {
// Contrat du port : l'implémentation concrète peut être quelconque, mais la méthode d'envoi est commune
void send(String message);
}
Nous voulons ajouter un comportement purement technique : imprimer une ligne de diagnostic avant l’envoi de la notification (par exemple, pour voir que les notifications sont bien envoyées et quels messages circulent). Et nous ne voulons pas réécrire chaque implémentation (EmailNotificationSender, SmsNotificationSender, ConsoleNotificationSender) en y ajoutant un System.out.println.
Créons l’enveloppe :
package com.example.contextflow.support.postprocessor;
import com.example.contextflow.domain.ports.NotificationSender;
public final class TrackingNotificationSender implements NotificationSender {
private final NotificationSender delegate; // L'expéditeur original, qui sait déjà faire le vrai travail
public TrackingNotificationSender(NotificationSender delegate) { this.delegate = delegate; }
@Override public void send(String message) {
// Diagnostic technique : on ajoute un comportement sans modifier les implémentations d'origine
System.out.println("[tracked] " + message); // [tracked] Order created: A-101
delegate.send(message); // On délègue ensuite à l'implémentation « réelle »
}
}
C’est un décorateur classique : il implémente la même interface et délègue à l’expéditeur « réel ». Voici maintenant la partie intéressante : forçons le conteneur à substituer le bean par cette enveloppe lors de la phase postProcessAfterInitialization.
package com.example.contextflow.support.postprocessor;
import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class TrackedComponentBeanPostProcessor implements BeanPostProcessor {
@Override public Object postProcessAfterInitialization(Object bean, String name) {
// On ne wrappe que le type nécessaire pour ne pas interférer avec tous les beans
if (bean instanceof NotificationSender sender && !(bean instanceof TrackingNotificationSender)) {
// Protection contre le double wrapping : si c'est déjà TrackingNotificationSender, on ne touche pas
return new TrackingNotificationSender(sender);
}
// Si la condition n'est pas remplie, on renvoie l'objet original
return bean;
}
}
La logique est petite, mais le sens est immense. Si le conteneur a créé un NotificationSender, après la phase init nous renvoyons non pas l’objet original, mais une enveloppe. Dès lors, tous ceux qui demandent un NotificationSender (via DI) recevront non pas l’implémentation « pure », mais l’implémentation avec suivi.
Voici une réflexion méthodologique importante : BPP modifie le comportement de l’application non pas en changeant la classe d’origine, mais en remplaçant l’objet qui vit dans le contexte. De l’extérieur, tout se passe comme si vous aviez « ajouté » du comportement. En réalité, vous avez simplement glissé une enveloppe, et ce de manière centralisée.
Si maintenant, quelque part dans l’application, on appelle :
notificationSender.send("Order created: A-101"); // À l'exécution, il peut déjà s'agir d'un appel sur le décorateur, et non sur l'implémentation « pure »
la sortie pourrait être :
[tracked] Order created: A-101
...ensuite la sortie de l'implémentation spécifique (par exemple ConsoleNotificationSender)
Et cela, au niveau le plus basique, explique comment Spring est capable d’« ajouter du comportement » aux beans. Nous n’approfondissons pas la façon exacte dont Spring fabrique de grandes enveloppes universelles — c’est un sujet pour les prochains jours. Pour l’instant, il suffit de comprendre le principe : le conteneur décide quel objet est réellement considéré comme le bean.
5. Intégration de BPP dans ContextFlow
Il est très facile d’écrire un BeanPostProcessor qui « fonctionne d’une certaine manière ». Il est bien plus difficile de le faire de sorte que, dans une semaine, vous ne vous disiez pas : « Qui a écrit ça ? Ah, c’est moi… bon, alors c’est encore plus gênant. »
Dans ContextFlow, nous maintenons une discipline : les post‑processors vivent dans support.postprocessor et sont branchés via config.*. La couche métier (domain, application) ne doit pas savoir que nous avons des BPP. Si les services métier commencent à dépendre du « fait d’être enveloppés », vous courez le risque de rendre le domaine dépendant du framework (et vous soignerez cela dans les prochains cours).
Techniquement, l’enregistrement reste le même que celui de BppDemoConfig ci‑dessus : BPP est aussi un bean. Dans le projet, l’important est autre : le garder dans un module de configuration séparé et ne pas transporter cette mécanique dans la couche métier.
C’est justement là que Spring gagne souvent : une mécanique interne complexe est branchée par un petit code de connexion, lisible, honnête.
6. Subtilités de l’utilisation de BeanPostProcessor
Quand on obtient un mécanisme qui s’exécute sur chaque bean et peut potentiellement remplacer n’importe quel objet, c’est un peu comme obtenir les droits d’administrateur sur un système d’exploitation. C’est génial, mais les conséquences des erreurs sont aussi « administrateur ».
La subtilité la plus importante : BPP est appelé pour un très grand nombre de beans. Votre contexte ne contient pas seulement les beans de l’application, mais aussi les beans d’infrastructure de Spring. Par conséquent, toute action « lourde » dans BPP (calculs coûteux, lecture de fichiers, appels réseau, tentatives d’accès à la base – même si ici nous n’avons pas de base) dégradera fortement le démarrage et rendra le comportement opaque.
Deuxième subtilité : l’objet renvoyé doit rester compatible avec le contrat. Si toutes les dépendances attendent l’interface NotificationSender, renvoyer un décorateur qui implémente NotificationSender est sûr. Mais si du code s’attend à une classe concrète (par exemple ConsoleNotificationSender) et que vous renvoyez une enveloppe d’une autre classe, alors instanceof ConsoleNotificationSender cessera soudainement de fonctionner. Ce n’est pas un « bug de Spring », c’est la conséquence du fait que vous avez vous‑même changé l’objet. C’est une raison de plus pour que, dans une conception saine, les dépendances soient faites sur des interfaces et non sur des classes concrètes.
Troisième subtilité : les BPP forment une chaîne. C’est‑à‑dire que vous pouvez avoir plusieurs BeanPostProcessor, chacun recevant le résultat du précédent. Cela signifie que le « bean » dans la méthode d’un BPP n’est pas forcément « l’objet d’origine », mais parfois déjà une enveloppe de quelqu’un d’autre. Dans un projet d’apprentissage, nous essayons de garder cela simple : un BFPP, un BPP. Mais il est utile d’avoir ce modèle mental dès maintenant.
Pour ne pas transformer cela en une liste d’« interdictions », je présente un petit tableau « adéquat / dangereux » comme repère lorsque vous concevez un BPP :
| Idée | Dans ContextFlow d’apprentissage | Pourquoi |
|---|---|---|
| Afficher un diagnostic (filtre étroit) | adéquat | aide à visualiser le pipeline du conteneur |
| Décorer un bean d’interface (comme NotificationSender) | adéquat, si limité | démontre la substitution d’objet sans casser le domaine |
| Traiter tous les beans sans filtre | dangereux | bruit, ralentissements, difficile à déboguer |
| Cacher la logique métier dans un BPP | dangereux | le comportement de l’application devient « invisible » dans le code des services |
| Injecter des dépendances dans le BPP qui pourraient créer des cycles | dangereux | peut casser le démarrage du contexte de manière très non‑triviale |
| Faire du BPP un « framework universel dans le framework » | dangereux | le cours porte sur la compréhension de Spring, pas sur l’écriture de votre propre Spring |
Si vous gardez ces simples repères en tête, BeanPostProcessor ne semblera plus magique, mais deviendra un point d’extension honnête.
7. Erreurs typiques avec BeanPostProcessor
Erreur n°1 : oublier de renvoyer le bean depuis postProcessBeforeInitialization(...) / postProcessAfterInitialization(...).
BeanPostProcessor n’est pas un écouteur d’événements qui « se contente de notifier ». Il fait partie de la chaîne et le conteneur attend de lui l’objet final. Si vous renvoyez null ou oubliez accidentellement le return, vous obtiendrez un comportement étrange et un diagnostic très désagréable. Dans un projet d’apprentissage, mieux vaut toujours commencer par « renvoyer tel quel », puis ajouter la décoration.
Erreur n°2 : traiter tous les beans à la suite et se noyer dans le bruit.
Le débutant veut « tout voir », donc il imprime des lignes pour chaque bean. Puis il voit 200 lignes de logs parmi lesquelles il est impossible de trouver ses services. Il est préférable de choisir un critère étroit : par nom (*Service), par paquetage (com.example.contextflow.*) ou par interface (NotificationSender). Ainsi BPP reste un outil et non un générateur de chaos.
Erreur n°3 : substituer un bean par une enveloppe qui ne correspond pas au type de la dépendance.
Le scénario le plus fréquent : vous renvoyez un objet qui n’implémente pas l’interface attendue, ou vous renvoyez une enveloppe alors que la dépendance attendait une classe concrète. En conséquence, la DI peut se casser, et parfois la logique se brise (par exemple, instanceof dans le diagnostic). Cela se soigne par la discipline : les dépendances sur des interfaces, les enveloppes compatibles.
Erreur n°4 : transférer la logique métier dans le BPP « parce que c’est pratique ».
Il est parfois tentant de se dire : « Et si nous vérifions directement dans le BPP quelque chose de domaine ou corrigions un état ? » Le problème est que le BPP s’exécute au démarrage, dans la couche d’infrastructure. Les règles métier doivent vivre dans le code métier, sinon vous obtenez une application dont le comportement ne peut être compris en lisant les services : la moitié du comportement est cachée dans les hooks du conteneur.
Erreur n°5 : rendre le BPP dépendant de la moitié du contexte.
Techniquement, BPP est un bean et on peut aussi lui injecter des dépendances. Mais si vous commencez à tirer beaucoup de services dans le BPP, vous arrivez facilement à des cycles, une initialisation précoce et un « tout est cassé mais on ne sait pas pourquoi ». Dans ContextFlow d’apprentissage, le BPP doit rester simple : au maximum, un diagnostic étroit et une démonstration d’enveloppe.
GO TO FULL VERSION