实现ServletContext

廖雪峰
资深软件开发工程师,业余马拉松选手。

在Java Web应用程序中,ServletContext代表应用程序的运行环境,一个Web应用程序对应一个唯一的ServletContext实例,ServletContext可以用于:

  • 提供初始化和全局配置:可以从ServletContext获取Web App配置的初始化参数、资源路径等信息;
  • 共享全局数据:ServletContext存储的数据可以被整个Web App的所有组件读写。

既然ServletContext是一个Web App的全局唯一实例,而Web App又运行在Servlet容器中,我们在实现ServletContext时,完全可以把它当作Servlet容器来实现,它在内部维护一组Servlet实例,并根据Servlet配置的路由信息将请求转发给对应的Servlet处理。假设我们编写了两个Servlet:

  • IndexServlet:映射路径为/
  • HelloServlet:映射路径为/hello

那么,处理HTTP请求的路径如下:

                     ┌────────────────────┐
                     │   ServletContext   │
                     ├────────────────────┤
                     │     ┌────────────┐ │
    ┌─────────────┐  │ ┌──▶│IndexServlet│ │
───▶│HttpConnector│──┼─┤   ├────────────┤ │
    └─────────────┘  │ └──▶│HelloServlet│ │
                     │     └────────────┘ │
                     └────────────────────┘

下面,我们来实现ServletContext。首先定义ServletMapping,它包含一个Servlet实例,以及将映射路径编译为正则表达式:

public class ServletMapping {
    final Pattern pattern; // 编译后的正则表达式
    final Servlet servlet; // Servlet实例
    public ServletMapping(String urlPattern, Servlet servlet) {
        this.pattern = buildPattern(urlPattern); // 编译为正则表达式
        this.servlet = servlet;
    }
}

接下来实现ServletContext

public class ServletContextImpl implements ServletContext {
    final List<ServletMapping> servletMappings = new ArrayList<>();
}

这个数据结构足够能让我们实现根据请求路径路由到某个特定的Servlet:

public class ServletContextImpl implements ServletContext {
    ...
    // HTTP请求处理入口:
    public void process(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // 请求路径:
        String path = request.getRequestURI();
        // 搜索Servlet:
        Servlet servlet = null;
        for (ServletMapping mapping : this.servletMappings) {
            if (mapping.matches(path)) {
                // 路径匹配:
                servlet = mapping.servlet;
                break;
            }
        }
        if (servlet == null) {
            // 未匹配到任何Servlet显示404 Not Found:
            PrintWriter pw = response.getWriter();
            pw.write("<h1>404 Not Found</h1><p>No mapping for URL: " + path + "</p>");
            pw.close();
            return;
        }
        // 由Servlet继续处理请求:
        servlet.service(request, response);
    }
}

这样我们就实现了ServletContext

不过,细心的同学会发现,我们编写的两个Servlet:IndexServletHelloServlet,还没有被添加到ServletContext中。那么问题来了:Servlet在什么时候被初始化?

答案是在创建ServletContext实例后,就立刻初始化所有的Servlet。我们编写一个initialize()方法,用于初始化Servlet:

public class ServletContextImpl implements ServletContext {
    Map<String, ServletRegistrationImpl> servletRegistrations = new HashMap<>();
    Map<String, Servlet> nameToServlets = new HashMap<>();
    List<ServletMapping> servletMappings = new ArrayList<>();

    public void initialize(List<Class<?>> servletClasses) {
        // 依次添加每个Servlet:
        for (Class<?> c : servletClasses) {
            // 获取@WebServlet注解:
            WebServlet ws = c.getAnnotation(WebServlet.class);
            Class<? extends Servlet> clazz = (Class<? extends Servlet>) c;
            // 创建一个ServletRegistration.Dynamic:
            ServletRegistration.Dynamic registration = this.addServlet(AnnoUtils.getServletName(clazz), clazz);
            registration.addMapping(AnnoUtils.getServletUrlPatterns(clazz));
            registration.setInitParameters(AnnoUtils.getServletInitParams(clazz));
        }
        // 实例化Servlet:
        for (String name : this.servletRegistrations.keySet()) {
            var registration = this.servletRegistrations.get(name);
            registration.servlet.init(registration.getServletConfig());
            this.nameToServlets.put(name, registration.servlet);
            for (String urlPattern : registration.getMappings()) {
                this.servletMappings.add(new ServletMapping(urlPattern, registration.servlet));
            }
            registration.initialized = true;
        }
    }

    @Override
    public ServletRegistration.Dynamic addServlet(String name, Servlet servlet) {
        var registration = new ServletRegistrationImpl(this, name, servlet);
        this.servletRegistrations.put(name, registration);
        return registration;
    }
}

从Servlet 3.0规范开始,我们必须要提供addServlet()动态添加一个Servlet,并且返回ServletRegistration.Dynamic,因此,我们在initialize()方法中调用addServlet(),完成所有Servlet的创建和初始化。

最后我们修改HttpConnector,实例化ServletContextImpl

public class HttpConnector implements HttpHandler {
    // 持有ServletContext实例:
    final ServletContextImpl servletContext;
    final HttpServer httpServer;

    public HttpConnector() throws IOException {
        // 创建ServletContext:
        this.servletContext = new ServletContextImpl();
        // 初始化Servlet:
        this.servletContext.initialize(List.of(IndexServlet.class, HelloServlet.class));
        ...
    }

    @Override
    public void handle(HttpExchange exchange) throws IOException {
        var adapter = new HttpExchangeAdapter(exchange);
        var request = new HttpServletRequestImpl(adapter);
        var response = new HttpServletResponseImpl(adapter);
        // process:
        this.servletContext.process(request, response);
    }
}

运行服务器,输入http://localhost:8080/,查看IndexServlet的输出:

index-page

输入http://localhost:8080/hello?name=Bob,查看HelloServlet的输出:

hello-page

输入错误的路径,查看404输出:

404-page

可见,我们已经成功完成了ServletContext和所有Servlet的管理,并实现了正确的路由。

有的同学会问:Servlet本身应该是Web App开发人员实现,而不是由服务器实现。我们在服务器中却写死了两个Servlet,这显然是不合理的。正确的方式是从外部war包加载Servlet,但是这个问题我们放到后面解决。

参考源码

可以从GitHubGitee下载源码。

GitHub

小结

编写Servlet容器时,直接实现ServletContext接口,并在内部完成所有Servlet的管理,就可以实现根据路径路由到匹配的Servlet。



Comments

Loading comments...