Dubbo的服务暴露是一个重要的特性,了解其机制很重要。之前有很多人写了有关的源代码分析,在本文中不再重新分析。官方文档中的一篇写的就很好,本文主要是有关内容进行补充与总结。
传送门:服务导出
为什么要服务暴露
服务暴露分为远程暴露和本地暴露。在远程服务暴露中会将服务信息上传到注册中心。这时客户端要调用某个服务时会从注册中心找到该服务的远程地址等信息。然后客户端根据这个地址进行远程调用。服务端接收到远程调用请求后会最终调用getInvoker()
方法进行查找对用的invoker
。在getInvoker()
方法中会从一个HashMap中进行查找,如果在这个Map中查找不到就会抛出异常。在远程服务暴露中,会按照规则将实例Invoker存储在HashMap中,其中Key名包含端口、接口名、接口版本和接口分组。所以进行服务暴露很重要。
本地服务暴露是暴露在JVM中,不需要远程通信。Dubbo会默认把远程服务用injvm协议再暴露一份。
为什么会有本地服务暴露
在Dubbo中,一个服务可以即是provider,又是Consumer,因此就存在它自己调用自己服务的时候,如果再通过网络去访问,那么就是舍近求远,因此有了本地暴露服务这个设计。消费者可以直接消费同一个JVM内部的服务,避免了跨网络进行远程通信。
服务暴露起点
我们会通过XML或注解的方式来指定要暴露的服务。l例子如下:
<bean id=“xxxService” class=“com.xxx.XxxServiceImpl” />
<!-- 增加暴露远程服务配置 -->
<dubbo:service interface=“com.xxx.XxxService” ref=“xxxService” />
这时会创建出一个ServiceBean
对象。
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
ApplicationContextAware, BeanNameAware,
ApplicationEventPublisherAware
public class ServiceConfig<T> extends ServiceConfigBase<T>
public abstract class ServiceConfigBase<T> extends AbstractServiceConfig {
private static final long serialVersionUID = 3033787999037024738L;
protected String interfaceName;
//要暴露服务类的接口类
protected Class<?> interfaceClass;
//实现类引用
protected T ref;
//服务名
protected String path;
protected ProviderConfig provider;
protected String providerIds;
protected volatile String generic;
protected ServiceMetadata serviceMetadata;
ServiceBean和Spring有关,它继承了InitializingBean和ApplicationEvent。在Bean初始化完成后会调用InitializingBean.afterPropertiesSet方法来执行服务暴露的准备工作。在Spring的context完成初始化后,会触发ApplicationEventListener事件进行服务暴露,会执行onApplicationEvent
方法。这时服务服务暴露就开始了。
public void onApplicationEvent(ContextRefreshedEvent event) {
// 是否有延迟导出 && 是否已导出 && 是不是已被取消导出
if (isDelay() && !isExported() && !isUnexported()) {
// 导出服务
export();
}
}
整体流程
Dubbo真正的服务暴露入口是ServiceConfig#doExport()
方法。首先ServiceConfig类拿到对外提供服务的实际类ref,然后通过ProxyFactory
类的getInvoker
方法使用ref生成一个AbstractProxyInvoker
实例,到这一步就完成具体服务到Invoker
的转化。然后就是把Invoker
通过具体的协议(比如Bubbo)转化成Exporter
。
参考了简书肥朝的一篇文章: dubbo源码解析-服务暴露原理
- 调用ServiceConfig中的
export
方法,对配置进行检查与更新,例如provider是否为空,注册中心是否为空,protocols是否为空。然后检测是否应该暴露,如果不应该暴露,则直接结束;然后检测是否配置了延迟加载,如果是,则使用定时器来实现延迟加载的目的。 - 调用
expoer
方法里面的doExport()
方法。通过exported
变量判断是否暴露,如果为true,直接返回;否则先设置为true,然后执行doExportUrls()
方法。 - 调用
doExportUrls()
方法,先通过loadRegistries
加载注册中心链接,然后遍历ProtocolConfig集合,它是用户指定的协议集合(比如Dubbo、REST),在里面执行doExportUrlsFor1Protocol(protocolConfig, registryURLs)
方法。 - 执行
doExportUrlsFor1Protocol(protocolConfig, registryURLs)
方法,刚开始是组装URL。(1)它把metrices、application、module、provider、protocol等所有配置都放入到map中,(2) 通过反射获取interfaceClass的方法列表,先做签名校验,判断该服务是否有配置的方法存在,然后该方法签名是否有这个参数存在,都核对成功才将method的配置加入到map中;(3)将范化调用、版本号、method或者methods、token等信息加入到map;(4)获取服务暴露地址和端口号,利用map内数据组装成URL。 - 然后根据url中的scope参数决定服务导出方式。
scope=none
,不导出服务;scope!=remote
,导出到本地;scope!=local
,导出到远程。 - 执行
proxyFactory.getInvoker(ref, (Class)interfaceClass, url)
方法,来获取invoker,Dubbo默认的ProxyFactory实现类是JavasistProxyFacoty
方法。
这时,第一大步服务转化成Invoker已经完成,然后执行第二大步Invoker转化成Exporter
。
先介绍本地服务的暴露机制
- 调用
exportLocal(url)
。首先判断URL的协议头如果等于injvm,说明已经导出到本地了,无需再次导出。如果不是,则创建一个新的URL并将协议头设置为injvm,并另外设置主机名和端口。然后创建invoker,并调用InjvmProtocal
的export()
方法暴露服务。 -
InjvmProtocol
的export
方法仅创建一个InjvmExporter
,无其他逻辑。
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
}
InjvmExporter(Invoker<T> invoker, String key, Map<String, Exporter<?>> exporterMap) {
super(invoker);
this.key = key;
this.exporterMap = exporterMap;
exporterMap.put(key, this);
}
然后介绍远程服务暴露
如果有注册中心,服务暴露后需要向注册中心注册服务信息
如果没有注册中心,直接暴露服务。
有注册中心的暴露
- 调用
doLocalExport()
方法暴露服务。 - 向注册中心注册服务
- 向注册中心进行订阅ovrride数据
- 创建并返回
DestroyableExporter
。
调用doLocalExport()
方法暴露服务
- 首先根据
invoker
得到key,从bounds
缓存变量中尝试获取exporter
,如果获取不到,则调用Protocal的export
方法,默认的协议是dubbo,所以调用的DubboProtocol的export方法。 - 调用DubboProtocol的export方法,首先从invoker中获取对用的URL,然后根据服务组名、服务名、服务版本号和端口组成key,然后创建一个DubboExporter。
// demoGroup/com.alibaba.dubbo.demo.DemoService:1.0.1:20880
String key = serviceKey(url);
// 创建 DubboExporter
DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
// 将 <key, exporter> 键值对放入缓存中
exporterMap.put(key, exporter);
- 调用
openServer(url)
方法。首先根据url获取host:port
,用于标识当前的服务器实例。在同一台机器上,同一个端口仅允许启动一个服务器实例。若某个端口上已有服务器实例,此时调用reset
方法重置一些服务器的配置;如果没有则调用createServer(url)
方法创建一个服务器实例。 - 调用
createServer(url)
方法,有三个核心的逻辑,首先检测是否存在server参数所代表的Transporter拓展,即网络传输方式(如Netty, Mina),如果没有,则抛出异常;然后通过Exchangers.bind(url, requestHandler)
方法创建服务器实例;最后是检测是否支持client参数所表示的Transporter拓展,不存在也是抛出异常。 - 调用
Exchangers.bind(url, requestHandler)
方法,里面又调用了下面几个方法。只保留了主要逻辑部分。
getExchanger(url).bind(url, handler);
public static Exchanger getExchanger(URL url) {
String type = url.getParameter(Constants.EXCHANGER_KEY, Constants.DEFAULT_EXCHANGER);
return getExchanger(type);
}
public static Exchanger getExchanger(String type) {
return ExtensionLoader.getExtensionLoader(Exchanger.class).getExtension(type);
}
Exchange层是为了封装请求/响应模式,例如:把同步请求转化为异步请求。默认的扩展点实现类是HeaderExchanger
。
- 调用HeaderExchanger的bind方法
public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
if (handlers == null || handlers.length == 0) {
throw new IllegalArgumentException("handlers == null");
}
ChannelHandler handler;
if (handlers.length == 1) {
handler = handlers[0];
} else {
// 如果 handlers 元素数量大于1,则创建 ChannelHandler 分发器
handler = new ChannelHandlerDispatcher(handlers);
}
// 获取自适应 Transporter 实例,并调用实例方法
return getTransporter().bind(url, handler);
}
- 根据自适应扩展机制动态创建一个Transporter,Dubbo默认是NettyTransporter。
- 调用
NettyTransporter.bind(URL, ChannelHandler)
方法。创建一个NettyServer
实例。 - 调用
NettyServer.doOPen()
方法,服务器被开启,服务也被暴露出来了。