Dubbo 服务导出过程始于 Spring 容器发布刷新事件,Dubbo 在接收到事件后,会立即执行服务导出逻辑。整个逻辑大致可分为三个部分,第一部分是前置工作,主要用于检查参数,组装 URL。第二部分是导出服务,包含导出服务到本地 (JVM),和导出服务到远程两个过程。第三部分是向注册中心注册服务,用于服务发现。本篇文章将会对这三个部分代码进行详细的分析。
实现细节概览
解析服务
基于 dubbo.jar 内的 META-INF/spring.handlers
配置,Spring 在遇到 dubbo 名称空间时,会回调 DubboNamespaceHandler
。
所有 dubbo 的标签,都统一用 DubboBeanDefinitionParser
进行解析,基于一对一属性映射,将 XML 标签解析为 Bean 对象。
在 ServiceConfig.export()
或 ReferenceConfig.get()
初始化时,将 Bean 对象转换 URL 格式,所有 Bean 属性转成 URL 的参数。
然后将 URL 传给 协议扩展点,基于扩展点的 扩展点自适应机制,根据 URL 的协议头,进行不同协议的服务暴露或引用。
暴露服务
1. 只暴露服务端口:
在没有注册中心,直接暴露提供者的情况下 [1],ServiceConfig
解析出的 URL 的格式为: dubbo://service-host/com.foo.FooService?version=1.0.0
。
基于扩展点自适应机制,通过 URL 的 dubbo://
协议头识别,直接调用 DubboProtocol
的 export()
方法,打开服务端口。
2. 向注册中心暴露服务:
在有注册中心,需要注册提供者地址的情况下 [2],ServiceConfig
解析出的 URL 的格式为: registry://registry-host/org.apache.dubbo.registry.RegistryService?export=URL.encode("dubbo://service-host/com.foo.FooService?version=1.0.0")
,
基于扩展点自适应机制,通过 URL 的 registry://
协议头识别,就会调用 RegistryProtocol
的 export()
方法,将 export
参数中的提供者 URL,先注册到注册中心。
再重新传给 Protocol
扩展点进行暴露: dubbo://service-host/com.foo.FooService?version=1.0.0
,然后基于扩展点自适应机制,通过提供者 URL 的 dubbo://
协议头识别,就会调用 DubboProtocol
的 export()
方法,打开服务端口。
服务提供者暴露一个服务的详细过程
上图是服务提供者暴露服务的主过程:
首先 ServiceConfig
类拿到对外提供服务的实际类 ref(如:HelloWorldImpl),然后通过 ProxyFactory
类的 getInvoker
方法使用 ref 生成一个 AbstractProxyInvoker
实例,到这一步就完成具体服务到 Invoker
的转化。接下来就是 Invoker
转换到 Exporter
的过程。
Dubbo 处理服务暴露的关键就在 Invoker
转换到 Exporter
的过程,上图中的红色部分。下面我们以 Dubbo 和 RMI 这两种典型协议的实现来进行说明:
Dubbo 的实现
Dubbo 协议的 Invoker
转为 Exporter
发生在 DubboProtocol
类的 export
方法,它主要是打开 socket 侦听服务,并接收客户端发来的各种请求,通讯细节由 Dubbo 自己实现。
RMI 的实现
RMI 协议的 Invoker
转为 Exporter
发生在 RmiProtocol
类的 export
方法,它通过 Spring 或 Dubbo 或 JDK 来实现 RMI 服务,通讯细节这一块由 JDK 底层来实现,这就省了不少工作量。
满眼都是 Invoker
由于 Invoker
是 Dubbo 领域模型中非常重要的一个概念,很多设计思路都是向它靠拢。这就使得 Invoker
渗透在整个实现代码里,对于刚开始接触 Dubbo 的人,确实容易给搞混了。 下面我们用一个精简的图来说明最重要的两种 Invoker
:服务提供 Invoker
和服务消费 Invoker
:
为了更好的解释上面这张图,我们结合服务消费和提供者的代码示例来进行说明:
服务消费者代码:
public class DemoClientAction {
private DemoService demoService;
public void setDemoService(DemoService demoService) {
this.demoService = demoService;
}
public void start() {
String hello = demoService.sayHello("world" + i);
}
}
上面代码中的 DemoService
就是上图中服务消费端的 proxy,用户代码通过这个 proxy 调用其对应的 Invoker
[5],而该 Invoker
实现了真正的远程服务调用。
服务提供者代码:
public class DemoServiceImpl implements DemoService {
public String sayHello(String name) throws RemoteException {
return "Hello " + name;
}
}
上面这个类会被封装成为一个 AbstractProxyInvoker
实例,并新生成一个 Exporter
实例。这样当网络通讯层收到一个请求后,会找到对应的 Exporter
实例,并调用它所对应的 AbstractProxyInvoker
实例,从而真正调用了服务提供者的代码。Dubbo 里还有一些其他的 Invoker
类,但上面两种是最重要的。
导出服务入口
服务导出的入口方法是 ServiceBean 的 onApplicationEvent。onApplicationEvent 是一个事件响应方法,该方法会在收到 Spring 上下文刷新事件后执行服务导出操作。方法代码如下:
ServiceBean 是 Dubbo 与 Spring 框架进行整合的关键,可以看做是两个框架之间的桥梁。具有同样作用的类还有 ReferenceBean。
检查参数略
加载注册中心URL列表
Dubbo 允许我们使用不同的协议导出服务,也允许我们向多个注册中心注册服务。Dubbo 在 doExportUrls 方法中对多协议,多注册中心进行了支持。相关代码如下:
private void doExportUrls() {
// 加载注册中心链接
List<URL> registryURLs = loadRegistries(true);
// 遍历 protocols,并在每个协议下导出服务
for (ProtocolConfig protocolConfig : protocols) {
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
组装provider URL
导出服务到本地
exportLocal()方法内部会把url协议替换为injvm协议,然后通过自适应扩展机制调用InjvmProtocol的export方法在本地暴露服务。
InjvmProtocol 的 export 方法仅创建了一个 InjvmExporter,无其他逻辑
导出服务到远程
生成Invoker实例
注意第518行 registryURL上加了一个export参数,参数的值就是providerURL,以便可以从registryURL中获取providerURL
导出服务
例子中是调用DubboProtocol的export方法暴露服务
跟踪方法最核心的一步就是创建服务端,开启监听端口,默认是使用nettyserver
注册服务
registryFactory是一个SPI接口的代理类
根据registryURL的协议zookeeper,实际执行ZookeeperRegistryFactory的方法,ZookeeperTransporter也是一个SPI接口。默认是使用curator的客户端。
通过curator客户端创建zookeeper目录