Spring Boot

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ícate

Qué 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 un Mono 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 un Mono<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.

Aprende SpringBoot GRATIS online

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)

Spring Boot
Test

Identificadores y relaciones JPA

Spring Boot
Puzzle

Borrar datos de base de datos

Spring Boot
Test

Web y Test Starters

Spring Boot
Puzzle

Métodos find en repositorios

Spring Boot
Test

Controladores Spring MVC

Spring Boot
Código

Inserción de datos

Spring Boot
Test

CRUD Customers Spring MVC + Spring Data JPA

Spring Boot
Proyecto

Backend API REST con Spring Boot

Spring Boot
Proyecto

Controladores Spring REST

Spring Boot
Código

Uso de Spring con Thymeleaf

Spring Boot
Puzzle

API Specification

Spring Boot
Puzzle

Registro de usuarios

Spring Boot
Test

Crear entidades JPA

Spring Boot
Código

Asociaciones en JPA

Spring Boot
Test

Asociaciones de entidades JPA

Spring Boot
Código

Integración con Vue

Spring Boot
Test

Consultas JPQL

Spring Boot
Código

Open API y cómo agregarlo en Spring Boot

Spring Boot
Puzzle

Uso de Controladores REST

Spring Boot
Puzzle

Repositorios reactivos

Spring Boot
Test

Inyección de dependencias

Spring Boot
Test

Introducción a Spring Boot

Spring Boot
Test

CRUD y JPA Repository

Spring Boot
Puzzle

Inyección de dependencias

Spring Boot
Código

Vista en Spring MVC con Thymeleaf

Spring Boot
Test

Servicios en Spring

Spring Boot
Código

Operadores Reactivos

Spring Boot
Puzzle

Configuración de Vue

Spring Boot
Puzzle

Entidades JPA

Spring Boot
Test

Integración con Angular

Spring Boot
Test

API Specification

Spring Boot
Test

API Query By Example (QBE)

Spring Boot
Puzzle

Controladores MVC

Spring Boot
Test

Anotaciones y mapeo en JPA

Spring Boot
Puzzle

Consultas JPQL con @Query en Spring Data JPA

Spring Boot
Test

Repositorios Spring Data

Spring Boot
Test

Inyección de dependencias

Spring Boot
Puzzle

Data JPA y Mail Starters

Spring Boot
Test

Configuración de Angular

Spring Boot
Puzzle

Controladores Spring REST

Spring Boot
Test

Configuración de Controladores MVC

Spring Boot
Puzzle

Consultas JPQL con @Query en Spring Data JPA

Spring Boot
Puzzle

Actualizar datos de base de datos

Spring Boot
Test

Verificar token JWT en peticiones

Spring Boot
Test

Login de usuarios

Spring Boot
Test

Integración con React

Spring Boot
Test

Configuración de React

Spring Boot
Puzzle

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

Spring Boot

Introducción Y Entorno

Spring Boot Starters

Spring Boot

Introducción Y Entorno

Inyección De Dependencias

Spring Boot

Introducción Y Entorno

Controladores Spring Mvc

Spring Boot

Spring Web

Vista En Spring Mvc Con Thymeleaf

Spring Boot

Spring Web

Controladores Spring Rest

Spring Boot

Spring Web

Open Api Y Cómo Agregarlo En Spring Boot

Spring Boot

Spring Web

Servicios En Spring

Spring Boot

Spring Web

Clientes Resttemplate Y Restclient

Spring Boot

Spring Web

Rxjava En Spring Web

Spring Boot

Spring Web

Crear Entidades Jpa

Spring Boot

Persistencia Spring Data

Asociaciones De Entidades Jpa

Spring Boot

Persistencia Spring Data

Repositorios Spring Data

Spring Boot

Persistencia Spring Data

Métodos Find En Repositorios

Spring Boot

Persistencia Spring Data

Inserción De Datos

Spring Boot

Persistencia Spring Data

Actualizar Datos De Base De Datos

Spring Boot

Persistencia Spring Data

Borrar Datos De Base De Datos

Spring Boot

Persistencia Spring Data

Consultas Jpql Con @Query En Spring Data Jpa

Spring Boot

Persistencia Spring Data

Api Query By Example (Qbe)

Spring Boot

Persistencia Spring Data

Api Specification

Spring Boot

Persistencia Spring Data

Repositorios Reactivos

Spring Boot

Persistencia Spring Data

Introducción E Instalación De Apache Kafka

Spring Boot

Mensajería Asíncrona

Crear Proyecto Con Apache Kafka

Spring Boot

Mensajería Asíncrona

Creación De Producers

Spring Boot

Mensajería Asíncrona

Creación De Consumers

Spring Boot

Mensajería Asíncrona

Kafka Streams En Spring Boot

Spring Boot

Mensajería Asíncrona

Introducción A Spring Webflux

Spring Boot

Reactividad Webflux

Spring Data R2dbc

Spring Boot

Reactividad Webflux

Controlador Rest Reactivo Basado En Anotaciones

Spring Boot

Reactividad Webflux

Controlador Rest Reactivo Funcional

Spring Boot

Reactividad Webflux

Operadores Reactivos Básicos

Spring Boot

Reactividad Webflux

Operadores Reactivos Avanzados

Spring Boot

Reactividad Webflux

Cliente Reactivo Webclient

Spring Boot

Reactividad Webflux

Introducción A Spring Security

Spring Boot

Seguridad Con Spring Security

Seguridad Basada En Formulario En Mvc Con Thymeleaf

Spring Boot

Seguridad Con Spring Security

Registro De Usuarios

Spring Boot

Seguridad Con Spring Security

Login De Usuarios

Spring Boot

Seguridad Con Spring Security

Verificar Token Jwt En Peticiones

Spring Boot

Seguridad Con Spring Security

Seguridad Jwt En Api Rest Spring Web

Spring Boot

Seguridad Con Spring Security

Seguridad Jwt En Api Rest Reactiva Spring Webflux

Spring Boot

Seguridad Con Spring Security

Autenticación Y Autorización Con Anotaciones

Spring Boot

Seguridad Con Spring Security

Testing Unitario De Componentes Y Servicios

Spring Boot

Testing Con Spring Test

Testing De Repositorios Spring Data Jpa Y Acceso A Datos Con Spring Test

Spring Boot

Testing Con Spring Test

Testing Controladores Spring Mvc Con Thymeleaf

Spring Boot

Testing Con Spring Test

Testing Controladores Rest Con Json

Spring Boot

Testing Con Spring Test

Testing De Aplicaciones Reactivas Webflux

Spring Boot

Testing Con Spring Test

Testing De Seguridad Spring Security

Spring Boot

Testing Con Spring Test

Testing Con Apache Kafka

Spring Boot

Testing Con Spring Test

Integración Con Angular

Spring Boot

Integración Frontend

Integración Con React

Spring Boot

Integración Frontend

Integración Con Vue

Spring Boot

Integración Frontend

Accede GRATIS a SpringBoot y certifícate

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