Contactez-nous

Utilisation de `WebClient` pour les appels HTTP réactifs

Apprenez à utiliser WebClient, le client HTTP moderne et non bloquant de Spring, pour effectuer des requêtes asynchrones et réactives dans vos applications.

Introduction : Le client HTTP moderne de Spring

Dans les applications modernes, en particulier celles basées sur des architectures microservices ou interagissant avec de nombreuses API externes, effectuer des appels HTTP est une tâche omniprésente. Traditionnellement, Spring proposait `RestTemplate` pour cela. Cependant, `RestTemplate` est basé sur un modèle bloquant, ce qui le rend moins adapté aux applications réactives qui visent à optimiser l'utilisation des ressources et à gérer une haute concurrence.

Pour répondre aux besoins de la programmation réactive, Spring 5 a introduit `WebClient`. C'est un client HTTP moderne, non bloquant et réactif, construit sur le framework Project Reactor (`Mono` et `Flux`). Il offre une API fluide (fluent API) pour construire et exécuter des requêtes HTTP de manière asynchrone, s'intégrant parfaitement avec Spring WebFlux et l'écosystème réactif.

Que vous construisiez une application entièrement réactive avec WebFlux ou que vous souhaitiez simplement introduire des appels sortants non bloquants dans une application Spring MVC traditionnelle, `WebClient` est l'outil recommandé pour les interactions HTTP modernes dans Spring.

Configurer et obtenir une instance de WebClient

Pour utiliser `WebClient`, vous devez d'abord ajouter la dépendance `spring-boot-starter-webflux` à votre projet (même si vous n'utilisez pas WebFlux pour la partie serveur, ce starter inclut `WebClient` et ses dépendances nécessaires comme Reactor Netty).


    org.springframework.boot
    spring-boot-starter-webflux

Il existe plusieurs façons d'obtenir une instance de `WebClient` :

  • Création simple : Pour une utilisation basique, vous pouvez créer une instance directement :
    WebClient client = WebClient.create(); // Client avec configuration par défaut
    WebClient clientWithBaseUrl = WebClient.create("http://api.example.com"); // Client avec une URL de base
  • Utilisation du Builder : Pour plus de contrôle sur la configuration (timeouts, stratégies de connexion, codecs, filtres, en-têtes par défaut, etc.), utilisez le builder :
    WebClient client = WebClient.builder()
            .baseUrl("http://api.example.com")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .defaultCookie("cookieKey", "cookieValue")
            // Ajouter des filtres (ex: pour logging, authentification)
            .filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> Mono.just(clientRequest)))
            // Configurer les timeouts
            .clientConnector(new ReactorClientHttpConnector(
                    HttpClient.create().responseTimeout(Duration.ofSeconds(20))
            ))
            .build();
  • Injection via Spring Boot : La manière la plus courante et recommandée dans une application Spring Boot est d'injecter un `WebClient.Builder` préconfiguré par Spring Boot. Ce builder hérite des configurations globales (comme les codecs). Vous pouvez ensuite le personnaliser davantage si nécessaire.
    import org.springframework.stereotype.Service;
    import org.springframework.web.reactive.function.client.WebClient;
    
    @Service
    public class MyApiClient {
    
        private final WebClient webClient;
    
        // Injecte le builder pré-configuré par Spring Boot
        public MyApiClient(WebClient.Builder webClientBuilder) {
            // Vous pouvez le personnaliser ici si besoin
            this.webClient = webClientBuilder
                                .baseUrl("http://api.external-service.com")
                                .build();
        }
    
        // ... méthodes utilisant this.webClient ...
    }
    

Effectuer des requêtes HTTP réactives

L'API fluide de `WebClient` rend la construction de requêtes très intuitive. Le processus général est :

  1. Choisir la méthode HTTP (`.get()`, `.post()`, `.put()`, `.delete()`, `.patch()`, `.method(HttpMethod)`).
  2. Spécifier l'URI (`.uri(...)`). Peut être une chaîne, un `URI`, ou utiliser un template avec variables.
  3. (Optionnel) Configurer les en-têtes (`.header(key, value)`, `.accept(MediaType)`, `.contentType(MediaType)`).
  4. (Optionnel, pour POST/PUT/PATCH) Fournir le corps de la requête (`.bodyValue(object)`, `.body(BodyInserters.fromPublisher(Mono/Flux, Class))`).
  5. Spécifier comment récupérer la réponse (`.retrieve()` ou `.exchangeToMono()` / `.exchangeToFlux()`).
  6. Décrire comment traiter la réponse (ex: convertir le corps en `Mono` ou `Flux`).

Exemple GET simple (récupérer un objet unique) :

// Supposons une classe Product
Mono productMono = webClient.get()        // 1. Méthode GET
    .uri("/products/{id}", 123)                  // 2. URI avec variable
    .accept(MediaType.APPLICATION_JSON)        // 3. Header Accept
    .retrieve()                                // 5. Récupérer la réponse (méthode simple)
    .bodyToMono(Product.class);                // 6. Convertir le corps en Mono

// La requête n'est exécutée que lors de la souscription
productMono.subscribe(
    product -> System.out.println("Produit reçu: " + product.getName()),
    error -> System.err.println("Erreur lors de la récupération: " + error.getMessage())
);

Exemple GET (récupérer une liste/flux d'objets) :

Flux productsFlux = webClient.get()
    .uri("/products?category=electronics")     // URI avec paramètre de requête
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .bodyToFlux(Product.class);                 // Convertir le corps en Flux

productsFlux.subscribe(
    product -> System.out.println("Produit: " + product.getName())
);

Exemple POST (envoyer un objet et recevoir un objet créé) :

Product newProduct = new Product(null, "Nouveau Gadget", 99.99);

Mono createdProductMono = webClient.post() // 1. Méthode POST
    .uri("/products")                           // 2. URI
    .contentType(MediaType.APPLICATION_JSON)    // 3. Header Content-Type
    .accept(MediaType.APPLICATION_JSON)
    .bodyValue(newProduct)                      // 4. Corps de la requête
    // Ou: .body(Mono.just(newProduct), Product.class)
    .retrieve()
    .bodyToMono(Product.class);

createdProductMono.subscribe(
    created -> System.out.println("Produit créé avec ID: " + created.getId())
);

Exemple DELETE (pas de corps de réponse attendu) :

Mono deleteResultMono = webClient.delete() // 1. Méthode DELETE
    .uri("/products/{id}", 456)                 // 2. URI
    .retrieve()
    .bodyToMono(Void.class);                    // 6. Attendre la complétion (pas de corps)

deleteResultMono.subscribe(
    null, // Pas d'action onNext car Mono
    error -> System.err.println("Erreur lors de la suppression: " + error),
    () -> System.out.println("Produit supprimé avec succès") // Action onComplete
);

Traitement avancé des réponses : `retrieve()` vs `exchangeTo...()`

`WebClient` offre deux approches principales pour traiter la réponse :

  1. `.retrieve()` : C'est la méthode la plus simple et la plus directe. Elle est idéale lorsque vous êtes principalement intéressé par le corps de la réponse et que vous considérez les statuts 4xx (erreur client) et 5xx (erreur serveur) comme des erreurs. Si un statut 4xx ou 5xx est reçu, `.retrieve()` lève automatiquement une `WebClientResponseException`. Vous pouvez ensuite utiliser `.bodyToMono()` ou `.bodyToFlux()` pour extraire le corps. Vous pouvez aussi ajouter un traitement d'erreur spécifique basé sur le statut avec `.onStatus(...)`.
  2. `.exchangeToMono()` / `.exchangeToFlux()` : Ces méthodes offrent un contrôle beaucoup plus fin. Elles vous donnent accès à l'objet `ClientResponse` complet, qui contient le statut, les en-têtes et le corps (sous forme de `Mono` ou `Flux`). C'est à vous d'inspecter le statut, de lire les en-têtes et surtout, de consommer explicitement le corps de la réponse (même si vous n'en avez pas besoin) pour libérer la connexion. Si vous ne consommez pas le corps, vous risquez des fuites de ressources. Ces méthodes sont utiles lorsque vous devez gérer différemment certains codes d'erreur (ex: traiter un 404 différemment d'un 500) ou extraire des informations des en-têtes de réponse.

Exemple avec `retrieve()` et `.onStatus()` pour gérer un 404 :

Mono productMono = webClient.get()
    .uri("/products/{id}", 999) // ID qui n'existe pas
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    // Gérer spécifiquement le statut 404 Not Found
    .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> 
        Mono.error(new ProductNotFoundException("Produit 999 non trouvé"))
    )
    // Gérer les autres erreurs serveur
    .onStatus(HttpStatusCode::is5xxServerError, clientResponse ->
        clientResponse.bodyToMono(String.class) // Essayer de lire le corps de l'erreur
            .flatMap(errorBody -> Mono.error(new ApiServiceException("Erreur serveur: " + errorBody)))
            .onErrorResume(ex -> Mono.error(new ApiServiceException("Erreur serveur inconnue")))
    )
    .bodyToMono(Product.class)
    // Alternative si 404 doit retourner un Mono vide
    .onErrorResume(ProductNotFoundException.class, ex -> Mono.empty()); 

Exemple avec `exchangeToMono` :

Mono productMono = webClient.get()
    .uri("/products/{id}", 123)
    .accept(MediaType.APPLICATION_JSON)
    .exchangeToMono(clientResponse -> { // Traitement explicite de la réponse
        if (clientResponse.statusCode().is2xxSuccessful()) {
            // OK, extraire le corps
            return clientResponse.bodyToMono(Product.class);
        } else if (clientResponse.statusCode().equals(HttpStatus.NOT_FOUND)) {
            // Gérer le 404 (ex: retourner Mono vide ou erreur spécifique)
            return Mono.empty(); // ou Mono.error(new ProductNotFoundException(...))
        } else {
            // Autre erreur, lire le corps si possible et lever une exception
            return clientResponse.createException().flatMap(Mono::error);
            // Ou lire le corps manuellement :
            // return clientResponse.bodyToMono(String.class)
            //           .flatMap(body -> Mono.error(new RuntimeException("Erreur " + clientResponse.statusCode() + ": " + body)));
        }
    });

En général, préférez `.retrieve()` pour sa simplicité lorsque le traitement d'erreur par défaut vous convient ou peut être adapté avec `.onStatus()`. Utilisez `.exchangeToMono()` / `.exchangeToFlux()` lorsque vous avez besoin d'un contrôle total sur la logique de traitement de la réponse et des erreurs.

Conclusion : La puissance des appels HTTP réactifs

`WebClient` est le client HTTP de choix pour les applications Spring modernes, qu'elles soient entièrement réactives avec WebFlux ou qu'elles cherchent à améliorer leur efficacité en introduisant des opérations non bloquantes dans une architecture MVC traditionnelle. Son API fluide, son intégration native avec Project Reactor (`Mono`/`Flux`) et sa nature non bloquante permettent de construire des applications plus résilientes, plus performantes et capables de gérer une charge élevée avec moins de ressources.

Maîtriser `WebClient`, comprendre la différence entre `retrieve()` et `exchangeTo...()`, et savoir gérer les réponses et les erreurs de manière réactive sont des compétences essentielles pour tout développeur travaillant avec l'écosystème Spring aujourd'hui.