在日常 Java Web 开发中,Tomcat 无疑是我们最常接触的 Web 服务器之一。它默默承担了请求解析、Servlet 加载、响应输出等一系列复杂的任务,让开发者可以专注于业务逻辑的实现。然而,你是否曾经好奇:Tomcat 是如何接收浏览器请求并将其交给 Servlet 处理的?Servlet 是在什么时候被加载和执行的?
与其只停留在"使用层面",不如亲手拆解并实现一遍核心机制。本篇博客将带你从零开始动手实现一个简易版的 Servlet 容器,通过手写 socket 通信、URL 映射、Servlet 管理等模块,深入理解 Tomcat 的底层架构与工作原理。
项目技术栈
Maven(项目工具包管理)、网络通信(Socket网络编程)、HTTP协议、IO流、XML解析(DOM4j)、反射机制和servlet规范、BIO多线程模型、模板设计模式。
项目系统框图

系统整体架构代码实现
请求封装模块
将底层 Socket 输入流中的原始 HTTP 请求报文解析成结构化的数据,供 Servlet 调用。
java
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;
// 存放参数列表 参数名-参数值 =》Hashmap
private 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=30
String 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) {
// 放入hashmap
parametersMapping.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;
}
@Override
public String toString() {
return "HspRequest{" +
"method='" + method + '\'' +
", uri='" + uri + '\'' +
", parametersMapping=" + parametersMapping +
", inputStream=" + inputStream +
'}';
}
}
服务端响应模块
将业务 Servlet 生成的响应内容,通过标准的 HTTP 响应格式写入 Socket 输出流,返回给浏览器客户端。
java
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 应该具备的核心生命周期方法:
java
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 容器中起到了业务调度核心枢纽的作用
java
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 {
@Override
public 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 响应返回给浏览器
java
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 {
@Override
public 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);
}
}
@Override
public void doPost(HspRequest request, HspResponse response) {
doGet(request, response);
}
@Override
public void init() throws Exception {
}
@Override
public void destroy() {
}
}
自定义 Servlet 容器核心类
实现servlet与多线程关联,同时实现xml的动态类加载(封装+继承+多态+io+dom4j(xml配置文件读取))
-
请求分发与多线程处理(基于 Socket)
-
通过解析 XML 配置,实现 Servlet 的类路径注册与动态加载(基于反射)
java
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解析得到类的全路径
// 定义映射类hashmap
public 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配置
// 使用反射机制将该实例放入servletMapping
System.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-mapping
System.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
<?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
<?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>