一、前言
本系列为个人Dubbo学习笔记,内容基于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章,仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。
系列文章地址:Dubbo源码分析:全集整理
在 Dubbo笔记④ : 服务发布流程 - doExportUrlsFor1Protocol 中介绍过服务发布时 Protocol 存在三种情况:
- 本地服务导出:其内部根据URL 中Protocol类型为 injvm,会选择InjvmProtocol
- 远程服务导出 & 有注册中心:其内部根据URL 中 Protocol 类型为 registry,会选择RegistryProtocol
- 远程服务导出 & 没有注册中心:根据服务协议头类型判断,我们这里假设是 dubbo ,则会选择 DubboProtocol
所以在本文中会介绍 RegistryProtocol#export 、 InjvmProtocol#export 、DubboProtocol#export 三个方法的执行过程。
这里我们可以看一下 Protocol#export 的流程图。其中 XxxProtocolWrapper 指的是QosProtocolWrapper、ProtocolListenerWrapper、ProtocolFilterWrapper。
由于 Dubbo 2.7 版本的节点监听方式和 Dubbo 2.6有所不同,在本文中注重介绍 Dubbo 2.6 版本的监听方式,对于 Dubbo 2.7 版本的监听方式,如有需要详参:Dubbo笔记衍生篇⑩:2.7 版本的动态配置监听
二、RegistryProtocol#export
当服务的暴露时有注册中心参与时,会调用 RegistryProtocol 来进行服务暴露。RegistryProtocol 可以认为并不是一个真正的协议,他是这些实际的协议(dubbo . rmi)包装者,这样客户端的请求在一开始如果没有服务端的信息,会先从注册中心拉取服务的注册信息,然后再和服务端直连。简单的说 RegistryProtocol 中完成的是与注册中心交互,而并非服务间的交互。
RegistryProtocol 通过 URL 的 registry://
协议头标识, DubboProtocol通过 URL 的dubbo://
协议头标识,在ServiceConfig或者RefrenceConfig中基于扩展点自适应机制会寻找对应的Protocol进行发布与引用
Registry类型Invoker不会被 Filter拦截,实际的协议Invoker才会被拦截, 因为ProtocolFilterWrapper 在构造 Dubbo Filter 时判断如果 URL 协议类型是 registry 则不构建过滤器链。如有需要,详看此文 :Dubbo笔记衍生篇③:ProtocolWrapper
下面以 Dubbo协议为例,ZK为注册中心,提供者服务暴露流程大致如下:
- 当服务进行发布时,会调用
Protocol#export
来进行服务暴露,需要注意的是由于 Dubbo SPI 机制此时的 Protocol 并非是某个具体的实现,而是Protocol$Adaptive
,Protocol$Adaptive
是 Dubbo SPI 动态生成的一个 Protocol 适配器类,会根据协议内容来选择合适的 Protocol 实现类。因为此次服务暴露以 ZK 作为注册中心,所以URL当前协议为registry。Protocol$Adaptive
根据registry://
获取到的对应的 Protocol 实现类 RegistryProtocol。但是由于由于Protocol 存在三个包装类QosProtocolWrapper
、ProtocolListenerWrapper
、ProtocolFilterWrapper
,Protocol$Adaptive
会先调用三个包装类,之后再调用 RegistryProtocol。 - Dubbo 提供了 Protocol 的自动包装类,这里会先调用其包装类(
QosProtocolWrapper
、ProtocolListenerWrapper
、ProtocolFilterWrapper
),三个包装类在协议为registry的时候并没有做什么处理(仅仅 QosProtocolWrapper完成了 Dubbo QOS 功能的处理)。 - 在包装类执行结束后,由于此时是ZK 作为注册中心,所以协议类型是registry,会选择使用RegistryProtocol作为处理类,因此此时会调用 RegistryProtocol#export()。
RegistryProtocol#export()
首先会解析URL信息,准备监听服务,在 2.7 版本的中RegistryProtocol#overrideUrlWithConfig 完成服务监听,之后开始交由DubboProtocol#export
进行服务导出。DubboProtocol#export
进行服务导出前仍会先经过ProtocolWrapper,此时由于协议类型不是 Registry而是Dubbo,所以包装类会生效。具体功能为ProtocolListenerWrapper增加了服务操作时候的监听功能, ProtocolFilterWrapper 完成了 Dubbo 拦截链的实现,即对Invoker进行了拦截链的包装。- 调用
DubboProtocol#export
,此时会开启Netty服务,并监听端口,经历了这一步,服务提供者的机器已经具备服务处理能力。 DubboProtocol#export
执行结束后,还需要将服务注册到注册中心的提供者节点,这一步是通过RegistryProtocol来完成,RegistryProtocol在zk上创建服务节点,同时对服务自身进行监听,当通过监控中心服务配置进行更新时,如果配置有变动将更新数据缓存重新发布服务。(消费者监听了提供者节点、配置节点、路由节点等,当服务重新发布时消费者可以感知到,并更新本地的提供者信息。)
即整个流程为 :
Protocol$Adaptive#export -> XXXProtocolWrapper#export -> RegistryProtocol#export
-> Protocol$Adaptive#export -> XXXProtocolWrapper#export -> DubboProtocol#export
关于ProtocolWrapper的内容我们在衍生篇中有过介绍,所以这里跳过 ProtocolWrapper,直接来看RegistryProtocol#export 的详细实现:
// 原始 URL (originInvoker.getUrl() ) :registry://localhost:2181/org.apache.dubbo.registry.RegistryService?application=Api-provider&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.111.1%3A20880%2Fapi.DemoService%3Fanyhost%3Dtrue%26application%3DApi-provider%26bind.ip%3D192.168.111.1%26bind.port%3D20880%26dubbo%3D2.0.2%26generic%3Dfalse%26group%3Ddubbo%26interface%3Dapi.DemoService%26methods%3DsayHelloForAsync%2CsayHello%2CsayMsg%2CsayHelloForAsyncByContext%26pid%3D11912%26register%3Dtrue%26release%3D2.7.0%26revision%3D1.0.0%26sayHello.retries%3D3%26side%3Dprovider%26timestamp%3D1616821309244%26version%3D1.0.0&pid=11912®istry=zookeeper&release=2.7.0×tamp=1616821309240
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
====================1. URL解析 ==================
// 1.1 获取注册中心信息 URL
URL registryUrl = getRegistryUrl(originInvoker);
// url to export locally
// 1.2 获取 需要暴露的 服务URL信息 : 通过 riginInvoker.getUrl().getParameterAndDecoded(EXPORT_KEY) 来获取
URL providerUrl = getProviderUrl(originInvoker);
// 1.3 订阅override数据
// 提供者订阅时,会影响 同一JVM即暴露服务,又引用同一服务的的场景,
// 因为subscribed以服务名为缓存的key,导致订阅信息覆盖。
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
// 根据 overrideSubscribeUrl 生成 OverrideListener,并缓存到 overrideListeners 中
// OverrideListener 是监听器实例
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 2.7 版本新增,使用 ServiceConfigurationListener 和 ProviderConfigurationListener 来监听服务
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
//export invoker
====================2. 服务暴露 ==================
// 进行服务暴露,这里会调用真正协议的 export 方法
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
====================3. 服务注册 ==================
// url to registry
// 获取注册中心实例 : 根据调用者的地址获取注册表的实例
final Registry registry = getRegistry(originInvoker);
// 获取 当前注册的服务提供者 URL,如果开启了简化 URL,则返回的是简化的URL
final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
// ProviderInvokerWrapper 中保存了当前注册服务的一些信息(originInvoker、registryUrl、registeredProviderUrl 以及是否注册)
ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
registryUrl, registeredProviderUrl);
//to judge if we need to delay publish
// 判断服务是否需要延迟发布,延迟发布则不会立即进行注册,否则进行注册
boolean register = registeredProviderUrl.getParameter("register", true);
if (register) {
// 调用远端注册中心的register方法进行服务注册
// 此时如果有消费者订阅了该服务,则推送消息让消费者引用此服务
// 注册中心缓存了所有提供者注册的服务以供消费者发现。
register(registryUrl, registeredProviderUrl);
// 将当前服务置为已注册
providerInvokerWrapper.setReg(true);
}
====================4. 服务订阅 ==================
// Deprecated! Subscribe to override rules in 2.6.x or before.
// 不推荐使用!订阅以覆盖2.6.x或更早版本中的规则。
// 提供者向注册中心订阅所有注册服务,当注册中心有此服务的覆盖配置注册进来时,推送消息给提供者,重新暴露服务,这由管理页面完成。
// org.apache.dubbo.registry.support.FailbackRegistry#subscribe
// 向注册中心进行订阅 override 数据
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
======================================
exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
//Ensure that a new exporter instance is returned every time export
// 保证每次export都返回一个新的exporter实例
// 返回暴露后的Exporter给上层ServiceConfig进行缓存,便于后期撤销暴露。
return new DestroyableExporter<>(exporter);
}
上面的逻辑大体可以分为三个部分:
- URL 解析:这一部分主要是对URL解析,获取关于注册中心,待发布服务等相关信息
- 服务暴露:当URL解析结束后,便开始进行服务暴露,此时RegistryProtocol 会调用真正的协议(默认是DubboProtocol)来进行服务的暴露,开启服务端口,绑定当前服务信息。
- 服务注册:经历上面两步后,服务已经暴露,但是并未在注册中心上注册,此时需要在注册中心上进行服务注册(比如此时会在zk上创建该服务的节点或者添加到提供者列表)。
- 服务订阅:服务发布成功后,除了依赖该服务的消费者,服务自身也会订阅自身服务。当服务进行通过动态配置时,会监听到该变化,并进行配置覆盖,重新发布服务,并通知其他消费者。
下面我们根据上面四个阶段进行详细解析:
1. URL解析
下面我们按照代码注释顺序来逐一分析
1.1 获取注册中心URL
该部分代码如下:
// 1.1 获取注册中心信息 URL
URL registryUrl = getRegistryUrl(originInvoker);
RegistryProtocol#getRegistryUrl 方法用来从 Invoker 中 获取注册中心 URL。其实现如下:
// org.apache.dubbo.registry.integration.RegistryProtocol#getRegistryUrl
private URL getRegistryUrl(Invoker<?> originInvoker) {
URL registryUrl = originInvoker.getUrl();
// 如果协议 是 register 则说明当前URL 保存的了 注册中心的信息,
// 通过 registryUrl.getParameter("registry") 来获取注册中心的类型(如redis、zookeeper。这里为zookeeper)并替换协议类型,返回注册中心的信息。
if (REGISTRY_PROTOCOL.equals(registryUrl.getProtocol())) {
String protocol = registryUrl.getParameter(REGISTRY_KEY, DEFAULT_DIRECTORY);
// 将 协议替换为 注册中心 真实协议。
registryUrl = registryUrl.setProtocol(protocol).removeParameter(REGISTRY_KEY);
}
return registryUrl;
}
在 Dubbo笔记 ③ : 服务发布流程 - ServiceConfig#export 一文 的 2.1. loadRegistries(true);
在加载注册中心的URL 时 会原始 URL 的协议类型从 zookeeper 替换成 registry,并且在URL 中添加 registry=zookeeper 用于保存注册中心的协议,而对于多种多样的注册中心(如 zk,nacos,redis), RegistryProtocol 会根据注册中心的实际协议类型来选择合适的 Registry 实现类来完成操作。
如下简化版URL :
registry://localhost:2181/org.apache.dubbo.registry.RegistryService?application=spring-dubbo-provider&dubbo=2.0.2&pid=10404®istry=zookeeper&release=2.7.0×tamp=1628842310884
registry://
可以让 Dubbo知晓这是一个注册中心 URL,会选择 RegistryProtocol 来处理流程registry=zookeeper
会让 Dubbo知道,当前服务选择使用zookeeper 协议来发布服务,即会选择 ZookeeperRegistry 来进行服务注册。
1.2 获取服务URL
该部分代码实现如下:
// 1.2 获取 需要暴露的 服务URL信息 : 通过 riginInvoker.getUrl().getParameterAndDecoded(EXPORT_KEY) 来获取
URL providerUrl = getProviderUrl(originInvoker);
RegistryProtocol#getProviderUrl 方法 用来获取需要暴露的服务信息,这里Dubbo笔记 ④ : 服务发布流程 - doExportUrlsFor1Protocol 的 2.2.1 生成代理类
章节 提及到,对于存在注册中心的情况,origin URL的结构一般为
registry://...&export=....&...
其中export 属性保存的则为暴露服务的URL。所以这里获取 URL 中的 “export” 属性来获取服务导出信息。下面获取的providerUrl 则表明该服务需要通过dubbo协议发布
// org.apache.dubbo.registry.integration.RegistryProtocol#getProviderUrl
// 获取服务提供者的URL 信息
private URL getProviderUrl(final Invoker<?> originInvoker) {
// 从原始URL 中获取 export 属性的value,这里保存的是需要导出的服务信息
String export = originInvoker.getUrl().getParameterAndDecoded(EXPORT_KEY);
if (export == null || export.length() == 0) {
throw new IllegalArgumentException("The registry export url is null! registry: " + originInvoker.getUrl());
}
// String 转化为 URL
return URL.valueOf(export);
}
1.3 获取订阅URL
该部分代码实现如下:
// 生成了服务提供者监听的 URL 信息
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
// 给当前 Invoker 注册监听器。当有信息更新时进行通知
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
// 2.7 版本新增,保存 URL 和监听器的映射关系,当 ProviderConfigurationListener 监听触发时通知所有监听器
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// 获取动态配置信息进行合并
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
这里主要是进行 URL 的准备工作,包括
- 获取监听信息生成URL overrideSubscribeUrl。
- 创建当前服务的监听器 overrideSubscribeListener,用于监听overrideSubscribeUrl,在 4. 服务订阅 中完成了服务订阅,该服务订阅为 2.6 版本的订阅方式。 Dubbo 为了兼容性保留了 2.6 版本的监听方式。
- RegistryProtocol#overrideUrlWithConfig 是 2.7版本新增逻辑,完成了2.7版本的动态配置获取和监听。
这里我们注意:
-
RegistryProtocol#getSubscribedOverrideUrl 的实现如下,该方法生成了服务提供者监听的 URL 信息:
private URL getSubscribedOverrideUrl(URL registeredProviderUrl) { // 1. protocol 修改为 provider。表明当前服务监听的是应用于服务提供者端的配置(消费者端则为 consumer); // 2. 添加 category configurators。声明监听的节点为 configurators 节点(消费者还会监听 routers、provider 等节点)。 return registeredProviderUrl.setProtocol(PROVIDER_PROTOCOL) .addParameters(CATEGORY_KEY, CONFIGURATORS_CATEGORY, CHECK_KEY, String.valueOf(false)); }
Dubbo提供了动态配置的功能,当服务发布后,用户可以通过管理中心来进行动态配置,提供者可以感知这些配置的变化并更新到本地,如果需要将服务合并配置后重新发布。如当我们使用dubbo-admin 进行动态配置时,dubbo-admin 会在注册中心或配置中心创建配置节点(Dubbo 2.6 没有配置中心,会在注册中心上创建,而 Dubbo 2.7 则是在配置中心上创建),提供者只需要监听这些节点的变化即可。监听的首要工作是生成一个 URL 代表监听的节点信息,即本步的工作。
-
RegistryProtocol#overrideUrlWithConfig 的实现如下, 该方法完成了配置同步 并且添加了服务监听。
private URL overrideUrlWithConfig(URL providerUrl, OverrideListener listener) { // 1. 获取应用级别现有配置规则,并在导出之前覆盖提供程序URL。 providerUrl = providerConfigurationListener.overrideUrl(providerUrl); // 2. 初始化服务级别监听器,并保存到 serviceConfigurationListeners 中,完成了对当前服务的监听。 ServiceConfigurationListener serviceConfigurationListener = new ServiceConfigurationListener(providerUrl, listener); serviceConfigurationListeners.put(providerUrl.getServiceKey(), serviceConfigurationListener); // 3. 获取服务级别现有配置规则,并在导出之前覆盖提供程序URL。 return serviceConfigurationListener.overrideUrl(providerUrl); }
需要注意,这里存在两个监听器 ServiceConfigurationListener 和 ProviderConfigurationListener ,这两个监听器 是 Dubbo 2.7 之后增加,其功能如下:
- ProviderConfigurationListener : 应用级别监听器,因为一个程序只能有一个应用名,所以这里之需要一个 ProviderConfigurationListener 实例即可。
- ServiceConfigurationListener :服务级别监听器。因为一个程序可以提供多个 Dubbo 接口服务,所以这使用
serviceConfigurationListeners
来保存不同接口的监听器。
关于介绍,使用哪一篇,配置中心还是监听方式
2. 服务暴露
在上面对 URL 处理结束后,我们获取到了三个URL
- registryUrl :注册中心 URL
- providerUrl :暴露服务的URL
- overrideSubscribeUrl :服务监听节点 URL
接下来便开始进行服务暴露,其暴露是通过 doLocalExport(originInvoker, providerUrl);
方法实现的,这里会调用 DubboProtocol#export (我们使用 Dubbo协议)来进行真正的服务暴露。其具体实现如下:
private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
// 将 originInvoker 转换为 缓存key,从 bounds 缓存中尝试获取
String key = getCacheKey(originInvoker);
ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
// 如果当前服务没发布过,则开始创建
if (exporter == null) {
synchronized (bounds) {
// DCL 双端检索机制
exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
if (exporter == null) {
// Invoker 委托
final Invoker<?> invokerDelegete = new InvokerDelegate<T>(originInvoker, providerUrl);
// Invoker 转换为 Exporter,
// 这里注意: protocol.export(invokerDelegete) 中 protocol 在 RegistryProtocol 在创建的时候依赖注入的,其实现还是Protocol适配器
exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
bounds.put(key, exporter);
}
}
}
return exporter;
}
上面可以看到,如果当前服务并没有暴露过(即缓存中不存在),则会通过protocol.export(invokerDelegete)
进行服务暴露。因为在 RegistryProtocol 通过 Dubbo SPI 初始化时存在 扩展点之间的依赖注入
的功能(SPI 的依赖注入即:即当一个 SPI 的实例进行创建时,如果其内部存在某个 SPI 接口的 set 方法,则会被反射注入。这部分内容Dubbo笔记衍生篇②:Dubbo SPI 原理 的 2.2. injectExtension
章节中有过分析), 所以此时的 protocol是 Protocol$Adaptive 类型,所以,此时会再走一遍下面的流程:
Protocol$Adaptive#export() => QosProtocolWrapper#export() => ProtocolListenerWrapper #export() => ProtocolFilterWrapper#export() => XxxProtocol#export()
即以Dubbo协议类型为例,整个流程大致如下:
这里需要注意(在Dubbo笔记衍生篇③:ProtocolWrapper 中有过详细介绍):
- 在第一次执行包装流程时,由于协议类型是Registry,所以包装类并没有进行任何干涉流程的处理,而是直接将请求透传。
- 在第二次执行包装流程时由于协议类型是Dubbo才会真正进行包装过程。
根据上面的流程图,在第二次包装结束后,则会调用 DubboProtocol#export 方法,该部分我们在下文讲解。
3. 服务注册
当上一步执行结束后,此时的服务已经开启,接下来我们需要在注册中心上保存相应的信息,如zk会建立相应的节点。下面我们来看一下服务注册相关的代码:
// url to registry
// 获取注册中心实例 ,通过 RegistryFactory 来获取,也是 SPI 接口。
// 我们是用zk作为注册中心,所以这里获取的实例是 ZookeeperRegistry
final Registry registry = getRegistry(originInvoker);
// 获取 当前注册的服务提供者 URL,如果开启了简化 URL,则返回的是简化的URL
final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
// ProviderInvokerWrapper 中保存了当前注册服务的一些信息(originInvoker、registryUrl、registeredProviderUrl 以及是否注册)
// 如当服务重新发布时就会通过 ProviderInvokerWrapper 判断当前服务是否已经注册
ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
registryUrl, registeredProviderUrl);
//to judge if we need to delay publish
// 判断服务是否需要延迟发布,延迟发布则不会立即进行注册,否则进行注册
boolean register = registeredProviderUrl.getParameter("register", true);
if (register) {
// 调用远端注册中心的register方法进行服务注册
// 此时如果有消费者订阅了该服务,则推送消息让消费者引用此服务
// 注册中心缓存了所有提供者注册的服务以供消费者发现。
register(registryUrl, registeredProviderUrl);
// 将当前服务置为已注册。当服务重新发布时会判断服务是否已经注册,已经注册的服务才能重新发布
providerInvokerWrapper.setReg(true);
}
我们主要关注 register(registryUrl, registeredProviderUrl);
方法: 该方法完成了服务的注册,即解析出当前URL中需要暴露的服务,并在ZK 上创建服务节点。其实现如下:
public void register(URL registryUrl, URL registeredProviderUrl) {
// SPI 方式获取 Registry 实例,这里获取的是 ZookeeperRegistry
Registry registry = registryFactory.getRegistry(registryUrl);
registry.register(registeredProviderUrl);
}
这里默认实现 Registry 的默认实现是 ZookeeperRegistry,即这里调用的是 ZookeeperRegistry#register
(FailbackRegistry#register
)方法,下面我们来看看其详细实现:
// org.apache.dubbo.registry.support.FailbackRegistry#register
@Override
public void register(URL url) {
// org.apache.dubbo.registry.support.AbstractRegistry#register
// 将 url添加到 org.apache.dubbo.registry.support.AbstractRegistry#registered 中
super.register(url);
// 开始注册,从失败列表删除
removeFailedRegistered(url);
removeFailedUnregistered(url);
try {
// Sending a registration request to the server side
// 这里调用 org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doRegister
doRegister(url);
} catch (Exception e) {
Throwable t = e;
// ... 失败处理
// 添加到注册失败列表,并定期重试
addFailedRegistered(url);
}
}
这里的逻辑还是比较清楚,我们这里关注 doRegister(url)
,其具体实现 ZookeeperRegistry#doRegister
的如下 :
// org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doRegister
@Override
public void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
可以看到,这里直接调用了 zkClient#create
开始创建了 zk 的节点信息。 (这里的ZkClient 是Dubbo的实现类,有CuratorZookeeperClient
和 ZkclientZookeeperClient
,默认ZkclientZookeeperClient
)
关于 zkClient.create
通过递归的方式创建了节点,详细实现这里就不再追溯。这里以com.kingfish.service.impl.DemoService
服务为例,其最后的创建结构如图(其中URL中并非仅仅IP端口信息,这里仅为了方便描述,此时还没有创建configurators 节点,configurators 节点在下一步才创建):
zk中的实际结构如下:
至此,RegistryProtocol#doLocalExport 开启了服务,这一步在zk 上创建了该服务的节点,也即是说说消费者已经可以通过zk获取到当前服务作为提供者,并进行服务调用。
4. 服务订阅
在此之前,服务已经完成了服务端口绑定和服务注册的功能,此时的服务已经可以对外提供功能。但是有些时候我们希望可以动态配置一些参数,而 Dubbo提供了控制台功能,我们可以动态的配置一些参数,提供者和消费者可以即时感知并做出响应的处理。该功能则是依赖于此处的服务订阅功能,Dubbo 服务在启动后,无论是消费者还是提供者都会监听部分节点信息(在zk上映射的节点(服务提供者为 configurators 节点,服务消费者为providers,configurators、routers 节点),当 控制台修改动态配置时会修改配置节点的信息,服务通过监听器感知到节点变化后自身更新配置。
需要注意的是: 这一步的是 Dubbo 2.6 版本的逻辑,Dubbo 2.7 以下版本为了兼容并未删除该部分逻辑, 在 Dubbo 2.7 及以上版本, 则是通过 ServiceConfigurationListener 来监听动态配置的节点。关于 2.7 版本的监听方式,如有需要详参: Dubbo笔记衍生篇⑩:2.7 版本的动态配置监听
下面我们来看详细过程,服务订阅我们只关注下面的代码:
// 这里的 registry 实现是 ZookeeperRegistry
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
我们先看一下 ZookeeperRegistry#subscribe 的两个入参:
-
overrideSubscribeUrl : 代表需要订阅的URL 节点,当该URL所对应的节点发生变化时,会通知当前服务。其简化结构如下,其中 category 代表要监听的节点:
provider://192.168.111.1:9998/com.kingfish.service.impl.DemoService?&category=configurators
-
overrideSubscribeListener : 当监听的节点发生变化时,会回调该监听器。
ZookeeperRegistry#subscribe 的具体 实现如下:
// org.apache.dubbo.registry.support.FailbackRegistry#subscribe
@Override
public void subscribe(URL url, NotifyListener listener) {
// 调用 org.apache.dubbo.registry.support.AbstractRegistry#subscribe
// 将 url 和 listener 添加到 org.apache.dubbo.registry.support.AbstractRegistry#subscribed (ConcurrentMap<URL, Set<NotifyListener>> subscribed)中
super.subscribe(url, listener);
// 移除订阅失败的url
removeFailedSubscribed(url, listener);
try {
// Sending a subscription request to the server side
// 进行订阅,这里调用的是 org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doSubscribe
// 这一步结束后,zk 上在服务下多个一个 configurators节点
// 如 : /dubbo/com.kingfish.service.impl.DemoService/configurators
doSubscribe(url, listener);
} catch (Exception e) {
// ... 异常处理
// Record a failed registration request to a failed list, retry regularly
// 添加到订阅失败列表,定时重试
addFailedSubscribed(url, listener);
}
}
关键逻辑在 doSubscribe(url, listener);
中,其具体实现在ZookeeperRegistry#doSubscribe
中。
4.1 ZookeeperRegistry#doSubscribe
ZookeeperRegistry#doSubscribe
的实现如下:
由于我们这里指定了ServiceInterface
为 com.kingfish.service.impl.DemoService
,所以会走else 分支,其实两个分支实现也基本相同。
@Override
public void doSubscribe(final URL url, final NotifyListener listener) {
try {
// 1. 如果url 指定的服务接口为 *, 则监听所有,直接监听root 节点 ,即zk 中的 dubbo 节点
if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
// 获取 root 节点(dubbo)
String root = toRootPath();
// 初始化监听器集合
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) {
zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
listeners = zkListeners.get(url);
}
ChildListener zkListener = listeners.get(listener);
if (zkListener == null) {
// 添加节点监听器,这里和else分支不同
listeners.putIfAbsent(listener, new ChildListener() {
@Override
public void childChanged(String parentPath, List<String> currentChilds) {
// 如果有节点更新,则使用anyServices进行缓存,并进行服务订阅
for (String child : currentChilds) {
child = URL.decode(child);
if (!anyServices.contains(child)) {
anyServices.add(child);
subscribe(url.setPath(child).addParameters(Constants.INTERFACE_KEY, child,
Constants.CHECK_KEY, String.valueOf(false)), listener);
}
}
}
});
zkListener = listeners.get(listener);
}
// 在zk 上创建dubbo节点
zkClient.create(root, false);
// 监听root节点,监听器为zkListener
List<String> services = zkClient.addChildListener(root, zkListener);
if (services != null && !services.isEmpty()) {
for (String service : services) {
service = URL.decode(service);
anyServices.add(service);
subscribe(url.setPath(service).addParameters(Constants.INTERFACE_KEY, service,
Constants.CHECK_KEY, String.valueOf(false)), listener);
}
}
} else {
List<URL> urls = new ArrayList<URL>();
// 2. 将 URL 分类遍历
// toCategoriesPath 是获取分类路径
// 如 :/dubbo/com.kingfish.service.impl.DemoService/configurators、/dubbo/com.kingfish.service.impl.DemoService/providers、/dubbo/com.kingfish.service.impl.DemoService/routers
// 这里获取的是 /dubbo/com.kingfish.service.impl.DemoService/configurators
for (String path : toCategoriesPath(url)) {
// 3. 监听器设置
// 获取当前 URL 的监听器Map
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) {
// 如果当前 URl还没有监听器,则创建一个 Map
zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
listeners = zkListeners.get(url);
}
// 获取当前 NotifyListener 对应的节点监听器
ChildListener zkListener = listeners.get(listener);
if (zkListener == null) {
// 如果没有NotifyListener 对应的 ChildListener ,则添加一个
// 这里设置了 ChildListener 的回调方法
listeners.putIfAbsent(listener, new ChildListener() {
@Override
public void childChanged(String parentPath, List<String> currentChilds) {
// 设置回调
ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
}
});
// 获取当前 NotifyListener 对应的 节点监听器
zkListener = listeners.get(listener);
}
// 创建/dubbo/com.kingfish.service.impl.DemoService/configurators 节点
zkClient.create(path, false);
// 添加节点监听器。使用 zkListener 监听 path,返回值为 path 的子节点
List<String> children = zkClient.addChildListener(path, zkListener);
if (children != null) {
// 将 children 转换为 url,其中 toUrlsWithEmpty是为了避免出现 null,如果无法转换,则返回一个 empty:// 协议头的 URL
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
// 主动调用通知事件
notify(url, listener, urls);
}
} catch (Throwable e) {
throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
我们按照代码注释顺序开始分析:
4.1.1 根节点的监听
如果url 指定的服务接口为 *
, 则监听所有,直接监听root 节点 ,即zk 中的 dubbo 节点。我们一般会具体监听到某个服务,所以不会走到这里。
4.1.2 URL 分类遍历
这里会调用 ZookeeperRegistry#toCategoriesPath 对 URL 按照类别进行划分。ZookeeperRegistry#toCategoriesPath 的实现如下:
private String[] toCategoriesPath(URL url) {
String[] categories;
// 如果 url 中的 category 属性为 *,则认为当前 URL 监听所有分类的节点。
// 包括providers、consumers、routers、configurators
if (Constants.ANY_VALUE.equals(url.getParameter(Constants.CATEGORY_KEY))) {
categories = new String[]{Constants.PROVIDERS_CATEGORY, Constants.CONSUMERS_CATEGORY,
Constants.ROUTERS_CATEGORY, Constants.CONFIGURATORS_CATEGORY};
} else {
// 否认则获取category 属性,默认为 providers
categories = url.getParameter(Constants.CATEGORY_KEY, new String[]{Constants.DEFAULT_CATEGORY});
}
// 对 categories 进一步处理,拼接上 servicepath
String[] paths = new String[categories.length];
for (int i = 0; i < categories.length; i++) {
paths[i] = toServicePath(url) + Constants.PATH_SEPARATOR + categories[i];
}
return paths;
}
我们这里以消费者 URL举例:
consumer://192.168.111.1:9998/com.kingfish.service.impl.DemoService?&category=providers,configurators,routers
则这里会被划分为下面三个节点,在后面的流程,消费者会监听这三个节点的变化:
// 提供者列表
/dubbo/com.kingfish.service.impl.DemoService/providers
// 动态配置信息
/dubbo/com.kingfish.service.impl.DemoService/configurators
// 路由信息
/dubbo/com.kingfish.service.impl.DemoService/routers
同理,此时的提供者的URL 结构为:
provider://192.168.111.1:9998/com.kingfish.service.impl.DemoService?&category=configurators
注:提供者只会监听 /dubbo/com.kingfish.service.impl.DemoService/configurators
节点 ,因为对于提供者来说只需要关心动态配置的内容,并不需要关注有多个提供者提供服务和路由策略。
所以 ZookeeperRegistry#toCategoriesPath 的返回结果 path 为 /dubbo/com.kingfish.service.impl.DemoService/configurators
,服务提供者将监听该节点。
4.1.3 监听器的设置
上面的代码注释比较清楚:
- 通过
zkListeners.get(url)
判断是否有监听器监听当前URL,如果没有,则创建一个空的Map。 - 通过
listeners.get(listener)
判断是否有使用当前 listener 作为监听器监听当前URL。如果没有则创建一个对应的监听。添加的监听器会在节点更新时调用ZookeeperRegistry#notify
。 - 通过
zkClient.create(path, false);
在zk 上创建对应的监听节点。 - 通过
zkClient.addChildListener(path, zkListener)
, 添加该节点的监听,此时的path 正是4.1.2 URL 分类遍历
中分类后的节点路径。 - 通过
notify(url, listener, urls);
主动调用通知事件。
我们这里看一下 zkListeners 的定义如下:
private final ConcurrentMap<URL, ConcurrentMap<NotifyListener, ChildListener>> zkListeners = new ConcurrentHashMap<URL, ConcurrentMap<NotifyListener, ChildListener>>();
一个 URL 可以被多个 NotifyListener监听,但是一个 URL 可以划分为多个分类,每个分类都需要一个 ChildListener 来监听节点变化,并且需要有一个 NotifyListener 来执行节点变化后的操作。如下URL,我们指定 Consumer NotifyListener 来监听 该URL:
URL = consumer://192.168.111.1:9998/com.kingfish.service.impl.DemoService?&category=providers,configurators,routers
该 URL 会被划分为 如下三个类型,也就是说我们需要监听下面三个节点,此时会设置三个 ChildListener 来监听,当节点变化时会触发操作:即交由当前指定的 Consumer NotifyListener来处理 :
/dubbo/com.kingfish.service.impl.DemoService/providers
/dubbo/com.kingfish.service.impl.DemoService/configurators
/dubbo/com.kingfish.service.impl.DemoService/routers
到这里,我们分析了服务订阅的整个流程,一言蔽之:服务提供者会订阅 zk 上的 /dubbo/com.kingfish.service.impl.DemoService/configurators
节点,当节点更新时会触发监听器的监听方法。
但是我们尚未分析Dubbo在感知到节点更新后做了何种处理。下面我们一起来分析。
4.2 ZookeeperRegistry#notify
在上面的代码我们可以看到,我们知道,节点更新时会调用 ZookeeperRegistry#notify 来处理,但实际上该方法的调用有两次,如下:
这里的调用场景如下:
- 当节点更新后的回调执行。这没什么好说的,管理端进行了动态配置后更新配置节点,提供者需要根据动态配置来选择是否重新发布服务。
- 当服务处理完发布订阅流程后会主动调用一次。这一步的目的是为了同步configurators 子节点的配置信息。即当服务发布完成后到这一步结束前,此时并未完成配置的监听操作,也就是说如果在此区间进行动态配置,提供者因为监听逻辑没有完成是无法感知的,所以需要当完成配置监听后主动调用一次来同步配置。当执行这一步时,根据 urls 的信息判断本地配置信息是否需要更新,如果需要更新则更新,并重新发布服务。也即是说,如果配置子节点不为空(则urls 不为空),服务发布会经历 :服务发布 -> 配置更新 -> 服务重新发布
ZookeeperRegistry#notify 入参的含义:
- url :当前暴露的服务信息,保存当前服务提供者的信息
- listener :configurators 子节点 的回调监听器实例,当 configurators 子节点更新时会回调该监听器
- urls :configurators 的子节点 转换而来的 Url。如果信息不合法,则转化为 empty 协议的 url,在后续处理会被过滤掉。
在这里 ZookeeperRegistry.this.notify
和 notify(url, listener, urls);
的调用顺序都如下:
1. org.apache.dubbo.registry.support.FailbackRegistry#notify
2. org.apache.dubbo.registry.support.FailbackRegistry#doNotify
3. org.apache.dubbo.registry.support.AbstractRegistry#notify(org.apache.dubbo.common.URL, org.apache.dubbo.registry.NotifyListener, java.util.List<org.apache.dubbo.common.URL>)
4. org.apache.dubbo.registry.integration.RegistryProtocol.OverrideListener#notify
其中 第三步 AbstractRegistry#notify
的实现如下:
protected void notify(URL url, NotifyListener listener, List<URL> urls) {
//... 入参校验
// key 为 类别( provider、configurators、routers ),value 为所属url
Map<String, List<URL>> result = new HashMap<String, List<URL>>();
// 按照url类别进行分组,如 Provider、configurators、routers
for (URL u : urls) {
// u 为 被监听的 URl, urls 为监听的节点转化为的 URl。这里判断节点 URL 是否可用于 u
// 即当前变更的配置u 是否适用于当前服务url
if (UrlUtils.isMatch(url, u)) {
// 按照 category 类别 划分 url
String category = u.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
List<URL> categoryList = result.get(category);
if (categoryList == null) {
categoryList = new ArrayList<URL>();
result.put(category, categoryList);
}
categoryList.add(u);
}
}
// 如果没有使用的url 则直接返回
// result 的 key 为 category,value 为对应分组的 URl 列表
if (result.size() == 0) {
return;
}
// key 为 类别, value 为 对应类别的 节点URL
Map<String, List<URL>> categoryNotified = notified.get(url);
if (categoryNotified == null) {
notified.putIfAbsent(url, new ConcurrentHashMap<String, List<URL>>());
categoryNotified = notified.get(url);
}
// 这里需要注意的是,这里回调是按照类别进行回调,即 如果 provider、configurators、routers 则 provider 子节点回调一次,configurators子节点回调一次,routers 子节点回调一次
for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
String category = entry.getKey();
List<URL> categoryList = entry.getValue();
categoryNotified.put(category, categoryList);
saveProperties(url);
// 进行服务通知,这里调用是 RegistryProtocol.OverrideListener#notify
listener.notify(categoryList);
}
}
很显然我们这里需要分析一下 RegistryProtocol.OverrideListener#notify
的具体实现,如下:
@Override
public synchronized void notify(List<URL> urls) {
// 获取 可以用于当前服务配置
List<URL> matchedUrls = getMatchedUrls(urls, subscribeUrl.addParameter(CATEGORY_KEY,
CONFIGURATORS_CATEGORY));
// No matching results
// 没有可用于当前服务的配置则返回。
if (matchedUrls.isEmpty()) {
return;
}
// 挑选出 配置节点URL 并转换为 configurators, 后面会通过 configurators进行合并
this.configurators = Configurator.toConfigurators(classifyUrls(matchedUrls, UrlUtils::isConfigurator))
.orElse(configurators);
// 在必要的情况下覆盖配置,此时对configurators 的数据进行合并,并缓存到服务本地
doOverrideIfNecessary();
}
public synchronized void doOverrideIfNecessary() {
final Invoker<?> invoker;
if (originInvoker instanceof InvokerDelegate) {
invoker = ((InvokerDelegate<?>) originInvoker).getInvoker();
} else {
invoker = originInvoker;
}
//The origin invoker
// 获取 原始 URL
URL originUrl = RegistryProtocol.this.getProviderUrl(invoker);
String key = getCacheKey(originInvoker);
ExporterChangeableWrapper<?> exporter = bounds.get(key);
if (exporter == null) {
logger.warn(new IllegalStateException("error state, exporter should not be null"));
return;
}
//The current, may have been merged many times
// 获取当前 URL,当前URL,可能已经合并过很多次,所以并不一定等同于 originUrl
URL currentUrl = exporter.getInvoker().getUrl();
//Merged with this configuration
// Dubbo 2.6 版本逻辑 使用originUrl 与当前配置合并后,产生新 url
URL newUrl = getConfigedInvokerUrl(configurators, originUrl);
// dubbo 2.7.0 版本新增,也是配置合并,合并服务级别配置和应用级别配置
newUrl = getConfigedInvokerUrl(serviceConfigurationListeners.get(originUrl.getServiceKey())
.getConfigurators(), newUrl);
newUrl = getConfigedInvokerUrl(providerConfigurationListener.getConfigurators(), newUrl);
// currentUrl != newUrl, 则说明配置有变动,则重新发布服务
if (!currentUrl.equals(newUrl)) {
// 重新发布服务
RegistryProtocol.this.reExport(originInvoker, newUrl);
...
}
}
至此,提供者的发布流程基本结束。
三、DubboProtocol#export
经过上面的分析,我们知道了 DubboProtocol#export 的调用场景有两个:
- 远程服务发布 & 没有注册中心。此时服务发布会直接调用 DubboProtocol#export。
- 远程服务发布 & 有注册中心。此时服务发布会调用 RegistryProtocol#export ,而RegistryProtocol#export 会调用 DubboProtocol#export 来完成具体的服务发布。
DubboProtocol 是 Dubbo协议下服务发布的过程实现,DubboProtocol 在服务发布时,开启了Netty 服务,进行端口监听。
下面我们来看 DubboProtocol#export 的详细实现:
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl();
/*********** 1. 前置工作 **********/
// export service.
// 获取需要暴露的服务的key,结构为 协议/服务接口:版本:端口号
// 如这里解析出来的key为 : dubbo/api.DemoService:1.0.0:20880
String key = serviceKey(url);
// 封装成 Export。exporterMap 中保存的是本机暴露的服务接口列表,在服务调用 Exporter#unexport 时会将服务从exporterMap 中移除。
DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
// exporterMap 中保存了当前应用发布的服务
exporterMap.put(key, exporter);
//export an stub service for dispatching event
// 本地存根相关代码
Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
if (isStubSupportEvent && !isCallbackservice) {
String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
if (logger.isWarnEnabled()) {
logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
"], has set stubproxy support event ,but no stub methods founded."));
}
} else {
stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
}
}
/*********** 2. 开启服务 **********/
// 开启服务,这里默认的是 Netty。 同一个机器的不同接口服务导出只会开启一个NettyServer
openServer(url);
/*********** 3. 序列化优化 **********/
// 对序列化进行优化,可以通过 optimizer 参数指定优化器类的全路径类名
optimizeSerialization(url);
return exporter;
}
1. 前置工作
前置工作并没有做什么复杂工作,主要就做了两件事:
-
将将要暴露的服务保存到 exporterMap 中。我们这里需要注意 exporterMap,其声明如下:
protected final Map<String, Exporter<?>> exporterMap = new ConcurrentHashMap<String, Exporter<?>>();
exporterMap key 为 服务唯一key(服务接口 + 服务分组 + 服务版本号确定唯一服务),value 为 服务Exporter。即 exporterMap 缓存了当前机器上暴露的服务信息。而此时对于 一个 Exporter 来说,其结构应为 :
当服务消费者进行调用时, 提供者会根据调用服务信息获取serviceKey 通过 exporterMap 来获取 服务Exporter,再通过 Exporter 获取 Invoker 来进行方法调用。这些我们在讲到消费者的调用过程时会详细分析。
-
本地存根和和回调服务的配置检查,并将其保存到 stubServiceMethodsMap 中。
2. 开启服务
这里通过 DubboProtocol#openServer 方法开启了服务。默认情况下,如果服务提供者机器尚未开启过服务,该方法会在服务提供者机器上开启Netty 服务。需要注意的是,服务并不会重复开启,如果 服务提供者的 ip:port 已经创建了服务,则不会重复创建。DubboProtocol#openServer 的 具体实现如下:
// 开启服务,
// org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#openServer
private void openServer(URL url) {
// find server.
// 获取服务提供者机器的 ip:port, 并将其作为服务器实例的 key,用于标识当前的服务器实例。
// 这里的端口是 dubbo 服务 端口
String key = url.getAddress();
//client can export a service which's only for server to invoke
// 判断是否是服务提供者,服务提供者才会启动监听
boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
if (isServer) {
// 从缓存中获取服务,如果 服务提供者的 ip:port 已经创建了服务,则不会重复创建
ExchangeServer server = serverMap.get(key);
if (server == null) {
synchronized (this) {
server = serverMap.get(key);
if (server == null) {
// 通过 createServer(url) 创建服务
serverMap.put(key, createServer(url));
}
}
} else {
// server supports reset, use together with override
server.reset(url);
}
}
}
可以看到,关键的操作在 createServer(URL url)
中。
2.1 createServer(URL url)
DubboProtocol#createServer 完成了服务的创建过程,在服务提供者上开启了端口监听。
DubboProtocol#createServer的返回值实例类型为 HeaderExchangeServer,内部包含一个NettyServer,NettyServer 包含属性 ChannelHandler,用来处理服务的具体消息,ChannelHandler 在传递过程中层层包裹,最终如下图中所示(部分委托类并未画出):
下面我们来看一下 DubboProtocol#createServer 的具体实现
// org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#createServer
private ExchangeServer createServer(URL url) {
// send readonly event when server closes, it's enabled by default
/******* 1. 参数设置 ********/
// 1.1 默认启用服务器关闭时发送只读事件
url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString());
// enable heartbeat by default
// 1.2 默认启用心跳
url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
// 1.3 获取传输协议,默认为 Netty
String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER);
// 根据 Constants.SERVER_KEY(server) 校验对应的SPI 实现接口
if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
throw new RpcException("Unsupported server type: " + str + ", url: " + url);
}
// 1.4 设置编解码器为 dubbo
url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
/******* 2. 开启服务端口 ********/
ExchangeServer server;
try {
// 2.1 绑定ip端口,开启服务,这里server 默认是NettyServer,需要注意的是这里传入的requestHandler 是最终处理消息的处理器
server = Exchangers.bind(url, requestHandler);
} catch (RemotingException e) {
throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
}
// 2.2 校验客户端的传输协议
str = url.getParameter(Constants.CLIENT_KEY);
if (str != null && str.length() > 0) {
Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();
if (!supportedTypes.contains(str)) {
throw new RpcException("Unsupported client type: " + str);
}
}
return server;
}
我们下面按照代码注释顺序来分析。
2.1.1 基础参数设置
这里可以分为下面几步:
-
该参数控制服务器关闭时发送只读事件,默认启动。 即服务在关闭时会主动想其他连接服务发送只读消息。调用时机如下:在服务关闭时会调用 HeaderExchangeServer#close 而其中会调用 HeaderExchangeServer#sendChannelReadOnlyEvent 方法发送,其实现如下:
private void sendChannelReadOnlyEvent() { // 设置消息内容 Request request = new Request(); request.setEvent(Request.READONLY_EVENT); request.setTwoWay(false); request.setVersion(Version.getProtocolVersion()); // 获取连接通路 Collection<Channel> channels = getChannels(); for (Channel channel : channels) { try { // 如果还保持连接 if (channel.isConnected()) { // 发送只读事件 channel.send(request, getUrl().getParameter(Constants.CHANNEL_READONLYEVENT_SENT_KEY, true)); } } catch (RemotingException e) { logger.warn("send cannot write message error.", e); } } }
-
该参数控是否启用心跳连接。调用时机如下:在 HeaderExchangeServer#HeaderExchangeServer 构造函数时 调用 startHeartbeatTimer 方法启动心跳和重连的任务。由 HeartbeatTimerTask 和 ReconnectTimerTask 来执行具体任务。
public HeaderExchangeServer(Server server) { if (server == null) { throw new IllegalArgumentException("server == null"); } this.server = server; this.heartbeat = server.getUrl().getParameter(Constants.HEARTBEAT_KEY, 0); this.heartbeatTimeout = server.getUrl().getParameter(Constants.HEARTBEAT_TIMEOUT_KEY, heartbeat * 3); if (heartbeatTimeout < heartbeat * 2) { throw new IllegalStateException("heartbeatTimeout < heartbeatInterval * 2"); } // 启动定时器 startHeartbeatTimer(); }
-
校验传输协议服务端的传输协议 Transporter。这里是为了确保Dubbo存在客户端指定的传输协议类型。默认为 Netty。
-
添加 DubboCodec 为指定编码器。
2.1.2 绑定服务端口
Dubbo 通过 Exchangers.bind(url, requestHandler)
来绑定Dubbo 服务端口,我们这里需要注意一下这里的 requestHandler 是 DubboProtocol 中的匿名 ExchangeHandlerAdapter 实现类,调用消息最终会交由该类来处理。
Exchangers.bind(url, requestHandler)
代码调用顺序较为复杂,具体如下:
public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
...
// 如果 codec 没有默认值,则添加 exchange。但是在基础参数设置中我们已经指定了编码器为 dubbo
url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
// 1. getExchanger(url) : 获取url 中的 exchanger 属性来获取到 Exchanger,默认是Header
// 2. bind(url, handler):第一步中默认是HeaderExchanger,所以这里实际上是 HeaderExchanger#bind(url, handler)
return getExchanger(url).bind(url, handler);
}
-
Exchangers.bind(url, requestHandler) 实现如下:getExchanger(url) : 通过SPI 获取 Exchanger,默认实现是 HeaderExchanger。所以这里bind(url, handler) 调用的是
HeaderExchanger#bind
// Exchangers#getExchanger 实现如下: public static Exchanger getExchanger(URL url) { String type = url.getParameter(Constants.EXCHANGER_KEY, Constants.DEFAULT_EXCHANGER); return getExchanger(type); } // 通过SPI 获取Exchange public static Exchanger getExchanger(String type) { return ExtensionLoader.getExtensionLoader(Exchanger.class).getExtension(type); }
-
HeaderExchanger#bind 实现如下:
// HeaderExchanger#bind @Override public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException { return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))); }
其中
Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))
实现如下:public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException { ... ChannelHandler handler; // 如果有多个 Handlers ,则使用 ChannelHandlerDispatcher 来分发处理。 if (handlers.length == 1) { handler = handlers[0]; } else { handler = new ChannelHandlerDispatcher(handlers); } // 1. getTransporter() 通过SPI 获取到了默认的 Transporter实现 NettyTransporter。 // 2. .bind(url, handler) 的实现是 NettyTransporter.bind(url, handler),完成了绑定了当前服务.所以此处调用的是NettyTransporter#bind return getTransporter().bind(url, handler); }
由于 在
2.1.1 基础参数设置
中我们指定了传输协议 server 的值为 netty。 getTransporter() 通过SPI 获取到了默认的 Transporter实现 NettyTransporter,所以此处调用的是NettyTransporter#bind,其详细实现如下:@Override public Server bind(URL url, ChannelHandler listener) throws RemotingException { return new NettyServer(url, listener); }
而在 Dubbo2.7 中提供了Netty3.x 和Netty4.x版本的实现,实现上流程大体相同。
我们这里看的是Netty4版本 即
org.apache.dubbo.remoting.transport.netty4.NettyServer
关于 NettyServer 内部的具体实现,篇幅所限,另开新篇:Dubbo笔记⑥ : 服务发布流程 - NettyServer
3. 序列化优化
DubboProtocol#optimizeSerialization
实现如下:
private void optimizeSerialization(URL url) throws RpcException {
// 获取指定的序列化方案
String className = url.getParameter(Constants.OPTIMIZER_KEY, "");
if (StringUtils.isEmpty(className) || optimizers.contains(className)) {
return;
}
try {
// 反射创建序列化类的实例,并检验其正确定
Class clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
if (!SerializationOptimizer.class.isAssignableFrom(clazz)) {
throw new RpcException("The serialization optimizer " + className + " isn't an instance of " + SerializationOptimizer.class.getName());
}
SerializationOptimizer optimizer = (SerializationOptimizer) clazz.newInstance();
if (optimizer.getSerializableClasses() == null) {
return;
}
for (Class c : optimizer.getSerializableClasses()) {
SerializableClassRegistry.registerClass(c);
}
// 将指定的序列化类添加到序列化集合中。
optimizers.add(className);
}
....
}
四、InjvmProtocol#export
对于远程服务来说,提供者和调用者一般不在同一个环境中,通过网络进行调用,如下
除此之外,Dubbo还提供了一种本地服务暴露和引用的方式,在同一个JVM 进行中发布和调用同一个服务时,这种方式可以避免一次远程调用,而直接在JVM 内进行通信。
在之前我们讲到本地服务暴露使用的协议是 Injvm,Injvm 是一个伪协议,不开启端口,不发起远程调用,仅与Jvm 内直接关联。在 Dubbo笔记④ : 服务发布流程 - doExportUrlsFor1Protocol 中我们提到Dubbo 默认情况下是会同时暴露远程服务和本地服务的, 而在ServiceConfig#doExportUrlsFor1Protocol中通过exportLocal 方法对客 InjvmProtocol 参数进行配置并且进行了服务暴露,其代码如下:
// ServiceConfig#exportLocal
private void exportLocal(URL url) {
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
URL local = URL.valueOf(url.toFullString())
// 设置协议为 injvm
.setProtocol(Constants.LOCAL_PROTOCOL)
// 设置host为127.0.0.1
.setHost(LOCALHOST)
// 设置端口为0
.setPort(0);
// 进行服务暴露,该方法下面会讲解
Exporter<?> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
// 添加到暴露的服务集合中
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
}
}
本地服务的暴露过程相对来说比较简单。这里我们直接来看 InjvmProtocol#export
的实现:
private static InjvmProtocol INSTANCE;
public InjvmProtocol() {
INSTANCE = this;
}
protected final Map<String, Exporter<?>> exporterMap = new ConcurrentHashMap<String, Exporter<?>>();
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}
这里我们需要注意两点 :
-
在 InjvmProtocol 的构造函数中,使用了静态变量 INSTANCE 来保存 来保存InjvmProtocol 自身实例。由于Dubbo的 SPI 会缓存每个扩展接口实现的Class对象,所以在整个JVM内对于每个扩展接口来说只会存在一个InjvmProtocol 实例,并且其中INSTANCE 保存的就是这个实例对象。
-
在 InjvmExporter 构造函数中将自身放入到了InjvmProtocol 管理的 exporterMap 中。即 exporterMap 中保存了已经暴露的本地服务。
InjvmExporter(Invoker<T> invoker, String key, Map<String, Exporter<?>> exporterMap) { super(invoker); // 这里的key为服务keu,如:dubbo/com.kingfish.service.impl.DemoService:1.0.0 this.key = key; this.exporterMap = exporterMap; exporterMap.put(key, this); }
综上,我们知道,在 JVM 中, InjvmProtocol 是单例的,InjvmProtocol#INSTANCE指向唯一实例,并且所有的本地暴露服务都被保存在 InjvmProtocol#exporterMap 中。
在消费者启动的时候,会通过 ReferenceConfig#createProxy
创建引用代理。其实现如下(这里我们暂时只关注本地服务引用的过程,远程服务的引用暂不关注):
private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
@SuppressWarnings({"unchecked", "rawtypes", "deprecation"})
private T createProxy(Map<String, String> map) {
URL tmpUrl = new URL("temp", "localhost", 0, map);
// 判断是否是 本地引用
final boolean isJvmRefer;
// 如果没有指定
if (isInjvm() == null) {
if (url != null && url.length() > 0) { // if a url is specified, don't do local reference
isJvmRefer = false;
} else {
// by default, reference local service if there is
// 判断是否是本地引用,InjvmProtocol.getInjvmProtocol() 返回的就是 InjvmProtocol#INSTANCE实例。
isJvmRefer = InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl);
}
} else {
// 使用指定参数
isJvmRefer = isInjvm();
}
// 如果是本地引用
if (isJvmRefer) {
// 创建一个本地URL,其中host为127.0.0.1,port 为 0,协议为 injvm,这些参数与服务导出时的一致
URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
// 由于此时url 的协议类型为 Injvm,refprotocol 通过 SPI机制获取到 InjvmProtocol,从而获取到InjvmInvoker
invoker = refprotocol.refer(interfaceClass, url);
} else {
...
}
// create service proxy
return (T) proxyFactory.getProxy(invoker);
}
可以看到,如果是本地引用,则会创建一个本地URL,其中host为127.0.0.1,port 为 0,协议为 injvm,这些参数与服务导出时的一致。随后会调用 refprotocol.refer(interfaceClass, url);
方法来进行服务引用,这里我们来看一下 refprotocol.refer(interfaceClass, url);
的实现 InjvmProtocol#refer
。
@Override
public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
// InjvmProtocol 将自身管理的 exporterMap 交由InjvmInvoker,而 exporterMap 中保存了本地暴露的服务,从而实现了本地服务的引用。
return new InjvmInvoker<T>(serviceType, url, url.getServiceKey(), exporterMap);
}
这里可以看到, InjvmProtocol#refer
将 exporterMap 传递给了 InjvmInvoker,在InjvmInvoker 调用服务的时候,便可以从 exporterMap 中获取到本地服务。(由于是本地引用,所以消费者和提供者公用同一个 JVM,所以提供者中的 InjvmProtocol 和消费者中的 InjvmProtocol 是同一个实例。)
以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/export-service/
https://blog.csdn.net/jadebai/article/details/80684849
https://blog.csdn.net/luoyang_java/article/details/86613015
https://segmentfault.com/a/1190000017315041
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正