26.10.2012
Einleitung
In diesem Artikel wird kurz beschrieben was es mit dem Java-Framework Lombok auf sich hat und wie man es am Besten einsetzen kann, um Quellcode lesbarer zu machen und sich gleichzeitig unnötige Tipparbeit erspart. Dazu wird in einem kleinen Beispiel eine Klasse Person
mit einigen Member-Variablen erstellt und eine Testklasse, die die Objekte vom Type Person
erstellt und auf verschiedene Weisen benutzt.
Wer hat sich nicht schon oft geärgert, dass er getter und setter erstellen musste? Klar. Inzwischen kann man sie mit IDEs, wie Eclipse oder NetBeans generieren lassen. Aber warum? Normalerweise sollen sie keine Businesslogik enthalten, sondern nur zum Setzen und Leser von Werten verwendet werden. Das ist jedem Entwickler klar und deshalb macht es eigentlich wenig Sinn, dass sie Teil des Quellcodes sind, an dem der Programmierer arbeit. Andere Sprachen, wie Objective-C, lassen solchen Code schon lange weg und sind dadurch erheblich kompakter und lesbarer. In der Java-Welt ist es aber so, dass auch diese Methoden Teil des Quellcodes sind. Wenn es ganz schlimm läuft, dann verlangt ein Software-Architekt, dass man die Methoden mit JavaDoc versehen soll, weil ein Tool, wie Sonar dies empfiehlt. Ein weiterer Nachteil ist, dass durch diesen Boilerplate Code der Text, den der Programmierer immer wieder lesen muss, mehr wird und er dadurch von den wichtigen Textstellen ablenkt und ihr dadurch ausbremst. Die gettern und settern sind nur ein Beispiel für solche Codefragmente. Es gibt zahlreiche weitere, die im Folgenden aufgelistet sind:
- Konstruktoren die alle Membervariablen als Parametern enthalten
- Prüfung auf not null im Konstruktor
toString()
-Methode überschreibenhashcode()
-Methode überschreibenequals()
-Methode überschreiben- Logging initialisieren.
- Variablendeklarationen mit Typ für lokale Variablen
- IO-Streams schließen
- Ein
try-catch
-Block, der eine Exception nur fängt, um eine Fehlermeldung auszugeben.
Es wäre doch schön, wenn es ein Framework gäbe, welches einem diese Routinetätigkeiten abnimmt und den notwendigen Code einfach zur Compilezeit generiert, so dass er beim Programmieren und Lesen des Codes nicht ablenkt.
Das Java-Framework Lombok erledigt genau das.
In dem folgenden Beispiel werden diese Annotationen verwendet:
@Getter
@Setter
@ToString
@EqualsAndHashCode
@AllArgsConstructor
@NonNull
@Log4j
@Cleanup
@SneakyThrows
Und das Schlüsselwort val
durch das beim Deklarieren von Variablen kein Typ mehr angegeben werden muss.
Ein vollständige Liste mit allen Annotation und passenden Beispielen findet man unter Lombok Features.
Installation von Lombok in Eclipse
Als erstes muss das Lombok-Framework von der Seite Project-Lombok heruntergeladen werden. Anschließend öffnet man ein Konsolenfenster und führt das Kommando java -jar lombok.jar
aus. Daraufhin öffnet sich ein Installer-Fenster in dem im Normalfall das installierte Eclipse erkannt wird. Falls dies nicht der Fall ist, muss der Installationspfad von Eclipse angegeben werden.
Nach der erfolgreichen Installation wird folgender Dialog angezeigt. Nun muss Eclipse noch neu gestartet werden, damit Lombok verwendet werden kann.
Hinweis
Wenn schon kompilierten Quellcode in einem Eclipse-Projekt vorhanden war, der Lombok-Annotationen verwendet, dann muss ggf. Project->Clean... aufgerufen werden, damit die Klassen neu übersetzt werden.
Beispiel ohne Lombok
Im Folgenden wird an einem einfachen Beispiel erklärt, wie man mit Hilfe von Lombok eine Menge Quellcode einsparen kann. Dazu implementieren wir als erstes ein Pojo Person
in der herkömmlichen Weise, also ohne Lombok.
Der Klasse Person
Die Klasse Person
soll folgende Werte speichern: Nachname, Vorname, Geburtstag, Adresse, Stadt und Postleitzahl. Außerdem werden getter und setter benötigt. Zusätzlich brauchen wir einen default-Konstruktor ohne Parameter und einen Konstruktor über den alle Werte gesetzt werden können. Nebenbedingung dabei ist, dass Name (lastname
) und Vorname (firstname
) nicht null
sind. Zusätzlich müssen die Methode toString()
, equals()
und hashcode()
überschrieben werden. Bei den letzten beiden Methoden gilt die Nebenbedingung, dass die Werte Vorname (firstname
, Adresse (address
) und Geburtstag (birthday
) nicht berücksichtigt werden.
Der daraus resultierende Quellcode könnte so aussehen. Wegen der Übersichtlichkeit habe ich die JavaDoc weggelassen .
package org.hameister.lombok; import java.io.Serializable; import java.util.Date; public class Person implements Serializable { private static final long serialVersionUID = 1L; private String lastname; private String firstname; private Date birthday; private String address; private String city; private String zip; public Person() { } public Person(String lastname, String firstname, Date birthday, String addresse, String city, String zip) { if (lastname == null) throw new NullPointerException("Lastname must not null."); if (firstname == null) throw new NullPointerException("Firstname must not null"); this.lastname = lastname; this.firstname = firstname; this.birthday = birthday; this.address = addresse; this.city = city; this.zip = zip; } @Override public String toString() { return "Person(lastname=" + lastname + ", firstname=" + firstname + ", birthday=" + birthday + ", address=" + address + ", city=" + city + ", zip=" + zip + ")"; } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Person)) return false; Person p = (Person) o; if (!lastname.equals(p.getLastname())) return false; if (!city.equals(p.getCity())) return false; if (!zip.equals(p.getZip())) return false; return true; } @Override public int hashCode() { final int PRIME = 31; int result = 1; result = (result * PRIME) + (this.lastname == null ? 0 : this.lastname.hashCode()); result = (result * PRIME) + (this.city == null ? 0 : this.city.hashCode()); result = (result * PRIME) + (this.zip == null ? 0 : this.zip.hashCode()); return result; } public String getLastname() { return lastname; } public void setLastname(String lastname) { this.lastname = lastname; } public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getZip() { return zip; } public void setZip(String zip) { this.zip = zip; } }
Testklasse für Person
Zum Testen der Klasse Person
erstellen wir eine weitere Klasse mit einer main
-Funktion. In der Funktion erledigen wir folgende Dinge:
- Logger für Log4J initialisieren
- Ein Personen-Objekt erstellen und mit den settern die Werte setzten (im Beispiel:
Donald Duck, Entenstrasse 1, 1234 Entenhausen
) - Nachname abfragen und ausgeben
- Test der Methode
toString()
- Eine
ArrayList
mit einer Person erstellen - Ein weiteres Personen-Objekt mit dem Konstruktor mit allen Parametern aufrufen
equals()
undhashcode()
mit beiden Personen ausprobieren- Die
NullPointerException
provozieren - Eine Person in eine Datei serialisieren und anschließend wieder deserialisieren.
Anmerkung: Normalerweise würde man solche Test mit JUnit durchführen. Hier soll aber gezeigt werden, wie die Klasse Person
im normalen Programmfluss verwendet wird.
package org.hameister.lombok; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.apache.log4j.Logger; public class PersonTester { private static final Logger log = Logger.getLogger(PersonTester.class); /** * @param args */ public static void main(String[] args) { Person person = new Person(); // Getter and setter person.setFirstname("Donald"); person.setLastname("Duck"); person.setAddress("Entenstrasse 5"); person.setCity("Entenhausen"); person.setZip("1234"); System.out.println("Lastname: " + person.getLastname()); // Logging and toString log.debug(person.toString()); // Array with persons List<Person> persons = new ArrayList<Person>(); persons.add(person); // Constructor Person person2 = new Person("Duck", "Dagobert", null, "Entenstrasse 1", "Entenhausen", "1234"); // equals and hashcode log.debug("Same values except firstname and address: " + person.equals(person2)); log.debug("Hashcode " + person.getFirstname() + ": " + person.hashCode()); log.debug("Hashcode " + person2.getFirstname() + ": " + person2.hashCode()); // NotNull values in constructor try { new Person("Duck", null, new Date(), "Entenstrasse 5", "Entenhausen", "1234"); } catch (NullPointerException exception) { log.debug("Expected NPE."); } // Close open streams try { FileOutputStream fos = new FileOutputStream("DonaldDuck.ser"); try { ObjectOutputStream out = new ObjectOutputStream(fos); try { out.writeObject(person); } finally { if (out != null) { out.close(); } } } finally { if (fos != null) { fos.close(); } } FileInputStream fis = new FileInputStream("DonaldDuck.ser"); try { ObjectInputStream in = new ObjectInputStream(fis); try { Person person4 = (Person) in.readObject(); log.debug("Serialized Person: " + person4); } finally { if (in != null) { in.close(); } } } finally { if (fis != null) { fis.close(); } } } catch (FileNotFoundException e) { log.error("DonaldDuck.ser not found.", e); } catch (IOException e) { log.error("Cannot read stream.", e); } catch (ClassNotFoundException e) { log.error("Cannot find Person class.", e); } } }
Beispiel mit Lombok
Nun wird das gleiche Beispiel mit Hilfe des Java-Frameworks Lombok erstellt, um zu zeigen, wie übersichtlich und lesbar der Quellcode werden kann, wenn man das Framework einsetzt. Dazu wird eine Menge Quellcode gelöscht und durch die oben aufgelisteten Annotationen ersetzt.
Die Klasse Person
Der folgende Beispielcode zeigt die Umsetzung mit Lombok. Mit Hilfe von Annotationen, werden verschiedene Funktionen zur Compilezeit generiert.
Was sofort auffällt ist, dass die Klasse erheblich kürzer und vor allen Dingen auch lesbarer ist. Selbst die fehlende JavaDoc ist aus meiner Sicht kein Problem.
package org.hameister.lombok; import java.io.Serializable; import java.util.Date; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString @EqualsAndHashCode(exclude={"firstname", "address", "birthday"}) @AllArgsConstructor public class Person implements Serializable { private static final long serialVersionUID = 1L; @NonNull private String lastname; @NonNull private String firstname; private Date birthday; private String address; private String city; private String zip; public Person() {} }
Die Annotationen @Getter
und @Setter
sorgen für das Generieren der Zugriffsmethoden. Mit der Annotation @ToString
wird die Methode toString()
erstellt, wobei alle Membervariablen beim Aufruf verwendet werden. Beim Generieren von hashcode()
und equals()
wird durch die Annotation EqualsAndHashCode
und die exclude
-Anweisung dafür gesorgt, dass nur lastname, zip, city
verwendet werden. Außerdem wird mit der Annotation @AllArgsConstructor
ein Konstruktor mit allen Parametern generiert.
Testklasse für Person mit Lombok
Auch bei der Testklasse ist die Reduzierung des Quellendes durch den Einsatz von Lombok deutlich sichtbar.
package org.hameister.lombok; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Date; import lombok.Cleanup; import lombok.SneakyThrows; import lombok.val; import lombok.extern.log4j.Log4j; //Logging @Log4j public class PersonTester { // Catch FileNotFoundException, IOException, ClassNotFoundException @SneakyThrows public static void main(String[] args) { Person person = new Person(); // Getter and setter person.setFirstname("Donald"); person.setLastname("Duck"); person.setAddress("Entenstrasse 5"); person.setCity("Entenhausen"); person.setZip("1234"); System.out.println("Lastname: " + person.getLastname()); // Logging and toString log.debug(person.toString()); // val val persons = new ArrayList<Person>(); persons.add(person); // Constructor Person person2 = new Person("Duck", "Dagobert", null, "Entenstrasse 1", "Entenhausen", "1234"); // equals and hashcode log.debug("Same values except firstname and address: " + person.equals(person2)); log.debug("Hashcode " + person.getFirstname() + ": " + person.hashCode()); log.debug("Hashcode " + person2.getFirstname() + ": " + person2.hashCode()); // NotNull values in constructor try { new Person("Duck", null, new Date(), "Entenstrasse 5", "Entenhausen", "1234"); } catch (NullPointerException exception) { log.debug("Expected NPE."); } // Close open streams @Cleanup FileOutputStream fos = new FileOutputStream("DonaldDuck.ser"); @Cleanup ObjectOutputStream out = new ObjectOutputStream(fos); out.writeObject(person); @Cleanup FileInputStream fis = new FileInputStream("DonaldDuck.ser"); @Cleanup ObjectInputStream in = new ObjectInputStream(fis); Person person4 = (Person) in.readObject(); log.debug("Serialized Person: " + person4); } }
Die Variable log
wird einfach durch die Annotation @Log4J
integriert und kann danach verwendet werden. Ein weiteres Feature von Lombok ist das Schlüsselwort val
. Damit kann eine lokale Variable ohne Typ definiert werden. In dem Beispiel oben spart man sich dadurch die Definition von List<Person>
. Eine weitere nützliche Annotation ist @Cleanup
. Damit werden die IO-Streams automatisch geschlossen, d.h. es wird der Boilerplate-Code generiert, der im letzten Beispiel noch per Hand geschrieben wurde. Die Annotation @SneakyThrows
sorgt dafür, dass wir uns nicht selbst um die Exceptions kümmern müssen, d.h. der try-catch
-Block wird generiert. Das hat den Vorteil, dass Exceptions, die im Normalfall nicht auftreten im Quellcode auch nicht vom Entwickler behandelt werden müssen. Ein Beispiel dafür ist Thread.sleep(1000);
. Es soll einfach eine Sekunde gewartet werden, aber die InterruptedException
-Exception, um die man sich normalerweise kümmern müsste, ist eher unwahrscheinlich. Also kann durch die Annotation @SneakyThrows
das Exception-Handling weggelassen werden und der Quellcode wird übersichtlicher.
Fazit
Wie man leicht sieht, lässt sich die Menge des Quellcodes durch den Einsatz von Lombok erheblich reduzieren und er wird durch das Weglassen des Boilerplate-Code erheblich lesbarer.
In Zahlen bedeutet dies:
Lombok | Klasse Person | Testklasse | Gesamt |
---|---|---|---|
nein | ca. 117 Zeilen | ca. 101 Zeilen | ca. 218 Zeilen |
ja | ca. 31 Zeilen | ca. 69 Zeilen | ca. 100 Zeilen |
Sicherlich ist das Beipsiel sehr "konstruktiert", aber trotzdem verdeutlicht es, wie übersichtlich Quellcode werden kann.
Bei den Tests, die ich mit dem Framework gemacht habe, hat es problemlos funktioniert. Was ich nicht getestet habe, wie Tools, wie Sonar, auf den generierten Code reagieren.
Ich denke, dass das Lombok-Framework gerade für Pojos sehr gut geeignet ist. Diese werden erheblich kompakter. Bei Klassen, die Businesslogik enthalten, muss man meiner Meinung nach gut abwägen, ob der Quellcode lesbarer und wartbarer wird. Um dazu eine belastbare Aussage zu machen, muss das Framework wohl in einem realen Kundenprojekt eingesetzt werden...