在从服务器拉取配置时,Nacos客户端的NacosPropertySourceLocator类实现的locate()方法中会生成ConfigService实例。调用NacosConfigProperties类中的configServiceInstance()使用当前的属性返回ConfigService实例。
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
ConfigService vendorImpl = (ConfigService)constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(-400, e.getMessage());
}
}
可以看到先加载NacosConfigService类,再以反射创建实例对象。此时会创建ServerHttpAgent实例对象用于发送http请求,与服务器交互。再创建一个ClientWorker客户端,ClientWorker会创建10ms执行一次的调度任务checkConfigInfo()。checkConfigInfo()做的事件就是检查配置源的数量有没有增加,如果增加到一定数量就开辟线程进行处理。
public void checkConfigInfo() {
//获取所有的配置缓存信息
int listenerSize = cacheMap.get().size();
// 向上取整为批数,一个线程可以处理3000=ParamUtil.getPerTaskConfigSize()
int longingTaskCount = (int)Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
if (longingTaskCount > currentLongingTaskCount) {
//currentLongingTaskCount的默认值为0,每3000个就创建一个线程去轮询处理
for (int i = (int)currentLongingTaskCount; i < longingTaskCount; i++) {
// 要判断任务是否在执行 这块需要好好想想。 任务列表现在是无序的。变化过程可能有问题
//LongPullingRunnable的run()
executorService.execute(new LongPullingRunnable(i));
}
//当前配置源改变,cacheMap新增时,就在原有基础上开线程
currentLongingTaskCount = longingTaskCount;
}
}
在checkConfigInfo()中,先获取cacheMap的大小再创建任务,所以接下来就得说说cacheMap这个是怎么来的了。 回到NacosConfigAutoConfiguration自动加载配置类,这个类中创建了NacosContextRefresher的Bean。NacosContextRefresher实现了ApplicationListener接口,会监听ApplicationReadyEvent事件,同时实现了ApplicationContextAware接口,可以保存应用上下文。
public class NacosContextRefresher
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware
首先在prepareContext()方法加载spring上下文完成时,会为每个监听器设置应用上下文,此时NacosContextRefresher便可以保存应用上下文。
public void contextLoaded(ConfigurableApplicationContext context) {
for (ApplicationListener<?> listener : this.application.getListeners()) {
if (listener instanceof ApplicationContextAware) {
((ApplicationContextAware) listener).setApplicationContext(context);
}
context.addApplicationListener(listener);
}
this.initialMulticaster.multicastEvent(
new ApplicationPreparedEvent(this.application, this.args, context));
}
当应用完成启动时,就会发布ApplicationReadyEvent事件,NacosContextRefresher监听器就会处理这个事件,离cacheMap很近了。在处理事件时,先获取Nacos的所有属性源,再从属性源中获取group和dataId,然后向listenerMap注册监听器,key为dataId,value为监听器(lambda表达式),之后添加这个CacheData,到这里就已经到cacheMap了。
int taskId = cacheMap.get().size() / (int)ParamUtil.getPerTaskConfigSize();
cache.setTaskId(taskId);
new CacheData(configFilterChainManager, agent.getName(), dataId, group, tenant)
回到LongPullingRunnable的run()方法,可以看到先获取所有缓存的CacheData,每个CacheData中的taskId都是除3000得来的,而线程的taskId也是除3000得来的,所有每个线程可以处理3000个CacheData。
for (CacheData cacheData : cacheMap.get().values()) {
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
try {
checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception e) {
log.error("NACOS-CLIENT", "get local config info error", e);
}
}
}
优先从本地配置文件中获取配置,检查本地配置文件是否变动。注意这里的本地配置文件不是从服务器上下载的配置文件。
private void checkLocalConfig(CacheData cacheData) {
final String dataId = cacheData.dataId;
final String group = cacheData.group;
final String tenant = cacheData.tenant;
File path = LocalConfigInfoProcessor.getFailoverFile(agent.getName(), dataId, group, tenant);
// 没有 -> 有
//创建cacheData时没有使用本地配置,当获取到服务器的配置源之后,就会生成path
if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
String md5 = MD5.getInstance().getMD5String(content);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
log.warn(agent.getName(),
"[failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
dataId, group, tenant, md5, ContentUtils.truncateContent(content));
return;
}
// 有 -> 没有。不通知业务监听器,从server拿到配置后通知。
//如果path不存在了,说明不能使用本地的配置文件了,改成false
if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
cacheData.setUseLocalConfigInfo(false);
log.warn(agent.getName(), "[failover-change] failover file deleted. dataId={}, group={}, tenant={}", dataId,
group, tenant);
return;
}
// 有变更
//如果本地的配置文件发生变动,就从本地加载配置文件
if (cacheData.isUseLocalConfigInfo() && path.exists()
&& cacheData.getLocalConfigInfoVersion() != path.lastModified()) {
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
String md5 = MD5.getInstance().getMD5String(content);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
log.warn(agent.getName(),
"[failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
dataId, group, tenant, md5, ContentUtils.truncateContent(content));
return;
}
}
接下来到服务器中获取配置源。首先如果是第一次初始化需要到服务器获取一次配置源,将这些cacheData的参数缓存下来批量获取。向服务器发post请求,服务器返回了也能说明当前的服务器是健康正在运行的,将服务器返回的变化的dataId和group保存下来。然后再根据dataId、group、tenant到服务器获取配置源内容,将配置内容和内容的md5写到CacheData中。
for (String dataIdAndGroup : response.split(LINE_SEPARATOR)) {
if (!StringUtils.isBlank(dataIdAndGroup)) {
String[] keyArr = dataIdAndGroup.split(WORD_SEPARATOR);
String dataId = keyArr[0];
String group = keyArr[1];
if (keyArr.length == 2) {
updateList.add(GroupKey.getKey(dataId, group));
log.info(agent.getName(), "[polling-resp] config changed. dataId={}, group={}", dataId, group);
} else if (keyArr.length == 3) {
String tenant = keyArr[2];
updateList.add(GroupKey.getKeyTenant(dataId, group, tenant));
log.info(agent.getName(), "[polling-resp] config changed. dataId={}, group={}, tenant={}", dataId,
group, tenant);
} else {
log.error(agent.getName(), "NACOS-XXXX", "[polling-resp] invalid dataIdAndGroup error",
dataIdAndGroup);
}
}
}
如果检测到配置变化了cacheData.checkListenerMd5()则通知所有的监听器,调用每个监听器的receiveConfigInfo(),即lambda表达式。
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
safeNotifyListener(dataId, group, content, md5, wrap);
}
}
}
这时会调用lambda表达式处理配置文件的变化。
public void receiveConfigInfo(String configInfo) {
//设置加载次数,每次变化都会新增
loadCount.incrementAndGet();
String md5 = "";
if (!StringUtils.isEmpty(configInfo)) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
.toString(16);
}
catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
LOGGER.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
}
}
//记录当前dataId对应的配置源的改变
refreshHistory.add(dataId, md5);
//发布RefreshEvent事件
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Refresh Nacos config group{},dataId{}", group, dataId);
}
}
spring启动时会创建RefreshEventListener监听器,内部的handle方法标注了@EventListener,因此能够处理RefreshEvent刷新事件。
@EventListener
public void handle(RefreshEvent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
在refresh()方法中先获取老的属性,使用当前的环境environment和空的Empty.class运行spring应用上下文,再获取新的配置addConfigFilesToEnvironment()。然后将新的属性和老的属性进行比较,如果发现不一样就用新的属性替换老的属性。
ConfigurableApplicationContext addConfigFilesToEnvironment() {
ConfigurableApplicationContext capture = null;
try {
//复制当前环境
StandardEnvironment environment = copyEnvironment(
this.context.getEnvironment());
//使用当前环境和空类运行一个新的应用上下文
SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
.environment(environment);
// Just the listeners that affect the environment (e.g. excluding logging
// listener because it has side effects)
builder.application()
.setListeners(Arrays.asList(new BootstrapApplicationListener(),
new ConfigFileApplicationListener()));
//运行spring应用上下文
capture = builder.run();
if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
}
//获取当前应用上下文属性源
MutablePropertySources target = this.context.getEnvironment()
.getPropertySources();
String targetName = null;
//遍历新的应用上下文的属性源
for (PropertySource<?> source : environment.getPropertySources()) {
String name = source.getName();
if (target.contains(name)) {
targetName = name;
}
if (!this.standardSources.contains(name)) {
//如果原来就有这个属性源,那么就用新的属性源进行替换
if (target.contains(name)) {
target.replace(name, source);
}
else {
if (targetName != null) {
//如果原来没有这个属性源,将新得到的属性源加到原来的应用上下文
target.addAfter(targetName, source);
}
else {
// targetName was null so we are at the start of the list
target.addFirst(source);
targetName = name;
}
}
}
}
}
finally {
ConfigurableApplicationContext closeable = capture;
while (closeable != null) {
try {
closeable.close();
}
catch (Exception e) {
// Ignore;
}
if (closeable.getParent() instanceof ConfigurableApplicationContext) {
closeable = (ConfigurableApplicationContext) closeable.getParent();
}
else {
break;
}
}
}
return capture;
}
获取变化了的属性源的key集合,发布一个EnvironmentChangeEvent
事件,之后再this.scope.refreshAll()将配置刷到使用了@RefreshScope注解的Bean中。
private Map<String, Object> changes(Map<String, Object> before,
Map<String, Object> after) {
Map<String, Object> result = new HashMap<String, Object>();
for (String key : before.keySet()) {
if (!after.containsKey(key)) {
result.put(key, null);
}
else if (!equal(before.get(key), after.get(key))) {
result.put(key, after.get(key));
}
}
for (String key : after.keySet()) {
if (!before.containsKey(key)) {
result.put(key, after.get(key));
}
}
return result;
}
ConfigurationPropertiesRebinder监听器负责处理EnvironmentChangeEvent事件,在处理事件时会刷新所有的配置源Bean(@ConfigurationProperties注解Bean),刷新的方法是先销毁这些属性源Bean,再重新创建。之后再看@RefreshScope注解标注的Bean的属性怎么获取的新的配置。