手写Tomcat原理整理

一、启动阶段

Tomcat 启动的时候,主要做一件事:把所有的 Servlet 找出来,实例化,然后存到容器里。具体分为六个步骤:

第1步:扫描目录

Tomcat 启动后首先会扫描 webapp 目录下的所有子目录和子文件,把里面所有的 .class 文件都筛选出来。

第2步:获取全类名

拿到每个 .class 文件后,获取它的全路径名,也就是包名加类名,比如 com.example.MyServlet。

第3步:反射加载

拿到全类名之后,调用 Class.forName("全类名") 这个方法。这里用的是反射机制。为什么要用反射而不是直接 new 呢?因为 Tomcat 的开发者在写框架的时候,根本不知道使用者将来会写什么 Servlet,类名叫什么,在哪个包里。new 的前提是编译时必须知道类名,所以这里只能通过反射在运行时动态加载类。

第4步:注解筛选

拿到所有 Class 对象之后,遍历每一个 Class 对象,检查它上面有没有 @WebServlet 注解。带这个注解的类才是我们要管理的 Servlet。注解里面还包含了 Servlet 的访问路径,比如 @WebServlet("/login")。

第5步:创建实例

对筛选出来的 Servlet 类,调用 newInstance() 方法创建实例。newInstance() 和 new 的区别在于,new 是编译时确定类名,而 newInstance() 是运行时通过 Class 对象动态创建,它只能调用无参构造器。

第6步:构建容器

实例创建好之后,再通过反射获取这个 Servlet 类里的 doGet 和 doPost 方法对象(Method)。然后我们构建两个 HashMap:

第一个存 Servlet 实例:key 是 @WebServlet 注解里的路径,value 是 Servlet 实例对象

第二个存方法:key 同样是路径,value 是 doGet 或 doPost 的 Method 对象

这样启动阶段就完成了。总结一下就是:扫目录 → 筛类文件 → 反射加载 → 注解筛选 → 实例化 → 存 HashMap。

二、请求处理阶段

启动完成之后,Tomcat 就进入运行状态,开始接收和处理 HTTP 请求。整个处理流程也是八个步骤:

第1步:Socket 监听端口

Tomcat 通过 Socket 监听 8080 端口。这里要注意,源码中的 Socket 不是 Java 标准库的普通 Socket,而是用了 Netty 框架的 Socket,因为 Netty 性能更高,支持异步事件驱动。

第2步:请求放入线程池队列

当一个 HTTP 请求到来时,Tomcat 不会立即创建新线程来处理,而是先把这个请求放到线程池的任务队列里。为什么要这么做呢?有两个原因:

  • 第一,防止内存耗尽。 如果瞬间来上千个请求,每个请求都开一个新线程,每个 Java 线程默认栈空间是 1MB,1000 个线程就是 1GB 内存,很容易 OOM 导致服务器死机。线程池有最大线程数限制,超出的请求在队列里等着,队列满了还有饱和策略直接拒绝,保护服务器不宕机。
  • 第二,减少线程创建销毁的开销。 线程的创建和销毁调用的是操作系统的本地方法,涉及栈空间的申请和回收,这个开销是毫秒级的。高并发场景下,频繁创建销毁线程的开销会非常大。线程池里的线程在 run() 方法里有一个死循环,会不断从队列里取任务,线程不会销毁,实现了线程复用。

第3步:线程池分配线程处理

线程池里的空闲线程会从任务队列中取出一个请求来处理。

第4步:封装 Request 和 Response 对象

线程拿到请求后,首先把通过端口传过来的 HTTP 字符流进行解析,封装成 HttpRequest 对象和 HttpResponse 对象。同时从 URL 中提取出请求路径,去掉 IP、端口号和项目名称,剩下的就是真正的资源访问路径。

第5步:判断请求类型

拿到路径之后,判断这个请求是要访问静态资源还是访问 Servlet:

如果是静态资源(CSS、JS、图片、HTML 等):就按路径去对应的目录下找文件,按相应的编码读取内容,直接返回给前端。

如果是Servlet 请求:就用这个路径作为 key,去启动阶段构建的两个 HashMap 里查找。

第6步:匹配 Servlet 实例和方法

从第一个 HashMap 里取出 Servlet 实例,从第二个 HashMap 里取出对应的 Method 对象(doGet 或 doPost,根据请求方式判断)。

第7步:动态代理调用方法

拿到实例和方法之后,调用 method.invoke(servletInstance, request, response) 来执行 Servlet 的业务方法。这里用的是动态代理。

为什么要用代理呢?因为编译时我们不知道具体的 Servlet 类名,没办法直接写 servlet.doGet(req, resp)。但 invoke() 的三要素和普通方法调用是完全对应的:第一个参数是对象实例(在哪个对象上调用),后面的参数是方法参数。所以本质上和直接调用是一样的,只是通过反射动态完成。

doGet 和 doPost 都是 void 方法,没有返回值,它们的处理结果会写到 response 对象里。

第8步:返回响应

最后通过 Socket 的输出流,把 response 对象里封装的数据返回给客户端。一次请求处理就完成了。

三、总结

所以手写 Tomcat 的核心就是:

启动阶段:扫描 → 反射 → 注解 → 实例化 → 存 HashMap

请求阶段:Socket 接收 → 线程池处理 → 封装 Request/Response → 判断静态资源还是 Servlet → HashMap 匹配 → invoke 调用 → 返回响应

而支撑这一切的三大核心技术就是:反射、线程池、动态代理。这也是几乎所有 Java 框架的通用模式。

相关推荐
孫治AllenSun9 小时前
【JVM】四大引用类型分析
java·开发语言·jvm
看-是灰机9 小时前
使用go语言实现对接
linux·开发语言·后端·docker·语言模型·golang·飞书
于慨9 小时前
安装java
java
夕除9 小时前
sign 是什么
java·前端
Penina10189 小时前
代码如何导入idea
java·intellij idea
.Hypocritical.9 小时前
Maven笔记——2
java·笔记·maven
乐橙开放平台9 小时前
养殖 SaaS 笔记:乐橙 IoT 物模型管环境,视频 OpenAPI 管回看
数据库·笔记·物联网·mysql·音视频
huohuopro9 小时前
手写 Tomcat 步骤教程
java·网络
snow@li10 小时前
Java:Lombok 完整讲解
java