從實踐 APT 到深入理解 Lombok

一、概述

  博客內所有文章均爲 原創,所有示意圖均爲 原創,若轉載請附原文鏈接。

1.1 起因

  在開始正文的分析之前我想先大概談一下自己爲什麼要寫這篇博文。首先是因爲自己正在學習 Java 虛擬機比較底層的一些東西,然後也是在對虛擬機的各個部分進行探索,在這個探索的過程中我接觸到了 APT(Annotation Processing Tool),也就是我們通常意義上的 註解處理器 ,對於 Java 中的註解大家一定都不會感到陌生,相信大家在準備面試的時候也背過很多關於註解的作用。

  在做開發的過程中我們最常使用到的比如 @Override 這樣提供給編譯器進行規範檢查的標記註解,還有一種就是在 Spring 開發中經常用到的比如 @Bean 或 @Import 等等這樣提供給框架自身使用的功能性註解(對於 Spring 當中註解的源碼分析可以回看我之前的博文)。但是我想你也應該接觸過一款叫做 Lombok 的插件,對於這款插件的功能我就不贅述了,網上的介紹一大堆,但是你有沒有思考過 Lombok 的底層到底是怎麼實現的。

  當我在學習 APT 的過程中通過資料的閱讀發現了 Lombok 的底層也是使用 APT 來實現的,但是對於具體是怎麼實現的,爲什麼要這麼實現,以及這麼實現有什麼優點和缺點等等一些列問題我查詢了很多的中文資料都是一無所獲,當我們在搜索 Lombok 的時候得到的結果更多的重複的 API 羅列,把官網中的話反覆地複製,相同的 Demo 展示,甚至還有一些博文直接憑自己的臆想來推測 Lombok 底層實現,卻不併沒有給出任何有力的證明(顯然這種臆想是錯的)。

  因此,我希望通過自己對資料的收集整理,並在 源碼求證代碼驗證 的基礎上對這個問題給出一個相對比較正確的答案。所以,本篇博文我們會從下面的提出的幾個疑問入手來分析 Lombok 底層和 Java APT 的相關原理。如果,你是一個小萌新或者僅僅想了解一下 Lombok 的使用方法,那這篇博文可能不太適合你。

1.2 疑問

  • Lombok 中的註解和 Spring 中的註解有什麼區別?
  • Lombok 到底是生成新的 Java 源文件還是修改已有的 Java 源文件?
  • 如果是修改已有的 Java 源文件那 Lombok 又是怎麼實現的呢?
  • 如何從源碼的角度來理解 Lombok 的實現方式?
  • 爲什麼會有很多人反對使用 Lombok ?
  • 我們是否可以利用 Lombok 提供的 API 來實現自己需要的功能?

二、前提

2.1 Java 編譯器的工作流程

pic

  對於這個 Java 編譯器的工作流程網上的博文數不勝數,且其中都引用到了這張著名的示意圖,所以我這裏再把它放上來,因爲網上相關的博文已經很多了,所以我不做贅述,這裏就直接引用官網的總結:

  • Parse and Enter: 所有在命令行中指定的源文件都被讀取,解析成語法樹,然後所有外部可見的定義都被輸入到編譯器的符號表中。
  • Annotation Processing: 調用所有適當的註釋處理器。如果任何註釋處理程序生成任何新的源文件或類文件,則重新開始編譯,直到沒有創建任何新文件爲止。
  • Analyse and Generate: 最後,解析器創建的語法樹將被分析並轉換爲類文件。在分析過程中,可能會發現對其他類的引用。編譯器將檢查這些類的源和類路徑,如果在源路徑上找到它們,也會編譯這些文件,儘管它們不需要進行註釋處理。

2.2 關於 Lombok

2.3 關於 Javac 的源碼


三、求證

3.1 Lombok 中的註解和 Spring 中的註解有什麼區別?

  這個問題其實是比較有趣的一個問題,當我第一次近距離接觸註解的時候還是在準備面試的過程中,當時知道註解根據運行機制可以分爲三類即源碼註解、編譯時註解和運行時註解,但是當時並不是很理解這樣劃分的作用,以及不同運行機制對註解產生的影響。後來在閱讀過 Spring 的源碼後,發現其實 Spring 當中的大多數註解都是 運行時註解(RUNTIME) ,比如常用的 @Autowire 等都是在編譯完成後且程序啓動時或者運行過程中發揮作用,Spring 可以通過反射動態的獲取到每個字段或方法註解,並對被不同註解註釋的採用不同的處理方式,所以這裏的註解其實更像是一種標誌和約定,能夠讓系統的動態的識別出它所需要的元素。

  另一種註解就是 編譯時註解(CLASS) ,這種註解一般都會存在於源碼和字節碼文件中,比如 @Override、@Deprecated 和 @SuppressWarnings ,這種類型的註解都是僅在編譯中發揮作用,而在類加載的時候就會被丟棄,可以理解爲僅對編譯器有效,用來進行一些語法規範的檢查。

  還有一種是我們平時很少接觸到的 源碼註解(SOURCE),這種註解一般僅存在於源文件中,當文件被編譯爲字節碼時就會被丟棄,通過這種類型的註解我們可以 定義新的編譯規則 ,並檢查被編譯的源文件、可以修改已有源代碼,還可以生成新的源代碼。而我們所說的 Lombok 使用的就是這種類型註解,也就是說 Lombok 實際的作用時間是在源碼編譯爲字節碼的過程中。

  所以概括來說,Spring 中的註解和 Lombok 中的註解最大的區別之處就在於運行機制的不同,且作用也是大不相同,Spring 中的註解主要是起一個標註性的作用,而 Lombok 中的註解更多的是對 Java 編譯器的工作流程進行干預。

3.2 Lombok 到底是生成新的 Java 源文件還是修改已有的 Java 源文件?

  這個也是一個很有趣的問題,關於這個問題的起因還要從我接觸了 APT 後開始嘗試自定義 APT 講起,我當時已經實現了一個比較簡單,用於檢查類中字段是否都存在 getter 方法的檢測註解,在這之後我聯想到了之前用過的 Lombok 插件,因爲這個插件的功能也是基於註解對類進行一些字段和方法的檢測和代碼插入,所以我就想嘗試着自己去實現一個低配版的 Lombok 插件。

  但是在開始的時候我就遇到了問題,因爲我發現當我們把 @Data 註解在類上的時候,我們雖然可以獲取到該類中的字段和方法,也可以通過 PrintWriter 把需要生成的代碼寫到新的文件中,但是我們好像沒有辦法將需要添加的代碼直接插入到當前 Java 源文件中。這個時候我就比較困惑了,如果我們不能夠將新生成的 getter 和 setter 等方法直接寫入到源 Java 文件,那麼我們就只能將當前 Java 原文件中的代碼先全部寫到另一個新文件中,然後再在新的文件中添加我們需要插入的代碼,但其實這也是不可行的,因爲對於每個方法你沒辦法獲取到源 Java 文件中該方法的方法體,那這是不是意味着我們就窮途末路了。

  而當我去網上查看別人的實現思路時,發現他們的實現思路僅僅是將當前被 @Data 註釋的類中的字段提取出來,先生成一個新的 Java 文件,然後根據剛剛提取到的字段信息,將字段以及其 getter 和 setter 一同寫入到新文件中,然後就完事大吉。看完這種實現方式之後的我是黑人問號,這麼做難道是源 java 文件中的方法全都捨棄不要了麼。因爲自己之前也沒有做過特殊的測試,所以特意用 Lombok 測試了一下,測試的結果很清晰即 Lombok 的 @Data 註解標註的類經 Lombok 的 processor 處理後其源 Java 文件中的方法仍然是可被調用的。

  因此通過上面的測試,我們可以首先明確的一點是 Lombok 並不是通過生成新的 Java 源文件來完成 getter 和 setter 等方法的插入的。那 Lombok 底層的實現到底是怎樣的呢,我又繼續開始了探索。

3.3 如果是修改已有的 Java 源文件那 Lombok 又是怎麼實現的呢(源碼解析)?

3.3.1 概述

  首先,通過上面的分析我們可以大致確定 Lombok 底層的實現應該是通過修改已有的 Java 源文件(準確來說修改是 AST)來完成的,那他到底是怎麼實現的呢,好奇心驅使我從 GitHub 上找到了 Lombok 的源碼,並對比較基礎的 @Getter 進行了剖析。這裏需要注意的一點,因爲 Eclipse 是使用自己的內置編譯器,而不是 Javac 所以 Lombok 提供了插件的兩套實現,這裏我們主要分析 javac 的實現版本,並對應下面這個示意圖進行分析(圖源)。

pic1

3.3.2 Processor

  我們都知道當我們在自定義一個 APT 的時候需要繼承 AbstractProcessor ,並實現其最核心的 process 方法來對當前輪編譯的結果進行處理,在 Lombok 中也不例外,Lombok 也是通過一個頂層的 Processor 來接收當前輪的編譯結果,而這個 Processor 就是 LombokProcessor ,所以我們第一步直接進入到這個類的 process 方法中。

// lombok/src/core/lombok/javac/apt/LombokProcessor.java
@Override 
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
	while(true){
		for (long prio : priorityLevels) {
			// ...
			// 類型轉換
			transformer.transform(prio, javacProcessingEnv.getContext(), cusForThisRound, cleanup);
		}
	}
}

  在 process 方法中最重要的一段代碼就是上面這段,我們先不要管這裏面的參數都是什麼意思,但是根據這個方法名我們可以猜測一下這個方法應該是將當前輪中的相關信息進行了封裝轉換,所以我們先繼續跟進這個方法。

// lombok/src/core/lombok/javac/JavacTransformer.java
public void transform(long priority, Context context, java.util.List<JCCompilationUnit> compilationUnitsRaw, CleanupRegistry cleanup) {
	// ...	

	java.util.List<JavacAST> asts = new ArrayList<JavacAST>();
		
	for (JCCompilationUnit unit : compilationUnits) {
		if (!Boolean.TRUE.equals(LombokConfiguration.read(ConfigurationKeys.LOMBOK_DISABLE, JavacAST.getAbsoluteFileLocation(unit)))) {
			// 將當前輪上下文封裝後添加到 asts 集合中
			asts.add(new JavacAST(messager, context, unit, cleanup));
		}
	}
		
	for (JavacAST ast : asts) {
		ast.traverse(new AnnotationVisitor(priority));
		// 從當前編譯單元開始深度優先遍歷 AST 並調用每個節點的 visit 方法
		handlers.callASTVisitors(ast, priority);
	}
}

// lombok/src/core/lombok/javac/HandlerLibrary.java
public void callASTVisitors(JavacAST ast, long priority) {
	for (VisitorContainer container : visitorHandlers) {
		try {
			if (container.getPriority() == priority) 
				ast.traverse(container.visitor);
		}
	}
}

// lombok/src/core/lombok/javac/JavacAST.java
public void traverse(JavacASTVisitor visitor) {
	// 從當前編譯單元開始深度優先遍歷 AST 並調用每個節點的 visit 方法
	// 其中 top 方法返回類型爲 JavacNode
	top().traverse(visitor);
}

// lombok/src/core/lombok/javac/JavacNode.java
public void traverse(JavacASTVisitor visitor) {
	switch (this.getKind()) { 
			//...
			case TYPE:
			visitor.visitType(this, (JCClassDecl) get());
			ast.traverseChildren(visitor, this);
			visitor.endVisitType(this, (JCClassDecl) get());
			break;
		case FIELD:
			visitor.visitField(this, (JCVariableDecl) get());
			ast.traverseChildren(visitor, this);
			visitor.endVisitField(this, (JCVariableDecl) get());
			break;
		case METHOD:
			visitor.visitMethod(this, (JCMethodDecl) get());
			ast.traverseChildren(visitor, this);
			visitor.endVisitMethod(this, (JCMethodDecl) get());
			break;
			//...
	}
}

  上面代碼的總體邏輯還算比較清晰,但是因爲開始的時候的處理邏輯還是蠻多的,但這裏比較關鍵的其實就是最後的 traverse 方法,通過該方法我們完成了對整棵 AST 樹的深度優先遍歷,並且對每個節點都調用了其對應的 visit 方法。其實從這個位置開始我們已經大概能猜到 Lombok 的操作方式了,通過上面的代碼 Lombok 已經獲取到了 javac 的 AST ,並對其進行了遍歷,其實在這裏開始 Lombok 已經越界了,因爲正常來說 javac 中的 JavacAST 和 JavacNode 等這些 Javac 的類是屬於內部類,對外部不可見的,Lombok 在這裏相當於走了後門,並且使用這樣的方法可以讓整棵 AST 對代碼可見,並可以追溯每個節點的父節點。

  下面的代碼邏輯就比較好理解了,就是運用了適配器設計模式,對 handlers 變量調用了 handleAnnotation 方法,這裏需要注意的是變量 handlers 的類型就是 HandlerLibrary 。

// lombok/src/core/lombok/javac/JavacASTVisitor.java
public interface JavacASTVisitor {
	void setTrees(Trees trees);
	//...
}

// lombok/src/core/lombok/javac/JavacASTAdapter.java
public class JavacASTAdapter implements JavacASTVisitor {
	// JavacASTVisitor 接口的標準適配器,接口上的每個方法以空方法體實現,僅需覆蓋需要使用的方法
	@Override 
	public void setTrees(Trees trees) {}
	//...
}

// lombok/src/core/lombok/javac/JavacTransformer.java
// AnnotationVisitor 爲 JavacTransformer 的內部類
private class AnnotationVisitor extends JavacASTAdapter {
	// ...
	@Override 
	public void visitAnnotationOnType(JCClassDecl type, JavacNode annotationNode, JCAnnotation annotation) {
		JCCompilationUnit top = (JCCompilationUnit) annotationNode.top().get();
		handlers.handleAnnotation(top, annotationNode, annotation, priority);
	}
		
	@Override 
	public void visitAnnotationOnField(JCVariableDecl field, JavacNode annotationNode, JCAnnotation annotation) {
	JCCompilationUnit top = (JCCompilationUnit) annotationNode.top().get();
		handlers.handleAnnotation(top, annotationNode, annotation, priority);
	}
		
	@Override 
	public void visitAnnotationOnMethod(JCMethodDecl method, JavacNode annotationNode, JCAnnotation annotation) {
		JCCompilationUnit top = (JCCompilationUnit) annotationNode.top().get();
		handlers.handleAnnotation(top, annotationNode, annotation, priority);
	}
	// ...
}

3.3.3 Handler

// lombok/src/core/lombok/javac/HandlerLibrary.java
public void handleAnnotation(JCCompilationUnit unit, JavacNode node, JCAnnotation annotation, long priority) {
	for (AnnotationHandlerContainer<?> container : containers) {
		try {
			if (container.getPriority() == priority) {
				if (checkAndSetHandled(annotation)) {
					// 主要邏輯調用 Handler
					container.handle(node);
				} else 					
					if (container.isEvenIfAlreadyHandled()) 
						container.handle(node);
					}
			}
		}
	}
}

// lombok/src/core/lombok/javac/HandlerLibrary.java
// AnnotationHandlerContainer 爲 HandlerLibrary 內部類
public void handle(final JavacNode node) {
	// 可以看到在這裏就是直接調用了 handle 方法
	handler.handle(JavacHandlerUtil.createAnnotation(annotationClass, node), (JCAnnotation)node.get(), node);
}

// lombok/src/core/lombok/javac/JavacAnnotationHandler.java
public abstract class JavacAnnotationHandler<T extends Annotation> {
	protected Trees trees;	
	// JavacAnnotationHandler 爲所有 Handler 的基類
	// 所有 Handler 應在 handle 方法中處理 AST
	public abstract void handle(AnnotationValues<T> annotation, JCAnnotation ast, JavacNode annotationNode);
	//...
}

  當我們對每個 JavacNode 節點調用 visit* 方法時,其實就已經將 AST 的相關信息傳遞到了 Handler ,在 HandlerLibrary 類中會接着進行一連串的驗證和調用,但歸根到底最終調用了抽象類 JavacAnnotationHandlerhandle 方法。

  接下來下面代碼的邏輯就是根據不同的註解選擇不同的 Handler 來進行處理,而抽象父類 JavacAnnotationHandler 中的泛型 T 就表示當前 Handler 所關注的註解類型,如 HandleGetter 所關注的就是 @Getter 註解。而通過下面的代碼我們也可以看到,在 HandleGetter 類中會首先通過 handle 方法來接收相關的 JavacNode 和 AST 信息,然後會根據不同的邏輯來使用 TreeMaker 來構建方法體,並最終組裝成 JCMethodDecl ,最後通過 JavacNode 的 add 方法將新組裝好的方法添加到 AST 中,這樣就完成了對 AST 的修改。

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
public class HandleGetter extends JavacAnnotationHandler<Getter> {
	@Override 
	public void handle(AnnotationValues<Getter> annotation, JCAnnotation ast, JavacNode annotationNode) {
		JavacNode node = annotationNode.up();
		switch (node.getKind()) {
		case FIELD:
			createGetterForFields(level, fields, annotationNode, true, lazy, onMethod);
			break;
		case TYPE:
			if (lazy) annotationNode.addError("'lazy' is not supported for @Getter on a type.");
			generateGetterForType(node, annotationNode, level, false, onMethod);
			break;
		}
	}
}

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
public void createGetterForFields(AccessLevel level, Collection<JavacNode> fieldNodes, JavacNode errorNode, boolean whineIfExists, boolean lazy, List<JCAnnotation> onMethod) {
	for (JavacNode fieldNode : fieldNodes) {
		createGetterForField(level, fieldNode, errorNode, whineIfExists, lazy, onMethod);
	}
}

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
public void createGetterForField(AccessLevel level,
		JavacNode fieldNode, JavacNode source, boolean whineIfExists, boolean lazy, List<JCAnnotation> onMethod) {
		
	if (fieldNode.getKind() != Kind.FIELD) {
		source.addError("@Getter is only supported on a class or a field.");
		return;
	}
		
	JCVariableDecl fieldDecl = (JCVariableDecl)fieldNode.get();
		
	// 通過 injectMethod 方法將組裝好的 JCMethodDecl 添加到當前字段的上層節點中(JCClassDecl)
	injectMethod(fieldNode.up(), createGetter(access, fieldNode, fieldNode.getTreeMaker(), source.get(), lazy, onMethod), List.<Type>nil(), getMirrorForFieldType(fieldNode));
}

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
public JCMethodDecl createGetter(long access, JavacNode field, JavacTreeMaker treeMaker, JCTree source, boolean lazy, List<JCAnnotation> onMethod) {
	// ...
	
	// 構建方法體	
	List<JCStatement> statements;
	JCTree toClearOfMarkers = null;
	if (lazy && !inNetbeansEditor(field)) {
		toClearOfMarkers = fieldNode.init;
		statements = createLazyGetterBody(treeMaker, field, source);
	} else {
		statements = createSimpleGetterBody(treeMaker, field);
	}
	JCBlock methodBody = treeMaker.Block(0, statements);
		
	// 構建 JCMethodDecl
	if (isFieldDeprecated(field)) annsOnMethod = annsOnMethod.prepend(treeMaker.Annotation(genJavaLangTypeRef(field, "Deprecated"), List.<JCExpression>nil()));
		JCMethodDecl decl = recursiveSetGeneratedBy(treeMaker.MethodDef(treeMaker.Modifiers(access, annsOnMethod), methodName, methodType, methodGenericParams, parameters, throwsClauses, methodBody, annotationMethodDefaultValue), source, field.getContext());

	// 將組裝好的 JCMethodDecl 返回
	return decl;
}

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
// 使用 TreeMaker 構建方法體
public List<JCStatement> createSimpleGetterBody(JavacTreeMaker treeMaker, JavacNode field) {
	return List.<JCStatement>of(treeMaker.Return(createFieldAccessor(treeMaker, field, FieldAccess.ALWAYS_FIELD)));
}
// lombok/src/core/lombok/javac/handlers/JavacHandlerUtil.java
public static void injectMethod(JavacNode typeNode, JCMethodDecl method, List<Type> paramTypes, Type returnType) {
	// ...
	typeNode.add(method, Kind.METHOD);
}

3.4 怎樣利用 Lombok 提供的 API 來實現自己需要的功能?

  這部分因爲網上已經有比較好的示例了,所以這裏我就不再贅述了。

3.5 爲什麼會有很多人反對使用 Lombok ?

  對於這個說法網上衆說紛紜,但說來說去都是在說 Lombok 修改了程序的源碼,且因爲其跟 JDK 的關聯較強(使用了未公開的內部 API),所以可能在每次 JDK 更新後都會出現問題,且可能因爲 JDK 更新後導致 Lombok 不可用進而導致程序崩潰。對於這個問題的話這裏也還是提供兩篇文章,一篇是比較公正的看法,另一篇是 Lombok 團隊自己的看法。


四、內容總結

  這篇博文中我們探討了 Lombok 底層的實現方式,並大概梳理了 Lombok 對註解的解析流程以及其功能實現的底層原理,說白了就是 Lombok 使用了一些小的技巧,使用了 Javac 未公開的一些 API 來取巧實現了動態修改 AST 的目的,從而實現了代替程序員向 Java 源代碼中 “添加” 代碼的操作,從而大大提升我們代碼的整潔性和可讀性。

  但是另一方面也正是因爲其使用了這樣的方式,所以也飽受爭議,因爲使用未公開的 API 就意味着每次 JDK 的更新都可能對 Lombok 的內部實現產生影響,也就意味着每次使用 Lombok 都存在着一些不可控的風險,比如如果 Lombok 在某次的 JDK 更新後沒有及時更上版本的適配,那麼就可能出現註解無法解析進而導致程序崩潰的情況。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章