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.

Spring Cloud Gateway: sollicitatievragen over routing, filters en load balancing

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.

GatewayApplication.javajava
// 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.

RouteConfiguration.javajava
// 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/(?<segment>.*)", "/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
CustomPredicateFactory.javajava
// Aanmaken van een eigen predicate
@Component
public class ApiKeyRoutePredicateFactory
    extends AbstractRoutePredicateFactory<ApiKeyRoutePredicateFactory.Config> {

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

    @Override
    public Predicate<ServerWebExchange> 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<String> validKeys = new ArrayList<>();

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

        public void setValidKeys(List<String> 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.

LoggingFilter.javajava
// Globaal logging-filter
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> 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;
    }
}
AuthenticationFilter.javajava
// JWT-authenticatiefilter
@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);

        // 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<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));
    }
}

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/(?<segment>.*), /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}"
RateLimiterConfiguration.javajava
// 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()
        );
    }
}

Klaar om je Spring Boot gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

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.

RequestBodyModificationFilter.javajava
// Filter voor aanpassing van de request-body
@Component
@RequiredArgsConstructor
public class RequestBodyModificationFilter implements GlobalFilter, Ordered {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> 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<String, Object> body = objectMapper.readValue(
                        bytes,
                        new TypeReference<Map<String, Object>>() {}
                    );

                    // 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<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
// 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/
LoadBalancerConfiguration.javajava
// 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<ServiceInstance> 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);
    }
}
WeightedLoadBalancer.javajava
// Gewogen load balancer
@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();
                }

                // Berekent gewichten op basis van metadata
                List<WeightedInstance> 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
FallbackController.javajava
// Fallback-controller
@RestController
@RequestMapping("/fallback")
@Slf4j
public class FallbackController {

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

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

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

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

        return Mono.just(ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(response));
    }
}
CircuitBreakerEventListener.javajava
// Monitoring 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
CustomRetryFilter.javajava
// 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<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) {
        // 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.

AggregationController.javajava
// 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<DashboardResponse> getUserDashboard(@PathVariable Long userId) {
        // Parallelle aanroepen naar verschillende services
        Mono<UserProfile> userMono = fetchUserProfile(userId);
        Mono<List<Order>> ordersMono = fetchRecentOrders(userId);
        Mono<NotificationCount> 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<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 van het geaggregeerde antwoord
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardResponse {
    private UserProfile user;
    private List<Order> 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
SecurityConfig.javajava
// 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;
    }
}
TokenRelayFilter.javajava
// Doorgifte van het token aan downstream-services
@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 -> {
                // 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;
    }
}

Klaar om je Spring Boot gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

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
MetricsFilter.javajava
// Filter voor aangepaste metrieken
@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();

        // 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;
    }
}
TracingFilter.javajava
// Doorgifte van tracing-context
@Component
@RequiredArgsConstructor
public class TracingFilter implements GlobalFilter, Ordered {

    private final Tracer tracer;

    @Override
    public Mono<Void> 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.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

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

Delen

Gerelateerde artikelen