āļŠāļąāļĄāļ āļēāļĐāļāđ Spring Cloud Gateway: Routing, Filter āđāļĨāļ° Load Balancing
āđāļāļĩāđāļĒāļ§āļāļēāļ Spring Cloud Gateway āļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđāđāļāļāļāļīāļ: 12 āļāļģāļāļēāļĄāļāļĢāļāļāļāļĨāļļāļĄ routing, filter, load balancing āđāļĨāļ° pattern API Gateway āļāļĢāđāļāļĄāļāļąāļ§āļāļĒāđāļēāļāđāļāđāļ.

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
// āļāļēāļĢāļāļąāđāļāļāđāļēāļāļ·āđāļāļāļēāļāļāļāļ Spring Cloud Gateway
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
// āđāļĢāļīāđāļĄāļāđāļāđāļāļīāļĢāđāļāđāļ§āļāļĢāđ reactive Netty (āđāļĄāđāđāļāđ Tomcat)
SpringApplication.run(GatewayApplication.class, args);
}
}# 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 āļāļĢāļąāļāđāļāļĨāļĩāđāļĒāļāļāļģāļāļāđāļĨāļ°āļāļēāļĢāļāļāļāļāļĨāļąāļ
// āļāļēāļĢāļāļąāđāļāļāđāļē 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/(?<segment>.*)", "/products/${segment}")
// Filter: āđāļāļīāđāļĄ header
.addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
)
// URI āļāļĨāļēāļĒāļāļēāļ
.uri("http://localhost:8083"))
.build();
}
}āļĨāļģāļāļąāļāļāļēāļĢāļāļĢāļ°āļĄāļ§āļĨāļāļĨāđāļāđāļāđāļāļāļąāļāļāļĩāđ:
āļāļģāļāļāđāļāđāļē
â
âž
âââââââââââââââââââ
â Predicates â â āļāļĢāļ°āđāļĄāļīāļāđāļāļ·āđāļāļāđāļ (path, method, header...)
ââââââââââŽâââââââââ
â āļāļāļāļēāļĢāļāļąāļāļāļđāđ
âž
âââââââââââââââââââ
â Pre-Filters â â āļāļĢāļąāļāļāļģāļāļāļāđāļāļ routing
ââââââââââŽâââââââââ
â
âž
âââââââââââââââââââ
â HTTP Proxy â â āļŠāđāļāļāđāļāđāļāļĒāļąāļāđāļāļāļĢāđāļ§āļīāļŠāļāļĨāļēāļĒāļāļēāļ
ââââââââââŽâââââââââ
â
âž
âââââââââââââââââââ
â Post-Filters â â āļāļĢāļąāļāļāļēāļĢāļāļāļāļāļĨāļąāļāļāđāļāļāļŠāđāļāļāļ·āļāđāļāļĨāđāļāļāļāđ
ââââââââââŽâââââââââ
â
âž
āļāļāļāļāļĨāļąāļāđāļāļĒāļąāļāđāļāļĨāđāļāļāļāđāļāļģāļāļēāļĄāļāļĩāđ 3: predicate āļāļĩāđāđāļāđāļāđāļāļĒāļĄāļĩāļāļ°āđāļĢāļāđāļēāļ?
Spring Cloud Gateway āđāļŦāđ predicate āļŠāļģāđāļĢāđāļāļĢāļđāļāļāļģāļāļ§āļāļĄāļēāļāļŠāļģāļŦāļĢāļąāļāđāļāļ·āđāļāļāđāļ routing āļāļĩāđāļŦāļĨāļēāļāļŦāļĨāļēāļĒ āļāļēāļĢāļĢāļ§āļĄ predicate āļŦāļĨāļēāļĒāļāļąāļ§āļāđāļ§āļĒāļŠāļĢāđāļēāļāļāļ routing āļāļĩāđāļāļąāļāļāđāļāļ
# 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// āļāļēāļĢāļŠāļĢāđāļēāļ predicate āļāļģāļŦāļāļāđāļāļ
@Component
public class ApiKeyRoutePredicateFactory
extends AbstractRoutePredicateFactory<ApiKeyRoutePredicateFactory.Config> {
public ApiKeyRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> 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<String> validKeys = new ArrayList<>();
public List<String> getValidKeys() {
return validKeys;
}
public void setValidKeys(List<String> validKeys) {
this.validKeys = validKeys;
}
}
}āļĨāļģāļāļąāļāļāļāļ predicate āđāļĄāđāļŠāđāļāļāļĨāļāđāļāļāļēāļĢāļāļĢāļ°āđāļĄāļīāļ āđāļāđāļĨāļģāļāļąāļāļāļāļ route āļŠāļģāļāļąāļ Route āļāļ°āļāļđāļāļāļĢāļ°āđāļĄāļīāļāļāļēāļĄāļĨāļģāļāļąāļāđāļĨāļ°āļāļ°āđāļāđāļāļđāđāļāļĩāđāļāļĢāļāļāļąāļāļāļđāđāđāļĢāļ
Filter āđāļĨāļ°āļāļēāļĢāđāļāļĨāļāļāļģāļāļ
āļāļģāļāļēāļĄāļāļĩāđ 4: filter āļāđāļāļāđāļĨāļ°āļŦāļĨāļąāļāļāļēāļĢāļāļĢāļ°āļĄāļ§āļĨāļāļĨāļāļģāļāļēāļāļāļĒāđāļēāļāđāļĢ?
Filter GatewayFilter āļāļģāļāļēāļāđāļāļŦāđāļ§āļāđāļāđāļāļĩāđāļĄāļĩāļĨāļģāļāļąāļ filter "pre" āļāļ°āļāļĢāļąāļāļāļģāļāļāļāđāļāļ routing āļŠāđāļ§āļ filter "post" āļāļ°āļāļĢāļąāļāļāļēāļĢāļāļāļāļāļĨāļąāļāļŦāļĨāļąāļāļāļēāļāđāļāđāļĢāļąāļāļāļēāļāđāļāļāļĢāđāļ§āļīāļŠāļāļĨāļēāļĒāļāļēāļ
// Filter logging āđāļāļāđāļāļĨāļāļāļĨ
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> 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;
}
}// Filter āļĒāļ·āļāļĒāļąāļāļāļąāļ§āļāļ JWT
@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
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<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));
}
}āļāļģāļāļēāļĄāļāļĩāđ 5: filter āļŠāļģāđāļĢāđāļāļĢāļđāļāļāļĩāđāļĄāļĩāļāļĢāļ°āđāļĒāļāļāđāļāļĩāđāļŠāļļāļāļĄāļĩāļāļ°āđāļĢāļāđāļēāļ?
Spring Cloud Gateway āļĄāļĩ filter āļŠāļģāđāļĢāđāļāļĢāļđāļāļāļĢāļāļāļāļĨāļļāļĄāļāļĢāļāļĩāđāļāđāļāļēāļāļāļąāđāļ§āđāļ: āļāļēāļĢāđāļāļĩāļĒāļ URL āđāļŦāļĄāđ āļāļēāļĢāļāļĢāļąāļ header āļāļēāļĢ retry āđāļĨāļ° circuit breaker
# application.yml
# Filter āļŠāļģāđāļĢāđāļāļĢāļđāļāļāļĩāđāđāļāđāļāđāļāļĒ
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
# āļāļēāļĢāđāļāļĩāļĒāļ path āđāļŦāļĄāđ
- RewritePath=/api/orders/(?<segment>.*), /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}"// āļāļēāļĢāļāļąāđāļāļāđāļē 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()
);
}
}āļāļĢāđāļāļĄāļāļĩāđāļāļ°āļāļīāļāļīāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđ Spring Boot āđāļĨāđāļ§āļŦāļĢāļ·āļāļĒāļąāļāļāļĢāļąāļ?
āļāļķāļāļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāđāļāļāđāļāđāļāļāļ, flashcards āđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āļāļģāļāļēāļĄāļāļĩāđ 6: āļāļ°āļāļĢāļąāļ body āļāđāļ§āļĒ filter āđāļāđāļāļĒāđāļēāļāđāļĢ?
āļāļēāļĢāļāļĢāļąāļ body āļāļāļāļāļģāļāļāļŦāļĢāļ·āļāļāļēāļĢāļāļāļāļāļĨāļąāļāļāđāļāļāđāļāđāđāļāļ§āļāļēāļāđāļāļāļēāļ°āļāļąāļ ModifyRequestBodyGatewayFilterFactory āļŦāļĢāļ·āļ ModifyResponseBodyGatewayFilterFactory
// Filter āļāļĢāļąāļ body āļāļāļāļāļģāļāļ
@Component
@RequiredArgsConstructor
public class RequestBodyModificationFilter implements GlobalFilter, Ordered {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> 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<String, Object> body = objectMapper.readValue(
bytes,
new TypeReference<Map<String, Object>>() {}
);
// āđāļāļīāđāļĄāđāļĄāļāļēāļāļēāļāđāļē
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<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;
}
}// āļāļēāļĢāļāļąāđāļāļāđāļēāđāļāļ·āđāļāļāļĢāļąāļāļāļēāļĢāļāļāļāļāļĨāļąāļ
@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 āļāļąāļāđāļāļĄāļąāļāļī
# 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/// āļāļēāļĢāļāļąāđāļāļāđāļē load balancer āđāļāļāļāļģāļŦāļāļāđāļāļ
@Configuration
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
}
// CustomLoadBalancerConfig.java
// āļāļĨāļĒāļļāļāļāđ load balancing āđāļāļāļāļģāļŦāļāļāđāļāļ
public class CustomLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> 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);
}
}// 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();
}
// āļāļģāļāļ§āļāļāđāļģāļŦāļāļąāļāļāļēāļāđāļĄāļāļēāļāļēāļāđāļē
List<WeightedInstance> 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 āļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļāļąāļāļāļēāļĢāļāļ§āļēāļĄāļĨāđāļĄāđāļŦāļĨāļ§āļāļąāđāļāļŠāļđāļ
# 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// 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));
}
}// āļāļēāļĢāļāļīāļāļāļēāļĄāđāļŦāļāļļāļāļēāļĢāļāđāļāļāļ 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 āđāļĨāļ° HTTP client timeout āļāļĩāđāļāļēāļāđāļāļīāļāđāļāļāļ°āļāļĨāđāļāļ thread āļŠāđāļ§āļāļāļĩāđāļŠāļąāđāļāđāļāļīāļāđāļāļāļģāđāļŦāđāđāļāļīāļāļŠāļąāļāļāļēāļāđāļāļ·āļāļāļāļīāļāļāļĨāļēāļ
āļāļģāļāļēāļĄāļāļĩāđ 9: āļāļģ retry āļāđāļ§āļĒ exponential backoff āļāļĒāđāļēāļāđāļĢ?
Retry āđāļāļāļāļĨāļēāļāļāđāļ§āļĒ exponential backoff āļāđāļ§āļĒāđāļĄāđāđāļŦāđāđāļāļāļĢāđāļ§āļīāļŠāļāļĩāđāļāļģāļĨāļąāļāļĄāļĩāļāļąāļāļŦāļēāļāļđāļāļāļĨāđāļĄāđāļāļīāđāļĄ āļāļāļ°āđāļāļĩāļĒāļ§āļāļąāļāļāđāđāļāļīāđāļĄāđāļāļāļēāļŠāļŠāļģāđāļĢāđāļāđāļŦāđāļŠāļđāļāļŠāļļāļ
# 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// 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) {
// 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
// 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<DashboardResponse> getUserDashboard(@PathVariable Long userId) {
// āđāļĢāļĩāļĒāļāđāļāļāļāļāļēāļāđāļāļĒāļąāļāđāļāļāļĢāđāļ§āļīāļŠāļāđāļēāļāđ
Mono<UserProfile> userMono = fetchUserProfile(userId);
Mono<List<Order>> ordersMono = fetchRecentOrders(userId);
Mono<NotificationCount> 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<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());
}
}// DTO āļāļāļāļāļēāļĢāļāļāļāļāļĨāļąāļāļāļĩāđāļĢāļ§āļĄāđāļĨāđāļ§
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardResponse {
private UserProfile user;
private List<Order> recentOrders;
private NotificationCount notificationCount;
private Instant generatedAt;
private String error;
}āļāļģāļāļēāļĄāļāļĩāđ 11: āļāļģāđāļŦāđ gateway āļāļĨāļāļāļ āļąāļĒāļāđāļ§āļĒ OAuth2 āļāļĒāđāļēāļāđāļĢ?
āļāļēāļĢāļāļŠāļēāļ OAuth2 āļĢāļ§āļĄāļĻāļđāļāļĒāđāļāļēāļĢāļĒāļ·āļāļĒāļąāļāļāļąāļ§āļāļāļāļĩāđāļĢāļ°āļāļąāļ gateway āļŦāļĨāļĩāļāđāļĨāļĩāđāļĒāļāļāļēāļĢāļāļģāļāđāļģāļāļĢāļĢāļāļ°āđāļāđāļāđāļĨāļ°āđāļĄāđāļāļĢāđāļāļāļĢāđāļ§āļīāļŠ
# 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// āļāļēāļĢāļāļąāđāļāļāđāļēāļāļ§āļēāļĄāļāļĨāļāļāļ āļąāļĒāļāļāļ 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;
}
}// āļāļēāļĢāļŠāđāļāļāđāļ token āđāļāļĒāļąāļāđāļāļāļĢāđāļ§āļīāļŠāļāļĨāļēāļĒāļāđāļģ
@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 āđāļāļĒāļąāļāđāļāļāļĢāđāļ§āļīāļŠāļāļĨāļēāļĒāļāđāļģ
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;
}
}āļāļĢāđāļāļĄāļāļĩāđāļāļ°āļāļīāļāļīāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđ Spring Boot āđāļĨāđāļ§āļŦāļĢāļ·āļāļĒāļąāļāļāļĢāļąāļ?
āļāļķāļāļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāđāļāļāđāļāđāļāļāļ, flashcards āđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āļāļģāļāļēāļĄāļāļĩāđ 12: āđāļāļ§āļāļāļīāļāļąāļāļīāļāļĩāđāļāļĩāđāļāļāļēāļĢāļĄāļāļāļīāđāļāļāļĢāđāđāļĨāļ° observability āļĄāļĩāļāļ°āđāļĢāļāđāļēāļ?
āļāļēāļĢāļĄāļāļāļīāđāļāļāļĢāđ gateway āļŠāļģāļāļąāļāļāļĒāđāļēāļāļĒāļīāđāļāļāđāļāļāļēāļĢāļĢāļ°āļāļļāļāļąāļāļŦāļēāļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāđāļĨāļ°āļāļ§āļēāļĄāļāļĢāđāļāļĄāđāļāđāļāļēāļāđāļāļŠāļāļēāļāļąāļāļĒāļāļĢāļĢāļĄāđāļĄāđāļāļĢāđāļāļāļĢāđāļ§āļīāļŠ
# 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// Filter āđāļĄāļāļĢāļīāļāđāļāļāļāļģāļŦāļāļāđāļāļ
@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();
// āļāļķāļāđāļāļāļĢāđāļ§āļīāļŠāļāļĨāļēāļĒāļāļēāļāļāļēāļ 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;
}
}// āļāļēāļĢāļŠāđāļāļāđāļāļāļĢāļīāļāļ tracing
@Component
@RequiredArgsConstructor
public class TracingFilter implements GlobalFilter, Ordered {
private final Tracer tracer;
@Override
public Mono<Void> 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;
}
}āļāļēāļĢāļēāļāđāļĄāļāļĢāļīāļāļŠāļģāļāļąāļ:
| āđāļĄāļāļĢāļīāļ | āļāļģāļāļāļīāļāļēāļĒ |
|-----------------------------------|----------------------------------|
| gateway.request.duration | āļāļ§āļēāļĄāļŦāļāđāļ§āļāļāđāļ route/āļŠāļāļēāļāļ° |
| gateway.requests.total | āļāļąāļ§āļāļąāļāļāļģāļāļ |
| resilience4j.circuitbreaker.state | āļŠāļāļēāļāļ°āļāļāļ circuit breaker |
| http.server.requests | āđāļĄāļāļĢāļīāļ HTTP āļĄāļēāļāļĢāļāļēāļ |
| spring.cloud.gateway.routes | route āļāļĩāđāļāļģāļĨāļąāļāđāļāđāļāļēāļ |āđāļāđ 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 āļāļĩāđāđāļāđāļāđāļāļĢāđāļāđāļĨāļ°āļāļĢāļ°āļŠāļīāļāļāļīāļ āļēāļāļŠāļđāļ
āđāļĢāļīāđāļĄāļāļķāļāļāđāļāļĄāđāļĨāļĒ!
āļāļāļŠāļāļāļāļ§āļēāļĄāļĢāļđāđāļāļāļāļāļļāļāļāđāļ§āļĒāļāļąāļ§āļāļģāļĨāļāļāļŠāļąāļĄāļ āļēāļĐāļāđāđāļĨāļ°āđāļāļāļāļāļŠāļāļāđāļāļāļāļīāļāļāļĢāļąāļ
āđāļāđāļ
āđāļāļĢāđ
āļāļāļāļ§āļēāļĄāļāļĩāđāđāļāļĩāđāļĒāļ§āļāđāļāļ

Spring Kafka: āļŠāļāļēāļāļąāļāļĒāļāļĢāļĢāļĄ event-driven āļāļĢāđāļāļĄ consumer āļāļĩāđāļāļāļāļēāļ
āļāļđāđāļĄāļ·āļ Spring Kafka āđāļāļāļāļĢāļāļāđāļ§āļāļŠāļģāļŦāļĢāļąāļāļŠāļāļēāļāļąāļāļĒāļāļĢāļĢāļĄ event-driven āļāļēāļĢāļāļąāđāļāļāđāļē consumer āļāļĩāđāļāļāļāļēāļ āļāđāļĒāļāļēāļĒ retry dead letter queue āđāļĨāļ°āļĢāļđāļāđāļāļāđāļāļĢāļāļąāļāļāļąāļāļŠāļģāļŦāļĢāļąāļāđāļāļāļāļĨāļīāđāļāļāļąāļāđāļāļāļāļĢāļ°āļāļēāļĒ

Spring Boot Logging āđāļāļāļĩ 2026: āļĨāđāļāļāđāļāļāļĄāļĩāđāļāļĢāļāļŠāļĢāđāļēāļāļŠāļģāļŦāļĢāļąāļāđāļāļĢāļāļąāļāļāļąāļāļāđāļ§āļĒ Logback āđāļĨāļ° JSON
āļāļđāđāļĄāļ·āļāļāļāļąāļāļŠāļĄāļāļđāļĢāļāđāļŠāļģāļŦāļĢāļąāļ structured logging āđāļ Spring Boot āļāļēāļĢāļāļąāđāļāļāđāļē Logback JSON, MDC āļŠāļģāļŦāļĢāļąāļ tracing āđāļāļ§āļāļāļīāļāļąāļāļīāļāļĩāđāļāļĩāļāļĩāđāļŠāļļāļāđāļāđāļāļĢāļāļąāļāļāļąāļ āđāļĨāļ°āļāļēāļĢāļĢāļ§āļĄāļāļąāļ ELK Stack

āļŠāļąāļĄāļ āļēāļĐāļāđ Spring GraphQL: Resolver, DataLoader āđāļĨāļ°āļ§āļīāļāļĩāđāļāđāļāļąāļāļŦāļē N+1
āđāļāļĢāļĩāļĒāļĄāļāļąāļ§āļŠāļģāļŦāļĢāļąāļāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļāđ Spring GraphQL āļāđāļ§āļĒāļāļđāđāļĄāļ·āļāļāļĩāđāļāļĢāļāļāđāļ§āļāļāļĩāđ Resolver, DataLoader, āļāļēāļĢāļāļąāļāļāļēāļĢāļāļąāļāļŦāļē N+1, mutation āđāļĨāļ°āđāļāļ§āļāļāļīāļāļąāļāļīāļāļĩāđāļāļĩāļāļĩāđāļŠāļļāļāļŠāļģāļŦāļĢāļąāļāļāļģāļāļēāļĄāļāļēāļāđāļāļāļāļīāļ