dubbo系列之服务导出

写在前面

以下参考自 Dubbo 官方文档;文章基于 dubbo-spring-boot-starter 2.7.0,配置是基本配置;

首先看下服务导出这个过程在 Spring 中是如何被触发的:
服务导出触发流程图
Dubbo 服务导出过程始于 Spring 容器发布刷新事件,Dubbo 在接收到事件后,会立即执行服务导出逻辑。

待导出服务的实现类 UserServiceImpl代码 :

package com.duofei.service;

import org.apache.dubbo.config.annotation.Service;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 用户服务实现类
 * @author duofei
 * @date 2020/1/16
 */
@Service(interfaceClass = UserService.class)
public class UserServiceImpl implements UserService {

    @Override
    public List<String> getDefaultUserNames() {
        return Stream.of("num1", "num2").collect(Collectors.toList());
    }
}


文档中提到服务导出的入口方法是 ServiceBeanonApplicationEvent 方法,这首先得保证 ServiceBean 能够正常注入 Spring 容器中;那这部分工作由谁来完成呢?ServiceAnnotationBeanPostProcessor 类负责将带有 org.apache.dubbo.config.annotation.Service 注解的类注册为普通的 bean 和 ServiceBean,也就是上面的 UserServiceImpl 最后会注入两个 bean 到 Spring 容器中,分别是 userServiceImpl (class=com.duofei.service.UserServiceImpl) 和 ServiceBean:com.duofei.service.UserService (class=org.apache.dubbo.config.spring.ServiceBean);所以最终服务导出的功能将由 ServiceBean 来完成。

导出服务

先看图:
服务导出流程图


组装 URL

需要导出的服务在被包装为 ServiceBean 放入Spring 容器,而 ServiceBean 本身又实现了很多能够获取回调的接口,其中服务导出需要关注的是实现 ApplicationListener<ContextRefreshedEvent> 接口的 onApplicationEvent 方法:

	@Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (!isExported() && !isUnexported()) {
            if (logger.isInfoEnabled()) {
                logger.info("The service ready on spring started. service: " + getInterface());
            }
            export();
        }
    }

if 判断语句中是来源于服务的配置,我们需要关注的是 export 方法:

    @Override
    public void export() {
        super.export();
        // Publish ServiceBeanExportedEvent
        publishExportEvent();
    }

	// 以下来自于 父类 ServiceConfig
	public synchronized void export() {
        checkAndUpdateSubConfigs();

        if (provider != null) {
            if (export == null) {
                export = provider.getExport();
            }
            if (delay == null) {
                delay = provider.getDelay();
            }
        }
        if (export != null && !export) {
            return;
        }

        if (delay != null && delay > 0) {
            delayExportExecutor.schedule(this::doExport, delay, TimeUnit.MILLISECONDS);
        } else {
            doExport();
        }
    }

	protected synchronized void doExport() {
        if (unexported) {
            throw new IllegalStateException("Already unexported!");
        }
        if (exported) {
            return;
        }
        exported = true;

        if (path == null || path.length() == 0) {
            path = interfaceName;
        }
        ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), ref, interfaceClass);
        ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
        doExportUrls();
    }
	
	private void doExportUrls() {
        List<URL> registryURLs = loadRegistries(true);
        for (ProtocolConfig protocolConfig : protocols) {
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    

在判断完是否需要延迟加载以后,服务导出所做的工作:

  1. 加载注册中心URL;
  2. 多协议多注册中心导出服务;

URL 的组装非常复杂,但也很重要,它主要集中在 doExportUrlsFor1Protocol 方法的前半段,鉴于过长的代码,这里就不粘贴。

Dubbo 使用 URL 作为配置载体,采用 URL 作为配置信息的统一格式,所有拓展点都通过传递 URL 携带配置信息。


导出服务

导出服务的具体代码在 doExportUrlsFor1Protocol 的后半段。

导出服务到本地

当配置不是远程的,则导出到本地,查看 exporLocal 方法:

	private void exportLocal(URL url) {
        // 如果 URL 的协议等于 injvm ,说明已经导出到本地了,无需再次导出
        if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
            URL local = URL.valueOf(url.toFullString())
                    .setProtocol(Constants.LOCAL_PROTOCOL)
                    .setHost(LOCALHOST)
                    .setPort(0);
            // 创建 Invoker,并导出服务,这里的 protocol 会在运行时调用 InjvmProtocol 的 export 方法,最终返回 InjvmExporter
            Exporter<?> exporter = protocol.export(
                    proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
            exporters.add(exporter);
            logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
        }
    }

这里使用的是 InjvmProtocol,最终产生的 ExporterInjvmExporter,保存下 exporter

这里就利用到了 dubbo 的 SPI 机制,简单来说,在这里,它会调用传入的参数的 getUrl 方法,然后调取 URL 的 getProtocol 方法,通过得到的值,来选择具体的实现类。因为这里可以发现使用了 invjm 协议,自然会去调用 InjvmProtocol 实现类。具体的原理参考我这篇博文:Dubbo系列之Dubbo SPI

导出服务

如果配置不是本地,则导出到远程,在我的 UserService 配置中,scope 为 null,所以既导出到了本地,又导出到了远程,这个需要注意;

因为这里的协议为 registry ,所以自然会调用 RegistryProtocol 的 export 方法。

RegistryProtocol 的 export 方法中的 doLocalExport 方法调用涉及到了导出服务,然后后续的其它代码则负责服务注册:

	private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
        String key = getCacheKey(originInvoker);
        // 访问缓存
        ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
        if (exporter == null) {
            synchronized (bounds) {
                exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
                if (exporter == null) {
					// 创建 Invoker 为委托对象
                    final Invoker<?> invokerDelegete = new InvokerDelegate<T>(originInvoker, providerUrl);
                    // 调用 protocol 的 export 方法导出服务,此处调用的是 DubboProtocol
                    exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
                    bounds.put(key, exporter);
                }
            }
        }
        return exporter;
    }

接下来跳转到 DubboProtocolexport 方法:

	public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        URL url = invoker.getUrl();

        // 获取服务标识,理解成服务坐标也行。由服务组名,服务名,服务版本号以及端口组成
        String key = serviceKey(url);
        DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
        exporterMap.put(key, exporter);

        //本地存根相关
        Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
        Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
        if (isStubSupportEvent && !isCallbackservice) {
            String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
            if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
                if (logger.isWarnEnabled()) {
                    logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
                            "], has set stubproxy support event ,but no stub methods founded."));
                }
            } else {
                stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
            }
        }
		// 启动服务器
        openServer(url);
        // 优化序列化
        optimizeSerialization(url);
        return exporter;
    }

重点关注启动服务器代码:

	private void openServer(URL url) {
        // 查找服务器 key,host:port,用于标识当前的服务器实例
        String key = url.getAddress();
        // 客户端可以导出仅用于服务器调用的服务
        boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
        if (isServer) {
            // 从缓存中获取
            ExchangeServer server = serverMap.get(key);
            if (server == null) {
                synchronized (this) {
                    server = serverMap.get(key);
                    if (server == null) {
                        // 创建服务器实例
                        serverMap.put(key, createServer(url));
                    }
                }
            } else {
                // 服务器已创建,则根据 url 中的配置重置服务器
                server.reset(url);
            }
        }
    }

客户端会缓存服务器实例,使用 host:port 标识当前的服务器实例,服务器实例存在时,则会根据当前 URL 重置服务器,否则新建服务器实例:

	private ExchangeServer createServer(URL url) {
        url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString());
        // 添加心跳检测配置到 url 中
        url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
        // 获取 server 参数,默认为 netty
        String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER);
		// 通过 SPI 检测是否存在 server 参数所代表的 Transporter 拓展,不存在则抛出异常
        if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
            throw new RpcException("Unsupported server type: " + str + ", url: " + url);
        }
		// 添加编码解码器参数
        url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
        ExchangeServer server;
        try {
           	// 创建 ExchangeServer
            server = Exchangers.bind(url, requestHandler);
        } catch (RemotingException e) {
            throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
        }
        // 获取 client 参数,可指定 netty, mina
        str = url.getParameter(Constants.CLIENT_KEY);
        if (str != null && str.length() > 0) {
            Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();
            if (!supportedTypes.contains(str)) {
                throw new RpcException("Unsupported client type: " + str);
            }
        }
        return server;
    }

创建服务器实例的详细代码就不再追述了,如果是 Netty,则最终会创建成功 NettyServer


向注册中心注册服务

这一步就是 RegistryProtocol 实现类中 export 方法剩下的代码所做的事了:

	@Override
    public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        // 获取注册中心 url:zookeeper://
        URL registryUrl = getRegistryUrl(originInvoker);
        // 获取提供者 url : dubbo://
        URL providerUrl = getProviderUrl(originInvoker);

        // 获取可订阅的 url:  provider://
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
        // 监听用于重写URL
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

        providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
        // 导出服务
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

        // 获取注册中心,此处是 ZookeeperRegistry
        final Registry registry = getRegistry(originInvoker);
        // 注册到注册中心的 url
        final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
        ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
                registryUrl, registeredProviderUrl);
        //判断是否需要延迟发布
        boolean register = registeredProviderUrl.getParameter("register", true);
        if (register) {
            // 使用注册中心注册 提供者URL
            register(registryUrl, registeredProviderUrl);
            // Invoker 设置注册标记位
            providerInvokerWrapper.setReg(true);
        }

        // Deprecated! Subscribe to override rules in 2.6.x or before.
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

        exporter.setRegisterUrl(registeredProviderUrl);
        exporter.setSubscribeUrl(overrideSubscribeUrl);
        //Ensure that a new exporter instance is returned every time export
        return new DestroyableExporter<>(exporter);
    }

这里主要做的事情是向注册中心注册 URL ,一定要理解清楚 DubboSPI 机制。


总结

分析过程中发现,一些可拓展点大量运用了 SPI 机制,你会发现,对于解耦以及拓展,该机制用起来真的很棒;但需要注意分析,否则有可能真的找不到具体调用的实现类;

dubbo 中拥有大量的配置,最终这些配置又会以 URL 为载体,ServiceBean 是针对单个服务的,每个服务都将会有以上过程,但又可以因为配置选择是否执行某些过程,以上过程大致分为:

  1. 配置检查;
  2. 组装 URL;
  3. 多协议多注册中心导出服务;
    1. 导出服务,如创建 NettyServer
    2. 向注册中心注册服务,如注册服务 URLzookeeper

过程的简单,并不意味着实现的容易,尤其是在多协议多注册中心导出服务这一步,很复杂,但仍然不丧失了灵活性,这还是多亏了 SPI 的自适应拓展机制;还有一些点并没有提到,如 Filter 是如何同 Invoker 构造链的,这个可以参考 ProtocolFilterWrapper 类的 export 方法,它介绍了如何将 filterInvoker 构造成执行链;

推荐博文


dubbo系列之dubbo SPI


参考博文


dubbo官方文档-服务导出


我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值