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 4b0170e0eb..66702daba8 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,6 +27,7 @@ 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; @@ -88,6 +89,14 @@ 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 0807129cf7..33fa0f315d 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,6 +33,7 @@ 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; @@ -100,6 +101,11 @@ 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, 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 44de4ed753..54373192c9 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 @@ -26,15 +26,18 @@ import java.util.Map; import java.util.function.Function; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; 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; @@ -325,6 +328,21 @@ class DefaultWebClient implements WebClient { return result; } } + + @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)); + } } } 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 0f608835c2..0da41102df 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 @@ -23,6 +23,7 @@ import java.util.Map; import java.util.function.Function; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; @@ -30,7 +31,9 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; 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; @@ -374,6 +377,38 @@ public interface WebClient { */ 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} + */ + 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); + } interface RequestBodySpec extends RequestHeadersSpec { 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 8c6e34694c..c6a3b204a9 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 @@ -133,6 +133,50 @@ public class WebClientIntegrationTests { Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT)); } + @Test + public void jsonStringRetrieveMono() 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) + .retrieveMono(String.class); + + StepVerifier.create(result) + .expectNext(content) + .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\"}"; + this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json") + .setBody(content)); + + Flux result = this.webClient.get() + .uri("/json") + .accept(MediaType.APPLICATION_JSON) + .retrieveFlux(String.class); + + StepVerifier.create(result) + .expectNext(content) + .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 jsonPojoMono() throws Exception { this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")