Skip to content

Support @RequestBody in interface method when computing "consumes" condition #35086

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -31,7 +30,9 @@
import org.jspecify.annotations.Nullable;

import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotatedMethod;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations;
Expand Down Expand Up @@ -410,13 +411,17 @@ protected void registerHandlerMethod(Object handler, Method method, RequestMappi

private void updateConsumesCondition(RequestMappingInfo info, Method method) {
ConsumesRequestCondition condition = info.getConsumesCondition();
if (!condition.isEmpty()) {
for (Parameter parameter : method.getParameters()) {
MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class);
if (annot.isPresent()) {
condition.setBodyRequired(annot.getBoolean("required"));
break;
}
if (condition.isEmpty()) {
return;
}

AnnotatedMethod annotatedMethod = new AnnotatedMethod(method);

for (MethodParameter parameter : annotatedMethod.getMethodParameters()) {
RequestBody requestBodyAnnotation = parameter.getParameterAnnotation(RequestBody.class);
if (requestBodyAnnotation != null) {
condition.setBodyRequired(requestBodyAnnotation.required());
break;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,52 @@ void httpExchangeWithCustomHeaders() throws Exception {
.containsExactly("h1=hv1", "!h2");
}

@Test
void requestBodyAnnotationFromInterfaceIsRespected() throws Exception {
RequestMappingHandlerMapping mapping = createMapping();

Class<?> controllerClass = InterfaceControllerImpl.class;
Method method = controllerClass.getDeclaredMethod("post", Foo.class);

RequestMappingInfo info = mapping.getMappingForMethod(method, controllerClass);
assertThat(info).isNotNull();

mapping.registerHandlerMethod(new InterfaceControllerImpl(), method, info);

assertThat(info.getConsumesCondition()).isNotNull();
assertThat(info.getConsumesCondition().isBodyRequired()).isFalse();
assertThat(info.getConsumesCondition().getConsumableMediaTypes()).containsOnly(MediaType.APPLICATION_JSON);

MockHttpServletRequest request = new MockHttpServletRequest("POST", "/controller/postMapping");
initRequestPath(mapping, request);

RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
assertThat(matchingInfo).isNotNull();
}

@Test
void requestBodyAnnotationFromImplementationOverridesInterface() throws Exception {
RequestMappingHandlerMapping mapping = createMapping();

Class<?> controllerClass = InterfaceControllerImplOverridesRequestBody.class;
Method method = controllerClass.getDeclaredMethod("post", Foo.class);

RequestMappingInfo info = mapping.getMappingForMethod(method, controllerClass);
assertThat(info).isNotNull();

mapping.registerHandlerMethod(new InterfaceControllerImpl(), method, info);

assertThat(info.getConsumesCondition()).isNotNull();
assertThat(info.getConsumesCondition().isBodyRequired()).isTrue();
assertThat(info.getConsumesCondition().getConsumableMediaTypes()).containsOnly(MediaType.APPLICATION_JSON);

MockHttpServletRequest request = new MockHttpServletRequest("POST", "/controller/postMapping");
initRequestPath(mapping, request);

RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
assertThat(matchingInfo).isNull();
}

private static RequestMappingHandlerMapping createMapping() {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(new StaticWebApplicationContext());
Expand Down Expand Up @@ -571,6 +617,22 @@ static class MethodLevelOverriddenHttpExchangeAnnotationsController implements S
public void post() {}
}

@RestController
@RequestMapping(value = "/controller", consumes = { "application/json" })
interface InterfaceController {
@PostMapping("/postMapping")
void post(@RequestBody(required = false) Foo foo);
}

static class InterfaceControllerImpl implements InterfaceController {
@Override
public void post(Foo foo) {}
}

static class InterfaceControllerImplOverridesRequestBody implements InterfaceController {
@Override
public void post(@RequestBody(required = true) Foo foo) {}
}

@HttpExchange
@Target(ElementType.TYPE)
Expand Down