1. 6 種方式讀取 Springboot 的配置,老鳥都這么玩(原理+實戰(zhàn))

        共 19358字,需瀏覽 39分鐘

         ·

        2023-06-20 10:37

        大家好,我是小富~

        從配置文件中獲取屬性應(yīng)該是SpringBoot開發(fā)中最為常用的功能之一,但就是這么常用的功能,仍然有很多開發(fā)者在這個方面踩坑。

        我整理了幾種獲取配置屬性的方式,目的不僅是要讓大家學(xué)會如何使用,更重要的是弄清配置加載、讀取的底層原理,一旦出現(xiàn)問題可以分析出其癥結(jié)所在,而不是一報錯取不到屬性,無頭蒼蠅般的重啟項目,在句句臥槽中逐漸抓狂~

        以下示例源碼 Springboot 版本均為 2.7.6

        下邊我們一一過下這幾種玩法和原理,看看有哪些是你沒用過的!話不多說,開始搞~

        一、Environment

        使用 Environment 方式來獲取配置屬性值非常簡單,只要注入Environment類調(diào)用其方法getProperty(屬性key)即可,但知其然知其所以然,簡單了解下它的原理,因為后續(xù)的幾種獲取配置的方法都和它息息相關(guān)。

        @Slf4j
        @SpringBootTest
        public class EnvironmentTest {

            @Resource
            private Environment env;

            @Test
            public void var1Test() {
                String var1 = env.getProperty("env101.var1");
                log.info("Environment 配置獲取 {}", var1);
            }
        }

        1、什么是 Environment?

        Environment 是 springboot 核心的環(huán)境配置接口,它提供了簡單的方法來訪問應(yīng)用程序?qū)傩?,包括系統(tǒng)屬性、操作系統(tǒng)環(huán)境變量、命令行參數(shù)、和應(yīng)用程序配置文件中定義的屬性等等。

        2、配置初始化

        Springboot 程序啟動加載流程里,會執(zhí)行SpringApplication.run中的prepareEnvironment()方法進行配置的初始化,那初始化過程每一步都做了什么呢?

        private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
           DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments)
         
        {
              /** 
              * 1、創(chuàng)建 ConfigurableEnvironment 對象:首先調(diào)用 getOrCreateEnvironment() 方法獲取或創(chuàng)建
              * ConfigurableEnvironment 對象,該對象用于存儲環(huán)境參數(shù)。如果已經(jīng)存在 ConfigurableEnvironment 對象,則直接使用它;否則,根據(jù)用戶的配置和默認配置創(chuàng)建一個新的。
              */

              ConfigurableEnvironment environment = getOrCreateEnvironment();
              /**
              * 2、解析并加載用戶指定的配置文件,將其作為 PropertySource 添加到環(huán)境對象中。該方法默認會解析 application.properties 和 application.yml 文件,并將其添加到 ConfigurableEnvironment 對象中。
              * PropertySource 或 PropertySourcesPlaceholderConfigurer 加載應(yīng)用程序的定制化配置。
              */

              configureEnvironment(environment, applicationArguments.getSourceArgs());
              // 3、加載所有的系統(tǒng)屬性,并將它們添加到 ConfigurableEnvironment 對象中
              ConfigurationPropertySources.attach(environment);
              // 4、通知監(jiān)聽器環(huán)境參數(shù)已經(jīng)準(zhǔn)備就緒
              listeners.environmentPrepared(bootstrapContext, environment);
              /**
              *  5、將默認的屬性源中的所有屬性值移到環(huán)境對象的隊列末尾,
              這樣用戶自定義的屬性值就可以覆蓋默認的屬性值。這是為了避免用戶無意中覆蓋了 Spring Boot 所提供的默認屬性。
              */

              DefaultPropertiesPropertySource.moveToEnd(environment);
              Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
                  "Environment prefix cannot be set via properties.");
              // 6、將 Spring Boot 應(yīng)用程序的屬性綁定到環(huán)境對象上,以便能夠正確地讀取和使用這些配置屬性
              bindToSpringApplication(environment);
              // 7、如果沒有自定義的環(huán)境類型,則使用 EnvironmentConverter 類型將環(huán)境對象轉(zhuǎn)換為標(biāo)準(zhǔn)的環(huán)境類型,并添加到 ConfigurableEnvironment 對象中。
              if (!this.isCustomEnvironment) {
                EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
                environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
              }
              // 8、再次加載系統(tǒng)配置,以防止被其他配置覆蓋
              ConfigurationPropertySources.attach(environment);
              return environment;
        }

        看看它的配置加載流程步驟:

        • 創(chuàng)建 環(huán)境對象 ConfigurableEnvironment 用于存儲環(huán)境參數(shù);
        • configureEnvironment 方法加載默認的 application.propertiesapplication.yml 配置文件;以及用戶指定的配置文件,將其封裝為 PropertySource 添加到環(huán)境對象中;
        • attach(): 加載所有的系統(tǒng)屬性,并將它們添加到環(huán)境對象中;
        • listeners.environmentPrepared(): 發(fā)送環(huán)境參數(shù)配置已經(jīng)準(zhǔn)備就緒的監(jiān)聽通知;
        • moveToEnd(): 將 系統(tǒng)默認 的屬性源中的所有屬性值移到環(huán)境對象的隊列末尾,這樣用戶自定義的屬性值就可以覆蓋默認的屬性值。
        • bindToSpringApplication: 應(yīng)用程序的屬性綁定到 Bean 對象上;
        • attach(): 再次加載系統(tǒng)配置,以防止被其他配置覆蓋;

        上邊的配置加載流程中,各種配置屬性會封裝成一個個抽象的數(shù)據(jù)結(jié)構(gòu) PropertySource中,這個數(shù)據(jù)結(jié)構(gòu)代碼格式如下,key-value形式。


        public abstract class PropertySource<T{
            protected final String name; // 屬性源名稱
            protected final T source; // 屬性源值(一個泛型,比如Map,Property)
            public String getName();  // 獲取屬性源的名字  
            public T getSource()// 獲取屬性源值  
            public boolean containsProperty(String name);  //是否包含某個屬性  
            public abstract Object getProperty(String name);   //得到屬性名對應(yīng)的屬性值   

        PropertySource 有諸多的實現(xiàn)類用于管理應(yīng)用程序的配置屬性。不同的 PropertySource 實現(xiàn)類可以從不同的來源獲取配置屬性,例如文件、環(huán)境變量、命令行參數(shù)等。其中涉及到的一些實現(xiàn)類有:

        關(guān)系圖
        • MapPropertySource: Map 鍵值對的對象轉(zhuǎn)換為 PropertySource 對象的適配器;
        • PropertiesPropertySource: Properties 對象中的所有配置屬性轉(zhuǎn)換為 Spring 環(huán)境中的屬性值;
        • ResourcePropertySource: 從文件系統(tǒng)或者 classpath 中加載配置屬性,封裝成 PropertySource對象;
        • ServletConfigPropertySource: Servlet 配置中讀取配置屬性,封裝成 PropertySource 對象;
        • ServletContextPropertySource: Servlet 上下文中讀取配置屬性,封裝成 PropertySource 對象;
        • StubPropertySource: 是個空的實現(xiàn)類,它的作用僅僅是給 CompositePropertySource 類作為默認的父級屬性源,以避免空指針異常;
        • CompositePropertySource: 是個復(fù)合型的實現(xiàn)類,內(nèi)部維護了 PropertySource集合隊列,可以將多個 PropertySource 對象合并;
        • SystemEnvironmentPropertySource: 操作系統(tǒng)環(huán)境變量中讀取配置屬性,封裝成 PropertySource 對象;

        上邊各類配置初始化生成的 PropertySource 對象會被維護到集合隊列中。

        List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>()

        配置初始化完畢,應(yīng)用程序上下文AbstractApplicationContext會加載配置,這樣程序在運行時就可以隨時獲取配置信息了。

         private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
           ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
           ApplicationArguments applicationArguments, Banner printedBanner)
         
        {
            // 應(yīng)用上下文加載環(huán)境對象
          context.setEnvironment(environment);
          postProcessApplicationContext(context);
            .........
          }

        3、讀取配置

        看明白上邊配置加載的流程,其實讀取配置就容易理解了,無非就是遍歷隊列里的PropertySource,拿屬性名稱name匹配對應(yīng)的屬性值source。

        PropertyResolver是獲取配置的關(guān)鍵類,其內(nèi)部提供了操作PropertySource 隊列的方法,核心方法getProperty(key)獲取配置值,看了下這個類的依賴關(guān)系,發(fā)現(xiàn) Environment 是它子類。

        那么直接用 PropertyResolver 來獲取配置屬性其實也是可以的,到這我們就大致明白了 Springboot 配置的加載和讀取了。

        @Slf4j
        @SpringBootTest
        public class EnvironmentTest {

            @Resource
            private PropertyResolver env;
            
            @Test
            public void var1Test() {
                String var1 = env.getProperty("env101.var1");
                log.info("Environment 配置獲取 {}", var1);
            }
        }

        二、@Value 注解

        @Value注解是Spring框架提供的用于注入配置屬性值的注解,它可用于類的成員變量、方法參數(shù)構(gòu)造函數(shù)參數(shù)上,這個記住很重要!

        在應(yīng)用程序啟動時,使用 @Value 注解的 Bean 會被實例化。所有使用了 @Value 注解的 Bean 會被加入到 PropertySourcesPlaceholderConfigurer 的后置處理器集合中。

        當(dāng)后置處理器開始執(zhí)行時,它會讀取 Bean 中所有 @Value 注解所標(biāo)注的值,并通過反射將解析后的屬性值賦值給標(biāo)有 @Value 注解的成員變量、方法參數(shù)和構(gòu)造函數(shù)參數(shù)。

        需要注意,在使用 @Value 注解時需要確保注入的屬性值已經(jīng)加載到 Spring 容器中,否則會導(dǎo)致注入失敗。

        如何使用

        src/main/resources目錄下的application.yml配置文件中添加env101.var1屬性。

        env101:
          var1: var1-公眾號:程序員小富

        只要在變量上加注解 @Value("${env101.var1}")就可以了,@Value 注解會自動將配置文件中的env101.var1屬性值注入到var1字段中,跑個單元測試看一下結(jié)果。

        @Slf4j
        @SpringBootTest
        public class EnvVariablesTest {

            @Value("${env101.var1}")
            private String var1;
            
            @Test
            public void var1Test(){
                log.info("配置文件屬性: {}",var1);
            }
        }

        毫無懸念,成功拿到配置數(shù)據(jù)。

        雖然@Value注解方式使用起來很簡單,如果使用不當(dāng)還會遇到不少坑。

        1、缺失配置

        如果在代碼中引用變量,配置文件中未進行配值,就會出現(xiàn)類似下圖所示的錯誤。

        為了避免此類錯誤導(dǎo)致服務(wù)啟動異常,我們可以在引用變量的同時給它賦一個默認值,以確保即使在未正確配值的情況下,程序依然能夠正常運行。

        @Value("${env101.var1:我是小富}")
        private String var1;

        2、靜態(tài)變量(static)賦值

        還有一種常見的使用誤區(qū),就是將 @Value 注解加到靜態(tài)變量上,這樣做是無法獲取屬性值的。靜態(tài)變量是類的屬性,并不屬于對象的屬性,而 Spring是基于對象的屬性進行依賴注入的,類在應(yīng)用啟動時靜態(tài)變量就被初始化,此時 Bean還未被實例化,因此不可能通過 @Value 注入屬性值。

        @Slf4j
        @SpringBootTest
        public class EnvVariablesTest {

            @Value("${env101.var1}")
            private static String var1;
            
            @Test
            public void var1Test(){
                log.info("配置文件屬性: {}",var1);
            }
        }

        即使 @Value 注解無法直接用在靜態(tài)變量上,我們?nèi)匀豢梢酝ㄟ^獲取已有 Bean實例化后的屬性值,再將其賦值給靜態(tài)變量來實現(xiàn)給靜態(tài)變量賦值。

        我們可以先通過 @Value 注解將屬性值注入到普通 Bean中,然后在獲取該 Bean對應(yīng)的屬性值,并將其賦值給靜態(tài)變量。這樣,就可以在靜態(tài)變量中使用該屬性值了。

        @Slf4j
        @SpringBootTest
        public class EnvVariablesTest {

            private static String var3;
            
            private static String var4;
            
            @Value("${env101.var3}")
            public void setVar3(String var3) {
                var3 = var3;
            }
            
            EnvVariablesTest(@Value("${env101.var4}") String var4){
                var4 = var4;
            }
            
            public static String getVar4() {
                return var4;
            }

            public static String getVar3() {
                return var3;
            }
        }

        3、常量(final)賦值

        @Value 注解加到final關(guān)鍵字上同樣也無法獲取屬性值,因為 final 變量必須在構(gòu)造方法中進行初始化,并且一旦被賦值便不能再次更改。而 @Value 注解是在 bean 實例化之后才進行屬性注入的,因此無法在構(gòu)造方法中初始化 final 變量。

        @Slf4j
        @SpringBootTest
        public class EnvVariables2Test {

            private final String var6;

            @Autowired
            EnvVariables2Test( @Value("${env101.var6}")  String var6) {

                this.var6 = var6;
            }

            /**
             * @value注解 final 獲取
             */

            @Test
            public void var1Test() {
                log.info("final 注入: {}", var6);
            }
        }

        4、非注冊的類中使用

        只有標(biāo)注了@Component@Service、@Controller、@Repository@Configuration容器管理注解的類,由 Spring 管理的 bean 中使用 @Value注解才會生效。而對于普通的POJO類,則無法使用 @Value注解進行屬性注入。

        /**
         * @value注解 非注冊的類中使用
         * `@Component`、`@Service`、`@Controller`、`@Repository` 或 `@Configuration` 等
         * 容器管理注解的類中使用 @Value注解才會生效
         */

        @Data
        @Slf4j
        @Component
        public class TestService {

            @Value("${env101.var7}")
            private String var7;

            public String getVar7(){
               return this.var7;
            }
        }

        5、引用方式不對

        如果我們想要獲取 TestService 類中的某個變量的屬性值,需要使用依賴注入的方式,而不能使用 new 的方式。通過依賴注入的方式創(chuàng)建 TestService 對象,Spring 會在創(chuàng)建對象時將對象所需的屬性值注入到其中。


          /**
           * @value注解 引用方式不對
           */

          @Test
          public void var7_1Test() {

              TestService testService = new TestService();
              log.info("引用方式不對 注入: {}", testService.getVar7());
          }

        最后總結(jié)一下 @Value注解要在 Bean的生命周期內(nèi)使用才能生效。

        三、@ConfigurationProperties 注解

        @ConfigurationProperties注解是 SpringBoot 提供的一種更加便捷來處理配置文件中的屬性值的方式,可以通過自動綁定和類型轉(zhuǎn)換等機制,將指定前綴的屬性集合自動綁定到一個Bean對象上。

        加載原理

        在 Springboot 啟動流程加載配置的 prepareEnvironment() 方法中,有一個重要的步驟方法 bindToSpringApplication(environment),它的作用是將配置文件中的屬性值綁定到被 @ConfigurationProperties 注解標(biāo)記的 Bean對象中。但此時這些對象還沒有被 Spring 容器管理,因此無法完成屬性的自動注入。

        那么這些Bean對象又是什么時候被注冊到 Spring 容器中的呢?

        這就涉及到了 ConfigurationPropertiesBindingPostProcessor 類,它是 Bean后置處理器,負責(zé)掃描容器中所有被 @ConfigurationProperties 注解所標(biāo)記的 Bean對象。如果找到了,則會使用 Binder 組件將外部屬性的值綁定到它們身上,從而實現(xiàn)自動注入。

        • bindToSpringApplication 主要是將屬性值綁定到 Bean 對象中;
        • ConfigurationPropertiesBindingPostProcessor 負責(zé)在 Spring 容器啟動時將被注解標(biāo)記的 Bean 對象注冊到容器中,并完成后續(xù)的屬性注入操作;

        如何使用

        演示使用 @ConfigurationProperties 注解,在 application.yml 配置文件中添加配置項:

        env101:
          var1: var1-公眾號:程序員小富
          var2: var2-公眾號:程序員小富

        創(chuàng)建一個 MyConf 類用于承載所有前綴為env101的配置屬性。

        @Data
        @Configuration
        @ConfigurationProperties(prefix = "env101")
        public class MyConf {

            private String var1;
            
            private String var2;
        }

        在需要使用var1、var2屬性值的地方,將 MyConf 對象注入到依賴對象中即可。

        @Slf4j
        @SpringBootTest
        public class ConfTest {

            @Resource
            private MyConf myConf;

            @Test
            public void myConfTest() {
                log.info("@ConfigurationProperties注解 配置獲取 {}", JSON.toJSONString(myConf));
            }
        }

        四、@PropertySources 注解

        除了系統(tǒng)默認的 application.yml 或者 application.properties 文件外,我們還可能需要使用自定義的配置文件來實現(xiàn)更加靈活和個性化的配置。與默認的配置文件不同的是,自定義的配置文件無法被應(yīng)用自動加載,需要我們手動指定加載。

        @PropertySources 注解的實現(xiàn)原理相對簡單,應(yīng)用程序啟動時掃描所有被該注解標(biāo)注的類,獲取到注解中指定自定義配置文件的路徑,將指定路徑下的配置文件內(nèi)容加載到 Environment 中,這樣可以通過 @Value 注解或 Environment.getProperty() 方法來獲取其中定義的屬性值了。

        如何使用

        在 src/main/resources/ 目錄下創(chuàng)建自定義配置文件 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")
        })
        public class PropertySourcesConf {

            @Value("${env101.var10}")
            private String var10;

            @Value("${env101.var9}")
            private String var9;
        }

        成功獲取配置了

        但是當(dāng)我試圖加載.yaml文件時,啟動項目居然報錯了,經(jīng)過一番摸索我發(fā)現(xiàn),@PropertySources 注解只內(nèi)置了PropertySourceFactory適配器。也就是說它只能加載.properties文件。

        那如果我想要加載一個.yaml類型文件,則需要自行實現(xiàn)yaml的適配器 YamlPropertySourceFactory。

        public class YamlPropertySourceFactory implements PropertySourceFactory {

            @Override
            public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException {
                YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
                factory.setResources(encodedResource.getResource());

                Properties properties = factory.getObject();

                return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
            }
        }

        而在加載配置時要顯示的指定使用 YamlPropertySourceFactory適配器,這樣就完成了@PropertySource注解加載 yaml 文件。

        @Data
        @Configuration
        @PropertySources({
                @PropertySource(value = "classpath:xiaofu.yaml", encoding = "utf-8", factory = YamlPropertySourceFactory.class)
        })
        public class PropertySourcesConf2 
        {

            @Value("${env101.var10}")
            private String var10;

            @Value("${env101.var9}")
            private String var9;
        }

        五、YamlPropertiesFactoryBean 加載 YAML 文件

        我們可以使用 YamlPropertiesFactoryBean 類將 YAML 配置文件中的屬性值注入到 Bean 中。

        @Configuration
        public class MyYamlConfig {

            @Bean
            public static PropertySourcesPlaceholderConfigurer yamlConfigurer() {
                PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
                YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
                yaml.setResources(new ClassPathResource("xiaofu.yml"));
                configurer.setProperties(Objects.requireNonNull(yaml.getObject()));
                return configurer;
            }
        }

        可以通過 @Value 注解或 Environment.getProperty() 方法來獲取其中定義的屬性值。

        @Slf4j
        @SpringBootTest
        public class YamlTest {

            @Value("${env101.var11}")
            private String var11;

            @Test
            public void  myYamlTest() {
                log.info("Yaml 配置獲取 {}", var11);
            }
        }

        六、自定義讀取

        如果上邊的幾種讀取配置的方式你都不喜歡,就想自己寫個更流批的輪子,那也很好辦。我們直接注入PropertySources獲取所有屬性的配置隊列,你是想用注解實現(xiàn)還是其他什么方式,就可以為所欲為了。

        @Slf4j
        @SpringBootTest
        public class CustomTest {

            @Autowired
            private PropertySources propertySources;

            @Test
            public void customTest() {
                for (PropertySource<?> propertySource : propertySources) {
                    log.info("自定義獲取 配置獲取 name {} ,{}", propertySource.getName(), propertySource.getSource());
                }
            }
        }

        總結(jié)

        我們可以通過 @Value 注解、Environment 類、@ConfigurationProperties 注解、@PropertySource 注解等方式來獲取配置信息。

        其中,@Value 注解適用于單個值的注入,而其他幾種方式適用于批量配置的注入。不同的方式在效率、靈活性、易用性等方面存在差異,在選擇配置獲取方式時,還需要考慮個人編程習(xí)慣和業(yè)務(wù)需求。

        如果重視代碼的可讀性和可維護性,則可以選擇使用 @ConfigurationProperties 注解;如果更注重運行效率,則可以選擇使用 Environment 類。總之,不同的場景需要選擇不同的方式,以達到最優(yōu)的效果。

        我是小富,下期見~

        ··········  END  ··············

        在看、點贊、轉(zhuǎn)發(fā),是對我最大的鼓勵。


        技術(shù)書籍公眾號內(nèi)回復(fù)[ pdf ] Get。


        面試筆記、springcloud進階實戰(zhàn)PDF,公眾號內(nèi)回復(fù)[ 1222 ] Get。



        有幾個技術(shù)群,想進的同學(xué)可以加我好友,備注:進群,一起成長。

        瀏覽 59
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 逼逼网站 | 麻豆乱婬一区二区三区 | 中文字幕精品无码 | 91大神羞羞熊猫视频 | 中文字幕精品av乱喷 |