Apache Dubbo 系列之不保证你能看懂的服务暴露过程

·  阅读 1012
Apache Dubbo 系列之不保证你能看懂的服务暴露过程

🌟 欢迎关注个人技术公众号:野区杰西

前言

如果看了这个标题你还进来,说明你是真爱粉或者单纯想进行看看我写的多烂的。那么恭喜你,有一半的几率会是你所期望的那样,我写的真的很烂。还有另一半的几率是但是也有可能写得只是很一般。

其实我的本意起这个名字是吸引更多地人进来,无论是那些第一次研究 Dubbo 的同行 or 已经麻溜玩转 Dubbo 的大佬,给我的文章提供更多的意见或者建议。

ok 废话不多说,开始切入主题!

核心描述

首先,我想说明的是服务暴露对于微服务来说,都不是非常高大上的事情。它的核心在于,将服务的调用方式暴露出来那么对于 Dubbo 来说,所谓的服务暴露实际上就是将远程调用的目标方法以 URL 的方式暴露出去。核心概念用一句话描述就是如此的简单,但是实际上在应用场景上实际过程的描述肯定是非常复杂且代码多。但是我们记住这句话,希望能将这句话贯穿全文!

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

那么我们对核心的拆解,剩下的就是一个大纲式的流程。初次接触过 Dubbo 的人都知道,你要调用一个服务肯定先对服务进行一个描述和配置。目前 Dubbo 支持两种方式来进行服务元数据的描述:注解和 XML;那么服务描述完了,那么我们需要将元数据的进行封装成 Dubbo 内部框架的对象/集合;封装好后,现在就是进行一些其他数据的处理例如注册中心、协议、访问等参数的校验;最后一步就是等待暴露。

所以我们总结下来就是几个步骤

  1. 服务元数据的描述
  2. 服务元数据的解析
  3. 根据各种配置参数(服务元数据、注册中心等)进行处理
  4. 等待暴露

只要记住步骤,我们就不会迷路。牵着我的手,咱们开始!

源码解析

版本

本文使用版本为 Dubbo 2.6.9。大家大可不必担忧版本差异太大的问题,因为在服务暴露这一块的思路都是差不多的。

元数据描述

之前我都是通过例子来进行讲述的。但是我觉得,例子这种东西其实到处都可以找得到,甚至我可以推荐给你去 Dubbo Gihub 上面,源码目录的 dubbo-demo 下,有关于注解与 XML 的两种方式的例子,自己可以去搭建一下。那我在这里使用的是 XML 的方式。下面是服务提供者的 XML。

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    <!-- 1. 应用级别的配置 -->
    <dubbo:application name="demo-provider">
        <dubbo:parameter key="mapping-type" value="metadata"/>
    </dubbo:application>
    <!-- 2. 配置中心的配置 -->
    <dubbo:config-center address="zookeeper://127.0.0.1:2181"/>
    <dubbo:metadata-report address="zookeeper://127.0.0.1:2181"/>
    <dubbo:registry id="registry1" address="zookeeper://127.0.0.1:2181?registry-type=service"/>
    <!-- 3. 协议的配置 -->
    <dubbo:protocol name="dubbo" port="-1"/>
    <!-- 4. 引入服务(方法) -->
    <bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>
    <!-- 5. 通过 Dubbo 的语言进行描述服务 -->
    <dubbo:service interface="org.apache.dubbo.demo.DemoService" timeout="3000" ref="demoService" registry="registry1"/>
</beans>
复制代码

上面的 XML 是具备清晰的描述,所以我们这里可以快速通过。

服务元数据的解析

元数据怎么解析?其实无非解析就是将 XML 的内容转成 Dubbo 内部的实体信息。但是什么时候解析呢?在实际场景中,我们最常见的使用 Spring Boot + Dubbo 的。那么很轻易我们可以猜到是使用 Spring 提供的 NameSpaceHandler 进行解析的。实际上解析的这个类是 DubboNamespaceHandler,我们康康!

    public BeanDefinition parse(Element element, ParserContext parserContext) {
        //获取注册器的上下文环境
        BeanDefinitionRegistry registry = parserContext.getRegistry();
        //注册注解解析器
        registerAnnotationConfigProcessors(registry);
        registerCommonBeans(registry);
        //通过父类获得解析的结果
        BeanDefinition beanDefinition = super.parse(element, parserContext);
        //处理
        setSource(beanDefinition);
        return beanDefinition;
    }
复制代码

处理根据各种配置参数

为了实现无侵入切入应用中,Dubbo 目前使用的是通过 Spring 的预留的机制实现的。首先是通过 setApplicationContext() 将监听器注册进 Spring 容器;然后等待 SpringApplicationContextonApplicationEvent 监听 ContextRefreshedEvent 事件。

用一张图来描述就是

而代码层面的描述就是

那么我们来看看 ServiceConfigonApplicationEvent 方法。

public void onApplicationEvent(ContextRefreshedEvent event) {
    if (isDelay() && !isExported() && !isUnexported()) {
        // log info
        export();
    }
}
复制代码

isDelay() 指的是是否延迟导出服务;isExported 指的是是否已经导出。

当通过上面的检测后,就进入 export 方法

public synchronized void export() {
    // 获取 export 和 delay 配置
    if (provider != null) {
        if (export == null) {
            export = provider.getExport();
        }
        if (delay == null) {
            delay = provider.getDelay();
        }
    }
    if (export != null && !export) {
        return;
    }
    // delay > 0,延时导出服务
    if (delay != null && delay > 0) {
        delayExportExecutor.schedule(new Runnable() {
            @Override
            public void run() {
                doExport();
            }
        }, delay, TimeUnit.MILLISECONDS);
    } else {
        //立即导出
        doExport();
    }
}
复制代码

export 是一个 Boolean 值。可通过配置文件设置是否导入到远程中心。

<dubbo:provider export="false" />
复制代码

我们继续看 ServiceConfig#doExport 方法。

    protected synchronized void doExport() {
        if (unexported) {
            //throw Exception
        }
        if (exported) {
            return;
        }
        exported = true;
        if (interfaceName == null || interfaceName.length() == 0) {
            //throw Exception
        }
        checkDefault();
        //检查 provider
        if (provider != null) {
            if (application == null) {
                application = provider.getApplication();
            }
            if (module == null) {
                module = provider.getModule();
            }
            if (registries == null) {
                registries = provider.getRegistries();
            }
            if (monitor == null) {
                monitor = provider.getMonitor();
            }
            if (protocols == null) {
                protocols = provider.getProtocols();
            }
        }
        //检查 Dubbo module
        if (module != null) {
            if (registries == null) {
                registries = module.getRegistries();
            }
            if (monitor == null) {
                monitor = module.getMonitor();
            }
        }
        //检查 application
        if (application != null) {
            if (registries == null) {
                registries = application.getRegistries();
            }
            if (monitor == null) {
                monitor = application.getMonitor();
            }
        }
        //
        if (ref instanceof GenericService) {
            interfaceClass = GenericService.class;
            if (StringUtils.isEmpty(generic)) {
                generic = Boolean.TRUE.toString();
            }
        } else {
            try {
                interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                        .getContextClassLoader());
            } catch (ClassNotFoundException e) {
                //throw Exception
            }
            checkInterfaceAndMethods(interfaceClass, methods);
            checkRef();
            generic = Boolean.FALSE.toString();
        }
        //检查 Dubbo local
        if (local != null) {
            if ("true".equals(local)) {
                local = interfaceName + "Local";
            }
            Class<?> localClass;
            try {
                localClass = ClassHelper.forNameWithThreadContextClassLoader(local);
            } catch (ClassNotFoundException e) {
                //throw Exception
            }
            if (!interfaceClass.isAssignableFrom(localClass)) {
                //throw Exception
            }
        }
        //检查 Dubbo 存根
        if (stub != null) {
            if ("true".equals(stub)) {
                stub = interfaceName + "Stub";
            }
            Class<?> stubClass;
            try {
                stubClass = ClassHelper.forNameWithThreadContextClassLoader(stub);
            } catch (ClassNotFoundException e) {
                //throw Exception
            }
            if (!interfaceClass.isAssignableFrom(stubClass)) {
                //throw Exception
            }
        }
        checkApplication();//检查应用信息
        checkRegistry();//检查注册中心
        checkProtocol();//检查协议
        appendProperties(this);//添加配置参数
        checkStub(interfaceClass);//检查存根
        checkMock(interfaceClass);//检查接口
        if (path == null || path.length() == 0) {
            path = interfaceName;
        }
        doExportUrls();//开始导出
        // ProviderModel 表示服务提供者模型,此对象中存储了与服务提供者相关的信息。
        // 比如服务的配置信息,服务实例等。每个被导出的服务对应一个 ProviderModel。
        // ApplicationModel 持有所有的 ProviderModel。
        ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), this, ref);
        ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
    }
复制代码

虽然代码很长,但是多数是一些配置校验和完善。下面介绍重点方法是 doExportUrls

doExportUrls 的大致流程

  • doExportUrls
    • loadRegistries
    • doExportUrlsFor1Protocol
private void doExportUrls() {
    List<URL> registryURLs = loadRegistries(true);
    for (ProtocolConfig protocolConfig : protocols) {
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}
复制代码

loadRegistries 方法

loadRegistries 加载注册中心链接,然后再遍历 ProtocolConfig 集合导出每个服务。

protected List<URL> loadRegistries(boolean provider) {
    //检查配置中心类
    checkRegistry();
    List<URL> registryList = new ArrayList<URL>();
    if (registries != null && !registries.isEmpty()) {
        for (RegistryConfig config : registries) {
            String address = config.getAddress();
            if (address == null || address.length() == 0) {
                //如果为空,就设为 0.0.0.0
                address = Constants.ANYHOST_VALUE;
            }
            //允许从系统变量中加载注册中心
            String sysaddress = System.getProperty("dubbo.registry.address");
            if (sysaddress != null && sysaddress.length() > 0) {
                address = sysaddress;
            }
            if (address.length() > 0 && !RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
                Map<String, String> map = new HashMap<String, String>();
                //添加 application 信息
                appendParameters(map, application);
                //添加 config 信息
                appendParameters(map, config );
                //设置 path / pid / procotol 等信息到 map 中
                map.put("path", RegistryService.class.getName());
                map.put("dubbo", Version.getProtocolVersion());
                map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));
                if (ConfigUtils.getPid() > 0) {
                    map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));
                }
                if (!map.containsKey("protocol")) {
                    if (ExtensionLoader.getExtensionLoader(RegistryFactory.class).hasExtension("remote")) {
                        map.put("protocol", "remote");
                    } else {
                        map.put("protocol", "dubbo");
                    }
                }
                // 解析得到 URL 列表,address 可能包含多个注册中心 ip,
                // 因此解析得到的是一个 URL 列表
                List<URL> urls = UrlUtils.parseURLs(address, map);
                for (URL url : urls) {
                    //设置头部信息 registry 
                    url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
                    // 通过判断条件,决定是否添加 url 到 registryList 中,条件如下:
                    // (服务提供者 && register = true 或 null) 
                    //    || (非服务提供者 && subscribe = true 或 null)
                    url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
                    if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
                            || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
                        registryList.add(url);
                    }
                }
            }
        }
    }
    return registryList;
}
复制代码

doExportUrlsFor1Protocol 方法

doExportUrlsFor1Protocol 方法很长,所以需要拆分进行讲解。首先是第一部分的内容 - 拼装URL;第二部分的内容是,服务暴露。先看拼装 URL 的代码。

// 添加 side、版本、时间戳以及进程号等信息到 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()));
}
//通过反射讲对象的字段添加进 map
appendParameters(map, application);
appendParameters(map, module);
appendParameters(map, provider, Constants.DEFAULT_KEY);
appendParameters(map, protocolConfig);
appendParameters(map, this);
//判断方法是否为空,不为空进行循环
if (methods != null && !methods.isEmpty()) {
    for (MethodConfig method : methods) {
        //添加方法名
        appendParameters(map, method, method.getName());
        String retryKey = method.getName() + ".retry";
        if (map.containsKey(retryKey)) {
            String retryValue = map.remove(retryKey);
            if ("false".equals(retryValue)) {
                map.put(method.getName() + ".retries", "0");
            }
        }
        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) {
                    Method[] methods = interfaceClass.getMethods();
                    // visit all methods
                    if (methods != null && methods.length > 0) {
                        for (int i = 0; i < methods.length; i++) {
                            String methodName = methods[i].getName();
                            // target the method, and get its signature
                            if (methodName.equals(method.getName())) {
                                Class<?>[] argtypes = methods[i].getParameterTypes();
                                // one callback in the method
                                if (argument.getIndex() != -1) {
                                    if (argtypes[argument.getIndex()].getName().equals(argument.getType())) {
                                        appendParameters(map, argument, method.getName() + "." + argument.getIndex());
                                    } else {
                                        throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
                                    }
                                } else {
                                    // multiple callbacks in the method
                                    for (int j = 0; j < argtypes.length; j++) {
                                        Class<?> argclazz = argtypes[j];
                                        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) {
                    appendParameters(map, argument, method.getName() + "." + argument.getIndex());
                } else {
                    //throw Exception
                }

            }
        }
    } // end of methods for
}
复制代码

然后是服务导出的过程,这个过程具体包含两个方面

  1. 本地暴露
  2. 服务注册

所谓的本地暴露指的是每个服务默认都会在本地暴露;在引用服务的时候,默认优先引用本地服务;如果希望引用远程服务可以使用一下配置强制引用远程服务。;所谓的服务注册就是将服务信息注册到注册中心,供其他服务发现和调用

下面让我们开始进入代码讲解。首先是一些元素的判断

if (ProtocolUtils.isGeneric(generic)) {
    map.put(Constants.GENERIC_KEY, generic);
    map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
} else {
    //获取版本信息
    String revision = Version.getVersion(interfaceClass, version);
    if (revision != null && revision.length() > 0) {
        map.put("revision", revision);
    }
    //获取所有方法
    String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
    if (methods.length == 0) {
        logger.warn("NO method found in service interface " + interfaceClass.getName());
        map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
    } else {
        map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
    }
}
if (!ConfigUtils.isEmpty(token)) {
    if (ConfigUtils.isDefault(token)) {
        map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
    } else {
        map.put(Constants.TOKEN_KEY, token);
    }
}
//自定义配置:如果配置的是本地协议,则对应的信息设为 false
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();
}
//获取 host 地址
String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
//获取端口
Integer port = this.findConfigedPorts(protocolConfig, name, map);
//组合 URL
URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);
//获取 ConfiguratorFactory,然后进行 url 的处理
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
        .hasExtension(url.getProtocol())) {
    url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
            .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}
复制代码

然后接下来我们开始服务导出的过程

//从 url 获取 scope 字段,这个是代表服务导出的范围
String scope = url.getParameter(Constants.SCOPE_KEY);
// 如果 scope!=NONE,则进行导出
if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {

    // export to local if the config is not remote (export to remote only when config is remote)
    // 如果 scope==REMOTE,则是仅仅只注册远程;如果 scope!=REMOTE,就本地导出
    if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
        exportLocal(url);
    }
    // 如果 scope!=LOCAL,说明远程也要导出
    if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
        //log info
        
        if (registryURLs != null && !registryURLs.isEmpty()) {
            //循环 url
            for (URL registryURL : registryURLs) {
                //添加 DYNAMIC_KEY 属性
                url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
                //启动监视器
                URL monitorUrl = loadMonitor(registryURL);
                if (monitorUrl != null) {
                    url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
                }
                if (logger.isInfoEnabled()) {
                    logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
                }

                // 针对 providers,可以自定义 proxy 去生成 invoker
                String proxy = url.getParameter(Constants.PROXY_KEY);
                if (StringUtils.isNotEmpty(proxy)) {
                    registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
                }
                // 添加参数,便于后续程序执行
                Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                // 将 invoker 以及 serviceConfig 封装成一个实体类
                DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
                // 根据 invoker 的地址获取对应的
                Exporter<?> exporter = protocol.export(wrapperInvoker);
                exporters.add(exporter);
            }
        } else {
            Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
            DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

            Exporter<?> exporter = protocol.export(wrapperInvoker);
            exporters.add(exporter);
        }
    }
}
复制代码

上面代码有一些判断非常关键,例如关于本地暴露的判断。例如说当 scope!=REMOTE 的时候,那么说明服务会进行本地暴露,所以会调用 exportLocal 方法。

private void exportLocal(URL url) {
    if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
       // 1.
        URL local = URL.valueOf(url.toFullString())
                .setProtocol(Constants.LOCAL_PROTOCOL)
                .setHost(LOCALHOST)
                .setPort(0);
        // 2. 
        StaticContext.getContext(Constants.SERVICE_IMPL_CLASS).put(url.getServiceKey(), getServiceClass(ref));
        // 3. 
        Exporter<?> exporter = protocol.export(
                proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
        // 4. 
        exporters.add(exporter);
        logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
    }
}
复制代码

上面的代码主要讲了以下步骤:

  1. url 的协议换成 jvm 这样可以获取 InjvmProtocol
  2. 封装上下文
  3. 通过 proxyFactory 获取对应的 invoker,通过 protocol 进行 export。这里的 protocol.export 实际上是等于 InjvmProtocol.export(这个在我上两篇文章有详细讲过)。
  4. 保存 exporter

接下来说服务注册的代码。

if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
    if (logger.isInfoEnabled()) {
        logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
    }
    // 1. 
    if (registryURLs != null && !registryURLs.isEmpty()) {
        for (URL registryURL : registryURLs) {
            url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
            // 2. 
            URL monitorUrl = loadMonitor(registryURL);
            if (monitorUrl != null) {
                url = url.addParameterAndEncoded(Constants.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
            // 3.
            String proxy = url.getParameter(Constants.PROXY_KEY);
            if (StringUtils.isNotEmpty(proxy)) {
                registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
            }
            // 4. 
            Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
            DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
            // 5. 
            Exporter<?> exporter = protocol.export(wrapperInvoker);
            exporters.add(exporter);
        }
    } else {
        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
        DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

        Exporter<?> exporter = protocol.export(wrapperInvoker);
        exporters.add(exporter);
    }
}
复制代码

当 scope!=SCOPE_LOCAL 的时候,就会进行服务注册。上面的代码主要执行了以下步骤:

  1. 判断 registryUrls 是否为空
  2. 加载 monitor,也就是 Dubbo 架构里面负责统计远程服务的各种数据
  3. 查看 url 是否有自定义 proxy 代理
  4. 根据 proxyFactory 获取对应的 invoker,并封装成 DelegateProviderMetaDataInvoker,同时在 registryURL 添加参数 Constants.EXPORT_KEY,值为 url.toFullString(),这个是因为在后续 RegistryProtocol 执行过程中需要判断根据什么协议进行导出的。
  5. 调用 protocol.export 方法进行服务导出。需要注意这里的 protocol 会获取的是 registryProtocol,因为当前的 url 的协议就是 registry

RegistryProtocol

上面已经讲完了大致上整个框架的导出了。那接下来就将服务注册的细节代码。导出服务的核心类是 RegistryProtocol

鉴于跳跃性避免过快,我这里还是唠叨几句。RegistryProtocol 是通过 ProtocolDubbo 生成的代理类 Protocol$Adaptiveexport 方法,然后根据 url 的协议 registry 找到了 RegistryProtocol(具体内容可以看我的文章Apache Dubbo 系列之注解 @Adaptive 实现原理)。生成 RegistryProtocol 的过程会对其进行注入,形成了调用链 ProtocolListenerWrapper -> QosProtocolWrapper -> ProtocolFilterWrapper -> RegistryProtocol -> DubboProtocol(生成调用链条的原理可以看我的文章Apache Dubbo 系列之 Wrapper 解析)。我们从调用链条可以看出,RegistryProtocol 实际上可以认为并不是一个真正的协议,他是这些实际的协议(dubbo \ rmi)包装者,然后生成这个调用链是因为 dubbo \ rmi 等协议通过注入的方式 injection() 方法设置进去的,这点非常重要!

目前我觉得过渡还 OK!接下来我们进入 RegistryProtocol 的代码探索吧~ 我们看它的 export 方法。

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    //1. 
    final ExporterChangeableWrapper<T> exporter = (originInvoker);
	//2.
    URL registryUrl = getRegistryUrl(originInvoker);

    //3. 
    final Registry registry = getRegistry(originInvoker);
    final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);

    //4. 
    boolean register = registeredProviderUrl.getParameter("register", true);
    ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);
    if (register) {
        register(registryUrl, registeredProviderUrl);
        ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
    }

    // 5. 
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
    overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
    // 6. 
    return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
}
复制代码

上面代码主要遵循以下步骤:

  1. 调用 doLocalExport 方法。这个方法会像我上面说的那样,RegistryProtocol 包装了具体的 Protocol,这时候会根据 Constants.EXPORT_KEY 找到服务的可访问地址,然后调用对应 Protocol 的 export 方法。下面我会以 DubboProtocol 为例子讲解。
  2. 获取注册中心的具体地址
  3. 根据 registryUrl 获取真正处理的注册中心处理类;获取提供的服务的可访问地址,并进行地址的元素清洗,避免暴露不必要的参数
  4. 通过 registryUrl 的属性判断是否延迟导出
  5. 订阅服务
  6. 封装返回的是一个全新的 exporter

上面步骤主要的作用就是为了调用对应的 Protocol 以及进行信息注册。而开启服务的是因为调用了 DubboProtocol。我们来看看 DubboProtocolexport 方法,我简单讲一下!

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

    // 1. 
    String key = serviceKey(url);
    DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
    exporterMap.put(key, exporter);

    //export an stub service for dispatching event
    // 2. 
    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);
        }
    }
	// 3. 
    openServer(url);
    optimizeSerialization(url);
    return exporter;
}
复制代码

上面代码主要做了以下步骤:

  1. 获取主要因素,拼装成可访问地址
  2. 导出用于分派事件的存根服务
  3. 开启新的 socket,用于接收其他服务的调用

结语

目前我们已经讲解完了 Dubbo 的服务导出的过程。看完源码最主要的还是要反思为什么代码要这么写以及这么写对整个框架的实现有什么好的优势,同时最后又对理解的一个反馈和输出。

完结!

下期见!

分类:
后端
标签:
分类:
后端
标签: