前言

忙的时候,会埋怨学习的时间太少,缺少个人的空间,于是会争分夺秒的工作、学习。而一旦繁忙的时候过去,有时间了之后,整个人又会不自觉的陷入一种懒散的状态中,时间也显得不那么重要了,随便就可以浪费掉几个小时。可见普通人的学习之路要主动地去克服掉很多阻碍,最主要的阻碍还是来自于自身,周期性的不想学习、不自觉的懒散、浅尝辄止的态度、好高骛远贪多的盲目...哎,学习之路,还是要时刻提醒自己,需勤勉致知。

闲话少叙,今天的学习目标是要尽量的了解清楚Dubbo框架中的服务导出功能,先附上Dubbo官网上的一个框架图

本文要说的服务导出,指的就是上图中的动作1-register,即将配置文件/类中服务提供者的信息在注册中心完成注册,以便后续的消费者能发现服务,此外,开放NettyClient用于与消费者通讯。

下面会从服务导出的触发时机、配置项准备、开放通讯client并注册三部分进行服务导出流程的了解。

一、服务导出的触发时机

Dubbo是一个可以完全兼容Spring的服务治理框架,兼容性体现在何处?一是Dubbo的配置可以做到对Spring配置无侵入;二是Dubbo默认是随着Spring容器的启动而启动的,不需要人为的控制。帮助Dubbo实现第二项的类有两个:ReferenceBean和ServiceBean,这两个类在Dubbo中担任了连接Spring的桥梁的作用。与本文相关的类是ServiceBean,此类实现了ApplicationListener接口,这样Spring框架在执行refresh方法完成容器初始化的时候,就会发布容器刷新事件,调用ServiceBean的重写方法onApplicationEvent,触发服务导出。

 public void onApplicationEvent(ContextRefreshedEvent event) {
// 判断是否已经导出 或者是否不应该导出
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
export();
}
}

二、配置项准备

既然有了入口,就顺着一路追溯下去。真正的export导出方法,在ServiceConfig类中。此时需要说明一下Dubbo中的配置类与实际配置项之间的对应关系。此处以Dubbo的xml配置文件为例。

<dubbo:service interface = "com.test.dubboservice.ITestService" ref = "testService" version="1.0"/>  对应 ServiceConfig类,

<dubbo:provider delay="-1" timeout="${DUBBO_TIMEOUT}" retries="0" token="test_demo"/>  对应 ProviderConfig类,此类与ServiceConfig同属于AbstractServiceConfig的实现类

<dubbo:application name = "dubbo_provider"/> 对应ApplicationConfig类,

<dubbo:registry address = "zookeeper://192/168.16.218:10110" check="false" subscribe="false" register=""/> 对应RegistryConfig类,

<dubbo:protocol name = "dubbo"/>  对应ProtocolConfig类,此外监控中心对应的类是 MonitorConfig

播放完插曲,继续往下追溯export方法 ServiceConfig中的 doExportUrls方法:

 private void doExportUrls() {
// 1、Dubbo支持多协议多注册中心
List<URL> registryURLs = loadRegistries(true);
///2、多协议导出服务,向多注册中心注册服务
for (ProtocolConfig protocolConfig : protocols) {
String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
ApplicationModel.initProviderModel(pathKey, providerModel);
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}

此方法分为两步,第一步是获取配置的注册中心生成的URL,第二步是通过不同协议进行注册。一步一步的看,先看loadRegistries方法。

1、loadRegistries(true)方法

 protected List<URL> loadRegistries(boolean provider) {
// check && override if necessary
List<URL> registryList = new ArrayList<URL>();
// 此处registries类型为List<RegistryConfig>,是解析配置文件时完成的注入,类似于Spring对配置文件的解析
if (CollectionUtils.isNotEmpty(registries)) {
for (RegistryConfig config : registries) {
String address = config.getAddress();
if (StringUtils.isEmpty(address)) {
address = ANYHOST_VALUE;
}
// 判断当前注册中心的地址是否为可用地址,不可用地址格式为 N/A
if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
Map<String, String> map = new HashMap<String, String>();
// !!!此方法极为重要,在拼接URL时经常会看到,是重点需要理解的方法
appendParameters(map, application);
appendParameters(map, config);
map.put(PATH_KEY, RegistryService.class.getName());
appendRuntimeParameters(map);
// 如果注册中心配置中没有配置protocol,则默认用dubbo
if (!map.containsKey(PROTOCOL_KEY)) {
map.put(PROTOCOL_KEY, DUBBO_PROTOCOL);
}
List<URL> urls = UrlUtils.parseURLs(address, map);
// 重新构建URL
for (URL url : urls) {
url = URLBuilder.from(url)
.addParameter(REGISTRY_KEY, url.getProtocol())
.setProtocol(REGISTRY_PROTOCOL)
.build();
// 满足两个条件会往里添加:1、是提供者且有配置注册中心;2、不是提供者但是配置了订阅
if ((provider && url.getParameter(REGISTER_KEY, true))
|| (!provider && url.getParameter(SUBSCRIBE_KEY, true))) {
registryList.add(url);
}
}
}
}
}
return registryList;
}

此方法逻辑不难理解,先是判断如果为空则设置address的默认值为127的本地地址,再是组装URL参数,最后重新build一下,判断是否添加,最终返回。只是有三个地方需要搞清楚,一个是appendParameters方法的原理及作用,一个是UrlUtils.parseURLs方法的原理,最后是26-29行代码的作用以及为什么还要重新创建(直接将23行的urls返回不行吗?)。下面一个个的来。

1.1 appendParameters方法

源码如下所示:

 // 第二个参数就是前面传入的配置类 ApplicationConfig、RegistryConfig等
protected static void appendParameters(Map<String, String> parameters, Object config, String prefix) {
if (config == null) {
return;
}
// 通过反射得到所有的方法
Method[] methods = config.getClass().getMethods();
for (Method method : methods) {
try {
String name = method.getName();
// 是get方法或is方法才进行赋值
if (MethodUtils.isGetter(method)) {
Parameter parameter = method.getAnnotation(Parameter.class);
// 返回值是当前配置类,或者此方法有Parameter注解且excluded参数为true,此时不往map中赋值
if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
continue;
}
String key;
if (parameter != null && parameter.key().length() > 0) {
// 如果Parameter注解中的key不为空,则将它作为往map中put的key
key = parameter.key();
} else {
// 否则截取get/is后面的字符串,转成application.version格式的key
key = calculatePropertyFromGetter(name);
}
// 通过反射获取value
Object value = method.invoke(config);
String str = String.valueOf(value).trim();
if (value != null && str.length() > 0) {
if (parameter != null && parameter.escaped()) {
// 转成utf-8格式
str = URL.encode(str);
}
// 注解中有可拼接配置,则在value前面拼接默认值
if (parameter != null && parameter.append()) {
String pre = parameters.get(DEFAULT_KEY + "." + key);
if (pre != null && pre.length() > 0) {
str = pre + "," + str;
}
pre = parameters.get(key);
if (pre != null && pre.length() > 0) {
str = pre + "," + str;
}
}
// 如果有前缀则在key上加前缀
if (prefix != null && prefix.length() > 0) {
key = prefix + "." + key;
}
// 放入map中
parameters.put(key, str);
} else if (parameter != null && parameter.required()) {
throw new IllegalStateException(config.getClass().getSimpleName() + "." + key + " == null");
}
// 如果是getParameters方法
} else if ("getParameters".equals(name)
&& Modifier.isPublic(method.getModifiers())
&& method.getParameterTypes().length == 0
&& method.getReturnType() == Map.class) {
Map<String, String> map = (Map<String, String>) method.invoke(config, new Object[0]);
if (map != null && map.size() > 0) {
String pre = (prefix != null && prefix.length() > 0 ? prefix + "." : "");
// 重新放入parameters中
for (Map.Entry<String, String> entry : map.entrySet()) {
parameters.put(pre + entry.getKey().replace('-', '.'), entry.getValue());
}
}
}
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
}

此方法的作用就是获取传入Object的get/is方法,将get/is后面的属性名跟返回的value值作为key-value放入map中,此外该方法还兼容了Dubbo本身的配置项功能。

 1.2 UrlUtils.parseURLs方法

此方法最终调用了org.apache.dubbo.common.utils.UrlUtils#parseURL方法,由于代码较长,就不贴出来了。

方法中做的事情为:拆分address,如果配置了多个注册中心地址,则每个地址对应一个URL,然后组装URL的七大基本参数 protocol, username, password, host, port, path, parameters,最后new一个URL并返回。

1.3 26-29行代码的作用

将registry、protocol的值作为key、value放入parameters中,将protocol设置为registry,然后用新七项信息创建一个新的URL,此时的URL就被标记为registry的URL了。

2、遍历协议配置,用每个协议配置往注册中心进行服务导出

其中pathkey字段,获取到的就是要导出的服务的接口全路径名,然后将ProviderModel放入map中缓存起来,最后再调用导出方法。

三、服务导出并注册

此部分代码较长,逻辑复杂,故只贴一下关键地方的代码,需结合代码一起看。

1、服务导出的触发

在ServiceConfig类的上述doExportUrlsFor1Protocol方法的后面,进行了导出的触发。分析如下:

 String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = this.findConfigedPorts(protocolConfig, name, map);

上面2.1中获取到的注册中心URL用于提供host地址,在获取到String protocol, String host, int port, String path, Map<String, String> parameters这五项信息后,new一个URL。然后通过proxyFactory(通过SPI机制生成的代理类)来生成invoker对象,包装成一个包装类,然后调用通过SPI机制生成的protocol代理类的export方法进行导出。

 if (CollectionUtils.isNotEmpty(registryURLs)) {
for (URL registryURL : registryURLs) {
// 若干无关代码
// 通过代理工厂生成invoker
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
// 用于持有invoker和ServiceConfig
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 进行服务导出
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}

Dubbo中一般都是这个套路,先将URL等准备好的对象转换成一个Invoker,再将Invoker对象传入目标方法完成特定功能。方便理解,可顾名思义,将Invoker看成一个调用体的抽象。

此处需要说明一点的是上述的if-else分支,if中的逻辑是当存在注册中心时的处理方式,即向注册中心注册,并暴露服务供调用;而else中的逻辑,是不存在注册中心时,只暴露服务。他们二者功能区别的实现,在于第5行跟第13行,getInvoker方法的第三个参数不同,一个是注册中心的URL中带有服务的URL,一个只是服务的URL。前者通过SPI的代理工厂类调用的是RegistryProtocol类,因为RegistryURL的protocol属性为registry;而后者通过代理工厂类调用的是DubboProtocol等具体的协议类,因为此URL的protocol属性是取得配置属性,默认dubbo协议。由此可见,以URL为媒介的配置类与SPI机制结合时的灵活性是很大的。

2、RegistryProtocol中的export方法

 public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
URL registryUrl = getRegistryUrl(originInvoker);
// url to export locally
URL providerUrl = getProviderUrl(originInvoker);
// 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
// *** 导出是指,创建可供客户端通讯的nettyClient,并启动
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl); // url to registry
final Registry registry = getRegistry(originInvoker);
final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
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(registryUrl, registeredProviderUrl);
providerInvokerWrapper.setReg(true);
} // Deprecated! Subscribe to override rules in 2.6.x or before.
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
//Ensure that a new exporter instance is returned every time export
return new DestroyableExporter<>(exporter);
}

此方法中最重要的两个方法我用*号打了标记,其中前者最终执行的是DubboProtocol等类中的export方法,即启动服务端的通讯Client,以进行后续的通讯调用,此方法下面再看。后者register方法的作用,即将服务注册到注册中心。不同的注册中心,注册的实现是不一样的,比如如果用的是ZooKeeper注册,则此处是调用ZooKeeper的相关API,在对应路径上创建节点,一个节点对应一个服务。

3、DubboProtocol中的export方法

此处以默认协议DubboProtocol为例,看它的export方法。

  public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl(); // export service.
String key = serviceKey(url);
DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
exporterMap.put(key, exporter); //export an stub service for dispatching event
Boolean isStubSupportEvent = url.getParameter(STUB_EVENT_KEY, DEFAULT_STUB_EVENT);
Boolean isCallbackservice = url.getParameter(IS_CALLBACK_SERVICE, false);
if (isStubSupportEvent && !isCallbackservice) {
String stubServiceMethods = url.getParameter(STUB_EVENT_METHODS_KEY);
if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
if (logger.isWarnEnabled()) {
logger.warn(new IllegalStateException("consumer [" + url.getParameter(INTERFACE_KEY) +
"], has set stubproxy support event ,but no stub methods founded."));
} } else {
stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
}
}
// 打开服务器,为什么是打开服务器?因为打开了服务端Client,消费者就可以与其进行通讯的交互了
openServer(url);
optimizeSerialization(url);//序列化的优化 return exporter;
}

此方法最主要的就是openServer方法,追踪下去你会发现,最终创建了某Client并启动(默认是NettyClient,此外还有MinaClient、GrizzlyClient),等待消息的传入。

总结

Dubbo中服务导出的流程,基本如上所述。3.2和3.3中由于有多层类调用代码量较多,所以未详细跟进流程,不过相信以道友们扎实的源码阅读功底,应该不难追溯下去,追溯到最后面就是封装的与其他组件交互的api了,感觉繁琐且陌生。源码阅读,还是要找到一个比较好的框架慢慢研读下去,勿好高骛远,勿心急气躁。一时看不懂的地方,就多研究一下,实在不行则先放放,过两天重看的时候就会有不一样的灵感,一般都很容易就领悟了,如若还不行,上网看看大神们的解读也能豁然开朗。

总之技术的提升没有什么捷径,需要多学习多积累多思考。与各位共勉!

Dubbo源码学习之-服务导出的更多相关文章

  1. dubbo 源码学习1 服务发布机制

    1.源码版本:2.6.1 源码demo中采用的是xml式的发布方式,在dubbo的 DubboNamespaceHandler 中定义了Spring Framework 的扩展标签,即 <dub ...

  2. dubbo源码阅读之服务导出

    dubbo服务导出 常见的使用dubbo的方式就是通过spring配置文件进行配置.例如下面这样 <?xml version="1.0" encoding="UTF ...

  3. Dubbo源码学习--服务是如何引用的

    ReferenceBean 跟服务引用一样,Dubbo的reference配置会被转成ReferenceBean类,ReferenceBean实现了InitializingBean接口,直接看afte ...

  4. Dubbo源码学习--服务是如何发布的

    相关文章: Dubbo源码学习--服务是如何发布的 Dubbo源码学习--服务是如何引用的 ServiceBean ServiceBean 实现ApplicationListener接口监听Conte ...

  5. Dubbo源码学习--注册中心分析

    相关文章: Dubbo源码学习--服务是如何发布的 Dubbo源码学习--服务是如何引用的 注册中心 关于注册中心,Dubbo提供了多个实现方式,有比较成熟的使用zookeeper 和 redis 的 ...

  6. Dubbo源码学习--集群负载均衡算法的实现

    相关文章: Dubbo源码学习文章目录 前言 Dubbo 的定位是分布式服务框架,为了避免单点压力过大,服务的提供者通常部署多台,如何从服务提供者集群中选取一个进行调用, 就依赖Dubbo的负载均衡策 ...

  7. Dubbo源码学习文章目录

    目录 Dubbo源码学习--服务是如何发布的 Dubbo源码学习--服务是如何引用的 Dubbo源码学习--注册中心分析 Dubbo源码学习--集群负载均衡算法的实现

  8. Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题

    Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题 相关文章: Dubbo源码学习文章目录 前言 主要是前一阵子换了工作,第一个任务就是解决目前团队在 Dubbo 停机时产生的问题 ...

  9. Dubbo源码学习(二)

    @Adaptive注解 在上一篇ExtensionLoader的博客中记录了,有两种扩展点,一种是普通的扩展实现,另一种就是自适应的扩展点,即@Adaptive注解的实现类. @Documented ...

随机推荐

  1. java代码书写易犯错误

    java代码书写易犯错误: 常见报错: 控制台报错: 找不到或无法加载主类 HelloWorld 原因: java.lang.NoClassDefFoundError: cn/itcast/day01 ...

  2. C语言:正负数之间取模运算(转载)

    如果 % 两边的操作数都为正数,则结果为正数或零:如果 % 两边的操作数都是负数,则结果为负数或零.C99 以前,并没有规定如果操作数中有一方为负数,模除的结果会是什么.C99 规定,如果 % 左边的 ...

  3. kafka入门(二)分区和group

    topic 在kafka中消息是按照topic进行分类的:每条发布到Kafka集群的消息都有一个类别,这个类别被称为topic parition 一个topic可以配置几个parition,每一个分区 ...

  4. HihoCoder 1496:寻找最大值(思维DP)

    http://hihocoder.com/problemset/problem/1496 题意:中文. 思路:一开始做有一种想法,把所有的数都变成二进制后,最优的情况肯定是挑选所有数中最高位的1能同时 ...

  5. Azkaban Flow 2.0 使用简介

    官方建议使用Flow 2.0来创建Azkaban工作流,且Flow 1.0将被弃用 目录 目录 一.简单的Flow 1. 新建 flow20.project 文件 2. 新建 .flow 文件 3. ...

  6. SQL Server Update 链接修改和when的应用

    一.自链接方式 update b1 set b1.money = b1.money + b2.money from (select * from wallet where type='余额') b1 ...

  7. Node热部署插件

    一.supervisor 首先需要使用 npm 安装 supervisor(这里需要注意一点,supervisor必须安装到全局) $ npm install -g supervisor Linux ...

  8. Bzoj 3131 [Sdoi2013]淘金 题解

    3131: [Sdoi2013]淘金 Time Limit: 30 Sec  Memory Limit: 256 MBSubmit: 733  Solved: 363[Submit][Status][ ...

  9. C语言指针专题——如何理解指针

    本文为原创,欢迎转发! 最近在研读C primer plus 5版中文版,老外写的,还是很经典的,推荐给读者们,有需要的朋友可以在这里购买:C primer plus 5版中文版 指针,传说中是C语言 ...

  10. form 利用BeginCollectionItem提交集合List<T>数据 以及提交的集合中含有集合的数据类型 如List<List<T>> 数据的解决方案

    例子: public class IssArgs { public List<IssTabArgs> Tabs { get; set; } } public class IssTabArg ...