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.
@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.
Twitter
Facebook
Reddit
LinkedIn
StumbleUpon
Email