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

创作缘由

平时使用 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 监听器

整体思路

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

整体流程

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

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

3)调用请求

1. servlet 实现

api 接口

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

xml 复制代码
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>${javax.servlet.version}</version>
</dependency>

抽象 servlet 定义

java 复制代码
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
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 复制代码
<?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>

解析方式

接口定义

java 复制代码
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 的解析方式,核心的处理方式:

java 复制代码
    //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 等静态资源

  1. servlet 的处理逻辑

设计

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

java 复制代码
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 的话,核心处理逻辑如下:

java 复制代码
// 直接和 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 读取输入流的时候,有时候读取为空,但是页面明明是正常请求的。

原始代码

java 复制代码
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() 方法的循环来动态读取数据。

java 复制代码
    /**
     * 直接根据 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

相关推荐
ChrisitineTX12 小时前
双 11 预演:系统吞吐量跌至 0!一次由 Log4j 锁竞争引发的线程“集体猝死”
java·log4j
薛纪克12 小时前
Lambda Query:让微软Dataverse查询像“说话”一样简单
java·spring·microsoft·lambda·dataverse
程序员-周李斌12 小时前
CopyOnWriteArrayList 源码分析
java·开发语言·哈希算法·散列表
廋到被风吹走12 小时前
【Spring】两大核心基石 IoC和 AOP
java·spring
明有所思12 小时前
springsecurity更换加密方式
java·spring
却话巴山夜雨时i12 小时前
295. 数据流的中位数【困难】
java·服务器·前端
java干货13 小时前
优雅停机!Spring Boot 应用如何使用 Hook 线程完成“身后事”?
java·spring boot·后端
tealcwu13 小时前
【Unity技巧】实现在Play时自动保存当前场景
java·unity·游戏引擎
uup13 小时前
Java 多线程下的可见性问题
java