Why I Love Lombok

Not the pictured Lombok. I’m talking about Project Lombok.

Explanation

What it does

Lombok is a Java library of annotations and processors that creates methods like getters, setters, and constructors for you. This allows you to write code that clearly shows your intent without a bunch of garbage in the way.

Why it’s so great

It doesn’t seem like much. Setters aren’t hard to write. Neither are constructors. In fact, most IDEs will generate them for you. But they’re cruft. They get in the way of the code that matters. Additionally, maintaining additional code introduces potential bugs.

You can use Lombok to strip your code down to its essence. Without all the excess, your code is clearer and less error prone.

Catalog

I will briefly summarize Lombok’s various annotations. For more detail, check out their documentation.

Constructor

Lombok provides several options for constructors. They are:

  • @NoArgsConstructor
  • @RequiredArgsConstructor
  • @AllArgsConstructor

They do about what you expect. NoArgs creates a no arguments constructor. AllArgs creates a constructor for all the fields. RequiredArgs creates a constructor for all final fields.

@Getter, @Setter, @EqualsAndHashCode, @ToString

These do exactly what you’d expect. @Setter only creates setters for non-final fields. By generating these automatically, you don’t have to remember to update them when you add a field.

@Data

Putting @Data on a class is equivalent to @Getter, @Setter, @EqualsAndHashCode, @ToString, @RequiredArgsConstructor on your class. It’s not my favorite, because I don’t like setters most of the time.

Patterns

The annotations are just pieces and parts. We can combine them in some very useful patterns. These patterns can be turned into file templates in IDEs like IntelliJ to eliminate boilerplate code.

(Almost) Immutable, Deserializable

@lombok.Getter
@lombok.AllArgsConstructor
@lombok.NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@lombok.EqualsAndHashCode
@lombok.ToString
public class Person {
    private String name;
    private String email;
    private String phoneNumber;
}

This class automatically generates a public all args constructor, a private no args constructor, getters on all your fields, equals(), hashCode(), and toString().

This combination of annotations gives us a class that is essentially immutable. None of the fields are final, but it doesn’t have any setters, only getters. Most consumers have to use the all args constructor. The reason we include a private no args constructor is for libraries like Jackson, a JSON parser, and Hibernate, an object relational mapper. These will use the no args constructor (even private) and then set the values directly on the fields.

The automatic equals(), hashCode(), and toString() are nice, because otherwise, when you add a field, you have to remember to update all of those methods. If you don’t, you may have a latent bug you probably won’t find for a while.

Business Entities

Even if you want the class to be mutable, (Almost) immutable is a good starting pattern. Setters encourage the anemic domain model antipattern by allowing external objects to change data on the object. Instead, you can have meaningful business operations on you entities. This allows you to enforce business rules in your domain objects.

Note: I’m using floats in the example for simplicity. Real code should use BigDecimal (or similar) for money.

@lombok.Getter
@lombok.AllArgsConstructor
@lombok.NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@lombok.EqualsAndHashCode
@lombok.ToString
public class BankAccount {
    private String id;
    private float amount;
    
    public void credit(float toCredit) {
        amount = amount + toCredit;
    }

    public void debit(float toDebit) {
        if (toDebit > amount) {
            throw new InsufficientFundsException();
        }

        amount = amount - toDebit;
    }
}

This class has only 2 methods on it. They’re both meaningful business methods. This class should be easy to understand. For comparision, this is the class without Lombok. Over half the code is boilerplate.

@lombok.Getter
@lombok.AllArgsConstructor
@lombok.NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@lombok.EqualsAndHashCode
@lombok.ToString
public class BankAccount {
    private String id;
    private float amount;

    private BankAccount() {}

    public BankAccount(String id, float amount) {
        this.id = id;
        this.amount = amount;
    }

    public String getId() {
        return this.id;
    }

    public String getAmount() {
        return this.amount;
    }
    
    public void credit(float toCredit) {
        amount = amount + toCredit;
    }

    public void debit(float toDebit) {
        if (toDebit > amount) {
            throw new InsufficientFundsException();
        }

        amount = amount - toDebit;
    }

    @Override public String toString() {
        return "BankAccount(id=" + this.getId() + ", amount=" + this.amount + ")";
    }

    @Override public boolean equals(Object o) {
      if (o == this) return true;
      if (!(o instanceof BankAccount)) return false;
      BankAccount other = (BankAccount) o;
      if (!other.canEqual((Object)this)) return false;
      if (!super.equals(o)) return false;
      if (this.id != other.id) return false;
      if (this.amount != other.amount) return false;
      return true;
    }
}

Spring Services

You have 3 ways you can do injection in Spring:

  • Property
  • Field
  • Constructor

Property injection ends up with a service polluted with getters and setters just so you can build it up.

Field injection eliminates that, but leaves a class that can’t be built without Spring or other magic.

Constructor injection solves all of those issues, but you have to keep your constructor in sync with your fields. When you add a field, you have to remember to add it to your service.

Using Lombok, you can create Spring services that use constructor injection and are always correct. As a bonus, we’re also automatically creating a logger.

@lombok.extern.slf4j.Slf4j
@lombok.AllArgsConstructor(onConstructor_={@Autowired})
@org.springframework.stereotype.Service
public class FirstService {
    private final SecondService secondService;
    private final ThirdService thirdService;

    public void doStuff() {
        log.info("Doing stuff now");
    }
}

The onConstructor part of the annotation applies @Autowired to the generated constructor. You don’t need it in Spring 4.3 and above, but I included it here anyway.

Parameter Objects

Methods with a bunch of parameters can be confusing to use and read. The Parameter Object pattern combines those into a single object to simplify things. Lombok can make it even better.

Start

Let’s look at an email service.

public class EmailService {
    public void send(String from, String to, String subject, String message) ...
}

We’d call it like this.

    emailService.send("seth@sethkraut.com", "you@someplace.com", "Hello", "Lorem ipsum ..."); 

This is fine, but it can be easy to get confused. Which parameter comes first? To or from?

When we add cc and bcc, it gets messier still. Do we add cc after to and leave it null when we don’t have a cc? Or do we start making new methods with different combinations of parameters.

Methods for different parameter combinations. Yuck.

public class EmailService {
    public void send(String from, String to, String subject, String message) {}
    public void send(String from, String to, List<String> cc, String subject, String message) {}
    public void send(String from, String to, List<String> cc, List<String> bcc, String subject, String message) {}
}

Or add more parameters.

public class EmailService {
    public void send(String from, String to, List<String> cc, List<String> bcc, String subject, String message) ...
}
    emailService.send("seth@sethkraut.com", "you@someplace.com", null, null, "Hello", "Lorem ipsum ...");

Also, yuck.

Clean Up

We can clean this up quite a bit by using a parameter object with the builder pattern. Implementing the builder pattern in Java is annoying. but we don’t need to, because Lombok does it for us.

@ToString
@Builder
public class Email {
    private String to;
    private String from;
    private List<String> cc;
    private List<String> bcc;
    private String subject;
    private String message;
}

Now our service takes a method with a single parameter.

public class EmailService {
    public void send(Email email) ...
}

The builder pattern makes it even nicer. It’s very clear and adding new parameters is trivial.

emailService.send(
        Email.builder()
        .from("seth@sethkraut.com")
        .to("you@someplace.com")
        .subject("Notification")
        .message("Hello")
        .build()
);
Seth Kraut
Seth Kraut

Obsessed with how the world works

Related