dubbo解析-详解服务目录的原理以及代码实现

本文基于dubbo 2.7.5版本代码

一、服务目录作用

dubbo提供了服务目录的功能。下面官网对服务目录的解释。

服务目录中存储了一些和服务提供者有关的信息,通过服务目录,服务消费者可获取到服务提供者的信息,比如 ip、端口、服务协议等。通过这些信息,服务消费者就可通过 Netty 等客户端进行远程调用。

下面以服务目录的其中一个实现类RegistryDirectory介绍一下服务目录的具体作用。
服务目录在启动时从注册中心获得服务提供者的信息,建立与服务端的网络建立,启动后,监听注册中心,当服务提供者发生变化时,服务目录会跟随着变化。
消费端访问服务时,首先从服务目录中获得服务提供者列表,然后通过路由规则、负载均衡选择一个合适的服务提供者,最后访问该服务提供者,过程如下图:
在这里插入图片描述
上图中虚框里面的内容都是由RegistryDirectory完成,其基本完成了客户端启动的最核心任务。
下图是服务目录的继承关系图(图片来自官网):
在这里插入图片描述
服务目录的接口是Directory。dubbo提供了两个实现类:RegistryDirectory和StaticDirectory。下面从代码介绍其实现。

二、Node接口

Node接口是一个顶级接口,像 Registry、Monitor、Invoker 、Directory等均继承了这个接口。接口内定义了几个基本方法。

public interface Node {
	//获得URL对象,
	//以RegistryDirectory为例,
	//URL对象是由客户端配置信息组成的,IP和端口是注册中心地址,协议表示注册中心类型
    URL getUrl();
	//判断当前对象是否有效,
	//以RegistryDirectory为例,
	//如果返回false表示已经调用过destroy方法或者RegistryDirectory保存的所有服务提供者都不可用
    boolean isAvailable();
	//销毁当前对象,以RegistryDirectory为例,
	//断开与服务提供者的连接,设置是否销毁属性为true,从注册中心上删除注册信息
    void destroy();
}

三、Directory接口

Directory接口是服务目录的最上层接口。所有服务目录的实现类均要实现该接口。

public interface Directory<T> extends Node {
	//服务接口的Class对象
    Class<T> getInterface();
	//获得所有可用的服务提供者列表,一个Invoker对象代表了一个远程服务提供者
    List<Invoker<T>> list(Invocation invocation) throws RpcException;
	//与list方法类似,
	//区别是list返回的Invoker需要经过Router的过滤,不符合过滤条件的不返回
	//getAllInvokers不经过任何处理直接返回当前所有可用的服务提供者列表
    List<Invoker<T>> getAllInvokers();
}

四、NotifyListener接口

该接口是一个监听器接口,RegistryDirectory实现了该接口。dubbo客户端启动的时候,将RegistryDirectory注册为"/dubbo/服务接口全限定名/providers"目录的监听器,这里注册中心使用的是zk。入参urls是被监听目录下所有服务提供者的url。

public interface NotifyListener {

    void notify(List<URL> urls);
}

下面介绍AbstractDirectory、RegistryDirectory和StaticDirectory,由于代码比较多,只介绍核心功能的代码。

五、AbstractDirectory

AbstractDirectory的代码有删减。AbstractDirectory代码比较简单,提供了一些方法的简单实现。

public abstract class AbstractDirectory<T> implements Directory<T> {
    private final URL url;
    private volatile boolean destroyed = false;
    private volatile URL consumerUrl;
    protected RouterChain<T> routerChain;
    public AbstractDirectory(URL url) {
        this(url, null);
    }

    public AbstractDirectory(URL url, RouterChain<T> routerChain) {
        this(url, url, routerChain);
    }

    public AbstractDirectory(URL url, URL consumerUrl, RouterChain<T> routerChain) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
		//判断url的协议是不是registry或者service-discovery-registry
        if (UrlUtils.isRegistry(url)) {
            Map<String, String> queryMap = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
            this.url = url.addParameters(queryMap).removeParameter(MONITOR_KEY);
        } else {
            this.url = url;
        }
        this.consumerUrl = consumerUrl;
        //routerChain是null,下面这个设置在当前版本里面没有用处
        //routerChain表示路由器链,dubbo将多个Router对象组装为一个链,使用时顺次访问链上的每个Router对象
        setRouterChain(routerChain);
    }

    @Override
    //list方法的作用是返回当前可用的服务提供者列表,doList方法有子类实现
    public List<Invoker<T>> list(Invocation invocation) throws RpcException {
        if (destroyed) {
            throw new RpcException("Directory already destroyed .url: " + getUrl());
        }
        return doList(invocation);
    }

    public URL getUrl() {
        return url;
    }
	
    protected void addRouters(List<Router> routers) {
        routers = routers == null ? Collections.emptyList() : routers;
        routerChain.addRouters(routers);
    }
    
    //。。。。代码有删减
    
    public boolean isDestroyed() {
        return destroyed;
    }
	//销毁当前对象
    public void destroy() {
        destroyed = true;
    }
    //doList由子类实现
    protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException;
}

六、RegistryDirectory

首先来看RegistryDirectory的构造方法。

//入参serviceType是服务接口的全限定名,
//url是用于向注册中心注册使用的,所以其协议表示的是注册中心的类型,
//serviceKey也是类RegistryService,ip地址和端口都指向的注册中心。
public RegistryDirectory(Class<T> serviceType, URL url) {
        super(url);
        if (serviceType == null) {
            throw new IllegalArgumentException("service type is null.");
        }
        if (url.getServiceKey() == null || url.getServiceKey().length() == 0) {
            throw new IllegalArgumentException("registry serviceKey is null.");
        }
        this.serviceType = serviceType;
        //serviceKey = org.apache.dubbo.registry.RegistryService
        this.serviceKey = url.getServiceKey();
        //queryMap记录了客户端引用的服务的配置参数
        this.queryMap = StringUtils.parseQueryString(url.getParameterAndDecoded(REFER_KEY));
        this.overrideDirectoryUrl = this.directoryUrl = turnRegistryUrlToConsumerUrl(url);
        //组信息
        String group = directoryUrl.getParameter(GROUP_KEY, "");
        this.multiGroup = group != null && (ANY_VALUE.equals(group) || group.contains(","));
    }

创建完对象后,下面是构建路由器链:

	public void buildRouterChain(URL url) {
		//buildChain方法从url中查找router参数的配置,并根据配置加载所有的Router对象
		//最后对Router对象排序从而根据优先顺序形成一个路由器链
		//构造的路由器链作用:list方法返回服务提供者列表前,使用路由器链过滤
        this.setRouterChain(RouterChain.buildChain(url));
    }

接着要设置监听器,监听注册中心的服务提供者信息。

	public void subscribe(URL url) {
        setConsumerUrl(url);
        CONSUMER_CONFIGURATION_LISTENER.addNotifyListener(this);
        serviceConfigurationListener = new ReferenceConfigurationListener(this, url);
        //订阅的核心逻辑在下面方法中,
        //registry是访问注册中心的对象,如果注册中心是zk,那么registry便是ZookeeperRegistry
        registry.subscribe(url, this);
    }
    //下面以ZookeeperRegistry为例,介绍subscribe方法
    //下面的代码都是ZookeeperRegistry类中的
    public void subscribe(URL url, NotifyListener listener) {
        //代码删减
        try {
            //subscribe其实是ZookeeperRegistry父类的方法,
            //具体的订阅方式是在子类中实现的,doSubscribe见下面[1]
            doSubscribe(url, listener);
        } catch (Exception e) {
            //代码删减
        }
    }
    [1]
    public void doSubscribe(final URL url, final NotifyListener listener) {
        try {
            if (ANY_VALUE.equals(url.getServiceInterface())) {
                //代码删减
            } else {
                List<URL> urls = new ArrayList<>();
                //toCategoriesPath根据入参url构建一个路径,默认是"/dubbo/服务接口全限定名/providers"
                //该路径也是服务提供者注册服务信息的路径
                for (String path : toCategoriesPath(url)) {
                    ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
                    if (listeners == null) {
                        zkListeners.putIfAbsent(url, new ConcurrentHashMap<>());
                        listeners = zkListeners.get(url);
                    }
                    ChildListener zkListener = listeners.get(listener);
                    if (zkListener == null) {
                        listeners.putIfAbsent(listener, (parentPath, currentChilds) -> ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds)));
                        zkListener = listeners.get(listener);
                    }
                    //创建"/dubbo/服务接口全限定名/providers"路径,如果已经存在则不创建
                    zkClient.create(path, false);
                    //zkClient是连接注册中心的客户端,下面这行代码便是订阅zk上的指定路径,也就是设置监听器监听path路径
                    //children是从注册中心得到的服务提供者信息
                    List<String> children = zkClient.addChildListener(path, zkListener);
                    if (children != null) {
                        urls.addAll(toUrlsWithEmpty(url, path, children));
                    }
                }
                //订阅完路径后,自己调用一次通知逻辑
                //正常情况下这个方法是在注册中心被监听目录的数据发生变化时才会调用,
                //但是一般情况下,被监听目录是由服务端先创建的,然后客户端去监听,
                //客户端设置监听的时候,并不会触发notify的调用,所以客户端启动的时候要手动调用一次通知逻辑,相当于初始化
                notify(url, listener, urls);
            }
        } catch (Throwable e) {
            throw new RpcException("Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

上面代码需要手工调用一次ZookeeperRegistry的通知逻辑,也就是notify方法,该方法最后调用到RegistryDirectory的notify方法,下面是RegistryDirectory的notify方法代码。

	//该方法的入参urls中含有服务提供者的信息,urls中除了服务提供者信息之外,还有别的内容,别的内容也是根据服务提供者信息修改得到的
	//服务提供者信息是从注册中心"/dubbo/服务接口全限定名/providers"目录下得到的
	public synchronized void notify(List<URL> urls) {
		//对urls过滤并分组
        Map<String, List<URL>> categoryUrls = urls.stream()
                .filter(Objects::nonNull)
                .filter(this::isValidCategory)
                .filter(this::isNotCompatibleFor26x)
                .collect(Collectors.groupingBy(url -> {
                    if (UrlUtils.isConfigurator(url)) {
                        return CONFIGURATORS_CATEGORY;
                    } else if (UrlUtils.isRoute(url)) {
                        return ROUTERS_CATEGORY;
                    } else if (UrlUtils.isProvider(url)) {
                        return PROVIDERS_CATEGORY;
                    }
                    return "";
                }));

        List<URL> configuratorURLs = categoryUrls.getOrDefault(CONFIGURATORS_CATEGORY, Collections.emptyList());
        this.configurators = Configurator.toConfigurators(configuratorURLs).orElse(this.configurators);
		
        List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
        //检查是否有需要添加的Router对象,如果有则添加到路由器链中
        toRouters(routerURLs).ifPresent(this::addRouters);
		//对categoryUrls过滤,只要表示服务提供者信息的url对象
        List<URL> providerURLs = categoryUrls.getOrDefault(PROVIDERS_CATEGORY, Collections.emptyList());
		//如果配置AddressListener监听器,则可以对表示服务端信息的url对象再次处理
        ExtensionLoader<AddressListener> addressListenerExtensionLoader = ExtensionLoader.getExtensionLoader(AddressListener.class);
        List<AddressListener> supportedListeners = addressListenerExtensionLoader.getActivateExtension(getUrl(), (String[]) null);
        if (supportedListeners != null && !supportedListeners.isEmpty()) {
            for (AddressListener addressListener : supportedListeners) {
                providerURLs = addressListener.notify(providerURLs, getUrl(),this);
            }
        }
        refreshOverrideAndInvoker(providerURLs);
    }
    private void refreshOverrideAndInvoker(List<URL> urls) {
        overrideDirectoryUrl();
        //最关键的是refreshInvoker方法
        refreshInvoker(urls);
    }
    private void refreshInvoker(List<URL> invokerUrls) {
        Assert.notNull(invokerUrls, "invokerUrls should not be null");
        if (invokerUrls.size() == 1
                && invokerUrls.get(0) != null
                && EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
            //对于一个可用的服务提供者,if分支是不会走进来的,因为协议不是EMPTY_PROTOCOL
            //代码删减
        } else {
            this.forbidden = false; 
            Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
            if (invokerUrls == Collections.<URL>emptyList()) {
                invokerUrls = new ArrayList<>();
            }
            if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
                invokerUrls.addAll(this.cachedInvokerUrls);
            } else {
            	//cachedInvokerUrls用于记录已经建立连接的服务提供者
                this.cachedInvokerUrls = new HashSet<>();
                this.cachedInvokerUrls.addAll(invokerUrls);
            }
            if (invokerUrls.isEmpty()) {
                return;
            }
            //toInvokers方法比较复杂,我简单介绍一下其流程
            //1、因为invokerUrls存储的是服务端信息,因此首先要用客户端的配置信息覆盖
            //2、检查enabled参数是否为true,如果为false,则返回
            //3、如果enabled为true,则使用Protocol接口的实现类
            //(如果服务是dubbo协议,则实现类是DubboProtocol)建立与服务端的连接,
            //并创建Invoker对象,Invoker对象相当于一个服务提供者在本地的客户端,所有对服务的访问都通过该Invoker对象
            //4、将创建好的Invoker对象保存在Map中,并返回,Map对象的key是客户端配置覆盖invokerUrls后的字符串表示
            Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);
            //如果没有服务提供者,则打印报错信息
            if (CollectionUtils.isEmptyMap(newUrlInvokerMap)) {
                logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :" + invokerUrls.size() + ", invoker.size :0. urls :" + invokerUrls
                        .toString()));
                return;
            }
	
            List<Invoker<T>> newInvokers = Collections.unmodifiableList(new ArrayList<>(newUrlInvokerMap.values()));
            routerChain.setInvokers(newInvokers);
            //RegistryDirectory最关键的就是得到this.invoker集合,
            //后面介绍doList方法就可以看到,doList方法就是从this.invokers集合中筛选合适的服务提供者,
            //最终从集合中得到一个Invoker对象,之后调用Invoker对象的invoke方法访问远程服务
            this.invokers = multiGroup ? toMergeInvokerList(newInvokers) : newInvokers;
            this.urlInvokerMap = newUrlInvokerMap;

            try {
            	//最后是将无效的服务提供者信息删除,并调用Invoker对象的destroy方法销毁
            	//oldUrlInvokerMap是原来记录的服务提供者信息,
            	//newUrlInvokerMap是最新从注册中心获取的服务提供者信息
            	//如果服务提供者不在newUrlInvokerMap里面的,则要销毁删除
                destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
            } catch (Exception e) {
                logger.warn("destroyUnusedInvokers error. ", e);
            }
        }
    }

上面介绍了客户端启动时,RegistryDirectory所进行的一系列操作。客户端启动完毕开始服务访问时,需要调用list方法获得可用的服务提供者列表,也就是Invoker对象列表,这个list方法是在抽象类AbstractDirectory中的,子类实现了doList方法。

public List<Invoker<T>> doList(Invocation invocation) {
		//只要能从注册中心得到可用的服务提供者,forbidden就等于false
        if (forbidden) {
            // 代码删减
        }
		//如果引用多个组的服务提供者,那么不使用路由器链筛选直接返回
        if (multiGroup) {
            return this.invokers == null ? Collections.emptyList() : this.invokers;
        }

        List<Invoker<T>> invokers = null;
        try {
            //根据路由器链对this.invokers集合过滤,routerChain.route最后返回一个过滤后的Invoker集合
            invokers = routerChain.route(getConsumerUrl(), invocation);
        } catch (Throwable t) {
            logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
        }

        return invokers == null ? Collections.emptyList() : invokers;
    }

上面的doList方法会返回一个Invoker集合,之后dubbo使用负载均衡筛选出一个Invoker对象,然后调用该Invoker对象的invoke方法访问远程服务。

七、StaticDirectory

当有多个注册中心或者引用多个分组的服务时,都会使用到StaticDirectory。先来看一下源代码(下面的代码有删减):

public class StaticDirectory<T> extends AbstractDirectory<T> {
    private final List<Invoker<T>> invokers;

    //构造方法有删减

    public StaticDirectory(URL url, List<Invoker<T>> invokers, RouterChain<T> routerChain) {
        super(url == null && CollectionUtils.isNotEmpty(invokers) ? invokers.get(0).getUrl() : url, routerChain);
        if (CollectionUtils.isEmpty(invokers)) {
            throw new IllegalArgumentException("invokers == null");
        }
        this.invokers = invokers;//保存服务提供者
    }

    @Override
    public Class<T> getInterface() {
        return invokers.get(0).getInterface();//返回服务接口
    }

    @Override
    public List<Invoker<T>> getAllInvokers() {
        return invokers;
    }

    @Override
    public boolean isAvailable() {
        if (isDestroyed()) {
            return false;
        }
        for (Invoker<T> invoker : invokers) {
            if (invoker.isAvailable()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void destroy() {
        if (isDestroyed()) {
            return;
        }
        super.destroy();
        for (Invoker<T> invoker : invokers) {
            invoker.destroy();
        }
        invokers.clear();
    }
	//构造Router链
    public void buildRouterChain() {
        RouterChain<T> routerChain = RouterChain.buildChain(getUrl());
        routerChain.setInvokers(invokers);
        this.setRouterChain(routerChain);
    }

    @Override
    //根据路由器链对this.invokers集合过滤
    protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
        List<Invoker<T>> finalInvokers = invokers;
        if (routerChain != null) {
            try {
                finalInvokers = routerChain.route(getConsumerUrl(), invocation);
            } catch (Throwable t) {
                logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
            }
        }
        return finalInvokers == null ? Collections.emptyList() : finalInvokers;
    }
}

StaticDirectory类相对来说还是比较简单的。
下面介绍一下StaticDirectory如何在多注册中心和多分组情况下使用的。

1、多注册中心

先介绍一下单注册中心如何处理。
在dubbo中,一个注册中心会有唯一一个对应的RegistryDirectory对象,RegistryDirectory对象创建好后访问注册中心获取本注册中心的服务提供者信息,然后RegistryDirectory建立与服务端的连接并创建出一个Invoker集合,该集合中的每个元素都代表了一个服务提供者。也就说一个RegistryDirectory对象持有对应的注册中心上服务提供者集合。之后dubbo使用AbstractClusterInvoker的实现类封装RegistryDirectory对象,一个实现类只能封装一个对象。当访问服务时,AbstractClusterInvoker的实现类会先调用RegistryDirectory的list方法,获得可用的服务提供者。
那么当有多个注册中心时该怎么处理?
duboo首先根据每个注册中心创建出对应的RegistryDirectory对象和AbstractClusterInvoker对象,这样最终创建出一个AbstractClusterInvoker对象集合,我们看到StaticDirectory的构造方法中有一个入参是List<Invoker> invokers,这个入参就是AbstractClusterInvoker对象集合。创建好StaticDirectory对象后,dubbo也会使用一个AbstractClusterInvoker实现类将StaticDirectory封装起来。这样形成了如下关系:
在这里插入图片描述
访问服务时,dubbo先调用最外层的AbstractClusterInvoker,AbstractClusterInvoker调用StaticDirectory的list方法选择一个内层的AbstractClusterInvoker对象,之后访问内层的AbstractClusterInvoker对象,内层的AbstractClusterInvoker对象再调用RegistryDirectory的list方法选择一个最终的服务提供者,之后访问该服务提供者从远程获取信息。

2、多分组

当消费端引用多个分组的服务时,dubbo会对每个分组创建一个对应的StaticDirectory对象。我们来看一下RegistryDirectory的refreshInvoker方法,在这个方法里面调用了toMergeInvokerList,代码如下:

	//入参invokers代表了一个注册中心上的所有服务提供者
	private List<Invoker<T>> toMergeInvokerList(List<Invoker<T>> invokers) {
        List<Invoker<T>> mergedInvokers = new ArrayList<>();
        Map<String, List<Invoker<T>>> groupMap = new HashMap<>();
        //将各个服务提供者按组分类
        for (Invoker<T> invoker : invokers) {
            String group = invoker.getUrl().getParameter(GROUP_KEY, "");
            groupMap.computeIfAbsent(group, k -> new ArrayList<>());
            groupMap.get(group).add(invoker);
        }

        if (groupMap.size() == 1) {
            mergedInvokers.addAll(groupMap.values().iterator().next());
        } else if (groupMap.size() > 1) {
        	//遍历每个分组
            for (List<Invoker<T>> groupList : groupMap.values()) {
            	//每个分组创建一个StaticDirectory对象
                StaticDirectory<T> staticDirectory = new StaticDirectory<>(groupList);
                //构建路由链
                staticDirectory.buildRouterChain();
                //使用AbstractClusterInvoker实现类封装StaticDirectory对象
                mergedInvokers.add(CLUSTER.join(staticDirectory));
            }
        } else {
            mergedInvokers = invokers;
        }
        return mergedInvokers;
    }

我们综合考虑一下多个注册中心和多个分组的情况。dubbo访问服务时,会先使用StaticDirectory从多个注册中心里面选择一个合适的注册中心,然后使用RegistryDirectory从多个分组中选择一个分组,最后使用StaticDirectory从分组的服务提供者集合里面选择一个合适的服务提供者,之后调用该提供者访问远程服务。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值