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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URI;
|
||||
|
|
@ -185,6 +187,61 @@ final class DefaultRestClient implements RestClient {
|
|||
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 {
|
||||
|
||||
|
|
@ -409,7 +466,8 @@ final class DefaultRestClient implements RestClient {
|
|||
}
|
||||
clientResponse = clientRequest.execute();
|
||||
observationContext.setResponse(clientResponse);
|
||||
return exchangeFunction.exchange(clientRequest, clientResponse);
|
||||
ConvertibleClientHttpResponse convertibleWrapper = new DefaultConvertibleClientHttpResponse(clientResponse);
|
||||
return exchangeFunction.exchange(clientRequest, convertibleWrapper);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
|
||||
|
|
@ -542,14 +600,14 @@ final class DefaultRestClient implements RestClient {
|
|||
|
||||
@Override
|
||||
public <T> T body(Class<T> bodyType) {
|
||||
return readWithMessageConverters(bodyType, bodyType);
|
||||
return readBody(bodyType, bodyType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T body(ParameterizedTypeReference<T> bodyType) {
|
||||
Type type = bodyType.getType();
|
||||
Class<T> bodyClass = bodyClass(type);
|
||||
return readWithMessageConverters(type, bodyClass);
|
||||
return readBody(type, bodyClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -565,7 +623,7 @@ final class DefaultRestClient implements RestClient {
|
|||
}
|
||||
|
||||
private <T> ResponseEntity<T> toEntityInternal(Type bodyType, Class<T> bodyClass) {
|
||||
T body = readWithMessageConverters(bodyType, bodyClass);
|
||||
T body = readBody(bodyType, bodyClass);
|
||||
try {
|
||||
return ResponseEntity.status(this.clientResponse.getStatusCode())
|
||||
.headers(this.clientResponse.getHeaders())
|
||||
|
|
@ -579,77 +637,96 @@ final class DefaultRestClient implements RestClient {
|
|||
@Override
|
||||
public ResponseEntity<Void> toBodilessEntity() {
|
||||
try (this.clientResponse) {
|
||||
applyStatusHandlers(this.clientRequest, this.clientResponse);
|
||||
applyStatusHandlers();
|
||||
return ResponseEntity.status(this.clientResponse.getStatusCode())
|
||||
.headers(this.clientResponse.getHeaders())
|
||||
.build();
|
||||
}
|
||||
catch (UncheckedIOException ex) {
|
||||
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex.getCause());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ResourceAccessException("Could not retrieve response status code: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@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 <T> T readBody(Type bodyType, Class<T> bodyClass) {
|
||||
return DefaultRestClient.this.readWithMessageConverters(this.clientResponse, this::applyStatusHandlers,
|
||||
bodyType, bodyClass);
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private <T> T readWithMessageConverters(Type bodyType, Class<T> bodyClass) {
|
||||
MediaType contentType = getContentType();
|
||||
|
||||
try (this.clientResponse) {
|
||||
applyStatusHandlers(this.clientRequest, this.clientResponse);
|
||||
|
||||
for (HttpMessageConverter<?> messageConverter : DefaultRestClient.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, 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);
|
||||
private void applyStatusHandlers() {
|
||||
try {
|
||||
ClientHttpResponse response = this.clientResponse;
|
||||
if (response instanceof DefaultConvertibleClientHttpResponse convertibleResponse) {
|
||||
response = convertibleResponse.delegate;
|
||||
}
|
||||
for (StatusHandler handler : this.statusHandlers) {
|
||||
if (handler.test(response)) {
|
||||
handler.handle(this.clientRequest, response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new UnknownContentTypeException(bodyType, contentType,
|
||||
this.clientResponse.getStatusCode(), this.clientResponse.getStatusText(),
|
||||
this.clientResponse.getHeaders(), RestClientUtils.getBody(this.clientResponse));
|
||||
}
|
||||
catch (IOException | HttpMessageNotReadableException ex) {
|
||||
throw new RestClientException("Error while extracting response for type [" +
|
||||
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;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
* @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
|
||||
void exchangeFor404(ClientHttpRequestFactory requestFactory) {
|
||||
startServer(requestFactory);
|
||||
|
|
|
|||
Loading…
Reference in New Issue