From 2b4d6ce3548ddf97d1bce1a3832f480de9043fa7 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Sun, 7 Jul 2019 21:03:41 +0200 Subject: [PATCH] Add body methods with Object parameter to WebFlux The commit deprecates syncBody(Object) in favor of body(Object) which has the same behavior in ServerResponse, WebClient and WebTestClient. It also adds body(Object, Class) and body(Object, ParameterizedTypeReference) methods in order to support any reactive type that can be adapted to a Publisher via ReactiveAdapterRegistry. Related BodyInserters#fromProducer methods are provided as well. Shadowed Kotlin body() extensions are deprecated in favor of bodyWithType() ones, including dedicated Publisher and Flow variants. Coroutines extensions are adapted as well, and body(Object) can now be used with suspending functions. Closes gh-23212 --- spring-test/spring-test.gradle | 2 + .../reactive/server/DefaultWebTestClient.java | 34 +++++- .../web/reactive/server/WebTestClient.java | 101 +++++++++++++---- .../server/WebTestClientExtensions.kt | 60 ++++++++-- .../server/ApplicationContextSpecTests.java | 2 +- .../reactive/server/samples/ErrorTests.java | 2 +- .../server/samples/JsonContentTests.java | 2 +- .../server/samples/ResponseEntityTests.java | 2 +- .../server/samples/XmlContentTests.java | 2 +- .../server/samples/bind/HttpServerTests.java | 2 +- .../samples/bind/RouterFunctionTests.java | 2 +- .../server/WebTestClientExtensionsTests.kt | 39 +++++-- .../http/client/MultipartBodyBuilder.java | 4 +- .../web/reactive/function/BodyInserters.java | 97 ++++++++++++++--- .../function/client/DefaultWebClient.java | 34 ++++-- .../reactive/function/client/WebClient.java | 97 +++++++++++++++-- .../server/DefaultServerResponseBuilder.java | 62 +++++++---- .../function/server/EntityResponse.java | 34 +++++- .../function/server/ServerResponse.java | 63 +++++++++-- .../function/client/WebClientExtensions.kt | 74 ++++++++----- .../server/ServerResponseExtensions.kt | 103 ++++++++++++------ .../reactive/function/BodyInsertersTests.java | 46 ++++++++ .../function/MultipartIntegrationTests.java | 8 +- .../client/DefaultWebClientTests.java | 2 +- .../client/WebClientIntegrationTests.java | 2 +- .../DefaultEntityResponseBuilderTests.java | 9 ++ .../DefaultServerResponseBuilderTests.java | 8 +- .../InvalidHttpMethodIntegrationTests.java | 4 +- .../server/NestedRouteIntegrationTests.java | 2 +- .../annotation/MultipartIntegrationTests.java | 14 +-- .../client/WebClientExtensionsTests.kt | 30 +++-- .../server/ServerResponseExtensionsTests.kt | 95 +++++++++------- src/docs/asciidoc/web/webflux-webclient.adoc | 13 ++- 33 files changed, 781 insertions(+), 270 deletions(-) diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index f46320cf9e..d1e81279ba 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -53,6 +53,8 @@ dependencies { optional("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") optional("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") optional("io.projectreactor:reactor-test") + optional("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}") + optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}") testCompile(project(":spring-context-support")) testCompile(project(":spring-oxm")) testCompile("javax.annotation:javax.annotation-api:1.3.2") diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index a382181e42..3d03e6e5ff 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -261,8 +261,20 @@ class DefaultWebTestClient implements WebTestClient { } @Override - public RequestHeadersSpec body(BodyInserter inserter) { - this.bodySpec.body(inserter); + public RequestHeadersSpec body(Object body) { + this.bodySpec.body(body); + return this; + } + + @Override + public RequestHeadersSpec body(Object producer, Class elementClass) { + this.bodySpec.body(producer, elementClass); + return this; + } + + @Override + public RequestHeadersSpec body(Object producer, ParameterizedTypeReference elementType) { + this.bodySpec.body(producer, elementType); return this; } @@ -273,11 +285,23 @@ class DefaultWebTestClient implements WebTestClient { } @Override - public RequestHeadersSpec syncBody(Object body) { - this.bodySpec.syncBody(body); + public > RequestHeadersSpec body(S publisher, ParameterizedTypeReference elementType) { + this.bodySpec.body(publisher, elementType); return this; } + @Override + public RequestHeadersSpec body(BodyInserter inserter) { + this.bodySpec.body(inserter); + return this; + } + + @Override + @Deprecated + public RequestHeadersSpec syncBody(Object body) { + return body(body); + } + @Override public ResponseSpec exchange() { ClientResponse clientResponse = this.bodySpec.exchange().block(getTimeout()); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index cc2e8ab96f..9180920bcf 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -30,6 +30,7 @@ import org.reactivestreams.Publisher; import org.springframework.context.ApplicationContext; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.format.FormatterRegistry; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -625,26 +626,7 @@ public interface WebTestClient { RequestBodySpec contentType(MediaType contentType); /** - * Set the body of the request to the given {@code BodyInserter}. - * @param inserter the inserter - * @return spec for decoding the response - * @see org.springframework.web.reactive.function.BodyInserters - */ - RequestHeadersSpec body(BodyInserter inserter); - - /** - * Set the body of the request to the given asynchronous {@code Publisher}. - * @param publisher the request body data - * @param elementClass the class of elements contained in the publisher - * @param the type of the elements contained in the publisher - * @param the type of the {@code Publisher} - * @return spec for decoding the response - */ - > RequestHeadersSpec body(S publisher, Class elementClass); - - /** - * Set the body of the request to the given synchronous {@code Object} and - * perform the request. + * Set the body of the request to the given {@code Object} and perform the request. *

This method is a convenient shortcut for: *

 		 * .body(BodyInserters.fromObject(object))
@@ -657,8 +639,83 @@ public interface WebTestClient {
 		 * part with body and headers. The {@code MultiValueMap} can be built
 		 * conveniently using
 		 * @param body the {@code Object} to write to the request
-		 * @return a {@code Mono} with the response
+		 * @return spec for decoding the response
+		 * @since 5.2
 		 */
+		RequestHeadersSpec body(Object body);
+
+		/**
+		 * Set the body of the request to the given producer.
+		 * @param producer the producer to write to the request. This must be a
+		 * {@link Publisher} or another producer adaptable to a
+		 * {@code Publisher} via {@link ReactiveAdapterRegistry}
+		 * @param elementClass the class of elements contained in the producer
+		 * @return spec for decoding the response
+		 * @since 5.2
+		 */
+		RequestHeadersSpec body(Object producer, Class elementClass);
+
+		/**
+		 * Set the body of the request to the given producer.
+		 * @param producer the producer to write to the request. This must be a
+		 * {@link Publisher} or another producer adaptable to a
+		 * {@code Publisher} via {@link ReactiveAdapterRegistry}
+		 * @param elementType the type reference of elements contained in the producer
+		 * @return spec for decoding the response
+		 * @since 5.2
+		 */
+		RequestHeadersSpec body(Object producer, ParameterizedTypeReference elementType);
+
+		/**
+		 * Set the body of the request to the given asynchronous {@code Publisher}.
+		 * @param publisher the request body data
+		 * @param elementClass the class of elements contained in the publisher
+		 * @param  the type of the elements contained in the publisher
+		 * @param  the type of the {@code Publisher}
+		 * @return spec for decoding the response
+		 */
+		> RequestHeadersSpec body(S publisher, Class elementClass);
+
+		/**
+		 * Set the body of the request to the given asynchronous {@code Publisher}.
+		 * @param publisher the request body data
+		 * @param elementType the type reference of elements contained in the publisher
+		 * @param  the type of the elements contained in the publisher
+		 * @param  the type of the {@code Publisher}
+		 * @return spec for decoding the response
+		 * @since 5.2
+		 */
+		> RequestHeadersSpec body(S publisher, ParameterizedTypeReference elementType);
+
+		/**
+		 * Set the body of the request to the given {@code BodyInserter}.
+		 * @param inserter the inserter
+		 * @return spec for decoding the response
+		 * @see org.springframework.web.reactive.function.BodyInserters
+		 */
+		RequestHeadersSpec body(BodyInserter inserter);
+
+		/**
+		 * Set the body of the request to the given {@code Object} and perform the request.
+		 * 

This method is a convenient shortcut for: + *

+		 * .body(BodyInserters.fromObject(object))
+		 * 
+ *

The body can be a + * {@link org.springframework.util.MultiValueMap MultiValueMap} to create + * a multipart request. The values in the {@code MultiValueMap} can be + * any Object representing the body of the part, or an + * {@link org.springframework.http.HttpEntity HttpEntity} representing a + * part with body and headers. The {@code MultiValueMap} can be built + * conveniently using + * @param body the {@code Object} to write to the request + * @return spec for decoding the response + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @deprecated as of Spring Framework 5.2 in favor of {@link #body(Object)} + */ + @Deprecated RequestHeadersSpec syncBody(Object body); } diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensions.kt b/spring-test/src/main/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensions.kt index d0c09a8cf3..e70ddee89c 100644 --- a/spring-test/src/main/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensions.kt +++ b/spring-test/src/main/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -16,7 +16,10 @@ package org.springframework.test.web.reactive.server +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow import org.reactivestreams.Publisher +import org.springframework.core.ParameterizedTypeReference import org.springframework.test.util.AssertionErrors.assertEquals import org.springframework.test.web.reactive.server.WebTestClient.* @@ -27,8 +30,49 @@ import org.springframework.test.web.reactive.server.WebTestClient.* * @author Sebastien Deleuze * @since 5.0 */ +@Deprecated("Use 'bodyWithType' instead.", replaceWith = ReplaceWith("bodyWithType(publisher)")) +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") inline fun > RequestBodySpec.body(publisher: S): RequestHeadersSpec<*> - = body(publisher, T::class.java) + = body(publisher, object : ParameterizedTypeReference() {}) + +/** + * Extension for [RequestBodySpec.body] providing a `bodyWithType(Any)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param producer the producer to write to the request. This must be a + * [Publisher] or another producer adaptable to a + * [Publisher] via [org.springframework.core.ReactiveAdapterRegistry] + * @param the type of the elements contained in the producer + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun RequestBodySpec.bodyWithType(producer: Any): RequestHeadersSpec<*> + = body(producer, object : ParameterizedTypeReference() {}) + +/** + * Extension for [RequestBodySpec.body] providing a `bodyWithType(Publisher)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param publisher the [Publisher] to write to the request + * @param the type of the elements contained in the publisher + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun RequestBodySpec.bodyWithType(publisher: Publisher): RequestHeadersSpec<*> = + body(publisher, object : ParameterizedTypeReference() {}) + +/** + * Extension for [RequestBodySpec.body] providing a `bodyWithType(Flow)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param flow the [Flow] to write to the request + * @param the type of the elements contained in the publisher + * @author Sebastien Deleuze + * @since 5.2 + */ +@FlowPreview +inline fun RequestBodySpec.bodyWithType(flow: Flow): RequestHeadersSpec<*> = + body(flow, object : ParameterizedTypeReference() {}) /** * Extension for [ResponseSpec.expectBody] providing an `expectBody()` variant and @@ -44,13 +88,11 @@ inline fun ResponseSpec.expectBody(): KotlinBodySpec = object : KotlinBodySpec { override fun isEqualTo(expected: B): KotlinBodySpec = it - .assertWithDiagnostics({ assertEquals("Response body", expected, it.responseBody) }) - .let { this } + .assertWithDiagnostics { assertEquals("Response body", expected, it.responseBody) } + .let { this } override fun consumeWith(consumer: (EntityExchangeResult) -> Unit): KotlinBodySpec = - it - .assertWithDiagnostics({ consumer.invoke(it) }) - .let { this } + it.assertWithDiagnostics { consumer.invoke(it) }.let { this } override fun returnResult(): EntityExchangeResult = it } @@ -88,7 +130,7 @@ interface KotlinBodySpec { * @since 5.0 */ inline fun ResponseSpec.expectBodyList(): ListBodySpec = - expectBodyList(E::class.java) + expectBodyList(object : ParameterizedTypeReference() {}) /** * Extension for [ResponseSpec.returnResult] providing a `returnResult()` variant. @@ -98,4 +140,4 @@ inline fun ResponseSpec.expectBodyList(): ListBodySpec = */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") inline fun ResponseSpec.returnResult(): FluxExchangeResult = - returnResult(T::class.java) + returnResult(object : ParameterizedTypeReference() {}) diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/ApplicationContextSpecTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/ApplicationContextSpecTests.java index 20dcdd649c..8704ff3266 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/ApplicationContextSpecTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/ApplicationContextSpecTests.java @@ -61,7 +61,7 @@ public class ApplicationContextSpecTests { .GET("/sessionClassName", request -> request.session().flatMap(session -> { String className = session.getClass().getSimpleName(); - return ServerResponse.ok().syncBody(className); + return ServerResponse.ok().body(className); })) .build(); } diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java index 83d61e8de4..c85ff0b1af 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java @@ -63,7 +63,7 @@ public class ErrorTests { EntityExchangeResult result = this.client.post() .uri("/post") .contentType(MediaType.APPLICATION_JSON) - .syncBody(new Person("Dan")) + .body(new Person("Dan")) .exchange() .expectStatus().isBadRequest() .expectBody().isEmpty(); diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java index 0d0f9c7a65..6725a0a267 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/JsonContentTests.java @@ -82,7 +82,7 @@ public class JsonContentTests { public void postJsonContent() { this.client.post().uri("/persons") .contentType(MediaType.APPLICATION_JSON) - .syncBody("{\"name\":\"John\"}") + .body("{\"name\":\"John\"}") .exchange() .expectStatus().isCreated() .expectBody().isEmpty(); diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java index 7a1361cd5a..dda30498fb 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java @@ -145,7 +145,7 @@ public class ResponseEntityTests { @Test public void postEntity() { this.client.post() - .syncBody(new Person("John")) + .body(new Person("John")) .exchange() .expectStatus().isCreated() .expectHeader().valueEquals("location", "/persons/John") diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java index faf18f6c6a..08c23635e5 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java @@ -116,7 +116,7 @@ public class XmlContentTests { this.client.post().uri("/persons") .contentType(MediaType.APPLICATION_XML) - .syncBody(content) + .body(content) .exchange() .expectStatus().isCreated() .expectHeader().valueEquals(HttpHeaders.LOCATION, "/persons/John") diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java index 773a6110e0..752ffca7c9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java @@ -45,7 +45,7 @@ public class HttpServerTests { @Before public void start() throws Exception { HttpHandler httpHandler = RouterFunctions.toHttpHandler( - route(GET("/test"), request -> ServerResponse.ok().syncBody("It works!"))); + route(GET("/test"), request -> ServerResponse.ok().body("It works!"))); this.server = new ReactorHttpServer(); this.server.setHandler(httpHandler); diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java index aebeaf2c15..710ad2355b 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java @@ -41,7 +41,7 @@ public class RouterFunctionTests { public void setUp() throws Exception { RouterFunction route = route(GET("/test"), request -> - ServerResponse.ok().syncBody("It works!")); + ServerResponse.ok().body("It works!")); this.testClient = WebTestClient.bindToRouterFunction(route).build(); } diff --git a/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensionsTests.kt b/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensionsTests.kt index 277c855246..6432b95418 100644 --- a/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensionsTests.kt +++ b/spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensionsTests.kt @@ -18,10 +18,14 @@ package org.springframework.test.web.reactive.server import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow import org.junit.Assert.assertEquals import org.junit.Test import org.reactivestreams.Publisher +import org.springframework.core.ParameterizedTypeReference import org.springframework.web.reactive.function.server.router +import java.util.concurrent.CompletableFuture /** * Mock object based tests for [WebTestClient] Kotlin extensions @@ -30,16 +34,31 @@ import org.springframework.web.reactive.function.server.router */ class WebTestClientExtensionsTests { - val requestBodySpec = mockk(relaxed = true) + private val requestBodySpec = mockk(relaxed = true) - val responseSpec = mockk(relaxed = true) + private val responseSpec = mockk(relaxed = true) @Test - fun `RequestBodySpec#body with Publisher and reified type parameters`() { + fun `RequestBodySpec#bodyWithType with Publisher and reified type parameters`() { val body = mockk>() - requestBodySpec.body(body) - verify { requestBodySpec.body(body, Foo::class.java) } + requestBodySpec.bodyWithType(body) + verify { requestBodySpec.body(body, object : ParameterizedTypeReference() {}) } + } + + @Test + @FlowPreview + fun `RequestBodySpec#bodyWithType with Flow and reified type parameters`() { + val body = mockk>() + requestBodySpec.bodyWithType(body) + verify { requestBodySpec.body(body, object : ParameterizedTypeReference() {}) } + } + + @Test + fun `RequestBodySpec#bodyWithType with CompletableFuture and reified type parameters`() { + val body = mockk>() + requestBodySpec.bodyWithType(body) + verify { requestBodySpec.body(body, object : ParameterizedTypeReference() {}) } } @Test @@ -51,7 +70,7 @@ class WebTestClientExtensionsTests { @Test fun `KotlinBodySpec#isEqualTo`() { WebTestClient - .bindToRouterFunction( router { GET("/") { ok().syncBody("foo") } } ) + .bindToRouterFunction( router { GET("/") { ok().body("foo") } } ) .build() .get().uri("/").exchange().expectBody().isEqualTo("foo") } @@ -59,7 +78,7 @@ class WebTestClientExtensionsTests { @Test fun `KotlinBodySpec#consumeWith`() { WebTestClient - .bindToRouterFunction( router { GET("/") { ok().syncBody("foo") } } ) + .bindToRouterFunction( router { GET("/") { ok().body("foo") } } ) .build() .get().uri("/").exchange().expectBody().consumeWith { assertEquals("foo", it.responseBody) } } @@ -67,7 +86,7 @@ class WebTestClientExtensionsTests { @Test fun `KotlinBodySpec#returnResult`() { WebTestClient - .bindToRouterFunction( router { GET("/") { ok().syncBody("foo") } } ) + .bindToRouterFunction( router { GET("/") { ok().body("foo") } } ) .build() .get().uri("/").exchange().expectBody().returnResult().apply { assertEquals("foo", responseBody) } } @@ -75,13 +94,13 @@ class WebTestClientExtensionsTests { @Test fun `ResponseSpec#expectBodyList with reified type parameters`() { responseSpec.expectBodyList() - verify { responseSpec.expectBodyList(Foo::class.java) } + verify { responseSpec.expectBodyList(object : ParameterizedTypeReference() {}) } } @Test fun `ResponseSpec#returnResult with reified type parameters`() { responseSpec.returnResult() - verify { responseSpec.returnResult(Foo::class.java) } + verify { responseSpec.returnResult(object : ParameterizedTypeReference() {}) } } class Foo diff --git a/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java b/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java index 8982cc4f97..d63f65f142 100644 --- a/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java @@ -41,7 +41,7 @@ import org.springframework.util.MultiValueMap; /** * Builder for the body of a multipart request, producing * {@code MultiValueMap}, which can be provided to the - * {@code WebClient} through the {@code syncBody} method. + * {@code WebClient} through the {@code body} method. * * Examples: *

@@ -67,7 +67,7 @@ import org.springframework.util.MultiValueMap;
  *
  * Mono<Void> result = webClient.post()
  *     .uri("...")
- *     .syncBody(multipartBody)
+ *     .body(multipartBody)
  *     .retrieve()
  *     .bodyToMono(Void.class)
  * 
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 72442eaf78..e869dc55c2 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-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -23,6 +23,8 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; @@ -44,6 +46,7 @@ import org.springframework.util.MultiValueMap; * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 5.0 */ public abstract class BodyInserters { @@ -61,6 +64,8 @@ public abstract class BodyInserters { private static final BodyInserter EMPTY_INSERTER = (response, context) -> response.setComplete(); + private static final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); + /** * Inserter that does not write. @@ -73,16 +78,68 @@ public abstract class BodyInserters { /** * Inserter to write the given object. - *

Alternatively, consider using the {@code syncBody(Object)} shortcuts on + *

Alternatively, consider using the {@code body(Object)} shortcuts on * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. * @param body the body to write to the response * @param the type of the body * @return the inserter to write a single object + * @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, Class)} or + * {@link #fromProducer(Object, Class)} should be used. + * @see #fromPublisher(Publisher, Class) + * @see #fromProducer(Object, Class) */ public static BodyInserter fromObject(T body) { + Assert.notNull(body, "Body 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.forInstance(body)); + writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body), null); + } + + /** + * Inserter to write the given producer of value(s) which must be a {@link Publisher} + * or another producer adaptable to a {@code Publisher} via + * {@link ReactiveAdapterRegistry}. + *

Alternatively, consider using the {@code body} shortcuts on + * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and + * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. + * @param the type of the body + * @param producer the source of body value(s). + * @param elementClass the type of values to be produced + * @return the inserter to write a producer + * @since 5.2 + */ + public static BodyInserter fromProducer(T producer, Class elementClass) { + Assert.notNull(producer, "'producer' must not be null"); + Assert.notNull(elementClass, "'elementClass' must not be null"); + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(producer.getClass()); + Assert.notNull(adapter, "'producer' type is unknown to ReactiveAdapterRegistry"); + return (message, context) -> + writeWithMessageWriters(message, context, producer, ResolvableType.forClass(elementClass), adapter); + } + + /** + * Inserter to write the given producer of value(s) which must be a {@link Publisher} + * or another producer adaptable to a {@code Publisher} via + * {@link ReactiveAdapterRegistry}. + *

Alternatively, consider using the {@code body} shortcuts on + * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and + * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}. + * @param the type of the body + * @param producer the source of body value(s). + * @param elementType the type of values to be produced + * @return the inserter to write a producer + * @since 5.2 + */ + public static BodyInserter fromProducer(T producer, ParameterizedTypeReference elementType) { + Assert.notNull(producer, "'producer' must not be null"); + Assert.notNull(elementType, "'elementType' must not be null"); + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(producer.getClass()); + Assert.notNull(adapter, "'producer' type is unknown to ReactiveAdapterRegistry"); + return (message, context) -> + writeWithMessageWriters(message, context, producer, ResolvableType.forType(elementType), adapter); } /** @@ -102,7 +159,7 @@ public abstract class BodyInserters { Assert.notNull(publisher, "Publisher must not be null"); Assert.notNull(elementClass, "Element Class must not be null"); return (message, context) -> - writeWithMessageWriters(message, context, publisher, ResolvableType.forClass(elementClass)); + writeWithMessageWriters(message, context, publisher, ResolvableType.forClass(elementClass), null); } /** @@ -122,7 +179,7 @@ public abstract class BodyInserters { Assert.notNull(publisher, "Publisher must not be null"); Assert.notNull(typeReference, "ParameterizedTypeReference must not be null"); return (message, context) -> - writeWithMessageWriters(message, context, publisher, ResolvableType.forType(typeReference.getType())); + writeWithMessageWriters(message, context, publisher, ResolvableType.forType(typeReference.getType()), null); } /** @@ -145,8 +202,8 @@ public abstract class BodyInserters { /** * Inserter to write the given {@code ServerSentEvent} publisher. *

Alternatively, you can provide event data objects via - * {@link #fromPublisher(Publisher, Class)}, and set the "Content-Type" to - * {@link MediaType#TEXT_EVENT_STREAM text/event-stream}. + * {@link #fromPublisher(Publisher, Class)} or {@link #fromProducer(Object, Class)}, + * and set the "Content-Type" to {@link MediaType#TEXT_EVENT_STREAM text/event-stream}. * @param eventsPublisher the {@code ServerSentEvent} publisher to write to the response body * @param the type of the data elements in the {@link ServerSentEvent} * @return the inserter to write a {@code ServerSentEvent} publisher @@ -169,7 +226,7 @@ public abstract class BodyInserters { * Return a {@link FormInserter} to write the given {@code MultiValueMap} * as URL-encoded form data. The returned inserter allows for additional * entries to be added via {@link FormInserter#with(String, Object)}. - *

Note that you can also use the {@code syncBody(Object)} method in the + *

Note that you can also use the {@code body(Object)} method in the * request builders of both the {@code WebClient} and {@code WebTestClient}. * In that case the setting of the request content type is also not required, * just be sure the map contains String values only or otherwise it would be @@ -201,7 +258,7 @@ public abstract class BodyInserters { * Object or an {@link HttpEntity}. *

Note that you can also build the multipart data externally with * {@link MultipartBodyBuilder}, and pass the resulting map directly to the - * {@code syncBody(Object)} shortcut method in {@code WebClient}. + * {@code body(Object)} shortcut method in {@code WebClient}. * @param multipartData the form data to write to the output message * @return the inserter that allows adding more parts * @see MultipartBodyBuilder @@ -217,7 +274,7 @@ public abstract class BodyInserters { * {@link HttpEntity}. *

Note that you can also build the multipart data externally with * {@link MultipartBodyBuilder}, and pass the resulting map directly to the - * {@code syncBody(Object)} shortcut method in {@code WebClient}. + * {@code body(Object)} shortcut method in {@code WebClient}. * @param name the part name * @param value the part value, an Object or {@code HttpEntity} * @return the inserter that allows adding more parts @@ -233,7 +290,7 @@ public abstract class BodyInserters { * as multipart data. *

Note that you can also build the multipart data externally with * {@link MultipartBodyBuilder}, and pass the resulting map directly to the - * {@code syncBody(Object)} shortcut method in {@code WebClient}. + * {@code body(Object)} shortcut method in {@code WebClient}. * @param name the part name * @param publisher the publisher that forms the part value * @param elementClass the class contained in the {@code publisher} @@ -251,7 +308,7 @@ public abstract class BodyInserters { * allows specifying generic type information. *

Note that you can also build the multipart data externally with * {@link MultipartBodyBuilder}, and pass the resulting map directly to the - * {@code syncBody(Object)} shortcut method in {@code WebClient}. + * {@code body(Object)} shortcut method in {@code WebClient}. * @param name the part name * @param publisher the publisher that forms the part value * @param typeReference the type contained in the {@code publisher} @@ -278,15 +335,25 @@ public abstract class BodyInserters { } - private static

, M extends ReactiveHttpOutputMessage> Mono writeWithMessageWriters( - M outputMessage, BodyInserter.Context context, P body, ResolvableType bodyType) { + private static Mono writeWithMessageWriters( + M outputMessage, BodyInserter.Context context, Object body, ResolvableType bodyType, @Nullable ReactiveAdapter adapter) { + Publisher publisher; + if (body instanceof Publisher) { + publisher = (Publisher) body; + } + else if (adapter != null) { + publisher = adapter.toPublisher(body); + } + else { + publisher = Mono.just(body); + } MediaType mediaType = outputMessage.getHeaders().getContentType(); return context.messageWriters().stream() .filter(messageWriter -> messageWriter.canWrite(bodyType, mediaType)) .findFirst() .map(BodyInserters::cast) - .map(writer -> write(body, bodyType, mediaType, outputMessage, context, writer)) + .map(writer -> write(publisher, bodyType, mediaType, outputMessage, context, writer)) .orElseGet(() -> Mono.error(unsupportedError(bodyType, context, mediaType))); } 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 81f7bb93fd..2be82f862b 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 @@ -61,6 +61,7 @@ import org.springframework.web.util.UriBuilderFactory; * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Sebastien Deleuze * @since 5.0 */ class DefaultWebClient implements WebClient { @@ -290,16 +291,27 @@ class DefaultWebClient implements WebClient { } @Override - public RequestHeadersSpec body(BodyInserter inserter) { - this.inserter = inserter; + public RequestHeadersSpec body(Object body) { + this.inserter = BodyInserters.fromObject(body); + return this; + } + + @Override + public RequestHeadersSpec body(Object producer, Class elementClass) { + this.inserter = BodyInserters.fromProducer(producer, elementClass); + return this; + } + + @Override + public RequestHeadersSpec body(Object producer, ParameterizedTypeReference elementType) { + this.inserter = BodyInserters.fromProducer(producer, elementType); return this; } @Override public > RequestHeadersSpec body( - P publisher, ParameterizedTypeReference typeReference) { - - this.inserter = BodyInserters.fromPublisher(publisher, typeReference); + P publisher, ParameterizedTypeReference elementType) { + this.inserter = BodyInserters.fromPublisher(publisher, elementType); return this; } @@ -310,13 +322,17 @@ class DefaultWebClient implements WebClient { } @Override - public RequestHeadersSpec syncBody(Object body) { - Assert.isTrue(!(body instanceof Publisher), - "Please specify the element class by using body(Publisher, Class)"); - this.inserter = BodyInserters.fromObject(body); + public RequestHeadersSpec body(BodyInserter inserter) { + this.inserter = inserter; return this; } + @Override + @Deprecated + public RequestHeadersSpec syncBody(Object body) { + return body(body); + } + @Override public Mono exchange() { ClientRequest request = (this.inserter != null ? 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 1a7f366076..b5cdfed4c2 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 @@ -30,6 +30,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -56,13 +57,15 @@ import org.springframework.web.util.UriBuilderFactory; * *

For examples with a request body see: *

    + *
  • {@link RequestBodySpec#body(Object) body(Object)} *
  • {@link RequestBodySpec#body(Publisher, Class) body(Publisher,Class)} - *
  • {@link RequestBodySpec#syncBody(Object) syncBody(Object)} + *
  • {@link RequestBodySpec#body(Object, Class) body(Object,Class)} *
  • {@link RequestBodySpec#body(BodyInserter) body(BodyInserter)} *
* * @author Rossen Stoyanchev * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 5.0 */ public interface WebClient { @@ -517,23 +520,80 @@ public interface WebClient { RequestBodySpec contentType(MediaType contentType); /** - * Set the body of the request using the given body inserter. - * {@link BodyInserters} provides access to built-in implementations of - * {@link BodyInserter}. - * @param inserter the body inserter to use for the request body + * A shortcut for {@link #body(BodyInserter)} with an + * {@linkplain BodyInserters#fromObject Object inserter}. + * For example: + *

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

For multipart requests, provide a + * {@link org.springframework.util.MultiValueMap MultiValueMap}. The + * values in the {@code MultiValueMap} can be any Object representing + * the body of the part, or an + * {@link org.springframework.http.HttpEntity HttpEntity} representing + * a part with body and headers. The {@code MultiValueMap} can be built + * with {@link org.springframework.http.client.MultipartBodyBuilder + * MultipartBodyBuilder}. + * @param body the {@code Object} to write to the request * @return this builder - * @see org.springframework.web.reactive.function.BodyInserters + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @since 5.2 */ - RequestHeadersSpec body(BodyInserter inserter); + RequestHeadersSpec body(Object body); + + /** + * A shortcut for {@link #body(BodyInserter)} with a + * {@linkplain BodyInserters#fromProducer inserter}. + * For example: + *

+		 * Single<Person> personSingle = ... ;
+		 *
+		 * Mono<Void> result = client.post()
+		 *     .uri("/persons/{id}", id)
+		 *     .contentType(MediaType.APPLICATION_JSON)
+		 *     .body(personSingle, Person.class)
+		 *     .retrieve()
+		 *     .bodyToMono(Void.class);
+		 * 
+ * @param producer the producer to write to the request. This must be a + * {@link Publisher} or another producer adaptable to a + * {@code Publisher} via {@link ReactiveAdapterRegistry} + * @param elementClass the class of elements contained in the producer + * @return this builder + * @since 5.2 + */ + RequestHeadersSpec body(Object producer, Class elementClass); + + /** + * A variant of {@link #body(Object, Class)} that allows providing + * element type information that includes generics via a + * {@link ParameterizedTypeReference}. + * @param producer the producer to write to the request. This must be a + * {@link Publisher} or another producer adaptable to a + * {@code Publisher} via {@link ReactiveAdapterRegistry} + * @param elementType the type reference of elements contained in the producer + * @return this builder + * @since 5.2 + */ + RequestHeadersSpec body(Object producer, ParameterizedTypeReference elementType); /** * A shortcut for {@link #body(BodyInserter)} with a * {@linkplain BodyInserters#fromPublisher Publisher inserter}. * For example: *

-		 * Mono personMono = ... ;
+		 * Mono<Person> personMono = ... ;
 		 *
-		 * Mono result = client.post()
+		 * Mono<Void> result = client.post()
 		 *     .uri("/persons/{id}", id)
 		 *     .contentType(MediaType.APPLICATION_JSON)
 		 *     .body(personMono, Person.class)
@@ -553,13 +613,23 @@ public interface WebClient {
 		 * element type information that includes generics via a
 		 * {@link ParameterizedTypeReference}.
 		 * @param publisher the {@code Publisher} to write to the request
-		 * @param typeReference the type reference of elements contained in the publisher
+		 * @param elementType the type reference of elements contained in the publisher
 		 * @param  the type of the elements contained in the publisher
 		 * @param 

the type of the {@code Publisher} * @return this builder */ > RequestHeadersSpec body(P publisher, - ParameterizedTypeReference typeReference); + ParameterizedTypeReference elementType); + + /** + * Set the body of the request using the given body inserter. + * {@link BodyInserters} provides access to built-in implementations of + * {@link BodyInserter}. + * @param inserter the body inserter to use for the request body + * @return this builder + * @see org.springframework.web.reactive.function.BodyInserters + */ + RequestHeadersSpec body(BodyInserter inserter); /** * A shortcut for {@link #body(BodyInserter)} with an @@ -585,7 +655,12 @@ public interface WebClient { * MultipartBodyBuilder}. * @param body the {@code Object} to write to the request * @return this builder + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @deprecated as of Spring Framework 5.2 in favor of {@link #body(Object)} */ + @Deprecated RequestHeadersSpec syncBody(Object body); } 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 cabbe2bf04..49e35a9cf3 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 @@ -59,6 +59,7 @@ import org.springframework.web.server.ServerWebExchange; * * @author Arjen Poutsma * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 5.0 */ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @@ -222,10 +223,43 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { } @Override - public > Mono body(P publisher, Class elementClass) { - Assert.notNull(publisher, "Publisher must not be null"); - Assert.notNull(elementClass, "Element Class must not be null"); + public Mono body(Object body) { + return new DefaultEntityResponseBuilder<>(body, + BodyInserters.fromObject(body)) + .status(this.statusCode) + .headers(this.headers) + .cookies(cookies -> cookies.addAll(this.cookies)) + .hints(hints -> hints.putAll(this.hints)) + .build() + .map(entityResponse -> entityResponse); + } + @Override + public Mono body(Object producer, Class elementClass) { + return new DefaultEntityResponseBuilder<>(producer, + BodyInserters.fromProducer(producer, elementClass)) + .status(this.statusCode) + .headers(this.headers) + .cookies(cookies -> cookies.addAll(this.cookies)) + .hints(hints -> hints.putAll(this.hints)) + .build() + .map(entityResponse -> entityResponse); + } + + @Override + public Mono body(Object producer, ParameterizedTypeReference elementType) { + return new DefaultEntityResponseBuilder<>(producer, + BodyInserters.fromProducer(producer, elementType)) + .status(this.statusCode) + .headers(this.headers) + .cookies(cookies -> cookies.addAll(this.cookies)) + .hints(hints -> hints.putAll(this.hints)) + .build() + .map(entityResponse -> entityResponse); + } + + @Override + public > Mono body(P publisher, Class elementClass) { return new DefaultEntityResponseBuilder<>(publisher, BodyInserters.fromPublisher(publisher, elementClass)) .status(this.statusCode) @@ -238,13 +272,9 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { @Override public > Mono body(P publisher, - ParameterizedTypeReference typeReference) { - - Assert.notNull(publisher, "Publisher must not be null"); - Assert.notNull(typeReference, "ParameterizedTypeReference must not be null"); - + ParameterizedTypeReference elementType) { return new DefaultEntityResponseBuilder<>(publisher, - BodyInserters.fromPublisher(publisher, typeReference)) + BodyInserters.fromPublisher(publisher, elementType)) .status(this.statusCode) .headers(this.headers) .cookies(cookies -> cookies.addAll(this.cookies)) @@ -254,19 +284,9 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { } @Override + @Deprecated public Mono syncBody(Object body) { - Assert.notNull(body, "Body must not be null"); - Assert.isTrue(!(body instanceof Publisher), - "Please specify the element class by using body(Publisher, Class)"); - - return new DefaultEntityResponseBuilder<>(body, - BodyInserters.fromObject(body)) - .status(this.statusCode) - .headers(this.headers) - .cookies(cookies -> cookies.addAll(this.cookies)) - .hints(hints -> hints.putAll(this.hints)) - .build() - .map(entityResponse -> entityResponse); + return body(body); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java index ab72a30f5a..152b038770 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/EntityResponse.java @@ -64,12 +64,38 @@ public interface EntityResponse extends ServerResponse { /** * Create a builder with the given object. - * @param t the object that represents the body of the response - * @param the type of the elements contained in the publisher + * @param body the object that represents the body of the response + * @param the type of the body * @return the created builder */ - static Builder fromObject(T t) { - return new DefaultEntityResponseBuilder<>(t, BodyInserters.fromObject(t)); + static Builder fromObject(T body) { + return new DefaultEntityResponseBuilder<>(body, BodyInserters.fromObject(body)); + } + + /** + * Create a builder with the given producer. + * @param producer the producer that represents the body of the response + * @param elementClass the class of elements contained in the publisher + * @return the created builder + * @since 5.2 + */ + static Builder fromProducer(T producer, Class elementClass) { + return new DefaultEntityResponseBuilder<>(producer, + BodyInserters.fromProducer(producer, elementClass)); + } + + /** + * Create a builder with the given producer. + * @param producer the producer that represents the body of the response + * @param typeReference the type of elements contained in the producer + * @return the created builder + * @since 5.2 + */ + static Builder fromProducer(T producer, + ParameterizedTypeReference typeReference) { + + return new DefaultEntityResponseBuilder<>(producer, + BodyInserters.fromProducer(producer, typeReference)); } /** 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 286845d9da..41a2fce19b 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 @@ -30,6 +30,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -384,6 +385,45 @@ public interface ServerResponse { */ BodyBuilder hints(Consumer> hintsConsumer); + /** + * Set the body of the response to the given {@code Object} and return it. + * This convenience method combines {@link #body(BodyInserter)} and + * {@link BodyInserters#fromObject(Object)}. + * @param body the body of the response + * @return the built response + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @since 5.2 + */ + Mono body(Object body); + + /** + * Set the body of the response to the given asynchronous {@code Publisher} and return it. + * This convenience method combines {@link #body(BodyInserter)} and + * {@link BodyInserters#fromProducer(Object, Class)}. + * @param producer the producer to write to the response. This must be a + * {@link Publisher} or another producer adaptable to a + * {@code Publisher} via {@link ReactiveAdapterRegistry} + * @param elementClass the class of elements contained in the producer + * @return the built response + * @since 5.2 + */ + Mono body(Object producer, Class elementClass); + + /** + * Set the body of the response to the given asynchronous {@code Publisher} and return it. + * This convenience method combines {@link #body(BodyInserter)} and + * {@link BodyInserters#fromProducer(Object, ParameterizedTypeReference)}. + * @param producer the producer to write to the response. This must be a + * {@link Publisher} or another producer adaptable to a + * {@code Publisher} via {@link ReactiveAdapterRegistry} + * @param typeReference a type reference describing the elements contained in the producer + * @return the built response + * @since 5.2 + */ + Mono body(Object producer, ParameterizedTypeReference typeReference); + /** * Set the body of the response to the given asynchronous {@code Publisher} and return it. * This convenience method combines {@link #body(BodyInserter)} and @@ -399,7 +439,7 @@ public interface ServerResponse { /** * Set the body of the response to the given asynchronous {@code Publisher} and return it. * This convenience method combines {@link #body(BodyInserter)} and - * {@link BodyInserters#fromPublisher(Publisher, Class)}. + * {@link BodyInserters#fromPublisher(Publisher, ParameterizedTypeReference)}. * @param publisher the {@code Publisher} to write to the response * @param typeReference a type reference describing the elements contained in the publisher * @param the type of the elements contained in the publisher @@ -410,23 +450,28 @@ public interface ServerResponse { ParameterizedTypeReference typeReference); /** - * Set the body of the response to the given synchronous {@code Object} and return it. + * Set the body of the response to the given {@code BodyInserter} and return it. + * @param inserter the {@code BodyInserter} that writes to the response + * @return the built response + */ + Mono body(BodyInserter inserter); + + /** + * Set the body of the response to the given {@code Object} and return it. * This convenience method combines {@link #body(BodyInserter)} and * {@link BodyInserters#fromObject(Object)}. * @param body the body of the response * @return the built response * @throws IllegalArgumentException if {@code body} is a {@link Publisher}, for which * {@link #body(Publisher, Class)} should be used. + * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an + * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()}, + * for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used. + * @deprecated as of Spring Framework 5.2 in favor of {@link #body(Object)} */ + @Deprecated Mono syncBody(Object body); - /** - * Set the body of the response to the given {@code BodyInserter} and return it. - * @param inserter the {@code BodyInserter} that writes to the response - * @return the built response - */ - Mono body(BodyInserter inserter); - /** * Render the template with the given {@code name} using the given {@code modelAttributes}. * The model attributes are mapped under 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 ef4edb4429..fed3f65291 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 @@ -16,14 +16,10 @@ package org.springframework.web.reactive.function.client -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactive.flow.asFlow -import kotlinx.coroutines.reactive.flow.asPublisher -import kotlinx.coroutines.reactor.mono import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec @@ -39,20 +35,59 @@ import reactor.core.publisher.Mono * @author Sebastien Deleuze * @since 5.0 */ +@Deprecated("Use 'bodyWithType' instead.", replaceWith = ReplaceWith("bodyWithType(publisher)")) +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") inline fun > RequestBodySpec.body(publisher: S): RequestHeadersSpec<*> = body(publisher, object : ParameterizedTypeReference() {}) /** - * Coroutines [Flow] based extension for [WebClient.RequestBodySpec.body] providing a - * body(Flow)` variant leveraging Kotlin reified type parameters. This extension is - * not subject to type erasure and retains actual generic type arguments. - * + * Extension for [WebClient.RequestBodySpec.body] providing a `bodyWithType(Any)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param producer the producer to write to the request. This must be a + * [Publisher] or another producer adaptable to a + * [Publisher] via [org.springframework.core.ReactiveAdapterRegistry] + * @param the type of the elements contained in the producer + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun RequestBodySpec.bodyWithType(producer: Any): RequestHeadersSpec<*> = + body(producer, object : ParameterizedTypeReference() {}) + +/** + * Extension for [WebClient.RequestBodySpec.body] providing a `bodyWithType(Publisher)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param publisher the [Publisher] to write to the request + * @param the type of the elements contained in the publisher + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun RequestBodySpec.bodyWithType(publisher: Publisher): RequestHeadersSpec<*> = + body(publisher, object : ParameterizedTypeReference() {}) + +/** + * Extension for [WebClient.RequestBodySpec.body] providing a `bodyWithType(Flow)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param flow the [Flow] to write to the request + * @param the type of the elements contained in the flow * @author Sebastien Deleuze * @since 5.2 */ @FlowPreview -inline fun > RequestBodySpec.body(flow: S): RequestHeadersSpec<*> = - body(flow.asPublisher(), object : ParameterizedTypeReference() {}) +inline fun RequestBodySpec.bodyWithType(flow: Flow): RequestHeadersSpec<*> = + body(flow, object : ParameterizedTypeReference() {}) + +/** + * Coroutines variant of [WebClient.RequestHeadersSpec.exchange]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun RequestHeadersSpec>.awaitExchange(): ClientResponse = + exchange().awaitSingle() + /** * Extension for [WebClient.ResponseSpec.bodyToMono] providing a `bodyToMono()` variant @@ -90,25 +125,6 @@ inline fun WebClient.ResponseSpec.bodyToFlux(): Flux = inline fun WebClient.ResponseSpec.bodyToFlow(batchSize: Int = 1): Flow = bodyToFlux().asFlow(batchSize) - -/** - * Coroutines variant of [WebClient.RequestHeadersSpec.exchange]. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -suspend fun RequestHeadersSpec>.awaitExchange(): ClientResponse = - exchange().awaitSingle() - -/** - * Coroutines variant of [WebClient.RequestBodySpec.body]. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -inline fun RequestBodySpec.body(crossinline supplier: suspend () -> T) - = body(GlobalScope.mono(Dispatchers.Unconfined) { supplier.invoke() }) - /** * Coroutines variant of [WebClient.ResponseSpec.bodyToMono]. * 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 8ed19bca04..01b8345f1a 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 @@ -19,7 +19,6 @@ package org.springframework.web.reactive.function.server import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.reactive.awaitSingle -import kotlinx.coroutines.reactive.flow.asPublisher import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import org.springframework.http.MediaType @@ -33,9 +32,63 @@ import reactor.core.publisher.Mono * @author Sebastien Deleuze * @since 5.0 */ +@Deprecated("Use 'bodyWithType' instead.", replaceWith = ReplaceWith("bodyWithType(publisher)")) +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") inline fun ServerResponse.BodyBuilder.body(publisher: Publisher): Mono = body(publisher, object : ParameterizedTypeReference() {}) +/** + * Extension for [ServerResponse.BodyBuilder.body] providing a `bodyWithType(Any)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param producer the producer to write to the response. This must be a + * [Publisher] or another producer adaptable to a + * [Publisher] via [org.springframework.core.ReactiveAdapterRegistry] + * @param the type of the elements contained in the producer + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun ServerResponse.BodyBuilder.bodyWithType(producer: Any): Mono = + body(producer, object : ParameterizedTypeReference() {}) + +/** + * Extension for [ServerResponse.BodyBuilder.body] providing a `bodyWithType(Publisher)` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * @param publisher the [Publisher] to write to the response + * @param the type of the elements contained in the publisher + * @author Sebastien Deleuze + * @since 5.2 + */ +inline fun ServerResponse.BodyBuilder.bodyWithType(publisher: Publisher): Mono = + body(publisher, object : ParameterizedTypeReference() {}) + +/** + * Coroutines variant of [ServerResponse.BodyBuilder.body] with an [Any] parameter. + * + * 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.fromObject]. + * @param body the body of the response + * @return the built response + * @throws IllegalArgumentException if `body` is a [Publisher] or an + * instance of a type supported by [org.springframework.core.ReactiveAdapterRegistry.getSharedInstance], + */ +suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse = + body(body).awaitSingle() + +/** + * Coroutines variant of [ServerResponse.BodyBuilder.body] with [Any] and + * [ParameterizedTypeReference] parameters providing a `bodyAndAwait(Flow)` variant. + * This extension is not subject to type erasure and retains actual generic type arguments. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +@FlowPreview +suspend inline fun ServerResponse.BodyBuilder.bodyAndAwait(flow: Flow): ServerResponse = + body(flow, object : ParameterizedTypeReference() {}).awaitSingle() + /** * Extension for [ServerResponse.BodyBuilder.body] providing a * `bodyToServerSentEvents(Publisher)` variant. This extension is not subject to type @@ -44,7 +97,7 @@ inline fun ServerResponse.BodyBuilder.body(publisher: Publishe * @author Sebastien Deleuze * @since 5.0 */ -@Deprecated("Use 'sse().body()' instead.") +@Deprecated("Use 'sse().bodyWithType(publisher)' instead.", replaceWith = ReplaceWith("sse().bodyWithType(publisher)")) inline fun ServerResponse.BodyBuilder.bodyToServerSentEvents(publisher: Publisher): Mono = contentType(MediaType.TEXT_EVENT_STREAM).body(publisher, object : ParameterizedTypeReference() {}) @@ -77,38 +130,7 @@ fun ServerResponse.BodyBuilder.html() = contentType(MediaType.TEXT_HTML) fun ServerResponse.BodyBuilder.sse() = contentType(MediaType.TEXT_EVENT_STREAM) /** - * Coroutines variant of [ServerResponse.HeadersBuilder.build]. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -suspend fun ServerResponse.HeadersBuilder>.buildAndAwait(): ServerResponse = - build().awaitSingle() - -/** - * Coroutines [Flow] based extension for [ServerResponse.BodyBuilder.body] providing a - * `bodyFlowAndAwait(Flow)` variant. This extension is not subject to type erasure and retains - * actual generic type arguments. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -@FlowPreview -suspend inline fun ServerResponse.BodyBuilder.bodyFlowAndAwait(flow: Flow): ServerResponse = - body(flow.asPublisher(), object : ParameterizedTypeReference() {}).awaitSingle() - -/** - * Coroutines variant of [ServerResponse.BodyBuilder.syncBody]. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse = - syncBody(body).awaitSingle() - -/** - * Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within - * another suspendable function. + * Coroutines variant of [ServerResponse.BodyBuilder.render]. * * @author Sebastien Deleuze * @since 5.2 @@ -117,11 +139,20 @@ suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, vararg model render(name, *modelAttributes).awaitSingle() /** - * Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within - * another suspendable function. + * Coroutines variant of [ServerResponse.BodyBuilder.render]. * * @author Sebastien Deleuze * @since 5.2 */ suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, model: Map): ServerResponse = render(name, model).awaitSingle() + +/** + * Coroutines variant of [ServerResponse.HeadersBuilder.build]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +suspend fun ServerResponse.HeadersBuilder>.buildAndAwait(): ServerResponse = + build().awaitSingle() + diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java index 39076cac07..a54bfc85f4 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/BodyInsertersTests.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Optional; import com.fasterxml.jackson.annotation.JsonView; +import io.reactivex.Single; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Flux; @@ -159,6 +160,51 @@ public class BodyInsertersTests { .verify(); } + @Test + public void ofProducerWithMono() { + Mono body = Mono.just(new User("foo", "bar")); + BodyInserter inserter = BodyInserters.fromProducer(body, User.class); + + MockServerHttpResponse response = new MockServerHttpResponse(); + Mono result = inserter.insert(response, this.context); + StepVerifier.create(result).expectComplete().verify(); + StepVerifier.create(response.getBodyAsString()) + .expectNext("{\"username\":\"foo\",\"password\":\"bar\"}") + .expectComplete() + .verify(); + } + + @Test + public void ofProducerWithFlux() { + Flux body = Flux.just("foo"); + BodyInserter inserter = BodyInserters.fromProducer(body, String.class); + + MockServerHttpResponse response = new MockServerHttpResponse(); + Mono result = inserter.insert(response, this.context); + StepVerifier.create(result).expectComplete().verify(); + StepVerifier.create(response.getBody()) + .consumeNextWith(buf -> { + String actual = DataBufferTestUtils.dumpString(buf, UTF_8); + assertThat(actual).isEqualTo("foo"); + }) + .expectComplete() + .verify(); + } + + @Test + public void ofProducerWithSingle() { + Single body = Single.just(new User("foo", "bar")); + BodyInserter inserter = BodyInserters.fromProducer(body, User.class); + + MockServerHttpResponse response = new MockServerHttpResponse(); + Mono result = inserter.insert(response, this.context); + StepVerifier.create(result).expectComplete().verify(); + StepVerifier.create(response.getBodyAsString()) + .expectNext("{\"username\":\"foo\",\"password\":\"bar\"}") + .expectComplete() + .verify(); + } + @Test public void ofPublisher() { Flux body = Flux.just("foo"); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java index 51b19b0677..deeb681a43 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java @@ -61,7 +61,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration Mono result = webClient .post() .uri("http://localhost:" + this.port + "/multipartData") - .syncBody(generateBody()) + .body(generateBody()) .exchange(); StepVerifier @@ -75,7 +75,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration Mono result = webClient .post() .uri("http://localhost:" + this.port + "/parts") - .syncBody(generateBody()) + .body(generateBody()) .exchange(); StepVerifier @@ -89,7 +89,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration Mono result = webClient .post() .uri("http://localhost:" + this.port + "/transferTo") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -169,7 +169,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration Path tempFile = Files.createTempFile("MultipartIntegrationTests", null); return part.transferTo(tempFile) .then(ServerResponse.ok() - .syncBody(tempFile.toString())); + .body(tempFile.toString())); } catch (Exception e) { return Mono.error(e); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index 99f1a354d3..275d51a3f0 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java @@ -186,7 +186,7 @@ public class DefaultWebClientTests { WebClient client = this.builder.build(); assertThatIllegalArgumentException().isThrownBy(() -> - client.post().uri("https://example.com").syncBody(mono)); + client.post().uri("https://example.com").body(mono)); } @Test diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 12cb207b46..274c545418 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -354,7 +354,7 @@ public class WebClientIntegrationTests { .uri("/pojo/capitalize") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) - .syncBody(new Pojo("foofoo", "barbar")) + .body(new Pojo("foofoo", "barbar")) .retrieve() .bodyToMono(Pojo.class); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java index 81ed266c56..2f0dcc8e0c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultEntityResponseBuilderTests.java @@ -24,6 +24,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Set; +import io.reactivex.Single; import org.junit.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -76,6 +77,14 @@ public class DefaultEntityResponseBuilderTests { assertThat(response.entity()).isSameAs(body); } + @Test + public void fromProducer() { + Single body = Single.just("foo"); + ParameterizedTypeReference typeReference = new ParameterizedTypeReference() {}; + EntityResponse> response = EntityResponse.fromProducer(body, typeReference).build().block(); + assertThat(response.entity()).isSameAs(body); + } + @Test public void status() { String body = "foo"; diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java index 5c0bed8cf1..979abb0302 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilderTests.java @@ -308,7 +308,7 @@ public class DefaultServerResponseBuilderTests { public void copyCookies() { Mono serverResponse = ServerResponse.ok() .cookie(ResponseCookie.from("foo", "bar").build()) - .syncBody("body"); + .body("body"); assertThat(serverResponse.block().cookies().isEmpty()).isFalse(); @@ -360,7 +360,7 @@ public class DefaultServerResponseBuilderTests { Mono mono = Mono.empty(); assertThatIllegalArgumentException().isThrownBy(() -> - ServerResponse.ok().syncBody(mono)); + ServerResponse.ok().body(mono)); } @Test @@ -368,7 +368,7 @@ public class DefaultServerResponseBuilderTests { String etag = "\"foo\""; ServerResponse responseMono = ServerResponse.ok() .eTag(etag) - .syncBody("bar") + .body("bar") .block(); MockServerHttpRequest request = MockServerHttpRequest.get("https://example.com") @@ -392,7 +392,7 @@ public class DefaultServerResponseBuilderTests { ServerResponse responseMono = ServerResponse.ok() .lastModified(oneMinuteBeforeNow) - .syncBody("bar") + .body("bar") .block(); MockServerHttpRequest request = MockServerHttpRequest.get("https://example.com") diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/InvalidHttpMethodIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/InvalidHttpMethodIntegrationTests.java index 206f0f7abf..4257e8f040 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/InvalidHttpMethodIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/InvalidHttpMethodIntegrationTests.java @@ -33,8 +33,8 @@ public class InvalidHttpMethodIntegrationTests extends AbstractRouterFunctionInt @Override protected RouterFunction routerFunction() { return RouterFunctions.route(RequestPredicates.GET("/"), - request -> ServerResponse.ok().syncBody("FOO")) - .andRoute(RequestPredicates.all(), request -> ServerResponse.ok().syncBody("BAR")); + request -> ServerResponse.ok().body("FOO")) + .andRoute(RequestPredicates.all(), request -> ServerResponse.ok().body("BAR")); } @Test diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/NestedRouteIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/NestedRouteIntegrationTests.java index 93873d62e3..a8ad88c02b 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/NestedRouteIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/NestedRouteIntegrationTests.java @@ -125,7 +125,7 @@ public class NestedRouteIntegrationTests extends AbstractRouterFunctionIntegrati public Mono pattern(ServerRequest request) { String pattern = matchingPattern(request).getPatternString(); - return ServerResponse.ok().syncBody(pattern); + return ServerResponse.ok().body(pattern); } @SuppressWarnings("unchecked") diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java index be7dc30f42..0926c28af8 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartIntegrationTests.java @@ -85,7 +85,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/requestPart") - .syncBody(generateBody()) + .body(generateBody()) .exchange(); StepVerifier @@ -99,7 +99,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/requestBodyMap") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -113,7 +113,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/requestBodyFlux") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -127,7 +127,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/filePartFlux") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -141,7 +141,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/filePartMono") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); @@ -155,7 +155,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Flux result = webClient .post() .uri("/transferTo") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToFlux(String.class); @@ -183,7 +183,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes Mono result = webClient .post() .uri("/modelAttribute") - .syncBody(generateBody()) + .body(generateBody()) .retrieve() .bodyToMono(String.class); 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 e8e5b9413f..7d3bf3fb74 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 @@ -27,6 +27,7 @@ import org.junit.Test import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import reactor.core.publisher.Mono +import java.util.concurrent.CompletableFuture /** * Mock object based tests for [WebClient] Kotlin extensions @@ -41,9 +42,9 @@ class WebClientExtensionsTests { @Test - fun `RequestBodySpec#body with Publisher and reified type parameters`() { + fun `RequestBodySpec#bodyWithType with Publisher and reified type parameters`() { val body = mockk>>() - requestBodySpec.body(body) + requestBodySpec.bodyWithType(body) verify { requestBodySpec.body(body, object : ParameterizedTypeReference>() {}) } } @@ -51,8 +52,16 @@ class WebClientExtensionsTests { @FlowPreview fun `RequestBodySpec#body with Flow and reified type parameters`() { val body = mockk>>() - requestBodySpec.body(body) - verify { requestBodySpec.body(ofType>>(), object : ParameterizedTypeReference>() {}) } + requestBodySpec.bodyWithType(body) + verify { requestBodySpec.body(ofType(), object : ParameterizedTypeReference>() {}) } + } + + @Test + @FlowPreview + fun `RequestBodySpec#body with CompletableFuture and reified type parameters`() { + val body = mockk>>() + requestBodySpec.bodyWithType>(body) + verify { requestBodySpec.body(ofType(), object : ParameterizedTypeReference>() {}) } } @Test @@ -83,19 +92,6 @@ class WebClientExtensionsTests { } } - @Test - fun body() { - val headerSpec = mockk>() - val supplier: suspend () -> String = mockk() - every { requestBodySpec.body(ofType>()) } returns headerSpec - runBlocking { - requestBodySpec.body(supplier) - } - verify { - requestBodySpec.body(ofType>()) - } - } - @Test fun awaitBody() { val spec = mockk() 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 b8b8882b9e..00525f3e32 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 @@ -19,6 +19,7 @@ package org.springframework.web.reactive.function.server import io.mockk.every import io.mockk.mockk import io.mockk.verify +import io.reactivex.Flowable import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.runBlocking @@ -28,12 +29,14 @@ import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference import org.springframework.http.MediaType.* import reactor.core.publisher.Mono +import java.util.concurrent.CompletableFuture /** * Mock object based tests for [ServerResponse] Kotlin extensions * * @author Sebastien Deleuze */ +@Suppress("UnassignedFluxMonoInstance") class ServerResponseExtensionsTests { private val bodyBuilder = mockk(relaxed = true) @@ -42,10 +45,51 @@ class ServerResponseExtensionsTests { @Test fun `BodyBuilder#body with Publisher and reified type parameters`() { val body = mockk>>() - bodyBuilder.body(body) + bodyBuilder.bodyWithType(body) verify { bodyBuilder.body(body, object : ParameterizedTypeReference>() {}) } } + @Test + fun `BodyBuilder#body with CompletableFuture and reified type parameters`() { + val body = mockk>>() + bodyBuilder.bodyWithType>(body) + verify { bodyBuilder.body(body, object : ParameterizedTypeReference>() {}) } + } + + @Test + fun `BodyBuilder#body with Flowable and reified type parameters`() { + val body = mockk>>() + bodyBuilder.bodyWithType(body) + verify { bodyBuilder.body(body, object : ParameterizedTypeReference>() {}) } + } + + @Test + fun `BodyBuilder#bodyAndAwait with object parameter`() { + val response = mockk() + val body = "foo" + every { bodyBuilder.body(ofType()) } returns Mono.just(response) + runBlocking { + bodyBuilder.bodyAndAwait(body) + } + verify { + bodyBuilder.body(ofType()) + } + } + + @Test + @FlowPreview + fun `BodyBuilder#bodyAndAwait with flow parameter`() { + val response = mockk() + val body = mockk>>() + every { bodyBuilder.body(ofType>>(), object : ParameterizedTypeReference>() {}) } returns Mono.just(response) + runBlocking { + bodyBuilder.bodyAndAwait(body) + } + verify { + bodyBuilder.body(ofType>>(), object : ParameterizedTypeReference>() {}) + } + } + @Test fun `BodyBuilder#json`() { bodyBuilder.json() @@ -71,42 +115,7 @@ class ServerResponseExtensionsTests { } @Test - fun await() { - val response = mockk() - val builder = mockk>() - every { builder.build() } returns Mono.just(response) - runBlocking { - assertEquals(response, builder.buildAndAwait()) - } - } - - @Test - fun `bodyAndAwait with object parameter`() { - val response = mockk() - val body = "foo" - every { bodyBuilder.syncBody(ofType()) } returns Mono.just(response) - runBlocking { - bodyBuilder.bodyAndAwait(body) - } - verify { - bodyBuilder.syncBody(ofType()) - } - } - - @Test - @FlowPreview - fun bodyFlowAndAwait() { - val response = mockk() - val body = mockk>>() - every { bodyBuilder.body(ofType>>()) } returns Mono.just(response) - runBlocking { - bodyBuilder.bodyFlowAndAwait(body) - } - verify { bodyBuilder.body(ofType>>(), object : ParameterizedTypeReference>() {}) } - } - - @Test - fun `renderAndAwait with a vararg parameter`() { + fun `BodyBuilder#renderAndAwait with a vararg parameter`() { val response = mockk() every { bodyBuilder.render("foo", any(), any()) } returns Mono.just(response) runBlocking { @@ -118,7 +127,7 @@ class ServerResponseExtensionsTests { } @Test - fun `renderAndAwait with a Map parameter`() { + fun `BodyBuilder#renderAndAwait with a Map parameter`() { val response = mockk() val map = mockk>() every { bodyBuilder.render("foo", map) } returns Mono.just(response) @@ -130,5 +139,15 @@ class ServerResponseExtensionsTests { } } + @Test + fun `HeadersBuilder#buildAndAwait`() { + val response = mockk() + val builder = mockk>() + every { builder.build() } returns Mono.just(response) + runBlocking { + assertEquals(response, builder.buildAndAwait()) + } + } + class Foo } diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index 7050c1cacf..f45c0dadd6 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -318,7 +318,8 @@ is closed and is not placed back in the pool. [[webflux-client-body]] == Request Body -The request body can be encoded from an `Object`, as the following example shows: +The request body can be encoded from any asynchronous type handled by `ReactiveAdapterRegistry`, +like `Mono` as the following example shows: [source,java,intent=0] [subs="verbatim,quotes"] @@ -348,7 +349,7 @@ You can also have a stream of objects be encoded, as the following example shows .bodyToMono(Void.class); ---- -Alternatively, if you have the actual value, you can use the `syncBody` shortcut method, +Alternatively, if you have the actual value, you can use the `body` shortcut method, as the following example shows: [source,java,intent=0] @@ -359,7 +360,7 @@ as the following example shows: Mono result = client.post() .uri("/persons/{id}", id) .contentType(MediaType.APPLICATION_JSON) - .syncBody(person) + .body(person) .retrieve() .bodyToMono(Void.class); ---- @@ -380,7 +381,7 @@ content is automatically set to `application/x-www-form-urlencoded` by the Mono result = client.post() .uri("/path", id) - .syncBody(formData) + .body(formData) .retrieve() .bodyToMono(Void.class); ---- @@ -428,7 +429,7 @@ explicitly provide the `MediaType` to use for each part through one of the overl builder `part` methods. Once a `MultiValueMap` is prepared, the easiest way to pass it to the the `WebClient` is -through the `syncBody` method, as the following example shows: +through the `body` method, as the following example shows: [source,java,intent=0] [subs="verbatim,quotes"] @@ -437,7 +438,7 @@ through the `syncBody` method, as the following example shows: Mono result = client.post() .uri("/path", id) - .syncBody(builder.build()) + .body(builder.build()) .retrieve() .bodyToMono(Void.class); ----