Lombok

Lesezeit: 10 Minuten

Boilerplate-Code ist ein ständiger Begleiter im Java-Umfeld. Die Mehrheit aller Getter und Setter tun genau das, was man erwartet: Den Wert einer Variablen eins zu eins zu setzen bzw. zurückzugeben. equals() und hashcode(), da wo sie benötigt werden, beziehen oftmals alle Felder einer Klasse mit ein. toString() liefert eine lesbare Aneinanderreihung der Werte aller Felder einer Klasse und Konstruktoren setzen die Werte aller Felder einer Klasse oder zumindest der Felder, die unveränderbar (immutable) sein sollen. All das führt zu einer Menge Code, die dann mitunter vom Wesentlichen ablenkt. Und hier setzt Lombok an.

Setup

Lombok aufzusetzen erfordert je nach Entwicklungsumgebung etwas Aufwand. Für Nutzer von Eclipse muss zunächst Lombok als Standalone-Jar ausgeführt werden. Dies startet einen Wizard, der Eclipse-Installationen entdeckt und für diese das Lombok-Plugin installiert.

Bei Verwendung von IntelliJ IDEA kann Lombok aus dem Pluginmenü ausgewählt werden. Zusätzlich muss in den Einstellungen noch das Annotation Processing aktiviert werden.

In jedem Fall muss Lombok auch als Abhängigkeit im verwendeten Build-Management-Tool wie Maven oder Gradle eingetragen werden.

Annotation Processing

Lombok macht sich das mit Java 5 eingeführte Annotation Processing zunutze. Ein Annotation Processor wie der von Lombok wird dabei vom Java-Compiler zur Compile-Zeit aufgerufen und kann den Java-Code auf Annotationen untersuchen und für diese den Abstract Syntax Tree (AST) modifizieren, sprich Code verändern. Dieser veränderte Code existiert dann nur im Bytecode, also nicht in den .java-Source-Dateien. Eine moderne IDE wie Eclipse und IntelliJ IDEA mit aktiviertem Lombok Plugin wendet diesen Mechanismus in Echtzeit an. Wird auf diesem Weg z.B. mit @Getter ein Getter für ein Feld einer Klasse erzeugt, so taucht dieser nicht im Quellcode auf, die Code Completion schlägt diese Methode aber trotzdem vor und der Code bleibt syntaktisch korrekt.

Akzessoren

Eine Menge Boilerplate-Code kann durch den Einsatz von @Getter und @Setter vermieden werden. Die Annotationen können sowohl an der Klasse als auch an einzelnen Feldern gesetzt werden. Zwei Beispiele:

1
2
3
4
5
6
7
8
9
10
11
@Getter
@Setter
public class CustomerRepresentation {
 
    private String id;
 
    private String name;
 
    private String address;
 
}

Diese Klasse generiert öffentliche Getter und Setter für alle Felder, da die entsprechenden Annotationen an der Klasse hinterlegt sind. Möchte man zum Beispiel, dass die id nur package private per Getter herausgegeben und nur innerhalb der Klasse per Setter gesetzt werden kann, so kann man die Felder einzeln annotieren:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CustomerRepresentation {
 
    @Getter(AccessLevel.PACKAGE)
    @Setter(AccessLevel.PRIVATE)
    private String id;
 
    @Getter
    @Setter
    private String name;
 
    @Getter
    @Setter
    private String address;
 
}

Ohne Lombok sähe der Code übrigens so aus:

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
public class CustomerRepresentation {
 
    private String id;
 
    private String name;
 
    private String address;
 
    String getId() {
        return id;
    }
 
    private void setId(String id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getAddress() {
        return address;
    }
 
    public void setAddress(String address) {
        this.address = address;
    }
}

Konstruktoren

Lombok bietet die drei Annotationen @NoArgsConstructor, @RequiredArgsConstructor und @AllArgsConstructor um Konstruktoren automatisch zu generieren. Der @RequiredArgsConstructor setzt nur die Felder, die als final markiert sind. Alle Konstruktoren können auch optional mit einer statischen Factory-Methode generiert werden, wie nachfolgendes Beispiel verdeutlicht:

1
2
3
4
5
6
7
8
9
10
11
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(staticName = "of")
public class CustomerRepresentation {
 
    private String id;
 
    private String name;
 
    private String address;
 
}

Dieser Code mit Lombok-Annotationen entspricht folgendem Java-Code ohne Annotationen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CustomerRepresentation {
 
    private String id;
 
    private String name;
 
    private String address;
     
    protected CustomerRepresentation() {
         
    }
     
    public static CustomerRepresentation of(String id, String name, String address) {
        CustomerRepresentation instance = new CustomerRepresentation();
        instance.id = id;
        instance.name = name;
        instance.address = address;
        return instance;
    }
 
}

Object-Methoden

Die Methoden Equals, Hashcode und ToString der Klasse Object können auch über entsprechende Lombok-Annotationen generiert werden. Für Equals und Hashcode gibt es die gemeinsame Annotation @EqualsAndHashcode. Für ToString gibt es @ToString. Beide Annotationen lassen sich konfigurieren, um zu steuern, ob ein Aufruf der entsprechenden Methode an der Elternklasse über super erfolgt und welche Felder in die Berechnung einbezogen werden. Hierbei können über exclude Felder angegeben werden, die nicht einbezogen werden, oder über of nur ganz spezifische Felder herangezogen werden. exclude ist ein sinnvoller Parameter, um bei wechselseitiger Abhängigkeit zweier Klassen zu vermeiden, dass es zu einer endlosen Rekursion kommt (Klasse A referenziert Klasse B, diese referenziert Klasse A). Leider lässt sich die Formatierung der toString()-Ausgabe nur sehr begrenzt konfigurieren. Nachfolgend ein Beispiel und die dazugehörige Ausgabe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public class CustomerRepresentation {
 
    private String id;
 
    private String name;
 
    private String address;
 
    public static void main(String... args) {
        System.out.println(new CustomerRepresentation("1", "Christian", "Postfach XXX 22303 Hamburg"));
    }
}
 
// Ausgabe: CustomerRepresentation(id=1, name=Christian, address=Postfach XXX 22303 Hamburg)

Builder

Die Annotation @Builder generiert einen Builder nach dem gleichnamigen Entwurfsmuster für die annotierte Klasse. Im nachfolgenden Beispiel setze ich auf das vorherige toString()-Beispiel auf und verwende statt dem AllArgsConstructor einen Builder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Builder
@EqualsAndHashCode
@ToString
public class CustomerRepresentation {
 
    private String id;
 
    private String name;
 
    private String address;
 
    public static void main(String... args) {
        System.out.println(CustomerRepresentation.builder()
                .id("1")
                .name("Christian")
                .address("Postfach XXX 22303 Hamburg")
                .build());
    }
}

Wenn ich übrigens die vorhandene Klasse CustomerRepresentation nicht erweitern dürfte, so kann ich die @Builder-Annotation auch an eine Methode hängen. Dafür würde ich dann beispielsweise eine neue Klasse CustomerRepresentationBuilder anlegen und eine Methode public CustomerRepresentation newInstance() mit @Builder annotieren.

Die aggregierte Annotation „Data“

Die Annotation @Data fasst die folgenden Annotationen zusammen:

  • @Getter
  • @Setter
  • @RequiredArgsConstructor
  • @ToString
  • @EqualsAndHashcode

Die @Data-Annotation steht für ein Datenobjekt, welches öffentliche (public) und in der Regel veränderbare (mutable) Informationen enthält. Es ist außerdem dazu geeignet in Collections verwaltet zu werden und bietet eine transparente Textdarstellung in Logausgaben.

Weitere Annotationen

Lombok unterstützt diverse Logging-Frameworks mit eigenen Annotationen. Dazu gehören das Apache Commons Logging, Googles Fluent Logger, JBoss Logging, Java Util Logging, Log4j und Slf4j. Die Funktionsweise ist aber immer die gleiche: Es wird ein Feld private static final <Logger> log bereitgestellt, über dieses in der Klasse dann komfortabel geloggt werden kann. Zur Demonstration greife ich das vorherige Beispiel nochmal auf und schreibe dieses Mal nicht nach stdout, sondern ins Log.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
@Slf4j
public class CustomerRepresentation {
 
    private String id;
 
    private String name;
 
    private String address;
 
    public static void main(String... args) {
        log.info(CustomerRepresentation.builder()
                .id("1")
                .name("Christian")
                .address("Postfach XXX 22303 Hamburg")
                .build().toString());
    }
}

Eine weitere erwähnenswerte Annotation is @NonNull. Für damit annotierte Felder und Parameter wird ein Null-Check eingebaut, so dass beim Versuch diese auf null zu setzen eine NullPointerException geworfen wird.

Der richtige Einsatz von Lombok

Die Verwendung von Lombok ist eine Team-Entscheidung, denn ohne das entsprechende Plugin kann mit Lombok annotierter Code nicht sinnvoll weiterentwickelt werden. Lombok erleichtert es einem sich auf das Wesentliche zu konzentrieren, indem ablenkender Standard-Code auf ein Minimum reduziert werden kann. Lombok führt aber nicht automatisch dazu, dass man besseren Code schreibt und der Einsatz von Lombok sollte mit Bedacht erfolgen. In einem Domain Model nach Domain Driven Design würde ich es zum Beispiel vermeiden, Klassen mit @Getter zu annotieren, sofern es sich nicht um komplett öffentliche unveränderliche Wertobjekte handelt. Die unterschiedlichen Konstruktor-Annotationen und den Builder würde ich nur dort einsetzen, wo ich sie auch explizit verwenden will und es spricht auch nichts dagegen auf Lombok zu verzichten, wenn das Ergebnis ohne Lombok besser aussieht.

Lombok hat für mich gegenüber der Code-Generierung durch eine IDE den Vorteil, dass der Code nicht veraltet. Ein @ToString bleibt aktuell, wenn einer Klasse neue Felder hinzugefügt werden. Ein händisch erzeugtes toString() veraltet aber, wenn man es nicht neu generiert. Das Gleiche gilt für equals() und hashcode().

Ein @NonNull ist viel dezenter, als ein händisch geschriebener If-Block mit einer Validierung, von denen es in großen Projekten schnell hunderte geben kann. Ich halte es allerdings für eine Fehlentscheidung, dass @NonNull per Default in eine NullPointerException übersetzt wird. Passender wäre meiner Meinung nach eine IllegalArgumentException. Zum Glück bietet Lombok die Möglichkeit, dies per Konfigurationsdatei anzupassen.

Schade finde ich auch, dass die Ausgabe von @ToString nicht der Json-ähnlichen Darstellung der generierten toString()-Methode moderner IDEs entspricht. Die Lombok-Formatierung Klasse(feld1=wert1, feld2=wert2) wird sehr schnell unübersichtlich, wenn Felder Leerzeichen oder sogar Kommata und Gleichheitszeichen enthalten). Vielleicht ändert sich hieran ja noch etwas in einer zukünftigen Version.

Folgende konkrete Anwendungszwecke für Lombok haben sich in meinem Umfeld besonders gut bewährt:

  • Constructor Injection via @RequiredArgsConstructor für Spring-Services (@Service)
  • @NoArgsConstructor(access = AccessLevel.PROTECTED) für JPA-Entitäten (JPA erfordert einen NoArgsConstructor, der mindestens protected ist)
  • @Builder und @Data für Schnittstellenobjekte wie etwa REST-Repräsentationen, die inkrementell gebaut werden und keine Geschäftslogik enthalten
  • @Slf4j für alle Klassen, in denen geloggt wird (bei Einsatz von Slf4j, ansonsten entsprechend alternative Log-Annotation)
  • @ToString für alle Klassen, die in Logausgaben auftauchen
- offizielle Webseite von Project Lombok