面试必备:Dubbo中暴露服务的过程解析

283 篇文章 18 订阅

Dubbo会在Spring实例化完bean之后,在刷新容器最后一步发布ContextRefreshEvent事件的时候,通知实现了ApplicationListener的ServiceBean类进行回调onApplicationEvent 事件方法,dubbo会在这个方法中调用ServiceBean父类ServiceConfig的export方法,而该方法真正实现了服务的(异步或者非异步)发布。

加载dubbo配置

Spring容器在启动的时候,会读取到Spring默认的一些schema以及Dubbo自定义的schema,每个schema都会对应一个自己的NamespaceHandler,NamespaceHandler里面通过BeanDefinitionParser来解析配置信息并转化为需要加载的bean对象。

遇到dubbo名称空间 ,首先会调用DubboNamespaceHandler类的 init方法 进行初始化操作。

根据命名空间去获取具体的处理器NamespaceHandler。那具体的处理器是在哪定义的呢,在” META-INF/spring.handlers”文件中,Spring在会自动加载该文件中所有内容。

META-INF/spring.handlers

http\:
//code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler

META-INF/spring.schemas

http\:
//code.alibabatech.com/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsd

根据不同的XML节点,会委托NamespaceHandlerSupport 类找出合适的BeanDefinitionParser,其中Dubbo所有的标签都使用 DubboBeanDefinitionParser进行解析,基于一对一属性映射,将XML标签解析为Bean对象。

DubboNamespaceHandler.java 类代码如下

package com.alibaba.dubbo.config.spring.schema;


import
 org.springframework.beans.factory.xml.
NamespaceHandlerSupport
;


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
));

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

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

    }


}

由于DubboBeanDefinitionParser 类中 parse转换的过程代码还是比较复杂,只抽离出来bean的注册这一块的代码如下

DubboBeanDefinitionParser.java 类代码如下

package com.alibaba.dubbo.config.spring.schema;


public
 
class
 
DubboBeanDefinitionParser
 
implements
 
BeanDefinitionParser
 {


    
@SuppressWarnings
(
"unchecked"
)

    
private
 
static
 
BeanDefinition
 parse(
Element
 element, 
ParserContext
 parserContext, 
Class
<?> beanClass, 
boolean
 required) {


    
RootBeanDefinition
 beanDefinition = 
new
 
RootBeanDefinition
();

        beanDefinition.setBeanClass(beanClass);

        beanDefinition.setLazyInit(
false
);

        
String
 id = element.getAttribute(
"id"
);

        
//省略......

         
if
(id != 
null
 && id.length() > 
0
) {

            
if
(parserContext.getRegistry().containsBeanDefinition(id)) {

                
throw
 
new
 
IllegalStateException
(
"Duplicate spring bean id "
 + id);

            }

            
//registerBeanDefinition 注册Bean的定义

            
//具体的id如下 applicationProvider.xml解析后的显示 id,

            
//如id="dubbo_provider"  beanDefinition = "ApplicationConfig"

            parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);

            beanDefinition.getPropertyValues().addPropertyValue(
"id"
, id);

        }

     }  

}

通过DubboBeanDefinitionParser 类的 parse方法会将class信息封装成BeanDefinition,然后将BeanDefinition再放进DefaultListableBeanFactory的beanDefinitionMap中。

最后通过Spring bean 的加载机制进行加载。

服务暴露过程

Dubbo会在Spring实例化完bean之后,在刷新容器最后一步发布ContextRefreshEvent事件的时候,通知实现了ApplicationListener的ServiceBean类进行回调onApplicationEvent 事件方法,dubbo会在这个方法中调用ServiceBean父类ServiceConfig的export方法,而该方法真正实现了服务的(异步或者非异步)发布。

服务暴露入口

由服务配置类 ServiceConfig 进行初始化工作及服务暴露入口,首先进去执行该类的export()方法。

ServiceConfig.java 类的 export 方法

export的步骤简介

  1. 首先会检查各种配置信息,填充各种属性,总之就是保证我在开始暴露服务之前,所有的东西都准备好了,并且是正确的。

  2. 加载所有的注册中心,因为我们暴露服务需要注册到注册中心中去。

  3. 根据配置的所有协议和注册中心url分别进行导出。

  4. 进行导出的时候,又是一波属性的获取设置检查等操作。

  5. 如果配置的不是remote,则做本地导出。

  6. 如果配置的不是local,则暴露为远程服务。

  7. 不管是本地还是远程服务暴露,首先都会获取Invoker。

  8. 获取完Invoker之后,转换成对外的Exporter,缓存起来。

export方法先判断是否需要延迟暴露(这里我们使用的是不延迟暴露),然后执行doExport方法。

doExport方法先执行一系列的检查方法,然后调用doExportUrls方法。检查方法会检测dubbo的配置是否在Spring配置文件中声明,没有的话读取properties文件初始化。

doExportUrls方法先调用loadRegistries获取所有的注册中心url,然后遍历调用doExportUrlsFor1Protocol方法。对于在标签中指定了registry属性的Bean,会在加载BeanDefinition的时候就加载了注册中心。

ServiceConfig.java 类的 export 方法

package com.alibaba.dubbo.config;


public
 
class
 
ServiceConfig
<T> 
extends
 
AbstractServiceConfig
 {


public
 
synchronized
 
void
 export() {

    
if
 (provider != 
null
) {

        
if
 (export == 
null
) {

            export = provider.getExport();

        }

        
if
 (delay == 
null
) {

            delay = provider.getDelay();

        }

    }

    
if
 (export != 
null
 && !export) {

        
return
;

    }


    
if
 (delay != 
null
 && delay > 
0
) {

        delayExportExecutor.schedule(
new
 
Runnable
() {

            
public
 
void
 run() {

                doExport();

            }

        }, delay, 
TimeUnit
.MILLISECONDS);

    } 
else
 {

        doExport();

    }

}

可以看出发布发布是支持延迟暴露发布服务的,这样可以用于当我们发布的服务非常多,影响到应用启动的问题,前提是应用允许服务发布的延迟特性。

接下来就进入到 ServiceConfig.java 类的 doExport() 方法。

检查DUBBO配置的合法性

ServiceConfig.java 类的 doExport方法。检查DUBBO配置的合法性,并调用doExportUrls 方法。

package com.alibaba.dubbo.config;


public
 
class
 
ServiceConfig
<T> 
extends
 
AbstractServiceConfig
 {


protected
 
synchronized
 
void
 doExport() {

       
// 省略。。。

        checkApplication();

        checkRegistry();

        checkProtocol();

        appendProperties(
this
);

        checkStubAndMock(interfaceClass);

        
if
 (path == 
null
 || path.length() == 
0
) {

            path = interfaceName;

        }

        doExportUrls();

    }

}

我们可以看出该方法的实现的逻辑包含了根据配置的优先级将 ProviderConfig,ModuleConfig,MonitorConfig,ApplicaitonConfig等一些配置信息进行组装和合并。还有一些逻辑是检查配置信息的合法性。最后又调用了doExportUrls方法。

服务多协议暴露过程

ServiceConfig.java 类的 doExportUrls() 方法

package com.alibaba.dubbo.config;


public
 
class
 
ServiceConfig
<T> 
extends
 
AbstractServiceConfig
 {


    
@SuppressWarnings
({
"unchecked"
, 
"rawtypes"
})

    
private
 
void
 doExportUrls() {

        
List
<URL> registryURLs = loadRegistries(
true
);

        
for
 (
ProtocolConfig
 protocolConfig : protocols) {

            doExportUrlsFor1Protocol(protocolConfig, registryURLs);

        }

    }

}

该方法第一步是加载注册中心列表

loadRegistries(true);加载注册中心列表响应示例

registry:
//127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.5.6&file=/data/dubbo/cache/dubbo-provider&pid=21448&registry=zookeeper&timestamp=1524134852031

第二部是将服务发布到多种协议的url上,并且携带注册中心列表的参数,从这里我们可以看出dubbo是支持同时将一个服务发布成为多种协议的,这个需求也是很正常的,客户端也需要支持多协议,根据不同的场景选择合适的协议。

ServiceConfig.java 类的 doExportUrlsFor1Protocol(ProtocolConfigprotocolConfig,List<URL>registryURLs) 方法。

package com.alibaba.dubbo.config;


public
 
class
 
ServiceConfig
<T> 
extends
 
AbstractServiceConfig
 {


    
private
 
void
 doExportUrlsFor1Protocol(
ProtocolConfig
 protocolConfig, 
List
<URL> registryURLs) {

        // 省略很多

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


                
//配置不是remote的情况下做本地暴露 (配置为remote,则表示只暴露远程服务)

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

                    exportLocal(url);

                }

                
//如果配置不是local则暴露为远程服务.(配置为local,则表示只暴露本地服务)

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

                    
if
 (logger.isInfoEnabled()) {

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

                    }

                    
if
 (registryURLs != 
null
 && registryURLs.size() > 
0

                            && url.getParameter(
"register"
, 
true
)) {

                        
for
 (URL registryURL : registryURLs) {

                            url = url.addParameterIfAbsent(
"dynamic"
, registryURL.getParameter(
"dynamic"
));

                            URL monitorUrl = loadMonitor(registryURL);

                            
if
 (monitorUrl != 
null
) {

                                url = url.addParameterAndEncoded(
Constants
.MONITOR_KEY, monitorUrl.toFullString());

                            }

                            
if
 (logger.isInfoEnabled()) {

                                logger.info(
"Register dubbo service "
 + interfaceClass.getName() + 
" url "
 + url + 
" to registry "
 + registryURL);

                            }

                            
Invoker
<?> invoker = proxyFactory.getInvoker(ref, (
Class
) interfaceClass, registryURL.addParameterAndEncoded(
Constants
.EXPORT_KEY, url.toFullString()));


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

                            exporters.add(exporter);

                        }

                    } 
else
 {

                        
Invoker
<?> invoker = proxyFactory.getInvoker(ref, (
Class
) interfaceClass, url);


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

                        exporters.add(exporter);

                    }

                }

            }

            
this
.urls.add(url);

        }

}

拼装dubbo服务URL

该方法的逻辑是先根据服务配置、协议配置、发布服务的服务器信息、方法列表、dubbo版本等等信息组装成一个发布的URL对象。

主要根据之前map里的数据组装成URL。

例如

dubbo://10.4.81.95:20880/io.ymq.dubbo.api.DemoService?

anyhost=
true
&

application=dubbo-provider&

default
.connections=
5
&

default
.delay=-
1
&

default
.retries=
0
&

default
.timeout=
10000
&

default
.version=
1.0
&

delay=-
1
&

dubbo=
2.5
.
6
&

generic
=
false
&

interface
=io.ymq.dubbo.api.
DemoService
&

methods=sayHello&

pid=
21448
&

side=provider&

threadpool=
fixed
&

threads=
500
&

timestamp=
1524135271940

本地暴露和远程暴露

  1. 如果服务配置的scope是发布范围,配置为none不暴露服务,则会停止发布操作;

  2. 如果配置不是remote的情况下先做本地暴露,则调用本地暴露exportLocal方法;

  3. 如果配置不是local则暴露为远程服务,则注册服务registryProcotol;

//配置为none不暴露

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


    
//配置不是remote的情况下做本地暴露 (配置为remote,则表示只暴露远程服务)

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

        exportLocal(url);

    }

    
//如果配置不是local则暴露为远程服务.(配置为local,则表示只暴露本地服务)

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

        省略更多

    }

}

更多BAT面试题:面试题内容集合

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。

  3. 同时可以期待后续文章ing🚀

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值