Dubbo源码学习四--Dubbo服务暴露机制

1. Dubbo服务启动过程

启动一个Dubbo服务,通过启动日志,查看Dubbo服务启动的过程中都做了哪些事情:

图1---Dubbo服务启动过程日志

通过启动日志可以看到,在Dubbo服务发布过程中做了以下的一系列动作:

1.暴露本地服务
2.暴露远程服务
3.启动Netty服务
4.连接Zookeeper并到Zookeeper进行注册
5.监听Zookeeper

通过Dubbo发布过程的详细图解看下服务提供者暴露服务的一个详细过程:

图1--服务提供者暴露服务过程

首先ServiceConfig拿到对外提供服务的实际类ref(如Dubbo源码中 dubbo-demo-xml-provider下的DemoServiceImpl类),然后通过ProxyFactory的getInvoker方法使用ref生成一个AbstractProxyInvoker的实例,到这一步就完成具体服务到Invoker的转换,接下来就是Invoker到Exporter的转换。

2.本地服务暴露的实现过程

从Dubbo的启动过程我们可以得知,Dubbo服务提供者,先进行本地暴露再进行远程暴露,在我们日常的使用场景中用的最多的是远程暴露。那么为什么还要进行本地暴露呢?
很多使用Dubbo框架的应用,可能存在在同一个JVM暴露了远程服务,同时同一个JVM内部又引用了自身服务的情况,Dubbo默认会把远程服务用injvm协议再暴露一份,这样消费方直接消费用一个JVM内部的服务,避免了跨网络进行远程通信。
我们先来看一下启动Dubbo服务提供者时最开始输出的日志:

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

可以根据日志看出启动的流程一下关键步骤:
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可以启动

那么我们根据这句日志输出做为一个切入点,来搜索下是哪里输出了 The service ready on spring started. 这句日志发现在ServiceBean类中出现
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这个类,到底是怎么执行的。
通过配置文件的加载过程我们可以知道,在服务启动时,Spring会解析dubbo服务的配置文件,并将配置文件中的参数转换成相应的Bean,当遇到 <dubbo:service> 时就会转换成ServiceBean。

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

以上为ServiceBean的定义,集成了ServiceConfig同时实现了InitializingBean、ApplicationListener等多个接口。在InitializingBean接口中有一个有个afterPropertiesSet方法,ServiceBean重写了该方法,在Bean的属性初始化时,Spring会默认调用该方法。
同时实现了ApplicationListener接口,并且重写了onApplicationEvent()方法。这两个接口都是Spring提供的接口,那么这两个接口会起到什么作用呢?

ServiceBean在创建完对象之后,会调用afterPropertiesSet()方法,该方法完成beanClass属性值的设置;在IOC容器启动完成之后,Spring会自动回调onApplicationEvent()方法,该方法完成服务的暴露,也就是在该方法中,我们看到了日志中的
The service ready on spring started.

3.服务暴露方法实现的解析

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

public void onApplicationEvent(ContextRefreshedEvent 
event) {   
/**如果是暴露的、并且没有暴露的则调用export方法,暴露服务*/
if (!isExported() && !isUnexported()) {       
if (logger.isInfoEnabled()) {            logger.info("The service ready on spring 
started. service: " + getInterface());        
        }       
export();    
    }
}

该方法为服务暴露的入口,那么暴露逻辑的真正实现则是在export方法中,接下来对export方法进行解析。
ServicebBean--export()方法:

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

通过export方法的代码可知,在这里调用父类的export()方法,之后调用调用publishExportEvent()方法。

下面看下 ServiceConfig的export()方法都实现了哪些逻辑。

 public synchronized void export() {
        //检测一些必要的属性和设置一些默认值
        checkAndUpdateSubConfigs();
        //判断是否已经导出
        if (!shouldExport()) {
            return;
        }
        //判断是否已经设置了延迟
        if (shouldDelay()) {
            DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
        } else {
        //执行导出动作
            doExport();
        }
    }

该方法除了检测基本的配置,以及在没有配置的情况下为配置设置默认值之外,最关键的是执行 doExport()方法,进一步查看doExzport方法都实现了什么逻辑。

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 表示服务提供者模型,此对象中存储了与服务提供者相关的信息。
        // 比如服务的配置信息,服务实例等。每个被导出的服务对应一个 ProviderModel。
        // ApplicationModel 持有所有的 ProviderModel。
 
            ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
            ApplicationModel.initProviderModel(pathKey, providerModel);
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

以上代码通过loadRegistries 加载注册中心链接,然后再遍历 ProtocolConfig 集合导出每个服务。并在暴露服务的过程中,将服务注册到注册中心。

loadRegistries()方法:

protected List<URL> loadRegistries(boolean provider) {
        // check && override if necessary
        List<URL> registryList = new ArrayList<URL>();
        //判断注册配置是否为空,及配置文件中有没有<dubbo:registry>标签(配置注册中心的一些信息)
        if (CollectionUtils.isNotEmpty(registries)) {
            //由于可能配置多个注册中心地址,在这里遍历注册列表
            for (RegistryConfig config : registries) {
                String address = config.getAddress();
                //判断配置的address是否为空,如果为空则配置为 0.0.0.0(由于在此步骤前已经对registry的地址做了非空校验,一般走不到这一步)
                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 协议头设置为 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;
    }

经过这一步骤之后,已经获取到了注册中心的地址,接下来就是调用doExportUrlsFor1Protocol()方法组装参数并且暴露Dubbo服务。

服务导出(暴露)过程概述

1. Spring容器启动时通过NameSpaceHandler来解析Dubbo服务的配置文件,并对配置文件中的参数进行初始化。
2.加载完配置文件之后,在ServiceBean的onApplicationEvent()方法受到Spring上下文刷新事件后会执行导出(export())方法。该方法为Dubbo服务导出的起点。

        Export()方法逻辑分析:
        
        1、进行参数配置的检查,检查该方法是否允许导出,另外检查该方法是否延迟导出。满足导出的条件之后,调用doExport()方法
        
            备注:

            *  检测 <dubbo:service> 标签的 interface 属性合法性,不合法则抛出异常   
            * 检测 ProviderConfig、ApplicationConfig 等核心配置类对象是否为空,若为空,则尝试从其他配置类对象中获取相应的实例。
            *检测并处理泛化服务和普通服务类
            *检测本地存根配置,并进行相应的处理
            *对 ApplicationConfig、RegistryConfig 等配置类进行检测,为空则尝试创建,若无法创建则抛出异常     
        2、doExport()在参数合法的情况下,调用doExportUrls()方法,该方法对多协议,多注册中心进行了支持。
        3、doExportUrls() 方法调用  doExportUrlsFor1Protocol()方法,该方法实现了由具体服务到Invoker的转换和Invoker到Exporter的转换。
            备注:
                invoker由ProxyFactory创建,Dubbo默认的ProxyFactory的实现类是JavassistProxyFactory。

我是割草的小猪头,不断学习,不断进步,后续陆续更新Dubbo系列的文章,如您有兴趣一起了解,欢迎关注,如文章中有不妥之处,欢迎指正!

Dubbo系列文章一--Dubbo重点掌握模块
Dubbo系列文章二--配置文件加载过程
Dubbo系列文章三--Dubbo源码结构及实现方

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容