之前一直从事偏客户端相关的工作,来到考拉之后终于有机会从事大型的分布式系统开发,在微服务大行其道的今天,dubbo框架作为一款优秀的开源RPC框架使用非常广泛,在使用dubbo的这几个月里,用零碎的时间对dubbo框架的部分源码浅读了一番,平时工作较忙,历时几个月才把读代码的过程整理出来,现在把这个过程记录下来,希望对跟我一样的初学者小伙伴有所帮助。本文从下面几个部分逐步对dubbo服务调用流程进行简单的剖析:
1.插件机制
2.标签解析
3.提供者初始化
4.消费者初始化
5.远程服务调用流程
1.dubbo插件机制
Dubbo
除了提供了高效的远程调用,其灵活的扩展机制也是值得我们学习的,提供了丰富的扩展性和灵活性,框架内部核心组件都采用插件化设计,大部分组件都定义了多个扩展,使用者可以任性的替换和扩展。框架的内部实现也依托它的插件框架,所以在研读dubbo
源码之前先要了解它的插件化机制以及实现,要不然在读框架的代码的时候容易晕头转向。
可以通过在
classpath
的
META-INF/dubbo/internal/、META-INF/dubbo/、META-INF/services/目录
下放置文件来定义组件扩展点,文件名称为组件接口的类全名,文件内容为扩展名
=
实现类名的形式,例如
Protocol
的组件扩展点文件(文件名称com.alibaba.dubbo.rpc.Protocol):
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=memcom.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
框架由ExtensionLoader
负责加载/
存储扩展点实例,每个组件接口有一个对应的ExtensionLoader
实例负责加载该组件的扩展点,有三种方式加载方式:
1
、名称加载扩展点
2
、加载激活扩展点
3
、加载自适应扩展点
名称加载扩展点
对应
ExtensionLoader.getExtension
方法
这是最直观的方式,在初始化时框架会读取classpath
下所有扩展点文件形成多个键值对存在在扩展点对应的ExtensionLoader
中,比如要获取名称为dubbo
的Protocol
扩展点,那么就会返回一个
DubboProtocol
实例,但是框架还有种特殊的扩展点叫Wrapper
扩展点,即实现类有一个参数并且参数类型是扩展点接口类型的构造函数的扩展点,这类扩展点一般做一些适配和封装性的工作。有Wrapper
扩展点时,获取任何名称的扩展点都会返回Wrapper
实例,Wrapper
实例封装真实的扩展实现,有多个Wrapper
实现时,Wrapper
封装Wrapper
,形成一个对象链,比如获取名称是dubbo
的Protocol
扩展点,通过ExtensionLoader.getExtension("dubbo")
获取,ExtensionLoader
会返回一个ProtocolListenerWrapper
实例,ProtocolListenerWrapper
实例持有一个ProtocolFilterWrapper
实例,ProtocolListenerWrapper
实例持有持有一个DubboProtocol
实例,对象引用关系:ProtocolListenerWrapper->ProtocolFilterWrapper->DubboProtocol
。Wrapper
扩展点相关的代码见下面代码片段:
com.alibaba.dubbo.common.extension.ExtensionLoader.loadFile(Map<String, Class<?>>, String)
加载激活扩展点
对应
ExtensionLoader.getActivateExtension
方法
在应用进行dubbo
调用时,dubbo
框架会根据条件加载一些自动激活的扩展点,最典型就是Filter
组件,dubbo
接口调用时会激活应用配置和框架内置的Filter
调用链。自动激活的扩展点一般通过两种形式定义:
- 通过Activate注解,框架启动时ExtensionLoader会加载所有带Activate注解的接口实现,并存在一个Map中,key是扩展点实现的类名首字母小写,如果实现类的结尾是接口名称,截掉这个接口名称,比如MonitorFilter实现,处理之后key就是monitor。
com.alibaba.dubbo.common.extension.ExtensionLoader.loadFile(Map<String, Class<?>>, String)
在查找激活的扩展点时,如果Activate注解设置了group和values属性,要根据这两个属性进行过滤,group属性有两个枚举值:provider、consumer,设置了group=”provider”则调用时提供端会加载此扩展点,设置了group=”consumer”则调用时消费端会加载此扩展点,如果设置了group={”provider”, ”consumer”}则提供端和消费端都会加载此扩展点,当然还要有个前提条件,就是应用层没有禁用次扩展点,禁用的方式就是在配置时扩展点名称前面加个-号,比如设置了filter=”-monitor”,那么不会激活MonitorFilter这个扩展点。如果values属性设置了值,那么框架会检查封装此次调用所有信息的URL的parameters中是否在values中的key,如果有的话也激活该扩展点。
2.应用中显示设置的扩展点,比如Filter组件,使用者可以在定义接口配置时设置filter属性来达到激活扩展点的目的。
com.alibaba.dubbo.common.extension.ExtensionLoader.getActivateExtension(URL, String[], String)
加载自适应扩展点
对应
ExtensionLoader.getAdaptiveExtension
方法
每一次dubbo
接口调用都是框架中的多个不同的组件来合作完成的,这时候完成核心工作的组件也是通过ExtensionLoader
来加载的,此时加载的扩展点由调用上下文决定,这种扩展点加载方式叫自适应加载。Dubbo
框架通过Adaptive
注解来定义自适应扩展点,同一个扩展点接口的实现中最多只能有一个实现类可以定义Adaptive
注解,定义了Adaptive
注解的扩展点就是被加载到的自适应扩展点Map
中,如果所有实现都没有使用Adaptive
注解,那么接口需要使用SPI
注解,并且设置value
属性,value
属性值就是默认扩展点的名称,如果URL
中未指定加载哪个扩展点则加载默认扩展点,同时需要扩展的方法也打上Adaptive
注解。自适应扩展点加载逻辑是:在URL
中获取扩展点名称,这个名称一般放在Url
的某个属性或者parameters
的某个key
中,如URL
中没有取到响应的名称取默认值,那么取SPI
注解中指定的value
属性。每个组件的自适应扩展点的加载都是通过该接口的一个代理实现来完成,框架中组件数量众多为了消除重复代码,这些代理类并没有对应的静态代码,代理类代码是dubbo
框架根据一个代码模板在运行时生成的,然后由Compiler
组件把生成的代码编译并加载成代理类Class
(相关代码见com.alibaba.dubbo.common.extension.ExtensionLoader.createAdaptiveExtensionClassCode()
这个方法),例如,当要加载Cluster
自适应组件时,dubbo
框架会在生成下面这段代码,然后把它当做Cluster组件的自适应加载代理类:
package com.alibaba.dubbo.rpc.cluster;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Cluster$Adpative implements com.alibaba.dubbo.rpc.cluster.Cluster {
public com.alibaba.dubbo.rpc.Invoker join(com.alibaba.dubbo.rpc.cluster.Directory arg0) throws com.alibaba.dubbo.rpc.cluster.Directory {
if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.cluster.Directory argument == null");
if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.cluster.Directory argument getUrl() == null");com.alibaba.dubbo.common.URL url = arg0.getUrl();
String extName = url.getParameter("cluster", "failover");
if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.cluster.Cluster) name from url(" + url.toString() + ") use keys([cluster])");
com.alibaba.dubbo.rpc.cluster.Cluster extension = (com.alibaba.dubbo.rpc.cluster.Cluster)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.cluster.Cluster.class).getExtension(extName);
return extension.join(arg0);
}
}
从上面的代码也可以看出,扩展点名称从URL
中获取,加入URL
中没有指定对应的名称,则去默认扩展实现,Cluster
组件的SPI
注解value=failover
,所以上面代码的extName
默认值是failover
。
2.dubbo标签解析
前面了解了dubbo
的插件化机制之后,接下来进入正题,研究一下dubbo
的核心原理,由于dubbo
的功能配置较多,为了更高效的研读代码,在阅读的过程中尽量忽略一些细节,重点关注它的主干流程,主干了解清楚之后再去分析它的一些细节功能就更轻松了,否则容易陷入各种细枝末节不能自拔让整个代码的阅读周期变得冗长无比效率大打折扣。
Dubbo
框架中,服务提供者和服务消费是两个核心角色,所以主要通过服务提供者初始化、服务消费者初始化、服务调用流程这三个部分来分析dubbo
框架是如何为我们的应用实现远程调用的。
做任何事之前都要找到开始的地方,阅读框架代码也不例外,对dubbo
框架来说它的最佳入口无疑是使用它的地方,由于Spring IOC
框架在java
后端程序的广泛应用,大部分程序都是结合Spring IOC
框架使用dubbo
框架,如下所示:
<dubbo:application name="${dubbo.application.name}"
owner="${dubbo.application.owner}" organization="${dubbo.application.organization}"
logger="log4j" environment="${dubbo.environment}" />
<dubbo:protocol name="dubbo" port="-1" accesslog="false" />
<dubbo:registry address="${dubbo.registry.address}"
check="false" />
<dubbo:monitor protocol="registry" />
<dubbo:provider delay="-1" group="${dubbo.provider.group}"
version="1.0" timeout="5000" filter="-monitor,traceFilter" dispatcher="kaolaall" />
<dubbo:service
interface="com.netease.haitao.mykaola.compose.api.UserBrandFacadeService" ref="userBrandFacadeServiceImpl" />
<dubbo:consumer group="${dubbo.consumer.group}" cluster="failfast"
version="1.0" check="false" filter="-monitor,traceFilter" />
<dubbo:reference id="sendCouponRemoteApiImpl" interface="com.netease.haitao.online.web.remote.api.SendCouponRemoteApi"
group="${dubbo.jxc.group}" />
上面的配置向dubbo
框架注册了应用名称、框架协议、提供者全局属性、服务接口、消费者全局属性、服务消费者,下面就由这段简单的配置入手来分析一下服务提供者初始化和服务消费者初始化。
从上面的使用示例可以看到,dubbo
框架扩展了一套dubbo
标签,先简单了解一下Spring
的标签扩展机制。Spring
框架提供了两个接口:NamespaceHandler
负责namespace
处理和BeanDefinitionParser
负责bean
的解析。
使用者可以实现NamespaceHandler
接口,然后可以在classpath
(包括class
文件路径和jar
包中的路径)的META-INF
目录下编写一个spring.handlers
文件,该文件中定义名称空间URL
和名称空间处理器类的映射,如dubbo
框架的spring.handlers
文件内容如下:
http\://code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler
Spring
框架初始化时会加载所有classpath
的spring.handlers
文件,把namespace URL
和namespace
处理器的映射存到一个Map
中,Spring
框架在解析bean
定义文档时,遇到了非IOC
内置(beans
名称空间下)的标签,会在这个Map
中查找namespace
处理器,使用这个自定义的处理器来进行标签解析工作,可以在DefaultBeanDefinitionDocumentReader
和BeanDefinitionParserDelegate
类中看到相关的代码:
Dubbo
框架实现了
DubboNamespaceHandler
来处理dubbo
名称空间
:
处理器初始化时给dubbo
所有标签都注册了一个解析器,其他的标签主要任务是给框架设置一些全局属性可以暂时先放一边,重点需要看下service
和reference
两个标签,它们分别用来注册服务和服务消费者,dubbo:service会被框架解析成一个ServiceBean,dubbo:reference会被框架解析成一个ReferenceBean。
3.服务提供者初始化
dubbo
服务提供者由dubbo:service
来定义,从前面可以看到,Spring
把dubbo:service
解析成一个ServiceBean
,ServiceBean
实现了ApplicationListener
和InitializingBean
接口,ServiceBean
有个核心方法export
,在这个方法中初始化服务提供者并且暴露远程服务。这个方法在bean
初始化或容器中所有bean
刷新完毕时被调用,根据provider
的延迟设置决定,如果设置了延迟(delay
属性)在bean
初始化结束之后调用否则容易刷新事件中被调用,默认会延迟export
,即在容器bean
的刷新结束事件中被调用。
com.alibaba.dubbo.config.spring.ServiceBean.onApplicationEvent(ApplicationEvent)
在export方法中,总体上export
方法暴露服务时主要做了下列这些步骤:
- 选用服务端口
- 生成URL对象
- 生成本地服务代理(如果需要)
- 生成远程服务代理
- 启用服务监听
- 服务注册到注册中心
选用服务端口
服务需要一个端口来进行服务调用侦听,框架默认会从20880
开始选定一个未被占用的端口来提供服务,也可以在配置中的port参数指定服务的端口:
com.alibaba.dubbo.config.ServiceConfig.doExportUrlsFor1Protocol(ProtocolConfig, List<URL>)
final int defaultPort = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(name).getDefaultPort();
if (port == null || port == 0) {
port = defaultPort;
}
if (port == null || port <= 0) {
port = getRandomPort(name);
if (port == null || port < 0) {
port = NetUtils.getAvailablePort(defaultPort);
putRandomPort(name, port);
}
logger.warn("Use random available port(" + port + ") for protocol " + name);
}
生成URL对象
URL
对象是dubbo
框架中的核心模型,它携带了调用链路中注册中心、消费端配置、提供端配置等全部信息。无论是消费端代理还是提供端代理都需持有一个URL
对象携带服务调用时的上下文信息。
在服务初始化时需要对注册中心进行访问,所以需要一个URL
来跟注册中心通信,根据应用的注册中心配置来生成这个URL
,URL协议是registry
,包含了注册中心地址、端口,path
部分是com.alibaba.dubbo.registry.RegistryService
类名,其中还有dubbo
版本、应用名称等信息放在URL
的参数中,格式化之后是这种形式:registry://10.165.124.205:2181/com.alibaba.dubbo.registry.RegistryService?application=…
除了注册中心URL
,还需要生成一个URL
来携带服务本身的信息,协议由应用侧指定,在我们的示例配置中是dubbo
协议,host
是本机ip
,端口是上面选中的随机或
配置中指定的端口,path
是服务对应的接口class全名(含包路径),添加side
参数provider
,dubbo
版本号、服务方法名称、group
等等,格式化之后是这种形式:
dubbo://10.240.176.159:20880/com.netease.haitao.mykaola.generic.api.InternalStaffServiceFacade?...&createInternalStaffOperDetail.timeout=10000&…&default.group=hzchenyunyun1&…&dubbo=3.0.0&…&methods=…&…&side=provider&…
这个url
会被设置到registry URL
的export
属性中,这点很重要,后面的初始化过程是围绕registry URL
,需要从这个export属性中拿到提供者服务URL。
生成本地服务代理
如果scope
属性没有被设置成remote
,服务同时会在本地暴露,生成一个本地服务代理对象,这里会生成一个新的URL
,协议是代表本地服务的injvm
,host
是127.0.0.1
端口是0
,生成一个InjvmExporter
,这时本地调用dubbo
接口时直接调用本地代理不走网络请求。生成Exporter
的过程和生成远程Exporter
的过程类似,在后面详细描述。
com.alibaba.dubbo.config.ServiceConfig.exportLocal(URL)
private void exportLocal(URL url) {
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
URL local = URL.valueOf(url.toFullString())
.setProtocol(Constants.LOCAL_PROTOCOL)
.setHost(NetUtils.LOCALHOST)
.setPort(0);
Exporter exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() +" to local registry");
}
}
生成远程服务代理
如果scope
属性没有被设置成local
,生成远程服务代理,框架提供了ProxyFactory
生成服务代理(dubbo
提供JDK
动态代理和Javaassist
代理,默认使用Javaassist
,使用者也可以替换成其他的代理,这里不展开),它会生成一个Invoker
,该Invoker
是服务ref
的一个代理包含了携带服务信息的URL
对象。这里的ref就是在dubbo:service中配置的ref属性,指定服务的具体实现类,Invoker的invoke
方法被调用时,最终会调用到ref
指定的服务实现。这里的ProxyFactory
使用ExtensionLoader
的自适应扩展点加载,如应用侧没有特别指定,默认的是JavassistProxyFactory
。
com.alibaba.dubbo.config.ServiceConfig.doExportUrlsFor1Protocol(ProtocolConfig, List<URL>)
从上面的代码截图里面看到,创建Invoker
之后接下来就是创建一个Exporter
,由Protocol
组件来创建。这里的Protocol
扩展点也是自适应加载,而当前的Url
的协议是registry
,自适应protocol
最终会调用ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(“registry”)
, Protocol
定义了ProtocolListenerWrapper
和ListenerExporterWrapper
两个Wrapper
扩展点,而当前的Url
的协议是registry
,根据ExtensionLoader
的加载规则,它会返回ProtocolListenerWrapper->ListenerExporterWrapper->RegistryProtocol
对象链,对于registry
协议,两个Wrapper
都不会做任何处理,会直接调到RegistryProtocol.export
方法。
com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
RegistryProtocol.export
会调用doLocalExport
方法,执行服务暴露逻辑:
com.alibaba.dubbo.registry.integration.RegistryProtocol.doLocalExport(Invoker<T>)
接着调用protocol.export,这时候url发生了变化不再是registryUrl了,而是存放在registryUrl的export参数中的providerUrl,根据不同协议会定位到不同的扩展,示例配置是dubbo协议,对应的实现是DubboProtocol,同样地根据Wrapper扩展点加载机制会加载出ProtocolListenerWrapper和 ListenerExporterWrapper两个Wrapper,然后依次调用ProtocolListenerWrapper->ListenerExporterWrapper->DubboProtocol的export方法。从上面的代码截图可以看到,ProtocolListenerWrapper.export会创建一个ListenerExporterWrapper实例,并添加所有激活的ExporterListener到ListenerExporterWrapper实例中,再经过ProtocolFilterWrapper处理,加载所有激活的Filter,并且构建Invoker-Filter链包装成新的Invoker。接着会调用DubboProtocol.export方法生成一个DubboExporter对象,Exporter中持有上面包装Filter链后的Invoker对象引用和url对应的key(key的生成规则在com.alibaba.dubbo.rpc.support.ProtocolUtils.serviceKey(URL)方法中),该Exporter对象会注册到DubboProtocol的exporterMap中,服务器监听到服务调用之后会在这个map中查找Exporter,并封装的Invoker。
com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
启动服务监听
上面已经生成了服务代理,为了能收到消费端的服务请求,还要在上面选中的端口上监听消费端的请求,调用DubboProtocol
的openServer
方法,dubbo
使用了Netty
、grizzly
、Mina
三种网络框架,框架默认使用Netty
,示例配置没有特殊配置,主要看一下Netty
。启动服务时组件的调用顺序:Exchanger.bind->Transporter.bind->Server
,Exchanger
和Transporter
也是通过ExtensionLoader
来加载,会加载到NettyTransporter
实现,它会创建生成一个NettyServer
实例,在构造NettyServer
实例时调用doOpen
打开端口监听, 并添加三个IO事件处理器,NettyCodecAdapter
负责请求解码、响应编码,nettyHandler
处理请求,把NettyCodecAdapter.decoder->NettyCodecAdapter.encoder->NettyHandler IO
事件处理器链到Netty
框架中,服务调用消息由多层框架内部的Handler
转发最终会转发到DubboProcotol
的requestHandler
中处理,服务的调用流程后面再详细分析。
com.alibaba.dubbo.remoting.transport.netty.NettyServer.doOpen()
ps:在这里建议先了解一些Netty框架的使用,Netty框架封装了NIO复杂的编程模型,使用者只需实现多个ChannelHandler接口注册到Netty框架中实现业务逻辑无需关注NIO的复杂性,像NettyCodecAdapter中的两个编解码处理器encoder和decoder都是ChannelHandler的实现。
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者陈云云授权发布。