Dubbo调用过程参与者有服务提供方、注册中心、服务消费方。其中注册中心是单独部署的,服务提供方和消费方是集成在业务里面的,今天来分析下服务提供方服务暴露的流程。
不管通过哪种启动方式(Dubbo的几种启动方式),服务接口暴露的起点都是从ServiceConfig.export方法开始的,这里先简单分析下通过xml中配置dubbo到export的流程。
从xml配置到ServiceConfig.export
xml中dubbo服务提供方的配置:
可以看到,配置文件中使用的是dubbo的自定义标签,那么,就先来看dubbo的自定义标签解析器。通过dubbo的spring.handlers文件,知道dubbo标签是通过
DubboNamespaceHandler来处理的:
registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
可以看到解析service标签委托给了DubboBeanDefinitionParser,beanClass为ServiceBean,继续看DubboBeanDefinitionParser.parse方法:
private static BeanDefinition parse(Element element, ParserContext parserContext, Class> beanClass, boolean required) {
RootBeanDefinition beanDefinition = new RootBeanDefinition();
beanDefinition.setBeanClass(beanClass);
beanDefinition.setLazyInit(false);
...
parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
...
parse的代码做了删减,主要的逻辑是配置bean的属性并往spring容器中注册了beanClass的beanDefinition,此时是ServiceBean。所以通过xml进行配置dubbo其实主要是在完成一件事情,就是配置好ServiceBean的属性,然后将它注册到spring容器中。而ServiceBean是ServiceConfig的子类,同时实现了InitializingBean、ApplicationContextAware、ApplicationListener
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
export();
}
}
public void afterPropertiesSet() throws Exception {
......
if (!supportedApplicationListener) {
export();
}
}
ServiceBean的export中调用了super.export,也就是ServiceConfig.export方法。
@Override
public void export() {
super.export();
// Publish ServiceBeanExportedEvent
publishExportEvent();
}
上面流程分析了xml中配置dubbo也是通过ServiceConfig.export方法来暴露服务的,下面从ServiceConfig.export来分析服务启动的过程。
ServiceConfig.export服务暴露流程分析
首先从需求角度来大至服务暴露需要完成哪些工作?
服务暴露成功后,消费端就可以通过接口透明的调用服务了,由于是RPC调用,是通过网络访问来完成的,所以服务暴露肯定需要创建一个网络服务器等待消费者来连接通信;要进行通信肯定需要定义协议,才能保证双方能够正常的通信;消费方并不是直接写死的服务提供方主机和端口,而是从注册中心拉取的,这样才灵活可扩展,所以服务暴露的过程肯定需要将自己的信息注册到注册中心。下面就从ServiceConfig.export源码开始看下这个过程
public synchronized void export() {
checkAndUpdateSubConfigs();
if (!shouldExport()) {
return;
}
if (shouldDelay()) {
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
doExport();
}
}
做了一些校验和延时暴露的判断,关键流程在doExport中:
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();
}
通过标识位设置避免重复暴露,然后进入doExportUrls:
private void doExportUrls() {
List registryURLs = loadRegistries(true);
for (ProtocolConfig protocolConfig : protocols) {
String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
ApplicationModel.initProviderModel(pathKey, providerModel);
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
迭代针对每一种配置的协议调用doExportUrlsFor1Protocol进行暴露(一个接口可以通过多种协议同时进行暴露),这个方法里面开始了上述逻辑,方法体比较长,节选几个比较关键的部分:
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs) {
//省略一些参数配置的部分
// export service
String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = this.findConfigedPorts(protocolConfig, name, map);
//将协议、接口、主机、端口和配置参数封装到URL对象里面
URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
...
//下面是服务暴露的关键代码
Invoker> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
最后几行是服务暴露的关键代码,着重看下。
- PROXY_FACTORY.getInvoker是将服务的接口的实现类封装成Invoker, Invoker是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。当远程请求过来是,就是通过这个invoker来调用服务端接口实现类的具体方法的。
- protocol.export(wrapperInvoker)是通过协议将invoker进行暴露,是最关键的地方了。接下来跟进这个方法里面详细查看,由于protocol是一个自适应拓展点,而且Protocol接口没有默认的自适应实现类(有关拓展点可查看Dubbo拓展点),所以protocol.export的执行是根据URL中protocol参数来决定的。
在有注册中心的情况下(没有注册中心的情况比较简单,且包含在了有注册中心的情况下),invoker中的url是registryURL,协议参数为registry,所以export方法调用的是RegistryProtocol. export:
@Override
public Exporter export(final Invoker 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 exporter = doLocalExport(originInvoker, providerUrl);
// url to registry
final Registry registry = getRegistry(originInvoker);
final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
ProviderInvokerWrapper providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
registryUrl, registeredProviderUrl);
//to judge if we need to delay publish
boolean register = registeredProviderUrl.getParameter("register", true);
if (register) {
register(registryUrl, registeredProviderUrl);
providerInvokerWrapper.setReg(true);
}
// Deprecated! Subscribe to override rules in 2.6.x or before.
registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
exporter.setRegisterUrl(registeredProviderUrl);
exporter.setSubscribeUrl(overrideSubscribeUrl);
//Ensure that a new exporter instance is returned every time export
return new DestroyableExporter<>(exporter);
}
其中doLocalExport(originInvoker, providerUrl)是本地暴露,而register(registryUrl, registeredProviderUrl)则是将本地暴露的URL注册到注册中心了。
private ExporterChangeableWrapper doLocalExport(final Invoker originInvoker, URL providerUrl) {
String key = getCacheKey(originInvoker);
return (ExporterChangeableWrapper) bounds.computeIfAbsent(key, s -> {
Invoker> invokerDelegate = new InvokerDelegate<>(originInvoker, providerUrl);
return new ExporterChangeableWrapper<>((Exporter) protocol.export(invokerDelegate), originInvoker);
});
}
本地暴露的时候,也是调用的protocol.export,不过此时的invoker中的URL是providerURL,协议参数为具体的协议,默认是dubbo,会进入dubboProtocol.export方法,如果没有配置注册中心的话,也是直接进入这个方法:
@Override
public Exporter export(Invoker invoker) throws RpcException {
URL url = invoker.getUrl();
// export service.
String key = serviceKey(url);
DubboExporter exporter = new DubboExporter(invoker, key, exporterMap);
exporterMap.put(key, exporter);
//export an stub service for dispatching event
Boolean isStubSupportEvent = url.getParameter(STUB_EVENT_KEY, DEFAULT_STUB_EVENT);
Boolean isCallbackservice = url.getParameter(IS_CALLBACK_SERVICE, false);
if (isStubSupportEvent && !isCallbackservice) {
String stubServiceMethods = url.getParameter(STUB_EVENT_METHODS_KEY);
if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
if (logger.isWarnEnabled()) {
logger.warn(new IllegalStateException("consumer [" + url.getParameter(INTERFACE_KEY) +
"], has set stubproxy support event ,but no stub methods founded."));
}
} else {
stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
}
}
openServer(url);
optimizeSerialization(url);
return exporter;
}
看到openServer(url)猜测应该就是开启服务器,监听远程网络连接了,源码就不一一列举了,简单理解就是开启了NettyServer并注册了requestHandler来处理入站数据。
这样本地暴露就完成了,结果是创建了一个netty服务器等待远程连接来访问,这个时候已经可以通过消费端来调用接口了,但是写死的服务端ip、端口不够灵活,所以一般都使用注册中心来进行服务发现。回过头来看服务注册到注册中心的流程:register(registryUrl, registeredProviderUrl):
public void register(URL registryUrl, URL registeredProviderUrl) {
Registry registry = registryFactory.getRegistry(registryUrl);
registry.register(registeredProviderUrl);
}
实际上就是通过工厂方法获取注册中心,然后注册服务提供方的URL,注册中心有多种实现,以zookeeper为例,
@Override
public void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
实现为向zookeeper根据接口参数创建一个临时目录,这样在消费端连接上注册中心后,就可以从对应的接口目录下获取provider的信息了。