Escriba código Java sin grasa con Project Lombok
Publicado: 2022-03-11Hay una serie de herramientas y bibliotecas sin las que no puedo imaginarme escribiendo código Java en estos días. Tradicionalmente, cosas como Google Guava o Joda Time (al menos para la era anterior a Java 8) se encuentran entre las dependencias que termino agregando a mis proyectos la mayor parte del tiempo, independientemente del dominio específico en cuestión.
Lombok seguramente también merece su lugar en mis compilaciones de POM o Gradle, aunque no sea una utilidad típica de biblioteca/marco. Lombok existe desde hace bastante tiempo (lanzado por primera vez en 2009) y ha madurado mucho desde entonces. Sin embargo, siempre sentí que merecía más atención: es una forma increíble de lidiar con la verbosidad natural de Java.
En esta publicación, exploraremos qué hace que Lombok sea una herramienta tan útil.
Java tiene muchas cosas a su favor más allá de la propia JVM, que es una pieza de software notable. Java es maduro y eficaz, y la comunidad y el ecosistema que lo rodea son enormes y animados.
Sin embargo, como lenguaje de programación, Java tiene algunas idiosincrasias propias, así como opciones de diseño que pueden hacerlo bastante detallado. Agregue algunas construcciones y patrones de clase que los desarrolladores de Java a menudo necesitamos usar y, con frecuencia, terminamos con muchas líneas de código que brindan poco o ningún valor real, aparte de cumplir con un conjunto de restricciones o convenciones de marco.
Aquí es donde entra en juego Lombok. Nos permite reducir drásticamente la cantidad de código "repetitivo" que necesitamos escribir. Los creadores de Lombok son un par de tipos muy inteligentes y ciertamente tienen gusto por el humor. ¡No te puedes perder esta introducción que hicieron en una conferencia pasada!
Veamos cómo Lombok hace su magia y algunos ejemplos de uso.
Cómo funciona Lombok
Lombok actúa como un procesador de anotaciones que "agrega" código a sus clases en tiempo de compilación. El procesamiento de anotaciones es una función agregada al compilador de Java en la versión 5. La idea es que los usuarios puedan colocar procesadores de anotaciones (escritos por ellos mismos o mediante dependencias de terceros, como Lombok) en la ruta de clases de compilación. Luego, a medida que avanza el proceso de compilación, cada vez que el compilador encuentra una anotación, pregunta: "Oye, ¿alguien en el classpath está interesado en esta @Anotación?". Para aquellos procesadores que levantan la mano, el compilador les transfiere el control junto con el contexto de compilación para que, bueno... procesen.
Quizás el caso más común para los procesadores de anotaciones es generar nuevos archivos fuente o realizar algún tipo de verificación en tiempo de compilación.
Lombok realmente no entra en estas categorías: lo que hace es modificar las estructuras de datos del compilador utilizadas para representar el código; es decir, su árbol de sintaxis abstracta (AST). Al modificar el AST del compilador, Lombok está alterando indirectamente la propia generación del código de bytes final.
Este enfoque inusual y bastante intrusivo ha resultado tradicionalmente en que Lombok sea visto como una especie de pirateo. Si bien yo mismo estaría de acuerdo con esta caracterización hasta cierto punto, en lugar de ver esto en el mal sentido de la palabra, vería a Lombok como una "alternativa inteligente, técnicamente meritoria y original".
Aún así, hay desarrolladores que lo consideran un hack y no usan Lombok por este motivo. Eso es comprensible, pero según mi experiencia, los beneficios de productividad de Lombok superan cualquiera de estas preocupaciones. Lo he estado usando felizmente para proyectos de producción durante muchos años.
Antes de entrar en detalles, me gustaría resumir las dos razones por las que valoro especialmente el uso de Lombok en mis proyectos:
- Lombok ayuda a mantener mi código limpio, conciso y directo. Encuentro que mis clases anotadas de Lombok son muy expresivas y, en general, encuentro que el código anotado es bastante revelador de intenciones, aunque no todos en Internet necesariamente están de acuerdo.
- Cuando comienzo un proyecto y pienso en un modelo de dominio, tiendo a comenzar escribiendo clases que son en gran medida un trabajo en progreso y que cambio iterativamente a medida que pienso más y las refino. En estas primeras etapas, Lombok me ayuda a moverme más rápido al no tener que moverme ni transformar el código repetitivo que genera para mí.
Patrón de frijol y métodos de objetos comunes
Muchas de las herramientas y marcos de Java que usamos se basan en Bean Pattern. Los Java Beans son clases serializables que tienen un constructor predeterminado de cero argumentos (y posiblemente otras versiones) y exponen su estado a través de captadores y definidores, normalmente respaldados por campos privados. Escribimos muchos de estos, por ejemplo, cuando trabajamos con JPA o marcos de serialización como JAXB o Jackson.
Considere este bean de usuario que contiene hasta cinco atributos (propiedades), para el cual nos gustaría tener un constructor adicional para todos los atributos, una representación de cadena significativa y definir igualdad/hashing en términos de su campo de correo electrónico:
public class User implements Serializable { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; // Empty constructor implementation: ~3 lines. // Utility constructor for all attributes: ~7 lines. // Getters/setters: ~38 lines. // equals() and hashCode() as per email: ~23 lines. // toString() for all attributes: ~3 lines. // Relevant: 5 lines; Boilerplate: 74 lines => 93% meaningless code :( }
Para abreviar aquí, en lugar de incluir la implementación real de todos los métodos, solo proporcioné comentarios que enumeran los métodos y la cantidad de líneas de código que tomaron las implementaciones reales. ¡Ese código repetitivo habría totalizado más del 90% del código para esta clase!
Además, si luego quisiera, digamos, cambiar el email
a una dirección de correo emailAddress
o hacer que registrationTs
sean una Date
en lugar de un Instant
, entonces necesitaría dedicar tiempo (con la ayuda de mi IDE en algunos casos, es cierto) para cambiar cosas como obtener /establecer nombres y tipos de métodos, modificar mi constructor de utilidades, etc. Nuevamente, tiempo invaluable para algo que no aporta ningún valor comercial práctico a mi código.
Veamos cómo puede ayudar Lombok aquí:
import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString @EqualsAndHashCode(of = {"email"}) public class User { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; }
¡Voila! Acabo de agregar un montón de anotaciones lombok.*
y logré justo lo que quería. La lista anterior es exactamente todo el código que necesito escribir para esto. Lombok se conecta a mi proceso de compilación y genera todo para mí (vea la captura de pantalla a continuación de mi IDE).
Como notará, el inspector de NetBeans (y esto sucederá independientemente del IDE) detecta el código de bytes de la clase compilada, incluidas las adiciones que Lombok incorporó al proceso. Lo que sucedió aquí es bastante sencillo:
- Usando
@Getter
y@Setter
, indiqué a Lombok que generara getters y setters para todos los atributos. Esto se debe a que usé las anotaciones a nivel de clase. Si quisiera especificar selectivamente qué generar para qué atributos, podría haber anotado los campos mismos. - Gracias a
@NoArgsConstructor
y@AllArgsConstructor
, obtuve un constructor vacío predeterminado para mi clase, así como uno adicional para todos los atributos. - La anotación
@ToString
genera automáticamente un práctico métodotoString()
, que muestra de forma predeterminada todos los atributos de clase con el prefijo de su nombre. - Finalmente, para tener el par de métodos
equals()
yhashCode()
definidos en términos del campo de correo electrónico, utilicé@EqualsAndHashCode
y lo parametricé con la lista de campos relevantes (solo el correo electrónico en este caso).
Personalización de las anotaciones de Lombok
Ahora usemos algunas personalizaciones de Lombok siguiendo este mismo ejemplo:
- Me gustaría reducir la visibilidad del constructor predeterminado. Debido a que solo lo necesito por razones de cumplimiento de beans, espero que los consumidores de la clase solo llamen al constructor que toma todos los campos. Para hacer cumplir esto, estoy personalizando el constructor generado con
AccessLevel.PACKAGE
. - Quiero asegurarme de que a mis campos nunca se les asignen valores nulos, ni a través del constructor ni a través de los métodos setter. Anotar atributos de clase con
@NonNull
es suficiente; Lombok generará comprobaciones nulas lanzandoNullPointerException
cuando corresponda en los métodos constructor y setter. - Agregaré un atributo de
password
, pero no quiero que se muestre al llamar atoString()
por razones de seguridad. Esto se logra a través del argumento de exclusiones de@ToString
. - Estoy bien exponiendo el estado públicamente a través de captadores, pero preferiría restringir la mutabilidad externa. Así que dejo
@Getter
como está, pero nuevamente usoAccessLevel.PROTECTED
para@Setter
. - Tal vez me gustaría forzar alguna restricción en el campo de
email
para que, si se modifica, se ejecute algún tipo de verificación. Para esto, solo implemento el métodosetEmail()
yo mismo. Lombok simplemente omitirá la generación de un método que ya existe.
Así es como se verá la clase Usuario:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName; private @NonNull Instant registrationTs; private boolean payingCustomer; protected void setEmail(String email) { // Check for null (=> NullPointerException) // and valid email code (=> IllegalArgumentException) this.email = email; } }
Tenga en cuenta que, para algunas anotaciones, estamos especificando atributos de clase como cadenas simples. No hay problema, porque Lombok arrojará un error de compilación si, por ejemplo, escribimos mal o hacemos referencia a un campo que no existe. Con Lombok, estamos a salvo.
Además, al igual que con el método setEmail()
, Lombok estará bien y no generará nada para un método que el programador ya haya implementado. Esto se aplica a todos los métodos y constructores.
Estructuras de datos inmutables
Otro caso de uso en el que destaca Lombok es al crear estructuras de datos inmutables. Por lo general, se denominan "tipos de valor". Algunos lenguajes tienen soporte incorporado para estos, e incluso hay una propuesta para incorporar esto en futuras versiones de Java.
Supongamos que queremos modelar una respuesta a una acción de inicio de sesión del usuario. Este es el tipo de objeto que nos gustaría instanciar y regresar a otras capas de la aplicación (por ejemplo, para ser JSON serializado como el cuerpo de una respuesta HTTP). Tal LoginResponse no necesitaría ser mutable en absoluto y Lombok puede ayudar a describir esto de manera sucinta. Claro, hay muchos otros casos de uso para estructuras de datos inmutables (son compatibles con subprocesos múltiples y caché, entre otras cualidades), pero sigamos con este ejemplo simple:
import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.experimental.Wither; @Getter @RequiredArgsConstructor @ToString @EqualsAndHashCode public final class LoginResponse { private final long userId; private final @NonNull String authToken; private final @NonNull Instant loginTs; @Wither private final @NonNull Instant tokenExpiryTs; }
Vale la pena señalar aquí:

- Se ha introducido una anotación
@RequiredArgsConstructor
. Acertadamente llamado, lo que hace es generar un constructor para todos los campos finales que aún no se han inicializado. - En los casos en los que queremos reutilizar un LoginResonse emitido anteriormente (imagine, por ejemplo, una operación de "actualizar token"), ciertamente no queremos modificar nuestra instancia existente, sino que queremos generar una nueva basada en ella. . Vea cómo la anotación
@Wither
nos ayuda aquí: le dice a Lombok que genere unwithTokenExpiryTs(Instant tokenExpiryTs)
que crea una nueva instancia de LoginResponse que tiene todos los valores de instancia con, excepto el nuevo que estamos especificando. ¿Le gustaría este comportamiento para todos los campos? Simplemente agregue@Wither
a la declaración de clase en su lugar.
@Datos y @Valor
Ambos casos de uso discutidos hasta ahora son tan comunes que Lombok envía un par de anotaciones para acortarlos aún más: Anotar una clase con @Data
hará que Lombok se comporte como si hubiera sido anotado con @Getter
+ @Setter
+ @ToString
+ @EqualsAndHashCode
+ @RequiredArgsConstructor
. Del mismo modo, el uso de @Value
convertirá su clase en una inmutable (y definitiva), también como si hubiera sido anotada con la lista anterior.
Patrón de constructor
Volviendo a nuestro ejemplo de usuario, si queremos crear una nueva instancia, necesitaremos usar un constructor con hasta seis argumentos. Este ya es un número bastante grande, que empeorará aún más si agregamos más atributos a la clase. Suponga también que deseamos establecer algunos valores predeterminados para los campos lastName
y payingCustomer
que paga.
Lombok implementa una función @Builder
muy poderosa, que nos permite usar un patrón de generador para crear nuevas instancias. Vamos a agregarlo a nuestra clase de usuario:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) @Builder public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName = ""; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }
Ahora podemos crear con fluidez nuevos usuarios como este:
User user = User .builder() .email("[email protected]") .password("secret".getBytes(StandardCharsets.UTF_8)) .firstName("Miguel") .registrationTs(Instant.now()) .build();
Es fácil imaginar lo conveniente que se vuelve esta construcción a medida que crecen nuestras clases.
Delegación/Composición
Si desea seguir la regla muy sensata de "favorecer la composición sobre la herencia", eso es algo con lo que Java realmente no ayuda, en cuanto a la verbosidad. Si desea componer objetos, normalmente necesitará escribir llamadas de método de delegación por todas partes.
Lombok propone una solución para esto a través de @Delegate
. Echemos un vistazo a un ejemplo.
Imagina que queremos introducir un nuevo concepto de ContactInformation
. Esta es información que nuestro User
tiene y es posible que deseemos que otras clases también la tengan. Luego podemos modelar esto a través de una interfaz como esta:
public interface HasContactInformation { String getEmail(); String getFirstName(); String getLastName(); }
Luego introduciríamos una nueva clase de información de ContactInformation
usando Lombok:
import lombok.Data; @Data public class ContactInformation implements HasContactInformation { private String email; private String firstName; private String lastName; }
Y finalmente, podríamos refactorizar User
para componer con Información de ContactInformation
y usar Lombok para generar todas las llamadas de delegación requeridas para que coincidan con el contrato de interfaz:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; import lombok.experimental.Delegate; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"contactInformation"}) public class User implements HasContactInformation { @Getter(AccessLevel.NONE) @Delegate(types = {HasContactInformation.class}) private final ContactInformation contactInformation = new ContactInformation(); private @NonNull byte[] password; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }
Tenga en cuenta que no necesitaba escribir implementaciones para los métodos de HasContactInformation
: esto es algo que le estamos diciendo a Lombok que haga, delegando las llamadas a nuestra instancia de ContactInformation
.
Además, dado que no quiero que se pueda acceder a la instancia delegada desde el exterior, la personalizo con un @Getter(AccessLevel.NONE)
, lo que impide efectivamente la generación de captadores.
Excepciones marcadas
Como todos sabemos, Java diferencia entre excepciones verificadas y no verificadas. Esta es una fuente tradicional de controversia y crítica al lenguaje, ya que el manejo de excepciones a veces se interpone demasiado en nuestro camino como resultado, especialmente cuando se trata de API diseñadas para generar excepciones verificadas y, por lo tanto, nos obliga a los desarrolladores a atraparlas o declarar nuestros métodos para Lánzalos.
Considere este ejemplo:
public class UserService { public URL buildUsersApiUrl() { try { return new URL("https://apiserver.com/users"); } catch (MalformedURLException ex) { // Malformed? Really? throw new RuntimeException(ex); } } }
Este es un patrón tan común: ciertamente sabemos que nuestra URL está bien formada, sin embargo, debido a que el constructor de URL
arroja una excepción verificada, nos vemos obligados a capturarlo o declarar nuestro método para lanzarlo y sacar a las personas que llaman en la misma situación. Envolver estas excepciones marcadas dentro de una RuntimeException
es una práctica muy extendida. Y esto empeora aún más si la cantidad de excepciones verificadas que debemos tratar crece a medida que codificamos.
Entonces, esto es exactamente para lo que @SneakyThrows
de Lombok, envolverá cualquier excepción marcada sujeta a ser lanzada en nuestro método en una sin marcar y nos liberará de la molestia:
import lombok.SneakyThrows; public class UserService { @SneakyThrows public URL buildUsersApiUrl() { return new URL("https://apiserver.com/users"); } }
Inicio sesión
¿Con qué frecuencia agrega instancias de registrador a sus clases de esta manera? (muestra SLF4J)
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
Voy a adivinar bastante. Sabiendo esto, los creadores de Lombok implementaron una anotación que crea una instancia de registrador con un nombre personalizable (log predeterminado), compatible con los marcos de registro más comunes en la plataforma Java. Así (de nuevo, basado en SLF4J):
import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @Slf4j public class UserService { @SneakyThrows public URL buildUsersApiUrl() { log.debug("Building users API URL"); return new URL("https://apiserver.com/users"); } }
Anotar código generado
Si usamos Lombok para generar código, puede parecer que perderíamos la capacidad de anotar esos métodos, ya que en realidad no los estamos escribiendo. Pero esto no es realmente cierto. Más bien, Lombok nos permite decirle cómo nos gustaría que se anotara el código generado, aunque usando una notación un tanto peculiar, la verdad sea dicha.
Considere este ejemplo, dirigido al uso de un marco de inyección de dependencia: tenemos una clase UserService
que usa la inyección de constructor para obtener las referencias a UserRepository
y UserApiClient
.
package com.mgl.toptal.lombok; import javax.inject.Inject; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor(onConstructor = @__(@Inject)) public class UserService { private final UserRepository userRepository; private final UserApiClient userApiClient; // Instead of: // // @Inject // public UserService(UserRepository userRepository, // UserApiClient userApiClient) { // this.userRepository = userRepository; // this.userApiClient = userApiClient; // } }
El ejemplo anterior muestra cómo anotar un constructor generado. Lombok nos permite hacer lo mismo con los métodos y parámetros generados.
Aprendiendo más
El uso de Lombok que se explica en esta publicación se centra en aquellas características que personalmente he encontrado más útiles a lo largo de los años. Sin embargo, hay muchas otras funciones y personalizaciones disponibles.
La documentación de Lombok es muy informativa y completa. Tienen páginas dedicadas para cada función individual (anotación) con explicaciones y ejemplos muy detallados. Si encuentra esta publicación interesante, lo animo a profundizar en lombok y su documentación para obtener más información.
El sitio del proyecto documenta cómo usar Lombok en varios entornos de programación diferentes. En resumen, se admiten los IDE más populares (Eclipse, NetBeans e IntelliJ). Yo mismo cambio regularmente de uno a otro por proyecto y uso Lombok en todos ellos sin problemas.
Delombok!
Delombok es parte de la "cadena de herramientas de Lombok" y puede ser muy útil. Lo que hace es básicamente generar el código fuente de Java para su código anotado de Lombok, realizando las mismas operaciones que hace el código de bytes generado por Lombok.
Esta es una excelente opción para las personas que están considerando adoptar Lombok pero que aún no están seguras. Puede comenzar a usarlo libremente y no habrá "bloqueo de proveedor". En caso de que usted o su equipo luego se arrepientan de la elección, siempre pueden usar delombok para generar el código fuente correspondiente que luego pueden usar sin depender de Lombok.
Delombok también es una gran herramienta para aprender exactamente lo que hará Lombok. Hay maneras muy fáciles de conectarlo a su proceso de construcción.
Alternativas
Hay muchas herramientas dentro del mundo de Java que hacen un uso similar de los procesadores de anotaciones para enriquecer o modificar su código en tiempo de compilación, como Immutables o Google Auto Value. Estos (¡y otros, por supuesto!) se superponen con las características de Lombok. En particular, me gusta mucho el enfoque de Immutables y también lo he usado en algunos proyectos.
También vale la pena señalar que existen otras excelentes herramientas que brindan funciones similares para "mejorar el código de bytes", como Byte Buddy o Javassist. Sin embargo, estos generalmente funcionan en tiempo de ejecución y comprenden un mundo propio más allá del alcance de esta publicación.
Java conciso
Hay una serie de lenguajes orientados a JVM modernos que proporcionan enfoques de diseño más idiomáticos, o incluso de nivel de lenguaje, que ayudan a abordar algunos de los mismos problemas. Seguramente Groovy, Scala y Kotlin son buenos ejemplos. Pero si está trabajando en un proyecto solo de Java, Lombok es una buena herramienta para ayudar a que sus programas sean más concisos, expresivos y fáciles de mantener.