1. 程式人生 > 實用技巧 >深入理解 Java Servlet

深入理解 Java Servlet

Servlet 簡介

Servlet(Server Applet),全稱 Java Servlet。是用 Java 編寫的伺服器端程式。其主要功能在於互動式地瀏覽和修改資料,生成動態 Web 內容。狹義的 Servlet 是指 Java 語言實現的一個介面,廣義的 Servlet 是指任何實現了這個 Servlet 介面的類,一般情況下,人們將 Servlet 理解為後者。Servlet 運行於支援 Java 的應用伺服器中。從實現上講,Servlet 可以響應任何型別的請求,但絕大多數情況下 Servlet 只用來擴充套件基於 HTTP 協議的 Web 伺服器。最早支援 Servlet 標準的是 JavaSoft 的 Java Web Server 。此後,一些其它的基於 Java 的 Web 伺服器開始支援標準的 Servlet。

如何實現 Servlet

首先 Servlet 的介面並不在 JDK 裡面,我們需要引入 servlet-api 這個第三方庫才可以找到對應的介面。下面分別是 Servlet2.x Servlet3.x Servlet4.x 最新穩定版本的依賴,這三個大版本之間有什麼特性更新會在最後的地方說明。

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
複製程式碼
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>
複製程式碼

Servlet 需要繼承 HttpServlet,然後實現父類用於處理請求的方法,如:doGet() doPost() service() 方法等,這裡我們建立一個 HelloServlet

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        PrintWriter writer = resp.getWriter();
        writer.write("<h1>Hello " + req.getParameter("name") + "</h1>");
        writer.flush();
    }
}
複製程式碼

Servlet 容器會在啟動的時候讀取 web.xml 檔案,找到檔案中配置的 Servlet 資訊,進行反射和例項化。所以還需要在 web.xml 中進行配置。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
        PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
    <servlet>
        <servlet-name>Controller</servlet-name>
        <servlet-class>com.servlet.learning.HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Controller</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>
複製程式碼

將上面的程式碼打包為 war 格式,部署至 tomcat,訪問 http://localhost:8080/JavaServletLearning/HelloServlet.do?name=nickname

如何理解 Servlet

狹義的理解 Servlet 是 javax.servlet-api 包裡面的一個介面 javax.servlet.Servlet,這個介面規範了應用在處理網路請求時必須做的一些事情,如初始化,處理業務邏輯,銷燬。下面是 Servlet 的原始碼註釋

package javax.servlet;
import java.io.IOException;

public interface Servlet {

    // 處理業務邏輯之前的初始化動作
    public void init(ServletConfig config) throws ServletException;

    // 獲取 Servlet 的配置資訊
    public ServletConfig getServletConfig();

    // 執行業務邏輯,Servlet 容器會將網路請求解析掉,封裝成 ServletRequest 物件傳遞進來
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

    public String getServletInfo();

    // 處理業務邏輯之後的銷燬動作
    public void destroy();
}
複製程式碼

為什麼要定義這樣的一個介面?Servlet 介面定義的是一套處理網路請求的規範,所有 Servlet 的實現類都必須實現這幾個方法,而所有支援 Servlet 的 Web 伺服器都會呼叫這五個方法。

以 Tomcat 為例,Tomcat 是一個 Servlet 容器,當瀏覽器發出一個請求的時候,這個請求首先會被 Tomcat 接收,根據請求的 URL 來決定由哪個 Servlet 實現類來處理,然後依次呼叫 Servlet 實現類的 init() service() destroy() 方法,最後將響應結果解析並返回給瀏覽器。

其它的 Servlet 容器如 Jetty Jboss 也都是這樣進行處理的,這也是為什麼同樣的一套程式碼,可以隨意部署在不同的支援了 Servlet 協議的 Web 伺服器中。

在 Java Servlet API 中已經提供了兩個抽象類方便開發者去實現自己的 Servlet,分別是 javax.servlet.GenericServletjavax.servlet.http.HttpServletGenericServlet 定義的是一個通用的和具體網路協議無關的 Servlet,而 HttpServlet 則定義了 Http 的 Servlet,我們在開發 Web 應用時通常繼承 HttpServlet 來定義自己的業務邏輯,SpringMvc 中的 DispatcherServlet 也是繼承於 HttpServlet 並進行了二次封裝。

package javax.servlet;

import java.io.IOException;
import java.util.Enumeration;
import java.util.ResourceBundle;

public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable {

    private transient ServletConfig config;

    public GenericServlet() { }

    public void init() throws ServletException {
    }

    // 初始化的時候設定 ServletConfig 物件
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }

    // GenericServlet 並沒有實現 service 方法,把實現留個了下面的子類去做
    public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

    // 這個方法並不是銷燬 Servlet 物件,而是在正在銷燬 Servlet 物件前必然呼叫的一個方法,我們可以重寫這個方法做一些回收的動作
    public void destroy() {
    }
}
複製程式碼
package javax.servlet.http;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;

import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public abstract class HttpServlet extends GenericServlet implements java.io.Serializable {

    // 我們在實現自己的業務邏輯的時候,一般不建議重寫這個方法,因為它已經做了很多處理
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    	HttpServletRequest	request;
    	HttpServletResponse	response;
    	try {
    	    request = (HttpServletRequest) req;
    	    response = (HttpServletResponse) res;
    	} catch (ClassCastException e) {
    	    throw new ServletException("non-HTTP request or response");
    	}
    	service(request, response);
    }

    // 真正的處理邏輯再這裡
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod(); // 獲取http請求型別

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < (lastModified / 1000 * 1000)) { // 判斷資源是否被修改
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); // 如果沒被修改,直接返回 304
                }
            }

        // 下面就是根據不同的請求型別呼叫不同的方法
        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);
        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
        } else {
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); // 匹配不到請求型別,返回 504
        }
    }
}
複製程式碼

在 Servlet 中使用過濾器

Servlet 過濾器可以動態地攔截請求和響應,以變換或使用包含在請求或響應中的資訊。可以將一個或多個 Servlet 過濾器附加到一個 Servlet 或一組 Servlet。Servlet 過濾器也可以附加到 JavaServer Pages (JSP) 檔案和 HTML 頁面。呼叫 Servlet 前呼叫所有附加的 Servlet 過濾器。Servlet 過濾器是可用於 Servlet 程式設計的 Java 類,可以實現以下目的:

  • 在客戶端的請求訪問後端資源之前,攔截這些請求。
  • 在伺服器的響應傳送回客戶端之前,處理這些響應。 首先需要實現 Filter 介面,並實現它的三個方法:
public class BeforeFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("BeforeFilter 初始化...");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("BeforeFilter 執行邏輯...");
        chain.doFilter(request, response);
    }
    @Override
    public void destroy() {
        System.out.println("BeforeFilter 被銷燬...");
    }
}
複製程式碼

與 Servlet 不同的是,過濾器會在容器啟動的時候就被初始化,而不是接收到請求了才初始化。現在,我們把過濾器配置到 web.xml 中:

<web-app>
    <!-- 過濾器的執行順序與配置順序一致,一般把過濾器配置在所有的 Servlet 之前 -->
    <filter>
        <filter-name>BeforeFilter</filter-name>
        <filter-class>com.servlet.learning.BeforeFilter</filter-class>
    </filter>
    <!-- 過濾器的路徑攔截規則 -->
    <filter-mapping>
        <filter-name>BeforeFilter</filter-name>
        <url-pattern>*.do</url-pattern>
    </filter-mapping>
</web-app>
複製程式碼

容器啟動時會讀取 web.xml 檔案,根據類的完全限定名去初始化過濾器。需要注意的是,在執行完 doFilter() 方法中的邏輯後,一定要呼叫 chain.doFilter(request, response) 方法,將請求傳回過濾器鏈。

Servlet 的生命週期

Servlet 的生命週期主要包括載入例項化、初始化、處理客戶端請求、銷燬。載入和例項化主要由 Web 容器完成,後面的三個步驟則分別由物件的 init() service() destroy() 方法提供。

初始化:init() 這個方法只會在建立 Servlet 物件時被呼叫一次,主要用來處理一些初始化的事情。Servlet 物件會在什麼時候初始化呢?1. 容器啟動時會自動初始化一些在 web.xml 中標記了 <loadstartup>1</loadstartup> 的 Servlet。2. Servlet 啟動後,首次接收到請求的時候。3. Servlet 類檔案被更新後。

處理客戶端請求:Servlet 容器呼叫 service() 方法來處理來自客戶端(瀏覽器)的請求,並把相應結果返回給客戶端。每次 Servlet 容器接收到一個 Http 請求,Servlet 容器會產生一個新的執行緒並呼叫 Servlet 例項的 service 方法,service 方法會檢查 HTTP 請求型別(GET、POST、PUT、DELETE 等),並在適當的時候呼叫 doGet、doPost、doPut、doDelete 方法。所以,在編碼請求處理邏輯的時候,我們只需要關注 doGet()、或 doPost() 的具體實現即可。

銷燬:destroy() 方法只會被呼叫一次,在 Servlet 生命週期結束時被呼叫。destroy() 方法可以讓您的 Servlet 關閉資料庫連線、停止後臺執行緒、把 Cookie 列表或點選計數器寫入到磁碟,並執行其他類似的清理活動。

什麼是 Servlet 容器?

Servlet 容器指的是支援 Servlet 協議的 Web 伺服器,容器會監聽埠,接收請求。將接收到的報文轉換為 ServletRequest 物件傳遞給 Servlet 物件,等待 Servlet 物件處理完邏輯後,把返回的 ServletResponse 物件 組裝成響應報文返回給客戶端。

Servlet 是執行緒安全的嗎?

Servlet 體系結構是建立在 Java 多執行緒機制之上的,它的生命週期是由 Web 容器負責的。當客戶端第一次請求時,容器會根據 web.xml 去例項化對應的 Servlet,後續的請求執行緒進來後不會再例項化新的 Servlet 物件,也就是會有多個執行緒在使用 Servlet 物件。這樣,當兩個或多個執行緒同時訪問同一個 Servlet 時,可能會發生多個執行緒同時訪問同一資源的情況,資料可能會變得不一致。所以在用 Servlet 構建的 Web 應用時如果不注意執行緒安全的問題,會使所寫的 Servlet 程式有難以發現的錯誤。那麼如何編寫執行緒安全的 Servlet 呢?主要由以下三種方式:

  1. 實現 SingleThreadModel 介面。該介面指定了系統如何處理對同一個 Servlet 的呼叫。如果一個 Servlet 被這個介面指定,那麼在這個 Servlet 中的 service 方法將不會有兩個執行緒被同時執行,當然也就不存線上程安全的問題。這種方法只要將前面的 HelloServlet 類的定義修改為:public class HelloServlet extends HttpServlet implements SingleThreadModel。這種方法在 Servlet 2.4 以後就不推薦使用了,因為這樣做會使每一個請求都去建立 Servlet 物件,犧牲空間去換安全的做法,會帶來很大的系統開銷。
  2. 使用 synchronized 關鍵字手動同步
  3. 避免使用例項變數,執行緒安全的問題是多個執行緒同時讀寫共享資料造成的。所以只要不在 service() 方法中使用例項變數,改為使用區域性變數,這種方法是最方便而且開銷最小的。