WebClient method to populate the Reactor Context
The alternative is to use a filter but this makes it a little easier and also guarantees that it will be downstream from all filters regardless of their order, and therefore the Context will be visible to all of them. Closes gh-25710
This commit is contained in:
parent
bd2640a9d6
commit
79f79e9306
|
@ -33,6 +33,7 @@ import java.util.function.Supplier;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.context.Context;
|
||||||
|
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
@ -173,6 +174,9 @@ class DefaultWebClient implements WebClient {
|
||||||
|
|
||||||
private final Map<String, Object> attributes = new LinkedHashMap<>(4);
|
private final Map<String, Object> attributes = new LinkedHashMap<>(4);
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Function<Context, Context> contextModifier;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private Consumer<ClientHttpRequest> httpRequestConsumer;
|
private Consumer<ClientHttpRequest> httpRequestConsumer;
|
||||||
|
|
||||||
|
@ -298,6 +302,13 @@ class DefaultWebClient implements WebClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestBodySpec context(Function<Context, Context> contextModifier) {
|
||||||
|
this.contextModifier = (this.contextModifier != null ?
|
||||||
|
this.contextModifier.andThen(contextModifier) : contextModifier);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestBodySpec httpRequest(Consumer<ClientHttpRequest> requestConsumer) {
|
public RequestBodySpec httpRequest(Consumer<ClientHttpRequest> requestConsumer) {
|
||||||
this.httpRequestConsumer = (this.httpRequestConsumer != null ?
|
this.httpRequestConsumer = (this.httpRequestConsumer != null ?
|
||||||
|
@ -412,9 +423,15 @@ class DefaultWebClient implements WebClient {
|
||||||
ClientRequest request = (this.inserter != null ?
|
ClientRequest request = (this.inserter != null ?
|
||||||
initRequestBuilder().body(this.inserter).build() :
|
initRequestBuilder().body(this.inserter).build() :
|
||||||
initRequestBuilder().build());
|
initRequestBuilder().build());
|
||||||
return Mono.defer(() -> exchangeFunction.exchange(request)
|
return Mono.defer(() -> {
|
||||||
|
Mono<ClientResponse> responseMono = exchangeFunction.exchange(request)
|
||||||
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
|
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
|
||||||
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR));
|
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR);
|
||||||
|
if (this.contextModifier != null) {
|
||||||
|
responseMono = responseMono.contextWrite(this.contextModifier);
|
||||||
|
}
|
||||||
|
return responseMono;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientRequest.Builder initRequestBuilder() {
|
private ClientRequest.Builder initRequestBuilder() {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import java.util.function.Predicate;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.context.Context;
|
||||||
|
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.core.ReactiveAdapterRegistry;
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
|
@ -470,6 +471,17 @@ public interface WebClient {
|
||||||
*/
|
*/
|
||||||
S attributes(Consumer<Map<String, Object>> attributesConsumer);
|
S attributes(Consumer<Map<String, Object>> attributesConsumer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a function to populate the Reactor {@code Context}. In contrast
|
||||||
|
* to {@link #attribute(String, Object) attributes} which apply only to
|
||||||
|
* the current request, the Reactor {@code Context} transparently propagates
|
||||||
|
* to the downstream processing chain which may include other nested or
|
||||||
|
* successive calls over HTTP or via other reactive clients.
|
||||||
|
* @param contextModifier the function to modify the context with
|
||||||
|
* @since 5.3.1
|
||||||
|
*/
|
||||||
|
S context(Function<Context, Context> contextModifier);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for access to the {@link ClientHttpRequest} that in turn
|
* Callback for access to the {@link ClientHttpRequest} that in turn
|
||||||
* provides access to the native request of the underlying HTTP library.
|
* provides access to the native request of the underlying HTTP library.
|
||||||
|
|
|
@ -129,6 +129,34 @@ public class DefaultWebClientTests {
|
||||||
assertThat(request.cookies().getFirst("id")).isEqualTo("123");
|
assertThat(request.cookies().getFirst("id")).isEqualTo("123");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void contextFromThreadLocal() {
|
||||||
|
WebClient client = this.builder
|
||||||
|
.filter((request, next) ->
|
||||||
|
// Async, continue on different thread
|
||||||
|
Mono.delay(Duration.ofMillis(10)).then(next.exchange(request)))
|
||||||
|
.filter((request, next) ->
|
||||||
|
Mono.deferContextual(contextView -> {
|
||||||
|
String fooValue = contextView.get("foo");
|
||||||
|
return next.exchange(ClientRequest.from(request).header("foo", fooValue).build());
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ThreadLocal<String> fooHolder = new ThreadLocal<>();
|
||||||
|
fooHolder.set("bar");
|
||||||
|
try {
|
||||||
|
client.get().uri("/path")
|
||||||
|
.context(context -> context.put("foo", fooHolder.get()))
|
||||||
|
.retrieve().bodyToMono(Void.class).block(Duration.ofSeconds(10));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
fooHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientRequest request = verifyAndGetRequest();
|
||||||
|
assertThat(request.headers().getFirst("foo")).isEqualTo("bar");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void httpRequest() {
|
public void httpRequest() {
|
||||||
this.builder.build().get().uri("/path")
|
this.builder.build().get().uri("/path")
|
||||||
|
@ -196,8 +224,6 @@ public class DefaultWebClientTests {
|
||||||
request = verifyAndGetRequest();
|
request = verifyAndGetRequest();
|
||||||
assertThat(request.headers().getFirst("Accept")).isEqualTo("application/xml");
|
assertThat(request.headers().getFirst("Accept")).isEqualTo("application/xml");
|
||||||
assertThat(request.cookies().getFirst("id")).isEqualTo("456");
|
assertThat(request.cookies().getFirst("id")).isEqualTo("456");
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -831,7 +831,7 @@ inline-style, through the built-in `BodyInserters`, as the following example sho
|
||||||
|
|
||||||
|
|
||||||
[[webflux-client-filter]]
|
[[webflux-client-filter]]
|
||||||
== Client Filters
|
== Filters
|
||||||
|
|
||||||
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:
|
||||||
|
@ -887,9 +887,36 @@ a filter for basic authentication through a static factory method:
|
||||||
.build()
|
.build()
|
||||||
----
|
----
|
||||||
|
|
||||||
Filters apply globally to every request. To change a filter's behavior for a specific
|
You can create a new `WebClient` instance by using another as a starting point. This allows
|
||||||
request, you can add request attributes to the `ClientRequest` that can then be accessed
|
insert or removing filters without affecting the original `WebClient`. Below is an example
|
||||||
by all filters in the chain, as the following example shows:
|
that inserts a basic authentication filter at index 0:
|
||||||
|
|
||||||
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||||
|
.Java
|
||||||
|
----
|
||||||
|
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
|
||||||
|
|
||||||
|
WebClient client = webClient.mutate()
|
||||||
|
.filters(filterList -> {
|
||||||
|
filterList.add(0, basicAuthentication("user", "password"));
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
----
|
||||||
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||||
|
.Kotlin
|
||||||
|
----
|
||||||
|
val client = webClient.mutate()
|
||||||
|
.filters { it.add(0, basicAuthentication("user", "password")) }
|
||||||
|
.build()
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
[[webflux-client-attributes]]
|
||||||
|
== Attributes
|
||||||
|
|
||||||
|
You can add attributes to a request. This is convenient if you want to pass information
|
||||||
|
through the filter chain and influence the behavior of filters for a given request.
|
||||||
|
For example:
|
||||||
|
|
||||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||||
.Java
|
.Java
|
||||||
|
@ -915,7 +942,8 @@ by all filters in the chain, as the following example shows:
|
||||||
.filter { request, _ ->
|
.filter { request, _ ->
|
||||||
val usr = request.attributes()["myAttribute"];
|
val usr = request.attributes()["myAttribute"];
|
||||||
// ...
|
// ...
|
||||||
}.build()
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
client.get().uri("https://example.org/")
|
client.get().uri("https://example.org/")
|
||||||
.attribute("myAttribute", "...")
|
.attribute("myAttribute", "...")
|
||||||
|
@ -923,29 +951,44 @@ by all filters in the chain, as the following example shows:
|
||||||
.awaitBody<Unit>()
|
.awaitBody<Unit>()
|
||||||
----
|
----
|
||||||
|
|
||||||
You can also replicate an existing `WebClient`, insert new filters, or remove already
|
|
||||||
registered filters. The following example, inserts a basic authentication filter at
|
[[webflux-client-context]]
|
||||||
index 0:
|
== Context
|
||||||
|
|
||||||
|
<<webflux-client-attributes>> provide a convenient way to pass information to the filter
|
||||||
|
chain but they only influence the current request. If you want to pass information that
|
||||||
|
propagates to additional requests that are nested, e.g. via `flatMap`, or executed after,
|
||||||
|
e.g. via `concatMap`, then you'll need to use the Reactor `Context`.
|
||||||
|
|
||||||
|
`WebClient` exposes a method to populate the Reactor `Context` for a given request.
|
||||||
|
This information is available to filters for the current request and it also propagates
|
||||||
|
to subsequent requests or other reactive clients participating in the downstream
|
||||||
|
processing chain. For example:
|
||||||
|
|
||||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||||
.Java
|
.Java
|
||||||
----
|
----
|
||||||
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
|
WebClient client = WebClient.builder()
|
||||||
|
.filter((request, next) ->
|
||||||
WebClient client = webClient.mutate()
|
Mono.deferContextual(contextView -> {
|
||||||
.filters(filterList -> {
|
String value = contextView.get("foo");
|
||||||
filterList.add(0, basicAuthentication("user", "password"));
|
// ...
|
||||||
})
|
}))
|
||||||
.build();
|
.build();
|
||||||
----
|
|
||||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
client.get().uri("https://example.org/")
|
||||||
.Kotlin
|
.context(context -> context.put("foo", ...))
|
||||||
----
|
.retrieve()
|
||||||
val client = webClient.mutate()
|
.bodyToMono(String.class)
|
||||||
.filters { it.add(0, basicAuthentication("user", "password")) }
|
.flatMap(body -> {
|
||||||
.build()
|
// perform nested request (context propagates automatically)...
|
||||||
|
});
|
||||||
----
|
----
|
||||||
|
|
||||||
|
Note that you can also specify how to populate the context through the `defaultRequest`
|
||||||
|
method at the level of the `WebClient.Builder` and that applies to all requests.
|
||||||
|
This could be used for to example to pass information from `ThreadLocal` storage onto
|
||||||
|
a Reactor processing chain in a Spring MVC application.
|
||||||
|
|
||||||
|
|
||||||
[[webflux-client-synchronous]]
|
[[webflux-client-synchronous]]
|
||||||
|
|
Loading…
Reference in New Issue