dubbo之服务暴露(ServiceConfig)

前言

dubbo的服务暴露和引用是dubbo使用过程中两个非常重要的环节,本篇先来探讨dubbo中服务的暴露流程。服务暴露对外提供可用服务,入口在ServiceConfig的export方法(暂不考虑spring生态),本文会以ServiceConfig.export方法为入口,分析服务暴露的整个过程。至于dubbo与spring生态的对接部分,后面会专门开篇来分析。我们来看官网对服务暴露的说明(以下内容摘自官网):

1、在没有注册中心,直接暴露提供者的情况下 1,ServiceConfig 解析出的 URL 的格式为:
 dubbo://service-host/com.foo.FooService?version=1.0.0。基于扩展点自适应机制,
通过 URL 的 dubbo:// 协议头识别,直接调用 DubboProtocol的 export() 方法,打开服务端口。
2、在有注册中心,需要注册提供者地址的情况下 ,ServiceConfig 解析出的 URL 的格式为:
 registry://registry-host/org.apache.dubbo.registry.RegistryService?export=
URL.encode("dubbo://service-host/com.foo.FooService?version=1.0.0"),基于扩展点
自适应机制,通过 URL 的 registry:// 协议头识别,就会调用 RegistryProtocol 的 
export() 方法,将 export 参数中的提供者 URL,先注册到注册中心。再重新传给 Protocol 
扩展点进行暴露: dubbo://service-host/com.foo.FooService?version=1.0.0,然后
基于扩展点自适应机制,通过提供者 URL 的 dubbo:// 协议头识别,就会调用 DubboProtocol
 的 export() 方法,打开服务端口。

也就是说,服务的暴露最终是调用Protocol.export方法,打开服务端口,供消费者使用。为了方便描述,本篇仅分析ServiceConfig至Procotol.export()的暴露流程,至于Procotol.export()的内部实现,会在后续文章中进行介绍。借用dubbo官方服务暴露的时序图,本篇将着重介绍分析的即红框中部分。


dubbo-export.jpg

服务暴露

不考虑对spring生态的依赖,dubbo服务的暴露逻辑(最简配置)如下(dubbo的必需配置:application):

ServiceConfig<DemoServiceImpl> serviceConfig = new ServiceConfig<DemoServiceImpl>();
service.setInterface(DemoService.class);
service.setRef(new DemoServiceImpl());
service.setApplication(app);
serviceConfig.export();

服务暴露的入口在serviceConfig.export方法,这里我把整个服务暴露过程分为两大步:加载、刷新服务配置,初始化服务暴露所需的各项配置;执行服务暴露,结果是缓存exporter和暴露的url,并返回exporter。后续的分析也按照这两步逐一进行。先看下服务暴露入口

public synchronized void export() {
    // 检查并刷新相关服务配置
    checkAndUpdateSubConfigs();
        
        // 已暴露过,则直接返回
    if (provider != null) {
        if (export == null) {
            export = provider.getExport();
        }
        if (delay == null) {
            delay = provider.getDelay();
        }
    }
    if (export != null && !export) {
        return;
    }
        
    //执行服务暴露或延迟执行服务暴露
    if (delay != null && delay > 0) {
        delayExportExecutor.schedule(this::doExport, delay, TimeUnit.MILLISECONDS);
    } else {
        doExport();
    }
}

一、加载、刷新服务配置

先来看配置的加载和刷新,有关dubbo配置相关请参考dubbo之Configuration,本文将只关注服务暴露过程中的配置加载与刷新。为方便管理服务相关配置,dubbo封装了一系列配置model及行为,我们先来看下相关model类之间的层次关系,为方便描述,服务引用、注册中心等所有配置model都会放在在一起解析。

dubbo-配置model (1).jpg

如上图所示,AbstractConfig、AbstractInterfaceConfig负责公共逻辑的处理,ServiceConfig、ReferenceConfig分用于服务的暴露和引用,逻辑比较复杂,除此之外(ServiceBean、ReferenceBean用于对接Spring生态,暂不作解析),其他config model都比较简单,大多是一些参数的getter、setter实现。

先来看AbstractConfig、AbstractInterfaceConfig中的公共逻辑,AbstractInterfaceConfig中的公共逻辑会在具体流程节点解析,先来看AbstratConfig。在AbstractConfig中,需要重点关注核心方法refresh(所有子类的刷新都依赖该方法)。refresh()的核心逻辑,利用复合配置CompositeConfiguration(初始化阶段,内置4个静态配置用于缓存配置加载阶段属性值),然后再依照静态配置优先级,读取静态配置中各属性值,通过反射刷新当前config字段。

public void refresh() {
    try {
        // 初始化复合配置,初始化内部各种静态配置;Environment可以理解为一个用于缓存Config的单例工具;
        // 只需关注getConfiguration()[1]即可,用于构建复合配置
        CompositeConfiguration compositeConfiguration =   
                                                    Environment.getInstance().getConfiguration(getPrefix(), getId());
        InmemoryConfiguration config = new InmemoryConfiguration(getPrefix(), getId());
        // 获取元数据并添加缓存config,getMetaData()[2]
        config.addProperties(getMetaData());
        // 默认配置中心配置优先
        if (Environment.getInstance().isConfigCenterFirst()) {
            // The sequence would be: SystemConfiguration -> ExternalConfiguration ->
            // AppExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
            // 添加的顺序决定了各配置的优先级顺序,原因可以查看对Configuration配置一文
            compositeConfiguration.addConfiguration(3, config);
        } else {
            // The sequence would be: SystemConfiguration -> AbstractConfig -> ExternalConfiguration ->
            // AppExternalConfiguration -> PropertiesConfiguration
            compositeConfiguration.addConfiguration(1, config);
        }

        // loop methods, get override value and set the new value back to method
        Method[] methods = getClass().getMethods();
        for (Method method : methods) {
            // 这里设计就牛逼了,遍历setter,内部调用extractPropertyName,将setter转getter,再根据getter上的注解,
            // 获取属性名称,最后再调用setter刷新属性值。
            if (ClassHelper.isSetter(method)) {
                try {
                        // 这里实质上调用的是CompositeConfiguration的getInternalProperty方法,
                    // 根据优先级从静态配置列表中取值。extractPropertyName,借助@Parameter注解,
                    // 解析属性名,优先取注解中的属性名称,找不到则直接取属性名称;比较简单
                    String value = compositeConfiguration.getString(extractPropertyName(getClass(), method));
                    // isTypeMatch() is called to avoid duplicate and incorrect update, 
                    // for example, we have two 'setGeneric' methods in ReferenceConfig.
                    if (StringUtils.isNotEmpty(value) && ClassHelper.isTypeMatch(method.getParameterTypes()
                        [0], value)) {
                        // 调用setter,设置属性值
                        method.invoke(this, ClassHelper.convertPrimitive(method.getParameterTypes()[0],
                                                    value));
                    }
                } catch (NoSuchMethodException e) {
                    logger.info("Failed to override the property " + method.getName() + " in " 
                                +this.getClass().getSimpleName() + ", please make sure every property has                                                                   getter/setter method provided.");
                }
            }
        }
        System.out.println("hello world!");
    } catch (Exception e) {
        logger.error("Failed to override ", e);
    }
}

// 按照顺序先来看Environment的getConfiguration[1]
// 获取配置组合,初始化 CompositeConfiguration;有意思,这里静态配置的唯一性由内部map保证。
public CompositeConfiguration getConfiguration(String prefix, String id) {
        CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
        // Config center has the highest priority
            // 均为静态配置,先从map缓存取,没有则创建
        compositeConfiguration.addConfiguration(this.getSystemConfig(prefix, id));
        compositeConfiguration.addConfiguration(this.getAppExternalConfig(prefix, id));
        compositeConfiguration.addConfiguration(this.getExternalConfig(prefix, id));
        compositeConfiguration.addConfiguration(this.getPropertiesConfig(prefix, id));
        return compositeConfiguration;
}

//getMetaData()[2],元数据
public Map<String, String> getMetaData() {
        Map<String, String> metaData = new HashMap<>();
        Method[] methods = this.getClass().getMethods();
        for (Method method : methods) {
            try {
                String name = method.getName();
                // 除去get、getClass、非共有方法、有参方法、返回值非基础类型的方法以外,其他方法认为是元数据方法
                if (isMetaMethod(method)) {
                    //从getter获取属性名成
                    String prop = calculateAttributeFromGetter(name);
                    String key;
                    Parameter parameter = method.getAnnotation(Parameter.class);
                    //先从parameter注解去,取不到则默认取从getter解析出的属性名
                    if (parameter != null && parameter.key().length() > 0 && parameter.useKeyAsProperty()) {
                        key = parameter.key();
                    } else {
                        key = prop;
                    }
                    // 返回值类型是Object,直接置null
                    if (method.getReturnType() == Object.class) {
                        metaData.put(key, null);
                        continue;
                    }
                    // 调用meta方法,并放入metaData
                    Object value = method.invoke(this);
                    String str = String.valueOf(value).trim();
                    if (value != null && str.length() > 0) {
                        metaData.put(key, str);
                    } else {
                        metaData.put(key, null);
                    }
                } else if ("getParameters".equals(name)
                        && Modifier.isPublic(method.getModifiers())
                        && method.getParameterTypes().length == 0
                        && method.getReturnType() == Map.class) {
                   //对getParameters单独处理,直接调用并全部放入metaData
                    Map<String, String> map = (Map<String, String>) method.invoke(this, new Object[0]);
                    if (map != null && map.size() > 0) {
                        for (Map.Entry<String, String> entry : map.entrySet()) {
                            metaData.put(entry.getKey().replace('-', '.'), entry.getValue());
                        }
                    }
                }
            } catch (Exception e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
        return metaData;
    }

整个refresh方法的核心逻辑到这里就分析完了,下面我们按照服务暴露过程,同时对配置逻辑进行分析。需要注意,在ServiceBean初始化过程中,会完成一部分config的加载(通过解析ServiceBean配置进行加载)。整个配置加载过程如下:

export-update-config (1).jpg

下面就按照加载流程依次进行解析。

1、加载混合配置

混合配置加载,其实根据ServiceBean的初始化结果,对ServiceConfig中未被初始化的属性进行补充。大体总结就是,根据已初始化的ProviderConfig、ApplicationConfig、ModuleConfig完善其他配置信息,直接来看代码

private void completeCompoundConfigs() {
    if (provider != null) {
        if (application == null) {
            setApplication(provider.getApplication());
        }
        if (module == null) {
            setModule(provider.getModule());
        }
        if (registries == null) {
            setRegistries(provider.getRegistries());
        }
        if (monitor == null) {
            setMonitor(provider.getMonitor());
        }
        if (protocols == null) {
            setProtocols(provider.getProtocols());
        }
        if (configCenter == null) {
            setConfigCenter(provider.getConfigCenter());
        }
    }
    if (module != null) {
        if (registries == null) {
            setRegistries(module.getRegistries());
        }
        if (monitor == null) {
            setMonitor(module.getMonitor());
        }
    }
    // regietryConfig和MonitorConfig最终以applicationConfig中的为准。
    if (application != null) {
        if (registries == null) {
            setRegistries(application.getRegistries());
        }
        if (monitor == null) {
            setMonitor(application.getMonitor());
        }
    }
}
2、启动配置中心

配置中心启动比较好理解,核心逻辑是初始化配置中心的配置、刷新所有配置。ConfigManager,对外表现为单例,用于管理所有核心Config,并用作部分Config的缓存,核心是一些getter、setter以及clear方法,比较简单,直接来看配置中心的启动逻辑。

void startConfigCenter() {
    // 借助ConfigManager初始化配置中心配置(ConfigCenterConfig)
    if (configCenter == null) {
        ConfigManager.getInstance().getConfigCenter().ifPresent(cc -> this.configCenter = cc);
    }

    if (this.configCenter != null) {
        // TODO there may have duplicate refresh
        // 配置中心刷新,这里调用父类方法,不解释
        this.configCenter.refresh();
        // 准备环境,逻辑比较简单,略过
        prepareEnvironment();
    }
    //刷新所有配置
    ConfigManager.getInstance().refreshAll();
}

// 来看ConfigManager.refreshAll方法
 public void refreshAll() {
     // refresh all configs here,刷新所有配置
     getApplication().ifPresent(ApplicationConfig::refresh);
     getMonitor().ifPresent(MonitorConfig::refresh);
     getModule().ifPresent(ModuleConfig::refresh);

     getProtocols().values().forEach(ProtocolConfig::refresh);
     getRegistries().values().forEach(RegistryConfig::refresh);
     getProviders().values().forEach(ProviderConfig::refresh);
     getConsumers().values().forEach(ConsumerConfig::refresh);
}
3、初始化ProviderConfig

检查Provider配置,没有则会先从ConfigManager缓存中取key为"default"的值,取不到则直接创建并刷新,然后放入ConfigManager的缓存,默认key是default(除非provider指定了id),代码比较简单,这里就省略了。

4、初始化ApplicationConfig

与Provider的初始化过程类似,也是先检查再创建(步骤与ProviderConfig类似,不再冗述)。除此之外,内部还对ApplicationModel应用名做了初始化和历史版本属性的兼容处理,比较简单。

5、初始化RegistryConfig

RegistryConfig的初始过程稍微复杂,分步骤做解析:

  1. 首先,为了兼容历史版本,解析"-Ddubbo.registry.address"参数值,并放入ConfigManager缓存;
  2. 若spring加载入口bean(ServiceBean、ReferenceBean等)过程中未完成registryIds、registries初始化,优先从Environment取registryIds用于初始化RegistryConfig并放入ConfigManager缓存,没有取到registryIds则尝试从ConfigManager缓存取,同样的,取不到则直接创建并写入ConfigManager缓存。
  3. 检查registries内配置有效性
  4. 兼容处理,若没有指定配置中心,而当前的注册协议是Zookeeper,那么会默认把当前的RegistryConfig的procotol、address信息刷入配置中心配置,并启动配置中心
6、初始化ProtocolConfig

逻辑相对简单,若当前protocols值为空,则优先从provider中取,并放入ConfigManager缓存。后续处理逻辑与RegistryConfig类似,优先从Environment取protocolIds用于初始化ProtocolConfig并放入ConfigManager缓存,没有取到protocolIds则继续尝试从ConfigManager缓存取,取不到则直接创建并写回ConfigManager缓存。

7、刷新ServiceConfig

ServiceConfig的刷新操作由父类AbstractConfig的refresh方法完成,前面已经介绍过了,这里直接略过。

8、初始化MetaDataReportConfig

同样比较简单,检查MetaDataReportConfig,并做初始化,然后放入ConfigManager缓存,最后refresh。

9、其他检查

初始化Generic服务标志、校验接口和方法、校验接口实现类、stub校验、mock校验,相对比较简单,就不过多解释了。

此时,所有配置都加载并初始化完毕(配置信息已经在相应config,同时在ConfigManager做了缓存),下一步需要根据已有配置信息,执行服务暴露。

二、执行服务暴露

服务暴露执行环节比较复杂,首先根据provider判断是否需要延迟暴露(通过ScheduleExcutorService实现),然后执行延迟暴露或者直接暴露,核心逻辑在doExport方法,直接来看代码。

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;
    }
    //构建providerModel,serviceName、serviceInstance、serviceInterface,以及服务提供的方法模型
    ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), ref, interfaceClass);
    // 向应用注册providerModel
    ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
    // 重头戏来了  
    doExportUrls();
}

private void doExportUrls() {
    // 加载registryConfig配置,并解析为URL
    List<URL> registryURLs = loadRegistries(true);
    for (ProtocolConfig protocolConfig : protocols) {
        // 真正执行服务的暴露
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}

上面的代码可以看到,服务暴露的核心逻辑在doExportUrlsFor1Protocol(),执行步骤如下:

  1. 获取host、port,将各config参数合并至map;

  2. 将1中的host、port、map组装成URL;

  3. 参照Configrurator,修正URL参数;

  4. 动态代理生成原始Invoker,这一步由ProxyFactory完成;

  5. 二次代理Invoker,将Invoker包装成DelegateProviderMetaDataInvoker;

  6. 执行Invoker的暴露,具体操作由Procotol完成;


    dubbo-service-export_exe.jpg

    过程如上图所示,下面我们按照步骤依次来进行分析:

首先来看第一步,参数合并会把前期配置准备阶段初始化后的配置信息,以及初始化后的config(ApplicationConfig、ProviderConfig、ModuleConfig、ProtocolConfig、ServiceConfig)中所有参数信息,合并至map;另外,服务host和端口的初始化也会在这一步完成,过程相对简单,重点关注取值优先级;其中,host的取值优先级,系统环境变量 -> java系统属性(-D参数) -> 配置文件中的host属性 -> /etc/hosts中的配置 -> 默认网络地址 -> 第一个可用的网络地址 -> 本地ip保底; port的取值优先级,系统环境变量 -> java系统属性(-D参数) -> ProtocolConfig中port值 -> Protocol中defaultPort(比如dubboProtocol中的20880)。下面直接来看代码

Map<String, String> map = new HashMap<String, String>();
map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE);
// 运行时参数合并至map
appendRuntimeParameters(map);
// applicationConfig配置参数合并至map
appendParameters(map, application);
// moduleConfig配置参数合并至map
appendParameters(map, module);
// providerConfig配置参数合并至map
appendParameters(map, provider, Constants.DEFAULT_KEY);
// protocolConfig配置参数合并至map
appendParameters(map, protocolConfig);
// service配置参数合并至map
appendParameters(map, this);
// ArgugmentConfig配置信息合并到map,代码比较长且逻辑比较简单,省略。其中,method中argumentConfig属性最终会以<methodName.index.xxx,xxx.value>

//中间夹杂对generic、version、methods、token的处理
if (ProtocolUtils.isGeneric(generic)) {
     map.put(Constants.GENERIC_KEY, generic);
     map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
} else {
     // 非通用方法,补充版本;版本取值逻辑比较简单,先从jar包的MANIFEST.MF文件读取,取不到则取jar包版本,由defaultVersion兜底。
     String revision = Version.getVersion(interfaceClass, version);
     if (revision != null && revision.length() > 0) {
           map.put("revision", revision);
     }

    //借助javaassit为接口生成代理,并获取方法列表;最终用于组装methods参数;这个方法比较有意思,下面会单独解析
    String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
    if (methods.length == 0) {
         logger.warn("No method found in service interface " + interfaceClass.getName());
         map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
    } else {
         map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
    }
}
// token,参数处理
if (!ConfigUtils.isEmpty(token)) {
   if (ConfigUtils.isDefault(token)) {
         //默认token是一个uuid
       map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
   } else {
       map.put(Constants.TOKEN_KEY, token);
   }
}

上面的代码中,Wrapper.getWrapper方法非常有意思,单独拉出来做解析。getWrapper主要用来生成服务接口的代理,逻辑相对简单:优先从Wrapper缓存取,缓存无值,则借助javassist动态生成Wrapper并放入缓存。重点关注生成Wrapper的方法makeWrapper:首先拼接Wrapper源码,然后通过ClassGenerator(借助javassist工具)生成Wrapper的Class实例,最后利用反射生成Wrapper实例;被代理接口的所有getter、setter汇总为两个方法,即对应Wrapper实例中的setProperty、getProperty方法,其他方法则统一于invokeMethod方法;篇幅关系,这里省略代码,有兴趣的同学可以自行查阅。参数合并部分到此结束。

URL的组装比较简单,直接通过上一步合并后的参数,创建url;注意,这一步创建的url,最终会作为registryUrl的参数。url组装完成后,接着通过SPI(ConfiguratorFactory接口)扩展,拿到对应Configurator,并对url中参数进行修正。

此时,url已经完全构建完毕,接下来,根据scope参数值来决定是否执行服务暴露、执行本地暴露还是远程暴露;若scope参数值不为none,则执行暴露;若scope参数值不为remote,执行本地暴露;若参数值不为local,则执行远程暴露。本地暴露与远程暴露逻辑相似,不同在于,本地暴露采用InjvmProtocol协议,端口为0,host为127.0.0.1,且少了加载monitor和二次代理的过程,其他逻辑与远程暴露一致。远程暴露过程中,dubbo会加载monitorConfig相关信息并组装成monitorUrl,作为参数存放于url,而后,url会作为registryUrl的参数,最终registryUrl作为服务暴露的主体。动态代理生成Invoker部分逻辑比较简单,直接来看代码(以javassistProxyFactory为例):

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    // Wrapper不支持对动态代理类的代理
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes,Object[] arguments)                             throws Throwable {
            // 实际执行wrapper的invokeMethod方法,即proxy方法名为methodName的方法。
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
}

接着,执行对代理Invoker的包装,最后通过Proctocol.export方法(在Protocol篇进行解析),完成服务的暴露,缓存exporter以及暴露过的url。

2.7.0版本以后,服务暴露完成后,dubbo新增了对provider元数据的处理,核心逻辑在MetadataReportService.publishProvider方法,简单了解一下。dubbo通过providerUrl中的信息,将元数据封装成FullServiceDefinition,然后通过MetadataReport接口的storeProviderMetadata方法(位于AbstractMetadataReport),下面来看方法逻辑:

public void storeProviderMetadata(MetadataIdentifier providerMetadataIdentifier, FullServiceDefinition serviceDefinition) {
    // url中sync.report参数值,是否同步上报
    if (syncReport) {
        storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition);
    } else {
        // 通过线程池,异步上报
        reportCacheExecutor.execute(new Runnable() {
            @Override
            public void run() {
                storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition);
            }
        });
    }
}

private void storeProviderMetadataTask(MetadataIdentifier providerMetadataIdentifier, FullServiceDefinition serviceDefinition) {
    try {
       if (logger.isInfoEnabled()) {
           logger.info("store provider metadata. Identifier : " + providerMetadataIdentifier + "; definition: " + serviceDefinition);
       }
       allMetadataReports.put(providerMetadataIdentifier, serviceDefinition);
       failedReports.remove(providerMetadataIdentifier);
       // Gson 序列化
       Gson gson = new Gson();
       String data = gson.toJson(serviceDefinition);
       // 具体逻辑由具体子类实现
       doStoreProviderMetadata(providerMetadataIdentifier, data);
       saveProperties(providerMetadataIdentifier, data, true, !syncReport);
   } catch (Exception e) {
        // 上报失败,进行重试,重试失败则抛异常
       failedReports.put(providerMetadataIdentifier, serviceDefinition);
       metadataReportRetry.startRetryTask();
       logger.error("Failed to put provider metadata " + providerMetadataIdentifier + " in  " + serviceDefinition + ", cause: " + e.getMessage(), e);
   }
}

其中,doStoreProviderMetadata由具体子类实现(RedisMetadataReport、ZookeeperMetadataReport),元数据信息保存在redis或者zookeeper,逻辑比较简单。

综上,本篇主要介绍了dubbo服务暴露流程在config层的处理,核心包括config的初始化和暴露执行两部分。config初始化过程,个人觉得作者参考了spring的refresh过程,代码比较容易理解。暴露执行分为本地暴露和远程暴露,本地暴露流程非常简单;远程暴露主要包括参数组合、组装URL、修正URL参数、生成代理Invoker、二次代理Invoker、通过protocol执行暴露6个核心步骤。通过protocol的export完成服务的最终暴露。

注:dubbo源码版本2.7.1,欢迎指正。

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

推荐阅读更多精彩内容