Spring Cloud Consul外部配置动态刷新源码解析

Consul

Consul是一款中间件,可提供KV存储功能,同时提供了创建、修改、查询KV存储的HTTP API,因此可作为配置中心。

Spring Cloud Consul

Spring Cloud Consul是基于Spring Cloud的公共接口,提供了集成Consul配置能力的微服务框架。

源码分析

ConfigWatch

这个类中提供了一个定时任务

    @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事件,触发本地配置刷新。

RefreshEventListener

这个类实现了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定义的一个类。

ContextRefresher

    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);这一行

prepareContext方法

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

applyInitializers方法

    /**
	 * 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加载。

PropertySourceBootstrapConfiguration

initialize方法

    @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。

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方法

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。

init方法

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的方式,去刷新配置。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页