一起玩dubbo,万字长文揭秘服务暴露

日常写组件,最近又接了一个需求,让我负责实现一个rpc组件,提高公司游戏跨服开发的效率,为了写好这个组件,算是将dubbo里里外外研究了一波,目前组件的实现也接近尾声了,因此打算给dubbo的学习做个总结,并穿插说说rpc实现的心路历程,同样需要实现rpc的朋友,或者对dubbo有兴趣的朋友可以关注这个系列。

在写rpc组件之前,我先提了几个灵魂疑问,并从dubbo中找到了答案。

服务是啥?

一个模块,一种玩法,只要是需要进行远程调度的都可以用服务的概念进行包装,我这边简单包装了一个副本服务,类情况如下

image-20210527013946923

平平无奇,等等,我们来看看提供方如何标记服务

image-20210527014002431

到了这一步,服务已经完成了基本定义。

服务最终被注册到了那里?

在xml配置上,我们已经看到了有注册中心的配置

image-20210527014013076

没错,最后提供方定义好的服务会注册到注册中心,目前支持的类型有多种

image-20210527014023579

具体可以查看里边提供的demo实例,那么注册中心有什么作用呢?

简单点描述就是注册中心就是管理服务的地方,提供方将服务放到了这个管理处,而订阅方要用的话则从这个管理处将服务拿过来用,通过注册中心实现了服务的感知。

服务谁来消费?

消费方来使用,我们可以看到

image-20210527014040894

同样也是平平无奇的代码,就是消费方拿到boss接口后,直接调用对应接口即可。

对应提供方有xml去定义服务的注册,同样消费方也是有xml去定义服务的订阅信息,可以看到

image-20210527014051399

简单来说就是,提供方将服务放到注册中心,订阅方从注册中心拿来用。

接下来会涉及到源码部分,以下源码的示例接来自dubbo2.6x,源码方面的注释都已经提交到github上,有需要的可以clone:

https://github.com/wiatingpub/dubbo/tree/2.6.x

什么时候触发的服务暴露

在设计rpc组件的时候,不得不面对这个问题,本着抄dubbo的想法,研究了下dubbo的实现方案

dubbo采用了比较经典的xml配置,并理所当然的使用了NamespaceHandlerSupport将xml中的节点配置映射成了对应对象

可以看到在dubbo-config-spring包底下有个spring.handlers的配置,通过该配置指定了DubboNamespaceHandler

image-20210527014228992

DubboNamespaceHandler会将xml配置对应标签的配置映射成对象,比如service

image-20210527014215448
image-20210527014207406

看看ServiceBean在映射成对象后做了啥,先看看ServiceBean结构

image-20210527014200847

自身是一个监听器,再通过CTRL+F12看看有哪些方法

image-20210527014147890

看到export暴露这个方法后,ALT+F7反调下发现除了注解Annoatition外有两个地方调用,分别是

image-20210527014138412

第一种是在属性被设置后调用,可以看到如果是延迟函数则不会调用。

image-20210527014130269

第二种是看到isDelay的时候才会调用export,也就是说延迟暴露的服务是在监听到ContextRefreshedEvent事件后进行调用的。

在export方法内可以看到

image-20210527014121568

可以针对不同的服务配置配置delay延迟时间,具体的肯定是在xml上配置了。

触发机制到这里基本就结束了,总结一下dubbo的触发机制就是建立在NamespaceHandlerSupport上,将xml中的标签实例化,并通过在afterPropertiesSet或者在监听到Spring容器抛出的容器刷新事件后,触发服务的暴露。

画个流程图总结下

image-20210529184757359

由于我司这边的服务配置最终落地在使用yaml方案上,不引入xml,最终我并没有使用NamespaceHandlerSupport去实例化,而是模仿dubbo3.0的方案包装了一个ServiceBootstrap对象,依赖SmartLifeCycle的生命周期,在start的时候取到yaml的配置,遍历进行服务暴露。dubbo3.0做了比较大调整,后续会专门讲,有兴趣的持续关注该系列。

提一波URL

在说服务暴露之前必须先提一波URL,否则主线没了,后续不好讲。

在我没有接触到dubbo之前,我对URL的定位是指网络地址,而在dubbo中,可以认为是一种约定,几乎dubbo的所有模块都是通过URL来传参,这有什么好处呢?

我们可以想想,如果没有约定好,那么不同的接口之间进行交互的参数便会乱掉,一会是字符串,一会是map,而有了统一的约定后,代码便会更加的规范和统一,我们在看代码的时候也会比较清晰,也容易拓展,比如如果你想拓展什么东西,直接往URL上拼接参数就可以了。

image-20210527014111466

我们可以看到,除了几个基础的参数外,很多参数其实最终都放到了parameters中。

而在我司项目中,我们参考了URL的设计,构建了元数据的结构,也就是map,将服务的部分动态参数通过map进行传递。

服务暴露过程

在深入源码之前先大概总结下服务暴露的几个步骤,分别是:

  1. 配置的构建、合并、检查。

  2. URL的组装。

  3. 服务的暴露、注册。

我将这三个主要的过程放入流程图内

继续跟进服务暴露的具体逻辑,也就是doExport后

protected synchronized void doExport() {
    if (unexported) {
        throw new IllegalStateException("Already unexported!");
    }
    if (exported) {
        return;
    }
    exported = true;
    if (interfaceName == null || interfaceName.length() == 0) {
        throw new IllegalStateException("<dubbo:service interface=\"\" /> interface not allow null!");
    }
    // TODO: 2021/5/27 检查provider是否为空,为空则创建一个,并通过系统变量为其初始化
    checkDefault();

    /** 各种初始值的设置 **/
    
    // TODO: 2021/5/27 检查Application是否为空
    checkApplication();
    // TODO: 2021/5/27 检查注册中心是否为空
    checkRegistry();
    // TODO: 2021/5/27 检查protocols是否为空
    checkProtocol();
    // TODO: 2021/5/27 补充各种参数
    appendProperties(this);
    // TODO: 2021/5/27 Stub合法性检查
    checkStub(interfaceClass);
    // TODO: 2021/5/27 mock合法性检查
    checkMock(interfaceClass);
    if (path == null || path.length() == 0) {
        path = interfaceName;
    }
    // TODO: 2021/5/27 多协议多注册中心暴露服务
    doExportUrls();
    ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), this, ref);
    ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
}

总结下来不外乎两步:

  • 对各类配置进行校验,并且更新部分配置;

  • 多协议多注册中心暴露服务;

其中检查的细节暂时不铺开,因为服务暴露整个过程才是重点,后续服务治理了再重新讲这块,接下来继续讲重点doExportUrls方法

@SuppressWarnings({"unchecked", "rawtypes"})
private void doExportUrls() {
    // TODO: 2021/5/27 加载注册中心URL 
    List<URL> registryURLs = loadRegistries(true);
    for (ProtocolConfig protocolConfig : protocols) {
        // TODO: 2021/5/27 根据不同协议进行服务暴露 
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}

loadRegistries也很简单,其实就是根据注册中心的配置组装成URL,这里多个注册中心比较好理解,多个protocols是什么鬼呢?

其实是这样的,一个服务如果有多个协议那么就都需要暴露,比如同时支持 dubbo 协议和 hessian 协议,那么需要将这个服务用两种协议分别向多个注册中心暴露注册。

参考了这块逻辑,在我司项目中,我们规范了注册中心的接口,允许注册中心有多种实现, 甚至是本地注册中心,但是并不允许有多个注册中心,目前来说是没有这种需求,而要选择哪个注册中心,只需要在yaml文件上进行配置即可

接下来看doExportUrlsFor1Protocol方法

在分析服务暴露流程之前便有提到过,dubbo内部使用URL来携带各类数据,从而贯穿整个生命周期的,而入口其实就是从这个方法开始的,等下我们便可以看到该方法可以分为两个步骤,前个步骤是组装URL的逻辑,后个步骤是真正实现暴露dubbo服务等逻辑的地方,不说了,继续code

private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
    /**组装服务的URL开始**/
    // TODO: 2021/5/27 获取协议名
    String name = protocolConfig.getName();
    // TODO: 2021/5/27 如果为空,则默认是dubbo
    if (name == null || name.length() == 0) {
        name = "dubbo";
    }

    // TODO: 2021/5/27 设置map等各种参数
    Map<String, String> map = new HashMap<String, String>();
    map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE);
    map.put(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion());
    map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));
    if (ConfigUtils.getPid() > 0) {
        map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));
    }
    // TODO: 2021/5/27 添加application、module、provider等信息到map中
    appendParameters(map, application);
    appendParameters(map, module);
    appendParameters(map, provider, Constants.DEFAULT_KEY);
    appendParameters(map, protocolConfig);
    appendParameters(map, this);
    // TODO: 2021/5/27 如果methods的配置列表不为空,则遍历methods配置列表
    if (methods != null && !methods.isEmpty()) {
        for (MethodConfig method : methods) {
            // TODO: 2021/5/27 把方法名加入map
            appendParameters(map, method, method.getName());
            // TODO: 2021/5/27 添加methodconfig对象的字段信息到map中
            String retryKey = method.getName() + ".retry";
            if (map.containsKey(retryKey)) {
                String retryValue = map.remove(retryKey);
                if ("false".equals(retryValue)) {
                    map.put(method.getName() + ".retries", "0");
                }
            }
            // TODO: 2021/5/27 添加ArgumentConfig列表
            List<ArgumentConfig> arguments = method.getArguments();
            if (arguments != null && !arguments.isEmpty()) {
                for (ArgumentConfig argument : arguments) {
                    // convert argument type
                    if (argument.getType() != null && argument.getType().length() > 0) {
                        // TODO: 2021/5/27 利用反射拿到接口类的所有方法
                        Method[] methods = interfaceClass.getMethods();
                        if (methods != null && methods.length > 0) {
                            // TODO: 2021/5/27 遍历methods
                            for (int i = 0; i < methods.length; i++) {
                                String methodName = methods[i].getName();
                                // TODO: 2021/5/27 找到目标方法
                                if (methodName.equals(method.getName())) {
                                    // TODO: 2021/5/27 通过反射拿到方法参数类型
                                    Class<?>[] argtypes = methods[i].getParameterTypes();
                                    // TODO: 2021/5/27 如果下表为-1
                                    if (argument.getIndex() != -1) {
                                        // TODO: 2021/5/27 检测argtypes的名称与ArgumentConfig中的type是否一致
                                        if (argtypes[argument.getIndex()].getName().equals(argument.getType())) {
                                            appendParameters(map, argument, method.getName() + "." + argument.getIndex());
                                        } else {
                                            // TODO: 2021/5/27 不一致则抛出异常
                                            throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
                                        }
                                    } else {
                                        // TODO: 2021/5/27 遍历参数,查找argument.type的类型
                                        for (int j = 0; j < argtypes.length; j++) {
                                            Class<?> argclazz = argtypes[j];
                                            // TODO: 2021/5/27 如果找得到则将ArgumentConfig字段添加map中
                                            if (argclazz.getName().equals(argument.getType())) {
                                                appendParameters(map, argument, method.getName() + "." + j);
                                                if (argument.getIndex() != -1 && argument.getIndex() != j) {
                                                    throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    } else if (argument.getIndex() != -1) {
                        // TODO: 2021/5/27 用户未配置type属性,但配置了index属性,且index != -1,则直接添加到map中
                        appendParameters(map, argument, method.getName() + "." + argument.getIndex());
                    } else {
                        throw new IllegalArgumentException("argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>");
                    }

                }
            }
        } // end of methods for
    }

    // TODO: 2021/5/27 如果是泛化调用,则在map中设置generic和methods
    if (ProtocolUtils.isGeneric(generic)) {
        map.put(Constants.GENERIC_KEY, generic);
        map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
    } else {
        // TODO: 2021/5/27 获得版本号
        String revision = Version.getVersion(interfaceClass, version);
        // TODO: 2021/5/27 放入map中
        if (revision != null && revision.length() > 0) {
            map.put("revision", revision);
        }

        // TODO: 2021/5/27 获得方法集合
        String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
        if (methods.length == 0) {
            logger.warn("NO method found in service interface " + interfaceClass.getName());
            // TODO: 2021/5/27 设置方法为*
            map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
        } else {
            // TODO: 2021/5/27 否则加入方法集合中
            map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
        }
    }
    // TODO: 2021/5/27 将token加入map
    if (!ConfigUtils.isEmpty(token)) {
        if (ConfigUtils.isDefault(token)) {
            map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
        } else {
            map.put(Constants.TOKEN_KEY, token);
        }
    }
    if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) {
        protocolConfig.setRegister(false);
        map.put("notify", "false");
    }
    
    String contextPath = protocolConfig.getContextpath();
    if ((contextPath == null || contextPath.length() == 0) && provider != null) {
        contextPath = provider.getContextpath();
    }

    // TODO: 2021/5/27 获得地址、端口号
    String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
    Integer port = this.findConfigedPorts(protocolConfig, name, map);
    // TODO: 2021/5/27 组装生成URL
    URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);

    /**组装服务的URL结束**/
 
    /*
  * 后续讲解服务暴露
  */
}

这个方法实在是又臭又长,我特意分成两部分,目前这部分是组装服务的URL部分,其实简单点说就是:

先将provider、applicaiton、module等各种基础配置直接放入map中,再针对method配置等进行校验,查看该配置是否有配置方法存在,并进行方法签名的校验,如果是才放入map中,然后还额外将一些多余数据,比如泛化调用、版本号等加入map中,最终根据host和port,结合map组装成URL,貌似还是有点长。

总归就是结合服务自身的各种配置放入map中,然后根据host和port以及map等生成URL就是了。

接下来看看后续服务暴露部分

private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
     /*
  * 前面URL组装
  */
    
    // TODO: 2021/5/27 加载ConfiguratorFactory,并生成Configurator实例,判断是否有该协议的实现存在
    if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
            .hasExtension(url.getProtocol())) {
        // TODO: 2021/5/27 通过SPI机制配置URL
        url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
    }

    String scope = url.getParameter(Constants.SCOPE_KEY);
    // TODO: 2021/5/27 如果scope为none,则什么都不做
    if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {

        // TODO: 2021/5/27 如果scope不是远程,则暴露到本地
        if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
            /** 本地服务暴露 **/
            exportLocal(url);
        }
        // TODO: 2021/5/27 如果不是local,则暴露到远程
        if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
            if (logger.isInfoEnabled()) {
                logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
            }
            if (registryURLs != null && !registryURLs.isEmpty()) {
                // TODO: 2021/5/27 遍历注册中心
                for (URL registryURL : registryURLs) {
                    url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
                    // TODO: 2021/5/27 加载监视器连接
                    URL monitorUrl = loadMonitor(registryURL);
                    if (monitorUrl != null) {
                        // TODO: 2021/5/27 如果没有则添加一个
                        url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
                    }
                    if (logger.isInfoEnabled()) {
                        logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
                    }

                    // TODO: 2021/5/27 根据URL拿到代理方式
                    String proxy = url.getParameter(Constants.PROXY_KEY);
                    if (StringUtils.isNotEmpty(proxy)) {
                        // TODO: 2021/5/27 给注册中心的URL添加代理方式
                        registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
                    }

                    // TODO: 2021/5/24 通过SPI机制拿到对应的proxyFactory
                    /** 根据proxyFactory拿到Invoker **/
                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                    DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                    // TODO: 2021/5/24 通过SPI机制拿到对应的protocol,先是RegistryProtocol,再被AOP强化
                    /** 服务暴露 **/
                    Exporter<?> exporter = protocol.export(wrapperInvoker);
                    exporters.add(exporter);
                }
            } else {
                // TODO: 2021/5/24 通过SPI机制拿到对应的proxyFactory
                /** 根据proxyFactory拿到Invoker **/
                Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
                DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                // TODO: 2021/5/24 通过SPI机制拿到对应的protocol
                /** 服务暴露 **/
                Exporter<?> exporter = protocol.export(wrapperInvoker);
                exporters.add(exporter);
            }
        }
    }
    this.urls.add(url);
}

后续重要的地方可以认为其实就是遍历注册中心进行服务暴露,只是会根据服务配置域scope来针对性做一些暴露处理,比如如果scope不是远程,则暴露到本地,如果不是local,则暴露到远程。

该方法中又包含了几个核心的拓展实现,包括:

  1. 本地服务暴露

  2. 根据proxyFactory拿到Invoker

  3. 远程服务暴露、注册

继续补充流程图,整理思路

image-20210529230858560

首先第1点,看看本地服务暴露逻辑

private void exportLocal(URL url) {
    if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
        URL local = URL.valueOf(url.toFullString())
                .setProtocol(Constants.LOCAL_PROTOCOL)
                .setHost(LOCALHOST)
                .setPort(0);
        StaticContext.getContext(Constants.SERVICE_IMPL_CLASS).put(url.getServiceKey(), getServiceClass(ref));
        // TODO: 2021/5/27 根据SPI拿到了InjvmProtocol调用了export方
        Exporter<?> exporter = protocol.export(
                proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
        // 放入集合中缓存
        exporters.add(exporter);
        logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
    }
}
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}

暴露到本地的大致逻辑其实就是根据SPI机制拿到了InjvmProtocol生成了InjvmExporter,之后放入集合缓存中,至于SPI机制,后续需要开个文章专门讲讲,有兴趣持续关注该系列。

为啥要有本地服务暴露?

大致原因应该是因为可能存在同一个 JVM 内部引用自身服务的情况,因此暴露的本地服务在内部调用的时候可以直接消费同一个 JVM 的服务避免了网络间的通信。

继续看第2点,根据proxyFactory拿到Invoker部分,首先我们看ProxyFactory类名就大概可以猜到该类具备生成代理对象的能力,我们看proxyFactory的生成模式

image-20210527012515717

可以看到,该对象也是通过SPI机制生成的,由于SPI机制也是比较庞大的,为了避免混淆,后续再开篇文章讲解,有兴趣的持续关注。

通过SPI机制拿到了ProxyFactory的实现对象JavassisProxyFactory,最终调用的代码

@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    // TODO: 2021/5/23 为目标类创建Wrapper
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    // TODO: 2021/5/23 创建匿名的Invoker对象,并实现doInvoker方法
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName,
                                  Class<?>[] parameterTypes,
                                  Object[] arguments) throws Throwable {
            // TODO: 2021/5/23 调用Wrapper的invokeMethod方法,invokeMethod最终会调用目标方法
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
}

该方法就是创建了一个匿名的Inovker对象,在doInvker方法中调用wrapper.invokeMethod方法,invokeMethod最终会调用目标方法。

那么wrapper又是啥?

Wrapper是一个抽象类,在调用Wrapper.getWrapper创建子类的时候,会根据目标Class对象进行解析,拿到各种方法、类成员变量等信息,以及生成invokeMethod方法等代码,在代码生成完毕后,通过Javassist生成Class对象,可以理解为该Class对象就是BossServiceImpl的代理实例,有兴趣了解生成过程的可以看Wrapper.makeWrapper方法。

为啥一定要封装Invoker?

其实就是为了屏蔽本地调用或者远程调用或者集群调用的细节,统一暴露出一个可执行体,方便调用者调用,而不管怎么封装,其实最终都是调向目标方法。

为啥要封装Exporter?

这个涉及到后续服务被具体调用,后面会开一篇文章专门讲这个,有兴趣的可以持续关注。

在我司的rpc框架中,倒是没有使用Javassist去生成代理对象,而是选择了使用jdk提供的Proxy生成机制。

继续补充流程图,整理思路

image-20210529232357356

接下来说说远程服务暴露

远程服务暴露要比本地复杂的多,在doExportUrlsFor1Protocol后半部分,通过proxyFactory生成Inovker后,就需要调用protocol.export做真的服务暴露了,我们可以看到protocol是如何实例化的

image-20210529150630482

又是通过SPI实例化的,通过断点可以看到会先被AOP切面拦截额外做了一些其他的操作,不过最终走向的RegisterProtocol,AOP这块后续再分析,有兴趣的持续关注。

接下来继续看RegisterProtocol.export做了啥

@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    // TODO: 2021/5/29 服务暴露
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
    // TODO: 2021/5/23 获得注册中心的URL
    URL registryUrl = getRegistryUrl(originInvoker);

    final Registry registry = getRegistry(originInvoker);
    // TODO: 2021/5/23 获得已经注册的服务提供者URL
    final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);

    boolean register = registeredProviderUrl.getParameter("register", true);

    ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);
    if (register) {
        // TODO: 2021/5/29 真正做服务注册的地方
        register(registryUrl, registeredProviderUrl);
        ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
    }

    // TODO: 2021/5/23 获取override订阅URL
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);
    // TODO: 2021/5/23 创建override的监听器
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
    // TODO: 2021/5/23 缓存监听器到集合中
    overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
    // TODO: 2021/5/23 向注册中心订阅override数据
    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
    // TODO: 2021/5/23 创建并返回DestroyableExporter
    return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
}

从代码上看,该方法其实做了两件事情,分别是服务暴露和注册:

  • 执行了doLocalExport进行服务暴露

  • 加载注册中心实现类,向注册中心注册服务

  • 向注册中心订阅override数据

  • 创建并返回DestroyableExporter

接下来继续看看doLocalExport做了啥

private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) {
    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) {
                // TODO: 2021/5/24 创建Invoker为委托对象
                final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
                // TODO: 2021/5/24 调用protocol的export方法暴露服务
                exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
                bounds.put(key, exporter);
            }
        }
    }
    return exporter;
}

看逻辑比较简单,主要是根据不同协议配置,根据SPI调用不同的protocol实现,跟暴露到本地时实现的InjvmPortocol一样,默认这里调用的是DubboProtocol.export

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

    // TODO: 2021/5/29 得到服务key,格式:group+"/"+serviceName+":"+serviceVersion+":"+port
    String key = serviceKey(url);
    // TODO: 2021/5/29 创建exporter 
    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);
        }
    }

    // TODO: 2021/5/24 开启服务器
    openServer(url);
    // TODO: 2021/5/29 序列化 
    optimizeSerialization(url);
    return exporter;
}

可以到export先是new了一个DubboExporter对象, 后续打开了服务,接下来继续看openServer做了啥

private void openServer(URL url) {
    String key = url.getAddress();
    boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
    if (isServer) {
        ExchangeServer server = serverMap.get(key);
        if (server == null) {
            // TODO: 2021/5/24 启动一个服务实例
            serverMap.put(key, createServer(url));
        } else {
            // server supports reset, use together with override
            server.reset(url);
        }
    }
}

private ExchangeServer createServer(URL url) {
    // TODO: 2021/5/29 服务器关闭是发送readonly时间 
    url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString());
    // TODO: 2021/5/29 心跳默认时间 
    url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
    // TODO: 2021/5/29 获得远程通讯服务端实现方式 
    String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER);

    if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str))
        throw new RpcException("Unsupported server type: " + str + ", url: " + url);
    // TODO: 2021/5/29 添加编解码器DubboCodec实现 
    url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
    ExchangeServer server;
    try {
        // TODO: 2021/5/29 启动服务器 
        server = Exchangers.bind(url, requestHandler);
    } catch (RemotingException e) {
        throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
    }
    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;
}

可以看到最终还是依赖URL携带的远程通讯实现方法创建了一个服务器对象。

总结一下:doLocalExport最终其实就是根据URL开启了服务器,并返回了Exporter。

接下来继续看注册服务部分

public void register(URL registryUrl, URL registedProviderUrl) {
    // TODO: 2021/5/29 获取注册中心实例
    Registry registry = registryFactory.getRegistry(registryUrl);
    // TODO: 2021/5/29 调用register
    registry.register(registedProviderUrl);
}

Regsitry的生成最终也是依赖了SPI机制,最终走向FailbackRegistry.register

@Override
public void register(URL url) {
    super.register(url);
    // TODO: 2021/5/24 从失败的集合中移除
    failedRegistered.remove(url);
    failedUnregistered.remove(url);
    try {
        // TODO: 2021/5/24 向注册中心发起注册请求
        doRegister(url);
    } catch (Exception e) {
        Throwable t = e;

        boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
                && url.getParameter(Constants.CHECK_KEY, true)
                && !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol());
        boolean skipFailback = t instanceof SkipFailbackWrapperException;
        if (check || skipFailback) {
            if (skipFailback) {
                t = t.getCause();
            }
            throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
        } else {
            logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
        }

        // TODO: 2021/5/29 发生异常则放入failedRegistered
        failedRegistered.add(url);
    }
}

可以看到注册的核心实现是在doRegister中,不过通过代码机制我们也可以看出,在注册报错的时候会被trycatch拦截,然后放入failedRegistered容器中,结合FailbackRegistry该类名可以推测应该是有个重试机制存在,看看构造方法

// TODO: 2021/5/24 从url中获取重试频率参数,启动定时器进行重试逻辑
public FailbackRegistry(URL url) {
    super(url);
    this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
    this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            try {
                // TODO: 2021/5/29 定时重试 
                retry();
            } catch (Throwable t) { // Defensive fault tolerance
                logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t);
            }
        }
    }, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS);
}

果不其然,最终如果注册发生了异常,则会进行定时重试。

关于重试机制也是要有的,在我司的rpc框架中,我们将重试时间放在yaml上去配置,不过定时器并没有采用Executor机制,而是模仿了dubbo3.0的写法,也就是时间轮的机制,性能更好。

接下来看注册核心部分doRegister,可以看到该方法是一个抽象方法,由于我在xml配置中配置的注册中心是Zookeeper,因而最终走向ZookeeperRegistry

@Override
protected void doRegister(URL url) {
    try {
        zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
    } catch (Throwable e) {
        throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}

服务注册走到这里基本到头了,再深入便是看注册中心的实现了。

接下来看看向注册中心订阅override数据部分

上面有说过registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener)最终走向的方法是FailbackRegistry.subscribe

public void subscribe(URL url, NotifyListener listener) {
    super.subscribe(url, listener);
    removeFailedSubscribed(url, listener);
    try {
        // TODO: 2021/5/29 真正做订阅的地方 
        doSubscribe(url, listener);
    } catch (Exception e) {
        Throwable t = e;

        List<URL> urls = getCacheUrls(url);
        if (urls != null && !urls.isEmpty()) {
            notify(url, listener, urls);
            logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + getUrl().getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/dubbo-registry-" + url.getHost() + ".cache") + ", cause: " + t.getMessage(), t);
        } else {
            // If the startup detection is opened, the Exception is thrown directly.
            boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
                    && url.getParameter(Constants.CHECK_KEY, true);
            boolean skipFailback = t instanceof SkipFailbackWrapperException;
            if (check || skipFailback) {
                if (skipFailback) {
                    t = t.getCause();
                }
                throw new IllegalStateException("Failed to subscribe " + url + ", cause: " + t.getMessage(), t);
            } else {
                logger.error("Failed to subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
            }
        }

        // TODO: 2021/5/29 订阅失败,则放入失败容器中 
        addFailedSubscribed(url, listener);
    }
}

同样,订阅失败后也是放入失败容器中,定时重试进行订阅。

再看看核心实现方法doSubscribe方法,最终走向ZookeeperRegistry.doSubscribe中

@Override
protected void doSubscribe(final URL url, final NotifyListener listener) {
    try {
        // TODO: 2021/5/29 处理URL参数中interface为*的订阅,例如监控中心的订阅
        if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
            /** 先无视 **/
        } else {
            List<URL> urls = new ArrayList<URL>();
            // TODO: 2021/5/29 遍历分类数组
            for (String path : toCategoriesPath(url)) {
                // TODO: 2021/5/29 获得监听器集合
                ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                // TODO: 2021/5/29 如果没有则创建
                if (listeners == null) {
                    zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
                    listeners = zkListeners.get(url);
                }
                // TODO: 2021/5/29 获得监听器
                ChildListener zkListener = listeners.get(listener);
                if (zkListener == null) {
                    listeners.putIfAbsent(listener, new ChildListener() {
                        @Override
                        public void childChanged(String parentPath, List<String> currentChilds) {
                            // TODO: 2021/5/29 通知服务变化,回调NotifyListener
                            ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
                        }
                    });
                    zkListener = listeners.get(listener);
                }
                // TODO: 2021/5/29 创建节点,如:/dubbo/com.alibaba.dubbo.demo.DemoService/providers
                zkClient.create(path, false);
                List<String> children = zkClient.addChildListener(path, zkListener);
                if (children != null) {
                    urls.addAll(toUrlsWithEmpty(url, path, children));
                }
            }
            // TODO: 2021/5/29 通知数据变更,如RegistryDirectory
            notify(url, listener, urls);
        }
    } catch (Throwable e) {
        throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}

这个方法主要做了订阅和监听触发逻辑,具体逻辑就是订阅了某个服务的URL,在服务变更的时候触发逻辑变化。其实此处已经是可以归纳入服务治理模块了,后续会有专门的文章分享服务治理,有兴趣可以持续关注。

画个流程图,整理下思路

image-20210529232729748

看到这里服务暴露流程基本理完了,还是有点东西在里面的,并且还需要掌握 Dubbo SPI,不然有些点例如自适应什么的还是很难理解的,为了写这篇文章,我前前后后也是花了不少的时间。

最后我再来一张完整的流程图带大家再过一遍,具体还是有很多细节,不过不是主干我就不做分析了,不然文章散掉了。

dubbo流程

后续服务治理、APO、SPI机制也会在该流程图上进行拓展,有兴趣的也可以关注流程图链接:

https://www.processon.com/view/link/60b25f275653bb3c7e646934

总结

虽然看完了该篇文章,但是还是建议大家自己打断点过一遍,可以更加清晰,而如果是为了应付面试官提问的话,基本上记住上面流程图的内容就差不多了,当你研究完了dubbo后,其实会发现dubbo有很多东西可以写,比如服务应用、SPI、dubbo中的AOP机制、服务治理等好几个模块,最后就是带大家撸一个RPC框架了,还是那句话,想学dubbo的可以持续关注这一系列。

- 原创不易,帮忙点个在看、分享支持一下 -

- 往期文章推荐 -

一起玩dubbo,先入个门


Caffeine实战教程篇


元旦用SpringBoot撸了个博客网站送给大家


聊聊Autowired的常考面试题


面试官告诉你什么是JMM和常考面试题


去年面了多个候选人,看看我挖的坑还有他们应该要补的Java基础(二)


去年面了多个候选人,看看我挖的坑还有他们应该要补的Java基础(一)


想学dubbo的关注我

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值