SpringCloud Nacos配置中心--客户端源码解析
1、 SpringCloud实现配置加载
SpringCLoud在启动时,会在PropertySourceBootstrapConfiguration.initialize()
方法中调用PropertySourceLocator.locate()
方法来获取远程配置信息。
PropertySourceLocator
接口的主要作用是实现应用外部化配置可动态加载
SpringBoot启动时,回在SpringApplication.run
方法中调用prepareEnvironment
方法进行环境准备工作
public ConfigurableApplicationContext run(String... args) {
...
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
...
}
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
...
// 这里发布了一个ApplicationEnvironmentPreparedEvent事件
listeners.environmentPrepared((ConfigurableEnvironment)environment);
...
return (ConfigurableEnvironment)environment;
}
BootstrapApplicationListener
监听器会去订阅这个事件。
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
...
if (context == null) {
context = bootstrapServiceContext(environment, event.getSpringApplication(),
configName);
event.getSpringApplication()
.addListeners(new CloseContextOnFailureApplicationListener(context));
}
...
}
private ConfigurableApplicationContext bootstrapServiceContext(
...
// 通过SpringBoot的自动装配机制,导入了BootstrapImportSelector.class
builder.sources(BootstrapImportSelectorConfiguration.class);
...
}
@Configuration(proxyBeanMethods = false)
@Import(BootstrapImportSelector.class)
public class BootstrapImportSelectorConfiguration {
}
public class BootstrapImportSelector implements EnvironmentAware, DeferredImportSelector {
public String[] selectImports(AnnotationMetadata annotationMetadata) {
...
// Use names and ensure unique to protect against duplicates
List<String> names = new ArrayList<>(SpringFactoriesLoader
.loadFactoryNames(BootstrapConfiguration.class, classLoader));
...
}
}
BootstrapApplicationListener
订阅到事件后,会通过SpringBoot的自动装配机制,引入BootstrapImportSelector
,从加载spring.facotories
文件上的BootstrapConfiguration
配置信息
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
通过SpringBoot的SPI机制,装载了NacosConfigBootstrapConfiguration
到Ioc容器中,之后便是熟悉的SpringBoot注入Bean的方式了,在这里会将NacosPropertySourceLocator
注入到Spring中。
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(
NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(
NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}
}
1)、NacosPropertySourceLocator
public PropertySource<?> locate(Environment env) {
nacosConfigProperties.setEnvironment(env);
// 创建Nacos的Java SDK,用于从Nacos服务器拉取配置
ConfigService configService = nacosConfigManager.getConfigService();
/*
...
*/
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
loadSharedConfiguration(composite);
loadExtConfiguration(composite);
// 加载应用程序的配置
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
loadApplicationConfiguration
方法最终会调用loadNacosData
方法从Nacos配置中心加载配置
private Map<String, Object> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
try {
// 从Nacos配置中心加载配置
data = configService.getConfig(dataId, group, timeout);
/*
...
*/
// 通过责任链模式,使用不同的解析器解析从Nacos加载的配置(properties、yaml、json、xml等)
Map<String, Object> dataMap = NacosDataParserHandler.getInstance()
.parseNacosData(data, fileExtension);
return dataMap == null ? EMPTY_MAP : dataMap;
}
catch (NacosException e) {
log.error("get data from Nacos error,dataId:{}, ", dataId, e);
}
catch (Exception e) {
log.error("parse data from Nacos error,dataId:{},data:{},", dataId, data, e);
}
return EMPTY_MAP;
}
2、 Nacos配置热刷新
Nacos会通过SpringBoot的SPI机制加载NacosConfigAutoConfiguration
,在该Bean中会注入NacosContextRefresher
监听器
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\
NacosContextRefresher
实现了ApplicationListener
,监听了ApplicationReadyEvent
事件,在SpringBoot环境加载完成后会触发此监听器,具体代码如下:
public class NacosContextRefresher
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
if (this.ready.compareAndSet(false, true)) {
// 注册nacos配置监听器
this.registerNacosListenersForApplications();
}
}
private void registerNacosListenersForApplications() {
...
registerNacosListener(propertySource.getGroup(), dataId);
...
}
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
// 生成一个Nacos的监听器并记录到listenerMap中,
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
// 发布配置刷新事件
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug(String.format(
"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
group, dataId, configInfo));
}
}
});
try {
// 将监听器添加到Nacos的客户端中,如果NacosServer的相关配置发生了变化,便会触发对应Id的监听器,发布一个Spring的配置刷新事件,从而刷新Spring的本地配置
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
log.warn(String.format(
"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
groupKey), e);
}
}
}
Nacos在感知到SpringBoot的环境配置加载完成后,会生成一个Nacos客户端的监听器,监听NacosServer的配置信息,如果有服务端出现了配置变化,则会触发Nacos的监听器发布一个Spring的RefreshEvent
事件来刷新SpringBoot的本地配置。
public class RefreshEventListener implements SmartApplicationListener {
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);
}
}
}
SpringBoot的RefreshEventListener
监听器监听到配置刷新事件后,会调用handle
方法,刷新本地的Bean信息,将加了@RefreshScope
注解的Bean给销毁掉。这样,在重新使用到对应Bean的时候,会使用新的配置信息来创建Bean。
3、Nacos获取配置客户端
上面提到Nacos Client端会在NacosPropertySourceLocator
中通过ConfigService
来获取Nacos Server端的配置,创建ConfigService
的路线如下:
nacosConfigManager.getConfigService();
--> NacosFactory.createConfigService
public class ConfigFactory {
public ConfigFactory() {
}
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 var4) {
throw new NacosException(-400, var4);
}
}
}
可以看到在NacosFactory
工厂类中,最终是通过反射机制创建了com.alibaba.nacos.client.config.NacosConfigService
public class NacosConfigService implements ConfigService {
public NacosConfigService(Properties properties) throws NacosException {
String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
if (StringUtils.isBlank(encodeTmp)) {
encode = Constants.ENCODE;
} else {
encode = encodeTmp.trim();
}
initNamespace(properties);
agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
agent.start();
worker = new ClientWorker(agent, configFilterChainManager, properties);
}
}
在NacosConfigService
的构造方法中,会初始化一个HttpAgent
,这里用了装饰器模式,实际使用的是ServerHttpAgent
;还初始化了一个ClientWork
,这是客户端的一个工作类;
ClientWork
的构造方法如下
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
// Initialize the timeout parameter
init(properties);
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;
}
});
executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
t.setDaemon(true);
return t;
}
});
executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
checkConfigInfo();
} catch (Throwable e) {
LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
在ClientWork
的构造方法中,创建了两个线程池,每10毫秒会执行checkConfigInfo
方法,检查Nacos配置
public void checkConfigInfo() {
// 分任务
int listenerSize = cacheMap.get().size();
// 向上取整为批数,计算需要启动的线程数,配置数量/3000
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// 要判断任务是否在执行 这块需要好好想想。 任务列表现在是无序的。变化过程可能有问题
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
在该方法中,会启动一个长轮询线程LongPollingRunnable
来监听服务端的配置,每个任务会处理3000个监听配置集,多于3000个配置,则会启动新的LongPollingRunnable
线程来执行监听。
class LongPollingRunnable implements Runnable {
private int taskId;
public LongPollingRunnable(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
List<CacheData> cacheDatas = new ArrayList<CacheData>();
List<String> inInitializingCacheList = new ArrayList<String>();
try {
// check failover config
for (CacheData cacheData : cacheMap.get().values()) {
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
try {
checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception e) {
LOGGER.error("get local config info error", e);
}
}
}
// check server config
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
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];
}
try {
String[] ct = getServerConfig(dataId, group, tenant, 3000L);
CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
cache.setContent(ct[0]);
if (null != ct[1]) {
cache.setType(ct[1]);
}
LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
agent.getName(), dataId, group, tenant, cache.getMd5(),
ContentUtils.truncateContent(ct[0]), ct[1]);
} catch (NacosException ioe) {
String message = String.format(
"[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
agent.getName(), dataId, group, tenant);
LOGGER.error(message, ioe);
}
}
for (CacheData cacheData : cacheDatas) {
if (!cacheData.isInitializing() || inInitializingCacheList
.contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
cacheData.checkListenerMd5();
cacheData.setInitializing(false);
}
}
inInitializingCacheList.clear();
executorService.execute(this);
} catch (Throwable e) {
// If the rotation training task is abnormal, the next execution time of the task will be punished
LOGGER.error("longPolling error : ", e);
executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
}
}
}
LongPollingRunnable
实现了Runnable
接口,所以启动线程时会执行run
方法,在run
方法中,会先遍历cacheMap
,筛选出当前长轮询线程负责监听的配置数据,并检查是否本地配置文件的数据是否有更新,如果有变更则直接触发事件通知。
接着会调用checkUpdateDataIds
方法,基于长连接的方式来监听服务端配置的变化,最后根据数据的key去服务端获取最新的数据,最后再重新调度一下this
,继续启动长轮询线程监听配置。
在checkUpdateDataIds
方法中,最终调用了checkUpdateConfigStr
方法。
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
List<String> params = new ArrayList<String>(2);
params.add(Constants.PROBE_MODIFY_REQUEST);
params.add(probeUpdateString);
List<String> headers = new ArrayList<String>(2);
headers.add("Long-Pulling-Timeout");
headers.add("" + timeout);
// told server do not hang me up if new initializing cacheData added in
if (isInitializingCacheList) {
headers.add("Long-Pulling-Timeout-No-Hangup");
headers.add("true");
}
if (StringUtils.isBlank(probeUpdateString)) {
return Collections.emptyList();
}
try {
// In order to prevent the server from handling the delay of the client's long task,
// increase the client's read timeout to avoid this problem.
// readTimeOutMs = 45000
long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
agent.getEncode(), readTimeoutMs);
if (HttpURLConnection.HTTP_OK == result.code) {
setHealthServer(true);
return parseUpdateDataIdResponse(result.content);
} else {
setHealthServer(false);
LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(), result.code);
}
} catch (IOException e) {
setHealthServer(false);
LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
throw e;
}
return Collections.emptyList();
}
可以看到checkUpdateConfigStr
方法最终是通过agent.httoPost
调用了/v1/cs/configs/listener
接口实现长轮询请求。而长轮询请求是在客户端设置了一个比较长的超时时间,默认45秒(老版本30秒)。如果服务端的数据发生了变更,客户端会收到一个HttpResult,服务端返回的是变更配置的DataId
,Group
,Tenant
。获得这些信息后,会在LongPollingRunnable
线程中调用getServerConfig
方法去Nacos服务端读取具体的配置内容。
public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
throws NacosException {
String[] ct = new String[2];
if (StringUtils.isBlank(group)) {
group = Constants.DEFAULT_GROUP;
}
HttpResult result = null;
try {
List<String> params = null;
if (StringUtils.isBlank(tenant)) {
params = new ArrayList<String>(Arrays.asList("dataId", dataId, "group", group));
} else {
params = new ArrayList<String>(Arrays.asList("dataId", dataId, "group", group, "tenant", tenant));
}
result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
} catch (IOException e) {
String message = String.format(
"[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s", agent.getName(),
dataId, group, tenant);
LOGGER.error(message, e);
throw new NacosException(NacosException.SERVER_ERROR, e);
}
switch (result.code) {
case HttpURLConnection.HTTP_OK:
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.content);
ct[0] = result.content;
if (result.headers.containsKey(CONFIG_TYPE)) {
ct[1] = result.headers.get(CONFIG_TYPE).get(0);
} else {
ct[1] = ConfigType.TEXT.getType();
}
return ct;
case HttpURLConnection.HTTP_NOT_FOUND:
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
return ct;
case HttpURLConnection.HTTP_CONFLICT: {
LOGGER.error(
"[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, "
+ "tenant={}", agent.getName(), dataId, group, tenant);
throw new NacosException(NacosException.CONFLICT,
"data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
}
case HttpURLConnection.HTTP_FORBIDDEN: {
LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", agent.getName(), dataId,
group, tenant);
throw new NacosException(result.code, result.content);
}
default: {
LOGGER.error("[{}] [sub-server-error] dataId={}, group={}, tenant={}, code={}", agent.getName(), dataId,
group, tenant, result.code);
throw new NacosException(result.code,
"http error, code=" + result.code + ",dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
}
}
}
可以看到,getServerConfig
最终是通过agent.httpGet
调用服务端的/v1/cs/configs
接口来获取服务端的配置数据。