JAVA網路程式設計基本功之Servlet與Servlet容器

等不到的口琴發表於2021-03-01

Servlet與Servlet容器關係

Servlet

比較這兩個的區別, 就得先搞清楚Servlet 的含義, Servlet (/ˈsərvlit/ ) 翻譯成中文就是小型應用程式或者小服務程式, 與之相類似的是Server (/ˈsɜːrvər/), 翻譯過來是伺服器的意思, 可見這二者承擔類似的功能,但是Servlet更輕量,

web開發的本質就一句話:客戶端和伺服器交換資料。於是使用 Java 的 Socket 套接字進行程式設計,去處理客戶端來的 tcp 請求,經過編解碼處理讀取請求體,獲取請求行,然後找到請求行對應的處理邏輯步入伺服器的處理中,處理完畢把對應的結果返回給當前的 Socket 連結,響應完畢,關閉 Socket。

上述過程中, 建立連線、傳輸資料、關閉連線等過程是tomcat容器幫你做了這些事情, 而拿到請求行之後去找對應的 url 路由,這一部分是誰做的呢?是Servlet ! 簡單來說Servlet就是一段處理 web 請求的邏輯。

具體來說Servlet具有以下幾個特點:

  1. Servlet是用Java編寫的Server端程式,它與協議和平臺無關。
  2. Servlet執行於Java-enabled Web Server中。
  3. Java Servlet可以動態地擴充套件Server的能力,並採用請求-響應模式提供Web服務。
  4. 最早支援Servlet技術的是JavaSoft的Java Web Server。
  5. 此後,一些其它的基於Java的Web Server開始支援標準的Servlet API。
  6. Servlet的主要功能在於互動式地瀏覽和修改資料,生成動態Web內容。

上面六點中,最需要被記住的是Servlet可以動態地擴充套件Server的能力,並採用請求-響應模式提供Web服務

JDK中的Servlet是一個介面:

public interface Servlet {
   
    public void init(ServletConfig config) throws ServletException;

    public ServletConfig getServletConfig();

    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;

    public String getServletInfo();

    public void destroy();
}

可以看到Servlet 是一個介面, 規定了請求從容器到達 web 服務端的規範,詳細內容在後面的Servlet生命週期中詳細梳理,這兒簡單概括三個重要步驟是:

  1. init():初始化請求的時候要做什麼;
  2. service():拿到請求的時候要做什麼;
  3. destory():處理完請求銷燬的時候要做什麼。

所有實現 Servlet 的實現方都是在這個規範的基礎上進行開發。那麼 Servlet 中的資料是從哪裡來的呢?答案就是 Servlet 容器。容器才是真正與客戶端打交道的那一方。一個容器中 Servlet 可以有多個, 常見的Servlet容器Tomcat,它監聽了客戶端的請求埠,根據請求行資訊確定將請求交給哪個Servlet 處理,找到處理的Servlet之後,呼叫該Servlet的 service() 方法,處理完畢將對應的處理結果包裝成ServletResponse 物件返回給客戶端。

Servlet容器

現在講講Servlet容器, 前面說過看Servlet只是一個介面或者說是規範, 那麼就勢必有具體實現, 而Servlet具體實現或者說包裝器是Wrapper, 直接管理Wrapper的容器就是Context, 一個 Context 對應一個 Web 工程, 也就是說Context 容器如何執行將直接影響 Servlet 的工作。

由圖可以知道, Tomcat底層是Context, Context負責管理Servlet包裝類Wrapper。

下面建立一個例項物件並呼叫 start 方法就可以很容易啟動 Tomcat,我們還可以通過這個物件來增加和修改 Tomcat 的配置引數,如可以動態增加 Context、Servlet 等。我們就選擇 Tomcat7 自帶的 examples Web 工程,並看看它是如何加到這個 Context 容器中的。

//給 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);

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

//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 應用時將會建立一個 StandardContext 容器,並且給這個 Context 容器設定必要的引數(
url 代表這個應用在 Tomcat 中的訪問路徑; path 代表這個應用實際的物理路徑) 其中最重要的一個配置是 ContextConfig,【ContextConfig監聽器】繼承了 【LifecycleListener 監聽器介面】,它是在呼叫清單 2 時被加入到 StandardContext 容器中。 當 Context 容器初始化狀態設為 init 時,新增在 Context 容器的 Listener 將會被呼叫。【ContextConfig監聽器】將會負責整個 Web 應用配置檔案的解析工作。最後將這個 Context 容器加到父容器 Host 中。

Servlet生命週期

Servlet生命週期分為四個部分: 例項化==>初始化==>執行處理==>銷燬。

例項化

new , 伺服器第一次被訪問時,載入一個Servlet容器,只會被載入一次。

初始化

init:建立完Servlet容器後,會呼叫僅執行一次的init()初始化方法,用於初始化Servlet物件,無論多少臺客戶端在伺服器執行期間訪問都不會再執行init()方法。

可以在繼承的GenericServlet這個抽象類中看到初始化方法:

public void init() throws ServletException {
    }

而在我們的Servlet類中應繼承呼叫該方法:

public void init() throws ServletException {
        super.init();
    }

建立Servlet物件的時機:

  1. Servlet容器啟動時:讀取web.xml配置檔案中的資訊,構造指定的Servlet物件,建立ServletConfig物件,同時將ServletConfig物件作為引數來呼叫Servlet物件的init方法。
  2. 在Servlet容器啟動後:客戶首次向Servlet發出請求,Servlet容器會判斷記憶體中是否存在指定的Servlet物件,如果沒有則建立它,然後根據客戶的請求建立HttpRequest、HttpResponse物件,從而呼叫Servlet 物件的service方法。
  3. Servlet Servlet容器在啟動時自動建立Servlet,這是由在web.xml檔案中為Servlet設定的<load-on-startup>屬性決定的。從中我們也能看到同一個型別的Servlet物件在Servlet容器中以單例的形式存在。

執行處理

執行處理——service()方法

它是Servlet的核心,負責響應客戶的請求。每當一個客戶請求一個HttpServlet物件,該物件的Service()方法就要呼叫,而且傳遞給這個方法一個“請求”(ServletRequest)物件和一個“響應”(ServletResponse)物件作為引數。在HttpServlet中已存在Service()方法。預設的服務功能是呼叫與HTTP請求的方法相應的do功能。

HttpServlet的抽象類提供了doGet()、doPost()……等方法。對應了request請求的傳送方法,與之相匹配:

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_get_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(405, msg);
        } else {
            resp.sendError(400, msg);
        }

    }
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String protocol = req.getProtocol();
        String msg = lStrings.getString("http.method_post_not_supported");
        if (protocol.endsWith("1.1")) {
            resp.sendError(405, msg);
        } else {
            resp.sendError(400, msg);
        }

    }

上面是操作性最高的部分。

銷燬

銷燬——destroy:在伺服器關閉或重啟時,Servlet會呼叫destroy方法來銷燬,將Servlet容器標記為垃圾檔案,讓GC做回收處理。我們編寫的Servlet是呼叫了GenericServlet抽象類的destroy方法:

@Override
    public void destroy() {
        super.destroy();
    }

Servlet工作原理

1、首先簡單解釋一下Servlet接收和響應客戶請求的過程:
客戶傳送一個請求,Servlet是呼叫service()方法對請求進行響應,service()方法中對請求的方式進行了匹配。選擇呼叫doGet,doPost等這些方法,然後再進入對應的方法中呼叫邏輯層的方法,實現對客戶的響應。在Servlet介面和GenericServlet中是沒有doGet()、doPost()等等這些方法的,HttpServlet中定義了這些方法,但是都是返回error資訊,所以,我們每次定義一個Servlet的時候,都必須實現doGet或doPost等這些方法。

2、每一個自定義的Servlet都必須實現Servlet的介面,Servlet介面中定義了五個方法,其中比較重要的三個方法涉及到Servlet的生命週期,分別是上文提到的init(),service(),destroy()方法。GenericServlet是一個通用的,不特定於任何協議的Servlet,它實現了Servlet介面。而HttpServlet繼承於GenericServlet,因此HttpServlet也實現了Servlet介面。所以我們定義Servlet的時候只需要繼承HttpServlet即可。

3、Servlet介面和GenericServlet是不特定於任何協議的,而HttpServlet是特定於HTTP協議的類,所以HttpServlet中實現了service()方法,並將請求ServletRequest、ServletResponse 強轉為HttpRequest 和 HttpResponse。

4、另外,Servlet是單例模式,執行緒是不安全的,因此在service()方法中儘量不要操作全域性變數。但實際上,可以通過使用session和application來代替全域性變數,只是會加大伺服器負載。

Servlet處理請求的過程

  1. 客戶端傳送請求給伺服器。
  2. 容器根據請求及web.xml判斷對應的Servlet是否存在,如果不存在則返回404
  3. 容器根據請求及web.xml判斷對應的Servlet是否已經被例項化,若是相應的Servlet沒有被例項化,則容器將會載入相應的Servlet到Java虛擬機器並例項化
  4. 呼叫例項物件的service()方法,並開啟一個新的執行緒去執行相關處理。呼叫servce方法,判斷是呼叫doGet方法還是doPost方法
  5. 業務完成後響應相關的頁面傳送給客戶端。

相關文章