從零手寫實現 tomcat-09-servlet 處理類

老马啸西风發表於2024-05-11

創作緣由

平時使用 tomcat 等 web 伺服器不可謂不多,但是一直一知半解。

於是想著自己實現一個簡單版本,學習一下 tomcat 的精髓。

系列教程

從零手寫實現 apache Tomcat-01-入門介紹

從零手寫實現 apache Tomcat-02-web.xml 入門詳細介紹

從零手寫實現 tomcat-03-基本的 socket 實現

從零手寫實現 tomcat-04-請求和響應的抽象

從零手寫實現 tomcat-05-servlet 處理支援

從零手寫實現 tomcat-06-servlet bio/thread/nio/netty 池化處理

從零手寫實現 tomcat-07-war 如何解析處理三方的 war 包?

從零手寫實現 tomcat-08-tomcat 如何與 springboot 整合?

從零手寫實現 tomcat-09-servlet 處理類

從零手寫實現 tomcat-10-static resource 靜態資原始檔

從零手寫實現 tomcat-11-filter 過濾器

從零手寫實現 tomcat-12-listener 監聽器

前言

還記得我們最初 web.xml 中的 servlet 嗎?

<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
    <!-- servlet 配置 -->
    <servlet>
        <servlet-name>my</servlet-name>
        <servlet-class>com.github.houbb.minicat.support.servlet.MyMiniCatHttpServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>my</servlet-name>
        <url-pattern>/my</url-pattern>
    </servlet-mapping>
</web-app>

servlet 是什麼?我們又該如何解析實現呢?

servlet 是什麼?

Servlet可以被看作是Tomcat中的一個“服務員”。

就像餐廳裡的服務員負責接待顧客、接收點餐、上菜等任務一樣,Servlet在Web伺服器中負責處理網路請求,並返回響應結果。

Servlet的主要工作包括:

  1. 接收請求:當瀏覽器(客戶端)傳送一個HTTP請求到Tomcat時,這個請求會被對映到一個特定的Servlet。

  2. 處理請求:Servlet會根據請求的型別(比如GET或POST)和內容,執行相應的處理邏輯。比如,如果請求是要顯示一個網頁,Servlet就會生成這個網頁的內容。

  3. 生成響應:處理完請求後,Servlet會生成一個HTTP響應,這個響應包含了客戶端需要的資訊,比如網頁內容、圖片、影片等。

  4. 返回響應:最後,Servlet把生成的響應返回給客戶端,客戶端收到響應後,就可以展示網頁或者進行其他操作。

servlet 處理流程

  1. 客戶端(比如瀏覽器)傳送一個HTTP請求到Tomcat。

  2. Tomcat的請求分發器(RequestDispatcher)會根據請求的URL,找到對應的Servlet。

  3. Tomcat呼叫Servlet的service()方法,把請求交給Servlet處理。在service()方法內部,Servlet會根據HTTP請求的方法(GET、POST等)呼叫相應的處理方法。

  4. Servlet處理請求,並生成響應。比如,如果是GET請求,Servlet可能會查詢資料庫,生成一個網頁;如果是POST請求,Servlet可能會處理表單資料,執行一些業務邏輯。

  5. Servlet把生成的響應返回給Tomcat。

  6. Tomcat把響應傳送回客戶端。

透過使用Servlet,你可以靈活地處理各種HTTP請求,並生成相應的響應。

舉個板栗

好的,讓我們用餐廳的例子來比喻Tomcat中的Servlet:

想象一下,你走進一家餐廳,服務員會過來接待你。

服務員會問你需要什麼服務,這就像是HTTP請求。在Tomcat中,Servlet就扮演了服務員的角色。

  1. 接收點餐:當顧客(客戶端)進入餐廳(訪問網站),服務員(Servlet)會過來記錄顧客的點餐(接收HTTP請求)。

  2. 處理點餐:服務員會根據顧客的點餐內容(請求型別,如GET或POST),去廚房(後端邏輯)準備食物(處理請求)。

  3. 上菜:食物準備好後,服務員會將食物(響應內容)端上桌(生成HTTP響應)。

  4. 結賬:顧客享用完畢後,服務員會拿來賬單(請求結束,返回響應),顧客結賬離開。

Servlet透過這種方式,可以處理各種型別的點餐(請求),無論是簡單的檢視選單(靜態頁面請求),還是複雜的定製菜品(複雜的業務邏輯請求)。

透過合理設計Servlet,餐廳(網站)可以提供豐富多樣的服務(功能)。

自己實現

介面定義

這裡就不定義了,直接複用 servlet 的標準 api

抽象類

我們實現一個基礎的抽象類:

package com.github.houbb.minicat.support.servlet;

import com.github.houbb.minicat.constant.HttpMethodType;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public abstract class AbstractMiniCatHttpServlet extends HttpServlet {

    public abstract void doGet(HttpServletRequest request, HttpServletResponse response);

    public abstract void doPost(HttpServletRequest request, HttpServletResponse response);

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) req;
        HttpServletResponse httpServletResponse = (HttpServletResponse) res;
        if(HttpMethodType.GET.getCode().equalsIgnoreCase(httpServletRequest.getMethod())) {
            this.doGet(httpServletRequest, httpServletResponse);
            return;
        }

        this.doPost(httpServletRequest, httpServletResponse);
    }

}

介面實現

簡單的實現

package com.github.houbb.minicat.support.servlet;

import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.minicat.dto.IMiniCatResponse;
import com.github.houbb.minicat.dto.MiniCatResponseBio;
import com.github.houbb.minicat.util.InnerHttpUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 僅用於測試
 *
 * @since 0.3.0
 * @author 老馬嘯西風
 */
public class MyMiniCatHttpServlet extends AbstractMiniCatHttpServlet {

    private static final Log logger = LogFactory.getLog(MyMiniCatHttpServlet.class);


    // 方法實現
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        logger.info("MyMiniCatServlet-get");
        // 模擬耗時
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String content = "MyMiniCatServlet-get";
        IMiniCatResponse miniCatResponse = (IMiniCatResponse) response;
        miniCatResponse.write(InnerHttpUtil.http200Resp(content));
        logger.info("MyMiniCatServlet-get-end");
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        String content = "MyMiniCatServlet-post";

        IMiniCatResponse miniCatResponse = (IMiniCatResponse) response;
        miniCatResponse.write(InnerHttpUtil.http200Resp(content));
    }

}

應用啟動解析

那麼, 應該如何解析處理 servlet 呢?

DefaultServletManager

定義一個 servlet 的管理類

package com.github.houbb.minicat.support.servlet.manager;

import com.github.houbb.heaven.util.lang.StringUtil;
import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.minicat.exception.MiniCatException;

import javax.servlet.http.HttpServlet;
import java.util.HashMap;
import java.util.Map;

/**
 * servlet 管理
 *
 * 基於 web.xml 的讀取解析
 * @since 0.5.0
 * @author 老馬嘯西風
 */
public class DefaultServletManager implements IServletManager {

    // 基礎屬性省略

    @Override
    public void register(String url, HttpServlet servlet) {
        logger.info("[MiniCat] register servlet, url={}, servlet={}", url, servlet.getClass().getName());

        servletMap.put(url, servlet);
    }

    @Override
    public HttpServlet getServlet(String url) {
        return servletMap.get(url);
    }

}

register 的時機

以本地的 web.xml 解析為例

  1. 解析對應的 web.xml 中的 servlet
protected void processWebServlet(Element root, final IServletManager servletManager) {
    Map<String, String> servletClassNameMap = new HashMap<>();
    Map<String, String> urlPatternMap = new HashMap<>();
    List<Element> servletElements = root.elements("servlet");
    for (Element servletElement : servletElements) {
        String servletName = servletElement.elementText("servlet-name");
        String servletClass = servletElement.elementText("servlet-class");
        servletClassNameMap.put(servletName, servletClass);
    }
    List<Element> urlMappingElements = root.elements("servlet-mapping");
    for (Element urlElem : urlMappingElements) {
        String servletName = urlElem.elementText("servlet-name");
        String urlPattern = urlElem.elementText("url-pattern");
        urlPatternMap.put(servletName, urlPattern);
    }
    handleServletConfigMap(servletClassNameMap, urlPatternMap, servletManager);
}
  1. 註冊對應的資訊
protected void handleServletConfigMap(Map<String, String> servletClassNameMap, Map<String, String> urlPatternMap, final IServletManager servletManager) {
    try {
        for (Map.Entry<String, String> urlPatternEntry : urlPatternMap.entrySet()) {
            String servletName = urlPatternEntry.getKey();
            String urlPattern = urlPatternEntry.getValue();
            String className = servletClassNameMap.get(servletName);
            if (StringUtil.isEmpty(className)) {
                throw new MiniCatException("className not found for servletName: " + servletName);
            }
            Class servletClazz = Class.forName(className);
            HttpServlet httpServlet = (HttpServlet) servletClazz.newInstance();
            // 構建
            String fullUrlPattern = buildFullUrlPattern(urlPattern);
            servletManager.register(fullUrlPattern, httpServlet);
        }
    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
        throw new MiniCatException(e);
    }
}

呼叫的時機

servlet 註冊好了,我們什麼時候使用呢?

當然是根據請求地址 url 分發處理了。

public class ServletRequestDispatcher implements IRequestDispatcher {

    private static final Log logger = LogFactory.getLog(ServletRequestDispatcher.class);

    public void dispatch(final IMiniCatRequest request,
                         final IMiniCatResponse response,
                         final MiniCatContextConfig config) {
        final IServletManager servletManager = config.getServletManager();

        // 直接和 servlet 對映
        final String requestUrl = request.getUrl();
        HttpServlet httpServlet = servletManager.getServlet(requestUrl);
        if(httpServlet == null) {
            logger.warn("[MiniCat] requestUrl={} mapping not found", requestUrl);
            response.write(InnerHttpUtil.http404Resp());
        } else {
            // 正常的邏輯處理
            try {
                httpServlet.service(request, response);
            } catch (Exception e) {
                logger.error("[MiniCat] http servlet handle meet ex", e);

                throw new MiniCatException(e);
            }
        }
    }
}

這樣,一個簡單的 servlet 處理流程就實現了。

開源地址

 /\_/\  
( o.o ) 
 > ^ <

mini-cat 是簡易版本的 tomcat 實現。別稱【嗅虎】(心有猛虎,輕嗅薔薇。)

開源地址:https://github.com/houbb/minicat

相關文章