index.fr.md 7.1 KB
Newer Older
1
---
2
date: 2022-04-29T07:00:00+02:00
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
title: L'injection Spring par constructeur avec Lombok
subtitle: Les pièces du puzzle s'assemblent
slug: spring-injection-dependances-constructeur-lombok
description: |-
  Lombok simplifie l'injection de dépendances par constructeur avec Spring.
  Il y a juste une astuce à connaître pour utiliser l'annotation `@Value`.
author: chop
categories: [ software-creation ]
tags: [ spring-boot, java, programming ]
keywords: [ injection de dépendance, java, spring, spring boot, lombok, autowired, constructeur, immuable ]
---

Par le passé, nous vous conseillions d'[utiliser le constructeur pour injecter les dépendances][kp-spring] avec Spring, et nous avons [présenté Lombok][kp-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.

<!--more-->

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

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

```java
import org.springframework.stereotype.Service;

@Service
public class Library {
    private final BooksDatabase booksDb;
    private final BorrowersDatabase borrowersDb;

    public Library(BooksDatabase booksDb, BorrowersDatabase borrowersDb) {
        this.booksDb = booksDb;
        this.borrowersDb = borrowersDb;
    }
}
```

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


```java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class Library {
    private final BooksDatabase booksDb;
    private final BorrowersDatabase borrowersDb;
}
```

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é.


```java
@Service
public class Library {
    private final BooksDatabase booksDb;
    private final BorrowersDatabase borrowersDb;
    private final int maxLendingDays;

    public Library(
      BooksDatabase booksDb,
      BorrowersDatabase borrowersDb,
      @Value("${library.lending.days.max}") maxLendingDays
    ) {
        this.booksDb = booksDb;
        this.borrowersDb = borrowersDb;
        this.maxLendingDays = maxLendingDays;
    }
}
```

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 :

```java
@Service
@RequiredArgsConstructor
public class Library {
    private final BooksDatabase booksDb;
    private final BorrowersDatabase borrowersDb;

    @Value("${library.lending.days.max}")
    private int maxLendingDays;
}
```

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 :

```java
@Service
public class Library {
    private final BooksDatabase booksDb;
    private final BorrowersDatabase borrowersDb;

    @Value("${library.lending.days.max}")
    private int maxLendingDays;

    public Library(BooksDatabase booksDb, BorrowersDatabase borrowersDb) {
        this.booksDb = booksDb;
        this.borrowersDb = borrowersDb;
    }
}
```

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][kp-lombok] 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][lombok-constructors].
Pour cela, il suffit d'ajouter la ligne suivante au fichier de configuration :

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

Écrivez ensuite votre service ainsi :

```java
@Service
@RequiredArgsConstructor
public class Library {
    private final BooksDatabase booksDb;
    private final BorrowersDatabase borrowersDb;

    @Value("${library.lending.days.max}")
    private final int maxLendingDays;
}
```

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

```java
@Service
public class Library {
    private final BooksDatabase booksDb;
    private final BorrowersDatabase borrowersDb;

    @Value("${library.lending.days.max}")
    private final int maxLendingDays;

    public Library(
      BooksDatabase booksDb,
      BorrowersDatabase borrowersDb,
      @Value("${library.lending.days.max}") maxLendingDays
    ) {
        this.booksDb = booksDb;
        this.borrowersDb = borrowersDb;
        this.maxLendingDays = maxLendingDays;
    }
}
```


## 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 facultative[^fn-autowired-constructor], 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 :

```java
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class Library {
    // ...
}
```

[^fn-autowired-constructor]: `@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é.


## 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 !


[kp-spring]: {{< relref path="/blog/2021/01/04-spring-constructor-injection" >}}
[kp-lombok]: {{< relref path="/blog/2022/04/08-lombok" >}}
[lombok-constructors]: https://projectlombok.org/features/constructor