Introductie tot Project Lombok

1. Vermijd herhaalde code

Java is een geweldige taal, maar het wordt soms te uitgebreid voor dingen die u in uw code moet doen voor algemene taken of voor het voldoen aan bepaalde framework-praktijken. Deze hebben vaak geen echte waarde voor de zakelijke kant van uw programma's - en dit is waar Lombok hier is om uw leven gelukkiger en uzelf productiever te maken.

De manier waarop het werkt, is door in te pluggen in uw bouwproces en automatisch de Java-bytecode in uw .klasse bestanden volgens een aantal projectannotaties die u in uw code invoert.

Het opnemen in uw builds, welk systeem u ook gebruikt, is heel eenvoudig. Hun projectpagina heeft gedetailleerde instructies over de details. De meeste van mijn projecten zijn gebaseerd op maven, dus ik laat meestal hun afhankelijkheid in het voorzien scope en ik ben klaar om te gaan:

 ... org.projectlombok lombok 1.18.10 voorzien ... 

Kijk hier voor de meest recente beschikbare versie.

Merk op dat afhankelijk van Lombok geen gebruikers van uw .pots zijn er ook van afhankelijk, aangezien het een pure build-afhankelijkheid is, geen runtime.

2. Getters / Setters, Constructors - Zo repetitief

Het inkapselen van objecteigenschappen via publieke getter- en setter-methoden is zo gebruikelijk in de Java-wereld, en veel frameworks vertrouwen in grote mate op dit "Java Bean" -patroon: een klasse met een lege constructor en get / set-methoden voor "eigenschappen".

Dit komt zo vaak voor dat de meeste IDE's automatisch genereren van code voor deze patronen (en meer) ondersteunen. Deze code moet echter in uw bronnen voorkomen en ook worden onderhouden wanneer er bijvoorbeeld een nieuwe eigenschap wordt toegevoegd of een veld wordt hernoemd.

Laten we deze klasse beschouwen die we als een PPV-entiteit willen gebruiken als voorbeeld:

@Entity public class Gebruiker implementeert Serializable {private @Id Long id; // zal worden ingesteld bij het aanhouden van private String firstName; private String achternaam; privé int leeftijd; public User () {} public User (String firstName, String lastName, int age) {this.firstName = firstName; this.lastName = achternaam; this.age = leeftijd; } // getters en setters: ~ 30 extra regels code}

Dit is een vrij eenvoudige klasse, maar bedenk nog steeds dat als we de extra code voor getters en setters zouden toevoegen, we een definitie zouden krijgen waarin we meer standaard nulwaardecode zouden hebben dan de relevante bedrijfsinformatie: "een gebruiker heeft eerst en achternaam en leeftijd. "

Laat het ons weten Lombok-ize deze klas:

@Entity @Getter @Setter @NoArgsConstructor // <--- DIT is het openbare klasse Gebruiker implementeert Serializable {privé @Id Lange id; // zal worden ingesteld bij het aanhouden van private String firstName; private String achternaam; privé int leeftijd; openbare gebruiker (String firstName, String lastName, int age) {this.firstName = firstName; this.lastName = achternaam; this.age = leeftijd; }}

Door de @Beter en @Setter annotaties die we Lombok hebben verteld om deze voor alle velden van de klas te genereren. @NoArgsConstructor zal leiden tot een lege constructorgeneratie.

Merk op dat dit het heel class-code, ik laat niets weg in tegenstelling tot de bovenstaande versie met de // getters en setters commentaar. Voor een klasse met drie relevante attributen is dit een aanzienlijke besparing in code!

Als u verder attributen (eigenschappen) toevoegt aan uw Gebruiker class, gebeurt hetzelfde: je hebt de annotaties op het type zelf toegepast, zodat ze standaard op alle velden letten.

Wat als u de zichtbaarheid van sommige eigendommen wilt verfijnen? Ik hou er bijvoorbeeld van om mijn entiteiten te behouden ' ID kaart veldmodificatoren pakket of beschermd zichtbaar omdat wordt verwacht dat ze worden gelezen, maar niet expliciet worden ingesteld door de toepassingscode. Gebruik gewoon een fijnere korrel @Setter voor dit specifieke veld:

privé @Id @Setter (AccessLevel.PROTECTED) Lange id;

3. Luie getter

Toepassingen moeten vaak een dure handeling uitvoeren en de resultaten opslaan voor later gebruik.

Laten we bijvoorbeeld zeggen dat we statische gegevens uit een bestand of een database moeten lezen. Het is over het algemeen een goede gewoonte om deze gegevens een keer op te halen en ze vervolgens in de cache op te slaan om in-memory reads in de applicatie mogelijk te maken. Dit voorkomt dat de applicatie de dure operatie moet herhalen.

Een ander veel voorkomend patroon is om haal deze gegevens alleen op als ze voor het eerst nodig zijn. Met andere woorden, alleen de gegevens ophalen wanneer de overeenkomstige getter de eerste keer wordt aangeroepen. Dit heet trage voortgang.

Stel dat deze gegevens in de cache worden opgeslagen als een veld binnen een klasse. De klasse moet er nu voor zorgen dat elke toegang tot dit veld de gegevens in de cache retourneert. Een mogelijke manier om een ​​dergelijke klasse te implementeren, is door de getter-methode de gegevens alleen op te laten halen als het veld dat is nul. Om deze reden, we noemen dit een lui getter.

Lombok maakt dit mogelijk met de lui parameter in de @Getter annotatie we zagen hierboven.

Beschouw bijvoorbeeld deze eenvoudige klasse:

openbare klasse GetterLazy {@Getter (lazy = true) privé definitieve kaarttransacties = getTransactions (); privékaart getTransactions () {laatste kaartcache = nieuwe HashMap (); Lijst txnRows = readTxnListFromFile (); txnRows.forEach (s -> {String [] txnIdValueTuple = s.split (DELIMETER); cache.put (txnIdValueTuple [0], Long.parseLong (txnIdValueTuple [1]));}); terugkeer cache; }}

Dit leest sommige transacties uit een bestand in een Kaart. Omdat de gegevens in het bestand niet veranderen, zullen we het een keer cachen en toegang geven via een getter.

Als we nu naar de gecompileerde code van deze klasse kijken, zien we een getter-methode die de cache bijwerkt als dat het geval was nul en retourneert vervolgens de gegevens in de cache:

openbare klasse GetterLazy {privé definitieve AtomicReference-transacties = nieuwe AtomicReference (); public GetterLazy () {} // andere methoden public Map getTransactions () {Objectwaarde = this.transactions.get (); if (waarde == null) {gesynchroniseerd (this.transactions) {waarde = this.transactions.get (); if (waarde == null) {Map actualValue = this.readTxnsFromFile (); waarde = actualValue == null? this.transactions: actualValue; this.transactions.set (waarde); }}} return (Map) ((Map) (value == this.transactions? null: value)); }}

Het is interessant om erop te wijzen Lombok heeft het dataveld verpakt in een AtomicReference.Dit zorgt voor atomaire updates van het transacties veld. De getTransactions () methode zorgt er ook voor dat u het bestand leest if transacties is nul.

Het gebruik van de AtomicReference-transacties veld rechtstreeks vanuit de klas wordt afgeraden. Het wordt aanbevolen om de getTransactions () methode voor toegang tot het veld.

Om deze reden, als we een andere Lombok-annotatie gebruiken, zoals ToString in dezelfde klas, het zal gebruiken getTransactions () in plaats van rechtstreeks toegang te krijgen tot het veld.

4. Waardeklassen / DTO's

Er zijn veel situaties waarin we een gegevenstype willen definiëren met als enig doel complexe 'waarden' weer te geven of als 'gegevensoverdrachtobjecten', meestal in de vorm van onveranderlijke gegevensstructuren die we een keer bouwen en nooit willen veranderen .

We ontwerpen een klasse om een ​​succesvolle inlogoperatie te vertegenwoordigen. We willen dat alle velden niet-null zijn en dat objecten onveranderlijk zijn, zodat we veilig toegang kunnen krijgen tot de eigenschappen ervan:

openbare klasse LoginResult {privé finale Instant loginTs; private final String authToken; privé finale Duur tokenValidity; privé uiteindelijke URL-tokenRefreshUrl; // constructor neemt elk veld en controleert nulls // alleen-lezen accessor, niet noodzakelijk als get * () formulier}

Nogmaals, de hoeveelheid code die we zouden moeten schrijven voor de becommentarieerde secties zou van een veel groter volume zijn dan de informatie die we willen inkapselen en dat heeft echte waarde voor ons. We kunnen Lombok opnieuw gebruiken om dit te verbeteren:

@RequiredArgsConstructor @Accessors (fluent = true) @Getter openbare klasse LoginResult {privé finale @NonNull Directe loginTs; privé finale @NonNull String authToken; privé finale @NonNull Duur tokenValidity; privé laatste @NonNull URL-tokenRefreshUrl; }

Voeg gewoon het @BuienRadarNL annotatie en je krijgt een constructor voor alle laatste velden in de klasse, net zoals je ze hebt gedeclareerd. Toevoegen @RTLnieuws to attributes laat onze constructor controleren op nullability en throw NullPointerExceptions overeenkomstig. Dit zou ook gebeuren als de velden niet definitief waren en we hebben toegevoegd @Setter voor hen.

Wil je niet saai oud zijn krijgen*() formulier voor uw eigendommen? Omdat we hebben toegevoegd @Accessors (vloeiend = waar) in dit voorbeeld zouden "getters" dezelfde methode naam hebben als de eigenschappen: getAuthToken () wordt gewoon authToken ().

Dit ‘vloeiende’ formulier zou van toepassing zijn op niet-definitieve velden voor attribuutsetters en zou ook geketende oproepen mogelijk maken:

// Stel je voor dat velden niet langer definitief waren, retourneer nu nieuwe LoginResult () .loginTs (Instant.now ()) .authToken ("asdasd"). // enzovoorts

5. Kern Java Boilerplate

Een andere situatie waarin we code schrijven die we moeten onderhouden, is bij het genereren van toString (), is gelijk aan () en hashCode () methoden. IDE's proberen te helpen met sjablonen voor het automatisch genereren van deze in termen van onze klasseattributen.

We kunnen dit automatiseren door middel van andere Lombok-annotaties op klasniveau:

  • @Tomvangrieken: genereert een toString () methode inclusief alle klassenattributen. Het is niet nodig om er zelf een te schrijven en deze te onderhouden, aangezien we ons datamodel verrijken.
  • @EqualsAndHashCode: zal beide genereren is gelijk aan () en hashCode () methoden standaard rekening houdend met alle relevante velden, en volgens een zeer goed doordachte semantiek.

Deze generatoren leveren zeer handige configuratie-opties. Als uw geannoteerde klassen bijvoorbeeld deel uitmaken van een hiërarchie, kunt u gewoon de callSuper = true parameter en bovenliggende resultaten worden meegenomen bij het genereren van de code van de methode.

Meer hierover: stel dat we onze hadden Gebruiker Voorbeeld van een PPV-entiteit bevat een verwijzing naar gebeurtenissen die aan deze gebruiker zijn gekoppeld:

@OneToMany (mappedBy = "user") privélijstgebeurtenissen;

We zouden niet graag willen dat de hele lijst met evenementen wordt gedumpt wanneer we het toString () methode van onze gebruiker, alleen omdat we de @Tomvangrieken annotatie. Geen probleem: parametreer het gewoon als volgt: @ToString (exclude = {"events"}), en dat zal niet gebeuren. Dit is ook handig om kringverwijzingen te vermijden als UserEvents had een verwijzing naar een Gebruiker.

Voor de LoginResultaat We kunnen bijvoorbeeld gelijkheid en hashcode-berekening definiëren in termen van het token zelf en niet de andere laatste attributen in onze klasse. Schrijf dan gewoon zoiets als @EqualsAndHashCode (of = {"authToken"}).

Bonus: als je de functies van de annotaties die we tot nu toe hebben besproken, leuk vond, wil je misschien eens kijken @Gegevens en @Waarde annotaties omdat ze zich gedragen alsof een reeks ervan op onze lessen is toegepast. Deze besproken gebruiken worden immers in veel gevallen heel vaak samengebracht.

5.1. (Niet) met behulp van de @EqualsAndHashCode Met PPV-entiteiten

Of u de default is gelijk aan () en hashCode () methoden of maak aangepaste methoden voor de PPV-entiteiten, is een vaak besproken onderwerp onder ontwikkelaars. Er zijn meerdere benaderingen die we kunnen volgen; elk met zijn voor- en nadelen.

Standaard, @EqualsAndHashCode bevat alle niet-definitieve eigenschappen van de entiteitsklasse. We kunnen proberen dit op te lossen door de onlyExplicitlyIncluded attribuut van de @EqualsAndHashCode om Lombok alleen de primaire sleutel van de entiteit te laten gebruiken. Toch worden de gegenereerde is gelijk aan () methode kan enkele problemen veroorzaken. Thorben Janssen legt dit scenario nader uit in een van zijn blogposts.

Over het algemeen, we moeten vermijden Lombok te gebruiken om de is gelijk aan () en hashCode () methoden voor onze PPV-entiteiten!

6. Het Builder-patroon

Het volgende zou kunnen zorgen voor een voorbeeldconfiguratieklasse voor een REST API-client:

openbare klasse ApiClientConfiguration {privé String-host; privé int poort; privé booleaans useHttps; privé lange connectTimeout; privé lange readTimeout; private String gebruikersnaam; privé String-wachtwoord; // Welke andere opties je ook hebt. // Lege constructor? Alle combinaties? // getters ... en setters? }

We zouden een eerste benadering kunnen hebben op basis van het gebruik van de standaard lege constructor van de klasse en het verstrekken van setter-methoden voor elk veld. We willen echter idealiter dat configuraties niet opnieuw wordenset zodra ze zijn gebouwd (geïnstantieerd), waardoor ze in feite onveranderlijk zijn. We willen daarom setters vermijden, maar het schrijven van zo'n potentieel lange args-constructor is een antipatroon.

In plaats daarvan kunnen we de tool vertellen om een bouwer patroon, waardoor we er niet meer over konden schrijven Bouwer class en bijbehorende vloeiende setter-achtige methoden door simpelweg de @Builder-annotatie toe te voegen aan onze ApiClientConfiguration.

@Builder openbare klasse ApiClientConfiguration {// ... al het andere blijft hetzelfde}

De klassendefinitie hierboven als zodanig verlaten (geen declareer constructors of setters + @Bouwer) kunnen we het uiteindelijk gebruiken als:

ApiClientConfiguration config = ApiClientConfiguration.builder () .host ("api.server.com") .port (443) .useHttps (true) .connectTimeout (15_000L) .readTimeout (5_000L) .username ("myusername") .password (" geheim ") .build ();

7. Gecontroleerde uitzonderingslast

Veel Java API's zijn zo ontworpen dat ze een aantal aangevinkte uitzonderingen kunnen gooien waar de clientcode toe wordt gedwongen vangst of verklaren aan gooit. Hoe vaak heb je deze uitzonderingen, waarvan je weet dat ze niet zullen gebeuren, in zoiets veranderd?

public String resourceAsString () {probeer (InputStream is = this.getClass (). getResourceAsStream ("sure_in_my_jar.txt")) {BufferedReader br = nieuwe BufferedReader (nieuwe InputStreamReader (is, "UTF-8")); return br.lines (). collect (Collectors.joining ("\ n")); } catch (IOException | UnsupportedCharsetException ex) {// Als dit ooit gebeurt, dan is het een bug. gooi nieuwe RuntimeException (ex); <--- inkapselen in een Runtime ex. }}

Als u deze codepatronen wilt vermijden omdat de compiler anders niet gelukkig zal zijn (en u weten de gecontroleerde fouten kunnen niet voorkomen), gebruik dan de toepasselijke naam @BuienRadarNL:

@SneakyThrows openbare String resourceAsString () {probeer (InputStream is = this.getClass (). GetResourceAsStream ("sure_in_my_jar.txt")) {BufferedReader br = nieuwe BufferedReader (nieuwe InputStreamReader (is, "UTF-8")); return br.lines (). collect (Collectors.joining ("\ n")); }}

8. Zorg ervoor dat uw middelen worden vrijgegeven

Java 7 introduceerde het try-with-resources-blok om ervoor te zorgen dat uw resources worden vastgehouden door instanties van alles wat wordt geïmplementeerd java.lang.AutoCloseable worden vrijgegeven bij het verlaten.

Lombok biedt een alternatieve manier om dit te bereiken, en flexibeler via @Cleanup. Gebruik het voor elke lokale variabele waarvan u zeker wilt weten dat ze worden vrijgegeven. Ze hoeven geen specifieke interface te implementeren, u krijgt gewoon de dichtbij() methode genaamd.

@Cleanup InputStream is = this.getClass (). GetResourceAsStream ("res.txt");

Heeft uw vrijgavemethode een andere naam? Geen probleem, pas gewoon de annotatie aan:

@Cleanup ("dispose") JFrame mainFrame = nieuw JFrame ("Hoofdvenster");

9. Maak aantekeningen in uw klas om een ​​logger te krijgen

Velen van ons voegen spaarzaam logboekinstructies toe aan onze code door een instantie van een Logger vanuit ons keuzekader. Zeg, SLF4J:

openbare klasse ApiClientConfiguration {privé statische Logger LOG = LoggerFactory.getLogger (ApiClientConfiguration.class); // LOG.debug (), LOG.info (), ...}

Dit is zo'n veel voorkomend patroon dat Lombok-ontwikkelaars het voor ons hebben vereenvoudigd:

@ Slf4j // of: @Log @CommonsLog @ Log4j @ Log4j2 @ XSlf4j openbare klasse ApiClientConfiguration {// log.debug (), log.info (), ...}

Veel frameworks voor logboekregistratie worden ondersteund en u kunt natuurlijk de instantienaam, het onderwerp enz. Aanpassen.

10. Schrijf thread-veiliger methoden

In Java kunt u de gesynchroniseerd trefwoord om kritieke secties te implementeren. Dit is echter geen 100% veilige aanpak: andere clientcode kan uiteindelijk ook op uw exemplaar worden gesynchroniseerd, wat mogelijk tot onverwachte impasses kan leiden.

Dit is waar @Gesynchroniseerd komt binnen: annoteer uw methoden (zowel instantie als statisch) ermee en u krijgt een automatisch gegenereerd privé, onbelicht veld dat uw implementatie zal gebruiken voor het vergrendelen:

@Synchronized public / * beter dan: synchronized * / void putValueInCache (String key, Object value) {// wat hier ook een thread-safe code is}

11. Automatiseer de samenstelling van objecten

Java heeft geen constructies op taalniveau om een ​​"overerving van voorkeurssamenstelling" -benadering glad te strijken. Andere talen hebben ingebouwde concepten zoals Eigenschappen of Mixins om dit te behalen.

Lombok's @Delegate is erg handig als je dit programmeerpatroon wilt gebruiken. Laten we een voorbeeld bekijken:

  • Wij willen Gebruikers en Klants om enkele gemeenschappelijke kenmerken voor naamgeving en telefoonnummer te delen
  • Voor deze velden definiëren we zowel een interface- als een adapterklasse
  • We zullen onze modellen de interface laten implementeren en @Delegeren effectief op hun adapter componeren hen met onze contactgegevens

Laten we eerst een interface definiëren:

openbare interface HasContactInformation {String getFirstName (); void setFirstName (String firstName); String getFullName (); String getLastName (); void setLastName (String lastName); String getPhoneNr (); void setPhoneNr (String phoneNr); }

En nu een adapter als ondersteuning klasse:

@Data openbare klasse ContactInformationSupport implementeert HasContactInformation {private String firstName; private String achternaam; privé String phoneNr; @Override public String getFullName () {return getFirstName () + "" + getLastName (); }}

Het interessante deel komt nu, kijk hoe gemakkelijk het is om nu contactgegevens in beide modelklassen samen te stellen:

openbare klasse Gebruiker implementeert HasContactInformation {// Welke andere gebruikersspecifieke attributen @Delegate (types = {HasContactInformation.class}) privé definitief ContactInformationSupport contactInformation = nieuw ContactInformationSupport (); // De gebruiker implementeert zelf alle contactgegevens via delegatie}

De zaak voor Klant zou zo op elkaar lijken dat we het voorbeeld kortheidshalve weglaten.

12. Lombok terug rollen?

Kort antwoord: helemaal niet.

Je maakt je misschien zorgen dat er een kans bestaat dat je Lombok in een van je projecten gebruikt, maar wil die beslissing later terugdraaien. Je zou dan misschien een groot aantal klassen laten annoteren… wat zou je kunnen doen?

Ik heb hier nooit echt spijt van gehad, maar wie weet voor jou, je team of je organisatie. Voor deze gevallen bent u gedekt dankzij de delombok tool uit hetzelfde project.

Door delombok-ing je code zou je automatisch gegenereerde Java-broncode krijgen met exact dezelfde functies als de bytecode die Lombok heeft gebouwd. Dus dan kunt u eenvoudig uw originele geannoteerde code vervangen door deze nieuwe delomboked bestanden en er niet langer afhankelijk van zijn.

Dit is iets dat je kunt integreren in je build en ik heb dit in het verleden gedaan om alleen de gegenereerde code te bestuderen of om Lombok te integreren met een andere op Java-broncode gebaseerde tool.

13. Conclusie

Er zijn enkele andere functies die we niet in dit artikel hebben gepresenteerd, ik raad u aan om dieper in het functieoverzicht te duiken voor meer details en gebruiksscenario's.

Ook hebben de meeste functies die we hebben laten zien een aantal aanpassingsopties die u wellicht handig vindt om de tool dingen te laten genereren die het meest voldoen aan de praktijken van uw team voor naamgeving enz. Het beschikbare ingebouwde configuratiesysteem kan u daarbij ook helpen.

Ik hoop dat je de motivatie hebt gevonden om Lombok een kans te geven om in je Java-ontwikkeltoolset te komen. Probeer het eens en verhoog uw productiviteit!

De voorbeeldcode is te vinden in het GitHub-project.


$config[zx-auto] not found$config[zx-overlay] not found