Lombok, o como eliminar código boilerplate

Dejando un poco de lado tanto Spring y Spring Boot, y otro post que tengo a medias sobre JAX-RS, quiero con este post hablar un poco de una librería que estoy usando casi por defecto en todos mis nuevos proyectos o pocs, y que en mi empresa ya también se está estandarizando bastante en los proyectos Java. Es la librería Lombok.

Concepto:
El proyecto Lombok tiene como objetivo eliminar todo el código ‘boilerplate’, repetitivo, etc de nuestras clases para dejar un código mucho más legible, todo esto simplemente añadiendo las correspondientes anotaciones que necesitemos. Para explicarlo un poco más en detalle, iré contando por pasos algunas de las funcionalidades que aporta.

Entorno usado:
Java JDK 1.8
Maven 3.2.1
Git 1.9.4
IDE Intellij 13.1.5 Ultimate version

Pasos:
1. Empecemos revisando esta clase, típico bean / entity para guardar la información de un usuario:

package com.edwise.pocs.lombok.withoutlombok;

import java.time.LocalDate;
import java.util.List;

public class User {

    private long id;
    private String name;
    private String surname;
    private int type;
    private LocalDate birthDate;
    private String phoneNumber;
    private List<String> favoriteBooks;

    public User() {
    }

    public User(long id, String name, String surname, int type,
                LocalDate birthDate, String phoneNumber,
                List<String> favoriteBooks) {
        if (name == null) {
            throw new NullPointerException("Name field can not be null!");
        }
        if (surname == null) {
            throw new NullPointerException("Surname field can not be null!");
        }

        this.id = id;
        this.name = name;
        this.surname = surname;
        this.type = type;
        this.birthDate = birthDate;
        this.phoneNumber = phoneNumber;
        this.favoriteBooks = favoriteBooks;
    }

    public long getId() {
        return id;
    }

    public User setId(long id) {
        this.id = id;
        return this;
    }

    public String getName() {
        return name;
    }

    public User setName(String name) {
        if (name == null) {
            throw new NullPointerException("Name field can not be null!");
        }
        this.name = name;
        return this;
    }

    public String getSurname() {
        return surname;
    }

    public User setSurname(String surname) {
        if (surname == null) {
            throw new NullPointerException("Surname field can not be null!");
        }
        this.surname = surname;
        return this;
    }

    public int getType() {
        return type;
    }

    public User setType(int type) {
        this.type = type;
        return this;
    }

    public LocalDate getBirthDate() {
        return birthDate;
    }

    public User setBirthDate(LocalDate birthDate) {
        this.birthDate = birthDate;
        return this;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public User setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
        return this;
    }

    public List<String> getFavoriteBooks() {
        return favoriteBooks;
    }

    public User setFavoriteBooks(List<String> favoriteBooks) {
        this.favoriteBooks = favoriteBooks;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        User user = (User) o;

        return type == user.type &&
                !(birthDate != null ?
                        !birthDate.equals(user.birthDate) : user.birthDate != null) &&
                !(favoriteBooks != null ?
                        !favoriteBooks.equals(user.favoriteBooks) : user.favoriteBooks != null) &&
                name.equals(user.name) &&
                !(phoneNumber != null ?
                        !phoneNumber.equals(user.phoneNumber) : user.phoneNumber != null) &&
                surname.equals(user.surname);

    }

    @Override
    public int hashCode() {
        int result = name.hashCode();
        result = 31 * result + surname.hashCode();
        result = 31 * result + type;
        result = 31 * result + (birthDate != null ? birthDate.hashCode() : 0);
        result = 31 * result + (phoneNumber != null ? phoneNumber.hashCode() : 0);
        result = 31 * result + (favoriteBooks != null ? favoriteBooks.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name=" + name  +
                ", surname=" + surname  +
                ", type=" + type +
                ", birthDate=" + birthDate +
                ", phoneNumber=" + phoneNumber  +
                ", favoriteBooks=" + favoriteBooks +
                '}';
    }

}

Nada que nos sorprenda en este código, tenemos todo lo básico que se le puede meter a un bean:
– Getters y setters de toda la vida, implementando incluso fluent interface en los setters.
– Constructor con todos los parámetros.
– Constructor sin parametros: lo tenemos que añadir si lo necesitamos, ya que al tener uno creado (el con parámetros), java no nos lo crea por defecto).
– Método toString sobrescrito, con todos sus campos.
– Métodos equals y hashcode sobrescritos.
– Incluso tenemos comprobaciones de nulos para los campos que no queremos que sean nulos por cuestiones de negocio (name y surname en el ejemplo).

Es decir, que, resumiendo, tenemos una clase de unas 150 lineas de código, que no es más que un simple bean con 7 campos. Nada más. Más aún, la mayoría del código de la clase ni siquiera lo habremos escrito nosotros, sino que lo habremos autogenerado con el IDE de turno. ¿Vale la pena tener todo ese código ahí a la vista, tener que copi-pastearlo o generarlo cada vez que creamos una nueva entity o bean?

2. Para testear que nuestro bean tiene todas esas «funcionalidades», he implementado la siguiente clase test. No es todo lo ‘clean code’ que debería, y no tiene sentido testear algunas de estas cosas, pero para nuestro ejemplo nos va a ser muy útil 😛

package com.edwise.pocs.lombok.withlombok;

// imports...

public class UserTest {

    // constants...

    @Test
    public void testUserLombokedSettersAndGetters() {
        User user = new User()
                .setId(USER_ID_1234)
                .setName(USER_NAME_FRODO)
                .setSurname(USER_SURNAME_BAGGINS)
                .setType(USER_TYPE_2)
                .setBirthDate(USER_BIRTHDATE_1500_10_23)
                .setFavoriteBooks(
                        Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT))
                .setPhoneNumber(USER_PHONE_NUMBER_666887766);

        assertThat(user.getId(), is(USER_ID_1234));
        assertThat(user.getName(), is(USER_NAME_FRODO));
        assertThat(user.getSurname(), is(USER_SURNAME_BAGGINS));
        assertThat(user.getType(), is(USER_TYPE_2));
        assertThat(user.getBirthDate(), is(USER_BIRTHDATE_1500_10_23));
        assertThat(user.getFavoriteBooks(),
                contains(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));
        assertThat(user.getPhoneNumber(), is(USER_PHONE_NUMBER_666887766));
    }

    @Test(expected = NullPointerException.class)
    public void testUserLombokedSetterNameWithNameNull() {
        User user = new User()
                .setName(null);
    }

    @Test(expected = NullPointerException.class)
    public void testUserLombokedSetterSurNameWithSurNameNull() {
        User user = new User()
                .setSurname(null);
    }

    @Test
    public void testUserLombokedConstructorWithCorrectValues() {
        User user = new User(USER_ID_1234,
                USER_NAME_FRODO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));

        assertThat(user.getId(), is(USER_ID_1234));
        assertThat(user.getName(), is(USER_NAME_FRODO));
        assertThat(user.getSurname(), is(USER_SURNAME_BAGGINS));
        assertThat(user.getType(), is(USER_TYPE_2));
        assertThat(user.getBirthDate(), is(USER_BIRTHDATE_1500_10_23));
        assertThat(user.getFavoriteBooks(),
                contains(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));
        assertThat(user.getPhoneNumber(), is(USER_PHONE_NUMBER_666887766));
    }

    @Test(expected = NullPointerException.class)
    public void testUserLombokedConstructorWithNameNull() {
        User user = new User(USER_ID_1234,
                null,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));
    }

    @Test(expected = NullPointerException.class)
    public void testUserLombokedConstructorWithSurNameNull() {
        User user = new User(USER_ID_1234,
                USER_NAME_FRODO,
                null,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));
    }

    @Test
    public void testUserLombokedToStringMethod() {
        User user = new User(USER_ID_1234,
                USER_NAME_FRODO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));

        String userString = user.toString();

        assertThat(userString, containsString("id=" + USER_ID_1234));
        assertThat(userString, containsString("name=" + USER_NAME_FRODO));
        assertThat(userString, containsString("surname=" + USER_SURNAME_BAGGINS));
        assertThat(userString, containsString("type=" + USER_TYPE_2));
        assertThat(userString, containsString("birthDate=" + USER_BIRTHDATE_1500_10_23));
        assertThat(userString, containsString("phoneNumber=" + USER_PHONE_NUMBER_666887766));
        assertThat(userString, containsString("favoriteBooks="
                + Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT)));
    }

    @Test
    public void testUserLombokedEqualsMethod() {
        User user1 = new User(USER_ID_1234,
                USER_NAME_FRODO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));

        User user2 = new User(USER_ID_1234,
                USER_NAME_FRODO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));

        assertTrue(user1.equals(user2));
        assertTrue(user2.equals(user1));
    }

    @Test
    public void testUserLombokedEqualsMethodNotEqualUserLombokeds() {
        User user1 = new User(USER_ID_1234,
                USER_NAME_FRODO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));

        User user2 = new User(USER_ID_1234,
                USER_NAME_BILBO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887765,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK));

        assertFalse(user1.equals(user2));
        assertFalse(user2.equals(user1));
    }

    @Test
    public void testUserLombokedHashCodeMethod() {
        User user1 = new User(USER_ID_1234,
                USER_NAME_FRODO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));

        User user2 = new User(USER_ID_1234,
                USER_NAME_FRODO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));

        assertEquals(user1.hashCode(), user2.hashCode());
    }

    @Test
    public void testUserLombokedHashCodeMethodNotEquals() {
        User user1 = new User(USER_ID_1234,
                USER_NAME_FRODO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887766,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK, USER_FAVORITE_BOOK_THE_HOBBIT));

        User user2 = new User(USER_ID_1234,
                USER_NAME_BILBO,
                USER_SURNAME_BAGGINS,
                USER_TYPE_2,
                USER_BIRTHDATE_1500_10_23,
                USER_PHONE_NUMBER_666887765,
                Arrays.asList(USER_FAVORITE_BOOK_RED_BOOK));

        assertNotEquals(user1.hashCode(), user2.hashCode());
    }
}

Como veis, testeo los getters, setters, constructor, los equals y hashcode, los toString, incluso los NPE si insertamos nulo en los campos name y surname. Si lo ejecutamos ahora mismo, todos los tests pasan correctamente. Nos servirá para comprobar que lombok funciona.

3. Ahora vamos a instalar lombok. Empezaremos por el plugin para nuestro IDE. Lombok, al encargarse de generar él por su cuenta todo los getters, setters, constructores, etc (en los siguientes pasos lo veremos), aunque nuestro proyecto compile y funcione (una vez tengamos la dependencia maven o el jar en nuestras librerias), si no hacemos nada en nuestro IDE, este, nos marcará errores de compilación, ya que para él no existirán los getters, setters, etc. Para ello, en la web de lombok tenemos todo lo necesario. Para el caso de Intellij, es un plugin normal: https://code.google.com/p/lombok-intellij-plugin/. Lo instalamos como cualquier plugin. Para eclipse, por ejemplo, hay que bajarse un jar y ejecutarlo.

4. Teniendo ya nuestro IDE preparado para lombok, ahora vamos a añadir la dependencia maven en nuestro proyecto:

        ...
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.14.8</version>
            <scope>provided</scope>
        </dependency>
        ...

5. Modificamos nuestra clase User, y la dejamos tal que así:

package com.edwise.pocs.lombok.withlombok;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.experimental.Accessors;

import java.time.LocalDate;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class User {

    private long id;

    @NonNull
    private String name;

    @NonNull
    private String surname;

    private int type;
    private LocalDate birthDate;
    private String phoneNumber;
    private List<String> favoriteBooks;
}

Mucho más simple y limpia. Y si probamos a ejecutar los tests de antes, veremos que funcionan. Vamos a explicar un poco cada anotación que genera toda esta «magia», aunque alguna es bastante autoexplicativa:

@Data: Esta anotación suele ser la anotación por defecto usada en cualquier bean con lombok. Realmente engloba varias anotaciones, que son, principalmente: @Getter, @Setter, @ToString y @EqualsAndHasCode. Bastante claras: generan getters, setters, método toString y metodos equals y hashcode (para todos los campos de nuestro bean), respectivamente.

@AllArgsConstructor: genera un constructor con todos los campos, en el orden en el que están creados.

@NoArgsConstructor: genera un constructor sin argumentos.

@Accessors(chain = true): provoca que los setters se generen con fluent interface (devolviendo una instancia del objeto).

@NonNull: genera comprobaciones de nulo para esos campos (tanto en setters como en constructor, si los hay), y lanza una NPE en el caso de venir a nulo.

6. Esas son algunas de las anotaciones más usadas en beans. Otras anotaciones que me parecen interesantes de lombok (hay más aún…):

@Log, @Log4j, @Log4j2, @Slf4j…: Te genera la típica constante estática para logs (para commons log de apache, Log4j, Log4j2, etc):

import lombok.extern.log4j.Log4j2;

@Log4j2
public class FooService {

    // ...
}

// ES EQUIVALENTE A 

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class FooService {
    private static final Logger log =
            LogManager.getLogger(FooService.class);

    // ...
}

@Builder: esta ahora mismo está como «experimental» (igual que la de Accessors, por cierto), pero es muy útil: te genera un builder completo del bean, viene muy bien para construir beans con muchos parámetros de una manera muy legible:

import lombok.Data;
import lombok.experimental.Accessors;
import lombok.experimental.Builder;

@Data
@Accessors(chain = true)
@Builder
public class Book {
    private Long id;
    private String title;
    private String isbn;
    private Short type;
}

// ES EQUIVALENTE A 

public class Book {
    private Long id;
    private String title;
    private String isbn;
    private Short type;

    // constructor...

    // getters and setters...

    public static BookBuilder builder() {
         		return new BookBuilder();
         	}

         	public static class BookBuilder {
                private Long id;
                private String title;
                private String isbn;
                private Short type;

         		private BookBuilder() {}

         		public BookBuilder id(Long id) {
         			this.id = id;
         			return this;
         		}

         		public BookBuilder title(String title) {
         			this.title = title;
         			return this;
         		}

                public BookBuilder isbn(String isbn) {
                    this.isbn = isbn;
                    return this;
                }

                public BookBuilder type(Short type) {
                    this.type = type;
                    return this;
                }

         		@Override
                public String toString() {
         			return "BookBuilder(id = " + id
                            + ", title = " + title
                            + ", isbn = " + isbn
                            + ", type = " + type + ")";
         		}

         		public Book build() {
         			return new Book(id, title, isbn, type);
         		}
         	}
}

7. Lombok también ofrece una funcionalidad para «transformar» el código implementado con anotaciones lombok al código equivalente sin ellas, con el jar. Se puede poner como una tarea de ant o de maven. Más info, aquí. Puede venir bien para revisar posibles errores.

8. Hay varias anotaciones más, podéis echarle un vistazo en su API.
Como veis, usando lombok, tendremos un código mucho más legible en nuestros proyectos, principalmente para los beans. Podeis descargaros el código de este post en mi github por si queréis jugar un poco con ello.