本文首發於個人微信公衆號《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 的設計,還沒有分析,下次這個會單獨摘成一篇文章呈現出來。如果你對源碼也非常感興趣,歡迎進羣我們一起研讀。
相關閱讀: