Springboot/Angular2 - How to handle HTML5 urls?

Solution 1:

In my Spring Boot applications (version 1 and 2), my static resources are at a single place :

src/main/resources/static

static being a folder recognized by Spring Boot to load static resources.

Then the idea is to customize the Spring MVC configuration.
The simpler way is using Spring Java configuration.

I implement WebMvcConfigurer to override addResourceHandlers(). I add in a single ResourceHandler to the current ResourceHandlerRegistry.
The handler is mapped on every request and I specify classpath:/static/ as resource location value (you may of course adding others if required).
I add a custom PathResourceResolver anonymous class to override getResource(String resourcePath, Resource location).
And the rule to return the resource is the following : if the resource exists and is readable (so it is a file), I return it. Otherwise, by default I return the index.html page. Which is the expected behavior to handle HTML 5 urls.

Spring Boot 1.X Application :

Extending org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter is the way.
The class is an adapter of the WebMvcConfigurer interface with empty methods allowing sub-classes to override only the methods they're interested in.

Here is the full code :

import java.io.IOException;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.resource.PathResourceResolver;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

       
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

    registry.addResourceHandler("/**/*")
        .addResourceLocations("classpath:/static/")
        .resourceChain(true)
        .addResolver(new PathResourceResolver() {
            @Override
            protected Resource getResource(String resourcePath,
                Resource location) throws IOException {
                  Resource requestedResource = location.createRelative(resourcePath);
                  return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
                : new ClassPathResource("/static/index.html");
            }
        });
    }
}

Spring Boot 2.X Application :

org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter was deprecated.
Implementing directly WebMvcConfigurer is the way now as it is still an interface but it has now default methods (made possible by a Java 8 baseline) and can be implemented directly without the need for the adapter.

Here is the full code :

import java.io.IOException;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

      registry.addResourceHandler("/**/*")
        .addResourceLocations("classpath:/static/")
        .resourceChain(true)
        .addResolver(new PathResourceResolver() {
            @Override
            protected Resource getResource(String resourcePath,
                Resource location) throws IOException {
                Resource requestedResource = location.createRelative(resourcePath);
                return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
                : new ClassPathResource("/static/index.html");
            }
        });
    }
}

EDIT to address some comments :

For those that store their static resources at another location as src/main/resources/static, change the value of the var args parameter of addResourcesLocations() consequently.
For example if you have static resources both in static and in the public folder (no tried) :

  registry.addResourceHandler("/**/*")
    .addResourceLocations("classpath:/static/", "/public")

Solution 2:

I have a solution for you, you can add a ViewController to forward requests to Angular from Spring boot.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ViewController {

@RequestMapping({ "/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers" })
   public String index() {
       return "forward:/index.html";
   }
}

here I have redirected all my angular2 ("/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers") to my SPA You can do the same for your preject and you can also redirect your 404 error page to the index page as a further step. Enjoy!

Solution 3:

You can forward all not found resources to your main page by providing custom ErrorViewResolver. All you need to do is to add this to your @Configuration class:

@Bean
ErrorViewResolver supportPathBasedLocationStrategyWithoutHashes() {
    return new ErrorViewResolver() {
        @Override
        public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
            return status == HttpStatus.NOT_FOUND
                    ? new ModelAndView("index.html", Collections.<String, Object>emptyMap(), HttpStatus.OK)
                    : null;
        }
    };
}

Solution 4:

You can forward everything not mapped to Angular using something like this:

@Controller
public class ForwardController {

    @RequestMapping(value = "/**/{[path:[^\\.]*}")
    public String redirect() {
        // Forward to home page so that route is preserved.
        return "forward:/";
    }
} 

Source: https://stackoverflow.com/a/44850886/3854385

My Spring Boot server for angular is also a gateway server with the API calls to /api to not have a login page in front of the angular pages, you can use something like.

import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

/**
 * This sets up basic authentication for the microservice, it is here to prevent
 * massive screwups, many applications will require more secuity, some will require less
 */

@EnableOAuth2Sso
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .logout().logoutSuccessUrl("/").and()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .anyRequest().permitAll().and()
                .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

Solution 5:

To make it more simple you can just implement ErrorPageRegistrar directly..

@Component
public class ErrorPageConfig implements ErrorPageRegistrar {

    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/"));
    }

}

This would forward the requests to index.html.

@Controller
@RequestMapping("/")
public class MainPageController {

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping({ "/" })
    public String forward() {
        return "forward:/";
    }
}