Spring Cloud Gateway Mülakatı: Routing, Filtreler ve Load Balancing

Teknik mülakatlar için Spring Cloud Gateway'i öğrenin: routing, filtreler, load balancing ve API Gateway pattern'leri üzerine kod örnekleriyle 12 soru.

Spring Cloud Gateway: routing, filtreler ve load balancing üzerine mülakat soruları

Spring Cloud Gateway, Spring mikroservis mimarisinde API Gateway uygulamak için referans çözümdür. Teknik mülakatlar; routing yapılandırma, özel filtreler oluşturma ve load balancing'i etkin biçimde yönetme yetkinliğini değerlendirir.

Mülakat hazırlık ipucu

İşe alım uzmanları Gateway pattern'lerinin anlaşılmasını ölçer: merkezi kimlik doğrulama, rate limiting ve circuit breaker. Spring Cloud Gateway'in alternatiflere göre neden tercih edildiğini açıklayabilmek fark yaratır.

Spring Cloud Gateway mimarisi ve temelleri

Soru 1: Spring Cloud Gateway nedir ve neden kullanılmalı?

Spring Cloud Gateway, Spring WebFlux ve Project Reactor üzerine kurulu reaktif bir API Gateway'dir. Mikroservislere yönelen tüm isteklerin tek giriş noktası olarak çalışır ve routing, filtreleme ve load balancing yetenekleri sunar.

GatewayApplication.javajava
// Spring Cloud Gateway'in temel yapılandırması
@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        // Reaktif Netty sunucusunu başlatır (Tomcat değil)
        SpringApplication.run(GatewayApplication.class, args);
    }
}
yaml
# application.yml
# Minimum gateway yapılandırması
spring:
  cloud:
    gateway:
      routes:
        # Kullanıcı servisine route
        - id: user-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/users/**
        # Sipariş servisine route
        - id: order-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/orders/**

Spring Cloud Gateway'in temel avantajları şunlardır: yüksek performans için bloke etmeyen mimari, Spring Cloud ekosistemiyle yerel entegrasyon ve modern reaktif pattern'ler için destek.

Soru 2: Route, Predicate ve Filter kavramlarını açıklayın

Spring Cloud Gateway'i üç temel kavram yapılandırır: Route'lar hedefleri tanımlar, Predicate'ler bir route'un ne zaman uygulanacağını belirler, Filter'lar istek ve yanıtları değiştirir.

RouteConfiguration.javajava
// Programatik route yapılandırması
@Configuration
public class RouteConfiguration {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // Birden fazla predicate içeren route
            .route("product-service", r -> r
                // Predicate: URL yolu
                .path("/api/products/**")
                // Predicate: HTTP yöntemi
                .and()
                .method(HttpMethod.GET, HttpMethod.POST)
                // Predicate: header mevcut
                .and()
                .header("X-Api-Version", "v2")
                // Filter: yol yeniden yazma
                .filters(f -> f
                    .rewritePath("/api/products/(?<segment>.*)", "/products/${segment}")
                    // Filter: header ekleme
                    .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
                )
                // Hedef URI
                .uri("http://localhost:8083"))
            .build();
    }
}

İşleme akışı şu sırayı izler:

text
Gelen istek
┌─────────────────┐
│   Predicates    │ → Koşulları değerlendirir (path, yöntem, header...)
└────────┬────────┘
         │ Eşleşme bulundu
┌─────────────────┐
│  Pre-Filters    │ → İsteği routing öncesi değiştirir
└────────┬────────┘
┌─────────────────┐
│   HTTP Proxy    │ → Hedef servise yönlendirir
└────────┬────────┘
┌─────────────────┐
│  Post-Filters   │ → Yanıtı istemciye dönmeden önce değiştirir
└────────┬────────┘
   İstemciye yanıt

Soru 3: En çok kullanılan predicate'ler hangileridir?

Spring Cloud Gateway, çeşitli routing koşulları için pek çok yerleşik predicate sağlar. Birden fazla predicate'i birleştirmek karmaşık routing kuralları oluşturmayı sağlar.

yaml
# application.yml
# Yaygın predicate örnekleri
spring:
  cloud:
    gateway:
      routes:
        # Değişken yakalamalı path routing
        - id: user-details
          uri: http://user-service
          predicates:
            - Path=/users/{userId}

        # HTTP yöntemine göre routing
        - id: user-create
          uri: http://user-service
          predicates:
            - Path=/users
            - Method=POST

        # Header tabanlı routing
        - id: mobile-api
          uri: http://mobile-service
          predicates:
            - Header=X-Client-Type, mobile

        # Query parametreli routing
        - id: search-api
          uri: http://search-service
          predicates:
            - Query=q

        # Host tabanlı routing
        - id: admin-portal
          uri: http://admin-service
          predicates:
            - Host=admin.example.com

        # Zaman tabanlı routing
        - id: maintenance-mode
          uri: http://maintenance-service
          predicates:
            - Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z
CustomPredicateFactory.javajava
// Özel predicate oluşturma
@Component
public class ApiKeyRoutePredicateFactory
    extends AbstractRoutePredicateFactory<ApiKeyRoutePredicateFactory.Config> {

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

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            // API key varlığını ve geçerliliğini kontrol eder
            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;
        }
    }
}
Predicate sırası

Predicate sırası değerlendirmeyi etkilemez ancak route sırası önemlidir. Route'lar sırayla değerlendirilir ve ilk eşleşme kullanılır.

Filtreler ve istek dönüşümü

Soru 4: Pre ve post-processing filtreleri nasıl çalışır?

GatewayFilter filtreleri sıralı bir zincirde çalışır. "Pre" filtreler isteği routing öncesi, "post" filtreler ise yanıtı hedef servisten aldıktan sonra değiştirir.

LoggingFilter.javajava
// Global logging filtresi
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {

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

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

        // İstek ID'sini header'lara ekler
        ServerHttpRequest modifiedRequest = exchange.getRequest()
            .mutate()
            .header("X-Request-Id", requestId)
            .build();

        // Zinciri sürdürür ve yanıtı işler
        return chain.filter(exchange.mutate().request(modifiedRequest).build())
            .then(Mono.fromRunnable(() -> {
                // POST-FILTER: yanıt sonrası
                long duration = System.currentTimeMillis() - startTime;
                HttpStatusCode status = exchange.getResponse().getStatusCode();

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

    @Override
    public int getOrder() {
        // Negatif sıra = önce çalışır
        return -1;
    }
}
AuthenticationFilter.javajava
// JWT kimlik doğrulama filtresi
@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);

        // Token varlığını kontrol eder
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return handleUnauthorized(exchange, "Missing or invalid Authorization header");
        }

        String token = authHeader.substring(7);

        // Token'ı reaktif olarak doğrular
        return tokenValidator.validate(token)
            .flatMap(claims -> {
                // İsteği kullanıcı bilgileriyle zenginleştirir
                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));
    }
}

Soru 5: En faydalı yerleşik filtreler hangileridir?

Spring Cloud Gateway, yaygın senaryoları kapsayan birçok yerleşik filtre sunar: URL yeniden yazma, header düzenleme, retry ve circuit breaker.

yaml
# application.yml
# Yaygın yerleşik filtreler
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Yol yeniden yazma
            - RewritePath=/api/orders/(?<segment>.*), /orders/${segment}

            # İstek header'larını ekle
            - AddRequestHeader=X-Gateway-Version, 1.0

            # Hassas yanıt header'larını kaldır
            - RemoveResponseHeader=X-Powered-By
            - RemoveResponseHeader=Server

            # Yol öneki
            - PrefixPath=/v2

            # Önek kaldır
            - StripPrefix=1

            # Hatalarda otomatik retry
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
                methods: GET
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 500ms
                  factor: 2

            # Hız sınırlandırma
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@userKeyResolver}"
RateLimiterConfiguration.javajava
// Kullanıcı başına rate limiter yapılandırması
@Configuration
public class RateLimiterConfiguration {

    @Bean
    public KeyResolver userKeyResolver() {
        // Kimliği doğrulanmış kullanıcı başına sınır
        return exchange -> Mono.just(
            exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id")
        ).defaultIfEmpty("anonymous");
    }

    @Bean
    public KeyResolver ipKeyResolver() {
        // IP adresi başına sınır
        return exchange -> Mono.just(
            Objects.requireNonNull(exchange.getRequest()
                .getRemoteAddress())
                .getAddress()
                .getHostAddress()
        );
    }
}

Spring Boot mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Soru 6: Body değiştiren filtre nasıl uygulanır?

İstek veya yanıt body'sini değiştirmek; ModifyRequestBodyGatewayFilterFactory veya ModifyResponseBodyGatewayFilterFactory ile özel bir yaklaşım gerektirir.

RequestBodyModificationFilter.javajava
// İstek body'sini değiştiren filtre
@Component
@RequiredArgsConstructor
public class RequestBodyModificationFilter implements GlobalFilter, Ordered {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Yalnızca JSON içeren POST/PUT isteklerini değiştirir
        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 {
                    // JSON'ı parse edip değiştirir
                    Map<String, Object> body = objectMapper.readValue(
                        bytes,
                        new TypeReference<Map<String, Object>>() {}
                    );

                    // Meta veri ekler
                    body.put("processedAt", Instant.now().toString());
                    body.put("gatewayVersion", "1.0");

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

                    // Değiştirilmiş body ile yeni istek oluşturur
                    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
// Yanıtları değiştirmek için yapılandırma
@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) -> {
                        // Yanıtı standart bir biçimde sarmalar
                        return Mono.just(String.format(
                            "{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}",
                            responseBody,
                            Instant.now()
                        ));
                    }
                ))
                .uri("lb://user-service"))
            .build();
    }
}

Load Balancing ve dayanıklılık

Soru 7: Spring Cloud LoadBalancer ile load balancing nasıl yapılandırılır?

Spring Cloud Gateway, trafiği servis örnekleri arasında dağıtmak için Spring Cloud LoadBalancer ile entegre olur. lb:// URI şeması otomatik load balancing'i etkinleştirir.

yaml
# application.yml
# Load balancing yapılandırması
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          # lb:// load balancing'i etkinleştirir
          uri: lb://user-service
          predicates:
            - Path=/api/users/**

    # Load balancer yapılandırması
    loadbalancer:
      ribbon:
        enabled: false  # Spring Cloud LoadBalancer kullanır (Ribbon değil)

      # Servis bazlı yapılandırma
      configurations: default

      # Load balancing için health check
      health-check:
        path:
          user-service: /actuator/health
        interval: 10s

# Service discovery (Eureka veya başka)
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
LoadBalancerConfiguration.javajava
// Özel load balancer yapılandırması
@Configuration
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
}

// CustomLoadBalancerConfig.java
// Özel load balancing stratejisi
public class CustomLoadBalancerConfig {

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

        // Varsayılan olarak Round Robin
        return new RoundRobinLoadBalancer(
            clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
            serviceId
        );
    }

    @Bean
    public ServiceInstanceListSupplier serviceInstanceListSupplier(
        ConfigurableApplicationContext context
    ) {
        // Örneklere health check ekler
        return ServiceInstanceListSupplier.builder()
            .withDiscoveryClient()
            .withHealthChecks()
            .withCaching()
            .build(context);
    }
}
WeightedLoadBalancer.javajava
// Ağırlıklı 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();
                }

                // Meta verilerden ağırlıkları hesaplar
                List<WeightedInstance> weighted = instances.stream()
                    .map(instance -> {
                        int weight = Integer.parseInt(
                            instance.getMetadata().getOrDefault("weight", "1")
                        );
                        return new WeightedInstance(instance, weight);
                    })
                    .toList();

                // Ağırlıklı seçim
                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) {}
}

Soru 8: Gateway'de circuit breaker nasıl uygulanır?

Circuit breaker, kademeli arızalara karşı koruma sağlar. Spring Cloud Gateway, gelişmiş arıza yönetimi için Resilience4j ile entegre olur.

yaml
# application.yml
# Resilience4j Circuit Breaker yapılandırması
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Fallback'li circuit breaker
            - name: CircuitBreaker
              args:
                name: orderServiceCB
                fallbackUri: forward:/fallback/orders

resilience4j:
  circuitbreaker:
    configs:
      default:
        # Değerlendirilen istek sayısı
        slidingWindowSize: 10
        # Devreyi açmak için arıza eşiği
        failureRateThreshold: 50
        # Yeniden denemeden önce devre açık kalma süresi
        waitDurationInOpenState: 30s
        # Half-open durumunda izin verilen istekler
        permittedNumberOfCallsInHalfOpenState: 3
        # Otomatik geçişler
        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
// Circuit breaker olay izleme
@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 metriği
        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 ve circuit breaker

Circuit breaker ile HTTP istemcisi arasında tutarlı bir timeout yapılandırın. Çok uzun timeout thread'leri bloke ederken çok kısa olanı yanlış pozitiflere yol açar.

Soru 9: Üstel backoff'lu retry nasıl uygulanır?

Üstel backoff'lu akıllı retry, zorlanan bir servisi ezmemeyi sağlarken başarı şansını da en üst düzeye çıkarır.

yaml
# application.yml
# Backoff'lu retry yapılandırması
spring:
  cloud:
    gateway:
      routes:
        - id: payment-service
          uri: lb://payment-service
          predicates:
            - Path=/api/payments/**
          filters:
            - name: Retry
              args:
                # Deneme sayısı
                retries: 3
                # Retry tetikleyen HTTP kodları
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
                # Yalnızca idempotent HTTP yöntemleri
                methods: GET,PUT
                # Retry tetikleyen istisnalar
                exceptions:
                  - java.io.IOException
                  - java.net.ConnectException
                  - org.springframework.cloud.gateway.support.TimeoutException
                # Üstel backoff
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 2000ms
                  factor: 2
                  basedOnPreviousValue: true
CustomRetryFilter.javajava
// İş mantığı içeren özel retry
@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) {
        // Sadece idempotent yöntemleri tekrar dener
        HttpMethod method = exchange.getRequest().getMethod();
        if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE)
            .contains(method)) {
            return false;
        }

        // İstisna türünü kontrol eder
        return throwable instanceof ConnectException ||
               throwable instanceof TimeoutException ||
               throwable instanceof ServiceUnavailableException;
    }

    private Duration calculateBackoff(int attempt) {
        // Jitter'lı üstel backoff
        long baseBackoff = (long) (
            INITIAL_BACKOFF.toMillis() * Math.pow(BACKOFF_MULTIPLIER, attempt)
        );

        // Rastgele jitter ekler (±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;
    }
}

Gelişmiş pattern'ler ve iyi uygulamalar

Soru 10: İstek toplaması nasıl uygulanır?

Toplama, birden fazla mikroservis çağrısını istemci için tek bir yanıtta birleştirerek gecikmeyi ve frontend karmaşıklığını azaltır.

AggregationController.javajava
// Çoklu servis toplaması
@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) {
        // Farklı servislere paralel çağrılar
        Mono<UserProfile> userMono = fetchUserProfile(userId);
        Mono<List<Order>> ordersMono = fetchRecentOrders(userId);
        Mono<NotificationCount> notificationsMono = fetchNotificationCount(userId);

        // Sonuçların toplanması
        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
// Toplanmış yanıt DTO'su
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardResponse {
    private UserProfile user;
    private List<Order> recentOrders;
    private NotificationCount notificationCount;
    private Instant generatedAt;
    private String error;
}

Soru 11: Gateway OAuth2 ile nasıl güvenli hale getirilir?

OAuth2 entegrasyonu, kimlik doğrulamayı gateway seviyesinde merkezileştirerek her mikroserviste mantığın tekrarını ortadan kaldırır.

yaml
# application.yml
# OAuth2 Resource Server yapılandırması
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
// Gateway güvenlik yapılandırması
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
            // Stateless API için CSRF'yi devre dışı bırakır
            .csrf(ServerHttpSecurity.CsrfSpec::disable)

            // Yetkilendirme yapılandırması
            .authorizeExchange(exchanges -> exchanges
                // Genel endpoint'ler
                .pathMatchers("/actuator/health", "/actuator/info").permitAll()
                .pathMatchers("/api/public/**").permitAll()
                .pathMatchers("/api/auth/**").permitAll()

                // Role özel endpoint'ler
                .pathMatchers("/api/admin/**").hasRole("ADMIN")
                .pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")

                // Geri kalan her şey kimlik doğrulama gerektirir
                .anyExchange().authenticated()
            )

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

            .build();
    }

    @Bean
    public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
            new JwtGrantedAuthoritiesConverter();
        // Rolleri "roles" claim'inden çıkarır
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

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

        return converter;
    }
}
TokenRelayFilter.javajava
// Token'ı downstream servislere iletme
@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 -> {
                // Token'ı downstream servislere iletir
                ServerHttpRequest request = exchange.getRequest()
                    .mutate()
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue())
                    // Token'dan çıkarılan kullanıcı bilgilerini ekler
                    .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() {
        // Kimlik doğrulamadan sonra, routing'den önce
        return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1;
    }
}

Spring Boot mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Soru 12: İzleme ve gözlemlenebilirlikte iyi uygulamalar nelerdir?

Gateway izleme, mikroservis mimarisindeki performans ve erişilebilirlik sorunlarını tespit etmek için kritik önemdedir.

yaml
# application.yml
# Gözlemlenebilirlik yapılandırması
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
// Özel metrik filtresi
@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();

        // Hedef servisi route'tan çıkarır
        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";

        // Gecikme için timer
        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);

        // İstek sayacı
        meterRegistry.counter(
            "gateway.requests.total",
            "route", routeId,
            "status", statusCode
        ).increment();

        // Yavaş isteklerin loglanması
        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
// Tracing bağlamının yayılımı
@Component
@RequiredArgsConstructor
public class TracingFilter implements GlobalFilter, Ordered {

    private final Tracer tracer;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Tracing span'ini oluşturur veya alır
        Span span = tracer.nextSpan()
            .name("gateway-request")
            .tag("http.method", exchange.getRequest().getMethod().name())
            .tag("http.url", exchange.getRequest().getURI().toString())
            .start();

        // Tracing header'larını enjekte eder
        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;
    }
}

Temel metrikler tablosu:

text
| Metrik                            | Açıklama                         |
|-----------------------------------|----------------------------------|
| gateway.request.duration          | Route/durum başına gecikme       |
| gateway.requests.total            | İstek sayacı                     |
| resilience4j.circuitbreaker.state | Circuit breaker durumları        |
| http.server.requests              | Standart HTTP metrikleri         |
| spring.cloud.gateway.routes       | Aktif route'lar                  |
Önerilen panolar

Metrikleri görselleştirmek için Spring Cloud Gateway ve Resilience4j panolarıyla Grafana kullanın. P99 gecikmesi ve hata oranı üzerinde uyarılar yapılandırın.

Sonuç

Spring Cloud Gateway, modern mikroservis mimarilerinin temel bileşenlerinden biridir. Mülakatlar için akılda tutulması gereken anahtar noktalar:

Mimari ve kavramlar:

  • ✅ Routes, Predicates ve Filters temel modeli oluşturur
  • ✅ WebFlux ve Netty ile reaktif mimari
  • ✅ Spring Cloud ekosistemiyle yerel entegrasyon

Temel özellikler:

  • ✅ Çoklu kritere dayalı dinamik routing
  • ✅ İstek ve yanıt dönüşümü için pre/post filtreler
  • ✅ Spring Cloud LoadBalancer ile load balancing

Dayanıklılık ve güvenlik:

  • ✅ Resilience4j ile circuit breaker ve fallback
  • ✅ Üstel backoff ve jitter ile retry
  • ✅ Merkezi OAuth2/JWT kimlik doğrulaması

Gözlemlenebilirlik:

  • ✅ Route bazlı tag'li Micrometer metrikleri
  • ✅ Bağlam yayılımıyla dağıtık tracing
  • ✅ Health check ve actuator endpoint'leri

Spring Cloud Gateway'i kullanmayı bilmek, mikroservis pattern'leri ve ölçeklenebilirlik konularına dair derin bir anlayışı kanıtlar. Bu yetkinlikler, sağlam ve yüksek performanslı API Gateway'ler tasarlamak için vazgeçilmezdir.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

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

Paylaş

İlgili makaleler