Skip to content

Commit a69ef16

Browse files
author
Renato Mameli
committed
Fix support for @RequestBody on interface methods
Previously, @RequestBody annotations declared in interfaces were ignored when resolving the `required` flag for request body matching. This caused mappings with `required = false` on interface methods to fail if no body was provided. This change uses AnnotatedMethod to retrieve parameter annotations from both the implementation and its interfaces, ensuring consistent behavior. Closes #35085 Signed-off-by: Renato Mameli <[email protected]>
1 parent a265a13 commit a69ef16

File tree

2 files changed

+75
-8
lines changed

2 files changed

+75
-8
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.lang.annotation.Annotation;
2020
import java.lang.reflect.AnnotatedElement;
2121
import java.lang.reflect.Method;
22-
import java.lang.reflect.Parameter;
2322
import java.util.Collections;
2423
import java.util.LinkedHashMap;
2524
import java.util.List;
@@ -31,7 +30,9 @@
3130
import org.jspecify.annotations.Nullable;
3231

3332
import org.springframework.context.EmbeddedValueResolverAware;
33+
import org.springframework.core.MethodParameter;
3434
import org.springframework.core.annotation.AnnotatedElementUtils;
35+
import org.springframework.core.annotation.AnnotatedMethod;
3536
import org.springframework.core.annotation.MergedAnnotation;
3637
import org.springframework.core.annotation.MergedAnnotationPredicates;
3738
import org.springframework.core.annotation.MergedAnnotations;
@@ -410,13 +411,17 @@ protected void registerHandlerMethod(Object handler, Method method, RequestMappi
410411

411412
private void updateConsumesCondition(RequestMappingInfo info, Method method) {
412413
ConsumesRequestCondition condition = info.getConsumesCondition();
413-
if (!condition.isEmpty()) {
414-
for (Parameter parameter : method.getParameters()) {
415-
MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class);
416-
if (annot.isPresent()) {
417-
condition.setBodyRequired(annot.getBoolean("required"));
418-
break;
419-
}
414+
if (condition.isEmpty()) {
415+
return;
416+
}
417+
418+
AnnotatedMethod annotatedMethod = new AnnotatedMethod(method);
419+
420+
for (MethodParameter parameter : annotatedMethod.getMethodParameters()) {
421+
RequestBody requestBodyAnnotation = parameter.getParameterAnnotation(RequestBody.class);
422+
if (requestBodyAnnotation != null) {
423+
condition.setBodyRequired(requestBodyAnnotation.required());
424+
break;
420425
}
421426
}
422427
}

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,52 @@ void httpExchangeWithCustomHeaders() throws Exception {
386386
.containsExactly("h1=hv1", "!h2");
387387
}
388388

389+
@Test
390+
void httpExchangeAnnotationOfRequestBodyIsTakenFromInterface() throws Exception {
391+
RequestMappingHandlerMapping mapping = createMapping();
392+
393+
Class<?> controllerClass = InterfaceControllerImpl.class;
394+
Method method = controllerClass.getDeclaredMethod("post", Foo.class);
395+
396+
RequestMappingInfo info = mapping.getMappingForMethod(method, controllerClass);
397+
assertThat(info).isNotNull();
398+
399+
mapping.registerHandlerMethod(new InterfaceControllerImpl(), method, info);
400+
401+
assertThat(info.getConsumesCondition()).isNotNull();
402+
assertThat(info.getConsumesCondition().isBodyRequired()).isFalse();
403+
assertThat(info.getConsumesCondition().getConsumableMediaTypes()).containsOnly(MediaType.APPLICATION_JSON);
404+
405+
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/controller/postMapping");
406+
initRequestPath(mapping, request);
407+
408+
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
409+
assertThat(matchingInfo).isNotNull();
410+
}
411+
412+
@Test
413+
void httpExchangeAnnotationOfRequestBodyOverwrittenByImpl() throws Exception {
414+
RequestMappingHandlerMapping mapping = createMapping();
415+
416+
Class<?> controllerClass = InterfaceControllerImplOverridesRequestBody.class;
417+
Method method = controllerClass.getDeclaredMethod("post", Foo.class);
418+
419+
RequestMappingInfo info = mapping.getMappingForMethod(method, controllerClass);
420+
assertThat(info).isNotNull();
421+
422+
mapping.registerHandlerMethod(new InterfaceControllerImpl(), method, info);
423+
424+
assertThat(info.getConsumesCondition()).isNotNull();
425+
assertThat(info.getConsumesCondition().isBodyRequired()).isTrue();
426+
assertThat(info.getConsumesCondition().getConsumableMediaTypes()).containsOnly(MediaType.APPLICATION_JSON);
427+
428+
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/controller/postMapping");
429+
initRequestPath(mapping, request);
430+
431+
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
432+
assertThat(matchingInfo).isNull();
433+
}
434+
389435
private static RequestMappingHandlerMapping createMapping() {
390436
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
391437
mapping.setApplicationContext(new StaticWebApplicationContext());
@@ -571,6 +617,22 @@ static class MethodLevelOverriddenHttpExchangeAnnotationsController implements S
571617
public void post() {}
572618
}
573619

620+
@RestController
621+
@RequestMapping(value = "/controller", consumes = { "application/json" })
622+
interface InterfaceController {
623+
@PostMapping("/postMapping")
624+
void post(@RequestBody(required = false) Foo foo);
625+
}
626+
627+
static class InterfaceControllerImpl implements InterfaceController {
628+
@Override
629+
public void post(Foo foo) {}
630+
}
631+
632+
static class InterfaceControllerImplOverridesRequestBody implements InterfaceController {
633+
@Override
634+
public void post(@RequestBody(required = true) Foo foo) {}
635+
}
574636

575637
@HttpExchange
576638
@Target(ElementType.TYPE)

0 commit comments

Comments
 (0)