看看Dubbo源码

一、解析服务

Spring遇到 <dubbo:......> 时,根据 spring.handlers 回调 DubboNameSpaceHandler,使用 DubboBeanDefinitionParser 来解析。此处涉及一个 parser 方法,用于将 xml 配置信息定义为 spring 的 BeanDefinition,其包含类名、scope、属性、构造函数参数列表、依赖 Bean、是否是单例类、是否是懒加载等信息,之后的操作直接对 BeanDefinition 进行,可根据其中参数利用反射进行对象创建。

解析服务

基于 dubbo.jar 内的 META-INF/spring.handlers 配置,Spring 在遇到 dubbo 名称空间时,会回调 DubboNamespaceHandler

所有 dubbo 的标签,都统一用 DubboBeanDefinitionParser 进行解析,基于一对一属性映射,将 XML 标签解析为 Bean 对象。

ServiceConfig.export()ReferenceConfig.get() 初始化时,将 Bean 对象转换 URL 格式,所有 Bean 属性转成 URL 的参数。

然后将 URL 传给协议扩展点,基于扩展点的扩展点自适应机制,根据 URL 的协议头,进行不同协议的服务暴露或引用。


二、暴露服务

暴露服务对应的 ServiceConfig.export 和引用服务对应的 ReferenceConfig.get 初始化时,Bean 转化为 URL,Bean 的属性转化为 URL 参数。然后由协议拓展点处理 URL,根据 URL 的协议头进行不同协议的操作。

暴露服务时序图

此为暴露服务时序图

暴露服务

1. 只暴露服务端口:

在没有注册中心,直接暴露提供者的情况下,ServiceConfig 解析出的 URL 的格式为: dubbo://service-host/com.foo.FooService?version=1.0.0

基于扩展点自适应机制,通过 URL 的 dubbo:// 协议头识别,直接调用 DubboProtocolexport() 方法,打开服务端口。

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:// 协议头识别,就会调用 RegistryProtocolexport() 方法,将 export 参数中的提供者 URL,先注册到注册中心。

再重新传给 Protocol 扩展点进行暴露: dubbo://service-host/com.foo.FooService?version=1.0.0,然后基于扩展点自适应机制,通过提供者 URL 的 dubbo:// 协议头识别,就会调用 DubboProtocolexport() 方法,打开服务端口。

<dubbo:service> 先被解析成 ServiceBean,由 ServiceBean 实现接口 ApplicationContextAware,调用 setApplicationContext 方法,为后续初始化做基础。

ServiceBean实现接口InitializingBean,调用 afterPropertiesSet方法,依次解析<dubbo:provider><dubbo:application><dubbo:module><dubbo:registry><dubbo:monitor><dubbo:protocol>(新版本中可能不止这些),同时进行export

最后ServiceBean实现接口ApplicationListener<ContextRefreshedEvent>,调用onApplicationEvent方法,进行export。(二三两步中都有export操作,目前认识为防止未执行)。

ServiceBean拓展了ServiceConfig,调用export方法时由ServiceConfig完成服务暴露发功能实现。

public synchronized void export() {
 checkAndUpdateSubConfigs();//老版本中doExport()里对各种的检查
 if (!shouldExport()) {
 return;
 }
 if (shouldDelay()) {
 DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
 } else {
 doExport();
 }
}
//上半部分为笔者使用的,也是写的时候最新的源码里的代码
//下半部分为网络中找到的,可能单独拿出来更清楚的以前的源码
public synchronized void export() {
 if (provider != null) {
 if (export == null) {
 export = provider.getExport();
 }
 if (delay == null) {
 delay = provider.getDelay();
 }
 }
 if (export != null && ! export.booleanValue()) {
 return;
 }
 if (delay != null && delay > 0) {
 Thread thread = new Thread(new Runnable() {
 public void run() {
 try {
 Thread.sleep(delay);
 } catch (Throwable e) {
 }
 doExport();
 }
 });
 thread.setDaemon(true);
 thread.setName("DelayExportServiceThread");
 thread.start();
 } else {
 doExport();
 }
}

Dubbo 当前的源码为上半部分,将一些判断内容放到其他方法里实现。export的作用为判断服务是否已经打开以及是否需要延迟打开,老版本中doExport方法中会进行参数检查和设置,包括泛化调用,本地实现,本地存根,本地伪装和配置,故实际逻辑还在方法doExport里。(新版本中,这些被移到了checkAndUpdateSubConfigs方法中(export中))

//此为新版本里的doExport()
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();
}

检查完参数后,开始暴露服务。doExportUrls表示,Dubbo支持多协议和多注册中心。

private void doExportUrls() {
 List<URL> registryURLs = loadRegistries(true);
 for (ProtocolConfig protocolConfig : protocols) {
 String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
 ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
 ApplicationModel.initProviderModel(pathKey, providerModel);//生成的serviceBean在这里
 doExportUrlsFor1Protocol(protocolConfig, registryURLs);
 }
}

这中途涉及了另一个方法loadRegistries

protected List<URL> loadRegistries(boolean provider) {
 // check && override if necessary
 List<URL> registryList = new ArrayList<URL>();
 if (CollectionUtils.isNotEmpty(registries)) {
 for (RegistryConfig config : registries) {
 String address = config.getAddress();
 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 = 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;
}

该方法将registries变量里每个地址拼上applicationregistryConfig里的参数,拼成一个registryUrl带参数的标准格式。然后返回这些url的列表。

然后针对每个协议与注册中心,开始组装URL(主要使用doExportUrlsFor1Protocol方法),并且会转成invoker以及创建expoter,给一个service标签的serviceBean生成了一个代理好的Invoker,该invoker放在exporter对象下,exporter在serviceBean的一个exporters变量下,准备被调用。

大体流程图

首先 ServiceConfig 类拿到对外提供服务的实际类 ref (如:HelloWorldImpl),然后通过 ProxyFactory 类的 getInvoker 方法使用 ref 生成一个 AbstractProxyInvoker 实例,到这一步就完成具体服务到 Invoker 的转化。接下来就是 Invoker 转换到 Exporter 的过程。

Dubbo 处理服务暴露的关键就在 Invoker 转换到 Exporter 的过程。

根据 scope 来决定进行本地暴露还是远程暴露还是不暴露。

  • 本地暴露(提供者同时也是消费者时,没必要通过网络通信去获取自己提供的服务):
private void exportLocal(URL url) {
 URL local = URLBuilder.from(url)
 .setProtocol(LOCAL_PROTOCOL)
 .setHost(LOCALHOST_VALUE)
 .setPort(0)
 .build();
 Exporter<?> exporter = protocol.export(
 PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local));
 exporters.add(exporter);
 logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}

通过代理创建 Invoker,本地暴露使用 injvm 协议,injvm 是伪协议,不开启端口,不能被远程调用,只在 JVM 内直接关联,但执行 Dubbo 的 Filter 链。

  • 远程暴露:
// export to remote if the config is not local (export to local only when config is local)
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
 if (!isOnlyInJvm() && logger.isInfoEnabled()) {
 logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
 }
 if (CollectionUtils.isNotEmpty(registryURLs)) {
 for (URL registryURL : registryURLs) {
 //if protocol is only injvm ,not register
 if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
 continue;
 }
 url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
 URL monitorUrl = loadMonitor(registryURL);
 if (monitorUrl != null) {
 url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString());
 }
 if (logger.isInfoEnabled()) {
 logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
 }
 // For providers, this is used to enable custom proxy to generate invoker
 String proxy = url.getParameter(PROXY_KEY);
 if (StringUtils.isNotEmpty(proxy)) {
 registryURL = registryURL.addParameter(PROXY_KEY, proxy);
 }
​
 Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
 DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
​
 Exporter<?> exporter = protocol.export(wrapperInvoker);
 exporters.add(exporter);
 }
 } else {
 Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
 DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
​
 Exporter<?> exporter = protocol.export(wrapperInvoker);
 exporters.add(exporter);
 }
 /**
 * @since 2.7.0
 * ServiceData Store
 */
 MetadataReportService metadataReportService = null;
 if ((metadataReportService = getMetadataReportService()) != null) {
 metadataReportService.publishProvider(url);
 }
}

此时服务的暴露有两种:使用注册中心和不使用注册中心。不使用的情况下,直接暴露对应协议的服务,引用服务只能通过直连的方式;使用的情况下,引用服务可以通过注册中心动态获取提供者列表,也可以通过直连方式引用。

组装完的URL基于扩展点自适应机制,由协议头调用对应协议的export来暴露服务。(即开头为"dubbo://"则调用DubboProtocolexport方法;开头为"registry://"则调用RegistryProtocolexport方法...)

1. 只暴露服务端口:

在没有注册中心,直接暴露提供者的情况下,ServiceConfig 解析出的 URL 的格式为: dubbo://service-host/com.foo.FooService?version=1.0.0

基于扩展点自适应机制,通过 URL 的 dubbo:// 协议头识别,直接调用 DubboProtocolexport() 方法,打开服务端口。

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:// 协议头识别,就会调用RegistryProtocolexport() 方法,将 export 参数中的提供者 URL,先注册到注册中心。

再重新传给 Protocol 扩展点进行暴露: dubbo://service-host/com.foo.FooService?version=1.0.0,然后基于扩展点自适应机制,通过提供者 URL 的 dubbo:// 协议头识别,就会调用 DubboProtocolexport() 方法,打开服务端口。


此处笔者列举一下Dubbo文档中的源码解读中服务暴露部分与目前源码中有不同的地方(笔者一开始用的最新的源码,目前官网源码解读文档为2.6.4版本匹配的,大家不要步我后尘)以及感觉有点疑惑的地方。

  • 定位:2. 源码解读

    文档中提示注意isDelay方法,该方法返回true意为“无需延迟导出”,返回false意为”需要延迟导出“,和一般理解有出入,会让人有困惑。源码中,在ServiceConfig下使用了shouldDelay方法,解决了该问题。

  • 定位:2.1.1 检查配置

    文档中获取export和delay来分析是否导出以及是否如何延迟导出,源码中采用调用 shouldExportshouldDelay,使得 export 方法更简洁。

    文档中的doExport方法过于冗长,其原因是进行了配置检查,包括:检测 <dubbo:service> 标签的interface属性合法性,不合法抛出异常;检测ProviderConfigApplicationConfig 等核心配置类对象是否为空,若为空,则尝试从其他配置类对象中获取相应的实例;检测并处理泛化服务和普通服务类;检测本地存根配置,并进行相应的处理;对 ApplicationConfigRegistryConfig 等配置类进行检测,为空则尝试创建,若无法创建则抛出异常。而在源码中,我们发现doExport方法简短,其检查配置部分在export时便调用checkAndUpdateSubConfigs完成。

    • 定位:2.2 导出Dubbo服务

      这里针对scope参数的处理为:scope == none → 不导出服务;scope != remote → 导出到本地;scope != local → 导出到远程。是不是代表scope不为none且同时不为remote和local就同时导出到本地和远程?但是源码里这里注释为export to remote only when config is remote以及export to local only when config is local

  • 定位:2.2.2 导出服务到本地

    exportLocal中使用了URLBuilder代替原有的构造修改URL方法。

  • 定位:2.2.3 导出服务到远程

    RegistryProtocol下的export也有些许改变。将providerUrl也加入doLocalExport的参数里,同时将overrideSubscUrlregisteredProviderUrl塞进exporter里,使得DestroyableExporter的参数减少。

    doLocalExport中简化格式,利用computeIfAbsent取代了原先的双重检查锁。

    文档中createServer方法内第二行url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY,疑似缺少一部分,根据源码推测应为url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString())

  • 定位:2.2.4.1 创建注册中心

    文档中CuratorZookeeperTransporter类中的connect方法在源码中不存在。接口ZookeeperTransporterconnect方法由AbstractZookeeperTransporter实现。CuratorZookeeperTransporter类继承AbstractZookeeperTransporter类并实现createZookeeperClient方法再由AbstractZookeeperTransporter调用。


服务导出到远程的过程:

RegistryProtocol.export 开始

  1. 调用 RegistryProtocol.doLocalExport 导出服务

    1. 双重锁检查 (源码中有更改)

    2. protocol.export 根据协议在运行时会自动加载,例如DubboProtocol.export

      1. DubboExporter的创建

      2. DubboProtocol.openServerDubboProtocol.createServer

        1. 检测是否存在 server 参数所代表的 Transporter 拓展,不存在则抛出异常 (代码直白,略)

        2. 创建服务器实例 (Exchangers.bind

          1. Exchangers.bindHeaderExchanger.bind,包含三层逻辑

            1. new HeaderExchangeHandler(handler)

            2. new DecodeHandler(new HeaderExchangeHandler(handler))

            3. Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))

              1. Transporters.getTransporter是动态创建的,类名为TransporterAdaptive,即自适应拓展类,根据传入的URL参数决定加载什么类型的Transporter,默认为NettyTransporter。 (NettyTransporter.bindNettyServerAbstractServerNettyServer.doOpen
        3. 检测是否支持 client 参数所表示的 Transporter 拓展,不存在也是抛出异常 (代码直白,略)

  2. 向注册中心注册服务 (RegistryProtocol.register

    1. 获取注册中心实例 (AbstractRegistryFactory.getRegistry

      1. 访问缓存,未命中则调用createRegistry创建Registry

        1. ZookeeperRegistryFactory.createRegistryZookeeperRegistry

          1. 获取组名,默认为dubbo

          2. 创建Zookeeper客户端,默认为CuratorZookeeperTransporterZookeeperTransporter.connect,此处文档和最新源码有出入,CuratorZookeeperTransporter.connect不存在,但CuratorZookeeperTransporter.createZookeeperClientCuratorZookeeperClient

            1. CuratorZookeeperClient构造方法主要用于创建和启动CuratorFramework实例,大部分为Curator框架的代码,可参考Curator官方文档
          3. 添加状态监听器

      2. 写入缓存

    2. 向注册中心注册服务 (将服务配置数据写入到Zookeeper的某个路径的节点下)

      1. FailbackRegistry.registryZookeeperRegistry.doRegistry

        1. toUrlPath方法生成节点路径

        2. AbstractZookeeperClient.create

          1. 通过递归创建当前节点的上一个路径

          2. 根据ephemeral的值决定创建临时节点还是持久节点 (createEphemeralcreatePersistent

            1. 只需分析其中一个,以createEphemeral为例

            2. CuratorZookeeperClient.createEphemeral,通过Curator框架创建节点

  3. 向注册中心进行订阅 override 数据 (非本章重点)

  4. 创建并返回 DestroyableExporter (无难度,略)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345