Lomblog

Project Lombok [1] ist ein Java-Framework, welches unter Verwendung von Annotationen eine automatisierte Manipulation und Erweiterung von Java-Sourcecode auf Bytecode-Ebene zur Kompilierzeit ermöglicht. Das Ziel ist dabei die Vermeidung von repetetivem Boilerplate-Code.

Im Rahmen dieses Blogbeitrags soll anhand einiger Beispiele vorgestellt werden, welchen Funktionsumfang Lombok bietet und welcher konkrete Bytecode hiermit generiert werden kann. Abschließend soll eine Bewertung im Hinblick auf Nutzbarkeit und Nützlichkeit im Entwicklungs-Alltag erfolgen.

Verwendung von Lombok

Zunächst ein kurzes Beispiel für die Verwendung von Lombok anhand einer Java-Klasse:

@RequiredArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class Movie {
    private final String name;
    private final int year;
    private List<String> genres;
}

Zu sehen sind Lombok-spezifische Klassenannotationen, die dafür sorgen, dass verschiedene Methoden während des Kompilierprozesses innerhalb der zu kompilierenden Klasse erzeugt werden. Auf die konkrete Bedeutung der Annotationen wird an späterer Stelle eingegangen, hier soll lediglich die Verwendung der Annotationen gezeigt werden.

Damit diese Annotationen durch Lombok während des Kompiliervorgangs ausgewertet und verarbeitet werden können, muss sich die lombok.jar hierfür lediglich im Klassenpfad befinden. Um dies zu erreichen, bieten sich unter anderem die folgenden Möglichkeiten an:

Manuell:

javac -cp lombok.jar Movie.java

Maven:

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

Gradle:

provided group: 'org.projectlombok', name: 'lombok', version: '1.16.16'

Aber warum genau ist es ausreichend, dass sich Lombok im Klassenpfad während der Kompilierzeit befindet? Die Antwort auf diese Frage liefert der Java Specification Request 269 [2]. Dieser beschreibt die Pluggable Annotation Processing API, die mit Java 6 eingeführt wurde. Sie bietet die Möglichkeit, durch die Implementierung und Registrierung eines sogenannten Annotation Processors Annotationen zur Kompilierzeit auszuwerten und auf Basis dieser Auswertung Manipulationen und Erweiterungen an Bytecode vorzunehmen oder vollständig neue Sourcefiles zu erzeugen.

Auf das Java Annotation Processing soll im Detail in einem zuküntigen Blogbeitrag näher eingegangen werden, da hier weniger die technische Umsetzung als vielmehr der fachliche Funktionsumfang Lomboks thematisiert werden soll.

Lomboks Funktionsumfang

Wie zuvor erläutert, basiert Lomboks Funktionalität auf Annotationen. Hierfür wird eine Menge von Klassen-, Attribut- und Methoden-Annotationen angeboten, um unterschiedliche Zwecke zu erfüllen. Eine vollständige Liste aller Annotationen samt möglicher Parametrisierung ist in der offiziellen Dokumentation [3] zu finden. Im folgenden soll ein kurzer Überblick über vorhandene Annotationen geliefert werden. Die nachfolgenden Code-Beispiele sind gesammelt in einer Beispiel-Applikation [4] zu finden.

Basis-Annotationen

@RequiredArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class Movie {
    private final String name;
    private final int year;
    private List<String> genres;
}

Obiges Beispiel zeigt eindrucksvoll, wie sehr die Verwendung von Lombok-Annotationen den Umfang der annotierten Klasse reduziert. Eine entsprechende vollständig ausimplementierte Java-Klasse käme, je nach Formatierung, auf einen Umfang von 90 bis 100 Zeilen gegenüber der zehn Zeilen umfassenden Java-Klasse mit Lombok-Annotationen. Hier wird erheblich an Boilerplate-Code eingespart, was im Umkehrschluss die Lesbarkeit des Sourcecodes für den Entwickler massiv erhöht. Die Bedeutung der Annotationen vermittelt die folgende Tabelle.

Annotation Bedeutung
@NoArgsConstructor Generiert einen Standardkonstruktor ohne Parameter. Existieren finale Felder resultiert dies in einem Compilerfehler.
@RequiredArgsConstructor Generiert einen Konstruktor, der alle finalen Felder als Parameter abbildet.
@AllArgsConstructor Generiert einen Konstruktor, der alle Felder als Parameter abbildet.
@Getter, @Setter Generieren Getter bzw. Setter und sind sowohl als Klassen- als auch als Attribut-Annotation verwendbar. @Setter generiert dabei Setter für alle nicht-finalen Felder.
@EqualsAndHashCode Generiert eine equals– und eine hashCode-Methode, wobei in deren Implementierung alle nicht-statischen Felder Berücksichtigung finden.
@ToString Generiert eine toString-Methode, die in der Standardkonfiguration den Klassennamen und alle Felder kommasepariert ausgibt.

Meta-Annotationen

Meta-Annotationen fassen eine Menge von Basis-Annotationen sinnvoll für verschiedene Anwendungsszenarien zusammen. Diese Meta-Annotationen sind @Data und @Value.

@Data

@Data kombiniert die Annotationen @RequiredArgsConstructor, @Getter, @Setter, @EqualsAndHashCode und @ToString zu einer Meta-Annotation. Eine hiermit annotierte Klasse ist analog zu einem Value Object aus C [5] oder einer Data Class aus Kotlin [6] zu verstehen. Durch die Verwendung dieser Annotation reduziert sich der Umfang der zuvor vorgestellten Movie-Klasse auf nur noch sechs Zeilen.

@Data
public class Movie {
    private final String name;
    private final int year;
    private List<String> genres;
}

@Value

@Value stellt die Immutable-Variante von @Data dar. Alle Felder werden, sofern sie nicht durch die Annotation @NonFinal explizit markiert werden, durch Lombok als private final deklariert.

@Value
public class Movie {
    String name;
    int year;
    @NonFinal
    List<String> genres;
}

Spezielle Annotationen

Im folgenden sollen einige Annotationen vorgestellt werden, die für spezifische Anwendungsszenarien angeboten werden.

@Builder

@Builder generiert eine statische innere Klasse nach dem Builder-Pattern. Dabei wird eine builder-Methode zum Start und eine build-Methode zum Abschluss der Objektkonstruktion erzeugt. Über die Annotation @Singular lässt sich steuern, ob einzelne Elemente über eine entsprechende Methode einer Liste von Objekten hinzugefügt werden kann.

@Builder
@Getter
@EqualsAndHashCode
@ToString
public class Movie {
    private final String name;
    private final int year;
    @Singular
    private List<String> genres;
}

Nachfolgendes Beispiel zeigt die Verwendung obiger Klasse.

Movie.builder()
    .name("Jurassic Park")
    .year(1993)
    .genre("Science-Fiction")
    .genre("Horror")
    .build();

@NonNull

Generiert Sourcecode zur Prüfung auf NullPointer für Parameter einer Methode oder eines Konstruktors. Enthält der Parameter eine NULL-Referenz, wird eine NullPointerException("parametername") geworfen.

public void addMovie(@NonNull Movie movie) {
    this.movies.add(movie);
}

@Synchronized

@Synchronized umgibt den Methodeninhalt mit dem synchronized-Modifier, wobei für jeden synchronized-Block über das generierte Klasseattribut $lock als Lock-Objekt synchronisiert wird.

@Synchronized
public void addMovie(Movie movie) {
    this.movies.add(movie);
}

Obiger Sourcecode wird von Lombok in die nachfolgend abgebildeten Elemente übersetzt, die aus dem resultierenden Bytecode zur Veranschaulichung dekompiliert wurden.

private final Object $lock = new Object[1];

public void addMovie(Movie movie) {
    synchronized($lock) {
        this.movies.add(movie);
    }
}

@Cleanup

@Cleanup führt eine Aufräum-Aktion beim Verlassen des aktuellen Programmscopes aus. Hierzu wird ein try/catch-Block um den entsprechenden Codeblock generiert und die Methode close auf dem annotierten Objekt im finally-Block aufgerufen. Besitzt das annotierte Objekt keine close-Methode, kann eine alternative parameterlose Methode konfiguriert werden.

public void saveMovies(byte[] moviesAsJson, File file) throws IOException {
    @Cleanup OutputStream outputStream = new FileOutputStream(file);
    outputStream.write(moviesAsJson);
}

Obiger Sourcecode wird von Lombok in die nachfolgend abgebildeten Elemente übersetzt.

public void saveMovies(byte[] moviesAsJson, File file) throws IOException {
    FileOutputStream outputStream = new FileOutputStream(file);
    try {
        outputStream.write(json.getBytes());
    } finally {
        if(Collections.singletonList(outputStream).get(0) != null) {
            outputStream.close();
        }
    }
}

@SneakyThrows

@SneakyThrows bietet die Möglichkeit, Checked-Exceptions zu werfen, ohne diese in der Methodensignatur definieren zu müssen. Dies ist deshalb möglich, weil auf Ebene der Java-VM alle Exceptions, unabhängig davon ob diese Checked sind oder nicht, geworfen werden können

@SneakyThrows(IOException.class)
public void saveMovies(byte[] moviesAsJson, File file) {
    @Cleanup OutputStream outputStream = new FileOutputStream(file);
    outputStream.write(moviesAsJson);
}

@Slf4j

Generiert ein statisches Klassenattribut vom Typ Logger stammend aus dem SLF4J-Logging-Framework.

@Slf4j
public class MovieDB {
    ...
}
public class MovieDB {
   private static final Logger log = LoggerFactory.getLogger(MovieDB.class);
   ...
}

Unterstützt werden darüber hinaus auch: @CommonsLog, @JBossLog, @Log, @Log4j, @Log4j2, @XSlf4j.

Lombok zur Entwicklungszeit

Lombok generiert Code erst zur Kompilierzeit. Dies führt natürlich zur Entwicklungszeit in jeder gängigen IDE zu Fehlern hinsichtlich fehlender Konstruktoren oder Methoden. Um dieses Problem zu beheben, werden für viele dieser IDEs Plugins bereitgestellt, die bereits zur Entwicklungszeit entsprechende Kompilierungen vornehmen. Dabei bieten diese Plugins sogar eine lombok– bzw. delombok-Funktionalität, um den Umstieg zu Lombok bzw. die Abkehr von Lombok in einem Projekt zu vereinfachen.

Zur Installation der Plugins sei auf die offizielle Lombok-Dokumentation verwiesen: Intellij [7], Eclipse [8], Netbeans [9]

Zusammenfassung und Bewertung

Die vorherigen Ausführungen haben gezeigt, dass Lombok ein sehr mächtiges, aber gleichzeitig auch sehr einfach zu verwendendes Framework ist.

Die Basisannotationen zur Generierung von Konstruktoren, Gettern und Settern sowie ToString-, Equals- und HashCode-Methoden stellen dabei das herausragende Merkmal Lomboks dar. Zusammengefasst in entsprechenden Meta-Annotationen führt Lombok hier Sprachfeatures aus modernen Programmiersprachen, wie etwa die aus Kotlin bekannten Data-Classes, in Java ein. Hieraus resultiert eine erhebliche Einsparung an Lines-of-Code verbunden mit der Einsparung an Entwicklungs- und Wartungsaufwand. Sicherlich bieten alle fortgeschrittenen IDEs ebenfalls Möglichkeiten zur Sourcecode-Generierung an, hier muss man jedoch immer mit dem Nachteil leben, dass diese bei Anpassungen in der Klasse nicht entsprechend automatisiert nachgezogen werden.

Darüber hinaus bietet Lombok eine Menge von teilweise sehr speziellen Annotationen, die vermutlich in „realem“ Code eher selten zum Einsatz kommen. Dennoch scheinen die allermeisten dieser Annotationen durchaus Berechtigung zu besitzen, wenn es darum geht, maximale Einsparung an Boilerplate-Code zu erreichen.

Ein negativer Aspekt ist ganz sicher die Tatsache, dass der mit Lombok-Annotationen versehene Sourcecode nach der Java-Spezifikation syntaktisch nicht valide und somit auch nicht kompilierfähig ist, wenn Lombok nicht im Kompilierprozess Verwendung findet. Hiervon betroffen sind möglicherweise auch Tools, wie etwa solche zur statischen Codeanalyse oder der Analyse von Testabdeckung, die nicht auf Bytecode-Ebene, sondern auf Ebene des Sourcecodes arbeiten. Die Funktionsfähigkeit solcher Tools müsste vor der Verwendung Lomboks untersucht und bewertet werden.

Darüber hinaus bringt ein Einsatz Lomboks grundsätzlich ein großes Risiko mit sich: Die Weiterentwicklung Lomboks ist nicht garantiert an die Entwicklung der Sprache Java gebunden. Im schlechtesten Fall kann der Einsatz ein Upgrade auf eine neue Java-Version verhindern, wenn Lombok mit der neuen Version schlichtweg nicht mehr kompatibel ist. Ein Beispiel hierfür ist die Unterstützung des kürzlich erschienenen Java 9 mit dem Modulsystem Jigsaw [10]. War diese zunächst überhaupt nicht gegeben, wird sie inzwischen durch stetige Entwicklung sukzessive hergestellt [11]. Trotz erkennbarer Bemühungen des Entwicklerteams hinsichtlich Java 9 hat man jedoch keine Garantie für die Kompatibilität von Lombok-verwendendem Legacy-Code mit zukünftigen Java-Versionen, was darüberhinaus im krassen Gegensatz zur Philisophie der Sprache Java steht.

Zuvor erwähntes Risiko wird jedoch durch die hervorragende Toolunterstützung Lomboks in Form von Plugins für jede gängige IDE gemildert. Gerade die delombok-Funktionalität macht hier eine Ablösung Lomboks im zweifelsfall vergleichsweise einfach. Darüberhinaus macht die lombok-Funktionalität eine Evaluierung sowohl der grundsätzlichen Verwendung als auch der Kompatibilität sehr einfach.

Eben jene Einfachheit zeichnet Lombok zusammenfassend aus: Lombok in den Klassenpfad zur Kompilierzeit aufzunehmen und ein entsprechendes Plugin in der präferierten IDE zu installieren ist auf der einen Seite ein durchaus vertretbarer Aufwand, um auf der anderen Seite von den sehr gut durchdachten Annotationen enormen Profit zu erreichen.

Lombok bei n-design

Wir bei n-design sind immer an guter Qualität von Sourcecode hinsichtlich seiner technischen und fachlichen Sauberkeit, Lesbarkeit und Testbarkeit interessiert. Infolgedessen haben wir uns im Rahmen unseres freitäglichen Entwicklerfrühstücks mit Lombok in Form eines kurzen vorstellenden Vortrags mit anschließender Diskussion beschäftigt. Hier gab es durchaus gegensätzliche Meinungen über Lombok, die in die vorherige Bewertung eingeflossen sind. Wir kamen letztendlich zu dem Entschluss, Lombok im Rahmen eines unserer Projekte mit Bedacht einsetzen und auf die Probe stellen zu wollen.

„Mit Bedacht“ heißt in diesem Zusammenhang, dass wir ausschließlich die von Lombok bereitgestellten Basis- und Meta-Annotationen einsetzen wollen. Den Einsatz der Speziellen Annotationen erachten wir nicht als empfehlenswert, da diese synatktisch maßgeblich in den Sourcecode eingreifen und zudem kritische Aufgaben implizit erledigen (@Synchronized, @Cleanup), die aufgrund ihrer Wichtigkeit explizit durch den Entwickler implementiert werden sollten.

Wir haben Lombok nunmehr seit etwa einem halben Jahr in einem auf Spring und Java 8 basierenden Projekt im Einsatz und können konstatieren, dass Lombok aus technischer Sicht weder im Buildprozess noch im Entwicklungsprozess Probleme bereitet hat. Hier ist erfreulich zu sehen, dass sich die sehr stark auf Verwendung von Annotationen ausgerichteten Frameworks Spring und Lombok gegenseitig nicht negativ beeinflussen. Zudem verwenden wir problemlos sowohl Plugins für Eclipse als auch IntelliJ. Auf fachlicher Ebene haben wir als Entwickler gerade die Data-Classes (@Data) sehr schnell zu schätzen gelernt. Umso mehr freuen wir uns, dass jene Datenklassen als Feature für ein zukünftiges Java angedacht sind. Bis dahin halten wir aus technischer und fachlicher Sicht den Einsatz von Lombok eingeschränkt als empfehlenswert.