📜 ⬆️ ⬇️

Lombok returns greatness to java



We in Grubhub use Java almost everywhere. This is a proven language that over the past 20 years has proven its speed and reliability. But over the years, the age of the "old man" still began to show up.

Java is one of the most popular JVM languages , but not the only one. In recent years, Scala, Clojure and Kotlin have competed with it, which provide new functionality and optimized language features. In short, they allow you to do more with more concise code.

These innovations in the JVM ecosystem are very interesting. Because of competition, Java is forced to change in order to remain competitive. The new six-month release schedule and several JEP (JDK enhancement proposals) in Java 8 (Valhalla, local-Variable Type Inference, Loom) are proof that Java will remain a competitive language for many years.

However, the size and scale of Java means that development is moving slower than we would like, not to mention the strong desire to maintain backward compatibility at all costs. In any development, the first priority should be functions, but here the necessary functions are too long developed, if at all, into the language. Therefore, we use Project Lombok in Grubhub to have at our disposal an optimized and improved Java. The Lombok project is a compiler plugin that adds new “keywords” to Java and turns annotations into Java code, reducing development efforts and providing some additional functionality.

Lombok setup


Grubhub is always striving to improve the software life cycle, but each new tool and process has a cost to consider. Fortunately, to connect Lombok, just add a couple of lines to the gradle file.

Lombok converts the source code annotations into Java operators before the compiler processes them: the lombok dependency is lombok available in runtime, so using a plugin will not increase the size of the assembly. To configure Lombok with Gradle (it also works with Maven), simply add the following lines to the build.gradle file:

 plugins { id 'io.franzbecker.gradle-lombok' version '1.14' id 'java' } repositories { jcenter() // or Maven central, required for Lombok dependency } lombok { version = '1.18.4' sha256 = "" } 

When using Lombok, our source code will not be valid Java code. Therefore, you will need to install a plugin for IDE, otherwise the development environment will not understand what it is dealing with. Lombok supports all major Java IDEs. Integration seamless. All functions like “show usage” and “go to implementation” continue to work as before, moving you to the appropriate field / class.

Lombok in action


The best way to get to know Lombok is to see it in action. Consider a few typical examples.

Animate the POJO object


With the help of the good old Java objects (POJO), we separate data from processing to make the code easier to read and to simplify network transmissions. In a simple POJO, there are several private fields, as well as corresponding getters and setters. They cope with the work, but require a large number of template code.

Lombok helps you use your POJO in a more flexible and structured way without additional code. So with the help of the @Data annotation we simplify the basic POJO:

 @Data public class User { private UUID userId; private String email; } 

@Data is just a convenient annotation that applies several Lombok annotations at once.


This abstract alone simply and elegantly covers many typical uses. But POJO does not always cover the necessary functionality. @Data is a fully modifiable class, the abuse of which can increase complexity and limit concurrency, which negatively affects application survivability.

There is another solution. Let's go back to our User class, make it immutable, and add a few other useful annotations.

 @Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; } 

The @Value annotation @Value similar to @Data except that all fields are private and final by default, and no setters are created. Thanks to this, @Value objects immediately become immutable. Since all fields are final, there is no argument constructor. Instead, Lombok uses @AllArgsConstructor . The result is a fully functional, immutable object.

But immutability is not very useful if you only need to create an object using the all-args constructor. As Joshua Bloch explains in the book "Effective Java Programming", if you have a large number of constructor parameters, you should use builders. This is @Builder class comes into effect, automatically generating an internal builder class:

 User user = User.builder() .userId(UUID.random()) .email(“grubhub@grubhub.com”) .favoriteFood(“burritos”) .favoriteFood(“dosas”) .build() 

Generating a builder makes it easy to create objects with a large number of arguments and add new fields in the future. The static method returns an instance of the builder to set all properties of the object. After that, the build() call returns the instance.

@NonNull can use the @NonNull annotation to @NonNull that these fields are not null when an object is instantiated, otherwise NullPointerException thrown. Notice that the avatar field is annotated @NonNull , but not specified. The point is that the default @Builder.Default default points to default.png .

Also note how the builder uses favoriteFood , the only property name in our object. When placing @Singular annotation on a collection property, Lombok creates special builder methods for adding items to the collection individually, rather than adding the entire collection at the same time. This is especially good for tests, because the ways to create small collections in Java cannot be called simple and fast.

Finally, the toBuilder = true parameter adds an instance method toBuilder() , which creates a builder object filled with all the values ​​of that instance. So it is easy to create a new instance, pre-filled with all the values ​​from the source, so that it remains to change only the required fields. This is especially useful for the @Value classes, since the fields are immutable.

A few notes additionally customize the special functions of the setter. @Wither creates methods withX for each property. The input is the value, and the output is the clone of the instance with the updated value of one field. @Accessors allows you to customize automatically created setters. The fluent=true disables the “get” and “set” convention for getters and setters. In certain situations, this may be a useful replacement for @Builder .

If the Lombok implementation is not suitable for your task (and you looked at the annotation modifiers), you can always just go and write your own implementation. For example, if you have a @Data class, but one getter needs user logic, just implement this getter. Lombok will see that the implementation is already provided, and will not overwrite it with an automatically generated implementation.

With just a few simple annotations, the base POJO has got so many rich features that make it easy to use, without having to load us, the engineers, without wasting time and not increasing the development costs.

Deleting a template code


Lombok is not only useful for POJO: it can be applied at any application level. The following ways to use Lombok are especially useful in component classes, such as controllers, services, and DAO (data access objects).

Logging is a basic requirement for all parts of the program. Any class that performs meaningful work must write a log. Thus, the standard logger becomes a template for each class. Lombok simplifies this template to a single annotation, which automatically identifies and creates an instance of the logger with the correct class name. There are several different annotations depending on the structure of the journal.

 @Slf4j // also: @CommonsLog @Flogger @JBossLog @Log @Log4j @Log4j2 @XSlf4j public class UserService { // created automatically // private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class); } 

After the declaration of the logger, add our dependencies:

 @Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; } 

The @FieldDefaults adds final and private modifiers to all fields. @RequiredArgsConstructor creates a constructor that installs an instance of UserDao . The @NonNull adds a check in the constructor and UserDao NullPointerException if the UserDao instance is zero.

But wait, that's not all!


There are many more situations where Lombok shows its best. Previous sections have shown specific examples, but Lombok can facilitate development in many areas. Here are some small examples of how to use it more effectively.

Although the var keyword appeared in Java 9, you can still reassign the variable. Lombok has a val keyword that displays the final type of a local variable.

 // final Map map = new HashMap<Integer, String>(); val map = new HashMap<Integer, String>(); 

Some classes with purely static functions are not intended for initialization. One way to prevent the creation of an instance is to declare a private constructor that throws an exception. Lombok codified this pattern in the @UtilityClass annotation. It generates a private constructor that creates an exception, finally displays the class and makes all methods static.

 @UtilityClass // will be made final public class UtilityClass { // will be made static private final int GRUBHUB = “ GRUBHUB”; // autogenerated by Lombok // private UtilityClass() { // throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated"); //} // will be made static public void append(String input) { return input + GRUBHUB; } } 

Java is often criticized for wordiness due to checked exceptions. A separate Lombok annotation fixes them: @SneakyThrows . As expected, the implementation is pretty tricky. It does not catch the exception and does not even throw an exception in a RuntimeException . Instead, it relies on the fact that during execution, the JVM does not check the consistency of the checked exceptions. Only javac does that. Therefore, Lombok disables this check by compile-time conversion bytecode. The result is run code.

 public class SneakyThrows { @SneakyThrows public void sneakyThrow() { throw new Exception(); } } 

Side by side comparison


A direct comparison best demonstrates how much code Lombok saves. The IDE plugin has a “de-lombok” function, which roughly converts most Lombok annotations into native Java code ( @NonNull annotations @NonNull not converted). Thus, any IDE with a plug-in installed can convert most annotations into native Java code and back. Let's go back to our User class.

 @Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; } 

The Lombok class is just 13 simple, readable, comprehensible lines. But after running de-lombok, the class turns into more than a hundred lines of sample code!

 public class User { @NonNull UUID userId; @NonNull String email; Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = "default.png"; @java.beans.ConstructorProperties({"userId", "email", "favoriteFoods", "avatar"}) User(UUID userId, String email, Set<String> favoriteFoods, String avatar) { this.userId = userId; this.email = email; this.favoriteFoods = favoriteFoods; this.avatar = avatar; } public static UserBuilder builder() { return new UserBuilder(); } @NonNull public UUID getUserId() { return this.userId; } @NonNull public String getEmail() { return this.email; } public Set<String> getFavoriteFoods() { return this.favoriteFoods; } @NonNull public String getAvatar() { return this.avatar; } public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof User)) return false; final User other = (User) o; final Object this$userId = this.getUserId(); final Object other$userId = other.getUserId(); if (this$userId == null ? other$userId != null : !this$userId.equals(other$userId)) return false; final Object this$email = this.getEmail(); final Object other$email = other.getEmail(); if (this$email == null ? other$email != null : !this$email.equals(other$email)) return false; final Object this$favoriteFoods = this.getFavoriteFoods(); final Object other$favoriteFoods = other.getFavoriteFoods(); if (this$favoriteFoods == null ? other$favoriteFoods != null : !this$favoriteFoods.equals(other$favoriteFoods)) return false; final Object this$avatar = this.getAvatar(); final Object other$avatar = other.getAvatar(); if (this$avatar == null ? other$avatar != null : !this$avatar.equals(other$avatar)) return false; return true; } public int hashCode() { final int PRIME = 59; int result = 1; final Object $userId = this.getUserId(); result = result * PRIME + ($userId == null ? 43 : $userId.hashCode()); final Object $email = this.getEmail(); result = result * PRIME + ($email == null ? 43 : $email.hashCode()); final Object $favoriteFoods = this.getFavoriteFoods(); result = result * PRIME + ($favoriteFoods == null ? 43 : $favoriteFoods.hashCode()); final Object $avatar = this.getAvatar(); result = result * PRIME + ($avatar == null ? 43 : $avatar.hashCode()); return result; } public String toString() { return "User(userId=" + this.getUserId() + ", email=" + this.getEmail() + ", favoriteFoods=" + this.getFavoriteFoods() + ", avatar=" + this.getAvatar() + ")"; } public UserBuilder toBuilder() { return new UserBuilder().userId(this.userId).email(this.email).favoriteFoods(this.favoriteFoods).avatar(this.avatar); } public static class UserBuilder { private UUID userId; private String email; private ArrayList<String> favoriteFoods; private String avatar; UserBuilder() { } public User.UserBuilder userId(UUID userId) { this.userId = userId; return this; } public User.UserBuilder email(String email) { this.email = email; return this; } public User.UserBuilder favoriteFood(String favoriteFood) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.add(favoriteFood); return this; } public User.UserBuilder favoriteFoods(Collection<? extends String> favoriteFoods) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.addAll(favoriteFoods); return this; } public User.UserBuilder clearFavoriteFoods() { if (this.favoriteFoods != null) this.favoriteFoods.clear(); return this; } public User.UserBuilder avatar(String avatar) { this.avatar = avatar; return this; } public User build() { Set<String> favoriteFoods; switch (this.favoriteFoods == null ? 0 : this.favoriteFoods.size()) { case 0: favoriteFoods = java.util.Collections.emptySet(); break; case 1: favoriteFoods = java.util.Collections.singleton(this.favoriteFoods.get(0)); break; default: favoriteFoods = new java.util.LinkedHashSet<String>(this.favoriteFoods.size() < 1073741824 ? 1 + this.favoriteFoods.size() + (this.favoriteFoods.size() - 3) / 3 : Integer.MAX_VALUE); favoriteFoods.addAll(this.favoriteFoods); favoriteFoods = java.util.Collections.unmodifiableSet(favoriteFoods); } return new User(userId, email, favoriteFoods, avatar); } public String toString() { return "User.UserBuilder(userId=" + this.userId + ", email=" + this.email + ", favoriteFoods=" + this.favoriteFoods + ", avatar=" + this.avatar + ")"; } } } 

Do the same for the UserService class.

 @Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; } 

Here is an approximate equivalent in standard Java code.

  public class UserService { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class); private final UserDao userDao; @java.beans.ConstructorProperties({"userDao"}) public UserService(UserDao userDao) { if (userDao == null) { throw new NullPointerException("userDao is marked @NonNull but is null") } this.userDao = userDao; } } 

Effect evaluation


The portal Grubhub more than a hundred business services related to the delivery of food. We took one of them and started the “de-lombok” function in the Lombok IntelliJ plugin. As a result, about 180 files changed, and the codebase grew by about 18,000 lines of code after deleting 800 instances of using Lombok. On average, each Lombok line saves 23 Java lines. With this effect, it’s hard to imagine Java without Lombok.

Summary


Lombok is a great helper that implements new language features without requiring much effort from the developer. Of course, it is easier to install a plugin than to teach all engineers a new language and port the existing code. Lombok is not omnipotent, but already out of the box is powerful enough to really help in the work.

Another advantage of Lombok is that it maintains consistency of code bases. We have over a hundred different services and a distributed team around the world, so the coherence of code bases makes it easier for teams to scale up and reduce the burden on context switching when starting a new project. Lombok works for any version since Java 6, so we can count on its availability in all projects.

For Grubhub, this is more than just new features. In the end, all this code can be written manually. But Lombok simplifies the boring parts of the code base without affecting the business logic. This allows you to focus on things that are really important for business and the most interesting for our developers. A monton template code is a waste of time for programmers, reviewers, and maintainers. In addition, since this code is no longer written by hand, it eliminates entire classes of typos. The advantages of autogeneration combined with the power of @NonNull reduce the likelihood of errors and help our development, which aims to deliver food to your table!

Source: https://habr.com/ru/post/438870/