Scrieți cod Java fără grăsimi cu Project Lombok
Publicat: 2022-03-11Există o serie de instrumente și biblioteci fără de care nu mă pot imagina scriind cod Java în zilele noastre. În mod tradițional, lucruri precum Google Guava sau Joda Time (cel puțin pentru era pre Java 8) se numără printre dependențele pe care ajung să le arunc în proiectele mele de cele mai multe ori, indiferent de domeniul specific la îndemână.
Lombok își merită cu siguranță locul în POM-urile mele sau în versiunile Gradle, deși nu este un utilitar tipic de bibliotecă/cadru. Lombok există de ceva vreme acum (lansat pentru prima dată în 2009) și de atunci s-a maturizat foarte mult. Cu toate acestea, am simțit întotdeauna că merită mai multă atenție - este un mod uimitor de a face față verbozității naturale a Java.
În această postare, vom explora ceea ce face din Lombok un instrument atât de util.
Java are multe lucruri pentru el, dincolo de JVM-ul în sine, care este o bucată de software remarcabilă. Java este matur și performant, iar comunitatea și ecosistemul din jurul său sunt uriașe și pline de viață.
Cu toate acestea, ca limbaj de programare, Java are unele idiosincrazii proprii, precum și opțiuni de proiectare care îl pot face destul de pronunțat. Adăugați câteva constructe și modele de clasă pe care dezvoltatorii Java trebuie să le utilizeze adesea și ajungem frecvent cu multe linii de cod care aduc puțină sau deloc valoare reală, în afară de respectarea unor constrângeri sau convenții cadru.
Aici intervine Lombok. Ne permite să reducem drastic cantitatea de cod „boilerplate” pe care trebuie să o scriem. Creatorii lui Lombok sunt câțiva băieți foarte deștepți și, cu siguranță, au un gust pentru umor - nu puteți rata această introducere pe care au făcut-o la o conferință trecută!
Să vedem cum își face magia Lombok și câteva exemple de utilizare.
Cum funcționează Lombok
Lombok acționează ca un procesor de adnotări care „adaugă” cod la clasele tale în timpul compilării. Procesarea adnotărilor este o caracteristică adăugată compilatorului Java la versiunea 5. Ideea este că utilizatorii pot pune procesoare de adnotări (scrise de către sine sau prin dependențe de la terți, cum ar fi Lombok) în calea clasei de construcție. Apoi, pe măsură ce procesul de compilare se desfășoară, ori de câte ori compilatorul găsește o adnotare, într-un fel întreabă: „Hei, cineva din calea clasei este interesat de această @Adnotation?”. Pentru acele procesoare care ridică mâna, compilatorul le transferă apoi controlul împreună cu contextul de compilare pentru ca ei să, ei bine... să proceseze.
Poate că cel mai obișnuit caz pentru procesoarele de adnotare este generarea de noi fișiere sursă sau efectuarea unor verificări la timp de compilare.
Lombok nu se încadrează cu adevărat în aceste categorii: ceea ce face este să modifice structurile de date ale compilatorului utilizate pentru a reprezenta codul; adică arborele său de sintaxă abstractă (AST). Prin modificarea AST al compilatorului, Lombok modifică indirect generarea finală de bytecode în sine.
Această abordare neobișnuită și destul de intruzivă a dus în mod tradițional la ca Lombok să fie privit ca un hack. Deși eu însumi aș fi de acord cu această caracterizare într-o oarecare măsură, mai degrabă decât să văd acest lucru în sensul rău al cuvântului, aș vedea Lombok ca pe o „alternativă inteligentă, meritorie din punct de vedere tehnic și originală”.
Totuși, există dezvoltatori care consideră că este un hack și nu folosesc Lombok din acest motiv. Este de înțeles, dar din experiența mea, beneficiile de productivitate ale Lombok depășesc oricare dintre aceste preocupări. De mulți ani îl folosesc cu plăcere pentru proiecte de producție.
Înainte de a intra în detalii, aș dori să rezum cele două motive pentru care prețuiesc în mod deosebit utilizarea Lombok în proiectele mele:
- Lombok ajută la menținerea codului meu curat, concis și la obiect. Mi se pare foarte expresive clasele mele adnotate Lombok și, în general, găsesc că codul adnotat este destul de revelator de intenții, deși nu toată lumea de pe internet este neapărat de acord.
- Când încep un proiect și mă gândesc la un model de domeniu, tind să încep prin a scrie cursuri care sunt în mare măsură în lucru și pe care le schimb iterativ pe măsură ce mă gândesc și le perfecționez. În aceste etape incipiente, Lombok mă ajută să mă mișc mai repede, fără a fi nevoie să mă mișc sau să transform codul standard pe care îl generează pentru mine.
Modelul de fasole și metodele comune ale obiectelor
Multe dintre instrumentele și cadrele Java pe care le folosim se bazează pe Bean Pattern. Java Bean-urile sunt clase serializabile care au un constructor implicit zero-args (și posibil alte versiuni) și își expun starea prin intermediul getter-urilor și setter-urilor, susținute de obicei de câmpuri private. Scriem multe dintre acestea, de exemplu atunci când lucrăm cu JPA sau cadre de serializare precum JAXB sau Jackson.
Luați în considerare acest User bean care deține până la cinci atribute (proprietăți), pentru care ne-am dori să avem un constructor suplimentar pentru toate atributele, o reprezentare semnificativă a șirurilor și să definim egalitatea/hashing în ceea ce privește câmpul său de e-mail:
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 :( }
Pentru concizie aici, mai degrabă decât să includ implementarea efectivă a tuturor metodelor, am furnizat doar comentarii care enumeră metodele și numărul de linii de cod pe care le-au luat implementările reale. Acest cod standard ar fi însumat mai mult de 90% din codul pentru această clasă!
Mai mult decât atât, dacă mai târziu aș fi vrut, să zicem, să schimb e- email
în emailAddress
de e-mail sau ca registrationTs
să fie o Date
în loc de un Instant
, atunci ar trebui să dedic timp (cu ajutorul IDE-ului meu pentru unele cazuri, desigur) pentru a schimba lucruri precum obținerea /set numele și tipurile metodei, modificați constructorul meu de utilitate și așa mai departe. Din nou, timp neprețuit pentru ceva care nu aduce nicio valoare practică de afaceri codului meu.
Să vedem cum poate ajuta Lombok aici:
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! Tocmai am adăugat o grămadă de adnotări lombok.*
și am realizat exact ceea ce mi-am dorit. Lista de mai sus este exact tot codul pe care trebuie să-l scriu pentru asta. Lombok se conectează la procesul meu de compilare și a generat totul pentru mine (vezi captura de ecran de mai jos a IDE-ului meu).
După cum observați, inspectorul NetBeans (și acest lucru se va întâmpla indiferent de IDE) detectează codul de octet al clasei compilat, inclusiv adăugările aduse de Lombok în proces. Ce s-a întâmplat aici este destul de simplu:
- Folosind
@Getter
și@Setter
, l-am instruit pe Lombok să genereze getters și setters pentru toate atributele. Acest lucru se datorează faptului că am folosit adnotările la nivel de clasă. Dacă aș fi vrut să specific selectiv ce să generez pentru ce atribute, aș fi putut adnota câmpurile în sine. - Datorită
@NoArgsConstructor
și@AllArgsConstructor
, am primit un constructor gol implicit pentru clasa mea, precum și unul suplimentar pentru toate atributele. - Adnotarea
@ToString
generează automat o metodătoString()
la îndemână, arătând implicit toate atributele clasei prefixate de numele lor. - În cele din urmă, pentru a avea perechea de metode
equals()
șihashCode()
definite în ceea ce privește câmpul de e-mail, am folosit@EqualsAndHashCode
și l-am parametrizat cu lista de câmpuri relevante (doar e-mailul în acest caz).
Personalizarea adnotărilor Lombok
Să folosim acum câteva personalizări Lombok urmând același exemplu:
- Aș dori să reduc vizibilitatea constructorului implicit. Deoarece am nevoie doar din motive de conformitate cu fasolea, mă aștept ca consumatorii clasei să apeleze doar constructorul care preia toate câmpurile. Pentru a impune acest lucru, personalizez constructorul generat cu
AccessLevel.PACKAGE
. - Vreau să mă asigur că câmpurile mele nu primesc niciodată valori nule atribuite, nici prin constructor, nici prin metodele setter. Adnotarea atributelor de clasă cu
@NonNull
este suficientă; Lombok va genera verificări nule, aruncândNullPointerException
atunci când este cazul în metodele constructor și setter. - Voi adăuga un atribut de
password
, dar nu vreau să fie afișat atunci cândtoString()
din motive de securitate. Acest lucru se realizează prin argumentul exclude al lui@ToString
. - Sunt de acord să expun starea public prin getters, dar aș prefera să restricționez mutabilitatea exterioară. Așa că
@Getter
așa cum este, dar din nou folosindAccessLevel.PROTECTED
pentru@Setter
. - Poate aș dori să forțez o constrângere pe câmpul de
email
-mail, astfel încât, dacă este modificat, să fie executat un fel de verificare. Pentru aceasta, implementez chiar eu metodasetEmail()
. Lombok va omite doar generarea pentru o metodă care există deja.
Iată cum va arăta apoi clasa User:
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; } }
Rețineți că, pentru unele adnotări, specificăm atributele clasei ca șiruri simple. Nu este o problemă, deoarece Lombok va arunca o eroare de compilare dacă, de exemplu, introducem greșit sau ne referim la un câmp inexistent. Cu Lombok, suntem în siguranță.
De asemenea, la fel ca pentru metoda setEmail()
, Lombok va fi doar OK și nu va genera nimic pentru o metodă pe care programatorul a implementat-o deja. Acest lucru se aplică tuturor metodelor și constructorilor.
Structuri de date imuabile
Un alt caz de utilizare în care Lombok excelează este atunci când se creează structuri de date imuabile. Acestea sunt de obicei denumite „tipuri de valoare”. Unele limbi au suport încorporat pentru acestea și există chiar și o propunere pentru încorporarea acestuia în viitoarele versiuni Java.
Să presupunem că vrem să modelăm un răspuns la o acțiune de conectare a utilizatorului. Acesta este tipul de obiect pe care am dori să-l instanțiem și să revenim la alte straturi ale aplicației (de exemplu, să fie serializat JSON ca corpul unui răspuns HTTP). Un astfel de LoginResponse nu ar trebui să fie deloc mutabil și Lombok vă poate ajuta să descrie acest lucru succint. Sigur, există multe alte cazuri de utilizare pentru structurile de date imuabile (sunt multithreading și cache-friendly, printre alte calități), dar să rămânem la acest exemplu simplu:
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; }
De remarcat aici:

- A fost introdusă o adnotare
@RequiredArgsConstructor
. Numit corect, ceea ce face este să genereze un constructor pentru toate câmpurile finale care nu au fost deja inițializate. - În cazurile în care dorim să reutilizam un LoginResonse emis anterior (imaginați-vă, de exemplu, o operațiune de „refresh token”), cu siguranță nu dorim să ne modificăm instanța existentă, ci mai degrabă dorim să generăm una nouă pe baza acesteia . Vedeți cum ne ajută adnotarea
@Wither
aici: îi spune lui Lombok să genereze owithTokenExpiryTs(Instant tokenExpiryTs)
care creează o nouă instanță de LoginResponse având toate valorile instanței with'ed, cu excepția celei noi pe care o specificăm. Ați dori acest comportament pentru toate domeniile? Doar adăugați@Wither
la declarația de clasă.
@Date și @Valoare
Ambele cazuri de utilizare discutate până acum sunt atât de comune încât Lombok oferă câteva adnotări pentru a le face și mai scurte: adnotarea unei clase cu @Data
va declanșa Lombok să se comporte exact ca și cum ar fi fost adnotat cu @Getter
+ @Setter
+ @ToString
+ @EqualsAndHashCode
+ @RequiredArgsConstructor
. De asemenea, folosirea @Value
vă va transforma clasa într-una imuabilă (și finală), tot din nou ca și cum ar fi fost adnotată cu lista de mai sus.
Model de constructor
Revenind la exemplul nostru de utilizator, dacă dorim să creăm o instanță nouă, va trebui să folosim un constructor cu până la șase argumente. Acesta este deja un număr destul de mare, care se va înrăutăți dacă mai adăugăm atribute la clasă. De asemenea, să presupunem că am dori să setăm niște valori implicite pentru câmpurile lastName
și payingCustomer
.
Lombok implementează o caracteristică @Builder
foarte puternică, permițându-ne să folosim un model Builder pentru a crea instanțe noi. Să-l adăugăm la clasa noastră de utilizator:
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; }
Acum suntem capabili să creăm fluent noi utilizatori, astfel:
User user = User .builder() .email("[email protected]") .password("secret".getBytes(StandardCharsets.UTF_8)) .firstName("Miguel") .registrationTs(Instant.now()) .build();
Este ușor să ne imaginăm cât de convenabil devine această construcție pe măsură ce clasele noastre cresc.
Delegare/Compunere
Dacă doriți să urmați regula foarte sănătoasă de „favorizare compoziție față de moștenire”, asta este ceva cu care Java nu ajută cu adevărat, în ceea ce privește verbozitatea. Dacă doriți să compuneți obiecte, de obicei ar trebui să scrieți apeluri de metodă de delegare peste tot.
Lombok propune o soluție pentru aceasta prin @Delegate
. Să aruncăm o privire la un exemplu.
Imaginați-vă că vrem să introducem un nou concept de ContactInformation
. Acestea sunt câteva informații pe care le are User
nostru și este posibil să dorim să aibă și alte clase. Apoi putem modela acest lucru printr-o interfață ca aceasta:
public interface HasContactInformation { String getEmail(); String getFirstName(); String getLastName(); }
Am introduce apoi o nouă clasă ContactInformation
folosind Lombok:
import lombok.Data; @Data public class ContactInformation implements HasContactInformation { private String email; private String firstName; private String lastName; }
Și, în cele din urmă, am putea refactoriza User
pentru a compune cu ContactInformation
și să folosim Lombok pentru a genera toate apelurile de delegare necesare pentru a se potrivi cu contractul de interfață:
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; }
Rețineți că nu a fost nevoie să scriu implementări pentru metodele HasContactInformation
: acesta este ceva ce îi spunem lui Lombok să facă, delegând apelurile instanței noastre ContactInformation
.
De asemenea, pentru că nu vreau ca instanța delegată să fie accesibilă din exterior, o personalizez cu un @Getter(AccessLevel.NONE)
, prevenind efectiv generarea getterului pentru aceasta.
Excepții verificate
După cum știm cu toții, Java face diferența între excepțiile verificate și cele neverificate. Aceasta este o sursă tradițională de controversă și critică la adresa limbii, deoarece gestionarea excepțiilor ne împiedică uneori prea mult, mai ales atunci când avem de-a face cu API-uri concepute pentru a arunca excepții verificate și, prin urmare, ne obligă dezvoltatorii fie să le prindem, fie să ne declarăm metodele arunca-le.
Luați în considerare acest exemplu:
public class UserService { public URL buildUsersApiUrl() { try { return new URL("https://apiserver.com/users"); } catch (MalformedURLException ex) { // Malformed? Really? throw new RuntimeException(ex); } } }
Acesta este un model atât de comun: știm cu siguranță că URL-ul nostru este bine format, totuși, deoarece constructorul URL
-ului aruncă o excepție bifată, fie suntem forțați să-l prindem, fie să declarăm metoda noastră de a o arunca și de a scoate apelanții în aceeași situație. Încadrarea acestor excepții verificate într-o RuntimeException
este o practică foarte extinsă. Și acest lucru devine și mai rău dacă numărul de excepții verificate cu care trebuie să ne confruntăm crește pe măsură ce codificăm.
Deci, acesta este exact scopul @SneakyThrows
de la Lombok, va îngloba orice excepții verificate care vor fi introduse în metoda noastră într-una neverificată și ne va elibera de bătăi de cap:
import lombok.SneakyThrows; public class UserService { @SneakyThrows public URL buildUsersApiUrl() { return new URL("https://apiserver.com/users"); } }
Logare
Cât de des adăugați instanțe de înregistrare la cursurile dvs. ca acesta? (Eșantion SLF4J)
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
Am să ghicesc destul de mult. Știind acest lucru, creatorii Lombok au implementat o adnotare care creează o instanță de logger cu un nume personalizabil (implicit pentru log), care acceptă cele mai comune cadre de logare de pe platforma Java. La fel (din nou, bazat pe 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"); } }
Adnotarea codului generat
Dacă folosim Lombok pentru a genera cod, se poate părea că ne-am pierde capacitatea de a adnota acele metode, deoarece nu le scriem de fapt. Dar acest lucru nu este chiar adevărat. Mai degrabă, Lombok ne permite să-i spunem cum ne-am dori ca codul generat să fie adnotat, folosind totuși o notație oarecum particulară, adevărul să fie spus.
Luați în considerare acest exemplu, care vizează utilizarea unui cadru de injecție de dependență: avem o clasă UserService
care utilizează injecția de constructor pentru a obține referințele la un UserRepository
și 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; // } }
Exemplul de mai sus arată cum se adnotă un constructor generat. Lombok ne permite să facem același lucru și pentru metodele și parametrii generați.
Învățați mai multe
Utilizarea Lombok explicată în această postare se concentrează pe acele caracteristici pe care personal le-am găsit a fi cele mai utile de-a lungul anilor. Cu toate acestea, există multe alte caracteristici și personalizări disponibile.
Documentația Lombok este foarte informativă și amănunțită. Au pagini dedicate pentru fiecare caracteristică (adnotare) cu explicații și exemple foarte detaliate. Dacă vi se pare interesantă această postare, vă încurajez să vă aprofundați în lombok și în documentația acestuia pentru a afla mai multe.
Site-ul proiectului documentează modul de utilizare a Lombok în mai multe medii de programare diferite. Pe scurt, cele mai populare IDE-uri (Eclipse, NetBeans și IntelliJ) sunt acceptate. Eu însumi trec în mod regulat de la unul la altul pe bază de proiect și folosesc Lombok pe toate fără probleme.
Delombok!
Delombok face parte din „lanțul de instrumente Lombok” și poate fi foarte util. Ceea ce face este, practic, să genereze codul sursă Java pentru codul dvs. adnotat Lombok, efectuând aceleași operațiuni pe care le face codul de octeți generat de Lombok.
Aceasta este o opțiune excelentă pentru cei care se gândesc să adopte Lombok, dar nu sunt încă siguri. Puteți începe liber să îl utilizați și nu va exista nicio „blocare a furnizorului”. În cazul în care ulterior dumneavoastră sau echipa dumneavoastră regretați alegerea, puteți utiliza oricând delombok pentru a genera codul sursă corespunzător pe care apoi îl puteți utiliza fără a mai depinde de Lombok.
Delombok este, de asemenea, un instrument excelent pentru a afla exact ce va face Lombok. Există modalități foarte simple de a-l conecta la procesul de construire.
Alternative
Există multe instrumente în lumea Java care fac o utilizare similară a procesoarelor de adnotare pentru a vă îmbogăți sau modifica codul în timpul compilării, cum ar fi Immutables sau Google Auto Value. Acestea (și altele, cu siguranță!) se suprapun cu funcțiile Lombok. Îmi place foarte mult abordarea Imutables și am folosit-o și în unele proiecte.
De asemenea, merită remarcat faptul că există și alte instrumente grozave care oferă caracteristici similare pentru „îmbunătățirea bytecode”, cum ar fi Byte Buddy sau Javassist. Acestea funcționează de obicei în timpul execuției și cuprind o lume proprie dincolo de scopul acestei postări.
Java concis
Există o serie de limbaje moderne vizate de JVM care oferă abordări de proiectare mai idiomatice – sau chiar la nivel de limbaj – care ajută la rezolvarea unora dintre aceleași probleme. Cu siguranță Groovy, Scala și Kotlin sunt exemple frumoase. Dar dacă lucrați la un proiect numai pentru Java, atunci Lombok este un instrument bun pentru a vă ajuta programele să fie mai concise, mai expresive și mai ușor de întreținut.