1. Dubbo服务启动过程
启动一个Dubbo服务,通过启动日志,查看Dubbo服务启动的过程中都做了哪些事情:
通过启动日志可以看到,在Dubbo服务发布过程中做了以下的一系列动作:
1.暴露本地服务
2.暴露远程服务
3.启动Netty服务
4.连接Zookeeper并到Zookeeper进行注册
5.监听Zookeeper
通过Dubbo发布过程的详细图解看下服务提供者暴露服务的一个详细过程:
首先ServiceConfig拿到对外提供服务的实际类ref(如Dubbo源码中 dubbo-demo-xml-provider下的DemoServiceImpl类),然后通过ProxyFactory的getInvoker方法使用ref生成一个AbstractProxyInvoker的实例,到这一步就完成具体服务到Invoker的转换,接下来就是Invoker到Exporter的转换。
2.本地服务暴露的实现过程
从Dubbo的启动过程我们可以得知,Dubbo服务提供者,先进行本地暴露再进行远程暴露,在我们日常的使用场景中用的最多的是远程暴露。那么为什么还要进行本地暴露呢?
很多使用Dubbo框架的应用,可能存在在同一个JVM暴露了远程服务,同时同一个JVM内部又引用了自身服务的情况,Dubbo默认会把远程服务用injvm协议再暴露一份,这样消费方直接消费用一个JVM内部的服务,避免了跨网络进行远程通信。
我们先来看一下启动Dubbo服务提供者时最开始输出的日志:
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
可以根据日志看出启动的流程一下关键步骤:
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可以启动
那么我们根据这句日志输出做为一个切入点,来搜索下是哪里输出了 The service ready on spring started. 这句日志发现在ServiceBean类中出现
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这个类,到底是怎么执行的。
通过配置文件的加载过程我们可以知道,在服务启动时,Spring会解析dubbo服务的配置文件,并将配置文件中的参数转换成相应的Bean,当遇到 <dubbo:service> 时就会转换成ServiceBean。
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware,
ApplicationEventPublisherAware
以上为ServiceBean的定义,集成了ServiceConfig同时实现了InitializingBean、ApplicationListener等多个接口。在InitializingBean接口中有一个有个afterPropertiesSet方法,ServiceBean重写了该方法,在Bean的属性初始化时,Spring会默认调用该方法。
同时实现了ApplicationListener接口,并且重写了onApplicationEvent()方法。这两个接口都是Spring提供的接口,那么这两个接口会起到什么作用呢?
ServiceBean在创建完对象之后,会调用afterPropertiesSet()方法,该方法完成beanClass属性值的设置;在IOC容器启动完成之后,Spring会自动回调onApplicationEvent()方法,该方法完成服务的暴露,也就是在该方法中,我们看到了日志中的
The service ready on spring started.
3.服务暴露方法实现的解析
ServiceBean的 onApplicationEvent
ServiceBean--onApplicationEvent()方法源码如下:
public void onApplicationEvent(ContextRefreshedEvent
event) {
/**如果是暴露的、并且没有暴露的则调用export方法,暴露服务*/
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) { logger.info("The service ready on spring
started. service: " + getInterface());
}
export();
}
}
该方法为服务暴露的入口,那么暴露逻辑的真正实现则是在export方法中,接下来对export方法进行解析。
ServicebBean--export()方法:
@Override
public void export() {
super.export();
// Publish ServiceBeanExportedEvent
publishExportEvent();
}
通过export方法的代码可知,在这里调用父类的export()方法,之后调用调用publishExportEvent()方法。
下面看下 ServiceConfig的export()方法都实现了哪些逻辑。
public synchronized void export() {
//检测一些必要的属性和设置一些默认值
checkAndUpdateSubConfigs();
//判断是否已经导出
if (!shouldExport()) {
return;
}
//判断是否已经设置了延迟
if (shouldDelay()) {
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
//执行导出动作
doExport();
}
}
该方法除了检测基本的配置,以及在没有配置的情况下为配置设置默认值之外,最关键的是执行 doExport()方法,进一步查看doExzport方法都实现了什么逻辑。
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 表示服务提供者模型,此对象中存储了与服务提供者相关的信息。
// 比如服务的配置信息,服务实例等。每个被导出的服务对应一个 ProviderModel。
// ApplicationModel 持有所有的 ProviderModel。
ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
ApplicationModel.initProviderModel(pathKey, providerModel);
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
以上代码通过loadRegistries 加载注册中心链接,然后再遍历 ProtocolConfig 集合导出每个服务。并在暴露服务的过程中,将服务注册到注册中心。
loadRegistries()方法:
protected List<URL> loadRegistries(boolean provider) {
// check && override if necessary
List<URL> registryList = new ArrayList<URL>();
//判断注册配置是否为空,及配置文件中有没有<dubbo:registry>标签(配置注册中心的一些信息)
if (CollectionUtils.isNotEmpty(registries)) {
//由于可能配置多个注册中心地址,在这里遍历注册列表
for (RegistryConfig config : registries) {
String address = config.getAddress();
//判断配置的address是否为空,如果为空则配置为 0.0.0.0(由于在此步骤前已经对registry的地址做了非空校验,一般走不到这一步)
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 协议头设置为 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;
}
经过这一步骤之后,已经获取到了注册中心的地址,接下来就是调用doExportUrlsFor1Protocol()方法组装参数并且暴露Dubbo服务。
服务导出(暴露)过程概述
1. Spring容器启动时通过NameSpaceHandler来解析Dubbo服务的配置文件,并对配置文件中的参数进行初始化。
2.加载完配置文件之后,在ServiceBean的onApplicationEvent()方法受到Spring上下文刷新事件后会执行导出(export())方法。该方法为Dubbo服务导出的起点。
Export()方法逻辑分析:
1、进行参数配置的检查,检查该方法是否允许导出,另外检查该方法是否延迟导出。满足导出的条件之后,调用doExport()方法
备注:
* 检测 <dubbo:service> 标签的 interface 属性合法性,不合法则抛出异常
* 检测 ProviderConfig、ApplicationConfig 等核心配置类对象是否为空,若为空,则尝试从其他配置类对象中获取相应的实例。
*检测并处理泛化服务和普通服务类
*检测本地存根配置,并进行相应的处理
*对 ApplicationConfig、RegistryConfig 等配置类进行检测,为空则尝试创建,若无法创建则抛出异常
2、doExport()在参数合法的情况下,调用doExportUrls()方法,该方法对多协议,多注册中心进行了支持。
3、doExportUrls() 方法调用 doExportUrlsFor1Protocol()方法,该方法实现了由具体服务到Invoker的转换和Invoker到Exporter的转换。
备注:
invoker由ProxyFactory创建,Dubbo默认的ProxyFactory的实现类是JavassistProxyFactory。
我是割草的小猪头,不断学习,不断进步,后续陆续更新Dubbo系列的文章,如您有兴趣一起了解,欢迎关注,如文章中有不妥之处,欢迎指正!
Dubbo系列文章一--Dubbo重点掌握模块
Dubbo系列文章二--配置文件加载过程
Dubbo系列文章三--Dubbo源码结构及实现方