手动实现 Tomcat 核心机制:打造属于自己的 Servlet 容器
在日常 Java Web 开发中,Tomcat 无疑是我们最常接触的 Web 服务器之一。它默默承担了请求解析、Servlet 加载、响应输出等一系列复杂的任务,让开发者可以专注于业务逻辑的实现。然而,你是否曾经好奇:Tomcat 是如何接收浏览器请求并将其交给 Servlet 处理的?Servlet 是在什么时候被加载和执行的?
与其只停留在“使用层面”,不如亲手拆解并实现一遍核心机制。本篇博客将带你从零开始动手实现一个简易版的 Servlet 容器,通过手写 socket 通信、URL 映射、Servlet 管理等模块,深入理解 Tomcat 的底层架构与工作原理。
项目技术栈
Maven(项目工具包管理)、网络通信(Socket网络编程)、HTTP协议、IO流、XML解析(DOM4j)、反射机制和servlet规范、BIO多线程模型、模板设计模式。
项目系统框图
系统整体架构代码实现
请求封装模块
将底层 Socket 输入流中的原始 HTTP 请求报文解析成结构化的数据,供 Servlet 调用。
package http;import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;/*** @author xuchuanlei* @version 1.0* description 封装http请求为我们自定义servlet的请求信息类型(切面编程思想)*/
public class HspRequest {private String method;private String uri;// 存放参数列表 参数名-参数值 =》Hashmapprivate HashMap<String, String> parametersMapping=new HashMap<>();private InputStream inputStream=null;// 消息体调用构造器封装public HspRequest(InputStream inputStream) {this.inputStream = inputStream;// 封装的具体操作,即解析http请求encapHttpRequest();}private void encapHttpRequest() {System.out.println("HspRequest init()");try {
// 获取字符流BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"utf-8"));// 数据示例
// GET /hspCalServlet?num1=10&num2=30 HTTP/1.1
// GET /hspCalServlet?num1=10&num2=30 HTTP/1.1
// GET /?num1=10&num2=10 HTTP/1.1
// Host: localhost:8080
// 读取第一行,先解析method方法String requestLine = bufferedReader.readLine();String[] requestLineArr = requestLine.split(" ");method = requestLineArr[0];// 在解析uri有无参数列表int index = requestLineArr[1].indexOf("?");if (index == -1) {uri = requestLineArr[1];}else {
// 有参数列表进行进一步处理uri = requestLineArr[1].substring(0, index);//获取参数列表->parametersMapping//parameters => num1=10&num2=30String parameters = requestLineArr[1].substring(index+1);String[] parametersPair = parameters.split("&");
// 鲁棒性的判断if (null != parametersPair && !"".equals(parametersPair)) {
// 再次分割for (String parameterPair : parametersPair) {String[] parameterVal = parameterPair.split("=");if (parameterVal.length == 2) {
// 放入hashmapparametersMapping.put(parameterVal[0], parameterVal[1]);}}}}} catch (Exception e) {throw new RuntimeException(e);}}// 获取参数值,根据参数名public String getParameter(String name) {if (parametersMapping.containsKey(name)) {return parametersMapping.get(name);}else {return "";}}public String getMethod() {return method;}public void setMethod(String method) {this.method = method;}public String getUri() {return uri;}public void setUri(String uri) {this.uri = uri;}public HashMap<String, String> getParametersMapping() {return parametersMapping;}public void setParametersMapping(HashMap<String, String> parametersMapping) {this.parametersMapping = parametersMapping;}public InputStream getInputStream() {return inputStream;}public void setInputStream(InputStream inputStream) {this.inputStream = inputStream;}@Overridepublic String toString() {return "HspRequest{" +"method='" + method + '\'' +", uri='" + uri + '\'' +", parametersMapping=" + parametersMapping +", inputStream=" + inputStream +'}';}
}
服务端响应模块
将业务 Servlet 生成的响应内容,通过标准的 HTTP 响应格式写入 Socket 输出流,返回给浏览器客户端。
package http;import java.io.OutputStream;/*** @author xuchuanlei* @version 1.0* description http响应消息的封装*/
public class HspResponse {private OutputStream outputStream=null;
// 写一个消息头public static final String respHeader = "HTTP/1.1 200 OK\r\n" +"Content-Type:text/html;charset=utf-8\r\n\r\n";// 在创建时传入对象,构造器public HspResponse(OutputStream outputStream) {this.outputStream = outputStream;}//获取输出流实例,完成消息传输public OutputStream getOutputStream() {return outputStream;}
}
自定义 Servlet 容器中的核心接口定义
定义了一个自定义 Servlet 应该具备的核心生命周期方法:
package servlet;import http.HspRequest;
import http.HspResponse;import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @author xuchuanlei* @version 1.0* description 定义servlet接口,和init,doGet,doPost方法 (切面编程)*/
public interface HspServlet {public void init() throws Exception;public void service(HspRequest request, HspResponse response) throws Exception;public void destroy();
}
Servlet 的“模板控制器”
简易 Servlet 容器中起到了业务调度核心枢纽的作用
package servlet;import http.HspRequest;
import http.HspResponse;import javax.servlet.http.HttpServlet;/*** @author xuchuanlei* @version 1.0* description 抽象类,使用模板设计模式,重写service*/
public abstract class HspHttpServlet implements HspServlet {@Overridepublic void service(HspRequest request, HspResponse response) throws Exception {
// 利用多态的动态绑定机制,让doPost和doGet的实现交予我们的业务servlet子类if ("GET".equalsIgnoreCase(request.getMethod())) {this.doGet(request, response);}else if ("POST".equalsIgnoreCase(request.getMethod())) {this.doPost(request, response);}}public abstract void doGet(HspRequest request, HspResponse response);public abstract void doPost(HspRequest request, HspResponse response);}
业务 Servlet 的核心逻辑处理类
它实现了 HspHttpServlet
,即你自定义 Servlet 容器中:
-
接收并解析客户端请求参数
-
执行业务计算(num1 + num2)
-
构建 HTTP 响应返回给浏览器
package servlet;import http.HspRequest;
import http.HspResponse;
import utils.WebUtils;import javax.servlet.http.HttpServlet;
import java.io.IOException;
import java.io.OutputStream;/*** @author xuchuanlei* @version 1.0* description .......*/
public class HspCalServlet extends HspHttpServlet {@Overridepublic void doGet(HspRequest request, HspResponse response) {
// 实现我们的定制servlet逻辑
// 拿到请求并解析String num1 = request.getParameter("num1");String num2 = request.getParameter("num2");int num1_ = WebUtils.parseInt(num1,0);int num2_ = WebUtils.parseInt(num2,0);int sum = num1_ + num2_;OutputStream outputStream = response.getOutputStream();String respMes = HspResponse.respHeader+ "<h1>" + num1 + " + " + num2 + " = " + sum + " HspTomcatV3 - 反射+xml创建</h1>";try {outputStream.write(respMes.getBytes());outputStream.flush();outputStream.close();} catch (IOException e) {throw new RuntimeException(e);}}@Overridepublic void doPost(HspRequest request, HspResponse response) {doGet(request, response);}@Overridepublic void init() throws Exception {}@Overridepublic void destroy() {}
}
自定义 Servlet 容器核心类
实现servlet与多线程关联,同时实现xml的动态类加载(封装+继承+多态+io+dom4j(xml配置文件读取))
-
请求分发与多线程处理(基于 Socket)
-
通过解析 XML 配置,实现 Servlet 的类路径注册与动态加载(基于反射)
package tomcat;import handler.HspRequestHandler;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.junit.Test;
import servlet.HspCalServlet;
import servlet.HspHttpServlet;import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;/*** @author xuchuanlei* @version 1.0* description 实现servlet与多线程关联,同时实现xml的动态类加载(封装+继承+多态+io+dom4j(xml配置文件读取))*/
public class HspTomcatV3 {// 类加载,通过xml解析得到类的全路径
// 定义映射类hashmappublic static final ConcurrentHashMap<String, HspHttpServlet>servletMapping = new ConcurrentHashMap<>();public static final ConcurrentHashMap<String,String>servletUrlMapping = new ConcurrentHashMap<>();public static void main(String[] args) throws Exception {HspTomcatV3 hspTomcatV3 = new HspTomcatV3();hspTomcatV3.init();hspTomcatV3.run();}public void run() throws Exception {//在8080端口监听ServerSocket serverSocket = new ServerSocket(8080);System.out.println("=======hsptomcatV2 在8080监听=======");//只要 serverSocket没有关闭,就一直等待浏览器/客户端的连接while (!serverSocket.isClosed()) {//1. 接收到浏览器的连接后,如果成功,就会得到socket//2. 这个socket 就是 服务器和 浏览器的数据通道Socket socket = serverSocket.accept();//3. 创建一个线程对象,并且把socket给该线程// 这个是java线程基础HspRequestHandler hspRequestHandler =new HspRequestHandler(socket);new Thread(hspRequestHandler).start();}}// 对容器进行初始化public void init() throws Exception {
// 读取xml文件,利用dom4j工具,利用发射机制String resource = HspTomcatV3.class.getResource("/").getPath();System.out.println(resource);SAXReader saxReader = new SAXReader();try {Document read = saxReader.read(new File(resource + "web.xml"));System.out.println("xml=\t"+read);
// 得到根元素Element rootElement = read.getRootElement();
// 获取根元素下面的所有子元素List<Element> element = rootElement.elements();
// 遍历for(Element e:element){if ("servlet".equals(e.getName())){
// 确保这是一个servlet配置
// 使用反射机制将该实例放入servletMappingSystem.out.println("发现现有的servlet!!!!!");Element elementName = e.element("servlet-name");Element elementClass = e.element("servlet-class");System.out.println( "类名:\t"+ elementName.getText());System.out.println("类的全路径:\t"+elementClass.getText());System.out.println("类的全路径:\t"+Class.forName(elementClass.getText().trim()));servletMapping.put(elementName.getText(),(HspHttpServlet)Class.forName(elementClass.getText().trim()).newInstance());} else if ("servlet-mapping".equals(e.getName())){//这是一个servlet-mappingSystem.out.println("发现 servlet-mapping");Element servletName = e.element("servlet-name");Element urlPatter = e.element("url-pattern");System.out.println("映射名称:\t" + servletName.getText());System.out.println(urlPatter.getText());servletUrlMapping.put(urlPatter.getText(),servletName.getText());}}} catch (Exception e) {throw new Exception(e);}//老韩验证,这两个容器是否初始化成功System.out.println("servletMapping= " + servletMapping);System.out.println("servletUrlMapping= " + servletUrlMapping);}
}
web.XML文件配置
手动实现的 HspTomcatV3
中,它承担了至关重要的作用:描述 Servlet 的类路径与 URL 映射关系,并用于容器启动时通过 DOM4J 解析并完成动态加载。
注意,这里手动实现的XML文件,在web架构下可能不被识别和编译的工作路径下,为此,这里我们考虑直接拷贝.xml文件到target目录。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><!-- com.xulay.servlet.HspCalServlet--><servlet><servlet-name>HspCalServlet</servlet-name><servlet-class>servlet.HspCalServlet</servlet-class></servlet><servlet-mapping><servlet-name>HspCalServlet</servlet-name><url-pattern>/hspCalServlet</url-pattern></servlet-mapping><servlet><servlet-name>XCLcalServlet</servlet-name><servlet-class>servlet.XCLcalServlet</servlet-class></servlet><servlet-mapping><servlet-name>XCLcalServlet</servlet-name><url-pattern>/XCLcalServlet</url-pattern></servlet-mapping></web-app>
项目工具管理Maven配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>myTomCat</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>provided</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version></dependency><dependency><groupId>dom4j</groupId><artifactId>dom4j</artifactId><version>1.1</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.1</version><scope>compile</scope></dependency></dependencies></project>