Dubbo源码深入学习-服务导出原理
前言
我们在平常开发中使用 dubbo 是需要把服务注册到 zookeeper 中去,然后消费者从 zookeeper 中拿到我们的服务列表进行远程调用,但是 zookeeper 具体如何存放我们的服务信息的呢?这对于我们开发人员来说都是一个黑盒,本篇我们就一块对 dubbo 服务导出的原理进行探索
整体逻辑
服务导出 主要得做三件事情:
1.获取服务的配置信息
2.根据服务的协议信息,启动对应的网络服务器(netty、tomcat、jetty等),用来接收网络请求
3.将服务的信息注册到注册中⼼
4.监听配置的变化
dubbo 服务的参数,除开可以在@Service 注解中去配置,还会继承dubbo 服务所属应⽤(Application)上的配置,还可以在配置中心 或jvm 环境变量 中去配置某个服务的参数,所以首先要做的是确定好当前服务最终的(优先级最高)的参数值。 确定好服务参数之后,就根据所配置的协议 启动对应的网络服务器。在启动网络服务器时,并且在网络服务器接收请求的过程中,都可以从服务参数中获取信息,比如最⼤连接数,线程数,socket超时时间等等。 启动完网络服务器之后,就将服务信息注册到注册中心。同时还有向注册中心注册监听器,监听Dubbo中的动态配置信息变更。
从之前dubbo 和spring 进行整合的源码中我们可以知道,当一个dubbo 服务被spring管理的时候,会生成一个ServiceBean对象,代表这个是一个dubbo 服务,ServiceBean 对象实现了ApplicationListener接口,并监听了容器刷新的事件,那么当容器启动完毕后,就会回调onApplicationEvent方法,在onApplicationEvent方法中会进行服务的导出,如下图
ServiceBean:
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 当前服务没有被导出并且没有卸载,才导出服务
if (!isExported() && !isUnexported()) {
if (logger.isInfoEnabled()) {
logger.info("The service ready on spring started. service: " + getInterface());
}
// 服务导出(服务注册)
export();
}
}
在这里会调用 export()方法进行服务的导出
服务导出
export();方法是将服务导出,注册到注册中心中。
public synchronized void export() {
//1.检查并更新配置
checkAndUpdateSubConfigs();
// 2.检查服务是否需要导出
if (!shouldExport()) {
return;
}
// 3.检查是否需要延迟发布
if (shouldDelay()) {
DELAY_EXPORT_EXECUTOR.schedule(this::doExport, getDelay(), TimeUnit.MILLISECONDS);
} else {
// 4.导出服务
doExport();
}
}
在服务导出之前,我们需要读取最新的配置,再把服务提供者的信息注册到注册中心中去。
1.检查并更新配置
在导出主方法中, checkAndUpdateSubConfigs()是对我们服务的配置参数进行检查和更新,ServiceBean在和Spring整合的时候已经把配置文件的配置和@Service配置的参数添加到ServiceBean的属性中了,但是我们服务的配置项来源有多处,优先级也是不一样的,比如:
- dubbo.properties文件,你可以建⽴这个⽂件,dubbo会去读取这个⽂件的内容作为服务的参数,对应的配置类为PropertiesConfiguration。
- 配置中心,dubbo在2.7版本后就⽀持了分布式配置中⼼,你可以在dubbo-admin中去操作配置中心,分布式配置中心就相当于⼀个远程的dubbo.properties文件,你可以在dubbo-admin中去修改这个dubbo.properties文件,当然配置中心⽀持按应用进行配置,也可以按全局进行配置,对应的配置为AppExternalConfiguration表示应用配置,ExternalConfiguration表示全局配置。
- 系统环境变量,你可以在启动应用程序时,通过-D的⽅式来指定参数,对应的配置类为SystemConfiguration
- @Service注解所配置的参数,对应的配置类叫AbstractConfig
- dubbo-admin的动态配置
配置项可以来源于这几项,这里先说优先级高低结论:
SystemConfiguration -> AppExternalConfiguration -> ExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
当然,服务的参数除开来自于服务的⾃身配置外,还可以来自其上级。 比如如果服务本身没有配置timeout参数,但是如果服务所属的应⽤的配置了timeout,那么这个应用下的服务都会继承这个timeout配置。 所以在确定服务参数时,需要先从上级获取参数,获取之后,如果服务本身配置了相同的参数,那么则进行覆盖。
读取配置流程图:
接下来我们就从源码开始分析:
public void checkAndUpdateSubConfigs() {
// ServiceConfig中的某些属性如果是空的,那么就从ProviderConfig、ModuleConfig、ApplicationConfig中获取
// 补全ServiceConfig中的属性
completeCompoundConfigs();
//省略不重要的代码
// 从配置中心获取配置,包括应用配置和全局配置
// 把获取到的配置放入到Environment中的externalConfigurationMap和appExternalConfigurationMap中
// 并刷新所有的XxConfig的属性(除开ServiceConfig),刷新的意思就是将配置中心的配置覆盖调用XxConfig中的属性
startConfigCenter();
// 如果protocol不是只有injvm协议,表示服务调用不是只在本机jvm里面调用,那就需要用到注册中心
if (!isOnlyInJvm()) {
checkRegistry();
}
// 刷新ServiceConfig
this.refresh();
}
这里有几个主要的方法:
2.1补全配置信息
completeCompoundConfigs();
private void completeCompoundConfigs() {
// 如果配置了provider,那么则从provider中获取信息赋值其他属性,在这些属性为空的情况下
if (provider != null) {
if (application == null) {
setApplication(provider.getApplication());
}
if (module == null) {
setModule(provider.getModule());
}
if (registries == null) {
setRegistries(provider.getRegistries());
}
if (monitor == null) {
setMonitor(provider.getMonitor());
}
if (protocols == null) {
setProtocols(provider.getProtocols());
}
if (configCenter == null) {
setConfigCenter(provider.getConfigCenter());
}
}
// 如果配置了module,那么则从module中获取信息赋值其他属性,在这些属性为空的情况下
if (module != null) {
if (registries == null) {
setRegistries(module.getRegistries());
}
if (monitor == null) {
setMonitor(module.getMonitor());
}
}
// 如果配置了application,那么则从application中获取信息赋值其他属性,在这些属性为空的情况下
if (application != null) {
if (registries == null) {
setRegistries(application.getRegistries());
}
if (monitor == null) {
setMonitor(application.getMonitor());
}
}
}
ServiceConfig中的某些属性如果是空的,那么就从ProviderConfig、ModuleConfig、ApplicationConfig中获取
2.2 从配置中心读取配置并更新配置信息
startConfigCenter();
oid startConfigCenter() {
if (configCenter == null) {
ConfigManager.getInstance().getConfigCenter().ifPresent(cc -> this.configCenter = cc);
}
// 如果配置了ConfigCenter
if (this.configCenter != null) {
// 从其他位置获取配置中心的相关属性信息,比如配置中心地址
this.configCenter.refresh();
// 属性更新后,从远程配置中心获取数据(应用配置,全局配置)
prepareEnvironment();
}
// 从配置中心取到配置数据后,刷新所有的XxConfig中的属性,除开ServiceConfig
ConfigManager.getInstance().refreshAll();
}
this.configCenter.refresh();这个方法主要目的是从读取配置的地方获取我们配置中心的配置信息,比如配置中心地址,方便后续连接配置中心拉取配置
public void refresh() {
try {
//拿到配置项的聚合对象
CompositeConfiguration compositeConfiguration = Environment.getInstance().getConfiguration(getPrefix(), getId());
// 表示XxConfig对象本身- AbstractConfig
Configuration config = new ConfigConfigurationAdapter(this); // ServiceConfig
if (Environment.getInstance().isConfigCenterFirst()) {
// The sequence would be: SystemConfiguration -> AppExternalConfiguration -> ExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
compositeConfiguration.addConfiguration(4, config);
} else {
// The sequence would be: SystemConfiguration -> AbstractConfig -> AppExternalConfiguration -> ExternalConfiguration -> PropertiesConfiguration
compositeConfiguration.addConfiguration(2, config);
}
// loop methods, get override value and set the new value back to method
Method[] methods = getClass().getMethods(); //ServiceBean
for (Method method : methods) {
// 是不是setXX()方法
if (MethodUtils.isSetter(method)) {
// 获取xx配置项的value
String value = StringUtils.trim(compositeConfiguration.getString(extractPropertyName(getClass(), method)));
// isTypeMatch() is called to avoid duplicate and incorrect update, for example, we have two 'setGeneric' methods in ReferenceConfig.
if (StringUtils.isNotEmpty(value) && ClassUtils.isTypeMatch(method.getParameterTypes()[0], value)) {
method.invoke(this, ClassUtils.convertPrimitive(method.getParameterTypes()[0], value));
}
// 是不是setParameters()方法
} else if (isParametersSetter(method)) {
// 获取parameter配置项的value
String value = StringUtils.trim(compositeConfiguration.getString(extractPropertyName(getClass(), method)));
if (StringUtils.isNotEmpty(value)) {
Map<String, String> map = invokeGetParameters(getClass(), this);
map = map == null ? new HashMap<>() : map;
map.putAll(convert(StringUtils.parseParameters(value), ""));
invokeSetParameters(getClass(), this, map);
}
}
}
} catch (Exception e) {
logger.error("Failed to override ", e);
}
}
在配置刷新方法中,会生成CompositeConfiguration对象,CompositeConfiguration其实就是配置项的集合,内部使用了一个List来存放我们各个配置项来源的配置信息。
获取配置项对象的主要逻辑就是CompositeConfiguration compositeConfiguration = Environment.getInstance().getConfiguration(getPrefix(), getId());
public CompositeConfiguration getConfiguration(String prefix, String id) {
CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
// Config center has the highest priority
// JVM环境变量 SystemConfiguration
compositeConfiguration.addConfiguration(this.getSystemConfig(prefix, id));
// 操作系统环境变量 EnvironmentConfiguration
compositeConfiguration.addConfiguration(this.getEnvironmentConfig(prefix, id));
// 配置中心APP配置 InmemoryConfiguration - >appExternalConfigurationMap
compositeConfiguration.addConfiguration(this.getAppExternalConfig(prefix, id));
// 配置中心Global配置 InmemoryConfiguration - >externalConfigurationMap
compositeConfiguration.addConfiguration(this.getExternalConfig(prefix, id));
// dubbo.properties中的配置 PropertiesConfiguration
compositeConfiguration.addConfiguration(this.getPropertiesConfig(prefix, id));
return compositeConfiguration;
}
通过这个添加顺序,可以看出我们配置项加载优先级顺序:
JVM环境变量>=操作系统环境变量>=配置中心APP配置>=配置中心Global配置>=dubbo.properties
接下来也会把当前ServiceConfig保证成一个Configuration添加到链表集合中,然后通过configCenterFirst参数判断放在链表的哪个位置。
如果配置为ture:
放在链表下标为4的位置:
优先级顺序为:SystemConfiguration -> AppExternalConfiguration -> ExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
如果配置为false:
放在链表下标为2的位置:
优先级顺序: SystemConfiguration -> AbstractConfig -> AppExternalConfiguration -> ExternalConfiguration -> PropertiesConfiguration
接下来就是遍历配置项对象的所有方法,拿到对应属性的set方法以及属性名称,从配置项中调用compositeConfiguration.getString拿到对应的配置项的值赋值给我们的属性。
2.3 属性更新,拉取配置中心配置
prepareEnvironment();
private void prepareEnvironment() {
if (configCenter.isValid()) {
if (!configCenter.checkOrUpdateInited()) {
return;
}
// 动态配置中心,管理台上的配置中心
DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
// 如果是zookeeper,获取的就是/dubbo/config/dubbo/dubbo.properties节点中的内容
String configContent = dynamicConfiguration.getProperties(configCenter.getConfigFile(), configCenter.getGroup());
String appGroup = application != null ? application.getName() : null;
String appConfigContent = null;
if (StringUtils.isNotEmpty(appGroup)) {
// 获取的就是/dubbo/config/dubbo-demo-consumer-application/dubbo.properties节点中的内容
// 这里有bug
appConfigContent = dynamicConfiguration.getProperties
(StringUtils.isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),
appGroup
);
}
try {
Environment.getInstance().setConfigCenterFirst(configCenter.isHighestPriority());
//更新配置中心全局配置
Environment.getInstance().updateExternalConfigurationMap(parseProperties(configContent));
//更新配置中心应用配置
Environment.getInstance().updateAppExternalConfigurationMap(parseProperties(appConfigContent));
} catch (IOException e) {
throw new IllegalStateException("Failed to parse configurations from Config Center.", e);
}
}
}
这里就是通过连接配置中心拉取全局配置和应用配置,然后更新我们对应的配置中心的配置项内容。
2.4 刷新各个config对象的值
读取完配置中心的配置信息,就要刷新各个config对象的值。
public void refreshAll() {
// refresh all configs here,
getApplication().ifPresent(ApplicationConfig::refresh);
getMonitor().ifPresent(MonitorConfig::refresh);
getModule().ifPresent(ModuleConfig::refresh);
getProtocols().values().forEach(ProtocolConfig::refresh);
getRegistries().values().forEach(RegistryConfig::refresh);
getProviders().values().forEach(ProviderConfig::refresh);
getConsumers().values().forEach(ConsumerConfig::refresh);
}
可以看到这一块我们没有刷新ServiceConfig的属性,因为这一步我们是放到最后一步实现的。回到checkAndUpdateSubConfigs主方法中。
在这一步重新更新我们ServiceConfig的属性值,至此我们的配置项拿到的就是最新的配置。
2.生成URL
我们知道Dubbo中有个概念为URL,URL就代表一个资源,Dubbo会先把注册中心配置,服务提供者配置等生成URL,再根据URL信息调用相应的扩展点进行服务导出。
服务导出生成URL逻辑比较长,这里先上流程图
我们先看服务导出的主方法
在检查并更新本地配置之后,判断是否可以导出,是否要延迟导出,如果不需要延迟导出且服务没有导出过,我们就可以拿着最新的配置进行服务导出,也就是doExport()方法,这里加锁是防止重复导出服务。
在doExport()方法中,如果我们的@Service属性没有配置Path属性,则把path属性指定为服务的全类路径,然后调用doExportUrls();方法进行服务的导出。
private void doExportUrls() {
// registryURL 表示一个注册中心
List<URL> registryURLs = loadRegistries(true);
for (ProtocolConfig protocolConfig : protocols) {
// pathKey = group/contextpath/path:version
// 例子:myGroup/user/org.apache.dubbo.demo.DemoService:1.0.1
String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
// ProviderModel中存在服务提供者访问路径,实现类,接口,以及接口中的各个方法对应的ProviderMethodModel
// ProviderMethodModel表示某一个方法,方法名,所属的服务的,
ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass);
// ApplicationModel表示应用中有哪些服务提供者和引用了哪些服务
ApplicationModel.initProviderModel(pathKey, providerModel);
// 重点
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
doExportUrls()在这个方法中
3.1 获取注册中心URL
根据注册中心的配置,获取对应的URL,方法逻辑是loadRegistries(true)方法
protected List<URL> loadRegistries(boolean provider) {
// check && override if necessary
List<URL> registryList = new ArrayList<URL>();
if (CollectionUtils.isNotEmpty(registries)) {
for (RegistryConfig config : registries) {
String address = config.getAddress();
// 如果注册中心没有配地址,则地址为0.0.0.0
if (StringUtils.isEmpty(address)) {
address = ANYHOST_VALUE;
}
// 如果注册中心的地址不是"N/A"
if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
Map<String, String> map = new HashMap<String, String>();
// 把application中的参数放入map中,注意,map中的key是没有prefix的
appendParameters(map, application);
// 把config中的参数放入map中,注意,map中的key是没有prefix的
// config是RegistryConfig,表示注册中心
appendParameters(map, config);
// 此处path值固定为RegistryService.class.getName(),因为现在是在加载注册中心
map.put(PATH_KEY, RegistryService.class.getName());
// 把dubbo的版本信息和pid放入map中
appendRuntimeParameters(map);
// 如果map中如果没有protocol,那么默认为dubbo
if (!map.containsKey(PROTOCOL_KEY)) {
map.put(PROTOCOL_KEY, DUBBO_PROTOCOL);
}
// 构造注册中心url,地址+参数
List<URL> urls = UrlUtils.parseURLs(address, map);
for (URL url : urls) {
url = URLBuilder.from(url)
.addParameter(REGISTRY_KEY, url.getProtocol())
.setProtocol(REGISTRY_PROTOCOL)
.build();
//到这里我们生成的注册中心的URL为
//registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-demo-provider1-application&dubbo=2.0.2&logger=log4j&pid=10524®istry=zookeeper&release=2.7.0×tamp=1702382286275
// 这里是服务提供者和服务消费者区别的逻辑
// 如果是服务提供者,获取register的值,如果为false,表示该服务不注册到注册中心
// 如果是服务消费者,获取subscribe的值,如果为false,表示该引入的服务不订阅注册中心中的数据
if ((provider && url.getParameter(REGISTER_KEY, true))
|| (!provider && url.getParameter(SUBSCRIBE_KEY, true))) {
registryList.add(url);
}
}
}
}
}
return registryList;
}
生成注册中心URL的过程其实就是
1.把应用的配置和注册中心的配置放在一个Map中
2.根据Map生成一个URL
3.设置URL协议参数为registry
3.添加到注册中心URL集合中
3.2 遍历协议配置,生成协议URL
private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
// protocolConfig表示某个协议,registryURLs表示所有的注册中心
// 如果配置的某个协议,没有配置name,那么默认为dubbo
String name = protocolConfig.getName();
if (StringUtils.isEmpty(name)) {
name = DUBBO;
}
// 这个map表示服务url的参数
Map<String, String> map = new HashMap<String, String>();
map.put(SIDE_KEY, PROVIDER_SIDE);
appendRuntimeParameters(map);
// 监控中心参数
appendParameters(map, metrics);
// 应用相关参数
appendParameters(map, application);
// 模块相关参数
appendParameters(map, module);
// 提供者相关参数
appendParameters(map, provider);
// 协议相关参数
appendParameters(map, protocolConfig);
// 服务本身相关参数
appendParameters(map, this);
// 服务中某些方法参数,这块是针对服务的某个方法进行配置
//比如在@Service注解中 加method注解并指定配置,在这里会进行解析,放到Map中
if (CollectionUtils.isNotEmpty(methods)) {
.
}
// Token是为了防止服务不经过注册中心被消费者直接调用(伪造http请求)
if (!ConfigUtils.isEmpty(token)) {
if (ConfigUtils.isDefault(token)) {
map.put(TOKEN_KEY, UUID.randomUUID().toString());
} else {
map.put(TOKEN_KEY, token);
}
}
// export service
// 通过该host和port访问该服务
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);
// dubbo://172.16.32.39:20881/com.lx.service.DemoService?anyhost=true&application=dubbo-provider-demo&bind.ip=172.16.32.39&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.lx.service.DemoService&methods=sayHello&pid=16856&qos.enable=false&release=2.7.5&revision=default&side=provider×tamp=1702000516738&version=default
String scope = url.getParameter(SCOPE_KEY); // scope可能为null,remote, local,none
// don't export when none is configured
if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
// 如果scope为none,则不会进行任何的服务导出,既不会远程,也不会本地
// export to local if the config is not remote (export to remote only when config is remote)
if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
// 如果scope不是remote,则会进行本地导出,会把当前url的protocol改为injvm,然后进行导出
exportLocal(url);
}
// export to remote if the config is not local (export to local only when config is local)
if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
// 如果scope不是local,则会进行远程导出
if (CollectionUtils.isNotEmpty(registryURLs)) {
// 如果有注册中心,则将服务注册到注册中心
for (URL registryURL : registryURLs) {
//if protocol is only injvm ,not register
// 如果是injvm,则不需要进行注册中心注册
if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
continue;
}
// 该服务是否是动态,对应zookeeper上表示是否是临时节点,对应dubbo中的功能就是静态服务
url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
// 拿到监控中心地址
URL monitorUrl = loadMonitor(registryURL);
// 当前服务连接哪个监控中心
if (monitorUrl != null) {
url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString());
}
// 服务的register参数,如果为true,则表示要注册到注册中心
if (logger.isInfoEnabled()) {
if (url.getParameter(REGISTER_KEY, true)) {
logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
} else {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
}
// For providers, this is used to enable custom proxy to generate invoker
// 服务使用的动态代理机制,如果为空则使用javassit
String proxy = url.getParameter(PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(PROXY_KEY, proxy);
}
// 生成一个当前服务接口的代理对象
// 使用代理生成一个Invoker,Invoker表示服务提供者的代理,可以使用Invoker的invoke方法执行服务
// 对应的url为 dubbo://172.16.32.39:20881/com.lx.service.DemoService?anyhost=true&application=dubbo-provider-demo&bind.ip=172.16.32.39&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.lx.service.DemoService&methods=sayHello&pid=16856&qos.enable=false&release=2.7.5&revision=default&side=provider×tamp=1702000516738&version=default
// 这个Invoker中包括了服务的实现者、服务接口类、服务的注册地址(针对当前服务的,参数export指定了当前服务)
// 此invoker表示一个可执行的服务,调用invoker的invoke()方法即可执行服务,同时此invoker也可用来导出
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
// invoker.invoke(Invocation)
// DelegateProviderMetaDataInvoker也表示服务提供者,包括了Invoker和服务的配置
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 使用特定的协议来对服务进行导出,这里的协议为RegistryProtocol,导出成功后得到一个Exporter
// 1. 先使用RegistryProtocol进行服务注册
// 2. 注册完了之后,使用DubboProtocol进行导出
// 到此为止做了哪些事情? ServiceBean.export()-->刷新ServiceBean的参数-->得到注册中心URL和协议URL-->遍历每个协议URL-->组成服务URL-->生成可执行服务Invoker-->导出服务
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
} else {
// 没有配置注册中心时,也会导出服务
if (logger.isInfoEnabled()) {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
/**
* @since 2.7.0
* ServiceData Store
*/
// 根据服务url,讲服务的元信息存入元数据中心
MetadataReportService metadataReportService = null;
if ((metadataReportService = getMetadataReportService()) != null) {
metadataReportService.publishProvider(url);
}
}
}
this.urls.add(url);
}
这个方法有点长,我们先说大体逻辑:
1.判断当前的协议配置是否配置name,没有配置就是默认dubbo协议
2.把监控中心配置,应用配置,模块模式,生产者配置…服务本身配置放在一个map中,为后来生成url做准备
3.把接口的所有方法放在map中
4.判断是否配置token参数,如果配置了,把token参数放在map中(这个是防止消费端不通过注册中心拿服务,如果配置了,在调用的时候会进行校验)
5.获取暴露服务的ip加端口,放在map中
6.根据map、path生成一个url
编码
dubbo://172.16.32.39:20881/com.lx.service.DemoService?anyhost=true&application=dubbo-provider-demo&bind.ip=172.16.32.39&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.lx.service.DemoService&methods=sayHello&pid=16856&qos.enable=false&release=2.7.5&revision=default&side=provider×tamp=1702000516738&version=default
7.进行本地导出
private void exportLocal(URL url) {
URL local = URLBuilder.from(url)
.setProtocol(LOCAL_PROTOCOL)
.setHost(LOCALHOST_VALUE)
.setPort(0)
.build();
//把url的协议切换为injvm,name通过SPI机制,调用export会调用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);
}
我们重点看远程导出,本地导出这边不详细介绍了。
3.远程导出
if (CollectionUtils.isNotEmpty(registryURLs)) {
// 如果有注册中心,则将服务注册到注册中心
for (URL registryURL : registryURLs) {
//if protocol is only injvm ,not register
// 如果是injvm,则不需要进行注册中心注册
if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
continue;
}
// 该服务是否是动态,对应zookeeper上表示是否是临时节点,对应dubbo中的功能就是静态服务
url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
// 拿到监控中心地址
URL monitorUrl = loadMonitor(registryURL);
// 当前服务连接哪个监控中心
if (monitorUrl != null) {
url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString());
}
// 服务的register参数,如果为true,则表示要注册到注册中心
if (logger.isInfoEnabled()) {
if (url.getParameter(REGISTER_KEY, true)) {
logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
} else {
logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
}
}
// For providers, this is used to enable custom proxy to generate invoker
// 服务使用的动态代理机制,如果为空则使用javassit
String proxy = url.getParameter(PROXY_KEY);
if (StringUtils.isNotEmpty(proxy)) {
registryURL = registryURL.addParameter(PROXY_KEY, proxy);
}
// 生成一个当前服务接口的代理对象
// 使用代理生成一个Invoker,Invoker表示服务提供者的代理,可以使用Invoker的invoke方法执行服务
// 这个Invoker中包括了服务的实现者、服务接口类、服务的注册地址(针对当前服务的,参数export指定了当前服务)
// 此invoker表示一个可执行的服务,调用invoker的invoke()方法即可执行服务,同时此invoker也可用来导出
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
// invoker.invoke(Invocation)
// DelegateProviderMetaDataInvoker也表示服务提供者,包括了Invoker和服务的配置
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
// 使用特定的协议来对服务进行导出,这里的协议为RegistryProtocol,导出成功后得到一个Exporter
// 1. 先使用RegistryProtocol进行服务注册
// 2. 注册完了之后,使用DubboProtocol进行导出
// 到此为止做了哪些事情? ServiceBean.export()-->刷新ServiceBean的参数-->得到注册中心URL和协议URL-->遍历每个协议URL-->组成服务URL-->生成可执行服务Invoker-->导出服务
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);
}
远程导出主逻辑:
1.服务协议url添加动态配置参数和监控中心参数
2.根据@Service是否配置了proxy参数来决定采用那种创建代理的方法,如果没有配采用默认的动态代理javassit
3.根据SPI机制采用对应的动态代理工厂获得一个Invoker对象,后续进行网络远程调用会调用Invoker的invoke的方法
4.把Invoker和服务的配置信息包装
这个Invoker中包括了服务的实现者、服务接口类、服务的注册信息URL+服务本身信息URL(针对当前服务的,参数export指定了当前服务),并且invoker表示一个可执行的服务,调用invoker的invoke()方法即可执行服务,在调用的时候会接受一个Invocation对象,这个是消费者传递过来的,包含了我们服务的接口,参数类型列表,参数列表,方法名称等信息。
5.进行服务导出,生成一个Exporter,并发expoter放在集合中,Exporter的作用主要是可以进行服务的销毁。
这里的protocol也是通过SPI机制来判断是哪个对象的,我们生成的invoker对象是有URL参数的,根据URL参数,可以拿到具体接口的实现类,目前URL的协议参数是registry,所以protocol调用export方法的时候,会来到RegistryProtocol方法,
这里的protocol是registry,所以会来到RegistryProtocol的export方法中。
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
//拿到注册中心的URL,这里拿到注册参数,设置URL的协议为zookeeper
//zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider-demo&dubbo=2.0.2&export=dubbo%3A%2F%2F172.16.32.39%3A20881%2Fcom.lx.service.DemoService%3Fanyhost%3Dtrue%26application%3Ddubbo-provider-demo%26bind.ip%3D172.16.32.39%26bind.port%3D20881%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcom.lx.service.DemoService%26methods%3DsayHello%26pid%3D16856%26qos.enable%3Dfalse%26release%3D2.7.5%26revision%3Ddefault%26side%3Dprovider%26timestamp%3D1702000516738%26version%3Ddefault&pid=16856&qos.enable=false&release=2.7.5×tamp=1702000049821
URL registryUrl = getRegistryUrl(originInvoker); //
//拿到服务的URL
//dubbo://172.16.32.39:20881/com.lx.service.DemoService?anyhost=true&application=dubbo-provider-demo&bind.ip=172.16.32.39&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.lx.service.DemoService&methods=sayHello&pid=16856&qos.enable=false&release=2.7.5&revision=default&side=provider×tamp=1702000516738&version=default
URL providerUrl = getProviderUrl(originInvoker); //
// overrideSubscribeUrl是老版本的动态配置监听url,表示了需要监听的服务以及监听的类型(configurators, 这是老版本上的动态配置)
// 在服务提供者url的基础上,生成一个overrideSubscribeUrl,协议为provider://,增加参数category=configurators&check=false
//provider://172.16.32.39:20881/com.lx.service.DemoService?anyhost=true&application=dubbo-provider-demo&bind.ip=172.16.32.39&bind.port=20881&category=configurators&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.lx.service.DemoService&methods=sayHello&pid=16856&qos.enable=false&release=2.7.5&revision=default&side=provider×tamp=1702000516738&version=default
final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
//新建一个监听器
final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
// // 在这个方法里会利用providerConfigurationListener和serviceConfigurationListener去重写providerUrl
// providerConfigurationListener表示应用级别的动态配置监听器,providerConfigurationListener是RegistyProtocol的一个属性
// serviceConfigurationListener表示服务级别的动态配置监听器,serviceConfigurationListener是在每暴露一个服务时就会生成一个
// 这两个监听器都是新版本中的监听器
// 新版本监听的zk路径是:
// 服务: /dubbo/config/dubbo/org.apache.dubbo.demo.DemoService.configurators节点的内容
// 应用: /dubbo/config/dubbo/dubbo-demo-provider-application.configurators节点的内容
// 注意,要和配置中心的路径区分开来,配置中心的路径是:
// 应用:/dubbo/config/dubbo/org.apache.dubbo.demo.DemoService/dubbo.properties节点的内容
// 全局:/dubbo/config/dubbo/dubbo.properties节点的内容
providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
// 根据动态配置重写了providerUrl之后,就会调用DubboProtocol或HttpProtocol去进行导出服务了
final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
final Registry registry = getRegistry(originInvoker);
// 得到存入到注册中心去的providerUrl,会对服务提供者url中的参数进行简化
final URL registeredProviderUrl = getRegisteredProviderUrl(providerUrl, registryUrl);
// 将当前服务提供者Invoker,以及该服务对应的注册中心地址,以及简化后的服务url存入ProviderConsumerRegTable
ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker,
registryUrl, registeredProviderUrl);
//to judge if we need to delay publish
//是否需要注册到注册中心
boolean register = providerUrl.getParameter(REGISTER_KEY, true);
if (register) {
// 注册服务,把简化后的服务提供者url注册到registryUrl中去
register(registryUrl, registeredProviderUrl);
providerInvokerWrapper.setReg(true);
}
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);
}
真正导出这块有三个大逻辑:
1.获取动态配置最新配置信息,重新生成我们服务提供者URL信息,这个单独抽出来看配置监听章节
2.开启本地服务,进行服务暴露,比如Dubbo协议会开启Netty,Http会开启Tomcat,这块在doLocalExport方法中,在服务调用的时候在详细讲这块的逻辑。
3.获取注册中心服务,进行服务注册。
最后我们看下zookeeper上的节点信息:
dubbo://172.16.35.146:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-provider1-application&bean.name=ServiceBean:org.apache.dubbo.demo.DemoService:1.0.1&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=tulings&interface=org.apache.dubbo.demo.DemoService&logger=log4j&methods=sayHello&pid=31728&release=2.7.0&revision=1.0.1&side=provider×tamp=1702371526687&version=1.0.1
4.配置监听
上边在服务导出的时候,有一个过程是要获取动态配置的最新信息的,所以在服务进行导出的时候,会创建一些监听配置监听我们在dubbo-admin控制台配置的动态配置,拉取到动态配置最新配置重写URL后再进行网络服务的创建以及服务的注册,监听器的创建如下图代码
OverrideListener这个监听器是为了兼容老版本的dubbo-admin控制台,监听老版本的路径,比如/dubbo/org.apache.dubbo.demo.DemoService/configurators/override
我们再看下overrideUrlWithConfig这个方法
private URL overrideUrlWithConfig(URL providerUrl, OverrideListener listener) {
// 应用配置,providerConfigurationListener是在属性那里直接初始化好的,providerConfigurationListener会监听配置中心的应用配置信息变动
providerUrl = providerConfigurationListener.overrideUrl(providerUrl);
// 服务配置,new ServiceConfigurationListener的时候回初始化,ServiceConfigurationListener会监听配置中心的服务信息配置信息变动
ServiceConfigurationListener serviceConfigurationListener = new ServiceConfigurationListener(providerUrl, listener);
serviceConfigurationListeners.put(providerUrl.getServiceKey(), serviceConfigurationListener);
return serviceConfigurationListener.overrideUrl(providerUrl);
}
这个方法有两个监听器,ProviderConfigurationListener和ServiceConfigurationListener
providerConfigurationListener在类加载的时候就初始化好了。
在构造方法中调用initWith方法:
protected final void initWith(String key) {
//获取动态配置服务
DynamicConfiguration dynamicConfiguration = DynamicConfiguration.getDynamicConfiguration();
// 添加Listener,进行了订阅
dynamicConfiguration.addListener(key, this);
// 从配置中心ConfigCenter获取属于当前应用的动态配置数据,从zk中拿到原始数据(主动从配置中心获取数据)
String rawConfig = dynamicConfiguration.getRule(key, DynamicConfiguration.DEFAULT_GROUP);
// 如果存在应用配置信息则根据配置信息生成Configurator
if (!StringUtils.isEmpty(rawConfig)) {
genConfiguratorsFromRawRule(rawConfig);
}
}
1.获取动态配置服务
2.绑定监听器
3.读取动态配置
4.根据动态配置生成configurators,监听器的内部属性
同理,在创建ServiceConfigurationListener的时候也会调用initWith方法,和ProviderConfigurationListener不同的时候,每个服务都会创建一个ServiceConfigurationListener来监听服务级别的动态配置。至此我们拿到的就是一个最新配置的服务的URL信息,然后进行网络服务的创建,以及服务的注册。但是这里还有一个问题,我们一共创建了三个监听器,ProviderConfigurationListener和ServiceConfigurationListener都已经进行监听,但是老版本的OverrideListener还没有开始监听,其代码是在服务注册后的代码的registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
当我们dubbo-admin修改动态配置内容的时候,会回调我们监听器的方法,无论哪个监听器都会调用doOverrideIfNecessary方法进行配置更新。
在这里逻辑为:
1.从exporter中获取目前已经导出了的服务URL-currentUrl
2.根据老版本管理台的Configurator重写服务URL
3.根据providerConfigurationListener中的Configurator重写服务URL
4.根据serviceConfigurationListeners中对应的服务的Configurator重写服务URL
5.如果重写之后newUrl和currentUrl相等,那么不需要做什么了
6.如果重写之后newUrl和currentUrl不相等,则需要进行服务重新导出,根据新的服务提供者URL调用reExport方法
我们在看下reExport方法
public <T> void reExport(final Invoker<T> originInvoker, URL newInvokerUrl) {
// 根据newInvokerUrl进行导出
ExporterChangeableWrapper exporter = doChangeLocalExport(originInvoker, newInvokerUrl);
// 获取准确的ProviderUrl
// update registry
URL registryUrl = getRegistryUrl(originInvoker);
// 对于一个服务提供者url,在注册到注册中心时,会先进行简化
final URL registeredProviderUrl = getRegisteredProviderUrl(newInvokerUrl, registryUrl);
//decide if we need to re-publish
// 根据getServiceKey获取ProviderInvokerWrapper
ProviderInvokerWrapper<T> providerInvokerWrapper = ProviderConsumerRegTable.getProviderWrapper(registeredProviderUrl, originInvoker);
// 生成一个新的ProviderInvokerWrapper
ProviderInvokerWrapper<T> newProviderInvokerWrapper = ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);
/**
* 如果新的服务提供者url简化后的url和这个服务之前的服务提供者url简化后的url不相等,则需要把新的简化后的服务提供者url注册到注册中心去
*/
if (providerInvokerWrapper.isReg() && !registeredProviderUrl.equals(providerInvokerWrapper.getProviderUrl())) {
unregister(registryUrl, providerInvokerWrapper.getProviderUrl());
register(registryUrl, registeredProviderUrl);
newProviderInvokerWrapper.setReg(true);
}
exporter.setRegisterUrl(registeredProviderUrl);
}
在reExport其实做了三件事
1.在doChangeLocalExport方法中,根据新的服务提供者URL进行服务重新导出,调用的是DubboProtocol的export方法,重置我们的Server服务,比如Netty的连接超时时间等,并不会关闭我们的Netty服务。
2.对newUrl进行简化,调用RegistryProtocol的unregister()方法,把当前服务之前的服务URL从注册中心删除
3.把新的Url注册到注册中心中去。
总结
至此我们Dubbo服务导出的基本原理已经走完了,不过还是留了一个问题,比如我们配置的Dubbo协议,那么服务导出的时候,服务提供者是如何开启一个Netty服务和消费者进行数据传输的,这一块是我会在服务调用的地方进行详解,咱们下一篇见。