手搓Tomcat

目錄

Tomcat是什么?

前置工作準備

構建并啟動Tomcat

處理Socket邏輯順序

獲取輸入流并讀取數據封裝到Request

自定義Servlet對象

暫存響應體

按Http協議發送響應數據

部署Tomcat


?


Tomcat是什么?


Tomcat 是一個 Web 應用服務器(準確說是 Servlet 容器JSP 引擎),是目前 Java Web 開發中最常用的中間件之一。

本質:

  • Tomcat 是由 Apache 基金會開發和維護的開源 Web 服務器。

  • 它實現了 ServletJSP 規范,是 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 協議的應用層協議:

  1. 瀏覽器執行 http://localhost:8080/test

  2. 它會向 localhost8080 端口發送一個 TCP 三次握手

  3. 建立連接后,瀏覽器會將 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 規范中,doGetdoPostdoPutdoDelete 等方法是用來處理不同 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 {}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/922180.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/922180.shtml
英文地址,請注明出處:http://en.pswp.cn/news/922180.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux網絡:初識網絡

文章目錄1. 網絡發展1.1 獨立模式1.2 網絡互聯1.3 局域網LAN1.4 廣域網WAN2. 認識 “協議”2.1 什么是協議&#xff1f;2.2 為什么要有協議&#xff1f;2.3 深入了解協議序&#xff1a;開網絡之篇章&#xff0c;建網絡之基礎&#xff0c;將近2月過去&#xff0c;暑假期間不曾有…

文件檢查與拷貝-簡化版

本篇繼續來學習shell腳本&#xff0c;對上一篇的文件檢查與拷貝腳本進行簡化修改。 1 功能說明 在Linux系統中&#xff0c;通過一個shell腳本&#xff0c;實現將一個目錄中的所有文件&#xff08;包括子目錄中的&#xff09;&#xff0c;拷貝到頂一個指定的目錄&#xff0c;要求…

DCA1000 AWR1843 環境安裝

mmWaveStudio GUI設計用于表征和評估TI Radar器械。mmWaveStudio通過SPI向mmWave設備發送命令來配置和控制mmWave設備。使用DCA 1000 EVM或TSW 1400 EVM板捕獲ADC數據&#xff0c;并在Matlab中處理數據&#xff0c;結果顯示在GUI中。mmWaveStudio GUI利用C DLL和一組API通過FTD…

機器學習實操項目01——Numpy入門(基本操作、數組形狀操作、復制與試圖、多種索引技巧、線性代數)

上一章&#xff1a;【從 0 到 1 落地】機器學習實操項目目錄&#xff1a;覆蓋入門到進階&#xff0c;大學生就業 / 競賽必備 下一章&#xff1a; 機器學習核心知識點目錄&#xff1a;機器學習核心知識點目錄 機器學習實戰項目&#xff1a;【從 0 到 1 落地】機器學習實操項目目…

【vscode】如何離線下載vsxi插件,且在無網環境下離線安裝插件-2026最新實驗教程

文章目錄插件市場也可以從APP進入無網環境下安裝插件插件市場 https://marketplace.visualstudio.com/vscode 也可以從APP進入 這里以下載python插件為例 選擇版本 無網環境下安裝插件

vue2 偵聽器watch

一、watch 核心作用監測數據變化&#xff1a;當被監聽的數據發生改變時&#xff0c;自動執行指定的處理函數處理副作用&#xff1a;適合執行異步操作&#xff08;如接口請求&#xff09;、復雜邏輯處理等 “副作用” 代碼二、基礎語法&#xff08;3 種寫法&#xff09;簡單寫法…

今天繼續學習Linux系統中shell腳本

首先繼續上次的內容看一下另一個案例案例&#xff1a;持續檢查服務器負載uptime查看負載情況&#xff08;也可以用top命令&#xff09;[rootlocalhost ~]# uptime22:11:26 up 7:05, 3 users, load average: 0.00, 0.00, 0.00#!/bin/bash #Function:持續檢查服務器負載,如果負…

Win系統下配置PCL庫第一步之下載Visual Studio和Qt 5.15.2(超詳細)

之前在上篇文章Win系統下配置PCL庫_windows pcl庫 下載-CSDN博客中提到配置PCL庫的教程是下載Visual Studio和Qt 5.15.2&#xff0c;后續在測試中我發現前面這兩步很重要&#xff0c;一般Qt在線下載器選項選不好的話Qt是裝的Qt6&#xff0c;在VTK編譯的時候Qt6往往需要C17編譯&…

openCV3.0 C++ 學習筆記補充(自用 代碼+注釋)---持續更新 四(91-)

環境&#xff1a;OpenCV3.2.0 VS201791、合并Y方向重疊的輪廓以輪廓的最小垂直外接矩形框的y為依據&#xff0c;合并y重疊的輪廓。數學邏輯&#xff1a;幾何合并的數學表達坐標系統&#xff1a;假設矩形由左上角坐標(x, y)和寬高(width, height)定義。合并公式&#xff1a;合并…

numpy數組的升維和降維的方法集錦

為適配計算包對numpy數組的維度要求&#xff0c;對numpy數組進行升維或降維轉化&#xff0c;是非常常見的操作。這里嘗試通過多種方式對numpy數組進行升維或降維。1 數組升維1.1 np.expand_dims在0維升維&#xff0c;示例如下a np.array([1,2,3,4,5]) np.expand_dims(a, axis0…

介紹 Python Elasticsearch Client 的 ES|QL 查詢構建器

作者&#xff1a;來自 Elastic Miguel Grinberg 學習如何使用 ES|QL 查詢構建器&#xff0c;這是一個新的 Python Elasticsearch client 功能&#xff0c;可以更輕松地使用熟悉的 Python 語法構建 ES|QL 查詢。 想要獲得 Elastic 認證嗎&#xff1f;快來了解下一期 Elasticsear…

三坐標測量儀:高精度測量內徑檢測手段及其實際運用

在工業制造領域中&#xff0c;內徑尺寸的精準度直接關系到產品的裝配性能、運行穩定性乃至使用壽命。傳統檢測方法如卡尺、內徑千分尺等難以滿足高精度、復雜結構件的需求。三坐標測量儀技術的出現&#xff0c;打破了這一困境&#xff0c;成為當前工業領域實現高精度內徑檢測的…

DIPMARK:一種隱蔽、高效且具備魯棒性的大語言模型水印技術

摘要水印技術為通過在數據中嵌入隱蔽信息來保障數據安全提供了一種很有前景的方法。該領域的一個首要挑戰在于&#xff0c;在水印嵌入過程中保持原始數據的分布。我們的研究拓展并優化了現有的水印框架&#xff0c;著重強調了保持分布&#xff08;DiP&#xff09;水印的重要性。…

IMU傳感器價格與高精度慣性導航系統供應商分析

本段將對IMU傳感器價格及高精度慣性導航系統的市場情況進行概覽。IMU傳感器作為慣性導航的重要組成部分&#xff0c;其價格水平受到技術、需求和供應商競爭等多重因素的影響。隨著無人機、自動駕駛車輛等新興應用場景的興起&#xff0c;IMU傳感器的市場需求逐漸攀升。這不僅帶動…

3-9〔OSCP ? 研記〕? WEB應用攻擊?利用REST API提權

鄭重聲明&#xff1a; 本文所有安全知識與技術&#xff0c;僅用于探討、研究及學習&#xff0c;嚴禁用于違反國家法律法規的非法活動。對于因不當使用相關內容造成的任何損失或法律責任&#xff0c;本人不承擔任何責任。 如需轉載&#xff0c;請注明出處且不得用于商業盈利。 …

UE5 基礎應用 —— 07 - 角色藍圖 簡單使用

目錄 一、角色藍圖 1.1 Pawn / Character 1.2 角色基類 1.3 角色基類設置 1.3.1 基礎設置 1.3.2 角色移動和相機旋轉 1.3.3 角色移動 —— 鎖定視角 1.3.4 角色跳躍 1.4 角色派生類設置 1.4.1 添加動畫藍圖 一、角色藍圖 1.1 Pawn / Character Pawn / Character 有什…

流暢的Python(二) 豐富的序列

流暢的Python 第二章&#xff1a;豐富的序列 摘要&#xff1a;在日常Python開發中&#xff0c;我們頻繁與各種數據結構打交道&#xff0c;其中序列類型&#xff08;如列表、元組、字符串&#xff09;是基石。然而&#xff0c;你是否曾因對它們理解不深&#xff0c;而在性能優化…

嵌入式 - ARM6

一、按鍵1. 初始化key.c手冊C32 - IOMUXC1. 復用功能配置IOMUXC_SW_MUX_CTL_PAD_UART1_CTS_B: 低四位&#xff08;0101&#xff09; IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);SION(信號監控)1: 0 //0 DISABLED — Input Path is determined by functionality MUX_…

菊水PBZ電源在蓄電池充放電測試中的應用探討

通過高速雙極性電源PBZ系列進行蓄電池恒流&#xff0c;恒壓充電的方法 對于儀器廠商來說&#xff0c;要求“請按照使用說明書使用”是產品的使用方針&#xff0c;或者說是正確用法。但是&#xff0c;作為具有代表性的通用產品&#xff0c;直流電源的實際使用方法可謂五花八門&…

Zephyr嵌入式實時操作系統安裝配置

Zephyr簡介 Zephyr 是一款由 Linux 基金會 托管的開源實時操作系統(RTOS),專為資源受限的嵌入式設備(從微控制器到小型邊緣計算節點)設計,廣泛應用于物聯網(IoT)、工業自動化、消費電子、醫療設備、汽車電子等領域。其核心優勢在于輕量級、高可配置性和對多架構硬件的廣…