Tomcat 源碼分析 主要構件 (基于8.0.5)

1. Tomcat 定義

很多書籍都介紹 Tomcat 是 "一個免費開源的Servlet容器", 的確 Tomcat 是參考 Servlet 規(guī)范實現出來, 它是一個Servlet容器, 而現在在我看來 Servlet 容器是Tomcat 特性的一部分, 另外很大一部分是: Tomcat 是一個代碼優(yōu)美, 設計精巧,層次分明, 擴張性很好的 網絡服務框架

2. Tomcat 架構

為什么這么說呢, 來看一個 Tomcat 的架構圖吧(圖片若不清晰的話, 在瀏覽器中新開一個窗口, 放大看!)

tomcat_arch.png

上面這張圖展現是 Tomcat 物理視圖層面的架構, 我們來分析一個各個組件的作用

1. Bootstrap   是Tomcat 默認的引導啟動了, 我們平時 執(zhí)行 ${catalina.base}/bin/startup.sh 就是 將 Bootstrap 當做 main 類 進行啟動(PS: Bootstrap 有個非常重要的功能, 就是定義整個程序的 classLoader 層級, 這個在 Classloader 中會詳細敘述)
2. Catalina    在 Tomcat 中, Catalina其實就是代表了整個 服務控制中心, 其控制著整個 Tomcat 的 start, stop, await, 配置文件測試, 將配置文件 server.xml 解析成對應的 StandardServer, 觸發(fā) StandardServer 及其子組件的 init, start, stop, destory 等操作
3. Server      一個Tomcat程序, 一個 Server, 上面 Catalina 的 start, stop, await 其實都是代理 Server 操作的, 而Catalina 又比 Server 多了 解析配置文件 server.xml
4. Service     在代碼層面看到, 理論上一個 Server可以包含多個 Service, 而Service 對應多個 請求連接處理器 connector 與一個子容器 Container, 另一個非常重要的屬性就是 Mapper (路由模塊)
5. Mapper      請求路由模塊, 這個模塊里面的數據就是整個容器的各個層級的容器的信息, 每次容器中有組件變化, 就會觸發(fā) MapperListener, 而MapperListener 又會觸發(fā)Mapper 里面數據的變化(PS:Tomcat支持動態(tài)增加容器, 而當容器變化時, 對應的路由信息也要進行相應的變化, 這個工作就交由 MapperListener來完成)
    在進行路由請求時, 請求連接器 connector 將通過 Service 獲取對應 路由模塊Mapper 進行路由操作
6. Engine      連接器connector 接到請求, 通過路由模塊就能找到對應的 Engine, 并且整個消息經過容器的第一層就是 Engine, 那Engine 是什么作用呢? 路由到請求的 host, 在整個 Tomcat 中可能有好幾個 Host 
    (一個host其實就是代表一個域名, 而大家也知道, 現在的一塔物理機可以同時又好幾個域名指向它), 所以需要 Engine 進行路由 (PS: 在產線上一般都是一臺物理機器 一個Tomcat, 而 Engine 默認路由的也是其 defaultHost)
    見 ${catalina.base}/conf/server.xml 中的 "<Engine name="Catalina" defaultHost="localhost">" 現在是不是有點感覺了
7. Host        一個host其實就是代表一個域名, 這里的Host 主要完成 Context的部署(deployOnStartup, 默認目錄 ${catalina.base}/webapps/)/自動部署(autoDeploy), host 層面的配置文件的解析, 在部署環(huán)節(jié)需要 HostConfig 進行相應操作 (HostConfig非常重要) 
8. Context      代表Tomcat容器里面的一個項目, 也就是我們平時在 ${catalina.base}/webapps/ 下面放的一個 war 包 (其中包括 WEB-INF/lib, WEB-INF/classes, web.xml 等)
9. Wrapper      一個Servlet 就代表一個 Wrapper(單例模式), 所有的請求都會被servlet來處理(JSP 由JSPServlet來處理, 靜態(tài)文件由WebdavServlet來處理), 現在一般項目都由 Spring 的 Dispatcherservlet 來統一處理, 而Dispatcherservlet 又非常大
    當然 Tomcat 也支持 一個線程一個 Servlet這種線程安全的 Servlet 模式, 每個Servlet 最多對應 20個實例, 若出現第21個請求來(針對同一個 Servlet), 請求就會被阻塞掉 (啃爹哇, 若使用這種模式 + Spring的DispatchServlet 則Tomcat的并發(fā)請求就被限制在20了...)
10. ApplicationFilterChain 這是一條 Filter 的處理鏈, 程序會根據URI匹配對應的 Filter, 最后設置 Servlet, 程序會依次執(zhí)行 ApplicationFilterChain里面的Filter(通過遞歸), 最后執(zhí)行對應的 Servlet (到這里只是處理了 Servlet 里面的業(yè)務邏輯), 而正真的寫數據是在 CoyoteAdapter 中的 response.finishResponse()

對了還有一個 PipeLine(管道) + Valve(閥門) 
11. PipeLine    每個容器都有其對應的 PipeLine, PipeLine 里面裝載著 Valve(以鏈表的形式), 此時是不是想到了 netty 里面的 ChannelPipeline, 只是 netty 里面叫做 valve 叫做了 ChannelHandler, 并且由其 ChannelHandlerContext 控制了鏈表的結構

到這里, 大家可能會困惑, 干嘛定義這么多對象, 直接定義一個 請求連接處理模塊 + 一個 封裝Servlet的模塊 不久行了
額  沒事 我們來看一下這個  
    Example: 我自己的服務器, 假設 IP: 23.89.15.9, 在這個服務器上有兩個域名指向它, 一個是 www.tuomatuo.com, 一個是 localhost

    場景一: 請求 URL = http:www.tuomatuo.com:8080/manager/login.do
        (1). www.tuomatuo.com   就代表組件中的 Host
        (2). manager             代表我們在 ${catalina.base}/webapps/ 下面部署的運用, 也就是 Host 的子容器 Context
        (3). login.do           就是 Servlet匹配的URI了, 你可以假想成代表 Wrapper
    場景二: 請求 URL = http:localhost:8080/taobao/index.do
        (1). localhost          就代表組件中的 Host
        (2). taobao               代表我們在 ${catalina.base}/webapps/ 下面部署的運用, 也就是 Host 的子容器 Context
        (3). index.do           就是 Servlet匹配的URI了, 你可以假想成代表 Wrapper
    從上面的兩個場景中, 我們得知 Tomcat 支持多 Host, 并且每個 Host 下面可以部署多個 Context, 每個Context 下面也可以有多個 Servlet 
3. Tomcat 代碼結構

tomcat 整體的代碼結構比較清晰

javax:                  用Java語言實現的 Servlet API (基本上都是接口 也就是我們 Maven 里面的 javax.servlet-api 包)
org.apache.catalina:    這個包下面的代碼就是整個 catalina, 里面包含一層一層的容器
org.apache.coyote:      這個包下面的代碼就是Tomcat的網絡處理框架, 主要處理的協議有 http1.1, ajp, http2.0 (每種協議對應 bio, nio, aio, 其實這里可以用 成熟的 IO框架, 比如 netty, mina)
org.apache.el:          el 表達式, 這個東西好像是個遠古生物, 暫時還沒使用過
org.apache.jasper:      jsp 解析成對應Servlet, 變成為 class, 通過 jasperClassLoader 加載進來, jsp 熱部署 都是這個 package 下面的代碼完成的 
org.apache.juli:        tomcat 的日志框架(默認日志的配置文件為 ${catalina.base}/conf/logging.properties)
org.apache.naming:      tomcat 中資源服務管理類包(主要是 jndi 的操作, 其中涉及對加載資源的尋找 + 監(jiān)控是否修改 -> 熱部署)
org.apache.tomcat:      tomcat 的工具包 主要是 threads, scan, net, digester, buf, codec
4. Tomcat Lifecycle 組件

Lifecycle 定義了組件的生命周期事件, 以及因有的監(jiān)聽器接口

lifecycle.png

Tomcat中有大量的實現類: StandardServer, StandardService, StandardEngine, StandardHost, StandardContext, StandardWrapper

lifecycle_imp.png
這里以 StandardServer 為例子進行說明:
class StandardServer extends LifecycleMBeanBase implements Server 
StandardServer 繼承 LifecycleMBeanBase 實現 Server
從上面的 UML 圖中, 我們知道 
    1. 接口 Server 繼承 Lifecycle 
    2. LifecycleMBeanBase 繼承 LifecycleBase, LifecycleBase 又實現了 Lifecycle 
結論: 接口 Server 中包含 Lifecycle 中未實現的方法,  類 LifecycleMBeanBase 中又有 Lifecycle 實現了的方法
    那其實 對于 StandardServer, 沒有必要讓 接口Server 繼承Lifecycle, 從而獲取 Lifecycle 的生命周期方法?
    代碼完全可以變成: class StandardServer implements Server, Lifecycle  (PS: 其中 Server 不繼承 Lifecycle)
    若真的這樣的話, 等過了幾版 Tomcat 后, 代碼可能變成 class StandardServer implements Server, 我想這是 Catalina 作者不想看到的
    所以, 你看到在 Tomcat 中的基礎接口都繼承了 Lifecycle(如 Server, Service, Container, Executor, WebResourceRoot, WebResourceSet)

這時我們在想,為什么Tomcat中有這么多的 Lifecycle 實現類, 主要是 Tomcat中的組件支持動態(tài)增加, 并且在不同生命周期都有對應的事件需要處理, 當然我們也可以在我們的項目中使用 Lifecycle 這個概念(比如Spring里面Bean的生命周期, 只是與Tomcat中的組件的生命周期的環(huán)節(jié)不一樣)

5. Tomcat Valve 組件

Valve 作用:

  1. 在 Pipeline 路由到下一級容器 
  2. 起到AOP的作用, 在調用前, 調用后 起到環(huán)繞監(jiān)視的作用
  3. 還可以給 Request, Response 傳輸的對象起到分層次加工的作用

見下面的類實現圖:

valve.png

下面是主要子類的實現

ValveBase:                    主要完成子類的一些公共的方法
JDBCAccesslogValve:           將 Tomcat 訪問的日志記錄信息記錄到 數據庫里面
PersistentValve:              在配置了 PersistentManager 的情況下, 會對每次請求后 持久化 Session (這個有點扯蛋, 若請求量一大, 則程序直接掛了)
SemaphoreValve:               可以附屬于任何 Container 的,  用于控制并發(fā)請求 的 Valve (內部使用 Semaphore 來實現, 其實在 connector 內部也有控制請求數的處理類 LimitLatch)
ErrorReportValve:             檢測 Http 請求過程中是否出現過什么異常, 有異常的話, 直接拼裝 html 頁面, 輸出到客戶端
RemoteIpValve:                通常請求到達 Tomcat 會經過多層的反向代理, 這個 Valve 的作用就是 根據 Header 里面的信息, 將真實的 IP 地址信息設置到 request 里面 (其中也涉及到將 IP 等信息加入到只有 Accesslog 才會使用的屬性中)
SingleSignOn:                 單點登錄 Valve, Tomcat 集群中能使用到, 通過 cookie 機制
RequestFilterValve:           對請求進行過濾的 Valve, 主要是 IP, Host (見其子類 RemoteHostValve, RemoteAddrValve)
CrawlerSessionManagerValve:   通過解析 Http header 里面的 user-agent 來實現反爬蟲的 Valve, 其實可以加上 refer(這個作用不大), 主要目的還是為了防止 大量爬蟲請求, 而導致創(chuàng)建大量 Session

StandardEngineValve:          請求路由 Valve, 通過 Request 里面的信息, 將請求路由到對應的 StandardHost
StandardHostValve:            根據請求的信息將其路由到對應的 StandardContext (PS: 其中比較重要的是, 在每次進行操作前后, 需要更改對應線程的 ContextClassloader 為 StandardContext 中的 WebappClassloader)
StandardContextValve:         根據 Request 里面的信息, 將請求路由到對應的 wrapper 中
StandardWrapperValve:
                                (1). 根據 URI 獲取 對應 Servlet, 若還沒有生成, 則進行相應的創(chuàng)建 (通過 StandardContext 的 實例創(chuàng)建類 InstanceManager 進行創(chuàng)建, 其會對 Servlet 上的注解進行相應的處理, 最后會調用 servlet.init() 方法)
                                (2). 根據請求的URI 獲取相應的 Filter, servlet 組裝成 ApplicationFilterChain 進行相應處理 (每一個 ApplicationFilterConfig 代表一個 Filter 封裝類, 在第一次請求后會通過反射生成對應的 Filter 實例, 以后就用這個 Filter)
                                (3). 釋放 ApplicationFilterChain 里面的 Filter, Servlet 資源
6. Tomcat Filter 組件

Filter 存在于 ApplicationFilterChain 中主要是對請求的參數 進行一些過濾/修飾措施, 現對于 Valve, 其可以控制請求是否流向下層組件,并且在實際代碼中 Filter 請求下個節(jié)點是通過遞歸的方法進行, 一開始程序中是沒有 Filter 對象的, 在第一次請求過后, 通過 StandardContext 的實例生成器 InstanceManager 來生成(InstanceManager 會處理 Filter 上注解修飾的一些東西), 后面直接緩存在 StandardContext 里面
下面是Tomcat中主要的Filter, 見圖

filter.png
FilterBase:                   通過反射工具類 IntrospectionUtils 將 filter 里面的一些屬性設置進去(這些參數的設置通過 web.xml 或  context.xml)
CometFilter:                  長連接的 filter (鑒于在 Tomcat 9.x.x 中移除了 comet 功能, 所以這里....)
RequestFilter:                定義請求過濾模板方法, 主要由子類 RemoteAddrValve, RemoteHostValve 來時現對應的邏輯
RemoteIpFilter:               對請求的 IP 進行限制的 Filter
ExpiresFilter:                通過這個 Filter, 在請求處理后, 在 http header 中控制緩存時間的信息 
SetCharacterEncodingFilter:   在請求處理之前(即 Request 對 請求參數處理之前) 設置一個 Request 的編碼格式
AddDefaultCharsetFilter:      負責統一設置 Response 處理的編碼格式 (這個暫且還沒有過)
7. Tomcat LifecycleListener 組件

LifecycleListener: Tomcat 容器的生命周期監(jiān)聽器

  1. 監(jiān)聽實時修改路由的規(guī)則
  2. 監(jiān)控部署目錄, 完成自動部署
  3. 監(jiān)控容器, 在停止時查看是否存在內存泄露等問題

下面是其主要的實現類, 見圖:

LifecycleListener.png

JreMemoryLeakPreventionListener:        這個監(jiān)聽器是在容器 init 之前, 將做一些公共類加入到 commonClassloader 中, 啟動節(jié)省內存的作用
                                        何時出發(fā)這個 Listener ? BEFORE_INIT_EVENT 就是在 監(jiān)聽的容器組件 init 調用init之前, 將一些公共的數據先加載到 CommonClassLoader 里面 (看代碼中 直接將 Thread.ContextClassLoader 設置為 ClassLoader.getSystemClassLoader())
                                        這里所做的 保護內存泄露 無非就是 將 本來在 每個 WebappClassLoader 中都進行加載的 class, 事先在 commonClassLoader 里面進行加載一遍, 比如說 數據庫連接驅動 等(PS: 代碼中其他的一些也不常用)             
MemoryLeakTrackingListener:             MemoryLeakTrackingListener 歸屬于 StandardContext
                                        其用 WeakHashMap 裝載 WebAppClassLoader, 等關閉 StandardContext 之后, 在看看 與之對應的 WebappClassLoader 是否存活, 若還存在, 則說明 WebappClassLoader 沒有沒 GC, 存在 Perm 區(qū)域內存泄露
                                        見 StandardHost.findReloadedContextMemoryLeaks()
ThreadLocalLeakPreventionListener:      防止因 ThreadLocal 的存在, 而造成內存泄露
                                        在進行 Tomcat 熱部署時, 工作線程是不會停止的, 而需要關閉 StandardContext 對應的 WebappClassLoader, 而 ThreadLocal.threadLocalMap 里面有存儲了 由WebappClassLoader 加載出來的類, 所以有可能導致 WebappClassLoader 因被引用而不能被 GC, 最終導致內存泄露
                                        (PS:  ThreadLocalMap 的生命長度與 Thread 一樣, Tomcat中的工作線程池不因 StandardContext 的stop, 而銷毀)
                                        見官網 : https://wiki.apache.org/tomcat/MemoryLeakProtection
MapperListener:                         MapperListener 歸屬于 StandardService, 在各個組件/容器進行init/start 時都會發(fā)出消息通知(這里的消息通知在 LifecycleBase 里面進行操作), MapperListener 會根據Tomcat里面各個組件的組成映射到 Mapper 里面(Mappper 主要是完成請求路由作用, 而路由的信息最終會存儲在 org.apache.catalina.connector.Request 里面(PS: Tomcat里面有兩個 Request))
EngineConfig:                           這里 EngineConfig 是 StandardEngine 容器生命周期的監(jiān)聽器, 主要做些日志記錄
HostConfig:                             HostConfig 是 StandardHost 的監(jiān)聽器, 主要是下面兩張用途
                                            (1) StandardHost 后臺周期性檢測是否需要重新部署 (三種方式 xml, war包, 文件夾), 我們平時在 ${catalina.base}/webapps/ 下面部署 war/文件夾, 而對應的解析加載工作就是這里做的 (見 HostConfig.deployDirectory())
                                            (2) 通過 MBeanFactory 或 HostManagerServlet 觸發(fā)部署操作
ContextConfig:                         主要功能的 :
                                            (1): 組裝 web.xml 的解析器 WebXmlParser
                                            (2): 根據 Host的 appBase 以及 Context的 docBase 計算 docBase 的絕對路徑
                                            (3). 掃描 web.xml 文件, 在遇到全局 web.xml 或 host 層面的 web.xml, 則應用層面的 web.xml 的屬性能覆蓋上面兩個, 這里面的知識點非常多, 通過 SPI 機制加載 ServletContainerInitializer, 并且將它們 set 到對應的 StandardContext
                                            (4). 解析應用程序注解配置 主要是 (listener, Filter, Servlet類的, 最后會將這些資源信息加入到 StandardContext.NamingResourcesImpl 里面, 在實例化 Servlet/Filter/Listener 時會用到)
8. 總結

Tomcat 作為一個互聯網上的web服務器, 存在了這么多年, 肯定是有其原因的, 還是很建議大家有時間的話去看看 Tomcat 的源碼, 你會發(fā)現里面有非常多的優(yōu)秀的設計的地方(比如, Tomcat 中網絡連接層設計, Tomcat 里面的 classLoader 設計, Tomcat中所使用的設計模式, Tomcat 的熱部署, Tomcat 的Session實現及管理機制 等等)

9. 參考

Tomcat 7.x.x 源碼分析
Tomcat 5.x.x 源碼分析
Tomcat 7.0 原理與源碼分析
Tomcat 內核設計剖析
Tomcat 架構解析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容