这篇文章主要总结一下dubbo服务端启动的时候服务暴露过程,虽然官方网站和各种博客上已经有很多介绍服务暴露的帖子,但还是想把自己跟源码过程中遇到的问题和心得记录下来,算是个总结,并且本篇文章是基于dubbo最新的2.7.6版本,和官网介绍的2.6.5版本差别还是有点大的(重构了很多模块,代码逻辑比之前清晰多了,说实话这里要吐槽下之前版本的代码,常常看见一个上百行的代码,只有屈指可数的注释,真让人看的头大。。),本文也会提到某些有差异的地方:
2.7.5版本dubbo中加入了OneTimeExecutionApplicationContextEventListener类来专门负责dubbo的启动和停止,(老版本中是直接写在serviceBean中,并且只监听ContextRefreshedEvent事件来暴露服务,而不负责服务的关闭清理),这个类监听了ContextRefreshedEvent事件和ContextClosedEvent事件分别来处理服务的启动和停止,在监听到ContextRefreshedEvent事件之后,会有一个单例的DubboBootstrap(2.7.5版本新增)被创建出来,专门负责dubbo服务的启动,这部分代码如下:
public class DubboBootstrapApplicationListener extends OneTimeExecutionApplicationContextEventListener
implements Ordered {
@Override
public void onApplicationContextEvent(ApplicationContextEvent event) {
if (event instanceof ContextRefreshedEvent) {
//调用到下面的dubboBootstrap.start()方法
onContextRefreshedEvent((ContextRefreshedEvent) event);
} else if (event instanceof ContextClosedEvent) {
onContextClosedEvent((ContextClosedEvent) event);
}
}
private void onContextRefreshedEvent(ContextRefreshedEvent event) {
dubboBootstrap.start();
}
}
可以看到,在DubboBootStrap的start方法中,在经过初始化之后,就进入了重点的服务暴露流程,后面的第二点暴露元服务和注册本地服务实例不是今天的重点,后面有时间再研究。在服务暴露过程中,会获取到所有配置的远程服务,逐个地进行服务暴露,可配置为异步地在线程池里的暴露,和延迟暴露(内部线程池为ScheduledExecutorService,用来实现延迟暴露)来提升服务的暴露时间,然后进入到具体的serviceBean的export方法中,这个方法就回到了之前版本的export方法中,但方法内部仍有许多代码的重构和优化。
public class DubboBootstrap extends GenericEventListener {
/**
* DubboBootstrap被设计成单例模式,并有解释(todo)
* See {@link ApplicationModel} and {@link ExtensionLoader} for why DubboBootstrap is
* designed to be singleton.
*/
public static synchronized DubboBootstrap getInstance() {
if (instance == null) {
instance = new DubboBootstrap();
}
return instance;
}
/**
* Start the bootstrap
*/
public DubboBootstrap start() {
if (started.compareAndSet(false, true)) {
initialize();
if (logger.isInfoEnabled()) {
logger.info(NAME + " is starting...");
}
// 1. export Dubbo Services
// 这里是暴露服务接口的主要流程,具体实现在下面
exportServices();
// Not only provider register
if (!isOnlyRegisterProvider() || hasExportedServices()) {
// 2. export MetadataService
exportMetadataService();
//3. Register the local ServiceInstance if required
registerServiceInstance();
}
referServices();
if (logger.isInfoEnabled()) {
logger.info(NAME + " has started.");
}
}
return this;
}
private void exportServices() {
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(() -> {
//异步调到具体的serviceBean的export方法,和老版本的export作用一样,但内部也做了重构和优化
sc.export();
});
asyncExportingFutures.add(future);
} else {
//同步调到具体的serviceBean的export方法,和老版本的export作用一样,但内部也做了重构和优化
sc.export();
exportedServices.add(sc);
}
});
}
}
新版本对服务暴露方法进行了拆分,把原来整段的代码拆到不同的私有方法中明确各个部分的逻辑和职责,可读性比之前的代码好了很多(最初看老版本代码的时候,真的看的脑壳疼)
服务导出的总体流程(重要的流程)有以下几点:
-
校验和初始化
-
加载配置中心
-
组装服务URL和解析服务接口
-
创建服务对应Invoker对象
-
暴露服务到本地和远程
下面分别对上面的流程做下详细的总结:
校验和初始化:
这个阶段不是本文的重点,流程也比较冗长,新版本唯一不同的是将这部分代码提出来在ServiceConfig的checkAndUpdateSubConfigs()方法中实现,主要处理这几个功能:
-
检测 <dubbo:service> 标签的 interface 属性合法性,不合法则抛出异常
-
检测 ProviderConfig、ApplicationConfig 等核心配置类对象是否为空,若为空,则尝试从其他配置类对象中获取相应的实例。
-
检测并处理泛化服务和普通服务类
-
检测本地存根(stub)配置,并进行相应的处理
-
对 ApplicationConfig、RegistryConfig 等配置类进行检测,为空则尝试创建,若无法创建则抛出异常
加载配置中心(registry)
dubbo支持多种配置中心,也支持同个服务在多个配置中心上注册,所以需要将所有的配置中心信息获取到,这里要做的任务只是获取到系统和配置文件中配置的注册中心信息,并且通过判断条件,分辨出是否要将某个注册中心添加到List<URL>中用来暴露serivice,新版本也对这部分代码做了重构,单独有一个ConfigValidationUtils方法来负责加载registry,具体方法和描述如下:
public class ConfigValidationUtils {
public static List<URL> loadRegistries(AbstractInterfaceConfig interfaceConfig, boolean provider) {
// check && override if necessary
List<URL> registryList = new ArrayList<URL>();
//从配置中获取注册中心信息
ApplicationConfig application = interfaceConfig.getApplication();
List<RegistryConfig> registries = interfaceConfig.getRegistries();
if (CollectionUtils.isNotEmpty(registries)) {
for (RegistryConfig config : registries) {
String address = config.getAddress();
if (StringUtils.isEmpty(address)) {
address = ANYHOST_VALUE;
}
if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
Map<String, String> map = new HashMap<String, String>();
AbstractConfig.appendParameters(map, application);
AbstractConfig.appendParameters(map, config);
map.put(PATH_KEY, RegistryService.class.getName());
AbstractInterfaceConfig.appendRuntimeParameters(map);
if (!map.containsKey(PROTOCOL_KEY)) {
map.put(PROTOCOL_KEY, DUBBO_PROTOCOL);
}
List<URL> urls = UrlUtils.parseURLs(address, map);
//可能根据配置,过滤某些注册中心。
for (URL url : urls) {
url = URLBuilder.from(url)
.addParameter(REGISTRY_KEY, url.getProtocol())
.setProtocol(extractRegistryType(url))
.build();
if ((provider && url.getParameter(REGISTER_KEY, true))
|| (!provider && url.getParameter(SUBSCRIBE_KEY, true))) {
registryList.add(url);
}
}
}
}
}
return registryList;
}
}
组装服务URL和解析服务接口
URL对象是dubbo在自己框架内部定义的一个对象,记录的是dubbo的各种配置信息,URL 之于 Dubbo,犹如水之于鱼,非常重要,service对应的URL创建的过程非常繁琐,需要将配置的各种信息以及接口的信息汇总到一个map中,生成一个能够完整描述一个service的URL,其中还涉及到通过反射,去获取某些配置对象中的getter方法,并调用该方法获取信息,注入进map;解析<dubbo:method> 标签等等。方法大致展示如下,这部分代码非常复杂,代码也很长,截取部分,这部分代码新版本没有大的改动,具体分析可以查看官网(我是真的看不懂,只能用到时候在研究了。。)。
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
String name = protocolConfig.getName();
if (StringUtils.isEmpty(name)) {
name = DUBBO;
}
Map<String, String> map = new HashMap<String, String>();
// 添加 side信息到 map 中,新版本、版本号、时间戳以及进程号没有了。
map.put(SIDE_KEY, PROVIDER_SIDE);
// 通过反射将对象的字段信息添加到 map 中
ServiceConfig.appendRuntimeParameters(map);
AbstractConfig.appendParameters(map, getMetrics());
AbstractConfig.appendParameters(map, getApplication());
AbstractConfig.appendParameters(map, getModule());
// remove 'default.' prefix for configs from ProviderConfig
// appendParameters(map, provider, Constants.DEFAULT_KEY);
AbstractConfig.appendParameters(map, provider);
AbstractConfig.appendParameters(map, protocolConfig);
AbstractConfig.appendParameters(map, this);
MetadataReportConfig metadataReportConfig = getMetadataReportConfig();
if (metadataReportConfig != null && metadataReportConfig.isValid()) {
map.putIfAbsent(METADATA_KEY, REMOTE_METADATA_STORAGE_TYPE);
}
if (CollectionUtils.isNotEmpty(getMethods())) {
for (MethodConfig method : getMethods()) {
AbstractConfig.appendParameters(map, method, method.getName());
String retryKey = method.getName() + ".retry";
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey);
if ("false".equals(retryValue)) {
map.put(method.getName() + ".retries", "0");
}
}
List<ArgumentConfig> arguments = method.getArguments();
if (CollectionUtils.isNotEmpty(arguments)) {
// 获取 ArgumentConfig 列表
for (遍历 ArgumentConfig 列表) {
if (type 不为 null,也不为空串) { // 分支1
1. 通过反射获取 interfaceClass 的方法列表
for (遍历方法列表) {
1. 比对方法名,查找目标方法
2. 通过反射获取目标方法的参数类型数组 argtypes
if (index != -1) { // 分支2
1. 从 argtypes 数组中获取下标 index 处的元素 argType
2. 检测 argType 的名称与 ArgumentConfig 中的 type 属性是否一致
3. 添加 ArgumentConfig 字段信息到 map 中,或抛出异常
} else { // 分支3
1. 遍历参数类型数组 argtypes,查找 argument.type 类型的参数
2. 添加 ArgumentConfig 字段信息到 map 中
}
}
} else if (index != -1) { // 分支4
1. 添加 ArgumentConfig 字段信息到 map 中
}
}
}
} // end of methods for
}
// export service
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);
// You can customize Configurator to append extra parameters
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.hasExtension(url.getProtocol())) {
url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
.getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}
...
}
创建服务对应Invoker对象
上面生成完URL后,URL里已经有了能代表服务本身的所有信息,接下来就会拿着这个URL去进行本地暴露和远程暴露,但是暴露的是服务,所以需要先根据URL里的信息生成一个Invoker对象,Invoker对象由ProxyFactory创建出来,内部使用javassist构建class对象(给对象加上附加的字段和方法增强),可以理解为一个代理对象,这个对象负责真实接口的调用,可以完成各种所需的定制功能,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现,内部维护
暴露服务到本地和远程
构造出了Invoker对象之后终于到了最后一步,服务暴露过程,服务暴露根据配置可以分为两个过程:本地暴露和远程暴露。
由于dubbo用于暴露服务的Protocol也是经过层层包装的,最终生成ProtocolxxxWrapper,这些Wrapper会对暴露的Invoker再做对应的增强,本地暴露的过程中,最主要的是要在invoker中增加一系列拦截器:
这些拦截器各种有各自的作用,是个很常见的责任链模式,具体拦截器不再详细分析,有需求可以自己研究或新增。本地暴露的最终结果是将生产的exporter放在serviceConfig的exporters列表里保存起来,本地暴露的流程大致就是这样。
public class ServiceConfig<T> extends AbstractServiceConfig {
private void exportLocal(URL url) {
if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
URL local = URL.valueOf(url.toFullString())
.setProtocol(Constants.LOCAL_PROTOCOL)
.setHost(LOCALHOST)
.setPort(0);
//这里这个protocol是经过包装的protocol,实例是ProtocolListenerWrapper,具体的包装方法实现再下面。
Exporter<?> exporter = protocol.export(
proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
exporters.add(exporter);
logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
}
}
}
public class ProtocolListenerWrapper implements Protocol {
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
//到这里的protocol仍是一个包装类
if (Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
return protocol.export(invoker);
}
return new ListenerExporterWrapper<T>(protocol.export(invoker),
Collections.unmodifiableList(ExtensionLoader.getExtensionLoader(ExporterListener.class)
.getActivateExtension(invoker.getUrl(), Constants.EXPORTER_LISTENER_KEY)));
}
}
public class ProtocolFilterWrapper implements Protocol {
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
//这里的protocol还是一个包装对象
if (Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
return protocol.export(invoker);
}
//本地暴露Invoker在这里和过滤器关联。
return protocol.export(buildInvokerChain(invoker, Constants.SERVICE_FILTER_KEY, Constants.PROVIDER));
}
}
public class QosProtocolWrapper implements Protocol {
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
//这里调到真正的protocol
if (Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
startQosServer(invoker.getUrl());
return protocol.export(invoker);
}
return protocol.export(invoker);
}
}
外部暴露的过程比较复杂,大体上又分为两部,1、通过DubboProtocol监听通讯端口(一般是通过netty),可以理解为在底层启动一个服务器,接收外部(一般是TCP)请求。2、通过RegistryProtocol将服务注册在注册中心上。(严格地说,还有第三步服务订阅)下面重点分析一下各自的流程:
public class RegistryProtocol implements Protocol {
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
URL registryUrl = getRegistryUrl(originInvoker);
// url to export locally
URL providerUrl = getProviderUrl(originInvoker);
// Subscribe the override data
// FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call
// the same service. Because the subscribed is cached key with the name of the service, it causes the
// subscription information to cover.
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
//export invoker 第一步服务暴露
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
// url to registry
final Registry registry = getRegistry(originInvoker);
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);
// decide if we need to delay publish
boolean register = providerUrl.getParameter(REGISTER_KEY, true);
//第二步服务注册
if (register) {
register(registryUrl, registeredProviderUrl);
}
// Deprecated! Subscribe to override rules in 2.6.x or before.
// 第三部订阅注册中心
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
notifyExport(exporter);
//Ensure that a new exporter instance is returned every time export
return new DestroyableExporter<>(exporter);
}
}
- DubboProtocol
这一步主要是启动netty服务器,并将服务器信息和exporter缓存到DubboProtocol的两个不同的Map中,重点在创建Transporter并启动的过程,这部分也是可以通过SPI扩展的,默认是NettyTransporter,最后会调到下面的方法,在创建了解码Handler和头部交换Handler之后进行绑定,重点是Transporters 的 bind 方法,由于SPI的自适应机制,会进入到NettyTransporter的bind方法,这个方法会在各种变量初始化之后启动netty服务器,并监听请求,到这里DubboProtocol的export方法就执行完了,这部分流程比较长,但整体逻辑不复杂,新老版本改动也不大,官网介绍比较详细,就不再多介绍了。public class HeaderExchanger implements Exchanger { @Override public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException { return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))); } }
- RegistryProtocol
程序执行到这里的时候,netty服务器已经启动,并且已经把包装后的Exporter放进了ResigtryProtocol对象自身的bounds缓存中,接下来就是获取注册中心,把服务注册到注册中心的过程,获取注册中心的过程也是通过SPI自适应机制获取的,这部分整体流程也比较清晰,注册的最终结果就是把本服务的信息注册到注册中心上去。public class RegistryProtocol implements Protocol { public void register(URL registryUrl, URL registeredProviderUrl) { // 获取注册中心 Registry registry = registryFactory.getRegistry(registryUrl); // 服务信息注册 registry.register(registeredProviderUrl); ProviderModel model = ApplicationModel.getProviderModel(registeredProviderUrl.getServiceKey()); model.addStatedUrl(new ProviderModel.RegisterStatedURL( registeredProviderUrl, registryUrl, true )); } }
Dubbo服务启动的流程先总结到这里,整体流程虽然复杂,但思路还是清晰的。