Dubbo最核心功能——服务暴露的配置、使用及原理(2)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: Dubbo最核心功能——服务暴露的配置、使用及原理

三、远程暴露

1. 配置

远程暴露的配置其实与本地暴露雷同,只需将 scope 的值赋成 remote 即可,并指定使用的协议


<bean id="demoServiceTarget" class="org.apache.dubbo.samples.local.impl.DemoServiceImpl"/>
<!-- 服务提供者指定scope -->
<dubbo:service interface="org.apache.dubbo.samples.local.api.DemoService" ref="demoServiceTarget" scope="remote"/>
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:reference id="demoService" interface="org.apache.dubbo.samples.local.api.DemoService"/>

2. 实现原理

同本地暴露一样,我们仍然来看基础方法 ServiceConfig 下私有方法 exportUrl

private void exportUrl(URL url, List<URL> registryURLs) {
    String scope = url.getParameter(SCOPE_KEY);
    if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
        if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
            // 本地暴露略
        }
        // 远程暴露
        if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
            // 远程暴露获取额外设置的协议
            String extProtocol = url.getParameter("ext.protocol", "");
            List<String> protocols = new ArrayList<>();
            if (StringUtils.isNotBlank(extProtocol)) {
                // export original url
                url = URLBuilder.from(url).
                    addParameter(IS_PU_SERVER_KEY, Boolean.TRUE.toString()).
                    removeParameter("ext.protocol").
                    build();
            }
            // 远程暴露
            url = exportRemote(url, registryURLs);
            if (!isGeneric(generic) && !getScopeModel().isInternal()) {
                MetadataUtils.publishServiceDefinition(url, providerModel.getServiceModel(), getApplicationModel());
            }
            if (StringUtils.isNotBlank(extProtocol)) {
                String[] extProtocols = extProtocol.split(",", -1);
                protocols.addAll(Arrays.asList(extProtocols));
            }
            // 对于额外设定的协议,也进行暴露
            for(String protocol : protocols) {
                if(StringUtils.isNotBlank(protocol)){
                    URL localUrl = URLBuilder.from(url).
                        setProtocol(protocol).
                        build();
                    localUrl = exportRemote(localUrl, registryURLs);
                    if (!isGeneric(generic) && !getScopeModel().isInternal()) {
                        MetadataUtils.publishServiceDefinition(localUrl, providerModel.getServiceModel(), getApplicationModel());
                    }
                    this.urls.add(localUrl);
                }
            }
        }
    }
    this.urls.add(url);
}

通过上述方法,我们可以知道,在服务远程暴露的时候,可以设置额外的导出协议,这将导致一个服务将以多个协议的形式进行暴露,但不管是哪种方式,其核心方法都是调用的 exportRemote,该方法第一个参数为要暴露的服务的信息,第二个参数为各注册中心的信息,我们来看看该方法。

private URL exportRemote(URL url, List<URL> registryURLs) {
    if (CollectionUtils.isNotEmpty(registryURLs)) {
      // 遍历各注册中心
        for (URL registryURL : registryURLs) {
          // 判断是否是service-discovery-registry协议,该协议用于自省架构
            if (SERVICE_REGISTRY_PROTOCOL.equals(registryURL.getProtocol())) {
                url = url.addParameterIfAbsent(SERVICE_NAME_MAPPING_KEY, "true");
            }
            // 本地暴露不予执行
            if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
                continue;
            }
            url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
            URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);
            if (monitorUrl != null) {
                url = url.putAttribute(MONITOR_KEY, monitorUrl);
            }
            // For providers, this is used to enable custom proxy to generate invoker
            String proxy = url.getParameter(PROXY_KEY);
            if (StringUtils.isNotEmpty(proxy)) {
                registryURL = registryURL.addParameter(PROXY_KEY, proxy);
            }
            if (logger.isInfoEnabled()) {
                if (url.getParameter(REGISTER_KEY, true)) {
                    logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL.getAddress());
                } else {
                    logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
                }
            }
      // 把服务消息作为一个属性包裹在注册中心url内,并执行导出
            doExportUrl(registryURL.putAttribute(EXPORT_KEY, url), true);
        }
    } else {
        if (logger.isInfoEnabled()) {
            logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
        }
        doExportUrl(url, true);
    }
    return url;
}

可以看到,如果指定了注册中心,会把服务消息包装成在各个注册中心的URL中,我们以ZK为例,当我们向Zookeeper 进行注册时,会获得如下的注册Url


d9fe4eb718ab477cb003629c77312fec.png

并进行发布;如果没有,则执行默认的发布,方法都是 doExportUrl,这个我们在本地暴露时已经讲过,会根据协议类型来选择实现类来进行发布,此处显然就是 RegistryProtocol 作为实现类,我们直接找到其注册的位置


// RegistryProtocol
    private void register(Registry registry, URL registeredProviderUrl) {
        registry.register(registeredProviderUrl);
    }

d8b700b6828a4edaa14dea0a0fb4b4c8.png

最后则是以 ZookeeperRegistry 通过ZK客户端向Zookeeper 注册服务信息

// ZookeeperRegistry
public void doRegister(URL url) {
        try {
            checkDestroyed();
            zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true), false);
        } catch (Throwable e) {
            throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
        }
}

四、服务的注解配置

除了上面通过xml文件进行的配置,Dubbo其实更适合使用注解进行服务的启用,当然由于历代Dubbo的变化,注解也发生了变化


org.apache.dubbo.config.annotation.DubboService (Since:2.7.7)

org.apache.dubbo.config.annotation.Service (Since:2.7.0)

com.alibaba.dubbo.config.annotation.Service (阿里时期)

我们以最新的 @DubboService 来看看,其中很多字段都是与XML的配置完全一样的

f7bd28448630451992af9482a287a25d.png

当然,我们上面提到的 scope 也在其中,用法也是完全相同

7e6d89d1f721464080ef5849d8f49986.png


五、服务暴露的触发

我们前面说的服务暴露,都是以 ServiceConfig 下私有方法 exportUrl 为起点,但是当应用启动时,这个方法是怎样被触发的呢,我们现在来看这个问题。我们以结合Spring 的项目来进行研究


1. dubbo 的启动

我们在前面进行工程搭建的时候,就提到部署时需要在启动类上写上注解 @EnableDubbo,大部分三方框架都需要以类似的方式执行与Spring工程的融合,我们先来看看加上该注解的原理吧。

ba2f3d938c934e2e9646a7dc484c1f5b.png

这两个注解顾名思义,一个负责启用默认配置,一个负责加载组件。我们现在看组件部分


d305eb6978444f53a58c8d55939570cf.png

关于注解上再使用 @Import 注解,其实也是诸多框架里经常采用的导入Spring的模式了,该注解能用我们在外层注解上的值进行某些操作,在此处,就是把该目录下的组件扫描进Spring容器中


d90c86eb7ff84a00a0ff5cad4ff2f5b6.png

2. 服务识别——后置处理器

经过上述的组件扫描,我们要获得什么呢?当然是我们配置的服务或者引用能够正确的装载进Spring容器了

   // DubboComponentScanRegistrar
private void registerServiceAnnotationPostProcessor(Set<String> packagesToScan, BeanDefinitionRegistry registry) {
    // 获取 ServiceAnnotationPostProcessor 的信息,将其的Bean定义注入Spring容器
        BeanDefinitionBuilder builder = rootBeanDefinition(ServiceAnnotationPostProcessor.class);
        builder.addConstructorArgValue(packagesToScan);
        builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, registry);
}
public static String registerWithGeneratedName(AbstractBeanDefinition definition, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException {
        String generatedName = generateBeanName(definition, registry, false);
        // 注入进Spring 容器
        registry.registerBeanDefinition(generatedName, definition);
        return generatedName;
}

这里,我们需要知道,把ServiceAnnotationPostProcessor的Bean定义注入进Spring的原因是什么,因为其是一个后置处理器,它继承了BeanDefinitionRegistryPostProcessor,能负责在Bean工厂创建完成后执行操作,我们在MyBatis+Springboot 启动到SQL执行全流程 和 pringBean生成流程详解 两篇文章中都有解释,此处就不再赘述。来关注下它做了什么。

// 至今为止,Dubbo 经历过几个阶段,使用的服务注解各不相同,此处算做个兼容
private final static List<Class<? extends Annotation>> serviceAnnotationTypes = asList(
            // @since 2.7.7 Add the @DubboService , the issue : https://github.com/apache/dubbo/issues/6007
            DubboService.class,
            // @since 2.7.0 the substitute @com.alibaba.dubbo.config.annotation.Service
            Service.class,
            // @since 2.7.3 Add the compatibility for legacy Dubbo's @Service , the issue : https://github.com/apache/dubbo/issues/4330
            com.alibaba.dubbo.config.annotation.Service.class
    );
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    if (this.registry == null) {
        // In spring 3.x, may be not call postProcessBeanDefinitionRegistry()
        this.registry = (BeanDefinitionRegistry) beanFactory;
    }
    // 扫描Bean定义
    String[] beanNames = beanFactory.getBeanDefinitionNames();
    for (String beanName : beanNames) {
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
        Map<String, Object> annotationAttributes = getServiceAnnotationAttributes(beanDefinition);
        if (annotationAttributes != null) {
            // process @DubboService at java-config @bean method
            processAnnotatedBeanDefinition(beanName, (AnnotatedBeanDefinition) beanDefinition, annotationAttributes);
        }
    }
    if (!scanned) {
        // 把目录下符合条件的类定义,注册进Spring容器
        scanServiceBeans(resolvedPackagesToScan, registry);
    }
}

3. 服务暴露的时机

首先我们得知道Spring 提供的监听器机制(不了解的可以看这篇:Spring监听器用法与原理详解),知道了在Spring容器刷新完毕后的时候会发送事件 ContextRefreshedEvent

// AbstractApplicationContext.class
    protected void finishRefresh() {
        this.clearResourceCaches();
        this.initLifecycleProcessor();
        this.getLifecycleProcessor().onRefresh();
        this.publishEvent((ApplicationEvent)(new ContextRefreshedEvent(this)));
        if (!NativeDetector.inNativeImage()) {
            LiveBeansView.registerApplicationContext(this);
        }
    }

而Dubbo 正是利用了这个机制来进行启动的,Dubbo内建了一个监听器 DubboDeployApplicationListener 来监听 ContextRefreshedEvent 事件,以此为触发,进行Dubbo模块的启动

 // DubboDeployApplicationListener.class
    private void onContextRefreshedEvent(ContextRefreshedEvent event) {
        ModuleDeployer deployer = moduleModel.getDeployer();
        Assert.notNull(deployer, "Module deployer is null");
        // 启动模块
        Future future = deployer.start();
        // if the module does not start in background, await finish
        if (!deployer.isBackground()) {
            try {
                future.get();
            } catch (InterruptedException e) {
                logger.warn(CONFIG_FAILED_START_MODEL, "", "", "Interrupted while waiting for dubbo module start: " + e.getMessage());
            } catch (Exception e) {
                logger.warn(CONFIG_FAILED_START_MODEL, "", "", "An error occurred while waiting for dubbo module start: " + e.getMessage(), e);
            }
        }
    }

但是,我们还有一个问题,就是我们知道Spring的监听器必须置于Spring容器下,如果是我们自己写代码,类上加个注解@Component就可以解决,但眼下情形显然不同,==这个监听器是如何融入Spring体系并将自身加入进Spring容器的呢?==其实上面就已经提到了

acdeeeeb2b114fb19655bb2470d7a84a.png

这里的 DubboSpringInitializer.initialize(registry) 就是把Dubbo 自身的很多内容注册进Spring 容器的,其调用链路为

initialize -> initContext -> DubboBeanUtils.registerCommonBeans(registry)


static void registerCommonBeans(BeanDefinitionRegistry registry) {
    registerInfrastructureBean(registry, ServicePackagesHolder.BEAN_NAME, ServicePackagesHolder.class);
    registerInfrastructureBean(registry, ReferenceBeanManager.BEAN_NAME, ReferenceBeanManager.class);
    // Since 2.5.7 Register @Reference Annotation Bean Processor as an infrastructure Bean
    registerInfrastructureBean(registry, ReferenceAnnotationBeanPostProcessor.BEAN_NAME,
        ReferenceAnnotationBeanPostProcessor.class);
    // TODO Whether DubboConfigAliasPostProcessor can be removed ?
    // Since 2.7.4 [Feature] https://github.com/apache/dubbo/issues/5093
    registerInfrastructureBean(registry, DubboConfigAliasPostProcessor.BEAN_NAME,
        DubboConfigAliasPostProcessor.class);
    // 注册监听器,其中就包含了我们这次提到的 DubboDeployApplicationListener
    registerInfrastructureBean(registry, DubboDeployApplicationListener.class.getName(), DubboDeployApplicationListener.class);
    registerInfrastructureBean(registry, DubboConfigApplicationListener.class.getName(), DubboConfigApplicationListener.class);
    // Since 2.7.6 Register DubboConfigDefaultPropertyValueBeanPostProcessor as an infrastructure Bean
    registerInfrastructureBean(registry, DubboConfigDefaultPropertyValueBeanPostProcessor.BEAN_NAME,
        DubboConfigDefaultPropertyValueBeanPostProcessor.class);
    // Dubbo config initializer
    registerInfrastructureBean(registry, DubboConfigBeanInitializer.BEAN_NAME, DubboConfigBeanInitializer.class);
    // register infra bean if not exists later
    registerInfrastructureBean(registry, DubboInfraBeanRegisterPostProcessor.BEAN_NAME, DubboInfraBeanRegisterPostProcessor.class);
}

4. 与Spring结合的流程图


9585da0ace414b45ab7dccc07e827dd9.png

相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
目录
相关文章
|
1天前
|
负载均衡 监控 Dubbo
Dubbo 原理和机制详解(非常全面)
本文详细解析了 Dubbo 的核心功能、组件、架构设计及调用流程,涵盖远程方法调用、智能容错、负载均衡、服务注册与发现等内容。欢迎留言交流。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Dubbo 原理和机制详解(非常全面)
|
19天前
|
监控 Dubbo Java
dubbo学习三:springboot整合dubbo+zookeeper,并使用dubbo管理界面监控服务是否注册到zookeeper上。
这篇文章详细介绍了如何将Spring Boot与Dubbo和Zookeeper整合,并通过Dubbo管理界面监控服务注册情况。
31 0
dubbo学习三:springboot整合dubbo+zookeeper,并使用dubbo管理界面监控服务是否注册到zookeeper上。
|
3月前
|
JSON Dubbo Java
【Dubbo协议指南】揭秘高性能服务通信,选择最佳协议的终极攻略!
【8月更文挑战第24天】在分布式服务架构中,Apache Dubbo作为一款高性能的Java RPC框架,支持多种通信协议,包括Dubbo协议、HTTP协议及Hessian协议等。Dubbo协议是默认选择,采用NIO异步通讯,适用于高要求的内部服务通信。HTTP协议通用性强,利于跨语言调用;Hessian协议则在数据传输效率上有优势。选择合适协议需综合考虑性能需求、序列化方式、网络环境及安全性等因素。通过合理配置,可实现服务性能最优化及系统可靠性提升。
52 3
|
3月前
|
负载均衡 Dubbo 应用服务中间件
Dubbo服务调用过程原理
该文章主要介绍了Dubbo服务调用过程的原理,包括服务调用的主要阶段和服务调用的具体步骤。
Dubbo服务调用过程原理
|
3月前
|
缓存 Dubbo Java
Dubbo服务消费者启动与订阅原理
该文章主要介绍了Dubbo服务消费者启动与订阅的原理,包括服务消费者的启动时机、启动过程以及订阅和感知最新提供者信息的方式。
Dubbo服务消费者启动与订阅原理
|
3月前
|
C# 开发者 Windows
勇敢迈出第一步:手把手教你如何在WPF开源项目中贡献你的第一行代码,从选择项目到提交PR的全过程解析与实战技巧分享
【8月更文挑战第31天】本文指导您如何在Windows Presentation Foundation(WPF)相关的开源项目中贡献代码。无论您是初学者还是有经验的开发者,参与这类项目都能加深对WPF框架的理解并拓展职业履历。文章推荐了一些适合入门的项目如MvvmLight和MahApps.Metro,并详细介绍了从选择项目、设置开发环境到提交代码的全过程。通过具体示例,如添加按钮点击事件处理程序,帮助您迈出第一步。此外,还强调了提交Pull Request时保持专业沟通的重要性。参与开源不仅能提升技能,还能促进社区交流。
43 0
|
3月前
|
缓存 负载均衡 Dubbo
Dubbo服务集群容错原理(重要)
该文章主要介绍了Dubbo服务集群容错的原理,包括集群容错技术的概念、Dubbo中使用的集群容错技术种类及其原理。
|
6月前
|
Dubbo Java 应用服务中间件
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
|
11天前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
32 2
|
3月前
|
Dubbo Java 应用服务中间件
💥Spring Cloud Dubbo火爆来袭!微服务通信的终极利器,你知道它有多强大吗?🔥
【8月更文挑战第29天】随着信息技术的发展,微服务架构成为企业应用开发的主流模式,而高效的微服务通信至关重要。Spring Cloud Dubbo通过整合Dubbo与Spring Cloud的优势,提供高性能RPC通信及丰富的生态支持,包括服务注册与发现、负载均衡和容错机制等,简化了服务调用管理并支持多种通信协议,提升了系统的可伸缩性和稳定性,成为微服务通信领域的优选方案。开发者仅需关注业务逻辑,而无需过多关心底层通信细节,使得Spring Cloud Dubbo在未来微服务开发中将更加受到青睐。
80 0