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

抓到 Dubbo 异步调用的小 Bug,再送你一个贡献开源代码的机会 问题复现 今天发现一个问题 有一个 dubbo 接口返回类型是 boolean。把接口从同步改成异步 server 端返回 true,消费端却返回 false。把 boolean 改成 Boolean 就能正常返回结果。有碰到过这个问题吗? 阅读详情

前言

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

其实我的本意起这个名字是吸引更多地人进来,无论是那些第一次研究 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 容器;然后等待 Spring 的 ApplicationContext 的 onApplicationEvent 监听 ContextRefreshedEvent 事件。

用一张图来描述就是

而代码层面的描述就是

那么我们来看看 ServiceConfig 的 onApplicationEvent 方法。

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 是通过 Protocol 在 Dubbo 生成的代理类 Protocol$Adaptive 的export 方法,然后根据 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。我们来看看 DubboProtocol 的 export 方法,我简单讲一下!

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 的服务导出的过程。看完源码最主要的还是要反思为什么代码要这么写以及这么写对整个框架的实现有什么好的优势,同时最后又对理解的一个反馈和输出。

完结!

下期见!

Apache Dubbo 架构与实战 Dubbo、SOA、架构演变、架构、处理流程、开发实战、配置方式、XML配置Dubbo、dubbo-admin、作用、控制台安装、使用、Dubbo配置项说明、dubbo:application、dubbo:registry、dubbo:protocol、dubbo:service、dubbo:reference、 dubbo:method、dubbo:service和dubbo:reference详解 阅读详情

相关推荐

从ResNet到EfficientNet:特征金字塔网络(FPN)在不同骨干网中的适配指南

本文深入探讨了特征金字塔网络(FPN)与ResNet、VGG、EfficientNet等主流骨干网络的适配策略。文章详细解析了FPN的核心原理,并针对不同骨干网络的特征提取特性,提供了通道对齐、参数量估算及迁移学习的系统化指南,旨在帮助开发者在目标检测等视觉任务中构建高性能、高效率的模型。

weixin_29047919的博客 221

com.alibaba.dubbo.remoting.RemotingException: Not found exported service

文章目录问题信息问题描述错误信息排查过程问题原因 问题信息 问题描述 服务调用方同学反馈,XX应用调用我们对外提供的库存查询服务,找不到服务 ··· 错误信息 调用方输出的错误日志信息 [2019-01-24 16:46:43,985] INFO -:-:-:- [main] com.wwwarehouse.commons.dubbo.DubboConsumerAuthFilter.invoke(...

随风飘流的博客 2万+

【科研绘图】【数据拟合曲线】:附Origin详细画图流程

拟合曲线图

weixin_50236590的博客 4万+

Dubbo3 配置rest和dubbo两种协议发布应用服务

REST 是在 2000 年他的博士论文“架构风格以及基于网络的软件架构设计” 中提出来的一个概念。REST 是REStateTransfer 的缩写,翻译过来就是 “表现层状态转化”。REST 就是 Roy 在这篇论文中提出的面向互联网的软件所应当具备的架构风格。按照 REpresentational State Transfer 的字面意思,可以把应用看成是一个虚拟的状态机,软件提供的不是服务而是一系列的资源,对这些资源的访问通过统一的操作来访问,而返回的结果代表了资源状态的一次跃迁。

yimint的博客 3166

三相电机控制中的端电压、相电压与线电压波形解析

本文深入解析了三相电机控制中端电压、相电压与线电压的核心概念与波形差异。通过对比方波控制、SPWM和SVPWM三种主流控制策略下的实测波形特征,揭示了不同控制逻辑对电压波形(如梯形波、正弦波、马鞍波)的影响。文章重点阐述了SVPWM控制如何通过生成马鞍波形端电压来提高直流母线电压利用率,并提供了实用的波形测量与调试指南,帮助工程师深入理解电机控制原理并进行有效调试。

qsc901234567的博客 1400

VS2019+CAXACAD2023二次开发教程(一、环境搭建)

CAXACAD2023的二次开发相关文件和库都在的文件夹下。CAXACAD2023的默认开发环境是VS2019,如果是用VS2019的环境话,可以直接安装这个插件,安装好后就可以一键新建的项目,新建的项目会自动帮我们配置好所有的环境,并且生成必要的代码和初始的框架。(本来想用VS2022的,但配置环境太麻烦,用VS2022,增加一个环境变量是可以运行自带的例程的,但新建项目要配置的东西和环境太多,就放弃折腾了)

weixin_42968757的博客 4621

dubbo源码分析第二十八篇一dubbo3.0拥抱云原生

cloud-native 简介: 每一种概念,都需要一种技术支撑 k8s+docker做为底层基础设施 Jenkins + git + 自建devops 等等作为服务发布部署工具 服务治理: istio ,dubbo3.0等实现治理: 包含服务发现,负载,流量控制等 文章目录应用级注册中心应用级注册中心demo消费者配置提供者配置k8s-apiserver作为注册中心(dns方案忽略)消费者属性配置消费者配置消费者启动配置提供者属性配置提供者配置提供者启动配置注册内容mapping映射信息metadat.

小麒麟的博客 1809

【保姆级教程】基于UNet++&MobileNetv3的语义分割模型从训练到RK3588部署:让边缘AI落地效率提升300%

通过SMP库一键构建encoder_name="mobilenet_v2", # 选择MobileNetv3作为骨干encoder_weights="imagenet", # 加载预训练权重加速收敛in_channels=3, # 输入图像通道数(RGB)classes=29, # Cityscapes数据集类别数activation=None # 输出无激活,便于后续损失计算本教程从“模型设计-训练-转换-边缘部署”全链路拆解了。

FJN110的博客 256

Dubbo笔记 ㉗ : 服务自省-提供者

一、前言 Dubbo服务自省,以及 2.7.5 的优势分析。 要研究 : Dubbo2.7.5 的 应用粒度服务注册,即 服务自省。如何实现,优势等 https://mp.weixin.qq.com/s/V5S_vO6Mgtq9-v2ed9eDrw https://blog.youkuaiyun.com/weixin_45583158/article/details/103900690 https://www.jianshu.com/p/8f72c14f6a79 ...

猫吻鱼的博客 5027

013 PID参数整定:Ziegler-Nichols法

几年前在调试一台伺服压机,客户要求位置精度±0.01mm,响应时间小于50ms。我按照教科书上的经验值设了P=1.5,I=0.02,D=0.01,结果电机一启动就开始尖叫,位置超调直接干到0.3mm,然后开始震荡,像极了喝醉的舞者。现场工程师看我的眼神,就像在看一个刚毕业的实习生。那次之后我明白了一个道理:PID参数不是靠猜的,也不是靠经验公式硬套的。你需要一个系统的方法,而Ziegler-Nichols法就是那个让你从“瞎调”走向“有章可循”的起点。

m0_50546716的博客 101

[网络安全提高篇] 一二〇.恶意软件动态分析经典沙箱Cape批量提取动态API特征

这是作者网络安全自学教程系列,主要是关于安全工具和实践操作的在线笔记,特分享出来与博友们学习,希望您喜欢,一起进步。前文详细介绍动态分析沙箱Cape的安装过程,其是一个开源的自动恶意软件分析系统,通过自动运行和分析恶意软件,全面分析和提取恶意软件的关键特征。然而,当样本数量增加时,单个样本分析会降低效率。这篇文章将讲解如何实现Cape沙箱的批量分析,通过调用Python脚本文件来实施批量处理,并将分析结果存储在指定位置,最后补充submit.py的参数及Python调用方式。基础性文章,希望对您有帮助!

杨秀璋的专栏 4121

boolean boolean解析不出来_Apache Dubbo 系列之不保证你能看懂的服务暴露过程

前言如果看了这个标题你还进来,说明你是真爱粉或者单纯想进行看看我写的多烂的。那么恭喜你,有一半的几率会是你所期望的那样,我写的真的很烂。还有另一半的几率是但是也有可能写得只是很一般。其实我的本意起这个名字是吸引更多地人进来,无论是那些第一次研究 Dubbo 的同行 or 已经麻溜玩转 Dubbo 的大佬,给我的文章提供更多的意见或者建议。ok 废话不多说,开始切入主题!核心描述首先,我想说明的是服...

weixin_30278943的博客 340

SAP 经营范围(Operating Concern) 和业务范围 (Business Area) 在核算架构或是管理架构中的区别和联系分别是什么 请给具体的示例

摘要:SAP系统中,业务范围(Business Area)和经营范围(Operating Concern)在核算架构中具有不同功能。业务范围是FI/CO共用维度,用于跨公司码的业务线核算(如电子产品、机械设备),支持内部管理和简单对外报表。经营范围则是CO-PA专属维度,聚焦获利能力分析,可细化到产品+客户+区域组合(如分析手机在华东区域的获利情况)。两者存在层级关联:经营范围包含业务范围作为基础维度,业务范围数据会传递至经营范围进行深度分析。实际应用中需先配置业务范围,再设置经营范围并关联相关维度,共同支

金牌架构师 95

Dubbo服务暴露的过程源码分析

代码基于2.7.4-release分支 服务注册(暴露)过程 1、首先,ServiceConfig类拿到对外提供服务的实际类ref(本例中的DemoServiceImpl),然后通过ProxyFactory接口实现类中的getInvoker方法使ref生成一个AbstractProxyInvoker实例,到这一步完成具体服务到Invoker的转化 Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceCl.

weixin_35711816的博客 415

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

深入浅出中间件之Apache Dubbo

深入浅出RPC框架dubbo

鲜衣怒马楼兰月 3706

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

dubbo原理

dubbo官网:Apache Dubbo 主要包括五个节点:Provider、Consumer、Container、Register、Monitordubbo工作过程:这里的Invoker是Provider的一个可调用Service的抽象,Invoker封装了Provider地址及Service接口信息。Directory代表多个Invoker,可以把它看成List,但与List不同的是,它的值可能是动态变化的,比如注册中心推送变更。Cluster将Directory中的多个Invoker伪装成一个Invo

sumengnan的博客 6140

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

Dubbo服务暴露过程

概览 dubbo暴露服务有两种情况,一种是设置了延迟暴露(比如delay=”5000”),另外一种是没有设置延迟暴露或者延迟设置为-1(delay=”-1”): 设置了延迟暴露,dubbo在Spring实例化bean(initializeBean)的时候会对实现了InitializingBean的类进行回调,回调方法是afterProp...

weixin_33980459的博客 769

深入解析Dubbo服务暴露机制:从原理到实战避坑指南

在微服务架构中,服务暴露是服务提供者将自身能力发布到网络、供消费者发现和调用的核心过程。其原理涉及本地JVM内的服务包装、协议服务器启动、服务元数据组装与注册中心同步等多个精密环节。理解这一机制的技术价值在于,它能保障服务调用的高可用与高性能,是构建稳定分布式系统的基石。典型应用场景包括服务注册发现、负载均衡、流量调度和优雅上下线等。本文将结合线上实战经验,重点剖析Dubbo服务暴露的两阶段流程(本地暴露与远程暴露),并深入探讨配置参数如delay、weight、warmup的生效时机与常见“陷阱”,同时解

weixin_30932215的博客 274

Apache Dubbo 快速使用入门、管理控制台 dubbo-admin

3.0 在通信协议、服务发现、部署架构、服务治理上都对云原生基础设施进行了全面适配, 提供了Triple、应用级服务发现、Dubbo Mesh等核心特性。是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案,涵盖 Java、Golang 等多种语言 SDK 实现。类似 Spring Cloud。4、如下图所示,注册中心协调提供方与消费方的地址,提供者启动之后向注册中心注册自身地址,消费方通过拉取或订阅注册中心特定节点,动态的感知提供方地址列表的变化。

蚩尤后裔-汪茂雄 2581

【Dubbo01】消息队列与微服务之Dubbo介绍

Apache Dubbo 是一款 RPC 服务开发框架,用于解决微服务架构下的服务治理与通信问题,官方提供了 Java、Golang 等多语言 SDK 实 现。使用 Dubbo 开发的微服务原生具备相互之间的远程地址发现与通信能力, 利用 Dubbo 提供的丰富服务治理特性,可以实现诸如服务发现、负载均衡、流量调度等服务治理诉求。Dubbo 被设计为高度可扩展,用户可以方便的实现流量拦截、选址的各种定制逻辑。

运维&陈同学的博客 1765

【Dubbo】深入理解Apache Dubbo(一):带你走近高性能RPC通信框架

1. 简介 从本篇开始,笔者将会带你进入Apache顶级项目——Dubbo的学习。本篇侧重介绍Dubbo的来龙去脉,会从架构的演进过程说明Dubbo的重要性和意义。 2. 应用架构的演进过程 2.1 单体应用 1. JEE时期 特征 优点 缺点 JEE是早期提供了企业级应用开发环境的工具,整体上分为展示层、业务逻辑层和数据存储层。各层的组件都被聚合在一起运行在统一的服务器上(如To...

少侠露飞的学习笔记 3万+

Apache Dubbo - 服务引用(四)

1. 简介 上一篇文章详细分析了服务导出的过程,本篇文章我们趁热打铁,继续分析服务引用过程。在 Dubbo 中,我们可以通过两种方式引用远程服务。第一种是使用服务直连的方式引用服务,第二种方式是基于注册中心进行引用。服务直连的方式仅适合在调试或测试服务的场景下使用,不适合在线上环境使用。因此,本文我将重点分析通过注册中心引用服务的过程。从注册中心中获取服务配置只是服务引用过程中的一环,除此之外,...

您好!请捕获您的异常。 1623

Dubbo 服务引用的过程和实现细节

在 Dubbo 中,我们可以通过两种方式引用远程服务。第一种是使用服务直连的方式引用服务,第二种方式是基于注册中心进行引用。服务直连的方式仅适合在调试或测试服务的场景下使用,不适合在线上环境使用。因此,本文我将重点分析通过注册中心引用服务的过程。从注册中心中获取服务配置只是服务引用过程中的一环,除此之外,服务消费者还需要经历 Invoker 创建、代理类创建等步骤。

肖哥弹架构博客 712

dubbo:深入理解Apache Dubbo与实战

服务提供者启动时,会广播自己的地址。ZooKeeper注册中心采用的是“事件通知” + “客户端拉取”的方式,客户端在第一次连接上注册中心时,会获取对应目录下全量的数据。注册中心之间保持TCP长连接,后续每个节点有任何数据变化的时候,注册中心会根据watcher的回调主动通知客户端(事件通知),客户端接到通知后,会把对应节点下的全量数据都拉取过。ZooKeeper是官方推荐的注册中心,在生产环境中有过实际使用,具体的实现在Dubbo 源码的dubbo-registry-zookeeper模块中。

weixin_43675252的博客 1548

Pinpoint Apache Dubbo 插件测试应用(dubbo-plugin-testweb)实战:构建、运行与调用链验证

本指南围绕 Pinpoint 仓库中的 `agent-module/agent-testweb/dubbo-plugin-testweb` 测试应用展开,介绍如何基于该模块快速搭建一套 Apache Dubbo 的 Provider/Consumer 调用链,并验证 Pinpoint 的 [apache-dubbo 插件](https://link.gitcode.com/i/8bc95ee6da

gitblog_01063的博客 1185
上一篇: 一个字就是“爽“ 经历了阿里Java岗三轮技术面和一轮Hr面!也不过如此嘛
下一篇: 阿里P8架构师复盘2020最全实战文档:Spring家桶+Mysql+Redis+MyBatis+设计模式
java架构师小芷
博客等级 码龄6年 216粉丝 519原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值