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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章