第一部分,刷新觸發事件代碼說明
Spring Cloud Consul配置的自動刷新功能是通過
org.springframework.cloud.consul.config.ConfigWatch進行實現,ConfigWatch初始化後,會調用定時器,跟服務器上面的配置文件的版本進行比較,如果版本不一致,則調用Spring 的刷新事件,觸發事件刷新,否則代表配置沒有變化。
具體代碼說明:
org.springframework.cloud.consul.config.ConfigWatch
public class ConfigWatch implements ApplicationEventPublisherAware, SmartLifecycle {
private static final Log log = LogFactory.getLog(ConfigWatch.class);
private final ConsulConfigProperties properties;
private final ConsulClient consul;
private LinkedHashMap<String, Long> consulIndexes;
private final TaskScheduler taskScheduler;
private final AtomicBoolean running = new AtomicBoolean(false);
private ApplicationEventPublisher publisher;
private boolean firstTime = true;
private ScheduledFuture<?> watchFuture;
public ConfigWatch(ConsulConfigProperties properties, ConsulClient consul, LinkedHashMap<String, Long> initialIndexes) {
this(properties, consul, initialIndexes, getTaskScheduler());
}
//初始化定時器
private static ThreadPoolTaskScheduler getTaskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.initialize();
return taskScheduler;
}
public ConfigWatch(ConsulConfigProperties properties, ConsulClient consul, LinkedHashMap<String, Long> initialIndexes,
TaskScheduler taskScheduler) {
this.properties = properties;
this.consul = consul;
this.consulIndexes = new LinkedHashMap<>(initialIndexes);
this.taskScheduler = taskScheduler;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
//啓動事件,進行定時器的啓動,用於進行配置文件版本的比較
@Override
public void start() {
if (this.running.compareAndSet(false, true)) {
this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(this::watchConfigKeyValues,
this.properties.getWatch().getDelay());
}
}
定時器代碼,用於刷新配置,如果檢測到配置的版本變化,則調用Spring的刷新事件,進行本地配置的刷新處理
@Timed(value ="consul.watch-config-keys")
public void watchConfigKeyValues() {
if (this.running.get()) {
for (String context : this.consulIndexes.keySet()) {
// turn the context into a Consul folder path (unless our config format are FILES)
if (properties.getFormat() != FILES && !context.endsWith("/")) {
context = context + "/";
}
try {
Long currentIndex = this.consulIndexes.get(context);
if (currentIndex == null) {
currentIndex = -1L;
}
log.trace("watching consul for context '"+context+"' with index "+ currentIndex);
// use the consul ACL token if found
String aclToken = properties.getAclToken();
if (StringUtils.isEmpty(aclToken)) {
aclToken = null;
}
Response<List<GetValue>> response = this.consul.getKVValues(context, aclToken,
new QueryParams(this.properties.getWatch().getWaitTime(),
currentIndex));
// if response.value == null, response was a 404, otherwise it was a 200
// reducing churn if there wasn't anything
if (response.getValue() != null && !response.getValue().isEmpty()) {
Long newIndex = response.getConsulIndex();
if (newIndex != null && !newIndex.equals(currentIndex)) {
// don't publish the same index again, don't publish the first time (-1) so index can be primed
if (!this.consulIndexes.containsValue(newIndex) && !currentIndex.equals(-1L)) {
log.trace("Context "+context + " has new index " + newIndex);
RefreshEventData data = new RefreshEventData(context, currentIndex, newIndex);
this.publisher.publishEvent(new RefreshEvent(this, data, data.toString()));
} else if (log.isTraceEnabled()) {
log.trace("Event for index already published for context "+context);
}
this.consulIndexes.put(context, newIndex);
} else if (log.isTraceEnabled()) {
log.trace("Same index for context "+context);
}
} else if (log.isTraceEnabled()) {
log.trace("No value for context "+context);
}
} catch (Exception e) {
// only fail fast on the initial query, otherwise just log the error
if (firstTime && this.properties.isFailFast()) {
log.error("Fail fast is set and there was an error reading configuration from consul.");
ReflectionUtils.rethrowRuntimeException(e);
} else if (log.isTraceEnabled()) {
log.trace("Error querying consul Key/Values for context '" + context + "'", e);
} else if (log.isWarnEnabled()) {
// simplified one line log message in the event of an agent failure
log.warn("Error querying consul Key/Values for context '" + context + "'. Message: " + e.getMessage());
}
}
}
}
firstTime = false;
}
其中
Response<List<GetValue>> response = this.consul.getKVValues(context, aclToken, new QueryParams(this.properties.getWatch().getWaitTime(), currentIndex));
得到的對像爲Consul配置中心的配置文件對像,一般爲查找本身及本身目錄下面的data信息,如:/config/application及/config/application/data或者對應的應用名稱對應的值,前綴爲ConsulProperties對應的key,即配置中心對應的文件名稱
如下圖所示:
對於每一個應用會請求當前應用,當前應用對應的Active Profile,及Application跟Application ActiveProfile對應的配置,請求地址如下:
http://127.0.0.1:8500/v1/kv/config/mtenant-service/?recurse
如果在本地已經緩存,則會附帶上版本號如下所示:
http://127.0.0.1:8500/v1/kv/config/mtenant-service/?recurse&wait=55s&index=316209
返回的結果如下所示:
[{"LockIndex":0,"Key":"config/mtenant-service/data","Flags":0,"Value":"c2VydmVyLnBvcnQ9ODA4NAoKI+Wkmuenn+aIt+mFjee9rgpzeXMubXRlbmFudC51c2U9Y2Q3NmE0NjM4MGNhNTNiOTVhYWViNzI5NGU0MmZiNmEKCiNzd2FnZ2Vy5byA5YWzIHRydWU95byA5ZCvIGZhbHNlPeWFs+mXrQpzd2FnZ2VyLmVuYWJsZT10cnVlCgoj5pWw5o2u5bqT6buY6K6k6YWN572uIApkZHMuZ2VuZXJhbC5kZWZhdWx0U2NoZW1hPW11bHRpdGVuYW50CmRkcy5nZW5lcmFsLmZpbHRlclVybHM9L2Rkcy1zYW1wbGUvYWN0dWF0b3IvaGVhbHRoLC9zd2FnZ2VyLC93ZWJqYXJzLC90b2tlbiwvc3BpZGVyCgojZW1t5L2/55So57yT5a2Y57G75Z6LIGVoY2FjaGUgb3IgcmVkaXMKY2FjaGUudHlwZT1yZWRpcwoKI3JlZGlz6L+e5o6l5rGg6YWN572uCnJlZGlzLnBvb2wubWF4SWRsZT0zMDAKcmVkaXMucG9vbC5tYXhBY3RpdmU9NjAwCnJlZGlzLnBvb2wubWF4V2FpdD0xMDAwMApyZWRpcy5wb29sLnRlc3RPbkJvcnJvdz10cnVlCgojZnJlZW1ha2VyCnNwcmluZy5mcmVlbWFya2VyLmNoYXJzZXQ9VVRGLTgKc3ByaW5nLmZyZWVtYXJrZXIuY29udGVudC10eXBlPXRleHQvaHRtbApzcHJpbmcuZnJlZW1hcmtlci5zdWZmaXg9LmZ0bApzcHJpbmcuZnJlZW1hcmtlci50ZW1wbGF0ZS1sb2FkZXItcGF0aD1jbGFzc3BhdGg6L3RlbXBsYXRlcy8Kc3ByaW5nLmZyZWVtYXJrZXIuc2V0dGluZ3MuZGVmYXVsdF9lbmNvZGluZz1VVEYtOAoKCiNsaWNlbnNlIGNvbmZpZyBpbmZvIApzcWxfY3JlYXRlX3RlbmFudF9kYj1zaCAvb3B0L2VtbS9tdGVuYW50LXNlcnZpY2UvZGJvcC5zaCAtY3JlYXRlIC0tbWRtX2RiX2hvc3Q9e21kbV9kYl9ob3N0fSAtLW1kbV9kYl91c2VyPXttZG1fZGJfdXNlcn0gLS1tZG1fZGJfcGFzc3dvcmQ9e21kbV9kYl9wYXNzd29yZH0gLS10ZW5hbnRfZGJuYW1lPXt0ZW5hbnRfZGJuYW1lfSAtLW1kbV9wYXNzd29yZD17bWRtX3Bhc3N3b3JkfQoKCg==","CreateIndex":315874,"ModifyIndex":316209}]
第二部分刷新業務邏輯說明
Spring Application中對應的上下文org.springframework.context.support.AbstractApplicationContext中通過觸發refresh事件,調用相關應用上下文的刷新處理,refresh方法中,會調用prepareReresh()方法,在prepareRefresh()方法中,會調用初始化initPropertySource()方法,該方法會進行配置類的初始化
/**
* Prepare this context for refreshing, setting its startup date and
* active flag as well as performing any initialization of property sources.
*/
protected void prepareRefresh() {
// Switch to active.
this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true);
if (logger.isInfoEnabled()) {
logger.info("Refreshing " + this);
}
// Initialize any placeholder property sources in the context environment.
initPropertySources();
.....
/**
* {@inheritDoc}
* <p>Replace {@code Servlet}-related property sources.
*/
@Override
protected void initPropertySources() {
ConfigurableEnvironment env = getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(this.servletContext, this.servletConfig);
}
}
Spring Cloud 的配置文件是通過org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration進行加載的,所以該類會在Application的刷新及加載事件進行重新初始化,即調用初始化代碼,該類的聲明如下:
@Configuration
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
@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);
if (source == null) {
continue;
}
logger.info("Located property source: " + source);
composite.addPropertySource(source);
empty = false;
}
if (!empty) {
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);
reinitializeLoggingSystem(environment, logConfig, logFile);
setLogLevels(applicationContext, environment);
handleIncludedProfiles(environment);
}
}
該類的初始化時,會自動調用org.springframework.cloud.consul.config.ConsulPropertySourceLocator的locate進行配置文件的重新加載
該類的聲明如下:
@Order(0)
public class ConsulPropertySourceLocator implements PropertySourceLocator {
ConsulPropertySourceLocator的定義位於org.springframework.cloud.consul.config.ConsulConfigBootstrapConfiguration中,代碼如下所示:
@Configuration
@ConditionalOnConsulEnabled
public class ConsulConfigBootstrapConfiguration {
@Configuration
@EnableConfigurationProperties
@Import(ConsulAutoConfiguration.class)
@ConditionalOnProperty(name = "spring.cloud.consul.config.enabled", matchIfMissing = true)
protected static class ConsulPropertySourceConfiguration {
@Autowired
private ConsulClient consul;
@Bean
public ConsulConfigProperties consulConfigProperties() {
return new ConsulConfigProperties();
}
@Bean
public ConsulPropertySourceLocator consulPropertySourceLocator(
ConsulConfigProperties consulConfigProperties) {
return new ConsulPropertySourceLocator(consul, consulConfigProperties);
}
}
}
其中org.springframework.cloud.context.refresh.ContextRefresher實現了配置到Environment的刷新處理,具體實現如下:
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
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(context, keys));
return keys;
}
/* for testing */ 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()));
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;
}