美文网首页Dubbo源码解析
Dubbo源码学习四--Dubbo服务暴露机制

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

作者: 割草的小猪头 | 来源:发表于2019-08-19 22:13 被阅读0次

    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源码结构及实现方

    相关文章

      网友评论

        本文标题:Dubbo源码学习四--Dubbo服务暴露机制

        本文链接:https://www.haomeiwen.com/subject/ylrysctx.html