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 è 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.
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.
// 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);
}
}# 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.
// 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:
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 clientDomanda 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.
# 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// 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;
}
}
}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.
// 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;
}
}// 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.
# 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}"// 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.
// 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;
}
}// 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.
# 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/// 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);
}
}// 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.
# 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// 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));
}
}// 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());
}
}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.
# 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// 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.
// 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());
}
}// 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.
# 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// 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;
}
}// 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.
# 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// 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;
}
}// 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:
| 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 |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
Condividi
Articoli correlati

Spring Kafka: architettura event-driven con consumer resilienti
Guida completa a Spring Kafka per architetture event-driven. Configurazione, consumer resilienti, politiche di retry, dead letter queue e pattern di produzione per applicazioni distribuite.

Logging in Spring Boot 2026: log strutturati in produzione con Logback e JSON
Guida completa al logging strutturato in Spring Boot. Configurazione Logback JSON, MDC per il tracing, best practice in produzione e integrazione con ELK Stack.

Colloquio Spring GraphQL: Resolver, DataLoader e Soluzioni al Problema N+1
Preparazione ai colloqui Spring GraphQL con questa guida completa. Resolver, DataLoader, gestione del problema N+1, mutation e migliori pratiche per le domande tecniche.