日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

Dubbo 源码分析 - SPI 机制

發(fā)布時間:2025/3/21 18 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Dubbo 源码分析 - SPI 机制 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

1.簡介

SPI 全稱為 Service Provider Interface,是一種服務(wù)發(fā)現(xiàn)機制。SPI 的本質(zhì)是將接口實現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件,加載實現(xiàn)類。這樣可以在運行時,動態(tài)為接口加載實現(xiàn)類。正因此特性,我們可以很容易的通過 SPI 機制為我們的程序提供拓展功能。SPI 機制在第三方框架中也有所應(yīng)用,比如 Dubbo 就是通過 SPI 機制加載所有的組件。不過,Dubbo 并未使用 Java 原生的 SPI 機制,而是對其進(jìn)行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的模塊。如果大家想要學(xué)習(xí) Dubbo 的源碼,SPI 機制務(wù)必弄懂。下面,我們先來了解一下 Java SPI 與 Dubbo SPI 的使用方法,然后再來分析 Dubbo SPI 的源碼。

?2.SPI 示例

?2.1 Java SPI 示例

前面簡單介紹了 SPI 機制的原理,本節(jié)通過一個示例來演示 JAVA SPI 的使用方法。首先,我們定義一個接口,名稱為 Robot。

1 2 3 public interface Robot {void sayHello(); }

接下來定義兩個實現(xiàn)類,分別為擎天柱 OptimusPrime 和大黃蜂 Bumblebee。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class OptimusPrime implements Robot {@Overridepublic void sayHello() {System.out.println("Hello, I am Optimus Prime.");} }public class Bumblebee implements Robot {@Overridepublic void sayHello() {System.out.println("Hello, I am Bumblebee.");} }

接下來 META-INF/services 文件夾下創(chuàng)建一個文件,名稱為 Robot 的全限定名 com.tianxiaobo.spi.Robot。文件內(nèi)容為實現(xiàn)類的全限定的類名,如下:

1 2 com.tianxiaobo.spi.OptimusPrime com.tianxiaobo.spi.Bumblebee

做好了所需的準(zhǔn)備工作,接下來編寫代碼進(jìn)行測試。

1 2 3 4 5 6 7 8 9 public class JavaSPITest {@Testpublic void sayHello() throws Exception {ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);System.out.println("Java SPI");serviceLoader.forEach(Robot::sayHello);} }

最后來看一下測試結(jié)果,如下:

從測試結(jié)果可以看出,我們的兩個實現(xiàn)類被成功的加載,并輸出了相應(yīng)的內(nèi)容。關(guān)于 Java SPI 的演示先到這,接下來演示 Dubbo SPI。

?2.2 Dubbo SPI 示例

Dubbo 并未使用 Java SPI,而是重新實現(xiàn)了一套功能更強的 SPI 機制。Dubbo SPI 的相關(guān)邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實現(xiàn)類。Dubbo SPI 的實現(xiàn)類配置放置在 META-INF/dubbo 路徑下,下面來看一下配置內(nèi)容。

1 2 optimusPrime = com.tianxiaobo.spi.OptimusPrime bumblebee = com.tianxiaobo.spi.Bumblebee

與 Java SPI 實現(xiàn)類配置不同,Dubbo SPI 是通過鍵值對的方式進(jìn)行配置,這樣我們就可以按需加載指定的實現(xiàn)類了。另外,在測試 Dubbo SPI 時,需要在 Robot 接口上標(biāo)注 @SPI 注解。下面來演示一下 Dubbo SPI 的使用方式:

1 2 3 4 5 6 7 8 9 10 11 12 public class DubboSPITest {@Testpublic void sayHello() throws Exception {ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);Robot optimusPrime = extensionLoader.getExtension("optimusPrime");optimusPrime.sayHello();Robot bumblebee = extensionLoader.getExtension("bumblebee");bumblebee.sayHello();} }

測試結(jié)果如下:

演示完 Dubbo SPI,下面來看看 Dubbo SPI 對 Java SPI 做了哪些改進(jìn),以下內(nèi)容引用至 Dubbo 官方文檔。

  • JDK 標(biāo)準(zhǔn)的 SPI 會一次性實例化擴展點所有實現(xiàn),如果有擴展實現(xiàn)初始化很耗時,但如果沒用上也加載,會很浪費資源。
  • 如果擴展點加載失敗,連擴展點的名稱都拿不到了。比如:JDK 標(biāo)準(zhǔn)的 ScriptEngine,通過 getName() 獲取腳本類型的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導(dǎo)致 RubyScriptEngine 類加載失敗,這個失敗原因被吃掉了,和 ruby 對應(yīng)不起來,當(dāng)用戶執(zhí)行 ruby 腳本時,會報不支持 ruby,而不是真正失敗的原因。
  • 增加了對擴展點 IOC 和 AOP 的支持,一個擴展點可以直接 setter 注入其它擴展點。

在以上改進(jìn)項中,第一個改進(jìn)項比較好理解。第二個改進(jìn)項沒有進(jìn)行驗證,就不多說了。第三個改進(jìn)項是增加了對 IOC 和 AOP 的支持,這是什么意思呢?這里簡單解釋一下,Dubbo SPI 加載完拓展實例后,會通過該實例的 setter 方法解析出實例依賴項的名稱。比如通過 setProtocol 方法名,可知道目標(biāo)實例依賴 Protocal。知道了具體的依賴,接下來即可到 IOC 容器中尋找或生成一個依賴對象,并通過 setter 方法將依賴注入到目標(biāo)實例中。說完 Dubbo IOC,接下來說說 Dubbo AOP。Dubbo AOP 是指使用 Wrapper 類(可自定義實現(xiàn))對拓展對象進(jìn)行包裝,Wrapper 類中包含了一些自定義邏輯,這些邏輯可在目標(biāo)方法前行前后被執(zhí)行,類似 AOP。Dubbo AOP 實現(xiàn)的很簡單,其實就是個代理模式。這個官方文檔中有所說明,大家有興趣可以查閱一下。

關(guān)于 Dubbo SPI 的演示,以及與 Java SPI 的對比就先這么多,接下來加入源碼分析階段。

?3. Dubbo SPI 源碼分析

上一章,我簡單演示了 Dubbo SPI 的使用方法。我們首先通過 ExtensionLoader 的 getExtensionLoader 方法獲取一個 ExtensionLoader 實例,然后再通過 ExtensionLoader 的 getExtension 方法獲取拓展類對象。這其中,getExtensionLoader 用于從緩存中獲取與拓展類對應(yīng)的 ExtensionLoader,若緩存未命中,則創(chuàng)建一個新的實例。該方法的邏輯比較簡單,本章就不就行分析了。下面我們從 ExtensionLoader 的 getExtension 方法作為入口,對拓展類對象的獲取過程進(jìn)行詳細(xì)的分析。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public T getExtension(String name) {if (name == null || name.length() == 0)throw new IllegalArgumentException("Extension name == null");if ("true".equals(name)) {// 獲取默認(rèn)的拓展實現(xiàn)類return getDefaultExtension();}// Holder 僅用于持有目標(biāo)對象,沒其他什么邏輯Holder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<Object>());holder = cachedInstances.get(name);}Object instance = holder.get();if (instance == null) {synchronized (holder) {instance = holder.get();if (instance == null) {// 創(chuàng)建拓展實例,并設(shè)置到 holder 中instance = createExtension(name);holder.set(instance);}}}return (T) instance; }

上面代碼的邏輯比較簡單,首先檢查緩存,緩存未命中則創(chuàng)建拓展對象。下面我們來看一下創(chuàng)建拓展對象的過程是怎樣的。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private T createExtension(String name) {// 從配置文件中加載所有的拓展類,形成配置項名稱到配置類的映射關(guān)系Class<?> clazz = getExtensionClasses().get(name);if (clazz == null) {throw findException(name);}try {T instance = (T) EXTENSION_INSTANCES.get(clazz);if (instance == null) {// 通過反射創(chuàng)建實例EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());instance = (T) EXTENSION_INSTANCES.get(clazz);}// 向?qū)嵗凶⑷胍蕾噄njectExtension(instance);Set<Class<?>> wrapperClasses = cachedWrapperClasses;if (wrapperClasses != null && !wrapperClasses.isEmpty()) {// 循環(huán)創(chuàng)建 Wrapper 實例for (Class<?> wrapperClass : wrapperClasses) {// 將當(dāng)前 instance 作為參數(shù)創(chuàng)建 Wrapper 實例,然后向 Wrapper 實例中注入屬性值,// 并將 Wrapper 實例賦值給 instanceinstance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));}}return instance;} catch (Throwable t) {throw new IllegalStateException("...");} }

createExtension 方法的邏輯稍復(fù)雜一下,包含了如下的步驟:

  • 通過 getExtensionClasses 獲取所有的拓展類
  • 通過反射創(chuàng)建拓展對象
  • 向拓展對象中注入依賴
  • 將拓展對象包裹在相應(yīng)的 Wrapper 對象中
  • 以上步驟中,第一個步驟是加載拓展類的關(guān)鍵,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實現(xiàn)。在接下來的章節(jié)中,我將會重點分析 getExtensionClasses 方法的邏輯,以及簡單分析 Dubbo IOC 的具體實現(xiàn)。

    ?3.1 獲取所有的拓展類

    我們在通過名稱獲取拓展類之前,首先需要根據(jù)配置文件解析出名稱到拓展類的映射,也就是 Map<名稱, 拓展類>。之后再從 Map 中取出相應(yīng)的拓展類即可。相關(guān)過程的代碼分析如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private Map<String, Class<?>> getExtensionClasses() {// 從緩存中獲取已加載的拓展類Map<String, Class<?>> classes = cachedClasses.get();if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {// 加載拓展類classes = loadExtensionClasses();cachedClasses.set(classes);}}}return classes; }

    這里也是先檢查緩存,若緩存未命中,則通過 synchronized 加鎖。加鎖后再次檢查緩存,并判空。此時如果 classes 仍為 null,則加載拓展類。以上代碼的寫法是典型的雙重檢查鎖,前面所分析的 getExtension 方法中有相似的代碼。關(guān)于雙重檢查就說這么多,下面分析 loadExtensionClasses 方法的邏輯。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private Map<String, Class<?>> loadExtensionClasses() {// 獲取 SPI 注解,這里的 type 是在調(diào)用 getExtensionLoader 方法時傳入的final SPI defaultAnnotation = type.getAnnotation(SPI.class);if (defaultAnnotation != null) {String value = defaultAnnotation.value();if ((value = value.trim()).length() > 0) {// 對 SPI 注解內(nèi)容進(jìn)行切分String[] names = NAME_SEPARATOR.split(value);// 檢測 SPI 注解內(nèi)容是否合法,不合法則拋出異常if (names.length > 1) {throw new IllegalStateException("...");}// 設(shè)置默認(rèn)名稱,cachedDefaultName 用于加載默認(rèn)實現(xiàn),參考 getDefaultExtension 方法if (names.length == 1) {cachedDefaultName = names[0];}}}Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();// 加載指定文件夾配置文件loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);loadDirectory(extensionClasses, DUBBO_DIRECTORY);loadDirectory(extensionClasses, SERVICES_DIRECTORY);return extensionClasses; }

    loadExtensionClasses 方法總共做了兩件事情,一是對 SPI 注解進(jìn)行解析,二是調(diào)用 loadDirectory 方法加載指定文件夾配置文件。SPI 注解解析過程比較簡單,無需多說。下面我們來看一下 loadDirectory 做了哪些事情。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {// fileName = 文件夾路徑 + type 全限定名 String fileName = dir + type.getName();try {Enumeration<java.net.URL> urls;ClassLoader classLoader = findClassLoader();if (classLoader != null) {// 根據(jù)文件名加載所有的同名文件urls = classLoader.getResources(fileName);} else {urls = ClassLoader.getSystemResources(fileName);}if (urls != null) {while (urls.hasMoreElements()) {java.net.URL resourceURL = urls.nextElement();// 加載資源loadResource(extensionClasses, classLoader, resourceURL);}}} catch (Throwable t) {logger.error("...");} }

    loadDirectory 方法代碼不多,理解起來不難。該方法先通過 classLoader 獲取所有資源鏈接,然后再通過 loadResource 方法加載資源。我們繼續(xù)跟下去,看一下 loadResource 方法的實現(xiàn)。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {try {BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));try {String line;// 按行讀取配置內(nèi)容while ((line = reader.readLine()) != null) {final int ci = line.indexOf('#');if (ci >= 0) {// 截取 # 之前的字符串,# 之后的內(nèi)容為注釋line = line.substring(0, ci);}line = line.trim();if (line.length() > 0) {try {String name = null;int i = line.indexOf('=');if (i > 0) {// 以 = 為界,截取鍵與值。比如 dubbo=com.alibaba....DubboProtocolname = line.substring(0, i).trim();line = line.substring(i + 1).trim();}if (line.length() > 0) {// 加載解析出來的限定類名loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);}} catch (Throwable t) {IllegalStateException e = new IllegalStateException("...");}}}} finally {reader.close();}} catch (Throwable t) {logger.error("...");} }

    loadResource 方法用于讀取和解析配置文件,并通過反射加載類,最后調(diào)用 loadClass 方法進(jìn)行其他操作。loadClass 方法有點名不副實,它的功能只是操作緩存,而非加載類。該方法的邏輯如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {if (!type.isAssignableFrom(clazz)) {throw new IllegalStateException("...");}if (clazz.isAnnotationPresent(Adaptive.class)) { // 檢測目標(biāo)類上是否有 Adaptive 注解if (cachedAdaptiveClass == null) {// 設(shè)置 cachedAdaptiveClass緩存cachedAdaptiveClass = clazz;} else if (!cachedAdaptiveClass.equals(clazz)) {throw new IllegalStateException("...");}} else if (isWrapperClass(clazz)) { // 檢測 clazz 是否是 Wrapper 類型Set<Class<?>> wrappers = cachedWrapperClasses;if (wrappers == null) {cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();wrappers = cachedWrapperClasses;}// 存儲 clazz 到 cachedWrapperClasses 緩存中wrappers.add(clazz);} else { // 程序進(jìn)入此分支,表明是一個普通的拓展類// 檢測 clazz 是否有默認(rèn)的構(gòu)造方法,如果沒有,則拋出異常clazz.getConstructor();if (name == null || name.length() == 0) {// 如果 name 為空,則嘗試從 Extension 注解獲取 name,或使用小寫的類名作為 namename = findAnnotationName(clazz);if (name.length() == 0) {throw new IllegalStateException("...");}}// 切分 nameString[] names = NAME_SEPARATOR.split(name);if (names != null && names.length > 0) {Activate activate = clazz.getAnnotation(Activate.class);if (activate != null) {// 如果類上有 Activate 注解,則使用 names 數(shù)組的第一個元素作為鍵,// 存儲 name 到 Activate 注解對象的映射關(guān)系cachedActivates.put(names[0], activate);}for (String n : names) {if (!cachedNames.containsKey(clazz)) {// 存儲 Class 到名稱的映射關(guān)系cachedNames.put(clazz, n);}Class<?> c = extensionClasses.get(n);if (c == null) {// 存儲名稱到 Class 的映射關(guān)系extensionClasses.put(n, clazz);} else if (c != clazz) {throw new IllegalStateException("...");}}}} }

    如上,loadClass 方法操作了不同的緩存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,該方法沒有其他什么邏輯了,就不多說了。

    到此,關(guān)于緩存類加載的過程就分析完了。整個過程沒什么特別復(fù)雜的地方,大家按部就班的分析就行了,不懂的地方可以調(diào)試一下。接下來,我們來聊聊 Dubbo IOC 方面的內(nèi)容。

    ?3.2 Dubbo IOC

    Dubbo IOC 是基于 setter 方法注入依賴。Dubbo 首先會通過反射獲取到實例的所有方法,然后再遍歷方法列表,檢測方法名是否具有 setter 方法特征。若有,則通過 ObjectFactory 獲取依賴對象,最后通過反射調(diào)用 setter 方法將依賴設(shè)置到目標(biāo)對象中。整個過程對應(yīng)的代碼如下:

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private T injectExtension(T instance) {try {if (objectFactory != null) {// 遍歷目標(biāo)類的所有方法for (Method method : instance.getClass().getMethods()) {// 檢測方法是否以 set 開頭,且方法僅有一個參數(shù),且方法訪問級別為 publicif (method.getName().startsWith("set")&& method.getParameterTypes().length == 1&& Modifier.isPublic(method.getModifiers())) {// 獲取 setter 方法參數(shù)類型Class<?> pt = method.getParameterTypes()[0];try {// 獲取屬性名String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";// 從 ObjectFactory 中獲取依賴對象Object object = objectFactory.getExtension(pt, property);if (object != null) {// 通過反射調(diào)用 setter 方法設(shè)置依賴method.invoke(instance, object);}} catch (Exception e) {logger.error("...");}}}}} catch (Exception e) {logger.error(e.getMessage(), e);}return instance; }

    在上面代碼中,objectFactory 變量的類型為 AdaptiveExtensionFactory,AdaptiveExtensionFactory 內(nèi)部維護(hù)了一個 ExtensionFactory 列表,用于存儲其他類型的 ExtensionFactory。Dubbo 目前提供了兩種 ExtensionFactory,分別是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于創(chuàng)建自適應(yīng)的拓展,關(guān)于自適應(yīng)拓展,我將會在下一篇文章中進(jìn)行說明。SpringExtensionFactory 則是到 Spring 的 IOC 容器中獲取所需拓展,該類的實現(xiàn)并不復(fù)雜,大家自行分析源碼,這里就不多說了。

    Dubbo IOC 的實現(xiàn)比較簡單,僅支持 setter 方式注入。總的來說,邏輯簡單易懂。

    ?4.總結(jié)

    本篇文章簡單介紹了 Java SPI 與 Dubbo SPI 用法與區(qū)別,并對 Dubbo SPI 的部分源碼進(jìn)行了分析。在 Dubbo SPI 中還有一塊重要的邏輯沒有進(jìn)行分析,那就是 Dubbo SPI 的擴展點自適應(yīng)機制。該機制的邏輯較為復(fù)雜,我將會在下一篇文章中進(jìn)行分析。好了,其他的就不多說了,本篇文件就先到這里了。

    • 本文鏈接:?https://www.tianxiaobo.com/2018/10/01/Dubbo-源碼分析-SPI-機制/

    http://www.tianxiaobo.com/2018/10/01/Dubbo-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-SPI-%E6%9C%BA%E5%88%B6/?

    總結(jié)

    以上是生活随笔為你收集整理的Dubbo 源码分析 - SPI 机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。