Project Lombok 是很實用且被廣泛運用的語法糖 library,它可以減少許多樣板程式例如 getter, setter 等,我們只需幾個簡單的 annotation,例如 @Data,就能幫助我們省下大量重複的工作,讓開發者更專注於關鍵的邏輯,進而提高開發效率。但我認為 Lombok 也存在缺點,若過度使用 Lombok 可能導致問題,使程式碼難以維護。
Lombok 的 annotation 都是在編譯時才被轉換成 java code,因此早期在使用 Lombok 時,開發人員還要在 IDE 安裝套件才能正常使用 Lombok。時至今日,它已直接被整合進了 IntelliJ IDEA,不需額外安裝套件,只要引入 dependency 即可;但使用其他 IDE 的開發者就要比較麻煩一點了,可以參考官方的安裝教學,支援 Eclipse, VS code 等等。
Lombok @Data
Lombok 中的 @Data
應該是最常被使用的 annotation,到處都看得見這樣的程式碼:
@Data
public class Student {
private int id;
private String name;
private String email;
// ...
}
根據官方的文件,它是由5個 Lombok annotation 組合而成,詳細成分如下:
@Getter
: 為每一個 field 產生一個 getter。
@Setter
: 為每一個 non-final field 產生一個 setter。
@RequiredArgsConstructor
: 建立一個 constructor,其參數為 class 中所有 @NonNull
, final
field。
@ToString
: 將每個 field 按順序並以逗號分隔,以 name=value 的形式組成字串,大幅提高可讀性。
@EqualsAndHashCode
: 以所有 non-static 和 non-transient field 來實作 equals()
和 hashCode()
。如果 class 不能夠良好的 override 這兩個 method,導致無法如預期判斷兩物件的相等,一旦放入 HashSet 或當作 HashMap 的 key 值時,可能會引發 memory leak。
這就是 Lombok 的強大之處!只需短短一行 @Data
就能產出數十行甚至上百行的程式碼。Lombok 雖然能減少開發負擔,也正因如此就導致有些開發者過度依賴了它,因此在使用 Lombok 之餘,應關心以下幾個議題:
Lombok 的議題
StackOverflowError
如果兩個之間的有雙向依賴關係,例如 Student, Teacher 兩類別互相依賴著,並且都標記了 @Data
或 @ToString
或 @EqualsAndHashCode
,就會導致 StackOverflowError
。
透過 Delombok 可以發現它們的 hashCode()
, toString()
都存在循環引用,導致無窮迴圈,最後因記憶體不足引發錯誤。最好的解法就是藉由重構來消除雙向依賴的關係。
@Builder 的危險性
@Builder
是 Builder Pattern 的語法糖,標記在 class 上就能優雅的建立一個 instance:
Student student = Student.builder()
.id(12345)
.name("KaiSheng")
.email("KaiSheng714@github.com")
.score(92)
.build();
這在建立物件時還挺方便的,尤其是在測試裡。不過,它彈性優雅的風格相對帶來副作用。例如 field 太多時,容易導致開發者忘了賦值而創建出一個不完整的 instance,最後可能導致難以發現的 bug。因此應盡量按規格在必填的 field 加上 Lombok 的 @NonNull
:如果沒有賦值,則立即拋出 NullPointerException
,fail fast。又或者可以透過重構手段,讓 class 變小一點,減少開發者忘記賦值的機率。
備註: @NonNull
僅適用於非 primitive type。
與第三方 library 的衝突
Jackson 是一個可以幫助我們簡單、快速轉換 Java 物件與 json 的 library。若標記 @Builder
,則無參數的 constructor 會被設成 package-private,這時若我們反序列化 json 字串,就會導致 Jackson 無法找到 constructor 而拋出 Exception:
Error on Jackson Deserialization
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of ... cannot deserialize from Object value
(no delegate- or property-based Creator)
解法是在 class 上多加一個 @Jacksonized
即可。不過,我並不喜歡這個作法,因為要讓團隊花額外時間去了解、學習它的意義,太累了,如果越能降低專案的學習門檻就越好。順帶一提,Google Gson 能很好的處理 @Builder
class,無需額外設定或標記。
隱藏複雜性
之前提過,Constructor Injection 暴露了 constructor 中含有過多參數 (Long Parameter List) 的壞味道,正常的開發者看到過多參數肯定會不舒服,表示它可能違反了 SRP 原則,需要被重構。
但我看過這樣的用法:使用 Lombok @AllArgsConstructor
輕鬆建立 constructor,取代 Spring @Autowired
:
@Component
@AllArgsConstructor
public class HelloBean {
private final AnotherBean anotherBean;
private final AnotherBean2 anotherBean2;
private final AnotherBean3 anotherBean3;
// ...
private final AnotherBean15 anotherBean15;
// method
}
這會讓開發者看不到巨大的 constructor,即使注入很多個 dependency 看起來好像也沒關係。Lombok + constructor injection 的組合用法反而隱藏了複雜性。但如果團隊成員都有發現程式壞味道的意識、熟悉重構的手段,這種用法能使程式更簡潔,我認為是可被允許的。
Tell, Don’t Ask.
因為 Lombok 能方便快速產出所有 getter,就容易讓人忽略了 Tell, Don’t Ask 原則的建議。若物件中有過多不適當的 getter,會導致本不應被暴露的內部狀態被洩漏出去了,破壞了封裝 (Encapsulation)。
此原則提醒開發者:所謂 OOP 就是將資料與操作該資料的 method 綁在一起,設計出內聚力強的類別。與其在 service 層用了一堆 getter 詢問然後進行邏輯運算,不如直接請它做完再回傳出來。因此,必要時可在指定的 field 個別去標記 @Getter
或 @Getter(AccessLevel.NONE)
,來決定是否要對外暴露哪個 field,以盡量符合此原則的精神。
我常用的做法
綜上所述,我認為 Lombok 還是最適合用於 Entity, Model。這樣用就足以應付大部分的狀況了:
@Log4j2
@Data
@Accessors(chain = true)
public class Student {
private final int id; // 可在必填欄位加上 final
private String name;
private String email;
private int score;
// 可自行實作 setter
public void setEmail(String email) {
EmailUtil.validate(email);
this.email = email;
}
// 實作業務邏輯
public boolean isPassExam() {
return this.score >= 60;
}
}
在 class 標註 @Accessors(chain=true)
後就能以 Method Chaining 的方式建立 instance。另外,將必要的 field 加上 final
,使用者就必須在 constructor 裡填入它:
Student student = new Student(12345)
.setName("KaiSheng")
.setEmail("KaiSheng714@github.com")
.setScore(92);
可能有些人會問我:用 @Builder
不是也差不多嗎?但我的經驗是,因為我有時會用 Factory 來建立物件,這時如果在工廠裡再用 Builder Pattern 反而有點多此一舉,此外,我的方式不需要額外呼叫 builder()
, build()
,而且也不必透過 Lombok 實作整個 Builder Pattern 的程式碼,而是只有 setter 而已,變得單純許多,而且 setter 也是可以自行實作的。
另外,標註 @Log4j2
或 @Slf4j
,就可以使用 log 功能,非常方便,適用於任何 class,通常建議是需要時再標註上去即可。
結論
雖然 Lombok 雖然很方便,但世上沒有完美的工具,也沒有完美的用法。開發者們不僅要懂得取捨,也要根據 context 來決定該如何使用。只要能符合本身需求,同時考慮到可讀性與維護成本,並且盡量減少出錯的風險,就會是個好的用法。
此外,我們也要明白 Lombok 到底做了什麼,並且不要標記過多的 annotation 讓程式看起來更複雜(尤其是有邏輯的地方),也要注意與其他 library 衝突的可能性,否則萬一出了問題就會很麻煩。