手写一个迷你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.RequestFacade和org.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:
- HttpServer:ServerSocket + HTTP解析 + 返回静态文件------这就是Connector
- HttpServer1:URLClassLoader动态加载Servlet + service()调用------这就是Wrapper
- HttpServer2:RequestFacade/ResponseFacade门面模式------这就是安全隔离
写完这三步,Tomcat的Connector、Container、Wrapper、Facade,不再是概念,而是你亲手写过的代码。
造轮子的意义不是替代框架,是理解框架。理解了,用起来才心里有底。
感谢豆包、智谱、OpenCode在写作过程中的辅助。