dubbo源码分析学习--消费端的服务引用过程

源码基于2.7.2

一、服务引用大致流程

我们已经得知 Provider将自己的服务暴露出来,注册到注册中心,而 Consumer无非就是通过一波操作从注册中心得知 Provider 的信息,然后自己封装一个调用类和 Provider 进行深入地交流。

而之前的文章我都已经提到在 Dubbo中一个可执行体就是 Invoker所有调用都要向 Invoker 靠拢,因此可以推断出应该要先生成一个 Invoker,然后又因为框架需要往不侵入业务代码的方向发展,那我们的 Consumer 需要无感知的调用远程接口,因此需要搞个代理类,包装一下屏蔽底层的细节。

整体大致流程如下:

       

二、服务引入的时机

       Dubbo 服务引用的时机有两个,第一个是在 Spring 容器调用 ReferenceBean 的 afterPropertiesSet 方法时引用服务饿汉式,这个只有在配置了 \<dubbo:reference> 的 init 属性开启才会启动),第二个是在 ReferenceBean 对应的服务被注入到其他类中时引用(懒汉式。这两个引用服务的时机区别在于,第一个是饿汉式的,第二个是懒汉式的。默认情况下,Dubbo 使用懒汉式引用服务。如果需要使用饿汉式,可通过配置 \<dubbo:reference> 的 init 属性开启。

     饿汉式是通过实现 Spring 的InitializingBean接口中的 afterPropertiesSet方法,容器通过调用 ReferenceBean的 afterPropertiesSet方法时引入服务。

     懒汉式是只有当这个服务被注入到其他类中时启动引入流程,也就是说用到了才会开始服务引入。其实现是通过FactoryBean的getObject()实现

public class ReferenceBean<T> extends ReferenceConfig<T> implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean {}

三、服务引入的三种方式

服务的引入又分为了三种,第一种是本地引入、第二种是直接连接引入远程服务、第三种是通过注册中心引入远程服务。

本地引入不知道大家是否还有印象,之前服务暴露的流程每个服务都会通过搞一个本地暴露,走 injvm 协议(当然你要是 scope = remote 就没本地引用了),因为存在一个服务端既是 Provider 又是 Consumer 的情况,然后有可能自己会调用自己的服务,因此就弄了一个本地引入,这样就避免了远程网络调用的开销。

所以服务引入会先去本地缓存找找看有没有本地服务

直连远程引入服务,这个其实就是平日测试的情况下用用,不需要启动注册中心,由 Consumer 直接配置写死 Provider 的地址,然后直连即可。

注册中心引入远程服务,这个就是重点了,Consumer 通过注册中心得知 Provider 的相关信息,然后进行服务的引入,这里还包括多注册中心,同一个服务多个提供者的情况,如何抉择如何封装,如何进行负载均衡、容错并且让使用者无感知,这就是个技术活。

本文用的就是单注册中心引入远程服务,让我们来看看 Dubbo 是如何做的吧。

四、服务引入流程解析

下面看看我们调用接口 通过 @Reference 注入的是一个代理对象。

默认是懒汉式的,所以服务引入的入口就是 ReferenceBean 的 getObject 方法。

//服务引用的入口方法为 ReferenceBean 的 getObject 方法
    @Override
    public Object getObject() {
        return get();
    }

get()方法调用ReferenceConfig get()方法 返回 代理对象。

4.1、ReferenceConfig

入口:get()

 //返回一个代理对象()
    public synchronized T get() {
        checkAndUpdateSubConfigs(); //检查配置和完善配置

        if (destroyed) {
            throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
        }
        // 检测 ref 是否为空,为空则通过 init 方法创建
        if (ref == null) {
            // init 方法主要用于处理配置,以及调用 createProxy 生成代理类
            init();
        }
        return ref;
    }

4.1.1、init() 处理配置

Dubbo 提供了丰富的配置,用于调整和优化框架行为,性能等。Dubbo 在引用或导出服务时,首先会对这些配置进行检查和处理,以保证配置的正确性。配置解析逻辑封装在 ReferenceConfig 的 init 方法中。

这其实就是整个流程了,简述一下就是先检查配置,通过配置构建一个 map ,然后利用 map 来构建 URL ,再通过 URL 上的协议利用自适应扩展机制调用对应的 protocol.refer 得到相应的 invoker 。

在有多个 URL 的时候,先遍历构建出 invoker 然后再由 StaticDirectory 封装一下,然后通过 cluster 进行合并,只暴露出一个 invoker 。

然后再构建代理,封装 invoker 返回服务引用,之后 Comsumer 调用的就是这个代理类

 private void init() {
        // 避免重复初始化
        if (initialized) {
            return;
        }
        //。。。。
        //registry: 192.168.1.13:2181
        // 获取服务消费者 ip 地址
        String hostToRegistry = ConfigUtils.getSystemProperty(DUBBO_IP_TO_REGISTRY);
        if (StringUtils.isEmpty(hostToRegistry)) {
            hostToRegistry = NetUtils.getLocalHost(); //0.0.0.0
        } else if (isInvalidLocalHost(hostToRegistry)) {
            throw new IllegalArgumentException("Specified invalid registry ip from property:" + DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry);
        }
        map.put(REGISTER_IP_KEY, hostToRegistry);
        //TODO 真正意义上去构建proxy
        ref = createProxy(map); //真正意义上去构建proxy
        // 根据服务名,ReferenceConfig,代理类构建 ConsumerModel,
        // 并将 ConsumerModel 存入到 ApplicationModel 中
        String serviceKey = URL.buildKey(interfaceName, group, version);
        ApplicationModel.initConsumerModel(serviceKey, buildConsumerModel(serviceKey, attributes));
        initialized = true;
    }

init 方法很长,不过大部分就是检查配置然后将配置构建成 map ,这一大段我就不分析了,我们直接看一下构建完的 map 长什么样。

4.1.2、createProxy(Map<String, String> map)

从字面意思上来看,createProxy 似乎只是用于创建代理对象的。但实际上并非如此,该方法还会调用其他方法构建以及合并 Invoker 实例。

1、本地引用

 private T createProxy(Map<String, String> map) {
        //走本地 jvm调用
        if (shouldJvmRefer(map)) {//是否在injvm
            URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
            invoker = REF_PROTOCOL.refer(interfaceClass, url);
            if (logger.isInfoEnabled()) {
                logger.info("Using injvm service " + interfaceClass.getName());
            }
        }
   ......................... 
 }

如果是走本地的话,那么直接构建个走本地协议的 URL 然后进行服务的引入,即 refprotocol.refer,这个方法之后会做分析,本地的引入就不深入了,就是去之前服务暴露的 exporterMap 拿到服务

2、配置ip直连服务地址

3、注册中心引入远程服务了

这里拼接 URL

最终拼接出来的 URL 长这样。

下面是 注册中心 url,并将读取到的 url 存储到 urls 中。然后根据 urls 元素数量进行后续操作。若 urls 元素数量为1,则直接通过 Protocol 自适应拓展类构建 Invoker 实例接口。若 urls 元素数量大于1,即存在多个注册中心或服务直连 url,此时先根据 url 构建 Invoker。然后再通过 Cluster 合并多个 Invoker,最后调用 ProxyFactory 生成代理类 return (T) proxyFactory.getProxy(invoker);

  // 单个注册中心或服务提供者(服务直连,下同)
            if (urls.size() == 1) {
                //构建一个invoker(Protocol)
                //Protocol$Adaptive -> getExtension("registry")->Qos(listener(filter(RegisterProtol)))
                //RegisterProtocol.refer()
                // 调用 RegistryProtocol 的 refer 构建 Invoker 实例
                //Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
                // url 为:registry://118.31.58.7:2181/org.apache.dubbo.registry.RegistryService?application=practice-client
                invoker = REF_PROTOCOL.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(REF_PROTOCOL.refer(interfaceClass, url));
                    if (REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                        registryURL = url; // use last registry url
                    }
                }
                if (registryURL != null) { // registry url is available
                    // use RegistryAwareCluster only when register's CLUSTER is available
                    URL u = registryURL.addParameter(CLUSTER_KEY, RegistryAwareCluster.NAME);
                    // The invoker wrap relation would be: RegistryAwareClusterInvoker(StaticDirectory) -> FailoverClusterInvoker(RegistryDirectory, will execute route) -> Invoker
                    invoker = CLUSTER.join(new StaticDirectory(u, invokers));
                } else { // not a registry url, must be direct invoke.
                    invoker = CLUSTER.join(new StaticDirectory(invokers));
                }
            }
        }

        // create service proxy 根据invoker 构建代理对象
        return (T) PROXY_FACTORY.getProxy(invoker);

4.1.3、创建 Invoker

Invoker 是 Dubbo 的核心模型,代表一个可执行体。在服务提供方,Invoker 用于调用服务提供类。在服务消费方,Invoker 用于执行远程调用。Invoker 是由 Protocol 实现类构建而来。Protocol 实现类有很多,本节会分析最常用的两个,分别是 RegistryProtocol 和 DubboProtocol。
//构建一个invoker(Protocol)
//Protocol$Adaptive -> getExtension("registry")->Qos(listener(filter(RegisterProtol)))
//RegisterProtocol.refer()
// 调用 RegistryProtocol 的 refer 构建 Invoker 实例
//Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
// url 为:registry://118.31.58.7:2181/org.apache.dubbo.registry.RegistryService?application=practice-client
invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));

可以看到这一部分其实就是根据各种参数来组装 URL ,因为我们的自适应扩展都需要根据 URL 的参数来进行的。

从前面的截图我们可以看到此时的协议是 registry 因此走的是 RegistryProtocol#refer,我们来看一下这个方法。

4.2、RegistryProtocol

 public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        //把url registry:// 换成 zookeeper://
        url = URLBuilder.from(url)
                .setProtocol(url.getParameter(REGISTRY_KEY, DEFAULT_REGISTRY))
                .removeParameter(REGISTRY_KEY)
                .build();
        //ZookeeperRegistery 获取注册中心实例
        Registry registry = registryFactory.getRegistry(url);
        //zookeeper://
        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(REFER_KEY));
        String group = qs.get(GROUP_KEY);
        if (group != null && group.length() > 0) {
            if ((COMMA_SPLIT_PATTERN.split(group)).length > 1 || "*".equals(group)) {
                return doRefer(getMergeableCluster(), registry, type, url);
            }
        }
        //Cluster->?
        return doRefer(cluster, registry, type, url);
    }

主要就是获取注册中心实例,然后调用 doRefer 进行真正的 refer。

4.2.1、doRefer()

/**
     *以看到生成了RegistryDirectory 这个 directory 塞了注册中心实例,
     * 它自身也实现了NotifyListener 接口,因此注册中心的监听其实是靠这家伙来处理的。

     */
    private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {

        //url 为 registry://xx:2181/org.apache.dubbo.registry.RegistryService?application=practice-client
        /*********************下面主要作用*********************/
        //1. 连接到注册中心 ->curator
        //2. 从注册中心拿到地址(providerUrl)
        //3. 基于provider地址建立通信

        // 构建一个consumer://ip:port  保存到zk   provider /configurator/consumer/router
        RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
        //设置注册中心实例
        directory.setRegistry(registry); // registry -> 连接zk的api   ->获得url地址
        //设置动态生产的 Protocol$Adaptive 使用这个构建通信
        directory.setProtocol(protocol); //   protocol -> DubboProtocol()  -> 建立通信
        // all attributes of REFER_KEY
        Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
        //生产消费者服务端的 url consumer://160.6.69.112/com.gupaoedu.dubbo.practice.ISayHelloService?


        URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
        if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {
            directory.setRegisteredConsumerUrl(getRegisteredConsumerUrl(subscribeUrl, url));
            //注册中心注册消费者信息 在consumer下面创建临时节点
            registry.register(directory.getRegisteredConsumerUrl()); //consumer://
        }
        directory.buildRouterChain(subscribeUrl);
        ///TODO 跟根据 protocol 建立通信、获取地址 很关键  订阅注册中心的 providers目录、configurators 和 routers 目录
        directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
                PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));
        /*****************上面已经建立好连接了*************************/

         // proxy.sayHello() 执行 invoker去调用
        //MockClusterWrapper(FailoverCluster(directory)) TODO 重点 构建一个 invoker
        //Directory 利用cluser 封装directory
        //里面包括 客户端对服务端的连接
        Invoker invoker = cluster.join(directory);
        ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
        return invoker;
    }

这个方法很关键,可以看到生成了RegistryDirectory 这个 directory 塞了注册中心实例,它自身也实现了NotifyListener 接口,因此注册中心的监听其实是靠这家伙来处理的

然后向注册中心注册自身的信息,并且向注册中心订阅了 providers 节点、 configurators 节点 和 routers 节点,订阅了之后 RegistryDirectory 会收到这几个节点下的信息,就会触发 DubboInvoker 的生成了,即用于远程调用的 Invoker

然后通过 cluster 再包装一下得到 Invoker,因此一个服务可能有多个提供者,最终在 ProviderConsumerRegTable 中记录这些信息,然后返回 Invoker。

///TODO 跟根据 protocol 建立通信、获取地址 很关键  订阅注册中心的 providers目录、configurators 和 routers 目录
directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY, PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));

这里会触发  DubboProtocol 的 refer 。

4.3、DubboProtocol

下面先来分析 DubboProtocol 的 refer 方法源码。在新版2.7.2中 refer方法放在了父类 抽象类AbstractProtocol 中实现

 @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        return new AsyncToSyncInvoker<>(protocolBindingRefer(type, url));
    }

    @Override
    public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
        optimizeSerialization(url);

        // create rpc invoker.
        //getClients(url) 建立连接 客户端获取一个 DubboInvoker  客户端最终是一个DubboInvoker调用  doInvoke 方法
        //创建 DubboInvoker
        DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
        invokers.add(invoker);

        return invoker;
    }

这里有一个调用需要我们注意一下,即 getClients。这个方法用于获取客户端实例,实例类型为 ExchangeClient。ExchangeClient 实际上并不具备通信能力,它需要基于更底层的客户端实例进行通信。比如 NettyClient、MinaClient 等,默认情况下,Dubbo 使用 NettyClient 进行通信。接下来,我们简单看一下 getClients 方法的逻辑。

4.3.1、  getClients

    private ExchangeClient[] getClients(URL url) {
        // whether to share connection

        boolean useShareConnect = false;
        // 获取连接数,默认为0,表示未配置
        int connections = url.getParameter(CONNECTIONS_KEY, 0);
        List<ReferenceCountExchangeClient> shareClients = null;
        // 如果未配置 connections,则共享连接
        // if not configured, connection is shared, otherwise, one connection for one service
        if (connections == 0) {
            useShareConnect = true;

            /**
             * The xml configuration should have a higher priority than properties.
             */
            String shareConnectionsStr = url.getParameter(SHARE_CONNECTIONS_KEY, (String) null);
            connections = Integer.parseInt(StringUtils.isBlank(shareConnectionsStr) ? ConfigUtils.getProperty(SHARE_CONNECTIONS_KEY,
                    DEFAULT_SHARE_CONNECTIONS) : shareConnectionsStr);
            shareClients = getSharedClient(url, connections);
        }

        ExchangeClient[] clients = new ExchangeClient[connections];
        for (int i = 0; i < clients.length; i++) {
            if (useShareConnect) {
                // 获取共享客户端
                clients[i] = shareClients.get(i);

            } else {
                // 初始化新的客户端
                clients[i] = initClient(url);
            }
        }

        return clients;
    }

这里根据 connections 数量决定是获取共享客户端还是创建新的客户端实例,默认情况下,使用共享客户端实例getSharedClient 我就不分析了,就是通过远程地址找 client ,这个 client 还有引用计数的功能,如果该远程地址还没有 client 则调用 initClient,我们就来看一下 initClient 方法。

private ExchangeClient initClient(URL url) {

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

        // 检测客户端类型是否存在,不存在则抛出异常
        if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
            throw new RpcException("Unsupported client type: " + str + "," +
                    " supported client type is " + StringUtils.join(ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions(), " "));
        }

        ExchangeClient client;
        try {
            // connection should be lazy
            if (url.getParameter(LAZY_CONNECT_KEY, false)) {
                client = new LazyConnectExchangeClient(url, requestHandler);

            } else {
                // 创建普通 ExchangeClient 实例
                client = Exchangers.connect(url, requestHandler);
            }

        } catch (RemotingException e) {
            throw new RpcException("Fail to create remoting client for service(" + url + "): " + e.getMessage(), e);
        }

        return client;
    }

而这个connect最终返回 HeaderExchangeClient里面封装的是 NettyClient 。

现在我们回到 RegistryProtocol.doRefer 方法

如上,doRefer 方法创建一个 RegistryDirectory 实例,然后生成服务者消费者链接,并向注册中心进行注册。注册完毕后,紧接着订阅 providers、configurators、routers 等节点下的数据。完成订阅后,RegistryDirectory 会收到这几个节点下的子节点信息。由于一个服务可能部署在多台服务器上,这样就会在 providers 产生多个节点,这个时候就需要 Cluster 将多个服务节点合并为一个,并生成一个 Invoker。

4.4 、创建代理   ProxyFactory 的 getProxy

现在回到 ReferenceConfig 中createProxy方法中最后一把 创建代理对象。

return (T) PROXY_FACTORY.getProxy(invoker);

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

调用过程为  ProxyFactory 的 getProxy() 最后 进入JavassistProxyFactory 的getProxy() 方法

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

上面代码并不多,首先是通过 Proxy 的 getProxy 方法获取 Proxy 子类,然后创建 InvokerInvocationHandler 对象,并将该对象传给 newInstance 生成 Proxy 实例。InvokerInvocationHandler 实现 JDK 的 InvocationHandler 接口,具体的用途是拦截接口类调用。

到这里,整个流程就是分析完了,不知道大家清晰了没?我再补充前面的图,来一个完整的流程给大家再过一遍。

五、调用过程最终形式

下面是接口我们写在消费端的接口调用

@RestController
public class SayHelloController {

    //这个是 proxy代理对象
    @Reference
    ISayHelloService iSayHelloService;

    @GetMapping("/say")
    public String say(){
        return iSayHelloService.sayHello("Mic");
    }
}

 ISayHelloService iSayHelloService;是一个代理对象。如下:

下面是 生成的代理对象:

package org.apache.dubbo.common.bytecode;

public class proxy0 implements com.gupaoedu.dubbo.practice.ISayHelloService {

    public static java.lang.reflect.Method[] methods;

    private java.lang.reflect.InvocationHandler handler;

    public proxy0() {
    }

    public proxy0(java.lang.reflect.InvocationHandler arg0) {
        handler = $1;
    }

    public java.lang.String sayHello(java.lang.String arg0) {
        Object[] args = new Object[1];
        args[0] = ($w) $1;
        Object ret = handler.invoke(this, methods[0], args);
        return (java.lang.String) ret;
    }
}

然后调用调用是 InvokerInvocationHandler中的 得服务端结构。

 @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //method  调用的目标方法
        //args 目标方法的参数
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(invoker, args);
        }
        if ("toString".equals(methodName) && parameterTypes.length == 0) {
            return invoker.toString();
        }
        if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
            return invoker.hashCode();
        }
        if ("equals".equals(methodName) && parameterTypes.length == 1) {
            return invoker.equals(args[0]);
        }

        return invoker.invoke(new RpcInvocation(method, args)).recreate();
    }


 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值