摘要

本博文将详细介绍Dubbo配置的设计模型、服务注册原理。

Dubbo——服务注册(Provider)原理与源码解析_spring

Dubbo——服务注册(Provider)原理与源码解析_ide_02

Dubbo前置知识

URL

不过在进行服务暴露流程分析之前有必要先谈一谈 URL,有人说这 URL 和 Dubbo 啥关系?有关系,有很大的关系!一般而言我们说的 URL 指的就是统一资源定位符,在网络上一般指代地址,本质上看其实就是一串包含特殊格式的字符串,标准格式如下:

protocol://username:password@host:port/path?key=value&key=value

Dubbo 就是采用 URL 的方式来作为约定的参数类型,被称为公共契约,就是我们都通过 URL 来交互,来交流。用了一个统一的契约之后,那么代码就更加的规范化、形成一种统一的格式,所有人对参数就一目了然,不用去揣测一些参数的格式等等。因此 Dubbo 用 URL 作为配置总线,贯穿整个体系,源码中 URL 的身影无处不在。

URL 具体的参数如下:

  • protocol:指的是 dubbo 中的各种协议,如:dubbo thrift http
  • username/password:用户名/密码
  • host/port:主机/端口
  • path:接口的名称
  • parameters:参数键值对

配置解析

目前Dubbo框架同时提供了 3种配置方式:XML配置、注解、属性文件(properties和ymal)配置,最常用的还是XML和注解两种方式。

基于XML配置原理解析

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<context:component-scan base-package="com.zhuangxiaoyan.dubbo.service.impl"></context:component-scan>


<dubbo:application name="order-service-consumer"></dubbo:application>

<dubbo:registry address="zookeeper://192.168.25.140:2181"></dubbo:registry>

<!-- 配置本地存根-->
<!--声明需要调用的远程服务的接口;生成远程服务代理 -->
<!--
1)、精确优先 (方法级优先,接口级次之,全局配置再次之)
2)、消费者设置优先(如果级别一样,则消费方优先,提供方次之)
-->
<!-- timeout="0" 默认是1000ms-->
<!-- retries="":重试次数,不包含第一次调用,0代表不重试-->
<!-- 幂等(设置重试次数)【查询、删除、修改】、非幂等(不能设置重试次数)【新增】 -->
<dubbo:reference interface="com.zhuangxiaoyan.dubbo.service.UserService"
id="userService" timeout="5000" retries="3" version="*">
<!-- <dubbo:method name="getUserAddressList" timeout="1000"></dubbo:method> -->
</dubbo:reference>

<!-- 配置当前消费者的统一规则:所有的服务都不检查 -->
<dubbo:consumer check="false" timeout="3000"></dubbo:consumer>

<dubbo:monitor protocol="registry"></dubbo:monitor>
<!-- <dubbo:monitor address="127.0.0.1:7070"></dubbo:monitor> -->

</beans>

Dubbo——服务注册(Provider)原理与源码解析_ide_03

基于注解配置原理解析

@EnableDubboConfig
@DubboComponentScan
public interface EnableDubbo {

}

@Import(DubboConfigConfigurationSelector.class) //EnableDubboConfig 注解
public ^interface EnableDubboConfig {

}

@Import(DubboComponentScanRegistrar.class)//DubboComponentScan 注解
public ginterface DubboComponentScan (

)

服务注册的原理

配置承载初始化

不管在服务暴露还是服务消费场景下,Dubbo框架都会根据优先级对配置信息做聚合处理,目前默认覆盖策略主要遵循以下几点规则:

  • -D 传递给 JVM 参数优先级最高,比如-Ddubbo. protocol.port=20880
  • 代码或XML配置优先级次高,比如Spring中XML文件指定<dubbo:protocol port=208807>
  • 配置文件优先级最低,比如 dubbo.properties 文件指定 dubbo.protocol.port=20880

一般推荐使用dubbo.properties作为默认值,只有XML没有配置时,dubbo.properties配置项才会生效,通常用于共享公共配置,比如应用名等。

Dubbo的配置也会受到provider的影响,这个属于运行期属性值影响,同样遵循以下几点规则:

  • 如果只有provider端指定配置,则会自动透传到客户端(比如timeout)
  • 如果客户端也配置了相应属性,则服务端配置会被覆盖(比如timeout)
  • 运行时属性随着框架特性可以动态添加,因此覆盖策略中包含的属性没办法全部列出来一般不允许透传的属性都会在ClusterUtils#mergellrl中进行特殊处理。

远程服务注册原理

远程服务注册的大致步骤:

  • 第一步是检测配置,如果有些配置空的话会默认创建,并且组装成 URL 。
  • 第二步是暴露服务,包括暴露到本地的服务和远程的服务。
  • 第三步是注册服务至注册中心。

Dubbo——服务注册(Provider)原理与源码解析_spring_04

从对象构建转换的角度分为两个步骤

Dubbo——服务注册(Provider)原理与源码解析_ide_05

服务暴露源码分析

这样就会在Spring IOC 容器刷新完成后调用 onApplicationEvent 方法,而这个方法里面做的就是服务暴露,这就是服务暴露的启动点。

//服务暴露的启动点
@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(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
//直接暴露
doExport();
}
//调用暴露方法
exported();
}
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();
bootstrap.setReady(true);
}

主要就是检查了一下配置,确认需要暴露的话就暴露服务,doExport 这个方法很长,不过都是一些检测配置的过程,虽说不可或缺不过不是我们关注的重点,我们重点关注里面的 doExportUrls 方法。

private void doExportUrls() {
//获取当前服务的注册中心,可以看到是个list,可以有多个服务中心
ServiceRepository repository = ApplicationModel.getServiceRepository();
ServiceDescriptor serviceDescriptor = repository.registerService(getInterfaceClass());
repository.registerProvider(
getUniqueServiceName(),
ref,
serviceDescriptor,
this,
serviceMetadata
);
//获取需要注册的地址的服务。
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);
doExportUrlsFor1Protocol(protocolConfig, registryURLs, protocolConfigNum);
}
}

可以看到 Dubbo 支持多注册中心,并且支持多个协议,一个服务如果有多个协议那么就都需要暴露,比如同时支持 dubbo 协议和 hessian 协议,那么需要将这个服务用两种协议分别向多个注册中心(如果有多个的话)暴露注册。

/**
* @description 生产者注册的主要函数
* @param: protocolConfig
* @param: registryURLs
* @param: protocolConfigNum
* @date: 2021/12/11 11:12
* @return: void
* @author: xjl
*/
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs, int protocolConfigNum) {
String name = protocolConfig.getName();
if (StringUtils.isEmpty(name)) {
name = DUBBO;
}

Map<String, String> map = new HashMap<String, String>();
map.put(SIDE_KEY, PROVIDER_SIDE);

ServiceConfig.appendRuntimeParameters(map);
AbstractConfig.appendParameters(map, getMetrics());
AbstractConfig.appendParameters(map, getApplication());
AbstractConfig.appendParameters(map, getModule());
// remove 'default.' prefix for configs from ProviderConfig
// appendParameters(map, provider, Constants.DEFAULT_KEY);
AbstractConfig.appendParameters(map, provider);
AbstractConfig.appendParameters(map, protocolConfig);
AbstractConfig.appendParameters(map, this);
MetadataReportConfig metadataReportConfig = getMetadataReportConfig();
if (metadataReportConfig != null && metadataReportConfig.isValid()) {
map.putIfAbsent(METADATA_KEY, REMOTE_METADATA_STORAGE_TYPE);
}
if (CollectionUtils.isNotEmpty(getMethods())) {
for (MethodConfig method : getMethods()) {
AbstractConfig.appendParameters(map, method, method.getName());
String retryKey = method.getName() + RETRY_SUFFIX;
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey);
if (FALSE_VALUE.equals(retryValue)) {
map.put(method.getName() + RETRIES_SUFFIX, ZERO_VALUE);
}
}
List<ArgumentConfig> arguments = method.getArguments();
if (CollectionUtils.isNotEmpty(arguments)) {
for (ArgumentConfig argument : arguments) {
// convert argument type
if (argument.getType() != null && argument.getType().length() > 0) {
Method[] methods = interfaceClass.getMethods();
// visit all methods
if (methods.length > 0) {
for (int i = 0; i < methods.length; i++) {
String methodName = methods[i].getName();
// target the method, and get its signature
if (methodName.equals(method.getName())) {
Class<?>[] argtypes = methods[i].getParameterTypes();
// one callback in the method
if (argument.getIndex() != -1) {
if (argtypes[argument.getIndex()].getName().equals(argument.getType())) {
AbstractConfig
.appendParameters(map, argument, method.getName() + "." + argument.getIndex());
} else {
throw new IllegalArgumentException(
"Argument config error : the index attribute and type attribute not match :index :" +
argument.getIndex() + ", type:" + argument.getType());
}
} else {
// multiple callbacks in the method
for (int j = 0; j < argtypes.length; j++) {
Class<?> argclazz = argtypes[j];
if (argclazz.getName().equals(argument.getType())) {
AbstractConfig.appendParameters(map, argument, method.getName() + "." + j);
if (argument.getIndex() != -1 && argument.getIndex() != j) {
throw new IllegalArgumentException(
"Argument config error : the index attribute and type attribute not match :index :" +
argument.getIndex() + ", type:" + argument.getType());
}
}
}
}
}
}
}
} else if (argument.getIndex() != -1) {
AbstractConfig.appendParameters(map, argument, method.getName() + "." + argument.getIndex());
} else {
throw new IllegalArgumentException(
"Argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>");
}

}
}
} // end of methods for
}

if (ProtocolUtils.isGeneric(generic)) {
map.put(GENERIC_KEY, generic);
map.put(METHODS_KEY, ANY_VALUE);
} else {
String revision = Version.getVersion(interfaceClass, version);
if (revision != null && revision.length() > 0) {
map.put(REVISION_KEY, revision);
}

String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
if (methods.length == 0) {
logger.warn("No method found in service interface " + interfaceClass.getName());
map.put(METHODS_KEY, ANY_VALUE);
} else {
map.put(METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
}
}

/**
* Here the token value configured by the provider is used to assign the value to ServiceConfig#token
*/
if (ConfigUtils.isEmpty(token) && provider != null) {
token = provider.getToken();
}

if (!ConfigUtils.isEmpty(token)) {
if (ConfigUtils.isDefault(token)) {
map.put(TOKEN_KEY, UUID.randomUUID().toString());
} else {
map.put(TOKEN_KEY, token);
}
}
//init serviceMetadata attachments
serviceMetadata.getAttachments().putAll(map);

// export service
String host = findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = findConfigedPorts(protocolConfig, name, map, protocolConfigNum);
URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);

// You can customize Configurator to append extra parameters
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}

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)) {
/**如果SCOPE_REMOTE 是null 那就会进行本地暴露*/
exportLocal(url);
}
/**如果配置不是本地的,则导出到远程(仅当配置为本地时才导出到本地)*/
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
if (CollectionUtils.isNotEmpty(registryURLs)) {
for (URL registryURL : registryURLs) {
if (SERVICE_REGISTRY_PROTOCOL.equals(registryURL.getProtocol())) {
url = url.addParameterIfAbsent(REGISTRY_TYPE_KEY, SERVICE_REGISTRY_TYPE);
}

//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) {
/**如果有监控中心haul则会在添加后想其汇报*/
url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString());
}
if (logger.isInfoEnabled()) {
if (url.getParameter(REGISTER_KEY, true)) {
logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " +
registryURL);
} else {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
}
/**对于提供者,这用于启用自定义代理以生成调用者*/
String proxy = url.getParameter(PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(PROXY_KEY, proxy);
}
/**拿到具体的类 进行装换 invoker*/
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass,
registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
/**进行包装 wrapperInvoker */
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
/**wrapperInvoker 转为 exporter */
Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
if (logger.isInfoEnabled()) {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
/** 自行一样的操作 只不过是直接进行暴露而已 */
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

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

MetadataUtils.publishServiceDefinition(url);
}
}
this.urls.add(url);
}

本地服务注册源码

exportLocal 方法,这个方法是本地暴露,走的是 injvm 协议,可以看到它搞了个新的 URL 修改了协议。

/**
* 本地暴露,走的是 injvm 协议
*/
private void exportLocal(URL url) {
URL local = URLBuilder.from(url)
.setProtocol(LOCAL_PROTOCOL)
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
Exporter<?> exporter = PROTOCOL.export(
PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}

Dubbo——服务注册(Provider)原理与源码解析_ide_06

Exporter<?> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

Dubbo——服务注册(Provider)原理与源码解析_ide_07

@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}

Dubbo——服务注册(Provider)原理与源码解析_XML_08

Protocol 的 export 方法是标注了 @ Adaptive 注解的,因此会生成代理类,然后代理类会根据 Invoker 里面的 URL 参数得知具体的协议,然后通过 Dubbo SPI 机制选择对应的实现类进行 export,而这个方法就会调用 InjvmProtocol#export 方法。

从图中可以看到实际上就是具体实现类层层封装, invoker 其实是由 Javassist 创建的。

远程服务注册源码

/**对于提供者,这用于启用自定义代理以生成调用者*/
String proxy = url.getParameter(PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(PROXY_KEY, proxy);
}
/**拿到具体的类 进行装换 invoker*/
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass,
registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
/**进行包装 wrapperInvoker */
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
/**wrapperInvoker 转为 exporter */
Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);

也和本地暴露一样,需要封装成 Invoker ,不过这里相对而言比较复杂一些。registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo://192.168.1.17:20880/com.alibaba.dubbo.demo.DemoService....
@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);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);

//export invoker这里就是进行前面说的dubbo:// 的暴露,并且还会打开端口等操作
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

//获取注册中心的URL 比如zookeeper://127.0.0.1:2181/ 根据URL的加载Registry的实现类 比如我们这里用的就是 ZookeeperRegistry
final Registry registry = getRegistry(originInvoker);
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);

// decide if we need to delay publish
boolean register = providerUrl.getParameter(REGISTER_KEY, true);
if (register) {
//如果需要注册 想注册中心注册服务
registry.register(registeredProviderUrl);
}

// 获取注册的服务提供者的URL,这里就是刚才我们说的 Dubbo://... .
registerStatedUrl(registryUrl, registeredProviderUrl, register);


exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);

//想注册中心订阅
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

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

可以看到这一步主要是将上面的 export=dubbo://... 先转换成 exporter ,然后获取注册中心的相关配置,如果需要注册则向注册中心注册,并且在 ProviderConsumerRegTable 这个表格中记录服务提供者,其实就是往一个 ConcurrentHashMap 中将塞入 invoker,key 就是服务接口全限定名,value 是一个 set,set 里面会存包装过的 invoker。

Dubbo——服务注册(Provider)原理与源码解析_ide_09

通过 Dubbo SPI 扫包会把 wrapper 结尾的类缓存起来,然后当加载具体实现类的时候会包装实现类,来实现 Dubbo 的 AOP,我们看到 DubboProtocol 有什么包装类。

Dubbo——服务注册(Provider)原理与源码解析_XML_10

 可以看到有两个,分别是 ProtocolFilterWrapper 和 ProtocolListenerWrapper。对于所有的 Protocol 实现类来说就是这么个调用链。

Dubbo——服务注册(Provider)原理与源码解析_XML_11

 而在 ProtocolFilterWrapper 的 export 里面就会把 invoker 组装上各种 Filter。

@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
if (UrlUtils.isRegistry(invoker.getUrl())) {
//如果是注册协议直接返回
return protocol.export(invoker);
}
// 不然就来一波过滤链组装
return protocol.export(buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER));
}
// 构建过滤链组装    
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
Invoker<T> last = invoker;
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);

if (!filters.isEmpty()) {
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
last = new FilterNode<T>(invoker, last, filter);
}
}

return last;
}

我们再来看下 zookeeper 里面现在是怎么样的,关注 dubbo 目录。

Dubbo——服务注册(Provider)原理与源码解析_Dubbo_12

两个 service 占用了两个目录,分别有 configurators 和 providers 文件夹,文件夹里面记录的就是 URL 的那一串,值是服务提供者 ip。

至此服务流程暴露差不多完结了,可以看到还是有点内容在里面的,并且还需要掌握 Dubbo SPI,不然有些点例如自适应什么的还是很难理解的。最后我再来一张完整的流程图带大家再过一遍。

Dubbo——服务注册(Provider)原理与源码解析_spring_13

展开总设计图左边服务提供方暴露服务的蓝色初始化链,时序图如下:

Dubbo——服务注册(Provider)原理与源码解析_spring_14

总的而言服务暴露的过程起始于 Spring IOC 容器刷新完成之时,具体的流程就是根据配置得到 URL,再利用 Dubbo SPI 机制根据 URL 的参数选择对应的实现类,实现扩展。

通过 javassist 动态封装 ref (你写的服务实现类),统一暴露出 Invoker 使得调用方便,屏蔽底层实现细节,然后封装成 exporter 存储起来,等待消费者的调用,并且会将 URL 注册到注册中心,使得消费者可以获取服务提供者的信息。

博文参考


​框架设计 | Apache Dubbo​