Skip to content

Commit e6b4edc

Browse files
poutsmarstoyanchev
authored andcommitted
Simplify access to response body in WebClient
This commit makes a change to WebClient in oder to facilitate getting the response body as a `Mono<Object>` or `Flux<Object>` without having to deal with `ClientResponse`. Specifically, this commit: - Adds `RequestHeaderSpec.retrieve` methods, next to `exchange`, that return the response body (and not a `ClientResponse`). Two convenience methods return the response body as `Mono` or `Flux`. - Adds ClientResponse.toRequestEntity to convert the ClientResponse into a RequestEntity. Issue: SPR-15294
1 parent dd5a73b commit e6b4edc

File tree

5 files changed

+112
-0
lines changed

5 files changed

+112
-0
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientResponse.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.http.HttpStatus;
2828
import org.springframework.http.MediaType;
2929
import org.springframework.http.ResponseCookie;
30+
import org.springframework.http.ResponseEntity;
3031
import org.springframework.http.client.reactive.ClientHttpResponse;
3132
import org.springframework.util.MultiValueMap;
3233
import org.springframework.web.reactive.function.BodyExtractor;
@@ -88,6 +89,14 @@ public interface ClientResponse {
8889
*/
8990
<T> Flux<T> bodyToFlux(Class<? extends T> elementClass);
9091

92+
/**
93+
* Converts this {@code ClientResponse} into a {@code ResponseEntity}.
94+
* @param responseClass the type of response contained in the {@code ResponseEntity}
95+
* @param <T> the response type
96+
* @return a mono containing the response entity
97+
*/
98+
<T> Mono<ResponseEntity<T>> toResponseEntity(Class<T> responseClass);
99+
91100

92101
/**
93102
* Represents the headers of the HTTP response.

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.http.HttpStatus;
3434
import org.springframework.http.MediaType;
3535
import org.springframework.http.ResponseCookie;
36+
import org.springframework.http.ResponseEntity;
3637
import org.springframework.http.client.reactive.ClientHttpResponse;
3738
import org.springframework.http.codec.HttpMessageReader;
3839
import org.springframework.util.MultiValueMap;
@@ -100,6 +101,11 @@ public <T> Flux<T> bodyToFlux(Class<? extends T> elementClass) {
100101
return bodyToPublisher(BodyExtractors.toFlux(elementClass), Flux::error);
101102
}
102103

104+
@Override
105+
public <T> Mono<ResponseEntity<T>> toResponseEntity(Class<T> responseClass) {
106+
return bodyToMono(responseClass)
107+
.map(t -> new ResponseEntity<>(t, headers().asHttpHeaders(), statusCode()));
108+
}
103109

104110
private <T extends Publisher<?>> T bodyToPublisher(
105111
BodyExtractor<T, ? super ClientHttpResponse> extractor,

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@
2626
import java.util.function.Function;
2727

2828
import org.reactivestreams.Publisher;
29+
import reactor.core.publisher.Flux;
2930
import reactor.core.publisher.Mono;
3031

3132
import org.springframework.http.HttpHeaders;
3233
import org.springframework.http.HttpMethod;
3334
import org.springframework.http.MediaType;
3435
import org.springframework.http.client.reactive.ClientHttpRequest;
36+
import org.springframework.http.client.reactive.ClientHttpResponse;
3537
import org.springframework.util.CollectionUtils;
3638
import org.springframework.util.LinkedMultiValueMap;
3739
import org.springframework.util.MultiValueMap;
40+
import org.springframework.web.reactive.function.BodyExtractor;
3841
import org.springframework.web.reactive.function.BodyInserter;
3942
import org.springframework.web.reactive.function.BodyInserters;
4043
import org.springframework.web.util.DefaultUriBuilderFactory;
@@ -325,6 +328,21 @@ else if (CollectionUtils.isEmpty(this.cookies)) {
325328
return result;
326329
}
327330
}
331+
332+
@Override
333+
public <T> Mono<T> retrieve(BodyExtractor<T, ? super ClientHttpResponse> extractor) {
334+
return exchange().map(clientResponse -> clientResponse.body(extractor));
335+
}
336+
337+
@Override
338+
public <T> Mono<T> retrieveMono(Class<T> responseType) {
339+
return exchange().then(clientResponse -> clientResponse.bodyToMono(responseType));
340+
}
341+
342+
@Override
343+
public <T> Flux<T> retrieveFlux(Class<T> responseType) {
344+
return exchange().flatMap(clientResponse -> clientResponse.bodyToFlux(responseType));
345+
}
328346
}
329347

330348
}

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@
2323
import java.util.function.Function;
2424

2525
import org.reactivestreams.Publisher;
26+
import reactor.core.publisher.Flux;
2627
import reactor.core.publisher.Mono;
2728

2829
import org.springframework.http.HttpHeaders;
2930
import org.springframework.http.HttpMethod;
3031
import org.springframework.http.MediaType;
3132
import org.springframework.http.client.reactive.ClientHttpConnector;
3233
import org.springframework.http.client.reactive.ClientHttpRequest;
34+
import org.springframework.http.client.reactive.ClientHttpResponse;
3335
import org.springframework.util.MultiValueMap;
36+
import org.springframework.web.reactive.function.BodyExtractor;
3437
import org.springframework.web.reactive.function.BodyInserter;
3538
import org.springframework.web.util.UriBuilder;
3639
import org.springframework.web.util.UriBuilderFactory;
@@ -374,6 +377,38 @@ interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
374377
*/
375378
Mono<ClientResponse> exchange();
376379

380+
/**
381+
* Execute the built request, and use the given extractor to return the response body as a
382+
* delayed {@code T}.
383+
* @param extractor the extractor for the response body
384+
* @param <T> the response type
385+
* @return the body of the response, extracted with {@code extractor}
386+
*/
387+
<T> Mono<T> retrieve(BodyExtractor<T, ? super ClientHttpResponse> extractor);
388+
389+
/**
390+
* Execute the built request, and return the response body as a delayed {@code T}.
391+
* <p>This method is a convenient shortcut for {@link #retrieve(BodyExtractor)} with a
392+
* {@linkplain org.springframework.web.reactive.function.BodyExtractors#toMono(Class)
393+
* Mono body extractor}.
394+
* @param responseType the class of the response
395+
* @param <T> the response type
396+
* @return the body of the response
397+
*/
398+
<T> Mono<T> retrieveMono(Class<T> responseType);
399+
400+
/**
401+
* Execute the built request, and return the response body as a delayed sequence of
402+
* {@code T}'s.
403+
* <p>This method is a convenient shortcut for {@link #retrieve(BodyExtractor)} with a
404+
* {@linkplain org.springframework.web.reactive.function.BodyExtractors#toFlux(Class)}
405+
* Flux body extractor}.
406+
* @param responseType the class of the response
407+
* @param <T> the response type
408+
* @return the body of the response
409+
*/
410+
<T> Flux<T> retrieveFlux(Class<T> responseType);
411+
377412
}
378413

379414
interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,50 @@ public void jsonString() throws Exception {
133133
Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
134134
}
135135

136+
@Test
137+
public void jsonStringRetrieveMono() throws Exception {
138+
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
139+
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
140+
.setBody(content));
141+
142+
Mono<String> result = this.webClient.get()
143+
.uri("/json")
144+
.accept(MediaType.APPLICATION_JSON)
145+
.retrieveMono(String.class);
146+
147+
StepVerifier.create(result)
148+
.expectNext(content)
149+
.expectComplete()
150+
.verify(Duration.ofSeconds(3));
151+
152+
RecordedRequest recordedRequest = server.takeRequest();
153+
Assert.assertEquals(1, server.getRequestCount());
154+
Assert.assertEquals("/json", recordedRequest.getPath());
155+
Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
156+
}
157+
158+
@Test
159+
public void jsonStringRetrieveFlux() throws Exception {
160+
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
161+
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
162+
.setBody(content));
163+
164+
Flux<String> result = this.webClient.get()
165+
.uri("/json")
166+
.accept(MediaType.APPLICATION_JSON)
167+
.retrieveFlux(String.class);
168+
169+
StepVerifier.create(result)
170+
.expectNext(content)
171+
.expectComplete()
172+
.verify(Duration.ofSeconds(3));
173+
174+
RecordedRequest recordedRequest = server.takeRequest();
175+
Assert.assertEquals(1, server.getRequestCount());
176+
Assert.assertEquals("/json", recordedRequest.getPath());
177+
Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
178+
}
179+
136180
@Test
137181
public void jsonPojoMono() throws Exception {
138182
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")

0 commit comments

Comments
 (0)