SpringCloud 源码分析 - application.properties 优先级高于 bootstrap.properties

版本信息

  • SpringCloud: Greenwich.RC2
  • SpringBoot:2.1.2.RELEASE

阅读要求

  • 需要对 SpringBoot 的启动流程有一定了解
  • 了解 ApplicationContextInitializer
  • 了解 SpringApplicationRunListenerApplicationListener 之间的关系

Tips

  • 代码中是用 PropertySource 的子类 OriginTrackedMapPropertySource 封装配置文件中的内容的
 public abstract class PropertySource<T> {

		protected final String name;  // application.properties 或 bootstrap.properties

	 protected final T source;  // 配置文件中的内容
public final class OriginTrackedMapPropertySource extends MapPropertySource
		implements OriginLookup<String> {

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public OriginTrackedMapPropertySource(String name, Map source) {
		super(name, source);
	}
  • PropertySource 还可以封装其他来源的配置,如封装程序命令行启动参数配置,封装 远程请求的配置,也可以编写代码自定义 PropertySource 添加配置和指定优先级
  • 本文不去分析 PropertySource 如何封装配置文件的内容,有兴趣可以在 OriginTrackedMapPropertySource 的构造函数中打断点,在 IDEA 中回溯栈帧,其大致流程:
→ SpringApplication#prepareEnvironment() 准备环境
→ SpringApplicationRunListeners#environmentPrepared() 环境准备完毕,发布 ApplicationEnvironmentPreparedEvent 事件
→ ConfigFileApplicationListener#onApplicationEvent() 监听事件
→ ConfigFileApplicationListener#postProcessEnvironment() 
→ ConfigFileApplicationListener#addPropertySources() 
→ ConfigFileApplicationListener.Loader#load()... 这里各种 load..() ,不回溯栈帧还真不好分析
→ ConfigFileApplicationListener.Loader#loadDocuments()
→ PropertiesPropertySourceLoader#load()    PropertySourceLoader 的实现类
										   方法最后会将 配置文件名 和 下面返回的 Map 封装到 OriginTrackedMapPropertySource 对象中
→ PropertiesPropertySourceLoader#loadProperties()
→ OriginTrackedPropertiesLoader#load()    将配置文件中的 K-V 添加到一个 Map<String, OriginTrackedValue> 集合中并返回

文章目标

  • 通过分析源码,了解 application.properties 优先级高于 bootstrap.properties 的原理
  • 学习如何使用 ApplicationContextInitializerApplicationListener

测试示例

application.properties

hello=application
...

bootstrap.properties

hello=bootstrap
...

测试结果:真正起作用的是 application.properties 中配置的值。
SpringCloud 源码分析 - application.properties 优先级高于 bootstrap.properties_第1张图片

源码分析

  • 思路
  1. 因为之前了解过 ConfigFileApplicationListener 是用来处理配置文件的(看名字也能看出来),跟踪其源码,最终是通过其内部类 ConfigFileApplicationListener.Loader#addLoadedPropertySource() 向环境中添加封装了 application.propertiesPropertySource
public class ConfigFileApplicationListener
		implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {

	private static final String DEFAULT_PROPERTIES = "defaultProperties";

	// Note the order is from least to most specific (last one wins)
	private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

	private static final String DEFAULT_NAMES = "application";

	...

	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
======>		onApplicationEnvironmentPreparedEvent(
					(ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}

	private void onApplicationEnvironmentPreparedEvent(
			ApplicationEnvironmentPreparedEvent event) {
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		postProcessors.add(this);
		AnnotationAwareOrderComparator.sort(postProcessors);
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
======>		postProcessor.postProcessEnvironment(event.getEnvironment(),
					event.getSpringApplication());
		}
	}

	...

	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment,
			SpringApplication application) {
======>	addPropertySources(environment, application.getResourceLoader());
	}

	...

	/**
	 * Add config file property sources to the specified environment.
	 * @param environment the environment to add source to
	 * @param resourceLoader the resource loader
	 * @see #addPostProcessors(ConfigurableApplicationContext)
	 */
	protected void addPropertySources(ConfigurableEnvironment environment,
			ResourceLoader resourceLoader) {
		RandomValuePropertySource.addToEnvironment(environment);
======>	new Loader(environment, resourceLoader).load();
	}

	...

	/**
	 * Loads candidate property sources and configures the active profiles.
	 */
	private class Loader {

		...

		public void load() {
			this.profiles = new LinkedList<>();
			this.processedProfiles = new LinkedList<>();
			this.activatedProfiles = false;
			this.loaded = new LinkedHashMap<>();
			initializeProfiles();
			while (!this.profiles.isEmpty()) {
				Profile profile = this.profiles.poll();
				if (profile != null && !profile.isDefaultProfile()) {
					addProfileToEnvironment(profile.getName());
				}
				load(profile, this::getPositiveProfileFilter,
						addToLoaded(MutablePropertySources::addLast, false));
				this.processedProfiles.add(profile);
			}
			resetEnvironmentProfiles(this.processedProfiles);
			load(null, this::getNegativeProfileFilter,
					addToLoaded(MutablePropertySources::addFirst, true));
======>		addLoadedPropertySources();
		}

		...

		private void addLoadedPropertySources() {
			MutablePropertySources destination = this.environment.getPropertySources();
			List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
			Collections.reverse(loaded);
			String lastAdded = null;
			Set<String> added = new HashSet<>();
			for (MutablePropertySources sources : loaded) {
				for (PropertySource<?> source : sources) {
					if (added.add(source.getName())) {
======>					addLoadedPropertySource(destination, lastAdded, source);
						lastAdded = source.getName();
					}
				}
			}
		}

		private void addLoadedPropertySource(MutablePropertySources destination,
				String lastAdded, PropertySource<?> source) {
			if (lastAdded == null) {
				if (destination.contains(DEFAULT_PROPERTIES)) {
					// 将封装了 application.properties 的 PropertySource 添加到 "defaultProperties" 之前
======>				destination.addBefore(DEFAULT_PROPERTIES, source);
				}
				else {
					destination.addLast(source);
				}
			}
			else {
				destination.addAfter(lastAdded, source);
			}
		}

	}
	...
}
  1. SpringCloud 文档中提示我们可以自定义实现 ApplicationContextInitializer 接口,通过重写其 initialize(ConfigurableApplicationContext) 方法,以硬代码的方式添加 PropertySource,既然这样,那 SpringCloud 内部是不是也是这样把封装了 bootstrap.propertiesPropertySource 添加到容器的 ConfigurableEnvironment 中的呢?本着这个猜想,找到了BootstrapApplicationListener.AncestorInitializer 类:
public class BootstrapApplicationListener
		implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {

	...

	private static class AncestorInitializer implements
			ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
		
		...
			
		@Override
		public void initialize(ConfigurableApplicationContext context) {
			while (context.getParent() != null && context.getParent() != context) {
				context = (ConfigurableApplicationContext) context.getParent();
			}
======>		reorderSources(context.getEnvironment());
			new ParentContextApplicationContextInitializer(this.parent)
					.initialize(context);
		}
	
		private void reorderSources(ConfigurableEnvironment environment) {
			// 从 SpringBoot 上下文的环境变量中移除键为 "defaultProperties" 的 ExtendeDefaultPropertySource
			PropertySource<?> removed = environment.getPropertySources()
					.remove(DEFAULT_PROPERTIES);
			if (removed instanceof ExtendedDefaultPropertySource) {
				ExtendedDefaultPropertySource defaultProperties = (ExtendedDefaultPropertySource) removed;
				// 向 SpringBoot 上下文的环境变量中添加键为 "defaultProperties" 值为 LinkedHashMap (size=0,其实就是一个空的 Map) 的 PropertySource
				environment.getPropertySources().addLast(new MapPropertySource(
						DEFAULT_PROPERTIES, defaultProperties.getSource()));
				// 遍历刚才移除的 ExtendedDefaultPropertySource(该对象内部包含封装了 bootstrap.properties 的 OriginTrackedMapPropertySource)
				for (PropertySource<?> source : defaultProperties.getPropertySources()
						.getPropertySources()) {
					if (!environment.getPropertySources().contains(source.getName())) {
						// 将封装了 bootstrap.properties 的 PropertySource 添加到 "defaultProperties" 之前
======>					environment.getPropertySources().addBefore(DEFAULT_PROPERTIES,
								source);
					}
				}
			}
		}
	
	}
}

有人会疑问:既然都是加到 defaultProperties之前,那到底谁在前谁在后呢?
这就需要大家了解 SpringBoot 启动流程(具体流程不再详述):
SpringApplication#run()

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);
			// 准备容器环境
			// 这一步会将封装 application.properties 的 PropertySource 对象添加到环境变量中
======>		ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			// 准备容器
			// 这一步会将封装 bootstrap.properties 的 PropertySource 对象添加到环境变量中
======>		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#prepareEnvironment(...) 准备环境

	private ConfigurableEnvironment prepareEnvironment(
			SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		// 调用所有 SpringApplicationRunListener 的 environmentPrepared() 方法
		// 使用 SimpleApplicationEventMulticaster 对象发布 ApplicationEnvironmentPreparedEvent事件
		// 通知所有监听此事件的 ApplicationListener ,包括 BootstrapApplicationListener 和 ConfigFileApplicationListener
======>	listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader())
					.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

SpringApplication#prepareContext(...) 准备容器

	private void prepareContext(ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
		context.setEnvironment(environment);
		postProcessApplicationContext(context);
		// 调用当前 SpringApplication 对象中所有 ApplicationContextInitializer 的
		// initialize 方法,包括 BootstrapApplicationListener.AncestorInitializer
======>	applyInitializers(context);
		...
	}

分析流程得出:

  1. 先将封装了 application.propertiesPropertySource 添加到键为 "defaultProperties" 的前面;
  2. 再将封装了 bootstrap.propertiesPropertySource 添加到键为 "defaultProperties" 的前面;
  3. 需要注意的是:两次操作时,"defaultProperties"对应的 PropertySource 不一样,前者是 ExtendedDefaultPropertySource,后者是 MapPropertySource,但这不影响结果的顺序;
  4. 最终顺序如 测试示例 所见。

注意事项

  1. 在 SpringCloud 启动流程结束前,会向 SpringBoot 上下文的环境变量中添加一个键为 defaultProperties 值为 ExtendedDefaultPropertySource 的对象,而 ExtendedDefaultPropertySource 对象内包含着封装了 bootstrap.propertiesOriginTrackedMapPropertySource,分析源代码可以看到,最终会把OriginTrackedMapPropertySourceExtendedDefaultPropertySource 中取出,然后直接添加到 SpringBoot 上下文的环境变量中
  2. 在 SpringCloud 启动流程完毕后,封装 application.propertiesbootstrap.propertiesPropertySource 才最终被添加到容器环境中,也就是说是在 SpringBoot 启动流程中完成添加的。间接说明了在 application.properties 中的配置项不会影响 SpringCloud 的启动流程,如在 application.properties 中配置的 spring.cloud.bootstrap.enabled=false 不会起作用,根本原因是 BootstrapApplicationListener 的优先级高于 ConfigFileApplicationListener
  3. BootstrapApplicationListener.AncestorInitializer 类并没有被配置在 META-INF/spring.factories 文件中,它是在 SpringCloud 启动流程中被保存到 SpringBoot 的 SpringApplication 对象中的;
  4. xxEnvironment 内部使用 MutablePropertySources 保存 PropertySource,本文分析时把中间环节 environment.getPropertySources() 省略了。

结束语

本来想着分析的细一点儿,但分着分着几乎要把 SpringBoot 和 SpringCloud 两大启动流程分析个遍,碍于时间精力和对源码的了解程度,遂只介绍了一些思路和关键点,如有不当之处,还请指点一二。

你可能感兴趣的:(SpringCloud 源码分析 - application.properties 优先级高于 bootstrap.properties)