Browse Source

Fix context class resolution for nested types

This commit introduces ContextClassRequestBodyAdvice which adds a
"contextClass" hint allowing to resolve generics for Optional,
HttpEntity or ServerSentEvent container types.

Closes gh-36111
pull/36115/head
Sébastien Deleuze 3 weeks ago
parent
commit
eea9130ea6
  1. 3
      spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java
  2. 2
      spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
  3. 58
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ContextClassRequestBodyAdvice.java
  4. 10
      spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java
  5. 6
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorTests.java

3
spring-web/src/main/java/org/springframework/http/converter/AbstractJacksonHttpMessageConverter.java

@ -313,7 +313,8 @@ public abstract class AbstractJacksonHttpMessageConverter<T extends ObjectMapper @@ -313,7 +313,8 @@ public abstract class AbstractJacksonHttpMessageConverter<T extends ObjectMapper
public Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints)
throws IOException, HttpMessageNotReadableException {
Class<?> contextClass = (type.getSource() instanceof MethodParameter parameter ? parameter.getContainingClass() : null);
Class<?> contextClass = (type.getSource() instanceof MethodParameter parameter ? parameter.getContainingClass() :
(hints != null ? (Class<?>) hints.get("contextClass") : null));
JavaType javaType = getJavaType(type.getType(), contextClass);
return readJavaType(javaType, inputMessage, hints);
}

2
spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java

@ -83,6 +83,7 @@ import org.springframework.web.servlet.mvc.Controller; @@ -83,6 +83,7 @@ import org.springframework.web.servlet.mvc.Controller;
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ContextClassRequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.JsonViewRequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice;
@ -661,6 +662,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv @@ -661,6 +662,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
List<ResponseBodyAdvice<?>> responseBodyAdvices = new ArrayList<>(2);
if (JACKSON_PRESENT || JACKSON_2_PRESENT) {
requestBodyAdvices.add(new JsonViewRequestBodyAdvice());
requestBodyAdvices.add(new ContextClassRequestBodyAdvice());
responseBodyAdvices.add(new JsonViewResponseBodyAdvice());
}
if (KOTLIN_REFLECT_PRESENT && KOTLIN_SERIALIZATION_PRESENT) {

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

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import org.jspecify.annotations.Nullable;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.SmartHttpMessageConverter;
/**
* A {@link RequestBodyAdvice} implementation that adds a {@code "contextClass"} hint for {@link Optional},
* {@link HttpEntity} and {@link ServerSentEvent} container types.
*
* @author Sebastien Deleuze
* @since 7.0.3
*/
public class ContextClassRequestBodyAdvice extends RequestBodyAdviceAdapter {
@Override
public boolean supports(MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
if (!AbstractJacksonHttpMessageConverter.class.isAssignableFrom(converterType)) {
return false;
}
Class<?> parameterType = parameter.getParameterType();
return Optional.class.isAssignableFrom(parameterType) || HttpEntity.class.isAssignableFrom(parameterType) ||
ServerSentEvent.class.isAssignableFrom(parameterType);
}
@Override
public @Nullable Map<String, Object> determineReadHints(MethodParameter parameter, Type targetType,
Class<? extends SmartHttpMessageConverter<?>> converterType) {
return Collections.singletonMap("contextClass", parameter.getContainingClass());
}
}

10
spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java

@ -79,6 +79,7 @@ import org.springframework.web.servlet.handler.ConversionServiceExposingIntercep @@ -79,6 +79,7 @@ import org.springframework.web.servlet.handler.ConversionServiceExposingIntercep
import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ContextClassRequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.JsonViewRequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice;
@ -208,11 +209,12 @@ class WebMvcConfigurationSupportTests { @@ -208,11 +209,12 @@ class WebMvcConfigurationSupportTests {
DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(adapter);
@SuppressWarnings("unchecked")
List<Object> bodyAdvice = (List<Object>) fieldAccessor.getPropertyValue("requestResponseBodyAdvice");
assertThat(bodyAdvice).hasSize(4);
assertThat(bodyAdvice).hasSize(5);
assertThat(bodyAdvice.get(0).getClass()).isEqualTo(JsonViewRequestBodyAdvice.class);
assertThat(bodyAdvice.get(1).getClass()).isEqualTo(KotlinRequestBodyAdvice.class);
assertThat(bodyAdvice.get(2).getClass()).isEqualTo(JsonViewResponseBodyAdvice.class);
assertThat(bodyAdvice.get(3).getClass()).isEqualTo(KotlinResponseBodyAdvice.class);
assertThat(bodyAdvice.get(1).getClass()).isEqualTo(ContextClassRequestBodyAdvice.class);
assertThat(bodyAdvice.get(2).getClass()).isEqualTo(KotlinRequestBodyAdvice.class);
assertThat(bodyAdvice.get(3).getClass()).isEqualTo(JsonViewResponseBodyAdvice.class);
assertThat(bodyAdvice.get(4).getClass()).isEqualTo(KotlinResponseBodyAdvice.class);
}
@Test

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

@ -30,7 +30,6 @@ import jakarta.servlet.http.HttpServletRequest; @@ -30,7 +30,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
@ -152,7 +151,6 @@ public class HttpEntityMethodProcessorTests { @@ -152,7 +151,6 @@ public class HttpEntityMethodProcessorTests {
}
@Test
@Disabled("Determine why this fails with JacksonJsonHttpMessageConverter but passes with MappingJackson2HttpMessageConverter")
void resolveArgumentTypeVariable() throws Exception {
Method method = MySimpleParameterizedController.class.getMethod("handleDto", HttpEntity.class);
HandlerMethod handlerMethod = new HandlerMethod(new MySimpleParameterizedController(), method);
@ -164,7 +162,9 @@ public class HttpEntityMethodProcessorTests { @@ -164,7 +162,9 @@ public class HttpEntityMethodProcessorTests {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new JacksonJsonHttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters);
List<Object> requestResponseBodyAdvice = new ArrayList<>();
requestResponseBodyAdvice.add(new ContextClassRequestBodyAdvice());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters, requestResponseBodyAdvice);
@SuppressWarnings("unchecked")
HttpEntity<SimpleBean> result = (HttpEntity<SimpleBean>)

Loading…
Cancel
Save