SpringBoot
Tutorial SpringBoot: Cliente reactivo WebClient
Aprende programación reactiva al usar el cliente WebClient de Spring WebFlux para realizar peticiones HTTP de tipo GET, POST, PUT, PATCH, DELETE reactivas y no bloqueantes.
Aprende SpringBoot GRATIS y certifícateQué es WebClient y qué diferencia hay con RestTemplate y RestClient
WebClient es una herramienta proporcionada por Spring WebFlux que permite realizar peticiones HTTP de manera reactiva y no bloqueante. A diferencia de otros clientes HTTP en Spring, WebClient está diseñado para aprovechar las ventajas de la programación reactiva, ofreciendo un modelo más eficiente en términos de recursos y rendimiento.
Anteriormente, en aplicaciones basadas en Spring MVC, era común utilizar RestTemplate para realizar operaciones REST. RestTemplate es un cliente síncrono y bloqueante que, al realizar una petición, espera de forma activa hasta recibir la respuesta, consumiendo recursos del hilo actual. En aplicaciones con alto volumen de tráfico, este enfoque puede llevar a problemas de escalabilidad.
Con la introducción de Spring WebFlux, se buscó una alternativa más eficiente. Aquí es donde entra en juego WebClient. Al ser un cliente no bloqueante, permite manejar múltiples peticiones concurrentes sin necesidad de asignar un hilo por cada una. Esto se logra gracias al uso de reactores y flujos reactivos, lo que mejora significativamente la capacidad de respuesta de las aplicaciones.
Por otro lado, RestClient es una incorporación más reciente en el ecosistema de Spring. Introducido para simplificar y modernizar la comunicación con servicios REST, RestClient ofrece una interfaz más fluida y funcional que RestTemplate, pero sigue siendo un cliente síncrono. Aunque proporciona mejoras en la usabilidad y algunas optimizaciones, no alcanza el nivel de reactividad que ofrece WebClient.
Una diferencia clave entre estos tres clientes es su enfoque en la gestión de recursos y la capacidad para manejar operaciones asíncronas:
- RestTemplate: Cliente tradicional, bloqueante y síncrono. Adecuado para aplicaciones simples o con bajo requerimiento de concurrencia.
- RestClient: Una evolución de RestTemplate, con una API más moderna pero aún bloqueante. Facilita la interacción con servicios REST sin introducir programación reactiva.
- WebClient: Cliente reactivo y no bloqueante. Ideal para aplicaciones que requieren alta concurrencia y eficiencia en el uso de recursos.
A nivel de uso, WebClient permite construir peticiones de manera fluida. Por ejemplo:
WebClient client = WebClient.create("https://api.ejemplo.com");
Mono<Respuesta> respuestaMono = client.get()
.uri("/datos")
.retrieve()
.bodyToMono(Respuesta.class);
En este fragmento, se realiza una petición GET de forma no bloqueante. El método bodyToMono
devuelve un Mono que se suscribirá cuando se necesite procesar la respuesta, aprovechando así la pereza inherente de las operaciones reactivas.
Además, WebClient ofrece soporte completo para manejar flujos de datos continuos mediante el uso de Flux, lo que es especialmente útil en aplicaciones donde se requiere procesar streams de información en tiempo real.
Otra ventaja de WebClient es su capacidad para manejar de forma eficiente los backpressure, permitiendo controlar el ritmo al que se consumen las peticiones y respuestas, evitando la sobrecarga del sistema.
La elección entre RestTemplate, RestClient y WebClient depende de las necesidades específicas de la aplicación:
- Si se requiere compatibilidad con código legado y la aplicación es sencilla, RestTemplate puede ser suficiente.
- Para aplicaciones que buscan una API más moderna pero sin introducir reactividad, RestClient es una opción intermedia.
- Cuando la eficiencia, la concurrencia y el rendimiento son cruciales, especialmente en arquitecturas de microservicios, WebClient es la opción recomendada por su naturaleza no bloqueante y su integración con el modelo reactivo de Spring WebFlux.
Es importante destacar que, al utilizar WebClient, se aprovechan las ventajas de los procesadores multinúcleo y se mejora la escalabilidad de la aplicación, siendo una solución óptima para enfrentarse a las demandas actuales en el desarrollo de servicios web eficientes y robustos.
Peticiones GET en WebClient con operadores básicos y avanzados
El WebClient es una herramienta esencial en Spring WebFlux para realizar peticiones HTTP de manera reactiva. Al trabajar con peticiones GET, es fundamental comprender tanto los operadores básicos como los avanzados que ofrece este cliente para maximizar su potencial.
Para comenzar, es necesario crear una instancia de WebClient. Existen varias formas de hacerlo, pero la más común es mediante el método create()
:
WebClient webClient = WebClient.create("https://api.ejemplo.com");
En este ejemplo, se establece la URL base para todas las peticiones. Ahora, para realizar una petición GET sencilla y obtener un objeto Mono<Resultado>
, se pueden encadenar operadores básicos:
Mono<Resultado> resultadoMono = webClient.get()
.uri("/datos")
.retrieve()
.bodyToMono(Resultado.class);
En este fragmento:
get()
: Indica que se realizará una petición GET.uri("/datos")
: Especifica el endpoint al que se enviará la petición.retrieve()
: Prepara la respuesta para su posterior procesamiento.bodyToMono(Resultado.class)
: Convierte el cuerpo de la respuesta en un objeto Mono del tipo especificado.
Es importante destacar que hasta que no se suscriba al Mono
, la petición no se ejecutará debido a la naturaleza perezosa de las operaciones reactivas.
Para manejar peticiones más complejas, se pueden utilizar operadores avanzados. Por ejemplo, para agregar cabeceras HTTP o parámetros de consulta:
Mono<Resultado> resultadoMono = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/datos")
.queryParam("filtro", "activo")
.build())
.header("Autorización", "Bearer tokenEjemplo")
.retrieve()
.bodyToMono(Resultado.class);
En este caso, se utiliza uri()
con un UriBuilder para construir una URI con parámetros dinámicos. Además, se añade una cabecera de Autorización necesaria para ciertas API protegidas.
Otro operador avanzado es exchangeToMono()
, que permite un mayor control sobre la respuesta:
Mono<Resultado> resultadoMono = webClient.get()
.uri("/datos")
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Resultado.class);
} else {
return response.createException()
.flatMap(Mono::error);
}
});
Con exchangeToMono()
, se puede inspeccionar el estado HTTP y decidir cómo procesar la respuesta. Esto es útil para manejar errores de manera personalizada.
Para trabajar con flujos de datos, como listas o streams, se utiliza bodyToFlux()
:
Flux<Resultado> resultadoFlux = webClient.get()
.uri("/lista-datos")
.retrieve()
.bodyToFlux(Resultado.class);
Esto devuelve un Flux que emite múltiples elementos del tipo Resultado
. Es esencial cuando se espera recibir múltiples registros desde el servidor.
Además, se pueden encadenar operadores reactivos para transformar y combinar los datos recibidos. Por ejemplo:
Mono<Proceso> procesoMono = webClient.get()
.uri("/datos")
.retrieve()
.bodyToMono(Resultado.class)
.flatMap(resultado -> procesarResultado(resultado));
Aquí, se utiliza flatMap()
para transformar el Resultado
obtenido en otro Mono, posiblemente realizando operaciones adicionales o llamadas a otros servicios.
En situaciones donde se requiere reintentar una petición fallida, se puede emplear el operador retry()
:
Mono<Resultado> resultadoMono = webClient.get()
.uri("/datos")
.retrieve()
.bodyToMono(Resultado.class)
.retry(3);
Esto intentará realizar la petición hasta tres veces en caso de error, mejorando la robustez de la aplicación frente a fallos temporales.
Para manejar timeouts, es posible utilizar el operador timeout()
:
Mono<Resultado> resultadoMono = webClient.get()
.uri("/datos")
.retrieve()
.bodyToMono(Resultado.class)
.timeout(Duration.ofSeconds(5));
De esta manera, si la respuesta del servidor tarda más de cinco segundos, se emitirá un error de tiempo de espera, permitiendo gestionar este caso de forma apropiada.
Cuando se necesita agregar lógica condicional o manipular el flujo según ciertas condiciones, se pueden emplear operadores como filter()
o switchIfEmpty()
:
Mono<Resultado> resultadoMono = webClient.get()
.uri("/datos")
.retrieve()
.bodyToMono(Resultado.class)
.filter(resultado -> resultado.esValido())
.switchIfEmpty(Mono.error(new Exception("Resultado no válido")));
En este fragmento, solo se procesan los resultados que cumplen una condición específica, y se maneja el caso en que no haya resultados válidos.
Además, para casos en los que se requiera combinar múltiples peticiones, los operadores zip()
y concat()
son de utilidad:
Mono<Tuple2<Detalle, Precio>> combinacionMono = Mono.zip(
webClient.get().uri("/detalle").retrieve().bodyToMono(Detalle.class),
webClient.get().uri("/precio").retrieve().bodyToMono(Precio.class)
);
Con zip()
, se pueden ejecutar peticiones en paralelo y combinar sus resultados, optimizando la eficiencia del proceso.
Es fundamental también manejar los contextos de seguridad, especialmente si se trabaja con autenticación OAuth2 o tokens JWT. WebClient permite configurar un ExchangeFilterFunction para agregar automáticamente tokens en las peticiones:
WebClient webClient = WebClient.builder()
.baseUrl("https://api.ejemplo.com")
.filter(ExchangeFilterFunctions.oauth2Authentication(authorizedClientManager))
.build();
De esta forma, se garantiza que todas las peticiones incluyen la información de autenticación necesaria.
Finalmente, es posible personalizar el cliente HTTP subyacente para ajustar aspectos como el manejo de SSL o la configuración de proxy:
HttpClient httpClient = HttpClient.create()
.secure(spec -> spec.sslContext(sslContext));
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl("https://api.segura.com")
.build();
Esta configuración avanzada es útil en entornos que requieren un mayor control sobre las conexiones HTTP.
En resumen, el uso de operadores básicos y avanzados en peticiones GET con WebClient permite construir aplicaciones reactiva s robustas y eficientes. Al dominar estos operadores, se optimiza la gestión de peticiones y se aprovechan las ventajas del modelo reactivo en Spring Boot.
Peticiones POST, PUT y PATCH en WebClient con operadores básicos y avanzados
Al emplear WebClient para realizar peticiones POST, PUT y PATCH, se aprovecha el modelo reactivo de Spring WebFlux para manejar operaciones de manera asíncrona y no bloqueante. Estas operaciones son esenciales para enviar datos al servidor o actualizar recursos existentes.
Para comenzar, se suele crear una instancia de WebClient especificando la URL base:
WebClient webClient = WebClient.create("https://api.ejemplo.com");
Peticiones POST
Las peticiones POST se utilizan para crear nuevos recursos en el servidor. Con WebClient, se pueden enviar datos en el cuerpo de la petición utilizando el método bodyValue()
o body()
.
Ejemplo básico de una petición POST:
Mono<Respuesta> respuesta = webClient.post()
.uri("/crear")
.bodyValue(nuevoObjeto)
.retrieve()
.bodyToMono(Respuesta.class);
En este código:
post()
: Indica que se realizará una petición POST.uri("/crear")
: Especifica el endpoint de creación.bodyValue(nuevoObjeto)
: Incluye el objeto a enviar en el cuerpo de la petición.retrieve()
: Prepara la respuesta para su procesamiento.bodyToMono(Respuesta.class)
: Convierte la respuesta en unMono
del tipo especificado.
Para peticiones más complejas, se puede utilizar body()
junto con Mono
o Flux
, permitiendo enviar datos de forma reactiva:
Mono<Respuesta> respuesta = webClient.post()
.uri("/crear")
.body(Mono.just(nuevoObjeto), Objeto.class)
.retrieve()
.bodyToMono(Respuesta.class);
Si es necesario agregar cabeceras HTTP, como una Content-Type personalizada o una Autorización, se puede hacer así:
Mono<Respuesta> respuesta = webClient.post()
.uri("/crear")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header("Autorización", "Bearer tokenEjemplo")
.bodyValue(nuevoObjeto)
.retrieve()
.bodyToMono(Respuesta.class);
Para manejar errores de forma más detallada, se puede utilizar exchangeToMono()
:
Mono<Respuesta> respuesta = webClient.post()
.uri("/crear")
.bodyValue(nuevoObjeto)
.exchangeToMono(response -> {
if (response.statusCode().is2xxSuccessful()) {
return response.bodyToMono(Respuesta.class);
} else if (response.statusCode().is4xxClientError()) {
return Mono.error(new ClienteException("Error del cliente"));
} else {
return Mono.error(new ServerException("Error del servidor"));
}
});
Peticiones PUT
Las peticiones PUT se emplean para actualizar recursos completos en el servidor. El uso de WebClient para realizar una petición PUT es similar al de POST.
Ejemplo básico de una petición PUT:
Mono<Respuesta> respuesta = webClient.put()
.uri("/actualizar/{id}", idObjeto)
.bodyValue(objetoActualizado)
.retrieve()
.bodyToMono(Respuesta.class);
Aquí:
put()
: Indica una petición PUT.uri("/actualizar/{id}", idObjeto)
: Especifica el endpoint con un parámetro de ruta.bodyValue(objetoActualizado)
: Incluye el objeto actualizado en el cuerpo de la petición.
Si se desea manejar condiciones de concurrencia optimista utilizando cabeceras como If-Match, se puede añadir:
Mono<Respuesta> respuesta = webClient.put()
.uri("/actualizar/{id}", idObjeto)
.header(HttpHeaders.IF_MATCH, eTag)
.bodyValue(objetoActualizado)
.retrieve()
.bodyToMono(Respuesta.class);
Esto ayuda a evitar conflictos al actualizar recursos que pueden haber cambiado en el servidor.
Peticiones PATCH
Las peticiones PATCH permiten actualizar parcialmente un recurso. Con WebClient, se siguen pasos similares, pero es importante asegurarse de que el servidor soporte este método.
Ejemplo básico de una petición PATCH:
Mono<Respuesta> respuesta = webClient.patch()
.uri("/modificar/{id}", idObjeto)
.bodyValue(cambiosParciales)
.retrieve()
.bodyToMono(Respuesta.class);
En este caso:
patch()
: Indica una petición PATCH.bodyValue(cambiosParciales)
: Contiene solo los campos que se desean actualizar.
Para enviar datos en formato JSON Patch o Merge Patch, se puede especificar el Content-Type adecuado:
Mono<Respuesta> respuesta = webClient.patch()
.uri("/modificar/{id}", idObjeto)
.header(HttpHeaders.CONTENT_TYPE, "application/merge-patch+json")
.bodyValue(cambiosParciales)
.retrieve()
.bodyToMono(Respuesta.class);
Operadores avanzados
Al utilizar WebClient, los operadores avanzados permiten un mayor control y personalización de las peticiones y respuestas.
Manejo de tiempo de espera con timeout()
:
Mono<Respuesta> respuesta = webClient.post()
.uri("/crear")
.bodyValue(nuevoObjeto)
.retrieve()
.bodyToMono(Respuesta.class)
.timeout(Duration.ofSeconds(3))
.onErrorResume(TimeoutException.class, e -> Mono.error(new CustomException("Tiempo de espera excedido")));
Aquí, si la operación tarda más de tres segundos, se maneja el error de forma personalizada.
Reintentos en caso de error con retryWhen()
:
Mono<Respuesta> respuesta = webClient.post()
.uri("/crear")
.bodyValue(nuevoObjeto)
.retrieve()
.bodyToMono(Respuesta.class)
.retryWhen(Retry.fixedDelay(2, Duration.ofSeconds(2)))
.onErrorResume(e -> Mono.error(new CustomException("Fallo tras reintentos")));
Este ejemplo reintentará la operación dos veces con un retraso fijo, mejorando la resiliencia ante fallos temporales.
Transformación de datos utilizando flatMap()
y map()
:
Mono<ResultadoProcesado> resultado = webClient.post()
.uri("/procesar")
.bodyValue(datosEntrada)
.retrieve()
.bodyToMono(DatosSalida.class)
.flatMap(datosSalida -> procesarDatos(datosSalida))
.map(resultadoParcial -> new ResultadoProcesado(resultadoParcial));
Se encadenan operaciones para procesar la respuesta y transformarla en el formato deseado.
Configuración de filtros para modificar todas las peticiones:
WebClient webClientConFiltro = WebClient.builder()
.baseUrl("https://api.ejemplo.com")
.filter((request, next) -> {
ClientRequest nuevaPeticion = ClientRequest.from(request)
.header("X-Custom-Header", "ValorPersonalizado")
.build();
return next.exchange(nuevaPeticion);
})
.build();
Esto añade un filtro que incluye una cabecera personalizada en cada petición, facilitando la configuración común.
Manejo de respuesta completa con exchangeToMono()
:
Mono<ClienteResponse> respuesta = webClient.post()
.uri("/crear")
.bodyValue(nuevoCliente)
.exchangeToMono(response -> {
if (response.statusCode() == HttpStatus.CREATED) {
return Mono.just(new ClienteResponse("Cliente creado con éxito"));
} else {
return response.bodyToMono(ErrorResponse.class)
.flatMap(error -> Mono.error(new CustomException(error.getMensaje())));
}
});
Con este enfoque, se maneja la respuesta completa, permitiendo acciones basadas en el código de estado HTTP y el contenido del cuerpo.
Uso de **onStatus()**
para gestionar errores específicos:
Mono<Respuesta> respuesta = webClient.post()
.uri("/crear")
.bodyValue(nuevoObjeto)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> response.bodyToMono(ErrorResponse.class)
.flatMap(error -> Mono.error(new ClienteException(error.getMensaje()))))
.onStatus(HttpStatus::is5xxServerError, response -> response.bodyToMono(ErrorResponse.class)
.flatMap(error -> Mono.error(new ServerException(error.getMensaje()))))
.bodyToMono(Respuesta.class);
Este método permite interceptar y manejar errores basados en el código de estado sin necesidad de cambiar a exchangeToMono()
.
Envío de contenido multipart/form-data
Para enviar datos multipart/form-data, como archivos y formularios, se utiliza MultipartBodyBuilder
:
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("campoTexto", "Valor del campo");
builder.part("archivo", nuevoArchivo.getResource());
Mono<Respuesta> respuesta = webClient.post()
.uri("/subir")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(builder.build()))
.retrieve()
.bodyToMono(Respuesta.class);
Esto permite adjuntar múltiples partes en la petición, siendo útil para subir archivos o formularios complejos.
Autenticación y seguridad
Si se requiere autenticación Basic Auth, se puede configurar de la siguiente manera:
WebClient webClientAuth = WebClient.builder()
.baseUrl("https://api.segura.com")
.defaultHeaders(headers -> headers.setBasicAuth("usuario", "contraseña"))
.build();
Para autenticación OAuth2, se puede integrar con el gestor de clientes autorizados:
ExchangeFilterFunction oauth2Filtro = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
WebClient webClientOAuth2 = WebClient.builder()
.baseUrl("https://api.segura.com")
.filter(oauth2Filtro)
.build();
De esta forma, se manejan automáticamente los tokens de acceso en las peticiones.
Configuración avanzada del cliente HTTP
Para personalizar aspectos como tiempos de conexión, se configura el HttpClient subyacente:
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(5))
.addHandlerLast(new WriteTimeoutHandler(5)));
WebClient webClientConfig = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Esto permite ajustar los tiempos de espera de conexión, lectura y escritura, mejorando el control sobre el comportamiento de las peticiones.
Uso combinado de varios operadores
Es posible combinar múltiples operadores para crear flujos de trabajo más complejos:
Mono<Usuario> usuarioMono = webClient.post()
.uri("/usuarios")
.bodyValue(nuevoUsuario)
.retrieve()
.bodyToMono(Usuario.class)
.zipWhen(usuario -> webClient.put()
.uri("/permisos/{id}", usuario.getId())
.bodyValue(permisos)
.retrieve()
.bodyToMono(Permisos.class),
(usuario, permisos) -> {
usuario.setPermisos(permisos);
return usuario;
})
.flatMap(usuario -> guardarEnBaseDeDatos(usuario));
En este ejemplo, se crea un usuario y, una vez creado, se actualizan sus permisos. Luego, se guarda el usuario completo en la base de datos, demostrando la fluidez y potencia de los operadores reactivos.
Peticiones DELETE en WebClient con operadores básicos y avanzados
En Spring WebFlux, el uso de WebClient para realizar peticiones DELETE permite eliminar recursos de manera reactiva y eficiente. Las peticiones DELETE son esenciales en aplicaciones que requieren operaciones de borrado en servicios RESTful.
Para comenzar, es necesario disponer de una instancia de WebClient, generalmente configurada con una URL base:
WebClient webClient = WebClient.create("https://api.ejemplo.com");
Realización de una petición DELETE básica
Una petición DELETE básica implica especificar el recurso a eliminar mediante su URI. Por ejemplo, para eliminar un recurso identificado por un ID:
Mono<Void> resultado = webClient.delete()
.uri("/recurso/{id}", idRecurso)
.retrieve()
.bodyToMono(Void.class);
En este fragmento de código:
delete()
: Indica que se realizará una petición DELETE.uri("/recurso/{id}", idRecurso)
: Especifica el endpoint con el ID del recurso a eliminar.retrieve()
: Prepara la respuesta para su procesamiento.bodyToMono(Void.class)
: Indica que no se espera un cuerpo en la respuesta, devolviendo unMono<Void>
.
Es importante destacar que, al tratarse de una operación reactiva, el borrado no se ejecutará hasta que se produzca la suscripción al Mono
.
Personalización de la petición DELETE
En casos donde se requiera agregar cabeceras HTTP o parámetros de consulta, se pueden incluir mediante los métodos correspondientes:
Mono<Void> resultado = webClient.delete()
.uri(uriBuilder -> uriBuilder
.path("/recurso")
.queryParam("tipo", tipoRecurso)
.build())
.header("Autorización", "Bearer tokenSeguro")
.retrieve()
.bodyToMono(Void.class);
Aquí, se utiliza un UriBuilder
para construir la URI con un parámetro de consulta, y se añade una cabecera de Autorización.
Manejo avanzado de la respuesta
Para obtener un mayor control sobre la respuesta, especialmente al manejar códigos de estado HTTP, se puede utilizar el método exchangeToMono()
:
Mono<Respuesta> resultado = webClient.delete()
.uri("/recurso/{id}", idRecurso)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.NO_CONTENT)) {
return Mono.just(new Respuesta("Recurso eliminado correctamente"));
} else if (response.statusCode().is4xxClientError()) {
return response.bodyToMono(ErrorRespuesta.class)
.flatMap(error -> Mono.error(new ClienteException(error.getMensaje())));
} else {
return Mono.error(new ServerException("Error del servidor al eliminar el recurso"));
}
});
En este ejemplo:
- Se inspecciona el estado HTTP de la respuesta.
- Si la eliminación es exitosa (
NO_CONTENT
), se devuelve un mensaje de confirmación. - Si ocurre un error del cliente (por ejemplo,
NOT_FOUND
), se procesa el cuerpo de error. - Se manejan otros posibles errores del servidor.
Uso de onStatus()
para gestionar errores
Otra forma de manejar los errores basados en el estado HTTP es utilizando el método onStatus()
:
Mono<Void> resultado = webClient.delete()
.uri("/recurso/{id}", idRecurso)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
return response.bodyToMono(ErrorRespuesta.class)
.flatMap(error -> Mono.error(new ClienteException(error.getMensaje())));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
return Mono.error(new ServerException("Error interno del servidor"));
})
.bodyToMono(Void.class);
Esta aproximación permite capturar y manejar errores de manera más declarativa, manteniendo el código limpio y legible.
Añadiendo condiciones con If-Match
y If-Unmodified-Since
Para implementar control de concurrencia optimista y evitar eliminar recursos que han sido modificados, se pueden emplear cabeceras como **If-Match**
:
Mono<Void> resultado = webClient.delete()
.uri("/recurso/{id}", idRecurso)
.header(HttpHeaders.IF_MATCH, etag)
.retrieve()
.bodyToMono(Void.class);
Al incluir el ETag del recurso en la cabecera If-Match
, el servidor solo procederá a eliminarlo si coincide, evitando inconsistencias.
Manipulación avanzada con operadores reactivos
Los operadores de Reactor permiten encadenar operaciones y manejar flujos de datos de manera flexible. Por ejemplo, utilizando flatMap()
para realizar acciones adicionales tras la eliminación:
Mono<String> resultado = webClient.delete()
.uri("/recurso/{id}", idRecurso)
.retrieve()
.bodyToMono(Void.class)
.flatMap(ignored -> {
// Acción adicional, como registrar el evento de eliminación
return registrarEliminación(idRecurso);
});
En este caso, tras realizar la petición DELETE y recibir la confirmación, se procede a registrar la eliminación en otro sistema.
Reintentos y gestión de errores transitorios
Para mejorar la resiliencia de la aplicación frente a errores transitorios, se puede utilizar el operador retryWhen()
:
Mono<Void> resultado = webClient.delete()
.uri("/recurso/{id}", idRecurso)
.retrieve()
.bodyToMono(Void.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(2)))
.onErrorResume(e -> {
// Manejo del error después de los reintentos
return Mono.error(new CustomException("No se pudo eliminar el recurso tras varios intentos"));
});
Este enfoque reintenta la operación hasta tres veces con un retraso exponencial, gestionando adecuadamente los fallos temporales.
Tiempo de espera con timeout()
Para evitar que la petición se extienda indefinidamente, se puede establecer un tiempo límite:
Mono<Void> resultado = webClient.delete()
.uri("/recurso/{id}", idRecurso)
.retrieve()
.bodyToMono(Void.class)
.timeout(Duration.ofSeconds(5))
.onErrorResume(TimeoutException.class, e -> {
// Manejo del tiempo de espera excedido
return Mono.error(new CustomException("La operación de eliminación ha excedido el tiempo permitido"));
});
De esta manera, si el servidor no responde en el tiempo especificado, se puede gestionar la situación de forma controlada.
Uso de filter()
y switchIfEmpty()
En escenarios donde se necesita verificar condiciones antes de proceder, los operadores filter()
y switchIfEmpty()
resultan útiles:
Mono<Void> resultado = verificarPermisos(usuario)
.filter(permisos -> permisos.permiteEliminar())
.switchIfEmpty(Mono.error(new AccesoDenegadoException("No tiene permisos para eliminar el recurso")))
.flatMap(permisos -> webClient.delete()
.uri("/recurso/{id}", idRecurso)
.retrieve()
.bodyToMono(Void.class));
Con este enfoque, se evita realizar la petición DELETE si el usuario no dispone de los permisos necesarios, mejorando la seguridad de la aplicación.
Combinación de peticiones con zip()
y then()
Para realizar operaciones en secuencia o en paralelo, se pueden combinar peticiones:
Mono<Void> resultado = Mono.zip(
obtenerRecurso(idRecurso),
verificarDependencias(idRecurso))
.flatMap(tuple -> {
Recurso recurso = tuple.getT1();
boolean puedeEliminar = tuple.getT2();
if (puedeEliminar) {
return webClient.delete()
.uri("/recurso/{id}", idRecurso)
.retrieve()
.bodyToMono(Void.class);
} else {
return Mono.error(new OperacionNoPermitidaException("El recurso no puede ser eliminado debido a dependencias existentes"));
}
});
Aquí, se obtienen datos necesarios antes de proceder con la eliminación, asegurando la integridad de la operación.
Configuración avanzada del cliente HTTP
Se puede personalizar el cliente HTTP subyacente para ajustar configuraciones específicas:
HttpClient httpClient = HttpClient.create()
.secure(sslSpec -> sslSpec.sslContext(sslContext))
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(5));
WebClient webClientPersonalizado = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Con esta configuración, se controla el tiempo de conexión y se establecen opciones de seguridad adicionales.
Uso de doOnSuccess()
y doOnError()
para logging
Para registrar eventos o realizar acciones adicionales al completar la operación, se pueden utilizar los operadores doOnSuccess()
y doOnError()
:
Mono<Void> resultado = webClient.delete()
.uri("/recurso/{id}", idRecurso)
.retrieve()
.bodyToMono(Void.class)
.doOnSuccess(aVoid -> {
// Registro de éxito
logger.info("Recurso {} eliminado exitosamente", idRecurso);
})
.doOnError(error -> {
// Registro de error
logger.error("Error al eliminar el recurso {}: {}", idRecurso, error.getMessage());
});
Esto facilita la monitorización y depuración de las operaciones de eliminación.
Integración con flujos reactivos
Las peticiones DELETE pueden formar parte de flujos más complejos utilizando Flux:
Flux<ResultadoEliminacion> resultados = Flux.fromIterable(idsRecursos)
.flatMap(id -> webClient.delete()
.uri("/recurso/{id}", id)
.retrieve()
.bodyToMono(Void.class)
.map(aVoid -> new ResultadoEliminacion(id, true))
.onErrorResume(e -> Mono.just(new ResultadoEliminacion(id, false, e.getMessage()))));
En este caso, se procesan múltiples eliminaciones en paralelo, recopilando los resultados individuales de cada operación.
Generics en WebClient con Type erasure, TypeReference y ParameterizedTypeReference
Al utilizar WebClient en aplicaciones reactivas con Spring WebFlux, es común trabajar con tipos genéricos para manejar respuestas de manera flexible y reutilizable. Sin embargo, debido al type erasure de Java, surgen desafíos al intentar deserializar respuestas a tipos genéricos. Para resolver estos inconvenientes, se emplean herramientas como TypeReference y ParameterizedTypeReference.
El type erasure es un mecanismo del compilador de Java que elimina la información de tipos genéricos en tiempo de ejecución. Esto significa que, aunque en tiempo de compilación se tiene información completa sobre los parámetros de tipo, en tiempo de ejecución esa información se pierde. Por ejemplo, una clase genérica Respuesta<T>
en tiempo de ejecución se ve simplemente como Respuesta
. Este comportamiento complica la deserialización de respuestas que contienen tipos genéricos.
Consideremos una clase genérica:
public class Respuesta<T> {
private T datos;
// getters y setters
}
Al utilizar WebClient para consumir un API que devuelve una Respuesta<T>
, es necesario indicar el tipo específico de T
para que el deserializador pueda construir correctamente el objeto. Sin embargo, debido al type erasure, pasar simplemente Respuesta.class
al método bodyToMono()
no es suficiente, ya que no proporciona información sobre el tipo genérico T
.
Para solucionar este problema, se emplea TypeReference, una clase que mantiene la información de tipos genéricos incluso después del type erasure. A continuación se muestra cómo utilizar TypeReference
con WebClient:
TypeReference<Respuesta<Datos>> tipoReferencia = new TypeReference<>() {};
Mono<Respuesta<Datos>> respuestaMono = webClient.get()
.uri("/api/datos")
.retrieve()
.bodyToMono(tipoReferencia);
En este ejemplo, Datos
es la clase que representa el tipo concreto que se espera en T
. Al crear una instancia anónima de TypeReference<Respuesta<Datos>>
, se preserva la información de tipo genérico necesaria para la deserialización.
Otra alternativa es utilizar ParameterizedTypeReference, que ofrece una funcionalidad similar pero está más integrada con WebClient. Su uso es especialmente recomendado al trabajar con Flux o cuando se requiere mayor claridad en el código:
ParameterizedTypeReference<Respuesta<Datos>> tipoParametrizado =
new ParameterizedTypeReference<>() {};
Mono<Respuesta<Datos>> respuestaMono = webClient.get()
.uri("/api/datos")
.retrieve()
.bodyToMono(tipoParametrizado);
La ventaja de utilizar ParameterizedTypeReference
es que está específicamente diseñada para trabajar con tipos parametrizados en las operaciones del WebClient, facilitando la lectura y mantenimiento del código.
Cuando se necesitan manejar listas genéricas, el uso de ParameterizedTypeReference
es aún más relevante. Por ejemplo, para obtener una lista de Datos
dentro de una Respuesta
:
ParameterizedTypeReference<Respuesta<List<Datos>>> tipoListaParametrizada =
new ParameterizedTypeReference<>() {};
Mono<Respuesta<List<Datos>>> respuestaMono = webClient.get()
.uri("/api/lista-datos")
.retrieve()
.bodyToMono(tipoListaParametrizada);
Este enfoque garantiza que el deserializador conozca la estructura completa del tipo genérico, incluyendo la lista de objetos Datos
.
Es importante destacar que, al usar estas referencias de tipo, se debe crear una clase anónima debido a cómo Java maneja los genéricos. La creación de una instancia anónima de TypeReference
o ParameterizedTypeReference
permite capturar la información de tipos que, de otra manera, se perdería con el type erasure.
Otra situación común es cuando se trabaja con métodos genéricos dentro de servicios que consumen diferentes tipos de datos. Por ejemplo:
public <T> Mono<Respuesta<T>> obtenerRespuesta(String endpoint, Class<T> claseTipo) {
ParameterizedTypeReference<Respuesta<T>> tipoParametrizado =
new ParameterizedTypeReference<>() {};
return webClient.get()
.uri(endpoint)
.retrieve()
.bodyToMono(tipoParametrizado);
}
No obstante, este código presenta un problema debido al type erasure, ya que T
no es conocido en tiempo de ejecución. Para solventarlo, se puede modificar el método para recibir también la ParameterizedTypeReference
:
public <T> Mono<T> obtenerRespuesta(String endpoint, ParameterizedTypeReference<T> tipoRef) {
return webClient.get()
.uri(endpoint)
.retrieve()
.bodyToMono(tipoRef);
}
Al llamar al método, se proporciona el ParameterizedTypeReference
correspondiente:
ParameterizedTypeReference<Respuesta<Datos>> tipoRef = new ParameterizedTypeReference<>() {};
Mono<Respuesta<Datos>> respuesta = servicio.obtenerRespuesta("/api/datos", tipoRef);
Este patrón permite crear métodos genéricos que pueden manejar diferentes tipos, manteniendo la información de tipo necesaria para la deserialización.
En el contexto de programación funcional, es posible utilizar expresiones lambda y referencias de método para simplificar aún más el código. Por ejemplo, al procesar la respuesta obtenida:
respuestaMono.flatMap(respuesta -> procesarDatos(respuesta.getDatos()))
.subscribe();
Aquí, procesarDatos
es un método que realiza operaciones adicionales sobre los datos recibidos. La utilización de stream y operadores reactivos permite manejar los flujos de datos de manera eficiente y moderna.
También es relevante cuando se consume un API que devuelve datos paginados o anidados con estructuras genéricas complejas. En tales casos, es crucial proporcionar la información de tipo completa al deserializador:
ParameterizedTypeReference<Respuesta<Paginacion<Datos>>> tipoPaginado =
new ParameterizedTypeReference<>() {};
Mono<Respuesta<Paginacion<Datos>>> respuestaPaginada = webClient.get()
.uri("/api/datos/paginados")
.retrieve()
.bodyToMono(tipoPaginado);
Donde Paginacion<T>
es una clase genérica que contiene información sobre la página actual, número total de páginas, y la lista de datos.
Además, es importante manejar correctamente las excepciones y errores que puedan surgir durante la deserialización. Al utilizar genéricos y referencias de tipo, se pueden capturar y manejar los errores de manera específica:
respuestaMono
.doOnError(JsonProcessingException.class, error -> {
// Manejo específico para errores de deserialización JSON
})
.onErrorResume(e -> Mono.error(new CustomException("Error al procesar la respuesta", e)))
.subscribe();
De esta forma, se garantiza que la aplicación responde adecuadamente ante distintas situaciones erróneas.
Ejercicios de esta lección Cliente reactivo WebClient
Evalúa tus conocimientos de esta lección Cliente reactivo WebClient con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
API Query By Example (QBE)
Identificadores y relaciones JPA
Borrar datos de base de datos
Web y Test Starters
Métodos find en repositorios
Controladores Spring MVC
Inserción de datos
CRUD Customers Spring MVC + Spring Data JPA
Backend API REST con Spring Boot
Controladores Spring REST
Uso de Spring con Thymeleaf
API Specification
Registro de usuarios
Crear entidades JPA
Asociaciones en JPA
Asociaciones de entidades JPA
Integración con Vue
Consultas JPQL
Open API y cómo agregarlo en Spring Boot
Uso de Controladores REST
Repositorios reactivos
Inyección de dependencias
Introducción a Spring Boot
CRUD y JPA Repository
Inyección de dependencias
Vista en Spring MVC con Thymeleaf
Servicios en Spring
Operadores Reactivos
Configuración de Vue
Entidades JPA
Integración con Angular
API Specification
API Query By Example (QBE)
Controladores MVC
Anotaciones y mapeo en JPA
Consultas JPQL con @Query en Spring Data JPA
Repositorios Spring Data
Inyección de dependencias
Data JPA y Mail Starters
Configuración de Angular
Controladores Spring REST
Configuración de Controladores MVC
Consultas JPQL con @Query en Spring Data JPA
Actualizar datos de base de datos
Verificar token JWT en peticiones
Login de usuarios
Integración con React
Configuración de React
Todas las lecciones de SpringBoot
Accede a todas las lecciones de SpringBoot y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Spring Boot
Introducción Y Entorno
Spring Boot Starters
Introducción Y Entorno
Inyección De Dependencias
Introducción Y Entorno
Controladores Spring Mvc
Spring Web
Vista En Spring Mvc Con Thymeleaf
Spring Web
Controladores Spring Rest
Spring Web
Open Api Y Cómo Agregarlo En Spring Boot
Spring Web
Servicios En Spring
Spring Web
Clientes Resttemplate Y Restclient
Spring Web
Rxjava En Spring Web
Spring Web
Crear Entidades Jpa
Persistencia Spring Data
Asociaciones De Entidades Jpa
Persistencia Spring Data
Repositorios Spring Data
Persistencia Spring Data
Métodos Find En Repositorios
Persistencia Spring Data
Inserción De Datos
Persistencia Spring Data
Actualizar Datos De Base De Datos
Persistencia Spring Data
Borrar Datos De Base De Datos
Persistencia Spring Data
Consultas Jpql Con @Query En Spring Data Jpa
Persistencia Spring Data
Api Query By Example (Qbe)
Persistencia Spring Data
Api Specification
Persistencia Spring Data
Repositorios Reactivos
Persistencia Spring Data
Introducción E Instalación De Apache Kafka
Mensajería Asíncrona
Crear Proyecto Con Apache Kafka
Mensajería Asíncrona
Creación De Producers
Mensajería Asíncrona
Creación De Consumers
Mensajería Asíncrona
Kafka Streams En Spring Boot
Mensajería Asíncrona
Introducción A Spring Webflux
Reactividad Webflux
Spring Data R2dbc
Reactividad Webflux
Controlador Rest Reactivo Basado En Anotaciones
Reactividad Webflux
Controlador Rest Reactivo Funcional
Reactividad Webflux
Operadores Reactivos Básicos
Reactividad Webflux
Operadores Reactivos Avanzados
Reactividad Webflux
Cliente Reactivo Webclient
Reactividad Webflux
Introducción A Spring Security
Seguridad Con Spring Security
Seguridad Basada En Formulario En Mvc Con Thymeleaf
Seguridad Con Spring Security
Registro De Usuarios
Seguridad Con Spring Security
Login De Usuarios
Seguridad Con Spring Security
Verificar Token Jwt En Peticiones
Seguridad Con Spring Security
Seguridad Jwt En Api Rest Spring Web
Seguridad Con Spring Security
Seguridad Jwt En Api Rest Reactiva Spring Webflux
Seguridad Con Spring Security
Autenticación Y Autorización Con Anotaciones
Seguridad Con Spring Security
Testing Unitario De Componentes Y Servicios
Testing Con Spring Test
Testing De Repositorios Spring Data Jpa Y Acceso A Datos Con Spring Test
Testing Con Spring Test
Testing Controladores Spring Mvc Con Thymeleaf
Testing Con Spring Test
Testing Controladores Rest Con Json
Testing Con Spring Test
Testing De Aplicaciones Reactivas Webflux
Testing Con Spring Test
Testing De Seguridad Spring Security
Testing Con Spring Test
Testing Con Apache Kafka
Testing Con Spring Test
Integración Con Angular
Integración Frontend
Integración Con React
Integración Frontend
Integración Con Vue
Integración Frontend
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender qué es el WebClient
- Realizar peticiones GET con WebClient
- Realizar peticiones POST con WebClient
- Realizar peticiones PUT con WebClient
- Realizar peticiones PATCH con WebClient
- Realizar peticiones DELETE con WebClient
- Gestión de errores con WebClient