Tutoriel sur l'utilisation de la bibliothèque Lombok

Dans ce billet, je vais présenter la bibliothèque Lombok dont je ne peux plus me passer pour mes développements Java. Cela fait maintenant plus de quatre ans que je l'utilise et tout n'est pas rose. Ce sera donc l'occasion de partager aussi certaines recommandations.

J'en donne une : n'utilisez pas @Data et je vais vous expliquer pourquoi. Un peu de patience.

Pour réagir à cet article, un espace de dialogue vous est proposé sur le forum 5 commentaires Donner une note  l'article (5).

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Lombok, à quoi cela sert-il ?

À part être visiblement une très belle île d'Indonésie, encore, il s'agit d'une bibliothèque qui va générer pour vous, en respectant de nombreuses bonnes pratiques, ce qu'on appelle du « boiler plate ».

En Java, dans la catégorie « Boiler plate », voici les nominés :

  • getters / setters ;
  • equals / hashCode ;
  • toString ;
  • constructeurs ;
  • modificateurs d'accès (private, protected, etc.).

And the winner is : égalité entre getter/setters et equals/hashCode. toString n'est vraiment pas loin derrière.

II. Démonstration par l'exemple

Prenez par exemple les classes métiers suivantes :

Diag Classes

II-A. Implémentation sans Lombok

Pour montrer ce qu'il faudrait faire en Java « sans Lombok », je vais simplement coder la classe Vehicule afin qu'elle respecte les conventions Java Beans.

Pour l'exemple, l'unicité sera portée par les champs « numeroMoteur, numeroChassis ». Dans la réalité, l'unicité d'un véhicule est bien plus complexe et dans tous les cas ne doit pas reposer sur l'immatriculation.

 
Sélectionnez
1.
2.
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.
public class Vehicule implements Serializable
{
    // champs métier
    private String numeroMoteur;
    private String numeroChassis;
    private String numeroImmatriculation;
    private LocalDate dateMiseEnCirculation;
    
    // champs de relation
    private List<Intervention> interventions;
    
    // constructeurs
    public Vehicule()
    {
        interventions = new ArrayList<>();
    }
    
    public Vehicule(String numeroMoteur, 
            String numeroChassis, 
            String numeroImmatriculation, 
            LocalDate dateMiseEnCirculation)
    {
        this();
        this.numeroMoteur = numeroMoteur;
        this.numeroChassis = numeroChassis;
        this.numeroImmatriculation = numeroImmatriculation;
        this.dateMiseEnCirculation = dateMiseEnCirculation;
    }

    // getters/setters : génération par Eclipse
    
    public String getNumeroMoteur() 
    {
        return numeroMoteur;
    }

    public void setNumeroMoteur(String numeroMoteur) 
    {
        this.numeroMoteur = numeroMoteur;
    }

    public String getNumeroChassis() 
    {
        return numeroChassis;
    }

    public void setNumeroChassis(String numeroChassis) 
    {
        this.numeroChassis = numeroChassis;
    }

    public String getNumeroImmatriculation() 
    {
        return numeroImmatriculation;
    }

    public void setNumeroImmatriculation(String numeroImmatriculation) 
    {
        this.numeroImmatriculation = numeroImmatriculation;
    }

    public LocalDate getDateMiseEnCirculation() {
        return dateMiseEnCirculation;
    }

    public void setDateMiseEnCirculation(LocalDate dateMiseEnCirculation) {
        this.dateMiseEnCirculation = dateMiseEnCirculation;
    }

    public List<Intervention> getInterventions() 
    {
        return interventions;
    }

    public void setInterventions(List<Intervention> interventions) 
    {
        this.interventions = interventions;
    }
    
    // equals/hashCode : génération par Eclipse
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((numeroChassis == null) ? 0 : numeroChassis.hashCode());
        result = prime * result + ((numeroMoteur == null) ? 0 : numeroMoteur.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Vehicule other = (Vehicule) obj;
        if (numeroChassis == null) {
            if (other.numeroChassis != null)
                return false;
        } else if (!numeroChassis.equals(other.numeroChassis))
            return false;
        if (numeroMoteur == null) {
            if (other.numeroMoteur != null)
                return false;
        } else if (!numeroMoteur.equals(other.numeroMoteur))
            return false;
        return true;
    }    
    
    
    // toString : génération via Eclipse avec "StringBuilder chaining".
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Vehicule [numeroMoteur=")
                 .append(numeroMoteur)
                 .append(", numeroChassis=")
                 .append(numeroChassis)
                .append(", numeroImmatriculation=")
                .append(numeroImmatriculation)
                .append(", dateMiseEnCirculation=")
                .append(dateMiseEnCirculation)
                .append(", interventions=")
                .append(interventions)
                .append("]");
        return builder.toString();
    }    
}

Constat : déjà plus de 100 lignes de code pour une classe pourtant « mini-rikiki » à la base.

Le code généré par Eclipse est convenable. J'ai choisi dans cet exemple l'option « StringBuilder chaining » pour toString mais souvent je préfère la méthode String.format que je trouve plus maintenable au détriment peut-être d'un peu de performance.

Mais cela reste du code source généré : tout code source, même généré doit être maintenable et maintenu !

II-B. Implémentation avec Lombok

Pour utiliser Lombok, il faut déclarer les éléments suivants dans le fichier pom.xml de votre projet MAVEN.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

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

Là où Lombok va se différencier d'un générateur de code source, c'est que dans son cas il va générer du ByteCode. Donc rien à maintenir, rien de visible. Cette stratégie est bien différente des assistants de génération de code d'Eclipse ou Netbeans.

D'ailleurs, il faut déclarer l'agent Lombok dans le fichier eclipse.ini, ou alors laisser faire l'installeur intégré au fichier lombok.jar. L'installeur place d'ailleurs lombok.jar dans le répertoire racine d'Eclipse.

 
Sélectionnez
1.
-javaagent:/opt/eclipse/lombok.jar
Cela fonctionne de la même manière avec NetBeans.

On va pouvoir se concentrer uniquement sur ce qui est important dans notre classe : ses données et leur représentation. Le reste sera généré par Lombok au moyen de ses annotations.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
@FieldDefaults(level=AccessLevel.PRIVATE)
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(of= {"numeroMoteur","numeroChassis"})
@ToString(of= {"numeroMoteur","numeroChassis","numeroImmatriculation","dateMiseEnCirculation"})
public class Vehicule implements Serializable
{
    // champs métier
    String numeroMoteur;
    String numeroChassis;
    String numeroImmatriculation;
    LocalDate dateMiseEnCirculation;
    
    // champs de relation
    List<Intervention> interventions = new ArrayList<>();
}

Dans cet exemple, pour montrer la puissance de Lombok, j'ai volontairement omis les modificateurs private devant chacun des champs. Les champs sont pourtant bien private grâce à l'annotation @FieldDefaults(level=AccessLevel.PRIVATE). Cela étant, après quatre ans d'usage, je préfère quand même faire figurer les modificateurs d'accès au niveau des champs.

Vous noterez que j'ai reporté l'instanciation de la liste au niveau de la déclaration du champ interventions. Cette instanciation figurait, dans l'exemple précédent, au niveau du constructeur.

Et voilà comment passer de plus de 100 lignes de code à 16 lignes ! C'est quand même bien plus clair et quel temps gagné ! Mais on ne va pas en rester là. Lombok peut nous apporter plus encore.

Avant cela, détaillons un peu les annotations utilisées :

  • @FieldDefaults(level=AccessLevel.PRIVATE) : passe tous les champs en private ;
  • @NoArgsConstructor : génère le constructeur sans argument et public ;
  • @AllArgsConstructor : génère le constructeur avec tous les arguments et public (pour l'exemple) ;
  • @Getter : génère tous les getters sur les champs ;
  • @Setter : génère tous les setters sur les champs ;
  • @EqualsAndHashCode(of=...) : génère equals et hashCode (et d'autres méthodes) sur les champs donnés ;
  • @ToString(of=...) : génère toString sur les champs donnés.

C'est quand même bien pratique, mais on peut aller encore plus loin. D'ailleurs, il y a un petit problème avec le @AllArgsConstructor qui permet ainsi de passer une liste qui ira supplanter la liste initiale… Bof bof. On va régler cela bientôt.

III. Le Pattern « Factory Method » avec Lombok

Reprenons l'exemple précédent et avec quelques ajustements nous aurons une classe uniquement instanciable au moyen d'une « factory method » statique.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
@FieldDefaults(level=AccessLevel.PRIVATE)
@RequiredArgsConstructor(staticName="of")
@EqualsAndHashCode(of= {"numeroMoteur","numeroChassis"})
@ToString(of= {"numeroMoteur","numeroChassis","numeroImmatriculation","dateMiseEnCirculation"})
public class Vehicule implements Serializable
{
    @Getter
    final String numeroMoteur;

    @Getter
    final String numeroChassis;
    
    @Getter
    final LocalDate dateMiseEnCirculation;

    @Getter     @Setter
    String numeroImmatriculation;
    
    // champs de relation
    @Getter
    List<Intervention> interventions = new ArrayList<>();
}

L'exemple devient un peu plus « sympa ». Je vais détailler ses particularités.

  • Le constructeur public par défaut sans argument a disparu. En fait il est bien là, mais il a été passé private par l'annotation @RequiredArgsConstructor. Cela empêche donc l'instanciation sans argument : ce n'est plus un Java Bean, mais ce n'est pas forcément grave. Attention toutefois aux specs comme CDI, JSF, et JPA, qui réclament pourtant ce constructeur.
  • une méthode statique « factory method » est générée et est nommée of(...) au moyen de l'annotation @RequiredArgsConstructor(staticName="of"). Ici la convention « of » est utilisée, comme pour les nouvelles API de Java 8, mais j'aurais pu utiliser les vieilles conventions comme newInstance(...). La méthode prendra en argument tous les champs marqués final ou les champs annotés avec @NonNull de Lombok. Attention à ne pas confondre avec @NotNull de Bean Validation ou de Guava.
  • equals, hashCode et toString ne changent pas.
  • Cette fois-ci, un contrôle plus fin sur les getters/setters est mis en place : seule l'immatriculation peut changer.

Usage de cette classe :

 
Sélectionnez
1.
2.
3.
Vehicule v = Vehicule.of("AABBCC123", "X06123", LocalDate.of(1989, 01, 18));
v.setNumeroImmatriculation("AA-123-BB");
System.out.println(v);

Et son résultat dans la console grâce à la méthode toString générée par Lombok :

 
Sélectionnez
1.
Vehicule(numeroMoteur=AABBCC123, numeroChassis=X06123, dateMiseEnCirculation=1989-01-18, numeroImmatriculation=AA-123-BB)

Ça commence déjà à faire des choses plutôt agréables, mais ce n'est pas fini ! Loin de là…

IV. Le Pattern « Builder » avec Lombok

Dans la catégorie des patterns un peu verbeux à mettre en place de manière traditionnelle, impliquant de la duplication de code (surtout de définitions d'attributs) ce qui est un comble pour des Design Patterns, voici le Builder mis en œuvre avec Lombok.

Avant d'en percevoir sa facilité de mise en œuvre, voici la classe Intervention implémentée avec Lombok et une « factory method » pour son instanciation :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
@FieldDefaults(level=AccessLevel.PRIVATE)
@AllArgsConstructor(staticName="of")
@EqualsAndHashCode(of="dateIntervention")
@ToString
@Getter
public class Intervention implements Serializable, Comparable<Intervention> 
{
    final LocalDate dateIntervention;
    
    final Long kilometrage;
    
    @Setter
    String libelle;
    
    @Setter
    BigDecimal prix;

    @Override
    public int compareTo(Intervention o) 
    {    
        return this.dateIntervention.compareTo(o.getDateIntervention());
    }
}

et voici la classe Vehicule ainsi que son Builder sous forme de classe interne que je commenterai par la suite :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
@FieldDefaults(level=AccessLevel.PRIVATE)
@EqualsAndHashCode(of= {"numeroMoteur","numeroChassis"})
@ToString(of= {"numeroMoteur","numeroChassis","numeroImmatriculation","dateMiseEnCirculation"})
@Builder
public class Vehicule implements Serializable
{
    @Getter
    @NonNull
    String numeroMoteur;

    @Getter
    @NonNull
    String numeroChassis;
    
    @Getter
    @NonNull
    LocalDate dateMiseEnCirculation;

    @Getter    @Setter
    String numeroImmatriculation;
    
    // champs de relation
    @Getter
    @Singular
    List<Intervention> interventions = new ArrayList<>();
}

Notez donc l'usage des annotations Lombok suivantes :

  • @Builder : génère une classe interne de type « Builder » capable de construire au moyen de « method chaining » une instance de la classe. L'opération terminale sera build() ;
  • @AllArgsConstructor(access=AccessLevel.PROTECTED) : le constructeur avec tous les arguments est nécessaire au Builder, mais pour le rendre inaccessible depuis un autre package, mais toujours depuis le Builder, je le place ici en protected ;
  • @NonNull : indique au Builder tous les champs obligatoires ;
  • @Singular : indique au Builder que l'on pourra ajouter des occurrences à la liste toujours grâce à du method chaining.

Voici un exemple d'usage de ce builder :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
Vehicule.VehiculeBuilder builder = Vehicule.builder();

Vehicule v = builder.numeroChassis("AABBCC123")
        .numeroMoteur("X06123")
        .dateMiseEnCirculation(LocalDate.of(1989, 01, 18))
        .numeroImmatriculation("AA-123-BB")
        .intervention(Intervention.of(LocalDate.of(2018, 03, 05), 1850000L, "Vidange", new BigDecimal("175.0")))
        .intervention(Intervention.of(LocalDate.of(2018, 02, 03), 1840000L, "Freins", new BigDecimal("210.0")))
        .intervention(Intervention.of(LocalDate.of(2018, 01, 15), 1830000L, "Embrayage", new BigDecimal("350.0")))
        .intervention(Intervention.of(LocalDate.of(2017, 12, 10), 1820000L, "Pneus", new BigDecimal("450.0")))
        .build();

System.out.println(v);
v.getInterventions().forEach(System.out::println);

et voici le résultat dans la console :

 
Sélectionnez
1.
2.
3.
4.
5.
Vehicule(numeroMoteur=X06123, numeroChassis=AABBCC123, dateMiseEnCirculation=1989-01-18, numeroImmatriculation=AA-123-BB)
Intervention(dateIntervention=2018-03-05, kilometrage=1850000, libelle=Vidange, prix=175.0)
Intervention(dateIntervention=2018-02-03, kilometrage=1840000, libelle=Freins, prix=210.0)
Intervention(dateIntervention=2018-01-15, kilometrage=1830000, libelle=Embrayage, prix=350.0)
Intervention(dateIntervention=2017-12-10, kilometrage=1820000, libelle=Pneus, prix=450.0)

V. Le Pattern « Factory » avec Lombok

Il est parfois, voire souvent, nécessaire d'avoir un peu plus de contrôle pour la création des instances, notamment dans le Builder, ce qui va nous permettre de mettre en place une solution fondée sur Lombok pour le Design Pattern « Factory ».

Avec Lombok, cela reste assez simple, en utilisant toujours la même annotation @Builder mais cette fois-ci sur des méthodes :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
@UtilityClass
public class VehiculeFactory 
{
    @Builder(builderClassName="SmallBuilder", builderMethodName="smallBuilder")
    public static Vehicule newVehicule(String numeroChassis, String numeroMoteur)
    {
        return Vehicule.builder().numeroChassis(numeroChassis)
                     .numeroMoteur(numeroMoteur)
                     .dateMiseEnCirculation(LocalDate.now())
                     .numeroImmatriculation("XX-XXX-XX")
                     .build();
    }
    
    @Builder(builderClassName="FullBuilder", builderMethodName="fullBuilder")
    public static Vehicule newVehicule(String numeroChassis, 
                       String numeroMoteur, 
                       LocalDate dateMiseEnCirculation, 
                       String numeroImmatriculation)
    {
        return Vehicule.builder().numeroChassis(numeroChassis)
                     .numeroMoteur(numeroMoteur)
                     .dateMiseEnCirculation(dateMiseEnCirculation)
                     .numeroImmatriculation(numeroImmatriculation)
                     .build();
    }
}

Quelques explications :

  • @UtilityClass : parce que cette factory n'a pas vocation à être instanciée, le constructeur devient privé. De plus, la classe devient aussi final ;
  • les méthodes de construction doivent être statiques : rien de choquant ;
  • @Builder(builderClassName="...", builderMethodName="...") : déclarer une classe interne de type Builder et qui va se générer en fonction des arguments de la méthode. Il y aura autant de builders internes que d'annotations.

Et voici un petit usage :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
Vehicule v1 = VehiculeFactory.smallBuilder()
              .numeroChassis("AAABB123")
              .numeroMoteur("123ABCD")
              .build();

Vehicule v2 = VehiculeFactory.fullBuilder()
              .numeroChassis("AAABB578")
              .numeroMoteur("458AAA")
              .dateMiseEnCirculation(LocalDate.now())
              .numeroImmatriculation("789-AAA-987")
              .build();

System.out.println(v1);
System.out.println(v2);

Ce qui donne sur la console :

 
Sélectionnez
1.
2.
Vehicule(numeroMoteur=123ABCD, numeroChassis=AAABB123, dateMiseEnCirculation=2018-03-06, numeroImmatriculation=XX-XXX-XX)
Vehicule(numeroMoteur=458AAA, numeroChassis=AAABB578, dateMiseEnCirculation=2018-03-06, numeroImmatriculation=789-AAA-987)

VI. Préconisations (enfin…)

Je les jette en vrac, avec une petite justification quand même.

  • Ne pas utiliser @Data : comme annoncé en préambule, cela génère EqualsAndHashCode sur tous les champs, pareil pour ToString, ce qui peut occasionner des exécutions cycliques quand on a des relations bidirectionnelles entre les classes.
  • Toujours utiliser @EqualsAndHashCode et @ToString en précisant les champs avec l'attribut of=....
  • Spécifier les Getter / Setter sur les champs, et non pas sur la classe (en gros, pas comme dans tous les exemples que je viens de donner).
  • Attention au Builder : intégrer un Builder sera possible, mais JPA, JSF ou CDI attendront que le constructeur sans argument soit présent avec le niveau « protected ».
  • Attention aux logiciels de revue de code qui n'analysent pas le ByteCode mais que le code source : ils sont perdus…

VII. En guise de conclusion

C'est, vous l'aurez compris, une bibliothèque très puissante dont je ne peux plus me passer, comme annoncé en préambule.

Il y a encore pas mal d'annotations que je n'ai pas couvertes ici, mais qui sont tout aussi utiles :

  • @CommonsLog ou @Slf4j ou encore @Log : pour les logs faciles ;
  • @Value : pour les objets immuables ;
  • @Cleanup : pour la libération de ressources ;
  • @Delegate : pour gérer correctement les collections dans les compositions avec un délégué ;
  • @Synchronized : pour de la synchronisation ENFIN gérée simplement et correctement.

On va dire que ça fera partie d'un prochain billet…

N'hésitez pas à me faire part de vos usages de Lombok en commentaire, que je reporterai ici le cas échéant.

VIII. Remerciements

Cet article a été publié avec l'aimable autorisation François-Xavier Robin.

Nous tenons à remercier f-leb pour sa relecture orthographique attentive de cet article et Mickael Baron pour la mise au gabarit.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2019 François-Xavier Robin. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.