目录
Tomcat是什么?
Tomcat 是一个 Web 应用服务器 (准确说是 Servlet 容器 和 JSP 引擎),是目前 Java Web 开发中最常用的中间件之一。
本质:
-
Tomcat 是由 Apache 基金会开发和维护的开源 Web 服务器。
-
它实现了 Servlet 和 JSP 规范,是 Java EE 规范的一部分。
-
Tomcat 本身不是完整的 Java EE 应用服务器(如 JBoss、GlassFish),但足以支撑大部分 Web 应用。
核心功能:
-
Socket 监听:在指定端口(默认 8080)监听来自浏览器的 HTTP 请求。
-
请求解析 :解析 HTTP 请求报文,把它封装成
HttpServletRequest
对象。 -
Servlet 管理 :根据 URL 匹配到对应的 Servlet,调用其
service()
方法。 -
响应返回 :将
HttpServletResponse
的内容拼装成完整的 HTTP 响应报文,并写回浏览器。 -
静态资源处理:直接返回 HTML、CSS、JS、图片等静态文件。
假如浏览器访问 http:// localhost:8080/test,随后根据 URL 发送 HTTP 报文给服务器:
GET /test HTTP/1.1
Host: localhost:8080
随后Tomcat通过 ServerSocket 在端口 8080 监听,收到请求后解析出请求方法(GET)、路径(/test)、协议版本(HTTP/1.1)、头部信息等。
之后Tomcat 根据配置找到 /test 对应的 Servlet,然后调用 Servlet 的 service() 方法,service 再根据请求方法选择doGet()或doPost()等方法。在 Servlet 中执行业务逻辑,例如查询数据库、处理数据等。
最后Servlet 使用 HttpServletResponse 设置响应头、状态码、响应体内容。Tomcat 将这些内容封装成完整的 HTTP 响应报文:
html
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Length: 12
hello Eleven
浏览器收到响应后渲染页面。
所以实际上 Tomcat 是前端和后端之间的"桥梁",它把低层的 TCP/HTTP 通信细节封装起来,让开发者只需要处理业务逻辑。
而我们想手写一个Tomcat主要是去尝试使用Socket处理Http协议,之后再模拟 Servlet 调用流程。
前置工作准备
首先我们引入 javax.servlet-api 依赖:
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tomcat.com</groupId>
<artifactId>tomcat-eleven</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tomcat-eleven</name>
<description>tomcat-eleven</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
之后我们先构造一个Tomcat启动类,然后创建一个start()方法用来启动Tomcat:
java
package cn.tomcat.com;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TomcatElevenApplication {
/**
* 启动
*/
public void start(){
}
public static void main(String[] args) {
TomcatElevenApplication tomcatElevenApplication = new TomcatElevenApplication();
tomcatElevenApplication.start();
}
}
构建并启动Tomcat
首先我们需要知道,浏览器向服务发起 HTTP 请求时,实际上是通过 TCP 建立连接,另外 http 协议的本质上是基于 TCP 协议的应用层协议:
-
浏览器执行
http://localhost:8080/test
-
它会向
localhost
的8080
端口发送一个 TCP 三次握手 -
建立连接后,浏览器会将 HTTP 报文(如
GET /index.html HTTP/1.1
)通过 TCP 流发送过去。
而 Socket 是 Java 与底层 TCP 网络通信的接口,所以我们首先去启用 Socket 去监听,而 serverSocket.accept() 方法是阻塞方法,直到有连接到来才会继续执行。
然后为了保证主线程执行结束扔可以继续接受请求,我们使用while循环不断地去监听请求并处理,之后从线程池获取线程来处理socket的方法,所以这里我们去新建一个SocketProcesser类来去封装线程任务,以便交给线程池或新线程来执行,所以这个类就需要去引入Runnable。
首先完善启动Tomcat方法:
java
/**
* 线程池
*/
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
/**
* 启动
*/
public void start(){
try {
// socket 连接 TCP
ServerSocket serverSocket = new ServerSocket(8080);
while(true){
// 监听
Socket socket = serverSocket.accept();
// 处理 socket
executorService.execute(() -> new SocketProcesser(socket).run());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
然后我们创建SocketProcesser类引入Runnable:
java
package cn.tomcat.com;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* 处理 socket
*/
public class SocketProcesser implements Runnable {
private Socket socket;
public SocketProcesser(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
processSocket(socket);
}
/**
* 处理 socket
* @param socket
*/
private void processSocket(Socket socket) {
// 处理逻辑...
}
}
而在processSocket方法内就可以添加对于Socket的处理逻辑了。
处理Socket逻辑顺序
逻辑顺序:
从 Socket 读取客户端请求 → 解析 HTTP 报文 → 封装为 Request/Response → 调用 Servlet → 返回响应
获取输入流并读取数据封装到Request
首先我们调用 socket.getInputStream() 从 TCP 连接 中获取输入流,准备接收浏览器发送的 HTTP 请求数据,之后创建字节数组 byte[] bytes = new byte[1024] 存储数据并使用 inputStream.read(bytes) 方法阻塞式读取数据,返回读到的字节数:
java
try (InputStream inputStream = socket.getInputStream()) {
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
if (read <= 0) return;
// ...
}
之后将byte数据转换为字符串:
java
// 转成字符串
String requestText = new String(bytes, 0, read);
System.out.println("原始请求:\n" + requestText);
现在我们可以去浏览器访问 http://localhost:8080/test 地址来查看后端打印:

下面是Http协议结构:


可以发现在前面有++请求方法+空格+URL地址+空格+协议版本++,所以我们可以将这些数据封装到Request对象中:
java
package cn.tomcat.com;
import javax.servlet.http.HttpServletRequest;
import java.io.OutputStream;
import java.net.Socket;
public class Request {
private String method; // 请求方法
private String url; // 请求路径
private String protocol; // 请求协议
private Socket socket; // socket连接
public Request(String method, String url, String protocol, Socket socket) {
this.method = method;
this.url = url;
this.protocol = protocol;
this.socket = socket;
}
// Getter And Setter ...
}
之后我们将解析出来并封装Request:
第一行是请求行:
javaGET /test HTTP/1.1
用空格拆分:
parts[0] = "GET"
→ 请求方法。
parts[1] = "/test"
→ 请求路径(URL)。
parts[2] = "HTTP/1.1"
→ 协议版本。
java
// 按行拆分,第一行是请求行
String[] lines = requestText.split("\r\n");
if (lines.length > 0) {
String requestLine = lines[0]; // 例如: GET /test HTTP/1.1
String[] parts = requestLine.split(" ");
if (parts.length >= 3) {
String method = parts[0]; // GET
String url = parts[1]; // /test
String protocol = parts[2]; // HTTP/1.1
// 封装到 Request 对象
Request request = new Request(method, url, protocol, socket);
}
}
自定义Servlet对象
Tomcat底层是使用HttpServlet,而内部实现了service(),doGet(),doPost()等方法。而在 Servlet 规范中,doGet
、doPost
、doPut
、doDelete
等方法是用来处理不同 HTTP 请求方法 的回调方法。它们是 HttpServlet
类提供的钩子方法,当 Tomcat 收到特定类型的 HTTP 请求时,会调用这些方法,让开发者在其中编写自己的业务逻辑。
在底层,Tomcat 调用service()方法传入 Request + Response,其方法内部根据请求方法判断到底是去使用doGet还是doPost等方法:

所以我们来自定义一个 Servlet 对象去继承 HttpServlet 来实现这些方法:
service方法可以不用重构,我们先以doGet方法重写为例。
java
package cn.tomcat.com;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet // 在不编写 web.xml 的情况下注册 Servlet
public class ElevenServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println(req.getMethod());
// 需要先告诉浏览器一下响应体多少个字节
resp.addHeader("Content-Length", "12");
resp.addHeader("Content-Type", "text/html;charset=utf-8");
// 响应数据
resp.getOutputStream().write("hello Eleven".getBytes());
}
}
而原本的Service方法需要我们去传递ServletRequest与ServletResponse:

所以我们的request与response需要分别去实现HttpServletRequest与HttpServletResponse,这里我们不想全重写了,就直接通过抽象类来实现方法,之后request与response分别去继承抽象类就OK了:
java
package cn.tomcat.com;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.*;
public class AbstractHttpServletRequest implements HttpServletRequest {
// 省略一堆的重写方法 ...
}
java
package cn.tomcat.com;
import java.net.Socket;
public class Request extends AbstractHttpServletRequest {
private String method; // 请求方法
private String url; // 请求路径
private String protocol; // 请求协议
private Socket socket; // 客户端 socket
public Request(String method, String url, String protocol, Socket socket) {
this.method = method;
this.url = url;
this.protocol = protocol;
this.socket = socket;
}
// GETTER AND SETTER
// 这里强调HttpServletRequest实现的是StringBuffer getRequestURL()方法
// 所以我们需要更改回去请求路径方法
// 其他的也同理需要修改
public StringBuffer getRequestURL() {
return new StringBuffer(url);
}
// ...
}
response对象也同理:
java
package cn.tomcat.com;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
public class AbstractHttpServletResponse implements HttpServletResponse {
// 省略一堆的重写方法 ...
}
而响应信息主要有 响应状态码 + 状态描述信息 + 响应头headers,另外一个请求对应一个响应,所以在添加一个Request属性 :
java
package cn.tomcat.com;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
public class Response extends AbstractHttpServletResponse {
private int status = 200;
private String msg = "OK";
private Map<String,String> headers = new HashMap<>();
private Request request;
public Response(Request request) throws IOException {
this.request = request;
this.socketOutputStream = request.getSocket().getOutputStream();
}
@Override
public void setStatus(int i, String s) {
this.status = i;
this.msg = s;
}
@Override
public int getStatus() {
return status;
}
public String getMsg() {
return msg;
}
@Override
public void addHeader(String s, String s1) {
headers.put(s, s1);
}
}
这回我们就可以正常去使用service方法了:
java
package cn.tomcat.com;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* 处理 socket
*/
public class SocketProcesser implements Runnable {
private Socket socket;
public SocketProcesser(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
processSocket(socket);
}
/**
* 处理 socket
* @param socket
*/
private void processSocket(Socket socket) {
try (InputStream inputStream = socket.getInputStream()) {
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
if (read <= 0) return;
// 转成字符串
String requestText = new String(bytes, 0, read);
System.out.println("原始请求:\n" + requestText);
// 按行拆分,第一行是请求行
String[] lines = requestText.split("\r\n");
if (lines.length > 0) {
String requestLine = lines[0]; // 例如: GET /test HTTP/1.1
String[] parts = requestLine.split(" ");
if (parts.length >= 3) {
String method = parts[0]; // GET
String url = parts[1]; // /test
String protocol = parts[2]; // HTTP/1.1
// 封装到 Request 对象
Request request = new Request(method, url, protocol, socket);
// 封装到 Response 对象
Response response = new Response(request);
// 匹配Servlet
ElevenServlet servlet = new ElevenServlet();
// 调用Servlet的service方法,帮助我们判断到底要调用doGet还是doPost等方法
servlet.service(request, response);
// TODO 发送响应数据
}
}
} catch (IOException e) {
// 也需要构造一个Response去返回异常提示
throw new RuntimeException(e);
} catch (ServletException e) {
throw new RuntimeException(e);
}
}
}
但是我们发现运行后会产生空指针异常,这是因为我们将HttpServletResponse内部方法重写,导致我们在doGet方法内部调用的 resp.getOutputStream() 方法没有重写,而该方法表示的意思的将二进制数据写入 HTTP 响应体,并发送给客户端,所以接下来我们需要完善该方法。
暂存响应体
查看我们抽象类可以发现,这个方法返回了ServletOutputStream对象:

而ServletOutputStream是个抽象类,所以我们也肯定要自己去重写一个ServletOutputStream:

而write()方法的实现如下:

所以我们应先去重写write()方法,为了让doGet全部执行结束判断是否异常之后在调用write方法,我们需要将这个响应体存储,:
java
package cn.tomcat.com;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import java.io.IOException;
public class ResponseServletOutputStream extends ServletOutputStream {
private byte[] bytes = new byte[1024]; // 缓冲区
private int pos = 0; // 缓冲区的位置
@Override
public void write(int b) throws IOException {
bytes[pos] = (byte) b;
pos++;
}
public byte[] getBytes() {
return bytes;
}
public int getPos() {
return pos;
}
}
之后重写方法:
java
package cn.tomcat.com;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
public class Response extends AbstractHttpServletResponse {
// ...
@Override
public ResponseServletOutputStream getOutputStream() throws IOException {
return responseServletOutputStream;
}
}
随后就该去执行发送响应码了。
按Http协议发送响应数据
我们发送响应数据是通过Complete方法,所以需要重写:
java
package cn.tomcat.com;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
public class Response extends AbstractHttpServletResponse {
// ...
/**
* 完成响应
*/
public void complete() throws IOException {
sendResponseLine();
sendResponseHeaders();
sendResponseBody();
}
}
在这里面我们先定义三个方法来按照Http协议规范一次发送响应行、响应头、响应体。

而发送,我们还需要使用socket对象:
java
package cn.tomcat.com;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
public class Response extends AbstractHttpServletResponse {
private int status = 200;
private String msg = "OK";
private Map<String,String> headers = new HashMap<>();
private Request request;
private OutputStream socketOutputStream;
private ResponseServletOutputStream responseServletOutputStream = new ResponseServletOutputStream();
public Response(Request request) throws IOException {
this.request = request;
this.socketOutputStream = request.getSocket().getOutputStream();
}
}
那么接下来我们就尝试写发送响应行:
响应行格式: HTTP/1.1 + ' ' + 200 + ' ' + OK
java
public class Response extends AbstractHttpServletResponse {
private byte SP = ' '; // 空格
private byte CR = '\r'; // 回车
private byte LF = '\n'; // 换行
// ...
/**
* 发送响应行
*/
private void sendResponseLine() throws IOException {
socketOutputStream.write(request.getProtocol().getBytes());
socketOutputStream.write(SP);
socketOutputStream.write(status);
socketOutputStream.write(SP);
socketOutputStream.write(msg.getBytes());
socketOutputStream.write(CR);
socketOutputStream.write(LF);
}
}
发送响应头:
HTTP 协议规定:
javaContent-Type: text/html;charset=utf-8 Content-Length: 123 自定义头: 值
每个响应头占一行,格式为
键: 值
,行尾以\r\n
结束。响应头结束后,还需要再写入一个空行(即仅包含
\r\n
),表示头部部分结束,后面就是响应体。
java
private void sendResponseHeaders() throws IOException {
if(!headers.containsKey("Content-Length")) {
addHeader("Content-Length", String.valueOf(getOutputStream().getPos()));
}
if(!headers.containsKey("Content-Type")) {
addHeader("Content-Type", "text/html;charset=utf-8");
}
for (Map.Entry<String,String> entry : headers.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
socketOutputStream.write(key.getBytes()); // 写入键
ocketOutputStream.write(":".getBytes()); // 写入:
socketOutputStream.write(value.getBytes());// 写入值
socketOutputStream.write(CR); // 回车
socketOutputStream.write(LF); // 换行
}
// 头部结束后,再写一个空行
socketOutputStream.write(CR);
socketOutputStream.write(LF);
}
发送响应体:
而发送响应体就直接使用write方法传递:
java
private OutputStream socketOutputStream;
private ResponseServletOutputStream responseServletOutputStream = new ResponseServletOutputStream(); // 响应体
@Override
public ResponseServletOutputStream getOutputStream() throws IOException {
return responseServletOutputStream;
}
/**
* 发送响应体
*/
private void sendResponseBody() throws IOException {
socketOutputStream.write(getOutputStream().getBytes());
}
完整代码:
java
package cn.tomcat.com;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
public class Response extends AbstractHttpServletResponse {
private byte SP = ' '; // 空格
private byte CR = '\r'; // 回车
private byte LF = '\n'; // 换行
private int status = 200;
private String msg = "OK";
private Map<String,String> headers = new HashMap<>();
private Request request;
private OutputStream socketOutputStream;
private ResponseServletOutputStream responseServletOutputStream = new ResponseServletOutputStream(); // 响应体
public Response(Request request) throws IOException {
this.request = request;
this.socketOutputStream = request.getSocket().getOutputStream();
}
@Override
public void setStatus(int i, String s) {
this.status = i;
this.msg = s;
}
@Override
public int getStatus() {
return status;
}
public String getMsg() {
return msg;
}
@Override
public void addHeader(String s, String s1) {
headers.put(s, s1);
}
@Override
public ResponseServletOutputStream getOutputStream() throws IOException {
return responseServletOutputStream;
}
/**
* 完成响应
*/
public void complete() throws IOException {
sendResponseLine();
sendResponseHeaders();
sendResponseBody();
}
/**
* 发送响应体
*/
private void sendResponseBody() throws IOException {
socketOutputStream.write(getOutputStream().getBytes());
}
/**
* 发送响应头
*/
private void sendResponseHeaders() throws IOException {
if(!headers.containsKey("Content-Length")) {
addHeader("Content-Length", String.valueOf(getOutputStream().getPos()));
}
if(!headers.containsKey("Content-Type")) {
addHeader("Content-Type", "text/html;charset=utf-8");
}
for (Map.Entry<String,String> entry : headers.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
socketOutputStream.write(key.getBytes());
socketOutputStream.write(":".getBytes());
socketOutputStream.write(value.getBytes());
socketOutputStream.write(CR);
socketOutputStream.write(LF);
}
socketOutputStream.write(CR);
socketOutputStream.write(LF);
}
/**
* 发送响应行
*/
private void sendResponseLine() throws IOException {
socketOutputStream.write(request.getProtocol().getBytes());
socketOutputStream.write(SP);
socketOutputStream.write(status);
socketOutputStream.write(SP);
socketOutputStream.write(msg.getBytes());
socketOutputStream.write(CR);
socketOutputStream.write(LF);
}
}
最后调用complete方法:
java
package cn.tomcat.com;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
* 处理 socket
*/
public class SocketProcesser implements Runnable {
private Socket socket;
public SocketProcesser(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
processSocket(socket);
}
/**
* 处理 socket
* @param socket
*/
private void processSocket(Socket socket) {
try (InputStream inputStream = socket.getInputStream()) {
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
if (read <= 0) return;
// 转成字符串
String requestText = new String(bytes, 0, read);
System.out.println("原始请求:\n" + requestText);
// 按行拆分,第一行是请求行
String[] lines = requestText.split("\r\n");
if (lines.length > 0) {
String requestLine = lines[0]; // 例如: GET /test HTTP/1.1
String[] parts = requestLine.split(" ");
if (parts.length >= 3) {
String method = parts[0]; // GET
String url = parts[1]; // /test
String protocol = parts[2]; // HTTP/1.1
// 封装到 Request 对象
Request request = new Request(method, url, protocol, socket);
// 打印封装结果
System.out.println("方法: " + request.getMethod());
System.out.println("路径: " + request.getRequestURL());
System.out.println("协议: " + request.getProtocol());
// 封装到 Response 对象
Response response = new Response(request);
// 匹配Servlet
ElevenServlet servlet = new ElevenServlet();
// 调用Servlet的service方法,帮助我们判断到底要调用doGet还是doPost等方法
servlet.service(request, response);
// 发送响应数据
response.complete();
}
}
} catch (IOException e) {
// 也需要构造一个Response去返回异常提示
throw new RuntimeException(e);
} catch (ServletException e) {
throw new RuntimeException(e);
}
}
}
部署Tomcat
用过Tomcat的知道,Tomcat 的 webapps/ 目录是部署入口,所以我们先建立一个webapps目录:

这个hello就相当于一个项目,或者也可以称作一个Jar包,而在classes下就可以放入一些类,而tomcat关心的是这些项目或者类中哪里有servlet,然后根据servlet去匹配方法处理请求。
首先将我们的ElevenServlet.class文件放在classes/eleven/ElevenServlet.class下来伪造一个Servlet,之后将原本的ElevenServlet删除。那么现在就相当于我在tomcat下面部署了一个hello应用,而这个应用下面还有ElevenServlet,而在tomcat启动前首先需要完成部署App:
java
public class TomcatElevenApplication {
// ...
public static void main(String[] args) {
TomcatElevenApplication tomcatElevenApplication = new TomcatElevenApplication();
tomcatElevenApplication.deployApps(); // 部署APP
tomcatElevenApplication.start();
}
}
如何实现该方法呢?
首先肯定需要找到tomcat下有哪些应用,先拿到webApps文件夹,然后遍历内部应用,随后准备使用deployApp来比那里应用内的所有类:
java
/**
* 遍历webapps目录
*/
private void deployApps() {
File webApps = new File(System.getProperty("user.dir"), "/webapps");
if(webApps.exists()){
for(String app : webApps.list()){
deployApp(webApps, app);
}
}
}
之后编写deployApp方法,主要目的是判断当前应用下有哪些类继承了HttpServlet,然后在该类拿到@WebServlet注解值,并存储起来方便处理Socket时使用。
我们先创建存储类:
java
package cn.tomcat.com;
import javax.servlet.Servlet;
import java.util.HashMap;
import java.util.Map;
/**
* 应用上下文
*/
public class Context {
/**
* 应用名称
*/
private String appName;
/**
* url 映射
*/
private Map<String, Servlet> urlPatternMap = new HashMap<String, Servlet>();
public Context(String appName) {
this.appName = appName;
}
/**
* 添加servlet
* @param urlPattern
* @param servlet
*/
public void addServlet(String urlPattern, Servlet servlet) {
urlPatternMap.put(urlPattern, servlet);
}
/**
* 根据url获取servlet
* @param urlPattern
* @return
*/
public Servlet getByUrlPattern(String urlPattern) {
for (String key : urlPatternMap.keySet()) {
if (urlPattern.contains(key)) {
return urlPatternMap.get(key);
}
}
return null;
}
}
之后按照上面逻辑实现查找:
注意,加载类的时候要使用自定义类加载器,否则因为目录不在同一个,扫描不到classes:
javapackage cn.tomcat.com; import java.net.URL; import java.net.URLClassLoader; /** * 自定义类加载器 */ public class WebappClassLoader extends URLClassLoader { public WebappClassLoader(URL[] urls) { super(urls); } }
java
/**
* 保存Tomcat有哪些应用
*/
private Map<String, Context> contextMap = new HashMap<>();
/**
* 遍历当前应用内所有类中是否有继承HttpServlet的,
* 如果有,就将它添加到应用上下文
* @param webApps
* @param appName
*/
private void deployApp(File webApps, String appName) {
Context context = new Context(appName);
// 当前应用下面有哪些Servlet
File appDirectory = new File(webApps, appName); // hello文件夹
File classesDirectory = new File(appDirectory, "classes"); // classes文件夹
List<File> allFilePath = getAllFilePath(classesDirectory);
for (File file : allFilePath) {
if(file.getName().endsWith(".class")){
// 是类文件
// 思路:加载为Class对象,随后用反射判断是否继承了HttpServlet
// 转换类加载格式
String name = file.getPath();
name = name.replace(classesDirectory.getPath() + "\\ ", "/");
name = name.replace(".class", "");
name = name.replace("\\", "/");
// 类加载器加载类
try {
// 这样是加载不到的,因为应用不在这个cn.tomcat.com下
// Class<?> servletClass = Thread.currentThread().getContextClassLoader().loadClass(name);
// 使用自定义的类加载器加载类,让它去加载classes目录
WebappClassLoader webappClassLoader = new WebappClassLoader(new URL[]{classesDirectory.toURI().toURL()});
Class<?> servletClass = webappClassLoader.loadClass(name);
// 判断是否继承了HttpServlet
if(HttpServlet.class.isAssignableFrom(servletClass)){
// 是HttpServlet的子类
System.out.println("发现Servlet:" + name);
// 解析URL对应的匹配规则
if(servletClass.isAnnotationPresent(javax.servlet.annotation.WebServlet.class)){
// 获取注解value值
WebServlet webServlet = servletClass.getAnnotation(WebServlet.class);
String[] urlPatterns = webServlet.urlPatterns();
// 存储到上下文
for (String urlPattern : urlPatterns) {
System.out.println("发现URL:" + urlPattern);
// 存储到Map中
context.addServlet(urlPattern, (Servlet) servletClass.newInstance());
}
}
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
// 部署完成,保存应用映射
contextMap.put(appName, context);
}
最后我们去完善SocketProcesser内处理Socket方法:
西药修改的是我们的匹配Servlet,需要通过刚刚在TomcatElevenApplication保存到tomcat的map来根据url找到对应的Servlet。
java
/**
* 处理 socket
* @param socket
*/
private void processSocket(Socket socket) {
try (InputStream inputStream = socket.getInputStream()) {
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
if (read <= 0) return;
// 转成字符串
String requestText = new String(bytes, 0, read);
System.out.println("原始请求:\n" + requestText);
// 按行拆分,第一行是请求行
String[] lines = requestText.split("\r\n");
if (lines.length > 0) {
String requestLine = lines[0]; // 例如: GET /test HTTP/1.1
String[] parts = requestLine.split(" ");
if (parts.length >= 3) {
String method = parts[0]; // GET
String url = parts[1]; // /test
String protocol = parts[2]; // HTTP/1.1
// 封装到 Request 对象
Request request = new Request(method, url, protocol, socket);
// 封装到 Response 对象
Response response = new Response(request);
// // 匹配Servlet
// ElevenServlet servlet = new ElevenServlet();
// // 调用Servlet的service方法,帮助我们判断到底要调用doGet还是doPost等方法
// servlet.service(request, response);
// 判断请求是想访问哪些应用
String requestUrl = request.getRequestURL().toString();
// 例如: http://localhost:8080/test
// 我们要获取 /test 这部分
String contextPath = requestUrl.substring(requestUrl.indexOf("/", 7), requestUrl.indexOf(":", 7));
// 从应用中获取 Servlet
Context context = tomcatElevenApplication.getContextMap().get(contextPath);
Servlet servlet = context.getByUrlPattern(url);
if (servlet != null) {
servlet.service(request, response);
// 发送响应数据
response.complete();
} else {
new DefaultServlet().service(request, response);
// 404
response.setStatus(404, "Not Found");
response.complete();
}
}
}
} catch (IOException e) {
// 也需要构造一个Response去返回异常提示
throw new RuntimeException(e);
} catch (ServletException e) {
throw new RuntimeException(e);
}
}
这里为了让没找到也有对应的Servlet,我们设置一个默认的Servlet:
java
package cn.tomcat.com;
import javax.servlet.http.HttpServlet;
public class DefaultServlet extends HttpServlet {
}