Introduce SmartHttpMessageConverter

SmartHttpMessageConverter is similar to GenericHttpMessageConverter,
but more consistent with WebFlux Encoder and Decoder contracts, with
the following differences:
 - A ResolvableType parameter is used instead of the Type one
 - The MethodParameter can be retrieved via the ResolvableType source
 - No contextClass parameter
 - `@Nullable Map<String, Object> hints` additional parameter for write
   and read methods

This commit also refines RestTemplate#canReadResponse in order to use
the most specific converter contract when possible.

Closes gh-33118
This commit is contained in:
Sébastien Deleuze
2024-07-01 14:51:46 +02:00
parent 0717748f58
commit 4555384528
14 changed files with 513 additions and 48 deletions

View File

@@ -25,6 +25,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpStatus;
@@ -33,12 +34,14 @@ import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.SmartHttpMessageConverter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@@ -166,6 +169,26 @@ class HttpMessageConverterExtractorTests {
assertThat(result).isEqualTo(expected);
}
@Test
void smartConverter() throws IOException {
responseHeaders.setContentType(contentType);
String expected = "Foo";
ParameterizedTypeReference<List<String>> reference = new ParameterizedTypeReference<>() {};
ResolvableType resolvableType = ResolvableType.forType(reference.getType());
SmartHttpMessageConverter<String> converter = mock();
HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor<List<String>>(resolvableType.getType(), List.of(converter));
given(response.getStatusCode()).willReturn(HttpStatus.OK);
given(response.getHeaders()).willReturn(responseHeaders);
given(response.getBody()).willReturn(new ByteArrayInputStream(expected.getBytes()));
given(converter.canRead(resolvableType, contentType)).willReturn(true);
given(converter.read(eq(resolvableType), any(HttpInputMessage.class), isNull())).willReturn(expected);
Object result = extractor.extractData(response);
assertThat(result).isEqualTo(expected);
}
@Test // SPR-13592
void converterThrowsIOException() throws IOException {
responseHeaders.setContentType(contentType);

View File

@@ -35,6 +35,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
@@ -50,6 +51,7 @@ import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.SmartHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.util.DefaultUriBuilderFactory;
@@ -683,6 +685,41 @@ class RestTemplateTests {
verify(response).close();
}
@Test
@SuppressWarnings("rawtypes")
void exchangeParameterizedTypeWithSmartConverter() throws Exception {
SmartHttpMessageConverter converter = mock();
template.setMessageConverters(Collections.singletonList(converter));
ParameterizedTypeReference<List<Integer>> intList = new ParameterizedTypeReference<>() {};
given(converter.canRead(ResolvableType.forType(intList.getType()), null)).willReturn(true);
given(converter.getSupportedMediaTypes(any())).willReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
given(converter.canWrite(ResolvableType.forClass(String.class), String.class, null)).willReturn(true);
HttpHeaders requestHeaders = new HttpHeaders();
mockSentRequest(POST, "https://example.com", requestHeaders);
List<Integer> expected = Collections.singletonList(42);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.TEXT_PLAIN);
responseHeaders.setContentLength(10);
mockResponseStatus(HttpStatus.OK);
given(response.getHeaders()).willReturn(responseHeaders);
given(response.getBody()).willReturn(new ByteArrayInputStream(Integer.toString(42).getBytes()));
given(converter.canRead(ResolvableType.forType(intList.getType()), MediaType.TEXT_PLAIN)).willReturn(true);
given(converter.read(eq(ResolvableType.forType(intList.getType())), any(HttpInputMessage.class), eq(null))).willReturn(expected);
HttpHeaders entityHeaders = new HttpHeaders();
entityHeaders.set("MyHeader", "MyValue");
HttpEntity<String> requestEntity = new HttpEntity<>("Hello World", entityHeaders);
ResponseEntity<List<Integer>> result = template.exchange("https://example.com", POST, requestEntity, intList);
assertThat(result.getBody()).as("Invalid POST result").isEqualTo(expected);
assertThat(result.getHeaders().getContentType()).as("Invalid Content-Type").isEqualTo(MediaType.TEXT_PLAIN);
assertThat(requestHeaders.getFirst("Accept")).as("Invalid Accept header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
assertThat(requestHeaders.getFirst("MyHeader")).as("Invalid custom header").isEqualTo("MyValue");
assertThat(result.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK);
verify(response).close();
}
@Test // SPR-15066
void requestInterceptorCanAddExistingHeaderValueWithoutBody() throws Exception {
ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {