第一部分,刷新触发事件代码说明
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;
}