第五章:支持 Servlet 配置和 URL 映射

本步骤将实现基于 web.xml 配置文件的 Servlet 路由映射和初始化参数支持,使得 MiniTomcat 能够根据配置文件自动调用相应的 Servlet 处理请求,从而模拟真实 Tomcat 容器的核心行为。

5.1 功能目标

  • 配置文件管理路由和初始化参数:通过 web.xml 文件统一管理 Servlet 映射关系及初始化参数。
  • URL 路径映射:根据配置文件中的路径映射规则,将请求 URL 精准映射到对应的 Servlet 类。
  • 支持 ServletConfigImpl 获取初始化参数:提供 ServletConfigImpl 实现类,支持 Servlet 从配置中读取初始化参数。

5.2 代码结构

以下是更新后的 MiniTomcat 代码结构。新增了 ServletConfigImplServletContextImplServletLoaderWebXmlServletContainer 等类,以及 web.xml 配置文件。

MiniTomcat
├─ src
│ ├─ main
│ │ ├─ java
│ │ │ ├─ com.daicy.minitomcat
│ │ │ │ ├─ servlet
│ │ │ │ │ ├─ CustomServletOutputStream.java // 自定义的 Servlet 输出流类
│ │ │ │ │ ├─ HttpServletRequestImpl.java    // HTTP 请求的实现类
│ │ │ │ │ ├─ HttpServletResponseImpl.java   // HTTP 响应的实现类
│ │ │ │ │ ├─ ServletConfigImpl.java         // Servlet 配置的实现类
│ │ │ │ │ ├─ ServletContextImpl.java        // Servlet 上下文的实现类
│ │ │ │ ├─ HelloServlet.java                // Servlet 示例类
│ │ │ │ ├─ HttpConnector.java               // 连接器类
│ │ │ │ ├─ HttpProcessor.java               // 请求处理器
│ │ │ │ ├─ HttpServer.java                  // 主服务器类
│ │ │ │ ├─ ServletLoader.java               // Servlet 加载器
│ │ │ │ ├─ ServletProcessor.java            // Servlet 处理器
│ │ │ │ ├─ StaticResourceProcessor.java     // 静态资源处理器
│ │ │ │ ├─ WebXmlServletContainer.java      // Servlet 容器相关类
│ │ ├─ resources
│ │ │ ├─ webroot
│ │ │ │ ├─ index.html
│ │ │ ├─ web.xml
│ ├─ test
├─ pom.xml

5.3 代码实现

5.3.1 创建 ServletConfigImpl

ServletConfigImpl 用于存储 web.xml 中的初始化参数,并提供 getInitParameter() 方法供 Servlet 获取这些参数。

package com.daicy.minitomcat.servlet;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import java.util.HashMap;
import java.util.Map;

public class ServletConfigImpl implements ServletConfig {

    private String servletName;
    private ServletContext servletContext;
    private Map<String, String> initParameters;

    public ServletConfigImpl(String servletName, ServletContext servletContext, Map<String, String> initParameters) {
        this.servletName = servletName;
        this.servletContext = servletContext;
        this.initParameters = initParameters != null ? initParameters : new HashMap<>();
    }

    @Override
    public String getInitParameter(String name) {
        return initParameters.get(name);
    }

    // ... 省略其他方法实现
}

5.3.2 添加 web.xml 文件

resources 目录下创建 web.xml 文件,用于配置 Servlet 映射和初始化参数。以下配置将 /hello 路径映射到 HelloServlet 类,并设置了一个初始化参数。

<web-app>
    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.daicy.minitomcat.HelloServlet</servlet-class>
        <init-param>
            <param-name>greeting</param-name>
            <param-value>Hello from web.xml!</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

5.3.3 解析 web.xml 配置文件

创建 WebXmlServletContainer 类,用于解析 web.xml 文件,并将 Servlet 路径映射到对应的类及配置信息。

package com.daicy.minitomcat;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import com.daicy.minitomcat.servlet.ServletConfigImpl;
import org.w3c.dom.*;

import java.util.HashMap;
import java.util.Map;

public class WebXmlServletContainer {

    private Map<String, ServletConfig> servletConfigMap = new HashMap<>();
    private Map<String, Servlet> servletHashMap = new HashMap<>();
    private ServletContext servletContext;

    public void parse(String xmlPath, ServletContext servletContext) {
        try {
            this.servletContext = servletContext;

            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document doc = builder.parse(getClass().getResourceAsStream(xmlPath));

            // 解析 servlet 节点
            NodeList servletNodes = doc.getElementsByTagName("servlet");
            for (int i = 0; i < servletNodes.getLength(); i++) {
                Element servletElement = (Element) servletNodes.item(i);
                String servletName = servletElement.getElementsByTagName("servlet-name").item(0).getTextContent();
                String servletClass = servletElement.getElementsByTagName("servlet-class").item(0).getTextContent();

                Map<String, String> initParamsMap = new HashMap<>();
                NodeList initParams = servletElement.getElementsByTagName("init-param");
                for (int j = 0; j < initParams.getLength(); j++) {
                    Element param = (Element) initParams.item(j);
                    String paramName = param.getElementsByTagName("param-name").item(0).getTextContent();
                    String paramValue = param.getElementsByTagName("param-value").item(0).getTextContent();
                    initParamsMap.put(paramName, paramValue);
                }
                ServletConfig servletConfig = new ServletConfigImpl(servletName, servletContext, initParamsMap);
                servletConfigMap.put(servletClass, servletConfig);
                servletContext.setAttribute(servletName, servletClass);
            }

            // 解析 servlet-mapping 节点
            NodeList mappingNodes = doc.getElementsByTagName("servlet-mapping");
            for (int i = 0; i < mappingNodes.getLength(); i++) {
                Element mappingElement = (Element) mappingNodes.item(i);
                String servletName = mappingElement.getElementsByTagName("servlet-name").item(0).getTextContent();
                String urlPattern = mappingElement.getElementsByTagName("url-pattern").item(0).getTextContent();
                servletContext.setAttribute(urlPattern, servletName);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public ServletConfig getServletConfig(String urlPattern) {
        String servletClass = getServletClass(getServletName(urlPattern));
        return servletConfigMap.get(servletClass);
    }

    public String getServletName(String urlPattern) {
        return (String) servletContext.getAttribute(urlPattern);
    }

    public String getServletClass(String servletName) {
        return (String) servletContext.getAttribute(servletName);
    }

    public Servlet getServlet(String servletName) {
        return servletHashMap.get(servletName);
    }

    public void setServlet(String servletName, Servlet servlet) {
         servletHashMap.put(servletName, servlet);
    }

    public Map<String, Servlet> getServletHashMap() {
        return servletHashMap;
    }
}

5.3.4 修改 ServletProcessor

ServletProcessor 中集成 WebXmlServletContainer,获取 Servlet 映射信息和初始化参数,并调用对应的 Servlet 处理请求。

package com.daicy.minitomcat;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

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

public class ServletProcessor {

    public void process(HttpServletRequest request, HttpServletResponse response) {
        String uri = request.getRequestURI();
        try {
            PrintWriter writer = response.getWriter();
            WebXmlServletContainer parser = HttpServer.parser;
            String servletName = parser.getServletName(uri);
            
            if (null != servletName) {
                writeResponseHeaders(writer, 200, "OK");
                Servlet servlet = parser.getServlet(servletName);
                
                if (null == servlet) {
                    ServletConfig servletConfig = parser.getServletConfig(uri);
                    servlet = ServletLoader.loadServlet(servletConfig);
                    if (null == servlet) {
                        return;
                    }
                    // 将初始化后的 Servlet 存储在 WebXmlServletContainer 中,便于后续复用
                    parser.setServlet(servletName, servlet);
                }
                servlet.service(request, response);
            } else {
                send404Response(writer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ServletException e) {
            throw new RuntimeException(e);
        }
    }

    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();
    }
}

5.3.5 修改 HelloServlet 支持初始化参数

更新 HelloServlet,使其通过 ServletConfig 获取 web.xml 中配置的初始化参数。

package com.daicy.minitomcat;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class HelloServlet implements javax.servlet.Servlet {
    
    private ServletConfig config;

    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
    }

    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String greeting = config.getInitParameter("greeting");
        response.getWriter().println("<html><body><h1>" + greeting + "</h1></body></html>");
    }

    @Override
    public void destroy() {
        // 清理资源
    }

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

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

5.4 测试

启动服务器,在浏览器中访问 http://localhost:8080/hello。页面将返回 web.xml 中设置的 greeting 参数内容:"Hello from web.xml!"。

5.5 学习收获

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

  1. XML 配置文件解析:学习了解析 XML 文件的方法,并将配置数据映射为 Java 对象。
  2. Servlet URL 路径映射:基于配置文件实现了灵活的 URL 路径到 Servlet 类的映射机制。
  3. Servlet 初始化参数的使用:掌握了如何使用 ServletConfig 接口获取 web.xml 中定义的初始化参数。

这为后续实现会话管理、过滤器支持等高级功能打下了坚实基础。

项目源代码地址:

https://github.com/daichangya/MiniTomcat/tree/chapter5/mini-tomcat

说明:本文基于 Servlet 2.5 规范演示 XML 配置方式。在实际开发中,Servlet 3.0 及以上版本推荐使用注解(如 @WebServlet)进行配置,XML 方式虽仍兼容但已较少用于新项目。