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

1. Dubbo服務(wù)啟動(dòng)過(guò)程

啟動(dòng)一個(gè)Dubbo服務(wù),通過(guò)啟動(dòng)日志,查看Dubbo服務(wù)啟動(dòng)的過(guò)程中都做了哪些事情:

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

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

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

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

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

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

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

從Dubbo的啟動(dòng)過(guò)程我們可以得知,Dubbo服務(wù)提供者,先進(jìn)行本地暴露再進(jìn)行遠(yuǎn)程暴露,在我們?nèi)粘5氖褂脠?chǎng)景中用的最多的是遠(yuǎn)程暴露。那么為什么還要進(jìn)行本地暴露呢?
很多使用Dubbo框架的應(yīng)用,可能存在在同一個(gè)JVM暴露了遠(yuǎn)程服務(wù),同時(shí)同一個(gè)JVM內(nèi)部又引用了自身服務(wù)的情況,Dubbo默認(rèn)會(huì)把遠(yuǎn)程服務(wù)用injvm協(xié)議再暴露一份,這樣消費(fèi)方直接消費(fèi)用一個(gè)JVM內(nèi)部的服務(wù),避免了跨網(wǎng)絡(luò)進(jìn)行遠(yuǎn)程通信。
我們先來(lái)看一下啟動(dòng)Dubbo服務(wù)提供者時(shí)最開(kāi)始輸出的日志:

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ù)日志看出啟動(dòng)的流程一下關(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可以啟動(dòng)

那么我們根據(jù)這句日志輸出做為一個(gè)切入點(diǎn),來(lái)搜索下是哪里輸出了 The service ready on spring started. 這句日志發(fā)現(xiàn)在ServiceBean類(lèi)中出現(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();
        }
    }

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

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

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

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

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

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

public void onApplicationEvent(ContextRefreshedEvent 
event) {   
/**如果是暴露的、并且沒(méi)有暴露的則調(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方法中,接下來(lái)對(duì)export方法進(jìn)行解析。
ServicebBean--export()方法:

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

通過(guò)export方法的代碼可知,在這里調(diào)用父類(lèi)的export()方法,之后調(diào)用調(diào)用publishExportEvent()方法。

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

 public synchronized void export() {
        //檢測(cè)一些必要的屬性和設(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)出動(dòng)作
            doExport();
        }
    }

該方法除了檢測(cè)基本的配置,以及在沒(méi)有配置的情況下為配置設(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ù)提供者模型,此對(duì)象中存儲(chǔ)了與服務(wù)提供者相關(guān)的信息。
        // 比如服務(wù)的配置信息,服務(wù)實(shí)例等。每個(gè)被導(dǎo)出的服務(wù)對(duì)應(yīng)一個(gè) ProviderModel。
        // ApplicationModel 持有所有的 ProviderModel。
 
            ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
            ApplicationModel.initProviderModel(pathKey, providerModel);
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

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

loadRegistries()方法:

protected List<URL> loadRegistries(boolean provider) {
        // check && override if necessary
        List<URL> registryList = new ArrayList<URL>();
        //判斷注冊(cè)配置是否為空,及配置文件中有沒(méi)有<dubbo:registry>標(biāo)簽(配置注冊(cè)中心的一些信息)
        if (CollectionUtils.isNotEmpty(registries)) {
            //由于可能配置多個(gè)注冊(cè)中心地址,在這里遍歷注冊(cè)列表
            for (RegistryConfig config : registries) {
                String address = config.getAddress();
                //判斷配置的address是否為空,如果為空則配置為 0.0.0.0(由于在此步驟前已經(jīng)對(duì)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();
                        //判斷是否將地址增加到注冊(cè)中心地址列表中
                        if ((provider && url.getParameter(REGISTER_KEY, true))
                                || (!provider && url.getParameter(SUBSCRIBE_KEY, true))) {
                            registryList.add(url);
                        }
                    }
                }
            }
        }
        return registryList;
    }

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

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

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

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

            *  檢測(cè) <dubbo:service> 標(biāo)簽的 interface 屬性合法性,不合法則拋出異常   
            * 檢測(cè) ProviderConfig、ApplicationConfig 等核心配置類(lèi)對(duì)象是否為空,若為空,則嘗試從其他配置類(lèi)對(duì)象中獲取相應(yīng)的實(shí)例。
            *檢測(cè)并處理泛化服務(wù)和普通服務(wù)類(lèi)
            *檢測(cè)本地存根配置,并進(jìn)行相應(yīng)的處理
            *對(duì) ApplicationConfig、RegistryConfig 等配置類(lèi)進(jìn)行檢測(cè),為空則嘗試創(chuàng)建,若無(wú)法創(chuàng)建則拋出異常     
        2、doExport()在參數(shù)合法的情況下,調(diào)用doExportUrls()方法,該方法對(duì)多協(xié)議,多注冊(cè)中心進(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)類(lèi)是JavassistProxyFactory。

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容