走進JavaWeb技術世界4:Servlet 工做原理詳解

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到個人倉庫裏查看html

https://github.com/h2pl/Java-...

喜歡的話麻煩點下Star哈前端

文章首發於個人我的博客:java

www.how2playlife.com

本文是微信公衆號【Java技術江湖】的《走進JavaWeb技術世界》其中一篇,本文部份內容來源於網絡,爲了把本文主題講得清晰透徹,也整合了不少我認爲不錯的技術博客內容,引用其中了一些比較好的博客文章,若有侵權,請聯繫做者。python

該系列博文會告訴你如何從入門到進階,從servlet到框架,從ssm再到SpringBoot,一步步地學習JavaWeb基礎知識,並上手進行實戰,接着瞭解JavaWeb項目中常常要使用的技術和組件,包括日誌組件、Maven、Junit,等等內容,以便讓你更完整地瞭解整個Java Web技術體系,造成本身的知識框架。android

爲了更好地總結和檢驗你的學習成果,本系列文章也會提供每一個知識點對應的面試題以及參考答案。git

若是對本系列文章有什麼建議,或者是有什麼疑問的話,也能夠關注公衆號【Java技術江湖】聯繫做者,歡迎你參與本系列博文的創做和修訂。程序員

文末贈送8000G的Java架構師學習資料,須要的朋友能夠到文末了解領取方式,資料包括Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)
<!-- more -->github

什麼是Servlet

Servlet的做用是爲Java程序提供一個統一的web應用的規範,方便程序員統一的使用這種規範來編寫程序,應用容器可使用提供的規範來實現本身的特性。好比tomcat的代碼和jetty的代碼就不同,但做爲程序員你只須要了解servlet規範就能夠從request中取值,你能夠操做session等等。不用在乎應用服務器底層的實現的差異而影響你的開發。web

HTTP 協議只是一個規範,定義服務請求和響應的大體式樣。Java servlet 類將HTTP中那些低層的結構包裝在 Java 類中,這些類所包含的便利方法使其在 Java 語言環境中更易於處理。面試

正如您正使用的特定 servlet 容器的配置文件中所定義的,當用戶經過 URL 發出一個請求時,這些 Java servlet 類就將之轉換成一個 HttpServletRequest,併發送給 URL 所指向的目標。當服務器端完成其工做時,Java 運行時環境(Java Runtime Environment)就將結果包裝在一個 HttpServletResponse 中,而後將原 HTTP 響應送回給發出該請求的客戶機。在與 Web 應用程序進行交互時,一般會發出多個請求並得到多個響應。全部這些都是在一個會話語境中,Java 語言將之包裝在一個 HttpSession 對象中。在處理響應時,您能夠訪問該對象,並在建立響應時向其添加事件。它提供了一些跨請求的語境。

容器(如 Tomcat)將爲 servlet 管理運行時環境。您能夠配置該容器,定製 J2EE 服務器的工做方式,以便將 servlet 暴露給外部世界。正如咱們將看到的,經過該容器中的各類配置文件,您在 URL(由用戶在瀏覽器中輸入)與服務器端組件之間搭建了一座橋樑,這些組件將處理您須要該 URL 轉換的請求。在運行應用程序時,該容器將加載並初始化 servlet管理其生命週期

Servlet體系結構

Servlet頂級類關聯圖

Servlet

Servlet的框架是由兩個Java包組成的:javax.servlet與javax.servlet.http。在javax.servlet包中定義了全部的Servlet類都必須實現或者擴展的通用接口和類。在javax.servlet.http包中定義了採用Http協議通訊的HttpServlet類。Servlet的框架的核心是javax.servlet.Servlet接口,全部的Servlet都必須實現這個接口。

Servlet接口

在Servlet接口中定義了5個方法:

1\. init(ServletConfig)方法:負責初始化Servlet對象,在Servlet的生命週期中,該方法執行一次;該方法執行在單線程的環境下,所以開發者不用考慮線程安全的問題;
2\. service(ServletRequest req,ServletResponse res)方法:負責響應客戶的請求;爲了提升效率,Servlet規範要求一個Servlet實例必須可以同時服務於多個客戶端請求,即service()方法運行在多線程的環境下,Servlet開發者必須保證該方法的線程安全性;
3\. destroy()方法:當Servlet對象退出生命週期時,負責釋放佔用的資源;
4\. getServletInfo:就是字面意思,返回Servlet的描述;
5\. getServletConfig:這個方法返回由Servlet容器傳給init方法的ServletConfig。

ServletRequest & ServletResponse

對於每個HTTP請求,servlet容器會建立一個封裝了HTTP請求的ServletRequest實例傳遞給servlet的service方法,ServletResponse則表示一個Servlet響應,其隱藏了將響應發給瀏覽器的複雜性。經過ServletRequest的方法你能夠獲取一些請求相關的參數,而ServletResponse則能夠將設置一些返回參數信息,而且設置返回內容。

ServletConfig

ServletConfig封裝能夠經過@WebServlet或者web.xml傳給一個Servlet的配置信息,以這種方式傳遞的每一條信息都稱作初始化信息,初始化信息就是一個個K-V鍵值對。爲了從一個Servlet內部獲取某個初始參數的值,init方法中調用ServletConfig的getinitParameter方法或getinitParameterNames方法獲取,除此以外,還能夠經過getServletContext獲取ServletContext對象。

ServletContext

ServletContext是表明了Servlet應用程序。每一個Web應用程序只有一個context。在分佈式環境中,一個應用程序同時部署到多個容器中,而且每臺Java虛擬機都有一個ServletContext對象。有了ServletContext對象後,就能夠共享能經過應用程序的全部資源訪問的信息,促進Web對象的動態註冊,共享的信息經過一個內部Map中的對象保存在ServiceContext中來實現。保存在ServletContext中的對象稱做屬性。操做屬性的方法:

GenericServlet

前面編寫的Servlet應用中經過實現Servlet接口來編寫Servlet,可是咱們每次都必須爲Servlet中的全部方法都提供實現,還須要將ServletConfig對象保存到一個類級別的變量中,GenericServlet抽象類就是爲了爲咱們省略一些模板代碼,實現了Servlet和ServletConfig,完成了一下幾個工做:

將init方法中的ServletConfig賦給一個類級變量,使的能夠經過getServletConfig來獲取。

public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
}

同時爲避免覆蓋init方法後在子類中必須調用super.init(servletConfig),GenericServlet還提供了一個不帶參數的init方法,當ServletConfig賦值完成就會被第帶參數的init方法調用。這樣就能夠經過覆蓋不帶參數的init方法編寫初始化代碼,而ServletConfig實例依然得以保存

爲Servlet接口中的全部方法提供默認實現。

提供方法來包裝ServletConfig中的方法。

HTTPServlet

在編寫Servlet應用程序時,大多數都要用到HTTP,也就是說能夠利用HTTP提供的特性,javax.servlet.http包含了編寫Servlet應用程序的類和接口,其中不少覆蓋了javax.servlet中的類型,咱們本身在編寫應用時大多時候也是繼承的HttpServlet。

Servlet工做原理

當Web服務器接收到一個HTTP請求時,它會先判斷請求內容——若是是靜態網頁數據,Web服務器將會自行處理,而後產生響應信息;若是牽涉到動態數據,Web服務器會將請求轉交給Servlet容器。此時Servlet容器會找到對應的處理該請求的Servlet實例來處理,結果會送回Web服務器,再由Web服務器傳回用戶端。

針對同一個Servlet,Servlet容器會在第一次收到http請求時創建一個Servlet實例,而後啓動一個線程。第二次收到http請求時,Servlet容器無須創建相同的Servlet實例,而是啓動第二個線程來服務客戶端請求。因此多線程方式不但能夠提升Web應用程序的執行效率,也能夠下降Web服務器的系統負擔。

Web服務器工做流程

接着咱們描述一下Tomcat與Servlet是如何工做的,首先看下面的時序圖:

Servlet工做原理時序圖

  1. Web Client 向Servlet容器(Tomcat)發出Http請求;
  2. Servlet容器接收Web Client的請求;
  3. Servlet容器建立一個HttpRequest對象,將Web Client請求的信息封裝到這個對象中;
  4. Servlet容器建立一個HttpResponse對象;
  5. Servlet容器調用HttpServlet對象的service方法,把HttpRequest對象與HttpResponse對象做爲參數傳給 HttpServlet對象;
  6. HttpServlet調用HttpRequest對象的有關方法,獲取Http請求信息;
  7. HttpServlet調用HttpResponse對象的有關方法,生成響應數據;
  8. Servlet容器把HttpServlet的響應結果傳給Web Client;

Servlet生命週期

在Servlet接口中定義了5個方法,其中3個方法表明了Servlet的生命週期:

1\. init(ServletConfig)方法:負責初始化Servlet對象,在Servlet的生命週期中,該方法執行一次;該方法執行在單線程的環境下,所以開發者不用考慮線程安全的問題;
2\. service(ServletRequest req,ServletResponse res)方法:負責響應客戶的請求;爲了提升效率,Servlet規範要求一個Servlet實例必須可以同時服務於多個客戶端請求,即service()方法運行在多線程的環境下,Servlet開發者必須保證該方法的線程安全性;
3\. destroy()方法:當Servlet對象退出生命週期時,負責釋放佔用的資源;

編程注意事項說明:

  1. 當Server Thread線程執行Servlet實例的init()方法時,全部的Client Service Thread線程都不能執行該實例的service()方法,更沒有線程可以執行該實例的destroy()方法,所以Servlet的init()方法是工做在單線程的環境下,開發者沒必要考慮任何線程安全的問題。
  2. 當服務器接收到來自客戶端的多個請求時,服務器會在單獨的Client Service Thread線程中執行Servlet實例的service()方法服務於每一個客戶端。此時會有多個線程同時執行同一個Servlet實例的service()方法,所以必須考慮線程安全的問題。
  3. 雖然service()方法運行在多線程的環境下,並不必定要同步該方法。而是要看這個方法在執行過程當中訪問的資源類型及對資源的訪問方式。分析以下:
1\. 若是service()方法沒有訪問Servlet的成員變量也沒有訪問全局的資源好比靜態變量、文件、數據庫鏈接等,而是隻使用了當前線程本身的資源,好比非指向全局資源的臨時變量、request和response對象等。該方法自己就是線程安全的,沒必要進行任何的同步控制。

2\. 若是service()方法訪問了Servlet的成員變量,可是對該變量的操做是隻讀操做,該方法自己就是線程安全的,沒必要進行任何的同步控制。

3\. 若是service()方法訪問了Servlet的成員變量,而且對該變量的操做既有讀又有寫,一般須要加上同步控制語句。

4\. 若是service()方法訪問了全局的靜態變量,若是同一時刻系統中也可能有其它線程訪問該靜態變量,若是既有讀也有寫的操做,一般須要加上同步控制語句。

5\. 若是service()方法訪問了全局的資源,好比文件、數據庫鏈接等,一般須要加上同步控制語句。

在建立一個 Java servlet 時,通常須要子類 HttpServlet。該類中的方法容許您訪問請求和響應包裝器(wrapper),您能夠用這個包裝器來處理請求和建立響應。Servlet的生命週期,簡單的歸納這就分爲四步:

Servlet類加載--->實例化--->服務--->銷燬;

Servlet生命週期

建立Servlet對象的時機:

  1. 默認狀況下,在Servlet容器啓動後:客戶首次向Servlet發出請求,Servlet容器會判斷內存中是否存在指定的Servlet對象,若是沒有則建立它,而後根據客戶的請求建立HttpRequest、HttpResponse對象,從而調用Servlet對象的service方法;
  2. Servlet容器啓動時:當web.xml文件中若是<servlet>元素中指定了<load-on-startup>子元素時,Servlet容器在啓動web服務器時,將按照順序建立並初始化Servlet對象;
  3. Servlet的類文件被更新後,從新建立Servlet。Servlet容器在啓動時自動建立Servlet,這是由在web.xml文件中爲Servlet設置的<load-on-startup>屬性決定的。從中咱們也能看到同一個類型的Servlet對象在Servlet容器中以單例的形式存在;
注意:在web.xml文件中,某些Servlet只有<serlvet>元素,沒有<servlet-mapping>元素,這樣咱們沒法經過url的方式訪問這些Servlet,這種Servlet一般會在<servlet>元素中配置一個<load-on-startup>子元素,讓容器在啓動的時候自動加載這些Servlet並調用init(ServletConfig config)方法來初始化該Servlet。其中方法參數config中包含了Servlet的配置信息,好比初始化參數,該對象由服務器建立。

銷燬Servlet對象的時機:

Servlet容器中止或者從新啓動:Servlet容器調用Servlet對象的destroy方法來釋放資源。以上所講的就是Servlet對象的生命週期。那麼Servlet容器如何知道建立哪個Servlet對象?Servlet對象如何配置?實際上這些信息是經過讀取web.xml配置文件來實現的。

<servlet>
    <!-- Servlet對象的名稱 -->
    <servlet-name>action<servlet-name>
    <!-- 建立Servlet對象所要調用的類 -->
    <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
    <init-param>
        <!-- 參數名稱 -->
        <param-name>config</param-name>
        <!-- 參數值 -->
        <param-value>/WEB-INF/struts-config.xml</param-value>
    </init-param>
    <init-param>
        <param-name>detail</param-name>
        <param-value>2</param-value>
    </init-param>
    <init-param>
        <param-name>debug</param-name>
        <param-value>2</param-value>
    </init-param>
    <!-- Servlet容器啓動時加載Servlet對象的順序 -->
    <load-on-startup>2</load-on-startup>
</servlet>
<!-- 要與servlet中的servlet-name配置節內容對應 -->
<servlet-mapping>
    <servlet-name>action</servlet-name>
    <!-- 客戶訪問的Servlet的相對URL路徑 -->
    <url-pattern>*.do</url-pattern>
</servlet-mapping>
當Servlet容器啓動的時候讀取<servlet>配置節信息,根據<servlet-class>配置節信息建立Servlet對象,同時根據<init-param>配置節信息建立HttpServletConfig對象,而後執行Servlet對象的init方法,而且根據<load-on-startup>配置節信息來決定建立Servlet對象的順序,若是此配置節信息爲負數或者沒有配置,那麼在Servlet容器啓動時,將不加載此Servlet對象。當客戶訪問Servlet容器時,Servlet容器根據客戶訪問的URL地址,經過<servlet-mapping>配置節中的<url-pattern>配置節信息找到指定的Servlet對象,並調用此Servlet對象的service方法。

在整個Servlet的生命週期過程當中,建立Servlet實例、調用實例的init()和destroy()方法都只進行一次,當初始化完成後,Servlet容器會將該實例保存在內存中,經過調用它的service()方法,爲接收到的請求服務。下面給出Servlet整個生命週期過程的UML序列圖,如圖所示:

Servlet生命週期

若是須要讓Servlet容器在啓動時即加載Servlet,能夠在web.xml文件中配置<load-on-startup>元素。

Servlet中的Listener

Listener 使用的很是普遍,它是基於觀察者模式設計的,Listener 的設計對開發 Servlet 應用程序提供了一種快捷的手段,可以方便的從另外一個縱向維度控制程序和數據。目前 Servlet 中提供了 5 種兩類事件的觀察者接口,它們分別是:4 個 EventListeners 類型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 個 LifecycleListeners 類型的,ServletContextListener、HttpSessionListener。以下圖所示:

Servlet中的Listener

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

Cookie與Session

Servlet 可以給咱們提供兩部分數據,一個是在 Servlet 初始化時調用 init 方法時設置的 ServletConfig,這個類基本上含有了 Servlet 自己和 Servlet 所運行的 Servlet 容器中的基本信息。還有一部分數據是由 ServletRequest 類提供,從提供的方法中發現主要是描述此次請求的 HTTP 協議的信息。關於這一塊還有一個讓不少人迷惑的 Session 與 Cookie。

Session 與 Cookie 的做用都是爲了保持訪問用戶與後端服務器的交互狀態。它們有各自的優勢也有各自的缺陷。然而具備諷刺意味的是它們優勢和它們的使用場景又是矛盾的,例如使用 Cookie 來傳遞信息時,隨着 Cookie 個數的增多和訪問量的增長,它佔用的網絡帶寬也也會愈來愈大。因此大訪問量的時候但願用 Session,可是 Session 的致命弱點是不容易在多臺服務器之間共享,因此這也限制了 Session 的使用。

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

  • 基於 URL Path Parameter,默認就支持
  • 基於 Cookie,若是你沒有修改 Context 容器個 cookies 標識的話,默認也是支持的
  • 基於 SSL,默認不支持,只有 connector.getAttribute("SSLEnabled") 爲 TRUE 時才支持

第一種狀況下,當瀏覽器不支持 Cookie 功能時,瀏覽器會將用戶的 SessionCookieName 重寫到用戶請求的 URL 參數中,它的傳遞格式如:

/path/Servlet?name=value&name2=value2&JSESSIONID=value3

接着 Request 根據這個 JSESSIONID 參數拿到 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 來獲取到這個對象,也就達到了狀態的保持。

Session相關類圖

上從圖中能夠看出從 request.getSession 中獲取的 HttpSession 對象其實是 StandardSession 對象的門面對象,這與前面的 Request 和 Servlet 是同樣的原理。下圖是 Session 工做的時序圖:

Session工做的時序圖

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

參考文章

https://segmentfault.com/a/11...

https://www.cnblogs.com/hysum...

http://c.biancheng.net/view/9...

https://www.runoob.com/

https://blog.csdn.net/android...

微信公衆號

我的公衆號:黃小斜

黃小斜是跨考軟件工程的 985 碩士,自學 Java 兩年,拿到了 BAT 等近十家大廠 offer,從技術小白成長爲阿里工程師。

做者專一於 JAVA 後端技術棧,熱衷於分享程序員乾貨、學習經驗、求職心得和程序人生,目前黃小斜的CSDN博客有百萬+訪問量,知乎粉絲2W+,全網已有10W+讀者。

黃小斜是一個斜槓青年,堅持學習和寫做,相信終身學習的力量,但願和更多的程序員交朋友,一塊兒進步和成長!

原創電子書:
關注危險公衆號【黃小斜】後回覆【原創電子書】便可領取我原創的電子書《菜鳥程序員修煉手冊:從技術小白到阿里巴巴Java工程師》這份電子書總結了我2年的Java學習之路,包括學習方法、技術總結、求職經驗和麪試技巧等內容,已經幫助不少的程序員拿到了心儀的offer!

程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公衆號後,後臺回覆關鍵字 「資料」 便可免費無套路獲取,包括Java、python、C++、大數據、機器學習、前端、移動端等方向的技術資料。

技術公衆號:Java技術江湖

若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人微信公衆號【Java技術江湖】

這是一位阿里 Java 工程師的技術小站。做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

(關注公衆號後回覆」Java「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)

Java工程師必備學習資源: 一些Java工程師經常使用學習資源,關注公衆號後,後臺回覆關鍵字 「Java」 便可免費無套路獲取。

個人公衆號

相關文章
相關標籤/搜索