From 840d7abbf633745e8a7e17fe08ff356cac170e03 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 29 Mar 2017 14:09:38 -0400 Subject: [PATCH] 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 or ResponseEntity>. Issue: SPR-15294 --- .../function/client/ClientResponse.java | 9 -- .../client/DefaultClientResponse.java | 7 -- .../function/client/DefaultWebClient.java | 57 ++++++--- .../reactive/function/client/WebClient.java | 119 +++++++++++++----- .../client/WebClientIntegrationTests.java | 66 +++++++++- 5 files changed, 194 insertions(+), 64 deletions(-) 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)