Tomcat(二) Tomcat實現:Servlet與web.xml介紹 以及 源碼分析Tomcat實現細節

Tomcat(二) Tomcat實現:

Servlet與web.xml介紹 以及 源碼分析Tomcat實現細節


       在《Tomcat(一) Tomcat是什麼:Tomcat與Java技術 Tomcat與Web應用 以及Tomcat基本框架及相關配置》對Tomcat有了一個大體的認識,下面將深入瞭解Tomcat技術的實現:

      1、先來了解JavaEE Servlet技術的一些對象組件;

      2、再來了Web應用程序部署文件web.xml中對Servlet組件的定義;

      3、最後再從Tomcat源碼分析一些實現細節,重點關注:Tomcat的啓動/初始化、併發線程模式、接收請求與處理、以及Servlet容器的實現。

1、JavaEE Servlet技術

       Tomcat是一個Servlet容器,實現了Servlet規範,可以運行我們自己編寫的Servlet應用程序處理動態請求,並返回響應,下面介紹Listener、Filter、Servlet、Request、Respones這幾個比較常見的對象元素。

1、Listener

      Listener(監聽器)用來監聽一些對象的事件,當事件發生時可以進入到定義監聽器中進行處理,主要有ServletContextListener、HttpSessionListener 和 ServletRequestListener。

       我們程序中可以實現這些JavaEE Servlet API中提供的監聽器接口,然後通過web.xml定義部署發生作用,如ServletContextListenerServlet(Servlet 上下文事件監聽器)接口可以監聽到部署web應用Servlet的狀態改變通知(如創建、關閉),如Spring框架中常見的一些ServletContextListenerServlet實現在web.xml定義如下:

       它們都實現javax.servlet.ServletContextListener接口,如org.springframework.web.context.ContextLoaderListener用來在部署相關Web應用時初始化其上下文環境,在停止退出Web應用時清理其資源,如下:

       Servlet容器需要在開始執行進入應用的第一個請求之前完成Web應用中的監聽器類的實例化,而且保持每一個監聽器的引用直到爲Web應用最後一個請求提供服務。

       更多Listener信息請參考:《Servlet3.1規範(最終版)》第10章

2、Filter

      Filter(過濾器)可以改變HTTP請求的內容、響應、及header信息,過濾器通常不產生響應或像 servlet 那樣對請求作出響應,而是修改或調整到資源的請求,修改或調整來自資源的響應。

       Filter在部署描述符中通過<filter>元素聲明,一個過濾器或一組過濾器可以通過在部署描述符中定義<filter-mapping>來爲調用配置。

       容器部署Web應用時,必須確保它爲過濾器列表中的每一個都實例化了一個適當類的過濾器, 並調用其Filter.init()方法;當容器接收到傳入的請求時,它將獲取列表中所有符合該請求的過慮器組成過慮器鏈(FilterChain),然後通過FilterChain嵌套調用各過慮器的Filter.doFilter()方法(一個過濾器可以使得濾器鏈調用下一個過濾器),傳入ServletRequest 和ServletResponse進行處理。

      當濾器鏈調用完各Filter.doFilter()方法後,會調用相關Servlet.service()方法,從而進行Servlet的處理。注意,Filter.doFilter()可以阻止過濾器鏈接下來的調用,返回時過濾器負責填充Reponse對象。

      過濾器實例移除之前,容器必須先調用過濾器的Filter.destroy()方法。

      如Struts2框架通過在web.xml中定義其實現了Filter接口的StrutsPrepareAndExecuteFilter來作爲處理請求的入口,如下:

       StrutsPrepareAndExecuteFilter.init()在部署Web應用時被容器調用,其實現了Struts2配置文件的讀取及初始化,而在doFilter()方法中找到符合的Action處理程序後,就不再調用chain.doFilter(),而經Action處理後開始返回,如下:

       更多Filter信息請參考:《Servlet3.1規範(最終版)》第6章

3、Servlet

      Servlet 這裏指的是實現了javax.servlet.Servlet接口的處理程序,一般開發中都是繼承HttpServlet,其繼承自實現了Servlet接口的GenericServlet類。

       容器在部署Web應用程序時,或Servlet第一次請求處理時實例化調用Servlet.init()方法;而Servlet.service()方法從上面Filter介紹中可以知道是在各Filter.doFilter()嵌套調用中被調用的,而HttpServlet..service()中會根據請求類型分爲doGet(),doPost()等方法,繼承HttpServlet只需重寫這樣些需要的方法即可,如Tomcat的/conf/web.xml定義了默認的DefaultServlet用來處理靜態內容請求(如html/js/各種圖片),如下:

       而當Servlet容器確定servlet應該從服務中移除時(內存不足等),將調用Servlet.destroy()方法以允許Servlet釋放它使用的任何資源和保存任何持久化的狀態。

       容器中的Servlet實例通常都是單例的(可通過實現SingleThreadModel 接口實現每個請求對應一個Servlet),所以需要注意在多線程下的線程安全問題。

       更多Servlet信息請參考:《Servlet3.1規範(最終版)》第2章

4、Request與Response

       Request和Response都可以在上面這些處理接口程序中通過參數接收到,Request對象封裝了客戶端請求的所有信息,Response對象封裝了從服務器返回到客戶端的所有信息

      每個Request/Response對象只在Servlet.service()方法、或Filter.doFilter()方法的作用域內有效;發生異步處理的情況下,對象一直有效,直到調用AsyncContext.complete()方法。

      除了異步請求時的AsyncContext.startAsync()和AsyncContext.complete()方法,Request/Response對象的實現都不保證線程安全,多線程訪問時需要同步處理。

更多信息請參考:《Servlet3.1規範(最終版)》

2、web.xml文件說明

       上篇文件章介紹過web.xml是Web應用程序部署描述符文件web.xml,描述組成應用程序的servlet和其他組件、以及相關初始化參數等信息

       web.xml中組件加載順序爲:

       context-param -> listener -> filter -> servlet(同類則按編寫順序執行)。

      web.xml常用組件解析:

<web-app>

       <display-name></display-name> <!--WEB應用的名字 -->

       <description></description> <!--WEB應用的描述-->

       <context-param></context-param> <!--context-param元素聲明應用範圍內的初始化參數-->

       <!--例如指定spring加載多個spring配置文件-->

       <context-param>

              <param-name>contextConfigLocation</param-name>

              <param-value>

                     /WEB-INF/applicationContext.xml, /WEB-INF/action-servlet.xml

              </param-value>

       </context-param>

 

       <filter></filter> <!--過濾器將一個名字與一個實現javax.servlet.Filter接口的類相關聯-->

       <filter-mapping></filter-mapping> <!--一旦命名了一個過濾器,就要利用filter-mapping元素把它與一個或多個servlet或JSP頁面相關聯-->
       <listener></listener> <!--事件監聽程序在建立、修改和刪除會話或servlet環境時得到通知。Listener元素指出事件監聽程序類,如Log4j這個廣泛使用的監聽器-->

       <servlet></servlet> <!--在向servlet或JSP頁面制定初始化參數或定製URL時,必須首先命名servlet或JSP頁面。Servlet元素就是用來完成此項任務的。-->

       <servlet-mapping></servlet-mapping> <!--服務器一般爲servlet提供一個缺省的URL:http://host/webAppPrefix/servlet/ServletName,但是,常常會更改這個URL,以便servlet可以訪問初始化參數或更容易地處理相對URL。在更改缺省URL時,使用servlet-mapping元素-->

       <session-config></session-config> <!--如果某個會話在一定時間內未被訪問,服務器可以拋棄它以節省內存,可通過使用HttpSession的setMaxInactiveInterval方法明確設置單個會話對象的超時值,或者可利用session-config元素制定缺省超時值-->

       <mime-mapping></mime-mapping> <!--如果Web應用具有想到特殊的文件,希望能保證給他們分配特定的MIME類型,則mime-mapping元素提供這種保證-->

       <welcome-file-list></welcome-file-list> <!--指示服務器在收到引用一個目錄名而不是文件名的URL時,使用哪個文件(其實就是歡迎界面或者說入口界面一般爲index.*) -->

       <error-page></error-page> <!--在返回特定HTTP狀態代碼時,或者特定類型的異常被拋出時,能夠制定將要顯示的頁面-->

       <taglib></taglib> <!--對標記庫描述符文件(Tag Libraryu Descriptor file)指定別名。此功能使你能夠更改TLD文件的位置, 而不用編輯使用這些文件的JSP頁面-->

       <resource-env-ref></resource-env-ref> <!--聲明與資源相關的一個管理對象-->

       <resource-ref></resource-ref> <!--聲明一個資源工廠使用的外部資源-->

 
       <security-constraint></security-constraint> <!--制定應該保護的URL。它與login-config元素聯合使用-->

       <login-config></login-config> <!--指定服務器應該怎樣給試圖訪問受保護頁面的用戶授權,它與sercurity-constraint元素聯合使用-->

       security-role></security-role> <!--給出安全角色的一個列表,這些角色將出現在servlet元素內的security-role-ref元素的role-name子元素中。分別地聲明角色可使高級IDE處理安全信息更爲容易-->

       <env-entry></env-entry>  <!--聲明Web應用的環境項-->
</web-app>

      更多部署文件信息請參考:《Servlet3.1規範(最終版)》第14章

      另外從Servlet3.0開始可以支持一些註釋直接在代碼中進行定義部署,如下圖中,並不需要web.xml也能運行該Servlet:

3、Tomcat一些實現細節

      上面大體瞭解Servlet技術與web.xml,下面來了解Tomcat的一些具體實現細節,從一些關注點入手:Tomcat的啓動/初始化配置、Tomcat如何實現Servlet容器--處理請求並響應等。

      下面是以Tomcat 8.5.9版本源碼調試分析,不同版本可能有差異;另外,圖片看不清可以另外打開查看。

3-1、Tomcat啓動/初始化

3-1-1、catclina.shstart命令啓動

      Tomcat的項目工程名稱也稱爲"catclina",啓動是通常是通過"catclina.sh start"腳本命令(start.sh也是調用該命令),調用了JDK中的Java虛擬機來運行"org.apache.catalina.startup.Bootstrap"的main()方法入口,腳本內容如下:

3-1-2、設置類加載器/Tomcat的類加載器架構

      先是Bootstrap.initClassLoaders()設置類加載器:CommonClassLoader 、ServerClassLoader 、SharedClassLoader,分別對應的加載目錄爲(根目錄下):/common(存放Tomcat與所有Web應用程序共用的類庫)、/server(只Tomcat使用、而所有Web應用程序不可見的)、/shared(Tomcat不可見、而所有Web應用程序共用)。

      這是通過/conf/catalina.properties設置,默認只爲設置了CommonLoader,所以只會創建CommonLoader,而Server.loader 、SharedLoader都使用CommonLoader,而對應的三個目錄合併爲/lib(這是Tomcat6的簡化改進)。

      另外,在後面初始化每個Web應用程序解析web.xml時,會創建WebappClassLoader,只有對應的Web應用程序可見,加載對應Web應用程序的/WEB-INF/lib裏的類庫。

      所以默認情況下,Tomcat類加載器架構如下:

      其中Bootstrap類加載器爲Java虛擬機提供,包含JDK基本運行時類,而System類加載器用於Tomcat啓動初始化(通常忽略);另外,Tomcat類加載器架構是按照經典的"雙親委派模型"來實現的,即:當類加載器被要求加載特定的類或資源時,它首先將請求委託給父類加載器,然後只有當父類加載器找不到請求的類或資源時,它纔在自己的存儲庫中查找。

      Tomcat的類加載器架構的好處是可以按需要實現Tomcat與Web應用程序、以及不現Web應用程序之間的類庫共享與隔離,如常用的Spring等類庫可以放到共享目錄,爲多個Web應用程序共用;而"雙親委派模型"也是JDK類加載器的架構,可以有效組織類庫的層次結構,避免一個類被不同加載器加載多次(注意,同一個類文件被不同加載器加載表示不同的類)。

      更多JVM類加載器信息請參考:《Java虛擬機規範》第5章 加載、鏈接與初始化

      更多Tomcat類加載器信息請參考:Tomcat Doc《Class Loader HOW-TO》

3-1-3、解析server.xml,創建各Tomcat組件

      如圖,Catalina.load()中創建Diegster實例後,用該實例解析Tomcat的核心配置文件server.xml,然後根據server.xml文件中的配置規則開始創建各個組件,如Server、Connector等等,源碼中主要過程如下:

      創建Connector是根據配置的協議找到對應的處理器類名,然後通過反射來創建該處理器,如下:

3-1-4、初始化各Tomcat組件

      各組件創建後,Catalina.load()中調用Server組件的初始化函數,Server.initInternal()又調用內部包含的Service組件的初始化…以此類推,按配置文件的組件結構順序初始化,如Http類型的Connector組件初始化會對其ServerSocket進行綁定,源碼過程如下:

3-1-5、啓動各Tomcat組件

      上圖只展示了開始的一部分過程,同上面初始化一樣,是按照各組件在配置文件中的組織結構來啓動,從Catalina.start()開始,而在Service中分別啓動Engine和Connector,還需要注意的是MapperLinstener.start()的處理,如下:

(1)、Engine.start()--部署Web應用程序

(A)、啓動部署,創建Context組件

      其中Engine.startInternal()會爲每個Host創建一個部署線程,Host在部署線程中調用HostConfig.deployApps()部署三種定義的Web應用程序,如下:

      同樣,如果發現需要部署的Web應用程序,會爲每個Web應用程序創建一個單獨的部署線程,然後在該線程中創建相應的Context組件,並在Context.startInternal()中創建相應的WebApp類加載器,如下:

(B)、解析web.xml

      而後開始在ContextConfig.webConfig()中解析web.xml部署描述文件,在ContextConfig.configureContext(WebXml webxml)根據解析web.xml的信息創建封裝各種應用元素,如Filter、Servlet、Session等等,其中Servlet被封裝成Wrapper,加入到相應的Context組件中,如下:

(C)、Servlet.init()初始化

      接下來就是Servlet實例初始化,Servlet規範中定義可以通過"load-on-startup"參數來指定,不小於0的整數時加載後調用Servlet.init(),定義多個Servlet時越大越先初始化;而小於0或沒定義該參數時由容器實現來定義,Tomcat中除JspServlet外,其他的Servlet類型是在第一次使用處理請求時再調用Servlet.init(),/conf/web.xml文件中默認的兩個Servlet都定義了"load-on-startup"參數大於0,在加載後調用Servlet.init(),如下:

      DefaultServlet.init()調用過程如下:

      而處理請求時調用的Servlet.init()過程如下:

(D)、Web應用程序變化監控

      Host部署完成全部的Web應用程序後,退回到main線程,創建一個後臺線程,在Engine容器的生命週期中每隔10秒會監控檢查各Web應用程序內容(war文件的時間戳、context.xml、web.xml)是否已更改,如果改變會自動重新加載,創建過程如下:

(2)、MapperListener.start()—建立URL映射關係

      解析完web.xml得到封裝相關信息的各對象後,通過MapperListener建立配置的URL映射關係,即保存到Service中的一個"org.apache.catalina.mapper.Mapper"實例裏,主要是Host、Contex、 Wrappers(Servlet)與對應的URL的映射關係,當處理用戶請求時,可以很方便的通過請求URL從該Mapper實例中找到對應的Host、Contex、 Wrappers(Servlet)來處理

      建立映射關係的主要過程如下:

(3)、Connect.start()—創建併發線程模型

      前面說到了Connector和相應處理器的創建,而Connect.start()主要是創建併發線程模型來接收處理請求,下面以默認的Http/1.1 NIO類型的Connector爲例說明,它會創建四種類型的線程:Worker線程、Poller線程、Acceptor線程、AsyncTimeout線程,前三種整體創建過程如下:

(A)、創建Work線程

      Work(工作)/exec線程用來處理請求連接,先根據配置參數創建Work線程池,線程池最小和最大線程數量可以通過"minSpareThreads"和"maxThreads"設置,默認爲最小10和最大200,而線程空閒時間默認爲60s後關閉,源碼中創建過程如下:

(B)、創建Poller線程,並啓動

      說到Poller(輪詢)線程就得提下Java NIO,通過Selector來實現Socket I/O多路複用模型:當Acceptor線程中ServerSocket.accept()監聽到一個新的請求連接時,會把表示該請求連接的Socket註冊到Poller線程的Selector中,Selector可以阻塞監聽多個請求連接的Socket,當其中有Socket發生監聽事件(如請求數據到來)時會退出阻塞狀態,然後把發生請求數據事件的Socket交給Work線程處理。

      而Poller(輪詢)線程用來輪詢Selector,這樣的好處就是:在沒有請求數據時,只有極少數的Poller線程阻塞,而避免了直接使用多個Work線程阻塞的消耗

      創建過程如(前三種線程整體創建過程)中的圖,其中Poller線程數量可以通過參數"pollerThreadCount"設置,默認值爲每個處理器1個,但不超過2個,當大量的發送文件操作正在進行時,增加該值也可能是有益的。

      關於更多的I/O模型信息請參考:《5種IO模型、阻塞IO和非阻塞IO、同步IO和異步IO》

(C)、創建Acceptor線程,並啓動

      上面說到Acceptor線程用來調用ServerSocket.accept()(8080端口)阻塞監聽到用戶新的請求連接,創建過程如下:

(D)、創建AsyncTimeout線程,並啓動

      AsyncTimeout線程是用來實現異步請求的超時,創建過程如下:

      關於Http/1.1 NIO類型的Connector創建完成後如下圖(選中線程,而Work線程在接收請求數據時創建):

3-2、Tomcat請求處理過程

      上面介紹了Tomcat的啓動過程,主要初始化了各組件、Servlet,以及創建4種類型線程的併發模型,其中Work線在接收請求數據時再來創建。

      下面還是以HTTP NIO類型的Connector爲例,先介紹各類型線程間具體是如何接收請求的,再先來介紹請求處理過程。

3-2-1、各類型線程間接收請求/併發模型

1、Acceptor線程接收請求,創建連接Socket

      在瀏覽器中訪問Tomcat默認ROOT應用下的tomcat.png ,URL爲:http://localhost:8080/tomcat.png;然後Acceptor線程中ServerSocket.accept()就會監聽到這個新的請求連接,返回一個代表該請求連接的socket,如下:

2、連接Socket註冊到Poller線程Selector進行監聽

      然後爲該Socke創建一個NIO Channel(通道),該通道有許多特性,操作Socket是通過該通道進行的;然後把該通道註冊到其中的一個Poller線程(這裏是Poller1)的Selector多路複用器中,如下:

3、Selector監聽到數據,創建/交給Work(Exec)線程處理

      如上圖,NioEndpoint.Poller.register()註冊函數裏會插入一個"OP_READ"操作(Key),這使得Poller線程中的Selector返回該Key進行處理,然後Poller線程中先創建一個SocketProcessor處理器,然後再通過線程池創建Work(Exec)線程並啓動處理來該Socket,如下:

3-2-2、請求處理過程

      上面說到了請求如何傳遞到Work(Exec)線程的,下面主要介紹在Work線程運行SocketProcessor來處理來該Socket,整體過程如下:

1、提取請求信息到Request對象,找到URI映射關係

      先是創建一個Http11Processor,再用該處理器來處理,如下

      同樣 Http11Processor和CoyoteAdapter主要是都提取請求信息,封裝到Request對象,後面的處理都是通過Request對象來處理,而後通過Response對象返回響應。

      其中需要注意的是,CoyoteAdapter.postParseRequest()方法中會通過前面介紹Tomcat啓動時保存Host、Context、Wapper(Servlet)映射關係的Mapper實例,找到該URL請求對應的Host、Context、Wapper(Servlet),並保存到Request對象的MappingData成員對象中,如下:

2、Tomcat組件對請求的處理

      之後就是通過該Request對象的MappingData找到相應的一系列組件來處理的過程,整個過程都是以責任鏈模式來處理的。

3、Filter過慮器對請求處理

      接下主要關注StandardWrapper中Servlet的處理,先爲該Servlet創建一個FilterChain,並把Web.xml中定義的對該Servlet匹配的Filter(過慮器)加入到該FilterChain,如下:

      這是就是FilterChain.doFilter()調用Filter.doFilter()來處理請求(),在每個Filter.doFilter()又會調用FilterChain.doFilter(),這樣以責任鏈的模式不斷嵌套調用,直到FilterChain.doFilter()發現所有Filter都調用完成,再調用Servlet的處理函數servlet.service()

4、Servlet程序對請求處理,返回靜態資源

      如請求中會有一個WsFilter過濾器,然後調用到默認的DefaultServlet,過程如下:

      默認的DefaultServlet用來處理靜態內容請求(如html/js/各種圖片),由於繼承自JavaEE中的HttpServlet,所以HttpServlet.service()中會根據GET請求類型調用到DefaultServlet中重寫的doGet()方法,該方法只調用實例自己的serveResource()方法,如下:

      所以serveResource()方法是實現靜態資源請求處理的過程,如果請求資源不存在CacheEntry.exists爲false,則返回404(SC_NOT_FOUND),如下:

      如果客戶端存在緩存,checkIfHeaders()會根據客戶端請求頭的If-None-Match,If-Modified-Since等,來判斷請求資源是否被修改,如果未被修改,則返回304頭,客戶端直接從客戶端緩存中讀取資源文件,如下:

      否則,先設置一些Response Http頭的字段,如contentType,而後讀取請求的圖片數據,如下:

      最後返回,釋放相關資源,可以看到請求圖片如下:

 

      到這裏,我們對Tomcat的Servlet技術實現一個基本的認識,後面將進行配置Tomcat+nginx+keepalived的動靜分離、會話保持的高可用集羣……

 

【參考資料】

1、Servlet3.1規範(最終版)

2、Tomcat8.5.9源碼

3、《深入分析Java Web技術內幕》

4、《深入剖析tomcat》

5、Apache Tomcat User Guide:http://tomcat.apache.org/tomcat-8.5-doc/index.html

6、Apache Tomcat 8 Configuration Reference:ttp://tomcat.apache.org/tomcat-8.5-doc/config/index.html

7、web.xml組件加載順序:http://www.importnew.com/22909.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章