一起玩Dubbo,万字长文揭秘服务暴露

开发 项目管理
日常写组件,最近又接了一个需求,让我负责实现一个rpc组件,提高公司游戏跨服开发的效率,为了写好这个组件,算是将dubbo里里外外研究了一波,目前组件的实现也接近尾声了,因此打算给dubbo的学习做个总结。

[[402876]]

日常写组件,最近又接了一个需求,让我负责实现一个rpc组件,提高公司游戏跨服开发的效率,为了写好这个组件,算是将dubbo里里外外研究了一波,目前组件的实现也接近尾声了,因此打算给dubbo的学习做个总结,并穿插说说rpc实现的心路历程,同样需要实现rpc的朋友,或者对dubbo有兴趣的朋友可以关注这个系列。

在写rpc组件之前,我先提了几个灵魂疑问,并从dubbo中找到了答案。

  • 服务是啥?

一个模块,一种玩法,只要是需要进行远程调度的都可以用服务的概念进行包装,我这边简单包装了一个副本服务,类情况如下:

 

平平无奇,等等,我们来看看提供方如何标记服务

 

到了这一步,服务已经完成了基本定义。

  • 服务最终被注册到了那里?

在xml配置上,我们已经看到了有注册中心的配置

 

没错,最后提供方定义好的服务会注册到注册中心,目前支持的类型有多种

 

具体可以查看里边提供的demo实例,那么注册中心有什么作用呢?

简单点描述就是注册中心就是管理服务的地方,提供方将服务放到了这个管理处,而订阅方要用的话则从这个管理处将服务拿过来用,通过注册中心实现了服务的感知。

  • 服务谁来消费?

消费方来使用,我们可以看到

 

同样也是平平无奇的代码,就是消费方拿到boss接口后,直接调用对应接口即可。

对应提供方有xml去定义服务的注册,同样消费方也是有xml去定义服务的订阅信息,可以看到

 

简单来说就是,提供方将服务放到注册中心,订阅方从注册中心拿来用。

接下来会涉及到源码部分,以下源码的示例接来自dubbo2.6x,源码方面的注释都已经提交到github上,有需要的可以clone:

https://github.com/wiatingpub/dubbo/tree/2.6.x

什么时候触发的服务暴露

在设计rpc组件的时候,不得不面对这个问题,本着抄dubbo的想法,研究了下dubbo的实现方案

dubbo采用了比较经典的xml配置,并理所当然的使用了NamespaceHandlerSupport将xml中的节点配置映射成了对应对象

可以看到在dubbo-config-spring包底下有个spring.handlers的配置,通过该配置指定了DubboNamespaceHandler

 

DubboNamespaceHandler会将xml配置对应标签的配置映射成对象,比如service

 

 

看看ServiceBean在映射成对象后做了啥,先看看ServiceBean结构

 

自身是一个监听器,再通过CTRL+F12看看有哪些方法

 

看到export暴露这个方法后,ALT+F7反调下发现除了注解Annoatition外有两个地方调用,分别是

 

第一种是在属性被设置后调用,可以看到如果是延迟函数则不会调用。

 

第二种是看到isDelay的时候才会调用export,也就是说延迟暴露的服务是在监听到ContextRefreshedEvent事件后进行调用的。

在export方法内可以看到

 

可以针对不同的服务配置配置delay延迟时间,具体的肯定是在xml上配置了。

触发机制到这里基本就结束了,总结一下dubbo的触发机制就是建立在NamespaceHandlerSupport上,将xml中的标签实例化,并通过在afterPropertiesSet或者在监听到Spring容器抛出的容器刷新事件后,触发服务的暴露。

画个流程图总结下

 

由于我司这边的服务配置最终落地在使用yaml方案上,不引入xml,最终我并没有使用NamespaceHandlerSupport去实例化,而是模仿dubbo3.0的方案包装了一个ServiceBootstrap对象,依赖SmartLifeCycle的生命周期,在start的时候取到yaml的配置,遍历进行服务暴露。dubbo3.0做了比较大调整,后续会专门讲,有兴趣的持续关注该系列。

提一波URL

在说服务暴露之前必须先提一波URL,否则主线没了,后续不好讲。

在我没有接触到dubbo之前,我对URL的定位是指网络地址,而在dubbo中,可以认为是一种约定,几乎dubbo的所有模块都是通过URL来传参,这有什么好处呢?

我们可以想想,如果没有约定好,那么不同的接口之间进行交互的参数便会乱掉,一会是字符串,一会是map,而有了统一的约定后,代码便会更加的规范和统一,我们在看代码的时候也会比较清晰,也容易拓展,比如如果你想拓展什么东西,直接往URL上拼接参数就可以了。

我们可以看到,除了几个基础的参数外,很多参数其实最终都放到了parameters中。

而在我司项目中,我们参考了URL的设计,构建了元数据的结构,也就是map,将服务的部分动态参数通过map进行传递。

服务暴露过程

在深入源码之前先大概总结下服务暴露的几个步骤,分别是:

  1. 配置的构建、合并、检查。
  2. URL的组装。
  3. 服务的暴露、注册。

我将这三个主要的过程放入流程图内

继续跟进服务暴露的具体逻辑,也就是doExport后

  1. protected synchronized void doExport() { 
  2.     if (unexported) { 
  3.         throw new IllegalStateException("Already unexported!"); 
  4.     } 
  5.     if (exported) { 
  6.         return
  7.     } 
  8.     exported = true
  9.     if (interfaceName == null || interfaceName.length() == 0) { 
  10.         throw new IllegalStateException("<dubbo:service interface=\"\" /> interface not allow null!"); 
  11.     } 
  12.     // TODO: 2021/5/27 检查provider是否为空,为空则创建一个,并通过系统变量为其初始化 
  13.     checkDefault(); 
  14.  
  15.     /** 各种初始值的设置 **/ 
  16.      
  17.     // TODO: 2021/5/27 检查Application是否为空 
  18.     checkApplication(); 
  19.     // TODO: 2021/5/27 检查注册中心是否为空 
  20.     checkRegistry(); 
  21.     // TODO: 2021/5/27 检查protocols是否为空 
  22.     checkProtocol(); 
  23.     // TODO: 2021/5/27 补充各种参数 
  24.     appendProperties(this); 
  25.     // TODO: 2021/5/27 Stub合法性检查 
  26.     checkStub(interfaceClass); 
  27.     // TODO: 2021/5/27 mock合法性检查 
  28.     checkMock(interfaceClass); 
  29.     if (path == null || path.length() == 0) { 
  30.         path = interfaceName; 
  31.     } 
  32.     // TODO: 2021/5/27 多协议多注册中心暴露服务 
  33.     doExportUrls(); 
  34.     ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), this, ref); 
  35.     ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel); 

总结下来不外乎两步:

  • 对各类配置进行校验,并且更新部分配置;
  • 多协议多注册中心暴露服务;

其中检查的细节暂时不铺开,因为服务暴露整个过程才是重点,后续服务治理了再重新讲这块,接下来继续讲重点doExportUrls方法

  1. @SuppressWarnings({"unchecked""rawtypes"}) 
  2. private void doExportUrls() { 
  3.     // TODO: 2021/5/27 加载注册中心URL  
  4.     List<URL> registryURLs = loadRegistries(true); 
  5.     for (ProtocolConfig protocolConfig : protocols) { 
  6.         // TODO: 2021/5/27 根据不同协议进行服务暴露  
  7.         doExportUrlsFor1Protocol(protocolConfig, registryURLs); 
  8.     } 

loadRegistries也很简单,其实就是根据注册中心的配置组装成URL,这里多个注册中心比较好理解,多个protocols是什么鬼呢?

其实是这样的,一个服务如果有多个协议那么就都需要暴露,比如同时支持 dubbo 协议和 hessian 协议,那么需要将这个服务用两种协议分别向多个注册中心暴露注册。

参考了这块逻辑,在我司项目中,我们规范了注册中心的接口,允许注册中心有多种实现, 甚至是本地注册中心,但是并不允许有多个注册中心,目前来说是没有这种需求,而要选择哪个注册中心,只需要在yaml文件上进行配置即可

接下来看doExportUrlsFor1Protocol方法

在分析服务暴露流程之前便有提到过,dubbo内部使用URL来携带各类数据,从而贯穿整个生命周期的,而入口其实就是从这个方法开始的,等下我们便可以看到该方法可以分为两个步骤,前个步骤是组装URL的逻辑,后个步骤是真正实现暴露dubbo服务等逻辑的地方,不说了,继续code

  1. private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) { 
  2.     /**组装服务的URL开始**/ 
  3.     // TODO: 2021/5/27 获取协议名 
  4.     String name = protocolConfig.getName(); 
  5.     // TODO: 2021/5/27 如果为空,则默认是dubbo 
  6.     if (name == null || name.length() == 0) { 
  7.         name = "dubbo"
  8.     } 
  9.  
  10.     // TODO: 2021/5/27 设置map等各种参数 
  11.     Map<String, String> map = new HashMap<String, String>(); 
  12.     map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE); 
  13.     map.put(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion()); 
  14.     map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis())); 
  15.     if (ConfigUtils.getPid() > 0) { 
  16.         map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid())); 
  17.     } 
  18.     // TODO: 2021/5/27 添加application、module、provider等信息到map中 
  19.     appendParameters(map, application); 
  20.     appendParameters(map, module); 
  21.     appendParameters(map, provider, Constants.DEFAULT_KEY); 
  22.     appendParameters(map, protocolConfig); 
  23.     appendParameters(map, this); 
  24.     // TODO: 2021/5/27 如果methods的配置列表不为空,则遍历methods配置列表 
  25.     if (methods != null && !methods.isEmpty()) { 
  26.         for (MethodConfig method : methods) { 
  27.             // TODO: 2021/5/27 把方法名加入map 
  28.             appendParameters(map, method, method.getName()); 
  29.             // TODO: 2021/5/27 添加methodconfig对象的字段信息到map中 
  30.             String retryKey = method.getName() + ".retry"
  31.             if (map.containsKey(retryKey)) { 
  32.                 String retryValue = map.remove(retryKey); 
  33.                 if ("false".equals(retryValue)) { 
  34.                     map.put(method.getName() + ".retries""0"); 
  35.                 } 
  36.             } 
  37.             // TODO: 2021/5/27 添加ArgumentConfig列表 
  38.             List<ArgumentConfig> arguments = method.getArguments(); 
  39.             if (arguments != null && !arguments.isEmpty()) { 
  40.                 for (ArgumentConfig argument : arguments) { 
  41.                     // convert argument type 
  42.                     if (argument.getType() != null && argument.getType().length() > 0) { 
  43.                         // TODO: 2021/5/27 利用反射拿到接口类的所有方法 
  44.                         Method[] methods = interfaceClass.getMethods(); 
  45.                         if (methods != null && methods.length > 0) { 
  46.                             // TODO: 2021/5/27 遍历methods 
  47.                             for (int i = 0; i < methods.length; i++) { 
  48.                                 String methodName = methods[i].getName(); 
  49.                                 // TODO: 2021/5/27 找到目标方法 
  50.                                 if (methodName.equals(method.getName())) { 
  51.                                     // TODO: 2021/5/27 通过反射拿到方法参数类型 
  52.                                     Class<?>[] argtypes = methods[i].getParameterTypes(); 
  53.                                     // TODO: 2021/5/27 如果下表为-1 
  54.                                     if (argument.getIndex() != -1) { 
  55.                                         // TODO: 2021/5/27 检测argtypes的名称与ArgumentConfig中的type是否一致 
  56.                                         if (argtypes[argument.getIndex()].getName().equals(argument.getType())) { 
  57.                                             appendParameters(map, argument, method.getName() + "." + argument.getIndex()); 
  58.                                         } else { 
  59.                                             // TODO: 2021/5/27 不一致则抛出异常 
  60.                                             throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType()); 
  61.                                         } 
  62.                                     } else { 
  63.                                         // TODO: 2021/5/27 遍历参数,查找argument.type的类型 
  64.                                         for (int j = 0; j < argtypes.length; j++) { 
  65.                                             Class<?> argclazz = argtypes[j]; 
  66.                                             // TODO: 2021/5/27 如果找得到则将ArgumentConfig字段添加map中 
  67.                                             if (argclazz.getName().equals(argument.getType())) { 
  68.                                                 appendParameters(map, argument, method.getName() + "." + j); 
  69.                                                 if (argument.getIndex() != -1 && argument.getIndex() != j) { 
  70.                                                     throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType()); 
  71.                                                 } 
  72.                                             } 
  73.                                         } 
  74.                                     } 
  75.                                 } 
  76.                             } 
  77.                         } 
  78.                     } else if (argument.getIndex() != -1) { 
  79.                         // TODO: 2021/5/27 用户未配置type属性,但配置了index属性,且index != -1,则直接添加到map中 
  80.                         appendParameters(map, argument, method.getName() + "." + argument.getIndex()); 
  81.                     } else { 
  82.                         throw new IllegalArgumentException("argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>"); 
  83.                     } 
  84.  
  85.                 } 
  86.             } 
  87.         } // end of methods for 
  88.     } 
  89.  
  90.     // TODO: 2021/5/27 如果是泛化调用,则在map中设置generic和methods 
  91.     if (ProtocolUtils.isGeneric(generic)) { 
  92.         map.put(Constants.GENERIC_KEY, generic); 
  93.         map.put(Constants.METHODS_KEY, Constants.ANY_VALUE); 
  94.     } else { 
  95.         // TODO: 2021/5/27 获得版本号 
  96.         String revision = Version.getVersion(interfaceClass, version); 
  97.         // TODO: 2021/5/27 放入map中 
  98.         if (revision != null && revision.length() > 0) { 
  99.             map.put("revision", revision); 
  100.         } 
  101.  
  102.         // TODO: 2021/5/27 获得方法集合 
  103.         String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames(); 
  104.         if (methods.length == 0) { 
  105.             logger.warn("NO method found in service interface " + interfaceClass.getName()); 
  106.             // TODO: 2021/5/27 设置方法为* 
  107.             map.put(Constants.METHODS_KEY, Constants.ANY_VALUE); 
  108.         } else { 
  109.             // TODO: 2021/5/27 否则加入方法集合中 
  110.             map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ",")); 
  111.         } 
  112.     } 
  113.     // TODO: 2021/5/27 将token加入map 
  114.     if (!ConfigUtils.isEmpty(token)) { 
  115.         if (ConfigUtils.isDefault(token)) { 
  116.             map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString()); 
  117.         } else { 
  118.             map.put(Constants.TOKEN_KEY, token); 
  119.         } 
  120.     } 
  121.     if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) { 
  122.         protocolConfig.setRegister(false); 
  123.         map.put("notify""false"); 
  124.     } 
  125.      
  126.     String contextPath = protocolConfig.getContextpath(); 
  127.     if ((contextPath == null || contextPath.length() == 0) && provider != null) { 
  128.         contextPath = provider.getContextpath(); 
  129.     } 
  130.  
  131.     // TODO: 2021/5/27 获得地址、端口号 
  132.     String host = this.findConfigedHosts(protocolConfig, registryURLs, map); 
  133.     Integer port = this.findConfigedPorts(protocolConfig, name, map); 
  134.     // TODO: 2021/5/27 组装生成URL 
  135.     URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map); 
  136.  
  137.     /**组装服务的URL结束**/ 
  138.   
  139.     /* 
  140.   * 后续讲解服务暴露 
  141.   */ 

这个方法实在是又臭又长,我特意分成两部分,目前这部分是组装服务的URL部分,其实简单点说就是:

先将provider、applicaiton、module等各种基础配置直接放入map中,再针对method配置等进行校验,查看该配置是否有配置方法存在,并进行方法签名的校验,如果是才放入map中,然后还额外将一些多余数据,比如泛化调用、版本号等加入map中,最终根据host和port,结合map组装成URL,貌似还是有点长。

总归就是结合服务自身的各种配置放入map中,然后根据host和port以及map等生成URL就是了。

接下来看看后续服务暴露部分

  1. private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) { 
  2.      /* 
  3.   * 前面URL组装 
  4.   */ 
  5.      
  6.     // TODO: 2021/5/27 加载ConfiguratorFactory,并生成Configurator实例,判断是否有该协议的实现存在 
  7.     if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) 
  8.             .hasExtension(url.getProtocol())) { 
  9.         // TODO: 2021/5/27 通过SPI机制配置URL 
  10.         url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class) 
  11.                 .getExtension(url.getProtocol()).getConfigurator(url).configure(url); 
  12.     } 
  13.  
  14.     String scope = url.getParameter(Constants.SCOPE_KEY); 
  15.     // TODO: 2021/5/27 如果scope为none,则什么都不做 
  16.     if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) { 
  17.  
  18.         // TODO: 2021/5/27 如果scope不是远程,则暴露到本地 
  19.         if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) { 
  20.             /** 本地服务暴露 **/ 
  21.             exportLocal(url); 
  22.         } 
  23.         // TODO: 2021/5/27 如果不是local,则暴露到远程 
  24.         if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) { 
  25.             if (logger.isInfoEnabled()) { 
  26.                 logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url); 
  27.             } 
  28.             if (registryURLs != null && !registryURLs.isEmpty()) { 
  29.                 // TODO: 2021/5/27 遍历注册中心 
  30.                 for (URL registryURL : registryURLs) { 
  31.                     url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY)); 
  32.                     // TODO: 2021/5/27 加载监视器连接 
  33.                     URL monitorUrl = loadMonitor(registryURL); 
  34.                     if (monitorUrl != null) { 
  35.                         // TODO: 2021/5/27 如果没有则添加一个 
  36.                         url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString()); 
  37.                     } 
  38.                     if (logger.isInfoEnabled()) { 
  39.                         logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL); 
  40.                     } 
  41.  
  42.                     // TODO: 2021/5/27 根据URL拿到代理方式 
  43.                     String proxy = url.getParameter(Constants.PROXY_KEY); 
  44.                     if (StringUtils.isNotEmpty(proxy)) { 
  45.                         // TODO: 2021/5/27 给注册中心的URL添加代理方式 
  46.                         registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy); 
  47.                     } 
  48.  
  49.                     // TODO: 2021/5/24 通过SPI机制拿到对应的proxyFactory 
  50.                     /** 根据proxyFactory拿到Invoker **/ 
  51.                     Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())); 
  52.                     DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); 
  53.  
  54.                     // TODO: 2021/5/24 通过SPI机制拿到对应的protocol,先是RegistryProtocol,再被AOP强化 
  55.                     /** 服务暴露 **/ 
  56.                     Exporter<?> exporter = protocol.export(wrapperInvoker); 
  57.                     exporters.add(exporter); 
  58.                 } 
  59.             } else { 
  60.                 // TODO: 2021/5/24 通过SPI机制拿到对应的proxyFactory 
  61.                 /** 根据proxyFactory拿到Invoker **/ 
  62.                 Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url); 
  63.                 DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); 
  64.  
  65.                 // TODO: 2021/5/24 通过SPI机制拿到对应的protocol 
  66.                 /** 服务暴露 **/ 
  67.                 Exporter<?> exporter = protocol.export(wrapperInvoker); 
  68.                 exporters.add(exporter); 
  69.             } 
  70.         } 
  71.     } 
  72.     this.urls.add(url); 

后续重要的地方可以认为其实就是遍历注册中心进行服务暴露,只是会根据服务配置域scope来针对性做一些暴露处理,比如如果scope不是远程,则暴露到本地,如果不是local,则暴露到远程。

该方法中又包含了几个核心的拓展实现,包括:

  1. 本地服务暴露
  2. 根据proxyFactory拿到Invoker
  3. 远程服务暴露、注册

继续补充流程图,整理思路

 

首先第1点,看看本地服务暴露逻辑

  1. private void exportLocal(URL url) { 
  2.     if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) { 
  3.         URL local = URL.valueOf(url.toFullString()) 
  4.                 .setProtocol(Constants.LOCAL_PROTOCOL) 
  5.                 .setHost(LOCALHOST) 
  6.                 .setPort(0); 
  7.         StaticContext.getContext(Constants.SERVICE_IMPL_CLASS).put(url.getServiceKey(), getServiceClass(ref)); 
  8.         // TODO: 2021/5/27 根据SPI拿到了InjvmProtocol调用了export方 
  9.         Exporter<?> exporter = protocol.export( 
  10.                 proxyFactory.getInvoker(ref, (Class) interfaceClass, local)); 
  11.         // 放入集合中缓存 
  12.         exporters.add(exporter); 
  13.         logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry"); 
  14.     } 

  1. @Override 
  2. public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { 
  3.     return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap); 

暴露到本地的大致逻辑其实就是根据SPI机制拿到了InjvmProtocol生成了InjvmExporter,之后放入集合缓存中,至于SPI机制,后续需要开个文章专门讲讲,有兴趣持续关注该系列。

  • 为啥要有本地服务暴露?

大致原因应该是因为可能存在同一个 JVM 内部引用自身服务的情况,因此暴露的本地服务在内部调用的时候可以直接消费同一个 JVM 的服务避免了网络间的通信。

继续看第2点,根据proxyFactory拿到Invoker部分,首先我们看ProxyFactory类名就大概可以猜到该类具备生成代理对象的能力,我们看proxyFactory的生成模式

 

可以看到,该对象也是通过SPI机制生成的,由于SPI机制也是比较庞大的,为了避免混淆,后续再开篇文章讲解,有兴趣的持续关注。

通过SPI机制拿到了ProxyFactory的实现对象JavassisProxyFactory,最终调用的代码

  1. @Override 
  2. public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) { 
  3.     // TODO: 2021/5/23 为目标类创建Wrapper 
  4.     final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type); 
  5.     // TODO: 2021/5/23 创建匿名的Invoker对象,并实现doInvoker方法 
  6.     return new AbstractProxyInvoker<T>(proxy, type, url) { 
  7.         @Override 
  8.         protected Object doInvoke(T proxy, String methodName, 
  9.                                   Class<?>[] parameterTypes, 
  10.                                   Object[] arguments) throws Throwable { 
  11.             // TODO: 2021/5/23 调用Wrapper的invokeMethod方法,invokeMethod最终会调用目标方法 
  12.             return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments); 
  13.         } 
  14.     }; 

该方法就是创建了一个匿名的Inovker对象,在doInvker方法中调用wrapper.invokeMethod方法,invokeMethod最终会调用目标方法。

  • 那么wrapper又是啥?

Wrapper是一个抽象类,在调用Wrapper.getWrapper创建子类的时候,会根据目标Class对象进行解析,拿到各种方法、类成员变量等信息,以及生成invokeMethod方法等代码,在代码生成完毕后,通过Javassist生成Class对象,可以理解为该Class对象就是BossServiceImpl的代理实例,有兴趣了解生成过程的可以看Wrapper.makeWrapper方法。

  • 为啥一定要封装Invoker?

其实就是为了屏蔽本地调用或者远程调用或者集群调用的细节,统一暴露出一个可执行体,方便调用者调用,而不管怎么封装,其实最终都是调向目标方法。

  • 为啥要封装Exporter?

这个涉及到后续服务被具体调用,后面会开一篇文章专门讲这个,有兴趣的可以持续关注。

在我司的rpc框架中,倒是没有使用Javassist去生成代理对象,而是选择了使用jdk提供的Proxy生成机制。

继续补充流程图,整理思路

 

接下来说说远程服务暴露

远程服务暴露要比本地复杂的多,在doExportUrlsFor1Protocol后半部分,通过proxyFactory生成Inovker后,就需要调用protocol.export做真的服务暴露了,我们可以看到protocol是如何实例化的

又是通过SPI实例化的,通过断点可以看到会先被AOP切面拦截额外做了一些其他的操作,不过最终走向的RegisterProtocol,AOP这块后续再分析,有兴趣的持续关注。

接下来继续看RegisterProtocol.export做了啥

  1. @Override 
  2. public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException { 
  3.     // TODO: 2021/5/29 服务暴露 
  4.     final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker); 
  5.     // TODO: 2021/5/23 获得注册中心的URL 
  6.     URL registryUrl = getRegistryUrl(originInvoker); 
  7.  
  8.     final Registry registry = getRegistry(originInvoker); 
  9.     // TODO: 2021/5/23 获得已经注册的服务提供者URL 
  10.     final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker); 
  11.  
  12.     boolean register = registeredProviderUrl.getParameter("register"true); 
  13.  
  14.     ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl); 
  15.     if (register) { 
  16.         // TODO: 2021/5/29 真正做服务注册的地方 
  17.         register(registryUrl, registeredProviderUrl); 
  18.         ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true); 
  19.     } 
  20.  
  21.     // TODO: 2021/5/23 获取override订阅URL 
  22.     final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl); 
  23.     // TODO: 2021/5/23 创建override的监听器 
  24.     final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker); 
  25.     // TODO: 2021/5/23 缓存监听器到集合中 
  26.     overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener); 
  27.     // TODO: 2021/5/23 向注册中心订阅override数据 
  28.     registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); 
  29.     // TODO: 2021/5/23 创建并返回DestroyableExporter 
  30.     return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl); 

从代码上看,该方法其实做了两件事情,分别是服务暴露和注册:

  • 执行了doLocalExport进行服务暴露
  • 加载注册中心实现类,向注册中心注册服务
  • 向注册中心订阅override数据
  • 创建并返回DestroyableExporter

接下来继续看看doLocalExport做了啥

  1. private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) { 
  2.     String key = getCacheKey(originInvoker); 
  3.     ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key); 
  4.     if (exporter == null) { 
  5.         synchronized (bounds) { 
  6.             exporter = (ExporterChangeableWrapper<T>) bounds.get(key); 
  7.             if (exporter == null) { 
  8.                 // TODO: 2021/5/24 创建Invoker为委托对象 
  9.                 final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker)); 
  10.                 // TODO: 2021/5/24 调用protocol的export方法暴露服务 
  11.                 exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker); 
  12.                 bounds.put(key, exporter); 
  13.             } 
  14.         } 
  15.     } 
  16.     return exporter; 

看逻辑比较简单,主要是根据不同协议配置,根据SPI调用不同的protocol实现,跟暴露到本地时实现的InjvmPortocol一样,默认这里调用的是DubboProtocol.export

  1. public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { 
  2.     URL url = invoker.getUrl(); 
  3.  
  4.     // TODO: 2021/5/29 得到服务key,格式:group+"/"+serviceName+":"+serviceVersion+":"+port 
  5.     String key = serviceKey(url); 
  6.     // TODO: 2021/5/29 创建exporter  
  7.     DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap); 
  8.     exporterMap.put(key, exporter); 
  9.  
  10.     Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT); 
  11.     Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false); 
  12.     if (isStubSupportEvent && !isCallbackservice) { 
  13.         String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY); 
  14.         if (stubServiceMethods == null || stubServiceMethods.length() == 0) { 
  15.             if (logger.isWarnEnabled()) { 
  16.                 logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) + 
  17.                         "], has set stubproxy support event ,but no stub methods founded.")); 
  18.             } 
  19.         } else { 
  20.             stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods); 
  21.         } 
  22.     } 
  23.  
  24.     // TODO: 2021/5/24 开启服务器 
  25.     openServer(url); 
  26.     // TODO: 2021/5/29 序列化  
  27.     optimizeSerialization(url); 
  28.     return exporter; 

可以到export先是new了一个DubboExporter对象, 后续打开了服务,接下来继续看openServer做了啥

  1. private void openServer(URL url) { 
  2.     String key = url.getAddress(); 
  3.     boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true); 
  4.     if (isServer) { 
  5.         ExchangeServer server = serverMap.get(key); 
  6.         if (server == null) { 
  7.             // TODO: 2021/5/24 启动一个服务实例 
  8.             serverMap.put(key, createServer(url)); 
  9.         } else { 
  10.             // server supports reset, use together with override 
  11.             server.reset(url); 
  12.         } 
  13.     } 
  14.  
  15. private ExchangeServer createServer(URL url) { 
  16.     // TODO: 2021/5/29 服务器关闭是发送readonly时间  
  17.     url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString()); 
  18.     // TODO: 2021/5/29 心跳默认时间  
  19.     url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT)); 
  20.     // TODO: 2021/5/29 获得远程通讯服务端实现方式  
  21.     String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER); 
  22.  
  23.     if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) 
  24.         throw new RpcException("Unsupported server type: " + str + ", url: " + url); 
  25.     // TODO: 2021/5/29 添加编解码器DubboCodec实现  
  26.     url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME); 
  27.     ExchangeServer server; 
  28.     try { 
  29.         // TODO: 2021/5/29 启动服务器  
  30.         server = Exchangers.bind(url, requestHandler); 
  31.     } catch (RemotingException e) { 
  32.         throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e); 
  33.     } 
  34.     str = url.getParameter(Constants.CLIENT_KEY); 
  35.     if (str != null && str.length() > 0) { 
  36.         Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions(); 
  37.         if (!supportedTypes.contains(str)) { 
  38.             throw new RpcException("Unsupported client type: " + str); 
  39.         } 
  40.     } 
  41.     return server; 

可以看到最终还是依赖URL携带的远程通讯实现方法创建了一个服务器对象。

总结一下:doLocalExport最终其实就是根据URL开启了服务器,并返回了Exporter。

接下来继续看注册服务部分

  1. public void register(URL registryUrl, URL registedProviderUrl) { 
  2.     // TODO: 2021/5/29 获取注册中心实例 
  3.     Registry registry = registryFactory.getRegistry(registryUrl); 
  4.     // TODO: 2021/5/29 调用register 
  5.     registry.register(registedProviderUrl); 

Regsitry的生成最终也是依赖了SPI机制,最终走向FailbackRegistry.register

  1. @Override 
  2. public void register(URL url) { 
  3.     super.register(url); 
  4.     // TODO: 2021/5/24 从失败的集合中移除 
  5.     failedRegistered.remove(url); 
  6.     failedUnregistered.remove(url); 
  7.     try { 
  8.         // TODO: 2021/5/24 向注册中心发起注册请求 
  9.         doRegister(url); 
  10.     } catch (Exception e) { 
  11.         Throwable t = e; 
  12.  
  13.         boolean check = getUrl().getParameter(Constants.CHECK_KEY, true
  14.                 && url.getParameter(Constants.CHECK_KEY, true
  15.                 && !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol()); 
  16.         boolean skipFailback = t instanceof SkipFailbackWrapperException; 
  17.         if (check || skipFailback) { 
  18.             if (skipFailback) { 
  19.                 t = t.getCause(); 
  20.             } 
  21.             throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t); 
  22.         } else { 
  23.             logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t); 
  24.         } 
  25.  
  26.         // TODO: 2021/5/29 发生异常则放入failedRegistered 
  27.         failedRegistered.add(url); 
  28.     } 

可以看到注册的核心实现是在doRegister中,不过通过代码机制我们也可以看出,在注册报错的时候会被trycatch拦截,然后放入failedRegistered容器中,结合FailbackRegistry该类名可以推测应该是有个重试机制存在,看看构造方法

  1. // TODO: 2021/5/24 从url中获取重试频率参数,启动定时器进行重试逻辑 
  2. public FailbackRegistry(URL url) { 
  3.     super(url); 
  4.     this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD); 
  5.     this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() { 
  6.         @Override 
  7.         public void run() { 
  8.             try { 
  9.                 // TODO: 2021/5/29 定时重试  
  10.                 retry(); 
  11.             } catch (Throwable t) { // Defensive fault tolerance 
  12.                 logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t); 
  13.             } 
  14.         } 
  15.     }, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS); 

果不其然,最终如果注册发生了异常,则会进行定时重试。

关于重试机制也是要有的,在我司的rpc框架中,我们将重试时间放在yaml上去配置,不过定时器并没有采用Executor机制,而是模仿了dubbo3.0的写法,也就是时间轮的机制,性能更好。

接下来看注册核心部分doRegister,可以看到该方法是一个抽象方法,由于我在xml配置中配置的注册中心是Zookeeper,因而最终走向ZookeeperRegistry

  1. @Override 
  2. protected void doRegister(URL url) { 
  3.     try { 
  4.         zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true)); 
  5.     } catch (Throwable e) { 
  6.         throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); 
  7.     } 

服务注册走到这里基本到头了,再深入便是看注册中心的实现了。

接下来看看向注册中心订阅override数据部分

上面有说过registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener)最终走向的方法是FailbackRegistry.subscribe

  1. public void subscribe(URL url, NotifyListener listener) { 
  2.     super.subscribe(url, listener); 
  3.     removeFailedSubscribed(url, listener); 
  4.     try { 
  5.         // TODO: 2021/5/29 真正做订阅的地方  
  6.         doSubscribe(url, listener); 
  7.     } catch (Exception e) { 
  8.         Throwable t = e; 
  9.  
  10.         List<URL> urls = getCacheUrls(url); 
  11.         if (urls != null && !urls.isEmpty()) { 
  12.             notify(url, listener, urls); 
  13.             logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + getUrl().getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/dubbo-registry-" + url.getHost() + ".cache") + ", cause: " + t.getMessage(), t); 
  14.         } else { 
  15.             // If the startup detection is opened, the Exception is thrown directly. 
  16.             boolean check = getUrl().getParameter(Constants.CHECK_KEY, true
  17.                     && url.getParameter(Constants.CHECK_KEY, true); 
  18.             boolean skipFailback = t instanceof SkipFailbackWrapperException; 
  19.             if (check || skipFailback) { 
  20.                 if (skipFailback) { 
  21.                     t = t.getCause(); 
  22.                 } 
  23.                 throw new IllegalStateException("Failed to subscribe " + url + ", cause: " + t.getMessage(), t); 
  24.             } else { 
  25.                 logger.error("Failed to subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t); 
  26.             } 
  27.         } 
  28.  
  29.         // TODO: 2021/5/29 订阅失败,则放入失败容器中  
  30.         addFailedSubscribed(url, listener); 
  31.     } 

同样,订阅失败后也是放入失败容器中,定时重试进行订阅。

再看看核心实现方法doSubscribe方法,最终走向ZookeeperRegistry.doSubscribe中

  1. @Override 
  2. protected void doSubscribe(final URL url, final NotifyListener listener) { 
  3.     try { 
  4.         // TODO: 2021/5/29 处理URL参数中interface为*的订阅,例如监控中心的订阅 
  5.         if (Constants.ANY_VALUE.equals(url.getServiceInterface())) { 
  6.             /** 先无视 **/ 
  7.         } else { 
  8.             List<URL> urls = new ArrayList<URL>(); 
  9.             // TODO: 2021/5/29 遍历分类数组 
  10.             for (String path : toCategoriesPath(url)) { 
  11.                 // TODO: 2021/5/29 获得监听器集合 
  12.                 ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url); 
  13.                 // TODO: 2021/5/29 如果没有则创建 
  14.                 if (listeners == null) { 
  15.                     zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>()); 
  16.                     listeners = zkListeners.get(url); 
  17.                 } 
  18.                 // TODO: 2021/5/29 获得监听器 
  19.                 ChildListener zkListener = listeners.get(listener); 
  20.                 if (zkListener == null) { 
  21.                     listeners.putIfAbsent(listener, new ChildListener() { 
  22.                         @Override 
  23.                         public void childChanged(String parentPath, List<String> currentChilds) { 
  24.                             // TODO: 2021/5/29 通知服务变化,回调NotifyListener 
  25.                             ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds)); 
  26.                         } 
  27.                     }); 
  28.                     zkListener = listeners.get(listener); 
  29.                 } 
  30.                 // TODO: 2021/5/29 创建节点,如:/dubbo/com.alibaba.dubbo.demo.DemoService/providers 
  31.                 zkClient.create(path, false); 
  32.                 List<String> children = zkClient.addChildListener(path, zkListener); 
  33.                 if (children != null) { 
  34.                     urls.addAll(toUrlsWithEmpty(url, path, children)); 
  35.                 } 
  36.             } 
  37.             // TODO: 2021/5/29 通知数据变更,如RegistryDirectory 
  38.             notify(url, listener, urls); 
  39.         } 
  40.     } catch (Throwable e) { 
  41.         throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e); 
  42.     } 

这个方法主要做了订阅和监听触发逻辑,具体逻辑就是订阅了某个服务的URL,在服务变更的时候触发逻辑变化。其实此处已经是可以归纳入服务治理模块了,后续会有专门的文章分享服务治理,有兴趣可以持续关注。

画个流程图,整理下思路

 

看到这里服务暴露流程基本理完了,还是有点东西在里面的,并且还需要掌握 Dubbo SPI,不然有些点例如自适应什么的还是很难理解的,为了写这篇文章,我前前后后也是花了不少的时间。

最后我再来一张完整的流程图带大家再过一遍,具体还是有很多细节,不过不是主干我就不做分析了,不然文章散掉了。

后续服务治理、APO、SPI机制也会在该流程图上进行拓展,有兴趣的也可以关注流程图链接:

https://www.processon.com/view/link/60b25f275653bb3c7e646934

总结

虽然看完了该篇文章,但是还是建议大家自己打断点过一遍,可以更加清晰,而如果是为了应付面试官提问的话,基本上记住上面流程图的内容就差不多了,当你研究完了dubbo后,其实会发现dubbo有很多东西可以写,比如服务应用、SPI、dubbo中的AOP机制、服务治理等好几个模块,最后就是带大家撸一个RPC框架了,还是那句话,想学dubbo的可以持续关注这一系列。

本文转载自微信公众号「 稀饭下雪」,可以通过以下二维码关注。转载本文请联系 稀饭下雪公众号。

原文链接:https://mp.weixin.qq.com/s/gsPa2KHS1ZqxU6z3_wI46w

 

责任编辑:姜华 来源: 稀饭下雪
相关推荐

2022-09-27 14:01:25

知识图谱人工智能网络空间

2022-02-15 18:45:35

Linux进程调度器

2022-09-06 08:02:40

死锁顺序锁轮询锁

2021-10-18 11:58:56

负载均衡虚拟机

2022-09-14 09:01:55

shell可视化

2021-01-19 05:49:44

DNS协议

2020-07-09 07:54:35

ThreadPoolE线程池

2022-10-10 08:35:17

kafka工作机制消息发送

2024-03-07 18:11:39

Golang采集链接

2022-07-19 16:03:14

KubernetesLinux

2020-07-15 08:57:40

HTTPSTCP协议

2020-11-16 10:47:14

FreeRTOS应用嵌入式

2018-12-18 15:21:22

海量数据Oracle

2022-09-08 10:14:29

人脸识别算法

2021-08-26 05:02:50

分布式设计

2024-01-11 09:53:31

面试C++

2022-07-15 16:31:49

Postman测试

2024-01-05 08:30:26

自动驾驶算法

2023-06-12 08:49:12

RocketMQ消费逻辑

2024-05-10 12:59:58

PyTorch人工智能
点赞
收藏

51CTO技术栈公众号