前言:開閉原則一直是軟體開發領域中所追求的,開閉原則中的"開"是指對於元件功能的擴充套件是開放的,是允許對其進行功能擴充套件的,“閉”,是指對於原有程式碼的修改是封閉的,即不應該修改原有的程式碼。對於一個高度整合化的、成熟、穩健的系統來講,永遠不是封閉、固守的,它需要向外提供一定的可擴充套件的能力,外部的實現類或者jar包都可以呼叫它。在面向物件的開發領域中,介面是對系統功能的高度抽象,因為SPI可謂是"應運而生",本篇部落格就開始走進SPI,探究java自身的SPI和Dubbo的SPI到底是什麼原理

目錄

一:SPI是什麼

二:jdk的SPI

三:dubbo的SPI

四:總結

正文

一:SPI是什麼?

spi全稱英文是service provider Interface,翻譯成中文也就是服務提供介面,在jdk 1.6開始,就已經提供了SPI.它的使用比較簡單。即在專案的類路徑下提供一個META/services/xx檔案,配置一個檔案,檔名為介面的全路徑的名稱,內容為具體的實現類全路徑名。jdk將會使用ServiceLoader.load()方法去解析和載入介面和其中的實現類,按需執行不同的方法。

舉個簡單的例子:在jdbc中,jdk提供了driver(資料庫)介面,但是不同的廠商實現起來的方式不同,比如mysql、oracle、sqlLite等廠商底層的實現邏輯都是不同的,因此在對資料庫驅動driver實現方式上,可以採用SPI機制。比如在mysql-contactor.jar包中會在META/services路徑下,這裡相當於擴充套件了java.sql.Driver介面,jdk會在META/services路徑下掃描該檔案,然後載入mysql的diver實現類com.mysql.cj.jdbc.Driver,就相當於擴充套件了Driver的介面能力,按需載入mysql的實現類。oracle的連線jar包會有oracle的配置檔案,這樣不同的資料庫根據自身的不同邏輯按需擴充套件了Driver的能力,這就是SPI的最大好處。

 java.sql.Driver的內容:

二:java的SPI機制

從jdk1.6開始,java就提供了spi機制的支援,接下來我們就從一個例子來說明jdk的spi是如何實現的?

2.1:設計一個介面

public interface Animal {
    void sound();
}
.

2.2:有兩個實現類:

public class Cat implements Animal {
    public void sound() {
        System.out.println("小貓在叫");
    }
}
public class Dog implements Animal {
    public void sound() {
        System.out.println("小狗在叫");
    }
}

2.3:配置META-INF類

 

 2.4:讀取配置

    public static void main(String[] args) {
        ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);
        final Iterator<Animal> iterator = serviceLoader.iterator();
        while (iterator.hasNext()) {
            Animal next =  iterator.next();
            next.sound();
        }
    }
}

2.5:原理

在上面的例子中:定義了一個介面Animal,然後有兩個實現類:Cat和Dog,在META-INF的檔案目錄下,兩個介面都進行了相關的配置,介面實現類模組要同時載入兩個類,具體的呼叫邏輯在客戶端的ServiceLoader中來通過迭代器遍歷來呼叫具體的配置實現類,那麼程式碼具體的原理是什麼呢?跟著我一起走進原始碼來分析一下jdk:

在ServiceLoader的load方法中首先會獲取上下文類載入器,然後構造一個ServiceLoader,在ServiceLoader中有一個懶載入器,懶載入器會通過BufferedReader來從META-INF/services路徑下讀取對應的介面名的全路徑名檔案,也就是我們配置的檔案,然後通過檔案的類解析器讀取檔案中的內容,再通過類載入器載入類的全路徑

 仔細分析下java的spi具有以下缺點:

①無法按需載入。雖然 ServiceLoader 做了延遲載入,使用了LazyIterator,但是基本只能通過遍歷全部獲取,介面的實現類得全部載入並例項化一遍。如果你並不想用某些實現類,或者某些類例項化很耗時,它也被載入並例項化了,假如我只需要其中一個,其它的並不需要這就形成了一定的資源消耗浪費

②不具有IOC的功能,假如我有一個實現類,如何將它注入到我的容器中呢,類之間依賴關係如何完成呢?

③serviceLoader不是執行緒安全的,會出現執行緒安全的問題

 三:dubbo的SPI

 dubbo在原有的spi基礎上主要有以下的改變,①配置檔案採用鍵值對配置的方式,使用起來更加靈活和簡單 ② 增強了原本SPI的功能,使得SPI具備ioc和aop的功能,這在原本的java中spi是不支援的。dubbo的spi是通過ExtensionLoader來解析的,通過ExtensionLoader來載入指定的實現類,配置檔案的路徑在META-INF/dubbo路徑下,我們通過一個例子來了解dubbo的SPI執行機制:

3.1:dubbo的負載均衡機制其中就採用了spi機制,選擇哪個負載均衡策略是通過@SPI註解來實現的:

 利用ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(name)來獲取具體的LoadBalance的實現類,其中name是對應配置檔案(見下文)中的鍵;

 其中用了@SPI來指定了dubbo的負載均衡策略為隨機(random),我們再來了解一下@SPI註解和@Adaptive是如何工作的?

3.2:在dubbo的META_INF.dubbo.internal路徑下存在一個檔案:

com.alibaba.dubbo.rpc.cluster.LoadBalance檔案,檔案內容是這樣的:

 可以看出dubbo的spi配置是採用鍵值對的方式,鍵值對最大的好處就是可以以鍵來獲取值,取值比較簡單和方便。這點和java的spi配置方式是不同的,java的spi只有全路徑名;

3.3:@SPI和@Adaptive註解的作用是什麼?

Dubbo通過註解@Adaptive作為標記實現了一個介面卡類,dubbo將會為這個類動態生成代理物件;ExtensionLoader中獲取預設實現類或者通過實現類名稱(由@SPI註解指定的名稱)來獲取實現類

為什麼會出現@Adaptive這個註解呢?主要原因是因為dubbo的載入擴充套件了是從配置檔案載入的,是很動態的,但是實現類卻要固定寫死或者靈活實現,所以就得區分開。用@Adaptive就是表示由框架自己生成,不需要人為實現.

在dubbo載入SPI時會動態建立SPI Adaptive實現ExtensionLoader。從URL獲取金鑰,該金鑰將通過@Adaptive由介面方法定義的註釋提供

3.4:dubbo的spi讀取配置和實現類原理

3.4.1:從解析載入配置類的原始碼開始分析

ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE)

上面的原始碼比較簡單,首先是根據傳入的介面從快取(一個以class為鍵,ExtensionLoader為值的concurrentHashMap)中獲取,如果拿不到就放入到快取中;邏輯比較簡單,這裡就不做詳細分析了。接下來主要是分析:ExtensionLoader.getExtension(name)

 

 我們來看下具體的createExtension方法的原始碼:

createExtension 方法的邏輯稍複雜一下,包含了如下的步驟:

①通過 getExtensionClasses 獲取所有的拓展類,也就是所有META-INF下的配置檔案中的鍵值對名

②通過反射建立拓展物件

③向拓展物件中注入依賴

④將拓展物件包裹在相應的 Wrapper 物件中,後面需要從wrapper中取

來具體看一下dubbo是如何解析配置檔案的:

 上面可以看出三個路徑,這和我們剛才上面看到的路徑是一致的,dubbo就是讀取該路徑下的的檔案 

 載入配置檔案下的檔案內容,也就是上面的com.alibaba.dubbo.rpc.cluster.LoadBalance檔案

3.5:dubbo的IOC機制

dubbo的IOC是通過setter方法來實現注入的,通過遍歷物件例項的所有方法,找到其setter方法在進行擷取,從objectFactory中獲取擴充套件類再進行反射執行。這樣的話,就算實現例項中有依賴的擴充套件例項,都可以注入完成,是dubbo的IOC體現。ojectFactory 變數的型別為 AdaptiveExtensionFactory,AdaptiveExtensionFactory 內部維護了一個 ExtensionFactory 列表,用於儲存其他型別的 ExtensionFactory。

 

四:總結 

    本篇部落格簡單分別介紹了 Java SPI 與 Dubbo SPI 用法,java的spi舉了個簡單的例子來進行了說明。並仔細分析了jdk的spi不足,dubbo是如何面對jdk的不足之處,然後自己定製開發出一套更加合理和更好的dubbo自我實現。以及詳細分析了 Dubbo SPI 的載入拓展類的過程和原始碼的分析。其中可以看出來dubbo中對於快取和反射的利用是相當之多的.SPI是軟體設計中高擴充套件性的一個體現,通過spi機制可以靈活地實現廠商的規範訂製和不同企業的具體規範自己實現.高度擴充套件了原程式,使得我們設計出來的程式更加具有擴充套件力。