Dubbo源碼學(xué)習(xí)四--Dubbo服務(wù)暴露機(jī)制

1. Dubbo服務(wù)啟動過程

啟動一個Dubbo服務(wù)芥牌,通過啟動日志宛逗,查看Dubbo服務(wù)啟動的過程中都做了哪些事情:

圖1---Dubbo服務(wù)啟動過程日志

通過啟動日志可以看到,在Dubbo服務(wù)發(fā)布過程中做了以下的一系列動作:

1.暴露本地服務(wù)
2.暴露遠(yuǎn)程服務(wù)
3.啟動Netty服務(wù)
4.連接Zookeeper并到Zookeeper進(jìn)行注冊
5.監(jiān)聽Zookeeper

通過Dubbo發(fā)布過程的詳細(xì)圖解看下服務(wù)提供者暴露服務(wù)的一個詳細(xì)過程:

圖1--服務(wù)提供者暴露服務(wù)過程

首先ServiceConfig拿到對外提供服務(wù)的實(shí)際類ref(如Dubbo源碼中 dubbo-demo-xml-provider下的DemoServiceImpl類),然后通過ProxyFactory的getInvoker方法使用ref生成一個AbstractProxyInvoker的實(shí)例土铺,到這一步就完成具體服務(wù)到Invoker的轉(zhuǎn)換,接下來就是Invoker到Exporter的轉(zhuǎn)換板鬓。

2.本地服務(wù)暴露的實(shí)現(xiàn)過程

從Dubbo的啟動過程我們可以得知悲敷,Dubbo服務(wù)提供者,先進(jìn)行本地暴露再進(jìn)行遠(yuǎn)程暴露俭令,在我們?nèi)粘5氖褂脠鼍爸杏玫淖疃嗟氖沁h(yuǎn)程暴露后德。那么為什么還要進(jìn)行本地暴露呢?
很多使用Dubbo框架的應(yīng)用抄腔,可能存在在同一個JVM暴露了遠(yuǎn)程服務(wù)瓢湃,同時同一個JVM內(nèi)部又引用了自身服務(wù)的情況,Dubbo默認(rèn)會把遠(yuǎn)程服務(wù)用injvm協(xié)議再暴露一份赫蛇,這樣消費(fèi)方直接消費(fèi)用一個JVM內(nèi)部的服務(wù)绵患,避免了跨網(wǎng)絡(luò)進(jìn)行遠(yuǎn)程通信。
我們先來看一下啟動Dubbo服務(wù)提供者時最開始輸出的日志:

NFO support.ClassPathXmlApplicationContext: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3b192d32: startup date [Sun Aug 18 22:28:44 CST 2019]; root of context hierarchy
[18/08/19 22:28:44:714 CST] main  INFO xml.XmlBeanDefinitionReader: Loading XML bean definitions from class path resource [spring/dubbo-provider.xml]
[18/08/19 22:28:45:257 CST] main  INFO logger.LoggerFactory: using logger: org.apache.dubbo.common.logger.log4j.Log4jLoggerAdapter
[18/08/19 22:28:47:550 CST] main  INFO config.AbstractConfig:  [DUBBO] The service ready on spring started. service: org.apache.dubbo.demo.DemoService, dubbo version: , current host: 192.168.0.102
[18/08/19 22:28:48:151 CST] main  INFO utils.Compatibility: Running in ZooKeeper 3.4.x compatibility mode

可以根據(jù)日志看出啟動的流程一下關(guān)鍵步驟:
1.Loading XML bean definitions from class path resource [spring/dubbo-provider.xml] 加載配置文件

2.The service ready on spring started. service: org.apache.dubbo.demo.DemoService, dubbo version: , current host: 192.168.0.102 Service可以啟動

那么我們根據(jù)這句日志輸出做為一個切入點(diǎn)棍掐,來搜索下是哪里輸出了 The service ready on spring started. 這句日志發(fā)現(xiàn)在ServiceBean類中出現(xiàn)
ServiceBean--onApplicationEvent()方法)

@Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (!isExported() && !isUnexported()) {
            if (logger.isInfoEnabled()) {
                logger.info("The service ready on spring started. service: " + getInterface());
            }
            export();
        }
    }

我們來具體看一下ServiceBean這個類藏雏,到底是怎么執(zhí)行的。
通過配置文件的加載過程我們可以知道,在服務(wù)啟動時掘殴,Spring會解析dubbo服務(wù)的配置文件赚瘦,并將配置文件中的參數(shù)轉(zhuǎn)換成相應(yīng)的Bean,當(dāng)遇到 <dubbo:service> 時就會轉(zhuǎn)換成ServiceBean奏寨。

public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
        ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware,
        ApplicationEventPublisherAware

以上為ServiceBean的定義起意,集成了ServiceConfig同時實(shí)現(xiàn)了InitializingBean、ApplicationListener等多個接口病瞳。在InitializingBean接口中有一個有個afterPropertiesSet方法揽咕,ServiceBean重寫了該方法,在Bean的屬性初始化時套菜,Spring會默認(rèn)調(diào)用該方法亲善。
同時實(shí)現(xiàn)了ApplicationListener接口,并且重寫了onApplicationEvent()方法逗柴。這兩個接口都是Spring提供的接口蛹头,那么這兩個接口會起到什么作用呢?

ServiceBean在創(chuàng)建完對象之后戏溺,會調(diào)用afterPropertiesSet()方法渣蜗,該方法完成beanClass屬性值的設(shè)置;在IOC容器啟動完成之后旷祸,Spring會自動回調(diào)onApplicationEvent()方法耕拷,該方法完成服務(wù)的暴露,也就是在該方法中托享,我們看到了日志中的
The service ready on spring started.

3.服務(wù)暴露方法實(shí)現(xiàn)的解析

ServiceBean的 onApplicationEvent
ServiceBean--onApplicationEvent()方法源碼如下:

public void onApplicationEvent(ContextRefreshedEvent 
event) {   
/**如果是暴露的骚烧、并且沒有暴露的則調(diào)用export方法,暴露服務(wù)*/
if (!isExported() && !isUnexported()) {       
if (logger.isInfoEnabled()) {            logger.info("The service ready on spring 
started. service: " + getInterface());        
        }       
export();    
    }
}

該方法為服務(wù)暴露的入口闰围,那么暴露邏輯的真正實(shí)現(xiàn)則是在export方法中止潘,接下來對export方法進(jìn)行解析。
ServicebBean--export()方法:

@Override
    public void export() {
        super.export();
        // Publish ServiceBeanExportedEvent
        publishExportEvent();
    }

通過export方法的代碼可知辫诅,在這里調(diào)用父類的export()方法,之后調(diào)用調(diào)用publishExportEvent()方法涧狮。

下面看下 ServiceConfig的export()方法都實(shí)現(xiàn)了哪些邏輯炕矮。

 public synchronized void export() {
        //檢測一些必要的屬性和設(shè)置一些默認(rèn)值
        checkAndUpdateSubConfigs();
        //判斷是否已經(jīng)導(dǎo)出
        if (!shouldExport()) {
            return;
        }
        //判斷是否已經(jīng)設(shè)置了延遲
        if (shouldDelay()) {
            DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
        } else {
        //執(zhí)行導(dǎo)出動作
            doExport();
        }
    }

該方法除了檢測基本的配置,以及在沒有配置的情況下為配置設(shè)置默認(rèn)值之外者冤,最關(guān)鍵的是執(zhí)行 doExport()方法肤视,進(jìn)一步查看doExzport方法都實(shí)現(xiàn)了什么邏輯。

protected synchronized void doExport() {
        if (unexported) {
            throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
        }
        if (exported) {
            return;
        }
        exported = true;

        if (StringUtils.isEmpty(path)) {
            path = interfaceName;
        }
        doExportUrls();
    } 
    
    
    
    private void doExportUrls() {
        List<URL> registryURLs = loadRegistries(true);
        for (ProtocolConfig protocolConfig : protocols) {
        //拼接pathKey:group/contextpath/interfacename:version
            String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
            // ProviderModel 表示服務(wù)提供者模型涉枫,此對象中存儲了與服務(wù)提供者相關(guān)的信息邢滑。
        // 比如服務(wù)的配置信息,服務(wù)實(shí)例等愿汰。每個被導(dǎo)出的服務(wù)對應(yīng)一個 ProviderModel。
        // ApplicationModel 持有所有的 ProviderModel。
 
            ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
            ApplicationModel.initProviderModel(pathKey, providerModel);
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

以上代碼通過loadRegistries 加載注冊中心鏈接许赃,然后再遍歷 ProtocolConfig 集合導(dǎo)出每個服務(wù)部逮。并在暴露服務(wù)的過程中,將服務(wù)注冊到注冊中心晌坤。

loadRegistries()方法:

protected List<URL> loadRegistries(boolean provider) {
        // check && override if necessary
        List<URL> registryList = new ArrayList<URL>();
        //判斷注冊配置是否為空,及配置文件中有沒有<dubbo:registry>標(biāo)簽(配置注冊中心的一些信息)
        if (CollectionUtils.isNotEmpty(registries)) {
            //由于可能配置多個注冊中心地址,在這里遍歷注冊列表
            for (RegistryConfig config : registries) {
                String address = config.getAddress();
                //判斷配置的address是否為空宁昭,如果為空則配置為 0.0.0.0(由于在此步驟前已經(jīng)對registry的地址做了非空校驗(yàn),一般走不到這一步)
                if (StringUtils.isEmpty(address)) {
                    address = ANYHOST_VALUE;
                }
                if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
                    Map<String, String> map = new HashMap<String, String>();
                    appendParameters(map, application);
                    appendParameters(map, config);
                    map.put(PATH_KEY, RegistryService.class.getName());
                    appendRuntimeParameters(map);
                    if (!map.containsKey(PROTOCOL_KEY)) {
                        map.put(PROTOCOL_KEY, DUBBO_PROTOCOL);
                    }
                    List<URL> urls = UrlUtils.parseURLs(address, map);

                    for (URL url : urls) {
                        // 將 URL 協(xié)議頭設(shè)置為 registry
                        url = URLBuilder.from(url)
                                .addParameter(REGISTRY_KEY, url.getProtocol())
                                .setProtocol(REGISTRY_PROTOCOL)
                                .build();
                        //判斷是否將地址增加到注冊中心地址列表中
                        if ((provider && url.getParameter(REGISTER_KEY, true))
                                || (!provider && url.getParameter(SUBSCRIBE_KEY, true))) {
                            registryList.add(url);
                        }
                    }
                }
            }
        }
        return registryList;
    }

經(jīng)過這一步驟之后酗宋,已經(jīng)獲取到了注冊中心的地址积仗,接下來就是調(diào)用doExportUrlsFor1Protocol()方法組裝參數(shù)并且暴露Dubbo服務(wù)。

服務(wù)導(dǎo)出(暴露)過程概述

1. Spring容器啟動時通過NameSpaceHandler來解析Dubbo服務(wù)的配置文件蜕猫,并對配置文件中的參數(shù)進(jìn)行初始化寂曹。
2.加載完配置文件之后,在ServiceBean的onApplicationEvent()方法受到Spring上下文刷新事件后會執(zhí)行導(dǎo)出(export())方法丹锹。該方法為Dubbo服務(wù)導(dǎo)出的起點(diǎn)稀颁。

        Export()方法邏輯分析:
        
        1、進(jìn)行參數(shù)配置的檢查楣黍,檢查該方法是否允許導(dǎo)出匾灶,另外檢查該方法是否延遲導(dǎo)出。滿足導(dǎo)出的條件之后租漂,調(diào)用doExport()方法
        
            備注:

            *  檢測 <dubbo:service> 標(biāo)簽的 interface 屬性合法性阶女,不合法則拋出異常   
            * 檢測 ProviderConfig、ApplicationConfig 等核心配置類對象是否為空哩治,若為空秃踩,則嘗試從其他配置類對象中獲取相應(yīng)的實(shí)例。
            *檢測并處理泛化服務(wù)和普通服務(wù)類
            *檢測本地存根配置业筏,并進(jìn)行相應(yīng)的處理
            *對 ApplicationConfig憔杨、RegistryConfig 等配置類進(jìn)行檢測,為空則嘗試創(chuàng)建蒜胖,若無法創(chuàng)建則拋出異常     
        2消别、doExport()在參數(shù)合法的情況下,調(diào)用doExportUrls()方法台谢,該方法對多協(xié)議寻狂,多注冊中心進(jìn)行了支持。
        3朋沮、doExportUrls() 方法調(diào)用  doExportUrlsFor1Protocol()方法蛇券,該方法實(shí)現(xiàn)了由具體服務(wù)到Invoker的轉(zhuǎn)換和Invoker到Exporter的轉(zhuǎn)換。
            備注:
                invoker由ProxyFactory創(chuàng)建,Dubbo默認(rèn)的ProxyFactory的實(shí)現(xiàn)類是JavassistProxyFactory纠亚。

我是割草的小豬頭塘慕,不斷學(xué)習(xí),不斷進(jìn)步菜枷,后續(xù)陸續(xù)更新Dubbo系列的文章苍糠,如您有興趣一起了解,歡迎關(guān)注啤誊,如文章中有不妥之處岳瞭,歡迎指正!

Dubbo系列文章一--Dubbo重點(diǎn)掌握模塊
Dubbo系列文章二--配置文件加載過程
Dubbo系列文章三--Dubbo源碼結(jié)構(gòu)及實(shí)現(xiàn)方

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚊锹,一起剝皮案震驚了整個濱河市瞳筏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌牡昆,老刑警劉巖姚炕,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異丢烘,居然都是意外死亡柱宦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門播瞳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掸刊,“玉大人,你說我怎么就攤上這事赢乓∮遣啵” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵牌芋,是天一觀的道長蚓炬。 經(jīng)常有香客問我,道長躺屁,這世上最難降的妖魔是什么肯夏? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮犀暑,結(jié)果婚禮上熄捍,老公的妹妹穿的比我還像新娘。我一直安慰自己母怜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布缚柏。 她就那樣靜靜地躺著苹熏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上轨域,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天袱耽,我揣著相機(jī)與錄音,去河邊找鬼干发。 笑死朱巨,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的枉长。 我是一名探鬼主播冀续,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼必峰!你這毒婦竟也來了洪唐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吼蚁,失蹤者是張志新(化名)和其女友劉穎凭需,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肝匆,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡粒蜈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了旗国。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枯怖。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖粗仓,靈堂內(nèi)的尸體忽然破棺而出嫁怀,到底是詐尸還是另有隱情,我是刑警寧澤借浊,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布塘淑,位于F島的核電站,受9級特大地震影響蚂斤,放射性物質(zhì)發(fā)生泄漏存捺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一曙蒸、第九天 我趴在偏房一處隱蔽的房頂上張望捌治。 院中可真熱鬧,春花似錦纽窟、人聲如沸肖油。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽森枪。三九已至视搏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間县袱,已是汗流浹背浑娜。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留式散,地道東北人筋遭。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像暴拄,于是被迫代替她去往敵國和親漓滔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容