Spring Cloud Gateway en entretien : routage, filtres et load balancing

Maîtrisez Spring Cloud Gateway pour vos entretiens techniques : 12 questions sur le routage, les filtres, le load balancing et les patterns API Gateway.

Spring Cloud Gateway : questions d'entretien sur le routage, les filtres et le load balancing

Spring Cloud Gateway constitue la solution de référence pour implémenter un API Gateway dans une architecture microservices Spring. Les entretiens techniques évaluent la capacité à configurer le routage, créer des filtres personnalisés et gérer le load balancing de manière efficace.

Conseil de préparation

Les recruteurs testent la compréhension des patterns Gateway : authentification centralisée, rate limiting, et circuit breaker. Savoir expliquer pourquoi choisir Spring Cloud Gateway plutôt qu'une alternative fait la différence.

Architecture et fondamentaux de Spring Cloud Gateway

Question 1 : Qu'est-ce que Spring Cloud Gateway et pourquoi l'utiliser ?

Spring Cloud Gateway est un API Gateway réactif construit sur Spring WebFlux et Project Reactor. Il sert de point d'entrée unique pour toutes les requêtes vers les microservices, offrant routage, filtrage et load balancing.

GatewayApplication.javajava
// Configuration de base Spring Cloud Gateway
@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        // Démarre le serveur réactif Netty (pas Tomcat)
        SpringApplication.run(GatewayApplication.class, args);
    }
}
yaml
# application.yml
# Configuration minimale du gateway
spring:
  cloud:
    gateway:
      routes:
        # Route vers le service utilisateurs
        - id: user-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/users/**
        # Route vers le service commandes
        - id: order-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/orders/**

Les avantages principaux de Spring Cloud Gateway incluent : architecture non-bloquante pour haute performance, intégration native avec l'écosystème Spring Cloud, et support des patterns réactifs modernes.

Question 2 : Expliquez les concepts de Route, Predicate et Filter

Trois concepts fondamentaux structurent Spring Cloud Gateway : les Routes définissent les destinations, les Predicates déterminent quand appliquer une route, et les Filters modifient les requêtes/réponses.

RouteConfiguration.javajava
// Configuration programmatique des routes
@Configuration
public class RouteConfiguration {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // Route avec plusieurs predicates
            .route("product-service", r -> r
                // Predicate : chemin de l'URL
                .path("/api/products/**")
                // Predicate : méthode HTTP
                .and()
                .method(HttpMethod.GET, HttpMethod.POST)
                // Predicate : en-tête présent
                .and()
                .header("X-Api-Version", "v2")
                // Filtre : réécriture du chemin
                .filters(f -> f
                    .rewritePath("/api/products/(?<segment>.*)", "/products/${segment}")
                    // Filtre : ajout d'en-tête
                    .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
                )
                // URI de destination
                .uri("http://localhost:8083"))
            .build();
    }
}

Le flux de traitement suit cette séquence :

text
Requête entrante
┌─────────────────┐
│   Predicates    │ → Évalue les conditions (path, method, header...)
└────────┬────────┘
         │ Match trouvé
┌─────────────────┐
│  Pre-Filters    │ → Modifie la requête avant routage
└────────┬────────┘
┌─────────────────┐
│   Proxy HTTP    │ → Transmet au service cible
└────────┬────────┘
┌─────────────────┐
│  Post-Filters   │ → Modifie la réponse avant retour client
└────────┬────────┘
    Réponse client

Question 3 : Quels sont les predicates les plus utilisés ?

Spring Cloud Gateway fournit de nombreux predicates intégrés pour des conditions de routage variées. La combinaison de plusieurs predicates permet des règles de routage sophistiquées.

yaml
# application.yml
# Exemples de predicates courants
spring:
  cloud:
    gateway:
      routes:
        # Routage par chemin avec capture de variables
        - id: user-details
          uri: http://user-service
          predicates:
            - Path=/users/{userId}

        # Routage par méthode HTTP
        - id: user-create
          uri: http://user-service
          predicates:
            - Path=/users
            - Method=POST

        # Routage par en-tête
        - id: mobile-api
          uri: http://mobile-service
          predicates:
            - Header=X-Client-Type, mobile

        # Routage par paramètre de requête
        - id: search-api
          uri: http://search-service
          predicates:
            - Query=q

        # Routage par hôte
        - id: admin-portal
          uri: http://admin-service
          predicates:
            - Host=admin.example.com

        # Routage par plage temporelle
        - id: maintenance-mode
          uri: http://maintenance-service
          predicates:
            - Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z
CustomPredicateFactory.javajava
// Création d'un predicate personnalisé
@Component
public class ApiKeyRoutePredicateFactory
    extends AbstractRoutePredicateFactory<ApiKeyRoutePredicateFactory.Config> {

    public ApiKeyRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            // Vérifie la présence et validité de la clé API
            String apiKey = exchange.getRequest()
                .getHeaders()
                .getFirst("X-Api-Key");

            return apiKey != null && config.getValidKeys().contains(apiKey);
        };
    }

    @Validated
    public static class Config {
        private List<String> validKeys = new ArrayList<>();

        public List<String> getValidKeys() {
            return validKeys;
        }

        public void setValidKeys(List<String> validKeys) {
            this.validKeys = validKeys;
        }
    }
}
Ordre des predicates

L'ordre des predicates n'affecte pas l'évaluation, mais l'ordre des routes est important. Les routes sont évaluées séquentiellement, et la première correspondante est utilisée.

Filtres et transformation des requêtes

Question 4 : Comment fonctionnent les filtres pré et post-traitement ?

Les filtres GatewayFilter s'exécutent dans une chaîne ordonnée. Les filtres "pre" modifient la requête avant le routage, les filtres "post" modifient la réponse après réception du service cible.

LoggingFilter.javajava
// Filtre global de logging
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // PRE-FILTER : avant le routage
        String requestId = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();

        log.info("Request {} started: {} {}",
            requestId,
            exchange.getRequest().getMethod(),
            exchange.getRequest().getPath());

        // Ajoute l'ID de requête aux headers
        ServerHttpRequest modifiedRequest = exchange.getRequest()
            .mutate()
            .header("X-Request-Id", requestId)
            .build();

        // Continue la chaîne et gère la réponse
        return chain.filter(exchange.mutate().request(modifiedRequest).build())
            .then(Mono.fromRunnable(() -> {
                // POST-FILTER : après la réponse
                long duration = System.currentTimeMillis() - startTime;
                HttpStatusCode status = exchange.getResponse().getStatusCode();

                log.info("Request {} completed: status={}, duration={}ms",
                    requestId, status, duration);
            }));
    }

    @Override
    public int getOrder() {
        // Ordre négatif = exécuté en premier
        return -1;
    }
}
AuthenticationFilter.javajava
// Filtre d'authentification JWT
@Component
@RequiredArgsConstructor
public class AuthenticationFilter implements GatewayFilter {

    private final JwtTokenValidator tokenValidator;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String authHeader = exchange.getRequest()
            .getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION);

        // Vérifie la présence du token
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return handleUnauthorized(exchange, "Missing or invalid Authorization header");
        }

        String token = authHeader.substring(7);

        // Valide le token de manière réactive
        return tokenValidator.validate(token)
            .flatMap(claims -> {
                // Enrichit la requête avec les informations utilisateur
                ServerHttpRequest enrichedRequest = exchange.getRequest()
                    .mutate()
                    .header("X-User-Id", claims.getSubject())
                    .header("X-User-Roles", String.join(",", claims.getRoles()))
                    .build();

                return chain.filter(exchange.mutate().request(enrichedRequest).build());
            })
            .onErrorResume(e -> handleUnauthorized(exchange, e.getMessage()));
    }

    private Mono<Void> handleUnauthorized(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

        String body = String.format("{\"error\": \"%s\"}", message);
        DataBuffer buffer = exchange.getResponse()
            .bufferFactory()
            .wrap(body.getBytes(StandardCharsets.UTF_8));

        return exchange.getResponse().writeWith(Mono.just(buffer));
    }
}

Question 5 : Quels sont les filtres intégrés les plus utiles ?

Spring Cloud Gateway propose de nombreux filtres intégrés couvrant les cas d'usage courants : réécriture d'URL, modification d'en-têtes, retry, et circuit breaker.

yaml
# application.yml
# Filtres intégrés courants
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Réécriture du chemin
            - RewritePath=/api/orders/(?<segment>.*), /orders/${segment}

            # Ajout d'en-têtes de requête
            - AddRequestHeader=X-Gateway-Version, 1.0

            # Suppression d'en-têtes de réponse sensibles
            - RemoveResponseHeader=X-Powered-By
            - RemoveResponseHeader=Server

            # Prefix du chemin
            - PrefixPath=/v2

            # Strip de prefix
            - StripPrefix=1

            # Retry automatique sur erreurs
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
                methods: GET
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 500ms
                  factor: 2

            # Rate limiting
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@userKeyResolver}"
RateLimiterConfiguration.javajava
// Configuration du rate limiter par utilisateur
@Configuration
public class RateLimiterConfiguration {

    @Bean
    public KeyResolver userKeyResolver() {
        // Limite par utilisateur authentifié
        return exchange -> Mono.just(
            exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id")
        ).defaultIfEmpty("anonymous");
    }

    @Bean
    public KeyResolver ipKeyResolver() {
        // Limite par adresse IP
        return exchange -> Mono.just(
            Objects.requireNonNull(exchange.getRequest()
                .getRemoteAddress())
                .getAddress()
                .getHostAddress()
        );
    }
}

Prêt à réussir tes entretiens Spring Boot ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Question 6 : Comment implémenter un filtre de modification du body ?

Modifier le corps de la requête ou de la réponse nécessite une approche spécifique avec ModifyRequestBodyGatewayFilterFactory ou ModifyResponseBodyGatewayFilterFactory.

RequestBodyModificationFilter.javajava
// Filtre de modification du body de requête
@Component
@RequiredArgsConstructor
public class RequestBodyModificationFilter implements GlobalFilter, Ordered {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Ne modifie que les requêtes POST/PUT avec JSON
        if (!isJsonRequest(exchange)) {
            return chain.filter(exchange);
        }

        return DataBufferUtils.join(exchange.getRequest().getBody())
            .flatMap(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);

                try {
                    // Parse et modifie le JSON
                    Map<String, Object> body = objectMapper.readValue(
                        bytes,
                        new TypeReference<Map<String, Object>>() {}
                    );

                    // Ajoute des métadonnées
                    body.put("processedAt", Instant.now().toString());
                    body.put("gatewayVersion", "1.0");

                    byte[] modifiedBytes = objectMapper.writeValueAsBytes(body);

                    // Crée une nouvelle requête avec le body modifié
                    ServerHttpRequest modifiedRequest = new ServerHttpRequestDecorator(
                        exchange.getRequest()
                    ) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return Flux.just(
                                exchange.getResponse()
                                    .bufferFactory()
                                    .wrap(modifiedBytes)
                            );
                        }

                        @Override
                        public HttpHeaders getHeaders() {
                            HttpHeaders headers = new HttpHeaders();
                            headers.putAll(super.getHeaders());
                            headers.setContentLength(modifiedBytes.length);
                            return headers;
                        }
                    };

                    return chain.filter(exchange.mutate().request(modifiedRequest).build());

                } catch (IOException e) {
                    return Mono.error(new RuntimeException("Failed to parse request body", e));
                }
            });
    }

    private boolean isJsonRequest(ServerWebExchange exchange) {
        MediaType contentType = exchange.getRequest().getHeaders().getContentType();
        return contentType != null &&
               contentType.isCompatibleWith(MediaType.APPLICATION_JSON);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
ResponseBodyModificationConfig.javajava
// Configuration pour modifier les réponses
@Configuration
public class ResponseBodyModificationConfig {

    @Bean
    public RouteLocator responseModifyingRoutes(
        RouteLocatorBuilder builder,
        ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilter
    ) {
        return builder.routes()
            .route("modify-response", r -> r
                .path("/api/users/**")
                .filters(f -> f.modifyResponseBody(
                    String.class,
                    String.class,
                    MediaType.APPLICATION_JSON_VALUE,
                    (exchange, responseBody) -> {
                        // Enveloppe la réponse dans un format standard
                        return Mono.just(String.format(
                            "{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}",
                            responseBody,
                            Instant.now()
                        ));
                    }
                ))
                .uri("lb://user-service"))
            .build();
    }
}

Load balancing et résilience

Question 7 : Comment configurer le load balancing avec Spring Cloud LoadBalancer ?

Spring Cloud Gateway s'intègre avec Spring Cloud LoadBalancer pour distribuer le trafic entre instances de service. L'URI lb:// active le load balancing automatique.

yaml
# application.yml
# Configuration du load balancing
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          # lb:// active le load balancing
          uri: lb://user-service
          predicates:
            - Path=/api/users/**

    # Configuration du load balancer
    loadbalancer:
      ribbon:
        enabled: false  # Utilise Spring Cloud LoadBalancer (pas Ribbon)

      # Configuration par service
      configurations: default

      # Health check pour le load balancing
      health-check:
        path:
          user-service: /actuator/health
        interval: 10s

# Découverte de services (Eureka ou autre)
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
LoadBalancerConfiguration.javajava
// Configuration personnalisée du load balancer
@Configuration
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
}

// CustomLoadBalancerConfig.java
// Stratégie de load balancing personnalisée
public class CustomLoadBalancerConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> loadBalancer(
        Environment environment,
        LoadBalancerClientFactory clientFactory
    ) {
        String serviceId = environment.getProperty(
            LoadBalancerClientFactory.PROPERTY_NAME
        );

        // Utilise Round Robin par défaut
        return new RoundRobinLoadBalancer(
            clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
            serviceId
        );
    }

    @Bean
    public ServiceInstanceListSupplier serviceInstanceListSupplier(
        ConfigurableApplicationContext context
    ) {
        // Ajoute un health check aux instances
        return ServiceInstanceListSupplier.builder()
            .withDiscoveryClient()
            .withHealthChecks()
            .withCaching()
            .build(context);
    }
}
WeightedLoadBalancer.javajava
// Load balancer avec pondération
@Component
@RequiredArgsConstructor
public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final String serviceId;
    private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
    private final Random random = new Random();

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        return supplierProvider.getIfAvailable()
            .get()
            .next()
            .map(instances -> {
                if (instances.isEmpty()) {
                    return new EmptyResponse();
                }

                // Calcule les poids basés sur les métadonnées
                List<WeightedInstance> weighted = instances.stream()
                    .map(instance -> {
                        int weight = Integer.parseInt(
                            instance.getMetadata().getOrDefault("weight", "1")
                        );
                        return new WeightedInstance(instance, weight);
                    })
                    .toList();

                // Sélection pondérée
                int totalWeight = weighted.stream()
                    .mapToInt(WeightedInstance::weight)
                    .sum();

                int randomWeight = random.nextInt(totalWeight);
                int currentWeight = 0;

                for (WeightedInstance wi : weighted) {
                    currentWeight += wi.weight();
                    if (randomWeight < currentWeight) {
                        return new DefaultResponse(wi.instance());
                    }
                }

                return new DefaultResponse(weighted.get(0).instance());
            });
    }

    private record WeightedInstance(ServiceInstance instance, int weight) {}
}

Question 8 : Comment implémenter le circuit breaker dans le gateway ?

Le circuit breaker protège contre les cascades de pannes. Spring Cloud Gateway s'intègre avec Resilience4j pour une gestion avancée des pannes.

yaml
# application.yml
# Configuration Resilience4j Circuit Breaker
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Circuit breaker avec fallback
            - name: CircuitBreaker
              args:
                name: orderServiceCB
                fallbackUri: forward:/fallback/orders

resilience4j:
  circuitbreaker:
    configs:
      default:
        # Nombre de requêtes évaluées
        slidingWindowSize: 10
        # Seuil d'échec pour ouvrir le circuit
        failureRateThreshold: 50
        # Durée circuit ouvert avant tentative
        waitDurationInOpenState: 30s
        # Requêtes permises en half-open
        permittedNumberOfCallsInHalfOpenState: 3
        # Transitions automatiques
        automaticTransitionFromOpenToHalfOpenEnabled: true

    instances:
      orderServiceCB:
        baseConfig: default
        failureRateThreshold: 60

  timelimiter:
    configs:
      default:
        timeoutDuration: 5s

    instances:
      orderServiceCB:
        timeoutDuration: 3s
FallbackController.javajava
// Contrôleur de fallback
@RestController
@RequestMapping("/fallback")
@Slf4j
public class FallbackController {

    @GetMapping("/orders")
    public Mono<ResponseEntity<Map<String, Object>>> ordersFallback(
        ServerWebExchange exchange
    ) {
        log.warn("Circuit breaker activated for orders service");

        Map<String, Object> response = Map.of(
            "success", false,
            "error", "Service temporarily unavailable",
            "code", "SERVICE_UNAVAILABLE",
            "retryAfter", 30
        );

        return Mono.just(ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(response));
    }

    @PostMapping("/orders")
    public Mono<ResponseEntity<Map<String, Object>>> ordersPostFallback() {
        Map<String, Object> response = Map.of(
            "success", false,
            "error", "Order creation temporarily unavailable",
            "code", "SERVICE_UNAVAILABLE",
            "message", "Please try again later"
        );

        return Mono.just(ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(response));
    }
}
CircuitBreakerEventListener.javajava
// Monitoring des événements circuit breaker
@Component
@Slf4j
@RequiredArgsConstructor
public class CircuitBreakerEventListener {

    private final MeterRegistry meterRegistry;

    @EventListener
    public void onCircuitBreakerStateTransition(
        CircuitBreakerOnStateTransitionEvent event
    ) {
        CircuitBreaker.StateTransition transition = event.getStateTransition();

        log.info("Circuit breaker {} state changed: {} -> {}",
            event.getCircuitBreakerName(),
            transition.getFromState(),
            transition.getToState());

        // Métrique Micrometer
        meterRegistry.counter(
            "circuit_breaker.state_transition",
            "name", event.getCircuitBreakerName(),
            "from", transition.getFromState().name(),
            "to", transition.getToState().name()
        ).increment();
    }

    @EventListener
    public void onCircuitBreakerFailure(CircuitBreakerOnErrorEvent event) {
        log.error("Circuit breaker {} error: {}",
            event.getCircuitBreakerName(),
            event.getThrowable().getMessage());
    }
}
Timeout et circuit breaker

Configurer un timeout cohérent entre le circuit breaker et le client HTTP. Un timeout trop long bloque les threads, un timeout trop court déclenche des faux positifs.

Question 9 : Comment implémenter le retry avec backoff exponentiel ?

Le retry intelligent avec backoff exponentiel évite de surcharger un service en difficulté tout en maximisant les chances de succès.

yaml
# application.yml
# Configuration du retry avec backoff
spring:
  cloud:
    gateway:
      routes:
        - id: payment-service
          uri: lb://payment-service
          predicates:
            - Path=/api/payments/**
          filters:
            - name: Retry
              args:
                # Nombre de tentatives
                retries: 3
                # Codes HTTP déclenchant le retry
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
                # Méthodes HTTP idempotentes uniquement
                methods: GET,PUT
                # Exceptions déclenchant le retry
                exceptions:
                  - java.io.IOException
                  - java.net.ConnectException
                  - org.springframework.cloud.gateway.support.TimeoutException
                # Backoff exponentiel
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 2000ms
                  factor: 2
                  basedOnPreviousValue: true
CustomRetryFilter.javajava
// Retry personnalisé avec logique métier
@Component
@Slf4j
public class CustomRetryFilter implements GatewayFilter, Ordered {

    private static final int MAX_RETRIES = 3;
    private static final Duration INITIAL_BACKOFF = Duration.ofMillis(100);
    private static final double BACKOFF_MULTIPLIER = 2.0;
    private static final double JITTER_FACTOR = 0.1;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return Mono.defer(() -> attemptRequest(exchange, chain, 0));
    }

    private Mono<Void> attemptRequest(
        ServerWebExchange exchange,
        GatewayFilterChain chain,
        int attempt
    ) {
        return chain.filter(exchange)
            .onErrorResume(throwable -> {
                if (attempt >= MAX_RETRIES || !isRetryable(throwable, exchange)) {
                    return Mono.error(throwable);
                }

                Duration backoff = calculateBackoff(attempt);
                log.warn("Retry attempt {} after {}ms for {} {}",
                    attempt + 1,
                    backoff.toMillis(),
                    exchange.getRequest().getMethod(),
                    exchange.getRequest().getPath());

                return Mono.delay(backoff)
                    .then(Mono.defer(() ->
                        attemptRequest(exchange, chain, attempt + 1)
                    ));
            });
    }

    private boolean isRetryable(Throwable throwable, ServerWebExchange exchange) {
        // Ne retry que les méthodes idempotentes
        HttpMethod method = exchange.getRequest().getMethod();
        if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE)
            .contains(method)) {
            return false;
        }

        // Vérifie le type d'exception
        return throwable instanceof ConnectException ||
               throwable instanceof TimeoutException ||
               throwable instanceof ServiceUnavailableException;
    }

    private Duration calculateBackoff(int attempt) {
        // Backoff exponentiel avec jitter
        long baseBackoff = (long) (
            INITIAL_BACKOFF.toMillis() * Math.pow(BACKOFF_MULTIPLIER, attempt)
        );

        // Ajoute un jitter aléatoire (±10%)
        double jitter = 1.0 + (Math.random() - 0.5) * 2 * JITTER_FACTOR;
        long finalBackoff = (long) (baseBackoff * jitter);

        return Duration.ofMillis(finalBackoff);
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 1;
    }
}

Patterns avancés et bonnes pratiques

Question 10 : Comment implémenter l'agrégation de requêtes ?

L'agrégation combine plusieurs appels microservices en une seule réponse pour le client, réduisant la latence et la complexité côté frontend.

AggregationController.javajava
// Agrégation de plusieurs services
@RestController
@RequestMapping("/api/aggregate")
@RequiredArgsConstructor
@Slf4j
public class AggregationController {

    private final WebClient.Builder webClientBuilder;
    private final CircuitBreakerRegistry circuitBreakerRegistry;

    @GetMapping("/user-dashboard/{userId}")
    public Mono<DashboardResponse> getUserDashboard(@PathVariable Long userId) {
        // Appels parallèles aux différents services
        Mono<UserProfile> userMono = fetchUserProfile(userId);
        Mono<List<Order>> ordersMono = fetchRecentOrders(userId);
        Mono<NotificationCount> notificationsMono = fetchNotificationCount(userId);

        // Agrégation des résultats
        return Mono.zip(userMono, ordersMono, notificationsMono)
            .map(tuple -> DashboardResponse.builder()
                .user(tuple.getT1())
                .recentOrders(tuple.getT2())
                .notificationCount(tuple.getT3())
                .generatedAt(Instant.now())
                .build())
            .timeout(Duration.ofSeconds(5))
            .onErrorResume(this::handleAggregationError);
    }

    private Mono<UserProfile> fetchUserProfile(Long userId) {
        return webClientBuilder.build()
            .get()
            .uri("lb://user-service/users/{id}", userId)
            .retrieve()
            .bodyToMono(UserProfile.class)
            .transform(CircuitBreakerOperator.of(
                circuitBreakerRegistry.circuitBreaker("user-service")
            ))
            .onErrorReturn(new UserProfile(userId, "Unknown", null));
    }

    private Mono<List<Order>> fetchRecentOrders(Long userId) {
        return webClientBuilder.build()
            .get()
            .uri("lb://order-service/orders?userId={id}&limit=5", userId)
            .retrieve()
            .bodyToFlux(Order.class)
            .collectList()
            .transform(CircuitBreakerOperator.of(
                circuitBreakerRegistry.circuitBreaker("order-service")
            ))
            .onErrorReturn(Collections.emptyList());
    }

    private Mono<NotificationCount> fetchNotificationCount(Long userId) {
        return webClientBuilder.build()
            .get()
            .uri("lb://notification-service/notifications/count/{id}", userId)
            .retrieve()
            .bodyToMono(NotificationCount.class)
            .transform(CircuitBreakerOperator.of(
                circuitBreakerRegistry.circuitBreaker("notification-service")
            ))
            .onErrorReturn(new NotificationCount(0, 0));
    }

    private Mono<DashboardResponse> handleAggregationError(Throwable error) {
        log.error("Dashboard aggregation failed: {}", error.getMessage());
        return Mono.just(DashboardResponse.builder()
            .error("Partial data available")
            .generatedAt(Instant.now())
            .build());
    }
}
DashboardResponse.javajava
// DTO de réponse agrégée
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardResponse {
    private UserProfile user;
    private List<Order> recentOrders;
    private NotificationCount notificationCount;
    private Instant generatedAt;
    private String error;
}

Question 11 : Comment sécuriser le gateway avec OAuth2 ?

L'intégration OAuth2 centralise l'authentification au niveau du gateway, évitant la duplication de logique dans chaque microservice.

yaml
# application.yml
# Configuration OAuth2 Resource Server
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/myrealm
          jwk-set-uri: https://auth.example.com/realms/myrealm/protocol/openid-connect/certs
SecurityConfig.javajava
// Configuration de sécurité du gateway
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
            // Désactive CSRF pour API stateless
            .csrf(ServerHttpSecurity.CsrfSpec::disable)

            // Configuration des autorisations
            .authorizeExchange(exchanges -> exchanges
                // Endpoints publics
                .pathMatchers("/actuator/health", "/actuator/info").permitAll()
                .pathMatchers("/api/public/**").permitAll()
                .pathMatchers("/api/auth/**").permitAll()

                // Endpoints avec rôles spécifiques
                .pathMatchers("/api/admin/**").hasRole("ADMIN")
                .pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")

                // Tout le reste requiert authentification
                .anyExchange().authenticated()
            )

            // OAuth2 Resource Server avec JWT
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )

            .build();
    }

    @Bean
    public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
            new JwtGrantedAuthoritiesConverter();
        // Extraction des rôles depuis le claim "roles"
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

        ReactiveJwtAuthenticationConverter converter =
            new ReactiveJwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(
            new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter)
        );

        return converter;
    }
}
TokenRelayFilter.javajava
// Transmission du token aux services downstream
@Component
public class TokenRelayFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return ReactiveSecurityContextHolder.getContext()
            .map(SecurityContext::getAuthentication)
            .filter(auth -> auth instanceof JwtAuthenticationToken)
            .cast(JwtAuthenticationToken.class)
            .map(JwtAuthenticationToken::getToken)
            .map(jwt -> {
                // Transmet le token aux services downstream
                ServerHttpRequest request = exchange.getRequest()
                    .mutate()
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue())
                    // Ajoute des informations utilisateur extraites du token
                    .header("X-User-Id", jwt.getSubject())
                    .header("X-User-Email", jwt.getClaimAsString("email"))
                    .build();

                return exchange.mutate().request(request).build();
            })
            .defaultIfEmpty(exchange)
            .flatMap(chain::filter);
    }

    @Override
    public int getOrder() {
        // Après l'authentification, avant le routage
        return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1;
    }
}

Prêt à réussir tes entretiens Spring Boot ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Question 12 : Quelles sont les bonnes pratiques de monitoring et observabilité ?

Le monitoring du gateway est crucial pour identifier les problèmes de performance et de disponibilité dans une architecture microservices.

yaml
# application.yml
# Configuration de l'observabilité
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,gateway
  metrics:
    tags:
      application: api-gateway
    distribution:
      percentiles-histogram:
        http.server.requests: true
      percentiles:
        http.server.requests: 0.5, 0.95, 0.99
  tracing:
    sampling:
      probability: 1.0

spring:
  cloud:
    gateway:
      metrics:
        enabled: true
        tags:
          path:
            enabled: true
MetricsFilter.javajava
// Filtre de métriques personnalisées
@Component
@RequiredArgsConstructor
@Slf4j
public class MetricsFilter implements GlobalFilter, Ordered {

    private final MeterRegistry meterRegistry;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        long startTime = System.nanoTime();
        String path = exchange.getRequest().getPath().value();
        String method = exchange.getRequest().getMethod().name();

        // Extraction du service cible depuis la route
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        String routeId = route != null ? route.getId() : "unknown";

        return chain.filter(exchange)
            .doOnSuccess(v -> recordMetrics(exchange, startTime, routeId, "success"))
            .doOnError(e -> recordMetrics(exchange, startTime, routeId, "error"));
    }

    private void recordMetrics(
        ServerWebExchange exchange,
        long startTime,
        String routeId,
        String outcome
    ) {
        long duration = System.nanoTime() - startTime;
        HttpStatusCode status = exchange.getResponse().getStatusCode();
        String statusCode = status != null ? String.valueOf(status.value()) : "0";

        // Timer pour la latence
        Timer.builder("gateway.request.duration")
            .tag("route", routeId)
            .tag("method", exchange.getRequest().getMethod().name())
            .tag("status", statusCode)
            .tag("outcome", outcome)
            .register(meterRegistry)
            .record(duration, TimeUnit.NANOSECONDS);

        // Compteur de requêtes
        meterRegistry.counter(
            "gateway.requests.total",
            "route", routeId,
            "status", statusCode
        ).increment();

        // Log des requêtes lentes
        if (duration > TimeUnit.SECONDS.toNanos(1)) {
            log.warn("Slow request: {} {} - {}ms (route: {})",
                exchange.getRequest().getMethod(),
                exchange.getRequest().getPath(),
                TimeUnit.NANOSECONDS.toMillis(duration),
                routeId);
        }
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
TracingFilter.javajava
// Propagation du contexte de tracing
@Component
@RequiredArgsConstructor
public class TracingFilter implements GlobalFilter, Ordered {

    private final Tracer tracer;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Crée ou récupère le span de tracing
        Span span = tracer.nextSpan()
            .name("gateway-request")
            .tag("http.method", exchange.getRequest().getMethod().name())
            .tag("http.url", exchange.getRequest().getURI().toString())
            .start();

        // Injecte les headers de tracing
        ServerHttpRequest request = exchange.getRequest()
            .mutate()
            .header("X-Trace-Id", span.context().traceId())
            .header("X-Span-Id", span.context().spanId())
            .build();

        return chain.filter(exchange.mutate().request(request).build())
            .doOnSuccess(v -> {
                HttpStatusCode status = exchange.getResponse().getStatusCode();
                span.tag("http.status_code",
                    status != null ? String.valueOf(status.value()) : "0");
                span.end();
            })
            .doOnError(e -> {
                span.tag("error", e.getMessage());
                span.end();
            });
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}

Tableau des métriques essentielles :

text
| Métrique                          | Description                    |
|-----------------------------------|--------------------------------|
| gateway.request.duration          | Latence par route/status       |
| gateway.requests.total            | Compteur de requêtes           |
| resilience4j.circuitbreaker.state | État des circuit breakers      |
| http.server.requests              | Métriques HTTP standard        |
| spring.cloud.gateway.routes       | Routes actives                 |
Dashboards recommandés

Utiliser Grafana avec les dashboards Spring Cloud Gateway et Resilience4j pour visualiser les métriques. Configurer des alertes sur la latence P99 et le taux d'erreur.

Conclusion

Spring Cloud Gateway est un composant essentiel des architectures microservices modernes. Les points clés à retenir pour les entretiens :

Architecture et concepts :

  • ✅ Routes, Predicates et Filters forment le modèle de base
  • ✅ Architecture réactive avec WebFlux et Netty
  • ✅ Intégration native avec Spring Cloud ecosystem

Fonctionnalités essentielles :

  • ✅ Routage dynamique basé sur multiples critères
  • ✅ Filtres pre/post pour transformation des requêtes/réponses
  • ✅ Load balancing avec Spring Cloud LoadBalancer

Résilience et sécurité :

  • ✅ Circuit breaker avec Resilience4j et fallback
  • ✅ Retry avec backoff exponentiel et jitter
  • ✅ Authentification OAuth2/JWT centralisée

Observabilité :

  • ✅ Métriques Micrometer avec tags par route
  • ✅ Tracing distribué avec propagation de contexte
  • ✅ Health checks et endpoints actuator

La maîtrise de Spring Cloud Gateway démontre une compréhension approfondie des patterns microservices et des enjeux de scalabilité. Ces compétences sont essentielles pour concevoir des API Gateway robustes et performantes.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#spring cloud gateway
#microservices
#api gateway
#routage
#entretien technique

Partager

Articles similaires