Add ResponseSpec to WebClient
Replace the overloaded "retrieve" methods with a single retrieve() + ResponseSpec exposing shortcut methods (bodyToMono, bodyToFlux) mirroring the ClientResponse shortcuts it delegates to. Unlike exchange() however with retrieve() there is no access to other parts of ClientResponse so ResponseSpec exposes additional shortcuts for obtain ResponseEntity<T> or ResponseEntity<List<T>>. Issue: SPR-15294
This commit is contained in:
parent
e6b4edc757
commit
840d7abbf6
|
|
@ -27,7 +27,6 @@ import org.springframework.http.HttpHeaders;
|
|||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
|
|
@ -89,14 +88,6 @@ public interface ClientResponse {
|
|||
*/
|
||||
<T> Flux<T> bodyToFlux(Class<? extends T> elementClass);
|
||||
|
||||
/**
|
||||
* Converts this {@code ClientResponse} into a {@code ResponseEntity}.
|
||||
* @param responseClass the type of response contained in the {@code ResponseEntity}
|
||||
* @param <T> the response type
|
||||
* @return a mono containing the response entity
|
||||
*/
|
||||
<T> Mono<ResponseEntity<T>> toResponseEntity(Class<T> responseClass);
|
||||
|
||||
|
||||
/**
|
||||
* Represents the headers of the HTTP response.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import org.springframework.http.HttpHeaders;
|
|||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
|
@ -101,12 +100,6 @@ class DefaultClientResponse implements ClientResponse {
|
|||
return bodyToPublisher(BodyExtractors.toFlux(elementClass), Flux::error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<T>> toResponseEntity(Class<T> responseClass) {
|
||||
return bodyToMono(responseClass)
|
||||
.map(t -> new ResponseEntity<>(t, headers().asHttpHeaders(), statusCode()));
|
||||
}
|
||||
|
||||
private <T extends Publisher<?>> T bodyToPublisher(
|
||||
BodyExtractor<T, ? super ClientHttpResponse> extractor,
|
||||
Function<WebClientException, T> errorFunction) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import java.time.ZoneId;
|
|||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
|
|
@ -32,12 +33,11 @@ import reactor.core.publisher.Mono;
|
|||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
|
@ -330,19 +330,48 @@ class DefaultWebClient implements WebClient {
|
|||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> retrieve(BodyExtractor<T, ? super ClientHttpResponse> extractor) {
|
||||
return exchange().map(clientResponse -> clientResponse.body(extractor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> retrieveMono(Class<T> responseType) {
|
||||
return exchange().then(clientResponse -> clientResponse.bodyToMono(responseType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> retrieveFlux(Class<T> responseType) {
|
||||
return exchange().flatMap(clientResponse -> clientResponse.bodyToFlux(responseType));
|
||||
public ResponseSpec retrieve() {
|
||||
return new DefaultResponseSpec(exchange());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultResponseSpec implements ResponseSpec {
|
||||
|
||||
private final Mono<ClientResponse> responseMono;
|
||||
|
||||
|
||||
DefaultResponseSpec(Mono<ClientResponse> responseMono) {
|
||||
this.responseMono = responseMono;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> bodyToMono(Class<T> bodyType) {
|
||||
return this.responseMono.then(clientResponse -> clientResponse.bodyToMono(bodyType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> bodyToFlux(Class<T> elementType) {
|
||||
return this.responseMono.flatMap(clientResponse -> clientResponse.bodyToFlux(elementType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<T>> bodyToEntity(Class<T> bodyType) {
|
||||
return this.responseMono.then(response ->
|
||||
response.bodyToMono(bodyType).map(body -> {
|
||||
HttpHeaders headers = response.headers().asHttpHeaders();
|
||||
return new ResponseEntity<>(body, headers, response.statusCode());
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<List<T>>> bodyToEntityList(Class<T> responseType) {
|
||||
return this.responseMono.then(response ->
|
||||
response.bodyToFlux(responseType).collectList().map(body -> {
|
||||
HttpHeaders headers = response.headers().asHttpHeaders();
|
||||
return new ResponseEntity<>(body, headers, response.statusCode());
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package org.springframework.web.reactive.function.client;
|
|||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
|
|
@ -29,11 +30,10 @@ import reactor.core.publisher.Mono;
|
|||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.http.client.reactive.ClientHttpRequest;
|
||||
import org.springframework.http.client.reactive.ClientHttpResponse;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyExtractor;
|
||||
import org.springframework.web.reactive.function.BodyInserter;
|
||||
import org.springframework.web.util.UriBuilder;
|
||||
import org.springframework.web.util.UriBuilderFactory;
|
||||
|
|
@ -372,42 +372,53 @@ public interface WebClient {
|
|||
S headers(HttpHeaders headers);
|
||||
|
||||
/**
|
||||
* Exchange the built request for a delayed {@code ClientResponse}.
|
||||
* Exchange the request for a {@code ClientResponse} with full access
|
||||
* to the response status and headers before extracting the body.
|
||||
*
|
||||
* <p>Use {@link Mono#then(Function)} or {@link Mono#flatMap(Function)}
|
||||
* to compose further on the response:
|
||||
*
|
||||
* <pre>
|
||||
* Mono<Pojo> mono = client.get().uri("/")
|
||||
* .accept(MediaType.APPLICATION_JSON)
|
||||
* .exchange()
|
||||
* .then(response -> response.bodyToMono(Pojo.class));
|
||||
*
|
||||
* Flux<Pojo> flux = client.get().uri("/")
|
||||
* .accept(MediaType.APPLICATION_STREAM_JSON)
|
||||
* .exchange()
|
||||
* .then(response -> response.bodyToFlux(Pojo.class));
|
||||
* </pre>
|
||||
*
|
||||
* @return a {@code Mono} with the response
|
||||
*/
|
||||
Mono<ClientResponse> exchange();
|
||||
|
||||
/**
|
||||
* Execute the built request, and use the given extractor to return the response body as a
|
||||
* delayed {@code T}.
|
||||
* @param extractor the extractor for the response body
|
||||
* @param <T> the response type
|
||||
* @return the body of the response, extracted with {@code extractor}
|
||||
* A variant of {@link #exchange()} that provides the shortest path to
|
||||
* retrieving the full response (i.e. status, headers, and body) where
|
||||
* instead of returning {@code Mono<ClientResponse>} it exposes shortcut
|
||||
* methods to extract the response body.
|
||||
*
|
||||
* <p>Use of this method is simpler when you don't need to deal directly
|
||||
* with {@link ClientResponse}, e.g. to use a custom {@code BodyExtractor}
|
||||
* or to check the status and headers before extracting the response.
|
||||
*
|
||||
* <pre>
|
||||
* Mono<Pojo> bodyMono = client.get().uri("/")
|
||||
* .accept(MediaType.APPLICATION_JSON)
|
||||
* .retrieve()
|
||||
* .bodyToMono(Pojo.class);
|
||||
*
|
||||
* Mono<ResponseEntity<Pojo>> entityMono = client.get().uri("/")
|
||||
* .accept(MediaType.APPLICATION_JSON)
|
||||
* .retrieve()
|
||||
* .bodyToEntity(Pojo.class);
|
||||
* </pre>
|
||||
*
|
||||
* @return spec with options for extracting the response body
|
||||
*/
|
||||
<T> Mono<T> retrieve(BodyExtractor<T, ? super ClientHttpResponse> extractor);
|
||||
|
||||
/**
|
||||
* Execute the built request, and return the response body as a delayed {@code T}.
|
||||
* <p>This method is a convenient shortcut for {@link #retrieve(BodyExtractor)} with a
|
||||
* {@linkplain org.springframework.web.reactive.function.BodyExtractors#toMono(Class)
|
||||
* Mono body extractor}.
|
||||
* @param responseType the class of the response
|
||||
* @param <T> the response type
|
||||
* @return the body of the response
|
||||
*/
|
||||
<T> Mono<T> retrieveMono(Class<T> responseType);
|
||||
|
||||
/**
|
||||
* Execute the built request, and return the response body as a delayed sequence of
|
||||
* {@code T}'s.
|
||||
* <p>This method is a convenient shortcut for {@link #retrieve(BodyExtractor)} with a
|
||||
* {@linkplain org.springframework.web.reactive.function.BodyExtractors#toFlux(Class)}
|
||||
* Flux body extractor}.
|
||||
* @param responseType the class of the response
|
||||
* @param <T> the response type
|
||||
* @return the body of the response
|
||||
*/
|
||||
<T> Flux<T> retrieveFlux(Class<T> responseType);
|
||||
ResponseSpec retrieve();
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -465,4 +476,48 @@ public interface WebClient {
|
|||
|
||||
}
|
||||
|
||||
interface ResponseSpec {
|
||||
|
||||
/**
|
||||
* Extract the response body to an Object of type {@code <T>} by
|
||||
* invoking {@link ClientResponse#bodyToMono(Class)}.
|
||||
*
|
||||
* @param bodyType the expected response body type
|
||||
* @param <T> response body type
|
||||
* @return {@code Mono} with the result
|
||||
*/
|
||||
<T> Mono<T> bodyToMono(Class<T> bodyType);
|
||||
|
||||
/**
|
||||
* Extract the response body to a stream of Objects of type {@code <T>}
|
||||
* by invoking {@link ClientResponse#bodyToFlux(Class)}.
|
||||
*
|
||||
* @param elementType the type of element in the response
|
||||
* @param <T> the type of elements in the response
|
||||
* @return the body of the response
|
||||
*/
|
||||
<T> Flux<T> bodyToFlux(Class<T> elementType);
|
||||
|
||||
/**
|
||||
* A variant of {@link #bodyToMono(Class)} that also provides access to
|
||||
* the response status and headers.
|
||||
*
|
||||
* @param bodyType the expected response body type
|
||||
* @param <T> response body type
|
||||
* @return {@code Mono} with the result
|
||||
*/
|
||||
<T> Mono<ResponseEntity<T>> bodyToEntity(Class<T> bodyType);
|
||||
|
||||
/**
|
||||
* A variant of {@link #bodyToFlux(Class)} collected via
|
||||
* {@link Flux#collectList()} and wrapped in {@code ResponseEntity}.
|
||||
*
|
||||
* @param elementType the expected response body list element type
|
||||
* @param <T> the type of elements in the list
|
||||
* @return {@code Mono} with the result
|
||||
*/
|
||||
<T> Mono<ResponseEntity<List<T>>> bodyToEntityList(Class<T> elementType);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package org.springframework.web.reactive.function.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
|
|
@ -33,6 +35,7 @@ import reactor.test.StepVerifier;
|
|||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.Pojo;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
|
@ -142,7 +145,8 @@ public class WebClientIntegrationTests {
|
|||
Mono<String> result = this.webClient.get()
|
||||
.uri("/json")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieveMono(String.class);
|
||||
.retrieve()
|
||||
.bodyToMono(String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext(content)
|
||||
|
|
@ -155,6 +159,63 @@ public class WebClientIntegrationTests {
|
|||
Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jsonStringRetrieveEntity() throws Exception {
|
||||
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
|
||||
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
|
||||
.setBody(content));
|
||||
|
||||
Mono<ResponseEntity<String>> result = this.webClient.get()
|
||||
.uri("/json")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToEntity(String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(entity -> {
|
||||
assertEquals(HttpStatus.OK, entity.getStatusCode());
|
||||
assertEquals(MediaType.APPLICATION_JSON, entity.getHeaders().getContentType());
|
||||
assertEquals(31, entity.getHeaders().getContentLength());
|
||||
assertEquals(content, entity.getBody());
|
||||
})
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
Assert.assertEquals(1, server.getRequestCount());
|
||||
Assert.assertEquals("/json", recordedRequest.getPath());
|
||||
Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jsonStringRetrieveEntityList() throws Exception {
|
||||
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
|
||||
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json").setBody(content));
|
||||
|
||||
Mono<ResponseEntity<List<Pojo>>> result = this.webClient.get()
|
||||
.uri("/json")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToEntityList(Pojo.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(entity -> {
|
||||
assertEquals(HttpStatus.OK, entity.getStatusCode());
|
||||
assertEquals(MediaType.APPLICATION_JSON, entity.getHeaders().getContentType());
|
||||
assertEquals(58, entity.getHeaders().getContentLength());
|
||||
Pojo pojo1 = new Pojo("foo1", "bar1");
|
||||
Pojo pojo2 = new Pojo("foo2", "bar2");
|
||||
assertEquals(Arrays.asList(pojo1, pojo2), entity.getBody());
|
||||
})
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
|
||||
RecordedRequest recordedRequest = server.takeRequest();
|
||||
Assert.assertEquals(1, server.getRequestCount());
|
||||
Assert.assertEquals("/json", recordedRequest.getPath());
|
||||
Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jsonStringRetrieveFlux() throws Exception {
|
||||
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
|
||||
|
|
@ -164,7 +225,8 @@ public class WebClientIntegrationTests {
|
|||
Flux<String> result = this.webClient.get()
|
||||
.uri("/json")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieveFlux(String.class);
|
||||
.retrieve()
|
||||
.bodyToFlux(String.class);
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext(content)
|
||||
|
|
|
|||
Loading…
Reference in New Issue