9

So far, I've been writing a bunch of endpoints for my Spring boot app, with no html UI side. Now, I want to serve the HTML file and js files which contain react code.

When I visit http://localhost:8443 I get:

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Sun Nov 19 11:54:01 PST 2017
There was an unexpected error (type=Not Found, status=404).
Not Found

What I did so far:

1.Added a web config class that extends WebMvcConfigurerAdapter:

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Bean
    public ViewResolver internalResourceViewResolver() {
        InternalResourceViewResolver bean = new InternalResourceViewResolver();
        bean.setPrefix("/resources/static/");
        bean.setSuffix(".html");
        return bean;
    }
}

2.Added a rest controller endpoint:

@RestController
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView getdata() {
        ModelAndView model = new ModelAndView("index");
        return model;
    }
}

I have myproject/src/main/resources/static/index.html file in my project.

Arian
  • 7,397
  • 21
  • 89
  • 177
  • do you want to server react app on every single page or there are some api-rest endpoints you need to keep? I mean how are you going to distinguish between react and spring endpoints, like `/static/*` for react static files, `/api/*` for spring endpoints and the rest `/*` is for react index.html ? – varren Nov 21 '17 at 20:03
  • 1
    Try to follow this tutorial from spring boot documentation https://spring.io/guides/tutorials/react-and-spring-data-rest/ – Sasha Shpota Nov 21 '17 at 20:04
  • If I understand you correctly, you just want to server static html and js files. In that case you do not need the Controller or the WebMvcConfigurerAdapter, just put the html and js files to src/main/resources/static (where your index.html is) and it will automatically gets served by Spring Boot. – helospark Nov 21 '17 at 20:42
  • @OleksandrShpota the tutorial suggests installing Thymleaf package, while I don't use any of its features. – Arian Nov 22 '17 at 00:43
  • @varren That's exactly what I want to do. I want to put all the rest api's under `/api/*`, put all web related stuff under `/static/*` and route `/*` to index.html. Any idea how I can do that ? – Arian Nov 22 '17 at 00:52

4 Answers4

10

Spring Boot can automatically handle static files (by convention), just put all of your html, js, css, etc. files to src/main/resources/static, remove your ViewResolver and Controller for '/' and it will work, index.html will also be mapped to / by Spring Boot as well.

Besides this, you can of course make REST endpoints with the api prefix by just using the correct @RequestMapping on your @RestControllers

helospark
  • 1,420
  • 9
  • 12
8

It really depends on your setup. Lets suppose you want something like: enter image description here

Lets look at simple case: no thymeleaf templates or spring static files. Spring is uses for serving rest api and the rest is up to react. But you can use controllers at any request mapping url.

One option is to use ResourceResolver and configure it like this:

@Configuration
public class Config implements WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        ResourceResolver resolver = new ReactResourceResolver();
        registry.addResourceHandler("/**")
                .resourceChain(true)
                .addResolver(resolver);

        // Can try to play with
        // registry.addResourceHandler("/**")
        //        .addResourceLocations("classpath:/static/");
        // But this option can't map every path to index.html
        // Can try https://stackoverflow.com/a/42998817/1032167
        // to resolve this, but then you loose /api/** => rest
        // and to be honest it is some regex madness, so
        // it was easier for me to setup custom resource resolver


    }

    public class ReactResourceResolver implements ResourceResolver {
        // root dir of react files
        // example REACT_DIR/index.html
        private static final String REACT_DIR = "/static/";

        // this is directory inside REACT_DIR for react static files
        // example REACT_DIR/REACT_STATIC_DIR/js/
        // example REACT_DIR/REACT_STATIC_DIR/css/
        private static final String REACT_STATIC_DIR = "static";

        private Resource index = new ClassPathResource(REACT_DIR + "index.html");
        private List<String> rootStaticFiles = Arrays.asList("favicon.io",
                "asset-manifest.json", "manifest.json", "service-worker.js");

        @Override
        public Resource resolveResource(
            HttpServletRequest request, String requestPath,
            List<? extends Resource> locations, ResourceResolverChain chain) {

            return resolve(requestPath, locations);
        }

        @Override
        public String resolveUrlPath(
            String resourcePath, List<? extends Resource> locations,
            ResourceResolverChain chain) {

            Resource resolvedResource = resolve(resourcePath, locations);
            if (resolvedResource == null) {
                return null;
            }
            try {
                return resolvedResource.getURL().toString();
            } catch (IOException e) {
                return resolvedResource.getFilename();
            }
        }

        private Resource resolve(
            String requestPath, List<? extends Resource> locations) {

            if (requestPath == null) return null;

            if (rootStaticFiles.contains(requestPath)
                    || requestPath.startsWith(REACT_STATIC_DIR)) {
                return new ClassPathResource(REACT_DIR + requestPath);
            } else
                return index;
        }

    }
}

Here is full working demo for Spring 2.0.0.M4: https://github.com/varren/SpringBootReactExample


I had similar problem with a little bit different setup: Spring single page for every url route and subroute "/a/** => /a/index.html except /a/static/**".

And there is also an option to use regex Spring catch all route for index.html to kinda partially solve the problem, but i had no luck with this approach

varren
  • 14,551
  • 2
  • 41
  • 72
2

You need a @Controller that returns a ModelAndView

    @Controller
    public class HomeController {

        @RequestMapping(value = {"/", "/index.html"})
        public ModelAndView sellerHome(HttpServletRequest request, 
                                       HttpServletResponse response) {
             return new ModelAndView("../static/index");
        }

    }

You can then access http://localhost:8443 and should see your index page given that you configured the port correctly.

  • I get this error: `There was an unexpected error (type=Internal Server Error, status=500). javax.servlet.ServletException: Could not get RequestDispatcher for [../static/index]: Check that the corresponding file exists within your web application archive!` – Arian Nov 22 '17 at 00:50
1

Just so you know, in Spring Boot 2.1.4 the new configuration is:

package com.simplicity.resourceserver.configs;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/", "classpath:/resources/",
            "classpath:/static/", "classpath:/public/" };



    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);

        registry.addResourceHandler("/vanilla/**")
                .addResourceLocations("classpath:/vanilla/");

            registry.addResourceHandler("/react/**")
                    .addResourceLocations("classpath:/react/")
                .resourceChain(true);
    }

    @Bean
    public ViewResolver getViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setSuffix(".html");
        return resolver;
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseTrailingSlashMatch(true);
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/react")
                .setViewName("forward:/react/index.html");
        registry.addViewController("/react/{path:[^\\.]*}")
                .setViewName("forward:/react/index.html");
        registry.addViewController("/react/**/{path:[^\\.]*}")
                .setViewName("forward:/react/index.html");

    }
}

Spring Boot should be defined as "video poker" based programming: try something thousands of times (like when you play video poker) with little different details until it works.

In my case I have two different apps in two separate folders under src/main/resources

  • src/main/resources/vanilla
  • src/main/resources/react

In my case they are two proof concepts, but it could be a micro-frontend architecture.

Davide Pugliese
  • 366
  • 7
  • 16