Spring Boot et Spring Cloud

Spring Cloud pour les microservices : Config Server, discovery, gateway et résilience

Avancé9 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

Vous saurez expliquer quels problèmes de systèmes distribués Spring Cloud résout et quels projets s'en chargent aujourd'hui : faire tourner un Config Server et rafraîchir ses clients, enregistrer des services dans Eureka et les appeler par leur nom via un RestClient load-balancé, router le trafic externe à travers Spring Cloud Gateway et protéger les appels avec des circuit breakers Resilience4j, le tout sur Spring Boot 3 avec les release trains 2023 et 2024.

Ce que vous allez apprendre

  • Associer les problèmes récurrents d'une architecture microservices aux projets Spring Cloud qui y répondent
  • Mettre en place Spring Cloud Config Server avec un backend Git et rafraîchir la configuration des clients à chaud
  • Enregistrer des services dans Eureka et les appeler par leur nom logique via un RestClient load-balancé ou une HTTP interface
  • Définir des routes, des predicates et des filters dans Spring Cloud Gateway
  • Encapsuler les appels distants dans des circuit breakers, retries et time limiters Resilience4j, et expliquer quand chacun est utile
  • Repérer les composants Spring Cloud Netflix retirés et leurs remplaçants actuels

Découper une application en une douzaine de services remplace des problèmes que vous aviez résolus (un appel de méthode, un fichier de configuration partagé, un log unique) par des problèmes qui ne le sont pas encore : où se trouve l'autre service, comment obtient-il sa configuration, que se passe-t-il quand il est lent, et comment le trafic venu de l'extérieur atteint-il le bon service ? Spring Cloud est la bannière qui regroupe les projets Spring traitant ces problèmes au-dessus de Spring Boot. Il a beaucoup évolué depuis la première génération bâtie sur Netflix ; cette page décrit donc le paysage actuel pour Spring Boot 3 et montre l'essentiel de chaque brique : configuration, discovery, load balancing, gateway et résilience.

Les release trains et l'offre actuelle

Spring Cloud n'est pas une bibliothèque unique mais un ensemble coordonné de projets publiés ensemble sous forme de release train, dont la version s'importe comme un BOM :

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>2024.0.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Chaque train cible une ligne mineure de Spring Boot ; associer un train à la mauvaise version de Boot provoque des échecs obscurs au démarrage. Les projets que vous croiserez le plus souvent :

ProblèmeProjetRemarques
Configuration centralisée et versionnéeSpring Cloud ConfigBackends Git, Vault, JDBC et fichiers
Registre de servicesSpring Cloud Netflix Eureka, Consul, Zookeeper, KubernetesEureka est le seul composant Netflix restant
Load balancing côté clientSpring Cloud LoadBalancerA remplacé Ribbon
Clients HTTP déclaratifsSpring Cloud OpenFeign (maintenance), HTTP interfaces de Spring FrameworkTous deux s'intègrent avec LoadBalancer
API gatewaySpring Cloud GatewayA remplacé Zuul ; cœur réactif, avec une variante servlet
Circuit breakingSpring Cloud Circuit Breaker avec Resilience4jA remplacé Hystrix
Messaging orienté événementsSpring Cloud StreamBinders pour Kafka, RabbitMQ et d'autres
Diffusion de la configurationSpring Cloud BusPropage les événements de refresh via un broker
Contrats pilotés par le consommateurSpring Cloud ContractStubs générés à partir des contrats

Le tracing distribué a quitté Spring Cloud avec le retrait de Sleuth ; il est désormais assuré par Micrometer Tracing dans Boot lui-même, comme l'explique le guide sur l'observabilité.

Configuration centralisée

Douze services avec douze fichiers application.yml, qui répètent tous le même hôte de base de données et la même adresse de broker, divergent en quelques semaines. Spring Cloud Config Server sert la configuration depuis un backend versionné, le plus souvent un dépôt Git, indexée par nom d'application, profil et label (branche ou tag) :

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication { /* main */ }
server:
  port: 8888
spring:
  cloud:
    config:
      server:
        git:
          uri: https://git.example.com/platform/config.git
          default-label: main
          search-paths: '{application}'     # one folder per service
          clone-on-start: true

Le dépôt contient application.yml (partagé par tous), orders.yml, orders-prod.yml, etc. ; GET /orders/prod/main renvoie les property sources fusionnées du service orders pour le profil prod. Les valeurs chiffrées peuvent être stockées sous la forme {cipher}... et déchiffrées par le serveur avec une clé symétrique ou une paire de clés, ce qui évite de laisser des secrets en clair dans Git sans recourir à un coffre séparé, même si Spring Cloud Vault ou un gestionnaire de secrets cloud reste l'option la plus solide.

Côté client, on ajoute spring-cloud-starter-config et on pointe vers le serveur grâce au mécanisme de config import disponible depuis Boot 2.4 :

spring:
  application:
    name: orders            # becomes {application} in the request
  config:
    import: "optional:configserver:http://config:8888"
  cloud:
    config:
      fail-fast: true       # refuse to start without configuration in production
      retry:
        max-attempts: 6

L'ancienne approche par bootstrap.yml existe toujours derrière spring-cloud-starter-bootstrap, mais elle est héritée ; spring.config.import est le modèle actuel et s'intègre à l'ordre de priorité des propriétés décrit dans le guide sur la configuration.

Modifier une valeur dans Git ne change rien à un service en cours d'exécution tant qu'il ne relit pas sa configuration. Exposez l'endpoint Actuator refresh et envoyez-lui un POST : Boot refait alors le binding des beans @ConfigurationProperties et recrée les beans annotés @RefreshScope :

@RefreshScope
@Service
class PricingService {
    private final BigDecimal surcharge;

    PricingService(@Value("${pricing.surcharge}") BigDecimal surcharge) {
        this.surcharge = surcharge;   // a new instance is created on the next call after a refresh
    }
}

Rafraîchir chaque instance à la main ne passe pas à l'échelle ; Spring Cloud Bus relie les services à Kafka ou RabbitMQ pour qu'un seul POST /actuator/busrefresh sur n'importe quelle instance (ou un webhook de l'hébergeur Git vers l'endpoint /monitor du Config Server) se propage à toutes. @RefreshScope place le bean derrière un proxy, ce qui coûte un peu à chaque appel et se marie mal avec @Scheduled et avec les beans qui détiennent des connexions ; pour la plupart des réglages rafraîchissables, préférez @ConfigurationProperties, dont le binding est refait sur place.

Service discovery et load balancing

Coder en dur http://orders-2.internal:8080 échoue dès la première fois qu'une instance est remplacée. Un registre de services permet à chaque instance d'enregistrer son nom et son adresse au démarrage, d'envoyer des heartbeats et de se désenregistrer à l'arrêt ; les clients demandent au registre les instances actuelles de orders et en choisissent une.

Eureka est le registre que décrivent la plupart des ressources Spring Cloud. Le serveur est une application Boot avec spring-cloud-starter-netflix-eureka-server et @EnableEurekaServer ; chaque service ajoute spring-cloud-starter-netflix-eureka-client et s'enregistre sous son spring.application.name :

eureka:
  client:
    service-url:
      defaultZone: http://eureka-1:8761/eureka/,http://eureka-2:8761/eureka/
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 10

Eureka privilégie la disponibilité à la cohérence : les instances restent listées un moment après leur mort, et un serveur qui perd le contact avec ses pairs continue de servir le dernier registre connu (mode self-preservation). Les clients ont donc besoin de retries et de timeouts dans tous les cas : le registre est un cache, pas une source de vérité.

Côté client, c'est Spring Cloud LoadBalancer qui résout un nom d'hôte logique en instance et applique une politique round-robin (par défaut) ou aléatoire. Les services en Boot 3.2+ devraient utiliser un RestClient load-balancé :

@Configuration
class ClientConfig {

    @Bean
    @LoadBalanced
    RestClient.Builder loadBalancedRestClientBuilder() {
        return RestClient.builder();
    }

    @Bean
    InventoryApi inventoryApi(@LoadBalanced RestClient.Builder builder) {
        RestClient client = builder.baseUrl("http://inventory").build();   // "inventory" is a service name
        return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client))
                .build().createClient(InventoryApi.class);
    }
}

@LoadBalanced marque le builder afin qu'un intercepteur remplace inventory par un vrai hôte et un vrai port à chaque appel. Spring Cloud OpenFeign offre la même chose via des interfaces @FeignClient(name = "inventory"), avec un jeu plus riche d'annotations de contrat et d'encodeurs ; il reste pris en charge, mais le code neuf devrait préférer les HTTP interfaces du framework, qui ne demandent aucun projet supplémentaire et fonctionnent avec le même load balancer.

Sur Kubernetes, tout cela est facultatif. Un Service nommé inventory se résout déjà par DNS vers une IP virtuelle load-balancée : http://inventory fonctionne donc avec un simple RestClient, sans registre. Spring Cloud Kubernetes existe pour les applications qui veulent que l'abstraction DiscoveryClient lise les endpoints Kubernetes, surtout dans les déploiements hybrides.

La gateway

Les clients externes n'ont pas à connaître la topologie de vos services. Une API gateway est le point d'entrée unique qui route les requêtes vers les services, assure la terminaison TLS, applique l'authentification et les limites de débit, et réécrit les chemins. Spring Cloud Gateway repose sur Spring WebFlux et Project Reactor (une variante servlet, Spring Cloud Gateway Server MVC, existe pour les équipes qui veulent une stack bloquante), et les routes se déclarent en configuration ou en Java :

spring:
  cloud:
    gateway:
      routes:
        - id: orders
          uri: lb://orders                        # lb:// resolves through the load balancer and registry
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1                       # /api/orders/42 -> /orders/42 on the service
            - name: CircuitBreaker
              args:
                name: ordersBreaker
                fallbackUri: forward:/fallback/orders
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 50
                redis-rate-limiter.burstCapacity: 100
        - id: catalogue
          uri: http://catalogue:8080
          predicates:
            - Path=/api/catalogue/**
            - Method=GET
          filters:
            - AddResponseHeader=Cache-Control, public, max-age=60

Un predicate décide si une route correspond (chemin, méthode, en-tête, hôte, query, heure de la journée) ; un filter transforme la requête ou la réponse (supprimer ou réécrire des chemins, ajouter des en-têtes, retry, rate limiting, circuit breaking, relais d'un token OAuth 2.0). Les global filters s'appliquent à toutes les routes : c'est là que vivent généralement les correlation ids, le logging des requêtes et l'authentification. Avec le support resource server OAuth 2.0 de Spring Security, la gateway valide les access tokens une seule fois puis les transmet, si bien que les services en aval peuvent faire confiance à l'en-tête Authorization ; c'est le filter TokenRelay qui se charge de la transmission.

Dans le release train 2025.0, les artefacts de la gateway ont été renommés pour rendre explicites les deux stacks (spring-cloud-starter-gateway-server-webflux et spring-cloud-starter-gateway-server-webmvc, avec des préfixes de propriétés sous spring.cloud.gateway.server.*) ; les anciens noms restent disponibles pendant une période de transition. Vérifiez la documentation de référence du train que vous utilisez avant de copier des noms de propriétés.

Résilience : circuit breakers, retries et time limits

Une dépendance lente est plus dangereuse qu'une dépendance morte : les threads s'accumulent en attente, et l'appelant devient lent pour tout le monde. Resilience4j fournit les patterns, et Spring Cloud Circuit Breaker les encapsule derrière une factory indépendante du fournisseur :

@Service
class RecommendationService {

    private final CircuitBreakerFactory<?, ?> breakers;
    private final RecommendationApi api;

    RecommendationService(CircuitBreakerFactory<?, ?> breakers, RecommendationApi api) {
        this.breakers = breakers;
        this.api = api;
    }

    List<Product> forCustomer(long customerId) {
        return breakers.create("recommendations")
                .run(() -> api.recommend(customerId),
                     throwable -> List.of());          // fallback: an empty list, not an error page
    }
}
resilience4j:
  circuitbreaker:
    instances:
      recommendations:
        sliding-window-size: 20
        failure-rate-threshold: 50           # open after 50 % failures in the window
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 5
  timelimiter:
    instances:
      recommendations:
        timeout-duration: 800ms

Un circuit breaker compte les échecs dans une fenêtre glissante ; au-delà du seuil, il s'ouvre et fait échouer les appels immédiatement sans solliciter la dépendance, puis, après un délai, laisse passer quelques appels d'essai (half-open) et se referme s'ils réussissent. Un time limiter borne la durée d'un appel. Un retry répète un appel en échec avec un backoff, ce qui aide face aux erreurs transitoires mais aggrave une surcharge : combinez-le avec le circuit breaker et limitez le nombre de tentatives. Un bulkhead plafonne le nombre d'appels concurrents vers une dépendance, pour qu'une dépendance lente ne puisse pas accaparer tous les threads. Le starter resilience4j-spring-boot3 propose aussi des annotations (@CircuitBreaker(name = "recommendations", fallbackMethod = "empty")) appliquées par AOP, et publie états et métriques vers Actuator et Micrometer.

Les fallbacks doivent avoir du sens : une liste de recommandations vide, un prix en cache, une réponse « réessayez plus tard » avec un en-tête Retry-After. Un fallback qui renvoie en silence des données périmées ou inventées n'est qu'un bug à retardement.

Assembler le tout

Une topologie minimale mais réaliste sur machines virtuelles : deux instances de Config Server adossées à un même dépôt Git ; deux instances Eureka qui s'enregistrent l'une auprès de l'autre ; une gateway qui route /api/** vers les services par leur nom, avec un circuit breaker par route ; chaque service récupère sa configuration au démarrage, s'enregistre dans Eureka, appelle ses pairs via des HTTP interfaces load-balancées protégées par Resilience4j, et remonte health, métriques et traces via Actuator. Sur Kubernetes, supprimez Eureka et éventuellement le Config Server (ConfigMaps et Secrets avec spring.config.import=kubernetes: ou des config trees montés), gardez la gateway ou remplacez-la par un ingress plus une sécurité propre à chaque service, et conservez la couche de résilience, qu'aucune plateforme ne vous fournit.

Erreurs courantes

Mélanger release trains et versions de Boot. Le BOM doit correspondre à la version mineure de Boot ; les symptômes vont d'un NoSuchMethodError au démarrage à des beans qui manquent sans bruit.

Recopier des exemples Hystrix, Ribbon ou Zuul. Ils ne compileront pas avec les trains actuels. Transposez-les vers Resilience4j, Spring Cloud LoadBalancer et Spring Cloud Gateway.

Considérer le registre comme faisant autorité. Les instances qui ont planté restent listées jusqu'à l'expiration de leur lease. Chaque client a besoin de timeouts et de retries avec backoff, indépendamment de la discovery.

Rafraîchir avec @RefreshScope des beans qui détiennent un état ou des ressources. Le proxy recrée le bean et perd au passage caches, pools de connexions et tâches planifiées. Refaites plutôt le binding de @ConfigurationProperties, et réservez @RefreshScope aux beans peu coûteux à reconstruire.

Une seule instance de Config Server ou de registre. Les deux sont sur le chemin critique au démarrage ; faites-en tourner au moins deux, et réglez fail-fast en connaissance de cause selon qu'un service a le droit de démarrer avec des valeurs locales par défaut périmées.

Rejouer des appels non idempotents. Un POST /payments rejoué débite deux fois. Ne faites de retry que sur des opérations idempotentes, ou rendez l'opération idempotente grâce à une clé générée par le client.

Mettre de la logique métier dans les filters de la gateway. La gateway doit router, sécuriser, limiter et observer. La transformation des payloads et l'orchestration appartiennent aux services, où elles peuvent être testées et versionnées avec le domaine.

Points clés à retenir

  • Spring Cloud est un ensemble de projets coordonnés publiés sous forme de trains ; importez le BOM qui correspond à la version mineure de votre Spring Boot et ne les mélangez jamais.
  • Config Server sert une configuration versionnée depuis Git par application, profil et label ; les clients l'importent avec spring.config.import et la rafraîchissent via Actuator ou Spring Cloud Bus.
  • La discovery (Eureka, Consul, Kubernetes) associée à Spring Cloud LoadBalancer permet aux services de s'appeler par leur nom via un RestClient @LoadBalanced ou une HTTP interface ; sur Kubernetes, le DNS rend souvent le registre superflu.
  • Spring Cloud Gateway route le trafic externe à l'aide de predicates et de filters, et c'est là que doivent se trouver l'authentification, le rate limiting et les circuit breakers par route.
  • Les circuit breakers, time limiters, retries et bulkheads de Resilience4j empêchent une seule dépendance lente de faire tomber l'appelant ; les fallbacks doivent renvoyer quelque chose que le métier peut accepter.
  • Hystrix, Ribbon et Zuul ont disparu ; Sleuth est devenu Micrometer Tracing ; OpenFeign est en mode maintenance au profit des HTTP interfaces de Spring.

Sources et lectures complémentaires

Questions fréquentes

Quelle version de Spring Cloud fonctionne avec ma version de Spring Boot ?
Spring Cloud est publié sous forme de release trains, nommés d'après des stations du métro londonien et, depuis 2020, par année. Le train 2023.0.x (Leyton) cible Spring Boot 3.2 et 3.3 ; 2024.0.x (Moorgate) cible Boot 3.4 ; 2025.0.x (Northfields) cible Boot 3.5. Importez le BOM spring-cloud-dependencies du train correspondant et laissez-le gérer la version de chaque spring-cloud-starter ; la table de compatibilité de la page du projet Spring Cloud fait foi.
Ai-je encore besoin d'Eureka sur Kubernetes ?
En général, non. Les Services Kubernetes fournissent déjà une discovery basée sur le DNS et du load balancing, et un Spring Cloud Gateway ou un ingress controller peut router par nom de service. Eureka reste pertinent sur des machines virtuelles, sur des plateformes sans abstraction de service, ou quand vous avez besoin de décisions de load balancing côté client que Kubernetes ne propose pas. Spring Cloud Kubernetes fait le pont entre les deux quand une même base de code doit tourner sur les deux.
Spring Cloud OpenFeign est-il déprécié ?
Il est en mode maintenance : il reçoit des corrections de bugs et reste compatible avec les nouveaux release trains, mais l'équipe Spring recommande pour le code neuf les clients HTTP interface intégrés à Spring Framework 6. Les clients Feign existants continuent de fonctionner ; la migration peut se faire progressivement, interface par interface, car dans les deux cas il s'agit d'interfaces déclaratives adossées à un client load-balancé.
Que sont devenus Hystrix, Ribbon et Zuul ?
Netflix a cessé de les développer et Spring Cloud a supprimé leurs intégrations avec le release train 2020.0 (Ilford). Leurs remplaçants sont Spring Cloud Circuit Breaker avec Resilience4j pour Hystrix, Spring Cloud LoadBalancer pour Ribbon et Spring Cloud Gateway pour Zuul. Eureka est le seul composant Netflix encore présent dans Spring Cloud Netflix.
ArchitectureMicroservices : frontières de service, déploiement indépendant et coût réelCe qu'exige vraiment le style microservices (déploiement autonome, données par service, frontières saines) et comment savoir si vous en avez besoin.Intermédiaire13 min de lectureArchitecturePatterns de conception microservices : résilience, découverte, données et observabilitéUn catalogue pratique des patterns microservices : gateway, découverte, circuit breaker, bulkhead, retry, saga, outbox, CQRS et service mesh.Avancé11 min de lectureArchitectureAPI management et API gateways : politiques, rate limiting, plans et portailsCe qu'une API gateway apporte de plus qu'un load balancer ou un mesh, le fonctionnement du rate limiting et des clés d'API, et le choix d'une plateforme.Intermédiaire13 min de lectureSpring Boot Actuator et observabilité : health, métriques, traces et logsRendre un service Spring Boot 3 observable : endpoints Actuator, probes Kubernetes, métriques Micrometer pour Prometheus, tracing et corrélation des logs.Intermédiaire8 min de lectureConfiguration Spring Boot : properties, YAML, profils et @ConfigurationPropertiesLa configuration externalisée de Spring Boot 3 : priorité des sources, YAML et profils, @ConfigurationProperties typé, config import et secrets.Débutant10 min de lectureDevOpsLes bases de Kubernetes : architecture, Pods, Deployments et ServicesComment est construit un cluster Kubernetes, ce qu'est vraiment un Pod, et comment Deployments, Services, ConfigMaps et labels font tourner une application.Intermédiaire10 min de lecture