dubbo系列之消费者注册(十一)

581 阅读6分钟

欢迎关注公众号【sharedCode】致力于主流中间件的源码分析, 可以直接与我联系

博主个人网站:www.shared-code.com

前言

上文中介绍了dubbo的服务暴露过程,服务暴露之后,接下来就是消费者如何来引用服务,本文主要是解读一下dubbo消费者引用服务的原理

RefenenceBean

在消费者项目中,引用一个外部的服务,就会生成一个ReferenceBean实例,由于ReferenceBean实现了FactoryBean,所以直接看一下getObject方法,

看看这个Bean是如何生成的

@Override
    public Object getObject() throws Exception {
        return get();
    }

调用了父类ReferenceConfig的get方法

//ReferenceConfig.java
public synchronized T get() {
        if (destroyed) {
          	// 如果destroyed == true , 那么说明这个类已经准备销毁了。
            throw new IllegalStateException("Already destroyed!");
        }
        if (ref == null) {
          	// 如果引用为空,那么需要初始化,调用init方法。
            init();
        }
        return ref;
    }

从上面可以看出,大部分的逻辑都是在init里面 , 在这里,同时也说明了一个问题,消费者引用服务,这个逻辑是发生在ReferenceBean创建的时候。

ReferenceConfig.init()

private void init() {
        if (initialized) {
            return;
        }
        // ...
		// 省略代码200行
  		// ....
  		// 重点在这个方法,创建一个代理类,也就是我们的服务引用类
        ref = createProxy(map);
  		// 建立ConsumerModel,放入全局的MAP中。
        ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods());
        ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel);
    }

ReferenceConfig.createProxy()

private T createProxy(Map<String, String> map) {
        URL tmpUrl = new URL("temp", "localhost", 0, map);
        final boolean isJvmRefer; // 是否需要JVM级别的调用
        if (isInjvm() == null) { 
            if (url != null && url.length() > 0) { // if a url is specified, don't do local reference
                isJvmRefer = false;
            } else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {
                // by default, reference local service if there is
                isJvmRefer = true;
            } else {
                isJvmRefer = false;
            }
        } else {
            isJvmRefer = isInjvm().booleanValue();
        }
		// 上文我们说过,dubbo是本地暴露和远程暴露都会进行的,如果服务的消费者和服务提供者跑在同一个JVM里面,如:同一个Tomcat中,
  		// 那么,dubbo会优先使用本地暴露,因为这个就是直接JVM级别的交互,速度会更加快速
        if (isJvmRefer) {
            URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
            invoker = refprotocol.refer(interfaceClass, url);
            if (logger.isInfoEnabled()) {
                logger.info("Using injvm service " + interfaceClass.getName());
            }
        } else {
            if (url != null && url.length() > 0) { // 是否是直接指定URL的,如果直接指定了注册中心的地址。
                String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
                if (us != null && us.length > 0) {
                    for (String u : us) {
                        URL url = URL.valueOf(u);
                        if (url.getPath() == null || url.getPath().length() == 0) {
                            url = url.setPath(interfaceName);
                        }
                        if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                            urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                        } else {
                            urls.add(ClusterUtils.mergeUrl(url, map));
                        }
                    }
                }
            } else {
              	// 到了这个地方,就是说是需要从注册中心获取服务提供者的调用地址了,
              	// 获取注册中心地址
                List<URL> us = loadRegistries(false);
                if (us != null && !us.isEmpty()) {
                    for (URL u : us) {
                        URL monitorUrl = loadMonitor(u);
                        if (monitorUrl != null) {
                            map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
                        }
                      	// 构建注册中心的URL ,如:registry://xxxxx这种。
                        urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                    }
                }
                if (urls == null || urls.isEmpty()) {
                    throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
                }
            }
			
            if (urls.size() == 1) {
              	// 一般来说,就是一个注册中心的地址,走这里,调用refer方法
                invoker = refprotocol.refer(interfaceClass, urls.get(0));
            } else {
              	// 多个注册中心的,目前暂未去了解
                List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
                URL registryURL = null;
                for (URL url : urls) {
                    invokers.add(refprotocol.refer(interfaceClass, url));
                    if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                        registryURL = url; // use last registry url
                    }
                }
                if (registryURL != null) { // registry url is available
                    // use AvailableCluster only when register's cluster is available
                    URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
                    invoker = cluster.join(new StaticDirectory(u, invokers));
                } else { // not a registry url
                    invoker = cluster.join(new StaticDirectory(invokers));
                }
            }
        }
		// 是否启动时检查
        Boolean c = check;
        if (c == null && consumer != null) {
            c = consumer.isCheck();
        }
        if (c == null) {
            c = true; // 如果没有设置,那么就会启动启动时检查
        }
        if (c && !invoker.isAvailable()) {
          	// 如果服务提供者不可用,那么会直接报错
            throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
        }
        if (logger.isInfoEnabled()) {
            logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
        }
        // 获取服务引用的代理类、
        return (T) proxyFactory.getProxy(invoker);
    }

说明:

上面的核心代码主要是在下面这一行代码,连接注册中心,集群容错模式,连接dubbo服务提供者,建立长连接,生成服务引用对象。

invoker = refprotocol.refer(interfaceClass, urls.get(0));

refprotocol的定义如下

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

如果已经看了SPI拓展机制的原理的,就会知道,上面的这个代码会返回一个Protocol$Adaptive实现类,在执行该类的方法的时候,还要看传入的参数URL的协议是什么,虽然默认Protocol的协议是dubbo,但是如果url里面是其他的协议,那么就会以URL的为准

url的样例如下:

registry://localhost:2181/com.alibaba.dubbo.registry.RegistryService?application=dubbo-consumer&dubbo=2.6.2&pid=10768&refer=application%3Ddubbo-consumer%26check%3Dfalse%26dubbo%3D2.6.2%26interface%3Dcom.dubbo.service.user.UserFacadeService%26methods%3DgetUser%26pid%3D10768%26register.ip%3D192.168.59.3%26side%3Dconsumer%26timestamp%3D1540205102302&registry=zookeeper&timestamp=1540205144720

相关链接:

dubbo系列之内核SPI-Protocol扩展类生成(九)

dubbo系列之深入理解服务发布(十)

调用refer方法,他的执行链如下, 前面是哪个类的执行顺序是不确定的,但是最终都会执行到RegistryProtocol 这个类里面去。

ProtocolListenerWrapper > ProtocolFilterWrapper > QosProtocolWrapper > RegistryProtocol

RegistryProtocol

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
  		// 构建zookeeper连接地址
        url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
  		// 连接zk
        Registry registry = registryFactory.getRegistry(url);
        if (RegistryService.class.equals(type)) {
            return proxyFactory.getInvoker((T) registry, type, url);
        }

        // group="a,b" or group="*"
        Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
        String group = qs.get(Constants.GROUP_KEY);
        if (group != null && group.length() > 0) {
            if ((Constants.COMMA_SPLIT_PATTERN.split(group)).length > 1
                    || "*".equals(group)) {
                return doRefer(getMergeableCluster(), registry, type, url);
            }
        }
  		// 创建服务引用。
        return doRefer(cluster, registry, type, url);
    }

url的样例如下:

zookeeper://localhost:2181/com.alibaba.dubbo.registry.RegistryService?application=dubbo-consumer&dubbo=2.6.2&pid=10768&refer=application%3Ddubbo-consumer%26check%3Dfalse%26dubbo%3D2.6.2%26interface%3Dcom.dubbo.service.user.UserFacadeService%26methods%3DgetUser%26pid%3D10768%26register.ip%3D192.168.59.3%26side%3Dconsumer%26timestamp%3D1540205102302&timestamp=1540205144720
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
  		// 构建注册目录管理类,用来管理和zk的连接,长连接断开后,进行重试
        RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
        directory.setRegistry(registry);
        directory.setProtocol(protocol);
        // 请求参数。
        Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
  		// 需要订阅的url
        URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);
  		// consumer://192.168.59.3/com.dubbo.service.user.UserFacadeService?application=dubbo-consumer&category=
  		 //providers,configurators,routers&check=false&dubbo=2.6.2&interface=com.dubbo.service.user.UserFaca
          // deService&methods=getUser&pid=10768&side=consumer&timestamp=1540205102302
        if (!Constants.ANY_VALUE.equals(url.getServiceInterface())
                && url.getParameter(Constants.REGISTER_KEY, true)) {
            registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
                    Constants.CHECK_KEY, String.valueOf(false)));
        }
  		// 订阅
        directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
                Constants.PROVIDERS_CATEGORY
                        + "," + Constants.CONFIGURATORS_CATEGORY
                        + "," + Constants.ROUTERS_CATEGORY));
		// 集群容错功能加强,根据集群容错策略,生成新的Invoker,方便执行容错策略,这个后面会详细讲解。
        Invoker invoker = cluster.join(directory);
  		// 注册消费者,将Invoker添加到consumerInvokers里面去。
        ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
        return invoker;
    }

每个消费者注册到zk上的节点名称就是下面这个,也就是subscribeUrl , consumer://192.168.59.3/com.dubbo.service.user.UserFacadeService

下面这行代码,主要就是为了当和zk连接断开时,由于订阅了consumer://192.168.59.3/com.dubbo.service.user.UserFacadeService这个节点,所以可以监听到连接状态,可以进行重试,同时监听和服务提供者的连接状态,当和服务提供者的长连接断开时,也会进行重新连接。

directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
                Constants.PROVIDERS_CATEGORY
                        + "," + Constants.CONFIGURATORS_CATEGORY
                        + "," + Constants.ROUTERS_CATEGORY));

执行完上面的代码之后,在zk上可以看到如下目录:

总结:

本文其实主要就是介绍了一个流程,具体的细节上没有过多谈及, 总体上的流程如下:

1.初始化ReferenceBan,执行init方法

2.对消费者这边的一些参数做解析

3.判断是否需要进行本地调用,也就是JVM级别的调用。

4.生成注册中心的注册地址,执行Protocol扩展实现类的refer方法

5.在refer方法里面,最终调用到RegistryProtocol这个类里面,然后建立和zk的连接,订阅消费者的zk节点

6.生成RegistryDirectory注册管理类,监听zk的节点变化,刷新Invoker都是这个类来做的,后面会详细介绍

7.根据选择的集群容错策略,对Invoker进行代码加强

消费者引用服务这个功能还会再写一篇,下文中,主要讲解消费者和服务提供者建立长连接,和zk的连接,异步通知等。

欢迎关注公众号【sharedCode】致力于主流中间件的源码分析, 可以直接与我联系

博主个人网站:www.shared-code.com