Einführung in das Projekt Lombok

1. Vermeiden Sie sich wiederholenden Code

Java ist eine großartige Sprache, aber manchmal wird es zu ausführlich für Dinge, die Sie in Ihrem Code tun müssen, um allgemeine Aufgaben zu erledigen oder einige Framework-Praktiken einzuhalten. Diese bringen sehr oft keinen wirklichen Wert für die geschäftliche Seite Ihrer Programme - und hier ist Lombok hier, um Ihr Leben glücklicher und produktiver zu machen.

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 Ihre .class- Dateien zu generieren.

Es ist sehr einfach, es in Ihre Builds aufzunehmen, unabhängig davon, welches System Sie verwenden. Ihre Projektseite enthält detaillierte Anweisungen zu den Einzelheiten. Die meisten meiner Projekte sind Maven basiert, so dass ich in der gerade der Regel ihre Abhängigkeit fallen bereitgestellt Umfang und ich bin gut zu gehen:

 ...  org.projectlombok lombok 1.18.10 provided  ... 

Suchen Sie hier nach der neuesten verfügbaren Version.

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

2. Getter / Setter, Konstruktoren - also repetitiv

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 „Eigenschaften“.

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 jedoch in Ihren Quellen gespeichert 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: „Ein Benutzer hat zuerst und Nachnamen und Alter. "

Lassen Sie uns jetzt diese Klasse Lombok-ize :

@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 Annotationen @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 gesamte Klassencode ist. Ich lasse nichts aus, im Gegensatz zu der obigen Version mit dem Kommentar // getters and setters . Für eine Klasse mit drei relevanten Attributen bedeutet dies eine erhebliche Codeeinsparung!

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

Was ist, wenn Sie die Sichtbarkeit einiger Eigenschaften verfeinern möchten? Zum Beispiel möchte ich das ID- Feld-Modifikator- Paket meiner Entitäten sichtbar oder geschützt halten , da erwartet wird, dass sie gelesen, aber nicht explizit durch den Anwendungscode festgelegt werden. Verwenden Sie einfach einen feinkörnigeren @Setter für dieses spezielle Feld:

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

3. Lazy Getter

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

Nehmen wir zum Beispiel an, 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 speicherinterne Lesevorgänge in der Anwendung zu ermöglichen. Dies erspart der Anwendung die Wiederholung des teuren Vorgangs.

Ein weiteres gängiges Muster besteht darin, diese Daten nur dann abzurufen, wenn sie zum ersten Mal benötigt werden . Mit anderen Worten, erhalten Sie die Daten nur, wenn der entsprechende Getter zum ersten Mal aufgerufen wird. Dies wird als Lazy-Loading bezeichnet .

Angenommen, diese Daten werden als Feld innerhalb einer Klasse zwischengespeichert. Die Klasse muss nun 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 Feld null ist . Aus diesem Grund nennen wir dies einen faulen Getter .

Lombok macht dies mit dem Lazy- Parameter in der oben gezeigten @ Getter- Annotation möglich .

Betrachten Sie zum Beispiel diese einfache Klasse:

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

Dadurch werden einige Transaktionen aus einer Datei in eine Map gelesen . 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 eine Getter-Methode, die den Cache aktualisiert, wenn er null war, und dann die zwischengespeicherten Daten zurückgibt :

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, dass Lombok das Datenfeld in eine AtomicReference eingeschlossen hat. Dies stellt atomare Aktualisierungen des Transaktionsfeldes sicher . Die getTransactions () Methode stellt auch sicher , die Datei zu lesen , wenn Transaktionen sind null.

Von der Verwendung des AtomicReference-Transaktionsfelds direkt innerhalb der Klasse wird abgeraten. Es wird empfohlen, die Methode getTransactions () für den Zugriff auf das Feld zu verwenden.

Wenn wir aus demselben Grund eine andere Lombok-Annotation wie ToString in derselben Klasse verwenden , wird getTransactions () verwendet, anstatt direkt auf das Feld zuzugreifen.

4. Wertklassen / DTOs

Es gibt viele Situationen, in denen wir einen Datentyp mit dem alleinigen Zweck definieren möchten, komplexe „Werte“ oder als „Datenübertragungsobjekte“ darzustellen, meistens in Form unveränderlicher Datenstrukturen, die wir einmal erstellen und niemals ä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 wieder 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 @RequiredArgsConstructor hinzu, 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 Nullfähigkeit und löst entsprechend NullPointerExceptions aus . Dies würde auch passieren, wenn die Felder nicht endgültig wären und wir @Setter für sie hinzugefügt hätten .

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.

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. Das verfügbare integrierte Konfigurationssystem kann Ihnen auch dabei helfen.

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

Der Beispielcode befindet sich im GitHub-Projekt.