SpringBoot 就像一條巨蟒,慢慢纏繞着我們,使我們麻痹。不得不承認,使用了 SpringBoot 確實提高了工作效率,但同時也讓我們遺忘了很多技能。剛入社會的時候,我還是通過 Tomcat 手動部署 JavaWeb 項目,還經常對 Tomcat 進行性能調優。除此之外,還需要自己理清楚各 Jar 之間的關系,以避免 Jar 丟失和各版本沖突導致服務啟動異常的問題。到如今,這些繁瑣而又重復的工作已經統統交給 SpringBoot 處理,我們可以把更多的精力放在業務邏輯上。但是,清楚 Tomcat 的工作原理和處理請求流程和分析 Spring 框架源碼一樣的重要。至少面試官特別喜歡問這些底層原理和設計思路。希望這篇文章能給你一些幫助。
Tomcat 整體架構
Tomcat 是一個免費的、開源的、輕量級的 Web 應用服務器。適合在並發量不是很高的中小企業項目中使用。
文件目錄結構
以下是 Tomcat 8 主要目錄結構
目錄 | 功能說明 |
---|---|
bin | 存放可執行的文件,如 startup 和 shutdown |
conf | 存放配置文件,如核心配置文件 server.xml 和應用默認的部署描述文件 web.xml |
lib | 存放 Tomcat 運行需要的jar包 |
logs | 存放運行的日志文件 |
webapps | 存放默認的 web 應用部署目錄 |
work | 存放 web 應用代碼生成和編譯文件的臨時目錄 |
功能組件結構
Tomcat 的核心功能有兩個,分別是負責接收和反饋外部請求的連接器 Connector,和負責處理請求的容器 Container。其中連接器和容器相輔相成,一起構成了基本的 web 服務 Service。每個 Tomcat 服務器可以管理多個 Service。
組件 | 功能 |
---|---|
Connector | 負責對外接收反饋請求。它是 Tomcat 與外界的交通樞紐,監聽端口接收外界請求,並將請求處理后傳遞給容器做業務處理,最后將容器處理后的結果反饋給外界。 |
Container | 負責對內處理業務邏輯。其內部由Engine、Host、Context 和 Wrapper 四個容器組成,用於管理和調用 Servlet 相關邏輯。 |
Service | 對外提供的 Web 服務。主要包含連接器和容器兩個核心組件,以及其他功能組件。Tomcat 可以管理多個 Service,且各 Service 之間相互獨立。 |
Tomcat 連接器核心原理
Tomcat 連接器框架——Coyote
連接器核心功能
一、監聽網絡端口,接收和響應網絡請求。
二、網絡字節流處理。將收到的網絡字節流轉換成 Tomcat Request 再轉成標准的 ServletRequest 給容器,同時將容器傳來的 ServletResponse 轉成 Tomcat Response 再轉成網絡字節流。
連接器模塊設計
為滿足連接器的兩個核心功能,我們需要一個通訊端點來監聽端口;需要一個處理器來處理網絡字節流;最后還需要一個適配器將處理后的結果轉成容器需要的結構。
組件 | 功能 |
---|---|
Endpoint | 端點,用來處理 Socket 接收和發送的邏輯。其內部由 Acceptor 監聽請求、Handler 處理數據、AsyncTimeout 檢查請求超時。具體的實現有 NioEndPoint、AprEndpoint 等。 |
Processor | 處理器,負責構建 Tomcat Request 和 Response 對象。具體的實現有 Http11Processor、StreamProcessor 等。 |
Adapter | 適配器,實現 Tomcat Request、Response 與 ServletRequest、ServletResponse之間的相互轉換。這采用的是經典的適配器設計模式。 |
ProtocolHandler | 協議處理器,將不同的協議和通訊方式組合封裝成對應的協議處理器,如 Http11NioProtocol 封裝的是 HTTP + NIO。 |
對應的源碼包路徑 org.apache.coyote
。對應的結構圖如下
Tomcat 容器核心原理
Tomcat 容器框架——Catalina
容器結構分析
每個 Service 會包含一個容器。容器由一個引擎可以管理多個虛擬主機。每個虛擬主機可以管理多個 Web 應用。每個 Web 應用會有多個 Servlet 包裝器。Engine、Host、Context 和 Wrapper,四個容器之間屬於父子關系。
容器 | 功能 |
---|---|
Engine | 引擎,管理多個虛擬主機。 |
Host | 虛擬主機,負責 Web 應用的部署。 |
Context | Web 應用,包含多個 Servlet 封裝器。 |
Wrapper | 封裝器,容器的最底層。對 Servlet 進行封裝,負責實例的創建、執行和銷毀功能。 |
對應的源碼包路徑 org.apache.coyote
。對應的結構圖如下
容器請求處理
容器的請求處理過程就是在 Engine、Host、Context 和 Wrapper 這四個容器之間層層調用,最后在 Servlet 中執行對應的業務邏輯。各容器都會有一個通道 Pipeline,每個通道上都會有一個 Basic Valve(如StandardEngineValve), 類似一個閘門用來處理 Request 和 Response 。其流程圖如下。
Tomcat 請求處理流程
上面的知識點已經零零碎碎地介紹了一個 Tomcat 是如何處理一個請求。簡單理解就是連接器的處理流程 + 容器的處理流程 = Tomcat 處理流程。哈!那么問題來了,Tomcat 是如何通過請求路徑找到對應的虛擬站點?是如何找到對應的 Servlet 呢?
映射器功能介紹
這里需要引入一個上面沒有介紹的組件 Mapper。顧名思義,其作用是提供請求路徑的路由映射。根據請求URL地址匹配是由哪個容器來處理。其中每個容器都會它自己對應的Mapper,如 MappedHost。不知道大家有沒有回憶起被 Mapper class not found 支配的恐懼。在以前,每寫一個完整的功能,都需要在 web.xml 配置映射規則,當文件越來越龐大的時候,各個問題隨着也會出現
HTTP請求流程
打開 tomcat/conf 目錄下的 server.xml 文件來分析一個http://localhost:8080/docs/api 請求。
第一步:連接器監聽的端口是8080。由於請求的端口和監聽的端口一致,連接器接受了該請求。
第二步:因為引擎的默認虛擬主機是 localhost,並且虛擬主機的目錄是webapps。所以請求找到了 tomcat/webapps 目錄。
第三步:解析的 docs 是 web 程序的應用名,也就是 context。此時請求繼續從 webapps 目錄下找 docs 目錄。有的時候我們也會把應用名省略。
第四步:解析的 api 是具體的業務邏輯地址。此時需要從 docs/WEB-INF/web.xml 中找映射關系,最后調用具體的函數。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<!-- 連接器監聽端口是 8080,默認通訊協議是 HTTP/1.1 -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<!-- 名字為 Catalina 的引擎,其默認的虛擬主機是 localhost -->
<Engine name="Catalina" defaultHost="localhost">
<!-- 名字為 localhost 的虛擬主機,其目錄是 webapps-->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
</Service>
</Server>
SpringBoot 如何啟動內嵌的 Tomcat
SpringBoot 一鍵啟動服務的功能,讓有很多剛入社會的朋友都忘記 Tomcat 是啥。隨着硬件的性能越來越高,普通中小項目都可以直接用內置 Tomcat 啟動。但是有些大一點的項目可能會用到 Tomcat 集群和調優,內置的 Tomcat 就不一定能滿足需求了。
我們先從源碼中分析 SpringBoot 是如何啟動 Tomcat,以下是 SpringBoot 2.x 的代碼。
代碼從 main 方法開始,執行 run 方法啟動項目。
SpringApplication.run
從 run 方法點進去,找到刷新應用上下文的方法。
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
從 refreshContext 方法點進去,找 refresh 方法。並一層層往上找其父類的方法。
this.refresh(context);
在 AbstractApplicationContext 類的 refresh 方法中,有一行調用子容器刷新的邏輯。
this.postProcessBeanFactory(beanFactory);
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
從 onRefresh 方法點進去,找到 ServletWebServerApplicationContext 的實現方法。在這里終於看到了希望。
protected void onRefresh() {
super.onRefresh();
try {
this.createWebServer();
} catch (Throwable var2) {
throw new ApplicationContextException("Unable to start web server", var2);
}
}
從 createWebServer 方法點進去,找到從工廠類中獲取 WebServer的代碼。
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = this.getWebServerFactory();
// 獲取 web server
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
} else if (servletContext != null) {
try {
// 啟動 web server
this.getSelfInitializer().onStartup(servletContext);
} catch (ServletException var4) {
throw new ApplicationContextException("Cannot initialize servlet context", var4);
}
}
從 getWebServer 方法點進去,找到 TomcatServletWebServerFactory 的實現方法,與之對應的還有 Jetty 和 Undertow。這里配置了基本的連接器、引擎、虛擬站點等配置。
public WebServer getWebServer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat();
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();
while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}
this.prepareContext(tomcat.getHost(), initializers);
return this.getTomcatWebServer(tomcat);
}
服務啟動后會打印日志
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8900 (http)
o.apache.catalina.core.StandardService : Starting service [Tomcat]
org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.34
o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal ...
o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 16858 ms
END
文章到這里就結束了,實在是 hold 不住了,周末寫了一整天還沒有寫到源碼部分,只能放在下一章了。再寫真的就要廢了,有什么不對的地方請多多指出。最后完整版可以通過微信公眾號 學英語會編程 閱讀。完整版在這個基礎上添加了一些單詞解析,我們的口號是:英語學得好,源碼看的爽。