avatar

Dubbo源码分析之服务注册发布(四)

上篇我们粗略的过了一遍dubbo在ServiceBeanafterPropertiesSet()方法中做了那些事。那么我们这篇就开始进入服务导出的代码了。让我们一起来分析,看看它是如何实现的。

前情回顾

其实上一篇文章写的不怎么好,跟心情有一定的原因,还有一些原因是因为该部分的代码都是差不多的,只要看懂了一块,那么剩下的也基本上都差不多了。按照惯例,还是得先回忆下上一篇中到了说了写啥。有如下内容:

  • Dubbo是怎样从Spring容器中获取一个Bean的。
  • 存储对应的配置到ServiceBean中。

上面两个就是上一篇的全部内容和涉及到的东西,注意哦,每碰到一个<dubbo:service>标签,就会有一个ServiceBean对象。

服务导出流程

首先声明,这里的服务导出流程,并不是Dubbo的,而是我们猜想的。先跳出Dubbo,假设要我们做服务导出,我们应该怎么做?然后再对比Dubbo,这样看源码就不会太晕车。

回顾

还是回顾一下,在刚开始Dubbo系列的时候,Dubbo的第一篇就是如何简单的实现一个RPC框架(严格意义上,就是个demo),地址是这个Dubbo系列之RPC浅谈&简单实现一个RPC,当时我们说的流程是这样的:

新需求

假设如果要我们这个demo来集成Spring,并且需要引入注册中心,我们的流程会是怎样?如果基于上面的这个流程来的话,只考虑服务提供方的话,我们需要在第一步前插入如下流程:

  • 解析配置文件,映射成配置实体,然后保存到Spring中。
  • 在Spring配置文件解析完成后,在InitializingBean.afterPropertiesSet()中做文章

以上两点,就是前面两篇文章所说的内容。这样我们就继承了Spring,可以让Spring来帮我们管理一些bean。

然后接着看,如果要引入注册中心,需要动态的维护我们暴露出去的服务地址,这时候应该怎样做?基于上图,我们需要在哪里插入处理流程?

上面也说了,引入注册中心是为了动态的维护我们暴露的地址,所谓暴露地址就是需要告诉服务消费方应该连接哪个地址,那个端口来调服务提供方。

那这个暴露的地址是怎么来的?从哪来的?我们可以看上图的第一步,有个启动ServerSocket的操作,ServerSocket我们都知道是Java中用来网络通信的,这里ServerScoket启动后就会阻塞在那里,等待消费端的连接。这时候我们服务的地址以及端口我们都已经知道了,所以我们可以在第一步操作完后,把我们的地址+端口注册到注册中心上。

那么最后的流程大致如下(只针对服务提供方):

  • 解析配置文件,加入到Spring容器
  • 启动一个ServerSocket,监听消费者的连接
  • ServerScoket的地址、端口号保存到注册中心上

上面就是我们自己实现demo的流程,那Dubbo中是怎样的呢?其实Dubbo中抛去那些复杂的代码,以及对扩展的处理,总体的流程还是和上面的差不多,我们一起慢慢的看下去吧。

继续从ServiceBean看起

上次我们说到了ServiceBean.afterPropertiesSet()中的最后部分,也就是export()方法。代码如下:

1
2
3
if (!supportedApplicationListener) {
export();
}

后面这几篇文章,都是围绕这个export()方法去阐述。我们今天只是说一部分,慢慢来~

ServiceBean.export()

我们跟进去,export()代码如下:

1
2
3
4
5
6
@Override
public void export() {
super.export();
// Publish ServiceBeanExportedEvent
publishExportEvent();
}

方法里面就两行代码,先是调用super.export()方法,然后在发布一个事件,我们先看下发布的是什么事件。为什么要先看事件呢?因为重头戏在super.export()中,我们先看一些简单的。

ServiceBean.publishExportEvent()

1
2
3
4
private void publishExportEvent() {
ServiceBeanExportedEvent exportEvent = new ServiceBeanExportedEvent(this);
applicationEventPublisher.publishEvent(exportEvent);
}

可以很清楚的看到,他是通过Spring发布一个ServiceBeanExportEvent事件。那么事件是从这发送出去了,那么是谁在监听呢?这里我们暂时不管。也就是说,这个方法就看到这里了。我们来重点关注super.exoprt()方法。

super.export()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//问题:这里为什么要加锁?
public synchronized void export() {
//检查配置是否就绪
checkAndUpdateSubConfigs();
//判断是否需要导出
if (!shouldExport()) {
return;
}
//是否需要延迟导出
if (shouldDelay()) {
delayExportExecutor.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
//导出
doExport();
}
}

这里我们先看一个问题,为什么方法上要加上synchronized关键字?

  • 这里加锁,是为了防止重复导出,因为export()方法并不只有ServiceBean调用,还会有其他地方,而其他地方肯定是另外一个线程,这里加锁的目的就是为了防止并发情况下,多次导出。
  • 既然说到这,那么他肯定有个字段标识是否已经导出,然后也会有个地方去判断这个字段的值,从而中断方法向下执行,该字段在后面会看到的。

问题回答完了,我们再一步一步的开始分析代码。checkAndUpdateSubConfigs()方法,我们忽略掉不说。直接看他是怎么判断这个<dubbo:service>标签配置的bean是否需要导出的。

ServiceConfig.shouldExport()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private boolean shouldExport() {
//通过下面的分析。在第一次来调用的时候,这里的export一定为null。
Boolean export = getExport();
// default value is true(这段注释是官方的。所以默认这里是返回true)
return export == null ? true : export;
}

@Override
public Boolean getExport() {
//export是ServiceConfig的一个成员属性,是一个Boolean对象,默认为null。
//除非我们在<dubbo:service>中配置export属性。
//而因为provider我们不常用。所以也是null,综合来说,这里返回的是null
return (export == null && provider != null) ? provider.getExport() : export;
}

经过上面的分析,可以得出在第一次调用shouldExport()方法是,返回的是true。所以!shouldExport()false

然后我们继续看一下shouldDelay()方法。

ServiceConfig.shouldDelay()

1
2
3
4
5
6
7
8
9
10
11
12
private boolean shouldDelay() {
//获取延迟的毫秒数,为啥说是毫秒数呢?下面见分晓
Integer delay = getDelay();
//如果delay不为空并且>0时,返回true。表示需要延迟导出
return delay != null && delay > 0;
}

@Override
public Integer getDelay() {
//获取延迟时间的配置。单位是毫秒
return (delay == null && provider != null) ? provider.getDelay() : delay;
}

说白了,只有在我们配置了<dubbo:service>delay属性,shouldDelay()才会返回true

那我们看看如果配了delay属性,也就是shouldDelay()返回true时是怎么玩的吧。同时也看看如果不配延时导出,是怎么玩的。

延迟 OR 不延迟

1
2
3
4
5
6
7
8
9
10
 //是否需要延迟导出
if (shouldDelay()) {
//使用一个线程池,调用schedule方法,启动一个任务。等时间到了就会执行doExport()方法
//这里的时间为毫秒。这也就是为什么上面说,延迟的时间配置的单位是秒
delayExportExecutor.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
//如果不需要延迟导出
//直接调用导出
doExport();
}

那么现在所有的关键都指向doExport()方法,那咱们一起来look look。

ServiceConfig.doExport()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//问题:这里为什么也要加锁?前面在export()方法那里不是已经加了么?
protected synchronized void doExport() {
//这里校验一下服务是否已经取消导出,只有调用了unexported()方法,值才会为true。
if (unexported) {
throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
}
//如果已经导出了,就return。
//这里就体现出来了在解答export()方法加锁时提出的标识服务是否导出以及判断的地方。
if (exported) {
return;
}
//标识服务已导出。
exported = true;
//path:服务路径,缺省为接口名。这里就体现出来了
if (StringUtils.isEmpty(path)) {
path = interfaceName;
}
//导出
doExportUrls();
}

经过上面一系列操作后,又调用了doExportUrls()方法,一环套一环的来。

上面还留了个问题,这里为什么要加锁?明明调用该方法出已经加过锁了呀,这里再加一次,不就浪费了吗?原因如下:

  • 还是调用的问题,因为我们可以在export()方法的最后看到,如果配置了延迟导出,会在一定时间后再调用该方法,如果没得配置延迟导出,那就会马山调用该方法。
  • 而延迟导出,到了时间后是会新开一个线程去执行,而单位又是毫秒。假设如果doExport()方法没有加锁,我配置的是5000ms,也就是5s。然后我发现这个配置我配错了,我不需要延迟启动,所以我动态修改了delay的值,然后刷新Spring容器。而Dubbo会在ServiceBean中监听ContextRefreshedEvent事件(这个会在后面说到),然后就会再次调用export()方法。这时候因为delay的值为空,所以不会走延迟导出,就会立马调用doExport()方法。而好巧不巧,这时候延迟启动的时间到了,我也调用了doExport()方法,试想一下,如果不加锁,这里就有可能会导出两次同一个服务。

可以看到,从开始到这里为止,逻辑都不是很复杂,可以谓之超级简单;也可以看到,这部分的代码无非就是判断下服务是否导出,是否需要延迟导出,既然判断都差不多了,那么接下来应该该入正题了,是的,正题部分我们下一篇再分析,该篇就到这里了。谢谢大佬们观看。

总结

  • 没啥好说的,逻辑很简单,思路也很清晰。

  • 这篇就不画图了哈,我们下一篇开始。

  • 才疏学浅,难免会有错误之处,还望各位大佬不吝指教。谢谢~


评论