Einführung in das Projekt Lombok

Einführung in das Projekt Lombok

Lombok ist eines der Tools, die ich buchstäblich immer als erstes in meine Projekte einbaue. Ich könnte mir nicht vorstellen, Java heutzutage ohne es zu programmieren. Ich hoffe wirklich, dass Sie seine Kraft finden, diesen Artikel zu lesen!

1. Vermeiden Sie sich wiederholenden Code

Java ist eine großartige Sprache, aber es wird manchmal zu ausführlich für Dinge, die Sie in Ihrem Code für allgemeine Aufgaben oder die Einhaltung einiger Framework-Praktiken tun müssen. Diese bringen häufig keinen wirklichen Nutzen für die Geschäftsseite Ihrer Programme - und hier ist Lombok für Sie da, um Ihr Leben glücklicher und produktiver zu gestalten.

Die Funktionsweise besteht darin, sich in Ihren Erstellungsprozess einzufügen und Java-Bytecode gemäß einer Reihe von Projektanmerkungen, die Sie in Ihren Code einfügen, automatisch in die.class-Dateien zu generieren.

Weitere Lektüre:

Lombok Builder mit Standardwert

Erfahren Sie, wie Sie mit Lombok Standardeigenschaftswerte für Builder erstellen

Read more

Lombok mit Eclipse und Intellij einrichten

Erfahren Sie, wie Sie Lombok mit gängigen IDEs einrichten

Read more

Die Einbindung in Ihre Builds, unabhängig davon, welches System Sie verwenden, ist sehr einfach. Ihreproject page enthalten detaillierte Anweisungen zu den Einzelheiten. Die meisten meiner Projekte basieren auf Maven, daher lasse ich ihre Abhängigkeit normalerweise im Bereich vonprovidedfallen und kann loslegen:


    ...
    
        org.projectlombok
        lombok
        1.18.4
        provided
    
    ...

Suchen Sie nach der neuesten verfügbaren Versionhere.

Beachten Sie, dass abhängig von Lombok Benutzer Ihrer.jars nicht ebenfalls davon abhängig sind, da es sich um eine reine Build-Abhängigkeit handelt, nicht um eine Laufzeit.

2. Getters/Setters, Constructors – So Repetitive

Das Einkapseln von Objekteigenschaften über öffentliche Getter- und Setter-Methoden ist in der Java-Welt weit verbreitet, und viele Frameworks stützen sich weitgehend auf dieses „Java Bean“ -Muster: eine Klasse mit einem leeren Konstruktor und Get / Set-Methoden für „Properties“.

Dies ist so häufig, dass die meisten IDEs den Code für die automatische Generierung dieser Muster (und mehr) unterstützen. Dieser Code muss sich jedoch in Ihren Quellen befinden und auch beibehalten werden, wenn beispielsweise eine neue Eigenschaft hinzugefügt oder ein Feld umbenannt wird.

Betrachten wir diese Klasse, die wir als JPA-Entität verwenden möchten, als Beispiel:

@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
}

Dies ist eine ziemlich einfache Klasse, aber wenn wir den zusätzlichen Code für Getter und Setter hinzufügen, erhalten wir eine Definition, bei der wir mehr Nullwertcode als die relevanten Geschäftsinformationen haben würden: „Ein Benutzer hat zuerst und Nachnamen und Alter. "

Lassen Sie uns jetztLombok-ize diese Klasse:

@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;
    }
}

Durch Hinzufügen der Anmerkungen@Getter und@Setter haben wir Lombok angewiesen, diese für alle Felder der Klasse zu generieren. @NoArgsConstructor führt zu einer leeren Konstruktorgenerierung.

Beachten Sie, dass dies der Klassencode vonwholeist. Ich lasse nichts aus, im Gegensatz zu der obigen Version mit dem Kommentar von// getters and setters. Für eine Klasse mit drei relevanten Attributen bedeutet dies eine erhebliche Codeeinsparung!

Wenn Sie Ihrer KlasseUserweitere Attribute (Eigenschaften) hinzufügen, geschieht dasselbe: Sie haben die Anmerkungen auf den Typ selbst angewendet, sodass sie standardmäßig alle Felder berücksichtigen.

Was wäre, wenn Sie die Sichtbarkeit einiger Objekte optimieren möchten? Zum Beispiel möchte ich die Feldmodifikatorenpackage oderprotected meiner Entitäten sichtbar halten, da erwartet wird, dass sie gelesen, aber nicht explizit durch den Anwendungscode festgelegt werden. Verwenden Sie einfach feinkörnigere@Setter für dieses spezielle Feld:

private @Id @Setter(AccessLevel.PROTECTED) Long id;

3. Lazy Getter

Oft müssen Anwendungen teure Vorgänge ausführen und die Ergebnisse für die spätere Verwendung speichern.

Angenommen, wir müssen statische Daten aus einer Datei oder einer Datenbank lesen. Im Allgemeinen empfiehlt es sich, diese Daten einmal abzurufen und dann zwischenzuspeichern, um In-Memory-Lesevorgänge in der Anwendung zu ermöglichen. Dies erspart der Anwendung, den teuren Vorgang zu wiederholen.

Ein weiteres häufiges Muster istretrieve this data only when it’s first needed. Mit anderen Worten,only get the data when the corresponding getter is called the first time. This is called lazy-loading.

Angenommen, diese Daten werden als Feld in einer Klasse zwischengespeichert. Die Klasse muss jetzt sicherstellen, dass jeder Zugriff auf dieses Feld die zwischengespeicherten Daten zurückgibt. Eine Möglichkeit, eine solche Klasse zu implementieren, besteht darin, die Getter-Methode die Daten nur dann abrufen zu lassen, wenn das Feldnull ist. Aus diesem Grund istwe call this a lazy getter.

Lombok macht dies mit den oben gesehenenlazy parameter in the @Getter annotation möglich.

Betrachten Sie zum Beispiel diese einfache Klasse:

public class GetterLazy {

    @Getter(lazy = true)
    private final Map transactions = readTxnsFromFile();

    private Map readTxnsFromFile() {

        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;
    }
}

Dies liest einige Transaktionen aus einer Datei inMap. Da sich die Daten in der Datei nicht ändern, werden sie einmal zwischengespeichert und der Zugriff über einen Getter ermöglicht.

Wenn wir uns jetzt den kompilierten Code dieser Klasse ansehen, sehen wir eingetter method which updates the cache if it was null and then returns the cached data:

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 ist interessant darauf hinzuweisen, dassLombok wrapped the data field in an *https://www.example.com/java-atomic-variables[AtomicReference].* Dies sorgt für atomare Aktualisierungen destransaction fields. Die MethodegetTransactions() stellt auch sicher, dass die Datei gelesen wird, wenntransactions isnull. ist

Von der Verwendung des FeldsAtomicReference transactions direkt innerhalb der Klasse wird abgeraten. It’s recommended to use the getTransactions() method for accessing the field.

Wenn wir aus diesem Grund eine andere Lombok-Annotation wieToString in derselben Klasse, verwenden, werdengetTransactions() verwendet, anstatt direkt auf das Feld zuzugreifen.

4. Werteklassen / DTOs

Es gibt viele Situationen, in denen wir einen Datentyp mit dem einzigen Zweck definieren möchten, komplexe „Werte“ oder als „Datenübertragungsobjekte“ darzustellen, meist in Form von unveränderlichen Datenstrukturen, die wir einmal erstellen und nie ändern möchten .

Wir entwerfen eine Klasse, die einen erfolgreichen Anmeldevorgang darstellt. Wir möchten, dass alle Felder nicht null sind und Objekte unveränderlich sind, damit wir threadsicher auf ihre Eigenschaften zugreifen können:

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
}

Auch hier wäre die Menge an Code, die wir für die kommentierten Abschnitte schreiben müssten, viel größer als die Informationen, die wir kapseln möchten, und das hat für uns einen echten Wert. Wir können Lombok erneut verwenden, um dies zu verbessern:

@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;

}

Fügen Sie einfach die Annotation@RequiredArgsConstructorhinzu und Sie erhalten einen Konstruktor für alle endgültigen Felder in der Klasse, so wie Sie sie deklariert haben. Durch Hinzufügen von@NonNull zu Attributen überprüft unser Konstruktor die Nullbarkeit und wirftNullPointerExceptions entsprechend. Dies würde auch passieren, wenn die Felder nicht endgültig wären und wir@Setter für sie hinzufügen würden.

Möchten Sie nicht das langweilige alteget*()-Formular für Ihre Immobilien? Da wir in diesem Beispiel@Accessors(fluent=true) hinzugefügt haben, haben „Getter“ denselben Methodennamen wie die Eigenschaften:getAuthToken() wird einfach zuauthToken().

Dieses „fließende“ Formular würde für nicht endgültige Felder für Attributsetzer gelten und außerdem verkettete Aufrufe ermöglichen:

// Imagine fields were no longer final now
return new LoginResult()
  .loginTs(Instant.now())
  .authToken("asdasd")
  . // and so on

5. Core Java Boilerplate

Eine andere Situation, in der wir am Ende Code schreiben, den wir beibehalten müssen, ist das Generieren von MethodentoString(),equals() undhashCode(). IDEs versuchen, mit Vorlagen zu helfen, um diese anhand unserer Klassenattribute automatisch zu generieren.

Wir können dies mithilfe anderer Anmerkungen auf Lombok-Klassenebene automatisieren:

  • @ToString: Erzeugt einetoString()-Methode mit allen Klassenattributen. Es ist nicht erforderlich, selbst eines zu schreiben und es zu pflegen, während wir unser Datenmodell anreichern.

  • @EqualsAndHashCode: generiert standardmäßig sowohlequals() als auchhashCode() Methoden unter Berücksichtigung aller relevanten Felder und gemäßvery well though semantics.

Diese Generatoren werden mit sehr praktischen Konfigurationsoptionen ausgeliefert. Wenn Ihre mit Anmerkungen versehenen Klassen beispielsweise Teil einer Hierarchie sind, können Sie einfach den ParametercallSuper=trueverwenden. Die übergeordneten Ergebnisse werden beim Generieren des Methodencodes berücksichtigt.

Mehr dazu: Angenommen, unser Beispiel für die JPA-EntitätUserenthält einen Verweis auf Ereignisse, die diesem Benutzer zugeordnet sind:

@OneToMany(mappedBy = "user")
private List events;

Wir möchten nicht, dass die gesamte Liste der Ereignisse gelöscht wird, wenn wir dietoString()-Methode unseres Benutzers aufrufen, nur weil wir die Annotation@ToStringverwendet haben. Kein Problem: Parametrieren Sie es einfach so:@ToString(exclude = \{“events”}), und das wird nicht passieren. Dies ist auch hilfreich, um Zirkelverweise zu vermeiden, wenn beispielsweiseUserEvents einen Verweis aufUser hatte.

Für das Beispiel vonLoginResultmöchten wir möglicherweise die Berechnung der Gleichheit und des Hash-Codes nur anhand des Tokens selbst und nicht anhand der anderen endgültigen Attribute in unserer Klasse definieren. Dann schreiben Sie einfach so etwas wie@EqualsAndHashCode(of = \{“authToken”}).

Bonus: Wenn Ihnen die Funktionen aus den Anmerkungen gefallen haben, die wir bisher überprüft haben, können Sie die Anmerkungen von@Data und@Value untersuchen, da sie sich so verhalten, als ob eine Reihe von Anmerkungen auf unsere Klassen angewendet worden wäre. Schließlich werden diese diskutierten Verwendungen in vielen Fällen sehr häufig zusammengestellt.

6. Das Builder-Muster

Folgendes könnte für eine Beispielkonfigurationsklasse für einen REST-API-Client gelten:

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?
}

Wir könnten einen ersten Ansatz verfolgen, der auf der Verwendung des Klassenstandard-Leer-Konstruktors und der Bereitstellung von Setter-Methoden für jedes Feld basiert. Wir möchten jedoch im Idealfall, dass Konfigurationen nach ihrer Erstellung (Instanziierung) nicht wieder zusetwerden, wodurch sie effektiv unveränderlich werden. Wir möchten daher Setter vermeiden, aber das Schreiben eines solchen potenziell langen args-Konstruktors ist ein Anti-Pattern.

Stattdessen können wir das Tool anweisen, einbuilder-Muster zu generieren, wodurch verhindert wird, dass wir eine zusätzlicheBuilder-Klasse und zugehörige fließende Setter-ähnliche Methoden schreiben, indem wir einfach die @ Builder-Annotation zu unserenApiClientConfiguration hinzufügen .

@Builder
public class ApiClientConfiguration {

    // ... everything else remains the same

}

Wenn wir die obige Klassendefinition als solche belassen (keine Deklarationskonstruktoren oder Setter +@Builder), können wir sie am Ende wie folgt verwenden:

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

Viele Java-APIs sind so konzipiert, dass sie eine Reihe von geprüften Ausnahmen auslösen können. Der Clientcode wird entweder zucatch gezwungen oder zuthrows deklariert. Wie oft haben Sie diese Ausnahmen, von denen Sie wissen, dass sie nicht passieren, in so etwas verwandelt?

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.
    }
}

Wenn Sie diese Codemuster vermeiden möchten, weil der Compiler ansonsten nicht zufrieden ist (und Sieknow die überprüften Fehler nicht auftreten können), verwenden Sie die treffend benannten@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. Stellen Sie sicher, dass Ihre Ressourcen freigegeben werden

Java 7 hat den Try-with-Resources-Block eingeführt, um sicherzustellen, dass Ihre Ressourcen, die von Instanzen vonjava.lang implementiert werden.AutoCloseable werden beim Beenden freigegeben.

Lombok bietet eine alternative Möglichkeit, dies zu erreichen, und zwar flexibler über@Cleanup. Verwenden Sie diese Option für alle lokalen Variablen, deren Ressourcen freigegeben werden sollen. Sie müssen keine bestimmte Schnittstelle implementieren, sondern erhalten lediglich dieclose()-Methode.

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

Ihre Veröffentlichungsmethode hat einen anderen Namen? Kein Problem, passen Sie einfach die Annotation an:

@Cleanup("dispose") JFrame mainFrame = new JFrame("Main Window");

9. Kommentieren Sie Ihre Klasse, um einen Logger zu erhalten

Viele von uns fügen ihrem Code sparsam Protokollierungsanweisungen hinzu, indem sie eine Instanz vonLogger aus dem Framework ihrer Wahl erstellen. Sagen wir, SLF4J:

public class ApiClientConfiguration {

    private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class);

    // LOG.debug(), LOG.info(), ...

}

Dies ist ein so verbreitetes Muster, dass Lombok-Entwickler es für uns vereinfacht haben:

@Slf4j // or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j
public class ApiClientConfiguration {

    // log.debug(), log.info(), ...

}

Vielelogging frameworks werden unterstützt und natürlich können Sie den Instanznamen, das Thema usw. anpassen.

10. Schreiben Sie Thread-sicherere Methoden

In Java können Sie das Schlüsselwortsynchronizedverwenden, um kritische Abschnitte zu implementieren. Dies ist jedoch kein 100{}icherer Ansatz: Möglicherweise wird auch anderer Clientcode auf Ihrer Instanz synchronisiert, was zu unerwarteten Deadlocks führen kann.

Hier kommt@Synchronized ins Spiel: Kommentieren Sie Ihre Methoden (sowohl instanziell als auch statisch) damit, und Sie erhalten ein automatisch generiertes privates, nicht belichtetes Feld, das Ihre Implementierung zum Sperren verwendet:

@Synchronized
public /* better than: synchronized */ void putValueInCache(String key, Object value) {
    // whatever here will be thread-safe code
}

11. Objektzusammensetzung automatisieren

Java verfügt nicht über Sprachniveaukonstrukte, um einen Ansatz zur "Bevorzugung der Kompositionsvererbung" auszugleichen. Andere Sprachen haben integrierte Konzepte wieTraits oderMixins, um dies zu erreichen.

Lomboks@Delegate sind sehr praktisch, wenn Sie dieses Programmiermuster verwenden möchten. Betrachten wir ein Beispiel:

  • Wir möchten, dassUsers undCustomers einige gemeinsame Attribute für die Benennung und Telefonnummer haben

  • Für diese Felder definieren wir sowohl eine Interface- als auch eine Adapterklasse

  • Wir lassen unsere Modelle die Schnittstelle und@Delegate in ihren Adapter implementieren, effektivcomposing mit unseren Kontaktinformationen

Definieren wir zunächst eine Schnittstelle:

public interface HasContactInformation {

    String getFirstName();
    void setFirstName(String firstName);

    String getFullName();

    String getLastName();
    void setLastName(String lastName);

    String getPhoneNr();
    void setPhoneNr(String phoneNr);

}

Und jetzt ein Adapter alssupport-Klasse:

@Data
public class ContactInformationSupport implements HasContactInformation {

    private String firstName;
    private String lastName;
    private String phoneNr;

    @Override
    public String getFullName() {
        return getFirstName() + " " + getLastName();
    }
}

Der interessante Teil kommt jetzt, sehen Sie, wie einfach es ist, Kontaktinformationen in beiden Modellklassen zu erstellen:

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

}

Der Fall fürCustomer wäre so ähnlich, dass wir die Stichprobe der Kürze halber weglassen würden.

12. Lombok zurückrollen?

Kurze Antwort: Eigentlich gar nicht.

Möglicherweise haben Sie die Befürchtung, dass Sie Lombok in einem Ihrer Projekte verwenden, diese Entscheidung aber später rückgängig machen möchten. Sie hätten dann vielleicht eine große Anzahl von Klassen, die dafür kommentiert sind ... was könnten Sie tun?

Ich habe es nie wirklich bereut, aber wer weiß für Sie, Ihr Team oder Ihre Organisation. In diesen Fällen sind Sie dank des Toolsdelombokaus demselben Projekt abgesichert.

Mitdelombok-ing Ihres Codes erhalten Sie automatisch generierten Java-Quellcode mit genau den gleichen Funktionen wie der von Lombok erstellte Bytecode. Dann können Sie einfach Ihren ursprünglichen kommentierten Code durch diese neuendelomboked-Dateien ersetzen und sind nicht mehr davon abhängig.

Dies ist etwas, das Sieintegrate in your build können, und ich habe dies in der Vergangenheit getan, um nur den generierten Code zu studieren oder Lombok in ein anderes Java-Quellcode-basiertes Tool zu integrieren.

13. Fazit

Es gibt einige andere Funktionen, die wir in diesem Artikel nicht vorgestellt haben. Ich möchte Sie dazu ermutigen, sich eingehender mitfeature overviewzu befassen, um weitere Details und Anwendungsfälle zu erhalten.

Außerdem verfügen die meisten Funktionen, die wir gezeigt haben, über eine Reihe von Anpassungsoptionen, die Sie möglicherweise als nützlich erachten, damit das Tool Dinge generiert, die Ihren Teampraktiken für die Benennung usw. am besten entsprechen. Die verfügbaren integriertenconfiguration system könnten Ihnen auch dabei helfen.

Ich hoffe, Sie haben die Motivation gefunden, Lombok die Möglichkeit zu geben, in Ihr Java-Entwicklungs-Toolset einzusteigen. Probieren Sie es aus und steigern Sie Ihre Produktivität!

Der Beispielcode befindet sich inGitHub project.