從代碼層面看spring boot啟動(dòng)過(guò)程

前言
我們都知道spring boot項(xiàng)目是通過(guò)main方法來(lái)啟動(dòng)運(yùn)行的,但是main方法執(zhí)行之后,spring boot都替我們完成了哪些操作,最終讓我們的服務(wù)成功啟動(dòng)呢?今天我們就來(lái)從源碼層面探討下這個(gè)問(wèn)題。
spring boot啟動(dòng)過(guò)程
在開(kāi)始之前,我們先看這樣一段代碼:
@SpringBootApplication
public class DailyNoteApplication {
public static void main(String[] args) {
SpringApplication.run(DailyNoteApplication.class, args);
}
}
上面的這段代碼就是我們最常見(jiàn)的spring boot啟動(dòng)的main方法,今天我們就從這個(gè)main方法開(kāi)始,進(jìn)入spring boot的世界。
springApplication
首先,在main方法內(nèi)部執(zhí)行了SpringApplication.run(DailyNoteApplication.class, args),這個(gè)方法有兩個(gè)入?yún)ⅲ粋€(gè)是項(xiàng)目主類(lèi)(當(dāng)前類(lèi))的class,另一個(gè)就是main方法的args。
這里先說(shuō)下這個(gè)args參數(shù),我們都知道spring boot是支持以命令行的方式注入配置信息的,它的實(shí)現(xiàn)就是依賴(lài)于這個(gè)args參數(shù)的。如果你將args刪掉,項(xiàng)目也是可以正常啟動(dòng)的,只是你再也沒(méi)辦法通過(guò)命令行的方式注入配置了。關(guān)于這一塊,我們之前在通過(guò)k8s啟動(dòng)spring boot項(xiàng)目的時(shí)候踩過(guò)坑,發(fā)現(xiàn)注入的參數(shù)不起作用,最后發(fā)現(xiàn)就是少了args。
run方法
在run方法內(nèi)部,首先實(shí)例化了一個(gè)springApplication對(duì)象,然后又調(diào)用了另一個(gè)run方法:

springApplication實(shí)例化
我們先看springApplication的實(shí)例化過(guò)程:

前兩個(gè)this都是簡(jiǎn)單的賦值,這里暫時(shí)先不過(guò)多研究,第三個(gè)this這里的WebApplicationType.deduceFromClasspath()是判斷我們的服務(wù)器類(lèi)別,在spring boot中,有兩種服務(wù)器一種就是傳統(tǒng)的sevlet,也就是基于tomcat(其中一種)這種,另一種就是reactive,也就是我們前面分享的webflux這種流式服務(wù)器。
緊接著是初始化ApplicationContextInitializer和ApplicationListener,這里主要是獲取他們的spring工程實(shí)例,方便后續(xù)創(chuàng)建他們的實(shí)例,為了保證主流程的連貫性,我們暫時(shí)不看其方法內(nèi)部實(shí)現(xiàn)。
最后一個(gè)賦值操作是找出包含main方法的類(lèi)的className。
run方法開(kāi)始執(zhí)行
下面我們看下springApplication實(shí)例的 run方法內(nèi)部執(zhí)行過(guò)程:
創(chuàng)建了一個(gè)
StopWatch對(duì)象,并調(diào)用它的start方法StopWatch stopWatch = new StopWatch();
stopWatch.start();設(shè)置
java的java.awt.headless值,如果已經(jīng)設(shè)置過(guò)就取系統(tǒng)設(shè)置的值,如果沒(méi)有設(shè)置,則設(shè)置為true。這個(gè)是設(shè)置java的無(wú)頭模式,啟用之后,可以用計(jì)算能力來(lái)處理可視化操作(類(lèi)似于用算力代替顯卡渲染能力)configureHeadlessProperty();獲取
spring boot運(yùn)行監(jiān)聽(tīng)器SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();解析控制臺(tái)參數(shù)(
args),獲取應(yīng)用參數(shù)ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);配置需要忽略的
bean信息,從源碼中我們可以看出了,如果我們沒(méi)有設(shè)置spring.beaninfo.ignore,spring boot會(huì)給他默認(rèn)true:configureIgnoreBeanInfo(environment);
//方法內(nèi)部實(shí)現(xiàn)
private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}打印
banner信息,這個(gè)banner就是spring boot啟動(dòng)的時(shí)候打印的哪個(gè)logo,那個(gè)是支持自定義的Banner printedBanner = printBanner(environment);創(chuàng)建
spring boot容器,這里創(chuàng)建的時(shí)候會(huì)根據(jù)我們應(yīng)用的不同,選擇不同的容器context = createApplicationContext();
創(chuàng)建
spring工廠實(shí)例exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);準(zhǔn)備容器,這一步會(huì)進(jìn)行初始化操作,把環(huán)境設(shè)置、系統(tǒng)參數(shù)、
banner注入到容器中,并把容器綁定到監(jiān)聽(tīng)器上prepareContext(context, environment, listeners, applicationArguments, printedBanner);刷新容器,這里其實(shí)進(jìn)行了兩步操作,一個(gè)是給我們的
spring boot綁定SpringContextShutdownHook鉤子函數(shù),有了這個(gè)函數(shù),我們就可以?xún)?yōu)雅地關(guān)閉spring boot了;另一個(gè)是刷新beanFactory,默認(rèn)情況下spring boot為我們創(chuàng)建的是GenericApplicationContext容器,初始化完后,所有的對(duì)象都被初始化在它的beanFactory,為了確保其他組件也能拿到beanFactory中的內(nèi)容,refreshContext方法內(nèi)還進(jìn)行了同步操作(直接copy給他們):
refreshContext(context);
從源碼中可以很明顯看出這一點(diǎn):

刷新完成后會(huì)執(zhí)行
afterRefresh方法,但是這個(gè)方法默認(rèn)情況下是空的afterRefresh(context, applicationArguments);
停止秒表。這個(gè)秒表的作用應(yīng)該就是計(jì)時(shí)
stopWatch.stop();調(diào)用監(jiān)聽(tīng)器
started方法,這方法修改了容器的狀態(tài)。和前面starting方法不同的是,這個(gè)方法必須在beanfactory刷新后執(zhí)行:listeners.started(context);
運(yùn)行容器中的
runner,這里的runner主要有兩類(lèi),一類(lèi)是繼承ApplicationRunner的,一類(lèi)是繼承CommandLineRunner。我猜測(cè)這個(gè)應(yīng)該是為了方便我們實(shí)現(xiàn)更復(fù)雜的需求實(shí)現(xiàn)的,目前還沒(méi)用到過(guò),后面可以找時(shí)間研究下callRunners(context, applicationArguments);
最后一步還是監(jiān)聽(tīng)器的操作。這個(gè)方法最后將容器的狀態(tài)改為
ACCEPTING_TRAFFIC,表示可以接受請(qǐng)求listeners.running(context);到這里,
spring boot就啟動(dòng)成功了。下面是整個(gè)run方法的源碼,雖然不長(zhǎng),但是我感覺(jué)讀起來(lái)還是有點(diǎn)吃力,想想自己模仿spring boot寫(xiě)的demo,真的是小巫見(jiàn)大巫。

總結(jié)
spring boot啟動(dòng)過(guò)程雖然看起來(lái)簡(jiǎn)單,用起來(lái)簡(jiǎn)單,但是當(dāng)我一行一行看源碼的時(shí)候,我覺(jué)得不簡(jiǎn)單,就好比老遠(yuǎn)看一棵大樹(shù),不就是一個(gè)直立的桿嘛,但是當(dāng)你抵近看的時(shí)候,你會(huì)發(fā)現(xiàn)樹(shù)干有樹(shù)杈,樹(shù)杈又有小樹(shù)杈,總之看起來(lái)盤(pán)根錯(cuò)節(jié)的,總是感覺(jué)看不到樹(shù)真實(shí)的樣子。不過(guò),隨著后面我們不斷地將spring boot的樹(shù)葉、小樹(shù)杈一一拿掉的時(shí)候,我相信我們會(huì)越來(lái)越清楚地看到spring boot這棵大樹(shù)真實(shí)的樣子。
今天的內(nèi)容,其實(shí)如果有一張時(shí)序圖,看起來(lái)就比較友好了,但是由于時(shí)間的關(guān)系,今天來(lái)不及做了,我們明天爭(zhēng)取把時(shí)序圖搞出來(lái)。
另外,后面我還會(huì)把今天一筆帶過(guò)的方法盡可能詳細(xì)地研究然后講解的,我的目標(biāo)就是由大到?。◤臉?shù)干到樹(shù)杈,最后到樹(shù)葉)地剖析spring boot的源碼,最后把spring boot的核心技術(shù)梳理清楚。好了,今天就先到這里吧!
