Mit dem „Projekt Lombok“ kann man den Boilerplatecode seines Projektes auf ein Minimum reduzieren.
Inhaltsverzeichnis
Aber was ist Boilerplatecode?
Boilerplatecode ist Quellcode welcher für die Verwendung von Klassen (und APIs) benötigt wird und nur ein geringes Abstraktionsniveau hat. D.h. es geht hier um Methoden wie Getter, Setter, Konstuktoren, equals, hascode und toString. Diese Methoden werden Bsp. Benötigt um POJOs, Entitys oder andere Objekte zu erzeugen, aber sonst haben diese keinen besonderen Mehrwert für die Anwendung.
Wie richtet man Projekt Lombok ein?
Um die Funktionen von Projekt Lombok (Im nachfolgenden nur noch als Lombok bezeichnet.) zu nutzen muss man ein Plugin installieren.
Dieses Plugin ist derzeit (stand 25.09.2018) leider nicht per Marketplace installierbar.
Das Plugin muss somit von der Herstellerseite https://projectlombok.org/ heruntergeladen werden und kann danach mit einem doppelklick gestartet werden.
Das Plugin ist ein JavaArchiv, einige Browser werden beim Download eine Warnung anzeigen welche man Aktzeptieren muss.
Installieren des Plugins
Nachdem die Datei „lombok.jar“ mit einem doppelklick gestartet wurde, wird nach Eclipse Installationen gesucht.
Die Entwicklungsumgebung Eclipse kann als ZIP entpackt und in ein beliebiges Verzeichnis abgelegt werden, daher kann die Suche etwas länger dauern.
Wenn die Eclipse Installation nicht gefunden wurde, so kann diese mit der Schaltfläche „Specify location…“ hinzugefügt werden.
Im nächsten Schritt wird nun die Eclipse Installation durch das Setzen der Checkbox ausgewählt (1) (per default sind alle gewählt) und mit der Schaltfläche „Install / Update“ (2) wird die Installation gestartet.
Wurde das Plugin korrekt installiert, so wird der folgende Dialog angezeigt.
Wenn die Eclipse IDE mit eigenen Parametern für die virtuelle Maschine (-vm parameter) gestartet wird, so muss zusätzlich das JavaArchiv eingebunden werden. In diesem speziellen Fall wird die Zeile „-vmargs -javaagent:lombok.jar“ den Parametern angehängt.
Prüfen ob Projekt Lombok installiert wurde
Auch wenn die Installation des Frameworks „Projekt Lombok“ vermeldet, dass die Installation erfolgreich verlaufen ist, wollen wir prüfen, ob dieses wirklich so ist.
Dazu startet man Eclipse und öffnet über das Menü „Help“ > „About Eclipse IDE“ den Infodialog. Dort sollte es nun eine zusätzliche Zeile geben, wo auf das Plugin hingewiesen wird.
Beispielprojekt
Nehmen wir uns ein kleines einfaches Beispielprojekt „Greetings“.
public class Greeting { private String text; public Greeting(String text) { super(); this.text = text; } public String getText() { return text; } public void setText(String text) { this.text = text; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((text == null) ? 0 : text.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Greeting other = (Greeting) obj; if (text == null) { if (other.text != null) return false; } else if (!text.equals(other.text)) return false; return true; } @Override public String toString() { return "Greeting [text=" + text + "]"; } }
In diesem Quellcode sieht man nun welchen Overhead man hat, wenn man nur eine kleine Klasse mit nur einem Text erzeugen möchte. Okay auf die Methoden equals, hashCode und toString kann verzichtet werden, jedoch ist es eine gängige Praxis diese Methoden zu implementieren.
In Eclipse lassen sich diese Methoden einfach über das Kontextmenü erzeugen und somit ist die „arbeit“ nur sehr gering.
JUnit & Ecplise EclEmma Plugin
In jedem guten Java Projekt gibt es Testfälle für den Quellcode.
Hier nun ein kleiner JUnit Testfall für die Klasse „Greeting“
public class TestGreeting { private static Greeting greetingGerman; private static Greeting greetingEnglish; @BeforeAll static void beforeAllTestcases() { greetingGerman = new Greeting("Hallo Welt!"); greetingEnglish = new Greeting("Hello World!"); } @Test void testGreetingShouldBeNotNull() { assertNotNull(greetingGerman); assertNotNull(greetingEnglish); } @Test void testGreetingTextShouldBeNotEmpty() { assertFalse(isBlank(greetingGerman.getText())); assertFalse(isBlank(greetingEnglish.getText())); } private boolean isBlank(String text) { return text == null || text.trim().length() == 0; } }
Führen wir die oben stehenden Testfälle einmal aus und nutzen zusätzlich das Testcoverage Plugin EclEmma zur Analyse.
Das Plugin zeigt nun an, das die Methoden equals, hashCode, toString sowie setText nicht getestet wurde und somit nur eine Testabdeckung von 39,2 % besteht. Um auf 100 % Testabdeckung zu kommen, muss man nun alle rot markierten Stellen mit einem JUnit Test abdecken. Und genau hier entstehen Aufwände, welche man sich sparen kann.
@Test void testEquals() { Greeting g2 = new Greeting("Hallo Welt!"); assertTrue(g2.equals(greetingGerman)); assertTrue(g2.equals(g2)); assertFalse(g2.equals(null)); assertFalse(g2.equals(new Integer(1337))); Greeting g3 = new Greeting(null); assertFalse(g2.equals(g3)); assertFalse(g3.equals(g2)); Greeting g4 = new Greeting(null); assertTrue(g3.equals(g4)); } @Test void testSetText(){ Greeting g4 = new Greeting("test"); assertTrue(g4.getText().equalsIgnoreCase("test")); g4.setText("Hallo Welt!"); assertTrue(g4.getText().equalsIgnoreCase("Hallo Welt!")); } @Test void testHashCode() { Greeting g2 = new Greeting("Hallo Welt!"); assertTrue(g2.hashCode() == greetingGerman.hashCode()); Greeting g3 = new Greeting(null); assertFalse(g3.hashCode() == 0); Greeting g4 = new Greeting("test"); assertTrue(g4.hashCode() == 3556529); } @Test void testToString() { Greeting g2 = new Greeting("Hallo Welt!"); assertTrue(g2.toString().equalsIgnoreCase("Greeting [text=Hallo Welt!]")); } private boolean isBlank(String text) { return text == null || text.trim().length() == 0; }
Wir haben es nun geschafft eine 100%ige Testabdeckung zu erstellen, okay das war jetzt bei der Klasse nicht die größte Herausforderung.
Projekt Lombok gegen Boilerplatecode
Nun wollen wir die Klasse „Greeting“ mit Annotations versehen, damit die unnötigen Methoden entfallen können. Dazu müssen wir dem Eclipseprojekt das JavaArchiv „lombok.jar“ hinzufügen.
Im folgenden werde ich zeigen wie die Klasse „Greeting“ mit Hilfe von Lombok und Annotationen verkürzt werden kann.
Konstruktor
Widmen wir uns zuerst dem Konstruktor der Klasse, dieser nimmt den Wert für die Membervariable „Text“ entgegen.
Den Konstruktor können wir mit der Annotation „@RequiredArgsConstructor“ an der Klasse sowie der Annotation „@NonNull“ an der Membervariable „text“ ersetzen. Dieses wird uns auch angezeigt das dieser nun doppelt vorhanden ist.
Hier hat Lombok den Konstruktor bereits erzeugt nur das wir diesen nicht sehen können. Also entfernen wir den Konstrukur, man sieht in dem Tab „Outline“ das der Konstruktor trotzdem erhalten bleibt.
Getter & Setter
Als nächstes geht es dem Getter & Setter an den Kragen, diese werden mit der Annotation „@Getter“ & „@Setter“ überflüssig.
Der Annotation kann zusätzlich noch die Sichtbarkeit mitgegeben werden, d.h. soll der Getter bzw. Setter private, protected oder public sein.
@NonNull @Getter(value=AccessLevel.PRIVATE) @Setter(value=AccessLevel.PROTECTED) private String text;
Equals, hashCode & toString
Wie eingangs erwähnt, ist es gute Praxis die Methoden equals, hashCode und toString zu überschreiben. Dieses wird mit der Annotation „@EqualsAndHashCode“ & „@ToString“ an der Klasse erledigt.
@EqualsAndHashCode @ToString public class Greeting {}
Das Ergebnis & ein Fazit
Haben wir nun alle Annotationen an der Klasse versehen, so sieht diese nun wie folgt aus:
package de.draegerit.greetings; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.ToString; @RequiredArgsConstructor @EqualsAndHashCode @ToString public class Greeting { @NonNull @Getter(value=AccessLevel.PRIVATE) @Setter(value=AccessLevel.PROTECTED) private String text; }
Und der Tab „Outline“ zeigt nun die Methoden, an welche wir durch die Annotationen ersetzt haben.
In dem Tab „Outline“ sieht man nun:
- den Konstruktor,
- die private Methode „getText();“,
- die protected Methode „setText(String);“,
Des Weiteren sind die Methoden equals, hashCode und toString mit einem kleinen Dreieck nach oben versehen, das bedeutet, dass die Methode überschrieben wurden.
Jetzt könnten wir unseren JUnit Testfall ausdünnen und die Testfälle für equals, hashCode, toString und setText entfernen, aber weit gefehlt, denn diese Methoden existieren ja trotzdem noch. D.h. alles, was wir getan haben ist, die Methoden vor unseren Augen zu verstecken der Compiler sieht diese trotzdem noch.
Die Testfälle für equals, hashCode und toString werden nun durch Lombok implementiert und dieses weicht von der Eclipse Version ab daher sind nun die Testfälle nicht mehr valide. Die toString Methode kann man sich ja noch auf der Konsole ausgeben aber beim Rest wird es dann doch schon etwas schwieriger. Hier hilft ein decompiler Plugin weiter.
Binäre Datei Greeting.class decompilieren
Für das decompilieren verwende ich das Eclipse Plugin „Enhanced Class Decompiler“ welches bequem über den Marketplace installiert werden kann.
decompilierter Code
Mit dem Plugin können wir uns nun den Quellcode anzeigen lassen und sieht die Implementierung von equals und hashCode.
package de.draegerit.greetings; import lombok.NonNull; public class Greeting { @NonNull private String text; @NonNull public String getText() { return this.text; } public void setText(@NonNull String text) { if (text == null) { throw new NullPointerException("text is marked @NonNull but is null"); } else { this.text = text; } } public Greeting(@NonNull String text) { if (text == null) { throw new NullPointerException("text is marked @NonNull but is null"); } else { this.text = text; } } public boolean equals(Object o) { if (o == this) { return true; } else if (!(o instanceof Greeting)) { return false; } else { Greeting other = (Greeting) o; if (!other.canEqual(this)) { return false; } else { Object this$text = this.getText(); Object other$text = other.getText(); if (this$text == null) { if (other$text != null) { return false; } } else if (!this$text.equals(other$text)) { return false; } return true; } } } protected boolean canEqual(Object other) { return other instanceof Greeting; } public int hashCode() { int PRIME = true; int result = 1; Object $text = this.getText(); int result = result * 59 + ($text == null ? 43 : $text.hashCode()); return result; } public String toString() { return "Greeting(text=" + this.getText() + ")"; } }