Project Lombok Tutorial


Project Lombok ist eine Bibliothek, welche Annotationen bereitstellt um einfacher mit Java arbeiten zu können. Neben Annotationen welche Getter und Setter automatisch generieren bietet Lombok jedoch noch eine Handvoll weiterer Features, welche ich kurz vorstellen möchte.

val/var

Mit val und var lassen sich lokale Variable deklarieren. Ihr Typ wird anhand des initialen Ausdrucks einmalig festgelegt. var deklariert eine normale lokale Variable, während val diese zusätzlich als final markiert.

var count = 0;
for (val i : new int[] {1,2,3,4}) {
  count++;
}

@NonNull

@NonNull wirft eine java.lang.NullPointerException sobald das damit annotierte Argument als Null übergeben wird. Damit kann der klassische Null-Check am Anfang einer Methode einfach ersetzt werden.

public void doSomething(@NonNull Integer param) {
}

@Cleanup

@Cleanup kümmert sich darum, dass Streams beim verlassen des aktuellen Scopes geschlossen werden. Im Hintergrund wird dabei nichts weiter als ein try-finally-Block um den Stream gebaut. Dem Entwickler bleibt diese lästige Aufgabe damit jedoch erspart.

@Cleanup
InputStream in = new FileInputStream("/etc/hallo.txt");

@Getter/@Setter

Die Annotationen @Getter und @Setter fügen für ein Feld automatisch Getter- und Setter-Methoden zu seiner Klasse hinzu. Die Annotationen können ebenso auf die Klasse direkt angewandt werden um alle Felder mit Zugriffsmethoden zu versehen.

Mit Hilfe des AccessLevel kann zudem die Sichtbarkeit bestimmt werden. Mögliche Werte hierfür sind PUBLIC, PROTECTED, PACKAGE und PRIVATE.

@Getter
@Setter
public class MyClass {

  @Setter(AccessLevel.PROTECTED)
  private Integer field;
}

@ToString

Die @ToString-Annotation dient hauptsächlich dem Debugging. Klassen erhalten damit eine ToString-Methode, welche es dem Debugger erlaubt eine Beschreibung der Klasse mit ihren Feldern anzuzeigen. Die Annotation hat mehrere Parameter:

  • includeFieldNames: Zeigt die Namen der Felder an. Diese Option ist standardmäßig true.
  • exclude: Entfernt einzelne Member.
  • of: Zeigt nur die hier definierten Felder in der Ausgabe an.
  • callSuper: Bezieht die Superklasse mit ein.
  • doNotUseGetters: Falls true, werden die Getter nicht aufgerufen, sondern direkt die Felder der Klasse.
  • onlyExplicitlyIncluded: Bezieht nur Felder mit der @ToString.Include-Annotation ein.

Um die Ausgabe weiter anpassen zu können kann jedes Feld mit @ToString.Include und den Parametern rank und name angepasst werden.

@ToString(onlyExplicitlyIncluded=true)
public class MyClass {
  @ToString.Include(name="Best Field")
  private String bestField;
}

@EqualsAndHashCode

Die @EqualsAndHashCode-Annotation erstellt Implementierungen für hashcode() und equals(Object other). Wird der Parameter callSuper auf true gesetzt, so werden dabei die Methoden der Superklasse ebenfalls in die Implementierung aufgenommen. Logischerweise funktioniert das nur mit Klassen, welche auch von einer Klasse ableiten und nicht nur von Object.

Im allgemeinen werden alle Felder welche nicht statisch sind in die jeweiligen Implementierungen einbezogen. Mit @EqualsAndHashCode.Exclude kann zudem ein Feld explizit ausgeschlossen werden. Der umgekehrte weg ist mit dem Parameter onlyExplicitlyIncluded gegangen werden. Ist dieser gesetzt wird kein Feld berücksichtigt, das nicht mit @EqualsAndHashCode.Inlude annotiert worden ist.

@EqualsAndHashCode(callSuper=true)
class CommentedPerson extends Person {
  @EqualsAndHashCode.Exclude
  private String comment;
}

@NoArgsConstructor/@RequiredArgsConstructor/@AllArgsConstructor

Die @NoArgsConstructor-Annotation erzeugt einen Standardkonstruktor für die annotierte Klasse. Dies ist besonders für hilfreich für serialisierbare Klassen, spart jedoch nur zwei Zeilen Code. Falls ein Feld final ist und dadurch initialisiert werden muss, so wird dies mit null, 0 oder false initialisiert.

Mit @ReguiredArgsConstruor wird ein Konstruktor erstellt, welcher alle final-Felder, sowie Felder mit der @NonNull-Annotation als Parameter erzeugt.

Die @AllArgsConstructor-Annotation erzeugt einen Konstruktor mit Parametern für jedes einzelne Feld der Klasse. Statische Felder werden ignoriert. @Non-Null-Felder werden explizit auf null hin überprüft.

@Data

Mit @Data werden die Annotationen @ToString, @EqualsAndHashCode, @Getter, @Setter und @NoArgsConstructor zusammengefasst. Die Regeln für die erwähnten Klassen wie etwa der AccessLevel für Setter gelten dabei weiterhin. Die @Data-Annotation ist der einfachste Weg alle üblicherweise benötigten Methoden zu einer Klasse hinzuzufügen.

@Builder

Die @Builder-Annotation erlaubt die einfache Generierung von Klassen, welche dem Builder-Pattern folgen. Das Builder-Pattern erlaubt es Klassen komfortabel zu initialisieren.

Address.builder().name("Bill Gates").street("73rd Ave").zip("1835").build();

In der annotierten Klasse erzeugt Lombok dabei eine statische innere Builder-Klasse, welche mit der Methode builder() instantiiert werden kann. Die Builder-Klasse wiederum besitzt Setter-Methoden für die einzelnen Felder welche die Builder-Klasse selbst zurück geben. Somit ist es möglich mehrere Setter aneinander zu reihen. Nachdem die benötigten Felder befüllt sind, kann die ursprüngliche Klasse mit build() erzeugt werden.

Mit @Builder.Default können Default-Werte für Felder angegeben werden, welche bei dem Build-Prozess nicht berücksichtigt werden müssen.

Mit @Singular können Collection, Set und Map jeweils einzeln befüllt werden. Anstelle einer Collection an den Setter zu übergeben kann so beispielsweise der Setter während des Build-Prozesses mehrmals aufgerufen werden um die Collection zu füllen. Neben den Java-Collections werden zudem auch googles Guava-Collections unterstützt.

Address.builder().name("Bill Gates").name("Melinda Gates").build();
@Builder
public class Adress {
  @Singular
  private List<String> name;

  @Builder.Default
  private String street = "unknown";

  private String zip;
}

@SneakyThrows

@SneakyThrows ist eine eher umstrittene Annotation. Mit ihr können Methoden annotiert werden welche eigentlich mit der throw-Klausel deklariert werden müssten. Das Problem dürfte bekannt sein: Irgendwo in den Untiefen eines Programms soll eine Methode aufgerufen werden, welche eine Exception wirft. Wenn die Fehlerbehandlung nicht direkt implementiert werden soll, sondern dies viel weiter oben in der Objektstruktur geschieht, so muss nun jede Methode in der Aufrufkette mit der throw-Klausel versehen werden. Dies ist nicht nur lästig, sondern verleitet auch dazu jede Methode schlicht eine Exception werfen zu lassen ohne explizit auf die spezielle Klasse einzugehen.

Intern wrapped Lombok die Methode in einen try-catch-Block und ruft die Methode Lombok.sneakyThrow(exception) auf. Dadurch wird die originale Exception nicht verschluckt, sondern erneut geworfen, jedoch unsichtbar für den Java-Compiler.

Im Idealfall sollte @SneakyThrows dazu verwendet werden, wenn der Entwickler genau weiß, dass keine Exception geworfen werden kann.

@SneakyThrows(UnsupportedEncodingException.class)
public String createString(byte[] bytes) {
  return new String(bytes, "UTF-8");
}

@Synchronized

Die Annotation @Synchronized verhält sich ähnlich zum Methoden-Modifizierer synchronized. Der Unterschied besteht darin, dass nicht auf .this gelocked wird, sondern auf ein Feld mit dem Namen $LOCK. Es ist zudem möglich mehrere Felder für unterschiedliche Locks zu definieren. Für Locks mit einem definierten Namen müssen die entsprechenden Felder jedoch vorhanden sein. Lombok erkennt zudem ob es sich um einen nur lesenden Lock handelt und

public class LockMe {
  private Object secondLock = new Object();

  @Lock
  public void doSomething() {...}

  @Lock("secondLock")
  public void doSomethingElse() {...}
}

@Log

@Log generiert einen Logger, welcher als Feld log referenziert werden kann. @Log benutzt dabei die Logger-Klasse java.util.logging.Logger. Für weitere Logging-Frameworks stehen @CommonsLog, @Flogger, @JBossLog, @Log4j, @Log4j2, @Slf4j und @XSlf4j zur Verfügung. Alle diese Annotationen haben eigene Parameter mit denen sie konfiguriert werden können.

@Log
public class MyClass {
  public void myFunction() {
    this.log.severe("Hello World");
  }
}