MockMvc created by MockMvcBuilders.standaloneSetup() ignores @RestControllerAdvice annotation attributes but works well for a @ControllerAdvice/ResponseBody pair.
- Spring Boot: 2.3.2
- Spring Framework: 5.2.8
Example: https://github.com/thecederick/MockMvc-ignores-RestControllerAdvice-annotation-fields
Failing Configuration
Controller:
@RestController
@RequestMapping
public class Api1Controller {
@PostMapping(value = "/endpoint1")
public String endpoint() {
return "done";
}
}
Controller advice:
@RestControllerAdvice(assignableTypes = Api1Controller.class)
@Order(Ordered.HIGHEST_PRECEDENCE)
public class Api1ControllerAdvice {
@ExceptionHandler(Throwable.class)
public String handleException(Throwable throwable) {
return this.getClass() + " - " + throwable.getClass();
}
}
Test:
@BeforeEach
public void setup() {
this.mockMvc = MockMvcBuilders
.standaloneSetup(controller)
.addDispatcherServletCustomizer(
dispatcherServlet -> dispatcherServlet.setThrowExceptionIfNoHandlerFound(true))
.setControllerAdvice(Api1ControllerAdvice.class, DefaultControllerAdvice.class)
.build();
}
@Test
void notFound() throws Exception {
mockMvc
.perform(
post("/test")
.contentType("application/json")
.content("{}"))
.andExpect(content().string(
"class com.example.demo.root.DefaultControllerAdvice - class org.springframework.web.servlet.NoHandlerFoundException"));
}
Working Configuration
@RestController
@RequestMapping
public class Api2Controller {
@PostMapping(value = "/endpoint2")
public String endpoint() {
return "done";
}
}
Controller advice:
@ControllerAdvice(assignableTypes = Api2Controller.class)
@ResponseBody
@Order(Ordered.HIGHEST_PRECEDENCE)
public class Api2ControllerAdvice {
@ExceptionHandler(Throwable.class)
public String handleException(Throwable throwable) {
return this.getClass() + " - " + throwable.getClass();
}
}
Test:
@BeforeEach
public void setup() {
this.mockMvc = MockMvcBuilders
.standaloneSetup(controller)
.addDispatcherServletCustomizer(
dispatcherServlet -> dispatcherServlet.setThrowExceptionIfNoHandlerFound(true))
.setControllerAdvice(
Api2ControllerAdvice.class,
DefaultControllerAdvice.class)
.build();
}
@Test
void notFound() throws Exception {
mockMvc
.perform(
post("/test")
.contentType("application/json")
.content("{}"))
.andExpect(content().string(
"class com.example.demo.root.DefaultControllerAdvice - class org.springframework.web.servlet.NoHandlerFoundException"));
}
Default advice
@RestControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
public class DefaultControllerAdvice {
@ExceptionHandler(Throwable.class)
public String handleException(Throwable throwable) {
return this.getClass() + " - " + throwable.getClass();
}
}
Summary
In the first configuration using the @RestControllerAdvice annotation the test fails; with the second one, it passes as expected.
Comment From: sbrannen
Thanks for raising the issue and providing the example project to debug. Much appreciated!
I've determined that there is a bug in the implementation of StaticListableBeanFactory.findAnnotationOnBean(). The implementation in DefaultListableBeanFactory (which is used in production and with MockMvc when providing a WebApplicationContext to the builder) looks up "merged annotations" with support for @AliasFor attribute overrides; whereas, the implementation in StaticListableBeanFactory (which is used when using the stand-alone MockMvc builder) currently only looks up raw annotations. That's why the annotation attributes in @RestControllerAdvice get ignored when using the stand-alone MockMvc support.
Comment From: sbrannen
This issue is related to:
- gh-22584 :: 50c257794f7845829ac9ce78a102ef94e7e28a2e
- gh-23163 :: 978adbdae749566fbf458f4f72847dfc0b5aabf7
Comment From: sbrannen
The fix has been merged into 5.2.x and master.
Feel free to try it out in an upcoming 5.2.9 or 5.3 M2 snapshot.
Thanks again for raising the issue. 👍
Comment From: yevhenii-pazii
Thank you!