-
1 前言
本文是一個(gè)系列缔赠,整個(gè)啟動(dòng)流程比較長(zhǎng)涨醋,涉及到配置的初始化,環(huán)境的初始化郁季,以及bean的加載流程等冷溃。本系列將從分階段的逐步進(jìn)行解析钱磅。
文章基于springboot2.3.x系列的源碼,github的源碼與實(shí)際發(fā)版的可能略微不同似枕,不過(guò)整理流程差別不大盖淡。
本人第一次寫文章,如有錯(cuò)誤或誤導(dǎo)歡迎留言指正凿歼。文章整體可能比較啰嗦褪迟,盡可能的將流程中的每一個(gè)重要的方法都講到。
-
2 SpringApplication初始化
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
//基礎(chǔ)資源啟動(dòng)類答憔,默認(rèn)為當(dāng)前啟動(dòng)類
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//判斷web服務(wù)類型牵咙,現(xiàn)在不是推出了reactive web嘛 區(qū)別一下
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//獲取ApplicationContextInitializer子類包括自定義的類,用于在applicationContext容器刷新之前調(diào)用
//在prepareContext方法中通過(guò)applyInitializers對(duì)上下文加載器進(jìn)行定制化的操作
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
//從堆棧信息中推斷當(dāng)前啟動(dòng)的類
//感興趣的也可以了解一下
this.mainApplicationClass = deduceMainApplicationClass();
}
getSpringFactoriesInstances攀唯,從方法的名稱可以大概知道是獲取ApplicationContextInitializer(應(yīng)用上下文初始化)和 ApplicationListener 實(shí)例工廠洁桌,根據(jù)傳入類的類型。
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = this.getClassLoader();
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
getSpringFactoriesInstances的內(nèi)部又引出了兩個(gè)方法侯嘀,loadFactoryNames和createSpringFactoriesInstances另凌。
下面逐個(gè)進(jìn)行分析。
loadFactoryNames:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
// 這里使用Map的getOrDefault方法 平時(shí)很少使用戒幔,將默認(rèn)值提前設(shè)置進(jìn)去吠谢,在map中取不到結(jié)果的時(shí)候返回默認(rèn)值。
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 優(yōu)先從緩存中獲取
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
// 通過(guò)classLoader獲取jar中的spring.factories文件路徑
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
// 以下為主要代碼
// 循環(huán)遍歷url結(jié)果
UrlResource resource = new UrlResource(url);
// 通過(guò)路徑加載配置诗茎, 將配置內(nèi)的屬性轉(zhuǎn)換成Map的形式
// Properties為HashMap的子類
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍歷map 對(duì)value串通過(guò)“,”截取成list
// 遍歷list將其存入LinkedMultiValueMap中 注意這里的LinkedMultiValueMap為MultiValueMap子類 其頂層依然是一個(gè)
// map不過(guò)內(nèi)部的value是一個(gè)list
LinkedMultiValueMap result = new LinkedMultiValueMap();
// 緩存類加載器作為key 緩存結(jié)果工坊,減少后續(xù)加載
cache.put(classLoader, result);
return result;
}
實(shí)現(xiàn)過(guò)或看過(guò)springboot的start包的小伙伴應(yīng)該豁然開(kāi)朗了,在start類的jar包下會(huì)有個(gè)spring.factories文件,記錄著配置類。
那也就是在這里對(duì)這些文件進(jìn)行了掃描加載并將路徑存放到緩存內(nèi)重抖。
另外,從這里也能看到其實(shí)spring用了很多java底層的東西昭齐,比如這里的classloader.getSystemResources,自定義Map類等矾柜。經(jīng)弛寮荩看源碼可能了解到很多沒(méi)用過(guò)的騷操作。
這里留一個(gè)問(wèn)題怪蔑,為何源碼中將classloader作為參數(shù)層層傳遞下去里覆,而不是直接在當(dāng)前類中使用this.getClassloader?
到此只是找到了這些需要加載的類,但是還未加載缆瓣。
createSpringFactoriesInstances:
private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args, Set<String> names) {
// 獲取class對(duì)象喧枷,這個(gè)classUtils感興趣的也可以了解一下
// 其內(nèi)部在啟動(dòng)時(shí)候初始化了一部分java基礎(chǔ)的提升加載速度
Class<?> instanceClass = ClassUtils.forName(name, classLoader);
Assert.isAssignable(type, instanceClass);
//獲取一個(gè)指定參數(shù)類型的構(gòu)造函數(shù)
Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
// 通過(guò)構(gòu)造函數(shù)和參數(shù) 獲取一個(gè)實(shí)例
// instantiateClass 方法感興趣的也可以去了解一下,內(nèi)部使用了spring自身的反射工具類,這里能了解到更多的反射應(yīng)用
// 而不僅僅是class.forname 有空的話這些工具類的方法和使用也會(huì)做一下割去,這里面才有很多java底層的應(yīng)用窟却。而且相對(duì)很規(guī)范
T instance = BeanUtils.instantiateClass(constructor, args);
}
到此為止昼丑,從spring.factories文件中加載了全部的配置呻逆,并且初始化了與ApplicationContextInitializer(應(yīng)用上下文初始化) 和 ApplicationListener相關(guān)的實(shí)例工廠。至于它們的作用菩帝,我們繼續(xù)向下看咖城。
這里強(qiáng)調(diào)一下,這里是實(shí)例化相當(dāng)于java的new關(guān)鍵字呼奢,且并沒(méi)有放入spring的bean容器中宜雀。
-
3 run方法執(zhí)行
上run方法源碼前,這里在提一下握础,不同的版本辐董,加載上下文(即初始化bean)前后的方法可能不太相同,但一般影響不大禀综。
源碼:
//基礎(chǔ)的run方法
public ConfigurableApplicationContext run(String... args) {
//spring的計(jì)時(shí)器 以后可以用這個(gè)實(shí)現(xiàn)简烘,可記錄多個(gè)任務(wù)時(shí)間
StopWatch stopWatch = new StopWatch();
//開(kāi)啟計(jì)時(shí)器
stopWatch.start();
//初始化上下文應(yīng)用
ConfigurableApplicationContext context = null;
//初始化異常報(bào)告集合
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
//配置系統(tǒng)參數(shù) java.awt.headless ,簡(jiǎn)單的來(lái)說(shuō)就是告訴服務(wù)需要在沒(méi)有外設(shè)和一些硬件的方式下運(yùn)行,和圖形處理有關(guān)定枷。
this.configureHeadlessProperty();
//監(jiān)聽(tīng)器集合發(fā)布事件孤澎,注意這個(gè)是一個(gè)異步事件
SpringApplicationRunListeners listeners = this.getRunListeners(args);
//在run()方法開(kāi)始執(zhí)行時(shí),該方法就立即被調(diào)用欠窒,可用于在初始化最早期時(shí)做一些工作
//SpringApplicationRunListener中的每個(gè)方法都有調(diào)用的階段覆旭。
listeners.starting();
Collection exceptionReporters;
try {
//初始化應(yīng)用默認(rèn)參數(shù)
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//初始化準(zhǔn)備環(huán)境
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
//創(chuàng)建打印類
Banner printedBanner = this.printBanner(environment);
//創(chuàng)建應(yīng)用上下文
context = this.createApplicationContext();
//準(zhǔn)備異常報(bào)告器
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
//準(zhǔn)備應(yīng)用上下文
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//刷新應(yīng)用上下文
this.refreshContext(context);
//上下文后置處理
this.afterRefresh(context, applicationArguments);
//計(jì)時(shí)器結(jié)束
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
//應(yīng)用上下文啟動(dòng)后執(zhí)行監(jiān)聽(tīng)器
listeners.started(context);
//執(zhí)行所有的runner,CommandLineRunner&ApplicationRunner
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
//異常處理
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}
try {
//應(yīng)用上下文就緒事件
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}
run包含了整個(gè)啟動(dòng)的流程岖妄,本節(jié)只講解到banner的打印型将,并簡(jiǎn)單的說(shuō)一下如何個(gè)性化的定制自己專屬的啟動(dòng)圖文,雖然沒(méi)有卵用荐虐,但是快樂(lè)呀茶敏!
選出本節(jié)關(guān)注的內(nèi)容:
//spring的計(jì)時(shí)器 以后可以用這個(gè)實(shí)現(xiàn),可記錄多個(gè)任務(wù)時(shí)間
StopWatch stopWatch = new StopWatch();
//開(kāi)啟計(jì)時(shí)器
stopWatch.start();
//初始化上下文應(yīng)用
ConfigurableApplicationContext context = null;
//初始化異常報(bào)告集合
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
//配置系統(tǒng)參數(shù) java.awt.headless ,簡(jiǎn)單的來(lái)說(shuō)就是告訴服務(wù)需要在沒(méi)有外設(shè)和一些硬件的方式下運(yùn)行缚俏,和圖形處理有關(guān)惊搏。
this.configureHeadlessProperty();
//監(jiān)聽(tīng)器集合發(fā)布事件,注意這個(gè)是一個(gè)異步事件
SpringApplicationRunListeners listeners = this.getRunListeners(args);
//在run()方法開(kāi)始執(zhí)行時(shí)忧换,該方法就立即被調(diào)用恬惯,可用于在初始化最早期時(shí)做一些工作
//SpringApplicationRunListener中的每個(gè)方法都有調(diào)用的階段。
listeners.starting();
Collection exceptionReporters;
//初始化應(yīng)用默認(rèn)參數(shù)
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//初始化準(zhǔn)備環(huán)境
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
//創(chuàng)建打印類
Banner printedBanner = this.printBanner(environment);
從計(jì)時(shí)器StopWatch開(kāi)始亚茬,想想spring框架的貢獻(xiàn)者是什么樣的大佬酪耳,怎么可會(huì)直接使用System.currentTimeMillis()來(lái)計(jì)算呢!這個(gè)原理并不復(fù)雜,內(nèi)部使用的也是System.nanoTime精度更高的納秒碗暗,而且在很多開(kāi)源的工具包中都有提供颈将,比如我們常用的Apache的commons,hutool等言疗,但內(nèi)部的方法并不完全相同晴圾,spring提供的更像跑步比賽時(shí)的計(jì)時(shí)器,可以停止多次噪奄。
configureHeadlessProperty :設(shè)置java.awt.headless屬性死姚,告訴java應(yīng)用程序,當(dāng)前運(yùn)行環(huán)境缺少鍵盤勤篮、鼠標(biāo)都毒、顯示器等設(shè)備,你得自己堅(jiān)強(qiáng)碰缔。
監(jiān)聽(tīng)器:
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();
這里就知道在初始化階段账劲,獲取監(jiān)聽(tīng)器是做啥用的了,記住這里的監(jiān)聽(tīng)器金抡,是為了整個(gè)啟動(dòng)流程服務(wù)的瀑焦。不是我們?nèi)粘V凶远x的用來(lái)處理異步事件的監(jiān)聽(tīng)器,它們實(shí)現(xiàn)的接口是不同的竟终,這里實(shí)現(xiàn)的是SpringApplicationRunListener 從名稱就很容易看出蝠猬,是為了上下文啟動(dòng)使用的,而我們常用的是 ApplicationListener
统捶。另外特別強(qiáng)調(diào)一下starting不能理解為啟動(dòng)監(jiān)聽(tīng)器榆芦,而是執(zhí)行與啟動(dòng)前準(zhǔn)備工作相關(guān)的監(jiān)聽(tīng)器。
這點(diǎn)通過(guò)源碼可以很容易理解:
public interface SpringApplicationRunListener {
void starting();
void environmentPrepared(ConfigurableEnvironment environment);
void contextPrepared(ConfigurableApplicationContext context);
void contextLoaded(ConfigurableApplicationContext context);
void started(ConfigurableApplicationContext context);
void running(ConfigurableApplicationContext context);
void failed(ConfigurableApplicationContext context, Throwable exception);
}
除了starting之外還有environmentPrepared喘鸟、contextPrepared匆绣、contextLoaded、started什黑、runing等崎淳。對(duì)應(yīng)啟動(dòng)流程的各個(gè)階段。
關(guān)于監(jiān)聽(tīng)器內(nèi)部的原理這里也不展開(kāi)說(shuō)了愕把,單拿出來(lái)還能水一篇哈哈拣凹!挖坑挖坑!
因此在自定義start啟動(dòng)jar的時(shí)候恨豁,若要使用SpringApplicationRunListener嚣镜,則需要明確自己所想應(yīng)用的階段。
到這為止橘蜜,還都算很簡(jiǎn)單菊匿。下面開(kāi)始稍微有那么一內(nèi)內(nèi)復(fù)雜的。默認(rèn)參數(shù)初始化,雖然我們?cè)谑褂胷un方法的時(shí)候幾乎不會(huì)傳入?yún)?shù)跌捆,但人家支持呀徽职!那就要了解一下!
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ApplicationArguments 對(duì)象本身沒(méi)有什么好說(shuō)的佩厚,提供應(yīng)用上下文的參數(shù)初始化姆钉。但它有一個(gè)內(nèi)部類
private static class Source extends SimpleCommandLinePropertySource {
...
...
}
看到這個(gè)父類的名稱,應(yīng)該就能猜到這個(gè)與PropertySource有關(guān)可款,那我們?cè)诖_認(rèn)一下類的關(guān)系圖育韩。
關(guān)系圖上我們可以大致了解到克蚂,這里可以接受run方法傳入的參數(shù)作為comandline的命令參數(shù)闺鲸,這里將參數(shù)封裝成PropertySource對(duì)象并放入 ApplicationArguments 中以備使用。
參數(shù)最終都是通過(guò)構(gòu)造函數(shù)super關(guān)鍵字傳遞到頂層的PropertySource中埃叭。
關(guān)于SimpleCommandLinePropertySource 這個(gè)就不展開(kāi)說(shuō)了摸恍,這里的確是命令行參數(shù)設(shè)置,例如: --name=zhangsan,另外這里的設(shè)置可以通過(guò)@Value獲取到赤屋,這屬于騷操作的范圍了吧立镶,很少這么使用。這里可以替換配置文件中的屬性类早,比如在application.properties中的server.port屬性可以在這里通過(guò)參數(shù)--server.port的替換,本質(zhì)上它是作為啟動(dòng)參數(shù)來(lái)執(zhí)行的媚媒,相當(dāng)于 java -jar --server.port=8080 優(yōu)先級(jí)高于配置文件。
入?yún)⒌母袷揭残枰⒁猓?-開(kāi)頭和非 --開(kāi)頭的解析方式不同這里需要格外注意涩僻。
public PropertySource(String name, T source) {
this.logger = LogFactory.getLog(this.getClass());
Assert.hasText(name, "Property source name must contain at least one character");
Assert.notNull(source, "Property source must not be null");
this.name = name;
this.source = source;
}
PropertySource 也是一個(gè)spring中比較重要的類缭召,它是頂層的配置資源封裝類,在很多地方都有應(yīng)用逆日,與BeanDefinition一樣嵌巷,作為spring的規(guī)范應(yīng)用的一種,對(duì)擴(kuò)展使用很方便室抽。
prepareEnvironment : 準(zhǔn)備啟動(dòng)環(huán)境
源碼:
ConfigurableEnvironment environment = this.prepareEnvironment(listeners,applicationArguments);
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
這里有兩個(gè)部分:初始化環(huán)境配置和打印banner
初始化環(huán)境配置源碼:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
//根據(jù)web應(yīng)用類型獲取環(huán)境
ConfigurableEnvironment environment = this.getOrCreateEnvironment();
//配置環(huán)境參數(shù)和profiles.active信息
this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach((Environment)environment);
listeners.environmentPrepared((ConfigurableEnvironment)environment);
this.bindToSpringApplication((ConfigurableEnvironment)environment);
if (!this.isCustomEnvironment) {
environment = (new EnvironmentConverter(this.getClassLoader())).convertEnvironmentIfNecessary((ConfigurableEnvironment)environment, this.deduceEnvironmentClass());
}
ConfigurationPropertySources.attach((Environment)environment);
return (ConfigurableEnvironment)environment;
}
老樣子搪哪,逐個(gè)分析。
getOrCreateEnvironment:獲取環(huán)境類
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
} else {
switch(this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}
}
webApplicationType: 參數(shù)在SpringApplication構(gòu)造函數(shù)內(nèi)(部分源碼可能是創(chuàng)建時(shí)就直接初始化了)初始化的坪圾。
這里返回的結(jié)果一般都是StandardServletEnvironment 目前我們未使用響應(yīng)式web框架晓折,即spring 5開(kāi)始推出的webflux.這點(diǎn)以后也需要學(xué)習(xí)一下。
StandardServletEnvironment的父類AbstractEnvironment的構(gòu)造函數(shù)中也進(jìn)行了配置資源MutablePropertySources(環(huán)境配置資源保存)和propertyResolver(資源解析)的初始化兽泄,獲取了系統(tǒng)配置和環(huán)境變量等配置信息漓概,后面用到了再說(shuō)再細(xì)說(shuō)。
configureEnvironment:配置環(huán)境
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
ConversionService conversionService = ApplicationConversionService.getSharedInstance();
environment.setConversionService((ConfigurableConversionService)conversionService);
}
this.configurePropertySources(environment, args);
this.configureProfiles(environment, args);
}
ConversionService:為環(huán)境設(shè)置轉(zhuǎn)換服務(wù)已日,這里配置參數(shù)addConversionService默認(rèn)為true垛耳,因此這里必定會(huì)初始化。
ApplicationConversionService.getSharedInstance()也并不復(fù)雜,從名稱可以知道為上下文轉(zhuǎn)換服務(wù)堂鲜,這里 getSharedInstance 通過(guò)懶漢式的方式獲取了一個(gè)ApplicationConversionService的單例栈雳。
在構(gòu)造函數(shù)內(nèi)初始化了一些轉(zhuǎn)換和格式化服務(wù)。比如String轉(zhuǎn)number缔莲,Date轉(zhuǎn)String等哥纫。也是配置文件讀取時(shí),屬性轉(zhuǎn)到配置類中或@value注解綁定參數(shù)等地方使用痴奏。
擴(kuò)展說(shuō)一下單例模式的實(shí)現(xiàn)蛀骇,餓漢式和懶漢式,正常的情況下 懶漢式是非線程安全的读拆。而餓漢式是天生的線程安全擅憔。因此若使用懶漢式,需要解決線程安全問(wèn)題檐晕。這里getSharedInstance示范了一個(gè)標(biāo)準(zhǔn)的線程安全懶漢式模式之一暑诸,比如還可以通過(guò)枚舉類實(shí)現(xiàn)。
下面我們看一下getSharedInstance源碼:
public static ConversionService getSharedInstance() {
ApplicationConversionService sharedInstance = ApplicationConversionService.sharedInstance;
if (sharedInstance == null) {
Class var1 = ApplicationConversionService.class;
synchronized(ApplicationConversionService.class) {
sharedInstance = ApplicationConversionService.sharedInstance;
if (sharedInstance == null) {
sharedInstance = new ApplicationConversionService();
ApplicationConversionService.sharedInstance = sharedInstance;
}
}
}
return sharedInstance;
}
這個(gè)扯遠(yuǎn)了辟灰,但是真心覺(jué)得源碼中个榕,代碼的編寫和一些java原生工具的使用都非常好,很多細(xì)小的點(diǎn)都值得學(xué)習(xí)芥喇。不僅僅只為了弄清一個(gè)spring的啟動(dòng)流程西采,更多的要看大佬們是如何編寫代碼的。
繼續(xù)继控,獲取轉(zhuǎn)換服務(wù)后設(shè)置到環(huán)境對(duì)象中以備使用械馆。
configurePropertySources 配置資源處理。
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
// 這個(gè)屬性在AbstractEnvironment的構(gòu)造函數(shù)中初始化
MutablePropertySources sources = environment.getPropertySources();
//這里defaultProperties 默認(rèn)為空
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
}
// 前面說(shuō)到的commandline 配置參數(shù)
if (this.addCommandLineProperties && args.length > 0) {
String name = "commandLineArgs";
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
} else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
這里將傳入的配置資源以及默認(rèn)的配置資源存入環(huán)境的資源中湿诊。
順便吐槽一下狱杰,原來(lái)spring中也有很多寫死的常量屬性,雖大部分情況下這種屬性相對(duì)不會(huì)改變厅须。但個(gè)人還是有點(diǎn)想吐槽仿畸!
ConfigurationPropertySources.attach:設(shè)置:configurationProperties配置屬性
public static void attach(Environment environment) {
Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
MutablePropertySources sources = ((ConfigurableEnvironment)environment).getPropertySources();
PropertySource<?> attached = sources.get("configurationProperties");
if (attached != null && attached.getSource() != sources) {
sources.remove("configurationProperties");
attached = null;
}
if (attached == null) {
sources.addFirst(new ConfigurationPropertySourcesPropertySource("configurationProperties", new SpringConfigurationPropertySources(sources)));
}
}
校驗(yàn)原環(huán)境配置屬性列表中是否包含 configurationProperties 若不包含則新增進(jìn)去。存在的話對(duì)比是否是同一個(gè)對(duì)象朗和,若不是則刪除重新添加错沽。這里比較有意思的是,sources是從環(huán)境對(duì)象中獲取的眶拉,然后將獲取的結(jié)果通過(guò)SpringConfigurationPropertySources封裝千埃,并作為sources的一個(gè)新的configurationProperties屬性放入sources中.
到目前為止還不能明確這么做的目的。帶著疑問(wèn)繼續(xù)向下看忆植。
listeners.environmentPrepared:這個(gè)就不多說(shuō)了放可,環(huán)境已經(jīng)準(zhǔn)備好執(zhí)行與此相關(guān)的監(jiān)聽(tīng)器谒臼。
bindToSpringApplication:
這個(gè)就比較有意思了,從名字理解耀里,為SpringApplication綁定蜈缤,綁定什么呢?
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
} catch (Exception var3) {
throw new IllegalStateException("Cannot bind to SpringApplication", var3);
}
}
為了方便理解binder的原理冯挎,我們?cè)诜乓幌耣inder.get的源碼:
public static Binder get(Environment environment, BindHandler defaultBindHandler) {
Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources.get(environment);
PropertySourcesPlaceholdersResolver placeholdersResolver = new PropertySourcesPlaceholdersResolver(environment);
return new Binder(sources, placeholdersResolver, (ConversionService)null, (Consumer)null, defaultBindHandler);
}
從這里可以看到一個(gè)ConfigurationPropertySources.get方法底哥,從環(huán)境中獲取了ConfigurationPropertySource的配置資源迭代器。
其源碼:
public static Iterable<ConfigurationPropertySource> get(Environment environment) {
Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
MutablePropertySources sources = ((ConfigurableEnvironment)environment).getPropertySources();
ConfigurationPropertySourcesPropertySource attached = (ConfigurationPropertySourcesPropertySource)sources.get("configurationProperties");
return attached == null ? from((Iterable)sources) : (Iterable)attached.getSource();
}
這里知道了前文為何又將配置資源設(shè)置到configurationProperties中房官,在這里通過(guò)可配置環(huán)境類ConfigurableEnvironment獲取環(huán)境中的指定的配置資源趾徽。
這里最后返回的結(jié)果是Iterable類型,因?yàn)閏onfigurationProperties內(nèi)保存的是一個(gè)列表翰守。
我們?cè)倩氐紹inder.get的源碼中孵奶。除了獲取環(huán)境類中的配置資源,還初始化了一個(gè)配置資源占位符解析器
PropertySourcesPlaceholdersResolver placeholdersResolver = new PropertySourcesPlaceholdersResolver(environment);
PropertySourcesPlaceholdersResolver:
public PropertySourcesPlaceholdersResolver(Iterable<PropertySource<?>> sources, PropertyPlaceholderHelper helper) {
this.sources = sources;
this.helper = helper != null ? helper : new PropertyPlaceholderHelper("${", "}", ":", true);
}
這樣就比較直觀潦俺,這里是要替換掉配置文件中的${xx}占位符拒课。
至于bind方法這里不打算展開(kāi)說(shuō)了徐勃,知道是將spring.main下面的配置綁定到SpringApplication對(duì)象上事示。如:sources,bannerMode等屬性賦值給當(dāng)前的對(duì)象僻肖。
這里額外提一點(diǎn)肖爵,從類的名稱區(qū)猜測(cè)類的作用在spring源碼中真的非常好用,比如上面源碼中的ConfigurableEnvironment臀脏,從名稱可猜測(cè)為可配置的環(huán)境劝堪,他的實(shí)現(xiàn)類之一就是我們上面說(shuō)的StandardServletEnvironment,spring將環(huán)境中可配置屬性的方式揉稚,提到接口中秒啦。既方便擴(kuò)展,也直觀的展示了那些屬性是可以通過(guò)配置修改的搀玖。
判斷是否需要轉(zhuǎn)換環(huán)境對(duì)象
if (!this.isCustomEnvironment) {
environment = (new EnvironmentConverter(this.getClassLoader())).convertEnvironmentIfNecessary((ConfigurableEnvironment)environment, this.deduceEnvironmentClass());
}
isCustomEnvironment參數(shù)默認(rèn)為false余境,這里必定會(huì)執(zhí)行。內(nèi)部對(duì)比了當(dāng)前環(huán)境對(duì)象是否為StandardEnvironment子類灌诅。若不是則重新生成StandardEnvironment環(huán)境對(duì)象芳来,并將內(nèi)部的參數(shù)轉(zhuǎn)換到新的對(duì)象中
方法結(jié)尾,重新執(zhí)行了ConfigurationPropertySources.attach方法猜拾。因?yàn)榍拔目赡芤呀?jīng)對(duì)資源或環(huán)境類進(jìn)行樂(lè)修改即舌。這里相當(dāng)于重新初始化了一次環(huán)境配置資源類中的configurationProperties的配置。
到此為止挎袜,我們講完了prepareEnvironment(環(huán)境對(duì)象初始化流程)顽聂。
configureIgnoreBeanInfo :設(shè)置需要被忽略的beaninfo肥惭。
最后介紹一下:printBanner 啟動(dòng)banner的打印。
源碼:
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Mode.OFF) {
return null;
} else {
ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader((ClassLoader)null);
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter((ResourceLoader)resourceLoader, this.banner);
return this.bannerMode == Mode.LOG ? bannerPrinter.print(environment, this.mainApplicationClass, logger) : bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
}
this.bannerMode在SpringApplication構(gòu)造函數(shù)內(nèi)初始化(部分版本源碼是創(chuàng)建時(shí)初始化)紊搪。默認(rèn)為“CONSOLE”务豺。
上面的源碼比較簡(jiǎn)單,提供了兩種打印方式嗦明,并初始化一個(gè)banner對(duì)象笼沥。
bannerPrinter.print內(nèi)提供了兩種不同的banner類型。
private Banner getBanner(Environment environment) {
SpringApplicationBannerPrinter.Banners banners = new SpringApplicationBannerPrinter.Banners();
banners.addIfNotNull(this.getImageBanner(environment));
banners.addIfNotNull(this.getTextBanner(environment));
if (banners.hasAtLeastOneBanner()) {
return banners;
} else {
return this.fallbackBanner != null ? this.fallbackBanner : DEFAULT_BANNER;
}
}
getImageBanner和getTextBanner娶牌,圖片和文本兩種模式奔浅。
這里還未真正打印,只是初始化好了一個(gè)banner對(duì)象诗良。
private Banner getTextBanner(Environment environment) {
// spring.banner.location 屬性汹桦, 默認(rèn)為 banner.txt
String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
Resource resource = this.resourceLoader.getResource(location);
try {
if (resource.exists() && !resource.getURL().toExternalForm().contains("liquibase-core")) {
return new ResourceBanner(resource);
}
}
catch (IOException ex) {
// Ignore
}
return null;
}
private Banner getImageBanner(Environment environment) {
//spring.banner.location 需要指定文件
String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
if (StringUtils.hasLength(location)) {
Resource resource = this.resourceLoader.getResource(location);
return resource.exists() ? new ImageBanner(resource) : null;
}
for (String ext : IMAGE_EXTENSION) {
Resource resource = this.resourceLoader.getResource("banner." + ext);
if (resource.exists()) {
return new ImageBanner(resource);
}
}
return null;
}
通過(guò)配置可以看出在resource下默認(rèn)創(chuàng)建一個(gè)banner.txt文本即可替換原始的banner信息。這種方式比較簡(jiǎn)單鉴裹。
-
4 小結(jié)
本文講解了從啟動(dòng)到創(chuàng)建上下文之前的動(dòng)作舞骆,包括SpringApplication初始化(加載spring.factories文件中的配置類,監(jiān)聽(tīng)器等)径荔,運(yùn)行環(huán)境的初始化(環(huán)境類以及內(nèi)部的配置資源的初始化)督禽,banner對(duì)象的構(gòu)建等。
到此為止都算比較簡(jiǎn)單总处,這里重點(diǎn)關(guān)注一下環(huán)境類(Environment)后續(xù)使用的會(huì)比較多狈惫,配置資源是從這里獲取,提供了springboot運(yùn)行時(shí)環(huán)境鹦马。
文章講的比較啰嗦胧谈,很多點(diǎn)沒(méi)有詳細(xì)深入,如果詳細(xì)的說(shuō)荸频,一篇文章根本說(shuō)不完菱肖,比如監(jiān)聽(tīng)器以及一些工具類等。
簡(jiǎn)單的流程:
1.初始化SpringApplication構(gòu)造函數(shù)
2.從spring.factories文件中加載配置類
3.加載配置資源(系統(tǒng)配置旭从,環(huán)境變量稳强,項(xiàng)目的application.yml配置等)
4.初始化環(huán)境參數(shù),將配置資源注入
5.構(gòu)建banner對(duì)象