• 面试时Dubbo原理记不住?来看看《Dubbo原理浅析——从RPC本质看Dubbo》


    RPC的本质是什么?通俗地讲RPC就是要解决远程服务间的调用问题,也就是管理服务配置并提供便捷可靠高效的服务间调用。

    我们来看看dubbo的定义:dubbo是一个分布式的服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。

    通过定义,我们提出以下几个问题,并通过这几个问题来介绍DUBBO。

    1. DUBBO的实现思想(总体架构)什么?
    2. DUBBO是如何实现透明化使用的?
    3. DUBBO中服务配置与实际调用是怎么结合的(如何实现远程服务调用)?

    总体架构

    先附DUBBO官网的架构图,
    在这里插入图片描述
    简单解释下架构图,

    DUBBO分为四个模块,分别为:注册中心(Registry)、提供者(Provider)、消费者(Consumer)和监控(Monitor)。

    • 注册中心(Registry):可以是zookeeper、redis、multicast、simple(官方推荐使用Zookeeper);
    • 提供者(Provider):服务启动时,Provider引用容器中的服务,并向Registry注册服务,同时暴露服务(Consumer是直接和Provider通讯实现服务调用的)。
    • 消费者(Consumer):服务启动时,Consumer向Registry订阅服务,如果没有订阅到自己想获得的服务,它会不断的尝试订阅。新的服务注册到注册中心以后,注册中心会将这些服务通过notify到消费者。Consumer直接调用Provider提供的服务。
    • 监控(Monitor):Consumer和Provider会通过异步的方式定时向Monitor发送消息,报告服务的状态。Monitor在整个架构中是可选的,Monitor功能需要单独配置,不配置或者配置后挂掉并不会影响服务的调用。

    所以,DUBBO的实现思路是通过注册中心实现服务的动态注册与发现,Provider暴露服务,Consumer直接和Provider通讯实现服务间的调用的。

    透明化使用

    本节需要对Spring的Bean加载机制有一定的了解,如果大家感兴趣,后续我可以再详细介绍

    用过DUBBO的,应该都知道,使用DUBBO时,只需要进行一些Spring的配置即能享受DUBBO远程调用的便利。显而易见,DUBBO就是利用的Spring的扩展性,通过全Spring配置方式,使得用户能够透明化接入应用,并且对应用没有任何API侵入。

    Dubbo通过扩展Spring Schema完成自定义bean的IoC容器注入。

    • 自定义了XML Schema文件META-INF/dubbo.xsd描述自定义元素;
    • 自定义继承自org.springframework.beans.factory.xml.NamespaceHandlerSupport抽象类的DubboNamespaceHandler处理器类;
    • 自定义实现了org.springframework.beans.factory.xml.BeanDefinitionParser接口的DubboBeanDefinitionParser解析器类;

    这样,DUBBO很好地将配置转化成bean,基于Spring容器的应用能够方便地使用。

    public class DubboNamespaceHandler extends NamespaceHandlerSupport {
    
    	static {
    		Version.checkDuplicate(DubboNamespaceHandler.class);
    	}
    
    	public void init() {
    	    registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
            registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
            registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
            registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
            registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
            registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
            registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
            registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
            registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
            registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    如上,DubboNamespaceHandler实现NamespaceHandlerSupport接口,通过注册DubboBeanDefinitionParser解析器,将对应配置解析后转化成ApplicationConfig、ModuleConfig、RegistryConfig、MonitorConfig、ProviderConfig、ConsumerConfig、ProtocolConfig、AnnotationBean、ServiceBean和ReferenceBean。

    我们着重看ServiceBean和ReferenceBean,这两个bean是服务的调用bean,业务上我们调用的服务就是通过这两个bean来交互的。

    服务调用

    在看ServiceBean和ReferenceBean前,我们先看一张图,
    在这里插入图片描述

    图例说明:
    图中小方块 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor
    代表层或模块,蓝色的表示与业务有交互,绿色的表示只对 Dubbo 内部交互。 图中背景方块 Consumer, Provider,
    Registry, Monitor 代表部署逻辑拓扑节点。 图中蓝色虚线为初始化时调用,红色虚线为运行时异步调用,红色实线为运行时同步调用。
    图中只包含 RPC 的层,不包含 Remoting 的层,Remoting 整体都隐含在 Protocol 中。

    图中我们可以看出,Consumer和Provider之间是通过Protocol交互的,不错,DUBBO可以自定义Protocol完成服务调用(Protocol是什么时候初始化,什么时候调用的,可以看后续对ReferenceBean和ServiceBean介绍)。

    @SPI("dubbo")
    public interface Protocol {
    
        /**
         * 获取缺省端口,当用户没有配置端口时使用。
         * 
         * @return 缺省端口
         */
        int getDefaultPort();
    
        /**
         * 暴露远程服务:
    * 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();
    * 2. export()必须是幂等的,也就是暴露同一个URL的Invoker两次,和暴露一次没有区别。
    * 3. export()传入的Invoker由框架实现并传入,协议不需要关心。
    * * @param 服务的类型 * @param invoker 服务的执行体 * @return exporter 暴露服务的引用,用于取消暴露 * @throws RpcException 当暴露服务出错时抛出,比如端口已占用 */ @Adaptive Exporter export(Invoker invoker) throws RpcException; /** * 引用远程服务:
    * 1. 当用户调用refer()所返回的Invoker对象的invoke()方法时,协议需相应执行同URL远端export()传入的Invoker对象的invoke()方法。
    * 2. refer()返回的Invoker由协议实现,协议通常需要在此Invoker中发送远程请求。
    * 3. 当url中有设置check=false时,连接失败不能抛出异常,并内部自动恢复。
    * * @param 服务的类型 * @param type 服务的类型 * @param url 远程服务的URL地址 * @return invoker 服务的本地代理 * @throws RpcException 当连接服务提供方失败时抛出 */ @Adaptive Invoker refer(Class type, URL url) throws RpcException; /** * 释放协议:
    * 1. 取消该协议所有已经暴露和引用的服务。
    * 2. 释放协议所占用的所有资源,比如连接和端口。
    * 3. 协议在释放后,依然能暴露和引用新的服务。
    */ void destroy(); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    Protocol的接口有export方法和refer方法,export方法负责暴露远程Invoker服务,而refer方法获取远程服务的Invoker实现。

    这里,Invoker是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。

    public interface Invoker extends Node {
    
        /**
         * get service interface.
         * 
         * @return service interface.
         */
        Class getInterface();
    
        /**
         * invoke.
         * 
         * @param invocation
         * @return result
         * @throws RpcException
         */
        Result invoke(Invocation invocation) throws RpcException;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    Invoker接口中的invoke方法需要一个Invocation类型的参数,很容易想象,Invocation是就是会话域,它持有调用过程中的变量,比如方法名,参数等。

    Protocol、Invoker和Invocation是Dubbo中有3个很重要的概念,一切都是围绕这三个展开的。

    现在我们继续来看ServiceBean和ReferenceBean。

    配置与实际调用的关联

    Consumer之ReferenceBean

    在这里插入图片描述
    ReferenceBean实现了FactoryBean接口,用于根据加载的配置(xml中dubbo:reference节点的配置或者@Reference注解的配置)创建bean实例。创建实例的过程就是创建引用接口的代理,ReferenceConfig中有个ref属性用于引用该接口的代理。

    public class ReferenceBean extends ReferenceConfig implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean {
        public Object getObject() throws Exception {
            return get();
        }
    }
    
    public class ReferenceConfig extends AbstractReferenceConfig {
    
        private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
    
        // 接口代理类引用
        private transient volatile T ref;
        private transient volatile Invoker invoker;
    
        public synchronized T get() {
            if (destroyed){
                throw new IllegalStateException("Already destroyed!");
            }
            if (ref == null) {
                init();
            }
            return ref;
        }
    
        private void init() {
            ...
        // 创建引用的代理,这里的map是一些配置信息,如interface、methods、retried、application、dubbo(版本)等信息
            ref = createProxy(map);
            ...
        }
    
        private T createProxy(Map map) {
            ...
            // Protocol映射的远程服务的Invoker,有对应Protocol有对应的Invoker实现
            invoker = refprotocol.refer(interfaceClass, urls.get(0));
            ... 
            // 根据invoker创建服务代理
            return (T) proxyFactory.getProxy(invoker);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    根据代码,我们可以发现ReferenceBean在get Bean的时候就是调用ReferenceConfig的get方法,而get方法最终调用的是createProxy方法,着重看这个方法,会先通过refprotocol的refer方法获得Invoker,refprotocol实际上就是通过ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()创建的Protocol,而后再由Invoker创建该接口的代理。这样ReferenceBean中接口类代理的创建过程就很清晰了。
    在这里插入图片描述

    Provider之ServiceBean

    ServiceBean实现了ApplicationListener接口,在容器初始化后调用Protocol的export方法暴露服务。

    public class ServiceBean extends ServiceConfig implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {
    	public void onApplicationEvent(ApplicationEvent event) {
            if (ContextRefreshedEvent.class.getName().equals(event.getClass().getName())) {
            	if (isDelay() && ! isExported() && ! isUnexported()) {
                    if (logger.isInfoEnabled()) {
                        logger.info("The service ready on spring started. service: " + getInterface());
                    }
                    export();
                }
            }
        }
    }
    
    public class ServiceConfig extends AbstractServiceConfig {
    
        private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
    
        // 接口实现类引用
        private T                   ref;
        
        public synchronized void export() {
        	...
        	doExport();
        	...
        }
    
        protected synchronized void doExport() {
        	...
        	doExportUrls();
        	...
        }
    
        /**
         * 广播注册中心暴露发现服务地址
         **/
        private void doExportUrls() {
        	// 将registry配置转化成url,例如:registry://host:port/com.alibaba.dubbo.registry.RegistryService?application=xxx&pid=xxx®istry=zookeeper×tamp=xxx
            List registryURLs = loadRegistries(true);
            // 这里的protocols是List,按指定支持的协议暴露服务
            for (ProtocolConfig protocolConfig : protocols) {
                doExportUrlsFor1Protocol(protocolConfig, registryURLs);
            }
        }
        
        private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs) {
            ...
            // 将接口实体类引用转化成Invoker
            Invoker invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
    
            Exporter exporter = protocol.export(invoker);
            exporters.add(exporter);
            ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    根据代码,我们可以发现在Spring在调用onApplicationEvent方法的时候,会调用export方法,而这个方法最终会调用doExportUrlsFor1Protocol方法,方法中很重要的一段逻辑是通过ProxyFactory根据引用的接口对象和拼装的URL生成Invoker,而后通过Protocol暴露出来。Provider暴露一个服务的详细过程如下:
    在这里插入图片描述

    现在我们就可以回答Protocol是如何和服务建立联系的?

    ReferenceConfig和ServiceConfig都有一个属性Protocol,服务与Protocol间就是通过这个属性建立联系的。

    细心地你会发现ReferenceConfig和ServiceConfig的Protocol属性是static final的,我们知道Dubbo是支持多协议的,那又是怎么实现的呢?

    扩展点加载ExtensionLoader

    ReferenceConfig和ServiceConfig的Protocol属性赋值是通过ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()赋值的,深入代码,我们可以发现,它返回的是动态生成的实现Protocol接口的类,生成的类会对打了@Adaptive标注的方法重新生成方法,生成的方法调用逻辑是根据传入参数的协议号动态调用对应协议的方法。

    public class ExtensionLoader {
    	public T getAdaptiveExtension() {
            Object instance = cachedAdaptiveInstance.get();
            if (instance == null) {
                ...
                instance = createAdaptiveExtension();
                cachedAdaptiveInstance.set(instance);
                ...
            }
    
            return (T) instance;
        }
    
        private T createAdaptiveExtension() {
            try {
                return injectExtension((T) getAdaptiveExtensionClass().newInstance());
            } catch (Exception e) {
                throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
            }
        }
    
        private Class getAdaptiveExtensionClass() {
            getExtensionClasses();
            if (cachedAdaptiveClass != null) {
                return cachedAdaptiveClass;
            }
            return cachedAdaptiveClass = createAdaptiveExtensionClass();
        }
    
        private Class createAdaptiveExtensionClass() {
            String code = createAdaptiveExtensionClassCode();
            ClassLoader classLoader = findClassLoader();
            com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
            return compiler.compile(code, classLoader);
        }
    
        private String createAdaptiveExtensionClassCode() {
            StringBuilder codeBuidler = new StringBuilder();
            ...
            codeBuidler.append("package " + type.getPackage().getName() + ";");
            codeBuidler.append("
    import " + ExtensionLoader.class.getName() + ";");
            codeBuidler.append("
    public class " + type.getSimpleName() + "$Adpative" + " implements " + type.getCanonicalName() + " {");
            ...
            return codeBuidler.toString();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    附.通过ExtensionLoader动态生成的实现Protocol接口的类

    package com.alibaba.dubbo.rpc;
    
    import com.alibaba.dubbo.common.extension.ExtensionLoader;
    
    public class Protocol$Adpative 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.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0)
                                                                                        throws com.alibaba.dubbo.rpc.Invoker {
            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);
        }
    
        public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1)
                                                                                                           throws java.lang.Class {
            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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    小结

    Dubbo通过扩展Spring Schema完成自定义bean的IoC容器注入。在Spring注入和初始化bean时,通过Protocol和ProxyFactory配合完成服务的暴露和服务代理的创建。这种全Spring的配置方式,使得用户能够透明化接入应用,并且对应用没有任何API侵入。

    在Dubbo的服务实际调用中都是围绕着Protocol、Invoker和Invocation三者,这也是DUBBO的核心领域模型:

    • Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。
    • Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。
    • Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等。

    参考

    Dubbo官方文档

  • 相关阅读:
    Ubuntu24.04 LTS安装中文输入法
    C++Qt开发——动画框架、状态机框架
    JSP图书销售管理系统
    Leetcode6238-统计构造好字符串的方案数
    Linux学习笔记之设备驱动篇(2)_内核模块_理论篇
    【算法萌新闯力扣】:找到所有数组中消失对数字
    Autoware-建图
    【进阶版】 机器学习分类算法之XGBoost(集成学习算法)、LightGBM(梯度提升框架)(13)
    Spring Bean的生命周期
    不同框架实现LSTM代码及转Onnx方法
  • 原文地址:https://blog.csdn.net/m0_67391377/article/details/126620077