dubbo的服務註冊發現是怎麼實現的.

Zookeeper做爲Dubbo服務的註冊中心爲例, 先來看看如何使用:java

 

webapps/ROOT/WEB-INF下,有一個dubbo.properties文件,裏面指向Zookeeper ,使用的是Zookeeper 的註冊中心web

 

服務端配置spring

<dubbo:application name="dubbo_provider"></dubbo:application>緩存

<dubbo:registry address="zookeeper://127.0.0.1:2181" check="false" subscribe="false" register=""></dubbo:registry>服務器

<!-- 暴露服務-->app

<dubbo:service interface="cn.test.dubbo.registry.service.TestRegistryService" ref="testRegistryService" />負載均衡

 

 

客戶端配置:框架

<dubbo:application name="dubbo_consumer"></dubbo:application>
   <!-- 使用zookeeper註冊中心暴露服務地址 --> 
   <dubbo:registry address="zookeeper://192.168.74.129:2181" check="false"></dubbo:registry>
     <!-- 要引用的服務 --> 
   <dubbo:reference interface="cn.test.dubbo.registry.service.TestRegistryService" id="testRegistryService"></dubbo:reference>
webapp

 

 

 

從配置上看, 能夠以ApplicationConfig,RegistryConfig,ServiceConfig,ReferenceConfig這幾個類爲入口來分析.jvm

 

這幾個類主要存放配置信息, 須要關注:

1, dubbo是如何將配置類轉變爲spring上下文中的bean,

2,    如何暴露服務,

3,   在暴露服務的時候,

4,  是如何在zookeeper上註冊的,

5, 客戶端是如何發現服務的,

6, 如何發起遠程服務調用的,

7, 服務端在收到請求以後, 是如何找到對應的服務的?

 

 

1,spring 配置讀取,解析, 再到生成bean, 放到spring上下文的過程.

 

dubbo自定義了名稱空間"dubbo",

spring支持自定義名稱空間, 須要如下幾步操做

  1. 繼承抽象類NamespaceHandlerSupport, 在子類中調用registerBeanDefinitionParser方法, 註冊解析器,

 

registerBeanDefinitionParser("service",new DubboBeanDefinitionParser(ServiceBean.class,true));

 

說明 dubbo:service, 最終會生成ServiceBean, 解析轉換的細節是spring的源碼範疇, 再也不深究.

  1. META-INF, 編寫xsd定義文件

   

 

其中, dubbo.xsd xsd定義文件, spring.handlers, 指定了dubbo名稱空間節點解析器,spring.schemas配置告訴名稱空xsd文件在哪裏.

 

com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler中能夠看到配置節點對應生成的bean實例:

 

registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));

        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));

        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));

        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));

        registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));

        registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));

        registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));

        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));

        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));

        registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));

 

咱們着重來看ServiceBeanReferenceBean, 這兩個分別涉及到服務的暴露及引用.

ServiceBean的類繼承關係及實現接口來看:

public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {

 

繼承自ServiceConfig將獲得配置屬性及暴露服務等相關方法.

實現InitializingBean接口, spring在實例化完成以後, 將自動調用afterPropertiesSet方法作初始化

實現DisposableBean接口, spring在容器銷燬的時候, 會調用destroy方法.

實現ApplicationContextAware接口, spring會給這個bean注入ApplicationContext, serviceBean中經過applicationContext抓了不少bean注進來.

實現了ApplicationListener接口, 會監聽spring的特有的應用生命週期事件 onApplicationEvent, ServiceBean監聽ContextRefreshedEvent事件, 再上下文初始化完成以後, 若是服務未暴露(export)再暴露一下.

實現了BeanNameAware 接口, beanName設置爲beanid.

 

serviceBeanafterPropertiesSet邏輯能夠看出, 在讀取配置到ServiceConfig, 在上下文中, 根據ServiceConfig配置屬性找到對應的bean注入, 完了調用ServiceConfigexport() 方法暴露服務.

 

export() 方法作了不少初始化屬性(找相關bean來注入), 某些屬性若是未配置, 使用默認值注入, 還有就是一些校驗邏輯.

繼續跟蹤到export 跟蹤到doExportUrls():

 

private void doExportUrls() {

        List<URL> registryURLs = loadRegistries(true);

        for (ProtocolConfig protocolConfig : protocols) {

            doExportUrlsFor1Protocol(protocolConfig, registryURLs);

        }

    }

先獲取全部註冊中心地址, 可能配置了多個, 就是下面這個配置

<dubbo:registry address="zookeeper://127.0.0.1:2181" check="false" subscribe="false" register=""></dubbo:registry>這裏只使用了zookeeper做爲註冊中心.

 

斷點調試查到registryURLs  :

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=hello-world-app&check=false&dubbo=2.0.0&pid=47708&registry=zookeeper&subscribe=false&timestamp=1467342589223

 

而後根據配置的協議(protocols), 來暴露服務, 若是未配置協議, 默認的是:dubbo

配置在dubbo-default.properties  dubbo.provider.protocol=dubbo.

protocols 會從ProviderConfig裏取,ServiceConfig判空寫默認值的時候, 實例化了ProviderConfig, 完了會給這個實例寫入默認值, 其中的protocols就是從默認配置文件裏取的.

private void checkDefault() {

        if (provider == null) {

            provider = new ProviderConfig();

        }

        appendProperties(provider);

    }

 

再回到doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs)方法看看具體的實現, 這個方法比較長, 吐槽一下寫得不是很好, 縮進嵌套太深, 可讀性差.

1, 方法一開始先從protocolConfig上取協議名稱, 若是取不到仍然默認協議爲"dubbo"

 

================2~5步爲了獲得須要暴露的服務的主機IP=======================

2, protocolConfig上取host, 取不到, provider上取host. (對應dubbo:protocolhost屬性, 官方手冊解釋爲

服務主機名,多網卡選擇或指定VIP及域名時使用,爲空則自動查找本機IP-建議不要配置,讓Dubbo自動獲取本機IP)

 

3, 若是host是無效的本地地址(isInvalidLocalHost):

 

host == null

                                || host.length() == 0

                    || host.equalsIgnoreCase("localhost")

                    || host.equals("0.0.0.0")

                    || (LOCAL_IP_PATTERN.matcher(host).matches());

經過InetAddress.自動獲取本機IP.

4, 若是仍然是無效的或者是本地地址遍歷註冊中心的url, 發起socket鏈接, 而後經過socket.getLocalAddress().getHostAddress()獲得主機地址

5, 仍是得不到無效的或者是本地地址的話, 經過NetUtils.getLocalHost()獲得主機ip, 這個方法經過遍歷本地網卡,返回第一個合理的IP

================2~5步爲了獲得須要暴露的服務的主機IP=======================

================6~7 爲了獲得端口========================

6, protocolConfig上取端口, 取不到從provider上取, 仍然取不到的話, dubboProtocol上獲取默認端口(20880)

final int defaultPort = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(name).getDefaultPort();

上面分析了這個name, 若是沒配置的話, 取的dubbo. 這個Extension取到的是DubboProtocol.

7, 若是配置了協議可是XxxProtocol上沒有默認端口, 那就隨機生成一個端口.經過NetUtils.getAvailablePort(defaultPort)取得.

================6~7 爲了獲得端口========================

8, 準備一些公共默認參數值, Constants.SIDE_KEY,Constants.DUBBO_VERSION_KEY,Constants.TIMESTAMP_KEY, 寫入各核心實例(application,module,provider,protocolConfig,serviceConfig)

===============

9,若是有配置method子標籤, :

 

<dubbo:service interface="com.callback.CallbackService" ref="callbackService" connections="1" callbacks="1000">

    <dubbo:method name="addListener">

        <dubbo:argument index="1" callback="true" />

        <!--也能夠經過指定類型的方式-->

        <!--<dubbo:argument type="com.demo.CallbackListener" callback="true" />-->

    </dubbo:method>

</dubbo:service>

10, 遍歷method子標籤

  1. 寫入method的重試次數值, (service標籤上取, 來自第8步的map), 若是沒有的話寫0
  2. 遍歷method下面的argument子標籤,
    1. 與這個要暴露的服務的接口方法匹配, index或者type屬性來匹配, 並作標記存到map;
    2. 看手冊說這個argument配置主要用來反向代理回調用的, 暫時沒看到關於callback的處理, 可能到了實際調用的時候, 纔會用到.

====================9~10 處理method及其argument子標籤, 看配置手冊, 還有parameter子標籤, 這裏沒處理.

 

11, 若是是泛化實現. 往上面那個map寫入標記generic = true;methods Constants.ANY_VALUE (就是一個*, 表示任意方法)

(

泛接口實現方式主要用於服務器端沒有API接口及模型類元的狀況,參數及返回值中的全部POJO均用Map表示,一般用於框架集成,好比:實現一個通用的遠程服務Mock框架,可經過實現GenericService接口處理全部服務請求。)

若是不是泛化實現map寫入revision, 取的是接口類的版本. 而後看看這個接口有沒有包裝器類, 要把全部包裝器類的method都加進來, 用逗號隔開拼一個字符串寫入methods

 

12, 若是有配置要求token, 默認的話, 隨機給一個uuid, 寫入map, 不然就用配置給定的token. token做令牌驗證用的.

 

13, 若是協議是"injvm", 就不須要註冊.而且給map標記notify=false

 

14, protocolConfig上取得ContextPath, 若是爲空, providerConfig上取.

 

15, host, port, contextPath等建立URL.

 

16, 若是這個url使用的協議(dubbo)存在ConfiguratorFactory的擴展, 調用對應的擴展來配置修改url

目前只有override,absent, 用於向註冊中心修改註冊信息.

override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&timeout=1000

 

17, url中獲取scope信息, 若是scope=none 啥都不作, 不暴露服務.

 

18, 若是scope != remote, 就在本地暴露服務

if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {

                exportLocal(url);

 }

可是exportLocal方法裏頭, 只有當url不是injvm(LOCAL_PROTOCOL)的時候, 纔去作一些暴露操做

也就是說injvm exportLocal裏什麼都不作

 

if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {

    URL local = URL.valueOf(url.toFullString())

            .setProtocol(Constants.LOCAL_PROTOCOL)

            .setHost(NetUtils.LOCALHOST)

            .setPort(0);

    Exporter<?> exporter = protocol.export(

            proxyFactory.getInvoker(ref, (Class) interfaceClass, local));

    exporters.add(exporter);

    logger.info("Export dubbo service " + interfaceClass.getName() +" to local registry");

}

 

19, 若是scope配置不是local. 遍歷每個註冊中心url

若是存在monitor, 則在url參數裏添加monitor信息.

 

20, 經過proxyFactoryurl, 接口類型轉化成invoker

proxyFactory在這裏是由javassist實現的, 也就是JavassistProxyFactory

@SPI("javassist")

public interface ProxyFactory{

 

JavassistProxyFactorygetInvoker, 會去找這個接口類的Wrapper

 

21, 經過protocolinvoker暴露出去

Exporter<?> exporter = protocol.export(invoker);

這裏protocol 根據url中的registry協議, 嘗試去獲取RegistryProtocol

 

Protocol是如何知道要根據url的協議來建立Protocol?

ServiceConfig, Protocol一開始初始化是:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

這說明是在調用方法的時候, 再決定用哪一個擴展

下面是javassist生成的Protocol$Adpative字節碼代碼, 一目瞭然.

 

從下面這個調用鏈能夠看出, registryProtocol不存在, 而後去建立, 而後注入各類屬性, 屬性實例不存在, 又調用各類create擴展的方法, ..

 

最後獲得RegistryProtocol, 並執行其export方法.

斷點執行時發現, 在調用registryProtocolexport方法以前, 還調用了兩個ProtocolWrapper:

根據ExtensionLoader裏的邏輯可知, 只要某個類實現了Protocol接口, 又有Protocol類型入參的構造方法, 可認爲此類是Protocol的包裝器類.

ExtensionLoadergetExtension方法只返回最後最外層的包裝器類

因此從上圖調用關係來看,RegistryProtocolexport方法最後才被執行.

這兩個包裝器類主要用來添加過濾器及監聽器.

 

RegistryProtocolexport方法:

1, 先作本地服務暴露(doLocalExport()),調用DubboProtocol export一下.

完了以後, "dubbo://192.168...."這個url做爲key,綁定剛纔暴露返回的exporter.

DubboProtocol export過程涉及底層具體協議(Netty)建立服務的細節,再也不深刻探討.

2,得到註冊中心對象Registry(registryFactory根據協議zookeeper,知道返回的是ZookeeperRegistry

3, 能夠看到往zookeeper上註冊,實際就是調用zkClientzk樹上建立一個路徑節點.

zkClient.create(toUrlPath(url),url.getParameter(Constants.DYNAMIC_KEY,true));

 

4,根據"dubbo://...."的註冊url, 轉換獲得一個"provider://..."URL, 綁定到一個OverrideListener, registry 訂閱這個provider url的變化通知,並交由對應的listener處理.

5,最後實例化一個Exporter返回.

 

 

ServiceConfig 最後將exporter緩存, 至此, 以上即是服務暴露的過程.

總結一下, 就是在配置解析讀取裝配成bean以後, 初始化, 根據配置協議, 找到註冊中心(Zookeeper)註冊, 找到對應服務Protocol(DubboProtocol)暴露服務.

 

下面再來看看服務引用的過程:

ReferenceBean看起, 這個類繼承自ReferenceConfig, 所以能夠獲得關於dubbo:Reference的配置屬性, 實現了FactoryBean,ApplicationContextAware,InitializingBean,DisposableBean接口:

實現FactoryBean接口, 定製實例化bean的邏輯, 能夠定製返回的bean實例,返回是否單例, 返回實例的類型.

實現ApplicationContextAware, spring會給注入ApplicationContext

實現InitializingBean, 會自動調用afterPropertiesSet, 完成實例化以後的一些初始化工做

實現DisposableBean接口, spring會自動調用destroy方法, 作資源銷燬或者回收操做.

 

ReferenceBean的配置解析裝配沒啥好說的, 主要看afterPropertiesSet方法, 初始化作了哪些事情:

 

afterPropertiesSet中貌似仍然作了一大堆初始化的事情, 一直到最後:

 

 

if (b != null && b.booleanValue()) {

            getObject();

 }

 

getObject();會觸發ReferenceConfiginit方法:

public synchronized T get() {

        if (destroyed){

            throw new IllegalStateException("Already destroyed!");

        }

            if (ref == null) {

                    init();

            }

            return ref;

    }

 

init 邏輯:

寫入默認值之類的邏輯再也不詳細說明,核心邏輯:

ref=createProxy(map);

map 寫了不少配置參數及公共參數, 默認參數等等.

 

經過註冊中心配置拼裝URL

List<URL>us=loadRegistries(false);

 

調試過程當中,抓取的us

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=consumer-of-helloworld-app&check=false&dubbo=2.0.0&pid=408500&registry=zookeeper&timestamp=1467478634195

 

invoker=refprotocol.refer(interfaceClass,urls.get(0));

 

這裏 refprotocol RegistryProtocol, 由於傳入的url參數是registry協議

Protocol refprotocol=ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

 

 

RegistryProtocol中的refer方法:

1, "registry://..."協議的url轉成"zookeeper://..."的協議url

2, registryFactory據此獲得對應的Registry, 咱們使用zookeeper作實驗, 實際就是ZookeeperRegistry

3,調用doRefer方法,

  1. 建立RegistryDirectory,
  2. 構建訂閱url , 調試獲得consumer://192.168.99.1/com.linzp.dubbo.test.DemoService?application=consumer-of-helloworld-app&dubbo=2.0.0&interface=com.linzp.dubbo.test.DemoService&methods=sayHello&pid=408500&side=consumer&timestamp=1467478613998

 

  1. 將此comsumer訂閱url, 到註冊中心註冊
  2. 註冊目錄(RegistryDirectory)訂閱subscribeUrl的通知, 此過程當中會把invoker封裝爲invokerDelegate並在RegistryDirectory中緩存urlInvokerMap

 

  1. 最後由cluster.join(directory)返回invoker實例

cluster實際是FailoverCluster(Cluster接口上有@SPI(FailoverCluster.NAME))

join方法直接返回new FailoverClusterInvoker<T>(directory)

 

 

  1. 最後使用proxyFactoryinvoker建立代理返回.
  2. proxyFactory.getProxy(invoker);
  3. proxyFactory 此刻是JavassistProxyFactory,從它的getProxy中能夠得知
  4. return(T)Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));

 

這裏給interfaces接口建立代理(字節碼實例), 並傳入InvokerInvocationHandler實例化. 具體字節碼代理生成邏輯有待深究.

DemoService demoService = (DemoService)context.getBean("demoService");

獲得的bean實際就是這個代理實例

 

當調用時, String hello = demoService.sayHello("world");

實際發起調用的是包裝了invokerInvokerInvocationHandler對象.

 

下面是斷點調試的截圖, 能夠發現InvokerInvocationHandler中的invoker變成了MockClusterInvoker, 是由代理實例生成的, 暫時不知道爲啥傳入的是MockClusterInvoker

 

 

 

跟進到MockClusterInvoker invoker繼續觀察, 能夠看到這時的invokerFailoverClusterInvoker, 懷疑是在字節碼代理類中使用了包裝器類

 

繼續跟進, 因爲FailoverClusterInvoker中沒有invoke方法, 能夠找到是在父類AbstractClusterInvoker中實現的.

父類作了一些負載均衡的邏輯, 最後調用doInvoke抽象方法, FailoverClusterInvoker中實現的:

doInvoke從父類的select方法獲得一個負載均衡計算後的invoker, 並調用:

Result result = invoker.invoke(invocation);

 

繼續往上跟蹤, 一直跟到DubboInvokerdoInvoke方法

 

這個方法裏頭使用ExchangeClient發起遠程調用

return (Result)currentClient.request(inv,timeout).get();

 

這個再往下挖, 就是基於具體rpc協議(netty)層面上的調用了, 再也不此專題細究.

 

總結一下, 客戶端發起遠程調用的大體思路是:

將配置轉化爲子節碼生成的代理實例存到spring上下文中, 調用時, 經過代理實例, 找到相關協議方法發起遠程調用.

相關文章
相關標籤/搜索