1. Dubbo服務(wù)啟動過程
啟動一個Dubbo服務(wù)芥牌,通過啟動日志宛逗,查看Dubbo服務(wù)啟動的過程中都做了哪些事情:
通過啟動日志可以看到,在Dubbo服務(wù)發(fā)布過程中做了以下的一系列動作:
1.暴露本地服務(wù)
2.暴露遠(yuǎn)程服務(wù)
3.啟動Netty服務(wù)
4.連接Zookeeper并到Zookeeper進(jìn)行注冊
5.監(jiān)聽Zookeeper
通過Dubbo發(fā)布過程的詳細(xì)圖解看下服務(wù)提供者暴露服務(wù)的一個詳細(xì)過程:
首先ServiceConfig拿到對外提供服務(wù)的實(shí)際類ref(如Dubbo源碼中 dubbo-demo-xml-provider下的DemoServiceImpl類),然后通過ProxyFactory的getInvoker方法使用ref生成一個AbstractProxyInvoker的實(shí)例土铺,到這一步就完成具體服務(wù)到Invoker的轉(zhuǎn)換,接下來就是Invoker到Exporter的轉(zhuǎn)換板鬓。
2.本地服務(wù)暴露的實(shí)現(xiàn)過程
從Dubbo的啟動過程我們可以得知悲敷,Dubbo服務(wù)提供者,先進(jìn)行本地暴露再進(jìn)行遠(yuǎn)程暴露俭令,在我們?nèi)粘5氖褂脠鼍爸杏玫淖疃嗟氖沁h(yuǎn)程暴露后德。那么為什么還要進(jìn)行本地暴露呢?
很多使用Dubbo框架的應(yīng)用抄腔,可能存在在同一個JVM暴露了遠(yuǎn)程服務(wù)瓢湃,同時同一個JVM內(nèi)部又引用了自身服務(wù)的情況,Dubbo默認(rèn)會把遠(yuǎn)程服務(wù)用injvm協(xié)議再暴露一份赫蛇,這樣消費(fèi)方直接消費(fèi)用一個JVM內(nèi)部的服務(wù)绵患,避免了跨網(wǎng)絡(luò)進(jìn)行遠(yuǎn)程通信。
我們先來看一下啟動Dubbo服務(wù)提供者時最開始輸出的日志:
NFO support.ClassPathXmlApplicationContext: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3b192d32: startup date [Sun Aug 18 22:28:44 CST 2019]; root of context hierarchy
[18/08/19 22:28:44:714 CST] main INFO xml.XmlBeanDefinitionReader: Loading XML bean definitions from class path resource [spring/dubbo-provider.xml]
[18/08/19 22:28:45:257 CST] main INFO logger.LoggerFactory: using logger: org.apache.dubbo.common.logger.log4j.Log4jLoggerAdapter
[18/08/19 22:28:47:550 CST] main INFO config.AbstractConfig: [DUBBO] The service ready on spring started. service: org.apache.dubbo.demo.DemoService, dubbo version: , current host: 192.168.0.102
[18/08/19 22:28:48:151 CST] main INFO utils.Compatibility: Running in ZooKeeper 3.4.x compatibility mode
可以根據(jù)日志看出啟動的流程一下關(guān)鍵步驟:
1.Loading XML bean definitions from class path resource [spring/dubbo-provider.xml] 加載配置文件
2.The service ready on spring started. service: org.apache.dubbo.demo.DemoService, dubbo version: , current host: 192.168.0.102 Service可以啟動
那么我們根據(jù)這句日志輸出做為一個切入點(diǎn)棍掐,來搜索下是哪里輸出了 The service ready on spring started. 這句日志發(fā)現(xiàn)在ServiceBean類中出現(xiàn)
ServiceBean--onApplicationEvent()方法)
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
export();
}
}
我們來具體看一下ServiceBean這個類藏雏,到底是怎么執(zhí)行的。
通過配置文件的加載過程我們可以知道,在服務(wù)啟動時掘殴,Spring會解析dubbo服務(wù)的配置文件赚瘦,并將配置文件中的參數(shù)轉(zhuǎn)換成相應(yīng)的Bean,當(dāng)遇到 <dubbo:service> 時就會轉(zhuǎn)換成ServiceBean奏寨。
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware,
ApplicationEventPublisherAware
以上為ServiceBean的定義起意,集成了ServiceConfig同時實(shí)現(xiàn)了InitializingBean、ApplicationListener等多個接口病瞳。在InitializingBean接口中有一個有個afterPropertiesSet方法揽咕,ServiceBean重寫了該方法,在Bean的屬性初始化時套菜,Spring會默認(rèn)調(diào)用該方法亲善。
同時實(shí)現(xiàn)了ApplicationListener接口,并且重寫了onApplicationEvent()方法逗柴。這兩個接口都是Spring提供的接口蛹头,那么這兩個接口會起到什么作用呢?
ServiceBean在創(chuàng)建完對象之后戏溺,會調(diào)用afterPropertiesSet()方法渣蜗,該方法完成beanClass屬性值的設(shè)置;在IOC容器啟動完成之后旷祸,Spring會自動回調(diào)onApplicationEvent()方法耕拷,該方法完成服務(wù)的暴露,也就是在該方法中托享,我們看到了日志中的
The service ready on spring started.
3.服務(wù)暴露方法實(shí)現(xiàn)的解析
ServiceBean的 onApplicationEvent
ServiceBean--onApplicationEvent()方法源碼如下:
public void onApplicationEvent(ContextRefreshedEvent
event) {
/**如果是暴露的骚烧、并且沒有暴露的則調(diào)用export方法,暴露服務(wù)*/
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) { logger.info("The service ready on spring
started. service: " + getInterface());
}
export();
}
}
該方法為服務(wù)暴露的入口闰围,那么暴露邏輯的真正實(shí)現(xiàn)則是在export方法中止潘,接下來對export方法進(jìn)行解析。
ServicebBean--export()方法:
@Override
public void export() {
super.export();
// Publish ServiceBeanExportedEvent
publishExportEvent();
}
通過export方法的代碼可知辫诅,在這里調(diào)用父類的export()方法,之后調(diào)用調(diào)用publishExportEvent()方法涧狮。
下面看下 ServiceConfig的export()方法都實(shí)現(xiàn)了哪些邏輯炕矮。
public synchronized void export() {
//檢測一些必要的屬性和設(shè)置一些默認(rèn)值
checkAndUpdateSubConfigs();
//判斷是否已經(jīng)導(dǎo)出
if (!shouldExport()) {
return;
}
//判斷是否已經(jīng)設(shè)置了延遲
if (shouldDelay()) {
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
//執(zhí)行導(dǎo)出動作
doExport();
}
}
該方法除了檢測基本的配置,以及在沒有配置的情況下為配置設(shè)置默認(rèn)值之外者冤,最關(guān)鍵的是執(zhí)行 doExport()方法肤视,進(jìn)一步查看doExzport方法都實(shí)現(xiàn)了什么邏輯。
protected synchronized void doExport() {
if (unexported) {
throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
}
if (exported) {
return;
}
exported = true;
if (StringUtils.isEmpty(path)) {
path = interfaceName;
}
doExportUrls();
}
private void doExportUrls() {
List<URL> registryURLs = loadRegistries(true);
for (ProtocolConfig protocolConfig : protocols) {
//拼接pathKey:group/contextpath/interfacename:version
String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
// ProviderModel 表示服務(wù)提供者模型涉枫,此對象中存儲了與服務(wù)提供者相關(guān)的信息邢滑。
// 比如服務(wù)的配置信息,服務(wù)實(shí)例等愿汰。每個被導(dǎo)出的服務(wù)對應(yīng)一個 ProviderModel。
// ApplicationModel 持有所有的 ProviderModel。
ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
ApplicationModel.initProviderModel(pathKey, providerModel);
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
以上代碼通過loadRegistries 加載注冊中心鏈接许赃,然后再遍歷 ProtocolConfig 集合導(dǎo)出每個服務(wù)部逮。并在暴露服務(wù)的過程中,將服務(wù)注冊到注冊中心晌坤。
loadRegistries()方法:
protected List<URL> loadRegistries(boolean provider) {
// check && override if necessary
List<URL> registryList = new ArrayList<URL>();
//判斷注冊配置是否為空,及配置文件中有沒有<dubbo:registry>標(biāo)簽(配置注冊中心的一些信息)
if (CollectionUtils.isNotEmpty(registries)) {
//由于可能配置多個注冊中心地址,在這里遍歷注冊列表
for (RegistryConfig config : registries) {
String address = config.getAddress();
//判斷配置的address是否為空宁昭,如果為空則配置為 0.0.0.0(由于在此步驟前已經(jīng)對registry的地址做了非空校驗(yàn),一般走不到這一步)
if (StringUtils.isEmpty(address)) {
address = ANYHOST_VALUE;
}
if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
Map<String, String> map = new HashMap<String, String>();
appendParameters(map, application);
appendParameters(map, config);
map.put(PATH_KEY, RegistryService.class.getName());
appendRuntimeParameters(map);
if (!map.containsKey(PROTOCOL_KEY)) {
map.put(PROTOCOL_KEY, DUBBO_PROTOCOL);
}
List<URL> urls = UrlUtils.parseURLs(address, map);
for (URL url : urls) {
// 將 URL 協(xié)議頭設(shè)置為 registry
url = URLBuilder.from(url)
.addParameter(REGISTRY_KEY, url.getProtocol())
.setProtocol(REGISTRY_PROTOCOL)
.build();
//判斷是否將地址增加到注冊中心地址列表中
if ((provider && url.getParameter(REGISTER_KEY, true))
|| (!provider && url.getParameter(SUBSCRIBE_KEY, true))) {
registryList.add(url);
}
}
}
}
}
return registryList;
}
經(jīng)過這一步驟之后酗宋,已經(jīng)獲取到了注冊中心的地址积仗,接下來就是調(diào)用doExportUrlsFor1Protocol()方法組裝參數(shù)并且暴露Dubbo服務(wù)。
服務(wù)導(dǎo)出(暴露)過程概述
1. Spring容器啟動時通過NameSpaceHandler來解析Dubbo服務(wù)的配置文件蜕猫,并對配置文件中的參數(shù)進(jìn)行初始化寂曹。
2.加載完配置文件之后,在ServiceBean的onApplicationEvent()方法受到Spring上下文刷新事件后會執(zhí)行導(dǎo)出(export())方法丹锹。該方法為Dubbo服務(wù)導(dǎo)出的起點(diǎn)稀颁。
Export()方法邏輯分析:
1、進(jìn)行參數(shù)配置的檢查楣黍,檢查該方法是否允許導(dǎo)出匾灶,另外檢查該方法是否延遲導(dǎo)出。滿足導(dǎo)出的條件之后租漂,調(diào)用doExport()方法
備注:
* 檢測 <dubbo:service> 標(biāo)簽的 interface 屬性合法性阶女,不合法則拋出異常
* 檢測 ProviderConfig、ApplicationConfig 等核心配置類對象是否為空哩治,若為空秃踩,則嘗試從其他配置類對象中獲取相應(yīng)的實(shí)例。
*檢測并處理泛化服務(wù)和普通服務(wù)類
*檢測本地存根配置业筏,并進(jìn)行相應(yīng)的處理
*對 ApplicationConfig憔杨、RegistryConfig 等配置類進(jìn)行檢測,為空則嘗試創(chuàng)建蒜胖,若無法創(chuàng)建則拋出異常
2消别、doExport()在參數(shù)合法的情況下,調(diào)用doExportUrls()方法台谢,該方法對多協(xié)議寻狂,多注冊中心進(jìn)行了支持。
3朋沮、doExportUrls() 方法調(diào)用 doExportUrlsFor1Protocol()方法蛇券,該方法實(shí)現(xiàn)了由具體服務(wù)到Invoker的轉(zhuǎn)換和Invoker到Exporter的轉(zhuǎn)換。
備注:
invoker由ProxyFactory創(chuàng)建,Dubbo默認(rèn)的ProxyFactory的實(shí)現(xiàn)類是JavassistProxyFactory纠亚。
我是割草的小豬頭塘慕,不斷學(xué)習(xí),不斷進(jìn)步菜枷,后續(xù)陸續(xù)更新Dubbo系列的文章苍糠,如您有興趣一起了解,歡迎關(guān)注啤誊,如文章中有不妥之處岳瞭,歡迎指正!
Dubbo系列文章一--Dubbo重點(diǎn)掌握模塊
Dubbo系列文章二--配置文件加載過程
Dubbo系列文章三--Dubbo源碼結(jié)構(gòu)及實(shí)現(xiàn)方