有用过Nacos小伙伴应该清楚,在Spring Cloud项目中,可以动态更新配置类的值,例如下面场景。
我们在使用 Spring Cloud Gateway时,向做一个开关,如果新项目上线凌晨上线,测试还在验收,需要做一套ip白名单,这样场景下,就可以利用配置中心,当验收通过后,再更新对应变量值,实现动态配置。
本文将从配置加载、配置动态刷新两个角度进行分析。
配置加载
上一篇文章,研究过Spring 启动时 prepareEnvironment
方法过程,其中最后留下的疑惑为,加载完bootstrap后,还会去加载application配置吗?答案却是会的。因为二者不冲突。bootstrap配置可以立刻被夹在,但是application配置则可能由于spring.config.name
名称被更改而不被加载。
从Nacos中,加载配置类的关键在 prepareContext
中
SpringApplication
的prepareContext
方法:
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
applyInitializers(context);
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
...
PropertySourceBootstrapConfiguration 作用
PropertySourceBootstrapConfiguration
是一个 ApplicationContextInitializer
类型,他在 prepareContext
时,会进行初始化,并执行 initialize
方法。
PropertySourceBootstrapConfiguration
的 initialize
方法。
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
CompositePropertySource composite = new CompositePropertySource(
BOOTSTRAP_PROPERTY_SOURCE_NAME);
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySourceLocator locator : this.propertySourceLocators) {
PropertySource<?> source = null;
source = locator.locate(environment); // 从 PropertySourceLocator 中,加载配置
if (source == null) {
continue;
}
logger.info("Located property source: " + source);
composite.addPropertySource(source); // 加入配置
empty = false;
}
if (!empty) { // 配置不为空,则加入到application中。
MutablePropertySources propertySources = environment.getPropertySources();
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
}
insertPropertySources(propertySources, composite); // 将composite配置加入到environment中
reinitializeLoggingSystem(environment, logConfig, logFile);
setLogLevels(applicationContext, environment); // 设置数据绑定日志
handleIncludedProfiles(environment); // 设置active profiles
}
}
在 上面代码中,有一段 从 propertySourceLocators
中获取PropertySource
,而后将拼凑完的propertySource
信息,放到environment
中。
并且他并不会管是否有相同actives.profile
被加载,同样会加载一份配置。通过 spring.cloud.config.allowOverride
判断是否需要覆盖对应的本地配置,默认是能覆盖。
PropertySourceLocator
PropertySourceLocator
主要用于为environment
加载配置,里面只有一个方法:
public interface PropertySourceLocator {
/**
* @param environment The current Environment.
* @return A PropertySource, or null if there is none.
* @throws IllegalStateException if there is a fail-fast condition.
*/
PropertySource<?> locate(Environment environment);
}
本文以Nacos配置为例,NacosPropertySourceLocator
:
@Override
public PropertySource<?> locate(Environment env) {
ConfigService configService = nacosConfigProperties.configServiceInstance();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
}
long timeout = nacosConfigProperties.getTimeout();
nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
timeout);
String name = nacosConfigProperties.getName();
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
loadSharedConfiguration(composite);
loadExtConfiguration(composite);
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
在执行 NacosPropertySourceLocator
的locate
方法,总体思路比较容易理解:
Nacos通过applicationName,profile名字,加上nameSpace去调用nacos服务端接口获取配置信息,再将获取的配置解析并构造一个 CompositePropertySource
并返回。
配置更新
用过Nacos的小伙伴应该清楚,当使用Nacos作为配置中心时,在线修改Nacos配置,当使用@Refresh
或者 @ConfigurationProperties
,往往可以动态刷新配置的值。
使用Nacos作为配置中心时,本地会启动一个client端,为 com.alibaba.nacos.client.config.impl.ClientWorker
。在其构造方法中,会启动多个定时任务线程池 ScheduledThreadPoolExecutor
。用于定时拉取nacos变更配置信息。
对于Nacos实时更新,Nacos目前版本(1.1.4) 是通过一个timeout为30秒的http请求,即30s请求一次,一次超时30秒,服务端不立刻返回,如果这段时间有配置变更则立刻返回http请求。
具体代码可由 ClientWorker
构造方法深入理解。
当有拉取完信息后,nacos会将本次和上次拉取的信息进行对比,如果有变更,则会将变更的信息发送。
CacheData
的 safeNotifyListener
方法:
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() {
ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader appClassLoader = listener.getClass().getClassLoader();
try {
if (listener instanceof AbstractSharedListener) {
AbstractSharedListener adapter = (AbstractSharedListener) listener;
adapter.fillContext(dataId, group);
LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
}
// 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
Thread.currentThread().setContextClassLoader(appClassLoader);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setGroup(group);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
String contentTmp = cr.getContent();
listener.receiveConfigInfo(contentTmp);
listenerWrap.lastCallMd5 = md5;
LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
listener);
} catch (NacosException de) {
LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}", name,
dataId, group, md5, listener, de.getErrCode(), de.getErrMsg());
} catch (Throwable t) {
LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId, group,
md5, listener, t.getCause());
} finally {
Thread.currentThread().setContextClassLoader(myClassLoader);
}
}
};
final long startNotify = System.currentTimeMillis();
try {
if (null != listener.getExecutor()) {
listener.getExecutor().execute(job);
} else {
job.run();
}
} catch (Throwable t) {
LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId, group,
md5, listener, t.getCause());
}
final long finishNotify = System.currentTimeMillis();
LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
name, (finishNotify - startNotify), dataId, group, md5, listener);
}
准备完成之后,通过 NacosContextRefresher
的registerNacosListener
发送Spring 事件出去。
NacosContextRefresher
的registerNacosListener
方法:
@Override
public void receiveConfigInfo(String configInfo) {
refreshCountIncrement();
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) {
log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
}
}
refreshHistory.add(dataId, md5);
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug("Refresh Nacos config group " + group + ",dataId" + dataId);
}
}
RefreshEvent
是Spring cloud提供的刷新事件,会引发 RefreshEndpoint.refresh()
方法调用。
最终到 ContextRefresher
时,会将所有变更的keys都从environment中获取,比较完后将keys再发送 EnvironmentChangeEvent
出去有Binder进行配置刷新。
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
该 EnvironmentChangeEvent
则是由 ConfigurationPropertiesRebinder
进行处理
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
rebind();
}
}
public void rebind() {
this.errors.clear();
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
具体的ConfigurationPropertiesRebinder
.rebind
操作:
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
if (this.applicationContext != null) {
try {
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {
this.applicationContext.getAutowireCapableBeanFactory()
.destroyBean(bean); // 销毁
this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name); // 初始化
return true;
}
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
catch (Exception e) {
this.errors.put(name, e);
throw new IllegalStateException("Cannot rebind to " + name, e);
}
}
return false;
}
具体处理逻辑,则是将所有配置bean中,符合要求的bean进行重新绑定一次,即destroyBean
再 initializeBean
一次,而对应配置则同初始化一样,从environment中获取,这样就完成了刷新。
以上则为Spring Cloud中配置动态刷新原理。
关注博主公众号: 六点A君。
哈哈哈,一起研究Spring: