天天看點

從源碼全面解析 dubbo 服務暴露的來龍去脈

作者:愛做夢的程式員

一、引言

對于 Java 開發者而言,關于 dubbo ,我們一般當做黑盒來進行使用,不需要去打開這個黑盒。

但随着目前程式員行業的發展,我們有必要打開這個黑盒,去探索其中的奧妙。

本期 dubbo 源碼解析系列文章,将帶你領略 dubbo 源碼的奧秘

本期源碼文章吸收了之前 Spring、Kakfa、JUC源碼文章的教訓,将不再一行一行的帶大家分析源碼,我們将一些不重要的部分當做黑盒處理,以便我們更快、更有效的閱讀源碼。

雖然現在是網際網路寒冬,但乾坤未定,你我皆是黑馬!

廢話不多說,發車!

二、環境配置

本篇文章适合對 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 的實作:

java複制代碼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:應用級注冊
從源碼全面解析 dubbo 服務暴露的來龍去脈

我們看一下 exportServices 是如何對其進行處理的

直接來到 org.apache.dubbo.config.ServiceConfig 的 doExport 方法

java複制代碼protected synchronized void doExport() {
    // 将一個服務暴露成多個URL
    doExportUrls();
    exported();
}
           

1.1 擷取注冊的URL

java複制代碼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®ister-mode=instance®istry=zookeeper&release=3.1.8×tamp=1685375979873
    }
    
    // 根據注冊類型拼接URL配置
    return genCompatibleRegistries(interfaceConfig.getScopeModel(), registryList, provider);
}
           

1.2 應用注冊

  • 目前 registerMode(注冊類型) 是 instance 或者 all 時,走應用注冊
java複制代碼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:
  • http複制代碼
  • 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®ister-mode=instance®istry=zookeeper&release=3.1.8×tamp=1685375979873

1.3 接口注冊

  • 目前 registerMode(注冊類型) 是 interface 或者 all 時,走接口注冊
  • java複制代碼
  • if (DEFAULT_REGISTER_MODE_INTERFACE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode)) { result.add(registryURL); }
  • URL
  • http複制代碼
  • 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®ister-mode=instance®istry=zookeeper&release=3.1.8×tamp=1685375979873

2、服務導出

從源碼全面解析 dubbo 服務暴露的來龍去脈
java複制代碼// 周遊目前的傳輸協定(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:
  • http複制代碼
  • 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®ister-mode=instance&release=3.1.8&revision=1.0.0.dev&side=provider&timeout=100×tamp=1685377125239&version=1.0.0.dev

2.1 服務暴露

java複制代碼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 裡面的方法

java複制代碼public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('#39;) < 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 服務導出

java複制代碼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 啟動的

java複制代碼final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
           

我們直接跳轉到 DubboProtocol 的 export 方法

java複制代碼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);
    }
}
           

建立服務端:

java複制代碼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);
}
           

直接跳到 NettyServer 的 doOpen 方法

java複制代碼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

我們這裡直接跳到 ZookeeperRegistry 的 doRegister 方法

java複制代碼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®ister-mode=interface&release=3.1.8&side=provider&timeout=100×tamp=1685542170287
           

我們看下 Zookeeper 的前後對比:

前:

從源碼全面解析 dubbo 服務暴露的來龍去脈

後:

從源碼全面解析 dubbo 服務暴露的來龍去脈

這時候拿出我們的UnCode轉換器将亂碼進行轉換:

http複制代碼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®ister-mode=interface&release=3.1.8&side=provider&timeout=100×tamp=1685542170287
           

發現其完全正确,這樣我們的資訊就被注冊到了 Zookeeper 上。

3、疑惑解答

我們可以看到,我們第一次擷取注冊 Zookeeper 的 URL 是:

這個 127.0.0.1:2181 是我們目前 Zookeeper 的位址,

java複制代碼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®ister-mode=instance®istry=zookeeper&release=3.1.8×tamp=1685375979873
           

通過該 URL 我們可以和我們的 Zookeeper 進行一些互相

第二次拿到的 URL:

這個 192.168.0.103:20883 是我們 Netty 伺服器暴露的位址,将該位址注冊至 Zookeeper,便于消費者的通路

java複制代碼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®ister-mode=interface&release=3.1.8&side=provider&timeout=100×tamp=1685542170287
           

四、總結

魯迅先生曾說:獨行難,衆行易,和志同道合的人一起進步。彼此毫無保留的分享經驗,才是對抗網際網路寒冬的最佳選擇。

其實很多時候,并不是我們不夠努力,很可能就是自己努力的方向不對,如果有一個人能稍微指點你一下,你真的可能會少走幾年彎路。

作者:愛敲代碼的小黃

連結:https://juejin.cn/post/7240636320762413114

繼續閱讀