Colloquio Spring Cloud Gateway: Routing, Filtri e Load Balancing

Padroneggia Spring Cloud Gateway per i colloqui tecnici: 12 domande su routing, filtri, load balancing e pattern API Gateway con esempi di codice.

Spring Cloud Gateway: domande di colloquio su routing, filtri e load balancing

Spring Cloud Gateway è la soluzione di riferimento per implementare un API Gateway in un'architettura a microservizi Spring. I colloqui tecnici valutano la capacità di configurare il routing, creare filtri personalizzati e gestire il load balancing in modo efficace.

Suggerimento per la preparazione

I recruiter testano la comprensione dei pattern Gateway: autenticazione centralizzata, rate limiting e circuit breaker. Saper spiegare perché Spring Cloud Gateway rispetto alle alternative fa la differenza.

Architettura e fondamenti di Spring Cloud Gateway

Domanda 1: Cos'è Spring Cloud Gateway e perché utilizzarlo?

Spring Cloud Gateway è un API Gateway reattivo costruito su Spring WebFlux e Project Reactor. Funge da unico punto d'ingresso per tutte le richieste verso i microservizi, fornendo capacità di routing, filtraggio e load balancing.

GatewayApplication.javajava
// Configurazione di base di Spring Cloud Gateway
@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        // Avvia il server reattivo Netty (non Tomcat)
        SpringApplication.run(GatewayApplication.class, args);
    }
}
yaml
# application.yml
# Configurazione minima del gateway
spring:
  cloud:
    gateway:
      routes:
        # Rotta verso il servizio utenti
        - id: user-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/users/**
        # Rotta verso il servizio ordini
        - id: order-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/orders/**

I principali vantaggi di Spring Cloud Gateway includono: architettura non bloccante per alte prestazioni, integrazione nativa con l'ecosistema Spring Cloud e supporto per pattern reattivi moderni.

Domanda 2: Spiega i concetti di Route, Predicate e Filter

Tre concetti fondamentali strutturano Spring Cloud Gateway: le Route definiscono le destinazioni, i Predicate determinano quando applicare una rotta e i Filter modificano richieste e risposte.

RouteConfiguration.javajava
// Configurazione delle rotte tramite codice
@Configuration
public class RouteConfiguration {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // Rotta con più predicate
            .route("product-service", r -> r
                // Predicate: percorso URL
                .path("/api/products/**")
                // Predicate: metodo HTTP
                .and()
                .method(HttpMethod.GET, HttpMethod.POST)
                // Predicate: header presente
                .and()
                .header("X-Api-Version", "v2")
                // Filter: riscrittura del path
                .filters(f -> f
                    .rewritePath("/api/products/(?<segment>.*)", "/products/${segment}")
                    // Filter: aggiungi header
                    .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
                )
                // URI di destinazione
                .uri("http://localhost:8083"))
            .build();
    }
}

Il flusso di elaborazione segue questa sequenza:

text
Richiesta in arrivo
┌─────────────────┐
│   Predicates    │ → Valuta condizioni (path, metodo, header...)
└────────┬────────┘
         │ Corrispondenza trovata
┌─────────────────┐
│  Pre-Filters    │ → Modifica la richiesta prima del routing
└────────┬────────┘
┌─────────────────┐
│   HTTP Proxy    │ → Inoltra al servizio destinazione
└────────┬────────┘
┌─────────────────┐
│  Post-Filters   │ → Modifica la risposta prima di restituirla al client
└────────┬────────┘
   Risposta al client

Domanda 3: Quali sono i predicate più utilizzati?

Spring Cloud Gateway fornisce numerosi predicate integrati per diverse condizioni di routing. La combinazione di più predicate consente regole di routing sofisticate.

yaml
# application.yml
# Esempi di predicate comuni
spring:
  cloud:
    gateway:
      routes:
        # Routing per path con cattura di variabile
        - id: user-details
          uri: http://user-service
          predicates:
            - Path=/users/{userId}

        # Routing per metodo HTTP
        - id: user-create
          uri: http://user-service
          predicates:
            - Path=/users
            - Method=POST

        # Routing basato su header
        - id: mobile-api
          uri: http://mobile-service
          predicates:
            - Header=X-Client-Type, mobile

        # Routing per parametri di query
        - id: search-api
          uri: http://search-service
          predicates:
            - Query=q

        # Routing basato su host
        - id: admin-portal
          uri: http://admin-service
          predicates:
            - Host=admin.example.com

        # Routing temporale
        - id: maintenance-mode
          uri: http://maintenance-service
          predicates:
            - Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z
CustomPredicateFactory.javajava
// Creazione di un predicate personalizzato
@Component
public class ApiKeyRoutePredicateFactory
    extends AbstractRoutePredicateFactory<ApiKeyRoutePredicateFactory.Config> {

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

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            // Verifica la presenza e la validità dell'API key
            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;
        }
    }
}
Ordine dei predicate

L'ordine dei predicate non influisce sulla valutazione, ma l'ordine delle rotte conta. Le rotte vengono valutate in sequenza e viene utilizzata la prima corrispondenza.

Filtri e trasformazione delle richieste

Domanda 4: Come funzionano i filtri pre e post-elaborazione?

I filtri GatewayFilter vengono eseguiti in una catena ordinata. I filtri «pre» modificano la richiesta prima del routing, i filtri «post» modificano la risposta dopo averla ricevuta dal servizio destinazione.

LoggingFilter.javajava
// Filtro globale di logging
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {

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

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

        // Aggiunge l'ID di richiesta agli header
        ServerHttpRequest modifiedRequest = exchange.getRequest()
            .mutate()
            .header("X-Request-Id", requestId)
            .build();

        // Continua la catena e gestisce la risposta
        return chain.filter(exchange.mutate().request(modifiedRequest).build())
            .then(Mono.fromRunnable(() -> {
                // POST-FILTER: dopo la risposta
                long duration = System.currentTimeMillis() - startTime;
                HttpStatusCode status = exchange.getResponse().getStatusCode();

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

    @Override
    public int getOrder() {
        // Ordine negativo = eseguito per primo
        return -1;
    }
}
AuthenticationFilter.javajava
// Filtro di autenticazione 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);

        // Verifica la presenza del token
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return handleUnauthorized(exchange, "Missing or invalid Authorization header");
        }

        String token = authHeader.substring(7);

        // Valida il token in modo reattivo
        return tokenValidator.validate(token)
            .flatMap(claims -> {
                // Arricchisce la richiesta con informazioni utente
                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));
    }
}

Domanda 5: Quali sono i filtri integrati più utili?

Spring Cloud Gateway fornisce molti filtri integrati che coprono casi d'uso comuni: riscrittura URL, modifica degli header, retry e circuit breaker.

yaml
# application.yml
# Filtri integrati comuni
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Riscrittura del path
            - RewritePath=/api/orders/(?<segment>.*), /orders/${segment}

            # Aggiunge header di richiesta
            - AddRequestHeader=X-Gateway-Version, 1.0

            # Rimuove header sensibili dalla risposta
            - RemoveResponseHeader=X-Powered-By
            - RemoveResponseHeader=Server

            # Prefisso del path
            - PrefixPath=/v2

            # Rimuove prefisso
            - StripPrefix=1

            # Retry automatico in caso di errore
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
                methods: GET
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 500ms
                  factor: 2

            # Limitazione di frequenza
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@userKeyResolver}"
RateLimiterConfiguration.javajava
// Configurazione del rate limiter per utente
@Configuration
public class RateLimiterConfiguration {

    @Bean
    public KeyResolver userKeyResolver() {
        // Limitazione per utente autenticato
        return exchange -> Mono.just(
            exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id")
        ).defaultIfEmpty("anonymous");
    }

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

Pronto a superare i tuoi colloqui su Spring Boot?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Domanda 6: Come implementare un filtro di modifica del body?

La modifica del corpo della richiesta o della risposta richiede un approccio specifico con ModifyRequestBodyGatewayFilterFactory o ModifyResponseBodyGatewayFilterFactory.

RequestBodyModificationFilter.javajava
// Filtro di modifica del body della richiesta
@Component
@RequiredArgsConstructor
public class RequestBodyModificationFilter implements GlobalFilter, Ordered {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Modifica solo le richieste POST/PUT con 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 {
                    // Effettua il parsing e modifica il JSON
                    Map<String, Object> body = objectMapper.readValue(
                        bytes,
                        new TypeReference<Map<String, Object>>() {}
                    );

                    // Aggiunge metadati
                    body.put("processedAt", Instant.now().toString());
                    body.put("gatewayVersion", "1.0");

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

                    // Crea una nuova richiesta con il body modificato
                    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
// Configurazione per modificare le risposte
@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) -> {
                        // Incapsula la risposta in un formato standard
                        return Mono.just(String.format(
                            "{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}",
                            responseBody,
                            Instant.now()
                        ));
                    }
                ))
                .uri("lb://user-service"))
            .build();
    }
}

Load Balancing e resilienza

Domanda 7: Come configurare il load balancing con Spring Cloud LoadBalancer?

Spring Cloud Gateway si integra con Spring Cloud LoadBalancer per distribuire il traffico tra le istanze di servizio. Lo schema URI lb:// attiva il load balancing automatico.

yaml
# application.yml
# Configurazione del load balancing
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          # lb:// attiva il load balancing
          uri: lb://user-service
          predicates:
            - Path=/api/users/**

    # Configurazione del load balancer
    loadbalancer:
      ribbon:
        enabled: false  # Usa Spring Cloud LoadBalancer (non Ribbon)

      # Configurazione per servizio
      configurations: default

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

# Service discovery (Eureka o altro)
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
LoadBalancerConfiguration.javajava
// Configurazione personalizzata del load balancer
@Configuration
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
}

// CustomLoadBalancerConfig.java
// Strategia personalizzata di load balancing
public class CustomLoadBalancerConfig {

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

        // Round Robin di default
        return new RoundRobinLoadBalancer(
            clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
            serviceId
        );
    }

    @Bean
    public ServiceInstanceListSupplier serviceInstanceListSupplier(
        ConfigurableApplicationContext context
    ) {
        // Aggiunge health check alle istanze
        return ServiceInstanceListSupplier.builder()
            .withDiscoveryClient()
            .withHealthChecks()
            .withCaching()
            .build(context);
    }
}
WeightedLoadBalancer.javajava
// Load balancer ponderato
@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();
                }

                // Calcola i pesi a partire dai metadati
                List<WeightedInstance> weighted = instances.stream()
                    .map(instance -> {
                        int weight = Integer.parseInt(
                            instance.getMetadata().getOrDefault("weight", "1")
                        );
                        return new WeightedInstance(instance, weight);
                    })
                    .toList();

                // Selezione ponderata
                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) {}
}

Domanda 8: Come implementare un circuit breaker nel gateway?

Il circuit breaker protegge dai guasti a cascata. Spring Cloud Gateway si integra con Resilience4j per una gestione avanzata dei guasti.

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

resilience4j:
  circuitbreaker:
    configs:
      default:
        # Numero di richieste valutate
        slidingWindowSize: 10
        # Soglia di errori per aprire il circuito
        failureRateThreshold: 50
        # Durata con circuito aperto prima del nuovo tentativo
        waitDurationInOpenState: 30s
        # Richieste consentite in half-open
        permittedNumberOfCallsInHalfOpenState: 3
        # Transizioni automatiche
        automaticTransitionFromOpenToHalfOpenEnabled: true

    instances:
      orderServiceCB:
        baseConfig: default
        failureRateThreshold: 60

  timelimiter:
    configs:
      default:
        timeoutDuration: 5s

    instances:
      orderServiceCB:
        timeoutDuration: 3s
FallbackController.javajava
// Controller di 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
// Monitoraggio degli eventi del 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());

        // Metrica 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 e circuit breaker

Configura un timeout coerente tra circuit breaker e client HTTP. Un timeout troppo lungo blocca i thread, mentre uno troppo breve provoca falsi positivi.

Domanda 9: Come implementare il retry con backoff esponenziale?

Un retry intelligente con backoff esponenziale evita di sovraccaricare un servizio in difficoltà, massimizzando al contempo le probabilità di successo.

yaml
# application.yml
# Configurazione di retry con backoff
spring:
  cloud:
    gateway:
      routes:
        - id: payment-service
          uri: lb://payment-service
          predicates:
            - Path=/api/payments/**
          filters:
            - name: Retry
              args:
                # Numero di tentativi
                retries: 3
                # Codici HTTP che attivano il retry
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
                # Solo metodi HTTP idempotenti
                methods: GET,PUT
                # Eccezioni che attivano il retry
                exceptions:
                  - java.io.IOException
                  - java.net.ConnectException
                  - org.springframework.cloud.gateway.support.TimeoutException
                # Backoff esponenziale
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 2000ms
                  factor: 2
                  basedOnPreviousValue: true
CustomRetryFilter.javajava
// Retry personalizzato con logica di business
@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) {
        // Riprova solo i metodi idempotenti
        HttpMethod method = exchange.getRequest().getMethod();
        if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE)
            .contains(method)) {
            return false;
        }

        // Verifica il tipo di eccezione
        return throwable instanceof ConnectException ||
               throwable instanceof TimeoutException ||
               throwable instanceof ServiceUnavailableException;
    }

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

        // Aggiunge jitter casuale (±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;
    }
}

Pattern avanzati e buone pratiche

Domanda 10: Come implementare l'aggregazione delle richieste?

L'aggregazione combina più chiamate ai microservizi in un'unica risposta per il client, riducendo la latenza e la complessità del frontend.

AggregationController.javajava
// Aggregazione multi-servizio
@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) {
        // Chiamate parallele a servizi diversi
        Mono<UserProfile> userMono = fetchUserProfile(userId);
        Mono<List<Order>> ordersMono = fetchRecentOrders(userId);
        Mono<NotificationCount> notificationsMono = fetchNotificationCount(userId);

        // Aggregazione dei risultati
        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 della risposta aggregata
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardResponse {
    private UserProfile user;
    private List<Order> recentOrders;
    private NotificationCount notificationCount;
    private Instant generatedAt;
    private String error;
}

Domanda 11: Come proteggere il gateway con OAuth2?

L'integrazione OAuth2 centralizza l'autenticazione a livello di gateway, evitando la duplicazione della logica in ciascun microservizio.

yaml
# application.yml
# Configurazione 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
// Configurazione di sicurezza del gateway
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
            // Disabilita CSRF per un'API stateless
            .csrf(ServerHttpSecurity.CsrfSpec::disable)

            // Configurazione dell'autorizzazione
            .authorizeExchange(exchanges -> exchanges
                // Endpoint pubblici
                .pathMatchers("/actuator/health", "/actuator/info").permitAll()
                .pathMatchers("/api/public/**").permitAll()
                .pathMatchers("/api/auth/**").permitAll()

                // Endpoint specifici per ruolo
                .pathMatchers("/api/admin/**").hasRole("ADMIN")
                .pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")

                // Tutto il resto richiede autenticazione
                .anyExchange().authenticated()
            )

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

            .build();
    }

    @Bean
    public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
            new JwtGrantedAuthoritiesConverter();
        // Estrae i ruoli dal claim "roles"
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

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

        return converter;
    }
}
TokenRelayFilter.javajava
// Inoltro del token ai servizi 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 -> {
                // Inoltra il token ai servizi downstream
                ServerHttpRequest request = exchange.getRequest()
                    .mutate()
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue())
                    // Aggiunge informazioni utente estratte dal 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() {
        // Dopo l'autenticazione, prima del routing
        return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1;
    }
}

Pronto a superare i tuoi colloqui su Spring Boot?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Domanda 12: Quali sono le buone pratiche di monitoraggio e osservabilità?

Il monitoraggio del gateway è cruciale per individuare problemi di prestazioni e disponibilità in un'architettura a microservizi.

yaml
# application.yml
# Configurazione dell'osservabilità
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
// Filtro di metriche personalizzate
@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();

        // Estrae il servizio destinazione dalla rotta
        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 per la latenza
        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);

        // Contatore delle richieste
        meterRegistry.counter(
            "gateway.requests.total",
            "route", routeId,
            "status", statusCode
        ).increment();

        // Log delle richieste lente
        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
// Propagazione del contesto di tracing
@Component
@RequiredArgsConstructor
public class TracingFilter implements GlobalFilter, Ordered {

    private final Tracer tracer;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Crea o recupera lo span di tracing
        Span span = tracer.nextSpan()
            .name("gateway-request")
            .tag("http.method", exchange.getRequest().getMethod().name())
            .tag("http.url", exchange.getRequest().getURI().toString())
            .start();

        // Inietta gli header di 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;
    }
}

Tabella delle metriche essenziali:

text
| Metrica                           | Descrizione                      |
|-----------------------------------|----------------------------------|
| gateway.request.duration          | Latenza per rotta/stato          |
| gateway.requests.total            | Contatore delle richieste        |
| resilience4j.circuitbreaker.state | Stati del circuit breaker        |
| http.server.requests              | Metriche HTTP standard           |
| spring.cloud.gateway.routes       | Rotte attive                     |
Dashboard consigliate

Utilizza Grafana con le dashboard di Spring Cloud Gateway e Resilience4j per visualizzare le metriche. Configura allarmi sulla latenza P99 e sul tasso di errore.

Conclusione

Spring Cloud Gateway è un componente essenziale delle architetture a microservizi moderne. Punti chiave da ricordare per i colloqui:

Architettura e concetti:

  • ✅ Route, Predicate e Filter formano il modello di base
  • ✅ Architettura reattiva con WebFlux e Netty
  • ✅ Integrazione nativa con l'ecosistema Spring Cloud

Funzionalità essenziali:

  • ✅ Routing dinamico basato su molteplici criteri
  • ✅ Filtri pre/post per la trasformazione di richieste e risposte
  • ✅ Load balancing con Spring Cloud LoadBalancer

Resilienza e sicurezza:

  • ✅ Circuit breaker con Resilience4j e fallback
  • ✅ Retry con backoff esponenziale e jitter
  • ✅ Autenticazione OAuth2/JWT centralizzata

Osservabilità:

  • ✅ Metriche Micrometer con tag per rotta
  • ✅ Tracing distribuito con propagazione del contesto
  • ✅ Health check ed endpoint actuator

Padroneggiare Spring Cloud Gateway dimostra una comprensione profonda dei pattern di microservizi e delle problematiche di scalabilità. Queste competenze sono essenziali per progettare API Gateway robusti e ad alte prestazioni.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#spring cloud gateway
#microservices
#api gateway
#routing
#technical interview

Condividi

Articoli correlati