Dubbo3.2.x-Provider启动 服务发布与注册流程源码解析

开始之前,先认识一些核心组件

DubboBootstrap

印象里,Dubbo2还没有DubboBootstrap,该组件应该是Dubbo3新加入的,这个组件的引入,主要是为了解决Dubbo在启动和初始化过程中的一些复杂性和灵活性问题,相当于一个Dubbo服务启动的引导工具。

ScopeModel

ScopeModel类在 Apache Dubbo 框架中是一个抽象基类,用于定义不同作用域(如应用级[ApplicationModel]、模块级[ModuleModel]等)的模型,一个application下可以存在多个module。

ModuleDeployer

顾名思义,在Dubbo中扮演着模块部署者的角色,继承了Deployer接口,对应着ModuleModel,负责module的初始化、启动、停止、销毁等生命周期。官方给出的注释是"Export/refer services of module",意为"发布/引用模块的应用实例"

MetaDataCenter

元数据中心是Dubbo3.0之后产生的,在此版本之前,Dubbo还是接口级服务发现机制,之后,产生了应用级服务发现机制来解决异构微服务体系互通与大规模集群实践的性能问题,MetaData包含接口-应用映射关系以及接口配置元数据两部分内容,Dubbo官网对MetaDataCenter有详细的介绍,感兴趣可以跳转到官网详细查看https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/metadata-center/overview/

Invoker

Invoker属于Dubbo远程调用的核心组件,代表了一个可以执行远程调用的引用。

正式开始解析Provider从启动到services发布的源码流程

从Dubbo官网,将3.2.x版本的源码下载下来之后,打开我们的zk,就可以开始我们的调试。 在这之前,声明一点,下面讲解的源码流程会略过一些方法,着重于重点核心流程的讲解。

调试入口

dubbo源码中,有天然的demo供我们进行本地调试,路径:org.apache.dubbo.demo.provider.Application

Provider启动的demo中的代码如下:

java 复制代码
public class Application {

    private static final String REGISTRY_URL = "zookeeper://127.0.0.1:2181";

    public static void main(String[] args) {
        startWithBootstrap();
    }

    private static void startWithBootstrap() {
        // 服务提供者暴露服务配置
        ServiceConfig<DemoServiceImpl> service = new ServiceConfig<>();
        // 服务提供者暴露服务的接口
        service.setInterface(DemoService.class);
        // 服务提供者暴露服务的实现
        service.setRef(new DemoServiceImpl());

        DubboBootstrap bootstrap = DubboBootstrap.getInstance();
        // 启动服务提供者暴露服务
        bootstrap
                // 应用名
                .application(new ApplicationConfig("dubbo-demo-api-provider"))
                // 注册中心地址
                .registry(new RegistryConfig(REGISTRY_URL))
                // 服务暴露协议配置
                .protocol(new ProtocolConfig(CommonConstants.DUBBO, -1))
                .service(service)
                // 启动服务提供者暴露服务,等待服务消费方调用(v2版本没有start,是直接使用ModuleDeployer调用的export()方法)
                .start()
                // 线程等待
                .await();
    }
}

代码解析:这段代码其实就是Dubbo的Provider启动入口,设置了DubboBootstrap实例的必要参数,后续通过.start()方法来开始启动Provider。

一路点,随后跳到org.apache.dubbo.config.deploy.DefaultApplicationDeployer#start

java 复制代码
@Override
public Future start() {
    // 涉及共享资源,加同步锁
    synchronized (startLock) {
        if (isStopping() || isStopped() || isFailed()) {
            throw new IllegalStateException(getIdentifier() + " is stopping or stopped, can not start again");
        }

        try {
            // maybe call start again after add new module, check if any new module
            boolean hasPendingModule = hasPendingModule();
            // 若目前处于starting状态
            if (isStarting()) {
                // currently, is starting, maybe both start by module and application
                // if it has new modules, start them
                if (hasPendingModule) {
                    startModules();
                }
                // if it is starting, reuse previous startFuture
                return startFuture;
            }

            // if is started and no new module, just return
            if (isStarted() && !hasPendingModule) {
                return CompletableFuture.completedFuture(false);
            }

            // 状态的修改
            // pending -> starting : first start app
            // started -> starting : re-start app
            onStarting();
            // ApplicationDeployer的初始化
            initialize();

            doStart();
        } catch (Throwable e) {
            onFailed(getIdentifier() + " start failure", e);
            throw e;
        }

        return startFuture;
    }
}

代码解析:上述代码做的也是一些准备工作,首次启动的话,会跳过前部代码,到onStarting(),随后进行ApplicationDeployer的初始化,完成初始化后,执行doStart(),下面进入到initialize()来具体看一下初始化都干什么,doStart()流程后续再讲。

应用发布器初始化

org.apache.dubbo.config.deploy.DefaultApplicationDeployer#initialize代码如下,来看一下ApplicationDeployer的初始化流程

java 复制代码
@Override
public void initialize() {
    if (initialized) {
        return;
    }
    // Ensure that the initialization is completed when concurrent calls
    // 加同步锁,其他线程必须等待初始化执行完毕
    synchronized (startLock) {
        if (initialized) {
            return;
        }
        onInitialize();

        // register shutdown hook
        // 注册退出时销毁资源的狗子
        registerShutdownHook();

        // 启动配置中心
        startConfigCenter();

        // 加载应用配置信息
        loadApplicationConfigs();

        // 初始化模块部署器
        initModuleDeployers();

        // 初始化指标上报器
        initMetricsReporter();

        // 初始化指标服务,用于监控
        initMetricsService();

        // @since 2.7.8 启动元数据中心
        startMetadataCenter();

        initialized = true;

        if (logger.isInfoEnabled()) {
            logger.info(getIdentifier() + " has been initialized!");
        }
    }
}

代码解析:defaultApplicationDeployer的初始化流程中,对注册退出销毁资源的钩子、配置中心、应用配置信息、moduleDeployers、指标reporter、指标服务、元数据中心等进行了加载或启动,其中,initModuleDeployers()是对模块级别的Deployer进行初始化,而此处是对应用级别的初始化,这个关系可做了解,还有startMetadataCenter()方法中,会生成MetadataReport的组件,用于后续将服务实例的元数据上传到Zookeeper中。

到目前为止,可以画出下面这张流程图,来标识ApplicationDeployer的初始化过程

回到前面org.apache.dubbo.config.deploy.DefaultApplicationDeployer#start中的doStart()继续向下来看。

java 复制代码
private void doStart() {
    // 启动模块
    startModules();
    // 下面官方注释掉的无效代码此处省略掉了
}

private void startModules() {
    // ensure init and start internal module first
    prepareInternalModule();

    // filter and start pending modules, ignore new module during starting, throw exception of module start
    for (ModuleModel moduleModel : applicationModel.getModuleModels()) {
        if (moduleModel.getDeployer().isPending()) {
            moduleModel.getDeployer().start();
        }
    }
}

代码解析:前面认识核心组件里说过,一个Application下对应多个module,在doStart()方法中就对这些module进行了启动,跳到startModules()方法中可以看到,是通过moduleModel.getDeployer().start()去启动具体的module的(也就是通过ModuleDeployer去执行start的)。

服务上报

接下来开始讲解export services的流程,跟随start()->startSync()->exportServices()->exportServiceInternal(sc)->export()代码一路跳到org.apache.dubbo.config.ServiceConfig#export,如下所示

java 复制代码
@Override
public void export(RegisterTypeEnum registerType) {
    if (this.exported) {
        return;
    }

    if (getScopeModel().isLifeCycleManagedExternally()) {
        // prepare model for reference
        // 进行服务实例的准备工作
        getScopeModel().getDeployer().prepare();
    } else {
        // ensure start module, compatible with old api usage
        getScopeModel().getDeployer().start();
    }

    synchronized (this) {
        if (this.exported) {
            return;
        }

        if (!this.isRefreshed()) {
            this.refresh();
        }
        if (this.shouldExport()) {
            this.init();

            if (shouldDelay()) {
                // should register if delay export
                doDelayExport();
            } else if (Integer.valueOf(-1).equals(getDelay())
                    && Boolean.parseBoolean(ConfigurationUtils.getProperty(
                            getScopeModel(), CommonConstants.DUBBO_MANUAL_REGISTER_KEY, "false"))) {
                // should not register by default
                doExport(RegisterTypeEnum.MANUAL_REGISTER);
            } else {
                doExport(registerType);
            }
        }
    }
}

代码解析:prepare()中会进行ApplicationDeployer和ModuleDeployer的又一次初始化(可能很多人看到这里会有疑问,为什么前面执行的代码已经初始化过这些Deployer,后面还要再次调用初始化呢?答:其实是为了保证,不管从哪个口子进来,都能在真正的export之前保证初始化完成,避免遗漏)。接下来会执行refresh()init(),后续会根据是否延迟发布来决定执行doDelayExport()还是doExport()。下面对这几个方法进行单独讲解。

上报前配置刷新

refresh

此方法官方注解是Dubbo config property override,及刷新Dubbo服务实例的配置。 源码如下

java 复制代码
/**
 * Dubbo config property override
 */
public void refresh() {
    if (needRefresh) {
        try {
            // check and init before do refresh
            preProcessRefresh();
            // 根据前缀刷新
            refreshWithPrefixes(getPrefixes(), getConfigMode());
        } catch (Exception e) {
            logger.error(
                    COMMON_FAILED_OVERRIDE_FIELD,
                    "",
                    "",
                    "Failed to override field value of config bean: " + this,
                    e);
            throw new IllegalStateException("Failed to override field value of config bean: " + this, e);
        }

        postProcessRefresh();
    }
    refreshed.set(true);
}

调试时发现,preProcessRefesh()会先进行一个预处理,check and init before the refresh

java 复制代码
@Override
protected void preProcessRefresh() {
    super.preProcessRefresh();
    convertProviderIdToProvider();
    if (provider == null) {
        provider = getModuleConfigManager()
                .getDefaultProvider()
                .orElseThrow(() -> new IllegalStateException("Default provider is not initialized"));
    }
    // try set properties from `dubbo.service` if not set in current config
    refreshWithPrefixes(super.getPrefixes(), ConfigMode.OVERRIDE_IF_ABSENT);
}

在此方法中,首先会根据providerId换取provider,若为空,生成默认的provider,最后走到refreshWithPrefixes中。

java 复制代码
protected void refreshWithPrefixes(List<String> prefixes, ConfigMode configMode) {
    // 获取application级的环境配置
    Environment environment = getScopeModel().modelEnvironment();
    List<Map<String, String>> configurationMaps = environment.getConfigurationMaps();

    // Search props starts with PREFIX in order
    String preferredPrefix = null;
    for (String prefix : prefixes) {
        if (ConfigurationUtils.hasSubProperties(configurationMaps, prefix)) {
            preferredPrefix = prefix;
            break;
        }
    }
    if (preferredPrefix == null) {
        preferredPrefix = prefixes.get(0);
    }
    // Extract sub props (which key was starts with preferredPrefix)
    Collection<Map<String, String>> instanceConfigMaps = environment.getConfigurationMaps(this, preferredPrefix);
    Map<String, String> subProperties = ConfigurationUtils.getSubProperties(instanceConfigMaps, preferredPrefix);
    InmemoryConfiguration subPropsConfiguration = new InmemoryConfiguration(subProperties);

    if (logger.isDebugEnabled()) {
        String idOrName = "";
        if (StringUtils.hasText(this.getId())) {
            idOrName = "[id=" + this.getId() + "]";
        } else {
            String name = ReflectUtils.getProperty(this, "getName");
            if (StringUtils.hasText(name)) {
                idOrName = "[name=" + name + "]";
            }
        }
        logger.debug("Refreshing " + this.getClass().getSimpleName() + idOrName + " with prefix ["
                + preferredPrefix + "], extracted props: "
                + subProperties);
    }

    // 设置properties
    assignProperties(this, environment, subProperties, subPropsConfiguration, configMode);

    // process extra refresh of subclass, e.g. refresh method configs
    processExtraRefresh(preferredPrefix, subPropsConfiguration);
}

这个方法看着复杂,其实没太多核心代码,大多都是配置信息的设置,填充当前Config对象,重点看下processExtraRefresh()方法

java 复制代码
@Override
protected void processExtraRefresh(String preferredPrefix, InmemoryConfiguration subPropsConfiguration) {
    if (StringUtils.hasText(interfaceName)) {
        Class<?> interfaceClass;
        try {
            interfaceClass = ClassUtils.forName(interfaceName);
        } catch (ClassNotFoundException e) {
            // There may be no interface class when generic call
            return;
        }

        if (!interfaceClass.isInterface() && !canSkipInterfaceCheck()) {
            throw new IllegalStateException(interfaceName + " is not an interface");
        }

        // Auto create MethodConfig/ArgumentConfig according to config props
        Map<String, String> configProperties = subPropsConfiguration.getProperties();
        Method[] methods;
        try {
            // 通过反射获取当前接口中的方法
            methods = interfaceClass.getMethods();
        } catch (Throwable e) {
            // NoClassDefFoundError may be thrown if interface class's dependency jar is missing
            return;
        }

        for (Method method : methods) {
            if (ConfigurationUtils.hasSubProperties(configProperties, method.getName())) {
                MethodConfig methodConfig = getMethodByName(method.getName());
                // Add method config if not found
                if (methodConfig == null) {
                    methodConfig = new MethodConfig();
                    methodConfig.setName(method.getName());
                    this.addMethod(methodConfig);
                }
                // Add argument config
                // dubbo.service.{interfaceName}.{methodName}.{arg-index}.xxx=xxx
                java.lang.reflect.Parameter[] arguments = method.getParameters();
                for (int i = 0; i < arguments.length; i++) {
                    if (getArgumentByIndex(methodConfig, i) == null
                            && hasArgumentConfigProps(configProperties, methodConfig.getName(), i)) {

                        ArgumentConfig argumentConfig = new ArgumentConfig();
                        argumentConfig.setIndex(i);
                        methodConfig.addArgument(argumentConfig);
                    }
                }
            }
        }

        // refresh MethodConfigs
        List<MethodConfig> methodConfigs = this.getMethods();
        if (methodConfigs != null && methodConfigs.size() > 0) {
            // whether ignore invalid method config
            Object ignoreInvalidMethodConfigVal = getEnvironment()
                    .getConfiguration()
                    .getProperty(ConfigKeys.DUBBO_CONFIG_IGNORE_INVALID_METHOD_CONFIG, "false");
            boolean ignoreInvalidMethodConfig = Boolean.parseBoolean(ignoreInvalidMethodConfigVal.toString());

            Class<?> finalInterfaceClass = interfaceClass;
            List<MethodConfig> validMethodConfigs = methodConfigs.stream()
                    .filter(methodConfig -> {
                        methodConfig.setParentPrefix(preferredPrefix);
                        methodConfig.setScopeModel(getScopeModel());
                        methodConfig.refresh();
                        // verify method config
                        return verifyMethodConfig(methodConfig, finalInterfaceClass, ignoreInvalidMethodConfig);
                    })
                    .collect(Collectors.toList());
            this.setMethods(validMethodConfigs);
        }
    }
}

在此方法中,会对大量的interface下的Method和Method下的Arguments进行配置,得到MethodConfig和ArgumentConfig,此处就不一一赘述,我们只要了解它干了什么即可。至于上面拼接得到的prefix,在填充各种config时会用到,比如methodConfig.setParentPrefix(preferredPrefix)....

总结一下,ServiceConfig的refresh过程,实际上就是拼接 + 填充,拼接prefix,填充大量Config。 可以用下面这张图来概括一下refresh的过程

上报前元数据初始化

init 初始化服务的元数据

scss 复制代码
/**java
 * for early init serviceMetadata
 */
public void init() {
    if (this.initialized.compareAndSet(false, true)) {
        // load ServiceListeners from extension
        ExtensionLoader<ServiceListener> extensionLoader = this.getExtensionLoader(ServiceListener.class);
        this.serviceListeners.addAll(extensionLoader.getSupportedExtensionInstances());
    }
    initServiceMetadata(provider);
    serviceMetadata.setServiceType(getInterfaceClass());
    serviceMetadata.setTarget(getRef());
    serviceMetadata.generateServiceKey();
}
java 复制代码
protected void initServiceMetadata(AbstractInterfaceConfig interfaceConfig) {
    serviceMetadata.setVersion(getVersion(interfaceConfig));
    serviceMetadata.setGroup(getGroup(interfaceConfig));
    serviceMetadata.setDefaultGroup(getGroup(interfaceConfig));
    serviceMetadata.setServiceInterfaceName(getInterface());
}

上述代码主要对serviceMetadata进行了填充,包括接口类型、名称、实现类、版本、组等信息。

正式开始上报

doExport

根据源码可知,有三种doExport的方式,延迟发布、手动register、自动register,此处发布走的是自动register,可以直接跳到org.apache.dubbo.config.ServiceConfig#doExportUrls

java 复制代码
private void doExportUrls(RegisterTypeEnum registerType) {
    ModuleServiceRepository repository = getScopeModel().getServiceRepository();
    ServiceDescriptor serviceDescriptor;
    final boolean serverService = ref instanceof ServerService;
    if (serverService) {
        serviceDescriptor = ((ServerService) ref).getServiceDescriptor();
        if (!this.provider.getUseJavaPackageAsPath()) {
            // for stub service, path always interface name or IDL package name
            this.path = serviceDescriptor.getInterfaceName();
        }
        // 注册服务
        repository.registerService(serviceDescriptor);
    } else {
        // 走这里
        serviceDescriptor = repository.registerService(getInterfaceClass());
    }
    providerModel = new ProviderModel(
            serviceMetadata.getServiceKey(),
            ref,
            serviceDescriptor,
            getScopeModel(),
            serviceMetadata,
            interfaceClassLoader);

    // Compatible with dependencies on ServiceModel#getServiceConfig(), and will be removed in a future version
    providerModel.setConfig(this);

    providerModel.setDestroyRunner(getDestroyRunner());
    // 注册provider
    repository.registerProvider(providerModel);
    // 针对zk的注册url
    List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);
    // 协议
    for (ProtocolConfig protocolConfig : protocols) {
        String pathKey = URL.buildKey(
                getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
        // stub service will use generated service name
        if (!serverService) {
            // In case user specified path, register service one more time to map it to path.
            repository.registerService(pathKey, interfaceClass);
        }
        // 核心上报流程
        doExportUrlsFor1Protocol(protocolConfig, registryURLs, registerType);
    }

    providerModel.setServiceUrls(urls);
}

代码解析:首先通过getScopeModel()获取到了ModuleModel,并拿到了ModuleModelModuleServiceRepository,可以理解为拿到模块级服务仓库,要往该仓库注册一系列信息。后续将Service的ServiceDescriptorProviderModel注册到ModuleServiceRepository中(此处ServiceDescriptor注册了两次,第二次是为了In case user specified path, register service one more time to map it to path),并且会生成针对zk的注册url:registryURLs

后续来看doExportUrlsFor1Protocol方法

java 复制代码
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs, RegisterTypeEnum registerType) {
    // 这里构建元数据中的attributes
    Map<String, String> map = buildAttributes(protocolConfig);

    // remove null key and null value
    map.keySet().removeIf(key -> StringUtils.isEmpty(key) || StringUtils.isEmpty(map.get(key)));
    // init serviceMetadata attachments
    serviceMetadata.getAttachments().putAll(map);

    // 创建上报的服务实例url
    URL url = buildUrl(protocolConfig, map);
    // 处理服务执行器相关配置
    processServiceExecutor(url);

    exportUrl(url, registryURLs, registerType);

    initServiceMethodMetrics(url);
}

这个方法首先根据protocolConfig构建了metadata的attributes,并将这个map放到了serviceMetadata中;后续创建了一个服务实例上报的url(ex:dubbo://10.1.xx.xxx:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&background=false&bind.ip=10.1.68.219&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&executor-management-mode=isolation&file-cache=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=436&prefer.serialization=fastjson2,hessian2&side=provider&timestamp=1741333869816),走完这一步,后续会处理服务执行器相关配置(判断是否有执行器,存在执行器再判断执行器的管理模式,如果确定使用该执行器,则将执行器配置到ServiceMetadataattributes中),剩下的流程会上报url和初始化服务方法的指标,下面走进exportUrl()来看下url上报是干什么的。

org.apache.dubbo.config.ServiceConfig#exportUrl

java 复制代码
private void exportUrl(URL url, List<URL> registryURLs, RegisterTypeEnum registerType) {
    String scope = url.getParameter(SCOPE_KEY);
    // don't export when none is configured
    if (!SCOPE_NONE.equalsIgnoreCase(scope)) {

        // export to local if the config is not remote (export to remote only when config is remote)
        if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
            exportLocal(url);
        }

        // export to remote if the config is not local (export to local only when config is local)
        if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
            // export to extra protocol is used in remote export
            String extProtocol = url.getParameter("ext.protocol", "");
            List<String> protocols = new ArrayList<>();

            if (StringUtils.isNotBlank(extProtocol)) {
                // export original url
                url = URLBuilder.from(url)
                        .addParameter(IS_PU_SERVER_KEY, Boolean.TRUE.toString())
                        .removeParameter("ext.protocol")
                        .build();
            }

            url = exportRemote(url, registryURLs, registerType);
            if (!isGeneric(generic) && !getScopeModel().isInternal()) {
                MetadataUtils.publishServiceDefinition(url, providerModel.getServiceModel(), getApplicationModel());
            }

            if (StringUtils.isNotBlank(extProtocol)) {
                String[] extProtocols = extProtocol.split(",", -1);
                protocols.addAll(Arrays.asList(extProtocols));
            }
            // export extra protocols
            for (String protocol : protocols) {
                if (StringUtils.isNotBlank(protocol)) {
                    URL localUrl =
                            URLBuilder.from(url).setProtocol(protocol).build();
                    localUrl = exportRemote(localUrl, registryURLs, registerType);
                    if (!isGeneric(generic) && !getScopeModel().isInternal()) {
                        MetadataUtils.publishServiceDefinition(
                                localUrl, providerModel.getServiceModel(), getApplicationModel());
                    }
                    this.urls.add(localUrl);
                }
            }
        }
    }
    this.urls.add(url);
}

上述url上报可以分为local上报和remote上报,及jvm和zk(或者你的其他注册中心)

本地服务实例发布

exportLocal

java 复制代码
/**
 * always export injvm
 */
private void exportLocal(URL url) {
    URL local = URLBuilder.from(url)
            .setProtocol(LOCAL_PROTOCOL)
            .setHost(LOCALHOST_VALUE)
            .setPort(0)
            .build();
    local = local.setScopeModel(getScopeModel()).setServiceModel(providerModel);
    local = local.addParameter(EXPORTER_LISTENER_KEY, LOCAL_PROTOCOL);
    doExportUrl(local, false, RegisterTypeEnum.AUTO_REGISTER);
    logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}

build后的url:injvm://127.0.0.1/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&background=false&bind.ip=10.1.68.219&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&executor-management-mode=isolation&exporter.listener=injvm&file-cache=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=15060&prefer.serialization=fastjson2,hessian2&side=provider&timestamp=1741335639247,后续走下面的method

doExportUrl

java 复制代码
private void doExportUrl(URL url, boolean withMetaData, RegisterTypeEnum registerType) {
    if (!url.getParameter(REGISTER_KEY, true)) {
        registerType = RegisterTypeEnum.MANUAL_REGISTER;
    }
    if (registerType == RegisterTypeEnum.NEVER_REGISTER
            || registerType == RegisterTypeEnum.MANUAL_REGISTER
            || registerType == RegisterTypeEnum.AUTO_REGISTER_BY_DEPLOYER) {
        url = url.addParameter(REGISTER_KEY, false);
    }

    // 拿到代理调用组件Invoker
    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
    // 判断是否需要元数据
    if (withMetaData) {
        // 若需要,则将serviceConfig和invoker一同构造,得到新的invoker
        invoker = new DelegateProviderMetaDataInvoker(invoker, this);
    }
    Exporter<?> exporter = protocolSPI.export(invoker);
    exporters
            .computeIfAbsent(registerType, k -> new CopyOnWriteArrayList<>())
            .add(exporter);
}

local的export会走到这个方法,后续remote的export也会走到这里,这个方法主要是拿到这个service的invoker,然后走injvmProtocolexport得到发布成功后的exporter,并将其放入到exporters(这里可以猜测一下,后续会通过发布的exporter来进行调用,exportersMap<RegisterTypeEnum, List<Exporter<?>>>类型,可以看一下Exporter类,其中包含一个Invoker,然而,Invoker中又存在invoke()方法,是不是感觉很熟悉了),我们接着往下进行。

远程服务实例发布

随着调试,可以看到,代码直接来到了exportRemote()

java 复制代码
private URL exportRemote(URL url, List<URL> registryURLs, RegisterTypeEnum registerType) {
    if (CollectionUtils.isNotEmpty(registryURLs) && registerType != RegisterTypeEnum.NEVER_REGISTER) {
        for (URL registryURL : registryURLs) {
            if (SERVICE_REGISTRY_PROTOCOL.equals(registryURL.getProtocol())) {
                url = url.addParameterIfAbsent(SERVICE_NAME_MAPPING_KEY, "true");
            }

            // if protocol is only injvm ,not register
            if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
                continue;
            }

            url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
            URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);
            if (monitorUrl != null) {
                url = url.putAttribute(MONITOR_KEY, monitorUrl);
            }

            // For providers, this is used to enable custom proxy to generate invoker
            String proxy = url.getParameter(PROXY_KEY);
            if (StringUtils.isNotEmpty(proxy)) {
                registryURL = registryURL.addParameter(PROXY_KEY, proxy);
            }

            if (logger.isInfoEnabled()) {
                if (url.getParameter(REGISTER_KEY, true)) {
                    logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url
                            + " to registry " + registryURL.getAddress());
                } else {
                    logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
                }
            }
            // 走这里
            doExportUrl(registryURL.putAttribute(EXPORT_KEY, url), true, registerType);
        }

    } else {

        if (logger.isInfoEnabled()) {
            logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
        }

        doExportUrl(url, true, registerType);
    }

    return url;
}

这里可以看到,在对registryURL进行了一些处理后,调用了doExportUrl()方法,此处和前面exportLocal()中调用的是同一个,不同的是,这里是一个循环,会多次调用此方法,下面为了更好的理解,再放一下doExportUrl()的代码。

doExportUrl

java 复制代码
private void doExportUrl(URL url, boolean withMetaData, RegisterTypeEnum registerType) {
    if (!url.getParameter(REGISTER_KEY, true)) {
        registerType = RegisterTypeEnum.MANUAL_REGISTER;
    }
    if (registerType == RegisterTypeEnum.NEVER_REGISTER
            || registerType == RegisterTypeEnum.MANUAL_REGISTER
            || registerType == RegisterTypeEnum.AUTO_REGISTER_BY_DEPLOYER) {
        url = url.addParameter(REGISTER_KEY, false);
    }

    // 拿到代理调用组件Invoker
    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
    // 判断是否需要元数据
    if (withMetaData) {
        // 若需要,则将serviceConfig和invoker一同构造,得到新的invoker
        invoker = new DelegateProviderMetaDataInvoker(invoker, this);
    }
    Exporter<?> exporter = protocolSPI.export(invoker);
    exporters
            .computeIfAbsent(registerType, k -> new CopyOnWriteArrayList<>())
            .add(exporter);
}

此处在得到Invoker之后,和local不同的是,此处会进入到构造DelegateProviderMetaDataInvoker里去,将ServiceConfig塞进Invoker,后续会进入到RegistryProtocolexport()中。

org.apache.dubbo.registry.integration.RegistryProtocol#export

java 复制代码
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    URL registryUrl = getRegistryUrl(originInvoker);
    // url to export locally
    URL providerUrl = getProviderUrl(originInvoker);

    // Subscribe the override data
    // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call
    //  the same service. Because the subscribed is cached key with the name of the service, it causes the
    //  subscription information to cover.
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
    Map<URL, Set<NotifyListener>> overrideListeners =
            getProviderConfigurationListener(overrideSubscribeUrl).getOverrideListeners();
    overrideListeners
            .computeIfAbsent(overrideSubscribeUrl, k -> new ConcurrentHashSet<>())
            .add(overrideSubscribeListener);

    providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
    // export invoker
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    // url to registry
    final Registry registry = getRegistry(registryUrl);
    final URL registeredProviderUrl = customizeURL(providerUrl, registryUrl);

    // decide if we need to delay publish (provider itself and registry should both need to register)
    boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        register(registry, registeredProviderUrl);
    }

    // register stated url on provider model
    registerStatedUrl(registryUrl, registeredProviderUrl, register);

    exporter.setRegisterUrl(registeredProviderUrl);
    exporter.setSubscribeUrl(overrideSubscribeUrl);
    exporter.setNotifyListener(overrideSubscribeListener);
    exporter.setRegistered(register);

    ApplicationModel applicationModel = getApplicationModel(providerUrl.getScopeModel());
    if (applicationModel
            .modelEnvironment()
            .getConfiguration()
            .convert(Boolean.class, ENABLE_26X_CONFIGURATION_LISTEN, true)) {
        if (!registry.isServiceDiscovery()) {
            // Deprecated! Subscribe to override rules in 2.6.x or before.
            registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
        }
    }

    notifyExport(exporter);
    // Ensure that a new exporter instance is returned every time export
    return new DestroyableExporter<>(exporter);
}

在这个方法中,前半部分都是对url的一个处理,我们直接看一下doLocalExport()方法,我的理解是执行得到本地的exporter(和exportLocal中的exporter不同,前者是DubboExporter(外面有一层包装),后者是injvmExporter

doLocalExport()中核心代码如下

java 复制代码
ReferenceCountExporter<?> exporter =
        exporterFactory.createExporter(providerUrlKey, () -> protocol.export(invokerDelegate));

重点看下protocol.export(invokerDelegate),跟着调试,此处的protocolDubboProtocol,在org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#export方法中,着重看下openServer(url)方法中的createServer(url)方法。代码如下 org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#createServer

Netty Server启动

在这里会创建一个服务器用于远程通信,此处DEFAULT_REMOTING_SERVER为netty

java 复制代码
private ProtocolServer createServer(URL url) {
    url = URLBuilder.from(url)
            // send readonly event when server closes, it's enabled by default
            .addParameterIfAbsent(CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString())
            // enable heartbeat by default
            .addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT))
            .addParameter(CODEC_KEY, DubboCodec.NAME)
            .build();

    String transporter = url.getParameter(SERVER_KEY, DEFAULT_REMOTING_SERVER);
    if (StringUtils.isNotEmpty(transporter)
            && !url.getOrDefaultFrameworkModel()
                    .getExtensionLoader(Transporter.class)
                    .hasExtension(transporter)) {
        throw new RpcException("Unsupported server type: " + transporter + ", url: " + url);
    }

    ExchangeServer server;
    try {
        // 生成ExchangeServer
        server = Exchangers.bind(url, requestHandler);
    } catch (RemotingException e) {
        throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
    }

    // 网络传输
    transporter = url.getParameter(CLIENT_KEY);
    if (StringUtils.isNotEmpty(transporter)
            && !url.getOrDefaultFrameworkModel()
                    .getExtensionLoader(Transporter.class)
                    .hasExtension(transporter)) {
        throw new RpcException("Unsupported client type: " + transporter);
    }

    DubboProtocolServer protocolServer = new DubboProtocolServer(server);
    loadServerProperties(protocolServer);
    return protocolServer;
}

代码解析:具体创建流程在Exchangers.bind(url, requestHandler)中,一路调试通过org.apache.dubbo.remoting.exchange.support.header.HeaderExchanger#bind进入到org.apache.dubbo.remoting.transport.netty4.NettyTransporter#bind也就是netty4包下的bind方法,继续往下走,进入到org.apache.dubbo.remoting.transport.netty4.NettyServer#doOpen方法,也就是初始化和启动netty服务器的方法,到了这里终于看到庐山真面目了,前面一直在做一些配置化的处理和其他的一些工作。

java 复制代码
/**
 * Init and start netty server
 *
 * @throws Throwable
 */
@Override
protected void doOpen() throws Throwable {
    bootstrap = new ServerBootstrap();

    // boss是什么意思?负责对端口号监听是否有外来系统的连接请求,可以是一个event loop group
    bossGroup = createBossGroup();
    // 如果发现了网络请求,需要进行处理,workerGroup会并发处理这些请求
    workerGroup = createWorkerGroup();

    final NettyServerHandler nettyServerHandler = createNettyServerHandler();
    channels = nettyServerHandler.getChannels();

    initServerBootstrap(nettyServerHandler);

    // bind
    try {
        ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
        channelFuture.syncUninterruptibly();
        channel = channelFuture.channel();
    } catch (Throwable t) {
        closeBootstrap();
        throw t;
    }
}

在这里会创建一个EventLoopGroup类型的bossGroup来负责对端口号监听,是否有外来系统的网络连接请求,创建时默认是一个线程,并且会判断走epoll还是nio,代码如下。

java 复制代码
public static EventLoopGroup eventLoopGroup(int threads, String threadFactoryName) {
    ThreadFactory threadFactory = new DefaultThreadFactory(threadFactoryName, true);
    // epoll(),否则走nio,这部分属于网络的知识,可以复习一下
    return shouldEpoll()
            ? new EpollEventLoopGroup(threads, threadFactory)
            : new NioEventLoopGroup(threads, threadFactory);
}

接着创建了一个workerGroup,负责在发现了网络请求后,对这些请求进行处理,然后执行createNettyServerHandler(),内部逻辑是根据url和当前NettyServer对象创建一个NettyServerHandler,为channelsthe cache for alive worker channel.<ip:port, dubbo channel>)赋值,后续初始化服务器启动引导器。。。(中间相关bind流程可以具体调试下,这里不过多赘述)最后赋值到channel(接收连接并将其发送到工作通道的主通道)。至此完成NettyServer的创建。完成doOpen()后会打印[DUBBO] Start NettyServer bind /0.0.0.0:20880, export /10.1.68.219:20880, dubbo version: , current host: 10.1.68.219日志,标志着netty服务器启动

在zk创建服务的监听(订阅)

这里可以回到前面的org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#createServer方法,下面只放该方法还未提及的代码片段

java 复制代码
try {
    // 生成ExchangeServer(NettyServer)
    server = Exchangers.bind(url, requestHandler);
} catch (RemotingException e) {
    throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
}

// 网络传输
transporter = url.getParameter(CLIENT_KEY);
if (StringUtils.isNotEmpty(transporter)
        && !url.getOrDefaultFrameworkModel()
                .getExtensionLoader(Transporter.class)
                .hasExtension(transporter)) {
    throw new RpcException("Unsupported client type: " + transporter);
}

DubboProtocolServer protocolServer = new DubboProtocolServer(server);
loadServerProperties(protocolServer);
return protocolServer;

在生成了NettyServer后,封装成了ExchangeServerDubboProtocolServer,并加载了服务器配置,到此createServer就结束了。

后续回到org.apache.dubbo.registry.integration.RegistryProtocol#export,下面是该方法的后半部分

java 复制代码
// export invoker,此处是封装的DubboExporter
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

// 根据invoker的url获取注册表实例
final Registry registry = getRegistry(registryUrl);
// 获得注册到注册表的url
final URL registeredProviderUrl = customizeURL(providerUrl, registryUrl);

// 决定是否需要延迟发布 (provider itself and registry should both need to register)
boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);
if (register) {
    register(registry, registeredProviderUrl);
}

// 在providerModel上注册url
registerStatedUrl(registryUrl, registeredProviderUrl, register);

exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
exporter.setNotifyListener(overrideSubscribeListener);
exporter.setRegistered(register);

ApplicationModel applicationModel = getApplicationModel(providerUrl.getScopeModel());
if (applicationModel
        .modelEnvironment()
        .getConfiguration()
        .convert(Boolean.class, ENABLE_26X_CONFIGURATION_LISTEN, true)) {
    if (!registry.isServiceDiscovery()) {
        // Deprecated! Subscribe to override rules in 2.6.x or before.
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
    }
}
// 通知监听器监听服务注册
notifyExport(exporter);
// Ensure that a new exporter instance is returned every time export
return new DestroyableExporter<>(exporter);

代码解析:代码中的registeredProviderUrl及为要注册到注册表的url,ex:(dubbo://10.1.xx.xxx:20880/org.apache.dubbo.demo.DemoService?application=dubbo-demo-api-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&prefer.serialization=fastjson2,hessian2&service-name-mapping=true&side=provider&timestamp=1741399822964)。

在拿到exporter后,会根据invoker中的url获取注册表实例,当registryUrl是(service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=default&application=dubbo-demo-api-provider&dubbo=2.0.2&executor-management-mode=isolation&file-cache=true&pid=8204&register=false&registry=zookeeper&timestamp=1741399822328)时,获得ServiceDiscoveryRegistry,同时,在getRegistry时,会生成ZookeeperServiceDiscovery,并把它塞到ServiceDiscoveryRegistry中,在Dubbo3.2.x中,真正的注册逻辑并不是从这里走进来的(3.0.x的dubbo,从这个方法中可以直接走register流程)。调试一下可以发现providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);获取的register都是false,无法进入下面的register()流程。而是向expoter中填充了各种数据并通知了监听器。

registryUrl是(zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=default&application=dubbo-demo-api-provider&dubbo=2.0.2&executor-management-mode=isolation&file-cache=true&pid=22800&register=false&timestamp=1741401987928)时,获得ZookeeperRegistry,后续会走到subscribe流程,去Zookeeper中创建该url的监听,这块逻辑在org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe,可以看到里面有关于zkClient的一系列操作,最后会通知监听器。

至此,exportRemote()核心流程就结束了

那么,真正的服务注册流程在哪里呢?

回到org.apache.dubbo.config.deploy.DefaultModuleDeployer#startSync,关注下exportServices()结束的后续代码。

可以发现下面的方法

java 复制代码
// register services to registry
registerServices();

根据注释可知,注册服务到registry,那么没错,服务注册的代码逻辑就在这里了

服务注册

registerServices()

java 复制代码
private void registerServices() {
    for (ServiceConfigBase sc : configManager.getServices()) {
        if (!Boolean.FALSE.equals(sc.isRegister())) {
            registerServiceInternal(sc);
        }
    }
    applicationDeployer.refreshServiceInstance();
}

代码解析:从模块配置管理器拿到服务配置信息,第一个模块配置中并不存在services,直到第二个模块时,代码继续向下进行。

内部注册元数据

一路调试,走到org.apache.dubbo.registry.integration.RegistryProtocol.ExporterChangeableWrapper#register

register()

java 复制代码
@Override
public void register() {
    if (registered.compareAndSet(false, true)) {
        URL registryUrl = getRegistryUrl(originInvoker);
        Registry registry = getRegistry(registryUrl);
        RegistryProtocol.register(registry, getRegisterUrl());

        ProviderModel providerModel = frameworkModel
                .getServiceRepository()
                .lookupExportedService(getRegisterUrl().getServiceKey());

        List<ProviderModel.RegisterStatedURL> statedUrls = providerModel.getStatedUrl();
        statedUrls.stream()
                .filter(u -> u.getRegistryUrl().equals(registryUrl)
                        && u.getProviderUrl()
                                .getProtocol()
                                .equals(getRegisterUrl().getProtocol()))
                .forEach(u -> u.setRegistered(true));
        logger.info("Registered dubbo service " + getRegisterUrl().getServiceKey() + " url " + getRegisterUrl()
                + " to registry " + registryUrl);
    }
}

此时获取到的RegistryServiceDiscoveryRegistry,再向下就是将dubbo协议的url发布到registry了。

通过org.apache.dubbo.registry.integration.RegistryProtocol#register方法入参可以看出,要将registeredProviderUrl注册到Registry

java 复制代码
private static void register(Registry registry, URL registeredProviderUrl)

继续调试,可跳转到org.apache.dubbo.registry.client.AbstractServiceDiscovery#register(org.apache.dubbo.common.URL)

java 复制代码
@Override
public void register(URL url) {
    metadataInfo.addService(url);
}

可以看出,是对元数据进行填充,具体如下

java 复制代码
public synchronized void addService(URL url) {
    // fixme, pass in application mode context during initialization of MetadataInfo.
    if (this.loader == null) {
        this.loader = url.getOrDefaultApplicationModel().getExtensionLoader(MetadataParamsFilter.class);
    }
    List<MetadataParamsFilter> filters = loader.getActivateExtension(url, "params-filter");
    // generate service level metadata
    ServiceInfo serviceInfo = new ServiceInfo(url, filters);
    this.services.put(serviceInfo.getMatchKey(), serviceInfo);
    // extract common instance level params
    extractInstanceParams(url, filters);

    if (exportedServiceURLs == null) {
        exportedServiceURLs = new ConcurrentSkipListMap<>();
    }
    addURL(exportedServiceURLs, url);
    updated = true;
}

代码解析:创建了ServiceInfo对象,并将其放入到MetadataInfo,至此,内部将dubbo协议的url注册到元数据的逻辑就结束了,后续开始分析向zk进行注册的流程。

注册服务到Zookeeper

第二次走到org.apache.dubbo.registry.integration.RegistryProtocol.ExporterChangeableWrapper#register方法时,可以看到此时的注册地址为:

zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=default&application=dubbo-demo-api-provider&dubbo=2.0.2&executor-management-mode=isolation&file-cache=true&pid=9752&register=false&timestamp=1741420065263

并且RegistryZookeeperRegistry

继续向下调试,走到org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doRegister

java 复制代码
@Override
public void doRegister(URL url) {
    try {
        checkDestroyed();
        zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true), true);
    } catch (Throwable e) {
        throw new RpcException(
                "Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}

可以看出,此处开始有了对zkClient的操作,点进去代码如下

java 复制代码
@Override
public void create(String path, boolean ephemeral, boolean faultTolerant) {
    // 如果是持久节点
    if (!ephemeral) {
        // 如果持久节点路径缓存中存在这个path
        if (persistentExistNodePath.contains(path)) {
            return;
        }
        // 检查是否存在,不存在就add
        if (checkExists(path)) {
            persistentExistNodePath.add(path);
            return;
        }
    }
    int i = path.lastIndexOf('/');
    if (i > 0) {
        create(path.substring(0, i), false, true);
    }
    if (ephemeral) {
        // 创建临时节点
        createEphemeral(path, faultTolerant);
    } else {
        // 创建持久节点
        createPersistent(path, faultTolerant);
        persistentExistNodePath.add(path);
    }
}

代码解析:向zk中创建该dubbo协议的接口url,判断是持久节点还是临时节点,来进行不同的create,将对应服务的url在zk中完成创建,也就完成了服务的注册。

下面是整个流程的流程图

根据流程图总结一下整个服务Provider启动到Service发布的过程

首先通过ApplicationDeployer获取应用下的ModuleDeployer,使用ModuleDeployer进行初始化,初始化时,进行注册资源销毁钩子、启动ConfigCenter和MetadataCenter,其中MetadataCenter中有一个MetadataReport组件(该组件在export之后的exported方法中会用到,届时会使用MetadataReport将元数据注册到zk中,上述并没有提及这部分逻辑,感兴趣可以自己去看一下org.apache.dubbo.metadata.store.zookeeper.ZookeeperMetadataReport#registerServiceAppMapping方法); 随后,会刷新ServiceConfig,填充服务相关的一些配置信息(方法、参数等);再进行服务配置的初始化,主要是对ServiceMetadata进行设置。

之后开始服务的发布了,发布之前,还是进行必要数据的填充(包括要注册的service、providerModel),这些数据都存放在ModuleModel下的ModuleServiceRepository中,另外还要生成针对zk的注册地址。

后续得到要注册的dubbo协议的url,再将此url来进行本地服务实例发布和远程服务实例发布,不论是哪种发布方式,都要生成代理执行器invoker,若有外界请求该接口,后续可以通过invoker来invoke服务。本地服务实例发布结束后会得到一个InjvmExporter,远程服务实例发布会得到DubboExporter。

远程服务实例发布会涉及Netty服务器的启动,启动之后会进行服务的注册,首先会向zk订阅相关服务,在zk创建监听,然后开始内部的注册,通过生成ServiceInfo来进一步完善MetadataInfo;最后进行zk的服务注册,通过创建zk节点,将服务相关信息放入到zk注册中心中。

服务发布注册完毕之后,会通过MetadataReport将元数据进行上报到zk。

至此,整个流程就结束了。。。

后续我会继续更新dubbo源码解析,主要用于自己学习记录,也欢迎大家做补充和指正。

相关推荐
拾忆,想起1 天前
Dubbo RPC 实战全流程:从零搭建高可用微服务系统
网络·网络协议·微服务·性能优化·rpc·架构·dubbo
代码or搬砖2 天前
Nginx详讲
运维·nginx·dubbo
Gavin在路上2 天前
dubbo源码之微服务治理的“隐形遥控器”——QOS 机制解析
微服务·架构·dubbo
C182981825753 天前
Dubbo负载均衡实现原理
python·负载均衡·dubbo
廋到被风吹走3 天前
【Dubbo】接口特性与开发注意事项
dubbo
拾忆,想起3 天前
Dubbo vs Spring Cloud Gateway:本质剖析与全面对比指南
微服务·性能优化·架构·dubbo·safari
java_logo4 天前
LinuxServer.io LibreOffice 容器化部署指南
java·开发语言·docker·dubbo·openoffice·libreoffice·opensource
拾忆,想起4 天前
Dubbo多协议暴露完全指南:让一个服务同时支持多种通信方式
xml·微服务·性能优化·架构·dubbo
拾忆,想起5 天前
Dubbo服务调用幂等性深度解析:彻底解决重复请求的终极方案
微服务·性能优化·服务发现·dubbo
拾忆,想起5 天前
Dubbo深度解析:从零到一,高性能RPC框架如何重塑微服务架构
网络协议·微服务·云原生·性能优化·rpc·架构·dubbo