理解Spring MVC的@RequestMapping POST如何工作

32

我有一个简单的控制器,代码如下:

@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) {
        ...
    }
}
基本上,此页面具有以下功能:
- 用户访问主页面(/groups GET)。 - 用户创建新组(/groups POST)或选择特定组(/groups/1 GET)。 - 用户编辑现有组(/groups/1 POST)。
我了解这里两个GET请求映射的工作方式。如果没有定义映射#2,则(/groups/1 GET)将导致"No mapping found"异常。
我想在这里理解的是为什么映射3处理(/groups POST)和(/groups / 1 POST)?它应该在此处处理(/groups POST),因为请求映射与URI匹配。为什么(/groups / 1 POST)不会在这里引发“No mapping found”异常呢?实际上,似乎任何以/groups开头的POST URI(例如:/groups/bla/1 POST)也都将由映射#3处理。
有人能给我提供一个清晰的解释吗?非常感谢。
澄清:
我知道我可以使用更适当的方法(如GET、POST、PUT或DELETE)...或者我可以创建另一个请求映射来处理/groups/{id} POST。
然而,我真正想知道的是...
.... "为什么映射#3也处理/groups/1 POST?"
“最接近匹配”的推理似乎并不成立,因为如果我删除映射#2,那么我认为映射#1将处理/groups/1 GET,但它并没有,而是导致了"No mapping found"异常。
我只是有点困惑。

为什么不使用PUT来更新资源?这将是正确的HTTP协议。 - Eric Winter
网页表单提交仅支持GET和POST,由于我没有进行AJAX调用,因此我目前不能依赖PUT和DELETE。 - limc
@limc,那不完全是真的,使用org.springframework.web.filter.HiddenHttpMethodFilter可以将POSTs(在服务器端)修改为其他请求类型。 - Ralph
@Ralph,我的错...我刚刚看到了相关内容,意识到我可以使用_method技巧。根据你的帖子,我现在正在查看Spring源代码。 - limc
4个回答

19

这很复杂,我认为最好阅读代码。

在Spring 3.0中,这个魔法是由org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter的内部类ServletHandlerMethodResolver的方法public Method resolveHandlerMethod(HttpServletRequest request)完成的。

对于每个请求控制器类,都会存在这个类的一个实例,并且它有一个handlerMethods字段,其中包含所有请求方法的列表。

但让我总结一下我的理解:

  • 首先,Spring检查是否至少有一个处理程序方法匹配(这可能包含错误的匹配)
  • 然后,它创建一个所有真正匹配的处理程序方法的映射表
  • 接下来,它按照请求路径排序:使用RequestSpecificMappingInfoComparator
  • 并选择第一个

排序的方式是这样的:RequestSpecificMappingInfoComparator首先使用AntPathMatcher比较路径,如果两种方法相等,则考虑其他指标(例如参数数量、头文件数量等)与请求相关。


4
哇哦...我看了一下 resolveHandlerMethod(...) 的代码,真是超高循环复杂度啊,嵌套的 if 语句把我绕晕了。我读了一下 RequestSpecificMappingInfoComparator 的 Javadoc,它谈到了顺序列表(order list)。我很好奇为什么它们在 GET 和 POST 方法上表现不同。换句话说,如果我删除映射 #2,为什么映射 #1 没有处理 /groups/1 GET 而是 Spring 抛出了一个异常... - limc
@Ralph - 内部机制的解释很好。 - raddykrish

2

Spring试图找到最匹配的映射。
因此,在任何POST请求的情况下,请求类型的唯一映射是映射#3。 映射1和映射2都不与您的请求类型匹配,因此被忽略。 也许你可以尝试删除映射#3,并查看Spring是否会抛出运行时错误,因为它找不到匹配项!


1
我最初也认为Spring是在寻找最接近的匹配项。然而,我意识到这并不完全正确,因为如果是这样的话,我应该能够删除映射#2,然后/groups/1 GET应该由映射#1处理,因为它是最接近的匹配项...但是我在这里却得到了一个“未找到映射”的异常。我无法找到任何Spring文档来解释这种情况。 - limc

1
我会为/groups/{id}添加一个PUT映射。POST也可以,但从HTTP的角度来看,不是严格正确的。
加上@RequestMapping("/{id}", POST)就可以了吗?

如何使用PUT提交Web表单,而不使用AJAX调用?我仍然想知道Spring在我的当前情况下为什么会这样行事。 - limc
也许还可以查看如何使用Spring模拟PUT请求。https://dev59.com/7G855IYBdhLWcg3wZzbf - Eric Winter
您没有为groups/{id}映射定义处理程序。我认为这也是一个错误,因为我很难看到您所看到的行为的用例。 - Eric Winter

-3
在映射 #2 中的 Long id 参数上添加 @PathVariable。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接