从零手写实现 tomcat-05-servlet 处理支持

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 该文档描述了模拟实现Servlet逻辑的步骤。首先定义Servlet接口并实现,通过`web.xml`配置Servlet实例与URL的映射。接着,介绍了AbstractMiniCatHttpServlet抽象类,它处理GET和POST请求,并提供了简单的MyMiniCatHttpServlet示例。然后,解析`web.xml`以注册Servlet。最后,讨论了URL处理,包括静态资源和Servlet的分发逻辑,以及修复了在读取请求时可能出现的问题。项目开源于[https://github.com/houbb/minicat](https://github.com/houbb/minicat)。

整体思路

模拟实现 servlet 的逻辑处理,而不是局限于上一节的静态文件资源。

整体流程

1)定义 servlet 标准的 接口+实现

2)解析 web.xml 获取对应的 servlet 实例与 url 之间的映射关系。

3)调用请求

1. servlet 实现

api 接口

servlet 接口,我们直接引入 servlet-api 的标准。

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>${javax.servlet.version}</version>
</dependency>

抽象 servlet 定义

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);
    }

}

根据请求方式分别处理

简单的实现例子

下面是一个简单的处理实现:

  • MyMiniCatHttpServlet.java
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.MiniCatResponse;
import com.github.houbb.minicat.util.InnerHttpUtil;

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

/**
 * 仅用于测试
 *
 * @since 0.3.0
 */
public class MyMiniCatHttpServlet extends AbstractMiniCatHttpServlet {
   

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

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

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

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

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

}

2. web.xml 解析

说明

web.xml 需要解析处理。

比如这样的:

<?xml version="1.0" encoding="UTF-8" ?>
<web-app>

    <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>

解析方式

接口定义

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

import javax.servlet.Servlet;
import javax.servlet.http.HttpServlet;

/**
 * servlet 管理
 *
 * @since 0.3.0
 */
public interface IServletManager {
   

    /**
     * 注册 servlet
     *
     * @param url     url
     * @param servlet servlet
     */
    void register(String url, HttpServlet servlet);

    /**
     * 获取 servlet
     *
     * @param url url
     * @return servlet
     */
    HttpServlet getServlet(String url);

}

web.xml

web.xml 的解析方式,核心的处理方式:

    //1. 解析 web.xml
    //2. 读取对应的 servlet mapping
    //3. 保存对应的 url + servlet 示例到 servletMap
    private void loadFromWebXml() {
   
        InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
        SAXReader saxReader = new SAXReader();
        try {
   
            Document document = saxReader.read(resourceAsStream);
            Element rootElement = document.getRootElement();

            List<Element> selectNodes = rootElement.selectNodes("//servlet");
            //1, 找到所有的servlet标签,找到servlet-name和servlet-class
            //2, 根据servlet-name找到<servlet-mapping>中与其匹配的<url-pattern>
            for (Element element : selectNodes) {
   
                /**
                 * 1, 找到所有的servlet标签,找到servlet-name和servlet-class
                 */
                Element servletNameElement = (Element) element.selectSingleNode("servlet-name");
                String servletName = servletNameElement.getStringValue();
                Element servletClassElement = (Element) element.selectSingleNode("servlet-class");
                String servletClass = servletClassElement.getStringValue();

                /**
                 * 2, 根据servlet-name找到<servlet-mapping>中与其匹配的<url-pattern>
                 */
                //Xpath表达式:从/web-app/servlet-mapping下查询,查询出servlet-name=servletName的元素
                Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']'");

                String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
                HttpServlet httpServlet = (HttpServlet) Class.forName(servletClass).newInstance();

                this.register(urlPattern, httpServlet);
            }

        } catch (Exception e) {
   
            logger.error("[MiniCat] read web.xml failed", e);

            throw new MiniCatException(e);
        }
    }

解析之后的 HttpServlet 全部放在 servletMap 中。

然后在对应的 url 我们选取处理即可。

3. url 的处理

说明

根据 url 找到对应的 servlet 进行处理。

主要分为 3 大类:

1)url 不存在

2)url 为 html 等静态资源

3) servlet 的处理逻辑

设计

我们把这部分抽象为接口:

public void dispatch(RequestDispatcherContext context) {
   
    final MiniCatRequest request = context.getRequest();
    final MiniCatResponse response = context.getResponse();
    final IServletManager servletManager = context.getServletManager();
    // 判断文件是否存在
    String requestUrl = request.getUrl();
    if (StringUtil.isEmpty(requestUrl)) {
   
        emptyRequestDispatcher.dispatch(context);
    } else {
   
        // 静态资源
        if (requestUrl.endsWith(".html")) {
   
            staticHtmlRequestDispatcher.dispatch(context);
        } else {
   
            // servlet 
            servletRequestDispatcher.dispatch(context);
        }
    }
}

servlet 例子

如果是 servlet 的话,核心处理逻辑如下:

// 直接和 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);
    }
}

4. 读取 request 的问题修复

问题

发现 request 读取输入流的时候,有时候读取为空,但是页面明明是正常请求的。

原始代码

private void readFromStream() {
   
    try {
   
        //从输入流中获取请求信息
        int count = inputStream.available();
        byte[] bytes = new byte[count];
        int readResult = inputStream.read(bytes);
        String inputsStr = new String(bytes);
        logger.info("[MiniCat] readCount={}, input stream {}", readResult, inputsStr);
        if(readResult <= 0) {
   
            logger.info("[MiniCat] readCount is empty, ignore handle.");
            return;
        }
        //获取第一行数据
        String firstLineStr = inputsStr.split("\\n")[0];  //GET / HTTP/1.1
        String[] strings = firstLineStr.split(" ");
        this.method = strings[0];
        this.url = strings[1];
        logger.info("[MiniCat] method={}, url={}", method, url);
    } catch (IOException e) {
   
        logger.error("[MiniCat] readFromStream meet ex", e);
        throw new RuntimeException(e);
    }
}

问题分析

问题其实出在 inputStream.available() 中,网络流(如 Socket 流)与文件流不同,网络流的 available() 方法可能返回 0,即使实际上有数据可读。这是因为网络通讯是间断性的,数据可能分多个批次到达。

修正

由于 available() 方法在网络流中可能不准确,您可以尝试不使用此方法来预分配字节数组。

相反,您可以使用一个固定大小的缓冲区,或者使用 read() 方法的循环来动态读取数据。

    /**
     * 直接根据 available 有时候读取不到数据
     * @since 0.3.0
     */
    private void readFromStreamByBuffer() {
   
        byte[] buffer = new byte[1024]; // 使用固定大小的缓冲区
        int bytesRead = 0;

        try {
   
            while ((bytesRead = inputStream.read(buffer)) != -1) {
    // 循环读取数据直到EOF
                String inputStr = new String(buffer, 0, bytesRead);

                // 检查是否读取到完整的HTTP请求行
                if (inputStr.contains("\n")) {
   
                    // 获取第一行数据
                    String firstLineStr = inputStr.split("\\n")[0];
                    String[] strings = firstLineStr.split(" ");
                    this.method = strings[0];
                    this.url = strings[1];

                    logger.info("[MiniCat] method={}, url={}", method, url);
                    break; // 退出循环,因为我们已经读取到请求行
                }
            }

            if ("".equals(method)) {
   
                logger.info("[MiniCat] No HTTP request line found, ignoring.");
                // 可以选择抛出异常或者返回空请求对象
            }
        } catch (IOException e) {
   
            logger.error("[MiniCat] readFromStream meet ex", e);
            throw new RuntimeException(e);
        }
    }

开源地址

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

mini-cat 是简易版本的 tomcat 实现。别称【嗅虎】(心有猛虎,轻嗅蔷薇。)

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

相关文章
|
7月前
|
前端开发 Java 应用服务中间件
从零手写实现 tomcat-08-tomcat 如何与 springboot 集成?
该文是一系列关于从零开始手写实现 Apache Tomcat 的教程概述。作者希望通过亲自动手实践理解 Tomcat 的核心机制。文章讨论了 Spring Boot 如何实现直接通过 `main` 方法启动,Spring 与 Tomcat 容器的集成方式,以及两者生命周期的同步原理。文中还提出了实现 Tomcat 的启发,强调在设计启动流程时确保资源的正确加载和初始化。最后提到了一个名为 mini-cat(嗅虎)的简易 Tomcat 实现项目,开源于 [GitHub](https://github.com/houbb/minicat)。
|
7月前
|
Java 应用服务中间件 Apache
从零手写实现 apache Tomcat-01-入门介绍
创建简易Tomcat涉及理解其作为Java服务器的角色,建立HTTP服务器,实现Servlet接口处理动态和静态内容,以及启动和关闭服务器。项目mini-cat是一个简化版Tomcat实现,支持Servlet、静态网页和基础功能。可通过maven添加依赖并运行测试类快速体验。开源项目位于[GitHub](https://github.com/houbb/minicat)。
|
安全 Java 应用服务中间件
【JavaWeb】Tomcat底层机制和Servlet运行原理
网络通信:Tomcat使用Java的Socket API来监听特定的端口(通常是8080),接收来自客户端的HTTP请求。 线程池:Tomcat使用线程池来处理并发的请求。当有新的请求到达时,Tomcat会从线程池中获取一个空闲线程来处理该请求,这样可以提高处理效率。 生命周期管理:Tomcat负责管理Servlet和其他Web组件的生命周期,包括初始化、请求处理和销毁等阶段。(init(), run())
|
7月前
|
自然语言处理 Java 应用服务中间件
从零手写实现 tomcat-09-servlet 处理类
该文是一个关于手写实现 Apache Tomcat 简化版的系列教程摘要。作者希望通过亲自实现一个简单的 Tomcat,来深入理解其工作原理。系列教程包括了从入门介绍到解析处理 WAR 包、与 SpringBoot 集成等多个步骤。文章介绍了 Servlet 的概念,将其比作餐厅服务员,负责处理网络请求和响应。文中还详细阐述了 Servlet 的处理流程,并通过实例解释了如何实现一个基础的 Servlet。最后,提到了如何根据请求 URL 进行调度和处理,并给出了迷你版 Tomcat(Mini-Cat)的开源地址。
|
7月前
|
Java 应用服务中间件 容器
JavaWeb手写Tomcat底层机制
综上所述,Tomcat作为JavaWeb应用的Servlet容器,在接收请求、解析请求、查找Servlet、创建请求和响应对象、请求分发、生成响应、连接管理等方面起着关键作用。其底层机制通过Socket通信、Servlet生命周期管理、线程池、Session管理等技术实现了整个JavaWeb应用的运行。
47 0
|
Java 应用服务中间件 Maven
JavaWeb 手写Tomcat底层机制
JavaWeb——手写Tomcat底层 BIO线程模型 + 反射机制。
59 0
|
应用服务中间件
Tomcat - 源码分析Tomcat是如何处理一个Servlet请求的
Tomcat - 源码分析Tomcat是如何处理一个Servlet请求的
80 0
|
Java 应用服务中间件 程序员
《JavaWeb篇》07. HTTP&Tomcat&Servlet看这一篇就够了(二)
《JavaWeb篇》07. HTTP&Tomcat&Servlet看这一篇就够了(二)
169 0
《JavaWeb篇》07. HTTP&Tomcat&Servlet看这一篇就够了(二)
|
XML 前端开发 Java
《JavaWeb篇》07. HTTP&Tomcat&Servlet看这一篇就够了(三)
《JavaWeb篇》07. HTTP&Tomcat&Servlet看这一篇就够了(三)
161 0
《JavaWeb篇》07. HTTP&Tomcat&Servlet看这一篇就够了(三)
|
存储 前端开发 网络协议
《JavaWeb篇》07. HTTP&Tomcat&Servlet看这一篇就够了(一)
《JavaWeb篇》07. HTTP&Tomcat&Servlet看这一篇就够了(一)
101 0
《JavaWeb篇》07. HTTP&Tomcat&Servlet看这一篇就够了(一)

相关实验场景

更多