Thursday, November 11, 2010

Project Lombok: Annotation-driven development - Part 1

As the Java language has reached an evolutionary plateau, annotations are emerging as a way to extend the language. Two interesting frameworks that I've been looking at are Project Lombok and the Checker Framework. In this first entry of a series I'm calling "Annotation-driven development", I'll be looking at Project Lombok.

Project Lombok


Project Lombok aims to eliminate boilerplate code. For example most POJO classes are littered with trivial getters/setters like the following:


public class Person {

private final String firstName; // read-only

private String lastName;

public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}
public void setLastName(String value) {
this.lastName = value;
}

}


With Lombok, you can use annotations to create the equivalent set of methods:


@AllArgsConstructor
public class Person {
@Getter @Setter private final String firstName;
@Getter private String lastName;
}


Or even simpler:


@Data
public class Person {
private final String firstName;
private String lastName;
}


Actually, the final example generates more than just getters/setters and constructor. You also get a toString and hashCode method, which are often "forgotten" because they are a pain to write correctly (unless you are using Pojomatic).

Lombok isn't just about POJO boilerplate. From the Project Lombok features page, here are all the annotations available currently:
  • @Getter / @Setter
    Never write public int getFoo() {return foo;} again.<
  • @ToString
    No need to start a debugger to see your fields: Just let lombok generate a toString for you!
  • @EqualsAndHashCode
    Equality made easy: Generates hashCode and equals implementations from the fields of your object.
  • @NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor
    Constructors made to order: Generates constructors that take no arguments, one argument per final / non-null field, or one argument for every field.
  • @Data
    All together now: A shortcut for @ToString, @EqualsAndHashCode,
    @Getter on all fields, and @Setter on all non-final fields, and @RequiredArgsConstructor!
  • @Cleanup
    Automatic resource management: Call your close() methods safely with no hassle.
  • @Synchronized
    synchronized done right: Don't expose your locks.
  • @SneakyThrows
    To boldly throw checked exceptions where no one has thrown them before!

Setting up Lombok


To use lombok, you'll need the lombok.jar. This can be obtained from the Project Lombok website. Maven users can simply add the lombok dependency and repository. For example:

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>0.9.3</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>projectlombok.org</id>
<url>http://projectlombok.org/mavenrepo</url>
</repository>
</repositories>
</repositories>


This will allow you to use lombok using javac.

For the 99.9% of developers who use an IDE, you'll want Lombok IDE support unless you enjoy seeing code that won't compile. You're in luck if you're using Eclipse or NetBeans, as those are the only IDEs supported (sorry IntelliJ users).

For Eclipse 3+ and NetBeans 6.8+, simply run java -jar lombok.jar and an install wizard will guide you through adding Lombok support to your chosen IDE(s). The wizard will modify your IDE start script to include Lombok.jar as a Java agent. NetBeans 6.9 users also have the option of using Lombok as a inline annotation processor.

Using Lombok


Once you're got your environment set up to use Lombok, you simply add a Lombok annotation your class, and your class is magically enhanced. Lombok generates code during the compilation phase. Within the IDE, you'll now have access to methods that aren't in your source file. The methods exist in the generated class so they show up in your class outline and are available for code completion. If you ask your IDE to open/jump to the method, it will open the source file but obviously you won't see the code for the method.

If you want to view the generated code, you can use JAD to decompile the class or you can use the delombok tool to generate a Lomboked source file from your original source file. You can run delombok manually from the command-line or automatically via the Maven Lombok plugin. Despite being a Maven user, I found it easier to use delombok from the command-line because the maven plugin requires moving all Lomboked files to a non-standard directory.

Delombok serves a few useful purposes. You may be curious to see what code Lombok is generating. Or you later decide you want to remove your dependency on Lombok, then you can delombok all your source, and replace the original source with the delomboked source.

Running Delombok on the previously mentioned @Data example, generated the following source code:

public class Person {
private final String firstName;
private String lastName;

@java.beans.ConstructorProperties({"firstName"})
@java.lang.SuppressWarnings("all")
public Person(final String firstName) {
this.firstName = firstName;
}

@java.lang.SuppressWarnings("all")
public String getFirstName() {
return this.firstName;
}

@java.lang.SuppressWarnings("all")
public String getLastName() {
return this.lastName;
}

@java.lang.SuppressWarnings("all")
public void setLastName(final String lastName) {
this.lastName = lastName;
}

@java.lang.Override
@java.lang.SuppressWarnings("all")
public boolean equals(final java.lang.Object o) {
if (o == this) return true;
if (o == null) return false;
if (o.getClass() != this.getClass()) return false;
final Person other = (Person)o;
if (this.getFirstName() == null ? other.getFirstName() != null : !this.getFirstName().equals(other.getFirstName())) return false;
if (this.getLastName() == null ? other.getLastName() != null : !this.getLastName().equals(other.getLastName())) return false;
return true;
}

@java.lang.Override
@java.lang.SuppressWarnings("all")
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = result * PRIME + (this.getFirstName() == null ? 0 : this.getFirstName().hashCode());
result = result * PRIME + (this.getLastName() == null ? 0 : this.getLastName().hashCode());
return result;
}

@java.lang.Override
@java.lang.SuppressWarnings("all")
public java.lang.String toString() {
return "Person(firstName=" + this.getFirstName() + ", lastName=" + this.getLastName() + ")";
}
}


Like any source code generator, delombok produces code that looks, well, generated. It seems that Lombok lazily adds @java.lang.SuppressWarnings("all") to all methods. The toString/hasCode/equals methods are definitely uglier that what you'd get if you were using Pojomatic.

Extending Lombok


After playing with Lombok, you will likely think of other boilerplate code you'd like to eliminate using the Lombok. The Builder pattern came to my mind and apparently others others as well.

There are very few resources that explain how to extend Lombok. You can obviously download the source. Besides that, the best resource I could find was a blog by Nicolas Frankel that describes the basic steps as well as example source. Nicolas states that writing custom Lombok plugins is not for the faint-hearted and I'd agree. You'll quickly discover that you need to know something about annotation processors as well as some rather low-level Javac APIs. Honestly, when you look under the covers of Lombok, things get a bit scary. Some have called Lombok a hack because it relies on internal javac APIs.

Final Thoughts


Lombok is a very interesting use of annotations to extend the Java language. For those who really hate writing getters/setters/etc, Lombok is worth checking out. Although I don't enjoy writing getters/setters/etc, I don't spend a lot of my time writing those types of methods and the IDE can generate much of the boilerplate. It should be noted that Scala has eliminated many of the pain points that Lombok aims to remedy.

Right now I'm just using Lombok for prototyping. It definitely speeds up the process of creating POJOs and let's me focus on the more interesting aspects of the prototype. I am not using Lombok for production code. Although I use Eclipse, other developers at Overstock use IntelliJ and the lack of support for IntelliJ is a show-stopper. Even if all IDEs were supported, I'm still not comfortable unleashing Lombok until I've spent some more time with it. I don't think I've found all its warts yet. I'm also concerned about later releases of JDK (or Eclipse) breaking Lombok compatibility. It wouldn't be the end of the world because I can always delombok my source, but that could be a painful process for large projects.

That said, it's definitely worth looking Lombok. Even if it never makes it into my Java toolkit, it's a fascinating library to examine and my knowledge of Java has increased because of it.

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. I just started looking at Lombok and the presence of a wizard as opposed to an eclipse plugin was surprising to me.
    Do you know what features an eclipse plugin (when it is eventually offered) would offer over doing it via the wizard?
    Is an eclipse plugin really just a matter of convenience or does it some how hook more closely into how the IDE behaves
    and offer additional features a wizard cannot offer due to limitations in how the wizard is done?


    http://groups.google.com/group/project-lombok/browse_thread/thread/3f04331b8a50de8f?pli=1

    ReplyDelete
  3. Phanindra,
    Lombok uses the wizard approach because they need to patch the actual Eclipse compiler classes to inject their magic. I don't think the Eclipse plugin architecture provides the necessary hooks into the compilation process for Lombok to work.

    There was mention on the Lombok forums of creating an Eclipse plugin that would show the delomboked code right in the IDE. It would also be nice to have debugging support because right now there is no way to debug Lombok generated code unless you delombok first. It works but its tedious.

    ReplyDelete