springboot原始碼分析7-環境屬性構造過程(上)
使用springboot的目的就是在專案開發中,快速出東西,因此springboot對於配置檔案的格式支援是非常豐富的,最常見的配置檔案字尾有如下四種:properties、xml、yml、yaml,比如我們在springboot專案根目錄中配置了一個application.properties檔案,則springboot專案啟動的時候就會自動將該檔案的內容解析並設定到環境中,這樣後續需要使用該檔案中配置的屬性的時候,只需要使用@value即可。同理application.xml、application.yml、application.yaml檔案也會自動被載入並最終設定到環境中。
上面我們提到了環境,那麼環境到底是個什麼玩意呢?在這裡提前說一下:我們這裡關注的是原始碼層面的事情。並非講解
大家首先思考一下,springboot專案如何啟動,這個到很簡單,無外乎引入springboot依賴包,設定專案啟動的main方法如下所示:
@EnableAutoConfiguration
public class Application {
private static Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
SpringApplication.run(Application.class,args);
logger.info("程式啟動花費時間為:" + (System.currentTimeMillis() - startTime) / 1000 + "秒");
}
}
上述的程式碼非常的簡單,但是springboot做了非常多的事情,因為springboot程式碼體系非常龐大,所以後續的文章是我們講解那些原始碼就直接看那些原始碼,把不需要了解的暫時放到一邊。因此在這裡暫時先關注環境的建立原始碼,我們快速定位到SpringApplication類中的public ConfigurableApplicationContext run(String... args)方法,該方法關於環境的準備程式碼如下所示:
...
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
...
prepareEnvironment方法從名字就可以看出來是準備環境(Environment),prepareEnvironment程式碼如下:
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
//獲取或者建立環境
ConfigurableEnvironment environment = getOrCreateEnvironment();
//配置環境的資訊
configureEnvironment(environment, applicationArguments.getSourceArgs());
//通知所有的觀察者,環境已經準備好了。
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (this.webApplicationType == WebApplicationType.NONE) {
environment = new EnvironmentConverter(getClassLoader())
.convertToStandardEnvironmentIfNecessary(environment);
}
ConfigurationPropertySources.attach(environment);
return environment;
}
接下來,我們一步步的分析。
1.1. 1.獲取或者建立環境
getOrCreateEnvironment()方法如下所示:
if (this.environment != null) {
return this.environment;
}
if (this.webApplicationType == WebApplicationType.SERVLET) {
return new StandardServletEnvironment();
}
return new StandardEnvironment();
上述程式碼邏輯如下:
1.如果environment不為空則直接返回。
2.如果是web環境則直接例項化StandardServletEnvironment類。
3.如果不是web環境則直接例項化StandardEnvironment類。
1.2. WebApplicationType型別
Springboot2版本開始增加了WebApplicationType的型別,其定義如下:
public enum WebApplicationType {
/**
* 不需要再web容器的環境下執行,也就是普通的工程
*/
NONE,
/**
基於servlet的Web專案
*/
SERVLET,
/**
響應式web應用==reactive web Spring5版本的新特性
*/
REACTIVE
}
1.3. ConfigurableEnvironment類
environment 為ConfigurableEnvironment型別。我們不妨看一下該類的層次圖如下所示:
Environment介面是Spring對當前程式執行期間的環境的封裝(spring)。主要提供了兩大功能:profile和property(頂級介面PropertyResolver提供)。目前主要有StandardEnvironment、StandardServletEnvironment和MockEnvironment、StandardReactiveWebEnvironment4種實現,分別代表普通程式、Web程式、測試程式的環境、響應式web環境。通過上述的getOrCreateEnvironment方法處理邏輯也是可以總結出來的。
StandardReactiveWebEnvironment是Springboot2新引入的,之前的版本沒有這個類。關於這一個後續的章節會單獨的詳細講解。
2.環境的裝載
在上面的程式碼中例項化了StandardServletEnvironment類(我自己的環境是web),例項化該類的時候肯定會例項化其父類AbstractEnvironment,AbstractEnvironment類的建構函式如下:
public AbstractEnvironment() {
customizePropertySources(this.propertySources);
}
需要注意一點,因為例項化的是StandardServletEnvironment類,jvm會自動觸發其父類中的建構函式,但是當前程式的this指標依然是StandardServletEnvironment。
this.propertySources屬性如下所示:
AbstractEnvironment.java
private final MutablePropertySources propertySources = new MutablePropertySources(this.logger);
我們繼續跟蹤customizePropertySources方法,如下所示:
AbstractEnvironment.java
protected void customizePropertySources(MutablePropertySources propertySources) {
}
好吧,customizePropertySources方法竟然是個空的實現,但是注意一點,當前程式this是StandardServletEnvironment例項,我們不妨看一下StandardServletEnvironment類中是否重寫了該方法。果不其然,StandardServletEnvironment類重寫了customizePropertySources方法,詳細程式碼如下所示:
StandardServletEnvironment.java
protected void customizePropertySources(MutablePropertySources propertySources) {
//servletConfigInitParams
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
//servletContextInitParams
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
//jndiProperties
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
super.customizePropertySources(propertySources);
}
上述的程式碼中,propertySources為AbstractEnvironment.java中的propertySources欄位,因為他是個引用型別,所以可以拿到指標即可修改其值。
1.4. propertySources類
雖然我們暫時還不知道propertySources要幹啥,但是我們還是先看明白PropertySources到底要幹啥。PropertySources類可以參考springboot原始碼分析6-springboot之PropertySource類初探一文。
我們再次看一下customizePropertySources方法的實現:
首先新增servletConfigInitParams,然後新增servletContextInitParams,其次判斷是否是jndi環境,如果是則新增jndiProperties,最後呼叫父類的customizePropertySources(propertySources)。
在跟進父類的customizePropertySources(propertySources)方法之前,我們總結一下MutablePropertySources類中propertySourceList已經存在的屬性為servletConfigInitParams、servletContextInitParams、jndiProperties(如果存在)。
StandardEnvironment類為StandardServletEnvironment類的父類,該類的customizePropertySources方法如下:
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
1、新增systemProperties
2、新增systemEnvironment。
上述的方法邏輯執行完畢之後,MutablePropertySources類中propertySourceList已經存在的屬性為servletConfigInitParams、servletContextInitParams、jndiProperties(如果存在)、systemProperties、systemEnvironment。
經過一系列的跟蹤getOrCreateEnvironment方法所做的事情已經分析完畢了。我們不妨繼往下看。
3.配置環境資訊
configureEnvironment(environment, applicationArguments.getSourceArgs())方法詳細實現如下所示:
protected void configureEnvironment(ConfigurableEnvironment environment,
String[] args) {
configurePropertySources(environment, args);
configureProfiles(environment, args);
}
3.1配置屬性源
configurePropertySources(environment, args)方法的核心實現如下:
protected void configurePropertySources(ConfigurableEnvironment environment,
String[] args) {
MutablePropertySources sources = environment.getPropertySources();
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
sources.addLast(
new MapPropertySource("defaultProperties", this.defaultProperties));
}
if (this.addCommandLineProperties && args.length > 0) {
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource(
name + "-" + args.hashCode(), args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
1、如果defaultProperties不為空,則繼續新增defaultProperties。思考一個問題defaultProperties怎麼設定?
2、如果addCommandLineProperties為true並且有命令引數,分兩步驟走:第一步存在commandLineArgs則繼續設定屬性;第二步commandLineArgs不存在則在頭部新增commandLineArgs。
上述的程式碼執行完畢之後,MutablePropertySources類中propertySourceList已經存在的屬性為commandLineArgs、servletConfigInitParams、servletContextInitParams、jndiProperties(如果存在)、systemProperties、systemEnvironment、defaultProperties(如果存在)。
3.2配置Profiles
這個後續我們用到了再來講解。
本文我們暫時講解到這裡,後續的文章中,我們繼續跟蹤屬性檔案的載入規則以及載入過程。提前曝光一點:
commandLineArgs、servletConfigInitParams、servletContextInitParams、jndiProperties(如果存在)、systemProperties、systemEnvironment、defaultProperties(如果存在)中的屬性優先順序從前到後依次降低。在最前面的使用優先順序最高。
比如commandLineArgs中存在一個屬性a=1; systemProperties中存在一個屬性a=2,則我們程式使用的時候a=1,因為越靠前的優先順序越高。通過上述的優先順序我們可以發現一個規律,命令列的優先順序最高、其次是程式中的、然後是系統的環境變數以及屬性、最後是預設的。
propertySources介面我們下一節課重點進行分析。
歡迎關注我的微信公眾號,第一時間獲得部落格更新提醒,以及更多成體系的Java相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,即可關注。