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 URL:http://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應用下的資源:
getResource
和getResourceAsStream
;
傳入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的
getWriter
和getOutputStream
在同一次請求中不能同時被呼叫。呼叫了一個之後在呼叫另一個會丟擲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
sendRedirect
和sendError
;
這兩方法有一些相似性:
(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也可以設定編碼,在setContentType
和setCharacterEncoding
之前,呼叫setLocale
設定編碼,使用上面配置中的編碼;但是這並不會設定HTTP響應頭的content-type等頭,因此瀏覽器/客戶端將使用預設的解碼方式來解碼這可能導致亂碼;
PS:setLocale將通過Content-Language
響應頭來傳遞;但是編碼方式如果沒有指定Content-Type,是不能通過HTTP header傳遞的;
因此,應該在getWriter方法被呼叫或響應被提交之前通過setContentType
或setCharacterEncoding
或addHeader
設定編碼方式,否則將使用預設編碼:ISO-8859-1;
setCharacterEncoding:這個方法可以覆蓋setLocale
和setContentType
設定的編碼方式,但不會設定Content-Type頭;
setLocale
,setCaracterEncoding
和setContentType
都可以設定編碼方式,但是要通過setContentType
和addHeader
設定Content-Type
響應頭,並且它們都要在getWriter
呼叫前或響應提交前設定;
5.4 結束響應物件
以下時間表明servlet滿足了請求且響應物件即將關閉“
(1)servlet的service方法終止;
(2)響應的setContentLength
或setContentLong
制定了大於零的內容量,且已經寫入到響應;
(3)sendError
方法或sendRedirect
方法已呼叫;
(4)AsyncContext的compelete
方法已呼叫;
setContentLength
和setContentLengthLong
方法一般有Web容器在響應完成後負責呼叫,後者是Servlet3.1的新方法;
5.5 Response生命週期
和Request相似,在servlet的service方法和Filter的doFilter方法內有效,如果啟動非同步處理,直到complete方法被呼叫有效。
6. Filter
6.1 對Filter的理解
Filter和Servlet/其他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.DefaultServlet
和org.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:
從這個類圖中,我們可以看到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中建立的。
Request和Response:
Tomcat同樣使用內部表示<—>門面類<—>傳入Servlet;
(1)Tomcat接收到請求後建立org.apache.coyote.Request
和org.apache.coyote.Resposne
,這兩個類是輕量級的類,物件很小;這是有Tomcat內部工作執行緒建立的;
(2)將org.apache.coyote.Request
和org.apache.coyote.Resposne
傳遞給使用者執行緒,建立org.apache.catalina.connector.Request
和org.apache.catalina.connector.Resposne
,這兩個物件一直整個Servlet容器直到要傳給Servlet;
(3)建立門面類RequestFacade
和ResponseFacade
給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方法。
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容器主要通過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規範中一些定義,我們也能看到上述初始化和銷燬順序,這也是必須要理解明白的重要知識點。