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如下圖所示