面试必备:Dubbo中暴露服务的过程解析
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的步骤简介
-
首先会检查各种配置信息,填充各种属性,总之就是保证我在开始暴露服务之前,所有的东西都准备好了,并且是正确的。
-
加载所有的注册中心,因为我们暴露服务需要注册到注册中心中去。
-
根据配置的所有协议和注册中心url分别进行导出。
-
进行导出的时候,又是一波属性的获取设置检查等操作。
-
如果配置的不是remote,则做本地导出。
-
如果配置的不是local,则暴露为远程服务。
-
不管是本地还是远程服务暴露,首先都会获取Invoker。
-
获取完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®istry=zookeeper×tamp=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
本地暴露和远程暴露
-
如果服务配置的scope是发布范围,配置为none不暴露服务,则会停止发布操作;
-
如果配置不是remote的情况下先做本地暴露,则调用本地暴露exportLocal方法;
-
如果配置不是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)) {
省略更多
}
}
最新2020整理收集的一些面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友微信扫描下方二维码免费获取:暗号:【CSDN】
看完三件事❤️
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
-
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
-
关注公众号 『 java烂猪皮 』,不定期分享原创知识。
-
同时可以期待后续文章ing????
上一篇: Redis面试必备(一)
下一篇: 前端跳槽面试必备