Spring Boot url mappings order for controllers and static pages - spring

I have a Spring Boot web application which is meant to serve both static and controller based (ModelAndView) pages. Problem is that a controller can serve something like /{string} and a static page must be served with /test.
The problem is that the controller mapping takes precedence, and I need to avoid that. If the user hits /test, he must be forwarded to the test.html static page.
I tried to use the order property of ViewControllerRegistry in this way, with no success:
#Configuration
public class MyWebMvcConfig extends WebMvcConfigurerAdapter {
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/test").setViewName("forward:/test.html");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE); // but I tried with 0 and -1 as well: annotated controllers should have order equals to 0
}
}
This is my SpringBootApplication class:
#SpringBootApplication
public class VipApplication {
public static void main(String[] args) {
SpringApplication.run(VipApplication.class, args);
}
}
And this is the controller code:
#Controller
public class VipController {
#RequestMapping(value = "/{string}")
public ModelAndView vip(#PathVariable("string") String string) {
ModelAndView mv = new ModelAndView("mypage");
return mv;
}
}
How can I reorder the mappings to make sure static pages are considered before annotated controllers?

(I'm not sure, but) I suggest to override WebMvcConfigurerAdapter.addResourceHandlers() method and configure order of resource handler by invoking ResourceHandlerRegistry.setOrder()

Related

Why is my browser downloading file instead of rendering SpringBoot & Sitemesh output?

I'm trying to use SpringBoot with Freemarker and Sitemesh.
When I go to a URL at the moment the request is handled by the application, data loaded and HTML output generated, but for some reason the browser has decided it wants to download the file (which contains the correct content) rather than rendering it as a page.
This was working a while back, trouble is I'm not sure which change I've made has broken it!
Sitemesh filter:
#WebFilter
public class SitemeshFilter extends ConfigurableSiteMeshFilter {
private static final Logger LOG = Logger.getLogger(SitemeshFilter.class);
#Override
protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
LOG.debug("SiteMeshFilter creation");
builder.addDecoratorPath("/*", "/templates/main.ftl")
.addExcludedPath("/h2console/*");
}
}
Application:
#ServletComponentScan
#SpringBootApplication
#EnableAutoConfiguration(exclude = {ErrorMvcAutoConfiguration.class})
public class ClubManagementApplication {
private static Logger LOG = Logger.getLogger(ClubManagementApplication.class);
public static void main(String[] args) {
SpringApplication.run(ClubManagementApplication.class, args);
}
}
Snippet of controller:
#Controller
public class ClubController {
#Autowired
ClubService clubService;
#RequestMapping(value = {"Club/{id}","club/{id}"})
public ModelAndView viewClub(#PathVariable("id") int clubId) {
ModelAndView mv = new ModelAndView("club");
....
return mv;
}
}
EDIT:
From the HttpServletRequest object in controller...
accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
In the response headers:
Content-Type : application/octet-stream;charset=UTF-8
I guess the content type is the problem....just gotta find why it's being set like that.
In case someone else stumbles on this question, I changed my template file from an ftl to a html extension and suddenly it has woken up.
#Override
protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
LOG.debug("SiteMeshFilter creation");
//builder.addDecoratorPath("/*", "/templates/main.ftl");
builder.addDecoratorPath("/*", "/templates/main.html");
}

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

Spring RequestMapping with root path (/{custom})

Let's say my website name is: foo.com
When a user types foo.com, I want to show index.html.
When a user types foo.com/something, I want the server catches the request at the controller.
Here is what I did in the HomeController:
#Controller
public class HomeController {
#RequestMapping(value={"/"}, method=RequestMethod.GET)
public String getHome() {
return "index.html";
}
}
And, the CustomController should catch the request
#Controller
public class CustomController {
#RequestMapping(value={"/{custom}"}, method=RequestMethod.GET)
public String getCustom(#PathVariable String custom) {
// Do something here..
}
}
However, it throws an error: Circular view path [index.html]: would dispatch back to the current handler URL [/index.html] again. It's because the CustomController catches the GET request: foo.com/index.html after the HomeController returns the string: index.html.
I did some research like this:
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/assets"); // My asset
registry.addResourceHandler("index.html").addResourceLocations("file:/index.html");
} // It's not working
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("forward:/" + FileNames.INDEX);
} // This also not working
}
And changing the annotation from #Controller to #RestController in the CustomController is not an option.
Also, I don't have JSP files in the project - they are plain *.html files.
I am using Spring 1.3.3 release, so please help me out.
This solution works with ui-router (AngularJS library). Also, you have to change $resourceProvider setting:
// In your app module config
$resourceProvider.defaults.stripTrailingSlashes = false;
Then, in the Spring server codes, you can do this:
#RequestMapping(value = "/{custom:[^\\.]*}")
public String redirect(#PathVariable String custom) {
// Do something here...
return "forward:/";
}
Found the solution at this link: https://spring.io/blog/2015/05/13/modularizing-the-client-angular-js-and-spring-security-part-vii

Spring MVC TrailingSlash matching

I am using Spring MVC 4.1, and this is the core config:
#EnableWebMvc
#Configuration
#ComponentScan(basePackages = {""})
public class MvcConfig extends WebMvcConfigurerAdapter {
#Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(true);
configurer.setUseTrailingSlashMatch(false);
}
....
}
#Controller
#RequestMapping("/api/deps")
public class DepartmentCtrl {
#RequestMapping(value = "", method = RequestMethod.GET)
public Result index() {
return ...;
}
}
While when I open this the url:
/context/api/deps
I will get the result as expected, however once I will get a 404 once I visit the link:
/context/api/deps/
As shown, I have config the PathMatchConfigurer by setUseTrailingSlashMatch(false) , but it seems that it does not work.
Is there anything wrong in my configuration?
use this:
setUseTrailingSlashMatch(true)
According to javadoc, setUseTrailingSlashMatch determines:
Whether to match to URLs irrespective of the presence of a trailing
slash. If enabled a method mapped to "/users" also matches to
"/users/".
Honestly, there is no need for this piece of configuration, since its enabled by default and you by passing false to it, disabled it.
What happens when you pass "/" in the value attribute of RequestMapping Annotation?

Spring serving static content while having wildcard controller route

My application is build using backbone on frontend and spring framework on backend. It is a single html application. Routes are handled by backbone, so I have a backend route with the next structure:
#RequestMapping(value="/**", method=RequestMethod.GET)
public String Pages()
{
return "index";
}
To point everything to my index.html. The thing is that the static content
files are pointed to this route too, and I don't want this. I've tried to
config WebMvcConfigurerAdapter by overriding addResourceHandler method for
static content, but it doesn't work.
#Configuration
public class StaticResourceConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**").addResourceLocations("/resources/js");
}
}
How can I point every route to my index.html except /js/** and /assets/** ?
Thank you
The first thing is that your controller method that's mapped to /** will be taking priority over any resource requests. You can address this by increasing the precedence of ResourceHandlerRegistry. Add a call to registry.setOrder(Ordered.HIGHEST_PRECEDENCE) in the addResourceHandlers method of StaticResourceConfiguration:
#Configuration
public class StaticResourceConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
registry.addResourceHandler("/js/**").addResourceLocations("/resources/js");
}
}
The second thing is that, by default, Spring Boot configures two resource handlers for you by default, one mapped to /** and one mapped to /webjars/**. Due to the change described above, this will now take priority over the method in your controller that's also mapped to /**. To overcome this, you should turn off default resource handling via a setting in application.properties:
spring.resources.addMappings=false

Resources