解密Springboot內(nèi)嵌Tomcat

Springboot簡介
相信大多數(shù)開發(fā)者對Springboot比較熟悉了,它能夠快速地創(chuàng)建一個spring應用怎诫,能夠完全摒棄XML的配置方式虐唠,并且內(nèi)嵌了Tomcat、Jetty這樣的Servlet容器少态,即無需再將應用打包成war部署城侧。

在Springboot之前,部署一個應用如下


而現(xiàn)在彼妻,由于Springboot內(nèi)嵌了Servlet容器嫌佑,于是可以將應用打包成jar,直接運行一個jar包就能啟動一個web服務侨歉。
Springboot是如何做到的呢屋摇?接下來進入今天的正題

Tomcat-embed
Springboot能夠?qū)omcat內(nèi)嵌,是因為Tomcat提供了一套JavaAPI幽邓,能夠通過Tomcat tomcat = new Tomcat()來創(chuàng)建一個Tomcat容器炮温。
只需要引入Maven依賴

<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>

接下來我們通過Springboot源碼來看看,spring是如何使用這套API與自身結(jié)合的

Springboot源碼解讀
首先牵舵,任意一個Springboot應用柒啤,都有一個main()函數(shù)作為應用的啟動方法倦挂,里面調(diào)用了SpringApplication.run(MyApplication.class, args),我們就從這個run()開始白修,解密spring容器如何啟動Tomcat妒峦。

這個run()的實現(xiàn)代碼如下,這里去掉了一些與主線邏輯無關(guān)的代碼

/**
* Run the Spring application, creating and refreshing a new
* {@linkApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
ConfigurableApplicationContext context = null;
// 創(chuàng)建spring容器對象 ApplicationContext
context = createApplicationContext();
// 做一些初始化容器之前的預備操作
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 啟動spring容器核心方法兵睛,包括啟動tomcat
refreshContext(context);
// 做一些容器啟動之后的操作(當前這個方法是空的)
afterRefresh(context, applicationArguments);
return context;
}
看過Spring源碼的同學應該清楚肯骇,啟動spring容器的核心方法就是refresh(),而這里的方法名叫refreshContext(context)祖很,不過是springboot對refresh()的封裝罷了笛丙,所以內(nèi)部依然是調(diào)用的refresh(),于是我們來到這個核心方法內(nèi)部

@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 在容器啟動之前做一些準備操作
prepareRefresh();

// 通知子類去刷新實例工廠
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 初始化當前容器中的實例工廠
prepareBeanFactory(beanFactory);

try {

// 允許容器中的基類對實例工廠進行后置處理
postProcessBeanFactory(beanFactory);

// 調(diào)用實例工廠中的后置處理器
invokeBeanFactoryPostProcessors(beanFactory);

// 注冊實例的后置處理器假颇,在創(chuàng)建實例時進行攔截
registerBeanPostProcessors(beanFactory);

// 初始化消息資源
initMessageSource();

// 初始化容器中的事件處理器
initApplicationEventMulticaster();

// 初始化一些在特定容器中特殊的實例
onRefresh();

// 檢查監(jiān)聽器的實例并注冊它們
registerListeners();

// 實例化所有非懶加載的實例
finishBeanFactoryInitialization(beanFactory);

// 最后一步:發(fā)布一些響應事件
finishRefresh();
}
catch (BeansException ex) {
...
}
finally {
...
}
}
}

當這個方法調(diào)用完胚鸯,spring容器就基本完成了初始化過程,tomcat當然也是在這個方法內(nèi)部完成的創(chuàng)建

創(chuàng)建WebServer容器

Springboot內(nèi)嵌的各種web容器實例笨鸡,都是在onRefresh()中進行創(chuàng)建的姜钳。
查看方法實現(xiàn)可以發(fā)現(xiàn)這個方法是個空方法

protected void onRefresh() throws BeansException {
// For subclasses: do nothing by default.
}

但是其子類的都實現(xiàn)了這個方法,子類列表如下


因為Tomcat是一個Servlet容器形耗,所以我們直接看ServletWebServerApplicationContext這個類中是如何實現(xiàn)的

進入這個類中哥桥,該方法的實現(xiàn)代碼比較容易看懂,重點在createWebServer()激涤,這里同樣只保留了主要代碼


@Override
protected void onRefresh() {
super.onRefresh();
createWebServer();
}

private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 通過工廠創(chuàng)建WebServer實例
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
}
}

前面1-5步都是在配置Tomcat拟糕,而完成Tomcat是在第6步getTomcatWebServer(tomcat)完成的,我們接著進去看看

getTomcatWebServer(tomcat)方法返回一個TomcatWebServer對象
TomcatWebServer對象是springboot對Tomcat對象的封裝倦踢,內(nèi)部存了tomcat實例的引用送滞,這里執(zhí)行的是TomcatWebServer的構(gòu)造方法,

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0);
}

構(gòu)造方法內(nèi)部如下

public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
initialize();
}

我們主要來看initialize()辱挥,在這個方法內(nèi)部會調(diào)用tomcat.start()

private void initialize() throws WebServerException {
synchronized (this.monitor) {
try {
// 給Engine命名
addInstanceIdToEngineName();
// 獲取Host中的Context
Context context = findContext();
// 綁定Context的生命周期監(jiān)聽器
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
removeServiceConnectors();
}
});
// 啟動tomcat犁嗅,觸發(fā)初始化監(jiān)聽器
this.tomcat.start();
// 啟動過程中子線程的異常從主線程拋出
rethrowDeferredStartupExceptions();
// 給當前Context綁定類加載器
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
// tomcat的所有線程都是守護線程,這里啟動一個阻塞的非守護線程來確保tomcat能及時停止
startDaemonAwaitThread();
}
catch (Exception ex) {
...
}
}
}

要注意的是這個方法啟動的是tomcat工作需要的一些守護線程晤碘,并非是web服務愧哟;我們可以來看看start()的定義

/**
* Prepare for the beginning of active use of the public methods other than
* property getters/setters and life cycle methods of this component. This
* method should be called before any of the public methods other than
* property getters/setters and life cycle methods of this component are
* utilized. The following {@link LifecycleEvent}s will be fired in the
* following order:
* <ol>
* <li>BEFORE_START_EVENT: At the beginning of the method. It is as this
* point the state transitions to
* {@link LifecycleState#STARTING_PREP}.</li>
* <li>START_EVENT: During the method once it is safe to call start() for
* any child components. It is at this point that the
* state transitions to {@link LifecycleState#STARTING}
* and that the public methods other than property
* getters/setters and life cycle methods may be
* used.</li>
* <li>AFTER_START_EVENT: At the end of the method, immediately before it
* returns. It is at this point that the state
* transitions to {@link LifecycleState#STARTED}.
* </li>
* </ol>
*
* @exception LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
public void start() throws LifecycleException;

啟動Web服務

真正完成springboot啟動的方法,依然是由TomcatWebServer這個類完成的哼蛆,這個類封裝了控制整個web服務聲明周期的方法蕊梧,比如initialize(), start(), stop()等等,而這個start()顯然就是web服務啟動的方法了腮介。

@Override
public void start() throws WebServerException {
synchronized (this.monitor) {
Connector connector = this.tomcat.getConnector();
if (connector != null && this.autoStart) {
performDeferredLoadOnStartup();
}
logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '"
+ getContextPath() + "'");
}
}

看到這個方法里的這行l(wèi)og肥矢,大家應該很熟悉了,說明最終啟動web容器的函數(shù)就是performDeferredLoadOnStartup()
performDeferredLoadOnStartup()方法內(nèi)部邏輯如下

private void performDeferredLoadOnStartup() {
for (Container child : this.tomcat.getHost().findChildren()) {
if (child instanceof TomcatEmbeddedContext) {
((TomcatEmbeddedContext) child).deferredLoadOnStartup();
}
}
}

public void deferredLoadOnStartup() throws LifecycleException {
doWithThreadContextClassLoader(getLoader().getClassLoader(),
() -> getLoadOnStartupWrappers(findChildren()).forEach(this::load));
}


getLoadOnStartupWrappers()負責將Wrapper按照loadOnStartup的優(yōu)先級進行重排序,這里的主要邏輯就是按照Wrapper的優(yōu)先級甘改,依次調(diào)用wrapper.load()旅东,然后打印"Tomcat started on port …"完成web容器啟動。

最后十艾,還有一個問題未解答抵代,就是start()是何時調(diào)用的?

start()何時調(diào)用
要解答這個問題忘嫉,可以通過觀察springboot的啟動日志荤牍,看"Tomcat started on port …"這行日志是何時打印的。
按照這個方法庆冕,我們不難發(fā)現(xiàn)康吵,這行日志仍然是在spring容器的refresh()里打印出來的,繼續(xù)深入最終是鎖定了refresh()里的最后一步访递,也就是finishRefresh()
ServletWebServerApplicationContext重寫了finishRefresh()

@Override
protected void finishRefresh() {
super.finishRefresh();
WebServer webServer = startWebServer();
if (webServer != null) {
publishEvent(new ServletWebServerInitializedEvent(webServer, this));
}
}

private WebServer startWebServer() {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.start();
}
return webServer;
}

上面方法中的WebServer是一個接口晦嵌,里面有三個方法start(), stop(), getPort() TomcatWebServer對其進行了實現(xiàn),其中的start()就是在這里調(diào)用的拷姿,這里也就是最終啟動web服務的地方惭载。

至此,Springboot內(nèi)嵌web服務器的秘密解開了响巢。

總結(jié)
Spring框架基于接口的設計模式描滔,使得整個框架具有良好的擴展性。我們再來回顧下spring是如何整合tomcat的抵乓。

首先伴挚,通過繼承AbstractApplicationContext靶衍,重寫onRefresh()對web容器進行初始化灾炭,重寫finishRefresh()啟動web服務。

其次颅眶,spring抽象了WebServer接口蜈出,提供了“啟動”和“停止”兩個基本方法,具體方法由不同的web容器各自實現(xiàn)涛酗,其中tomcat的實現(xiàn)類叫TomcatWebServer铡原。

最后,TomcatWebServer在構(gòu)造方法中調(diào)用initialize()初始化tomcat商叹,調(diào)用start()方法啟動web服務燕刻,在spring容器銷毀之前調(diào)用stop()完成tomcat生命周期。




?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剖笙,一起剝皮案震驚了整個濱河市卵洗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌弥咪,老刑警劉巖过蹂,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件十绑,死亡現(xiàn)場離奇詭異,居然都是意外死亡酷勺,警方通過查閱死者的電腦和手機本橙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脆诉,“玉大人甚亭,你說我怎么就攤上這事】馑担” “怎么了狂鞋?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長潜的。 經(jīng)常有香客問我骚揍,道長,這世上最難降的妖魔是什么啰挪? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任信不,我火速辦了婚禮,結(jié)果婚禮上亡呵,老公的妹妹穿的比我還像新娘抽活。我一直安慰自己,他們只是感情好锰什,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布下硕。 她就那樣靜靜地躺著,像睡著了一般汁胆。 火紅的嫁衣襯著肌膚如雪梭姓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天嫩码,我揣著相機與錄音誉尖,去河邊找鬼。 笑死铸题,一個胖子當著我的面吹牛铡恕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丢间,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼探熔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了烘挫?” 一聲冷哼從身側(cè)響起诀艰,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后涡驮,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體暗甥,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年捉捅,在試婚紗的時候發(fā)現(xiàn)自己被綠了撤防。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡棒口,死狀恐怖寄月,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情无牵,我是刑警寧澤漾肮,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站茎毁,受9級特大地震影響克懊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜七蜘,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一谭溉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧橡卤,春花似錦扮念、人聲如沸雷则。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拳氢。三九已至,卻和暖如春榄檬,著一層夾襖步出監(jiān)牢的瞬間饵婆,已是汗流浹背笼吟。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工伞鲫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粘茄,地道東北人签舞。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓秕脓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親儒搭。 傳聞我的和親對象是個殘疾皇子吠架,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355