dubbo源码之启动过程分析

前言

       Apache Dubbo™ (incubating)是一款高性能Java RPC框架。在平常业务开发过程中使用的越来越频繁,同时也会遇到更多的问题。这就需要我们更多的了解一下dubbo源码,以便更好的处理问题。
      看源码的话就会直面一个棘手的问题:不知道从哪下手,找不到切入点。所以,本文准备就dubbo的启动过程做一下宏观的流程分析,希望对大家有所帮助。

问题引入

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

<dubbo:service interface="com.alibaba.dubbo.demo.hello.HelloService" ref="helloService" timeout="300"  ></dubbo:service>

直入主题

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

1.Spring可扩展Schema的支持

      Spring框架从2.0版本开始,提供了基于Schema风格的XML扩展机制,允许开发者扩展最基本的spring配置文件,这样我们就可以编写自定义的xml bean解析器然后集成到Spring IoC容器中。
也就是说利用这个机制就可以把我们在xml文件中配置的东西实例化成对象。
使用这种机制需要通过以下几步:

  1. 设计配置属性和JavaBean
  2. 编写XSD文件
  3. 编写NamespaceHandler和BeanDefinitionParser完成解析工作
  4. 编写spring.handlers和spring.schemas串联起所有部件
    接着我们以dubbo的provider为例开始分析
<dubbo:provider registry="test_zk" version="1.0.0" iothreads="300" retries="0"/>

spring启动过程中会去扫描META-INF/spring.schemas,拿到dubbo的扩展配置,然后根据配置找到META-INF/dubbo.xsd文件

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

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

public class PluggableSchemaResolver implements EntityResolver {
    public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
    private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class);
    private final ClassLoader classLoader;
    private final String schemaMappingsLocation;
    private volatile Map<String, String> schemaMappings;

    public PluggableSchemaResolver(ClassLoader classLoader) {
        this.classLoader = classLoader;
        this.schemaMappingsLocation = "META-INF/spring.schemas";
    }
}

dubbo.xsd文件中定义了我们Bean的标签,和Bean中定义的字段一一对应的;
这一步spring会把dubbo.xsd文件解析成 Dom 树,在解析的自定义标签的时候, spring 会根据标签的命名空间和标签名找到一个解析器。


Provider

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

 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:beans="http://www.springframework.org/schema/beans"
            xmlns:tool="http://www.springframework.org/schema/tool"
            xmlns="http://dubbo.apache.org/schema/dubbo"
            targetNamespace="http://dubbo.apache.org/schema/dubbo">
 http\://dubbo.apache.org/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler

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

public class DubboNamespaceHandler extends NamespaceHandlerSupport {
    public DubboNamespaceHandler() {
    }

    public void init() {
        this.registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        this.registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        this.registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        this.registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        this.registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
        this.registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
        this.registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
        this.registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
        this.registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        this.registerBeanDefinitionParser("annotation", new AnnotationBeanDefinitionParser());
    }

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }

在这个过程中就会把dubbo自定义的schema配置初始化成Bean对象,并维护在spring容器中。
(深入了解schema机制 可参考:https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/xsd-configuration.html

2.spring 事件机制

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

因为是和spring整合的,我们就直接定位到dubbo-config-spring目录下,定位发现一个类ServiceBean。其实如果仔细留意一下dubbo的启动日志,通过文本搜索也是可以快速定位到这个类的。
结构图
日志.png
看一下它的继承体系,它继承了ApplicationListener.这个就是spring的事件机制,spring容器初始化完成之后就会触发ServiceBean的onApplicationEvent方法。这个就是整个dubbo服务启动的入口了。
继承体系
public void onApplicationEvent(ApplicationEvent event) {
        if (ContextRefreshedEvent.class.getName().equals(event.getClass().getName())) {
            if (isDelay() && ! isExported() && ! isUnexported()) {
                if (logger.isInfoEnabled()) {
                    logger.info("The service ready on spring started. service: " + getInterface());
                }
                export();
            }
        }
    }

3.服务暴露

从export()方法开始,才真正进入了dubbo的服务暴露流程,在这个过程中就会涉及到多协议暴露服务、注册zk、暴露本地和远程服务,获取invoker,将invoker转化成exporter等一系列操作。如同官方提供的那样:


服务暴露

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

ServiceConfig.doExport()主要是做一些合法性的校验工作:
  1. application&registry&protocol等有效性检查;
  2. <dubbo:service>中配置的interface合法性检查:接口不能为空,检查接口类型必需为接口,检查方法是否在接口中存在.(checkInterfaceAndMethods)
  3. 检查xml配置中interface和ref是否匹配(interfaceClass.isInstance(ref))
  4. 有效性检查通过后,调用doExportUrls()发布dubbo服务;
    在ServiceConfig.doExportUrls()方法,这里会进行多协议暴露服务,由于dubbo不仅支持dubbo协议同时还支持http、webservice、thrift等协议。如果我们配置的service需要同时提供多种服务,那么会根据不同的协议进行循环暴露。
<dubbo:service interface="com.alibaba.dubbo.demo.hello.HelloService" ref="helloService" timeout="300" protocol="dubbo"></dubbo:service>  
<dubbo:service interface="com.alibaba.dubbo.demo.hello.HelloService" ref="helloService" timeout="300" protocol="http"></dubbo:service>
protocol.png

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

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

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

  1. 如果配置scope=none,不发布这个dubbo服务;
  2. 如果配置scope=local,只本地暴露这个dubbo服务;
  3. 如果配置remote,只远程暴露这个dubbo服务
  4. 如果不配置或者不为以上三种,既暴露本地服务,又暴露远程服务;
       //配置为none不暴露
        if (! Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
            if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
                exportLocal(url);
            }
            if (! Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope) ){
                if (logger.isInfoEnabled()) {
                    logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
                }
                if (registryURLs != null && registryURLs.size() > 0
                        && url.getParameter("register", true)) {
                    for (URL registryURL : registryURLs) {
                        url = url.addParameterIfAbsent("dynamic", registryURL.getParameter("dynamic"));
                        URL monitorUrl = loadMonitor(registryURL);
                        if (monitorUrl != null) {
                            url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
                        }
                        if (logger.isInfoEnabled()) {
                            logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
                        }
                        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));

                        Exporter<?> exporter = protocol.export(invoker);
                        exporters.add(exporter);
                    }
                } else {
                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);

                    Exporter<?> exporter = protocol.export(invoker);
                    exporters.add(exporter);
                }
            }
        }

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

  1. 本地暴露是暴露在JVM中,不需要网络通信.
  2. 远程暴露是将ip,端口等信息暴露给远程客户端,调用时需要网络通信.

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

    registry.png

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

 /**
     * create invoker.
     * 
     * @param <T>
     * @param proxy
     * @param type
     * @param url
     * @return invoker
     */
    @Adaptive({Constants.PROXY_KEY})
    <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;

这里用到了Adaptive,就会生成动态编译的Adaptive类。这个类就是getInvoker方法的具体实现。
拿到invoker之后,调用protocol.export(invoker)把invoker转换成exporter。

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

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

4.netty服务启动

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

定位到了DubboProtocol.export()方法,接着会调用openServer(url) -> createServer(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 调用。
final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);

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

  @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
        NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
        try {
            handler.received(channel, e.getMessage());
        } finally {
            NettyChannel.removeChannelIfDisconnected(ctx.getChannel());
        }
    }

执行了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 调用。

private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
        
        public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
            if (message instanceof Invocation) {
                Invocation inv = (Invocation) message;
                Invoker<?> invoker = getInvoker(channel, inv);
                //如果是callback 需要处理高版本调用低版本的问题
                if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))){
                    String methodsStr = invoker.getUrl().getParameters().get("methods");
                    boolean hasMethod = false;
                    if (methodsStr == null || methodsStr.indexOf(",") == -1){
                        hasMethod = inv.getMethodName().equals(methodsStr);
                    } else {
                        String[] methods = methodsStr.split(",");
                        for (String method : methods){
                            if (inv.getMethodName().equals(method)){
                                hasMethod = true;
                                break;
                            }
                        }
                    }
                    if (!hasMethod){
                        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 );
                        return null;
                    }
                }
                RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
                return invoker.invoke(inv);
            }
            throw new RemotingException(channel, "Unsupported request: " + message == null ? null : (message.getClass().getName() + ": " + message) + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
        }

        @Override
        public void received(Channel channel, Object message) throws RemotingException {
            if (message instanceof Invocation) {
                reply((ExchangeChannel) channel, message);
            } else {
                super.received(channel, message);
            }
        }

最后

       如图,dubbo的的模型十分易懂,但涉及到的东西确实很多。以上只是对第一步:0.start 做了一个简单的流水账分析。
      所以,本文只是想做个引子,更多的细节还需要靠大家去挖掘。剩下的只有去debug the universe了。

Architecture.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • dubbo.xsd 文件说明 当我们要使用Dubbo进行 配置时,需要像下面那样引入dubbo.xsd这个文件. ...
    Mis_Gtw阅读 1,092评论 0 1
  • 先看官网两张图【引用来自官网】:image.png 官网说明: 1.首先 ServiceConfig 类拿到对外提...
    致虑阅读 1,409评论 1 4
  • 先看官网两张图【引用来自官网】:image.png 官网说明: 1.首先 ReferenceConfig 类的 i...
    致虑阅读 1,016评论 0 2
  • 1. 简介 我们分析服务引用的原理。在 Dubbo 中,我们可以通过两种方式引用远程服务。第一种是使用服务直联的方...
    Java黎先生阅读 714评论 1 0