52

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?

Felipe S.
  • 1,633
  • 2
  • 16
  • 23

9 Answers9

63

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")
davidxxx
  • 125,838
  • 23
  • 214
  • 215
  • Should `WebMvcConfig` extends `WebMvcConfigurerAdapter` instead of implementing `WebMvcConfigurer` since it's an interface? – Sydney Oct 30 '17 at 09:01
  • 1
    If you use Spring Boot 1, yes you should use `WebMvcConfigurerAdapter` . But in Spring Boot 2, it was deprecated, `WebMvcConfigurer` 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. – davidxxx Oct 30 '17 at 09:17
  • I updated to make a clear distinction according to the version. – davidxxx Oct 30 '17 at 09:33
  • I had exclude the my angular app url from security config along with this. All is well except the images part. I had images in assets in agular those are not displaying now. Also I had other static html in public folder which are not working now. – Mahendra Oct 09 '18 at 06:05
  • @Mahendra did you resolve this issue, assets not working – Krish May 05 '19 at 10:34
  • @Krish assets are probably not being loaded because it routes all requests to index.html, including *.js, *.png, *.ttf, etc, etc? (unless I understood wrong) – Raid Jun 30 '20 at 05:01
  • 1
    This is the best Solution and should be the accepted answer. – p.sherif Dec 15 '20 at 09:40
  • None of solutions worked for me.. I have upgraded to Spring 2.7 recently and hitting url localhost:8888 doesn’t redirect me to index.html it gives 404 error & warning in spring saying - No mapping for GET / – NewBie Jul 06 '22 at 07:26
57

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!

Hasson
  • 1,894
  • 1
  • 21
  • 25
AndroidLover
  • 1,171
  • 1
  • 13
  • 16
  • if i use this method, i always get a full page refresh :( – Hans Feb 01 '17 at 07:25
  • @Hans no you should not get a full page refresh u have another problem – AndroidLover Feb 02 '17 at 11:16
  • 1
    @AndroidLover no all right, only get full page refresh if i reload with f5 oder press enter the new url. but thats how it should be. i thought wrong... – Hans Feb 02 '17 at 13:03
  • @AndroidLover you also saved my day! :) but I am wondering if there is a way not to provide all `url's` to redirect to `index.html` but just provide some `generic` solution that whatever I type "wrong" it will also redirect to `index.html` something like: `@RequestMapping({ "/" })` -> but it doesn't work :( – bielas Oct 21 '17 at 21:14
  • 1
    @bielas Of course, you have. You even have several ways to do it. IMHO, the most natural is customizing the Spring MVC configuration. https://stackoverflow.com/a/46854105/270371 – davidxxx Nov 24 '17 at 04:26
  • 2
    This answer works. However, I believe this is the BEST ANSWER: https://stackoverflow.com/questions/38516667/springboot-angular2-how-to-handle-html5-urls/46854105#46854105 Thank you @davidxxx – john Oct 19 '18 at 22:07
  • This working, but I would not recommend this because when you add some new routes, you will have to modify the backend side. So prefer @davidxxx answer. – Peter S. Feb 25 '20 at 14:42
  • Looking at David's answer, it routes ALL resource paths to index.html? Doesn't that also route the *.js, *.png, *.ttf, etc, etc to index.html? How would the server pass those resources? – Raid Jun 30 '20 at 05:00
  • @Raid it routes to index.html just if the resource can't be resolved. It is delegating to Angular the job to deal with the 404 errors. I think it is a really good approach. – Iogui May 08 '21 at 21:35
  • @Raid The problem with that approach is that 404 errors are meant to be dealt with in the server side. If they are not, Angular receives a nonexistent route and it generates a javascript error on browser console. – Iogui May 08 '21 at 22:16
  • @Raid For the 404 problem, one can follow this approach: https://www.techiediaries.com/angular-9-8-path-redirection-and-handling-404-using-wildcard-routes/ – Iogui May 08 '21 at 22:41
  • it will work only if both Angular + Spring are in the same project no ? – Adir Dayan Jul 21 '21 at 12:29
16

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;
        }
    };
}
Dmitry Serdiuk
  • 468
  • 3
  • 17
  • To add an explanation, ErrorViewResolver is an interface that needs to be implemented by your class having the @Configuration annotation, other then that, this is a good dynamic solution, handling over the responsability for errorhandling to the Angular app, running inside the Spring Boot app. – Learning Nov 23 '16 at 11:28
  • Since I am using Angular 6, I had to use "index" instead of "index.html". – Brandon Dudek Aug 29 '18 at 11:53
  • That's the solution i finally went for. It would be cleaner to return a `HttpStatus` of type redirect instead of OK though, it makes more sense semantically. – gotson Jul 23 '19 at 05:59
  • `HttpStatus.NOT_FOUND` should be used instead of `OK`. – daiscog Jan 07 '20 at 20:00
11

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());
    }
}
Loren
  • 9,783
  • 4
  • 39
  • 49
  • In my case, all I needed was @RequestMapping(value = "/{:[^\\.]*}") in the ForwardController – r590 Jul 31 '18 at 21:20
3

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:/";
    }
}
Akhil Bojedla
  • 1,968
  • 12
  • 19
1

I did it with a plain old filter:

public class PathLocationStrategyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

        if(request instanceof HttpServletRequest) {
            HttpServletRequest servletRequest = (HttpServletRequest) request;

            String uri = servletRequest.getRequestURI();
            String contextPath = servletRequest.getContextPath();
            if(!uri.startsWith(contextPath + "/api") && 
                !uri.startsWith(contextPath + "/assets") &&
                !uri.equals(contextPath) &&
                // only forward if there's no file extension (exclude *.js, *.css etc)
                uri.matches("^([^.]+)$")) {

                RequestDispatcher dispatcher = request.getRequestDispatcher("/");
                dispatcher.forward(request, response);
                return;
            }
        }        

        chain.doFilter(request, response);
    }
}

Then in web.xml:

<web-app>
    <filter>
        <filter-name>PathLocationStrategyFilter</filter-name>
        <filter-class>mypackage.PathLocationStrategyFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>PathLocationStrategyFilter</filter-name>
        <url-pattern>*</url-pattern>
    </filter-mapping>
</web-app>
fidke
  • 151
  • 3
  • 2
0

These are the three steps you need to follow:

  1. Implement your own TomcatEmbeddedServletContainerFactory bean and set up the RewriteValve

      import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;  
      ...
      import org.apache.catalina.valves.rewrite.RewriteValve; 
      ... 
    
      @Bean TomcatEmbeddedServletContainerFactory servletContainerFactory() {
        TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
        factory.setPort(8080);
        factory.addContextValves(new RewriteValve());
        return factory;
      }
    
  2. Add a rewrite.conf file to the WEB-INF directory of your application and specify the rewrite rules. Here is an example rewrite.conf content, which I'm using in the angular application to take advantage of the angular's PathLocationStrategy (basicly I just redirect everything to the index.html as we just use spring boot to serve the static web content, otherwise you need to filter your controllers out in the RewriteCond rule):

      RewriteCond %{REQUEST_URI} !^.*\.(bmp|css|gif|htc|html?|ico|jpe?g|js|pdf|png|swf|txt|xml|svg|eot|woff|woff2|ttf|map)$
      RewriteRule ^(.*)$ /index.html [L]
    
  3. Get rid of the useHash (or set it to false) from your routing declarations:

      RouterModule.forRoot(routes)
    

or

      RouterModule.forRoot(routes, {useHash: false})
  • My app is a standalone jar with embedded tomcat. Where is the WEB-INF directory suppose to be? I only know about my /src/main/resources/public folder where I put all my angular 4 static htmls. – Neeraj Verma Jan 05 '18 at 00:47
0

forward all Angular routing with index.html. Including base href.

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

@Controller
public class ViewController {

@RequestMapping({ "jsa/customer","jsa/customer/{id}",})
   public String index() {
       return "forward:/index.html";
   }
}

In my case jsa is base href.

Anis Mulla
  • 75
  • 7
0

in my opinion the best way is to separate the User Interface paths and API paths by adding a prefix to them and serve the UI app entrypoint (index.html) for every path that matches UI prefix:

step 1 - add a prefix for all your UI paths (for example /app/page1, /app/page2, /app/page3, /app/page2/section01 and so on).

step 2 - copy UI files (HTML, JS, CSS, ...) into /resources/static/

step 3 - serve index.html for every path that begins with /app/ by a controller like this:

@Controller
public class SPAController {
    @RequestMapping(value = "/app/**", method = RequestMethod.GET)
    public ResponseEntity<String> defaultPath() {
        try {
            // Jar
            InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("/static/index.html");
            // IDE
            if (inputStream == null) {
                inputStream = this.getClass().getResourceAsStream("/static/index.html");
            }
            String body = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
            return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(body);
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error in redirecting to index");
        }
    }

    @GetMapping(value = "/")
    public String home(){
        return "redirect:/app";
    }
}
mhrsalehi
  • 1,124
  • 1
  • 12
  • 29