Write less Java boilerplate with Lombok


 

Lombok is one of the tools we are using in almost all of our projects. It is a neat little library helping to reduce the boilerplate code and and make our developers life a little bit easier. 

1) What is Lombok and how it works

Lombok provokes a lot of different opinions. Part of the developers love it, because it allows more succinct code and reduces the boilerplate so typical for the Java programming language. Another part of the developers, really don’t like the “hacky” nature of the library. It hooks in “Annotation Processing” phase of Java compilation process and performs bytecode manipulations, based on metadata provided by annotations . You can read more about how Lombok makes its magic here.

I personally find using it really practical. It helps reducing the amount of source code without real business value saving time and lets me concentrate on more important tasks. It’s true that modern development tools provide a lot of code generation features and things like getters and setters can be generated in seconds, but still this is additional source code and it has to me maintained and this requires additional effort. With Lombok no new action is required when new property is added. 

Another good thing about Lombok is that, runtime dependency isn’t required. It is only triggered compile time and after it makes its magic, you are ready to roll. This means that you can release your source code without having explicit dependency to the library. Additionally, if you at some point decide that Lombok is not your thing, you can use delombok tool provided by the creators of the library. This tool replaces all Lombok annotations with generated Java source code, removing source code dependency entirely.    

Something you should have in mind when considering Lombok is that future Java changes can prevent it from working. Because the way it works it is sensitive to changes in some of the core Java APIs. For example Lombok team just recently announced Java 9 support. They had some difficulties because of changes related to modularization and JigSaw project.

2) Installation

With the time the support for Lombok increases and gets better and better. The integration with most of the widely used development tools is straightforward. You can get the latest version from Maven Central repository, add it as “compileOnly” dependency in Gradle 2.12 and that’s all. It has also integration with popular java IDEs. In IntelliJ, you have to install Lombok Plugin and enable “Annotation Processing” and you are ready to use it. Lombok plugin supports all framework features and additionally provided refactoring features like renaming, lombok and delombok. You can see more information on how to use it with other build tools and IDEs  can be seen in the Install section.

3) Usage

3.1) Getters and Setters

Today many popular Java frameworks and enterprise patterns are based on usage of Java Bean / POJO style classes. Following the encapsulation principle, properties of these classes are declared private and can be manipulated through getters and setters. In many cases this leads to writing classes like this:

  public class User {
   private String firstName;
   private String lastName;
   private String username;
   public String getFirstName() {
       return firstName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
   }
   public String getLastName() {
       return lastName;
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
   }
   public String getUsername() {
       return username;
   }
   public void setUsername(String username) {
       this.username = username;
   }
}
@Getter
@Setter
public class User {
   private String firstName;
   private String lastName;
   private String username;
}

Here, big part of the source code has no real business value. In most cases it will be generated with modern IDEs, but still have to be maintained after that. With Lombok @Getters and @Setters annotations the source code can be significantly reduced. This class is fully equivalent to the class listed above. They can be used on class level or for each individual property. By default Lombok will generate getters and setters starting with “get”, “set” and “is” prefixes and public access. Access modifier can be customized using AccessLevel annotation property.

3.2)   Constructors

Lombok provides three annotations related to constructor generation.@NoArgsConstructor generates constructor with no arguments. @RequiredArgsConstructor generates constructor with one argument for each final non-static property. And @AllArgsConstructor generates constructor with one argument for each non-static property. One usage that I personally like is the combination with Spring beans. Using constructor dependency injection is considered a good practice, as is declaring all class properties final when possible. Following these principles often Spring beans look like that:

@Service
public class UsersService implements  ... {
   private final UserRepository userRepository;
   private final PasswordEncoder passwordEncoder;
   @Autowired
   public UsersService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
       this.userRepository = userRepository;
       this.passwordEncoder = passwordEncoder;
   }
   ...
}

Using Lombok this can be replaced with:

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UsersService implements  ... {
   private final UserRepository userRepository;
   private final PasswordEncoder passwordEncoder;
	...
}

Lombok will generate constructor for both final class properties and will add @Autowred annotation. Explanation of the wierd @__ syntax can be found on this Lombok page. As of Spring 4.3 in single constructor beans @Autowired annotation can be skipped and if you are using Spring 4.3 and later this can be further simplified to:

@Service
@RequiredArgsConstructor
public class UsersService implements  ... {
   private final UserRepository userRepository;
   private final PasswordEncoder passwordEncoder;
	...
}

3.3) EqualsAndHashCode and Đ¢oString.

Correct implementation of hashCode() can be tricky. There are a lot of resources on this topic with one of my favourite to be the implementation described by Josh Bloch’s Effective Java in item 8. As a rule of a thumb I always prefer to use IDE’s code generation, java.util.Objects utility or third party libraries for the implementation. Another option, which I started using with Lombok is @EqualsAndHashCode annotation. It will generate equals() and hashCode() methods for you in all annotated classes. By default it uses all non transient and non static fields and generates equals() and hashCode() based on them. If you want to customize the behaviour you can use exclude annotation property and list the fields you don’t want to be included. Additionally if your class is a part of object hierarchy you can include call to the parent class methods using callSuper.

public class User extends AbstractEntity {
    private String firstName;
    private String lastName;
    private String username;
	...
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        if (!super.equals(o)) return false;
        User user = (User) o;
        return Objects.equals(getFirstName(), user.getFirstName()) &&
                Objects.equals(getLastName(), user.getLastName()) &&
                Objects.equals(getUsername(), user.getUsername());
    }
    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), getFirstName(), getLastName(), getUsername());
    }
}
@EqualsAndHashCode(callSuper = true)
public class User extends AbstractEntity {
    private String firstName;
    private String lastName;
    private String username;
	...
}

@ToString annotation generates toString() method for you. It behaves similar to @EqualsAndHashCode and by default it uses all and non static fields to generate string representation. The behaviour can be customized with exclude, callSuper and of annotation parameters.

public class User {
    private String firstName;
    private String lastName;
    private String username;
    ...
    @Override 
    public String toString() {
        return "User(" + this.getFirstName() + ", " + this.getLastName() + ")";
    }
}
@ToString(exclude = "username")
public class User extends AbstractEntity {
    private String firstName;
    private String lastName;
    private String username;
	...
}

3.4) Beans, POJOs and Value objects

@Data and @Value annotations provide mechanism for reducing the boilerplate code typical for POJOs and Value object implementations. @Data can be seen as a collection of @ToString, @EqualsAndHashCode, @Getter / @Setter and @RequiredArgsConstructor annotations:

 public class Location {
   private final String name;
   private final String address;
   private Person resident;
   public Location(String name, String address) {
       this.name = name;
       this.address = address;
   }
   public String getName() {
       return name;
   }
   public String getAddress() {
       return address;
   }
   public Person getResident() {
       return resident;
   }
   public void setResident(Person resident) {
       this.resident = resident;
   }
   @Override
   public String toString() {
       return "Location{" +
               "name='" + name + '\'' +
               ", address='" + address + '\'' +
               ", resident='" + resident.toString() + '\'' +
               '}';
   }
   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       if (!super.equals(o)) return false;
       Location location = (Location) o;
       return Objects.equals(getName(), location.getName()) &&
               Objects.equals(getAddress(), location.getAddress()) &&
               Objects.equals(getResident(), location.getResident());
   }
   @Override
   public int hashCode() {
       return Objects.hash(super.hashCode(), getName(), getAddress(), getResident());
   }
}
@Data
public class Location {
   private String name;
   private String address;
   private Person resident;
}

It generates toString(), equals() and hashCode methods. Constructor with parameters for all final and non static fields if no explicit constructor exists. Public getters and setters for all non final fields and non static fields. One downside is that @Data doesn’t provide the customization abilities of all separate annotations. Fortunately Lombok allows us to override the default behaviour using the explicit annotations.

@Data
public class Location {
   @Getter(AccessLevel.PACKAGE)
   private final String name;
   @Getter(AccessLevel.PACKAGE)
   private final String address;
   private String resident;
	...
}

Also if any of the methods is explicitly defined Lombok is smart enough and will skip it without any warning or error message. @Value can be seen as immutable version of @Data annotation and helps creating and enforcing immutable value objects. In addition to generated toString(), equals() and hashCode() the class is made final. All fields are also made private and final and only getters are generated. Constructor is generated with parameters for all fields that are not initialized during the declaration. Again as @Data annotation, if any constructor is declared explicitly, Lombok will skip constructor generation.

public final class BluePont {
   private final int x;
   private final int y;
   private final String color = "blue";
   public BluePont(int x, int y) {
       this.x = x;
       this.y = y;
   }
   public int getX() {
       return x;
   }
   public int getY() {
       return y;
   }
   public String getColor() {
       return color;
   }
   ...
}
@Value
public class BluePont {
   private int x;
   private int y;
   private String color = "blue";
}

3.4) Builders

Builders are handy way of initialization of complex objects. Again builder implementation requires a lot of trivial boilerplate code to be written. @Builder annotation can save us a lot of time and hustle. Annotating class or constructor creates public static class with name the class name plus Builder suffix. This class has build() method and contains appropriate initialization methods for all annotated class fields. Combined with @Singular annotation it will provide single argument method for all Iterable, Collections, Map fields.

@Builder
public class Point {
   private int x;
   private int y;
   @Singular
   private List colors;
}
Point colourfulPoint = new PointBuilder()
                           .x(1)
                           .y(2)
                           .color(“blue”)
                           .color(“red)
                           .build();

3.5) Logging

Adding logger definition in each class can be really annoying. Because of that it is common practice just to copy and paste logger definition from another class. This often leads to errors, because the developers just forgot to change the target class.

public class A {  
   private static Logger LOGGER = LoggerFactory.getLogger(B.class);
}

Lombok provides logging annotations that can eliminate this problem and also reduce the amount of boilerplate code related to logger definition.

@Slf4j
public class A {
	public voic method() {
			log.debug("...")
	}
}

@Slf4j annotation creates Slf4j logger with name log and inserts it into the annotated class as private static final field. If you prefer another logging implementation Lombok provides annotations for those of them that are most widely used. You can find detailed description here.

3.6) Checked exceptions

In many cases checked exceptions only pollute method definitions and caller source code without contributing with any real value. It is common practice checked exceptions to be wrapped in runtime exception and simply rethrown eliminating the need all callers to handle them. Lombok provides @SneakyThrows annotation, which allows throwing checked exceptions without declaring them in method definition. This annotation is really handy in some situations, but it has to be used with care. If you want to catch sneaky thrown exception later in the call stack you won’t be able, because the compiler won’t let you catch not thrown exception in try block. You have to be careful if this is meaningful API exception or just “impossible” exception polluting the method declaration.

4) Conclusion

These are only part of the most common Lombok features we use. It provides additional customizations and more functionalities, which you can find on Lombok official websiteIt is really useful little library, helping to save time and reduce the boilerplate code. We will continue to use it in our Java projects. May be not for long, because Kotlin really looks great, but this a topic for another story. 

Author: Alexander Ivanov