红帽白皮书新鲜出炉!点击获取,让你的云战略更胜一筹! 了解详情
写点什么

dubbo 源码之启动过程分析

  • 2019-09-25
  • 本文字数:14215 字

    阅读完需:约 47 分钟

dubbo源码之启动过程分析

Apache Dubbo™ (incubating)是一款高性能 Java RPC 框架。在平常业务开发过程中使用的越来越频繁,同时也会遇到更多的问题。这就需要我们更多的了解一下 dubbo 源码,以便更好的处理问题。


看源码的话就会直面一个棘手的问题:不知道从哪下手,找不到切入点。所以,本文准备就 dubbo 的启动过程做一下宏观的流程分析,希望对大家有所帮助。

问题引入

用过 dubbo 的同学都知道,我们只需要在 xml 文件中配置 zk、协议、要暴露的服务等信息,发布 jar 包、然后启动 spring。我们的服务就可以被调用了。如下,我们暴露了 HelloService。启动 spring 就可以被远程调用了:


1<dubbo:service interface="com.alibaba.dubbo.demo.hello.HelloService" ref="helloService" timeout="300"  ></dubbo:service>
复制代码

直入主题

那么在 spring 容器启动的过程中,都做了什么操作才使我们的服务可以暴露出去呢?为什么 dubbo 是透明化接入应用,对应用没有任何 API 侵入的呢?

Spring 可扩展 Schema 的支持

Spring 框架从 2.0 版本开始,提供了基于 Schema 风格的 XML 扩展机制,允许开发者扩展最基本的 spring 配置文件,这样我们就可以编写自定义的 xml bean 解析器然后集成到 Spring IoC 容器中。


也就是说利用这个机制就可以把我们在 xml 文件中配置的东西实例化成对象。


使用这种机制需要通过以下几步:


  • 设计配置属性和 JavaBean

  • 编写 XSD 文件

  • 编写 NamespaceHandler 和 BeanDefinitionParser 完成解析工作

  • 编写 spring.handlers 和 spring.schemas 串联起所有部件


接着我们以 dubbo 的 provider 为例开始分析:


1<dubbo:provider registry="test_zk" version="1.0.0" iothreads="300" retries="0"/>spring启动过程中会去扫描META-INF/spring.schemas,拿到dubbo的扩展配置,然后根据配置找到META-INF/dubbo.xsd文件。
复制代码


1http://dubbo.apache.org/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsd


至于 spring 为什么会扫面 META-INF/spring.schemas 目录呢?答案在 PluggableSchemaResolver.java 中。


 1public class PluggableSchemaResolver implements EntityResolver { 2    public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas"; 3    private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class); 4    private final ClassLoader classLoader; 5    private final String schemaMappingsLocation; 6    private volatile Map<String, String> schemaMappings; 7 8    public PluggableSchemaResolver(ClassLoader classLoader) { 9        this.classLoader = classLoader;10        this.schemaMappingsLocation = "META-INF/spring.schemas";11    }12}
复制代码


dubbo.xsd 文件中定义了我们 Bean 的标签,和 Bean 中定义的字段一一对应的;


这一步 spring 会把 dubbo.xsd 文件解析成 Dom 树,在解析的自定义标签的时候, spring 会根据标签的命名空间和标签名找到一个解析器。


















provider


这个命名空间就是 targetNamespace。拿到这个参数去扫面 META-INF/spring.handlers。拿到 dubbo 配置的 handler 路径:


1 <?xml version="1.0" encoding="UTF-8" standalone="no"?>2<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"3            xmlns:beans="http://www.springframework.org/schema/beans"4            xmlns:tool="http://www.springframework.org/schema/tool"5            xmlns="http://dubbo.apache.org/schema/dubbo"6            targetNamespace="http://dubbo.apache.org/schema/dubbo">
复制代码


1 http\://dubbo.apache.org/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler
复制代码


这样就找到了 DubboNamespaceHandler,由该解析器来完成对该标签内容的解析,并返回一个 BeanDefinition 。


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


在这个过程中就会把 dubbo 自定义的 schema 配置初始化成 Bean 对象,并维护在 spring 容器中。


(深入了解 schema 机制,可参考:https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/xsd-configuration.html)

spring 事件机制

dubbo 使用到的配置信息都已经托管在 spring 容器中了,服务又是怎么暴露的呢?万事俱别,只欠东风,此时就需要一个触发 dubbo 服务启动的事件。


因为是和 spring 整合的,我们就直接定位到 dubbo-config-spring 目录下,定位发现一个类 ServiceBean。其实如果仔细留意一下 dubbo 的启动日志,通过文本搜索也是可以快速定位到这个类的。




日志


看一下它的继承体系,它继承了 ApplicationListener.这个就是 spring 的事件机制,spring 容器初始化完成之后就会触发 ServiceBean 的 onApplicationEvent 方法。这个就是整个 dubbo 服务启动的入口了。



继承体系


``` 1public void onApplicationEvent(ApplicationEvent event) { 2 if (ContextRefreshedEvent.class.getName().equals(event.getClass().getName())) { 3 if (isDelay() && ! isExported() && ! isUnexported()) { 4 if (logger.isInfoEnabled()) { 5 logger.info("The service ready on spring started. service: " + getInterface()); 6 } 7 export(); 8 } 9 }10 }```### 服务暴露从export()方法开始,才真正进入了dubbo的服务暴露流程,在这个过程中就会涉及到多协议暴露服务、注册zk、暴露本地和远程服务,获取invoker,将invoker转化成exporter等一系列操作。如同官方提供的那样:



服务暴露


接着会到 ServiceConfig.export()方法,这里面涉及到 dubbo 服务延迟暴露的一个点,delay 这个参数可以配置在或者中,目的是为了延迟注册服务时间(毫秒) ,设为-1 时,表示延迟到 Spring 容器初始化完成时暴露服务。一些特殊的场景,可以通过修改该参数来解决服务刚启动接口响应较慢的案例。



delay


ServiceConfig.doExport()主要是做一些合法性的校验工作:


  • application&registry&protocol 等有效性检查;

  • 中配置的 interface 合法性检查:接口不能为空,检查接口类型必需为接口,检查方法是否在接口中存在(checkInterfaceAndMethods);

  • 检查 xml 配置中 interface 和 ref 是否匹配(interfaceClass.isInstance(ref));

  • 有效性检查通过后,调用 doExportUrls()发布 dubbo 服务。


在 ServiceConfig.doExportUrls()方法,这里会进行多协议暴露服务,由于 dubbo 不仅支持 dubbo 协议同时还支持 http、webservice、thrift 等协议。如果我们配置的 service 需要同时提供多种服务,那么会根据不同的协议进行循环暴露。


1<dubbo:service interface="com.alibaba.dubbo.demo.hello.HelloService" ref="helloService" timeout="300" protocol="dubbo"></dubbo:service>  2<dubbo:service interface="com.alibaba.dubbo.demo.hello.HelloService" ref="helloService" timeout="300" protocol="http"></dubbo:service>
复制代码



protocol


在 doExportUrlsFor1Protocol 中会把所有的相关属性封装到 Map 中,构造 dubbo 定义的统一数据模型 URL,这个 url 会贯穿服务暴露和调用的整个流程。


1 URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);
复制代码


接着是根据参数 scope 判断服务的发布范围。服务的发布范围分为不暴露、本地暴露、远程暴露。


scope 的配置规则如下:


  • 如果配置 scope=none,不发布这个 dubbo 服务;

  • 如果配置 scope=local,只本地暴露这个 dubbo 服务;

  • 如果配置 remote,只远程暴露这个 dubbo 服务;

  • 如果不配置或者不为以上三种,既暴露本地服务,又暴露远程服务;


 1       //配置为none不暴露 2        if (! Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) { 3            if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) { 4                exportLocal(url); 5            } 6            if (! Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope) ){ 7                if (logger.isInfoEnabled()) { 8                    logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url); 9                }10                if (registryURLs != null && registryURLs.size() > 011                        && url.getParameter("register", true)) {12                    for (URL registryURL : registryURLs) {13                        url = url.addParameterIfAbsent("dynamic", registryURL.getParameter("dynamic"));14                        URL monitorUrl = loadMonitor(registryURL);15                        if (monitorUrl != null) {16                            url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());17                        }18                        if (logger.isInfoEnabled()) {19                            logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);20                        }21                        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));2223                        Exporter<?> exporter = protocol.export(invoker);24                        exporters.add(exporter);25                    }26                } else {27                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);2829                    Exporter<?> exporter = protocol.export(invoker);30                    exporters.add(exporter);31                }32            }33        }
复制代码


那么为什么会有本地暴露呢?因为在 dubbo 中我们一个服务可能既是 Provider,又是 Consumer,因此就存在他自己调用自己服务的情况,如果再通过网络去访问,就会白白增加一层网络开销。所以本地暴露和远程暴露的区别如下:


  • 本地暴露是暴露在 JVM 中,不需要网络通信;

  • 远程暴露是将 ip、端口等信息暴露给远程客户端,调用时需要网络通信。


本地暴露服务的时候 url 是以 injvm 开头的,而远程服务是以 registry 开头的,如图示:



injvm

registry

上面代码也可以看出来,本地暴露和远程暴露的本质都是是通过把拼装好的url转换成invoker,然后把invoker转换为exporter。

点开getInvoker方法:

 1 /**
 2     * create invoker.
 3     * 
 4     * @param <T>
 5     * @param proxy
 6     * @param type
 7     * @param url
 8     * @return invoker
 9     */
10    @Adaptive({Constants.PROXY_KEY})
11    <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;

这里用到了Adaptive,就会生成动态编译的Adaptive类。这个类就是getInvoker方法的具体实现。

拿到invoker之后,调用protocol.export(invoker)把invoker转换成exporter。

 1 /**
 2     * 暴露远程服务:<br>
 3     * 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();<br>
 4     * 2. export()必须是幂等的,也就是暴露同一个URL的Invoker两次,和暴露一次没有区别。<br>
 5     * 3. export()传入的Invoker由框架实现并传入,协议不需要关心。<br>
 6     * 
 7     * @param <T> 服务的类型
 8     * @param invoker 服务的执行体
 9     * @return exporter 暴露服务的引用,用于取消暴露
10     * @throws RpcException 当暴露服务出错时抛出,比如端口已占用
11     */
12    @Adaptive
13    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

到这里就是服务暴露的总流程。

netty服务启动

在invoker->exporter转换的过程中又涉及到了dubbo连接池的创建和netty的初始化。

定位到了DubboProtocol.export()方法,接着会调用openServer(url) -> reateServer(url)

DubboProtocol

下图openServer的key就是ip:port

OpenServer

createServer

在createServer方法中利用dubbo SPI机制找到NettyTransporter,new NettyServer()->doOpen().最终我们就看到boss 线程,worker 线程,和 ServerBootstrap。

NettyTransporter

doOpen

netty

到此,netty开始进行初始化,并指定了handler为nettyHandler,然后调用 bind 方法,完成端口的绑定,开启端口监听;

而 Client 在 Spring getBean 的时候,会创建 Client.当调用远程方法的时候,将数据通过 dubbo 协议编码发送到 NettyServer,然后 NettServer 收到数据后解码,并调用本地方法,并返回数据,完成一次RPC 调用。

1final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);

NettyHandler类它继承了netty框架的SimpleChannelHandler类,重写了messageReceived方法。接收到客户端请求的入口就是messageReceived方法。

1  @Override
2    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
3        NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
4        try {
5            handler.received(channel, e.getMessage());
6        } finally {
7            NettyChannel.removeChannelIfDisconnected(ctx.getChannel());
8        }
9    }

执行了handler的received方法,这个handler其实就是DubboProtocol中的requestHandler,因为在启动netty服务的时候,就将requestHandler对象一直传递到了NettyServer,再通过NettyServer类的构造函数将它保存到了NettyServer类的终极父类AbstractPeer的handler属性上,AbstractPeer类又实现了ChannelHandler接口,重写了received方法。

所以当netty框架接收到请求时执行messageReceived方法里面的handler.received(channel, e.getMessage()); ,其实执行的是AbstractPeer类的received方法,received然后里面又执行了handler.received(ch, msg); ,然后received中又调用了reply方法。

在reply方法中,完成了数据的解码,和合法性校验。最终调用本地方法,返回数据,完成一次RPC 调用。

 1private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
 2
 3        public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
 4            if (message instanceof Invocation) {
 5                Invocation inv = (Invocation) message;
 6                Invoker<?> invoker = getInvoker(channel, inv);
 7                //如果是callback 需要处理高版本调用低版本的问题
 8                if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))){
 9                    String methodsStr = invoker.getUrl().getParameters().get("methods");
10                    boolean hasMethod = false;
11                    if (methodsStr == null || methodsStr.indexOf(",") == -1){
12                        hasMethod = inv.getMethodName().equals(methodsStr);
13                    } else {
14                        String[] methods = methodsStr.split(",");
15                        for (String method : methods){
16                            if (inv.getMethodName().equals(method)){
17                                hasMethod = true;
18                                break;
19                            }
20                        }
21                    }
22                    if (!hasMethod){
23                        logger.warn(new IllegalStateException("The methodName "+inv.getMethodName()+" not found in callback service interface ,invoke will be ignored. please update the api interface. url is:" + invoker.getUrl()) +" ,invocation is :"+inv );
24                        return null;
25                    }
26                }
27                RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
28                return invoker.invoke(inv);
29            }
30            throw new RemotingException(channel, "Unsupported request: " + message == null ? null : (message.getClass().getName() + ": " + message) + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
31        }
32
33        @Override
34        public void received(Channel channel, Object message) throws RemotingException {
35            if (message instanceof Invocation) {
36                reply((ExchangeChannel) channel, message);
37            } else {
38                super.received(channel, message);
39            }
40        }

4最后

如图,dubbo的的模型十分易懂,但涉及到的东西确实很多。以上只是对第一步:0.start 做了一个简单的流水账分析。

所以,本文只是想做个引子,更多的细节还需要靠大家去挖掘。剩下的只有去debug the universe了。

作者介绍:

江白圭(企业代号名),目前负责贝壳找房java后台研发工作。

本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。

原文链接:

https://mp.weixin.qq.com/s/Vn2tErPWn2Fs52Igd0R5Qw



2019-09-25 00:002501

评论

发布
暂无评论
发现更多内容

破解双中台困局:万家数科 x StarRocks 数字化技术实践

StarRocks

大数据

字节跳动基于ClickHouse优化实践之Upsert

字节跳动数据平台

OLAP Clickhouse 数据库优化 数据库开发 数据库·

人非圣贤孰能无过,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang错误处理机制EP11

刘悦的技术博客

Go Go web go语言 Go 语言

开源一夏 | TypeScript对于Duck类型和模块命名空间的应用实战

恒山其若陋兮

开源 8月月更

Redis API——List功能实践与性能测试【Go版】

FunTester

需求子任务的数据管理提效实践

转转技术团队

开发工具 测试赋能

【Metaverse系列二】3D引擎知多少

ThingJS数字孪生引擎

元宇宙

阿里SpringBoot实战手册横空出世!从此不再是易学难精

冉然学Java

Java 编程 程序员 Spring Boot 构架

过等保费用包含哪些?大概多少钱?

行云管家

等保 等级保护 过等保

华为云SparkRTC面向低时延、大通量传输业务的技术探索

华为云开发者联盟

云计算 后端 华为云

49张图带领小伙伴们体验一把 Flowable-UI

江南一点雨

spring springboot workflow flowable

影响全彩LED显示屏质量的几个因素

Dylan

LED显示屏 全彩LED显示屏 led显示屏厂家

深度学习公式推导(1):神经元的数学公式

老崔说架构

如何成就更高远控帧率和流畅度?向日葵SADDC算法浅析

贝锐

算法 视频解码 视觉策略 远程控制

迄今为止把Mybatis讲解的最详细的PDF,图文并茂,通俗易懂

冉然学Java

Java 编程 程序员 mybatis 构架

Go-Excelize API源码阅读(十)—— SetActiveSheet(index int)

Regan Yue

Go 开源 源码阅读 8月日更 8月月更

华能 + Alluxio | 数字化浪潮下跨地域数据联邦访问与分析

Alluxio

数字化 国产化 东数西算 大数据 开源 数据编排

阿里IM技术分享(八):深度解密钉钉即时消息服务DTIM的技术设计

JackJiang

架构设计 即时通讯 im开发

深入浅出分布式事务的实现原理

清风

面试 分布式事务 后端 原理 事务

博云入选国家级专精特新「小巨人」名单!

BoCloud博云

云计算 容器 “小巨人”企业

数据说|数字经济,山东16市谁最“炫”?排行榜来了

易观分析

数字经济 山东

从这 5 个 DevOps “恐怖故事”,我们能学到什么?

SoFlu软件机器人

应用实例分析——图像检索

Geek_e369a5

图像搜索

兆骑科创创新创业服务平台,双创活动承办,企业落地孵化

兆骑科创凤阁

开源图编辑库 NebulaGraph VEditor 的设计思路分享

NebulaGraph

数据库 图数据库 知识图谱 NebulaGraph

明源云参加2022数字化转型发展高峰论坛并获多项殊荣

科技热闻

面试的朋友听我说,18个MyBatis高频知识及学习笔记,双手奉上

冉然学Java

Java 源码 分布式 mybatis 构架

得物黑科技|AR测量脚型,解决尺码烦恼

得物技术

AR

构建元宇宙概念NFT商城系统——艺术数字藏品平台源码部署

开源直播系统源码

软件开发 数字藏品软件开发 数字藏品源码出售

兆骑科创赛事承办平台,高层次人才引进,创业服务平台

兆骑科创凤阁

开源一夏 | 为什么应该参与开源项目

baiyutang

开源 架构 微服务 开源文化 CloudWeGo

dubbo源码之启动过程分析_文化 & 方法_江白圭_InfoQ精选文章