1. 程式人生 > >Servlet深入學習,規範,理解和實現(上)

Servlet深入學習,規範,理解和實現(上)

學習參考資料
(1)Servet 3.1 final 規範;
(2)《Java Web高階程式設計》;
(3)《深入分析Java Web技術內幕》(第2版)

心得:雖然現在是實際工作中很少直接使用Servlet,但瞭解Servlet規範中對不同元件(Servlet,Filter,Listener等等)以及Servlet容器的實現對於基於Servlet的Java EE應用的理解也是大有益處的。因此基於上面3個資料的學習所得以及我自己閱讀Tomcat 8相關部分原始碼的一些收穫在這裡總結記錄一下。

1. Servlet容器

Servlet 容器是 web server 或 application server 的一部分,提供基於請求/響應傳送模型的網路服務,解碼基於 MIME 的請求,並且格式化基於 MIME 的響應。Servlet 容器也包含了管理 Servlet 生命週期。

2. Servlet

Servlet 是基於 Java 技術的 web 元件,被容器所託管的,用於生成動態內容。像其他基於 Java 的元件技術一樣,Servlet 也是基於平臺無關的 Java 類格式,被編譯為平臺無關的位元組碼,可以被基於 Java 技術的 web server 動態載入並執行。

2.1 Servlet的數量

Servlet預設是執行緒不安全的,一個容器中只有每個servlet一個例項,但是如果實現了SingleThreadModule介面,容器將實現多個servlet例項
SingleThreadModule也不能保證執行緒安全,它只能保證任意兩個執行緒不會使用同一個Servlet例項(可能由一個物件池來維護),servlet2.4已經將這個介面已經標註為已過時

了;

我查看了Tomcat 8.0中StandardWrapper原始碼,這個類負責Servlet的建立,其中SingleThreadModule模式下建立的例項數不能超過20個,也就是同時只能支援20個執行緒訪問這個Serlvet,因此,這種物件池的設計會進一步限制併發能力和可伸縮性。

2.2 servlet的生命週期

載入和例項化:servlet容器負責載入和例項化Servlet,在容器啟動時根據設定決定是在啟動時初始化(loadOnStartup大於等於0在容器啟動時進行初始化,值越小優先順序越高),還是延遲初始化致第一次請求前;

初始化:

init(),執行一些一次性的動作,可以通過ServletConfig配置物件,獲取初始化引數,訪問ServletContext上下文環境;
初始化時可能發生錯誤,UnavailableException和ServletException,那麼servlet不應放置活動服務中,未成功初始化,destroy方法也應被呼叫

請求處理:

servlet容器封裝Request和Response物件傳給對應的servlet的service方法,對於HttpServlet,就是HttpServletRequest和HttpServletResponse;
HttpServlet中使用模板方法模式service方法根據HTTP請求方法進一步分派到doGet,doPost等不同的方法來進行處理;

對於HTTP請求的處理,只有重寫了支援HTTP方法的對應HTTP servlet方法(doGet),才可以支援,否則放回405(Method Not Allowed)。

執行緒不安全
servlet中預設執行緒不安全,單例多執行緒,因此對於共享的資料(靜態變數,堆中的物件例項等)自己維護進行同步控制,不要在service方法或doGet等由service分派出去的方法,直接使用synchronized方法,很顯然要根據業務控制同步控制塊的大小進行細粒度的控制,將不影響執行緒安全的耗時操作移出同步控制塊;

異常
請求處理時同樣可能丟擲異常,UnavailableException和ServletException;
UnavailableException表示不可用,永久不可用狀態返回404;暫時不可用返回503(服務不可用),標註Retry-After頭;

非同步處理:

Servlet中等待是一個低效的操作,因為這是阻塞操作。
非同步處理請求能力,使執行緒可以返回到容器,從而執行更多的任務。當開始非同步處理請求時,另一個執行緒或回撥可以:(1)產生響應;或者,(2)請求分派;或者,(3)呼叫完成;

關鍵方法:

啟用:讓servlet支援非同步支援:asyncSupported=true;

啟動:AsyncContextasyncContext=req.startAsyncContext();或startAsyncContext(req,resp);

完成:asyncContext.complete();必須在startAsync呼叫之後,分派進行之前呼叫;同一個AsyncContext不能同時呼叫dispatch和complete

分派:asyncContext.dispatch();dispatch(Stringpath);dispatch(ServletContextcontext,Stringpath);
不能在complete之後呼叫;
從一個同步servlet分派到非同步servlet是非法的;

超時:asyncContext.setTimeout(millis);
超時之後,將不能通過asyncContext進行操作,但是可以執行其他耗時操作;
在非同步週期開始後,容器啟動的分派已經返回後,呼叫該方法丟擲IllegalStateException;如果設定成0或小於0就表示notimeout;
超時表示HTTP連線已經結束,HTTP已經關閉,請求已經結束了。

啟動新執行緒
通過AsyncCOntext.start(Runnable)方法,向執行緒池提交一個任務,其中可以使用AsyncContext(未超時前);

事件監聽:addListener(newAsyncListener{…});
onComplete:完成時回撥,如果進行了分派,onComplete方法將延遲到分派返回容器後進行呼叫;
onError:可以通過AsyncEvent.getThrowable獲取異常;
onTimeout:超時進行回撥;
onStartAsync:在該AsyncContext中啟動一個新的非同步週期(呼叫startAsyncContext)時,進行回撥;

超時和異常處理,步驟:
(1)呼叫所有註冊的AsyncListener例項的onTimeout/onError;
(2)如果沒有任何AsyncListener呼叫AsyncContext.complete()或AsyncContext.dispatch(),執行一個狀態碼為HttpServletResponse
.SC_INTERNAL_SERVER_ERROR出錯分派;
(3)如果沒有找到錯誤頁面或者錯誤頁面沒有呼叫AsyncContext.complete()/dispatch(),容器要呼叫complete方法;

終止:

servlet容器確定從服務中移除servlet時,可以通過呼叫destroy()方法將釋放servlet佔用的任何資源和儲存的持久化狀態等。呼叫destroy方法之前必須保證當前所有正在執行service方法的執行緒執行完成或者超時;
之後servlet例項可以被垃圾回收,當然什麼時候回收並不確定,因此destroy方法是是否必要的。

2.3 Servlet(Filter)中的url-pattern

Serlvet和Filter有三種不同的匹配規則:
(1)精確匹配:/foo;
(2)路徑匹配:/foo/*;
(3)字尾匹配:*.html;
Serlvet的匹配順序是:
首先進行精確匹配;如果不存在精確匹配的進行路徑匹配;最後根據字尾進行匹配;一次請求只會匹配一個Servlet;(Filter是隻要匹配成功就新增到FilterChain)

PS:其他寫法(/foo/,/*.html,*/foo)都不對;“/foo*”不能匹配/foo,/foox;

3. Request

3.1 HTTP協議引數

通過HttpServletRequest物件獲取Http引數:
getParameter,getParameterNames,getParameterValues,getParameterMap;

這些方法從getRequestURI方法或getPathInfo方法返回的字串值中解析,如果是POST方法,也是在第一次呼叫getParameter方法時候進行解碼獲取到引數集合當中,因此要在呼叫這些方法之前設定編解碼方式,否則可能導致亂碼;

POST表單資料也會被彙總到請求引數集合中,但要滿足:
(1)Content-Type必須是application/x-www-form-urlencoded;
(2)進行getParameter呼叫;
如果不滿足獲取POST引數的條件,servlet可以通過request物件的輸入流得到POST資料;相反如果滿足條件,輸入流中也不再可以讀取POST資料(因為已經讀取過了);

3.2 檔案上傳

資料以multipart/form-data格式傳送,servlet支援檔案上傳;
通過 HttpServletRequest的:
public Collection<Part> getParts()
public Part getPart(String name)

每個Part類代表從multipart/form-data格式的POST請求中接受的一個部分或表單項,每個Part可以通過Part.getInputStream方法訪問頭部,內容型別和內容;

對於表單資料的Content-Disposition,即使沒有檔名,也可使用part的名稱通過HttpServletRequest的getParameter和getParameterValues得到part的字串值;

3.3 屬性:

屬性的作用域與請求相關;
getAttribute/getAttributeNames/setAttribute;

3.4 請求路徑元素

Context Path:ServletContext關聯路徑,getContextPath,“/example”;
Servlet Path:getServletPath,“/servlets/servlet”,請求“/*”與“”模式匹配對應的servlet path是空字串;
PathInfo:請求路徑一部分,不屬於Content Path或Servlet Path,“/空幻”,要麼為null,要麼為以“/”開頭的字串;
Request URI:getRequestURI,等於contetPath + servletPath + pathInfo;
QueryString:getQueryString,“author=空幻”;
Request URLhttp://localhost:8080/example/servlets/servlet/空幻;

路徑轉換方法

ServletContext.getRealPath
HttpServletRequest.getPathTranslated
比如:
(1)“http://localhost:8080/s/request/pathinfo”, 在我的機器上`getPathTranslated()返回“/home/yjh/wks/workspace/ServletTest/target/servletTest/pathinfo”;
其中”/request”是serlvet path,“servletTest”是專案根目錄名;這兩個方法都是基於專案根目錄返回的;

3.5 Servlet 3.1新特性,非阻塞I/O

非阻塞I/O只能用在Serlvet和Filter的非同步請求處理升級處理中; 否則設定時丟擲IllegalStateException;

Request——ServletInputStream——ReadListener;
Response——ServletOutputStream——WriterListener;

ReadListener:
(1)onDataAvailable:當可以從傳入請求流中讀取資料,onDataAvailable將被呼叫,和ServletInputStream.isReady相關;
(2)onAllDataRead:讀取完成ServletRequest的所有資料時呼叫onAllDataRead方法,和ServletInputStream.isFinished()相關;
(3)onError(Throwable);

3.6 請求資料編碼

getParameter等引數獲取方法會將引數部分從流中讀取出來,因此一定要在getParameter呼叫前設定編解碼方式:

Request:
setCharacterEncoding()

Response:
setCharacterEncoding()
setHead()
setContentType()

下面在Response總結中會進一步說明編碼和響應及其緩衝區之間的關係.

3.7 Request 物件的生命週期

每個Request物件在Servlet的service(這就包括JSP的表示式,指令碼,宣告),Filter的doFilter的作用域中有效;

啟用了非同步處理後,request物件將到AsyncContext的complete呼叫時;

4. ServletContext介面

每個基於Servlet的Web應用都有自己的ServletContext儲存和維護自己的上下文資訊,包括:初始化引數,Servlet,Filter,Listener配置,容器屬性等等。

4.1 配置

主要有3種方式:
(1)Web.xml部署描述符;
(2)註解;
(3)通過ServletContextListener/ServletContainerInitializer使用Servlet/Filter的Registration配置;

4.2 上下文屬性

容器也有自己的屬性,這裡提一下是因為這涉及到:
(1)EL表示式的隱式變數作用域:applicationScope包含所有繫結到ServletContext的特性;EL表示式中變數的作用域也是一層層查詢的,最後一層查詢範圍就是ServletContext的特性;
(2)同樣JSP中的隱式變數application也是ServletContext例項;

4.3 資源

獲取Web應用下的資源:
getResourcegetResourceAsStream
傳入path,必須要以“/”開頭,相對與兩個目錄:上下文的根目錄web應用的WEB-INF/lib中的JAR檔案中的META-INF/resources目錄。依次查詢這兩個地方;

這兩個方法不能獲取動態內容,比如jsp,獲取的是jsp檔案原始碼而不是處理後的響應;

4.4 臨時工作目錄

Servlet容器必須為每一個servlet上下文提供一個私有的臨時目錄,並將通過javax.servlet.context.tempdir上下文屬性使其可用,該屬性關聯的是java.io.File。
這個目錄也是Multipart處理中臨時目錄的預設目錄,並且location如果是相對路徑也是基於它的。

5. Response

Response的getWritergetOutputStream在同一次請求中不能同時被呼叫。呼叫了一個之後在呼叫另一個會丟擲IllegalStateException;

5.1 緩衝

獲取和設定緩衝區大小:getBufferSize和setBufferSize,不能在緩衝區寫入內容之後設定緩衝區大小呼叫setBufferSize;

PS:tomcat 8中緩衝區大小為8192

是否提交到客戶端:isCommitted;

重新整理緩衝區:flushBuffer,也可以通過getWriter/getOutputStream呼叫輸出流的flush;

重置緩衝區:reset和resetBuffer,不能在響應提交後呼叫,否則丟擲IllegalStateException,響應及關聯的緩衝區不變;

PS:一般並不需要進行手動重新整理緩衝區,service方法結束或請求處理完成後,容器會自動重新整理緩衝區.但如果使用非同步處理分派的話,Response的生命週期其實已經延伸到了開始非同步的service方法之外了,這樣如果你想要在service方法返回前提交響應則可以手動重新整理緩衝區,否則只能等到非同步完成/超時請求處理結束或者緩衝區滿了才能提交到客戶端了.

5.2 重定向和設定Error

sendRedirectsendError
這兩方法有一些相似性:
(1)如果在呼叫前已有響應提交到客戶端,呼叫它們將丟擲IllegalStateException;
(2)如果沒有響應提交,sendRedirect和sendError將重置緩衝區,捨棄原來緩衝區中的舊資料,Servlet中之後的輸出也是無效的(將被忽略);

5.3 Response編碼和國際化

同樣需要在響應未提交或resp.getWriter()之前進行設定,否則將無效(面向字元的輸出已經設定預設編碼);

國際化配置
在部署描述符中配置,如果沒有配置將使用容器依賴的mapping等配置:

    <locale-encoding-mapping-list>
        <locale-encoding-mapping>
            <locale>zh_CN</locale>
            <encoding>UTF8</encoding>
        </locale-encoding-mapping>
    </locale-encoding-mapping-list>

setLocale也可以設定編碼,在setContentTypesetCharacterEncoding之前,呼叫setLocale設定編碼,使用上面配置中的編碼;但是這並不會設定HTTP響應頭的content-type等頭,因此瀏覽器/客戶端將使用預設的解碼方式來解碼這可能導致亂碼;

PS:setLocale將通過Content-Language響應頭來傳遞;但是編碼方式如果沒有指定Content-Type,是不能通過HTTP header傳遞的;

因此,應該在getWriter方法被呼叫或響應被提交之前通過setContentTypesetCharacterEncodingaddHeader設定編碼方式,否則將使用預設編碼:ISO-8859-1;

setCharacterEncoding:這個方法可以覆蓋setLocalesetContentType設定的編碼方式,但不會設定Content-Type頭;

setLocalesetCaracterEncodingsetContentType都可以設定編碼方式,但是要通過setContentTypeaddHeader設定Content-Type響應頭,並且它們都要在getWriter呼叫前或響應提交前設定;

5.4 結束響應物件

以下時間表明servlet滿足了請求且響應物件即將關閉“
(1)servlet的service方法終止;
(2)響應的setContentLengthsetContentLong制定了大於零的內容量,且已經寫入到響應;
(3)sendError方法或sendRedirect方法已呼叫;
(4)AsyncContext的compelete方法已呼叫;

setContentLengthsetContentLengthLong方法一般有Web容器在響應完成後負責呼叫,後者是Servlet3.1的新方法;

5.5 Response生命週期

和Request相似,在servlet的service方法和Filter的doFilter方法內有效,如果啟動非同步處理,直到complete方法被呼叫有效。

6. Filter

6.1 對Filter的理解

FilterServlet/其他Web資源(包括靜態資源)組合起來使用,實現了一個職責鏈模式的請求處理呼叫棧,Servlet/Web資源是最後一個“入棧的節點”(當然Filter可以阻止請求到達Servlet/Web資源,)。Filter可以在servlet呼叫前和呼叫後進行一些額外的處理過程(比如,驗證,日誌,壓縮等等)。

FilterChain.doFilter(req, resp)呼叫前後,正分別是呼叫棧“入棧”和“出棧”之時,做相應的處理。

每個Filter配置對應的每個JVM的容器僅例項化一個例項。

6.2 Filter的生命週期

(1)init()/init(FilterConfig filterConfig):和Servlet一樣可能丟擲UnavaliableException(暫時不可用/永久不可用);init()方法總是在應用程式啟動時呼叫(ServletContextListener初始化之後,Servlet初始化之前);
(2)doFilter():服務中,處理傳入請求和返回響應:可以進行檢查請求頭,修改請求頭/資料,修改響應頭等等;

Filter可以呼叫chain.doFilter()方法呼叫過濾器鏈中下一個實體;也可以不呼叫來阻止請求;

doFilter過程中也可能丟擲UnavailableException,容器負責停止處理剩下的過濾器鏈,若不是永久不可用,可以選擇稍後重試整個鏈。

(3)destroy():容器把服務中的Filter例項移除前,先呼叫它的destroy方法,進行釋放資源等清理工作;

6.3 Filter的型別

Servlet容器中,存在多種分派方式,Servlet2.4之後,可以對不同的請求分派進行過濾:
(1)普通請求
(2)轉發請求RequestDispatcher.forward()<jsp:forward>觸發的請求,這種轉發,本質上是伺服器應用內部的方法呼叫;
(3)包含請求RequestDispatcher.include()<jsp:include>,注意這種是包含輸出和<%@ inlcude %>靜態匯入的區別;
(4)錯誤資源請求:發生異常,請求錯誤頁面;
(5)非同步請求:如果要結合非同步處理的Servlet使用,Filter同樣也要開啟支援非同步處理。這裡非同步請求指的是有AsyncContext派發的請求,實現非同步過濾器要注意可能被單個非同步請求呼叫多次(潛在的多個不同執行緒);

在部署描述符中,通過<dispatcher>元素中可以選擇Filter支援的請求型別。

6.4 Filter的配置

初始化引數的設定
同樣Filter也可以通過程式設計註解XML三種方式配置;

到Serlvet 3.1,註解配置Filter不能保證設定順序。

Filter的順序
(1)基本順序:
首先<url-pattern>匹配,按照Filter在部署描述符中出現的順序匹配過濾器對映;
其次再按照<serlvet-name>出現的順序匹配;
(2)程式設計設定Filter順序:

    registration.addMappingForUrlPatterns(null, false, "/foo", "bar/*");

第二個引數表示是否在部署描述符之後的Filter之後

6.5 Servlet,Filter和UnavailableException

UnavailableException表示Servlet或Filter不可用,這種情況一般Servlet容器負責處理,重試或者返回響應;
UnavailableException的分為兩種:
(1)永久不可用:比如servlet配置不正確或者Filter狀態異常。
(2)暫時不可用:可能由於一些system-wide的問題導致請求無法處理,比如第三方服務不可用,記憶體和磁碟不足等等;可以在稍後重試;

7. Servlet及其容器的工作原理(Tomcat 8.0為例)

Tomcat分為4層結構:Container容器->Engine容器->Host容器->Servlet容器;

一個請求,根據它的URL,Tomcat將根據它的Host,Context一層層將其轉發到合適的Servlet(對於很多MVC是對映到一個Servlet,在根據之後的pathinfo解析分派到對應的處理函式)。

一個Context對應一個Web工程:

    <Context path="/projectOne" docBase="/home/xxx/xxx" reloadable="true" />

7.1 Servlet容器的啟動

這裡的config也是通過Tomcat.addWebApp的過載版本中呼叫構造器建立的,傳入該方法進行設定;contextPath和docBase分別對應Web應用的訪問路徑物理路徑
(1)新增Web應用,設定訪問路徑,工作目錄監聽器,建立注入ContextConfig物件;

    public Context addWebapp(Host host, String contextPath, String docBase, ContextConfig config) {
        silence(host, contextPath);

        Context ctx = createContext(host, contextPath);
        //設定訪問路徑
        ctx.setPath(contextPath);
        //設定物理工作目錄 
        ctx.setDocBase(docBase);
        //新增監聽器
        ctx.addLifecycleListener(new DefaultWebXmlListener());
        ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));
        //ContextConfig也同樣實現了監聽介面
        ctx.addLifecycleListener(config);

        // prevent it from looking ( if it finds one - it'll have dup error )
        config.setDefaultWebXml(noDefaultWebXmlPath());

        //將Servlet容器(Context)新增到Host下
        if (host == null) {
            getHost().addChild(ctx);
        } else {
            host.addChild(ctx);
        }

        return ctx;
    }

(2)Tomcat啟動Tomcat.start()
Tomcat中啟動中使用了觀察者設計模式,所有容器實現了LifeCycle介面(也就是Observable),所有修改和狀態變化由容器通知已註冊的Observer(Listener)。

(3)Context容器初始化:
當Context容器初始化狀態為init時,ContextConfig實現了LifeCycleListener介面,之前在addWebApp()已經將其註冊到了Context中,這時會被呼叫。ContextConfig負責整個Web應用配置檔案的解析工作,在ContextConfig.init()方法中(包括/conf目錄下的context.xml,預設HOST配置檔案/server.xml,Context自身的配置檔案)。

(4)配置檔案解析完成後呼叫Context.startInternal:這個方法十分重要,包含很多工作,之後要涉及的比如Web應用初始化servlet的建立初始化(loadOnStartup的),Filter的建立和初始化等等都是這個方法子環節:

建立讀取資原始檔的物件;
建立ClassLoader物件(WepAppClassLoader,載入Web應用目錄lib下的jar包中的類,不同Web應用這裡相互隔離);
設定應用的工作目錄‘;
啟動相關的輔助類(logger,realm,resources等);
修改啟動狀態,通知感興趣的觀察者;
子容器的初始化;
獲取ServletContext並設定必要的引數;
建立並初始化Filter
初始化LoadOnStartup的Servlet

(5)Web應用初始化:
在上面說過Context.startInternal方法中會“修改啟動狀態,通知感興趣的觀察者”,檢視該方法原始碼可以發現:

    fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

這個方法通知註冊對於CONFIGURE_START_EVENT感興趣的監聽器,就包括ContextConfig,這時ContextConfig呼叫configureStart方法開始Web應用的初始化工作,主要的工作就是web.xml檔案的解析(包括全域性的web.xml,應用自己的web.xml,jar包中META-INF/web-fragment.xml,註解的讀取,解析,合併)。

這些web.xml部署描述符和註解是依據Serlvet規範的,WebXml物件將它們抽象組裝成StandardWapper,Tomcat容器內部的表示方法,而不是直接強耦合於Serlvet規範。

這個過程將我們熟悉的Serlvet,Filter,Multipart配置抽象包裝成StandardWrapper物件,作為子容器新增到Context中,Context容器是真正執行Servlet的Servlet容器,一個Web應用一個Context容器。

7.2 建立Servlet例項

這個工作是在Context.startInternal()中開始的:

    if (ok) {
        if (!loadOnStartup(findChildren())) {
            log.error(sm.getString("standardContext.servletFail"));
            ok = false;
        }
    }

StandardContext.loadOnStartup對loadOnstartup值大於等於0的StandardWrapper呼叫其load方法,開始建立和初始化Servlet物件。

/conf/web.xml全域性的部署描述符中定義兩個Servlet:
org.apache.catalina.servlets.DefaultServletorg.apache.jasper.servlet.JspServlet(loadOnStartup分別是1和3);根據/conf/web.xml總的定義我們可以知道它們分別是處理靜態資源和jsp檔案請求的Servlet。

7.2.1 建立Servlet物件:Servlet建立中的單例模式(synchronized+反射建立)

之前在介紹Servlet規範時,我們提及了Servlet是單例的,這裡就看看Tomcat是怎樣支援這一規範要求的。

首先,根據前面的知識,已經知道我們在部署描述符中定義的每個Servlet會被解析組裝成對應一個StandardWrapper物件,也正是這個物件負責建立Servlet;建立就在StandardWrapper.loadServlet方法中,下面來看看這個方法的一些關鍵步驟:

(1)基於synchronized同步控制,保證create-if-not的原子性/記憶體可見性:

    public synchronized Servlet loadServlet() throws ServletException

這裡到沒有DCL,靜態內部類,列舉等豐富多彩的單例模式實現方法,其中也沒有什麼比較特別耗時的操作;但是這也說明了Serlvet使用LoadOnStartup可以避免在Web應用執行的時候因為建立Servlet的一些同步開銷。

(2)如果單例已存在,直接返回:

// Nothing to do if we already have an          instance or an instance pool
        if (!singleThreadModel && (instance != null))
            return instance;

singleThreadModel(以下簡稱STM)前面已經說過使用物件池來保證不會有兩個執行緒使用同一個Servlet例項(但這不是一個好辦法)。

(3)獲取InstatnceManager例項:

    InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();

(4)InstanceManager通過反射建立servlet例項:

            try {
                servlet = (Servlet) instanceManager.newInstance(servletClass);
            } catch (ClassCastException e) {
                /* 略 */
            } catch (Throwable e) {
                /* 略 */
            }

(5)開始初始化Servlet,初始化完成後通知執行回撥:

    initServlet(servlet);
    fireContainerEvent("load", this);

7.2.2 初始化Servlet

在上面一小節的StandardWrapper.loadServlet的結尾開始進行Servlet的初始化工作(根據StandardWrapper的原始碼,initServlet方法一般會在loadServlet呼叫後,檢查如果沒有完成初始化就進行呼叫,因為loadServlet可能因為一些異常,比如UnavailableException等原因中途退出而沒有完成初始化)。

initServlet方法中有大量的回撥事件通知:

    InstanceEvent.BEFORE_INIT_EVENT
    /* servlet.init(facade)*/
    InstanceEvent.AFTER_INIT_EVENT

(1)基於synchronized關鍵字的同步控制:

    private synchronized void initServlet(Servlet servlet)
            throws ServletException

(2)如果已經初始化過了或者不是STM模式直接返回:

    if (instanceInitialized && !singleThreadModel) return;

(3)將StandardWrapper包裝成StandardWrapperFacade作為ServletConfig傳給Servlet,呼叫Servlet.init(facade)

            if( Globals.IS_SECURITY_ENABLED) {
                /* 略:SecurityUtil.doAsPrivilege方法呼叫 */
            } else {
                servlet.init(facade);
            }
            instanceInitialized = true;

(4)GenericServlet(HttpServlet,JspServlet基類)的init注入儲存了ServletConfig物件。
如果Servlet是JspServlet,需要編譯這個JSP檔案為類,並初始化這個類。

7.3 Serlvet核心結構和門面設計模式

Servet直接相關的幾個類:ServletConfig,ServletReuqest,ServletResponse。

Tomcat容器中使用內部的表示方法,通過門面設計模式將Facade物件傳遞給Servlet:
Tomcat-Servlet體系結構
從這個類圖中,我們可以看到ServletConfig和ServletContext與Servlet的關係,以及Tomcat對它們的實現:
(1)一個ServletContext對應多個Servlet,Tomcat中的實現型別是ApplicationContext,ApplicationContextFacade是它的門面類;
(2)一個ServletConfig對應一個Servlet,Tomcat中的實現型別是StandardWrapper,StandardWrapperFacade是其門面類;
(3)ServletConfig是Servlet配置集合,ServletContext是容器內所有Servlet的“交易環境”;
(4)Servlet桶構init方法獲取ServletConfig(實際上是StandardWrapperFacade物件);
(5)ApplicationContext和StandardWrapper都是在StandardContext中建立的。

RequestResponse
Tomcat-Request-Response體系結構
Tomcat同樣使用內部表示<—>門面類<—>傳入Servlet;

(1)Tomcat接收到請求後建立org.apache.coyote.Requestorg.apache.coyote.Resposne,這兩個類是輕量級的類,物件很小;這是有Tomcat內部工作執行緒建立的;
(2)將org.apache.coyote.Requestorg.apache.coyote.Resposne傳遞給使用者執行緒,建立org.apache.catalina.connector.Requestorg.apache.catalina.connector.Resposne,這兩個物件一直整個Servlet容器直到要傳給Servlet;
(3)建立門面類RequestFacadeResponseFacade給Servlet;

7.3 請求和對映/分派

Tomcat8通過org.apache.catalina.mapper(和Tomcat7位置有差別)儲存容器中所有子容器的資訊,在org.apache.catalina.connector.Request進入Container容器前,Mapper會根據這次請求的hostname和contextPath將host和context容器設定到Request的mappingData屬性中。

這裡同樣使用觀察者模式,MappingListener註冊到Engine,Host各級容器上,容器狀態發生變化就通知它變化更新到Mapper中。

根據Mapper可以確定將請求分派到哪個Host和哪個Servlet容器上以及哪個Servlet上,在傳到Servlet前,通過Filter鏈並在這個過程中呼叫可能的Listener,最終執行Servlet的service方法。
Tomcat-Mapper結構

7.4 Listener的體系結構和建立

Servlet規範中定義了很多監聽器,基於觀察者模式將主要流程的控制/管理和事件的響應處理分離。主要分為兩類:
(1)LifeCycleListener:ServletContextListener,HttpSessionListener;監聽目標物件的建立和銷燬事件;
(2)EventListener:ServletContextAttributeListener,ServletRequestAttributeListener,ServletRequestListener,HttpSessionAttributeListener等等;

PS:ServletContextListeer在容器啟動之後不能在新增新的,因為容器啟動這個事件不會再次發生;我們可以在ServletContainerInitializer中建立配置它。

Listener的建立

再次回到StandardContext.startInternal方法中:

    if (ok) {
        if (!listenerStart()) {
           log.error(sm.getString("standardContext.listenerFail"));
           ok = false;
        }
    }

這裡的關鍵還是listenerStart方法,該方法在StandardContext.filterStart之前,我們來看一看這個方法的關鍵步驟:
(1)反射建立所有Listener:

// Instantiate the required listeners
String listeners[] = findApplicationListeners();
Object results[] = new Object[listeners.length];
boolean ok = true;
for (int i = 0; i < results.length; i++) {
    /* 略 */
    try {
        String listener = listeners[i];
        results[i] = getInstanceManager().newInstance(listener);
    } catch (Throwable t) {
        /* 略 */
        ok = false;
    }
}

(2)將監聽器整理為eventListeners和lifecycleListeners兩類:

// Sort listeners in two arrays
ArrayList<Object> eventListeners = new ArrayList<>();
ArrayList<Object> lifecycleListeners = new ArrayList<>();
for (int i = 0; i < results.length; i++) {
    if ((results[i] instanceof ServletContextAttributeListener)
        || (results[i] instanceof ServletRequestAttributeListener)
        || (results[i] instanceof ServletRequestListener)
        || (results[i] instanceof HttpSessionIdListener)
        || (results[i] instanceof HttpSessionAttributeListener)) {
        eventListeners.add(results[i]);
    }
    if ((results[i] instanceof ServletContextListener)
        || (results[i] instanceof HttpSessionListener)) {
        lifecycleListeners.add(results[i]);
    }
}

(3)將Intializers或其他通過程式設計方式新增的監聽新增到位:
這裡就不一定是反射建立的了,在ServletContainerInitializer.onStratup中我們可以通過構造器來建立指定的listener;

// Listener instances may have been added directly to this Context by
        // ServletContextInitializers and other code via the pluggability APIs.
        // Put them these listeners after the ones defined in web.xml and/or
        // annotations then overwrite the list of instances with the new, full
        // list.
        for (Object eventListener: getApplicationEventListeners()) {
            eventListeners.add(eventListener);
        }
        setApplicationEventListeners(eventListeners.toArray());
        for (Object lifecycleListener: getApplicationLifecycleListeners()) {
            lifecycleListeners.add(lifecycleListener);
            if (lifecycleListener instanceof ServletContextListener) {
                noPluggabilityListeners.add(lifecycleListener);
            }
        }
        setApplicationLifecycleListeners(lifecycleListeners.toArray());

(4)呼叫ServletContextListener的contextInitialized

7.5 Filter的建立,初始化和使用

Filter的建立

回到StandardContext.startInternal方法中:

        // Configure and call application filters
        if (ok) {
            if (!filterStart()) {
                log.error(sm.getString("standardContext.filterFail"));
                ok = false;
            }
        }

StandardContext.filterStart中將根據配置建立所有的ApplicationFilterConfig以及根據FilterClass反射建立愛Filter例項,實際上還是通過(synchronized+反射建立保證單例),該方法在StandardContext.loadOnStartup之前呼叫。

Filter鏈的結構和呼叫過程

上面我們根據Servlet規範介紹了Filter的基本情況。這裡結合Tomcat 8具體介紹下Filter的建立,初始和相關細節。
Tomcat-Filter結構

Tomcat容器主要通過ApplicationFilterChain管理和執行過濾器鏈。它通過一個數組儲存所有Filter的FilterConfig物件,在Tomcat中是ApplicationFilterConfig(每個FilterConfig包含一個Filter引用)。

    private ApplicationFilterConfig[] filters =
        new ApplicationFilterConfig[0];

該陣列是一個大小動態增長的陣列(每次增長10)。處理請求時通過ApplicationFilterChain.doFilter該方法會呼叫陣列中每個Filter.doFilter

7.6 Initializer,Listener,Filter,Servlet的建立/初始化,銷燬順序

Listener,Filter,Servlet的建立和初始化上面已經結合Tomcat 8的實現進行了總結說明。它們都是在StandardContext.startInternal這一生命週期方法中進行的。加上Initializer順序依次是:
Initaializer—>Listener—>Filter—>Servlet(loadOnstart);

因為前面沒有提及Intializer的相關知識,我們在這裡介紹下:
ServletContainInitalizer是Java EE 6中Servlet 3.0的新增介面;它的onStartup方法是一個web應用中我們的程式碼可以控制到的最早時間點。

它不需要通過web.xml部署描述符來定義,需要在/META-INF/services/javax.servlet.ServletContainerInitializer中列出具體的實現,Servlet容器在啟動時會自動掃描載入它們並呼叫onStartUp方法。但是檔案不能放在WAR檔案的/META-INF/services中,而是需要放在JAR檔案的/META-INF/services中,這樣就很不方便。如果你使用Spring的話,Spring Framework提供了一個橋介面,在Spring中SpringServletContainerInitializer類實現了ServletContainerInitializer介面,Spring的JAR中列出了SpringServletContainerInitializer,如下。在SpringServletContainerInitializer中會掃描所有WebApplicationInitializer的實現,呼叫它們的onStartUp方法,因此我們不必在勞神費心了。

Initializer的呼叫

StandardContext.startInternal中在lislistener,filter,servlet(loadOnStartup)之前對所有的ServletContainerInitializers進行呼叫:

// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
    initializers.entrySet()) {
    try {
        entry.getKey().onStartup(entry.getValue(),
                getServletContext());
    } catch (ServletException e) {
        log.error(sm.getString("standardContext.sciFail"), e);
        ok = false;
        break;
    }
}

因此,我們可以看到,根據規範結合實現,Initializer中可以配置servlets,filters和listeners;在ServletContextListener可以配置其他的listener(因此listenerStart中分了兩步載入),filters和servlets;而Filter鏈在Servlet之前呼叫。因此我們能看到這樣一個順序:
Initaializer—>Listener—>Filter—>Servlet(loadOnstart);

銷燬順序

和C++構造&析構等等這類東西很相似,它們的銷燬順序和載入/初始化順序是相反的。

結合Servlet規範中一些定義,我們也能看到上述初始化和銷燬順序,這也是必須要理解明白的重要知識點。