Java SPI機制簡介,及dubbo SPI

轉自:https://www.jianshu.com/p/46aa69643c97

SPI 簡介

SPI 全稱爲 (Service Provider Interface) ,是JDK內置的一種服務提供發現機制。 目前有不少框架用它來做服務的擴展發現, 簡單來說,它就是一種動態替換髮現的機制, 舉個例子來說, 有個接口,想運行時動態的給它添加實現,你只需要添加一個實現,

而後,把新加的實現,描述給JDK知道就行啦(通過改一個文本文件即可) 公司內部,目前Dubbo框架就基於SPI機制提供擴展功能。

簡單示例

通過一個簡單例子來說明SPI是如何使用的。 首先通過一張圖來看看,用SPI需要遵循哪些規範,因爲spi畢竟是JDK的一種標準。  

我們首先需要一個目錄,META-INF\services 如下,最終的目錄路徑就像這樣:

└── src
├── com
│   └── ivanzhang
│       └── spi
│           ├── HelloInterface.java
│           ├── impl
│           │   ├── ImageHello.java
│           │   └── TextHello.java
│           └── SPIMain.java
└── META-INF
    └── services
        └── com.ivanzhang.spi.HelloInterface

文件名字爲 接口/抽象類: 全名 文件內容: 接口/抽象類 實現類

就像這樣: com.ivanzhang.spi.impl.TextHello com.ivanzhang.spi.impl.ImageHello

接下來, 我們需要定義接口和實現類:

public interface HelloInterface {
  public void sayHello();
}

實現類:

public class TextHello implements HelloInterface {

  @Override
  public void sayHello() {
      System.out.println("Text Hello.");
  }

}

public class ImageHello implements HelloInterface {
  @Override
  public void sayHello() {
      System.out.println("Image Hello");
  }
}

最後,來看看,如果使用SPI機制,客戶端代碼:

public class SPIMain {
    public static void main(String[] args) {

        ServiceLoader<HelloInterface> loaders = 
              ServiceLoader.load(HelloInterface.class);

        for (HelloInterface in : loaders) {
            in.sayHello();
        }
    }
}

最後的輸出: Text Hello.Image Hello

 

Dubbo SPI

Java SPI的使用很簡單。也做到了基本的加載擴展點的功能。但Java SPI有以下的不足:

  • 需要遍歷所有的實現,並實例化,然後我們在循環中才能找到我們需要的實現。
  • 配置文件中只是簡單的列出了所有的擴展實現,而沒有給他們命名。導致在程序中很難去準確的引用它們。
  • 擴展如果依賴其他的擴展,做不到自動注入和裝配
  • 不提供類似於Spring的AOP功能
  • 擴展很難和其他的框架集成,比如擴展裏面依賴了一個Spring bean,原生的Java SPI不支持
  • 所以Java SPI應付一些簡單的場景是可以的,但對於Dubbo,它的功能還是比較弱的。Dubbo對原生SPI機制進行了一些擴展。接下來,我們就更深入地瞭解下Dubbo的SPI機制。

Dubbo擴展點機制基本概念

在深入學習Dubbo的擴展機制之前,我們先明確Dubbo SPI中的一些基本概念。在接下來的內容中,我們會多次用到這些術語。

  1. 擴展點(Extension Point)
    是一個Java的接口。
  2. 擴展(Extension)
    擴展點的實現類。
  3. 擴展實例(Extension Instance)
    擴展點實現類的實例。
  4. 擴展自適應實例(Extension Adaptive Instance)
    第一次接觸這個概念時,可能不太好理解(我第一次也是這樣的...)。如果稱它爲擴展代理類,可能更好理解些。擴展的自適應實例其實就是一個Extension的代理,它實現了擴展點接口。在調用擴展點的接口方法時,會根據實際的參數來決定要使用哪個擴展。比如一個IRepository的擴展點,有一個save方法。有兩個實現MysqlRepository和MongoRepository。IRepository的自適應實例在調用接口方法的時候,會根據save方法中的參數,來決定要調用哪個IRepository的實現。如果方法參數中有repository=mysql,那麼就調用MysqlRepository的save方法。如果repository=mongo,就調用MongoRepository的save方法。和麪向對象的延遲綁定很類似。爲什麼Dubbo會引入擴展自適應實例的概念呢?

    • Dubbo中的配置有兩種,一種是固定的系統級別的配置,在Dubbo啓動之後就不會再改了。還有一種是運行時的配置,可能對於每一次的RPC,這些配置都不同。比如在xml文件中配置了超時時間是10秒鐘,這個配置在Dubbo啓動之後,就不會改變了。但針對某一次的RPC調用,可以設置它的超時時間是30秒鐘,以覆蓋系統級別的配置。對於Dubbo而言,每一次的RPC調用的參數都是未知的。只有在運行時,根據這些參數才能做出正確的決定。
    • 很多時候,我們的類都是一個單例的,比如Spring的bean,在Spring bean都實例化時,如果它依賴某個擴展點,但是在bean實例化時,是不知道究竟該使用哪個具體的擴展實現的。這時候就需要一個代理模式了,它實現了擴展點接口,方法內部可以根據運行時參數,動態的選擇合適的擴展實現。而這個代理就是自適應實例。 自適應擴展實例在Dubbo中的使用非常廣泛,Dubbo中,每一個擴展都會有一個自適應類,如果我們沒有提供,Dubbo會使用字節碼工具爲我們自動生成一個。所以我們基本感覺不到自適應類的存在。後面會有例子說明自適應類是怎麼工作的。
  5. @SPI
    @SPI註解作用於擴展點的接口上,表明該接口是一個擴展點。可以被Dubbo的ExtentionLoader加載。如果沒有此ExtensionLoader調用會異常。
  6. @Adaptive
    @Adaptive註解用在擴展接口的方法上。表示該方法是一個自適應方法。Dubbo在爲擴展點生成自適應實例時,如果方法有@Adaptive註解,會爲該方法生成對應的代碼。方法內部會根據方法的參數,來決定使用哪個擴展。
  7. ExtentionLoader
    類似於Java SPI的ServiceLoader,負責擴展的加載和生命週期維護。
  8. 擴展別名
    和Java SPI不同,Dubbo中的擴展都有一個別名,用於在應用中引用它們。比如

random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance 
其中的random,roundrobin就是對應擴展的別名。這樣我們在配置文件中使用random或roundrobin就可以了。

  1. 一些路徑
    和Java SPI從/META-INF/services目錄加載擴展配置類似,Dubbo也會從以下路徑去加載擴展配置文件:
  • META-INF/dubbo/internal
  • META-INF/dubbo
  • META-INF/services

Dubbo的LoadBalance擴展點解讀

在瞭解了Dubbo的一些基本概念後,讓我們一起來看一個Dubbo中實際的擴展點,對這些概念有一個更直觀的認識。
我們選擇的是Dubbo中的LoadBalance擴展點。Dubbo中的一個服務,通常有多個Provider,consumer調用服務時,需要在多個Provider中選擇一個。這就是一個LoadBalance。我們一起來看看在Dubbo中,LoadBalance是如何成爲一個擴展點的。

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

    /**
     * select one invoker in list.
     *
     * @param invokers   invokers.
     * @param url        refer url
     * @param invocation invocation.
     * @return selected invoker.
     */
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

LoadBalance接口只有一個select方法。select方法從多個invoker中選擇其中一個。上面代碼中和Dubbo SPI相關的元素有:

  • @SPI(RandomLoadBalance.NAME) @SPI作用於LoadBalance接口,表示接口LoadBalance是一個擴展點。如果沒有@SPI註解,試圖去加載擴展時,會拋出異常。@SPI註解有一個參數,該參數表示該擴展點的默認實現的別名。如果沒有顯示的指定擴展,就使用默認實現。RandomLoadBalance.NAME是一個常量,值是"random",是一個隨機負載均衡的實現。 random的定義在配置文件META-INF/dubbo/internal/com.alibaba.dubbo.rpc.cluster.LoadBalance中:
random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance 
roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
leastactive=com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
consistenthash=com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance

可以看到文件中定義了4個LoadBalance的擴展實現。由於負載均衡的實現不是本次的內容,這裏就不過多說明。只用知道Dubbo提供了4種負載均衡的實現,我們可以通過xml文件,properties文件,JVM參數顯式的指定一個實現。如果沒有,默認使用隨機。

  • @Adaptive("loadbalance") @Adaptive註解修飾select方法,表明方法select方法是一個可自適應的方法。Dubbo會自動生成該方法對應的代碼。當調用select方法時,會根據具體的方法參數來決定調用哪個擴展實現的select方法。@Adaptive註解的參數loadbalance表示方法參數中的loadbalance的值作爲實際要調用的擴展實例。 但奇怪的是,我們發現select的方法中並沒有loadbalance參數,那怎麼獲取loadbalance的值呢?select方法中還有一個URL類型的參數,Dubbo就是從URL中獲取loadbalance的值的。這裏涉及到Dubbo的URL總線模式,簡單說,URL中包含了RPC調用中的所有參數。URL類中有一個Map parameters字段,parameters中就包含了loadbalance。
  1. 獲取LoadBalance擴展
    Dubbo中獲取LoadBalance的代碼如下:
LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName); 

使用ExtensionLoader.getExtensionLoader(LoadBalance.class)方法獲取一個ExtensionLoader的實例,然後調用getExtension,傳入一個擴展的別名來獲取對應的擴展實例。

ExtensionLoader中含有一個靜態屬性:
ConcurrentMap, ExtensionLoader>EXTENSION_LOADERS = new ConcurrentHashMap, ExtensionLoader>();
用於緩存所有的擴展加載實例,這裏加載LoadBalance.class,就以LoadBalance.class爲key,創建的ExtensionLoader爲value存儲到上述EXTENSION_LOADERS中。

自定義一個LoadBalance擴展

本節中,我們通過一個簡單的例子,來自己實現一個LoadBalance,並把它集成到Dubbo中。我會列出一些關鍵的步驟和代碼。

  1. 實現LoadBalance接口
    首先,編寫一個自己實現的LoadBalance,因爲是爲了演示Dubbo的擴展機制,而不是LoadBalance的實現,所以這裏LoadBalance的實現非常簡單,選擇第一個invoker,並在控制檯輸出一條日誌。
package com.dubbo.spi.demo.consumer;

public class DemoLoadBalance implements LoadBalance {

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        System.out.println("DemoLoadBalance : Select the first invoker...");
        return invokers.get(0);
    }
}
  1. 添加擴展配置文件
    添加文件:META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance。文件內容如下:
demo=com.dubbo.spi.demo.consumer.DemoLoadBalance
  1. 配置使用自定義LoadBalance
    通過上面的兩步,已經添加了一個名字爲demo的LoadBalance實現,並在配置文件中進行了相應的配置。接下來,需要顯式的告訴Dubbo使用demo的負載均衡實現。如果是通過spring的方式使用Dubbo,可以在xml文件中進行設置。


在consumer端的dubbo:reference中配置

<dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" />
  1. 啓動Dubbo
    啓動Dubbo,調用一次IHelloService,可以看到控制檯會輸出一條DemoLoadBalance: Select the first invoker...日誌。說明Dubbo的確是使用了我們自定義的LoadBalance。

總結一下,Dubbo SPI有以下的特點:

• 對Dubbo進行擴展,不需要改動Dubbo的源碼
• 自定義的Dubbo的擴展點實現,是一個普通的Java類,Dubbo沒有引入任何Dubbo特有的元素,對代碼侵入性幾乎爲零。
• 將擴展註冊到Dubbo中,只需要在ClassPath中添加配置文件。使用簡單。而且不會對現有代碼造成影響。符合開閉原則。
• Dubbo的擴展機制支持IoC,AOP等高級功能
• Dubbo的擴展機制能很好的支持第三方IoC容器,默認支持Spring Bean,可自己擴展來支持其他容器,比如Google的Guice。
• 切換擴展點的實現,只需要在配置文件中修改具體的實現,不需要改代碼。使用方便。

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