32

I have a simple Controller that looks like this:-

@Controller
@RequestMapping(value = "/groups")
public class GroupsController {
    // mapping #1
    @RequestMapping(method = RequestMethod.GET)
    public String main(@ModelAttribute GroupForm groupForm, Model model) {
        ...
    }

    // mapping #2
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public String changeGroup(@PathVariable Long id, @ModelAttribute GroupForm groupForm, Model model) {
        ...
    }

    // mapping #3
    @RequestMapping(method = RequestMethod.POST)
    public String save(@Valid @ModelAttribute GroupForm groupForm, BindingResult bindingResult, Model model) {
        ...
    }
}

Basically, this page has the following functionalities:-

  • User visits main page (/groups GET).
  • User creates a new group (/groups POST) or selects a specific group (/groups/1 GET).
  • User edits an existing group (/groups/1 POST).

I understand how both GET request mappings work here. Mapping #2 is defined, otherwise (/groups/1 GET) will cause a "No mapping found" exception.

What I'm trying to understand here is why mapping #3 handles both (/groups POST) and (/groups/1 POST)? It makes sense that it should handle (/groups POST) here since the request mapping matches the URI. Why (/groups/1 POST) isn't causing a "No mapping found" exception being thrown here? In fact, it almost seems like any POST with URI beginning with /groups (ex: /groups/bla/1 POST) will also be handled by mapping #3.

Can someone provide a clear explanation of this to me? Thanks much.

CLARIFICATION

I understand the fact that I can use more appropriate methods (like GET, POST, PUT or DELETE)... or I can create yet another request mapping to handle /groups/{id} POST.

However, what I want to really know is...

.... "Why does mapping #3 handle /groups/1 POST too?"

The "closest match" reasoning don't seem to hold true because if I remove mapping #2, then I would think mapping #1 will handle /groups/1 GET, but it doesn't and it causes a "No mapping found" exception.

I'm just a little stumped here.

Aniket Kulkarni
  • 12,825
  • 9
  • 67
  • 90
limc
  • 39,366
  • 20
  • 100
  • 145
  • Why not use PUT for the update of a resource? That would be the proper HTTP protocol. – Eric Winter Mar 16 '12 at 14:23
  • The web form submission only supports GET and POST, and I'm not doing AJAX call here, thus I can't rely on PUT and DELETE at this point. – limc Mar 16 '12 at 14:25
  • @limc, that is not really true, POSTs can be modified (on server side) to an other Request type with help of `org.springframework.web.filter.HiddenHttpMethodFilter` – Ralph Mar 16 '12 at 14:53
  • @Ralph, my bad... I was reading about that just now, and I realized that I can use the `_method` hack. I'm going through the spring source code right now, according to your post. – limc Mar 16 '12 at 15:05

4 Answers4

19

This is complicated, I think it is better to read the code.

In Spring 3.0 The magic is done by method public Method resolveHandlerMethod(HttpServletRequest request) of the inner class ServletHandlerMethodResolver of org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.

An instance of this class exists for every Request Controller Class, and has a field handlerMethods that contains a list of all the request methods.

But let me summarize how I understand it

  • Spring first checks if at least one handler method matches (this can contain false negatives)
  • Then it creates a map of all really matching handler methods
  • Then it sorts the map by request path: RequestSpecificMappingInfoComparator
  • and takes the first one

The sorting works this way: the RequestSpecificMappingInfoComparator first compares the path with the help of an AntPathMatcher, if two methods are equal according to this, then other metrics (like number of parameters, number of headers, etc.) are taken into account with respect to the request.

Nathan Hughes
  • 94,330
  • 19
  • 181
  • 276
Ralph
  • 118,862
  • 56
  • 287
  • 383
  • 4
    Wow... I look through `resolveHandlerMethod(...)`, talk about super high cyclomatic complexity code, I got lost after nth nested if-statements. I read the javadoc on `RequestSpecificMappingInfoComparator` that talks about the order list. I'm curious why they don't behave the same for GET and POST methods. In another word, if I remove mapping #2, why mapping #1 isn't handling `/groups/1 GET` but instead, Spring throws an exception... – limc Mar 16 '12 at 16:00
  • @Ralph - nice explanation of the internals – raddykrish Mar 17 '12 at 16:51
2

Spring tries to find the mapping which matches the closest.
Hence, in your case of any POST request, the only map found for the request type is Mapping# 3. Neither of Mapping 1 or Mapping 2 matches your request type, and hence are ignored. May be you can try removing the Mapping #3, and see that Spring throws a runtime error since it does not find a match!

PaiS
  • 1,282
  • 5
  • 16
  • 23
  • 1
    I initially thought the same too that Spring is finding the closest match. However, I realized it isn't totally true because if that is the case, I should be able to remove mapping #2, and `/groups/1 GET` should be handled by mapping #1 then since it is the closest match... but I'm getting a "No mapping found" exception here. I couldn't find any Spring documentation that explains more on this situation. – limc Mar 16 '12 at 14:04
1

I would add a PUT mapping for /groups/{id}. I guess POST would work too but not strictly correct from a HTTP perspective.

adding @RequestMapping("/{id}", POST) should cover it?

Eric Winter
  • 960
  • 1
  • 8
  • 17
  • How do I submit the web form using PUT, without using AJAX calls? I'm still interested to know why Spring behaves this way with my current situation. – limc Mar 16 '12 at 14:29
  • Maybe also check out how to mock a PUT with spring.http://stackoverflow.com/questions/4362791/can-spring-mvc-handle-requests-from-html-forms-other-then-post-and-get – Eric Winter Mar 16 '12 at 14:31
  • You don't have a handler defined for the groups/{id} mapping. I would also consider it a bug because I would be hard pressed to see the use case for the behavior you are seeing. – Eric Winter Mar 16 '12 at 14:46
-3

add @PathVariable to the Long id parameter in mapping #2

AdrianS
  • 1,980
  • 7
  • 33
  • 51