本章将实现对 HTTP Session 和 Cookie 的支持，以便维护客户端的会话状态，使服务器能够识别每次请求是否来自同一客户端并跟踪其状态。我们将通过实现一个计数器 Servlet，来记录每个客户端的访问次数，从而验证会话管理功能的有效性。6.1 功能目标实现会话管理：通过 HttpSession 接口支持，为每个客户端分配唯一的 Session ID，并在服务器端保持会话状态。支持 Cookie：实现对请求中 Cookie 的解析，提取相关信息，并能够在响应中设置新的 Cookie 信息（如 JSESSIONID）。6.2 代码结构更新后的 MiniTomcat 项目结构如下。新增了 CustomHttpSession、SessionManager、HttpRequestParser 等相关类，以及用于测试的 CounterServlet 示例。MiniTomcat ├─ src │ ├─ main │ │ ├─ java │ │ │ ├─ com.daicy.minitomcat │ │ │ │ ├─ servlet │ │ │ │ │ ├─ CustomServletOutputStream.java // 自定义的 Servlet 输出流类 │ │ │ │ │ ├─ CustomHttpSession.java // 自定义的 HttpSession 实现 │ │ │ │ │ ├─ HttpServletRequestImpl.java // HTTP 请求的实现类 │ │ │ │ │ ├─ HttpServletResponseImpl.java // HTTP 响应的实现类 │ │ │ │ │ ├─ ServletConfigImpl.java // Servlet 配置的实现类 │ │ │ │ │ ├─ ServletContextImpl.java // Servlet 上下文的实现类 │ │ │ │ ├─ CounterServlet.java // Session 功能 Servlet 示例类 │ │ │ │ ├─ HelloServlet.java // Servlet 示例类 │ │ │ │ ├─ HttpConnector.java // 连接器类 │ │ │ │ ├─ HttpProcessor.java // 请求处理器 │ │ │ │ ├─ HttpServer.java // 主服务器类 │ │ │ │ ├─ HttpRequestParser.java // HttpRequest 信息解析类 │ │ │ │ ├─ ServletLoader.java // Servlet 加载器 │ │ │ │ ├─ ServletProcessor.java // Servlet 处理器 │ │ │ │ ├─ StaticResourceProcessor.java // 静态资源处理器 │ │ │ │ ├─ SessionManager.java // Session 管理器 │ │ │ │ ├─ WebXmlServletContainer.java // Servlet 容器相关类 │ │ ├─ resources │ │ │ ├─ webroot │ │ │ │ ├─ index.html │ │ │ ├─ web.xml │ ├─ test ├─ pom.xml6.3 代码实现6.3.1 创建 CustomHttpSession 类CustomHttpSession 类负责管理每个客户端的会话数据，并为每个会话分配唯一的 Session ID。该类实现了 javax.servlet.http.HttpSession 接口的核心功能。package com.daicy.minitomcat.servlet; import javax.servlet.ServletContext; import javax.servlet.http.HttpSession; import java.util.*; // 自定义类模拟实现 HttpSession 接口的部分功能 public class CustomHttpSession implements HttpSession { private String id; private Date creationTime; private Date lastAccessedTime; private int maxInactiveInterval; private Map<String, Object> attributes = new HashMap<>(); public CustomHttpSession(String sessionId) { this.id = sessionId; this.creationTime = new Date(); this.lastAccessedTime = new Date(); this.maxInactiveInterval = 1800; // 设置默认的会话超时时间为 30 分钟（单位：秒） } @Override public String getId() { return id; } @Override public long getCreationTime() { return creationTime.getTime(); } @Override public long getLastAccessedTime() { return lastAccessedTime.getTime(); } @Override public ServletContext getServletContext() { return null; } @Override public void setMaxInactiveInterval(int interval) { this.maxInactiveInterval = interval; } @Override public int getMaxInactiveInterval() { return maxInactiveInterval; } @Override public javax.servlet.http.HttpSessionContext getSessionContext() { // 在 Servlet 3.1 之后，HttpSessionContext 接口已被废弃，这里返回 null return null; } @Override public Object getAttribute(String name) { return attributes.get(name); } @Override public Object getValue(String name) { return null; } @Override public Enumeration<String> getAttributeNames() { return new Enumeration<String>() { private final Iterator<String> iterator = attributes.keySet().iterator(); @Override public boolean hasMoreElements() { return iterator.hasNext(); } @Override public String nextElement() { return iterator.next(); } }; } @Override public String[] getValueNames() { return new String[0]; } @Override public void setAttribute(String name, Object value) { attributes.put(name, value); } @Override public void putValue(String name, Object value) { } @Override public void removeAttribute(String name) { attributes.remove(name); } @Override public void removeValue(String name) { } @Override public void invalidate() { attributes.clear(); } @Override public boolean isNew() { // 简单判断，如果会话创建时间和最后访问时间相差在一定范围内，认为是新会话 long timeDiff = getLastAccessedTime() - getCreationTime(); return timeDiff < 1000; // 这里假设 1 秒内为新会话 } public boolean isExpired() { long currentTime = System.currentTimeMillis(); return (currentTime - lastAccessedTime.getTime()) > (maxInactiveInterval * 1000L); } // 辅助方法，用于根据请求更新最后访问时间 public void updateLastAccessedTime() { this.lastAccessedTime = new Date(); } }6.3.2 创建 SessionManager 类SessionManager 类用于集中管理存储 Session 信息，包括创建、获取和失效会话。package com.daicy.minitomcat; import com.daicy.minitomcat.servlet.CustomHttpSession; import java.util.HashMap; import java.util.Map; import java.util.UUID; public class SessionManager { private static final Map<String, CustomHttpSession> sessions = new HashMap<>(); public static CustomHttpSession getSession(String sessionId) { CustomHttpSession session = sessions.get(sessionId); if (session != null) { session.updateLastAccessedTime(); } return session; } public static CustomHttpSession createSession() { String sessionId = UUID.randomUUID().toString(); CustomHttpSession session = new CustomHttpSession(sessionId); sessions.put(sessionId, session); return session; } public static CustomHttpSession getOrCreateSession(String sessionId) { CustomHttpSession session = sessions.get(sessionId); if (session == null) { session = createSession(); } session.updateLastAccessedTime(); return session; } public static void invalidateSession(String sessionId) { sessions.remove(sessionId); } }6.3.3 修改请求与响应类以支持 Session 和 Cookie需要在 HttpServletRequest 中添加获取 Session 和解析请求中 Cookie 的方法，同时在 HttpServletResponse 中支持设置 Cookie。HttpServletRequest 修改以下为 HttpServletRequestImpl 类中的构造函数片段及相关方法实现，主要增加了 Cookie 解析与 Session 获取逻辑。// 片段：HttpServletRequestImpl 构造函数及部分方法 public HttpServletRequestImpl(String method, String requestURI, String queryString, Map<String, String> headers) { this.method = method; this.requestURI = requestURI; this.queryString = queryString; this.headers = headers; // 解析 queryString 并填充参数映射 if (queryString != null) { String[] pairs = queryString.split("&"); for (String pair : pairs) { String[] keyValue = pair.split("="); if (keyValue.length == 2) { parameters.put(keyValue[0], new String[]{keyValue[1]}); } } } // 解析 cookies String cookieHeader = headers.get("Cookie"); if (cookieHeader != null) { String[] cookiePairs = cookieHeader.split("; "); for (String cookiePair : cookiePairs) { String[] keyValue = cookiePair.split("="); if (keyValue.length == 2) { Cookie cookie = new Cookie(keyValue[0], keyValue[1]); cookies.add(cookie); // 检查是否有 session ID if ("JSESSIONID".equals(cookie.getName())) { session = SessionManager.getOrCreateSession(cookie.getValue()); } } } } // 如果没有找到 JSESSIONID，则创建一个新的 session if (session == null) { session = SessionManager.createSession(); cookies.add(new Cookie("JSESSIONID", session.getId())); } } @Override public HttpSession getSession() { return session; } @Override public HttpSession getSession(boolean create) { if (session == null && create) { session = SessionManager.createSession(); cookies.add(new Cookie("JSESSIONID", session.getId())); } return session; } @Override public String getRequestedSessionId() { return this.sessionId; } @Override public boolean isRequestedSessionIdValid() { if (sessionId == null) return false; HttpSession existingSession = SessionManager.getSession(sessionId); return existingSession != null && !((CustomHttpSession) existingSession).isExpired(); } @Override public boolean isRequestedSessionIdFromCookie() { return this.sessionIdFromCookie; } @Override public boolean isRequestedSessionIdFromURL() { return !this.sessionIdFromCookie; } @Override public String changeSessionId() { if (session == null) { getSession(true); } String newSessionId = UUID.randomUUID().toString(); // 从存储中移除旧的 sessionId if (sessionId != null) { SessionManager.invalidateSession(sessionId); } // 更新新的 sessionId 并保存会话到存储 sessionId = newSessionId; sessionIdChanged = true; return sessionId; } public boolean isSessionIdChanged() { return sessionIdChanged; }HttpServletResponse 修改在 HttpServletResponse 中添加设置 Cookie 的方法，并在发送响应时写入 Set-Cookie 头信息。package server; import java.util.ArrayList; import java.util.List; public class HttpServletResponse { private List<Cookie> cookies = new ArrayList<>(); public void addCookie(Cookie cookie) { cookies.add(cookie); } public void sendResponse() throws IOException { // 确保 writer 的内容刷新到 body 中 writer.flush(); setCharacterEncoding(characterEncoding); if (null == getContentType()) { setContentType("text/html; charset=UTF-8"); } if (null == getHeader("Content-Length")) { setContentLength(body.size()); } PrintWriter responseWriter = new PrintWriter(new OutputStreamWriter(outputStream, characterEncoding)); // 写入状态行 responseWriter.printf("HTTP/1.1 %d %s\r\n", statusCode, statusMessage); // 写入头信息 for (Map.Entry<String, List<String>> entry : headers.entrySet()) { String headerName = entry.getKey(); for (String headerValue : entry.getValue()) { responseWriter.printf("%s: %s\r\n", headerName, headerValue); } } // 写入 Cookie for (Cookie cookie : cookies) { StringBuilder cookieHeader = new StringBuilder(); cookieHeader.append(cookie.getName()).append("=").append(cookie.getValue()); if (cookie.getMaxAge() > 0) { cookieHeader.append("; Max-Age=").append(cookie.getMaxAge()); } if (cookie.getPath() != null) { cookieHeader.append("; Path=").append(cookie.getPath()); } if (cookie.getDomain() != null) { cookieHeader.append("; Domain=").append(cookie.getDomain()); } responseWriter.printf("Set-Cookie: %s\r\n", cookieHeader.toString()); } // 空行标识头部结束 responseWriter.print("\r\n"); responseWriter.flush(); // 写入主体内容 body.writeTo(outputStream); responseWriter.flush(); } }6.3.4 实现 CounterServlet 类CounterServlet 是一个简单的计数器 Servlet，用于测试 Session 功能。每次访问该 Servlet 时，它会从 Session 中获取计数值并增加，然后返回当前计数值。package com.daicy.minitomcat; import com.daicy.minitomcat.servlet.HttpServletResponseImpl; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.IOException; public class CounterServlet implements Servlet { @Override public void init(ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig() { return null; } @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponseImpl response = (HttpServletResponseImpl) res; HttpSession session = request.getSession(); Integer count = (Integer) session.getAttribute("count"); if (count == null) { count = 1; } else { count++; } session.setAttribute("count", count); response.getWriter().println(" Visit Count: " + count + " "); } @Override public String getServletInfo() { return ""; } @Override public void destroy() { } }6.3.5 实现 HttpRequestParser 解析类HttpRequestParser 负责解析原始的 HTTP 请求流，提取方法、URI、参数及 Header 信息，并构建请求对象。package com.daicy.minitomcat; import com.daicy.minitomcat.servlet.HttpServletRequestImpl; import javax.servlet.http.Cookie; import java.io.*; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; public class HttpRequestParser { public static HttpServletRequestImpl parseHttpRequest(InputStream inputStream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); // 读取请求行 String requestLine = reader.readLine(); if (requestLine == null || requestLine.isEmpty()) { System.out.println(reader.readLine()); throw new IOException("Empty request line"); } // 解析请求行 String[] parts = requestLine.split(" "); if (parts.length < 3) { throw new IOException("Invalid request line: " + requestLine); } String method = parts[0]; String uri = parts[1]; int queryIndex = uri.indexOf('?'); String requestURI = (queryIndex >= 0) ? uri.substring(0, queryIndex) : uri; String queryString = (queryIndex >= 0) ? uri.substring(queryIndex + 1) : null; // 读取并解析 headers Map<String, String> headers = new HashMap<>(); String line; while ((line = reader.readLine()) != null && !line.isEmpty()) { int separatorIndex = line.indexOf(": "); if (separatorIndex != -1) { String headerName = line.substring(0, separatorIndex); String headerValue = line.substring(separatorIndex + 2); headers.put(headerName, headerValue); } } // 创建并返回 HttpServletRequestImpl return new HttpServletRequestImpl(method, requestURI, queryString, headers); } public static void main(String[] args) throws IOException { // 示例 HTTP 请求 String httpRequest = "GET /hello?name=world HTTP/1.1\r\n" + "Host: localhost\r\n" + "User-Agent: TestAgent\r\n" + "Accept: */*\r\n" + "Cookie: sessionId=abc123; theme=light\r\n\r\n"; InputStream inputStream = new ByteArrayInputStream(httpRequest.getBytes()); HttpServletRequestImpl request = parseHttpRequest(inputStream); // 输出解析后的信息 System.out.println("Method: " + request.getMethod()); System.out.println("Request URI: " + request.getRequestURI()); System.out.println("Query String: " + request.getQueryString()); System.out.println("Session ID: " + request.getSession().getId()); System.out.println("Cookies:"); for (Cookie cookie : request.getCookies()) { System.out.println(" " + cookie.getName() + "=" + cookie.getValue()); } } }6.4 测试启动服务器，在浏览器中访问 http://localhost:8080/counter。第一次访问时，页面将显示访问计数 1，并在响应头中设置 JSESSIONID Cookie。刷新页面后，计数器将继续增加（如显示 2、3...），展示会话管理的效果。若清除浏览器 Cookie 后再次访问，计数将重置为 1。6.5 学习收获Session 管理：学习了如何通过 Session ID 管理用户会话，理解了客户端会话状态在服务器端的存储机制。Cookie 使用：掌握了使用 Cookie 在客户端和服务器间传递信息的方法，特别是 Session ID 的传递。Servlet 状态维护：实现了服务器与客户端间的状态管理基础，为后续实现更复杂的功能（如登录认证）打下基础。项目源代码地址：https://github.com/daichangya/MiniTomcat/tree/chapter6/mini-tomcat说明：本示例基于 javax.servlet API 编写，适用于 Tomcat 9 及以下版本。若使用 Tomcat 10 及以上版本，包名需变更为 jakarta.servlet，且需相应调整导入语句。
第八章：实现异步请求支持（基于 Servlet 协议）在本章中，我们将进一步扩展 MiniTomcat，加入对 Servlet 规范 的支持，特别是 异步请求处理 功能。Servlet 3.0 引入了异步处理机制，允许请求在后台线程中异步执行，从而避免阻塞主线程。这一机制能显著提高服务器处理效率，尤其适用于处理耗时较长的任务（如文件上传、大数据处理或外部 API 调用等）。8.1 功能目标本章主要实现以下目标：实现 Servlet 3.0 异步请求处理：基于 Servlet 3.0 标准构建异步支持能力。提供 AsyncContext 机制：通过 AsyncContext 管理异步请求和响应的生命周期，允许后台线程处理长时间任务而不阻塞主线程。支持异步响应：确保异步任务完成后，能够正确地向客户端发送响应数据。8.2 异步处理流程异步请求的处理流程主要包含以下三个阶段：启动异步请求：客户端发起请求后，Servlet 容器检查请求是否需要异步处理。若需要，调用 request.startAsync() 启动异步上下文。异步任务执行：Servlet 容器将请求处理逻辑分配到后台线程执行，主线程立即返回并释放资源，继续处理其他请求。完成异步处理：后台线程执行完任务后，调用 AsyncContext.complete() 结束异步处理，并向客户端发送响应。8.3 代码实现8.3.1 修改 HttpServletRequestImpl 类，支持异步请求为了支持异步请求，我们需要在 HttpServletRequestImpl 中引入 AsyncContext 相关状态，并实现启动异步任务的方法。public class HttpServletRequestImpl implements HttpServletRequest { // ... 其他成员变量 private boolean asyncStarted = false; private AsyncContext asyncContext; public HttpServletRequestImpl(String method, String requestURI, String queryString, Map<String, String> headers) { // ... 初始化逻辑 } @Override public boolean isAsyncStarted() { return asyncStarted; } @Override public AsyncContext getAsyncContext() { if (!asyncStarted) { throw new IllegalStateException("Async not started"); } return asyncContext; } @Override public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { if (asyncStarted) { throw new IllegalStateException("Async already started"); } asyncStarted = true; asyncContext = new AsyncContextImpl(this, servletResponse); return asyncContext; } @Override public boolean isAsyncSupported() { return asyncStarted; } // ... 其他方法 }8.3.2 修改 HttpProcessor 类，支持异步请求在 HttpProcessor 中，我们需要判断当前请求是否进入了异步模式。如果是异步请求，连接关闭的逻辑需要相应调整，以避免在后台任务完成前过早关闭连接。package com.daicy.minitomcat; import com.daicy.minitomcat.servlet.HttpServletRequestImpl; import com.daicy.minitomcat.servlet.HttpServletResponseImpl; import javax.servlet.AsyncContext; import javax.servlet.http.HttpServletRequest; import java.io.*; import java.net.Socket; import java.util.HashMap; import java.util.Map; public class HttpProcessor implements Runnable { private Socket socket; private final static ServletProcessor servletProcessor = new ServletProcessor(); private final static StaticResourceProcessor staticProcessor = new StaticResourceProcessor(); public HttpProcessor(Socket socket) { this.socket = socket; } @Override public void run() { // 处理请求并返回响应 process(); } public void process() { boolean keepAlive = false; HttpServletRequestImpl request = null; try { InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); // 解析请求 request = HttpRequestParser.parseHttpRequest(inputStream); // 构建响应 HttpServletResponseImpl response = new HttpServletResponseImpl(outputStream); if (null == request) { return; } keepAlive = parseKeepAliveHeader(request) && !isCloseConnection(request); String uri = request.getRequestURI(); WebXmlServletContainer parser = HttpServer.parser; String servletName = parser.getServletName(uri); if (uri.endsWith(".html") || uri.endsWith(".css") || uri.endsWith(".js")) { staticProcessor.process(request, response); } else if (null != servletName) { // 普通请求处理 servletProcessor.process(request, response); } else { send404Response(outputStream); } } catch (Exception e) { System.out.println("HttpProcessor error " + e.getMessage()); } finally { try { // 如果是 keep-alive，连接保持打开，否则关闭连接 // 注意：如果是异步请求，连接关闭逻辑可能由异步完成时控制，此处简化处理 if (!keepAlive && (null != request && !request.isAsyncSupported())) { socket.close(); } } catch (IOException e) { e.printStackTrace(); } } } private boolean parseKeepAliveHeader(HttpServletRequest request) { String connectionHeader = request.getHeader("Connection"); return connectionHeader != null && connectionHeader.equalsIgnoreCase("keep-alive"); } public boolean isCloseConnection(HttpServletRequest request) { String connectionHeader = request.getHeader("Connection"); return connectionHeader != null && connectionHeader.equalsIgnoreCase("close"); } static void send404Response(OutputStream outputStream) { sendResponse(outputStream, 404, "Not Found", "The requested resource was not found."); } // 发送普通文本响应 private static void sendResponse(OutputStream outputStream, int statusCode, String statusText, String message) { PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream)); String html = " " + statusCode + " " + statusText + " " + message + " "; writer.println("HTTP/1.1 " + statusCode + " " + statusText); writer.println("Content-Type: text/html; charset=UTF-8"); writer.println("Content-Length: " + html.length()); writer.println(); writer.println(html); } }8.3.3 实现 AsyncContext 类AsyncContext 是 Servlet 3.0 异步请求处理的核心接口。它提供了异步请求的生命周期管理、后台线程启动及任务完成的处理机制。在 MiniTomcat 中，我们需要自定义一个简单的 AsyncContext 实现类，负责管理异步请求的相关操作。1. AsyncContextImpl 类实现AsyncContextImpl 类是我们自定义的 AsyncContext 接口实现类，主要负责以下功能：start()：启动异步请求处理任务，将任务分配给后台线程。complete()：完成异步请求并发送响应。getRequest()：返回当前的请求对象。getResponse()：返回当前的响应对象。package com.daicy.minitomcat.servlet; import javax.servlet.*; import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class AsyncContextImpl implements AsyncContext { private ServletRequest request; private ServletResponse response; private static ExecutorService executor = Executors.newCachedThreadPool(); public AsyncContextImpl(ServletRequest request, ServletResponse response) { this.request = request; this.response = response; } @Override public void start(Runnable run) { executor.submit(run); } @Override public void complete() { try { // 完成异步响应 HttpServletResponseImpl response = (HttpServletResponseImpl) this.response; response.sendResponse(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public ServletRequest getRequest() { return null; } @Override public ServletResponse getResponse() { return null; } @Override public boolean hasOriginalRequestAndResponse() { return false; } @Override public void dispatch() { } @Override public void dispatch(String path) { } @Override public void dispatch(ServletContext context, String path) { } @Override public void addListener(AsyncListener listener) { } @Override public void addListener(AsyncListener listener, ServletRequest servletRequest, ServletResponse servletResponse) { } @Override public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException { return null; } @Override public void setTimeout(long timeout) { } @Override public long getTimeout() { return 0; } }8.3.4 实现 AsyncServlet 类AsyncServlet 是一个简单的异步 Servlet，用于测试异步功能。每次访问该 Servlet 时，请求会有延迟并异步执行，从而验证主线程是否被释放。package com.daicy.minitomcat; import java.io.IOException; import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class AsyncServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext asyncContext = request.startAsync(request, response); asyncContext.start(() -> { try { Thread.sleep(2000); // 模拟耗时操作 response.getWriter().write("异步响应完成"); } catch (Exception e) { e.printStackTrace(); } finally { asyncContext.complete(); } }); } }8.4 测试启动服务器。访问支持异步操作的 URL，例如 /async。观察服务器日志及响应时间，验证请求是否在后台异步处理且主线程未被阻塞。8.5 学习收获Servlet 异步处理机制：通过 AsyncContext 和 startAsync()，我们成功实现了异步请求处理。在高并发情况下，这能够显著提升服务器的响应速度和吞吐量。线程管理：通过异步处理机制，将长时间操作的任务分配给后台线程，避免了阻塞主线程，提高了线程利用率。性能优化：引入异步处理使得服务器能够同时响应多个请求，优化了对高并发场景的支持。通过实现这一功能，我们为 MiniTomcat 增添了更强的并发处理能力，特别是在面对长时间任务时，能够有效地提升整体的性能。项目源代码地址：https://github.com/daichangya/MiniTomcat/tree/chapter8/mini-tomcat说明：本章内容基于 Servlet 3.0 规范实现，代码主要用于教学演示异步处理原理，实际生产环境请使用成熟的 Web 容器（如 Tomcat、Jetty 等）。
第七章：实现多线程支持在本章中，我们将为 MiniTomcat 引入多线程支持，旨在显著提升服务器的并发处理能力。通过引入线程池（Thread Pool）机制，服务器能够同时处理多个客户端请求，避免单线程模型下的请求阻塞问题。这将使服务器在面对高并发场景时更加高效，更好地支持多用户同时访问。7.1 功能目标本章的核心改造目标如下：多线程支持：利用线程池管理线程资源，为每个客户端请求分配独立的线程进行处理。线程池机制：避免为每个请求频繁创建和销毁线程，通过复用线程降低系统开销，提高执行效率。并发处理：支持多个客户端同时访问不同的 Servlet，确保请求之间互不干扰，实现真正的并发处理。7.2 代码结构本次修改将引入线程池机制，主要通过 ExecutorService 来管理工作线程。项目代码结构更新如下：MiniTomcat ├─ src │ ├─ main │ │ ├─ java │ │ │ ├─ com.daicy.minitomcat │ │ │ │ ├─ servlet │ │ │ │ │ ├─ CustomServletOutputStream.java // 自定义的 Servlet 输出流类 │ │ │ │ │ ├─ CustomHttpSession.java // 自定义的 HttpSession │ │ │ │ │ ├─ HttpServletRequestImpl.java // HTTP 请求的实现类 │ │ │ │ │ ├─ HttpServletResponseImpl.java // HTTP 响应的实现类 │ │ │ │ │ ├─ ServletConfigImpl.java // Servlet 配置的实现类 │ │ │ │ │ ├─ ServletContextImpl.java // Servlet 上下文的实现类 │ │ │ │ ├─ CounterServlet.java // Session 功能 Servlet 示例类 │ │ │ │ ├─ HelloServlet.java // Servlet 示例类 │ │ │ │ ├─ HttpConnector.java // 连接器类 │ │ │ │ ├─ HttpProcessor.java // 请求处理器 │ │ │ │ ├─ HttpServer.java // 主服务器类 │ │ │ │ ├─ HttpRequestParser.java // HttpRequest 信息解析类 │ │ │ │ ├─ ServletLoader.java // Servlet 加载器 │ │ │ │ ├─ ServletProcessor.java // Servlet 处理器 │ │ │ │ ├─ StaticResourceProcessor.java // 静态资源处理器 │ │ │ │ ├─ SessionManager.java // Session 管理器 │ │ │ │ ├─ ThreadPool.java // 线程池管理 │ │ │ │ ├─ WebXmlServletContainer.java // Servlet 容器相关类 │ │ ├─ resources │ │ │ ├─ webroot │ │ │ │ ├─ index.html │ │ │ ├─ web.xml │ ├─ test ├─ pom.xml7.3 代码实现7.3.1 创建 ThreadPool 类我们将基于 Java 并发包中的 ThreadPoolExecutor 来实现线程池管理。ThreadPool 类负责封装线程池的生命周期，并提供提交任务的接口。package com.daicy.minitomcat; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; public class ThreadPool { private ExecutorService executor; public ThreadPool(int poolSize) { // 创建固定大小的线程池 executor = Executors.newFixedThreadPool(poolSize); } public void submitTask(Runnable task) { executor.submit(task); } public void shutdown() { if (executor != null) { executor.shutdown(); } } public boolean isShutdown() { return executor.isShutdown(); } public ThreadPoolExecutor getExecutor() { return (ThreadPoolExecutor) executor; } }7.3.2 修改 HttpProcessor 类以支持多线程HttpProcessor 类将实现 Runnable 接口，以便能够作为任务提交给线程池。每个客户端请求将由线程池中的一个独立线程来处理，从而避免阻塞主线程。public class HttpProcessor implements Runnable { private Socket socket; private final static ServletProcessor servletProcessor = new ServletProcessor(); private final static StaticResourceProcessor staticProcessor = new StaticResourceProcessor(); public HttpProcessor(Socket socket) { this.socket = socket; } @Override public void run() { // 处理请求并返回响应 process(); } // ...... 其他代码保持不变 }7.3.3 修改 HttpConnector 类以启动线程池HttpConnector 类负责启动服务器监听端口，并利用初始化好的线程池来处理客户端连接请求。package com.daicy.minitomcat; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class HttpConnector implements Runnable { private static final int PORT = 8080; // 创建一个线程池，最多支持 10 个并发请求 private static ThreadPool threadPool = new ThreadPool(10); public void start() { Thread thread = new Thread(this); thread.start(); } @Override public void run() { try (ServerSocket serverSocket = new ServerSocket(PORT)) { System.out.println("HTTP Connector is running on port " + PORT); while (true) { Socket clientSocket = serverSocket.accept(); System.out.println("Accepted connection from " + clientSocket.getInetAddress()); // 将连接交给 HttpProcessor 处理，并提交给线程池 HttpProcessor processor = new HttpProcessor(clientSocket); threadPool.submitTask(processor); } } catch (IOException e) { e.printStackTrace(); } } }7.4 测试完成代码修改后，请按以下步骤验证多线程支持是否生效：启动服务器：运行主程序，确保服务器正常启动且无报错。并发访问：使用多个浏览器标签页或不同的 HTTP 客户端工具，同时访问不同的路径（如静态资源与 Servlet 接口）。观察日志：检查控制台日志，确认多个请求被同时接收和处理，且没有出现请求阻塞或排队等待的现象。性能验证：观察服务器的响应时间，确保在高并发下线程池有效提升了吞吐能力。7.5 学习收获通过本章的实现，我们获得了以下技术经验：线程池应用：掌握了线程池的核心概念，学会了如何使用 ExecutorService 和 ThreadPoolExecutor 管理线程，避免频繁创建销毁线程带来的性能开销。多线程编程：深入理解了如何在 Web 服务器架构中引入多线程，实现请求的并发处理，从而提升服务器的吞吐量和响应速度。并发安全设计：学习了如何设计并发安全的服务器结构，确保多个请求在同一时刻处理时互不干扰，数据状态一致。项目源代码地址：https://github.com/daichangya/MiniTomcat/tree/chapter7/mini-tomcat说明：本章代码基于 Java 并发包（java.util.concurrent）实现，适用于 Java 5 及以上版本。示例中的线程池配置主要用于学习与演示，在生产环境中建议根据实际负载情况调整线程池参数，并注意资源泄露风险。
第十五章：支持配置热加载和自动部署-MiniTomcat
第十五章：支持配置热加载和自动部署功能目标本章旨在为 MiniTomcat 实现以下核心功能：支持热部署（Hot Deployment）：能够在不重启服务器的情况下加载新的 Web 应用。目录监控：监控应用目录的变化，当检测到新的 Web 应用时，自动加载该应用的 Servlet 和资源。配置热加载：支持 web.xml 的重新加载和应用更新，确保配置修改后即时生效。实现内容为达成上述目标，我们将实现以下机制：目录监控机制：当检测到应用目录中的变化（如新增应用、修改或删除文件）时，自动触发加载或卸载操作。配置重载支持：支持 web.xml 文件的重新加载，使得配置更新后能自动生效。文件监控线程：提供一个后台线程，持续检测应用目录中的变化，并在变化发生时触发相关的业务逻辑。15.1 热部署的设计热部署的关键在于动态地检测文件系统的变化，并自动加载或卸载 Web 应用。为此，我们可以借助 Java NIO 提供的 WatchService 接口，实现对应用目录中文件的监控。15.1.1 设计思路文件监控：使用 WatchService 监控应用目录中的文件变化，包括新增、修改或删除操作。自动加载应用：当监控到新的应用目录或文件发生变化时，自动加载或更新应用的 Servlet 和配置。重新加载配置：当 web.xml 文件发生变化时，重新加载该配置文件，并应用新的路由和初始化参数。15.1.2 使用 WatchService 实现文件监控以下是 HotDeployment 类的实现代码，展示了如何利用 WatchService 监听目录事件：package com.daicy.minitomcat; import com.daicy.minitomcat.core.StandardContext; import java.net.URL; import java.nio.file.*; import static com.daicy.minitomcat.HttpServer.CONF_PATH; public class HotDeployment { // private static final String APP_BASE_PATH = "/path/to/apps"; // Web 应用目录 public void startDeploymentMonitor() { try { URL url = getClass().getResource(CONF_PATH); Path path = Paths.get(url.getPath()); WatchService watchService = FileSystems.getDefault().newWatchService(); // 注册监控事件：创建、修改和删除 path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); System.out.println("Monitoring directory for changes: " + path.toString()); // 监控目录变化 while (true) { WatchKey key; try { key = watchService.take(); // 等待文件变化事件 } catch (InterruptedException e) { System.out.println("Monitoring interrupted"); return; } // 处理监控到的事件 for (WatchEvent<?> event : key.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); Path filePath = (Path) event.context(); System.out.println("Event detected: " + kind + " on file: " + filePath); // 根据事件类型进行相应处理 if (kind == StandardWatchEventKinds.ENTRY_CREATE) { // 新应用被添加，加载该应用 deployApplication(filePath); } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) { // 修改了应用，重新加载或更新 reloadApplication(filePath); } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) { // 应用被删除，卸载应用 undeployApplication(filePath); } } // 重置 key 以继续监控 boolean valid = key.reset(); if (!valid) { break; } } } catch (Exception e) { e.printStackTrace(); } } // 部署新应用 private void deployApplication(Path filePath) { System.out.println("Deploying new application: " + filePath); // 实际应用加载逻辑 // 例如加载 Servlet、web.xml 配置等 } // 重新加载应用 private void reloadApplication(Path filePath) { System.out.println("Reloading application: " + filePath); // 重新加载应用的 Servlet 和配置 try { HttpServer.context = new StandardContext("/conf/web.xml"); HttpServer.context.start(); } catch (Exception e) { e.printStackTrace(); } } // 卸载应用 private void undeployApplication(Path filePath) { System.out.println("Undeploying application: " + filePath); // 卸载应用，释放资源 } }15.1.3 主要方法解析startDeploymentMonitor：启动文件监控线程，持续监控应用目录中的变化。deployApplication：当检测到新应用被创建时，执行部署操作，加载应用。reloadApplication：当检测到应用被修改时，执行重新加载操作，更新配置或类文件。undeployApplication：当检测到应用被删除时，执行卸载操作，移除应用及其资源。15.2 web.xml 文件的重新加载在热部署过程中，web.xml 文件的更新是非常重要的一部分。我们需要在 web.xml 文件被修改后重新加载它，以使新的配置生效。15.2.1 实现 web.xml 重新加载核心逻辑集中在 reloadApplication 方法中，该方法会重新实例化 StandardContext 并启动： // 重新加载应用 private void reloadApplication(Path filePath) { System.out.println("Reloading application: " + filePath); // 重新加载应用的 Servlet 和配置 try { HttpServer.context = new StandardContext(WEB_XML); HttpServer.context.start(); } catch (Exception e) { e.printStackTrace(); } }15.2.2 主要方法解析reloadApplication：重新加载 web.xml 文件，并解析其中的配置，更新 Servlet 的路由和初始化参数。15.3 集成文件监控与配置热加载我们将 HotDeployment 组件和 StandardContext 结合，在 HttpServer 中完成整个热部署过程的集成。15.3.1 集成示例在服务器启动时，我们将监控线程设置为守护线程，确保主服务运行时能后台监控文件变化：package com.daicy.minitomcat; import com.daicy.minitomcat.core.StandardContext; import com.daicy.minitomcat.servlet.ServletContextImpl; import javax.servlet.*; /** * @author 代长亚 * 一个简单的 HTTP 服务器，用于处理 GET 请求并返回静态文件。 */ public class HttpServer { // 静态文件根目录 static final String WEB_ROOT = "webroot"; public static final String CONF_PATH = "/conf"; public static final String WEB_XML = CONF_PATH + "/web.xml"; public static ServletContextImpl servletContext = new ServletContextImpl(); public static StandardContext context; public static HotDeployment hotDeployment = new HotDeployment(); public static FilterManager filterManager = new FilterManager(); private static ServletContextListenerManager servletContextListenerManager = new ServletContextListenerManager(); public static HttpSessionListenerManager sessionListenerManager = new HttpSessionListenerManager(); public static void main(String[] args) throws Exception { Thread daemonThread = new Thread(() -> { hotDeployment.startDeploymentMonitor(); }); // 将线程设置为守护线程，必须在启动线程之前调用 daemonThread.setDaemon(true); daemonThread.start(); servletContextListenerManager.addListener(new ServletContextListenerImpl()); sessionListenerManager.addListener(new HttpSessionListenerImpl()); // 启动监听器 servletContextListenerManager.notifyContextInitialized(new ServletContextEvent(servletContext)); context = new StandardContext(WEB_XML); context.start(); filterManager.addFilter(new LoggingFilter()); HttpConnector connector = new HttpConnector(); connector.start(); // 模拟服务器关闭 Runtime.getRuntime().addShutdownHook(new Thread(HttpServer::stop)); } public static void stop() { try { LogManager.getLogger().info("Server stopping..."); context.stop(); servletContextListenerManager.notifyContextDestroyed(new ServletContextEvent(servletContext)); SessionManager.removeSession(); } catch (Exception e) { e.printStackTrace(); } } }15.4 学习收获通过实现热加载和自动部署机制，我们学习了以下内容：目录监控和事件处理：使用 Java NIO 的 WatchService 实现了对文件系统的实时监控，能够及时响应目录中的变化。热部署与应用更新：通过监控目录变化，实现了在不重启服务器的情况下自动加载、更新或卸载应用。这为现代 Web 容器提供了动态部署的能力。web.xml 文件的重新加载：实现了 web.xml 配置文件的重新加载机制，能够在配置发生变化时自动更新容器的行为。这些实现使得我们的 Web 容器在开发和生产环境中都具有高度的灵活性和动态扩展能力，可以支持快速迭代和应用的持续部署。项目源代码地址：https://github.com/daichangya/MiniTomcat/tree/chapter15/mini-tomcat说明：本章实现的热部署机制主要适用于开发环境或学习场景。在生产环境中，文件监控热部署可能带来一致性风险，通常建议通过容器编排或 CI/CD 流程进行应用更新。
引言在构建 MiniTomcat 的过程中，处理网络请求与数据传输是核心环节，而这正是 Java 的输入/输出（I/O）流所能胜任的关键任务。I/O 流负责数据的读取与写入，是实现客户端与服务器之间数据交互的基础。因此，深入理解 Java I/O 流机制，将有助于我们在 MiniTomcat 中高效地实现 HTTP 请求解析与响应构建功能。1. Java I/O 流概述Java I/O 流体系主要可以分为以下两大类：字节流（Byte Stream）：主要用于处理二进制数据（如图片、音频、网络数据包），核心基类为 InputStream 和 OutputStream。字符流（Character Stream）：主要用于处理字符数据（文本），核心基类为 Reader 和 Writer。在 MiniTomcat 的开发场景中，字节流通常用于底层的网络数据传输，而字符流则更适合处理文本数据，例如读取 HTTP 请求头或写入 HTML 响应内容。2. 字节流（Byte Stream）字节流是 I/O 操作的基础，适用于所有类型的数据。InputStream：用于读取字节数据，常见实现类包括 FileInputStream 和 BufferedInputStream。OutputStream：用于写入字节数据，常见实现类包括 FileOutputStream 和 BufferedOutputStream。示例：从文件中读取字节数据FileInputStream fis = new FileInputStream("example.txt"); int data; // 逐字节读取，直到文件末尾（返回 -1） while ((data = fis.read()) != -1) { // 注意：直接将 byte 转为 char 仅适用于文本文件演示 System.out.print((char) data); } fis.close();3. 字符流（Character Stream）字符流在字节流的基础上增加了编码转换功能，更适合处理文本信息。Reader：用于读取字符数据，常见实现类包括 FileReader 和 BufferedReader。Writer：用于写入字符数据，常见实现类包括 FileWriter 和 BufferedWriter。示例：从文件中读取字符数据FileReader fr = new FileReader("example.txt"); BufferedReader br = new BufferedReader(fr); String line; // 按行读取文本 while ((line = br.readLine()) != null) { System.out.println(line); } br.close();4. 缓冲流（Buffered Stream）为了提升 I/O 操作效率，Java 提供了缓冲流包装类。BufferedReader 和 BufferedWriter：用于字符数据的缓冲处理。BufferedInputStream 和 BufferedOutputStream：用于字节数据的缓冲处理。在服务器开发中，直接使用底层流会导致频繁的系统调用。缓冲流通过在内存中建立缓冲区，减少了实际 I/O 操作的次数，能显著提升读取和写入的效率，特别适用于高频的网络 I/O 操作。5. 综合运用：Socket I/O在服务器与客户端之间建立通信时，通常使用 Socket 来传输数据。Socket 的输入/输出流可以通过字节流或字符流来操作，具体取决于数据处理的需求。InputStream 和 OutputStream：可从 Socket 中直接获取，分别用于读取请求二进制数据和发送响应二进制数据。常见的 Socket 操作包括：读取 HTTP 请求头、构建响应内容、关闭连接等。示例：简单的 Socket 读取与响应操作ServerSocket serverSocket = new ServerSocket(8080); Socket clientSocket = serverSocket.accept(); // 通过 InputStreamReader 将字节流转换为字符流，便于处理文本协议 BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream())); String line; // 读取请求头，直到遇到空行 while ((line = in.readLine()) != null && !line.isEmpty()) { System.out.println(line); // 打印请求头信息 } // 构建 HTTP 响应 out.write("HTTP/1.1 200 OK\r\n"); out.write("Content-Type: text/html\r\n"); out.write("\r\n"); out.write(" Hello, World! "); out.flush(); clientSocket.close(); serverSocket.close();6. 实践任务 📝使用 Java Socket 编程，实现一个简单的 Web 服务器，能够接收 HTTP 请求并返回一个固定的 HTML 响应。在服务器上使用 BufferedReader 和 BufferedWriter 处理请求和响应数据，体会缓冲流对性能的提升。尝试在本地浏览器访问 http://localhost:8080，观察服务器返回的响应内容。通过练习 Java I/O 流操作，特别是在 Socket 编程中的实际应用，我们就能更好地理解在 MiniTomcat 中如何高效处理客户端请求和服务器响应的流数据。有任何问题请随时提问哦！🦌说明：本文示例代码基于 Java 基础 I/O 编写，适用于 Java 7 及以上版本。在实际生产环境中，建议使用 try-with-resources 语法自动管理流资源的关闭，以避免资源泄露。
上一章内容：打造属于你的 MiniTomcat：深入理解 Web 容器核心架构与实现之路从零开始实现一个类似 Tomcat 的轻量级 Java Web 容器，可以分为多个步骤逐步构建核心功能。以下是一个详细的实现路径大纲，涵盖了每一步的目标功能，旨在帮助你从简单的 HTTP 服务器起步，逐步深入实现基本的 Servlet 容器。第 1 章：实现基础 HTTP 服务器功能目标创建一个简单的 HTTP 服务器，能够监听指定端口、接收 HTTP 请求，并返回 HTTP 响应。实现内容使用 Java 的 ServerSocket 监听指定端口。读取客户端请求，解析 HTTP 请求行和头部信息。返回简单的 HTTP 响应，例如 200 OK 或 404 Not Found。示例功能访问 http://localhost:8080/ 时，返回一段静态文本。学习收获理解 HTTP 协议的基本结构，包括请求和响应的格式。第 2 章：解析 HTTP 请求，支持静态文件功能目标解析 HTTP 请求中的路径、方法（GET、POST 等）和头部信息。基于请求路径提供静态文件支持，类似于静态资源服务器。实现内容根据 URL 路径，查找服务器本地的文件并返回其内容（如 HTML、CSS、JS 文件）。支持 MIME 类型识别，如 text/html、application/javascript，返回正确的 Content-Type。示例功能访问 http://localhost:8080/index.html 时，返回服务器上的 index.html 文件内容。学习收获加深对 HTTP 请求和响应头的理解，熟悉静态文件的处理方式。第 3 章：实现连接器（Connector）组件功能目标实现一个连接器组件，用于处理 HTTP 连接和数据包解析。该组件用于管理网络传输和请求解析，将网络传输与请求处理解耦，提升代码清晰度和容错性。实现内容创建 Connector 组件，用于与客户端建立连接，读取 HTTP 数据包，并交由请求解析器处理。第 4 章：实现 Servlet 容器的基本功能功能目标实现一个简单的 Servlet 容器，支持加载、初始化、调用和销毁 Servlet。实现内容实现 HttpServletRequest 和 HttpServletResponse 类，用于封装请求和响应数据。根据请求路径映射到对应的 Servlet 类，并调用 Servlet 的 service() 方法处理请求。提供 ServletContext 和 ServletConfig 支持，维护 Servlet 的生命周期。示例功能定义一个简单的 Servlet，如 HelloServlet，访问时返回自定义的响应内容。学习收获熟悉 Servlet 的生命周期（init、service、destroy），理解 Servlet 和 Web 容器之间的交互关系。第 5 章：支持 Servlet 配置和 URL 映射功能目标通过配置文件（例如 web.xml）管理 Servlet 路由映射和初始化参数。实现内容解析 web.xml 文件，读取 Servlet 映射信息，将 URL 路径映射到对应的 Servlet 类。支持 Servlet 的初始化参数，并在 ServletConfig 中提供获取方式。示例功能根据 web.xml 的配置，访问 /hello 路径时自动调用 HelloServlet。学习收获学习解析 XML 文件，掌握基于配置的 URL 路由映射。第 6 章：支持 Session 和 Cookie功能目标增加对 Session 和 Cookie 的支持，实现简单的会话管理。实现内容实现 HttpSession，为每个客户端会话分配唯一的 Session ID，并保持会话状态。实现对 Cookie 的支持，解析请求中的 Cookie，并将响应的 Cookie 发送到客户端。示例功能实现一个简单的计数器 Servlet，记录每个客户端的访问次数。学习收获掌握会话管理的基本原理，理解如何使用 Session 和 Cookie 维护客户端状态。第 7 章：实现多线程支持功能目标支持多线程处理，提高服务器并发能力，允许多个请求同时处理。实现内容使用线程池处理每个客户端的连接，避免阻塞其他请求。可以使用 ExecutorService 或 ThreadPoolExecutor 创建线程池。示例功能多个客户端可以同时访问不同的 Servlet，不会互相影响。学习收获了解多线程编程和线程池的概念，掌握如何设计并发安全的 Web 服务器。第 8 章：实现异步请求支持功能目标在多线程支持的基础上，引入异步请求处理。异步请求能够在高并发和长时间请求（如大文件上传、长连接）场景下提升处理效率。第 9 章：实现过滤器（Filter）和监听器（Listener）功能目标增加 Filter 和 Listener 支持，用于在请求处理过程中插入额外的操作或监听事件。实现内容实现 Filter 接口，支持请求过滤，例如实现日志记录、认证拦截等功能。实现 Listener 接口，支持监听 Servlet 上下文或会话的创建和销毁事件。示例功能实现一个简单的日志过滤器，记录每个请求的访问时间和路径。学习收获掌握 Servlet 规范中的过滤器和监听器机制，理解如何扩展请求处理流程。第 10 章：实现 Valve 和 Pipeline 机制功能目标实现 Valve 和 Pipeline 机制，允许在请求处理流程中插入额外的控制和功能扩展。实现内容Valve：一种过滤器链机制，可以在请求和响应过程中插入额外的处理逻辑。例如，可以实现权限控制、日志记录和请求过滤等功能。Pipeline：管理多个 Valve 的执行顺序。每个请求都会按顺序通过 Pipeline 中的 Valve 执行操作。实现方式：定义 Valve 接口，让每个 Valve 实现特定逻辑，并将 Valve 按顺序添加到 Pipeline 中。示例功能实现一个日志记录 Valve，记录每个请求的 URI 和执行时间，将该 Valve 添加到 Pipeline 中。学习收获掌握如何设计灵活的请求处理链，理解 Tomcat 的 Valve 和 Pipeline 机制。第 11 章：实现 Wrapper 和 Context功能目标实现 Wrapper 和 Context 组件，用于管理 Servlet 和 Web 应用。实现内容Wrapper：Servlet 的封装容器，负责管理单个 Servlet 的生命周期。每个 Wrapper 都关联一个特定的 Servlet 实例。Context：Web 应用的上下文容器，一个 Context 可以包含多个 Wrapper。Context 管理应用的配置和生命周期，可以加载和卸载整个应用。实现方式：设计 Wrapper 和 Context 接口，分别实现 Servlet 和应用的管理功能。示例功能在 Context 中配置多个 Wrapper，每个 Wrapper 绑定到不同的 Servlet，支持多个 Servlet 在同一 Web 应用中共存。学习收获理解 Web 应用和 Servlet 容器的结构关系，学习如何实现组件化的 Web 应用管理。第 12 章：实现 Facade（外观模式）功能目标使用 Facade 模式简化外部对 Servlet API 的访问，隐藏内部复杂的实现细节。实现内容Facade：一种设计模式，用于封装复杂的子系统。Servlet 容器中可以通过 Facade 包装 HttpServletRequest 和 HttpServletResponse 等对象，限制对内部结构的直接访问。实现方式：实现 RequestFacade 和 ResponseFacade 类，对外提供标准的请求和响应接口，内部则处理复杂的请求和响应解析。示例功能创建一个 RequestFacade 类，封装实际的请求对象，屏蔽不必要的内部细节。学习收获了解 Facade 模式的设计思想，掌握如何通过封装提高系统的安全性和易用性。第 13 章：实现生命周期管理（Lifecycle）功能目标实现 Lifecycle 组件，用于统一管理各个组件的启动、停止等生命周期操作。实现内容定义 Lifecycle 接口，提供 start、stop 方法，供 Context、Wrapper 等组件使用，方便容器管理不同组件的生命周期。第 14 章：实现 Log 模块功能目标实现 Log 模块，支持日志记录和日志级别管理。实现内容Log 模块：实现一个 Log 组件，用于记录请求日志、错误日志和系统日志。日志级别：支持不同的日志级别（INFO、DEBUG、ERROR 等），以便控制日志的详细程度。实现方式：可以定义一个简单的 Logger 类，提供不同级别的日志输出，并配置输出格式和文件路径。示例功能在 Logger 中实现 info()、debug() 和 error() 方法，按需记录不同级别的日志。学习收获掌握日志管理的基本原理，理解如何使用日志级别进行调试和监控。第 15 章：支持配置热加载和自动部署功能目标支持热部署（Hot Deployment）功能，能够在不重启服务器的情况下加载新的应用。实现内容监控应用目录的变化，当检测到新的 Web 应用时，自动加载该应用的 Servlet 和资源。支持 web.xml 的重新加载和应用更新。示例功能向服务器添加新的应用目录，系统自动检测并部署该应用。学习收获学习如何实现配置热加载和资源监控，理解现代应用服务器的自动部署机制。总结通过上述步骤，可以从零开始实现一个功能逐步完善的轻量级 Java Web 容器，理解每个组件的设计与实现。每一步的功能在 Servlet 容器的发展历程中都扮演着重要角色。这个实现过程不仅可以帮助掌握 Web 容器的基本概念，也可以深入了解 Java Web 应用的底层原理。说明：本文大纲基于传统 Java EE Servlet 规范（如 Servlet 3.x/4.x，包名为 javax.servlet）。若使用 Jakarta EE 9 及以上版本（如 Tomcat 10+），相关包名已变更为 jakarta.servlet，部分 API 可能存在差异，请根据实际使用的 Servlet 版本进行调整。
我们将以 Java I/O 的基础知识为起点，为构建 MiniTomcat 打下坚实的文件处理与网络输入输出基础。在本模块中，你将深入了解 Java 的 I/O 系统体系，学会使用字节流和字符流进行高效的数据传输，并掌握常见的文件管理操作。1. Java I/O 概览Java 中的 I/O（Input/Output，输入/输出）操作允许程序与外部资源（如文件系统、网络套接字等）进行数据交互。Java 的 I/O 系统主要基于流（Stream）的概念，分为两种核心方式：字节流（Byte Stream）：以字节为单位处理数据，适用于传输图像、音频、视频等二进制内容。字符流（Character Stream）：以字符为单位处理数据，内置了字符编码转换机制，更适合处理文本文件。2. 字节流与字符流2.1 字节流 (InputStream 和 OutputStream)字节流是 I/O 操作的基础，直接处理原始二进制数据。其主要抽象基类为 InputStream 和 OutputStream。常用的具体实现类包括：FileInputStream：用于从文件中读取字节数据。FileOutputStream：用于向文件中写入字节数据。2.2 字符流 (Reader 和 Writer)字符流在字节流的基础上进行了封装，能够自动处理字符编码问题，简化了文本操作。其主要抽象基类为 Reader 和 Writer。常用的具体实现类包括：FileReader：用于从文件中读取字符数据。FileWriter：用于向文件中写入字符数据。3. 文件处理操作在 Java 中，可以通过 java.io.File 类来管理文件和目录的元数据。常见的操作包括：创建文件：file.createNewFile()删除文件：file.delete()判断文件是否存在：file.exists()获取文件路径：file.getPath()4. 代码示例以下代码演示了如何使用字符流进行基本的文件写入与读取操作。示例中使用了 try-with-resources 语法，确保流在使用完毕后自动关闭，避免资源泄露。import java.io.*; public class FileIOExample { public static void main(String[] args) { // 写入文件 try (FileWriter writer = new FileWriter("example.txt")) { writer.write("Hello, MiniTomcat!"); } catch (IOException e) { e.printStackTrace(); } // 读取文件 try (FileReader reader = new FileReader("example.txt")) { int character; // 逐字符读取，直到文件末尾（返回 -1） while ((character = reader.read()) != -1) { System.out.print((char) character); } } catch (IOException e) { e.printStackTrace(); } } }5. 实践任务 📝为了巩固所学知识，请完成以下练习：字节流练习：创建一个新的 Java 类，尝试使用 FileInputStream 和 FileOutputStream 来复制一个二进制文件（如图片），体会字节流的处理方式。文件管理练习：使用 File 类创建一个指定目录，并在该目录下创建一个文件，将简单的文本内容写入其中。完成这些练习后，你将掌握 Java I/O 的基础核心，并为后续 MiniTomcat 的文件处理与网络通信模块做好准备！如果在实践中遇到问题，欢迎随时提问 🦌说明：本文示例代码使用了 try-with-resources 语法，需确保 Java 版本为 Java 7 或以上。此外，FileWriter 默认使用系统编码，在生产环境中处理文本时，建议显式指定字符编码（如 UTF-8）以避免乱码问题。
深入理解Servlet：从基础概念到高级特性与实战应用
一、Servlet 简介与工作原理Servlet 是 Java Web 开发的核心组件，运行于服务器端，主要负责处理客户端请求并返回响应。其工作原理涉及多个组件的协同运作，从客户端发起请求到服务器端处理并返回响应，整个过程严谨有序。（一）Servlet 容器与 TomcatServlet 容器（Servlet Container）是 Servlet 运行的环境，负责管理 Servlet 的生命周期、资源分配以及请求分发等工作。Apache Tomcat 是最常用的 Servlet 容器之一，具备强大的功能和良好的性能。在 Tomcat 架构中，Context 容器直接管理 Servlet 的包装类 Wrapper，一个 Context 对应一个 Web 应用。例如，在 Tomcat 的配置文件中，可以通过 <Context> 标签配置 Web 应用的相关参数，如上下文路径、文档根目录等。（二）Servlet 的生命周期Servlet 的生命周期由容器管理，主要包含以下四个阶段：加载和实例化Servlet 容器通常在启动时或首次接收到请求时加载 Servlet 类。它通过类加载器从本地文件系统、远程文件系统或网络服务中获取类定义。例如，Web 应用启动时，Tomcat 会根据 web.xml 中的配置找到对应的 Servlet 类并加载。容器使用 Java 反射 API 创建 Servlet 实例，调用默认构造方法（无参构造）。因此，编写 Servlet 类时不应提供带参数的构造方法。初始化实例化后，容器调用 Servlet 的 init() 方法进行初始化。在此方法中，Servlet 可执行准备工作，如建立数据库连接、获取配置信息等。示例如下：public void init(ServletConfig config) throws ServletException { super.init(config); // 在这里进行初始化操作，如获取初始化参数 String paramValue = config.getInitParameter("paramName"); // 其他初始化逻辑 }每个 Servlet 实例的 init() 方法仅被调用一次。初始化期间可使用 ServletConfig 对象获取 web.xml 中配置的初始化参数。若发生错误，可抛出 ServletException 或 UnavailableException 异常通知容器。请求处理容器调用 Servlet 的 service() 方法处理请求。在 service() 方法中，Servlet 通过 ServletRequest 对象获取客户端和请求信息，处理后通过 ServletResponse 对象设置响应信息。例如，处理登录请求的 Servlet 如下：protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 获取用户名和密码 String username = request.getParameter("username"); String password = request.getParameter("password"); // 进行登录验证等业务逻辑处理 if (isValidUser(username, password)) { response.getWriter().println("登录成功"); } else { response.getWriter().println("登录失败"); } }若 service() 方法执行期间发生错误，可抛出 ServletException 或 UnavailableException 异常。若 UnavailableException 指示实例永久不可用，容器将调用 destroy() 方法释放实例。服务终止当容器检测到 Servlet 实例应被移除时（如应用停止或重新加载），调用 destroy() 方法释放资源，如关闭数据库连接、保存数据等。示例如下：public void destroy() { // 释放资源的逻辑，如关闭数据库连接 if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } super.destroy(); }destroy() 方法调用后，容器释放 Servlet 实例，由 Java 垃圾收集器回收。若再次需要该 Servlet 处理请求，容器会创建新的实例。（三）Servlet 的体系结构Servlet 规范基于几个关键类运转，其中 ServletConfig、ServletRequest 和 ServletResponse 与 Servlet 主动关联。ServletConfig 在初始化时传递给 Servlet，用于获取配置属性；ServletRequest 和 ServletResponse 在请求处理时传递，分别用于获取请求信息和设置响应信息。在 Tomcat 容器中，广泛应用了门面设计模式（Facade Pattern）。例如，StandardWrapper 和 StandardWrapperFacade 实现了 ServletConfig 接口，传给 Servlet 的实际是 StandardWrapperFacade 对象。这保证了 Servlet 能获取所需数据，同时不暴露内部无关数据。同样，ServletContext 也有类似结构，Servlet 中获取的实际对象是 ApplicationContextFacade，用于安全地获取应用相关信息。二、Servlet 的基本使用与配置（一）创建 Servlet 类创建 Servlet 类通常需要继承 HttpServlet 类并重写相应方法（如 doGet 或 doPost）。例如，创建一个简单的 HelloWorldServlet：import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class HelloWorldServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置响应内容类型为 HTML response.setContentType("text/html"); // 获取输出流对象 PrintWriter out = response.getWriter(); // 输出 HTML 内容 out.println(" "); out.println(" Hello, World! "); out.println(" "); } }（二）在 web.xml 中配置 Servlet在 web.xml 文件中，需配置 Servlet 的相关信息，包括名称、类名、初始化参数和映射路径等。以下是上述 HelloWorldServlet 的配置示例：<servlet> <servlet-name>HelloWorldServlet</servlet-name> <servlet-class>com.example.HelloWorldServlet</servlet-class> <init-param> <param-name>greeting</param-name> <param-value>Hello!</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>HelloWorldServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping>（三）Servlet 与 JSP 的关系JSP 本质上是 Servlet 的扩展。JSP 页面在第一次被访问时会被翻译成 Servlet 源码并编译执行。在 Tomcat 中，通过 JspServlet 来处理 JSP 页面的翻译工作，其在 conf/web.xml 中有相应配置，会拦截所有以 .jsp 或 .jspx 为后缀的请求。例如，一个简单的 JSP 页面：<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> JSP Example <% // 这里可以嵌入 Java 代码 String message = "This is a JSP page."; %> <%=message%> 当访问该 JSP 页面时，Tomcat 会将其翻译成对应的 Servlet 类并执行，最终将生成的 HTML 内容返回给客户端。三、Servlet 3.0 新特性详解Servlet 3.0 规范引入了多项重要特性，旨在简化开发并提升性能。（一）异步处理支持解决的问题在 Servlet 3.0 之前，Servlet 线程在处理业务时一直处于阻塞状态，直到业务处理完毕才能输出响应并结束线程。若业务处理耗时较长（如数据库操作、跨网络调用等），会导致服务器线程资源占用过多，影响并发处理能力。异步处理流程Servlet 接收到请求后，可先进行预处理，然后将请求转交给异步线程处理，自身返回容器。异步线程处理完业务后，可直接生成响应数据或转发请求。示例如下：import javax.servlet.AsyncContext; 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; import java.io.PrintWriter; import java.util.Date; @WebServlet(urlPatterns = "/asyncDemo", asyncSupported = true) public class AsyncDemoServlet extends HttpServlet { @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("进入 Servlet 的时间：" + new Date() + "."); out.flush(); // 启动异步处理 AsyncContext ctx = req.startAsync(); // 执行异步业务逻辑 new Thread(new AsyncTask(ctx)).start(); out.println("结束 Servlet 的时间：" + new Date() + "."); out.flush(); } } class AsyncTask implements Runnable { private AsyncContext ctx; public AsyncTask(AsyncContext ctx) { this.ctx = ctx; } @Override public void run() { try { // 模拟耗时业务操作，这里等待 5 秒 Thread.sleep(5000); PrintWriter out = ctx.getResponse().getWriter(); out.println("业务处理完毕的时间：" + new Date() + "."); out.flush(); ctx.complete(); } catch (Exception e) { e.printStackTrace(); } } }配置方式使用传统 web.xml 配置时，在 <servlet> 标签中添加 <async-supported>true</async-supported> 子标签。使用注解配置时，在 @WebServlet 或 @WebFilter 注解中设置 asyncSupported = true。（二）新增的注解支持简化配置Servlet 3.0 新增了多个注解，用于简化 Servlet、过滤器和监听器的声明，使 web.xml 部署描述文件不再是必选的。常用注解介绍@WebServlet：用于将类声明为 Servlet，可配置名称、URL 匹配模式、加载顺序、初始化参数、异步支持等属性。@WebServlet(urlPatterns = {"/demoServlet"}, asyncSupported = true, loadOnStartup = 1, name = "DemoServlet", displayName = "DS", initParams = {@WebInitParam(name = "param1", value = "value1")}) public class DemoServlet extends HttpServlet {...}@WebFilter：用于声明过滤器，可配置过滤器名称、URL 匹配模式、应用的 Servlet、转发模式、初始化参数、异步支持等属性。@WebFilter(servletNames = {"DemoServlet"}, filterName = "DemoFilter") public class DemoFilter implements Filter {...}@WebListener：用于将类声明为监听器，被标注的类需实现至少一个相关接口，如 ServletContextListener 等。@WebListener("This is a demo listener") public class SimpleListener implements ServletContextListener {...}@MultipartConfig：辅助 HttpServletRequest 对上传文件的支持。标注在 Servlet 上，表示希望处理的请求的 MIME 类型是 multipart/form-data，并可配置文件大小阈值、存放地址、允许上传的最大值等属性。@MultipartConfig(fileSizeThreshold = 1024 * 1024, location = "/tmp/uploads", maxFileSize = 1024 * 1024 * 5, maxRequestSize = 1024 * 1024 * 10) @WebServlet("/uploadServlet") public class UploadServlet extends HttpServlet {...}（三）可插性支持功能扩充方式可插性支持允许在不修改已有 Web 应用的前提下，将通过一定格式打包的 JAR 包放到 WEB-INF/lib 目录下，实现新功能的扩充。web-fragment.xml 文件Servlet 3.0 引入了 web-fragment.xml 部署描述文件，存放在 JAR 文件的 META-INF 目录下，可包含 web.xml 中能定义的内容。示例如下：<?xml version="1.0" encoding="UTF-8"?> <web-fragment xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd" metadata-complete="true"> <servlet> <servlet-name>FragmentServlet</servlet-name> <servlet-class>com.example.FragmentServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>FragmentServlet</servlet-name> <url-pattern>/fragment</url-pattern> </servlet-mapping> </web-fragment>加载顺序规则web-fragment.xml 包含 <name> 和 <ordering> 两个可选顶层标签，用于指定加载顺序。<name> 标识文件，<ordering> 通过 <after> 和 <before> 子标签指定与其他文件的相对位置关系，还可使用 <others/> 表示除自身外的其他文件，其优先级低于明确指定的相对位置关系。（四）ServletContext 的性能增强动态部署与配置ServletContext 对象在 Servlet 3.0 中支持在运行时动态部署 Servlet、过滤器、监听器，以及为 Servlet 和过滤器增加 URL 映射等。例如，动态添加 Servlet：ServletContext context = getServletContext(); ServletRegistration.Dynamic dynamicServlet = context.addServlet("DynamicServlet", DynamicServlet.class); dynamicServlet.addMapping("/dynamic"); dynamicServlet.setLoadOnStartup(2);与相关接口和类的配合这些动态配置方法通常在 ServletContextListener 的 contextInitialized 方法或 ServletContainerInitializer 的 onStartup() 方法中调用。ServletContainerInitializer 是 Servlet 3.0 新增接口，容器启动时使用 JAR 服务 API 发现其实现类，并将 WEB-INF/lib 目录下 JAR 包中的类交给 onStartup() 方法处理，通常需使用 @HandlesTypes 注解指定处理的类。（五）HttpServletRequest 对文件上传的支持简化文件上传操作Servlet 3.0 之前，处理上传文件需使用第三方框架。现在 HttpServletRequest 提供了 getPart() 和 getParts() 方法用于从请求中解析上传文件。每个文件用 javax.servlet.http.Part 对象表示，该接口提供了处理文件的简易方法，如 write()、delete() 等。示例如下：import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import java.io.IOException; @WebServlet("/upload") @MultipartConfig public class FileUploadServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Part filePart = request.getPart("file"); if (filePart != null) { filePart.write("/tmp/uploadedFile.txt"); response.getWriter().println("文件上传成功"); } else { response.getWriter().println("没有选择文件上传"); } } }配置与注意事项需配合 @MultipartConfig 注解对上传操作进行自定义配置，如限制文件大小和保存路径等。注意，如果请求的 MIME 类型不是 multipart/form-data，使用上述方法会抛出异常。四、Servlet 在实际应用中的场景与案例分析（一）在 Web 应用中的常见应用场景处理用户请求与业务逻辑Servlet 可接收用户在浏览器中输入的 URL 请求，根据请求参数进行业务逻辑处理，如登录验证、数据查询与更新等。例如，在电商网站中，LoginServlet 接收用户名和密码，与数据库中的用户信息进行比对，验证用户身份。生成动态页面内容通过获取数据库数据或其他业务逻辑处理结果，Servlet 可以动态生成 HTML、XML 等格式的页面内容返回给客户端。比如，新闻网站的 NewsServlet 根据用户请求的新闻类别，从数据库中查询相关新闻数据，然后生成包含新闻列表的 HTML 页面返回给用户。（二）案例分析：使用 Servlet 实现简单的用户登录系统功能需求用户在登录页面输入用户名和密码，点击登录按钮后，请求发送到服务器端的 LoginServlet。LoginServlet 验证用户名和密码是否正确：如果正确，跳转到欢迎页面；如果错误，返回错误提示信息到登录页面。代码实现登录页面（login.jsp）<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> Login Page Login Username: Password: LoginServlet.javaimport javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置响应内容类型为 HTML response.setContentType("text/html"); // 获取用户名和密码 String username = request.getParameter("username"); String password = request.getParameter("password"); // 假设这里进行简单的用户名和密码验证，实际应用中应与数据库比对 if ("admin".equals(username) && "123456".equals(password)) { // 登录成功，将用户名存入 Session 后跳转到欢迎页面 request.getSession().setAttribute("username", username); response.sendRedirect("welcome.jsp"); } else { // 登录失败，返回错误提示 PrintWriter out = response.getWriter(); out.println(" "); out.println(" Login Failed "); out.println(" Invalid username or password. "); out.println(" "); } } }web.xml 配置<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>欢迎页面（welcome.jsp）<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> Welcome Welcome, <%= session.getAttribute("username") %>! 通过这个案例，可以看到 Servlet 在处理用户请求、验证用户身份以及控制页面跳转等方面的实际应用。它是构建 Java Web 应用的重要基础组件，在实际开发中还有更多复杂和高级的应用场景等待开发者去探索和实践。五、总结与展望Servlet 作为 Java Web 开发的核心技术之一，在服务器端处理请求和生成响应方面有着不可替代的作用。从其基本的工作原理、生命周期到配置使用，再到 Servlet 3.0 带来的一系列新特性（如异步处理、注解配置、动态注册等），都为 Java Web 开发提供了更强大、更灵活的工具。在实际应用中，它广泛应用于各种 Web 系统的构建，从简单的网站到复杂的企业级应用。随着技术的不断发展，Servlet 规范也在持续演进。未来可能会在性能优化、与新兴技术的融合等方面有更多的突破。开发者需要持续关注其发展动态，以便更好地利用 Servlet 构建高效、稳定的 Web 应用。说明：本文主要基于 Servlet 3.0 规范及 javax.servlet 包进行讲解。自 Jakarta EE 9 起，Servlet 规范已迁移至 jakarta.servlet 包下（如 Servlet 5.0/6.0），但核心生命周期与工作原理保持一致。在实际开发中，请根据所使用的 Java EE 或 Jakarta EE 版本选择对应的依赖包。
第九章：实现过滤器（Filter）和监听器（Listener）本章目标本章旨在为 Minitomcat 容器增加 Filter（过滤器） 和 Listener（监听器） 的支持，以便在请求处理过程中插入额外的操作或监听特定事件。具体实现目标如下：实现 Filter 接口：支持在请求到达 Servlet 之前和响应返回客户端之前插入自定义逻辑，例如日志记录、认证拦截等。实现 Listener 接口：支持监听 Servlet 上下文（Context）、会话（Session）或请求的生命周期事件。示例功能：实现一个日志过滤器，记录每个请求的访问时间和路径。在 Servlet 上下文初始化时创建监听器，监听上下文和会话的创建与销毁事件。9.1 过滤器（Filter）Filter 是 Servlet 容器提供的一个接口，它允许开发者在请求到达 Servlet 之前和响应返回客户端之前插入自定义的处理逻辑。过滤器常用于执行日志记录、权限验证、请求重定向、输入输出数据处理等通用任务。1. Filter 接口的实现标准的 Filter 接口主要包含三个方法：init(FilterConfig config)：初始化过滤器。doFilter(ServletRequest request, ServletResponse response, FilterChain chain)：执行过滤器的核心逻辑，并将请求传递给下一个过滤器或目标 Servlet。destroy()：销毁过滤器，释放资源。2. 日志过滤器的实现下面实现一个简单的日志过滤器，用于记录每个请求的访问时间和请求路径。package com.daicy.minitomcat; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Date; public class LoggingFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化操作，如果有需要可以在这里读取配置参数等 } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { long startTime = System.currentTimeMillis(); HttpServletRequest request = (HttpServletRequest) servletRequest; System.out.println("Request started at: " + startTime + " for path: " + request.getRequestURI()); // 将请求传递给下一个过滤器或目标 Servlet filterChain.doFilter(servletRequest, servletResponse); long endTime = System.currentTimeMillis(); System.out.println("Request completed at: " + endTime + " for path: " + request.getRequestURI() + " Took: " + (endTime - startTime) + "ms"); } @Override public void destroy() { // 清理资源操作 } }在 LoggingFilter 中，doFilter() 方法记录了请求的开始时间、路径，并通过 chain.doFilter() 将请求传递给链中的下一个节点。过滤器执行完毕后，记录结束时间及总耗时。3. FilterChain 接口的实现为了实现过滤器的链式调用，我们需要设计一个 FilterChain 结构。它能够依次调用过滤器列表中的每个 Filter，直到最后一个过滤器或者目标 Servlet 被调用。FilterChain 设计概念FilterChain 是责任链模式的实现，每个过滤器执行完后，调用链中的下一个过滤器。doFilter 方法将在链中的每个过滤器之间传递请求和响应对象。一旦所有过滤器执行完，FilterChain 会将请求传递给实际的 Servlet 处理。FilterChain 代码实现首先定义接口：public interface FilterChain { // 执行链中的下一个过滤器或目标处理 void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException; }接下来是实现类。我们需要在内部维护过滤器列表，每次调用 doFilter 方法时，依次执行过滤器链中的过滤器，直到最终的 Servlet 被调用。package com.daicy.minitomcat.servlet; import javax.servlet.*; import java.io.IOException; import java.util.LinkedList; import java.util.List; /** * @author: create by daichangya * @version: v1.0 * @description: com.daicy.minitomcat.servlet * @date: 19-11-12 */ public class FilterChainImpl implements FilterChain { private List<Filter> filters; // 存储过滤器的列表 private int currentIndex = 0; // 当前过滤器的索引 public FilterChainImpl(List<Filter> filters) { this.filters = filters; } @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { // 如果当前索引小于过滤器链的长度，执行下一个过滤器 if (currentIndex < filters.size()) { Filter filter = filters.get(currentIndex); currentIndex++; // 递增索引，指向下一个过滤器 filter.doFilter(request, response, this); // 递归调用，传入下一个过滤器 } } }4. FilterManager 代码FilterManager 负责创建过滤器链，并传递请求和响应给链中的过滤器。它将 FilterChain 作为一个链式调用对象传递给每个过滤器。package com.daicy.minitomcat; import com.daicy.minitomcat.servlet.FilterChainImpl; import javax.servlet.*; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class FilterManager { private List<Filter> filters = new ArrayList<>(); // 添加过滤器 public void addFilter(Filter filter) { filters.add(filter); } // 执行过滤器链 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterChain defaultChain = new FilterChainImpl(filters); defaultChain.doFilter(request, response); // 递归执行过滤器链 } public List<Filter> getFilters() { return filters; } }9.2 监听器（Listener）Listener 是 Servlet 容器提供的另一种机制，用于监听容器内的特定事件。监听器用于捕捉和响应 Servlet 上下文、会话或请求的生命周期变化。例如，可以用来监听会话的创建和销毁，或者在应用启动时初始化共享资源。1. ServletContextListener 接口ServletContextListener 接口用于监听 Servlet 上下文的生命周期事件。当 Servlet 容器启动或销毁时，会触发对应的事件。package com.daicy.minitomcat; import javax.servlet.*; public class ServletContextListenerImpl implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent servletContextEvent) { ServletContext servletContext = servletContextEvent.getServletContext(); System.out.println("Servlet context initialized."); // 在这里可以进行初始化操作，如加载配置文件等 servletContext.setAttribute("initialized", true); } @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { ServletContext servletContext = servletContextEvent.getServletContext(); System.out.println("Servlet context destroyed."); // 在这里可以进行资源清理操作，如关闭数据库连接池等 servletContext.removeAttribute("initialized"); } }contextInitialized() 方法在 Servlet 上下文初始化时调用，contextDestroyed() 方法在 Servlet 上下文销毁时调用。上述代码简单打印了日志，表示 Servlet 上下文的生命周期状态。2. HttpSessionListener 接口HttpSessionListener 接口用于监听 HTTP 会话的创建和销毁。当用户的会话创建或销毁时，会触发对应的事件。package com.daicy.minitomcat; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; public class HttpSessionListenerImpl implements HttpSessionListener { @Override public void sessionCreated(HttpSessionEvent httpSessionEvent) { HttpSession session = httpSessionEvent.getSession(); System.out.println("Session created with ID: " + session.getId()); // 可以在这里进行与新会话相关的初始化操作，如创建会话相关的缓存 session.setAttribute("created", true); } @Override public void sessionDestroyed(HttpSessionEvent httpSessionEvent) { HttpSession session = httpSessionEvent.getSession(); System.out.println("Session destroyed with ID: " + session.getId()); // 可以在这里清理与该会话相关的资源 session.removeAttribute("created"); } }sessionCreated() 方法在会话创建时调用，sessionDestroyed() 方法在会话销毁时调用。3. 注册监听器我们需要通过管理器注册实现的监听器，以便容器能够触发相应的事件。servletContextListenerManager.addListener(new ServletContextListenerImpl()); sessionListenerManager.addListener(new HttpSessionListenerImpl());上述配置分别监听 Servlet 上下文和 HTTP 会话的生命周期事件。9.3 学习收获通过实现 Filter 和 Listener，我们深入理解了 Servlet 规范中的两大重要机制：Filter（过滤器）：允许我们在请求处理链中插入额外的处理逻辑，如日志记录、认证验证、数据修改等。通过实现 Filter 接口，我们能够自定义请求处理的前后逻辑。Listener（监听器）：使我们能够监听 Servlet 上下文、会话和请求的生命周期事件。通过实现 Listener 接口，我们能够在特定的生命周期阶段（如应用启动、会话创建等）执行特定的逻辑。这两者的结合，使得我们能够对请求处理流程进行灵活的扩展和控制，并实现更加复杂和丰富的功能。这对于开发者来说，是一个非常强大的扩展点，能够让我们轻松地插入横切逻辑（如日志、权限验证、性能监控等），同时也能方便地管理应用生命周期事件。项目源代码地址：https://github.com/daichangya/MiniTomcat/tree/chapter9/mini-tomcat说明：本文代码基于 javax.servlet 包（Servlet 3.x/4.x 规范）。若使用 Jakarta EE 9 及以上版本，包名已变更为 jakarta.servlet，请注意兼容性问题。
第十四章：实现 Log 模块功能目标与实现内容本章旨在为 MiniTomcat 实现一个基础的 Log 模块，主要目标包括：日志记录：支持记录请求日志、错误日志和系统日志。级别管理：支持不同的日志级别（INFO、DEBUG、ERROR 等），以便控制日志的详细程度。实现方式：设计一个简单的 Logger 接口及实现类，提供不同级别的日志输出，并支持配置输出格式和文件路径。14.1 日志记录的重要性在 Web 应用开发中，日志记录是至关重要的环节。日志帮助我们跟踪系统的运行状态、诊断问题，并提供可用的监控信息。常见的日志类型包括：请求日志：记录每个 HTTP 请求的相关信息。错误日志：记录程序异常和错误信息。系统日志：记录系统级别的信息，如服务启动、停止等事件。为了更高效地记录日志，我们需要将日志分为不同的级别，并根据日志级别来控制日志内容的输出。14.2 日志级别的设计常见的日志级别如下，我们可以通过控制日志级别来决定输出多少日志信息，避免在生产环境中输出过多的调试信息：DEBUG：最详细的日志，用于调试阶段，记录系统的详细信息。INFO：常规信息，记录系统的正常操作，如请求处理过程等。WARN：警告信息，用于记录可能出现问题的地方，但不一定会导致错误。ERROR：错误信息，用于记录异常或错误，系统无法继续运行的情况。FATAL：严重错误，通常导致系统终止。14.3 实现 Logger 类实现步骤1. 定义日志级别枚举首先定义日志级别的枚举类：public enum LogLevel { DEBUG, INFO, WARN, ERROR, FATAL }2. 创建日志记录器接口定义 Logger 接口，规范日志记录的行为：package com.daicy.minitomcat.log; public interface Logger { void log(LogLevel level, String message); void log(LogLevel level, String message, Throwable throwable); // debug 级别日志 void debug(String message); // info 级别日志 void info(String message); // warn 级别日志 void warn(String message); // error 级别日志 void error(String message); }3. 创建抽象日志类创建 AbstractLogger 抽象类，实现公共逻辑（如级别判断、时间格式化）：package com.daicy.minitomcat.log; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public abstract class AbstractLogger implements Logger { final LogLevel minLogLevel; protected AbstractLogger(LogLevel minLogLevel) { this.minLogLevel = minLogLevel; } private String throwableToString(Throwable throwable) { StringBuilder sb = new StringBuilder(); for (StackTraceElement element : throwable.getStackTrace()) { sb.append("\tat ").append(element).append("\n"); } return sb.toString(); } String getCurrentTime() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); return LocalDateTime.now().format(formatter); } @Override public void log(LogLevel level, String message, Throwable throwable) { if (level.ordinal() >= minLogLevel.ordinal()) { String logMessage = message + "\n" + throwableToString(throwable); log(level, logMessage); } } // debug 级别日志 @Override public void debug(String message) { log(LogLevel.DEBUG, message); } // info 级别日志 @Override public void info(String message) { log(LogLevel.INFO, message); } // warn 级别日志 @Override public void warn(String message) { log(LogLevel.WARN, message); } // error 级别日志 @Override public void error(String message) { log(LogLevel.ERROR, message); } }注意：AbstractLogger 未直接实现 log(LogLevel level, String message) 方法，因为该类是抽象类，具体实现将由子类（如 ConsoleLogger 或 FileLogger）完成。4. 实现基础日志类（控制台）创建 ConsoleLogger，将日志输出到控制台：package com.daicy.minitomcat.log; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class ConsoleLogger extends AbstractLogger { public ConsoleLogger(LogLevel minLogLevel) { super(minLogLevel); } @Override public void log(LogLevel level, String message) { if (level.ordinal() >= minLogLevel.ordinal()) { System.out.println(formatLog(level, message)); } } @Override public void log(LogLevel level, String message, Throwable throwable) { if (level.ordinal() >= minLogLevel.ordinal()) { System.out.println(formatLog(level, message)); throwable.printStackTrace(System.out); } } private String formatLog(LogLevel level, String message) { String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return String.format("[%s] [%s] %s", timestamp, level, message); } }5. 增加文件日志支持扩展 FileLogger，将日志保存到文件：package com.daicy.minitomcat.log; import java.io.FileWriter; import java.io.IOException; public class FileLogger extends AbstractLogger { private final String logFilePath; public FileLogger(LogLevel minLogLevel, String logFilePath) { super(minLogLevel); this.logFilePath = logFilePath; } @Override public void log(LogLevel level, String message) { if (level.ordinal() >= minLogLevel.ordinal()) { String logMessage = String.format("[%s] [%s] %s", getCurrentTime(), level, message); // 输出到文件 try (FileWriter writer = new FileWriter(logFilePath, true)) { writer.write(logMessage + "\n"); } catch (IOException e) { e.printStackTrace(); } } } }6. 创建日志管理器日志管理器 LogManager 用于统一管理日志记录器，支持多日志源输出：package com.daicy.minitomcat.log; import java.util.ArrayList; import java.util.List; public class LogManager { private static final List<Logger> loggers = new ArrayList<>(); public static void addLogger(Logger logger) { loggers.add(logger); } public static void log(LogLevel level, String message) { for (Logger logger : loggers) { logger.log(level, message); } } public static void log(LogLevel level, String message, Throwable throwable) { for (Logger logger : loggers) { logger.log(level, message, throwable); } } // debug 级别日志 public static void debug(String message) { log(LogLevel.DEBUG, message); } // info 级别日志 public static void info(String message) { log(LogLevel.INFO, message); } // warn 级别日志 public static void warn(String message) { log(LogLevel.WARN, message); } // error 级别日志 public static void error(String message) { log(LogLevel.ERROR, message); } }7. 集成日志模块在 MiniTomcat 启动时，添加统一的日志调用配置：LogManager.addLogger(new ConsoleLogger(LogLevel.INFO)); LogManager.addLogger(new FileLogger(LogLevel.DEBUG, "mini-tomcat.log"));在需要记录日志的地方调用：LogManager.log(LogLevel.INFO, "MiniTomcat started"); LogManager.log(LogLevel.ERROR, "Error handling request", exception);测试日志模块启动 MiniTomcat，观察控制台是否输出正确的日志。查看日志文件内容，确保日志条目正确写入。修改日志级别，验证不同级别的日志过滤效果。优化与扩展异步日志：使用线程池异步写入日志，提升性能。日志分割：按日期或文件大小分割日志文件。可配置性：从外部配置文件读取日志设置，例如日志级别、输出路径等。通过以上步骤，我们成功为 MiniTomcat 构建了一个灵活、高效的日志模块。14.4 配置和使用 Logger在实际使用中，我们可以通过 LogManager 记录 Web 容器中的各种日志信息。14.4.1 配置和使用示例以下是一个 Filter 示例，用于记录请求的处理时间：package com.daicy.minitomcat; import com.daicy.minitomcat.log.LogManager; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; public class LoggingFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化操作，如果有需要可以在这里读取配置参数等 } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { long startTime = System.currentTimeMillis(); HttpServletRequest request = (HttpServletRequest) servletRequest; LogManager.info("Request started at: " + startTime + " for path: " + request.getRequestURI()); filterChain.doFilter(servletRequest, servletResponse); long endTime = System.currentTimeMillis(); LogManager.info("Request completed at: " + endTime + " for path: " + request.getRequestURI() + " Took: " + (endTime - startTime) + "ms"); } @Override public void destroy() { // 清理资源操作 } }在这个示例中，我们使用了 LogManager 进行日志记录。假设控制台日志级别设置为 INFO，这意味着 DEBUG 级别的日志将不会输出，而 INFO、WARN 和 ERROR 级别的日志会被输出。输出结果（控制台和 mini-tomcat.log 文件中）如下：[2024-11-23 15:39:00] [INFO] Servlet context initialized. [2024-11-23 15:39:00] [INFO] HelloServlet initialized. [2024-11-23 15:39:00] [INFO] HTTP Connector is running on port 8080 [2024-11-23 15:39:07] [INFO] Accepted connection from /0:0:0:0:0:0:0:1 [2024-11-23 15:39:07] [INFO] Accepted connection from /0:0:0:0:0:0:0:1 [2024-11-23 15:39:07] [INFO] Session created with ID: afeb595f-354d-4b6a-a136-f11ba15f4bb2 [2024-11-23 15:39:07] [INFO] LogValve: Logging request /hello [2024-11-23 15:39:07] [INFO] Request started at: 1732347547912 for path: /hello [2024-11-23 15:39:07] [INFO] Request completed at: 1732347547912 for path: /hello Took: 0ms [2024-11-23 15:39:40] [INFO] Server stopping... [2024-11-23 15:39:40] [INFO] HelloServlet destroyed. [2024-11-23 15:39:40] [INFO] Servlet context destroyed.14.5 日志管理和扩展14.5.1 日志级别管理在生产环境中，我们通常会将日志级别设置为 INFO 或更高的级别，这样可以避免过多的调试信息输出。而在开发和调试过程中，DEBUG 级别的日志有助于我们进行问题排查。14.5.2 日志文件管理为了防止日志文件过大，我们可以定期轮换日志文件。例如，使用时间戳或文件大小来分割日志文件。日志文件的管理可以通过日志框架（如 Log4j 或 SLF4J）来实现，本示例中为简化实现未包含此功能。14.5.3 日志的多线程安全如果系统是多线程的，日志输出可能会受到线程竞争的影响。可以通过同步方法或使用线程安全的日志框架来确保日志输出的正确性。本示例中的 FileWriter 在多线程环境下可能需要额外的同步处理。14.6 学习收获通过实现日志模块，我们学习了以下内容：日志级别控制：掌握了日志级别控制的原理，理解了如何根据不同的日志级别输出不同的日志信息。日志输出到文件和控制台：实现了日志的双重输出，既能在控制台显示，也能记录到日志文件中，方便后期分析。日志模块的可扩展性：通过 Logger 类的设计，我们可以灵活地扩展日志模块，支持更多功能，如日志轮转、异步日志等。通过日志模块的实现，我们为 Web 容器的调试、监控和运维提供了基础支持。说明：本文代码示例使用了 java.time 包，适用于 Java 8 及以上版本。在生产环境中，建议直接使用成熟的日志框架（如 Logback、Log4j2）以获得更好的性能和功能支持。