Dubbo源码学习之-服务导出

前言

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

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

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

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

一、服务导出的触发时机

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

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

二、配置项准备

既然有了入口,就顺着一路追溯下去。真正的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方法:

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

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

1、loadRegistries(true)方法

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

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

1.1 appendParameters方法

源码如下所示:

 1 // 第二个参数就是前面传入的配置类 ApplicationConfig、RegistryConfig等
 2     protected static void appendParameters(Map<String, String> parameters, Object config, String prefix) {
 3         if (config == null) {
 4             return;
 5         }
 6         // 通过反射得到所有的方法
 7         Method[] methods = config.getClass().getMethods();
 8         for (Method method : methods) {
 9             try {
10                 String name = method.getName();
11                 // 是get方法或is方法才进行赋值
12                 if (MethodUtils.isGetter(method)) {
13                     Parameter parameter = method.getAnnotation(Parameter.class);
14                     // 返回值是当前配置类,或者此方法有Parameter注解且excluded参数为true,此时不往map中赋值
15                     if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
16                         continue;
17                     }
18                     String key;
19                     if (parameter != null && parameter.key().length() > 0) {
20                         // 如果Parameter注解中的key不为空,则将它作为往map中put的key
21                         key = parameter.key();
22                     } else {
23                         // 否则截取get/is后面的字符串,转成application.version格式的key
24                         key = calculatePropertyFromGetter(name);
25                     }
26                     // 通过反射获取value
27                     Object value = method.invoke(config);
28                     String str = String.valueOf(value).trim();
29                     if (value != null && str.length() > 0) {
30                         if (parameter != null && parameter.escaped()) {
31                             // 转成utf-8格式
32                             str = URL.encode(str);
33                         }
34                         // 注解中有可拼接配置,则在value前面拼接默认值
35                         if (parameter != null && parameter.append()) {
36                             String pre = parameters.get(DEFAULT_KEY + "." + key);
37                             if (pre != null && pre.length() > 0) {
38                                 str = pre + "," + str;
39                             }
40                             pre = parameters.get(key);
41                             if (pre != null && pre.length() > 0) {
42                                 str = pre + "," + str;
43                             }
44                         }
45                         // 如果有前缀则在key上加前缀
46                         if (prefix != null && prefix.length() > 0) {
47                             key = prefix + "." + key;
48                         }
49                         // 放入map中
50                         parameters.put(key, str);
51                     } else if (parameter != null && parameter.required()) {
52                         throw new IllegalStateException(config.getClass().getSimpleName() + "." + key + " == null");
53                     }
54                     // 如果是getParameters方法
55                 } else if ("getParameters".equals(name)
56                         && Modifier.isPublic(method.getModifiers())
57                         && method.getParameterTypes().length == 0
58                         && method.getReturnType() == Map.class) {
59                     Map<String, String> map = (Map<String, String>) method.invoke(config, new Object[0]);
60                     if (map != null && map.size() > 0) {
61                         String pre = (prefix != null && prefix.length() > 0 ? prefix + "." : "");
62                         // 重新放入parameters中
63                         for (Map.Entry<String, String> entry : map.entrySet()) {
64                             parameters.put(pre + entry.getKey().replace(‘-‘, ‘.‘), entry.getValue());
65                         }
66                     }
67                 }
68             } catch (Exception e) {
69                 throw new IllegalStateException(e.getMessage(), e);
70             }
71         }
72     }

此方法的作用就是获取传入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方法的后面,进行了导出的触发。分析如下:

1 String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
2 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方法进行导出。

 1 if (CollectionUtils.isNotEmpty(registryURLs)) {
 2     for (URL registryURL : registryURLs) {
 3         // 若干无关代码
 4         // 通过代理工厂生成invoker
 5         Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
 6         // 用于持有invoker和ServiceConfig
 7         DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
 8         // 进行服务导出
 9         Exporter<?> exporter = protocol.export(wrapperInvoker);
10         exporters.add(exporter);
11     }
12 } else {
13     Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
14     DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
15
16     Exporter<?> exporter = protocol.export(wrapperInvoker);
17     exporters.add(exporter);
18}

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方法

 1 public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
 2         URL registryUrl = getRegistryUrl(originInvoker);
 3         // url to export locally
 4         URL providerUrl = getProviderUrl(originInvoker);
 5         //  subscription information to cover.
 6         final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
 7         final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
 8         overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
 9
10         providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
11         //export invoker
12         // *** 导出是指,创建可供客户端通讯的nettyClient,并启动
13         final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
14
15         // url to registry
16         final Registry registry = getRegistry(originInvoker);
17         final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
18         ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
19                 registryUrl, registeredProviderUrl);
20         //to judge if we need to delay publish
21         boolean register = registeredProviderUrl.getParameter("register", true);
22         if (register) {
23             // *** 将服务注册到注册中心
24             register(registryUrl, registeredProviderUrl);
25             providerInvokerWrapper.setReg(true);
26         }
27
28         // Deprecated! Subscribe to override rules in 2.6.x or before.
29         registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
30
31         exporter.setRegisterUrl(registeredProviderUrl);
32         exporter.setSubscribeUrl(overrideSubscribeUrl);
33         //Ensure that a new exporter instance is returned every time export
34         return new DestroyableExporter<>(exporter);
35     }

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

3、DubboProtocol中的export方法

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

 1  public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
 2         URL url = invoker.getUrl();
 3
 4         // export service.
 5         String key = serviceKey(url);
 6         DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
 7         exporterMap.put(key, exporter);
 8
 9         //export an stub service for dispatching event
10         Boolean isStubSupportEvent = url.getParameter(STUB_EVENT_KEY, DEFAULT_STUB_EVENT);
11         Boolean isCallbackservice = url.getParameter(IS_CALLBACK_SERVICE, false);
12         if (isStubSupportEvent && !isCallbackservice) {
13             String stubServiceMethods = url.getParameter(STUB_EVENT_METHODS_KEY);
14             if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
15                 if (logger.isWarnEnabled()) {
16                     logger.warn(new IllegalStateException("consumer [" + url.getParameter(INTERFACE_KEY) +
17                             "], has set stubproxy support event ,but no stub methods founded."));
18                 }
19
20             } else {
21                 stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
22             }
23         }
24         // 打开服务器,为什么是打开服务器?因为打开了服务端Client,消费者就可以与其进行通讯的交互了
25         openServer(url);
26         optimizeSerialization(url);//序列化的优化
27
28         return exporter;
29     }

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

总结

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

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

原文地址:https://www.cnblogs.com/zzq6032010/p/11275478.html

时间: 2024-11-05 17:36:10

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

dubbo源码阅读之服务导出

dubbo服务导出 常见的使用dubbo的方式就是通过spring配置文件进行配置.例如下面这样 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns

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

相关文章: Dubbo源码学习文章目录 前言 主要是前一阵子换了工作,第一个任务就是解决目前团队在 Dubbo 停机时产生的问题,同时最近又看了一下 Dubbo 的源码,想重新写一下 Dubbo 相关的文章. 优雅停机原理 对于一个 java 应用,如果想在关闭应用时,执行一些释放资源的操作一般是通过注册一个 ShutDownHook ,当关闭应用时,不是调用 kill -9 命令来直接终止应用,而是通过调用 kill -15 命令来触发这个 ShutDownHook 进行停机前的释放资源操作.

Dubbo源码学习(二)

@Adaptive注解 在上一篇ExtensionLoader的博客中记录了,有两种扩展点,一种是普通的扩展实现,另一种就是自适应的扩展点,即@Adaptive注解的实现类. @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Adaptive { String[] value() default {}; } @Adapt

dubbo源码学习(四)初始化过程细节:解析服务

初学dubbo的源码,只做尝试性的去学习,做为自己学习的一个记录,各位看官如果觉得写的有错误或理解的不对,请在留言区告诉我,互相学习.本人能力有限,有大神进入 时请指点. 前面大概介绍了一下关于学习dubbo源码的一些基本知识,今天将真正去看dubbo内部的实现过程,看dubbo的源码前我先把dubbo的用户指南和开发指指南大概的看了一遍,然后从上面找到相应的切入点去看源码,今天将介绍的是dubbo的初始化解析bean的过程.从之前使用过dubbo一些经验,加上http://dubbo.io/的

dubbo源码学习(五)dubbo暴露服务的过程

初学dubbo的源码,只做尝试性的去学习,做为自己学习的一个记录,各位看官如果觉得写的有错误或理解的不对,请在留言区告诉我,互相学习.本人能力有限,有大神进入 时请指点. dubbo采用的nio异步的通信,通信协议默认为 netty,当然也可以选择 mina,grizzy.在服务端(provider)在启动时主要是开启netty监听,在zookeeper上注册服务节点,处理消费者请求,返回处理后的消息给消费者,消费者使用服务时主要是订阅服务的节点,监听zookeeper节点目录,服务端的变化时z

Dubbo源码学习之-Adaptive自适应扩展

前言 最近三周基本处于9-10-6与9-10-7之间,忙碌的节奏机会丢失了自己.除了之前干施工的那段经历,只看参加软件开发以来,前段时间是最繁忙的了.忙的原因,不是要完成的工作量大,而是各种环境问题,各种沟通协调问题.从这个项目,我是体会到了人一多,花在沟通协调上的成本真的会不成比例的放大,制度好,再加上协调好,会极大的提高整体工作效率.怪不得当年华为跟IBM学完工作组织管理制度之后能爆发出如此强劲的战斗力.从另一个角度,也能发觉出为什么大公司招人都比较注重员工的个人实力与团队协作能力,因为如果

Dubbo源码分析系列-服务的发布

RPC简化类图 RPC模块核心接口和抽象实现 默认实现Dubbo协议的接口和抽象实现 服务发布过程 调用过程 上图是服务提供者暴露服务的主过程: 首先ServiceConfig类拿到对外提供服务的实际类ref(如:HelloWorldImpl),然后通过ProxyFactory类的getInvoker方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成具体服务到Invoker的转化.接下来就是Invoker转换到Exporter的过程. Dubbo处理服务暴露的关键

dubbo源码阅读笔记--服务调用时序

上接dubbo源码阅读笔记--暴露服务时序,继续梳理服务调用时序,下图右面红线流程. 整理了调用时序图 分为3步,connect,decode,invoke. 连接 AllChannelHandler.connected(Channel) line: 38 HeartbeatHandler.connected(Channel) line: 47 MultiMessageHandler(AbstractChannelHandlerDelegate).connected(Channel) line:

Dubbo源码学习--服务发布(ProxyFactory、Invoker)

上文分析了Dubbo服务发布的整体流程,但服务代理生成的具体细节介绍得还不是很详细.下面将会接着上文继续分析.上文介绍了服务代理生成的切入点,如下: Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url); 这里的proxyFactory是在ServiceConfig中定义的,是final类型静态变量,赋值后无法进行修改.如下: private static final ProxyFactor