learn.lianglianglee.com/专栏/Dubbo源码解读与实战-完/42 加餐:服务引用流程全解析.md.html
2022-05-11 19:04:14 +08:00

955 lines
41 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>42 加餐:服务引用流程全解析.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/Dubbo源码解读与实战-完/00 开篇词 深入掌握 Dubbo 原理与实现,提升你的职场竞争力.md.html">00 开篇词 深入掌握 Dubbo 原理与实现,提升你的职场竞争力.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/01 Dubbo 源码环境搭建:千里之行,始于足下.md.html">01 Dubbo 源码环境搭建:千里之行,始于足下.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/02 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo.md.html">02 Dubbo 的配置总线:抓住 URL就理解了半个 Dubbo.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/03 Dubbo SPI 精析,接口实现两极反转(上).md.html">03 Dubbo SPI 精析,接口实现两极反转(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/04 Dubbo SPI 精析,接口实现两极反转(下).md.html">04 Dubbo SPI 精析,接口实现两极反转(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/05 海量定时任务,一个时间轮搞定.md.html">05 海量定时任务,一个时间轮搞定.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/06 ZooKeeper 与 Curator求你别用 ZkClient 了(上).md.html">06 ZooKeeper 与 Curator求你别用 ZkClient 了(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/07 ZooKeeper 与 Curator求你别用 ZkClient 了(下).md.html">07 ZooKeeper 与 Curator求你别用 ZkClient 了(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/08 代理模式与常见实现.md.html">08 代理模式与常见实现.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/09 Netty 入门,用它做网络编程都说好(上).md.html">09 Netty 入门,用它做网络编程都说好(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/10 Netty 入门,用它做网络编程都说好(下).md.html">10 Netty 入门,用它做网络编程都说好(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/11 简易版 RPC 框架实现(上).md.html">11 简易版 RPC 框架实现(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/12 简易版 RPC 框架实现(下).md.html">12 简易版 RPC 框架实现(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/13 本地缓存:降低 ZooKeeper 压力的一个常用手段.md.html">13 本地缓存:降低 ZooKeeper 压力的一个常用手段.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/14 重试机制是网络操作的基本保证.md.html">14 重试机制是网络操作的基本保证.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/15 ZooKeeper 注册中心实现,官方推荐注册中心实践.md.html">15 ZooKeeper 注册中心实现,官方推荐注册中心实践.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/16 Dubbo Serialize 层:多种序列化算法,总有一款适合你.md.html">16 Dubbo Serialize 层:多种序列化算法,总有一款适合你.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?.md.html">17 Dubbo Remoting 层核心接口分析:这居然是一套兼容所有 NIO 框架的设计?.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工.md.html">18 Buffer 缓冲区:我们不生产数据,我们只是数据的搬运工.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/19 Transporter 层核心实现:编解码与线程模型一文打尽(上).md.html">19 Transporter 层核心实现:编解码与线程模型一文打尽(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/20 Transporter 层核心实现:编解码与线程模型一文打尽(下).md.html">20 Transporter 层核心实现:编解码与线程模型一文打尽(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/21 Exchange 层剖析:彻底搞懂 Request-Response 模型(上).md.html">21 Exchange 层剖析:彻底搞懂 Request-Response 模型(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/22 Exchange 层剖析:彻底搞懂 Request-Response 模型(下).md.html">22 Exchange 层剖析:彻底搞懂 Request-Response 模型(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/23 核心接口介绍RPC 层骨架梳理.md.html">23 核心接口介绍RPC 层骨架梳理.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/24 从 Protocol 起手,看服务暴露和服务引用的全流程(上).md.html">24 从 Protocol 起手,看服务暴露和服务引用的全流程(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/25 从 Protocol 起手,看服务暴露和服务引用的全流程(下).md.html">25 从 Protocol 起手,看服务暴露和服务引用的全流程(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/26 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html">26 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/27 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html">27 加餐:直击 Dubbo “心脏”,带你一起探秘 Invoker.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/28 复杂问题简单化,代理帮你隐藏了多少底层细节?.md.html">28 复杂问题简单化,代理帮你隐藏了多少底层细节?.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/29 加餐HTTP 协议 + JSON-RPCDubbo 跨语言就是如此简单.md.html">29 加餐HTTP 协议 + JSON-RPCDubbo 跨语言就是如此简单.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/30 Filter 接口,扩展 Dubbo 框架的常用手段指北.md.html">30 Filter 接口,扩展 Dubbo 框架的常用手段指北.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/31 加餐:深潜 Directory 实现,探秘服务目录玄机.md.html">31 加餐:深潜 Directory 实现,探秘服务目录玄机.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/32 路由机制:请求到底怎么走,它说了算(上).md.html">32 路由机制:请求到底怎么走,它说了算(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/33 路由机制:请求到底怎么走,它说了算(下).md.html">33 路由机制:请求到底怎么走,它说了算(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/34 加餐:初探 Dubbo 动态配置的那些事儿.md.html">34 加餐:初探 Dubbo 动态配置的那些事儿.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上).md.html">35 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/36 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下).md.html">36 负载均衡:公平公正物尽其用的负载均衡策略,这里都有(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/37 集群容错:一个好汉三个帮(上).md.html">37 集群容错:一个好汉三个帮(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/38 集群容错:一个好汉三个帮(下).md.html">38 集群容错:一个好汉三个帮(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/39 加餐多个返回值不用怕Merger 合并器来帮忙.md.html">39 加餐多个返回值不用怕Merger 合并器来帮忙.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/40 加餐模拟远程调用Mock 机制帮你搞定.md.html">40 加餐模拟远程调用Mock 机制帮你搞定.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/41 加餐:一键通关服务发布全流程.md.html">41 加餐:一键通关服务发布全流程.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/Dubbo源码解读与实战-完/42 加餐:服务引用流程全解析.md.html">42 加餐:服务引用流程全解析.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/43 服务自省设计方案:新版本新方案.md.html">43 服务自省设计方案:新版本新方案.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/44 元数据方案深度剖析,如何避免注册中心数据量膨胀?.md.html">44 元数据方案深度剖析,如何避免注册中心数据量膨胀?.md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/45 加餐:深入服务自省方案中的服务发布订阅(上).md.html">45 加餐:深入服务自省方案中的服务发布订阅(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/46 加餐:深入服务自省方案中的服务发布订阅(下).md.html">46 加餐:深入服务自省方案中的服务发布订阅(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上).md.html">47 配置中心设计与实现:集中化配置 and 本地化配置,我都要(上).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下).md.html">48 配置中心设计与实现:集中化配置 and 本地化配置,我都要(下).md.html</a>
</li>
<li>
<a href="/专栏/Dubbo源码解读与实战-完/49 结束语 认真学习,缩小差距.md.html">49 结束语 认真学习,缩小差距.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>42 加餐:服务引用流程全解析</h1>
<p>Dubbo 作为一个 RPC 框架,暴露给用户最基本的功能就是服务发布和服务引用。在上一课时,我们已经分析了服务发布的核心流程。那么在本课时,我们就接着深入分析<strong>服务引用的核心流程</strong></p>
<p>Dubbo 支持两种方式引用远程的服务:</p>
<ul>
<li><strong>服务直连的方式</strong>,仅适合在<strong>调试服务</strong>的时候使用;</li>
<li><strong>基于注册中心引用服务</strong>,这是<strong>生产环境</strong>中使用的服务引用方式。</li>
</ul>
<h3>DubboBootstrap 入口</h3>
<p>在上一课时介绍服务发布的时候,我们介绍了 DubboBootstrap.start() 方法的核心流程,其中除了会<strong>调用 exportServices() 方法完成服务发布</strong>之外,还会<strong>调用 referServices() 方法完成服务引用</strong>,这里就不再贴出 DubboBootstrap.start() 方法的具体代码,你若感兴趣的话可以参考<a href="https://github.com/xxxlxy2008/dubbo">源码</a>进行学习。</p>
<p>在 DubboBootstrap.referServices() 方法中,会从 ConfigManager 中获取所有 ReferenceConfig 列表,并根据 ReferenceConfig 获取对应的代理对象,入口逻辑如下:</p>
<pre><code>private void referServices() {
if (cache == null) { // 初始ReferenceConfigCache
cache = ReferenceConfigCache.getCache();
}
configManager.getReferences().forEach(rc -&gt; {
// 遍历ReferenceConfig列表
ReferenceConfig referenceConfig = (ReferenceConfig) rc;
referenceConfig.setBootstrap(this);
if (rc.shouldInit()) { // 检测ReferenceConfig是否已经初始化
if (referAsync) { // 异步
CompletableFuture&lt;Object&gt; future = ScheduledCompletableFuture.submit(
executorRepository.getServiceExporterExecutor(),
() -&gt; cache.get(rc)
);
asyncReferringFutures.add(future);
} else { // 同步
cache.get(rc);
}
}
});
}
</code></pre>
<p>这里的 ReferenceConfig 是哪里来的呢?在[第 01 课时]dubbo-demo-api-consumer 示例中,我们可以看到构造 ReferenceConfig 对象的逻辑,这些新建的 ReferenceConfig 对象会通过 DubboBootstrap.reference() 方法添加到 ConfigManager 中进行管理,如下所示:</p>
<pre><code>public DubboBootstrap reference(ReferenceConfig&lt;?&gt; referenceConfig) {
configManager.addReference(referenceConfig);
return this;
}
</code></pre>
<h3>ReferenceConfigCache</h3>
<p><strong>服务引用的核心实现在 ReferenceConfig 之中</strong>,一个 ReferenceConfig 对象对应一个服务接口,每个 ReferenceConfig 对象中都封装了与注册中心的网络连接,以及与 Provider 的网络连接,这是一个非常重要的对象。</p>
<p><strong>为了避免底层连接泄漏造成性能问题,从 Dubbo 2.4.0 版本开始Dubbo 提供了 ReferenceConfigCache 用于缓存 ReferenceConfig 实例。</strong></p>
<p>在 dubbo-demo-api-consumer 示例中,我们可以看到 ReferenceConfigCache 的基本使用方式:</p>
<pre><code>ReferenceConfig&lt;DemoService&gt; reference = new ReferenceConfig&lt;&gt;();
reference.setInterface(DemoService.class);
...
// 这一步在DubboBootstrap.start()方法中完成
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
...
DemoService demoService = ReferenceConfigCache.getCache().get(reference);
</code></pre>
<p>在 ReferenceConfigCache 中维护了一个静态的 MapCACHE_HOLDER字段其中 Key 是由 Group、服务接口和 version 构成Value 是一个 ReferenceConfigCache 对象。在 ReferenceConfigCache 中可以传入一个 KeyGenerator 用来修改缓存 Key 的生成逻辑KeyGenerator 接口的定义如下:</p>
<pre><code>public interface KeyGenerator {
String generateKey(ReferenceConfigBase&lt;?&gt; referenceConfig);
}
</code></pre>
<p>默认的 KeyGenerator 实现是 ReferenceConfigCache 中的匿名内部类,其对象由 DEFAULT_KEY_GENERATOR 这个静态字段引用,具体实现如下:</p>
<pre><code>public static final KeyGenerator DEFAULT_KEY_GENERATOR = referenceConfig -&gt; {
String iName = referenceConfig.getInterface();
if (StringUtils.isBlank(iName)) { // 获取服务接口名称
Class&lt;?&gt; clazz = referenceConfig.getInterfaceClass();
iName = clazz.getName();
}
if (StringUtils.isBlank(iName)) {
throw new IllegalArgumentException(&quot;No interface info in ReferenceConfig&quot; + referenceConfig);
}
// Key的格式是group/interface:version
StringBuilder ret = new StringBuilder();
if (!StringUtils.isBlank(referenceConfig.getGroup())) {
ret.append(referenceConfig.getGroup()).append(&quot;/&quot;);
}
ret.append(iName);
if (!StringUtils.isBlank(referenceConfig.getVersion())) {
ret.append(&quot;:&quot;).append(referenceConfig.getVersion());
}
return ret.toString();
};
</code></pre>
<p>在 ReferenceConfigCache 实例对象中,会维护下面两个 Map 集合。</p>
<ul>
<li>proxiesConcurrentMap&lt;Class&lt;?&gt;, ConcurrentMap&lt;String, Object&gt;&gt;类型):该集合用来存储服务接口的全部代理对象,其中第一层 Key 是服务接口的类型,第二层 Key 是上面介绍的 KeyGenerator 为不同服务提供方生成的 KeyValue 是服务的代理对象。</li>
<li>referredReferencesConcurrentMap&lt;String, ReferenceConfigBase&lt;?&gt;&gt; 类型):该集合用来存储已经被处理的 ReferenceConfig 对象。</li>
</ul>
<p>我们回到 DubboBootstrap.referServices() 方法中,看一下其中与 ReferenceConfigCache 相关的逻辑。</p>
<p>首先是 ReferenceConfigCache.getCache() 这个静态方法,会在 CACHE_HOLDER 集合中添加一个 Key 为“<em>DEFAULT</em>”的 ReferenceConfigCache 对象(使用默认的 KeyGenerator 实现),它将作为默认的 ReferenceConfigCache 对象。</p>
<p>接下来,无论是同步服务引用还是异步服务引用,都会调用 ReferenceConfigCache.get() 方法,创建并缓存代理对象。下面就是 ReferenceConfigCache.get() 方法的核心实现:</p>
<pre><code>public &lt;T&gt; T get(ReferenceConfigBase&lt;T&gt; referenceConfig) {
// 生成服务提供方对应的Key
String key = generator.generateKey(referenceConfig);
// 获取接口类型
Class&lt;?&gt; type = referenceConfig.getInterfaceClass();
// 获取该接口对应代理对象集合
proxies.computeIfAbsent(type, _t -&gt; new ConcurrentHashMap&lt;&gt;());
ConcurrentMap&lt;String, Object&gt; proxiesOfType = proxies.get(type);
// 根据Key获取服务提供方对应的代理对象
proxiesOfType.computeIfAbsent(key, _k -&gt; {
// 服务引用
Object proxy = referenceConfig.get();
// 将ReferenceConfig记录到referredReferences集合
referredReferences.put(key, referenceConfig);
return proxy;
});
return (T) proxiesOfType.get(key);
}
</code></pre>
<h3>ReferenceConfig</h3>
<p>通过前面的介绍我们知道,<strong>ReferenceConfig 是服务引用的真正入口</strong>,其中会创建相关的代理对象。下面先来看 ReferenceConfig.get() 方法:</p>
<pre><code>public synchronized T get() {
if (destroyed) { // 检测当前ReferenceConfig状态
throw new IllegalStateException(&quot;...&quot;);
}
if (ref == null) {// ref指向了服务的代理对象
init(); // 初始化ref字段
}
return ref;
}
</code></pre>
<p>在 ReferenceConfig.init() 方法中,首先会对服务引用的配置进行处理,以保证配置的正确性。这里的具体实现其实本身并不复杂,但由于涉及很多的配置解析和处理逻辑,代码就显得非常长,我们就不再一一展示,你若感兴趣的话可以参考<a href="https://github.com/xxxlxy2008/dubbo">源码</a>进行学习。</p>
<p><strong>ReferenceConfig.init() 方法的核心逻辑是调用 createProxy() 方法</strong>,调用之前会从配置中获取 createProxy() 方法需要的参数:</p>
<pre><code>public synchronized void init() {
if (initialized) { // 检测ReferenceConfig的初始化状态
return;
}
if (bootstrap == null) { // 检测DubboBootstrap的初始化状态
bootstrap = DubboBootstrap.getInstance();
bootstrap.init();
}
... // 省略其他配置的检查
Map&lt;String, String&gt; map = new HashMap&lt;String, String&gt;();
map.put(SIDE_KEY, CONSUMER_SIDE); // 添加side参数
// 添加Dubbo版本、release参数、timestamp参数、pid参数
ReferenceConfigBase.appendRuntimeParameters(map);
// 添加interface参数
map.put(INTERFACE_KEY, interfaceName);
... // 省略其他参数的处理
String hostToRegistry = ConfigUtils.getSystemProperty(DUBBO_IP_TO_REGISTRY);
if (StringUtils.isEmpty(hostToRegistry)) {
hostToRegistry = NetUtils.getLocalHost();
} else if (isInvalidLocalHost(hostToRegistry)) {
throw new IllegalArgumentException(&quot;...&quot;);
}
// 添加ip参数
map.put(REGISTER_IP_KEY, hostToRegistry);
// 调用createProxy()方法
ref = createProxy(map);
...// 省略其他代码
initialized = true;
// 触发ReferenceConfigInitializedEvent事件
dispatch(new ReferenceConfigInitializedEvent(this, invoker));
}
</code></pre>
<p>ReferenceConfig.createProxy() 方法中处理了多种服务引用的场景,例如,直连单个/多个Provider、单个/多个注册中心。下面是 createProxy() 方法的核心流程,大致可以梳理出这么 5 个步骤。</p>
<ol>
<li>根据传入的参数集合判断协议是否为 injvm 协议,如果是,直接通过 InjvmProtocol 引用服务。</li>
<li>构造 urls 集合。Dubbo 支持<strong>直连 Provider</strong><strong>依赖注册中心</strong>两种服务引用方式。如果是直连服务的模式,我们可以通过 url 参数指定一个或者多个 Provider 地址,会被解析并填充到 urls 集合;如果通过注册中心的方式进行服务引用,则会调用 AbstractInterfaceConfig.loadRegistries() 方法加载所有注册中心。</li>
<li>如果 urls 集合中只记录了一个 URL通过 Protocol 适配器选择合适的 Protocol 扩展实现创建 Invoker 对象。如果是直连 Provider 的场景,则 URL 为 dubbo 协议,这里就会使用 DubboProtocol 这个实现;如果依赖注册中心,则使用 RegistryProtocol 这个实现。</li>
<li>如果 urls 集合中有多个注册中心,则使用 ZoneAwareCluster 作为 Cluster 的默认实现,生成对应的 Invoker 对象;如果 urls 集合中记录的是多个直连服务的地址,则使用 Cluster 适配器选择合适的扩展实现生成 Invoker 对象。</li>
<li>通过 ProxyFactory 适配器选择合适的 ProxyFactory 扩展实现,将 Invoker 包装成服务接口的代理对象。</li>
</ol>
<p>通过上面的流程我们可以看出<strong>createProxy() 方法中有两个核心</strong>:一是通过 Protocol 适配器选择合适的 Protocol 扩展实现创建 Invoker 对象;二是通过 ProxyFactory 适配器选择合适的 ProxyFactory 创建代理对象。</p>
<p>下面我们来看 createProxy() 方法的具体实现:</p>
<pre><code>private T createProxy(Map&lt;String, String&gt; map) {
if (shouldJvmRefer(map)) { // 根据url的协议、scope以及injvm等参数检测是否需要本地引用
// 创建injvm协议的URL
URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
// 通过Protocol的适配器选择对应的Protocol实现创建Invoker对象
invoker = REF_PROTOCOL.refer(interfaceClass, url);
if (logger.isInfoEnabled()) {
logger.info(&quot;Using injvm service &quot; + interfaceClass.getName());
}
} else {
urls.clear();
if (url != null &amp;&amp; url.length() &gt; 0) {
String[] us = SEMICOLON_SPLIT_PATTERN.split(url); // 配置多个URL的时候会用分号进行切分
if (us != null &amp;&amp; us.length &gt; 0) { // url不为空表明用户可能想进行点对点调用
for (String u : us) {
URL url = URL.valueOf(u);
if (StringUtils.isEmpty(url.getPath())) {
url = url.setPath(interfaceName); // 设置接口完全限定名为URL Path
}
if (UrlUtils.isRegistry(url)) { // 检测URL协议是否为registry若是说明用户想使用指定的注册中心
// 这里会将map中的参数整理成一个参数添加到refer参数中
urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
} else {
// 将map中的参数添加到url中
urls.add(ClusterUtils.mergeUrl(url, map));
}
}
}
} else {
if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {
checkRegistry();
// 加载注册中心的地址RegistryURL
List&lt;URL&gt; us = ConfigValidationUtils.loadRegistries(this, false);
if (CollectionUtils.isNotEmpty(us)) {
for (URL u : us) {
URL monitorUrl = ConfigValidationUtils.loadMonitor(this, u);
if (monitorUrl != null) {
map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
// 将map中的参数整理成refer参数添加到RegistryURL中
urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
}
}
if (urls.isEmpty()) { // 既不是服务直连,也没有配置注册中心,抛出异常
throw new IllegalStateException(&quot;...&quot;);
}
}
}
if (urls.size() == 1) {
// 在单注册中心或是直连单个服务提供方的时候通过Protocol的适配器选择对应的Protocol实现创建Invoker对象
invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
} else {
// 多注册中心或是直连多个服务提供方的时候会根据每个URL创建Invoker对象
List&lt;Invoker&lt;?&gt;&gt; invokers = new ArrayList&lt;Invoker&lt;?&gt;&gt;();
URL registryURL = null;
for (URL url : urls) {
invokers.add(REF_PROTOCOL.refer(interfaceClass, url));
if (UrlUtils.isRegistry(url)) { // 确定是多注册中心还是直连多个Provider
registryURL = url;
}
}
if (registryURL != null) {
// 多注册中心的场景中会使用ZoneAwareCluster作为Cluster默认实现多注册中心之间的选择
URL u = registryURL.addParameterIfAbsent(CLUSTER_KEY, ZoneAwareCluster.NAME);
invoker = CLUSTER.join(new StaticDirectory(u, invokers));
} else {
// 多个Provider直连的场景中使用Cluster适配器选择合适的扩展实现
invoker = CLUSTER.join(new StaticDirectory(invokers));
}
}
}
if (shouldCheck() &amp;&amp; !invoker.isAvailable()) {
// 根据check配置决定是否检测Provider的可用性
invoker.destroy();
throw new IllegalStateException(&quot;...&quot;);
}
...// 元数据处理相关的逻辑
// 通过ProxyFactory适配器选择合适的ProxyFactory扩展实现创建代理对象
return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}
</code></pre>
<h3>RegistryProtocol</h3>
<p>在直连 Provider 的场景中,会使用 DubboProtocol.refer() 方法完成服务引用DubboProtocol.refer() 方法的具体实现在前面[第 25 课时]中已经详细介绍过了这里我们重点来看存在注册中心的场景中Dubbo Consumer 是如何通过 RegistryProtocol 完成服务引用的。</p>
<p>在 RegistryProtocol.refer() 方法中,会先根据 URL 获取注册中心的 URL再调用 doRefer 方法生成 Invoker在 refer() 方法中会使用 MergeableCluster 处理多 group 引用的场景。</p>
<pre><code>public &lt;T&gt; Invoker&lt;T&gt; refer(Class&lt;T&gt; type, URL url) throws RpcException {
url = getRegistryUrl(url); // 从URL中获取注册中心的URL
// 获取Registry实例这里的RegistryFactory对象是通过Dubbo SPI的自动装载机制注入的
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// 从注册中心URL的refer参数中获取此次服务引用的一些参数其中就包括group
Map&lt;String, String&gt; qs = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
String group = qs.get(GROUP_KEY);
if (group != null &amp;&amp; group.length() &gt; 0) {
if ((COMMA_SPLIT_PATTERN.split(group)).length &gt; 1 || &quot;*&quot;.equals(group)) {
// 如果此次可以引用多个group的服务则Cluser实现使用MergeableCluster实现
// 这里的getMergeableCluster()方法就会通过Dubbo SPI方式找到MergeableCluster实例
return doRefer(getMergeableCluster(), registry, type, url);
}
}
// 如果没有group参数或是只指定了一个group则通过Cluster适配器选择Cluster实现
return doRefer(cluster, registry, type, url);
}
</code></pre>
<p>在 doRefer() 方法中,首先会根据 URL 初始化 RegistryDirectory 实例,然后生成 Subscribe URL 并进行注册,之后会通过 Registry 订阅服务,最后通过 Cluster 将多个 Invoker 合并成一个 Invoker 返回给上层,具体实现如下:</p>
<pre><code>private &lt;T&gt; Invoker&lt;T&gt; doRefer(Cluster cluster, Registry registry, Class&lt;T&gt; type, URL url) {
// 创建RegistryDirectory实例
RegistryDirectory&lt;T&gt; directory = new RegistryDirectory&lt;T&gt;(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// 生成SubscribeUrl协议为consumer具体的参数是RegistryURL中refer参数指定的参数
Map&lt;String, String&gt; parameters = new HashMap&lt;String, String&gt;(directory.getConsumerUrl().getParameters());
URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
if (directory.isShouldRegister()) {
directory.setRegisteredConsumerUrl(subscribeUrl); // 在SubscribeUrl中添加category=consumers和check=false参数
registry.register(directory.getRegisteredConsumerUrl()); // 服务注册在Zookeeper的consumers节点下添加该Consumer对应的节点
}
directory.buildRouterChain(subscribeUrl); // 根据SubscribeUrl创建服务路由
// 订阅服务toSubscribeUrl()方法会将SubscribeUrl中category参数修改为&quot;providers,configurators,routers&quot;
// RegistryDirectory的subscribe()在前面详细分析过了其中会通过Registry订阅服务同时还会添加相应的监听器
directory.subscribe(toSubscribeUrl(subscribeUrl));
// 注册中心中可能包含多个Provider相应地也就有多个Invoker
// 这里通过前面选择的Cluster将多个Invoker对象封装成一个Invoker对象
Invoker&lt;T&gt; invoker = cluster.join(directory);
// 根据URL中的registry.protocol.listener参数加载相应的监听器实现
List&lt;RegistryProtocolListener&gt; listeners = findRegistryProtocolListeners(url);
if (CollectionUtils.isEmpty(listeners)) {
return invoker;
}
// 为了方便在监听器中回调这里将此次引用使用到的Directory对象、Cluster对象、Invoker对象以及SubscribeUrl
// 封装到一个RegistryInvokerWrapper中传递给监听器
RegistryInvokerWrapper&lt;T&gt; registryInvokerWrapper = new RegistryInvokerWrapper&lt;&gt;(directory, cluster, invoker, subscribeUrl);
for (RegistryProtocolListener listener : listeners) {
listener.onRefer(this, registryInvokerWrapper);
}
return registryInvokerWrapper;
}
</code></pre>
<p>这里涉及的 RegistryDirectory、Router 接口、Cluster 接口及其相关的扩展实现,我们都已经在前面的课时详细分析过了,这里不再重复。</p>
<h3>总结</h3>
<p>本课时,我们重点介绍了 Dubbo 服务引用的整个流程。</p>
<ul>
<li>首先,我们介绍了 DubboBootStrap 这个入口门面类与服务引用相关的方法,其中涉及 referServices()、reference() 等核心方法。</li>
<li>接下来,我们分析了 ReferenceConfigCache 这个 ReferenceConfig 对象缓存,以及 ReferenceConfig 实现服务引用的核心流程。</li>
<li>最后,我们还讲解了 RegistryProtocol 从注册中心引用服务的核心实现。</li>
</ul>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Dubbo源码解读与实战-完/41 加餐:一键通关服务发布全流程.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Dubbo源码解读与实战-完/43 服务自省设计方案:新版本新方案.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70996f791aaa3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>