随着越来越多的应用进行了微服务化改造以及相同的应用程序对不同环境(开发、测试、生产环境)、不同部署集群的需求,将应用中的配置与程序解耦变得越来越重要,在过去,我们的配置文件往往和程序捆绑在一起,当需要修改配置文件时,需要对应用程序进行重新打包的操作,从而导致了应用发布效率的降低。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 m_configs = Maps.newConcurrentMap();
//(3) 生成的ConfigFile存储在这里的Map数据结构,key为文件名
private Map 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创建的流程,在分析源码过程中,我们要记住分析的主线,对于其它一些内容可以在完成主线分析后再进行分析,最后我们通过一个时序图来讲上述表示上述分析。(文中分析不足之处请各位指正)