From 2f96ac6a5786a86d8273cd6598b61193a3f9c65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 29 Apr 2024 14:54:46 +0200 Subject: [PATCH] Add BodyInserters.fromValue(T, ParameterizedTypeReference) This commit introduces BodyInserters.fromValue(T, ParameterizedTypeReference) variant as well as related WebClient.RequestBodySpec API, ServerResponse.BodyBuilder API and Kotlin extensions. Closes gh-32713 --- .../web/reactive/function/BodyInserters.java | 28 ++++++- .../function/client/DefaultWebClient.java | 6 ++ .../reactive/function/client/WebClient.java | 28 +++++++ .../server/DefaultServerResponseBuilder.java | 7 +- .../function/server/ServerResponse.java | 16 +++- .../function/client/WebClientExtensions.kt | 14 +++- .../server/ServerResponseExtensions.kt | 18 ++++- .../function/KotlinBodyInsertersTests.kt | 81 +++++++++++++++++++ .../client/WebClientExtensionsTests.kt | 9 ++- .../server/ServerResponseExtensionsTests.kt | 15 +++- 10 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/KotlinBodyInsertersTests.kt diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java index 35d9a1be2a59..96fac2afbf04 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -103,6 +103,32 @@ public static BodyInserter fromValue(T body) { writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body), null); } + /** + * Inserter to write the given value. + *

Alternatively, consider using the {@code bodyValue(Object, ParameterizedTypeReference)} shortcuts on + * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and + * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. + * @param body the value to write + * @param bodyType the type of the body, used to capture the generic type + * @param the type of the body + * @return the inserter to write a single value + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #fromPublisher(Publisher, ParameterizedTypeReference)} or + * {@link #fromProducer(Object, ParameterizedTypeReference)} should be used. + * @since 6.2 + * @see #fromPublisher(Publisher, ParameterizedTypeReference) + * @see #fromProducer(Object, ParameterizedTypeReference) + */ + public static BodyInserter fromValue(T body, ParameterizedTypeReference bodyType) { + Assert.notNull(body, "'body' must not be null"); + Assert.notNull(bodyType, "'bodyType' must not be null"); + Assert.isNull(registry.getAdapter(body.getClass()), + "'body' should be an object, for reactive types use a variant specifying a publisher/producer and its related element type"); + return (message, context) -> + writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forType(bodyType), null); + } + /** * Inserter to write the given object. *

Alternatively, consider using the {@code bodyValue(Object)} shortcuts on 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 6308ddf5a9fc..01ea08c5098c 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 @@ -367,6 +367,12 @@ public RequestHeadersSpec bodyValue(Object body) { return this; } + @Override + public RequestHeadersSpec bodyValue(T body, ParameterizedTypeReference bodyType) { + this.inserter = BodyInserters.fromValue(body, bodyType); + return this; + } + @Override public > RequestHeadersSpec body( P publisher, ParameterizedTypeReference elementTypeRef) { 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 02586478e33c..7c164b135802 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 @@ -692,9 +692,37 @@ interface RequestBodySpec extends RequestHeadersSpec { * @throws IllegalArgumentException if {@code body} is a * {@link Publisher} or producer known to {@link ReactiveAdapterRegistry} * @since 5.2 + * @see #bodyValue(Object, ParameterizedTypeReference) */ RequestHeadersSpec bodyValue(Object body); + /** + * Shortcut for {@link #body(BodyInserter)} with a + * {@linkplain BodyInserters#fromValue value inserter}. + * For example: + *

+		 * List<Person> list = ... ;
+		 *
+		 * Mono<Void> result = client.post()
+		 *     .uri("/persons/{id}", id)
+		 *     .contentType(MediaType.APPLICATION_JSON)
+		 *     .bodyValue(list, new ParameterizedTypeReference<List<Person>>() {};)
+		 *     .retrieve()
+		 *     .bodyToMono(Void.class);
+		 * 
+ *

For multipart requests consider providing + * {@link org.springframework.util.MultiValueMap MultiValueMap} prepared + * with {@link org.springframework.http.client.MultipartBodyBuilder + * MultipartBodyBuilder}. + * @param body the value to write to the request body + * @param bodyType the type of the body + * @return this builder + * @throws IllegalArgumentException if {@code body} is a + * {@link Publisher} or producer known to {@link ReactiveAdapterRegistry} + * @since 6.2 + */ + RequestHeadersSpec bodyValue(T body, ParameterizedTypeReference bodyType); + /** * Shortcut for {@link #body(BodyInserter)} with a * {@linkplain BodyInserters#fromPublisher Publisher inserter}. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java index 701854fe06a8..9f0df3b736de 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -225,6 +225,11 @@ public Mono bodyValue(Object body) { return initBuilder(body, BodyInserters.fromValue(body)); } + @Override + public Mono bodyValue(T body, ParameterizedTypeReference bodyType) { + return initBuilder(body, BodyInserters.fromValue(body, bodyType)); + } + @Override public > Mono body(P publisher, Class elementClass) { return initBuilder(publisher, BodyInserters.fromPublisher(publisher, elementClass)); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java index 2bb5c9454b00..f10d770ad2ff 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -420,6 +420,20 @@ interface BodyBuilder extends HeadersBuilder { */ Mono bodyValue(Object body); + /** + * Set the body of the response to the given {@code Object} and return it. + * This is a shortcut for using a {@link #body(BodyInserter)} with a + * {@linkplain BodyInserters#fromValue value inserter}. + * @param body the body of the response + * @param bodyType the type of the body, used to capture the generic type + * @param the type of the body + * @return the built response + * @throws IllegalArgumentException if {@code body} is a + * {@link Publisher} or producer known to {@link ReactiveAdapterRegistry} + * @since 6.2 + */ + Mono bodyValue(T body, ParameterizedTypeReference bodyType); + /** * Set the body from the given {@code Publisher}. Shortcut for * {@link #body(BodyInserter)} with a diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt index ce917db7d91f..375eb16a1e9d 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,6 +68,18 @@ inline fun RequestBodySpec.body(flow: Flow): RequestHeaders inline fun RequestBodySpec.body(producer: Any): RequestHeadersSpec<*> = body(producer, object : ParameterizedTypeReference() {}) +/** + * Extension for [WebClient.RequestBodySpec.bodyValue] providing a `bodyValueWithType(Any)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param body the value to write to the request body + * @param T the type of the body + * @author Sebastien Deleuze + * @since 6.2 + */ +inline fun RequestBodySpec.bodyValueWithType(body: T): RequestHeadersSpec<*> = + bodyValue(body, object : ParameterizedTypeReference() {}) + /** * Coroutines variant of [WebClient.RequestHeadersSpec.exchange]. * diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt index 4411105ce279..b3d6a9a15c4f 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt @@ -51,9 +51,6 @@ inline fun ServerResponse.BodyBuilder.body(producer: Any): Mon /** * Coroutines variant of [ServerResponse.BodyBuilder.bodyValue]. * - * Set the body of the response to the given {@code Object} and return it. - * This convenience method combines [body] and - * [org.springframework.web.reactive.function.BodyInserters.fromValue]. * @param body the body of the response * @return the built response * @throws IllegalArgumentException if `body` is a [Publisher] or an @@ -62,6 +59,21 @@ inline fun ServerResponse.BodyBuilder.body(producer: Any): Mon suspend fun ServerResponse.BodyBuilder.bodyValueAndAwait(body: Any): ServerResponse = bodyValue(body).awaitSingle() +/** + * Coroutines variant of [ServerResponse.BodyBuilder.bodyValue] providing a `bodyValueWithTypeAndAwait(Any)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * + * @param body the body of the response + * @param T the type of the body + * @return the built response + * @throws IllegalArgumentException if `body` is a [Publisher] or an + * instance of a type supported by [org.springframework.core.ReactiveAdapterRegistry.getSharedInstance], + * @since 6.2 + */ +suspend inline fun ServerResponse.BodyBuilder.bodyValueWithTypeAndAwait(body: T): ServerResponse = + bodyValue(body, object : ParameterizedTypeReference() {}).awaitSingle() + /** * Coroutines variant of [ServerResponse.BodyBuilder.body] with [Any] and * [ParameterizedTypeReference] parameters providing a `bodyAndAwait(Flow)` variant. diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/KotlinBodyInsertersTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/KotlinBodyInsertersTests.kt new file mode 100644 index 000000000000..d0bc01ceeef2 --- /dev/null +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/KotlinBodyInsertersTests.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.function + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.codec.EncoderHttpMessageWriter +import org.springframework.http.codec.HttpMessageWriter +import org.springframework.http.codec.json.KotlinSerializationJsonEncoder +import org.springframework.http.server.reactive.ServerHttpRequest +import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse +import reactor.test.StepVerifier +import java.util.* + +/** + * @author Sebastien Deleuze + */ +class KotlinBodyInsertersTests { + + private lateinit var context: BodyInserter.Context + + private lateinit var hints: Map + + + @BeforeEach + fun createContext() { + val messageWriters: MutableList> = ArrayList() + val jsonEncoder = KotlinSerializationJsonEncoder() + messageWriters.add(EncoderHttpMessageWriter(jsonEncoder)) + + this.context = object : BodyInserter.Context { + override fun messageWriters(): List> { + return messageWriters + } + + override fun serverRequest(): Optional { + return Optional.empty() + } + + override fun hints(): Map { + return hints + } + } + this.hints = HashMap() + } + + @Test + fun ofObjectWithBodyType() { + val somebody = SomeBody(1, "name") + val body = listOf(somebody) + val inserter = BodyInserters.fromValue(body, object: ParameterizedTypeReference>() {}) + val response = MockServerHttpResponse() + val result = inserter.insert(response, context) + StepVerifier.create(result).expectComplete().verify() + + StepVerifier.create(response.bodyAsString) + .expectNext("[{\"user_id\":1,\"name\":\"name\"}]") + .expectComplete() + .verify() + } + + @Serializable + data class SomeBody(@SerialName("user_id") val userId: Int, val name: String) +} diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt index 7bca596a0109..9d6ab8e1185a 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,6 +66,13 @@ class WebClientExtensionsTests { verify { requestBodySpec.body(ofType(), object : ParameterizedTypeReference>() {}) } } + @Test + fun `RequestBodySpec#bodyValueWithType with reified type parameters`() { + val body = mockk>() + requestBodySpec.bodyValueWithType>(body) + verify { requestBodySpec.bodyValue(body, object : ParameterizedTypeReference>() {}) } + } + @Test fun `ResponseSpec#bodyToMono with reified type parameters`() { responseSpec.bodyToMono>() diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt index a4455cf1c11e..7a9648b83e01 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,6 +75,19 @@ class ServerResponseExtensionsTests { } } + @Test + fun `BodyBuilder#bodyValueWithTypeAndAwait with object parameter and reified type parameters`() { + val response = mockk() + val body = listOf("foo", "bar") + every { bodyBuilder.bodyValue(ofType>(), object : ParameterizedTypeReference>() {}) } returns Mono.just(response) + runBlocking { + bodyBuilder.bodyValueWithTypeAndAwait>(body) + } + verify { + bodyBuilder.bodyValue(body, object : ParameterizedTypeReference>() {}) + } + } + @Test fun `BodyBuilder#bodyAndAwait with flow parameter`() { val response = mockk()