擴展點動態編譯的實現
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的主要抽象邏輯如下:
- 通過正則匹配出包路徑、類名,再根據包路徑、類名拼接處全路徑類名。
- 嘗試通過Class.forName記載該類並返回,防止重複編譯。如果類加載器中沒有該類,則進入第三步。
- 調用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對象。
具體步驟如下:
- 初始化Javassist,設置默認參數,如設置當前的classpath。
- 通過正則匹配出所有import的包,並使用Javassist添加import。
- 通過正則匹配出所有extends的包,創建Class對象,並使用Javassist添加extends。
- 通過正則匹配出所有implements包,並使用Javassist添加implements。
- 通過正則匹配出類裏面所有內容,即得到{}中的內容,再通過正則匹配出所有方法,並使用Javassist添加類方法。
- 生成Class對象。
JDK動態代碼編譯
JdkCompiler是Dubbo編譯器的另一種實現,使用了JDK自帶的編譯器,原生JDK編譯器包位於java.tools
下。主要使用了三個東西:JavaFileObject接口、ForwardingJavaFileManager接口、JavaCompiler.CompilationTask方法
。
整個動態編譯過程可以簡單地總結爲:首先初始化一個JavaFileObject對象,並把字符串作爲參數傳入構造方法,然後調用JavaCompiler.CompilationTask方法編譯出具體的類。JavaFileManager負責管理類文件輸入/輸出的位置。
-
JavaFileObject接口:
字符串代碼會被包裝成一個文件對象,並提供獲取二進制流的接口。Dubbo框架中的JavaFileObjectImpl類可以看做該接口的一種擴展實現,構造方法中需要傳入生成好的字符串代碼,此文件對象的輸入和輸入都是ByteArray流。 -
JavaFileManager接口:
主要管理文件的讀取和輸出位置。JDK中沒有可以直接使用的實現類,唯一的實現鱷梨ForwardingJavaFileManager構造器又是protect類型。因此Dubbo中定製化實現了一個JavaFileManagerImpl類,並通過一個自定義類加載器ClassLoaderImpl完成資源加載。 -
JavaCompiler.CompilationTask:
把JavaFileObject對象編譯層具體的類。