Skip to content

Injection de dépendance

Sélection statique d'implémentation

Dans le cas où un service (interface) a plusieurs implémentations, pour spécifier quelle implémentation est attendue, l'annotation @Qualifier est utilisée.

1
2
3
public interface MockInterface {
    public String doSomething();
}
1
2
3
4
@Service
public class MockImpl implements MockInterface {
    public String doSomething() { return "Meow"; }
}
1
2
3
4
@Service
public class SecondMockImpl implements MockInterface {
    public String doSomething() { return "Another Meow"; }
}

Avec le contrôleur suivant, Spring plantera car il n'est pas capable de déterminer quelle implémentation de MockInterface utiliser.

1
2
3
4
5
6
7
8
@Controller
public class HelloController {
    private final MockInterface service;

    public HelloController(MockInterface service) {
        this.service = service;
    }
}

L'attribut @Qualifier("beanName") peut être utilisé pour "indiquer" à Spring quelle implémentation utiliser, où beanName est, par défaut, le nom de la classe en question, avec la première lettre en minuscule.

1
2
3
4
5
6
7
8
@Controller
public class HelloController {
    private final MockInterface service;

    public HelloController(@Qualifier("secondMockImpl") MockInterface service) {
        this.service = service; // instanceof SecondMockImpl
    }
}

De façon simplifiée, si, parmi toutes ces implémentations, une seule est voulue dans le cas "général" (par défaut), il suffit de lui appliquer l'annotation @Primary, qui indique, dans un cas où seule une implémentation dispose de cet attribut, qu'il s'agira de la classe dont Spring doit se servir.

1
2
3
4
5
6
// ...
@Primary
@Service
public class SecondMockImpl implements MockInterface {
    public String doSomething() { return "Another Meow"; }
}
1
2
3
4
5
6
7
8
@Controller
public class HelloController {
    private final MockInterface service;

    public HelloController(MockInterface service) {
        this.service = service; // instanceof SecondMockImpl
    }
}

Il est possible, dans un cas où @Primary n'est pas utilisé, d'automatiquement obtenir instance du bon type en nommant la variable avec la valeur voulue du beanName (ex. private final MockInterface secondMockImpl).

Ça a pour désavantage d'inclure un comportement implicite, et la possibilité de bug dans le cas où @Primary serait utilisé, car @Primary prend la priorité sur le nom de la variable.

A éviter au maximum, donc.

Sélection dynamique d'implémentation

Pour dynamiquement sélectionner une implémentation, on peut utiliser le principe de "profil".

Chaque bean d'implémentation est marqué d'un @Profile("key"), où key est la clé du profil voulu.

Ce bean ne sera que utilisé lorsque le profil key est utilisé.

Contrairement aux qualificateurs (statiques, nécessitant de recompiler le code), on peut décider de changer de profil durant le fonctionnement de notre app, nous permettant de remplacer les implémentations utilisées à l'aide de configuration (par exemple, pour changer des types data sources pour des tests d'intégration).

La différence est que, au lieu de marquer, au niveau de la variable cible, le type attendu, on déclare, au niveau de l'implémentation, le mode visé.

La sélection statique et la sélection dynamique sont deux outils différents avec des use-case différents.

Configuration

Pour configurer le profil utilisé, on utilise la clé spring.profiles.active.

Par exemple,

1
2
3
interface IMock {
    void push(String data);
}
1
2
3
4
5
6
7
@Service
@Profile("test")
class ActionMQImpl implements IMock {
    void push(String data) {
        // Pushing to ActionMQ
    }
}
1
2
3
4
5
6
7
@Service
@Profile("prod")
class RabbitMQImpl implements IMock {
    void push(String data) {
        // Pushing to RabbitMQ
    }
}

La configuration de production aurait donc la clé suivante dans le fichier application.properties: spring.profiles.active=prod.

Pour déclarer qu'une implémentation ne doit pas être utilisée lorsqu'un profil est utilisé, il suffit de préfixer le profil d'un !, par exemple !prod permettra d'utiliser l'implémentation à condition que le profil actif ne soit pas prod.

Bean par défaut

Dans le cas où aucun profil n'a été sélectionné lors de la configuration de spring, tout service déclaré sans profil explicite, ou avec la clé default sera sélectionné.

Si un bean n'est pas explicitement déclaré comme associé à un profil, il est automatiquement associé au profil default.

Si la configuration déclare l'utilisation d'un profil, et que aucune implémentation ne supporte ce profil, Spring ne prendra pas default!

Il plantera simplement en disant ne pas être capable de deviner le profil à utiliser.

Si on veut à la fois associer une clé à une implémentation, et la déclarer comme défaut, on peut passer un tableau à @Profile.

1
2
3
4
5
6
7
@Service
@Profile({"prod", "default"})
class RabbitMQImpl implements IMock {
    void push(String data) {
        // Pushing to RabbitMQ
    }
}

Profil par défaut

La propriété spring.profiles.default permet de définir le profil à utiliser par défaut (quand aucun profil n'est renseigné).

Liste d'implémentations

Il est possible d'obtenir une liste de toutes les implémentations d'une interface, en changeant le type de paramètre (ou d'attribut) en une liste.

Par exemple, IMock devient List<IMock>.

Source d'instance

Une classe marquée par l'annotation @Component, ou par toute autre annotation l'étendant (@Service, etc.), peut être source d'instance, comme vu précédemment.

Additionnellement, il est possible de fournir une instance grâce au retour d'une fonction.

Par exemple, dans le cas où on veut obtenir une instance d'un objet disposant d'un paramètre de type générique, on peut construire cette instance à l'aide de fonctions.

1
2
3
public interface IMock<T extends List> {
    // ...
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class MockFactory {
    @Bean
    public IMock<ArrayList> buildArrayListInstance() {
        // ...
    }

    @Bean
    public IMock<LinkedList> buildLinkedListInstance() {
        // ...
    }
}

Si une autre classe déclare avoir besoin d'une instance utilisant ArrayList, buildArrayListInstance sera appelé et son résultat sera fourni à la classe.

1
2
3
4
5
6
@Component
public class MyComponent {
    public MyComponent(IMock<ArrayList> arrayInstance) {
        // ...
    }
}

Note: L'injection ici suit un principe de localisation de service: "Si j'ai une instance en cache, je la fournis, sinon je recherche et j'obtiens une instance".