
Wir bei Grubhub verwenden Java im gesamten Backend. Dies ist eine bewährte Sprache, die sich in den letzten 20 Jahren als schnell und zuverlässig erwiesen hat. Aber im Laufe der Jahre begann sich das Alter des "alten Mannes" immer noch zu beeinflussen.
Java ist
eine der beliebtesten JVM-Sprachen , aber nicht die einzige. In den letzten Jahren hat er mit Scala, Clojure und Kotlin konkurriert, die neue Funktionen und optimierte Sprachfunktionen bieten. Kurz gesagt, Sie können mit präziserem Code mehr erreichen.
Diese Innovationen im JVM-Ökosystem sind sehr interessant. Aufgrund des Wettbewerbs ist Java gezwungen, Änderungen vorzunehmen, um wettbewerbsfähig zu bleiben. Der neue sechsmonatige Release-Zeitplan und mehrere JEP (JDK-Erweiterungsvorschläge) in Java 8 (Valhalla, Local-Variable Type Inference, Loom) sind ein Beweis dafür, dass Java jahrelang eine wettbewerbsfähige Sprache bleiben wird.
Aufgrund der Größe und des Umfangs von Java schreitet die Entwicklung jedoch langsamer voran, als wir es uns wünschen, ganz zu schweigen von dem starken Wunsch, die Abwärtskompatibilität um jeden Preis aufrechtzuerhalten. Bei jeder Entwicklung sollte die erste Priorität Funktionen sein, aber hier wurden die notwendigen Funktionen zu lange, wenn überhaupt, in der Sprache entwickelt. Daher nutzen wir bei Grubhub Project Lombok, um Java jetzt zu optimieren und zu verbessern. Das Lombok-Projekt ist ein Compiler-Plugin, das Java neue „Schlüsselwörter“ hinzufügt und Anmerkungen in Java-Code umwandelt, wodurch der Entwicklungsaufwand verringert und einige zusätzliche Funktionen bereitgestellt werden.
Konfigurieren Sie Lombok
Grubhub ist stets bemüht, den Software-Lebenszyklus zu verbessern, aber jedes neue Tool und jeder neue Prozess ist mit Kosten verbunden. Um Lombok zu verbinden, fügen Sie der Gradle-Datei zum Glück nur ein paar Zeilen hinzu.
Lombok konvertiert Anmerkungen im Quellcode in Java-Anweisungen, bevor der Compiler sie verarbeitet: Die
lombok
Abhängigkeit
lombok
keine Laufzeit, sodass die Verwendung des Plugins die Assemblygröße nicht erhöht. Um
Lombok mit Gradle zu
konfigurieren (es funktioniert auch mit Maven), fügen Sie der Datei
build.gradle einfach die
folgenden Zeilen
hinzu :
plugins { id 'io.franzbecker.gradle-lombok' version '1.14' id 'java' } repositories { jcenter()
Mit Lombok ist unser Quellcode
kein gültiger Java-Code. Daher müssen Sie ein Plugin für die IDE installieren, da die Entwicklungsumgebung sonst nicht versteht, worum es geht. Lombok unterstützt alle wichtigen Java-IDEs. Nahtlose Integration. Alle Funktionen wie "show use" und "go to implementation" funktionieren weiterhin wie zuvor und verschieben Sie in das entsprechende Feld / die entsprechende Klasse.
Lombok in Aktion
Der beste Weg, um Lombok kennenzulernen, ist es, es in Aktion zu sehen. Betrachten Sie einige typische Beispiele.
Erleben Sie das POJO-Objekt erneut
Mit „guten alten Java-Objekten“ (POJOs) trennen wir Daten von der Verarbeitung, um das Lesen von Code zu vereinfachen und Netzwerkübertragungen zu optimieren. Ein einfaches POJO verfügt über mehrere private Felder sowie entsprechende Getter und Setter. Sie erledigen den Job, erfordern aber viel Code für das Boilerplate.
Lombok hilft dabei, POJO flexibler und strukturierter ohne zusätzlichen Code zu verwenden. So vereinfachen wir das zugrunde liegende POJO mit der Annotation
@Data
:
@Data public class User { private UUID userId; private String email; }
@Data
ist nur eine praktische Annotation, die mehrere Lombok-Annotationen gleichzeitig anwendet.
@ToString
generiert eine Implementierung für die toString()
-Methode, die aus einer übersichtlichen Darstellung des Objekts besteht: dem Klassennamen, allen Feldern und ihren Werten.
@EqualsAndHashCode
generiert Implementierungen von equals
und hashCode
, die standardmäßig nicht statische und nicht stationäre Felder verwenden, aber anpassbar sind.
@Getter / @Setter
generiert Getter und Setter für private Felder.
@RequiredArgsConstructor
erstellt einen Konstruktor mit den erforderlichen Argumenten, wobei die endgültigen Felder und Felder mit der Annotation @NonNull
(mehr dazu weiter unten).
Diese Anmerkung allein deckt einfach und elegant viele typische Anwendungsfälle ab. POJO deckt jedoch nicht immer die erforderlichen Funktionen ab.
@Data
ist eine vollständig modifizierbare Klasse, deren Missbrauch die Komplexität erhöhen und die Parallelität begrenzen kann, was sich negativ auf die Überlebensfähigkeit der Anwendung auswirkt.
Es gibt eine andere Lösung. Kehren wir zu unserer
User
, machen Sie sie unveränderlich und fügen Sie einige andere nützliche Anmerkungen hinzu.
@Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; }
Die Annotation
@Value
ähnelt
@Data
außer dass alle Felder standardmäßig privat und endgültig sind und keine Setter erstellt werden.
@Value
werden
@Value
Objekte sofort unveränderlich. Da alle Felder endgültig sind, gibt es keinen Argumentkonstruktor. Stattdessen verwendet Lombok den
@AllArgsConstructor
. Das Ergebnis ist ein voll funktionsfähiges, unveränderliches Objekt.
Unveränderlichkeit ist jedoch nicht sehr nützlich, wenn Sie nur ein Objekt mit dem Konstruktor all-args erstellen müssen. Wie Joshua Bloch in seinem Buch Effective Java Programming erklärt, sollten Sie Builder verwenden, wenn Sie über eine große Anzahl von Designerparametern verfügen. Hier kommt die
@Builder
Klasse ins
@Builder
, die automatisch die innere Klasse des Builders generiert:
User user = User.builder() .userId(UUID.random()) .email(“grubhub@grubhub.com”) .favoriteFood(“burritos”) .favoriteFood(“dosas”) .build()
Das Generieren eines Builders erleichtert das Erstellen von Objekten mit einer großen Anzahl von Argumenten und das Hinzufügen neuer Felder in der Zukunft. Die statische Methode gibt eine Instanz des Builders zurück, um alle Eigenschaften des Objekts festzulegen. Danach gibt der Aufruf von
build()
die Instanz zurück.
Die
@NonNull
kann verwendet werden, um
@NonNull
, dass diese Felder beim Erstellen einer Instanz des Objekts nicht null sind. Andernfalls wird eine
NullPointerException
ausgelöst. Beachten Sie, dass das Avatar-Feld mit
@NonNull
kommentiert, aber nicht festgelegt ist. Tatsache ist, dass die Annotation
@Builder.Default
standardmäßig auf
@Builder.Default
verweist.
Beachten Sie auch, wie der Builder FavoritFood verwendet, den einzigen Eigenschaftsnamen in unserem Objekt. Beim Platzieren von
@Singular
Annotationen in einer Sammlungseigenschaft erstellt Lombok spezielle Builder-Methoden, um der Sammlung Elemente einzeln hinzuzufügen und nicht die gesamte Sammlung gleichzeitig hinzuzufügen. Dies ist besonders gut für Tests geeignet, da die Möglichkeiten zum Erstellen kleiner Sammlungen in Java nicht einfach und schnell genannt werden können.
Schließlich fügt der Parameter
toBuilder = true
die
toBuilder()
hinzu, mit der ein Builder-Objekt erstellt wird, das mit allen Werten dieser Instanz gefüllt ist. Es ist so einfach, eine neue Instanz zu erstellen, die mit allen Werten aus dem Original gefüllt ist, sodass nur die erforderlichen Felder geändert werden müssen. Dies ist besonders nützlich für
@Value
Klassen, da die Felder unveränderlich sind.
Einige Hinweise passen die speziellen Funktionen des Setters weiter an.
@Wither
erstellt
@Wither
Methoden für jede Eigenschaft. Bei der Eingabe der Wert, bei der Ausgabe der Klon der Instanz mit dem aktualisierten Wert eines Feldes.
@Accessors
können Sie automatisch erstellte Setter konfigurieren. Der Parameter fluent
fluent=true
deaktiviert die Get- und Set-Konventionen für Getter und Setter. In bestimmten Situationen kann dies ein nützlicher Ersatz für
@Builder
.
Wenn die Lombok-Implementierung nicht für Ihre Aufgabe geeignet ist (und Sie sich die Anmerkungsmodifikatoren angesehen haben), können Sie jederzeit einfach Ihre eigene Implementierung erstellen. Wenn Sie beispielsweise die
@Data
Klasse haben, ein Getter jedoch eine benutzerdefinierte Logik benötigt, implementieren Sie einfach diesen Getter. Lombok erkennt, dass die Implementierung bereits bereitgestellt ist, und überschreibt sie nicht mit der automatisch erstellten Implementierung.
Mit nur wenigen einfachen Anmerkungen hat das Basis-POJO so viele umfangreiche Funktionen erhalten, die seine Verwendung vereinfachen, ohne die Arbeit von uns Ingenieuren zu belasten, ohne Zeit zu verschwenden oder die Entwicklungskosten zu erhöhen.
Vorlagencode entfernen
Lombok ist nicht nur für POJO nützlich: Es kann auf jeder Ebene der Anwendung angewendet werden. Die folgenden Verwendungen von Lombok sind besonders nützlich in Komponentenklassen wie Controllern, Diensten und DAOs (Datenzugriffsobjekten).
Die Protokollierung ist eine Grundvoraussetzung für alle Teile des Programms. Jede Klasse, die sinnvolle Arbeit leistet, sollte ein Protokoll schreiben. Somit wird der Standardlogger zu einer Vorlage für jede Klasse. Lombok vereinfacht diese Vorlage zu einer einzigen Anmerkung, die einen Logger mit dem richtigen Klassennamen automatisch identifiziert und instanziiert. Abhängig von der Struktur des Journals gibt es verschiedene Anmerkungen.
@Slf4j
Fügen Sie nach dem Deklarieren des Loggers unsere Abhängigkeiten hinzu:
@Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; }
Die
@FieldDefaults
fügt allen Feldern endgültige und private Modifikatoren hinzu.
@RequiredArgsConstructor
erstellt einen Konstruktor, der eine Instanz von
UserDao
. Die
@NonNull
fügt eine Validierung im Konstruktor hinzu und
NullPointerException
UserDao
NullPointerException
wenn die
UserDao
Instanz Null ist.
Aber warte, das ist noch nicht alles!
Es gibt noch viele weitere Situationen, in denen Lombok sein Bestes gibt. Die vorherigen Abschnitte zeigten spezifische Beispiele, aber Lombok kann die Entwicklung in vielen Bereichen erleichtern. Hier sind einige kleine Beispiele, wie Sie es effizienter nutzen können.
Obwohl das Schlüsselwort
var
in Java 9 vorkam, kann die Variable dennoch neu zugewiesen werden. Lombok hat das Schlüsselwort
val
, das den endgültigen Typ einer lokalen Variablen druckt.
Einige Klassen mit rein statischen Funktionen sollen nicht initialisiert werden. Eine Möglichkeit, die Instanziierung zu verhindern, besteht darin, einen privaten Konstruktor zu deklarieren, der eine Ausnahme auslöst. Lombok hat diese Vorlage in der Annotation
@UtilityClass
kodifiziert. Es generiert einen privaten Konstruktor, der eine Ausnahme auslöst, schließlich die Klasse ausgibt und alle Methoden statisch macht.
@UtilityClass
Java wird aufgrund überprüfter Ausnahmen häufig wegen seiner Ausführlichkeit kritisiert. Eine separate Lombok-Annotation behebt sie:
@SneakyThrows
. Wie erwartet ist die Implementierung ziemlich schwierig. Es werden keine Ausnahmen
RuntimeException
oder Ausnahmen in eine
RuntimeException
. Stattdessen wird die Tatsache berücksichtigt, dass die JVM die Konsistenz der überprüften Ausnahmen zur Laufzeit nicht überprüft. Das macht nur Javac. Daher verwendet Lombok zur Kompilierungszeit die Bytecode-Konvertierung, um diese Prüfung zu deaktivieren. Das Ergebnis ist ausführbarer Code.
public class SneakyThrows { @SneakyThrows public void sneakyThrow() { throw new Exception(); } }
Nebeneinander Vergleich
Direkte Vergleiche zeigen am besten, wie viel Code Lombok spart. Das IDE-Plugin verfügt über eine De-Lombok-Funktion, die die meisten Lombok-Annotationen ungefähr in nativen Java-Code konvertiert (
@NonNull
Annotationen
@NonNull
nicht konvertiert). Somit kann jede IDE mit installiertem Plugin die meisten Anmerkungen in nativen Java-Code konvertieren und umgekehrt. Zurück zu unserer
User
.
@Value @Builder(toBuilder = true) public class User { @NonNull UUID userId; @NonNull String email; @Singular Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = “default.png”; }
Die Lombok-Klasse besteht nur aus 13 einfachen, lesbaren und verständlichen Zeilen. Aber nach dem Ausführen von De-Lombok verwandelt sich die Klasse in mehr als hundert Zeilen Boilerplate-Code!
public class User { @NonNull UUID userId; @NonNull String email; Set<String> favoriteFoods; @NonNull @Builder.Default String avatar = "default.png"; @java.beans.ConstructorProperties({"userId", "email", "favoriteFoods", "avatar"}) User(UUID userId, String email, Set<String> favoriteFoods, String avatar) { this.userId = userId; this.email = email; this.favoriteFoods = favoriteFoods; this.avatar = avatar; } public static UserBuilder builder() { return new UserBuilder(); } @NonNull public UUID getUserId() { return this.userId; } @NonNull public String getEmail() { return this.email; } public Set<String> getFavoriteFoods() { return this.favoriteFoods; } @NonNull public String getAvatar() { return this.avatar; } public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof User)) return false; final User other = (User) o; final Object this$userId = this.getUserId(); final Object other$userId = other.getUserId(); if (this$userId == null ? other$userId != null : !this$userId.equals(other$userId)) return false; final Object this$email = this.getEmail(); final Object other$email = other.getEmail(); if (this$email == null ? other$email != null : !this$email.equals(other$email)) return false; final Object this$favoriteFoods = this.getFavoriteFoods(); final Object other$favoriteFoods = other.getFavoriteFoods(); if (this$favoriteFoods == null ? other$favoriteFoods != null : !this$favoriteFoods.equals(other$favoriteFoods)) return false; final Object this$avatar = this.getAvatar(); final Object other$avatar = other.getAvatar(); if (this$avatar == null ? other$avatar != null : !this$avatar.equals(other$avatar)) return false; return true; } public int hashCode() { final int PRIME = 59; int result = 1; final Object $userId = this.getUserId(); result = result * PRIME + ($userId == null ? 43 : $userId.hashCode()); final Object $email = this.getEmail(); result = result * PRIME + ($email == null ? 43 : $email.hashCode()); final Object $favoriteFoods = this.getFavoriteFoods(); result = result * PRIME + ($favoriteFoods == null ? 43 : $favoriteFoods.hashCode()); final Object $avatar = this.getAvatar(); result = result * PRIME + ($avatar == null ? 43 : $avatar.hashCode()); return result; } public String toString() { return "User(userId=" + this.getUserId() + ", email=" + this.getEmail() + ", favoriteFoods=" + this.getFavoriteFoods() + ", avatar=" + this.getAvatar() + ")"; } public UserBuilder toBuilder() { return new UserBuilder().userId(this.userId).email(this.email).favoriteFoods(this.favoriteFoods).avatar(this.avatar); } public static class UserBuilder { private UUID userId; private String email; private ArrayList<String> favoriteFoods; private String avatar; UserBuilder() { } public User.UserBuilder userId(UUID userId) { this.userId = userId; return this; } public User.UserBuilder email(String email) { this.email = email; return this; } public User.UserBuilder favoriteFood(String favoriteFood) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.add(favoriteFood); return this; } public User.UserBuilder favoriteFoods(Collection<? extends String> favoriteFoods) { if (this.favoriteFoods == null) this.favoriteFoods = new ArrayList<String>(); this.favoriteFoods.addAll(favoriteFoods); return this; } public User.UserBuilder clearFavoriteFoods() { if (this.favoriteFoods != null) this.favoriteFoods.clear(); return this; } public User.UserBuilder avatar(String avatar) { this.avatar = avatar; return this; } public User build() { Set<String> favoriteFoods; switch (this.favoriteFoods == null ? 0 : this.favoriteFoods.size()) { case 0: favoriteFoods = java.util.Collections.emptySet(); break; case 1: favoriteFoods = java.util.Collections.singleton(this.favoriteFoods.get(0)); break; default: favoriteFoods = new java.util.LinkedHashSet<String>(this.favoriteFoods.size() < 1073741824 ? 1 + this.favoriteFoods.size() + (this.favoriteFoods.size() - 3) / 3 : Integer.MAX_VALUE); favoriteFoods.addAll(this.favoriteFoods); favoriteFoods = java.util.Collections.unmodifiableSet(favoriteFoods); } return new User(userId, email, favoriteFoods, avatar); } public String toString() { return "User.UserBuilder(userId=" + this.userId + ", email=" + this.email + ", favoriteFoods=" + this.favoriteFoods + ", avatar=" + this.avatar + ")"; } } }
Wir werden dasselbe für die
UserService
Klasse tun.
@Slf4j @RequiredArgsConstructor @FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE) public class UserService { @NonNull UserDao userDao; }
Hier ist ein Beispielgegenstück in Standard-Java-Code.
public class UserService { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class); private final UserDao userDao; @java.beans.ConstructorProperties({"userDao"}) public UserService(UserDao userDao) { if (userDao == null) { throw new NullPointerException("userDao is marked @NonNull but is null") } this.userDao = userDao; } }
Effektbewertung
Grubhub hat über hundert Lebensmittellieferdienste. Wir haben eine davon genommen und die Funktion „De-Lombok“ im Lombok IntelliJ-Plugin gestartet. Infolgedessen änderten sich ungefähr 180 Dateien, und die Codebasis wuchs um ungefähr 18.000 Codezeilen, nachdem 800 Fälle von Lombok entfernt wurden. Im Durchschnitt speichert jede Lombok-Zeile 23 Java-Zeilen. Mit diesem Effekt ist Java ohne Lombok kaum vorstellbar.
Zusammenfassung
Lombok ist ein großartiger Helfer, der neue Sprachfunktionen implementiert, ohne dass der Entwickler viel Aufwand betreiben muss. Natürlich ist es einfacher, das Plugin zu installieren, als alle Ingenieure in einer neuen Sprache zu schulen und vorhandenen Code zu portieren. Lombok ist nicht allmächtig, aber sofort einsatzbereit genug, um wirklich bei der Arbeit zu helfen.
Ein weiterer Vorteil von Lombok besteht darin, dass konsistente Codebasen beibehalten werden. Wir haben mehr als hundert verschiedene Dienste und ein verteiltes Team auf der ganzen Welt. Die Kohärenz der Codebasen erleichtert die Skalierung von Teams und verringert die Belastung durch das Wechseln von Kontexten beim Starten eines neuen Projekts. Lombok funktioniert für jede Version seit Java 6, sodass wir in allen Projekten auf die Verfügbarkeit zählen können.
Für Grubhub ist dies mehr als nur eine neue Funktion. Am Ende kann der gesamte Code manuell geschrieben werden. Aber Lombok vereinfacht die langweiligen Teile der Codebasis, ohne die Geschäftslogik zu beeinträchtigen. So können Sie sich auf Dinge konzentrieren, die für das Unternehmen wirklich wichtig und für unsere Entwickler am interessantesten sind. Monton-Vorlagencode ist Zeitverschwendung für Programmierer, Prüfer und Betreuer. Da dieser Code nicht mehr manuell geschrieben wird, werden außerdem ganze Klassen von Tippfehlern entfernt. Die Vorteile der automatischen
@NonNull
Kombination mit der Leistung von
@NonNull
verringern die
@NonNull
und unterstützen unsere Entwicklung, die darauf abzielt, Lebensmittel an Ihren Tisch zu liefern!