轉(zhuǎn)自:https://www.yuque.com/docs/share/10960cb9-449b-4177-94b6-493b8f4ff9b9?
一店印、引言
服務(wù)引入是rpc調(diào)用不可或缺的一部分,本文會(huì)圍繞以下幾個(gè)問題對(duì)服務(wù)引入進(jìn)行講解。
- 什么是服務(wù)引入窥浪?服務(wù)引入需要做什么事情?
- spring作為當(dāng)前java項(xiàng)目幾乎必備的框架惧财,如何將服務(wù)引入切入spring?
- 服務(wù)引入如何做到降低對(duì)業(yè)務(wù)代碼的侵入性宋税?
- 不同的提高方可能支持不同的通信協(xié)議,作為一個(gè)rpc框架怎么做到讓調(diào)用端可以針對(duì)不同提高端使用不同的通信協(xié)議坪圾?甚至支持配置自定義協(xié)議晓折?
- 服務(wù)提供者可能出現(xiàn)部分宕機(jī)的情況,如何保證引入服務(wù)的可用性兽泄?
本文以Dubbo的實(shí)現(xiàn)進(jìn)行分析漓概,因?yàn)槿粘m?xiàng)目中常常是配合Spring使用的,所以本文會(huì)以Dubbo結(jié)合Spring的使用進(jìn)行分析病梢。
二胃珍、解析
2.1 概況
2.1.1 什么是服務(wù)引入?
RPC(Remote Procedure Call) 即遠(yuǎn)程過程調(diào)用,包含了兩個(gè)最基本的角色蜓陌,服務(wù)提供者和服務(wù)消費(fèi)者觅彰。
既然是遠(yuǎn)程過程調(diào)用,那么消費(fèi)者在調(diào)用提供者之前就得先拿到服務(wù)提供者的服務(wù)信息(比如最基本的提供者的服務(wù)地址)钮热,并對(duì)調(diào)用信息進(jìn)行封裝準(zhǔn)備好填抬,我們才能對(duì)提供者發(fā)起調(diào)用請求。
而服務(wù)引入就是引入提供者服務(wù)信息隧期,并封裝調(diào)用類的一個(gè)過程飒责。
2.1.2 服務(wù)引入類型
服務(wù)引用****有三種方式
(1) 本地引入
一個(gè)服務(wù)即可以提供者,同時(shí)也可以是消費(fèi)者厌秒。
所以會(huì)存在消費(fèi)者消費(fèi)的服務(wù)读拆,同時(shí)也是當(dāng)前服務(wù)提供的服務(wù),如圖所示:
圖 2.1
針對(duì)這種情況鸵闪,RPC框架應(yīng)該避免發(fā)起網(wǎng)絡(luò)請求檐晕,直接本地調(diào)用。封裝的調(diào)用類應(yīng)當(dāng)通過本地導(dǎo)出的服務(wù)發(fā)起調(diào)用。
(2) 直接服務(wù)
直接服務(wù)指的是直接在消費(fèi)端指定提供端的服務(wù)地址辟灰。
如圖直接將提供者url由消費(fèi)端直接指定个榕,發(fā)起調(diào)用時(shí)直接根據(jù)配置好的url發(fā)起調(diào)用。
圖 2.2
優(yōu)點(diǎn):方便測試芥喇,直接連接服務(wù)西采,不依賴第三方。
缺點(diǎn):存在服務(wù)可用性問題继控,也不能動(dòng)態(tài)伸縮服務(wù)械馆,不建議在線上使用。
(3) 基于注冊中心引入
為了避免提高可用性武通,引入了注冊中心霹崎。服務(wù)提供者將服務(wù)信息注冊到注冊中心,消費(fèi)者訂閱注冊中心獲取提供者url冶忱、提供者配置等服務(wù)信息尾菇,根據(jù)獲取到的服務(wù)信息封裝調(diào)用類發(fā)起調(diào)用。如圖所示:
圖 2.3
優(yōu)點(diǎn):提供者服務(wù)不可用時(shí)自動(dòng)刪除提供者信息囚枪,重啟時(shí)自動(dòng)恢復(fù)派诬,可以動(dòng)態(tài)伸縮服務(wù)。
缺點(diǎn):相比直接引用链沼,需要依賴服務(wù)中心默赂,且需要保證注冊中心的可用性。
2.2 切入spring
spring框架作為當(dāng)前java項(xiàng)目幾乎必不可少的框架忆植,如何接入spring也是一個(gè)RPC框架需要考慮的點(diǎn)放可。
那么Dubbo是怎么接入的spring的呢?
在spring項(xiàng)目中引入Dubbo服務(wù)朝刊,只需要配置<****dubbo****:reference/>標(biāo)簽耀里,然后依賴注入提供者就可以實(shí)現(xiàn)RPC調(diào)用。
那么spring項(xiàng)目啟動(dòng)的時(shí)候拾氓,dubbo是如何做到讓<****dubbo****:reference/>標(biāo)簽被spring識(shí)別并解析呢冯挎?
2.2.1 切入入口
Spring啟動(dòng)時(shí)ClassPathXmlApplicationContext會(huì)對(duì)引入的配置文件進(jìn)行解析,并將bean注入到spring容器中咙鞍。
但是spring并不認(rèn)識(shí)第三方自定義的標(biāo)簽房官,為了支持解析外部自定義的標(biāo)簽,Spring提供了擴(kuò)展點(diǎn)续滋,會(huì)通過查找classPath下所有 spring.handlers 文件翰守,從該文件中獲取所有擴(kuò)展的命名空間處理器。也就是獲取外部的標(biāo)簽處理器疲酌。
文件中的內(nèi)容需要以鍵值對(duì)的方式表示蜡峰,NamespaceUrl為key了袁,value為解析器。spring在解析到某個(gè)外部標(biāo)簽時(shí)湿颅,會(huì)以外部標(biāo)簽的NamespaceUrl為key,獲取對(duì)應(yīng)的解析器载绿,解析該xml標(biāo)簽。
圖 2.4
而Dubbo就是通過這種方式去擴(kuò)展油航,如圖2.4所示崭庸,我們可以看到在Dubbo包下在 META-INF/spring.handlers 文件中,以dubbo標(biāo)簽的NamespaceUrl為key谊囚,解析處理器全限定名為value存儲(chǔ)怕享。Spring解析到dubbo標(biāo)簽時(shí)會(huì)通過dubbo提供的命名空間處理器DubboNamespaceHandler進(jìn)行解析。
在Spring中spring.handlers文件最終由spring的DefaultNamespaceHandlerResolver加載并保存到標(biāo)簽處理器****集合handlerMappings中秒啦。獲取handlerMappings源碼如下:
圖 2.5
如圖2.5所示getHandllerMapping將DubboNamespaceHandler加載進(jìn)了handlerMappings 中熬粗。
spring加載spring.handlers調(diào)用時(shí)序圖如下:
圖 2.6
由圖2.6可知,Spring最終獲取到解析xml標(biāo)簽的處理器之后余境,調(diào)用處理器的init方法、paser方法灌诅,最終獲取到一個(gè)BeanDefinition注冊到spring容器中芳来。
(spring要求自定義命名空間處理器要實(shí)現(xiàn)NamespaceHandler接口,因此都會(huì)init方法和parse方法)
2.2.2 切入細(xì)節(jié)
我們已經(jīng)的得知切入Spring的入口猜拾,那么作為一個(gè)RPC框架即舌,應(yīng)該如何去實(shí)現(xiàn)這個(gè)命名空間解析器,如何將標(biāo)簽轉(zhuǎn)換成spring的BeanDefinition呢挎袜?
針對(duì)Dubbo的分析顽聂,我們已經(jīng)得知dubbo標(biāo)簽最終會(huì)由DubboNamespaceHandler進(jìn)行解析,并且最終Spring會(huì)調(diào)用命名空間解析器的init方法和parse方法盯仪,最終轉(zhuǎn)換成spring的BeanDefinition紊搪。那么DubboNamespaceHandler這兩步做了什么事情?
DubboNamespaceHandler類圖如下:
(1)init 方法
一個(gè)完整的RPC服務(wù)不止是包含服務(wù)引入全景,同時(shí)也是還有協(xié)議定義耀石、服務(wù)導(dǎo)出、注冊中心等等模塊爸黄,而RPC框架就得針對(duì)不同的模塊定義不同的聲明標(biāo)簽滞伟。那么在正式解析之前,就得先將不同模塊的解析區(qū)分開炕贵,Spring也考慮了這一點(diǎn)梆奈,提供了NamespaceHandler接口的實(shí)現(xiàn)抽象類NamespaceHandlerSupport,該類提供了針對(duì)不同模塊的標(biāo)簽進(jìn)行注冊的方法称开,實(shí)際上就是以模塊名為key,對(duì)應(yīng)的解析器為value存儲(chǔ)在一個(gè)Map集合亩钟。
init顧名思義就是做一些初始化操作的,Dubbo就選擇在init方法中注冊不同模塊的標(biāo)簽解析器。
DubboNamespaceHandler#init 源碼如下
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
static {
Version.checkDuplicate(DubboNamespaceHandler.class);
}
public void init() {
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));
// 服務(wù)引入標(biāo)簽解析 指定BeanDefinition BeanClass 為 ReferenceBean
registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
}
}
代碼塊 2.1
如代碼塊2.2.1所示径荔,DubboNamespaceHandler 實(shí)現(xiàn)了NamespaceHandler 的init方法督禽,將各類型標(biāo)簽的對(duì)應(yīng)的解析器注冊到解析器集合Map中。并且可以看出dubbo每個(gè)標(biāo)簽都是通過DubboBeanDefinitionParser進(jìn)行解析总处,只是指定解析后BeanDefinition的beanClass不同狈惫。
(2) parse方法
parse也就是解析標(biāo)簽的方法,已經(jīng)分析過Dubbo會(huì)注冊不同標(biāo)簽的解析器鹦马,那么可以猜想parse方法會(huì)根據(jù)不同標(biāo)簽取出對(duì)應(yīng)的解析器胧谈,再進(jìn)行解析。
NamespaceHandlerSuppor****t#parse 源碼如下:
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 根據(jù)標(biāo)簽名從解析器map拿出解析bean荸频,根據(jù)解析bean的parse方法進(jìn)行解析
return findParserForElement(element, parserContext)
.parse(element, parserContext);
}
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
// 根據(jù)標(biāo)簽名從解析器map拿出解析bean
String localName = parserContext.getDelegate().getLocalName(element);
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
代碼塊 2.2
如代碼塊2.2所示菱肖,根據(jù)標(biāo)簽名,取出對(duì)應(yīng)的解析器旭从,再通過解析器DubboBeanDefinitionParser進(jìn)行解析稳强。
已經(jīng)拿到對(duì)應(yīng)的解析器后,下一步就是將標(biāo)簽解析處理成BeanDefinition并注冊到spring容器和悦。
解析標(biāo)簽創(chuàng)建BeanDefinition退疫,需要指定實(shí)際引用類beanClass, 保存標(biāo)簽類的各種配置信息,再指定beanName將BeanDefinition注冊到spring容器鸽素。
DubboBeanDefinitionParser 通過parse方法進(jìn)行解析xml標(biāo)簽褒繁,部分源碼如下:
public BeanDefinition parse(Element element, ParserContext parserContext) {
// beanClass 即為Dubbo在init創(chuàng)建DubboNamespaceHandler時(shí)指定beanClass.
return parse(element, parserContext, beanClass, required);
}
private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {
RootBeanDefinition beanDefinition = new RootBeanDefinition();
// 配置RootBeanDefinition bean類型
// 比如服務(wù)引用類型 則為ReferenceBean,id屬性即為引用的bean名
beanDefinition.setBeanClass(beanClass);
String id = element.getAttribute("id");
...
if (id != null && id.length() > 0) {
if (parserContext.getRegistry().containsBeanDefinition(id)) {
throw new IllegalStateException("Duplicate spring bean id " + id);
}
// 以id作為bean名注冊beanDefinition 到spring容器
parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
beanDefinition.getPropertyValues().addPropertyValue("id", id);
}
... 一系列配置bean過程
... 將標(biāo)簽上配置的參數(shù)存到beandefinition的PropertyValues屬性中
return beanDefinition;
}
代碼塊 2.3
以服務(wù)引用標(biāo)簽為例子馍忽,beanClass為ReferenceBean棒坏。
根據(jù)代碼塊2.3,DubboBeanDefinitionParser 通過 parse方法進(jìn)行解析獲取到beanClass為ReferenceBean的BeanDefinition遭笋,注冊到spring容器并返回解析結(jié)果坝冕。
ps:
從該源碼還可以看到,在解析的時(shí)候坐梯,發(fā)現(xiàn)spring容器中已經(jīng)包含這個(gè)bean名徽诲,那么會(huì)報(bào)錯(cuò)重復(fù)的bean id,因此****dubbo****:reference標(biāo)簽指定的id不能是已經(jīng)存在spring容器中的bean名
2.2.3 結(jié)論
基于Dubbo在服務(wù)引入切入Spring的方式吵血,我們可以得到一下結(jié)論:
- RPC框架服務(wù)引入切入spring可以通過spring提供的xml解析擴(kuò)展點(diǎn)spring.handlers谎替,對(duì)xml解析進(jìn)行了擴(kuò)展。
- 框架定義的解析器蹋辅,可以基于NamespaceHandlerSupport在init針對(duì)不同標(biāo)簽注冊不同的自定義解析器钱贯,NamespaceHandlerSupport#****parse方法會(huì)去根據(jù)不同標(biāo)簽獲取對(duì)應(yīng)的自定義解析器。
- 自定義解析器parse方法需要?jiǎng)?chuàng)建BeanDefinition,保存標(biāo)簽配置信息侦另,注冊BeanDefinition到Spring容器秩命。
2.3 服務(wù)引入實(shí)現(xiàn)分析
服務(wù)引入需要根據(jù)配置信息封裝服務(wù)調(diào)用類尉共,作為一個(gè)RPC框架還需要考慮如何支持區(qū)分多種服務(wù)引入類型,如何避免服務(wù)引入對(duì)業(yè)務(wù)代碼的侵入弃锐,如何提高服務(wù)引入的可擴(kuò)展性袄友,支持用戶自己指定引入服務(wù)調(diào)用使用的協(xié)議。
本節(jié)通過分析Dubbo的實(shí)現(xiàn)霹菊,解析Dubbo是怎么處理的剧蚣。
根據(jù)2.2節(jié)的分析,xml解析只是將引入的bean轉(zhuǎn)化成BeanDefinition旋廷,保存了配置一些信息鸠按,并沒有去封裝一個(gè)服務(wù)調(diào)用類。Dubbo的服務(wù)引用標(biāo)簽<reference>標(biāo)簽最終會(huì)被spring解析成ReferenceBean類型的BeanDefinition饶碘,并將標(biāo)簽上配置的參數(shù)注入到BeanDefinition(比如引用服務(wù)的權(quán)限定名)目尖,并加載到bean容器中。
我們根據(jù)以下幾點(diǎn)做下分析
- ReferenceBean是什么時(shí)候真正去封裝服務(wù)調(diào)用類的
- 我們依賴注入的是引用類, 為什么beanClass是ReferenceBean
- 為什么我們直接注入引用類就可以關(guān)聯(lián)上提供者類扎运,并發(fā)起rpc調(diào)用
ReferenceBean類圖如下:
圖 2.7
2.3.1 封裝調(diào)用類時(shí)機(jī)
(1)懶漢式
懶漢式即用到ReferenceBean這個(gè)引入服務(wù)被用到才去封裝瑟曲。
由圖2.7可以看到ReferenceBean實(shí)現(xiàn)了FactoryBean,因此當(dāng)我們依賴ReferenceBean的時(shí)候绪囱,spring會(huì)調(diào)用getObject()方法去獲取真實(shí)的bean测蹲。
ReferenceBean#getObject部分源碼如下:
public Object getObject() throws Exception {
return get();
}
public synchronized T get() {
if (destroyed){
throw new IllegalStateException("Already destroyed!");
}
// ReferenceBean 全局變量ref 代表這個(gè)引用bean真實(shí)的業(yè)務(wù)bean
if (ref == null) {
init();
}
return ref;
}
private void init() {
// 做一些創(chuàng)建代理類前的校驗(yàn)和配置操作(比如校驗(yàn)接口合法性、封裝URL參數(shù)到map(比如interface=com.xxService))
...
// 根據(jù)參數(shù)map調(diào)用封裝調(diào)用類
// 根據(jù)調(diào)用類創(chuàng)建代理類鬼吵,將代理類賦予ref變量
ref = createProxy(map);
...
}
代碼塊 2.4
可以看到這個(gè)方法就是調(diào)用init()方法初始化,并將引用ref返回, 也就是當(dāng)引入服務(wù)被依賴到的時(shí)候篮赢,會(huì)去封裝調(diào)用類齿椅。
(2)****餓漢式
餓漢式即引用類沒有被依賴也會(huì)
ReferenceBean實(shí)現(xiàn)了InitializingBean,因此初始化ReferenceBean時(shí)启泣,Spring容器會(huì)調(diào)用 ReferenceBean的afterPropertiesSet方法涣脚。
ReferenceBean部分源碼如下:
public void afterPropertiesSet() throws Exception {
····
裝載 監(jiān)控器、注冊中心信息寥茫、應(yīng)用配置信息遣蚀、消費(fèi)端配置信息等等
····
Boolean b = isInit();
if (b == null && getConsumer() != null) {
b = getConsumer().isInit();
}
if (b != null && b.booleanValue()) {
getObject();
}
}
代碼塊 2.5
通過代碼塊2.5 我們可以看到,afterPropertiesSet 方法主要做了一些初始化操作纱耻,最后判斷是否初始化bean, 如果需要?jiǎng)t會(huì)調(diào)用init()方法初始化bean芭梯,賦予ref變量。
ps:
默認(rèn)是關(guān)閉狀態(tài)弄喘,即不會(huì)開啟玖喘。需要初始化可通過配置<dubbo:reference>
的 init 屬性開啟。
2.3.2 調(diào)用類封裝細(xì)節(jié)
服務(wù)引入需要封裝調(diào)用類蘑志,需要做哪些事情累奈?
- 獲取提供端地址贬派。封裝調(diào)用類,首先要知道提供者的地址澎媒,并且因?yàn)橛卸喾N類型的服務(wù)引入搞乏,得區(qū)分多種服務(wù)引入方式的服務(wù)地址。
- 獲取傳輸協(xié)議戒努。不同的提供者可能支持的傳輸協(xié)議不一致请敦,因此需要獲取傳輸協(xié)議類型。
- 根據(jù)獲取到的配置信息創(chuàng)建調(diào)用類柏卤。
- 封裝代理類冬三。為了避免造成代碼侵入,不能讓業(yè)務(wù)代碼直接依賴框架封裝的調(diào)用類缘缚,所以需要支持讓業(yè)務(wù)代碼可以直接依賴提供端勾笆。那么RPC框架就需要根據(jù)提供端和調(diào)用類封裝一個(gè)代理類。
2.3.2.1 獲取提供端地址
前面我們講過桥滨,服務(wù)引入分為三種類型(本地引入窝爪、直接引入、基于注冊中心引入)齐媒,三種引入類型獲取的提供端地址也不同蒲每。
- 本地引入
判斷是否為本地調(diào)用鬼癣,如果是本地調(diào)用則根據(jù)提供端信息拼接URL,格式為injvm:127.0.0.1:0/com.service?param矢劲。
判斷是否為本地調(diào)用流程圖如下:
圖 2.8
通過流程圖我們可以看到,如果ReferenceBean指定的inJvm=ture或者scope=local則認(rèn)為是本地調(diào)用(通過標(biāo)簽配置)他膳。
否則如果作用域沒有指定remote唬血、并且不是泛化調(diào)用望蜡、并且本地暴露的服務(wù)包含該服務(wù)才認(rèn)為是本地引用。
- 直接引用
如果判斷不是本地調(diào)用拷恨,則判斷是否存在直接引用地址(通過標(biāo)簽的url指定)脖律。
如果是存在直接引用URL,假設(shè)配置的URL是dubbo協(xié)議的腕侄,則url的格式為 dubbo://service-host/com.service?param小泉。
因?yàn)橹苯右靡部赡苁桥渲米灾行牡刂?/p>
因此Dubbo判斷是直接引用是registry前綴的地址,則會(huì)加上refer參數(shù)冕杠,標(biāo)示實(shí)際調(diào)用哪個(gè)提供者微姊,如下。
- 基于注冊中心引用
如果沒有指定引用URL拌汇,則會(huì)通過加載注冊中心地址柒桑,獲取到注冊中心的地址集合,URL的格式為
registry://registry-host/org.apache.dubbo.registry.RegistryService?refer=URL.encode("consumer://host/com.Service?version=1.0.0")
即地址為注冊中心地址,refer參數(shù)為實(shí)際引用的提供者
需要注意的是噪舀,直接引用都是可能配置多個(gè)地址的魁淳,而通過注冊中心獲取也可能會(huì)獲取到多個(gè)提供端地址飘诗,因此獲取到的地址可能是多個(gè)的。
2.3.2.2 獲取傳輸協(xié)議
Dubbo針對(duì)不同協(xié)議都封裝了對(duì)應(yīng)的Protocol類界逛,因此本節(jié)分析Dubbo如何根據(jù)當(dāng)前傳輸協(xié)議獲取對(duì)應(yīng)的Protocol類昆稿。
截取ReferenceBean Protocol的獲取如下,
private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
我們可以看到ReferenceBean中并沒有固定創(chuàng)建某一個(gè)ReferenceBean的實(shí)現(xiàn)類息拜,那么Dubbo是怎么做到根據(jù)不同傳輸協(xié)議獲取對(duì)應(yīng)Protocol的呢溉潭?
分析getAdaptiveExtension()的實(shí)現(xiàn),該方法最終創(chuàng)建了一個(gè)Protocol的代理對(duì)象少欺,由該代理對(duì)象來根據(jù)當(dāng)前傳輸U(kuò)RL獲取對(duì)應(yīng)的Protocol喳瓣。
該方法內(nèi)部根據(jù)代理的對(duì)象類型(比如:Protocol)動(dòng)態(tài)拼接java代碼,動(dòng)態(tài)拼接code生成自適應(yīng)擴(kuò)展對(duì)象赞别,并動(dòng)態(tài)編譯畏陕,通過類加載器加載到j(luò)vm中,返回代理對(duì)象仿滔。
流程圖如下:
圖 2.9
以Protocol為例子惠毁,拼接后的java代碼如下:
package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
// 不被代理的方法 如果被調(diào)用直接報(bào)錯(cuò)
public void destroy() {throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}
public int getDefaultPort() {throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}
// 代理導(dǎo)出方法
public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");com.alibaba.dubbo.common.URL url = arg0.getUrl();
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.export(arg0);
}
public void destroyServer() {throw new UnsupportedOperationException("method public default void com.alibaba.dubbo.rpc.Protocol.destroyServer() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}
// 代理引入方法
public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws java.lang.Class {
if (arg1 == null) throw new IllegalArgumentException("url == null");
// 獲取url參數(shù)
com.alibaba.dubbo.common.URL url = arg1;
// 獲取url上配置的協(xié)議
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
// 根據(jù)協(xié)議名稱 從dubbo容器中獲取對(duì)應(yīng)的協(xié)議類
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
// 根據(jù)協(xié)議類創(chuàng)建調(diào)用對(duì)應(yīng)的服務(wù)引入方法
return extension.refer(arg0, arg1);
}
}
dubbo就是通過動(dòng)態(tài)拼接java code,在運(yùn)行時(shí)生成自適應(yīng)擴(kuò)展bean崎页,由這個(gè)bean來獲取ReferenceBean配置的protocol和cluster對(duì)應(yīng)的實(shí)現(xiàn)類.
動(dòng)態(tài)拼接code邏輯如下:
圖 2.10
主要思想:
通過@Adaptive注解表明哪些方法需要被代理鞠绰,被代理的方法都要能提供URL參數(shù),代理對(duì)象會(huì)根據(jù)URL以被代理類為key飒焦,獲取對(duì)應(yīng)參數(shù)值蜈膨,從而返回對(duì)應(yīng)的實(shí)現(xiàn)類的bean名,再通過bean名從Dubbo容器中獲取對(duì)應(yīng)的實(shí)現(xiàn)累牺荠。如果URL上沒有指明用哪個(gè)實(shí)現(xiàn)類丈挟,則用@SPI注解上的值為key獲取對(duì)應(yīng)的實(shí)現(xiàn)類。
以Protocol為例,源碼如下:
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
void destroyServer();
}
代碼塊 2.6
export和refer方法都被@Adaptive修飾志电,因此這兩個(gè)方法。生成的代理類從URL上獲取以protocol為名的參數(shù)值作為key蛔趴,從dubbo容器中取出對(duì)應(yīng)的實(shí)現(xiàn)類去執(zhí)行挑辆。如果沒有獲取到則以@SPI上的值dubbo作為key取出對(duì)應(yīng)的protocol。即默認(rèn)得到了DubboProtocol孝情。
ps
同樣通過這個(gè)機(jī)制鱼蝉,也進(jìn)而得以支持Spi擴(kuò)展,可以在運(yùn)行時(shí)才確認(rèn)使用哪個(gè)實(shí)現(xiàn)類箫荡,方便外部擴(kuò)展魁亦。比如我新增一個(gè)自定義協(xié)議MyProtocol,配置指定服務(wù)引入的協(xié)議為myProtocol, 并將這個(gè)協(xié)議對(duì)應(yīng)的實(shí)現(xiàn)類基于dubbo的spi擴(kuò)展注入到Dubbo容器中羔挡。那么代理類就可以根據(jù)URL上的協(xié)議名洁奈,基于spi獲取對(duì)應(yīng)的協(xié)議實(shí)現(xiàn)類间唉,再根據(jù)調(diào)用類調(diào)用refer方法。
2.3.2.3 封裝調(diào)用類
獲取到提供者的url和傳輸?shù)膮f(xié)議對(duì)象后利术,就開始封裝調(diào)用對(duì)象了呈野。
封裝調(diào)用對(duì)象需要考慮做幾個(gè)事情
- 先前基于注冊中心引入服務(wù)的地址,并非最終發(fā)起調(diào)用的協(xié)議和地址印叁,而是以注冊中心地址為路徑被冒,提供端地址為參數(shù)組合。因此需要區(qū)分開來轮蜕,封裝真正的調(diào)用地址昨悼。并且registry協(xié)議并非真正傳輸協(xié)議,只是標(biāo)識(shí)是注冊中心引入跃洛,封裝調(diào)用類還得替換成真正的傳輸協(xié)議率触,比如dubbo協(xié)議。
- 由于提供者有可能有多個(gè)提供者税课,因此需要考慮如何將多個(gè)提供者封裝成一個(gè)調(diào)用者闲延,發(fā)起調(diào)用時(shí)如何處理。
- 為了便于知道消費(fèi)端消費(fèi)情況韩玩,消費(fèi)端也需要將消費(fèi)的服務(wù)注冊到注冊中心垒玲。并且為了在提供者發(fā)生變動(dòng)時(shí)收到通知,還需要訂閱提供者的節(jié)點(diǎn)數(shù)據(jù)找颓。
截取ReferenceConfig#createProxy封裝調(diào)用類invoke源碼如下:
// 截取部分注冊中心和直接引用獲取到URL集合后的代碼
if (urls.size() == 1) {
invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
....
// 遍歷多個(gè)url 生成invoker集合
for (URL url : urls) {
invokers.add(refprotocol.refer(interfaceClass, url));
}
if (registryURL != null) {
// 指定Cluster為AvailableCluster 選擇任意可用的服務(wù)
// 如果是注冊中心則說明當(dāng)前遍歷的是注冊中心地址合愈,所以使用AvailableCluster封裝invoke
URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
// 基于Cluster合并多個(gè)invoker 基于集群容錯(cuò)策略調(diào)用
invoker = cluster.join(new StaticDirectory(u, invokers));
} else {
invoker = cluster.join(new StaticDirectory(invokers));
}
}
代碼塊 2.7
如代碼塊2.7所示,只有一個(gè)URL時(shí)直接通過協(xié)議類封裝一個(gè)invoker對(duì)象击狮,如果有多個(gè)URL(即多個(gè)服務(wù)提供者)則通過Cluster封裝多個(gè)佛析,后續(xù)基于集群容錯(cuò)策略做調(diào)用(關(guān)于Cluster相關(guān)本文不做講解,屬于負(fù)載均衡處理模塊的范疇)彪蓬。
以Protocol為例寸莫,調(diào)用該代理對(duì)象refer方法,代理對(duì)象會(huì)解析refer方法傳入的URL不同的協(xié)議獲取到不同的Protocol實(shí)現(xiàn)類档冬,通過對(duì)應(yīng)協(xié)議類****Protocol創(chuàng)建對(duì)應(yīng)invoke實(shí)現(xiàn)類膘茎,不同協(xié)議類會(huì)創(chuàng)建不同的invoke類。
關(guān)于refer方法的實(shí)現(xiàn)酷誓,本地引入和直接引入都是直接根據(jù)URL披坏、interfaceClass創(chuàng)建Invoke實(shí)現(xiàn)類, 重點(diǎn)講一下RegistryProtocol。
RegistryProtocol的refer方法盐数,截取關(guān)鍵源碼如下:
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 修改url的協(xié)議內(nèi)容 根據(jù)參數(shù)配置的協(xié)議進(jìn)行修改棒拂,如果沒有配置默認(rèn)為dubbo
url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
// 連接注冊中心
Registry registry = registryFactory.getRegistry(url);
...
return doRefer(cluster, registry, type, url);
}
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// 生成消費(fèi)服務(wù)地址 如consumer:consumer-host/com.xxService?param
// com.xxService表示消費(fèi)的提供者全限制定名
URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters());
...
// 在注冊中心消費(fèi)者目錄注冊消費(fèi)端地址
// 例如:在zk注冊消費(fèi)端地址的目錄為 /分組名/服務(wù)權(quán)限定名/consumers/subscribeUrl
registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
Constants.CHECK_KEY, String.valueOf(false)));
// 訂閱注冊中心節(jié)點(diǎn)數(shù)據(jù) providers、routers玫氢、configurators
// 訂閱的時(shí)候會(huì)
directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY, Constants.PROVIDERS_CATEGORY +
"," + Constants.CONFIGURATORS_CATEGORY + "," + Constants.ROUTERS_CATEGORY));
// 這里使用cluster是因?yàn)樽灾行目赡軙?huì)有多個(gè)提供者帚屉,因此返回的invoke是具備選擇提供者能力的invoke
return cluster.join(directory);
}
代碼塊 2.8
如代碼塊2.8所示谜诫,RegistryProtocol的refer方法主要是做了幾個(gè)事情
- 將原本url為registry協(xié)議,修改成真正發(fā)起調(diào)用使用的協(xié)議涮阔,默認(rèn)為dubbo猜绣。
- 連接注冊中心,創(chuàng)建注冊中心實(shí)例敬特。
- 注冊消費(fèi)端地址掰邢。
- 訂閱注冊中心提供端和配置數(shù)據(jù)。
- 基于RegistryDirectory和cluster創(chuàng)建invoke對(duì)象返回伟阔。
思考:
- 當(dāng)多注冊中心和多提供者時(shí)辣之,怎么選擇注冊中心和怎么選提供者流程是怎么設(shè)計(jì)?
- 注冊中心最大的作用是可以提高服務(wù)調(diào)用的可用性皱炉,某個(gè)提供者服務(wù)掛了之后怀估,自動(dòng)下線該提供者,避免調(diào)用到該提供者合搅,或者流量增大服務(wù)無法應(yīng)對(duì)多搀,動(dòng)態(tài)擴(kuò)容提供者。那么如何在提供者變動(dòng)時(shí)去更新invoke.
圖 2.10
針對(duì)第一點(diǎn)灾部,dubbo基于多個(gè)注冊中心url生成invoke集合康铭,再通過StaticDirectory包裝,cluster固定使用****AvailableCluster進(jìn)行選擇任意可用的節(jié)點(diǎn)(代碼塊2.7)赌髓。獲取到clusterInvoke之后从藤,再根據(jù)cluster策略(服務(wù)引入配置)選擇一個(gè)提供者。
針對(duì)第二點(diǎn)锁蠕,dubbo處理某個(gè)注冊中心url時(shí)夷野,返回的是通過RegistryDirectory與cluster創(chuàng)建的invoke,RegistryDirectory會(huì)監(jiān)聽注冊中心的通知,動(dòng)態(tài)更新提供者集合荣倾。
dubbo基于RegistryDirectory訂閱注冊中心悯搔,訂閱的時(shí)候會(huì)將當(dāng)前RegistryDirectory作為監(jiān)聽器,當(dāng)訂閱的節(jié)點(diǎn)發(fā)生變動(dòng)的時(shí)候就會(huì)通知RegistryDirectory更新invoke集合舌仍。notity方法源碼截取如下
public synchronized void notify(List<URL> urls) {
// 1. 根據(jù)URL的協(xié)議類型封裝各種訂閱URL集合
// 2. 更新configurators URL
// 3. 更新routers URL
...
// 4. 更新providers URL
refreshInvoker(invokerUrls);
}
private void refreshInvoker(List<URL> invokerUrls){
...
// 將URL列表轉(zhuǎn)成Invoker列表
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls) ;
...
this.urlInvokerMap = newUrlInvokerMap;
// 關(guān)閉未使用的Invoker
destroyUnusedInvokers(oldUrlInvokerMap,newUrlInvokerMap);
}
代碼塊 2.10
2.3.2.4 創(chuàng)建代理類
根據(jù)引用的服務(wù)類和invoke對(duì)象生成代理對(duì)象返回鳖孤。
// ReferenceConfig源碼 代理工廠代理類,根據(jù)url獲取對(duì)應(yīng)代理類
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
proxyFactory.getProxy(invoker);
代碼塊 2.9
默認(rèn)代理方式支持Javassist和Jdk代理抡笼,可以通過引用標(biāo)簽配置proxy指定。
與上面講述的Protocol一樣黄鳍,是一個(gè)動(dòng)態(tài)生成的類推姻,會(huì)根據(jù)url上的參數(shù)獲取對(duì)應(yīng)的動(dòng)態(tài)代理實(shí)現(xiàn)類。
為什么要?jiǎng)?chuàng)建代理類框沟?
假設(shè)我們不創(chuàng)建代理類藏古,那么生成的就是invoke對(duì)象增炭,客戶端就不能通過直接注入提供服務(wù)類方式,而是要依賴invoke對(duì)象拧晕,造成代碼入侵隙姿。
有了代理類我們就可以直接注入提供者,實(shí)際上調(diào)用的時(shí)候就是通過invoke發(fā)起調(diào)用了厂捞。
2.4 拓展思考回顧
- 本地通過<dubbo:provider>暴露了dubbo服務(wù)输玷,那么我們調(diào)用本地dubbo服務(wù)時(shí)是否會(huì)發(fā)起網(wǎng)絡(luò)請求?
- 通過手動(dòng)創(chuàng)建T****estService注入spring容器,又通過<dubbo:reference>引用服務(wù)T****estService靡馁,依賴注入獲取到的是哪個(gè)bean欲鹏?
- 使用懶漢式時(shí),只通過Spring注入引用類就不會(huì)立即創(chuàng)建調(diào)用類臭墨,而是實(shí)際用到才創(chuàng)建嗎赔嚎?
問題解答:
- 不會(huì),使用的是本地服務(wù)調(diào)用
- 以注入beanName為主,beanName無法對(duì)應(yīng)則隨機(jī)取一個(gè)胧弛。
- 會(huì)取封裝調(diào)用類尤误。
針對(duì)第三個(gè)問題進(jìn)行解析,在依賴注入的時(shí)候结缚,注入的bean就會(huì)被加載了损晤,因此ReferenceBean實(shí)現(xiàn)的getObject方法就會(huì)被調(diào)用,調(diào)用類也會(huì)被封裝創(chuàng)建掺冠。
ReferenceBean****懶加載常規(guī)情況下沉馆,只能保證當(dāng)你服務(wù)中沒有依賴引入的服務(wù)時(shí),保證getObject不會(huì)被執(zhí)行德崭。
這時(shí)有人可能想問了斥黑,ReferenceBean的BeanDefinition不是都加入到Spring容器中了嗎,Spring容器不是會(huì)對(duì)進(jìn)行所有BeanDefinition進(jìn)行初始化創(chuàng)建嗎眉厨?
其實(shí)Spring加載所有BeanDefinition去創(chuàng)建時(shí)锌奴,BeanDefinition因?yàn)楸旧韺?shí)際上是ReferenceBean,會(huì)先以 &beanName 創(chuàng)建ReferenceBean本身憾股。然后再判斷要不要是否需要早期初始化鹿蜀,如果需要才會(huì)去創(chuàng)建真實(shí)的bean。
所以如果在沒有被依賴的情況下服球,也就不會(huì)以beanName去創(chuàng)建bean茴恰,所以也就不會(huì)去調(diào)用getObject。
public void preInstantiateSingletons() throws BeansException {
...
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
// 是否為懶加載
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
// 判斷是否為FactoryBean
if (isFactoryBean(beanName)) {
// FACTORY_BEAN_PREFIX = &
// &beanName 表示獲取實(shí)現(xiàn)FactoryBean的類本身
final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
// 判斷是否是SmartFactoryBean
// 如果只是實(shí)現(xiàn)FactoryBean斩熊,則默認(rèn)不會(huì)去創(chuàng)建真實(shí)的object
boolean isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
// 不需要早期初始化 因此不會(huì)去調(diào)用getObject
if (isEagerInit) {
// 獲取beanName對(duì)應(yīng)的bean 真正創(chuàng)建調(diào)用getObject創(chuàng)建bean
getBean(beanName);
}
}
....
}
}
}
怎么實(shí)現(xiàn)懶漢式加載ReferenceBean****往枣,但是又要依賴引用類?
可以搭配@Lazy使用,讓依賴的bean被懶加載分冈,這時(shí)獲取到的是懶加載bean代理類圾另,只有真正發(fā)起調(diào)用時(shí)才會(huì)去獲取bean,這樣就可以實(shí)現(xiàn)在真正發(fā)起調(diào)用才調(diào)用getObject創(chuàng)建服務(wù)引用調(diào)用類雕沉。
三集乔、總結(jié)
服務(wù)引入最基本的實(shí)現(xiàn)就是根據(jù)提供者信息封裝成一個(gè)調(diào)用類,但是作為一個(gè)優(yōu)秀的RPC框架坡椒,得考慮方方面面的問題扰路。
- 避免代碼侵入。為引入服務(wù)生成代理類肠牲。
- 提高可用性幼衰。引入了注冊中心處理機(jī)制。
- 提高擴(kuò)展性缀雳。引入了自定義適應(yīng)類渡嚣,根據(jù)url參數(shù)自動(dòng)選擇對(duì)應(yīng)的實(shí)現(xiàn)類。同時(shí)也牽扯到Dubbo實(shí)現(xiàn)了自己的IOC容器肥印。
- 提高啟動(dòng)性能识椰,避免加載無效引入。引入了懶加載機(jī)制深碱。
- Spring作為廣泛使用的框架如何接入啟動(dòng)腹鹉。基于Spring的擴(kuò)展機(jī)制敷硅,實(shí)現(xiàn)了一套加載機(jī)制功咒。