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
安裝完成,如下圖所示:
2.添加 Lombok 庫
接下來我們需要在項目中添加最新的 Lombok 庫,如果是 Maven 項目,直接在 pom.xml 中添加如下配置:
<dependencies><!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version><scope>provided</scope></dependency> </dependencies>如果是 JDK 9+ 可使用模塊的方式添加,配置如下:
<annotationProcessorPaths><path><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version></path> </annotationProcessorPaths>3.使用 Lombok
接下來到了前半部分中最重要的 Lombok 使用環節了,我們先來看在沒有使用 Lombok 之前的代碼:
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 之后的代碼:
@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,而 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 使用
val sets = new HashSet<String>(); // 相當于 final Set<String> 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 使用
@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 使用
@NoArgsConstructor @RequiredArgsConstructor(staticName = "of") @AllArgsConstructor public class LombokDemo {@NonNullprivate int id;@NonNullprivate 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;@Singularprivate Set<String> occupations;public static void main(String[] args) {BuilderExample test = BuilderExample.builder().age(11).name("Java").build();} }⑨ SneakyThrows 使用
public class ThrowsTest {@SneakyThrows()public void read() {InputStream inputStream = new FileInputStream("");}@SneakyThrowspublic 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 {@Synchronizedpublic 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<java.lang.Object> 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 的編譯過程大致可以分為三個階段:
解析與填充符號表
注解處理
分析與字節碼生成
編譯過程如下圖所示:
而 Lombok 正是利用「注解處理」這一步進行實現的,Lombok 使用的是 JDK 6 實現的 JSR 269: Pluggable Annotation Processing API (編譯期的注解處理器) ,它是在編譯期時把 Lombok 的注解代碼,轉換為常規的 Java 方法而實現優雅地編程的。
這一點可以在程序中得到驗證,比如本文剛開始用 @Data?實現的代碼:
在我們編譯之后,查看 Person 類的編譯源碼發現,代碼竟然是這樣的:
可以看出 Person 類在編譯期被注解翻譯器修改成了常規的 Java 方法,添加 Getter、Setter、equals、hashCode 等方法。
Lombok 的執行流程如下:
可以看出,在編譯期階段,當 Java 源碼被抽象成語法樹 (AST) 之后,Lombok 會根據自己的注解處理器動態的修改 AST,增加新的代碼 (節點),在這一切執行之后,再通過分析生成了最終的字節碼 (.class) 文件,這就是 Lombok 的執行原理。
??
手擼一個 Lombok
我們實現一個簡易版的 Lombok 自定義一個 Getter 方法,我們的實現步驟是:
自定義一個注解標簽接口,并實現一個自定義的注解處理器;
利用 tools.jar 的 javac api 處理 AST (抽象語法樹)
使用自定義的注解處理器編譯代碼。
這樣就可以實現一個簡易版的 Lombok 了。
1.定義自定義注解和注解處理器
首先創建一個 MyGetter.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}再實現一個自定義的注解處理器,代碼如下:
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; // 提供了創建標識符的方法@Overridepublic 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);}@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MyGetter.class);elementsAnnotatedWith.forEach(e -> {JCTree tree = javacTrees.getTree(e);tree.accept(new TreeTranslator() {@Overridepublic void visitClassDef(JCTree.JCClassDecl jcClassDecl) {List<JCTree.JCVariableDecl> 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<JCTree.JCStatement> 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<JCTree.JCVariableDecl> 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?功能了,代碼如下:
@MyGetter public class Person {private String name; }2.使用自定義的注解處理器編譯代碼
上面的所有流程執行完成之后,我們就可以編譯代碼測試效果了。首先,我們先進入代碼的根目錄,執行以下三條命令。
進入的根目錄如下:
① 使用 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
源碼文件如下:
可以看到我們自定義的 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://www.tuicool.com/articles/y6rUz2V
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 二叉树常见面试题
- 下一篇: 阿里开源的那个牛X的问题排查工具——Ar