CodeGym /Cours /Spring Core /Bean par défaut : @Primary et @Fallback

Bean par défaut : @Primary et @Fallback

Spring Core
Niveau 8, Leçon 1
Disponible

1. Conflit de candidats et choix par défaut

Lorsque vous attrapez pour la première fois NoUniqueBeanDefinitionException, un désir très humain surgit : « Choisis n’importe quoi, je dois lancer le projet, je verrai plus tard ». Ce sentiment est familier à chaque développeur, surtout le lundi matin. Mais c’est à ce moment précis que Spring nous offre un cadeau : il nous oblige à formuler une convention — ce qui est considéré comme le variant principal et ce qui est considéré comme le variant de secours.

Imaginez que vous ayez une interface NotificationSender et que vous ayez ajouté deux implémentations : EmailNotificationSender et SmsNotificationSender. Les deux sont honnêtement enregistrées en tant que beans. Le conteneur voit : « correspond au type — correspond. » Mais lorsque le service demande un seul NotificationSender, le conteneur n’a pas le droit de choisir au hasard : sinon le comportement de l’application dépendra de l’ordre de scan des classes, des détails internes de construction et de la phase de la Lune (en production, elle aime particulièrement la pleine lune).

Reprenons intentionnellement le simple variant « one-sender » de NotificationDispatchService : il sert uniquement de point d’injection pur, sur lequel le comportement du choix par défaut est visible.

Voici une illustration minimale du « point sensible » — le service ne fait rien de suspect, il veut simplement un objet :

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationSender sender) {
        // Spring injecte ici un bean de type NotificationSender.
        // Si plusieurs candidats existent sans règle de choix -> NoUniqueBeanDefinitionException.
        this.sender = sender;
    }
}

Si NotificationSender est enregistré dans le conteneur en deux exemplaires, Spring doit dire : « D’accord, mais lequel ? ». Et c’est là qu’apparaissent les deux outils que nous allons étudier aujourd’hui : @Primary et @Fallback. Ils aident à décrire le choix « normal » et le variant « de réserve » au niveau du conteneur, et non via des if/else à l’intérieur des méthodes métier.

2. @Primary : le variant principal

Avec @Primary, nous résolvons un problème très quotidien : « Dans la plupart des endroits de l’application, j’ai besoin de ce variant, sans précisions supplémentaires. » Cela ressemble au paramétrage du « navigateur par défaut » dans un système d’exploitation. Vous pouvez avoir cinq navigateurs installés, mais si vous cliquez simplement sur un lien, le système doit en choisir un : le principal. @Primary est le même réglage, mais pour le conteneur Spring.

Le sens est simple : si plusieurs beans correspondent par type mais qu’un seul est nécessaire, et que l’un d’eux est marqué @Primary, alors Spring choisit celui-là. Les autres beans ne disparaissent pas : ils restent dans le conteneur et peuvent être utilisés là où le choix est explicite ou là où le service a besoin de l’ensemble des implémentations.

On peut montrer @Primary de deux manières naturelles : sur un composant de classe ou sur une méthode @Bean dans une configuration. Le sens est le même — le marquage entre dans la définition du bean.

2.1. @Primary sur un composant

Dans notre projet ContextFlow, il est logique de faire du canal de notification le plus « sûr » le principal — la console. Pour une application non-web pédagogique, c’est un choix par défaut idéal : toujours fonctionnel, ne nécessite pas d’infrastructure externe et ne force pas l’étudiant à configurer SMTP au milieu de la journée.

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Primary // Ce bean sera choisi par défaut si plusieurs candidats existent
@Component
public class ConsoleNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        // Canal le plus "sûr" : toujours disponible, ne nécessite pas d'infrastructure
        System.out.println("CONSOLE: " + message); // CONSOLE: Order created
    }
}

Maintenant, si vous avez aussi EmailNotificationSender et SmsNotificationSender, et qu’un service demande simplement un NotificationSender, le conteneur choisira ConsoleNotificationSender parce qu’il est « principal ».

2.2. @Primary sur une méthode @Bean

Parfois, il est plus pratique de définir l’implémentation non pas comme un @Component séparé, mais comme un @Bean (par exemple, parce que vous devez configurer finement l’objet, passer des paramètres, assembler une chaîne de dépendances, etc.). Dans ce cas, @Primary est placé sur la méthode :

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class NotificationConfig {

    @Bean
    @Primary // Par défaut, choisir ce NotificationSender
    NotificationSender consoleNotificationSender() {
        // Ici, nous enregistrons le bean manuellement, non via @Component
        return message -> System.out.println("CONSOLE: " + message);
    }
}

Notez une nuance importante : le service métier ne change pas du tout. Nous ne touchons pas à NotificationDispatchService, n’insérons pas de if/else, ne créons pas de dépendances à la main. Nous disons simplement au conteneur : « Quand on ne précise pas, prends celui-ci. »

Si vous voulez fixer mentalement l’idée : @Primary est la « voie principale » que Spring choisit par défaut lorsque le type correspond à plusieurs beans.

Et là, Spring se comporte à nouveau comme un adulte. Si vous marquez @Primary sur deux implémentations du même type, le conteneur ne choisira pas « le plus primaire ». Il échouera au démarrage avec une erreur claire : « plusieurs beans primary trouvés. » C’est exactement ce que nous voulons : mieux vaut que l’application ne démarre pas, plutôt que de démarrer avec un choix imprévisible.

Une réflexion pratique importante : @Primary est une convention globale. Si vous voulez qu’à un endroit on utilise un variant et à un autre un autre, ce n’est plus une histoire de « default ». Il faut alors un choix explicite au point d’injection, et non un autre default aléatoire.

3. @Fallback : le variant de secours

Si @Primary répond à la question « que prendre normalement ? », alors @Fallback répond à une question plus subtile : « Que peut-on prendre s’il n’y a pas d’autre candidat normal ? ». Cela ressemble à une roue de secours dans le coffre : vous ne la choisissez pas à la place de la roue normale si tout va bien, mais elle sauve la mise quand sinon vous ne pouvez pas rouler du tout.

Le sens de @Fallback (en formulation humaine) : si parmi les candidats il y a au moins un bean sans @Fallback, alors les beans fallback sont considérés comme « de seconde classe » et perdent. Si, en revanche, il s’avère qu’il n’y a que des candidats de type fallback, alors le conteneur peut choisir un fallback (s’il est seul) et poursuivre l’assemblage.

On pourrait penser « c’est comme @Primary, mais inversé », mais il est important de saisir la différence : @Primary est un vainqueur explicitement désigné. @Fallback est un candidat qui accepte d’être choisi seulement s’il n’y a personne d’autre.

3.1. No-Op / No-Discount comme filet de sécurité

Dans ContextFlow, l’exemple des remises est parfait. Imaginons que nous ayons une politique de remise « réelle » (par exemple pour les clients fidèles) et une politique « zéro » qui ne change rien. La politique zéro est une excellente réserve : si pour une raison quelconque nous n’avons pas branché la politique réelle, le système peut quand même calculer le montant de la commande.

import org.springframework.stereotype.Component;

@Component
public class LoyalCustomerDiscountPolicy implements DiscountPolicy {
    @Override
    public int apply(int total) {
        // Politique "réelle" : modifie le prix (fait de manière très pédagogique)
        return total - 10; // réduction très pédagogique, ne demandez pas "pourquoi 10"
    }
}

Ajoutons maintenant un variant fallback :

import org.springframework.context.annotation.Fallback;
import org.springframework.stereotype.Component;

@Fallback // Ce bean ne sera choisi que s'il n'y a pas d'autre candidat DiscountPolicy
@Component
public class NoDiscountPolicy implements DiscountPolicy {
    @Override
    public int apply(int total) {
        // Variant de secours : ne change rien, mais permet à l'application de démarrer
        return total;
    }
}

Si le conteneur contient LoyalCustomerDiscountPolicy, il sera choisi (car il n’est pas fallback). NoDiscountPolicy en tant que fallback reste sur le banc de touche. Mais si vous supprimez la politique réelle (ou ne l’enregistrez pas temporairement), le fallback devient utile : le conteneur pourra quand même injecter DiscountPolicy, et l’application démarrera.

Ici, les débutants espèrent souvent de la magie : « Eh bien, puisqu’il y a un fallback, que Spring choisisse n’importe quel candidat normal. » Non. Si vous avez deux candidats non-fallback (par exemple, EmailNotificationSender et SmsNotificationSender), et que ConsoleNotificationSender est marqué @Fallback, le conteneur ne pourra toujours pas choisir entre email et sms. Le fallback sera simplement éliminé de la course, mais cela ne créera pas de vainqueur.

Donc @Fallback n’est pas un outil de choix entre plusieurs implémentations équivalentes. C’est un outil pour « donner un variant de secours », qui ne gêne pas quand il y a des candidats normaux.

4. Algorithme de sélection : @Primary et @Fallback

Tant qu’on n’a pas vu plusieurs combinaisons sur une seule image, le cerveau aime se mélanger : « Primary est un default, fallback est aussi un default, mais comment ? ». Faisons une mini-carte des décisions. Nous n’entrons pas dans l’algorithme complet de résolution, il nous faut un niveau pédagogiquement honnête : ce qui se passe dans les situations typiques quand un service demande un seul bean d’un type d’interface.

Ci-dessous, un tableau « ce qui se passe si… ». Considérons que nous injectons DiscountPolicy dans un service via son constructeur.

Candidats dans le conteneur Que choisit Spring Pourquoi
1 bean normal Ce bean Aucune ambiguïté
2 beans normaux Erreur Impossible de deviner
1 @Primary + n’importe quels autres @Primary Vainqueur explicitement désigné
2 @Primary Erreur Trop de « principaux »
1 normal + 1 @Fallback Normal Fallback cède
Seulement 1 @Fallback Ce fallback Personne à qui céder, mais un seul candidat
2 @Fallback Erreur Personne à qui céder, mais plusieurs candidats

Et pour bien ancrer le processus, voici un organigramme. Il contient exactement ce dont nous avons besoin aujourd’hui, sans complications futures.

flowchart TD
    %% Schéma simplifié : uniquement le cas "un seul bean par type requis"
    A["Un seul bean de type T requis"] --> B["Candidats trouvés par type"]
    B -->|0| E["Erreur : pas de bean du type requis"]
    B -->|1| F["Choisir l'unique candidat"]
    B -->|>1| C["Y a-t-il exactement un @Primary ?"]
    C -->|oui| G["Choisir @Primary"]
    C -->|non| D["Y a-t-il des candidats non-fallback ?"]
    D -->|oui| H["Ignorer les fallbacks, mais si reste >1 -> erreur"]
    D -->|non| I["Tous candidats fallback : si >1 -> erreur, si 1 -> choisir"]

L’idée principale de ce schéma est la suivante : @Primary aide à « choisir un vainqueur », tandis que @Fallback aide à « ne pas gêner le vainqueur » tout en ayant un variant de secours si le vainqueur n’existe pas.

5. Incrément dans ContextFlow

Faisons maintenant l’exercice le plus important du jour — dans la tête, et mieux encore dans le code du projet : nous ajoutons plusieurs implémentations d’interface, mais sans transformer les services en usines. C’est-à-dire que OrderPlacementService, NotificationDispatchService et OrderPricingService restent simples : ils sont dédiés au scénario métier, et non au choix d’implémentations spécifiques.

5.1. Canal de notification par défaut via @Primary

Supposons que NotificationDispatchService dépende toujours intentionnellement d’un seul NotificationSender. Sur un tel contrat, il est plus facile de voir comment le conteneur applique le candidat par défaut sans forcer le service à choisir l’implémentation manuellement.

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationSender sender) {
        this.sender = sender;

        // Affichage de diagnostic pratique : aide à voir ce qui a été injecté
        System.out.println("Sender = " + sender.getClass().getSimpleName());
        // Sender = ConsoleNotificationSender
    }
}

Ce qui est important ici, c’est une seule chose : le service demande un seul sender, et la règle de choix vit à l’extérieur.

Si vous ajoutez plusieurs sender et marquez celui de la console comme @Primary, vous n’aurez pas à modifier le code du service du tout. C’est le critère le plus important de la santé architecturale : le choix de l’implémentation se fait « à l’extérieur », dans la couche de câblage (wiring).

5.2. DiscountPolicy : variant principal + filet de sécurité fallback

Supposons que OrderPricingService demande un seul DiscountPolicy. Nous voulons qu’en présence d’une politique de remise « intelligente », elle gagne, mais que son absence ne transforme pas le projet en brique.

import org.springframework.stereotype.Service;

@Service
public class OrderPricingService {
    private final DiscountPolicy discountPolicy;

    public OrderPricingService(DiscountPolicy discountPolicy) {
        // Ici aussi, un seul bean DiscountPolicy est injecté
        this.discountPolicy = discountPolicy;
    }

    public int price(int total) {
        // Toute la logique de choix d'implémentation est déportée dans le conteneur,
        // le service applique simplement le contrat
        return discountPolicy.apply(total);
    }
}

Ensuite, nous fournissons simplement les règles de choix au conteneur : la politique « intelligente » est normale, la « zéro » est fallback. C’est tout. Encore une fois, le service ne change pas.

5.3. Où cela est particulièrement agréable en pratique

Lorsque vous commencez à développer un projet, de telles « valeur par défaut » deviennent nombreuses : parfois un canal de notification « normal », parfois une politique de remise « normale », parfois un writer d’audit « normal ». Si chaque service choisit lui-même son implémentation, vous obtenez un chaos de câblage dispersé dans tout le projet. Si, en revanche, vous définissez les defaults et les fallbacks via @Primary et @Fallback, vous obtenez deux bénéfices à la fois : les services sont plus lisibles, et le conteneur reste le point central d’assemblage de l’application.

Et oui, c’est le moment où Spring cesse d’être « magique » pour devenir simplement un assemblage discipliné du graphe d’objets : vous dites vous-même ce qui est principal et ce qui est de secours. Spring veille simplement à ce que vous ne soyez pas en contradiction avec vous-même.

6. Erreurs typiques lors de l’utilisation de @Primary et @Fallback

Erreur n°1 : mettre @Primary « au cas où » et perdre en lisibilité.
Si on ne vérifie pas où le default est utilisé, on peut aboutir à une situation où un nouveau développeur ouvre le service et ne comprend pas quelle implémentation est réellement injectée, car le choix est caché quelque part dans les profondeurs du projet. @Primary est bon quand le choix est vraiment massif et évident, pas quand vous « ne voulez pas réfléchir. »

Erreur n°2 : marquer deux beans comme @Primary et s’étonner que ce soit pire.
Intuitivement, on se dit : « J’ai deux principaux — que Spring en choisisse un. » Mais le conteneur ne fonctionne pas ainsi (et heureusement). Deux primary restent une ambiguïté, simplement sous une forme plus officielle. En conséquence, l’application échoue au démarrage, et c’est correct : un type doit avoir un seul vainqueur par défaut explicite.

Erreur n°3 : s’attendre à ce que @Fallback résolve un conflit entre deux implémentations normales.
@Fallback n’est pas un « primary faible » ni un « choisis n’importe quel normal ». C’est exactement un variant de secours. Si vous avez deux candidats normaux, le conteneur ne sait toujours pas lequel choisir. Dans une telle situation, fallback perdra assurément, mais ne créera pas de vainqueur.

Erreur n°4 : soigner l’ambiguïté en revenant au new à l’intérieur du service.
C’est l’erreur la plus « émotive » : vous voyez une stack trace rouge et vous pensez « Zut, je vais juste créer un new SmsNotificationSender() et on continue. » Hélas, c’est un pas en arrière vers un câblage manuel et des dépendances cachées. Le choix de l’implémentation doit être conteneurisé, sinon l’injection de dépendances devient une décoration, pas une architecture.

Erreur n°5 : essayer d’utiliser @Primary/@Fallback pour gérer des collections de beans.
Parfois, un développeur injecte List<NotificationSender> et s’attend à ce que le primary soit « premier » ou que le fallback « n’apparaisse pas dans la liste ». Il ne faut pas penser ainsi : @Primary et @Fallback résolvent le problème « un seul bean est nécessaire ». Si vous avez besoin d’un ensemble, c’est un contrat de dépendance distinct, et il doit être conçu séparément, sans espérer que primary commence soudain à gérer l’ordre et la composition de la collection.

1
Mission
Spring Core, niveau 8, leçon 1
Bloqué
Expéditeur principal via `@Primary`
Expéditeur principal via `@Primary`
1
Mission
Spring Core, niveau 8, leçon 1
Bloqué
Politique de réserve via `@Fallback`
Politique de réserve via `@Fallback`
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION