
En Grubhub, usamos Java en casi todo el backend. Este es un lenguaje comprobado que ha demostrado su velocidad y confiabilidad en los últimos 20 años. Pero a lo largo de los años, la edad del "viejo" todavía comenzó a afectar.
Java es
uno de los lenguajes JVM más populares , pero no el único. En los últimos años, ha estado compitiendo con Scala, Clojure y Kotlin, que proporcionan nuevas funcionalidades y funciones de lenguaje optimizadas. En resumen, le permiten hacer más con un código más conciso.
Estas innovaciones en el ecosistema JVM son muy interesantes. Debido a la competencia, Java se ve obligado a cambiar para seguir siendo competitivo. El nuevo calendario de lanzamiento de seis meses y varias JEP (propuestas de mejora de JDK) en Java 8 (Valhalla, inferencia de tipo variable local, Loom) son prueba de que Java seguirá siendo un lenguaje competitivo durante años.
Sin embargo, el tamaño y la escala de Java significan que el desarrollo progresa más lentamente de lo que quisiéramos, sin mencionar el fuerte deseo de mantener la compatibilidad con versiones anteriores a toda costa. En cualquier desarrollo, la primera prioridad deben ser las funciones, pero aquí las funciones necesarias se han desarrollado durante demasiado tiempo, si es que lo han hecho, en el lenguaje. Por lo tanto, en Grubhub usamos Project Lombok para tener Java optimizado y mejorado a nuestra disposición en este momento. El proyecto Lombok es un complemento compilador que agrega nuevas "palabras clave" a Java y convierte las anotaciones en código Java, lo que reduce el esfuerzo de desarrollo y proporciona algunas funcionalidades adicionales.
Configurar Lombok
Grubhub siempre se esfuerza por mejorar el ciclo de vida del software, pero cada nueva herramienta y proceso tiene un costo que considerar. Afortunadamente, para conectar Lombok, solo agregue un par de líneas al archivo gradle.
Lombok convierte las anotaciones en el código fuente en declaraciones Java antes de que el compilador las procese: la dependencia de
lombok
no
lombok
en tiempo de ejecución, por lo que el uso del complemento no aumentará el tamaño del ensamblaje. Para
configurar Lombok con Gradle (también funciona con Maven), simplemente agregue las
siguientes líneas al archivo
build.gradle :
plugins { id 'io.franzbecker.gradle-lombok' version '1.14' id 'java' } repositories { jcenter()
Con Lombok, nuestro código fuente
no será un código Java válido. Por lo tanto, deberá instalar un complemento para el IDE; de lo contrario, el entorno de desarrollo no comprenderá de qué se trata. Lombok es compatible con todos los principales IDE de Java. Integración perfecta. Todas las funciones como "mostrar uso" e "ir a implementación" continúan funcionando como antes, moviéndolo al campo / clase correspondiente.
Lombok en acción
La mejor manera de conocer Lombok es verlo en acción. Considere algunos ejemplos típicos.
Revive el objeto POJO
Con los "viejos objetos Java" (POJO), separamos los datos del procesamiento para facilitar la lectura del código y agilizar las transferencias de red. Un POJO simple tiene varios campos privados, así como getters y setters correspondientes. Hacen el trabajo, pero requieren mucho código repetitivo.
Lombok ayuda a usar POJO de una manera más flexible y estructurada sin código adicional. Así es
@Data
simplificamos el POJO subyacente con la anotación
@Data
:
@Data public class User { private UUID userId; private String email; }
@Data
es solo una anotación conveniente que aplica múltiples anotaciones de Lombok a la vez.
@ToString
genera una implementación para el método toString()
, que consiste en una representación ordenada del objeto: el nombre de la clase, todos los campos y sus valores.
@EqualsAndHashCode
genera implementaciones de equals
y hashCode
, que por defecto usan campos no estáticos y no estacionarios, pero son personalizables.
@Getter / @Setter
genera @Getter / @Setter
y establecedores para campos privados.
@RequiredArgsConstructor
crea un constructor con los argumentos requeridos, donde se requieren los campos finales y los campos con la anotación @NonNull
(más sobre esto a continuación).
Esta anotación sola cubre de manera simple y elegante muchos casos de uso típicos. Pero POJO no siempre cubre la funcionalidad necesaria.
@Data
es una clase totalmente modificable, cuyo abuso puede aumentar la complejidad y limitar la concurrencia, lo que afecta negativamente la capacidad de supervivencia de la aplicación.
Hay otra solucion. Volvamos a nuestra clase de
User
, hagámosla inmutable y agreguemos algunas otras anotaciones útiles.
@Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; }
La anotación
@Value
similar a
@Data
excepto que todos los campos son privados y finales de forma predeterminada, y no se crean setters. Gracias a esto, los objetos
@Value
se vuelven inmediatamente inmutables. Como todos los campos son finales, no hay constructor de argumentos. En cambio, Lombok usa el
@AllArgsConstructor
. El resultado es un objeto completamente funcional e inmutable.
Pero la inmutabilidad no es muy útil si solo necesita crear un objeto usando el constructor all-args. Como explica Joshua Bloch en su libro Programación efectiva de Java, debe usar constructores si tiene una gran cantidad de parámetros de diseño. Aquí la clase
@Builder
entra en
@Builder
, generando automáticamente la clase interna del constructor:
User user = User.builder() .userId(UUID.random()) .email(“grubhub@grubhub.com”) .favoriteFood(“burritos”) .favoriteFood(“dosas”) .build()
La generación de un generador facilita la creación de objetos con una gran cantidad de argumentos y la adición de nuevos campos en el futuro. El método estático devuelve una instancia del constructor para establecer todas las propiedades del objeto. Después de eso, la llamada a
build()
devuelve la instancia.
La
@NonNull
se puede usar para
@NonNull
que estos campos no son nulos al crear una instancia del objeto; de lo contrario, se
NullPointerException
una
NullPointerException
. Tenga en cuenta que el campo de avatar se anota con
@NonNull
pero no se establece. El hecho es que la anotación
@Builder.Default
apunta a
default.png por defecto.
Observe también cómo el constructor usa
favoriteFood
, el único nombre de propiedad en nuestro objeto. Al colocar anotaciones
@Singular
en una propiedad de colección, Lombok crea métodos especiales de creación para agregar elementos a la colección individualmente y no para agregar la colección completa al mismo tiempo. Esto es especialmente bueno para las pruebas, porque las formas de crear pequeñas colecciones en Java no pueden llamarse simples y rápidas.
Finalmente, el parámetro
toBuilder = true
agrega el método de instancia
toBuilder()
, que crea un objeto generador lleno de todos los valores de esta instancia. Es muy fácil crear una nueva instancia, rellenada previamente con todos los valores del original, de modo que solo queda cambiar los campos necesarios. Esto es especialmente útil para
@Value
clases
@Value
, porque los campos son inmutables.
Algunas notas personalizan aún más las funciones especiales del setter.
@Wither
crea métodos con
@Wither
para cada propiedad. En la entrada, el valor; en la salida, el clon de la instancia con el valor actualizado de un campo.
@Accessors
permite configurar setters creados automáticamente. El parámetro fluent
fluent=true
deshabilita las convenciones get y set para getters y setters. En ciertas situaciones, esto puede ser un reemplazo útil para
@Builder
.
Si la implementación de Lombok no es adecuada para su tarea (y miró los modificadores de anotación), siempre puede tomar y escribir su propia implementación. Por ejemplo, si tiene la clase
@Data
, pero un getter necesita una lógica personalizada, simplemente implemente este getter. Lombok verá que la implementación ya está provista y no la sobrescribirá con la implementación creada automáticamente.
Con solo unas pocas anotaciones simples, el POJO base ha recibido tantas funciones ricas que simplifican su uso sin cargar el trabajo de los ingenieros, sin perder tiempo ni aumentar los costos de desarrollo.
Eliminar código de plantilla
Lombok es útil no solo para POJO: se puede aplicar en cualquier nivel de la aplicación. Los siguientes usos de Lombok son especialmente útiles en clases de componentes como controladores, servicios y DAO (objetos de acceso a datos).
El registro es un requisito básico para todas las partes del programa. Cualquier clase que haga un trabajo significativo debe escribir un registro. Por lo tanto, el registrador estándar se convierte en una plantilla para cada clase. Lombok simplifica esta plantilla en una sola anotación que identifica e instancia automáticamente un registrador con el nombre de clase correcto. Existen varias anotaciones diferentes según la estructura de la revista.
@Slf4j
Después de declarar el registrador, agregue nuestras dependencias:
@Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; }
La
@FieldDefaults
agrega modificadores finales y privados a todos los campos.
@RequiredArgsConstructor
crea un constructor que configura una instancia de
UserDao
. La
@NonNull
agrega validación en el constructor y
UserDao
NullPointerException
si la instancia de
UserDao
es cero.
¡Pero espera, eso no es todo!
Hay muchas más situaciones en las que Lombok hace su mejor esfuerzo. Las secciones anteriores mostraron ejemplos específicos, pero Lombok puede facilitar el desarrollo en muchas áreas. Aquí hay algunos pequeños ejemplos de cómo usarlo de manera más eficiente.
Aunque la palabra clave
var
apareció en Java 9, la variable aún puede reasignarse. Lombok tiene la palabra clave
val
, que imprime el tipo final de una variable local.
Algunas clases con funciones puramente estáticas no están destinadas a ser inicializadas. Una forma de evitar la creación de instancias es declarar un constructor privado que arroje una excepción. Lombok codificó esta plantilla en la anotación
@UtilityClass
. Genera un constructor privado que arroja una excepción, finalmente genera la clase y hace que todos los métodos sean estáticos.
@UtilityClass
Java a menudo es criticado por su verbosidad debido a las excepciones marcadas. Una anotación separada de Lombok los arregla:
@SneakyThrows
. Como se esperaba, la implementación es bastante complicada. No captura excepciones ni siquiera ajusta excepciones en una
RuntimeException
. En cambio, se basa en el hecho de que la JVM no verifica la consistencia de las excepciones verificadas en tiempo de ejecución. Solo Java hace esto. Por lo tanto, Lombok utiliza la conversión de bytecode en tiempo de compilación para deshabilitar esta comprobación. El resultado es un código ejecutable.
public class SneakyThrows { @SneakyThrows public void sneakyThrow() { throw new Exception(); } }
Comparación lado a lado
Las comparaciones directas muestran mejor cuánto ahorra Lombok. El complemento IDE tiene una función "de-lombok" que convierte aproximadamente la mayoría de las anotaciones de Lombok a código Java nativo (
@NonNull
anotaciones
@NonNull
no se convierten). Por lo tanto, cualquier IDE con el complemento instalado podrá convertir la mayoría de las anotaciones en código nativo de Java y viceversa. Volver a nuestra clase de
User
.
@Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; }
La clase Lombok es solo 13 líneas simples, legibles y comprensibles. ¡Pero después de ejecutar de-lombok, la clase se convierte en más de cien líneas de código repetitivo!
public class User { @NonNull UUID userId; @NonNull String email; Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = "default.png"; @java.beans.ConstructorProperties({"userId", "email", "favoriteFoods", "avatar"}) User(UUID userId, String email, Set<String> favoriteFoods, String avatar) { this.userId = userId; this.email = email; this.favoriteFoods = favoriteFoods; this.avatar = avatar; } public static UserBuilder builder() { return new UserBuilder(); } @NonNull public UUID getUserId() { return this.userId; } @NonNull public String getEmail() { return this.email; } public Set<String> getFavoriteFoods() { return this.favoriteFoods; } @NonNull public String getAvatar() { return this.avatar; } public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof User)) return false; final User other = (User) o; final Object this$userId = this.getUserId(); final Object other$userId = other.getUserId(); if (this$userId == null ? other$userId != null : !this$userId.equals(other$userId)) return false; final Object this$email = this.getEmail(); final Object other$email = other.getEmail(); if (this$email == null ? other$email != null : !this$email.equals(other$email)) return false; final Object this$favoriteFoods = this.getFavoriteFoods(); final Object other$favoriteFoods = other.getFavoriteFoods(); if (this$favoriteFoods == null ? other$favoriteFoods != null : !this$favoriteFoods.equals(other$favoriteFoods)) return false; final Object this$avatar = this.getAvatar(); final Object other$avatar = other.getAvatar(); if (this$avatar == null ? other$avatar != null : !this$avatar.equals(other$avatar)) return false; return true; } public int hashCode() { final int PRIME = 59; int result = 1; final Object $userId = this.getUserId(); result = result * PRIME + ($userId == null ? 43 : $userId.hashCode()); final Object $email = this.getEmail(); result = result * PRIME + ($email == null ? 43 : $email.hashCode()); final Object $favoriteFoods = this.getFavoriteFoods(); result = result * PRIME + ($favoriteFoods == null ? 43 : $favoriteFoods.hashCode()); final Object $avatar = this.getAvatar(); result = result * PRIME + ($avatar == null ? 43 : $avatar.hashCode()); return result; } public String toString() { return "User(userId=" + this.getUserId() + ", email=" + this.getEmail() + ", favoriteFoods=" + this.getFavoriteFoods() + ", avatar=" + this.getAvatar() + ")"; } public UserBuilder toBuilder() { return new UserBuilder().userId(this.userId).email(this.email).favoriteFoods(this.favoriteFoods).avatar(this.avatar); } public static class UserBuilder { private UUID userId; private String email; private ArrayList<String> favoriteFoods; private String avatar; UserBuilder() { } public User.UserBuilder userId(UUID userId) { this.userId = userId; return this; } public User.UserBuilder email(String email) { this.email = email; return this; } public User.UserBuilder favoriteFood(String favoriteFood) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.add(favoriteFood); return this; } public User.UserBuilder favoriteFoods(Collection<? extends String> favoriteFoods) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.addAll(favoriteFoods); return this; } public User.UserBuilder clearFavoriteFoods() { if (this.favoriteFoods != null) this.favoriteFoods.clear(); return this; } public User.UserBuilder avatar(String avatar) { this.avatar = avatar; return this; } public User build() { Set<String> favoriteFoods; switch (this.favoriteFoods == null ? 0 : this.favoriteFoods.size()) { case 0: favoriteFoods = java.util.Collections.emptySet(); break; case 1: favoriteFoods = java.util.Collections.singleton(this.favoriteFoods.get(0)); break; default: favoriteFoods = new java.util.LinkedHashSet<String>(this.favoriteFoods.size() < 1073741824 ? 1 + this.favoriteFoods.size() + (this.favoriteFoods.size() - 3) / 3 : Integer.MAX_VALUE); favoriteFoods.addAll(this.favoriteFoods); favoriteFoods = java.util.Collections.unmodifiableSet(favoriteFoods); } return new User(userId, email, favoriteFoods, avatar); } public String toString() { return "User.UserBuilder(userId=" + this.userId + ", email=" + this.email + ", favoriteFoods=" + this.favoriteFoods + ", avatar=" + this.avatar + ")"; } } }
Haremos lo mismo para la clase
UserService
.
@Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; }
Aquí hay una contraparte de muestra en código Java estándar.
public class UserService { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class); private final UserDao userDao; @java.beans.ConstructorProperties({"userDao"}) public UserService(UserDao userDao) { if (userDao == null) { throw new NullPointerException("userDao is marked @NonNull but is null") } this.userDao = userDao; } }
Calificación de efecto
Grubhub cuenta con más de un centenar de servicios empresariales de entrega de alimentos. Tomamos uno de ellos y lanzamos la función "de-lombok" en el complemento Lombok IntelliJ. Como resultado, cambiaron unos 180 archivos y la base de código creció en aproximadamente 18,000 líneas de código después de eliminar 800 casos de uso de Lombok. En promedio, cada línea de Lombok ahorra 23 líneas de Java. Con este efecto, es difícil imaginar Java sin Lombok.
Resumen
Lombok es un gran ayudante que implementa nuevas funciones de lenguaje sin requerir mucho esfuerzo por parte del desarrollador. Por supuesto, es más fácil instalar el complemento que capacitar a todos los ingenieros en un nuevo idioma y portar el código existente. Lombok no es omnipotente, pero fuera de la caja es lo suficientemente poderoso como para ayudar realmente en el trabajo.
Otra ventaja de Lombok es que mantiene bases de código consistentes. Tenemos más de cien servicios diferentes y un equipo distribuido en todo el mundo, por lo que la coherencia de las bases de código facilita la ampliación de los equipos y reduce la carga de cambiar de contexto al comenzar un nuevo proyecto. Lombok funciona para cualquier versión desde Java 6, por lo que podemos contar con su disponibilidad en todos los proyectos.
Para Grubhub, esto es más que solo nuevas características. Al final, todo este código
puede escribirse manualmente. Pero Lombok simplifica las partes aburridas de la base de código sin afectar la lógica de negocios. Esto le permite centrarse en cosas que son realmente importantes para el negocio y las más interesantes para nuestros desarrolladores. El código de plantilla de Monton es una pérdida de tiempo para programadores, revisores y mantenedores. Además, dado que este código ya no se escribe manualmente, elimina clases enteras de errores tipográficos. ¡Los beneficios de la
@NonNull
automática combinados con el poder de
@NonNull
reducen la probabilidad de errores y ayudan a nuestro desarrollo, que tiene como objetivo entregar alimentos a su mesa!