Spring Cloud 配置加载(二)—— 不同位置配置文件加载顺序

2,551 阅读7分钟

    这篇文章主要说一说上一篇文章留下的问题 如果某个配置项存在于多个配置位置(环境变量、系统属性、命令行参数、内部配置文件,外部配置文件),那么会取哪一个配置中的值作为最终值呢?     阅读了一遍源代码,得出了如下一张类图,整个配置文件加载过程涉及到的类均在途中列举出来

简要描述一下整个流程。

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
		ApplicationArguments applicationArguments) {
	// Create and configure the environment
	//1、创建environment
	ConfigurableEnvironment environment = getOrCreateEnvironment();
	//2、配置envirnoment
	configureEnvironment(environment, applicationArguments.getSourceArgs());
	ConfigurationPropertySources.attach(environment);
	//3、监听器准备
	listeners.environmentPrepared(environment);
	bindToSpringApplication(environment);
	if (!this.isCustomEnvironment) {
		environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
				deduceEnvironmentClass());
	}
	ConfigurationPropertySources.attach(environment);
	return environment;
}

1 创建并配置加载environment

1.1 getOrCreateEnvironment().

实现environment创建,加载环境变量、系统属性,profile、命令行参数等

/**
*创建
*/
private ConfigurableEnvironment getOrCreateEnvironment() {
	if (this.environment != null) {
		return this.environment;
	}
	switch (this.webApplicationType) {
	case SERVLET: //servlet 模式,我们一般用的是这种模式
		return new StandardServletEnvironment();
	case REACTIVE: //reactive 模式
		return new StandardReactiveWebEnvironment();
	default:
		return new StandardEnvironment();
	}
}
	
/**
* 配置
*/
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
	if (this.addConversionService) {
		ConversionService conversionService = ApplicationConversionService.getSharedInstance();
		environment.setConversionService((ConfigurableConversionService) conversionService);
	}
	//配置propertysource
	configurePropertySources(environment, args);
	//配置profile
	configureProfiles(environment, args);
}

1.2 环境变量,系统属性加载 (StandardServletEnvironment的构造)

protected void customizePropertySources(MutablePropertySources propertySources) {
	propertySources.addLast(new StubPropertySource("servletConfigInitParams"));
	propertySources.addLast(new StubPropertySource("servletContextInitParams"));
	if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
		propertySources.addLast(new JndiPropertySource("jndiProperties"));
	}

	super.customizePropertySources(propertySources);
}

//父类(StandardEnvironment )的实现
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
	propertySources.addLast(
			new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
	propertySources.addLast(
			new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}

从上面的代码可以看出构建的servlet environment加载了 ServletConfigInitParams ServletContentInitParams(暂时为空) SystemProperties SystemEnvironment

1.3 配置 PropertySource

 /**
 * Add, remove or re-order any {@link PropertySource}s in this application's
 * environment.  添加、移除、重新排序 PropertySource
 * @param environment this application's environment
 * @param args arguments passed to the {@code run} method
 * @see #configureEnvironment(ConfigurableEnvironment, String[])
 */
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
	MutablePropertySources sources = environment.getPropertySources();
	if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
		sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
	}
	
	//如果需要加载命令行参数(该值默认为true)且传入了参数,执行如下代码, 
	if (this.addCommandLineProperties && args.length > 0) {
		String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
		if (sources.contains(name)) {
			//合并命令行参数。
			PropertySource<?> source = sources.get(name);
			CompositePropertySource composite = new CompositePropertySource(name);
			composite.addPropertySource(
					new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
			composite.addPropertySource(source);
			sources.replace(name, composite);
		}
		else {
			//特别注意加载在第一位
			sources.addFirst(new SimpleCommandLinePropertySource(args));
		}
	}
}   

到了这里,如果传入有参数,则此时顺序应该是 commandLineArgs, ServletConfigInitParams ServletContentInitParams(暂时为空) SystemProperties SystemEnvironment

1.4 加载 active profile

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
	environment.getActiveProfiles(); // ensure they are initialized
	// But these ones should go first (last wins in a property key clash)
	Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
	profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
	environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}

2 广播 ApplicationEnvironmentPreparedEvent

environmentPrepared(). 采用观察者模式,发布Event,通知listeners

3 具体通知方法委托SimpleApplicationEventMulticaster执行

@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
	ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
	Executor executor = getTaskExecutor();
	//getApplicationListener为父类中的方法
	for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
		if (executor != null) {
		//线程池执行
			executor.execute(() -> invokeListener(listener, event));
		}
		else {
			invokeListener(listener, event);
		}
	}
}

//父类方法获取listener
//AbstractApplicationEventMulticaster.getApplicationListeners()
protected Collection<ApplicationListener<?>> getApplicationListeners(
		ApplicationEvent event, ResolvableType eventType) {

	//省略一些代码
	
	if (this.beanClassLoader == null ||
			(ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
					(sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
		// Fully synchronized building and caching of a ListenerRetriever
		synchronized (this.retrievalMutex) {
			retriever = this.retrieverCache.get(cacheKey);
			if (retriever != null) {
				return retriever.getApplicationListeners();
			}
			retriever = new ListenerRetriever(true);
			Collection<ApplicationListener<?>> listeners =
			
			//真正的去获取ApplicationListener
			
			retrieveApplicationListeners(eventType, sourceType, retriever);
			this.retrieverCache.put(cacheKey, retriever);
			return listeners;
		}
	}
	else {
		// No ListenerRetriever caching -> no synchronization necessary
		return retrieveApplicationListeners(eventType, sourceType, null);
	}
}

private Collection<ApplicationListener<?>> retrieveApplicationListeners(){
    //省略一些代码  
    synchronized (this.retrievalMutex) {
         //最重要的一段代码, listener来自于这里
	     listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
	     listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
    }
}

//往defaultRetriever.applicatioinListeners中添加listener, 但是这个方法何时在哪里调用?
@Override
public void addApplicationListener(ApplicationListener<?> listener) {
	synchronized (this.retrievalMutex) {
		// Explicitly remove target for a proxy, if registered already,
		// in order to avoid double invocations of the same listener.
		Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
		if (singletonTarget instanceof ApplicationListener) {
			this.defaultRetriever.applicationListeners.remove(singletonTarget);
		}
		this.defaultRetriever.applicationListeners.add(listener);
		this.retrieverCache.clear();
	}
}

这里需要主要的是,要是一直从文章开始看代码,可能并不知道需要用到的listener从哪里来,我也调式了几遍源代码才发现,其实是在加载prepareEnvironment之前就已经加载。

3.1 获取applicationlistener,从spring.factories文件中读取。

//加载listener在这里,而我们一开始分析的入口在下面
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
	ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
	//我们一开分析的入口在这里
	ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
	

private SpringApplicationRunListeners getRunListeners(String[] args) {
	Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
	return new SpringApplicationRunListeners(logger,
			getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

//SpringApplicationRunListeners的构造
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
	this.log = log;
	this.listeners = new ArrayList<>(listeners);
}

/**
* 从spring.factories加载listener 
*/
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
	ClassLoader classLoader = getClassLoader();
	// Use names and ensure unique to protect against duplicates
	//先加载出名称
	Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
	//构造
	List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
	AnnotationAwareOrderComparator.sort(instances);
	return instances;
}

//加载名称
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
	String factoryClassName = factoryClass.getName();
	return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

//加载SpringFactory,从spring.factories文件中读取
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
	MultiValueMap<String, String> result = cache.get(classLoader);
	if (result != null) {
		return result;
	}

        // FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
	try {
		Enumeration<URL> urls = (classLoader != null ?
				classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
				ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
		result = new LinkedMultiValueMap<>();
		while (urls.hasMoreElements()) {
			URL url = urls.nextElement();
			UrlResource resource = new UrlResource(url);
			Properties properties = PropertiesLoaderUtils.loadProperties(resource);
			for (Map.Entry<?, ?> entry : properties.entrySet()) {
				String factoryClassName = ((String) entry.getKey()).trim();
				for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
					result.add(factoryClassName, factoryName.trim());
				}
			}
		}
		cache.put(classLoader, result);
		return result;
	}
	catch (IOException ex) {
		throw new IllegalArgumentException("Unable to load factories from location [" +
				FACTORIES_RESOURCE_LOCATION + "]", ex);
	}
}

而其中有一个listener名叫 EventPublishingRunListener,他所实现的功能就是注册listener

public EventPublishingRunListener(SpringApplication application, String[] args) {
	this.application = application;
	this.args = args;
	this.initialMulticaster = new SimpleApplicationEventMulticaster();
	for (ApplicationListener<?> listener : application.getListeners()) {
		this.initialMulticaster.addApplicationListener(listener);
	}
}

// AbstractApplicationEventMulticaster中的方法
@Override
public void addApplicationListener(ApplicationListener<?> listener) {
	synchronized (this.retrievalMutex) {
		// Explicitly remove target for a proxy, if registered already,
		// in order to avoid double invocations of the same listener.
		Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
		if (singletonTarget instanceof ApplicationListener) {
			this.defaultRetriever.applicationListeners.remove(singletonTarget);
		}
		this.defaultRetriever.applicationListeners.add(listener);
		this.retrieverCache.clear();
	}
}

实际上在更早之前,SpringApplication的构造,上面这代码就有调用到,多数的listener都是在那个时候加载

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	this.resourceLoader = resourceLoader;
	Assert.notNull(primarySources, "PrimarySources must not be null");
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
	setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
	this.mainApplicationClass = deduceMainApplicationClass();
}

3.2 几个相关的Listener

主要讲几个和Spring cloud关系比较的listener,会涉及到Spring Cloud的启动

3.2.1 BootstrapApplicationListener

实现BootstrapContext构造初始化,加载spring cloud特定的类。默认从bootstrap中读取spring cloud相关配置

public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
	ConfigurableEnvironment environment = event.getEnvironment();
	if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
			true)) {
		return;
	}
	//如果spring cloud已经初始化则不做处理
	// don't listen to events in a bootstrap context 
	if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
		return;
	}
	ConfigurableApplicationContext context = null;
	//spring cloud配置文件读取,默认从bootstrap中获取
	String configName = environment
			.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
	for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
			.getInitializers()) {
		if (initializer instanceof ParentContextApplicationContextInitializer) {
			context = findBootstrapContext(
					(ParentContextApplicationContextInitializer) initializer,
					configName);
		}
	}
	if (context == null) {
	    //构造bootstrap context
		context = bootstrapServiceContext(environment, event.getSpringApplication(),
				configName);
		event.getSpringApplication()
				.addListeners(new CloseContextOnFailureApplicationListener(context));
	}

	apply(context, event.getSpringApplication(), environment);
}
3.2.1.1 构造spring cloud bootstrap

可以通过spring.cloud.bootstrap.location(环境变量、系统属性、命令行参数)来指定配置文件位置。

private ConfigurableApplicationContext bootstrapServiceContext(
		ConfigurableEnvironment environment, final SpringApplication application,
		String configName) {
	StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
	MutablePropertySources bootstrapProperties = bootstrapEnvironment
			.getPropertySources();
	for (PropertySource<?> source : bootstrapProperties) {
		bootstrapProperties.remove(source.getName());
	}
	String configLocation = environment
			.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
	Map<String, Object> bootstrapMap = new HashMap<>();
	bootstrapMap.put("spring.config.name", configName);
	// if an app (or test) uses spring.main.web-application-type=reactive, bootstrap
	// will fail
	// force the environment to use none, because if though it is set below in the
	// builder
	// the environment overrides it
	bootstrapMap.put("spring.main.web-application-type", "none");
	if (StringUtils.hasText(configLocation)) {
	//如果通过spring.cloud.bootstrap.location指定了配置文件路径
		bootstrapMap.put("spring.config.location", configLocation);
	}
	
	//将spring boot配置放在第一位
	bootstrapProperties.addFirst(
			new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
	for (PropertySource<?> source : environment.getPropertySources()) {
		if (source instanceof StubPropertySource) {
			continue;//(servlet相关的属于这里,会被排除)
		}
		//将其他类型的Property在bootstrap最后,意味着命令行参数都要排在bootstrap之后
		bootstrapProperties.addLast(source);
	}
	// TODO: is it possible or sensible to share a ResourceLoader?
	SpringApplicationBuilder builder = new SpringApplicationBuilder()
			.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
			.environment(bootstrapEnvironment)
			// Don't use the default properties in this builder
			.registerShutdownHook(false).logStartupInfo(false)
			.web(WebApplicationType.NONE);
	final SpringApplication builderApplication = builder.application();
	if (builderApplication.getMainApplicationClass() == null) {
		// gh_425:
		// SpringApplication cannot deduce the MainApplicationClass here
		// if it is booted from SpringBootServletInitializer due to the
		// absense of the "main" method in stackTraces.
		// But luckily this method's second parameter "application" here
		// carries the real MainApplicationClass which has been explicitly
		// set by SpringBootServletInitializer itself already.
		builder.main(application.getMainApplicationClass());
	}
	if (environment.getPropertySources().contains("refreshArgs")) {
		// If we are doing a context refresh, really we only want to refresh the
		// Environment, and there are some toxic listeners (like the
		// LoggingApplicationListener) that affect global static state, so we need a
		// way to switch those off.
		builderApplication
				.setListeners(filterListeners(builderApplication.getListeners()));
	}
	builder.sources(BootstrapImportSelectorConfiguration.class);
	//开始运行,将会加载上一篇文章讲到的外部的配置
	final ConfigurableApplicationContext context = builder.run(); 
	// gh-214 using spring.application.name=bootstrap to set the context id via
	// `ContextIdApplicationContextInitializer` prevents apps from getting the actual
	// spring.application.name
	// during the bootstrap phase.
	context.setId("bootstrap");
	// Make the bootstrap context a parent of the app context
	addAncestorInitializer(application, context);
	
	//移除bootstrap中配置的参数
	// It only has properties in it now that we don't want in the parent so remove
	// it (and it will be added back later)
	bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
	//合并默认参数
	mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
	return context;
}

3.2.2 ConfigFileApplicationListener (加载spring cloud专用的属性)

负责读取配置文件(bootstrap),如果指定了spring.config.location,则读取spring.config.location位置的配置文件,否则读取默认位置的配置文件。默认位置按照下图倒序读取。

3.3 prepareContext()

执行过程中将会调用到前一篇文章说到的prepareContext(),加载自定义的外部配置路径,比如zookeeper上

3.4 其他操作

完成bootstrap context构造

4 继续执行外部context操作。

ConfigFileApplicationListener 加载application文件

5 总结

全部看下来,确实有点搞晕了,需要慢慢的消化消化。
最终效果,大致是这样在 Srping Boot环境中

  1. 命令行参数(main方法中的args接收到的值)
  2. ServletConfigInitParam
  3. ServeltContextInitParam
  4. SystemProperties
  5. SystemEnvironment
  6. appliction
  7. bootstrap(zookeeper) (在Spring Cloud环境中则是 bootstrap中的内容排在第一,然后才是system properties等)


    需要注意的是,实际上运行的时候,bootstrap(zookeekper)配置的内容会被首先使用,因为会先初始化spring cloud环境,初始化完成过后,会将这个配置文件中的内容追加到外部spring boot environment中
而bootstrap和application文件查找的中的优先级则为 file./config file./ classpath:config classpath:.

示例:

  • resources 目录下有application.yml(或application.properties) 中配置server.port=5555
  • resources 目录下有config子目录,其目录下有application.yml(或application.properties) 中配置server.port=4444
  • resources 目录下有bootstrap.yml(或bootstrap.properties) 中配置server.port=7777
  • resources 目录下有config子目录,其目录下有bootstrap.yml(或bootstrap.properties) 中配置server.port=6666
  • 执行命令 java -jar -Dserver.port=3333 test.jar --server.port=2222

要想使用数值比较大的端口号,则需要去掉比起端口小的配置。