从零手写一个迷你 Tomcat —— 彻底理解 Servlet 容器原理

本文将带你从零开始,一步步实现一个简易版 Tomcat,彻底理解 Servlet 容器的工作原理。全文包含完整代码,跟着做就能跑起来!


前言

很多 Java 开发者每天都在用 Tomcat,但你真的了解它是怎么工作的吗?

  • 浏览器发送请求后,Tomcat 是怎么接收的?
  • @WebServlet 注解是怎么生效的?
  • doGet()doPost() 是怎么被调用的?

今天,我们就通过手写一个迷你 Tomcat,彻底搞懂这些问题!


一、Tomcat 的本质是什么?

Tomcat 本质上就是一个 Java 程序,它做三件事:

步骤 描述 类比
1 监听端口,等待浏览器连接 餐厅门口的迎宾员
2 解析 HTTP 请求,提取方法和路径 听客人点什么菜
3 找到对应的 Servlet 处理请求 让对应的厨师做菜

核心流程图:

复制代码
浏览器输入 URL 
    ↓
Tomcat 接收请求 
    ↓
解析请求(GET /first)
    ↓
查找 Servlet(/first → MyFirstServlet)
    ↓
调用 servlet.service()
    ↓
返回响应给浏览器

二、项目结构

复制代码
com.qcby/
├── lib/                          # Servlet 核心类
│   ├── Servlet.java              # Servlet 接口
│   ├── GenericServlet.java       # 通用 Servlet
│   ├── HttpServlet.java          # HTTP Servlet
│   ├── ServletRequest.java       # 请求接口
│   ├── ServletResponse.java      # 响应接口
│   ├── HttpServletRequest.java   # 请求实现
│   ├── HttpServletResponse.java  # 响应实现
│   └── WebServlet.java           # 注解
├── utils/                        # 工具类
│   ├── SearchClassUtil.java      # 包扫描
│   └── FileUtil.java             # 文件读取
├── config/
│   └── ServletConfigMapping.java # Servlet 容器
├── MyTomcat.java                 # 主程序
└── webapps/myweb/
    └── MyFirstServlet.java       # 业务 Servlet

三、开始实现

3.1 定义 Servlet 接口

这是所有 Servlet 的"祖宗",定义了 Servlet 的生命周期方法:

java 复制代码
package com.qcby.lib;

public interface Servlet {
    void init();      // 初始化
    void service(ServletRequest request, ServletResponse response);  // 处理请求
    void destroy();   // 销毁
}

3.2 GenericServlet 抽象类

提供 init()destroy() 的默认实现:

java 复制代码
package com.qcby.lib;

public abstract class GenericServlet implements Servlet {
    
    public void init() {
        System.out.println("Servlet 初始化...");
    }
    
    public abstract void service(ServletRequest request, ServletResponse response);
    
    public void destroy() {
        System.out.println("Servlet 销毁...");
    }
}

3.3 HttpServlet 抽象类

核心! 根据请求方法分发到 doGet()doPost()

java 复制代码
package com.qcby.lib;

public abstract class HttpServlet extends GenericServlet {
    
    @Override
    public void service(ServletRequest request, ServletResponse response) {
        String method = request.getMethod();
        
        if ("GET".equalsIgnoreCase(method)) {
            doGet(request, response);
        } else if ("POST".equalsIgnoreCase(method)) {
            doPost(request, response);
        }
    }
    
    public void doGet(ServletRequest request, ServletResponse response) {}
    public void doPost(ServletRequest request, ServletResponse response) {}
}

💡 这就是为什么我们写 Servlet 只需要重写 doGet()doPost()


3.4 请求和响应接口

ServletRequest 接口:

java 复制代码
package com.qcby.lib;

public interface ServletRequest {
    String getMethod();
    void setMethod(String method);
    String getPath();
    void setPath(String path);
}

ServletResponse 接口:

java 复制代码
package com.qcby.lib;

import java.io.IOException;

public interface ServletResponse {
    void append(String context) throws IOException;
}

3.5 请求和响应实现类

HttpServletRequest:

java 复制代码
package com.qcby.lib;

public class HttpServletRequest implements ServletRequest {
    private String method;
    private String path;
    
    @Override
    public String getMethod() { return method; }
    
    @Override
    public void setMethod(String method) { this.method = method; }
    
    @Override
    public String getPath() { return path; }
    
    @Override
    public void setPath(String path) { this.path = path; }
}

HttpServletResponse:

java 复制代码
package com.qcby.lib;

import com.qcby.utils.FileUtil;
import java.io.*;

public class HttpServletResponse implements ServletResponse {
    private OutputStream outputStream;
    
    public void setOutputStream(OutputStream outputStream) {
        this.outputStream = outputStream;
    }
    
    public OutputStream getOutputStream() {
        return outputStream;
    }
    
    @Override
    public void append(String context) throws IOException {
        outputStream.write(context.getBytes());
    }
    
    public void returnStatic(String path) throws Exception {
        String resource = FileUtil.getResourcePath(path);
        File file = new File(resource);
        if (file.exists()) {
            FileUtil.writeFile(file, outputStream);
        }
    }
}

3.6 @WebServlet 注解

java 复制代码
package com.qcby.lib;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)  // 运行时保留
@Target(ElementType.TYPE)            // 只能用在类上
public @interface WebServlet {
    String value();  // URL 路径
}

💡 @Retention(RUNTIME) 是关键! 没有它,注解在编译后就消失了,反射读不到。


四、工具类

4.1 包扫描工具

扫描指定包下所有类,这是 Spring @ComponentScan 的原理

java 复制代码
package com.qcby.utils;

import java.io.File;
import java.util.*;

public class SearchClassUtil {
    public static List<String> classPaths = new ArrayList<>();
    
    public static List<String> searchClass(String path) {
        String classPath = SearchClassUtil.class.getResource("/").getPath();
        String basePack = path.replace(".", File.separator);
        String searchPath = classPath + basePack;
        doPath(new File(searchPath), classPath);
        return classPaths;
    }
    
    private static void doPath(File file, String classpath) {
        if (file.isDirectory()) {
            for (File f : file.listFiles()) {
                doPath(f, classpath);
            }
        } else if (file.getName().endsWith(".class")) {
            String path = file.getPath()
                    .replace(classpath.replace("/", "\\").replaceFirst("\\\\", ""), "")
                    .replace("\\", ".")
                    .replace(".class", "");
            classPaths.add(path);
        }
    }
}

4.2 文件工具类

java 复制代码
package com.qcby.utils;

import java.io.*;

public class FileUtil {
    
    public static String getResourcePath(String path) {
        return FileUtil.class.getResource("/").getPath() + path;
    }
    
    public static void writeFile(File file, OutputStream out) throws Exception {
        FileInputStream fis = new FileInputStream(file);
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        fis.close();
    }
}

五、Servlet 容器

这是核心! 管理所有 Servlet,维护 URL → Servlet 的映射:

java 复制代码
package com.qcby.config;

import com.qcby.lib.*;
import com.qcby.utils.SearchClassUtil;
import java.util.*;

public class ServletConfigMapping {
    
    // URL → Servlet 映射表
    public static Map<String, HttpServlet> classMapping = new HashMap<>();
    
    // 静态代码块,类加载时自动执行
    static {
        // 1. 扫描包
        List<String> paths = SearchClassUtil.searchClass("com.qcby.webapps.myweb");
        
        // 2. 遍历每个类
        for (String path : paths) {
            try {
                // 3. 反射加载类
                Class<?> clazz = Class.forName(path);
                
                // 4. 获取 @WebServlet 注解
                WebServlet annotation = clazz.getDeclaredAnnotation(WebServlet.class);
                
                if (annotation != null) {
                    // 5. 创建实例并注册
                    HttpServlet servlet = (HttpServlet) clazz.getDeclaredConstructor().newInstance();
                    classMapping.put(annotation.value(), servlet);
                    System.out.println("注册: " + annotation.value() + " -> " + path);
                }
            } catch (Exception e) {
                // 忽略
            }
        }
    }
}

💡 静态代码块在 main 方法之前执行! 所以程序启动时,所有 Servlet 就已经注册好了。


六、MyTomcat 主程序

这是整个项目的核心!

java 复制代码
package com.qcby;

import com.qcby.config.ServletConfigMapping;
import com.qcby.lib.*;
import java.io.*;
import java.net.*;

public class MyTomcat {
    
    private static final int PORT = 7788;
    
    public static void start() throws IOException {
        // 1. 创建服务器
        ServerSocket serverSocket = new ServerSocket(PORT);
        System.out.println("MyTomcat 启动成功!端口: " + PORT);
        
        // 2. 无限循环接收请求
        while (true) {
            Socket socket = serverSocket.accept();  // 阻塞等待
            InputStream input = socket.getInputStream();
            OutputStream output = socket.getOutputStream();
            
            handler(input, output);
            socket.close();
        }
    }
    
    public static void handler(InputStream input, OutputStream output) throws IOException {
        // 1. 等待数据
        int count = 0;
        while (count == 0) {
            count = input.available();
        }
        
        // 2. 读取请求
        byte[] bytes = new byte[count];
        input.read(bytes);
        String request = new String(bytes);
        
        if (request.isEmpty()) return;
        
        // 3. 解析请求行:GET /first HTTP/1.1
        String firstLine = request.split("\n")[0];
        String method = firstLine.split("\\s")[0];  // GET
        String path = firstLine.split("\\s")[1];    // /first
        
        System.out.println("请求: " + method + " " + path);
        
        // 4. 创建 Request/Response
        HttpServletRequest httpRequest = new HttpServletRequest();
        httpRequest.setMethod(method);
        httpRequest.setPath(path);
        
        HttpServletResponse httpResponse = new HttpServletResponse();
        httpResponse.setOutputStream(output);
        
        // 5. 分发请求
        HttpServlet servlet = ServletConfigMapping.classMapping.get(path);
        
        if (servlet != null) {
            // 找到 Servlet
            String header = "HTTP/1.1 200 OK\r\nContent-Type: text/html;charset=UTF-8\r\n\r\n";
            output.write(header.getBytes());
            servlet.service(httpRequest, httpResponse);
            
        } else if (path.endsWith(".html")) {
            // 静态资源
            String header = "HTTP/1.1 200 OK\r\nContent-Type: text/html;charset=UTF-8\r\n\r\n";
            output.write(header.getBytes());
            try {
                httpResponse.returnStatic(path.substring(1));
            } catch (Exception e) {
                System.out.println("404: " + path);
            }
            
        } else {
            // 404
            String resp = "HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\n\r\n<h1>404</h1>";
            output.write(resp.getBytes());
        }
    }
    
    public static void main(String[] args) throws IOException {
        start();
    }
}

七、编写业务 Servlet

java 复制代码
package com.qcby.webapps.myweb;

import com.qcby.lib.*;

@WebServlet("/first")
public class MyFirstServlet extends HttpServlet {
    
    @Override
    public void doGet(ServletRequest request, ServletResponse response) {
        System.out.println("MyFirstServlet doGet 被调用!");
        
        try {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.append("<html><body>");
            httpResponse.append("<h1>Hello MyFirstServlet!</h1>");
            httpResponse.append("<p>请求路径: " + request.getPath() + "</p>");
            httpResponse.append("<p>当前时间: " + new java.util.Date() + "</p>");
            httpResponse.append("</body></html>");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

八、运行测试

  1. 运行 MyTomcat.main() 方法
  2. 打开浏览器访问:http://localhost:7788/first
  3. 看到页面显示 "Hello MyFirstServlet!" 就成功了!

控制台输出:

复制代码
注册: /first -> com.qcby.webapps.myweb.MyFirstServlet
MyTomcat 启动成功!端口: 7788
请求: GET /first
MyFirstServlet doGet 被调用!

九、核心原理总结

9.1 完整请求流程

复制代码
1. 浏览器发送: GET /first HTTP/1.1
        ↓
2. ServerSocket.accept() 接收连接
        ↓
3. 读取请求,解析出 method=GET, path=/first
        ↓
4. 创建 HttpServletRequest 和 HttpServletResponse
        ↓
5. 从 ServletConfigMapping 查找: /first → MyFirstServlet
        ↓
6. 调用 servlet.service(request, response)
        ↓
7. HttpServlet.service() 判断是 GET,调用 doGet()
        ↓
8. MyFirstServlet.doGet() 写入 HTML
        ↓
9. 响应返回给浏览器

9.2 核心技术点

技术 用途
Socket 编程 网络通信
HTTP 协议 解析请求、构造响应
反射 动态加载类、创建实例
注解 标记 Servlet、指定 URL
模板方法模式 HttpServlet.service() 分发请求

9.3 与真正 Tomcat 的差距

功能 我们的实现 真正的 Tomcat
并发 单线程 线程池
请求解析 只解析方法和路径 完整解析
Session 支持
Filter 支持

总结

通过这个项目,我们理解了:

  1. Tomcat 的本质:监听端口 + 解析请求 + 分发到 Servlet
  2. Servlet 生命周期:init → service → destroy
  3. @WebServlet 原理:反射 + 注解
  4. Spring 包扫描原理:递归遍历 + Class.forName

希望这篇文章对你有帮助!如果觉得不错,欢迎点赞收藏 👍


完整代码已上传,有问题欢迎评论区交流!

相关推荐
2601_962055974 小时前
跟据spring boot版本,查看对应的tomcat,并查看可支持的tomcat的版本范围
spring boot·后端·tomcat
EXI-小洲4 小时前
Java 操作 Word:字符串替换、图片插入、动态生成表格与API接口下载
java·开发语言·spring boot·word
2601_961901705 小时前
SpringBoot使用Nacos进行application.yml配置管理
java·spring boot·spring
何以解忧,唯有..6 小时前
LangChain 工具调用(Tool Calling)实战指南
java·前端·langchain
QQ_21696290966 小时前
【源码编号:project79475】SpringBoot校内二手交易平台:商品发布、分类检索、留言交流、订单管理全流程实战
java·spring boot·后端
前端 贾公子7 小时前
第09章:上下文与记忆 (2)
java·服务器·前端
2601_962203517 小时前
Java进阶07集合(续)
java·开发语言
郑州光合科技余经理8 小时前
本地生活服务系统:模块边界与结算字段怎么拆
java·开发语言·前端·后端·系统架构·uni-app·php
roman_日积跬步-终至千里8 小时前
【数据治理(6)】DataOps 不是调度工具,而是让数据产品长期可信的运行机制
java·服务器·网络
落魄大学生之流水线上谋生计8 小时前
LangGraph 基本用法指南
java·windows·python·langchain