Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Springboot/Angular2 - How to handle HTML5 urls?

I believe this is a simple question, but I couldn't find an answer or at least use the correct terms in the search.

I am setting up Angular2 and Springboot together. By default, Angular will use paths like localhost:8080\dashboard and localhost:8080\dashboard\detail.

I'd like to avoid using path as hashs, if possible. As Angular documentation states:

The router's provideRouter function sets the LocationStrategy to the PathLocationStrategy, making it the default strategy. We can switch to the HashLocationStrategy with an override during the bootstrapping process if we prefer it.

And then...

Almost all Angular 2 projects should use the default HTML 5 style. It produces URLs that are easier for users to understand. And it preserves the option to do server-side rendering later.

The issue is that when I try to access localhost:8080\dashboard, Spring will look for some controller mapping to this path, which it won't have.

Whitelabel Error Page
There was an unexpected error (type=Not Found, status=404).
No message available

I thought initially to make all my services to be under localhost:8080\api and all my static under localhost:8080\app. But how do I tell Spring to ignore requests to this app path?

Is there a better solution with either Angular2 or Boot?

like image 701
Felipe S. Avatar asked Jul 22 '16 01:07

Felipe S.


3 Answers

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")
like image 173
davidxxx Avatar answered Nov 14 '22 02:11

davidxxx


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!

like image 56
AndroidLover Avatar answered Nov 14 '22 02:11

AndroidLover


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;
        }
    };
}
like image 16
Dmitry Serdiuk Avatar answered Nov 14 '22 00:11

Dmitry Serdiuk