Dubbo的SPI機制


SPI 全稱為 Service Provider Interface,是一種服務發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態為接口替換實現類。正因此特性,我們可以很容易的通過 SPI 機制為我們的程序提供拓展功能。SPI 機制在第三方框架中也有所應用,比如 Dubbo 就是通過 SPI 機制加載所有的組件。不過,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的模塊。

Dubbo SPI的改進

Dubbo 的擴展點加載從 JDK 標准的 SPI (Service Provider Interface) 擴展點發現機制加強而來。

Dubbo 改進了 JDK 標准的 SPI 的以下問題:

  • DK 標准的 SPI 會一次性實例化擴展點所有實現,如果有擴展實現初始化很耗時,但如果沒用上也加載,會很浪費資源。
  • 如果擴展點加載失敗,連擴展點的名稱都拿不到了。比如:JDK 標准的 ScriptEngine,通過 getName() 獲取腳本類型的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導致 RubyScriptEngine 類加載失敗,這個失敗原因被吃掉了,和 ruby 對應不起來,當用戶執行 ruby 腳本時,會報不支持 ruby,而不是真正失敗的原因。
  • 增加了對擴展點 IoC 和 AOP 的支持,一個擴展點可以直接 setter 注入其它擴展點。

Dubbo SPI的約定

在擴展類的 jar 包內,放置擴展點配置文件 META-INF/dubbo/接口全限定名,內容為:配置名=擴展實現類全限定名,多個實現類用換行符分隔。

以擴展 Dubbo 的協議為例,在協議的實現 jar 包內放置文本文件:META-INF/dubbo/org.apache.dubbo.rpc.Protocol,內容為:

xxx=com.alibaba.xxx.XxxProtocol

實現類內容:

package com.alibaba.xxx;
 
import org.apache.dubbo.rpc.Protocol;
 
public class XxxProtocol implements Protocol { 
    // ...
}

Dubbo 配置模塊中,擴展點均有對應配置屬性或標簽,通過配置指定使用哪個擴展實現。比如:

<dubbo:protocol name="xxx" />

擴展點特性


擴展點自動包裝(AOP特性)

自動包裝擴展點的 Wrapper 類。ExtensionLoader 在加載擴展點時,如果加載到的擴展點有拷貝構造函數,則判定為擴展點 Wrapper 類。

Wrapper類內容:

package com.alibaba.xxx;
 
import org.apache.dubbo.rpc.Protocol;
 
public class XxxProtocolWrapper implements Protocol {
    Protocol impl;
 
    public XxxProtocolWrapper(Protocol protocol) { impl = protocol; }
 
    // 接口方法做一個操作后,再調用extension的方法
    public void refer() {
        //... 一些操作
        impl.refer();
        // ... 一些操作
    }
 
    // ...
}

Wrapper 類同樣實現了擴展點接口,但是 Wrapper 不是擴展點的真正實現。它的用途主要是用於從 ExtensionLoader 返回擴展點時,包裝在真正的擴展點實現外。即從 ExtensionLoader 中返回的實際上是 Wrapper 類的實例,Wrapper 持有了實際的擴展點實現類。

擴展點的 Wrapper 類可以有多個,也可以根據需要新增。

通過 Wrapper 類可以把所有擴展點公共邏輯移至 Wrapper 中。新加的 Wrapper 在所有的擴展點上添加了邏輯,有些類似 AOP,即 Wrapper 代理了擴展點。

擴展點自動裝配(IoC特性)

加載擴展點時,自動注入依賴的擴展點。加載擴展點時,擴展點實現類的成員如果為其它擴展點類型,ExtensionLoader 在會自動注入依賴的擴展點。ExtensionLoader 通過掃描擴展點實現類的所有 setter 方法來判定其成員。即 ExtensionLoader 會執行擴展點的拼裝操作。

示例:有兩個為擴展點 CarMaker(造車者)、WheelMaker (造輪者)

接口類如下:

public interface CarMaker {
    Car makeCar();
}
 
public interface WheelMaker {
    Wheel makeWheel();
}

CarMaker 的一個實現類:

public class RaceCarMaker implements CarMaker {
    WheelMaker wheelMaker;
 
    public setWheelMaker(WheelMaker wheelMaker) {
        this.wheelMaker = wheelMaker;
    }
 
    public Car makeCar() {
        // ...
        Wheel wheel = wheelMaker.makeWheel();
        // ...
        return new RaceCar(wheel, ...);
    }
}

ExtensionLoader 加載 CarMaker 的擴展點實現 RaceCar 時,setWheelMaker 方法的 WheelMaker 也是擴展點則會注入 WheelMaker 的實現。

這里帶來另一個問題,ExtensionLoader 要注入依賴擴展點時,如何決定要注入依賴擴展點的哪個實現。在這個示例中,即是在多個WheelMaker 的實現中要注入哪個。

這個問題在下面一點 擴展點自適應 中說明。

擴展點自適應(Adaptive 特性)

ExtensionLoader 注入的依賴擴展點是一個 Adaptive 實例,直到擴展點方法執行時才決定調用是一個擴展點實現。

Dubbo 使用 URL 對象(包含了Key-Value)傳遞配置信息。

擴展點方法調用會有URL參數(或是參數有URL成員)

這樣依賴的擴展點也可以從URL拿到配置信息,所有的擴展點自己定好配置的Key后,配置信息從URL上從最外層傳入。URL在配置傳遞上即是一條總線。

示例:有兩個為擴展點 CarMakerWheelMaker

接口類如下:

public interface CarMaker {
    Car makeCar(URL url);
}
 
public interface WheelMaker {
    Wheel makeWheel(URL url);
}

CarMaker 的一個實現類:

public class RaceCarMaker implements CarMaker {
    WheelMaker wheelMaker;
 
    public setWheelMaker(WheelMaker wheelMaker) {
        this.wheelMaker = wheelMaker;
    }
 
    public Car makeCar(URL url) {
        // ...
        Wheel wheel = wheelMaker.makeWheel(url);
        // ...
        return new RaceCar(wheel, ...);
    }
}

當上面執行

// ...
Wheel wheel = wheelMaker.makeWheel(url);
// ...

時,注入的 Adaptive 實例可以提取約定 Key 來決定使用哪個 WheelMaker 實現來調用對應實現的真正的 makeWheel 方法。如提取 wheel.type, key 即 url.get("wheel.type") 來決定 WheelMake 實現。Adaptive 實例的邏輯是固定,指定提取的 URL 的 Key,即可以代理真正的實現類上,可以動態生成。

在 Dubbo 的 ExtensionLoader 的擴展點類對應的 Adaptive 實現是在加載擴展點里動態生成。指定提取的 URL 的 Key 通過 @Adaptive 注解在接口方法上提供。

下面是 Dubbo 的 Transporter 擴展點的代碼:

public interface Transporter {
    @Adaptive({"server", "transport"})
    Server bind(URL url, ChannelHandler handler) throws RemotingException;
 
    @Adaptive({"client", "transport"})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;
}

對於 bind() 方法,Adaptive 實現先查找 server key,如果該 Key 沒有值則找 transport key 值,來決定代理到哪個實際擴展點。

擴展點自動激活(Activate 特性)

對於集合類擴展點,比如:Filter, InvokerListener, ExportListener, TelnetHandler, StatusChecker 等,可以同時加載多個實現,此時,可以用自動激活來簡化配置,如:

import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;
 
@Activate // 無條件自動激活
public class XxxFilter implements Filter {
    // ...
}

或:

import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;
 
@Activate("xxx") // 當配置了xxx參數,並且參數為有效值時激活,比如配了cache="lru",自動激活CacheFilter。
public class XxxFilter implements Filter {
    // ...
}

或:

import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;
 
@Activate(group = "provider", value = "xxx") // 只對提供方激活,group可選"provider"或"consumer"
public class XxxFilter implements Filter {
    // ...
}

注意:這里的配置文件是放在你自己的 jar 包內,不是 dubbo 本身的 jar 包內,Dubbo 會全 ClassPath 掃描所有 jar 包內同名的這個文件,然后進行合並。
注意:擴展點使用單一實例加載(請確保擴展實現的線程安全性),緩存在 ExtensionLoader。 中

總結

SPI機制是Dubbo的內核,這里實現了Dubbo自己的IoC和AOP等機制,在實現擴展自適應特性時,還用到了動態編譯,可以說要學習Dubbo的源碼,SPI機制務必要弄懂。

說明:Dubbo官方文檔寫得很好,Dubbo源碼分析系列的很多文章都是從官網摘抄的。官網傳送門:https://dubbo.incubator.apache.org/zh-cn/docs/user/quick-start.html


免責聲明!

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



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