以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支持自定義名稱空間, 須要如下幾步操做
registerBeanDefinitionParser("service",new DubboBeanDefinitionParser(ServiceBean.class,true));
說明 dubbo:service, 最終會生成ServiceBean, 解析轉換的細節是spring的源碼範疇, 再也不深究.
其中, 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));
咱們着重來看ServiceBean和ReferenceBean, 這兩個分別涉及到服務的暴露及引用.
從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.
從serviceBean的afterPropertiesSet邏輯能夠看出, 在讀取配置到ServiceConfig後, 在上下文中, 根據ServiceConfig配置屬性找到對應的bean注入, 完了調用ServiceConfig的export() 方法暴露服務.
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®istry=zookeeper&subscribe=false×tamp=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:protocol的host屬性, 官方手冊解釋爲
服務主機名,多網卡選擇或指定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子標籤
====================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, 經過proxyFactory將url, 接口類型轉化成invoker
proxyFactory在這裏是由javassist實現的, 也就是JavassistProxyFactory
@SPI("javassist")
public interface ProxyFactory{
在JavassistProxyFactory的getInvoker中, 會去找這個接口類的Wrapper類
21, 經過protocol將invoker暴露出去
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方法.
斷點執行時發現, 在調用registryProtocol的export方法以前, 還調用了兩個Protocol的Wrapper:
根據ExtensionLoader裏的邏輯可知, 只要某個類實現了Protocol接口, 又有Protocol類型入參的構造方法, 可認爲此類是Protocol的包裝器類.
ExtensionLoader的getExtension方法只返回最後最外層的包裝器類
因此從上圖調用關係來看,RegistryProtocol的export方法最後才被執行.
這兩個包裝器類主要用來添加過濾器及監聽器.
RegistryProtocol的export方法:
1, 先作本地服務暴露(doLocalExport()),調用DubboProtocol export一下.
完了以後, 將"dubbo://192.168...."這個url做爲key,綁定剛纔暴露返回的exporter.
DubboProtocol export過程涉及底層具體協議(如Netty)建立服務的細節,再也不深刻探討.
2,得到註冊中心對象Registry(registryFactory根據協議zookeeper,知道返回的是ZookeeperRegistry
3, 能夠看到往zookeeper上註冊,實際就是調用zkClient往zk樹上建立一個路徑節點.
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();會觸發ReferenceConfig的init方法:
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®istry=zookeeper×tamp=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方法,
cluster實際是FailoverCluster(Cluster接口上有@SPI(FailoverCluster.NAME))
join方法直接返回new FailoverClusterInvoker<T>(directory)
這裏給interfaces接口建立代理(字節碼實例), 並傳入InvokerInvocationHandler實例化. 具體字節碼代理生成邏輯有待深究.
DemoService demoService = (DemoService)context.getBean("demoService");
獲得的bean實際就是這個代理實例
當調用時, String hello = demoService.sayHello("world");
實際發起調用的是包裝了invoker的InvokerInvocationHandler對象.
下面是斷點調試的截圖, 能夠發現InvokerInvocationHandler中的invoker變成了MockClusterInvoker, 是由代理實例生成的, 暫時不知道爲啥傳入的是MockClusterInvoker
跟進到MockClusterInvoker 的invoker繼續觀察, 能夠看到這時的invoker是FailoverClusterInvoker, 懷疑是在字節碼代理類中使用了包裝器類
繼續跟進, 因爲FailoverClusterInvoker中沒有invoke方法, 能夠找到是在父類AbstractClusterInvoker中實現的.
父類作了一些負載均衡的邏輯, 最後調用doInvoke抽象方法, 在FailoverClusterInvoker中實現的:
doInvoke從父類的select方法獲得一個負載均衡計算後的invoker, 並調用:
Result result = invoker.invoke(invocation);
繼續往上跟蹤, 一直跟到DubboInvoker的doInvoke方法
這個方法裏頭使用ExchangeClient發起遠程調用
return (Result)currentClient.request(inv,timeout).get();
這個再往下挖, 就是基於具體rpc協議(如netty)層面上的調用了, 再也不此專題細究.
總結一下, 客戶端發起遠程調用的大體思路是:
將配置轉化爲子節碼生成的代理實例存到spring上下文中, 調用時, 經過代理實例, 找到相關協議方法發起遠程調用.