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<T>() extensions are deprecated in favor of
bodyWithType<T>() ones, including dedicated Publisher<T> and
Flow<T> variants. Coroutines extensions are adapted as well, and
body(Object) can now be used with suspending functions.

Closes gh-23212
This commit is contained in:
Sebastien Deleuze 2019-07-07 21:03:41 +02:00
parent 0fbc9bf461
commit 2b4d6ce354
33 changed files with 781 additions and 270 deletions

View File

@ -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")

View File

@ -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<?, ? super ClientHttpRequest> 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 <T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, ParameterizedTypeReference<T> elementType) {
this.bodySpec.body(publisher, elementType);
return this;
}
@Override
public RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> 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());

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -30,6 +30,7 @@ import org.reactivestreams.Publisher;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -625,26 +626,7 @@ public interface WebTestClient {
RequestBodySpec contentType(MediaType contentType);
/**
* Set the body of the request to the given {@code BodyInserter}.
* @param inserter the inserter
* @return spec for decoding the response
* @see org.springframework.web.reactive.function.BodyInserters
*/
RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter);
/**
* Set the body of the request to the given asynchronous {@code Publisher}.
* @param publisher the request body data
* @param elementClass the class of elements contained in the publisher
* @param <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return spec for decoding the response
*/
<T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> elementClass);
/**
* Set the body of the request to the given synchronous {@code Object} and
* perform the request.
* Set the body of the request to the given {@code Object} and perform the request.
* <p>This method is a convenient shortcut for:
* <pre class="code">
* .body(BodyInserters.fromObject(object))
@ -657,8 +639,83 @@ public interface WebTestClient {
* part with body and headers. The {@code MultiValueMap} can be built
* conveniently using
* @param body the {@code Object} to write to the request
* @return a {@code Mono} with the response
* @return spec for decoding the response
* @since 5.2
*/
RequestHeadersSpec<?> body(Object body);
/**
* Set the body of the request to the given producer.
* @param producer the producer to write to the request. This must be a
* {@link Publisher} or another producer adaptable to a
* {@code Publisher} via {@link ReactiveAdapterRegistry}
* @param elementClass the class of elements contained in the producer
* @return spec for decoding the response
* @since 5.2
*/
RequestHeadersSpec<?> body(Object producer, Class<?> elementClass);
/**
* Set the body of the request to the given producer.
* @param producer the producer to write to the request. This must be a
* {@link Publisher} or another producer adaptable to a
* {@code Publisher} via {@link ReactiveAdapterRegistry}
* @param elementType the type reference of elements contained in the producer
* @return spec for decoding the response
* @since 5.2
*/
RequestHeadersSpec<?> body(Object producer, ParameterizedTypeReference<?> elementType);
/**
* Set the body of the request to the given asynchronous {@code Publisher}.
* @param publisher the request body data
* @param elementClass the class of elements contained in the publisher
* @param <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return spec for decoding the response
*/
<T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> elementClass);
/**
* Set the body of the request to the given asynchronous {@code Publisher}.
* @param publisher the request body data
* @param elementType the type reference of elements contained in the publisher
* @param <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return spec for decoding the response
* @since 5.2
*/
<T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, ParameterizedTypeReference<T> 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<?, ? super ClientHttpRequest> inserter);
/**
* Set the body of the request to the given {@code Object} and perform the request.
* <p>This method is a convenient shortcut for:
* <pre class="code">
* .body(BodyInserters.fromObject(object))
* </pre>
* <p>The body can be a
* {@link org.springframework.util.MultiValueMap MultiValueMap} to create
* a multipart request. The values in the {@code MultiValueMap} can be
* any Object representing the body of the part, or an
* {@link org.springframework.http.HttpEntity HttpEntity} representing a
* part with body and headers. The {@code MultiValueMap} can be built
* conveniently using
* @param body the {@code Object} to write to the request
* @return spec for decoding the response
* @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an
* instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()},
* for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used.
* @deprecated as of Spring Framework 5.2 in favor of {@link #body(Object)}
*/
@Deprecated
RequestHeadersSpec<?> syncBody(Object body);
}

View File

@ -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 <reified T : Any, S : Publisher<T>> RequestBodySpec.body(publisher: S): RequestHeadersSpec<*>
= body(publisher, T::class.java)
= body(publisher, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [RequestBodySpec.body] providing a `bodyWithType<T>(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 <T> the type of the elements contained in the producer
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T : Any> RequestBodySpec.bodyWithType(producer: Any): RequestHeadersSpec<*>
= body(producer, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [RequestBodySpec.body] providing a `bodyWithType(Publisher<T>)` 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 <T> the type of the elements contained in the publisher
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T : Any> RequestBodySpec.bodyWithType(publisher: Publisher<T>): RequestHeadersSpec<*> =
body(publisher, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [RequestBodySpec.body] providing a `bodyWithType(Flow<T>)` 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 <T> the type of the elements contained in the publisher
* @author Sebastien Deleuze
* @since 5.2
*/
@FlowPreview
inline fun <reified T : Any> RequestBodySpec.bodyWithType(flow: Flow<T>): RequestHeadersSpec<*> =
body(flow, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [ResponseSpec.expectBody] providing an `expectBody<Foo>()` variant and
@ -44,13 +88,11 @@ inline fun <reified B : Any> ResponseSpec.expectBody(): KotlinBodySpec<B> =
object : KotlinBodySpec<B> {
override fun isEqualTo(expected: B): KotlinBodySpec<B> = it
.assertWithDiagnostics({ assertEquals("Response body", expected, it.responseBody) })
.let { this }
.assertWithDiagnostics { assertEquals("Response body", expected, it.responseBody) }
.let { this }
override fun consumeWith(consumer: (EntityExchangeResult<B>) -> Unit): KotlinBodySpec<B> =
it
.assertWithDiagnostics({ consumer.invoke(it) })
.let { this }
it.assertWithDiagnostics { consumer.invoke(it) }.let { this }
override fun returnResult(): EntityExchangeResult<B> = it
}
@ -88,7 +130,7 @@ interface KotlinBodySpec<B> {
* @since 5.0
*/
inline fun <reified E : Any> ResponseSpec.expectBodyList(): ListBodySpec<E> =
expectBodyList(E::class.java)
expectBodyList(object : ParameterizedTypeReference<E>() {})
/**
* Extension for [ResponseSpec.returnResult] providing a `returnResult<Foo>()` variant.
@ -98,4 +140,4 @@ inline fun <reified E : Any> ResponseSpec.expectBodyList(): ListBodySpec<E> =
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
inline fun <reified T : Any> ResponseSpec.returnResult(): FluxExchangeResult<T> =
returnResult(T::class.java)
returnResult(object : ParameterizedTypeReference<T>() {})

View File

@ -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();
}

View File

@ -63,7 +63,7 @@ public class ErrorTests {
EntityExchangeResult<Void> result = this.client.post()
.uri("/post")
.contentType(MediaType.APPLICATION_JSON)
.syncBody(new Person("Dan"))
.body(new Person("Dan"))
.exchange()
.expectStatus().isBadRequest()
.expectBody().isEmpty();

View File

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

View File

@ -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")

View File

@ -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")

View File

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

View File

@ -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();
}

View File

@ -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<WebTestClient.RequestBodySpec>(relaxed = true)
private val requestBodySpec = mockk<WebTestClient.RequestBodySpec>(relaxed = true)
val responseSpec = mockk<WebTestClient.ResponseSpec>(relaxed = true)
private val responseSpec = mockk<WebTestClient.ResponseSpec>(relaxed = true)
@Test
fun `RequestBodySpec#body with Publisher and reified type parameters`() {
fun `RequestBodySpec#bodyWithType with Publisher and reified type parameters`() {
val body = mockk<Publisher<Foo>>()
requestBodySpec.body(body)
verify { requestBodySpec.body(body, Foo::class.java) }
requestBodySpec.bodyWithType(body)
verify { requestBodySpec.body(body, object : ParameterizedTypeReference<Foo>() {}) }
}
@Test
@FlowPreview
fun `RequestBodySpec#bodyWithType with Flow and reified type parameters`() {
val body = mockk<Flow<Foo>>()
requestBodySpec.bodyWithType(body)
verify { requestBodySpec.body(body, object : ParameterizedTypeReference<Foo>() {}) }
}
@Test
fun `RequestBodySpec#bodyWithType with CompletableFuture and reified type parameters`() {
val body = mockk<CompletableFuture<Foo>>()
requestBodySpec.bodyWithType<Foo>(body)
verify { requestBodySpec.body(body, object : ParameterizedTypeReference<Foo>() {}) }
}
@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<String>().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<String>().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<String>().returnResult().apply { assertEquals("foo", responseBody) }
}
@ -75,13 +94,13 @@ class WebTestClientExtensionsTests {
@Test
fun `ResponseSpec#expectBodyList with reified type parameters`() {
responseSpec.expectBodyList<Foo>()
verify { responseSpec.expectBodyList(Foo::class.java) }
verify { responseSpec.expectBodyList(object : ParameterizedTypeReference<Foo>() {}) }
}
@Test
fun `ResponseSpec#returnResult with reified type parameters`() {
responseSpec.returnResult<Foo>()
verify { responseSpec.returnResult(Foo::class.java) }
verify { responseSpec.returnResult(object : ParameterizedTypeReference<Foo>() {}) }
}
class Foo

View File

@ -41,7 +41,7 @@ import org.springframework.util.MultiValueMap;
/**
* Builder for the body of a multipart request, producing
* {@code MultiValueMap<String, HttpEntity>}, which can be provided to the
* {@code WebClient} through the {@code syncBody} method.
* {@code WebClient} through the {@code body} method.
*
* Examples:
* <pre class="code">
@ -67,7 +67,7 @@ import org.springframework.util.MultiValueMap;
*
* Mono&lt;Void&gt; result = webClient.post()
* .uri("...")
* .syncBody(multipartBody)
* .body(multipartBody)
* .retrieve()
* .bodyToMono(Void.class)
* </pre>

View File

@ -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<Void, ReactiveHttpOutputMessage> 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.
* <p>Alternatively, consider using the {@code syncBody(Object)} shortcuts on
* <p>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 <T> 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 <T> BodyInserter<T, ReactiveHttpOutputMessage> fromObject(T body) {
Assert.notNull(body, "Body must not be null");
Assert.isNull(registry.getAdapter(body.getClass()), "'body' should be an object, for reactive types use a variant specifying a publisher/producer and its related element type");
return (message, context) ->
writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body));
writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body), null);
}
/**
* Inserter to write the given producer of value(s) which must be a {@link Publisher}
* or another producer adaptable to a {@code Publisher} via
* {@link ReactiveAdapterRegistry}.
* <p>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 <T> 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 <T> BodyInserter<T, ReactiveHttpOutputMessage> 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}.
* <p>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 <T> 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 <T> BodyInserter<T, ReactiveHttpOutputMessage> fromProducer(T producer, ParameterizedTypeReference<?> elementType) {
Assert.notNull(producer, "'producer' must not be null");
Assert.notNull(elementType, "'elementType' must not be null");
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(producer.getClass());
Assert.notNull(adapter, "'producer' type is unknown to ReactiveAdapterRegistry");
return (message, context) ->
writeWithMessageWriters(message, context, producer, ResolvableType.forType(elementType), adapter);
}
/**
@ -102,7 +159,7 @@ public abstract class BodyInserters {
Assert.notNull(publisher, "Publisher must not be null");
Assert.notNull(elementClass, "Element Class must not be null");
return (message, context) ->
writeWithMessageWriters(message, context, publisher, ResolvableType.forClass(elementClass));
writeWithMessageWriters(message, context, publisher, ResolvableType.forClass(elementClass), null);
}
/**
@ -122,7 +179,7 @@ public abstract class BodyInserters {
Assert.notNull(publisher, "Publisher must not be null");
Assert.notNull(typeReference, "ParameterizedTypeReference must not be null");
return (message, context) ->
writeWithMessageWriters(message, context, publisher, ResolvableType.forType(typeReference.getType()));
writeWithMessageWriters(message, context, publisher, ResolvableType.forType(typeReference.getType()), null);
}
/**
@ -145,8 +202,8 @@ public abstract class BodyInserters {
/**
* Inserter to write the given {@code ServerSentEvent} publisher.
* <p>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 <T> 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)}.
* <p>Note that you can also use the {@code syncBody(Object)} method in the
* <p>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}.
* <p>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}.
* <p>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.
* <p>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.
* <p>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 <P extends Publisher<?>, M extends ReactiveHttpOutputMessage> Mono<Void> writeWithMessageWriters(
M outputMessage, BodyInserter.Context context, P body, ResolvableType bodyType) {
private static <M extends ReactiveHttpOutputMessage> Mono<Void> 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)));
}

View File

@ -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<?, ? super ClientHttpRequest> inserter) {
this.inserter = inserter;
public RequestHeadersSpec<?> body(Object body) {
this.inserter = BodyInserters.fromObject(body);
return this;
}
@Override
public RequestHeadersSpec<?> body(Object producer, Class<?> elementClass) {
this.inserter = BodyInserters.fromProducer(producer, elementClass);
return this;
}
@Override
public RequestHeadersSpec<?> body(Object producer, ParameterizedTypeReference<?> elementType) {
this.inserter = BodyInserters.fromProducer(producer, elementType);
return this;
}
@Override
public <T, P extends Publisher<T>> RequestHeadersSpec<?> body(
P publisher, ParameterizedTypeReference<T> typeReference) {
this.inserter = BodyInserters.fromPublisher(publisher, typeReference);
P publisher, ParameterizedTypeReference<T> 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<?, ? super ClientHttpRequest> inserter) {
this.inserter = inserter;
return this;
}
@Override
@Deprecated
public RequestHeadersSpec<?> syncBody(Object body) {
return body(body);
}
@Override
public Mono<ClientResponse> exchange() {
ClientRequest request = (this.inserter != null ?

View File

@ -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;
* </ul>
* <p>For examples with a request body see:
* <ul>
* <li>{@link RequestBodySpec#body(Object) body(Object)}
* <li>{@link RequestBodySpec#body(Publisher, Class) body(Publisher,Class)}
* <li>{@link RequestBodySpec#syncBody(Object) syncBody(Object)}
* <li>{@link RequestBodySpec#body(Object, Class) body(Object,Class)}
* <li>{@link RequestBodySpec#body(BodyInserter) body(BodyInserter)}
* </ul>
*
* @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:
* <p><pre class="code">
* Person person = ... ;
*
* Mono&lt;Void&gt; result = client.post()
* .uri("/persons/{id}", id)
* .contentType(MediaType.APPLICATION_JSON)
* .body(person)
* .retrieve()
* .bodyToMono(Void.class);
* </pre>
* <p>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<?, ? super ClientHttpRequest> inserter);
RequestHeadersSpec<?> body(Object body);
/**
* A shortcut for {@link #body(BodyInserter)} with a
* {@linkplain BodyInserters#fromProducer inserter}.
* For example:
* <p><pre>
* Single&lt;Person&gt; personSingle = ... ;
*
* Mono&lt;Void&gt; result = client.post()
* .uri("/persons/{id}", id)
* .contentType(MediaType.APPLICATION_JSON)
* .body(personSingle, Person.class)
* .retrieve()
* .bodyToMono(Void.class);
* </pre>
* @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:
* <p><pre>
* Mono<Person> personMono = ... ;
* Mono&lt;Person&gt; personMono = ... ;
*
* Mono<Void> result = client.post()
* Mono&lt;Void&gt; 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 <T> the type of the elements contained in the publisher
* @param <P> the type of the {@code Publisher}
* @return this builder
*/
<T, P extends Publisher<T>> RequestHeadersSpec<?> body(P publisher,
ParameterizedTypeReference<T> typeReference);
ParameterizedTypeReference<T> 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<?, ? super ClientHttpRequest> 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);
}

View File

@ -59,6 +59,7 @@ import org.springframework.web.server.ServerWebExchange;
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 5.0
*/
class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
@ -222,10 +223,43 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
}
@Override
public <T, P extends Publisher<T>> Mono<ServerResponse> body(P publisher, Class<T> elementClass) {
Assert.notNull(publisher, "Publisher must not be null");
Assert.notNull(elementClass, "Element Class must not be null");
public Mono<ServerResponse> body(Object body) {
return new DefaultEntityResponseBuilder<>(body,
BodyInserters.fromObject(body))
.status(this.statusCode)
.headers(this.headers)
.cookies(cookies -> cookies.addAll(this.cookies))
.hints(hints -> hints.putAll(this.hints))
.build()
.map(entityResponse -> entityResponse);
}
@Override
public Mono<ServerResponse> body(Object producer, Class<?> elementClass) {
return new DefaultEntityResponseBuilder<>(producer,
BodyInserters.fromProducer(producer, elementClass))
.status(this.statusCode)
.headers(this.headers)
.cookies(cookies -> cookies.addAll(this.cookies))
.hints(hints -> hints.putAll(this.hints))
.build()
.map(entityResponse -> entityResponse);
}
@Override
public Mono<ServerResponse> 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 <T, P extends Publisher<T>> Mono<ServerResponse> body(P publisher, Class<T> elementClass) {
return new DefaultEntityResponseBuilder<>(publisher,
BodyInserters.fromPublisher(publisher, elementClass))
.status(this.statusCode)
@ -238,13 +272,9 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
@Override
public <T, P extends Publisher<T>> Mono<ServerResponse> body(P publisher,
ParameterizedTypeReference<T> typeReference) {
Assert.notNull(publisher, "Publisher must not be null");
Assert.notNull(typeReference, "ParameterizedTypeReference must not be null");
ParameterizedTypeReference<T> elementType) {
return new DefaultEntityResponseBuilder<>(publisher,
BodyInserters.fromPublisher(publisher, typeReference))
BodyInserters.fromPublisher(publisher, elementType))
.status(this.statusCode)
.headers(this.headers)
.cookies(cookies -> cookies.addAll(this.cookies))
@ -254,19 +284,9 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
}
@Override
@Deprecated
public Mono<ServerResponse> syncBody(Object body) {
Assert.notNull(body, "Body must not be null");
Assert.isTrue(!(body instanceof Publisher),
"Please specify the element class by using body(Publisher, Class)");
return new DefaultEntityResponseBuilder<>(body,
BodyInserters.fromObject(body))
.status(this.statusCode)
.headers(this.headers)
.cookies(cookies -> cookies.addAll(this.cookies))
.hints(hints -> hints.putAll(this.hints))
.build()
.map(entityResponse -> entityResponse);
return body(body);
}
@Override

View File

@ -64,12 +64,38 @@ public interface EntityResponse<T> extends ServerResponse {
/**
* Create a builder with the given object.
* @param t the object that represents the body of the response
* @param <T> the type of the elements contained in the publisher
* @param body the object that represents the body of the response
* @param <T> the type of the body
* @return the created builder
*/
static <T> Builder<T> fromObject(T t) {
return new DefaultEntityResponseBuilder<>(t, BodyInserters.fromObject(t));
static <T> Builder<T> 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 <T> Builder<T> 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 <T> Builder<T> fromProducer(T producer,
ParameterizedTypeReference<?> typeReference) {
return new DefaultEntityResponseBuilder<>(producer,
BodyInserters.fromProducer(producer, typeReference));
}
/**

View File

@ -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<Map<String, Object>> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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 <T> the type of the elements contained in the publisher
@ -410,23 +450,28 @@ public interface ServerResponse {
ParameterizedTypeReference<T> 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<ServerResponse> body(BodyInserter<?, ? super ServerHttpResponse> 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<ServerResponse> 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<ServerResponse> body(BodyInserter<?, ? super ServerHttpResponse> inserter);
/**
* Render the template with the given {@code name} using the given {@code modelAttributes}.
* The model attributes are mapped under a

View File

@ -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 <reified T : Any, S : Publisher<T>> RequestBodySpec.body(publisher: S): RequestHeadersSpec<*> =
body(publisher, object : ParameterizedTypeReference<T>() {})
/**
* Coroutines [Flow] based extension for [WebClient.RequestBodySpec.body] providing a
* body(Flow<T>)` 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<T>(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 <T> the type of the elements contained in the producer
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T : Any> RequestBodySpec.bodyWithType(producer: Any): RequestHeadersSpec<*> =
body(producer, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [WebClient.RequestBodySpec.body] providing a `bodyWithType(Publisher<T>)` 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 <T> the type of the elements contained in the publisher
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T : Any> RequestBodySpec.bodyWithType(publisher: Publisher<T>): RequestHeadersSpec<*> =
body(publisher, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [WebClient.RequestBodySpec.body] providing a `bodyWithType(Flow<T>)` 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 <T> the type of the elements contained in the flow
* @author Sebastien Deleuze
* @since 5.2
*/
@FlowPreview
inline fun <reified T : Any, S : Flow<T>> RequestBodySpec.body(flow: S): RequestHeadersSpec<*> =
body(flow.asPublisher(), object : ParameterizedTypeReference<T>() {})
inline fun <reified T : Any> RequestBodySpec.bodyWithType(flow: Flow<T>): RequestHeadersSpec<*> =
body(flow, object : ParameterizedTypeReference<T>() {})
/**
* Coroutines variant of [WebClient.RequestHeadersSpec.exchange].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun RequestHeadersSpec<out RequestHeadersSpec<*>>.awaitExchange(): ClientResponse =
exchange().awaitSingle()
/**
* Extension for [WebClient.ResponseSpec.bodyToMono] providing a `bodyToMono<Foo>()` variant
@ -90,25 +125,6 @@ inline fun <reified T : Any> WebClient.ResponseSpec.bodyToFlux(): Flux<T> =
inline fun <reified T : Any> WebClient.ResponseSpec.bodyToFlow(batchSize: Int = 1): Flow<T> =
bodyToFlux<T>().asFlow(batchSize)
/**
* Coroutines variant of [WebClient.RequestHeadersSpec.exchange].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun RequestHeadersSpec<out RequestHeadersSpec<*>>.awaitExchange(): ClientResponse =
exchange().awaitSingle()
/**
* Coroutines variant of [WebClient.RequestBodySpec.body].
*
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T: Any> RequestBodySpec.body(crossinline supplier: suspend () -> T)
= body(GlobalScope.mono(Dispatchers.Unconfined) { supplier.invoke() })
/**
* Coroutines variant of [WebClient.ResponseSpec.bodyToMono].
*

View File

@ -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 <reified T : Any> ServerResponse.BodyBuilder.body(publisher: Publisher<T>): Mono<ServerResponse> =
body(publisher, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [ServerResponse.BodyBuilder.body] providing a `bodyWithType<T>(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 <T> the type of the elements contained in the producer
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T : Any> ServerResponse.BodyBuilder.bodyWithType(producer: Any): Mono<ServerResponse> =
body(producer, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [ServerResponse.BodyBuilder.body] providing a `bodyWithType(Publisher<T>)` 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 <T> the type of the elements contained in the publisher
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T : Any> ServerResponse.BodyBuilder.bodyWithType(publisher: Publisher<T>): Mono<ServerResponse> =
body(publisher, object : ParameterizedTypeReference<T>() {})
/**
* 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<T>)` 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 <reified T : Any> ServerResponse.BodyBuilder.bodyAndAwait(flow: Flow<T>): ServerResponse =
body(flow, object : ParameterizedTypeReference<T>() {}).awaitSingle()
/**
* Extension for [ServerResponse.BodyBuilder.body] providing a
* `bodyToServerSentEvents(Publisher<T>)` variant. This extension is not subject to type
@ -44,7 +97,7 @@ inline fun <reified T : Any> 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 <reified T : Any> ServerResponse.BodyBuilder.bodyToServerSentEvents(publisher: Publisher<T>): Mono<ServerResponse> =
contentType(MediaType.TEXT_EVENT_STREAM).body(publisher, object : ParameterizedTypeReference<T>() {})
@ -77,38 +130,7 @@ fun ServerResponse.BodyBuilder.html() = contentType(MediaType.TEXT_HTML)
fun ServerResponse.BodyBuilder.sse() = contentType(MediaType.TEXT_EVENT_STREAM)
/**
* Coroutines variant of [ServerResponse.HeadersBuilder.build].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerResponse.HeadersBuilder<out ServerResponse.HeadersBuilder<*>>.buildAndAwait(): ServerResponse =
build().awaitSingle()
/**
* Coroutines [Flow] based extension for [ServerResponse.BodyBuilder.body] providing a
* `bodyFlowAndAwait(Flow<T>)` 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 <reified T : Any> ServerResponse.BodyBuilder.bodyFlowAndAwait(flow: Flow<T>): ServerResponse =
body(flow.asPublisher(), object : ParameterizedTypeReference<T>() {}).awaitSingle()
/**
* Coroutines variant of [ServerResponse.BodyBuilder.syncBody].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerResponse.BodyBuilder.bodyAndAwait(body: Any): ServerResponse =
syncBody(body).awaitSingle()
/**
* Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within
* another suspendable function.
* Coroutines variant of [ServerResponse.BodyBuilder.render].
*
* @author Sebastien Deleuze
* @since 5.2
@ -117,11 +139,20 @@ suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, vararg model
render(name, *modelAttributes).awaitSingle()
/**
* Coroutines variant of [ServerResponse.BodyBuilder.syncBody] without the sync prefix since it is ok to use it within
* another suspendable function.
* Coroutines variant of [ServerResponse.BodyBuilder.render].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerResponse.BodyBuilder.renderAndAwait(name: String, model: Map<String, *>): ServerResponse =
render(name, model).awaitSingle()
/**
* Coroutines variant of [ServerResponse.HeadersBuilder.build].
*
* @author Sebastien Deleuze
* @since 5.2
*/
suspend fun ServerResponse.HeadersBuilder<out ServerResponse.HeadersBuilder<*>>.buildAndAwait(): ServerResponse =
build().awaitSingle()

View File

@ -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<User> body = Mono.just(new User("foo", "bar"));
BodyInserter<?, ReactiveHttpOutputMessage> inserter = BodyInserters.fromProducer(body, User.class);
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> 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<String> body = Flux.just("foo");
BodyInserter<?, ReactiveHttpOutputMessage> inserter = BodyInserters.fromProducer(body, String.class);
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> 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<User> body = Single.just(new User("foo", "bar"));
BodyInserter<?, ReactiveHttpOutputMessage> inserter = BodyInserters.fromProducer(body, User.class);
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> 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<String> body = Flux.just("foo");

View File

@ -61,7 +61,7 @@ public class MultipartIntegrationTests extends AbstractRouterFunctionIntegration
Mono<ClientResponse> 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<ClientResponse> 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<String> 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);

View File

@ -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

View File

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

View File

@ -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<String> body = Single.just("foo");
ParameterizedTypeReference<String> typeReference = new ParameterizedTypeReference<String>() {};
EntityResponse<Single<String>> response = EntityResponse.fromProducer(body, typeReference).build().block();
assertThat(response.entity()).isSameAs(body);
}
@Test
public void status() {
String body = "foo";

View File

@ -308,7 +308,7 @@ public class DefaultServerResponseBuilderTests {
public void copyCookies() {
Mono<ServerResponse> 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<Void> 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")

View File

@ -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

View File

@ -125,7 +125,7 @@ public class NestedRouteIntegrationTests extends AbstractRouterFunctionIntegrati
public Mono<ServerResponse> pattern(ServerRequest request) {
String pattern = matchingPattern(request).getPatternString();
return ServerResponse.ok().syncBody(pattern);
return ServerResponse.ok().body(pattern);
}
@SuppressWarnings("unchecked")

View File

@ -85,7 +85,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<ClientResponse> result = webClient
.post()
.uri("/requestPart")
.syncBody(generateBody())
.body(generateBody())
.exchange();
StepVerifier
@ -99,7 +99,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/requestBodyMap")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);
@ -113,7 +113,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/requestBodyFlux")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);
@ -127,7 +127,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/filePartFlux")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);
@ -141,7 +141,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/filePartMono")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);
@ -155,7 +155,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Flux<String> result = webClient
.post()
.uri("/transferTo")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToFlux(String.class);
@ -183,7 +183,7 @@ public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTes
Mono<String> result = webClient
.post()
.uri("/modelAttribute")
.syncBody(generateBody())
.body(generateBody())
.retrieve()
.bodyToMono(String.class);

View File

@ -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<Publisher<List<Foo>>>()
requestBodySpec.body(body)
requestBodySpec.bodyWithType(body)
verify { requestBodySpec.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@ -51,8 +52,16 @@ class WebClientExtensionsTests {
@FlowPreview
fun `RequestBodySpec#body with Flow and reified type parameters`() {
val body = mockk<Flow<List<Foo>>>()
requestBodySpec.body(body)
verify { requestBodySpec.body(ofType<Publisher<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
requestBodySpec.bodyWithType(body)
verify { requestBodySpec.body(ofType<Any>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
@FlowPreview
fun `RequestBodySpec#body with CompletableFuture and reified type parameters`() {
val body = mockk<CompletableFuture<List<Foo>>>()
requestBodySpec.bodyWithType<List<Foo>>(body)
verify { requestBodySpec.body(ofType<Any>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
@ -83,19 +92,6 @@ class WebClientExtensionsTests {
}
}
@Test
fun body() {
val headerSpec = mockk<WebClient.RequestHeadersSpec<*>>()
val supplier: suspend () -> String = mockk()
every { requestBodySpec.body(ofType<Mono<String>>()) } returns headerSpec
runBlocking {
requestBodySpec.body(supplier)
}
verify {
requestBodySpec.body(ofType<Mono<String>>())
}
}
@Test
fun awaitBody() {
val spec = mockk<WebClient.ResponseSpec>()

View File

@ -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<ServerResponse.BodyBuilder>(relaxed = true)
@ -42,10 +45,51 @@ class ServerResponseExtensionsTests {
@Test
fun `BodyBuilder#body with Publisher and reified type parameters`() {
val body = mockk<Publisher<List<Foo>>>()
bodyBuilder.body(body)
bodyBuilder.bodyWithType(body)
verify { bodyBuilder.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun `BodyBuilder#body with CompletableFuture and reified type parameters`() {
val body = mockk<CompletableFuture<List<Foo>>>()
bodyBuilder.bodyWithType<List<Foo>>(body)
verify { bodyBuilder.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun `BodyBuilder#body with Flowable and reified type parameters`() {
val body = mockk<Flowable<List<Foo>>>()
bodyBuilder.bodyWithType(body)
verify { bodyBuilder.body(body, object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun `BodyBuilder#bodyAndAwait with object parameter`() {
val response = mockk<ServerResponse>()
val body = "foo"
every { bodyBuilder.body(ofType<String>()) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyAndAwait(body)
}
verify {
bodyBuilder.body(ofType<String>())
}
}
@Test
@FlowPreview
fun `BodyBuilder#bodyAndAwait with flow parameter`() {
val response = mockk<ServerResponse>()
val body = mockk<Flow<List<Foo>>>()
every { bodyBuilder.body(ofType<Flow<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyAndAwait(body)
}
verify {
bodyBuilder.body(ofType<Flow<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {})
}
}
@Test
fun `BodyBuilder#json`() {
bodyBuilder.json()
@ -71,42 +115,7 @@ class ServerResponseExtensionsTests {
}
@Test
fun await() {
val response = mockk<ServerResponse>()
val builder = mockk<ServerResponse.HeadersBuilder<*>>()
every { builder.build() } returns Mono.just(response)
runBlocking {
assertEquals(response, builder.buildAndAwait())
}
}
@Test
fun `bodyAndAwait with object parameter`() {
val response = mockk<ServerResponse>()
val body = "foo"
every { bodyBuilder.syncBody(ofType<String>()) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyAndAwait(body)
}
verify {
bodyBuilder.syncBody(ofType<String>())
}
}
@Test
@FlowPreview
fun bodyFlowAndAwait() {
val response = mockk<ServerResponse>()
val body = mockk<Flow<List<Foo>>>()
every { bodyBuilder.body(ofType<Publisher<List<Foo>>>()) } returns Mono.just(response)
runBlocking {
bodyBuilder.bodyFlowAndAwait(body)
}
verify { bodyBuilder.body(ofType<Publisher<List<Foo>>>(), object : ParameterizedTypeReference<List<Foo>>() {}) }
}
@Test
fun `renderAndAwait with a vararg parameter`() {
fun `BodyBuilder#renderAndAwait with a vararg parameter`() {
val response = mockk<ServerResponse>()
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<ServerResponse>()
val map = mockk<Map<String, *>>()
every { bodyBuilder.render("foo", map) } returns Mono.just(response)
@ -130,5 +139,15 @@ class ServerResponseExtensionsTests {
}
}
@Test
fun `HeadersBuilder#buildAndAwait`() {
val response = mockk<ServerResponse>()
val builder = mockk<ServerResponse.HeadersBuilder<*>>()
every { builder.build() } returns Mono.just(response)
runBlocking {
assertEquals(response, builder.buildAndAwait())
}
}
class Foo
}

View File

@ -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<Void> 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<Void> 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<Void> result = client.post()
.uri("/path", id)
.syncBody(builder.build())
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
----