Dubbo源码阅读——服务暴露


Dubbo框架使用方式一般是分为两类应用(application):服务提供者(我把它成为p端应用)和服务消费者(c端应用),他们的联系可以是直连,也可以是通过注册中心做服务注册发现。每个application可以包含多支服务或多个引用。
上一篇文章讲了应用在启动时,dubbo如何集成spring对配置标签进行解析,本文继续研究启动应用的过程中,标签解析完后,dubbo如何将服务进行暴露,也叫服务导出。
dubbo服务暴露分为远程服务暴露和本地服务暴露,大多应用场景下,我们都是进行远程服务暴露。

一、服务暴露的整体流程

首先,我们先对服务暴露的过程做一个整体的认识。
dubbo p端应用启动,spring解析配置文件,将dubbo标签转化成xxxconfig实例放在spring容器里。然后触发ServiceBean的事件监听,也就是onApplicationEvent方法,这里就是服务暴露过程的入口。
后续服务暴露逻辑大体分为三个部分:

  1. 第一部分是前置工作,主要用于检查参数,组装 URL。
  2. 第二部分是导出服务,包含导出服务到本地 (JVM),和导出服务到远程两个过程。
  3. 第三部分是向注册中心注册服务,用于服务发现。

其中第二部分是服务暴露的核心内容,它以服务实际实现类的实例和用于组装参数的url为参数,调用ProxyFactorygetInvoker方法生成贯穿dubbo框架的一个重要概念 Invoker。然后以Invoker为参数,调用protocol的export方法,生成另一个重要概念Exporter。第三部分开始解析Exporter转化的URL的,进行服务暴露到注册中心。dubbo服务支持多协议多注册中心暴露。
用官网的一张图来表示服务暴露的大体过程:
在这里插入图片描述
简单来说,整体流程就是首先将<dubbo:service>标签解析出来的各种属性组装成URL,这个过程包含许多检查,然后,通过URL和服务实现类实例ref生成Invoker,然后根据协议进行服务暴露,打开监听服务端,这个过程生成Exporter,如果URL带有registry,则还要进行服务注册,把服务元数据以URL的形式发到注册中心。

二、服务暴露详细过程源码阅读

1. 第一部分 前置工作

首先,ServiceBeanonApplicationEvent是服务暴露过程的入口:

(1) 检查

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

这个方法做了三个检查,是否有延迟导出 、 是否已导出 、是不是已被取消导出
,满足可导出条件后,调用export方法:

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

        if (delay != null && delay > 0) {
            delayExportExecutor.schedule(new Runnable() {
                @Override
                public void run() {
                    doExport();
                }
            }, delay, TimeUnit.MILLISECONDS);
        } else {
            doExport();
        }
    }

依然是检查一些配置,可以通过export属性设置为false,禁止服务暴露。另外如果设置了延迟导出时间,通过定时任务线程池延迟执行导出代码。然后看doExport方法

doExport
    protected synchronized void doExport() {
        /*......*/
        //检查interfaceName是否合法
        if (interfaceName == null || interfaceName.length() == 0) {
            throw new IllegalStateException("<dubbo:service interface=\"\" /> interface not allow null!");
        }
        // 检查 provider 是否为空,为空则新建一个,并通过系统变量为其初始化
        checkDefault();
        // 检查dubbo标签承载对象实例,从这里可以看到从这些承载对象取值的优先级
        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();
            }
        }
        if (module != null) {
            if (registries == null) {
                registries = module.getRegistries();
            }
            if (monitor == null) {
                monitor = module.getMonitor();
            }
        }
        if (application != null) {
            if (registries == null) {
                registries = application.getRegistries();
            }
            if (monitor == null) {
                monitor = application.getMonitor();
            }
        }
        // 检测 ref 是否为泛化服务类型
        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 new IllegalStateException(e.getMessage(), e);
            }
            checkInterfaceAndMethods(interfaceClass, methods);
            checkRef();
            generic = Boolean.FALSE.toString();
        }
        // 配置本地存根
        if (local != null) {
            if ("true".equals(local)) {
                local = interfaceName + "Local";
            }
            Class<?> localClass;
            try {
                localClass = ClassHelper.forNameWithThreadContextClassLoader(local);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            if (!interfaceClass.isAssignableFrom(localClass)) {
                throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName);
            }
        }
        if (stub != null) {
          	//与local代码基本一致,也用于配置存根,这里省略
        }
        // 检测各种对象是否为空,为空则新建,或者抛出异常
        checkApplication();
        checkRegistry();
        checkProtocol();
        // 遍历set前缀方法,并反射调用为对象注入属性值
        appendProperties(this);
        checkStubAndMock(interfaceClass);
        if (path == null || path.length() == 0) {
            path = interfaceName;
        }
        // 继续导出服务的逻辑
        doExportUrls();
        //每个被导出的服务对应一个 ProviderModel。
        ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), this, ref);
        // ApplicationModel 持有所有的 ProviderModel。
        ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
    }

以上代码过长,省略了一些代码,通过注释可以看清方法的轮廓。
doExport方法还是做一些更细层面的检查,比如检查一些属性或者承载对象为null应该如何处理,是从上层对象取默认值还是抛出异常。检测并处理泛化服务和普通服务类,操作本地存根等等。
值得注意的是appendProperties(this);方法,它是AbstractConfig的一个通用方法,用于给AbstractConfig或其子类对象注入属性值,注入方式还是熟悉的遍历setter方法反射调用。这里就不点开代码看了。
再继续看doExportUrls方法

(2) 多协议多配置中心导出服务

doExportUrls
    private void doExportUrls() {
    	// 加载注册中心链接 多个注册中心放到list里
        List<URL> registryURLs = loadRegistries(true);
        for (ProtocolConfig protocolConfig : protocols) {
        	//根据各个协议都导出服务
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

以上这几行代码使得dubbo支持多注册中心多协议暴露服务。其中loadRegistries方法用于加载注册中心链接。
然后就是组装url的过程。

(3) 组装url

doExportUrlsFor1Protocol(上)

dubbo用URL作为dubbo配置的载体,各模块之间传递标签属性都是通过URL。这里的URL不是jdk的URL,是dubbo框架自己实现的URL。

    private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
        String name = protocolConfig.getName();
        if (name == null || name.length() == 0) {
            name = "dubbo";
        }
		
        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中,用于后续组装url
        appendParameters(map, application);
        appendParameters(map, module);
        //读取全局配置信息,会自动添加前缀
        appendParameters(map, provider, Constants.DEFAULT_KEY);
        appendParameters(map, protocolConfig);
        appendParameters(map, this);
        
        /*......省略一大截*/
        // 从各个模块获取到需要的参数,然后组装url
        URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);

       // 后面是服务暴露相关代码,下节看
    }

doExportUrlsFor1Protocol方法篇幅较长,主要包含两件事情:组装url,服务暴露。上面贴出了组装url的关键代码,省略了大部分细节。后面看该方法的下半段,服务暴露逻辑,这部分相对更重要一些。

2. 第二部分 导出服务

(1) 服务导出的代码

服务暴露过程根据URL的不同又分为两种,有注册中心和无注册中心的导出,下面给出这两种URL的示例:

URL示例
有注册中心 registry://registry-host/org.apache.dubbo.registry.RegistryService?export=URL.encode(“dubbo://service-host/com.foo.FooService?version=1.0.0”)
无注册中心 dubbo://service-host/com.foo.FooService?version=1.0.0

其中有注册中心的URL通过RegistryProtocol来导出,无注册中心的URL默认通过DubboProtocol,会根据自适应扩展机制根据参数动态选择Protocol来export。

doExportUrlsFor1Protocol(下)

导出服务根据代码分为导出服务到本地和导出到远程两个过程。

    private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
    	//省略上一节组装url的代码
        if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                .hasExtension(url.getProtocol())) {
            url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                    .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
        }

        String scope = url.getParameter(Constants.SCOPE_KEY);
        // don't export when none is configured
        if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {

            // export to local if the config is not remote (export to remote only when config is remote)
            if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
            	//导出到本地
                exportLocal(url);
            }
            // export to remote if the config is not local (export to local only when config is 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()) {
                    for (URL registryURL : registryURLs) {
                        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());
                        }

                        // For providers, this is used to enable custom proxy to generate invoker
                        String proxy = url.getParameter(Constants.PROXY_KEY);
                        if (StringUtils.isNotEmpty(proxy)) {
                            registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
                        }
						//使用动态代理转换成Invoker,registryURL存储的是注册中心的地址
                        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                        // DelegateProviderMetaDataInvoker 用于持有 Invoker 和 ServiceConfig
                        DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
						//服务导出后像注册中心注册服务信息
                        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);
                }
            }
        }
        this.urls.add(url);
    }

以上代码是导出服务的关键代码,首先根据scope判断是否导出服务,导出到本地还是导出到远程。进行服务导出之前,需要先创建 Invoker。

(2) 创建invoker

Invoker是贯穿dubbo框架的一个重要模型。它通过动态代理创建。ProxyFactory有多个实现,主要的是JavasisstProxyFactoryJdkProxyFactory,这里也是通过自适应扩展机制动态加载Factory,默认是JavasisstProxyFactory,下面是JavasisstProxyFactory的getInvoker方法

getInvoker
    @Override
    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
        final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            protected Object doInvoke(T proxy, String methodName,
                                      Class<?>[] parameterTypes,
                                      Object[] arguments) throws Throwable {
                // 调用 Wrapper 的 invokeMethod 方法,invokeMethod 最终会调用目标方法
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }

JavassistProxyFactory 创建了一个继承自 AbstractProxyInvoker 类的匿名对象,并覆写了抽象方法 doInvoke。该方法有调用Wapper类的invokeMethod方法,最终调用目标方法。

(3) dubbo协议注册

export

无注册中心的服务暴露过程默认在DubboProtocol的export方法:

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

        // export service.
        // 根据服务分组、版本、接口和端口构造key
        String key = serviceKey(url);
        DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
        // 把exporter存储到单例DubboProtocol中
        exporterMap.put(key, exporter);

        //export an stub service for dispatching event
        Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
        Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
        if (isStubSupportEvent && !isCallbackservice) {
            String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
            if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
                if (logger.isWarnEnabled()) {
                    logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
                            "], has set stubproxy support event ,but no stub methods founded."));
                }
            } else {
                stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
            }
        }
		// 服务初次暴露会创建监听服务器
        openServer(url);
        optimizeSerialization(url);
        return exporter;
    }
    
    private void openServer(URL url) {
        // find server.
        String key = url.getAddress();
        //client can export a service which's only for server to invoke
        boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
        if (isServer) {
            ExchangeServer server = serverMap.get(key);
            if (server == null) {
                serverMap.put(key, createServer(url));
            } else {
                // server supports reset, use together with override
                server.reset(url);
            }
        }
    }

后面openServer方法,为每一个url创建一个监听服务器,放在serverMap里,创建过程默认使用netty实现。
至此,服务暴露完成,服务端应用持续监听导出服务的url。

3. 第三部分 服务注册

dubbo服务注册并不是必要的,通过服务直连也可以实现服务调用,但是通常会使用注册中心做服务注册和发现,dubbo默认注册中心使用广播方式,但是dubbo推荐使用zookeeper做注册中心,实际应用也大多使用zk,另外dubbo还支持redis做注册中心。
服务注册入口是RegistryProtocol 的 export 方法,这个在服务导出的后面部分有调用。

(1) 注册中心控制服务暴露

有注册中心的服务暴露过程在RegistryProtocol 的 export 方法。

export
    @Override
    public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        //export invoker
        //打开端口,把服务实例存放到map
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);

        URL registryUrl = getRegistryUrl(originInvoker);

        //registry provider
        //创建注册中心实例
        final Registry registry = getRegistry(originInvoker);
        final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);

        //to judge to delay publish whether or not
        boolean register = registeredProviderUrl.getParameter("register", true);

        ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);

        if (register) {
            register(registryUrl, registeredProviderUrl);
            ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
        }

        // Subscribe the override data
        // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call the same service. Because the subscribed is cached key with the name of the service, it causes the subscription information to cover.
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        // 监听服务端口接口下configurations节点,用于处理动态配置
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
        //Ensure that a new exporter instance is returned every time export
        // 可从注册中心移除的Exporter,里面实现了unexport方法
        return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
    }

然而,服务注册生成Export的过程远不止这么简单,以上export方法调用之前,框架会做拦截器初始化,Dubbo在加载Protocol扩展点的时候会自动初始ProtocolListenerWrapperProtocolFilterWrapper,在DubboProtocol暴露服务之前,会先调用这两个监听器和拦截器,它们会过滤provider端分组,把服务对象ref放到拦截器的末尾,为每一个filter生成一个exporter,然后依次串起来。

三、总结

dubbo服务暴露的过程属于dubbo服务端应用启动过程的一部分。暴露过程比较复杂,这里再梳理一遍。大体上还是前面说的三个部分。
首先spring容器完成配置解析触发监听事件,dubbo开始做一系列属性及承载对象的检查,然后从各个承载对象xxxConfig中拿到参数组装URL,再根据url和服务实现类实例ref生成Invoker,之后根据URL的类型(有注册中心和无注册中心)进行服务导出生成Exporter,有注册中心类型使用RegistryProtocol#export导出,无注册中心使用DubboProtocol#export导出;导出过程中使用netty为url开启服务监听,等待服务被调用。

发布了43 篇原创文章 · 获赞 17 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/103301463