Aller au contenu

Spring Boot REST Client : découvrez les HTTP Interfaces, l’alternative moderne à WebClient

Découvrez comment les HTTP Interfaces de Spring Boot révolutionnent la création de clients REST. Plus simples et plus lisibles que Spring WebClient, elles permettent de consommer des API de manière déclarative, avec moins de code et une meilleure maintenabilité.

Spring HTTP Interface
Spring HTTP Interface

Sommaire

  1. Introduction 📚
  2. Qu'est-ce que HttpExchange ? 🤔
  3. Qu'est-ce que HttpServiceProxyFactory ? 🏭
  4. Mise en place d'un projet Spring 🛠️
  5. Créer un client HTTP déclaratif 📝
  6. Cas d'usage avancés 🔬
  7. Gestion des erreurs et exceptions 🚨
  8. Tests unitaires et d'intégration 🧪
  9. Bonnes pratiques, FAQ et limitations 💡
  10. Aller plus loin / Pour experts 🏅
  11. Conclusion 🎯
  12. Glossaire express (vulgarisé)

Introduction 📚

Cet article s'adresse aux développeurs Java/Spring de niveau intermédiaire qui souhaitent consommer des API REST externes de façon moderne et typée. Avec Spring 6 et Spring Boot 3, l'interface HttpExchange propose une nouvelle façon, simple et déclarative, de déclarer des clients HTTP, à la manière de Feign ou Retrofit, mais 100% Spring. Ce guide vous explique pas à pas comment l'utiliser efficacement, avec des exemples concrets et des conseils pratiques. ✨

À la fin de cet article, vous saurez comment intégrer, tester et fiabiliser vos appels HTTP dans vos applications Spring modernes. 💪

Version vulgarisée en 20 secondes

  • HttpExchange = vous écrivez le "contrat" (interface Java), Spring fait le "travail technique".
  • HttpServiceProxyFactory = la "machine" qui transforme ce contrat en vrai client HTTP utilisable.
  • Résultat : moins de code technique, plus de code métier, moins d'erreurs.

1. Qu'est-ce que HttpExchange ? 🤔

HttpExchange permet de définir des interfaces Java annotées qui décrivent les appels HTTP à effectuer. Spring se charge de générer l'implémentation à l'exécution. Cela permet :

  • D'écrire du code client HTTP lisible, typé et testable ✅
  • De s'intégrer nativement à l'écosystème Spring (sécurité, configuration, etc.) 🌱
  • De réduire le code « boilerplate » (infrastructure) 🧹

Image mentale (vulgarisation)

Imaginez une télécommande :

  • les boutons = les méthodes de votre interface (getUserById, createUser, etc.)
  • la télécommande ne contient pas la TV, mais sait quoi déclencher
  • Spring joue le rôle de l'électronique interne qui envoie le bon signal HTTP

Vous appuyez sur un bouton (méthode Java), Spring envoie l'appel HTTP réel.

Schéma de choix rapide (débutant) :

flowchart TD
A["Je dois appeler une API HTTP"] --> B{"Je suis en mode reactif ?"}
B -->|"Non"| C["RestClient + HttpExchange"]
B -->|"Oui"| D["WebClient + HttpExchange"]
C --> E["Code simple, synchrone"]
D --> F["Flux reactifs Mono/Flux"]

Schéma du fonctionnement général:

flowchart LR
A["Interface annotée (UserClient)"] -->|"Décrit les endpoints"| B["HttpExchange"]
B -->|"Analyse à l'exécution"| C["Spring"]
C -->|"Génère"| D["Implémentation dynamique du client"]
D -->|"Appels HTTP typés"| E["API distante"]

Ce schéma illustre comment une interface annotée avec HttpExchange est analysée par Spring pour générer dynamiquement un client HTTP typé.

Comparaison rapide:

Approche Déclaratif Typé Intégration Spring Modernité
RestTemplate Non Oui Oui Moyen
WebClient Non Oui Oui Oui
Feign Oui Oui Partiel (lib) Oui
HttpExchange Oui Oui Oui Oui

Pourquoi la déclarativité ?
Déclarer ses appels HTTP sous forme d'interfaces permet d'écrire moins de code, d'améliorer la lisibilité, de faciliter les tests et de réduire les erreurs liées au mapping manuel. 🧑‍💻


Qu'est-ce que HttpServiceProxyFactory ? 🏭

HttpServiceProxyFactory est une classe utilitaire de Spring qui permet de générer dynamiquement une implémentation d'une interface Java annotée avec @HttpExchange, @GetExchange, etc.

Concrètement, elle prend une interface qui décrit des appels HTTP (par exemple UserClient) et crée automatiquement, à l'exécution, un "proxy" qui réalise les requêtes HTTP correspondantes.

Fonctionnement:

  • Vous déclarez une interface Java annotée pour décrire vos endpoints HTTP.
  • Vous configurez un client HTTP (RestClient ou WebClient) avec l'URL de base, les timeouts, etc.
  • Vous utilisez HttpServiceProxyFactory pour lier ce client à votre interface.
  • Spring génère alors un bean prêt à l'emploi, typé, testable et intégré à l'écosystème Spring.

Exemple:

// caption: Création d'un bean UserClient
@Bean
UserClient userClient(RestClient.Builder builder) {
  RestClient restClient = builder.baseUrl("https://jsonplaceholder.typicode.com").build();
  HttpServiceProxyFactory factory = HttpServiceProxyFactory
    .builderFor(RestClientAdapter.create(restClient))
    .build();
  return factory.createClient(UserClient.class);
}

Résumé:
HttpServiceProxyFactory automatise la création de clients HTTP typés à partir d'interfaces Java, sans avoir à écrire le code d'appel HTTP manuellement. Cela favorise la lisibilité, la testabilité et la maintenance du code. 🤖


Schéma du rôle de HttpServiceProxyFactory:

flowchart LR
A["RestClient/WebClient configuré"] --> B["HttpServiceProxyFactory"]
B -->|"createClient"| C["Proxy dynamique de l'interface"]
C -->|"Utilisation dans Spring"| D["Service métier"]

Ce schéma montre comment la factory relie un client HTTP configuré à une interface pour produire un proxy prêt à l'emploi dans Spring.

2. Mise en place d'un projet Spring avec HttpExchange 🛠️

Prérequis

  • Spring Boot 3.x ou Spring Framework 6.x
  • Dépendance Maven:
// caption: Ajout du starter web
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Pour WebFlux:

// caption: Ajout du starter web flux
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Exemple Gradle:

// caption: Ajout du starter web/webflux
implementation 'org.springframework.boot:spring-boot-starter-web'
// Pour WebFlux :
implementation 'org.springframework.boot:spring-boot-starter-webflux'

Exemple de configuration de timeout dans application.yml:

// caption: Configuration de l'api
clients:
  user-api:
    base-url: https://jsonplaceholder.typicode.com
    connect-timeout: 3s
    read-timeout: 5s

Vous pouvez ensuite lire ces propriétés via @ConfigurationProperties et les appliquer dans la construction du RestClient.


3. Créer un client HTTP déclaratif 📝

Supposons que vous souhaitez consommer l'API publique JSONPlaceholder pour manipuler des utilisateurs.

a) Déclaration de l'interface

// caption: Déclaration des endpoints
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

// L'URL de base sera définie dans la configuration du bean (voir plus bas)
// Vous pouvez aussi versionner l'API ici : @HttpExchange("/v1/users")
@HttpExchange("/users")
public interface UserClient {
  @GetExchange
  List<User> getAllUsers();

  @GetExchange("/{id}")
  User getUserById(@PathVariable Long id);

  @PostExchange
  User createUser(@RequestBody User user);
}

b) Exemple de DTO (Data Transfer Object)

// caption: Création du model User
// Le DTO doit correspondre à la structure JSON de l'API cible
public record User(Long id, String name, String username, String email) {
}

c) Configuration du bean client

// caption: Configuration du Bean Client
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Bean
UserClient userClient(RestClient.Builder builder) {
  // Définir ici l'URL de base de l'API
  RestClient restClient = builder.baseUrl("https://jsonplaceholder.typicode.com").build();
  HttpServiceProxyFactory factory = HttpServiceProxyFactory
    .builderFor(RestClientAdapter.create(restClient))
    .build();
  return factory.createClient(UserClient.class);
}
// Pensez à externaliser l'URL de base dans vos fichiers de configuration pour plus de flexibilité

d) Utilisation dans un service Spring

// caption: Utilisation du client dans le service
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
  private final UserClient userClient;

  public UserService(UserClient userClient) {
    this.userClient = userClient;
  }

  public List<User> fetchAllUsers() {
    return userClient.getAllUsers();
  }

  public User fetchUser(Long id) {
    return userClient.getUserById(id);
  }
}

Cycle de vie d’un appel client :

sequenceDiagram
participant Service as Service Spring
participant Client as Interface annotée (UserClient)
participant Proxy as Proxy dynamique
participant API as API distante
Service->>Client: Appel méthode (ex: getAllUsers)
Client->>Proxy: Délégation automatique
Proxy->>API: Requête HTTP (GET /users)
API-->>Proxy: Réponse JSON
Proxy-->>Client: Mapping vers DTO (User)
Client-->>Service: Retourne le résultat typé

Ce diagramme séquentiel illustre le cheminement d’un appel HTTP depuis le service Spring jusqu’à la réponse typée.

e) Mini scénario concret (ultra simple)

Cas concret: "Je dois afficher la fiche d'un utilisateur dans mon app".

  1. Le contrôleur appelle userService.fetchUser(42).
  2. UserService appelle userClient.getUserById(42).
  3. Le proxy Spring envoie GET /users/42.
  4. La réponse JSON est convertie en User.
  5. Votre code récupère un objet Java prêt à l'emploi.

Pourquoi c'est plus simple ?

  • Vous raisonnez en Java métier (User) et non en infrastructure technique HTTP.
  • Le mapping JSON -> objet est centralisé.
  • Les tests sont plus faciles à écrire.

4. Cas d'usage avancés 🔬

a) Appel POST avec payload

// caption: Appel de l'endpoint avec un payload
User newUser = new User(null, "Jean Dupont", "jdupont", "jean@exemple.com");
User created = userClient.createUser(newUser);

b) Passage de headers personnalisés

// caption: Ajout du header dans l'appel Client
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;

// La clé API peut venir d'une propriété de configuration (ex: @Value)

@GetExchange("/{id}")
User getUserWithApiKey(@PathVariable Long id, @RequestHeader("X-API-KEY") String apiKey);

c) Paramètres de requête

// caption: Paramètrage des appels (mettre en query params)
import org.springframework.web.bind.annotation.RequestParam;

@GetExchange()
List<User> searchUsers(@RequestParam String name);

// Exemple d'appel : userClient.searchUsers("Lea");
// Générera une requête GET /users?name=Lea

5. Gestion des erreurs et exceptions 🚨

Spring lève automatiquement des exceptions pour les codes HTTP d’erreur (4xx, 5xx), selon le client HTTP sous-jacent (RestClient, WebClient). Vous pouvez les intercepter:

// caption: Gestion des erreurs
import org.springframework.web.client.HttpClientErrorException;

public User getUserSafe(Long id) {
  try {
    return userClient.getUserById(id);
  } catch (HttpClientErrorException.NotFound e) {
    // Gérer l'absence d'utilisateur
    throw new UserNotFoundException("Utilisateur non trouvé");
  }
}

// Exemple d'exception personnalisée
public class UserNotFoundException extends RuntimeException {
  public UserNotFoundException(String message) {
    super(message);
  }
}

Pour une gestion globale, utilisez un @ControllerAdvice ou personnalisez la gestion dans vos services.

Exemple minimaliste:

// caption: Jeter une erreur de type ProblemDetail (RFC 9457)
import java.net.URI;

import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(UserNotFoundException.class)
  public ProblemDetail handleUserNotFound(UserNotFoundException ex) {
    ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
    problem.setTitle("Utilisateur non trouvé");
    problem.setType(URI.create("https://example.com/errors/user-not-found"));
    problem.setDetail(ex.getMessage());
    return problem;
  }
}

6. Tests unitaires et d'intégration 🧪

a) Test unitaire avec MockWebServer

Pour simuler des réponses HTTP sans dépendre d'un serveur externe, utilisez MockWebServer:

// caption: Test Unitaire du Client
import java.util.List;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.Test;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

import static org.assertj.core.api.Assertions.assertThat;

@Test
void testGetAllUsers_withMockWebServer() throws Exception {
  try (MockWebServer server = new MockWebServer()) {
    server.enqueue(new MockResponse().setBody("[{\"id\":1,\"name\":\"Lea\"}]").setResponseCode(200));
    server.start();

    RestClient restClient = RestClient.builder()
      .baseUrl(server.url("/").toString())
      .build();
    UserClient userClient = HttpServiceProxyFactory
      .builderFor(RestClientAdapter.create(restClient))
      .build()
      .createClient(UserClient.class);

    List<User> users = userClient.getAllUsers();
    assertThat(users).isNotEmpty();
  }
}

b) Test d’intégration

Pour tester l'intégration réelle du client HTTP, privilégiez @SpringBootTest avec un serveur de test comme
WireMock:

// caption: Test Intégration du Client
import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertFalse;

@SpringBootTest
class UserClientIntegrationTest {
  @Autowired
  UserClient userClient;

  @Test
  void testGetAllUsers() {
    List<User> users = userClient.getAllUsers();
    assertFalse(users.isEmpty());
  }
}

7. Bonnes pratiques, FAQ et limitations 💡

  • Utilisez des DTOs pour le typage fort
  • Gérez les erreurs explicitement (ne masquez pas les exceptions)
  • Privilégiez l’injection via constructeur pour la testabilité
  • Externalisez l'URL de base et les timeouts dans la configuration (application.yml ou application.properties)
  • Activez les logs HTTP pour faciliter le debug (ex: niveau DEBUG sur org.springframework.web)
  • Attention: pas de support pour les WebSockets ou le streaming avancé

FAQ et limitations courantes

1. Peut-on utiliser HttpExchange avec WebFlux (réactif) ?
Oui, il suffit de configurer un WebClient au lieu d’un RestClient dans la factory. Les méthodes de l’interface peuvent alors retourner des types réactifs (Mono<T>, Flux<T>).

2. Comment gérer l’authentification ?
Configurez le RestClient ou WebClient avec un filtre ou un interceptor pour ajouter les headers d’authentification (Bearer, Basic, etc.).

3. Peut-on personnaliser la sérialisation JSON ?
Oui, en passant un RestClient/WebClient configuré avec un ObjectMapper personnalisé.

4. Quelles sont les limitations ?

  • Pas de support WebSocket/streaming avancé.
  • Pas de gestion automatique du circuit breaker/retry (utilisez Resilience4j ou Spring Retry).
  • Les interfaces doivent être publiques et annotées correctement.

5. Où trouver la documentation officielle ?


Aller plus loin / Pour experts 🏅

Exemple de configuration avancée

// caption: Configuration avancée
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

// Exemple: token récupéré depuis une propriété ou un provider dédié.

@Bean
UserClient userClient(RestClient.Builder builder) {
  String token = "<token>";

  RestClient restClient = builder
    .baseUrl("https://api.exemple.com")
    .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token)
    .requestInterceptor((request, body, execution) -> {
      // Log ou enrichissement personnalisé
      return execution.execute(request, body);
    })
    .build();
  return HttpServiceProxyFactory
    .builderFor(RestClientAdapter.create(restClient))
    .build()
    .createClient(UserClient.class);
}

Utilisation avec WebFlux (réactif)

// caption: Utilisation du client en Réactive
import java.util.List;

import reactor.core.publisher.Mono;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;

@HttpExchange("/users")
public interface ReactiveUserClient {
  @GetExchange
  Mono<List<User>> getAllUsers();
}

Résilience avec Resilience4j

Intégrez un décorateur autour du client pour ajouter du retry/circuit breaker:

Tableau récapitulatif des annotations principales

Annotation Méthode HTTP Usage principal
@GetExchange GET Lecture de ressources
@PostExchange POST Création de ressources
@PutExchange PUT Mise à jour de ressources
@DeleteExchange DELETE Suppression de ressources
@PatchExchange PATCH Modification partielle

Différences avec Feign, Retrofit, RestTemplate

  • Feign: nécessite une dépendance externe, moins intégré à Spring Boot 3+.
  • Retrofit: orienté Android/Java, non natif Spring.
  • RestTemplate: impératif, verbeux, obsolète à terme.
  • HttpExchange: natif Spring, moderne, typé, supporte RestClient/WebClient.

Migration rapide RestTemplate -> HttpExchange

  1. Identifiez les appels restTemplate.getForObject(...), postForEntity(...), etc.
  2. Créez une interface annotée @HttpExchange avec les méthodes équivalentes.
  3. Remplacez les appels impératifs par l'injection du client généré.
  4. Conservez la logique métier dans le service, pas dans le client.
  5. Ajoutez des tests de contrat (MockWebServer/WireMock) pour valider la migration.

Checklist avant mise en production

  • URL de base externalisée (application.yml) et versionnée.
  • Timeouts configurés (connexion + lecture).
  • Gestion explicite des erreurs 4xx/5xx (ProblemDetail + mapping métier).
  • Logs HTTP activables en debug, désactivés en production par défaut.
  • Stratégie de résilience en place (retry/circuit breaker) selon le contexte.
  • Tests unitaires et d'intégration automatisés dans la CI.

Astuces de debug

  • Activez les logs HTTP: logging.level.org.springframework.web=DEBUG dans application.yml.
  • Utilisez Postman/Insomnia pour tester vos endpoints indépendamment du code.

Ressources complémentaires


Conclusion 🎯

L’interface HTTP de Spring via HttpExchange modernise la consommation d’API REST: code plus lisible, typé, testable et parfaitement intégré à Spring. Pour tout nouveau projet Spring Boot 3+, c’est la solution recommandée.

N’hésitez pas à explorer la documentation officielle et à expérimenter sur vos propres projets pour aller plus loin.

Ressources de base:


Glossaire express (vulgarisé)

  • 🌐 API REST : un service distant que vous appelez via HTTP. Lien : https://www.sfeir.dev/rest-definition/
  • 📦 DTO : un objet Java simple qui transporte les données.
  • 🪞 Proxy : un objet généré qui exécute l'appel HTTP à votre place.
  • 🧱 Boilerplate : du code répétitif, peu utile côté métier.
  • 🚗 RestClient : client HTTP Spring pour un usage synchrone.
  • 🚀 WebClient : client HTTP Spring pour un usage réactif (Mono, Flux).

Dernier