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 使用。