Dubbo源码解析-Dubbo服务提供者_Injvm协议(一)

前言:

    根据前面两篇自定义RPC框架的文章,我们带着问题来学习Dubbo。

    学习Dubbo是如何解决这些服务调用、注册中心、序列化、集群容错等问题。

    注意:笔者使用的Dubbo版本为2.7.7,源码地址为:https://github.com/apache/dubbo;

    控制台为Dubbo-admin,源码地址为:https://github.com/apache/dubbo-admin

    本文主要集中在Dubbo服务端,使用最原始的API方式来构建一个可用服务。

1.使用API构建服务端

public class ProviderApplication {
    public static void main(String[] args) {
        // 服务实现(自定义DemoService接口)
        DemoService demoService = new DemoServiceImpl();

        // 当前应用配置
        ApplicationConfig application = new ApplicationConfig();
        application.setName("provider");

        // 连接注册中心配置
        RegistryConfig registry = new RegistryConfig();
        // 本地zookeeper作为配置中心
        registry.setAddress("zookeeper://localhost:2181");

        // 服务提供者协议配置
        ProtocolConfig protocol = new ProtocolConfig();
        // dubbo协议,并以20881端口暴露
        protocol.setName("dubbo");
        protocol.setPort(20881);

        // 服务提供者暴露服务配置
        ServiceConfig<DemoService> service = new ServiceConfig<DemoService>();
        service.setApplication(application);
        service.setRegistry(registry);
        service.setProtocol(protocol);
        service.setInterface(DemoService.class);
        service.setRef(demoService);
        service.setVersion("1.0.0");

        // 暴露及注册服务
        service.export();
    }
}

// 接口
public interface DemoService {
    String sayHello(String name);
}

    我们使用最原始的这种方式来构造一个Dubbo provider。不建议在分析源码时使用spring-dubbo等方式,因为最终还是解析相关dubbo标签然后生成这样原始的bean的方式来发起dubbo服务的。

    有些比较关键的配置类,我们一起来简单分析下。

1.1 ApplicationConfig解析

// 应用相关信息
public class ApplicationConfig extends AbstractConfig {
    // 名称
    private String name;
    // 版本
    private String version;
    
    // Java字节码编译器,用于动态类的生成,可选:jdk或javassist
    private String compiler;
    
    // 日志输出方式,可选:slf4j,jcl,log4j,log4j2,jdk,默认为slf4j
    private String logger;
    
    // 配置参数
    private Map<String, String> parameters;

    // dubbo 2.5.8 新版本增加了 QOS 模块,提供了新的 telnet 命令支持
    // 具体可参考https://dubbo.apache.org/zh/docsv2.7/user/references/qos/
    private Boolean qosEnable;
    private String qosHost;
    private Integer qosPort;
    private Boolean qosAcceptForeignIp;
    
    // 对应的注册中心信息
    private List<RegistryConfig> registries;

	// 对应的监控配置信息
	private MonitorConfig monitor;

	// 更多可参考:https://dubbo.apache.org/zh/docsv2.7/user/references/xml/dubbo-application/ 
	...
}

ApplicationConfig主要描述了该服务端应用的配置信息。

1.2 RegistryConfig

// 注册中心信息
public class RegistryConfig extends AbstractConfig {
 
    // 注册中心地址,如本地zookeeper,则address为zookeeper://localhost:2181
    private String address;
    private Integer port;
    
    // 协议名称,支持dubbo, multicast, zookeeper, redis等
    private String protocol;
    // 网络传输方式,可选mina,netty
    private String transporter;
    
    // 更多配置可参考:https://dubbo.apache.org/zh/docsv2.7/user/references/xml/dubbo-registry/ 
    ...
}

为什么需要注册中心,因为我们需要一个能够保存服务提供者信息的地方,需要能够动态的提醒消费者服务的上下线。

1.3 ProtocolConfig

// 协议配置
public class ProtocolConfig extends AbstractConfig {
 
    // 协议名称,默认为dubbo
    private String name;
    
    // 协议端口号,默认dubbo协议为20880端口
    private Integer port;
    
    // 请求及响应数据包大小限制,默认为8M
    private Integer payload;
    // 协议编码方式,默认为dubbo
    private String codec;
    // 序列化方式,默认dubbo为hessian2
    private String serialization;
    
    // 服务提供者上下文路径,为服务path的前缀
    private String contextpath;
    
    // 协议的消息派发方式,用于指定线程模型,比如:dubbo协议的all, direct, message, execution, connection等
    private String dispatcher;
    
    // 网络读写缓冲区大小,默认为8Kb
    private Integer buffer;
    
    // 该协议的服务是否注册到注册中心
    private Boolean register;
    
    // 用户可自定义线程池信息,用于在接收请求时分配线程
    // 比较简单的参数定义,具体可直接看源码注释
    private String threadpool;
    private String threadname;
    private Integer corethreads;
    private Integer threads;
    private Integer iothreads;
    private Integer queues;
    
    // 更多协议配置信息可参考:https://dubbo.apache.org/zh/docsv2.7/user/references/xml/dubbo-protocol/
    ...
    
}

Dubbo提供了很多种的协议实现方式,org.apache.dubbo.rpc.Protocol接口,所有的实现类即dubbo提供的协议。具体如下:

后续我们会仔细分析,本文我们先知道有这么多协议类型即可

1.4 ServiceConfig

public class ServiceConfig<T> extends ServiceConfigBase<T> {

    // 主要属性如下
    // 服务接口名
    protected String interfaceName;
    // 服务对象实现引用
    protected T ref;
    // 服务路径
    protected String path;
    
    // 远程服务调用超时时间(毫秒),默认为1秒
    protected Integer timeout;
    // 远程服务调用重试次数,不包括第一次调用,默认为2,则说明总共会调用3次
    protected Integer retries;

    // 服务是否动态注册,如果设为false,注册后将显示后disable状态,需人工启用,并且服务提供者停止时,也不会自动取消册,需人工禁用。
    protected Boolean dynamic = true;
    
    // 更多参数请参考https://dubbo.apache.org/zh/docsv2.7/user/references/xml/dubbo-service/
    ...
    
}

ServiceConfig本身并没有什么属性,主要就是一些方法。

我们使用ServiceConfig主要就是为了指定接口和实现类。

它的属性都在父类中提供,具体类结构图如下:

2.ServiceConfig.export()

    上面所有的配置都是为了集成到ServiceConfig中,最重要的还是ServiceConfig.export()方法。

public class ServiceConfig<T> extends ServiceConfigBase<T> {
 
    public synchronized void export() {
        ...
       	// 支持延时
        if (shouldDelay()) {
            DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
        } else {
            // 交由doExport处理
            doExport();
        }

        exported();
    }

	// doExport
	protected synchronized void doExport() {
        if (unexported) {
            throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
        }
        // 只能export一次
        if (exported) {
            return;
        }
        exported = true;

        if (StringUtils.isEmpty(path)) {
            path = interfaceName;
        }
        // 交由doExportUrls处理
        doExportUrls();
    }

	// doExportUrls
	private void doExportUrls() {
        ...
        List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);

        // 这个protocols就是我们上面ServiceConfig添加的的ProtocolConfig对象,如果添加多个,则说明当前服务支持多协议暴露
        for (ProtocolConfig protocolConfig : protocols) {
            String pathKey = URL.buildKey(getContextPath(protocolConfig)
                    .map(p -> p + "/" + path)
                    .orElse(path), group, version);
            repository.registerService(pathKey, interfaceClass);
            serviceMetadata.setServiceKey(pathKey);
            // 重点在这里
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

	// 真正的暴露服务的方法
	private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
        String name = protocolConfig.getName();
        if (StringUtils.isEmpty(name)) {
            name = DUBBO;
        }

        // 配置参数添加到map中
        Map<String, String> map = new HashMap<String, String>();
        map.put(SIDE_KEY, PROVIDER_SIDE);

        ServiceConfig.appendRuntimeParameters(map);
        ...
        MetadataReportConfig metadataReportConfig = getMetadataReportConfig();
        if (metadataReportConfig != null && metadataReportConfig.isValid()) {
            map.putIfAbsent(METADATA_KEY, REMOTE_METADATA_STORAGE_TYPE);
        }
        // 当MethodConfig不为空时,对其参数进行解析,不是本文重点,直接略过
        if (CollectionUtils.isNotEmpty(getMethods())) {
            for (MethodConfig method : getMethods()) {
                AbstractConfig.appendParameters(map, method, method.getName());
                String retryKey = method.getName() + ".retry";
                List<ArgumentConfig> arguments = method.getArguments();
                ...
            } 
        }

        // GenericService是Dubbo提供的泛化接口,用来进行泛化调用。后续会详细介绍
        if (ProtocolUtils.isGeneric(generic)) {
            map.put(GENERIC_KEY, generic);
            map.put(METHODS_KEY, ANY_VALUE);
        } else {
            ...
            String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
            if (methods.length == 0) {
                map.put(METHODS_KEY, ANY_VALUE);
            } else {
                map.put(METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
            }
        }
        ...
        // 获取当前服务在当前协议下的所暴露的ip和port
        String host = findConfigedHosts(protocolConfig, registryURLs, map);
        Integer port = findConfigedPorts(protocolConfig, name, map);
        // dubbo中的服务最终都会以URL的形式暴露出去。该URL是dubbo自定义的 
        // 就是将所有必须的参数都拼接上去
        // 在本例中url为 dubbo://192.168.xxx.xx:20881/xw.demo.DemoService?anyhost=true&application=provider&bind.ip=192.168.xxx.xx&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=xw.demo.DemoService&methods=sayHello&pid=14816&release=&revision=1.0.0&side=provider&timestamp=1627271120573&version=1.0.0
        URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
        ...
        // scope可选项为local(只暴露在本地)、remote(暴露在远程,可以对外提供服务)。默认为空,说明远程和本地都要暴露
        String scope = url.getParameter(SCOPE_KEY);
        if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
            if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
                // 暴露在本地
                exportLocal(url);
            }
            // export to remote if the config is not local (export to local only when config is local)
            if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
                if (CollectionUtils.isNotEmpty(registryURLs)) {
                    for (URL registryURL : registryURLs) {
                        ...
                        url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
                        URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);
                        if (monitorUrl != null) {
                            url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString());
                        }
                       ...
                        Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
                        DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                        // 不同的协议有不同的暴露方式
                        // 最终在这里实现
                        Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
                        exporters.add(exporter);
                    }
                } else {
                    ...
                }
               ...
            }
        }
        this.urls.add(url);
    }

}

虽然代码量比较大,但是真正有用的代码不算多,主要就是拼装URL的参数。

最终会根据ServiceConfig所拥有的ProtocolConfig,进行不同协议的暴露。

有关于服务暴露到本地和远程的逻辑,我们后续专门来说明一下。

3.dubbo-admin控制台展示

    dubbo控制台,作为服务提供者和消费者的展示平台,有助于我们对服务的分析和控制。

    笔者的控制台展示如下:

总结:

    本文主要从API示例的角度,来展示了dubbo服务提供者的的创建过程。并且分析了在创建过程中使用到的配置类,正如下图所示:

应用所属配置是provider和consumer都需要的:ApplicationConfig、RegistryConfig、MonitorConfig(监控项,非必须项)

服务提供者则需要:ProtocolConfig、ServiceConfig、ProviderConfig(相比ServiceConfig而言,ProviderConfig作为一种服务提供者的基础配置,ServiceConfig中没有配置的项则使用ProviderConfig的配置项)

本文中没有涉及更深层次的调用,只走到Protocol就结束了,后面还有很多代码,后续会继续分析。

猜你喜欢

转载自blog.csdn.net/qq_26323323/article/details/121419704