我今天來就帶大家看看 Dubbo 服務(wù)暴露過程棚品,這個(gè)過程在 Dubbo 中其實(shí)是很核心的過程之一,關(guān)乎到你的 Provider 如何能被 Consumer 得知并調(diào)用。
今天還是會進(jìn)行源碼解析得滤,畢竟我們需要深入的去了解 Dubbo 是如何做的奶稠,只有深入它才能了解它俯艰。
不用擔(dān)心源碼問題,因?yàn)椴粌H僅有源碼解析锌订,敖丙也會通過畫圖和總結(jié)性的語言幫助大家理解竹握,而且在面對面試官的時(shí)候,總結(jié)性的語言才是最重要的辆飘,因?yàn)椴灰姷妹嬖嚬僖捕没蛘哂浀镁唧w的細(xì)節(jié)啦辐。
對了,源碼是 2.6.5 版本蜈项。
URL
不過在進(jìn)行服務(wù)暴露流程分析之前有必要先談一談 URL芹关,有人說這 URL 和 Dubbo 啥關(guān)系?有關(guān)系紧卒,有很大的關(guān)系侥衬!
一般而言我們說的 URL 指的就是統(tǒng)一資源定位符,在網(wǎng)絡(luò)上一般指代地址跑芳,本質(zhì)上看其實(shí)就是一串包含特殊格式的字符串轴总,標(biāo)準(zhǔn)格式如下:
protocol://username:password@host:port/path?key=value&key=value復(fù)制代碼
Dubbo 就是采用 URL 的方式來作為約定的參數(shù)類型,被稱為公共契約聋亡,就是我們都通過 URL 來交互肘习,來交流际乘。
你想一下如果沒有一個(gè)約束坡倔,沒有指定一個(gè)都公共的契約那么不同的接口就會以不同的參數(shù)來傳遞信息,一會兒用 Map脖含、一會兒用特定分隔的字符串罪塔,這就是導(dǎo)致整體很亂,并且解析不能統(tǒng)一养葵。
而用了一個(gè)統(tǒng)一的契約之后征堪,那么代碼就更加的規(guī)范化、形成一種統(tǒng)一的格式关拒,所有人對參數(shù)就一目了然佃蚜,不用去揣測一些參數(shù)的格式等等。
而且用 URL 作為一個(gè)公共約束充分的利用了我們對已有概念的印象着绊,通俗易懂并且容易擴(kuò)展谐算,我們知道 URL 要加參數(shù)只管往后面拼接就完事兒了。
因此 Dubbo 用 URL 作為配置總線归露,貫穿整個(gè)體系洲脂,源碼中 URL 的身影無處不在。
URL 具體的參數(shù)如下:
protocol:指的是 dubbo 中的各種協(xié)議剧包,如:dubbo thrift http
username/password:用戶名/密碼
host/port:主機(jī)/端口
path:接口的名稱
parameters:參數(shù)鍵值對
配置解析
一般常用 XML 或者注解來進(jìn)行 Dubbo 的配置恐锦,我稍微說一下 XML 的往果,這塊其實(shí)是屬于 Spring 的內(nèi)容,我不做過多的分析一铅,就稍微講一下大概的原理陕贮。
Dubbo 利用了 Spring 配置文件擴(kuò)展了自定義的解析,像 dubbo.xsd 就是用來約束 XML 配置時(shí)候的標(biāo)簽和對應(yīng)的屬性用的潘飘,然后 Spring 在解析到自定義的標(biāo)簽的時(shí)候會查找 spring.schemas 和 spring.handlers飘蚯。
spring.schemas 就是指明了約束文件的路徑,而 spring.handlers 指明了利用該 handler 來解析標(biāo)簽福也,你看好的框架都是會預(yù)留擴(kuò)展點(diǎn)的局骤,講白了就是去固定路徑的固定文件名去找你擴(kuò)展的東西,這樣才能讓用戶靈活的使用暴凑。
我們再來看一下 DubboNamespaceHandler 都干了啥峦甩。
講白了就是將標(biāo)簽對應(yīng)的解析類關(guān)聯(lián)起來,這樣在解析到標(biāo)簽的時(shí)候就知道委托給對應(yīng)的解析類解析现喳,本質(zhì)就是為了生成 Spring 的 BeanDefinition凯傲,然后利用 Spring 最終創(chuàng)建對應(yīng)的對象。
服務(wù)暴露全流程
我們在深入源碼之前來看下總的流程嗦篱,有個(gè)大致的印象看起來比較不容易暈冰单。
從代碼的流程來看大致可以分為三個(gè)步驟(本文默認(rèn)都需要暴露服務(wù)到注冊中心)。
第一步是檢測配置灸促,如果有些配置空的話會默認(rèn)創(chuàng)建诫欠,并且組裝成 URL 。
第二步是暴露服務(wù)浴栽,包括暴露到本地的服務(wù)和遠(yuǎn)程的服務(wù)荒叼。
第三步是注冊服務(wù)至注冊中心。
從對象構(gòu)建轉(zhuǎn)換的角度看可以分為兩個(gè)步驟典鸡。
第一步是將服務(wù)實(shí)現(xiàn)類轉(zhuǎn)成 Invoker被廓。
第二部是將 Invoker 通過具體的協(xié)議轉(zhuǎn)換成 Exporter。
服務(wù)暴露源碼分析
接下來我們進(jìn)入源碼分析階段萝玷,從上面配置解析的截圖標(biāo)紅了的地方可以看到 service 標(biāo)簽其實(shí)就是對應(yīng) ServiceBean嫁乘,我們看下它的定義。
這里又涉及到 Spring 相關(guān)內(nèi)容了球碉,可以看到它實(shí)現(xiàn)了ApplicationListener<ContextRefreshedEvent>蜓斧,這樣就會在 Spring IOC 容器刷新完成后調(diào)用onApplicationEvent方法,而這個(gè)方法里面做的就是服務(wù)暴露汁尺,這就是服務(wù)暴露的啟動點(diǎn)法精。
可以看到,如果不是延遲暴露、并且還沒暴露過搂蜓、并且支持暴露的話就執(zhí)行 export 方法狼荞,而 export 最終會調(diào)用父類的 export 方法,我們來看看帮碰。
主要就是檢查了一下配置相味,確認(rèn)需要暴露的話就暴露服務(wù), doExport 這個(gè)方法很長殉挽,不過都是一些檢測配置的過程丰涉,雖說不可或缺不過不是我們關(guān)注的重點(diǎn),我們重點(diǎn)關(guān)注里面的 doExportUrls 方法斯碌。
可以看到 Dubbo 支持多注冊中心一死,并且支持多個(gè)協(xié)議,一個(gè)服務(wù)如果有多個(gè)協(xié)議那么就都需要暴露傻唾,比如同時(shí)支持 dubbo 協(xié)議和 hessian 協(xié)議投慈,那么需要將這個(gè)服務(wù)用兩種協(xié)議分別向多個(gè)注冊中心(如果有多個(gè)的話)暴露注冊。
loadRegistries 方法我就不做分析了冠骄,就是根據(jù)配置組裝成注冊中心相關(guān)的 URL 伪煤,我就給大家看下拼接成的 URL的樣子。
registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&pid=7960&qos.port=22222®istry=zookeeper×tamp=1598624821286復(fù)制代碼
我們接下來關(guān)注的重點(diǎn)在 doExportUrlsFor1Protocol 方法中凛辣,這個(gè)方法挺長的抱既,我會截取大致的部分來展示核心的步驟。
此時(shí)構(gòu)建出來的 URL 長這樣扁誓,可以看到走得是 dubbo 協(xié)議防泵。
然后就是要根據(jù) URL 來進(jìn)行服務(wù)暴露了,我們再來看下代碼跋理,這段代碼我就直接截圖了择克,因?yàn)樾枰獢帱c(diǎn)的解釋。
本地暴露
我們再來看一下 exportLocal 方法前普,這個(gè)方法是本地暴露,走的是 injvm 協(xié)議壹堰,可以看到它搞了個(gè)新的 URL 修改了協(xié)議拭卿。
我們來看一下這個(gè) URL,可以看到協(xié)議已經(jīng)變成了 injvm贱纠。
這里的 export 其實(shí)就涉及到上一篇文章講的自適應(yīng)擴(kuò)展了峻厚。
Exporter exporter = protocol.export(? ? ? ? ? ? ? ? ? ? proxyFactory.getInvoker(ref, (Class) interfaceClass, local));復(fù)制代碼
Protocol 的 export 方法是標(biāo)注了 @ Adaptive 注解的,因此會生成代理類谆焊,然后代理類會根據(jù) Invoker 里面的 URL 參數(shù)得知具體的協(xié)議惠桃,然后通過 Dubbo SPI 機(jī)制選擇對應(yīng)的實(shí)現(xiàn)類進(jìn)行 export,而這個(gè)方法就會調(diào)用 InjvmProtocol#export 方法。
我們再來看看轉(zhuǎn)換得到的 export 到底長什么樣子辜王。
從圖中可以看到實(shí)際上就是具體實(shí)現(xiàn)類層層封裝劈狐, invoker 其實(shí)是由 Javassist 創(chuàng)建的,具體創(chuàng)建過程 proxyFactory.getInvoker 就不做分析了呐馆,對 Javassist 有興趣的同學(xué)自行去了解肥缔,之后可能會寫一篇,至于 dubbo 為什么用 javassist 而不用 jdk 動態(tài)代理是因?yàn)?javassist 快汹来。
為什么要封裝成 invoker
至于為什么要封裝成 invoker 其實(shí)就是想屏蔽調(diào)用的細(xì)節(jié)续膳,統(tǒng)一暴露出一個(gè)可執(zhí)行體,這樣調(diào)用者簡單的使用它收班,向它發(fā)起 invoke 調(diào)用坟岔,它有可能是一個(gè)本地的實(shí)現(xiàn),也可能是一個(gè)遠(yuǎn)程的實(shí)現(xiàn)摔桦,也可能一個(gè)集群實(shí)現(xiàn)炮车。
為什么要搞個(gè)本地暴露呢
因?yàn)榭赡艽嬖谕粋€(gè) JVM 內(nèi)部引用自身服務(wù)的情況,因此暴露的本地服務(wù)在內(nèi)部調(diào)用的時(shí)候可以直接消費(fèi)同一個(gè) JVM 的服務(wù)避免了網(wǎng)絡(luò)間的通信酣溃。
可以有些同學(xué)已經(jīng)有點(diǎn)暈瘦穆,沒事我這里立馬搞個(gè)圖帶大家過一遍。
對 exportLocal 再來一波時(shí)序圖分析赊豌。
遠(yuǎn)程暴露
至此本地暴露已經(jīng)好了扛或,接下來就是遠(yuǎn)程暴露了,即下面這一部分代碼
也和本地暴露一樣碘饼,需要封裝成 Invoker 熙兔,不過這里相對而言比較復(fù)雜一些,我們先來看下? registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()) 將 URL 拼接成什么樣子艾恼。
registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo://192.168.1.17:20880/com.alibaba.dubbo.demo.DemoService....
因?yàn)楹荛L住涉,我就不截全了,可以看到走 registry 協(xié)議钠绍,然后參數(shù)里又有 export=dubbo://舆声,這個(gè)走 dubbo 協(xié)議,所以我們可以得知會先通過 registry 協(xié)議找到? RegistryProtocol 進(jìn)行 export柳爽,并且在此方法里面還會根據(jù) export 字段得到值然后執(zhí)行 DubboProtocol 的 export 方法媳握。
大家要挺住,就快要完成整個(gè)流程的解析了磷脯!
現(xiàn)在我們把目光聚焦到 RegistryProtocol#export 方法上萧豆,我們先過一遍整體的流程颁股,然后再進(jìn)入 doLocalExport 的解析。
可以看到這一步主要是將上面的 export=dubbo://... 先轉(zhuǎn)換成 exporter ,然后獲取注冊中心的相關(guān)配置,如果需要注冊則向注冊中心注冊,并且在 ProviderConsumerRegTable 這個(gè)表格中記錄服務(wù)提供者,其實(shí)就是往一個(gè) ConcurrentHashMap 中將塞入 invoker,key 就是服務(wù)接口全限定名碰声,value 是一個(gè) set,set 里面會存包裝過的 invoker 展辞。
我們再把目光聚焦到? doLocalExport 方法內(nèi)部奥邮。
這個(gè)方法沒什么難度,主要就是根據(jù)URL上 Dubbo 協(xié)議暴露出 exporter罗珍,接下來就看下 DubboProtocol#export 方法洽腺。
可以看到這里的關(guān)鍵其實(shí)就是打開 Server ,RPC 肯定需要遠(yuǎn)程調(diào)用覆旱,這里我們用的是 NettyServer 來監(jiān)聽服務(wù)蘸朋。
再下面我就不跟了,我總結(jié)一下 Dubbo 協(xié)議的 export 主要就是根據(jù) URL 構(gòu)建出 key(例如有分組扣唱、接口名端口等等)藕坯,然后 key 和 invoker 關(guān)聯(lián),關(guān)聯(lián)之后存儲到 DubboProtocol 的 exporterMap 中噪沙,然后如果是服務(wù)初次暴露則會創(chuàng)建監(jiān)聽服務(wù)器炼彪,默認(rèn)是 NettyServer,并且會初始化各種 Handler 比如心跳啊正歼、編解碼等等辐马。
看起來好像流程結(jié)束了?并沒有局义, Filter 到現(xiàn)在還沒出現(xiàn)呢喜爷?有隱藏的措施,上一篇 Dubbo SPI 看的仔細(xì)的各位就知道在哪里觸發(fā)的萄唇。
其實(shí)上面的 protocol 是個(gè)代理類檩帐,在內(nèi)部會通過 SPI 機(jī)制找到具體的實(shí)現(xiàn)類。
這張圖是上一篇文章的另萤,可以看到 export 具體的實(shí)現(xiàn)湃密。
復(fù)習(xí)下上一篇的要點(diǎn),通過 Dubbo SPI 掃包會把 wrapper 結(jié)尾的類緩存起來仲墨,然后當(dāng)加載具體實(shí)現(xiàn)類的時(shí)候會包裝實(shí)現(xiàn)類勾缭,來實(shí)現(xiàn) Dubbo 的 AOP,我們看到 DubboProtocol 有什么包裝類目养。
可以看到有兩個(gè),分別是 ProtocolFilterWrapper 和 ProtocolListenerWrapper
對于所有的 Protocol 實(shí)現(xiàn)類來說就是這么個(gè)調(diào)用鏈毒嫡。
而在 ProtocolFilterWrapper 的 export 里面就會把 invoker 組裝上各種 Filter癌蚁。
看看有 8 個(gè)在幻梯。
我們再來看下 zookeeper 里面現(xiàn)在是怎么樣的,關(guān)注 dubbo 目錄努释。
兩個(gè) service 占用了兩個(gè)目錄碘梢,分別有 configurators 和 providers 文件夾,文件夾里面記錄的就是 URL 的那一串伐蒂,值是服務(wù)提供者 ip煞躬。
至此服務(wù)流程暴露差不多完結(jié)了,可以看到還是有點(diǎn)內(nèi)容在里面的逸邦,并且還需要掌握 Dubbo SPI恩沛,不然有些點(diǎn)例如自適應(yīng)什么的還是很難理解的。最后我再來一張完整的流程圖帶大家再過一遍缕减,具體還是有很多細(xì)節(jié)雷客,不過不是主干我就不做分析了,不然文章就有點(diǎn)散桥狡。
然后再引用一下官網(wǎng)的時(shí)序圖搅裙。
總結(jié)
還是建議大家自己打斷點(diǎn)過一遍,這樣能夠更加的清晰裹芝,到時(shí)候面試官問起來一點(diǎn)都不虛部逮,不過只要你認(rèn)真看了這篇文章也差不多了,總的流程能說出來能證明你看過源碼嫂易,一些細(xì)節(jié)記不住的兄朋,你想想看你自己寫的代碼過一兩個(gè)月你記得住不?更別說別人寫的了炬搭。
其實(shí)我可以不源碼分析蜈漓,我可以直接口述 + 畫圖,觀賞性更佳宫盔,但是為什么我還是貼代碼呢融虽?
想帶著大家從源碼級別來過一遍流程,這樣能讓大家更有底氣灼芭,畢竟你看圖理解了是一回事有额,真正的看到源碼,就會很直觀的知道一些點(diǎn)彼绷,例如巍佑,緩存原來就是放一個(gè) map 中,這過濾鏈原來是這樣拼接的等等等等寄悯。
總的而言服務(wù)暴露的過程起始于 Spring IOC 容器刷新完成之時(shí)萤衰,具體的流程就是根據(jù)配置得到 URL,再利用 Dubbo SPI 機(jī)制根據(jù) URL 的參數(shù)選擇對應(yīng)的實(shí)現(xiàn)類猜旬,實(shí)現(xiàn)擴(kuò)展脆栋。
通過 javassist 動態(tài)封裝 ref (你寫的服務(wù)實(shí)現(xiàn)類)倦卖,統(tǒng)一暴露出 Invoker 使得調(diào)用方便,屏蔽底層實(shí)現(xiàn)細(xì)節(jié)椿争,然后封裝成 exporter 存儲起來怕膛,等待消費(fèi)者的調(diào)用,并且會將 URL 注冊到注冊中心秦踪,使得消費(fèi)者可以獲取服務(wù)提供者的信息褐捻。
今天這個(gè)就差不多了,Dubbo 系列估計(jì)還有幾篇椅邓,到時(shí)候再來個(gè)面試匯總柠逞,等著吧!