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

7

26

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

}
Jackinthepulpit answered 20/8, 2012 at 19:10 Comment(6)
Change your url pattern in your web xml from /ui/* to /uiDiantha
@Sajan, I should have included the other RequestMappings. (See edit) Changing the pattern from /ui/* to /ui breaks all the mappings and I get 404 for URIs /ui, /ui/ and /ui/otherJackinthepulpit
What context is the webapp deployed to in Tomcat? Is your application called ROOT.war or <another_name>.war or does the application just exist in a directory under webapps?Hundredth
@andyb, it is deployed to another context. In my example I use "myapp". Am I wrong to think this should work?Jackinthepulpit
No, it definitely should work as expected. Which version of Spring are you using? Also, how are you configuring Spring, something like <context:component-scan base-package="..."/>?Hundredth
Spring 3.1.2. Yep, got the component-scan scanning the appropriate package.Jackinthepulpit
H
17

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!

Hundredth answered 22/8, 2012 at 15:13 Comment(0)
R
18

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();
    }
}
Rondon answered 16/12, 2016 at 20:49 Comment(0)
H
17

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!

Hundredth answered 22/8, 2012 at 15:13 Comment(0)
D
13

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!

Devitalize answered 22/7, 2017 at 20:2 Comment(1)
Didn't know about this property. Thanks!Sit
J
3

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.

Jackinthepulpit answered 21/8, 2012 at 16:24 Comment(0)
C
2

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);
}
Conducive answered 24/3, 2015 at 19:43 Comment(1)
The only solution that worked for mi so far, thank you!Dicho
S
1

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

Sardonic answered 14/5, 2014 at 14:6 Comment(1)
Advanced @RequestMapping tricks - This is a broken link. Plz update the new one or remove the link.Bilyeu
H
0

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.

Heartbreaking answered 16/4, 2022 at 17:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.