【问题标题】:Spring catch all route for index.htmlSpring 捕获 index.html 的所有路由
【发布时间】:2017-01-12 21:40:46
【问题描述】:

我正在为基于 react 的单页应用程序开发一个 spring 后端,我正在使用 react-router 进行客户端路由。

在 index.html 页面旁边,后端提供路径 /api/** 上的数据。

为了在我的应用程序的根路径 / 上从 src/main/resources/public/index.html 提供我的 index.html,我添加了一个资源处理程序

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/").addResourceLocations("/index.html");
}

我想要的是在没有其他路由匹配时提供 index.html 页面,例如当我调用/api以外的路径时。

如何在春季配置这样的包罗万象的路线?

【问题讨论】:

    标签: spring spring-mvc spring-boot single-page-application catch-all


    【解决方案1】:

    经过多次尝试,我发现以下解决方案是最简单的解决方案。它基本上会绕过所有很难处理的 Spring 处理。

    @Component
    public class StaticContentFilter implements Filter {
        
        private List<String> fileExtensions = Arrays.asList("html", "js", "json", "csv", "css", "png", "svg", "eot", "ttf", "woff", "appcache", "jpg", "jpeg", "gif", "ico");
        
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
        }
        
        private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            String path = request.getServletPath();
            
            boolean isApi = path.startsWith("/api");
            boolean isResourceFile = !isApi && fileExtensions.stream().anyMatch(path::contains);
            
            if (isApi) {
                chain.doFilter(request, response);
            } else if (isResourceFile) {
                resourceToResponse("static" + path, response);
            } else {
                resourceToResponse("static/index.html", response);
            }
        }
        
        private void resourceToResponse(String resourcePath, HttpServletResponse response) throws IOException {
            InputStream inputStream = Thread.currentThread()
                    .getContextClassLoader()
                    .getResourceAsStream(resourcePath);
            
            if (inputStream == null) {
                response.sendError(NOT_FOUND.value(), NOT_FOUND.getReasonPhrase());
                return;
            }
            
            inputStream.transferTo(response.getOutputStream());
        }
    }
    

    【讨论】:

      【解决方案2】:

      另一种解决方案(更改/添加/删除 myurl1myurl2、... 使用您的路线):

      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      import javax.servlet.http.HttpServletRequest;
      
      @Controller
      public class SinglePageAppController {
      
          /**
           * If the user refreshes the page while on a React route, the request will come here.
           * We need to tell it that there isn't any special page, just keep using React, by
           * forwarding it back to the root.
           */
          @RequestMapping({"/myurl1/**", "/myurl2/**"})
          public String forward(HttpServletRequest httpServletRequest) {
              return "forward:/";
          }
      }
      

      注意:使用public String index() 也可以正常工作,但前提是您使用模板。并且不推荐使用WebMvcConfigurerAdapter

      【讨论】:

        【解决方案3】:

        我在我的 Spring Boot 应用程序中托管了一个基于 Polymer 的 PWA,以及图像等静态 Web 资源,以及“/api/...”下的 REST API。我希望客户端应用程序处理 PWA 的 URL 路由。这是我使用的:

        @Configuration
        public class WebConfig extends WebMvcConfigurerAdapter {
            /**
             * Ensure client-side paths redirect to index.html because client handles routing. NOTE: Do NOT use @EnableWebMvc or it will break this.
             */
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                // Map "/"
                registry.addViewController("/")
                        .setViewName("forward:/index.html");
        
                // Map "/word", "/word/word", and "/word/word/word" - except for anything starting with "/api/..." or ending with
                // a file extension like ".js" - to index.html. By doing this, the client receives and routes the url. It also
                // allows client-side URLs to be bookmarked.
        
                // Single directory level - no need to exclude "api"
                registry.addViewController("/{x:[\\w\\-]+}")
                        .setViewName("forward:/index.html");
                // Multi-level directory path, need to exclude "api" on the first part of the path
                registry.addViewController("/{x:^(?!api$).*$}/**/{y:[\\w\\-]+}")
                        .setViewName("forward:/index.html");
            }
        
            @Override
            public void addResourceHandlers(ResourceHandlerRegistry registry) {
                registry.addResourceHandler("/**").addResourceLocations("classpath:/webapp/");
            }
        }
        

        这也应该适用于 Angular 和 React 应用程序。

        【讨论】:

        • 这对我不起作用,我们有到 /resources/static/index.html 的 webapp 路径但仍然无法正常工作,我得到的只是来自任何 RestController 的随机视图
        • 谢谢,这对我有用 - 只需将 resourceLocation 更改为 classpath:/static/ 即可使其与我的 React 应用程序一起运行。请注意,从 Spring 5.x.x 开始。扩展 WebMvcConfigurerAdapter 已弃用,而必须实施 WebMvcConfigurer
        • 这比当前投票率最高的答案更简洁,解释得更好:)
        【解决方案4】:

        要回答您在所有情况下都提供单页应用程序 (SPA) 的具体问题除了 /api 路线是我修改 Petri 的答案.

        我有一个名为 polymer 的模板,其中包含我的 SPA 的 index.html。所以挑战变成了让我们将除 /api 和 /public-api 之外的所有路由转发到该视图。

        在我的 WebMvcConfigurerAdapter 中,我覆盖了 addViewControllers 并使用了正则表达式:^((?!/api/|/public-api/).)*$

        在您的情况下,您需要正则表达式:^((?!/api/).)*$

        public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
        
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/{spring:^((?!/api/).)*$}").setViewName("polymer");
            super.addViewControllers(registry);
        }
        

        这导致能够点击 http://localhosthttp://localhost/community 来提供我的 SPA,并且 SPA 发出的所有其余调用都成功路由到 http://localhost/api/postshttp://localhost/public-api/posts 等。

        【讨论】:

        • 这工作从/ 开始并在 SPA 中导航,但不可收藏/并且无法在刷新后保留。
        【解决方案5】:

        避免使用@EnableWebMvc

        默认情况下,Spring-Boot 在src/main/resources 中提供静态内容:

        • /META-INF/resources/
        • /资源/
        • /静态/
        • /公共/

        看看thisthis

        或者保留@EnableWebMvc 并覆盖addViewControllers

        您是否指定了 @EnableWebMvc ?看看这个:Java Spring Boot: How to map my app root (“/”) to index.html?

        要么删除@EnableWebMvc,要么重新定义addViewControllers

        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("forward:/index.html");
        }
        

        或者定义一个Controller来捕捉/

        你可以看看这个spring-boot-reactjs sample project on github:

        它使用控制器做你想做的事:

        @Controller
        public class HomeController {
        
            @RequestMapping(value = "/")
            public String index() {
                return "index";
            }
        
        }
        

        它的index.htmlsrc/main/resources/templates下面

        【讨论】:

        • 我认为这应该被标记为你的问题的答案。
        • 我在模板文件夹中有probe.html,而不是index.html。如果我按照这个例子,我得到:“没有找到 GET /probe 的处理程序”我也尝试使用 index.html,但我得到了同样的错误。
        • 这并不能解决问题所要求的客户端路由。它只为根路径服务index.html。它不会将 /campaigns/33 之类的 URL 重定向到 index.html
        【解决方案6】:

        由于我的反应应用程序可以使用根作为前向目标,这最终对我有用

        @Configuration
        public class WebConfiguration extends WebMvcConfigurerAdapter {
        
          @Override
          public void addViewControllers(ViewControllerRegistry registry) {
              registry.addViewController("/{spring:\\w+}")
                    .setViewName("forward:/");
              registry.addViewController("/**/{spring:\\w+}")
                    .setViewName("forward:/");
              registry.addViewController("/{spring:\\w+}/**{spring:?!(\\.js|\\.css)$}")
                    .setViewName("forward:/");
          }
        }
        

        说实话,我不知道为什么它必须完全采用这种特定格式以避免无限转发循环。

        【讨论】:

        • 谢谢!这适用于 Angular 4 RouterModule + Spring Boot Embedded Container 类型设置;)
        • 我遇到了第三个addViewController 方法调用的问题。我的情况是我有未提供的 webfonts 文件夹。我认为这是因为正则表达式只指定了 js 和 css 扩展。我最终删除了第三个并添加了一个新的资源处理程序。
        • 您好,代码中的spring前缀是什么意思?
        • 正则表达式不适用于连字符 (-) 和下划线 (_)。您可以将 \\w 替换为 ^[a-zA-Z\d-_] 以使其适用于具有这些字符的 URL
        • @hguser spring 是分配匹配部分的变量名。但是,它仅用于在 AntMatcher (docs.spring.io/spring-framework/docs/current/javadoc-api/org/…) 中启用正则表达式的使用,并且由于未使用它而不能满足目的。您可以将其更改为其他任何内容,但其目的是启用与正则表达式的匹配。要使用 is,在路径中使用 {spring} 重定向或转发,然后您将使用匹配的任何内容。最后一个条目不明确,因为两个匹配项都分配了同一个变量...
        【解决方案7】:

        我在我的 Spring Boot 应用程序中使用 react 和 react-router,它就像创建一个映射到 / 和我网站的子树(如 /users/**)的控制器一样简单 这是我的解决方案

        @Controller
        public class SinglePageAppController {
            @RequestMapping(value = {"/", "/users/**", "/campaigns/**"})
            public String index() {
                return "index";
            }
        }
        

        此控制器不会捕获 API 调用,并且会自动处理资源。

        【讨论】:

        • 这对我不起作用。我得到“没有为 GET /index 找到处理程序”
        • 尝试“index.html”而不是“index”
        • 我当前的项目遵循这种方法,但我们有像“/portal/**”、“/portal/user/**”、“/portal/profile/**”这样的网址。正常导航工作。但是当我们刷新 url 时,说“/portal/user”,我得到错误 Circular view path [index.html]: will dispatch back to the current handler URL。如何处理这个,有什么想法吗?
        【解决方案8】:

        通过查看this question找到答案

        @Bean
        public EmbeddedServletContainerCustomizer notFoundCustomizer() {
            return new EmbeddedServletContainerCustomizer() {
                @Override
                public void customize(ConfigurableEmbeddedServletContainer container) {
                    container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/"));
                }
            };
        }
        

        【讨论】:

          猜你喜欢
          • 2020-12-03
          • 1970-01-01
          • 1970-01-01
          • 2016-02-11
          • 2021-08-13
          • 1970-01-01
          • 2011-04-29
          • 2015-12-31
          • 1970-01-01
          相关资源
          最近更新 更多