從配置文件中獲取屬性應該是SpringBoot 開發中最為常用的功能之一,但就是這么常用的功能,仍然有很多開發者在這個方面踩坑。
我整理了幾種獲取配置屬性的方式,目的不僅是要讓大家學會如何使用,更重要的是 弄清配置加載、讀取的底層原理,一旦出現問題可以分析出其癥結所在,而不是一報錯取不到屬性,無頭蒼蠅般的重啟項目,在句句 臥槽中逐漸抓狂~
以下示例源碼 Springboot 版本均為 2.7.6
下邊我們一一過下這幾種玩法和原理,看看有哪些是你沒用過的!話不多說,開始搞~
一、Environment
使用 Environment 方式來獲取配置屬性值非常簡單,只要注入Environment類調用其方法 getProperty(屬性key) 即可,但知其然知其所以然,簡單了解下它的原理,因為后續的幾種獲取配置的方法都和它息息相關。
@Slf4j
@SpringBootTest
publicclassEnvironmentTest{
@Resource
privateEnvironment env;
@Test
publicvoidvar1Test{
String var1 = env.getProperty( "env101.var1");
log.info( "Environment 配置獲取 {}", var1);
}
}
1、什么是 Environment?
Environment 是 springboot 核心的環境配置接口,它提供了簡單的方法來訪問應用程序屬性,包括系統屬性、操作系統環境變量、命令行參數、和應用程序配置文件中定義的屬性等等。
2、配置初始化
Springboot 程序啟動加載流程里,會執行 SpringApplication.run 中的 prepareEnvironment 方法進行配置的初始化,那初始化過程每一步都做了什么呢?
privateConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
/**
* 1、創建 ConfigurableEnvironment 對象:首先調用 getOrCreateEnvironment 方法獲取或創建
* ConfigurableEnvironment 對象,該對象用于存儲環境參數。如果已經存在 ConfigurableEnvironment 對象,則直接使用它;否則,根據用戶的配置和默認配置創建一個新的。
*/
ConfigurableEnvironment environment = getOrCreateEnvironment;
/**
* 2、解析并加載用戶指定的配置文件,將其作為 PropertySource 添加到環境對象中。該方法默認會解析 application.properties 和 application.yml 文件,并將其添加到 ConfigurableEnvironment 對象中。
* PropertySource 或 PropertySourcesPlaceholderConfigurer 加載應用程序的定制化配置。
*/
configureEnvironment(environment, applicationArguments.getSourceArgs);
// 3、加載所有的系統屬性,并將它們添加到 ConfigurableEnvironment 對象中
ConfigurationPropertySources.attach(environment);
// 4、通知監聽器環境參數已經準備就緒
listeners.environmentPrepared(bootstrapContext, environment);
/**
* 5、將默認的屬性源中的所有屬性值移到環境對象的隊列末尾,
這樣用戶自定義的屬性值就可以覆蓋默認的屬性值。這是為了避免用戶無意中覆蓋了 Spring Boot 所提供的默認屬性。
*/
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.contAInsProperty( "spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
// 6、將 Spring Boot 應用程序的屬性綁定到環境對象上,以便能夠正確地讀取和使用這些配置屬性
bindToSpringApplication(environment);
// 7、如果沒有自定義的環境類型,則使用 EnvironmentConverter 類型將環境對象轉換為標準的環境類型,并添加到 ConfigurableEnvironment 對象中。
if(! this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = newEnvironmentConverter(getClassLoader);
environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass);
}
// 8、再次加載系統配置,以防止被其他配置覆蓋
ConfigurationPropertySources.attach(environment);
returnenvironment;
}
看看它的配置加載流程步驟:
-
創建 環境對象ConfigurableEnvironment 用于存儲環境參數;
-
configureEnvironment 方法加載默認的 application.properties 和 application.yml 配置文件;以及用戶指定的配置文件,將其封裝為 PropertySource添加到環境對象中;
-
attach : 加載所有的系統屬性,并將它們添加到環境對象中;
-
listeners.environmentPrepared : 發送環境參數配置已經準備就緒的監聽通知;
-
moveToEnd : 將 系統默認的屬性源中的所有屬性值移到環境對象的隊列末尾,這樣用戶自定義的屬性值就可以覆蓋默認的屬性值。
-
bindToSpringApplication : 應用程序的屬性綁定到 Bean 對象上;
-
attach : 再次加載系統配置,以防止被其他配置覆蓋;
上邊的配置加載流程中,各種配置屬性會封裝成一個個抽象的數據結構 PropertySource 中,這個數據結構代碼格式如下,key-value形式。
publicabstractclassPropertySource< T> {
protectedfinalString name; // 屬性源名稱
protectedfinalT source; // 屬性源值(一個泛型,比如Map,Property)
publicString getName; // 獲取屬性源的名字
publicT getSource; // 獲取屬性源值
publicbooleancontainsProperty(String name); //是否包含某個屬性
publicabstractObject getProperty(String name); //得到屬性名對應的屬性值
}
PropertySource 有諸多的實現類用于管理應用程序的配置屬性。不同的 PropertySource 實現類可以從不同的來源獲取配置屬性,例如文件、環境變量、命令行參數等。其中涉及到的一些實現類有:
關系圖
-
MapPropertySource : Map 鍵值對的對象轉換為 PropertySource 對象的適配器;
-
PropertiesPropertySource : Properties 對象中的所有配置屬性轉換為 Spring 環境中的屬性值;
-
ResourcePropertySource : 從文件系統或者 classpath 中加載配置屬性,封裝成 PropertySource對象;
-
ServletConfigPropertySource : Servlet 配置中讀取配置屬性,封裝成 PropertySource 對象;
-
ServletContextPropertySource : Servlet 上下文中讀取配置屬性,封裝成 PropertySource 對象;
-
StubPropertySource : 是個空的實現類,它的作用僅僅是給 CompositePropertySource 類作為默認的父級屬性源,以避免空指針異常;
-
CompositePropertySource : 是個復合型的實現類,內部維護了 PropertySource集合隊列,可以將多個 PropertySource 對象合并;
-
SystemEnvironmentPropertySource : 操作系統環境變量中讀取配置屬性,封裝成 PropertySource 對象;
上邊各類配置初始化生成的 PropertySource 對象會被維護到集合隊列中。
List<PropertySource<?>> sources = newArrayList<PropertySource<?>>
配置初始化完畢,應用程序上下文 AbstractApplicationContext 會加載配置,這樣程序在運行時就可以隨時獲取配置信息了。
privatevoidprepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
// 應用上下文加載環境對象
context.setEnvironment(environment);
postProcessApplicationContext(context);
.........
}
3、讀取配置
看明白上邊配置加載的流程,其實讀取配置就容易理解了,無非就是遍歷隊列里的 PropertySource ,拿屬性名稱 name 匹配對應的屬性值 source 。
PropertyResolver 是獲取配置的關鍵類,其內部提供了操作 PropertySource 隊列的方法,核心方法 getProperty(key) 獲取配置值,看了下這個類的依賴關系,發現 Environment 是它子類。
那么直接用 PropertyResolver 來獲取配置屬性其實也是可以的,到這我們就大致明白了 Springboot 配置的加載和讀取了。
@Slf4j
@SpringBootTest
publicclassEnvironmentTest{
@Resource
privatePropertyResolver env;
@Test
publicvoidvar1Test{
String var1 = env.getProperty( "env101.var1");
log.info( "Environment 配置獲取 {}", var1);
}
}
二、@Value 注解
@Value注解是 Spring 框架提供的用于注入配置屬性值的注解,它可用于類的 成員變量 、 方法參數 和 構造函數 參數上, 這個記住很重要!
在應用程序啟動時,使用 @Value 注解的 Bean 會被實例化。所有使用了 @Value 注解的 Bean 會被加入到 PropertySourcesPlaceholderConfigurer 的后置處理器集合中。
當后置處理器開始執行時,它會讀取 Bean 中所有 @Value 注解所標注的值,并通過反射將解析后的屬性值賦值給標有 @Value 注解的成員變量、方法參數和構造函數參數。
如何使用需要注意,在使用 @Value 注解時需要確保注入的屬性值已經加載到 Spring 容器中,否則會導致注入失敗。
在 src/main/resources 目錄下的 application.yml 配置文件中添加 env101.var1 屬性。
env101:
var1: var1-公眾號:程序員小富
只要在變量上加注解 @Value("${env101.var1}") 就可以了,@Value 注解會自動將配置文件中的 env101.var1 屬性值注入到 var1 字段中,跑個單元測試看一下結果。
@Slf4j
@SpringBootTest
publicclassEnvVariablesTest{
@Value( "${env101.var1}")
privateString var1;
@Test
publicvoidvar1Test{
log.info( "配置文件屬性: {}",var1);
}
}
毫無懸念,成功拿到配置數據。
雖然@Value注解方式使用起來很簡單,如果使用不當還會遇到不少坑。
1、缺失配置
如果在代碼中引用變量,配置文件中未進行配值,就會出現類似下圖所示的錯誤。
為了避免此類錯誤導致服務啟動異常,我們可以在引用變量的同時給它賦一個默認值,以確保即使在未正確配值的情況下,程序依然能夠正常運行。
@Value( "${env101.var1:我是小富}")
privateString var1;
2、靜態變量(static)賦值
還有一種常見的使用誤區,就是將 @Value 注解加到靜態變量上,這樣做是無法獲取屬性值的。靜態變量是類的屬性,并不屬于對象的屬性,而 Spring是基于對象的屬性進行依賴注入的,類在應用啟動時靜態變量就被初始化,此時 Bean還未被實例化,因此不可能通過 @Value 注入屬性值。
@Slf4j
@SpringBootTest
publicclassEnvVariablesTest{
@Value( "${env101.var1}")
privatestaticString var1;
@Test
publicvoidvar1Test{
log.info( "配置文件屬性: {}",var1);
}
}
即使 @Value 注解無法直接用在靜態變量上,我們仍然可以通過獲取已有 Bean實例化后的屬性值,再將其賦值給靜態變量來實現給靜態變量賦值。
我們可以先通過 @Value 注解將屬性值注入到普通 Bean中,然后在獲取該 Bean對應的屬性值,并將其賦值給靜態變量。這樣,就可以在靜態變量中使用該屬性值了。
@Slf4j
@SpringBootTest
publicclassEnvVariablesTest{
privatestaticString var3;
privatestaticString var4;
@Value( "${env101.var3}")
publicvoidsetVar3(String var3){
var3 = var3;
}
EnvVariablesTest( @Value( "${env101.var4}") String var4){
var4 = var4;
}
publicstaticString getVar4{
returnvar4;
}
publicstaticString getVar3{
returnvar3;
}
}
3、常量(final)賦值
@Value 注解加到 final 關鍵字上同樣也無法獲取屬性值,因為 final 變量必須在構造方法中進行初始化,并且一旦被賦值便不能再次更改。而 @Value 注解是在 bean 實例化之后才進行屬性注入的,因此無法在構造方法中初始化 final 變量。
@Slf4j
@SpringBootTest
publicclassEnvVariables2Test{
privatefinalString var6;
@Autowired
EnvVariables2Test( @Value( "${env101.var6}") String var6) {
this.var6 = var6;
}
/**
* @value注解 final 獲取
*/
@Test
publicvoidvar1Test{
log.info( "final 注入: {}", var6);
}
}
4、非注冊的類中使用
只有標注了 @Component 、 @Service 、 @Controller 、 @Repository 或 @Configuration 等 容器管理注解的類,由 Spring 管理的 bean 中使用 @Value注解才會生效。而對于普通的POJO類,則無法使用 @Value注解進行屬性注入。
/**
* @value注解 非注冊的類中使用
* ` @Component`、` @Service`、` @Controller`、` @Repository` 或 ` @Configuration` 等
* 容器管理注解的類中使用 @Value注解才會生效
*/
@Data
@Slf4j
@Component
publicclassTestService{
@Value( "${env101.var7}")
privateString var7;
publicString getVar7{
returnthis.var7;
}
}
5、引用方式不對
如果我們想要獲取 TestService 類中的某個變量的屬性值,需要使用依賴注入的方式,而不能使用 new 的方式。通過依賴注入的方式創建 TestService 對象,Spring 會在創建對象時將對象所需的屬性值注入到其中。
/**
* @value注解 引用方式不對
*/
@Test
publicvoidvar7_1Test{
TestService testService = newTestService;
log.info( "引用方式不對 注入: {}", testService.getVar7);
}
三、@ConfigurationProperties 注解最后總結一下 @Value注解要在 Bean的生命周期內使用才能生效。
@ConfigurationProperties 注解是 SpringBoot 提供的一種更加便捷來處理配置文件中的屬性值的方式,可以通過自動綁定和類型轉換等機制,將指定前綴的屬性集合自動綁定到一個Bean對象上。
加載原理
在 Springboot 啟動流程加載配置的 prepareEnvironment 方法中,有一個重要的步驟方法 bindToSpringApplication(environment) ,它的作用是將配置文件中的屬性值綁定到被 @ConfigurationProperties 注解標記的 Bean對象中。但此時這些對象還沒有被 Spring 容器管理,因此無法完成屬性的自動注入。
那么這些Bean對象又是什么時候被注冊到 Spring 容器中的呢?
這就涉及到了 ConfigurationPropertiesBindingPostProcessor 類,它是 Bean后置處理器,負責掃描容器中所有被 @ConfigurationProperties 注解所標記的 Bean對象。如果找到了,則會使用 Binder 組件將外部屬性的值綁定到它們身上,從而實現自動注入。
-
bindToSpringApplication 主要是將屬性值綁定到 Bean 對象中;
-
ConfigurationPropertiesBindingPostProcessor 負責在 Spring 容器啟動時將被注解標記的 Bean 對象注冊到容器中,并完成后續的屬性注入操作;
演示使用 @ConfigurationProperties 注解,在 application.yml 配置文件中添加配置項:
env101:
var1:var1-公眾號:程序員小富
var2:var2-公眾號:程序員小富
創建一個 MyConf 類用于承載所有前綴為 env101 的配置屬性。
@Data
@Configuration
@ConfigurationProperties(prefix = "env101")
publicclassMyConf{
privateString var1;
privateString var2;
}
在需要使用 var1 、 var2 屬性值的地方,將 MyConf 對象注入到依賴對象中即可。
@Slf4j
@SpringBootTest
publicclassCoNFTest{
@Resource
privateMyConf myConf;
@Test
publicvoidmyConfTest{
log.info( "@ConfigurationProperties注解 配置獲取 {}", JSON.toJSONString(myConf));
}
}
四、@PropertySources 注解
除了系統默認的 application.yml 或者 application.properties 文件外,我們還可能需要使用自定義的配置文件來實現更加靈活和個性化的配置。與默認的配置文件不同的是,自定義的配置文件無法被應用自動加載,需要我們手動指定加載。
@PropertySources 注解的實現原理相對簡單,應用程序啟動時掃描所有被該注解標注的類,獲取到注解中指定自定義配置文件的路徑,將指定路徑下的配置文件內容加載到 Environment 中,這樣可以通過 @Value 注解或 Environment.getProperty 方法來獲取其中定義的屬性值了。
如何使用
在 src/main/resources/ 目錄下創建自定義配置文件 xiaofu.properties ,增加兩個屬性。
env101.var9=var9-程序員小富
env101.var10=var10-程序員小富
在需要使用自定義配置文件的類上添加 @PropertySources 注解,注解 value屬性中指定自定義配置文件的路徑,可以指定多個路徑,用逗號隔開。
@Data
@Configuration
@PropertySources({
@PropertySource(value = "classpath:xiaofu.properties",encoding = "utf-8"),
@PropertySource(value = "classpath:xiaofu.properties",encoding = "utf-8")
})
publicclassPropertySourcesConf{
@Value( "${env101.var10}")
privateString var10;
@Value( "${env101.var9}")
privateString var9;
}
成功獲取配置了
但是當我試圖加載 .yaml 文件時,啟動項目居然報錯了,經過一番摸索我發現,@PropertySources 注解只內置了 PropertySourceFactory 適配器。也就是說它只能加載 .properties 文件。
那如果我想要加載一個 .yaml 類型文件,則需要自行實現yaml的適配器 YamlPropertySourceFactory 。
publicclassYamlPropertySourceFactoryimplementsPropertySourceFactory{
@Override
publicPropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throwsIOException {
YamlPropertiesFactoryBean factory = newYamlPropertiesFactoryBean;
factory.setResources(encodedResource.getResource);
Properties properties = factory.getObject;
returnnewPropertiesPropertySource(encodedResource.getResource.getFilename, properties);
}
}
而在加載配置時要顯示的指定使用 YamlPropertySourceFactory 適配器,這樣就完成了@PropertySource注解加載 yaml 文件。
@Data
@Configuration
@PropertySources({
@PropertySource(value = "classpath:xiaofu.yaml", encoding = "utf-8", factory = YamlPropertySourceFactory . class)
})
publicclassPropertySourcesConf2{
@Value( "${env101.var10}")
privateString var10;
@Value( "${env101.var9}")
privateString var9;
}
五、YamlPropertiesFactoryBean 加載 YAML 文件
我們可以使用 YamlPropertiesFactoryBean 類將 YAML 配置文件中的屬性值注入到 Bean 中。
@Configuration
publicclassMyYamlConfig{
@Bean
publicstaticPropertySourcesPlaceholderConfigurer yamlConfigurer{
PropertySourcesPlaceholderConfigurer configurer = newPropertySourcesPlaceholderConfigurer;
YamlPropertiesFactoryBean yaml = newYamlPropertiesFactoryBean;
yaml.setResources( newClassPathResource( "xiaofu.yml"));
configurer.setProperties(Objects.requireNonNull(yaml.getObject));
returnconfigurer;
}
}
可以通過 @Value 注解或 Environment.getProperty 方法來獲取其中定義的屬性值。
@Slf4j
@SpringBootTest
public class YamlTest {
@Value( " ${env101.var11}" )
private String var11;
@Test
public void myYamlTest{
log.info( "Yaml 配置獲取 {}", var11);
}
}
六、自定義讀取
如果上邊的幾種讀取配置的方式你都不喜歡,就想自己寫個更流批的輪子,那也很好辦。我們直接注入 PropertySources 獲取所有屬性的配置隊列,你是想用注解實現還是其他什么方式,就可以為所欲為了。
@Slf4j
@SpringBootTest
publicclassCustomTest{
@Autowired
privatePropertySources propertySources;
@Test
publicvoidcustomTest{
for(PropertySource<?> propertySource : propertySources) {
log.info( "自定義獲取 配置獲取 name {} ,{}", propertySource.getName, propertySource.getSource);
}
}
}
總結
我們可以通過 @Value 注解、 Environment 類、 @ConfigurationProperties 注解、 @PropertySource 注解等方式來獲取配置信息。
其中,@Value 注解適用于單個值的注入,而其他幾種方式適用于批量配置的注入。不同的方式在效率、靈活性、易用性等方面存在差異,在選擇配置獲取方式時,還需要考慮個人編程習慣和業務需求。
如果重視代碼的可讀性和可維護性,則可以選擇使用 @ConfigurationProperties 注解;如果更注重運行效率,則可以選擇使用 Environment 類。總之,不同的場景需要選擇不同的方式,以達到最優的效果。