目錄
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 {}