TP13.3: Spring Cloud Gateway
🎯 أهداف التمرين
- إنشاء بوابة API باستخدام Spring Cloud Gateway
- تكوين التوجيه نحو الخدمات
- تنفيذ مرشحات مخصصة
- تكوين تحديد المعدل
- إضافة تراجع للخدمات غير المتاحة
المدة المقدرة: 1 ساعة و30 دقيقة
📋 الهندسة المعمارية
الجزء 1: إنشاء بوابة API (20 دقيقة)
1.1 إنشاء المشروع
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>api-gateway</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<dependencies>
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Circuit Breaker -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.2 التطبيق الرئيسي
package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
1.3 تكوين المسارات (YAML)
# src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
# مسار خدمة المستخدم
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- name: CircuitBreaker
args:
name: userServiceCB
fallbackUri: forward:/fallback/users
# مسار خدمة المنتج
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**
filters:
- name: CircuitBreaker
args:
name: productServiceCB
fallbackUri: forward:/fallback/products
# اكتشاف تلقائي (اختياري)
discovery:
locator:
enabled: true
lower-case-service-id: true
# يوريكا
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# Actuator
management:
endpoints:
web:
exposure:
include: health,info,gateway
endpoint:
gateway:
enabled: true
# Resilience4j
resilience4j:
circuitbreaker:
instances:
userServiceCB:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
productServiceCB:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
# تسجيل
logging:
level:
org.springframework.cloud.gateway: DEBUG
الجزء 2: وحدة التحكم في التراجع (15 دقيقة)
2.1 إنشاء وحدة التحكم في التراجع
package com.example.gateway.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.Map;
@RestController
@RequestMapping("/fallback")
public class FallbackController {
@GetMapping("/users")
public ResponseEntity<Map<String, Object>> userServiceFallback() {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of(
"status", "SERVICE_UNAVAILABLE",
"message", "خدمة المستخدم غير متاحة حاليًا. يرجى المحاولة مرة أخرى لاحقًا.",
"timestamp", LocalDateTime.now().toString(),
"service", "user-service"
));
}
@GetMapping("/products")
public ResponseEntity<Map<String, Object>> productServiceFallback() {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of(
"status", "SERVICE_UNAVAILABLE",
"message", "خدمة المنتج غير متاحة حاليًا. يرجى المحاولة مرة أخرى لاحقًا.",
"timestamp", LocalDateTime.now().toString(),
"service", "product-service"
));
}
@GetMapping("/default")
public ResponseEntity<Map<String, Object>> defaultFallback() {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of(
"status", "SERVICE_UNAVAILABLE",
"message", "الخدمة غير متاحة مؤقتًا",
"timestamp", LocalDateTime.now().toString()
));
}
}
الجزء 3: مرشح تسجيل مخصص (20 دقيقة)
3.1 مرشح عالمي لتسجيل الطلبات
package com.example.gateway.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.UUID;
@Component
public class LoggingFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// توليد معرف ارتباط
String correlationId = UUID.randomUUID().toString().substring(0, 8);
long startTime = System.currentTimeMillis();
ServerHttpRequest request = exchange.getRequest();
// تسجيل الطلب الوارد
log.info(">>> [{}] Incoming: {} {} from {}",
correlationId,
request.getMethod(),
request.getURI().getPath(),
request.getRemoteAddress()
);
// إضافة معرف الارتباط إلى الرأس
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-Correlation-Id", correlationId)
.build();
ServerWebExchange mutatedExchange = exchange.mutate()
.request(mutatedRequest)
.build();
return chain.filter(mutatedExchange)
.then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
log.info("<<< [{}] Response: {} | Duration: {}ms",
correlationId,
exchange.getResponse().getStatusCode(),
duration
);
}));
}
@Override
public int getOrder() {
return -100; // تنفيذ مبكر جدًا
}
}
3.2 مرشح لإضافة رؤوس الاستجابة
package com.example.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class ResponseHeaderFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("X-Gateway-Version", "1.0.0");
response.getHeaders().add("X-Powered-By", "Spring Cloud Gateway");
}));
}
@Override
public int getOrder() {
return -1;
}
}
الجزء 4: تكوين CORS (10 دقائق)
4.1 تكوين CORS
package com.example.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
// الأصول المسموح بها
corsConfig.setAllowedOrigins(List.of(
"http://localhost:3000", // تطوير React
"http://localhost:4200", // تطوير Angular
"http://localhost:8080" // نفس الأصل
));
// الطرق المسموح بها
corsConfig.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"
));
// الرؤوس المسموح بها
corsConfig.setAllowedHeaders(List.of("*"));
// بيانات الاعتماد
corsConfig.setAllowCredentials(true);
// تخزين مؤقت للطلبات المسبقة
corsConfig.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return new CorsWebFilter(source);
}
}
الجزء 5: اختبار البوابة (15 دقيقة)
5.1 ترتيب بدء التشغيل
- خادم يوريكا (8761)
- خدمة المستخدم (8081)
- خدمة المنتج (8083)
- بوابة API (8080)
# الطرفية 1
cd eureka-server && mvn spring-boot:run
# الطرفية 2
cd user-service && mvn spring-boot:run
# الطرفية 3
cd product-service && mvn spring-boot:run
# الطرفية 4
cd api-gateway && mvn spring-boot:run
5.2 اختبار المسارات
# عبر البوابة - المستخدمون
curl http://localhost:8080/api/users
curl http://localhost:8080/api/users/1
# عبر البوابة - المنتجات
curl http://localhost:8080/api/products
curl http://localhost:8080/api/products/1
# تحقق من رؤوس الاستجابة
curl -i http://localhost:8080/api/users
# يجب أن يتضمن:
# X-Gateway-Version: 1.0.0
# X-Powered-By: Spring Cloud Gateway
5.3 اختبار التراجع
# إيقاف خدمة المستخدم (Ctrl+C في طرفيتها)
# انتظر 30 ثانية (إزالة يوريكا)
# اختبار عبر البوابة
curl http://localhost:8080/api/users
# النتيجة المتوقعة:
{
"status": "SERVICE_UNAVAILABLE",
"message": "خدمة المستخدم غير متاحة حاليًا. يرجى المحاولة مرة أخرى لاحقًا.",
"timestamp": "2024-01-15T10:30:00",
"service": "user-service"
}
5.4 استعراض المسارات النشطة
curl http://localhost:8080/actuator/gateway/routes
الاستجابة:
[
{
"predicate": "Paths: [/api/users/**], match trailing slash: true",
"route_id": "user-service",
"filters": ["CircuitBreaker name='userServiceCB' fallback='forward:/fallback/users'"],
"uri": "lb://user-service",
"order": 0
},
{
"predicate": "Paths: [/api/products/**], match trailing slash: true",
"route_id": "product-service",
"filters": ["CircuitBreaker name='productServiceCB' fallback='forward:/fallback/products'"],
"uri": "lb://product-service",
"order": 0
}
]
✅ قائمة التحقق من التحقق
- بوابة API تعمل على http://localhost:8080
-
curl http://localhost:8080/api/users→ استجابة من خدمة المستخدم -
curl http://localhost:8080/api/products→ استجابة من خدمة المنتج - رؤوس
X-Gateway-VersionوX-Powered-Byموجودة - سجلات الطلبات مع معرف الارتباط
- التراجع يعمل عندما تكون الخدمة معطلة
-
/actuator/gateway/routesيسرد المسارات
🎯 تمارين إضافية
التمرين 1: تحديد المعدل
أضف محدد معدل (يتطلب Redis):
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
التمرين 2: إعادة كتابة المسار
إعادة توجيه /v1/users/** إلى /api/users/**:
- id: user-service-v1
uri: lb://user-service
predicates:
- Path=/v1/users/**
filters:
- RewritePath=/v1/(?<segment>.*), /api/$\{segment}
التمرين 3: مصادقة بسيطة
أنشئ مرشحًا يتحقق من وجود رأس X-API-Key.