一、启动阶段
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 框架的通用模式。