Dubbo——擴展點動態編譯的實現

擴展點動態編譯的實現

Dubbo SPI的自適應特性讓整個框架非常靈活,而動態編譯又是自適應特性的基礎,因爲動態生成的自適應類只是字符串,需要通過編譯才能得到真正的Class。雖然我們可以使用反射來動態代理一個類,但是在性能上和直接編譯好的Class會有一定差距。Dubbo SPI通過代碼的動態生成,並配合動態編譯器,靈活地在原始類基礎上創建新的自適應類。

總體結構

Dubbo中有三種代碼編譯器,分別是JDK編譯器、Javassist編譯器和AdaptiveCompiler編譯器。這幾種編譯器都實現了Compiler接口,編譯器類之間的關係如下圖:

在這裏插入圖片描述
Compiler接口上含有一個SPI註解,註解的默認值是@SPI("javassist"),很明顯,Javassist編譯器將作爲默認編譯器。如果用戶想改變默認編譯器,則可以通過<dubbo:application compiler="jdk" />標籤進行設置。

AdaptiveCompiler上面有@Adaptive註解,說明AdaptiveCompiler會固定爲默認實現,這個Compiler的主要作用和AdaptiveExtensionFactory相似,就是爲了管理其他compiler,如下圖所示:
在這裏插入圖片描述

AdaptiveCompiler#setDefaultCompiler方法會在ApplicationConfig中被調用,也就是Dubbo在啓動時,救護解析<dubbo:application compiper="jdk" />標籤,獲取設置的值,初始化對應的編譯器。如果沒有標籤設置,則使用@SPI("javassist")中的設置,即javassistCompiler。

然後看一下AbstractCompiler,它是一個抽象類,無法實例化,但在裏面封裝了通用的模板邏輯。還定義了一個抽象方法doCompile,留給子類實現的編譯邏輯。JavassistCompiler和JDKCompiler都實現了這個抽象方法。

AbstractCompiler的主要抽象邏輯如下:

  1. 通過正則匹配出包路徑、類名,再根據包路徑、類名拼接處全路徑類名。
  2. 嘗試通過Class.forName記載該類並返回,防止重複編譯。如果類加載器中沒有該類,則進入第三步。
  3. 調用doCompiler方法進行編譯。

Javassist動態代碼編譯

Java中動態生成Class的方式有恩多,可以直接基於字節碼的方式生成,常見的工具庫有CGLIB、ASM、Javassist等。而自適應擴展點使用了生成字符串代碼再編譯爲Class的方式。

Javassist使用示例:

@Test
public void test_javassist() throws CannotCompileException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    //初始化Javassist類池
    ClassPool classPool = ClassPool.getDefault();
    //創建一個Hello World類
    CtClass ctClass = classPool.makeClass("Hello World");
    //添加一個test方法,會打印Hello World,直接傳入方法的字符串
    CtMethod method = CtMethod.make("" +
            "public static void test(){" +
            "System.out.println(\"Hello World\");" +
            "}", ctClass);
    ctClass.addMethod(method);
    //生成類
    Class aClass = ctClass.toClass();
    //通過反射調用這個類實例
    Object object = aClass.newInstance();
    Method m = aClass.getDeclaredMethod("test", null);
    m.invoke(object, null);
}

在這裏插入圖片描述

由於之前已經生成了代碼字符串,因此在JavassistCompiler中,就是不斷通過正則表達式匹配不同部位的代碼,然後調用Javassist庫中的API生成不同部位的代碼,最後得到一個完整的Class對象。

具體步驟如下:

  1. 初始化Javassist,設置默認參數,如設置當前的classpath。
  2. 通過正則匹配出所有import的包,並使用Javassist添加import。
  3. 通過正則匹配出所有extends的包,創建Class對象,並使用Javassist添加extends。
  4. 通過正則匹配出所有implements包,並使用Javassist添加implements。
  5. 通過正則匹配出類裏面所有內容,即得到{}中的內容,再通過正則匹配出所有方法,並使用Javassist添加類方法。
  6. 生成Class對象。

JDK動態代碼編譯

JdkCompiler是Dubbo編譯器的另一種實現,使用了JDK自帶的編譯器,原生JDK編譯器包位於java.tools下。主要使用了三個東西:JavaFileObject接口、ForwardingJavaFileManager接口、JavaCompiler.CompilationTask方法

整個動態編譯過程可以簡單地總結爲:首先初始化一個JavaFileObject對象,並把字符串作爲參數傳入構造方法,然後調用JavaCompiler.CompilationTask方法編譯出具體的類。JavaFileManager負責管理類文件輸入/輸出的位置。

  1. JavaFileObject接口:字符串代碼會被包裝成一個文件對象,並提供獲取二進制流的接口。Dubbo框架中的JavaFileObjectImpl類可以看做該接口的一種擴展實現,構造方法中需要傳入生成好的字符串代碼,此文件對象的輸入和輸入都是ByteArray流。

  2. JavaFileManager接口: 主要管理文件的讀取和輸出位置。JDK中沒有可以直接使用的實現類,唯一的實現鱷梨ForwardingJavaFileManager構造器又是protect類型。因此Dubbo中定製化實現了一個JavaFileManagerImpl類,並通過一個自定義類加載器ClassLoaderImpl完成資源加載。

  3. JavaCompiler.CompilationTask:把JavaFileObject對象編譯層具體的類。

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