源码解析之配置加载流程
文章有点长,还需慢慢看。
源码阅读前的建议:
1.了解过Spring&SpringBoot源码(必须)。
2.了解过SpringBoot配置文件的加载流程(必须)
开始撸起!
1.前序
在我们项目中使用Nacos做配置中心时,我们首先会引入Nacos-Config的依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
了解过SpringBoot源码的小伙伴肯定知道其他框架与SpringBoot进行整合的时候肯定会有个XXXAutoConfiguration的类,这是一个自动配置装配的类,关于自动装配类的加载原理本文暂时不做介绍。
那么Nacos是否存在这样的类呢?答案是肯定有的
这个地方需要我们需要重点关注的2个类:
- NacosConfigBootstrapConfiguration
启动时拉取配置的原理。 - NacosConfigAutoConfiguration
更改配置时实现动态刷新的原理。
2.NacosConfigBootstrapConfiguration
public class NacosConfigBootstrapConfiguration {
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(
NacosConfigProperties nacosConfigProperties) {
// 构造器里面创建了NacosConfigService(下面会说到)
return new NacosConfigManager(nacosConfigProperties);
}
...
@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(
NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}
}
自动配置了一个 NacosPropertySourceLocator,PropertySourceLocator接口是实现配置文件加载的接口,
spring boot 在启动时读取配置文件时,会获取 PropertySourceLocator 的实现类进行循环调用,本文暂时不对这块进行解释。
最终会调用 locate()方法:PropertySource<?> locate(Environment environment);
public PropertySource<?> locate(Environment env) {
nacosConfigProperties.setEnvironment(env);
// 连接nacos server的客户端
ConfigService configService = nacosConfigManager.getConfigService();
String name = nacosConfigProperties.getName();
String dataIdPrefix = nacosConfigProperties.getPrefix();
// 名字为 nacos 的 PropertySource
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
// 1.先加载共享配置
loadSharedConfiguration(composite);
// 2.在加载扩展配置
loadExtConfiguration(composite);
// 3.加载指定|没指定profile的配置
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
上面就能很清楚的看到nacos的配置加载顺序了,根据配置覆盖原则,先加载的会被后加载的覆盖,外部配置的加载,不优先本地配置的加载,后加载的优先级会更高。
直接分析 loadApplicationConfiguration 的加载,因为其他的都是一样的方式。
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();
// load directly once by default
// 直接就是 服务名的:web-provider
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// load with suffix, which have a higher priority than the default
// 带上后缀的 web-provider.yaml
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// Loaded with profile, which have a higher priority than the suffix
// 带上profile+后缀的 web-provider-dev.yaml
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
loadNacosDataIfPresent():
private void loadNacosDataIfPresent(final CompositePropertySource composite,
final String dataId, final String group, String fileExtension,
boolean isRefreshable) {
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
fileExtension, isRefreshable);
// 从server端拉取成功后将配置信息放到最前面
this.addFirstPropertySource(composite, propertySource, false);
}
private NacosPropertySource loadNacosPropertySource(final String dataId,
final String group, String fileExtension, boolean isRefreshable) {
return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
isRefreshable);
}
NacosPropertySource build(String dataId, String group, String fileExtension,
boolean isRefreshable) {
// 这个地方开始向远端拉取配置
List<PropertySource<?>> propertySources = loadNacosData(dataId, group,
fileExtension);
NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
group, dataId, new Date(), isRefreshable);
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
loadNacosData(): 真正拉取配置的开始
private List<PropertySource<?>> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
data = configService.getConfig(dataId, group, timeout);
return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
fileExtension);
}
顺着调用一直往下:
com.alibaba.nacos.client.config.NacosConfigService#getConfig
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
// 1.优先使用本地配置
// 配置文件存在路径:System.getProperty("user.home") + File.separator + "nacos"+ File.separator + "config";
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
if (content != null) {
return content;
}
// 2.远程拉取
String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(ct[0]);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout){
HttpRestResult<String> result = null;
// 发起http请求 GET /v1/cs/configs
result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
}
从原理分析上应证了官网的描述。
从上面分析可以看出应用在启动时会进行远程配置的读取,Nacos在启动时读取配置的原则是:
1.先使用本地路径的
2.在远端拉取
然后把加载到的配置放到spring环境里面,上面就是启动时配置加载的大致过程。
下面来重点分析几个类:
NacosConfigService的实例化:
public NacosConfigService(Properties properties) throws NacosException {
initNamespace(properties);
this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
this.agent.start();
// 这个地方创建了一个ClientWorker类
this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
ClientWorker的实例化:
init(properties);
// 创建了一个线程池
this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
t.setDaemon(true);
return t;
}
});
// 执行线程任务
this.executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
// 调用了ClientWorker中的这个方法
checkConfigInfo();
} catch (Throwable e) {
LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
checkConfigInfo():执行长轮训任务,检查远端配置和本地配置的一致性,不一致就重新拉取远端配置到本地,并且回调监听方法。
public void checkConfigInfo() {
// Dispatch taskes.
int listenerSize = cacheMap.size();
// Round up the longingTaskCount.
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// The task list is no order.So it maybe has issues when changing.
// 线程池执行 LongPollingRunnable 线程任务类
// LongPollingRunnable就是长轮训,运用了servlet3的异步servlet技术(这个需要结合服务端源码一起分析才能看出来)
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
LongPollingRunnable长轮训线程任务类:
直接分析run方法:
public void run() {
// CacheData 里面有个 Listener 属性就是需要回调的监听方法
List<CacheData> cacheDatas = new ArrayList<CacheData>();
List<String> inInitializingCacheList = new ArrayList<String>();
try {
// check failover config
for (CacheData cacheData : cacheMap.values()) {
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
...
}
}
// 本地配置文件加密的md5等信息,发送给server端进行比较
// 如果md5不一样,这个地方只会返回需要拉取的配置信息
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
// 这个循环里面会进行配置重新拉取
for (String groupKey : changedGroupKeys) {
String[] key = GroupKey.parseKey(groupKey);
String dataId = key[0];
String group = key[1];
String tenant = null;
if (key.length == 3) {
tenant = key[2];
}
// 重新从server端拉取最新的配置
String[] ct = getServerConfig(dataId, group, tenant, 3000L);
CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
cache.setContent(ct[0]);
}
for (CacheData cacheData : cacheDatas) {
if (!cacheData.isInitializing() || inInitializingCacheList
.contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
// 这个地方回调Listener方法
cacheData.checkListenerMd5();
cacheData.setInitializing(false);
}
}
executorService.execute(this);
} catch (Throwable e) {
executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
}
}
先看checkUpdateDataIds(),这是个比较重要的方法:
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
Map<String, String> params = new HashMap<String, String>(2);
params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
Map<String, String> headers = new HashMap<String, String>(2);
headers.put("Long-Pulling-Timeout", "" + timeout);
try {
long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
// POST /v1/cs/configs/listener 这个请求是用来注册配置监听的,会返回有变化的配置文件的基本信息
// 在后面进行server端源码分析的时候会进行分析的
HttpRestResult<String> result = agent
.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(),readTimeoutMs);
if (result.ok()) {
setHealthServer(true);
// 解析结果
return parseUpdateDataIdResponse(result.getData());
}
return Collections.emptyList();
}
server端的 监听配置接口 运用了servlet3的异步servlet技术来达到长轮训。
下面只要拉取变化了的配置即可:
getServerConfig(xxx):
public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
throws NacosException {
HttpRestResult<String> result = null;
try {
Map<String, String> params = new HashMap<String, String>(3);
params.put("dataId", dataId);
params.put("group", group);
params.put("tenant", tenant);
// 拉取配置 GET /v1/cs/configs,和上面的获取配置一样
result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
} catch (Exception ex) {
throw new NacosException(NacosException.SERVER_ERROR, ex);
}
switch (result.getCode()) {
case HttpURLConnection.HTTP_OK:
// 保存快照到本地
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
ct[0] = result.getData();
if (result.getHeader().getValue(CONFIG_TYPE) != null) {
ct[1] = result.getHeader().getValue(CONFIG_TYPE);
} else {
ct[1] = ConfigType.TEXT.getType();
}
return ct;
.....
}
}
cacheData.checkListenerMd5();
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
// md5 不一样直接发布 配置更新事件
safeNotifyListener(dataId, group, content, type, md5, wrap);
}
}
}
private void safeNotifyListener(final String dataId, final String group, final String content,
final String md5, final ManagerListenerWrap listenerWrap) {
final Listener listener = listenerWrap.listener;
Runnable job = new Runnable() {
@Override
public void run() {
try {
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setGroup(group);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
String contentTmp = cr.getContent();
// 回调 Listener的 receiveConfigInfo
listener.receiveConfigInfo(contentTmp);
// compare lastContent and content
if (listener instanceof AbstractConfigChangeListener) {
Map data = ConfigChangeHandler.getInstance()
.parseChangeData(listenerWrap.lastContent, content, type);
ConfigChangeEvent event = new ConfigChangeEvent(data);
// AbstractConfigChangeListener.receiveConfigChange(event);
((AbstractConfigChangeListener) listener).receiveConfigChange(event);
listenerWrap.lastContent = content;
}
}
}
};
if (null != listener.getExecutor()) {
listener.getExecutor().execute(job);
} else {
job.run();
}
}
这个地方会回调listener的事件,bean的更新也是在这个地方触发的,下面的这个方法发布了RefreshEvent事件,
RefreshEventListener是监听类,它会刷新与配置有关的bean,达到动态配置更新。
NacosConfigAutoConfiguration中注册的bean:
@Bean
public NacosContextRefresher nacosContextRefresher(
NacosConfigManager nacosConfigManager,
NacosRefreshHistory nacosRefreshHistory) {
return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
}
NacosContextRefresher:bean刷新的自动配置类。
com.alibaba.cloud.nacos.refresh.NacosContextRefresher#registerNacosListener
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
// RefreshEvent
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
}
});
// 注册指定配置文件的监听
configService.addListener(dataKey, groupKey, listener);
}
从这里我们也可以看出,我们也可以自己实现listener接口,达到自定义监听指定配置文件的目的。
OK,客户端大致部分已经分析完毕。
Nacos 客户端配置更新感知完全都是通过定时任务来完成的,核心在于 ClientWork类, ClientWork内部使用线程池启动LongPollingRunnable线程任务。
LongPollingRunnable线程任务类:
- 1.向server端注册配置监听,server会返回变动了的配置信息。
- 2.拉取最新变动了的配置信息。
- 3.通过返回的md5和本地的md5进行比较,不一样发布配置更新事件,刷新本地容器中的bean。
以上就是client端的源码解析,涉及到server端请求的接口,下一篇来进行分析。