Lombok rend la grandeur de Java



Chez Grubhub, nous utilisons Java dans presque tout le backend. Il s'agit d'un langage éprouvé qui a prouvé sa vitesse et sa fiabilité au cours des 20 dernières années. Mais au fil des ans, l'âge du "vieil homme" a encore commencé à affecter.

Java est l' un des langages JVM les plus populaires , mais pas le seul. Ces dernières années, il a concurrencé Scala, Clojure et Kotlin, qui fournissent de nouvelles fonctionnalités et des fonctionnalités linguistiques optimisées. En bref, ils vous permettent de faire plus avec un code plus concis.

Ces innovations dans l'écosystème JVM sont très intéressantes. En raison de la concurrence, Java est obligé de changer pour rester compétitif. Le nouveau calendrier de publication de six mois et plusieurs JEP (propositions d'amélioration JDK) dans Java 8 (Valhalla, Local-Variable Type Inference, Loom) sont la preuve que Java restera un langage compétitif pendant des années.

Cependant, la taille et l'échelle de Java signifient que le développement progresse plus lentement que nous le souhaiterions, sans parler de la forte volonté de maintenir la compatibilité descendante à tout prix. Dans tout développement, la première priorité devrait être les fonctions, mais ici les fonctions nécessaires sont développées trop longtemps, voire pas du tout, dans le langage. C'est pourquoi nous, à Grubhub, utilisons Project Lombok pour avoir Java optimisé et amélioré à notre disposition en ce moment. Le projet Lombok est un plugin de compilation qui ajoute de nouveaux «mots clés» à Java et transforme les annotations en code Java, réduisant l'effort de développement et fournissant des fonctionnalités supplémentaires.

Configurer Lombok


Grubhub s'efforce toujours d'améliorer le cycle de vie des logiciels, mais chaque nouvel outil et processus a un coût à considérer. Heureusement, pour connecter Lombok, ajoutez simplement quelques lignes au fichier gradle.

Lombok convertit les annotations du code source en instructions Java avant que le compilateur ne les traite: la dépendance lombok pas en runtime, donc l'utilisation du plugin n'augmentera pas la taille de l'assembly. Pour configurer Lombok avec Gradle (cela fonctionne également avec Maven), ajoutez simplement les lignes suivantes au fichier build.gradle :

 plugins { id 'io.franzbecker.gradle-lombok' version '1.14' id 'java' } repositories { jcenter() // or Maven central, required for Lombok dependency } lombok { version = '1.18.4' sha256 = "" } 

Avec Lombok, notre code source ne sera pas un code Java valide. Par conséquent, vous devrez installer un plug-in pour l'IDE, sinon l'environnement de développement ne comprendra pas de quoi il s'agit. Lombok prend en charge tous les principaux IDE Java. Intégration transparente. Toutes les fonctions comme «afficher l'utilisation» et «passer à l'implémentation» continuent de fonctionner comme avant, vous déplaçant vers le champ / la classe correspondant.

Lombok en action


La meilleure façon d'apprendre à connaître Lombok est de le voir en action. Prenons quelques exemples typiques.

Revivez l'objet POJO


Avec les «bons vieux objets Java» (POJO), nous séparons les données du traitement pour faciliter la lecture du code et rationaliser les transferts réseau. Un POJO simple a plusieurs champs privés, ainsi que les getters et setters correspondants. Ils font le travail, mais nécessitent beaucoup de code passe-partout.

Lombok aide à utiliser POJO de manière plus flexible et structurée sans code supplémentaire. Voici @Data nous simplifions le POJO sous-jacent avec l'annotation @Data :

 @Data public class User { private UUID userId; private String email; } 

@Data n'est qu'une annotation pratique qui applique plusieurs annotations Lombok à la fois.

  • @ToString génère une implémentation pour la toString() , qui consiste en une représentation soignée de l'objet: le nom de la classe, tous les champs et leurs valeurs.
  • @EqualsAndHashCode génère des implémentations de equals et hashCode , qui par défaut utilisent des champs non statiques et non stationnaires, mais sont personnalisables.
  • @Getter / @Setter génère des getters et setters pour les champs privés.
  • @RequiredArgsConstructor crée un constructeur avec les arguments requis, où les champs finaux et les champs avec l'annotation @NonNull sont @NonNull (plus de détails ci-dessous).

Cette annotation à elle seule couvre simplement et avec élégance de nombreux cas d'utilisation typiques. Mais POJO ne couvre pas toujours les fonctionnalités nécessaires. @Data est une classe entièrement modifiable, dont l'abus peut augmenter la complexité et limiter la concurrence, ce qui affecte négativement la capacité de survie de l'application.

Il y a une autre solution. Revenons à notre classe User , rendons-la immuable et ajoutons quelques autres annotations utiles.

 @Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; } 

L'annotation @Value similaire à @Data sauf que tous les champs sont privés et définitifs par défaut, et les setters ne sont pas créés. Grâce à cela, les objets @Value deviennent immédiatement immuables. Puisque tous les champs sont finaux, il n'y a pas de constructeur d'argument. Au lieu de cela, Lombok utilise le @AllArgsConstructor . Le résultat est un objet entièrement fonctionnel et immuable.

Mais l'immuabilité n'est pas très utile si vous avez seulement besoin de créer un objet en utilisant le constructeur all-args. Comme l'explique Joshua Bloch dans son livre Effective Java Programming, vous devez utiliser des générateurs si vous avez un grand nombre de paramètres de concepteur. Ici, la classe @Builder entre en @Builder , générant automatiquement la classe interne du générateur:

 User user = User.builder() .userId(UUID.random()) .email(“grubhub@grubhub.com”) .favoriteFood(“burritos”) .favoriteFood(“dosas”) .build() 

La génération d'un générateur facilite la création d'objets avec un grand nombre d'arguments et l'ajout de nouveaux champs à l'avenir. La méthode statique renvoie une instance du générateur pour définir toutes les propriétés de l'objet. Après cela, l'appel à build() renvoie l'instance.

L' @NonNull peut être utilisée pour @NonNull que ces champs ne sont pas nuls lors de la création d'une instance de l'objet; sinon, une exception NullPointerException levée. Notez que le champ d'avatar est annoté avec @NonNull mais n'est pas défini. Le fait est que l'annotation @Builder.Default pointe vers default.png par défaut.

Notez également comment le générateur utilise favoriteFood , le seul nom de propriété de notre objet. Lors du placement d'annotations @Singular sur une propriété de collection, Lombok crée des méthodes de générateur spéciales pour ajouter des éléments à la collection individuellement, et non pour ajouter la collection entière en même temps. C'est particulièrement bon pour les tests, car les moyens de créer de petites collections en Java ne peuvent pas être appelés simples et rapides.

Enfin, le paramètre toBuilder = true ajoute la méthode d'instance toBuilder() , qui crée un objet de générateur rempli avec toutes les valeurs de cette instance. Il est si facile de créer une nouvelle instance, préremplie avec toutes les valeurs de l'original, de sorte qu'il ne reste plus qu'à modifier les champs nécessaires. Ceci est particulièrement utile pour les classes @Value , car les champs sont immuables.

Quelques notes personnalisent davantage les fonctions spéciales du setter. @Wither crée des méthodes @Wither pour chaque propriété. En entrée, la valeur; en sortie, le clone de l'instance avec la valeur mise à jour d'un champ. @Accessors vous permet de configurer des setters créés automatiquement. Le paramètre fluent=true désactive les conventions get et set pour les getters et les setters. Dans certaines situations, cela peut être un remplacement utile pour @Builder .

Si l'implémentation de Lombok ne convient pas à votre tâche (et que vous avez examiné les modificateurs d'annotation), vous pouvez toujours simplement prendre et écrire votre propre implémentation. Par exemple, si vous avez la classe @Data , mais qu'un getter a besoin d'une logique personnalisée, implémentez simplement ce getter. Lombok verra que l'implémentation est déjà fournie et ne l'écrasera pas avec l'implémentation créée automatiquement.

Avec seulement quelques annotations simples, le POJO de base a reçu tant de fonctionnalités riches qui simplifient son utilisation sans charger le travail de nos ingénieurs, sans perdre de temps ni augmenter les coûts de développement.

Suppression du code de modèle


Lombok est utile non seulement pour POJO: il peut être appliqué à n'importe quel niveau de l'application. Les utilisations suivantes de Lombok sont particulièrement utiles dans les classes de composants telles que les contrôleurs, les services et les DAO (objets d'accès aux données).

La journalisation est une exigence de base pour toutes les parties du programme. Toute classe qui fait un travail significatif devrait écrire un journal. Ainsi, l'enregistreur standard devient un modèle pour chaque classe. Lombok simplifie ce modèle en une seule annotation qui identifie et instancie automatiquement un enregistreur avec le nom de classe correct. Il existe plusieurs annotations différentes selon la structure du journal.

 @Slf4j // also: @CommonsLog @Flogger @JBossLog @Log @Log4j @Log4j2 @XSlf4j public class UserService { // created automatically // private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class); } 

Après avoir déclaré l'enregistreur, ajoutez nos dépendances:

 @Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; } 

L' @FieldDefaults ajoute des modificateurs finaux et privés à tous les champs. @RequiredArgsConstructor crée un constructeur qui configure une instance de UserDao . L' @NonNull ajoute une validation dans le constructeur et UserDao NullPointerException si l'instance UserDao est nulle.

Mais attendez, ce n'est pas tout!


Il existe de nombreuses autres situations où Lombok fait de son mieux. Les sections précédentes ont montré des exemples spécifiques, mais Lombok peut faciliter le développement dans de nombreux domaines. Voici quelques petits exemples de la façon de l'utiliser plus efficacement.

Bien que le mot clé var apparu dans Java 9, la variable peut toujours être réaffectée. Lombok a le mot-clé val , qui affiche le type final d'une variable locale.

 // final Map map = new HashMap<Integer, String>(); val map = new HashMap<Integer, String>(); 

Certaines classes avec des fonctions purement statiques ne sont pas destinées à être initialisées. Une façon d'empêcher l'instanciation consiste à déclarer un constructeur privé qui lève une exception. Lombok a codifié ce modèle dans l'annotation @UtilityClass . Il génère un constructeur privé qui lève une exception, génère finalement la classe et rend toutes les méthodes statiques.

 @UtilityClass // will be made final public class UtilityClass { // will be made static private final int GRUBHUB = “ GRUBHUB”; // autogenerated by Lombok // private UtilityClass() { // throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated"); //} // will be made static public void append(String input) { return input + GRUBHUB; } } 

Java est souvent critiqué pour sa verbosité en raison d'exceptions vérifiées. Une annotation Lombok distincte les corrige: @SneakyThrows . Comme prévu, la mise en œuvre est assez délicate. Il n'attrape pas les exceptions ni même RuntimeException exceptions dans une RuntimeException . Au lieu de cela, il repose sur le fait que la JVM ne vérifie pas la cohérence des exceptions vérifiées au moment de l'exécution. Seul javac fait cela. Par conséquent, Lombok utilise la conversion de bytecode au moment de la compilation pour désactiver cette vérification. Le résultat est un code exécutable.

 public class SneakyThrows { @SneakyThrows public void sneakyThrow() { throw new Exception(); } } 

Comparaison côte à côte


Les comparaisons directes montrent le mieux la quantité de code que Lombok enregistre. Le plugin IDE possède une fonction «de-lombok» qui convertit approximativement la plupart des annotations Lombok en code Java natif (les annotations @NonNull ne @NonNull pas converties). Ainsi, n'importe quel IDE avec le plugin installé pourra convertir la plupart des annotations en code Java natif et vice versa. Retour à notre classe User .

 @Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; } 

La classe Lombok est composée de 13 lignes simples, lisibles et compréhensibles. Mais après avoir exécuté de-lombok, la classe se transforme en plus d'une centaine de lignes de code passe-partout!

 public class User { @NonNull UUID userId; @NonNull String email; Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = "default.png"; @java.beans.ConstructorProperties({"userId", "email", "favoriteFoods", "avatar"}) User(UUID userId, String email, Set<String> favoriteFoods, String avatar) { this.userId = userId; this.email = email; this.favoriteFoods = favoriteFoods; this.avatar = avatar; } public static UserBuilder builder() { return new UserBuilder(); } @NonNull public UUID getUserId() { return this.userId; } @NonNull public String getEmail() { return this.email; } public Set<String> getFavoriteFoods() { return this.favoriteFoods; } @NonNull public String getAvatar() { return this.avatar; } public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof User)) return false; final User other = (User) o; final Object this$userId = this.getUserId(); final Object other$userId = other.getUserId(); if (this$userId == null ? other$userId != null : !this$userId.equals(other$userId)) return false; final Object this$email = this.getEmail(); final Object other$email = other.getEmail(); if (this$email == null ? other$email != null : !this$email.equals(other$email)) return false; final Object this$favoriteFoods = this.getFavoriteFoods(); final Object other$favoriteFoods = other.getFavoriteFoods(); if (this$favoriteFoods == null ? other$favoriteFoods != null : !this$favoriteFoods.equals(other$favoriteFoods)) return false; final Object this$avatar = this.getAvatar(); final Object other$avatar = other.getAvatar(); if (this$avatar == null ? other$avatar != null : !this$avatar.equals(other$avatar)) return false; return true; } public int hashCode() { final int PRIME = 59; int result = 1; final Object $userId = this.getUserId(); result = result * PRIME + ($userId == null ? 43 : $userId.hashCode()); final Object $email = this.getEmail(); result = result * PRIME + ($email == null ? 43 : $email.hashCode()); final Object $favoriteFoods = this.getFavoriteFoods(); result = result * PRIME + ($favoriteFoods == null ? 43 : $favoriteFoods.hashCode()); final Object $avatar = this.getAvatar(); result = result * PRIME + ($avatar == null ? 43 : $avatar.hashCode()); return result; } public String toString() { return "User(userId=" + this.getUserId() + ", email=" + this.getEmail() + ", favoriteFoods=" + this.getFavoriteFoods() + ", avatar=" + this.getAvatar() + ")"; } public UserBuilder toBuilder() { return new UserBuilder().userId(this.userId).email(this.email).favoriteFoods(this.favoriteFoods).avatar(this.avatar); } public static class UserBuilder { private UUID userId; private String email; private ArrayList<String> favoriteFoods; private String avatar; UserBuilder() { } public User.UserBuilder userId(UUID userId) { this.userId = userId; return this; } public User.UserBuilder email(String email) { this.email = email; return this; } public User.UserBuilder favoriteFood(String favoriteFood) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.add(favoriteFood); return this; } public User.UserBuilder favoriteFoods(Collection<? extends String> favoriteFoods) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.addAll(favoriteFoods); return this; } public User.UserBuilder clearFavoriteFoods() { if (this.favoriteFoods != null) this.favoriteFoods.clear(); return this; } public User.UserBuilder avatar(String avatar) { this.avatar = avatar; return this; } public User build() { Set<String> favoriteFoods; switch (this.favoriteFoods == null ? 0 : this.favoriteFoods.size()) { case 0: favoriteFoods = java.util.Collections.emptySet(); break; case 1: favoriteFoods = java.util.Collections.singleton(this.favoriteFoods.get(0)); break; default: favoriteFoods = new java.util.LinkedHashSet<String>(this.favoriteFoods.size() < 1073741824 ? 1 + this.favoriteFoods.size() + (this.favoriteFoods.size() - 3) / 3 : Integer.MAX_VALUE); favoriteFoods.addAll(this.favoriteFoods); favoriteFoods = java.util.Collections.unmodifiableSet(favoriteFoods); } return new User(userId, email, favoriteFoods, avatar); } public String toString() { return "User.UserBuilder(userId=" + this.userId + ", email=" + this.email + ", favoriteFoods=" + this.favoriteFoods + ", avatar=" + this.avatar + ")"; } } } 

Nous ferons de même pour la classe UserService .

 @Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; } 

Voici un exemple d'homologue en code Java standard.

  public class UserService { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class); private final UserDao userDao; @java.beans.ConstructorProperties({"userDao"}) public UserService(UserDao userDao) { if (userDao == null) { throw new NullPointerException("userDao is marked @NonNull but is null") } this.userDao = userDao; } } 

Évaluation de l'effet


Grubhub compte plus d'une centaine de services commerciaux de livraison de nourriture. Nous en avons pris un et avons lancé la fonction «de-lombok» dans le plugin Lombok IntelliJ. En conséquence, environ 180 fichiers ont changé et la base de code a augmenté d'environ 18 000 lignes de code après avoir supprimé 800 cas d'utilisation de Lombok. En moyenne, chaque ligne Lombok enregistre 23 lignes Java. Avec cet effet, il est difficile d'imaginer Java sans Lombok.

Résumé


Lombok est un excellent assistant qui implémente de nouvelles fonctionnalités de langage sans nécessiter beaucoup d'efforts de la part du développeur. Bien sûr, il est plus facile d'installer le plugin que de former tous les ingénieurs dans une nouvelle langue et de porter le code existant. Lombok n'est pas omnipotent, mais hors de la boîte est assez puissant pour vraiment aider dans le travail.

Un autre avantage de Lombok est qu'il maintient des bases de code cohérentes. Nous avons plus d'une centaine de services différents et une équipe distribuée à travers le monde, donc la cohérence des bases de code facilite la mise à l'échelle des équipes et réduit la charge de changement de contexte lors du démarrage d'un nouveau projet. Lombok fonctionne pour n'importe quelle version depuis Java 6, nous pouvons donc compter sur sa disponibilité dans tous les projets.

Pour Grubhub, c'est plus que de nouvelles fonctionnalités. Au final, tout ce code peut être écrit manuellement. Mais Lombok simplifie les parties ennuyeuses de la base de code sans affecter la logique métier. Cela vous permet de vous concentrer sur des choses qui sont vraiment importantes pour l'entreprise et les plus intéressantes pour nos développeurs. Le code de modèle Monton est une perte de temps pour les programmeurs, les réviseurs et les mainteneurs. De plus, comme ce code n'est plus écrit manuellement, il élimine des classes entières de fautes de frappe. Les avantages de l'auto- @NonNull combinés à la puissance de @NonNull réduisent les risques d'erreurs et aident notre développement, qui vise à livrer de la nourriture à votre table!

Source: https://habr.com/ru/post/fr438870/


All Articles