作為 Java
程式設計師,相信大家都知道,我們日常的 SpringBoot
專案會有一個配置檔案 application.properties
檔案。
裡面會配置很多引數,例如服務的埠等,這些都只是預設值,在不改變配置檔案裡面內容的情況下,我們可以透過在部署的時候,傳遞一個相應的引數來替換預設的引數。
那麼問題來了,你有想過為什麼可以這樣嗎?為什麼 SpringBoot
部署時傳遞的啟動配置會生效,而配置檔案中的配置就不生效了呢?或者說這兩者的優先順序是什麼樣子的呢?
外部化配置
要解釋上面的問題,我們就需要知道 SpringBoot
到底支援哪些配置形式,以及這些配置方式的優先順序是什麼樣子的,只有搞清楚了這個,才能真正的解決配置的優先順序問題。
在 SpringBoot
的官方文件中我們可以看到這麼一段描述
用了不起我拙劣的英語翻譯一下,大概的意思就是:Spring Boot
提供了將配置檔案外部化的功能,這樣您就可以在不同環境下使用相同的應用程式程式碼。您可以使用 properties
檔案、YAML
檔案、環境變數以及命令列引數來外部化配置檔案。
透過 @Value
註解,屬性值可以直接注入到 beans
中,透過 Environment abstraction
(環境對映)可以訪問其他位置,或者使用 @ConfigurationProperties
繫結結構化物件。
有哪些外部配置
既然上面提到了 SpringBoot
提供了外部化配置,那麼 SpringBoot
提供了哪些配置呢?依然是透過官方文件,我們可以看到有如下配置列表
從上圖可以看到 SpringBoot
總共內建了 17 種外部化配置方法,而且這 17 種的優先順序是從上到下依次優先的。這些方式中我們常用的有 4 命令列方法,9 Java 系統環境變數,10 作業系統環境變數,以及 12 到 15 到配置檔案的形式。
透過上面的順序我們就可以解釋為什麼我們透過命令列配置的引數會生效,而配置檔案中的預設值就會忽略了,從而達到了覆蓋配置的目的。
PropertySource
上面的文件中也提到了,SpringBoot
主要是透過 PropertySource
機制來實現多樣屬性源的,SpringBoot
的 PropertySource
是一種機制,用於載入和解析配置屬性,可以從多種來源獲取這些屬性,例如檔案、系統環境變數、JVM
系統屬性和命令列引數等。PropertySource
是 Spring
框架中的一個抽象介面,它定義瞭如何讀取屬性源的方法。
透過 SpringBoot
的程式碼,我們可以看到,org.springframework.core.env.PropertySource
是一個抽象類,實現在子類有很多,我們上面提到的命令列 PropertySource
是 org.springframework.core.env.CommandLinePropertySource
。整體的類圖如下,涵蓋的內容還是很多的,感興趣的小夥伴可以好好研究一番。
另外在 SpringBoot
中,我們還可以使用 @PropertySource
註解來自定義指定要載入的屬性檔案。例如,可以在應用程式的主類上新增以下註解:
@SpringBootApplication
@PropertySource("classpath:customer.properties")
public class CustomerProperties {
// ...
}
這將告訴 SpringBoot
在 classpath
下查詢名為 customer.properties
的檔案,並將其載入為屬性源。然後,可以使用 @Value
註解將屬性值注入到 bean
中,如下所示:
@Service
public class MyService {
@Value("${my.property}")
private String myProperty;
// ...
}
這裡的 ${my.property}
是從 customer.properties
檔案中獲取的屬性值。如果找不到該屬性,那麼 SpringBoot
將使用預設值,這裡因為是自定義的屬性,是沒有預設值的,就會報錯,專案無法啟動。
具體實現是,SpringBoot
在啟動時會自動載入和解析所有的 PropertySource
,包括預設的 PropertySource
和自定義的PropertySource
。這些屬性值被儲存在 Spring
環境中,可以透過 Spring
的 Environment
物件訪問。當屬性被注入到 bean
中時, Spring
會查詢 Environment
物件並嘗試解析屬性的值。
總之,SpringBoot
的 PropertySource
提供了一種簡單的方法來載入和解析應用程式的配置屬性,這些屬性可以從多個來源獲取。它透過將屬性值儲存在 Spring
環境中,使其易於在應用程式的不同部分中使用。
除錯
為了驗證上面說的命令列的引數配置要優先於配置檔案,我們建立一個 SpringBoot 專案,並且在 application.properties
檔案中配置一個引數 name=JavaGeekTech
,而在 IDEA 啟動視窗中配置 name=JAVA_JIKEJUSHU
,分別如下所示
在寫一個簡單的 HelloController
類,並且透過 @Value
註解注入 name
屬性,接下來我們就需要除錯看下,SpringBoot
是如何將 name
屬性賦值的。透過驗證 name
會被賦值成 JAVA_JIKEJISHU
而不是 JavaGeekTech
。
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Value("${name}")
private String name;
@GetMapping(value = "/hello")
public String hello() {
return helloService.sayHello(name);
}
}
接著我們啟動 debug
,因為我們是基於 SpringBoot
的,屬性的賦值是在建立 bean
的時候,從 createBean
,到 doCreateBean
,再到 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean
,因為每個 bean
都會經過很多 PostProcessor
的處理,屬性賦值的 PostProcessor
是 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties
裡面的 metadata.inject
會呼叫到 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject
,再到 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#resolveFieldValue
,
org.springframework.beans.factory.support.DefaultListableBeanFactory#resolveDependency
,
org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency
,
org.springframework.beans.factory.support.AbstractBeanFactory#resolveEmbeddedValue
,
org.springframework.core.env.AbstractPropertyResolver#resolveRequiredPlaceholders
,
org.springframework.core.env.PropertySourcesPropertyResolver#getPropertyAsRawString
,
org.springframework.core.env.PropertySourcesPropertyResolver#getProperty(java.lang.String, java.lang.Class<T>, boolean)
整體呼叫鏈還是挺長的,不過只要跟著思路,在配合斷點,還是可以看看看出來的。
在 getProperty
方法中,我們可以看到如下的邏輯,根據 key
獲取到的 value
值為JAVA_JIKEJISHU
。
繼續跟蹤 getProperty
方法,我們可以看到這個方法 org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource#findConfigurationProperty(org.springframework.boot.context.properties.source.ConfigurationPropertyName)
,
其中的 getSource()
中就有我們配置的兩個屬性源的資料,如下所示
根據程式碼邏輯,我們也可以看到,在迭代的時候,如果找到了一個就直接返回了,所以得到的結果是JAVA_JIKEJISHU
。
總結
今天了不起帶大家研究了一個 SpringBoot
的外部化配置,並且透過實際的一個 case
跟蹤程式碼的呼叫鏈來給大家測試了一下,雖然說這個知識點我們經常都在使用,但是沒看到底層原始碼的時候我們並不知道這樣的一個功能底層是怎樣的複雜的。
這裡還是要敬佩一下 SpringBoot
的開發者,同時也建議大家,在日常的開發中我們需要多看看底層的原始碼,透過不斷的看原始碼,我們能更好的理解特性的實現原理,從而加強我們自身的能力。