In the world of microservices architecture, API Gateways have become essential components for handling cross-cutting concerns like routing, security, monitoring, and resilience. Spring Cloud Gateway builds upon Project Reactor to provide a powerful, non-blocking way to route requests to appropriate services. At the heart of this capability lie two fundamental concepts: Route Predicates and Filters.
This article explores how predicates and filters work together in Spring Cloud Gateway to create sophisticated routing rules and request transformations.
Spring Cloud Gateway Architecture
Before diving into predicates and filters, let's understand the basic architecture:
HTTP Request → Gateway Handler → Route Predicate Matching → Filter Chain → Backend Service ↑ Configuration: - Predicates (WHEN) - Filters (WHAT/WHERE)
- Routes: The basic building block containing an ID, a destination URI, a collection of predicates, and a collection of filters
- Predicates: Determine WHEN a route should be matched
- Filters: Define WHAT transformations to apply to requests and responses
Route Predicates: The "When" of Routing
Predicates are conditions that must be true for a route to be matched. Think of them as the "if" conditions for your routing rules.
Common Predicate Factories:
Spring Cloud Gateway provides numerous built-in predicate factories:
- Path Route Predicate: Matches based on request path
- Method Route Predicate: Matches based on HTTP method
- Header Route Predicate: Matches based on request headers
- Host Route Predicate: Matches based on the Host header
- Query Route Predicate: Matches based on query parameters
- Cookie Route Predicate: Matches based on cookies
- RemoteAddr Route Predicate: Matches based on client IP address
- DateTime Route Predicate: Matches based on time windows
- Weight Route Predicate: For A/B testing and canary deployments
Configuring Predicates
Method 1: Application.yml Configuration
spring:
cloud:
gateway:
routes:
# Route 1: Simple path-based routing
- id: user_service_route
uri: http://localhost:8081
predicates:
- Path=/api/users/**
- Method=GET,POST
filters:
- StripPrefix=1
# Route 2: Header-based routing
- id: premium_service_route
uri: http://localhost:8082
predicates:
- Path=/api/**
- Header=X-User-Type, premium
filters:
- StripPrefix=1
# Route 3: Host-based routing with rewrite
- id: host_based_route
uri: http://localhost:8083
predicates:
- Host=**.example.com
filters:
- RewritePath=/api/v1/(?<segment>.*), /$\{segment}
# Route 4: Query parameter routing
- id: query_param_route
uri: http://localhost:8084
predicates:
- Path=/search
- Query=type,advanced
filters:
- AddRequestParameter=source,gateway
# Route 5: Cookie-based routing
- id: cookie_based_route
uri: http://localhost:8085
predicates:
- Path=/premium/**
- Cookie=premium_user, true
# Route 6: Time-based routing
- id: business_hours_route
uri: http://localhost:8086
predicates:
- Path=/support/**
- Between=2024-01-01T09:00:00.000-05:00, 2024-12-31T17:00:00.000-05:00
# Route 7: Weight-based routing (A/B testing)
- id: weight_high
uri: http://localhost:8087
predicates:
- Path=/feature/**
- Weight=group-a, 80
filters:
- AddRequestHeader=X-Feature-Version, v2
- id: weight_low
uri: http://localhost:8088
predicates:
- Path=/feature/**
- Weight=group-a, 20
filters:
- AddRequestHeader=X-Feature-Version, v1
Method 2: Java DSL Configuration
@Configuration
public class GatewayConfiguration {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// Path and method based routing
.route("user_service", r -> r
.path("/api/users/**")
.and()
.method("GET", "POST")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:8081"))
// Header based routing
.route("premium_service", r -> r
.path("/api/**")
.and()
.header("X-User-Type", "premium")
.filters(f -> f.stripPrefix(1))
.uri("http://localhost:8082"))
// Host based routing with rewrite
.route("host_route", r -> r
.host("*.example.com")
.filters(f -> f.rewritePath("/api/v1/(?<segment>.*)", "/${segment}"))
.uri("http://localhost:8083"))
// Query parameter routing
.route("query_route", r -> r
.path("/search")
.and()
.query("type", "advanced")
.filters(f -> f.addRequestParameter("source", "gateway"))
.uri("http://localhost:8084"))
// Complex predicate combination
.route("complex_route", r -> r
.path("/api/**")
.and()
.method("POST", "PUT")
.and()
.header("X-API-Version", "v2")
.and()
.cookie("session_id", ".*")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway-Route", "complex"))
.uri("http://localhost:8089"))
.build();
}
}
Gateway Filters: The "What" of Request Processing
Filters modify requests and responses as they pass through the gateway. They can perform various transformations, add headers, modify bodies, and more.
Filter Categories:
- Pre Filters: Execute before forwarding the request to the backend
- Post Filters: Execute after receiving the response from the backend
- Route Filters: Can affect the request routing itself
Common Gateway Filters
1. Request Modification Filters
spring: cloud: gateway: routes: - id: request_modification_route uri: http://localhost:8090 predicates: - Path=/api/** filters: - AddRequestHeader=X-Gateway-Request-ID, 12345 - AddRequestParameter=source, gateway - AddRequestHeader=X-Forwarded-For, 192.168.1.1 - RemoveRequestHeader=Authorization # Remove sensitive headers - SetRequestHeader=Content-Type, application/json - SetRequestHeader=X-API-Version, v2 - StripPrefix=2 # Remove first 2 path segments
2. Path Rewriting Filters
- id: path_rewrite_route
uri: http://localhost:8091
predicates:
- Path=/old-api/**
filters:
- RewritePath=/old-api/(?<segment>.*), /new-api/$\{segment}
# Converts /old-api/users to /new-api/users
- id: complex_rewrite_route
uri: http://localhost:8092
predicates:
- Path=/v1/api/**
filters:
- RewritePath=/v1/api/users/(?<id>.*), /api/v2/users/$\{id}/profile
# Converts /v1/api/users/123 to /api/v2/users/123/profile
3. Response Modification Filters
- id: response_modification_route uri: http://localhost:8093 predicates: - Path=/api/public/** filters: - AddResponseHeader=X-Processed-By, api-gateway - SetResponseHeader=Content-Type, application/json - RemoveResponseHeader=Server # Hide backend server info - DedupeResponseHeader=Access-Control-Allow-Origin
4. Circuit Breaker and Retry Filters
- id: resilient_route uri: http://localhost:8094 predicates: - Path=/api/orders/** filters: - name: CircuitBreaker args: name: orderService fallbackUri: forward:/fallback/order - name: Retry args: retries: 3 series: SERVER_ERROR methods: GET,POST backoff: firstBackoff: 50ms maxBackoff: 500ms factor: 2 basedOnPreviousValue: false
5. Rate Limiting Filter
- id: rate_limited_route uri: http://localhost:8095 predicates: - Path=/api/limited/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1 - AddRequestHeader=X-RateLimit-Bypass, false
Custom Filters Implementation
You can also create custom filters for specific business logic.
Custom Global Filter:
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// Check for API key in header
String apiKey = request.getHeaders().getFirst("X-API-Key");
if (!isValidApiKey(apiKey)) {
// Reject request with 401 Unauthorized
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// Add custom header for downstream services
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-Authenticated-User", "user-from-api-key")
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
}
private boolean isValidApiKey(String apiKey) {
return apiKey != null && apiKey.startsWith("key_");
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
Custom Gateway Filter Factory:
@Component
public class LoggingGatewayFilterFactory extends
AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {
public LoggingGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// Pre-processing logging
if (config.isEnabled()) {
System.out.println("Incoming request: " +
request.getMethod() + " " +
request.getURI() +
" - Headers: " + request.getHeaders());
}
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
// Post-processing logging
if (config.isEnabled()) {
ServerHttpResponse response = exchange.getResponse();
System.out.println("Outgoing response: " +
response.getStatusCode() +
" - Headers: " + response.getHeaders());
}
}));
};
}
public static class Config {
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("enabled");
}
}
Using Custom Filter in Configuration:
spring: cloud: gateway: routes: - id: custom_filter_route uri: http://localhost:8096 predicates: - Path=/api/logged/** filters: - Logging=true - StripPrefix=1
Advanced Routing Scenarios
Scenario 1: Version-Based Routing
spring:
cloud:
gateway:
routes:
- id: api_v1_route
uri: http://localhost:8101
predicates:
- Path=/api/v1/**
filters:
- StripPrefix=2
- id: api_v2_route
uri: http://localhost:8102
predicates:
- Path=/api/v2/**
filters:
- StripPrefix=2
- id: api_latest_route
uri: http://localhost:8103
predicates:
- Path=/api/**
- Header=X-API-Version, latest
filters:
- StripPrefix=1
- SetPath=/api/v3/{segment}
Scenario 2: Tenant-Based Routing
@Bean
public RouteLocator tenantBasedRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("tenant_a_route", r -> r
.header("X-Tenant-ID", "tenant-a")
.filters(f -> f
.addRequestHeader("X-Tenant-Name", "Company A")
.rewritePath("/(?<segment>.*)", "/tenant-a/${segment}"))
.uri("http://tenant-a-service:8080"))
.route("tenant_b_route", r -> r
.header("X-Tenant-ID", "tenant-b")
.filters(f -> f
.addRequestHeader("X-Tenant-Name", "Company B")
.rewritePath("/(?<segment>.*)", "/tenant-b/${segment}"))
.uri("http://tenant-b-service:8080"))
.route("default_tenant_route", r -> r
.alwaysTrue() // Catch-all predicate
.filters(f -> f
.addRequestHeader("X-Tenant-Name", "Default")
.rewritePath("/(?<segment>.*)", "/default/${segment}"))
.uri("http://default-service:8080"))
.build();
}
Best Practices
- Order Matters: Routes are evaluated in order, so put specific routes before generic ones
- Use Meaningful IDs: Route IDs should clearly indicate the route's purpose
- Monitor Performance: Use Actuator endpoints to monitor gateway performance
- Test Thoroughly: Always test routing rules with various scenarios
- Security First: Remove sensitive headers and validate inputs
- Circuit Breaking: Implement resilience patterns for dependent services
- Rate Limiting: Protect backend services from excessive traffic
Conclusion
Spring Cloud Gateway's predicates and filters provide a powerful, flexible mechanism for building sophisticated API gateways. By mastering these components, you can:
- Implement Complex Routing: Based on path, headers, methods, and other request attributes
- Transform Requests/Responses: Modify headers, rewrite paths, and manipulate content
- Enhance Security: Add authentication, remove sensitive information
- Improve Resilience: Implement circuit breakers, retries, and rate limiting
- Enable Advanced Patterns: Support for A/B testing, canary releases, and multi-tenancy
The combination of declarative configuration and programmatic customization makes Spring Cloud Gateway an excellent choice for modern microservices architectures, providing the routing intelligence needed to build robust, scalable systems.