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.

Spring Boot series: generar war para Tomcat

Comienzo hoy una serie de miniposts sobre Spring Boot, en los que trataré funcionalidades, consejos y demás sobre este proyecto de los chicos de Spring que me gusta tanto.
Y, para empezar con esta serie, empezaré con una funcionalidad sencilla de Spring Boot: generar un war que funcione perfectamente en cualquier Tomcat u otro contenedor web similar. ¡Al lio!

Concepto:
Como ya comenté en mi primer post sobre Spring Boot, con Spring Boot, por defecto, lo que hacemos realmente es generar un jar, ejecutable, con el que podemos arrancar nuestra aplicación fácilmente (o directamente con el plugin de maven spring-boot-maven-plugin, que viene a hacer lo mismo).
Pero… ¿Y si queremos crear un war para llevárnoslo a nuestro Tomcat de toda la vida? Para ello, tendremos que añadir un par de cosas a nuestro proyecto Spring Boot.
Como proyecto base he usado el que ya creé en el post sobre Spring Boot, lo podéis descargar de github, aquí.

Entorno usado:
Java JDK 1.8
Maven 3.2.1
Git 1.9.4
IDE Intellij 13.1.5 Ultimate version
Apache Tomcat 8.0.14 (64bits)

Pasos:

1. Lo primero, editaremos nuestro pom.xml de maven, para poner como tipo de «empaquetado», war (por defecto está en jar).

    ...
    <packaging>war</packaging>
    ...

2. Modificaremos nuestra clase Application, desde donde se arranca toda la «magia» de Spring Boot, y haremos que herede de SpringBootServletInitializer. Sobreescribiremos el método ‘configure‘ de esa clase, quedando nuestra Application.java tal que así:

package com.edwise.springbootseries.war;

// imports...

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }
}

Esta clase SpringBootServletInitializer es cargada al inicio, y da información al contenedor sobre nuestra aplicación (viene a ser una especie de web.xml). Con Spring Boot solo es necesario usarla si queremos precisamente lo que buscamos en este post: generar un war para desplegarlo en un contenedor aparte.

No es necesario que sea nuestra clase «base» de Spring Boot (Application.java), la que herede de está clase. Podríamos crear una clase nueva solo para heredar de SpringBootServletInitializer, si preferimos tenerlo organizado de otra manera. Lo que si es necesario, por supuesto, es que el parámetro que pasemos, dentro del método ‘configure’, al método ‘application.sources’ siga siendo Application.class.

3. Ejecutamos el comando maven ‘mvn clean package’ desde una consola o desde la tool window de maven en intellij, lo que nos generará nuestro war.

4. Si no tenemos tomcat instalado, lo descargamos directamente de su web. Descomprimimos el zip descargado donde queramos, y, para arrancar, ejecutamos el script ‘bin\startup.bat‘. Copiamos nuestro war generado (estará directamente en la carpeta ‘target‘ de nuestro proyecto) en la subcarpeta ‘webapps’ de tomcat. Se desplegará automáticamente.

5. Accediendo directamente (con el navegador o un cliente REST) a la url http://localhost:8080/springbootseries-war-1.0/ , veremos que nuestro controller responde correctamente.

De esta manera tendríamos ya un proyecto Spring Boot que fácilmente podemos generar como war y llevarnos a nuestro contenedor web de turno. Podéis bajaros el proyecto completo en mi github.

Cualquier duda o sugerencia, espero vuestros comentarios 🙂