配置中心 Apollo 原始碼解析 —— 客戶端 API 配置(三)之 ConfigFile
������關注微信公眾號:【芋道原始碼】有福利:
1. RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
3. 您對於原始碼的疑問每條留言都將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢。
4. 新的原始碼解析文章實時收到通知。每週更新一篇左右。
> 5. 認真的原始碼交流微信群。
1. 概述
從實現上,ConfigFile 和 Config 超級類似,所以本文會寫的比較簡潔。
- Config 基於 KV
- ConfigFile 基於 String 資料結構。
2. ConfigFile
3. AbstractConfigFile
com.ctrip.framework.apollo.internals.AbstractConfigFile
,實現 ConfigFile、RepositoryChangeListener 介面,ConfigFile 抽象類,實現了 1)非同步通知監聽器、2)計算屬性變化等等特性,是 AbstractConfig + DefaultConfig 的功能子集。
3.1 構造方法
private static final Logger logger = LoggerFactory.getLogger(AbstractConfigFile.class);
/**
* ExecutorService 物件,用於配置變化時,非同步通知 ConfigChangeListener 監聽器們
*
* 靜態屬性,所有 Config 共享該執行緒池。
*/
private static ExecutorService m_executorService;
/**
* Namespace 的名字
*/
protected String m_namespace;
/**
* ConfigChangeListener 集合
*/
private List m_listeners = Lists.newCopyOnWriteArrayList();
protected ConfigRepository m_configRepository;
/**
* 配置 Properties 的快取引用
*/
protected AtomicReference m_configProperties;
static {
m_executorService = Executors.newCachedThreadPool(ApolloThreadFactory.create("ConfigFile", true));
}
public AbstractConfigFile(String namespace, ConfigRepository configRepository) {
m_configRepository = configRepository;
m_namespace = namespace;
m_configProperties = new AtomicReference<>();
// 初始化
initialize();
}
private void initialize() {
try {
// 初始化 m_configProperties
m_configProperties.set(m_configRepository.getConfig());
} catch (Throwable ex) {
Tracer.logError(ex);
logger.warn("Init Apollo Config File failed - namespace: {}, reason: {}.", m_namespace, ExceptionUtil.getDetailMessage(ex));
} finally {
//register the change listener no matter config repository is working or not
//so that whenever config repository is recovered, config could get changed
// 註冊到 ConfigRepository 中,從而實現每次配置發生變更時,更新配置快取 `m_configProperties` 。
m_configRepository.addChangeListener(this);
}
}
3.2 獲得內容
交給子類自己實現。
3.3 獲得 Namespace 名字
@Override
public String getNamespace() {
return m_namespace;
}
3.4 新增配置變更監聽器
@Override
public void addChangeListener(ConfigFileChangeListener listener) {
if (!m_listeners.contains(listener)) {
m_listeners.add(listener);
}
}
3.5 觸發配置變更監聽器們
private void fireConfigChange(final ConfigFileChangeEvent changeEvent) {
// 快取 ConfigChangeListener 陣列
for (final ConfigFileChangeListener listener : m_listeners) {
m_executorService.submit(new Runnable() {
@Override
public void run() {
String listenerName = listener.getClass().getName();
Transaction transaction = Tracer.newTransaction("Apollo.ConfigFileChangeListener", listenerName);
try {
// 通知監聽器
listener.onChange(changeEvent);
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
Tracer.logError(ex);
logger.error("Failed to invoke config file change listener {}", listenerName, ex);
} finally {
transaction.complete();
}
}
});
}
}
3.6 onRepositoryChange
#onRepositoryChange(namespace, newProperties)
方法,當 ConfigRepository 讀取到配置發生變更時,計算配置變更集合,並通知監聽器們。程式碼如下:
@Override
public synchronized void onRepositoryChange(String namespace, Properties newProperties) {
// 忽略,若未變更
if (newProperties.equals(m_configProperties.get())) {
return;
}
// 讀取新的 Properties 物件
Properties newConfigProperties = new Properties();
newConfigProperties.putAll(newProperties);
// 獲得【舊】值
String oldValue = getContent();
// 更新為【新】值
update(newProperties);
// 獲得新值
String newValue = getContent();
// 計算變化型別
PropertyChangeType changeType = PropertyChangeType.MODIFIED;
if (oldValue == null) {
changeType = PropertyChangeType.ADDED;
} else if (newValue == null) {
changeType = PropertyChangeType.DELETED;
}
// 通知監聽器們
this.fireConfigChange(new ConfigFileChangeEvent(m_namespace, oldValue, newValue, changeType));
Tracer.logEvent("Apollo.Client.ConfigChanges", m_namespace);
}
呼叫
#update(newProperties)
抽象方法,更新為【新】值。該方法需要子類自己去實現。抽象方法如下:protected abstract void update(Properties newProperties);
4. PropertiesConfigFile
com.ctrip.framework.apollo.internals.PropertiesConfigFile
,實現 AbstractConfigFile 抽象類,型別為 .properties
的 ConfigFile 實現類。
4.1 構造方法
private static final Logger logger = LoggerFactory.getLogger(PropertiesConfigFile.class);
/**
* 配置字串快取
*/
protected AtomicReference m_contentCache;
public PropertiesConfigFile(String namespace, ConfigRepository configRepository) {
super(namespace, configRepository);
m_contentCache = new AtomicReference<>();
}
- 因為 Properties 是 KV 資料結構,需要將多條 KV 拼接成一個字串,進行快取到
m_contentCache
中。
4.2 更新內容
@Override
protected void update(Properties newProperties) {
// 設定【新】Properties
m_configProperties.set(newProperties);
// 清空快取
m_contentCache.set(null);
}
4.3 獲得內容
@Override
public String getContent() {
// 更新到快取
if (m_contentCache.get() == null) {
m_contentCache.set(doGetContent());
}
// 從快取中,獲得配置字串
return m_contentCache.get();
}
String doGetContent() {
if (!this.hasContent()) {
return null;
}
try {
return PropertiesUtil.toString(m_configProperties.get()); // 拼接 KV 屬性,成字串
} catch (Throwable ex) {
ApolloConfigException exception = new ApolloConfigException(String.format("Parse properties file content failed for namespace: %s, cause: %s", m_namespace, ExceptionUtil.getDetailMessage(ex)));
Tracer.logError(exception);
throw exception;
}
}
@Override
public boolean hasContent() {
return m_configProperties.get() != null && !m_configProperties.get().isEmpty();
}
呼叫
PropertiesUtil#toString(Properties)
方法,將 Properties 拼接成字串。程式碼如下:/** * Transform the properties to string format * * @param properties the properties object * @return the string containing the properties * @throws IOException */ public static String toString(Properties properties) throws IOException { StringWriter writer = new StringWriter(); properties.store(writer, null); StringBuffer stringBuffer = writer.getBuffer(); // 去除頭部自動新增的註釋 filterPropertiesComment(stringBuffer); return stringBuffer.toString(); } /** * filter out the first comment line * * @param stringBuffer the string buffer * @return true if filtered successfully, false otherwise */ static boolean filterPropertiesComment(StringBuffer stringBuffer) { //check whether has comment in the first line if (stringBuffer.charAt(0) != '#') { return false; } int commentLineIndex = stringBuffer.indexOf("\n"); if (commentLineIndex == -1) { return false; } stringBuffer.delete(0, commentLineIndex + 1); return true; }
因為
Properties#store(writer, null)
方法,會自動在首行,新增註釋時間。程式碼如下:private void store0(BufferedWriter bw, String comments, boolean escUnicode) throws IOException { if (comments != null) { writeComments(bw, comments); } bw.write("#" + new Date().toString()); // 自動在**首行**,新增**註釋時間**。 bw.newLine(); synchronized (this) { for (Enumeration e = keys(); e.hasMoreElements();) { String key = (String)e.nextElement(); String val = (String)get(key); key = saveConvert(key, true, escUnicode); /* No need to escape embedded and trailing spaces for value, hence * pass false to flag. */ val = saveConvert(val, false, escUnicode); bw.write(key + "=" + val); bw.newLine(); } } bw.flush(); }
從實現程式碼,我們可以看出,拼接的字串,每一行一個 KV 屬性。例子如下:
key2=value2 key1=value1
- x
4.4 獲得 Namespace 名字
@Override
public ConfigFileFormat getConfigFileFormat() {
return ConfigFileFormat.Properties;
}
5. PlainTextConfigFile
com.ctrip.framework.apollo.internals.PlainTextConfigFile
,實現 AbstractConfigFile 抽象類,純文字 ConfigFile 抽象類,例如 xml
yaml
等等。
更新內容
@Override
protected void update(Properties newProperties) {
m_configProperties.set(newProperties);
}
獲得內容
@Override
public String getContent() {
if (!this.hasContent()) {
return null;
}
return m_configProperties.get().getProperty(ConfigConsts.CONFIG_FILE_CONTENT_KEY);
}
@Override
public boolean hasContent() {
if (m_configProperties.get() == null) {
return false;
}
return m_configProperties.get().containsKey(ConfigConsts.CONFIG_FILE_CONTENT_KEY);
}
- 直接從
"content"
配置項,獲得配置文字。這也是為什麼類名以 PlainText 開頭的原因。
�� PlainTextConfigFile 的子類,程式碼基本一致,差別在於 #getConfigFileFormat()
實現方法,返回不同的 ConfigFileFormat 。
5.1 XmlConfigFile
com.ctrip.framework.apollo.internals.XmlConfigFile
,實現 PlainTextConfigFile 抽象類,型別為 .xml
的 ConfigFile 實現類。程式碼如下:
public class XmlConfigFile extends PlainTextConfigFile {
public XmlConfigFile(String namespace, ConfigRepository configRepository) {
super(namespace, configRepository);
}
@Override
public ConfigFileFormat getConfigFileFormat() {
return ConfigFileFormat.XML;
}
}
5.2 JsonConfigFile
com.ctrip.framework.apollo.internals.JsonConfigFile
,實現 PlainTextConfigFile 抽象類,型別為 .json
的 ConfigFile 實現類。程式碼如下:
public class JsonConfigFile extends PlainTextConfigFile {
public JsonConfigFile(String namespace,
ConfigRepository configRepository) {
super(namespace, configRepository);
}
@Override
public ConfigFileFormat getConfigFileFormat() {
return ConfigFileFormat.JSON;
}
}
5.3 YamlConfigFile
com.ctrip.framework.apollo.internals.YamlConfigFile
,實現 PlainTextConfigFile 抽象類,型別為 .yaml
的 ConfigFile 實現類。程式碼如下:
public class YamlConfigFile extends PlainTextConfigFile {
public YamlConfigFile(String namespace, ConfigRepository configRepository) {
super(namespace, configRepository);
}
@Override
public ConfigFileFormat getConfigFileFormat() {
return ConfigFileFormat.YAML;
}
}
5.4 YmlConfigFile
com.ctrip.framework.apollo.internals.YmlConfigFile
,實現 PlainTextConfigFile 抽象類,型別為 .yaml
的 ConfigFile 實現類。程式碼如下:
public class YmlConfigFile extends PlainTextConfigFile {
public YmlConfigFile(String namespace, ConfigRepository configRepository) {
super(namespace, configRepository);
}
@Override
public ConfigFileFormat getConfigFileFormat() {
return ConfigFileFormat.YML;
}
}
666. 彩蛋
恩。
嗯嗯。
嗯嗯嗯。
水更,哈哈哈。
������關注微信公眾號:【芋道原始碼】有福利:
1. RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
3. 您對於原始碼的疑問每條留言都將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢。
4. 新的原始碼解析文章實時收到通知。每週更新一篇左右。
5. 認真的原始碼交流微信群。