一键搞定内网穿透 联行号查询|开户行查询 在线工具箱 藏经阁
当前位置:首页 / 互联网与IT技术 / 正文
实现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的输出:

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

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

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

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

参考源码

可以从GitHub或Gitee下载源码。

GitHub

小结

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

转载