隨着越來越多的應用進行了微服務化改造以及相同的應用程序對不同環境(開發、測試、生產環境)、不同部署集羣的需求,將應用中的配置與程序解耦變得越來越重要,在過去,我們的配置文件往往和程序捆綁在一起,當需要修改配置文件時,需要對應用程序進行重新打包的操作,從而導致了應用發佈效率的降低。Apollo是攜程開源的一套配置中心框架,也是目前使用較多的配置中心之一,本系列文章本着學習的態度,逐步由簡單到複雜對Apollo配置中心源碼進行學習,從而幫助需要了解配置中心和提高代碼編寫能力的朋友。廢話不多說,我們接下來開始從Apollo配置中心Client代碼學起走。(Apollo的介紹和使用並非本文的重點,請參考Apollo Github官方文檔)
一、從最簡單Demo入手,分析配置是如何獲取和創建
Apollo源碼(版本1.5.1)下載後,導入IDE,可以看到是典型的Maven 父子工程,在父工程下由許多子模塊,本文從apollo-client開始學起,apollo-client是apollo的客戶端,在實際使用當中是會通過依賴被集成在應用程序當中,那麼我們要開始學習該部分,首先我們從Apollo源碼中Apollo-demo中給出的一個簡單例子入手。一下是SimpleApolloConfigDemo的代碼,其中對代碼的詳細解釋已經通過標註來解釋。可以看到SimpleApolloConfigDemo的構造方法中,首先實例化了一個配置改變監聽器類ConfigChangeListener(1),並且複寫了onChange方法,在onChange方法中打印出了配置改變前後的值。接下來通過ConfigService的getAppConfig(2)方法來獲取配置,最後將配置修改監聽器加入到獲取到配置的監聽隊列當中。在SimpleApolloConfigDemo中getConfig方法中(4),通過傳入key來獲取相應的配置。通過上面對SimpleApolloConfigDemo的分析,我們看到應用程序是通過ConfigService來與Apollo客戶端進行交互的,那麼接下來我們進入ConfigService來分析下一個配置(Config)是如何創建和獲取的。(其餘代碼的詳細分析請看註解)
/**
* @author Jason Song([email protected])
*/
public class SimpleApolloConfigDemo {
private static final Logger logger = LoggerFactory.getLogger(SimpleApolloConfigDemo.class);
//Demo中配置的默認值
private String DEFAULT_VALUE = "undefined";
private Config config;
public SimpleApolloConfigDemo() {
//(1) 註冊ConfigChangeListener,當發生改變後打印出改變前和改變後的值
ConfigChangeListener changeListener = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
logger.info("Changes for namespace {}", changeEvent.getNamespace());
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
logger.info("Change - key: {}, oldValue: {}, newValue: {}, changeType: {}",
change.getPropertyName(), change.getOldValue(), change.getNewValue(),
change.getChangeType());
}
}
};
//(2) 通過ConfigServie獲取Config
config = ConfigService.getAppConfig();
//(3) 將監聽器加入config當中
config.addChangeListener(changeListener);
}
//(4) 獲取配置的方法,參數是配置的key
private String getConfig(String key) {
//(5) 調用config的getProperty獲取key對應的值,如果沒有獲取到就用DEFAULT_VALUE
String result = config.getProperty(key, DEFAULT_VALUE);
logger.info(String.format("Loading key : %s with value: %s", key, result));
return result;
}
//(6) main方法中實例化SimpleApolloConfigDemo,通過while循環不停打印輸入key的值
public static void main(String[] args) throws IOException {
SimpleApolloConfigDemo apolloConfigDemo = new SimpleApolloConfigDemo();
System.out.println(
"Apollo Config Demo. Please input key to get the value. Input quit to exit.");
while (true) {
System.out.print("> ");
String input = new BufferedReader(new InputStreamReader(System.in, Charsets.UTF_8)).readLine();
if (input == null || input.length() == 0) {
continue;
}
input = input.trim();
if (input.equalsIgnoreCase("quit")) {
System.exit(0);
}
apolloConfigDemo.getConfig(input);
}
}
}
二、Apollo客戶端與應用程序交互的窗口ConfigService
翻開Apollo-Client的代碼立馬就能找ConfigService的位置(com.ctrip.framework.apollo.ConfigService),下面是ConfigService的代碼,ConfigService是一個單例(1),也就是說對於應用程序來說只會有一個ConfigService的實例,並且實例是被通過私有靜態變量被持有在ConfigService當中,同時我們看到ConfigService持有兩個屬性ConfigManager(2)和ConfigRegistry(3),其中ConfigManager是配置(ConfigManager)的管理器,ConfigRegistry用於手工配置注入,這兩個屬性的初始化均是通過ApolloInjector來注入的,ApolloInjector和ConfigRegistry這塊我們之後單獨來分析,我文集中在配置的獲取和創建。我們在看ConfigService中的另外一個方法getAppConfig(6),該方法用戶獲取application配置文件的內容(apollo中創建的默認配置文件(namespace))。而getAppConfig中會實際調用getConfig(7)獲取配置,getConfig則是通過ConfigManager去獲取配置,接下來我們安圖索驥,進入ConfigManager中進行分析。
/**
* Entry point for client config use
*
* @author Jason Song([email protected])
*/
public class ConfigService {
//(1) ConfigService爲單例
private static final ConfigService s_instance = new ConfigService();
//(2) 包含了ConfigManager屬性,ConfigManager是config的管理器
private volatile ConfigManager m_configManager;
//(3) 包含了ConfigRegistry屬性,ConfigRegistry是ConfigFactory的註冊器
private volatile ConfigRegistry m_configRegistry;
//(4) 實例化ConfigManager,是從ApolloInjector中獲取實例,默認獲取到的是DefaultConfigManager
private ConfigManager getManager() {
if (m_configManager == null) {
synchronized (this) {
if (m_configManager == null) {
m_configManager = ApolloInjector.getInstance(ConfigManager.class);
}
}
}
return m_configManager;
}
//(5) 實例化ConfigRegistry,是從ApolloInjector中獲取實例,默認獲取到的是DefaultConfigRegistry
private ConfigRegistry getRegistry() {
if (m_configRegistry == null) {
synchronized (this) {
if (m_configRegistry == null) {
m_configRegistry = ApolloInjector.getInstance(ConfigRegistry.class);
}
}
}
return m_configRegistry;
}
/**
* Get Application's config instance.
*
* @return config instance
*/
//(6) 獲取namespace爲application的配置,application是默認namespace下的配置
public static Config getAppConfig() {
return getConfig(ConfigConsts.NAMESPACE_APPLICATION);
}
/**
* Get the config instance for the namespace.
*
* @param namespace the namespace of the config
* @return config instance
*/
//(7) 通過namespace獲取配置,實際是從configManager中去獲取的配置
public static Config getConfig(String namespace) {
return s_instance.getManager().getConfig(namespace);
}
//(8) Apollo還支持通過文件的方式獲取配置,參數中需要執行namespace和ConfigFileFormat(此爲文件類型的枚舉類)
public static ConfigFile getConfigFile(String namespace, ConfigFileFormat configFileFormat) {
return s_instance.getManager().getConfigFile(namespace, configFileFormat);
}
static void setConfig(Config config) {
setConfig(ConfigConsts.NAMESPACE_APPLICATION, config);
}
/**
* Manually set the config for the namespace specified, use with caution.
*
* @param namespace the namespace
* @param config the config instance
*/
//(9) 手工設置指定namespace的配置,實際是通過ConfigRegistry創建了ConfigFactory來設置
static void setConfig(String namespace, final Config config) {
s_instance.getRegistry().register(namespace, new ConfigFactory() {
@Override
public Config create(String namespace) {
return config;
}
@Override
public ConfigFile createConfigFile(String namespace, ConfigFileFormat configFileFormat) {
return null;
}
});
}
//(10) 設置ConfigFactory
static void setConfigFactory(ConfigFactory factory) {
setConfigFactory(ConfigConsts.NAMESPACE_APPLICATION, factory);
}
/**
* Manually set the config factory for the namespace specified, use with caution.
*
* @param namespace the namespace
* @param factory the factory instance
*/
//(11) 設置ConfigFactory的方法,實際是通過ConfigRegistry的register方法設置
static void setConfigFactory(String namespace, ConfigFactory factory) {
s_instance.getRegistry().register(namespace, factory);
}
// for test only
//(12) 重置ConfigService,標註爲只用於測試
static void reset() {
synchronized (s_instance) {
s_instance.m_configManager = null;
s_instance.m_configRegistry = null;
}
}
}
三、配置的管理器ConfigManager
ConfigManager(com.ctrip.framework.apollo.internals.ConfigManager)實際上是一個接口類,其默認包含了一個實現類DefaultConfigManager(com.ctrip.framework.apollo.internals.DefaultConfigManager),我們之後會看到實際上Apollo-Client默認也是使用的DefaultConfigManager(通過ApolloInjector注入),ConfigManager包含兩個接口方法getConfig和getConfigFile,getConfig用於獲取property類型的配置文件(即key-value形式),getConfigFile用於支持其它類型的配置文件,例如xml,yaml,yml等。接下來我們來分析下DefaultConfigManager.
/**
* @author Jason Song([email protected])
*/
public interface ConfigManager {
/**
* Get the config instance for the namespace specified.
* @param namespace the namespace
* @return the config instance for the namespace
*/
//(1) 通過namespace獲取配置
public Config getConfig(String namespace);
/**
* Get the config file instance for the namespace specified.
* @param namespace the namespace
* @param configFileFormat the config file format
* @return the config file instance for the namespace
*/
//(2) 通過namespace和文件類型獲取配置文件
public ConfigFile getConfigFile(String namespace, ConfigFileFormat configFileFormat);
}
DefaultConfigManager中持有ConfigFactoryManager(1),並且持有兩個配置緩存m_configs(2)和m_configFiles(3),用於將對應namespace的配置緩存在內存中,前面提到的ConfigFactoryManager是工廠類ConfigFactory的管理器,實際在獲取配置的時候需要先獲取ConfigFactory工廠類後再創建配置。我們仔細來看下getConfig方法(5),首先它會嘗試從緩存m_configs中獲取配置,如果沒找到則通過ConfigFactoryManager獲取ConfigFactory來創造配置。那接下來我們繼續跟蹤,來看下配置的工廠類ConfigFactory.
package com.ctrip.framework.apollo.internals;
import java.util.Map;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigFile;
import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.spi.ConfigFactory;
import com.ctrip.framework.apollo.spi.ConfigFactoryManager;
import com.google.common.collect.Maps;
/**
* @author Jason Song([email protected])
*/
public class DefaultConfigManager implements ConfigManager {
//(1) 持有ConfigFactoryManager,ConfigFactoryManager是ConfigFactory的管理器
private ConfigFactoryManager m_factoryManager;
//(2) 生成的Config存儲在這裏的Map數據結構,key爲namespace
private Map<String, Config> m_configs = Maps.newConcurrentMap();
//(3) 生成的ConfigFile存儲在這裏的Map數據結構,key爲文件名
private Map<String, ConfigFile> m_configFiles = Maps.newConcurrentMap();
//(4) 構造函數中通過ApolloInjector注入了ConfigFactoryManager
public DefaultConfigManager() {
m_factoryManager = ApolloInjector.getInstance(ConfigFactoryManager.class);
}
@Override
//(5) 通過namespace獲取Config,首先從m_configs緩存中獲取,
// 如果沒有獲取則通過ConfigFacotryManager獲取ConfigFactory並創建Config
public Config getConfig(String namespace) {
Config config = m_configs.get(namespace);
if (config == null) {
synchronized (this) {
config = m_configs.get(namespace);
if (config == null) {
ConfigFactory factory = m_factoryManager.getFactory(namespace);
config = factory.create(namespace);
m_configs.put(namespace, config);
}
}
}
return config;
}
@Override
//(6) 獲取ConfigFile,首先從緩存中獲取,
// 如果沒有獲取則通過ConfigFacotryManager獲取ConfigFactory並創建ConfigFile
public ConfigFile getConfigFile(String namespace, ConfigFileFormat configFileFormat) {
String namespaceFileName = String.format("%s.%s", namespace, configFileFormat.getValue());
ConfigFile configFile = m_configFiles.get(namespaceFileName);
if (configFile == null) {
synchronized (this) {
configFile = m_configFiles.get(namespaceFileName);
if (configFile == null) {
ConfigFactory factory = m_factoryManager.getFactory(namespaceFileName);
configFile = factory.createConfigFile(namespaceFileName, configFileFormat);
m_configFiles.put(namespaceFileName, configFile);
}
}
}
return configFile;
}
}
四、配置的工廠類ConfigFactory
ConfigFactory也是一個接口類,其默認包含一個DefaultConfigFactory,那麼Apollo也是通過ApolloInjector來講DefaultConfigFactory注入到ConfigFactoryManager當中。ConfigFactory中包含create(1)和createConfigFile(2)兩個方法,接下來我們看下DefaultConfigFactory實現類。
/**
* @author Jason Song([email protected])
*/
public interface ConfigFactory {
/**
* Create the config instance for the namespace.
*
* @param namespace the namespace
* @return the newly created config instance
*/
//(1) 創建指定namespace的Config
public Config create(String namespace);
/**
* Create the config file instance for the namespace
* @param namespace the namespace
* @return the newly created config file instance
*/
//(2) 創建指定格式和namespace的ConfigFile
public ConfigFile createConfigFile(String namespace, ConfigFileFormat configFileFormat);
}
DefaultConfigFactory的源碼如下,我們重點關注create(3)方法,在create方法中,通過namespace這個參數來判斷配置文件的類型(因爲在apollo中對於非property文件來說,都需要將文件完整名稱(包含後綴名)來作爲namespace進行出傳遞),如果後綴爲yaml或者yml則創建具備PropertiesCompatibleFileConfigRepository的DefaultConfig;如果不是,則創建帶LocalConfigRepository的DefaultConfig,這裏DefaultConfig就是配置對應的對象,DefaultConfig中持有Repository,用作配置更新的來源。關於如何獲取配置,我們將在後續的文章進行介紹。
/**
* @author Jason Song([email protected])
*/
public class DefaultConfigFactory implements ConfigFactory {
private static final Logger logger = LoggerFactory.getLogger(DefaultConfigFactory.class);
//(1) 持有Config工具類
private ConfigUtil m_configUtil;
//(2) 構造方法通過ApolloInjector注入ConfigUtil
public DefaultConfigFactory() {
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
}
@Override
//(3) 創建Config
public Config create(String namespace) {
//判斷namespace的文件類型
ConfigFileFormat format = determineFileFormat(namespace);
//如果是property兼容的文件,比如yaml,yml
if (ConfigFileFormat.isPropertiesCompatible(format)) {
//創建了默認的Config,並通過參數制定了namespace和Repository(倉庫)
return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
}
//否則創建LocalConfigRepository
return new DefaultConfig(namespace, createLocalConfigRepository(namespace));
}
@Override
//(4) 創建各種類型的ConfigFile
public ConfigFile createConfigFile(String namespace, ConfigFileFormat configFileFormat) {
ConfigRepository configRepository = createLocalConfigRepository(namespace);
switch (configFileFormat) {
case Properties:
return new PropertiesConfigFile(namespace, configRepository);
case XML:
return new XmlConfigFile(namespace, configRepository);
case JSON:
return new JsonConfigFile(namespace, configRepository);
case YAML:
return new YamlConfigFile(namespace, configRepository);
case YML:
return new YmlConfigFile(namespace, configRepository);
case TXT:
return new TxtConfigFile(namespace, configRepository);
}
return null;
}
//(5) 創建LocalFileConfigRepository,如果Apollo是以本地模式運行,則創建沒有upstream的LocalFileConfigRepository,否則
//創建一個帶有遠程倉庫RemoteConfigRepository的創建LocalFileConfigRepository
LocalFileConfigRepository createLocalConfigRepository(String namespace) {
if (m_configUtil.isInLocalMode()) {
logger.warn(
"==== Apollo is in local mode! Won't pull configs from remote server for namespace {} ! ====",
namespace);
return new LocalFileConfigRepository(namespace);
}
return new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));
}
//(6) 創建遠程RemoteConfigRepository
RemoteConfigRepository createRemoteConfigRepository(String namespace) {
return new RemoteConfigRepository(namespace);
}
//(7) 創建Property格式兼容的PropertiesCompatibleFileConfigRepository
PropertiesCompatibleFileConfigRepository createPropertiesCompatibleFileConfigRepository(String namespace,
ConfigFileFormat format) {
String actualNamespaceName = trimNamespaceFormat(namespace, format);
PropertiesCompatibleConfigFile configFile = (PropertiesCompatibleConfigFile) ConfigService
.getConfigFile(actualNamespaceName, format);
return new PropertiesCompatibleFileConfigRepository(configFile);
}
// for namespaces whose format are not properties, the file extension must be present, e.g. application.yaml
//(8) 確定文件格式
ConfigFileFormat determineFileFormat(String namespaceName) {
String lowerCase = namespaceName.toLowerCase();
for (ConfigFileFormat format : ConfigFileFormat.values()) {
if (lowerCase.endsWith("." + format.getValue())) {
return format;
}
}
return ConfigFileFormat.Properties;
}
//(9) 去掉文件擴展名的namespace
String trimNamespaceFormat(String namespaceName, ConfigFileFormat format) {
String extension = "." + format.getValue();
if (!namespaceName.toLowerCase().endsWith(extension)) {
return namespaceName;
}
return namespaceName.substring(0, namespaceName.length() - extension.length());
}
}
五、總結
文章的最後,我們總結下,本文作爲Apollo配置中心源碼學習的第一篇文章,從Demo入手,按照Config創建的主線進行分析,學習Config創建的流程,在分析源碼過程中,我們要記住分析的主線,對於其它一些內容可以在完成主線分析後再進行分析,最後我們通過一個時序圖來講上述表示上述分析。(文中分析不足之處請各位指正)