查看原文
其他

Dubbo中暴露服务的过程解析

大程熙 搜云库技术团队 2019-04-07
搜云库互联网/架构/开发/运维关注

原文地址:http://cxis.me/2017/02/19/Dubbo中暴露服务的过程解析

示例项目源码:https://github.com/souyunku/spring-boot-examples/tree/master/spring-boot-dubbo

Dubbo会在Spring实例化完bean之后,在刷新容器最后一步发布ContextRefreshEvent事件的时候,通知实现了ApplicationListener的ServiceBean类进行回调onApplicationEvent 事件方法,dubbo会在这个方法中调用ServiceBean父类ServiceConfig的export方法,而该方法真正实现了服务的(异步或者非异步)发布。

加载dubbo配置

Spring容器在启动的时候,会读取到Spring默认的一些schema以及Dubbo自定义的schema,每个schema都会对应一个自己的NamespaceHandler,NamespaceHandler里面通过BeanDefinitionParser来解析配置信息并转化为需要加载的bean对象。

遇到dubbo名称空间 ,首先会调用DubboNamespaceHandler类的 init方法 进行初始化操作。

根据命名空间去获取具体的处理器NamespaceHandler。那具体的处理器是在哪定义的呢,在” META-INF/spring.handlers”文件中,Spring在会自动加载该文件中所有内容。

META-INF/spring.handlers

  1. http\://code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler

META-INF/spring.schemas

  1. http\://code.alibabatech.com/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsd

根据不同的XML节点,会委托NamespaceHandlerSupport 类找出合适的BeanDefinitionParser,其中Dubbo所有的标签都使用 DubboBeanDefinitionParser进行解析,基于一对一属性映射,将XML标签解析为Bean对象。

DubboNamespaceHandler.java 类代码如下

  1. package com.alibaba.dubbo.config.spring.schema;

  2. import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

  3. public class DubboNamespaceHandler extends NamespaceHandlerSupport {

  4.    static {

  5.        Version.checkDuplicate(DubboNamespaceHandler.class);

  6.    }

  7.    public void init() {

  8.        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));

  9.        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));

  10.        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));

  11.        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));

  12.        registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));

  13.        registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));

  14.        registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));

  15.        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));

  16.        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));

  17.        registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));

  18.    }

  19. }

由于DubboBeanDefinitionParser 类中 parse转换的过程代码还是比较复杂,只抽离出来bean的注册这一块的代码如下

DubboBeanDefinitionParser.java 类代码如下

  1. package com.alibaba.dubbo.config.spring.schema;

  2. public class DubboBeanDefinitionParser implements BeanDefinitionParser {

  3.    @SuppressWarnings("unchecked")

  4.    private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {

  5.    RootBeanDefinition beanDefinition = new RootBeanDefinition();

  6.        beanDefinition.setBeanClass(beanClass);

  7.        beanDefinition.setLazyInit(false);

  8.        String id = element.getAttribute("id");

  9.        //省略......

  10.         if(id != null && id.length() > 0) {

  11.            if(parserContext.getRegistry().containsBeanDefinition(id)) {

  12.                throw new IllegalStateException("Duplicate spring bean id " + id);

  13.            }

  14.            //registerBeanDefinition 注册Bean的定义

  15.            //具体的id如下 applicationProvider.xml解析后的显示 id,

  16.            //如id="dubbo_provider"  beanDefinition = "ApplicationConfig"

  17.            parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);

  18.            beanDefinition.getPropertyValues().addPropertyValue("id", id);

  19.        }

  20.     }  

  21. }

通过DubboBeanDefinitionParser 类的 parse方法会将class信息封装成BeanDefinition,然后将BeanDefinition再放进DefaultListableBeanFactory的beanDefinitionMap中。

最后通过Spring bean 的加载机制进行加载。

服务暴露过程

Dubbo会在Spring实例化完bean之后,在刷新容器最后一步发布ContextRefreshEvent事件的时候,通知实现了ApplicationListener的ServiceBean类进行回调onApplicationEvent 事件方法,dubbo会在这个方法中调用ServiceBean父类ServiceConfig的export方法,而该方法真正实现了服务的(异步或者非异步)发布。

服务暴露入口

由服务配置类 ServiceConfig 进行初始化工作及服务暴露入口,首先进去执行该类的export()方法。

ServiceConfig.java 类的 export 方法

export的步骤简介

  1. 首先会检查各种配置信息,填充各种属性,总之就是保证我在开始暴露服务之前,所有的东西都准备好了,并且是正确的。

  2. 加载所有的注册中心,因为我们暴露服务需要注册到注册中心中去。

  3. 根据配置的所有协议和注册中心url分别进行导出。

  4. 进行导出的时候,又是一波属性的获取设置检查等操作。

  5. 如果配置的不是remote,则做本地导出。

  6. 如果配置的不是local,则暴露为远程服务。

  7. 不管是本地还是远程服务暴露,首先都会获取Invoker。

  8. 获取完Invoker之后,转换成对外的Exporter,缓存起来。

export方法先判断是否需要延迟暴露(这里我们使用的是不延迟暴露),然后执行doExport方法。

doExport方法先执行一系列的检查方法,然后调用doExportUrls方法。检查方法会检测dubbo的配置是否在Spring配置文件中声明,没有的话读取properties文件初始化。

doExportUrls方法先调用loadRegistries获取所有的注册中心url,然后遍历调用doExportUrlsFor1Protocol方法。对于在标签中指定了registry属性的Bean,会在加载BeanDefinition的时候就加载了注册中心。

ServiceConfig.java 类的 export 方法

  1. package com.alibaba.dubbo.config;

  2. public class ServiceConfig<T> extends AbstractServiceConfig {

  3. public synchronized void export() {

  4.    if (provider != null) {

  5.        if (export == null) {

  6.            export = provider.getExport();

  7.        }

  8.        if (delay == null) {

  9.            delay = provider.getDelay();

  10.        }

  11.    }

  12.    if (export != null && !export) {

  13.        return;

  14.    }

  15.    if (delay != null && delay > 0) {

  16.        delayExportExecutor.schedule(new Runnable() {

  17.            public void run() {

  18.                doExport();

  19.            }

  20.        }, delay, TimeUnit.MILLISECONDS);

  21.    } else {

  22.        doExport();

  23.    }

  24. }

可以看出发布发布是支持延迟暴露发布服务的,这样可以用于当我们发布的服务非常多,影响到应用启动的问题,前提是应用允许服务发布的延迟特性。

接下来就进入到 ServiceConfig.java 类的 doExport() 方法。

检查DUBBO配置的合法性

ServiceConfig.java 类的 doExport方法。检查DUBBO配置的合法性,并调用doExportUrls 方法。

  1. package com.alibaba.dubbo.config;

  2. public class ServiceConfig<T> extends AbstractServiceConfig {

  3. protected synchronized void doExport() {

  4.       // 省略。。。

  5.        checkApplication();

  6.        checkRegistry();

  7.        checkProtocol();

  8.        appendProperties(this);

  9.        checkStubAndMock(interfaceClass);

  10.        if (path == null || path.length() == 0) {

  11.            path = interfaceName;

  12.        }

  13.        doExportUrls();

  14.    }

  15. }

我们可以看出该方法的实现的逻辑包含了根据配置的优先级将 ProviderConfig,ModuleConfig,MonitorConfig,ApplicaitonConfig等一些配置信息进行组装和合并。还有一些逻辑是检查配置信息的合法性。最后又调用了doExportUrls方法。

服务多协议暴露过程

ServiceConfig.java 类的 doExportUrls() 方法

  1. package com.alibaba.dubbo.config;

  2. public class ServiceConfig<T> extends AbstractServiceConfig {

  3.    @SuppressWarnings({"unchecked", "rawtypes"})

  4.    private void doExportUrls() {

  5.        List<URL> registryURLs = loadRegistries(true);

  6.        for (ProtocolConfig protocolConfig : protocols) {

  7.            doExportUrlsFor1Protocol(protocolConfig, registryURLs);

  8.        }

  9.    }

  10. }

该方法第一步是加载注册中心列表

loadRegistries(true);加载注册中心列表响应示例

  1. registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.5.6&file=/data/dubbo/cache/dubbo-provider&pid=21448&registry=zookeeper&timestamp=1524134852031

第二部是将服务发布到多种协议的url上,并且携带注册中心列表的参数,从这里我们可以看出dubbo是支持同时将一个服务发布成为多种协议的,这个需求也是很正常的,客户端也需要支持多协议,根据不同的场景选择合适的协议。

ServiceConfig.java 类的 doExportUrlsFor1Protocol(ProtocolConfigprotocolConfig,List<URL>registryURLs) 方法。

  1. package com.alibaba.dubbo.config;

  2. public class ServiceConfig<T> extends AbstractServiceConfig {

  3.    private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {

  4.        // 省略很多

  5.            if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {

  6.                //配置不是remote的情况下做本地暴露 (配置为remote,则表示只暴露远程服务)

  7.                if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {

  8.                    exportLocal(url);

  9.                }

  10.                //如果配置不是local则暴露为远程服务.(配置为local,则表示只暴露本地服务)

  11.                if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {

  12.                    if (logger.isInfoEnabled()) {

  13.                        logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);

  14.                    }

  15.                    if (registryURLs != null && registryURLs.size() > 0

  16.                            && url.getParameter("register", true)) {

  17.                        for (URL registryURL : registryURLs) {

  18.                            url = url.addParameterIfAbsent("dynamic", registryURL.getParameter("dynamic"));

  19.                            URL monitorUrl = loadMonitor(registryURL);

  20.                            if (monitorUrl != null) {

  21.                                url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());

  22.                            }

  23.                            if (logger.isInfoEnabled()) {

  24.                                logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);

  25.                            }

  26.                            Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));

  27.                            Exporter<?> exporter = protocol.export(invoker);

  28.                            exporters.add(exporter);

  29.                        }

  30.                    } else {

  31.                        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);

  32.                        Exporter<?> exporter = protocol.export(invoker);

  33.                        exporters.add(exporter);

  34.                    }

  35.                }

  36.            }

  37.            this.urls.add(url);

  38.        }

  39. }

拼装dubbo服务URL

该方法的逻辑是先根据服务配置、协议配置、发布服务的服务器信息、方法列表、dubbo版本等等信息组装成一个发布的URL对象。

主要根据之前map里的数据组装成URL。

例如

  1. dubbo://10.4.81.95:20880/io.ymq.dubbo.api.DemoService?

  2. anyhost=true&

  3. application=dubbo-provider&

  4. default.connections=5&

  5. default.delay=-1&

  6. default.retries=0&

  7. default.timeout=10000&

  8. default.version=1.0&

  9. delay=-1&

  10. dubbo=2.5.6&

  11. generic=false&

  12. interface=io.ymq.dubbo.api.DemoService&

  13. methods=sayHello&

  14. pid=21448&

  15. side=provider&

  16. threadpool=fixed&

  17. threads=500&

  18. timestamp=1524135271940

本地暴露和远程暴露

  1. 如果服务配置的scope是发布范围,配置为none不暴露服务,则会停止发布操作;

  2. 如果配置不是remote的情况下先做本地暴露,则调用本地暴露exportLocal方法;

  3. 如果配置不是local则暴露为远程服务,则注册服务registryProcotol;

  1. //配置为none不暴露

  2. if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {

  3.    //配置不是remote的情况下做本地暴露 (配置为remote,则表示只暴露远程服务)

  4.    if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {

  5.        exportLocal(url);

  6.    }

  7.    //如果配置不是local则暴露为远程服务.(配置为local,则表示只暴露本地服务)

  8.    if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {

  9.        省略更多

  10.    }

  11. }

由于文章过长

请点击文章末尾, 阅读原文

长按:二维码关注

专注于开发技术研究与知识分享

搜云库互联网/架构/开发/运维关注

福利:加群深入交流

【Q Q群】点击链接 搜云库-技术分享QQ群

【微信群】公众号后台回复 “进群” 邀请您进 技术分享群

点击下方“阅读原文”查看更多

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存