第四章:实现 Servlet 容器的基本功能

上一章内容:第三章:实现连接器(Connector)组件-MiniTomcat 系列

本章将深入探索 MiniTomcat 的核心进阶环节——实现 Servlet 容器的基本功能。这是服务器处理动态请求的关键步骤,通过合理的代码设计,我们将赋予 MiniTomcat 处理 Java Servlet 的能力,使其从静态资源服务器升级为支持动态内容的 Web 容器。

一、Servlet 容器:核心概念

(一)核心使命

在 MiniTomcat 架构中,Servlet 容器负责掌控处理动态请求的核心逻辑。其主要任务包括:

  1. 生命周期管理:管理 Servlet 的初始化(init)、请求处理(service)及销毁(destroy)过程。
  2. 请求分发:当 HTTP 请求到达时,根据请求路径找到匹配的 Servlet,并调用其 service() 方法进行处理。

(二)功能目标

  1. 请求与响应封装:通过 HttpServletRequestHttpServletResponse 接口封装 HTTP 请求数据(如路径、方法、头部)和响应数据,规范数据传递流程。
  2. 请求路径映射:实现简单的路径映射机制,根据客户端请求的 URI 定位到对应的 Servlet 实例,确保请求被准确处理。

二、项目结构升级

随着 Servlet 容器功能的加入,项目结构进行了相应调整,增加了请求/响应封装类及 Servlet 处理器。更新后的结构如下:

MiniTomcat 
├─ src 
│  ├─ main 
│  │  ├─ java 
│  │  │  ├─ com.daicy.minitomcat 
│  │  │  │  ├─ CustomServletOutputStream.java  // ServletOutputStream 封装
│  │  │  │  ├─ HttpConnector.java              // 连接器类
│  │  │  │  ├─ HttpProcessor.java              // 请求处理器(升级)
│  │  │  │  ├─ HttpServer.java                 // 主服务器类
│  │  │  │  ├─ HttpServletRequest.java         // 请求封装类(接口实现)
│  │  │  │  ├─ HttpServletResponse.java        // 响应封装类(接口实现)
│  │  │  │  ├─ ServletProcessor.java           // Servlet 处理器
│  │  │  │  ├─ StaticResourceProcessor.java    // 静态资源处理器
│  │  │  │  ├─ HelloServlet.java               // 示例 Servlet
│  │  ├─ resources 
│  │  │  ├─ webroot 
│  │  │  │  ├─ index.html 
├─ pom.xml

三、代码实现剖析

(一)请求与响应的封装

1. HttpServletRequestImpl 类

该类实现了 HttpServletRequest 接口,负责收集并存储请求的关键信息(如方法、URI)。

package com.daicy.minitomcat;

import javax.servlet.http.HttpServletRequest;

public class HttpServletRequestImpl implements HttpServletRequest {
    private String method;
    private String requestUri;

    public HttpServletRequestImpl(String method, String requestURI) {
        this.method = method;
        this.requestUri = requestURI;
    }

    // 此处省略其他接口方法的实现,主要聚焦于请求信息的存储
    @Override
    public String getRequestURI() {
        return requestUri;
    }

    @Override
    public String getMethod() {
        return method;
    }
    
    // 其他方法需根据接口定义补充实现或抛出 UnsupportedOperationException
}

2. HttpServletResponseImpl 类

该类实现了 HttpServletResponse 接口,负责管理输出流并构建响应数据。

package com.daicy.minitomcat;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;

public class HttpServletResponseImpl implements HttpServletResponse {
    private OutputStream outputStream;

    public HttpServletResponseImpl(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public ServletOutputStream getOutputStream() {
        return new CustomServletOutputStream(outputStream);
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return new PrintWriter(outputStream, true);
    }
    
    // 其他方法需根据接口定义补充实现
}

(二)Servlet 处理器:请求调度

ServletProcessor 类负责根据请求路径查找并调用对应的 Servlet。本示例中采用简化的硬编码映射方式。

package com.daicy.minitomcat;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

import static com.daicy.minitomcat.HttpProcessor.send404Response;

public class ServletProcessor {
    private Map<String, Servlet> servletMappings = new HashMap<>();

    public void process(HttpServletRequest request, HttpServletResponse response) {
        String servletName = getServletName(request.getRequestURI());
        try {
            PrintWriter writer = response.getWriter();
            if ("HelloServlet".equals(servletName)) {
                writeResponseHeaders(writer, 200, "OK");
                Servlet servlet;
                // 简单的单例缓存逻辑
                if (servletMappings.containsKey(servletName)) {
                    servlet = servletMappings.get(servletName);
                } else {
                    servlet = new HelloServlet();
                    servlet.init(null);
                    servletMappings.put(servletName, servlet);
                }
                servlet.service(request, response);
            } else {
                send404Response(writer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ServletException e) {
            throw new RuntimeException(e);
        }
    }

    private String getServletName(String path) {
        if ("/hello".equals(path)) {
            return "HelloServlet";
        }
        return null;
    }

    private void writeResponseHeaders(PrintWriter writer, int statusCode, String statusMessage) {
        writer.println("HTTP/1.1 " + statusCode + " " + statusMessage);
        writer.println("Content-Type: text/html; charset=UTF-8");
        writer.println();
    }
}

(三)示例 Servlet:HelloServlet

HelloServlet 是一个标准的 Servlet 实现示例,展示了生命周期的基本方法。

package com.daicy.minitomcat;

import javax.servlet.*;
import java.io.IOException;

public class HelloServlet implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("HelloServlet initialized.");
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) {
        try {
            res.getWriter().println("<html><body><h1>Hello from HelloServlet!</h1></body></html>");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getServletInfo() {
        return "";
    }

    @Override
    public void destroy() {
        System.out.println("HelloServlet destroyed.");
    }
}

(四)HttpProcessor 升级:动静分离

HttpProcessor 经过升级,能够根据请求路径区分静态资源与动态请求,并分发至不同的处理器。

package com.daicy.minitomcat;

import java.io.*;
import java.net.Socket;

public class HttpProcessor {
    private Socket socket;
    private final static ServletProcessor servletProcessor = new ServletProcessor();
    private final static StaticResourceProcessor staticProcessor = new StaticResourceProcessor();

    public HttpProcessor(Socket socket) {
        this.socket = socket;
    }

    public void process() {
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            
            // 解析请求
            HttpServletRequestImpl request = parseRequest(inputStream);
            // 构建响应
            HttpServletResponseImpl response = new HttpServletResponseImpl(outputStream);
            
            if (null == request) {
                return;
            }
            
            String uri = request.getRequestURI();
            // 简单的动静分离判断
            if (uri.endsWith(".html") || uri.endsWith(".css") || uri.endsWith(".js")) {
                staticProcessor.process(request, response);
            } else {
                servletProcessor.process(request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private HttpServletRequestImpl parseRequest(InputStream inputStream) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        String requestLine = reader.readLine();
        if (requestLine == null || requestLine.isEmpty()) {
            return null;
        }
        System.out.println("Request Line: " + requestLine);
        String[] parts = requestLine.split(" ");
        if (parts.length < 2) {
            return null;
        }
        String method = parts[0];
        String path = parts[1];
        return new HttpServletRequestImpl(method, path);
    }

    static void send404Response(PrintWriter writer) {
        sendResponse(writer, 404, "Not Found", "The requested resource was not found.");
    }

    private static void sendResponse(PrintWriter writer, int statusCode, String statusText, String message) {
        String html = "<html><body><h1>" + statusCode + " " + statusText + "</h1><p>" + message + "</p></body></html>";
        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);
    }
}

四、测试与验证

启动服务器后,在浏览器中访问 http://localhost:8080/hello。若配置正确,页面将显示 HelloServlet 返回的内容:"Hello from HelloServlet!"。这表明 MiniTomcat 已成功具备处理动态 Servlet 请求的能力。

五、总结与展望

通过本章实现,我们掌握了以下核心内容:

  1. 请求响应封装:熟悉了 HttpServletRequestHttpServletResponse 在容器内部的实现原理。
  2. 容器基础架构:理解了 Servlet 生命周期管理及请求分发的基本流程。
  3. 动静分离:实现了静态资源与动态请求的初步分流处理。

这为后续拓展更复杂的功能(如注解映射、配置文件加载、过滤器链等)奠定了坚实基础。

项目源代码地址:
https://github.com/daichangya/MiniTomcat/tree/chapter4/mini-tomcat

说明:本文代码基于 javax.servlet 包(Java EE 规范),适用于 Tomcat 9 及以下版本。若使用 Tomcat 10+ 或 Jakarta EE 9+ 环境,包名需变更为 jakarta.servlet,且需调整相关依赖配置。