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中,我通过以下方式实现(注: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对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

