Dubbo源码浅析(三)—Dubbo服务的暴露过程

138 阅读7分钟

一、服务暴露概览

1.1 服务暴露实例

当我们在日常开发中需要暴露出一个服务提供给别人使用时,一般有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">

    <!-- provider's application name, used for tracing dependency relationship -->
    <!-- 我们的应用名称-->
    <dubbo:application name="demo-provider"/>

    <!-- use multicast registry center to export service -->
    <!-- 注册中心地址-->
    <dubbo:registry address="multicast://224.5.6.7:1234"/>

    <!-- use dubbo protocol to export service on port 20880 -->
    <!-- 使用dubbo协议,端口号为20880-->
    <dubbo:protocol name="dubbo" port="20880"/>

    <!-- service implementation, as same as regular local bean -->
    <!-- 加载我们提供的bean-->
    <bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl"/>

    <!-- declare the service interface to be exported -->
    <!-- 要暴露的接口名-->
    <dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService"/>

</beans>

这样的话,我们已经将服务给暴露出去了,后续有其他的应用想要远程调用我们的服务,只需要填写相应的配置,然后即可调用。

1.2 服务暴露流程

服务暴露主要有三个流程:

  1. 检查参数,组装 URL。
  2. 暴露服务,包括本地或远程注册中心。
  3. 如果是远程的话,向注册中心注册服务,用于服务发现。

1.2.1 组装URL

首先来看一个dubbo协议下的url:

 dubbo://26.26.26.1:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider

主要由以下几部分组成:

  • protocol:使用什么协议,一般有dubbo、http等
  • IP:Port:IP和端口号,用于暴露我们的位置
  • path:接口的全名称,一般来说在一个dubbo集群中,该名称唯一
  • parameters:接口参数

通过以上的信息,我们就可以把服务注册到注册中心或者远程调用其他服务了。

1.2.2 暴露服务

暴露服务主要上将我们提供服务的bean层层包装,然后根据需要暴露到本地或者远程。

本地跟远程的区别在于,有时候我们选择暴露出去的服务也会在应用内部自己使用,如果都暴露到远程注册中心,那么使用的时候还需要再网路连接获取服务,效率低下。因此如果我们有时候会将服务暴露到本地,本地应用在获取服务时会先看是否已经存在,存在的话则直接使用,不需要调用远程服务了。

1.2.3 注册服务

这一步的作用主要是将上一步骤中层层包装好的产物以及我们应用的相关配置注册到注册中心中,当其他应用调取服务时,会先去注册中心获取我们的相关配置,然后远程服务根据配置调用我们所提供的服务时,就会直接使用包装好的服务。

二、服务暴露源码解析

2.1 服务暴露的入口

在我们定义号的xml注解被spring加载时,如果有标签则会进入到ServiceBean类中进行解析,该类实现了ApplicationListener接口,那么在spring IOC 容器刷新完成后调用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()又会调用父类的该方法,接着进入doExport()方法,该方法进行了一系列的检查后,进入到doExportUrls()

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

可以看到 Dubbo 支持多注册中心,并且支持多个协议,一个服务如果有多个协议那么就都需要暴露,比如同时支持 dubbo 协议和 hessian 协议,那么需要将这个服务用两种协议分别向多个注册中心(如果有多个的话)暴露注册。

2.2 服务暴露

doExportUrlsFor1ProtocolI()比较长,核心代码如下:

//首先根据各种参数拼装成URL
URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);

String scope = url.getParameter(Constants.SCOPE_KEY);
if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {

    // 如果要暴露在本地的话
    if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
        exportLocal(url);
    }
    // 否则暴露远程服务
    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) {

                //首先通过构造出invoker
                Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
                        //构造exporter
                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);
        }
    }
}

2.2.1 暴露到本地

暴露到本地通过exportLocal()方法实现:

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));
        //首先该方法内部会先构建出invoker,然后再将invoker包装成exporter
        Exporter<?> exporter = protocol.export(
                proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
        exporters.add(exporter);
        logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
    }
}

暴露到本地首先会通过url构建成invoker,然后再将invoker包装成exporter。invoker就是通过java ssist构建出的一种代理类,用于屏蔽内部调用细节,对于调用者来说,无需关心内部是如何实现以及由谁来实现的,只需要拿到invoker执行其中的代理方法即可,而具体由什么服务实现,则可能是本地服务,也可能是背后的远程集群服务。

先看一下exporter:

可以看到exporter中主要包含了一个invoker,而invoker中持有一个被代理对象。

通过java ssist生成代理类的细节就不再描述,主要来看一下生产exporter过程。

因为是暴露在本地,所以是使用的injvm协议:

InjvmExporter(Invoker<T> invoker, String key, Map<String, Exporter<?>> exporterMap) {
    super(invoker);
    this.key = key;
    this.exporterMap = exporterMap;
    exporterMap.put(key, this);
}

可以看到expoter主要就是对Invoker做了进一步封装,然后将本身的exporter暴露在map中完成本地的注册。

2.2.2 暴露到远程

与暴露到本地相同,暴露到远程服务也会先创建一个invoker对象,不同的是在构建URL后,服务暴露时首先是register协议,代表该操作需要现将服务暴露到注册中心,然后是根据配置的相关信息再将该服务注册为不同的协议,例如dubbo。

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo://192.168.1.17:20880/com.alibaba.dubbo.demo.DemoService

所以此时会首先进入到RegistryProtocol的实现类中:

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    //根据协议获取exporter,例如刚刚就是dubbo
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);

    //获取注册中心url
    URL registryUrl = getRegistryUrl(originInvoker);

    //获取对应的实现类,例如这里就是ZookeeperRegistry
    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) {
        //如果需要注册的话,那么向注册中心注册Provider url
        register(registryUrl, registeredProviderUrl);
        ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
    }

    //Ensure that a new exporter instance is returned every time export
    return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
}

可以看到首先会将需要暴露的服务根据协议获取对应的exporter(该过程会将对应的服务加载到本地),然后后去对应的注册中心,如果需要注册的话那么将注册中心注册Provider的url,在完成这一系列操作后,后续调用者就可以根据注册中心的Provider url找到我们服务,然后本地服务找到对应的exporter中的invoker执行其中的方法。

那么我们来看一下Dubbo协议是如何注册的:

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) {
                //再包装一次invoker
                final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
                //根据协议注册获取相应的exporter,此时为dubbo协议
                exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
                bounds.put(key, exporter);
            }
        }
    }
    return exporter;
}

这里就是再包装一次invoker之后通过dubbo协议获取其exporter:

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

    String key = serviceKey(url);
    //将invoker包装成exporter
    DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
    //注册到本地
    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);
        }
    }

    //打开监听器,此时一般是netty server
    openServer(url);
    optimizeSerialization(url);
    return exporter;
}

2.3 总结

根据我们上面的陈述,可以发现服务暴露首先会构建出一个url,然后根据对应的方式暴露到本地或暴露到远程服务。暴露到本地的话就是首先根据java ssist构建出代理类,然后将其再封装成一个exporter注册到本地。而暴露到远程的话,则会首先根据要暴露的协议创建对应的exporter,在创建时会将本身的exporter注册到本地,然后打开对应netty server。再获取注册中心的相关配置,将provider的url暴露到注册中心中,以提供给调用者从注册中心获取提供者的相关配置。

作者:韩国凯