一个Dubbo服务是如何发布的

今天这篇,我们来聊一聊一个Dubbo服务是如何发布的。
Dubbo支持多种容器,我们今天聊的,是基于Spring的Dubbo服务发布过程。

如何发布一个Dubbo服务

在看源码之前,我们不妨自己想一想,如何发布一个Dubbo服务。

大家都应该有过Dubbo的使用经验。我们的Dubbo服务信息一般都是维护在如Zookeeper这样的注册中心的(不考虑injvm这种本机调用)。所以,发布一个Dubbo服务,应当要有注册中心的连接,服务信息载体(即url)构建,服务注册到注册中心,监听注册中心变更这样一系列的功能。

实际上,Dubbo内部为了实现这样的功能,对整个流程进行了层级的划分,整体来说,可以看如下图。

服务发布整体机制

Dubbo服务暴露机制
从整体来看,Dubbo框架做服务暴露主要分为两部分

  1. 将持有的服务实例通过代理转换成Invoker
  2. 将Invoker通过具体的协议转换成Exporter

两部分连接起来的关键点时Invoker。那么Dubbo中Invoker是什么呢?
Invoker是Dubbo实体域,所有模型都会向它靠拢。在服务端中,它可以理解为一个真实的服务对象实例。在客户端中,它可以理解为服务端服务的一个引用对象。

下面我们就从源码角度入手,看看一个Dubbo服务是如何发布的?

服务发布

关于服务发布的入口,本篇文章就略过了。如果有兴趣,可以参考:Dubbo是如何搭上Spring的车的?

下面直接进入正题。
服务暴露会调用ServiceConfig的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值,则搞一个delay线程池hold一下,然后再暴露
    if (delay != null && delay > 0) {
    
    
        delayExportExecutor.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                doExport();
            }
        }, delay, TimeUnit.MILLISECONDS);
    } else {
    
    
        doExport();
    }
}

在此export方法中,主要是属性填充、暴露次数控制、延迟暴露控制。真正的暴露方法在doExport方法中。

protected synchronized void doExport() {
    
    
    // $-- 配置了不暴露(unexported),则抛异常
    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!");
    }
    // $-- 检查provider配置(如果xml没有配置,则从系统环境变量中寻找)
    checkDefault();
    // $-- 填充各种属性
    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();
        }
    }
    if (ref instanceof GenericService) {
    
    
        // $-- 泛化调用配置interfaceClass和generic参数
        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);
        // $-- 校验ref
        checkRef();
        // $-- 设置generic为非泛化调用
        generic = Boolean.FALSE.toString();
    }
    // $-- local属性解析(已经@Deprecated)
    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);
        }
    }
    // $-- stub属性解析
    if (stub != null) {
    
    
        // $-- stub为true,表示使用缺省代理类名,即:接口名+Stub后缀。用于提供方有时候想在客户端也执行部分逻辑的场景
        if ("true".equals(stub)) {
    
    
            stub = interfaceName + "Stub";
        }
        Class<?> stubClass;
        try {
    
    
            stubClass = ClassHelper.forNameWithThreadContextClassLoader(stub);
        } catch (ClassNotFoundException e) {
    
    
            throw new IllegalStateException(e.getMessage(), e);
        }
        if (!interfaceClass.isAssignableFrom(stubClass)) {
    
    
            throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + interfaceName);
        }
    }
    // $-- 校验application
    checkApplication();
    // $-- 校验registry
    checkRegistry();
    // $-- 校验protocol
    checkProtocol();
    // $-- ServiceConfig属性注入
    appendProperties(this);
    // $-- 校验stub与mock
    checkStubAndMock(interfaceClass);
    if (path == null || path.length() == 0) {
    
    
        path = interfaceName;
    }
    // $-- 暴露服务url
    doExportUrls();
    ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), this, ref);
    ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
}

此方法主要还是一些方法的填充,包括一些Dubbo特性,如泛化调用、Stub属性等。真正重要的逻辑,即暴露服务URL,会调用doExportUrls方法。

private void doExportUrls() {
    
    
    // $-- 获取当前服务对应的注册中心实例,获取url列表
    List<URL> registryURLs = loadRegistries(true);
    for (ProtocolConfig protocolConfig : protocols) {
    
    
        // $-- 如果服务指定暴露多个协议,则依次暴露服务
        doExportUrlsFor1Protocol(protocolConfig, registryURLs);
    }
}

doExportUrls包含了大体的一个处理逻辑。即获取注册中心实例,然后将服务注册到注册中心中。
由于协议可以配置多个,因此可能需要暴露多次。

继续来看暴露方法doExportUrlsFor1Protocol

private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
    
    
    // $-- 取protocol配置的协议名,默认协议为dubbo
    String name = protocolConfig.getName();
    if (name == null || name.length() == 0) {
    
    
        name = "dubbo";
    }

    // $-- 下面进行构建配置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,用于后续构造URL
    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 new IllegalArgumentException("argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>");
                    }

                }
            }
        } // end of methods for
    }

    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)), ","));
        }
    }
    // $-- token配置设置
    if (!ConfigUtils.isEmpty(token)) {
    
    
        if (ConfigUtils.isDefault(token)) {
    
    
            map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
        } else {
    
    
            map.put(Constants.TOKEN_KEY, token);
        }
    }
    // $-- injvm协议配置设置
    if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) {
    
    
        protocolConfig.setRegister(false);
        map.put("notify", "false");
    }
    // export service
    String contextPath = protocolConfig.getContextpath();
    if ((contextPath == null || contextPath.length() == 0) && provider != null) {
    
    
        contextPath = provider.getContextpath();
    }

    // $-- 将配置信息(主要为配置map)转化为url
    String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
    Integer port = this.findConfigedPorts(protocolConfig, name, map);
    URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);

    if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
            .hasExtension(url.getProtocol())) {
    
    
        url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
    }

    // $-- scope非none才进行服务暴露
    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());
                    }
                    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
                    String proxy = url.getParameter(Constants.PROXY_KEY);
                    if (StringUtils.isNotEmpty(proxy)) {
    
    
                        registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
                    }

                    // $-- 通过动态代理转换成Invoker,registryURL存储的是注册中心地址,使用export作为key追加服务元数据信息
                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                    DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                    // $-- 服务暴露后向注册中心注册服务信息(链路:ProtocolFilterWrapper->ProtocolListenerWrapper->RegistryProtocol)
                    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判断到底该如何暴露。

  • 如果scope不是remote,则代表要进行本地服务暴露。即在本地服务器上进行服务的暴露,不必与注册中心或其他服务器交互。对应的协议为injvm协议,调用ServiceConfig的exportLocal方法
  • 如果scope不是local,则要进行远程服务暴露

下面我们分别从这两条线来看一下服务暴露

本地服务暴露(injvm协议)

本地服务暴露方法调用了exportLocal方法

private void exportLocal(URL url) {
    
    
    // $-- Dubbo默认会把远程服务用injvm协议再暴露一份
    if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
    
    
        // $-- 将协议改为injvm,127.0.0.1:0,示例:injvm://127.0.0.1/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&bean.name=com.alibaba.dubbo.demo.DemoService&bind.ip=192.168.0.105&bind.port=20880&dubbo=2.0.2&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=7444&qos.port=22222&side=provider×tamp=1588660944080
        URL local = URL.valueOf(url.toFullString())
                .setProtocol(Constants.LOCAL_PROTOCOL)
                .setHost(LOCALHOST)
                .setPort(0);
        ServiceClassHolder.getInstance().pushServiceClass(getServiceClass(ref));
        // $-- 调用InjvmProtocol#export(链路:ProtocolFilterWrapper->ProtocolListenerWrapper->InjvmProtocol)
        Exporter<?> exporter = protocol.export(
                proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
        exporters.add(exporter);
        logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
    }
}

本地服务暴露第一行代码你可能就蒙掉了。为啥非injvm协议的反而才进行本地服务暴露呢?
那是因为Dubbo默认会将远程服务都用injvm协议暴露一次,如果本来就是injvm协议,那么就不用处理喽~

随后会进行协议URL的拼装,主要是将协议改成injvm协议,然后host设置为127.0.0.1,port设置为0.
最重要的服务暴露方法只有一行,全在protocol的export这个方法中。

export方法的参数是一个Invoker。那么这个Invoker是怎么创建的呢?
它是通过proxyFactory的getInvoker方法来创建的。而proxyFactory是ServiceConfig的一个属性,是通过SPI机制来生成的。默认情况下,最终调用的实现类是JavassistProxyFactory。

private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();

JavassistProxyFactory的getInvoker方法中,实际上先是对要暴露的服务类,利用Wrapper生成了代理,然后再利用AbstractProxyInvoker来将这个代理包装了一下返回。

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    
    
    // $-- 对实现类进行包装,如果类名含有$,则使用接口类型
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    // $-- 创建Invoker实例并返回
    return new AbstractProxyInvoker<T>(proxy, type, url) {
    
    
        @Override
        protected Object doInvoke(T proxy, String methodName,
                                  Class<?>[] parameterTypes,
                                  Object[] arguments) throws Throwable {
    
    
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
}

所以回到export方法,其参数实际上是一个包装了实际服务类的AbstractProxyInvoker类。
拿到Invoker之后,就会调用export方法了。其调用类protocol实际上也是一个SPI扩展类。

private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

这里的Protocol类比较复杂,它是通过SPI的自适应扩展机制生成的。你可以想象为,在程序运行时,SPI生成了一个Protocol A d a p t i v e 类 , 运 行 时 实 际 上 执 行 的 是 P r o t o c o l Adaptive类,运行时实际上执行的是Protocol AdaptiveProtocolAdaptive类的export方法。

我将Protocol$Adaptive类复制下来,方便大家理解。代码如下

package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
    
    
public void destroy() {
    
    
  throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}

public int getDefaultPort() {
    
    
  throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}

public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws com.alibaba.dubbo.rpc.RpcException {
    
    
  if (arg1 == null) 
   throw new IllegalArgumentException("url == null");
  com.alibaba.dubbo.common.URL url = arg1;
  String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
  if(extName == null) 
   throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
  com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
  return extension.refer(arg0, arg1);
}

public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.RpcException {
    
    
  if (arg0 == null) 
   throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
  if (arg0.getUrl() == null) 
   throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
  com.alibaba.dubbo.common.URL url = arg0.getUrl();
  String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
  if(extName == null) 
   throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
  com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
  return extension.export(arg0);
}
}

可以看到,生成的代码里还有SPI调用。具体的SPI逻辑,可以参考以前的文章,这里就不赘述了。

可以看到,实际上调用export方法时,调用主体仍然是一个SPI扩展。具体扩展加载流程,我们这里就不赘述了。实际上,这里调用的是ProtocolFilterWrapper类,而ProtocolFilterWrapper内部又包装了ProtocolListenerWrapper,ProtocolListenerWrapper内部又包装了InjvmProtocol。他们的功能分别如下:

  • ProtocolFilterWrapper主要负责拦截器的处理,即在真正调用前进行一些额外的Filter处理。
  • ProtocolListnerWrapper主要负责监听器的处理。

剥去层层外壳,在InjvmProtocol的export中,则会进行服务的暴露。

Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    
    
    // $-- 直接放到exporterMap内存中
    return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}

额,可以看到,这里直接就是new了一个InjvmExporter类,将Invoker包装成了Exporter。
在InjvmExporter类的构造方法中,可以看到,直接将服务放到了内存中的一个Map中

private final Map<String, Exporter<?>> exporterMap;

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

由于本地的服务暴露不需要与注册中心或其他外部服务器交互,因此直接将服务信息放到内存中就可以满足本地的调用需求了,这种实现还是挺清晰的。

至此,一个Injvm协议的服务暴露就完成了。
下面继续我们的旅程,来看一下远程服务的暴露。

远程服务暴露

首先让我们回到scope判断处,来看一下另一种情况,即远程服务调用。
远程服务调用,先是要获取注册中心的地址。

  • 如果有注册中心,则需要配置服务调用监控,然后进行服务暴露。
  • 如果没有注册中心,则直接暴露服务。

无注册中心和有注册中心,基本代码都相同,只是少了注册中心的处理。我们直接看有注册中心的场景,这样更全面。调用方法如下:

// $-- 通过动态代理转换成Invoker,registryURL存储的是注册中心地址,使用export作为key追加服务元数据信息
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

// $-- 服务暴露后向注册中心注册服务信息(链路:ProtocolFilterWrapper->ProtocolListenerWrapper->RegistryProtocol)
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);

这里的protocol.export我们在"本地服务暴露"时已经看到过了。不过这里最后调用的是RegistryProtocol,我们进到RegistryProtocol中看一下它的export方法。

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    
    
    // $-- 具体协议服务暴露(打开本地ip、端口,进行监听)
    //export invoker
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);

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

    // $-- 获取注册中心实例
    //registry provider
    final Registry registry = getRegistry(originInvoker);
    // $-- 获取注册到注册中心的服务提供者url
    final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);

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

    // $-- 将服务提供者invoker记录到内存map中
    ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);

    if (register) {
    
    
        // $-- 具体服务暴露之后,向注册中心注册服务
        register(registryUrl, registeredProviderUrl);
        // $-- 设置内存map中的invoker为已注册
        ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
    }

    // $-- 监听服务接口下configurators节点,用于处理动态配置
    // 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);
    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
    // $-- 保证每次服务暴露都返回一个新的exporter实例
    //Ensure that a new exporter instance is returned every time export
    return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
}

这个方法里对注册中心场景下服务暴露一系列逻辑进行了统筹。逻辑主要可以分为以下几个方面:

  1. 服务暴露
  2. 向注册中心注册服务
  3. 订阅节点配置变更事件

这几个过程都比较复杂,下面我们分小节一一来看一下。

服务暴露

单说“服务暴露”,可能有点让人迷惑。简单地来说,这一部分代码的逻辑就是我们提供了一个服务,要在本地的服务器上开一个端口,将该服务暴露出去。理解了这个概念,相信你对接下来的服务暴露会有一点更清晰的认识。

我们先来看看服务暴露,即RegistryProtocol的doLocalExport方法

private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) {
    
    
     // $-- 生成服务的cache key(主要为url中键exporter对应的值拼接而成)
    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));
                // $-- 进行服务暴露(链路:ProtocolFilterWrapper->ProtocolListenerWrapper->DubboProtocol),并包装返回的exporter
                exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);
                bounds.put(key, exporter);
            }
        }
    }
    return exporter;
}

略过缓存的那一套逻辑,这个方法主要先是生成Invoker代理(实际上也是包装了一下Invoker),然后调用protocol的export方法进行服务暴露。

你可能有点奇怪,我们刚从protocol的export方法进来,这里为什么又要进行protocol的export了呢?
实际上,前面RegistryProtocol是可以看做是注册中心统一的协议。凡是走注册中心的,都是要走到RegistryProtocol的exporter方法。里面封装了注册中心的一系列操作。然而RegistryProtocol并不是自己暴露,它依然将服务“外包”了出去。最终这里经过层层调用,执行的是DubboProtocol的exporter方法。

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

    // $-- 根据服务分组、版本、接口和端口构造key。远程调用时,通过此key判断调用哪个Exporter
    // export service.
    String key = serviceKey(url);
    // $-- 构建exporter
    DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
    // $-- 把exporter存储到缓存中
    exporterMap.put(key, exporter);

    /**
     *  $-- 是否支持本地存根
     *  远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,
     *     比如:做ThreadLocal缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在API中带上Stub,
     *  客户端生成Proxy实,会把Proxy通过构造函数传给Stub,然后把Stub暴露组给用户,Stub可以决定要不要去调Proxy。
     */
    //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;
}

在DubboProtocol的export方法里,我们终于能够看到一点服务暴露的影子了。
和injvm协议一样,先是要生成服务的key,生成exporter,随后DubboProtocol进行了Stub属性的处理,最后会打开服务连接。

openServerUrl方法用来进行与注册中心的连接。代码如下:

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) {
    
    
            // $-- 创建server
            serverMap.put(key, createServer(url));
        } else {
    
    
            // $-- reset server(配合override功能使用)
            // server supports reset, use together with override
            server.reset(url);
        }
    }
}

如果是初次连接,则会createServer,代码如下:

private ExchangeServer createServer(URL url) {
    
    
    // $-- 默认开启server关闭时发送readonly事件
    // send readonly event when server closes, it's enabled by default
    url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString());
    // $-- 默认开启心跳
    // enable heartbeat by default
    url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
    // $-- 默认使用netty
    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);

    // $-- 默认使用dubbo协议编码
    url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
    ExchangeServer server;
    try {
    
    
        // $-- 创建NettyServer并且初始化Handler
        server = Exchangers.bind(url, requestHandler);
    } catch (RemotingException e) {
    
    
        throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
    }
    // $-- 校验client
    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;
}

这段的关键代码在于Exchangers.bind 一句,这会调用底层的NettyServer进行处理,同时创建N个Handler,用来支持例如心跳、编解码等连接传输相关功能。

后续的调用链路比较长,涉及到Dubbo的底层模型和netty的连接处理,如果你不关注,可以略过这一段。直接看“向注册中心注册服务”这一小节。

下面,我们从Exchangers.bind 开始,一起起来看看Dubbo暴露服务的底层逻辑。

public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
    
    
    if (url == null) {
    
    
        throw new IllegalArgumentException("url == null");
    }
    if (handler == null) {
    
    
        throw new IllegalArgumentException("handler == null");
    }
    url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
    // $-- 根据url获取Exchanger(默认 HeaderExchanger),然后bind
    return getExchanger(url).bind(url, handler);
}

这里依然会利用SPI机制寻找实现类,默认会调用HeaderExchanger的bind方法来生成ExchangeServer。

public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
    
    
    return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
}

HeaderExchanger的bind方法只有一句,但是包含了多个Handler。我们主要关注一下Transporters.bind方法。

public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException {
    
    
    if (url == null) {
    
    
        throw new IllegalArgumentException("url == null");
    }
    if (handlers == null || handlers.length == 0) {
    
    
        throw new IllegalArgumentException("handlers == null");
    }
    ChannelHandler handler;
    if (handlers.length == 1) {
    
    
        handler = handlers[0];
    } else {
    
    
        // $-- 如果有多个handler的话,需要用分发器包装下
        handler = new ChannelHandlerDispatcher(handlers);
    }
    // $-- 获取Adaptive的Transporter(默认是 NettyTransporter),然后bind
    return getTransporter().bind(url, handler);
}

该方法最后一句,依然是利用SPI自适应的特性获取实际类,默认是NettyTransporter。

public Server bind(URL url, ChannelHandler listener) throws RemotingException {
    
    
  return new NettyServer(url, listener);
}

NettyTransporter的bind方法直接new了一个NettyServer,而NettyServer的构造方法中直接调用了父类AbstractServer的构造方法。

public AbstractServer(URL url, ChannelHandler handler) throws RemotingException {
    
    
    super(url, handler);
    localAddress = getUrl().toInetSocketAddress();

    String bindIp = getUrl().getParameter(Constants.BIND_IP_KEY, getUrl().getHost());
    int bindPort = getUrl().getParameter(Constants.BIND_PORT_KEY, getUrl().getPort());
    if (url.getParameter(Constants.ANYHOST_KEY, false) || NetUtils.isInvalidLocalHost(bindIp)) {
    
    
        bindIp = NetUtils.ANYHOST;
    }
    bindAddress = new InetSocketAddress(bindIp, bindPort);
    this.accepts = url.getParameter(Constants.ACCEPTS_KEY, Constants.DEFAULT_ACCEPTS);
    this.idleTimeout = url.getParameter(Constants.IDLE_TIMEOUT_KEY, Constants.DEFAULT_IDLE_TIMEOUT);
    try {
    
    
        // $-- 初始化的时候会打开Server
        doOpen();
        if (logger.isInfoEnabled()) {
    
    
            logger.info("Start " + getClass().getSimpleName() + " bind " + getBindAddress() + ", export " + getLocalAddress());
        }
    } catch (Throwable t) {
    
    
        throw new RemotingException(url.toInetSocketAddress(), null, "Failed to bind " + getClass().getSimpleName()
                + " on " + getLocalAddress() + ", cause: " + t.getMessage(), t);
    }
    //fixme replace this with better method
    DataStore dataStore = ExtensionLoader.getExtensionLoader(DataStore.class).getDefaultExtension();
    executor = (ExecutorService) dataStore.get(Constants.EXECUTOR_SERVICE_COMPONENT_KEY, Integer.toString(url.getPort()));
}

注意这里的doOpen方法,看起来要到我们关注的地方了。它是在子类NettyServer中实现的。

protected void doOpen() throws Throwable {
    
    
    NettyHelper.setNettyLoggerFactory();
    // $-- 创建boss线程池、worker线程池
    ExecutorService boss = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerBoss", true));
    ExecutorService worker = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerWorker", true));
    // $-- 没有指定工作者线程数量,就使用cpu+1
    ChannelFactory channelFactory = new NioServerSocketChannelFactory(boss, worker, getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS));
    bootstrap = new ServerBootstrap(channelFactory);

    final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);
    channels = nettyHandler.getChannels();
    // https://issues.jboss.org/browse/NETTY-365
    // https://issues.jboss.org/browse/NETTY-379
    // final Timer timer = new HashedWheelTimer(new NamedThreadFactory("NettyIdleTimer", true));
    bootstrap.setOption("child.tcpNoDelay", true);
    bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
    
    
        @Override
        public ChannelPipeline getPipeline() {
    
    
            NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
            ChannelPipeline pipeline = Channels.pipeline();
            /*int idleTimeout = getIdleTimeout();
            if (idleTimeout > 10000) {
                pipeline.addLast("timer", new IdleStateHandler(timer, idleTimeout / 1000, 0, 0));
            }*/
            pipeline.addLast("decoder", adapter.getDecoder());
            pipeline.addLast("encoder", adapter.getEncoder());
            pipeline.addLast("handler", nettyHandler);
            return pipeline;
        }
    });
    // $-- 绑定到指定的ip和端口之上
    // bind
    channel = bootstrap.bind(getBindAddress());
}

从这段代码中,我们终于看到服务的暴露相关操作了。通过doOpen方法,Netty将对应ip和端口打开,从而实现了“服务暴露”。

以上就是服务暴露的过程了,下面我们将视线往回转,继续服务暴露后注册到注册中心的过程。

向注册中心注册服务

Registry的export方法中,向注册中心注册服务相关的代码如下

if (register) {
    
    
    // $-- 具体服务暴露之后,向注册中心注册服务
    register(registryUrl, registeredProviderUrl);
    // $-- 设置内存map中的invoker为已注册
    ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
}

我们主要看register方法

public void register(URL registryUrl, URL registedProviderUrl) {
    
    
    Registry registry = registryFactory.getRegistry(registryUrl);
    registry.register(registedProviderUrl);
}

这里分为两个部分,先是获取registry实例,然后再向registry中注册服务。

registry实例的获取

registry实例是通过registryFactory来获取的。这里的registryFactory也是通过Dubbo SPI机制生成的自适应扩展类。默认的实现是DubboRegistryFactory,不过我们一般会配置protocol为zookeeper,所以我们就直接看ZookeeperRegistry类的实现吧。(后续的实现我们都只关注zookeeper实现)

ZookeeperRegistry的getRegistry方法继承自父类AbstractRegistryFactory,代码如下:

public Registry getRegistry(URL url) {
    
    
    url = url.setPath(RegistryService.class.getName())
            .addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
            .removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
    // $-- key示例:multicast://224.5.6.7:1234/com.alibaba.dubbo.registry.RegistryService
    String key = url.toServiceString();
    // $-- 加锁保证单例
    LOCK.lock();
    try {
    
    
        // $-- 老套路,先从缓存中获取
        Registry registry = REGISTRIES.get(key);
        if (registry != null) {
    
    
            return registry;
        }
        // $-- 缓存中获取不到,直接创建registry
        registry = createRegistry(url);
        if (registry == null) {
    
    
            throw new IllegalStateException("Can not create registry " + url);
        }
        // $-- 写入缓存
        REGISTRIES.put(key, registry);
        return registry;
    } finally {
    
    
        // Release the lock
        LOCK.unlock();
    }
}

熟悉的缓存套路,我们不再赘述。这里主要看一下createRegistry方法,它是由ZookeeperRegistryFactory类实现的。

public Registry createRegistry(URL url) {
    
    
    return new ZookeeperRegistry(url, zookeeperTransporter);
}

createRegistry方法直接创建一个ZookeeperRegistry实例。

ZookeeperRegistry继承自FailbackRegistry,而FailbackRegistry又是继承自AbstractRegistry。创建ZookeeperRegistry实例时,会依次调用其父类的构造方法进行初始化。

AbstractRegistry的初始化

AbstractRegistry的构造函数如下:

public AbstractRegistry(URL url) {
    
    
    // $-- 设置url
    setUrl(url);
    // $-- 获取缓存文件路径
    // Start file save timer
    syncSaveFile = url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY, false);
    String filename = url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter(Constants.APPLICATION_KEY) + "-" + url.getAddress() + ".cache");
    File file = null;
    if (ConfigUtils.isNotEmpty(filename)) {
    
    
        file = new File(filename);
        if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
    
    
            if (!file.getParentFile().mkdirs()) {
    
    
                throw new IllegalArgumentException("Invalid registry store file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
            }
        }
    }
    this.file = file;
    // $-- 加载文件中的属性
    loadProperties();
    // $-- 通知订阅者,当前节点上线了 notify
    notify(url.getBackupUrls());
}

Dubbo会将一些服务信息缓存在本地文件中。这样当注册中心不可用时,本地也可以找到相应的服务进行调用。
而AbstractRegistry的初始化方法中对这一特性做了支持,当注册中心启动时,要去尝试加载缓存的配置文件。

FailbackRegistry的初始化

FailbackRegistry的构造函数如下:

public FailbackRegistry(URL url) {
    
    
   super(url);
    // $-- 重试时间,默认5000ms
    this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
    // $-- 启动一个失败重试定时任务,定时对注册失败/订阅失败等URL集合进行重试
    this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            // Check and connect to the registry
            try {
    
    
                retry();
            } catch (Throwable t) {
    
     // Defensive fault tolerance
                logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t);
            }
        }
    }, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS);
}

protected void retry() {
    
    
    // $-- 对注册失败的URL集合进行重试
    if (!failedRegistered.isEmpty()) {
    
    
        Set<URL> failed = new HashSet<URL>(failedRegistered);
        if (failed.size() > 0) {
    
    
            if (logger.isInfoEnabled()) {
    
    
                logger.info("Retry register " + failed);
            }
            try {
    
    
                for (URL url : failed) {
    
    
                    try {
    
    
                        doRegister(url);
                        failedRegistered.remove(url);
                    } catch (Throwable t) {
    
     // Ignore all the exceptions and wait for the next retry
                        logger.warn("Failed to retry register " + failed + ", waiting for again, cause: " + t.getMessage(), t);
                    }
                }
            } catch (Throwable t) {
    
     // Ignore all the exceptions and wait for the next retry
                logger.warn("Failed to retry register " + failed + ", waiting for again, cause: " + t.getMessage(), t);
            }
        }
    }
    // ...
}

FailbackRegistry顾名思义,失败重试的意思。因此这一层的初始化主要关注于对于注册失败/订阅失败等场景的支持。在代码里,会启动用一个定时任务,不断对这些失败场景进行重试。

FailbackRegistry支持以下几个场景的重试:

  • 对注册失败的URL集合进行重试(failedRegistered)
  • 对取消注册失败的URL集合进行重试(failedUnregistered)
  • 对订阅失败的URL集合进行重试(failedSubscribed)
  • 对取消注册失败的URL集合进行重试(failedUnsubscribed)
  • 对通知失败的URL集合进行重试(failedNotified)
ZookeeperRegistry的初始化
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
    
    
    super(url);
    if (url.isAnyHost()) {
    
    
        throw new IllegalStateException("registry address == null");
    }
    // $-- 获取注册中心分组,默认/dubbo
    String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT);
    if (!group.startsWith(Constants.PATH_SEPARATOR)) {
    
    
        group = Constants.PATH_SEPARATOR + group;
    }
    this.root = group;
    // $-- zk连接,获得Zookeeper客户端连接
    zkClient = zookeeperTransporter.connect(url);
    // $-- 添加状态改变监听器
    zkClient.addStateListener(new StateListener() {
    
    
        @Override
        public void stateChanged(int state) {
    
    
            if (state == RECONNECTED) {
    
    
                try {
    
    
                    recover();
                } catch (Exception e) {
    
    
                    logger.error(e.getMessage(), e);
                }
            }
        }
    });
}

protected void recover() throws Exception {
    
    
   // $-- 重连之后,将原先注册的URL集合放入注册失败URL集合,等待线程轮询重试注册
    // register
    Set<URL> recoverRegistered = new HashSet<URL>(getRegistered());
    if (!recoverRegistered.isEmpty()) {
    
    
        if (logger.isInfoEnabled()) {
    
    
            logger.info("Recover register url " + recoverRegistered);
        }
        for (URL url : recoverRegistered) {
    
    
            failedRegistered.add(url);
        }
    }
    // $-- 同理,重连之后,将原先订阅的URL集合放入订阅失败URL集合,等待线程轮询重试订阅
    // subscribe
    Map<URL, Set<NotifyListener>> recoverSubscribed = new HashMap<URL, Set<NotifyListener>>(getSubscribed());
    if (!recoverSubscribed.isEmpty()) {
    
    
        if (logger.isInfoEnabled()) {
    
    
            logger.info("Recover subscribe url " + recoverSubscribed.keySet());
        }
        for (Map.Entry<URL, Set<NotifyListener>> entry : recoverSubscribed.entrySet()) {
    
    
            URL url = entry.getKey();
            for (NotifyListener listener : entry.getValue()) {
    
    
                addFailedSubscribed(url, listener);
            }
        }
    }
}

ZookeeperRegistry的构造方法中,就要进行Zookeeper注册中心相关的操作了。首先获取Zookeeper客户端,然后添加一个状态监听器。

状态监听器的功能比较简单,我们先来看一下。它主要处理重连注册中心的场景,此时会调用recover方法来进行“恢复”。恢复啥?主要恢复以下几个方面:

  • 将原先注册的URL集合放入注册失败URL集合(failedRegistered),等待线程轮询重试注册
  • 将原先订阅的URL集合放入订阅失败URL集合(failedSubscribed),等待线程轮询重试订阅

等待的线程是啥时候定义的呢?还记得我们刚看的FailbackRegistry类吗?在FailbackRegistry初始化的时候,会添加定时重试任务,这里就是它的应用了。

下面我们回头看一下Zookeeper客户端的获取方法。即 zookeeperTransporter.connect 方法。
ZookeeperTransporter也是通过Dubbo SPI自适应扩展机制获取的,默认实现为CuratorZookeeperTransporter类。

public ZookeeperClient connect(URL url) {
    
    
    return new CuratorZookeeperClient(url);
}

可以看到,该方法是直接创建了一个CuratorZookeeperClient类。在CuratorZookeeperClient类的构造方法中,会进行Zookeeper客户端的获取,代码如下:

public CuratorZookeeperClient(URL url) {
    
    
  super(url);
    try {
    
    
        // $-- 创建CuratorFramework构造器
        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
                .connectString(url.getBackupAddress())
                .retryPolicy(new RetryNTimes(1, 1000))
                .connectionTimeoutMs(5000);
        String authority = url.getAuthority();
        if (authority != null && authority.length() > 0) {
    
    
            builder = builder.authorization("digest", authority.getBytes());
        }
        // $-- 构建CuratorFramework实例
        client = builder.build();
        // $-- 添加监听器
        client.getConnectionStateListenable().addListener(new ConnectionStateListener() {
    
    
            @Override
            public void stateChanged(CuratorFramework client, ConnectionState state) {
    
    
                if (state == ConnectionState.LOST) {
    
    
                    CuratorZookeeperClient.this.stateChanged(StateListener.DISCONNECTED);
                } else if (state == ConnectionState.CONNECTED) {
    
    
                    CuratorZookeeperClient.this.stateChanged(StateListener.CONNECTED);
                } else if (state == ConnectionState.RECONNECTED) {
    
    
                    CuratorZookeeperClient.this.stateChanged(StateListener.RECONNECTED);
                }
            }
        });
        // $-- 启动客户端
        client.start();
    } catch (Exception e) {
    
    
        throw new IllegalStateException(e.getMessage(), e);
    }
}

这样一个ZookeeperRegistry就创建完成了。

向registry中注册服务

上面创建了一个registry实例,接下来就要将我们要暴露的服务添加到注册中心中。
ZookeeperRegistry中并没有重写register方法,它的的register方法继承自父类FailbackRegistry。其代码如下:

public void register(URL url) {
    
    
// $-- 将该url添加到已注册url集合中
    super.register(url);
    failedRegistered.remove(url);
    failedUnregistered.remove(url);
    try {
    
    
        // $-- 向server发送注册请求
        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);
        }

        // $-- 记录注册失败url到失败列表,定时重试
        failedRegistered.add(url);
    }
}

因为要新注册一个服务的url,所以这段代码先维护了几个url集合的增删。然后就是主要逻辑 doRegister方法了,它由子类实现,会进行真正的真注册。另外,这里值得注意的是,如果注册异常,会进行一些特殊处理,包括:

  • 如果开启了启动时检查,则抛出异常
  • 如果未开启启动时检查,则记录该失败的url到注册失败集合中,等待定时重试

下面看一下doRegister的实现,即ZookeeperRegistry的doRegister方法。代码如下:

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);
    }
}

可以看到,zk中的注册,是直接通过zk客户端来创建节点完成的。
创建节点的逻辑如下:

public void create(String path, boolean ephemeral) {
    
    
    if (!ephemeral) {
    
    
        // $-- 如果要创建的节点非临时节点,则检查节点是否存在
        if (checkExists(path)) {
    
    
            return;
        }
    }
    int i = path.lastIndexOf('/');
    if (i > 0) {
    
    
        // $-- 递归创建上一级路径
        create(path.substring(0, i), false);
    }
    // $-- 根据ephemeral的值创建临时或持久节点
    if (ephemeral) {
    
    
        createEphemeral(path);
    } else {
    
    
        createPersistent(path);
    }
}

熟悉Zookeeper的同学应该都知道,Zookeeper中的节点存在临时节点和持久节点的区分,Dubbo中创建节点可以通过url的dynamic属性来指定使用哪一种节点。这段代码考虑了这两种节点,逻辑主要如下:

  1. 创建节点的时候,先判断是否是持久节点,如果是,要先检查该节点是否存在。
  2. 如果要创建的节点路径包含“/”,则要先创建该节点的父级节点
  3. 根据配置创建临时/持久节点

创建节点的方法比较简单,Curator中自有一套创建的API,直接调用即可,这里我们就不再赘述了。

订阅节点配置变更事件

上面介绍了服务的暴露和注册中心的注册。接下来还要订阅节点配置变更事件。

ZookeeperRegistry中也没有重写subscribe方法,该方法继承自FailbackRegistry

public void subscribe(URL url, NotifyListener listener) {
    
    
    super.subscribe(url, listener);
    removeFailedSubscribed(url, listener);
    try {
    
    
        // $-- 向服务器发送订阅请求
        // Sending a subscription request to the server side
        doSubscribe(url, listener);
    } catch (Exception e) {
    
    
        Throwable t = e;

        List<URL> urls = getCacheUrls(url);
        if (urls != null && !urls.isEmpty()) {
    
    
            // $-- 订阅注册中心节点信息失败,从缓存中获取url,并通知url
            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);
            }
        }

        // $-- 将失败的订阅请求记录到失败列表,定时重试
        // Record a failed registration request to a failed list, retry regularly
        addFailedSubscribed(url, listener);
    }
}

这一段代码与register的方法很像,处理逻辑也是类似。这里我们主要关注一下由子类ZookeeperRegistry实现的doSubscribe方法。

protected void doSubscribe(final URL url, final NotifyListener listener) {
    
    
    try {
    
    
        if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
    
    
            // $-- 全量订阅场景,*代表所有,如 监控中心的订阅
            String root = toRootPath();
            // $-- 1. 获取listeners
            ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
            if (listeners == null) {
    
    
                // $-- listeners为空,说明缓存中没有,这里把listeners放入缓存
                zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
                listeners = zkListeners.get(url);
            }
            ChildListener zkListener = listeners.get(listener);
            if (zkListener == null) {
    
    
                // $-- zkListener为空,说明是第一次,新建一个listener
                listeners.putIfAbsent(listener, new ChildListener() {
    
    
                    @Override
                    public void childChanged(String parentPath, List<String> currentChilds) {
    
    
                        for (String child : currentChilds) {
    
    
                            child = URL.decode(child);
                            if (!anyServices.contains(child)) {
    
    
                                // $-- 如果有子节点还没有被订阅,说明是新节点,则订阅
                                anyServices.add(child);
                                subscribe(url.setPath(child).addParameters(Constants.INTERFACE_KEY, child,
                                        Constants.CHECK_KEY, String.valueOf(false)), listener);
                            }
                        }
                    }
                });
                zkListener = listeners.get(listener);
            }
            // $-- 2. 创建root持久节点(ephemeral:短暂的,临时的)
            zkClient.create(root, false);
            // $-- 3. 订阅root节点,并返回该节点下所有子节点
            List<String> services = zkClient.addChildListener(root, zkListener);
            if (services != null && !services.isEmpty()) {
    
    
                // $-- 遍历所有子节点进行订阅
                for (String service : services) {
    
    
                    service = URL.decode(service);
                    anyServices.add(service);
                    subscribe(url.setPath(service).addParameters(Constants.INTERFACE_KEY, service,
                            Constants.CHECK_KEY, String.valueOf(false)), listener);
                }
            }
        } else {
    
    
            // $-- 指定类别订阅场景
            List<URL> urls = new ArrayList<URL>();
            // $-- 根据URL的类别,获取要订阅的路径(将URL转换为Dubbo注册的路径),如:/dubbo/cn.hewie.hservice.facade.UserService/configurators
            for (String path : toCategoriesPath(url)) {
    
    
                // $-- 1. 给该URL添加监听器
                ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                if (listeners == null) {
    
    
                    // $-- 如果listeners为空,则创建
                    zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
                    listeners = zkListeners.get(url);
                }
                ChildListener zkListener = listeners.get(listener);
                if (zkListener == null) {
    
    
                    // $-- 如果ChildListener为空,则创建ChildListener,添加监听器监听子节点变化
                    listeners.putIfAbsent(listener, new ChildListener() {
    
    
                        @Override
                        public void childChanged(String parentPath, List<String> currentChilds) {
    
    
                            ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
                        }
                    });
                    zkListener = listeners.get(listener);
                }
                // $-- 2. 创建节点,订阅监听
                zkClient.create(path, false);
                // $-- 对该节点(如 provider)发起监听,该方法会返回所有的子节点
                List<String> children = zkClient.addChildListener(path, zkListener);
                if (children != null) {
    
    
                    urls.addAll(toUrlsWithEmpty(url, path, children));
                }
            }
            // $-- 3. 所有订阅完成后,进行通知。这里会连接provider,实例化invoker
            notify(url, listener, urls);
        }
    } catch (Throwable e) {
    
    
        throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}

全量订阅的场景我们暂且不看,这里我们主要需要关注一下指定类别订阅的场景。

这段代码看起来复杂,其实主要就是先将订阅的url转化为zookeeper上的节点,然后创建该节点,添加对该节点的监听。

由于代码中的listener比较多,大家可能会乱掉。其实这里最重要的就是ChildListener接口。ChildListener是一个内部类,用来在监听的节点发生变更时,通知对应的消费端,具体的监听处理是在zkClient.addChildListener中实现的

public List<String> addChildListener(String path, final ChildListener listener) {
    
    
    ConcurrentMap<ChildListener, TargetChildListener> listeners = childListeners.get(path);
    if (listeners == null) {
    
    
        childListeners.putIfAbsent(path, new ConcurrentHashMap<ChildListener, TargetChildListener>());
        listeners = childListeners.get(path);
    }
    TargetChildListener targetListener = listeners.get(listener);
    if (targetListener == null) {
    
    
     // $-- 创建ChildListener的处理方法类,对于curator,是CuratorWatcherImpl类
        listeners.putIfAbsent(listener, createTargetChildListener(path, listener));
        targetListener = listeners.get(listener);
    }
    return addTargetChildListener(path, targetListener);
}

public CuratorWatcher createTargetChildListener(String path, ChildListener listener) {
    
    
    return new CuratorWatcherImpl(listener);
}

public List<String> addTargetChildListener(String path, CuratorWatcher listener) {
    
    
    try {
    
    
        return client.getChildren().usingWatcher(listener).forPath(path);
    } catch (NoNodeException e) {
    
    
        return null;
    } catch (Exception e) {
    
    
        throw new IllegalStateException(e.getMessage(), e);
    }
}

private class CuratorWatcherImpl implements CuratorWatcher {
    
    

    private volatile ChildListener listener;

    public CuratorWatcherImpl(ChildListener listener) {
    
    
        this.listener = listener;
    }

    public void unwatch() {
    
    
        this.listener = null;
    }

    @Override
    public void process(WatchedEvent event) throws Exception {
    
    
        if (listener != null) {
    
    
            String path = event.getPath() == null ? "" : event.getPath();
            listener.childChanged(path,
                    StringUtils.isNotEmpty(path)
                            ? client.getChildren().usingWatcher(this).forPath(path)
                            : Collections.<String>emptyList());
        }
    }
}

CuratorWatcherImpl实现了Zookeeper的监听接口CuratorWatcher,用来在节点有变更时通知对应的ChildListener

这里有的同学可能会有一个问题。对于服务暴露过程来说,因为自己本身是提供服务的,那么为什么要订阅呢?
其实仔细想一想,Dubbo是提供了修改configurator功能的。所以如果我们修改了zookeeper上的configurator信息,那么服务本地自己也是要动态更新的,所以这里就需要订阅了。

无论是第一次subscribe,还是通过childListener的childChanged方法调用,都会走到com.alibaba.dubbo.registry.support.FailbackRegistry#notify方法,进行通知。

protected void notify(URL url, NotifyListener listener, List<URL> urls) {
    
    
    if (url == null) {
    
    
        throw new IllegalArgumentException("notify url == null");
    }
    if (listener == null) {
    
    
        throw new IllegalArgumentException("notify listener == null");
    }
    try {
    
    
        doNotify(url, listener, urls);
    } catch (Exception t) {
    
    
        // $-- 将失败的通知请求记录到失败列表中,定时重试
        // Record a failed registration request to a failed list, retry regularly
        Map<NotifyListener, List<URL>> listeners = failedNotified.get(url);
        if (listeners == null) {
    
    
            failedNotified.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, List<URL>>());
            listeners = failedNotified.get(url);
        }
        listeners.put(listener, urls);
        logger.error("Failed to notify for subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
    }
}

protected void doNotify(URL url, NotifyListener listener, List<URL> urls) {
    
    
   super.notify(url, listener, urls);
}

这里还是老套路,我们主要关注一下doNotify方法。它最终会调用AbstractRegistry的notify方法。

protected void notify(URL url, NotifyListener listener, List<URL> urls) {
    
    
    if (url == null) {
    
    
        throw new IllegalArgumentException("notify url == null");
    }
    if (listener == null) {
    
    
        throw new IllegalArgumentException("notify listener == null");
    }
    if ((urls == null || urls.isEmpty())
            && !Constants.ANY_VALUE.equals(url.getServiceInterface())) {
    
    
        logger.warn("Ignore empty notify urls for subscribe url " + url);
        return;
    }
    if (logger.isInfoEnabled()) {
    
    
        logger.info("Notify urls for subscribe url " + url + ", urls: " + urls);
    }
    // $-- 将urls按category分类,存放入result中
    Map<String, List<URL>> result = new HashMap<String, List<URL>>();
    for (URL u : urls) {
    
    
        if (UrlUtils.isMatch(url, u)) {
    
    
            String category = u.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
            List<URL> categoryList = result.get(category);
            if (categoryList == null) {
    
    
                categoryList = new ArrayList<URL>();
                result.put(category, categoryList);
            }
            categoryList.add(u);
        }
    }
    // $-- result为空,无需通知
    if (result.size() == 0) {
    
    
        return;
    }
    // $-- 获取已通知Map集合
    Map<String, List<URL>> categoryNotified = notified.get(url);
    if (categoryNotified == null) {
    
    
        notified.putIfAbsent(url, new ConcurrentHashMap<String, List<URL>>());
        categoryNotified = notified.get(url);
    }
    for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
    
    
        String category = entry.getKey();
        List<URL> categoryList = entry.getValue();
        // $-- 保存已通知url Map
        categoryNotified.put(category, categoryList);
        // $-- 保存到文件中
        saveProperties(url);
        // $-- 监听器进行通知,进行zk目录节点下变化的更新,服务上下线的通知更新
        // $-- 对于服务提供者来说(服务暴露过程),这里的listener是OverrideListener,是RegistryProtocol的内部类
        // $-- 对于消费者来说(服务引用过程),这里的listener是RegistryDirectory
        listener.notify(categoryList);
    }
}

AbstractRegistry的notify方法进行通用的notify处理,即将要通知的url按照category分类,同时维护到本地的已通知url集合中,最后会调用相应的listener来进行处理。
对于服务暴露过程来说,是由RegistryProtocol的内部类OverrideListener来进行notify处理。

public synchronized void notify(List<URL> urls) {
    
    
    logger.debug("original override urls: " + urls);
    List<URL> matchedUrls = getMatchedUrls(urls, subscribeUrl);
    logger.debug("subscribe url: " + subscribeUrl + ", override urls: " + matchedUrls);
    // No matching results
    if (matchedUrls.isEmpty()) {
    
    
        return;
    }

    List<Configurator> configurators = RegistryDirectory.toConfigurators(matchedUrls);

    final Invoker<?> invoker;
    if (originInvoker instanceof InvokerDelegete) {
    
    
        invoker = ((InvokerDelegete<?>) originInvoker).getInvoker();
    } else {
    
    
        invoker = originInvoker;
    }
    //The origin invoker
    URL originUrl = RegistryProtocol.this.getProviderUrl(invoker);
    String key = getCacheKey(originInvoker);
    ExporterChangeableWrapper<?> exporter = bounds.get(key);
    if (exporter == null) {
    
    
        logger.warn(new IllegalStateException("error state, exporter should not be null"));
        return;
    }
    //The current, may have been merged many times
    URL currentUrl = exporter.getInvoker().getUrl();
    //Merged with this configuration
    URL newUrl = getConfigedInvokerUrl(configurators, originUrl);
    if (!currentUrl.equals(newUrl)) {
    
    
        // $-- 对修改了url的invoker重新进行暴露
        RegistryProtocol.this.doChangeLocalExport(originInvoker, newUrl);
        logger.info("exported provider url changed, origin url: " + originUrl + ", old export url: " + currentUrl + ", new export url: " + newUrl);
    }
}

在服务暴露的场景下,notify主要的判断此服务是否已经发生了变更,如果变更了,则需要重新暴露。重新暴露会调用doChangeLocalExport方法。

private <T> void doChangeLocalExport(final Invoker<T> originInvoker, URL newInvokerUrl) {
    
    
    String key = getCacheKey(originInvoker);
    final ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
    if (exporter == null) {
    
    
        logger.warn(new IllegalStateException("error state, exporter should not be null"));
    } else {
    
    
        final Invoker<T> invokerDelegete = new InvokerDelegete<T>(originInvoker, newInvokerUrl);
        exporter.setExporter(protocol.export(invokerDelegete));
    }
}

重新暴露的方法,又要回到上面的逻辑了,这里就不重述了。

至此,一个远程服务的暴露过程就介绍完成了。

总结

总结一下吧~
服务器端在框架启动时,会初始化服务实例,通过Proxy组件调用具体协议,把服务端要暴露的接口封装成Invoker,然后转换成Exporter,这个时候框架会打开服务端口等并记录服务实例到内存中,最后通过Registry把服务元数据注册到注册中心,并注册相应的监听。

以上就是Dubbo的服务暴露过程了 ,看起来有点长哈~

猜你喜欢

转载自blog.csdn.net/somehow1002/article/details/107306776