Écrire du code Java sans gras avec Project Lombok
Publié: 2022-03-11Il existe un certain nombre d'outils et de bibliothèques sans lesquels je ne peux pas m'imaginer écrire du code Java ces jours-ci. Traditionnellement, des choses comme Google Guava ou Joda Time (du moins pour l'ère pré-Java 8) font partie des dépendances que je finis par jeter dans mes projets la plupart du temps, quel que soit le domaine spécifique à portée de main.
Lombok mérite sûrement sa place dans mes versions POM ou Gradle, même s'il ne s'agit pas d'un utilitaire typique de bibliothèque/framework. Lombok existe depuis un certain temps déjà (sorti pour la première fois en 2009) et a beaucoup mûri depuis. Cependant, j'ai toujours pensé qu'il méritait plus d'attention - c'est une façon étonnante de gérer la verbosité naturelle de Java.
Dans cet article, nous allons explorer ce qui fait de Lombok un outil si pratique.
Java a beaucoup de choses à faire au-delà de la JVM elle-même, qui est un logiciel remarquable. Java est mature et performant, et la communauté et l'écosystème qui l'entourent sont énormes et vivants.
Cependant, en tant que langage de programmation, Java a ses propres particularités ainsi que des choix de conception qui peuvent le rendre plutôt verbeux. Ajoutez des constructions et des modèles de classe que les développeurs Java ont souvent besoin d'utiliser et nous nous retrouvons souvent avec de nombreuses lignes de code qui n'apportent que peu ou pas de valeur réelle autre que le respect d'un ensemble de contraintes ou de conventions de cadre.
C'est ici que Lombok entre en jeu. Cela nous permet de réduire considérablement la quantité de code « passe-partout » que nous devons écrire. Les créateurs de Lombok sont un couple de gars très intelligents, et ont certainement le goût de l'humour - vous ne pouvez pas manquer cette introduction qu'ils ont faite lors d'une conférence précédente !
Voyons comment Lombok fait sa magie et quelques exemples d'utilisation.
Comment fonctionne Lombok
Lombok agit comme un processeur d'annotation qui "ajoute" du code à vos classes au moment de la compilation. Le traitement des annotations est une fonctionnalité ajoutée au compilateur Java à la version 5. L'idée est que les utilisateurs peuvent mettre des processeurs d'annotations (écrits par eux-mêmes ou via des dépendances tierces, comme Lombok) dans le chemin de classe de construction. Ensuite, au fur et à mesure que le processus de compilation se poursuit, chaque fois que le compilateur trouve une annotation, il demande en quelque sorte : "Hé, quelqu'un dans le chemin de classe est intéressé par cette @Annotation ?". Pour ces processeurs qui lèvent la main, le compilateur leur transfère ensuite le contrôle ainsi que le contexte de compilation pour qu'ils puissent, eh bien… traiter.
Le cas le plus courant pour les processeurs d'annotation est peut-être de générer de nouveaux fichiers source ou d'effectuer une sorte de vérification au moment de la compilation.
Lombok n'entre pas vraiment dans ces catégories : ce qu'il fait, c'est modifier les structures de données du compilateur utilisées pour représenter le code ; c'est-à-dire son arbre de syntaxe abstraite (AST). En modifiant l'AST du compilateur, Lombok modifie indirectement la génération finale du bytecode elle-même.
Cette approche inhabituelle et plutôt intrusive a traditionnellement fait de Lombok une sorte de piratage. Bien que je sois moi-même d'accord avec cette caractérisation dans une certaine mesure, plutôt que de la considérer dans le mauvais sens du terme, je considérerais Lombok comme une "alternative intelligente, techniquement méritoire et originale".
Pourtant, certains développeurs considèrent qu'il s'agit d'un hack et n'utilisent pas Lombok pour cette raison. C'est compréhensible, mais d'après mon expérience, les avantages de productivité de Lombok l'emportent sur toutes ces préoccupations. Je l'utilise avec plaisir pour des projets de production depuis de nombreuses années maintenant.
Avant d'entrer dans les détails, j'aimerais résumer les deux raisons pour lesquelles j'apprécie particulièrement l'utilisation de Lombok dans mes projets :
- Lombok m'aide à garder mon code propre, concis et précis. Je trouve mes cours annotés de Lombok très expressifs et je trouve généralement que le code annoté est assez révélateur d'intention, bien que tout le monde sur Internet ne soit pas nécessairement d'accord.
- Lorsque je démarre un projet et que je pense à un modèle de domaine, j'ai tendance à commencer par écrire des classes qui sont en grande partie un travail en cours et que je modifie de manière itérative au fur et à mesure que je réfléchis et que je les affine. Dans ces premières étapes, Lombok m'aide à aller plus vite en n'ayant pas besoin de me déplacer ou de transformer le code passe-partout qu'il génère pour moi.
Modèle de haricot et méthodes d'objet communes
De nombreux outils et frameworks Java que nous utilisons reposent sur le Bean Pattern. Les Java Beans sont des classes sérialisables qui ont un constructeur sans argument par défaut (et éventuellement d'autres versions) et exposent leur état via des getters et des setters, généralement soutenus par des champs privés. Nous en écrivons beaucoup, par exemple lorsque nous travaillons avec JPA ou des frameworks de sérialisation tels que JAXB ou Jackson.
Considérez ce bean User qui contient jusqu'à cinq attributs (propriétés), pour lequel nous aimerions avoir un constructeur supplémentaire pour tous les attributs, une représentation sous forme de chaîne significative et définir l'égalité/le hachage en termes de son champ email :
public class User implements Serializable { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; // Empty constructor implementation: ~3 lines. // Utility constructor for all attributes: ~7 lines. // Getters/setters: ~38 lines. // equals() and hashCode() as per email: ~23 lines. // toString() for all attributes: ~3 lines. // Relevant: 5 lines; Boilerplate: 74 lines => 93% meaningless code :( }
Par souci de concision, plutôt que d'inclure l'implémentation réelle de toutes les méthodes, j'ai simplement fourni des commentaires répertoriant les méthodes et le nombre de lignes de code que les implémentations réelles ont prises. Ce code passe-partout aurait totalisé plus de 90 % du code pour cette classe !
De plus, si je voulais plus tard, par exemple, changer l'e- email
en adresse e- emailAddress
ou faire en sorte que registrationTs
soit une Date
au lieu d'un Instant
, j'aurais besoin de consacrer du temps (avec l'aide de mon IDE dans certains cas, certes) pour changer des choses comme obtenir /set noms et types de méthode, modifier mon constructeur d'utilitaire, etc. Encore une fois, un temps inestimable pour quelque chose qui n'apporte aucune valeur commerciale pratique à mon code.
Voyons comment Lombok peut aider ici :
import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString @EqualsAndHashCode(of = {"email"}) public class User { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; }
Voila ! Je viens d'ajouter un tas d'annotations lombok.*
et j'ai obtenu exactement ce que je voulais. La liste ci-dessus est exactement tout le code que j'ai besoin d'écrire pour cela. Lombok s'accroche à mon processus de compilation et a tout généré pour moi (voir la capture d'écran ci-dessous de mon IDE).
Comme vous le remarquez, l'inspecteur NetBeans (et cela se produira quel que soit l'IDE) détecte le bytecode de la classe compilée, y compris les ajouts que Lombok a apportés au processus. Ce qui s'est passé ici est assez simple :
- En utilisant
@Getter
et@Setter
, j'ai demandé à Lombok de générer des getters et des setters pour tous les attributs. C'est parce que j'ai utilisé les annotations au niveau de la classe. Si je voulais spécifier de manière sélective quoi générer pour quels attributs, j'aurais pu annoter les champs eux-mêmes. - Grâce à
@NoArgsConstructor
et@AllArgsConstructor
, j'ai obtenu un constructeur vide par défaut pour ma classe ainsi qu'un autre pour tous les attributs. - L'annotation
@ToString
génère automatiquement une méthode pratiquetoString()
, affichant par défaut tous les attributs de classe préfixés par leur nom. - Enfin, pour que la paire de méthodes
equals()
ethashCode()
soit définie en termes de champ de courrier électronique, j'ai utilisé@EqualsAndHashCode
et je l'ai paramétré avec la liste des champs pertinents (juste le courrier électronique dans ce cas).
Personnalisation des annotations de Lombok
Utilisons maintenant quelques personnalisations de Lombok en suivant ce même exemple :
- J'aimerais réduire la visibilité du constructeur par défaut. Parce que je n'en ai besoin que pour des raisons de conformité du bean, je m'attends à ce que les consommateurs de la classe n'appellent que le constructeur qui prend tous les champs. Pour appliquer cela, je personnalise le constructeur généré avec
AccessLevel.PACKAGE
. - Je veux m'assurer que mes champs ne reçoivent jamais de valeurs nulles, ni via le constructeur ni via les méthodes de définition. Annoter les attributs de classe avec
@NonNull
suffit ; Lombok générera des vérifications nulles en lançantNullPointerException
le cas échéant dans les méthodes du constructeur et du setter. - Je vais ajouter un attribut de
password
de passe, mais je ne veux pas qu'il s'affiche lors de l'appel detoString()
pour des raisons de sécurité. Ceci est accompli via l'argument excludes de@ToString
. - Je suis d'accord pour exposer publiquement l'état via des getters, mais je préférerais restreindre la mutabilité extérieure. Je laisse
@Getter
tel quel, mais en utilisant à nouveauAccessLevel.PROTECTED
pour@Setter
. - Peut-être que je voudrais forcer une certaine contrainte sur le champ e-
email
afin que, s'il est modifié, une sorte de vérification soit exécutée. Pour cela, j'implémente moi-même la méthodesetEmail()
. Lombok omettra juste la génération pour une méthode qui existe déjà.
Voici à quoi ressemblera la classe User :
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName; private @NonNull Instant registrationTs; private boolean payingCustomer; protected void setEmail(String email) { // Check for null (=> NullPointerException) // and valid email code (=> IllegalArgumentException) this.email = email; } }
Notez que, pour certaines annotations, nous spécifions les attributs de classe sous forme de chaînes simples. Ce n'est pas un problème, car Lombok lancera une erreur de compilation si, par exemple, nous faisons une erreur de frappe ou nous référons à un champ inexistant. Avec Lombok, nous sommes en sécurité.
De plus, tout comme pour la méthode setEmail()
, Lombok sera juste OK et ne générera rien pour une méthode que le programmeur a déjà implémentée. Cela s'applique à toutes les méthodes et constructeurs.
Structures de données immuables
Un autre cas d'utilisation où Lombok excelle est lors de la création de structures de données immuables. Ceux-ci sont généralement appelés "types de valeur". Certains langages ont un support intégré pour ceux-ci, et il existe même une proposition pour l'incorporer dans les futures versions de Java.
Supposons que nous voulions modéliser une réponse à une action de connexion d'un utilisateur. C'est le genre d'objet que nous voudrions simplement instancier et renvoyer à d'autres couches de l'application (par exemple, pour être sérialisé JSON en tant que corps d'une réponse HTTP). Une telle LoginResponse n'aurait pas du tout besoin d'être modifiable et Lombok peut aider à décrire cela succinctement. Bien sûr, il existe de nombreux autres cas d'utilisation pour les structures de données immuables (elles sont compatibles avec le multithreading et le cache, entre autres qualités), mais restons-en à cet exemple simple :
import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.experimental.Wither; @Getter @RequiredArgsConstructor @ToString @EqualsAndHashCode public final class LoginResponse { private final long userId; private final @NonNull String authToken; private final @NonNull Instant loginTs; @Wither private final @NonNull Instant tokenExpiryTs; }
A noter ici :

- Une annotation
@RequiredArgsConstructor
a été introduite. Bien nommé, il génère un constructeur pour tous les champs finaux qui n'ont pas encore été initialisés. - Dans les cas où nous voulons réutiliser une LoginResonse précédemment émise (imaginez, par exemple, une opération "refresh token"), nous ne voulons certainement pas modifier notre instance existante, mais plutôt, nous voulons en générer une nouvelle basée sur celle-ci . Voyez comment l'annotation
@Wither
nous aide ici : elle indique à Lombok de générer unewithTokenExpiryTs(Instant tokenExpiryTs)
qui crée une nouvelle instance de LoginResponse ayant toutes les valeurs d'instance with'ed, à l'exception de la nouvelle que nous spécifions. Aimeriez-vous ce comportement pour tous les champs ? Ajoutez simplement@Wither
à la déclaration de classe à la place.
@Données et @Valeur
Les deux cas d'utilisation discutés jusqu'à présent sont si courants que Lombok fournit quelques annotations pour les rendre encore plus courtes : Annoter une classe avec @Data
déclenchera Lombok pour qu'il se comporte comme s'il avait été annoté avec @Getter
+ @Setter
+ @ToString
+ @EqualsAndHashCode
+ @RequiredArgsConstructor
. De même, l'utilisation @Value
transformera votre classe en une classe immuable (et finale), également comme si elle avait été annotée avec la liste ci-dessus.
Modèle de constructeur
Pour en revenir à notre exemple User, si nous voulons créer une nouvelle instance, nous devrons utiliser un constructeur avec jusqu'à six arguments. C'est déjà un nombre assez important, qui s'aggravera encore si nous ajoutons des attributs à la classe. Supposons également que nous souhaitions définir des valeurs par défaut pour les champs lastName
et payingCustomer
.
Lombok implémente une fonctionnalité @Builder
très puissante, nous permettant d'utiliser un modèle de construction pour créer de nouvelles instances. Ajoutons-le à notre classe User :
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) @Builder public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName = ""; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }
Maintenant, nous sommes en mesure de créer facilement de nouveaux utilisateurs comme celui-ci :
User user = User .builder() .email("[email protected]") .password("secret".getBytes(StandardCharsets.UTF_8)) .firstName("Miguel") .registrationTs(Instant.now()) .build();
Il est facile d'imaginer à quel point cette construction devient pratique à mesure que nos classes grandissent.
Délégation/Composition
Si vous voulez suivre la règle très sensée de "favoriser la composition à l'héritage", c'est quelque chose que Java n'aide pas vraiment, en termes de verbosité. Si vous souhaitez composer des objets, vous devez généralement écrire des appels de méthode de délégation partout.
Lombok propose une solution pour cela via @Delegate
. Prenons un exemple.
Imaginez que nous voulions introduire un nouveau concept de ContactInformation
. Il s'agit de certaines informations dont dispose notre User
et nous souhaitons peut-être que d'autres classes en aient également. Nous pouvons ensuite modéliser cela via une interface comme celle-ci :
public interface HasContactInformation { String getEmail(); String getFirstName(); String getLastName(); }
Nous introduirions alors une nouvelle classe ContactInformation
utilisant Lombok :
import lombok.Data; @Data public class ContactInformation implements HasContactInformation { private String email; private String firstName; private String lastName; }
Et enfin, nous pourrions refactoriser User
pour composer avec ContactInformation
et utiliser Lombok pour générer tous les appels de délégation requis pour correspondre au contrat d'interface :
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; import lombok.experimental.Delegate; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"contactInformation"}) public class User implements HasContactInformation { @Getter(AccessLevel.NONE) @Delegate(types = {HasContactInformation.class}) private final ContactInformation contactInformation = new ContactInformation(); private @NonNull byte[] password; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }
Notez que je n'ai pas eu besoin d'écrire des implémentations pour les méthodes de HasContactInformation
: c'est quelque chose que nous disons à Lombok de faire, en déléguant les appels à notre instance ContactInformation
.
De plus, parce que je ne veux pas que l'instance déléguée soit accessible de l'extérieur, je la personnalise avec un @Getter(AccessLevel.NONE)
, empêchant efficacement la génération de getter pour elle.
Exceptions vérifiées
Comme nous le savons tous, Java fait la différence entre les exceptions vérifiées et non vérifiées. Il s'agit d'une source traditionnelle de controverse et de critique du langage, car la gestion des exceptions nous gêne parfois trop, en particulier lorsqu'il s'agit d'API conçues pour lancer des exceptions vérifiées et nous obligeant ainsi, les développeurs, à les attraper ou à déclarer nos méthodes à Jette-les.
Considérez cet exemple :
public class UserService { public URL buildUsersApiUrl() { try { return new URL("https://apiserver.com/users"); } catch (MalformedURLException ex) { // Malformed? Really? throw new RuntimeException(ex); } } }
C'est un modèle si courant : nous savons certainement que notre URL est bien formée, mais, parce que le constructeur d' URL
lève une exception vérifiée, nous sommes soit obligés de l'attraper, soit de déclarer notre méthode pour la lancer et mettre les appelants dans la même situation. L'encapsulation de ces exceptions vérifiées dans une RuntimeException
est une pratique très étendue. Et cela s'aggrave encore si le nombre d'exceptions vérifiées que nous devons traiter augmente à mesure que nous codons.
C'est donc exactement à cela que @SneakyThrows
de Lombok, il enveloppera toutes les exceptions vérifiées susceptibles d'être jetées dans notre méthode dans une non vérifiée et nous libérera des tracas:
import lombok.SneakyThrows; public class UserService { @SneakyThrows public URL buildUsersApiUrl() { return new URL("https://apiserver.com/users"); } }
Enregistrement
À quelle fréquence ajoutez-vous des instances de journalisation à vos classes comme celle-ci ? (exemple SLF4J)
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
Je vais deviner un peu. Sachant cela, les créateurs de Lombok ont implémenté une annotation qui crée une instance de journalisation avec un nom personnalisable (log par défaut), prenant en charge les frameworks de journalisation les plus courants sur la plate-forme Java. Juste comme ça (encore une fois, basé sur SLF4J):
import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @Slf4j public class UserService { @SneakyThrows public URL buildUsersApiUrl() { log.debug("Building users API URL"); return new URL("https://apiserver.com/users"); } }
Annotation du code généré
Si nous utilisons Lombok pour générer du code, il peut sembler que nous perdrions la possibilité d'annoter ces méthodes puisque nous ne les écrivons pas réellement. Mais ce n'est pas vraiment vrai. Au lieu de cela, Lombok nous permet de lui dire comment nous voudrions que le code généré soit annoté, en utilisant une notation quelque peu particulière, à vrai dire.
Considérez cet exemple, ciblant l'utilisation d'un framework d'injection de dépendances : nous avons une classe UserService
qui utilise l'injection de constructeur pour obtenir les références à un UserRepository
et UserApiClient
.
package com.mgl.toptal.lombok; import javax.inject.Inject; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor(onConstructor = @__(@Inject)) public class UserService { private final UserRepository userRepository; private final UserApiClient userApiClient; // Instead of: // // @Inject // public UserService(UserRepository userRepository, // UserApiClient userApiClient) { // this.userRepository = userRepository; // this.userApiClient = userApiClient; // } }
L'exemple ci-dessus montre comment annoter un constructeur généré. Lombok nous permet de faire la même chose pour les méthodes et les paramètres générés.
Apprendre plus
L'utilisation de Lombok expliquée dans cet article se concentre sur les fonctionnalités que j'ai personnellement trouvées les plus utiles au fil des ans. Cependant, de nombreuses autres fonctionnalités et personnalisations sont disponibles.
La documentation de Lombok est très informative et complète. Ils ont des pages dédiées pour chaque fonctionnalité (annotation) avec des explications et des exemples très détaillés. Si vous trouvez cet article intéressant, je vous encourage à vous plonger plus profondément dans lombok et sa documentation pour en savoir plus.
Le site du projet documente comment utiliser Lombok dans plusieurs environnements de programmation différents. En bref, les IDE les plus populaires (Eclipse, NetBeans et IntelliJ) sont pris en charge. Je passe moi-même régulièrement de l'un à l'autre projet par projet et j'utilise Lombok sur chacun d'eux sans problème.
Delombok !
Delombok fait partie de la "chaîne d'outils Lombok" et peut être très utile. Ce qu'il fait est essentiellement de générer le code source Java pour votre code annoté Lombok, en effectuant les mêmes opérations que le bytecode généré par Lombok.
C'est une excellente option pour les personnes qui envisagent d'adopter Lombok mais qui ne sont pas encore tout à fait sûres. Vous pouvez librement commencer à l'utiliser et il n'y aura pas de "verrouillage du fournisseur". Au cas où vous ou votre équipe regretteriez plus tard le choix, vous pouvez toujours utiliser delombok pour générer le code source correspondant que vous pourrez ensuite utiliser sans aucune dépendance à Lombok.
Delombok est également un excellent outil pour savoir exactement ce que Lombok va faire. Il existe des moyens très simples de le connecter à votre processus de construction.
Alternatives
Il existe de nombreux outils dans le monde Java qui utilisent de manière similaire les processeurs d'annotation pour enrichir ou modifier votre code au moment de la compilation, tels que Immutables ou Google Auto Value. Ceux-ci (et d'autres, bien sûr !) se chevauchent avec Lombok en termes de fonctionnalités. J'aime particulièrement beaucoup l'approche Immutables et je l'ai également utilisée dans certains projets.
Il convient également de noter qu'il existe d'autres excellents outils offrant des fonctionnalités similaires pour "l'amélioration du bytecode", tels que Byte Buddy ou Javassist. Ceux-ci fonctionnent généralement au moment de l'exécution et constituent un monde à part au-delà de la portée de cet article.
Java concis
Il existe un certain nombre de langages ciblés JVM modernes qui fournissent des approches de conception plus idiomatiques, voire au niveau du langage, aidant à résoudre certains des mêmes problèmes. Groovy, Scala et Kotlin sont sûrement de bons exemples. Mais si vous travaillez sur un projet Java uniquement, alors Lombok est un bon outil pour aider vos programmes à être plus concis, expressifs et maintenables.