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.
GO TO FULL VERSION