天天看點

Dubbo SPI 簡介

引言

前面,我們已經介紹了 Dubbo 設計上的一些思想,本文主要介紹 Dubbo 在 SPI(Service Provider Interface)上的一些改進,其他 Dubbo 相關文章均收錄于

<Dubbo系列文章>

SPI

我們知道Dubbo的設計原則是微核心+富擴充,它的核心部分就是将各個子產品組裝起來,而各個子產品都抽象稱為接口,這樣替換任意子產品都非常友善。接下來就讓我們一起來看一看Dubbo的擴充點是如何設計的。

來源

Dubbo 的擴充點加載從 JDK 标準的 SPI (Service Provider Interface) 擴充點發現機制加強而來。

Dubbo 改進了 JDK 标準的 SPI 的以下問題:

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

Java SPI示例

首先,我們定義一個接口,名稱為 Robot。

public interface Robot {
    void sayHello();
}           

接下來定義兩個實作類,分别為 OptimusPrime 和 Bumblebee。

public class OptimusPrime implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}           

接下來 META-INF/services 檔案夾下建立一個檔案,名稱為 Robot 的全限定名 org.apache.spi.Robot。檔案内容為實作類的全限定的類名,如下:

org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee           

做好所需的準備工作,接下來編寫代碼進行測試。

public class JavaSPITest {

    @Test
    public void sayHello() throws Exception {
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
        System.out.println("Java SPI");
        serviceLoader.forEach(Robot::sayHello);
    }
}           

最後來看一下測試結果,如下:

Dubbo SPI 簡介

從測試結果可以看出,我們的兩個實作類被成功的加載,并輸出了相應的内容。關于 Java SPI 的示範先到這裡,接下來示範 Dubbo SPI。

Dubbo 約定

在擴充類的 jar 包内,放置擴充點配置檔案META-INF/dubbo/接口全限定名,内容為:配置名=擴充實作類全限定名,多個實作類用換行符分隔。Dubbo 會全 ClassPath 掃描所有 jar 包内同名的這個檔案,然後進行合并。

此外,在Dubbo中一次使用隻會執行個體化指定的實作類,并不會像Java SPI中那樣一次性執行個體化所有的實作類,相比而言Dubbo這種實作在性能上更具優勢。

Dubbo SPI示例

Dubbo SPI 所需的配置檔案需放置在 META-INF/dubbo 路徑下,配置内容如下。

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee           

與 Java SPI 實作類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣我們可以按需加載指定的實作類。另外,在測試 Dubbo SPI 時,需要在 Robot 接口上标注 @SPI 注解。下面來示範 Dubbo SPI 的用法:

public class DubboSPITest {

    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Robot> extensionLoader =
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}           

測試結果如下:

Dubbo SPI 簡介

Dubbo SPI 除了支援按需加載接口實作類,還增加了 IOC 和 AOP 等特性,這些内容我們會後再面一一介紹。

特性

Dubbo的SPI除了上述的根據配置資訊使用特定實作類這個核心功能外,還具有四種額外的特性,它們分别是自動包裝、自動裝配、自動适應、自動激活,接下來我們就分别介紹一下這四大特性。

自動包裝

自動包裝對應的是擴充點的 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 代理了擴充點。

自動裝配

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

public interface CarMaker {
    Car makeCar();
}

public interface WheelMaker {
    Wheel makeWheel();
}

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 的擴充點實作 RaceCarMaker 時,setWheelMaker 方法的 WheelMaker 也是擴充點則會注入 WheelMaker 的實作。

這裡帶來另一個問題,ExtensionLoader 要注入依賴擴充點時,如何決定要注入依賴擴充點的哪個實作。在這個示例中,即是在多個WheelMaker 的實作中要注入哪個。我們知道在Spring中,是通過引用時指定bean name來進行指定的,但是在dubbo中,因為各個子產品的實作都是根據配置資訊來指定的,接下來要介紹的自動适應特性就是對應了這部分的功能。

自動适應

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

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

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

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

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

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

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

上面的的代碼中我們可以看到

makeCar

多了一個URL參數,這個URL就是前面所說的配置資訊,Dubbo将配置資訊轉化為URL的形式存儲。

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

WheelMaker 接口的自适應實作類如下:

public class AdaptiveWheelMaker implements WheelMaker {
    public Wheel makeWheel(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }

        // 1.從 URL 中擷取 WheelMaker 名稱
        String wheelMakerName = url.getParameter("Wheel.maker");
        if (wheelMakerName == null) {
            throw new IllegalArgumentException("wheelMakerName == null");
        }

        // 2.通過 SPI 加載具體的 WheelMaker
        WheelMaker wheelMaker = ExtensionLoader
            .getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName);

        // 3.調用目标方法
        return wheelMaker.makeWheel(url);
    }
}           

AdaptiveWheelMaker 是一個代理類,與傳統的代理邏輯不同,AdaptiveWheelMaker 所代理的對象是在 makeWheel 方法中通過 SPI 加載得到的。makeWheel 方法主要做了三件事情:

  1. 從 URL 中擷取 WheelMaker 名稱
  2. 通過 SPI 加載具體的 WheelMaker 實作類
  3. 調用目标方法

RaceCarMaker 持有一個 WheelMaker 類型的成員變量,在程式啟動時,我們可以将 AdaptiveWheelMaker 通過 setter 方法注入到 RaceCarMaker 中。在運作時,假設有這樣一個 url 參數傳入:

dubbo://192.168.0.101:20880/XxxService?wheel.maker=MichelinWheelMaker           

RaceCarMaker 的 makeCar 方法将上面的 url 作為參數傳給 AdaptiveWheelMaker 的 makeWheel 方法,makeWheel 方法從 url 中提取 wheel.maker 參數,得到 MichelinWheelMaker。之後再通過 SPI 加載配置名為 MichelinWheelMaker 的實作類,得到具體的 WheelMaker 執行個體。

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

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 值,來決定代理到哪個實際擴充點。

自動激活

對于集合類擴充點,比如: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 {
    // ...
}           

Dubbo中可擴充的接口

  • 協定
  • 調用攔截
  • 引用監聽
  • 暴露監聽
  • 叢集
  • 路由
  • 負載均衡
  • 合并結果
  • 注冊中心
  • 監控中心
  • 擴充點加載
  • 動态代理
  • 編譯器
  • 消息派發
  • 線程池
  • 序列化
  • 網絡傳輸
  • 資訊交換
  • 組網
  • Telnet
  • 狀态檢查
  • 容器
  • 頁面
  • 緩存
  • 驗證
  • 日志适配
  • 配置中心

文章說明

更多有價值的文章均收錄于

貝貝貓的文章目錄
Dubbo SPI 簡介

版權聲明: 本部落格所有文章除特别聲明外,均采用 BY-NC-SA 許可協定。轉載請注明出處!

創作聲明: 本文基于下列所有參考内容進行創作,其中可能涉及複制、修改或者轉換,圖檔均來自網絡,如有侵權請聯系我,我會第一時間進行删除。

參考内容

[1]《深入了解Apache Dubbo與實戰》

[2]

dubbo 官方文檔