diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java index 66702daba8..4b0170e0eb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java @@ -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 { */ Flux bodyToFlux(Class elementClass); - /** - * Converts this {@code ClientResponse} into a {@code ResponseEntity}. - * @param responseClass the type of response contained in the {@code ResponseEntity} - * @param the response type - * @return a mono containing the response entity - */ - Mono> toResponseEntity(Class responseClass); - /** * Represents the headers of the HTTP response. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java index 33fa0f315d..a64be4599e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java @@ -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 Mono> toResponseEntity(Class responseClass) { - return bodyToMono(responseClass) - .map(t -> new ResponseEntity<>(t, headers().asHttpHeaders(), statusCode())); - } - private > T bodyToPublisher( BodyExtractor extractor, Function errorFunction) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 54373192c9..ce43790f59 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -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 Mono retrieve(BodyExtractor extractor) { - return exchange().map(clientResponse -> clientResponse.body(extractor)); - } - - @Override - public Mono retrieveMono(Class responseType) { - return exchange().then(clientResponse -> clientResponse.bodyToMono(responseType)); - } - - @Override - public Flux retrieveFlux(Class responseType) { - return exchange().flatMap(clientResponse -> clientResponse.bodyToFlux(responseType)); + public ResponseSpec retrieve() { + return new DefaultResponseSpec(exchange()); } } + private static class DefaultResponseSpec implements ResponseSpec { + + private final Mono responseMono; + + + DefaultResponseSpec(Mono responseMono) { + this.responseMono = responseMono; + } + + @Override + public Mono bodyToMono(Class bodyType) { + return this.responseMono.then(clientResponse -> clientResponse.bodyToMono(bodyType)); + } + + @Override + public Flux bodyToFlux(Class elementType) { + return this.responseMono.flatMap(clientResponse -> clientResponse.bodyToFlux(elementType)); + } + + @Override + public Mono> bodyToEntity(Class 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 Mono>> bodyToEntityList(Class responseType) { + return this.responseMono.then(response -> + response.bodyToFlux(responseType).collectList().map(body -> { + HttpHeaders headers = response.headers().asHttpHeaders(); + return new ResponseEntity<>(body, headers, response.statusCode()); + }) + ); + } + } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 0da41102df..d9e486d7eb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -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. + * + *

Use {@link Mono#then(Function)} or {@link Mono#flatMap(Function)} + * to compose further on the response: + * + *

+		 *	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));
+		 * 
+ * * @return a {@code Mono} with the response */ Mono 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 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} it exposes shortcut + * methods to extract the response body. + * + *

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. + * + *

+		 *	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);
+		 * 
+ * + * @return spec with options for extracting the response body */ - Mono retrieve(BodyExtractor extractor); - - /** - * Execute the built request, and return the response body as a delayed {@code T}. - *

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 the response type - * @return the body of the response - */ - Mono retrieveMono(Class responseType); - - /** - * Execute the built request, and return the response body as a delayed sequence of - * {@code T}'s. - *

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 the response type - * @return the body of the response - */ - Flux retrieveFlux(Class responseType); + ResponseSpec retrieve(); } @@ -465,4 +476,48 @@ public interface WebClient { } + interface ResponseSpec { + + /** + * Extract the response body to an Object of type {@code } by + * invoking {@link ClientResponse#bodyToMono(Class)}. + * + * @param bodyType the expected response body type + * @param response body type + * @return {@code Mono} with the result + */ + Mono bodyToMono(Class bodyType); + + /** + * Extract the response body to a stream of Objects of type {@code } + * by invoking {@link ClientResponse#bodyToFlux(Class)}. + * + * @param elementType the type of element in the response + * @param the type of elements in the response + * @return the body of the response + */ + Flux bodyToFlux(Class 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 response body type + * @return {@code Mono} with the result + */ + Mono> bodyToEntity(Class 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 the type of elements in the list + * @return {@code Mono} with the result + */ + Mono>> bodyToEntityList(Class elementType); + + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index c6a3b204a9..68d94b9772 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -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 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> 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>> 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 result = this.webClient.get() .uri("/json") .accept(MediaType.APPLICATION_JSON) - .retrieveFlux(String.class); + .retrieve() + .bodyToFlux(String.class); StepVerifier.create(result) .expectNext(content)