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的内部处理逻辑
代码示例
- 定义全局状态标记,注意用
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监听端口逻辑省略 } }
- 实现统一分发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); } } }
- 修改原来测试页的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
相关产品推荐
相关产品推荐

