Dubbo服务导出原理


1. 服务导出的入口方法

        通过在服务实现类上面加上@Service注解,Dubbo可以扫描并生成两个bean对象:Spring中的beanServiceBean,然后把服务导出到zookeeper、redis等注册中心!

@Service(version = "async")
public class AsyncDemoService implements DemoService {

    @Override
    public String sayHello(String name) {
        System.out.println("sayhello方法 " + name);
        return name;
    }

        上一篇文章中通过源码,我们明白了 ServiceBean的生成过程, 那么Dubbo是如何进行服务导出的呢?ServiceBean由于实现了ApplicationListener<ContextRefreshedEvent>,发布了一个容器刷新事件,意味着在spring容器启动完毕后,会调用onApplicationEvent方法,在方法内部调用export()向注册中心暴露服务!源码如下:

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 当前服务没有被导出并且没有卸载,才导出服务
        if (!isExported() && !isUnexported()) {
            if (logger.isInfoEnabled()) {
                logger.info("The service ready on spring started. service: " + getInterface());
            }
            // 服务导出(服务注册)
            export();
        }
    }


2. 服务导出原理

服务导出:主要是指 服务提供者向注册中心上提供服务的过程
在这里插入图片描述

Dubbo的服务导出主要做以下几件事情

  1. 根据配置方式的优先级,刷新Dubbo配置参数
  2. 确定协议,生成URL
  3. 根据服务的参数信息,启动对应的网络服务器(netty、tomcat、jetty等),用来接收网络请求
  4. 将服务的信息注册到注册中心
  5. 启动监听器,监听动态配置修改

        

2.1 刷新配置参数

        一个Dubbo服务的配置参数有多种,比如version、group、delay、weight等等,这些配置参数的配置方式也有多种,而Dubbo选择配置方式的优先级从高到低如图所示,高优先级的配置覆盖低优先级的配置
在这里插入图片描述
Dubbo使用一个LinkedList集合来保存各种配置方式的配置信息,因为LinkedList是有序集合,方便后续对配置按优先级顺序进行覆盖和更新,源码如下所示:

    public CompositeConfiguration getConfiguration(String prefix, String id) {

        // 存储所有配置方式的集合:compositeConfiguration  = Linklist
        // 按添加顺序决定优先级
        CompositeConfiguration compositeConfiguration = new CompositeConfiguration();

        // JVM环境变量配置
        compositeConfiguration.addConfiguration(this.getSystemConfig(prefix, id));

        // 操作系统环境变量配置
        compositeConfiguration.addConfiguration(this.getEnvironmentConfig(prefix, id));

        // 配置中心APP配置
        compositeConfiguration.addConfiguration(this.getAppExternalConfig(prefix, id));

        // 配置中心Global配置
        compositeConfiguration.addConfiguration(this.getExternalConfig(prefix, id));

        // dubbo.properties中的配置
        compositeConfiguration.addConfiguration(this.getPropertiesConfig(prefix, id));
        return compositeConfiguration;
    }

有发现上面源码中缺失了@Service()中的配置了吗?Dubbo对@Service()上的配置优先级进行了特殊处理

  // isConfigCenterFirst()默认是true,
  if (Environment.getInstance().isConfigCenterFirst()) {
          //把@Service上的配置 放入集合中第五位,优先级位于Dubbo控制台配置之后
          compositeConfiguration.addConfiguration(4, config);
      } else {
          //把@Service上的配置 放入集合中第三位,优先级位于Dubbo控制台配置之前
          compositeConfiguration.addConfiguration(2, config);
      }

我们可以自定义isConfigCenterFirst的属性,决定@Service()上的配置优先级
在这里插入图片描述
最后遍历LinkedList集合中的元素(配置方式),进行配置信息覆盖和更新

        //遍历LinkedList集合,更新或者覆盖配置信息
        for (Configuration config : configList) {
            try {
                if (config.containsKey(key)) {
                    firstMatchingConfiguration = config;
                    break;
                }
            } catch (Exception e) {
                logger.error("Error when trying to get value for key " + key + " from " + config + ", will continue to try the next one.");
            }
        }
        if (firstMatchingConfiguration != null) {
            return firstMatchingConfiguration.getProperty(key);
        } else {
            return null;
        }

        从以上分析我们可以看出,在服务导出之前,首先得确定服务的参数。服务的参数除开来自于服务的自身配置外,还可以来自比自身优先级高的配置。所以在确定服务参数时,需要先从上级获取参数,获取之后,如果服务本身配置了相同的参数,那么则进行覆盖

        如果不同优先级之间没有出现冲突,则会互补。比如:@Service本身没有配置timeout参数,但是如果服务所属的应用的properties配置文件配置了timeout,那么这个应用下的服务都会继承这个timeout配置。

        

2.1 确定协议,生成URL

一个服务可以配置多个协议:

# 端口号不同的两个Dubbo协议
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20880
dubbo.protocols.p1.host=0.0.0.0

dubbo.protocols.p2.name=http
dubbo.protocols.p2.port=20881
dubbo.protocols.p2.host=0.0.0.0

源码中会遍历所有的协议,每一种协议导出一个单独的服务

        // 遍历每个协议
        // 一个协议一个服务
        for (ProtocolConfig protocolConfig : protocols) {
        
            // path表示服务名
            // contextPath表示应用名(可配置)
            // pathKey = group/contextpath/path:version
            // 例子:myGroup/user/org.apache.dubbo.demo.DemoService:1.0.1
            String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);

            // ProviderModel中存在服务提供者访问路径,实现类,接口,以及接口中的各个方法对应的ProviderMethodModel
            // ProviderMethodModel表示某一个方法,方法名,所属的服务的,
            ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);

            // ApplicationModel表示应用中有哪些服务提供者和引用了哪些服务
            ApplicationModel.initProviderModel(pathKey, providerModel);

            // 每种协议导出一个单独的服务,注册到各个注册中心
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }

有了确定的协议,服务名,服务参数后,自然就可以组装成服务的URL了,在doExportUrlsFor1Protocol()方法中组装服务URL
在这里插入图片描述
有了准确的服务URL之后,就可以把URL注册到注册中心上去了,在注册之前,还需要获得注册中心的URL
在这里插入图片描述
注意:上面所说的服务URL或者注册中心URL,并不是浏览器上的url连接地址,而是一个名为URL的类,类内部封装了端口号、ip、协议等注册信息,如下所示

class URL implements Serializable {

    private static final long serialVersionUID = -1985165475234910535L;

    private final String protocol;

    private final String username;

    private final String password;

    private final String host;

    private final int port;

    private final String path;
	
	//上面不够的,由parameters参数做更详细的补充!
    private final Map<String, String> parameters;

        

2.3 启动对应协议的服务器

        Dubbo在与其他组件交互的时候,会使用在服务URL中指定的协议,根据不同的协议启动不同的服务器,比如:

  • Http协议就启动TomcatJetty
  • Dubbo协议就启动Netty

不能只启动Server,还需要绑定一个RequestHandler,用来处理请求。
比如,Http协议对应的就是InternalHandler。Dubbo协议对应的就是ExchangeHandler

源码中方法如下:

    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

	
    private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker, URL providerUrl) {
        String key = getCacheKey(originInvoker);

        return (ExporterChangeableWrapper<T>) bounds.computeIfAbsent(key, s -> {
            Invoker<?> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
            // protocol属性的值是哪来的,是在SPI中注入进来的,是一个代理类
            // 这里实际利用的就是DubboProtocol或HttpProtocol去export  NettyServer
            // 为什么需要ExporterChangeableWrapper?方便注销已经被导出的服务
            return new ExporterChangeableWrapper<>((Exporter<T>) protocol.export(invokerDelegate), originInvoker);
        });
    }

	//如果是Dubbo协议最终会调用openServer方法 ,启动服务器
    // 开启NettyServer
    openServer(url);

        

2.4 向注册中心注册服务

有了服务URL 和 注册中心的地址的URL,就可以向zookeeper注册服务,注册失败的话会进行重试!

    // 注册服务,把简化后的服务提供者url注册到registryUrl中去,失败会重试
    // 通过zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
    register(registryUrl, registeredProviderUrl);

	// zk的注册服务方法
    @Override
    public void doRegister(URL url) {
        try {
        	//使用zk客户端创建节点,注册服务
            zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
        } catch (Throwable e) {
            throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

如上2.1中的配置,可以在zookeeper中生成两个服务
在这里插入图片描述

以dubbo协议的url为例,把dubbo协议的url通过编解码工具,解码后得到:

dubbo://192.168.100.1:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-provider1-application&bean.name=ServiceBean:org.apache.dubbo.demo.DemoService&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&logger=log4j&methods=sayHello&pid=12236&release=2.7.0&side=provider&timestamp=1614435347611&token=tulingtoken

可以看到注册到zookeeper上的服务其实就是这个服务配置的全部信息,其中token的主要作用是:调用服务时,为了分辨是否从zookeeper上拉取的服务!

        

2.6 监听动态配置修改

        服务在导出的过程中需要向动态配置中心的数据进行订阅,以便当管理人员修改了动态配置中心中对应服务的参数后,服务提供者能及时做出变化。

        服Dubbo如果使用的是zookeeper作为配置中心,那么服务配置就存储在zookeeper节点上,就需要利用ZookeeperWatcher机制,监听的是节点变化。所以在一个服务进行导出时,需要在服务提供者端给当前服务生成一个对应的监听器实例,这个监听器实例为OverrideListener,它负责监听对应服务的动态配置变化,并且根据动态配置中心的参数重写服务URL。

        // 一个overrideSubscribeUrl对应一个OverrideListener,用来监听变化事件,监听到overrideSubscribeUrl的变化后,
        // OverrideListener就会根据变化进行相应处理,具体处理逻辑看OverrideListener的实现
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);

当动态配置中心修改了某个服务的配置后,就会触发OverrideListener中的notify对注册中心的URL进行重写覆盖,相当于重新发布服务,实现实时发布服务

        @Override
        public synchronized void notify(List<URL> urls) {
            logger.debug("original override urls: " + urls);

            List<URL> matchedUrls = getMatchedUrls(urls, subscribeUrl.addParameter(CATEGORY_KEY,
                    CONFIGURATORS_CATEGORY));
            logger.debug("subscribe url: " + subscribeUrl + ", override urls: " + matchedUrls);

            // No matching results
            if (matchedUrls.isEmpty()) {
                return;
            }

            // 对发生了变化的url进行过滤,只取url是override协议,或者参数category等于configurators的url
            this.configurators = Configurator.toConfigurators(classifyUrls(matchedUrls, UrlUtils::isConfigurator))
                    .orElse(configurators);
            // 根据最新配置进行URL重写,并重新发布服务!
            doOverrideIfNecessary();
        }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值