jdk和dubbo的SPI機制


前言:開閉原則一直是軟件開發領域中所追求的,開閉原則中的"開"是指對於組件功能的擴展是開放的,是允許對其進行功能擴展的,“閉”,是指對於原有代碼的修改是封閉的,即不應該修改原有的代碼。對於一個高度集成化的、成熟、穩健的系統來講,永遠不是封閉、固守的,它需要向外提供一定的可擴展的能力,外部的實現類或者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機制可以靈活地實現廠商的規范訂制和不同企業的具體規范自己實現.高度擴展了原程序,使得我們設計出來的程序更加具有擴展力。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM