一、启动阶段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 线程默认栈空间是 1MB1000 个线程就是 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 框架的通用模式。
手写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 线程默认栈空间是 1MB1000 个线程就是 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 框架的通用模式。