Lanky Dan Blog

Request Mapping with multiple Rest Controllers

April 08, 2017

javaspringspring bootspring web
GITHUB REPO FOR POST

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.


Dan Newton

Saving transactions where only a subset of parties are signers

July 05, 2019
cordakotlindltdistributed ledger technologyblockchain

It took a while for me to think of a title that could summarise the contents of this post without becoming a full sentence itself. I think I…

Kotlin primitive and object arrays

June 21, 2019
kotlinjavabeginner

I initially set out to write this post because I was playing around with some reflection code and thought I found something interesting…

Preventing invalid spending of broadcasted states

June 12, 2019
cordakotlindltdistributed ledger technologyblockchain

Corda is super flexible and will allow you to put together the code needed to write many complex workflows. This flexibility does come with…

Broadcasting a transaction to external organisations

May 31, 2019
cordakotlindltdistributed ledger technologyblockchain

There is a misconception that Corda cannot broadcast data across a network. This is simply wrong. In fact, Corda can send anything between…

Running a Kotlin class as a subprocess

May 25, 2019
kotlin

Last week I wrote a post on running a Java class as a subprocess . That post was triggered by my need to run a class from within a test…