Skip to content

Commit 840d7ab

Browse files
committed
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
1 parent e6b4edc commit 840d7ab

File tree

5 files changed

+189
-59
lines changed

5 files changed

+189
-59
lines changed

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

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

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-
10091

10192
/**
10293
* Represents the headers of the HTTP response.

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

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

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-
}
109-
110103
private <T extends Publisher<?>> T bodyToPublisher(
111104
BodyExtractor<T, ? super ClientHttpResponse> extractor,
112105
Function<WebClientException, T> errorFunction) {

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

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.time.ZonedDateTime;
2323
import java.time.format.DateTimeFormatter;
2424
import java.util.Arrays;
25+
import java.util.List;
2526
import java.util.Map;
2627
import java.util.function.Function;
2728

@@ -32,12 +33,11 @@
3233
import org.springframework.http.HttpHeaders;
3334
import org.springframework.http.HttpMethod;
3435
import org.springframework.http.MediaType;
36+
import org.springframework.http.ResponseEntity;
3537
import org.springframework.http.client.reactive.ClientHttpRequest;
36-
import org.springframework.http.client.reactive.ClientHttpResponse;
3738
import org.springframework.util.CollectionUtils;
3839
import org.springframework.util.LinkedMultiValueMap;
3940
import org.springframework.util.MultiValueMap;
40-
import org.springframework.web.reactive.function.BodyExtractor;
4141
import org.springframework.web.reactive.function.BodyInserter;
4242
import org.springframework.web.reactive.function.BodyInserters;
4343
import org.springframework.web.util.DefaultUriBuilderFactory;
@@ -330,19 +330,48 @@ else if (CollectionUtils.isEmpty(this.cookies)) {
330330
}
331331

332332
@Override
333-
public <T> Mono<T> retrieve(BodyExtractor<T, ? super ClientHttpResponse> extractor) {
334-
return exchange().map(clientResponse -> clientResponse.body(extractor));
333+
public ResponseSpec retrieve() {
334+
return new DefaultResponseSpec(exchange());
335+
}
336+
}
337+
338+
private static class DefaultResponseSpec implements ResponseSpec {
339+
340+
private final Mono<ClientResponse> responseMono;
341+
342+
343+
DefaultResponseSpec(Mono<ClientResponse> responseMono) {
344+
this.responseMono = responseMono;
335345
}
336346

337347
@Override
338-
public <T> Mono<T> retrieveMono(Class<T> responseType) {
339-
return exchange().then(clientResponse -> clientResponse.bodyToMono(responseType));
348+
public <T> Mono<T> bodyToMono(Class<T> bodyType) {
349+
return this.responseMono.then(clientResponse -> clientResponse.bodyToMono(bodyType));
340350
}
341351

342352
@Override
343-
public <T> Flux<T> retrieveFlux(Class<T> responseType) {
344-
return exchange().flatMap(clientResponse -> clientResponse.bodyToFlux(responseType));
353+
public <T> Flux<T> bodyToFlux(Class<T> elementType) {
354+
return this.responseMono.flatMap(clientResponse -> clientResponse.bodyToFlux(elementType));
345355
}
346-
}
347356

357+
@Override
358+
public <T> Mono<ResponseEntity<T>> bodyToEntity(Class<T> bodyType) {
359+
return this.responseMono.then(response ->
360+
response.bodyToMono(bodyType).map(body -> {
361+
HttpHeaders headers = response.headers().asHttpHeaders();
362+
return new ResponseEntity<>(body, headers, response.statusCode());
363+
})
364+
);
365+
}
366+
367+
@Override
368+
public <T> Mono<ResponseEntity<List<T>>> bodyToEntityList(Class<T> responseType) {
369+
return this.responseMono.then(response ->
370+
response.bodyToFlux(responseType).collectList().map(body -> {
371+
HttpHeaders headers = response.headers().asHttpHeaders();
372+
return new ResponseEntity<>(body, headers, response.statusCode());
373+
})
374+
);
375+
}
376+
}
348377
}

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

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.net.URI;
2020
import java.nio.charset.Charset;
2121
import java.time.ZonedDateTime;
22+
import java.util.List;
2223
import java.util.Map;
2324
import java.util.function.Function;
2425

@@ -29,11 +30,10 @@
2930
import org.springframework.http.HttpHeaders;
3031
import org.springframework.http.HttpMethod;
3132
import org.springframework.http.MediaType;
33+
import org.springframework.http.ResponseEntity;
3234
import org.springframework.http.client.reactive.ClientHttpConnector;
3335
import org.springframework.http.client.reactive.ClientHttpRequest;
34-
import org.springframework.http.client.reactive.ClientHttpResponse;
3536
import org.springframework.util.MultiValueMap;
36-
import org.springframework.web.reactive.function.BodyExtractor;
3737
import org.springframework.web.reactive.function.BodyInserter;
3838
import org.springframework.web.util.UriBuilder;
3939
import org.springframework.web.util.UriBuilderFactory;
@@ -372,42 +372,53 @@ interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
372372
S headers(HttpHeaders headers);
373373

374374
/**
375-
* Exchange the built request for a delayed {@code ClientResponse}.
375+
* Exchange the request for a {@code ClientResponse} with full access
376+
* to the response status and headers before extracting the body.
377+
*
378+
* <p>Use {@link Mono#then(Function)} or {@link Mono#flatMap(Function)}
379+
* to compose further on the response:
380+
*
381+
* <pre>
382+
* Mono&lt;Pojo&gt; mono = client.get().uri("/")
383+
* .accept(MediaType.APPLICATION_JSON)
384+
* .exchange()
385+
* .then(response -> response.bodyToMono(Pojo.class));
386+
*
387+
* Flux&lt;Pojo&gt; flux = client.get().uri("/")
388+
* .accept(MediaType.APPLICATION_STREAM_JSON)
389+
* .exchange()
390+
* .then(response -> response.bodyToFlux(Pojo.class));
391+
* </pre>
392+
*
376393
* @return a {@code Mono} with the response
377394
*/
378395
Mono<ClientResponse> exchange();
379396

380397
/**
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}
398+
* A variant of {@link #exchange()} that provides the shortest path to
399+
* retrieving the full response (i.e. status, headers, and body) where
400+
* instead of returning {@code Mono<ClientResponse>} it exposes shortcut
401+
* methods to extract the response body.
402+
*
403+
* <p>Use of this method is simpler when you don't need to deal directly
404+
* with {@link ClientResponse}, e.g. to use a custom {@code BodyExtractor}
405+
* or to check the status and headers before extracting the response.
406+
*
407+
* <pre>
408+
* Mono&lt;Pojo&gt; bodyMono = client.get().uri("/")
409+
* .accept(MediaType.APPLICATION_JSON)
410+
* .retrieve()
411+
* .bodyToMono(Pojo.class);
412+
*
413+
* Mono&lt;ResponseEntity&lt;Pojo&gt;&gt; entityMono = client.get().uri("/")
414+
* .accept(MediaType.APPLICATION_JSON)
415+
* .retrieve()
416+
* .bodyToEntity(Pojo.class);
417+
* </pre>
418+
*
419+
* @return spec with options for extracting the response body
386420
*/
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);
421+
ResponseSpec retrieve();
411422

412423
}
413424

@@ -465,4 +476,48 @@ interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
465476

466477
}
467478

479+
interface ResponseSpec {
480+
481+
/**
482+
* Extract the response body to an Object of type {@code <T>} by
483+
* invoking {@link ClientResponse#bodyToMono(Class)}.
484+
*
485+
* @param bodyType the expected response body type
486+
* @param <T> response body type
487+
* @return {@code Mono} with the result
488+
*/
489+
<T> Mono<T> bodyToMono(Class<T> bodyType);
490+
491+
/**
492+
* Extract the response body to a stream of Objects of type {@code <T>}
493+
* by invoking {@link ClientResponse#bodyToFlux(Class)}.
494+
*
495+
* @param elementType the type of element in the response
496+
* @param <T> the type of elements in the response
497+
* @return the body of the response
498+
*/
499+
<T> Flux<T> bodyToFlux(Class<T> elementType);
500+
501+
/**
502+
* A variant of {@link #bodyToMono(Class)} that also provides access to
503+
* the response status and headers.
504+
*
505+
* @param bodyType the expected response body type
506+
* @param <T> response body type
507+
* @return {@code Mono} with the result
508+
*/
509+
<T> Mono<ResponseEntity<T>> bodyToEntity(Class<T> bodyType);
510+
511+
/**
512+
* A variant of {@link #bodyToFlux(Class)} collected via
513+
* {@link Flux#collectList()} and wrapped in {@code ResponseEntity}.
514+
*
515+
* @param elementType the expected response body list element type
516+
* @param <T> the type of elements in the list
517+
* @return {@code Mono} with the result
518+
*/
519+
<T> Mono<ResponseEntity<List<T>>> bodyToEntityList(Class<T> elementType);
520+
521+
}
522+
468523
}

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

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.web.reactive.function.client;
1818

1919
import java.time.Duration;
20+
import java.util.Arrays;
21+
import java.util.List;
2022

2123
import okhttp3.mockwebserver.MockResponse;
2224
import okhttp3.mockwebserver.MockWebServer;
@@ -33,6 +35,7 @@
3335
import org.springframework.http.HttpHeaders;
3436
import org.springframework.http.HttpStatus;
3537
import org.springframework.http.MediaType;
38+
import org.springframework.http.ResponseEntity;
3639
import org.springframework.http.codec.Pojo;
3740

3841
import static org.junit.Assert.assertEquals;
@@ -142,7 +145,8 @@ public void jsonStringRetrieveMono() throws Exception {
142145
Mono<String> result = this.webClient.get()
143146
.uri("/json")
144147
.accept(MediaType.APPLICATION_JSON)
145-
.retrieveMono(String.class);
148+
.retrieve()
149+
.bodyToMono(String.class);
146150

147151
StepVerifier.create(result)
148152
.expectNext(content)
@@ -155,6 +159,63 @@ public void jsonStringRetrieveMono() throws Exception {
155159
Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
156160
}
157161

162+
@Test
163+
public void jsonStringRetrieveEntity() throws Exception {
164+
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
165+
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json")
166+
.setBody(content));
167+
168+
Mono<ResponseEntity<String>> result = this.webClient.get()
169+
.uri("/json")
170+
.accept(MediaType.APPLICATION_JSON)
171+
.retrieve()
172+
.bodyToEntity(String.class);
173+
174+
StepVerifier.create(result)
175+
.consumeNextWith(entity -> {
176+
assertEquals(HttpStatus.OK, entity.getStatusCode());
177+
assertEquals(MediaType.APPLICATION_JSON, entity.getHeaders().getContentType());
178+
assertEquals(31, entity.getHeaders().getContentLength());
179+
assertEquals(content, entity.getBody());
180+
})
181+
.expectComplete()
182+
.verify(Duration.ofSeconds(3));
183+
184+
RecordedRequest recordedRequest = server.takeRequest();
185+
Assert.assertEquals(1, server.getRequestCount());
186+
Assert.assertEquals("/json", recordedRequest.getPath());
187+
Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
188+
}
189+
190+
@Test
191+
public void jsonStringRetrieveEntityList() throws Exception {
192+
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
193+
this.server.enqueue(new MockResponse().setHeader("Content-Type", "application/json").setBody(content));
194+
195+
Mono<ResponseEntity<List<Pojo>>> result = this.webClient.get()
196+
.uri("/json")
197+
.accept(MediaType.APPLICATION_JSON)
198+
.retrieve()
199+
.bodyToEntityList(Pojo.class);
200+
201+
StepVerifier.create(result)
202+
.consumeNextWith(entity -> {
203+
assertEquals(HttpStatus.OK, entity.getStatusCode());
204+
assertEquals(MediaType.APPLICATION_JSON, entity.getHeaders().getContentType());
205+
assertEquals(58, entity.getHeaders().getContentLength());
206+
Pojo pojo1 = new Pojo("foo1", "bar1");
207+
Pojo pojo2 = new Pojo("foo2", "bar2");
208+
assertEquals(Arrays.asList(pojo1, pojo2), entity.getBody());
209+
})
210+
.expectComplete()
211+
.verify(Duration.ofSeconds(3));
212+
213+
RecordedRequest recordedRequest = server.takeRequest();
214+
Assert.assertEquals(1, server.getRequestCount());
215+
Assert.assertEquals("/json", recordedRequest.getPath());
216+
Assert.assertEquals("application/json", recordedRequest.getHeader(HttpHeaders.ACCEPT));
217+
}
218+
158219
@Test
159220
public void jsonStringRetrieveFlux() throws Exception {
160221
String content = "{\"bar\":\"barbar\",\"foo\":\"foofoo\"}";
@@ -164,7 +225,8 @@ public void jsonStringRetrieveFlux() throws Exception {
164225
Flux<String> result = this.webClient.get()
165226
.uri("/json")
166227
.accept(MediaType.APPLICATION_JSON)
167-
.retrieveFlux(String.class);
228+
.retrieve()
229+
.bodyToFlux(String.class);
168230

169231
StepVerifier.create(result)
170232
.expectNext(content)

0 commit comments

Comments
 (0)