'How do I map Spring MVC controller to a uri with and without trailing slash?

I have a Spring Controller with several RequestMappings for different URIs. My servlet is "ui". The servlet's base URI only works with a trailing slash. I would like my users to not have to enter the trailing slash.

This URI works:

http://localhost/myapp/ui/

This one does not:

http://localhost/myapp/ui

It gives me a HTTP Status 404 message.

The servlet and mapping from my web.xml are:

<servlet>
    <servlet-name>ui</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>ui</servlet-name>
    <url-pattern>/ui/*</url-pattern>
</servlet-mapping>    

My Controller:

@Controller
public class UiRootController {

    @RequestMapping(value={"","/"})
    public ModelAndView mainPage() { 
        DataModel model = initModel();
        model.setView("intro");     
        return new ModelAndView("main", "model", model);
    }

    @RequestMapping(value={"/other"})
    public ModelAndView otherPage() { 
        DataModel model = initModel();
        model.setView("otherPage");     
        return new ModelAndView("other", "model", model);
    }

}


Solution 1:[1]

If your web application exists in the web server's webapps directory, for example webapps/myapp/ then the root of this application context can be accessed at http://localhost:8080/myapp/ assuming the default Tomcat port. This should work with or without the trailing slash, I think by default - certainly that is the case in Jetty v8.1.5

Once you hit /myapp the Spring DispatcherServlet takes over, routing requests to the <servlet-name> as configured in your web.xml, which in your case is /ui/*.

The DispatcherServlet then routes all requests from http://localhost/myapp/ui/ to the @Controllers.

In the Controller itself you can use @RequestMapping(value = "/*") for the mainPage() method, which will result in both http://localhost/myapp/ui/ and http://localhost/myapp/ui being routed to mainPage().

Note: you should also be using Spring >= v3.0.3 due to SPR-7064

For completeness, here are the files I tested this with:

src/main/java/controllers/UIRootController.java

package controllers;

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

@Controller
public class UiRootController {
  @RequestMapping(value = "/*")
  public ModelAndView mainPage() {
    return new ModelAndView("index");
  }

  @RequestMapping(value={"/other"})
  public ModelAndView otherPage() {
    return new ModelAndView("other");
  }
}

WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0" metadata-complete="false">
  <servlet>
    <servlet-name>ui</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    <!-- spring automatically discovers /WEB-INF/<servlet-name>-servlet.xml -->
  </servlet>

  <servlet-mapping>
    <servlet-name>ui</servlet-name>
    <url-pattern>/ui/*</url-pattern>
  </servlet-mapping>
</web-app>

WEB-INF/ui-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/context
  http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<context:component-scan base-package="controllers" />

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
  p:order="2"
  p:viewClass="org.springframework.web.servlet.view.JstlView"
  p:prefix="/WEB-INF/views/"
  p:suffix=".jsp"/>
</beans>

And also 2 JSP files at WEB-INF/views/index.jsp and WEB-INF/views/other.jsp.

Result:

  • http://localhost/myapp/ -> directory listing
  • http://localhost/myapp/ui and http://localhost/myapp/ui/ -> index.jsp
  • http://localhost/myapp/ui/other and http://localhost/myapp/ui/other/ -> other.jsp

Hope this helps!

Solution 2:[2]

Using Springboot, my app could reply both with and without trailing slash by setting @RequestMapping's "value" option to the empty string:

@RestController
@RequestMapping("/some")
public class SomeController {
//                  value = "/" (default) ,
//                  would limit valid url to that with trailing slash.
    @RequestMapping(value = "", method = RequestMethod.GET)
    public Collection<Student> getAllStudents() {
        String msg = "getting all Students";
        out.println(msg);
        return StudentService.getAllStudents();
    }
}

Solution 3:[3]

PathMatchConfigurer api allows you to configure various settings related to URL mapping and path matching. As per the latest version of spring, trail path matching is enabled by default. For customization, check the below example.

For Java-based configuration

@Configuration
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseTrailingSlashMatch(true);
    }
}

For XML-based configuration

<mvc:annotation-driven>
    <mvc:path-matching trailing-slash="true"/>
</mvc:annotation-driven>

For @RequestMapping("/foo"), if trailing slash match set to false, example.com/foo/ != example.com/foo and if it's set to true (default), example.com/foo/ == example.com/foo

Cheers!

Solution 4:[4]

I eventually added a new RequestMapping to redirect the /ui requests to /ui/. Also removed the empty string mapping from the mainPage's RequestMapping. No edit required to web.xml.

Ended up with something like this in my controller:

    @RequestMapping(value="/ui")
    public ModelAndView redirectToMainPage() {
        return new ModelAndView("redirect:/ui/");
    }

    @RequestMapping(value="/")
    public ModelAndView mainPage() { 
        DataModel model = initModel();
        model.setView("intro");     
        return new ModelAndView("main", "model", model);
    }

    @RequestMapping(value={"/other"})
    public ModelAndView otherPage() { 
        DataModel model = initModel();
        model.setView("otherPage");     
        return new ModelAndView("other", "model", model);
    }

Now the URL http://myhost/myapp/ui redirects to http://myhost/myapp/ui/ and then my controller displays the introductory page.

Solution 5:[5]

Another solution I found is to not give the request mapping for mainPage() a value:

@RequestMapping
public ModelAndView mainPage() { 
    DataModel model = initModel();
    model.setView("intro");     
    return new ModelAndView("main", "model", model);
}

Solution 6:[6]

try adding

@RequestMapping(method = RequestMethod.GET) public String list() { return "redirect:/strategy/list"; }

the result:

    @RequestMapping(value = "/strategy")
    public class StrategyController {
    static Logger logger = LoggerFactory.getLogger(StrategyController.class);

    @Autowired
    private StrategyService strategyService;

    @Autowired
    private MessageSource messageSource;

    @RequestMapping(method = RequestMethod.GET)
    public String list() {
        return "redirect:/strategy/list";
    }   

    @RequestMapping(value = {"/", "/list"}, method = RequestMethod.GET)
    public String listOfStrategies(Model model) {
        logger.info("IN: Strategy/list-GET");

        List<Strategy> strategies = strategyService.getStrategies();
        model.addAttribute("strategies", strategies);

        // if there was an error in /add, we do not want to overwrite
        // the existing strategy object containing the errors.
        if (!model.containsAttribute("strategy")) {
            logger.info("Adding Strategy object to model");
            Strategy strategy = new Strategy();
            model.addAttribute("strategy", strategy);
        }
        return "strategy-list";
    }  

** credits:

Advanced @RequestMapping tricks – Controller root and URI Template

Solution 7:[7]

Not sure if this is the ideal approach, but what worked for me was to treat them as if they were two different paths and make them both accepted by each of my endpoints, such as.

@RestController
@RequestMapping("/api/mb/actor")
public class ActorController {

@GetMapping({"", "/"})
public ResponseEntity<Object> getAllActors() {

    ...
}

@GetMapping({"/{actorId}", "/{actorId}/"})
public ResponseEntity<Object> getActor(@PathVariable UUID actorId) {

    ...
}

There may be best ways to do this and to avoid this duplication, and I'd love to know that. However, what I found when I tried using configurer.setUseTrailingSlashMatch(true); is that broken paths also start becoming accepted, such as /api/mb////actor (with many slashs), and that's why I ended up going the multiple paths instead.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 andyb
Solution 2 user1767316
Solution 3
Solution 4 km1
Solution 5 Patrick
Solution 6 Brad Larson
Solution 7 Francislainy Campos