前言
忙的时候,会埋怨学习的时间太少,缺少个人的空间,于是会争分夺秒的工作、学习。而一旦繁忙的时候过去,有时间了之后,整个人又会不自觉的陷入一种懒散的状态中,时间也显得不那么重要了,随便就可以浪费掉几个小时。可见普通人的学习之路要主动地去克服掉很多阻碍,最主要的阻碍还是来自于自身,周期性的不想学习、不自觉的懒散、浅尝辄止的态度、好高骛远贪多的盲目...哎,学习之路,还是要时刻提醒自己,需勤勉致知。
闲话少叙,今天的学习目标是要尽量的了解清楚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配置文件为例。
播放完插曲,继续往下追溯export方法 ServiceConfig中的 doExportUrls方法:
1 private void doExportUrls() { 2 // 1、Dubbo支持多协议多注册中心 3 ListregistryURLs = 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 ListloadRegistries(boolean provider) { 2 // check && override if necessary 3 List registryList = new ArrayList (); 4 // 此处registries类型为List ,是解析配置文件时完成的注入,类似于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 Mapmap = new HashMap (); 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 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(Mapparameters, 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 map = (Map ) 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 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
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 publicExporter export(final Invoker 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 exporter = doLocalExport(originInvoker, providerUrl); 14 15 // url to registry 16 final Registry registry = getRegistry(originInvoker); 17 final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl); 18 ProviderInvokerWrapper 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 publicExporter export(Invoker invoker) throws RpcException { 2 URL url = invoker.getUrl(); 3 4 // export service. 5 String key = serviceKey(url); 6 DubboExporter exporter = new DubboExporter (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了,感觉繁琐且陌生。源码阅读,还是要找到一个比较好的框架慢慢研读下去,勿好高骛远,勿心急气躁。一时看不懂的地方,就多研究一下,实在不行则先放放,过两天重看的时候就会有不一样的灵感,一般都很容易就领悟了,如若还不行,上网看看大神们的解读也能豁然开朗。
总之技术的提升没有什么捷径,需要多学习多积累多思考。与各位共勉!