从零手写一个迷你 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

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


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

相关推荐
速易达网络2 小时前
Java Web旅游网站系统介绍
java·tomcat
AI分享猿2 小时前
Java后端实战:SpringBoot接口遇袭后,用轻量WAF兼顾安全与性能
java·spring boot·安全·免费waf·web防火墙推荐·企业网站防护·防止恶意爬虫
invicinble2 小时前
关于认识,和优化idea开发
java·ide·intellij-idea
⑩-2 小时前
MVC-三层架构详解
java·架构·mvc
小刘不想改BUG2 小时前
LeetCode 56.合并区间 Java
java·python·leetcode·贪心算法·贪心
Pluchon2 小时前
硅基计划4.0 算法 BFS最短路问题&多源BFS&拓扑排序
java·算法·哈希算法·近邻算法·广度优先·宽度优先·迭代加深
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于Java的星星儿童救助帮扶系统为例,包含答辩的问题和答案
java·开发语言
清晓粼溪2 小时前
SpringBoot3-02:整合资源
java·开发语言·spring boot
CoderYanger2 小时前
C.滑动窗口-求子数组个数-越短越合法——3134. 找出唯一性数组的中位数
java·开发语言·数据结构·算法·leetcode