Introduction to Project Lombok

Reading Time: 9 minutes

Lombok is a library expedite many tedious tasks and reduces Java source code verbosity.

Lombok uses annotation processing through APT (Annotation Processing Tool). So, when the compiler calls it, the library generates new source file based on annotation in original.

How Lombok Works Internally

To understand how Project Lombok works, first we need to understand how Java compilation works. OpenJDK provides an excellent overview of the compilation process. To paraphrase, compilation has 3 stages:
1. Parse and Enter
2. Annotation Processing
3. Analyse and Generate

This image has an empty alt attribute; its file name is javac-flow.png

In the Parse and Enter phase, the compiler parses source files into an Abstract Syntax Tree. Think of the Abstract Syntax Tree as the DOM-equivalent for Java code. Parsing will only throw errors if the syntax is invalid. Compilation errors such as invalid class or method usage are checked in phase 3.

In the Annotation Processing phase, custom annotation processors are invoked. This is a pre-compilation phase. Annotation processors can do things like validate classes or generate new resources, including source files. Annotation processors can generate errors that will cause the compilation process to fail.

In the last phase, Analyse and Generate, the compiler generates class files (byte code) from the Abstract Syntax Trees generated in phase 1.

Lombok and Compilation

Lombok attach itself into the compilation process as an annotation processor. But Lombok is not your normal annotation processor. Normally, annotation processors only generate new source files whereas Lombok modifies existing classes.

The trick is that Lombok modifies the Abstract Syntax Tree. . It turns out that changes made to the Abstract Syntax Tree. in the Annotation Processing phase will be visible to the Analyse and Generate phase. Thus, changing the Abstract Syntax Tree. will change the generated class file. Actually the annotation processing spec doesn’t allow you to modify existing classes. The annotation processing API doesn’t provide a mechanism for changing the Abstract Syntax Tree. of a class. But people at Project Lombok got around this through some unpublished APIs of javac.

To understand Lombok execution path, you can visit Javac: Launching as annotation processor

If we want to use Lombok in our project than, we need to do some configuration in IDEs, so for our example I am configuring in two most popular IDEs i.e. IntelliJ & Eclipse.

IDE Configuration:

1.1 IntelliJ

We need to enable annotation processing in IntelliJ using following steps:

  1. Enable annotation processing box is checked.
  2. Obtain processors from project classpath option is selected.

Installing IDE plugin:

We need to go to the settings and then plugin, open the Marketplace tab, type lombok and choose Lombok Plugin by Michail Plushnikov:

1.2 Eclipse

If we’re using Eclipse IDE, we need to get the Lombok jar first. The latest version is located on Maven Central. For our example, we’re using lombok-1.18.10.jar.

Next, we can run the jar via java -jar command and an installer UI will open. This tries to automatically detect all available Eclipse installations, but it’s also possible to specify the location manually.

Once we’ve selected the installations, then we press the Install/Update button:

Adding Lombok to the Compile Classpath

The last remaining part of configuration is to ensure that Lombok binaries are on the compiler classpath. Using Maven, we can add the dependency to the pom.xml:

Lombok Annotations

@Data

All together: A shortcut for @ToString, @EqualsAndHashCode, @Getter on all fields, and @Setter on all non-final fields, and @RequiredArgsConstructor!

import lombok.Data;

@Data
public class User {
    private String name;
    private int age;
    private String address;
}

In following code snippet, we are using setName, setAddress, setAge but we didn’t define these methods in User class. so for us lombok modified User class and put all setters, getters & toString method.

public class TestApp {
    public static void main(String[] args){
        User user = new User();
        user.setName("TestUser1");
        user.setAddress("Mumbai");
        user.setAge(30);
        System.out.println(user); // calling toString 
        System.out.println(user.getName());
    }
}

output:

User(name=TestUser1, age=30, address=Mumbai)
TestUser1

@Builder

@Builder allows you to automatically generate the code required to have your class be instantiable with code such as:

User anotherUser = User.builder().name("Ram Mohan").address("Mumbai").age(29).build();

@Singular annotation, lombok will treat that builder node as a collection, and it generates 2 ‘adder’ methods instead of a ‘setter’ method. One which adds a single element to the collection, and one which adds all elements of another collection to the collection. No setter to just set the collection (replacing whatever was already added) will be generated. A ‘clear’ method is also generated.

Note: @Singular can be use with @builder only.

import java.util.Set;

public class TestApp {
    public static void main(String[] args){
        Person person = Person.builder()
                              .name("Mohan")
                              .age(29)
                              .Address("Mumbai")
                              .Address("Indore").build();
        System.out.println(person);
    }
}

@Builder
@ToString
 class Person {
    @Builder.Default private long created = System.currentTimeMillis();
    private String name;
    private int age;
    @Singular("Address")
    private Set Address;
}

output:

Person(created=1574363271726, name=Mohan, age=29, Address=[Mumbai, Indore])

@Value: Immutable classes made very easy.

@Value is shorthand for: final @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter, except that explicitly including an implementation of any of the relevant methods simply means that part won’t be generated and no warning will be emitted.

 For example, if you write your own toString, no error occurs, and lombok will not generate a toString method. Also, any explicit constructor, no matter the arguments list, implies lombok will not generate a constructor. If you do want lombok to generate the all-args constructor, you can add @AllArgsConstructor to the class.

@Value is the immutable variant of @Data

import lombok.Builder;
import lombok.Value;

public class TestApp {
    public static void main(String[] args){
        ImmutableUser immutableUser = TestApp.immutableBuilder();
        System.out.println("Immutable Demo");
        System.out.println("Immutable User Name: "+immutableUser.getName());
    }
    @Builder(builderMethodName = "immutableBuilder")
    public static ImmutableUser immutableBuilder(){
        return new ImmutableUser("Mohan", 30);
    }
}

@Value
class ImmutableUser {
    private String name;
    private int age;
}

output:

Immutable Demo
Immutable User Name: Mohan

@val final local variables.

import lombok.val;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;


public class TestApp {
    public static void main(String[] args){
        valDemo();
        valDemoWithMap();
        valDemoWithList();
    }
    public static void valDemoWithList(){
        val list = new ArrayList();
        list.add("One");
        list.add("Two");
        System.out.println("Lenght of list: "+list.size());
    }

    public static void valDemoWithMap(){
        val map = new HashMap();
        map.put("One", 1);
        map.put("Nine", 9);
        map.put("Seven", 7);
        for(final Map.Entry entry : map.entrySet()){
            System.out.println("Key: "+entry.getKey()+" \n Value: "+entry.getValue());
        }
    }

    public static void valDemo(){
        val name = "xyz"; // name is final by default
        /*  Can not assign value to final 'name'
         *  name = "abc";
         */
        System.out.println("Val example");
        System.out.println("Name= "+ name);
    }
}

output:

Val example
Name= xyz
Key: One 
 Value: 1
Key: Nine 
 Value: 9
Key: Seven 
 Value: 7
Lenght of list: 2

@Var local variables.

import lombok.var;

public class TestApp {
    public static void main(String[] args){
       varDemo();
    }
    public static void varDemo(){
        var name = "xyz"; // name is not final by default
        name = "abc";
        System.out.println("Var example");
        System.out.println("Name= "+ name);
    }
}

output:

Var example
Name= abc

@NonNull

You can use @NonNull on the parameter of a method or constructor to have lombok generate a null-check statement for you.

import lombok.Data;
import lombok.NonNull;

public class TestApp {
    public static void main(String[] args){
        var user = new User();
        user.setName("Mohan");
        user.setAge(30);
        nonNullDemo(user);
        // following method will throw nullPointer Exception
        //nonNullDemo(null);
    }

    public static void nonNullDemo(@NonNull User user){
        System.out.println("@nonNull example");
        System.out.println("name: "+user.getName());
        System.out.println("Age: "+user.getAge());
    }
}

@Data
class User {
    private String name;
    private int age;
    private String address;
}

output:

@nonNull example
 name: Mohan
 Age: 30

@nonNull with nullPointer Exception

import lombok.Data;
import lombok.NonNull;

public class TestApp {
    public static void main(String[] args){
        // following method will throw nullPointer Exception
        nonNullDemo(null);
    }

    public static void nonNullDemo(@NonNull User user){
        System.out.println("@nonNull example");
        System.out.println("name: "+user.getName());
        System.out.println("Age: "+user.getAge());
    }
}

@Data
class User {
    private String name;
    private int age;
    private String address;
}

output:

Exception in thread "main" java.lang.NullPointerException: user is marked non-null but is null
     at org.projectlombok.example.application.TestApp.nonNullDemo(TestApp.java:14)
     at org.projectlombok.example.application.TestApp.main(TestApp.java:11)

Other useful tags are @Synchronized, @Cleanup, @EqualsAndHashCode, @Log,

@Synchronized is a safer variant of the synchronized method modifier. Like synchronized, the annotation can be used on static and instance methods only.

Note: It works similarly to the synchronized keyword, but it locks on different objects. The keyword locks on this, but the annotation locks on a field named $lock, which is private.

If the field does not exist, it is created for you. If you annotate a static method, the annotation locks on a static field named $LOCK instead.

We can use @Cleanup  to ensure a given resource is automatically cleaned up before the code execution path exits your current scope. You do this by annotating any local variable declaration with the @Cleanup annotation like so:

@Cleanup InputStream in = new FileInputStream("some/file");

Happy Coding!!

Leave a Reply