引言
前面,我們已經介紹了 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);
}
}
最後來看一下測試結果,如下:

從測試結果可以看出,我們的兩個實作類被成功的加載,并輸出了相應的内容。關于 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 除了支援按需加載接口實作類,還增加了 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 方法主要做了三件事情:
- 從 URL 中擷取 WheelMaker 名稱
- 通過 SPI 加載具體的 WheelMaker 實作類
- 調用目标方法
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
- 狀态檢查
- 容器
- 頁面
- 緩存
- 驗證
- 日志适配
- 配置中心
文章說明
更多有價值的文章均收錄于
貝貝貓的文章目錄版權聲明: 本部落格所有文章除特别聲明外,均采用 BY-NC-SA 許可協定。轉載請注明出處!
創作聲明: 本文基于下列所有參考内容進行創作,其中可能涉及複制、修改或者轉換,圖檔均來自網絡,如有侵權請聯系我,我會第一時間進行删除。
參考内容
[1]《深入了解Apache Dubbo與實戰》
[2]
dubbo 官方文檔