Consul是一款中间件,可提供KV存储功能,同时提供了创建、修改、查询KV存储的HTTP API,因此可作为配置中心。
Spring Cloud Consul是基于Spring Cloud的公共接口,提供了集成Consul配置能力的微服务框架。
这个类中提供了一个定时任务
@Override
public void start() {
if (this.running.compareAndSet(false, true)) {
this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(
this::watchConfigKeyValues, this.properties.getWatch().getDelay());
}
}
该方法定时执行(getDelay():每当上一次执行完成后,经过固定延迟时间开始下一次执行)watchConfigKeyValues方法。
@Timed("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 (this.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 = this.properties.getAclToken();
if (StringUtils.isEmpty(aclToken)) {
aclToken = null;
}
// 调用consul的HTTP API,获取配置
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();
// currentIndex是缓存了KV配置的上一次更新的版本号,newIndex是当前版本号,若两者不等,则说明配置已更新,所以需要刷新
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);
// 通过Spring的事件机制触发配置动态刷新
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 (this.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());
}
}
}
}
this.firstTime = false;
}
重要的代码行我已经加了中文注释。主要就是调用Consul的HTTP API获取配置,同时通过版本号判断配置是否更新,如果更新则通过事件机制,发布RefreshEvent事件,触发本地配置刷新。
这个类实现了SmartApplicationListener接口,SmartApplicationListener接口继承了ApplicationListener接口,因此会消费RefreshEvent。
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
handle((ApplicationReadyEvent) event);
}
else if (event instanceof RefreshEvent) {
handle((RefreshEvent) event);
}
}
handle方法
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实例变量的refresh变量进行刷新。refresh的类型是ContextRefresher,它是Spring Cloud Commons定义的一个类。
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
refreshEnvironment方法
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;
}
addConfigFilesToEnvironment方法
ConfigurableApplicationContext addConfigFilesToEnvironment() {
ConfigurableApplicationContext capture = null;
try {
// 复制当前运行环境
StandardEnvironment environment = copyEnvironment(
this.context.getEnvironment());
// 构建一个新SpringApplicationBuilder
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()));
// 生成一个新的SpringApplication,通过Spring的生命周期去刷新配置,保存在本方法的environment临时变量中
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;
// 从environment中的PropertySource添加刷新后的配置
for (PropertySource<?> source : environment.getPropertySources()) {
String name = source.getName();
if (target.contains(name)) {
targetName = name;
}
// 如果不属于默认PropertySource才添加
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;
}
重要的代码加了注释,下面看capture = builder.run();这行代码,最终进入了SpringApplication的run方法
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
由此看出,确实是新建了一个SpringApplication,这个类的作用就是对Spring的ApplicationContext进行一系列启动前的配置。与动态刷新相关的是prepareContext(context, environment, listeners, applicationArguments, printedBanner);这一行
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
// 触发刷新配置相关的ApplicationContextInitializer
applyInitializers(context);
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
/**
* Apply any {@link ApplicationContextInitializer}s to the context before it is
* refreshed.
* @param context the configured ApplicationContext (not refreshed yet)
* @see ConfigurableApplicationContext#refresh()
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
initializer.initialize(context);
}
}
循环应用所有注册的ApplicationContextInitializer到ApplicationContext。其中有一个PropertySourceBootstrapConfiguration的ApplicationContextInitializer,它负责Spring Cloud的外部PropertySource加载。
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
List<PropertySource<?>> composite = new ArrayList<>();
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
// 遍历propertySourceLocators,获取PropertySourceLocator并应用
for (PropertySourceLocator locator : this.propertySourceLocators) {
Collection<PropertySource<?>> source = locator.locateCollection(environment);
if (source == null || source.size() == 0) {
continue;
}
List<PropertySource<?>> sourceList = new ArrayList<>();
for (PropertySource<?> p : source) {
sourceList.add(new BootstrapPropertySource<>(p));
}
logger.info("Located property source: " + sourceList);
composite.addAll(sourceList);
empty = false;
}
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
for (PropertySource<?> p : environment.getPropertySources()) {
if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.remove(p.getName());
}
}
insertPropertySources(propertySources, composite);
reinitializeLoggingSystem(environment, logConfig, logFile);
setLogLevels(applicationContext, environment);
handleIncludedProfiles(environment);
}
}
该类有一个PropertySourceLocator集合,其中有一个ConsulPropertySourceLocator。
它有一个locate方法
@Override
@Retryable(interceptor = "consulRetryInterceptor")
public PropertySource<?> locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
String appName = this.properties.getName();
if (appName == null) {
appName = env.getProperty("spring.application.name");
}
List<String> profiles = Arrays.asList(env.getActiveProfiles());
String prefix = this.properties.getPrefix();
List<String> suffixes = new ArrayList<>();
if (this.properties.getFormat() != FILES) {
suffixes.add("/");
}
else {
suffixes.add(".yml");
suffixes.add(".yaml");
suffixes.add(".properties");
}
// 获取默认context(consul的概念)
String defaultContext = getContext(prefix,
this.properties.getDefaultContext());
for (String suffix : suffixes) {
this.contexts.add(defaultContext + suffix);
}
for (String suffix : suffixes) {
addProfiles(this.contexts, defaultContext, profiles, suffix);
}
String baseContext = getContext(prefix, appName);
for (String suffix : suffixes) {
this.contexts.add(baseContext + suffix);
}
for (String suffix : suffixes) {
addProfiles(this.contexts, baseContext, profiles, suffix);
}
Collections.reverse(this.contexts);
CompositePropertySource composite = new CompositePropertySource("consul");
for (String propertySourceContext : this.contexts) {
try {
ConsulPropertySource propertySource = null;
// 若Consul配置类型为FILES
if (this.properties.getFormat() == FILES) {
Response<GetValue> response = this.consul.getKVValue(
propertySourceContext, this.properties.getAclToken());
addIndex(propertySourceContext, response.getConsulIndex());
if (response.getValue() != null) {
ConsulFilesPropertySource filesPropertySource = new ConsulFilesPropertySource(
propertySourceContext, this.consul, this.properties);
filesPropertySource.init(response.getValue());
propertySource = filesPropertySource;
}
}
// 若Consul配置类型非FILES
else {
propertySource = create(propertySourceContext, this.contextIndex);
}
if (propertySource != null) {
composite.addPropertySource(propertySource);
}
}
catch (Exception e) {
if (this.properties.isFailFast()) {
log.error(
"Fail fast is set and there was an error reading configuration from consul.");
ReflectionUtils.rethrowRuntimeException(e);
}
else {
log.warn("Unable to load consul config from "
+ propertySourceContext, e);
}
}
}
return composite;
}
return null;
}
这里才是真正再次去consul获取最新的配置,并进行替换的地方。常用的是非FILES类型,所以看一下注释的create方法
private ConsulPropertySource create(String context, Map<String, Long> contextIndex) {
ConsulPropertySource propertySource = new ConsulPropertySource(context,
this.consul, this.properties);
propertySource.init();
addIndex(context, propertySource.getInitialIndex());
return propertySource;
}
ConsulPropertySource是Spring Cloud Consul定义的PropertySource。
public void init() {
if (!this.context.endsWith("/")) {
this.context = this.context + "/";
}
// 调用consul的HTTP API,获取配置
Response<List<GetValue>> response = this.source.getKVValues(this.context,
this.configProperties.getAclToken(), QueryParams.DEFAULT);
this.initialIndex = response.getConsulIndex();
final List<GetValue> values = response.getValue();
ConsulConfigProperties.Format format = this.configProperties.getFormat();
// 根据配置类型(key-value,PROPERTIES,YAML)进行解析
switch (format) {
case KEY_VALUE:
parsePropertiesInKeyValueFormat(values);
break;
case PROPERTIES:
case YAML:
parsePropertiesWithNonKeyValueFormat(values, format);
}
}
到此,就将consul上配置的最新值解析到了应用本地。分析就到这里了,后面的就是将这个ConsulPropertySource实例应用到本地的ApplicationContext中。
Spring Cloud Consul的KV配置动态刷新,主要是依赖了Consul提供的HTTP API,在应用端定时去读取Consul的配置,并根据版本号(Index)判断配置是否更新,如果更新再通过Spring的事件机制发布一个事件。Spring消费该事件时,会通过新建一个SpringApplication的方式,去刷新配置。