# Spring Cloud Gateway-sollicitatiegesprek: Routing, Filters en Load Balancing > Beheers Spring Cloud Gateway voor technische sollicitatiegesprekken: 12 vragen over routing, filters, load balancing en API Gateway-patronen met codevoorbeelden. - Published: 2026-03-20 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring cloud gateway, microservices, api gateway, routing, technical interview - Reading time: 15 min --- Spring Cloud Gateway is de referentieoplossing voor het implementeren van een API Gateway in een Spring-microservicesarchitectuur. Technische sollicitatiegesprekken beoordelen het vermogen om routing te configureren, eigen filters te maken en load balancing effectief te beheren. > **Tip ter voorbereiding** > > Recruiters toetsen het begrip van Gateway-patronen: gecentraliseerde authenticatie, rate limiting en circuit breaker. Kunnen uitleggen waarom Spring Cloud Gateway boven alternatieven maakt het verschil. ## Architectuur en grondbeginselen van Spring Cloud Gateway ### Vraag 1: Wat is Spring Cloud Gateway en waarom zou je het gebruiken? Spring Cloud Gateway is een reactieve API Gateway gebouwd op Spring WebFlux en Project Reactor. Het fungeert als enkel toegangspunt voor alle verzoeken naar de microservices en biedt mogelijkheden voor routing, filteren en load balancing. ```java // GatewayApplication.java // Basisconfiguratie van Spring Cloud Gateway @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { // Start de reactieve Netty-server (geen Tomcat) SpringApplication.run(GatewayApplication.class, args); } } ``` ```yaml # application.yml # Minimale gateway-configuratie spring: cloud: gateway: routes: # Route naar de gebruikersservice - id: user-service uri: http://localhost:8081 predicates: - Path=/api/users/** # Route naar de bestelservice - id: order-service uri: http://localhost:8082 predicates: - Path=/api/orders/** ``` De belangrijkste voordelen van Spring Cloud Gateway zijn: niet-blokkerende architectuur voor hoge prestaties, native integratie met het Spring Cloud-ecosysteem en ondersteuning voor moderne reactieve patronen. ### Vraag 2: Leg de concepten Route, Predicate en Filter uit Drie fundamentele concepten structureren Spring Cloud Gateway: Routes definiëren bestemmingen, Predicates bepalen wanneer een route toegepast wordt en Filters wijzigen verzoeken en antwoorden. ```java // RouteConfiguration.java // Programmatorische route-configuratie @Configuration public class RouteConfiguration { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() // Route met meerdere predicates .route("product-service", r -> r // Predicate: URL-pad .path("/api/products/**") // Predicate: HTTP-methode .and() .method(HttpMethod.GET, HttpMethod.POST) // Predicate: header aanwezig .and() .header("X-Api-Version", "v2") // Filter: pad-rewrite .filters(f -> f .rewritePath("/api/products/(?.*)", "/products/${segment}") // Filter: header toevoegen .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway") ) // Doel-URI .uri("http://localhost:8083")) .build(); } } ``` De verwerkingsstroom volgt deze volgorde: ```text Inkomend verzoek │ ▼ ┌─────────────────┐ │ Predicates │ → Beoordeelt voorwaarden (pad, methode, header...) └────────┬────────┘ │ Match gevonden ▼ ┌─────────────────┐ │ Pre-Filters │ → Wijzigt het verzoek vóór de routing └────────┬────────┘ │ ▼ ┌─────────────────┐ │ HTTP Proxy │ → Stuurt door naar de doelservice └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Post-Filters │ → Wijzigt het antwoord vóór retournering aan de client └────────┬────────┘ │ ▼ Antwoord aan de client ``` ### Vraag 3: Wat zijn de meest gebruikte predicates? Spring Cloud Gateway biedt veel ingebouwde predicates voor uiteenlopende routing-voorwaarden. Het combineren van meerdere predicates maakt geavanceerde routing-regels mogelijk. ```yaml # application.yml # Voorbeelden van gangbare predicates spring: cloud: gateway: routes: # Routing op pad met variabele-vastlegging - id: user-details uri: http://user-service predicates: - Path=/users/{userId} # Routing op HTTP-methode - id: user-create uri: http://user-service predicates: - Path=/users - Method=POST # Header-gebaseerde routing - id: mobile-api uri: http://mobile-service predicates: - Header=X-Client-Type, mobile # Routing op queryparameters - id: search-api uri: http://search-service predicates: - Query=q # Host-gebaseerde routing - id: admin-portal uri: http://admin-service predicates: - Host=admin.example.com # Tijdsgebaseerde routing - id: maintenance-mode uri: http://maintenance-service predicates: - Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z ``` ```java // CustomPredicateFactory.java // Aanmaken van een eigen predicate @Component public class ApiKeyRoutePredicateFactory extends AbstractRoutePredicateFactory { public ApiKeyRoutePredicateFactory() { super(Config.class); } @Override public Predicate apply(Config config) { return exchange -> { // Controleert aanwezigheid en geldigheid van de 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 validKeys = new ArrayList<>(); public List getValidKeys() { return validKeys; } public void setValidKeys(List validKeys) { this.validKeys = validKeys; } } } ``` > **Volgorde van predicates** > > De volgorde van predicates beïnvloedt de evaluatie niet, maar de volgorde van routes wel. Routes worden sequentieel beoordeeld en de eerste match wordt gebruikt. ## Filters en verzoektransformatie ### Vraag 4: Hoe werken pre- en post-processing-filters? GatewayFilters worden uitgevoerd in een geordende keten. "Pre"-filters wijzigen het verzoek vóór de routing, "post"-filters wijzigen het antwoord na ontvangst van de doelservice. ```java // LoggingFilter.java // Globaal logging-filter @Component @Slf4j public class LoggingFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // PRE-FILTER: vóór de routing String requestId = UUID.randomUUID().toString(); long startTime = System.currentTimeMillis(); log.info("Request {} started: {} {}", requestId, exchange.getRequest().getMethod(), exchange.getRequest().getPath()); // Voegt de request-ID toe aan de headers ServerHttpRequest modifiedRequest = exchange.getRequest() .mutate() .header("X-Request-Id", requestId) .build(); // Vervolgt de keten en behandelt het antwoord return chain.filter(exchange.mutate().request(modifiedRequest).build()) .then(Mono.fromRunnable(() -> { // POST-FILTER: na het antwoord long duration = System.currentTimeMillis() - startTime; HttpStatusCode status = exchange.getResponse().getStatusCode(); log.info("Request {} completed: status={}, duration={}ms", requestId, status, duration); })); } @Override public int getOrder() { // Negatieve volgorde = eerst uitgevoerd return -1; } } ``` ```java // AuthenticationFilter.java // JWT-authenticatiefilter @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); // Controleert aanwezigheid van het token if (authHeader == null || !authHeader.startsWith("Bearer ")) { return handleUnauthorized(exchange, "Missing or invalid Authorization header"); } String token = authHeader.substring(7); // Valideert het token reactief return tokenValidator.validate(token) .flatMap(claims -> { // Verrijkt het verzoek met gebruikersinformatie 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)); } } ``` ### Vraag 5: Wat zijn de meest nuttige ingebouwde filters? Spring Cloud Gateway biedt veel ingebouwde filters voor gangbare gebruiksscenario's: URL-rewrite, header-aanpassing, retry en circuit breaker. ```yaml # application.yml # Gangbare ingebouwde filters spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: # Pad-rewrite - RewritePath=/api/orders/(?.*), /orders/${segment} # Voeg request-headers toe - AddRequestHeader=X-Gateway-Version, 1.0 # Verwijder gevoelige response-headers - RemoveResponseHeader=X-Powered-By - RemoveResponseHeader=Server # Pad-prefix - PrefixPath=/v2 # Verwijder prefix - StripPrefix=1 # Automatische retry bij fouten - 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 // Configuratie van rate limiter per gebruiker @Configuration public class RateLimiterConfiguration { @Bean public KeyResolver userKeyResolver() { // Limiet per geauthenticeerde gebruiker return exchange -> Mono.just( exchange.getRequest() .getHeaders() .getFirst("X-User-Id") ).defaultIfEmpty("anonymous"); } @Bean public KeyResolver ipKeyResolver() { // Limiet per IP-adres return exchange -> Mono.just( Objects.requireNonNull(exchange.getRequest() .getRemoteAddress()) .getAddress() .getHostAddress() ); } } ``` ### Vraag 6: Hoe implementeer je een filter voor body-aanpassing? Het aanpassen van de body van het verzoek of antwoord vereist een specifieke aanpak met ModifyRequestBodyGatewayFilterFactory of ModifyResponseBodyGatewayFilterFactory. ```java // RequestBodyModificationFilter.java // Filter voor aanpassing van de request-body @Component @RequiredArgsConstructor public class RequestBodyModificationFilter implements GlobalFilter, Ordered { private final ObjectMapper objectMapper; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // Past alleen POST/PUT-verzoeken met JSON aan 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 { // Parseert en wijzigt de JSON Map body = objectMapper.readValue( bytes, new TypeReference>() {} ); // Voegt metadata toe body.put("processedAt", Instant.now().toString()); body.put("gatewayVersion", "1.0"); byte[] modifiedBytes = objectMapper.writeValueAsBytes(body); // Maakt een nieuw verzoek met de aangepaste body 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 // Configuratie om antwoorden aan te passen @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) -> { // Verpakt het antwoord in een standaardformaat return Mono.just(String.format( "{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}", responseBody, Instant.now() )); } )) .uri("lb://user-service")) .build(); } } ``` ## Load Balancing en veerkracht ### Vraag 7: Hoe configureer je load balancing met Spring Cloud LoadBalancer? Spring Cloud Gateway integreert met Spring Cloud LoadBalancer om verkeer te verdelen over service-instanties. Het URI-schema `lb://` activeert automatische load balancing. ```yaml # application.yml # Load balancing-configuratie spring: cloud: gateway: routes: - id: user-service # lb:// activeert load balancing uri: lb://user-service predicates: - Path=/api/users/** # Configuratie van de load balancer loadbalancer: ribbon: enabled: false # Gebruik Spring Cloud LoadBalancer (geen Ribbon) # Configuratie per service configurations: default # Health check voor load balancing health-check: path: user-service: /actuator/health interval: 10s # Service discovery (Eureka of anders) eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ ``` ```java // LoadBalancerConfiguration.java // Aangepaste configuratie van de load balancer @Configuration @LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class) public class LoadBalancerConfiguration { } // CustomLoadBalancerConfig.java // Aangepaste load balancing-strategie public class CustomLoadBalancerConfig { @Bean public ReactorLoadBalancer loadBalancer( Environment environment, LoadBalancerClientFactory clientFactory ) { String serviceId = environment.getProperty( LoadBalancerClientFactory.PROPERTY_NAME ); // Standaard Round Robin return new RoundRobinLoadBalancer( clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId ); } @Bean public ServiceInstanceListSupplier serviceInstanceListSupplier( ConfigurableApplicationContext context ) { // Voegt health check toe aan de instanties return ServiceInstanceListSupplier.builder() .withDiscoveryClient() .withHealthChecks() .withCaching() .build(context); } } ``` ```java // WeightedLoadBalancer.java // Gewogen load balancer @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(); } // Berekent gewichten op basis van metadata List weighted = instances.stream() .map(instance -> { int weight = Integer.parseInt( instance.getMetadata().getOrDefault("weight", "1") ); return new WeightedInstance(instance, weight); }) .toList(); // Gewogen selectie 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) {} } ``` ### Vraag 8: Hoe implementeer je een circuit breaker in de gateway? De circuit breaker beschermt tegen cascadestoringen. Spring Cloud Gateway integreert met Resilience4j voor geavanceerd faalbeheer. ```yaml # application.yml # Resilience4j Circuit Breaker-configuratie spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: # Circuit breaker met fallback - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/orders resilience4j: circuitbreaker: configs: default: # Aantal beoordeelde verzoeken slidingWindowSize: 10 # Foutdrempel om het circuit te openen failureRateThreshold: 50 # Duur dat circuit open blijft voor nieuwe poging waitDurationInOpenState: 30s # Toegestane verzoeken in half-open permittedNumberOfCallsInHalfOpenState: 3 # Automatische overgangen automaticTransitionFromOpenToHalfOpenEnabled: true instances: orderServiceCB: baseConfig: default failureRateThreshold: 60 timelimiter: configs: default: timeoutDuration: 5s instances: orderServiceCB: timeoutDuration: 3s ``` ```java // FallbackController.java // Fallback-controller @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 van circuit breaker-gebeurtenissen @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()); // Micrometer-metriek 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 en circuit breaker** > > Configureer een consistente timeout tussen circuit breaker en HTTP-client. Een te lange timeout blokkeert threads, een te korte veroorzaakt valse positieven. ### Vraag 9: Hoe implementeer je retry met exponentiële backoff? Slimme retry met exponentiële backoff voorkomt het overbelasten van een worstelende service en maximaliseert tegelijk de kans op succes. ```yaml # application.yml # Retry-configuratie met backoff spring: cloud: gateway: routes: - id: payment-service uri: lb://payment-service predicates: - Path=/api/payments/** filters: - name: Retry args: # Aantal pogingen retries: 3 # HTTP-codes die retry triggeren statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT # Alleen idempotente HTTP-methodes methods: GET,PUT # Excepties die retry triggeren exceptions: - java.io.IOException - java.net.ConnectException - org.springframework.cloud.gateway.support.TimeoutException # Exponentiële backoff backoff: firstBackoff: 100ms maxBackoff: 2000ms factor: 2 basedOnPreviousValue: true ``` ```java // CustomRetryFilter.java // Aangepaste retry met bedrijfslogica @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) { // Probeert alleen idempotente methodes opnieuw HttpMethod method = exchange.getRequest().getMethod(); if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE) .contains(method)) { return false; } // Controleert het exceptietype return throwable instanceof ConnectException || throwable instanceof TimeoutException || throwable instanceof ServiceUnavailableException; } private Duration calculateBackoff(int attempt) { // Exponentiële backoff met jitter long baseBackoff = (long) ( INITIAL_BACKOFF.toMillis() * Math.pow(BACKOFF_MULTIPLIER, attempt) ); // Voegt willekeurige jitter toe (±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; } } ``` ## Geavanceerde patronen en best practices ### Vraag 10: Hoe implementeer je verzoek-aggregatie? Aggregatie combineert meerdere microservice-aanroepen in één antwoord voor de client en vermindert latentie en frontend-complexiteit. ```java // AggregationController.java // Multi-service-aggregatie @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) { // Parallelle aanroepen naar verschillende services Mono userMono = fetchUserProfile(userId); Mono> ordersMono = fetchRecentOrders(userId); Mono notificationsMono = fetchNotificationCount(userId); // Aggregatie van resultaten 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 van het geaggregeerde antwoord @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DashboardResponse { private UserProfile user; private List recentOrders; private NotificationCount notificationCount; private Instant generatedAt; private String error; } ``` ### Vraag 11: Hoe beveilig je de gateway met OAuth2? OAuth2-integratie centraliseert authenticatie op gateway-niveau en voorkomt duplicatie van logica in elke microservice. ```yaml # application.yml # OAuth2 Resource Server-configuratie 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 // Beveiligingsconfiguratie van de gateway @Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http // CSRF uitschakelen voor stateless API .csrf(ServerHttpSecurity.CsrfSpec::disable) // Configuratie van autorisatie .authorizeExchange(exchanges -> exchanges // Publieke endpoints .pathMatchers("/actuator/health", "/actuator/info").permitAll() .pathMatchers("/api/public/**").permitAll() .pathMatchers("/api/auth/**").permitAll() // Rolspecifieke endpoints .pathMatchers("/api/admin/**").hasRole("ADMIN") .pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN") // Al het overige vereist authenticatie .anyExchange().authenticated() ) // OAuth2 Resource Server met JWT .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) ) .build(); } @Bean public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // Haalt rollen uit de "roles"-claim grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter( new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter) ); return converter; } } ``` ```java // TokenRelayFilter.java // Doorgifte van het token aan downstream-services @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 -> { // Geeft het token door aan downstream-services ServerHttpRequest request = exchange.getRequest() .mutate() .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue()) // Voegt uit het token gehaalde gebruikersinformatie toe .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() { // Na authenticatie, vóór routing return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1; } } ``` ### Vraag 12: Wat zijn de best practices voor monitoring en observability? Monitoring van de gateway is cruciaal om prestatie- en beschikbaarheidsproblemen in een microservicesarchitectuur op te sporen. ```yaml # application.yml # Observability-configuratie 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 // Filter voor aangepaste metrieken @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(); // Haalt de doelservice uit de 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 voor latentie 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); // Verzoekteller meterRegistry.counter( "gateway.requests.total", "route", routeId, "status", statusCode ).increment(); // Logs van trage verzoeken 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 // Doorgifte van tracing-context @Component @RequiredArgsConstructor public class TracingFilter implements GlobalFilter, Ordered { private final Tracer tracer; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // Maakt of haalt tracing-span op Span span = tracer.nextSpan() .name("gateway-request") .tag("http.method", exchange.getRequest().getMethod().name()) .tag("http.url", exchange.getRequest().getURI().toString()) .start(); // Injecteert tracing-headers 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; } } ``` Tabel met essentiële metrieken: ```text | Metriek | Beschrijving | |-----------------------------------|----------------------------------| | gateway.request.duration | Latentie per route/status | | gateway.requests.total | Verzoekteller | | resilience4j.circuitbreaker.state | Circuit breaker-statussen | | http.server.requests | Standaard HTTP-metrieken | | spring.cloud.gateway.routes | Actieve routes | ``` > **Aanbevolen dashboards** > > Gebruik Grafana met de dashboards van Spring Cloud Gateway en Resilience4j om metrieken te visualiseren. Configureer alerts op P99-latentie en foutpercentage. ## Conclusie Spring Cloud Gateway is een essentieel onderdeel van moderne microservicesarchitecturen. Belangrijke punten om te onthouden voor sollicitatiegesprekken: **Architectuur en concepten:** - ✅ Routes, Predicates en Filters vormen het basismodel - ✅ Reactieve architectuur met WebFlux en Netty - ✅ Native integratie met het Spring Cloud-ecosysteem **Essentiële functionaliteiten:** - ✅ Dynamische routing op meerdere criteria - ✅ Pre-/post-filters voor transformatie van verzoeken en antwoorden - ✅ Load balancing met Spring Cloud LoadBalancer **Veerkracht en beveiliging:** - ✅ Circuit breaker met Resilience4j en fallback - ✅ Retry met exponentiële backoff en jitter - ✅ Gecentraliseerde OAuth2/JWT-authenticatie **Observability:** - ✅ Micrometer-metrieken met tags per route - ✅ Gedistribueerde tracing met contextpropagatie - ✅ Health checks en actuator-endpoints Het beheersen van Spring Cloud Gateway toont een diep begrip van microservices-patronen en schaalbaarheidsvraagstukken. Deze vaardigheden zijn essentieel voor het ontwerpen van robuuste en performante API Gateways. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/spring-boot/spring-cloud-gateway-interview-routing-filters