Add body conversion capabilities in RestClient::exchange
This commit introduces a ConvertibleClientHttpResponse type that extends ClientHttpResponse, and that can convert the body to a desired type. Before this commit, it was not easy to use the configured HTTP message converters in combination with RestClient::exchange. Closes gh-31597
This commit is contained in:
parent
dd97dee7fd
commit
8f21479234
|
|
@ -17,6 +17,8 @@
|
||||||
package org.springframework.web.client;
|
package org.springframework.web.client;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
@ -185,6 +187,61 @@ final class DefaultRestClient implements RestClient {
|
||||||
return new DefaultRestClientBuilder(this.builder);
|
return new DefaultRestClientBuilder(this.builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
private <T> T readWithMessageConverters(ClientHttpResponse clientResponse, Runnable callback, Type bodyType, Class<T> bodyClass) {
|
||||||
|
MediaType contentType = getContentType(clientResponse);
|
||||||
|
|
||||||
|
try (clientResponse) {
|
||||||
|
callback.run();
|
||||||
|
|
||||||
|
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
|
||||||
|
if (messageConverter instanceof GenericHttpMessageConverter genericHttpMessageConverter) {
|
||||||
|
if (genericHttpMessageConverter.canRead(bodyType, null, contentType)) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Reading to [" + ResolvableType.forType(bodyType) + "]");
|
||||||
|
}
|
||||||
|
return (T) genericHttpMessageConverter.read(bodyType, null, clientResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (messageConverter.canRead(bodyClass, contentType)) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Reading to [" + bodyClass.getName() + "] as \"" + contentType + "\"");
|
||||||
|
}
|
||||||
|
return (T) messageConverter.read((Class)bodyClass, clientResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new UnknownContentTypeException(bodyType, contentType,
|
||||||
|
clientResponse.getStatusCode(), clientResponse.getStatusText(),
|
||||||
|
clientResponse.getHeaders(), RestClientUtils.getBody(clientResponse));
|
||||||
|
}
|
||||||
|
catch (UncheckedIOException | IOException | HttpMessageNotReadableException ex) {
|
||||||
|
throw new RestClientException("Error while extracting response for type [" +
|
||||||
|
ResolvableType.forType(bodyType) + "] and content type [" + contentType + "]", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaType getContentType(ClientHttpResponse clientResponse) {
|
||||||
|
MediaType contentType = clientResponse.getHeaders().getContentType();
|
||||||
|
if (contentType == null) {
|
||||||
|
contentType = MediaType.APPLICATION_OCTET_STREAM;
|
||||||
|
}
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> Class<T> bodyClass(Type type) {
|
||||||
|
if (type instanceof Class<?> clazz) {
|
||||||
|
return (Class<T>) clazz;
|
||||||
|
}
|
||||||
|
if (type instanceof ParameterizedType parameterizedType &&
|
||||||
|
parameterizedType.getRawType() instanceof Class<?> rawType) {
|
||||||
|
return (Class<T>) rawType;
|
||||||
|
}
|
||||||
|
return (Class<T>) Object.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
|
private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
|
||||||
|
|
||||||
|
|
@ -409,7 +466,8 @@ final class DefaultRestClient implements RestClient {
|
||||||
}
|
}
|
||||||
clientResponse = clientRequest.execute();
|
clientResponse = clientRequest.execute();
|
||||||
observationContext.setResponse(clientResponse);
|
observationContext.setResponse(clientResponse);
|
||||||
return exchangeFunction.exchange(clientRequest, clientResponse);
|
ConvertibleClientHttpResponse convertibleWrapper = new DefaultConvertibleClientHttpResponse(clientResponse);
|
||||||
|
return exchangeFunction.exchange(clientRequest, convertibleWrapper);
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
|
ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
|
||||||
|
|
@ -542,14 +600,14 @@ final class DefaultRestClient implements RestClient {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T body(Class<T> bodyType) {
|
public <T> T body(Class<T> bodyType) {
|
||||||
return readWithMessageConverters(bodyType, bodyType);
|
return readBody(bodyType, bodyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T body(ParameterizedTypeReference<T> bodyType) {
|
public <T> T body(ParameterizedTypeReference<T> bodyType) {
|
||||||
Type type = bodyType.getType();
|
Type type = bodyType.getType();
|
||||||
Class<T> bodyClass = bodyClass(type);
|
Class<T> bodyClass = bodyClass(type);
|
||||||
return readWithMessageConverters(type, bodyClass);
|
return readBody(type, bodyClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -565,7 +623,7 @@ final class DefaultRestClient implements RestClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> ResponseEntity<T> toEntityInternal(Type bodyType, Class<T> bodyClass) {
|
private <T> ResponseEntity<T> toEntityInternal(Type bodyType, Class<T> bodyClass) {
|
||||||
T body = readWithMessageConverters(bodyType, bodyClass);
|
T body = readBody(bodyType, bodyClass);
|
||||||
try {
|
try {
|
||||||
return ResponseEntity.status(this.clientResponse.getStatusCode())
|
return ResponseEntity.status(this.clientResponse.getStatusCode())
|
||||||
.headers(this.clientResponse.getHeaders())
|
.headers(this.clientResponse.getHeaders())
|
||||||
|
|
@ -579,77 +637,96 @@ final class DefaultRestClient implements RestClient {
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<Void> toBodilessEntity() {
|
public ResponseEntity<Void> toBodilessEntity() {
|
||||||
try (this.clientResponse) {
|
try (this.clientResponse) {
|
||||||
applyStatusHandlers(this.clientRequest, this.clientResponse);
|
applyStatusHandlers();
|
||||||
return ResponseEntity.status(this.clientResponse.getStatusCode())
|
return ResponseEntity.status(this.clientResponse.getStatusCode())
|
||||||
.headers(this.clientResponse.getHeaders())
|
.headers(this.clientResponse.getHeaders())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
catch (UncheckedIOException ex) {
|
||||||
|
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex.getCause());
|
||||||
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex);
|
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static <T> Class<T> bodyClass(Type type) {
|
private <T> T readBody(Type bodyType, Class<T> bodyClass) {
|
||||||
if (type instanceof Class<?> clazz) {
|
return DefaultRestClient.this.readWithMessageConverters(this.clientResponse, this::applyStatusHandlers,
|
||||||
return (Class<T>) clazz;
|
bodyType, bodyClass);
|
||||||
}
|
|
||||||
if (type instanceof ParameterizedType parameterizedType &&
|
|
||||||
parameterizedType.getRawType() instanceof Class<?> rawType) {
|
|
||||||
return (Class<T>) rawType;
|
|
||||||
}
|
|
||||||
return (Class<T>) Object.class;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
private void applyStatusHandlers() {
|
||||||
private <T> T readWithMessageConverters(Type bodyType, Class<T> bodyClass) {
|
try {
|
||||||
MediaType contentType = getContentType();
|
ClientHttpResponse response = this.clientResponse;
|
||||||
|
if (response instanceof DefaultConvertibleClientHttpResponse convertibleResponse) {
|
||||||
try (this.clientResponse) {
|
response = convertibleResponse.delegate;
|
||||||
applyStatusHandlers(this.clientRequest, this.clientResponse);
|
}
|
||||||
|
for (StatusHandler handler : this.statusHandlers) {
|
||||||
for (HttpMessageConverter<?> messageConverter : DefaultRestClient.this.messageConverters) {
|
if (handler.test(response)) {
|
||||||
if (messageConverter instanceof GenericHttpMessageConverter genericHttpMessageConverter) {
|
handler.handle(this.clientRequest, response);
|
||||||
if (genericHttpMessageConverter.canRead(bodyType, null, contentType)) {
|
return;
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug("Reading to [" + ResolvableType.forType(bodyType) + "]");
|
|
||||||
}
|
|
||||||
return (T) genericHttpMessageConverter.read(bodyType, null, this.clientResponse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (messageConverter.canRead(bodyClass, contentType)) {
|
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug("Reading to [" + bodyClass.getName() + "] as \"" + contentType + "\"");
|
|
||||||
}
|
|
||||||
return (T) messageConverter.read((Class)bodyClass, this.clientResponse);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new UnknownContentTypeException(bodyType, contentType,
|
|
||||||
this.clientResponse.getStatusCode(), this.clientResponse.getStatusText(),
|
|
||||||
this.clientResponse.getHeaders(), RestClientUtils.getBody(this.clientResponse));
|
|
||||||
}
|
}
|
||||||
catch (IOException | HttpMessageNotReadableException ex) {
|
catch (IOException ex) {
|
||||||
throw new RestClientException("Error while extracting response for type [" +
|
throw new UncheckedIOException(ex);
|
||||||
ResolvableType.forType(bodyType) + "] and content type [" + contentType + "]", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MediaType getContentType() {
|
|
||||||
MediaType contentType = this.clientResponse.getHeaders().getContentType();
|
|
||||||
if (contentType == null) {
|
|
||||||
contentType = MediaType.APPLICATION_OCTET_STREAM;
|
|
||||||
}
|
|
||||||
return contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyStatusHandlers(HttpRequest request, ClientHttpResponse response) throws IOException {
|
|
||||||
for (StatusHandler handler : this.statusHandlers) {
|
|
||||||
if (handler.test(response)) {
|
|
||||||
handler.handle(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class DefaultConvertibleClientHttpResponse implements RequestHeadersSpec.ConvertibleClientHttpResponse {
|
||||||
|
|
||||||
|
private final ClientHttpResponse delegate;
|
||||||
|
|
||||||
|
|
||||||
|
public DefaultConvertibleClientHttpResponse(ClientHttpResponse delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <T> T bodyTo(Class<T> bodyType) {
|
||||||
|
return readWithMessageConverters(this.delegate, () -> {} , bodyType, bodyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <T> T bodyTo(ParameterizedTypeReference<T> bodyType) {
|
||||||
|
Type type = bodyType.getType();
|
||||||
|
Class<T> bodyClass = bodyClass(type);
|
||||||
|
return readWithMessageConverters(this.delegate, () -> {} , type, bodyClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getBody() throws IOException {
|
||||||
|
return this.delegate.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpHeaders getHeaders() {
|
||||||
|
return this.delegate.getHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpStatusCode getStatusCode() throws IOException {
|
||||||
|
return this.delegate.getStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStatusText() throws IOException {
|
||||||
|
return this.delegate.getStatusText();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
this.delegate.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -623,7 +623,33 @@ public interface RestClient {
|
||||||
* @return the exchanged type
|
* @return the exchanged type
|
||||||
* @throws IOException in case of I/O errors
|
* @throws IOException in case of I/O errors
|
||||||
*/
|
*/
|
||||||
T exchange(HttpRequest clientRequest, ClientHttpResponse clientResponse) throws IOException;
|
T exchange(HttpRequest clientRequest, ConvertibleClientHttpResponse clientResponse) throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension of {@link ClientHttpResponse} that can convert the body.
|
||||||
|
*/
|
||||||
|
interface ConvertibleClientHttpResponse extends ClientHttpResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the response body as an object of the given type.
|
||||||
|
* @param bodyType the type of return value
|
||||||
|
* @param <T> the body type
|
||||||
|
* @return the body, or {@code null} if no response body was available
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
<T> T bodyTo(Class<T> bodyType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the response body as an object of the given type.
|
||||||
|
* @param bodyType the type of return value
|
||||||
|
* @param <T> the body type
|
||||||
|
* @return the body, or {@code null} if no response body was available
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
<T> T bodyTo(ParameterizedTypeReference<T> bodyType);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -661,6 +661,55 @@ class RestClientIntegrationTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedRestClientTest
|
||||||
|
void exchangeForJson(ClientHttpRequestFactory requestFactory) {
|
||||||
|
startServer(requestFactory);
|
||||||
|
|
||||||
|
prepareResponse(response -> response
|
||||||
|
.setHeader("Content-Type", "application/json")
|
||||||
|
.setBody("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"));
|
||||||
|
|
||||||
|
Pojo result = this.restClient.get()
|
||||||
|
.uri("/pojo")
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.exchange((request, response) -> response.bodyTo(Pojo.class));
|
||||||
|
|
||||||
|
assertThat(result.getFoo()).isEqualTo("foofoo");
|
||||||
|
assertThat(result.getBar()).isEqualTo("barbar");
|
||||||
|
|
||||||
|
expectRequestCount(1);
|
||||||
|
expectRequest(request -> {
|
||||||
|
assertThat(request.getPath()).isEqualTo("/pojo");
|
||||||
|
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedRestClientTest
|
||||||
|
void exchangeForJsonArray(ClientHttpRequestFactory requestFactory) {
|
||||||
|
startServer(requestFactory);
|
||||||
|
|
||||||
|
prepareResponse(response -> response
|
||||||
|
.setHeader("Content-Type", "application/json")
|
||||||
|
.setBody("[{\"bar\":\"bar1\",\"foo\":\"foo1\"},{\"bar\":\"bar2\",\"foo\":\"foo2\"}]"));
|
||||||
|
|
||||||
|
List<Pojo> result = this.restClient.get()
|
||||||
|
.uri("/pojo")
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.exchange((request, response) -> response.bodyTo(new ParameterizedTypeReference<>() {}));
|
||||||
|
|
||||||
|
assertThat(result).hasSize(2);
|
||||||
|
assertThat(result.get(0).getFoo()).isEqualTo("foo1");
|
||||||
|
assertThat(result.get(0).getBar()).isEqualTo("bar1");
|
||||||
|
assertThat(result.get(1).getFoo()).isEqualTo("foo2");
|
||||||
|
assertThat(result.get(1).getBar()).isEqualTo("bar2");
|
||||||
|
|
||||||
|
expectRequestCount(1);
|
||||||
|
expectRequest(request -> {
|
||||||
|
assertThat(request.getPath()).isEqualTo("/pojo");
|
||||||
|
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedRestClientTest
|
@ParameterizedRestClientTest
|
||||||
void exchangeFor404(ClientHttpRequestFactory requestFactory) {
|
void exchangeFor404(ClientHttpRequestFactory requestFactory) {
|
||||||
startServer(requestFactory);
|
startServer(requestFactory);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue