dinsdag 6 oktober 2009

Einde aan boilerplate met Project Lombok

Weer een getter en setter schrijven, weer die equals en hashCode updaten omdat er weer een field bij is gekomen. Gelukkig heb je die twee generiek gemaakt via reflectie. Maar wat word daardoor die HashSet toch traag, toch maar weer handmatig implementeren. En oeps, een tikfout die toch wel valid Java blijkt te zijn is erin geslopen! En oh ja, ook nog toString aanpassen…

Toch vreemd dat zoiets als een getter, setter, toString, equals en hashCode toch weer zoveel problemen geven, vooral omdat IDE’s deze toch al weer een tijdje voor je kunnen genereren. Maar deze zul je toch ook nog moeten onderhouden (al was het alleen maar regenereren van deze methodes). Je zou toch haast zeggen dat dit werk is voor een computer? Daarnaast blijf je constant tegen die methodes aankijken in de source, en dat geeft, en brengt, extra mentale belasting die eigenlijk helemaal niet nodig is in de meeste gevallen.

Er zijn over de jaren een aantal oplossingen bedacht voor dit probleem, een ervan is equals, hashCode en toString via reflectie. Dit werkt, helaas werkt het ook traag. Dit komt omdat de JVM (HotSpot) deze niet goed kan inlinen omdat het in feite type-less code is. En reflectie kan ook geen getters en setters maken die je in je code kan gebruiken (of in Swing).

Daarnaast zijn er door de jaren heen een aantal JSR Proposals geweest om een property keyword te krijgen, zodat getters en setters automatisch gegenereerd worden.
Bijvoorbeeld:

class Point {
public property int x;
public property int y;
}

om het volgende te genereren:
class Point {
private int x;
private int y;

public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}

Maar de meest recente versie van deze proposal, in Project Coin, heeft de selectie niet gemaakt (voor Project Coin). Daarnaast lost dit ook niet direct het equals, hashCode en toString probleem op (uiteraard wel met de reflectie oplossing).

Het is in feite interessant om te zien dat, bijvoorbeeld, Scala dit soort dingen wel doet, in de vorm van case classes, bijvoorbeeld de Point klasse hierboven:
case class Point(var x:Int, var y:Int)

Dit genereert constructor, getters, setters, toString, equals, hashCode en Scala specifieke features, zoals Pattern Matching.

Maar er is licht aan het einde van de tunnel.

In Java5 zijn er, samen met Generics, Annotations toegevoegd. Hiermee kan je klassen, methoden, fields en parameters annoteren met metadata. En samen hiermee is er ook "APT" of "Annotation Processing Tool" uitgebracht. Dit is een losse tool, zoals javac, dat met een set processors een selectie klassen doorloopt en deze aanpast. Het is voornamelijk een source processing tool, waarbij acties getriggerd worden op annotaties.

Het is dus mogelijk om een annotatie te schrijven die een getter en setter van een veld genereert, of een toString, etc, etc. Het mooie is ook dat zodra dit is gebeurd je de nieuwe methode direct kan zien, en gebruiken, in een IDE, de meeste IDEs gebruiken class files om te zien of een methode (of zelfs class) bestaat.
Maar het schrijven van een AnnotationProcessor is lastig, en dit is een losse stap, en je kan het dus vergeten, en dan kan je leuke situaties krijgen, bijvoorbeeld dat getters en setters niet bestaan terwijl je deze op andere plekken wel verwacht.

Gelukkig is het in Java6 nu mogelijk om direct, bij het aanroepen van javac, annotations te processen. En daarom is er nu Project Lombok.

Lombok komt met een aantal standaard annotations om boilerplate te verwijderen.
Je voegt het gewoon toe aan je classpath (tijdens het compilen) en de annotation processors erin gaan aan het werk. Een lijstje met de annotations die meegeleverd worden zijn:
  • @Getter en @Setter
  • @ToString
  • @EqualsAndHashCode
  • @Data
  • @Cleanup
  • @Synchronized
  • @SneakyThrows
Alle annotations zijn te configureren.
De eerste drie (punten, vier annotations) spreken voor.
@Data combineerd de eerste 3 punten.
@Cleanup kan je gebruiken op variabelen in methodes zodat deze try { } finally { } blocken genereerd en een methode aanroept (default is close, maar kan ook "myOwnCleanupMethod" zijn).
@Synchronized is een method level annotation en word gebruikt om een algemeen synchronized pattern te gebruiken die beter locked op een field i.p.v. een (static) field. Het is ook mogelijk om meerdere, andere, locks te definieren (beter gezegd, specifieke velden te gebruiken).
Als laatste is er @SneakyThrows, dit word gebruikt om een methode bepaalde checked exceptions te laten throwen, zonder dat deze in de method descriptor hoeven te staan (de throws keyword). Een voorbeeld is UnsupportedEncodingException, zeg nou zelf, wat is de kans dat UTF-8 niet bestaat in jouw JDK? Wat dit doet is je methode wrappen in een try { } catch() {} block, en de ongewenste exception catchen, en deze dan te throwen met een unchecked exception.
Enkel voor de @SneakyThrows hoeft de lombok.jar runtime beschikbaar te zijn (omdat het een utility methode aanroept die de echte sneaky-throw doet).

Mijn vorige voorbeeld klasse kan met Lombok gereduceerd worden tot:
@EqualsAndHashCode @ToString
class Point {
@Setter @Getter private int x;
@Setter @Getter private int y;
}

Of nog korter:
@Data
class Point {
private int x;
private int y;
}
En dat is veel beter! Nog mooier is dat je het niet kan vergeten, als je de lombok.jar vergeet tijdens het compilen dan kan de compiler de verschillende annotaties niet vinden, en kan het compilen niet werken.

Om af te sluiten raad ik het je aan om de screen-cast te bekijken die op de Project Lombok website staat, en het gewoon uit te proberen. Eindelijk einde aan boilerplate code!