Browse Source

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
pull/31608/head
Arjen Poutsma 1 year ago
parent
commit
8f21479234
  1. 187
      spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java
  2. 28
      spring-web/src/main/java/org/springframework/web/client/RestClient.java
  3. 49
      spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java

187
spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java

@ -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) {
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;
}
@SuppressWarnings({"rawtypes", "unchecked"}) private <T> T readBody(Type bodyType, Class<T> bodyClass) {
private <T> T readWithMessageConverters(Type bodyType, Class<T> bodyClass) { return DefaultRestClient.this.readWithMessageConverters(this.clientResponse, this::applyStatusHandlers,
MediaType contentType = getContentType(); bodyType, bodyClass);
try (this.clientResponse) { }
applyStatusHandlers(this.clientRequest, this.clientResponse);
private void applyStatusHandlers() {
for (HttpMessageConverter<?> messageConverter : DefaultRestClient.this.messageConverters) { try {
if (messageConverter instanceof GenericHttpMessageConverter genericHttpMessageConverter) { ClientHttpResponse response = this.clientResponse;
if (genericHttpMessageConverter.canRead(bodyType, null, contentType)) { if (response instanceof DefaultConvertibleClientHttpResponse convertibleResponse) {
if (logger.isDebugEnabled()) { response = convertibleResponse.delegate;
logger.debug("Reading to [" + ResolvableType.forType(bodyType) + "]"); }
} for (StatusHandler handler : this.statusHandlers) {
return (T) genericHttpMessageConverter.read(bodyType, null, this.clientResponse); if (handler.test(response)) {
} handler.handle(this.clientRequest, response);
} return;
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(); private class DefaultConvertibleClientHttpResponse implements RequestHeadersSpec.ConvertibleClientHttpResponse {
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM; private final ClientHttpResponse delegate;
}
return contentType;
public DefaultConvertibleClientHttpResponse(ClientHttpResponse delegate) {
this.delegate = delegate;
} }
private void applyStatusHandlers(HttpRequest request, ClientHttpResponse response) throws IOException {
for (StatusHandler handler : this.statusHandlers) { @Nullable
if (handler.test(response)) { @Override
handler.handle(request, response); public <T> T bodyTo(Class<T> bodyType) {
return; 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();
}
} }
} }

28
spring-web/src/main/java/org/springframework/web/client/RestClient.java

@ -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);
} }
} }

49
spring-web/src/test/java/org/springframework/web/client/RestClientIntegrationTests.java

@ -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…
Cancel
Save