Spring WebFlux contiene un cliente para realizar solicitudes HTTP. WebClient tiene una API fluida y funcional basada en Reactor que le permite componer lógica asincrónica de forma declarativa sin la necesidad de lidiar con subprocesos o concurrencia. Es completamente sin bloqueo, admite transmisión y se basa en los mismos códecs utilizados para codificar y decodificar el contenido de las solicitudes y respuestas en el lado del servidor.

WebClient requiere un Biblioteca de cliente HTTP para ejecutar solicitudes. Hay soporte integrado para:

Configuración

El La forma más sencilla de crear WebClient es utilizar uno de los métodos estáticos de fábrica:

  • WebClient.create()

  • WebClient.create(String baseUrl)

También puedes usar WebClient.builder() con parámetros adicionales:

  • uriBuilderFactory: UriBuilderFactory configurado para ser utilizado como URL base.< /p>

  • defaultUriVariables: valores predeterminados que se utilizarán al expandir las plantillas de URI.

  • defaultHeader: encabezados para cada solicitud.

  • defaultCookie: cookies para cada solicitud.

  • defaultRequest: Consumidor para configurar cada solicitud.

  • filter: Filtro de cliente para cada solicitud.

  • exchangeStrategies: Configuración para leer/escribir mensajes HTTP.

  • clientConnector: configuración de la biblioteca HTTP del cliente.

Por ejemplo

Java

WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
Kotlin
val webClient = WebClient.builder()
.codecs { configurer -> ... }
.build()

Una vez creado, el WebClient es inmutable. Sin embargo, puedes clonarlo y crear una copia modificada de la siguiente manera:

Java

WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// cliente1 tiene filterA, filterB
// cliente2 tiene filterA, filterB, filterC, filterD
Kotlin

val client1 = WebClient.builder()
.filter(filterA).filter(filterB).build()
val client2 = client1.mutate()
.filter(filterC).filter(filterD).build()
// cliente1 tiene filterA, filterB
// cliente2 tiene filterA, filterB, filterC, filterD

MaxInMemorySize

Los códecs tienen limitaciones en el almacenamiento intermedio de datos en la memoria para evitar problemas de memoria de la aplicación. Por defecto están configurados en 256 KB. Si esto no es suficiente, se recibirá el siguiente error:

org.springframework.core.io.buffer.DataBufferLimitException: se superó el límite de bytes máximos en el búfer

Para cambiar el límite predeterminado para códecs, haga lo siguiente:

Java
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
Kotlin
val webClient = WebClient.builder()
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
.build()

Reactor Netty

Para configurar los parámetros de Reactor Netty, proporcione una configuración preconfigurada. HttpClient:

Java
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Kotlin
val httpClient = HttpClient.create().secure { ... }
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()

Recursos

De forma predeterminada, HttpClient participa en el uso de los recursos globales de Reactor Netty almacenados en reactor.netty.http.HttpResources, incluidos los subprocesos y el grupo de bucles de eventos. conexiones. Se recomienda este modo porque, a efectos de simultaneidad en los bucles de espera de eventos, es preferible utilizar recursos fijos compartidos. En este modo, los recursos globales permanecen activos hasta que finaliza el proceso.

Si el servidor está sincronizado con el proceso, normalmente no hay necesidad de finalizar el proceso explícitamente. Sin embargo, si el servidor se puede iniciar o detener durante el proceso (como es el caso de una aplicación Spring MVC implementada como un archivo WAR), entonces puede declarar un bean administrado Spring de tipo ReactorResourceFactory con el parámetro globalResources=true (predeterminado) para que se garantice que el uso de los recursos globales de Reactor Netty finalice cuando ApplicationContext se cierre desde Spring, como se muestra en el siguiente ejemplo:

Java
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
Kotlin
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()

También puede evitar el uso de recursos globales de Reactor Netty. Sin embargo, en este modo, usted es responsable de garantizar que todas las instancias del cliente y servidor Reactor Netty compartan recursos, como se muestra en el siguiente ejemplo:

Java
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false); 
return factory;
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// Configuración adicional...
};
ClientHttpConnector connector =
    new ReactorClientHttpConnector(resourceFactory(), mapper); 
return WebClient.builder().clientConnector(connector).build(); 
}
  1. Creamos recursos independientes de global.
  2. Utilice el constructor ReactorClientHttpConnector con la fábrica de recursos.
  3. Conecte el conector a WebClient.Builder.
Kotlin
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
isUseGlobalResources = false 
}
@Bean
fun webClient(): WebClient {
val mapper: (HttpClient) -> HttpClient = {
// Configuración adicional...
}
val connector = ReactorClientHttpConnector(resourceFactory(), mapper) 
return WebClient.builder().clientConnector(connector).build() 
}
  1. Creamos recursos independientes de global.
  2. Utilice el constructor ReactorClientHttpConnector con la fábrica de recursos.
  3. Conecte el conector a WebClient.Builder.

Tiempo de espera

Configuración de valores de tiempo de espera de conexión:

Java
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Kotlin
import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
val webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();

Configuración del valor de tiempo de espera de lectura y escritura:

Java

import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.doOnConnected(conn -> conn
        .addHandlerLast(new ReadTimeoutHandler(10))
        .addHandlerLast(new WriteTimeoutHandler(10)));
// Crear un cliente web...
Kotlin
 
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
val httpClient = HttpClient.create()
.doOnConnected { conn -> conn
        .addHandlerLast(new ReadTimeoutHandler(10))
        .addHandlerLast(new WriteTimeoutHandler(10))
}// Crear un cliente web...

Configurar el tiempo de espera de respuesta para una solicitud específica:

Java

HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Crear un cliente web...
Kotlin
 
val httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Crear un cliente web...

El siguiente ejemplo muestra cómo configurar el HttpClient de Jetty:

Java
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
    HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
    reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
Kotlin
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest { httpRequest: ClientHttpRequest ->
    val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>()
    reactorRequest.responseTimeout(Duration.ofSeconds(2))
}
.retrieve()
.bodyToMono(String::class.java)

Jetty

El siguiente ejemplo muestra cómo configurar los ajustes de HttpClient desde Jetty:

Java
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
Kotlin
 val httpClient = HttpClient()
httpClient.cookieStore = ...
val webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();

Por de forma predeterminada, HttpClient crea sus propios recursos (Ejecutor, ByteBufferPool, Scheduler) que permanecen activos hasta que se completa el proceso. ejecución o llamada a la función stop().

Es posible compartir recursos entre múltiples instancias del cliente (y servidor) Jetty y garantizar que el uso de recursos finalice cuando ApplicationContext se cierra desde Spring, declarando un bean administrado por Spring de tipo JettyResourceFactory, como se muestra en el siguiente ejemplo:

Java

@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Configuración adicional...
ClientHttpConnector connector =
    new JettyClientHttpConnector(httpClient, resourceFactory()); 
return WebClient.builder().clientConnector(connector).build(); 
}
Kotlin

@Bean
fun resourceFactory() = JettyResourceFactory()
@Bean
fun webClient(): WebClient {
val httpClient = HttpClient()
// Configuración adicional...
val connector = JettyClientHttpConnector(httpClient, resourceFactory()) 
return WebClient.builder().clientConnector(connector).build() 
}

HttpComponents

El siguiente ejemplo muestra cómo configurar los parámetros HttpClient de Apache HttpComponents:

Java

HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
Kotlin
val client = HttpAsyncClients.custom().apply {
setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()

retrieve()

El método retrieve() se puede utilizar para declarar cómo recuperar la respuesta. Por ejemplo:

Java
WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
Kotlin
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity<Person>().awaitSingle()

O obtenemos solo el cuerpo:

Java
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
Kotlin
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.awaitBody<Person>()

Obtener una secuencia de objetos decodificados:

Java
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
Kotlin
val result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlow<Quote>()

De forma predeterminada, las respuestas 4xx o 5xx dan como resultado una WebClientResponseException, incluidas subclases para ciertos códigos de estado HTTP . Para configurar cómo se manejan los mensajes de error, use los controladores onStatus de la siguiente manera:

Java
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
Kotlin

val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { ... }
.onStatus(HttpStatus::is5xxServerError) { ... }
.awaitBody<Person>()

Exchange

Métodos exchangeToMono() y exchangeToFlux() (o awaitExchange { } y exchangeToFlow { } en Kotlin) útil para casos más complejos que requieren más control, como decodificar una respuesta de manera diferente según el estado de la respuesta:

Java

Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
    if (response.statusCode().equals(HttpStatus.OK)) {
        return response.bodyToMono(Person.class);
    }
    else {
        // Solucionar el error
        return response.createException().flatMap(Mono::error);
    }
});
Kotlin
val entity = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.awaitExchange {
if (response.statusCode() == HttpStatus.OK) {
     return response.awaitBody<Person>()
}
else {
     throw response.createExceptionAndAwait()
}
}}

Cuando se utiliza el código anterior, después de completar el Mono o devuelto Flux , el cuerpo de la respuesta se verifica y, si no se usa, se libera para evitar pérdidas de memoria y conexión. Por lo tanto, la respuesta no se puede decodificar más adelante. La función proporcionada debe determinar cómo decodificar la respuesta si es necesario.

Cuerpo de la solicitud

El cuerpo de la solicitud se puede codificar desde cualquier tipo asincrónico manejado por ReactiveAdapterRegistry , como Mono o Deferred de las corrutinas de Kotlin, como se muestra en el siguiente ejemplo:

Java
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
    
Kotlin
val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()

También puede ser flujo codificado de objetos, como se muestra en el siguiente ejemplo:

Java

Flux<Person> personFlux = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);
Kotlin
val people: Flow<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(people)
.retrieve()
.awaitBody<Unit>()

Además, si hay un valor real, entonces puede usar el método abreviado bodyValue, como se muestra en el siguiente ejemplo:

Java

Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
Kotlin
val person: Person = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.awaitBody<Unit>()

Datos del formulario

Para enviar datos del formulario, puede especificar MultiValueMap<String, String> como cuerpo. Tenga en cuenta que el contenido se establece automáticamente en application/x-www-form-urlencoded usando FormHttpMessageWriter. El siguiente ejemplo muestra cómo utilizar MultiValueMap<String, String>:

Java
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
Kotlin
val formData: MultiValueMap<String, String> = ...
client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.awaitBody<Unit>()

También puede agregar datos de formulario en línea usando BodyInserters como se muestra en el siguiente ejemplo:

Java
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.awaitBody<Unit>()

Datos multicomponente

Para enviar datos multicomponente, debe especificar un MultiValueMap<String, ?> cuyos valores son instancias de Object que representan el contenido del componente o HttpEntity instancias que representan el contenido y los encabezados de los componentes. MultipartBodyBuilder proporciona una API conveniente para preparar una solicitud de varias partes. El siguiente ejemplo muestra cómo crear un MultiValueMap<String, ?>:

Java
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
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()

En la mayoría de los casos, no es necesario establecer Content-Type para cada componente. El tipo de contenido se determina automáticamente en función del HttpMessageWriter seleccionado para la serialización o, en el caso de Resource, en función de la extensión del archivo. Si es necesario, puede establecer explícitamente MediaType para cada componente a través de una de las sobrecargas de part de la herramienta de compilación.

Después de preparar MultiValueMap La forma más sencilla de pasarlo al WebClient es a través del método body, como se muestra en el siguiente ejemplo:

Java
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
Kotlin
val builder: MultipartBodyBuilder = ...
client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.awaitBody<Unit>()

Si MultiValueMap contiene al menos un único valor que no es String y que también puede representar datos de formulario regulares (es decir, application/x-www-form-urlencoded), no es necesario establecer Content-Type en multipart/form-data. Esto siempre sucede cuando se utiliza MultipartBodyBuilder, que proporciona una función contenedora HttpEntity.

Como alternativa a MultipartBodyBuilder, puede También proporcione contenido de varias partes de estilo en línea utilizando BodyInserters en línea, como se muestra en el siguiente ejemplo:

Java
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.bodyToMono(Void.class);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.awaitBody<Unit>()

Filtros

Puede registrar un filtro de cliente ExchangeFilterFunction) a través de WebClient.Builder para interceptar y modificar solicitudes, como se muestra en la siguiente ejemplo:

Java
WebClient client = WebClient.builder()
.filter((request, next) -> {
    ClientRequest filtered = ClientRequest.from(request)
            .header("foo", "bar")
            .build();
    return next.exchange(filtered);
})
.build();
Kotlin
val client = WebClient.builder()
.filter { request, next ->
    val filtered = ClientRequest.from(request)
            .header("foo", "bar")
            .build()
    next.exchange(filtered)
}
.build()

Esto puede ser Se utiliza para funciones de extremo a extremo, como la autenticación. El siguiente ejemplo utiliza un filtro para la autenticación básica a través de un método de fábrica estático:

Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
Kotlin
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
val client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build()

Los filtros se pueden agregar o eliminar modificando una instancia de WebClient existente, lo que crea una nueva instancia de WebClient sin afectar la original. Por ejemplo:

Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
    filterList.add(0, basicAuthentication("user", "password"));
})
.build();
Kotlin
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()

WebClient es una interfaz delgada encima de la cadena de filtros, acompañada de una ExchangeFunction . Proporciona un flujo de trabajo para realizar solicitudes, codificar hacia y desde objetos de nivel superior y ayuda a garantizar que el contenido de la respuesta siempre se consuma. Si los filtros procesan la respuesta de alguna manera, se debe tener cuidado de garantizar que su contenido siempre se consuma o se propague de otro modo hacia abajo al WebClient, que proporcionará lo mismo. A continuación se muestra un filtro que maneja el código de estado UNAUTHORIZED pero garantiza que se devuelva cualquier contenido de respuesta, ya sea esperado o no:

Java
 public ExchangeFilterFunction renewTokenFilter() {
return (request, next) -> next.exchange(request).flatMap(response -> {
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
    return response.releaseBody()
            .then(renewToken())
            .flatMap(token -> {
                ClientRequest newRequest = ClientRequest.from(request).build();
                return next.exchange(newRequest);
            });
} else {
    return Mono.just(response);
}
});
}
Kotlin
fun renewTokenFilter(): ExchangeFilterFunction? {
return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction ->
next.exchange(request!!).flatMap { response: ClientResponse ->
    if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
        return@flatMap response.releaseBody()
                .then(renewToken())
                .flatMap { token: String? ->
                    val newRequest = ClientRequest.from(request).build()
                    next.exchange(newRequest)
                }
    } else {
        return@flatMap Mono.just(response)
    }
}
}
}

Atributos

Se pueden agregar atributos a la solicitud. Esto es conveniente si necesita pasar información a lo largo de una cadena de filtros e influir en la lógica de los filtros dentro de una solicitud determinada. Por ejemplo:

Java
WebClient client = WebClient.builder()
.filter((request, next) -> {
    Optional<Object> usr = request.attribute("myAttribute");
    // ...
})
.build();
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}
Kotlin
val client = WebClient.builder()
.filter { request, _ ->
    val usr = request.attributes()["myAttribute"];
    // ...
}
.build()
client.get().uri("https://example.org/")
    .attribute("myAttribute", "...")
    .retrieve()
    .awaitBody<Unit>())

Tenga en cuenta que puede configurar globalmente una devolución de llamada defaultRequest en el nivel WebClient.Builder, lo que permite insertar atributos en todas las solicitudes, que se pueden usar, por ejemplo, en una aplicación en Spring MVC para completar los atributos de solicitud basados en datos ThreadLocal.

Contexto

Los atributos proporcionan una transmisión conveniente de información a la cadena de filtros, pero solo afectan la solicitud actual. Si necesita pasar información que se extienda a solicitudes adicionales que están anidadas, por ejemplo, a través de flatMap, o ejecutadas después, por ejemplo, a través de concatMap, entonces necesita usar Contexto de Reactor.

El Context del proyecto Reactor debe completarse al final de la cadena reactiva para que se aplique a todas las operaciones. Por ejemplo:

Java
WebClient client = WebClient.builder()
.filter((request, next) ->
        Mono.deferContextual(contextView -> {
            String value = contextView.get("foo");
            // ...
        }))
.build();
client.get().uri("https://example.org/")
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
        // execute a nested query (the context is propagated automatically)...
})
.contextWrite(context -> context.put("foo", ...));

Uso sincrónico

WebClient se puede utilizar de forma síncrona, bloqueando al final para obtener el resultado:

Java
Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();;
Kotlin
val persona = runBlocking {
client.get().uri("/person/{id}", i).retrieve( )
    .awaitBody<Persona>()
}
val personas = ejecutarBloqueo {
cliente.get().uri("/persons").retrieve()
    .bodyToFlow<Persona>()
    .Listar()
}

Sin embargo, si necesitas realizar varias llamadas, es más eficaz no bloquear cada respuesta individualmente, sino esperar el resultado combinado:

Java
Mono<Persona> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Persona.clase);
Mono<Lista<Hobby>> aficionesMono = client.get().uri("/persona/{id}/aficiones", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Mapa<Cadena, Objeto> datos = Mono.zip(personaMono, aficionesMono, (persona, aficiones) -> {
    Mapa<Cadena, Cadena> mapa = nuevo LinkedHashMap<>();
    map.put("persona", persona);
    map.put("aficiones", aficiones);
    regresar mapa;
})
.bloque();
Kotlin
val datos = runBlocking {
val personaDeferred = async {
    cliente.get().uri("/person/{id}", personId)
            .retrieve().awaitBody<Persona>()
}
val pasatiemposDeferidos = asíncrono {
    cliente.get().uri("/person/{id}/hobbies", personId)
            .retrieve().bodyToFlow<Hobby>().toList()
}
mapOf("persona" a personDeferred.await(), "hobbies" a hobbiesDeferred.await())
}

Lo anterior es sólo un ejemplo. Hay muchos otros patrones y operadores para crear una canalización reactiva que realiza muchas llamadas remotas, potencialmente múltiples anidadas, interdependientes, sin bloquearse hasta el final.

Cuando utilices Flux o Mono, no tendrás que bloquear el controlador Spring MVC o Spring WebFlux en absoluto. Simplemente será posible devolver el tipo reactivo resultante del método del controlador. El mismo principio se aplica a las corrutinas de Kotlin y Spring WebFlux: simplemente use una función de pausa o devuelva Flow en el método del controlador.

Pruebas

Para probar el código que utiliza WebClient, puede utilizar un objeto de servidor web simulado, por ejemplo OkHttp MockWebServer. Para ver un ejemplo de su uso, consulte WebClientIntegrationTests en el conjunto de pruebas Spring Framework o en el ejemplo servidor estático en el repositorio OkHttp.