Introduction au projet Lombok

Lombok est l’un des outils que je glisse toujours littéralement dans mes projets. Je ne pouvais pas m’imaginer programmer Java sans cela ces jours-ci. J’espère vraiment que vous trouverez son pouvoir en lisant cet article!

1. Évitez le code répétitif

Java est un langage génial, mais il devient parfois trop détaillé pour les tâches que vous devez faire dans votre code pour des tâches courantes ou pour la conformité à certaines pratiques du framework. Très souvent, ces solutions n’apportent aucune valeur réelle à l’aspect commercial de vos programmes - et c’est là que Lombok est là pour rendre votre vie plus heureuse et vous rendre plus productive.

Cela fonctionne en connectant votre processus de construction et en générant automatiquement du bytecode Java dans vos fichiers .class conformément au nombre d’annotations de projet que vous introduisez dans votre code.

L’inclure dans vos builds, quel que soit le système que vous utilisez, est très simple. Leur project page contient des instructions détaillées sur les détails. La plupart de mes projets sont basés sur Maven, je supprime donc généralement leur dépendance dans le champ d’application fourni et je suis prêt à partir:

<dependencies>
    ...
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.8</version>
        <scope>provided</scope>
    </dependency>
    ...
</dependencies>

Recherchez la version la plus récente disponible here .

Notez que selon Lombok, les utilisateurs de votre __. Jar __s en dépendront également, car il s’agit d’une dépendance de construction pure, et non d’exécution.

2. Getters/Setters, Constructors - So Repetitive

L’encapsulation de propriétés d’objet via des méthodes getter et setter publiques est une pratique courante dans le monde Java. De nombreux frameworks s’appuient largement sur ce modèle «Java Bean»: une classe avec un constructeur vide et des méthodes get/set pour les «propriétés».

C’est tellement courant que la plupart des IDE prennent en charge le code de génération automatique de ces modèles (et plus). Cependant, ce code doit vivre dans vos sources et être également maintenu lorsqu’une nouvelle propriété est ajoutée ou un champ renommé.

Considérons cette classe que nous souhaitons utiliser comme exemple d’entité JPA:

@Entity
public class User implements Serializable {

    private @Id Long id;//will be set when persisting

    private String firstName;
    private String lastName;
    private int age;

    public User() {
    }

    public User(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

   //getters and setters: ~30 extra lines of code
}

C’est une classe assez simple, mais envisagez tout de même si nous ajoutions le code supplémentaire pour les accesseurs et les setters, nous aboutirions à une définition selon laquelle nous aurions plus de code de valeur zéro standard que les informations commerciales pertinentes: «Un utilisateur a d’abord et noms de famille et âge.

Laissez-nous maintenant Lombok-ize cette classe:

@Entity
@Getter @Setter @NoArgsConstructor//<--- THIS is it
public class User implements Serializable {

    private @Id Long id;//will be set when persisting

    private String firstName;
    private String lastName;
    private int age;

    public User(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
}

En ajoutant les annotations @ Getter et @ Setter , nous avons demandé à Lombok de générer ces informations pour tous les champs de la classe.

@ NoArgsConstructor conduira à une génération de constructeur vide.

Notez que c’est le code de la classe entière , je n’omets rien, contrairement à la version ci-dessus avec le commentaire //getters and setters .

Pour une classe d’attribut pertinente, il s’agit d’une économie de code significative!

Si vous ajoutez des attributs (propriétés) à votre classe User , il en ira de même: vous avez appliqué les annotations au type lui-même, de sorte qu’ils gèrent tous les champs par défaut.

Et si vous vouliez améliorer la visibilité de certaines propriétés? Par exemple, j’aime bien garder les modificateurs id de champ de mes entités package ou protected visibles, car ils doivent être lus mais non explicitement définis par le code de l’application. Utilisez simplement un @ Setter à grain plus fin pour ce champ particulier:

private @Id @Setter(AccessLevel.PROTECTED) Long id;

3. Classes de valeur/DTO’s

Il existe de nombreuses situations dans lesquelles nous voulons définir un type de données dans le seul but de représenter des «valeurs» complexes ou des «objets de transfert de données», la plupart du temps sous la forme de structures de données immuables que nous construisons une fois et que nous ne voulons jamais changer. .

Nous concevons une classe pour représenter une opération de connexion réussie. Nous voulons que tous les champs soient non nuls et que les objets soient immuables afin que nous puissions accéder en mode thread à ses propriétés:

public class LoginResult {

    private final Instant loginTs;

    private final String authToken;
    private final Duration tokenValidity;

    private final URL tokenRefreshUrl;

   //constructor taking every field and checking nulls

   //read-only accessor, not necessarily as get** () form
}

Encore une fois, la quantité de code que nous aurions à écrire pour les sections commentées serait d’un volume beaucoup plus important que les informations que nous voulons encapsuler et qui a une réelle valeur pour nous. Nous pouvons utiliser à nouveau Lombok pour améliorer ceci:

@RequiredArgsConstructor
@Accessors(fluent = true) @Getter
public class LoginResult {

    private final @NonNull Instant loginTs;

    private final @NonNull String authToken;
    private final @NonNull Duration tokenValidity;

    private final @NonNull URL tokenRefreshUrl;

}

Ajoutez simplement l’annotation @ RequiredArgsConstructor et vous obtiendrez un constructeur pour tous les champs finaux de la classe, exactement comme vous les avez déclarés. L’ajout de @ NonNull aux attributs permet à notre constructeur de vérifier la nullité et de lancer NullPointerExceptions en conséquence. Cela se produirait également si les champs n’étaient pas définitifs et que nous ajoutions @ Setter pour eux.

Vous ne voulez pas de vieux formulaire get ** () ennuyeux pour vos propriétés? Parce que nous avons ajouté @ Accessors (fluent = true) dans cet exemple, les «getters» auraient le même nom de méthode que les propriétés: getAuthToken () devient simplement authToken () .

Ce formulaire «couramment» s’appliquerait aux champs non finaux des paramètres d’attributs et permettrait également des appels chaînés:

----//Imagine fields were no longer final now
return new LoginResult()
  .loginTs(Instant.now())
  .authToken("asdasd")
  .//and so on
----

4. Boilerplate Java Core

Une autre situation dans laquelle nous finissons par écrire du code que nous devons conserver est lors de la génération des méthodes toString () , equals () et hashCode () . Les IDE essaient de vous aider avec des modèles pour la génération automatique de ceux-ci en termes d’attributs de classe.

Nous pouvons automatiser cela au moyen d’autres annotations de classe Lombok:

générer une méthode toString () incluant tous les attributs de classe. Pas besoin d’en écrire un nous-mêmes et de le maintenir pendant que nous enrichissons notre modèle de données

générera les deux méthodes equals () et hashCode () par défaut, en tenant compte de tous les champs pertinents, et selon le site bien que la sémantique .

Ces générateurs sont livrés avec des options de configuration très pratiques. Par exemple, si vos classes annotées font partie d’une hiérarchie, vous pouvez simplement utiliser le paramètre callSuper = true et les résultats parent seront pris en compte lors de la génération du code de la méthode.

Plus à ce sujet: supposons que notre exemple d’entité User JPA comprenne une référence aux événements associés à cet utilisateur:

@OneToMany(mappedBy = "user")
private List<UserEvent> events;

Nous ne voudrions pas que la liste des trous soit vidée lorsque nous appelons la méthode toString () de notre utilisateur, simplement parce que nous avons utilisé l’annotation @ ToString . Pas de problème: il suffit de le paramétrer comme ceci:

@ ToString (exclude = \ {"events"}) , et cela ne se produira pas. Ceci est également utile pour éviter les références circulaires si, par exemple, _UserEvent s avait une référence à User_ .

Pour l’exemple LoginResult , nous pouvons vouloir définir l’égalité et le calcul du code de hachage uniquement en fonction du jeton lui-même et non des autres attributs finaux de notre classe. Ensuite, écrivez simplement quelque chose comme @ EqualsAndHashCode (of = \ {"authToken"}) .

Bonus: si vous avez aimé les fonctionnalités des annotations que nous avons examinées jusqu’à présent, vous voudrez peut-être examiner @Data et @ Value annotations car elles se comportent comme si un ensemble d’entre elles avait été appliqué à nos classes. Après tout, ces usages discutés sont très souvent réunis.

5. Le modèle de constructeur

Les éléments suivants pourraient constituer un exemple de classe de configuration pour un client d’API REST:

public class ApiClientConfiguration {

    private String host;
    private int port;
    private boolean useHttps;

    private long connectTimeout;
    private long readTimeout;

    private String username;
    private String password;

   //Whatever other options you may thing.

   //Empty constructor? All combinations?

   //getters... and setters?
}

Nous pourrions avoir une approche initiale basée sur l’utilisation du constructeur vide de la classe par défaut et sur la fourniture de méthodes de définition pour chaque champ. Cependant, nous souhaiterions idéalement que les configurations non soient configurées une fois qu’elles ont été construites (instanciées), ce qui les rend effectivement immuables. Nous voulons donc éviter les setters, mais écrire un tel constructeur potentiellement long est un anti-pattern.

Au lieu de cela, nous pouvons demander à l’outil de générer un motif builder , nous empêchant d’écrire une classe supplémentaire Builder et des méthodes associées similaires à un outil de définition courante en ajoutant simplement l’annotation @Builder à notre ApiClientConfiguration .

@Builder
public class ApiClientConfiguration {

   //... everything else remains the same

}

En laissant la définition de classe ci-dessus en tant que telle (pas de constructeurs ni de constructeurs déclarés @ Builder ), nous pouvons finir par l’utiliser comme

ApiClientConfiguration config =
    new ApiClientConfigurationBuilder()
        .host("api.server.com")
        .port(443)
        .useHttps(true)
        .connectTimeout(15__000L)
        .readTimeout(5__000L)
        .username("myusername")
        .password("secret")
    .build();

6. Exception vérifiée Fardeau

De nombreuses API Java sont conçues pour pouvoir générer un certain nombre d’exceptions vérifiées. Le code client est forcé de catch ou de déclarer à throws . Combien de fois avez-vous transformé ces exceptions que vous savez ne se produira pas en quelque chose comme ça?

public String resourceAsString() {
    try (InputStream is = this.getClass().getResourceAsStream("sure__in__my__jar.txt")) {
        BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        return br.lines().collect(Collectors.joining("\n"));
    } catch (IOException | UnsupportedCharsetException ex) {
       //If this ever happens, then its a bug.
        throw new RuntimeException(ex); <--- encapsulate into a Runtime ex.
    }
}

Si vous voulez éviter ces modèles de code parce que le compilateur ne sera autrement pas heureux (et, après tout, vous savez que les erreurs vérifiées ne peuvent pas se produire), utilisez le bien nommé https://projectlombok.org/features/SneakyThrows . html[ @ SneakyThrows ]:

@SneakyThrows
public String resourceAsString() {
    try (InputStream is = this.getClass().getResourceAsStream("sure__in__my__jar.txt")) {
        BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        return br.lines().collect(Collectors.joining("\n"));
    }
}

7. Assurez-vous que vos ressources sont libérées

Java 7 a introduit le bloc try-with-resources pour vous assurer que vos ressources détenues par des instances de tout ce qui implémente java.lang . AutoCloseable sont libérées lors de la sortie.

Lombok offre un moyen alternatif d’atteindre cet objectif et avec plus de souplesse via @Cleanup . Utilisez-le pour toute variable locale dont vous voulez vous assurer que les ressources sont libérées. Nul besoin pour eux d’implémenter une interface particulière, vous obtiendrez simplement sa méthode close () appelée.

@Cleanup InputStream is = this.getClass().getResourceAsStream("res.txt");

Votre méthode de libération a un nom différent? Pas de problème, personnalisez simplement l’annotation:

@Cleanup("dispose") JFrame mainFrame = new JFrame("Main Window");

8. Annotez votre classe pour obtenir un enregistreur

Beaucoup d’entre nous ajoutent des instructions de journalisation à notre code avec parcimonie en créant une instance de Logger à partir de notre structure de choix. Dites, SLF4J:

public class ApiClientConfiguration {

    private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class);

   //LOG.debug(), LOG.info(), ...

}

C’est un modèle si courant que les développeurs de Lombok se sont employés à le simplifier:

@Slf4j//or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j
public class ApiClientConfiguration {

   //log.debug(), log.info(), ...

}

De nombreux logging frameworks sont pris en charge et vous pouvez bien sûr personnaliser le nom de l’instance, le sujet, etc.

9. Écrire des méthodes plus sûres pour les threads

En Java, vous pouvez utiliser le mot clé synchronized pour implémenter des sections critiques. Cependant, cette approche n’est pas sûre à 100%: d’autres codes clients peuvent également se synchroniser sur votre instance, ce qui peut entraîner des blocages inattendus.

C’est ici que @Synchronized entre: annotez vos méthodes (à la fois instance et static) avec elle et vous obtiendrez un champ privé non exposé généré automatiquement que votre implémentation utilisera. pour le verrouillage:

@Synchronized
public/**  better than: synchronized ** /void putValueInCache(String key, Object value) {
   //whatever here will be thread-safe code
}

10. Automatiser la composition d’objets

Java ne possède pas de construction de niveau de langue pour lisser l’approche «héritage de composition de faveur». D’autres langues ont des concepts intégrés tels que Traits ou Mixins pour y parvenir.

Https://projectlombok.org/features/experimental/Delegate.html[@Delegate]de Lombok est très pratique lorsque vous souhaitez utiliser ce modèle de programmation. Prenons un exemple:

  • Nous voulons que __User s et Customer __s partagent des attributs communs.

pour nommer et numéro de téléphone ** Nous définissons à la fois une interface et une classe d’adaptateur pour ces champs

  • Nous aurons nos modèles implémenter l’interface et @ Delegate à leur

adaptateur, efficacement composer avec nos informations de contact

Commençons par définir une interface:

public interface HasContactInformation {

    String getFirstName();
    void setFirstName(String firstName);

    String getFullName();

    String getLastName();
    void setLastName(String lastName);

    String getPhoneNr();
    void setPhoneNr(String phoneNr);

}

Et maintenant, un adaptateur en tant que classe support :

@Data
public class ContactInformationSupport implements HasContactInformation {

    private String firstName;
    private String lastName;
    private String phoneNr;

    @Override
    public String getFullName() {
        return getFirstName() + " " + getLastName();
    }
}

La partie intéressante vient maintenant, voyez comme il est facile de composer maintenant les informations de contact dans les deux classes de modèle:

public class User implements HasContactInformation {

   //Whichever other User-specific attributes

    @Delegate(types = {HasContactInformation.class})
    private final ContactInformationSupport contactInformation =
            new ContactInformationSupport();

   //User itself will implement all contact information by delegation

}

Le cas de Customer serait si semblable que nous aurions omis l’échantillon par souci de concision.

11. Faire reculer Lombok?

Réponse courte: Pas du tout vraiment.

Vous craignez peut-être que vous utilisiez Lombok dans l’un de vos projets, mais que vous souhaitiez ensuite revenir sur cette décision. Vous auriez alors peut-être un grand nombre de classes annotées pour cela …​ que pourriez-vous faire?

Je n’ai jamais vraiment regretté cela, mais qui sait pour vous, votre équipe ou votre organisation? Pour ces cas, vous êtes couvert grâce à l’outil delombok du même projet.

En delombok-ing votre code, vous obtiendrez un code source Java généré automatiquement avec exactement les mêmes fonctionnalités que le bytecode créé par Lombok. Vous pouvez donc simplement remplacer votre code annoté d’origine par ces nouveaux fichiers delomboked et ne plus en dépendre.

C’est quelque chose que vous pouvez integrate dans votre construction et je l’ai déjà fait dans le passé pour étudier le code généré ou pour intégrer Lombok à un autre outil basé sur le code source Java. .

12. Conclusion

Il existe certaines autres fonctionnalités que nous n’avons pas présentées dans cet article. Je vous encourage à approfondir votre exploration de la page https://projectlombok.org/features/index.html

De plus, la plupart des fonctions que nous avons présentées possèdent un certain nombre d’options de personnalisation que vous pouvez juger utiles pour que l’outil génère les éléments les plus conformes aux pratiques de votre équipe en matière de dénomination, etc. Le fichier intégré disponible, https://projectlombok.org/features/configuration .html[système de configuration]pourrait aussi vous aider.

J’espère que vous avez trouvé la motivation pour donner à Lombok une chance d’entrer dans votre ensemble d’outils de développement Java. Essayez-le et améliorez votre productivité!

Cet exemple de code est disponible dans le projet GitHub .