Dubbo源码阅读——服务引用

1 Dubbo consumer启动过程

Dubbo provider启动伴随的重要动作是dubbo服务的导出,也叫服务暴露。而Dubbo consumer启动框架则会进行服务引用,让consumer应用获取到自己关注的服务信息。
在dubbo中,引用服务有两种方式。第一种是使用服务直连的方式引用服务,第二种方式是基于注册中心进行引用。其中服务直连方式又包含远程服务直连引用和本地JVM服务引用。
服务直连方式引用服务适用于测试环境,实际应用场景都会使用到注册中心,基于注册中心进行引用。引用服务过程不仅包括从注册服务获取服务元数据,还包括consumer应用生成Invoker,创建代理类等步骤。

1.1 服务引用原理与过程

1.1.1 服务引用触发时机

Dubbo 服务引用的时机有两个:

  • 饿汉式:第一个是在 Spring 容器初始化ReferenceBean,会调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务
  • 懒汉式:第二个是在 ReferenceBean 对应的服务被注入到其他类中时引用。

Dubbo 使用懒汉式引用服务。如果需要使用饿汉式,可通过配置 <dubbo:reference> 的 init 属性开启。
饿汉式的开始服务引用的时机很清楚, 调用ReferenceBean的afterPropertiesSet 方法时触发,对于懒汉式,当我们的服务被注入到其他类中时,Spring 会第一时间调用 getObject 方法,并由该方法执行服务引用逻辑。(getObject 方法定义在 Spring 的 FactoryBean 接口中)下面看看服务引用的流程。

1.1.2 服务引用过程

服务引用的过程大概是以下几个步骤

  1. 参数收集和配置检查。
  2. 解析配置信息,根据URL头决定服务用的方式。有三种,本地JVM服务引用、远程服务直连、通过注册中心引用。
  3. 生成Invoker。如果有多个注册中心,多个服务提供者,这个时候会得到一组 Invoker 实例,此时需要通过集群管理类 Cluster 将多个 Invoker 合并成一个实例。
  4. 通过代理工厂类 (ProxyFactory) 为服务接口生成代理类,让代理类去调用 Invoker 逻辑。避免了 Dubbo 框架代码对业务代码的侵入。

下面用官网一张图表示服务引用的总体过程:
在这里插入图片描述

2 服务引用过程源码分析

服务引用的入口方法为 ReferenceBean 的 getObject 方法,该方法在上面说的两个时机被调用。

2.1 入口代码

getObject()

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

get方法在ReferenceBean的父类ReferenceConfig中
get()

public synchronized T get() {
    if (destroyed) {
        throw new IllegalStateException("Already destroyed!");
    }
    // 检测 ref 是否为空,为空则通过 init 方法创建
    if (ref == null) {
        // init 方法主要用于处理配置,以及调用 createProxy 生成代理类
        init();
    }
    return ref;
}

2.2 配置收集与检查

Dubbo 在引用或导出服务时,首先会对这些配置进行检查和处理,以保证配置的正确性。服务引用过程的配置解析逻辑封装在 ReferenceConfig 的 init 方法中
init()

private void init() {

	//省略。。。

    // 创建代理类
    ref = createProxy(map);

    // 根据服务名,ReferenceConfig,代理类构建 ConsumerModel,
    // 并将 ConsumerModel 存入到 ApplicationModel 中
    ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), this, ref, interfaceClass.getMethods());
    ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel);
}

init方法主要做的事情就是处理配置,原理与服务暴露时的doExport方法大同小异。由于init方法代码较长,这里具体代码就不贴出来了,但是它所做的事情需要弄清楚。
init方法做的事情比较多,按代码顺序详细分为以下几个步骤:

  1. 首先做一些检查,包括是否已经初始化、接口名合法性校验、检测 ConsumerConfig 实例是否存在,如不存在则创建一个新的实例,然后通过系统变量或 dubbo.properties 配置文件填充 ConsumerConfig 的字段、检测泛化配置,并根据配置设置 interfaceClass 的值。
  2. 从系统属性或配置文件中加载与接口名相对应的配置,并将解析结果赋值给 url 字段。当然,系统属性优先级高于配置文件。
  3. 检测几个核心配置类是否为空,为空则尝试从其他配置类中获取。比如reference里的application、registries配置为空则从consumer中获取。类似service从provider里获取。
  4. 然后收集各种配置,并将配置存储到 map 中。其中用到一个appendParameters方法,这个方法时父类abstractConfig提供的,服务暴露时也用这个方法来将配置存储到map中。
  5. 然后一段代码是处理 MethodConfig 实例。该实例包含了事件通知配置,比如 onreturn、onthrow、oninvoke 等
  6. 最后解析服务消费者 ip,以及调用 createProxy 创建代理对象。

init方法处理并保存完配置信息,就可以创建Invoker代理了

2.3 判断服务引用方式

createProxy()

private T createProxy(Map<String, String> map) {
        URL tmpUrl = new URL("temp", "localhost", 0, map);
        //===============1.判断服务引用方式,组装url================
        final boolean isJvmRefer;
        if (isInjvm() == null) {
        	// if a url is specified, don't do local reference
            if (url != null && url.length() > 0) { 
                isJvmRefer = false;
            // 根据 url 的协议、scope 以及 injvm 等参数检测是否需要本地引用
        	// 比如如果用户显式配置了 scope=local,此时 isInjvmRefer 返回 true
            } else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {
                // by default, reference local service if there is
                isJvmRefer = true;
            } else {
                isJvmRefer = false;
            }
        } else {
            isJvmRefer = isInjvm().booleanValue();
        }
		//本地引用
        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 {// 远程引用
        	// url 不为空,表明用户可能想进行点对点调用,或者是注册中心地址
            if (url != null && url.length() > 0) { 
                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 { // 从注册中心的配置中装配URL
                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()));
                        }
                        urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                    }
                }
                if (urls.isEmpty()) {
                    throw new IllegalStateException("...");
                }
            }
            
			//===============2.调用对应Protocol实例的refer方法创建Invoker================
			// 单个注册中心或服务提供者(服务直连,下同)
            if (urls.size() == 1) {
                invoker = refprotocol.refer(interfaceClass, urls.get(0));
            } else {// 多个注册中心或多个服务提供者,或者两者混合
                List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
                URL registryURL = null;
                for (URL url : urls) {
                	// 通过 refprotocol 调用 refer 构建 Invoker,refprotocol 会在运行时
                	// 根据 url 协议头加载指定的 Protocol 实例,并调用实例的 refer 方法
                    invokers.add(refprotocol.refer(interfaceClass, url));
                    if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                        registryURL = url; 
                    }
                }
                if (registryURL != null) { // registry url is available
                    // 如果注册中心链接不为空,则将使用 AvailableCluster
                    URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
                    // 创建 StaticDirectory 实例,并由 Cluster 对多个 Invoker 进行合并
                    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; // default true
        }
        if (c && !invoker.isAvailable()) {
            throw new IllegalStateException("...");
        }
        // invoker 可用性检查
        if (logger.isInfoEnabled()) {
            logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
        }
        // 创建服务代理
        return (T) proxyFactory.getProxy(invoker);
    }

上面代码很多,不过逻辑比较清晰。首先根据配置判断服务引用方式,是本地JVM引用、还是远程直连、还是注册中心。若是本地调用,则调用 InjvmProtocol 的 refer 方法生成 InjvmInvoker 实例。若不是,则读取直连配置项,或注册中心 url,并将读取到的 url 存储到 urls 中。然后根据 urls 元素数量进行后续操作。若 urls 元素数量为1,则直接通过 Protocol 自适应拓展类构建 Invoker 实例接口。若 urls 元素数量大于1,即存在多个注册中心或服务直连 url,此时先根据 url 构建 Invoker。然后再通过 Cluster 合并多个 Invoker,最后调用 ProxyFactory 生成代理类。
生成服务代理类,用户就可以通过该类进行远程服务调用了。服务引用的流程就算完成了。
上面的流程还有两个重要的过程需要细看,一个是调用Protocol实例的refer方法生成Invoker,一个是proxyFactory.getProxy(invoker)生成服务代理对象

2.4 创建Invoker

Invoker 是 Dubbo 的核心模型,代表一个可执行体。
在服务提供方,Invoker 由ProxyFactory的以下方法创建,用于调用服务提供类。

    @Adaptive({Constants.PROXY_KEY})
    <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;

在服务消费方,Invoker由Protocol的以下方法创建, 用于执行远程调用。

	@Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

Protocol的实现分为两类,一种是RegistryProtocol,会先处理注册中中心相关逻辑,一种是各种PRC协议的实现,如InjvmProtocol、ThriftProtocol、以及默认使用的DubboProtocol。下面先介绍DubboProtocol的refer方法现实流程,再介绍RegistProtocol的refer方法。

2.4.1 DubboProtocol#refer()创建Invoker

refer()

	public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
        optimizeSerialization(url);
        // create rpc invoker.
        DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
        invokers.add(invoker);
        return invoker;
    }

上面代码主要是通过服务类型,url,网络通信客户端生成Invoker实例,并保存在invokers集合中。
getClients(url)获取客户端实例为ExchangeClient类型,ExchangeClient 实际上并不具备通信能力,它需要基于更底层的客户端实例进行通信。比如 NettyClient、MinaClient 等,默认情况下,Dubbo 使用 NettyClient 进行通信。
下面看看getClients的代码:
getClients()

private ExchangeClient[] getClients(URL url) {
    // 是否共享连接
    boolean service_share_connect = false;
  	// 获取连接数,默认为0,表示未配置
    int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
    // 如果未配置 connections,则共享连接
    if (connections == 0) {
        service_share_connect = true;
        connections = 1;
    }

    ExchangeClient[] clients = new ExchangeClient[connections];
    for (int i = 0; i < clients.length; i++) {
        if (service_share_connect) {
            // 获取共享客户端
            clients[i] = getSharedClient(url);
        } else {
            // 初始化新的客户端
            clients[i] = initClient(url);
        }
    }
    return clients;
}

这里根据 connections 数量决定是获取共享客户端还是创建新的客户端实例,默认情况下,使用共享客户端实例。getSharedClient 方法中也会调用 initClient 方法。

getSharedClient方法上先访问缓存,若缓存未命中,则通过 initClient 方法创建新的 ExchangeClient 实例,并将该实例传给 ReferenceCountExchangeClient 构造方法创建一个带有引用计数功能的 ExchangeClient 实例。
下面看看initClient()方法:
initClient()

private ExchangeClient initClient(URL url) {
        // 获取客户端类型,默认为 netty
        String str = url.getParameter(Constants.CLIENT_KEY, url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_CLIENT));
        url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
        // 添加编解码和心跳包参数到 url 中
        url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));

        // 检测客户端类型是否存在,不存在则抛出异常
        // 不再支持BIO,因为它会带来性能问题
        if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
            throw new RpcException("..."));
        }

        ExchangeClient client;
        try {
            // 根据配置生成懒加载或普通客户端实例
            if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) {
                client = new LazyConnectExchangeClient(url, requestHandler);
            } else {
                client = Exchangers.connect(url, requestHandler);
            }
        } catch (RemotingException e) {
            throw new RpcException("Fail to create remoting client for service...");
        }
        return client;
    }

initClient 方法首先获取用户配置的客户端类型,默认为 netty,检查该类型是否存在,然后在connect方法里,通过SPI机制根据客户端类型名称获取对应客户端实例。比如new NettyClient(url, listener);
DubboProtocol默认使用单一长连接BIO异步通信。
然后来看看RegistyProtocol的refer方法,用于处理注册中心相关的服务引用。

2.4.1 RegistyProtocol#refer()创建Invoker

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    // 取 registry 参数值,并将其设置为协议头
    url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
    // 获取注册中心实例
    Registry registry = registryFactory.getRegistry(url);
    if (RegistryService.class.equals(type)) {
        return proxyFactory.getInvoker((T) registry, type, url);
    }

    // 将 url 查询字符串转为 Map
    Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
    // 获取 group 配置
    String group = qs.get(Constants.GROUP_KEY);
    if (group != null && group.length() > 0) {
        if ((Constants.COMMA_SPLIT_PATTERN.split(group)).length > 1
                || "*".equals(group)) {
            // 通过 SPI 加载 MergeableCluster 实例,并调用 doRefer 继续执行服务引用逻辑
            return doRefer(getMergeableCluster(), registry, type, url);
        }
    }
    
    // 调用 doRefer 继续执行服务引用逻辑
    return doRefer(cluster, registry, type, url);
}

上面代码首先为 url 设置协议头,然后根据 url 参数加载注册中心实例。继续看doRefer()方法。
doRefer()

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    // 创建 RegistryDirectory 实例
    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 subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);

    // 注册服务消费者,在 consumers 目录下新节点
    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)));
    }

    // 订阅 providers、configurators、routers 等节点数据
    directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
            Constants.PROVIDERS_CATEGORY
                    + "," + Constants.CONFIGURATORS_CATEGORY
                    + "," + Constants.ROUTERS_CATEGORY));

    // 一个注册中心可能有多个服务提供者,因此这里需要将多个服务提供者合并为一个
    Invoker invoker = cluster.join(directory);
    ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
    return invoker;
}

doRefer 方法创建一个 RegistryDirectory 实例,然后生成服务者消费者链接,并向注册中心进行注册。注册完毕后,紧接着订阅 providers、configurators、routers 等节点下的数据。Cluster 将多个服务节点合并为一个,并生成一个 Invoker。
以上就是生成Invoker的逻辑,下面再看另一个服务引用的重要过程,生成代理。

2.5 创建服务代理

Invoker 创建完毕后,接下来要做的事情是为服务接口生成代理对象。有了代理对象,即可进行远程调用。
代理对象生成的入口方法为 ProxyFactory 的 getProxy方法
getProxy()

public <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException {
    Class<?>[] interfaces = null;
    // 获取接口列表
    String config = invoker.getUrl().getParameter("interfaces");
    if (config != null && config.length() > 0) {
        // 切分接口列表
        String[] types = Constants.COMMA_SPLIT_PATTERN.split(config);
        if (types != null && types.length > 0) {
            interfaces = new Class<?>[types.length + 2];
            // 设置服务接口类和 EchoService.class 到 interfaces 中
            interfaces[0] = invoker.getInterface();
            interfaces[1] = EchoService.class;
            for (int i = 0; i < types.length; i++) {
                // 加载接口类
                interfaces[i + 1] = ReflectUtils.forName(types[i]);
            }
        }
    }
    if (interfaces == null) {
        interfaces = new Class<?>[]{invoker.getInterface(), EchoService.class};
    }

    // 为 http 和 hessian 协议提供泛化调用支持,
    if (!invoker.getInterface().equals(GenericService.class) && generic) {
        int len = interfaces.length;
        Class<?>[] temp = interfaces;
        // 创建新的 interfaces 数组
        interfaces = new Class<?>[len + 1];
        System.arraycopy(temp, 0, interfaces, 0, len);
        // 设置 GenericService.class 到数组中
        interfaces[len] = GenericService.class;
    }

    // 调用重载方法
    return getProxy(invoker, interfaces);
}

以上代码在AbstractProxyFactory中,用于获取interfaces列表,最后调用的getProxy重载方法由子类实现。
javaassistProxyFactory的实现:

	public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }

JdkProxyFactory的实现:

public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new InvokerInvocationHandler(invoker));
    }

两种实现都会用到jdk动态代理,区别是javaassist实现方式是先生成Proxy.getProxy(interfaces)生成Proxy的子类,再使用jdk动态代理生成服务代理。

3 总结

最后总结一下服务引用的具体过程:

  1. 应用启动,服务引用两大入口:第一,Spring容器初始化ReferenceBean会触发ReferenceConfig的get()方法。第二,其他类型引用ReferenceBean触发ReferenceBean的getObject()方法,进而调用ReferenceConfig的get()方法。
  2. 先进行一些检查,然后执行init方法,进行配置检查。
  3. 配置检查与参数获取,判断服务引用方式
  4. <T> Invoker<T> refer(Class<T> type, URL url)方法生成Invoker,这个过程可能包含注册订阅信息的注册中心的操作
  5. (T) proxyFactory.getProxy(invoker)方法创建服务代理

有了服务代理,就可以进行远程服务调用了。服务引用完成。

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

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/103524664
今日推荐