本章将实现对 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：深入理解 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 版本进行调整。
编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。如何正确使用 NIO 构建网络服务器一直是值得深入探讨的问题。为此，我们分析了 Jetty、Tomcat 和 Mina 有关 NIO 的源码，发现三者基于类似的方式实现。这应该算是 NIO 构架网络服务器的经典模式。基于这种模式，我们编写了一个小型网络服务器，经过压力测试，效果良好。下文将具体分析这三者是如何使用 NIO 的。Jetty Connector 的实现先看看有关类图：其中各组件职责如下：SelectChannelConnector：负责组装各组件SelectSet：负责侦听客户端请求SelectChannelEndPoint：负责 IO 的读和写HttpConnection：负责逻辑处理在整个服务端处理请求的过程可以分为三个阶段，时序图如下所示：阶段一：监听并建立连接这一过程主要是启动一个线程负责 accept 新连接，监听到后分配给相应的 SelectSet，分配的策略就是轮询。阶段二：监听客户端的请求这一过程主要是启动多个线程（线程数一般为服务器 CPU 的个数），让 SelectSet 监听所管辖的 channel 队列。每个 SelectSet 维护一个 Selector，这个 Selector 监听队列里所有的 channel，一旦有读事件，从线程池里拿线程去做处理请求。阶段三：处理请求这一过程就是每次客户端请求的数据处理过程。值得注意的是，为了不让后端的业务处理阻碍 Selector 监听新的请求，采用多线程来分隔开监听请求和处理请求两个阶段。由此可以大致总结出 Jetty 有关 NIO 使用的模式，如下图所示：最核心的设计就是把三件不同的事情隔离开，并用不同规模的线程去处理，最大限度地利用 NIO 的异步和通知特性。Tomcat Connector 的实现下面再来看看 Tomcat 是如何使用 NIO 来构架 Connector 这块的。先看看 Tomcat Connector 这块的类图：其中各组件职责如下：NioEndpoint：负责组装各部件Acceptor：负责监听新连接，并把连接交给 PollerPoller：负责监听所管辖的 channel 队列，并把请求交给 SocketProcessor 处理SocketProcessor：负责数据处理，并把请求传递给后端业务处理模块在整个服务端处理请求的过程可以分为三个阶段，时序图如下所示：阶段一：监听并建立连接这一阶段主要是 Acceptor 监听新连接，并轮询取一个 Poller，把连接交付给 Poller。阶段二：监听客户端的请求这一过程主要是让每个 Poller 监听所管辖的 channel 队列，select 到新请求后交付给 SocketProcessor 处理。阶段三：处理请求这一过程就是从多线程执行 SocketProcessor，做数据和业务处理。于是我们发现，抛开具体代码细节，Tomcat 和 Jetty 在 NIO 的使用方面是非常一致的，采用的模式依然是下图：Mina 的实现最后我们再看看 NIO 方面最著名的框架 Mina。抛开 Mina 有关 session 和处理链条等方面的设计，单单挑出前端网络层处理来看，也采用的是与 Jetty 和 Tomcat 类似的模式。只不过它做了些简化，没有隔开请求侦听和请求处理两个阶段，因此宏观上看它只分为两个阶段。先看看它的类图：其中各组件职责如下：SocketAcceptor：起线程调用 SocketAcceptor.Work 负责新连接侦听，并交给 SocketIoProcessor 处理SocketIoProcessor：起线程调用 SocketIoProcessor.Work 负责侦听所管辖的 channel 队列，select 到新请求后交给 IoFilterChain 处理IoFilterChain：组装了 Mina 的处理链条在整个服务端处理请求的过程可以分为两个阶段，时序图如下所示：阶段一：监听并建立连接阶段二：监听并处理客户端的请求总结与实践总结来看 Jetty、Tomcat 和 Mina，我们大概清楚了该如何基于 NIO 来构架网络服务器。通过这个提炼出来的模式，我们写了个很简单的 NIO Server。在保持连接的情况下，可以很轻松地保持 6 万连接（由于有 65535 连接限制），并能在负载只有 3 左右的情况下（4 核），承担 3 到 4 万的 TPS 请求（当然做的事情很简单，仅仅是把 buffer 转化为自定义协议的包，然后再把包转为 buffer 写到客户端）。简单地实践一下可以证明这个模式的有效性。不妨再看看这个图，希望对大伙以后写 server 有用：说明：本文内容基于 2011 年左右的技术背景整理（参考图片链接时间戳），文中涉及的 Jetty、Tomcat 及 Mina 版本架构可能已与当前最新版本存在差异。现代 NIO 框架（如 Netty）及 JDK NIO.2 已有更多演进，实际开发请以官方最新文档为准。
门面设计模式门面设计模式（Facade Pattern）在 Tomcat 中有多处应用，例如 Request 和 Response 对象的封装、Standard Wrapper 到 ServletConfig 的封装、以及 ApplicationContext 到 ServletContext 的封装等。门面设计模式的原理门面设计模式的核心作用在于封装。顾名思义，就是将复杂的内部结构封装成一个统一的“门面”，以便与其他模块更便捷地进行交互，类似于国家外交部统一对外接口的角色。该模式主要应用于由多个子系统组成的大型系统中。子系统之间必然涉及相互通信，但每个子系统不应将内部数据过多地暴露给其他系统，否则划分子系统的意义将大打折扣。因此，每个子系统都会设计一个门面，将外部感兴趣的数据封装起来，并通过这个门面提供访问接口。这就是门面设计模式存在的意义。门面设计模式示意图如下：客户端（Client）仅能访问 Facade 提供的数据，这是门面设计模式的关键所在。至于 Client 如何访问 Facade，以及 Subsystem 如何提供 Facade，该模式并没有严格规定。Tomcat 的门面设计模式示例Tomcat 中包含众多不同组件，组件间需要相互交互数据，使用门面模式隔离数据是一种有效的方法。以下是 Request 对象上使用的门面设计模式示例：从类图可以看出，HttpRequestFacade 类封装了 HttpRequest 接口能够提供的数据。通过 HttpRequestFacade 访问到的数据都被代理到 HttpRequest 中。通常被封装的对象会被设为 Private 或者 Protected 访问修饰符，以防止在 Facade 之外被直接访问。观察者设计模式观察者设计模式（Observer Pattern）是一种常用的设计方法，通常也称为发布 - 订阅模式（Publish-Subscribe Pattern）或事件监听机制。它通常在某个事件发生的前后触发一些特定操作。观察者模式的原理观察者模式的原理相对简单：当主体（Subject）状态发生变化时，依赖该主体的观察者（Observer）会收到通知并执行相应操作。观察者必须向主体注册（登记），否则主体无法通知它。观察者模式通常包含以下几个角色：Subject（抽象主题）：负责管理所有观察者的引用，同时定义主要的事件操作。ConcreteSubject（具体主题）：实现抽象主题定义的所有接口，当自身状态发生变化时，通知所有观察者。Observer（观察者）：定义监听主题发生变化时的相应操作接口。Tomcat 的观察者模式示例Tomcat 中多处使用了观察者模式。前面提到的控制组件生命周期的 Lifecycle 接口就是这种模式的体现，此外 Servlet 实例的创建、Session 的管理、Container 的管理等都采用了同样的原理。下面主要看一下 Lifecycle 的具体实现。Lifecycle 的观察者模式结构图如下：在上述结构图中：LifecycleListener 代表抽象观察者，它定义了一个 lifecycleEvent 方法，这是当主题变化时要执行的方法。ServerLifecycleListener 代表具体观察者，它实现了 LifecycleListener 接口的方法。Lifecycle 接口代表抽象主题，它定义了管理观察者的方法以及其他相关方法。StandardServer 代表具体主题，它实现了抽象主题的所有方法。Tomcat 对观察者模式做了扩展，增加了两个辅助类：LifecycleSupport 和 LifecycleEvent。LifecycleEvent 使得可以定义事件类别，不同的事件可区别处理，更加灵活。LifecycleSupport 类代理了主题对多观察者的管理，将这个管理逻辑抽出来统一实现。以后如果需要修改，只需修改 LifecycleSupport 类，而不需要修改所有具体主题，因为所有具体主题对观察者的操作都被代理给了 LifecycleSupport 类。这可以认为是观察者模式的一种改进版。LifecycleSupport 调用观察者的方法代码如下：public void fireLifecycleEvent(String type, Object data) { LifecycleEvent event = new LifecycleEvent(lifecycle, type, data); LifecycleListener interested[] = null; synchronized (listeners) { interested = (LifecycleListener[]) listeners.clone(); } for (int i = 0; i < interested.length; i++) { interested[i].lifecycleEvent(event); } }主题是如何通知观察者的呢？请看以下代码示例：public void start() throws LifecycleException { lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; synchronized (services) { for (int i = 0; i < services.length; i++) { if (services[i] instanceof Lifecycle) { ((Lifecycle) services[i]).start(); } } } lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); }命令设计模式在 Tomcat 架构中，核心组件 Connector 和 Container 的交互体现了命令设计模式。Connector 将接收到的请求封装为命令，传递给 Container 进行处理。命令模式的原理命令模式（Command Pattern）的主要作用是将命令封装，把发出命令的责任和执行命令的责任分离开，实现功能分工。不同的模块可以对同一个命令做出不同的解释。命令模式通常包含以下几个角色：Client（客户端）：创建一个命令，并决定接受者。Command（命令）：命令接口，定义一个抽象方法。ConcreteCommand（具体命令）：负责调用接受者的相应操作。Invoker（请求者）：负责调用命令对象执行请求。Receiver（接受者）：负责具体实施和执行一次请求。Tomcat 中的命令模式示例Tomcat 作为应用服务器，会接收到很多请求，如何分配和执行这些请求是核心功能之一。命令模式在 Connector 和 Container 组件之间得到了体现。以下是 Tomcat 命令模式的结构图：在此结构中：Connector 作为抽象请求者，HttpConnector 作为具体请求者。HttpProcessor 作为命令。Container 作为命令的抽象接受者，ContainerBase 作为具体的接受者。客户端即应用服务器 Server 组件。Server 首先创建命令请求者 HttpConnector 对象，然后创建命令 HttpProcessor 对象，再把命令对象交给命令接受者 ContainerBase 容器来处理。命令最终是被 Tomcat 的 Container 执行的。命令可以以队列的方式进入，Container 也可以以不同的方式来处理请求，例如 HTTP/1.0 协议和 HTTP/1.1 的处理方式会有所不同。责任链模式责任链模式（Chain of Responsibility Pattern）是 Tomcat 中最容易发现的设计模式之一，也是 Tomcat 中 Container 设计的基础。整个容器通过一条链连接在一起，这条链一直将请求正确地传递给最终处理请求的 Servlet。责任链模式的原理责任链模式将很多对象通过每个对象对其下家的引用连接起来形成一条链。请求在这条链上传递，直到链上的某个对象处理此请求；或者每个对象都可以处理请求，并传给下一家，直到最终链上每个对象都处理完。这样可以在不影响客户端的情况下，在链上增加任意的处理节点。通常责任链模式包含以下几个角色：Handler（抽象处理者）：定义一个处理请求的接口。ConcreteHandler（具体处理者）：处理请求的具体类，或者传给下家。Tomcat 中责任链模式示例在 Tomcat 中，这种设计模式几乎被完整地使用。Tomcat 的容器设置就是责任链模式，从 Engine 到 Host，再到 Context，一直到 Wrapper，都是通过一个链传递请求。Tomcat 中责任链模式的类结构图如下：上图基本描述了四个子容器使用责任链模式的类结构。对应的责任链模式角色中，Container 扮演抽象处理者角色，具体处理者由 StandardEngine 等子容器扮演。与标准的责任链不同的是，这里引入了 Pipeline 和 Valve 接口。它们有什么作用呢？实际上，Pipeline 和 Valve 扩展了这个链的功能，使得在链往下传递过程中，能够接受外界的干预。Pipeline 如同连接每个子容器的管道，里面传递的 Request 和 Response 对象好比管子里流的水；而 Valve 则是这个管子上开的一个个小口子（阀门），让你有机会能够接触到里面的水，做一些额外的事情。为了防止水被引出来而不能流到下一个容器中，每一段管子最后总有一个节点保证它一定能流到下一个子容器。所以每个容器都有一个 StandardXXXValve。只要涉及到这种链式处理流程，这是一个非常值得借鉴的模式。说明：本文涉及的部分类名（如 HttpConnector、HttpProcessor）主要基于 Tomcat 5/6 版本架构。在 Tomcat 7 及更高版本中，Connector 和 Container 的内部实现有所调整（例如移除了 HttpConnector），但整体设计模式思想（门面、观察者、命令、责任链）依然适用。
引言在前端优化的第一部分中，主要讲解了对静态资源的优化措施，包括图片压缩、CSS Sprites 技术、GZIP 压缩等。本文作为系列的第二部分，将深入讲解前端优化中关键的 Flush 机制、动静分离、HTTP 持久连接、HTTP 协议的灵活应用以及 CDN 等技术。结合这些技术策略，旨在进一步提升 Java Web 应用程序的整体性能。Flush 机制的使用在 Web 技术中，Flush 机制并非新概念。其核心思想是无需等待网页内容全部加载完毕再一次性写回客户端，而是可以分部分逐次返回。对于内容较大的网页，一次性写回全部内容会导致用户长时间面对空白页面，体验较差。Flush 机制允许开发人员将网页内容按文档流顺序逐步返回给客户端，使用户感知到系统正在工作，从而提升等待过程中的用户体验。这是一个经典的优化手段，至今仍在广泛使用。在 Java Web 技术中，实现 Flush 非常简单。只需调用 HttpServletResponse.getWriter() 输出流的 flush() 方法，即可将已经加载完成的内容写回给客户端。然而，是否每个网页都需要使用该技术？并不建议。将网页内容加载完毕后一次性返回客户端也有其优势。网络传输存在最大传输单元（MTU），内容加载完毕后一次性输出可以最大程度地利用传输带宽，减少分块和传输次数。换言之，Flush 机制可能会略微增加用户等待时间和浏览器渲染时间。但对于大型网页而言，牺牲这点效率来换取用户体验的提升，通常是值得的。动静分离所谓的动静分离，就是将 Web 应用程序中的静态内容和动态内容分别部署在不同的 Web 服务器上，针对性地处理不同类型的请求，从而达到性能提升的目的。本文基于 Java Web 环境讲解优化，主流服务器软件为 Tomcat。需要注意的是，Tomcat 在高并发和静态资源处理能力上相对较弱。既然选择了 Java Web，我们就应通过架构优化来弥补不足。动静分离便是其中一种有效方法：将静态资源的处理任务交给更擅长的软件，而让 Tomcat 专注于处理 JSP/Servlet 动态请求。服务器软件选择对于静态资源处理，可以选择 Nginx。这是一款高性能的 HTTP 和反向代理服务器，支持高并发，对静态资源的处理能力较强。事实上，动静分离的方案多种多样：有人采用 Apache+Tomcat 组合，也有人使用 Tomcat+Tomcat 组合（部署于不同主机或域名）。其中 Apache+Tomcat 方案与 Nginx 方案原理相似，均基于反向代理。相比之下，Nginx 配置动静分离更为简洁，而 Apache 则需要配置 mod_proxy 模块。Apache 配置示例在 Apache 中，mod_proxy 模块负责反向代理的实现。核心配置内容如清单 1所示（该配置源自实际项目经验）。清单 1. 动静分离的 Apache 核心配置<Proxy balancer://proxy> BalancerMember http://192.168.1.178:8080 loadfactor=1 BalancerMember http://192.168.1.145:8080 loadfactor=1 </Proxy> NameVirtualHost *:80 <VirtualHost *:80> ServerAdmin service@xuanli365.com ServerName www.xuanli365.com DocumentRoot /www DirectoryIndex index.shtml <Directory /www> AllowOverride All AddType text/html .shtml AddType application/x-rar .rar AddHandler server-parsed .shtml Options +IncludesNOEXEC </Directory> RewriteEngine on ProxyRequests Off ProxyPass /static/! ProxyPass / balancer://proxy/ ProxyPassReverse / balancer://proxy/ ProxyPreserveHost on </VirtualHost>根据 Apache 官方对 mod_proxy 模块的介绍，ProxyPass 属性可以将远端服务器映射到本地服务器的 URL 空间中，实现地址映射。在清单 1的配置中：当访问路径不在 /static/ 下时（! 表示非），请求转发给后端服务器（即 Tomcat）。当访问路径在 /static/ 下时，直接访问本机。例如，访问 www.xuanli365.com/static/css/index.css 时，实际处理请求的是 Apache 服务器；而访问 www.xuanli365.com/index.jsp 时，Apache 会将请求转发到后端的 Tomcat 服务器（如 http://192.168.1.178:8080/index.jsp）。这就实现了动静分离。此外，该配置中也包含了简单的负载均衡（通过 loadfactor 因子）。大型网站案例分析打开任意大型门户网站（如腾讯网站），查看图片资源地址，可能会发现类似以下域名：http://mat1.gtimg.com/www/iskin960/qqcomlogo.pnghttp://img1.gtimg.com/v/pics/hv1/95/225/832/54158270.jpg可见该网站存放图片资源使用了多个域名。使用 Linux 的 host 命令查看这两个域名的 IP 地址，结果如图 1所示。图 1. 某网站的动静分离通过查看 IP 地址可以发现，这些图片很可能存放在不同的主机上（注：一个主机可以拥有多个 IP）。图片内容和网页的动态内容不在同一 IP 下，这也是动静分离的体现。使用多个域名可以增加浏览器的并发下载数，从而提高下载效率。本文演示架构本文采用另一种策略对动静分离进行演示，大致结构如图 2所示。图 2. 本文设计的动静分离结构在该架构中：A 主机：存放静态资源，安装 Nginx。B 主机：存放动态程序，安装 Tomcat。配置 Nginx 后，当请求的是 html、jpg 等静态资源时，直接访问 A 主机上的静态资源目录；当用户提出动态资源请求时，则将请求转发到后端的 B 服务器，交由 Tomcat 处理，再由 Nginx 将结果返回给客户端。可能会有疑问：动态请求要先访问 A，A 转发访问 B，再由 B 返回结果给 A，A 最后又将结果返回给客户端，是否多余？初看的确如此，但这种结构至少有两点好处：为负载均衡做准备：随着系统发展，单台 B 主机处理动态请求可能不足。基于图 2 的结构，可以直接扩展 B1、B2 等节点，只需修改 Nginx 配置即可实现负载均衡。便于程序开发与维护：基于该结构，程序代码中只需写相对路径（如 ），无需关心具体资源服务器的地址。若静态资源扩展到其他服务器，只需调整 Nginx 配置，无需修改代码。Nginx 配置示例按照图 2 所示的架构图，安装好 Nginx 和 Tomcat 后，对 nginx.conf 进行配置。与本文该部分相关的配置如清单 2所示。清单 2. 动静分离的 Nginx 配置# 转发的服务器，upstream 为负载均衡做准备 upstream tomcat_server { server 192.168.1.117:8080; } server { listen 9090; server_name localhost; index index.html index.htm index.jsp; charset koi8-r; # 静态资源存放目录 root /home/wq243221863/Desktop/ROOT; access_log logs/host.access.log main; # 动态请求的转发 location ~ .*.jsp$ { proxy_pass http://tomcat_server; proxy_set_header Host $host; } # 静态请求直接读取 location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|css)$ { expires 30d; } …… }清单 2配置简洁明了：动态请求（以 .jsp 结尾）发到 B 主机（192.168.1.117:8080），静态请求（gif|jpg 等）则直接访问定义的 root 目录。测试环境搭建接下来在 Tomcat 中新建 Web 项目，添加一个 test.jsp 文件，目录结构如图 3所示。图 3. B 上的测试项目结构定义一张测试用的静态图片，放置在 A 主机的桌面 ROOT/seperate 目录下。结构如图 4所示。图 4. A 上的静态资源文件夹结构注意：这里的 separate 目录名是与 B 的项目文件夹同名的。查看图 3 中 test.jsp 的源码，如清单 3所示。清单 3. test.jsp 源码<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="java.util.Date" %> <%@ page import="java.text.SimpleDateFormat" %> 动静分离的测试 这是动态脚本处理的结果 <% // 这是一段测试的动态脚本 Date now=new Date(); SimpleDateFormat f=new SimpleDateFormat("现在是"+"yyyy 年 MM 月 dd 日 E kk 点 mm 分"); %> <%=f.format(now)%> 这是静态资源的请求结果 清单 3是一个非常简单的 JSP 页面，主要使用 img 标签来访问 jquery.gif。已知 test.jsp 在 B 服务器上，而 jquery.gif 在 A 服务器上。用于访问 jquery.gif 的代码里不需要指定 A 的地址，而是直接使用相对路径，就好像该图片也在 B 上一样，这就是本结构的优点。在 A 上访问 test.jsp 文件，结果如图 5所示。图 5. test.jsp 的结果配置顺利，完全按照预期实现了动静分离。性能测试对比初步完成动静分离配置后，我们需要验证其性能提升效果。将 Tomcat 服务器也迁移到 A 服务器上，同时将 jquery.gif 拷贝一份到 separate 项目目录下，图 3 的结构变为图 6所示。图 6. 拷贝 jquery.gif 的 separate 项目将 Tomcat 端口设置为 8080，Nginx 端口依然是 9090。现在：访问 http://localhost:9090/separate/test.jsp（未使用动静分离，静态资源由 Tomcat 处理）。访问 http://localhost:8080/separate/test.jsp（使用了动静分离，静态资源由 Nginx 处理）。注：原文此处端口描述可能存在笔误，根据上下文逻辑，9090 应为 Nginx 代理端口，8080 为 Tomcat 直连端口。测试时分别对比 Nginx 代理下的静态资源访问与 Tomcat 直连的静态资源访问。使用 Apache 的 AB 压力测试工具，对相关 URL 分别进行压力和吞吐率测试。1. 静态资源测试首先，对静态资源（jquery.gif）的处理结果如清单 4所示。清单 4. 静态资源的 AB 测试测试脚本：ab -c 100 -n 1000 http://localhost:{port}/seperate/jquery.gif 9090 端口，也就是 Nginx 的测试结果： Concurrency Level: 100 Time taken for tests: 0.441 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 4497000 bytes HTML transferred: 4213000 bytes Requests per second: 2267.92 [#/sec] (mean) Time per request: 44.093 [ms] (mean) Time per request: 0.441 [ms] (mean, across all concurrent requests) Transfer rate: 9959.82 [Kbytes/sec] received 8080 端口，也就是 Tomcat 的测试结果： Concurrency Level: 100 Time taken for tests: 1.869 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 4460000 bytes HTML transferred: 4213000 bytes Requests per second: 535.12 [#/sec] (mean) Time per request: 186.875 [ms] (mean) Time per request: 1.869 [ms] (mean, across all concurrent requests) Transfer rate: 2330.69 [Kbytes/sec] received清单 4的测试脚本代表同时处理 100 个请求并下载 1000 次 jquery.gif 文件。关注 Requests per second（吞吐率）指标可以看出 Nginx 实现动静分离的优势：动静分离每秒可以处理 2267 个请求，而不使用则只可以处理 535 个请求。由此可见，动静分离后效率提升显著。2. 动态脚本测试动态请求的转发会导致动态脚本的处理效率降低吗？降低多少？再用 AB 工具对 test.jsp 进行测试，结果如清单 5所示。清单 5. 动态脚本的 AB 测试测试脚本：ab -c 1000 -n 1000 http://localhost:{port}/seperate/test.jsp 9090 端口，也就是 Nginx 的测试结果： Concurrency Level: 100 Time taken for tests: 0.420 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 709000 bytes HTML transferred: 469000 bytes Requests per second: 2380.97 [#/sec] (mean) Time per request: 42.000 [ms] (mean) Time per request: 0.420 [ms] (mean, across all concurrent requests) Transfer rate: 1648.54 [Kbytes/sec] received 8080 端口，也就是 Tomcat 的测试结果： Concurrency Level: 100 Time taken for tests: 0.376 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 714000 bytes HTML transferred: 469000 bytes Requests per second: 2660.06 [#/sec] (mean) Time per request: 37.593 [ms] (mean) Time per request: 0.376 [ms] (mean, across all concurrent requests) Transfer rate: 1854.77 [Kbytes/sec] received经过多次测试，得出清单 5的较为稳定的结果。可以看到在使用 Nginx 实现动静分离以后，动态请求的吞吐率的确会有所下降（因为多了一层转发）。然而，对于网站整体性能来说，静态资源的高吞吐率，以及未来可以实现的负载均衡、可扩展性、高可用性等优势，该牺牲是值得的。任何技术都是有利有弊，动静分离也不例外。选择了动静分离，就意味着选择了更为复杂的系统架构，维护成本在一定程度上会增加。但鉴于其带来的性能提升，这仍是很多系统架构师会选择的一种解决方案。HTTP 持久连接持久连接（Keep-Alive）也叫做长连接，它是一种 TCP 连接方式。连接会被浏览器和服务器所缓存，在下次连接同一服务器时，缓存的连接被重新使用。由于 HTTP 的无状态性，传统的 HTTP 通信往往是“一次性”的。持久连接减少了创建连接的开销，提高了性能。HTTP/1.1 已经支持长连接，大部分浏览器和服务器也提供了支持。要想发起长连接，服务器和浏览器必须共同合作。一方面浏览器要保持连接，另一方面服务器也不会断开连接。也就是说，双方需要进行协商，而协商机制依赖于 HTTP 协议。协商结构如图 7所示。图 7. 长连接协商浏览器在请求头部添加 Connection: Keep-Alive，告诉服务器“我支持长连接”。倘若服务器支持，则在响应头部添加 Connection: Keep-Alive，确认建立长连接。服务器还可以通过 Keep-Alive: timeout=10, max=100 头部告诉浏览器超时时间和最大请求数。Tomcat 配置在 Tomcat 里允许配置长连接。编辑 conf/server.xml 文件，配置 Connector 节点。该节点负责控制浏览器与 Tomcat 的连接，其中与长连接直接相关的有两个属性：keepAliveTimeout：表示在 Connector 关闭连接前，为另外一个请求 Keep Alive 所等待的毫秒数。默认值和 connectionTimeout 一样。maxKeepAliveRequests：表示 HTTP/1.0 Keep Alive 和 HTTP/1.1 Keep Alive / Pipeline 的最大请求数目。如果设置为 1，将会禁用 Keep Alive 和 Pipeline；如果设置为小于 0 的数，Keep Alive 的最大请求数将没有限制。也就是说在 Tomcat 里，默认长连接是打开的。当我们想关闭长连接时，只要将 maxKeepAliveRequests 设置为 1 即可。配置验证首先将 maxKeepAliveRequests 设置为 20，keepAliveTimeout 为 10000，通过 Firefox 查看请求头部（访问上面提到的 test.jsp）。结果如图 8所示。图 8. 服务器打开长连接接下来，将 maxKeepAliveRequests 设置为 1，并且重启服务器，再次请求网页后查看的结果如图 9所示。图 9. 服务器关闭长连接对比可以发现，Tomcat 关闭长连接后，在服务器的请求响应中明确标识了：Connection: close，告诉浏览器服务器并不支持长连接。性能测试长连接究竟可以带来怎样的性能提升？我们用数据说话。依然使用 AB 工具，它可以使用 -k 参数模拟浏览器使用 HTTP 的 Keep-Alive 特性。对 http://localhost:8080/seperate/jquery.gif 进行测试，结果如清单 6所示。清单 6. AB 测试长连接测试脚本：ab -k -c 1000 -n 10000 http://localhost:8080/seperate/jquery.gif 关闭长连接时： Concurrency Level: 1000 Time taken for tests: 5.067 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Keep-Alive requests: 0 Total transferred: 44600000 bytes HTML transferred: 42130000 bytes Requests per second: 1973.64 [#/sec] (mean) Time per request: 506.678 [ms] (mean) Time per request: 0.507 [ms] (mean, across all concurrent requests) Transfer rate: 8596.13 [Kbytes/sec] received 打开长连接时，maxKeepAliveRequests 设置为 50： Concurrency Level: 1000 Time taken for tests: 1.671 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Keep-Alive requests: 10000 Total transferred: 44650000 bytes HTML transferred: 42130000 bytes Requests per second: 5983.77 [#/sec] (mean) Time per request: 167.119 [ms] (mean) Time per request: 0.167 [ms] (mean, across all concurrent requests) Transfer rate: 26091.33 [Kbytes/sec] received结果令人惊讶。使用长连接和不使用长连接的性能对比，对于 Tomcat 配置的 maxKeepAliveRequests 为 50 来说，性能提升了将近 5 倍。可见服务器默认打开长连接是有原因的。HTTP 协议的合理使用很多程序员将精力专注在了技术实现上，认为性能的高低完全取决于代码的实现，却忽略了已经成型的规范、协议和工具。最典型的就是在 Web 开发上，部分开发人员没有意识到 HTTP 协议的重要性。其实，HTTP 协议可以提供另一条性能优化之路。通过简单地在 JSP 的 request 对象中添加响应头部，往往可以迅速提升程序性能。本系列文章的宗旨也在于让程序员编最少的代码，提升最大的性能。本文提出一个需求：在前面部分提到的 test.jsp 中，它的一部分功能是显示服务器的当前时间。现在我们希望这个动态网页允许被浏览器缓存。这似乎有点不合理，但在很多场景下，虽然是动态网页，却只执行一次（例如网页的主菜单存入数据库，不希望每次加载菜单都去读数据库）。浏览器缓存带来的性能提升众人皆知，而缓存过期时间、缓存删除、什么页面可以缓存等，都可以由程序员控制。只要熟悉 HTTP 协议，就可以轻松控制浏览器。访问上面提及的 test.jsp，用 Firebug 查看请求情况，发现每次请求都会重新到服务器下载内容。这不难理解，因为 test.jsp 是动态内容，每次服务器必须执行后才可以返回结果。图 10是访问当前的 test.jsp 的头部情况。图 10. 修改 test.jsp 前的访问头部情况现在我们往 test.jsp 添加清单 7的内容。清单 7. 在 test.jsp 的首部添加的代码<% SimpleDateFormat f2=new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss"); String ims = request.getHeader("If-Modified-Since"); if (ims != null) { try { Date dt = f2.parse(ims.substring(0, ims.length()-4)); if (dt.after(new Date(2009, 1, 1))) { response.setStatus(304); return; } } catch(Exception e) { } } response.setHeader("Last-Modified", f2.format(new Date(2010, 5, 5)) + " GMT"); %>上述代码的意图是：服务器获得浏览器请求头部中的 If-Modified-Since 时间。这个时间是浏览器询问服务器，它所请求的资源是否过期。如果没过期就返回 304 状态码，告诉浏览器直接使用本地的缓存。修改完 test.jsp 代码后，使用鼠标激活浏览器地址栏，按下回车刷新页面。这次的结果如图 11所示。图 11. 修改 test.jsp 后的首次访问可以看到图 11和图 10的请求报头没有区别，而在服务器的响应中，图 11增加了 Last-Modified 头部，这个头部告诉浏览器可以将此页面缓存。按下 F5（必须是 F5 刷新），F5 会强制 Firefox 加载服务器内容，并且发出 If-Modified-Since 头部。得到的报头结果如图 12所示。图 12. 修改 test.jsp 后的再次访问可以看到，图 12的底部已经提示所有内容都来自缓存。浏览器的请求头部多出了 If-Modified-Since，以此询问服务器从缓存时间起，服务器是否对资源进行了修改。服务器判断后发现没有对此资源（test.jsp）修改，就返回 304 状态码，告诉浏览器可以使用缓存。我们在上面的实验中，用到了 HTTP 协议的相关知识，其中涉及了 If-Modified-Since、Last-Modified、304 状态码等。事实上与缓存相关的 HTTP 头部还有许多，诸如过期设置的头部等。熟悉了 HTTP 头部，就如同学会了如何与用户的浏览器交谈，也可以利用协议提升您的程序性能。那么对于 test.jsp 这个小网页来说，基于缓存的方案提升了多少性能呢？我们用 AB 给您答案。AB 是个很强大的工具，它提供了 -H 参数，允许测试人员手动添加 HTTP 请求头部。测试结果如清单 8所示。清单 8. AB 测试 HTTP 缓存测试脚本：ab -c 1000 – n 10000 – H ' If-Modified-Since: Sun, 05 Jun 3910 00:00:00 GMT ' http://localhost:8080/seperate/test.jsp 未修改 test.jsp 前 : Document Path: /seperate/test.jsp Document Length: 362 bytes Concurrency Level: 1000 Time taken for tests: 10.467 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 6080000 bytes HTML transferred: 3630000 bytes Requests per second: 955.42 [#/sec] (mean) Time per request: 1046.665 [ms] (mean) Time per request: 1.047 [ms] (mean, across all concurrent requests) Transfer rate: 567.28 [Kbytes/sec] received 修改 test.jsp 后： Document Path: /seperate/test.jsp Document Length: 0 bytes Concurrency Level: 1000 Time taken for tests: 3.535 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Non-2xx responses: 10000 Total transferred: 1950000 bytes HTML transferred: 0 bytes Requests per second: 2829.20 [#/sec] (mean) Time per request: 353.457 [ms] (mean) Time per request: 0.353 [ms] (mean, across all concurrent requests) Transfer rate: 538.76 [Kbytes/sec] received分别对比 Document Length、Requests per second 以及 Transfer rate 这三个指标。可以发现没使用缓存的 Document Length（下载内容的长度）是 362 字节，而使用了缓存的长度为 0。在吞吐率方面，使用缓存是不使用缓存的 3 倍左右。同时在传输率方面，缓存的传输率比没缓存的小。这些都是用到了客户端缓存的缘故。CDN 的使用CDN（Content Delivery Network，内容分发网络）也是笔者最近才了解和接触到的技术。在淘宝的前端技术报告上、在一个好朋友的创新工场创业之路上，我都听到了这个词，因此有必要对此技术了解一下。所谓的 CDN，就是一种内容分发网络。它采用智能路由和流量管理技术，及时发现能够给访问者提供最快响应的加速节点，并将访问者的请求导向到该加速节点，由该加速节点提供内容服务。利用内容分发与复制机制，CDN 客户不需要改动原来的网站结构，只需修改少量的 DNS 配置，就可以加速网络的响应速度。当用户访问了使用 CDN 服务的网站时，DNS 域名服务器通过 CNAME 方式将最终域名请求重定向到 CDN 系统中的智能 DNS 负载均衡系统。智能 DNS 负载均衡系统通过一组预先定义好的策略（如内容类型、地理区域、网络负载状况等），将当时能够最快响应用户的节点地址提供给用户，使用户可以得到快速的服务。同时，它还与分布在不同地点的所有 CDN 节点保持通信，搜集各节点的健康状态，确保不将用户的请求分配到任何一个已经不可用的节点上。而我们的 CDN 还具有在网络拥塞和失效情况下，能拥有自适应调整路由的能力。由于笔者对 CDN 没有亲身实践，不便多加讲解，但是各大网站都在一定程度使用到了 CDN，淘宝的前端技术演讲中就提及了 CDN，可见 CDN 的威力不一般。图 13. 淘宝的 CDN 前端优化因此 CDN 也是不得不提的一项技术。国内有免费提供 CDN 服务的网站：http://www.webluker.com/，它需要您有备案的域名，感兴趣的您可以去试试。小结本文总结了 HTTP 长连接、动静分离、HTTP 协议等等。在您需要的时候，可以查看本文的内容，相信按照本文的方法，可以辅助您进行前端的高性能优化。笔者将继续写后续的部分，包括数据库的优化、负载均衡、反向代理等。由于笔者水平有限，如有错误，请联系我批评指正。接下来在第三部分文章中，我将介绍服务器端缓存、静态化与伪静态化、分布式缓存等，并且将它们应用到 Java Web 的开发中。使用这些技术可以帮助提高 Java Web 应用程序的性能。*相关主题查看本系列的 第 1 部分：本系列文章将讲述如何提高 Java Web 应用的性能，同时用实际的工具、实际的数据来对比被优化前后的 Java Web 程序。本文主要讲解网页前端的性能优化，重点放在了对于图片使用的优化方面，如图片压缩、CSS Sprites 等方法的介绍。查看"深入理解 HTTP 协议"文章，了解 HTTP 协议，更加理解 HTTP 的含义。查看"利用 Nginx 实现动静分离和负载均衡"，了解如何实现动静分离和负载均衡。查看"developerWorks 的 Web 开发专题"，可以看看顶尖程序员写的文章。查看"Content delivery network"，维基百科对 CDN 的定义。查看"JSP 的缓存技术"，了解 JSP 技术对缓存的多种控制方法。查看 "Apache 反向代理 Mod_Proxy", 了解 Apache 对反向代理的支持。说明：本文部分内容源自 IBM DeveloperWorks 早期文章，部分外部链接可能已失效或内容过时（如 Tomcat 版本配置、CDN 服务提供商等）。文中涉及的技术原理（如动静分离、HTTP 缓存、Keep-Alive）依然适用，但具体配置参数请参考当前使用的软件版本官方文档。
第九章：实现过滤器（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，请注意兼容性问题。
第二章：解析 HTTP 请求，支持静态文件-MiniTomcat上一章内容：第一章：实现基础 HTTP 服务器-MiniTomcat在本章节中，我们将为 HTTP 服务器增加对请求路径、方法和头部信息的解析能力，并基于请求路径返回服务器上的静态文件内容。通过实现这一功能，服务器将能够类似于静态资源服务器，为客户端提供 HTML、CSS、JavaScript 等文件的访问服务。2.1 功能目标解析 HTTP 请求：识别请求路径、请求方法（如 GET、POST）。静态文件支持：根据请求路径查找服务器上的文件（如 index.html），并返回文件内容。MIME 类型支持：根据文件类型设置正确的 Content-Type 响应头，确保浏览器可以正确解析文件。2.2 代码实现我们将扩展之前的 SimpleHttpServer 类，增加对 HTTP 请求解析的逻辑，并在服务器文件系统中查找并返回请求的静态文件。import java.io.*; import java.net.*; public class SimpleHttpServer { private static final int PORT = 8080; private static final String WEB_ROOT = "webroot"; // 静态文件根目录 public static void main(String[] args) { try (ServerSocket serverSocket = new ServerSocket(PORT)) { System.out.println("HTTP Server is running on port " + PORT); while (true) { // 接受客户端连接 Socket clientSocket = serverSocket.accept(); System.out.println("New connection from " + clientSocket.getInetAddress()); // 处理请求并发送响应 handleRequest(clientSocket); } } catch (IOException e) { e.printStackTrace(); } } private static void handleRequest(Socket clientSocket) { try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { // 读取并解析请求行 String requestLine = reader.readLine(); if (requestLine == null || requestLine.isEmpty()) return; System.out.println("Request: " + requestLine); // 解析请求方法和路径 String[] parts = requestLine.split(" "); String method = parts[0]; String path = parts[1]; // 检查是否为 GET 请求 if (!method.equals("GET")) { sendResponse(outputStream, 405, "Method Not Allowed", "Only GET method is supported."); return; } // 查找请求的静态文件 URL url = SimpleHttpServer.class.getClassLoader().getResource(WEB_ROOT + path); if (null == url) { sendResponse(outputStream, 404, "Not Found", "The requested resource was not found."); return; } File file = new File(url.getPath()); if (file.exists() && !file.isDirectory()) { sendFileResponse(outputStream, file); } else { sendResponse(outputStream, 404, "Not Found", "The requested resource was not found."); } } catch (IOException e) { e.printStackTrace(); } finally { try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } // 发送普通文本响应 private static void sendResponse(OutputStream outputStream, int statusCode, String statusText, String message) throws IOException { PrintWriter writer = new PrintWriter(outputStream, true); writer.println("HTTP/1.1 " + statusCode + " " + statusText); writer.println("Content-Type: text/html; charset=UTF-8"); writer.println(); writer.println(" " + statusCode + " " + statusText + " " + message + " "); } // 发送文件响应 private static void sendFileResponse(OutputStream outputStream, File file) throws IOException { PrintWriter writer = new PrintWriter(outputStream, true); writer.println("HTTP/1.1 200 OK"); writer.println("Content-Type: " + getContentType(file)); writer.println("Content-Length: " + file.length()); writer.println(); // 发送文件内容 try (FileInputStream fis = new FileInputStream(file)) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } } } // 根据文件后缀返回 Content-Type private static String getContentType(File file) { String name = file.getName().toLowerCase(); if (name.endsWith(".html") || name.endsWith(".htm")) { return "text/html"; } else if (name.endsWith(".css")) { return "text/css"; } else if (name.endsWith(".js")) { return "application/javascript"; } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) { return "image/jpeg"; } else if (name.endsWith(".png")) { return "image/png"; } else { return "application/octet-stream"; } } }2.3 代码解析请求解析以下是一个普通的 HTTP 请求传输文本示例：GET /index.html HTTP/1.1 Host: localhost:8080 User-Agent: curl/8.7.1 Accept: */* Proxy-Connection: Keep-Alive目前我们只需要解析第一行（请求行）：String requestLine = reader.readLine(); String[] parts = requestLine.split(" "); String method = parts[0]; String path = parts[1];我们读取请求行，并将其按空格分割，从而提取请求方法和路径。这里仅支持 GET 请求，对于其他请求方法返回 405 错误。静态文件查找// 查找请求的静态文件 URL url = SimpleHttpServer.class.getClassLoader().getResource(WEB_ROOT + path); File file = new File(url.getPath());将请求的路径和服务器的根目录 WEB_ROOT 拼接，找到相应的文件。如果文件存在且不是目录，就返回该文件的内容，否则返回 404 错误。文件响应try (FileInputStream fis = new FileInputStream(file)) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } }我们通过文件流读取文件内容，并将其写入输出流发送给客户端。MIME 类型判断private static String getContentType(File file) { ... }根据文件后缀来确定文件的 MIME 类型，从而设置正确的 Content-Type，如 text/html、application/javascript，确保客户端能正确解析文件内容。完整的响应信息文本HTTP/1.1 200 OK Content-Length: 111 Connection: keep-alive Content-Type: text/html Keep-Alive: timeout=4 Proxy-Connection: keep-alive Test Page This is a test page. 2.4 测试静态文件支持在项目根目录下创建 webroot 文件夹，并放入测试文件 index.html： Test Page This is a test page. 启动服务器，并在浏览器中访问 http://localhost:8080/index.html，应显示 index.html 的内容。请求一个不存在的文件（如 http://localhost:8080/notfound.html），应返回 404 错误。2.5 学习收获通过本章节的实现，我们深入理解了以下知识点：HTTP 请求解析：解析请求的路径、方法，处理不同请求的基本逻辑。静态文件的处理：利用文件系统中的路径查找和返回内容，为客户端提供静态资源的访问。响应头的设置：根据请求的文件类型，设置 Content-Type 响应头，从而确保客户端正确渲染不同类型的文件。项目源代码地址：https://github.com/daichangya/MiniTomcat/tree/chapter2/mini-tomcat在下一章中，我们将继续实现连接器组件，以便进一步解耦网络连接处理和请求解析。
第十二章：实现 Facade（外观模式）功能目标本章的核心目标是使用 Facade（外观）模式 简化外部对 Servlet API 的访问。通过该模式，我们可以隐藏内部复杂的实现细节，提供更简洁、统一的接口供外部调用，从而降低系统的耦合度。实现内容Facade 模式概述：Facade 是一种结构型设计模式，用于封装复杂的子系统。它提供一个统一的接口，使得外部系统无需了解内部实现细节即可轻松访问子系统功能。在 Servlet 容器中的应用：在 Web 容器内部，我们可以通过 Facade 包装 HttpServletRequest 和 HttpServletResponse 等核心对象。这样做不仅限制了对内部结构的直接访问，还简化了对外接口的复杂度。具体实现类：本章将实现 RequestFacade 和 ResponseFacade 类。它们分别封装了请求和响应对象，隐藏了底层复杂细节，对外提供标准的 Servlet 接口。示例功能我们将创建一个 RequestFacade 类，封装实际的请求对象，屏蔽不必要的内部细节，从而简化外部对请求信息的访问流程。12.1 Facade 模式设计Facade 模式 的关键在于将复杂的子系统操作封装在一个简单的接口背后。对于 Web 容器而言，我们需要简化客户端访问 HttpServletRequest 和 HttpServletResponse 的过程，同时隐藏底层实现的具体细节。在本项目中，我们将向用户提供 RequestFacade 和 ResponseFacade 对象，代替直接暴露原始的 HttpServletRequest 和 HttpServletResponse 实现类。12.2 实现 RequestFacade 类RequestFacade 类封装了对 HttpServletRequest 的操作，隐藏了请求底层的复杂实现细节。客户端仅通过 RequestFacade 来访问请求的相关信息，无法直接操作底层对象。package com.daicy.minitomcat; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; public class RequestFacade extends HttpServletRequestWrapper { /** * Constructs a request object wrapping the given request. * * @param request * @throws IllegalArgumentException if the request is null */ public RequestFacade(HttpServletRequest request) { super(request); } }通过继承 HttpServletRequestWrapper，RequestFacade 可以方便地代理以下常用方法：getRequestURI()：获取请求的 URI。getHeaderNames()：获取请求头的名称枚举。getHeader(String name)：根据头名称获取请求头的值。getParameter(String name)：获取请求参数。getMethod()：获取请求方法（如 GET、POST 等）。通过封装 HttpServletRequest，外部系统不需要直接与复杂的请求对象交互，只需使用 RequestFacade 提供的简洁接口即可。12.3 实现 ResponseFacade 类ResponseFacade 类封装了对 HttpServletResponse 的操作，隐藏了响应的复杂实现细节。客户端通过 ResponseFacade 来操作响应数据，确保响应行为符合容器规范。package com.daicy.minitomcat; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; public class ResponseFacade extends HttpServletResponseWrapper { /** * Constructs a response adaptor wrapping the given response. * * @param response * @throws IllegalArgumentException if the response is null */ public ResponseFacade(HttpServletResponse response) { super(response); } }ResponseFacade 主要代理以下关键方法：setStatus(int statusCode)：设置响应的状态码。setHeader(String name, String value)：设置响应头。write(String content)：将内容写入响应体（通过 Writer 或 OutputStream）。sendRedirect(String location)：发送重定向响应。通过 ResponseFacade，外部系统可以更安全、简单地管理 HTTP 响应。12.4 使用 Facade 模式简化代码在 Web 服务器的核心实现中，RequestFacade 和 ResponseFacade 简化了请求和响应的处理流程，让外部调用（如 Filter 链或 Servlet 调用）更加直观且安全。以下是在处理请求时实例化并使用 Facade 对象的示例代码：// 创建 Facade 对象包装原始请求和响应 RequestFacade requestFacade = new RequestFacade(request); ResponseFacade responseFacade = new ResponseFacade(response); // 应用 Header 处理 headerHandler.applyHeaders(requestFacade, responseFacade, requestFacade.getSession().getId()); // 获取过滤器链 List<Filter> filters = HttpServer.filterManager.getFilters(); FilterChain filterChain = new FilterChain() { int index = 0; @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (index == filters.size()) { try { // 执行最终的 Servlet 逻辑 wrapper.invoke((HttpServletRequest) request, (HttpServletResponse) response); } catch (Exception e) { HttpServletResponse httpServletResponse = (HttpServletResponse) response; // 捕获异常并设置错误状态码 httpServletResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); httpServletResponse.getWriter().write("Internal Server Error: " + e.getMessage()); } } else { Filter filter = filters.get(index); index++; // 递归调用下一个过滤器 filter.doFilter(request, response, this); } } }; // 启动过滤器链，传入 Facade 对象 filterChain.doFilter(requestFacade, responseFacade);在 ServletProcessor 类中，通过 RequestFacade 和 ResponseFacade 处理请求和响应，使得外部调用变得简单，同时防止用户代码直接操作容器内部对象。12.5 学习收获通过实现 Facade 模式，我们获得了以下核心经验：Facade 模式的设计思想：Facade 模式通过封装复杂的子系统，提供了一个简化的接口，使得外部系统不需要了解内部的实现细节，符合“迪米特法则”（最少知道原则）。简化接口设计：通过 RequestFacade 和 ResponseFacade，我们能够把复杂的请求和响应处理逻辑隐藏在内部，只暴露简单易用的接口给外部调用者。提高安全性和易用性：Facade 模式通过限制对复杂系统的直接访问，提供了更高的安全性（防止内部状态被意外修改），同时降低了外部系统的使用难度。通过 Facade 模式，我们使得 Web 服务器的接口变得更加简洁和安全，同时也更易于扩展和维护。项目源代码地址：https://github.com/daichangya/MiniTomcat/tree/chapter12/mini-tomcat说明：本文代码基于 javax.servlet 包（Servlet API 4.0 及以前版本）。在 Jakarta EE 9 及更高版本中，包名已变更为 jakarta.servlet，实际开发中请根据所使用的容器版本调整依赖。