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); ----