美團二面:SpringBoot讀取配置優先順序順序是什麼?

码农Academy發表於2024-05-09

引言

Spring Boot作為一種輕量級的Java應用程式框架,以其開箱即用、快速搭建新專案的特性贏得了廣大開發者的青睞。其核心理念之一就是簡化配置過程,使開發者能夠快速響應複雜多變的生產環境需求。為了實現這一點,Spring Boot支援豐富的外部化配置機制,允許應用程式根據不同的部署環境靈活載入相應的配置屬性,而無需修改程式碼本身。

在Spring Boot生態系統中,配置屬性可以從各種來源獲取,比如:Java屬性檔案、YAML檔案、環境變數、命令列引數等。這些配置屬效能夠在執行時動態注入到Bean中,極大地提高了系統的可擴充套件性和可配置性。然而,為了確保一致性和防止配置衝突,Spring Boot在載入這些外部配置時遵循一套嚴格的優先順序順序。掌握這套優先順序規則至關重要,因為它直接影響著最終生效的配置屬性值,進而決定了應用程式的行為模式。

本文將深入探討Spring Boot載入外部配置屬性的優先順序規則,詳盡梳理各個配置源的載入順序,並結合實際應用場景舉例說明,以便我們能夠更高效地管理和遷移配置,確保在不同環境下應用程式都能穩定、準確地執行。

Spring Boot外部化配置概述

Spring Boot的核心價值之一在於其強大的外部化配置能力,這使得應用程式能夠在不改變程式碼的情況下適應不同的執行環境。外部化配置意味著將應用程式的關鍵配置資訊移至應用程式程式碼之外,便於根據不同環境(如開發、測試、生產等)進行定製化配置。Spring Boot提供了多樣化的外部配置源以及便捷的屬性注入方式,使得這種配置機制變得異常靈活且易於管理。

多樣化配置源

Spring Boot支援多種型別的外部配置源,主要有如下幾個方面:

  1. Properties檔案:
    通常使用.properties格式,採用鍵值對的形式儲存配置資訊。
server.port=8080
logging.level.root=DEBUG
  1. YAML檔案:
    相較於傳統的properties檔案,YAML提供了更直觀、層次更分明的資料結構,尤其適合儲存複雜配置。使用.yml格式。
server:
  port: 8080
logging:
  level:
    root: DEBUG
  1. 環境變數
    作業系統級別的環境變數可以被Spring Boot識別並作為配置源,這對於雲環境和容器化部署尤為實用。

  2. 命令列引數
    啟動Spring Boot應用時,可以傳入命令列引數(以--開頭)直接覆蓋已有配置。

屬性注入方式

在Spring Boot中,外部配置的屬性值可以透過以下幾種方式方便地注入到Bean中。

  • @Value註解:可以直接在欄位或方法引數上使用此註解,將配置屬性值注入到目標物件中。

  • Environment介面:Spring框架提供的環境抽象類,可以用來查詢所有已載入的配置資訊。

  • @ConfigurationProperties註解:用於繫結一組相關配置到一個專門的Java Bean中,提供更結構化的配置管理方式。

配置載入優先順序

Spring Boot對來自不同配置源的同名屬性可以按照一定的優先順序順序進行覆蓋。其優先順序從上到下變高,即後面的配置源將覆蓋前面的配置源。

  1. 預設屬性(透過SpringApplication.setDefaultProperties方法設定)
  2. @PropertySource註解載入的配置
  3. Config Data(配置資料)(本地檔案系統或打包在jar中的application.properties和application-{profile}.properties)
  4. 特殊屬性源(如隨機數生成器、環境變數、系統屬性、JNDI屬性等)
  5. Servlet容器相關的初始化引數
  6. SPRING_APPLICATION_JSON格式的環境變數或系統屬性
  7. 命令列引數
  8. 測試相關的屬性注入方式(如@SpringBootTest@DynamicPropertySource@TestPropertySource

以上優先順序順序來源於官網:Spring Boot Reference Documentation

Spring Boot配置載入順序詳解

預設屬性

預設屬性是指Spring Boot框架內建的一些預設配置值。可以在建立SpringApplication例項時,透過呼叫setDefaultProperties(Map<String, Object> defaultProperties)方法來提供一組預設屬性,這些屬性將被優先載入,但是也會被其他配置覆蓋。

@SpringBootApplication
public class SpringBootBaseApplication {

	public static void main(String[] args) {
		Map<String, Object> defaultProperties = new HashMap<>();
		defaultProperties.put("server.port", "9000"); // 自定義預設埠
		SpringApplication app = new SpringApplication(SpringBootBaseApplication.class);
		app.setDefaultProperties(defaultProperties);
		app.run(args);
	}
}

image.png

@PropertySource註解

@PropertySource註解用於在Spring Boot的@Configuration類上載入外部屬性檔案。當我們在配置類上使用@PropertySource時,需要注意的是,這些屬性源並不會立即被新增到Spring的Environment中。它們是在Spring應用上下文重新整理(refresh)階段才會被真正載入併合併到環境變數中。

有興趣的可以跟一下原始碼,org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors中執行的。

Spring Boot的主引導配置,如伺服器埠(server.port)、日誌框架的初始化(例如日誌級別設定)等,也是在應用上下文重新整理之前就被讀取並應用的。因此,對於這類早期就需要讀取的配置,應該直接在application.properties或者環境變數等更早被載入的配置源中進行設定。

我們建立一個propertysource.properties檔案:

server.port = 9001
coderacademy.name = CoderAcademy

然後我們在@Configuration配置上使用@PropertySource匯入propertysource.properties檔案。

@PropertySource(value = "classpath:propertysource.properties")
@Configuration
public class MyConfig {

}

我們在應用啟動後看一下上述配置:

@SpringBootApplication
public class SpringBootBaseApplication {

	public static void main(String[] args) {
		Map<String, Object> defaultProperties = new HashMap<>();
		defaultProperties.put("server.port", "9000"); // 自定義預設埠
		SpringApplication app = new SpringApplication(SpringBootBaseApplication.class);
		app.setDefaultProperties(defaultProperties);
		ConfigurableApplicationContext context = app.run(args);
		Environment environment = context.getEnvironment();
		System.out.println("coderacademy.name: " + environment.getProperty("coderacademy.name"));
	}
}

列印結果:
image.png

可以看出server.port變成了9001,即@PropertySource載入的配置覆蓋了SpringBoot預設的屬性值。

Config Data(配置資料)

Config Data(配置資料)是Spring Boot中用於外部化應用配置的核心部分。主要由內部配置檔案以及外部配置檔案。

內部配置檔案

內部配置檔案最基礎的應用配置檔案,位於專案構建後的jar包內部。位於src/main/resource目錄下的檔案。

image.png

外部配置檔案

可以將配置檔案放在jar包外面的某個路徑下。這種方式有助於在不修改jar包的情況下變更配置。比如我們使用的配置中心(nacosapollo等),也可以透過spring.config.location或者spring.config.additional-location指定的檔案等。

SpringBoot在啟動時會預設從特定的目錄中載入這些配置檔案。我們可以從ConfigDataEnvironment中找到這些目錄:

image.png

其目錄的載入順序由低到高為:

file:./
file:./config/
file:./config/*/
classpath:/
classpath:/config/

其中file代表應用根目錄下的檔案,而classpathresources下的檔案。

這些配置檔案的配置優先順序順序由低到高為:

classpath:/
classpath:/config/
file:./
file:./config/
file:./config/*/

本例基於SpringBoot2.7版本。
關於SpringBoot載入內部配置檔案的執行流程以及原理,請參考:
華為二面:SpringBoot讀取_配置檔案_的原理是什麼?載入順序是什麼?

我們分別在這些目錄下建立配置檔案application.properties

我們在對應檔案中寫入他們的目錄路徑:

1: config.data.path = classpath:./
2: config.data.path = classpath:./config/
3: config.data.path = file:./
4: config.data.path = file:./config/
5: config.data.path = file:./config/dev

我們在SpringBoot啟動時列印config.data.path的值:

@SpringBootApplication
public class SpringBootConfigApplication {

	public static void main(String[] args) {
		Map<String, Object> defaultProperties = new HashMap<>();
		defaultProperties.put("server.port", "9000"); // 自定義預設埠
		SpringApplication app = new SpringApplication(SpringBootConfigApplication.class);
		app.setDefaultProperties(defaultProperties);
		ConfigurableApplicationContext context = app.run(args);
		Environment environment = context.getEnvironment();
		System.out.println("config.data.path: " + environment.getProperty("config.data.path"));
	}
}

我們分步進行驗證,先驗證1,2,列印結果:

config.data.path: classpath:./config/

繼續驗證1,2,3,列印結果:

config.data.path: file:./

驗證1,2,3,4,列印結果:

config.data.path: file:./config/

驗證1,2,3,4,5,列印結果:

config.data.path: file:./config/dev

隨機值屬性源

RandomValuePropertySource 在Spring Boot中,RandomValuePropertySource是一個特殊屬性源,它並不來源於固定的配置檔案或環境變數,而是由Spring Boot框架在啟動時自動新增。這個屬性源提供的屬性名以random.*開頭,可以用於生成隨機值。例如,你可以在配置檔案中引用random.intrandom.long等屬性,Spring Boot在啟動時會為這些屬性生成隨機整數值。這對於需要在執行時生成一些臨時或隨機值的場景非常有用,如臨時密碼、快取金鑰等。

比如我們在application.properties中設定random.int=100

random.int=100

我們在SpringBoot啟動時獲取``random.int`的值:

@SpringBootApplication
public class ConfigApplication
{
    public static void main( String[] args )
    {
        SpringApplication app = new SpringApplication(ConfigApplication.class);
        ConfigurableApplicationContext context = app.run(args);
        Environment environment = context.getEnvironment();
        System.out.println("random.int: " + environment.getProperty("random.int"));
    }
}

列印結果為:

random.int: -510589238

並且每次重新啟動應用,列印的結果都不一樣。

作業系統環境變數

在Spring Boot中,環境變數可以用作配置源,Spring Boot會自動檢測並載入這些環境變數作為應用的配置屬性。例如,如果在作業系統中設定了環境變數MY_APP_PORT=8080,那麼在Spring Boot應用中可以透過${MY_APP_PORT}來引用這個值。

我們設定環境變數為config.data.path=環境變數:

image.png

我們啟動引用,依然列印config.data.path的結果為:

config.data.path: 環境變數

Java系統屬性

Java系統屬性是透過System.setProperty()方法設定一系列鍵值對。

@SpringBootApplication
public class ConfigApplication
{
    static {
        System.setProperty("config.data.path", "SystemProperty"); // 設定系統屬性
    }

    public static void main( String[] args )
    {
        SpringApplication app = new SpringApplication(ConfigApplication.class);
        ConfigurableApplicationContext context = app.run(args);
        Environment environment = context.getEnvironment();
        System.out.println("config.data.path: " + environment.getProperty("config.data.path"));
    }
}

列印結果為:

config.data.path: SystemProperty

SPRING_APPLICATION_JSON環境變數中的內嵌JSON屬性

SPRING_APPLICATION_JSON 是 Spring Boot 提供的一種機制,允許透過環境變數傳遞 JSON 格式的配置給應用程式。這個環境變數的內容會被解析成一個 JSON 物件,併合併到Spring的Environment中,就像其他屬性源一樣。

@SpringBootApplication
public class ConfigApplication
{
    static {
        System.setProperty("config.data.path", "SystemProperty"); // 設定系統屬性
        System.setProperty("SPRING_APPLICATION_JSON", "{\"config.data.path\":\"SPRING_APPLICATION_JSON環境變數中的內嵌JSON屬性\"}");
    }

    public static void main( String[] args )
    {
        SpringApplication app = new SpringApplication(ConfigApplication.class);
        ConfigurableApplicationContext context = app.run(args);
        Environment environment = context.getEnvironment();
        System.out.println("config.data.path: " + environment.getProperty("config.data.path"));
    }
}

列印結果:

config.data.path: SPRING_APPLICATION_JSON環境變數中的內嵌JSON屬性

命令列引數

啟動Spring Boot應用時,可以直接透過命令列引數來覆蓋或設定配置屬性。命令列引數通常以--開頭,後面緊跟屬性名和值,如--server.port=8080。這種方式可以在不修改配置檔案的前提下臨時調整應用配置。命令列引數具有較高的優先順序,可以覆蓋其它配置源中的屬性值。

我們使用java -jar啟動SpringBoot:

java -jar ./springboot-config-1.0-SNAPSHOT.jar --config.data.path=命令列引數

列印結果為:

config.data.path: 命令列引數

關於SpringBoot的jar包,可以透過java -jar命令直接執行的原因請參考:位元組二面:為什麼SpringBoot的 jar 可以直接執行?我說內嵌了Tomcat容器,他讓我出門左轉

至於其他的跟單測有關的配置,我們就不一一細說了

總結

Spring Boot配置載入優先順序的設計具有深遠的實際意義和重要性。這一機制確保了應用在不同環境和部署場景下的高度靈活性和可移植性,同時也極大提升了開發和運維團隊的生產力和協同效率。

優先順序順序的嚴謹性使得開發者能夠精細地控制配置的覆蓋層級,從而使同一份程式碼可以根據不同環境的需求載入不同的配置屬性。例如,在開發、測試和生產環境中分別啟用不同的資料庫連線、日誌級別或API金鑰等敏感資訊,而無需在程式碼中硬編碼。

Spring Boot的配置載入流程首先考慮了預設配置,然後逐步載入使用者透過@PropertySource註解引入的屬性源、打包在jar包內外的各種application.properties和application-{profile}.properties或YAML檔案、環境變數、系統屬性,直至命令列引數等。這種分層載入策略確保了越靠後的配置源擁有更高的優先順序,從而可以覆蓋之前的配置,這也體現了配置的靈活性和即時性。

理解併合理運用Spring Boot配置載入的優先順序,對於保障應用的安全性、可維護性以及降低部署複雜度至關重要。特別是在大規模微服務架構中,合理的配置管理和遷移對於整體系統的穩定性有著不可忽視的作用。透過對配置優先順序的深入掌控,開發者和運維人員能夠輕鬆應對複雜環境下的配置管理挑戰,使得Spring Boot應用具備良好的擴充套件性和適應性。

本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等

相關文章