Spring的catch all路由用于index.html

60

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

除了index.html页面外,后端还在路径/api/**上提供数据。

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

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/").addResourceLocations("/index.html");
}
我想要的是在没有其他路由匹配时提供index.html页面,例如当我调用除/api之外的路径时。

我该如何在Spring中配置这样的捕获所有路由?

9个回答

47

由于我的React应用程序可以使用根作为前向目标,因此这对我起作用了。

@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:/");
  }
}

说实话,我不知道为什么要以这种特定格式避免无限转发循环。


1
我在第三个addViewController方法调用中遇到了问题。我的情况是我有一个未被服务的webfonts文件夹。我认为这是因为正则表达式只指定了js和css扩展名。最终我删除了第三个并添加了一个新的资源处理程序。 - Lichader
10
你好,spring前缀在代码中代表什么意思? - hguser
5
正则表达式无法匹配连字符(-)和下划线(_)。您可以将\\w替换为^[a-zA-Z\d-_],以使其适用于包含这些字符的URL。 - victor.ja
2
@hguser spring是一个变量名,用于分配匹配部分。但是,它仅在此处用于启用AntMatcher(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/AntPathMatcher.html)中的正则表达式的使用,并且没有填充任何目的,因为它没有被使用。您可以将其更改为其他任何内容,但其目的是启用与正则表达式的匹配。要使用它,请在路径中重定向或转发到`{spring}`,然后您将使用匹配的任何内容。最后一个条目不明确,因为相同的变量分配了两个匹配... - fast-reflexes
1
我该如何使用PathPatternParser来完成这个任务?Spring Boot将默认值从AntPathMatcher更改为了它。 - F43nd1r
显示剩余7条评论

32

我有一个基于Polymer的PWA托管在我的Spring Boot应用程序内,以及像图片这样的静态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应用程序。


1
这对我不起作用,我们将Web应用程序路径设置为/resources/static/index.html,但仍然无法正常工作,我只能得到来自任何RestController的随机视图。 - Yago Rodriguez
5
谢谢,这对我有用 - 我只需要将resourceLocation更改为classpath:/static/即可在我的React应用程序中运行它。请注意,自Spring 5.x.x.起,扩展 WebMvcConfigurerAdapter已被弃用,必须改为实现WebMvcConfigurer - mtsz
这个比当前得票最高的答案更清晰、更好地解释了 :) - bluemind
这对我有用,但它无法处理末尾的斜杠。有什么建议吗? - sometimes24

12

避免使用 @EnableWebMvc

默认情况下,Spring Boot 会在 src/main/resources 目录下提供静态资源服务:

  • /META-INF/resources/
  • /resources/
  • /static/
  • /public/

可以查看 这个这个 进一步了解;

或者您可以保留 @EnableWebMvc 并重写 addViewControllers 方法

您是否使用了 @EnableWebMvc?可以参考这篇文章:Java Spring Boot:如何将应用程序根目录(“/”)映射到index.html?

您可以删除 @EnableWebMvc,或重新定义 addViewControllers 方法。

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

或者定义一个控制器来捕获/

您可以查看这个GitHub上的spring-boot-reactjs示例项目

它使用控制器实现了您想要的功能:

@Controller
public class HomeController {

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

}

它的index.html位于src/main/resources/templates下面。


1
我认为这应该被标记为您问题的答案。 - pesoklp13
而不是在模板文件夹中使用index.html,我使用probe.html。如果按照这个例子进行操作,我会得到:“找不到GET /probe的处理程序”错误。 我还尝试过使用index.html,但是我得到了同样的错误。 - Angelina
9
这并没有解决问题中要求的客户端路由。它仅为根路径提供“index.html”,不会重定向像“/campaigns/33”这样的URL到“index.html”。 - Radek Matěj

9

我在我的Spring Boot应用中使用React和React-Router,只需创建一个映射到/和我的网站子树/users/**的控制器,就可以轻松地实现了。以下是我的解决方案:

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

这个控制器无法捕获API调用,而且资源会自动处理。


这对我不起作用。我得到了“未找到GET /index的处理程序”。 - Angelina
请尝试使用 "index.html" 而不是 "index"。 - epsilonmajorquezero
我的当前项目遵循这种方法,但我们有像“/portal/**”,“/portal/user/**”,“/portal/profile/**”这样的URL。正常导航可以工作。但是当我们在URL上刷新,比如说“/portal/user”,我会得到错误信息:“Circular view path [index.html]: would dispatch back to the current handler URL.” 如何处理这个问题?你有什么想法吗? - this.srivastava

2

通过查看 这个问题 找到了答案。

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

1

另一种解决方案(更改/添加/删除您的路由中的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的使用已被弃用。


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());
    }
}

0
我想分享一个基于Jurass答案的解决方案。
Spring Boot 3.1 + SPA Angular应用程序位于/resources/static文件夹中。
以下是过滤器:
private Filter staticResourceFilter() {
    return (request, response, chain) -> {
        String path = ((HttpServletRequest) request).getRequestURI();

        boolean isApi = path.startsWith("/api/v1");
        boolean isStaticResource = path.matches(".*\\.(js|css|ico|html)");

        if (isApi || isStaticResource) {
            chain.doFilter(request, response);
        } else {
            request.getRequestDispatcher("/index.html").forward(request, response);
        }
    };
}

在Spring Security过滤器链中的使用方式是什么?
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .cors(withDefaults())
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(authorizeConfig -> authorizeConfig
            .requestMatchers("/index.html", "/*.js", "/*.css", "/*.ico",
                "/api/v1/auth/login",
                // others routes...
            ).permitAll()
            .anyRequest().fullyAuthenticated()
        )
        .addFilterBefore(staticResourceFilter(), AuthorizationFilter.class)
        // others security stuff (oauth2, etc.)
    return http.build();
}

所有不是API调用或静态资源的请求都将被转发到/index.html页面,以便Angular接管路由过程。

0

针对您的具体问题,涉及在所有情况下提供单页应用程序(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://localhost或者http://localhost/community来提供我的SPA,并且所有SPA发起的其余请求都能成功路由到http://localhost/api/postshttp://localhost/public-api/posts等。

这个程序可以从 / 开始导航到 SPA,但无法添加到书签并且无法在刷新后保存状态。 - mtsz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接