Skip to content

Commit 331bdb0

Browse files
committed
Add BodyInserters.fromValue(T, ParameterizedTypeReference<T>)
This commit introduces BodyInserters.fromValue(T, ParameterizedTypeReference<T>) variant as well as related WebClient.RequestBodySpec API, ServerResponse.BodyBuilder API and Kotlin extensions. Closes gh-32713
1 parent 9492d88 commit 331bdb0

File tree

10 files changed

+214
-9
lines changed

10 files changed

+214
-9
lines changed

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

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -103,6 +103,32 @@ public static <T> BodyInserter<T, ReactiveHttpOutputMessage> fromValue(T body) {
103103
writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body), null);
104104
}
105105

106+
/**
107+
* Inserter to write the given value.
108+
* <p>Alternatively, consider using the {@code bodyValue(Object, ParameterizedTypeReference)} shortcuts on
109+
* {@link org.springframework.web.reactive.function.client.WebClient WebClient} and
110+
* {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}.
111+
* @param body the value to write
112+
* @param bodyType the type of the body, used to capture the generic type
113+
* @param <T> the type of the body
114+
* @return the inserter to write a single value
115+
* @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an
116+
* instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()},
117+
* for which {@link #fromPublisher(Publisher, ParameterizedTypeReference)} or
118+
* {@link #fromProducer(Object, ParameterizedTypeReference)} should be used.
119+
* @since 6.2
120+
* @see #fromPublisher(Publisher, ParameterizedTypeReference)
121+
* @see #fromProducer(Object, ParameterizedTypeReference)
122+
*/
123+
public static <T> BodyInserter<T, ReactiveHttpOutputMessage> fromValue(T body, ParameterizedTypeReference<T> bodyType) {
124+
Assert.notNull(body, "'body' must not be null");
125+
Assert.notNull(bodyType, "'bodyType' must not be null");
126+
Assert.isNull(registry.getAdapter(body.getClass()),
127+
"'body' should be an object, for reactive types use a variant specifying a publisher/producer and its related element type");
128+
return (message, context) ->
129+
writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forType(bodyType), null);
130+
}
131+
106132
/**
107133
* Inserter to write the given object.
108134
* <p>Alternatively, consider using the {@code bodyValue(Object)} shortcuts on

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

+6
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,12 @@ public RequestHeadersSpec<?> bodyValue(Object body) {
367367
return this;
368368
}
369369

370+
@Override
371+
public <T> RequestHeadersSpec<?> bodyValue(T body, ParameterizedTypeReference<T> bodyType) {
372+
this.inserter = BodyInserters.fromValue(body, bodyType);
373+
return this;
374+
}
375+
370376
@Override
371377
public <T, P extends Publisher<T>> RequestHeadersSpec<?> body(
372378
P publisher, ParameterizedTypeReference<T> elementTypeRef) {

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

+29
Original file line numberDiff line numberDiff line change
@@ -692,9 +692,38 @@ interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
692692
* @throws IllegalArgumentException if {@code body} is a
693693
* {@link Publisher} or producer known to {@link ReactiveAdapterRegistry}
694694
* @since 5.2
695+
* @see #bodyValue(Object, ParameterizedTypeReference)
695696
*/
696697
RequestHeadersSpec<?> bodyValue(Object body);
697698

699+
/**
700+
* Shortcut for {@link #body(BodyInserter)} with a
701+
* {@linkplain BodyInserters#fromValue value inserter}.
702+
* For example:
703+
* <p><pre class="code">
704+
* List&lt;Person&gt; list = ... ;
705+
*
706+
* Mono&lt;Void&gt; result = client.post()
707+
* .uri("/persons/{id}", id)
708+
* .contentType(MediaType.APPLICATION_JSON)
709+
* .bodyValue(list, new ParameterizedTypeReference&lt;List&lt;Person&gt;&gt;() {};)
710+
* .retrieve()
711+
* .bodyToMono(Void.class);
712+
* </pre>
713+
* <p>For multipart requests consider providing
714+
* {@link org.springframework.util.MultiValueMap MultiValueMap} prepared
715+
* with {@link org.springframework.http.client.MultipartBodyBuilder
716+
* MultipartBodyBuilder}.
717+
* @param body the value to write to the request body
718+
* @param bodyType the type of the body, used to capture the generic type
719+
* @param <T> the type of the body
720+
* @return this builder
721+
* @throws IllegalArgumentException if {@code body} is a
722+
* {@link Publisher} or producer known to {@link ReactiveAdapterRegistry}
723+
* @since 6.2
724+
*/
725+
<T> RequestHeadersSpec<?> bodyValue(T body, ParameterizedTypeReference<T> bodyType);
726+
698727
/**
699728
* Shortcut for {@link #body(BodyInserter)} with a
700729
* {@linkplain BodyInserters#fromPublisher Publisher inserter}.

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -225,6 +225,11 @@ public Mono<ServerResponse> bodyValue(Object body) {
225225
return initBuilder(body, BodyInserters.fromValue(body));
226226
}
227227

228+
@Override
229+
public <T> Mono<ServerResponse> bodyValue(T body, ParameterizedTypeReference<T> bodyType) {
230+
return initBuilder(body, BodyInserters.fromValue(body, bodyType));
231+
}
232+
228233
@Override
229234
public <T, P extends Publisher<T>> Mono<ServerResponse> body(P publisher, Class<T> elementClass) {
230235
return initBuilder(publisher, BodyInserters.fromPublisher(publisher, elementClass));

Diff for: spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -420,6 +420,20 @@ interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
420420
*/
421421
Mono<ServerResponse> bodyValue(Object body);
422422

423+
/**
424+
* Set the body of the response to the given {@code Object} and return it.
425+
* This is a shortcut for using a {@link #body(BodyInserter)} with a
426+
* {@linkplain BodyInserters#fromValue value inserter}.
427+
* @param body the body of the response
428+
* @param bodyType the type of the body, used to capture the generic type
429+
* @param <T> the type of the body
430+
* @return the built response
431+
* @throws IllegalArgumentException if {@code body} is a
432+
* {@link Publisher} or producer known to {@link ReactiveAdapterRegistry}
433+
* @since 6.2
434+
*/
435+
<T> Mono<ServerResponse> bodyValue(T body, ParameterizedTypeReference<T> bodyType);
436+
423437
/**
424438
* Set the body from the given {@code Publisher}. Shortcut for
425439
* {@link #body(BodyInserter)} with a

Diff for: spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -68,6 +68,18 @@ inline fun <reified T : Any> RequestBodySpec.body(flow: Flow<T>): RequestHeaders
6868
inline fun <reified T : Any> RequestBodySpec.body(producer: Any): RequestHeadersSpec<*> =
6969
body(producer, object : ParameterizedTypeReference<T>() {})
7070

71+
/**
72+
* Extension for [WebClient.RequestBodySpec.bodyValue] providing a `bodyValueWithType<T>(Any)` variant
73+
* leveraging Kotlin reified type parameters. This extension is not subject to type
74+
* erasure and retains actual generic type arguments.
75+
* @param body the value to write to the request body
76+
* @param T the type of the body
77+
* @author Sebastien Deleuze
78+
* @since 6.2
79+
*/
80+
inline fun <reified T : Any> RequestBodySpec.bodyValueWithType(body: T): RequestHeadersSpec<*> =
81+
bodyValue(body, object : ParameterizedTypeReference<T>() {})
82+
7183
/**
7284
* Coroutines variant of [WebClient.RequestHeadersSpec.exchange].
7385
*

Diff for: spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt

+15-3
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,6 @@ inline fun <reified T : Any> ServerResponse.BodyBuilder.body(producer: Any): Mon
5151
/**
5252
* Coroutines variant of [ServerResponse.BodyBuilder.bodyValue].
5353
*
54-
* Set the body of the response to the given {@code Object} and return it.
55-
* This convenience method combines [body] and
56-
* [org.springframework.web.reactive.function.BodyInserters.fromValue].
5754
* @param body the body of the response
5855
* @return the built response
5956
* @throws IllegalArgumentException if `body` is a [Publisher] or an
@@ -62,6 +59,21 @@ inline fun <reified T : Any> ServerResponse.BodyBuilder.body(producer: Any): Mon
6259
suspend fun ServerResponse.BodyBuilder.bodyValueAndAwait(body: Any): ServerResponse =
6360
bodyValue(body).awaitSingle()
6461

62+
/**
63+
* Coroutines variant of [ServerResponse.BodyBuilder.bodyValue] providing a `bodyValueWithTypeAndAwait<T>(Any)` variant
64+
* leveraging Kotlin reified type parameters. This extension is not subject to type
65+
* erasure and retains actual generic type arguments.
66+
*
67+
* @param body the body of the response
68+
* @param T the type of the body
69+
* @return the built response
70+
* @throws IllegalArgumentException if `body` is a [Publisher] or an
71+
* instance of a type supported by [org.springframework.core.ReactiveAdapterRegistry.getSharedInstance],
72+
* @since 6.2
73+
*/
74+
suspend inline fun <reified T: Any> ServerResponse.BodyBuilder.bodyValueWithTypeAndAwait(body: T): ServerResponse =
75+
bodyValue(body, object : ParameterizedTypeReference<T>() {}).awaitSingle()
76+
6577
/**
6678
* Coroutines variant of [ServerResponse.BodyBuilder.body] with [Any] and
6779
* [ParameterizedTypeReference] parameters providing a `bodyAndAwait(Flow<T>)` variant.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.reactive.function
18+
19+
import kotlinx.serialization.SerialName
20+
import kotlinx.serialization.Serializable
21+
import org.junit.jupiter.api.BeforeEach
22+
import org.junit.jupiter.api.Test
23+
import org.springframework.core.ParameterizedTypeReference
24+
import org.springframework.http.codec.EncoderHttpMessageWriter
25+
import org.springframework.http.codec.HttpMessageWriter
26+
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
27+
import org.springframework.http.server.reactive.ServerHttpRequest
28+
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse
29+
import reactor.test.StepVerifier
30+
import java.util.*
31+
32+
/**
33+
* @author Sebastien Deleuze
34+
*/
35+
class KotlinBodyInsertersTests {
36+
37+
private lateinit var context: BodyInserter.Context
38+
39+
private lateinit var hints: Map<String, Any>
40+
41+
42+
@BeforeEach
43+
fun createContext() {
44+
val messageWriters: MutableList<HttpMessageWriter<*>> = ArrayList()
45+
val jsonEncoder = KotlinSerializationJsonEncoder()
46+
messageWriters.add(EncoderHttpMessageWriter(jsonEncoder))
47+
48+
this.context = object : BodyInserter.Context {
49+
override fun messageWriters(): List<HttpMessageWriter<*>> {
50+
return messageWriters
51+
}
52+
53+
override fun serverRequest(): Optional<ServerHttpRequest> {
54+
return Optional.empty()
55+
}
56+
57+
override fun hints(): Map<String, Any> {
58+
return hints
59+
}
60+
}
61+
this.hints = HashMap()
62+
}
63+
64+
@Test
65+
fun ofObjectWithBodyType() {
66+
val somebody = SomeBody(1, "name")
67+
val body = listOf(somebody)
68+
val inserter = BodyInserters.fromValue(body, object: ParameterizedTypeReference<List<SomeBody>>() {})
69+
val response = MockServerHttpResponse()
70+
val result = inserter.insert(response, context)
71+
StepVerifier.create(result).expectComplete().verify()
72+
73+
StepVerifier.create(response.bodyAsString)
74+
.expectNext("[{\"user_id\":1,\"name\":\"name\"}]")
75+
.expectComplete()
76+
.verify()
77+
}
78+
79+
@Serializable
80+
data class SomeBody(@SerialName("user_id") val userId: Int, val name: String)
81+
}

Diff for: spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -66,6 +66,13 @@ class WebClientExtensionsTests {
6666
verify { requestBodySpec.body(ofType<Any>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
6767
}
6868

69+
@Test
70+
fun `RequestBodySpec#bodyValueWithType with reified type parameters`() {
71+
val body = mockk<List<Foo>>()
72+
requestBodySpec.bodyValueWithType<List<Foo>>(body)
73+
verify { requestBodySpec.bodyValue(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
74+
}
75+
6976
@Test
7077
fun `ResponseSpec#bodyToMono with reified type parameters`() {
7178
responseSpec.bodyToMono<List<Foo>>()

Diff for: spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -75,6 +75,19 @@ class ServerResponseExtensionsTests {
7575
}
7676
}
7777

78+
@Test
79+
fun `BodyBuilder#bodyValueWithTypeAndAwait with object parameter and reified type parameters`() {
80+
val response = mockk<ServerResponse>()
81+
val body = listOf("foo", "bar")
82+
every { bodyBuilder.bodyValue(ofType<List<String>>(), object : ParameterizedTypeReference<List<String>>() {}) } returns Mono.just(response)
83+
runBlocking {
84+
bodyBuilder.bodyValueWithTypeAndAwait<List<String>>(body)
85+
}
86+
verify {
87+
bodyBuilder.bodyValue(body, object : ParameterizedTypeReference<List<String>>() {})
88+
}
89+
}
90+
7891
@Test
7992
fun `BodyBuilder#bodyAndAwait with flow parameter`() {
8093
val response = mockk<ServerResponse>()

0 commit comments

Comments
 (0)