Springboot/Angular2 - How to handle HTML5 urls? - spring

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?

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")

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!

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

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

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

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>

These are the three steps you need to follow:
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;
}
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]
Get rid of the useHash (or set it to false) from your routing declarations:
RouterModule.forRoot(routes)
or
RouterModule.forRoot(routes, {useHash: false})

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.

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";
}
}

Related

Spring boot How to add prefix "/api" to certain urls, but not on "/" "/login"

springboot 2.2.4 version
so i was trying to make prefix to every controller on my application
"/api"
i have done by following code
//DispatcherServletCustomConfiguration.java
#Configuration
public class DispatcherServletCustomConfiguration {
#Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
#Bean
public DispatcherServletRegistrationBean dispatcherServletRegistration() {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(
dispatcherServlet(), "/api/");
registration.setName(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME);
return registration;
}
}
but i want to exclude certain urls that returns static resources
such as "/" "/login" "/404page"
those url need to return index.html
however by adding those prefix, "index.html" is mapped to /api/
how can i distinguish url that return static resources(image, html, css)
and api calls that returns json to add prefix
ex) there is too many controller to add requestMapping for each controller
1st solution:
Remove the DispatcherServletRegistrationBean configuration and configure nothing at servlet level.
Use #RequestMapping("/api") on all the controllers at class level. Then you can achieve that the "/api" will be appended at the initial path for all requests and also the "/", "/login" paths can also be called succesfully.
2nd solution:
add url mappings in the config like below:
package com.test.sampleproject;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
#Configuration
public class AppConfig {
#Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
#SuppressWarnings("rawtypes")
#Bean
public ServletRegistrationBean axisServletRegistrationBean() {
#SuppressWarnings("unchecked")
ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet(), "/*");
registration.addUrlMappings("/api/*");
return registration;
}
}
Controller class:
package com.test.sampleproject.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class RetryController {
#GetMapping("/test1")
public String sampleApi1() {
return "test1 api called";
}
#GetMapping("/")
public String sampleApi2() {
return "index";
}
}
The following url's are valid from this config:
localhost:8080/ ---> will hit the "/" path method in
RetryConroller
localhost:8080/api/test1 ---> will hit the "test1" path method in
RetryConroller
localhost:8080/test1 ---> will hit the "test1" path method in
RetryConroller

SpringBoot resource versioning failing due to null ResourceUrlProvider in ResourceUrlEncodingFilter.resolveUrlPath

I have a fairly plain Spring Boot 2.3.3 application created straight from the fine articles at various popular tutorial sites.
I want to enable static resource versioning. This is a standard problem in web development - if you are not familiar with static resource versioning, please look it up before attempting to interpret my question.
So I have this in my WebConfig class:
#Bean
public FilterRegistrationBean<?> resourceUrlEncodingFilterRegistration() {
FilterRegistrationBean<ResourceUrlEncodingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new ResourceUrlEncodingFilter());
registration.addUrlPatterns("/*");
return registration;
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/clientlibs/**")
.addResourceLocations("classpath:/clientlibs/", "classpath:/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addContentVersionStrategy("/**"))
.addTransformer(new CssLinkResourceTransformer());
}
And this in my web.xml:
<filter>
<filter-name>resourceUrlEncodingFilter</filter-name>
<filter-class>
org.springframework.web.servlet.resource.ResourceUrlEncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>resourceUrlEncodingFilter</filter-name>
<servlet-name>servletSpringDispatcher</servlet-name>
</filter-mapping>
And this in my spring-context.xml:
<mvc:resources location="/clientlibs/" mapping="/clientlibs/**" />
And for good measure, I also have this application.properties:
# Enable HTML5 application cache manifest rewriting.
spring.resources.chain.html-application-cache=true
# Enable the Spring Resource Handling chain. Disabled by default unless at least one strategy has been enabled.
spring.resources.chain.enabled=true
# Enable the content Version Strategy.
spring.resources.chain.strategy.content.enabled=true
# Comma-separated list of patterns to apply to the Version Strategy.
spring.resources.chain.strategy.content.paths=/**
My static resources are located in WEB-INF, but I also tried moving them to src/main/resources, for no change in behaviour.
Oh, and this pattern is seen in my template.html:
<link rel="stylesheet" th:href="#{/clientlibs/styles/custom/_global.css}" href="../../../clientlibs/styles/custom/_global.css"/>
So here the expected behaviour is that my resources would be versioned - the reference to _global in the example above would be rendered with a hex string.
But the actual behaviour is that nothing happens. There are no warnings, no errors, but also the _global.css is spit out to the browser unmodified.
So I did some deep investigation by stepping through the Spring Framework code in the debugger. I found this in ResourceUrlEncodingFilter.class:
#Nullable
public String resolveUrlPath(String url) {
if (this.resourceUrlProvider == null) {
logger.trace("ResourceUrlProvider not available via request attribute " +
ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR);
return null;
}
if (this.indexLookupPath != null && url.startsWith(this.prefixLookupPath)) {
int suffixIndex = getEndPathIndex(url);
String suffix = url.substring(suffixIndex);
String lookupPath = url.substring(this.indexLookupPath, suffixIndex);
lookupPath = this.resourceUrlProvider.getForLookupPath(lookupPath);
if (lookupPath != null) {
return this.prefixLookupPath + lookupPath + suffix;
}
}
return null;
}
Where I found that the this.resourceUrlProvider == null is what's happening.
Assuming this is the root cause of my problem, why is it null and how can I fix it?
STRANGE THINGS
If I change to .resourceChain(false), then the solution partially works - versioning is present on the outbound links, but inbound is not recognized.
I gave up trying to use the spring framework solution for this. I have gone through every possible tutorial, SO question, Spring documentation, step through Spring code, etc. Nothing works.
Finally implemented a custom solution in a simple filter:
package ctc.front.web.util;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.web.filter.GenericFilterBean;
public class SimpleResourceVersioningFilter extends GenericFilterBean {
// location of static resources in the public URL
private static final String RESOURCE_URL_PREFIX = "/clientlibs/";
// location of static resources in the classpath
private static final String RESOURCE_CLASSPATH_PREFIX = "/static/";
private static final Map<String,String> hashCache = new ConcurrentHashMap<>();
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
filterChain.doFilter(request,
new Wrapper((HttpServletResponse) response));
}
private static class Wrapper extends HttpServletResponseWrapper {
Wrapper(HttpServletResponse wrapped) {
super(wrapped);
}
#Override
public String encodeURL(String url) {
if (url.startsWith(RESOURCE_URL_PREFIX)) {
return url + "?" + hashCache.computeIfAbsent(url, key -> {
try (InputStream is = getClass().getClassLoader()
.getResourceAsStream(RESOURCE_CLASSPATH_PREFIX + url)) {
if (is == null) {
throw new FileNotFoundException(url);
}
return DigestUtils.sha1Hex(is);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} else
return url;
}
}
}
This solution appends the version to the query string, so it's not necessary to implement any kind of un-filtering on the inbound requests. Adjust your CDN cache configurations accordingly.
I have a Springboot (v2.4.2) project with Thymeleaf, also with spring security. This is what I did to enable resource versioning or cache buster.
in my application.yaml file added
spring:
web:
resources:
chain:
enabled: true
strategy:
content:
enabled: true
paths: /**/*.js,/**/*.css,/js/**,/css/**,/icon/**
and added a configuration to enable ResourceUrlEncodingFilter as Bean.
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;
#Configuration
public class CacheBusterMVCConfig implements WebMvcConfigurer {
#Bean
#ConditionalOnEnabledResourceChain
public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
return new ResourceUrlEncodingFilter();
}
}
and have this in my SecurityConfig
#Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/js/**", "/css/**", "/icon/**", "/webjars/**");
}
Now, I can see version for static resource.
<link href="/webjars/bulma/0.9.1/css/bulma.min-281a08362e797a0922f6074359ad15be.css" rel="stylesheet"/>
<link href="/webjars/font-awesome/5.15.2/css/all.min-c4af24ce595437830af0a401897698b2.css" rel="stylesheet"/>
<link href="/icon/favicon-32x32-9ff600ad27cf43113130456752dc2ef6.png" rel="icon" sizes="32x32" type="image/png"/>
<link href="/icon/favicon-16x16-9501d9cfa7e2976c8bb3dd745d2bd503.png" rel="icon" sizes="16x16" type="image/png"/>

Spring boot Angular routing [duplicate]

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?
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")
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!
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;
}
};
}
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());
}
}
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:/";
}
}
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>
These are the three steps you need to follow:
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;
}
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]
Get rid of the useHash (or set it to false) from your routing declarations:
RouterModule.forRoot(routes)
or
RouterModule.forRoot(routes, {useHash: false})
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.
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";
}
}

Spring catch all route for index.html

I'm developing a spring backend for a react-based single page application where I'm using react-router for client-side routing.
Beside the index.html page the backend serves data on the path /api/**.
In order to serve my index.html from src/main/resources/public/index.html on the root path / of my application I added a resource handler
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/").addResourceLocations("/index.html");
}
What I want to is to serve the index.html page whenever no other route matches, e.g. when I call a path other than /api.
How do I configure such catch-all route in spring?
Since my react app could use the root as forward target this ended up working for me
#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:/");
}
}
To be honest I have no idea why it has to be exactly in this specific format to avoid infinite forwarding loop.
I have a Polymer-based PWA hosted inside of my Spring Boot app, along with static web resources like images, and a REST API under "/api/...". I want the client-side app to handle the URL routing for the PWA. Here's what I use:
#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/");
}
}
This should work for Angular and React apps as well.
Avoid #EnableWebMvc
By default Spring-Boot serves static content in src/main/resources:
/META-INF/resources/
/resources/
/static/
/public/
Take a look at this and this;
Or keep #EnableWebMvc and override addViewControllers
Did you specify #EnableWebMvc ? Take a look a this: Java Spring Boot: How to map my app root (“/”) to index.html?
Either you remove #EnableWebMvc, or you can re-define addViewControllers:
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("forward:/index.html");
}
Or define a Controller to catch /
You may take a look a this spring-boot-reactjs sample project on github:
It does what you want using a Controller:
#Controller
public class HomeController {
#RequestMapping(value = "/")
public String index() {
return "index";
}
}
Its index.html is under src/main/resources/templates
I use react and react-router in my spring boot app, and it was as easy as creating a controller that has mapping to / and subtrees of my website like /users/**
Here is my solution
#Controller
public class SinglePageAppController {
#RequestMapping(value = {"/", "/users/**", "/campaigns/**"})
public String index() {
return "index";
}
}
Api calls aren't caught by this controller and resources are handled automatically.
Found an answer by looking at this question
#Bean
public EmbeddedServletContainerCustomizer notFoundCustomizer() {
return new EmbeddedServletContainerCustomizer() {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/"));
}
};
}
Another solution (change/add/remove myurl1, myurl2, ... with your routes):
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:/";
}
}
Note: Using public String index() also works fine, but only if you use templates. And the use of WebMvcConfigurerAdapter is deprecated.
To answer your specific question which involves serving up the Single Page App (SPA) in all cases except the /api route here is what I did to modify Petri's answer.
I have a template named polymer that contains the index.html for my SPA. So the challenge became let's forward all routes except /api and /public-api to that view.
In my WebMvcConfigurerAdapter I override addViewControllers and used the regular expression: ^((?!/api/|/public-api/).)*$
In your case you want the regular expression: ^((?!/api/).)*$
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/{spring:^((?!/api/).)*$}").setViewName("polymer");
super.addViewControllers(registry);
}
This results in being able to hit http://localhost or http://localhost/community to serve up my SPA and all of the rest calls that the SPA makes being successfully routed to http://localhost/api/posts, http://localhost/public-api/posts, etc.
After lot of tries I've found the following solution as most simple one. It will basically bypass all the Spring handling which was so difficult to deal with.
#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());
}
}

How do I create a 404 controller using Spring Boot?

I'd like to return a custom 404 error using SpringBoot, but I'd like to be able to add some server-side logic to it, not just serve a static page.
1. I switched off the default whitelabel page in application.properties
error.whitelabel.enabled=false
2. I added a Thymeleaf error.html under resources/templates
This works by the way. The page is served, but no controller is called.
3. I created a class Error to be the "Controller"
package com.noxgroup.nitro.pages;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
#Controller
#RequestMapping("/error")
public class Error {
#ExceptionHandler
public String index() {
System.out.println("Returning Error");
return "index";
}
}
Unfortunately, I'm not seeing Returning Error printed anywhere in the console.
I'm using the Embedded Tomcat with Spring Boot. I've seen various options, non of which seem to work including using #ControllerAdvice, removing the RequestMapping, etc. Neither work for me.
The servlet container is going to pick up the 404 before it can get to Spring, so you'll need to define an error page at servlet container level, which forwards to your custom controller.
#Component
public class CustomizationBean implements EmbeddedServletContainerCustomizer {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/error"));
}
}
Easiest way I found was to implement the ErrorController.
#Controller
public class RedirectUnknownUrls implements ErrorController {
#GetMapping("/error")
public void redirectNonExistentUrlsToHome(HttpServletResponse response) throws IOException {
response.sendRedirect("/");
}
#Override
public String getErrorPath() {
return "/error";
}
}

Resources