[Bonne pratique] Utilisez l’injection par constructeur avec Spring

Les vieilles habitudes ont la vie dure, mais il faut leur faire la peau
  EN
 Lecture : 5 min
 chop
 Pas encore commenté
  (MàJ : 2021-02-28)

Si vous utilisez Spring depuis longtemps, ou si vous copiez-collez quelques tutos ou solutions trouvés sur la toile, vous avez certainement quelques champs privés annotés avec @Autowired. Ça fonctionne, mais il y a une meilleure façon de faire.

La version courte #

N’utilisez pas @Autowired, créez un constructeur qui prend en paramètres toutes les dépendances requises.

Si votre classe a besoin de plusieurs constructeurs, annotez celui que Spring doit utiliser avec @Autowired.

La recommandation de l’équipe Spring #

Si vous avez une licence Ultimate d’IntelliJ IDEA, lorsque vous annotez un champ privé avec @Autowired, un avertissement devrait apparaître. En affichant toutes ses infos, voici ce que vous devriez pouvoir lire :

Field injection is not recommended
Inspection info: Spring Team recommends: “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies”.

Soit, en français :

L’injection par champ n’est pas recommandée
Infos sur l’inspection : Recommandation de l’équipe Spring : « Utilisez toujours l’injection de dépendance par constructeur dans vos beans. Utilisez toujours les assertions pour les dépendances obligatoires ».

Faire de l’injection de dépendance par constructeur #

Avec une configuration par annotation #

Imaginons un HelloService qui a besoin que Spring lui injecte un HelloRepository et une propriété de l'application.properties. Pour y parvenir, ces deux dépendances doivent être des paramètres du constructeur plutôt que des champs privés annotés.

 1@Service
 2public class HelloService {
 3
 4    private HelloRepository repository;
 5    private String fromName;
 6
 7    public HelloService(
 8        HelloRepository repository,
 9        @Value("${kp.hello.from.name}") String fromName
10    ) {
11        this.repository = repository;
12        this.fromName = fromName;
13    }
14
15    // Logic here
16}

À partir de la version 4.3 de Spring, l’annotation @Autowired peut être omise pour les classes avec un seul constructeur. Pour les versions précédentes ou les classes avec plusieurs constructeurs, elle doit être utilisée pour indiquer à Spring quel constructeur utiliser.

En arrière-boutique, quand Spring démarrera, il découvrira le HelloService grâce à l’annotation @Service et saura qu’il doit être initialisé. Il découvrira ensuite le constructeur dont il devra fournir les paramètres demandés. Il les cherchera dans son contexte. Attendez-vous à une exception s’il ne peut pas tous les y trouver.

Avec une configuration par XML #

XML reste une solution valide pour configurer Spring (j’ai eu du mal à passer aux annotations), et il peut toujours être utilisé pour configurer une injection par constructeur. Par exemple :

 1<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
 2  <property name="location" value="application.properties"/>
 3</bean>
 4
 5<!-- ... -->
 6
 7<bean id="helloService" class="org.keyboardplaying.demo.HelloService">
 8    <constructor-arg index="0" ref="helloRepository"/>
 9    <constructor-arg index="1" value="${kp.hello.from.name}"/>
10</bean>

Dans ce cas, Spring ne fera pas d’introspection puisqu’on lui a déjà tout spécifié explicitement.

Avantages & inconvénients #

Avantage : testabilité #

Examinons un HelloService écrit de façon « traditionnelle ».

 1@Service
 2public class HelloService {
 3
 4    @Autowired
 5    private HelloRepository repository;
 6
 7    @Value("${kp.hello.from.name}")
 8    private String fromName;
 9
10    // Logic here
11}

À présent, imaginez que vous voulez faire des tests unitaires. En premier lieu, vous ne pourriez pas injecter ces champs sans passer par la réflexion, qui est en totale opposition avec l’encapsulation.

Un autre inconvénient de l’injection par champ est que vous ne pouvez pas garantir que le bean a été complètement initialisé. Vous pourriez tomber sur des NullPointerException si votre code fait appel à une dépendance qui n’a pas été prise en compte à la mise en place de vos tests.

Avantage : paradigme POO #

Du point de vue de la programmation orientée objet, il est plus intuitif d’utiliser un constructeur pour créer un objet.

Avantage/inconvénient : verbosité #

Les services complexes peuvent avoir de nombreuses dépendances. Les constructeurs associés peuvent devenir assez encombrants.

Cependant, comme Baeldung le souligne, cela peut nous inciter à faire plus attention au nombre de dépendances de nos services. Peut-être pouvons-nous écrire des composants plus spécialisés et tirer parti de composition intelligente. Maybe write more specialized components and benefit from intelligent composition.

Là où je vais plus loin #

Rendre les beans immuables #

Puisque toutes les dépendances d’un service sont initialisées à l’instanciation, pourquoi ne pas les rendre final ? Cela évite qu’un autre composant puisse le modifier à l’exécution, que ce soit lié à une erreur de programmation ou à une intrusion.

Ne pas écrire les constructeurs #

Je n’ai (toujours) pas écrit au sujet du Projet Lombok, mais il a un grand but commun avec Spring Boot : éviter le code boilerplate.

Pourquoi passer du temps à écrire des constructeurs et les maintenir à jour lorsque les dépendances évoluent ? Lombok utilise la réflexion pour les générer au moment de la compilation.

Par exemple, nous pourrions modifier notre service comme suit :

1@Service
2@AllArgsConstructor
3public class HelloService {
4
5    private HelloRepository repository;
6
7    // Logic here
8}

Ou, si nous rendons le service immuable, nous obtiendrions :

1@Service
2@RequiredArgsConstructor
3public class HelloService {
4
5    private final HelloRepository repository;
6
7    // Logic here
8}

Je vous laisse consulter la documentation de Lombok pour voir les différences entre @AllArgsConstructor et @RequiredArgsConstructor.

Il y a toutefois une grande restriction à cette utilisation de Lombok : on ne peut pas passer un paramètre annoté (comme les paramètres @Value). Par expérience, la plupart des beans Spring peuvent cependant être instanciés de cette façon.

Et avec Quarkus ? #

L’injection de dépendances simplifiée de Quarkus, ArC, n’est pas une implémentation complète de CDI. Toutefois, elle apporte la possibilité d’utiliser l’injection par constructeur.

J’en ai récemment profité, au lieu de m’appuyer sur @Inject, qui a les mêmes défauts qu’@Autowired..

Conclusion #

Si vous avez d’autres remarques intéressantes sur l’utilisation de l’injection de dépendance par constructeur, partagez avec nous.