Lombok讓Java再次變得酷酷的 - Grubhub Bytes

banq發表於2019-02-02

在Grubhub,我們在大多數後端程式設計中都使用Java。Java是經過實踐考驗的語言,在過去的20年裡證明了它的速度和可靠性。雖然我們已經使用Java多年,但最近它已經開始顯示它歲數大了。
雖然Java是最流行的JVM語言之一,但它並不是唯一的。在過去幾年中,它面臨著一些挑戰者,如Scala,Clojure和Kotlin,它們提供了新功能和簡化的語言功能。簡而言之,它們可以讓您使用更簡潔的程式碼來完成更多工作。
JVM生態系統中的這一創新令人興奮。更多的競爭意味著Java被迫改變以保持競爭力。自Java 8(Valhalla,Local-Variable Type Inference,Loom)以來,新的六個月釋出計劃和幾個JEP(JDK增強提議)證明了Java在未來幾年仍將是一種競爭性語言。
但是,Java語言的大小規模意味著開發速度比我們想要的要慢,更不用說Java不惜一切代價保持向後相容性的強烈願望。透過任何軟體工程工作,功能都需要優先考慮,因此我們想要完成一個功能可能需要很長時間。
與此同時,Grubhub利用Lombok專案獲得了簡化和改進的Java 。Lombok是一個編譯器外掛,它為Java新增了新的“關鍵字”,並將註釋轉換為Java程式碼,減少了工程繁忙工作並提供了一些額外的功能。

設定Lombok
我們Grubhub一直在尋求改進我們的軟體生命週期,但每個新工具和流程都需要在採用之前考慮成本。幸運的是,新增Lombok就像在gradle檔案中新增幾行一樣簡單。
Lombok是一個編譯器外掛,因為它在編譯器處理它們之前將原始碼中的註釋轉換為Java語句 - 在執行時不需要提供lombok依賴項,因此新增Lombok不會增加構建工件的大小。因此,您需要下載Lombok並將其新增到您的構建工具中。要使用Gradle 設定Lombok(它也適用於Maven),請將此塊新增到build.gradle檔案中:

plugins {
    id 'io.franzbecker.gradle-lombok' version '1.14'
    id 'java'
}
repositories {
    jcenter() // or Maven central, required for Lombok dependency
}
lombok {
    version = '1.18.4'
    sha256 = ""
}

由於Lombok是一個編譯器外掛,我們為它編寫的原始碼實際上並不是有效的Java。因此,您還需要為你正在使用的IDE安裝外掛。幸運的是,Lombok支援所有主要的Java IDE。如果沒有外掛,IDE不知道如何解析程式碼。IDE整合是無縫的。諸如“show usages”和“go to implementation”等功能會按預期工作,帶您進入相關領域/類。

Lombok在行動
瞭解Lombok的最佳方式是看它如何實現。讓我們深入研究一些如何將Lombok應用於Java應用程式的常見方面的示例。

1.為POJO增添趣味
我們使用普通的舊Java物件(PO​​JO)將資料與處理分開,使我們的程式碼更易於閱讀並簡化網路有效負載。簡單的P​​OJO有一些私有欄位和相應的getter和setter。需要編寫很多樣板程式碼。Lombok有助於使POJO更有用,更靈活,更有條理,而無需編寫更多其他程式碼。使用Lombok,我們可以使用@Data註釋簡化最基本的POJO :

@Data
public class User {
  private UUID userId;
  private String email;
}


@Data註釋實際上只是綜合了多個Lombok註釋的方便標註。
  • @ToString會自動生成物件的toString()方法。
  • @EqualsAndHashCode生成equals和hashCode方法的實現,預設情況下,它們使用所有非靜態,非瞬態欄位,但是可配置。
  • @Getter[url=https://projectlombok.org/features/GetterSetter]/[/url]@Setter為私有欄位生成getter和setter方法。
  • @RequiredArgsConstructor生成帶有所需引數的建構函式,所需引數是final欄位,和帶註釋@NonNull的欄位(稍後將詳細介紹)。

這一個註釋簡單而優雅地涵蓋了許多常見用例。但一個POJO並不總是足夠的。一個@Data類是完全可變的,它一旦被濫用,會增加應用的複雜性和限制併發使用,這兩種都會傷害應用的壽命。

讓我們重新審視我們的User類,使其不可變,並新增一些其他有用的Lombok註釋。

@Value
@Builder(toBuilder = true)
public class User {
  @NonNull 
  UUID userId;
  @NonNull 
  String email;
  @Singular
  Set<String> favoriteFoods;
  @NonNull
  @Builder.Default
  String avatar = “default.png”;
}


不變性所需要的只是@Value註釋,@Value類似於@Data,但是除了:所有欄位都預設為private和final,並且不生成setter。(banq注,可用來實現DDD中值物件)。這些註釋使@Value物件保持有效地不變性。由於欄位都是最終的,因此不能有無引數的建構函式。相反,Lombok用於@AllArgsConstructor生成所有引數建構函式。這導致了一個功能完備,有效不可變的物件。

但是,如果只能使用all args建構函式建立物件,那麼不可變是不太有用的。約書亞·布洛克的《有效的Java》說:當面臨著有許多建構函式的引數時應該使用Builder建設模式。這就是Lombok的@Builder使用的地方:能自動生成一個內部構建器類,然後呼叫:

User user = User.builder()
  .userId(UUID.random())
  .email(“grubhub@grubhub.com”)
  .favoriteFood(“burritos”)
  .favoriteFood(“dosas”)
  .build()


使用Lombok生成的構建器可以輕鬆建立具有多個引數的物件,並在將來新增新欄位。靜態構建器方法返回構建器例項以設定物件的所有屬性。設定後,build()在構建器上呼叫以返回例項。

@NonNull註釋可被用來斷言這些欄位不為空時,物件被例項化,丟擲一個NullPointerException空指標時。請注意頭像avatar欄位是如何註釋@NonNull但未在構建器中設定,這是因為@Builder.Default註釋表示預設使用“default.png”。

還要注意構建器的使用方式favoriteFood,即物件上屬性的單數singular 名稱。當@Singular註釋放在集合屬性上時,Lombok會建立特殊的構建器方法來單獨向該集合新增專案,而不是一次新增整個集合。這對於測試來說特別好,因為在Java中建立小型集合並不簡潔。

最後,該toBuilder = true設定會新增在一個例項方法toBuilder(),該方法建立一個使用該例項的所有值填充的構建器物件。這樣可以輕鬆建立一個預先填充原始例項中所有值的新例項,並僅更改所需的欄位。這對於@Value類特別有用,因為欄位是不可變的。

透過一些註釋,您可以進一步配置專門的setter功能:
@Wither為每個屬性建立 withX”方法:這個方法接受一個值,並返回當前例項的克隆,且更新一個欄位值。
@Accessors允許您配置自動建立的setter。預設情況下,它允許將setter連結起來,就像構建器一樣,有返回而不是void。它還有一個引數:fluent=true,它刪除了getter和setter上的“get”和“set”字首約定。如果需要更多自定義,這可以是一個有用的@Builder替代。

如果Lombok實現不適合您的用例(並且您已經檢視了註釋的修飾符),那麼您始終可以手動編寫自己的實現。例如,如果您有一個@Data類但是一個getter需要自定義邏輯,那麼只需實現該getter。Lombok將看到已經提供了一個實現,並且不會使用自動生成的實現覆蓋它。

只需幾個簡單的註釋,最初的使用者POJO已經獲得了許多豐富的功能,使其更易於使用,而不會給我們的工程師帶來太多負擔或增加開發的時間或成本。

刪除元件樣板程式碼
Lombok不僅在POJO中有用 - 它可以應用於應用程式的任何層。Lombok的以下用法在應用程式的元件類中特別有用,例如控制器,服務和DAO(資料訪問物件)。
記錄是每個軟體的基準要求,作為關鍵的調查工具。任何正在做有意義的工作的類應該是記錄資訊。由於日誌記錄是一個貫穿各領域的問題,因此在每個類中宣告一個私有靜態最終記錄器將成為即時模板。Lombok將此樣板簡化為一個註釋,該註釋自動定義並例項化具有正確類名的記錄器。根據您使用的日誌記錄框架,有一些不同的註釋。

@Slf4j // also: @CommonsLog @Flogger @JBossLog @Log @Log4j @Log4j2 @XSlf4j
public class UserService {
  // created automatically
  // private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserService.class);
}

在宣告瞭logger之後,接下來讓我們新增我們的依賴項:

@Slf4j
@RequiredArgsConstructor
@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
public class UserService {
  @NonNull UserDao userDao;
}


@FieldDefaults註釋增加了最終final的和私有private修飾符的所有欄位。在@RequiredArgsConstructor建立接受並設定一個建構函式UserDao例項。該@NonNull註釋在建構函式中增加了一個檢查,如果UserDao例項為null就丟擲一個NullPointerException。

但等等,還有更多!
有很多方法可以使用Lombok。以上兩節主要針對特定​​用例,但Lombok可以在許多方面使開發更容易。以下是一些小例子,展示瞭如何更有效地利用Lombok。

儘管Java 9引入了var關鍵字,var但仍可以重新分配。Lombok提供了一個val關鍵字,它可以從var中斷,提供本地最終型別推斷變數。

// final Map map = new HashMap<Integer, String>();
val map = new HashMap<Integer, String>();


有些類只具有純靜態函式,而且從不打算初始化。宣告丟擲異常的私有建構函式是阻止它被例項化的一種方式。Lombok在其@UtilityClass註釋中編寫了該模式,該註釋建立了一個私有建構函式,它丟擲異常,使類成為final,並使所有方法都是靜態的。

@UtilityClass
// will be made final
public class UtilityClass {
  // will be made static
  private final int GRUBHUB = “ GRUBHUB”;

  // autogenerated by Lombok
  // private UtilityClass() {
  //   throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated");
  //}

  // will be made static
  public void append(String input) {
    return input + GRUBHUB;
  }
}


對Java的常見批評是建立冗長的丟擲異常,Lombok有一個註釋,可以刪除那些討厭的關鍵詞:@SneakyThrows。正如您所料,實施非常狡猾。它不會吞下異常,也不會將異常包裝成一個RuntimeException。相反,它依賴於以下事實:在執行時,JVM不會檢查已檢查的異常的一致性。只有javac這樣做。因此,Lombok使用位元組碼轉換在編譯時選擇退出此檢查。結果,異常就變成了一段可執行的程式碼。

public class SneakyThrows {

    @SneakyThrows
    public void sneakyThrow() {
        throw new Exception();
    }

}


並排比較
沒有什麼能比看到Lombok並行比較時顯著地節省了程式碼。IDE外掛提供了一個“de-lombok”函式,可將大多數Lombok註釋轉換為近似的本機Java程式碼(@NonNull註釋不會轉換)。安裝了Lombok外掛的任何IDE都允許您將大多數註釋轉換為本機Java程式碼(並再次返回)。讓我們回到我們的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”;
}

Lombok類只有13條簡單易讀的描述性程式碼行。但是在執行de-lombok之後,這個類變成了一百多行的樣板,沒有人願意看到,但每個人都想要!

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 + ")";
       }
   }
}

我們可以為UserService做同樣的事情。

@Slf4j
@RequiredArgsConstructor
@FieldDefaults(makeFinal=true, level=AccessLevel.PRIVATE)
public class UserService {
  @NonNull UserDao userDao;
}


將導致大約這個Java程式碼。

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

 }


Grubhub有超過一百種服務來滿足業務需求。我們採用了其中一種服務並執行了Lombok IntelliJ外掛的“de-lombok”功能,以檢視使用Lombok儲存了多少行程式碼。結果是大約180個檔案的更改,導致大約18,000個額外的程式碼行和800個Lombok使用的刪除。這是18,000行自動生成,標準化和經過實戰考驗的程式碼行!平均而言,每行Lombok程式碼都節省了23行Java程式碼。有了這樣的影響,很難想象沒有Lombok就使用Java。

總結
Lombok是一種很好的方式,可以激發工程師的新語言功能,而無需在整個組織內付出太多努力。將外掛應用於專案當然比使用現有程式碼訓練所有工程師使用新語言和埠更容易。lombok可能沒有一切,但它確實提供了足夠的開箱即用,對工程經驗產生了明顯的影響。
Lombok的另一個好處是它使我們的程式碼庫保持一致。憑藉遍佈全球的一百多種不同服務和分散式團隊,使我們的程式碼庫保持一致,可以更輕鬆地擴充套件團隊並減少啟動新專案時上下文切換的負擔。自6以來,Lombok與任何版本的Java都相關,因此我們可以指望它在所有專案中都可用。
Lombok對Grubhub的意義遠遠超過了閃亮的新功能。畢竟,Lombok做的任何事情都可以手工編寫。如圖所示,Lombok簡化了程式碼庫的無聊部分,而不會影響業務邏輯。這使我們專注於為Grubhub提供最大價值的工作,並且是我們工程師最感興趣的工作。編寫者,審閱者和維護者浪費時間讓程式碼庫的這麼大部分成為單調的樣板程式碼。此外,由於此程式碼不再手動編寫,因此它消除了所有型別的拼寫錯誤。自動生成的好處與強大的功能相結合,@NonNull減少了漏洞的可能性,並使我們的工程工作專注於為您提供服務!

(banq注:Lombok提高了效率 節省了程式碼,防止了繁瑣程式碼,但是在易理解性或可閱讀性方面有些難度,特別是使用IDE進行程式碼呼叫路徑跟蹤方面,跟蹤到setXX方法,就無法進入setXX 方法,因為沒有這個方法,這個方法是lombok自動生成的,你的跟蹤路徑就斷了。)

相關文章