【dubbo源碼解析】 --- dubbo spi 機制(@SPI、@Adaptive)詳解

本文對應源碼地址:https://github.com/nieandsun/dubbo-study



上篇文章《【SPI】 — java spi 機制簡介》中, 可以看到,java spi 機制非常簡單, 就是讀取指定的配置文件, 將所有的類都加載到程序中。 而這種機制, 存在很多缺陷, 比如:

  • 所有實現類無論是否使用, 直接被加載, 可能存在浪費
  • 不能夠靈活控制什麼時候什麼時機, 匹配什麼實現, 功能太弱

Dubbo 基於自己的需要,對SPI 機制進行了增強, 本篇文章將簡單介紹一下 Dubbo 中的 SPI用法。


1 @SPI 標籤 及其使用簡介

與 Java SPI 實現類配置不同, Dubbo SPI 是通過鍵值對的方式進行配置, 這樣我們就可以按需加載指定的實現類。
@SPI註解的玩法如下:

  • (1)在某個接口上標註@SPI註解,表明此接口是 SPI 的擴展點
@SPI("b") //指定缺省實現類的別名
public interface InfoService {
    Object sayHello(String name);
}
  • (2)在META-INF下的dubbo文件夾下指定接口與實現類之間的映射關係,具體配置如下:

在這裏插入圖片描述
-(3)簡單看一下某一個實現類的源代碼:

public class InfoServiceAImpl implements InfoService {
    @Override
    public Object sayHello(String name) {
        System.out.println(name + ",你好,調通了A實現!");
        return name + ",你好,調通了A實現!";
    }
}
  • (4)通過(2)中指定的實現類的key獲取具體的實現類
 /**
  * dubbo spi類加載驗證
  * extensionLoader.getExtension("a") ---------- 取到對應的擴展類
  * extensionLoader.getDefaultExtension() ------ 取得SPI("b")指定的實現類(默認實現類)
  */
 @Test
 public void dubboSPI() {
     //獲取InfoService的 Loader 實例
     ExtensionLoader<InfoService> extensionLoader = ExtensionLoader.getExtensionLoader(InfoService.class);
     //取得a拓展類
     InfoService infoServiceA = extensionLoader.getExtension("a");
     infoServiceA.sayHello("yoyo");
     //取得b拓展類
     InfoService infoServiceB = extensionLoader.getDefaultExtension();
     infoServiceB.sayHello("nrsc");
 }
  • (5)測試結果如下

在這裏插入圖片描述


2 @Adaptive標籤 及其使用簡介


2.1 @Adaptive標註在類上的具體含義詳解 — 將該類作爲最佳適配類


2.1.1 場景介紹

試想假如我寫了一個接口,並想將其作爲dubbo的SPI擴展點,但卻並沒在該接口上標註一個【缺省實現類的別名】----> 也就是說沒有指定默認的實現類。
但是我又想讓該擴展點有一個【不得不走的默認實現】,並且在該默認實現裏我可以拿到其他非默認實現,這該怎麼搞呢???

有了以上需求後,回顧一下1中的用法,做個簡圖如下:
在這裏插入圖片描述
回顧完之後你應該會有這樣的思路:

我可以先通過extensionLoader.getExtension(【不得不走的默認實現】的別名),獲取到該默認實現類,然後在該默認實現裏再遍歷獲取到所有的非默認實現!!!

是的,可以做到,但是有點不夠優雅:

  • 首先,按照這種思路這個【不得不走的默認實現】的別名應該得寫死在調用的代碼裏
  • 其次,在這個不得不走的默認實現類裏,找其他非默認實現類的的方法還得自己去寫

而@Adaptive註解標註在類上的使用姿勢就優雅地解決了上訴問題。


2.1.2 @Adaptive標註在類上使用姿勢及其意義詳解

使用姿勢介紹
(1)在某個接口上標註@SPI註解,表明此接口是 SPI 的擴展點
在這裏插入圖片描述
(2)在某個具體實現類上標註上@Adaptive 表明該實現類是【不得不走的默認實現類】,如下:

@Adaptive
public class InfoServiceAImpl implements InfoService {

    @Override
    public Object sayHello(String name) {
        System.out.println(name + ",你好,調通了A實現!");
        return name + ",你好,調通了A實現!";
    }
}

(3)當然要像1中的(2)一樣,在META-INF下的dubbo文件夾下指定接口與實現類之間的映射關係

(4)測試類如下:

    @Test
    public void test1() {
        //獲取InfoService的ExtensionLoader
        ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
        InfoService adaptiveExtension = loader.getAdaptiveExtension(); //獲取到@Adaptive註解標註的實現類
        System.out.println(adaptiveExtension.sayHello("yoyo"));

        InfoService defaultExtension = loader.getDefaultExtension();//獲取接口上@SPI("別名")中別名指定的實現類
        System.out.println(defaultExtension.sayHello("nrsc"));
    }

(5)通過打斷點的方式來看一下,@Adaptive註解標註在類上如何優雅的解決了我在上面說的問題
在這裏插入圖片描述
由上圖可知,其解決思路如下:
在這裏插入圖片描述
這裏需要注意的是:

  • (1)生成的InfoService的ExtensionLoader會加入到ExtensionLoader中的一個ConcurrentMap緩存起來,因此我們假若在標有的@Adapter註解的實現類裏再獲取InfoService的ExtensionLoader,可以直接從ConcurrentMap緩存裏取
  • (2)遍歷並執行所有非@Adapter註解標註的實現類的姿勢如下:
 //獲取所有未標註@Adapter註解的實現類的別名,並通過別名獲取到實現類
 Set<String> supportedExtensions = loader.getSupportedExtensions();
 for (String name : supportedExtensions) {
     System.out.println(loader.getExtension(name).sayHello("MM"));
 }

2.2 @Adaptive標註在方法上具體含義詳解 — 靜態代理該方法


2.2.1 場景介紹

現在有這樣一個需求,一個接口下里有多個方法,但是我只想將某個方法暴漏給別人,該怎麼搞???

相信大家肯定都知道,解決方式主要有兩種:

  • 動態代理
  • 靜態代理

而將@Adaptive註解標註在方法上就可以完成對該方法進行靜態代理的工作 —》 據說因爲靜態代理比動態代理效率更高,所以dubbo採用了靜態代理 —> 有興趣的可以進行考證!!!


2.2.2 @Adaptive標註在方法上的使用姿勢

(1)
在某個接口上標註@SPI註解,表明此接口是 SPI 的擴展點
在該接口的某個方法上標註@Adapter註解,表明該方法需要被靜態代理
並且注意這個方法必須要有一個參數爲URL
在這裏插入圖片描述
(2)如1中的(2)一樣,在META-INF下的dubbo文件夾下指定接口與實現類之間的映射關係

(3)某一個實現類的源碼:

public class InfoServiceAImpl implements InfoService {
    @Override
    public Object sayHello(String name) {
        System.out.println(name + ",你好,調通了A實現!");
        return name + ",你好,調通了A實現!";
    }
    @Override
    public Object passInfo(String msg, URL url) {
        System.out.println("恭喜你,調通了A實現:" + url);
        return msg;
    }
}

(4)測試

【情況1】各個實現類上都沒有標註@Adaptive註解, 接口的SPI註解爲@SPI(“b”),url無參數的情況

/**
 * 各個實現類上都沒有@Adaptive
 * SPI上有註解@SPI("b"),
 * url無參數
 * 則以@SPI("b")爲準,選擇InfoServiceBImpl 實現中的方法進行靜態代理
 */
@Test
public void test1() {
    ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
    InfoService adaptiveExtension = loader.getAdaptiveExtension();
    URL url = URL.valueOf("test://localhost/test");
    System.out.println(adaptiveExtension.passInfo("yoyo", url));
}

測試結果如下:
在這裏插入圖片描述


【情況2】各個實現類上都沒有標註@Adaptive註解, 接口的SPI註解爲@SPI(“b”),URL中有具體的值info.service=a
注意:url中的info.service是按照接口名稱InfoService拆解的結果,dubbo會按此規則找到InfoService接口別名爲a的實現類,並對此實現類中的方法進行靜態代理

/**
 * 各個實現類上都沒有@Adaptive
 * SPI上有註解@SPI("b")
 * URL中有具體的值info.service=a,
 * 則以URL爲準,選擇InfoServiceAImpl 實現中的方法進行靜態代理
 */
@Test
public void test2() {
    ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
    InfoService adaptiveExtension = loader.getAdaptiveExtension();
    URL url = URL.valueOf("test://localhost/test?info.service=a");
    System.out.println(adaptiveExtension.passInfo("james", url));
}

測試結果如下:
在這裏插入圖片描述


【情況3】各個實現類上都沒有標註@Adaptive註解, 接口方法中加上的註解爲@Adaptive({“NRSC”}),URL中有具體的值爲NRSC=c

  • 此時InfoService接口應爲:
@SPI("b") //指定缺省實現類的別名
public interface InfoService {
    Object sayHello(String name);

    @Adaptive({"NRSC"})
    //@Adaptive
    Object passInfo(String msg, URL url);
}
  • 測試類爲:
/**
 * 各個實現類上面沒有@Adaptive
 * 接口方法中加上的註解爲@Adaptive({"NRSC"}),
 * URL中有具體的值NRSC=c,
 * 則以URL中的NRSC參數爲準,選擇InfoServiceCImpl 實現中的方法進行靜態代理
 */
@Test
public void test3() {
    ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
    InfoService adaptiveExtension = loader.getAdaptiveExtension();
    URL url = URL.valueOf("test://localhost/test?info.service=a&NRSC=c");
    System.out.println(adaptiveExtension.passInfo("james", url));
}
  • 測試結果如下:

在這裏插入圖片描述
由此可知這種方式其實是對 【情況2】的一種更優雅的實現!!!


【情況4】InfoServiceCImpl實現類上有@Adaptive, 接口方法中加上的註解爲@Adaptive({“NRSC”}),URL隨意傳值

/**
 * InfoServiceCImpl實現類上有@Adaptive,URL隨意傳值
 * 直接會拿到InfoServiceCImpl的實現類 ---》 並未做靜態代理
 */
@Test
public void test4() {
    ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
    InfoService adaptiveExtension = loader.getAdaptiveExtension();
    URL url = URL.valueOf("test://localhost/test");
    System.out.println(adaptiveExtension.passInfo("james", url));
}

測試結果:
在這裏插入圖片描述


2.3 簡單擼一下源碼,看看爲啥 @Adaptive標註在方法上會靜態代理該方法,標註在類上卻不會走靜態代理

打着斷點可以看到源碼中ExtensionLoader中創建具體實現類的關鍵代碼如下:
在這裏插入圖片描述
接下來我們看一下接口的任何實現類都沒有標註@Adapter註解時,dubbo爲我們拼接的代理類,即上圖中code對應的字符串,將其格式化後如下:

package com.nrsc.service;

import org.apache.dubbo.common.extension.ExtensionLoader;


public class InfoService$Adaptive implements com.nrsc.service.InfoService {
    public java.lang.Object passInfo(java.lang.String arg0,
        org.apache.dubbo.common.URL arg1) {
        if (arg1 == null) {
            throw new IllegalArgumentException("url == null");
        }

        org.apache.dubbo.common.URL url = arg1;
        String extName = url.getParameter("NRSC", "b");

        if (extName == null) {
            throw new IllegalStateException(
                "Failed to get extension (com.nrsc.service.InfoService) name from url (" +
                url.toString() + ") use keys([NRSC])");
        }
		//通過別名找到具體的實現類
        com.nrsc.service.InfoService extension = (com.nrsc.service.InfoService) ExtensionLoader.getExtensionLoader(com.nrsc.service.InfoService.class)
                                                                                               .getExtension(extName);
		//拿着實現類調用方法
        return extension.passInfo(arg0, arg1);
    }
	
	//未標註@Adapter註解的方法無法被調用
    public java.lang.Object sayHello(java.lang.String arg0) {
        throw new UnsupportedOperationException(
            "The method public abstract java.lang.Object com.nrsc.service.InfoService.sayHello(java.lang.String) of interface com.nrsc.service.InfoService is not adaptive method!");
    }
}

看到這段字符串,再聯繫一下上篇文章《【dubbo源碼解析】— 通過javassist/JavassistCompiler動態生成一個實例對象》,是不是就可以很清楚的明白下面的結論了:

  • @Adapter標籤標註在方法上可以對該方法進行靜態代理
  • 如果接口的某個實現類上標註了@Adapter註解,將直接調用該實現類,而不會進行靜態代理

end!!!

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