配置承载初始化
不管在服务暴露还是服务消费场景下,Dubbo框架都会根据优先级对配置信息做聚合处理,目前默认覆盖策略主要遵循以下几点规则:
-
-D 传递给JVM参数优先级最高,
比如-Ddubbo.protocol.port=20880。 -
代码或XML配置优先级次高,
比如Spring中XML文件制定<dubbo:protocol port="20880"/>
。 -
配置文件优先级最低,
比如dubbo.properties文件制定dubbo.protocol.port=20880。
一般推荐使用dubbo.properties作为默认值,只有XML没有配置时,dubbo.properties配置项才会生效,通常用于共享配置,比如应用名等。
Dubbo的配置也会受到provider的影响,这个属于运行期属性值影响,同样遵循以下几点规则:
- 如果只有provider端指定配置,则会自动透传到客户端(比如timeout)。
- 如果客户端也配置了响应属性,则服务端配置会被覆盖(比如timeout)。
运行时属性随着框架特性可以动态添加,因此覆盖策略中包含的属性没办法全部列出来,一般不允许透传的属性都会在ClusterUtils#mergeUrl
中进行处理。
远程服务的暴露机制
整体RPC的暴露原理:
ServiceConfig ------> ref
↓
ProxyFactory --------> Javassist、JDK动态代理
↓
Invoker ----------> AbstractProxyInvoker
↓
Protocol --------> Dubbo、injvm等
↓
Exporter
在整体上看,Dubbo框架做服务暴露分为两大部分,第一步将持有的服务实例通过代理转换成Invoker
,第二步会把Invoker通过具体协议(比如Dubbo)转换成Exporter,
框架做了这层抽象大大方便了功能扩展。这里的Invoker可以简单理解成一个真实的服务对象实例,是Dubbo框架实体域,所有模型都会向它靠拢,可向它发起invoer调用。它可能是本地的实现,也可能是一个远程的实现,还可能是一个集群的实现。
框架真正进行服务暴露的入口点在ServiceConfig#doExport
中,无论XML还是注解,都会转换成ServiceBean,它集成自ServiceConfig,在服务暴露之前会按照上面的覆盖策略生效,主要处理思路就是遍历服务的所有方法,如果没有值则尝试从 -D 选项中读取,如果还没有则自动从配置文件dubbo.properties中读取。
Dubbo支持多注册中心同时写,如果配置了服务同时注册多个注册中心,则会在ServiceConfig#doExportUrls
中依次暴露:
Dubbo也支持相同服务暴露多个协议,比如同时暴露Dubbo和REST协议,框架内部会依次对使用的协议都依次服务暴露,每个协议注册元数据都会写入多个注册中心。在①中会自动获取用户配置的注册中心,如果没有显示指定注册中心,则默认会用全局配置的注册中心。在②处理多协议服务暴露的场景,真实服务暴露逻辑是在doExportUrlsFor1Protocol
方法中实现的:
- 在①中主要通过反射获取配置对象并放到map中用于后续构造URL参数(比如应用名等)
- 在②中主要区分全局配置,默认在属性前面增加
default.
前缀,当框架获取URL中的参数时,如果不存在则会自动尝试获取default. 前缀对应的值 - 在③中主要处理本地内存JVM协议暴露
- 在④中主要追加监控上报地址,框架会在拦截器中执行数据上报,这部分是可选的
- 在⑤中会通过动态代理的方式创建Invoker对象,在服务端生成的是
AbstractProxyInvoker
实例,所有真实的方法调用都会委托给代理,然后代理转发给服务ref调用。目前框架实现两种代理:JavassistProxyFactory和JdkProxyFactory
。
- JavassistProxyFactory模式原理:创建Wrapper子类,在子类中实现invokeMethod方法,方法体内会为每个ref方法都做方法名和方法参数匹配校验,如果匹配则直接调用即可,相比JDKProxyFactory省去了反射调用的开销。
- JDKProxyFactory:通过反射获取真实对象的方法,然后调用即可。
- 在⑥中主要先触发服务暴露(端口打开等),然后进行服务元数据注册
- 在⑦中主要处理没有使用注册中心的场景,直接进行服务暴露不需要元数据注册,因为这里暴露的URL信息是以具体RPC协议开头的,并不是以注册中心协议开头的。
为了更容易地理解服务暴露于注册中心的干洗,以下列表分别展示有注册中心和无注册中心的URL:
registry://host:port/com.alibaba.dubbo.registry.RegistryService?protocol==zookeeper&export=dubbo://ip:port/xxx?..
dubbo://ip:host/xxx.Service?timeout=1000&..
protocol实例会自动根据服务暴露URL自动做适配,有注册中心场景会取出具体协议,比如Zookeeper,首先会创建注册中心实例,然后取出export对应的具体服务URL,最后用服务URL对应的协议(默认为Dubbo)进行服务暴露,当服务暴露成功后把服务数据注册到ZooKeeper。如果没有注册中心,则在⑦中会自动判断URL对应的协议(Dubbo)并直接暴露服务,从而没有经过注册中心。
将服务实例ref转换成Invoker之后,如果有注册中心时,则会通过RegistryProtocol#export进行更细粒度的控制,比如先进行服务暴露再注册服务元数据。注册中心在做服务暴露时依次做了一下几件事:
- 委托具体协议(Dubbo)进行服务暴露,创建NettyServer监听端口和保存服务实例。
- 创建注册中心对象,与注册中心创建TCP连接。
- 注册服务元数据到注册中心。
- 订阅configuators节点,监听服务动态属性变更事件。
- 服务销毁收尾工作,比如关闭端口、反注册服务信息等。
DestroyableExporter中重写的unexport()方法如下:
当服务真实调用时会触发各种拦截器Filter,这个是在哪里初始化的呢?在①中进行服务暴露前,框架会做拦截器初始化,Dubbo在加载protocol扩展点时会自动注入ProtocolListennerWrapper和ProtocolFilterWrapper
。
ProtocolFilterWrapper --> ProtocolListennerWrapper --> DubboProtocol
在ProtocolListenerWrapper实现中,在对服务提供者进行暴露时回调对应的监听器方法。ProtocolFilterWrapper会调用下一级ListenerExporterWrapper#export方法,在该方法内部会触发buildInvokerChain进行拦截器构造:
①:在触发Dubbo协议暴露前先对服务Invoker做了一层拦截器构建,在加载所有拦截器时会过滤只对provider生效的数据。
②: 首先获取真实服务ref对应的Invoker并挂载到整个拦截器链尾部,然后逐级包裹其他拦截器,这样保证了真实服务调用是最后触发的。
③:逐层转发拦截器服务调用,是否调用下一个拦截器由具体拦截器实现。
在构造调用拦截器之后会调用Dubbo协议进行服务暴露:
①和②:中主要根据服务分组、版本、服务接口和暴露端口作为key用于关联具体服务Invoker。
③:对服务暴露做校验判断,因为同一个协议暴露有很多接口,只有初次暴露的接口才需要打开端口监听。
然后在④中触发HeaderExchanger中绑定的方法,最后会调用底层NettyServer进行处理。在初始化Server过程中会初始化很多Handl用于支持一些特性,比如心跳、业务线程池处理编解码的Handler和响应方法调用的Handler。
本地服务的暴露机制
很多实用Dubbo框架的应用可能存在同一个JVM暴露了远程服务,同时同一个JVM内部又引用了自身服务的情况,Dubbo默认会把远程服务用injvm协议再暴露一份
,这样消费方直接消费同一个JVM内部的服务,避免了跨网络进行远程通信。
通过exportLocal实现可以发现,在①中显式Dubbo指定用injvm协议暴露服务,这个协议比较特殊,不会做端口打开操作,仅仅把服务保存在内存中而已。在②中会提取URL中的协议,在InjvmProtocol类中存储服务实例信息,它的实现也是非常直接了当的,直接返回InjvmExporter实例对象,构造函数内部会把当前Invoker加入exporterMap: