关于Spring Cloud Consul配置实现的自动刷新功能解读

 

第一部分,刷新触发事件代码说明

 

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;
	}

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值