Lombok returns greatness to java

https://bytes.grubhub.com/lombok-makes-java-cool-again-171102bdcc52
  • Transfer


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 annotations in the source code to Java operators before the compiler processes them: the dependency is lombokabsent 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 annotation @Datawe simplify the basic POJO:

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

@Data - just a convenient annotation, which applies several Lombok annotations at once.

  • @ToStringgenerates an implementation for the method toString(), which consists of an accurate representation of the object: the name of the class, all the fields and their values.
  • @EqualsAndHashCodegenerates implementations equalsand hashCode, which by default use non-static and non-stationary fields, but are customizable.
  • @Getter / @Setter generates getters and setters for private fields.
  • @RequiredArgsConstructorcreates a constructor with the required arguments, where final fields and fields with annotation are required @NonNull(more on this below).

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

There is another solution. Let us return to our class User, 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 annotation is @Valuesimilar @Dataexcept that all fields are private and final by default, and no setters are created. Due to this, objects @Valueimmediately 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 @Builderwhere the class comes in , automatically generating the inner class of the builder:

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 this call build()returns the instance.

Annotation @NonNullcan be used to state that these fields are not null when an object is created, otherwise an exception is thrown NullPointerException. Note that the avatar field is annotated @NonNullbut not specified. The fact is that the @Builder.Defaultdefault annotation points to default.png .

Also note how the builder uses favoriteFood, the only property name in our object. When posting annotations@Singularon the collection property, Lombok creates special builder methods for individually adding items to the collection, not for simultaneously adding the entire collection. This is especially good for tests, because the ways to create small collections in Java cannot be called simple and fast.

Finally, the parameter toBuilder = trueadds an instance method toBuilder()that 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 classes @Value, because fields are immutable.

A few notes additionally customize the special functions of the setter. @Withercreates methodswithXfor each property. The input is the value, and the output is the clone of the instance with the updated value of one field. @Accessorsallows you to customize automatically created setters. This parameter fluent=truedisables the “get” and “set” conventions for getters and setters. In certain situations, this may be a useful substitute @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 class @Data, 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 annotation @FieldDefaultsadds final and private modifiers to all fields. @RequiredArgsConstructorcreates a constructor that sets up an instance UserDao. The annotation @NonNulladds a check in the constructor and throws an exception NullPointerExceptionif the instance UserDaois 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 a keyword appeared in Java 9, varyou can still reassign a variable. Lombok has a keyword valthat 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 annotation @UtilityClass. 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 abstract Lombok eliminates it: @SneakyThrows. As expected, the implementation is pretty tricky. It does not catch the exception and does not even wrap the exception in 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 that roughly converts most Lombok annotations into native Java code (annotations are @NonNullnot converted). Thus, any IDE with a plug-in installed can convert most annotations into native Java code and back. Let's return to our class User.


@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 class UserService.

@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 codeYou can write by hand. 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 in combination with power @NonNullreduce the likelihood of errors and help our design, which aims to deliver food to your table!

Also popular now: