L’injection Spring par constructeur avec Lombok

Les pièces du puzzle s’assemblent
  EN, EO
 Lecture : 4 min
 chop
 Pas encore commenté
 

Par le passé, nous vous conseillions d’utiliser le constructeur pour injecter les dépendances avec Spring, et nous avons présenté Lombok. Devinez… Ces deux compères s’entendent parfaitement, à une petite astuce près pour pouvoir utiliser le @Value de Spring. Nous parlerons de tout ceci dans ce billet.

Générer le constructeur pour l’injection avec Lombok #

Créons un service immuable qui injecte ses dépendances par son constructeur.

 1import org.springframework.stereotype.Service;
 2
 3@Service
 4public class Library {
 5    private final BooksDatabase booksDb;
 6    private final BorrowersDatabase borrowersDb;
 7
 8    public Library(BooksDatabase booksDb, BorrowersDatabase borrowersDb) {
 9        this.booksDb = booksDb;
10        this.borrowersDb = borrowersDb;
11    }
12}

En appliquant ce que nous avons vu de Lombok la dernière fois, une tournure plus concise serait la suivante :

1import lombok.RequiredArgsConstructor;
2import org.springframework.stereotype.Service;
3
4@Service
5@RequiredArgsConstructor
6public class Library {
7    private final BooksDatabase booksDb;
8    private final BorrowersDatabase borrowersDb;
9}

Et c’est fini ! En bonus, aucune maintenance du constructeur n’est nécessaire dans le cas où l’on ajouterait ou retirerait des champs. N’est-ce pas là un vrai plaisir que de s’appuyer sur ces deux-là ?

Le cas particulier de @Value #

Ne pas s’embrouiller #

Lombok a lombok.Value, Spring a org.springframework.beans.factory.annotation.Value. Ils n’ont rien à voir. Dans ce billet, nous nous intéressons uniquement à celui de Spring.

Le souci #

Pour utiliser l’injection par constructeur, tout doit passer par le constructeur. S’il est nécessaire de passer une configuration, il faut spécifier @Value sur le paramètre concerné.

 1@Service
 2public class Library {
 3    private final BooksDatabase booksDb;
 4    private final BorrowersDatabase borrowersDb;
 5    private final int maxLendingDays;
 6
 7    public Library(
 8      BooksDatabase booksDb,
 9      BorrowersDatabase borrowersDb,
10      @Value("${library.lending.days.max}") maxLendingDays
11    ) {
12        this.booksDb = booksDb;
13        this.borrowersDb = borrowersDb;
14        this.maxLendingDays = maxLendingDays;
15    }
16}

Comment faire cependant si c’est Lombok qui écrit le constructeur à votre place ?

La solution naïve #

Confrontés à ce souci pour la première fois, mon équipe a eu l’instinct de faire ceci :

1@Service
2@RequiredArgsConstructor
3public class Library {
4    private final BooksDatabase booksDb;
5    private final BorrowersDatabase borrowersDb;
6
7    @Value("${library.lending.days.max}")
8    private int maxLendingDays;
9}

Que se passe-t-il dans ce cas ?

Tout d’abord, Lombok va générer un constructeur qui prend un argument pour les deux champs final mais va ignorer l’entier. Le code généré ressemblerait à ça :

 1@Service
 2public class Library {
 3    private final BooksDatabase booksDb;
 4    private final BorrowersDatabase borrowersDb;
 5
 6    @Value("${library.lending.days.max}")
 7    private int maxLendingDays;
 8
 9    public Library(BooksDatabase booksDb, BorrowersDatabase borrowersDb) {
10        this.booksDb = booksDb;
11        this.borrowersDb = borrowersDb;
12    }
13}

Quand Spring rencontre ce code, il instancie la classe en deux étapes :

  1. Puisqu’il y a un constructeur, il lui injecte les deux champs nécessaires.
  2. Il utilise ensuite la réflexion pour initialiser le champ non final annoté avec @Value. Tout le gain de l’injection par constructeur est perdue.

La véritable solution #

Nous avons dit que le comportement de Lombok peut être personnalisé par le biais d’un fichier lombok.config. Ceci est l’un des cas les plus pertinents.

Il est par exemple possible de dire à Lombok de copier certaines annotations des champs sur les paramètres correspondants du constructeur. Pour cela, il suffit d’ajouter la ligne suivante au fichier de configuration :

lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value

Écrivez ensuite votre service ainsi :

1@Service
2@RequiredArgsConstructor
3public class Library {
4    private final BooksDatabase booksDb;
5    private final BorrowersDatabase borrowersDb;
6
7    @Value("${library.lending.days.max}")
8    private final int maxLendingDays;
9}

Le code généré devrait être équivalent à ce qui suit :

 1@Service
 2public class Library {
 3    private final BooksDatabase booksDb;
 4    private final BorrowersDatabase borrowersDb;
 5
 6    @Value("${library.lending.days.max}")
 7    private final int maxLendingDays;
 8
 9    public Library(
10      BooksDatabase booksDb,
11      BorrowersDatabase borrowersDb,
12      @Value("${library.lending.days.max}") maxLendingDays
13    ) {
14        this.booksDb = booksDb;
15        this.borrowersDb = borrowersDb;
16        this.maxLendingDays = maxLendingDays;
17    }
18}

Mes autres astuces #

@RequiredArgsConstructor ou @AllArgsConstructor #

J’aime @RequiredArgsConstructor parce qu’il offre un certain contrôle sur quels champs doivent être passés au constructeur ou non. Pourtant, dans le cas d’un service Spring, je pense que tous les champs devraient être initialisés ainsi dans 99 % des cas. @AllArgsConstructor est donc une solution tout à fait correcte et nous épargne un peu de réflexion (« Est-ce que ce champ sera bien inclus dans les paramètres du constructeur ? » Oui, il le sera !).

Ajouter @Autowired à un constructeur généré #

Vous pourriez avoir envie que votre constructeur porte l’annotation @Autowired. Bien que facultative1, elle peut aider les développeurs qui ne connaissent pas bien Spring à comprendre comment la classe est chargée à l’exécution, par exemple. Ceci peut être fait en paramétrant votre annotation Lombok :

1@Service
2@RequiredArgsConstructor(onConstructor = @__(@Autowired))
3public class Library {
4    // ...
5}

Conclusion #

J’espère que ce rapide billet vous aidera à éliminer un peu du code boilerplate lié à l’instanciation de vos composants Spring.

N’hésitez pas à partager vos astuces !


  1. @Autowired n’est nécessaire sur le constructeur que si la classe comprend plusieurs constructeurs. Dans ce cas, un et un seul d’entre eux doit être ainsi annoté. ↩︎