Skip to content

Commit 5e954dc

Browse files
committed
Use ParameterizedTypeReference in public-facing WebFlux APIs
This commit changes the use of `ResolvableType` to `ParameterizedTypeReference` in all public-facing WebFlux APIs. This change removes the necessity for providing the parameterized type information twice: once for creating the `ResolvableType`, and once for specifying a `BodyExtractor`. Issue: SPR-15636
1 parent b6c09fa commit 5e954dc

File tree

10 files changed

+119
-58
lines changed

10 files changed

+119
-58
lines changed

spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,20 @@
3232
import reactor.core.publisher.Flux;
3333
import reactor.core.publisher.Mono;
3434

35-
import org.springframework.core.ResolvableType;
35+
import org.springframework.core.ParameterizedTypeReference;
3636
import org.springframework.core.io.ByteArrayResource;
3737
import org.springframework.http.HttpHeaders;
3838
import org.springframework.http.HttpMethod;
3939
import org.springframework.http.MediaType;
4040
import org.springframework.http.client.reactive.ClientHttpConnector;
4141
import org.springframework.http.client.reactive.ClientHttpRequest;
42+
import org.springframework.http.client.reactive.ClientHttpResponse;
4243
import org.springframework.lang.Nullable;
4344
import org.springframework.test.util.JsonExpectationsHelper;
4445
import org.springframework.util.Assert;
4546
import org.springframework.util.MimeType;
4647
import org.springframework.util.MultiValueMap;
48+
import org.springframework.web.reactive.function.BodyExtractor;
4749
import org.springframework.web.reactive.function.BodyInserter;
4850
import org.springframework.web.reactive.function.client.ClientResponse;
4951
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
@@ -289,20 +291,19 @@ private static class UndecodedExchangeResult extends ExchangeResult {
289291
this.timeout = timeout;
290292
}
291293

292-
@SuppressWarnings("unchecked")
293-
public <T> EntityExchangeResult<T> decode(ResolvableType bodyType) {
294-
T body = (T) this.response.body(toMono(bodyType)).block(this.timeout);
294+
public <T> EntityExchangeResult<T> decode(BodyExtractor<Mono<T>, ? super ClientHttpResponse> extractor) {
295+
T body = this.response.body(extractor).block(this.timeout);
295296
return new EntityExchangeResult<>(this, body);
296297
}
297298

298-
public <T> EntityExchangeResult<List<T>> decodeToList(ResolvableType elementType) {
299-
Flux<T> flux = this.response.body(toFlux(elementType));
299+
public <T> EntityExchangeResult<List<T>> decodeToList(BodyExtractor<Flux<T>, ? super ClientHttpResponse> extractor) {
300+
Flux<T> flux = this.response.body(extractor);
300301
List<T> body = flux.collectList().block(this.timeout);
301302
return new EntityExchangeResult<>(this, body);
302303
}
303304

304-
public <T> FluxExchangeResult<T> decodeToFlux(ResolvableType elementType) {
305-
Flux<T> body = this.response.body(toFlux(elementType));
305+
public <T> FluxExchangeResult<T> decodeToFlux(BodyExtractor<Flux<T>, ? super ClientHttpResponse> extractor) {
306+
Flux<T> body = this.response.body(extractor);
306307
return new FluxExchangeResult<>(this, body, this.timeout);
307308
}
308309

@@ -333,25 +334,23 @@ public HeaderAssertions expectHeader() {
333334
}
334335

335336
@Override
336-
@SuppressWarnings("unchecked")
337337
public <B> BodySpec<B, ?> expectBody(Class<B> bodyType) {
338-
return (BodySpec<B, ?>) expectBody(ResolvableType.forClass(bodyType));
338+
return new DefaultBodySpec<>(this.result.decode(toMono(bodyType)));
339339
}
340340

341341
@Override
342-
@SuppressWarnings({"rawtypes", "unchecked"})
343-
public <B> BodySpec<B, ?> expectBody(ResolvableType bodyType) {
344-
return new DefaultBodySpec(this.result.decode(bodyType));
342+
public <B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType) {
343+
return new DefaultBodySpec<>(this.result.decode(toMono(bodyType)));
345344
}
346345

347346
@Override
348347
public <E> ListBodySpec<E> expectBodyList(Class<E> elementType) {
349-
return expectBodyList(ResolvableType.forClass(elementType));
348+
return new DefaultListBodySpec<>(this.result.decodeToList(toFlux(elementType)));
350349
}
351350

352351
@Override
353-
public <E> ListBodySpec<E> expectBodyList(ResolvableType elementType) {
354-
return new DefaultListBodySpec<>(this.result.decodeToList(elementType));
352+
public <E> ListBodySpec<E> expectBodyList(ParameterizedTypeReference<E> elementType) {
353+
return new DefaultListBodySpec<>(this.result.decodeToList(toFlux(elementType)));
355354
}
356355

357356
@Override
@@ -361,12 +360,12 @@ public BodyContentSpec expectBody() {
361360

362361
@Override
363362
public <T> FluxExchangeResult<T> returnResult(Class<T> elementType) {
364-
return returnResult(ResolvableType.forClass(elementType));
363+
return this.result.decodeToFlux(toFlux(elementType));
365364
}
366365

367366
@Override
368-
public <T> FluxExchangeResult<T> returnResult(ResolvableType elementType) {
369-
return this.result.decodeToFlux(elementType);
367+
public <T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementType) {
368+
return this.result.decodeToFlux(toFlux(elementType));
370369
}
371370
}
372371

spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import org.reactivestreams.Publisher;
2929

3030
import org.springframework.context.ApplicationContext;
31-
import org.springframework.core.ResolvableType;
31+
import org.springframework.core.ParameterizedTypeReference;
3232
import org.springframework.format.FormatterRegistry;
3333
import org.springframework.http.HttpHeaders;
3434
import org.springframework.http.MediaType;
@@ -534,7 +534,7 @@ interface ResponseSpec {
534534
/**
535535
* Variant of {@link #expectBody(Class)} for a body type with generics.
536536
*/
537-
<B> BodySpec<B, ?> expectBody(ResolvableType bodyType);
537+
<B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType);
538538

539539
/**
540540
* Declare expectations on the response body decoded to {@code List<E>}.
@@ -545,7 +545,7 @@ interface ResponseSpec {
545545
/**
546546
* Variant of {@link #expectBodyList(Class)} for element types with generics.
547547
*/
548-
<E> ListBodySpec<E> expectBodyList(ResolvableType elementType);
548+
<E> ListBodySpec<E> expectBodyList(ParameterizedTypeReference<E> elementType);
549549

550550
/**
551551
* Declare expectations on the response body content.
@@ -565,7 +565,7 @@ interface ResponseSpec {
565565
/**
566566
* Variant of {@link #returnResult(Class)} for element types with generics.
567567
*/
568-
<T> FluxExchangeResult<T> returnResult(ResolvableType elementType);
568+
<T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementType);
569569
}
570570

571571
/**

spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import reactor.core.publisher.Flux;
2727
import reactor.test.StepVerifier;
2828

29+
import org.springframework.core.ParameterizedTypeReference;
2930
import org.springframework.http.MediaType;
3031
import org.springframework.http.ResponseEntity;
3132
import org.springframework.test.web.reactive.server.FluxExchangeResult;
@@ -39,9 +40,7 @@
3940

4041
import static java.time.Duration.ofMillis;
4142
import static org.hamcrest.CoreMatchers.endsWith;
42-
import static org.junit.Assert.assertEquals;
43-
import static org.junit.Assert.assertThat;
44-
import static org.springframework.core.ResolvableType.forClassWithGenerics;
43+
import static org.junit.Assert.*;
4544
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
4645

4746
/**
@@ -98,7 +97,7 @@ public void entityMap() throws Exception {
9897
this.client.get().uri("/persons?map=true")
9998
.exchange()
10099
.expectStatus().isOk()
101-
.expectBody(forClassWithGenerics(Map.class, String.class, Person.class)).isEqualTo(map);
100+
.expectBody(new ParameterizedTypeReference<Map<String, Person>>() {}).isEqualTo(map);
102101
}
103102

104103
@Test

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

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import reactor.core.publisher.Flux;
2626
import reactor.core.publisher.Mono;
2727

28+
import org.springframework.core.ParameterizedTypeReference;
2829
import org.springframework.core.ResolvableType;
2930
import org.springframework.core.io.buffer.DataBuffer;
3031
import org.springframework.http.HttpMessage;
@@ -68,11 +69,25 @@ public static <T> BodyExtractor<Mono<T>, ReactiveHttpInputMessage> toMono(Class<
6869

6970
/**
7071
* Return a {@code BodyExtractor} that reads into a Reactor {@link Mono}.
71-
* @param elementType the type of element in the {@code Mono}
72+
* The given {@link ParameterizedTypeReference} is used to pass generic type information, for
73+
* instance when using the {@link org.springframework.web.reactive.function.client.WebClient WebClient}
74+
* <pre class="code">
75+
* Mono&lt;Map&lt;String, String&gt;&gt; body = this.webClient
76+
* .get()
77+
* .uri("http://example.com")
78+
* .exchange()
79+
* .flatMap(r -> r.body(toMono(new ParameterizedTypeReference&lt;Map&lt;String,String&gt;&gt;() {})));
80+
* </pre>
81+
* @param typeReference a reference to the type of element in the {@code Mono}
7282
* @param <T> the element type
7383
* @return a {@code BodyExtractor} that reads a mono
7484
*/
75-
public static <T> BodyExtractor<Mono<T>, ReactiveHttpInputMessage> toMono(ResolvableType elementType) {
85+
public static <T> BodyExtractor<Mono<T>, ReactiveHttpInputMessage> toMono(ParameterizedTypeReference<T> typeReference) {
86+
Assert.notNull(typeReference, "'typeReference' must not be null");
87+
return toMono(ResolvableType.forType(typeReference.getType()));
88+
}
89+
90+
static <T> BodyExtractor<Mono<T>, ReactiveHttpInputMessage> toMono(ResolvableType elementType) {
7691
Assert.notNull(elementType, "'elementType' must not be null");
7792
return (inputMessage, context) -> readWithMessageReaders(inputMessage, context,
7893
elementType,
@@ -93,7 +108,7 @@ public static <T> BodyExtractor<Mono<T>, ReactiveHttpInputMessage> toMono(Resolv
93108
* Return a {@code BodyExtractor} that reads into a Reactor {@link Flux}.
94109
* @param elementClass the class of element in the {@code Flux}
95110
* @param <T> the element type
96-
* @return a {@code BodyExtractor} that reads a mono
111+
* @return a {@code BodyExtractor} that reads a flux
97112
*/
98113
public static <T> BodyExtractor<Flux<T>, ReactiveHttpInputMessage> toFlux(Class<? extends T> elementClass) {
99114
Assert.notNull(elementClass, "'elementClass' must not be null");
@@ -102,11 +117,25 @@ public static <T> BodyExtractor<Flux<T>, ReactiveHttpInputMessage> toFlux(Class<
102117

103118
/**
104119
* Return a {@code BodyExtractor} that reads into a Reactor {@link Flux}.
105-
* @param elementType the type of element in the {@code Flux}
120+
* The given {@link ParameterizedTypeReference} is used to pass generic type information, for
121+
* instance when using the {@link org.springframework.web.reactive.function.client.WebClient WebClient}
122+
* <pre class="code">
123+
* Flux&lt;ServerSentEvent&lt;String&gt;&gt; body = this.webClient
124+
* .get()
125+
* .uri("http://example.com")
126+
* .exchange()
127+
* .flatMap(r -> r.body(toFlux(new ParameterizedTypeReference&lt;ServerSentEvent&lt;String&gt;&gt;() {})));
128+
* </pre>
129+
* @param typeReference a reference to the type of element in the {@code Flux}
106130
* @param <T> the element type
107-
* @return a {@code BodyExtractor} that reads a mono
131+
* @return a {@code BodyExtractor} that reads a flux
108132
*/
109-
public static <T> BodyExtractor<Flux<T>, ReactiveHttpInputMessage> toFlux(ResolvableType elementType) {
133+
public static <T> BodyExtractor<Flux<T>, ReactiveHttpInputMessage> toFlux(ParameterizedTypeReference<T> typeReference) {
134+
Assert.notNull(typeReference, "'typeReference' must not be null");
135+
return toFlux(ResolvableType.forType(typeReference.getType()));
136+
}
137+
138+
static <T> BodyExtractor<Flux<T>, ReactiveHttpInputMessage> toFlux(ResolvableType elementType) {
110139
Assert.notNull(elementType, "'elementType' must not be null");
111140
return (inputMessage, context) -> readWithMessageReaders(inputMessage, context,
112141
elementType,

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.reactivestreams.Publisher;
2424
import reactor.core.publisher.Mono;
2525

26+
import org.springframework.core.ParameterizedTypeReference;
2627
import org.springframework.core.ResolvableType;
2728
import org.springframework.core.io.Resource;
2829
import org.springframework.core.io.buffer.DataBuffer;
@@ -100,17 +101,17 @@ public static <T, P extends Publisher<T>> BodyInserter<P, ReactiveHttpOutputMess
100101
/**
101102
* Return a {@code BodyInserter} that writes the given {@link Publisher}.
102103
* @param publisher the publisher to stream to the response body
103-
* @param elementType the type of elements contained in the publisher
104+
* @param typeReference the type of elements contained in the publisher
104105
* @param <T> the type of the elements contained in the publisher
105106
* @param <P> the type of the {@code Publisher}
106107
* @return a {@code BodyInserter} that writes a {@code Publisher}
107108
*/
108109
public static <T, P extends Publisher<T>> BodyInserter<P, ReactiveHttpOutputMessage> fromPublisher(
109-
P publisher, ResolvableType elementType) {
110+
P publisher, ParameterizedTypeReference<T> typeReference) {
110111

111112
Assert.notNull(publisher, "'publisher' must not be null");
112-
Assert.notNull(elementType, "'elementType' must not be null");
113-
return bodyInserterFor(publisher, elementType);
113+
Assert.notNull(typeReference, "'typeReference' must not be null");
114+
return bodyInserterFor(publisher, ResolvableType.forType(typeReference.getType()));
114115
}
115116

116117
/**
@@ -197,7 +198,7 @@ public static <T, S extends Publisher<T>> BodyInserter<S, ServerHttpResponse> fr
197198
* Return a {@code BodyInserter} that writes the given {@code Publisher} publisher as
198199
* Server-Sent Events.
199200
* @param eventsPublisher the publisher to write to the response body as Server-Sent Events
200-
* @param eventType the type of event contained in the publisher
201+
* @param typeReference the type of event contained in the publisher
201202
* @param <T> the type of the elements contained in the publisher
202203
* @return a {@code BodyInserter} that writes the given {@code Publisher} publisher as
203204
* Server-Sent Events
@@ -207,6 +208,15 @@ public static <T, S extends Publisher<T>> BodyInserter<S, ServerHttpResponse> fr
207208
// ReactiveHttpOutputMessage like other methods, since sending SSEs only typically happens on
208209
// the server-side
209210
public static <T, S extends Publisher<T>> BodyInserter<S, ServerHttpResponse> fromServerSentEvents(S eventsPublisher,
211+
ParameterizedTypeReference<T> typeReference) {
212+
213+
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");
214+
Assert.notNull(typeReference, "'typeReference' must not be null");
215+
return fromServerSentEvents(eventsPublisher,
216+
ResolvableType.forType(typeReference.getType()));
217+
}
218+
219+
static <T, S extends Publisher<T>> BodyInserter<S, ServerHttpResponse> fromServerSentEvents(S eventsPublisher,
210220
ResolvableType eventType) {
211221

212222
Assert.notNull(eventsPublisher, "'eventsPublisher' must not be null");

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import org.reactivestreams.Publisher;
2424
import reactor.core.publisher.Mono;
2525

26-
import org.springframework.core.ResolvableType;
26+
import org.springframework.core.ParameterizedTypeReference;
2727
import org.springframework.http.CacheControl;
2828
import org.springframework.http.HttpHeaders;
2929
import org.springframework.http.HttpMethod;
@@ -81,14 +81,14 @@ static <T, P extends Publisher<T>> Builder<P> fromPublisher(P publisher, Class<T
8181
/**
8282
* Create a builder with the given publisher.
8383
* @param publisher the publisher that represents the body of the response
84-
* @param elementType the type of elements contained in the publisher
84+
* @param typeReference the type of elements contained in the publisher
8585
* @param <T> the type of the elements contained in the publisher
8686
* @param <P> the type of the {@code Publisher}
8787
* @return the created builder
8888
*/
89-
static <T, P extends Publisher<T>> Builder<P> fromPublisher(P publisher, ResolvableType elementType) {
89+
static <T, P extends Publisher<T>> Builder<P> fromPublisher(P publisher, ParameterizedTypeReference<T> typeReference) {
9090
return new DefaultEntityResponseBuilder<>(publisher,
91-
BodyInserters.fromPublisher(publisher, elementType));
91+
BodyInserters.fromPublisher(publisher, typeReference));
9292
}
9393

9494

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.ArrayList;
2222
import java.util.Collections;
2323
import java.util.HashMap;
24+
import java.util.LinkedHashMap;
2425
import java.util.List;
2526
import java.util.Map;
2627
import java.util.Optional;
@@ -32,6 +33,7 @@
3233
import reactor.core.publisher.Mono;
3334
import reactor.test.StepVerifier;
3435

36+
import org.springframework.core.ParameterizedTypeReference;
3537
import org.springframework.core.codec.ByteBufferDecoder;
3638
import org.springframework.core.codec.StringDecoder;
3739
import org.springframework.core.io.buffer.DataBuffer;
@@ -120,6 +122,28 @@ public void toMono() throws Exception {
120122
.verify();
121123
}
122124

125+
@Test
126+
public void toMonoParameterizedTypeReference() throws Exception {
127+
ParameterizedTypeReference<Map<String, String>> typeReference = new ParameterizedTypeReference<Map<String, String>>() {};
128+
BodyExtractor<Mono<Map<String, String>>, ReactiveHttpInputMessage> extractor = BodyExtractors.toMono(typeReference);
129+
130+
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
131+
DefaultDataBuffer dataBuffer =
132+
factory.wrap(ByteBuffer.wrap("{\"username\":\"foo\",\"password\":\"bar\"}".getBytes(StandardCharsets.UTF_8)));
133+
Flux<DataBuffer> body = Flux.just(dataBuffer);
134+
135+
MockServerHttpRequest request = MockServerHttpRequest.post("/").contentType(MediaType.APPLICATION_JSON).body(body);
136+
Mono<Map<String, String>> result = extractor.extract(request, this.context);
137+
138+
Map<String, String > expected = new LinkedHashMap<>();
139+
expected.put("username", "foo");
140+
expected.put("password", "bar");
141+
StepVerifier.create(result)
142+
.expectNext(expected)
143+
.expectComplete()
144+
.verify();
145+
}
146+
123147
@Test
124148
public void toMonoWithHints() throws Exception {
125149
BodyExtractor<Mono<User>, ReactiveHttpInputMessage> extractor = BodyExtractors.toMono(User.class);

spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import reactor.core.publisher.Mono;
3131
import reactor.test.StepVerifier;
3232

33-
import org.springframework.core.ResolvableType;
33+
import org.springframework.core.ParameterizedTypeReference;
3434
import org.springframework.core.codec.CharSequenceEncoder;
3535
import org.springframework.core.io.buffer.DataBuffer;
3636
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
@@ -70,10 +70,10 @@ public void fromPublisherClass() throws Exception {
7070
}
7171

7272
@Test
73-
public void fromPublisherResolvableType() throws Exception {
73+
public void fromPublisher() throws Exception {
7474
Flux<String> body = Flux.just("foo", "bar");
75-
ResolvableType type = ResolvableType.forClass(String.class);
76-
EntityResponse<Flux<String>> response = EntityResponse.fromPublisher(body, type).build().block();
75+
ParameterizedTypeReference<String> typeReference = new ParameterizedTypeReference<String>() {};
76+
EntityResponse<Flux<String>> response = EntityResponse.fromPublisher(body, typeReference).build().block();
7777
assertSame(body, response.entity());
7878
}
7979

0 commit comments

Comments
 (0)