Request Mapping with multiple Rest Controllers

javaspringspring bootspring web

In this post we will look at a possible problem when multiple rest controllers are defined onto the same path and how to use multiple rest controllers within your application. I used Spring Boot to write this application.

So what happens when you have two rest controller defined onto the same path? If you don’t have any overlapping request mappings other than the code being slightly confusing, nothing will actually go wrong and you can successfully send requests to the methods inside each controller. But if you have the same mapping defined in each one, then you are going to run into a problem.

In the code below there are two different controllers where both are mapped to the same path and also each contain a mapping to the same location.

  • PersonRestController
@RestController
public class PersonRestController {

  @RequestMapping("/get")
  public PersonDTO getFromPersonController() {
    return new PersonDTO("Joe", "Blogs", new Date(), "Programmer", BigDecimal.ZERO);
  }

  @RequestMapping("/getPerson")
  public PersonDTO getPerson() {
    return new PersonDTO("Joe", "Blogs", new Date(), "Programmer", BigDecimal.ZERO);
  }
}
  • UserRestController
@RestController
public class UserRestController {

  @RequestMapping("/get")
  public UserDTO getFromUserController() {
    return new UserDTO("joeblogs", "Joe Blogs", "joeblogs@gmail.co.uk");
  }

  @RequestMapping("/getUser")
  public UserDTO getUser() {
    return new UserDTO("joeblogs", "Joe Blogs", "joeblogs@gmail.co.uk");
  }
}

If you were to start up you application now the following stack trace would appear in your console.

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: 
Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'userRestController' method
public lankydan.tutorial.springboot.dto.UserDTO lankydan.tutorial.springboot.controller.UserRestController.getFromUserController()
to {[/get]}: There is already 'personRestController' bean method
public lankydan.tutorial.springboot.dto.PersonDTO lankydan.tutorial.springboot.controller.PersonRestController.getFromPersonController() mapped.
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761) ~[spring-beans-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:866) ~[spring-context-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:542) ~[spring-context-4.3.6.RELEASE.jar:4.3.6.RELEASE]
 at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) ~[spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
 at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
 at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
 at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1162) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1151) [spring-boot-1.5.1.RELEASE.jar:1.5.1.RELEASE]
 at lankydan.tutorial.springboot.Application.main(Application.java:10) [classes/:na]
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'userRestController' method

From looking at this stack trace it paints quite a clear picture at what has gone wrong.

Starting from the top

Ambiguous mapping. Cannot map 'userRestController' method

telling us that there must be at least one other place that has the same mapping defined.

Going down a line

UserRestController.getFromUserController() to {[/get]}: There is already 'personRestController' bean method

Makes it even clearer. UserRestController.getFromUserController() is mapped to /get but there is already one on the PersonRestController.

And just to be sure

PersonRestController.getFromPersonController() mapped

Indicating which method in the PersonRestController has already taken the /get mapping.

This can be tested by commenting out the getFromUserController method in UserRestController, restarting the application and sending the request via postman.

The results will be

{
 "firstName": "Joe",
 "secondName": "Blogs",
 "dateOfBirth": "07/04/2017",
 "profession": "Programmer",
 "salary": 0
}

which is the JSON of the PersonDTO that was correctly returned from the get method in the PersonRestController, which is now the only request mapping trying to point to /get.

To double check that the other request mappings can be accessed we can send a request to /getUser and /getPerson which will both return the expected JSON.

A possible fix to this is to change the request mapping path of one of the controller methods. This will quickly fix this problem but suggests that these methods should be not only separated by class but also by their request mappings. Which leads onto a better solutions.

One solution is to manually append a base mapping to each @RequestMapping annotation.

This would be defined by

@RequestMapping("/person/get")
public PersonDTO getFromPersonController() {
  return new PersonDTO("Joe", "Blogs", new Date(), "Programmer", BigDecimal.ZERO);
}

and

@RequestMapping("/user/get")
public UserDTO getFromUserController() {
  return new UserDTO("joeblogs", "Joe Blogs", "joeblogs@gmail.co.uk");
}

This would fix the immediate problem but will not prevent further errors from occurring in the future unless you always remember to append the domain on each @RequestMapping

For example if we only altered the code above and not the other mappings in the controllers the paths that they would accept from would be

localhost:8080/person/get
localhost:8080/getPerson

and

localhost:8080/user/get
localhost:8080/getUser

In this scenario the correct solution would be to change the path that the controllers accept requests from. To do so we need to add the @RequestMapping annotation to each class in a similar way that it has already been used on the methods.

@RestController
@RequestMapping("/person")
public class PersonRestController {

  @RequestMapping("/get")
  public PersonDTO getFromPersonController() {
    return new PersonDTO("Joe", "Blogs", new Date(), "Programmer", BigDecimal.ZERO);
  }

  @RequestMapping("/getPerson")
  public PersonDTO getPerson() {
    return new PersonDTO("Joe", "Blogs", new Date(), "Programmer", BigDecimal.ZERO);
  }
}

and

@RestController
@RequestMapping("/user")
public class UserRestController {

  @RequestMapping("/get")
  public UserDTO getFromUserController() {
    return new UserDTO("joeblogs", "Joe Blogs", "joeblogs@gmail.co.uk");
  }

  @RequestMapping("/getUser")
  public UserDTO getUser() {
    return new UserDTO("joeblogs", "Joe Blogs", "joeblogs@gmail.co.uk");
  }
}

This is more likely to reduce conflicts of mappings as each class will have its own specific base mapping and then each method’s path will be built upon it.

Now the PersonRestController accepts requests from

localhost:8080/person/...

for example

localhost:8080/person/get
localhost:8080/person/getPerson

The UserRestController accepts from

localhost:8080/user/...

for example

localhost:8080/user/get
localhost:8080/user/getUser

Not only does it prevent the conflicts but it is much clearer from looking at the request what controller it will eventually go to, which in the future could help with debugging.

After reading this post you should know how to set the path of a rest controller and understand the consequences if you don’t. As well as some reasons for setting the path via the controller to make the code tidier and less likely to run into issues in the future.

The code for this post can be found on my Github.

Written by Dan Newton
Twitter
LinkedIn
GitHub