天天看點

Mapstruct源碼解析- 架構實作原理

隻有用過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的調用過程就不詳細闡述,另外會開一篇講其中的原理。

Mapstruct源碼解析- 架構實作原理

跟蹤整個調用鍊路,最後是不是驚喜地發現了MappingProcessor的入口。

Mapstruct源碼解析- 架構實作原理

      其實,這就是“JSR 269 Pluggable Annotation Processing API”規範,隻要程式實作了該API,就能在javac運作的時候得到調用。 舉例來說,現在有一個實作了"JSR 269 API"的程式A,那麼使用javac編譯源碼的時候具體流程如下:

  1. javac對源代碼進行分析,生成一棵抽象文法樹(AST) ;
  2. 運作過程中調用實作了"JSR 269 API"的A程式 ;
  3. 此時A程式就可以完成它自己的邏輯,包括修改第一步驟得到的抽象文法樹(AST) ;
  4. javac使用修改後的抽象文法樹(AST)生成位元組碼檔案.

mapstruct本質上就是這樣的一個實作了"JSR 269 API"的程式。在使用javac的過程中,它産生作用的具體流程如下:

Mapstruct源碼解析- 架構實作原理

2. 實作原理

2.1 架構主體

       在上節中,mapsstruct利用的JSR269規範去掃描和生成的,但是從一個接口定義就能生成一個.class檔案,是不是還有點遙遠?

Mapstruct源碼解析- 架構實作原理
Mapstruct源碼解析- 架構實作原理

       首先我們看下整個架構代碼的組成部分,主要分為兩個包: 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的相關定義。

Mapstruct源碼解析- 架構實作原理

其中每個process節點都繼承ModelElementProcessor基類。

Mapstruct源碼解析- 架構實作原理

注解處理器以及架構自帶的處理類 都以java SPI的方式使用ServiceClas加載進來了,主要實作方法在MappingProcessor#getProcessors中。這裡是用serviceClassLoader去加載所有定義好的Process類,形成類似于處理鍊,類似于責任鍊的一種方式(用數組記錄執行節點而不是用連結清單)

Mapstruct源碼解析- 架構實作原理

主要調用鍊路圖如下,每個process節點都有優先級,順序執行後将生成的内容寫到了檔案中。

Mapstruct源碼解析- 架構實作原理

3.怎樣debug注解處理器?

因為這個注解處理器是在解析->編譯的過程完成,跟普通的jar包調試不太一樣,maven架構為我們提供了調試入口,需要借助maven才能實作debug。是以隻需要在編譯過程打開debug才可調試。

  • 在項目的pom檔案所在目錄執行mvnDebug compile
  • 接着用idea打開項目,添加一個remote,端口為8000
  • 打上斷點,debug 運作remote即可調試。
Mapstruct源碼解析- 架構實作原理

4.小結

       本篇先簡短介紹了java動态編譯的過程,并用相關api接口實作了從源檔案到執行檔案的整個過程,并針對JSR269規範進行了注解掃描與處理過程,結合mapstruct 架構解析了生成轉換代碼的過程原理。希望大家對mapstruct利用JSR269規範生成代碼有所幫助,同樣的剖析思路可用于lombok/kotlin等文法糖的原理探究。

參考資料:

​​www.jianshu.com/p/26c88fbb5…​​

​​stackoverflow.com/questions/3…​​