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

Java技術江湖發表於2019-10-21

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視

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

喜歡的話麻煩點下Star哈

文章首發於我的個人部落格:

www.how2playlife.com

本文是微信公眾號【Java技術江湖】的《走進JavaWeb技術世界》其中一篇,本文部分內容來源於網路,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術部落格內容,引用其中了一些比較好的部落格文章,如有侵權,請聯絡作者。

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

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

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公眾號【Java技術江湖】聯絡作者,歡迎你參與本系列博文的創作和修訂。

文末贈送8000G的Java架構師學習資料,需要的朋友可以到文末了解領取方式,資料包括Java基礎、進階、專案和架構師等免費學習資料,更有資料庫、分散式、微服務等熱門技術學習視訊,內容豐富,兼顧原理和實踐,另外也將贈送作者原創的Java學習指南、Java程式設計師面試指南等乾貨資源)

什麼是Servlet

Servlet的作用是 為Java程式提供一個統一的web應用的規範,方便程式設計師統一的使用這種規範來編寫程式,應用容器可以使用提供的規範來實現自己的特性。比如tomcat的程式碼和jetty的程式碼就不一樣,但作為程式設計師你只需要瞭解servlet規範就可以從request中取值,你可以操作session等等。不用在意應用伺服器底層的實現的差別而影響你的開發。

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請求;
  1. Servlet容器接收Web Client的請求;
  1. Servlet容器建立一個HttpRequest物件,將Web Client請求的資訊封裝到這個物件中;
  1. Servlet容器建立一個HttpResponse物件;
  1. Servlet容器呼叫HttpServlet物件的service方法,把HttpRequest物件與HttpResponse物件作為引數傳給 HttpServlet物件;
  1. HttpServlet呼叫HttpRequest物件的有關方法,獲取Http請求資訊;
  1. HttpServlet呼叫HttpResponse物件的有關方法,生成響應資料;
  1. 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容器在啟動web伺服器時,將按照順序建立並初始化Servlet物件;
  3. Servlet的類檔案被更新後,重新建立Servlet。Servlet容器在啟動時自動建立Servlet,這是由在web.xml檔案中為Servlet設定的 屬性決定的。從中我們也能看到同一個型別的Servlet物件在Servlet容器中以單例的形式存在;

注意:在web.xml檔案中,某些Servlet只有 元素,沒有 元素,這樣我們無法通過url的方式訪問這些Servlet,這種Servlet通常會在 元素中配置一個 子元素,讓容器在啟動的時候自動載入這些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物件,同時根據 配置節資訊建立HttpServletConfig物件,然後執行Servlet物件的init方法,並且根據 配置節資訊來決定建立Servlet物件的順序,如果此配置節資訊為負數或者沒有配置,那麼在Servlet容器啟動時,將不載入此Servlet物件。當客戶訪問Servlet容器時,Servlet容器根據客戶訪問的URL地址,通過 配置節中的 配置節資訊找到指定的Servlet物件,並呼叫此Servlet物件的service方法。

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

Servlet生命週期

如果需要讓Servlet容器在啟動時即載入Servlet,可以在web.xml檔案中配置 元素。

Servlet中的Listener

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

Servlet中的Listener

它們基本上涵蓋了整個 Servlet 生命週期中,你感興趣的每種事件。這些 Listener 的實現類可以配置在 web.xml 中的 標籤中。當然也可以在應用程式中動態新增 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/1190000009707894

https://www.cnblogs.com/hysum/p/7100874.html

http://c.biancheng.net/view/939.html

https://www.runoob.com/

https://blog.csdn.net/android_hl/article/details/53228348

微信公眾號

個人公眾號:黃小斜

黃小斜是跨考軟體工程的 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” 即可免費無套路獲取。

我的公眾號

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69906029/viewspace-2660899/,如需轉載,請註明出處,否則將追究法律責任。

相關文章