Servlet 工作原理解析

從 Servlet 容器說(shuō)起

  • 要介紹 Servlet 必須要先把 Servlet 容器說(shuō)清楚壹店,Servlet 與 Servlet 容器的關(guān)系有點(diǎn)像槍和子彈的關(guān)系硅卢,槍是為子彈而生将塑,而子彈又讓槍有了殺傷力抬旺。雖然它們是彼此依存的开财,但是又相互獨(dú)立發(fā)展碾褂,這一切都是為了適應(yīng)工業(yè)化生產(chǎn)的結(jié)果正塌。從技術(shù)角度來(lái)說(shuō)是為了解耦乓诽,通過(guò)標(biāo)準(zhǔn)化接口來(lái)相互協(xié)作鸠天。既然接口是連接 Servlet 與 Servlet 容器的關(guān)鍵稠集,那我們就從它們的接口說(shuō)起剥纷。
  • 前面說(shuō)了 Servlet 容器作為一個(gè)獨(dú)立發(fā)展的標(biāo)準(zhǔn)化產(chǎn)品晦鞋,目前它的種類很多鳖宾,但是它們都有自己的市場(chǎng)定位渔肩,很難說(shuō)誰(shuí)優(yōu)誰(shuí)劣周偎,各有特點(diǎn)蓉坎。例如現(xiàn)在比較流行的 Jetty蛉艾,在定制化和移動(dòng)領(lǐng)域有不錯(cuò)的發(fā)展勿侯,我們這里還是以大家最為熟悉 Tomcat 為例來(lái)介紹 Servlet 容器如何管理 Servlet助琐。Tomcat 本身也很復(fù)雜蛆橡,我們只從 Servlet 與 Servlet 容器的接口部分開(kāi)始介紹泰演,關(guān)于 Tomcat 的詳細(xì)介紹可以參考我的另外一篇文章《 Tomcat 系統(tǒng)架構(gòu)與模式設(shè)計(jì)分析》粥血。
  • Tomcat 的容器等級(jí)中,Context 容器是直接管理 Servlet 在容器中的包裝類 Wrapper缭嫡,所以 Context 容器如何運(yùn)行將直接影響 Servlet 的工作方式。
    圖 1 . Tomcat 容器模型


    圖 1 . Tomcat 容器模型

從上圖可以看出 Tomcat 的容器分為四個(gè)等級(jí)评架,真正管理 Servlet 的容器是 Context 容器纵诞,一個(gè) Context 對(duì)應(yīng)一個(gè) Web 工程,在 Tomcat 的配置文件中可以很容易發(fā)現(xiàn)這一點(diǎn)嗡呼,如下:
清單 1 Context 配置參數(shù)

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

下面詳細(xì)介紹一下 Tomcat 解析 Context 容器的過(guò)程南窗,包括如何構(gòu)建 Servlet 的過(guò)程万伤。

Servlet 容器的啟動(dòng)過(guò)程

Tomcat7 也開(kāi)始支持嵌入式功能,增加了一個(gè)啟動(dòng)類 org.apache.catalina.startup.Tomcat放妈。創(chuàng)建一個(gè)實(shí)例對(duì)象并調(diào)用 start 方法就可以很容易啟動(dòng) Tomcat芜抒,我們還可以通過(guò)這個(gè)對(duì)象來(lái)增加和修改 Tomcat 的配置參數(shù)宅倒,如可以動(dòng)態(tài)增加 Context蹭劈、Servlet 等铺韧。下面我們就利用這個(gè) Tomcat 類來(lái)管理新增的一個(gè) Context 容器哈打,我們就選擇 Tomcat7 自帶的 examples Web 工程料仗,并看看它是如何加到這個(gè) Context 容器中的立轧。
清單 2 . 給 Tomcat 增加一個(gè) 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 的代碼是創(chuàng)建一個(gè) Tomcat 實(shí)例并新增一個(gè) Web 應(yīng)用肺孵,然后啟動(dòng) Tomcat 并調(diào)用其中的一個(gè) HelloWorldExample Servlet颜阐,看有沒(méi)有正確返回預(yù)期的數(shù)據(jù)凳怨。
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; 
}

前面已經(jīng)介紹了一個(gè) Web 應(yīng)用對(duì)應(yīng)一個(gè) Context 容器肤舞,也就是 Servlet 運(yùn)行時(shí)的 Servlet 容器李剖,添加一個(gè) Web 應(yīng)用時(shí)將會(huì)創(chuàng)建一個(gè) StandardContext 容器偶芍,并且給這個(gè) Context 容器設(shè)置必要的參數(shù)匪蟀,url 和 path 分別代表這個(gè)應(yīng)用在 Tomcat 中的訪問(wèn)路徑和這個(gè)應(yīng)用實(shí)際的物理路徑材彪,這個(gè)兩個(gè)參數(shù)與清單 1 中的兩個(gè)參數(shù)是一致的段化。其中最重要的一個(gè)配置是 ContextConfig普气,這個(gè)類將會(huì)負(fù)責(zé)整個(gè) Web 應(yīng)用配置的解析工作,后面將會(huì)詳細(xì)介紹履肃。最后將這個(gè) Context 容器加到父容器 Host 中。
接下去將會(huì)調(diào)用 Tomcat 的 start 方法啟動(dòng) Tomcat膘螟,如果你清楚 Tomcat 的系統(tǒng)架構(gòu)荆残,你會(huì)容易理解 Tomcat 的啟動(dòng)邏輯,Tomcat 的啟動(dòng)邏輯是基于觀察者模式設(shè)計(jì)的俘闯,所有的容器都會(huì)繼承 Lifecycle 接口真朗,它管理者容器的整個(gè)生命周期蝗碎,所有容器的的修改和狀態(tài)的改變都會(huì)由它去通知已經(jīng)注冊(cè)的觀察者(Listener)衍菱,關(guān)于這個(gè)設(shè)計(jì)模式可以參考《 Tomcat 的系統(tǒng)架構(gòu)與設(shè)計(jì)模式,第二部分:設(shè)計(jì)模式》琼锋。Tomcat 啟動(dòng)的時(shí)序圖可以用圖 2 表示缕坎。
圖 2. Tomcat 主要類的啟動(dòng)時(shí)序圖(查看大圖

圖 2. Tomcat 主要類的啟動(dòng)時(shí)序圖

上圖描述了 Tomcat 啟動(dòng)過(guò)程中,主要類之間的時(shí)序關(guān)系,下面我們將會(huì)重點(diǎn)關(guān)注添加 examples 應(yīng)用所對(duì)應(yīng)的 StandardContext 容器的啟動(dòng)過(guò)程女仰。
當(dāng) Context 容器初始化狀態(tài)設(shè)為 init 時(shí)疾忍,添加在 Contex 容器的 Listener 將會(huì)被調(diào)用。ContextConfig 繼承了 LifecycleListener 接口擒抛,它是在調(diào)用清單 3 時(shí)被加入到 StandardContext 容器中。ContextConfig 類會(huì)負(fù)責(zé)整個(gè) Web 應(yīng)用的配置文件的解析工作诊胞。
ContextConfig 的 init 方法將會(huì)主要完成以下工作:

  1. 創(chuàng)建用于解析 xml 配置文件的 contextDigester 對(duì)象
  2. 讀取默認(rèn) context.xml 配置文件迈着,如果存在解析它
  3. 讀取默認(rèn) Host 配置文件裕菠,如果存在解析它
  4. 讀取默認(rèn) Context 自身的配置文件奴潘,如果存在解析它
  5. 設(shè)置 Context 的 DocBase

ContextConfig 的 init 方法完成后,Context 容器的會(huì)執(zhí)行 startInternal 方法奈虾,這個(gè)方法啟動(dòng)邏輯比較復(fù)雜,主要包括如下幾個(gè)部分:

  1. 創(chuàng)建讀取資源文件的對(duì)象
  2. 創(chuàng)建 ClassLoader 對(duì)象
  3. 設(shè)置應(yīng)用的工作目錄
  4. 啟動(dòng)相關(guān)的輔助類如:logger浪册、realm、resources 等
  5. 修改啟動(dòng)狀態(tài)笆环,通知感興趣的觀察者(Web 應(yīng)用的配置)
  6. 子容器的初始化
  7. 獲取 ServletContext 并設(shè)置必要的參數(shù)
  8. 初始化“l(fā)oad on startup”的 Servlet

Web 應(yīng)用的初始化工作

Web 應(yīng)用的初始化工作是在 ContextConfig 的 configureStart 方法中實(shí)現(xiàn)的攒至,應(yīng)用的初始化主要是要解析 web.xml 文件,這個(gè)文件描述了一個(gè) Web 應(yīng)用的關(guān)鍵信息躁劣,也是一個(gè) Web 應(yīng)用的入口迫吐。
Tomcat 首先會(huì)找 globalWebXml 這個(gè)文件的搜索路徑是在 engine 的工作目錄下尋找以下兩個(gè)文件中的任一個(gè) org/apache/catalin/startup/NO_DEFAULT_XML 或 conf/web.xml。接著會(huì)找 hostWebXml 這個(gè)文件可能會(huì)在 System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default账忘,接著尋找應(yīng)用的配置文件 examples/WEB-INF/web.xml。web.xml 文件中的各個(gè)配置項(xiàng)將會(huì)被解析成相應(yīng)的屬性保存在 WebXml 對(duì)象中鳖擒。如果當(dāng)前應(yīng)用支持 Servlet3.0溉浙,解析還將完成額外 9 項(xiàng)工作,這個(gè)額外的 9 項(xiàng)工作主要是為 Servlet3.0 新增的特性蒋荚,包括 jar 包中的 META-INF/web-fragment.xml 的解析以及對(duì) annotations 的支持戳稽。
接下去將會(huì)將 WebXml 對(duì)象中的屬性設(shè)置到 Context 容器中,這里包括創(chuàng)建 Servlet 對(duì)象期升、filter惊奇、listener 等等互躬。這段代碼在 WebXml 的 configureContext 方法中。下面是解析 Servlet 的代碼片段:
清單 4. 創(chuàng)建 Wrapper 實(shí)例

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颂郎,這里有個(gè)疑問(wèn)吼渡,為什么要將 Servlet 包裝成 StandardWrapper 而不直接是 Servlet 對(duì)象。這里 StandardWrapper 是 Tomcat 容器中的一部分乓序,它具有容器的特征寺酪,而 Servlet 為了一個(gè)獨(dú)立的 web 開(kāi)發(fā)標(biāo)準(zhǔn),不應(yīng)該強(qiáng)耦合在 Tomcat 中竭缝。
除了將 Servlet 包裝成 StandardWrapper 并作為子容器添加到 Context 中房维,其它的所有 web.xml 屬性都被解析到 Context 中,所以說(shuō) Context 容器才是真正運(yùn)行 Servlet 的 Servlet 容器抬纸。一個(gè) Web 應(yīng)用對(duì)應(yīng)一個(gè) Context 容器咙俩,容器的配置屬性由應(yīng)用的 web.xml 指定,這樣我們就能理解 web.xml 到底起到什么作用了湿故。

創(chuàng)建 Servlet 實(shí)例

前面已經(jīng)完成了 Servlet 的解析工作匹耕,并且被包裝成 StandardWrapper 添加在 Context 容器中,但是它仍然不能為我們工作霉咨,它還沒(méi)有被實(shí)例化污秆。下面我們將介紹 Servlet 對(duì)象是如何創(chuàng)建的,以及如何被初始化的墅茉。

創(chuàng)建 Servlet 對(duì)象

如果 Servlet 的 load-on-startup 配置項(xiàng)大于 0命黔,那么在 Context 容器啟動(dòng)的時(shí)候就會(huì)被實(shí)例化,前面提到在解析配置文件時(shí)會(huì)讀取默認(rèn)的 globalWebXml就斤,在 conf 下的 web.xml 文件中定義了一些默認(rèn)的配置項(xiàng)悍募,其定義了兩個(gè) Servlet,分別是:org.apache.catalina.servlets.DefaultServlet 和 org.apache.jasper.servlet.JspServlet 它們的 load-on-startup 分別是 1 和 3洋机,也就是當(dāng) Tomcat 啟動(dòng)時(shí)這兩個(gè) Servlet 就會(huì)被啟動(dòng)坠宴。
創(chuàng)建 Servlet 實(shí)例的方法是從 Wrapper. loadServlet 開(kāi)始的。loadServlet 方法要完成的就是獲取 servletClass 然后把它交給 InstanceManager 去創(chuàng)建一個(gè)基于 servletClass.class 的對(duì)象绷旗。如果這個(gè) Servlet 配置了 jsp-file喜鼓,那么這個(gè) servletClass 就是 conf/web.xml 中定義的 org.apache.jasper.servlet.JspServlet 了。
創(chuàng)建 Servlet 對(duì)象的相關(guān)類結(jié)構(gòu)圖如下:
圖 3. 創(chuàng)建 Servlet 對(duì)象的相關(guān)類結(jié)構(gòu)


圖 3. 創(chuàng)建 Servlet 對(duì)象的相關(guān)類結(jié)構(gòu)

初始化 Servlet

初始化 Servlet 在 StandardWrapper 的 initServlet 方法中衔肢,這個(gè)方法很簡(jiǎn)單就是調(diào)用 Servlet 的 init 的方法庄岖,同時(shí)把包裝了 StandardWrapper 對(duì)象的 StandardWrapperFacade 作為 ServletConfig 傳給 Servlet。Tomcat 容器為何要傳 StandardWrapperFacade 給 Servlet 對(duì)象將在后面做詳細(xì)解析膀懈。
如果該 Servlet 關(guān)聯(lián)的是一個(gè) jsp 文件顿锰,那么前面初始化的就是 JspServlet,接下去會(huì)模擬一次簡(jiǎn)單請(qǐng)求,請(qǐng)求調(diào)用這個(gè) jsp 文件硼控,以便編譯這個(gè) jsp 文件為 class刘陶,并初始化這個(gè) class。
這樣 Servlet 對(duì)象就初始化完成了牢撼,事實(shí)上 Servlet 從被 web.xml 中解析到完成初始化匙隔,這個(gè)過(guò)程非常復(fù)雜,中間有很多過(guò)程熏版,包括各種容器狀態(tài)的轉(zhuǎn)化引起的監(jiān)聽(tīng)事件的觸發(fā)纷责、各種訪問(wèn)權(quán)限的控制和一些不可預(yù)料的錯(cuò)誤發(fā)生的判斷行為等等。我們這里只抓了一些關(guān)鍵環(huán)節(jié)進(jìn)行闡述撼短,試圖讓大家有個(gè)總體脈絡(luò)再膳。
下面是這個(gè)過(guò)程的一個(gè)完整的時(shí)序圖曲横,其中也省略了一些細(xì)節(jié)。
圖 4. 初始化 Servlet 的時(shí)序圖(查看大圖

圖 4. 初始化 Servlet 的時(shí)序圖

Servlet 體系結(jié)構(gòu)

我們知道 Java Web 應(yīng)用是基于 Servlet 規(guī)范運(yùn)轉(zhuǎn)的灾杰,那么 Servlet 本身又是如何運(yùn)轉(zhuǎn)的呢熙参?為何要設(shè)計(jì)這樣的體系結(jié)構(gòu)。
圖 5.Servlet 頂層類關(guān)聯(lián)圖

圖 5.Servlet 頂層類關(guān)聯(lián)圖

從上圖可以看出 Servlet 規(guī)范就是基于這幾個(gè)類運(yùn)轉(zhuǎn)的昭娩,與 Servlet 主動(dòng)關(guān)聯(lián)的是三個(gè)類题禀,分別是 ServletConfig、ServletRequest 和 ServletResponse。這三個(gè)類都是通過(guò)容器傳遞給 Servlet 的削彬,其中 ServletConfig 是在 Servlet 初始化時(shí)就傳給 Servlet 了全庸,而后兩個(gè)是在請(qǐng)求達(dá)到時(shí)調(diào)用 Servlet 時(shí)傳遞過(guò)來(lái)的。我們很清楚 ServletRequest 和 ServletResponse 在 Servlet 運(yùn)行的意義融痛,但是 ServletConfig 和 ServletContext 對(duì) Servlet 有何價(jià)值壶笼?仔細(xì)查看 ServletConfig 接口中聲明的方法發(fā)現(xiàn),這些方法都是為了獲取這個(gè) Servlet 的一些配置屬性雁刷,而這些配置屬性可能在 Servlet 運(yùn)行時(shí)被用到覆劈。而 ServletContext 又是干什么的呢? Servlet 的運(yùn)行模式是一個(gè)典型的“握手型的交互式”運(yùn)行模式。所謂“握手型的交互式”就是兩個(gè)模塊為了交換數(shù)據(jù)通常都會(huì)準(zhǔn)備一個(gè)交易場(chǎng)景责语,這個(gè)場(chǎng)景一直跟隨個(gè)這個(gè)交易過(guò)程直到這個(gè)交易完成為止炮障。這個(gè)交易場(chǎng)景的初始化是根據(jù)這次交易對(duì)象指定的參數(shù)來(lái)定制的,這些指定參數(shù)通常就會(huì)是一個(gè)配置類坤候。所以對(duì)號(hào)入座胁赢,交易場(chǎng)景就由 ServletContext 來(lái)描述,而定制的參數(shù)集合就由 ServletConfig 來(lái)描述白筹。而 ServletRequest 和 ServletResponse 就是要交互的具體對(duì)象了智末,它們通常都是作為運(yùn)輸工具來(lái)傳遞交互結(jié)果。
ServletConfig 是在 Servlet init 時(shí)由容器傳過(guò)來(lái)的徒河,那么 ServletConfig 到底是個(gè)什么對(duì)象呢系馆?
下圖是 ServletConfig 和 ServletContext 在 Tomcat 容器中的類關(guān)系圖。
圖 6. ServletConfig 在容器中的類關(guān)聯(lián)圖
圖 6. ServletConfig 在容器中的類關(guān)聯(lián)圖

上圖可以看出 StandardWrapper 和 StandardWrapperFacade 都實(shí)現(xiàn)了 ServletConfig 接口顽照,而 StandardWrapperFacade 是 StandardWrapper 門(mén)面類由蘑。所以傳給 Servlet 的是 StandardWrapperFacade 對(duì)象,這個(gè)類能夠保證從 StandardWrapper 中拿到 ServletConfig 所規(guī)定的數(shù)據(jù)棒厘,而又不把 ServletConfig 不關(guān)心的數(shù)據(jù)暴露給 Servlet纵穿。
同樣 ServletContext 也與 ServletConfig 有類似的結(jié)構(gòu),Servlet 中能拿到的 ServletContext 的實(shí)際對(duì)象也是 ApplicationContextFacade 對(duì)象谓媒。ApplicationContextFacade 同樣保證 ServletContex 只能從容器中拿到它該拿的數(shù)據(jù)句惯,它們都起到對(duì)數(shù)據(jù)的封裝作用抢野,它們使用的都是門(mén)面設(shè)計(jì)模式。
通過(guò) ServletContext 可以拿到 Context 容器中一些必要信息贬堵,比如應(yīng)用的工作路徑叉跛,容器支持的 Servlet 最小版本等筷厘。
Servlet 中定義的兩個(gè) ServletRequest 和 ServletResponse 它們實(shí)際的對(duì)象又是什么呢摊溶?更扁,我們?cè)趧?chuàng)建自己的 Servlet 類時(shí)通常使用的都是 HttpServletRequest 和 HttpServletResponse浓镜,它們繼承了 ServletRequest 和 ServletResponse。為何 Context 容器傳過(guò)來(lái)的 ServletRequest补鼻、ServletResponse 可以被轉(zhuǎn)化為 HttpServletRequest 和 HttpServletResponse 呢咨跌?
圖 7.Request 相關(guān)類結(jié)構(gòu)圖
圖 7.Request 相關(guān)類結(jié)構(gòu)圖

上圖是 Tomcat 創(chuàng)建的 Request 和 Response 的類結(jié)構(gòu)圖。Tomcat 一接受到請(qǐng)求首先將會(huì)創(chuàng)建 org.apache.coyote.Requestorg.apache.coyote.Response刊殉,這兩個(gè)類是 Tomcat 內(nèi)部使用的描述一次請(qǐng)求和相應(yīng)的信息類它們是一個(gè)輕量級(jí)的類记焊,它們作用就是在服務(wù)器接收到請(qǐng)求后遍膜,經(jīng)過(guò)簡(jiǎn)單解析將這個(gè)請(qǐng)求快速的分配給后續(xù)線程去處理,所以它們的對(duì)象很小岭粤,很容易被 JVM 回收剃浇。接下去當(dāng)交給一個(gè)用戶線程去處理這個(gè)請(qǐng)求時(shí)又創(chuàng)建 org.apache.catalina.connector. Requestorg.apache.catalina.connector. Response 對(duì)象角塑。這兩個(gè)對(duì)象一直穿越整個(gè) Servlet 容器直到要傳給 Servlet圃伶,傳給 Servlet 的是 Request 和 Response 的門(mén)面類 RequestFacade 和 RequestFacade窒朋,這里使用門(mén)面模式與前面一樣都是基于同樣的目的——封裝容器中的數(shù)據(jù)侥猩。一次請(qǐng)求對(duì)應(yīng)的 Request 和 Response 的類轉(zhuǎn)化如下圖所示:
圖 8.Request 和 Response 的轉(zhuǎn)變過(guò)程
圖 8.Request 和 Response 的轉(zhuǎn)變過(guò)程

Servlet 如何工作
我們已經(jīng)清楚了 Servlet 是如何被加載的、Servlet 是如何被初始化的划提,以及 Servlet 的體系結(jié)構(gòu)鹏往,現(xiàn)在的問(wèn)題就是它是如何被調(diào)用的掸犬。
當(dāng)用戶從瀏覽器向服務(wù)器發(fā)起一個(gè)請(qǐng)求湾碎,通常會(huì)包含如下信息:http://hostname: port /contextpath/servletpath,hostname 和 port 是用來(lái)與服務(wù)器建立 TCP 連接柔滔,而后面的 URL 才是用來(lái)選擇服務(wù)器中那個(gè)子容器服務(wù)用戶的請(qǐng)求萍虽。那服務(wù)器是如何根據(jù)這個(gè) URL 來(lái)達(dá)到正確的 Servlet 容器中的呢超全?
Tomcat7.0 中這件事很容易解決嘶朱,因?yàn)檫@種映射工作有專門(mén)一個(gè)類來(lái)完成的疏遏,這個(gè)就是 org.apache.tomcat.util.http.mapper倘零,這個(gè)類保存了 Tomcat 的 Container 容器中的所有子容器的信息视事,當(dāng) org.apache.catalina.connector. Request 類在進(jìn)入 Container 容器之前俐东,mapper 將會(huì)根據(jù)這次請(qǐng)求的 hostnane 和 contextpath 將 host 和 context 容器設(shè)置到 Request 的 mappingData 屬性中虏辫。所以當(dāng) Request 進(jìn)入 Container 容器之前,它要訪問(wèn)那個(gè)子容器這時(shí)就已經(jīng)確定了娄昆。
圖 9.Request 的 Mapper 類關(guān)系圖
圖 9.Request 的 Mapper 類關(guān)系圖

可能你有疑問(wèn)萌焰,mapper 中怎么會(huì)有容器的完整關(guān)系扒俯,這要回到圖 2 中 19 步 MapperListener 類的初始化過(guò)程,下面是 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 類作為一個(gè)監(jiān)聽(tīng)者加到整個(gè) Container 容器中的每個(gè)子容器中掌猛,這樣只要任何一個(gè)容器發(fā)生變化荔茬,MapperListener 都將會(huì)被通知,相應(yīng)的保存容器關(guān)系的 MapperListener 的 mapper 屬性也會(huì)修改坊萝。for 循環(huán)中就是將 host 及下面的子容器注冊(cè)到 mapper 中十偶。
圖 10.Request 在容器中的路由圖

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

上圖描述了一次 Request 請(qǐng)求是如何達(dá)到最終的 Wrapper 容器的,我們現(xiàn)正知道了請(qǐng)求是如何達(dá)到正確的 Wrapper 容器狮崩,但是請(qǐng)求到達(dá)最終的 Servlet 還要完成一些步驟,必須要執(zhí)行 Filter 鏈坦敌,以及要通知你在 web.xml 中定義的 listener狱窘。
接下去就要執(zhí)行 Servlet 的 service 方法了,通常情況下妇拯,我們自己定義的 servlet 并不是直接去實(shí)現(xiàn) javax.servlet.servlet 接口仗嗦,而是去繼承更簡(jiǎn)單的 HttpServlet 類或者 GenericServlet 類稀拐,我們可以有選擇的覆蓋相應(yīng)方法去實(shí)現(xiàn)我們要完成的工作铲咨。
Servlet 的確已經(jīng)能夠幫我們完成所有的工作了纤勒,但是現(xiàn)在的 web 應(yīng)用很少有直接將交互全部頁(yè)面都用 servlet 來(lái)實(shí)現(xiàn),而是采用更加高效的 MVC 框架來(lái)實(shí)現(xiàn)泉坐。這些 MVC 框架基本的原理都是將所有的請(qǐng)求都映射到一個(gè) Servlet,然后去實(shí)現(xiàn) service 方法纯丸,這個(gè)方法也就是 MVC 框架的入口。
當(dāng) Servlet 從 Servlet 容器中移除時(shí)滑凉,也就表明該 Servlet 的生命周期結(jié)束了畅姊,這時(shí) Servlet 的 destroy 方法將被調(diào)用,做一些掃尾工作粗合。
Session 與 Cookie
前面我們已經(jīng)說(shuō)明了 Servlet 如何被調(diào)用,我們基于 Servlet 來(lái)構(gòu)建應(yīng)用程序供屉,那么我們能從 Servlet 獲得哪些數(shù)據(jù)信息呢?
Servlet 能夠給我們提供兩部分?jǐn)?shù)據(jù)肛走,一個(gè)是在 Servlet 初始化時(shí)調(diào)用 init 方法時(shí)設(shè)置的 ServletConfig,這個(gè)類基本上含有了 Servlet 本身和 Servlet 所運(yùn)行的 Servlet 容器中的基本信息录别。根據(jù)前面的介紹 ServletConfig 的實(shí)際對(duì)象是 StandardWrapperFacade羹与,到底能獲得哪些容器信息可以看看這類提供了哪些接口。還有一部分?jǐn)?shù)據(jù)是由 ServletRequest 類提供庶灿,它的實(shí)際對(duì)象是 RequestFacade,從提供的方法中發(fā)現(xiàn)主要是描述這次請(qǐng)求的 HTTP 協(xié)議的信息吃衅。所以要掌握 Servlet 的工作方式必須要很清楚 HTTP 協(xié)議往踢,如果你還不清楚趕緊去找一些參考資料。關(guān)于這一塊還有一個(gè)讓很多人迷惑的 Session 與 Cookie徘层。
Session 與 Cookie 不管是對(duì) Java Web 的熟練使用者還是初學(xué)者來(lái)說(shuō)都是一個(gè)令人頭疼的東西。Session 與 Cookie 的作用都是為了保持訪問(wèn)用戶與后端服務(wù)器的交互狀態(tài)。它們有各自的優(yōu)點(diǎn)也有各自的缺陷拥褂。然而具有諷刺意味的是它們優(yōu)點(diǎn)和它們的使用場(chǎng)景又是矛盾的镊屎,例如使用 Cookie 來(lái)傳遞信息時(shí)党巾,隨著 Cookie 個(gè)數(shù)的增多和訪問(wèn)量的增加吗购,它占用的網(wǎng)絡(luò)帶寬也很大,試想假如 Cookie 占用 200 個(gè)字節(jié)榜晦,如果一天的 PV 有幾億的時(shí)候牙躺,它要占用多少帶寬。所以大訪問(wèn)量的時(shí)候希望用 Session,但是 Session 的致命弱點(diǎn)是不容易在多臺(tái)服務(wù)器之間共享,所以這也限制了 Session 的使用刀脏。
不管 Session 和 Cookie 有什么不足,我們還是要用它們揍魂。下面詳細(xì)講一下迷雪,Session 如何基于 Cookie 來(lái)工作扰柠。實(shí)際上有三種方式能可以讓 Session 正常工作:

  1. 基于 URL Path Parameter舔腾,默認(rèn)就支持
  2. 基于 Cookie,如果你沒(méi)有修改 Context 容器個(gè) cookies 標(biāo)識(shí)的話肢娘,默認(rèn)也是支持的
  3. 基于 SSL撬陵,默認(rèn)不支持,只有 connector.getAttribute("SSLEnabled")為 TRUE 時(shí)才支持

第一種情況下贱枣,當(dāng)瀏覽器不支持 Cookie 功能時(shí)吼句,瀏覽器會(huì)將用戶的 SessionCookieName 重寫(xiě)到用戶請(qǐng)求的 URL 參數(shù)中逢捺,它的傳遞格式如 /path/Servlet;name=value;name2=value2? Name3=value3辉巡,其中“Servlet;”后面的 K-V 對(duì)就是要傳遞的 Path Parameters郊楣,服務(wù)器會(huì)從這個(gè) Path Parameters 中拿到用戶配置的 SessionCookieName郎逃。關(guān)于這個(gè) SessionCookieName伦吠,如果你在 web.xml 中配置 session-config 配置項(xiàng)的話衡怀,其 cookie-config 下的 name 屬性就是這個(gè) SessionCookieName 值棍矛,如果你沒(méi)有配置 session-config 配置項(xiàng),默認(rèn)的 SessionCookieName 就是大家熟悉的“JSESSIONID”抛杨。接著 Request 根據(jù)這個(gè) SessionCookieName 到 Parameters 拿到 Session ID 并設(shè)置到 request.setRequestedSessionId 中够委。
請(qǐng)注意如果客戶端也支持 Cookie 的話,Tomcat 仍然會(huì)解析 Cookie 中的 Session ID怖现,并會(huì)覆蓋 URL 中的 Session ID茁帽。
如果是第三種情況的話將會(huì)根據(jù) javax.servlet.request.ssl_session 屬性值設(shè)置 Session ID。
有了 Session ID 服務(wù)器端就可以創(chuàng)建 HttpSession 對(duì)象了屈嗤,第一次觸發(fā)是通過(guò) request. getSession() 方法潘拨,如果當(dāng)前的 Session ID 還沒(méi)有對(duì)應(yīng)的 HttpSession 對(duì)象那么就創(chuàng)建一個(gè)新的,并將這個(gè)對(duì)象加到org.apache.catalina. Manager 的 sessions 容器中保存饶号,Manager 類將管理所有 Session 的生命周期铁追,Session 過(guò)期將被回收,服務(wù)器關(guān)閉茫船,Session 將被序列化到磁盤(pán)等脂信。只要這個(gè) HttpSession 對(duì)象存在癣蟋,用戶就可以根據(jù) Session ID 來(lái)獲取到這個(gè)對(duì)象,也就達(dá)到了狀態(tài)的保持狰闪。
圖 11.Session 相關(guān)類圖

圖 11.Session 相關(guān)類圖

上從圖中可以看出從 request.getSession 中獲取的 HttpSession 對(duì)象實(shí)際上是 StandardSession 對(duì)象的門(mén)面對(duì)象疯搅,這與前面的 Request 和 Servlet 是一樣的原理。下圖是 Session 工作的時(shí)序圖:
圖 12.Session 工作的時(shí)序圖(查看大圖
圖 12.Session 工作的時(shí)序圖

還有一點(diǎn)與 Session 關(guān)聯(lián)的 Cookie 與其它 Cookie 沒(méi)有什么不同埋泵,這個(gè)配置的配置可以通過(guò) web.xml 中的 session-config 配置項(xiàng)來(lái)指定幔欧。
Servlet 中的 Listener
整個(gè) Tomcat 服務(wù)器中 Listener 使用的非常廣泛,它是基于觀察者模式設(shè)計(jì)的丽声,Listener 的設(shè)計(jì)對(duì)開(kāi)發(fā) Servlet 應(yīng)用程序提供了一種快捷的手段礁蔗,能夠方便的從另一個(gè)縱向維度控制程序和數(shù)據(jù)。目前 Servlet 中提供了 5 種兩類事件的觀察者接口雁社,它們分別是:4 個(gè) EventListeners 類型的浴井,ServletContextAttributeListenerServletRequestAttributeListener霉撵、ServletRequestListener磺浙、HttpSessionAttributeListener 和 2 個(gè) LifecycleListeners 類型的,ServletContextListener徒坡、HttpSessionListener撕氧。如下圖所示:
圖 13.Servlet 中的 Listener(查看大圖
圖 13.Servlet 中的 Listener

它們基本上涵蓋了整個(gè) Servlet 生命周期中,你感興趣的每種事件喇完。這些 Listener 的實(shí)現(xiàn)類可以配置在 web.xml 中的 <listener> 標(biāo)簽中伦泥。當(dāng)然也可以在應(yīng)用程序中動(dòng)態(tài)添加 Listener,需要注意的是 ServletContextListener 在容器啟動(dòng)之后就不能再添加新的锦溪,因?yàn)樗O(jiān)聽(tīng)的事件已經(jīng)不會(huì)再出現(xiàn)不脯。掌握這些 Listener 的使用,能夠讓我們的程序設(shè)計(jì)的更加靈活刻诊。

總結(jié)

本文涉及到內(nèi)容有點(diǎn)多防楷,要把每個(gè)細(xì)節(jié)都說(shuō)清楚,似乎不可能坏逢,本文試著從 Servlet 容器的啟動(dòng)到 Servlet 的初始化域帐,以及 Servlet 的體系結(jié)構(gòu)等這些環(huán)節(jié)中找出一些重點(diǎn)來(lái)講述赘被,目的是能讀者有一個(gè)總體的完整的結(jié)構(gòu)圖是整,同時(shí)也詳細(xì)分析了其中的一些難點(diǎn)問(wèn)題,希望對(duì)大家有所幫助民假。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末浮入,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子羊异,更是在濱河造成了極大的恐慌事秀,老刑警劉巖彤断,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異易迹,居然都是意外死亡宰衙,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)睹欲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)供炼,“玉大人,你說(shuō)我怎么就攤上這事窘疮〈撸” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵闸衫,是天一觀的道長(zhǎng)涛贯。 經(jīng)常有香客問(wèn)我,道長(zhǎng)蔚出,這世上最難降的妖魔是什么弟翘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮身冬,結(jié)果婚禮上衅胀,老公的妹妹穿的比我還像新娘。我一直安慰自己酥筝,他們只是感情好滚躯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著嘿歌,像睡著了一般掸掏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宙帝,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天丧凤,我揣著相機(jī)與錄音,去河邊找鬼步脓。 笑死愿待,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的靴患。 我是一名探鬼主播仍侥,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鸳君!你這毒婦竟也來(lái)了农渊?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤或颊,失蹤者是張志新(化名)和其女友劉穎砸紊,沒(méi)想到半個(gè)月后传于,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡醉顽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年沼溜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片游添。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盛末,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出否淤,到底是詐尸還是另有隱情悄但,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布石抡,位于F島的核電站檐嚣,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏啰扛。R本人自食惡果不足惜嚎京,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望隐解。 院中可真熱鬧鞍帝,春花似錦、人聲如沸煞茫。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)续徽。三九已至蚓曼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钦扭,已是汗流浹背纫版。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留客情,地道東北人其弊。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像膀斋,于是被迫代替她去往敵國(guó)和親梭伐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 從三月份找實(shí)習(xí)到現(xiàn)在概页,面了一些公司籽御,掛了不少练慕,但最終還是拿到小米惰匙、百度技掏、阿里、京東项鬼、新浪哑梳、CVTE、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,246評(píng)論 11 349
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,810評(píng)論 6 342
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法绘盟,類相關(guān)的語(yǔ)法鸠真,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法龄毡,異常的語(yǔ)法吠卷,線程的語(yǔ)...
    子非魚(yú)_t_閱讀 31,630評(píng)論 18 399
  • 0 系列目錄# WEB請(qǐng)求處理 WEB請(qǐng)求處理一:瀏覽器請(qǐng)求發(fā)起處理 WEB請(qǐng)求處理二:Nginx請(qǐng)求反向代理 本...
    七寸知架構(gòu)閱讀 13,957評(píng)論 22 190
  • 一. Java基礎(chǔ)部分.................................................
    wy_sure閱讀 3,811評(píng)論 0 11