本次源码解析基于Dubbo 2.7.5版本,从以下三个方面进行解析:
- 服务导出基本原理分析
- 服务注册流程源码分析
- 服务暴露流程源码分析
1. 基本概念
1.1 URL
一般而言我们说的 URL 指的就是统一资源定位符,在网络上一般指代地址,本质上看其实就是一串包含特殊格式的字符串,标准格式如下:
protocol://username:password@host:port/path?key=value&key=value
Dubbo 就是采用 URL 的方式来作为约定的参数类型,被称为公共契约,就是我们都通过 URL 来交互,来交流。
具体参数如下:
- protocol:指的是 dubbo 中的各种协议,如:dubbo thrift http
- username/password:用户名/密码
- host/port:主机/端口
- path:接口的名称
- parameters:参数键值对
1.2 SPI机制
前文介绍过SPI的机制,Dubbo SPI机制 这里重点说下@Adaptive注解进行代理的条件,比如:
- 该⽅法如果是⽆参的,那么则会报错
- 该⽅法有参数,可以有多个,并且其中某个参数类型是URL,那么则可以进⾏代理
- 该⽅法有参数,可以有多个,但是没有URL类型的参数,那么则不能进⾏代理
- 该⽅法有参数,可以有多个,没有URL类型的参数,但是如果这些参数类型,对应的类中存在getUrl⽅法(返回值类型为URL),那么也可以进⾏代理
本次服务暴露过程源码就用到了最后一种代理方式,对应类中存在getUrl方法。
1.3 Exporter结构
⼀个服务导出成功后,会⽣成对应的Exporter:
- DestroyableExporter:Exporter的最外层包装类,这个类的主要作⽤是可以⽤来unexporter对应的服务
- ExporterChangeableWrapper:这个类主要负责在unexport对应服务之前,把服务URL从注册中⼼中 移除,把该服务对应的动态配置监听器移除
- ListenerExporterWrapper:这个类主要负责在unexport对应服务之后,把服务导出监听器移除
- DubboExporter:这个类中保存了对应服务的Invoker对象,和当前服务的唯⼀标志,当NettyServer 接收到请求后,会根据请求中的服务信息,找到服务对应的DubboExporter对象,然后从对象中得到
1.4 Invoker架构
- ProtocolFilterWrapper$CallbackRegistrationInvoker:会去调⽤下层Invoker,下层Invoker执⾏完了之后,会遍历过滤器,查看是否有过滤器实现了ListenableFilter接⼝,如果有,则回调对应的 onResponse⽅法,⽐如TimeoutFilter,当调⽤完下层Invoker之后,就会计算服务的执⾏时间
- ProtocolFilterWrapper$1:ProtocolFilterWrapper中的过滤器组成的Invoker,利⽤该Invoker,可以执⾏服务端的过滤器,执⾏完过滤器之后,调⽤下层Invoker
- RegistryProtocol$InvokerDelegate:服务的的委托类,⾥⾯包含了 DelegateProviderMetaDataInvoker对象和服务对应的providerUrl,执⾏时直接调⽤下层Invoker
- elegateProviderMetaDataInvoker:服务的的委托类,⾥⾯包含了AbstractProxyInvoker对象和 ServiceConfig对象,执⾏时直接调⽤下层Invoker
- AbstractProxyInvoker:服务接⼝的代理类,绑定了对应的实现类,执⾏时会利⽤反射调⽤服务实现 类实例的具体⽅法,并得到结果
2. 服务导出原理
2.1 源码入口
由于Dubbo 2.7.5在这部分进行了改动,服务导出的入口不再是ServiceBean中的export()方法了,现在我们就来找一下入口。
还是先从@EnableDubbo注解开始,注意到有一个@EnableDubboLifecycle注解。查看该注解@Import了一个DubboLifecycleComponentRegistrar类。按照Spring的套路,这里还是会执行该类的注册方法,注册一个Bean,来看DubboBootstrapApplicationListener类。
该类继承了OneTimeExecutionApplicationContextEventListener类,而这个类又实现了ApplicationListener类,如果有Spring源码基础的同学可以得知,这是Spring里的事件监听机制,当Spring启动之后,会执行该类的onApplicationEvent方法。
@Override
public void onApplicationContextEvent(ApplicationContextEvent event) {
if (event instanceof ContextRefreshedEvent) {//表示启动事件
onContextRefreshedEvent((ContextRefreshedEvent) event);
} else if (event instanceof ContextClosedEvent) {//表示结束事件
onContextClosedEvent((ContextClosedEvent) event);
}
}
再看onContextRefreshedEvent()方法,这里调用了dubboBootstrap.start()
private void onContextRefreshedEvent(ContextRefreshedEvent event) {
dubboBootstrap.start();
}
再看dubboBootstrap.start()方法
public DubboBootstrap start() {
if (started.compareAndSet(false, true)) {
//初始化
initialize();
if (logger.isInfoEnabled()) {
logger.info(NAME + " is starting...");
}
// 服务导出
exportServices();
// 不仅仅注册提供者
if (!isOnlyRegisterProvider() || hasExportedServices()) {
// 导出MetadataService
exportMetadataService();
//注册本地服务的实例
registerServiceInstance();
}
//刷新服务
referServices();
if (logger.isInfoEnabled()) {
logger.info(NAME + " has started.");
}
}
return this;
}
找到了服务导出入口exportServices()方法。
2.2 服务概念的演化
- DemoService接⼝表示⼀个服务,此时的服务表示服务定义
- DemoServiceImpl表示DemoService服务的具体实现,此时的服务表示服务的具体实现
- DemoService+group+version表示⼀个服务,此时的服务增加了分组和版本概念
- http://192.168.1.112:80/com.tuling.DemoService表示⼀个服务,此时的服务增加了机器IP和Port, 表示远程机器可以访问这个URL来使⽤com.tuling.DemoService这个服务
- http://192.168.1.112:80/com.tuling.DemoService? timeout=3000&version=1.0.1&application=dubbo-demo-provider-application表示⼀个服务, 此时的服务是拥有参数的,⽐如超时时间、版本号、所属应⽤
2.3 服务导出思路
- 确定服务的参数
- 确定服务支持的协议
- 构建服务最终的URL
- 将服务URL注册到注册中心
- 根据服务支持的不同协议,启动不同的Server,用来接收和处理请求
- 因为Dubbo⽀持动态配置服务参数,所以服务导出时还需要绑定⼀个监听器Listener来监听服务的参数是否有修改,如果发现有修改,则需要重新进⾏导出
3. 服务注册源码分析
首先从服务注册的准备工作开始,在服务注册之前,首先要获取所有的配置信息。
3.1 服务导出源码解析
3.1.1 DubboBootstrap.start()方法
首先从入口方法看
public DubboBootstrap start() {
if (started.compareAndSet(false, true)) {
//初始化服务参数
initialize();
if (logger.isInfoEnabled()) {
logger.info(NAME + " is starting...");
}
// 服务导出
exportServices();
// Not only provider register
if (!isOnlyRegisterProvider() || hasExportedServices()) {
// 2.导出MetadataService
exportMetadataService();
//3. 注册本地Service实例
registerServiceInstance();
}
//刷新参数
referServices();
if (logger.isInfoEnabled()) {
logger.info(NAME + " has started.");
}
}
return this;
}
3.1.2 initialize()方法
该方法进行参数初始化,包含很多小方法,我们一个一个分析。
private void initialize() {
if (!initialized.compareAndSet(false, true)) {
return;
}
//初始化参数信息
ApplicationModel.iniFrameworkExts();
//初始化配置中心
startConfigCenter();
//没有配置配置中心,就把注册中心当做配置中心
useRegistryAsConfigCenterIfNecessary();
//开启元数据配置
startMetadataReport();
//加载远程配置
loadRemoteConfigs();
//检查全局配置
checkGlobalConfigs();
//初始化MetadataService
initMetadataService();
//初始化MetadataService服务导出
initMetadataServiceExporter();
//初始化事件监听器
initEventListener();
if (logger.isInfoEnabled()) {
logger.info(NAME + " has been initialized!");
}
}
3.1.2.1 ApplicationModel.iniFrameworkExts()方法
这个方法主要是从ApplicationModel这里拿配置中心信息,最终放入Map里,由于这里没有进行配置中心的地址,所以此方法跳过。
3.1.2.2 startConfigCenter()方法
这个方法是开启配置中心,所以会执行configManager.refreshAll()方法。
private void startConfigCenter() {
Collection<ConfigCenterConfig> configCenters = configManager.getConfigCenters();
if (CollectionUtils.isNotEmpty(configCenters)) {
CompositeDynamicConfiguration compositeDynamicConfiguration = new CompositeDynamicConfiguration();
for (ConfigCenterConfig configCenter : configCenters) {
configCenter.refresh();
ConfigValidationUtils.validateConfigCenterConfig(configCenter);
compositeDynamicConfiguration.addConfiguration(prepareEnvironment(configCenter));
}
environment.setDynamicConfiguration(compositeDynamicConfiguration);
}
configManager.refreshAll();
}
refreshAll()方法,这里会调用每个类的refresh,最终会先调用AbstractConfig.refresh的方法,在这里调用服务配置参数,优先级如下: SystemConfiguration -> AppExternalConfiguration -> ExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
可以看出,-D⽅式配置的参数优先级最⾼,配置中⼼次之,注解随后,dubbo.properties最后。
注意到这里,由于我们并没有配置一个配置中心,所以这里还是没有配置中心的具体参数。
public void refreshAll() {
write(() -> {
// refresh all configs here,
getApplication().ifPresent(ApplicationConfig::refresh);
getMonitor().ifPresent(MonitorConfig::refresh);
getModule().ifPresent(ModuleConfig::refresh);
getProtocols().forEach(ProtocolConfig::refresh);
getRegistries().forEach(RegistryConfig::refresh);
getProviders().forEach(ProviderConfig::refresh);
getConsumers().forEach(ConsumerConfig::refresh);
});
}
3.1.2.3 useRegistryAsConfigCenterIfNecessary()方法
这个方法的作用是,如果没有配置配置中心且注册中心是zookeeper的话,就会把zookeeper作为默认的配置中心,赋值给configManager,然后再执行startConfigCenter()方法。
private void useRegistryAsConfigCenterIfNecessary() {
// we use the loading status of DynamicConfiguration to decide whether ConfigCenter has been initiated.
if (environment.getDynamicConfiguration().isPresent()) {
return;
}
if (CollectionUtils.isNotEmpty(configManager.getConfigCenters())) {
return;
}
configManager.getDefaultRegistries().stream()
.filter(registryConfig -> registryConfig.getUseAsConfigCenter() == null || registryConfig.getUseAsConfigCenter())
.forEach(registryConfig -> {
String protocol = registryConfig.getProtocol();
String id = "config-center-" + protocol + "-" + registryConfig.getPort();
ConfigCenterConfig cc = new ConfigCenterConfig();
.....//省略一些给cc赋值的代码
configManager.addConfigCenter(cc);
});
//开启配置中心
startConfigCenter();
}
3.1.2.4 startMetadataReport()
开启元数据,这里解释一下元数据。元数据定义为描述数据的数据,在服务治理中,例如服务接口名,重试次数,版本号等等都可以理解为元数据。在 2.7 之前,元数据一股脑丢在了注册中心之中,这造成了一系列的问题:
- 推送量大 -> 存储数据量大 -> 网络传输量大 -> 延迟严重
生产者端注册 30+ 参数,有接近一半是不需要作为注册中心进行传递;消费者端注册 25+ 参数,只有个别需要传递给注册中心。有了以上的理论分析,Dubbo 2.7 进行了大刀阔斧的改动,只将真正属于服务治理的数据发布到注册中心之中,大大降低了注册中心的负荷。
同时,将全量的元数据发布到另外的组件中:元数据中心。元数据中心目前支持 redis(推荐),zookeeper。
这里可以看这篇文章,Dubbo 2.7三大特性
由于本次示例,没有配置metadata-report所以,该方法直接返回。
3.1.2.5 loadRemoteConfigs()方法
这个方法,我猜测如果配置了元数据中心的话,服务的整体参数可能会发生变化,所以这里会再次加载一遍参数。但是如果没有配置元数据中心的话,此方法无用。
3.1.2.6 checkGlobalConfigs()
校验全局配置信息
3.1.2.7 initMetadataService()
初始化元数据中心服务
3.1.2.8 initMetadataServiceExporter
初始化元数据中心导出
3.1.2.9 initEventListener
初始化事件监听器
3.1.3 exportServices()方法
分析完initialize()方法之后,来到了重点方法服务导出方法。
configManager.getServices().forEach(sc -> {
// TODO, compatible with ServiceConfig.export()
ServiceConfig serviceConfig = (ServiceConfig) sc;
serviceConfig.setBootstrap(this);
if (exportAsync) {
ExecutorService executor = executorRepository.getServiceExporterExecutor();
Future<?> future = executor.submit(() -> {
sc.export();
});
asyncExportingFutures.add(future);
} else {
sc.export();
exportedServices.add(sc);
}
});
}
重点看export方法
3.1.3.1 export方法
public synchronized void export() {
if (!shouldExport()) {
return;
}
if (bootstrap == null) {
bootstrap = DubboBootstrap.getInstance();
bootstrap.init();
}
//检查并更新子配置信息
checkAndUpdateSubConfigs();
//初始化服务元数据
serviceMetadata.setVersion(version);
serviceMetadata.setGroup(group);
serviceMetadata.setDefaultGroup(group);
serviceMetadata.setServiceType(getInterfaceClass());
serviceMetadata.setServiceInterfaceName(getInterface());
serviceMetadata.setTarget(getRef());
if (shouldDelay()) {//需要延迟启动
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {//立即启动
doExport();
}
}
3.1.3.2 doExport()方法
重点看doExportUrls方法
protected synchronized void doExport() {
if (unexported) {
throw new IllegalStateException("The service " + interfaceClass.getName() + " has already unexported!");
}
if (exported) {
return;
}
exported = true;
if (StringUtils.isEmpty(path)) {
path = interfaceName;
}
doExportUrls();
// dispatch a ServiceConfigExportedEvent since 2.7.4
dispatch(new ServiceConfigExportedEvent(this));
}
3.1.3.3 doExportUrls()方法
在这个方法里获取URL列表,并根据协议信息,注册不同的服务。
private void doExportUrls() {
ServiceRepository repository = ApplicationModel.getServiceRepository();
ServiceDescriptor serviceDescriptor = repository.registerService(getInterfaceClass());
repository.registerProvider(
getUniqueServiceName(),
ref,
serviceDescriptor,
this,
serviceMetadata
);
//获取URL列表
List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);
for (ProtocolConfig protocolConfig : protocols) {
String pathKey = URL.buildKey(getContextPath(protocolConfig)
.map(p -> p + "/" + path)
.orElse(path), group, version);
//根据协议名注册不同的服务
repository.registerService(pathKey, interfaceClass);
// TODO, uncomment this line once service key is unified
serviceMetadata.setServiceKey(pathKey);
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
3.1.3.4 doExportUrlsFor1Protocol
这个方法太长,这里只说关键的,该方法根据host、port等信息,构建了服务对外提供的URL。
String host = findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = findConfigedPorts(protocolConfig, name, map);
URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
该方法还提供服务暴露功能,分两种情况,一种是本地服务、一种是远程服务。
3.1.3.4.1 本地暴露
为什么要有本地暴露呢?因为可能存在同一个 JVM 内部引用自身服务的情况,因此暴露的本地服务在内部调用的时候可以直接消费同一个 JVM 的服务避免了网络间的通信。
来看exportLocal方法
private void exportLocal(URL url) {
//从新构建URL,将协议改为injvm
URL local = URLBuilder.from(url)
.setProtocol(LOCAL_PROTOCOL)
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
//根据SPI机制,调用InjvmProtocol.export()
Exporter<?> exporter = protocol.export(
PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry url : " + local);
}
最终会生成一个InjvmExporter对象,作为服务暴露对象。
3.1.3.4.2 远程暴露
远程暴露会现在URL中加入动态配置参数,然后因为目前的URL里还是registry:前缀的,所以还是会先调用RegistryProtocol.export方法 该方法主要完成了以下几件事
- ⽣成监听器,监听动态配置中⼼此服务的参数数据的变化,⼀旦监听到变化,则重写服务URL,并且在服务导出时先重写⼀次服务URL
- 拿到重写之后的URL之后,调⽤doLocalExport()进⾏服务导出,在这个⽅法中就会调⽤DubboProtocol的export⽅法去导出服务了,导出成功后将得到⼀个 ExporterChangeableWrapper
- 在DubboProtocol的export⽅法中主要要做的事情就是启动NettyServer,并且设置⼀系列的 RequestHandler,以便在接收到请求时能依次被这些RequestHandler所处理
- 从originInvoker中获取注册中⼼的实现类,⽐如ZookeeperRegistry
- 将重写后的服务URL进⾏简化,把不⽤存到注册中⼼去的参数去除
- 把简化后的服务URL调⽤ZookeeperRegistry.registry()⽅法注册到注册中⼼去
- 最后将ExporterChangeableWrapper封装为DestroyableExporter对象返回,完成服务导出
至此服务导出过程完毕。
3.2 启动Service的过程
其实这个过程是在服务导出的过程中启动的,主要在DubboProtocol的export方法中的openServer方法。
3.2.1 openServer
private void openServer(URL url) {
//获取服务地址
String key = url.getAddress();
//client can export a service which's only for server to invoke
boolean isServer = url.getParameter(IS_SERVER_KEY, true);
if (isServer) {
ProtocolServer server = serverMap.get(key);
//双重检查
if (server == null) {
synchronized (this) {
server = serverMap.get(key);
if (server == null) {
//创建服务并放入缓存
serverMap.put(key, createServer(url));
}
}
} else {
// server supports reset, use together with override
server.reset(url);
}
}
}
3.2.2 createServer()方法
重点看这几行
try {
server = Exchangers.bind(url, requestHandler);
} catch (RemotingException e) {
throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
}
这里最终会调用NettyTransporter.bind方法
3.2.3 NettyTransporter.bind()方法
@Override
public RemotingServer bind(URL url, ChannelHandler listener) throws RemotingException {
return new NettyServer(url, listener);
}
3.2.4 NettyServer构造方法
public NettyServer(URL url, ChannelHandler handler) throws RemotingException {
// you can customize name and type of client thread pool by THREAD_NAME_KEY and THREADPOOL_KEY in CommonConstants.
// the handler will be warped: MultiMessageHandler->HeartbeatHandler->handler
super(ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME), ChannelHandlers.wrap(handler, url));
}
3.2.5 AbstractServer构造方法
public AbstractServer(URL url, ChannelHandler handler) throws RemotingException {
super(url, handler);
localAddress = getUrl().toInetSocketAddress();
String bindIp = getUrl().getParameter(Constants.BIND_IP_KEY, getUrl().getHost());
int bindPort = getUrl().getParameter(Constants.BIND_PORT_KEY, getUrl().getPort());
if (url.getParameter(ANYHOST_KEY, false) || NetUtils.isInvalidLocalHost(bindIp)) {
bindIp = ANYHOST_VALUE;
}
bindAddress = new InetSocketAddress(bindIp, bindPort);
this.accepts = url.getParameter(ACCEPTS_KEY, DEFAULT_ACCEPTS);
this.idleTimeout = url.getParameter(IDLE_TIMEOUT_KEY, DEFAULT_IDLE_TIMEOUT);
try {
doOpen();
if (logger.isInfoEnabled()) {
logger.info("Start " + getClass().getSimpleName() + " bind " + getBindAddress() + ", export " + getLocalAddress());
}
} catch (Throwable t) {
throw new RemotingException(url.toInetSocketAddress(), null, "Failed to bind " + getClass().getSimpleName()
+ " on " + getLocalAddress() + ", cause: " + t.getMessage(), t);
}
executor = executorRepository.createExecutorIfAbsent(url);
}
最终会调用NettyServer.doOpen()方法创建Netty服务。
3.3 服务监听
这里只介绍服务监听的作用,如果修改服务动态配置之后,整个同步流程如下:
- 修改服务动态配置,底层会修改Zookeeper中的数据
- ServiceConfigurationListener会监听到节点内容的变化,会触发ServiceConfigurationListener的 ⽗类AbstractConfiguratorListener的process(ConfigChangeEvent event)⽅法
- ConfigChangeEvent表示⼀个事件,事件中有事件类型,还有事件内容(节点内容),还有触发这个 事件的节点名字,事件类型有三个
- ADDED
- MODIFIED
- DELETED
- 当接收到⼀个ConfigChangeEvent事件后,会根据事件类型做对应的处理
- ADDED、MODIFIED:会根据节点内容去⽣成override://协议的URL,然后根据URL去⽣成 Configurator, Configurator对象很重要,表示⼀个配置器,根据配置器可以去重写URL
- DELETED:删除ServiceConfigurationListener内的所有的Configurator
- ⽣成了Configurator后,调⽤notifyOverrides()⽅法对服务URL进⾏重写