很多人对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
列表的,PropertySourcesPropertyResolver
是ConfigurablePropertyResolver
的实现,默认的profile就是字符串default。MutablePropertySources
的内部属性如下:
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
PropertySourceLoader接口目前有两个实现类:
- PropertiesPropertySourceLoader(支持从xml或properties格式的文件中加载数据)
- YamlPropertySourceLoader(支持从yml或者yaml格式的文件中加载数据)
Environment源码分析
public interface Environment extends PropertyResolver {
String[] getActiveProfiles();
String[] getDefaultProfiles();
@Deprecated
boolean acceptsProfiles(String... profiles);
boolean acceptsProfiles(Profiles profiles);
}
Environment
接口是Spring对当前程序运行期间的环境的封装。主要提供了两大功能:profile
和property
(父接口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进行匹配。可能相对复杂的就是占位符的解析和参数类型的转换
「真诚赞赏,手留余香」
请我喝杯咖啡?
使用微信扫描二维码完成支付
