Lombok and Java — the beauty and the beast

Lombok and Java — the beauty and the beast

Photo credit: pixabay, Lombok island, Indonesia

Table of Contents

  • Introduction
  • Using Lombok
  • Getters/Setters
  • Equals/HashCode
  • Builder
  • NoArgs/AllArgs Constructor
  • Immutability
  • SneakyThrows
  • The Rest
  • Delombok
  • Gotchas
  • Conclusion

Introduction

When we design and build modern software, one critical aspect we want to focus on is the separation of concerns. This is necessary for any good software to scale, and all the more important when building micro-services. DTOs (or POJOs) help encapsulate data when communicating within/across services and also when (de)serializing objects (from)to JSON/XML etc.

DTOs are an important aspect of such a system and are equally boring (and error-prone) to write. They contain a lot of boilerplate code, which seems trivial. At the same time, a small bug (like missing the final keyword, or returning a shallow copy) can wreak havoc, and be very hard to debug.

For those of us who code in Java, we know the pain of writing POJOs. We have seen never-ending DTOs that keep on growing in size as we add more fields. For a single field added, we might add getters and setters at the bare minimum. And if you are adventurous you might end up modifying constructors and adding builder functions. Not to mention, if you throw in immutability and deep-cloning, it becomes all the more complicated.

Using Lombok

If you ever opened up the atlas and looked up where the island of Java is, you might see the neighboring island is called Lombok. No wonder in the world of programming languages, project Lombok has been built to help out Java programmers.

Lombok started out with the goal to minimize the verbosity of POJOs. Over time it has introduced getters & setters, toString, equals, and hashCode, logging, builder, immutability, synchronization, etc. The goal is to reduce boilerplate code and improve engineering productivity, so engineers can focus on feature development, rather than fixing nuts and bolts. It would also simplify readability, refactoring, and identifying bugs easily.

That is the beauty of Lombok. However, things can turn beastly quickly if you do not really understand what is going on under the hood. Test cases can turn nasty and code coverage numbers would start to look weird.

If you want the ‘beauty’, you need to know how to tame the ‘beast’.

Here we will try to understand what Lombok does and how that is done. Consequently, we will understand how to use it the right way.

Source code, byte code, and Lombok

Since Java is platform-independent, the initial .java files are converted into .class bytecode by the compiler. This is what the JRE is interested in. The JRE is not platform-independent and executes the bytecode on that OS.

Lombok intervenes in between the .java and .class files and injects byte-level code into the .class files for getters, setters, builders, etc.

Nothing really changes beyond the .class files downstream, and the JRE is not aware of Lombok’s presence (and it should not be). As you might understand by now, Lombok is needed only at compile time. Because of that reason, the maven scope can (and should) be set to compile/provided.

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
<scope>provided</scope>
</dependency>

Lombok Annotations are used to reduce verbosity. These annotations take the place of boilerplate code that an engineer would normally write. At compile time, Lombok would read the annotations and inject the corresponding code into the generated class files. Next, we will look at the annotations and how to use them safely.

Getters/Setters

These are the simplest to start with. A typical POJO would look something like this. For only 4 attributes of the class, we have 8 methods and 38 loc.

<a href="https://medium.com/media/73fec7937bd895a161e7b2421521bf40/href">https://medium.com/media/73fec7937bd895a161e7b2421521bf40/href</a>

You can imagine a DTO with more attributes and a few custom methods being hundreds of lines long. Not anymore with Lombok:

<a href="https://medium.com/media/8cd51f8a6bbc782d90047424c4d9e564/href">https://medium.com/media/8cd51f8a6bbc782d90047424c4d9e564/href</a>

Equals/HashCode

You should understand the usage of equals() and hashCode(), before introducing these annotations. Otherwise, @EqualsAndHashCode would generate unnecessary code and reduce your test code coverage. For example, if you introduce this annotation but do not use the DTO in an equality check, or in a Set or Map of such DTOs (and consequently do not have test cases for such scenarios), these lines will never be executed.

Here is an example of what Lombok generates for a single attribute:

<a href="https://medium.com/media/e8b7e4bd890b89f718c208283b50e7a8/href">https://medium.com/media/e8b7e4bd890b89f718c208283b50e7a8/href</a>

Builder

The builder is a very useful construct, particularly for lengthy DTOs. The builder also helps avoid multiple constructors with optional parameters. As we can see from the example below, we are setting the fName and age using @Builder. This is based on the builder pattern by the Gang of Four.

Using setters would have generated more lines, and with constructors, we would need multiple overloads to satisfy all possible scenarios (or pass null/default values for the remaining fields). And much like @EqualsAndHashCode, the builder annotation generates a lot of code, so unless you have test cases that cover these, the code coverage is bound to drop.

Note that when using builder, fields that are not set will be defaulted (default values for primitives, and null for the others). Also note, a builder cannot mutate an object like a setter. It should be used to create a new object all at once. Further mutation, if required should be done by setters.

<a href="https://medium.com/media/7b38e887d154d0bef3e665f3f661d930/href">https://medium.com/media/7b38e887d154d0bef3e665f3f661d930/href</a>

NoArgs/AllArgs Constructor

As the names suggest, these annotations help to create the default (no-arg) constructor as well as an all-args constructor. Java (not Lombok) creates the default constructor (in the .class), only when there isn’t any user-defined one. So if you have neither of these annotations, java creates the no-arg (which, Jackson leverages by the way). However, if you are using @Builder or @AllArgsConstructor, and also using Jackson to (de)serialize the object (from)to JSON, make sure to add @NoArgsConstructor. Jackson needs the constructors to properly instantiate the objects. This holds true for most JSON/XML processing APIs.

Long story short, a working DTO-Jackson combo would start to fail if you introduce the @Builder or @AllArgsConstructor without adding the @NoArgsConstructor.

<a href="https://medium.com/media/99c2e48ce617b8addd46c31eeb1b6fe5/href">https://medium.com/media/99c2e48ce617b8addd46c31eeb1b6fe5/href</a>

Immutability

Creating immutable classes is also a nice feature that Lombok supports using the @Value annotation. It ensures that no setters are generated and the fields are defined as final so that their references can’t be changed. Here is an example of how the code actually looks. Notice the deep-immutability across all data types (including the custom Address class).

<a href="https://medium.com/media/399c0290b4178b8d123925100e1cae3f/href">https://medium.com/media/399c0290b4178b8d123925100e1cae3f/href</a>

If you do not need immutability, but need getters, setters, equals, hashCode, and allArgsConstructor, instead of annotating these separately, you can use the @Data annotation. @Data and @Value are similar with the exception of immutability. Also, checkout @With if you are interested in immutable setters.

SneakyThrows

If you are a good programmer, you will ensure that all exceptions are handled properly. This doesn’t necessarily mean a try-catch block in every method. Rather, you could have a delegator function that calls multiple layers of functions. The lower-level functions simply throw the exceptions up, and the delegator function handles them gracefully. Sounds good !! But that means, all the lower-level functions need to add a throws clause to their method signature, increasing verbosity, or changing every time a new code snippet is added that throws a specific exception.

@SneakyThrows helps wrap up all of the checked exceptions to an unchecked exception, thus avoiding the verbosity or necessity to handle new exceptions. As much as it makes the code cleaner, it makes it very dangerous, as there are no more compile-time warnings. You, and only you are responsible to handle the unchecked exception(s) before they make the JVM crash. So be very cautious when using this annotation. Here is an example:

<a href="https://medium.com/media/e1d1b93a8cad8efd9504c2b9bd30138f/href">https://medium.com/media/e1d1b93a8cad8efd9504c2b9bd30138f/href</a>

The rest

Using loggers, or synchronized locks can also be annotated using Lombok. For a full set of annotations, refer to this link.

Deep cloning can be achieved using the Builder annotation. The preferred way of cloning is to use copy constructors and not to use the Cloneable interface (you would be flagged by Sonar). It would be nice to get a copy constructor available via Lombok. Here is the list of experimental features and we do not see any such option yet.

Delombok

If you are curious about the code that Lombok generates, you can always ‘delombok’ and look into that. This can be done from the CLI like this, or from any IDE that has Lombok integration. Here is how IntelliJ does this:

Delomking code in IDE

Gotchas

Lombok treats native boolean and boxed Boolean differently when generating getter/setter. Be careful about how this can cause improper (de)serialization. Always test your serialized data before pushing such changes. Check out the getters for ‘adult’ and ‘smoker’ below:

<a href="https://medium.com/media/f09f3bdbf14b2380e56e8f5f0f5540e3/href">https://medium.com/media/f09f3bdbf14b2380e56e8f5f0f5540e3/href</a>

Conclusion

Lombok is a nifty utility that can reduce a lot of boilerplate code. It can be particularly useful for creating DTOs for micro-services (even shared DTOs) or quickly moving from lo4j2 to slf4j (by just changing one annotation).

You might argue, that most modern IDEs help generate getters/setters/constructors. This is true; however, that doesn’t reduce the verbosity of the code. An engineer who later needs to modify the code still needs to go through that code. Imagine a DTO with an overridden setAddresses below:

<a href="https://medium.com/media/8e8d56be57f336f6441252e61be5899d/href">https://medium.com/media/8e8d56be57f336f6441252e61be5899d/href</a>

If this same class had a handful of attributes and was not Lomboked, a simple eyeballing would not uncover the custom setter, that is so clearly visible above due to its compactness.

At the same time, since you are playing with bytecode, be responsible and cautious. Code coverage, exception handling, (de)serializing are some of the domains you would want to be careful about.

As I mentioned earlier, if you can tame the beast, you should have no trouble finding the beauty that Lombok provides. Happy Lombok-ing !!

Lombok and Java — the beauty and the beast was originally published in Walmart Global Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Article Link: Lombok and Java — the beauty and the beast | by Shouvik Dutta | Walmart Global Tech Blog | Medium