Dubbo原理和源码解析之服务引用
github新增仓库 "dubbo-read"(点此查看),集合所有《Dubbo原理和源码解析》系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行分享。不定期更新,欢迎Follow。
一、框架设计
在官方《Dubbo 开发指南》框架设计部分,给出了引用服务时序图:

另外,在官方《Dubbo 用户指南》集群容错部分,给出了服务引用的各功能组件关系图:

本文将根据以上两张图,分析服务引用的实现原理,并进行详细的代码跟踪与解析。
二、原理和源码解析
2.1 创建代理
Dubbo 基于 Spring 的 Schema 扩展实现 XML 配置解析,DubboNamespaceHandler 会将 <dubbo:reference> 标签解析为 ReferenceBean,ReferenceBean 实现了 FactoryBean,因此当它在代码中有引用时,会调用 ReferenceBean#getObject() 方法进入节点注册和服务发现流程。
ReferenceBean.java
public Object getObject() throws Exception {
return get();
}
ReferenceConfig.java
public synchronized T get() {
if (destroyed){
throw new IllegalStateException("Already destroyed!");
}
if (ref == null) {
init();
}
return ref;
}
private void init() {
//.......忽略
ref = createProxy(map);
}
private T createProxy(Map<String, String> map) {
//.....忽略
invoker = refprotocol.refer(interfaceClass, urls.get(0));
//.....忽略
// 创建服务代理
return (T) proxyFactory.getProxy(invoker);
}
2.2 服务发现
因为通过注册中心,因此在 ReferenceConfig.java#createProxy() 方法中,进入 RegistryProtocol.java#refer() 方法。
RegistryProtocol.java
private Cluster cluster;
public void setCluster(Cluster cluster) {
this.cluster = cluster;
}
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters());
if (! Constants.ANY_VALUE.equals(url.getServiceInterface())
&& url.getParameter(Constants.REGISTER_KEY, true)) {
registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
Constants.CHECK_KEY, String.valueOf(false)));
}
directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
Constants.PROVIDERS_CATEGORY
+ "," + Constants.CONFIGURATORS_CATEGORY
+ "," + Constants.ROUTERS_CATEGORY));
return cluster.join(directory);
}
RegistryDirectory 通过 RegistryDirectory#subscribeUrl() 向 Zookeeper 订阅服务节点信息并 watch 变更,这样就实现了服务自动发现。
2.3 Invoker选取
2.3.1 Cluster
上面我之所以把设置 Cluster 的代码贴上,是因为此处涉及到一个 Dubbo 服务框架核心的概念——微内核和插件机制(此处会单独一篇文章详细介绍):

有关 Dubbo 的设计原则,请查看Dubbo《一些设计上的基本常识》。
Cluster 类的定义如下:
Cluster.java
@SPI(FailoverCluster.NAME)
public interface Cluster { /**
* Merge the directory invokers to a virtual invoker.
*/
@Adaptive
<T> Invoker<T> join(Directory<T> directory) throws RpcException;
}
cluster 的类型是 Cluster$Adaptive,实际上是一个通用的代理类,它会根据 URL 中的 cluster 参数值定位到实际的 Cluster 实现类(默认是 FailoverCluster)。 由于 ExtensionLoader 在实例化对象时,会在实例化完成之后自动套上 Wrapper 类,而 MockerClusterWrapper 就是这样一个 Wrapper。
MockerClusterWrapper.java
public class MockClusterWrapper implements Cluster {
private Cluster cluster;
public MockClusterWrapper(Cluster cluster) {
this.cluster = cluster;
}
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new MockClusterInvoker<T>(directory, this.cluster.join(directory));
}
}
也就是说,实例化出来的 FailoverCluster 会作为参数赋予 MockerClusterWrapper#cluster,而 MockClusterWrapper 会作为参数赋予 RegistryProtocol#cluster。因此 RegistryProtocol#doRefer() 中调用 cluster.join(directory) 实际上是调用的 MockClusterWrapper#join(directory)。 使用这种机制,可以把一些公共的处理放在 Wrapper 类中,实现代码和功能收敛。
MockClusterInvoker.java
public Result invoke(Invocation invocation) throws RpcException {
Result result = null;
String value = directory.getUrl().getMethodParameter(invocation.getMethodName(),
Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
if (value.length() == 0 || value.equalsIgnoreCase("false")){
//no mock
result = this.invoker.invoke(invocation);
} else if (value.startsWith("force")) {
if (logger.isWarnEnabled()) {
logger.info("force-mock: " + invocation.getMethodName() +
" force-mock enabled , url : " + directory.getUrl());
}
//force:direct mock
result = doMockInvoke(invocation, null);
} else {
//fail-mock
try {
result = this.invoker.invoke(invocation);
}catch (RpcException e) {
if (e.isBiz()) {
throw e;
} else {
if (logger.isWarnEnabled()) {
logger.info("fail-mock: " + invocation.getMethodName() +
" fail-mock enabled , url : " + directory.getUrl(), e);
}
//fail:mock
result = doMockInvoke(invocation, e);
}
}
}
return result;
}
这里还涉及到 Dubbo 另外一个核心机制——Mock。Mock 可以在测试中模拟服务调用的各种异常情况,还用来实现服务降级。 从 MockClusterWrapper.join() 方法可知,实际创建的 ClusterInvoker 是封装了 FailoverClusterInvoker 的 MockerClusterInvoker。
在 MockerClusterInvoker 中,调用之前 Dubbo 会先检查 URL 中是否有 mock 参数(通过服务治理后台 Consumer 端的屏蔽和容错进行设置,或者直接动态设置 mock 参数值),如果存在且以 force 开头,则不发起远程调用直接执行降级逻辑;如果存在且以 fail 开头,则在远程调用异常时才会执行降级逻辑。
因此,通过 MockerClusterWrapper 成功地在 Invoker 中植入了 Mock 机制。
2.3.2 Directory
在 RegistryProtocol#doRefer() 中可以看到,服务发现过程是通过 RegistryDirectory 向 Zookeeper 订阅来实现的。 先看看 Directory 类之间的关系:

看下 Directory 接口的定义:
Directory.java
public interface Directory<T> extends Node {
Class<T> getInterface();
List<Invoker<T>> list(Invocation invocation) throws RpcException;
}
Directory 可以看做是对应 Interface 的 Invoker 列表,而这个列表可能是动态变化的,比如注册中心推送变更。
通过 ReferenceConfig#createProxy() 方法可知,StaticDirectory 主要用于多注册中心引用的场景,它的 invoker 列表是通过参数传入的、固定的。在此不做更详细的解析了。
RegistryDirectory 用于使用单注册中心发现服务的场景。RegistryDirectory 没有重写 list() 方法,所以使用 AbstractDirectory#list() 方法:
AbstractDirectory.java
public List<Invoker<T>> list(Invocation invocation) throws RpcException {
if (destroyed) {
throw new RpcException("Directory already destroyed .url: " + getUrl());
}
List<Invoker<T>> invokers = doList(invocation);
List<Router> localRouters = this.routers; // local reference
if (localRouters != null && !localRouters.isEmpty()) {
for (Router router : localRouters) {
try {
if (router.getUrl() == null ||
router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) {
invokers = router.route(invokers, getConsumerUrl(), invocation);
}
} catch (Throwable t) {
logger.error("Failed to execute router: " + getUrl() +
", cause: " + t.getMessage(), t);
}
}
}
return invokers;
}
RegistryDirectory.java
/**
* 获取 invoker 列表
*/
public List<Invoker<T>> doList(Invocation invocation) {
if (forbidden) {
throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "Forbid consumer " +
NetUtils.getLocalHost() + " access service " + getInterface().getName() +
" from registry " + getUrl().getAddress() + " use dubbo version " +
Version.getVersion() +
", Please check registry access list (whitelist/blacklist).");
}
List<Invoker<T>> invokers = null;
Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; //本地缓存
if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
String methodName = RpcUtils.getMethodName(invocation);
//根据方法名从本地缓存中获取invoker列表,此处略
//……
}
return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers;
} /**
* 节点变更通知
*/
public synchronized void notify(List<URL> urls) {
List<URL> invokerUrls = new ArrayList<URL>();
List<URL> routerUrls = new ArrayList<URL>();
List<URL> configuratorUrls = new ArrayList<URL>();
for (URL url : urls) {
String protocol = url.getProtocol();
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
if (Constants.ROUTERS_CATEGORY.equals(category)
|| Constants.ROUTE_PROTOCOL.equals(protocol)) {
routerUrls.add(url);
} else if (Constants.CONFIGURATORS_CATEGORY.equals(category)
|| Constants.OVERRIDE_PROTOCOL.equals(protocol)) {
configuratorUrls.add(url);
} else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
invokerUrls.add(url);
} else {
logger.warn("Unsupported category " + category + " in notified url: " +
url + " from registry " + getUrl().getAddress() +
" to consumer " + NetUtils.getLocalHost());
}
}
// configurators
if (configuratorUrls != null && configuratorUrls.size() >0 ){
this.configurators = toConfigurators(configuratorUrls);
}
// routers
if (routerUrls != null && routerUrls.size() >0 ){
List<Router> routers = toRouters(routerUrls);
if(routers != null){ // null - do nothing
setRouters(routers);
}
}
List<Configurator> localConfigurators = this.configurators; // local reference
// 合并override参数
this.overrideDirectoryUrl = directoryUrl;
if (localConfigurators != null && localConfigurators.size() > 0) {
for (Configurator configurator : localConfigurators) {
this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
}
}
// providers
refreshInvoker(invokerUrls);
} /**
* 根据invokerURL列表转换为invoker列表
*/
private void refreshInvoker(List<URL> invokerUrls){
//......
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls) ;// 将URL列表转成Invoker列表
//......
} /**
* 合并url参数 顺序为override > -D >Consumer > Provider
*/
private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
//......
URL url = mergeUrl(providerUrl);
//......
}
在 dolist() 方法中,如果通过服务治理禁止 Consumer 访问的话,此处直接抛出响应的异常。
RegistryDirectory 实现了 NotifyListener,在 ZK 节点变化时能收到通知更新内存缓存,其中 RegistryDirectory#mergeUrl() 方法中会按照优先级合并参数(动态配置在此处生效)。
服务引用时从内存缓存中获取并返回invoker列表,并根据路由规则再进行一次过滤。
2.3.3 Router
Router 的作用就是从 Directory 的 invoker 列表中刷选出符合路由规则的 invoker 子集。目前 Dubbo 提供了基于IP、应用名和协议等的静态路由功能,功能和实现比较简单,在此不做过多解释。
2.3.4 LoadBalance
通过 Direcotry 和 Router 之后,返回的是可用的 invoker 子集;在发起服务调用时,需要通过 LoadBalance 选择最终的一个目标 invoker。
在上面的 Cluster 章节中我们知道,调用时首先会经过 MockerClusterInvoker 拦截 Mock 设置,然后再根据配置调用实际的 Invoker(默认是 FailoverClusterInvoker)。
FailoverClusterInvoker 继承 AbstractClusterInvoker,在 AbstractClusterInvoker 中:
AbstractClusterInvoker.java
public Result invoke(final Invocation invocation) throws RpcException {
checkWheatherDestoried();
LoadBalance loadbalance;
List<Invoker<T>> invokers = list(invocation);
if (invokers != null && invokers.size() > 0) {
loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class)
.getExtension(invokers.get(0).getUrl()
.getMethodParameter(invocation.getMethodName(),
Constants.LOADBALANCE_KEY,
Constants.DEFAULT_LOADBALANCE));
} else {
loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class)
.getExtension(Constants.DEFAULT_LOADBALANCE);
}
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance);
}
FailoverClusterInvoker.java
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
List<Invoker<T>> copyinvokers = invokers;
checkInvokers(copyinvokers, invocation);
int len = getUrl().getMethodParameter(invocation.getMethodName(),
Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
if (len <= 0) {
len = 1;
}
// retry loop.
RpcException le = null; // last exception.
List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size());
Set<String> providers = new HashSet<String>(len);
for (int i = 0; i < len; i++) {
//重试时,进行重新选择,避免重试时invoker列表已发生变化.
//注意:如果列表发生了变化,那么invoked判断会失效,因为invoker示例已经改变
if (i > 0) {
checkWheatherDestoried();
copyinvokers = list(invocation);
//重新检查一下
checkInvokers(copyinvokers, invocation);
}
Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
invoked.add(invoker);
RpcContext.getContext().setInvokers((List)invoked);
try {
Result result = invoker.invoke(invocation);
if (le != null && logger.isWarnEnabled()) {
logger.warn("", le);
}
return result;
} catch (RpcException e) {
if (e.isBiz()) { // biz exception.
throw e;
}
le = e;
} catch (Throwable e) {
le = new RpcException(e.getMessage(), e);
} finally {
providers.add(invoker.getUrl().getAddress());
}
}
throw new RpcException();
}
在调用或重试时,每次都通过 LoadBalance 选出一个 Invoker 进行调用。
至此,调用流程结束。
Dubbo原理和源码解析之服务引用的更多相关文章
- Dubbo原理和源码解析之服务暴露
github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...
- Dubbo原理和源码解析之标签解析
一.Dubbo 配置方式 Dubbo 支持多种配置方式: XML 配置:基于 Spring 的 Schema 和 XML 扩展机制实现 属性配置:加载 classpath 根目录下的 dubbo.pr ...
- Dubbo原理和源码解析之“微内核+插件”机制
github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...
- Java 线程池架构原理和源码解析(ThreadPoolExecutor)
在前面介绍JUC的文章中,提到了关于线程池Execotors的创建介绍,在文章:<java之JUC系列-外部Tools>中第一部分有详细的说明,请参阅: 文章中其实说明了外部的使用方式,但 ...
- HashMap实现原理和源码解析
哈希表(hash table)也叫散列表,是一种非常重要的数据结构.许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中的对应实现HashMap的 ...
- JDK1.8的HashMap实现原理和源码解析
哈希表(hash table)也叫散列表,是一种非常重要的数据结构.许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中的对应实现HashMap的 ...
- 我是如何在短期内快速掌握Dubbo的原理和源码的(纯干货)?
写在前面 上周,在[Dubbo系列专题]中更新了两篇文章<冰河开始对Dubbo下手了!>和<俯瞰Dubbo全局,阅读源码前必须掌握这些!!>,收到了很多小伙伴的微信私聊消息,大 ...
- Go语言备忘录:net/http包的使用模式和源码解析
本文是晚辈对net/http包的一点浅显的理解,文中如有错误的地方请前辈们指出,以免误导! 转摘本文也请注明出处:Go语言备忘录:net/http包的使用模式和源码解析,多谢! 目录: 一.http ...
- Go语言备忘录(3):net/http包的使用模式和源码解析
本文是晚辈对net/http包的一点浅显的理解,文中如有错误的地方请前辈们指出,以免误导! 转摘本文也请注明出处:Go语言备忘录(3):net/http包的使用模式和源码解析,多谢! 目录: 一.h ...
随机推荐
- 刚收到一个吃瓜群众看了肯定不信的offer!
我教过了很多学生了,有的毕业后跟我依然保持联系,有的不知所踪,有的越混越好,有的没有什么变化,这让我不断思考,到底拉开人与人之间差距的是什么呢?
- 【转载】Session的生命周期
http://www.cnblogs.com/binger/archive/2013/03/19/2970171.html 以前在学习的时候没怎么注意,今天又回过头来仔细研究研究了一下Session的 ...
- C 四则运算表达式解析器
下载实例:http://www.wisdomdd.cn/Wisdom/resource/articleDetail.htm?resourceId=1074 程序主要包括:基础结构定义.词法分析.语法分 ...
- NewLife.Net——构建可靠的网络服务
网络程序应该注册成为系统服务,以保证其自启动以及稳定可靠运行! 这一场,讲讲怎么建立一个生产级别的网络服务. 老规矩,先上源码:https://github.com/nnhy/NewLife.Net. ...
- linux环境安装svn并进行多个源码库区分管理
关于svn的文档有很多大部分已Windows为例子,因公司没有Windows服务器经过一天的曲折终于初步安装了解了svn.下面一些经验希望能帮助新手 本文采用的yum安装(简单快速没必要源码) 1.y ...
- 如何优雅的关闭Java线程池
面试中经常会问到,创建一个线程池需要哪些参数啊,线程池的工作原理啊,却很少会问到线程池如何安全关闭的. 也正是因为大家不是很关注这块,即便是工作三四年的人,也会有因为线程池关闭不合理,导致应用无法正常 ...
- 3D Lut 电影级调色算法 附完整C代码
在前面的文章,我提到过VSCO Cam 的胶片滤镜算法实现是3d lut. 那么3d lut 到底是个什么东西呢? 或者说它是用来做什么的? 长话短说,3d lut(全称 : 3D Lookup t ...
- mysql 基础语法掌握~ This is just the beginning.
create database 数据库名; drop database 数据库名; use 数据库名; create table table_name ( column_name, column_ty ...
- URL 规范 整理
URL 规范 不用大写:(强制) 用中杠-不用下杠_:(强制) 参数列表要encode,编码使用utf-8:(强制) URI中的名词表示资源集合,使用复数形式.(建议) 增加版本号(建议) URI中统 ...
- Python_eval()
''' eval()用来把任意字符串转化为Python表达式并进行求值 ''' print(eval('3+4')) #计算表达式的值 a=3 b=4 print(eval('a+b')) #这时候要 ...