從配置文件中獲取屬性應(yīng)該是SpringBoot開發(fā)中最為常用的功能之一,但就是這么常用的功能,仍然有很多開發(fā)者在這個(gè)方面踩坑。
我整理了幾種獲取配置屬性的方式,目的不僅是要讓大家學(xué)會(huì)如何使用,更重要的是弄清配置加載、讀取的底層原理,一旦出現(xiàn)問題可以分析出其癥結(jié)所在,而不是一報(bào)錯(cuò)取不到屬性,無頭蒼蠅般的重啟項(xiàng)目,在句句臥槽中逐漸抓狂~
以下示例源碼 Springboot 版本均為 2.7.6
下邊我們一一過下這幾種玩法和原理,看看有哪些是你沒用過的!話不多說,開始搞~
一、Environment
使用 Environment 方式來獲取配置屬性值非常簡(jiǎn)單,只要注入Environment類調(diào)用其方法getProperty(屬性key)即可,但知其然知其所以然,簡(jiǎn)單了解下它的原理,因?yàn)楹罄m(xù)的幾種獲取配置的方法都和它息息相關(guān)。
@Slf4j @SpringBootTest publicclassEnvironmentTest{ @Resource privateEnvironmentenv; @Test publicvoidvar1Test(){ Stringvar1=env.getProperty("env101.var1"); log.info("Environment配置獲取{}",var1); } }
1、什么是 Environment?
Environment 是 springboot 核心的環(huán)境配置接口,它提供了簡(jiǎn)單的方法來訪問應(yīng)用程序?qū)傩裕ㄏ到y(tǒng)屬性、操作系統(tǒng)環(huán)境變量、命令行參數(shù)、和應(yīng)用程序配置文件中定義的屬性等等。
2、配置初始化
Springboot 程序啟動(dòng)加載流程里,會(huì)執(zhí)行SpringApplication.run中的prepareEnvironment()方法進(jìn)行配置的初始化,那初始化過程每一步都做了什么呢?
privateConfigurableEnvironmentprepareEnvironment(SpringApplicationRunListenerslisteners, DefaultBootstrapContextbootstrapContext,ApplicationArgumentsapplicationArguments){ /** *1、創(chuàng)建ConfigurableEnvironment對(duì)象:首先調(diào)用getOrCreateEnvironment()方法獲取或創(chuàng)建 *ConfigurableEnvironment對(duì)象,該對(duì)象用于存儲(chǔ)環(huán)境參數(shù)。如果已經(jīng)存在ConfigurableEnvironment對(duì)象,則直接使用它;否則,根據(jù)用戶的配置和默認(rèn)配置創(chuàng)建一個(gè)新的。 */ ConfigurableEnvironmentenvironment=getOrCreateEnvironment(); /** *2、解析并加載用戶指定的配置文件,將其作為PropertySource添加到環(huán)境對(duì)象中。該方法默認(rèn)會(huì)解析application.properties和application.yml文件,并將其添加到ConfigurableEnvironment對(duì)象中。 *PropertySource或PropertySourcesPlaceholderConfigurer加載應(yīng)用程序的定制化配置。 */ configureEnvironment(environment,applicationArguments.getSourceArgs()); //3、加載所有的系統(tǒng)屬性,并將它們添加到ConfigurableEnvironment對(duì)象中 ConfigurationPropertySources.attach(environment); //4、通知監(jiān)聽器環(huán)境參數(shù)已經(jīng)準(zhǔn)備就緒 listeners.environmentPrepared(bootstrapContext,environment); /** *5、將默認(rèn)的屬性源中的所有屬性值移到環(huán)境對(duì)象的隊(duì)列末尾, 這樣用戶自定義的屬性值就可以覆蓋默認(rèn)的屬性值。這是為了避免用戶無意中覆蓋了SpringBoot所提供的默認(rèn)屬性。 */ DefaultPropertiesPropertySource.moveToEnd(environment); Assert.state(!environment.containsProperty("spring.main.environment-prefix"), "Environmentprefixcannotbesetviaproperties."); //6、將SpringBoot應(yīng)用程序的屬性綁定到環(huán)境對(duì)象上,以便能夠正確地讀取和使用這些配置屬性 bindToSpringApplication(environment); //7、如果沒有自定義的環(huán)境類型,則使用EnvironmentConverter類型將環(huán)境對(duì)象轉(zhuǎn)換為標(biāo)準(zhǔn)的環(huán)境類型,并添加到ConfigurableEnvironment對(duì)象中。 if(!this.isCustomEnvironment){ EnvironmentConverterenvironmentConverter=newEnvironmentConverter(getClassLoader()); environment=environmentConverter.convertEnvironmentIfNecessary(environment,deduceEnvironmentClass()); } //8、再次加載系統(tǒng)配置,以防止被其他配置覆蓋 ConfigurationPropertySources.attach(environment); returnenvironment; }
看看它的配置加載流程步驟:
創(chuàng)建 環(huán)境對(duì)象 ConfigurableEnvironment 用于存儲(chǔ)環(huán)境參數(shù);
configureEnvironment 方法加載默認(rèn)的 application.properties 和 application.yml 配置文件;以及用戶指定的配置文件,將其封裝為 PropertySource 添加到環(huán)境對(duì)象中;
attach(): 加載所有的系統(tǒng)屬性,并將它們添加到環(huán)境對(duì)象中;
listeners.environmentPrepared(): 發(fā)送環(huán)境參數(shù)配置已經(jīng)準(zhǔn)備就緒的監(jiān)聽通知;
moveToEnd(): 將 系統(tǒng)默認(rèn) 的屬性源中的所有屬性值移到環(huán)境對(duì)象的隊(duì)列末尾,這樣用戶自定義的屬性值就可以覆蓋默認(rèn)的屬性值。
bindToSpringApplication: 應(yīng)用程序的屬性綁定到 Bean 對(duì)象上;
attach(): 再次加載系統(tǒng)配置,以防止被其他配置覆蓋;
上邊的配置加載流程中,各種配置屬性會(huì)封裝成一個(gè)個(gè)抽象的數(shù)據(jù)結(jié)構(gòu) PropertySource中,這個(gè)數(shù)據(jù)結(jié)構(gòu)代碼格式如下,key-value形式。
publicabstractclassPropertySource{ protectedfinalStringname;//屬性源名稱 protectedfinalTsource;//屬性源值(一個(gè)泛型,比如Map,Property) publicStringgetName();//獲取屬性源的名字 publicTgetSource();//獲取屬性源值 publicbooleancontainsProperty(Stringname);//是否包含某個(gè)屬性 publicabstractObjectgetProperty(Stringname);//得到屬性名對(duì)應(yīng)的屬性值 }
PropertySource 有諸多的實(shí)現(xiàn)類用于管理應(yīng)用程序的配置屬性。不同的 PropertySource 實(shí)現(xiàn)類可以從不同的來源獲取配置屬性,例如文件、環(huán)境變量、命令行參數(shù)等。其中涉及到的一些實(shí)現(xiàn)類有:
關(guān)系圖
MapPropertySource: Map 鍵值對(duì)的對(duì)象轉(zhuǎn)換為 PropertySource 對(duì)象的適配器;
PropertiesPropertySource: Properties 對(duì)象中的所有配置屬性轉(zhuǎn)換為 Spring 環(huán)境中的屬性值;
ResourcePropertySource: 從文件系統(tǒng)或者 classpath 中加載配置屬性,封裝成 PropertySource對(duì)象;
ServletConfigPropertySource: Servlet 配置中讀取配置屬性,封裝成 PropertySource 對(duì)象;
ServletContextPropertySource: Servlet 上下文中讀取配置屬性,封裝成 PropertySource 對(duì)象;
StubPropertySource: 是個(gè)空的實(shí)現(xiàn)類,它的作用僅僅是給 CompositePropertySource 類作為默認(rèn)的父級(jí)屬性源,以避免空指針異常;
CompositePropertySource: 是個(gè)復(fù)合型的實(shí)現(xiàn)類,內(nèi)部維護(hù)了 PropertySource集合隊(duì)列,可以將多個(gè) PropertySource 對(duì)象合并;
SystemEnvironmentPropertySource: 操作系統(tǒng)環(huán)境變量中讀取配置屬性,封裝成 PropertySource 對(duì)象;
上邊各類配置初始化生成的 PropertySource 對(duì)象會(huì)被維護(hù)到集合隊(duì)列中。
List>sources=newArrayList >()
配置初始化完畢,應(yīng)用程序上下文AbstractApplicationContext會(huì)加載配置,這樣程序在運(yùn)行時(shí)就可以隨時(shí)獲取配置信息了。
privatevoidprepareContext(DefaultBootstrapContextbootstrapContext,ConfigurableApplicationContextcontext, ConfigurableEnvironmentenvironment,SpringApplicationRunListenerslisteners, ApplicationArgumentsapplicationArguments,BannerprintedBanner){ //應(yīng)用上下文加載環(huán)境對(duì)象 context.setEnvironment(environment); postProcessApplicationContext(context); ......... }
3、讀取配置
看明白上邊配置加載的流程,其實(shí)讀取配置就容易理解了,無非就是遍歷隊(duì)列里的PropertySource,拿屬性名稱name匹配對(duì)應(yīng)的屬性值source。
PropertyResolver是獲取配置的關(guān)鍵類,其內(nèi)部提供了操作PropertySource 隊(duì)列的方法,核心方法getProperty(key)獲取配置值,看了下這個(gè)類的依賴關(guān)系,發(fā)現(xiàn) Environment 是它子類。
那么直接用 PropertyResolver 來獲取配置屬性其實(shí)也是可以的,到這我們就大致明白了 Springboot 配置的加載和讀取了。
@Slf4j @SpringBootTest publicclassEnvironmentTest{ @Resource privatePropertyResolverenv; @Test publicvoidvar1Test(){ Stringvar1=env.getProperty("env101.var1"); log.info("Environment配置獲取{}",var1); } }
二、@Value 注解
@Value注解是Spring框架提供的用于注入配置屬性值的注解,它可用于類的成員變量、方法參數(shù)和構(gòu)造函數(shù)參數(shù)上,這個(gè)記住很重要!
在應(yīng)用程序啟動(dòng)時(shí),使用 @Value 注解的 Bean 會(huì)被實(shí)例化。所有使用了 @Value 注解的 Bean 會(huì)被加入到 PropertySourcesPlaceholderConfigurer 的后置處理器集合中。
當(dāng)后置處理器開始執(zhí)行時(shí),它會(huì)讀取 Bean 中所有 @Value 注解所標(biāo)注的值,并通過反射將解析后的屬性值賦值給標(biāo)有 @Value 注解的成員變量、方法參數(shù)和構(gòu)造函數(shù)參數(shù)。
需要注意,在使用 @Value 注解時(shí)需要確保注入的屬性值已經(jīng)加載到 Spring 容器中,否則會(huì)導(dǎo)致注入失敗。
如何使用
在src/main/resources目錄下的application.yml配置文件中添加env101.var1屬性。
env101: var1:var1-公眾號(hào):程序員小富
只要在變量上加注解 @Value("${env101.var1}")就可以了,@Value 注解會(huì)自動(dòng)將配置文件中的env101.var1屬性值注入到var1字段中,跑個(gè)單元測(cè)試看一下結(jié)果。
@Slf4j @SpringBootTest publicclassEnvVariablesTest{ @Value("${env101.var1}") privateStringvar1; @Test publicvoidvar1Test(){ log.info("配置文件屬性:{}",var1); } }
毫無懸念,成功拿到配置數(shù)據(jù)。
雖然@Value注解方式使用起來很簡(jiǎn)單,如果使用不當(dāng)還會(huì)遇到不少坑。
1、缺失配置
如果在代碼中引用變量,配置文件中未進(jìn)行配值,就會(huì)出現(xiàn)類似下圖所示的錯(cuò)誤。
為了避免此類錯(cuò)誤導(dǎo)致服務(wù)啟動(dòng)異常,我們可以在引用變量的同時(shí)給它賦一個(gè)默認(rèn)值,以確保即使在未正確配值的情況下,程序依然能夠正常運(yùn)行。
@Value("${env101.var1:我是小富}") privateStringvar1;
2、靜態(tài)變量(static)賦值
還有一種常見的使用誤區(qū),就是將 @Value 注解加到靜態(tài)變量上,這樣做是無法獲取屬性值的。靜態(tài)變量是類的屬性,并不屬于對(duì)象的屬性,而 Spring是基于對(duì)象的屬性進(jìn)行依賴注入的,類在應(yīng)用啟動(dòng)時(shí)靜態(tài)變量就被初始化,此時(shí) Bean還未被實(shí)例化,因此不可能通過 @Value 注入屬性值。
@Slf4j @SpringBootTest publicclassEnvVariablesTest{ @Value("${env101.var1}") privatestaticStringvar1; @Test publicvoidvar1Test(){ log.info("配置文件屬性:{}",var1); } }
即使 @Value 注解無法直接用在靜態(tài)變量上,我們?nèi)匀豢梢酝ㄟ^獲取已有 Bean實(shí)例化后的屬性值,再將其賦值給靜態(tài)變量來實(shí)現(xiàn)給靜態(tài)變量賦值。
我們可以先通過 @Value 注解將屬性值注入到普通 Bean中,然后在獲取該 Bean對(duì)應(yīng)的屬性值,并將其賦值給靜態(tài)變量。這樣,就可以在靜態(tài)變量中使用該屬性值了。
@Slf4j @SpringBootTest publicclassEnvVariablesTest{ privatestaticStringvar3; privatestaticStringvar4; @Value("${env101.var3}") publicvoidsetVar3(Stringvar3){ var3=var3; } EnvVariablesTest(@Value("${env101.var4}")Stringvar4){ var4=var4; } publicstaticStringgetVar4(){ returnvar4; } publicstaticStringgetVar3(){ returnvar3; } }
3、常量(final)賦值
@Value 注解加到final關(guān)鍵字上同樣也無法獲取屬性值,因?yàn)?final 變量必須在構(gòu)造方法中進(jìn)行初始化,并且一旦被賦值便不能再次更改。而 @Value 注解是在 bean 實(shí)例化之后才進(jìn)行屬性注入的,因此無法在構(gòu)造方法中初始化 final 變量。
@Slf4j @SpringBootTest publicclassEnvVariables2Test{ privatefinalStringvar6; @Autowired EnvVariables2Test(@Value("${env101.var6}")Stringvar6){ this.var6=var6; } /** *@value注解final獲取 */ @Test publicvoidvar1Test(){ log.info("final注入:{}",var6); } }
4、非注冊(cè)的類中使用
只有標(biāo)注了@Component、@Service、@Controller、@Repository 或 @Configuration 等容器管理注解的類,由 Spring 管理的 bean 中使用 @Value注解才會(huì)生效。而對(duì)于普通的POJO類,則無法使用 @Value注解進(jìn)行屬性注入。
/** *@value注解非注冊(cè)的類中使用 *`@Component`、`@Service`、`@Controller`、`@Repository`或`@Configuration`等 *容器管理注解的類中使用@Value注解才會(huì)生效 */ @Data @Slf4j @Component publicclassTestService{ @Value("${env101.var7}") privateStringvar7; publicStringgetVar7(){ returnthis.var7; } }
5、引用方式不對(duì)
如果我們想要獲取 TestService 類中的某個(gè)變量的屬性值,需要使用依賴注入的方式,而不能使用 new 的方式。通過依賴注入的方式創(chuàng)建 TestService 對(duì)象,Spring 會(huì)在創(chuàng)建對(duì)象時(shí)將對(duì)象所需的屬性值注入到其中。
/** *@value注解引用方式不對(duì) */ @Test publicvoidvar7_1Test(){ TestServicetestService=newTestService(); log.info("引用方式不對(duì)注入:{}",testService.getVar7()); }
最后總結(jié)一下 @Value注解要在 Bean的生命周期內(nèi)使用才能生效。
三、@ConfigurationProperties 注解
@ConfigurationProperties注解是 SpringBoot 提供的一種更加便捷來處理配置文件中的屬性值的方式,可以通過自動(dòng)綁定和類型轉(zhuǎn)換等機(jī)制,將指定前綴的屬性集合自動(dòng)綁定到一個(gè)Bean對(duì)象上。
加載原理
在 Springboot 啟動(dòng)流程加載配置的 prepareEnvironment() 方法中,有一個(gè)重要的步驟方法 bindToSpringApplication(environment),它的作用是將配置文件中的屬性值綁定到被 @ConfigurationProperties 注解標(biāo)記的 Bean對(duì)象中。但此時(shí)這些對(duì)象還沒有被 Spring 容器管理,因此無法完成屬性的自動(dòng)注入。
那么這些Bean對(duì)象又是什么時(shí)候被注冊(cè)到 Spring 容器中的呢?
這就涉及到了 ConfigurationPropertiesBindingPostProcessor 類,它是 Bean后置處理器,負(fù)責(zé)掃描容器中所有被 @ConfigurationProperties 注解所標(biāo)記的 Bean對(duì)象。如果找到了,則會(huì)使用 Binder 組件將外部屬性的值綁定到它們身上,從而實(shí)現(xiàn)自動(dòng)注入。
bindToSpringApplication 主要是將屬性值綁定到 Bean 對(duì)象中;
ConfigurationPropertiesBindingPostProcessor 負(fù)責(zé)在 Spring 容器啟動(dòng)時(shí)將被注解標(biāo)記的 Bean 對(duì)象注冊(cè)到容器中,并完成后續(xù)的屬性注入操作;
如何使用
演示使用 @ConfigurationProperties 注解,在 application.yml 配置文件中添加配置項(xiàng):
env101: var1:var1-公眾號(hào):程序員小富 var2:var2-公眾號(hào):程序員小富
創(chuàng)建一個(gè) MyConf 類用于承載所有前綴為env101的配置屬性。
@Data @Configuration @ConfigurationProperties(prefix="env101") publicclassMyConf{ privateStringvar1; privateStringvar2; }
在需要使用var1、var2屬性值的地方,將 MyConf 對(duì)象注入到依賴對(duì)象中即可。
@Slf4j @SpringBootTest publicclassConfTest{ @Resource privateMyConfmyConf; @Test publicvoidmyConfTest(){ log.info("@ConfigurationProperties注解配置獲取{}",JSON.toJSONString(myConf)); } }
四、@PropertySources 注解
除了系統(tǒng)默認(rèn)的 application.yml 或者 application.properties 文件外,我們還可能需要使用自定義的配置文件來實(shí)現(xiàn)更加靈活和個(gè)性化的配置。與默認(rèn)的配置文件不同的是,自定義的配置文件無法被應(yīng)用自動(dòng)加載,需要我們手動(dòng)指定加載。
@PropertySources 注解的實(shí)現(xiàn)原理相對(duì)簡(jiǎn)單,應(yīng)用程序啟動(dòng)時(shí)掃描所有被該注解標(biāo)注的類,獲取到注解中指定自定義配置文件的路徑,將指定路徑下的配置文件內(nèi)容加載到 Environment 中,這樣可以通過 @Value 注解或 Environment.getProperty() 方法來獲取其中定義的屬性值了。
如何使用
在 src/main/resources/ 目錄下創(chuàng)建自定義配置文件 xiaofu.properties,增加兩個(gè)屬性。
env101.var9=var9-程序員小富 env101.var10=var10-程序員小富
在需要使用自定義配置文件的類上添加 @PropertySources 注解,注解 value屬性中指定自定義配置文件的路徑,可以指定多個(gè)路徑,用逗號(hào)隔開。
@Data @Configuration @PropertySources({ @PropertySource(value="classpath:xiaofu.properties",encoding="utf-8"), @PropertySource(value="classpath:xiaofu.properties",encoding="utf-8") }) publicclassPropertySourcesConf{ @Value("${env101.var10}") privateStringvar10; @Value("${env101.var9}") privateStringvar9; }
成功獲取配置了
但是當(dāng)我試圖加載.yaml文件時(shí),啟動(dòng)項(xiàng)目居然報(bào)錯(cuò)了,經(jīng)過一番摸索我發(fā)現(xiàn),@PropertySources 注解只內(nèi)置了PropertySourceFactory適配器。也就是說它只能加載.properties文件。
那如果我想要加載一個(gè).yaml類型文件,則需要自行實(shí)現(xiàn)yaml的適配器 YamlPropertySourceFactory。
publicclassYamlPropertySourceFactoryimplementsPropertySourceFactory{ @Override publicPropertySource>createPropertySource(Stringname,EncodedResourceencodedResource)throwsIOException{ YamlPropertiesFactoryBeanfactory=newYamlPropertiesFactoryBean(); factory.setResources(encodedResource.getResource()); Propertiesproperties=factory.getObject(); returnnewPropertiesPropertySource(encodedResource.getResource().getFilename(),properties); } }
而在加載配置時(shí)要顯示的指定使用 YamlPropertySourceFactory適配器,這樣就完成了@PropertySource注解加載 yaml 文件。
@Data @Configuration @PropertySources({ @PropertySource(value="classpath:xiaofu.yaml",encoding="utf-8",factory=YamlPropertySourceFactory.class) }) publicclassPropertySourcesConf2{ @Value("${env101.var10}") privateStringvar10; @Value("${env101.var9}") privateStringvar9; }
五、YamlPropertiesFactoryBean 加載 YAML 文件
我們可以使用 YamlPropertiesFactoryBean 類將 YAML 配置文件中的屬性值注入到 Bean 中。
@Configuration publicclassMyYamlConfig{ @Bean publicstaticPropertySourcesPlaceholderConfigureryamlConfigurer(){ PropertySourcesPlaceholderConfigurerconfigurer=newPropertySourcesPlaceholderConfigurer(); YamlPropertiesFactoryBeanyaml=newYamlPropertiesFactoryBean(); yaml.setResources(newClassPathResource("xiaofu.yml")); configurer.setProperties(Objects.requireNonNull(yaml.getObject())); returnconfigurer; } }
可以通過 @Value 注解或 Environment.getProperty() 方法來獲取其中定義的屬性值。
@Slf4j @SpringBootTest publicclassYamlTest{ @Value("${env101.var11}") privateStringvar11; @Test publicvoidmyYamlTest(){ log.info("Yaml配置獲取{}",var11); } }
六、自定義讀取
如果上邊的幾種讀取配置的方式你都不喜歡,就想自己寫個(gè)更流批的輪子,那也很好辦。我們直接注入PropertySources獲取所有屬性的配置隊(duì)列,你是想用注解實(shí)現(xiàn)還是其他什么方式,就可以為所欲為了。
@Slf4j @SpringBootTest publicclassCustomTest{ @Autowired privatePropertySourcespropertySources; @Test publicvoidcustomTest(){ for(PropertySource>propertySource:propertySources){ log.info("自定義獲取配置獲取name{},{}",propertySource.getName(),propertySource.getSource()); } } }
總結(jié)
我們可以通過 @Value 注解、Environment 類、@ConfigurationProperties 注解、@PropertySource 注解等方式來獲取配置信息。
其中,@Value 注解適用于單個(gè)值的注入,而其他幾種方式適用于批量配置的注入。不同的方式在效率、靈活性、易用性等方面存在差異,在選擇配置獲取方式時(shí),還需要考慮個(gè)人編程習(xí)慣和業(yè)務(wù)需求。
如果重視代碼的可讀性和可維護(hù)性,則可以選擇使用 @ConfigurationProperties 注解;如果更注重運(yùn)行效率,則可以選擇使用 Environment 類。總之,不同的場(chǎng)景需要選擇不同的方式,以達(dá)到最優(yōu)的效果。
審核編輯:劉清
-
適配器
+關(guān)注
關(guān)注
8文章
1970瀏覽量
68239 -
YAML
+關(guān)注
關(guān)注
0文章
21瀏覽量
2341 -
SpringBoot
+關(guān)注
關(guān)注
0文章
174瀏覽量
201
原文標(biāo)題:6種方式讀取Springboot的配置
文章出處:【微信號(hào):OSC開源社區(qū),微信公眾號(hào):OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論