Pitfalls of Automatically generated code – Lombok example

There are some rules that every programmer should follow while developing a software. They require using repeatable code which generally needs to be implemented by the same pattern. This is commonly known as boilerplate code.

 Although most of the current IDEs let us automatically generate this code, the lines remain visible which makes the coding process less eye-friendly. The most popular tool that helps us generate boilerplate is Lombok. It is quite popular due to the fact that it only requires a few annotations above the class declaration to create a fully featured class. It is so simple in use that a lot of programmers use it without any deeper research. This might later lead to its undertaking unpredictable actions, especially while working with other libraries or frameworks. This article is going to cover the most popular pitfalls that programmers might fall into.

Do not use Lombok or even IDE to generate getter on collection fields.

To understand this issue let’s think why we even use getters at all. They are used to retrieve information from an object without changing its state (that’s the setters’ role). Let’s take a look at the following example:

public class Student {

    private final List<Long> exampleList;

    public Student(List<Long> exampleList) {
        this.exampleList = exampleList;
    }

    public List<Long> getExampleList() {
        return exampleList;
    }

    @Override
    public String toString() {
        return "Student{" + "exampleList=" + exampleList + '}';
    }
}
List<Long> list = new ArrayList<>();
Student student = new Student(list);

list.add(3L);
list.add(5L);

System.out.println(student);

student.getExampleList().clear();

System.out.println(student);

Nothing interesting happens here; there is only a simple Student class which we populate with Long values. But take a look at the console output:

The state of a Student class was changed by accessing the field using a getter. This action is correct, as getter’s job is to return any reference to an object – which then forms the list. But changing the state of a class by simply using a getter might also be dangerous. This example covers a case of a code generated with the use of IntelliJ. However, with Lombok annotations @Getter works in the same way.

To avoid this potential issue, the getters of the collection fields should return a copy of the reference, not only the main reference itself. Here is what we get after introducing a small change inside the getter:

public class Student {

    private final List<Long> exampleList;

    public Student(List<Long> exampleList) {
        this.exampleList = exampleList;
    }

    public List<Long> getExampleList() {
        return new ArrayList<>(exampleList);
    }

    @Override
    public String toString() {
        return "Student{" + "exampleList=" + exampleList + '}';
    }
}

The console output looks like this: 

Be careful while using Lombok with Spring Data and Hibernate

As previously mentioned, Lombok is very popular among developers. It is commonly used in Spring Entities to make them clean and simple. Entities represent a database model which is often quite complex, therefore losing strict control over those classes may lead to performance issues. 

Consider avoiding @EqualsAndHashCode

Implementation of equals() and hashcode() methods is strongly discussed among the developers’ community. Lombok basic implementation uses every field of class/entity to compare objects. 

public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Student)) {
            return false;
        } else {
            Student other = (Student)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                label47: {
                    Object this$id = this.id;
                    Object other$id = other.id;
                    if (this$id == null) {
                        if (other$id == null) {
                            break label47;
                        }
                    } else if (this$id.equals(other$id)) {
                        break label47;
                    }

                    return false;
                }

                Object this$universitySet = this.universitySet;
                Object other$universitySet = other.universitySet;
                if (this$universitySet == null) {
                    if (other$universitySet != null) {
                        return false;
                    }
                } else if (!this$universitySet.equals(other$universitySet)) {
                    return false;
                }

                Object this$detailsSet = this.detailsSet;
                Object other$detailsSet = other.detailsSet;
                if (this$detailsSet == null) {
                    if (other$detailsSet != null) {
                        return false;
                    }
                } else if (!this$detailsSet.equals(other$detailsSet)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof Student;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $id = this.id;
        int result = result * 59 + ($id == null ? 43 : $id.hashCode());
        Object $universitySet = this.universitySet;
        result = result * 59 + ($universitySet == null ? 43 : $universitySet.hashCode());
        Object $detailsSet = this.detailsSet;
        result = result * 59 + ($detailsSet == null ? 43 : $detailsSet.hashCode());
        return result;
    }
}

This solution completely ignores the fact that some fields may undergo changes during runtime. It is possible to fix this by adding a configuration parameter inside the annotation @EqualsAndHashCode(onlyExplicitlyIncluded = true), and including some immutable field (such as business key) which is set when an object is created. You may ask „Why should I add another field when I already have the primary key?”. Well, the primary key may be null as long as the entity is not saved in the database. This means that two entities with primaryKey = null would return “true”, and that might not always be true.

Consider avoiding @ToString

Using @ToString method from Lombok or IDE is also dangerous while working with entities. Let’s say that we use the default implementation for our Student class:

@Entity
public class Student {
  @Id
  @GeneratedValue(
      strategy = GenerationType.IDENTITY
  )
  private Long id;
  @OneToMany
  private Set<University> universitySet;
  @OneToMany
  private Set<Details> detailsSet;

  public Student() {
  }

  public String toString(){
    return "Student(id=" + this.id + ", universitySet=" + this.universitySet + ", detailsSet=" + this.detailsSet + ")";
  }
}

This is the view from the Target directory. This implementation is almost the same as the IDE’s one. The issue is that some of these relations could be set to FetchType.LAZY which would lead to an additional call to the database in order to retrieve information needed to @ToString method. 

It’s possible to exclude fields from Lombok’s @ToString method by adding @ToString.Exclude above the field declaration but this makes the code less readable.

Do not use @Data while working with Entities!

The previous paragraphs explained how risky it is to use Lombok’s annotations while working with JPA. There is one more annotation that should be avoided – @Data. Documentation says that it is „Equivalent to @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode.”

Needless to say that having all these annotations combined without any supervision is a bad practice while working with entities.

Conclusion

In this article we have discussed the importance of understanding how automatically generated codes work before using them. Lombok is a very popular library which helps to cope with boilerplate code but it must be used wisely. It is really important to keep this in mind because the cost of the implementation of these annotations is just a few lines of code, yet consequences may be much more complex.