2

I am trying to setup versioned services with Spring MVC, using inheritance to extend older controllers to avoid rewriting unchanged controller methods. I've based my solution on a previous question about versioning services, however I've run into a problem with ambiguous mappings.

@Controller
@RequestMapping({"/rest/v1/bookmark"})
public class BookmarkJsonController {

  @ResponseBody
  @RequestMapping(value = "/write", produces = "application/json", method = RequestMethod.POST)
  public Map<String, String> writeBookmark(@RequestParam String parameter) {
    // Perform some operations and return String
  }
}

@Controller
@RequestMapping({"/rest/v2/bookmark"})
public class BookmarkJsonControllerV2 extends BookmarkJsonController {

  @ResponseBody
  @RequestMapping(value = "/write", produces = "application/json", method = RequestMethod.POST)
  public BookmarkJsonModel writeBookmark(@RequestBody @Valid BookmarkJsonModel bookmark) {
    // Perform some operations and return BookmarkJsonModel
  }
}

With this setup I get IllegalStateException: Ambiguous mapping found. My thought regarding this is that because I have two methods with different return/argument types I have two methods in BookmarkJsonControllerV2 with the same mapping. As a workaround I attempted to override writeBookmark in BookmarkJsonControllerV2 without any request mapping:

@Override
public Map<String, String> writeBookmark(@RequestParam String parameter) {
  return null; // Shouldn't actually be used
}

However, when I compiled and ran this code I still got the exception for an ambiguous mapping. However, when I hit the URL /rest/v2/bookmark/write I got back an empty/null response. Upon changing return null to:

return new HashMap<String, String>() {{
  put("This is called from /rest/v2/bookmark/write", "?!");
}};

I would receive JSON with that map, indicating that despite not having any request mapping annotation, it is apparently "inheriting" the annotation from the super class. At this point, my only "solution" to future-proofing the extension of the controllers is to make every controller return Object and only have the HttpServletRequest and HttpServletResponse objects as arguments. This seems like a total hack and I would rather never do this.

So is there a better approach to achieve URL versioning using Spring MVC that allows me to only override updated methods in subsequent versions or is my only real option to completely rewrite each controller?

Community
  • 1
  • 1
  • Can you post your applicationContext.xml please? Are you manually writing the beans or by a scan package? The mix of both of these configurations may cause this problem. – Deividi Cavarzan Jul 30 '13 at 20:32
  • @DeividiCavarzan I do have a handful of beans setup in `application-context.xml` related to Spring Security, the rest are setup via scanning a package. It's a bit of a moot point thought now as I've gotten this working by using a different handler mapping for my REST services. –  Aug 01 '13 at 11:28

1 Answers1

3

For whatever reason, using the @RequestMapping annotation was causing the ambiguous mapping exceptions. As a workaround I decided to try using springmvc-router for my REST services which would allow me to leverage inheritance on my controller classes so I would not have to reimplement endpoints that did not change between versions as desired. My solution also allowed me to continue using annotation mappings for my non-REST controllers.

Note: I am using Spring 3.1, which has different classes for the handler mappings than previous versions.

The springmvc-router project brings the router system from the Play framework over to Spring MVC. Inside of my application-context.xml, the relevant setup looks like:

<mvc:annotation-driven/>
<bean id="handlerAdapter" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" />

<bean class="org.resthub.web.springmvc.router.RouterHandlerMapping">
    <property name="routeFiles">
        <list>
            <value>routes/routes.conf</value>
        </list>
    </property>
    <property name="order" value="0" />
</bean>

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
    <property name="order" value="1" />
</bean>

This allows me to continue using my annotated controllers alongside the router. Spring uses a chain-of-responsibility system, so we can assign multiple mapping handlers. From here, I have a router configuration like so:

# Original Services

POST    /rest/bookmark/write     bookmarkJsonController.write
POST    /rest/bookmark/delete    bookmarkJsonController.delete

# Version 2 Services

POST    /rest/v2/bookmark/write  bookmarkJsonControllerV2.write
POST    /rest/v2/bookmark/delete bookmarkJsonControllerV2.delete

Alongside controllers looking like:

@Controller
public class BookmarkJsonController {
  @ResponseBody
  public Map<String, Boolean> write(@RequestParam String param) { /* Actions go here */ }

  @ResponseBody
  public Map<String, Boolean> delete(@RequestParam String param) { /* Actions go here */ }
}

@Controller
public class BookmarkJsonControllerV2 extends BoomarkJsonController {
  @ResponseBody
  public Model write(@RequestBody Model model) { /* Actions go here */ }
}

With a configuration like this, the URL /rest/v2/bookmark/write will hit the method BookmarkJsonControllerV2.write(Model model) and the URL /rest/v2/bookmark/delete will hit the inherited method BookmarkJsonController.delete(String param).

The only disadvantage from this comes from having to redefine entire routes for new versions, as opposed to changing the @RequestMapping(value = "/rest/bookmark") to @RequestMapping(value = "/rest/v2/bookmark") on the class.