99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个!|建议收藏
世界上只有一種英雄主義,就是看清生活的真相之后依然熱愛生活。
對于 Lombok 我相信大部分人都不陌生,但對于它的實(shí)現(xiàn)原理以及缺點(diǎn)卻鮮為人知,而本文將會從 Lombok 的原理出發(fā),手?jǐn)]一個(gè)簡易版的 Lombok,讓你理解這個(gè)熱門技術(shù)背后的執(zhí)行原理,以及它的優(yōu)缺點(diǎn)分析。
簡介
在講原理之前,我們先來復(fù)習(xí)一下 Lombok (老司機(jī)可直接跳過本段)。
Lombok 是一個(gè)非常熱門的開源項(xiàng)目 (https://github.com/rzwitserloot/lombok),使用它可以有效的解決 Java 工程中那些繁瑣又重復(fù)代碼,例如 Setter、Getter、toString、equals、hashCode 以及非空判斷等,都可以使用 Lombok 有效的解決。
使用
1.添加 Lombok 插件
在 IDE 中必須安裝 Lombok 插件,才能正常調(diào)用被 Lombok 修飾的代碼,以 Idea 為例,添加的步驟如下:
- 點(diǎn)擊 File > Settings > Plugins 進(jìn)入插件管理頁面
- 點(diǎn)擊 Browse repositories…
- 搜索 Lombok Plugin
- 點(diǎn)擊 Install plugin 安裝插件
- 重啟 IntelliJ IDEA
安裝完成,如下圖所示:
2.添加 Lombok 庫
接下來我們需要在項(xiàng)目中添加最新的 Lombok 庫,如果是 Maven 項(xiàng)目,直接在 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 使用環(huán)節(jié)了,我們先來看在沒有使用 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 之后,用一個(gè)注解就搞定了之前所有 Getter/Setter 的代碼,讓代碼瞬間優(yōu)雅了很多。
Lombok 所有注解如下:
- val:用在局部變量前面,相當(dāng)于將變量聲明為 final;
- @NonNull:給方法參數(shù)增加這個(gè)注解會自動在方法內(nèi)對該參數(shù)進(jìn)行是否為空的校驗(yàn),如果為空,則拋出 NPE(NullPointerException);
- @Cleanup:自動管理資源,用在局部變量之前,在當(dāng)前變量范圍內(nèi)即將執(zhí)行完畢退出之前會自動清理資源,自動生成 try-finally 這樣的代碼來關(guān)閉流;
- @Getter/@Setter:用在屬性上,再也不用自己手寫 setter 和 getter 方法了,還可以指定訪問范圍;
- @ToString:用在類上可以自動覆寫 toString 方法,當(dāng)然還可以加其他參數(shù),例如 @ToString(exclude=”id”) 排除 id 屬性,或者 @ToString(callSuper=true, includeFieldNames=true) 調(diào)用父類的 toString 方法,包含所有屬性;
- @EqualsAndHashCode:用在類上自動生成 equals 方法和 hashCode 方法;
- @NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor:用在類上,自動生成無參構(gòu)造和使用所有參數(shù)的構(gòu)造函數(shù)以及把所有 @NonNull 屬性作為參數(shù)的構(gòu)造函數(shù),如果指定 staticName=“of” 參數(shù),同時(shí)還會生成一個(gè)返回類對象的靜態(tài)工廠方法,比使用構(gòu)造函數(shù)方便很多;
- @Data:注解在類上,相當(dāng)于同時(shí)使用了 @ToString、@EqualsAndHashCode、@Getter、@Setter 和 @RequiredArgsConstrutor 這些注解,對于 POJO 類十分有用;
- @Value:用在類上,是 @Data 的不可變形式,相當(dāng)于為屬性添加 final 聲明,只提供 getter 方法,而不提供 setter 方法;
- @Builder:用在類、構(gòu)造器、方法上,為你提供復(fù)雜的 builder APIs,讓你可以像如下方式一樣調(diào)用Person.builder().name(“xxx”).city(“xxx”).build();
- @SneakyThrows:自動拋受檢異常,而無需顯式在方法上使用 throws 語句;
- @Synchronized:用在方法上,將方法聲明為同步的,并自動加鎖,而鎖對象是一個(gè)私有的屬性 $lock 或 $LOCK,而 Java 中的 synchronized 關(guān)鍵字鎖對象是 this,鎖在 this 或者自己的類對象上存在副作用,就是你不能阻止非受控代碼去鎖 this 或者類對象,這可能會導(dǎo)致競爭條件或者其它線程錯誤;
- @Getter(lazy=true):可以替代經(jīng)典的 Double Check Lock 樣板代碼;
- @Log:根據(jù)不同的注解生成不同類型的 log 對象,但是實(shí)例名稱都是 log,有六種可選實(shí)現(xiàn)類
- @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>(); // 相當(dāng)于 final Set<String> sets = new HashSet<>();② NonNull 使用
public void notNullExample(@NonNull String string) {string.length(); } // 相當(dāng)于 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();}// 相當(dāng)于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");// 使用靜態(tài)工廠方法LombokDemo.of(2, "Java");// 無參構(gòu)造new LombokDemo();// 包含所有參數(shù)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();}// 相當(dāng)于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");}// 相當(dāng)于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;} } // 相當(dāng)于 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 的編譯過程大致可以分為三個(gè)階段:
編譯過程如下圖所示:
而 Lombok 正是利用「注解處理」這一步進(jìn)行實(shí)現(xiàn)的,Lombok 使用的是 JDK 6 實(shí)現(xiàn)的 JSR 269: Pluggable Annotation Processing API (編譯期的注解處理器) ,它是在編譯期時(shí)把 Lombok 的注解代碼,轉(zhuǎn)換為常規(guī)的 Java 方法而實(shí)現(xiàn)優(yōu)雅地編程的。
這一點(diǎn)可以在程序中得到驗(yàn)證,比如本文剛開始用 @Data 實(shí)現(xiàn)的代碼:
在我們編譯之后,查看 Person 類的編譯源碼發(fā)現(xiàn),代碼竟然是這樣的:

可以看出 Person 類在編譯期被注解翻譯器修改成了常規(guī)的 Java 方法,添加 Getter、Setter、equals、hashCode 等方法。
Lombok 的執(zhí)行流程如下:
行流程.png&originHeight=563&originWidth=344&size=65503&status=done&style=none&width=344)
可以看出,在編譯期階段,當(dāng) Java 源碼被抽象成語法樹 (AST) 之后,Lombok 會根據(jù)自己的注解處理器動態(tài)的修改 AST,增加新的代碼 (節(jié)點(diǎn)),在這一切執(zhí)行之后,再通過分析生成了最終的字節(jié)碼 (.class) 文件,這就是 Lombok 的執(zhí)行原理。
手?jǐn)]一個(gè) Lombok
我們實(shí)現(xiàn)一個(gè)簡易版的 Lombok 自定義一個(gè) Getter 方法,我們的實(shí)現(xiàn)步驟是:
這樣就可以實(shí)現(xiàn)一個(gè)簡易版的 Lombok 了。
1.定義自定義注解和注解處理器
首先創(chuàng)建一個(gè) MyGetter.java 自定義一個(gè)注解,代碼如下:
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}再實(shí)現(xiàn)一個(gè)自定義的注解處理器,代碼如下:
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; // 編譯時(shí)期輸入日志的private JavacTrees javacTrees; // 提供了待處理的抽象語法樹private TreeMaker treeMaker; // 封裝了創(chuàng)建AST節(jié)點(diǎn)的一些方法private Names names; // 提供了創(chuàng)建標(biāo)識符的方法@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);}}// 對于變量進(jìn)行生成方法的操作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<>();// 生成表達(dá)式 例如 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());// 生成入?yún)?/span>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));} }自定義的注解處理器是我們實(shí)現(xiàn)簡易版的 Lombok 的重中之重,我們需要繼承 AbstractProcessor 類,重寫它的 init() 和 process() 方法,在 process() 方法中我們先查詢所有的變量,在給變量添加對應(yīng)的方法。我們使用 TreeMaker 對象和 Names 來處理 AST,如上代碼所示。
當(dāng)這些代碼寫好之后,我們就可以新增一個(gè) Person 類來試一下我們自定義的 @MyGetter 功能了,代碼如下:
@MyGetter public class Person {private String name; }2.使用自定義的注解處理器編譯代碼
上面的所有流程執(zhí)行完成之后,我們就可以編譯代碼測試效果了。
首先,我們先進(jìn)入代碼的根目錄,執(zhí)行以下三條命令。
進(jìn)入的根目錄如下:
① 使用 tools.jar 編譯自定義的注解器
javac -cp $JAVA_HOME/lib/tools.jar MyGetter* -d .
注意:命令最后面有一個(gè)“.”表示當(dāng)前文件夾。
② 使用自定義注解器,編譯 Person 類
javac -processor com.example.lombok.MyGetterProcessor Person.java
③ 查看 Person 源碼
javap -p Person.class
源碼文件如下:
可以看到我們自定義的 getName() 方法已經(jīng)成功生成了,到這里簡易版的 Lombok 就大功告成了。
Lombok 優(yōu)缺點(diǎn)
Lombok 的優(yōu)點(diǎn)很明顯,它可以讓我們寫更少的代碼,節(jié)約了開發(fā)時(shí)間,并且讓代碼看起來更優(yōu)雅,它的缺點(diǎn)有以下幾個(gè)。
缺點(diǎn)1: 降低了可調(diào)試性
Lombok 會幫我們自動生成很多代碼,但這些代碼是在編譯期生成的,因此在開發(fā)和調(diào)試階段這些代碼可能是“丟失的”,這就給調(diào)試代碼帶來了很大的不便。
缺點(diǎn)2:可能會有兼容性問題
Lombok 對于代碼有很強(qiáng)的侵入性,加上現(xiàn)在 JDK 版本升級比較快,每半年發(fā)布一個(gè)版本,而 Lombok 又屬于第三方項(xiàng)目,并且由開源團(tuán)隊(duì)維護(hù),因此就沒有辦法保證版本的兼容性和迭代的速度,進(jìn)而可能會產(chǎn)生版本不兼容的情況。
缺點(diǎn)3:可能會坑到隊(duì)友
尤其對于組人來的新人可能影響更大,假如這個(gè)之前沒用過 Lombok,當(dāng)他把代碼拉下來之后,因?yàn)闆]有安裝 Lombok 的插件,在編譯項(xiàng)目時(shí),就會提示找不到方法等錯誤信息,導(dǎo)致項(xiàng)目編譯失敗,進(jìn)而影響了團(tuán)結(jié)成員之間的協(xié)作。
缺點(diǎn)4:破壞了封裝性
面向?qū)ο蠓庋b的定義是:通過訪問權(quán)限控制,隱藏內(nèi)部數(shù)據(jù),外部僅能通過類提供的有限的接口訪問和修改內(nèi)部數(shù)據(jù)。
也就是說,我們不應(yīng)該無腦的使用 Lombok 對外暴露所有字段的 Getter/Setter 方法,因?yàn)橛行┳侄卧谀承┣闆r下是不允許直接修改的,比如購物車中的商品數(shù)量,它直接影響了購物詳情和總價(jià),因此在修改的時(shí)候應(yīng)該提供統(tǒng)一的方法,進(jìn)行關(guān)聯(lián)修改,而不是給每個(gè)字段添加訪問和修改的方法。
總結(jié)
本文我們介紹了 Lombok 的使用以及執(zhí)行原理,它是通過 JDK 6 實(shí)現(xiàn)的 JSR 269: Pluggable Annotation Processing API (編譯期的注解處理器) ,在編譯期時(shí)把 Lombok 的注解轉(zhuǎn)換為 Java 的常規(guī)方法的,我們可以通過繼承 AbstractProcessor 類,重寫它的 init() 和 process() 方法,實(shí)現(xiàn)一個(gè)簡易版的 Lombok。但同時(shí) Lombok 也存在這一些使用上的缺點(diǎn),比如:降低了可調(diào)試性、可能會有兼容性等問題,因此我們在使用時(shí)要根據(jù)自己的業(yè)務(wù)場景和實(shí)際情況,來選擇要不要使用 Lombok,以及應(yīng)該如何使用 Lombok。
最后提醒一句,再好的技術(shù)也不是萬金油,就好像再好的鞋子也得適合自己的腳才行!
感謝閱讀,希望本文對你能所啟發(fā)。覺得不錯的話,分享給需要的朋友,謝謝。
參考 & 鳴謝
https://juejin.im/post/5a6eceb8f265da3e467555fe
https://www.tuicool.com/articles/y6rUz2V
更多精彩內(nèi)容,請關(guān)注微信公眾號「Java中文社群」
關(guān)注下面二維碼,訂閱更多精彩內(nèi)容。
總結(jié)
以上是生活随笔為你收集整理的99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个!|建议收藏的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 秋招注意事项和面试通关攻略
- 下一篇: 6大分布式定时任务对比