jdk SPI與dubbo SPI

1.jdk SPI介紹


SPI 全稱為 Service Provider Interface,是一種服務發現機制。SPI 的\color{red}{本質是將接口實現類的全限定名配置在文件中,并由服務加載器讀取配置文件,加載實現類。}這樣可以在\color{red}{運行時,動態為接口替換實現類。正因此特性},我們可以很容易的通過 SPI 機制為我們的程序提供拓展功能。SPI 機制在第三方框架中也有所應用,比如 Dubbo 就是通過 SPI 機制加載所有的組件。不過,Dubbo 并未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的模塊。基于 SPI,我們可以很容易的對 Dubbo 進行拓展。如果大家想要學習 Dubbo 的源碼,SPI 機制務必弄懂。接下來,我們先來了解一下 Java SPI 與 Dubbo SPI 的用法,然后再來分析 Dubbo SPI 的源碼。以上來自dubbo官方文檔

我們系統里抽象的各個模塊,往往有很多不同的實現方案,比如日志模塊的方案,xml解析模塊、jdbc模塊的方案等。面向的對象的設計里,我們一般推薦模塊之間基于接口編程,模塊之間不對實現類進行硬編碼。一旦代碼里涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。為了實現在模塊裝配的時候能不在程序里動態指明,這就需要一種服務發現機制。 java spi就是提供這樣的一個機制:為某個接口尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。

SPI的具體約定:服務的提供者,提供了服務接口的一種實現之后,在jar包的META-INF/services/目錄里同時創建一個以服務接口命名的文件。該文件里就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/里的配置文件找到具體的實現類名,并裝載實例化,完成模塊的注入。 基于這樣一個約定就能很好的找到服務接口的實現類,而不需要再代碼里制定。jdk提供服務實現查找的一個工具類:java.util.ServiceLoader

  • common-logging

apache最早提供的日志的門面接口。只有接口,沒有實現。具體方案由各提供商實現, 發現日志提供商是通過掃描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,通過讀取該文件的內容找到日志提工商實現類。只要我們的日志實現里包含了這個文件,并在文件里制定 LogFactory工廠接口的實現類即可。

  • JDBC

jdbc4.0以前, 開發人員還需要基于Class.forName("xxx")的方式來裝載驅動,jdbc4也基于spi的機制來發現驅動提供商了,可以通過META-INF/services/java.sql.Driver文件里指定實現類的方式來暴露驅動提供者.

jdbc連接過程 賈璉預執事(加載驅動、鏈接數據庫、預執行、執行、釋放資源)

//加載JDBC驅動程序
Class.forName("com.mysql.jdbc.Driver") ; 
//2、提供JDBC連接的URL
String url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
//3、創建數據庫的連接
Connection con =  DriverManager.getConnection(url , username , password ) ; 
//4、創建一個Statement
PreparedStatement pstmt = con.prepareStatement(sql) ;   
//5、執行SQL語句
ResultSet rs = stmt.executeQuery("SELECT * FROM ...") ;   
 while(rs.next()){   
         //do something
     }  
//6、釋放資源
con.close()

我們都知道,也聽了無數遍,驅動的加載是由Class.forName 方法完成的。
由于JVM對類的加載有一個邏輯是:在類被需要的時候,或者首次調用的時候就會把類加載到JVM。反過來也就是:如果類沒有被需要的時候,一般是不會被加載到JVM的。

當連接數據庫的時候我們調用了Class.forName語句之后,數據庫驅動類被加載到JVM,那么靜態初始化塊就會被執行,從而完成驅動的注冊工作,也就是注冊到了JDBC的DriverManager類中。

由于是靜態初始化塊中完成的加載,所以也就不必擔心驅動被加載多次,原因可以參考單例模式相關的知識。

拋棄Class.forName()

在JDBC 4.0之后實際上我們不需要再調用Class.forName來加載驅動程序了,我們只需要把驅動的jar包放到工程的類加載路徑里,那么驅動就會被自動加載。

這個自動加載采用的技術叫做SPI,數據庫驅動廠商也都做了更新。可以看一下jar包里面的META-INF/services目錄,里面有一個java.sql.Driver的文件,文件里面包含了驅動的全路徑名。

比如mysql-connector里面的內容:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

那么SPI技術又是在什么階段加載的數據庫驅動呢?看一下JDBC的DriverManager類就知道了。

public class DriverManager {
    static {
        loadInitialDrivers();//......1
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                 return System.getProperty("jdbc.drivers");
                }
                });
           } catch (Exception ex) {
                drivers = null;
           }

           AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);//.....2
                   Iterator driversIterator = loadedDrivers.iterator();

                //.....
}

上述代碼片段標記…1的位置是在DriverManager類加載是執行的靜態初始化塊,這里會調用loadInitialDrivers方法。

再看loadInitialDrivers方法里面標記…2的位置,這里調用的 ServiceLoader.load(Driver.class); 就會加載所有在META-INF/services/java.sql.Driver文件里邊的類到JVM內存,完成驅動的自動加載。

這就是SPI的優勢所在,能夠自動的加載類到JVM內存。這個技術在阿里的dubbo框架里面也占到了很大的分量,有興趣的朋友可以看一下dubbo的代碼,或者百度一下dubbo的擴展機制。

JDBC如何區分多個驅動?

一個項目里邊很可能會即連接MySQL,又連接Oracle,這樣在一個工程里邊就存在了多個驅動類,那么這些驅動類又是怎么區分的呢?

關鍵點就在于getConnection的步驟,DriverManager.getConnection中會遍歷所有已經加載的驅動實例去創建連接,當一個驅動創建連接成功時就會返回這個連接,同時不再調用其他的驅動實例。DriverManager關鍵代碼如下:

private static Connection getConnection(
    //.....

    for(DriverInfo aDriver : registeredDrivers) {
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                println("    trying " + aDriver.driver.getClass().getName());
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    // Success!
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                      reason = ex;
                }
            }
        } else {
            println("    skipping: " + aDriver.getClass().getName());
        }
     }

是不是每個驅動實例都真真實實的要嘗試建立連接呢?不是的!

\color{red}{每個驅動實例在getConnetion的第一步就是按照url判斷是不是符合自己的處理規則,是的話才會和db建立連接。}比如,MySQL驅動類中的關鍵代碼:

public boolean acceptsURL(String url) throws SQLException {
        return (parseURL(url, null) != null);
    }

    public Properties parseURL(String url, Properties defaults)
            throws java.sql.SQLException {
        Properties urlProps = (defaults != null) ? new Properties(defaults)
                : new Properties();

        if (url == null) {
            return null;
        }

        if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX)
                && !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)
                && !StringUtils.startsWithIgnoreCase(url,
                        LOADBALANCE_URL_PREFIX)
                && !StringUtils.startsWithIgnoreCase(url,
                        REPLICATION_URL_PREFIX)) { //$NON-NLS-1$

            return null;
        }
        //......

2.SPI示例

3.JDK SPI缺點

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

4.dubbo SPI

擴展點(Extension Point)
是一個Java的接口。
擴展(Extension)
擴展點的實現類。
擴展實例
擴展點實現類的實例。
擴展自適應實例(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會使用字節碼工具為我們自動生成一個。所以我們基本感覺不到自適應類的存在。后面會有例子說明自適應類是怎么工作的。
@SPI
@SPI注解作用于擴展點的接口上,表明該接口是一個擴展點。可以被Dubbo的ExtentionLoader加載。如果沒有此ExtensionLoader調用會異常。
@Adaptive
@Adaptive注解用在擴展接口的方法上。表示該方法是一個自適應方法。Dubbo在為擴展點生成自適應實例時,如果方法有@Adaptive注解,會為該方法生成對應的代碼。方法內部會根據方法的參數,來決定使用哪個擴展。
ExtentionLoader
類似于Java SPI的ServiceLoader,負責擴展的加載和生命周期維護。
擴展別名
和Java SPI不同,Dubbo中的擴展都有一個別名,用于在應用中引用它們。比如

random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance 
roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance 

其中的random,roundrobin就是對應擴展的別名。這樣我們在配置文件中使用random或roundrobin就可以了。
路徑
和Java SPI從/META-INF/services目錄加載擴展配置類似,Dubbo也會從以下路徑去加載擴展配置文件:

  • META-INF/dubbo/internal
  • META-INF/dubbo
  • META-INF/services

5.Dubbo的LoadBalance擴展點解讀

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

package com.alibaba.dubbo.rpc.cluster;
@SPI("random")
public interface LoadBalance {
    @Adaptive({"loadbalance"})
    <T> Invoker<T> select(List<Invoker<T>> var1, URL var2, Invocation var3) throws RpcException;
}

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

  • @SPI("random") @SPI作用于LoadBalance接口,表示接口LoadBalance是一個擴展點。如果沒有@SPI注解,試圖去加載擴展時,會拋出異常。@SPI注解有一個參數,該參數表示該擴展點的默認實現的別名。如果沒有顯示的指定擴展,就使用默認實現。默認實現是"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 \color{red}{parameters}字段,parameters中就包含了loadbalance。
LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName); 

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

6.自定義一個LoadBalance擴展

1.實現LoadBalance接口,首先,編寫一個自己實現的LoadBalance,因為是為了演示Dubbo的擴展機制,而不是LoadBalance的實現,所以這里LoadBalance的實現非常簡單,選擇第一個invoker,并在控制臺輸出一條日志。

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);
    }
}

2.META-INF/dubbo下添加擴展配置文件

demo=com.dubbo.spi.demo.consumer.DemoLoadBalance
  1. 通過上面的兩步,已經添加了一個名字為demo的LoadBalance實現,并在配置文件中進行了相應的配置。接下來,需要顯式的告訴Dubbo使用demo的負載均衡實現。如果是通過spring的方式使用Dubbo,可以在xml文件中進行設置。
 <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
 <dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" />

啟動Dubbo
啟動Dubbo,調用一次IHelloService,可以看到控制臺會輸出一條DemoLoadBalance: Select the first invoker...日志。說明Dubbo的確是使用了我們自定義的LoadBalance。

zk

image.png

控制臺

服務提供者1

服務提供者2

總結dubbo SPI

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

轉載鏈接
轉載鏈接
dubbo各協議比較

Dubbo作者親述:那些輝煌、沉寂與重生的故事

dubbo官方文檔

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容