# 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. - Published: 2026-03-20 - Updated: 2026-03-31 - Author: SharpSkill - Tags: spring cloud gateway, microservices, api gateway, routage, entretien technique - Reading time: 15 min --- 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. ```java // GatewayApplication.java // 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. ```java // RouteConfiguration.java // 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/(?.*)", "/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 ``` ```java // CustomPredicateFactory.java // Création d'un predicate personnalisé @Component public class ApiKeyRoutePredicateFactory extends AbstractRoutePredicateFactory { public ApiKeyRoutePredicateFactory() { super(Config.class); } @Override public Predicate 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 validKeys = new ArrayList<>(); public List getValidKeys() { return validKeys; } public void setValidKeys(List 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. ```java // LoggingFilter.java // Filtre global de logging @Component @Slf4j public class LoggingFilter implements GlobalFilter, Ordered { @Override public Mono 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; } } ``` ```java // AuthenticationFilter.java // Filtre d'authentification JWT @Component @RequiredArgsConstructor public class AuthenticationFilter implements GatewayFilter { private final JwtTokenValidator tokenValidator; @Override public Mono 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 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/(?.*), /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}" ``` ```java // RateLimiterConfiguration.java // 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() ); } } ``` ### 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. ```java // RequestBodyModificationFilter.java // Filtre de modification du body de requête @Component @RequiredArgsConstructor public class RequestBodyModificationFilter implements GlobalFilter, Ordered { private final ObjectMapper objectMapper; @Override public Mono 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 body = objectMapper.readValue( bytes, new TypeReference>() {} ); // 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 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; } } ``` ```java // ResponseBodyModificationConfig.java // 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/ ``` ```java // LoadBalancerConfiguration.java // 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 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); } } ``` ```java // WeightedLoadBalancer.java // Load balancer avec pondération @Component @RequiredArgsConstructor public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer { private final String serviceId; private final ObjectProvider supplierProvider; private final Random random = new Random(); @Override public Mono> 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 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 ``` ```java // FallbackController.java // Contrôleur de fallback @RestController @RequestMapping("/fallback") @Slf4j public class FallbackController { @GetMapping("/orders") public Mono>> ordersFallback( ServerWebExchange exchange ) { log.warn("Circuit breaker activated for orders service"); Map 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>> ordersPostFallback() { Map 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)); } } ``` ```java // CircuitBreakerEventListener.java // 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 ``` ```java // CustomRetryFilter.java // 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 filter(ServerWebExchange exchange, GatewayFilterChain chain) { return Mono.defer(() -> attemptRequest(exchange, chain, 0)); } private Mono 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. ```java // AggregationController.java // 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 getUserDashboard(@PathVariable Long userId) { // Appels parallèles aux différents services Mono userMono = fetchUserProfile(userId); Mono> ordersMono = fetchRecentOrders(userId); Mono 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 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> 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 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 handleAggregationError(Throwable error) { log.error("Dashboard aggregation failed: {}", error.getMessage()); return Mono.just(DashboardResponse.builder() .error("Partial data available") .generatedAt(Instant.now()) .build()); } } ``` ```java // DashboardResponse.java // DTO de réponse agrégée @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DashboardResponse { private UserProfile user; private List 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 ``` ```java // SecurityConfig.java // 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; } } ``` ```java // TokenRelayFilter.java // Transmission du token aux services downstream @Component public class TokenRelayFilter implements GlobalFilter, Ordered { @Override public Mono 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; } } ``` ### 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 ``` ```java // MetricsFilter.java // Filtre de métriques personnalisées @Component @RequiredArgsConstructor @Slf4j public class MetricsFilter implements GlobalFilter, Ordered { private final MeterRegistry meterRegistry; @Override public Mono 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; } } ``` ```java // TracingFilter.java // Propagation du contexte de tracing @Component @RequiredArgsConstructor public class TracingFilter implements GlobalFilter, Ordered { private final Tracer tracer; @Override public Mono 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/spring-boot/spring-cloud-gateway-interview-routing-filters