Springboot基礎知識(05)- 多環境配置(Profile)、配置加載順序和自動配置原理



1. 多環境配置(Profile)

    在實際的項目開發中,一個項目通常會存在多個環境,例如,開發環境、測試環境和生產環境等。不同環境的配置也不盡相同,例如開發環境使用的是開發數據庫,測試環境使用的是測試數據庫,而生產環境使用的是線上的正式數據庫。

    Profile 為在不同環境下使用不同的配置提供了支持,我們可以通過激活、指定參數等方式快速切換環境。

    1) 多 Profile 文件方式

        Spring Boot 的配置文件共有兩種形式:.properties  文件和 .yml 文件,不管哪種形式,它們都能通過文件名的命名形式區分出不同的環境的配置,文件命名格式為:

            application-{profile}.properties 或 application-{profile}.yml
        
        其中,{profile} 一般為各個環境的名稱或簡稱,例如 dev、test 和 prod 等等。

        (1) properties 配置

            在上文 SpringbootBasic 的 src/main/resources 下添加 4 個配置文件:

                application.properties:主配置文件
                application-dev.properties:開發環境配置文件
                application-test.properties:測試環境配置文件
                application-prod.properties:生產環境配置文件

            在 application.properties 文件中,配置如下。

                # 默認環境
                user.username=admin-default
                user.age=10

                # 激活指定的 profile
                spring.profiles.active=prod

            在 application-dev.properties 文件中,配置如下。

                # 開發環境
                user.username=admin-dev
                user.age=11

            在 application-test.properties 文件中,配置如下。

                # 測試環境
                user.username=admin-test
                user.age=12

            在 application-prod.properties 文件中,配置如下。

                # 生產環境
                user.username=admin-prod
                user.age=13
        
        (2) yml 配置

            與 properties 文件類似,我們也可以添加 4 個配置文件:

                application.yml:默認配置
                application-dev.yml:開發環境配置
                application-test.yml:測試環境配置
                application-prod.yml:生產環境配置

            在 application.yml 文件中,配置如下。

                # 默認環境
                user:
                    username: admin-default
                    age: 10

                # 激活指定的 profile
                spring:
                    profiles:
                        active: test

            在 application-dev.yml 文件中,配置如下。

                # 開發環境
                user:
                    username: admin-dev
                    age: 11

            在 application-test.yml 文件中,配置如下。

                # 測試環境
                user:
                    username: admin-test
                    age: 12

            在 application-prod.yml 文件中,配置如下。

                # 生產環境
                user:
                    username: admin-prod
                    age: 13

    2) 多 Profile 文檔塊模式

        在 YAML 配置文件中,可以使用 “---” 把配置文件分割成了多個文檔塊,因此我們可以在不同的文檔塊中針對不同的環境進行不同的配置,並在第一個文檔塊內對配置進行切換。

        修改 application.yml,配置多個文檔塊,並在第一文檔快內激活測試環境的 Profile,代碼如下。

            # 默認環境
            user:
                username: admin-default
                age: 10
            # 激活指定的 profile
            spring:
                profiles:
                    active: test
            ---
            # 開發環境
            user:
                username: admin-dev
                age: 11            
            spring:
                config:
                    activate:
                        on-profile: dev
            ---
            # 測試環境
            user:
                username: admin-test
                age: 12
            spring:
                config:
                    activate:
                        on-profile: test
            ---
            # 生產環境
            user:
                username: admin-prod
                age: 13
            spring:
                config:
                    activate:
                        on-profile: prod

    3) 激活 Profile

        除了可以在配置文件中激活指定 Profile,Spring Boot 還為我們提供了另外 2 種激活 Profile 的方式:

            程序運行參數激活
            虛擬機參數激活

        (1) 程序運行參數激活
           
            這里把 “Spring基礎知識(23)- Spring Boot (四)” 里 “2. 默認配置文件” 修改過的 SpringbootBasic 項目打包成 JAR 文件,並在通過命令行運行時,配置命程序運行參數,激活指定的 Profile。

            點擊 IDEA 底部 Terminal 標簽頁,執行如下命令。

                java -jar target/SpringbootBasic-1.0-SNAPSHOT.jar --spring.profiles.active=dev

            Terminal 輸出:
            
                User {username = admin-dev, age = 11}

            注:IDEA Run/Debug Configuration 時,可以設置 Program arguments: --spring.profiles.active=dev

        (2) 虛擬機參數激活

            點擊 IDEA 底部 Terminal 標簽頁,執行如下命令。

                java -jar target/SpringbootBasic-1.0-SNAPSHOT.jar -Dspring.profiles.active=prod

            Terminal 輸出:

                User {username = admin-prod, age = 13}

            注:IDEA Run/Debug Configuration 時,可以設置 VM options: -Dspring.profiles.active=prod


2. 配置加載順序

    Spring Boot 不僅可以通過配置文件進行配置,還可以通過環境變量、命令行參數等多種形式進行配置。這些配置都可以讓開發人員在不修改任何代碼的前提下,直接將一套 Spring Boot  應用程序在不同的環境中運行。

    1) Spring Boot 配置優先級

        以下是常用的 Spring Boot 配置形式及其加載順序(優先級由高到低):

            (1) 命令行參數
            (2) 來自 java:comp/env 的 JNDI 屬性
            (3) Java 系統屬性(System.getProperties())
            (4) 操作系統環境變量
            (5) RandomValuePropertySource 配置的 random.* 屬性值
            (6) 配置文件(YAML 文件、Properties 文件)
            (7) @Configuration 注解類上的 @PropertySource 指定的配置文件
            (8) 通過 SpringApplication.setDefaultProperties 指定的默認屬性

        以上所有形式的配置都會被加載,當存在相同配置內容時,高優先級的配置會覆蓋低優先級的配置;存在不同的配置內容時,高優先級和低優先級的配置內容取並集,共同生效,形成互補配置。

    2) 命令行參數

        Spring Boot 中的所有配置,都可以通過命令行參數進行指定,其配置形式如下。

            java -jar {Jar文件名} --{參數1}={參數值1} --{參數2}={參數值2}

        示例,點擊 IDEA 底部 Terminal 標簽頁,執行如下命令。

            java -jar target/SpringbootBasic-1.0-SNAPSHOT.jar --user.username=admin-param --user.age=36  

        Terminal 輸出:

            User {username = admin-param, age = 36}

    3) 配置文件

        Spring Boot 啟動時,會自動加載 JAR 包內部及 JAR 包所在目錄指定位置的配置文件(Properties 文件、YAML 文件),同一位置下,Properties 文件優先級高於 YAML 文件。

        Spring Boot 配置文件的優先級順序,遵循以下規則:

            (1) 先加載 JAR 包外的配置文件,再加載 JAR 包內的配置文件;
            (2) 先加載 config 目錄內的配置文件,再加載 config 目錄外的配置文件;
            (3) 先加載 config 子目錄下的配置文件,再加載 config 目錄下的配置文件;
            (4) 先加載 appliction-{profile}.properties (或 appliction-{profile}.yml),再加載 application.properties(或 application.yml);
            (5) 先加載 .properties 文件,再加載 .yml 文件。


3. 自動配置原理

    Spring Boot 項目創建完成后,即使不進行任何的配置,也能夠順利地運行,這都要歸功於 Spring Boot 的自動化配置。

    Spring Boot 默認使用 application.properties 或 application.yml 作為其全局配置文件,我們可以在該配置文件中對各種自動配置屬性(server.port、logging.level.* 、spring.config.active.no-profile 等等)進行修改,並使之生效。

    1) Spring Factories 機制

        Spring Boot 的自動配置是基於 Spring `Factories` 機制實現的。

        Spring Factories 機制是 Spring Boot 中的一種服務發現機制,這種擴展機制與 Java SPI 機制十分相似。Spring Boot 會自動掃描所有 Jar 包類路徑下 META-INF/spring.factories 文件,並讀取其中的內容,進行實例化,這種機制也是 Spring Boot Starter 的基礎。

        spring.factories 文件本質上與 properties 文件相似,其中包含一組或多組鍵值對(key=vlaue),其中,key 的取值為接口的完全限定名;value 的取值為接口實現類的完全限定名,一個接口可以設置多個實現類,不同實現類之間使用 “,” 隔開,例如:

            org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
            org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
            org.springframework.boot.autoconfigure.condition.OnClassCondition,\
            org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

            注意:文件中配置的內容過長,為了閱讀方便而手動換行時,為了防止內容丟失可以使用“\”。

    2) Spring Factories 實現原理

        spring-core 包里定義了 SpringFactoriesLoader 類,這個類會掃描所有 Jar 包類路徑下的 META-INF/spring.factories 文件,並獲取指定接口的配置。在 SpringFactoriesLoader 類中定義了兩個對外的方法,如下表。

方法 描述
<T>
List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader)
靜態方法;根據接口獲取其實現類的實例;該方法返回的是實現類對象列表。
List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) 公共靜態方法;根據接口l獲取其實現類的名稱;該方法返回的是實現類的類名的列表


        以上兩個方法的關鍵都是從指定的 ClassLoader 中獲取 spring.factories 文件,並解析得到類名列表。

            (1) loadFactories() 方法能夠獲取指定接口的實現類對象;
            (2) loadFactoryNames() 方法能夠根據接口獲取其實現類類名的集合;

    3) 自動配置的加載

        Spring Boot 自動化配置也是基於 Spring Factories 機制實現的,在 spring-boot-autoconfigure-xxx.jar 類路徑下的 META-INF/spring.factories 中設置了 Spring Boot 自動配置的內容 ,如下。

 1             # Auto Configure
 2             org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 3             org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
 4             org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
 5             org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
 6             org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
 7             org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
 8             org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
 9 
10             ...


        以上配置中,value 取值是由多個 *.xxxAutoConfiguration (使用逗號分隔)組成,每個 xxxAutoConfiguration 都是一個自動配置類。Spring Boot 啟動時,會利用 Spring-Factories 機制,將這些 xxxAutoConfiguration 實例化並作為組件加入到容器中,以實現 Spring Boot 的自動配置

        (1) @SpringBootApplication 注解

            所有 Spring Boot 項目的主啟動程序類上都使用了一個 @SpringBootApplication 注解,該注解是 Spring Boot 中最重要的注解之一 ,也是 Spring Boot 實現自動化配置的關鍵。

            @SpringBootApplication 是一個組合元注解,其主要包含兩個注解:@SpringBootConfiguration 和 @EnableAutoConfiguration,其中 @EnableAutoConfiguration 注解是 SpringBoot 自動化配置的核心所在。

        (2) @EnableAutoConfiguration 注解

            @EnableAutoConfiguration 注解用於開啟 Spring Boot 的自動配置功能,它使用 Spring 框架提供的 @Import 注解通過 AutoConfigurationImportSelector類(選擇器)給容器中導入自動配置組件。

        (3) AutoConfigurationImportSelector 類

            AutoConfigurationImportSelector 類實現了 DeferredImportSelector 接口,AutoConfigurationImportSelector 中還包含一個靜態內部類 AutoConfigurationGroup,它實現了 DeferredImportSelector 接口的內部接口 Group(Spring 5 新增)。

            AutoConfigurationImportSelector 類中包含 3 個方法,如下表。

方法 描述
Class<? extends Group> getImportGroup() 該方法獲取實現了 Group 接口的類,並實例化

void process(AnnotationMetadata annotationMetadata,DeferredImportSelector deferredImportSelector)

該方法用於引入自動配置的集合
Iterable<Entry> selectImports() 遍歷自動配置類集合(Entry 類型的集合),並逐個解析集合中的配置類


            各方法執行順序如下。

                a) getImportGroup() 方法

                    AutoConfigurationImportSelector 類中 getImportGroup() 方法主要用於獲取實現了 DeferredImportSelector.Group 接口的類

                b) process() 方法

                    靜態內部類 AutoConfigurationGroup 中的核心方法是 process(),該方法通過調用 getAutoConfigurationEntry() 方法讀取 spring.factories 文件中的內容,獲得自動配置類的集合。

                    getAutoConfigurationEntry() 方法通過調用 getCandidateConfigurations() 方法來獲取自動配置類的完全限定名,並在經過排除、過濾等處理后,將其緩存到成員變量中。

                    在 getCandidateConfigurations() 方法中,根據 Spring Factories 機制調用 SpringFactoriesLoader 的 loadFactoryNames() 方法,根據 EnableAutoConfiguration.class (自動配置接口)獲取其實現類(自動配置類)的類名的集合。

                c) selectImports() 方法

                    以上所有方法執行完成后,AutoConfigurationImportSelector.AutoConfigurationGroup#selectImports() 會將 process() 方法處理后得到的自動配置類,進行過濾、排除,最后將所有自動配置類添加到容器中。

    4) 自動配置的生效和修改

        spring.factories 文件中的所有自動配置類(xxxAutoConfiguration),都是必須在一定的條件下才會作為組件添加到容器中,配置的內容才會生效。這些限制條件在 Spring Boot 中以 @Conditional 派生注解的形式體現,如下表。

注解 生效條件
@ConditionalOnJava 應用使用指定的 Java 版本時生效
@ConditionalOnBean 容器中存在指定的  Bean 時生效
@ConditionalOnMissingBean 容器中不存在指定的 Bean 時生效
@ConditionalOnExpression 滿足指定的 SpEL 表達式時生效
@ConditionalOnClass 存在指定的類時生效
@ConditionalOnMissingClass 不存在指定的類時生效
@ConditionalOnSingleCandidate 容器中只存在一個指定的 Bean 或這個 Bean 為首選 Bean 時生效
@ConditionalOnProperty 系統中指定屬性存在指定的值時生效
@ConditionalOnResource 類路徑下存在指定的資源文件時生效
@ConditionalOnWebApplication 當前應用是 web 應用時生效
@ConditionalOnNotWebApplication 當前應用不是 web 應用生效


        下面我們以 ServletWebServerFactoryAutoConfiguration 為例,介紹 Spring Boot 自動配置是如何生效的。

        (1) ServletWebServerFactoryAutoConfiguration

            ServletWebServerFactoryAutoConfiguration 代碼如下。

 1                 // 表示這是一個配置類,與 xml 配置文件等價,也可以給容器中添加組件
 2                 @Configuration(proxyBeanMethods = false) 
 3                 @AutoConfigureOrder(-2147483648)
 4                 // 判斷當前項目有沒有 ServletRequest 這個類
 5                 @ConditionalOnClass({ServletRequest.class}) 
 6                 // 判斷當前應用是否是 web 應用,如果是,當前配置類生效 
 7                 @ConditionalOnWebApplication(type = Type.SERVLET)
 8                 // 將配置文件中對應的值和 ServerProperties 綁定起來
 9                 @EnableConfigurationProperties({ServerProperties.class}) 
10                 @Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
11                         EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class})
12                 public class ServletWebServerFactoryAutoConfiguration {
13 
14                     public ServletWebServerFactoryAutoConfiguration() {
15                     }
16 
17                     @Bean // 給容器中添加一個組件,這個組件的某些值需要從 properties 中獲取
18                     public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties, 
19                         ObjectProvider<WebListenerRegistrar> webListenerRegistrars) {
20                         ...
21                     }
22 
23                     @Bean
24                     @ConditionalOnClass(name = {"org.apache.catalina.startup.Tomcat"})
25                     public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
26                         ServerProperties serverProperties) {
27                         ...
28                     }
29 
30                     @Bean
31                     @ConditionalOnMissingFilterBean({ForwardedHeaderFilter.class})
32                     @ConditionalOnProperty(value = {"server.forward-headers-strategy"}, havingValue = "framework")
33                     public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
34                         ...
35                     }
36 
37                     public static class BeanPostProcessorsRegistrar implements BeanFactoryAware,
38                         ImportBeanDefinitionRegistrar {
39                         
40                         private ConfigurableListableBeanFactory beanFactory;
41 
42                         public BeanPostProcessorsRegistrar() {
43                         }
44 
45                         public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
46                             ...
47                         }
48 
49                         public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
50                                                             BeanDefinitionRegistry registry) {
51                             ...
52                         }
53 
54                         private <T> void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, 
55                             String name, Class<T> beanClass, Supplier<T> instanceSupplier) {
56                             ...
57                         }
58                     }
59                 } 


            該類使用了以下注解:

                @Configuration:用於定義一個配置類,可用於替換 Spring 中的 xml 配置文件;
                @Bean:被 @Configuration 注解的類內部,可以包含有一個或多個被 @Bean 注解的方法,用於構建一個 Bean,並添加到 Spring 容器中;該注解與 spring 配置文件中 <bean> 等價,方法名與 <bean> 的 id 或 name 屬性等價,方法返回值與 class 屬性等價;

            除了 @Configuration 和 @Bean 注解外,該類還使用 5 個 @Conditional 衍生注解:

                @ConditionalOnClass({ServletRequest.class}):判斷當前項目是否存在 ServletRequest 這個類,若存在,則該配置類生效。
                @ConditionalOnWebApplication(type = Type.SERVLET):判斷當前應用是否是 Web 應用,如果是的話,當前配置類生效。
                @ConditionalOnClass(name = {"org.apache.catalina.startup.Tomcat"}):判斷是否存在 Tomcat 類,若存在則該方法生效。
                @ConditionalOnMissingFilterBean({ForwardedHeaderFilter.class}):判斷容器中是否有 ForwardedHeaderFilter 這個過濾器,若不存在則該方法生效。
                @ConditionalOnProperty(value = {"server.forward-headers-strategy"},havingValue = "framework"):判斷配置文件中是否存在 server.forward-headers-strategy = framework,若不存在則該方法生效。

        (2) ServerProperties

            ServletWebServerFactoryAutoConfiguration 類還使用了一個 @EnableConfigurationProperties 注解,通過該注解導入了一個 ServerProperties 類,其部分源碼如下。

 1                 @ConfigurationProperties(
 2                     prefix = "server",
 3                     ignoreUnknownFields = true
 4                 )
 5                 public class ServerProperties {
 6                     private Integer port;
 7                     private InetAddress address;
 8                     @NestedConfigurationProperty
 9                     private final ErrorProperties error = new ErrorProperties();
10                     private ServerProperties.ForwardHeadersStrategy forwardHeadersStrategy;
11                     private String serverHeader;
12 
13                     ...
14 
15                     public ServerProperties() {
16                         ...
17                     }
18 
19                     ...
20                 }


            ServletWebServerFactoryAutoConfiguration 使用了一個 @EnableConfigurationProperties 注解,而 ServerProperties 類上則使用了一個 @ConfigurationProperties 注解。這其實是 Spring Boot 自動配置機制中的通用用法。

            Spring Boot 中為我們提供了大量的自動配置類 XxxAutoConfiguration 以及 XxxProperties,每個自動配置類 XxxAutoConfiguration 都使用了 @EnableConfigurationProperties 注解,而每個 XxxProperties 上都使用 @ConfigurationProperties 注解。

            @ConfigurationProperties 注解的作用,是將這個類的所有屬性與配置文件中相關的配置進行綁定,以便於獲取或修改配置,但是 @ConfigurationProperties 功能是由容器提供的,被它注解的類必須是容器中的一個組件,否則該功能就無法使用。而 @EnableConfigurationProperties 注解的作用正是將指定的類以組件的形式注入到 IOC 容器中,並開啟其 @ConfigurationProperties 功能。因此,@ConfigurationProperties + @EnableConfigurationProperties 組合使用,便可以為 XxxProperties 類實現配置綁定功能。

            自動配置類 XxxAutoConfiguration 負責使用 XxxProperties 中屬性進行自動配置,而 XxxProperties 則負責將自動配置屬性與配置文件的相關配置進行綁定,以便於用戶通過配置文件修改默認的自動配置。也就是說,真正“限制”我們可以在配置文件中配置哪些屬性的類就是這些 XxxxProperties 類,它與配置文件中定義的 prefix 關鍵字開頭的一組屬性是唯一對應的。

            注意:XxxAutoConfiguration 與 XxxProperties 並不是一一對應的,大多數情況都是多對多的關系,即一個 XxxAutoConfiguration 可以同時使用多個 XxxProperties 中的屬性,一個 XxxProperties 類中屬性也可以被多個 XxxAutoConfiguration 使用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM