⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Apollo/client-config-api-4/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

阅读源码最好的方式,是使用 IDEA 进行调试 Apollo 源码,不然会一脸懵逼。

胖友可以点击「芋道源码」扫码关注,回复 git018 关键字
获得艿艿添加了中文注释的 Apollo 源码地址。

阅读源码很孤单,加入源码交流群,一起坚持!

1. 概述

老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》 ,特别是 《Java 客户端使用指南》

本文接 《Apollo 源码解析 —— 客户端 API 配置(二)之一览》 一文,分享 ConfigRepository 接口,及其子类,如下图:

ConfigRepository 类图

《Apollo 源码解析 —— 客户端 API 配置(一)之一览》「5.2.1.4 创建 LocalConfigRepository 对象」 中,我们简单定义 ConfigRepository 如下:

这里我们可以简单( 但不完全准确 )理解成配置的 Repository ,负责从远程的 Config Service 读取配置。

  • 为什么笔者会说 但不完全准确 呢?答案在 LocalConfigRepository 的实现中。

2. LocalFileConfigRepository

com.ctrip.framework.apollo.internals.LocalFileConfigRepository实现 RepositoryChangeListener 接口,继承 AbstractConfigRepository 抽象类,本地文件配置 Repository 实现类。

重点在 「2. 6 sync」 方法。

2.1 构造方法

/**
* 配置文件目录
*/
private static final String CONFIG_DIR = "/config-cache";

/**
* Namespace 名字
*/
private final String m_namespace;
/**
* 本地缓存配置文件目录
*/
private File m_baseDir;
private final ConfigUtil m_configUtil;
/**
* 配置文件 Properties
*/
private volatile Properties m_fileProperties;
/**
* 上游的 ConfigRepository 对象。一般情况下,使用 RemoteConfigRepository 对象,读取远程 Config Service 的配置
*/
private volatile ConfigRepository m_upstream;

/**
* Constructor.
*
* @param namespace the namespace
*/
public LocalFileConfigRepository(String namespace) {
this(namespace, null);
}

public LocalFileConfigRepository(String namespace, ConfigRepository upstream) {
m_namespace = namespace;
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
// 获得本地缓存配置文件的目录
this.setLocalCacheDir(findLocalCacheDir(), false);
// 设置 `m_upstream` 属性
this.setUpstreamRepository(upstream);
// 同步配置
this.trySync();
}

  • m_baseDir 字段,本地缓存配置文件目录( 😈 闭合比较好奇的是,为什么不直接是配置文件,而是配置文件目录,从代码看下来是非目录啊 )。在构造方法中,进行初始化,胖友先跳到 「2.2 findLocalCacheDir」「2.3 setLocalCacheDir」
  • m_fileProperties 字段,配置文件 Properties 。
  • m_upstream 字段,上游的 ConfigRepository 对象。一般情况下,使用 RemoteConfigRepository 对象,读取远程 Config Service 的配置。在构造方法中,调用 #setUpstreamRepository(ConfigRepository) 方法,设置 m_upstream 属性,初始拉取 Config Service 的配置,并监听配置变化。详细解析,胖友先跳到 「2.4 setUpstreamRepository」
  • 调用 #trySync() 方法,同步配置。详细解析,见 「2.6 sync」

2.2 findLocalCacheDir

#findLocalCacheDir() 方法,获得本地缓存目录。代码如下:

private File findLocalCacheDir() {
try {
// 获得默认缓存配置目录
String defaultCacheDir = m_configUtil.getDefaultLocalCacheDir();
// 若不存在该目录,进行创建
Path path = Paths.get(defaultCacheDir);
if (!Files.exists(path)) {
Files.createDirectories(path);
}
// 返回该目录下的 CONFIG_DIR 目录
if (Files.exists(path) && Files.isWritable(path)) {
return new File(defaultCacheDir, CONFIG_DIR);
}
} catch (Throwable ex) {
//ignore
}
// 若失败,使用 ClassPath 下的 CONFIG_DIR 目录
return new File(ClassLoaderUtil.getClassPath(), CONFIG_DIR);
}

  • 调用 ConfigUtil#getDefaultLocalCacheDir() 方法,获得默认缓存配置目录。代码如下:

    public String getDefaultLocalCacheDir() {
    String cacheRoot = isOSWindows() ? "C:\\opt\\data\\%s" : "/opt/data/%s";
    return String.format(cacheRoot, getAppId()); // appId
    }

    • 在非 Windows 的环境下,是 /opt/data/${appId} 目录。
  • 调用 Files#exists(path) 方法,判断若默认缓存配置目录不存在,进行创建。😈但是,可能我们的应用程序没有该目录的权限,此时会导致创建失败。那么就有会出现两种情况:

    • 第一种,有权限,使用 /opt/data/${appId}/ + config-cache 目录。
    • 第二种,无权限,使用 ClassPath/ + config-cache 目录。这个目录,应用程序下,肯定是有权限的。

2.3 setLocalCacheDir

调用 #setLocalCacheDir(baseDir, syncImmediately) 方法,设置 m_baseDir 字段。代码如下:

void setLocalCacheDir(File baseDir, boolean syncImmediately) {
m_baseDir = baseDir;
// 获得本地缓存配置文件的目录
this.checkLocalConfigCacheDir(m_baseDir);
// 若需要立即同步,则进行同步
if (syncImmediately) {
this.trySync();
}
}

  • 调用 #checkLocalConfigCacheDir(baseDir) 方法,校验本地缓存配置目录是否存在。若不存在,则进行创建。详细解析,见 「2.3.1 checkLocalConfigCacheDir」
  • syncImmediately = true ,则进行同步。目前仅在单元测试中,会出现这种情况。正式的代码,syncImmediately = false

2.3.1 checkLocalConfigCacheDir

#checkLocalConfigCacheDir(baseDir) 方法,校验本地缓存配置目录是否存在。若不存在,则进行创建。代码如下:

private void checkLocalConfigCacheDir(File baseDir) {
// 若本地缓存配置文件的目录已经存在,则返回
if (baseDir.exists()) {
return;
}
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "createLocalConfigDir");
transaction.addData("BaseDir", baseDir.getAbsolutePath());
try {
// 创建本地缓存配置目录
Files.createDirectory(baseDir.toPath());
transaction.setStatus(Transaction.SUCCESS);
} catch (IOException ex) {
ApolloConfigException exception = new ApolloConfigException(String.format("Create local config directory %s failed", baseDir.getAbsolutePath()), ex);
Tracer.logError(exception);
transaction.setStatus(exception);
logger.warn("Unable to create local config cache directory {}, reason: {}. Will not able to cache config file.", baseDir.getAbsolutePath(), ExceptionUtil.getDetailMessage(ex));
} finally {
transaction.complete();
}
}

  • 是不是有点懵逼?该方法校验创建config-cache 目录。这个目录在 #findLocalCacheDir() 方法中,并未创建。

2.3.2 assembleLocalCacheFile

那么完整的缓存配置文件到底路径是什么呢?${baseDir}/config-cache/ + ${appId}+${cluster} + ${namespace}.properties ,即 #assembleLocalCacheFile(baseDir, namespace) 方法,拼接完整的本地缓存配置文件的地址。代码如下:

File assembleLocalCacheFile(File baseDir, String namespace) {
String fileName = String.format("%s.properties", Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR) // + 号分隔
.join(m_configUtil.getAppId(), m_configUtil.getCluster(), namespace));
return new File(baseDir, fileName);
}

  • 这也是笔者疑惑的,【配置文件】是可以固定下来的。

2.3.3 loadFromLocalCacheFile

#loadFromLocalCacheFile(baseDir, namespace) 方法,缓存配置文件,读取 Properties 。代码如下:

private Properties loadFromLocalCacheFile(File baseDir, String namespace) {
Preconditions.checkNotNull(baseDir, "Basedir cannot be null");
// 拼接本地缓存的配置文件 File 对象
File file = assembleLocalCacheFile(baseDir, namespace);
// 从文件中,读取 Properties
Properties properties;
if (file.isFile() && file.canRead()) {
InputStream in = null;
try {
in = new FileInputStream(file);
properties = new Properties();
properties.load(in); // 读取
logger.debug("Loading local config file {} successfully!", file.getAbsolutePath());
} catch (IOException ex) {
Tracer.logError(ex);
throw new ApolloConfigException(String.format("Loading config from local cache file %s failed", file.getAbsolutePath()), ex);
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException ex) {
// ignore
}
}
} else {
throw new ApolloConfigException(String.format("Cannot read from local cache file %s", file.getAbsolutePath()));
}
return properties;
}

2.3.4 persistLocalCacheFile

#loadFromLocalCacheFile(baseDir, namespace) 方法,缓存配置文件,写入 Properties 。代码如下:

#loadFromLocalCacheFile(baseDir, namespace) 方法,相反。

void persistLocalCacheFile(File baseDir, String namespace) {
if (baseDir == null) {
return;
}
// 拼接本地缓存的配置文件 File 对象
File file = assembleLocalCacheFile(baseDir, namespace);
// 向文件中,写入 Properties
OutputStream out = null;
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "persistLocalConfigFile");
transaction.addData("LocalConfigFile", file.getAbsolutePath());
try {
out = new FileOutputStream(file);
m_fileProperties.store(out, "Persisted by DefaultConfig"); // 写入
transaction.setStatus(Transaction.SUCCESS);
} catch (IOException ex) {
ApolloConfigException exception = new ApolloConfigException(String.format("Persist local cache file %s failed", file.getAbsolutePath()), ex);
Tracer.logError(exception);
transaction.setStatus(exception);
logger.warn("Persist local cache file {} failed, reason: {}.", file.getAbsolutePath(), ExceptionUtil.getDetailMessage(ex));
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ex) {
//ignore
}
}
transaction.complete();
}
}

2.3.5 updateFileProperties

#updateFileProperties(newProperties) 方法,若 Properties 发生变化缓存配置文件,写入 Properties 。代码如下:

老艿艿:在 #persistLocalCacheFile(baseDir, namespace) 方法,进一步封装。

private synchronized void updateFileProperties(Properties newProperties) {
// 忽略,若未变更
if (newProperties.equals(m_fileProperties)) {
return;
}
// 设置新的 Properties 到 `m_fileProperties` 中。
this.m_fileProperties = newProperties;
// 持久化到本地缓存配置文件
persistLocalCacheFile(m_baseDir, m_namespace);
}

2.4 setUpstreamRepository

#setUpstreamRepository(ConfigRepository) 方法,设置 m_upstream 属性,初始拉取 Config Service 的配置,并监听配置变化。代码如下:

老艿艿:此处 ConfigRepository 以 RemoteConfigRepository 举例子。实际代码实现里,也是它。

 1: @Override
2: public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) {
3: if (upstreamConfigRepository == null) {
4: return;
5: }
6: // 从老的 `m_upstream` 移除自己
7: // clear previous listener
8: if (m_upstream != null) {
9: m_upstream.removeChangeListener(this);
10: }
11: // 设置新的 `m_upstream`
12: m_upstream = upstreamConfigRepository;
13: // 从 `m_upstream` 拉取配置
14: trySyncFromUpstream();
15: // 向新的 `m_upstream` 注册自己
16: upstreamConfigRepository.addChangeListener(this);
17: }

  • 第 6 至 10 行:调用 ConfigRepository#removeChangeListener(RepositoryChangeLister) 方法,从m_upstream 中,移除自己( 监听器 )。否则,会错误监听。
  • 第 12 行:设置m_upstream
  • 第 14 行:调用 #trySyncFromUpstream() 方法,从 m_upstream 拉取初始配置。详细解析,见 「2.5 trySyncFromUpstream」
  • 第 16 行:调用 ConfigRepository#addChangeListener(RepositoryChangeLister) 方法,向m_upstream 中,注册自己( 监听器 ) 。从而实现 Config Service 配置变更的监听。这也是为什么 LocalFileConfigRepository 实现了 RepositoryChangeListener 接口的原因。整体的监听和通知如下图:流程

2.5 trySyncFromUpstream

#trySyncFromUpstream() 方法,从 m_upstream 拉取初始配置,并返回是否拉取成功。代码如下:

 1: private boolean trySyncFromUpstream() {
2: if (m_upstream == null) {
3: return false;
4: }
5: try {
6: // 从 `m_upstream` 拉取配置 Properties
7: Properties properties = m_upstream.getConfig();
8: // 更新到 `m_fileProperties` 中
9: updateFileProperties(properties);
10: // 返回同步成功
11: return true;
12: } catch (Throwable ex) {
13: Tracer.logError(ex);
14: logger.warn("Sync config from upstream repository {} failed, reason: {}", m_upstream.getClass(), ExceptionUtil.getDetailMessage(ex));
15: }
16: // 返回同步失败
17: return false;
18: }

  • 第 2 至 4 行:当 m_upstream 为空时,返回拉取失败 false
  • 第 7 行:调用 ConfigRepository#getConfig() 方法,从 m_upstream 拉取配置 Properties 。
  • 第 9 行:调用 #updateFileProperties(properties) 方法,更新到 m_fileProperties 中。
  • 第 11 行:返回同步成功 true
  • 第 17 行:返回同步失败 false

那么,为什么要返回同步结果呢?答案在 「2.6 sync」 中。

2.6 sync

埋了这么多的伏笔( 代码 ),我们将要本文最重要的方法 #sync() !!!

非本地模式的情况下,LocalFileConfigRepository 在初始化时,会首先从远程 Config Service 同步( 加载 )配置。若同步(加载)失败,则读取本地缓存的配置文件。

本地模式的情况下,则只读取本地缓存的配置文件。当然,严格来说,也不一定是缓存,可以是开发者,手动创建的配置文件。

实现代码如下:

@Override
protected void sync() {
// 从 `m_upstream` 同步配置
// sync with upstream immediately
boolean syncFromUpstreamResultSuccess = trySyncFromUpstream();
// 若成功,则直接返回
if (syncFromUpstreamResultSuccess) {
return;
}
// 若失败,读取本地缓存的配置文件
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncLocalConfig");
Throwable exception = null;
try {
transaction.addData("Basedir", m_baseDir.getAbsolutePath());
// 加载本地缓存的配置文件
m_fileProperties = this.loadFromLocalCacheFile(m_baseDir, m_namespace);
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
transaction.setStatus(ex);
exception = ex;
//ignore
} finally {
transaction.complete();
}
// 若未读取到缓存的配置文件,抛出异常
if (m_fileProperties == null) {
throw new ApolloConfigException("Load config from local config failed!", exception);
}
}

  • 结合代码注释 + 上述说明,理解下具体的代码。

2.7 onRepositoryChange

当 RemoteRepositoryConfig 读取到配置变更时,调用 #onRepositoryChange(name, newProperties) 方法,更新 m_fileProperties ,并通知监听器们。代码如下:

@Override
public void onRepositoryChange(String namespace, Properties newProperties) {
// 忽略,若未变更
if (newProperties.equals(m_fileProperties)) {
return;
}
// 读取新的 Properties 对象
Properties newFileProperties = new Properties();
newFileProperties.putAll(newProperties);
// 更新到 `m_fileProperties` 中
updateFileProperties(newFileProperties);
// 发布 Repository 的配置发生变化,触发对应的监听器们
super.fireRepositoryChange(namespace, newProperties);
}

2.8 getConfig

@Override
public Properties getConfig() {
// 如果 `m_fileProperties` 为空,强制宫廷不
if (m_fileProperties == null) {
sync();
}
// 返回新创建的 `m_fileProperties` 对象,避免原有对象被修改。
Properties result = new Properties();
result.putAll(m_fileProperties);
return result;
}

666. 彩蛋

哇哈哈,开始 Apollo 客户端的 Spring 的整合方式。

知识星球

文章目录
  1. 1. 1. 概述
  2. 2. 2. LocalFileConfigRepository
    1. 2.1. 2.1 构造方法
    2. 2.2. 2.2 findLocalCacheDir
    3. 2.3. 2.3 setLocalCacheDir
      1. 2.3.1. 2.3.1 checkLocalConfigCacheDir
      2. 2.3.2. 2.3.2 assembleLocalCacheFile
      3. 2.3.3. 2.3.3 loadFromLocalCacheFile
      4. 2.3.4. 2.3.4 persistLocalCacheFile
      5. 2.3.5. 2.3.5 updateFileProperties
    4. 2.4. 2.4 setUpstreamRepository
    5. 2.5. 2.5 trySyncFromUpstream
    6. 2.6. 2.6 sync
    7. 2.7. 2.7 onRepositoryChange
    8. 2.8. 2.8 getConfig
  3. 3. 666. 彩蛋