Servlet 工作原理解析

許令波發表於2017-03-06

  Web 技術成為當今主流的網際網路 Web 應用技術之一,而 Servlet 是 Java Web 技術的核心基礎。因而掌握 Servlet 的工作原理是成為一名合格的 Java Web 技術開發人員的基本要求。本文將帶你認識 Java Web 技術是如何基於 Servlet 工作,你將知道:以 Tomcat 為例瞭解 Servlet 容器是如何工作的?一個 Web 工程在 Servlet 容器中是如何啟動的? Servlet 容器如何解析你在 web.xml 中定義的 Servlet ?使用者的請求是如何被分配給指定的 Servlet 的? Servlet 容器如何管理 Servlet 生命週期?你還將瞭解到最新的 Servlet 的 API 的類層次結構,以及 Servlet 中一些難點問題的分析。

 從 Servlet 容器說起

  要介紹 Servlet 必須要先把 Servlet 容器說清楚,Servlet 與 Servlet 容器的關係有點像槍和子彈的關係,槍是為子彈而生,而子彈又讓槍有了殺傷力。雖然它們是彼此依存的,但是又相互獨立發展,這一切都是為了適應工業化生產的結果。從技術角度來說是為了解耦,通過標準化介面來相互協作。既然介面是連線 Servlet 與 Servlet 容器的關鍵,那我們就從它們的介面說起。

  前面說了 Servlet 容器作為一個獨立發展的標準化產品,目前它的種類很多,但是它們都有自己的市場定位,很難說誰優誰劣,各有特點。例如現在比較流行的 Jetty,在定製化和移動領域有不錯的發展,我們這裡還是以大家最為熟悉 Tomcat 為例來介紹 Servlet 容器如何管理 Servlet。Tomcat 本身也很複雜,我們只從 Servlet 與 Servlet 容器的介面部分開始介紹,關於 Tomcat 的詳細介紹可以參考我的另外一篇文章《 Tomcat 系統架構與模式設計分析》。

  Tomcat 的容器等級中,Context 容器是直接管理 Servlet 在容器中的包裝類 Wrapper,所以 Context 容器如何執行將直接影響 Servlet 的工作方式。

  圖 1 . Tomcat 容器模型

圖 1 . Tomcat 容器模型

  從上圖可以看出 Tomcat 的容器分為四個等級,真正管理 Servlet 的容器是 Context 容器,一個 Context 對應一個 Web 工程,在 Tomcat 的配置檔案中可以很容易發現這一點,如下:

  清單 1 Context 配置引數

 <Context path="/projectOne " docBase="D:\projects\projectOne" 
 reloadable="true" />

下面詳細介紹一下 Tomcat 解析 Context 容器的過程,包括如何構建 Servlet 的過程。

  Servlet 容器的啟動過程

  Tomcat7 也開始支援嵌入式功能,增加了一個啟動類 org.apache.catalina.startup.Tomcat。建立一個例項物件並呼叫 start 方法就可以很容易啟動 Tomcat,我們還可以通過這個物件來增加和修改 Tomcat 的配置引數,如可以動態增加 Context、Servlet 等。下面我們就利用這個 Tomcat 類來管理新增的一個 Context 容器,我們就選擇 Tomcat7 自帶的 examples Web 工程,並看看它是如何加到這個 Context 容器中的。

  清單 2 . 給 Tomcat 增加一個 Web 工程

 Tomcat tomcat = getTomcatInstance(); 
 File appDir = new File(getBuildDirectory(), "webapps/examples"); 
 tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); 
 tomcat.start(); 
 ByteChunk res = getUrl("http://localhost:" + getPort() + 
               "/examples/servlets/servlet/HelloWorldExample"); 
 assertTrue(res.toString().indexOf("<h1>Hello World!</h1>") > 0);

  清單 1 的程式碼是建立一個 Tomcat 例項並新增一個 Web 應用,然後啟動 Tomcat 並呼叫其中的一個 HelloWorldExample Servlet,看有沒有正確返回預期的資料。

Tomcat 的 addWebapp 方法的程式碼如下:

  清單 3 .Tomcat.addWebapp

 public Context addWebapp(Host host, String url, String path) { 
        silence(url); 
        Context ctx = new StandardContext(); 
        ctx.setPath( url ); 
        ctx.setDocBase(path); 
        if (defaultRealm == null) { 
            initSimpleAuth(); 
        } 
        ctx.setRealm(defaultRealm); 
        ctx.addLifecycleListener(new DefaultWebXmlListener()); 
        ContextConfig ctxCfg = new ContextConfig(); 
        ctx.addLifecycleListener(ctxCfg); 
        ctxCfg.setDefaultWebXml("org/apache/catalin/startup/NO_DEFAULT_XML"); 
        if (host == null) { 
            getHost().addChild(ctx); 
        } else { 
            host.addChild(ctx); 
        } 
        return ctx; 
 }

  前面已經介紹了一個 Web 應用對應一個 Context 容器,也就是 Servlet 執行時的 Servlet 容器,新增一個 Web 應用時將會建立一個 StandardContext 容器,並且給這個 Context 容器設定必要的引數,url 和 path 分別代表這個應用在 Tomcat 中的訪問路徑和這個應用實際的物理路徑,這個兩個引數與清單 1 中的兩個引數是一致的。其中最重要的一個配置是 ContextConfig,這個類將會負責整個 Web 應用配置的解析工作,後面將會詳細介紹。最後將這個 Context 容器加到父容器 Host 中。

  接下去將會呼叫 Tomcat 的 start 方法啟動 Tomcat,如果你清楚 Tomcat 的系統架構,你會容易理解 Tomcat 的啟動邏輯,Tomcat 的啟動邏輯是基於觀察者模式設計的,所有的容器都會繼承 Lifecycle 介面,它管理者容器的整個生命週期,所有容器的的修改和狀態的改變都會由它去通知已經註冊的觀察者(Listener),關於這個設計模式可以參考《 Tomcat 的系統架構與設計模式,第二部分:設計模式》。Tomcat 啟動的時序圖可以用圖 2 表示。

  圖 2. Tomcat 主要類的啟動時序圖(檢視大圖

圖 2. Tomcat 主要類的啟動時序圖

  上圖描述了 Tomcat 啟動過程中,主要類之間的時序關係,下面我們將會重點關注新增 examples 應用所對應的 StandardContext 容器的啟動過程。

  當 Context 容器初始化狀態設為 init 時,新增在 Contex 容器的 Listener 將會被呼叫。ContextConfig 繼承了 LifecycleListener 介面,它是在呼叫清單 3 時被加入到 StandardContext 容器中。ContextConfig 類會負責整個 Web 應用的配置檔案的解析工作。

  ContextConfig 的 init 方法將會主要完成以下工作:

  1. 建立用於解析 xml 配置檔案的 contextDigester 物件
  2. 讀取預設 context.xml 配置檔案,如果存在解析它
  3. 讀取預設 Host 配置檔案,如果存在解析它
  4. 讀取預設 Context 自身的配置檔案,如果存在解析它
  5. 設定 Context 的 DocBase

  ContextConfig 的 init 方法完成後,Context 容器的會執行 startInternal 方法,這個方法啟動邏輯比較複雜,主要包括如下幾個部分:

  1. 建立讀取資原始檔的物件
  2. 建立 ClassLoader 物件
  3. 設定應用的工作目錄
  4. 啟動相關的輔助類如:logger、realm、resources 等
  5. 修改啟動狀態,通知感興趣的觀察者(Web 應用的配置)
  6. 子容器的初始化
  7. 獲取 ServletContext 並設定必要的引數
  8. 初始化“load on startup”的 Servlet

  Web 應用的初始化工作

  Web 應用的初始化工作是在 ContextConfig 的 configureStart 方法中實現的,應用的初始化主要是要解析 web.xml 檔案,這個檔案描述了一個 Web 應用的關鍵資訊,也是一個 Web 應用的入口。

  Tomcat 首先會找 globalWebXml 這個檔案的搜尋路徑是在 engine 的工作目錄下尋找以下兩個檔案中的任一個 org/apache/catalin/startup/NO_DEFAULT_XML 或 conf/web.xml。接著會找 hostWebXml 這個檔案可能會在 System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default,接著尋找應用的配置檔案 examples/WEB-INF/web.xml。web.xml 檔案中的各個配置項將會被解析成相應的屬性儲存在 WebXml 物件中。如果當前應用支援 Servlet3.0,解析還將完成額外 9 項工作,這個額外的 9 項工作主要是為 Servlet3.0 新增的特性,包括 jar 包中的 META-INF/web-fragment.xml 的解析以及對 annotations 的支援。

  接下去將會將 WebXml 物件中的屬性設定到 Context 容器中,這裡包括建立 Servlet 物件、filter、listener 等等。這段程式碼在 WebXml 的 configureContext 方法中。下面是解析 Servlet 的程式碼片段:

  清單 4. 建立 Wrapper 例項

 for (ServletDef servlet : servlets.values()) { 
            Wrapper wrapper = context.createWrapper(); 
            String jspFile = servlet.getJspFile(); 
            if (jspFile != null) { 
                wrapper.setJspFile(jspFile); 
            } 
            if (servlet.getLoadOnStartup() != null) { 
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); 
            } 
            if (servlet.getEnabled() != null) { 
                wrapper.setEnabled(servlet.getEnabled().booleanValue()); 
            } 
            wrapper.setName(servlet.getServletName()); 
            Map<String,String> params = servlet.getParameterMap(); 
            for (Entry<String, String> entry : params.entrySet()) { 
                wrapper.addInitParameter(entry.getKey(), entry.getValue()); 
            } 
            wrapper.setRunAs(servlet.getRunAs()); 
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); 
            for (SecurityRoleRef roleRef : roleRefs) { 
                wrapper.addSecurityReference( 
                        roleRef.getName(), roleRef.getLink()); 
            } 
            wrapper.setServletClass(servlet.getServletClass()); 
            MultipartDef multipartdef = servlet.getMultipartDef(); 
            if (multipartdef != null) { 
                if (multipartdef.getMaxFileSize() != null && 
                        multipartdef.getMaxRequestSize()!= null && 
                        multipartdef.getFileSizeThreshold() != null) { 
                    wrapper.setMultipartConfigElement(new 
 MultipartConfigElement( 
                            multipartdef.getLocation(), 
                            Long.parseLong(multipartdef.getMaxFileSize()), 
                            Long.parseLong(multipartdef.getMaxRequestSize()), 
                            Integer.parseInt( 
                                    multipartdef.getFileSizeThreshold()))); 
                } else { 
                    wrapper.setMultipartConfigElement(new 
 MultipartConfigElement( 
                            multipartdef.getLocation())); 
                } 
            } 
            if (servlet.getAsyncSupported() != null) { 
                wrapper.setAsyncSupported( 
                        servlet.getAsyncSupported().booleanValue()); 
            } 
            context.addChild(wrapper); 
 }

  這段程式碼清楚的描述瞭如何將 Servlet 包裝成 Context 容器中的 StandardWrapper,這裡有個疑問,為什麼要將 Servlet 包裝成 StandardWrapper 而不直接是 Servlet 物件。這裡 StandardWrapper 是 Tomcat 容器中的一部分,它具有容器的特徵,而 Servlet 為了一個獨立的 web 開發標準,不應該強耦合在 Tomcat 中。

  除了將 Servlet 包裝成 StandardWrapper 並作為子容器新增到 Context 中,其它的所有 web.xml 屬性都被解析到 Context 中,所以說 Context 容器才是真正執行 Servlet 的 Servlet 容器。一個 Web 應用對應一個 Context 容器,容器的配置屬性由應用的 web.xml 指定,這樣我們就能理解 web.xml 到底起到什麼作用了。


 建立 Servlet 例項

  前面已經完成了 Servlet 的解析工作,並且被包裝成 StandardWrapper 新增在 Context 容器中,但是它仍然不能為我們工作,它還沒有被例項化。下面我們將介紹 Servlet 物件是如何建立的,以及如何被初始化的。

  建立 Servlet 物件

  如果 Servlet 的 load-on-startup 配置項大於 0,那麼在 Context 容器啟動的時候就會被例項化,前面提到在解析配置檔案時會讀取預設的 globalWebXml,在 conf 下的 web.xml 檔案中定義了一些預設的配置項,其定義了兩個 Servlet,分別是:org.apache.catalina.servlets.DefaultServlet 和 org.apache.jasper.servlet.JspServlet 它們的 load-on-startup 分別是 1 和 3,也就是當 Tomcat 啟動時這兩個 Servlet 就會被啟動。

  建立 Servlet 例項的方法是從 Wrapper. loadServlet 開始的。loadServlet 方法要完成的就是獲取 servletClass 然後把它交給 InstanceManager 去建立一個基於 servletClass.class 的物件。如果這個 Servlet 配置了 jsp-file,那麼這個 servletClass 就是 conf/web.xml 中定義的 org.apache.jasper.servlet.JspServlet 了。

  建立 Servlet 物件的相關類結構圖如下:

  圖 3. 建立 Servlet 物件的相關類結構

圖 3. 建立 Servlet 物件的相關類結構

  初始化 Servlet

  初始化 Servlet 在 StandardWrapper 的 initServlet 方法中,這個方法很簡單就是呼叫 Servlet 的 init 的方法,同時把包裝了 StandardWrapper 物件的 StandardWrapperFacade 作為 ServletConfig 傳給 Servlet。Tomcat 容器為何要傳 StandardWrapperFacade 給 Servlet 物件將在後面做詳細解析。

  如果該 Servlet 關聯的是一個 jsp 檔案,那麼前面初始化的就是 JspServlet,接下去會模擬一次簡單請求,請求呼叫這個 jsp 檔案,以便編譯這個 jsp 檔案為 class,並初始化這個 class。

  這樣 Servlet 物件就初始化完成了,事實上 Servlet 從被 web.xml 中解析到完成初始化,這個過程非常複雜,中間有很多過程,包括各種容器狀態的轉化引起的監聽事件的觸發、各種訪問許可權的控制和一些不可預料的錯誤發生的判斷行為等等。我們這裡只抓了一些關鍵環節進行闡述,試圖讓大家有個總體脈絡。

  下面是這個過程的一個完整的時序圖,其中也省略了一些細節。

  圖 4. 初始化 Servlet 的時序圖(檢視大圖)

圖 4. 初始化 Servlet 的時序圖


 Servlet 體系結構

  我們知道 Java Web 應用是基於 Servlet 規範運轉的,那麼 Servlet 本身又是如何運轉的呢?為何要設計這樣的體系結構。

  圖 5.Servlet 頂層類關聯圖

圖 5.Servlet 頂層類關聯圖

  從上圖可以看出 Servlet 規範就是基於這幾個類運轉的,與 Servlet 主動關聯的是三個類,分別是 ServletConfig、ServletRequest 和 ServletResponse。這三個類都是通過容器傳遞給 Servlet 的,其中 ServletConfig 是在 Servlet 初始化時就傳給 Servlet 了,而後兩個是在請求達到時呼叫 Servlet 時傳遞過來的。我們很清楚 ServletRequest 和 ServletResponse 在 Servlet 執行的意義,但是 ServletConfig 和 ServletContext 對 Servlet 有何價值?仔細檢視 ServletConfig 介面中宣告的方法發現,這些方法都是為了獲取這個 Servlet 的一些配置屬性,而這些配置屬性可能在 Servlet 執行時被用到。而 ServletContext 又是幹什麼的呢? Servlet 的執行模式是一個典型的“握手型的互動式”執行模式。所謂“握手型的互動式”就是兩個模組為了交換資料通常都會準備一個交易場景,這個場景一直跟隨個這個交易過程直到這個交易完成為止。這個交易場景的初始化是根據這次交易物件指定的引數來定製的,這些指定引數通常就會是一個配置類。所以對號入座,交易場景就由 ServletContext 來描述,而定製的引數集合就由 ServletConfig 來描述。而 ServletRequest 和 ServletResponse 就是要互動的具體物件了,它們通常都是作為運輸工具來傳遞互動結果。

  ServletConfig 是在 Servlet init 時由容器傳過來的,那麼 ServletConfig 到底是個什麼物件呢?

  下圖是 ServletConfig 和 ServletContext 在 Tomcat 容器中的類關係圖。

  圖 6. ServletConfig 在容器中的類關聯圖

圖 6. ServletConfig 在容器中的類關聯圖

  上圖可以看出 StandardWrapper 和 StandardWrapperFacade 都實現了 ServletConfig 介面,而 StandardWrapperFacade 是 StandardWrapper 門面類。所以傳給 Servlet 的是 StandardWrapperFacade 物件,這個類能夠保證從 StandardWrapper 中拿到 ServletConfig 所規定的資料,而又不把 ServletConfig 不關心的資料暴露給 Servlet。

  同樣 ServletContext 也與 ServletConfig 有類似的結構,Servlet 中能拿到的 ServletContext 的實際物件也是 ApplicationContextFacade 物件。ApplicationContextFacade 同樣保證 ServletContex 只能從容器中拿到它該拿的資料,它們都起到對資料的封裝作用,它們使用的都是門面設計模式。

  通過 ServletContext 可以拿到 Context 容器中一些必要資訊,比如應用的工作路徑,容器支援的 Servlet 最小版本等。

  Servlet 中定義的兩個 ServletRequest 和 ServletResponse 它們實際的物件又是什麼呢?,我們在建立自己的 Servlet 類時通常使用的都是 HttpServletRequest 和 HttpServletResponse,它們繼承了 ServletRequest 和 ServletResponse。為何 Context 容器傳過來的 ServletRequest、ServletResponse 可以被轉化為 HttpServletRequest 和 HttpServletResponse 呢?

  圖 7.Request 相關類結構圖

圖 7.Request 相關類結構圖

  上圖是 Tomcat 建立的 Request 和 Response 的類結構圖。Tomcat 一接受到請求首先將會建立 org.apache.coyote.Request 和 org.apache.coyote.Response,這兩個類是 Tomcat 內部使用的描述一次請求和相應的資訊類它們是一個輕量級的類,它們作用就是在伺服器接收到請求後,經過簡單解析將這個請求快速的分配給後續執行緒去處理,所以它們的物件很小,很容易被 JVM 回收。接下去當交給一個使用者執行緒去處理這個請求時又建立 org.apache.catalina.connector. Request 和 org.apache.catalina.connector. Response 物件。這兩個物件一直穿越整個 Servlet 容器直到要傳給 Servlet,傳給 Servlet 的是 Request 和 Response 的門面類 RequestFacade 和 RequestFacade,這裡使用門面模式與前面一樣都是基於同樣的目的——封裝容器中的資料。一次請求對應的 Request 和 Response 的類轉化如下圖所示:

  圖 8.Request 和 Response 的轉變過程

圖 8.Request 和 Response 的轉變過程


 Servlet 如何工作

  我們已經清楚了 Servlet 是如何被載入的、Servlet 是如何被初始化的,以及 Servlet 的體系結構,現在的問題就是它是如何被呼叫的。

  當使用者從瀏覽器向伺服器發起一個請求,通常會包含如下資訊:http://hostname: port /contextpath/servletpath,hostname 和 port 是用來與伺服器建立 TCP 連線,而後面的 URL 才是用來選擇伺服器中那個子容器服務使用者的請求。那伺服器是如何根據這個 URL 來達到正確的 Servlet 容器中的呢?

  Tomcat7.0 中這件事很容易解決,因為這種對映工作有專門一個類來完成的,這個就是 org.apache.tomcat.util.http.mapper,這個類儲存了 Tomcat 的 Container 容器中的所有子容器的資訊,當 org.apache.catalina.connector. Request 類在進入 Container 容器之前,mapper 將會根據這次請求的 hostnane 和 contextpath 將 host 和 context 容器設定到 Request 的 mappingData 屬性中。所以當 Request 進入 Container 容器之前,它要訪問那個子容器這時就已經確定了。

  圖 9.Request 的 Mapper 類關係圖

圖 9.Request 的 Mapper 類關係圖

  可能你有疑問,mapper 中怎麼會有容器的完整關係,這要回到圖 2 中 19 步 MapperListener 類的初始化過程,下面是 MapperListener 的 init 方法程式碼 :

  清單 5. MapperListener.init

 public void init() { 
        findDefaultHost(); 
        Engine engine = (Engine) connector.getService().getContainer(); 
        engine.addContainerListener(this); 
        Container[] conHosts = engine.findChildren(); 
        for (Container conHost : conHosts) { 
            Host host = (Host) conHost; 
            if (!LifecycleState.NEW.equals(host.getState())) { 
                host.addLifecycleListener(this); 
                registerHost(host); 
            } 
        } 
 }

  這段程式碼的作用就是將 MapperListener 類作為一個監聽者加到整個 Container 容器中的每個子容器中,這樣只要任何一個容器發生變化,MapperListener 都將會被通知,相應的儲存容器關係的 MapperListener 的 mapper 屬性也會修改。for 迴圈中就是將 host 及下面的子容器註冊到 mapper 中。

  圖 10.Request 在容器中的路由圖

圖 10.Request 在容器中的路由圖

  上圖描述了一次 Request 請求是如何達到最終的 Wrapper 容器的,我們現正知道了請求是如何達到正確的 Wrapper 容器,但是請求到達最終的 Servlet 還要完成一些步驟,必須要執行 Filter 鏈,以及要通知你在 web.xml 中定義的 listener。

  接下去就要執行 Servlet 的 service 方法了,通常情況下,我們自己定義的 servlet 並不是直接去實現 javax.servlet.servlet 介面,而是去繼承更簡單的 HttpServlet 類或者 GenericServlet 類,我們可以有選擇的覆蓋相應方法去實現我們要完成的工作。

  Servlet 的確已經能夠幫我們完成所有的工作了,但是現在的 web 應用很少有直接將互動全部頁面都用 servlet 來實現,而是採用更加高效的 MVC 框架來實現。這些 MVC 框架基本的原理都是將所有的請求都對映到一個 Servlet,然後去實現 service 方法,這個方法也就是 MVC 框架的入口。

  當 Servlet 從 Servlet 容器中移除時,也就表明該 Servlet 的生命週期結束了,這時 Servlet 的 destroy 方法將被呼叫,做一些掃尾工作。


 Session 與 Cookie

  前面我們已經說明了 Servlet 如何被呼叫,我們基於 Servlet 來構建應用程式,那麼我們能從 Servlet 獲得哪些資料資訊呢?

  Servlet 能夠給我們提供兩部分資料,一個是在 Servlet 初始化時呼叫 init 方法時設定的 ServletConfig,這個類基本上含有了 Servlet 本身和 Servlet 所執行的 Servlet 容器中的基本資訊。根據前面的介紹 ServletConfig 的實際物件是 StandardWrapperFacade,到底能獲得哪些容器資訊可以看看這類提供了哪些介面。還有一部分資料是由 ServletRequest 類提供,它的實際物件是 RequestFacade,從提供的方法中發現主要是描述這次請求的 HTTP 協議的資訊。所以要掌握 Servlet 的工作方式必須要很清楚 HTTP 協議,如果你還不清楚趕緊去找一些參考資料。關於這一塊還有一個讓很多人迷惑的 Session 與 Cookie。

  Session 與 Cookie 不管是對 Java Web 的熟練使用者還是初學者來說都是一個令人頭疼的東西。Session 與 Cookie 的作用都是為了保持訪問使用者與後端伺服器的互動狀態。它們有各自的優點也有各自的缺陷。然而具有諷刺意味的是它們優點和它們的使用場景又是矛盾的,例如使用 Cookie 來傳遞資訊時,隨著 Cookie 個數的增多和訪問量的增加,它佔用的網路頻寬也很大,試想假如 Cookie 佔用 200 個位元組,如果一天的 PV 有幾億的時候,它要佔用多少頻寬。所以大訪問量的時候希望用 Session,但是 Session 的致命弱點是不容易在多臺伺服器之間共享,所以這也限制了 Session 的使用。

  不管 Session 和 Cookie 有什麼不足,我們還是要用它們。下面詳細講一下,Session 如何基於 Cookie 來工作。實際上有三種方式能可以讓 Session 正常工作:

  1. 基於 URL Path Parameter,預設就支援
  2. 基於 Cookie,如果你沒有修改 Context 容器個 cookies 標識的話,預設也是支援的
  3. 基於 SSL,預設不支援,只有 connector.getAttribute("SSLEnabled") 為 TRUE 時才支援

  第一種情況下,當瀏覽器不支援 Cookie 功能時,瀏覽器會將使用者的 SessionCookieName 重寫到使用者請求的 URL 引數中,它的傳遞格式如 /path/Servlet;name=value;name2=value2? Name3=value3,其中“Servlet;”後面的 K-V 對就是要傳遞的 Path Parameters,伺服器會從這個 Path Parameters 中拿到使用者配置的 SessionCookieName。關於這個 SessionCookieName,如果你在 web.xml 中配置 session-config 配置項的話,其 cookie-config 下的 name 屬性就是這個 SessionCookieName 值,如果你沒有配置 session-config 配置項,預設的 SessionCookieName 就是大家熟悉的“JSESSIONID”。接著 Request 根據這個 SessionCookieName 到 Parameters 拿到 Session ID 並設定到 request.setRequestedSessionId 中。

  請注意如果客戶端也支援 Cookie 的話,Tomcat 仍然會解析 Cookie 中的 Session ID,並會覆蓋 URL 中的 Session ID。

  如果是第三種情況的話將會根據 javax.servlet.request.ssl_session 屬性值設定 Session ID。

  有了 Session ID 伺服器端就可以建立 HttpSession 物件了,第一次觸發是通過 request. getSession() 方法,如果當前的 Session ID 還沒有對應的 HttpSession 物件那麼就建立一個新的,並將這個物件加到 org.apache.catalina. Manager 的 sessions 容器中儲存,Manager 類將管理所有 Session 的生命週期,Session 過期將被回收,伺服器關閉,Session 將被序列化到磁碟等。只要這個 HttpSession 物件存在,使用者就可以根據 Session ID 來獲取到這個物件,也就達到了狀態的保持。

  圖 11.Session 相關類圖

圖 11.Session 相關類圖

  上從圖中可以看出從 request.getSession 中獲取的 HttpSession 物件實際上是 StandardSession 物件的門面物件,這與前面的 Request 和 Servlet 是一樣的原理。下圖是 Session 工作的時序圖:

  圖 12.Session 工作的時序圖(檢視大圖)

圖 12.Session 工作的時序圖

  還有一點與 Session 關聯的 Cookie 與其它 Cookie 沒有什麼不同,這個配置的配置可以通過 web.xml 中的 session-config 配置項來指定。


 Servlet 中的 Listener

  整個 Tomcat 伺服器中 Listener 使用的非常廣泛,它是基於觀察者模式設計的,Listener 的設計對開發 Servlet 應用程式提供了一種快捷的手段,能夠方便的從另一個縱向維度控制程式和資料。目前 Servlet 中提供了 5 種兩類事件的觀察者介面,它們分別是:4 個 EventListeners 型別的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 個 LifecycleListeners 型別的,ServletContextListener、HttpSessionListener。如下圖所示:

  圖 13.Servlet 中的 Listener(檢視大圖)

圖 13.Servlet 中的 Listener

  它們基本上涵蓋了整個 Servlet 生命週期中,你感興趣的每種事件。這些 Listener 的實現類可以配置在 web.xml 中的 <listener> 標籤中。當然也可以在應用程式中動態新增 Listener,需要注意的是 ServletContextListener 在容器啟動之後就不能再新增新的,因為它所監聽的事件已經不會再出現。掌握這些 Listener 的使用,能夠讓我們的程式設計的更加靈活。


 總結

  本文涉及到內容有點多,要把每個細節都說清楚,似乎不可能,本文試著從 Servlet 容器的啟動到 Servlet 的初始化,以及 Servlet 的體系結構等這些環節中找出一些重點來講述,目的是能讀者有一個總體的完整的結構圖,同時也詳細分析了其中的一些難點問題,希望對大家有所幫助。

相關文章