دروس السلسلة: Virtual threads Java 25 · Spring Boot 4 + GraalVM Native · JPA Hibernate المتقدم · Resilience4j 3
عندما يتفكّك نظام إلى microservices، كشف كل واحد مباشرة للخارج يصبح سريعًا غير قابل للإدارة: مصادقة مُكرَّرة، CORS مُشتَّت، rate limiting غير متماسك، logs مُبَعثَرة. API Gateway تُمركز هذه الاهتمامات المتقاطعة وتُقدّم نقطة دخول وحيدة للعملاء. في منظومة Spring، Spring Cloud Gateway هو الحل الرسمي. الإصدار 4.3.4 (الصادر 2 أبريل 2026 في قطار Spring Cloud 2025.0) متوافق مع Spring Boot 4 وJava 25. يكشف نسختين: Server WebFlux (تفاعلية، مبنية Netty) وServer MVC (متزامنة، مبنية Tomcat).
المتطلبات
- Java 25 LTS
- Spring Boot 4.0+
- 2 microservices للكشف خلف Gateway
- اختياري: Redis لـ rate limiting موزّع
- الوقت المُقدَّر: 90 دقيقة
الخطوة 1 — إنشاء مشروع Gateway
Spring Initializr يقترح starters مميزين منذ Spring Cloud Gateway 4.1: Reactive Gateway (مبني Netty + WebFlux، الافتراضي التاريخي) وServer Gateway (مبني Tomcat + WebMVC، مُضاف في 4.1 للفرق غير التفاعلية).
<!-- pom.xml pour Reactive Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<!-- OU pour MVC Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webmvc</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
للبقية، نستخدم النسخة reactive (WebFlux) التي تبقى الاستخدام الأغلب وتُقدّم أفضل throughput لنمط Gateway. اختيار MVC ينفرض إذا تحتاج مكتبات Servlet غير متوافقة مع Netty.
الخطوة 2 — تعريف routes في YAML
route في Spring Cloud Gateway يربط prédicat (شرط matching) بـ URI هدف، اختياريًا مُثرى بـ filters تُحوّل الطلب/الرد.
# application.yml
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: catalogue
uri: http://catalogue-service:8081
predicates:
- Path=/api/catalogue/**
filters:
- RewritePath=/api/catalogue/(?<path>.*), /\${path}
- id: commandes
uri: http://commandes-service:8082
predicates:
- Path=/api/commandes/**
- Method=GET,POST,PUT
filters:
- StripPrefix=2
- AddRequestHeader=X-Source, gateway
- id: legacy
uri: https://legacy.example.com
predicates:
- Path=/legacy/**
- Header=X-Migration, true
ثلاثة prédicats مفتاحية. Path يُطابق مسار URL (wildcards * و**). Method يُقَيِّد بأفعال HTTP المسرودة. Header يُطابق header بقيمة (مفيد لـ A/B tests أو الترحيلات).
الخطوة 3 — Filters: إضافة/إعادة كتابة/حذف
filters:
# Ajouter un header avant de transmettre
- AddRequestHeader=X-Tenant, mon-tenant
# Ajouter un header dans la réponse
- AddResponseHeader=X-Gateway, ITSC
# Supprimer N segments du chemin
- StripPrefix=2 # /api/catalogue/produits → /produits
# Réécrire le chemin avec regex
- RewritePath=/api/v1/(?<path>.*), /v2/\${path}
# Ajouter un paramètre query
- AddRequestParameter=correlation_id, abc123
# Retry sur erreurs transitoires
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE
methods: GET, POST
backoff:
firstBackoff: 100ms
maxBackoff: 1s
factor: 2
# Circuit breaker (s'intègre avec Resilience4j)
- name: CircuitBreaker
args:
name: catalogueCB
fallbackUri: forward:/fallback/catalogue
# Cache (Reactor Cache)
- name: LocalResponseCache
args:
timeToLive: 30s
الـ Retry يستحق الانتباه: افتراضيًا، Gateway يُعيد المحاولة للطلبات idempotente (GET، HEAD) على أخطاء 502/503/504. تفعيل retry على POST/PUT دون idempotency keys على جانب backend يُسبّب تكرارات (طلب مرّر مرتين).
الخطوة 4 — Routes برمجية بـ RouteLocator
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder, TenantConfigService tenantConfig) {
var builderRoutes = builder.routes();
for (Tenant t : tenantConfig.tousActifs()) {
builderRoutes.route(t.getId(), r -> r
.path("/api/" + t.getCode() + "/**")
.filters(f -> f
.stripPrefix(2)
.addRequestHeader("X-Tenant", t.getId())
.addRequestHeader("X-Tier", t.getTier())
.retry(c -> c.setRetries(3))
)
.uri(t.getBackendUrl())
);
}
return builderRoutes.build();
}
}
لتحديث routes ديناميكيًا (بدون إعادة تشغيل)، نشر حدث RefreshRoutesEvent. يُتيح إضافة tenant جديد في الإنتاج دون إعادة إطلاق Gateway.
الخطوة 5 — مصادقة JWT مركزية
Gateway هو المكان الطبيعي للتحقق من tokens JWT مرة واحدة بدل في كل microservice. نستخدم spring-boot-starter-oauth2-resource-server.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://identity.example.com/realms/itsc
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
SecurityWebFilterChain chain(ServerHttpSecurity http) {
return http
.csrf(c -> c.disable())
.authorizeExchange(a -> a
.pathMatchers("/api/public/**").permitAll()
.pathMatchers("/actuator/health").permitAll()
.anyExchange().authenticated()
)
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.build();
}
}
@Component
public class PropagateAuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> (Jwt) ctx.getAuthentication().getPrincipal())
.map(jwt -> exchange.mutate().request(r -> r
.header("X-User-Id", jwt.getSubject())
.header("X-User-Roles", String.join(",", jwt.getClaimAsStringList("roles")))
).build())
.defaultIfEmpty(exchange)
.flatMap(chain::filter);
}
}
نمط النشر بـ headers يُتيح للـ microservices المخدومة معرفة هوية المستخدم دون إعادة التحقق من JWT بأنفسها. انضباط حرج: الـ microservices يجب أن ترفض أي استدعاء مباشر دون المرور بـ Gateway (firewall شبكي أو mTLS).
الخطوة 6 — Rate limiting بـ Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
data:
redis:
host: redis
port: 6379
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: api-publique
uri: http://service-public:8083
predicates:
- Path=/api/public/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
@Configuration
public class RateLimitConfig {
@Bean
public KeyResolver userKeyResolver() {
return exchange -> ReactiveSecurityContextHolder.getContext()
.map(ctx -> ((Jwt) ctx.getAuthentication().getPrincipal()).getSubject())
.defaultIfEmpty(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
الـ replenishRate (10/ث) يُحدّد المعدّل المتوسط المسموح، burstCapacity (20) الـ rafale المسموحة. فوق الحد، Gateway يُرجع 429 Too Many Requests.
الخطوة 7 — الملاحظة: metrics وtraces وlogs
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: health, prometheus, gateway
metrics:
distribution:
percentiles-histogram:
spring.cloud.gateway.requests: true
slo:
spring.cloud.gateway.requests: 100ms, 200ms, 500ms, 1s
tracing:
sampling:
probability: 0.1 # 10% sampling en prod
otel:
exporter:
otlp:
endpoint: http://otel-collector:4317
Metrics مفتاحية للرسم في Grafana: spring.cloud.gateway.requests بأبعاد routeId وstatus، resilience4j.circuitbreaker.state، jvm.memory.used.
الخطوة 8 — النشر والتوسّع
# Dockerfile multi-stage
FROM eclipse-temurin:25-jdk AS build
COPY . /app
WORKDIR /app
RUN ./mvnw clean package -DskipTests
FROM eclipse-temurin:25-jre
COPY --from=build /app/target/gateway-*.jar /app/gateway.jar
ENV JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseZGC"
EXPOSE 8080
ENTRYPOINT exec java $JAVA_OPTS -jar /app/gateway.jar
# Kubernetes : 3 replicas avec HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: gateway
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gateway
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
مع virtual threads مُفعَّلة أو النموذج reactive الأصلي، instance Gateway واحد يستطيع إدارة 5000-20000 req/ث على 2 vCPU.
أخطاء شائعة
| العَرَض | السبب | الحل |
|---|---|---|
| 503 Service Unavailable منهجيًا | URI هدف غير متاح | تحقّق من DNS، ping، المنفذ؛ logs Gateway |
| حلقة لا نهائية على route | Path الهدف يُطابق أيضًا الـ prédicat | أضف StripPrefix أو RewritePath |
| JWT صحيح لكن 401 على backend | Token غير مُنشَر بعد التحقق | تحقّق من filter يحذف header Authorization |
| Rate limit متجاهَل | Redis غير متاح أو KeyResolver null | تحقّق من اتصال Redis |
| latency p99 تنفجر | Pool Netty مُشبَع أو GC pauses | زِد reactor.netty.ioWorkerCount؛ فعّل ZGC |
| CORS محجوب | لا config CORS Gateway | أضف spring.cloud.gateway.globalcors |
الأسئلة الشائعة
Spring Cloud Gateway أم Nginx؟
Nginx للتوجيه الثابت البسيط وإنهاء TLS. Spring Cloud Gateway للتوجيه المهني الديناميكي مع auth/rate limiting/circuit breaker. غالبًا الاثنان: Nginx في الأمام، Gateway خلف.
Spring Cloud Gateway أم Kong؟
Kong gateway مخصصة غنية بـ plugins (Lua/Go). أكثر نضجًا للنشرات الكبيرة جدًا. Spring Cloud Gateway للفرق Java متجانسة حيث Gateway يُشارك كود مع microservices.
Reactive vs MVC Gateway في 2026؟
Reactive إذا أنت مرتاح مع WebFlux وتحتاج throughput أقصى. MVC + virtual threads إذا تريد بساطة المتزامن بأداء قابل للمقارنة.
كيف نُدير uploads (payloads كبيرة)؟
زِد spring.webflux.max-in-memory-size أو استخدم streaming مباشرة. للملفات الثقيلة (> 100 MB)، URLs موقّعة S3/R2 وbypass Gateway.
كم routes أقصى؟
عدة مئات بلا مشكلة على الأداء. ما بعد ذلك، انتقل لمنطق dispatch ديناميكي.