|
|
|
@ -38,8 +38,8 @@ You can also use `WebClient.builder()` with further options: |
|
|
|
|
|
|
|
|
|
|
|
The following example configures <<web-reactive.adoc#webflux-codecs, HTTP codecs>>: |
|
|
|
The following example configures <<web-reactive.adoc#webflux-codecs, HTTP codecs>>: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
ExchangeStrategies strategies = ExchangeStrategies.builder() |
|
|
|
ExchangeStrategies strategies = ExchangeStrategies.builder() |
|
|
|
.codecs(configurer -> { |
|
|
|
.codecs(configurer -> { |
|
|
|
@ -51,12 +51,25 @@ The following example configures <<web-reactive.adoc#webflux-codecs, HTTP codecs |
|
|
|
.exchangeStrategies(strategies) |
|
|
|
.exchangeStrategies(strategies) |
|
|
|
.build(); |
|
|
|
.build(); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val strategies = ExchangeStrategies.builder() |
|
|
|
|
|
|
|
.codecs { |
|
|
|
|
|
|
|
// ... |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
.build() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val client = WebClient.builder() |
|
|
|
|
|
|
|
.exchangeStrategies(strategies) |
|
|
|
|
|
|
|
.build() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
Once built, a `WebClient` instance is immutable. However, you can clone it and build a |
|
|
|
Once built, a `WebClient` instance is immutable. However, you can clone it and build a |
|
|
|
modified copy without affecting the original instance, as the following example shows: |
|
|
|
modified copy without affecting the original instance, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
WebClient client1 = WebClient.builder() |
|
|
|
WebClient client1 = WebClient.builder() |
|
|
|
.filter(filterA).filter(filterB).build(); |
|
|
|
.filter(filterA).filter(filterB).build(); |
|
|
|
@ -68,6 +81,19 @@ modified copy without affecting the original instance, as the following example |
|
|
|
|
|
|
|
|
|
|
|
// client2 has filterA, filterB, filterC, filterD |
|
|
|
// client2 has filterA, filterB, filterC, filterD |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val client1 = WebClient.builder() |
|
|
|
|
|
|
|
.filter(filterA).filter(filterB).build() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val client2 = client1.mutate() |
|
|
|
|
|
|
|
.filter(filterC).filter(filterD).build() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// client1 has filterA, filterB |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// client2 has filterA, filterB, filterC, filterD |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -76,8 +102,8 @@ modified copy without affecting the original instance, as the following example |
|
|
|
|
|
|
|
|
|
|
|
To customize Reactor Netty settings, simple provide a pre-configured `HttpClient`: |
|
|
|
To customize Reactor Netty settings, simple provide a pre-configured `HttpClient`: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...); |
|
|
|
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...); |
|
|
|
|
|
|
|
|
|
|
|
@ -85,6 +111,15 @@ To customize Reactor Netty settings, simple provide a pre-configured `HttpClient |
|
|
|
.clientConnector(new ReactorClientHttpConnector(httpClient)) |
|
|
|
.clientConnector(new ReactorClientHttpConnector(httpClient)) |
|
|
|
.build(); |
|
|
|
.build(); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val httpClient = HttpClient.create().secure { ... } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val webClient = WebClient.builder() |
|
|
|
|
|
|
|
.clientConnector(ReactorClientHttpConnector(httpClient)) |
|
|
|
|
|
|
|
.build() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webflux-client-builder-reactor-resources]] |
|
|
|
[[webflux-client-builder-reactor-resources]] |
|
|
|
@ -102,26 +137,32 @@ application deployed as a WAR), you can declare a Spring-managed bean of type |
|
|
|
Netty global resources are shut down when the Spring `ApplicationContext` is closed, |
|
|
|
Netty global resources are shut down when the Spring `ApplicationContext` is closed, |
|
|
|
as the following example shows: |
|
|
|
as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
@Bean |
|
|
|
@Bean |
|
|
|
public ReactorResourceFactory reactorResourceFactory() { |
|
|
|
public ReactorResourceFactory reactorResourceFactory() { |
|
|
|
return new ReactorResourceFactory(); |
|
|
|
return new ReactorResourceFactory(); |
|
|
|
} |
|
|
|
} |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
fun reactorResourceFactory() = ReactorResourceFactory() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
You can also choose not to participate in the global Reactor Netty resources. However, |
|
|
|
You can also choose not to participate in the global Reactor Netty resources. However, |
|
|
|
in this mode, the burden is on you to ensure that all Reactor Netty client and server |
|
|
|
in this mode, the burden is on you to ensure that all Reactor Netty client and server |
|
|
|
instances use shared resources, as the following example shows: |
|
|
|
instances use shared resources, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
@Bean |
|
|
|
@Bean |
|
|
|
public ReactorResourceFactory resourceFactory() { |
|
|
|
public ReactorResourceFactory resourceFactory() { |
|
|
|
ReactorResourceFactory factory = new ReactorResourceFactory(); |
|
|
|
ReactorResourceFactory factory = new ReactorResourceFactory(); |
|
|
|
factory.setGlobalResources(false); <1> |
|
|
|
factory.setUseGlobalResources(false); // <1> |
|
|
|
return factory; |
|
|
|
return factory; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -133,9 +174,33 @@ instances use shared resources, as the following example shows: |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
ClientHttpConnector connector = |
|
|
|
ClientHttpConnector connector = |
|
|
|
new ReactorClientHttpConnector(resourceFactory(), mapper); <2> |
|
|
|
new ReactorClientHttpConnector(resourceFactory(), mapper); // <2> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return WebClient.builder().clientConnector(connector).build(); // <3> |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
<1> Create resources independent of global ones. |
|
|
|
|
|
|
|
<2> Use the `ReactorClientHttpConnector` constructor with resource factory. |
|
|
|
|
|
|
|
<3> Plug the connector into the `WebClient.Builder`. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
fun resourceFactory() = ReactorResourceFactory().apply { |
|
|
|
|
|
|
|
isUseGlobalResources = false // <1> |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
fun webClient(): WebClient { |
|
|
|
|
|
|
|
|
|
|
|
return WebClient.builder().clientConnector(connector).build(); <3> |
|
|
|
val mapper: (HttpClient) -> HttpClient = { |
|
|
|
|
|
|
|
// Further customizations... |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val connector = ReactorClientHttpConnector(resourceFactory(), mapper) // <2> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return WebClient.builder().clientConnector(connector).build() // <3> |
|
|
|
} |
|
|
|
} |
|
|
|
---- |
|
|
|
---- |
|
|
|
<1> Create resources independent of global ones. |
|
|
|
<1> Create resources independent of global ones. |
|
|
|
@ -148,29 +213,50 @@ instances use shared resources, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
To configure a connection timeout: |
|
|
|
To configure a connection timeout: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
import io.netty.channel.ChannelOption; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HttpClient httpClient = HttpClient.create() |
|
|
|
|
|
|
|
.tcpConfiguration(client -> |
|
|
|
|
|
|
|
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)); |
|
|
|
---- |
|
|
|
---- |
|
|
|
import io.netty.channel.ChannelOption; |
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
import io.netty.channel.ChannelOption |
|
|
|
|
|
|
|
|
|
|
|
HttpClient httpClient = HttpClient.create() |
|
|
|
val httpClient = HttpClient.create() |
|
|
|
.tcpConfiguration(client -> |
|
|
|
.tcpConfiguration { it.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)} |
|
|
|
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)); |
|
|
|
|
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
To configure a read and/or write timeout values: |
|
|
|
To configure a read and/or write timeout values: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
import io.netty.handler.timeout.ReadTimeoutHandler; |
|
|
|
import io.netty.handler.timeout.ReadTimeoutHandler; |
|
|
|
import io.netty.handler.timeout.WriteTimeoutHandler; |
|
|
|
import io.netty.handler.timeout.WriteTimeoutHandler; |
|
|
|
|
|
|
|
|
|
|
|
HttpClient httpClient = HttpClient.create() |
|
|
|
HttpClient httpClient = HttpClient.create() |
|
|
|
.tcpConfiguration(client -> |
|
|
|
.tcpConfiguration(client -> |
|
|
|
client.doOnConnected(conn -> conn |
|
|
|
client.doOnConnected(conn -> conn |
|
|
|
.addHandlerLast(new ReadTimeoutHandler(10)) |
|
|
|
.addHandlerLast(new ReadTimeoutHandler(10)) |
|
|
|
.addHandlerLast(new WriteTimeoutHandler(10)))); |
|
|
|
.addHandlerLast(new WriteTimeoutHandler(10)))); |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
import io.netty.handler.timeout.ReadTimeoutHandler |
|
|
|
|
|
|
|
import io.netty.handler.timeout.WriteTimeoutHandler |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val httpClient = HttpClient.create().tcpConfiguration { |
|
|
|
|
|
|
|
it.doOnConnected { conn -> conn |
|
|
|
|
|
|
|
.addHandlerLast(ReadTimeoutHandler(10)) |
|
|
|
|
|
|
|
.addHandlerLast(WriteTimeoutHandler(10)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -180,8 +266,8 @@ HttpClient httpClient = HttpClient.create() |
|
|
|
|
|
|
|
|
|
|
|
The following example shows how to customize Jetty `HttpClient` settings: |
|
|
|
The following example shows how to customize Jetty `HttpClient` settings: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
HttpClient httpClient = new HttpClient(); |
|
|
|
HttpClient httpClient = new HttpClient(); |
|
|
|
httpClient.setCookieStore(...); |
|
|
|
httpClient.setCookieStore(...); |
|
|
|
@ -189,6 +275,15 @@ The following example shows how to customize Jetty `HttpClient` settings: |
|
|
|
|
|
|
|
|
|
|
|
WebClient webClient = WebClient.builder().clientConnector(connector).build(); |
|
|
|
WebClient webClient = WebClient.builder().clientConnector(connector).build(); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val httpClient = HttpClient() |
|
|
|
|
|
|
|
httpClient.cookieStore = ... |
|
|
|
|
|
|
|
val connector = JettyClientHttpConnector(httpClient) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val webClient = WebClient.builder().clientConnector(connector).build(); |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
By default, `HttpClient` creates its own resources (`Executor`, `ByteBufferPool`, `Scheduler`), |
|
|
|
By default, `HttpClient` creates its own resources (`Executor`, `ByteBufferPool`, `Scheduler`), |
|
|
|
which remain active until the process exits or `stop()` is called. |
|
|
|
which remain active until the process exits or `stop()` is called. |
|
|
|
@ -198,8 +293,8 @@ ensure that the resources are shut down when the Spring `ApplicationContext` is |
|
|
|
declaring a Spring-managed bean of type `JettyResourceFactory`, as the following example |
|
|
|
declaring a Spring-managed bean of type `JettyResourceFactory`, as the following example |
|
|
|
shows: |
|
|
|
shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
@Bean |
|
|
|
@Bean |
|
|
|
public JettyResourceFactory resourceFactory() { |
|
|
|
public JettyResourceFactory resourceFactory() { |
|
|
|
@ -209,12 +304,11 @@ shows: |
|
|
|
@Bean |
|
|
|
@Bean |
|
|
|
public WebClient webClient() { |
|
|
|
public WebClient webClient() { |
|
|
|
|
|
|
|
|
|
|
|
Consumer<HttpClient> customizer = client -> { |
|
|
|
HttpClient httpClient = new HttpClient(); |
|
|
|
// Further customizations... |
|
|
|
// Further customizations... |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ClientHttpConnector connector = |
|
|
|
ClientHttpConnector connector = |
|
|
|
new JettyClientHttpConnector(resourceFactory(), customizer); <1> |
|
|
|
new JettyClientHttpConnector(httpClient, resourceFactory()); <1> |
|
|
|
|
|
|
|
|
|
|
|
return WebClient.builder().clientConnector(connector).build(); <2> |
|
|
|
return WebClient.builder().clientConnector(connector).build(); <2> |
|
|
|
} |
|
|
|
} |
|
|
|
@ -222,7 +316,25 @@ shows: |
|
|
|
<1> Use the `JettyClientHttpConnector` constructor with resource factory. |
|
|
|
<1> Use the `JettyClientHttpConnector` constructor with resource factory. |
|
|
|
<2> Plug the connector into the `WebClient.Builder`. |
|
|
|
<2> Plug the connector into the `WebClient.Builder`. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
fun resourceFactory() = JettyResourceFactory() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Bean |
|
|
|
|
|
|
|
fun webClient(): WebClient { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val httpClient = HttpClient() |
|
|
|
|
|
|
|
// Further customizations... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val connector = JettyClientHttpConnector(httpClient, resourceFactory()) // <1> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return WebClient.builder().clientConnector(connector).build() // <2> |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
<1> Use the `JettyClientHttpConnector` constructor with resource factory. |
|
|
|
|
|
|
|
<2> Plug the connector into the `WebClient.Builder`. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webflux-client-retrieve]] |
|
|
|
[[webflux-client-retrieve]] |
|
|
|
@ -231,8 +343,8 @@ shows: |
|
|
|
The `retrieve()` method is the easiest way to get a response body and decode it. |
|
|
|
The `retrieve()` method is the easiest way to get a response body and decode it. |
|
|
|
The following example shows how to do so: |
|
|
|
The following example shows how to do so: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
WebClient client = WebClient.create("https://example.org"); |
|
|
|
WebClient client = WebClient.create("https://example.org"); |
|
|
|
|
|
|
|
|
|
|
|
@ -241,17 +353,35 @@ The following example shows how to do so: |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(Person.class); |
|
|
|
.bodyToMono(Person.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val client = WebClient.create("https://example.org") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val result = client.get() |
|
|
|
|
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.awaitBody<Person>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
You can also get a stream of objects decoded from the response, as the following example shows: |
|
|
|
You can also get a stream of objects decoded from the response, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
Flux<Quote> result = client.get() |
|
|
|
Flux<Quote> result = client.get() |
|
|
|
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) |
|
|
|
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToFlux(Quote.class); |
|
|
|
.bodyToFlux(Quote.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val result = client.get() |
|
|
|
|
|
|
|
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.bodyToFlow<Quote>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
By default, responses with 4xx or 5xx status codes result in an |
|
|
|
By default, responses with 4xx or 5xx status codes result in an |
|
|
|
`WebClientResponseException` or one of its HTTP status specific sub-classes, such as |
|
|
|
`WebClientResponseException` or one of its HTTP status specific sub-classes, such as |
|
|
|
@ -259,8 +389,8 @@ By default, responses with 4xx or 5xx status codes result in an |
|
|
|
You can also use the `onStatus` method to customize the resulting exception, |
|
|
|
You can also use the `onStatus` method to customize the resulting exception, |
|
|
|
as the following example shows: |
|
|
|
as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
Mono<Person> result = client.get() |
|
|
|
Mono<Person> result = client.get() |
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
@ -269,6 +399,16 @@ as the following example shows: |
|
|
|
.onStatus(HttpStatus::is5xxServerError, response -> ...) |
|
|
|
.onStatus(HttpStatus::is5xxServerError, response -> ...) |
|
|
|
.bodyToMono(Person.class); |
|
|
|
.bodyToMono(Person.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val result = client.get() |
|
|
|
|
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.onStatus(HttpStatus::is4xxClientError) { ... } |
|
|
|
|
|
|
|
.onStatus(HttpStatus::is5xxServerError) { ... } |
|
|
|
|
|
|
|
.awaitBody<Person>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
When `onStatus` is used, if the response is expected to have content, then the `onStatus` |
|
|
|
When `onStatus` is used, if the response is expected to have content, then the `onStatus` |
|
|
|
callback should consume it. If not, the content will be automatically drained to ensure |
|
|
|
callback should consume it. If not, the content will be automatically drained to ensure |
|
|
|
@ -283,25 +423,41 @@ resources are released. |
|
|
|
The `exchange()` method provides more control than the `retrieve` method. The following example is equivalent |
|
|
|
The `exchange()` method provides more control than the `retrieve` method. The following example is equivalent |
|
|
|
to `retrieve()` but also provides access to the `ClientResponse`: |
|
|
|
to `retrieve()` but also provides access to the `ClientResponse`: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
Mono<Person> result = client.get() |
|
|
|
Mono<Person> result = client.get() |
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
.exchange() |
|
|
|
.exchange() |
|
|
|
.flatMap(response -> response.bodyToMono(Person.class)); |
|
|
|
.flatMap(response -> response.bodyToMono(Person.class)); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val result = client.get() |
|
|
|
|
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
|
|
|
|
.awaitExchange() |
|
|
|
|
|
|
|
.awaitBody<Person>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
At this level, you can also create a full `ResponseEntity`: |
|
|
|
At this level, you can also create a full `ResponseEntity`: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
Mono<ResponseEntity<Person>> result = client.get() |
|
|
|
Mono<ResponseEntity<Person>> result = client.get() |
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
.exchange() |
|
|
|
.exchange() |
|
|
|
.flatMap(response -> response.toEntity(Person.class)); |
|
|
|
.flatMap(response -> response.toEntity(Person.class)); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val result = client.get() |
|
|
|
|
|
|
|
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) |
|
|
|
|
|
|
|
.awaitExchange() |
|
|
|
|
|
|
|
.toEntity<Person>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
Note that (unlike `retrieve()`), with `exchange()`, there are no automatic error signals for |
|
|
|
Note that (unlike `retrieve()`), with `exchange()`, there are no automatic error signals for |
|
|
|
4xx and 5xx responses. You have to check the status code and decide how to proceed. |
|
|
|
4xx and 5xx responses. You have to check the status code and decide how to proceed. |
|
|
|
@ -319,10 +475,10 @@ is closed and is not placed back in the pool. |
|
|
|
== Request Body |
|
|
|
== Request Body |
|
|
|
|
|
|
|
|
|
|
|
The request body can be encoded from any asynchronous type handled by `ReactiveAdapterRegistry`, |
|
|
|
The request body can be encoded from any asynchronous type handled by `ReactiveAdapterRegistry`, |
|
|
|
like `Mono` as the following example shows: |
|
|
|
like `Mono` or Kotlin Coroutines `Deferred` as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
Mono<Person> personMono = ... ; |
|
|
|
Mono<Person> personMono = ... ; |
|
|
|
|
|
|
|
|
|
|
|
@ -333,11 +489,23 @@ like `Mono` as the following example shows: |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(Void.class); |
|
|
|
.bodyToMono(Void.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val personDeferred: Deferred<Person> = ... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.post() |
|
|
|
|
|
|
|
.uri("/persons/{id}", id) |
|
|
|
|
|
|
|
.contentType(MediaType.APPLICATION_JSON) |
|
|
|
|
|
|
|
.bodyWithType<Person>(personDeferred) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.awaitBody<Unit>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
You can also have a stream of objects be encoded, as the following example shows: |
|
|
|
You can also have a stream of objects be encoded, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
Flux<Person> personFlux = ... ; |
|
|
|
Flux<Person> personFlux = ... ; |
|
|
|
|
|
|
|
|
|
|
|
@ -348,22 +516,46 @@ You can also have a stream of objects be encoded, as the following example shows |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(Void.class); |
|
|
|
.bodyToMono(Void.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val people: Flow<Person> = ... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.post() |
|
|
|
|
|
|
|
.uri("/persons/{id}", id) |
|
|
|
|
|
|
|
.contentType(MediaType.APPLICATION_JSON) |
|
|
|
|
|
|
|
.bodyWithType(people) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.awaitBody<Unit>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
Alternatively, if you have the actual value, you can use the `body` shortcut method, |
|
|
|
Alternatively, if you have the actual value, you can use the `bodyValue` shortcut method, |
|
|
|
as the following example shows: |
|
|
|
as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
Person person = ... ; |
|
|
|
Person person = ... ; |
|
|
|
|
|
|
|
|
|
|
|
Mono<Void> result = client.post() |
|
|
|
Mono<Void> result = client.post() |
|
|
|
.uri("/persons/{id}", id) |
|
|
|
.uri("/persons/{id}", id) |
|
|
|
.contentType(MediaType.APPLICATION_JSON) |
|
|
|
.contentType(MediaType.APPLICATION_JSON) |
|
|
|
.body(person) |
|
|
|
.bodyValue(person) |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(Void.class); |
|
|
|
.bodyToMono(Void.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val person: Person = ... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.post() |
|
|
|
|
|
|
|
.uri("/persons/{id}", id) |
|
|
|
|
|
|
|
.contentType(MediaType.APPLICATION_JSON) |
|
|
|
|
|
|
|
.bodyValue(person) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.awaitBody<Unit>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -374,22 +566,33 @@ To send form data, you can provide a `MultiValueMap<String, String>` as the body |
|
|
|
content is automatically set to `application/x-www-form-urlencoded` by the |
|
|
|
content is automatically set to `application/x-www-form-urlencoded` by the |
|
|
|
`FormHttpMessageWriter`. The following example shows how to use `MultiValueMap<String, String>`: |
|
|
|
`FormHttpMessageWriter`. The following example shows how to use `MultiValueMap<String, String>`: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
MultiValueMap<String, String> formData = ... ; |
|
|
|
MultiValueMap<String, String> formData = ... ; |
|
|
|
|
|
|
|
|
|
|
|
Mono<Void> result = client.post() |
|
|
|
Mono<Void> result = client.post() |
|
|
|
.uri("/path", id) |
|
|
|
.uri("/path", id) |
|
|
|
.body(formData) |
|
|
|
.bodyValue(formData) |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(Void.class); |
|
|
|
.bodyToMono(Void.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val formData: MultiValueMap<String, String> = ... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.post() |
|
|
|
|
|
|
|
.uri("/path", id) |
|
|
|
|
|
|
|
.bodyValue(formData) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.awaitBody<Unit>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
You can also supply form data in-line by using `BodyInserters`, as the following example shows: |
|
|
|
You can also supply form data in-line by using `BodyInserters`, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
import static org.springframework.web.reactive.function.BodyInserters.*; |
|
|
|
import static org.springframework.web.reactive.function.BodyInserters.*; |
|
|
|
|
|
|
|
|
|
|
|
@ -399,6 +602,17 @@ You can also supply form data in-line by using `BodyInserters`, as the following |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(Void.class); |
|
|
|
.bodyToMono(Void.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
import org.springframework.web.reactive.function.BodyInserters.* |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.post() |
|
|
|
|
|
|
|
.uri("/path", id) |
|
|
|
|
|
|
|
.body(fromFormData("k1", "v1").with("k2", "v2")) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.awaitBody<Unit>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -410,8 +624,8 @@ either `Object` instances that represent part content or `HttpEntity` instances |
|
|
|
headers for a part. `MultipartBodyBuilder` provides a convenient API to prepare a |
|
|
|
headers for a part. `MultipartBodyBuilder` provides a convenient API to prepare a |
|
|
|
multipart request. The following example shows how to create a `MultiValueMap<String, ?>`: |
|
|
|
multipart request. The following example shows how to create a `MultiValueMap<String, ?>`: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
MultipartBodyBuilder builder = new MultipartBodyBuilder(); |
|
|
|
MultipartBodyBuilder builder = new MultipartBodyBuilder(); |
|
|
|
builder.part("fieldPart", "fieldValue"); |
|
|
|
builder.part("fieldPart", "fieldValue"); |
|
|
|
@ -421,6 +635,18 @@ multipart request. The following example shows how to create a `MultiValueMap<St |
|
|
|
|
|
|
|
|
|
|
|
MultiValueMap<String, HttpEntity<?>> parts = builder.build(); |
|
|
|
MultiValueMap<String, HttpEntity<?>> parts = builder.build(); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val builder = MultipartBodyBuilder().apply { |
|
|
|
|
|
|
|
part("fieldPart", "fieldValue") |
|
|
|
|
|
|
|
part("filePart1", new FileSystemResource("...logo.png")) |
|
|
|
|
|
|
|
part("jsonPart", new Person("Jason")) |
|
|
|
|
|
|
|
part("myPart", part) // Part from a server request |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val parts = builder.build() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
In most cases, you do not have to specify the `Content-Type` for each part. The content |
|
|
|
In most cases, you do not have to specify the `Content-Type` for each part. The content |
|
|
|
type is determined automatically based on the `HttpMessageWriter` chosen to serialize it |
|
|
|
type is determined automatically based on the `HttpMessageWriter` chosen to serialize it |
|
|
|
@ -431,8 +657,8 @@ builder `part` methods. |
|
|
|
Once a `MultiValueMap` is prepared, the easiest way to pass it to the the `WebClient` is |
|
|
|
Once a `MultiValueMap` is prepared, the easiest way to pass it to the the `WebClient` is |
|
|
|
through the `body` method, as the following example shows: |
|
|
|
through the `body` method, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
MultipartBodyBuilder builder = ...; |
|
|
|
MultipartBodyBuilder builder = ...; |
|
|
|
|
|
|
|
|
|
|
|
@ -442,6 +668,17 @@ through the `body` method, as the following example shows: |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(Void.class); |
|
|
|
.bodyToMono(Void.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val builder: MultipartBodyBuilder = ... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.post() |
|
|
|
|
|
|
|
.uri("/path", id) |
|
|
|
|
|
|
|
.body(builder.build()) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.awaitBody<Unit>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
If the `MultiValueMap` contains at least one non-`String` value, which could also |
|
|
|
If the `MultiValueMap` contains at least one non-`String` value, which could also |
|
|
|
represent regular form data (that is, `application/x-www-form-urlencoded`), you need not |
|
|
|
represent regular form data (that is, `application/x-www-form-urlencoded`), you need not |
|
|
|
@ -451,8 +688,8 @@ set the `Content-Type` to `multipart/form-data`. This is always the case when us |
|
|
|
As an alternative to `MultipartBodyBuilder`, you can also provide multipart content, |
|
|
|
As an alternative to `MultipartBodyBuilder`, you can also provide multipart content, |
|
|
|
inline-style, through the built-in `BodyInserters`, as the following example shows: |
|
|
|
inline-style, through the built-in `BodyInserters`, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
import static org.springframework.web.reactive.function.BodyInserters.*; |
|
|
|
import static org.springframework.web.reactive.function.BodyInserters.*; |
|
|
|
|
|
|
|
|
|
|
|
@ -462,7 +699,17 @@ inline-style, through the built-in `BodyInserters`, as the following example sho |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(Void.class); |
|
|
|
.bodyToMono(Void.class); |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
import org.springframework.web.reactive.function.BodyInserters.* |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.post() |
|
|
|
|
|
|
|
.uri("/path", id) |
|
|
|
|
|
|
|
.body(fromMultipartData("fieldPart", "value").with("filePart", resource)) |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.awaitBody<Unit>() |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -472,74 +719,115 @@ inline-style, through the built-in `BodyInserters`, as the following example sho |
|
|
|
You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder` |
|
|
|
You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder` |
|
|
|
in order to intercept and modify requests, as the following example shows: |
|
|
|
in order to intercept and modify requests, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
WebClient client = WebClient.builder() |
|
|
|
WebClient client = WebClient.builder() |
|
|
|
.filter((request, next) -> { |
|
|
|
.filter((request, next) -> { |
|
|
|
|
|
|
|
|
|
|
|
ClientRequest filtered = ClientRequest.from(request) |
|
|
|
ClientRequest filtered = ClientRequest.from(request) |
|
|
|
.header("foo", "bar") |
|
|
|
.header("foo", "bar") |
|
|
|
.build(); |
|
|
|
.build(); |
|
|
|
|
|
|
|
|
|
|
|
return next.exchange(filtered); |
|
|
|
return next.exchange(filtered); |
|
|
|
}) |
|
|
|
}) |
|
|
|
.build(); |
|
|
|
.build(); |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val client = WebClient.builder() |
|
|
|
|
|
|
|
.filter { request, next -> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val filtered = ClientRequest.from(request) |
|
|
|
|
|
|
|
.header("foo", "bar") |
|
|
|
|
|
|
|
.build() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
next.exchange(filtered) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
.build() |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
This can be used for cross-cutting concerns, such as authentication. The following example uses |
|
|
|
This can be used for cross-cutting concerns, such as authentication. The following example uses |
|
|
|
a filter for basic authentication through a static factory method: |
|
|
|
a filter for basic authentication through a static factory method: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; |
|
|
|
|
|
|
|
|
|
|
|
// static import of ExchangeFilterFunctions.basicAuthentication |
|
|
|
WebClient client = WebClient.builder() |
|
|
|
|
|
|
|
.filter(basicAuthentication("user", "password")) |
|
|
|
|
|
|
|
.build(); |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication |
|
|
|
|
|
|
|
|
|
|
|
WebClient client = WebClient.builder() |
|
|
|
val client = WebClient.builder() |
|
|
|
.filter(basicAuthentication("user", "password")) |
|
|
|
.filter(basicAuthentication("user", "password")) |
|
|
|
.build(); |
|
|
|
.build() |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
Filters apply globally to every request. To change a filter's behavior for a specific |
|
|
|
Filters apply globally to every request. To change a filter's behavior for a specific |
|
|
|
request, you can add request attributes to the `ClientRequest` that can then be accessed |
|
|
|
request, you can add request attributes to the `ClientRequest` that can then be accessed |
|
|
|
by all filters in the chain, as the following example shows: |
|
|
|
by all filters in the chain, as the following example shows: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
WebClient client = WebClient.builder() |
|
|
|
WebClient client = WebClient.builder() |
|
|
|
.filter((request, next) -> { |
|
|
|
.filter((request, next) -> { |
|
|
|
Optional<Object> usr = request.attribute("myAttribute"); |
|
|
|
Optional<Object> usr = request.attribute("myAttribute"); |
|
|
|
// ... |
|
|
|
// ... |
|
|
|
}) |
|
|
|
}) |
|
|
|
.build(); |
|
|
|
.build(); |
|
|
|
|
|
|
|
|
|
|
|
client.get().uri("https://example.org/") |
|
|
|
client.get().uri("https://example.org/") |
|
|
|
.attribute("myAttribute", "...") |
|
|
|
.attribute("myAttribute", "...") |
|
|
|
.retrieve() |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(Void.class); |
|
|
|
.bodyToMono(Void.class); |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val client = WebClient.builder() |
|
|
|
|
|
|
|
.filter { request, _ -> |
|
|
|
|
|
|
|
val usr = request.attributes()["myAttribute"]; |
|
|
|
|
|
|
|
// ... |
|
|
|
|
|
|
|
}.build() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
client.get().uri("https://example.org/") |
|
|
|
|
|
|
|
.attribute("myAttribute", "...") |
|
|
|
|
|
|
|
.retrieve() |
|
|
|
|
|
|
|
.awaitBody<Unit>() |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
You can also replicate an existing `WebClient`, insert new filters, or remove already |
|
|
|
You can also replicate an existing `WebClient`, insert new filters, or remove already |
|
|
|
registered filters. The following example, inserts a basic authentication filter at |
|
|
|
registered filters. The following example, inserts a basic authentication filter at |
|
|
|
index 0: |
|
|
|
index 0: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; |
|
|
|
|
|
|
|
|
|
|
|
// static import of ExchangeFilterFunctions.basicAuthentication |
|
|
|
WebClient client = webClient.mutate() |
|
|
|
|
|
|
|
.filters(filterList -> { |
|
|
|
WebClient client = webClient.mutate() |
|
|
|
filterList.add(0, basicAuthentication("user", "password")); |
|
|
|
.filters(filterList -> { |
|
|
|
}) |
|
|
|
filterList.add(0, basicAuthentication("user", "password")); |
|
|
|
.build(); |
|
|
|
}) |
|
|
|
---- |
|
|
|
.build(); |
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val client = webClient.mutate() |
|
|
|
|
|
|
|
.filters { it.add(0, basicAuthentication("user", "password")) } |
|
|
|
|
|
|
|
.build() |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -548,38 +836,69 @@ WebClient client = webClient.mutate() |
|
|
|
|
|
|
|
|
|
|
|
`WebClient` can be used in synchronous style by blocking at the end for the result: |
|
|
|
`WebClient` can be used in synchronous style by blocking at the end for the result: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
Person person = client.get().uri("/person/{id}", i).retrieve() |
|
|
|
Person person = client.get().uri("/person/{id}", i).retrieve() |
|
|
|
.bodyToMono(Person.class) |
|
|
|
.bodyToMono(Person.class) |
|
|
|
.block(); |
|
|
|
.block(); |
|
|
|
|
|
|
|
|
|
|
|
List<Person> persons = client.get().uri("/persons").retrieve() |
|
|
|
List<Person> persons = client.get().uri("/persons").retrieve() |
|
|
|
.bodyToFlux(Person.class) |
|
|
|
.bodyToFlux(Person.class) |
|
|
|
.collectList() |
|
|
|
.collectList() |
|
|
|
.block(); |
|
|
|
.block(); |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val person = runBlocking { |
|
|
|
|
|
|
|
client.get().uri("/person/{id}", i).retrieve() |
|
|
|
|
|
|
|
.awaitBody<Person>() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val persons = runBlocking { |
|
|
|
|
|
|
|
client.get().uri("/persons").retrieve() |
|
|
|
|
|
|
|
.bodyToFlow<Person>() |
|
|
|
|
|
|
|
.toList() |
|
|
|
|
|
|
|
} |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
However if multiple calls need to be made, it's more efficient to avoid blocking on each |
|
|
|
However if multiple calls need to be made, it's more efficient to avoid blocking on each |
|
|
|
response individually, and instead wait for the combined result: |
|
|
|
response individually, and instead wait for the combined result: |
|
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0] |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
|
|
[subs="verbatim,quotes"] |
|
|
|
.Java |
|
|
|
---- |
|
|
|
---- |
|
|
|
Mono<Person> personMono = client.get().uri("/person/{id}", personId) |
|
|
|
Mono<Person> personMono = client.get().uri("/person/{id}", personId) |
|
|
|
.retrieve().bodyToMono(Person.class); |
|
|
|
.retrieve().bodyToMono(Person.class); |
|
|
|
|
|
|
|
|
|
|
|
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId) |
|
|
|
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId) |
|
|
|
.retrieve().bodyToFlux(Hobby.class).collectList(); |
|
|
|
.retrieve().bodyToFlux(Hobby.class).collectList(); |
|
|
|
|
|
|
|
|
|
|
|
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> { |
|
|
|
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> { |
|
|
|
Map<String, String> map = new LinkedHashMap<>(); |
|
|
|
Map<String, String> map = new LinkedHashMap<>(); |
|
|
|
map.put("person", personName); |
|
|
|
map.put("person", person); |
|
|
|
map.put("hobbies", hobbies); |
|
|
|
map.put("hobbies", hobbies); |
|
|
|
return map; |
|
|
|
return map; |
|
|
|
}) |
|
|
|
}) |
|
|
|
.block(); |
|
|
|
.block(); |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
|
|
|
|
|
|
.Kotlin |
|
|
|
|
|
|
|
---- |
|
|
|
|
|
|
|
val data = runBlocking { |
|
|
|
|
|
|
|
val personDeferred = async { |
|
|
|
|
|
|
|
client.get().uri("/person/{id}", personId) |
|
|
|
|
|
|
|
.retrieve().awaitBody<Person>() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val hobbiesDeferred = async { |
|
|
|
|
|
|
|
client.get().uri("/person/{id}/hobbies", personId) |
|
|
|
|
|
|
|
.retrieve().bodyToFlow<Hobby>().toList() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await()) |
|
|
|
|
|
|
|
} |
|
|
|
---- |
|
|
|
---- |
|
|
|
|
|
|
|
|
|
|
|
The above is merely one example. There are lots of other patterns and operators for putting |
|
|
|
The above is merely one example. There are lots of other patterns and operators for putting |
|
|
|
@ -588,8 +907,10 @@ inter-dependent, without ever blocking until the end. |
|
|
|
|
|
|
|
|
|
|
|
[NOTE] |
|
|
|
[NOTE] |
|
|
|
==== |
|
|
|
==== |
|
|
|
You should never have to block in a Spring MVC controller. Simply return the resulting |
|
|
|
With `Flux` or `Mono`, you should never have to block in a Spring MVC or Spring WebFlux controller. |
|
|
|
`Flux` or `Mono` from the controller method. |
|
|
|
Simply return the resulting reactive type from the controller method. The same principle apply to |
|
|
|
|
|
|
|
Kotlin Coroutines and Spring WebFlux, just use suspending function or return `Flow` in your |
|
|
|
|
|
|
|
controller method . |
|
|
|
==== |
|
|
|
==== |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|