本篇主要介紹 Dubbo 服務(wù)導(dǎo)出的實現(xiàn)細節(jié)。
定義服務(wù)接口 HelloWorld:
public interface HelloWorld {
String sayHello();
}
服務(wù)實現(xiàn)類 HelloWorldImpl:
public class HelloWorldImpl implements HelloWorld {
@Override
public String sayHello() {
return "Hello wlm..";
}
}
服務(wù)導(dǎo)出的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo
http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 提供方應(yīng)用信息石抡,用于計算依賴關(guān)系 -->
<dubbo:application name="hello-world-app" />
<!-- 使用 zookeeper 注冊中心暴露服務(wù)地址 -->
<dubbo:registry protocol="zookeeper" address="127.0.0.1:2181"/>
<!-- 用dubbo協(xié)議在20880端口暴露服務(wù) -->
<dubbo:protocol name="dubbo" port="20880" />
<bean id="helloWorldImpl" class="com.wlm.dubbo.service.HelloWorldImpl" />
<dubbo:service interface="com.wlm.dubbo.service.HelloWorld" ref="helloWorldImpl" version="1.0"/>
</beans>
以 Spring 方式啟動服務(wù):
public class DubboStarter {
public static void main(String[] args) throws InterruptedException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"classpath*:META-INF/spring/dubbo-start.xml"}, true);
context.start();
}
}
解析 bean 屬性
以 Spring 作為容器啟動服務(wù)時初烘,會先解析 bean 的屬性耸别。Spring 的 bean 屬性解析分為默認(rèn)標(biāo)簽和自定義標(biāo)簽兩種徘层,其中自定義標(biāo)簽的解析需要實現(xiàn) NamespaceHandler 接口流纹,Dubbo 的標(biāo)簽 dubbo 即為自定義標(biāo)簽畔师。
Dubbo 定義解析標(biāo)簽的實現(xiàn)類為 DubboNamespaceHandler:
這里面定義了 Dubbo 支持的各種標(biāo)簽,比如:application茎芭,registry揖膜,service,reference 等梅桩。
可以看到壹粟,dubbo 的 bean 屬性都通過 DubboBeanDefinitionParser 解析成 BeanDefinition 對象,同時構(gòu)造函數(shù)會傳入最終實例化的 beanClass宿百,比如 service 標(biāo)簽對應(yīng) ServiceBean趁仙,reference 標(biāo)簽對應(yīng) ReferenceBean。具體的實現(xiàn)不是本文的重點垦页,這里就不展開幸撕。
服務(wù)導(dǎo)出入口
根據(jù)前面的說明,服務(wù)提供者相關(guān)的屬性最終會實例化成 ServiceBean 對象外臂,該對象監(jiān)聽了 Spring 的上下文刷新事件 ContextRefreshedEvent,會在 Spring 完成上下文刷新律胀,也就是 bean 完成初始化之后被通知:
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
export();
}
}
Dubbo 就是在這里開啟服務(wù)導(dǎo)出流程宋光,并且在完成服務(wù)導(dǎo)出之后,發(fā)布服務(wù)導(dǎo)出事件 ServiceBeanExportedEvent:
public void export() {
super.export();
// Publish ServiceBeanExportedEvent
publishExportEvent();
}
具體的導(dǎo)出邏輯由父類 ServiceConfig 實現(xiàn):
Dubbo 通過 export 屬性設(shè)置是否導(dǎo)出服務(wù)炭菌,通過 delay 屬性設(shè)置延遲一定時間再導(dǎo)出服務(wù)罪佳。
接下來進入真正的服務(wù)導(dǎo)出流程:
分為兩步:加載注冊中心地址,遍歷協(xié)議(ProtocolConfig)列表執(zhí)行服務(wù)導(dǎo)出黑低。
加載注冊中心地址
通過 loadRegistries 方法加載注冊中心地址赘艳,得到一個協(xié)議頭為 registry 的 URL 列表:
入?yún)⒈硎臼欠?wù)提供者還是消費者,先遍歷 RegistryConfig 列表克握,將 address 屬性解析成 URL 列表蕾管,格式如下:
zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=wlm
&dubbo=2.0.2&pid=29043&release=2.7.3×tamp=1578322585876
再將上面的 URL 的協(xié)議頭轉(zhuǎn)換為 registry,原有的協(xié)議頭變?yōu)樾碌?URL 對象的 registry 屬性菩暗,格式如下:
registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=wlm
&dubbo=2.0.2&pid=29043®istry=zookeeper&release=2.7.3×tamp=1578322585876
服務(wù)導(dǎo)出
通過 doExportUrlsFor1Protocol 執(zhí)行服務(wù)導(dǎo)出掰曾,這里重點關(guān)注 scope=remote (即導(dǎo)出服務(wù)到遠程) 的場景:
核心邏輯分為兩步:
- 調(diào)用 ProxyFactory#getInvoker:將具體實現(xiàn)類轉(zhuǎn)換為 invoker 對象;
- 調(diào)用 Protocol#export:執(zhí)行服務(wù)導(dǎo)出停团,得到 Exporter 對象旷坦。
創(chuàng)建 invoker 對象
Invoker 是 Dubbo 的實體域掏熬,是 Dubbo 的核心模型。
ProxyFactory 即代理工廠類秒梅,是一種動態(tài)代理旗芬,包含兩個操作:
- getInvoker: 將實現(xiàn)類轉(zhuǎn)換為 invoker 對象;
- getProxy: 創(chuàng)建遠程服務(wù)的代理捆蜀。
ProxyFactory 支持 SPI 擴展疮丛,默認(rèn)的實現(xiàn)是 JavassistProxyFactory,依賴于 javassist 組件漱办,創(chuàng)建實現(xiàn)類的代理 Wrapper 對象:
創(chuàng)建代理類 Wrapper 對象的方式為:先動態(tài)生成代理類的 java 代碼这刷,再編譯代理類,并加載到類加載器中娩井,最后反射創(chuàng)建代理類的實例對象暇屋。
最終生成的代理類結(jié)構(gòu)如下:
此處不深究 javassist 的實現(xiàn)細節(jié),展示生成的代理類方法 invokeMethod:
JavassistProxyFactory 獲取到 Wrapper 實例對象后洞辣,再封裝成 AbstractProxyInvoker 匿名對象返回咐刨,該對象會將 doInvoke 請求轉(zhuǎn)發(fā)給 Wrapper 對象。
Protocol.export
接下來調(diào)用 Protocol.export 執(zhí)行服務(wù)導(dǎo)出扬霜。
首先通過 Dubbo 自適應(yīng)擴展機制獲取 Protocol 對象:
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
在 Dubbo SPI 機制中存在一些包裝類(類名以 Wrapper 結(jié)尾定鸟,構(gòu)造函數(shù)參數(shù)為當(dāng)前接口的實現(xiàn)類),而此處 Protocol 的實現(xiàn)類中也存在包裝類著瓶,因此最終得到的是一個 Protocol 鏈:
協(xié)議鏈最后一個節(jié)點是 RegistryProtocol联予,是因為協(xié)議頭為 "registry",RegistryProtocol 內(nèi)部還有個 protocol 對象材原,也是通過 Dubbo SPI 機制獲取沸久,也對應(yīng)著一個 Protocol 調(diào)用鏈,前面配置的協(xié)議為 "dubbo":
整體的流程圖如下:
主要分為以下幾個部分:
- 啟動 qos server余蟹;
- 構(gòu)建服務(wù)提供者的 Filter 鏈卷胯;
- 真正的服務(wù)導(dǎo)出;
- 發(fā)布服務(wù)導(dǎo)出事件威酒;
- 注冊服務(wù)地址窑睁;
- 訂閱
其中 1 在 QosProtocolWrapper 類,2 在 ProtocolFilterWrapper 類葵孤,3 在 DubboProtocol 類担钮,4 在 ProtocolListenerWrapper 類,5佛呻、6 在 RegistryProtocol 類裳朋。
1.啟動 qos server
qos 依賴于 netty,先從 URL 獲取 qos 監(jiān)聽端口信息,再啟動 qos server:
啟動 qos server 時鲤嫡,重點關(guān)注兩個操作:注冊解碼器 QosProcessHandler 和監(jiān)聽 qos 端口送挑,其中解碼器負責(zé)把 netty 接收到的字節(jié)流轉(zhuǎn)換成對應(yīng)業(yè)務(wù)所需要的格式:
2.構(gòu)建 filter 鏈
構(gòu)建邏輯如下:
構(gòu)建 filter 鏈時需要指定三個參數(shù):
- invoker:當(dāng)前 invoker,形成 filter 鏈的最后一個節(jié)點暖眼;
- key:指定擴展點名稱惕耕,多個以逗號分隔,從 URL 獲取诫肠,服務(wù)提供者對應(yīng) "service.filter"司澎,消費者對應(yīng) "reference.filter";
- group:擴展點所屬分組栋豫,提供者和消費者對應(yīng) provider 和 consumer挤安。
構(gòu)建 filter 鏈主要分為以下三個步驟:
- 通過 Dubbo SPI 機制獲取 filter 列表;
- 將 filter 列表轉(zhuǎn)換為 invoker 調(diào)用鏈丧鸯;
- 將 invoker 調(diào)用鏈封裝為支持回調(diào)的 invoker 返回蛤铜;
第1步獲取 filter 鏈?zhǔn)褂昧?@Activate 注解:
public @interface Activate {
/**
* 匹配規(guī)則:分組
*/
String[] group() default {};
/**
* 匹配規(guī)則:如果 URL 中出現(xiàn) value 指定的 key,則返回當(dāng)前擴展實現(xiàn)類
*/
String[] value() default {};
/**
* 從小到大排序丛肢,可選
*/
int order() default 0;
}
Dubbo 獲取的擴展實現(xiàn)類由兩部分組成:
- 有 @Activate 注解的類:遍歷實現(xiàn)類列表围肥,獲取與指定的 group、value 匹配的實現(xiàn)類蜂怎,并排序穆刻;
- 入?yún)⒅付ǖ臄U展類名稱,即入?yún)?key杠步,根據(jù)該名稱獲取擴展實現(xiàn)類氢伟,并加到 filter 列表最后。
第2步將調(diào)用 filter 封裝成 invoker 實現(xiàn)類幽歼,上一個創(chuàng)建的 invoker 都是下一個 filter.invoke 入?yún)⒏郑虼?Dubbo 是倒序遍歷 filter 列表,這樣第一個 filter 就是 invoker 調(diào)用鏈的第一個節(jié)點试躏。
第3步將 invoker 調(diào)用鏈封裝為 CallbackRegistrationInvoker 對象,在調(diào)用完 filter 之后设褐,調(diào)用回調(diào)接口:
3.真正的服務(wù)導(dǎo)出
在 RegistryProtocol 內(nèi)部颠蕴,調(diào)用了真正的服務(wù)導(dǎo)出邏輯:
在 doLocalExport 內(nèi)部,調(diào)用 Protocol.export 導(dǎo)出服務(wù):
這里配置的是 "dubbo"助析,即 DubboProtocol犀被,也就是前面介紹的第二個 Protocol 調(diào)用鏈。
整體服務(wù)導(dǎo)出的時序圖如下:
上述 Exchanger外冀、Transporter寡键、Server 都支持 SPI 擴展。
概念解釋:
1.Exchanger:信息交換層雪隧,封裝請求響應(yīng)模式西轩,以 Request员舵、Response 為中心,在 Transporter 層基礎(chǔ)之上藕畔,默認(rèn)實現(xiàn)為 HeaderExchanger马僻;
客戶端與服務(wù)端之間的消息發(fā)送模式(即 Message Exchange Pattern)有三種:
- 數(shù)據(jù)報(Datagram):客戶端發(fā)送消息后,服務(wù)端不會進行回復(fù)注服,比如電視新聞只管發(fā)布消息韭邓;
- 請求響應(yīng)(Request/Response):客戶端發(fā)送消息后,會等待服務(wù)端的回復(fù)溶弟,比如在街上問路時的一問一答女淑。這也是最常用的消息交換模式;
- 雙工(Duplex):客戶端和服務(wù)端可以任意的發(fā)送消息辜御,客戶端和服務(wù)端變成通信的兩個端點鸭你,比如兩個人通話時可以同時說話;
Dubbo 的 Exchanger 使用的就是 "請求響應(yīng)" 消息發(fā)送模式我抠,包含了請求和響應(yīng)兩次網(wǎng)絡(luò)傳輸苇本。
2.Transporter:網(wǎng)絡(luò)傳輸層,只有單次網(wǎng)絡(luò)傳輸菜拓,兩端對應(yīng) Client 和 Server瓣窄,以 Message 為中心,默認(rèn)實現(xiàn)為 NettyTransporter纳鼎;
3.Server:即服務(wù)提供者俺夕,與之對應(yīng),Client 為服務(wù)消費者贱鄙,默認(rèn)實現(xiàn)為 NettyServer劝贸。
監(jiān)聽服務(wù)端口的過程中,這些組件都會被創(chuàng)建并初始化。
NettyServer 在初始化時會啟動對服務(wù)端口的監(jiān)聽:
這里要注意的是,Dubbo 在 initChannel 時注冊了自定義的編解碼器 NettyCodecAdapter便监,這部分在服務(wù)調(diào)用時再介紹旺入。
4.發(fā)布服務(wù)導(dǎo)出事件
ProtocolListenerWrapper 調(diào)用完 export 后,會返回 ListenerExporterWrapper 包裝類,通過 Dubbo SPI 獲取監(jiān)聽器(由用戶自定義實現(xiàn))ExporterLIstener 列表:
并在對象構(gòu)造器內(nèi)調(diào)用 ExporterListener.exported() 發(fā)布服務(wù)導(dǎo)出事件:
5.注冊服務(wù)地址
RegistryProtocol 內(nèi)部邏輯如下:
先生成注冊中心 URL 信息 registryUrl 和服務(wù)提供方 URL 信息 providerUrl,如下所示:
// registry url
zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=wlm
&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.199.243%3A20880%2Fcom.wlm.dubbo.service
.HelloWorld%3Fanyhost%3Dtrue%26application%3Dwlm%26bean.name%3Dcom.wlm.dubbo.service.
HelloWorld%26bind.ip%3D192.168.199.243%26bind.port%3D20880%26deprecated%3Dfalse%26
dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcom.wlm.dubbo.service
.HelloWorld%26methods%3DsayHello%26pid%3D40140%26register%3Dtrue%26release%3D2.7.3%26
revision%3D1.0%26service.filter%3DdubboFilter%26side%3Dprovider%26timestamp%3D1578403901317%26
version%3D1.0&pid=40140&release=2.7.3×tamp=1578403901308
// provider url
dubbo://192.168.199.243:20880/com.wlm.dubbo.service.HelloWorld?anyhost=true
&application=wlm&bean.name=com.wlm.dubbo.service.HelloWorld&bind.ip=192.168.199.243
&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false
&interface=com.wlm.dubbo.service.HelloWorld&methods=sayHello&pid=40140®ister=true
&release=2.7.3&revision=1.0&service.filter=dubboFilter&side=provider
×tamp=1578403901317&version=1.0
provider url 就是 registry url 中的 export 屬性對應(yīng)的值。
注冊服務(wù)地址的邏輯如下:
public void register(URL registryUrl, URL registeredProviderUrl) {
// 通過注冊中心工廠獲取注冊中心
Registry registry = registryFactory.getRegistry(registryUrl);
// 注冊服務(wù)地址
registry.register(registeredProviderUrl);
}
先從 RegistryFactory 工廠獲取注冊中心 Registry 對象引有,再調(diào)用 Registry.register 將服務(wù)地址注冊到注冊中心。這兩個接口都支持 SPI 擴展倦逐,zookeeper 注冊中心分別對應(yīng)著 ZookeeperRegistryFactory譬正、ZookeeperRegistry。
往 zookeeper 注冊中心注冊服務(wù)地址,即創(chuàng)建目錄節(jié)點:
public void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
最終服務(wù)地址注冊到 zookeeper 路徑:/dubbo/com.wlm.dubbo.service.HelloWorld/providers/曾我,內(nèi)容如下:
6.訂閱
服務(wù)訂閱 URL 在服務(wù)提供方 URL 基礎(chǔ)上粉怕,將 protocol 設(shè)置為 "provider",并且添加了 "category" 屬性您单,生成 overrideSubscribeUrl 為:
provider://192.168.199.243:20880/com.wlm.dubbo.service.HelloWorld?anyhost=true
&application=wlm&bean.name=com.wlm.dubbo.service.HelloWorld&bind.ip=192.168.199.243
&bind.port=20880&category=configurators&check=false&deprecated=false&dubbo=2.0.2
&dynamic=true&generic=false&interface=com.wlm.dubbo.service.HelloWorld
&methods=sayHello&pid=39668®ister=true&release=2.7.3&revision=1.0
&service.filter=dubboFilter&side=provider×tamp=1578401531082&version=1.0
接著調(diào)用 ZookeeperRegistry.subscribe 進行訂閱斋荞,入?yún)?overrideSubscribeUrl 和 OverrideListener 對象,OverrideListener 是個監(jiān)聽器虐秦,是 NotifyListener 的子類平酿,訂閱的數(shù)據(jù)發(fā)生變化時會被通知。
ZookeeperRegistry 內(nèi)部根據(jù) interface 是否配置為 * 分為兩種邏輯悦陋,我們重點關(guān)注指定了 interface 的場景:
先調(diào)用 toCategoriesPath蜈彼,將 overrideSubscribeUrl 轉(zhuǎn)換為 category 目錄列表,這里只有 configurators:
/dubbo/com.wlm.dubbo.service.HelloWorld/configurators
再遍歷該列表俺驶,執(zhí)行以下邏輯:
- 創(chuàng)建監(jiān)聽器 ChildListener幸逆,內(nèi)部調(diào)用入?yún)鬟f的 NotifyListener;
- 創(chuàng)建 category 目錄節(jié)點暮现;
- 將 ChildListener 注冊到 zookeeper还绘,得到子節(jié)點信息 children;
- 調(diào)用 toUrlsWithEmpty 將 overrideSubscribeUrl栖袋、path拍顷、children 信息轉(zhuǎn)換成新的 provider URL。
得到的新 provider URL 格式如下:
empty://192.168.199.243:20880/com.wlm.dubbo.service.HelloWorld?anyhost=true
&application=wlm&bean.name=com.wlm.dubbo.service.HelloWorld&bind.ip=192.168.199.243
&bind.port=20880&category=configurators&check=false&deprecated=false&dubbo=2.0.2
&dynamic=true&generic=false&interface=com.wlm.dubbo.service.HelloWorld&methods=sayHello
&pid=40629®ister=true&release=2.7.3&revision=1.0&service.filter=dubboFilter
&side=provider×tamp=1578406499862&version=1.0
最后主動觸發(fā)一下 notify塘幅。
總結(jié)
本篇文章側(cè)重于 Dubbo 服務(wù)導(dǎo)出的實現(xiàn)細節(jié)昔案,主要包括:服務(wù)導(dǎo)出入口,加載注冊中心地址电媳,創(chuàng)建 invoker 對象踏揣,啟動 qos server,構(gòu)建 filter 鏈匾乓,服務(wù)導(dǎo)出捞稿,發(fā)布服務(wù)導(dǎo)出事件,注冊服務(wù)地址拼缝,訂閱等括享。其中省略了很多細節(jié),比如 Dubbo 的 bean 屬性配置如何解析珍促,并最終得到 URL 等,限于篇幅剩愧,讀者可自行查看猪叙。