# Dubbo3

\[TOC]

## 服务导出

当我们在某个接口上加上@`DubboService`后，就表示定义了一个**Dubbo**服务，启动应用的时候就会扫描到，并且解析对应的类，得到相对服务的配置信息：

1. 服务的类型
2. 服务的具体实现类
3. 服务的Version、Timeout等等(也就是`@DubboService`里面配置的信息)

解析完过后就会把这些配置信息封装成一个`ServiceConfig`对象，并调用`export()`进行服务的导出

所谓的服务导出，其实就是做三件事情：

1. 确定服务的最终参数配置
2. 按照不同的协议启动不同的服务器
3. 将服务进行注册，注册到注册中心

### 确定参数

这一个环节其实就是，确定好我们配置的参数：

* 在application里面配置的：name、protocol、timout……
* 在`@DubboService`里面配置的： version之类的接口级别的信息

最终确定出服务的各种参数

#### 服务注册

当确定好最终的配置参数过后，**Dubbo**就会根据配置信息生成对应 服务URL：

```txt
tri://192.168.65.221:20880/org.apache.dubbo.springboot.demo.DemoService?application=dubbo-springboot-demo-provider&timeout=3000
```

URL包括了服务的：ip、port、协议、服务名……

URL也确定了，是时候把这个URL存到注册中心里面去了

但是服务注册不仅仅这样简单了， 他需要注意：

* 如果配置信息改变了，消费者要如何感知到`change`呢
* 消费者要怎么知道现在他要用的某个**Dubbo**服务，是哪一个呢
* ……

#### 应用级注册

在Dubbo3.0之前，Dubbo进行的都是接口级注册，每个接口在注册中心(ZK)上都会有一个字符串

在Dubbo3.0之后，他兼容了以前的接口级注册，但同时提供了应用级的注册：

```sh
接口名1：tri://192.168.65.221:20880/接口名1?application=应用名
```

> 其中：
>
> key是接口名，value就是服务URL

那么为什么好端端的，要学SpringCloud Nacos那套，应用级注册呢？

来看看：

```sh
接口名1：tri://192.168.65.221:20880/接口名1?application=应用名 
接口名2：tri://192.168.65.221:20880/接口名2?application=应用名 
接口名3：tri://192.168.65.221:20880/接口名3?application=应用名 

接口名1：tri://192.168.65.222:20880/接口名1?application=应用名 
接口名2：tri://192.168.65.222:20880/接口名2?application=应用名 
接口名3：tri://192.168.65.222:20880/接口名3?application=应用名
```

一个分布式系统接口调用，就会存在就是说：一个应用中有3个Dubbo服务，那么每增加一个实例，就会向注册中心添加3条记录，那如果一个应用中有10个Dubbo服务，那么每增加一个实例，就会向注册中心添加10条记录，注册中i心的压力随着应用实例的增加而剧烈增加。

所以为了降低注册中心的压力，Dubbo3选择支持**应用级注册**，同时也兼容**接口级注册**，用户可逐步迁移称**应用级注册**

上面提到：

> 消费者要怎么知道现在他要用的某个**Dubbo**服务，是哪一个呢

在进行服务到处的过程中，会在ZK中存一个映射关系，在服务到导出的最后一步，`ServiceConfig`的`exported()`方法保存了这个映射关系：

```java
protected void exported() {
        exported = true;
        List<URL> exportedURLs = this.getExportedUrls();
        exportedURLs.forEach(url -> {
            if (url.getParameters().containsKey(SERVICE_NAME_MAPPING_KEY)) {
                ServiceNameMapping serviceNameMapping = ServiceNameMapping.getDefaultExtension(getScopeModel());
                ScheduledExecutorService scheduledExecutor = getScopeModel().getBeanFactory()
                    .getBean(FrameworkExecutorRepository.class).getSharedScheduledExecutor();
                mapServiceName(url, serviceNameMapping, scheduledExecutor);
            }
        });
        onExported();
    }
```

其中`mapServiceName(url, serviceNameMapping, scheduledExecutor);`保存了映射关系

消费者知道了要使用的**Dubbo**服务在哪个应用，那也就可以从注册中心中根据应用名查到应用的所有实例信息。但是在真正发送请求之前，还得知道服务的配置信息，对与消费者而言，他得知道当前要调用的这个Dubbo服务支持什么协议、timeout是多少……那么这个配置信息从上面最开始说到了，他们已经被注册到了注册中心了。

那么应用级注册是如何实现的呢？

首先，需要通过配置application里面来选择自己是要接口等级还是应用级注册，，当调用`RegistryProtocol`的`export()`方法处理`registry://`时，会利用**ZookeeperRegistry**把服务URL注册到**Zookeeper**中去，这就是接口级注册。

在`ServiceDiscoveryRegistry`里面的`doRegistry(URL url)`方法，就是**应用级注册**

```java
@Override
    public void doRegister(URL url) {
        // fixme, add registry-cluster is not necessary anymore
        url = addRegistryClusterKey(url);
        serviceDiscovery.register(url);
    }
```

这个方法做了两件事情：

* 将传进来的url进行处理
* 对处理过后的url进行注册

那么再来看看这个`register`方法吧：

```java
@Override
    public void register(URL url) {
        metadataInfo.addService(url);
    }
```

再进去：

```java
// key format is '{group}/{interface name}:{version}:{protocol}'
private final Map<String, ServiceInfo> services;

private transient ConcurrentNavigableMap<String, SortedSet<URL>> exportedServiceURLs;

public synchronized void addService(URL url) {
        // fixme, pass in application mode context during initialization of MetadataInfo.
        if (this.loader == null) {
            this.loader = url.getOrDefaultApplicationModel().getExtensionLoader(MetadataParamsFilter.class);
        }
        List<MetadataParamsFilter> filters = loader.getActivateExtension(url, "params-filter");
        // generate service level metadata
        ServiceInfo serviceInfo = new ServiceInfo(url, filters);
        this.services.put(serviceInfo.getMatchKey(), serviceInfo);
        // extract common instance level params
        extractInstanceParams(url, filters);

        if (exportedServiceURLs == null) {
            exportedServiceURLs = new ConcurrentSkipListMap<>();
        }
        addURL(exportedServiceURLs, url);
        updated = true;
    }
```

可以看出来，就是吧传进来的url构造出**ServiceInfo**对象放到**services**里面，一个**MetadataInfo**对象就存储以上的信息

前面提到过，在应用启动的最后，才会进行应用级注册，而应用级注册就是当前的应用实例上相关的信息存入注册中心， 包括：

1. 应用的名字
2. 获取与应用元数据的方式
3. 当前实例的ip和port
4. 当前实例支持的协议以及对应的端口

那么如果存在这样的一种情况呢：一个实例支持多个协议以及多个端口，呢么如何确定实例的ip和port？

就像这样：

```yaml
dubbo:
	application:
    	name: dubbo-springboot-demo-provider
    protocols:
    	p1:
    		name: dubbo
    		port: 20881
    	p2: 
    		name: dubbo
    		port: 20882
    	p3:
    		name: tri
    		port: 50051
```

如果是这样，最终存入`endpoint`中的会保证一个协议只对应一个端口，另外那个将被忽略，最终服消费者在进行服务引入的时候就会用到这个`endpoint`信息

在Dubbo2.7中就有了元数据中心，它其实就是减轻注册中心的压力的，Dubbo会把服务信息完整的存一份到元数据中心，元数据中心也可以用ZK来实现，自傲暴露完元数据服务之后，在注册实例信息到注册中心之前，就会把`MetadataInfo`存入元数据中心。

总结一下：

> 1. 在导出某个Dubbo服务URL时，会把服务URL存入MetadataInfo中(ServiceConfig)
>    1. 根据不同的协议，启动不同的服务器
>    2. 将URL存入MetadataInfo中
>    3. 所有服务都存入过后，将(接口:应用名)存入**元数据中心**
> 2. 应用级注册
>    1. Remote:将MedatadaInfo对象存入元数据中心
>    2. Local:启动**导出元数据服务**(默认dubbo协议，端口20880)
>       1. 确定实例port
>       2. 确定`dubbo.endpoints`
>       3. 确定实例编号revision
>       4. 将实例对象存入注册中心

## 服务引入

服务引入就是通过引入加了@`DubboReference`注解的类的**代理对象**

通过`ReferenceConfig`的`get()`方法得到一个当前接口的代理对象

```java
	 /**
     * The interface proxy reference
     */
    private transient volatile T ref;


	@Override
    @Transient
    public T get(boolean check) {
        if (destroyed) {
            throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
        }

        if (ref == null) {
            // ensure start module, compatible with old api usage
            getScopeModel().getDeployer().start();

            init(check);
        }

        return ref;
    }
```

```java
private T createProxy(Map<String, String> referenceParameters) {
        urls.clear();

        meshModeHandleUrl(referenceParameters);

        if (StringUtils.isNotEmpty(url)) {
            // user specified URL, could be peer-to-peer address, or register center's address.
            parseUrl(referenceParameters);
        } else {
            // if protocols not in jvm checkRegistry
            aggregateUrlFromRegistry(referenceParameters);
        }
        createInvoker();

        if (logger.isInfoEnabled()) {
            logger.info("Referred dubbo service: [" + referenceParameters.get(INTERFACE_KEY) + "]." +
                    (Boolean.parseBoolean(referenceParameters.get(GENERIC_KEY)) ?
                            " it's GenericService reference" : " it's not GenericService reference"));
        }

        URL consumerUrl = new ServiceConfigURL(CONSUMER_PROTOCOL, referenceParameters.get(REGISTER_IP_KEY), 0,
                referenceParameters.get(INTERFACE_KEY), referenceParameters);
        consumerUrl = consumerUrl.setScopeModel(getScopeModel());
        consumerUrl = consumerUrl.setServiceModel(consumerModel);
        MetadataUtils.publishServiceDefinition(consumerUrl, consumerModel.getServiceModel(), getApplicationModel());

        // create service proxy
        return (T) proxyFactory.getProxy(invoker, ProtocolUtils.isGeneric(generic));
    }
```

然后通过`RegistryProtocol.doRefer()`方法返回一个MigrationInvoker对象：

```java
protected <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url, Map<String, String> parameters) {
        Map<String, Object> consumerAttribute = new HashMap<>(url.getAttributes());
        consumerAttribute.remove(REFER_KEY);
        String p = isEmpty(parameters.get(PROTOCOL_KEY)) ? CONSUMER : parameters.get(PROTOCOL_KEY);
        URL consumerUrl = new ServiceConfigURL(
            p,
            null,
            null,
            parameters.get(REGISTER_IP_KEY),
            0, getPath(parameters, type),
            parameters,
            consumerAttribute
        );
        url = url.putAttribute(CONSUMER_URL_KEY, consumerUrl);
        ClusterInvoker<T> migrationInvoker = getMigrationInvoker(this, cluster, registry, type, url, consumerUrl);
        return interceptInvoker(migrationInvoker, url, consumerUrl);
    }
```

接着通过`MigrationRule.getStep()`方法对`step`进行赋值：

```java
public MigrationStep getStep(URL consumerURL) {
        MigrationStep value = getValue(consumerURL, SubMigrationRule::getStep);
        if (value != null) {
            return value;
        }

        /**
         * FIXME, it's really hard to follow setting default values here.
         */
        if (step == null) {
            // initial step : APPLICATION_FIRST
            step = MigrationStep.APPLICATION_FIRST;
            step = Enum.valueOf(MigrationStep.class,
                consumerURL.getParameter(MIGRATION_STEP_KEY, getDefaultStep(consumerURL, step.name())));
        }

        return step;
    }
    
```

反正最终就是得到一个基于`MigrationInvoker`的接口代理对象

## 服务调用

在引入服务之后，通过接口代理对象执行方法：执行`MigrationInvoker.invoke()`方法，去除`currentAvailableInvoker`属性对应的`ClusterInvoker`，然后执行`ClusterInvoker.invoke`方法，最后通过负载均衡根据不同的协议发送不同的数据包


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.ysngchrn.eu.org/dubbo3.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
