SpringBoot配置文件加载的路径和优先级

标签: springboot  

很多人对SpringBoot文件加载的位置和优先级有点疑惑,下面通过源码看下到底是如何加载的。

我们先来看下SpringBoot加载PropertySource的顺序是如何的呢?可以参考官网:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-external-config

配置加载的优先级从高到低,其中高优先级可以覆盖低优先级的配置。

简单的用中文翻译如下:

1. 开启 DevTools 时,~/.config/spring-boot/spring-boot-devtools.properties
2. 测试类上的 @TestPropertySource 注解 
3. @SpringBootTest#properties 属性
4. 命令⾏行行参数( --server.port=9000 ) 
5. SPRING_APPLICATION_JSON 中的属性
6. ServletConfig 初始化参数 
7. ServletContext 初始化参数
8. java:comp/env 中的 JNDI 属性 
9. System.getProperties()
10. 操作系统环境变量量
11. random.* 涉及到的 RandomValuePropertySource
12. jar 包外部的 application-{profile}.properties 或 .yml 
13. jar 包内部的 application-{profile}.properties 或 .yml 
14. jar 包外部的 application.properties 或 .yml
15. jar 包内部的 application.properties 或 .yml
16. @Configuration 类上的 @PropertySource 
17. SpringApplication.setDefaultProperties() 设置的默认属性

项目内部配置加载路径

–file:./config/
–file:./
–classpath:/config/
–classpath:/

加载顺序从高到低,高优先级的配置会覆盖低优先级配置内容。

其定义在文件ConfigFileApplicationListener

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
···
	// 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/";
		
···
}

PropertySourceLoader源码分析

SpringBoot把配置文件的加载封装成了PropertySourceLoader接口,该接口的定义如下:

public interface PropertySourceLoader {

	// 支持的文件后缀
	String[] getFileExtensions();

	// 把资源Resource加载成属性源PropertySource
	List<PropertySource<?>> load(String name, Resource resource) throws IOException;
}

PropertySource是Spring对name/value键值对的封装接口。该定义了getSource()方法,这个方法会返回得到属性源的源头。比如MapPropertySource的源头就是一个Map,PropertiesPropertySource的源头就是一个Properties。

PropertySource目前的实现类有不少,比如上面提到的MapPropertySource和PropertiesPropertySource,还有RandomValuePropertySource(source是Random)、SimpleCommandLinePropertySource(source是CommandLineArgs,命令行参数)、ServletConfigPropertySource(source是ServletConfig)等等。

PropertySource常用的子类有:

  • MapPropertySource:source指定为Map实例的PropertySource实现。
  • PropertiesPropertySource:source指定为Map实例的PropertySource实现,内部的Map实例由Properties实例转换而来。
  • ResourcePropertySource:继承自PropertiesPropertySource,source指定为通过Resource实例转化为Properties再转换为Map实例。
  • StubPropertySource:PropertySource的一个内部类,source设置为null,实际上就是空实现。
  • ComparisonPropertySource:继承自ComparisonPropertySource,所有属性访问方法强制抛出异常,作用就是一个不可访问属性的空实现。

AbstractEnvironment中的属性定义:

public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore";
public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";
public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default";
protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default";

private final Set<String> activeProfiles = new LinkedHashSet<>();

private final Set<String> defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles());

private final MutablePropertySources propertySources = new MutablePropertySources();

private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);

上面的propertySources(MutablePropertySources类型)属性就是用来存放PropertySource列表的,PropertySourcesPropertyResolverConfigurablePropertyResolver的实现,默认的profile就是字符串default。MutablePropertySources的内部属性如下:

private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();

PropertySourceLoader接口目前有两个实现类:

  1. PropertiesPropertySourceLoader(支持从xml或properties格式的文件中加载数据)
  2. YamlPropertySourceLoader(支持从yml或者yaml格式的文件中加载数据)

Environment源码分析

public interface Environment extends PropertyResolver {
	
	String[] getActiveProfiles();

	String[] getDefaultProfiles();
	
	@Deprecated
	boolean acceptsProfiles(String... profiles);

	boolean acceptsProfiles(Profiles profiles);

}

Environment接口是Spring对当前程序运行期间的环境的封装。主要提供了两大功能:profileproperty(父接口PropertyResolver提供)。

  • PropertyResolver:提供属性访问功能。
  • ConfigurablePropertyResolver:继承自PropertyResolver,主要提供属性类型转换(基于org.springframework.core.convert.ConversionService)功能。
  • Environment:继承自PropertyResolver,提供访问和判断profiles的功能。
  • ConfigurableEnvironment:继承自ConfigurablePropertyResolver和Environment,并且提供设置激活的profile和默认的profile的功能。
  • ConfigurableWebEnvironment:继承自ConfigurableEnvironment,并且提供配置Servlet上下文和Servlet参数的功能。
  • AbstractEnvironment:实现了ConfigurableEnvironment接口,默认属性和存储容器的定义,并且实现了ConfigurableEnvironment种的方法,并且为子类预留可覆盖了扩展方法。
  • StandardEnvironment:继承自AbstractEnvironment,非Servlet(Web)环境下的标准Environment实现。
  • StandardServletEnvironment:继承自StandardEnvironment,Servlet(Web)环境下的标准Environment实现。

Environment(主要是ConfigurableEnvironment)加载过程源码分析:

SpringApplication#prepareEnvironment

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		//创建ConfigurableEnvironment实例
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		//启动参数绑定到ConfigurableEnvironment中
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		ConfigurationPropertySources.attach(environment);
		//发布ConfigurableEnvironment准备完毕事件
		listeners.environmentPrepared(environment);
		//绑定ConfigurableEnvironment到当前的SpringApplication实例中
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		//绑定ConfigurationPropertySourcesPropertySource到ConfigurableEnvironment中,name为configurationProperties,实例是SpringConfigurationPropertySources,属性实际是ConfigurableEnvironment中的MutablePropertySources
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

看下getOrCreateEnvironment方法:

private ConfigurableEnvironment getOrCreateEnvironment() {
		if (this.environment != null) {
			return this.environment;
		}
		switch (this.webApplicationType) {
		case SERVLET:
			return new StandardServletEnvironment();
		case REACTIVE:
			return new StandardReactiveWebEnvironment();
		default:
			return new StandardEnvironment();
		}
	}

还有一个地方要重点关注:发布ConfigurableEnvironment准备完毕事件listeners.environmentPrepared(environment),实际上这里用到了同步的EventBus,事件的监听者是ConfigFileApplicationListener,具体处理逻辑是onApplicationEnvironmentPreparedEvent方法:

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

实际上,处理工作大部分都在ConfigFileApplicationListener中,见它的postProcessEnvironment方法:

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

	private void onApplicationPreparedEvent(ApplicationEvent event) {
		this.logger.switchTo(ConfigFileApplicationListener.class);
		addPostProcessors(((ApplicationPreparedEvent) event).getApplicationContext());
	}

	/**
	 * 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();
	}

主要的配置环境加载逻辑在内部类Loader,Loader会匹配多个路径下的文件把属性加载到ConfigurableEnvironment中,加载器主要是PropertySourceLoader的实例,例如我们用到application-${profile}.yaml文件做应用主配置文件,使用的是YamlPropertySourceLoader,这个时候activeProfiles也会被设置到ConfigurableEnvironment中。加载完毕之后,ConfigurableEnvironment中基本包含了所有需要加载的属性(activeProfiles是这个时候被写入ConfigurableEnvironment)。值得注意的是,几乎所有属性都是key-value形式存储,如xxx.yyyy.zzzzz=value、xxx.yyyy[0].zzzzz=value-1、xxx.yyyy[1].zzzzz=value-2。Loader中的逻辑相对复杂,有比较多的遍历和过滤条件,这里不做展开。

最后

Spring中的环境属性管理的源码个人认为是最清晰和简单的:从文件中读取数据转化为key-value结构,key-value结构存放在一个PropertySource实例中,然后得到的多个PropertySource实例存放在一个CopyOnWriteArrayList中,属性访问的时候总是遍历CopyOnWriteArrayList中的PropertySource进行匹配。可能相对复杂的就是占位符的解析和参数类型的转换

「真诚赞赏,手留余香」

请我喝杯咖啡?

使用微信扫描二维码完成支付

相关文章