手搓Tomcat
目录
Tomcat是什么?
前置工作准备
构建并启动Tomcat
处理Socket逻辑顺序
获取输入流并读取数据封装到Request
自定义Servlet对象
暂存响应体
按Http协议发送响应数据
部署Tomcat
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 响应报文:
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Length: 12hello Eleven
浏览器收到响应后渲染页面。
所以实际上 Tomcat 是前端和后端之间的“桥梁”,它把低层的 TCP/HTTP 通信细节封装起来,让开发者只需要处理业务逻辑。
而我们想手写一个Tomcat主要是去尝试使用Socket处理Http协议,之后再模拟 Servlet 调用流程。
前置工作准备
首先我们引入 javax.servlet-api 依赖:
<?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:
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方法:
/*** 线程池*/
private final ExecutorService executorService = Executors.newFixedThreadPool(10); /*** 启动*/
public void start(){try {// socket 连接 TCPServerSocket serverSocket = new ServerSocket(8080);while(true){// 监听Socket socket = serverSocket.accept();// 处理 socketexecutorService.execute(() -> new SocketProcesser(socket).run());}} catch (IOException e) {throw new RuntimeException(e);}
}
然后我们创建SocketProcesser类引入Runnable:
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;}@Overridepublic 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) 方法阻塞式读取数据,返回读到的字节数:
try (InputStream inputStream = socket.getInputStream()) {byte[] bytes = new byte[1024];int read = inputStream.read(bytes);if (read <= 0) return;// ...
}
之后将byte数据转换为字符串:
// 转成字符串
String requestText = new String(bytes, 0, read);
System.out.println("原始请求:\n" + requestText);
现在我们可以去浏览器访问 http://localhost:8080/test 地址来查看后端打印:
下面是Http协议结构:
可以发现在前面有请求方法+空格+URL地址+空格+协议版本,所以我们可以将这些数据封装到Request对象中:
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:
第一行是请求行:
GET /test HTTP/1.1
用空格拆分:
parts[0] = "GET"
→ 请求方法。
parts[1] = "/test"
→ 请求路径(URL)。
parts[2] = "HTTP/1.1"
→ 协议版本。
// 按行拆分,第一行是请求行
String[] lines = requestText.split("\r\n");
if (lines.length > 0) {String requestLine = lines[0]; // 例如: GET /test HTTP/1.1String[] parts = requestLine.split(" ");if (parts.length >= 3) {String method = parts[0]; // GETString url = parts[1]; // /testString 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方法重写为例。
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 {@Overrideprotected 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了:
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 {// 省略一堆的重写方法 ...
}
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; // 客户端 socketpublic 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对象也同理:
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属性 :
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();}@Overridepublic void setStatus(int i, String s) {this.status = i;this.msg = s;}@Overridepublic int getStatus() {return status;}public String getMsg() {return msg;}@Overridepublic void addHeader(String s, String s1) {headers.put(s, s1);}
}
这回我们就可以正常去使用service方法了:
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;}@Overridepublic 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.1String[] parts = requestLine.split(" ");if (parts.length >= 3) {String method = parts[0]; // GETString url = parts[1]; // /testString protocol = parts[2]; // HTTP/1.1// 封装到 Request 对象Request request = new Request(method, url, protocol, socket);// 封装到 Response 对象Response response = new Response(request);// 匹配ServletElevenServlet 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方法,我们需要将这个响应体存储,:
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; // 缓冲区的位置@Overridepublic void write(int b) throws IOException {bytes[pos] = (byte) b;pos++;}public byte[] getBytes() {return bytes;}public int getPos() {return pos;}
}
之后重写方法:
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 {// ...@Overridepublic ResponseServletOutputStream getOutputStream() throws IOException {return responseServletOutputStream;}}
随后就该去执行发送响应码了。
按Http协议发送响应数据
我们发送响应数据是通过Complete方法,所以需要重写:
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对象:
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
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 协议规定:
Content-Type: text/html;charset=utf-8 Content-Length: 123 自定义头: 值
每个响应头占一行,格式为
键: 值
,行尾以\r\n
结束。
响应头结束后,还需要再写入一个空行(即仅包含\r\n
),表示头部部分结束,后面就是响应体。
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方法传递:
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());
}
完整代码:
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();}@Overridepublic void setStatus(int i, String s) {this.status = i;this.msg = s;}@Overridepublic int getStatus() {return status;}public String getMsg() {return msg;}@Overridepublic void addHeader(String s, String s1) {headers.put(s, s1);}@Overridepublic 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方法:
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;}@Overridepublic 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.1String[] parts = requestLine.split(" ");if (parts.length >= 3) {String method = parts[0]; // GETString url = parts[1]; // /testString 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);// 匹配ServletElevenServlet 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:
public class TomcatElevenApplication {// ...public static void main(String[] args) {TomcatElevenApplication tomcatElevenApplication = new TomcatElevenApplication();tomcatElevenApplication.deployApps(); // 部署APPtomcatElevenApplication.start();}
}
如何实现该方法呢?
首先肯定需要找到tomcat下有哪些应用,先拿到webApps文件夹,然后遍历内部应用,随后准备使用deployApp来比那里应用内的所有类:
/*** 遍历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时使用。
我们先创建存储类:
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:
package cn.tomcat.com;import java.net.URL; import java.net.URLClassLoader;/*** 自定义类加载器*/ public class WebappClassLoader extends URLClassLoader {public WebappClassLoader(URL[] urls) {super(urls);} }
/*** 保存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);// 当前应用下面有哪些ServletFile 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);// 判断是否继承了HttpServletif(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。
/*** 处理 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.1String[] parts = requestLine.split(" ");if (parts.length >= 3) {String method = parts[0]; // GETString url = parts[1]; // /testString 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));// 从应用中获取 ServletContext 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);// 404response.setStatus(404, "Not Found");response.complete();}}}} catch (IOException e) {// 也需要构造一个Response去返回异常提示throw new RuntimeException(e);} catch (ServletException e) {throw new RuntimeException(e);}
}
这里为了让没找到也有对应的Servlet,我们设置一个默认的Servlet:
package cn.tomcat.com;import javax.servlet.http.HttpServlet;public class DefaultServlet extends HttpServlet {}