代码边缘
如果网络的边缘是设备那么代码的边缘可能是调用api的地方
最近有使用到携程的微服务配置服务apollo,根据介绍在客户端使用的是client来获取配置代码如下:
Config config = ConfigService.getAppConfig();
config.addChangeListener(configChangeEvent -> {
Set<String> changedKeys = configChangeEvent.changedKeys();
for (String key : changedKeys) {
ConfigChange change = configChangeEvent.getChange(key);
System.out.println("key " + key + ",oldValue: " + change.getOldValue() + ",newValue: "
+ change.getNewValue() + ",changeType: " + change.getChangeType().name());
}
});
进一步分析
首先是通过config类获取配置,然后再通过添加监听器可以来监听配置的修改和日志输出。接下来从ConfigService.getAppConfig()
开始进去分析
// 默认使用"application" 作为namespace
public static Config getAppConfig() {
return getConfig(ConfigConsts.NAMESPACE_APPLICATION);
}
// 进入getConfig
public static Config getConfig(String namespace) {
return s_instance.getManager().getConfig(namespace);
}
// DefaultConfigManager -》 getConfig
private Map<String, Config> m_configs = Maps.newConcurrentMap();
@Override
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;
}
这里首先从本地的map里获取配置信息,如果没有再同步地从本地的map里获取一次,其实这里是怕并发多次获取导致配置的不一致性避免重复获取远程的配置。相当于一个线程安全的单例模式。接下来获取完配置就设置到本地了供下一次获取。接下来进入factory.create()
此处使用的配置工厂方法创建了一个配置类config
// calss: DefaultConfigFactory
public Config create(String namespace) {
ConfigFileFormat format = determineFileFormat(namespace);
//此处判断是否是.yml文件 代码如下
if (ConfigFileFormat.isPropertiesCompatible(format)) {
return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
}
// 此时进入这里创建配置文件
return new DefaultConfig(namespace, createLocalConfigRepository(namespace));
}
// ConfigFileFormat.isPropertiesCompatible
public static boolean isPropertiesCompatible(ConfigFileFormat format) {
return format == YAML || format == YML;
}
接下来进入创建配置文件的里面,首先会创建个本地的配置仓库作为缓存。
LocalFileConfigRepository createLocalConfigRepository(String namespace) {
// 本地模式的话直接返回 此处可以在配置文件配置env
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));
}
// 这里主要看从远程创建的情况 createRemoteConfigRepository()
RemoteConfigRepository createRemoteConfigRepository(String namespace) {
return new RemoteConfigRepository(namespace);
}
// 接下来是一个构造器 忽略一些无关的代码
public RemoteConfigRepository(String namespace) {
m_namespace = namespace;
........
this.trySync(); // 首先进行一次同步
this.schedulePeriodicRefresh(); // 在配置一个定时的刷新也就是客户端从远程拉去新配置
this.scheduleLongPollingRefresh(); // 再来一个长轮训用来接收远端的配置信息
}
以上 RemoteConfigRepository
构造方法里的几个方法可以结合apollo的wiki图理解
方法细节
sync
本方法主要就是从远端拉去最新的配置信息同步到本地
// 此处进入同步
protected boolean trySync() {
try {
sync();
return true;
} catch (Throwable ex) {
Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
logger
.warn("Sync config failed, will retry. Repository {}, reason: {}", this.getClass(), ExceptionUtil
.getDetailMessage(ex));
}
return false;
}
protected synchronized void sync() {
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");
try {
// 获取远程和本地的配置
ApolloConfig previous = m_configCache.get();
ApolloConfig current = loadApolloConfig();
//在返回http码为304时候表示相等 有修改的时候就设置本地的cache
if (previous != current) {
logger.debug("Remote Config refreshed!");
m_configCache.set(current);
// 调用监听器有修改本地仓库配置的 也有打印日志的
this.fireRepositoryChange(m_namespace, this.getConfig());
}
if (current != null) {
// 输出日志;
}
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
throw ex;
} finally {
transaction.complete();
}
}
schedulePeriodicRefresh
定时远程获取 默认的单位是 5 * 分钟 也就是5分钟刷新一次 防止adminServer宕机未通知到位。
private void schedulePeriodicRefresh() {
logger.debug("Schedule periodic refresh with interval: {} {}",
m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());
m_executorService.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace));
logger.debug("refresh config for namespace: {}", m_namespace);
// 其实就是个定时的同步调用sync 此处的代码已经讲解
trySync();
Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION);
}
}, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),
m_configUtil.getRefreshIntervalTimeUnit());
}
scheduleLongPollingRefresh
这里又到了核心的代码,功能主要是对配置服务进行长轮训,如果配置服务更新会第一时间通知到客户端。这里的时间默认是1s
// 先追到最底层
private void scheduleLongPollingRefresh() {
remoteConfigLongPollService.submit(m_namespace, this);
}
public boolean submit(String namespace, RemoteConfigRepository remoteConfigRepository) {
boolean added = m_longPollNamespaces.put(namespace, remoteConfigRepository);
m_notifications.putIfAbsent(namespace, INIT_NOTIFICATION_ID);
if (!m_longPollStarted.get()) {
startLongPolling(); // 此处开始对远端的长轮训
}
return added;
}
private void startLongPolling() {
// CAS的方式查看是否已经开始轮训
if (!m_longPollStarted.compareAndSet(false, true)) {
//already started
return;
}
try {
// 加载项目的配置信息
final String appId = m_configUtil.getAppId();
final String cluster = m_configUtil.getCluster();
final String dataCenter = m_configUtil.getDataCenter();
final String secret = m_configUtil.getAccessKeySecret();
final long longPollingInitialDelayInMills = m_configUtil.getLongPollingInitialDelayInMills();
m_longPollingService.submit(new Runnable() {
@Override
public void run() {
// 判断是否延迟进行长轮训
if (longPollingInitialDelayInMills > 0) {
try {
logger.debug("Long polling will start in {} ms.", longPollingInitialDelayInMills);
TimeUnit.MILLISECONDS.sleep(longPollingInitialDelayInMills);
} catch (InterruptedException e) {
//ignore
}
}
doLongPollingRefresh(appId, cluster, dataCenter, secret); // 接下来进入这里
}
});
} // 忽略catch后面部分
}
接下去是doLongPollingRefresh
的代码
apollo
首先会获取配置服务的地址列表进行随机选择一个,在请求成功也就是返回200的时候进行通知,也就是1处。此时出现200的返回码说明发生了配置文件的修改。
在末尾也就是2处当配置文件一直没有修改时会随机的设置lastServiceDto
为null,而lastServiceDto
为null意味着需要重新去寻找一个配置服务地址,就有可能获取新的配置服务地址。如果有配置服务上下线也会进行更新。
结尾
那是不是不去获取配置就不会链接上配置服务了呢?那肯定不是的。通过以上说明我去掉了这个获取配置的方法,在项目启动时通过实现Spring的BeanFactoryPostProcessor接口实现了初始化的功能。
结束
L&P