隻有用過Mapstruct才知道它是有多麼的好用與順手。本篇主要講述Mapstuct的實作原理,它是怎麼去生成轉換代碼的過程,讓大家對這個架構的實作原理有個比較透徹的了解。
1. Java動态編譯與JSR 269
首先,我們先重溫下java的編譯過程:Java源代碼-->編譯器-->jvm可執行的Java位元組碼(即虛拟指令)-->jvm-->jvm中解釋器-->機器可執行的二進制機器碼-->程式。其實java編譯器提供了一套完整的api,我們使用接口可以友善地進行動态編譯。下面是一個簡單的從源代碼檔案到生成執行檔案的完整生成過程。
//建立源檔案
String currentDir = System.getProperty("user.dir");
String src = "package com.seewo.phoenix ;"
+ "public class TestCompiler {"
+ " public void disply() {"
+ " System.out.println(\"Hello\");"
+ "}}";
String filename = currentDir + "/src/main/java/com/seewo/phoenix/TestCompiler.java";
File file = new File(filename);
File fileParent = file.getParentFile();
if (!fileParent.exists()) {
fileParent.mkdir();
}
if (!file.exists()) {
file.createNewFile();
}
FileWriter fw = new FileWriter(file);
fw.write(src);
fw.flush();
fw.close();
// 使用JavaCompiler 編譯java檔案
JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = jc.getStandardFileManager(null, null, null);
Iterable fileObjects = fileManager.getJavaFileObjects(filename);
CompilationTask cTask = jc.getTask(null, fileManager, null, null, null, fileObjects);
cTask.call();
fileManager.close();
// 使用URLClassLoader加載class到記憶體
URL[] urls = new URL[] { new URL("file:/" + currentDir + "/src/main/java/com/seewo/phoenix/TestCompiler.java") };
URLClassLoader cLoader = new URLClassLoader(urls);
Class c = cLoader.loadClass("com.seewo.phoenix.TestCompiler");
cLoader.close();
// 利用class建立執行個體,反射執行方法
Object obj = c.newInstance();
Method method = c.getMethod("disply");
method.invoke(obj);
複制代碼
大家都會問,這個跟mapstruct說的有半毛錢關系?别急,請看下面的代碼過程,在執行JavaCompile#compile中就有去執行processAnnotation(注解掃描與處理)這個步驟。這就是mapstruct注解掃描的入口調用方法。這裡對java compile的調用過程就不詳細闡述,另外會開一篇講其中的原理。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICN4ETMfdHLkVGepZ2XtxSZ6l2clJ3LcBnYldHL0FWby9mZvwVPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsAjMfd3bkFGazxCMx8VesATMfhHLlN3XnxCMz8FdsYkRGZkRG9lcvx2bjxSa2EWNhJTW1AlUxEFeVRUUfRHelRHL2EzXlpXazxyayFWbyVGdhd3LcV2Zh1Wa9M3clN2byBXLzN3btg3PwJWZ35CM3IjN2UDOwQzY4AzN4YjZyYzXyUTO1ADMzEzLchDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.webp)
跟蹤整個調用鍊路,最後是不是驚喜地發現了MappingProcessor的入口。
其實,這就是“JSR 269 Pluggable Annotation Processing API”規範,隻要程式實作了該API,就能在javac運作的時候得到調用。 舉例來說,現在有一個實作了"JSR 269 API"的程式A,那麼使用javac編譯源碼的時候具體流程如下:
- javac對源代碼進行分析,生成一棵抽象文法樹(AST) ;
- 運作過程中調用實作了"JSR 269 API"的A程式 ;
- 此時A程式就可以完成它自己的邏輯,包括修改第一步驟得到的抽象文法樹(AST) ;
- javac使用修改後的抽象文法樹(AST)生成位元組碼檔案.
mapstruct本質上就是這樣的一個實作了"JSR 269 API"的程式。在使用javac的過程中,它産生作用的具體流程如下:
2. 實作原理
2.1 架構主體
在上節中,mapsstruct利用的JSR269規範去掃描和生成的,但是從一個接口定義就能生成一個.class檔案,是不是還有點遙遠?
首先我們看下整個架構代碼的組成部分,主要分為兩個包: org.mapstruct:mapstruct:包含了必要的注解,例如@Mapping; org.mapstruct:mapstruct-processor:包含生成映射器實作的注解處理器。這個就是整個mapstruct架構的入口,繼承了注解處理器,在java compile時将會調用process做操作。
//支援@Mapper注解
@SupportedAnnotationTypes("org.mapstruct.Mapper")
public class MappingProcessor extends AbstractProcessor {
//處理入口
@Override
public boolean process(final Set annotations, final RoundEnvironment roundEnvironment) {
if ( !roundEnvironment.processingOver() ) {
RoundContext roundContext = new RoundContext( annotationProcessorContext );
Set deferredMappers = getAndResetDeferredMappers();
processMapperElements( deferredMappers, roundContext );
Set mappers = getMappers( annotations, roundEnvironment );
processMapperElements( mappers, roundContext );
}
return ANNOTATIONS_CLAIMED_EXCLUSIVELY;
}
}
複制代碼
- Element是一個接口,表示一個程式元素,它可以是包、類、方法或者一個變量。Element已知的子接口有:
- PackageElement 表示一個包程式元素。提供對有關包及其成員的資訊的通路。
- ExecutableElement 表示某個類或接口的方法、構造方法或初始化程式(靜态或執行個體),包括注釋類型元素。
- TypeElement 表示一個類或接口程式元素。提供對有關類型及其成員的資訊的通路。注意,枚舉類型是一種類,而注解類型是一種接口。
- VariableElement 表示一個字段、enum 常量、方法或構造方法參數、局部變量或異常參數。
2.2 processor處理鍊
在入口方法中可以看到,主要使用了processor來對解析生成過程進行處理,我們可以看到processor的相關定義。
其中每個process節點都繼承ModelElementProcessor基類。
注解處理器以及架構自帶的處理類 都以java SPI的方式使用ServiceClas加載進來了,主要實作方法在MappingProcessor#getProcessors中。這裡是用serviceClassLoader去加載所有定義好的Process類,形成類似于處理鍊,類似于責任鍊的一種方式(用數組記錄執行節點而不是用連結清單)
主要調用鍊路圖如下,每個process節點都有優先級,順序執行後将生成的内容寫到了檔案中。
3.怎樣debug注解處理器?
因為這個注解處理器是在解析->編譯的過程完成,跟普通的jar包調試不太一樣,maven架構為我們提供了調試入口,需要借助maven才能實作debug。是以隻需要在編譯過程打開debug才可調試。
- 在項目的pom檔案所在目錄執行mvnDebug compile
- 接着用idea打開項目,添加一個remote,端口為8000
- 打上斷點,debug 運作remote即可調試。
4.小結
本篇先簡短介紹了java動态編譯的過程,并用相關api接口實作了從源檔案到執行檔案的整個過程,并針對JSR269規範進行了注解掃描與處理過程,結合mapstruct 架構解析了生成轉換代碼的過程原理。希望大家對mapstruct利用JSR269規範生成代碼有所幫助,同樣的剖析思路可用于lombok/kotlin等文法糖的原理探究。
參考資料:
www.jianshu.com/p/26c88fbb5…
stackoverflow.com/questions/3…