apollo提供了基础数据库存储的配置管理中心,服务分client、adminService、configService、portal等模块。其中client服务在configService获取配置数据,监听配置数据,提供给具体应用的配置初始化以及配置动态更新操作
配置信息模型接口
Config
/**
* 配置信息模型接口
* @author Jason Song(song_s@ctrip.com)
*/
public interface Config {
/**
* 获取String类型配置根据配置KEY
*/
public String getProperty(String key, String defaultValue);
/**
* 获取int类型配置根据配置KEY
*/
public Integer getIntProperty(String key, Integer defaultValue);
/**
* 获取long类型配置根据配置KEY
*/
public Long getLongProperty(String key, Long defaultValue);
/**
* 获取short类型配置根据配置KEY
*/
public Short getShortProperty(String key, Short defaultValue);
/**
* 获取float类型配置根据配置KEY
*/
public Float getFloatProperty(String key, Float defaultValue);
/**
* 获取double类型配置根据配置KEY
*/
public Double getDoubleProperty(String key, Double defaultValue);
/**
* 获取byte类型配置根据配置KEY
*/
public Byte getByteProperty(String key, Byte defaultValue);
/**
* Return the boolean property value with the given key, or {@code defaultValue} if the key
* doesn't exist.
*/
public Boolean getBooleanProperty(String key, Boolean defaultValue);
/**
* Return the array property value with the given
*/
public String[] getArrayProperty(String key, String delimiter, String[] defaultValue);
/**
* Return the Date property value with the given name, or {@code defaultValue} if the name doesn't exist.
*/
public Date getDateProperty(String key, Date defaultValue);
/**
* Return the Date property value with the given name, or {@code defaultValue} if the name doesn't exist.
*/
public Date getDateProperty(String key, String format, Date defaultValue);
/**
* Return the Date property value with the given name, or {@code defaultValue} if the name doesn't exist.
*/
public Date getDateProperty(String key, String format, Locale locale, Date defaultValue);
/**
* Return the Enum property value with the given key, or {@code defaultValue} if the key doesn't exist.
*/
public <T extends Enum<T>> T getEnumProperty(String key, Class<T> enumType, T defaultValue);
/**
* Return the duration property value(in milliseconds) with the given name, or {@code
* defaultValue} if the name doesn't exist. Please note the format should comply with the follow
* example (case insensitive). Examples:
* <pre>
* "123MS" -- parses as "123 milliseconds"
* "20S" -- parses as "20 seconds"
* "15M" -- parses as "15 minutes" (where a minute is 60 seconds)
* "10H" -- parses as "10 hours" (where an hour is 3600 seconds)
* "2D" -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
* "2D3H4M5S123MS" -- parses as "2 days, 3 hours, 4 minutes, 5 seconds and 123 milliseconds"
* </pre>
*
* @param key the property name
* @param defaultValue the default value when name is not found or any error occurred
* @return the parsed property value(in milliseconds)
*/
public long getDurationProperty(String key, long defaultValue);
/**
* 添加配置改变监听器
* @param listener the config change listener
*/
public void addChangeListener(ConfigChangeListener listener);
/**
* 添加配置改变的监听器
*/
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys);
/**
* 获取所有配置KEY
*/
public Set<String> getPropertyNames();
}
Config接口,配置信息超类,提供给应用获取配置信息的接口
功能如下:
- 提供基础类型值的配置获取
- 提供注册配置改变的监听器,用于配置改变回调
AbstractConfig
public abstract class AbstractConfig implements Config {
/**
* 监听器集合
*/
private final List<ConfigChangeListener> m_listeners = Lists.newCopyOnWriteArrayList();
private final Map<ConfigChangeListener, Set<String>> m_interestedKeys = Maps.newConcurrentMap();
private final ConfigUtil m_configUtil;
/**
* 配置信息缓存
*/
private volatile Cache<String, Integer> m_integerCache;
private volatile Cache<String, Long> m_longCache;
private volatile Cache<String, Short> m_shortCache;
private volatile Cache<String, Float> m_floatCache;
private volatile Cache<String, Double> m_doubleCache;
private volatile Cache<String, Byte> m_byteCache;
private volatile Cache<String, Boolean> m_booleanCache;
private volatile Cache<String, Date> m_dateCache;
private volatile Cache<String, Long> m_durationCache;
private final Map<String, Cache<String, String[]>> m_arrayCache;
private final List<Cache> allCaches;
/**
* 获取配置并缓存到本地缓存
* @param key
* @param parser
* @param cache
* @param defaultValue
* @param <T>
* @return
*/
private <T> T getValueAndStoreToCache(String key, Function<String, T> parser, Cache<String, T> cache, T defaultValue) {
long currentConfigVersion = m_configVersion.get();
String value = getProperty(key, null);
if (value != null) {
T result = parser.apply(value);
if (result != null) {
synchronized (this) {
if (m_configVersion.get() == currentConfigVersion) {
cache.put(key, result);
}
}
return result;
}
}
return defaultValue;
}
/**
* 配置改变调用监听器触发回调方法
* @param changeEvent
*/
protected void fireConfigChange(final ConfigChangeEvent changeEvent) {
for (final ConfigChangeListener listener : m_listeners) {
// check whether the listener is interested in this change event
if (!isConfigChangeListenerInterested(listener, changeEvent)) {
continue;
}
m_executorService.submit(new Runnable() {
@Override
public void run() {
String listenerName = listener.getClass().getName();
Transaction transaction = Tracer.newTransaction("Apollo.ConfigChangeListener", listenerName);
try {
listener.onChange(changeEvent);
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
Tracer.logError(ex);
logger.error("Failed to invoke config change listener {}", listenerName, ex);
} finally {
transaction.complete();
}
}
});
}
}
/**
* 对比最新配置与本地缓存差异构建改变事件
* @param namespace
* @param previous
* @param current
* @return
*/
List<ConfigChange> calcPropertyChanges(String namespace, Properties previous,
Properties current) {
if (previous == null) {
previous = new Properties();
}
if (current == null) {
current = new Properties();
}
Set<String> previousKeys = previous.stringPropertyNames();
Set<String> currentKeys = current.stringPropertyNames();
Set<String> commonKeys = Sets.intersection(previousKeys, currentKeys);
Set<String> newKeys = Sets.difference(currentKeys, commonKeys);
Set<String> removedKeys = Sets.difference(previousKeys, commonKeys);
List<ConfigChange> changes = Lists.newArrayList();
for (String newKey : newKeys) {
changes.add(new ConfigChange(namespace, newKey, null, current.getProperty(newKey),
PropertyChangeType.ADDED));
}
for (String removedKey : removedKeys) {
changes.add(new ConfigChange(namespace, removedKey, previous.getProperty(removedKey), null,
PropertyChangeType.DELETED));
}
for (String commonKey : commonKeys) {
String previousValue = previous.getProperty(commonKey);
String currentValue = current.getProperty(commonKey);
if (Objects.equal(previousValue, currentValue)) {
continue;
}
changes.add(new ConfigChange(namespace, commonKey, previousValue, currentValue,
PropertyChangeType.MODIFIED));
}
return changes;
}
}
AbstractConfig抽象实现类,完成了配置的通用功能
功能如下:
- 提供配置本地缓存功能
- calcPropertyChanges:对比本地缓存与最新配置构建配置改变事件
- 提供监听器注册以及监听器回调触发功能fireConfigChange
DefaultConfig
/**
* 配置信息默认实现
* @author Jason Song(song_s@ctrip.com)
*/
public class DefaultConfig extends AbstractConfig implements RepositoryChangeListener {
private static final Logger logger = LoggerFactory.getLogger(DefaultConfig.class);
/**
* 命名空间
*/
private final String m_namespace;
/**
* 本地文件缓存配置
*/
private Properties m_resourceProperties;
/**
* 配置中心配置本地缓存
*/
private AtomicReference<Properties> m_configProperties;
/**
* 配置仓库服务,用于获取远程配置中心配置
*/
private ConfigRepository m_configRepository;
/**
* 流控信息
*/
private RateLimiter m_warnLogRateLimiter;
/**
* 初始化,通过配置仓库加载配置中心配置信息
*/
private void initialize() {
try {
m_configProperties.set(m_configRepository.getConfig());
} catch (Throwable ex) {
Tracer.logError(ex);
logger.warn("Init Apollo Local Config 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
m_configRepository.addChangeListener(this);
}
}
/**
* 获取配置信息
* @param key the property name
* @param defaultValue the default value when key is not found or any error occurred
* @return
*/
@Override
public String getProperty(String key, String defaultValue) {
//System.getProperty方式获取配置优先级最高
// step 1: check system properties, i.e. -Dkey=value
String value = System.getProperty(key);
//远程仓库方式获取配置优先级次之
// step 2: check local cached properties file
if (value == null && m_configProperties.get() != null) {
value = m_configProperties.get().getProperty(key);
}
//System.getenv获取配置优先级第三
/**
* step 3: check env variable, i.e. PATH=...
* normally system environment variables are in UPPERCASE, however there might be exceptions.
* so the caller should provide the key in the right case
*/
if (value == null) {
value = System.getenv(key);
}
//本地配置资源获取配置优先级最低
// step 4: check properties file from classpath
if (value == null && m_resourceProperties != null) {
value = (String) m_resourceProperties.get(key);
}
if (value == null && m_configProperties.get() == null && m_warnLogRateLimiter.tryAcquire()) {
logger.warn("Could not load config for namespace {} from Apollo, please check whether the configs are released in Apollo! Return default value now!", m_namespace);
}
return value == null ? defaultValue : value;
}
/**
* 配置仓库中的配置改变回调函数
* @param namespace the namespace of this repository change
* @param newProperties the properties after change
*/
@Override
public synchronized void onRepositoryChange(String namespace, Properties newProperties) {
if (newProperties.equals(m_configProperties.get())) {
return;
}
Properties newConfigProperties = new Properties();
newConfigProperties.putAll(newProperties);
Map<String, ConfigChange> actualChanges = updateAndCalcConfigChanges(newConfigProperties);
//check double checked result
if (actualChanges.isEmpty()) {
return;
}
//调用配置改变回调方法,通知配置改变监听器
this.fireConfigChange(new ConfigChangeEvent(m_namespace, actualChanges));
Tracer.logEvent("Apollo.Client.ConfigChanges", m_namespace);
}
private Map<String, ConfigChange> updateAndCalcConfigChanges(Properties newConfigProperties) {
List<ConfigChange> configChanges =
calcPropertyChanges(m_namespace, m_configProperties.get(), newConfigProperties);
ImmutableMap.Builder<String, ConfigChange> actualChanges =
new ImmutableMap.Builder<>();
/** === Double check since DefaultConfig has multiple config sources ==== **/
//1. use getProperty to update configChanges's old value
for (ConfigChange change : configChanges) {
change.setOldValue(this.getProperty(change.getPropertyName(), change.getOldValue()));
}
//2. update m_configProperties
m_configProperties.set(newConfigProperties);
clearConfigCache();
//3. use getProperty to update configChange's new value and calc the final changes
for (ConfigChange change : configChanges) {
change.setNewValue(this.getProperty(change.getPropertyName(), change.getNewValue()));
switch (change.getChangeType()) {
case ADDED:
if (Objects.equals(change.getOldValue(), change.getNewValue())) {
break;
}
if (change.getOldValue() != null) {
change.setChangeType(PropertyChangeType.MODIFIED);
}
actualChanges.put(change.getPropertyName(), change);
break;
case MODIFIED:
if (!Objects.equals(change.getOldValue(), change.getNewValue())) {
actualChanges.put(change.getPropertyName(), change);
}
break;
case DELETED:
if (Objects.equals(change.getOldValue(), change.getNewValue())) {
break;
}
if (change.getNewValue() != null) {
change.setChangeType(PropertyChangeType.MODIFIED);
}
actualChanges.put(change.getPropertyName(), change);
break;
default:
//do nothing
break;
}
}
return actualChanges.build();
}
/**
* 加载本地资源配置信息根据命名空间;META-INF/config/{namespace}.properties
* @param namespace
* @return
*/
private Properties loadFromResource(String namespace) {
String name = String.format("META-INF/config/%s.properties", namespace);
InputStream in = ClassLoaderUtil.getLoader().getResourceAsStream(name);
Properties properties = null;
if (in != null) {
properties = new Properties();
try {
properties.load(in);
} catch (IOException ex) {
Tracer.logError(ex);
logger.error("Load resource config for namespace {} failed", namespace, ex);
} finally {
try {
in.close();
} catch (IOException ex) {
// ignore
}
}
}
return properties;
}
}
DefaultConfig类为客户端提供的默认配置获取类,此类主要完成以下功能
- 初始化从配置仓库拉取远程的配置信息
- 注册当前实例(配置仓库的监听器)到配置仓库,用于监听配置仓库的改变
- 提供配置仓库改变的回调函数onRepositoryChange
- 根据KEY查找配置值信息
- 加载当前配置命名空间的本地资源(loadFromResource)
其中获取配置有以下几种方式,按优先级由高到低如下:
- 通过System.getProperty方式获取配置
- 远程仓库方式获取配置
- System.getenv获取配置
- 本地配置资源获取配置