You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetty 9.4如何为相同pathSpec动态配置切换多个Servlet

问题原因

抛出异常的核心原因是ServletContextHandler.addServlet()不会覆盖同路径的已有Servlet映射,每次调用都会新增一条路径绑定规则。第一次点击按钮时/test路径没有绑定,添加Test1可以正常生效;第二次点击时/test已经存在映射,重复添加就会触发Multiple servlets map to path /test的错误。
注意:Servlet规范本身不允许多个Servlet绑定到完全相同的路径,动态增删映射属于容器层面的操作,存在并发风险,不推荐作为常规实现方案。

推荐实现方案:固定入口+内部分发

这是最稳定、符合Servlet规范的实现方式,不需要动态修改容器的映射规则,仅通过应用内部状态控制请求转发即可,零额外开销、线程安全。

实现步骤

  • 启动Jetty时一次性完成所有Servlet注册,不要在请求处理过程中动态调用addServlet
  • 用一个固定的分发Servlet绑定到对外暴露的/test路径
  • 用全局可见的状态变量标记当前要启用的服务
  • 分发Servlet收到请求后,根据当前状态把请求转发到对应Test1/Test2的内部处理逻辑

代码示例

  1. 定义全局状态标记,注意用volatile保证多线程下的状态可见性:
public class Main {
    // 标记当前启用的服务,默认启用test1
    public static volatile String activeService = "test1";
    public static ServletContextHandler handler;

    public static void main(String[] args) throws Exception {
        // Jetty启动基础逻辑省略
        // 启动时一次性注册所有Servlet,仅执行一次
        handler.addServlet(ControlServlet.class, "/"); // 原来的测试页面Servlet
        handler.addServlet(TestDispatcher.class, "/test"); // 对外暴露的统一入口
        handler.addServlet(Test1.class, "/internal/test1"); // Test1绑定内部路径,不对外暴露
        handler.addServlet(Test2.class, "/internal/test2"); // Test2绑定内部路径,不对外暴露
        // 后续启动Jetty监听端口逻辑省略
    }
}
  1. 实现统一分发Servlet,绑定到/test路径:
public class TestDispatcher extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 根据当前全局状态转发请求到对应内部Servlet
        if ("test1".equals(Main.activeService)) {
            req.getRequestDispatcher("/internal/test1").forward(req, resp);
        } else {
            req.getRequestDispatcher("/internal/test2").forward(req, resp);
        }
    }
}
  1. 修改原来测试页的doPost逻辑,不再动态添加Servlet,仅修改全局状态:
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    if (req.getParameter("test1") != null) {
        Main.activeService = "test1";
        resp.sendRedirect("/test");
        return;
    }
    if (req.getParameter("test2") != null) {
        Main.activeService = "test2";
        resp.sendRedirect("/test");
        return;
    }
    resp.sendRedirect("/");
}
备选方案:动态替换Servlet映射

如果你的场景必须要动态增删Servlet映射(比如Test1/Test2是完全独立、无法提前初始化的模块),需要在添加新Servlet前先彻底清理同路径下的旧映射和Servlet实例,操作时要加锁避免并发问题。

实现代码

// 加同步锁避免多线程同时修改映射导致异常
private synchronized static void replaceTestServlet(ServletContextHandler handler, Class<? extends HttpServlet> servletClass) {
    ServletHandler servletHandler = handler.getServletHandler();
    List<ServletMapping> removeMappings = new ArrayList<>();
    List<ServletHolder> removeHolders = new ArrayList<>();

    // 遍历找到所有绑定到/test的映射和对应的Servlet实例
    for (ServletMapping mapping : servletHandler.getServletMappings()) {
        for (String path : mapping.getPathSpecs()) {
            if ("/test".equals(path)) {
                removeMappings.add(mapping);
                ServletHolder holder = servletHandler.getServlet(mapping.getServletName());
                if (holder != null) {
                    removeHolders.add(holder);
                }
                break;
            }
        }
    }

    // 先移除旧的Servlet实例和映射
    removeHolders.forEach(holder -> servletHandler.removeServletHolder(holder.getName()));
    removeMappings.forEach(mapping -> servletHandler.removeServletMapping(mapping.getServletName()));

    // 再添加新的Servlet映射
    handler.addServlet(servletClass, "/test");
}

在doPost中替换原来的addServlet调用即可:

if (req.getParameter("test1") != null) {
    replaceTestServlet(Main.handler, Test1.class);
    resp.sendRedirect("/test");
    return;
}
if (req.getParameter("test2") != null) {
    replaceTestServlet(Main.handler, Test2.class);
    resp.sendRedirect("/test");
    return;
}

该方案存在明显缺陷:映射修改过程中存在短暂的空窗期,可能导致访问/test返回404;同步锁会阻塞所有涉及Servlet映射的操作,高并发场景下性能差,非必要不推荐使用。

内容的提问来源于stack exchange,提问作者Roi Tiefenbrunn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 20:18:29