本文将带你从零开始,一步步实现一个简易版 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();
}
}
}
八、运行测试
- 运行
MyTomcat.main()方法 - 打开浏览器访问:
http://localhost:7788/first - 看到页面显示 "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 | 无 | 支持 |
总结
通过这个项目,我们理解了:
- Tomcat 的本质:监听端口 + 解析请求 + 分发到 Servlet
- Servlet 生命周期:init → service → destroy
- @WebServlet 原理:反射 + 注解
- Spring 包扫描原理:递归遍历 + Class.forName
希望这篇文章对你有帮助!如果觉得不错,欢迎点赞收藏 👍
完整代码已上传,有问题欢迎评论区交流!