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 | 73 ++++++++++++-- .../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 | 56 +++++++---- .../function/server/EntityResponse.java | 34 ++++++- .../function/server/ServerResponse.java | 63 ++++++++++-- .../function/client/WebClientExtensions.kt | 74 ++++++++------ .../server/ServerResponseExtensions.kt | 97 ++++++++++++------- .../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, 761 insertions(+), 250 deletions(-) diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index f46320cf9e5..d1e81279ba9 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 a382181e424..3d03e6e5ffe 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 cc2e8ab96fc..9180920bcff 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,12 +626,45 @@ public interface WebTestClient { RequestBodySpec contentType(MediaType contentType); /** - * Set the body of the request to the given {@code BodyInserter}. - * @param inserter the 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 - * @see org.springframework.web.reactive.function.BodyInserters + * @since 5.2 */ - RequestHeadersSpec body(BodyInserter inserter); + 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}. @@ -643,8 +677,26 @@ public interface WebTestClient { > 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 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))
@@ -657,8 +709,13 @@ 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
+		 * @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 d0c09a8cf3e..e70ddee89c3 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 20dcdd649cf..8704ff3266c 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 83d61e8de47..c85ff0b1af2 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 0d0f9c7a65b..6725a0a2673 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 7a1361cd5a6..dda30498fb0 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 faf18f6c6a2..08c23635e51 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 773a6110e0d..752ffca7c96 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 aebeaf2c152..710ad2355bb 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 277c8552460..6432b954182 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 8982cc4f97c..d63f65f1428 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 72442eaf78e..e869dc55c2f 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), 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, Mono.just(body), ResolvableType.forInstance(body)); + 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 81f7bb93fd0..2be82f862b5 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( - P publisher, ParameterizedTypeReference typeReference) { + 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; + } - this.inserter = BodyInserters.fromPublisher(publisher, typeReference); + @Override + public > RequestHeadersSpec body( + 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 1a7f3660768..b5cdfed4c22 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 cabbe2bf045..49e35a9cf39 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,12 +223,9 @@ 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"); - - return new DefaultEntityResponseBuilder<>(publisher, - BodyInserters.fromPublisher(publisher, elementClass)) + public Mono body(Object body) { + return new DefaultEntityResponseBuilder<>(body, + BodyInserters.fromObject(body)) .status(this.statusCode) .headers(this.headers) .cookies(cookies -> cookies.addAll(this.cookies)) @@ -237,14 +235,33 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { } @Override - public > Mono body(P publisher, - ParameterizedTypeReference typeReference) { + 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); + } - Assert.notNull(publisher, "Publisher must not be null"); - Assert.notNull(typeReference, "ParameterizedTypeReference must not be null"); + @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, typeReference)) + BodyInserters.fromPublisher(publisher, elementClass)) .status(this.statusCode) .headers(this.headers) .cookies(cookies -> cookies.addAll(this.cookies)) @@ -254,13 +271,10 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { } @Override - 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)) + public > Mono body(P publisher, + ParameterizedTypeReference elementType) { + return new DefaultEntityResponseBuilder<>(publisher, + BodyInserters.fromPublisher(publisher, elementType)) .status(this.statusCode) .headers(this.headers) .cookies(cookies -> cookies.addAll(this.cookies)) @@ -269,6 +283,12 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { .map(entityResponse -> entityResponse); } + @Override + @Deprecated + public Mono syncBody(Object body) { + return body(body); + } + @Override public Mono body(BodyInserter inserter) { return Mono.just( 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 ab72a30f5ac..152b0387706 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 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 fromObject(T t) { - return new DefaultEntityResponseBuilder<>(t, BodyInserters.fromObject(t)); + 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 286845d9dac..41a2fce19bf 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 ef4edb44290..fed3f652917 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 8ed19bca04a..01b8345f1ac 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,51 +130,29 @@ 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. + * Coroutines variant of [ServerResponse.BodyBuilder.render]. * * @author Sebastien Deleuze * @since 5.2 */ -@FlowPreview -suspend inline fun ServerResponse.BodyBuilder.bodyFlowAndAwait(flow: Flow): ServerResponse = - body(flow.asPublisher(), object : ParameterizedTypeReference() {}).awaitSingle() +suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, vararg modelAttributes: String): ServerResponse = + render(name, *modelAttributes).awaitSingle() /** - * Coroutines variant of [ServerResponse.BodyBuilder.syncBody]. + * Coroutines variant of [ServerResponse.BodyBuilder.render]. * * @author Sebastien Deleuze * @since 5.2 */ -suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse = - syncBody(body).awaitSingle() +suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, model: Map): ServerResponse = + render(name, model).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.HeadersBuilder.build]. * * @author Sebastien Deleuze * @since 5.2 */ -suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, vararg modelAttributes: String): ServerResponse = - render(name, *modelAttributes).awaitSingle() +suspend fun ServerResponse.HeadersBuilder>.buildAndAwait(): ServerResponse = + build().awaitSingle() -/** - * Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within - * another suspendable function. - * - * @author Sebastien Deleuze - * @since 5.2 - */ -suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, model: Map): ServerResponse = - render(name, model).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 39076cac077..a54bfc85f45 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 51b19b06772..deeb681a431 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 99f1a354d3e..275d51a3f09 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 12cb207b46f..274c5454186 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 81ed266c560..2f0dcc8e0c2 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 5c0bed8cf14..979abb03027 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 206f0f7abff..4257e8f0405 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 93873d62e33..a8ad88c02bf 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 be7dc30f424..0926c28af8f 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 e8e5b9413f8..7d3bf3fb74e 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 b8b8882b9e1..00525f3e320 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,71 +45,77 @@ 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#json`() { - bodyBuilder.json() - verify { bodyBuilder.contentType(APPLICATION_JSON) } - } - - @Test - fun `BodyBuilder#xml`() { - bodyBuilder.xml() - verify { bodyBuilder.contentType(APPLICATION_XML) } - } - - @Test - fun `BodyBuilder#html`() { - bodyBuilder.html() - verify { bodyBuilder.contentType(TEXT_HTML) } - } - - @Test - fun `BodyBuilder#sse`() { - bodyBuilder.sse() - verify { bodyBuilder.contentType(TEXT_EVENT_STREAM) } + fun `BodyBuilder#body with CompletableFuture and reified type parameters`() { + val body = mockk>>() + bodyBuilder.bodyWithType>(body) + verify { bodyBuilder.body(body, object : ParameterizedTypeReference>() {}) } } @Test - fun await() { - val response = mockk() - val builder = mockk>() - every { builder.build() } returns Mono.just(response) - runBlocking { - assertEquals(response, builder.buildAndAwait()) - } + fun `BodyBuilder#body with Flowable and reified type parameters`() { + val body = mockk>>() + bodyBuilder.bodyWithType(body) + verify { bodyBuilder.body(body, object : ParameterizedTypeReference>() {}) } } @Test - fun `bodyAndAwait with object parameter`() { + fun `BodyBuilder#bodyAndAwait with object parameter`() { val response = mockk() val body = "foo" - every { bodyBuilder.syncBody(ofType()) } returns Mono.just(response) + every { bodyBuilder.body(ofType()) } returns Mono.just(response) runBlocking { bodyBuilder.bodyAndAwait(body) } verify { - bodyBuilder.syncBody(ofType()) + bodyBuilder.body(ofType()) } } @Test @FlowPreview - fun bodyFlowAndAwait() { + fun `BodyBuilder#bodyAndAwait with flow parameter`() { val response = mockk() val body = mockk>>() - every { bodyBuilder.body(ofType>>()) } returns Mono.just(response) + every { bodyBuilder.body(ofType>>(), object : ParameterizedTypeReference>() {}) } returns Mono.just(response) runBlocking { - bodyBuilder.bodyFlowAndAwait(body) + bodyBuilder.bodyAndAwait(body) + } + verify { + bodyBuilder.body(ofType>>(), object : ParameterizedTypeReference>() {}) } - verify { bodyBuilder.body(ofType>>(), object : ParameterizedTypeReference>() {}) } } @Test - fun `renderAndAwait with a vararg parameter`() { + fun `BodyBuilder#json`() { + bodyBuilder.json() + verify { bodyBuilder.contentType(APPLICATION_JSON) } + } + + @Test + fun `BodyBuilder#xml`() { + bodyBuilder.xml() + verify { bodyBuilder.contentType(APPLICATION_XML) } + } + + @Test + fun `BodyBuilder#html`() { + bodyBuilder.html() + verify { bodyBuilder.contentType(TEXT_HTML) } + } + + @Test + fun `BodyBuilder#sse`() { + bodyBuilder.sse() + verify { bodyBuilder.contentType(TEXT_EVENT_STREAM) } + } + + @Test + 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 7050c1cacfe..f45c0dadd63 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); ----