dubbo集成spring服务发布流程

  • Post author:
  • Post category:其他



前言:

本文主要讲解dubbo集成spring进行的服务暴露过程。大致流程我从处理标签开始讲解处理,从spring解析我们dubbo的xml文件到暴露服务的一个大致过程,(注:对spring的扩展点有一点了解。)后续流程发布细节讲持续更新发布。文中如有错误点请各位大佬指出,当前文章也只是本人在阅读源码当中的一个记录点。下面我们进入正文。


服务导出简要流程如下:

  1. 读取配置信息生成

    ServiceBean ->

    ServiceConfig.export()
  2. 服务注册根据配置信息将服务以及相关信息注册到注册中心->RegistryProtocol.export()
  3. 根据配置的协议不同,启动Netty或Tomcat
  4. Dubbo服务提供者启动监听动态配置修改
  5. 服务导出完成,发布导出完成事件

DubboApplicationListenerRegistrar集成于spring的ApplicationContextAware接口。这一步注册监听器。着重讲解下

DubboBootstrapApplicationListener

监听器,内部内部初始化了dubbo的启动类

DubboBootstrap

其中

DubboBootstrapApplicationListener


的监听器会由spring来进行对应的回调,在回调的时候进行了对应的服务注册。其中使用到了启动类


DubboBootstrap


启动类的主要作用如下:

  1. 初始化

    configManager



    environment

    对象。
  2. 注册

    DubboShutdownHook

    钩子,实现优雅停机。
  3. 更新配置中心

    ConfigCenterBean

    对象对应的值。
  4. 加载并更新远程配置。
  5. 检查

    configManager

    对象各个配置对象是否合法。
  6. 加载并初始化元数据中心。
  7. Dubbo服务导出。

启动类

DubboBootstrap

内置2个关键配置类

ConfigManager



Environment

这2个类都是于整个dubbo服务的配置信息。核心暴露方法为

DubboBootstrap.start()

  public DubboBootstrap start() {
        if (started.compareAndSet(false, true)) {
            // ......省略代码
            initialize();
            // ......省略代码
            exportServices();
        }
        return this;
    }

当前我们主要关注的是

start()


方法内部的

initialize()和exportServices()方法,该2个方法和我们暴露服务的过程息息相关,具体细致的源码各位可以GitHub拉取对应源码进行阅读。

initialize()该方法为Dubbo初始化配置、环境、元数据等,具体方法如下:

    public void initialize() {
        if (!initialized.compareAndSet(false, true)) {
            return;
        }
        // 初始化环境,就是我们读取到的配置文件信息,ConfigManager类和Environment
        ApplicationModel.initFrameworkExts();
        // 开启配置中心
        startConfigCenter();
        // 加载远程配置详细
        loadRemoteConfigs();
        // 验证全局配置是否合法
        checkGlobalConfigs();
        // @since 2.7.8,开启元数据中心
        startMetadataCenter();
        // 初始化元数据中心
        initMetadataService();
        if (logger.isInfoEnabled()) {
            logger.info(NAME + " has been initialized!");
        }
    }


上面也有介绍到我们对应的

configManager类,其中该类就保存着我们解析过后的配置信息,不管是标签也好还是注解也好,所有的配置信息都会存储在该管理器内部,顾名思义。内部由一个map进行存储着所有的配置信息。

final Map<String, Map<String, AbstractConfig>> configsCache = newMap();

完成上述步骤我们整个的dubbo所需要的环境是已经准备好了,下面就是我们注册服务和暴露服务的一个部分了。

exportServices()该方法主要用于暴露服务,其中注册,导出等一系列核心逻辑都在该方法完成。

 private void exportServices() {
        // 从配置管理器中获取所有关于暴露服务的配置信息类
        //  <dubbo:service interface="org.apache.dubbo.demo.DemoService" timeout="3000" ref="demoService" registry="myRegistry, ztoRegistry"/>
        configManager.getServices().forEach(sc -> {
            // 服务配置类,顾名思义存储我们服务暴露的配置信息,其实该类为ServiceBean->ServiceConfig类
            ServiceConfig serviceConfig = (ServiceConfig) sc;
            serviceConfig.setBootstrap(this);
            // 是否开启异步导出服务,其实都是调用exportService来导出我们的服务
            if (exportAsync) {
                ExecutorService executor = executorRepository.getServiceExporterExecutor();
                Future<?> future = executor.submit(() -> {
                    try {
                        exportService(serviceConfig);
                    } catch (Throwable t) {
                        logger.error("export async catch error : " + t.getMessage(), t);
                    }
                });
                asyncExportingFutures.add(future);
            } else {
                // 导出服务(暴露服务)
                exportService(serviceConfig);
            }
        });
    }
   private void exportService(ServiceConfig sc) {
        if (exportedServices.containsKey(sc.getServiceName())) {
            throw new IllegalStateException("There are multiple ServiceBean instances with the same service name: [" +
                    sc.getServiceName() + "], instances: [" +
                    exportedServices.get(sc.getServiceName()).toString() + ", " +
                    sc.toString() + "]. Only one service can be exported for the same triple (group, interface, version), " +
                    "please modify the group or version if you really need to export multiple services of the same interface.");
        }
        // 导出暴露服务
        sc.export();
        exportedServices.put(sc.getServiceName(), sc);
    }

ServiceConfig.export()进行服务响应的暴露,在暴露前针对默认的配置进行对应的检查。检查服务

checkAndUpdateSubConfigs()

,元数据的处理

initServiceMetadata()

,和争对服务的导出

doExport()

方法。源码如下:

    @Override
    public synchronized void export() {
        if (bootstrap == null) {
            bootstrap = DubboBootstrap.getInstance();
            // compatible with api call.
            if (null != this.getRegistry()) {
                bootstrap.registries(this.getRegistries());
            }
            bootstrap.initialize();
        }

        // 对默认配置进行检查,某些配置没有提供时,提供缺省值
        checkAndUpdateSubConfigs();
        // 初始化元数据的一些信息
        initServiceMetadata(provider);
        // 设置元数据信息,服务类型,服务引用
        serviceMetadata.setServiceType(getInterfaceClass());
        serviceMetadata.setTarget(getRef());
        serviceMetadata.generateServiceKey();
        if (!shouldExport()) {
            return;
        }
        // 是否异步导出
        if (shouldDelay()) {
            DELAY_EXPORT_EXECUTOR.schedule(() -> {
                try {
                    // Delay export server should print stack trace if there are exception occur.
                    this.doExport();
                } catch (Exception e) {
                    logger.error("delay export server occur exception, please check it.", e);
                }
            }, getDelay(), TimeUnit.MILLISECONDS);
        } else {
            // 导出服务
            doExport();
        }
        exported();
    }

这里我们重点讲解

doExport()


内部的


doExportUrls()

方法,该方法才是真正的导出服务的核心。

    private void doExportUrls() {
        // 所有的注册中心地址信息
        List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);
        int protocolConfigNum = protocols.size();
        // 所有的协议,这里涉及到多协议多注册的情况
        for (ProtocolConfig protocolConfig : protocols) {
            String pathKey = URL.buildKey(getContextPath(protocolConfig)
                    .map(p -> p + "/" + path)
                    .orElse(path), group, version);
            // In case user specified path, register service one more time to map it to path.
            repository.registerService(pathKey, interfaceClass);
            // 暴露服务,这里会核心会创建Invoker和Exporter
            doExportUrlsFor1Protocol(protocolConfig, registryURLs, protocolConfigNum);
        }
    }

这里我们需要着重讲解

doExportUrlsFor1Protocol()


方法。该方法内部核心就是创建一个可执行体


Invoker


对象和发布服务创建出


Exporter

对象。其中内部主要作用就是为一下几点:

1.本地服务导出:其内部根据URL 中Protocol类型为 injvm,会选择

InjvmProtocol

方法为

exportLocal(url);

2.远程服务导出 & 有注册中心:其内部根据URL 中 Protocol 类型为 registry,会选择

RegistryProtocol


3.远程服务导出 和没有注册中心:根据服务协议头类型判断,我们这里假设是 dubbo ,则会选择

DubboProtocol

核心的部分也就是 RegistryProtocol#export 、 InjvmProtocol#export 、DubboProtocol#export 三个方法的执行过程。后续文章我将更加的细致的讲解其三个的暴露过程。


RegistryProtocol#export

服务注册


DubboProtocol#export

服务暴露,比如启动 了Netty 服务,让提供者获得网络通信的能力也在内部进行处理


InjvmProtocol#export

本地服务暴露


doExportUrlsFor1Protocol()


方法内部的核心代码部分:

 Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass,
                                registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
                        DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                        Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
 Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));

首先我们来讲解下Invoker,其实在得到Invoker对象,已经能够调用到我们对应的服务,它个人被我们理解成一个可执行体,最终不管rpc框架如何封装和远程调用发起到服务端,都是围绕Invoker对象来处理。因为他内部存储着我们真正的引用对象。值得讲解的一下是,上述代码获取到的invoker对象是经历过重重封装的。

扩展知识:其内部涉及到的设计模式有代理模式,责任链模式,装饰者模式,其中内部的拦截器也是在内部封装存储着,就使用到了责任链模式,内部多个拦截器形成了一个链条各司其职的拦截。当然装饰者模式在dubbo源码中随处可见,比如spi机制获取到的装载对象。

Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);

其次我们讲解下Exporter对象,当前在执行

PROTOCOL.export(wrapperInvoker)


方法是整个dubbo的核心可以这样理解,当前方法把我们的服务暴露到了我们的注册中心,和建立了网络通信,监听端口,注册监听器,处理服务调用端的请求。到了这里就完成了一个服务的暴露的完整过程,其实内部的实现细节相当多。这里我们只讲服务的整体暴露流程。


扩展知识:在阅读


PROTOCOL.export(wrapperInvoker)


方法时候一定要先理解dubbo的SPI机制,不然在这里会找不到头脑,我也是在读这部分的源码途中被绕来绕去,所以在熟悉SPI机制之后的运行原理才来读该部分源码,才能如鱼得水读懂该源码。

该文章是我对自己看整个dubbo源码的一个梳理,如有什么有误的地方还请各位大佬指出。谢谢啦。整体执行流程如下:



版权声明:本文为m0_49516995原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。