Why is Lombok a game changing library in Java development?

Java has the reputation of being a verbose language, therefore it is not uncommon to end up with may lines of code, especially for those getter and setters.

Lombok to the rescue?

Lombok is a Java library that directly tackles this issue of code verbosity and repetition, there are many case studies where this significantly reduced the amount of code written, which in turn increase developer productivity!

How do I use Lombok?

Lombok is typically enabled by using either:

  • Gradle
  • Maven
  • Other tools

Maven example:

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
</dependency>

Gradle example:

compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.24'

Once this Java library is available, you are then able to utilise Lombok’s helpful annotations, for instance:

Automatic getters, setters, constructors…

@Entity
@Getter @Setter @NoArgsConstructor // <--- Lombok annotations
public class Customer implements Serializable {

    private @Id Long id; // will be set when persisting

    private String firstName;
    private String lastName;
    private int age;

    public Customer(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
}

And just like that the @Getter and @Setter annotations generate these for all the fields in the class and for the future fields we want to add on!

Also as you may have spotted, the @NoArgsConstructor will create an empty constructor… even more lines saved!

What if I need to control visibility for some properties!?

Lombok also has you covered, for example lets say if we want to keep our entities id field modifiers package or protected visible, as they maybe expected to be read but not set by the application, there is an annotation configuration that can be used here:

private @Id @Setter(AccessLevel.PROTECTED) Long id;

Lazy Getter

There are also instances where an application requires to expend intensive operations often, which requires the need to save results to get quick access to this data rather than impacting the application throughput.

Let us consider a scenario where we request static data from a database or file. It is generally good practice to:

  • Retrieve this data once
  • Cache the retrieved data, to allow in-memory reads

This is a common pattern and we call this lazy-loading:

  • Retrieve the data, only when it is first needed.
  • In code, this is only get the data when we call the corresponding getter and setter for the first time…

For example, Lombok provides the annotation configuration: @Getter(lazy = true):

public class GetterLazy {

    @Getter(lazy = true)
    private final Map<String, Long> transactions = getTransactions();

    private Map<String, Long> getTransactions() {

        final Map<String, Long> cache = new HashMap<>();
        List<String> txnRows = readTxnListFromFile();

        txnRows.forEach(s -> {
            String[] txnIdValueTuple = s.split(DELIMETER);
            cache.put(txnIdValueTuple[0], Long.parseLong(txnIdValueTuple[1]));
        });

        return cache;
    }
}

As you can see above, this reads some transaction from a file into a Map, as the data is not changing, we will cache it once and enable access via the getter!

Value Classes and Data Transfer Objects (DTOs)

There can be requirements in our applications where we want to create a data types that consist of complex values as (DTOs). It is not uncommon that it is an unchanging immutable data structure.

This could look like a class to represent a successful login operation, which we would like all the fields to be non-null and the object itself to be immutable! This way the fields are thread safe and can be accessed.

public class LoginResult {

    private final Instant loginTs;

    private final String authToken;
    private final Duration tokenValidity;
    
    private final URL tokenRefreshUrl;

    // constructor taking every field and checking nulls

    // read-only accessor, not necessarily as get*() form
}

Similar to the getter and setter annotations outlined earlier, the amount of code that is required would be quite extensive… Lombok could be used here to help this:

@RequiredArgsConstructor
@Accessors(fluent = true) @Getter
public class LoginResult {

    private final @NonNull Instant loginTs;

    private final @NonNull String authToken;
    private final @NonNull Duration tokenValidity;
    
    private final @NonNull URL tokenRefreshUrl;

}

@RequiredArgsConstructor

With the @RequiredArgsConstructor annotation, this creates a constructor with all the final field in the class (just as we declared them)

@NonNull

The @NonNull attributes makes our constructor check for the nullability of the fields being passed, if it is Null is will throw NullPointerException.

@Accessors(fluent=true) 

Since we added @Accessors(fluent=true), this means that we redact the ‘get’ from getAuthToken(), this becomes just authToken() instead.

Java boilerplate methods

Typical boilerplate methods we typically write in our Java classes:

  • toString()
  • equals()
  • hashCode()

These methods are generally created by the help of our IDEs similarly to getters and setter where we auto-generate them from our class attributes. Although again this adds many line of code.

Lombok can automate this by using this annotations within the classes:

  • @ToString
    • Generates a toString() method with all the attributes.
  • @EqualsAndHashCode
    • Generates both equals() and hashCode() methods.

These annotations ship with configurations options, that can provide further control to the developer. For instance, we can just use:

  • (callSuper=true)
    • This parameter will include parent results when generating the method code.

As a demonstration, User JPA entity example includes a reference to events to this associated user:

@OneToMany(mappedBy = "user")
private List<UserEvent> events;

@ToString annotation parameterisation

If this remains in its default configuration the whole list of events will be dumped whenever we call toString() here for our User! (Just because we used the @ToString annotation)

To avoid this, we can parameterise it:

  • @ToString(exclude = {“events”})
    • This will now remove events on the toString() return
    • Also avoid circular references , User reference was within the Event object

@EqualsAndHashCode annotation parameterisation

For the LoginResult example we presented earlier, the equality and hash code calculation may only be need for the token and not for the other final attributes… we can control this with:

  • @EqualsAndHashCode(of =[“authToken”])

All in one short hand annotations!

Now you may get to a point where you are writing multiple annotations to achieve multiple things with your classes, this may become something quite unreadable. But do not worry Lombok has you covered with @Data and @Value.

  • @Data
    • Combination of:
      • @Getter
      • @Setter
      • @RequiredArgsConstructor
      • @ToString
      • @EqualsAndHashCode

Lomboked @Data:

@Data
public class User {
  private Long id;
  private String username;
}

DeLomboked @Data

public class User {
  private Long id;
  private String username;

  public User() {
  }

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

  public String getUsername() {
    return this.username;
  }

  public void setId(final Long id) {
    this.id = id;
  }

  public void setUsername(final String username) {
    this.username = username;
  }

  @Override
  public boolean equals(final Object o) {
    if (o == this)
      return true;
    if (!(o instanceof User))
      return false;
    final User other = (User) o;
    if (!other.canEqual((Object) this))
      return false;
    final Object this$id = this.getId();
    final Object other$id = other.getId();
    if (this$id == null ? other$id != null : !this$id.equals(other$id))
      return false;
    final Object this$username = this.getUsername();
    final Object other$username = other.getUsername();
    if (this$username == null ? other$username != null : !this$username.equals(other$username))
      return false;
    return true;
  }

  protected boolean canEqual(final Object other) {
    return other instanceof User;
  }

  @Override
  public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    final Object $id = this.getId();
    result = result * PRIME + ($id == null ? 43 : $id.hashCode());
    final Object $username = this.getUsername();
    result = result * PRIME + ($username == null ? 43 : $username.hashCode());
    return result;
  }

  @Override
  public String toString() {
    return "User(id=" + this.getId() + ", username=" + this.getUsername() + ")";
  }
}
  • @Value
    • Combination of:
      • @Getter
      • @FieldDefaults(makeFinal=true, accessLevel.PRIVATE)
      • @AllArgsConstructor
      • @ToString
      • @EqualsAndHashCode

Lomboked @Value:

@Value
public class User {
  private Long id;
  private String username;
}

DeLomboked @Value

public final class User {
  private final Long id;
  private final String username;

  public User(final Long id, final String username) {
    this.id = id;
    this.username = username;
  }

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

  public String getUsername() {
    return this.username;
  }

  @Override
  public boolean equals(final Object o) {
    if (o == this)
      return true;
    if (!(o instanceof User))
      return false;
    final User other = (User) o;
    final Object this$id = this.getId();
    final Object other$id = other.getId();
    if (this$id == null ? other$id != null : !this$id.equals(other$id))
      return false;
    final Object this$username = this.getUsername();
    final Object other$username = other.getUsername();
    if (this$username == null ? other$username != null : !this$username.equals(other$username))
      return false;
    return true;
  }

  @Override
  public int hashCode() {
    final int PRIME = 59;
    int result = 1;
    final Object $id = this.getId();
    result = result * PRIME + ($id == null ? 43 : $id.hashCode());
    final Object $username = this.getUsername();
    result = result * PRIME + ($username == null ? 43 : $username.hashCode());
    return result;
  }

  @Override
  public String toString() {
    return "User(id=" + this.getId() + ", username=" + this.getUsername() + ")";
  }
}

Lombok should not be used everywhere

Notably, from Thorben Janssen blog he outlines issues using @EqualsAndHashCode with JPA entities, the behaviour output of what you are expecting from this can vary and even with a workaround solution he also outlines further issues here.

Furthermore, in the medium article Dont use Lombok by Gonzalo Vallejos indicates there are certain annotations which could be deemed less useful for what they are giving to your application, although to counter this argument, this does not mean you actually have to use everything that Lombok gives you. And to note, it is conclusive in that post that Lombok is still the only solution to reduce boilerplate code, but does have an inherent trade off tight coupling of utilising this annotation heavy library.

Auto Object Composition

Java currently does not have language level constructs to smooth out composition relationships. Lombok’s @Delegate can be useful to achieve this composition relationship.

As of this writing, the @Delegate annotations is still considered to be experimental feature and it has had a negative response from the community as it has not been used much, difficulty supporting edge cases and API is not intuitive at this point according to Project Lombok.

Builder Pattern

Yes, Lombok also allows us to implement the builder pattern very simply with @Builder.

Setting it up:

@Builder
public class ApiClientConfiguration {

    private String host;
    private int port;
    private boolean useHttps;

    private long connectTimeout;
    private long readTimeout;

    private String username;
    private String password;
}

And then this can then be implemented like this:

ApiClientConfiguration config = 
    ApiClientConfiguration.builder()
        .host("api.server.com")
        .port(443)
        .useHttps(true)
        .connectTimeout(15_000L)
        .readTimeout(5_000L)
        .username("myusername")
        .password("secret")
    .build();

Sneaky Exceptions

We can also generate try catch blocks with Lombok using @SneakThrows, more details on this can be found here on the project Lombok site.

Logging

More controversial, as this may not save lines of code in all instances, although it may save word count:

Before:

public class ApiClientConfiguration {
    private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class);
}

After:

@Slf4j // or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j
public class ApiClientConfiguration {
}

Thread safe methods

In the past, we used Java synchronized keyword, although this is not completely thread safe implementation. There could be situations, where client code can also synchronize on the same instance, which could lead to unexpected deadlocks.

Lombok has its own annotation for this @Synchronized which improve this situation, we can annotate either static or instance methods and this with auto create private, unexposed fields that our implementation will use for locking!

What happens when we want to Rollback Lombok?

We simply cannot rollback Lombok once it is implemented. In a situation where we may want to rollback we may have to go through multiple classes that have been annotated (Lomboked) and effectively delombok them, which is provided by the same project if needed thankfully, and can be invoked on build of a project.

Conclusion

This blog post does not cover all the features Lombok provides, but does give you an ample taste of what can be utilised here, it undoubtably an incredibly useful library for decreasing lines of code and increasing productivity for Java developers, even with its fallbacks it would be shame to miss out on what it offers here.

Leave a comment