This commit is contained in:
Renato Mameli 2025-07-01 12:42:06 +02:00 committed by GitHub
commit b343407085
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 146 additions and 16 deletions

View File

@ -19,7 +19,6 @@ package org.springframework.web.reactive.result.method.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -30,7 +29,9 @@ import java.util.stream.Stream;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotatedMethod;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationPredicates; import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations;
@ -375,13 +376,17 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
private void updateConsumesCondition(RequestMappingInfo info, Method method) { private void updateConsumesCondition(RequestMappingInfo info, Method method) {
ConsumesRequestCondition condition = info.getConsumesCondition(); ConsumesRequestCondition condition = info.getConsumesCondition();
if (!condition.isEmpty()) { if (condition.isEmpty()) {
for (Parameter parameter : method.getParameters()) { return;
MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class);
if (annot.isPresent()) {
condition.setBodyRequired(annot.getBoolean("required"));
break;
} }
AnnotatedMethod annotatedMethod = new AnnotatedMethod(method);
for (MethodParameter parameter : annotatedMethod.getMethodParameters()) {
RequestBody requestBody = parameter.getParameterAnnotation(RequestBody.class);
if (requestBody != null) {
condition.setBodyRequired(requestBody.required());
break;
} }
} }
} }

View File

@ -323,6 +323,48 @@ class RequestMappingHandlerMappingTests {
.containsExactly("h1=hv1", "!h2"); .containsExactly("h1=hv1", "!h2");
} }
@Test
void requestBodyAnnotationFromInterfaceIsRespected() {
this.handlerMapping.afterPropertiesSet();
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(new StaticWebApplicationContext());
mapping.afterPropertiesSet();
Class<InterfaceControllerImpl> clazz = InterfaceControllerImpl.class;
Method method = ReflectionUtils.findMethod(clazz, "post", Foo.class);
assertThat(method).isNotNull();
RequestMappingInfo info = mapping.getMappingForMethod(method, clazz);
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);
}
@Test
void requestBodyAnnotationFromImplementationOverridesInterface() {
this.handlerMapping.afterPropertiesSet();
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(new StaticWebApplicationContext());
mapping.afterPropertiesSet();
Class<InterfaceControllerImplOverridesRequestBody> clazz = InterfaceControllerImplOverridesRequestBody.class;
Method method = ReflectionUtils.findMethod(clazz, "post", Foo.class);
assertThat(method).isNotNull();
RequestMappingInfo info = mapping.getMappingForMethod(method, clazz);
assertThat(info).isNotNull();
mapping.registerHandlerMethod(new InterfaceControllerImplOverridesRequestBody(), method, info);
assertThat(info.getConsumesCondition()).isNotNull();
assertThat(info.getConsumesCondition().isBodyRequired()).isTrue();
assertThat(info.getConsumesCondition().getConsumableMediaTypes()).containsOnly(MediaType.APPLICATION_JSON);
}
private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) { private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) {
String methodName = requestMethod.name().toLowerCase(); String methodName = requestMethod.name().toLowerCase();
String path = "/" + methodName; String path = "/" + methodName;
@ -501,6 +543,22 @@ class RequestMappingHandlerMappingTests {
public void post() {} public void post() {}
} }
@Controller
@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 @HttpExchange
@Target(ElementType.TYPE) @Target(ElementType.TYPE)

View File

@ -19,7 +19,6 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -31,7 +30,9 @@ import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotatedMethod;
import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationPredicates; import org.springframework.core.annotation.MergedAnnotationPredicates;
import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations;
@ -415,13 +416,17 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
private void updateConsumesCondition(RequestMappingInfo info, Method method) { private void updateConsumesCondition(RequestMappingInfo info, Method method) {
ConsumesRequestCondition condition = info.getConsumesCondition(); ConsumesRequestCondition condition = info.getConsumesCondition();
if (!condition.isEmpty()) { if (condition.isEmpty()) {
for (Parameter parameter : method.getParameters()) { return;
MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class);
if (annot.isPresent()) {
condition.setBodyRequired(annot.getBoolean("required"));
break;
} }
AnnotatedMethod annotatedMethod = new AnnotatedMethod(method);
for (MethodParameter parameter : annotatedMethod.getMethodParameters()) {
RequestBody requestBody = parameter.getParameterAnnotation(RequestBody.class);
if (requestBody != null) {
condition.setBodyRequired(requestBody.required());
break;
} }
} }
} }

View File

@ -386,6 +386,52 @@ class RequestMappingHandlerMappingTests {
.containsExactly("h1=hv1", "!h2"); .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 InterfaceControllerImplOverridesRequestBody(), 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() { private static RequestMappingHandlerMapping createMapping() {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(new StaticWebApplicationContext()); mapping.setApplicationContext(new StaticWebApplicationContext());
@ -571,6 +617,22 @@ class RequestMappingHandlerMappingTests {
public void post() {} 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 @HttpExchange
@Target(ElementType.TYPE) @Target(ElementType.TYPE)