前言
dubbo的服务暴露和引用是dubbo使用过程中两个非常重要的环节,本篇先来探讨dubbo中服务的暴露流程。服务暴露对外提供可用服务,入口在ServiceConfig的export方法(暂不考虑spring生态),本文会以ServiceConfig.export方法为入口,分析服务暴露的整个过程。至于dubbo与spring生态的对接部分,后面会专门开篇来分析。我们来看官网对服务暴露的说明(以下内容摘自官网):
1、在没有注册中心,直接暴露提供者的情况下 1,ServiceConfig 解析出的 URL 的格式为:
dubbo://service-host/com.foo.FooService?version=1.0.0。基于扩展点自适应机制,
通过 URL 的 dubbo:// 协议头识别,直接调用 DubboProtocol的 export() 方法,打开服务端口。
2、在有注册中心,需要注册提供者地址的情况下 ,ServiceConfig 解析出的 URL 的格式为:
registry://registry-host/org.apache.dubbo.registry.RegistryService?export=
URL.encode("dubbo://service-host/com.foo.FooService?version=1.0.0"),基于扩展点
自适应机制,通过 URL 的 registry:// 协议头识别,就会调用 RegistryProtocol 的
export() 方法,将 export 参数中的提供者 URL,先注册到注册中心。再重新传给 Protocol
扩展点进行暴露: dubbo://service-host/com.foo.FooService?version=1.0.0,然后
基于扩展点自适应机制,通过提供者 URL 的 dubbo:// 协议头识别,就会调用 DubboProtocol
的 export() 方法,打开服务端口。
也就是说,服务的暴露最终是调用Protocol.export方法,打开服务端口,供消费者使用。为了方便描述,本篇仅分析ServiceConfig至Procotol.export()的暴露流程,至于Procotol.export()的内部实现,会在后续文章中进行介绍。借用dubbo官方服务暴露的时序图,本篇将着重介绍分析的即红框中部分。
服务暴露
不考虑对spring生态的依赖,dubbo服务的暴露逻辑(最简配置)如下(dubbo的必需配置:application):
ServiceConfig<DemoServiceImpl> serviceConfig = new ServiceConfig<DemoServiceImpl>();
service.setInterface(DemoService.class);
service.setRef(new DemoServiceImpl());
service.setApplication(app);
serviceConfig.export();
服务暴露的入口在serviceConfig.export方法,这里我把整个服务暴露过程分为两大步:加载、刷新服务配置,初始化服务暴露所需的各项配置;执行服务暴露,结果是缓存exporter和暴露的url,并返回exporter。后续的分析也按照这两步逐一进行。先看下服务暴露入口
public synchronized void export() {
// 检查并刷新相关服务配置
checkAndUpdateSubConfigs();
// 已暴露过,则直接返回
if (provider != null) {
if (export == null) {
export = provider.getExport();
}
if (delay == null) {
delay = provider.getDelay();
}
}
if (export != null && !export) {
return;
}
//执行服务暴露或延迟执行服务暴露
if (delay != null && delay > 0) {
delayExportExecutor.schedule(this::doExport, delay, TimeUnit.MILLISECONDS);
} else {
doExport();
}
}
一、加载、刷新服务配置
先来看配置的加载和刷新,有关dubbo配置相关请参考dubbo之Configuration,本文将只关注服务暴露过程中的配置加载与刷新。为方便管理服务相关配置,dubbo封装了一系列配置model及行为,我们先来看下相关model类之间的层次关系,为方便描述,服务引用、注册中心等所有配置model都会放在在一起解析。
如上图所示,AbstractConfig、AbstractInterfaceConfig负责公共逻辑的处理,ServiceConfig、ReferenceConfig分用于服务的暴露和引用,逻辑比较复杂,除此之外(ServiceBean、ReferenceBean用于对接Spring生态,暂不作解析),其他config model都比较简单,大多是一些参数的getter、setter实现。
先来看AbstractConfig、AbstractInterfaceConfig中的公共逻辑,AbstractInterfaceConfig中的公共逻辑会在具体流程节点解析,先来看AbstratConfig。在AbstractConfig中,需要重点关注核心方法refresh(所有子类的刷新都依赖该方法)。refresh()的核心逻辑,利用复合配置CompositeConfiguration(初始化阶段,内置4个静态配置用于缓存配置加载阶段属性值),然后再依照静态配置优先级,读取静态配置中各属性值,通过反射刷新当前config字段。
public void refresh() {
try {
// 初始化复合配置,初始化内部各种静态配置;Environment可以理解为一个用于缓存Config的单例工具;
// 只需关注getConfiguration()[1]即可,用于构建复合配置
CompositeConfiguration compositeConfiguration =
Environment.getInstance().getConfiguration(getPrefix(), getId());
InmemoryConfiguration config = new InmemoryConfiguration(getPrefix(), getId());
// 获取元数据并添加缓存config,getMetaData()[2]
config.addProperties(getMetaData());
// 默认配置中心配置优先
if (Environment.getInstance().isConfigCenterFirst()) {
// The sequence would be: SystemConfiguration -> ExternalConfiguration ->
// AppExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
// 添加的顺序决定了各配置的优先级顺序,原因可以查看对Configuration配置一文
compositeConfiguration.addConfiguration(3, config);
} else {
// The sequence would be: SystemConfiguration -> AbstractConfig -> ExternalConfiguration ->
// AppExternalConfiguration -> PropertiesConfiguration
compositeConfiguration.addConfiguration(1, config);
}
// loop methods, get override value and set the new value back to method
Method[] methods = getClass().getMethods();
for (Method method : methods) {
// 这里设计就牛逼了,遍历setter,内部调用extractPropertyName,将setter转getter,再根据getter上的注解,
// 获取属性名称,最后再调用setter刷新属性值。
if (ClassHelper.isSetter(method)) {
try {
// 这里实质上调用的是CompositeConfiguration的getInternalProperty方法,
// 根据优先级从静态配置列表中取值。extractPropertyName,借助@Parameter注解,
// 解析属性名,优先取注解中的属性名称,找不到则直接取属性名称;比较简单
String value = 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) && ClassHelper.isTypeMatch(method.getParameterTypes()
[0], value)) {
// 调用setter,设置属性值
method.invoke(this, ClassHelper.convertPrimitive(method.getParameterTypes()[0],
value));
}
} catch (NoSuchMethodException e) {
logger.info("Failed to override the property " + method.getName() + " in "
+this.getClass().getSimpleName() + ", please make sure every property has getter/setter method provided.");
}
}
}
System.out.println("hello world!");
} catch (Exception e) {
logger.error("Failed to override ", e);
}
}
// 按照顺序先来看Environment的getConfiguration[1]
// 获取配置组合,初始化 CompositeConfiguration;有意思,这里静态配置的唯一性由内部map保证。
public CompositeConfiguration getConfiguration(String prefix, String id) {
CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
// Config center has the highest priority
// 均为静态配置,先从map缓存取,没有则创建
compositeConfiguration.addConfiguration(this.getSystemConfig(prefix, id));
compositeConfiguration.addConfiguration(this.getAppExternalConfig(prefix, id));
compositeConfiguration.addConfiguration(this.getExternalConfig(prefix, id));
compositeConfiguration.addConfiguration(this.getPropertiesConfig(prefix, id));
return compositeConfiguration;
}
//getMetaData()[2],元数据
public Map<String, String> getMetaData() {
Map<String, String> metaData = new HashMap<>();
Method[] methods = this.getClass().getMethods();
for (Method method : methods) {
try {
String name = method.getName();
// 除去get、getClass、非共有方法、有参方法、返回值非基础类型的方法以外,其他方法认为是元数据方法
if (isMetaMethod(method)) {
//从getter获取属性名成
String prop = calculateAttributeFromGetter(name);
String key;
Parameter parameter = method.getAnnotation(Parameter.class);
//先从parameter注解去,取不到则默认取从getter解析出的属性名
if (parameter != null && parameter.key().length() > 0 && parameter.useKeyAsProperty()) {
key = parameter.key();
} else {
key = prop;
}
// 返回值类型是Object,直接置null
if (method.getReturnType() == Object.class) {
metaData.put(key, null);
continue;
}
// 调用meta方法,并放入metaData
Object value = method.invoke(this);
String str = String.valueOf(value).trim();
if (value != null && str.length() > 0) {
metaData.put(key, str);
} else {
metaData.put(key, null);
}
} else if ("getParameters".equals(name)
&& Modifier.isPublic(method.getModifiers())
&& method.getParameterTypes().length == 0
&& method.getReturnType() == Map.class) {
//对getParameters单独处理,直接调用并全部放入metaData
Map<String, String> map = (Map<String, String>) method.invoke(this, new Object[0]);
if (map != null && map.size() > 0) {
for (Map.Entry<String, String> entry : map.entrySet()) {
metaData.put(entry.getKey().replace('-', '.'), entry.getValue());
}
}
}
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
return metaData;
}
整个refresh方法的核心逻辑到这里就分析完了,下面我们按照服务暴露过程,同时对配置逻辑进行分析。需要注意,在ServiceBean初始化过程中,会完成一部分config的加载(通过解析ServiceBean配置进行加载)。整个配置加载过程如下:
下面就按照加载流程依次进行解析。
1、加载混合配置
混合配置加载,其实根据ServiceBean的初始化结果,对ServiceConfig中未被初始化的属性进行补充。大体总结就是,根据已初始化的ProviderConfig、ApplicationConfig、ModuleConfig完善其他配置信息,直接来看代码
private void completeCompoundConfigs() {
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());
}
}
if (module != null) {
if (registries == null) {
setRegistries(module.getRegistries());
}
if (monitor == null) {
setMonitor(module.getMonitor());
}
}
// regietryConfig和MonitorConfig最终以applicationConfig中的为准。
if (application != null) {
if (registries == null) {
setRegistries(application.getRegistries());
}
if (monitor == null) {
setMonitor(application.getMonitor());
}
}
}
2、启动配置中心
配置中心启动比较好理解,核心逻辑是初始化配置中心的配置、刷新所有配置。ConfigManager,对外表现为单例,用于管理所有核心Config,并用作部分Config的缓存,核心是一些getter、setter以及clear方法,比较简单,直接来看配置中心的启动逻辑。
void startConfigCenter() {
// 借助ConfigManager初始化配置中心配置(ConfigCenterConfig)
if (configCenter == null) {
ConfigManager.getInstance().getConfigCenter().ifPresent(cc -> this.configCenter = cc);
}
if (this.configCenter != null) {
// TODO there may have duplicate refresh
// 配置中心刷新,这里调用父类方法,不解释
this.configCenter.refresh();
// 准备环境,逻辑比较简单,略过
prepareEnvironment();
}
//刷新所有配置
ConfigManager.getInstance().refreshAll();
}
// 来看ConfigManager.refreshAll方法
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);
}
3、初始化ProviderConfig
检查Provider配置,没有则会先从ConfigManager缓存中取key为"default"的值,取不到则直接创建并刷新,然后放入ConfigManager的缓存,默认key是default(除非provider指定了id),代码比较简单,这里就省略了。
4、初始化ApplicationConfig
与Provider的初始化过程类似,也是先检查再创建(步骤与ProviderConfig类似,不再冗述)。除此之外,内部还对ApplicationModel应用名做了初始化和历史版本属性的兼容处理,比较简单。
5、初始化RegistryConfig
RegistryConfig的初始过程稍微复杂,分步骤做解析:
- 首先,为了兼容历史版本,解析"-Ddubbo.registry.address"参数值,并放入ConfigManager缓存;
- 若spring加载入口bean(ServiceBean、ReferenceBean等)过程中未完成registryIds、registries初始化,优先从Environment取registryIds用于初始化RegistryConfig并放入ConfigManager缓存,没有取到registryIds则尝试从ConfigManager缓存取,同样的,取不到则直接创建并写入ConfigManager缓存。
- 检查registries内配置有效性
- 兼容处理,若没有指定配置中心,而当前的注册协议是Zookeeper,那么会默认把当前的RegistryConfig的procotol、address信息刷入配置中心配置,并启动配置中心
6、初始化ProtocolConfig
逻辑相对简单,若当前protocols值为空,则优先从provider中取,并放入ConfigManager缓存。后续处理逻辑与RegistryConfig类似,优先从Environment取protocolIds用于初始化ProtocolConfig并放入ConfigManager缓存,没有取到protocolIds则继续尝试从ConfigManager缓存取,取不到则直接创建并写回ConfigManager缓存。
7、刷新ServiceConfig
ServiceConfig的刷新操作由父类AbstractConfig的refresh方法完成,前面已经介绍过了,这里直接略过。
8、初始化MetaDataReportConfig
同样比较简单,检查MetaDataReportConfig,并做初始化,然后放入ConfigManager缓存,最后refresh。
9、其他检查
初始化Generic服务标志、校验接口和方法、校验接口实现类、stub校验、mock校验,相对比较简单,就不过多解释了。
此时,所有配置都加载并初始化完毕(配置信息已经在相应config,同时在ConfigManager做了缓存),下一步需要根据已有配置信息,执行服务暴露。
二、执行服务暴露
服务暴露执行环节比较复杂,首先根据provider判断是否需要延迟暴露(通过ScheduleExcutorService实现),然后执行延迟暴露或者直接暴露,核心逻辑在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;
}
//构建providerModel,serviceName、serviceInstance、serviceInterface,以及服务提供的方法模型
ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), ref, interfaceClass);
// 向应用注册providerModel
ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
// 重头戏来了
doExportUrls();
}
private void doExportUrls() {
// 加载registryConfig配置,并解析为URL
List<URL> registryURLs = loadRegistries(true);
for (ProtocolConfig protocolConfig : protocols) {
// 真正执行服务的暴露
doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}
}
上面的代码可以看到,服务暴露的核心逻辑在doExportUrlsFor1Protocol(),执行步骤如下:
获取host、port,将各config参数合并至map;
将1中的host、port、map组装成URL;
参照Configrurator,修正URL参数;
动态代理生成原始Invoker,这一步由ProxyFactory完成;
二次代理Invoker,将Invoker包装成DelegateProviderMetaDataInvoker;
-
执行Invoker的暴露,具体操作由Procotol完成;
dubbo-service-export_exe.jpg
过程如上图所示,下面我们按照步骤依次来进行分析:
首先来看第一步,参数合并会把前期配置准备阶段初始化后的配置信息,以及初始化后的config(ApplicationConfig、ProviderConfig、ModuleConfig、ProtocolConfig、ServiceConfig)中所有参数信息,合并至map;另外,服务host和端口的初始化也会在这一步完成,过程相对简单,重点关注取值优先级;其中,host的取值优先级,系统环境变量 -> java系统属性(-D参数) -> 配置文件中的host属性 -> /etc/hosts中的配置 -> 默认网络地址 -> 第一个可用的网络地址 -> 本地ip保底; port的取值优先级,系统环境变量 -> java系统属性(-D参数) -> ProtocolConfig中port值 -> Protocol中defaultPort(比如dubboProtocol中的20880)。下面直接来看代码
Map<String, String> map = new HashMap<String, String>();
map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE);
// 运行时参数合并至map
appendRuntimeParameters(map);
// applicationConfig配置参数合并至map
appendParameters(map, application);
// moduleConfig配置参数合并至map
appendParameters(map, module);
// providerConfig配置参数合并至map
appendParameters(map, provider, Constants.DEFAULT_KEY);
// protocolConfig配置参数合并至map
appendParameters(map, protocolConfig);
// service配置参数合并至map
appendParameters(map, this);
// ArgugmentConfig配置信息合并到map,代码比较长且逻辑比较简单,省略。其中,method中argumentConfig属性最终会以<methodName.index.xxx,xxx.value>
//中间夹杂对generic、version、methods、token的处理
if (ProtocolUtils.isGeneric(generic)) {
map.put(Constants.GENERIC_KEY, generic);
map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
} else {
// 非通用方法,补充版本;版本取值逻辑比较简单,先从jar包的MANIFEST.MF文件读取,取不到则取jar包版本,由defaultVersion兜底。
String revision = Version.getVersion(interfaceClass, version);
if (revision != null && revision.length() > 0) {
map.put("revision", revision);
}
//借助javaassit为接口生成代理,并获取方法列表;最终用于组装methods参数;这个方法比较有意思,下面会单独解析
String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
if (methods.length == 0) {
logger.warn("No method found in service interface " + interfaceClass.getName());
map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
} else {
map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
}
}
// token,参数处理
if (!ConfigUtils.isEmpty(token)) {
if (ConfigUtils.isDefault(token)) {
//默认token是一个uuid
map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
} else {
map.put(Constants.TOKEN_KEY, token);
}
}
上面的代码中,Wrapper.getWrapper方法非常有意思,单独拉出来做解析。getWrapper主要用来生成服务接口的代理,逻辑相对简单:优先从Wrapper缓存取,缓存无值,则借助javassist动态生成Wrapper并放入缓存。重点关注生成Wrapper的方法makeWrapper:首先拼接Wrapper源码,然后通过ClassGenerator(借助javassist工具)生成Wrapper的Class实例,最后利用反射生成Wrapper实例;被代理接口的所有getter、setter汇总为两个方法,即对应Wrapper实例中的setProperty、getProperty方法,其他方法则统一于invokeMethod方法;篇幅关系,这里省略代码,有兴趣的同学可以自行查阅。参数合并部分到此结束。
URL的组装比较简单,直接通过上一步合并后的参数,创建url;注意,这一步创建的url,最终会作为registryUrl的参数。url组装完成后,接着通过SPI(ConfiguratorFactory接口)扩展,拿到对应Configurator,并对url中参数进行修正。
此时,url已经完全构建完毕,接下来,根据scope参数值来决定是否执行服务暴露、执行本地暴露还是远程暴露;若scope参数值不为none,则执行暴露;若scope参数值不为remote,执行本地暴露;若参数值不为local,则执行远程暴露。本地暴露与远程暴露逻辑相似,不同在于,本地暴露采用InjvmProtocol协议,端口为0,host为127.0.0.1,且少了加载monitor和二次代理的过程,其他逻辑与远程暴露一致。远程暴露过程中,dubbo会加载monitorConfig相关信息并组装成monitorUrl,作为参数存放于url,而后,url会作为registryUrl的参数,最终registryUrl作为服务暴露的主体。动态代理生成Invoker部分逻辑比较简单,直接来看代码(以javassistProxyFactory为例):
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// Wrapper不支持对动态代理类的代理
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName, Class<?>[] parameterTypes,Object[] arguments) throws Throwable {
// 实际执行wrapper的invokeMethod方法,即proxy方法名为methodName的方法。
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
接着,执行对代理Invoker的包装,最后通过Proctocol.export方法(在Protocol篇进行解析),完成服务的暴露,缓存exporter以及暴露过的url。
2.7.0版本以后,服务暴露完成后,dubbo新增了对provider元数据的处理,核心逻辑在MetadataReportService.publishProvider方法,简单了解一下。dubbo通过providerUrl中的信息,将元数据封装成FullServiceDefinition,然后通过MetadataReport接口的storeProviderMetadata方法(位于AbstractMetadataReport),下面来看方法逻辑:
public void storeProviderMetadata(MetadataIdentifier providerMetadataIdentifier, FullServiceDefinition serviceDefinition) {
// url中sync.report参数值,是否同步上报
if (syncReport) {
storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition);
} else {
// 通过线程池,异步上报
reportCacheExecutor.execute(new Runnable() {
@Override
public void run() {
storeProviderMetadataTask(providerMetadataIdentifier, serviceDefinition);
}
});
}
}
private void storeProviderMetadataTask(MetadataIdentifier providerMetadataIdentifier, FullServiceDefinition serviceDefinition) {
try {
if (logger.isInfoEnabled()) {
logger.info("store provider metadata. Identifier : " + providerMetadataIdentifier + "; definition: " + serviceDefinition);
}
allMetadataReports.put(providerMetadataIdentifier, serviceDefinition);
failedReports.remove(providerMetadataIdentifier);
// Gson 序列化
Gson gson = new Gson();
String data = gson.toJson(serviceDefinition);
// 具体逻辑由具体子类实现
doStoreProviderMetadata(providerMetadataIdentifier, data);
saveProperties(providerMetadataIdentifier, data, true, !syncReport);
} catch (Exception e) {
// 上报失败,进行重试,重试失败则抛异常
failedReports.put(providerMetadataIdentifier, serviceDefinition);
metadataReportRetry.startRetryTask();
logger.error("Failed to put provider metadata " + providerMetadataIdentifier + " in " + serviceDefinition + ", cause: " + e.getMessage(), e);
}
}
其中,doStoreProviderMetadata由具体子类实现(RedisMetadataReport、ZookeeperMetadataReport),元数据信息保存在redis或者zookeeper,逻辑比较简单。
综上,本篇主要介绍了dubbo服务暴露流程在config层的处理,核心包括config的初始化和暴露执行两部分。config初始化过程,个人觉得作者参考了spring的refresh过程,代码比较容易理解。暴露执行分为本地暴露和远程暴露,本地暴露流程非常简单;远程暴露主要包括参数组合、组装URL、修正URL参数、生成代理Invoker、二次代理Invoker、通过protocol执行暴露6个核心步骤。通过protocol的export完成服务的最终暴露。
注:dubbo源码版本2.7.1,欢迎指正。