1. 程式人生 > >99%的程式設計師都在用Lombok,原理竟然這麼簡單?我也手擼了一個!|建議收藏!!!

99%的程式設計師都在用Lombok,原理竟然這麼簡單?我也手擼了一個!|建議收藏!!!

> 羅曼羅蘭說過:世界上只有一種英雄主義,就是看清生活的真相之後依然熱愛生活。 對於 Lombok 我相信大部分人都不陌生,但對於它的實現原理以及缺點卻鮮為人知,而本文將會從 Lombok 的原理出發,手擼一個簡易版的 Lombok,讓你理解這個熱門技術背後的執行原理,以及它的優缺點分析。 ## 簡介 在講原理之前,我們先來複習一下 Lombok (老司機可以直接跳過本段看原理部分的內容)。 Lombok 是一個非常熱門的開源專案 (https://github.com/rzwitserloot/lombok),使用它可以有效的解決 Java 工程中那些繁瑣又重複程式碼,例如 Setter、Getter、toString、equals、hashCode 以及非空判斷等,都可以使用 Lombok 有效的解決。 ## 使用 ### 1.新增 Lombok 外掛 在 IDE 中必須安裝 Lombok 外掛,才能正常呼叫被 Lombok 修飾的程式碼,以 Idea 為例,新增的步驟如下: - 點選 File > Settings > Plugins 進入外掛管理頁面 - 點選 Browse repositories... - 搜尋 Lombok Plugin - 點選 Install plugin 安裝外掛 - 重啟 IntelliJ IDEA 安裝完成,如下圖所示: ![](https://cdn.nlark.com/yuque/0/2020/png/92791/1585453965127-91793452-aa81-4d64-85d6-ad02e29ca6fd.png#align=left&display=inline&height=1526&originHeight=1526&originWidth=1872&size=0&status=done&style=none&width=1872) ### 2.新增 Lombok 庫 接下來我們需要**在專案中新增最新的 Lombok 庫**,如果是 Maven 專案,直接在 pom.xml 中新增如下配置: ```xml
org.projectlombok lombok 1.18.12 provided
``` 如果是 **JDK 9+** 可使用模組的方式新增,配置如下: ```xml org.projectlombok lombok 1.18.12 ``` ### 3.使用 Lombok 接下來到了前半部分中最重要的 Lombok 使用環節了,我們先來看在沒有使用 Lombok 之前的程式碼: ```java public class Person { private Integer id; private String name; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ``` 這是使用 Lombok 之後的程式碼: ```java @Getter @Setter public class Person { private Integer id; private String name; } ``` 可以看出在 Lombok 之後,**用一個註解就搞定了之前所有 Getter/Setter 的程式碼,讓程式碼瞬間優雅了很多**。 Lombok 所有註解如下: - `val`:用在區域性變數前面,相當於將變數宣告為 final; - `@NonNull`:給方法引數增加這個註解會自動在方法內對該引數進行是否為空的校驗,如果為空,則丟擲 NPE(NullPointerException); - `@Cleanup`:自動管理資源,用在區域性變數之前,在當前變數範圍內即將執行完畢退出之前會自動清理資源,自動生成 try-finally 這樣的程式碼來關閉流; - `@Getter/@Setter`:用在屬性上,再也不用自己手寫 setter 和 getter 方法了,還可以指定訪問範圍; - `@ToString`:用在類上可以自動覆寫 toString 方法,當然還可以加其他引數,例如 @ToString(exclude=”id”) 排除 id 屬性,或者 @ToString(callSuper=true, includeFieldNames=true) 呼叫父類的 toString 方法,包含所有屬性; - `@EqualsAndHashCode`:用在類上自動生成 equals 方法和 hashCode 方法; - `@NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor`:用在類上,自動生成無參構造和使用所有引數的建構函式以及把所有 @NonNull 屬性作為引數的建構函式,如果指定 staticName="of" 引數,同時還會生成一個返回類物件的靜態工廠方法,比使用建構函式方便很多; - `@Data`:註解在類上,相當於同時使用了 @ToString、@EqualsAndHashCode、@Getter、@Setter 和 @RequiredArgsConstrutor 這些註解,對於 POJO 類十分有用; - `@Value`:用在類上,是 @Data 的不可變形式,相當於為屬性新增 final 宣告,只提供 getter 方法,而不提供 setter 方法; - `@Builder`:用在類、構造器、方法上,為你提供複雜的 builder APIs,讓你可以像如下方式一樣呼叫Person.builder().name("xxx").city("xxx").build(); - `@SneakyThrows`:自動拋受檢異常,而無需顯式在方法上使用 throws 語句; - `@Synchronized`:用在方法上,將方法宣告為同步的,並自動加鎖,而鎖物件是一個私有的屬性 $lock 或 $LOCK,而 Java 中的 synchronized 關鍵字鎖物件是 this,鎖在 this 或者自己的類物件上存在副作用,就是你不能阻止非受控程式碼去鎖 this 或者類物件,這可能會導致競爭條件或者其它執行緒錯誤; - `@Getter(lazy=true)`:可以替代經典的 Double Check Lock 樣板程式碼; - `@Log`:根據不同的註解生成不同型別的 log 物件,但是例項名稱都是 log,有六種可選實現類 - `@CommonsLog` Creates log = org.apache.commons.logging.LogFactory.getLog(LogExample.class); - `@Log` Creates log = java.util.logging.Logger.getLogger(LogExample.class.getName()); - `@Log4j` Creates log = org.apache.log4j.Logger.getLogger(LogExample.class); - `@Log4j2` Creates log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class); - `@Slf4j` Creates log = org.slf4j.LoggerFactory.getLogger(LogExample.class); - `@XSlf4j` Creates log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class); 它們的具體使用如下: #### ① val 使用 ```java val sets = new HashSet(); // 相當於 final Set sets = new HashSet<>(); ``` #### ② NonNull 使用 ``` public void notNullExample(@NonNull String string) { string.length(); } // 相當於 public void notNullExample(String string) { if (string != null) { string.length(); } else { throw new NullPointerException("null"); } } ``` #### ③ Cleanup 使用 ``` public static void main(String[] args) { try { @Cleanup InputStream inputStream = new FileInputStream(args[0]); } catch (FileNotFoundException e) { e.printStackTrace(); } // 相當於 InputStream inputStream = null; try { inputStream = new FileInputStream(args[0]); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } ``` #### ④ Getter/Setter 使用 ``` @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PROTECTED) private int id; private String shap; ``` #### ⑤ ToString 使用 ```java @ToString(exclude = "id", callSuper = true, includeFieldNames = true) public class LombokDemo { private int id; private String name; private int age; public static void main(String[] args) { // 輸出 LombokDemo(super=LombokDemo@48524010, name=null, age=0) System.out.println(new LombokDemo()); } } ``` #### ⑥ EqualsAndHashCode 使用 ``` @EqualsAndHashCode(exclude = {"id", "shape"}, callSuper = false) public class LombokDemo { private int id; private String shap; } ``` #### ⑦ NoArgsConstructor、RequiredArgsConstructor、AllArgsConstructor 使用 ```java @NoArgsConstructor @RequiredArgsConstructor(staticName = "of") @AllArgsConstructor public class LombokDemo { @NonNull private int id; @NonNull private String shap; private int age; public static void main(String[] args) { new LombokDemo(1, "Java"); // 使用靜態工廠方法 LombokDemo.of(2, "Java"); // 無參構造 new LombokDemo(); // 包含所有引數 new LombokDemo(1, "Java", 2); } } ``` #### ⑧ Builder 使用 ``` @Builder public class BuilderExample { private String name; private int age; @Singular private Set occupations; public static void main(String[] args) { BuilderExample test = BuilderExample.builder().age(11).name("Java").build(); } } ``` #### ⑨ SneakyThrows 使用 ```java public class ThrowsTest { @SneakyThrows() public void read() { InputStream inputStream = new FileInputStream(""); } @SneakyThrows public void write() { throw new UnsupportedEncodingException(); } // 相當於 public void read() throws FileNotFoundException { InputStream inputStream = new FileInputStream(""); } public void write() throws UnsupportedEncodingException { throw new UnsupportedEncodingException(); } } ``` #### ⑩ Synchronized 使用 ``` public class SynchronizedDemo { @Synchronized public static void hello() { System.out.println("world"); } // 相當於 private static final Object $LOCK = new Object[0]; public static void hello() { synchronized ($LOCK) { System.out.println("world"); } } } ``` ####  ⑪ Getter(lazy = true) 使用 ``` public class GetterLazyExample { @Getter(lazy = true) private final double[] cached = expensive(); private double[] expensive() { double[] result = new double[1000000]; for (int i = 0; i < result.length; i++) { result[i] = Math.asin(i); } return result; } } // 相當於 import java.util.concurrent.atomic.AtomicReference; public class GetterLazyExample { private final AtomicReference cached = new AtomicReference<>(); public double[] getCached() { java.lang.Object value = this.cached.get(); if (value == null) { synchronized (this.cached) { value = this.cached.get(); if (value == null) { final double[] actualValue = expensive(); value = actualValue == null ? this.cached : actualValue; this.cached.set(value); } } } return (double[]) (value == this.cached ? null : value); } private double[] expensive() { double[] result = new double[1000000]; for (int i = 0; i < result.length; i++) { result[i] = Math.asin(i); } return result; } } ``` ## 原理分析 我們知道 Java 的編譯過程大致可以分為三個階段: 1. 解析與填充符號表 1. 註解處理 1. 分析與位元組碼生成 編譯過程如下圖所示: ![編譯流程.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1585463226782-6c4eedcd-1fa6-49c4-b157-13da7a569851.png#align=left&display=inline&height=135&name=%E7%BC%96%E8%AF%91%E6%B5%81%E7%A8%8B.png&originHeight=135&originWidth=712&size=31883&status=done&style=none&width=712) 而 Lombok 正是利用「註解處理」這一步進行實現的,Lombok 使用的是 JDK 6 實現的 JSR 269: Pluggable Annotation Processing API (編譯期的註解處理器) ,它是在編譯期時把 Lombok 的註解程式碼,轉換為常規的 Java 方法而實現優雅地程式設計的。 這一點可以在程式中得到驗證,比如本文剛開始用 `@Data` 實現的程式碼: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1585458183261-40ef9df8-2542-48b4-ac55-dea9ab6eaecf.png#align=left&display=inline&height=134&name=image.png&originHeight=402&originWidth=610&size=31750&status=done&style=none&width=203.33333333333334) 在我們編譯之後,檢視 Person 類的編譯原始碼發現,程式碼竟然是這樣的: ![Person 生成的原始碼.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1585458363101-87352b07-1226-4b3a-a210-f7097abf9b1b.png#align=left&display=inline&height=706&name=Person%20%E7%94%9F%E6%88%90%E7%9A%84%E6%BA%90%E7%A0%81.png&originHeight=706&originWidth=1273&size=325352&status=done&style=none&width=1273) 可以看出 Person 類在編譯期被註解翻譯器修改成了常規的 Java 方法,新增 Getter、Setter、equals、hashCode 等方法。 Lombok 的執行流程如下: ![lombok 執行流程.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1585462671515-770b6fa3-92fb-4d4e-9668-846b17ba3808.png#align=left&display=inline&height=563&name=lombok%20%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B.png&originHeight=563&originWidth=344&size=65503&status=done&style=none&width=344) 可以看出,在編譯期階段,當 Java 原始碼被抽象成語法樹 (AST) 之後,Lombok 會根據自己的註解處理器動態的修改 AST,增加新的程式碼 (節點),在這一切執行之後,再通過分析生成了最終的位元組碼 (.class) 檔案,這就是 Lombok 的執行原理。 ## 手擼一個 Lombok 我們實現一個簡易版的 Lombok 自定義一個 Getter 方法,我們的實現步驟是: 1. 自定義一個註解標籤介面,並實現一個自定義的註解處理器; 1. 利用 tools.jar 的 javac api 處理 AST (抽象語法樹) 1. 使用自定義的註解處理器編譯程式碼。 這樣就可以實現一個簡易版的 Lombok 了。 ### 1.定義自定義註解和註解處理器 首先建立一個 `MyGetter.java` 自定義一個註解,程式碼如下: ```java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.SOURCE) // 註解只在原始碼中保留 @Target(ElementType.TYPE) // 用於修飾類 public @interface MyGetter { // 定義 Getter } ``` 再實現一個自定義的註解處理器,程式碼如下: ```java import com.sun.source.tree.Tree; import com.sun.tools.javac.api.JavacTrees; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.tree.TreeTranslator; import com.sun.tools.javac.util.*; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; import java.util.Set; @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("com.example.lombok.MyGetter") public class MyGetterProcessor extends AbstractProcessor { private Messager messager; // 編譯時期輸入日誌的 private JavacTrees javacTrees; // 提供了待處理的抽象語法樹 private TreeMaker treeMaker; // 封裝了建立AST節點的一些方法 private Names names; // 提供了建立識別符號的方法 @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.messager = processingEnv.getMessager(); this.javacTrees = JavacTrees.instance(processingEnv); Context context = ((JavacProcessingEnvironment) processingEnv).getContext(); this.treeMaker = TreeMaker.instance(context); this.names = Names.instance(context); } @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { Set elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MyGetter.class); elementsAnnotatedWith.forEach(e -> { JCTree tree = javacTrees.getTree(e); tree.accept(new TreeTranslator() { @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) { List jcVariableDeclList = List.nil(); // 在抽象樹中找出所有的變數 for (JCTree jcTree : jcClassDecl.defs) { if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) { JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree; jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl); } } // 對於變數進行生成方法的操作 jcVariableDeclList.forEach(jcVariableDecl -> { messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed"); jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl)); }); super.visitClassDef(jcClassDecl); } }); }); return true; } private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) { ListBuffer statements = new ListBuffer<>(); // 生成表示式 例如 this.a = a; JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident( names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName())); statements.append(aThis); JCTree.JCBlock block = treeMaker.Block(0, statements.toList()); // 生成入參 JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(), jcVariableDecl.vartype, null); List parameters = List.of(param); // 生成返回物件 JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType()); return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), methodType, List.nil(), parameters, List.nil(), block, null); } private Name getNewMethodName(Name name) { String s = name.toString(); return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length())); } private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) { return treeMaker.Exec( treeMaker.Assign( lhs, rhs ) ); } } ``` 自定義的註解處理器是我們實現簡易版的 Lombok 的重中之重,我們需要繼承 `AbstractProcessor` 類,重寫它的 init() 和 process() 方法,在 process() 方法中我們先查詢所有的變數,在給變數新增對應的方法。我們使用 TreeMaker 物件和 Names 來處理 AST,如上程式碼所示。 當這些程式碼寫好之後,我們就可以新增一個 Person 類來試一下我們自定義的 `@MyGetter` 功能了,程式碼如下: ```java @MyGetter public class Person { private String name; } ``` ### 2.使用自定義的註解處理器編譯程式碼 上面的所有流程執行完成之後,我們就可以編譯程式碼測試效果了。 首先,我們先進入程式碼的根目錄,執行以下三條命令。 進入的根目錄如下: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1585467535446-bd2fccc4-c546-4457-b48a-47de6d68a183.png#align=left&display=inline&height=61&name=image.png&originHeight=182&originWidth=506&size=14667&status=done&style=none&width=168.66666666666666) **① 使用 tools.jar 編譯自定義的註解器** > javac -cp $JAVA_HOME/lib/tools.jar MyGetter* -d . 注意:命令最後面有一個“.”表示當前資料夾。 **② 使用自定義註解器,編譯 Person 類** > javac -processor com.example.lombok.MyGetterProcessor Person.java **③ 檢視 Person 原始碼** > javap -p Person.class 原始碼檔案如下: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1585467304603-70024123-a1ef-44be-9b1f-dc1b380a4b2c.png#align=left&display=inline&height=89&name=image.png&originHeight=268&originWidth=824&size=46575&status=done&style=none&width=274.6666666666667) **可以看到我們自定義的 getName() 方法已經成功生成了**,到這裡簡易版的 Lombok 就大功告成了。 ## Lombok 優缺點 Lombok 的優點很明顯,它可以讓我們寫更少的程式碼,節約了開發時間,並且讓程式碼看起來更優雅,它的缺點有以下幾個。 ### 缺點1: 降低了可除錯性 Lombok 會幫我們自動生成很多程式碼,但這些程式碼是在編譯期生成的,因此在開發和除錯階段這些程式碼可能是“丟失的”,這就給除錯程式碼帶來了很大的不便。 ### 缺點2:可能會有相容性問題 Lombok 對於程式碼有很強的侵入性,加上現在 JDK 版本升級比較快,每半年釋出一個版本,而 Lombok 又屬於第三方專案,並且由開源團隊維護,因此就沒有辦法保證版本的相容性和迭代的速度,進而可能會產生版本不相容的情況。 ### 缺點3:可能會坑到隊友 尤其對於組人來的新人可能影響更大,假如這個之前沒用過 Lombok,當他把程式碼拉下來之後,因為沒有安裝 Lombok 的外掛,在編譯專案時,就會提示找不到方法等錯誤資訊,導致專案編譯失敗,進而影響了團結成員之間的協作。 ### 缺點4:破壞了封裝性 面向物件封裝的定義是:通過訪問許可權控制,隱藏內部資料,外部僅能通過類提供的有限的介面訪問和修改內部資料。 也就是說,我們不應該無腦的使用 Lombok 對外暴露所有欄位的 Getter/Setter 方法,因為有些欄位在某些情況下是不允許直接修改的,比如購物車中的商品數量,它直接影響了購物詳情和總價,因此在修改的時候應該提供統一的方法,進行關聯修改,而不是給每個欄位新增訪問和修改的方法。 ## 小結 本文我們介紹了 Lombok 的使用以及執行原理,它是通過 JDK 6 實現的 JSR 269: Pluggable Annotation Processing API (編譯期的註解處理器) ,在編譯期時把 Lombok 的註解轉換為 Java 的常規方法的,我們可以通過繼承 AbstractProcessor 類,重寫它的 init() 和  process() 方法,實現一個簡易版的 Lombok。但同時 Lombok 也存在這一些使用上的缺點,比如:降低了可除錯性、可能會有相容性等問題,因此我們在使用時要根據自己的業務場景和實際情況,來選擇要不要使用 Lombok,以及應該如何使用 Lombok。 最後提醒一句,再好的技術也不是萬金油,就好像再好的鞋子也得適合自己的腳才行! > 感謝閱讀,希望本文對你能所啟發。覺得不錯的話,分享給需要的朋友,謝謝。 參考 & 鳴謝 [https://juejin.im/post/5a6eceb8f265da3e467555fe] (https://juejin.im/post/5a6eceb8f265da3e467555fe) [https://www.tuicool.com/articles/y6rUz2V](https://www.tuicool.com/articles/y6rUz2V) > 更多精彩內容,請關注微信公眾號「Java中文社群」