1. Evite el código repetitivo
Java es un gran lenguaje, pero a veces se vuelve demasiado detallado para las cosas que tiene que hacer en su código para tareas comunes o para cumplir con algunas prácticas de marco. Muy a menudo, estos no aportan ningún valor real al lado comercial de sus programas, y aquí es donde Lombok está aquí para hacer su vida más feliz y usted más productivo.
La forma en que funciona es conectándose a su proceso de compilación y autogenerando el código de bytes de Java en sus archivos .class según una serie de anotaciones de proyecto que introduzca en su código.
Incluirlo en sus compilaciones, sea cual sea el sistema que esté utilizando, es muy sencillo. La página de su proyecto tiene instrucciones detalladas sobre los detalles. La mayoría de mis proyectos se basan en maven, por lo que normalmente elimino su dependencia en el alcance proporcionado y estoy listo para comenzar:
... org.projectlombok lombok 1.18.10 provided ...
Consulte aquí la versión más reciente disponible.
Tenga en cuenta que depender de Lombok no hará que los usuarios de sus .jar dependan de él también, ya que es una dependencia de compilación pura, no un tiempo de ejecución.
2. Getters / Setters, constructores: tan repetitivos
Encapsular propiedades de objetos a través de métodos públicos getter y setter es una práctica muy común en el mundo Java, y muchos frameworks confían en este patrón "Java Bean" ampliamente: una clase con un constructor vacío y métodos get / set para "propiedades".
Esto es tan común que la mayoría de los IDE admiten la generación automática de código para estos patrones (y más). Sin embargo, este código debe permanecer en sus fuentes y también debe mantenerse cuando, por ejemplo, se agrega una nueva propiedad o se cambia el nombre de un campo.
Consideremos esta clase que queremos usar como una entidad JPA como ejemplo:
@Entity public class User implements Serializable { private @Id Long id; // will be set when persisting private String firstName; private String lastName; private int age; public User() { } public User(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } // getters and setters: ~30 extra lines of code }
Esta es una clase bastante simple, pero aún considere que si agregamos el código adicional para los captadores y definidores, terminaríamos con una definición en la que tendríamos más código estándar de valor cero que la información comercial relevante: "un usuario tiene primero y apellidos y edad ".
Vamos ahora a Lombok-ize esta clase:
@Entity @Getter @Setter @NoArgsConstructor // <--- THIS is it public class User implements Serializable { private @Id Long id; // will be set when persisting private String firstName; private String lastName; private int age; public User(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } }
Al agregar las anotaciones @Getter y @Setter , le dijimos a Lombok que, bueno, las generara para todos los campos de la clase. @NoArgsConstructor conducirá a una generación de constructor vacía.
Tenga en cuenta que este es el código completo de la clase, no estoy omitiendo nada a diferencia de la versión anterior con el comentario // getters y setters . Para una clase de tres atributos relevantes, ¡esto es un ahorro significativo en código!
Si agrega más atributos (propiedades) a su clase de Usuario , sucederá lo mismo: aplicó las anotaciones al tipo en sí para que se fijen en todos los campos por defecto.
¿Y si quisiera refinar la visibilidad de algunas propiedades? Por ejemplo, me gusta mantener visible el paquete de modificadores de campo de identificación de mis entidades o protegido porque se espera que se lean pero no se establezcan explícitamente por el código de la aplicación. Simplemente use un @Setter más detallado para este campo en particular:
private @Id @Setter(AccessLevel.PROTECTED) Long id;
3. Getter perezoso
A menudo, las aplicaciones necesitan realizar una operación costosa y guardar los resultados para su uso posterior.
Por ejemplo, digamos que necesitamos leer datos estáticos de un archivo o una base de datos. En general, es una buena práctica recuperar estos datos una vez y luego almacenarlos en caché para permitir lecturas en memoria dentro de la aplicación. Esto evita que la aplicación repita la costosa operación.
Otro patrón común es recuperar estos datos solo cuando se necesitan por primera vez . En otras palabras, solo obtenga los datos cuando se llame por primera vez al getter correspondiente. Esto se llama carga diferida .
Suponga que estos datos se almacenan en caché como un campo dentro de una clase. La clase ahora debe asegurarse de que cualquier acceso a este campo devuelva los datos almacenados en caché. Una forma posible de implementar dicha clase es hacer que el método getter recupere los datos solo si el campo es nulo . Por esta razón, llamamos a esto un captador perezoso .
Lombok hace esto posible con el parámetro lazy en la anotación @ Getter que vimos arriba.
Por ejemplo, considere esta clase simple:
public class GetterLazy { @Getter(lazy = true) private final Map transactions = getTransactions(); private Map getTransactions() { final Map cache = new HashMap(); List txnRows = readTxnListFromFile(); txnRows.forEach(s -> { String[] txnIdValueTuple = s.split(DELIMETER); cache.put(txnIdValueTuple[0], Long.parseLong(txnIdValueTuple[1])); }); return cache; } }
Esto lee algunas transacciones de un archivo a un mapa . Como los datos del archivo no cambian, los almacenaremos en caché una vez y permitiremos el acceso a través de un captador.
Si ahora miramos el código compilado de esta clase, veremos un método getter que actualiza el caché si era nulo y luego devuelve los datos en caché :
public class GetterLazy { private final AtomicReference transactions = new AtomicReference(); public GetterLazy() { } //other methods public Map getTransactions() { Object value = this.transactions.get(); if (value == null) { synchronized(this.transactions) { value = this.transactions.get(); if (value == null) { Map actualValue = this.readTxnsFromFile(); value = actualValue == null ? this.transactions : actualValue; this.transactions.set(value); } } } return (Map)((Map)(value == this.transactions ? null : value)); } }
Es interesante señalar que Lombok envolvió el campo de datos en una AtomicReference. Esto asegura actualizaciones atómicas en el campo de transacciones . El método getTransactions () también se asegura de leer el archivo si las transacciones son nulas.
Se desaconseja el uso del campo de transacciones AtomicReference directamente desde dentro de la clase. Se recomienda utilizar el método getTransactions () para acceder al campo.
Por esta razón, si usamos otra anotación de Lombok como ToString en la misma clase , usará getTransactions () en lugar de acceder directamente al campo.
4. Clases de valor / DTO
Hay muchas situaciones en las que queremos definir un tipo de datos con el único propósito de representar “valores” complejos o como “Objetos de transferencia de datos”, la mayoría de las veces en forma de estructuras de datos inmutables que construimos una vez y nunca queremos cambiar. .
Diseñamos una clase para representar una operación de inicio de sesión exitosa. Queremos que todos los campos no sean nulos y que los objetos sean inmutables para que podamos acceder de forma segura a sus propiedades:
public class LoginResult { private final Instant loginTs; private final String authToken; private final Duration tokenValidity; private final URL tokenRefreshUrl; // constructor taking every field and checking nulls // read-only accessor, not necessarily as get*() form }
Nuevamente, la cantidad de código que tendríamos que escribir para las secciones comentadas sería de un volumen mucho mayor que la información que queremos encapsular y que tiene un valor real para nosotros. Podemos usar Lombok nuevamente para mejorar esto:
@RequiredArgsConstructor @Accessors(fluent = true) @Getter public class LoginResult { private final @NonNull Instant loginTs; private final @NonNull String authToken; private final @NonNull Duration tokenValidity; private final @NonNull URL tokenRefreshUrl; }
Simplemente agregue la anotación @RequiredArgsConstructor y obtendrá un constructor para todos los campos finales de la clase, tal como los declaró. Agregar @NonNull a los atributos hace que nuestro constructor verifique la nulabilidad y arroje NullPointerExceptions en consecuencia. Esto también sucedería si los campos no fueran definitivos y añadiéramos @Setter para ellos.
Don't you want boring old get*() form for your properties? Because we added @Accessors(fluent=true) in this example “getters” would have the same method name as the properties: getAuthToken() simply becomes authToken().
This “fluent” form would apply to non-final fields for attribute setters and as well allow for chained calls:
// Imagine fields were no longer final now return new LoginResult() .loginTs(Instant.now()) .authToken("asdasd") . // and so on
5. Core Java Boilerplate
Another situation in which we end up writing code we need to maintain is when generating toString(), equals() and hashCode() methods. IDEs try to help with templates for autogenerating these in terms of our class attributes.
We can automate this by means of other Lombok class-level annotations:
- @ToString: will generate a toString() method including all class attributes. No need to write one ourselves and maintain it as we enrich our data model.
- @EqualsAndHashCode: will generate both equals() and hashCode() methods by default considering all relevant fields, and according to very well though semantics.
These generators ship very handy configuration options. For example, if your annotated classes take part of a hierarchy you can just use the callSuper=true parameter and parent results will be considered when generating the method's code.
More on this: say we had our User JPA entity example include a reference to events associated to this user:
@OneToMany(mappedBy = "user") private List events;
We wouldn't like to have the whole list of events dumped whenever we call the toString() method of our User, just because we used the @ToString annotation. No problem: just parameterize it like this: @ToString(exclude = {“events”}), and that won't happen. This is also helpful to avoid circular references if, for example, UserEvents had a reference to a User.
For the LoginResult example, we may want to define equality and hash code calculation just in terms of the token itself and not the other final attributes in our class. Then, simply write something like @EqualsAndHashCode(of = {“authToken”}).
Bonus: if you liked the features from the annotations we've reviewed so far you may want to examine @Data and @Value annotations as they behave as if a set of them had been applied to our classes. After all, these discussed usages are very commonly put together in many cases.
5.1. (Not) Using the @EqualsAndHashCode With JPA Entities
Whether to use the default equals() and hashCode() methods or create custom ones for the JPA entities, is an often discussed topic among developers. There are multiple approaches we can follow; each having its pros and cons.
By default, @EqualsAndHashCode includes all non-final properties of the entity class. We can try to “fix” this by using the onlyExplicitlyIncluded attribute of the @EqualsAndHashCode to make Lombok use only the entity's primary key. Still, however, the generated equals() method can cause some issues. Thorben Janssen explains this scenario in greater detail in one of his blog posts.
In general, we should avoid using Lombok to generate the equals() and hashCode() methods for our JPA entities!
6. The Builder Pattern
The following could make for a sample configuration class for a REST API client:
public class ApiClientConfiguration { private String host; private int port; private boolean useHttps; private long connectTimeout; private long readTimeout; private String username; private String password; // Whatever other options you may thing. // Empty constructor? All combinations? // getters... and setters? }
We could have an initial approach based on using the class default empty constructor and providing setter methods for every field. However, we'd ideally want configurations not to be re-set once they've been built (instantiated), effectively making them immutable. We therefore want to avoid setters, but writing such a potentially long args constructor is an anti-pattern.
Instead, we can tell the tool to generate a builder pattern, preventing us to write an extra Builder class and associated fluent setter-like methods by simply adding the @Builder annotation to our ApiClientConfiguration.
@Builder public class ApiClientConfiguration { // ... everything else remains the same }
Leaving the class definition above as such (no declare constructors nor setters + @Builder) we can end up using it as:
ApiClientConfiguration config = ApiClientConfiguration.builder() .host("api.server.com") .port(443) .useHttps(true) .connectTimeout(15_000L) .readTimeout(5_000L) .username("myusername") .password("secret") .build();
7. Checked Exceptions Burden
Lots of Java APIs are designed so that they can throw a number of checked exceptions client code is forced to either catch or declare to throws. How many times have you turned these exceptions you know won't happen into something like this?
public String resourceAsString() { try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) { BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); return br.lines().collect(Collectors.joining("\n")); } catch (IOException | UnsupportedCharsetException ex) { // If this ever happens, then its a bug. throw new RuntimeException(ex); <--- encapsulate into a Runtime ex. } }
If you want to avoid this code patterns because the compiler won't be otherwise happy (and, after all, you know the checked errors cannot happen), use the aptly named @SneakyThrows:
@SneakyThrows public String resourceAsString() { try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) { BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); return br.lines().collect(Collectors.joining("\n")); } }
8. Ensure Your Resources Are Released
Java 7 introduced the try-with-resources block to ensure your resources held by instances of anything implementing java.lang.AutoCloseable are released when exiting.
Lombok provides an alternative way of achieving this, and more flexibly via @Cleanup. Use it for any local variable whose resources you want to make sure are released. No need for them to implement any particular interface, you'll just get its close() method called.
@Cleanup InputStream is = this.getClass().getResourceAsStream("res.txt");
Your releasing method has a different name? No problem, just customize the annotation:
@Cleanup("dispose") JFrame mainFrame = new JFrame("Main Window");
9. Annotate Your Class to Get a Logger
Many of us add logging statements to our code sparingly by creating an instance of a Logger from our framework of choice. Say, SLF4J:
public class ApiClientConfiguration { private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class); // LOG.debug(), LOG.info(), ... }
This is such a common pattern that Lombok developers have cared to simplify it for us:
@Slf4j // or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j public class ApiClientConfiguration { // log.debug(), log.info(), ... }
Many logging frameworks are supported and of course you can customize the instance name, topic, etc.
10. Write Thread-Safer Methods
In Java you can use the synchronized keyword to implement critical sections. However, this is not a 100% safe approach: other client code can eventually also synchronize on your instance, potentially leading to unexpected deadlocks.
This is where @Synchronized comes in: annotate your methods (both instance and static) with it and you'll get an autogenerated private, unexposed field your implementation will use for locking:
@Synchronized public /* better than: synchronized */ void putValueInCache(String key, Object value) { // whatever here will be thread-safe code }
11. Automate Objects Composition
Java does not have language level constructs to smooth out a “favor composition inheritance” approach. Other languages have built-in concepts such as Traits or Mixins to achieve this.
Lombok's @Delegate comes in very handy when you want to use this programming pattern. Let's consider an example:
- We want Users and Customers to share some common attributes for naming and phone number
- We define both an interface and an adapter class for these fields
- We'll have our models implement the interface and @Delegate to their adapter, effectively composing them with our contact information
First, let's define an interface:
public interface HasContactInformation { String getFirstName(); void setFirstName(String firstName); String getFullName(); String getLastName(); void setLastName(String lastName); String getPhoneNr(); void setPhoneNr(String phoneNr); }
And now an adapter as a support class:
@Data public class ContactInformationSupport implements HasContactInformation { private String firstName; private String lastName; private String phoneNr; @Override public String getFullName() { return getFirstName() + " " + getLastName(); } }
The interesting part comes now, see how easy it is to now compose contact information into both model classes:
public class User implements HasContactInformation { // Whichever other User-specific attributes @Delegate(types = {HasContactInformation.class}) private final ContactInformationSupport contactInformation = new ContactInformationSupport(); // User itself will implement all contact information by delegation }
The case for Customer would be so similar we'd omit the sample for brevity.
12. Rolling Lombok Back?
Short answer: Not at all really.
You may be worried there is a chance that you use Lombok in one of your projects, but later want to rollback that decision. You'd then have a maybe large number of classes annotated for it… what could you do?
I have never really regretted this, but who knows for you, your team or your organization. For these cases you're covered thanks to the delombok tool from the same project.
By delombok-ing your code you'd get autogenerated Java source code with exactly the same features from the bytecode Lombok built. So then you may simply replace your original annotated code with these new delomboked files and no longer depend on it.
This is something you can integrate in your build and I have done this in the past to just study the generated code or to integrate Lombok with some other Java source code based tool.
13. Conclusion
There are some other features we have not presented in this article, I'd encourage you to take a deeper dive into the feature overview for more details and use cases.
Además, la mayoría de las funciones que hemos mostrado tienen una serie de opciones de personalización que puede resultarle útil para que la herramienta genere las cosas más compatibles con las prácticas de su equipo para nombrar, etc. El sistema de configuración integrado disponible también podría ayudarlo con eso.
Espero que haya encontrado la motivación para darle a Lombok la oportunidad de ingresar a su conjunto de herramientas de desarrollo Java. Pruébelo y aumente su productividad.
El código de ejemplo se puede encontrar en el proyecto GitHub.