Recently I ran across a missing method="post" in a <form ...> tag within a Thymeleaf template which was working despite an @PostMapping annotation of the SpringFramework on the controller method. I was wondering how this could happen and did some investigation. Hopefully this article gives a better chance for the next one who runs into this problem.
The method looked like this:
@PostMapping
@RequestMapping("something")
public String saveOrUpdate(@ModelAttribute SomethingCommand command){
SomethingCommand savedCommand = [...];
return "redirect:/something/" + savedCommand.getId() + "/show";
}By the way, even after adding the method="post" to the form, the method was still called.
Finding the Cause#
First I verified that there there no filter configured which adds this attribute, but the HTML source in the browser did not contain this attribute either. Then I checked the variables in the call stack to the controller method in a debugger and I could clearly see, that it was really called with a GET request. Next, I had a look at the JavaDocs, but I could not find anything relevant. Of cause I also did a web search and still no result.
@PostMapping is actually a shortcut for @RequestMapping( method = RequestMethod.POST, ...), but which was also annotated at the same method and thus caused confusion.
How it Actually Works#
Then I tried various annotations on the saveOrUpdate() method and found the results shown in the comments:
// becomes effectively a mapping for "something" with any submit method,
// thus a @RequestMapping after a @PostMapping gets precedence over the @PostMapping
@PostMapping
@RequestMapping("something")
public String saveOrUpdate(@ModelAttribute SomethingCommand command){
long id = callSomethingService(command);
return "redirect:/something/" + id + "/show";
}
// becomes effectively a mapping for "somethingReverse" with any submit method,
// thus, @PostMapping after a @RequestMapping has no effect.
// If same path is used as in the previous method, Spring notices the double mapping and complains:
// "java.lang.IllegalStateException: Ambiguous mapping."
@RequestMapping("somethingReverse")
@PostMapping("otherthingReverse")
public String saveOrUpdateReverse(@ModelAttribute SomethingCommand command){
long id = callSomethingService(command);
return "redirect:/something/" + id + "/show";
}
// maps only POST requests, is not called for other submit methods,
// just to demonstrate that the submit method really makes a difference
@PostMapping("something")
public String saveOrUpdateByPost(@ModelAttribute SomethingCommand command){
long id = callSomethingService(command);
return "redirect:/something/" + id + "/show";
}
// maps only GET requests, is not called for other submit methods,
// just to demonstrate that the submit method really makes a difference
@GetMapping("something")
public String saveOrUpdateByGet(@ModelAttribute SomethingCommand command){
long id = callSomethingService(command);
return "redirect:/something/" + id + "/show";
}For the investigation I used this JUnit test utilizing Mockito and MockMvc:
[...]
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
public class SomethingControllerTest {
private SomethingController controller;
private MockMvc mockMvc;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
controller = new SomethingController();
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
@Test
public void testPostRequest() throws Exception {
SomethingCommand command = new SomethingCommand();
mockMvc.perform(post("/something") // vary post/get and path here
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("id", "")
.param("title", "some title")
)
.andExpect(status().is3xxRedirection())
// vary the fake IDs to find out which method was actually called:
.andExpect(view().name("redirect:/something/4711/show"));
}
}Conclusion#
If @RequestMapping is used along with any of it’s shortcuts like @PostRequest etc. at the same controller method then @RequestMapping gets preference and the specialized annotations are silently ignored.
If multiple mappings on different methods exist, which are effectively the same, then Spring complains at startup.
