本文首发于个人微信公众号《andyqian》,关注即可获取一线互联网内推机会!
前言
我们在编写项目过程中,配置文件几乎是标配,从最简单且最常用的 application.properties 文件,到现在集群环境下的配置中心。也算是互联网技术发展的一个缩影(单体应用到分布式应用)。在这方面,目前有非常多的开源实现,如:zookpeer,nacos,apollo 等等。像是命题作文下交付的不同答卷,各有优劣,当然,这有点偏题了。回到正题,今天讨论的话题是: Seata 中 config 模块的实现,主要会从以下几个方面进行分析:
- 职责。
- 设计
- 源码
- 设计模式
职责
config 模块承担的主要职责是:seata 框架配置文件的生命周期管理。这句话怎么理解呢?我们都知道,seata 框架的定位就是 分布式事务框架,从广义上来说也是中间件,而中间件就涉及到诸如:网络传输 (如:netty),传输就涉及到协议(如自定义的seata协议,http协议 …), 编解码 等等。恰恰对于中间件而言,在这方面应该予以用户更多的选择,而用户的个性化选择则在配置文件或配置中心体现。
设计
上面谈到了中间件应该予以用户更多的选择。在这方面:Microkernel + Plugin 模式是一个优秀的设计,其在 Dubbo 中有着非常好的实践。同样的:在 Seata 中,也有相同的设计,config 模块就是一个非常好的例子:
module 图:
其中:
- seata-config-core 就是配置核心 module,值得注意的是:seata-config-all 是 config 模块一个父pom,而并不是一个Plugin。
- 其他的 module 则以 Plugin 形式存在,目前有:apollo, consul,zk,nacos 等等。
类图:
- Configuration 接口是 config 模块一个非常重要的接口,所有配置相关的操作都围绕着该接口设计。不同的注册中心实现该接口,或者继承该接口的抽象类 AbstractConfiguration 类即可。其职责是:定义配置信息的原子操作,如:get,put, remove 等。类图如下所示:
需要注意的是: FileConfiguration 是 Seata 默认的配置实现。
除了Configuration 接口外,还有一个重要的接口就是:ConfigurationProvider,则表示 Configuration 能力的提供。类图如下所示:
入口
在 config 模块中,对外提供的入口是:ConfigurationFactory 类,通过其静态方法:getInstance() 对外提供 Configuration 接口的能力。
源码
通过上面的设计分析,现在我们来看看源码。
1. 首先,先看 Configuration 接口的源码:
public interface Configuration<T> {
/**
* Gets duration.
* @param dataId the data id
* @param defaultValue the default value
* @param timeoutMills the timeout mills
* @return he duration
*/
Duration getDuration(String dataId, Duration defaultValue, long timeoutMills);
/**
* Gets config.
* @param dataId the data id
* @param defaultValue the default value
* @param timeoutMills the timeout mills
* @return the config
*/
String getConfig(String dataId, String defaultValue, long timeoutMills);
/**
* Put config boolean.
* @param dataId the data id
* @param content the content
* @param timeoutMills the timeout mills
* @return the boolean
*/
boolean putConfig(String dataId, String content, long timeoutMills);
/**
* Put config if absent boolean.
* @param dataId the data id
* @param content the content
* @param timeoutMills the timeout mills
* @return the boolean
*/
boolean putConfigIfAbsent(String dataId, String content, long timeoutMills);
/**
* Remove config listener.
* @param dataId the data id
* @param listener the listener
*/
void removeConfigListener(String dataId, T listener);
/**
* Gets config listeners.
* @param dataId the data id
* @return the config listeners
*/
List<T> getConfigListeners(String dataId);
/**
* Gets config from sys pro.
* @param dataId the data id
* @return the config from sys pro
*/
default String getConfigFromSysPro(String dataId) {
return System.getProperty(dataId);
}
...
备注:上述源码经过一些简单的筛选,实际还有很多重载函数,其底层调用的还是上述方法,故省略。
2. ConfigurationProvider 类的源码则非常简单,实例化对应的 ConfigurationProvider 即可。以 Zookeeper 为例,源码如下所示:
@LoadLevel(name = "ZK", order = 1)
public class ZookeeperConfigurationProvider implements ConfigurationProvider {
@Override
public Configuration provide() {
try {
return new ZookeeperConfiguration();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
3. AbstractConfiguration 抽象类是 Configuration 接口的默认实现。除了 Configuration 定义的接口默认实现外,还定义了一个抽象方法:
public abstract class AbstractConfiguration<T> implements Configuration<T> {
/**
* The constant DEFAULT_CONFIG_TIMEOUT.
*/
protected static final long DEFAULT_CONFIG_TIMEOUT = 5 * 1000;
... // 因篇幅原因,不在这里显示
/**
* Gets type name.
*
* @return the type name
*/
public abstract String getTypeName();
4. ConfigurationFactory 类是使用 config 模块的入口,同时也是聚合,源码如下所示:
public final class ConfigurationFactory {
private static final String REGISTRY_CONF_PREFIX = "registry";
private static final String REGISTRY_CONF_SUFFIX = ".conf";
private static final String ENV_SYSTEM_KEY = "SEATA_ENV";
private static final String ENV_PROPERTY_KEY = "seataEnv";
/**
* the name of env
*/
private static String envValue;
// 加载配置文件
static {
envValue = System.getProperty(ENV_PROPERTY_KEY);
if (null == envValue) {
envValue = System.getenv(ENV_SYSTEM_KEY);
}
}
//定义默认的 file configuration instant,
private static final Configuration DEFAULT_FILE_INSTANCE = new FileConfiguration(
REGISTRY_CONF_PREFIX + REGISTRY_CONF_SUFFIX);
public static final Configuration CURRENT_FILE_INSTANCE = null == envValue ? DEFAULT_FILE_INSTANCE : new FileConfiguration(REGISTRY_CONF_PREFIX + "-" + envValue
+ REGISTRY_CONF_SUFFIX);
private static final String NAME_KEY = "name";
private static final String FILE_TYPE = "file";
private static volatile Configuration instance = null;
/**
* // 单例获取 Configuration
* @return the instance
*/
public static Configuration getInstance() {
if (instance == null) {
synchronized (Configuration.class) {
if (instance == null) {
instance = buildConfiguration();
}
}
}
return instance;
}
// 构造 Configuration 对象
private static Configuration buildConfiguration() {
ConfigType configType = null;
String configTypeName = null;
try {
configTypeName = CURRENT_FILE_INSTANCE.getConfig(
ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
+ ConfigurationKeys.FILE_ROOT_TYPE);
configType = ConfigType.getType(configTypeName);
} catch (Exception e) {
throw new NotSupportYetException("not support register type: " + configTypeName, e);
}
//如果 configType = file,则构造 FileConfiguration 实例
if (ConfigType.File == configType) {
String pathDataId = ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
+ FILE_TYPE + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
+ NAME_KEY;
String name = CURRENT_FILE_INSTANCE.getConfig(pathDataId);
return new FileConfiguration(name);
} else {
// 否则通过 spi 进行加载service,并调用其 provide方法,例如:zookpeer, naocs , 等等。return EnhancedServiceLoader.load(ConfigurationProvider.class, Objects.requireNonNull(configType).name())
.provide();
}
}
5. FileConfiguration 为 Seata 的默认 Configuration, 其底层依赖 typesafe.config 开源框架实现。源码如下所示:
public class FileConfiguration extends AbstractConfiguration<ConfigChangeListener> {
private static final Logger LOGGER = LoggerFactory.getLogger(FileConfiguration.class);
private final Config fileConfig;
private ExecutorService configOperateExecutor;
private ExecutorService configChangeExecutor;
....
public FileConfiguration(String name) {
if (null == name) {
fileConfig = ConfigFactory.load();
} else {
//加载指定名称的 configName
fileConfig = ConfigFactory.load(name);
}
// 构造配置获取的线程池
configOperateExecutor = new ThreadPoolExecutor(CORE_CONFIG_OPERATE_THREAD, MAX_CONFIG_OPERATE_THREAD,
Integer.MAX_VALUE, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
new NamedThreadFactory("configOperate", MAX_CONFIG_OPERATE_THREAD));
// 构造配置改变线程池,并启动, 每 1秒进行获取一次。configChangeExecutor = new ThreadPoolExecutor(CORE_CONFIG_CHANGE_THREAD, CORE_CONFIG_CHANGE_THREAD,
Integer.MAX_VALUE, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
new NamedThreadFactory("configChange", CORE_CONFIG_CHANGE_THREAD));
configChangeExecutor.submit(new ConfigChangeRunnable());
}
}
由于篇幅原因,config 模块的源码没有详细的列出,不过核心的源码如上所示。对于zookpeer, naocs 等的代码,则是按照 seata 定义的接口进行实现,可自行查看。
设计模式
在 config 模块中,使用到了多个设计模式,其中包括:
- ConfigurationFactory 类 中的 getInstance() 为典型的 单例模式。
- Configuration 接口, AbstractConfiguration 抽象类,以及其子类 为典型的 适配器模式。
config 模块的代码相对而言,还算容易理清楚。不过里面还涉及到一些例如:SPI 的设计,还没有分析,下次这个会单独摘成一篇文章呈现出来。如果你对源码也非常感兴趣,欢迎进群我们一起研读。
相关阅读: