Background: I am the developer of AngularJ Universal - a library that provides server side rendering support for Angular 2+ applications. Because it is quite hard to know all routes of a SPA application, the user can use a custom written Spring Boot starter and define all routes via a property value. The starter then registers endpoints for these routes and handles the matching requests by a custom view resolver/view.
Let's say we have an Angular application that registers the routes /, /home and /about. For reading the initial page on the server side, an index file index.html is placed in src/main/resources/public.
Problem The Spring Boot auto configuration finds a "welcome page" in src/main/resources/public/index.html and configures a WelcomePageHandlerMapping for the endpoint /.
For serving static resources, this is a well known and desired behavior, but I have all my resources in src/main/resources/public because the initial server side rendering process is done on the server side and after serving that to the client, the client code kicks in and accesses resources from the src/main/resources/public directory.
I tried to override this behavior with the following code snippet, but the WelcomePageHandlerMapping is still registered in the end:
public class GenericAdapter implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
ViewControllerRegistration registration = registry.addViewController("/");
registration.setViewName("/");
}
}
Sadly that doesn't work: Take a look at the missing / endpoint before /home and at the trailing WelcomePageHandlerMapping endpoint:
2018-03-03 22:51:05.874 INFO 17324 --- [ restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/home] onto handler of type [class org.springframework.web.servlet.mvc.ParameterizableViewController]
2018-03-03 22:51:05.874 INFO 17324 --- [ restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/about] onto handler of type [class org.springframework.web.servlet.mvc.ParameterizableViewController]
2018-03-03 22:51:05.887 INFO 17324 --- [ restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-03-03 22:51:05.887 INFO 17324 --- [ restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-03-03 22:51:05.917 INFO 17324 --- [ restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-03-03 22:51:05.940 INFO 17324 --- [ restartedMain] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [public/index.html]
Solution: So I am looking for a way to disable the WelcomePageHandlerMapping:
-
It's not a problem of Spring Boot. I should rename my index.html so it is not detected as default welcome page (Would be hard and annoying, but somehow understandable)
-
Provide a way that
ViewControllerRegistration.addViewControllercan override theWelcomePageHandlerMappingendpoint -
Provide a way to disable
WelcomePageHandlerMappingat all
Replication: Bellow you will find a minimal working example. Please keep in mind:
-
Don't forget to create a welcome page in
src/main/resources/public/index.htmlfor testing - otherwise you will not be able to reproduce the problem -
To provide a minimal working example, I had to "rip out" some code of my Spring Boot starter. The classes/beans
GenericAdapter(Including thegetGenericAdapterbean),GenericViewResolverandGenericVieware located in the Spring Boot starter.
You can find the original starter here: https://github.com/swaechter/angularj-universal/tree/master/angularj-universal-spring-boot-starter
(In case I made some mistakes/abuse an API, please feel free to mention that)
package ch.swaechter.viewtest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.AbstractTemplateView;
import org.springframework.web.servlet.view.AbstractTemplateViewResolver;
import org.springframework.web.servlet.view.AbstractUrlBasedView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@SpringBootApplication
public class ViewControllerApplication {
// Routes loaded by my starter via properties
private final List<String> routes = Arrays.asList("/", "/home", "/about");
public static void main(String[] args) {
SpringApplication.run(ViewControllerApplication.class, args);
}
// Comment in the code bellow to override the welcome page - a thing that doesn't work with ViewControllerRegistry.addViewController
/*@Controller
@RequestMapping("/")
public class ContentController {
@GetMapping("/")
public ModelAndView overrideWelcomePage() {
return new ModelAndView("/");
}
}*/
// All code bellow this line belongs to the custom Spring Boot starter and is just here to provide a minimal working example
// Custom web MVC configurer registered as bean (My Spring Boot starter is loaded via @AutoConfigureAfter(WebMvcAutoConfiguration.class)
@Bean
public WebMvcConfigurer getGenericAdapter() {
return new GenericAdapter();
}
/**
* Add all application routes dynamically
*/
@Component // Annotation is only here to provide a minimal working example
public class GenericAdapter implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
for (String url : routes) {
ViewControllerRegistration registration = registry.addViewController(url);
registration.setViewName(url);
}
}
}
/**
* Provide a view resolver that supports our application routes
*/
@Component // Annotation is only here to provide a minimal working example
public class GenericViewResolver extends AbstractTemplateViewResolver {
public GenericViewResolver() {
setViewClass(requiredViewClass());
setOrder(0);
}
@Override
public Class<?> requiredViewClass() {
return GenericView.class;
}
@Override
public boolean canHandle(String modelname, Locale locale) {
return routes.contains(modelname);
}
@Override
public AbstractUrlBasedView buildView(String uri) {
return new GenericView();
}
}
/**
* Render our application requests and write the result to the HTTP response
*/
public class GenericView extends AbstractTemplateView {
@Override
protected boolean isUrlRequired() {
return false;
}
@Override
protected void renderMergedTemplateModel(Map<String, Object> map, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Note: The URI is passed to an Angular application that is server side rendered
String uri = request.getRequestURI().substring(request.getContextPath().length());
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("Rendered SPA application for: " + uri);
writer.flush();
writer.close();
}
}
}
Comment From: swaechter
CC'ing @bclozel because we had a discussion about that via Gitter
Comment From: swaechter
Hey, @bclozel, any thoughts on this?
Comment From: bclozel
Hi @swaechter
You're right, there is currently no configuration property to disable the welcome page support - as soon as an index.html file is present, this is enabled.
The WelcomePageHandlerMapping is ordered at 0 by default, which explains why it is considered before any registered ViewController. Those are registered under a SimpleUrlHandlerMapping ordered itself at 1.
I think we should change the default order of the WelcomePageHandlerMapping to respect developers' opinions if they provide one about that.
The favicon HandlerMapping is ordered at Ordered.HIGHEST_PRECEDENCE + 1 but I think we should order WelcomePageHandlerMapping at Ordered.LOWEST_PRECEDENCE - 1. Would that work for you?
Comment From: wilkinsona
Reopening as https://github.com/spring-projects/spring-boot/commit/220f8cdca29810dd5c2d6561f66c6d89ab13b8b1 appears to have broken spring-boot-sample-web-static:
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 4.627 s <<< FAILURE! - in sample.web.staticcontent.SampleWebStaticApplicationTests
[ERROR] testHome(sample.web.staticcontent.SampleWebStaticApplicationTests) Time elapsed: 0.164 s <<< FAILURE!
org.junit.ComparisonFailure: expected:<[200]> but was:<[404]>
at sample.web.staticcontent.SampleWebStaticApplicationTests.testHome(SampleWebStaticApplicationTests.java:48)
Comment From: bclozel
Thanks @wilkinsona - the chosen order is not the right one and collides with the registration order for ResourceHttpRequestHandler. I've moved it to 2, right after the SimpleUrlHandlerMapping for ViewController instances.
Comment From: swaechter
@bclozel Sorry for not answering earlier. Your explanation sounded reasonable and everything works in the latest 2.0.1 release. So thank you very much for the time & change!
Comment From: mmichalowicznuix
@bclozel @swaechter It's now 2023, I'm on spring-boot 2.7.9, I have an integration test against a @Controller with a GetMapping for / which has exclusion logic in it, meaning my business logic should 404, so a priority doesn't work here in my case. I just want to disable that bean altogether but I can't seem to excludeName=welcomePageHandlerMapping. I need a custom route for / without meddling from Spring autoconfigure. I've tried to remove it, but my logic involves creating the correct route through MockMvcBuilders.webAppContextSetup(WebApplicationContext)
@GetMapping(value = "/", headers = "!SomeHeader")
public String handleApplicationRoot() {
return "forward:/index.html";
}
Comment From: bclozel
@mmichalowicznuix asking a question on a 5 year old issue is not the right way to proceed here. Please create a new issue explaining the expected behavior, what you've tried and what you are seeing instead. Feel free to link to this issue as a reference.