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:
Se pueden conectar otros clientes a través de
ClientHttpConnector
.
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
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
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:
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
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:
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
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
:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
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:
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
@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:
@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();
}
- Creamos recursos independientes de global.
- Utilice el constructor
ReactorClientHttpConnector
con la fábrica de recursos. - Conecte el conector a
WebClient.Builder
.
@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()
}
- Creamos recursos independientes de global.
- Utilice el constructor
ReactorClientHttpConnector
con la fábrica de recursos. - Conecte el conector a
WebClient.Builder
.
Tiempo de espera
Configuración de valores de tiempo de espera de conexión:
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
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:
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...
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:
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Crear un cliente web...
val httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Crear un cliente web...
El siguiente ejemplo muestra cómo configurar el HttpClient
de Jetty:
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
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:
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
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:
@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();
}
@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:
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
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:
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);
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:
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
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:
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
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:
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);
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:
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);
}
});
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:
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
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:
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);
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:
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
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>
:
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
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:
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);
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, ?>
:
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();
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:
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
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:
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);
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:
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
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:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
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:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
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:
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);
}
});
}
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:
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);
}
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:
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:
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();;
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:
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();
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.