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

Jetty 12中实现SPA路径NotFound转发至指定Servlet的方案

背景

我有使用history.pushState()进行导航的SPA应用,因此需要为多个URL提供同一HTML(可能由JSP或Servlet生成)。但存在不能被该机制“拦截/重写”的静态资源。我还需要添加仅匹配HTML、不匹配其他静态资源或Servlet的过滤器。最后,这种“转发”必须限定在特定路径范围内(我的应用包含多个此类SPA,每个都有自己的范围和HTML),且所有操作基于嵌入式Jetty。

我在Jetty 9、10和11中有可正常运行的代码,但Jetty 12引入了破坏性变更导致该代码无法使用,现寻求在Jetty 12中复现相同行为的方法。

假设有以下文件:

/index.html
/subdir/
  +- app.js
  +- style.css
  `- index.html

以及映射到/subdir/logout和/subdir/api/*的Servlet。

我需要以下请求均返回/subdir/index.html(需正确经过带有request或forward调度类型的过滤器):

  • /subdir/
  • /subdir/index.html
  • /subdir/foo
  • /subdir/foo/bar
  • /subdir/bar

而以下请求应正确返回静态文件或调用映射的Servlet:

  • /subdir/app.js
  • /subdir/style.css
  • /subdir/api/foo
  • /subdir/logout
Jetty 9/10/11中的可行代码

在Jetty 9/10/11中,我通过以下方式实现(注:context为ServletContextHandler):

// 有时我需要此类过滤器(向响应中注入HTML/JS)
// 或其他将未认证用户重定向至登录页的过滤器(可能为SSO)
context.addFilter(CsrfServletFilter.class, "*.html", EnumSet.of(REQUEST, ASYNC, FORWARD));
// 有时HTML由Servlet生成(其他时候为index.jsp文件)
context.addServlet(new ServletHolder(indexServlet), "/subdir/index.html");

ServletHolder resources =
    new ServletHolder(
        "resources",
        new DefaultServlet(
            new ResourceService() {
              @Override
              protected void notFound(HttpServletRequest request, HttpServletResponse response)
                  throws IOException {
                // 配合history.pushState使用:为所有未找到的请求返回首页
                try {
                  response.setHeader("Content-Location", "/subdir/");
                  request.getRequestDispatcher("/subdir/").forward(request, response);
                } catch (ServletException e) {
                  throw new RuntimeException(e);
                }
              }
            }));
resources.setInitParameter("dirAllowed", "false");
resources.setInitParameter("welcomeServlets", "exact");
context.addServlet(resources, "/subdir/*");
Jetty 12中的替代方案

Jetty 12对Servlet容器架构和DefaultServlet的API做了大幅调整,原有的ResourceService扩展方式不再适用。推荐两种实现方式:

方式一:使用RewriteHandler(推荐)

通过RewriteHandler统一处理SPA路由规则,逻辑更清晰,也更符合Jetty 12的设计理念:

// 创建并配置ServletContext(替代原ServletContextHandler)
Server server = new Server(8080);
ServletContext context = server.addContext("/");

// 添加针对HTML的过滤器,保留原有的调度类型配置
context.addFilter(CsrfServletFilter.class, "*.html", 
                  EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.FORWARD));

// 注册生成SPA入口HTML的Servlet(或直接使用JSP,需配置JSP支持)
context.addServlet(new ServletHolder(indexServlet), "/subdir/index.html");

// 配置DefaultServlet处理静态资源
DefaultServlet defaultServlet = new DefaultServlet();
ServletHolder defaultHolder = new ServletHolder("default", defaultServlet);
defaultHolder.setInitParameter("dirAllowed", "false");
defaultHolder.setInitParameter("welcomeServlets", "exact");
context.addServlet(defaultHolder, "/subdir/*");

// 配置RewriteHandler处理SPA路由转发
RewriteHandler rewriteHandler = new RewriteHandler();
rewriteHandler.setRewriteRequestURI(true);
rewriteHandler.setRewritePathInfo(true);
rewriteHandler.setOriginalPathAttribute("requestedPath");

// 添加SPA路由规则
rewriteHandler.addRule(new RewriteRule() {
    @Override
    public String apply(String target, HttpServletRequest request, HttpServletResponse response) 
            throws IOException, ServletException {
        String requestUri = request.getRequestURI();
        
        // 排除静态资源(可根据实际情况扩展后缀)
        if (requestUri.matches("/subdir/.*\\.(js|css|png|jpg|svg)$")) {
            return null; // 不重写,交给DefaultServlet处理
        }
        
        // 排除已映射的Servlet路径
        if (requestUri.startsWith("/subdir/api/") || requestUri.equals("/subdir/logout")) {
            return null; // 不重写,交给对应Servlet处理
        }
        
        // 排除入口HTML本身和根路径
        if (requestUri.equals("/subdir/") || requestUri.equals("/subdir/index.html")) {
            return null;
        }
        
        // 其他路径转发到SPA入口
        return "/subdir/index.html";
    }
});

// 将RewriteHandler加入Handler链
rewriteHandler.setHandler(context);
server.setHandler(rewriteHandler);

server.start();
server.join();

方式二:自定义DefaultServlet重写notFound逻辑

如果更倾向于沿用原有的“未找到时转发”思路,可以重写Jetty 12中DefaultServlet的notFound方法:

Server server = new Server(8080);
ServletContext context = server.addContext("/");

// 配置过滤器和入口Servlet,同方式一
context.addFilter(CsrfServletFilter.class, "*.html", 
                  EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.FORWARD));
context.addServlet(new ServletHolder(indexServlet), "/subdir/index.html");

// 自定义DefaultServlet处理未找到的请求
DefaultServlet customDefaultServlet = new DefaultServlet() {
    @Override
    protected void notFound(Request jettyRequest, Response jettyResponse, Resource resource) 
            throws IOException, ServletException {
        HttpServletRequest request = jettyRequest.getHttpServletRequest();
        HttpServletResponse response = jettyResponse.getHttpServletResponse();
        String requestUri = request.getRequestURI();
        
        // 判断是否属于当前SPA范围,且需要转发到入口
        if (requestUri.startsWith("/subdir/") 
                && !requestUri.startsWith("/subdir/api/") 
                && !requestUri.equals("/subdir/logout") 
                && !requestUri.matches("/subdir/.*\\.(js|css)$")) {
            response.setHeader("Content-Location", "/subdir/");
            request.getRequestDispatcher("/subdir/index.html").forward(request, response);
        } else {
            // 其他未找到的请求,按默认逻辑处理
            super.notFound(jettyRequest, jettyResponse, resource);
        }
    }
};

ServletHolder customDefaultHolder = new ServletHolder("custom-default", customDefaultServlet);
customDefaultHolder.setInitParameter("dirAllowed", "false");
customDefaultHolder.setInitParameter("welcomeServlets", "exact");
context.addServlet(customDefaultHolder, "/subdir/*");

server.setHandler(context);
server.start();
server.join();

两种方式都能实现需求,其中RewriteHandler的方式更易于维护和扩展多SPA场景。

内容的提问来源于stack exchange,提问作者Thomas Broyer

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 03:04:52