Commit a1ab14c5 authored by Cyrille Chopelet's avatar Cyrille Chopelet
Browse files

[Blog] Init a post about Spring + Lombok

parent 45e5daba
......@@ -9,7 +9,7 @@ description: |-
author: chop
categories: [ software-creation ]
tags: [ best-practices, spring-boot, quarkus, java, programming ]
keywords: [ bonne pratique, injection de dépendance, java, spring, spring boot, quarkus, autowired, test, poo, immuable, lombok ]
keywords: [ bonne pratique, injection de dépendance, constructeur, java, spring, spring boot, quarkus, autowired, test, poo, immuable, lombok ]
references:
- id: baeldung-constructor-injection
......
......@@ -9,7 +9,7 @@ description: |-
author: chop
categories: [ software-creation ]
tags: [ best-practices, spring-boot, quarkus, java, programming ]
keywords: [ best practice, dependency injection, java, spring, spring boot, quarkus, autowired, test, oop, immutable, lombok ]
keywords: [ best practice, dependency injection, constructor, java, spring, spring boot, quarkus, autowired, test, oop, immutable, lombok ]
references:
- id: baeldung-constructor-injection
......
......@@ -10,7 +10,7 @@ description: |-
author: chop
categories: [ software-creation ]
tags: [ java ]
keywords: [ Java, Lombok, boilerplate kodon, aliriloj, konstruiloj, Maven, IDE, protokolado ]
keywords: [ Java, Lombok, boilerplate kodon, aliriloj, konstruilo, Maven, IDE, protokolado ]
references:
- id: lombok
......
......@@ -10,7 +10,7 @@ description: |-
author: chop
categories: [ software-creation ]
tags: [ java ]
keywords: [ Java, Lombok, code boilerplate, accesseurs, constructeurs, Maven, IDE, logging ]
keywords: [ Java, Lombok, code boilerplate, accesseurs, constructeurs, Maven, IDE, logging, immuable ]
references:
- id: lombok
......
---
date: 2022-04-22T07:00:00+02:00
title: Injekto de dependecoj per konstruilo kun Spring kaj Lombok
subtitle: La pecoj de la puzlo kinuĝas.
slug: spring-lombok-injekto-dependecoj-konstruilo
description: |-
Lombok faciligas injekti dependecoj per konstruilo.
Estas nur unu konsileto vi bezonas scii por uzi kun la `@Value` prinoto de Spring.
author: chop
categories: [ software-creation ]
tags: [ spring-boot, java, programming ]
keywords: [ injekto de dependeco, java, spring, spring boot, lombok, autowired, konstruilo, neŝanĝebla ]
---
En la pasinteco, ni konsilis [uzi la konstruilo por injekti dependecoj][kp-spring] kun Spring, kaj ni [prezentis Lombok][kp-lombok].
Divenu…
Tiuj iras tre bone kune, krom eble por iom da ruzo por uzi la `@Value` de Spring.
<!--more-->
## Generi la injektantan konstruilon kun Lombok
Ni faru neŝanĝeblan servon, kiu injektas ĝiajn dependecojn per sia konstruilo.
```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;
}
}
```
Per petado de [kion ni vidis kun Lombok lasta fojo][kp-lombok], pli konciza maniero fari ĉi tion estus:
```java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class Library {
private final BooksDatabase booksDb;
private final BorrowersDatabase borrowersDb;
}
```
Finita!
Kaj neniu bontenado de la konstruilo estas necesa se ni aldonas aŭ forigas kampojn.
Ĉu tio ne estas vera plezuro fidi ĉi tiujn du?
## La ĝena kazo de `@Value`
### Ne blindu
Lombok havas `lombok.Value`, Spring havas `org.springframework.beans.factory.annotation.Value`.
Ili tute ne havas la saman celon.
En ĉi tiu afiŝo, ni enfokusigos la `@Value` de Spring.
### La problemo
Por uzi la injekton per konstruilo, ĉio devas trairi la konstruilon.
Se necesas agordon, oni devas specifi `@Value` pri la koncerna parametro.
```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;
}
}
```
Kiel fari tamen, se Lombok verkas la konstruilo anstataŭ vi?
### La solution naïve
Kiam unue alfrontis ĉi tiu, la instinkto de mia teamo estis fari tion:
```java
@Service
@RequiredArgsConstructor
public class Library {
private final BooksDatabase booksDb;
private final BorrowersDatabase borrowersDb;
@Value("${library.lending.days.max}")
private int maxLendingDays;
}
```
Sed kio okazas en ĉi tiu kazo?
Unue, Lombok generos konstruilon, kiu prenas argumenton por ambaŭ `final` kampoj, sed ignoros la entjeron.
La generita kodo aspektus tiel:
```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;
}
}
```
Kiam Spring renkontas ĉi tiun kodon, ĝi instigas la klason en du stadioj:
1. Ĉar ekzistas konstruilo, ĝi injektas la du necesajn kampojn.
2. Ĝi tiam uzas la reflektadon por agordi la ne-`final`, `@Value`-prinotatan kampon.
La tuta profito de la konstruilo-bazitan injekto tiam estas perdita.
### La vera solvo
[Ni diris][kp-lombok] ke la konduto de Lombok povas esti adaptita per `lombok.config` dosiero.
Ĉi tiu estas unu el la plej gravaj kazoj.
Ekzemple, eblas fari Lombok [kopii iujn prinotojn de la kampoj al la respondaj parametroj de la konstruilo][lombok-constructors].
Por ĉi tio, simple aldonu la sekvan linion al la agorda dosiero:
```properties
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value
```
Tiam skribu vian servon tiel:
```java
@Service
@RequiredArgsConstructor
public class Library {
private final BooksDatabase booksDb;
private final BorrowersDatabase borrowersDb;
@Value("${library.lending.days.max}")
private final int maxLendingDays;
}
```
La generita kodo devus esti ekvivalenta al la sekva:
```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;
}
}
```
## Miaj aliaj konsiloj
### `@RequiredArgsConstructor` aŭ `@AllArgsConstructor`
Mi ŝatas `@RequiredArgsConstructor`, ĉar ĝi ofertas iom da kontrolo pri kiuj kampoj devas esti transdonitaj al la konstruilo.
Tamen, por Spring servoj, ĉiuj kampoj devus tiel komencitaj, do `@AllArgsConstructor` estas tute korekta solvo.
### Aldoni `@Autowired` al generita konstruilo
Vi eble deziras, ke via konstruilo estas prinotata kun `@Autowired`.
Kvankam laŭvola[^fn-autowired-constructor], ĝi povas helpi programistojn, kiuj ne konas bone Spring, kompreni kiel la klaso estas ŝarĝita dum rultempo, ekzemple.
Ĉi tio povas esti farita per agordo de via Lombok prinoto:
```java
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class Library {
// ...
}
```
[^fn-autowired-constructor]: `@Autowired` estas necesa sur la konstruilo nur se la klaso havas aliajn konstruilojn.
En ĉi tiu kazo, akurate unu konstruilo devas esti prinotata.
## Konkludo
Mi esperas, ke ĉi tiu mallonga afiŝo helpos vin forigi iujn el la _boilerplate_ kodo necesita por la instanco de viaj Spring servojn.
Ne hezitu dividi viajn konsiletojn!
[kp-spring]: {{< relref path="/blog/2021/01/04-spring-constructor-injection" lang="en" >}}
[kp-lombok]: {{< relref path="/blog/2022/04/08-lombok" >}}
[lombok-constructors]: https://projectlombok.org/features/constructor
---
date: 2022-04-22T07:00:00+02:00
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
---
date: 2022-04-22T07:00:00+02:00
title: Spring's Constructor-Based Dependency Injection with Lombok
subtitle: When the picture of the puzzle appears
slug: spring-constructor-dependency-injection-lombok
description: |-
Lombok makes Spring's constructor-based injection easy.
You just need to know a simple trick to use the `@Value` annotation.
author: chop
categories: [ software-creation ]
tags: [ spring-boot, java, programming ]
keywords: [ dependency injection, java, spring, spring boot, lombok, autowired, constructor, immutable ]
---
In the past, we advised [using the constructor to inject dependencies][kp-spring] with Spring, and we [introduced Lombok][kp-lombok].
Guess what?
Those go quite well along together, except maybe for a little trick to known when you wish to use Spring's `@Value`.
We'll cover all that in this post.
<!--more-->
## Generate the Injecting Constructor with Lombok
Let's make an immutable service that injects its dependencies through a constructor.
```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;
}
}
```
If we apply [what we showed with Lombok last time][kp-lombok], a more concise way to do this would be:
```java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class Library {
private final BooksDatabase booksDb;
private final BorrowersDatabase borrowersDb;
}
```
And we're done!
Plus, no maintenance needed of the constructor if we add or remove fields.
Ain't that a match made in heaven?
## The Annoying Case of `@Value`
### Don't Mix Up!
Lombok has `lombok.Value`, Spring has `org.springframework.beans.factory.annotation.Value`.
They don't have the same purpose at all.
In this post, we'll focus on Spring's `@Value`.
### The Problem
To use Spring's constructor-based injection, everything needs to go through the constructor.
If you need to inject configuration, you must be able to set the `@Value` on the parameters.
```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;
}
}
```
However, since Lombok writes your constructor in your stead, how can you do that?
### The Naive Solution
When first confronted to this issue, the instinct of my team was to do that:
```java
@Service
@RequiredArgsConstructor
public class Library {
private final BooksDatabase booksDb;
private final BorrowersDatabase borrowersDb;
@Value("${library.lending.days.max}")