🏆【Java技術專區】「開發實戰專題」Lombok外掛開發實踐必知必會操作!

李浩宇Alex 發表於 2021-08-19
Java

前言

在目前眾多程式語言中,Java 語言的表現還是搶眼,不論是企業級服務端開發,還是 Andorid 客戶端開發,都是作為開發語言的首選,甚至在大資料開發領域,Java 語言也能佔有一席之地,如Hadoop,Spark,Flink 大資料等。而作為已經誕生 24 年的 Java 相比其他語言來說,編寫起來略顯得冗長和複雜,而為了能極大提升 Java 開發的效率和程式碼簡潔性,一個 Java 庫 Lombok 就這樣誕生了。

首先我們還是看下 Lombok 官方的描述:

Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java. Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more.

從上面的說明裡我們可以初步認識一下 Lombok,一個作用於編輯器和構建工具的 Java 庫,可以對編寫的 Java 程式碼進行增強,比如說不用再寫實體類的 getter 方法,equals 方法而是自動生成,自動生成日誌輸出變數等等,減少重複模板的程式碼。大概知道了 Lombok 框架提供的功能後,接下來我們就真正使用一下 Lombok 提供的註解,看它是如何幫助我們提高書寫 Java 程式碼的簡潔性和效率的。

本文主要內容涉及如下:

  • Lombok 外掛安裝
  • Lombok 常用註解使用

環境支援:

  • JDK 8
  • SpringBoot 2.1.4
  • Maven 3.6.0

正文

安裝 Lombok

使用 Lombok 之前我們先要在所使用的 IDE 中進行整合安裝,這裡以 IDEA 為例,安裝步驟十分簡單:

  • 前往 File -> Settings -> Plugin -> Marketplace ,搜尋 Lombok

  • 選擇搜尋結果 Lombok ,點選 Install 安裝。

  • 安裝完成後重啟即可。

基於 Eclipse 的 Lombok 外掛安裝方法這裡就不詳細描述了,官方也給了對應的文件說明:https://projectlombok.org/setup/eclipse

在 IDE 安裝了Lombok 外掛後,我們就可以在 pom.xml 檔案中新增 Lombok 的依賴進行使用了。

<dependency> 
    <groupId>org.projectlombok</groupId> 
    <artifactId>lombok</artifactId> 
    <version>1.18.8</version> 
    <scope>provided</scope> 
</dependency> 

注意:pom 依賴設定 scope 為 provided,是為了讓 Lombok 庫不被打包程式序。

@Getter/@Setter

  • 通常我們編寫實體類無論多少個欄位,都要為其提供 gettersetter 方法,如下面的示例類 User.java

  • 我們常會遇到這種情況:某個實體類新增和修改某個欄位,我們都需要單獨處理調整,十分麻煩並且重複。這時候如果我們使用 Lombok 提供 @Getter/@Setter 註解就能幫我們省去 getter 和 setter 方法的維護,由 Lombok 對 User 類自動生成 gettersetter 方法,兩者最終的位元組碼時一樣的,而我們現在在 User.java 上編寫的程式碼僅僅7 行即可:

@Getter
@Setter
public class User {
    private Integer id;
    private String username;
    private String password;
}

然後用測試類 UserTests.java 測試結果如下:

public class UserTests {
    @Test
    public void test() {
        User user = new User();
        user.setUsername("one");
        user.setPassword("zxc123");
        Assert.assertEquals(user.getUsername(), "one");
        Assert.assertEquals(user.getPassword(), "zxc123");
    }
}

@Getter/@Setter 註解不僅可以使用在類上,還可以使用在欄位上,這樣就是表示針對該欄位自動生成 getter /setter 方法。

@Getter
@Setter
private String password;

這裡該註解使用在類上,還是在欄位上的區別就是,如果註解使用在類上,只針對這個類的非靜態欄位有效。

需要注意的一點是:如果 @Getter 註解修飾了 boolean 型別的變數,其生成的 getter 方法簽名是 isXXX 形式,而不是 getXXX形式。

除此之外,@Getter/@Setter 還提供訪問許可權控制的屬性 lombok.AccessLevel value(), 預設為 PUBLIC,而其他選值都是列舉型別:MODULE, PROTECTED, PACKAGE, PRIVATE

@NonNull

顧名思義,@NonNull 用於標記類中不能允許為 null 的欄位或者引數上,任何使用該欄位的地方都生成空指標判斷程式碼,若@NonNull 標記的變數為 null,丟擲 NullPointException (NPE) 異常。比如下面示例程式碼:

public class User {
    private Integer id;
    private String username;
    private String password;

    public User(Integer id, @NonNull String username, @NonNull String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }
}

使用了 @NonNull 註解之後我們可以獲取到反編譯之後的位元組碼資訊如下,這就是 Lombok 給我們生成的最終的程式碼:

public class User {
    private Integer id;
    private String username;
    private String password;

    public User(Integer id, @NonNull String username, @NonNull String password) {
        if (username == null) {
            throw new NullPointerException("username is marked non-null but is null");
        } else if (password == null) {
            throw new NullPointerException("password is marked non-null but is null");
        } else {
            this.id = id;
            this.username = username;
            this.password = password;
        }
    }
}

構造器註解

再來看下平時經常會遇見的場景,為實體類編寫構造器方法,Lombok 提供了三個不同構造器註解 @NoArgsConstructor / @AllArgsConstructor / @RequiredArgsConstructor 分別對用不同構造器方法處理方式,接下來就一一描述。

  • @NoArgsConstructor 為實體類生成無參的構造器方法

  • @AllArgsConstructor 為實體類生成除了static修飾的欄位之外帶有各引數的構造器方法。

  • @RequiredArgsConstructor 為實體類生成指定欄位的構造器方法,而這些欄位需要被 final,或者 @NonNull 修飾。

    @RequiredArgsConstructor
    public class User3 {
        private Integer id;
        private final String username;
        @NonNull
        private String password;
    }
    

    編譯成功後使用構造器方法時就是這樣的效果:User3 user3 = new User3("user3", "zxc123");

@ToString

@ToString 會給類自動生成易閱讀的 toString 方法,帶上有所非靜態欄位的屬性名稱和值,這樣就十分便於我們日常開發時進行的列印操作。

@Getter
@Setter
@AllArgsConstructor
@ToString
public class User2 {
    private Integer id;
    private String username;
    private String password;
}

最終編譯成位元組碼,反編譯結果如下:

public class User2 {
    private Integer id;
    private String username;
    private String password;
    // 省去 setter/getter
    public String toString() {
        return "User2(id=" + this.getId() + ", username=" + this.getUsername() + ", password=" + this.getPassword() + ")";
    }
}

另外,註解 @ToString 還支援設定指定哪些欄位的日誌化輸出,哪些不需要出現在 toString 方法中。使用屬性 @ToString.Exclude 排除不需要在 toString 中出現的欄位,使用 @ToString.Include標記需要出現在 toString 中的欄位,具體用法可參見示例:

@Getter
@Setter
@AllArgsConstructor
@ToString
public class User2 {
    @ToString.Exclude
    private Integer id;
    @ToString.Include
    private String username;
    @ToString.Include
    private String password;
}

列印 User2 物件的日誌效果就是:User2(username=user2, password=zcx123)

@EqualsAndHashCode

@EqualsAndHashCode 註解就是用於根據類所擁有的非靜態欄位自動重寫 equals 方法和 hashCode 方法,方便我們用於物件間的比較。類似 @ToString@EqualsAndHashCode 還可以使用需要作為比較的欄位和排除不需要比較的欄位,具體用法可以看如下示例:

@Getter
@Setter
@AllArgsConstructor
@ToString
@EqualsAndHashCode
public class User4 {
    @EqualsAndHashCode.Exclude
    private Integer id;
    @EqualsAndHashCode.Include
    private String username;
    @EqualsAndHashCode.Exclude
    private String password;
}

寫完實體類程式碼,我們編寫測試方法試下效果:

@Test
public void testEqual() {
    User4 user4 = new User4(1, "user4", "zxc");
    User4 user4_2 = new User4(1, "user4", "123");
    Assert.assertEquals(user4, user4_2); // ture
}

@Data/@Value

@Data/@Value 註解,提供了更綜合的生成程式碼功能,等價於下面幾個註解

@Getter
@Setter
@AllArgsConstructor
@ToString
@EqualsAndHashCode

兩個註解都只能使用在類上,與 @Data 不同, @Value 用來修飾不可變的類上。一般實體類沒有特別的限制的話,通常可以直接使用 @Data 註解修飾。

@Builder

@Builder 是一個非常強大的註解,提供了一種基於建造者模式的構建物件的 API。使用 @Builder 註解為給我們的實體類自動生成 builder() 方法,並且直接根據欄位名稱方法進行欄位賦值,最後使用 build()方法構建出一個實體物件。

@Data
@Builder
public class User6 {
    private Integer id;
    private String username;
    private String password;
}

@Test
public void testBuilder() {
    User6 user6 = User6.builder().id(1).username("user6").password("zxc123").build();
    log.warn("testLog: {}", user6); // User6(id=1, username=user6, password=zxc123)
}

需要注意的是 @Builder 不支援父類欄位的生成,當一個實體類存在父類時,@Builder 只能生成當前類的欄位構建方法。若需要用到父類的欄位方法時, Lombok 提供了新的註解 @SuperBuilder 來應對這種情況,下面是 @SuperBuilder 註解的使用方式:

@SuperBuilder
@Getter 
@Setter
public class Parent {
   private int id;
   private String name;
}

@SuperBuilder
@Data
public class Child extends Parent {
    private String childName;
}

呼叫示例:

Child child = Child.builder().id(1).name("父類名稱").childName("子類名稱").build();
System.out.println(child.getId());

由於 Lombok Plugin 還未更新支援@SuperBuilder,所以以上寫法在 IDEA 下還會提示編譯錯誤,無法找到 builder()方法。

也可以參考此文方式去處理繼承的情況:https://reinhard.codes/2015/09/16/lomboks-builder-annotation-and-inheritance/

日誌註解

正對程式類中常見不同框架 Logger 物件,Lombok 也提供了註解,來自動生成 Logger物件,實現優雅地輸出日誌,只需要在類上使用日誌註解如 @Log。當然 Lombok 支援了多個日誌框架,並且提供對應的註解如下:

  • @CommonsLog 等價效果: private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);

  • @Flogger 等價效果: private static final com.google.common.flogger.FluentLogger log = com.google.common.flogger.FluentLogger.forEnclosingClass();

  • @JBosLog 等價效果: private static final org.jboss.logging.Logger log = org.jboss.logging.Logger.getLogger(LogExample.class);

  • @Log 等價效果: private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());

  • @Log4j 等價效果: private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogExample.class);

  • @Log4j2 等價效果: private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);

  • @Slf4j 等價效果: private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);

  • @XSlf4j 等價效果: private static final org.slf4j.ext.XLogger log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);

下面程式碼使用 @Slf4j 註解進行日誌輸出:

@Slf4j
public class UserTests {
	    // ....
    @Test
    public void testLog() {
        User5 user5 = new User5();
        user5.setId(1);
        user5.setUsername("user5");
        user5.setPassword("zxc123");
        log.warn("testLog: {}", user5); 
   // 21:57:15.488 [main] WARN com.one.learn.lombok.UserTests - testLog: User5(id=1, username=user5, password=zxc123)
    }
}

@Cleanup

@Cleanup 用於標記需要釋放清理操作的資源物件變數,如 FileInputStream, FileOutputStream 等,標記之後資源物件使用完畢後,就會被自動關閉和清理,實際上這裡 Lombok 實現效果與 Java7 特性 try with resource 一樣, 為我們遮蔽了關閉資源的模板程式碼,下面給出 @Cleanup 的使用示例:

public class CleanupExample {
    public static void main(String[] args) throws IOException {
        @Cleanup InputStream in = new FileInputStream(args[0]);
        @Cleanup OutputStream out = new FileOutputStream(args[1]);
        byte[] b = new byte[10000];
        while (true) {
            int r = in.read(b);
            if (r == -1) {
                break;
            }
            out.write(b, 0, r);
        }
    }
}

CleanupExample.java 編譯生成的位元組碼反編譯可以得到如下結果:

public class CleanupExample {
		    		//...
    public static void main(String[] args) throws IOException {
        FileInputStream in = new FileInputStream(args[0]);
        try {
            FileOutputStream out = new FileOutputStream(args[1]);
            try {
                byte[] b = new byte[10000];
                while(true) {
                    int r = in.read(b);
                    if (r == -1) {
                        return;
                    }
                    out.write(b, 0, r);
                }
            } finally {
                if (Collections.singletonList(out).get(0) != null) {
                    out.close();
                }
            }
        } finally {
            if (Collections.singletonList(in).get(0) != null) {
                in.close();
            }
        }
    }
}

@SneakyThrows

@SneakyThrows 主要用於在沒有 throws 關鍵字的情況下,隱蔽地丟擲受檢查異常,為我們平常開發中需要異常丟擲時省去的 throw 操作,下面為使用 @SneakyThrows 的示例程式碼:

public class SneakyThrowsExample implements Runnable {
  @SneakyThrows(UnsupportedEncodingException.class)
  public String utf8ToString(byte[] bytes) {
    return new String(bytes, "UTF-8");
  }
  
  @SneakyThrows
  public void run() {
    throw new Throwable();
  }
}

最終編譯成位元組碼,反編譯結果如下:

public class SneakyThrowsExample implements Runnable {
    public SneakyThrowsExample() {
    }

    public String utf8ToString(byte[] bytes) {
        try {
            return new String(bytes, "UTF-8");
        } catch (UnsupportedEncodingException var3) {
            throw var3;
        }
    }

    public void run() {
        try {
            throw new Throwable();
        } catch (Throwable var2) {
            throw var2;
        }
    }
}

val/var

val/var 用於區域性變數的修飾,有了這注解修飾後,變數的型別就會自動通過等號右邊的表示式推斷出來,這個功能借鑑於許多程式語言的自動型別推斷的特性。 而 valvar 的區別在於, val 用於修飾不可變變數,var 修飾可變變數。當 val 修飾的變數被重新賦值時,編譯器就會提示異常:Error: java: 無法為最終變數 X 分配值。實際用法也比較簡單,可參考下面程式碼:

@Slf4j
public class VarValExample {
    public static void main(String[] args) {
        val text = "abc";
								        // text = "1"; // Error: java: 無法為最終變數 text 分配值`。
        var num = 1;
        num = 2;
        log.info("text:{},num:{}", text, num); // text:abc,num:2
    }
}

結語

到這裡我們學習了 Lombok 的近乎 80% 常用的註解,應用在我們的日常開發中已經是綽綽有餘了,開始嘗試 使用 Lombok 吧,慢慢地就會感受下效率的提升以及程式碼的優雅。

參考