《Spring Boot源码博客》
spring 外部化配置官方文档 https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
Spring Boot允许外部化你的配置,这样你就可以在不同的环境中使用相同的应用程序代码,你可以使用properties文件、YAML文件、环境变量和命令行参数来外部化配置,属性值可以通过使用@Value注解直接注入到你的bean中,通过Spring的Environment抽象访问,或者通过@ConfigurationProperties绑定到结构化对象。
Spring Boot使用一种非常特殊的PropertySource命令,该命令旨在允许对值进行合理的覆盖,属性按以下顺序考虑:
1、Devtools全局设置属性在你的主目录(~/.spring-boot-devtools.properties当devtools处于激活状态时)。
2、测试中的@TestPropertySource注解
3、测试中的@SpringBootTestproperties注解属性
4、命令行参数
5、来自SPRING_APPLICATION_JSON(嵌入在环境变量或系统属性中的内联JSON)的属性
6、ServletConfig初始化参数
7、ServletContext初始化参数
8、java:comp/env中的JNDI属性
9、Java系统属性(System.getProperties())
10、操作系统环境变量
11、一个只有random.*属性的RandomValuePropertySource
12、在你的jar包之外的 特殊配置文件的 应用程序属性(application-{profile}.properties和YAML 变体)
13、打包在jar中的 特殊配置文件的 应用程序属性(application-{profile}.properties和YAML 变体)
14、在你的jar包之外的应用程序属性(application.properties和YAML 变体)
15、打包在jar中的应用程序属性(application.properties和YAML 变体)
16、@PropertySource注解在你的@Configuration类上,对yaml文件无效
17、默认属性(通过设置SpringApplication.setDefaultProperties指定)
下面实践这些参数配置
新建一个MyApplicationRunner类输出test.property属性
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Value("${test.property}")
String testProperty;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("###输出test.property:"+testProperty);
}
}
17、默认属性(通过设置SpringApplication.setDefaultProperties指定)。
在启动类中添加设置默认属性。
public static void main(String[] args) {
SpringApplication app = new SpringApplication(SpringBootDemoApplication.class);
Properties properties = new Properties();
properties.setProperty("test.property", "17、默认属性(通过设置SpringApplication.setDefaultProperties指定)");
app.setDefaultProperties(properties);
app.run(args);
}
启动工程,控制台输出 ###输出test.property:17、默认属性(通过设置SpringApplication.setDefaultProperties指定)
16、@PropertySource注解在你的@Configuration类上,对yaml文件无效。
启动类包含了@Configuration注解,可以在启动类上使用@PropertySource。
1、新建application-property-source.properties文件,配置test.property属性
test.property=16、@PropertySource注解在你的@Configuration类上,对yaml文件无效
2、启动类上配置 @PropertySource("classpath:application-property-source.properties")
@PropertySource("classpath:application-property-source.properties")
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(SpringBootDemoApplication.class);
Properties properties = new Properties();
properties.setProperty("test.property", "17、默认属性(通过设置SpringApplication.setDefaultProperties指定)");
app.setDefaultProperties(properties);
app.run(args);
}
}
启动工程,控制台输出 ###输出test.property:16、@PropertySource注解在你的@Configuration类上,对yaml文件无效
15、打包在jar中的应用程序属性(application.properties和YAML 变体)
在application.properties文件配置:test.property=15、打包在jar中的应用程序属性(application.properties和YAML 变体)
启动工程,控制台输出 ###输出test.property:15、打包在jar中的应用程序属性(application.properties和YAML 变体)
14、在你的jar包之外的应用程序属性(application.properties和YAML 变体)
1、新建目录 C:\Users\Administrator\Desktop\my-location,即在桌面新建目录my-location,my-location目录添加一个application.properties。填写配置
test.property=14、在你的jar包之外的应用程序属性(application.properties和YAML 变体)
2、添加程序参数 --spring.config.additional-location=C:\Users\Administrator\Desktop\my-location\
使用spring.config.additional-location配置,除了默认位置外,还搜索额外的位置。
启动工程,控制台输出 ###输出test.property:14、在你的jar包之外的应用程序属性(application.properties和YAML 变体)
13、打包在jar中的 特殊配置文件的 应用程序属性(application-{profile}.properties和YAML 变体)
1、新建 application-dev.properties 文件,配置 test.property=13、打包在jar中的 特殊配置文件的 应用程序属性(application-{profile}.properties和YAML 变体)
2、添加程序参数 --spring.profiles.active=dev
启动工程,控制台输出 ###输出test.property:13、打包在jar中的 特殊配置文件的 应用程序属性(application-{profile}.properties和YAML 变体)
12、在你的jar包之外的 特殊配置文件的 应用程序属性(application-{profile}.properties和YAML 变体)
在C:\Users\Administrator\Desktop\my-location 中新增一个文件 application-dev.properties,添加配置 test.property=12、在你的jar包之外的 特殊配置文件的 应用程序属性(application-{profile}.properties和YAML 变体)
启动工程,控制台输出 ###输出test.property:12、在你的jar包之外的 特殊配置文件的 应用程序属性(application-{profile}.properties和YAML 变体)
11、一个只有random.*属性的RandomValuePropertySource
用于生成随机数,在application.properties文件中配置:test.property=${random.int[20,30]} 。结果不会覆盖12的配置。
10、操作系统环境变量
使用idea配置操作系统环境变量,Environment variables 中添加 test.property=10、操作系统环境变量
启动工程,控制台输出 ###输出test.property:10、操作系统环境变量
9、Java系统属性(System.getProperties())
启动类中设置java系统属性 System.setProperty("test.property", "9、Java系统属性(System.getProperties())");
@PropertySource("classpath:application-property-source.properties")
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
System.setProperty("test.property", "9、Java系统属性(System.getProperties())");
SpringApplication app = new SpringApplication(SpringBootDemoApplication.class);
Properties properties = new Properties();
properties.setProperty("test.property", "17、默认属性(通过设置SpringApplication.setDefaultProperties指定)");
app.setDefaultProperties(properties);
app.run(args);
}
}
启动工程,控制台输出 ###输出test.property:9、Java系统属性(System.getProperties())
第8、7、6不讲了,我没去实践。
5、来自SPRING_APPLICATION_JSON(嵌入在环境变量或系统属性中的内联JSON)的属性
Environment variables 中添加 SPRING_APPLICATION_JSON={"test.property":"5、来自SPRING_APPLICATION_JSON(嵌入在环境变量或系统属性中的内联JSON)的属性"}
启动工程,控制台输出 ###输出test.property:###输出test.property:5、来自SPRING_APPLICATION_JSON(嵌入在环境变量或系统属性中的内联JSON)的属性
4、命令行参数
idea的Program Arguments 添加 --test.property=4、命令行参数
启动工程,控制台输出 ###输出test.property:4、命令行参数
3、测试中的@SpringBootTestproperties注解属性
在测试类上添加 @SpringBootTest(properties = {"test.property=3、测试中的@SpringBootTestproperties注解属性"})
@SpringBootTest(properties = {"test.property=3、测试中的@SpringBootTestproperties注解属性"})
class SpringBootDemoApplicationTests {
@Test
void contextLoads() {
}
}
运行测试类
启动工程,控制台输出 ###输出test.property:3、测试中的@SpringBootTestproperties注解属性
2、测试中的@TestPropertySource注解
1、新建application-test-property-source.properties,添加配置
test.property=2、测试中的@TestPropertySource注解
2、测试类加上@SpringBootTest、@TestPropertySource({"classpath:application-test-property-source.properties"}) 注解
@SpringBootTest
@TestPropertySource({"classpath:application-test-property-source.properties"})
class SpringBootDemoApplicationTests {
@Test
void contextLoads() {
}
}
启动工程,控制台输出 ###输出test.property:2、测试中的@TestPropertySource注解
1、Devtools全局设置属性在你的主目录(~/.spring-boot-devtools.properties当devtools处于激活状态时)。
这个我整不出来。
外部化配置源码
1、debug到SpringApplication#run(java.lang.String...)方法中,有下面这句代码
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); 这是配置环境变量的方法。
1.1、prepareEnvironment(listeners, applicationArguments) 解析。
// 源码位置 org.springframework.boot.SpringApplication.prepareEnvironment
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
/**
* 创建Environment对象,本教程使用的是servlet环境,创建的是StandardServletEnvironment对象
*/
ConfigurableEnvironment environment = getOrCreateEnvironment();
/**
* 将命令行参数属性添加到StandardServletEnvironment对象中
*/
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
/**
* 运行Environment准备完毕事件
*/
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
1.1.1、ConfigurableEnvironment environment = getOrCreateEnvironment(); 解析
environment 对象如下图所示:
propertySources和propertyResolver属性在AbstractEnvironment类中定义如下:
private final MutablePropertySources propertySources = new MutablePropertySources(this.logger);
// propertyResolver也存储了propertySources的引用
private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);
propertySources的propertySourceList是一个CopyOnwriteArrayList写时复制列表。
在MutablePropertySources中,propertySourceList定义如下:
List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
PropertySource是对键值对的抽象, 他的source属性用于存储键值对属性。
propertyResolver用于解析propertySources。
1.1.2、执行完 configureEnvironment(environment, applicationArguments.getSourceArgs()); 这行代码后,再看下 environment 会发生哪些变化。
最明显的变化是propertySourceList多了两个元素,其中SimpleCommandLinePropertySource的source承载了命令行参数属性。而MapPropertySource的source属性承载了properties.setProperty(key, value)设置的默认属性。
使用Environment的getProperty(String key)方法时,通过propertyResolver去遍历propertySourceList,找到key对应的值就把值返回。由于SimpleCommandLinePropertySource排在MapPropertySource的前面,getProperty("test.property")就会返回命令行中配置的值“4、命令行参数”。获取属性值的源码分析后面会讲。
1.1.3、listeners.environmentPrepared(environment); 解析
listeners是SpringApplicationRunListeners对象,是SpringApplicationRunListener的合集。环境变量的处理会使用到SpringApplication运行时监听器,关于监听器的内容可以查看 spring boot 2源码系列(二)- 监听器ApplicationListener 这篇博客。
EventPublishingRunListener是SpringApplicationRunListener的默认实现,会广播ApplicationEnvironmentPreparedEvent事件。源码位置 EventPublishingRunListener#environmentPrepared(ConfigurableEnvironment environment)
1.1.3.1、ApplicationEnvironmentPreparedEvent被 ConfigFileApplicationListener监听到
// 源码位置 org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
/**
* getApplicationListeners(event, type)
* 获取监听了 ApplicationEnvironmentPreparedEvent 事件的监听器,调用监听器的 onApplicationEvent(E event) 方法。
* getApplicationListeners(event, type)获取到的监听器中包含ConfigFileApplicationListener实例
* ConfigFileApplicationListener将加载配置文件
*/
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
1.1.3.1.1、ConfigFileApplicationListener监听到ApplicationEnvironmentPreparedEvent事件后,使用环境变量后置处理器将配置的属性绑定到Environment中。
// 源码位置 org.springframework.boot.context.config.ConfigFileApplicationListener.onApplicationEnvironmentPreparedEvent
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
/**
* 使用环境变量后置处理器将属性添加到Environment的propertySources中
* postProcessors包含很多个环境变量后置处理器,例如:
* SystemEnvironmentPropertySourceEnvironmentPostProcessor 处理系统属性
* SpringApplicationJsonEnvironmentPostProcessor 处理SPRING_APPLICATION_JSON属性
* ConfigFileApplicationListener 处理properties、yml配置文件
*/
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
当for (EnvironmentPostProcessor postProcessor : postProcessors) 循环完毕,Environment对象就变成了下面这样
配置文件application-{profile}.properties被存储到了propertySourceList的OriginTrackedMapPropertySource对象中,一个配置文件对应一个OriginTrackedMapPropertySource对象。
下面来分析application-{profile}.properties的加载过程
当 for (EnvironmentPostProcessor postProcessor : postProcessors) 遍历到ConfigFileApplicationListener时,debug进入方法内,最终进入addPropertySources方法
// 源码位置 org.springframework.boot.context.config.ConfigFileApplicationListener.addPropertySources
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
// 使用Loader加载属性源并配置active文件
new Loader(environment, resourceLoader).load();
}
Loader构造函数主要做一些属性赋值操作。
1、下面看下load()方法,这个方法就很重要了。
// 源码位置 org.springframework.boot.context.config.ConfigFileApplicationListener.Loader.load()
void load() {
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
(defaultProperties) -> {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// 初始化profile
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
// 加载配置文件
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
// 配置文件键值对绑定到environment对象
addLoadedPropertySources();
applyActiveProfiles(defaultProperties);
});
}
1.1、initializeProfiles方法解析。
// 源码位置 org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#initializeProfiles()
private void initializeProfiles() {
/**
* 给profiles添加一个null元素比较有意思。
* 可以把application.properties、application.yml的profile认为是null。
* 先添加null元素,即先处理默认配置文件application.properties、application.yml
*/
this.profiles.add(null);
/**
* 此时还没加载配置文件,只能读取命令行属性、SPRING_APPLICATION_JSON、系统环境变量等方式配置的spring.profiles.active、spring.profiles.include
*/
Set<ConfigFileApplicationListener.Profile> activatedViaProperty = getProfilesFromProperty(ACTIVE_PROFILES_PROPERTY);
Set<ConfigFileApplicationListener.Profile> includedViaProperty = getProfilesFromProperty(INCLUDE_PROFILES_PROPERTY);
List<ConfigFileApplicationListener.Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
this.profiles.addAll(otherActiveProfiles);
// Any pre-existing active profiles set via property sources (e.g.
// System properties) take precedence over those added in config files.
this.profiles.addAll(includedViaProperty);
addActiveProfiles(activatedViaProperty);
/**
* 如果没有配置spring.profiles.active、spring.profiles.include 就加一个default的profile
*/
if (this.profiles.size() == 1) { // only has null profile
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
ConfigFileApplicationListener.Profile defaultProfile = new ConfigFileApplicationListener.Profile(defaultProfileName, true);
this.profiles.add(defaultProfile);
}
}
}
initializeProfiles()执行完后,this.profiles=[null, 命令行等方式配置的spring.profiles.active和spring.profiles.include/或者default]。
在本教程中this.profiles=[null, "dev"]。然后便是循环this.profiles。
1.1.1、while循环中的 load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));解析
// 源码位置 org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#load()
private void load(String location, String name, ConfigFileApplicationListener.Profile
profile, ConfigFileApplicationListener.DocumentFilterFactory filterFactory,
ConfigFileApplicationListener.DocumentConsumer consumer) {
//省略部分代码
/**
* this.propertySourceLoaders有2个loader:
* PropertiesPropertySourceLoader("properties", "xml")、YamlPropertySourceLoader("yml", "yaml")
*/
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
// 使用loader加载配置文件
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
由这两个同名load方法可以看出
(1)、先通过getSearchLocations()获取配置文件的存放目录(是个集合),然后遍历目录集合。
(2)、在遍历目录的过程中,遍历加载器集合this.propertySourceLoaders,使用加载器加载相应的文件类型,可加载"properties"、"xml"、"yml"、"yaml"这4种文件类型。
1.1.1.1、加载器加载配置文件的过程
// 源码位置 org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#load()
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
// 仅列出主要代码
// 定位资源
Resource resource = this.resourceLoader.getResource(location);
// 将配置文件键值对加载到Document对象中
List<Document> documents = loadDocuments(loader, name, resource);
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
/**
* 如果配置文件中配置了spring.profiles.active、spring.profiles.include,
* 也添加到ConfigFileApplicationListener的profiles属性中
*/
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
// document添加到loaded
loaded.add(document);
}
}
/**
* 遍历loaded
* consumer.accept(profile, document)是将document添加到MutablePropertySources的propertySourceList中
*/
loaded.forEach((document) -> consumer.accept(profile, document));
}
List<Document> documents = loadDocuments(loader, name, resource); 这方法的主要代码如下:
// 源码位置 org.springframework.boot.env.PropertiesPropertySourceLoader.load()
// PropertiesPropertySourceLoader可以加载xml和properties文件
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
// PropertiesPropertySourceLoader使用一个map存储配置文件中的键值对
Map<String, ?> properties = loadProperties(resource);
if (properties.isEmpty()) {
return Collections.emptyList();
}
return Collections
.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));
}
Map<String, ?> properties = loadProperties(resource);详解
// 源码位置 org.springframework.boot.env.OriginTrackedPropertiesLoader.load(boolean)
Map<String, OriginTrackedValue> load(boolean expandLists) throws IOException {
//仅列表部分代码
// 使用CharacterReader读取配置文件
try (CharacterReader reader = new CharacterReader(this.resource)) {
Map<String, OriginTrackedValue> result = new LinkedHashMap<>();
StringBuilder buffer = new StringBuilder();
while (reader.read()) {
// 添加键值对到result中
put(result, key, value);
}
// 返回result
return result;
}
}
配置文件键值对存储到在document的propertySource属性中
loaded.forEach((document) -> consumer.accept(profile, document)); 继续debug这句代码,来到了
// 源码位置 org.springframework.core.env.MutablePropertySources.addLast()
public void addLast(PropertySource<?> propertySource) {
removeIfPresent(propertySource);
/**
* this是loaded,先将配置文件键值对添加到loaded的propertySourceList中
*/
this.propertySourceList.add(propertySource);
}
1.2、再回到 ConfigFileApplicationListener.Loader#load() 方法,load()方法中的 addLoadedPropertySources(); 便是将loaded的PropertySource添加给environment。
// 源码位置 org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#addLoadedPropertySources
private void addLoadedPropertySources() {
// 获取environment的propertySources
MutablePropertySources destination = this.environment.getPropertySources();
// 获取暂存在loaded中的键值对配置
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
// 遍历loaded
for (MutablePropertySources sources : loaded) {
// 将loaded添加到destination中
addLoadedPropertySource(destination, lastAdded, source);
}
}
至此,配置文件的键值对如何添加到environment对象中就讲完了。
获取Environment属性源码分析
新建一个MyEnvironmentAware
@Component
public class MyEnvironmentAware implements EnvironmentAware {
private static Environment env;
public static String getProperty(String key){
String pk = env.getProperty(key);
System.out.println(pk);
return pk;
}
@Override
public void setEnvironment(Environment environment) {
this.env = environment;
}
}
然后在 MyApplicationRunner#run方法中加入一行代码
MyEnvironmentAware.getProperty("test.property")
启动工程,debug到Environment的getProperty(String key)方法内部。
1、最开始是调用propertyResolver的getProperty方法
// 源码位置 org.springframework.core.env.AbstractEnvironment.getProperty(java.lang.String)
public String getProperty(String key) {
// 调用propertyResolver的getProperty方法
return this.propertyResolver.getProperty(key);
}
2、getProperty解析
//源码位置 org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(java.lang.String, java.lang.Class<T>, boolean)
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
// 仅展示部分代码
// 遍历this.propertySources
for (PropertySource<?> propertySource : this.propertySources) {
// propertySource.getProperty(key);其实就是获取获取source中的键值对
Object value = propertySource.getProperty(key);
/**
* 如果value带有占位符
* 1、截取占位符文本
* 2、再次遍历this.propertySources,获取到value。
* 从第2点可以看出,使用占位符与外部化配置的优先级无关。
* 比方说:命令行参数的值是占位符,占位符的作为key可以配置到application.properties中.
*/
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
}
}
}
}
Object value = propertySource.getProperty(key); 详解
// 源码位置 org.springframework.core.env.MapPropertySource#getProperty
public Object getProperty(String name) {
// Object value = propertySource.getProperty(key); 就是取source中的值
return this.source.get(name);
}
this.propertySources如下图所示
命令行属性的source如下图所示