
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