# 手写一个迷你Tomcat——三步理解Servlet容器的核心原理

手写一个迷你Tomcat------三步理解Servlet容器的核心原理

造过轮子的人学框架有多快?我自己写完IOC和AOP,Spring就是换个API。同样的道理,手写一个迷你Tomcat,Tomcat的源码你就看得懂了。

背景

我有一段时间想深入理解Tomcat的原理,看源码一头雾水------Connector、Container、Wrapper、Lifecycle、Pipeline、Valve,概念满天飞。

后来换了个思路:不看了,自己写一个。从最简单的HTTP服务器开始,一步步加功能,每一步都对照Tomcat的设计。

写完之后回头看Tomcat源码,发现它干的也就是这些事------只是做得更完整、更复杂。核心原理,就这么几个。

最终成果

一个能跑的迷你Servlet容器,三步演进:

复制代码
第一步:HttpServer   ------ 能返回静态HTML
第二步:HttpServer1  ------ 能动态加载并执行Servlet
第三步:HttpServer2  ------ 加Facade门面模式,安全隔离

完整代码12个类,总计不到600行。Tomcat的核心思想,全在里面了。

第一步:最简HTTP服务器

目标:浏览器发请求,服务器返回静态HTML文件。

java 复制代码
public class HttpServer {
    public static final String WEB_ROOT =
        System.getProperty("user.dir") + File.separator + "webroot";

    public void await() {
        ServerSocket serverSocket = new ServerSocket(80);
        while (!shutdown) {
            Socket socket = serverSocket.accept();
            InputStream input = socket.getInputStream();
            OutputStream output = socket.getOutputStream();

            Request request = new Request(input);
            request.parse();                          // 解析HTTP请求

            Response response = new Response(output);
            response.setRequest(request);
            response.sendStaticResource();             // 返回静态文件

            socket.close();
        }
    }
}

核心流程就四步:accept连接 → 解析请求 → 找文件 → 写回响应

Request的parse方法做的事很暴力------把输入流读成字符串,从中抠出URI:

java 复制代码
public void parse() {
    byte[] buffer = new byte[2048];
    int i = input.read(buffer);
    StringBuffer request = new StringBuffer(2048);
    for (int j = 0; j < i; j++) {
        request.append((char) buffer[j]);
    }
    uri = parseUri(request.toString());
}

private String parseUri(String requestString) {
    int index1 = requestString.indexOf(' ');
    int index2 = requestString.indexOf(' ', index1 + 1);
    return requestString.substring(index1 + 1, index2);
}

HTTP请求长这样:

复制代码
GET /index.html HTTP/1.1
Host: localhost

两个空格之间就是URI:/index.html

Response把文件读出来写回Socket:

java 复制代码
public void sendStaticResource() throws IOException {
    File file = new File(Constants.WEB_ROOT, request.getUri());
    FileInputStream fis = new FileInputStream(file);
    byte[] bytes = new byte[1024];
    int ch = fis.read(bytes, 0, 1024);
    while (ch != -1) {
        output.write(bytes, 0, ch);
        ch = fis.read(bytes, 0, 1024);
    }
}

到这里,你已经理解了Tomcat的Connector + 静态资源处理器。 Tomcat的DefaultServlet干的也是这个活------根据URI找文件,读出来写回去。

第二步:动态加载Servlet

目标 :URL以/servlet/开头时,不是返回文件,而是动态加载并执行一个Servlet类。

这是最关键的一步。Tomcat之所以是"容器",就是因为它能动态加载和执行Servlet。

java 复制代码
public class HttpServer1 {
    public void await() {
        // ... accept连接,解析请求 ...

        if (request.getUri().startsWith("/servlet/")) {
            ServletProcessor1 processor = new ServletProcessor1();
            processor.process(request, response);       // 动态加载Servlet
        } else {
            StaticResourceProcessor processor = new StaticResourceProcessor();
            processor.process(request, response);       // 返回静态文件
        }
    }
}

路由规则很简单:/servlet/xxx走Servlet处理器,其他走静态资源。

ServletProcessor1的核心------动态加载类并执行:

java 复制代码
public class ServletProcessor1 {
    public void process(Request request, Response response) {
        String uri = request.getUri();
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);

        // 1. 创建URLClassLoader,指向webroot目录
        URL[] urls = new URL[1];
        File classPath = new File(Constants.WEB_ROOT);
        String repository = new URL("file", null,
            classPath.getCanonicalPath() + File.separator).toString();
        urls[0] = new URL(null, repository, null);
        URLClassLoader loader = new URLClassLoader(urls);

        // 2. 动态加载Servlet类
        Class myClass = loader.loadClass(servletName);

        // 3. 实例化并执行service方法
        Servlet servlet = (Servlet) myClass.newInstance();
        servlet.service((ServletRequest) request, (ServletResponse) response);
    }
}

这三步就是Tomcat加载Servlet的精髓:

步骤 我们的代码 Tomcat的实现
创建ClassLoader URLClassLoader指向webroot WebappClassLoader指向WEB-INF/classes和lib
动态加载类 loader.loadClass(servletName) 同样,但加了缓存和热加载
实例化并执行 newInstance() + service() 用反射实例化,调用service()

到这里,你已经理解了Tomcat的Servlet容器核心------Wrapper。 Tomcat的StandardWrapper就是干这个的:加载Servlet类、实例化、调用service()。

第三步:Facade门面模式------安全隔离

目标:防止Servlet直接访问Request和Response的内部方法。

第二步有一个安全问题:ServletProcessor1把Request对象直接强转成ServletRequest传给了Servlet:

java 复制代码
servlet.service((ServletRequest) request, (ServletResponse) response);

问题在哪?Request类实现了ServletRequest接口,但它还有一个parse()方法。Servlet可以通过强转回去调用parse()

java 复制代码
// Servlet里写这样的代码:
if (request instanceof Request) {
    ((Request) request).parse();  // 灾难!重新解析了HTTP请求
}

这就是安全隐患------Servlet不应该能调用容器内部的方法。

Tomcat的解决方案:Facade门面模式。

java 复制代码
public class ServletProcessor2 {
    public void process(Request request, Response response) {
        // ... 加载Servlet类 ...

        RequestFacade requestFacade = new RequestFacade(request);
        ResponseFacade responseFacade = new ResponseFacade(response);

        servlet.service((ServletRequest) requestFacade,
                        (ServletResponse) responseFacade);
    }
}

RequestFacade的实现:

java 复制代码
public class RequestFacade implements ServletRequest {
    private ServletRequest request = null;

    public RequestFacade(Request request) {
        this.request = request;
    }

    public Object getAttribute(String attribute) {
        return request.getAttribute(attribute);  // 委托给真正的Request
    }

    public String getParameter(String name) {
        return request.getParameter(name);
    }

    // 只有ServletRequest接口定义的方法,没有parse()
}

关键区别:

传给Servlet的对象 Servlet能调用的方法
Request(第二步) parse()、getUri()、所有ServletRequest方法 + 内部方法
RequestFacade(第三步) 只有ServletRequest接口定义的方法

Servlet想强转?转不了------RequestFacade没有parse()方法。就算拿到RequestFacade实例,也调不到内部的Request

这就是Tomcat安全设计的核心思想。 Tomcat的org.apache.catalina.connector.RequestFacadeorg.apache.catalina.connector.ResponseFacade就是这么干的,和我们的代码结构一模一样。

对照Tomcat

手写完这三步,再回头看Tomcat的架构:

复制代码
Tomcat完整架构:

Connector(连接器)
  ├── Http11NioProtocol(处理HTTP请求)
  ├── 解析请求 → org.apache.coyote.Request
  └── 交给Container处理

Container(容器)
  ├── Engine(全局引擎)
  │   ├── Host(虚拟主机,如localhost)
  │   │   ├── Context(Web应用,如/myapp)
  │   │   │   ├── Wrapper(Servlet实例)
  │   │   │   │   ├── 动态加载Servlet类
  │   │   │   │   ├── 实例化并调用service()
  │   │   │   │   └── Facade门面模式隔离

对应关系:

我们的代码 Tomcat对应
HttpServer.await() Connector(接收HTTP连接)
Request.parse() Coyote的HTTP解析器
StaticResourceProcessor DefaultServlet
ServletProcessor2 StandardWrapper
URLClassLoader WebappClassLoader
RequestFacade/ResponseFacade RequestFacade/ResponseFacade
/servlet/路由 Context + Wrapper的URL映射

Tomcat多出来的东西(线程池、Pipeline-Valve、Lifecycle、Session管理、JNDI),都是在这些核心概念上的扩展。核心骨架我们已经写出来了。

这个练习教会我什么

1. Servlet容器本质上就是三件事

  • 接收HTTP请求(ServerSocket + 解析)
  • 路由到处理器(静态文件 or Servlet)
  • 安全隔离(Facade门面模式)

Tomcat百万行代码,最终干的也是这三件事。

2. 动态类加载是容器的灵魂

URLClassLoader.loadClass()这一行代码,就是Tomcat能"热部署"的底层原因------不需要重启JVM,用新的ClassLoader加载新的class文件就行。

Spring的IoC容器、OSGi的模块化、Java的SPI机制,底层都是这个:动态加载类,用接口隔离实现

3. 门面模式不是设计模式的考试题

很多人学门面模式就觉得是"简化接口",但在Servlet容器里,门面模式的目的是安全------防止外部代码访问内部实现。

Tomcat的Facade不是简化,是防御。这个认识不手写一遍代码,看设计模式的书是体会不到的。

和IOC/AOP的呼应

之前写过一篇《造过轮子的人学框架有多快》------自己写完IOC和AOP,Spring就是换个API。

这次又验证了一次:手写完迷你Tomcat,Tomcat源码你就看得懂了。

学框架最好的方式不是读文档、不是看视频,是自己造一个简化版的轮子。造完之后你会发现,那些"高大上"的框架,底层原理你早就在造轮子的时候弄明白了。

区别只在于:框架做得更完整、更健壮、处理了更多边界情况。但核心思想,你的轮子里已经有了。

代码结构

复制代码
HttpServer/
├── src/com/my/
│   ├── HttpServer.java              # 第一步:静态HTTP服务器
│   ├── HttpServer1.java             # 第二步:+ Servlet动态加载
│   ├── HttpServer2.java             # 第三步:+ Facade门面模式
│   ├── Request.java                 # HTTP请求解析
│   ├── Response.java                # HTTP响应输出
│   ├── RequestFacade.java           # Request的门面
│   ├── ResponseFacade.java          # Response的门面
│   ├── ServletProcessor1.java       # Servlet处理器(无Facade)
│   ├── ServletProcessor2.java       # Servlet处理器(有Facade)
│   ├── StaticResourceProcessor.java # 静态资源处理器
│   └── Constants.java               # 常量定义
└── webroot/                         # 静态文件和Servlet类

总结

三步手写迷你Tomcat:

  1. HttpServer:ServerSocket + HTTP解析 + 返回静态文件------这就是Connector
  2. HttpServer1:URLClassLoader动态加载Servlet + service()调用------这就是Wrapper
  3. HttpServer2:RequestFacade/ResponseFacade门面模式------这就是安全隔离

写完这三步,Tomcat的Connector、Container、Wrapper、Facade,不再是概念,而是你亲手写过的代码。

造轮子的意义不是替代框架,是理解框架。理解了,用起来才心里有底。


感谢豆包、智谱、OpenCode在写作过程中的辅助。

相关推荐
一诺加油鸭2 小时前
若依后端系统集成 Swagger 接口文档功能
java·开发语言
云烟成雨TD3 小时前
Spring AI Alibaba 1.x 系列【40】多智能体核心模式 - 智能体作为工具(Agent as Tool)
java·人工智能·spring
测试员周周3 小时前
【踩坑系列3】飞书机器人集体“失联“?3 个 Gateway 进程让我差点崩溃!一个测试老兵的排查实录
java·python
aq55356003 小时前
Laravel9.x新特性全解析
java·开发语言·数据库
亦暖筑序3 小时前
AI 客服系统升级实战:多 Agent 路由 + 多轮记忆 + 敏感词过滤
java·后端
zhangzeyuaaa3 小时前
深入理解 Python GIL:从机制到释放时机
java·网络·python
河阿里4 小时前
Spring AOP:企业级实战教学
java·后端·spring
lagrahhn4 小时前
IDEA一些提效的方法
java·ide·intellij-idea
yuanpan4 小时前
Python Scrapy 入门教程:从零学会抓取和解析网页数据
java·python·scrapy