# 六角鼠年鐵人賽 Week 11 - Spring Boot - Lombok 省時省力好幫手 ==大家好,我是 "為了拿到金角獎盃而努力著" 的文毅青年 - Kai== ## 賦得古原草送别 白居易 :::info 離離原上草,一歲一枯榮。 野火燒不盡,春風吹又生。 遠芳侵古道,晴翠接荒城。 又送王孫去,萋萋滿別情。 ::: ## 主題 :::info 說到 Clean Clear 是否有想起早年露得清的廣告台詞呢? 沒錯! 今天要介紹的 **Lombok** 就是具備了這樣特性的套件。 ::: 是否寫煩了每一個 Bean 的 Get()、Set()、toString() 方法? 是否厭倦每次處理 POJO 物件就要用DB套件去 Gen 出相關 Class? 那你一定得使用看看 **Lombok**! **Lombok** 便是專門幫助 Java 開發人員減少寫這些重複程式碼的好套件! ## Lombok 介紹 **Lombok** 是一個發展已久的 OpenSource,於 2011 年被開發出來支援當時的 JDK 1.6 版本,到現在依然維持著一年 3 ~ 4 個 patch 的更新。 **Lombok** 致力於代替開發者處理許多重複性質的程式碼撰寫,除了上述提到的部分外,還有如 Resources release、Log 等幾個實用的功能也在近幾年的 patch 中實裝了。 其實現的原理如同一般 Spring 的 @annotation 一樣,是透過編譯處理時,若有對應到的註解的 Class,註解編譯器會自動去對應項目中的註解文件,並自動編譯產生對應 Class 中物件的方法。 所以 Lombok 套件在使用時機,通常只會在 Compile 的時候,在 Compile 完成後,便不再需要 Lombok 套件,因此在 build.gradle 裡面的設定,就可以調整為只在 Compile 時候才加入這個套件的方式。 ## Lombok 提供的功能 |方法名稱|主要功用|包含方法|附註| |---|---|---|---| |@AllArgsConstructor |產生全類別參數的建構子||| |@NoArgsConstructor |產生無類別參數的建構子||對於 @NonNull 的參數不產出判斷式,因此可能會因無資料造成錯誤| |@RequiredArgsConstructor|針對@NonNull和未初始化的final參數 產生類別參數的建構子||使用上需與@AllArgsConstructor 或 @NoArgsConstructor搭配使用| |@Builder |提供新方式去建構類實體||[詳細可看官網介紹](https://projectlombok.org/features/Builder)| |@Builder.Default |當使用 @Builder 在 類別時,可用此方式設定給類別參數|| |@Singular |與@Builder合用,詳細可參考Builder 官網文件|| |@Cleanup |指定類別參數,當程式退出當前執行域的時候進行清除資源動作|| |@Data |產生DTO類別方法|@ToString @EqualsAndHashCode @Getter [非final欄位]@Setter @RequiredArgsConstructor| |@EqualsAndHashCode |產生equals和hashCode方法|| |@Getter |產生類別參數的GET方法|| |@Setter |產生類別參數的SET方法|| |@NonNull |產生類別參數的Null判斷式||會拋出NullPointException| |@SneakyThrows |產生略過拋出錯誤的程式|| |@Synchronized |將類別方法或參數宣告成同步|| |@ToString |產生類別參數的toString方法||可用@ToString(exclude="類別參數名稱") 進行排除;可用@ToString(callSuper=true, includeFieldNames=true)操作父類別的參數| |@log |根據不同註解產出不同LOG實體,default Field Name: log||支援類型: @CommonsLog、@Log4j、@Log4j2、@Slf4j、@Log、@Flogger、@JBossLog、@XSlf4j |val |使用在類別參數前,可將參數宣告為 final|| |@Value |使用在類別前,可將類別宣告為 final,並指產出類別參數的GET方法|| 特別注意到 @SneakyThrows 是將 checked exception 看做 unchecked exception,程式會不進行處理直接略過,好處是在很多需要寫 catch 的部分會減少很多程式碼,壞處就是真正發生問題時後非常難追,使用上要注意,盡量替用在那些基本不會出錯的程式中。 ## 實例 我將會使用在這篇文章 [六角鼠年鐵人賽 Week 10 - Spring Boot - Build first API](/cWafHYsTT6yRuqedj0CluQ) 所做的專案當作基底,直接增加範例 API 進來。 這次的專案程式架構將會長的如下圖: ![](https://i.imgur.com/9gUqZBH.png) ## build.gradle 增加套件設定 打開 **build.gradle** 檔案後,找到 **dependencies** 的部分,在裏頭增加下列兩行程式碼。 設定 lombok 只能在 Compile 時後執行,並設定 @annotation 的處理,這部分還需要搭配後續的動作才能設定正確。 ``` compileOnly 'org.projectlombok:lombok:1.18.4' annotationProcessor 'org.projectlombok:lombok:1.18.4' ``` :::spoiler **完整的 build.gradle 內容** ``` plugins { id 'java' id 'org.springframework.boot' version '2.2.0.RELEASE' id 'io.spring.dependency-management' version '1.0.8.RELEASE' } group 'kai.com' version '1.0-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } compileOnly 'org.projectlombok:lombok:1.18.4' annotationProcessor 'org.projectlombok:lombok:1.18.4' } test { useJUnitPlatform() } apply plugin: 'application' mainClassName = 'kai.com.springbootApplicationStarter' ``` ::: ## Enable Annotation Processor 打開 Settings 的窗格,方法如下: 1. 快捷鍵: Ctrl + Alt + S 2. 找到右上角 File > Settings 進入 **Build, Execution, Deployment** > **Compiler** > **Annotation Processors** 中 找到 **Enable annotation processing** 並打勾,如下圖所示: ![](https://i.imgur.com/IjOzYLI.png) ## 創建多個測試用的 Bean :::spoiler **Bean For @AllArgsConstructor @NonNull @Getter @Setter** ``` package kai.com.lombok.bean; import lombok.*; @Getter @Setter @AllArgsConstructor public class lombokAllArgsBean { private String username; private String password; @NonNull private Integer id; } /* you will get public lombokAllArgsBean(int id, String username, String password){ if (id == null) throw new NullPointerException("id"); this.id = id; this.username = username; this.password = password; } */ ``` ::: :::spoiler **Bean For @NoArgsConstructor** ``` package kai.com.lombok.bean; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; @Getter @Setter @NoArgsConstructor public class lombokNoArgsBean { private String username; private String password; @NonNull private Integer id; } /* you will get public lombokNoArgsBean(){} */ ``` ::: :::spoiler **Bean For @RequiredArgsConstructor** ``` package kai.com.lombok.bean; import lombok.*; @Getter @Setter @NoArgsConstructor @RequiredArgsConstructor public class lombokRequireArgsBean { private String username; private String password; @NonNull private Integer id; } /* you will get private lombokRequireArgsBean(int id){ if (id == null) throw new NullPointerException("id"); this.id = id; } public static lombokRequireArgsBean of(int id){ return new lombokRequireArgsBean(id); } */ ``` ::: :::spoiler **Bean For @Builder** ``` package kai.com.lombok.bean; import lombok.*; @Setter @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class lombokBuilderBean { private Integer id; private String username; private String password; } ``` ::: :::spoiler **Bean For @EqualsAndHashCode** ``` package kai.com.lombok.bean; import lombok.*; @Getter @Setter @AllArgsConstructor @EqualsAndHashCode public class lombokEqualsAndHashCodeBean { private String username; private String password; @NonNull private Integer id; } ``` ::: :::spoiler **Bean For @ToString** ``` package kai.com.lombok.bean; import lombok.*; @Getter @Setter @AllArgsConstructor @ToString public class lombokToStringBean { private String username; private String password; @NonNull private Integer id; } ``` ::: :::spoiler **Bean For @SneakyThrows** ``` package kai.com.lombok.bean; import lombok.*; import java.io.IOException; @Getter @Setter @AllArgsConstructor public class lombokSneakyThrowsBean { private String username; private String password; @NonNull private Integer id; public void throwCheckedException() throws IOException { throw new IOException("Checked, Declares and Throws IOException."); } public void handleCheckedException() throws IOException { try { throw new IOException("Checked, Declares and Handles IOException."); } catch(IOException e){ System.out.println(e.toString()); throw e; //for print on API response } } public void throwUnCheckedException() { throw new RuntimeException("Unchecked RuntimeException."); } @SneakyThrows public String sneakyThrowsCheckedException() { throw new IOException("Sneaky, Checked, Declares and Throws IOException."); } } ``` ::: :::spoiler **Bean For @log** ``` package kai.com.lombok.bean; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.extern.apachecommons.CommonsLog; import lombok.extern.flogger.Flogger; import lombok.extern.java.Log; import lombok.extern.jbosslog.JBossLog; import lombok.extern.log4j.Log4j; import lombok.extern.log4j.Log4j2; import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.XSlf4j; @Getter @Setter @AllArgsConstructor @CommonsLog /* @Log4j @Log4j2 @Slf4j @Log @Flogger @JBossLog @XSlf4j */ public class lombokLogBean { private String username; private String password; @NonNull private Integer id; public void doGenerateLog(){ log.error("Something else is wrong here"); } } ``` ::: ## 創建測試用的 Controller :::spoiler **Controller For Lombok** ``` package kai.com.lombok.controller; import kai.com.lombok.bean.*; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController public class lombokController { @PostMapping("/lombok/NoArgs") public lombokNoArgsBean doPost1(@RequestBody lombokNoArgsBean lombokB){ /* error, because there is any constructor with parameter be created by Lombok */ //lombokNoArgsBean newBean = new lombokNoArgsBean(lombokB.getUsername(),lombokB.getPassword(),lombokB.getId()); /* below can work */ lombokNoArgsBean newBean = new lombokNoArgsBean(); return newBean; } @PostMapping("/lombok/AllArgs") public lombokAllArgsBean doPost2(@RequestBody lombokAllArgsBean lombokB){ lombokAllArgsBean newBean = new lombokAllArgsBean(lombokB.getUsername(),lombokB.getPassword(),lombokB.getId()); return newBean; } @PostMapping("/lombok/RequireArgs") public lombokRequireArgsBean doPost3(@RequestBody lombokRequireArgsBean lombokB){ lombokRequireArgsBean newBean = new lombokRequireArgsBean(lombokB.getId()); return newBean; } @GetMapping("/lombok/builder") public lombokBuilderBean doGet(){ lombokBuilderBean bean = lombokBuilderBean.builder().id(1).username("Kai").password("password").build(); return bean; } @GetMapping("/lombok/toString") @ResponseBody public Map<String,String> doGet2(@RequestParam("id") int id, @RequestParam("username") String username, @RequestParam("password") String password){ lombokToStringBean bean = new lombokToStringBean(username,password,id); Map<String,String> map = new HashMap<String,String>(); map.put("ToString", bean.toString()); return map; } @GetMapping("/lombok/equalsAndHashCode") @ResponseBody public Map<String,Object> doGet3(@RequestParam("id") int id, @RequestParam("username") String username, @RequestParam("password") String password){ lombokEqualsAndHashCodeBean bean = new lombokEqualsAndHashCodeBean(username,password,id); Map<String,Object> map = new HashMap<String,Object>(); map.put("Equals ID Get and 1: ", bean.getId() == 1); map.put("Equals username Get and Kai: ", bean.getUsername().equals("Kai")); map.put("Equals password Get and test111: ", bean.getPassword().equals("test111")); map.put("HashCode: ", bean.hashCode()); return map; } @GetMapping("/lombok/sneakyThrows") @ResponseBody public Map<String,Object> doGet4(@RequestParam("id") int id, @RequestParam("username") String username, @RequestParam("password") String password){ lombokSneakyThrowsBean bean = new lombokSneakyThrowsBean(username,password,id); Map<String,Object> map = new HashMap<String,Object>(); try{ bean.throwCheckedException(); }catch(Exception e){ map.put("Case1: ", e.toString()); } try{ bean.handleCheckedException(); }catch(Exception e){ map.put("Case2: ", e.toString()); } try{ bean.throwUnCheckedException(); }catch(Exception e){ map.put("Case3: ", e.toString()); } try{ bean.sneakyThrowsCheckedException(); }catch(Exception e){ map.put("Case4: ", e.toString()); } return map; } @GetMapping("/lombok/log") public void doGet5(@RequestParam("id") int id, @RequestParam("username") String username, @RequestParam("password") String password){ lombokLogBean bean = new lombokLogBean(username,password,id); System.out.println("Record Logs"); bean.doGenerateLog(); return ; } } ``` ::: ## 測試結果與說明 ### @AllArgsConstructor @NonNull @Getter @Setter > 正常的Request中,開發者可以直接使用由 Lombok 建立的類別參數方法 ![](https://i.imgur.com/pTW9fuW.png) > 當應該不能為 Null 的 ID 為 Null 的狀況如下,會拋出Null錯誤並返回 Http Error Code 400 的 Bad Request 告知 ![](https://i.imgur.com/K2btZRk.png) ### @NoArgsConstructor > 由於產生的是沒有參數放入的建構子,因此在建立實體同時把值放入的狀況中就會發生錯誤 ![](https://i.imgur.com/azuqKta.png) ``` error: constructor lombokNoArgsBean in class lombokNoArgsBean cannot be applied to given types; lombokNoArgsBean newBean = new lombokNoArgsBean(lombokB.getUsername(),lombokB.getPassword(),lombokB.getId()); ^ required: no arguments found: String,String,Integer reason: actual and formal argument lists differ in length ``` ### @RequiredArgsConstructor > @RequiredArgsConstructor 必須要與 @AllArgsConstructor 或 @NoArgsConstructor 其中一個搭配使用 (本範例搭配 @NoArgsConstructor) ![](https://i.imgur.com/nhQezD2.png) > 同樣的當 ID 值為 Null 狀況,一樣會回傳錯誤訊息 ![](https://i.imgur.com/uAiqNAa.png) > @RequiredArgsConstructor(staticName="of") 會創建一個名稱為 of 的 static 類別出來 ### @Builder > 設置了 @Builder 我們便可以在實作該 Class 的時候以較簡約值觀的方式建立實體 > ![](https://i.imgur.com/vbHclvr.png) ![](https://i.imgur.com/dvh8yDi.png) ### @ToString > 協助生成 toString() ![](https://i.imgur.com/bPZtxl3.png) ### @EqualsAndHashCode > 協助生成 equals() 和 hashCode() > ![](https://i.imgur.com/zKCCz9W.png) ### @SneakyThrows > 這一塊可能比較不好懂,需要搭配以下圖解: > 方法 1 和 方法 2 都是針對 CheckedException 做處理,差別只在於一個拋出一個在當下 handle。 > 方法 3 則是處置 UnCheckedException > 方法 4 是 SneakyThrows,主要是讓開發者可以用類似處理 UnCheckedException 的方式,節省 CheckedException 的程式碼 > ![](https://i.imgur.com/qjW0EUo.png) >在 response 中看得更清楚,方法 4 與 方法 1 和 方法 2 達到了相同的目的。 >==注意== **盡量**用在確保不會出錯的程序中,避免增加除錯上的困難。 ![](https://i.imgur.com/VgkxVfj.png) ### @Log > 可以使用的 Log 類型如下: >> @CommonsLog @Log4j @Log4j2 @Slf4j @Log @Flogger @JBossLog @XSlf4j > 使用後也有正確在 console 上取得資訊 ![](https://i.imgur.com/1KyAMTJ.png) > 至於為何沒有產出 Log File 的部分? 這要保留到下一次說明 Log 的時候再說了~ 而做到這邊沒有看到 Log File 的朋友也不要緊張,這並不是 Lombok 的問題,而是 Log 的設定關係,後續在該章節的時候 Kai 這邊會好好說明。在目前只需理解成,系統沒有任何設定告訴它要把 Log 除了印在 Console 介面外還要印在哪裡? ### @Data > 對所有的類別參數建立 @ToString, @EqualsAndHashCode, @Getter,針對非 Final 屬性的類別參數建立 @Setter,加上 @RequiredArgsConstructor,其產出的 Class 方法與上述多雷同,不多贅述,多用於 DTO 的處理上。 ### @Cleanup > 加在任何 **具有 close() 方法的類別參數** 前,Lombok 會在 Compile 時,改寫該類別進 try catch { } 中,並自動建立 final 的 close() 動作,確保資源釋放。 > 特別注意,若想使用在 **無 close() 方法的類別參數**,其必須有至少一個無參數帶入的方法,可使用類似這樣的方式處理: ==@Cleanup("dispose") org.eclipse.swt.widgets.CoolBar bar = new CoolBar(parent, 0);== [下列為官方 @Cleanup 的範例](https://projectlombok.org/features/Cleanup) 編譯前: ``` import lombok.Cleanup; import java.io.*; 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); } } } ``` 編譯後: ``` import java.io.*; public class CleanupExample { public static void main(String[] args) throws IOException { InputStream in = new FileInputStream(args[0]); try { OutputStream out = new FileOutputStream(args[1]); try { byte[] b = new byte[10000]; while (true) { int r = in.read(b); if (r == -1) break; out.write(b, 0, r); } } finally { if (out != null) { out.close(); } } } finally { if (in != null) { in.close(); } } } } ``` ### @Synchronized > 同 Java 處理 Synchronized 修飾子的功能,差別在於其針對靜態類別方法與非靜態類別的方法自動建立的鎖名稱不同。 >> 靜態類別方法(static): $LOCK >> 非靜態類別方法: $lock > 當然你也可以設定屬於自己的鎖,並在設定時加入其中: @Synchronized("鎖物件名稱") ### @val > 設置在類別參數前,在 Compile 時候會被編譯加上 final 的修飾 ### @Value > 設置在類別前,與 @Data 功能相反,在 Compile 時所有類別參數與方法都會被加上 private 或 final 的修飾 > @Value(staticConstructor="of") 會額外建立一個 static 類別,同樣會帶有 final 的修飾。 ### @With > 添加給有使用 @AllArgsConstructor 的類別,會在判定目標類別參數輸入值與之前輸入不同時,自動建立一個新實體並回傳,新實體會保留原實體的其他欄位值。 > 應用在建立部分參數相同;部分參數不同的狀況下非常適合使用。 ## 結語 :::danger 以上內容就差不多介紹完 Lombok 這個好用的套件了,Kai 自己在練習的時候 Intellij 出現一堆紅色警告線,但執行 Compile 都可以通過,後續可能在補充如何把這線條消掉,避免誤會的狀況吧。 下一篇將介紹 Log 套件 [六角鼠年鐵人賽 Week 12 - Spring Boot - 一次搞懂 LOG 設定](/GuX8ngltT2yUWHWeb5UV-g) ::: 首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA) ###### tags: `Spring Boot`,`w3HexSchool`