# สัมภาษณ์ Spring Cloud Gateway: Routing, Filter และ Load Balancing > เชี่ยวชาญ Spring Cloud Gateway สำหรับการสัมภาษณ์เทคนิค: 12 คำถามครอบคลุม routing, filter, load balancing และ pattern API Gateway พร้อมตัวอย่างโค้ด. - 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 คือโซลูชันต้นแบบสำหรับการนำ API Gateway มาใช้งานในสถาปัตยกรรมไมโครเซอร์วิสของ Spring การสัมภาษณ์เทคนิคจะประเมินความสามารถในการตั้งค่า routing สร้าง filter ที่กำหนดเอง และจัดการ load balancing อย่างมีประสิทธิภาพ > **คำแนะนำในการเตรียมตัว** > > ผู้สรรหาบุคลากรจะตรวจสอบความเข้าใจในรูปแบบ Gateway: การยืนยันตัวตนแบบรวมศูนย์ rate limiting และ circuit breaker ความสามารถในการอธิบายว่าทำไมจึงเลือก Spring Cloud Gateway แทนทางเลือกอื่นเป็นจุดที่สร้างความแตกต่าง ## สถาปัตยกรรมและพื้นฐานของ Spring Cloud Gateway ### คำถามที่ 1: Spring Cloud Gateway คืออะไรและทำไมจึงควรใช้? Spring Cloud Gateway เป็น API Gateway แบบ reactive ที่สร้างบน Spring WebFlux และ Project Reactor ทำหน้าที่เป็นจุดเข้าเดียวสำหรับคำขอทั้งหมดที่ส่งไปยังไมโครเซอร์วิส โดยให้ความสามารถ routing การกรอง และ load balancing ```java // GatewayApplication.java // การตั้งค่าพื้นฐานของ Spring Cloud Gateway @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { // เริ่มต้นเซิร์ฟเวอร์ reactive Netty (ไม่ใช่ Tomcat) SpringApplication.run(GatewayApplication.class, args); } } ``` ```yaml # application.yml # การตั้งค่า gateway ขั้นต่ำ spring: cloud: gateway: routes: # เส้นทางไปยังเซอร์วิสผู้ใช้ - id: user-service uri: http://localhost:8081 predicates: - Path=/api/users/** # เส้นทางไปยังเซอร์วิสคำสั่งซื้อ - id: order-service uri: http://localhost:8082 predicates: - Path=/api/orders/** ``` ข้อดีหลักของ Spring Cloud Gateway ได้แก่ สถาปัตยกรรมแบบ non-blocking เพื่อประสิทธิภาพสูง การผสานรวมกับระบบนิเวศ Spring Cloud อย่างเป็นธรรมชาติ และรองรับ pattern reactive สมัยใหม่ ### คำถามที่ 2: อธิบายแนวคิด Route, Predicate และ Filter สามแนวคิดหลักที่ประกอบเป็น Spring Cloud Gateway: Route กำหนดปลายทาง Predicate ระบุว่าเมื่อใดควรใช้ route และ Filter ปรับเปลี่ยนคำขอและการตอบกลับ ```java // RouteConfiguration.java // การตั้งค่า route แบบโปรแกรม @Configuration public class RouteConfiguration { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() // Route ที่มีหลาย predicate .route("product-service", r -> r // Predicate: เส้นทาง URL .path("/api/products/**") // Predicate: HTTP method .and() .method(HttpMethod.GET, HttpMethod.POST) // Predicate: header ที่มีอยู่ .and() .header("X-Api-Version", "v2") // Filter: การเขียน path ใหม่ .filters(f -> f .rewritePath("/api/products/(?.*)", "/products/${segment}") // Filter: เพิ่ม header .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway") ) // URI ปลายทาง .uri("http://localhost:8083")) .build(); } } ``` ลำดับการประมวลผลเป็นไปดังนี้: ```text คำขอเข้า │ ▼ ┌─────────────────┐ │ Predicates │ → ประเมินเงื่อนไข (path, method, header...) └────────┬────────┘ │ พบการจับคู่ ▼ ┌─────────────────┐ │ Pre-Filters │ → ปรับคำขอก่อน routing └────────┬────────┘ │ ▼ ┌─────────────────┐ │ HTTP Proxy │ → ส่งต่อไปยังเซอร์วิสปลายทาง └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Post-Filters │ → ปรับการตอบกลับก่อนส่งคืนไคลเอนต์ └────────┬────────┘ │ ▼ ตอบกลับไปยังไคลเอนต์ ``` ### คำถามที่ 3: predicate ที่ใช้บ่อยมีอะไรบ้าง? Spring Cloud Gateway ให้ predicate สำเร็จรูปจำนวนมากสำหรับเงื่อนไข routing ที่หลากหลาย การรวม predicate หลายตัวช่วยสร้างกฎ routing ที่ซับซ้อน ```yaml # application.yml # ตัวอย่าง predicate ทั่วไป spring: cloud: gateway: routes: # Routing ตาม path พร้อมจับตัวแปร - id: user-details uri: http://user-service predicates: - Path=/users/{userId} # Routing ตาม HTTP method - id: user-create uri: http://user-service predicates: - Path=/users - Method=POST # Routing ตาม header - id: mobile-api uri: http://mobile-service predicates: - Header=X-Client-Type, mobile # Routing ตาม query parameter - id: search-api uri: http://search-service predicates: - Query=q # Routing ตาม host - id: admin-portal uri: http://admin-service predicates: - Host=admin.example.com # Routing ตามเวลา - id: maintenance-mode uri: http://maintenance-service predicates: - Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z ``` ```java // CustomPredicateFactory.java // การสร้าง predicate กำหนดเอง @Component public class ApiKeyRoutePredicateFactory extends AbstractRoutePredicateFactory { public ApiKeyRoutePredicateFactory() { super(Config.class); } @Override public Predicate apply(Config config) { return exchange -> { // ตรวจสอบการมีอยู่และความถูกต้องของ 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; } } } ``` > **ลำดับของ predicate** > > ลำดับของ predicate ไม่ส่งผลต่อการประเมิน แต่ลำดับของ route สำคัญ Route จะถูกประเมินตามลำดับและจะใช้คู่ที่ตรงกันคู่แรก ## Filter และการแปลงคำขอ ### คำถามที่ 4: filter ก่อนและหลังการประมวลผลทำงานอย่างไร? Filter GatewayFilter ทำงานในห่วงโซ่ที่มีลำดับ filter "pre" จะปรับคำขอก่อน routing ส่วน filter "post" จะปรับการตอบกลับหลังจากได้รับจากเซอร์วิสปลายทาง ```java // LoggingFilter.java // Filter logging แบบโกลบอล @Component @Slf4j public class LoggingFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // PRE-FILTER: ก่อน routing String requestId = UUID.randomUUID().toString(); long startTime = System.currentTimeMillis(); log.info("Request {} started: {} {}", requestId, exchange.getRequest().getMethod(), exchange.getRequest().getPath()); // เพิ่ม request ID เข้าใน header ServerHttpRequest modifiedRequest = exchange.getRequest() .mutate() .header("X-Request-Id", requestId) .build(); // ดำเนินห่วงโซ่ต่อและจัดการการตอบกลับ return chain.filter(exchange.mutate().request(modifiedRequest).build()) .then(Mono.fromRunnable(() -> { // POST-FILTER: หลังการตอบกลับ long duration = System.currentTimeMillis() - startTime; HttpStatusCode status = exchange.getResponse().getStatusCode(); log.info("Request {} completed: status={}, duration={}ms", requestId, status, duration); })); } @Override public int getOrder() { // ลำดับติดลบ = ทำงานก่อน return -1; } } ``` ```java // AuthenticationFilter.java // Filter ยืนยันตัวตน JWT @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); // ตรวจสอบการมีอยู่ของ token if (authHeader == null || !authHeader.startsWith("Bearer ")) { return handleUnauthorized(exchange, "Missing or invalid Authorization header"); } String token = authHeader.substring(7); // ตรวจสอบ token แบบ reactive return tokenValidator.validate(token) .flatMap(claims -> { // เสริมข้อมูลผู้ใช้ให้กับคำขอ 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)); } } ``` ### คำถามที่ 5: filter สำเร็จรูปที่มีประโยชน์ที่สุดมีอะไรบ้าง? Spring Cloud Gateway มี filter สำเร็จรูปครอบคลุมกรณีใช้งานทั่วไป: การเขียน URL ใหม่ การปรับ header การ retry และ circuit breaker ```yaml # application.yml # Filter สำเร็จรูปที่ใช้บ่อย spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: # การเขียน path ใหม่ - RewritePath=/api/orders/(?.*), /orders/${segment} # เพิ่ม header ของคำขอ - AddRequestHeader=X-Gateway-Version, 1.0 # ลบ header ตอบกลับที่อ่อนไหว - RemoveResponseHeader=X-Powered-By - RemoveResponseHeader=Server # คำนำหน้า path - PrefixPath=/v2 # ลบคำนำหน้า - StripPrefix=1 # Retry อัตโนมัติเมื่อเกิดข้อผิดพลาด - name: Retry args: retries: 3 statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE methods: GET backoff: firstBackoff: 100ms maxBackoff: 500ms factor: 2 # การจำกัดอัตรา - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: "#{@userKeyResolver}" ``` ```java // RateLimiterConfiguration.java // การตั้งค่า rate limiter ต่อผู้ใช้ @Configuration public class RateLimiterConfiguration { @Bean public KeyResolver userKeyResolver() { // จำกัดต่อผู้ใช้ที่ยืนยันตัวตนแล้ว return exchange -> Mono.just( exchange.getRequest() .getHeaders() .getFirst("X-User-Id") ).defaultIfEmpty("anonymous"); } @Bean public KeyResolver ipKeyResolver() { // จำกัดต่อที่อยู่ IP return exchange -> Mono.just( Objects.requireNonNull(exchange.getRequest() .getRemoteAddress()) .getAddress() .getHostAddress() ); } } ``` ### คำถามที่ 6: จะปรับ body ด้วย filter ได้อย่างไร? การปรับ body ของคำขอหรือการตอบกลับต้องใช้แนวทางเฉพาะกับ ModifyRequestBodyGatewayFilterFactory หรือ ModifyResponseBodyGatewayFilterFactory ```java // RequestBodyModificationFilter.java // Filter ปรับ body ของคำขอ @Component @RequiredArgsConstructor public class RequestBodyModificationFilter implements GlobalFilter, Ordered { private final ObjectMapper objectMapper; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // ปรับเฉพาะคำขอ POST/PUT ที่เป็น JSON if (!isJsonRequest(exchange)) { return chain.filter(exchange); } return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); try { // Parse และปรับ JSON Map body = objectMapper.readValue( bytes, new TypeReference>() {} ); // เพิ่มเมตาดาต้า body.put("processedAt", Instant.now().toString()); body.put("gatewayVersion", "1.0"); byte[] modifiedBytes = objectMapper.writeValueAsBytes(body); // สร้างคำขอใหม่ด้วย 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 // การตั้งค่าเพื่อปรับการตอบกลับ @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) -> { // หุ้มการตอบกลับในรูปแบบมาตรฐาน return Mono.just(String.format( "{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}", responseBody, Instant.now() )); } )) .uri("lb://user-service")) .build(); } } ``` ## Load Balancing และความทนทาน ### คำถามที่ 7: ตั้งค่า load balancing ด้วย Spring Cloud LoadBalancer อย่างไร? Spring Cloud Gateway ผสานกับ Spring Cloud LoadBalancer เพื่อกระจายทราฟฟิกระหว่างอินสแตนซ์ของเซอร์วิส รูปแบบ URI `lb://` เปิดใช้งาน load balancing อัตโนมัติ ```yaml # application.yml # การตั้งค่า load balancing spring: cloud: gateway: routes: - id: user-service # lb:// เปิดใช้งาน load balancing uri: lb://user-service predicates: - Path=/api/users/** # การตั้งค่า load balancer loadbalancer: ribbon: enabled: false # ใช้ Spring Cloud LoadBalancer (ไม่ใช่ Ribbon) # การตั้งค่าตามเซอร์วิส configurations: default # Health check สำหรับ load balancing health-check: path: user-service: /actuator/health interval: 10s # Service discovery (Eureka หรืออื่นๆ) eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ ``` ```java // LoadBalancerConfiguration.java // การตั้งค่า load balancer แบบกำหนดเอง @Configuration @LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class) public class LoadBalancerConfiguration { } // CustomLoadBalancerConfig.java // กลยุทธ์ load balancing แบบกำหนดเอง public class CustomLoadBalancerConfig { @Bean public ReactorLoadBalancer loadBalancer( Environment environment, LoadBalancerClientFactory clientFactory ) { String serviceId = environment.getProperty( LoadBalancerClientFactory.PROPERTY_NAME ); // ใช้ Round Robin เป็นค่าเริ่มต้น return new RoundRobinLoadBalancer( clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId ); } @Bean public ServiceInstanceListSupplier serviceInstanceListSupplier( ConfigurableApplicationContext context ) { // เพิ่ม health check ให้กับอินสแตนซ์ return ServiceInstanceListSupplier.builder() .withDiscoveryClient() .withHealthChecks() .withCaching() .build(context); } } ``` ```java // WeightedLoadBalancer.java // 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(); } // คำนวณน้ำหนักจากเมตาดาต้า List weighted = instances.stream() .map(instance -> { int weight = Integer.parseInt( instance.getMetadata().getOrDefault("weight", "1") ); return new WeightedInstance(instance, weight); }) .toList(); // การเลือกตามน้ำหนัก 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) {} } ``` ### คำถามที่ 8: นำ circuit breaker มาใช้ใน gateway อย่างไร? Circuit breaker ป้องกันความล้มเหลวต่อเนื่องเป็นลูกโซ่ Spring Cloud Gateway ผสานกับ Resilience4j สำหรับการจัดการความล้มเหลวขั้นสูง ```yaml # application.yml # การตั้งค่า Circuit Breaker ของ Resilience4j spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: # Circuit breaker พร้อม fallback - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/orders resilience4j: circuitbreaker: configs: default: # จำนวนคำขอที่ประเมิน slidingWindowSize: 10 # เกณฑ์ความล้มเหลวเพื่อเปิดวงจร failureRateThreshold: 50 # ระยะเวลาที่วงจรเปิดอยู่ก่อนลองใหม่ waitDurationInOpenState: 30s # คำขอที่อนุญาตในสถานะ half-open permittedNumberOfCallsInHalfOpenState: 3 # การเปลี่ยนสถานะอัตโนมัติ automaticTransitionFromOpenToHalfOpenEnabled: true instances: orderServiceCB: baseConfig: default failureRateThreshold: 60 timelimiter: configs: default: timeoutDuration: 5s instances: orderServiceCB: timeoutDuration: 3s ``` ```java // FallbackController.java // 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 // การติดตามเหตุการณ์ของ circuit breaker @Component @Slf4j @RequiredArgsConstructor public class CircuitBreakerEventListener { private final MeterRegistry meterRegistry; @EventListener public void onCircuitBreakerStateTransition( CircuitBreakerOnStateTransitionEvent event ) { CircuitBreaker.StateTransition transition = event.getStateTransition(); log.info("Circuit breaker {} state changed: {} -> {}", event.getCircuitBreakerName(), transition.getFromState(), transition.getToState()); // เมตริก Micrometer meterRegistry.counter( "circuit_breaker.state_transition", "name", event.getCircuitBreakerName(), "from", transition.getFromState().name(), "to", transition.getToState().name() ).increment(); } @EventListener public void onCircuitBreakerFailure(CircuitBreakerOnErrorEvent event) { log.error("Circuit breaker {} error: {}", event.getCircuitBreakerName(), event.getThrowable().getMessage()); } } ``` > **Timeout และ circuit breaker** > > ตั้ง timeout ให้สอดคล้องกันระหว่าง circuit breaker และ HTTP client timeout ที่นานเกินไปจะบล็อก thread ส่วนที่สั้นเกินไปทำให้เกิดสัญญาณเตือนผิดพลาด ### คำถามที่ 9: ทำ retry ด้วย exponential backoff อย่างไร? Retry แบบฉลาดด้วย exponential backoff ช่วยไม่ให้เซอร์วิสที่กำลังมีปัญหาถูกถล่มเพิ่ม ขณะเดียวกันก็เพิ่มโอกาสสำเร็จให้สูงสุด ```yaml # application.yml # การตั้งค่า retry พร้อม backoff spring: cloud: gateway: routes: - id: payment-service uri: lb://payment-service predicates: - Path=/api/payments/** filters: - name: Retry args: # จำนวนครั้งที่พยายาม retries: 3 # รหัส HTTP ที่กระตุ้น retry statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT # เฉพาะ HTTP method ที่ idempotent methods: GET,PUT # Exception ที่กระตุ้น retry exceptions: - java.io.IOException - java.net.ConnectException - org.springframework.cloud.gateway.support.TimeoutException # Exponential backoff backoff: firstBackoff: 100ms maxBackoff: 2000ms factor: 2 basedOnPreviousValue: true ``` ```java // CustomRetryFilter.java // 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 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) { // Retry เฉพาะ method ที่ idempotent HttpMethod method = exchange.getRequest().getMethod(); if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE) .contains(method)) { return false; } // ตรวจสอบประเภทของ exception return throwable instanceof ConnectException || throwable instanceof TimeoutException || throwable instanceof ServiceUnavailableException; } private Duration calculateBackoff(int attempt) { // Exponential backoff พร้อม jitter long baseBackoff = (long) ( INITIAL_BACKOFF.toMillis() * Math.pow(BACKOFF_MULTIPLIER, attempt) ); // เพิ่ม jitter แบบสุ่ม (±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; } } ``` ## รูปแบบขั้นสูงและแนวปฏิบัติที่ดี ### คำถามที่ 10: ทำ aggregation ของคำขออย่างไร? Aggregation รวมการเรียกไมโครเซอร์วิสหลายครั้งให้เป็นการตอบกลับเดียวสำหรับไคลเอนต์ ลดความหน่วงและความซับซ้อนของฝั่ง frontend ```java // AggregationController.java // Aggregation หลายเซอร์วิส @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) { // เรียกแบบขนานไปยังเซอร์วิสต่างๆ Mono userMono = fetchUserProfile(userId); Mono> ordersMono = fetchRecentOrders(userId); Mono notificationsMono = fetchNotificationCount(userId); // รวมผลลัพธ์ 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 ของการตอบกลับที่รวมแล้ว @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DashboardResponse { private UserProfile user; private List recentOrders; private NotificationCount notificationCount; private Instant generatedAt; private String error; } ``` ### คำถามที่ 11: ทำให้ gateway ปลอดภัยด้วย OAuth2 อย่างไร? การผสาน OAuth2 รวมศูนย์การยืนยันตัวตนที่ระดับ gateway หลีกเลี่ยงการทำซ้ำตรรกะในแต่ละไมโครเซอร์วิส ```yaml # application.yml # การตั้งค่า OAuth2 Resource Server spring: security: oauth2: resourceserver: jwt: issuer-uri: https://auth.example.com/realms/myrealm jwk-set-uri: https://auth.example.com/realms/myrealm/protocol/openid-connect/certs ``` ```java // SecurityConfig.java // การตั้งค่าความปลอดภัยของ gateway @Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http // ปิด CSRF สำหรับ API แบบไร้สถานะ .csrf(ServerHttpSecurity.CsrfSpec::disable) // การตั้งค่าการอนุญาต .authorizeExchange(exchanges -> exchanges // Endpoint สาธารณะ .pathMatchers("/actuator/health", "/actuator/info").permitAll() .pathMatchers("/api/public/**").permitAll() .pathMatchers("/api/auth/**").permitAll() // Endpoint ตามบทบาท .pathMatchers("/api/admin/**").hasRole("ADMIN") .pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN") // ส่วนที่เหลือต้องยืนยันตัวตน .anyExchange().authenticated() ) // OAuth2 Resource Server พร้อม JWT .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) ) .build(); } @Bean public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // ดึงบทบาทจาก claim "roles" grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter( new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter) ); return converter; } } ``` ```java // TokenRelayFilter.java // การส่งต่อ token ไปยังเซอร์วิสปลายน้ำ @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 -> { // ส่งต่อ token ไปยังเซอร์วิสปลายน้ำ ServerHttpRequest request = exchange.getRequest() .mutate() .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue()) // เพิ่มข้อมูลผู้ใช้ที่ดึงจาก token .header("X-User-Id", jwt.getSubject()) .header("X-User-Email", jwt.getClaimAsString("email")) .build(); return exchange.mutate().request(request).build(); }) .defaultIfEmpty(exchange) .flatMap(chain::filter); } @Override public int getOrder() { // หลังการยืนยันตัวตน ก่อน routing return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1; } } ``` ### คำถามที่ 12: แนวปฏิบัติที่ดีในการมอนิเตอร์และ observability มีอะไรบ้าง? การมอนิเตอร์ gateway สำคัญอย่างยิ่งต่อการระบุปัญหาประสิทธิภาพและความพร้อมใช้งานในสถาปัตยกรรมไมโครเซอร์วิส ```yaml # application.yml # การตั้งค่า observability 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 เมตริกแบบกำหนดเอง @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(); // ดึงเซอร์วิสปลายทางจาก 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 สำหรับความหน่วง 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); // ตัวนับคำขอ meterRegistry.counter( "gateway.requests.total", "route", routeId, "status", statusCode ).increment(); // Log คำขอที่ช้า 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 // การส่งต่อบริบท tracing @Component @RequiredArgsConstructor public class TracingFilter implements GlobalFilter, Ordered { private final Tracer tracer; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // สร้างหรือเรียก span tracing Span span = tracer.nextSpan() .name("gateway-request") .tag("http.method", exchange.getRequest().getMethod().name()) .tag("http.url", exchange.getRequest().getURI().toString()) .start(); // ฉีด header tracing ServerHttpRequest request = exchange.getRequest() .mutate() .header("X-Trace-Id", span.context().traceId()) .header("X-Span-Id", span.context().spanId()) .build(); return chain.filter(exchange.mutate().request(request).build()) .doOnSuccess(v -> { HttpStatusCode status = exchange.getResponse().getStatusCode(); span.tag("http.status_code", status != null ? String.valueOf(status.value()) : "0"); span.end(); }) .doOnError(e -> { span.tag("error", e.getMessage()); span.end(); }); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 1; } } ``` ตารางเมตริกสำคัญ: ```text | เมตริก | คำอธิบาย | |-----------------------------------|----------------------------------| | gateway.request.duration | ความหน่วงต่อ route/สถานะ | | gateway.requests.total | ตัวนับคำขอ | | resilience4j.circuitbreaker.state | สถานะของ circuit breaker | | http.server.requests | เมตริก HTTP มาตรฐาน | | spring.cloud.gateway.routes | route ที่กำลังใช้งาน | ``` > **Dashboard ที่แนะนำ** > > ใช้ Grafana ร่วมกับ dashboard ของ Spring Cloud Gateway และ Resilience4j เพื่อแสดงผลเมตริก ตั้งการแจ้งเตือนสำหรับความหน่วง P99 และอัตราข้อผิดพลาด ## บทสรุป Spring Cloud Gateway เป็นองค์ประกอบสำคัญของสถาปัตยกรรมไมโครเซอร์วิสยุคใหม่ ประเด็นหลักที่ควรจดจำสำหรับการสัมภาษณ์: **สถาปัตยกรรมและแนวคิด:** - ✅ Routes, Predicates และ Filters คือโมเดลพื้นฐาน - ✅ สถาปัตยกรรม reactive ด้วย WebFlux และ Netty - ✅ ผสานเข้ากับระบบนิเวศ Spring Cloud อย่างเป็นธรรมชาติ **ฟีเจอร์สำคัญ:** - ✅ Routing แบบไดนามิกตามหลายเกณฑ์ - ✅ Filter pre/post สำหรับการแปลงคำขอและการตอบกลับ - ✅ Load balancing ด้วย Spring Cloud LoadBalancer **ความทนทานและความปลอดภัย:** - ✅ Circuit breaker ด้วย Resilience4j พร้อม fallback - ✅ Retry ด้วย exponential backoff และ jitter - ✅ การยืนยันตัวตน OAuth2/JWT แบบรวมศูนย์ **Observability:** - ✅ เมตริก Micrometer พร้อม tag ตาม route - ✅ Tracing แบบกระจายพร้อมการส่งต่อบริบท - ✅ Health check และ endpoint ของ actuator ความเชี่ยวชาญใน Spring Cloud Gateway แสดงถึงความเข้าใจอย่างลึกซึ้งในรูปแบบไมโครเซอร์วิสและประเด็นการขยายระบบ ทักษะเหล่านี้จำเป็นสำหรับการออกแบบ API Gateway ที่แข็งแกร่งและประสิทธิภาพสูง --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/spring-boot/spring-cloud-gateway-interview-routing-filters