从源码全面解析 dubbo 服务暴露的来龙去脉

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云解析 DNS,旗舰版 1个月
简介: 从源码全面解析 dubbo 服务暴露的来龙去脉

一、引言

对于 Java 开发者而言,关于 dubbo ,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本期 dubbo 源码解析系列文章,将带你领略 dubbo 源码的奥秘

本期源码文章吸收了之前 SpringKakfaJUC源码文章的教训,将不再一行一行的带大家分析源码,我们将一些不重要的部分当做黑盒处理,以便我们更快、更有效的阅读源码。

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马!

废话不多说,发车!

二、环境配置

本篇文章适合对 dubbo 有兴趣 & 日常工作中有使用的人

环境配置:

  • dubbo版本:3.1.8
  • maven版本:3.5.4
  • JDK版本:JDK8
  • Zookeeper版本:3.4.9

因为服务数据是注册在 Zookeeper 上的,所以需要一个 Zookeeper 的可视化界面:ZooInspector

当然,就算你上述环境配置不全,也不影响你本篇文章的阅读体验。

三、服务暴露

上一篇文章《从源码全面解析dubbo服务注册的来龙去脉》 我们分析了我们的 Dubbo 是如何解析 @EnableDubboConfig@DubboComponentScan 这两个注解的

我们留了一个 服务暴露 的过程没有讲。因为 服务暴露Dubbo 中属于比较重要的知识点,所以单独一篇来进行讲解

我们上一篇文章讲到在 DubboDeployApplicationListener 里面实现了 服务暴露 的过程

我们主要讲一下 DubboDeployApplicationListener 的实现:

public class DubboDeployApplicationListener implements ApplicationListener<ApplicationContextEvent>, ApplicationContextAware, Ordered {
    @Override
    public void onApplicationEvent(ApplicationContextEvent event) {
        if (nullSafeEquals(applicationContext, event.getSource())) {
            // 判断当前的事件
            if (event instanceof ContextRefreshedEvent) {
                // 刷新事件
                onContextRefreshedEvent((ContextRefreshedEvent) event);
            } else if (event instanceof ContextClosedEvent) {
                // 关闭事件
                onContextClosedEvent((ContextClosedEvent) event);
            }
        }
    }
}
private void onContextRefreshedEvent(ContextRefreshedEvent event) {
    ModuleDeployer deployer = moduleModel.getDeployer();
    Future future = deployer.start();
}
public Future start() throws IllegalStateException {
    // 初始化模块
    applicationDeployer.initialize();
    return startSync();
}
private synchronized Future startSync() throws IllegalStateException {
    // 服务暴露
    exportServices();
}

1、判断注册方式

由于我们这个是 Dubbo3 的源码,Dubbo3 新增了一种注册方式:应用级注册,所以在这里会判断当前的注册方式是哪一种

  • dubbo2:接口级注册
  • dubbo3:应用级注册

我们看一下 exportServices 是如何对其进行处理的

直接来到 org.apache.dubbo.config.ServiceConfigdoExport 方法

protected synchronized void doExport() {
    // 将一个服务暴露成多个URL
    doExportUrls();
    exported();
}
1.1 获取注册的URL
private void doExportUrls() {
    // 获取当前
    List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);
    // 省略部分代码
}
public static List<URL> loadRegistries(AbstractInterfaceConfig interfaceConfig, boolean provider) {
    // 获取配置里面的地址
    String address = config.getAddress();
    if (StringUtils.isEmpty(address)) {
        address = ANYHOST_VALUE;
    }
    // 若配置多个URL地址,在这里切割
    List<URL> urls = UrlUtils.parseURLs(address, map);
    // 填充URL多个地址
    for (URL url : urls) {
        // 填充URL地址
        url = URLBuilder.from(url)
            .addParameter(REGISTRY_KEY, url.getProtocol())
          .setProtocol(extractRegistryType(url))
          .setScopeModel(interfaceConfig.getScopeModel())
          .build();
        // 判断当前是不是服务端,将URL添加至registryList
        if (provider || url.getParameter(SUBSCRIBE_KEY, true)) {
            registryList.add(url);
        }
        // 这里的URL示例:
        // registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873
    }
    // 根据注册类型拼接URL配置
    return genCompatibleRegistries(interfaceConfig.getScopeModel(), registryList, provider);
}
1.2 应用注册
  • 当前 registerMode(注册类型)instance 或者 all 时,走应用注册
if ((DEFAULT_REGISTER_MODE_INSTANCE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode))
    && registryNotExists(registryURL, registryList, SERVICE_REGISTRY_PROTOCOL)) {
    URL serviceDiscoveryRegistryURL = URLBuilder.from(registryURL)
        .setProtocol(SERVICE_REGISTRY_PROTOCOL)
        .removeParameter(REGISTRY_TYPE_KEY)
        .build();
    result.add(serviceDiscoveryRegistryURL);
}
  • URL:
service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873
1.3 接口注册
  • 当前 registerMode(注册类型)interface 或者 all 时,走接口注册
if (DEFAULT_REGISTER_MODE_INTERFACE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode)) {
    result.add(registryURL);
}
  • URL
registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873

2、服务导出

// 遍历当前的传输协议(dubbo、rest、tri)
for (ProtocolConfig protocolConfig : protocols) {
    // 组装当前的接口URL:com.common.service.IUserService:1.0.0.dev
    String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
    // 正式导出服务
    doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
    // 获取数据配置
  Map<String, String> map = buildAttributes(protocolConfig);
    serviceMetadata.getAttachments().putAll(map);
    // 组装URL
    URL url = buildUrl(protocolConfig, map);
    // 服务暴露
    exportUrl(url, registryURLs);
}
  • 组装的URL:
dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&bind.ip=192.168.0.103&bind.port=20883&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=1168&qos.enable=true&register-mode=instance&release=3.1.8&revision=1.0.0.dev&side=provider&timeout=100&timestamp=1685377125239&version=1.0.0.dev
2.1 服务暴露
private void exportUrl(URL url, List<URL> registryURLs) {
    exportLocal(url);
}
private void exportLocal(URL url) {
    // 组装配置
    URL local = URLBuilder.from(url)
        .setProtocol(LOCAL_PROTOCOL)
        .setHost(LOCALHOST_VALUE)
        .setPort(0)
        .build();
    local = local.setScopeModel(getScopeModel())
        .setServiceModel(providerModel);
    // 服务暴露
    doExportUrl(local, false);
}
private void doExportUrl(URL url, boolean withMetaData) {
    // 生成动态代理
    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
    if (withMetaData) {
        invoker = new DelegateProviderMetaDataInvoker(invoker, this);
    }
    // 服务导出
    Exporter<?> exporter = protocolSPI.export(invoker);
    exporters.add(exporter);
}
2.2 动态代理生成

这个里面的 getIUnvoker 很经典的动态代理模式,当我们客户端调用时,会调用 doInvoke 里面的方法

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName,Class<?>[] parameterTypes,Object[] arguments) {
            // proxy:UserServicelmpl
            // 直接调用实现类
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
} 
2.3 服务导出
Exporter<?> exporter = protocolSPI.export(invoker);
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    // 暴露服务
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
    // 获取注册的Registry
    final Registry registry = getRegistry(registryUrl);
    final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
    // 注册方式的选择
    boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        // 这里有两种注册情况:
        // 1、接口注册:直接将注册数据注册到Zookeeper即可
        // 2、应用注册:将注册数据转换成元数据等后面发布元数据
        register(registry, registeredProviderUrl);
    }
}
2.3.1 服务启动

我们都知道 dubbo 都是自定义的端口,比如上面的我们的 20883,这个端口哪里来的呢?

相信有部分同学可能猜到答案了,没错,就是 Netty 启动的

final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
• 1

我们直接跳转到 DubboProtocolexport 方法

public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    // 启动服务
    openServer(url);
    optimizeSerialization(url);
    return exporter;
}
private void openServer(URL url) {
    checkDestroyed();
    String key = url.getAddress();
    boolean isServer = url.getParameter(IS_SERVER_KEY, true);
    if (isServer) {
        ProtocolServer server = serverMap.get(key);
        // 典型的双端检锁机制
        if (server == null) {
            synchronized (this) {
                server = serverMap.get(key);
                if (server == null) {
                    // createServer:启动服务
                    serverMap.put(key, createServer(url));
                    return;
                }
            }
        }
        // 出现问题重置服务配置
        server.reset(url);
    }
}

创建服务端:

private ProtocolServer createServer(URL url) {
    // 数据组装
    url = URLBuilder.from(url)
        .addParameterIfAbsent(CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString())
        .addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT))
        .addParameter(CODEC_KEY, DubboCodec.NAME)
        .build();
    String transporter = url.getParameter(SERVER_KEY, DEFAULT_REMOTING_SERVER);
    ExchangeServer server = Exchangers.bind(url, requestHandler);
}

直接跳到 NettyServerdoOpen 方法

protected void doOpen() throws Throwable {
    bootstrap = new ServerBootstrap();
    bossGroup = createBossGroup();
    workerGroup = createWorkerGroup();
    final NettyServerHandler nettyServerHandler = createNettyServerHandler();
    channels = nettyServerHandler.getChannels();
    initServerBootstrap(nettyServerHandler);
    ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
    channelFuture.syncUninterruptibly();
    channel = channelFuture.channel();
}
2.3.2 注册Zookeeper

我们这里直接跳到 ZookeeperRegistrydoRegister 方法

public void doRegister(URL url) {
    // 校验
    checkDestroyed();
    // 将接口信息注册至Zookeeper
    zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true), false);
}
// URL:dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752&register-mode=interface&release=3.1.8&side=provider&timeout=100&timestamp=1685542170287

我们看下 Zookeeper 的前后对比:

前:

后:

这时候拿出我们的UnCode转换器将乱码进行转换:

dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752&register-mode=interface&release=3.1.8&side=provider&timeout=100&timestamp=1685542170287

发现其完全正确,这样我们的信息就被注册到了 Zookeeper 上。

3、疑惑解答

我们可以看到,我们第一次获取注册 ZookeeperURL 是:

这个 127.0.0.1:2181 是我们当前 Zookeeper 的地址,

service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873

通过该 URL 我们可以和我们的 Zookeeper 进行一些相互

第二次拿到的 URL

这个 192.168.0.103:20883 是我们 Netty 服务器暴露的地址,将该地址注册至 Zookeeper,便于消费者的访问

dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752&register-mode=interface&release=3.1.8&side=provider&timeout=100&timestamp=1685542170287

四、总结

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。


相关文章
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
73 2
|
18天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
23天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
51 12
|
1月前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
1月前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
54 3
|
7月前
|
Dubbo Java 应用服务中间件
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
|
2月前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
71 2
|
4月前
|
Dubbo Java 应用服务中间件
💥Spring Cloud Dubbo火爆来袭!微服务通信的终极利器,你知道它有多强大吗?🔥
【8月更文挑战第29天】随着信息技术的发展,微服务架构成为企业应用开发的主流模式,而高效的微服务通信至关重要。Spring Cloud Dubbo通过整合Dubbo与Spring Cloud的优势,提供高性能RPC通信及丰富的生态支持,包括服务注册与发现、负载均衡和容错机制等,简化了服务调用管理并支持多种通信协议,提升了系统的可伸缩性和稳定性,成为微服务通信领域的优选方案。开发者仅需关注业务逻辑,而无需过多关心底层通信细节,使得Spring Cloud Dubbo在未来微服务开发中将更加受到青睐。
89 0
|
27天前
|
Dubbo Cloud Native 应用服务中间件
阿里云的 Dubbo 和 Nacos 深度整合,提供了高效的服务注册与发现、配置管理等关键功能,简化了微服务治理,提升了系统的灵活性和可靠性。
在云原生时代,微服务架构成为主流。阿里云的 Dubbo 和 Nacos 深度整合,提供了高效的服务注册与发现、配置管理等关键功能,简化了微服务治理,提升了系统的灵活性和可靠性。示例代码展示了如何在项目中实现两者的整合,通过 Nacos 动态调整服务状态和配置,适应多变的业务需求。
37 2
|
2月前
|
Dubbo Java 应用服务中间件
Dubbo学习圣经:从入门到精通 Dubbo3.0 + SpringCloud Alibaba 微服务基础框架
尼恩团队的15大技术圣经,旨在帮助开发者系统化、体系化地掌握核心技术,提升技术实力,从而在面试和工作中脱颖而出。本文介绍了如何使用Dubbo3.0与Spring Cloud Gateway进行整合,解决传统Dubbo架构缺乏HTTP入口的问题,实现高性能的微服务网关。

推荐镜像

更多